subx_cli/commands/sync_command.rs
1//! Advanced subtitle synchronization command implementation.
2//!
3//! This module provides sophisticated subtitle timing alignment capabilities,
4//! using advanced audio analysis techniques to automatically detect optimal
5//! subtitle timing or apply manual adjustments. It supports both automatic
6//! synchronization through dialogue detection and manual offset application.
7//!
8//! # Synchronization Methods
9//!
10//! ## Automatic Synchronization
11//! Uses cutting-edge audio analysis to achieve precise timing alignment:
12//!
13//! ### Audio Analysis Pipeline
14//! 1. **Audio Extraction**: Extract audio track from video file
15//! 2. **Speech Detection**: Identify speech segments using voice activity detection
16//! 3. **Dialogue Recognition**: Classify speech vs. non-speech audio content
17//! 4. **Pattern Matching**: Correlate speech timing with subtitle timing
18//! 5. **Offset Calculation**: Determine optimal time shift for best alignment
19//! 6. **Quality Assessment**: Evaluate synchronization confidence and accuracy
20//!
21//! ### Advanced Features
22//! - **Multi-language Support**: Handle various spoken languages
23//! - **Background Noise Filtering**: Robust operation in noisy environments
24//! - **Music Separation**: Distinguish speech from background music
25//! - **Confidence Scoring**: Quantify synchronization quality
26//!
27//! ## Manual Synchronization
28//! Provides precise control for specific timing adjustments:
29//!
30//! - **Fixed Offset**: Apply uniform time shift to all subtitles
31//! - **Fractional Precision**: Support for millisecond-level adjustments
32//! - **Positive/Negative Shifts**: Advance or delay subtitle timing
33//! - **Preservation**: Maintain relative timing between subtitle entries
34//!
35//! # Audio Processing Features
36//!
37//! ## Dialogue Detection
38//! - **Voice Activity Detection (VAD)**: Identify speech segments
39//! - **Speaker Separation**: Handle multiple speakers
40//! - **Language Adaptation**: Optimize for different languages
41//! - **Noise Robustness**: Function in challenging audio environments
42//!
43//! ## Quality Analysis
44//! - **Speech Ratio**: Percentage of audio containing speech
45//! - **Confidence Metrics**: Reliability indicators for sync quality
46//! - **Timing Validation**: Verify subtitle timing consistency
47//! - **Content Alignment**: Ensure subtitles match spoken content
48//!
49//! # Configuration Integration
50//!
51//! The synchronization system respects comprehensive configuration:
52//! ```toml
53//! [sync]
54//! max_offset_seconds = 30.0 # Maximum search range
55//! correlation_threshold = 0.8 # Minimum correlation for acceptance
56//! dialogue_detection_threshold = 0.6 # Speech detection sensitivity
57//! min_dialogue_duration_ms = 500 # Minimum speech segment length
58//! enable_dialogue_detection = true # Enable advanced audio analysis
59//! ```
60//!
61//! # Performance Optimization
62//!
63//! - **Efficient Audio Processing**: Optimized algorithms for speed
64//! - **Memory Management**: Streaming processing for large files
65//! - **Parallel Processing**: Multi-threaded analysis where possible
66//! - **Caching**: Results cached for repeated operations
67//!
68//! # Examples
69//!
70//! ```rust,ignore
71//! use subx_cli::cli::SyncArgs;
72//! use subx_cli::commands::sync_command;
73//! use std::path::PathBuf;
74//!
75//! // Automatic synchronization
76//! let auto_sync = SyncArgs {
77//! video: PathBuf::from("movie.mp4"),
78//! subtitle: PathBuf::from("subtitle.srt"),
79//! offset: None,
80//! batch: false,
81//! range: Some(20.0),
82//! threshold: Some(0.85),
83//! };
84//! sync_command::execute(auto_sync).await?;
85//!
86//! // Manual offset adjustment
87//! let manual_sync = SyncArgs {
88//! video: PathBuf::from("episode.mkv"),
89//! subtitle: PathBuf::from("episode.srt"),
90//! offset: Some(2.5), // Delay by 2.5 seconds
91//! batch: false,
92//! range: None,
93//! threshold: None,
94//! };
95//! sync_command::execute(manual_sync).await?;
96//! ```
97
98use crate::Result;
99use crate::cli::SyncArgs;
100use crate::config::ConfigService;
101use crate::core::formats::Subtitle;
102use crate::core::formats::manager::FormatManager;
103use crate::core::matcher::{FileDiscovery, MediaFileType};
104use crate::core::sync::dialogue::DialogueDetector;
105use crate::core::sync::{SyncConfig, SyncEngine, SyncResult};
106use crate::error::SubXError;
107use std::path::{Path, PathBuf};
108
109/// Execute advanced subtitle synchronization with audio analysis or manual adjustment.
110///
111/// This function orchestrates the complete synchronization workflow, supporting
112/// both automatic audio-based timing correction and manual offset application.
113/// It includes comprehensive audio analysis, dialogue detection, and timing
114/// validation to ensure optimal subtitle-audio alignment.
115///
116/// # Synchronization Workflow
117///
118/// ## Automatic Mode (no offset specified)
119/// 1. **Configuration Setup**: Load sync parameters and thresholds
120/// 2. **Audio Analysis**: Extract and analyze audio from video file
121/// 3. **Dialogue Detection**: Identify speech segments and timing patterns
122/// 4. **Pattern Correlation**: Match speech timing with subtitle timing
123/// 5. **Offset Optimization**: Find optimal time shift for best alignment
124/// 6. **Quality Validation**: Assess synchronization confidence and accuracy
125/// 7. **Application**: Apply calculated offset to subtitle file
126///
127/// ## Manual Mode (offset specified)
128/// 1. **Configuration Loading**: Load basic sync settings
129/// 2. **Subtitle Loading**: Parse and validate subtitle file
130/// 3. **Offset Application**: Apply specified time shift uniformly
131/// 4. **Validation**: Verify timing consistency after adjustment
132/// 5. **Output**: Save synchronized subtitle file
133///
134/// # Audio Analysis Features
135///
136/// When dialogue detection is enabled, the system provides:
137/// - **Speech Segment Detection**: Identify when characters are speaking
138/// - **Speech Ratio Analysis**: Calculate percentage of audio containing speech
139/// - **Quality Metrics**: Assess suitability for automatic synchronization
140/// - **Confidence Scoring**: Quantify reliability of detected patterns
141///
142/// # Configuration Parameters
143///
144/// The function uses configuration settings to optimize performance:
145/// - **max_offset_seconds**: Maximum search range for automatic sync
146/// - **correlation_threshold**: Minimum correlation required for acceptance
147/// - **dialogue_detection_threshold**: Sensitivity for speech detection
148/// - **min_dialogue_duration_ms**: Minimum length of valid speech segments
149///
150/// # Arguments
151///
152/// * `args` - Synchronization arguments containing:
153/// - `video`: Video file path for audio analysis
154/// - `subtitle`: Subtitle file path to be synchronized
155/// - `offset`: Optional manual offset in seconds (overrides auto-detection)
156/// - `batch`: Enable batch processing mode
157/// - `range`: Override maximum offset search range
158/// - `threshold`: Override correlation threshold
159///
160/// # Returns
161///
162/// Returns `Ok(())` on successful synchronization, or an error describing:
163/// - Configuration loading failures
164/// - Video file access or audio extraction problems
165/// - Subtitle file parsing or validation issues
166/// - Synchronization processing errors
167/// - Output file creation problems
168///
169/// # Error Handling
170///
171/// Comprehensive error handling addresses:
172/// - **Input Validation**: File existence, format support, accessibility
173/// - **Audio Processing**: Codec support, extraction failures, analysis errors
174/// - **Synchronization**: Pattern matching failures, correlation issues
175/// - **Output Generation**: File writing, format validation, backup creation
176///
177/// # Quality Assurance
178///
179/// The synchronization process includes multiple quality checks:
180/// - **Input Validation**: Verify video and subtitle file integrity
181/// - **Audio Quality**: Assess audio suitability for analysis
182/// - **Sync Confidence**: Evaluate reliability of calculated offsets
183/// - **Output Verification**: Validate synchronized subtitle timing
184///
185/// # Examples
186///
187/// ```rust,ignore
188/// use subx_cli::cli::SyncArgs;
189/// use subx_cli::commands::sync_command;
190/// use std::path::PathBuf;
191///
192/// // High-precision automatic sync
193/// let precise_sync = SyncArgs {
194/// video: PathBuf::from("documentary.mp4"),
195/// subtitle: PathBuf::from("documentary.srt"),
196/// offset: None,
197/// batch: false,
198/// range: Some(10.0), // Narrow search range
199/// threshold: Some(0.9), // High confidence required
200/// };
201/// sync_command::execute(precise_sync).await?;
202///
203/// // Permissive automatic sync for challenging content
204/// let permissive_sync = SyncArgs {
205/// video: PathBuf::from("action_movie.mkv"),
206/// subtitle: PathBuf::from("action_movie.srt"),
207/// offset: None,
208/// batch: false,
209/// range: Some(45.0), // Wide search range
210/// threshold: Some(0.7), // Lower confidence threshold
211/// };
212/// sync_command::execute(permissive_sync).await?;
213///
214/// // Fine manual adjustment
215/// let fine_tune = SyncArgs {
216/// video: PathBuf::from("episode.mp4"),
217/// subtitle: PathBuf::from("episode.srt"),
218/// offset: Some(0.75), // 750ms delay
219/// batch: false,
220/// range: None,
221/// threshold: None,
222/// };
223/// sync_command::execute(fine_tune).await?;
224/// ```
225///
226/// # Performance Notes
227///
228/// - **Audio Processing**: CPU-intensive, may take time for long videos
229/// - **Memory Usage**: Proportional to video length and audio quality
230/// - **Disk I/O**: Temporary files created during audio extraction
231/// - **Optimization**: Results cached for repeated operations on same files
232///
233/// Execute advanced subtitle synchronization with dependency injection.
234///
235/// This function orchestrates the complete synchronization workflow using
236/// dependency injection for configuration management, supporting both automatic
237/// audio-based timing correction and manual offset application.
238///
239/// # Arguments
240///
241/// * `args` - Synchronization arguments including video/subtitle paths and settings
242/// * `config_service` - Configuration service providing access to sync settings
243///
244/// # Returns
245///
246/// Returns `Ok(())` on successful completion, or an error if synchronization fails.
247///
248/// # Examples
249///
250/// ```rust,ignore
251/// use subx_cli::commands::sync_command;
252/// use subx_cli::cli::SyncArgs;
253/// use subx_cli::config::ProductionConfigService;
254/// use std::path::PathBuf;
255/// use std::sync::Arc;
256///
257/// # async fn example() -> subx_cli::Result<()> {
258/// let config_service = Arc::new(ProductionConfigService::new()?);
259/// let args = SyncArgs {
260/// video: PathBuf::from("movie.mp4"),
261/// subtitle: PathBuf::from("movie.srt"),
262/// offset: None,
263/// batch: false,
264/// range: Some(15.0),
265/// threshold: Some(0.8),
266/// };
267///
268/// sync_command::execute(&args, config_service.as_ref()).await?;
269/// # Ok(())
270/// # }
271/// ```
272pub async fn execute(args: &SyncArgs, config_service: &dyn ConfigService) -> Result<()> {
273 // Load application configuration for synchronization parameters from injected service
274 let app_config = config_service.get_config()?;
275
276 // Configure synchronization engine with user overrides and defaults
277 let config = SyncConfig {
278 max_offset_seconds: args.range.unwrap_or(app_config.sync.max_offset_seconds),
279 correlation_threshold: args
280 .threshold
281 .unwrap_or(app_config.sync.correlation_threshold),
282 dialogue_threshold: app_config.sync.dialogue_detection_threshold,
283 min_dialogue_length: app_config.sync.min_dialogue_duration_ms as f32 / 1000.0,
284 };
285 let sync_engine = SyncEngine::new(config);
286
287 // Delegate to the shared synchronization logic
288 execute_sync_logic(args, app_config, sync_engine).await
289}
290
291/// Execute audio-subtitle synchronization with injected configuration service.
292///
293/// This function provides the new dependency injection interface for the sync command,
294/// accepting a configuration service instead of loading configuration globally.
295///
296/// # Arguments
297///
298/// * `args` - Synchronization arguments including video/subtitle paths and thresholds
299/// * `config_service` - Configuration service providing access to sync settings
300///
301/// # Returns
302///
303/// Returns `Ok(())` on successful completion, or an error if synchronization fails.
304pub async fn execute_with_config(
305 args: SyncArgs,
306 config_service: std::sync::Arc<dyn ConfigService>,
307) -> Result<()> {
308 // Load application configuration for synchronization parameters from injected service
309 let app_config = config_service.get_config()?;
310
311 // Configure synchronization engine with user overrides and defaults
312 let config = SyncConfig {
313 max_offset_seconds: args.range.unwrap_or(app_config.sync.max_offset_seconds),
314 correlation_threshold: args
315 .threshold
316 .unwrap_or(app_config.sync.correlation_threshold),
317 dialogue_threshold: app_config.sync.dialogue_detection_threshold,
318 min_dialogue_length: app_config.sync.min_dialogue_duration_ms as f32 / 1000.0,
319 };
320 let sync_engine = SyncEngine::new(config);
321
322 // Delegate to the shared synchronization logic
323 execute_sync_logic(&args, app_config, sync_engine).await
324}
325
326/// Internal function containing the core synchronization logic.
327///
328/// This function contains the shared sync logic that can be used by both
329/// the legacy execute() function and the new execute() function.
330async fn execute_sync_logic(
331 args: &SyncArgs,
332 app_config: crate::config::Config,
333 sync_engine: SyncEngine,
334) -> Result<()> {
335 // Perform advanced dialogue detection if enabled in configuration
336 if app_config.sync.enable_dialogue_detection {
337 let detector = DialogueDetector::new(&app_config.sync);
338 let segs = detector.detect_dialogue(&args.video).await?;
339 println!("Detected {} dialogue segments", segs.len());
340 println!(
341 "Speech ratio: {:.1}%",
342 detector.get_speech_ratio(&segs) * 100.0
343 );
344 }
345
346 if let Some(manual_offset) = args.offset {
347 // Manual synchronization mode: apply specified offset
348 let mut subtitle = load_subtitle(&args.subtitle).await?;
349 sync_engine.apply_sync_offset(&mut subtitle, manual_offset as f32)?;
350 save_subtitle(&subtitle, &args.subtitle).await?;
351 println!("✓ Applied manual offset: {}s", manual_offset);
352 } else if args.batch {
353 let media_pairs = discover_media_pairs(&args.video).await?;
354 for (video_file, subtitle_file) in media_pairs {
355 match sync_single_pair(&sync_engine, &video_file, &subtitle_file).await {
356 Ok(result) => {
357 println!(
358 "✓ {} - Offset: {:.2}s (Confidence: {:.2})",
359 subtitle_file.display(),
360 result.offset_seconds,
361 result.confidence
362 );
363 }
364 Err(e) => {
365 println!("✗ {} - Error: {}", subtitle_file.display(), e);
366 }
367 }
368 }
369 } else {
370 let subtitle = load_subtitle(&args.subtitle).await?;
371 let result = sync_engine.sync_subtitle(&args.video, &subtitle).await?;
372 if result.confidence > 0.5 {
373 let mut updated = subtitle;
374 sync_engine.apply_sync_offset(&mut updated, result.offset_seconds)?;
375 save_subtitle(&updated, &args.subtitle).await?;
376 println!(
377 "✓ Sync completed - Offset: {:.2}s (Confidence: {:.2})",
378 result.offset_seconds, result.confidence
379 );
380 } else {
381 println!(
382 "⚠ Low sync confidence ({:.2}), manual adjustment recommended",
383 result.confidence
384 );
385 }
386 }
387 Ok(())
388}
389
390/// Load and parse subtitle file
391async fn load_subtitle(path: &Path) -> Result<Subtitle> {
392 let content = tokio::fs::read_to_string(path).await?;
393 let mgr = FormatManager::new();
394 let mut subtitle = mgr.parse_auto(&content)?;
395 // Set source encoding
396 subtitle.metadata.encoding = "utf-8".to_string();
397 Ok(subtitle)
398}
399
400/// Serialize and save subtitle file
401async fn save_subtitle(subtitle: &Subtitle, path: &Path) -> Result<()> {
402 let mgr = FormatManager::new();
403 let text = mgr
404 .get_format_by_extension(
405 path.extension()
406 .and_then(|e| e.to_str())
407 .unwrap_or_default(),
408 )
409 .ok_or_else(|| SubXError::subtitle_format("Unknown", "Unknown subtitle format"))?
410 .serialize(subtitle)?;
411 tokio::fs::write(path, text).await?;
412 Ok(())
413}
414
415/// Scan directory and pair video with subtitle files
416async fn discover_media_pairs(dir: &Path) -> Result<Vec<(PathBuf, PathBuf)>> {
417 let discovery = FileDiscovery::new();
418 let files = discovery.scan_directory(dir, true)?;
419 let videos: Vec<_> = files
420 .iter()
421 .filter(|f| matches!(f.file_type, MediaFileType::Video))
422 .cloned()
423 .collect();
424 let subs: Vec<_> = files
425 .iter()
426 .filter(|f| matches!(f.file_type, MediaFileType::Subtitle))
427 .cloned()
428 .collect();
429 let mut pairs = Vec::new();
430 for video in videos {
431 if let Some(s) = subs.iter().find(|s| {
432 let video_base = video
433 .name
434 .strip_suffix(&format!(".{}", video.extension))
435 .unwrap_or(&video.name);
436 let sub_base = s
437 .name
438 .strip_suffix(&format!(".{}", s.extension))
439 .unwrap_or(&s.name);
440 video_base == sub_base
441 }) {
442 pairs.push((video.path.clone(), s.path.clone()));
443 }
444 }
445 Ok(pairs)
446}
447
448/// Synchronize single media file
449async fn sync_single_pair(
450 engine: &SyncEngine,
451 video: &Path,
452 subtitle_path: &Path,
453) -> Result<SyncResult> {
454 let mut subtitle = load_subtitle(subtitle_path).await?;
455 let result = engine.sync_subtitle(video, &subtitle).await?;
456 engine.apply_sync_offset(&mut subtitle, result.offset_seconds)?;
457 save_subtitle(&subtitle, subtitle_path).await?;
458 Ok(result)
459}