Michael Hu commited on
Commit
111538d
Β·
1 Parent(s): 48ba7e8

Implement application DTOs

Browse files
src/application/dtos/__init__.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Application Data Transfer Objects (DTOs)
2
+
3
+ This module contains DTOs used for data transfer between layers.
4
+ DTOs handle serialization/deserialization and validation of data
5
+ crossing layer boundaries.
6
+ """
7
+
8
+ from .audio_upload_dto import AudioUploadDto
9
+ from .processing_request_dto import ProcessingRequestDto
10
+ from .processing_result_dto import ProcessingResultDto
11
+ from .dto_validation import validate_dto, ValidationError
12
+
13
+ __all__ = [
14
+ 'AudioUploadDto',
15
+ 'ProcessingRequestDto',
16
+ 'ProcessingResultDto',
17
+ 'validate_dto',
18
+ 'ValidationError'
19
+ ]
src/application/dtos/audio_upload_dto.py ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Audio Upload Data Transfer Object"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+ import mimetypes
6
+ import os
7
+
8
+
9
+ @dataclass
10
+ class AudioUploadDto:
11
+ """DTO for file upload data
12
+
13
+ Handles audio file upload information including filename,
14
+ content, and content type validation.
15
+ """
16
+ filename: str
17
+ content: bytes
18
+ content_type: str
19
+ size: Optional[int] = None
20
+
21
+ def __post_init__(self):
22
+ """Validate the DTO after initialization"""
23
+ self._validate()
24
+ if self.size is None:
25
+ self.size = len(self.content)
26
+
27
+ def _validate(self):
28
+ """Validate audio upload data"""
29
+ if not self.filename:
30
+ raise ValueError("Filename cannot be empty")
31
+
32
+ if not self.content:
33
+ raise ValueError("Audio content cannot be empty")
34
+
35
+ if not self.content_type:
36
+ raise ValueError("Content type cannot be empty")
37
+
38
+ # Validate file extension
39
+ _, ext = os.path.splitext(self.filename.lower())
40
+ supported_extensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg']
41
+ if ext not in supported_extensions:
42
+ raise ValueError(f"Unsupported file extension: {ext}. Supported: {supported_extensions}")
43
+
44
+ # Validate content type
45
+ expected_content_type = mimetypes.guess_type(self.filename)[0]
46
+ if expected_content_type and not self.content_type.startswith('audio/'):
47
+ raise ValueError(f"Invalid content type: {self.content_type}. Expected audio/* type")
48
+
49
+ # Validate file size (max 100MB)
50
+ max_size = 100 * 1024 * 1024 # 100MB
51
+ if len(self.content) > max_size:
52
+ raise ValueError(f"File too large: {len(self.content)} bytes. Maximum: {max_size} bytes")
53
+
54
+ # Validate minimum file size (at least 1KB)
55
+ min_size = 1024 # 1KB
56
+ if len(self.content) < min_size:
57
+ raise ValueError(f"File too small: {len(self.content)} bytes. Minimum: {min_size} bytes")
58
+
59
+ @property
60
+ def file_extension(self) -> str:
61
+ """Get the file extension"""
62
+ return os.path.splitext(self.filename.lower())[1]
63
+
64
+ @property
65
+ def base_filename(self) -> str:
66
+ """Get filename without extension"""
67
+ return os.path.splitext(self.filename)[0]
68
+
69
+ def to_dict(self) -> dict:
70
+ """Convert to dictionary representation"""
71
+ return {
72
+ 'filename': self.filename,
73
+ 'content_type': self.content_type,
74
+ 'size': self.size,
75
+ 'file_extension': self.file_extension
76
+ }
src/application/dtos/dto_validation.py ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DTO Validation Utilities
2
+
3
+ Provides validation functions and utilities for DTOs,
4
+ including custom validation decorators and error handling.
5
+ """
6
+
7
+ from typing import Any, Callable, TypeVar, Union
8
+ from functools import wraps
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ T = TypeVar('T')
14
+
15
+
16
+ class ValidationError(Exception):
17
+ """Custom exception for DTO validation errors"""
18
+
19
+ def __init__(self, message: str, field: str = None, value: Any = None):
20
+ self.message = message
21
+ self.field = field
22
+ self.value = value
23
+ super().__init__(self.message)
24
+
25
+ def __str__(self):
26
+ if self.field:
27
+ return f"Validation error for field '{self.field}': {self.message}"
28
+ return f"Validation error: {self.message}"
29
+
30
+
31
+ def validate_dto(dto_instance: Any) -> bool:
32
+ """Validate a DTO instance
33
+
34
+ Args:
35
+ dto_instance: The DTO instance to validate
36
+
37
+ Returns:
38
+ bool: True if validation passes
39
+
40
+ Raises:
41
+ ValidationError: If validation fails
42
+ """
43
+ try:
44
+ # Call the DTO's validation method if it exists
45
+ if hasattr(dto_instance, '_validate'):
46
+ dto_instance._validate()
47
+
48
+ # Additional validation can be added here
49
+ logger.debug(f"Successfully validated {type(dto_instance).__name__}")
50
+ return True
51
+
52
+ except ValueError as e:
53
+ logger.error(f"Validation failed for {type(dto_instance).__name__}: {e}")
54
+ raise ValidationError(str(e)) from e
55
+ except Exception as e:
56
+ logger.error(f"Unexpected error during validation of {type(dto_instance).__name__}: {e}")
57
+ raise ValidationError(f"Validation failed: {e}") from e
58
+
59
+
60
+ def validation_required(func: Callable[..., T]) -> Callable[..., T]:
61
+ """Decorator to ensure DTO validation before method execution
62
+
63
+ Args:
64
+ func: The method to decorate
65
+
66
+ Returns:
67
+ Decorated function that validates 'self' before execution
68
+ """
69
+ @wraps(func)
70
+ def wrapper(self, *args, **kwargs):
71
+ try:
72
+ validate_dto(self)
73
+ return func(self, *args, **kwargs)
74
+ except ValidationError:
75
+ raise
76
+ except Exception as e:
77
+ raise ValidationError(f"Error in {func.__name__}: {e}") from e
78
+
79
+ return wrapper
80
+
81
+
82
+ def validate_field(value: Any, field_name: str, validator: Callable[[Any], bool],
83
+ error_message: str = None) -> Any:
84
+ """Validate a single field value
85
+
86
+ Args:
87
+ value: The value to validate
88
+ field_name: Name of the field being validated
89
+ validator: Function that returns True if value is valid
90
+ error_message: Custom error message
91
+
92
+ Returns:
93
+ The validated value
94
+
95
+ Raises:
96
+ ValidationError: If validation fails
97
+ """
98
+ try:
99
+ if not validator(value):
100
+ message = error_message or f"Invalid value for field '{field_name}'"
101
+ raise ValidationError(message, field_name, value)
102
+ return value
103
+ except ValidationError:
104
+ raise
105
+ except Exception as e:
106
+ raise ValidationError(f"Validation error for field '{field_name}': {e}", field_name, value) from e
107
+
108
+
109
+ def validate_required(value: Any, field_name: str) -> Any:
110
+ """Validate that a field is not None or empty
111
+
112
+ Args:
113
+ value: The value to validate
114
+ field_name: Name of the field being validated
115
+
116
+ Returns:
117
+ The validated value
118
+
119
+ Raises:
120
+ ValidationError: If field is None or empty
121
+ """
122
+ if value is None:
123
+ raise ValidationError(f"Field '{field_name}' is required", field_name, value)
124
+
125
+ if isinstance(value, (str, list, dict)) and len(value) == 0:
126
+ raise ValidationError(f"Field '{field_name}' cannot be empty", field_name, value)
127
+
128
+ return value
129
+
130
+
131
+ def validate_type(value: Any, field_name: str, expected_type: Union[type, tuple]) -> Any:
132
+ """Validate that a field is of the expected type
133
+
134
+ Args:
135
+ value: The value to validate
136
+ field_name: Name of the field being validated
137
+ expected_type: Expected type or tuple of types
138
+
139
+ Returns:
140
+ The validated value
141
+
142
+ Raises:
143
+ ValidationError: If type doesn't match
144
+ """
145
+ if not isinstance(value, expected_type):
146
+ if isinstance(expected_type, tuple):
147
+ type_names = [t.__name__ for t in expected_type]
148
+ expected_str = " or ".join(type_names)
149
+ else:
150
+ expected_str = expected_type.__name__
151
+
152
+ actual_type = type(value).__name__
153
+ raise ValidationError(
154
+ f"Field '{field_name}' must be of type {expected_str}, got {actual_type}",
155
+ field_name, value
156
+ )
157
+
158
+ return value
159
+
160
+
161
+ def validate_range(value: Union[int, float], field_name: str,
162
+ min_value: Union[int, float] = None,
163
+ max_value: Union[int, float] = None) -> Union[int, float]:
164
+ """Validate that a numeric value is within a specified range
165
+
166
+ Args:
167
+ value: The numeric value to validate
168
+ field_name: Name of the field being validated
169
+ min_value: Minimum allowed value (inclusive)
170
+ max_value: Maximum allowed value (inclusive)
171
+
172
+ Returns:
173
+ The validated value
174
+
175
+ Raises:
176
+ ValidationError: If value is outside the range
177
+ """
178
+ if min_value is not None and value < min_value:
179
+ raise ValidationError(
180
+ f"Field '{field_name}' must be >= {min_value}, got {value}",
181
+ field_name, value
182
+ )
183
+
184
+ if max_value is not None and value > max_value:
185
+ raise ValidationError(
186
+ f"Field '{field_name}' must be <= {max_value}, got {value}",
187
+ field_name, value
188
+ )
189
+
190
+ return value
191
+
192
+
193
+ def validate_choices(value: Any, field_name: str, choices: list) -> Any:
194
+ """Validate that a value is one of the allowed choices
195
+
196
+ Args:
197
+ value: The value to validate
198
+ field_name: Name of the field being validated
199
+ choices: List of allowed values
200
+
201
+ Returns:
202
+ The validated value
203
+
204
+ Raises:
205
+ ValidationError: If value is not in choices
206
+ """
207
+ if value not in choices:
208
+ raise ValidationError(
209
+ f"Field '{field_name}' must be one of {choices}, got '{value}'",
210
+ field_name, value
211
+ )
212
+
213
+ return value
src/application/dtos/processing_request_dto.py ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Processing Request Data Transfer Object"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Dict, Any
5
+ from .audio_upload_dto import AudioUploadDto
6
+
7
+
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
+ """
15
+ audio: AudioUploadDto
16
+ asr_model: str
17
+ target_language: str
18
+ voice: str
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',
48
+ 'ar', 'hi', 'tr', 'pl', 'nl', 'sv', 'da', 'no', 'fi'
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 {
82
+ 'audio': self.audio.to_dict(),
83
+ 'asr_model': self.asr_model,
84
+ 'target_language': self.target_language,
85
+ 'source_language': self.source_language,
86
+ 'voice': self.voice,
87
+ 'speed': self.speed,
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"""
95
+ audio_data = data.get('audio', {})
96
+ if isinstance(audio_data, dict):
97
+ # Reconstruct AudioUploadDto if needed
98
+ audio = AudioUploadDto(
99
+ filename=audio_data['filename'],
100
+ content=audio_data.get('content', b''),
101
+ content_type=audio_data['content_type']
102
+ )
103
+ else:
104
+ audio = audio_data
105
+
106
+ return cls(
107
+ audio=audio,
108
+ asr_model=data['asr_model'],
109
+ target_language=data['target_language'],
110
+ voice=data['voice'],
111
+ speed=data.get('speed', 1.0),
112
+ source_language=data.get('source_language'),
113
+ additional_params=data.get('additional_params')
114
+ )
src/application/dtos/processing_result_dto.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Processing Result Data Transfer Object"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional, Dict, Any
5
+ from datetime import datetime
6
+
7
+
8
+ @dataclass
9
+ class ProcessingResultDto:
10
+ """DTO for pipeline output data
11
+
12
+ Contains the results of processing audio through the
13
+ STT -> Translation -> TTS pipeline.
14
+ """
15
+ success: bool
16
+ original_text: Optional[str] = None
17
+ translated_text: Optional[str] = None
18
+ audio_path: Optional[str] = None
19
+ processing_time: float = 0.0
20
+ error_message: Optional[str] = None
21
+ error_code: Optional[str] = None
22
+ metadata: Optional[Dict[str, Any]] = None
23
+ timestamp: Optional[datetime] = None
24
+
25
+ def __post_init__(self):
26
+ """Validate and set defaults after initialization"""
27
+ self._validate()
28
+ if self.metadata is None:
29
+ self.metadata = {}
30
+ if self.timestamp is None:
31
+ self.timestamp = datetime.utcnow()
32
+
33
+ def _validate(self):
34
+ """Validate processing result data"""
35
+ if not isinstance(self.success, bool):
36
+ raise ValueError("Success must be a boolean value")
37
+
38
+ if self.processing_time < 0:
39
+ raise ValueError("Processing time cannot be negative")
40
+
41
+ if self.success:
42
+ # For successful processing, we should have some output
43
+ if not self.original_text and not self.translated_text and not self.audio_path:
44
+ raise ValueError("Successful processing must have at least one output (text or audio)")
45
+ else:
46
+ # For failed processing, we should have an error message
47
+ if not self.error_message:
48
+ raise ValueError("Failed processing must include an error message")
49
+
50
+ # Validate error code format if provided
51
+ if self.error_code:
52
+ valid_error_codes = [
53
+ 'STT_ERROR', 'TRANSLATION_ERROR', 'TTS_ERROR',
54
+ 'AUDIO_FORMAT_ERROR', 'VALIDATION_ERROR', 'SYSTEM_ERROR'
55
+ ]
56
+ if self.error_code not in valid_error_codes:
57
+ raise ValueError(f"Invalid error code: {self.error_code}. Valid codes: {valid_error_codes}")
58
+
59
+ # Validate metadata if provided
60
+ if self.metadata and not isinstance(self.metadata, dict):
61
+ raise ValueError("Metadata must be a dictionary")
62
+
63
+ @property
64
+ def has_text_output(self) -> bool:
65
+ """Check if result has text output"""
66
+ return bool(self.original_text or self.translated_text)
67
+
68
+ @property
69
+ def has_audio_output(self) -> bool:
70
+ """Check if result has audio output"""
71
+ return bool(self.audio_path)
72
+
73
+ @property
74
+ def is_complete(self) -> bool:
75
+ """Check if processing is complete (success or failure with error)"""
76
+ return self.success or bool(self.error_message)
77
+
78
+ def add_metadata(self, key: str, value: Any) -> None:
79
+ """Add metadata entry"""
80
+ if self.metadata is None:
81
+ self.metadata = {}
82
+ self.metadata[key] = value
83
+
84
+ def get_metadata(self, key: str, default: Any = None) -> Any:
85
+ """Get metadata value"""
86
+ if self.metadata is None:
87
+ return default
88
+ return self.metadata.get(key, default)
89
+
90
+ def to_dict(self) -> dict:
91
+ """Convert to dictionary representation"""
92
+ return {
93
+ 'success': self.success,
94
+ 'original_text': self.original_text,
95
+ 'translated_text': self.translated_text,
96
+ 'audio_path': self.audio_path,
97
+ 'processing_time': self.processing_time,
98
+ 'error_message': self.error_message,
99
+ 'error_code': self.error_code,
100
+ 'metadata': self.metadata or {},
101
+ 'timestamp': self.timestamp.isoformat() if self.timestamp else None,
102
+ 'has_text_output': self.has_text_output,
103
+ 'has_audio_output': self.has_audio_output,
104
+ 'is_complete': self.is_complete
105
+ }
106
+
107
+ @classmethod
108
+ def success_result(cls, original_text: str = None, translated_text: str = None,
109
+ audio_path: str = None, processing_time: float = 0.0,
110
+ metadata: Dict[str, Any] = None) -> 'ProcessingResultDto':
111
+ """Create a successful processing result"""
112
+ return cls(
113
+ success=True,
114
+ original_text=original_text,
115
+ translated_text=translated_text,
116
+ audio_path=audio_path,
117
+ processing_time=processing_time,
118
+ metadata=metadata
119
+ )
120
+
121
+ @classmethod
122
+ def error_result(cls, error_message: str, error_code: str = None,
123
+ processing_time: float = 0.0, metadata: Dict[str, Any] = None) -> 'ProcessingResultDto':
124
+ """Create a failed processing result"""
125
+ return cls(
126
+ success=False,
127
+ error_message=error_message,
128
+ error_code=error_code,
129
+ processing_time=processing_time,
130
+ metadata=metadata
131
+ )
132
+
133
+ @classmethod
134
+ def from_dict(cls, data: dict) -> 'ProcessingResultDto':
135
+ """Create instance from dictionary"""
136
+ timestamp = None
137
+ if data.get('timestamp'):
138
+ timestamp = datetime.fromisoformat(data['timestamp'].replace('Z', '+00:00'))
139
+
140
+ return cls(
141
+ success=data['success'],
142
+ original_text=data.get('original_text'),
143
+ translated_text=data.get('translated_text'),
144
+ audio_path=data.get('audio_path'),
145
+ processing_time=data.get('processing_time', 0.0),
146
+ error_message=data.get('error_message'),
147
+ error_code=data.get('error_code'),
148
+ metadata=data.get('metadata'),
149
+ timestamp=timestamp
150
+ )
test_dtos.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Test script for DTOs"""
3
+
4
+ import sys
5
+ import os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
7
+
8
+ from application.dtos import AudioUploadDto, ProcessingRequestDto, ProcessingResultDto, ValidationError
9
+
10
+ def test_audio_upload_dto():
11
+ """Test AudioUploadDto"""
12
+ print("Testing AudioUploadDto...")
13
+
14
+ # Test valid DTO
15
+ try:
16
+ audio_dto = AudioUploadDto(
17
+ filename="test.wav",
18
+ content=b"fake audio content" * 100, # Make it larger than 1KB
19
+ content_type="audio/wav"
20
+ )
21
+ print(f"βœ“ Valid AudioUploadDto created: {audio_dto.filename}")
22
+ print(f" Size: {audio_dto.size} bytes")
23
+ print(f" Extension: {audio_dto.file_extension}")
24
+ print(f" Base filename: {audio_dto.base_filename}")
25
+ except Exception as e:
26
+ print(f"βœ— Failed to create valid AudioUploadDto: {e}")
27
+
28
+ # Test invalid extension
29
+ try:
30
+ AudioUploadDto(
31
+ filename="test.txt",
32
+ content=b"fake content" * 100,
33
+ content_type="text/plain"
34
+ )
35
+ print("βœ— Should have failed with invalid extension")
36
+ except ValueError as e:
37
+ print(f"βœ“ Correctly rejected invalid extension: {e}")
38
+
39
+ # Test empty content
40
+ try:
41
+ AudioUploadDto(
42
+ filename="test.wav",
43
+ content=b"",
44
+ content_type="audio/wav"
45
+ )
46
+ print("βœ— Should have failed with empty content")
47
+ except ValueError as e:
48
+ print(f"βœ“ Correctly rejected empty content: {e}")
49
+
50
+ def test_processing_request_dto():
51
+ """Test ProcessingRequestDto"""
52
+ print("\nTesting ProcessingRequestDto...")
53
+
54
+ # Create valid audio DTO first
55
+ audio_dto = AudioUploadDto(
56
+ filename="test.wav",
57
+ content=b"fake audio content" * 100,
58
+ content_type="audio/wav"
59
+ )
60
+
61
+ # Test valid DTO
62
+ try:
63
+ request_dto = ProcessingRequestDto(
64
+ audio=audio_dto,
65
+ asr_model="whisper-small",
66
+ target_language="es",
67
+ voice="kokoro",
68
+ speed=1.2,
69
+ source_language="en"
70
+ )
71
+ print(f"βœ“ Valid ProcessingRequestDto created")
72
+ print(f" ASR Model: {request_dto.asr_model}")
73
+ print(f" Target Language: {request_dto.target_language}")
74
+ print(f" Requires Translation: {request_dto.requires_translation}")
75
+ print(f" Dict representation keys: {list(request_dto.to_dict().keys())}")
76
+ except Exception as e:
77
+ print(f"βœ— Failed to create valid ProcessingRequestDto: {e}")
78
+
79
+ # Test invalid speed
80
+ try:
81
+ ProcessingRequestDto(
82
+ audio=audio_dto,
83
+ asr_model="whisper-small",
84
+ target_language="es",
85
+ voice="kokoro",
86
+ speed=3.0 # Invalid speed
87
+ )
88
+ print("βœ— Should have failed with invalid speed")
89
+ except ValueError as e:
90
+ print(f"βœ“ Correctly rejected invalid speed: {e}")
91
+
92
+ # Test invalid ASR model
93
+ try:
94
+ ProcessingRequestDto(
95
+ audio=audio_dto,
96
+ asr_model="invalid-model",
97
+ target_language="es",
98
+ voice="kokoro"
99
+ )
100
+ print("βœ— Should have failed with invalid ASR model")
101
+ except ValueError as e:
102
+ print(f"βœ“ Correctly rejected invalid ASR model: {e}")
103
+
104
+ def test_processing_result_dto():
105
+ """Test ProcessingResultDto"""
106
+ print("\nTesting ProcessingResultDto...")
107
+
108
+ # Test successful result
109
+ try:
110
+ success_result = ProcessingResultDto.success_result(
111
+ original_text="Hello world",
112
+ translated_text="Hola mundo",
113
+ audio_path="/tmp/output.wav",
114
+ processing_time=2.5
115
+ )
116
+ print(f"βœ“ Valid success result created")
117
+ print(f" Success: {success_result.success}")
118
+ print(f" Has text output: {success_result.has_text_output}")
119
+ print(f" Has audio output: {success_result.has_audio_output}")
120
+ print(f" Is complete: {success_result.is_complete}")
121
+ except Exception as e:
122
+ print(f"βœ— Failed to create success result: {e}")
123
+
124
+ # Test error result
125
+ try:
126
+ error_result = ProcessingResultDto.error_result(
127
+ error_message="TTS generation failed",
128
+ error_code="TTS_ERROR",
129
+ processing_time=1.0
130
+ )
131
+ print(f"βœ“ Valid error result created")
132
+ print(f" Success: {error_result.success}")
133
+ print(f" Error message: {error_result.error_message}")
134
+ print(f" Error code: {error_result.error_code}")
135
+ except Exception as e:
136
+ print(f"βœ— Failed to create error result: {e}")
137
+
138
+ # Test invalid success result (no outputs)
139
+ try:
140
+ ProcessingResultDto(success=True) # No outputs provided
141
+ print("βœ— Should have failed with no outputs for success")
142
+ except ValueError as e:
143
+ print(f"βœ“ Correctly rejected success result with no outputs: {e}")
144
+
145
+ # Test invalid error result (no error message)
146
+ try:
147
+ ProcessingResultDto(success=False) # No error message
148
+ print("βœ— Should have failed with no error message for failure")
149
+ except ValueError as e:
150
+ print(f"βœ“ Correctly rejected error result with no message: {e}")
151
+
152
+ def test_dto_serialization():
153
+ """Test DTO serialization/deserialization"""
154
+ print("\nTesting DTO serialization...")
155
+
156
+ # Test ProcessingResultDto serialization
157
+ try:
158
+ original_result = ProcessingResultDto.success_result(
159
+ original_text="Test text",
160
+ translated_text="Texto de prueba",
161
+ audio_path="/tmp/test.wav",
162
+ processing_time=1.5
163
+ )
164
+
165
+ # Convert to dict and back
166
+ result_dict = original_result.to_dict()
167
+ restored_result = ProcessingResultDto.from_dict(result_dict)
168
+
169
+ print(f"βœ“ ProcessingResultDto serialization successful")
170
+ print(f" Original success: {original_result.success}")
171
+ print(f" Restored success: {restored_result.success}")
172
+ print(f" Original text matches: {original_result.original_text == restored_result.original_text}")
173
+
174
+ except Exception as e:
175
+ print(f"βœ— ProcessingResultDto serialization failed: {e}")
176
+
177
+ if __name__ == "__main__":
178
+ test_audio_upload_dto()
179
+ test_processing_request_dto()
180
+ test_processing_result_dto()
181
+ test_dto_serialization()
182
+ print("\nDTO testing completed!")