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 CHANGED
@@ -3,7 +3,7 @@ OrbitalsAI Python SDK
3
3
 
4
4
  A simple and powerful Python SDK for the OrbitalsAI API.
5
5
 
6
- Example:
6
+ Example (Batch Transcription):
7
7
  import orbitalsai
8
8
 
9
9
  # Synchronous usage
@@ -15,6 +15,22 @@ Example:
15
15
  async with orbitalsai.AsyncClient(api_key="your_api_key_here") as client:
16
16
  transcript = await client.transcribe("audio.mp3")
17
17
  print(transcript.text)
18
+
19
+ Example (Real-time Streaming):
20
+ from orbitalsai.streaming import AsyncStreamingClient, PrintingEventHandlers
21
+ import asyncio
22
+
23
+ async def main():
24
+ async with AsyncStreamingClient(api_key="your_key") as client:
25
+ await client.connect(PrintingEventHandlers())
26
+
27
+ with open("audio.pcm", "rb") as f:
28
+ while chunk := f.read(16000):
29
+ await client.send_audio(chunk)
30
+
31
+ await client.flush()
32
+
33
+ asyncio.run(main())
18
34
  """
19
35
 
20
36
  from .client import Client
@@ -29,7 +45,10 @@ from .exceptions import (
29
45
  TaskNotFoundError, TranscriptionError, TimeoutError, APIError
30
46
  )
31
47
 
32
- __version__ = "1.0.0"
48
+ # Streaming module (imported as submodule)
49
+ from . import streaming
50
+
51
+ __version__ = "2.0.0"
33
52
  __author__ = "OrbitalsAI"
34
53
  __email__ = "support@orbitalsai.com"
35
54
 
@@ -38,6 +57,9 @@ __all__ = [
38
57
  "Client",
39
58
  "AsyncClient",
40
59
 
60
+ # Streaming Module
61
+ "streaming",
62
+
41
63
  # Models
42
64
  "TranscriptTask",
43
65
  "Transcript",
@@ -0,0 +1,117 @@
1
+ """
2
+ OrbitalsAI Streaming Module
3
+
4
+ Real-time streaming transcription via WebSocket.
5
+
6
+ Example (Async - Recommended):
7
+ import asyncio
8
+ from orbitalsai.streaming import AsyncStreamingClient, PrintingEventHandlers
9
+
10
+ async def main():
11
+ async with AsyncStreamingClient(api_key="your_key") as client:
12
+ await client.connect(PrintingEventHandlers())
13
+
14
+ # Stream audio file
15
+ with open("audio.pcm", "rb") as f:
16
+ while chunk := f.read(16000):
17
+ await client.send_audio(chunk)
18
+
19
+ await client.flush()
20
+
21
+ asyncio.run(main())
22
+
23
+ Example (Sync):
24
+ import time
25
+ from orbitalsai.streaming import StreamingClient, StreamingEventHandlers
26
+
27
+ class MyHandlers(StreamingEventHandlers):
28
+ def on_transcript_final(self, transcript, metadata):
29
+ print(f"Final: {transcript}")
30
+ print(f"Cost: ${metadata['cost']:.4f}")
31
+
32
+ with StreamingClient(api_key="your_key") as client:
33
+ client.connect(MyHandlers())
34
+
35
+ with open("audio.pcm", "rb") as f:
36
+ while chunk := f.read(16000):
37
+ client.send_audio(chunk)
38
+ time.sleep(0.5) # Real-time pacing
39
+
40
+ client.flush()
41
+ """
42
+
43
+ from .config import (
44
+ StreamingConfig,
45
+ STREAMING_SUPPORTED_LANGUAGES,
46
+ DEFAULT_SAMPLE_RATE,
47
+ DEFAULT_CHUNK_SIZE,
48
+ )
49
+ from .events import (
50
+ StreamingEventHandlers,
51
+ PrintingEventHandlers,
52
+ CallbackEventHandlers,
53
+ )
54
+ from .exceptions import (
55
+ StreamingError,
56
+ ConnectionError,
57
+ AuthenticationError,
58
+ AudioFormatError,
59
+ InsufficientCreditsError,
60
+ ReconnectionFailedError,
61
+ ServiceUnavailableError,
62
+ ServerBusyError,
63
+ SessionClosedError,
64
+ ProtocolError,
65
+ )
66
+ from .async_client import AsyncStreamingClient
67
+ from .client import StreamingClient, StreamingTranscriptAccumulator
68
+ from .audio import (
69
+ AudioBuffer,
70
+ AudioConverter,
71
+ AudioFormat,
72
+ PCM16_MONO,
73
+ AudioSource,
74
+ FileAudioSource,
75
+ RawPCMFileSource,
76
+ MicrophoneSource,
77
+ )
78
+
79
+ __all__ = [
80
+ # Clients
81
+ "AsyncStreamingClient",
82
+ "StreamingClient",
83
+
84
+ # Configuration
85
+ "StreamingConfig",
86
+ "STREAMING_SUPPORTED_LANGUAGES",
87
+ "DEFAULT_SAMPLE_RATE",
88
+ "DEFAULT_CHUNK_SIZE",
89
+
90
+ # Event Handlers
91
+ "StreamingEventHandlers",
92
+ "PrintingEventHandlers",
93
+ "CallbackEventHandlers",
94
+ "StreamingTranscriptAccumulator",
95
+
96
+ # Exceptions
97
+ "StreamingError",
98
+ "ConnectionError",
99
+ "AuthenticationError",
100
+ "AudioFormatError",
101
+ "InsufficientCreditsError",
102
+ "ReconnectionFailedError",
103
+ "ServiceUnavailableError",
104
+ "ServerBusyError",
105
+ "SessionClosedError",
106
+ "ProtocolError",
107
+
108
+ # Audio Utilities
109
+ "AudioBuffer",
110
+ "AudioConverter",
111
+ "AudioFormat",
112
+ "PCM16_MONO",
113
+ "AudioSource",
114
+ "FileAudioSource",
115
+ "RawPCMFileSource",
116
+ "MicrophoneSource",
117
+ ]
@@ -0,0 +1,507 @@
1
+ """
2
+ OrbitalsAI Async Streaming Client
3
+
4
+ Asynchronous WebSocket client for real-time streaming transcription.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ from typing import Any, Dict, Optional
11
+
12
+ try:
13
+ import websockets
14
+ from websockets.exceptions import (
15
+ ConnectionClosed,
16
+ ConnectionClosedOK,
17
+ ConnectionClosedError,
18
+ InvalidStatusCode,
19
+ )
20
+ except ImportError:
21
+ raise ImportError(
22
+ "websockets is required for streaming. "
23
+ "Install it with: pip install websockets>=11.0.0"
24
+ )
25
+
26
+ from .config import StreamingConfig
27
+ from .events import StreamingEventHandlers
28
+ from .exceptions import (
29
+ StreamingError,
30
+ ConnectionError,
31
+ AuthenticationError,
32
+ AudioFormatError,
33
+ ReconnectionFailedError,
34
+ SessionClosedError,
35
+ ProtocolError,
36
+ exception_from_close_code,
37
+ )
38
+ from .protocol import (
39
+ MessageType,
40
+ MessageParser,
41
+ create_websocket_url,
42
+ should_retry,
43
+ get_close_reason,
44
+ )
45
+ from .connection import ConnectionManager, ConnectionState
46
+ from .audio import AudioBuffer
47
+
48
+ logger = logging.getLogger("orbitalsai.streaming")
49
+
50
+
51
+ class AsyncStreamingClient:
52
+ """
53
+ Asynchronous WebSocket client for streaming transcription.
54
+
55
+ Provides real-time audio streaming and transcription via WebSocket.
56
+ Supports automatic reconnection, event callbacks, and graceful shutdown.
57
+
58
+ Example:
59
+ async with AsyncStreamingClient(api_key="your_key") as client:
60
+ await client.connect(PrintingEventHandlers())
61
+
62
+ # Stream audio file
63
+ with open("audio.pcm", "rb") as f:
64
+ while chunk := f.read(16000):
65
+ await client.send_audio(chunk)
66
+
67
+ await client.flush()
68
+
69
+ Example with custom handlers:
70
+ class MyHandlers(StreamingEventHandlers):
71
+ def on_transcript_final(self, text, metadata):
72
+ print(f"Transcription: {text}")
73
+
74
+ client = AsyncStreamingClient(api_key="your_key")
75
+ await client.connect(MyHandlers())
76
+ # ... send audio ...
77
+ await client.disconnect()
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ api_key: str,
83
+ config: Optional[StreamingConfig] = None,
84
+ base_url: str = "wss://api.orbitalsai.com"
85
+ ):
86
+ """
87
+ Initialize the async streaming client.
88
+
89
+ Args:
90
+ api_key: OrbitalsAI API key or JWT token
91
+ config: Streaming configuration (optional)
92
+ base_url: WebSocket base URL (default: wss://api.orbitalsai.com)
93
+ """
94
+ self.api_key = api_key
95
+ self.config = config or StreamingConfig()
96
+ self.base_url = base_url
97
+
98
+ self._ws: Optional[websockets.WebSocketClientProtocol] = None
99
+ self._handlers: Optional[StreamingEventHandlers] = None
100
+ self._receiver_task: Optional[asyncio.Task] = None
101
+ self._keepalive_task: Optional[asyncio.Task] = None
102
+ self._connected = False
103
+ self._session_id: Optional[str] = None
104
+ self._audio_buffer: Optional[AudioBuffer] = None
105
+
106
+ # Connection management
107
+ self._connection_manager = ConnectionManager(
108
+ max_retries=self.config.max_retries,
109
+ base_delay=self.config.retry_delay,
110
+ max_delay=self.config.max_retry_delay,
111
+ connection_timeout=self.config.connection_timeout,
112
+ )
113
+
114
+ # Reconnection state
115
+ self._should_reconnect = True
116
+ self._reconnect_lock = asyncio.Lock()
117
+
118
+ @property
119
+ def is_connected(self) -> bool:
120
+ """Check if WebSocket is connected."""
121
+ return self._connected and self._ws is not None
122
+
123
+ @property
124
+ def session_id(self) -> Optional[str]:
125
+ """Get current session ID."""
126
+ return self._session_id
127
+
128
+ async def connect(self, handlers: StreamingEventHandlers) -> None:
129
+ """
130
+ Establish WebSocket connection and start receiver loop.
131
+
132
+ Args:
133
+ handlers: Event handlers for callbacks
134
+
135
+ Raises:
136
+ ConnectionError: If connection fails
137
+ AuthenticationError: If authentication fails
138
+ """
139
+ self._handlers = handlers
140
+ self._should_reconnect = True
141
+
142
+ await self._connect_internal()
143
+
144
+ async def _connect_internal(self) -> None:
145
+ """Internal connection method."""
146
+ self._connection_manager.mark_connecting()
147
+
148
+ # Build WebSocket URL with API key
149
+ ws_url = create_websocket_url(self.base_url, self.api_key)
150
+
151
+ logger.info(f"Connecting to {self.base_url}...")
152
+
153
+ try:
154
+ # Connect with timeout
155
+ self._ws = await asyncio.wait_for(
156
+ websockets.connect(
157
+ ws_url,
158
+ ping_interval=self.config.keepalive_interval,
159
+ ping_timeout=20,
160
+ close_timeout=10,
161
+ max_size=10 * 1024 * 1024, # 10MB max message
162
+ ),
163
+ timeout=self.config.connection_timeout
164
+ )
165
+
166
+ # Wait for ready message
167
+ ready_msg = await asyncio.wait_for(
168
+ self._ws.recv(),
169
+ timeout=self.config.connection_timeout
170
+ )
171
+
172
+ # Parse ready message
173
+ try:
174
+ message = MessageParser.parse(ready_msg)
175
+ except ValueError as e:
176
+ raise ProtocolError(f"Invalid ready message: {e}")
177
+
178
+ if message.type == MessageType.ERROR:
179
+ error_msg = message.error_message or "Connection error"
180
+ raise ConnectionError(error_msg)
181
+
182
+ if message.type != MessageType.READY:
183
+ raise ProtocolError(f"Expected 'ready' message, got '{message.type.value}'")
184
+
185
+ # Extract session info
186
+ self._session_id = message.session_id
187
+ self._connected = True
188
+ self._connection_manager.mark_connected()
189
+
190
+ # Initialize audio buffer
191
+ self._audio_buffer = AudioBuffer(
192
+ chunk_size=self.config.chunk_size,
193
+ sample_rate=self.config.sample_rate
194
+ )
195
+
196
+ logger.info(f"Connected: session_id={self._session_id}")
197
+
198
+ # Call on_open handler
199
+ self._safe_call_handler(
200
+ "on_open",
201
+ {
202
+ "session_id": message.session_id,
203
+ "language": message.language,
204
+ "supported_languages": message.supported_languages,
205
+ }
206
+ )
207
+
208
+ # Start receiver loop
209
+ self._receiver_task = asyncio.create_task(self._receiver_loop())
210
+
211
+ except asyncio.TimeoutError:
212
+ raise ConnectionError("Connection timeout")
213
+ except InvalidStatusCode as e:
214
+ if e.status_code == 401:
215
+ raise AuthenticationError("Invalid API key")
216
+ raise ConnectionError(f"Connection failed: HTTP {e.status_code}")
217
+ except ConnectionClosedError as e:
218
+ self._connection_manager.record_close(e.code, e.reason)
219
+ raise exception_from_close_code(e.code, e.reason)
220
+ except Exception as e:
221
+ if not isinstance(e, StreamingError):
222
+ raise ConnectionError(f"Connection failed: {e}")
223
+ raise
224
+
225
+ async def send_audio(self, audio_data: bytes) -> None:
226
+ """
227
+ Send PCM16 audio chunk.
228
+
229
+ Audio should be PCM16 mono little-endian format. The audio will be
230
+ buffered and sent in optimal chunk sizes.
231
+
232
+ Args:
233
+ audio_data: Raw PCM16 mono little-endian bytes
234
+
235
+ Raises:
236
+ SessionClosedError: If session is closed
237
+ AudioFormatError: If audio format is invalid
238
+ """
239
+ if not self.is_connected:
240
+ raise SessionClosedError("Session is not connected")
241
+
242
+ # Validate audio format
243
+ if len(audio_data) % 2 != 0:
244
+ raise AudioFormatError(
245
+ f"Audio data must have even length (PCM16), got {len(audio_data)}"
246
+ )
247
+
248
+ try:
249
+ # Send directly without additional buffering
250
+ # The server handles its own buffering
251
+ await self._ws.send(audio_data)
252
+ except ConnectionClosed as e:
253
+ self._handle_connection_closed(e)
254
+ raise SessionClosedError("Connection closed while sending audio")
255
+
256
+ async def configure(
257
+ self,
258
+ language: Optional[str] = None,
259
+ sample_rate: Optional[int] = None
260
+ ) -> None:
261
+ """
262
+ Update session configuration dynamically.
263
+
264
+ Args:
265
+ language: New transcription language (optional)
266
+ sample_rate: New sample rate in Hz (optional)
267
+
268
+ Raises:
269
+ SessionClosedError: If session is closed
270
+ """
271
+ if not self.is_connected:
272
+ raise SessionClosedError("Session is not connected")
273
+
274
+ config_msg = MessageParser.encode_config(
275
+ language=language,
276
+ sample_rate=sample_rate
277
+ )
278
+
279
+ try:
280
+ await self._ws.send(config_msg)
281
+ except ConnectionClosed as e:
282
+ self._handle_connection_closed(e)
283
+ raise SessionClosedError("Connection closed while configuring")
284
+
285
+ async def flush(self) -> None:
286
+ """
287
+ Force transcription of remaining audio buffer.
288
+
289
+ Sends a flush command to process any buffered audio on the server.
290
+ The server will emit final transcripts for any remaining audio.
291
+
292
+ Raises:
293
+ SessionClosedError: If session is closed
294
+ """
295
+ if not self.is_connected:
296
+ raise SessionClosedError("Session is not connected")
297
+
298
+ # Flush local buffer first
299
+ if self._audio_buffer:
300
+ remaining = self._audio_buffer.flush()
301
+ if remaining:
302
+ try:
303
+ await self._ws.send(remaining)
304
+ except ConnectionClosed:
305
+ pass # Will be handled by server flush
306
+
307
+ try:
308
+ flush_msg = MessageParser.encode_flush()
309
+ await self._ws.send(flush_msg)
310
+ except ConnectionClosed as e:
311
+ self._handle_connection_closed(e)
312
+ raise SessionClosedError("Connection closed while flushing")
313
+
314
+ async def disconnect(self) -> None:
315
+ """
316
+ Close connection gracefully.
317
+
318
+ Stops the receiver loop and closes the WebSocket connection.
319
+ """
320
+ self._should_reconnect = False
321
+ self._connected = False
322
+
323
+ # Cancel receiver task
324
+ if self._receiver_task and not self._receiver_task.done():
325
+ self._receiver_task.cancel()
326
+ try:
327
+ await self._receiver_task
328
+ except asyncio.CancelledError:
329
+ pass
330
+
331
+ # Cancel keepalive task
332
+ if self._keepalive_task and not self._keepalive_task.done():
333
+ self._keepalive_task.cancel()
334
+ try:
335
+ await self._keepalive_task
336
+ except asyncio.CancelledError:
337
+ pass
338
+
339
+ # Close WebSocket
340
+ if self._ws:
341
+ try:
342
+ await self._ws.close(code=1000, reason="Client disconnect")
343
+ except Exception as e:
344
+ logger.debug(f"Error closing WebSocket: {e}")
345
+ self._ws = None
346
+
347
+ self._connection_manager.mark_closed()
348
+ logger.info("Disconnected")
349
+
350
+ async def _receiver_loop(self) -> None:
351
+ """Background task to receive and dispatch messages."""
352
+ try:
353
+ async for message in self._ws:
354
+ await self._handle_message(message)
355
+ except ConnectionClosedOK:
356
+ logger.info("Connection closed normally")
357
+ self._safe_call_handler("on_close", 1000, "Normal closure")
358
+ except ConnectionClosedError as e:
359
+ logger.warning(f"Connection closed: code={e.code}, reason={e.reason}")
360
+ self._handle_connection_closed(e)
361
+ except asyncio.CancelledError:
362
+ logger.debug("Receiver loop cancelled")
363
+ raise
364
+ except Exception as e:
365
+ logger.exception(f"Receiver loop error: {e}")
366
+ self._safe_call_handler("on_error", e)
367
+ finally:
368
+ self._connected = False
369
+
370
+ async def _handle_message(self, raw_message: str) -> None:
371
+ """
372
+ Handle a received WebSocket message.
373
+
374
+ Args:
375
+ raw_message: Raw message string from WebSocket
376
+ """
377
+ try:
378
+ message = MessageParser.parse(raw_message)
379
+ except ValueError as e:
380
+ logger.warning(f"Invalid message: {e}")
381
+ self._safe_call_handler("on_error", ProtocolError(str(e)))
382
+ return
383
+
384
+ # Dispatch based on message type
385
+ msg_type = message.type
386
+
387
+ if msg_type == MessageType.PARTIAL:
388
+ self._safe_call_handler("on_transcript_partial", message.text or "")
389
+
390
+ elif msg_type == MessageType.FINAL:
391
+ metadata = {
392
+ "cost": message.cost or 0,
393
+ "audio_seconds": message.audio_seconds or 0,
394
+ "remaining_percent": message.remaining_percent or 100,
395
+ "capped": message.capped,
396
+ }
397
+ self._safe_call_handler("on_transcript_final", message.text or "", metadata)
398
+
399
+ elif msg_type == MessageType.SPEECH_START:
400
+ self._safe_call_handler("on_speech_start")
401
+
402
+ elif msg_type == MessageType.SPEECH_END:
403
+ self._safe_call_handler("on_speech_end")
404
+
405
+ elif msg_type == MessageType.LANGUAGE_SET:
406
+ self._safe_call_handler("on_language_detected", message.language or "")
407
+
408
+ elif msg_type == MessageType.SAMPLE_RATE_SET:
409
+ self._safe_call_handler("on_sample_rate_changed", message.sample_rate or 16000)
410
+
411
+ elif msg_type == MessageType.FLUSHED:
412
+ self._safe_call_handler("on_flushed")
413
+
414
+ elif msg_type == MessageType.CREDITS_WARNING:
415
+ self._safe_call_handler("on_credits_warning", message.remaining_percent or 0)
416
+
417
+ elif msg_type == MessageType.CREDITS_CRITICAL:
418
+ self._safe_call_handler("on_credits_critical", message.remaining_percent or 0)
419
+
420
+ elif msg_type == MessageType.CREDITS_EXHAUSTED:
421
+ self._safe_call_handler("on_credits_exhausted")
422
+
423
+ elif msg_type == MessageType.ERROR:
424
+ error = StreamingError(message.error_message or "Unknown error")
425
+ self._safe_call_handler("on_error", error)
426
+
427
+ def _safe_call_handler(self, handler_name: str, *args) -> None:
428
+ """
429
+ Safely call an event handler.
430
+
431
+ Wraps handler calls in try/except to prevent user code from
432
+ crashing the SDK.
433
+
434
+ Args:
435
+ handler_name: Name of handler method
436
+ *args: Arguments to pass to handler
437
+ """
438
+ if not self._handlers:
439
+ return
440
+
441
+ handler = getattr(self._handlers, handler_name, None)
442
+ if not handler:
443
+ return
444
+
445
+ try:
446
+ handler(*args)
447
+ except Exception as e:
448
+ logger.warning(f"Handler {handler_name} raised exception: {e}")
449
+
450
+ def _handle_connection_closed(self, error: ConnectionClosed) -> None:
451
+ """
452
+ Handle connection closure.
453
+
454
+ Args:
455
+ error: The connection closed error
456
+ """
457
+ self._connected = False
458
+ self._connection_manager.record_close(error.code, error.reason)
459
+
460
+ # Call on_close handler
461
+ self._safe_call_handler("on_close", error.code, error.reason)
462
+
463
+ # Check for non-retryable errors
464
+ if not should_retry(error.code):
465
+ return
466
+
467
+ # Attempt reconnection if enabled
468
+ if self._should_reconnect:
469
+ asyncio.create_task(self._attempt_reconnect())
470
+
471
+ async def _attempt_reconnect(self) -> None:
472
+ """Attempt to reconnect after connection loss."""
473
+ async with self._reconnect_lock:
474
+ if not self._should_reconnect:
475
+ return
476
+
477
+ while self._should_reconnect and self._connection_manager.should_reconnect():
478
+ self._connection_manager.increment_retry()
479
+ logger.info(
480
+ f"Reconnection attempt {self._connection_manager.retry_count}/"
481
+ f"{self.config.max_retries}"
482
+ )
483
+
484
+ await self._connection_manager.wait_for_retry()
485
+
486
+ try:
487
+ await self._connect_internal()
488
+ logger.info("Reconnected successfully")
489
+ return
490
+ except Exception as e:
491
+ logger.warning(f"Reconnection failed: {e}")
492
+
493
+ # Failed to reconnect
494
+ if self._should_reconnect:
495
+ error = ReconnectionFailedError(
496
+ f"Failed to reconnect after {self._connection_manager.retry_count} attempts",
497
+ attempts=self._connection_manager.retry_count
498
+ )
499
+ self._safe_call_handler("on_error", error)
500
+
501
+ async def __aenter__(self) -> 'AsyncStreamingClient':
502
+ """Async context manager entry."""
503
+ return self
504
+
505
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
506
+ """Async context manager exit."""
507
+ await self.disconnect()
@@ -0,0 +1,33 @@
1
+ """
2
+ OrbitalsAI Streaming Audio Utilities
3
+
4
+ Audio processing utilities for streaming transcription.
5
+ """
6
+
7
+ from .buffer import AudioBuffer
8
+ from .converter import AudioConverter
9
+ from .formats import (
10
+ AudioFormat,
11
+ PCM16_MONO,
12
+ get_format_for_file,
13
+ SUPPORTED_AUDIO_EXTENSIONS,
14
+ )
15
+ from .source import (
16
+ AudioSource,
17
+ FileAudioSource,
18
+ RawPCMFileSource,
19
+ MicrophoneSource,
20
+ )
21
+
22
+ __all__ = [
23
+ "AudioBuffer",
24
+ "AudioConverter",
25
+ "AudioFormat",
26
+ "PCM16_MONO",
27
+ "get_format_for_file",
28
+ "SUPPORTED_AUDIO_EXTENSIONS",
29
+ "AudioSource",
30
+ "FileAudioSource",
31
+ "RawPCMFileSource",
32
+ "MicrophoneSource",
33
+ ]