Spaces:
Sleeping
Sleeping
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!")
|