Spaces:
Build error
Build error
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'
|
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'
|
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'
|
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'
|
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
|