Michael Hu commited on
Commit
f7aaf3b
·
1 Parent(s): 3f340d1

set parakeet model to default asr

Browse files
src/application/dtos/processing_request_dto.py CHANGED
@@ -8,7 +8,7 @@ from .audio_upload_dto import AudioUploadDto
8
  @dataclass
9
  class ProcessingRequestDto:
10
  """DTO for pipeline input parameters
11
-
12
  Contains all parameters needed to process audio through
13
  the STT -> Translation -> TTS pipeline.
14
  """
@@ -19,29 +19,29 @@ class ProcessingRequestDto:
19
  speed: float = 1.0
20
  source_language: Optional[str] = None
21
  additional_params: Optional[Dict[str, Any]] = None
22
-
23
  def __post_init__(self):
24
  """Validate the DTO after initialization"""
25
  self._validate()
26
  if self.additional_params is None:
27
  self.additional_params = {}
28
-
29
  def _validate(self):
30
  """Validate processing request parameters"""
31
  if not isinstance(self.audio, AudioUploadDto):
32
  raise ValueError("Audio must be an AudioUploadDto instance")
33
-
34
  if not self.asr_model:
35
  raise ValueError("ASR model cannot be empty")
36
-
37
  # Validate ASR model options
38
- supported_asr_models = ['whisper-small', 'whisper-medium', 'whisper-large', 'parakeet']
39
  if self.asr_model not in supported_asr_models:
40
  raise ValueError(f"Unsupported ASR model: {self.asr_model}. Supported: {supported_asr_models}")
41
-
42
  if not self.target_language:
43
  raise ValueError("Target language cannot be empty")
44
-
45
  # Validate language codes (ISO 639-1)
46
  supported_languages = [
47
  'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
@@ -49,33 +49,33 @@ class ProcessingRequestDto:
49
  ]
50
  if self.target_language not in supported_languages:
51
  raise ValueError(f"Unsupported target language: {self.target_language}. Supported: {supported_languages}")
52
-
53
  if self.source_language and self.source_language not in supported_languages:
54
  raise ValueError(f"Unsupported source language: {self.source_language}. Supported: {supported_languages}")
55
-
56
  if not self.voice:
57
  raise ValueError("Voice cannot be empty")
58
-
59
  # Validate voice options
60
  supported_voices = ['kokoro', 'dia', 'cosyvoice2', 'dummy']
61
  if self.voice not in supported_voices:
62
  raise ValueError(f"Unsupported voice: {self.voice}. Supported: {supported_voices}")
63
-
64
  # Validate speed range
65
  if not 0.5 <= self.speed <= 2.0:
66
  raise ValueError(f"Speed must be between 0.5 and 2.0, got: {self.speed}")
67
-
68
  # Validate additional params if provided
69
  if self.additional_params and not isinstance(self.additional_params, dict):
70
  raise ValueError("Additional params must be a dictionary")
71
-
72
  @property
73
  def requires_translation(self) -> bool:
74
  """Check if translation is required"""
75
  if not self.source_language:
76
  return True # Assume translation needed if source not specified
77
  return self.source_language != self.target_language
78
-
79
  def to_dict(self) -> dict:
80
  """Convert to dictionary representation"""
81
  return {
@@ -88,7 +88,7 @@ class ProcessingRequestDto:
88
  'requires_translation': self.requires_translation,
89
  'additional_params': self.additional_params or {}
90
  }
91
-
92
  @classmethod
93
  def from_dict(cls, data: dict) -> 'ProcessingRequestDto':
94
  """Create instance from dictionary"""
@@ -102,7 +102,7 @@ class ProcessingRequestDto:
102
  )
103
  else:
104
  audio = audio_data
105
-
106
  return cls(
107
  audio=audio,
108
  asr_model=data['asr_model'],
 
8
  @dataclass
9
  class ProcessingRequestDto:
10
  """DTO for pipeline input parameters
11
+
12
  Contains all parameters needed to process audio through
13
  the STT -> Translation -> TTS pipeline.
14
  """
 
19
  speed: float = 1.0
20
  source_language: Optional[str] = None
21
  additional_params: Optional[Dict[str, Any]] = None
22
+
23
  def __post_init__(self):
24
  """Validate the DTO after initialization"""
25
  self._validate()
26
  if self.additional_params is None:
27
  self.additional_params = {}
28
+
29
  def _validate(self):
30
  """Validate processing request parameters"""
31
  if not isinstance(self.audio, AudioUploadDto):
32
  raise ValueError("Audio must be an AudioUploadDto instance")
33
+
34
  if not self.asr_model:
35
  raise ValueError("ASR model cannot be empty")
36
+
37
  # Validate ASR model options
38
+ supported_asr_models = ['parakeet', 'whisper-small', 'whisper-medium', 'whisper-large']
39
  if self.asr_model not in supported_asr_models:
40
  raise ValueError(f"Unsupported ASR model: {self.asr_model}. Supported: {supported_asr_models}")
41
+
42
  if not self.target_language:
43
  raise ValueError("Target language cannot be empty")
44
+
45
  # Validate language codes (ISO 639-1)
46
  supported_languages = [
47
  'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
 
49
  ]
50
  if self.target_language not in supported_languages:
51
  raise ValueError(f"Unsupported target language: {self.target_language}. Supported: {supported_languages}")
52
+
53
  if self.source_language and self.source_language not in supported_languages:
54
  raise ValueError(f"Unsupported source language: {self.source_language}. Supported: {supported_languages}")
55
+
56
  if not self.voice:
57
  raise ValueError("Voice cannot be empty")
58
+
59
  # Validate voice options
60
  supported_voices = ['kokoro', 'dia', 'cosyvoice2', 'dummy']
61
  if self.voice not in supported_voices:
62
  raise ValueError(f"Unsupported voice: {self.voice}. Supported: {supported_voices}")
63
+
64
  # Validate speed range
65
  if not 0.5 <= self.speed <= 2.0:
66
  raise ValueError(f"Speed must be between 0.5 and 2.0, got: {self.speed}")
67
+
68
  # Validate additional params if provided
69
  if self.additional_params and not isinstance(self.additional_params, dict):
70
  raise ValueError("Additional params must be a dictionary")
71
+
72
  @property
73
  def requires_translation(self) -> bool:
74
  """Check if translation is required"""
75
  if not self.source_language:
76
  return True # Assume translation needed if source not specified
77
  return self.source_language != self.target_language
78
+
79
  def to_dict(self) -> dict:
80
  """Convert to dictionary representation"""
81
  return {
 
88
  'requires_translation': self.requires_translation,
89
  'additional_params': self.additional_params or {}
90
  }
91
+
92
  @classmethod
93
  def from_dict(cls, data: dict) -> 'ProcessingRequestDto':
94
  """Create instance from dictionary"""
 
102
  )
103
  else:
104
  audio = audio_data
105
+
106
  return cls(
107
  audio=audio,
108
  asr_model=data['asr_model'],
src/application/services/audio_processing_service.py CHANGED
@@ -634,7 +634,7 @@ class AudioProcessingApplicationService:
634
  Dict[str, Any]: Supported configurations
635
  """
636
  return {
637
- 'asr_models': ['whisper-small', 'whisper-medium', 'whisper-large', 'parakeet'],
638
  'voices': ['kokoro', 'dia', 'cosyvoice2', 'dummy'],
639
  'languages': [
640
  'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
 
634
  Dict[str, Any]: Supported configurations
635
  """
636
  return {
637
+ 'asr_models': ['parakeet', 'whisper-small', 'whisper-medium', 'whisper-large'],
638
  'voices': ['kokoro', 'dia', 'cosyvoice2', 'dummy'],
639
  'languages': [
640
  'en', 'es', 'fr', 'de', 'it', 'pt', 'ru', 'ja', 'ko', 'zh',
tests/integration/test_audio_processing_pipeline.py CHANGED
@@ -37,7 +37,7 @@ class TestAudioProcessingPipeline:
37
  def mock_config(self, temp_dir):
38
  """Create mock configuration for testing."""
39
  config = Mock(spec=AppConfig)
40
-
41
  # Processing configuration
42
  config.get_processing_config.return_value = {
43
  'max_file_size_mb': 50,
@@ -45,7 +45,7 @@ class TestAudioProcessingPipeline:
45
  'temp_dir': temp_dir,
46
  'cleanup_temp_files': True
47
  }
48
-
49
  # Logging configuration
50
  config.get_logging_config.return_value = {
51
  'level': 'INFO',
@@ -53,17 +53,17 @@ class TestAudioProcessingPipeline:
53
  'log_file_path': os.path.join(temp_dir, 'test.log'),
54
  'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
55
  }
56
-
57
  # STT configuration
58
  config.get_stt_config.return_value = {
59
- 'preferred_providers': ['whisper-small', 'whisper-medium', 'parakeet']
60
  }
61
-
62
  # TTS configuration
63
  config.get_tts_config.return_value = {
64
  'preferred_providers': ['kokoro', 'dia', 'cosyvoice2', 'dummy']
65
  }
66
-
67
  return config
68
 
69
  @pytest.fixture
@@ -71,7 +71,7 @@ class TestAudioProcessingPipeline:
71
  """Create mock dependency container for testing."""
72
  container = Mock(spec=DependencyContainer)
73
  container.resolve.return_value = mock_config
74
-
75
  # Mock STT provider
76
  mock_stt_provider = Mock()
77
  mock_stt_provider.transcribe.return_value = TextContent(
@@ -79,7 +79,7 @@ class TestAudioProcessingPipeline:
79
  language="en"
80
  )
81
  container.get_stt_provider.return_value = mock_stt_provider
82
-
83
  # Mock translation provider
84
  mock_translation_provider = Mock()
85
  mock_translation_provider.translate.return_value = TextContent(
@@ -87,7 +87,7 @@ class TestAudioProcessingPipeline:
87
  language="es"
88
  )
89
  container.get_translation_provider.return_value = mock_translation_provider
90
-
91
  # Mock TTS provider
92
  mock_tts_provider = Mock()
93
  mock_audio_content = AudioContent(
@@ -98,7 +98,7 @@ class TestAudioProcessingPipeline:
98
  )
99
  mock_tts_provider.synthesize.return_value = mock_audio_content
100
  container.get_tts_provider.return_value = mock_tts_provider
101
-
102
  return container
103
 
104
  @pytest.fixture
@@ -133,7 +133,7 @@ class TestAudioProcessingPipeline:
133
  """Test successful execution of the complete audio processing pipeline."""
134
  # Execute the pipeline
135
  result = audio_service.process_audio_pipeline(sample_processing_request)
136
-
137
  # Verify successful result
138
  assert isinstance(result, ProcessingResultDto)
139
  assert result.success is True
@@ -156,9 +156,9 @@ class TestAudioProcessingPipeline:
156
  speed=1.0,
157
  requires_translation=False
158
  )
159
-
160
  result = audio_service.process_audio_pipeline(request)
161
-
162
  assert result.success is True
163
  assert result.original_text == "Hello, this is a test transcription."
164
  assert result.translated_text is None # No translation performed
@@ -175,9 +175,9 @@ class TestAudioProcessingPipeline:
175
  speed=1.5,
176
  requires_translation=True
177
  )
178
-
179
  result = audio_service.process_audio_pipeline(request)
180
-
181
  assert result.success is True
182
  assert result.metadata['voice'] == "dia"
183
  assert result.metadata['speed'] == 1.5
@@ -188,7 +188,7 @@ class TestAudioProcessingPipeline:
188
  start_time = time.time()
189
  result = audio_service.process_audio_pipeline(sample_processing_request)
190
  end_time = time.time()
191
-
192
  assert result.success is True
193
  assert result.processing_time > 0
194
  assert result.processing_time <= (end_time - start_time) + 0.1 # Allow small margin
@@ -203,7 +203,7 @@ class TestAudioProcessingPipeline:
203
  content_type="audio/wav",
204
  size=10 * 1024 * 1024
205
  )
206
-
207
  request = ProcessingRequestDto(
208
  audio=large_audio,
209
  asr_model="whisper-small",
@@ -212,9 +212,9 @@ class TestAudioProcessingPipeline:
212
  speed=1.0,
213
  requires_translation=True
214
  )
215
-
216
  result = audio_service.process_audio_pipeline(request)
217
-
218
  assert result.success is True
219
  assert result.metadata['file_size'] == 10 * 1024 * 1024
220
 
@@ -222,12 +222,12 @@ class TestAudioProcessingPipeline:
222
  """Test that temporary files are properly cleaned up."""
223
  # Count files before processing
224
  files_before = len(list(Path(temp_dir).rglob("*")))
225
-
226
  result = audio_service.process_audio_pipeline(sample_processing_request)
227
-
228
  # Verify processing succeeded
229
  assert result.success is True
230
-
231
  # Verify cleanup occurred (no additional temp files)
232
  files_after = len(list(Path(temp_dir).rglob("*")))
233
  assert files_after <= files_before + 1 # Allow for output file
@@ -235,14 +235,14 @@ class TestAudioProcessingPipeline:
235
  def test_pipeline_correlation_id_tracking(self, audio_service, sample_processing_request):
236
  """Test that correlation IDs are properly tracked throughout the pipeline."""
237
  result = audio_service.process_audio_pipeline(sample_processing_request)
238
-
239
  assert result.success is True
240
  assert 'correlation_id' in result.metadata
241
-
242
  correlation_id = result.metadata['correlation_id']
243
  assert isinstance(correlation_id, str)
244
  assert len(correlation_id) > 0
245
-
246
  # Verify correlation ID is used in status tracking
247
  status = audio_service.get_processing_status(correlation_id)
248
  assert status['correlation_id'] == correlation_id
@@ -250,29 +250,29 @@ class TestAudioProcessingPipeline:
250
  def test_pipeline_metadata_completeness(self, audio_service, sample_processing_request):
251
  """Test that pipeline result contains complete metadata."""
252
  result = audio_service.process_audio_pipeline(sample_processing_request)
253
-
254
  assert result.success is True
255
  assert result.metadata is not None
256
-
257
  expected_metadata_keys = [
258
- 'correlation_id', 'asr_model', 'target_language',
259
  'voice', 'speed', 'translation_required'
260
  ]
261
-
262
  for key in expected_metadata_keys:
263
  assert key in result.metadata
264
 
265
  def test_pipeline_supported_configurations(self, audio_service):
266
  """Test retrieval of supported pipeline configurations."""
267
  config = audio_service.get_supported_configurations()
268
-
269
  assert 'asr_models' in config
270
  assert 'voices' in config
271
  assert 'languages' in config
272
  assert 'audio_formats' in config
273
  assert 'max_file_size_mb' in config
274
  assert 'speed_range' in config
275
-
276
  assert isinstance(config['asr_models'], list)
277
  assert isinstance(config['voices'], list)
278
  assert isinstance(config['languages'], list)
@@ -283,7 +283,7 @@ class TestAudioProcessingPipeline:
283
  """Test audio service as context manager."""
284
  with AudioProcessingApplicationService(mock_container, mock_config) as service:
285
  assert service is not None
286
-
287
  # Service should be usable within context
288
  config = service.get_supported_configurations()
289
  assert config is not None
@@ -301,18 +301,18 @@ class TestAudioProcessingPipeline:
301
  requires_translation=True
302
  )
303
  requests.append(request)
304
-
305
  results = []
306
  for request in requests:
307
  result = audio_service.process_audio_pipeline(request)
308
  results.append(result)
309
-
310
  # Verify all requests succeeded
311
  for result in results:
312
  assert result.success is True
313
  assert result.original_text is not None
314
  assert result.translated_text is not None
315
-
316
  # Verify each request has unique correlation ID
317
  correlation_ids = [r.metadata['correlation_id'] for r in results]
318
  assert len(set(correlation_ids)) == 3 # All unique
@@ -321,27 +321,27 @@ class TestAudioProcessingPipeline:
321
  """Test pipeline behavior under concurrent processing."""
322
  import threading
323
  import queue
324
-
325
  results_queue = queue.Queue()
326
-
327
  def process_request():
328
  try:
329
  result = audio_service.process_audio_pipeline(sample_processing_request)
330
  results_queue.put(result)
331
  except Exception as e:
332
  results_queue.put(e)
333
-
334
  # Start multiple threads
335
  threads = []
336
  for _ in range(3):
337
  thread = threading.Thread(target=process_request)
338
  threads.append(thread)
339
  thread.start()
340
-
341
  # Wait for completion
342
  for thread in threads:
343
  thread.join()
344
-
345
  # Verify all results
346
  results = []
347
  while not results_queue.empty():
@@ -349,7 +349,7 @@ class TestAudioProcessingPipeline:
349
  if isinstance(result, Exception):
350
  pytest.fail(f"Concurrent processing failed: {result}")
351
  results.append(result)
352
-
353
  assert len(results) == 3
354
  for result in results:
355
  assert result.success is True
@@ -358,18 +358,18 @@ class TestAudioProcessingPipeline:
358
  """Test pipeline memory usage and cleanup."""
359
  import psutil
360
  import os
361
-
362
  process = psutil.Process(os.getpid())
363
  memory_before = process.memory_info().rss
364
-
365
  # Process multiple requests
366
  for _ in range(5):
367
  result = audio_service.process_audio_pipeline(sample_processing_request)
368
  assert result.success is True
369
-
370
  memory_after = process.memory_info().rss
371
  memory_increase = memory_after - memory_before
372
-
373
  # Memory increase should be reasonable (less than 50MB for test data)
374
  assert memory_increase < 50 * 1024 * 1024
375
 
@@ -377,7 +377,7 @@ class TestAudioProcessingPipeline:
377
  """Test pipeline with streaming TTS synthesis."""
378
  # Mock streaming TTS provider
379
  mock_tts_provider = mock_container.get_tts_provider.return_value
380
-
381
  def mock_stream():
382
  for i in range(3):
383
  yield AudioContent(
@@ -386,18 +386,18 @@ class TestAudioProcessingPipeline:
386
  sample_rate=22050,
387
  duration=0.5
388
  )
389
-
390
  mock_tts_provider.synthesize_stream.return_value = mock_stream()
391
-
392
  result = audio_service.process_audio_pipeline(sample_processing_request)
393
-
394
  assert result.success is True
395
  assert result.audio_path is not None
396
 
397
  def test_pipeline_configuration_validation(self, audio_service):
398
  """Test pipeline configuration validation."""
399
  config = audio_service.get_supported_configurations()
400
-
401
  # Verify configuration structure
402
  assert isinstance(config['asr_models'], list)
403
  assert isinstance(config['voices'], list)
@@ -405,7 +405,7 @@ class TestAudioProcessingPipeline:
405
  assert isinstance(config['audio_formats'], list)
406
  assert isinstance(config['max_file_size_mb'], (int, float))
407
  assert isinstance(config['speed_range'], dict)
408
-
409
  # Verify speed range
410
  speed_range = config['speed_range']
411
  assert 'min' in speed_range
@@ -422,10 +422,10 @@ class TestAudioProcessingPipeline:
422
  SpeechRecognitionException("First attempt failed"),
423
  TextContent(text="Recovered transcription", language="en")
424
  ]
425
-
426
  with patch('src.application.services.audio_processing_service.logger') as mock_logger:
427
  result = audio_service.process_audio_pipeline(sample_processing_request)
428
-
429
  assert result.success is True
430
  # Verify error and recovery were logged
431
  mock_logger.warning.assert_called()
@@ -436,16 +436,16 @@ class TestAudioProcessingPipeline:
436
  start_time = time.time()
437
  result = audio_service.process_audio_pipeline(sample_processing_request)
438
  end_time = time.time()
439
-
440
  total_time = end_time - start_time
441
-
442
  assert result.success is True
443
  assert result.processing_time > 0
444
  assert result.processing_time <= total_time
445
-
446
  # For mock providers, processing should be fast
447
  assert total_time < 5.0 # Should complete within 5 seconds
448
-
449
  # Verify timing metadata
450
  assert 'correlation_id' in result.metadata
451
  timing_info = result.metadata
 
37
  def mock_config(self, temp_dir):
38
  """Create mock configuration for testing."""
39
  config = Mock(spec=AppConfig)
40
+
41
  # Processing configuration
42
  config.get_processing_config.return_value = {
43
  'max_file_size_mb': 50,
 
45
  'temp_dir': temp_dir,
46
  'cleanup_temp_files': True
47
  }
48
+
49
  # Logging configuration
50
  config.get_logging_config.return_value = {
51
  'level': 'INFO',
 
53
  'log_file_path': os.path.join(temp_dir, 'test.log'),
54
  'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
55
  }
56
+
57
  # STT configuration
58
  config.get_stt_config.return_value = {
59
+ 'preferred_providers': ['parakeet', 'whisper-small', 'whisper-medium']
60
  }
61
+
62
  # TTS configuration
63
  config.get_tts_config.return_value = {
64
  'preferred_providers': ['kokoro', 'dia', 'cosyvoice2', 'dummy']
65
  }
66
+
67
  return config
68
 
69
  @pytest.fixture
 
71
  """Create mock dependency container for testing."""
72
  container = Mock(spec=DependencyContainer)
73
  container.resolve.return_value = mock_config
74
+
75
  # Mock STT provider
76
  mock_stt_provider = Mock()
77
  mock_stt_provider.transcribe.return_value = TextContent(
 
79
  language="en"
80
  )
81
  container.get_stt_provider.return_value = mock_stt_provider
82
+
83
  # Mock translation provider
84
  mock_translation_provider = Mock()
85
  mock_translation_provider.translate.return_value = TextContent(
 
87
  language="es"
88
  )
89
  container.get_translation_provider.return_value = mock_translation_provider
90
+
91
  # Mock TTS provider
92
  mock_tts_provider = Mock()
93
  mock_audio_content = AudioContent(
 
98
  )
99
  mock_tts_provider.synthesize.return_value = mock_audio_content
100
  container.get_tts_provider.return_value = mock_tts_provider
101
+
102
  return container
103
 
104
  @pytest.fixture
 
133
  """Test successful execution of the complete audio processing pipeline."""
134
  # Execute the pipeline
135
  result = audio_service.process_audio_pipeline(sample_processing_request)
136
+
137
  # Verify successful result
138
  assert isinstance(result, ProcessingResultDto)
139
  assert result.success is True
 
156
  speed=1.0,
157
  requires_translation=False
158
  )
159
+
160
  result = audio_service.process_audio_pipeline(request)
161
+
162
  assert result.success is True
163
  assert result.original_text == "Hello, this is a test transcription."
164
  assert result.translated_text is None # No translation performed
 
175
  speed=1.5,
176
  requires_translation=True
177
  )
178
+
179
  result = audio_service.process_audio_pipeline(request)
180
+
181
  assert result.success is True
182
  assert result.metadata['voice'] == "dia"
183
  assert result.metadata['speed'] == 1.5
 
188
  start_time = time.time()
189
  result = audio_service.process_audio_pipeline(sample_processing_request)
190
  end_time = time.time()
191
+
192
  assert result.success is True
193
  assert result.processing_time > 0
194
  assert result.processing_time <= (end_time - start_time) + 0.1 # Allow small margin
 
203
  content_type="audio/wav",
204
  size=10 * 1024 * 1024
205
  )
206
+
207
  request = ProcessingRequestDto(
208
  audio=large_audio,
209
  asr_model="whisper-small",
 
212
  speed=1.0,
213
  requires_translation=True
214
  )
215
+
216
  result = audio_service.process_audio_pipeline(request)
217
+
218
  assert result.success is True
219
  assert result.metadata['file_size'] == 10 * 1024 * 1024
220
 
 
222
  """Test that temporary files are properly cleaned up."""
223
  # Count files before processing
224
  files_before = len(list(Path(temp_dir).rglob("*")))
225
+
226
  result = audio_service.process_audio_pipeline(sample_processing_request)
227
+
228
  # Verify processing succeeded
229
  assert result.success is True
230
+
231
  # Verify cleanup occurred (no additional temp files)
232
  files_after = len(list(Path(temp_dir).rglob("*")))
233
  assert files_after <= files_before + 1 # Allow for output file
 
235
  def test_pipeline_correlation_id_tracking(self, audio_service, sample_processing_request):
236
  """Test that correlation IDs are properly tracked throughout the pipeline."""
237
  result = audio_service.process_audio_pipeline(sample_processing_request)
238
+
239
  assert result.success is True
240
  assert 'correlation_id' in result.metadata
241
+
242
  correlation_id = result.metadata['correlation_id']
243
  assert isinstance(correlation_id, str)
244
  assert len(correlation_id) > 0
245
+
246
  # Verify correlation ID is used in status tracking
247
  status = audio_service.get_processing_status(correlation_id)
248
  assert status['correlation_id'] == correlation_id
 
250
  def test_pipeline_metadata_completeness(self, audio_service, sample_processing_request):
251
  """Test that pipeline result contains complete metadata."""
252
  result = audio_service.process_audio_pipeline(sample_processing_request)
253
+
254
  assert result.success is True
255
  assert result.metadata is not None
256
+
257
  expected_metadata_keys = [
258
+ 'correlation_id', 'asr_model', 'target_language',
259
  'voice', 'speed', 'translation_required'
260
  ]
261
+
262
  for key in expected_metadata_keys:
263
  assert key in result.metadata
264
 
265
  def test_pipeline_supported_configurations(self, audio_service):
266
  """Test retrieval of supported pipeline configurations."""
267
  config = audio_service.get_supported_configurations()
268
+
269
  assert 'asr_models' in config
270
  assert 'voices' in config
271
  assert 'languages' in config
272
  assert 'audio_formats' in config
273
  assert 'max_file_size_mb' in config
274
  assert 'speed_range' in config
275
+
276
  assert isinstance(config['asr_models'], list)
277
  assert isinstance(config['voices'], list)
278
  assert isinstance(config['languages'], list)
 
283
  """Test audio service as context manager."""
284
  with AudioProcessingApplicationService(mock_container, mock_config) as service:
285
  assert service is not None
286
+
287
  # Service should be usable within context
288
  config = service.get_supported_configurations()
289
  assert config is not None
 
301
  requires_translation=True
302
  )
303
  requests.append(request)
304
+
305
  results = []
306
  for request in requests:
307
  result = audio_service.process_audio_pipeline(request)
308
  results.append(result)
309
+
310
  # Verify all requests succeeded
311
  for result in results:
312
  assert result.success is True
313
  assert result.original_text is not None
314
  assert result.translated_text is not None
315
+
316
  # Verify each request has unique correlation ID
317
  correlation_ids = [r.metadata['correlation_id'] for r in results]
318
  assert len(set(correlation_ids)) == 3 # All unique
 
321
  """Test pipeline behavior under concurrent processing."""
322
  import threading
323
  import queue
324
+
325
  results_queue = queue.Queue()
326
+
327
  def process_request():
328
  try:
329
  result = audio_service.process_audio_pipeline(sample_processing_request)
330
  results_queue.put(result)
331
  except Exception as e:
332
  results_queue.put(e)
333
+
334
  # Start multiple threads
335
  threads = []
336
  for _ in range(3):
337
  thread = threading.Thread(target=process_request)
338
  threads.append(thread)
339
  thread.start()
340
+
341
  # Wait for completion
342
  for thread in threads:
343
  thread.join()
344
+
345
  # Verify all results
346
  results = []
347
  while not results_queue.empty():
 
349
  if isinstance(result, Exception):
350
  pytest.fail(f"Concurrent processing failed: {result}")
351
  results.append(result)
352
+
353
  assert len(results) == 3
354
  for result in results:
355
  assert result.success is True
 
358
  """Test pipeline memory usage and cleanup."""
359
  import psutil
360
  import os
361
+
362
  process = psutil.Process(os.getpid())
363
  memory_before = process.memory_info().rss
364
+
365
  # Process multiple requests
366
  for _ in range(5):
367
  result = audio_service.process_audio_pipeline(sample_processing_request)
368
  assert result.success is True
369
+
370
  memory_after = process.memory_info().rss
371
  memory_increase = memory_after - memory_before
372
+
373
  # Memory increase should be reasonable (less than 50MB for test data)
374
  assert memory_increase < 50 * 1024 * 1024
375
 
 
377
  """Test pipeline with streaming TTS synthesis."""
378
  # Mock streaming TTS provider
379
  mock_tts_provider = mock_container.get_tts_provider.return_value
380
+
381
  def mock_stream():
382
  for i in range(3):
383
  yield AudioContent(
 
386
  sample_rate=22050,
387
  duration=0.5
388
  )
389
+
390
  mock_tts_provider.synthesize_stream.return_value = mock_stream()
391
+
392
  result = audio_service.process_audio_pipeline(sample_processing_request)
393
+
394
  assert result.success is True
395
  assert result.audio_path is not None
396
 
397
  def test_pipeline_configuration_validation(self, audio_service):
398
  """Test pipeline configuration validation."""
399
  config = audio_service.get_supported_configurations()
400
+
401
  # Verify configuration structure
402
  assert isinstance(config['asr_models'], list)
403
  assert isinstance(config['voices'], list)
 
405
  assert isinstance(config['audio_formats'], list)
406
  assert isinstance(config['max_file_size_mb'], (int, float))
407
  assert isinstance(config['speed_range'], dict)
408
+
409
  # Verify speed range
410
  speed_range = config['speed_range']
411
  assert 'min' in speed_range
 
422
  SpeechRecognitionException("First attempt failed"),
423
  TextContent(text="Recovered transcription", language="en")
424
  ]
425
+
426
  with patch('src.application.services.audio_processing_service.logger') as mock_logger:
427
  result = audio_service.process_audio_pipeline(sample_processing_request)
428
+
429
  assert result.success is True
430
  # Verify error and recovery were logged
431
  mock_logger.warning.assert_called()
 
436
  start_time = time.time()
437
  result = audio_service.process_audio_pipeline(sample_processing_request)
438
  end_time = time.time()
439
+
440
  total_time = end_time - start_time
441
+
442
  assert result.success is True
443
  assert result.processing_time > 0
444
  assert result.processing_time <= total_time
445
+
446
  # For mock providers, processing should be fast
447
  assert total_time < 5.0 # Should complete within 5 seconds
448
+
449
  # Verify timing metadata
450
  assert 'correlation_id' in result.metadata
451
  timing_info = result.metadata
tests/integration/test_performance_and_errors.py CHANGED
@@ -33,7 +33,7 @@ class TestPerformanceAndErrors:
33
  def mock_config(self, tmp_path):
34
  """Create mock configuration for testing."""
35
  config = Mock(spec=AppConfig)
36
-
37
  # Processing configuration
38
  config.get_processing_config.return_value = {
39
  'max_file_size_mb': 100,
@@ -43,7 +43,7 @@ class TestPerformanceAndErrors:
43
  'processing_timeout': 300, # 5 minutes
44
  'max_concurrent_requests': 10
45
  }
46
-
47
  # Logging configuration
48
  config.get_logging_config.return_value = {
49
  'level': 'INFO',
@@ -51,28 +51,28 @@ class TestPerformanceAndErrors:
51
  'log_file_path': str(tmp_path / 'test.log'),
52
  'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
53
  }
54
-
55
  # STT configuration
56
  config.get_stt_config.return_value = {
57
- 'preferred_providers': ['whisper-small', 'whisper-medium', 'parakeet'],
58
  'provider_timeout': 60.0,
59
  'max_retries': 2
60
  }
61
-
62
  # TTS configuration
63
  config.get_tts_config.return_value = {
64
  'preferred_providers': ['kokoro', 'dia', 'cosyvoice2', 'dummy'],
65
  'provider_timeout': 30.0,
66
  'max_retries': 3
67
  }
68
-
69
  # Translation configuration
70
  config.get_translation_config.return_value = {
71
  'provider_timeout': 45.0,
72
  'max_retries': 2,
73
  'chunk_size': 512
74
  }
75
-
76
  return config
77
 
78
  @pytest.fixture
@@ -80,10 +80,10 @@ class TestPerformanceAndErrors:
80
  """Create mock dependency container."""
81
  container = Mock(spec=DependencyContainer)
82
  container.resolve.return_value = mock_config
83
-
84
  # Mock providers with configurable behavior
85
  self._setup_mock_providers(container)
86
-
87
  return container
88
 
89
  def _setup_mock_providers(self, container):
@@ -95,7 +95,7 @@ class TestPerformanceAndErrors:
95
  language="en"
96
  )
97
  container.get_stt_provider.return_value = mock_stt_provider
98
-
99
  # Mock translation provider
100
  mock_translation_provider = Mock()
101
  mock_translation_provider.translate.return_value = TextContent(
@@ -103,7 +103,7 @@ class TestPerformanceAndErrors:
103
  language="es"
104
  )
105
  container.get_translation_provider.return_value = mock_translation_provider
106
-
107
  # Mock TTS provider
108
  mock_tts_provider = Mock()
109
  mock_tts_provider.synthesize.return_value = AudioContent(
@@ -128,7 +128,7 @@ class TestPerformanceAndErrors:
128
  content_type="audio/wav",
129
  size=len(b"performance_test_audio_data")
130
  )
131
-
132
  return ProcessingRequestDto(
133
  audio=audio_upload,
134
  asr_model="whisper-small",
@@ -142,37 +142,37 @@ class TestPerformanceAndErrors:
142
  """Test processing time performance benchmarks."""
143
  # Warm up
144
  audio_service.process_audio_pipeline(sample_request)
145
-
146
  # Measure processing time
147
  start_time = time.time()
148
  result = audio_service.process_audio_pipeline(sample_request)
149
  end_time = time.time()
150
-
151
  processing_time = end_time - start_time
152
-
153
  assert result.success is True
154
  assert result.processing_time > 0
155
  assert result.processing_time <= processing_time + 0.1 # Allow small margin
156
-
157
  # Performance benchmark: should complete within reasonable time
158
  assert processing_time < 5.0 # Should complete within 5 seconds for mock providers
159
 
160
  def test_memory_usage_performance(self, audio_service, sample_request):
161
  """Test memory usage during processing."""
162
  process = psutil.Process(os.getpid())
163
-
164
  # Measure initial memory
165
  initial_memory = process.memory_info().rss
166
-
167
  # Process multiple requests
168
  for _ in range(10):
169
  result = audio_service.process_audio_pipeline(sample_request)
170
  assert result.success is True
171
-
172
  # Measure final memory
173
  final_memory = process.memory_info().rss
174
  memory_increase = final_memory - initial_memory
175
-
176
  # Memory increase should be reasonable (less than 100MB for test data)
177
  assert memory_increase < 100 * 1024 * 1024
178
 
@@ -180,7 +180,7 @@ class TestPerformanceAndErrors:
180
  """Test performance under concurrent load."""
181
  num_threads = 5
182
  results_queue = queue.Queue()
183
-
184
  def process_request():
185
  try:
186
  start_time = time.time()
@@ -189,26 +189,26 @@ class TestPerformanceAndErrors:
189
  results_queue.put((result, end_time - start_time))
190
  except Exception as e:
191
  results_queue.put(e)
192
-
193
  # Start concurrent processing
194
  threads = []
195
  start_time = time.time()
196
-
197
  for _ in range(num_threads):
198
  thread = threading.Thread(target=process_request)
199
  threads.append(thread)
200
  thread.start()
201
-
202
  # Wait for completion
203
  for thread in threads:
204
  thread.join()
205
-
206
  total_time = time.time() - start_time
207
-
208
  # Collect results
209
  results = []
210
  processing_times = []
211
-
212
  while not results_queue.empty():
213
  item = results_queue.get()
214
  if isinstance(item, Exception):
@@ -216,12 +216,12 @@ class TestPerformanceAndErrors:
216
  result, proc_time = item
217
  results.append(result)
218
  processing_times.append(proc_time)
219
-
220
  # Verify all succeeded
221
  assert len(results) == num_threads
222
  for result in results:
223
  assert result.success is True
224
-
225
  # Performance checks
226
  avg_processing_time = sum(processing_times) / len(processing_times)
227
  assert avg_processing_time < 10.0 # Average should be reasonable
@@ -231,14 +231,14 @@ class TestPerformanceAndErrors:
231
  """Test performance with large audio files."""
232
  # Create large audio file (10MB)
233
  large_content = b"x" * (10 * 1024 * 1024)
234
-
235
  audio_upload = AudioUploadDto(
236
  filename="large_performance_test.wav",
237
  content=large_content,
238
  content_type="audio/wav",
239
  size=len(large_content)
240
  )
241
-
242
  request = ProcessingRequestDto(
243
  audio=audio_upload,
244
  asr_model="whisper-small",
@@ -247,13 +247,13 @@ class TestPerformanceAndErrors:
247
  speed=1.0,
248
  requires_translation=True
249
  )
250
-
251
  start_time = time.time()
252
  result = audio_service.process_audio_pipeline(request)
253
  end_time = time.time()
254
-
255
  processing_time = end_time - start_time
256
-
257
  assert result.success is True
258
  # Large files should still complete within reasonable time
259
  assert processing_time < 30.0
@@ -261,37 +261,37 @@ class TestPerformanceAndErrors:
261
  def test_stt_provider_failure_recovery(self, audio_service, sample_request, mock_container):
262
  """Test recovery from STT provider failures."""
263
  mock_stt_provider = mock_container.get_stt_provider.return_value
264
-
265
  # Mock first call to fail, second to succeed
266
  mock_stt_provider.transcribe.side_effect = [
267
  SpeechRecognitionException("STT provider temporarily unavailable"),
268
  TextContent(text="Recovered transcription", language="en")
269
  ]
270
-
271
  result = audio_service.process_audio_pipeline(sample_request)
272
-
273
  assert result.success is True
274
  assert "Recovered transcription" in result.original_text
275
 
276
  def test_translation_provider_failure_recovery(self, audio_service, sample_request, mock_container):
277
  """Test recovery from translation provider failures."""
278
  mock_translation_provider = mock_container.get_translation_provider.return_value
279
-
280
  # Mock first call to fail, second to succeed
281
  mock_translation_provider.translate.side_effect = [
282
  TranslationFailedException("Translation service temporarily unavailable"),
283
  TextContent(text="Traducción recuperada", language="es")
284
  ]
285
-
286
  result = audio_service.process_audio_pipeline(sample_request)
287
-
288
  assert result.success is True
289
  assert "Traducción recuperada" in result.translated_text
290
 
291
  def test_tts_provider_failure_recovery(self, audio_service, sample_request, mock_container):
292
  """Test recovery from TTS provider failures."""
293
  mock_tts_provider = mock_container.get_tts_provider.return_value
294
-
295
  # Mock first call to fail, second to succeed
296
  mock_tts_provider.synthesize.side_effect = [
297
  SpeechSynthesisException("TTS provider temporarily unavailable"),
@@ -302,9 +302,9 @@ class TestPerformanceAndErrors:
302
  duration=2.5
303
  )
304
  ]
305
-
306
  result = audio_service.process_audio_pipeline(sample_request)
307
-
308
  assert result.success is True
309
  assert result.audio_path is not None
310
 
@@ -314,13 +314,13 @@ class TestPerformanceAndErrors:
314
  mock_stt_provider = mock_container.get_stt_provider.return_value
315
  mock_translation_provider = mock_container.get_translation_provider.return_value
316
  mock_tts_provider = mock_container.get_tts_provider.return_value
317
-
318
  mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT failed")
319
  mock_translation_provider.translate.side_effect = TranslationFailedException("Translation failed")
320
  mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("TTS failed")
321
-
322
  result = audio_service.process_audio_pipeline(sample_request)
323
-
324
  assert result.success is False
325
  assert result.error_message is not None
326
  assert result.error_code is not None
@@ -328,19 +328,19 @@ class TestPerformanceAndErrors:
328
  def test_timeout_handling(self, audio_service, sample_request, mock_container):
329
  """Test handling of provider timeouts."""
330
  mock_stt_provider = mock_container.get_stt_provider.return_value
331
-
332
  def slow_transcribe(*args, **kwargs):
333
  time.sleep(2.0) # Simulate slow processing
334
  return TextContent(text="Slow transcription", language="en")
335
-
336
  mock_stt_provider.transcribe.side_effect = slow_transcribe
337
-
338
  start_time = time.time()
339
  result = audio_service.process_audio_pipeline(sample_request)
340
  end_time = time.time()
341
-
342
  processing_time = end_time - start_time
343
-
344
  # Should complete despite slow provider
345
  assert result.success is True
346
  assert processing_time >= 2.0 # Should include the delay
@@ -354,7 +354,7 @@ class TestPerformanceAndErrors:
354
  content_type="audio/xyz",
355
  size=len(b"invalid_audio_data")
356
  )
357
-
358
  request = ProcessingRequestDto(
359
  audio=invalid_audio,
360
  asr_model="whisper-small",
@@ -363,9 +363,9 @@ class TestPerformanceAndErrors:
363
  speed=1.0,
364
  requires_translation=True
365
  )
366
-
367
  result = audio_service.process_audio_pipeline(request)
368
-
369
  assert result.success is False
370
  assert result.error_code is not None
371
  assert "format" in result.error_message.lower() or "unsupported" in result.error_message.lower()
@@ -374,17 +374,17 @@ class TestPerformanceAndErrors:
374
  """Test handling of oversized files."""
375
  # Mock config to have small file size limit
376
  mock_config.get_processing_config.return_value['max_file_size_mb'] = 1
377
-
378
  # Create file larger than limit
379
  large_content = b"x" * (2 * 1024 * 1024) # 2MB
380
-
381
  oversized_audio = AudioUploadDto(
382
  filename="oversized.wav",
383
  content=large_content,
384
  content_type="audio/wav",
385
  size=len(large_content)
386
  )
387
-
388
  request = ProcessingRequestDto(
389
  audio=oversized_audio,
390
  asr_model="whisper-small",
@@ -393,9 +393,9 @@ class TestPerformanceAndErrors:
393
  speed=1.0,
394
  requires_translation=True
395
  )
396
-
397
  result = audio_service.process_audio_pipeline(request)
398
-
399
  assert result.success is False
400
  assert result.error_code is not None
401
  assert "size" in result.error_message.lower() or "large" in result.error_message.lower()
@@ -408,7 +408,7 @@ class TestPerformanceAndErrors:
408
  content_type="audio/wav",
409
  size=len(b"corrupted_data_not_audio")
410
  )
411
-
412
  request = ProcessingRequestDto(
413
  audio=corrupted_audio,
414
  asr_model="whisper-small",
@@ -417,25 +417,25 @@ class TestPerformanceAndErrors:
417
  speed=1.0,
418
  requires_translation=True
419
  )
420
-
421
  result = audio_service.process_audio_pipeline(request)
422
-
423
  # Should handle gracefully (success depends on implementation)
424
  assert result.error_message is None or "audio" in result.error_message.lower()
425
 
426
  def test_network_error_simulation(self, audio_service, sample_request, mock_container):
427
  """Test handling of network-related errors."""
428
  mock_translation_provider = mock_container.get_translation_provider.return_value
429
-
430
  # Simulate network errors
431
  mock_translation_provider.translate.side_effect = [
432
  ConnectionError("Network connection failed"),
433
  TimeoutError("Request timed out"),
434
  TextContent(text="Network recovered translation", language="es")
435
  ]
436
-
437
  result = audio_service.process_audio_pipeline(sample_request)
438
-
439
  # Should recover from network errors
440
  assert result.success is True
441
  assert "Network recovered translation" in result.translated_text
@@ -444,14 +444,14 @@ class TestPerformanceAndErrors:
444
  """Test handling of resource exhaustion scenarios."""
445
  # Simulate memory pressure by processing many requests
446
  results = []
447
-
448
  for i in range(20): # Process many requests
449
  result = audio_service.process_audio_pipeline(sample_request)
450
  results.append(result)
451
-
452
  # All should succeed despite resource pressure
453
  assert result.success is True
454
-
455
  # Verify all completed successfully
456
  assert len(results) == 20
457
  for result in results:
@@ -461,13 +461,13 @@ class TestPerformanceAndErrors:
461
  """Test error correlation tracking across pipeline stages."""
462
  mock_stt_provider = mock_container.get_stt_provider.return_value
463
  mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT correlation test error")
464
-
465
  result = audio_service.process_audio_pipeline(sample_request)
466
-
467
  assert result.success is False
468
  assert result.metadata is not None
469
  assert 'correlation_id' in result.metadata
470
-
471
  # Verify correlation ID is consistent
472
  correlation_id = result.metadata['correlation_id']
473
  assert isinstance(correlation_id, str)
@@ -478,13 +478,13 @@ class TestPerformanceAndErrors:
478
  # Mock translation to fail but allow STT and TTS to succeed
479
  mock_translation_provider = mock_container.get_translation_provider.return_value
480
  mock_translation_provider.translate.side_effect = TranslationFailedException("Translation unavailable")
481
-
482
  # Modify request to not require translation
483
  sample_request.requires_translation = False
484
  sample_request.target_language = "en" # Same as source
485
-
486
  result = audio_service.process_audio_pipeline(sample_request)
487
-
488
  # Should succeed without translation
489
  assert result.success is True
490
  assert result.translated_text is None # No translation performed
@@ -492,15 +492,15 @@ class TestPerformanceAndErrors:
492
  def test_circuit_breaker_behavior(self, audio_service, sample_request, mock_container):
493
  """Test circuit breaker behavior under repeated failures."""
494
  mock_tts_provider = mock_container.get_tts_provider.return_value
495
-
496
  # Mock repeated failures to trigger circuit breaker
497
  mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("Repeated TTS failure")
498
-
499
  results = []
500
  for _ in range(5): # Multiple attempts
501
  result = audio_service.process_audio_pipeline(sample_request)
502
  results.append(result)
503
-
504
  # All should fail, but circuit breaker should prevent excessive retries
505
  for result in results:
506
  assert result.success is False
@@ -509,11 +509,11 @@ class TestPerformanceAndErrors:
509
  def test_performance_metrics_collection(self, audio_service, sample_request):
510
  """Test collection of performance metrics."""
511
  result = audio_service.process_audio_pipeline(sample_request)
512
-
513
  assert result.success is True
514
  assert result.processing_time > 0
515
  assert result.metadata is not None
516
-
517
  # Verify performance-related metadata
518
  metadata = result.metadata
519
  assert 'correlation_id' in metadata
@@ -525,26 +525,26 @@ class TestPerformanceAndErrors:
525
  """Test system behavior under stress conditions."""
526
  num_requests = 50
527
  results = []
528
-
529
  start_time = time.time()
530
-
531
  for i in range(num_requests):
532
  result = audio_service.process_audio_pipeline(sample_request)
533
  results.append(result)
534
-
535
  end_time = time.time()
536
  total_time = end_time - start_time
537
-
538
  # Verify all requests completed
539
  assert len(results) == num_requests
540
-
541
  # Calculate success rate
542
  successful_results = [r for r in results if r.success]
543
  success_rate = len(successful_results) / len(results)
544
-
545
  # Should maintain high success rate under stress
546
  assert success_rate >= 0.95 # At least 95% success rate
547
-
548
  # Performance should remain reasonable
549
  avg_time_per_request = total_time / num_requests
550
  assert avg_time_per_request < 1.0 # Average less than 1 second per request
 
33
  def mock_config(self, tmp_path):
34
  """Create mock configuration for testing."""
35
  config = Mock(spec=AppConfig)
36
+
37
  # Processing configuration
38
  config.get_processing_config.return_value = {
39
  'max_file_size_mb': 100,
 
43
  'processing_timeout': 300, # 5 minutes
44
  'max_concurrent_requests': 10
45
  }
46
+
47
  # Logging configuration
48
  config.get_logging_config.return_value = {
49
  'level': 'INFO',
 
51
  'log_file_path': str(tmp_path / 'test.log'),
52
  'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
53
  }
54
+
55
  # STT configuration
56
  config.get_stt_config.return_value = {
57
+ 'preferred_providers': ['parakeet', 'whisper-small', 'whisper-medium'],
58
  'provider_timeout': 60.0,
59
  'max_retries': 2
60
  }
61
+
62
  # TTS configuration
63
  config.get_tts_config.return_value = {
64
  'preferred_providers': ['kokoro', 'dia', 'cosyvoice2', 'dummy'],
65
  'provider_timeout': 30.0,
66
  'max_retries': 3
67
  }
68
+
69
  # Translation configuration
70
  config.get_translation_config.return_value = {
71
  'provider_timeout': 45.0,
72
  'max_retries': 2,
73
  'chunk_size': 512
74
  }
75
+
76
  return config
77
 
78
  @pytest.fixture
 
80
  """Create mock dependency container."""
81
  container = Mock(spec=DependencyContainer)
82
  container.resolve.return_value = mock_config
83
+
84
  # Mock providers with configurable behavior
85
  self._setup_mock_providers(container)
86
+
87
  return container
88
 
89
  def _setup_mock_providers(self, container):
 
95
  language="en"
96
  )
97
  container.get_stt_provider.return_value = mock_stt_provider
98
+
99
  # Mock translation provider
100
  mock_translation_provider = Mock()
101
  mock_translation_provider.translate.return_value = TextContent(
 
103
  language="es"
104
  )
105
  container.get_translation_provider.return_value = mock_translation_provider
106
+
107
  # Mock TTS provider
108
  mock_tts_provider = Mock()
109
  mock_tts_provider.synthesize.return_value = AudioContent(
 
128
  content_type="audio/wav",
129
  size=len(b"performance_test_audio_data")
130
  )
131
+
132
  return ProcessingRequestDto(
133
  audio=audio_upload,
134
  asr_model="whisper-small",
 
142
  """Test processing time performance benchmarks."""
143
  # Warm up
144
  audio_service.process_audio_pipeline(sample_request)
145
+
146
  # Measure processing time
147
  start_time = time.time()
148
  result = audio_service.process_audio_pipeline(sample_request)
149
  end_time = time.time()
150
+
151
  processing_time = end_time - start_time
152
+
153
  assert result.success is True
154
  assert result.processing_time > 0
155
  assert result.processing_time <= processing_time + 0.1 # Allow small margin
156
+
157
  # Performance benchmark: should complete within reasonable time
158
  assert processing_time < 5.0 # Should complete within 5 seconds for mock providers
159
 
160
  def test_memory_usage_performance(self, audio_service, sample_request):
161
  """Test memory usage during processing."""
162
  process = psutil.Process(os.getpid())
163
+
164
  # Measure initial memory
165
  initial_memory = process.memory_info().rss
166
+
167
  # Process multiple requests
168
  for _ in range(10):
169
  result = audio_service.process_audio_pipeline(sample_request)
170
  assert result.success is True
171
+
172
  # Measure final memory
173
  final_memory = process.memory_info().rss
174
  memory_increase = final_memory - initial_memory
175
+
176
  # Memory increase should be reasonable (less than 100MB for test data)
177
  assert memory_increase < 100 * 1024 * 1024
178
 
 
180
  """Test performance under concurrent load."""
181
  num_threads = 5
182
  results_queue = queue.Queue()
183
+
184
  def process_request():
185
  try:
186
  start_time = time.time()
 
189
  results_queue.put((result, end_time - start_time))
190
  except Exception as e:
191
  results_queue.put(e)
192
+
193
  # Start concurrent processing
194
  threads = []
195
  start_time = time.time()
196
+
197
  for _ in range(num_threads):
198
  thread = threading.Thread(target=process_request)
199
  threads.append(thread)
200
  thread.start()
201
+
202
  # Wait for completion
203
  for thread in threads:
204
  thread.join()
205
+
206
  total_time = time.time() - start_time
207
+
208
  # Collect results
209
  results = []
210
  processing_times = []
211
+
212
  while not results_queue.empty():
213
  item = results_queue.get()
214
  if isinstance(item, Exception):
 
216
  result, proc_time = item
217
  results.append(result)
218
  processing_times.append(proc_time)
219
+
220
  # Verify all succeeded
221
  assert len(results) == num_threads
222
  for result in results:
223
  assert result.success is True
224
+
225
  # Performance checks
226
  avg_processing_time = sum(processing_times) / len(processing_times)
227
  assert avg_processing_time < 10.0 # Average should be reasonable
 
231
  """Test performance with large audio files."""
232
  # Create large audio file (10MB)
233
  large_content = b"x" * (10 * 1024 * 1024)
234
+
235
  audio_upload = AudioUploadDto(
236
  filename="large_performance_test.wav",
237
  content=large_content,
238
  content_type="audio/wav",
239
  size=len(large_content)
240
  )
241
+
242
  request = ProcessingRequestDto(
243
  audio=audio_upload,
244
  asr_model="whisper-small",
 
247
  speed=1.0,
248
  requires_translation=True
249
  )
250
+
251
  start_time = time.time()
252
  result = audio_service.process_audio_pipeline(request)
253
  end_time = time.time()
254
+
255
  processing_time = end_time - start_time
256
+
257
  assert result.success is True
258
  # Large files should still complete within reasonable time
259
  assert processing_time < 30.0
 
261
  def test_stt_provider_failure_recovery(self, audio_service, sample_request, mock_container):
262
  """Test recovery from STT provider failures."""
263
  mock_stt_provider = mock_container.get_stt_provider.return_value
264
+
265
  # Mock first call to fail, second to succeed
266
  mock_stt_provider.transcribe.side_effect = [
267
  SpeechRecognitionException("STT provider temporarily unavailable"),
268
  TextContent(text="Recovered transcription", language="en")
269
  ]
270
+
271
  result = audio_service.process_audio_pipeline(sample_request)
272
+
273
  assert result.success is True
274
  assert "Recovered transcription" in result.original_text
275
 
276
  def test_translation_provider_failure_recovery(self, audio_service, sample_request, mock_container):
277
  """Test recovery from translation provider failures."""
278
  mock_translation_provider = mock_container.get_translation_provider.return_value
279
+
280
  # Mock first call to fail, second to succeed
281
  mock_translation_provider.translate.side_effect = [
282
  TranslationFailedException("Translation service temporarily unavailable"),
283
  TextContent(text="Traducción recuperada", language="es")
284
  ]
285
+
286
  result = audio_service.process_audio_pipeline(sample_request)
287
+
288
  assert result.success is True
289
  assert "Traducción recuperada" in result.translated_text
290
 
291
  def test_tts_provider_failure_recovery(self, audio_service, sample_request, mock_container):
292
  """Test recovery from TTS provider failures."""
293
  mock_tts_provider = mock_container.get_tts_provider.return_value
294
+
295
  # Mock first call to fail, second to succeed
296
  mock_tts_provider.synthesize.side_effect = [
297
  SpeechSynthesisException("TTS provider temporarily unavailable"),
 
302
  duration=2.5
303
  )
304
  ]
305
+
306
  result = audio_service.process_audio_pipeline(sample_request)
307
+
308
  assert result.success is True
309
  assert result.audio_path is not None
310
 
 
314
  mock_stt_provider = mock_container.get_stt_provider.return_value
315
  mock_translation_provider = mock_container.get_translation_provider.return_value
316
  mock_tts_provider = mock_container.get_tts_provider.return_value
317
+
318
  mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT failed")
319
  mock_translation_provider.translate.side_effect = TranslationFailedException("Translation failed")
320
  mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("TTS failed")
321
+
322
  result = audio_service.process_audio_pipeline(sample_request)
323
+
324
  assert result.success is False
325
  assert result.error_message is not None
326
  assert result.error_code is not None
 
328
  def test_timeout_handling(self, audio_service, sample_request, mock_container):
329
  """Test handling of provider timeouts."""
330
  mock_stt_provider = mock_container.get_stt_provider.return_value
331
+
332
  def slow_transcribe(*args, **kwargs):
333
  time.sleep(2.0) # Simulate slow processing
334
  return TextContent(text="Slow transcription", language="en")
335
+
336
  mock_stt_provider.transcribe.side_effect = slow_transcribe
337
+
338
  start_time = time.time()
339
  result = audio_service.process_audio_pipeline(sample_request)
340
  end_time = time.time()
341
+
342
  processing_time = end_time - start_time
343
+
344
  # Should complete despite slow provider
345
  assert result.success is True
346
  assert processing_time >= 2.0 # Should include the delay
 
354
  content_type="audio/xyz",
355
  size=len(b"invalid_audio_data")
356
  )
357
+
358
  request = ProcessingRequestDto(
359
  audio=invalid_audio,
360
  asr_model="whisper-small",
 
363
  speed=1.0,
364
  requires_translation=True
365
  )
366
+
367
  result = audio_service.process_audio_pipeline(request)
368
+
369
  assert result.success is False
370
  assert result.error_code is not None
371
  assert "format" in result.error_message.lower() or "unsupported" in result.error_message.lower()
 
374
  """Test handling of oversized files."""
375
  # Mock config to have small file size limit
376
  mock_config.get_processing_config.return_value['max_file_size_mb'] = 1
377
+
378
  # Create file larger than limit
379
  large_content = b"x" * (2 * 1024 * 1024) # 2MB
380
+
381
  oversized_audio = AudioUploadDto(
382
  filename="oversized.wav",
383
  content=large_content,
384
  content_type="audio/wav",
385
  size=len(large_content)
386
  )
387
+
388
  request = ProcessingRequestDto(
389
  audio=oversized_audio,
390
  asr_model="whisper-small",
 
393
  speed=1.0,
394
  requires_translation=True
395
  )
396
+
397
  result = audio_service.process_audio_pipeline(request)
398
+
399
  assert result.success is False
400
  assert result.error_code is not None
401
  assert "size" in result.error_message.lower() or "large" in result.error_message.lower()
 
408
  content_type="audio/wav",
409
  size=len(b"corrupted_data_not_audio")
410
  )
411
+
412
  request = ProcessingRequestDto(
413
  audio=corrupted_audio,
414
  asr_model="whisper-small",
 
417
  speed=1.0,
418
  requires_translation=True
419
  )
420
+
421
  result = audio_service.process_audio_pipeline(request)
422
+
423
  # Should handle gracefully (success depends on implementation)
424
  assert result.error_message is None or "audio" in result.error_message.lower()
425
 
426
  def test_network_error_simulation(self, audio_service, sample_request, mock_container):
427
  """Test handling of network-related errors."""
428
  mock_translation_provider = mock_container.get_translation_provider.return_value
429
+
430
  # Simulate network errors
431
  mock_translation_provider.translate.side_effect = [
432
  ConnectionError("Network connection failed"),
433
  TimeoutError("Request timed out"),
434
  TextContent(text="Network recovered translation", language="es")
435
  ]
436
+
437
  result = audio_service.process_audio_pipeline(sample_request)
438
+
439
  # Should recover from network errors
440
  assert result.success is True
441
  assert "Network recovered translation" in result.translated_text
 
444
  """Test handling of resource exhaustion scenarios."""
445
  # Simulate memory pressure by processing many requests
446
  results = []
447
+
448
  for i in range(20): # Process many requests
449
  result = audio_service.process_audio_pipeline(sample_request)
450
  results.append(result)
451
+
452
  # All should succeed despite resource pressure
453
  assert result.success is True
454
+
455
  # Verify all completed successfully
456
  assert len(results) == 20
457
  for result in results:
 
461
  """Test error correlation tracking across pipeline stages."""
462
  mock_stt_provider = mock_container.get_stt_provider.return_value
463
  mock_stt_provider.transcribe.side_effect = SpeechRecognitionException("STT correlation test error")
464
+
465
  result = audio_service.process_audio_pipeline(sample_request)
466
+
467
  assert result.success is False
468
  assert result.metadata is not None
469
  assert 'correlation_id' in result.metadata
470
+
471
  # Verify correlation ID is consistent
472
  correlation_id = result.metadata['correlation_id']
473
  assert isinstance(correlation_id, str)
 
478
  # Mock translation to fail but allow STT and TTS to succeed
479
  mock_translation_provider = mock_container.get_translation_provider.return_value
480
  mock_translation_provider.translate.side_effect = TranslationFailedException("Translation unavailable")
481
+
482
  # Modify request to not require translation
483
  sample_request.requires_translation = False
484
  sample_request.target_language = "en" # Same as source
485
+
486
  result = audio_service.process_audio_pipeline(sample_request)
487
+
488
  # Should succeed without translation
489
  assert result.success is True
490
  assert result.translated_text is None # No translation performed
 
492
  def test_circuit_breaker_behavior(self, audio_service, sample_request, mock_container):
493
  """Test circuit breaker behavior under repeated failures."""
494
  mock_tts_provider = mock_container.get_tts_provider.return_value
495
+
496
  # Mock repeated failures to trigger circuit breaker
497
  mock_tts_provider.synthesize.side_effect = SpeechSynthesisException("Repeated TTS failure")
498
+
499
  results = []
500
  for _ in range(5): # Multiple attempts
501
  result = audio_service.process_audio_pipeline(sample_request)
502
  results.append(result)
503
+
504
  # All should fail, but circuit breaker should prevent excessive retries
505
  for result in results:
506
  assert result.success is False
 
509
  def test_performance_metrics_collection(self, audio_service, sample_request):
510
  """Test collection of performance metrics."""
511
  result = audio_service.process_audio_pipeline(sample_request)
512
+
513
  assert result.success is True
514
  assert result.processing_time > 0
515
  assert result.metadata is not None
516
+
517
  # Verify performance-related metadata
518
  metadata = result.metadata
519
  assert 'correlation_id' in metadata
 
525
  """Test system behavior under stress conditions."""
526
  num_requests = 50
527
  results = []
528
+
529
  start_time = time.time()
530
+
531
  for i in range(num_requests):
532
  result = audio_service.process_audio_pipeline(sample_request)
533
  results.append(result)
534
+
535
  end_time = time.time()
536
  total_time = end_time - start_time
537
+
538
  # Verify all requests completed
539
  assert len(results) == num_requests
540
+
541
  # Calculate success rate
542
  successful_results = [r for r in results if r.success]
543
  success_rate = len(successful_results) / len(results)
544
+
545
  # Should maintain high success rate under stress
546
  assert success_rate >= 0.95 # At least 95% success rate
547
+
548
  # Performance should remain reasonable
549
  avg_time_per_request = total_time / num_requests
550
  assert avg_time_per_request < 1.0 # Average less than 1 second per request