intellema-vdk 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. intellema_vdk/__init__.py +67 -10
  2. intellema_vdk/config.py +14 -0
  3. intellema_vdk/providers/__init__.py +35 -0
  4. intellema_vdk/providers/livekit/__init__.py +19 -0
  5. intellema_vdk/providers/livekit/client.py +612 -0
  6. intellema_vdk/providers/livekit/exceptions.py +23 -0
  7. intellema_vdk/providers/protocols.py +33 -0
  8. intellema_vdk/providers/retell/__init__.py +17 -0
  9. intellema_vdk/providers/retell/client.py +468 -0
  10. intellema_vdk/providers/retell/exceptions.py +19 -0
  11. intellema_vdk/{retell_lib → providers/retell}/import_phone_number.py +1 -1
  12. intellema_vdk/stt/__init__.py +17 -0
  13. intellema_vdk/stt/client.py +482 -0
  14. intellema_vdk/stt/exceptions.py +19 -0
  15. intellema_vdk/tts/__init__.py +15 -0
  16. intellema_vdk/tts/__pycache__/__init__.cpython-312.pyc +0 -0
  17. intellema_vdk/tts/__pycache__/client.cpython-312.pyc +0 -0
  18. intellema_vdk/tts/__pycache__/exceptions.cpython-312.pyc +0 -0
  19. intellema_vdk/tts/__pycache__/providers.cpython-312.pyc +0 -0
  20. intellema_vdk/tts/client.py +541 -0
  21. intellema_vdk/tts/exceptions.py +15 -0
  22. intellema_vdk/tts/providers.py +293 -0
  23. intellema_vdk/utils/logger_config.py +41 -0
  24. intellema_vdk-0.2.2.dist-info/METADATA +311 -0
  25. intellema_vdk-0.2.2.dist-info/RECORD +29 -0
  26. {intellema_vdk-0.2.1.dist-info → intellema_vdk-0.2.2.dist-info}/WHEEL +1 -1
  27. intellema_vdk/__pycache__/__init__.cpython-312.pyc +0 -0
  28. intellema_vdk/livekit_lib/__init__.py +0 -3
  29. intellema_vdk/livekit_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  30. intellema_vdk/livekit_lib/__pycache__/client.cpython-312.pyc +0 -0
  31. intellema_vdk/livekit_lib/client.py +0 -280
  32. intellema_vdk/retell_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  33. intellema_vdk/retell_lib/__pycache__/retell_client.cpython-312.pyc +0 -0
  34. intellema_vdk/retell_lib/retell_client.py +0 -248
  35. intellema_vdk/speech_lib/__init__.py +0 -2
  36. intellema_vdk/speech_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  37. intellema_vdk/speech_lib/__pycache__/stt_client.cpython-312.pyc +0 -0
  38. intellema_vdk/speech_lib/__pycache__/tts_streamer.cpython-312.pyc +0 -0
  39. intellema_vdk/speech_lib/stt_client.py +0 -110
  40. intellema_vdk/speech_lib/tts_streamer.py +0 -188
  41. intellema_vdk-0.2.1.dist-info/METADATA +0 -221
  42. intellema_vdk-0.2.1.dist-info/RECORD +0 -22
  43. /intellema_vdk/{retell_lib/__init__.py → stt/providers.py} +0 -0
  44. {intellema_vdk-0.2.1.dist-info → intellema_vdk-0.2.2.dist-info}/licenses/LICENSE +0 -0
  45. {intellema_vdk-0.2.1.dist-info → intellema_vdk-0.2.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,541 @@
1
+ """TTS streaming client for real-time text-to-speech audio playback."""
2
+
3
+ import queue
4
+ import threading
5
+ import time
6
+ import logging
7
+ import subprocess
8
+ import sys
9
+ import platform
10
+ from typing import Optional, Union, Literal, overload, TYPE_CHECKING
11
+
12
+ # Lazy import pyaudio - only load when TTSStreamer is instantiated
13
+ if TYPE_CHECKING:
14
+ import pyaudio
15
+ else:
16
+ pyaudio = None
17
+
18
+ from .providers import (
19
+ TTSProvider,
20
+ TogetherTTSProvider,
21
+ OpenAITTSProvider,
22
+ TogetherTTSConfig,
23
+ OpenAITTSConfig,
24
+ )
25
+ from ..config import (
26
+ get_env,
27
+ TTS_AUDIO_SAMPLE_RATE,
28
+ )
29
+ from .exceptions import (
30
+ TTSConfigurationError,
31
+ TTSStreamError,
32
+ TTSAPIError,
33
+ TTSError
34
+ )
35
+
36
+
37
+ # Setup logger for this module.
38
+ logger = logging.getLogger(__name__)
39
+
40
+
41
+
42
+
43
+ class TTSStreamer:
44
+ """
45
+ Real-time text-to-speech streaming and playback.
46
+
47
+ Streams text-to-speech audio from various providers (Together AI, OpenAI)
48
+ and plays it in real-time with minimal latency. Designed for continuous
49
+ text streams (e.g., from language models) with immediate audio feedback.
50
+
51
+ The streamer uses separate threads for fetching and playing audio to
52
+ ensure smooth, non-blocking playback. Text is buffered until sentence
53
+ boundaries are detected, then immediately converted to speech.
54
+
55
+ Supported Providers:
56
+ - Together AI: Low-latency streaming with Orpheus model
57
+ - OpenAI: High-quality voices with tts-1 and tts-1-hd models
58
+
59
+ Attributes:
60
+ provider: The TTS provider instance for generating audio.
61
+ p: PyAudio instance for audio I/O.
62
+ stream: Audio stream for playback (24kHz, 16-bit PCM, mono).
63
+ text_queue: Thread-safe queue for incoming text sentences.
64
+ audio_queue: Thread-safe queue for outgoing audio chunks.
65
+ text_buffer: Buffer accumulating text until sentence completion.
66
+ is_running: Flag controlling thread execution state.
67
+ threads_started: Flag indicating if worker threads are active.
68
+
69
+ Examples:
70
+ Basic usage with Together AI:
71
+ >>> streamer = TTSStreamer(provider="together")
72
+ >>> streamer.feed("Hello, world. ")
73
+ >>> streamer.feed("How are you?")
74
+ >>> streamer.flush()
75
+ >>> streamer.close()
76
+
77
+ Using OpenAI with custom voice:
78
+ >>> streamer = TTSStreamer(
79
+ ... provider="openai",
80
+ ... voice="nova",
81
+ ... model="tts-1-hd"
82
+ ... )
83
+ >>> for token in text.split():
84
+ ... streamer.feed(token + " ")
85
+ >>> streamer.flush()
86
+ >>> streamer.close()
87
+
88
+ Streaming LLM output:
89
+ >>> streamer = TTSStreamer(provider="together")
90
+ >>> for chunk in llm_stream:
91
+ ... streamer.feed(chunk)
92
+ >>> streamer.flush()
93
+ >>> streamer.close()
94
+
95
+ Notes:
96
+ - Call flush() to wait for current audio to finish playing
97
+ - Call close() to stop all threads and release audio resources
98
+ - Requires system audio output and PyAudio installed
99
+ - Works best with whole words/phrases but also handles character streaming
100
+ - Sentence boundaries (. ! ? \n) trigger immediate speech synthesis
101
+ """
102
+
103
+ @overload
104
+ def __init__(
105
+ self,
106
+ provider: Literal["together"],
107
+ api_key: Optional[str] = None,
108
+ model: Literal["canopylabs/orpheus-3b-0.1-ft"] = "canopylabs/orpheus-3b-0.1-ft",
109
+ voice: Literal["tara"] = "tara",
110
+ ) -> None: ...
111
+
112
+ @overload
113
+ def __init__(
114
+ self,
115
+ provider: Literal["openai"],
116
+ api_key: Optional[str] = None,
117
+ model: Literal["tts-1", "tts-1-hd"] = "tts-1",
118
+ voice: Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer"] = "alloy",
119
+ ) -> None: ...
120
+
121
+ @overload
122
+ def __init__(
123
+ self,
124
+ provider: TTSProvider,
125
+ api_key: Optional[str] = None,
126
+ **provider_kwargs
127
+ ) -> None: ...
128
+
129
+ def __init__(self,
130
+ provider: Union[Literal["together", "openai"], TTSProvider] = "together",
131
+ api_key: Optional[str] = None,
132
+ **provider_kwargs) -> None:
133
+ """
134
+ Initialize the TTS streamer with a provider.
135
+
136
+ Args:
137
+ provider: Either a provider name or a custom TTSProvider instance.
138
+ - "together": Use Together AI (requires TOGETHER_API_KEY)
139
+ - "openai": Use OpenAI (requires OPENAI_API_KEY)
140
+ - TTSProvider instance: Use custom provider
141
+ api_key: API key for the provider. If not provided, reads from
142
+ environment variables (TOGETHER_API_KEY or OPENAI_API_KEY).
143
+ **provider_kwargs: Provider-specific configuration options:
144
+
145
+ For Together AI:
146
+ model: Model identifier (default: "canopylabs/orpheus-3b-0.1-ft")
147
+ voice: Voice identifier (default: "tara")
148
+
149
+ For OpenAI:
150
+ model: "tts-1" (fast) or "tts-1-hd" (high quality)
151
+ voice: "alloy", "echo", "fable", "onyx", "nova", or "shimmer"
152
+
153
+ Raises:
154
+ TTSConfigurationError: If API key is missing or provider is invalid.
155
+ TTSStreamError: If audio stream initialization fails.
156
+ ImportError: If required dependencies (pyaudio, provider SDK) are missing.
157
+
158
+ Examples:
159
+ >>> # Together AI with defaults
160
+ >>> streamer = TTSStreamer(provider="together")
161
+
162
+ >>> # OpenAI with custom voice
163
+ >>> streamer = TTSStreamer(
164
+ ... provider="openai",
165
+ ... voice="nova",
166
+ ... model="tts-1-hd"
167
+ ... )
168
+
169
+ >>> # Custom provider
170
+ >>> custom = MyCustomProvider()
171
+ >>> streamer = TTSStreamer(provider=custom)
172
+ """
173
+ # Lazy import pyaudio - only install when actually used
174
+ global pyaudio
175
+ if pyaudio is None:
176
+ try:
177
+ import pyaudio as _pyaudio
178
+ pyaudio = _pyaudio
179
+ except ImportError:
180
+ print("\n" + "="*70)
181
+ print("PyAudio is not installed.")
182
+ print("="*70)
183
+ print("\nPyAudio requires the PortAudio library to be installed on your system.")
184
+ print("\nInstallation instructions by platform:")
185
+ print("\n Windows:")
186
+ print(" Option 1: pip install pipwin && pipwin install pyaudio")
187
+ print(" Option 2: pip install pyaudio")
188
+ print(" Option 3: Download wheel from https://www.lfd.uci.edu/~gohlke/pythonlibs/#pyaudio")
189
+ print("\n macOS:")
190
+ print(" brew install portaudio")
191
+ print(" pip install pyaudio")
192
+ print("\n Linux (Debian/Ubuntu):")
193
+ print(" sudo apt-get install portaudio19-dev")
194
+ print(" pip install pyaudio")
195
+ print("\n Linux (Fedora):")
196
+ print(" sudo dnf install portaudio-devel")
197
+ print(" pip install pyaudio")
198
+ print("\n" + "="*70)
199
+
200
+ # Attempt automatic installation
201
+ current_os = platform.system()
202
+ print(f"\nDetected OS: {current_os}")
203
+ print("Attempting automatic installation...")
204
+
205
+ try:
206
+ if current_os == "Windows":
207
+ # Try pipwin first, fall back to pip
208
+ try:
209
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "pipwin"],
210
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
211
+ subprocess.check_call([sys.executable, "-m", "pipwin", "install", "pyaudio"])
212
+ print("✓ PyAudio installed successfully via pipwin!")
213
+ except:
214
+ # Fall back to pip
215
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "pyaudio>=0.2.13"])
216
+ print("✓ PyAudio installed successfully via pip!")
217
+ else:
218
+ # For macOS/Linux, just try pip (user needs to install PortAudio first)
219
+ subprocess.check_call([sys.executable, "-m", "pip", "install", "pyaudio>=0.2.13"])
220
+ print("✓ PyAudio installed successfully!")
221
+
222
+ import pyaudio as _pyaudio
223
+ pyaudio = _pyaudio
224
+ except Exception as e:
225
+ error_msg = (
226
+ "\nFailed to install PyAudio automatically. Please install manually:\n"
227
+ " pip install intellema-vdk[audio]\n"
228
+ "or follow the platform-specific instructions above.\n"
229
+ )
230
+ if current_os != "Windows":
231
+ error_msg += "\nMake sure PortAudio is installed on your system first!\n"
232
+ raise TTSConfigurationError(error_msg) from e
233
+
234
+ # Initialize the provider
235
+ if isinstance(provider, str):
236
+ if provider == "together":
237
+ api_key = api_key or get_env("TOGETHER_API_KEY")
238
+ if not api_key:
239
+ raise TTSConfigurationError(
240
+ "Together API Key is missing. Set TOGETHER_API_KEY env var or pass api_key."
241
+ )
242
+ self.provider = TogetherTTSProvider(api_key=api_key, **provider_kwargs)
243
+ elif provider == "openai":
244
+ api_key = api_key or get_env("OPENAI_API_KEY")
245
+ if not api_key:
246
+ raise TTSConfigurationError(
247
+ "OpenAI API Key is missing. Set OPENAI_API_KEY env var or pass api_key."
248
+ )
249
+ self.provider = OpenAITTSProvider(api_key=api_key, **provider_kwargs)
250
+ else:
251
+ raise TTSConfigurationError(
252
+ f"Unknown provider: {provider}. Supported providers: 'together', 'openai'."
253
+ )
254
+ else:
255
+ # Custom provider instance
256
+ self.provider = provider
257
+
258
+ # Audio configuration.
259
+ try:
260
+ self.p = pyaudio.PyAudio()
261
+ self.stream = self.p.open(
262
+ format=pyaudio.paInt16, channels=1, rate=TTS_AUDIO_SAMPLE_RATE, output=True
263
+ )
264
+ except Exception as e:
265
+ raise TTSStreamError(
266
+ f"Failed to initialize audio stream: {e}") from e
267
+
268
+ # Queues for inter-thread communication.
269
+ self.text_queue = queue.Queue()
270
+ self.audio_queue = queue.Queue()
271
+
272
+ # State management.
273
+ self.text_buffer = ""
274
+ self.is_running = True
275
+ self.threads_started = False
276
+
277
+ # Thread placeholders.
278
+ self.fetcher_thread = None
279
+ self.player_thread = None
280
+
281
+ def _ensure_started(self) -> None:
282
+ """Initialize and start worker threads for audio fetching and playback.
283
+
284
+ Creates and starts two daemon threads:
285
+ - fetcher_thread: Converts text to audio using the TTS provider
286
+ - player_thread: Plays audio chunks through the audio device
287
+
288
+ This method is called lazily on the first call to feed().
289
+ """
290
+ if self.threads_started:
291
+ return
292
+
293
+ # Start the threads for fetching and playing audio.
294
+ self.fetcher_thread = threading.Thread(
295
+ target=self._tts_fetcher, daemon=True)
296
+ self.player_thread = threading.Thread(
297
+ target=self._audio_player, daemon=True)
298
+
299
+ self.fetcher_thread.start()
300
+ self.player_thread.start()
301
+ self.threads_started = True
302
+
303
+ def feed(self, text_chunk: str) -> None:
304
+ """
305
+ Feed a chunk of text to the streamer for conversion to speech.
306
+
307
+ Text is buffered until a sentence-ending punctuation mark is detected
308
+ (. ! ? or newline), then the complete sentence is queued for TTS processing.
309
+ Audio playback begins as soon as the first audio chunks are received.
310
+
311
+ This method is thread-safe and can be called repeatedly to stream text.
312
+
313
+ Args:
314
+ text_chunk: A piece of text to convert to speech. Can be a single
315
+ character, word, or multiple sentences. Empty strings are ignored.
316
+
317
+ Examples:
318
+ >>> streamer.feed("Hello, world. ") # Processes "Hello, world."
319
+ >>> streamer.feed("How are you?") # Processes "How are you?"
320
+ >>>
321
+ >>> # Streaming word by word
322
+ >>> for word in sentence.split():
323
+ ... streamer.feed(word + " ")
324
+ >>>
325
+ >>> # Streaming character by character
326
+ >>> for char in text:
327
+ ... streamer.feed(char)
328
+
329
+ Note:
330
+ The streamer will only speak complete sentences. Any partial text
331
+ at the end (not ending with . ! ? or \n) remains in the buffer until
332
+ more text is fed or flush() is called.
333
+ """
334
+ if not self.is_running or not text_chunk:
335
+ return
336
+
337
+ self._ensure_started()
338
+
339
+ self.text_buffer += text_chunk
340
+ sentence_endings = [".", "!", "?", "\n"]
341
+
342
+ # Split text into sentences and queue them.
343
+ for ending in sentence_endings:
344
+ if ending in self.text_buffer:
345
+ parts = self.text_buffer.split(ending)
346
+ for sentence in parts[:-1]:
347
+ if sentence.strip():
348
+ full_sentence = sentence.strip() + ending
349
+ self.text_queue.put(full_sentence)
350
+ self.text_buffer = parts[-1] # Keep the remainder.
351
+
352
+ def flush(self) -> None:
353
+ """
354
+ Process any remaining buffered text and wait for all audio to finish playing.
355
+
356
+ This method:
357
+ 1. Converts any remaining text in the buffer to speech
358
+ 2. Waits for the text queue to be fully processed
359
+ 3. Waits for the audio queue to be fully played
360
+ 4. Adds a small delay for the hardware audio buffer to drain
361
+
362
+ This method is non-destructive - the streamer can be reused after flushing.
363
+ Call this when you've finished feeding text and want to ensure all audio
364
+ has been spoken before continuing.
365
+
366
+ Examples:
367
+ >>> streamer.feed("Hello world")
368
+ >>> streamer.flush() # Wait for "Hello world" to finish playing
369
+ >>> streamer.feed("More text") # Can continue using the streamer
370
+ >>> streamer.flush()
371
+ >>> streamer.close() # Final cleanup
372
+
373
+ Note:
374
+ If no text has been fed yet (threads not started), this method returns
375
+ immediately without doing anything.
376
+ """
377
+ if not self.threads_started:
378
+ return # Nothing to flush if never started.
379
+
380
+ # Push any remaining text from the buffer.
381
+ if self.text_buffer.strip():
382
+ self.text_queue.put(self.text_buffer.strip())
383
+ self.text_buffer = ""
384
+
385
+ # Wait for both queues to be empty.
386
+ self.text_queue.join()
387
+ self.audio_queue.join()
388
+
389
+ # A small delay to allow the hardware audio buffer to drain.
390
+ time.sleep(0.5)
391
+
392
+ def close(self) -> None:
393
+ """
394
+ Stop all threads and immediately close the audio stream.
395
+
396
+ This method performs complete cleanup:
397
+ 1. Sets the running flag to False to signal threads to stop
398
+ 2. Sends poison pills to both queues to unblock waiting threads
399
+ 3. Clears both queues of any pending items
400
+ 4. Closes the PyAudio stream and terminates PyAudio
401
+
402
+ After calling close(), the streamer cannot be reused. Create a new
403
+ instance if you need to stream more audio.
404
+
405
+ It's safe to call this method multiple times - subsequent calls will
406
+ have no effect.
407
+
408
+ Examples:
409
+ >>> streamer = TTSStreamer(provider="together")
410
+ >>> streamer.feed("Hello world.")
411
+ >>> streamer.flush() # Wait for audio to finish
412
+ >>> streamer.close() # Clean up resources
413
+ >>>
414
+ >>> # Using with context manager pattern
415
+ >>> streamer = TTSStreamer(provider="openai")
416
+ >>> try:
417
+ ... streamer.feed("Some text")
418
+ ... streamer.flush()
419
+ >>> finally:
420
+ ... streamer.close()
421
+
422
+ Note:
423
+ Always call close() when finished with the streamer to free system
424
+ resources and prevent audio device locks.
425
+ """
426
+ if not self.is_running:
427
+ return
428
+
429
+ self.is_running = False
430
+
431
+ # Send 'poison pill' to threads to signal them to stop.
432
+ self.text_queue.put(None)
433
+ self.audio_queue.put(None)
434
+
435
+ # Clear queues to unblock threads that might be waiting.
436
+ with self.text_queue.mutex:
437
+ self.text_queue.queue.clear()
438
+ with self.audio_queue.mutex:
439
+ self.audio_queue.queue.clear()
440
+
441
+ # Close the audio stream.
442
+ try:
443
+ self.stream.stop_stream()
444
+ self.stream.close()
445
+ self.p.terminate()
446
+ except Exception as e:
447
+ logger.warning(f"Error during audio stream closure: {e}")
448
+
449
+ def _tts_fetcher(self) -> None:
450
+ """
451
+ Worker thread that fetches TTS audio from the provider API.
452
+
453
+ Continuously pulls text from the text_queue, sends it to the TTS provider's
454
+ stream() method, and pushes received audio chunks to the audio_queue.
455
+
456
+ Implements retry logic with up to 2 retries for failed API calls.
457
+ If all retries fail, the text chunk is dropped and an error is logged.
458
+
459
+ The thread runs until:
460
+ - is_running flag is set to False, or
461
+ - A poison pill (None) is received from the text queue
462
+
463
+ Note:
464
+ This is a daemon thread started automatically by _ensure_started().
465
+ """
466
+ while self.is_running:
467
+ try:
468
+ text = self.text_queue.get(timeout=0.5)
469
+ except queue.Empty:
470
+ continue
471
+
472
+ if text is None: # Poison pill received.
473
+ break
474
+
475
+ max_retries = 2
476
+ for attempt in range(max_retries + 1):
477
+ try:
478
+ # Use the provider's stream method
479
+ for audio_data in self.provider.stream(text):
480
+ if not self.is_running:
481
+ break
482
+ if audio_data:
483
+ self.audio_queue.put(audio_data)
484
+
485
+ break # Success, exit retry loop.
486
+
487
+ except Exception as e:
488
+ logger.error(
489
+ f"TTS Error (Attempt {attempt + 1}/{max_retries + 1}): {e}")
490
+ if attempt >= max_retries:
491
+ logger.error(
492
+ f"Max retries reached. Dropping text chunk: {text[:50]}...")
493
+
494
+ self.text_queue.task_done()
495
+
496
+ def _audio_player(self) -> None:
497
+ """
498
+ Worker thread that plays audio chunks from the audio queue.
499
+
500
+ Continuously pulls audio data from the audio_queue and writes it to the
501
+ PyAudio stream for playback. Maintains a small buffer to handle frame
502
+ alignment (audio must be written in complete 16-bit samples).
503
+
504
+ The thread runs until:
505
+ - is_running flag is set to False, or
506
+ - A poison pill (None) is received from the audio queue
507
+
508
+ Error Handling:
509
+ If an OSError occurs while writing to the audio stream (e.g., device
510
+ disconnected), the error is logged and the thread exits gracefully.
511
+
512
+ Note:
513
+ This is a daemon thread started automatically by _ensure_started().
514
+ """
515
+ buffer = b""
516
+ while self.is_running:
517
+ try:
518
+ audio_data = self.audio_queue.get(timeout=0.5)
519
+ except queue.Empty:
520
+ continue
521
+
522
+ if audio_data is None: # Poison pill received.
523
+ break
524
+
525
+ buffer += audio_data
526
+
527
+ # Play audio in chunks aligned with frame boundaries.
528
+ if len(buffer) >= 2:
529
+ frame_count = len(buffer) // 2
530
+ bytes_to_play = frame_count * 2
531
+ play_chunk = buffer[:bytes_to_play]
532
+ buffer = buffer[bytes_to_play:]
533
+
534
+ try:
535
+ self.stream.write(play_chunk)
536
+ except OSError as e:
537
+ logger.error(f"Error writing to audio stream: {e}")
538
+ self.audio_queue.task_done()
539
+ break
540
+
541
+ self.audio_queue.task_done()
@@ -0,0 +1,15 @@
1
+ class TTSError(Exception):
2
+ """Base exception for all TTS-related errors."""
3
+ pass
4
+
5
+ class TTSConfigurationError(TTSError):
6
+ """Raised when configuration (API keys) is missing or invalid."""
7
+ pass
8
+
9
+ class TTSStreamError(TTSError):
10
+ """Raised when there are issues with the audio stream."""
11
+ pass
12
+
13
+ class TTSAPIError(TTSError):
14
+ """Raised when the TTS API (Together) returns an error."""
15
+ pass