subx_cli/commands/
sync_command.rs1use 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
12pub 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 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
72async 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 subtitle.metadata.encoding = "utf-8".to_string();
79 Ok(subtitle)
80}
81
82async 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
97async 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
120async 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}