Michael Hu commited on
Commit
55e29e2
·
1 Parent(s): 5009cb8

Create domain service interfaces

Browse files
src/domain/interfaces/speech_synthesis.py CHANGED
@@ -5,7 +5,8 @@ from typing import Iterator, TYPE_CHECKING
5
 
6
  if TYPE_CHECKING:
7
  from ..models.speech_synthesis_request import SpeechSynthesisRequest
8
- from ..models.audio_content import AudioContent, AudioChunk
 
9
 
10
 
11
  class ISpeechSynthesisService(ABC):
 
5
 
6
  if TYPE_CHECKING:
7
  from ..models.speech_synthesis_request import SpeechSynthesisRequest
8
+ from ..models.audio_content import AudioContent
9
+ from ..models.audio_chunk import AudioChunk
10
 
11
 
12
  class ISpeechSynthesisService(ABC):
src/domain/models/__init__.py CHANGED
@@ -1,15 +1,19 @@
1
  """Domain models package for value objects and entities."""
2
 
3
  from .audio_content import AudioContent
 
4
  from .text_content import TextContent
5
  from .voice_settings import VoiceSettings
6
  from .translation_request import TranslationRequest
7
  from .speech_synthesis_request import SpeechSynthesisRequest
 
8
 
9
  __all__ = [
10
  'AudioContent',
 
11
  'TextContent',
12
  'VoiceSettings',
13
  'TranslationRequest',
14
  'SpeechSynthesisRequest',
 
15
  ]
 
1
  """Domain models package for value objects and entities."""
2
 
3
  from .audio_content import AudioContent
4
+ from .audio_chunk import AudioChunk
5
  from .text_content import TextContent
6
  from .voice_settings import VoiceSettings
7
  from .translation_request import TranslationRequest
8
  from .speech_synthesis_request import SpeechSynthesisRequest
9
+ from .processing_result import ProcessingResult
10
 
11
  __all__ = [
12
  'AudioContent',
13
+ 'AudioChunk',
14
  'TextContent',
15
  'VoiceSettings',
16
  'TranslationRequest',
17
  'SpeechSynthesisRequest',
18
+ 'ProcessingResult',
19
  ]
src/domain/models/audio_chunk.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """AudioChunk value object for streaming audio data."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class AudioChunk:
9
+ """Value object representing a chunk of audio data for streaming."""
10
+
11
+ data: bytes
12
+ format: str
13
+ sample_rate: int
14
+ chunk_index: int
15
+ is_final: bool = False
16
+ timestamp: Optional[float] = None
17
+
18
+ def __post_init__(self):
19
+ """Validate audio chunk after initialization."""
20
+ self._validate()
21
+
22
+ def _validate(self):
23
+ """Validate audio chunk properties."""
24
+ if not isinstance(self.data, bytes):
25
+ raise TypeError("Audio data must be bytes")
26
+
27
+ if not self.data:
28
+ raise ValueError("Audio data cannot be empty")
29
+
30
+ if self.format not in ['wav', 'mp3', 'flac', 'ogg', 'raw']:
31
+ raise ValueError(f"Unsupported audio format: {self.format}")
32
+
33
+ if not isinstance(self.sample_rate, int) or self.sample_rate <= 0:
34
+ raise ValueError("Sample rate must be a positive integer")
35
+
36
+ if not isinstance(self.chunk_index, int) or self.chunk_index < 0:
37
+ raise ValueError("Chunk index must be a non-negative integer")
38
+
39
+ if not isinstance(self.is_final, bool):
40
+ raise TypeError("is_final must be a boolean")
41
+
42
+ if self.timestamp is not None:
43
+ if not isinstance(self.timestamp, (int, float)) or self.timestamp < 0:
44
+ raise ValueError("Timestamp must be a non-negative number")
45
+
46
+ @property
47
+ def size_bytes(self) -> int:
48
+ """Get the size of audio chunk data in bytes."""
49
+ return len(self.data)
50
+
51
+ @property
52
+ def duration_estimate(self) -> float:
53
+ """Estimate duration in seconds based on data size and sample rate."""
54
+ # Rough estimation assuming 16-bit audio (2 bytes per sample)
55
+ bytes_per_second = self.sample_rate * 2
56
+ return len(self.data) / bytes_per_second if bytes_per_second > 0 else 0.0
src/domain/models/processing_result.py ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ProcessingResult value object for audio processing pipeline results."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+ from .text_content import TextContent
6
+ from .audio_content import AudioContent
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class ProcessingResult:
11
+ """Value object representing the result of audio processing pipeline."""
12
+
13
+ success: bool
14
+ original_text: Optional[TextContent]
15
+ translated_text: Optional[TextContent]
16
+ audio_output: Optional[AudioContent]
17
+ error_message: Optional[str]
18
+ processing_time: float
19
+
20
+ def __post_init__(self):
21
+ """Validate processing result after initialization."""
22
+ self._validate()
23
+
24
+ def _validate(self):
25
+ """Validate processing result properties."""
26
+ if not isinstance(self.success, bool):
27
+ raise TypeError("Success must be a boolean")
28
+
29
+ if self.original_text is not None and not isinstance(self.original_text, TextContent):
30
+ raise TypeError("Original text must be a TextContent instance or None")
31
+
32
+ if self.translated_text is not None and not isinstance(self.translated_text, TextContent):
33
+ raise TypeError("Translated text must be a TextContent instance or None")
34
+
35
+ if self.audio_output is not None and not isinstance(self.audio_output, AudioContent):
36
+ raise TypeError("Audio output must be an AudioContent instance or None")
37
+
38
+ if self.error_message is not None and not isinstance(self.error_message, str):
39
+ raise TypeError("Error message must be a string or None")
40
+
41
+ if not isinstance(self.processing_time, (int, float)):
42
+ raise TypeError("Processing time must be a number")
43
+
44
+ if self.processing_time < 0:
45
+ raise ValueError("Processing time cannot be negative")
46
+
47
+ # Business rule validations
48
+ if self.success:
49
+ if self.error_message is not None:
50
+ raise ValueError("Successful result cannot have an error message")
51
+ if self.original_text is None:
52
+ raise ValueError("Successful result must have original text")
53
+ else:
54
+ if self.error_message is None or not self.error_message.strip():
55
+ raise ValueError("Failed result must have a non-empty error message")
56
+
57
+ @property
58
+ def has_translation(self) -> bool:
59
+ """Check if the result includes translated text."""
60
+ return self.translated_text is not None
61
+
62
+ @property
63
+ def has_audio_output(self) -> bool:
64
+ """Check if the result includes audio output."""
65
+ return self.audio_output is not None
66
+
67
+ @property
68
+ def is_complete_pipeline(self) -> bool:
69
+ """Check if the result represents a complete pipeline execution."""
70
+ return (self.success and
71
+ self.original_text is not None and
72
+ self.translated_text is not None and
73
+ self.audio_output is not None)
74
+
75
+ @classmethod
76
+ def success_result(
77
+ cls,
78
+ original_text: TextContent,
79
+ translated_text: Optional[TextContent] = None,
80
+ audio_output: Optional[AudioContent] = None,
81
+ processing_time: float = 0.0
82
+ ) -> 'ProcessingResult':
83
+ """Create a successful processing result."""
84
+ return cls(
85
+ success=True,
86
+ original_text=original_text,
87
+ translated_text=translated_text,
88
+ audio_output=audio_output,
89
+ error_message=None,
90
+ processing_time=processing_time
91
+ )
92
+
93
+ @classmethod
94
+ def failure_result(
95
+ cls,
96
+ error_message: str,
97
+ processing_time: float = 0.0,
98
+ original_text: Optional[TextContent] = None,
99
+ translated_text: Optional[TextContent] = None,
100
+ audio_output: Optional[AudioContent] = None
101
+ ) -> 'ProcessingResult':
102
+ """Create a failed processing result."""
103
+ return cls(
104
+ success=False,
105
+ original_text=original_text,
106
+ translated_text=translated_text,
107
+ audio_output=audio_output,
108
+ error_message=error_message,
109
+ processing_time=processing_time
110
+ )