subx_cli/commands/
sync_command.rs

1use crate::Result;
2use crate::cli::SyncArgs;
3use crate::config::load_config;
4use crate::core::formats::Subtitle;
5use crate::core::formats::manager::FormatManager;
6use crate::core::matcher::{FileDiscovery, MediaFileType};
7use crate::core::sync::dialogue::DialogueDetector;
8use crate::core::sync::{SyncConfig, SyncEngine, SyncResult};
9use crate::error::SubXError;
10use std::path::{Path, PathBuf};
11
12/// 執行 Sync 命令
13pub async fn execute(args: SyncArgs) -> Result<()> {
14    let app_config = load_config()?;
15    let config = SyncConfig {
16        max_offset_seconds: args.range.unwrap_or(app_config.sync.max_offset_seconds),
17        correlation_threshold: args
18            .threshold
19            .unwrap_or(app_config.sync.correlation_threshold),
20        dialogue_threshold: app_config.sync.dialogue_detection_threshold,
21        min_dialogue_length: app_config.sync.min_dialogue_duration_ms as f32 / 1000.0,
22    };
23    let sync_engine = SyncEngine::new(config);
24    // 若啟用對話檢測,先行偵測語音片段與比例
25    if app_config.sync.enable_dialogue_detection {
26        let detector = DialogueDetector::new()?;
27        let segs = detector.detect_dialogue(&args.video).await?;
28        println!("檢測到 {} 個對話片段", segs.len());
29        println!("語音比例: {:.1}%", detector.get_speech_ratio(&segs) * 100.0);
30    }
31
32    if let Some(manual_offset) = args.offset {
33        let mut subtitle = load_subtitle(&args.subtitle).await?;
34        sync_engine.apply_sync_offset(&mut subtitle, manual_offset as f32)?;
35        save_subtitle(&subtitle, &args.subtitle).await?;
36        println!("✓ 已應用手動偏移: {}秒", manual_offset);
37    } else if args.batch {
38        let media_pairs = discover_media_pairs(&args.video).await?;
39        for (video_file, subtitle_file) in media_pairs {
40            match sync_single_pair(&sync_engine, &video_file, &subtitle_file).await {
41                Ok(result) => {
42                    println!(
43                        "✓ {} - 偏移: {:.2}秒 (信心度: {:.2})",
44                        subtitle_file.display(),
45                        result.offset_seconds,
46                        result.confidence
47                    );
48                }
49                Err(e) => {
50                    println!("✗ {} - 錯誤: {}", subtitle_file.display(), e);
51                }
52            }
53        }
54    } else {
55        let subtitle = load_subtitle(&args.subtitle).await?;
56        let result = sync_engine.sync_subtitle(&args.video, &subtitle).await?;
57        if result.confidence > 0.5 {
58            let mut updated = subtitle;
59            sync_engine.apply_sync_offset(&mut updated, result.offset_seconds)?;
60            save_subtitle(&updated, &args.subtitle).await?;
61            println!(
62                "✓ 同步完成 - 偏移: {:.2}秒 (信心度: {:.2})",
63                result.offset_seconds, result.confidence
64            );
65        } else {
66            println!("⚠ 同步信心度較低 ({:.2}),建議手動調整", result.confidence);
67        }
68    }
69    Ok(())
70}
71
72/// 載入字幕檔案並解析
73async fn load_subtitle(path: &Path) -> Result<Subtitle> {
74    let content = tokio::fs::read_to_string(path).await?;
75    let mgr = FormatManager::new();
76    let mut subtitle = mgr.parse_auto(&content)?;
77    // 設定來源編碼
78    subtitle.metadata.encoding = "utf-8".to_string();
79    Ok(subtitle)
80}
81
82/// 序列化並儲存字幕檔案
83async fn save_subtitle(subtitle: &Subtitle, path: &Path) -> Result<()> {
84    let mgr = FormatManager::new();
85    let text = mgr
86        .get_format_by_extension(
87            path.extension()
88                .and_then(|e| e.to_str())
89                .unwrap_or_default(),
90        )
91        .ok_or_else(|| SubXError::subtitle_format("Unknown", "未知的字幕格式"))?
92        .serialize(subtitle)?;
93    tokio::fs::write(path, text).await?;
94    Ok(())
95}
96
97/// 掃描資料夾並配對影片與字幕檔案
98async fn discover_media_pairs(dir: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
99    let discovery = FileDiscovery::new();
100    let files = discovery.scan_directory(dir, true)?;
101    let videos: Vec<_> = files
102        .iter()
103        .filter(|f| matches!(f.file_type, MediaFileType::Video))
104        .cloned()
105        .collect();
106    let subs: Vec<_> = files
107        .iter()
108        .filter(|f| matches!(f.file_type, MediaFileType::Subtitle))
109        .cloned()
110        .collect();
111    let mut pairs = Vec::new();
112    for video in videos {
113        if let Some(s) = subs.iter().find(|s| s.name == video.name) {
114            pairs.push((video.path.clone(), s.path.clone()));
115        }
116    }
117    Ok(pairs)
118}
119
120/// 同步單一媒體檔案
121async fn sync_single_pair(
122    engine: &SyncEngine,
123    video: &Path,
124    subtitle_path: &Path,
125) -> Result<SyncResult> {
126    let mut subtitle = load_subtitle(subtitle_path).await?;
127    let result = engine.sync_subtitle(video, &subtitle).await?;
128    engine.apply_sync_offset(&mut subtitle, result.offset_seconds)?;
129    save_subtitle(&subtitle, subtitle_path).await?;
130    Ok(result)
131}