orbitalsai 1.1.0__py3-none-any.whl → 1.2.0__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.
- orbitalsai/__init__.py +24 -2
- orbitalsai/streaming/__init__.py +117 -0
- orbitalsai/streaming/async_client.py +507 -0
- orbitalsai/streaming/audio/__init__.py +33 -0
- orbitalsai/streaming/audio/buffer.py +171 -0
- orbitalsai/streaming/audio/converter.py +327 -0
- orbitalsai/streaming/audio/formats.py +112 -0
- orbitalsai/streaming/audio/source.py +317 -0
- orbitalsai/streaming/client.py +384 -0
- orbitalsai/streaming/config.py +207 -0
- orbitalsai/streaming/connection.py +298 -0
- orbitalsai/streaming/events.py +360 -0
- orbitalsai/streaming/exceptions.py +179 -0
- orbitalsai/streaming/protocol.py +245 -0
- orbitalsai-1.2.0.dist-info/METADATA +850 -0
- orbitalsai-1.2.0.dist-info/RECORD +24 -0
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/WHEEL +1 -1
- orbitalsai-1.1.0.dist-info/METADATA +0 -491
- orbitalsai-1.1.0.dist-info/RECORD +0 -11
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {orbitalsai-1.1.0.dist-info → orbitalsai-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI Streaming Event Handlers
|
|
3
|
+
|
|
4
|
+
Event handler classes for streaming transcription callbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
from abc import ABC
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("orbitalsai.streaming")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StreamingEventHandlers(ABC):
|
|
16
|
+
"""
|
|
17
|
+
Base class for streaming event handlers.
|
|
18
|
+
|
|
19
|
+
Override methods as needed to handle streaming events. All methods have
|
|
20
|
+
default no-op implementations, so you only need to override the events
|
|
21
|
+
you care about.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
class MyHandlers(StreamingEventHandlers):
|
|
25
|
+
def on_transcript_final(self, transcript: str, metadata: dict) -> None:
|
|
26
|
+
print(f"Final: {transcript}")
|
|
27
|
+
print(f"Cost: ${metadata['cost']:.4f}")
|
|
28
|
+
|
|
29
|
+
async with AsyncStreamingClient(api_key="...") as client:
|
|
30
|
+
await client.connect(MyHandlers())
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def on_open(self, session_info: Dict[str, Any]) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Called when WebSocket connection is established.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
session_info: Dictionary containing:
|
|
39
|
+
- session_id: Unique session identifier
|
|
40
|
+
- language: Current transcription language
|
|
41
|
+
- supported_languages: List of supported languages
|
|
42
|
+
"""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def on_transcript_partial(self, transcript: str) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Called for interim transcription results.
|
|
48
|
+
|
|
49
|
+
Partial transcripts may change as more audio is processed.
|
|
50
|
+
Use these for real-time display but don't rely on them for final output.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
transcript: Partial transcript text (may change)
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
def on_transcript_final(self, transcript: str, metadata: Dict[str, Any]) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Called for final transcription results.
|
|
60
|
+
|
|
61
|
+
Final transcripts are stable and won't change. This is the definitive
|
|
62
|
+
transcription for the processed audio segment.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
transcript: Final transcript text
|
|
66
|
+
metadata: Dictionary containing:
|
|
67
|
+
- cost: Cost of this segment in dollars
|
|
68
|
+
- audio_seconds: Duration of processed audio
|
|
69
|
+
- remaining_percent: Percentage of credits remaining
|
|
70
|
+
- capped: Whether billing was capped due to low balance
|
|
71
|
+
"""
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def on_speech_start(self) -> None:
|
|
75
|
+
"""
|
|
76
|
+
Called when speech is detected.
|
|
77
|
+
|
|
78
|
+
Indicates that the VAD (Voice Activity Detection) has detected
|
|
79
|
+
the start of speech in the audio stream.
|
|
80
|
+
"""
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
def on_speech_end(self) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Called when speech ends (silence detected).
|
|
86
|
+
|
|
87
|
+
Indicates that the VAD has detected a pause or end of speech.
|
|
88
|
+
This typically triggers processing of the accumulated audio.
|
|
89
|
+
"""
|
|
90
|
+
pass
|
|
91
|
+
|
|
92
|
+
def on_language_detected(self, language: str) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Called when language is auto-detected or changed.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
language: The detected or set language
|
|
98
|
+
"""
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
def on_sample_rate_changed(self, sample_rate: int) -> None:
|
|
102
|
+
"""
|
|
103
|
+
Called when sample rate is changed.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
sample_rate: The new sample rate in Hz
|
|
107
|
+
"""
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
def on_flushed(self) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Called when a flush operation completes.
|
|
113
|
+
|
|
114
|
+
Indicates that all buffered audio has been processed and
|
|
115
|
+
final transcripts have been emitted.
|
|
116
|
+
"""
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
def on_credits_warning(self, remaining_percent: int) -> None:
|
|
120
|
+
"""
|
|
121
|
+
Called when credits fall below 20%.
|
|
122
|
+
|
|
123
|
+
This is an early warning to top up credits before they run out.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
remaining_percent: Percentage of credits remaining (0-20)
|
|
127
|
+
"""
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
def on_credits_critical(self, remaining_percent: int) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Called when credits fall below 5%.
|
|
133
|
+
|
|
134
|
+
Critical warning - credits are about to run out.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
remaining_percent: Percentage of credits remaining (0-5)
|
|
138
|
+
"""
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
def on_credits_exhausted(self) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Called when credits are exhausted.
|
|
144
|
+
|
|
145
|
+
The connection will be closed after this event. User needs to
|
|
146
|
+
top up credits before continuing.
|
|
147
|
+
"""
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def on_error(self, error: Exception) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Called when an error occurs.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
error: The exception that occurred
|
|
156
|
+
"""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
def on_close(self, code: int, reason: str) -> None:
|
|
160
|
+
"""
|
|
161
|
+
Called when WebSocket connection closes.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
code: WebSocket close code
|
|
165
|
+
reason: Human-readable close reason
|
|
166
|
+
"""
|
|
167
|
+
pass
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PrintingEventHandlers(StreamingEventHandlers):
|
|
171
|
+
"""
|
|
172
|
+
Event handlers that print all events to stdout.
|
|
173
|
+
|
|
174
|
+
Useful for debugging and testing. Shows timestamps and formatted
|
|
175
|
+
output for all streaming events.
|
|
176
|
+
|
|
177
|
+
Example:
|
|
178
|
+
async with AsyncStreamingClient(api_key="...") as client:
|
|
179
|
+
await client.connect(PrintingEventHandlers())
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
def __init__(self, file=None, show_partials: bool = True):
|
|
183
|
+
"""
|
|
184
|
+
Initialize printing event handlers.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
file: File to print to (default: sys.stdout)
|
|
188
|
+
show_partials: Whether to print partial transcripts (default: True)
|
|
189
|
+
"""
|
|
190
|
+
self.file = file or sys.stdout
|
|
191
|
+
self.show_partials = show_partials
|
|
192
|
+
|
|
193
|
+
def _print(self, message: str) -> None:
|
|
194
|
+
"""Print a message with timestamp."""
|
|
195
|
+
from datetime import datetime
|
|
196
|
+
timestamp = datetime.now().strftime("%H:%M:%S.%f")[:-3]
|
|
197
|
+
print(f"[{timestamp}] {message}", file=self.file, flush=True)
|
|
198
|
+
|
|
199
|
+
def on_open(self, session_info: Dict[str, Any]) -> None:
|
|
200
|
+
self._print(f"🔗 Connected: session={session_info.get('session_id', 'unknown')}")
|
|
201
|
+
self._print(f" Language: {session_info.get('language', 'unknown')}")
|
|
202
|
+
languages = session_info.get('supported_languages', [])
|
|
203
|
+
self._print(f" Supported: {', '.join(languages)}")
|
|
204
|
+
|
|
205
|
+
def on_transcript_partial(self, transcript: str) -> None:
|
|
206
|
+
if self.show_partials:
|
|
207
|
+
self._print(f"📝 Partial: {transcript}")
|
|
208
|
+
|
|
209
|
+
def on_transcript_final(self, transcript: str, metadata: Dict[str, Any]) -> None:
|
|
210
|
+
self._print(f"✅ Final: {transcript}")
|
|
211
|
+
cost = metadata.get('cost', 0)
|
|
212
|
+
seconds = metadata.get('audio_seconds', 0)
|
|
213
|
+
remaining = metadata.get('remaining_percent', 100)
|
|
214
|
+
self._print(f" Cost: ${cost:.4f} | Duration: {seconds:.1f}s | Credits: {remaining}%")
|
|
215
|
+
|
|
216
|
+
def on_speech_start(self) -> None:
|
|
217
|
+
self._print("🎤 Speech started")
|
|
218
|
+
|
|
219
|
+
def on_speech_end(self) -> None:
|
|
220
|
+
self._print("🔇 Speech ended")
|
|
221
|
+
|
|
222
|
+
def on_language_detected(self, language: str) -> None:
|
|
223
|
+
self._print(f"🌐 Language set: {language}")
|
|
224
|
+
|
|
225
|
+
def on_sample_rate_changed(self, sample_rate: int) -> None:
|
|
226
|
+
self._print(f"📊 Sample rate set: {sample_rate} Hz")
|
|
227
|
+
|
|
228
|
+
def on_flushed(self) -> None:
|
|
229
|
+
self._print("💨 Flushed")
|
|
230
|
+
|
|
231
|
+
def on_credits_warning(self, remaining_percent: int) -> None:
|
|
232
|
+
self._print(f"⚠️ Credits warning: {remaining_percent}% remaining")
|
|
233
|
+
|
|
234
|
+
def on_credits_critical(self, remaining_percent: int) -> None:
|
|
235
|
+
self._print(f"🚨 Credits critical: {remaining_percent}% remaining")
|
|
236
|
+
|
|
237
|
+
def on_credits_exhausted(self) -> None:
|
|
238
|
+
self._print("❌ Credits exhausted! Please top up.")
|
|
239
|
+
|
|
240
|
+
def on_error(self, error: Exception) -> None:
|
|
241
|
+
self._print(f"❌ Error: {error}")
|
|
242
|
+
|
|
243
|
+
def on_close(self, code: int, reason: str) -> None:
|
|
244
|
+
self._print(f"🔌 Disconnected: code={code}, reason={reason}")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class CallbackEventHandlers(StreamingEventHandlers):
|
|
248
|
+
"""
|
|
249
|
+
Event handlers using callback functions.
|
|
250
|
+
|
|
251
|
+
Allows passing callback functions directly instead of subclassing.
|
|
252
|
+
Useful for simple use cases where you only need a few handlers.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
handlers = CallbackEventHandlers(
|
|
256
|
+
on_final=lambda text, meta: print(f"Got: {text}"),
|
|
257
|
+
on_error=lambda e: print(f"Error: {e}")
|
|
258
|
+
)
|
|
259
|
+
await client.connect(handlers)
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
def __init__(
|
|
263
|
+
self,
|
|
264
|
+
on_open: Optional[callable] = None,
|
|
265
|
+
on_partial: Optional[callable] = None,
|
|
266
|
+
on_final: Optional[callable] = None,
|
|
267
|
+
on_speech_start: Optional[callable] = None,
|
|
268
|
+
on_speech_end: Optional[callable] = None,
|
|
269
|
+
on_language_detected: Optional[callable] = None,
|
|
270
|
+
on_sample_rate_changed: Optional[callable] = None,
|
|
271
|
+
on_flushed: Optional[callable] = None,
|
|
272
|
+
on_credits_warning: Optional[callable] = None,
|
|
273
|
+
on_credits_critical: Optional[callable] = None,
|
|
274
|
+
on_credits_exhausted: Optional[callable] = None,
|
|
275
|
+
on_error: Optional[callable] = None,
|
|
276
|
+
on_close: Optional[callable] = None,
|
|
277
|
+
):
|
|
278
|
+
"""
|
|
279
|
+
Initialize callback event handlers.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
on_open: Callback for connection open (session_info: dict)
|
|
283
|
+
on_partial: Callback for partial transcripts (text: str)
|
|
284
|
+
on_final: Callback for final transcripts (text: str, metadata: dict)
|
|
285
|
+
on_speech_start: Callback for speech start ()
|
|
286
|
+
on_speech_end: Callback for speech end ()
|
|
287
|
+
on_language_detected: Callback for language detection (language: str)
|
|
288
|
+
on_sample_rate_changed: Callback for sample rate change (sample_rate: int)
|
|
289
|
+
on_flushed: Callback for flush completion ()
|
|
290
|
+
on_credits_warning: Callback for credits warning (remaining_percent: int)
|
|
291
|
+
on_credits_critical: Callback for credits critical (remaining_percent: int)
|
|
292
|
+
on_credits_exhausted: Callback for credits exhausted ()
|
|
293
|
+
on_error: Callback for errors (error: Exception)
|
|
294
|
+
on_close: Callback for connection close (code: int, reason: str)
|
|
295
|
+
"""
|
|
296
|
+
self._on_open = on_open
|
|
297
|
+
self._on_partial = on_partial
|
|
298
|
+
self._on_final = on_final
|
|
299
|
+
self._on_speech_start = on_speech_start
|
|
300
|
+
self._on_speech_end = on_speech_end
|
|
301
|
+
self._on_language_detected = on_language_detected
|
|
302
|
+
self._on_sample_rate_changed = on_sample_rate_changed
|
|
303
|
+
self._on_flushed = on_flushed
|
|
304
|
+
self._on_credits_warning = on_credits_warning
|
|
305
|
+
self._on_credits_critical = on_credits_critical
|
|
306
|
+
self._on_credits_exhausted = on_credits_exhausted
|
|
307
|
+
self._on_error = on_error
|
|
308
|
+
self._on_close = on_close
|
|
309
|
+
|
|
310
|
+
def on_open(self, session_info: Dict[str, Any]) -> None:
|
|
311
|
+
if self._on_open:
|
|
312
|
+
self._on_open(session_info)
|
|
313
|
+
|
|
314
|
+
def on_transcript_partial(self, transcript: str) -> None:
|
|
315
|
+
if self._on_partial:
|
|
316
|
+
self._on_partial(transcript)
|
|
317
|
+
|
|
318
|
+
def on_transcript_final(self, transcript: str, metadata: Dict[str, Any]) -> None:
|
|
319
|
+
if self._on_final:
|
|
320
|
+
self._on_final(transcript, metadata)
|
|
321
|
+
|
|
322
|
+
def on_speech_start(self) -> None:
|
|
323
|
+
if self._on_speech_start:
|
|
324
|
+
self._on_speech_start()
|
|
325
|
+
|
|
326
|
+
def on_speech_end(self) -> None:
|
|
327
|
+
if self._on_speech_end:
|
|
328
|
+
self._on_speech_end()
|
|
329
|
+
|
|
330
|
+
def on_language_detected(self, language: str) -> None:
|
|
331
|
+
if self._on_language_detected:
|
|
332
|
+
self._on_language_detected(language)
|
|
333
|
+
|
|
334
|
+
def on_sample_rate_changed(self, sample_rate: int) -> None:
|
|
335
|
+
if self._on_sample_rate_changed:
|
|
336
|
+
self._on_sample_rate_changed(sample_rate)
|
|
337
|
+
|
|
338
|
+
def on_flushed(self) -> None:
|
|
339
|
+
if self._on_flushed:
|
|
340
|
+
self._on_flushed()
|
|
341
|
+
|
|
342
|
+
def on_credits_warning(self, remaining_percent: int) -> None:
|
|
343
|
+
if self._on_credits_warning:
|
|
344
|
+
self._on_credits_warning(remaining_percent)
|
|
345
|
+
|
|
346
|
+
def on_credits_critical(self, remaining_percent: int) -> None:
|
|
347
|
+
if self._on_credits_critical:
|
|
348
|
+
self._on_credits_critical(remaining_percent)
|
|
349
|
+
|
|
350
|
+
def on_credits_exhausted(self) -> None:
|
|
351
|
+
if self._on_credits_exhausted:
|
|
352
|
+
self._on_credits_exhausted()
|
|
353
|
+
|
|
354
|
+
def on_error(self, error: Exception) -> None:
|
|
355
|
+
if self._on_error:
|
|
356
|
+
self._on_error(error)
|
|
357
|
+
|
|
358
|
+
def on_close(self, code: int, reason: str) -> None:
|
|
359
|
+
if self._on_close:
|
|
360
|
+
self._on_close(code, reason)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OrbitalsAI Streaming Exceptions
|
|
3
|
+
|
|
4
|
+
Streaming-specific exceptions for the OrbitalsAI SDK.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StreamingError(Exception):
|
|
11
|
+
"""
|
|
12
|
+
Base exception for all streaming errors.
|
|
13
|
+
|
|
14
|
+
Inherit from this class for streaming-specific exceptions.
|
|
15
|
+
"""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ConnectionError(StreamingError):
|
|
20
|
+
"""
|
|
21
|
+
WebSocket connection errors.
|
|
22
|
+
|
|
23
|
+
Raised when connection fails, drops unexpectedly, or cannot be established.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
code: WebSocket close code (if applicable)
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, code: Optional[int] = None):
|
|
30
|
+
"""
|
|
31
|
+
Initialize connection error.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
message: Error description
|
|
35
|
+
code: WebSocket close code (optional)
|
|
36
|
+
"""
|
|
37
|
+
super().__init__(message)
|
|
38
|
+
self.code = code
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
if self.code:
|
|
42
|
+
return f"{super().__str__()} (code: {self.code})"
|
|
43
|
+
return super().__str__()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AuthenticationError(StreamingError):
|
|
47
|
+
"""
|
|
48
|
+
Invalid or expired authentication token.
|
|
49
|
+
|
|
50
|
+
Raised when the API key or JWT token is invalid, expired, or doesn't
|
|
51
|
+
have permission for streaming transcription.
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AudioFormatError(StreamingError):
|
|
57
|
+
"""
|
|
58
|
+
Invalid audio format or encoding.
|
|
59
|
+
|
|
60
|
+
Raised when audio data doesn't meet requirements:
|
|
61
|
+
- Must be PCM16 mono little-endian
|
|
62
|
+
- Must have even byte length
|
|
63
|
+
- Sample rate must be in valid range (8000-48000 Hz)
|
|
64
|
+
"""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class InsufficientCreditsError(StreamingError):
|
|
69
|
+
"""
|
|
70
|
+
Insufficient credits for streaming.
|
|
71
|
+
|
|
72
|
+
Raised when the user doesn't have enough credits to start or continue
|
|
73
|
+
a streaming session.
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ReconnectionFailedError(StreamingError):
|
|
79
|
+
"""
|
|
80
|
+
Failed to reconnect after maximum retries.
|
|
81
|
+
|
|
82
|
+
Raised when all reconnection attempts have been exhausted and the
|
|
83
|
+
connection could not be restored.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
attempts: Number of reconnection attempts made
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, message: str, attempts: int):
|
|
90
|
+
"""
|
|
91
|
+
Initialize reconnection failed error.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
message: Error description
|
|
95
|
+
attempts: Number of reconnection attempts made
|
|
96
|
+
"""
|
|
97
|
+
super().__init__(message)
|
|
98
|
+
self.attempts = attempts
|
|
99
|
+
|
|
100
|
+
def __str__(self) -> str:
|
|
101
|
+
return f"{super().__str__()} (attempts: {self.attempts})"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class ServiceUnavailableError(StreamingError):
|
|
105
|
+
"""
|
|
106
|
+
Transcription service is unavailable.
|
|
107
|
+
|
|
108
|
+
Raised when the backend service (Triton) is not available or unhealthy.
|
|
109
|
+
This is typically a temporary condition that may resolve with retry.
|
|
110
|
+
"""
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ServerBusyError(StreamingError):
|
|
115
|
+
"""
|
|
116
|
+
Server is too busy to accept new connections.
|
|
117
|
+
|
|
118
|
+
Raised when the server has reached its maximum connection limit.
|
|
119
|
+
Retry after a longer delay.
|
|
120
|
+
"""
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class SessionClosedError(StreamingError):
|
|
125
|
+
"""
|
|
126
|
+
Operation attempted on a closed session.
|
|
127
|
+
|
|
128
|
+
Raised when trying to send audio or perform operations after the
|
|
129
|
+
streaming session has been closed.
|
|
130
|
+
"""
|
|
131
|
+
pass
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class ProtocolError(StreamingError):
|
|
135
|
+
"""
|
|
136
|
+
WebSocket protocol error.
|
|
137
|
+
|
|
138
|
+
Raised when receiving invalid or unexpected messages from the server.
|
|
139
|
+
"""
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# Close code mapping for user-friendly error handling
|
|
144
|
+
CLOSE_CODE_ERRORS = {
|
|
145
|
+
4001: AuthenticationError,
|
|
146
|
+
4002: ServiceUnavailableError,
|
|
147
|
+
4003: ServerBusyError,
|
|
148
|
+
4004: InsufficientCreditsError,
|
|
149
|
+
4005: InsufficientCreditsError,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def exception_from_close_code(code: int, message: str = "") -> StreamingError:
|
|
154
|
+
"""
|
|
155
|
+
Create appropriate exception from WebSocket close code.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
code: WebSocket close code
|
|
159
|
+
message: Optional error message
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Appropriate StreamingError subclass instance
|
|
163
|
+
"""
|
|
164
|
+
error_class = CLOSE_CODE_ERRORS.get(code, ConnectionError)
|
|
165
|
+
|
|
166
|
+
default_messages = {
|
|
167
|
+
4001: "Authentication failed",
|
|
168
|
+
4002: "Service unavailable",
|
|
169
|
+
4003: "Server busy",
|
|
170
|
+
4004: "Insufficient balance",
|
|
171
|
+
4005: "Credits exhausted",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if not message:
|
|
175
|
+
message = default_messages.get(code, f"Connection closed with code {code}")
|
|
176
|
+
|
|
177
|
+
if error_class == ConnectionError:
|
|
178
|
+
return ConnectionError(message, code=code)
|
|
179
|
+
return error_class(message)
|