orbitalsai 1.0.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.
@@ -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)