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}