1use log::{debug, warn};
7use serde::{Deserialize, Serialize};
8use serde_json::json;
9use std::path::Path;
10use std::time::{Duration, Instant};
11
12use crate::config::SyncConfig;
13use crate::core::formats::Subtitle;
14use crate::services::vad::VadSyncDetector;
15use crate::{Result, error::SubXError};
16
17pub struct SyncEngine {
22 config: SyncConfig,
23 vad_detector: Option<VadSyncDetector>,
24 reporter: std::sync::Arc<dyn crate::core::report::Reporter>,
28}
29
30impl SyncEngine {
31 pub fn new(config: SyncConfig) -> Result<Self> {
48 let vad_detector = if config.vad.enabled {
49 match VadSyncDetector::new(config.vad.clone()) {
50 Ok(det) => {
51 debug!(
52 "[SyncEngine] VAD detector initialized successfully with config: {:?}",
53 config.vad
54 );
55 Some(det)
56 }
57 Err(e) => {
58 warn!("[SyncEngine] VAD initialization failed: {}", e);
59 None
60 }
61 }
62 } else {
63 debug!("[SyncEngine] VAD is disabled in config");
64 None
65 };
66
67 if vad_detector.is_none() {
68 warn!("[SyncEngine] VAD detector is required but not available");
69 return Err(SubXError::config(
70 "VAD detector is required but not available",
71 ));
72 }
73
74 debug!("[SyncEngine] SyncEngine created with VAD detector");
75 Ok(Self {
76 config,
77 vad_detector,
78 reporter: crate::core::report::noop(),
79 })
80 }
81
82 pub fn with_reporter(
89 mut self,
90 reporter: std::sync::Arc<dyn crate::core::report::Reporter>,
91 ) -> Self {
92 self.reporter = reporter;
93 self
94 }
95
96 pub fn reporter(&self) -> &std::sync::Arc<dyn crate::core::report::Reporter> {
99 &self.reporter
100 }
101
102 pub async fn detect_sync_offset(
114 &self,
115 audio_path: &Path,
116 subtitle: &Subtitle,
117 method: Option<SyncMethod>,
118 ) -> Result<SyncResult> {
119 debug!(
120 "[SyncEngine] detect_sync_offset called | audio_path: {:?}, subtitle entries: {}, method: {:?}",
121 audio_path,
122 subtitle.entries.len(),
123 method
124 );
125 let start = Instant::now();
126 let m = method.unwrap_or_else(|| self.determine_default_method());
127 debug!("[SyncEngine] Using sync method: {:?}", m);
128 let mut res = match m {
129 SyncMethod::Auto | SyncMethod::LocalVad => {
130 self.vad_detect_sync_offset(audio_path, subtitle).await?
131 }
132 SyncMethod::Manual => {
133 debug!("[SyncEngine] Manual method selected but not supported in this context");
134 return Err(SubXError::config("Manual method requires explicit offset"));
135 }
136 };
137 res.processing_duration = start.elapsed();
138 debug!(
139 "[SyncEngine] detect_sync_offset finished | offset_seconds: {:.3}, confidence: {:.3}, duration_ms: {}",
140 res.offset_seconds,
141 res.confidence,
142 res.processing_duration.as_millis()
143 );
144 Ok(res)
145 }
146
147 async fn auto_detect_sync_offset(
148 &self,
149 audio_path: &Path,
150 subtitle: &Subtitle,
151 ) -> Result<SyncResult> {
152 debug!(
153 "[SyncEngine] auto_detect_sync_offset called | audio_path: {:?}, subtitle entries: {}",
154 audio_path,
155 subtitle.entries.len()
156 );
157 if self.vad_detector.is_some() {
159 return self.vad_detect_sync_offset(audio_path, subtitle).await;
160 }
161 Err(SubXError::audio_processing(
162 "No detector available in auto mode",
163 ))
164 }
165
166 pub fn apply_manual_offset(
181 &self,
182 subtitle: &mut Subtitle,
183 offset_seconds: f32,
184 ) -> Result<SyncResult> {
185 debug!(
186 "[SyncEngine] apply_manual_offset called | offset_seconds: {:.3}, entries: {}",
187 offset_seconds,
188 subtitle.entries.len()
189 );
190 if offset_seconds.abs() > self.config.max_offset_seconds {
192 warn!(
193 "[SyncEngine] Offset {:.2}s exceeds maximum allowed value {:.2}s. Aborting.",
194 offset_seconds, self.config.max_offset_seconds
195 );
196 return Err(SubXError::config(format!(
197 "Offset {:.2}s exceeds maximum allowed value {:.2}s. Please check the sync.max_offset_seconds configuration or use a smaller offset.",
198 offset_seconds, self.config.max_offset_seconds
199 )));
200 }
201
202 super::shift_subtitle_timing(subtitle, offset_seconds)
203 }
204
205 fn determine_default_method(&self) -> SyncMethod {
206 debug!(
207 "[SyncEngine] determine_default_method called | config.default_method: {}",
208 self.config.default_method
209 );
210 match self.config.default_method.as_str() {
211 "vad" => SyncMethod::LocalVad,
212 _ => SyncMethod::Auto,
213 }
214 }
215
216 async fn vad_detect_sync_offset(
217 &self,
218 audio_path: &Path,
219 subtitle: &Subtitle,
220 ) -> Result<SyncResult> {
221 debug!(
222 "[SyncEngine] vad_detect_sync_offset called | audio_path: {:?}, subtitle entries: {}",
223 audio_path,
224 subtitle.entries.len()
225 );
226 let det = self
227 .vad_detector
228 .as_ref()
229 .ok_or_else(|| SubXError::audio_processing("VAD detector not available"))?;
230
231 let mut result = det.detect_sync_offset(audio_path, subtitle, 0).await?; if result.offset_seconds.abs() > self.config.max_offset_seconds {
235 warn!(
236 "[SyncEngine] Detected offset {:.2}s exceeds configured maximum value {:.2}s. Clamping and warning.",
237 result.offset_seconds, self.config.max_offset_seconds
238 );
239
240 result.warnings.push(format!(
242 "Detected offset {:.2}s exceeds configured maximum value {:.2}s. Consider checking audio quality or adjusting sync.max_offset_seconds configuration.",
243 result.offset_seconds, self.config.max_offset_seconds
244 ));
245
246 let sign = if result.offset_seconds >= 0.0 {
248 1.0
249 } else {
250 -1.0
251 };
252 let original_offset = result.offset_seconds;
253 result.offset_seconds = sign * self.config.max_offset_seconds;
254
255 result.additional_info = Some(json!({
256 "original_offset": original_offset,
257 "clamped_offset": result.offset_seconds,
258 "reason": "Exceeded max_offset_seconds configuration"
259 }));
260 } else {
261 debug!(
262 "[SyncEngine] VAD sync offset detected | offset_seconds: {:.3}, confidence: {:.3}",
263 result.offset_seconds, result.confidence
264 );
265 }
266
267 Ok(result)
268 }
269}
270
271#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
276pub enum SyncMethod {
277 Auto,
279 LocalVad,
281 Manual,
283}
284
285#[derive(Debug, Clone)]
290pub struct SyncResult {
291 pub offset_seconds: f32,
293 pub confidence: f32,
295 pub method_used: SyncMethod,
297 pub correlation_peak: f32,
299 pub additional_info: Option<serde_json::Value>,
301 pub processing_duration: Duration,
303 pub warnings: Vec<String>,
305}
306
307#[derive(Debug, Clone)]
312pub struct MethodSelectionStrategy {
313 pub preferred_methods: Vec<SyncMethod>,
315 pub min_confidence_threshold: f32,
317 pub allow_fallback: bool,
319 pub max_attempt_duration: u32,
321}
322
323#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::config::{TestConfigBuilder, TestConfigService, service::ConfigService};
328 use crate::core::formats::{Subtitle, SubtitleEntry, SubtitleFormatType, SubtitleMetadata};
329 use std::time::Duration;
330
331 #[tokio::test]
332 async fn test_sync_engine_creation() {
333 let config = TestConfigBuilder::new()
334 .with_vad_enabled(true)
335 .build_config();
336 let config_service = TestConfigService::new(config);
337 let result = SyncEngine::new(config_service.get_config().unwrap().sync);
338 assert!(result.is_ok());
339 }
340
341 #[tokio::test]
342 async fn test_manual_offset_application() {
343 let config = TestConfigBuilder::new().build_config();
344 let config_service = TestConfigService::new(config);
345 let engine = SyncEngine::new(config_service.get_config().unwrap().sync).unwrap();
346
347 let mut subtitle = create_test_subtitle();
348 let original_start = subtitle.entries[0].start_time;
349
350 let result = engine.apply_manual_offset(&mut subtitle, 2.5).unwrap();
351 assert_eq!(result.offset_seconds, 2.5);
352 assert_eq!(result.method_used, SyncMethod::Manual);
353 assert_eq!(result.confidence, 1.0);
354
355 let expected_start = original_start + Duration::from_secs_f32(2.5);
356 assert_eq!(subtitle.entries[0].start_time, expected_start);
357 }
358
359 #[tokio::test]
360 async fn test_manual_offset_negative_application() {
361 let config = TestConfigBuilder::new().build_config();
362 let config_service = TestConfigService::new(config);
363 let engine = SyncEngine::new(config_service.get_config().unwrap().sync).unwrap();
364
365 let mut subtitle = create_test_subtitle();
366 let original_start = subtitle.entries[0].start_time;
367
368 let result = engine.apply_manual_offset(&mut subtitle, -2.5).unwrap();
369 assert_eq!(result.offset_seconds, -2.5);
370
371 let expected_start = original_start - Duration::from_secs_f32(2.5);
372 assert_eq!(subtitle.entries[0].start_time, expected_start);
373 }
374
375 #[tokio::test]
376 async fn test_determine_default_method() {
377 let test_cases = vec![("vad", SyncMethod::LocalVad), ("unknown", SyncMethod::Auto)];
378
379 for (config_value, expected_method) in test_cases {
380 let config = TestConfigBuilder::new()
381 .with_sync_method(config_value)
382 .build_config();
383 let engine = SyncEngine::new(config.sync).unwrap();
384 assert_eq!(engine.determine_default_method(), expected_method);
385 }
386 }
387
388 #[tokio::test]
389 async fn test_method_selection_strategy_struct() {
390 let strategy = MethodSelectionStrategy {
391 preferred_methods: vec![SyncMethod::LocalVad],
392 min_confidence_threshold: 0.7,
393 allow_fallback: true,
394 max_attempt_duration: 60,
395 };
396 assert_eq!(strategy.preferred_methods.len(), 1);
397 assert!(strategy.allow_fallback);
398 }
399
400 fn create_test_subtitle() -> Subtitle {
401 Subtitle {
402 entries: vec![SubtitleEntry::new(
403 1,
404 Duration::from_secs(10),
405 Duration::from_secs(12),
406 "Test subtitle".to_string(),
407 )],
408 metadata: SubtitleMetadata::default(),
409 format: SubtitleFormatType::Srt,
410 }
411 }
412}