dominus-sdk-python-dev 2.3.0__py3-none-any.whl → 2.5.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.
- dominus/__init__.py +82 -5
- dominus/helpers/cache.py +12 -4
- dominus/namespaces/oracle/__init__.py +120 -0
- dominus/namespaces/oracle/audio_capture.py +382 -0
- dominus/namespaces/oracle/oracle_websocket.py +230 -0
- dominus/namespaces/oracle/session.py +252 -0
- dominus/namespaces/oracle/types.py +127 -0
- dominus/namespaces/oracle/vad_gate.py +276 -0
- dominus/start.py +26 -32
- {dominus_sdk_python_dev-2.3.0.dist-info → dominus_sdk_python_dev-2.5.0.dist-info}/METADATA +21 -6
- {dominus_sdk_python_dev-2.3.0.dist-info → dominus_sdk_python_dev-2.5.0.dist-info}/RECORD +13 -11
- dominus/namespaces/_deprecated_crossover.py +0 -10
- dominus/namespaces/_deprecated_sql.py +0 -1341
- dominus/services/_deprecated_architect.py +0 -323
- dominus/services/_deprecated_sovereign.py +0 -93
- {dominus_sdk_python_dev-2.3.0.dist-info → dominus_sdk_python_dev-2.5.0.dist-info}/WHEEL +0 -0
- {dominus_sdk_python_dev-2.3.0.dist-info → dominus_sdk_python_dev-2.5.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OracleWebSocket - WebSocket wrapper for Oracle streaming (INTERNAL)
|
|
3
|
+
|
|
4
|
+
Handles:
|
|
5
|
+
- First-message authentication (browser WS doesn't support headers)
|
|
6
|
+
- Binary frames for audio (NOT base64)
|
|
7
|
+
- JSON frames for control messages
|
|
8
|
+
- Ping/pong keepalive during IDLE
|
|
9
|
+
- Transcript parsing and callback firing
|
|
10
|
+
|
|
11
|
+
This module is INTERNAL and should NOT be exported publicly.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import json
|
|
16
|
+
from typing import Callable, Optional
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import websockets
|
|
21
|
+
from websockets.client import WebSocketClientProtocol
|
|
22
|
+
WEBSOCKETS_AVAILABLE = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
WEBSOCKETS_AVAILABLE = False
|
|
25
|
+
WebSocketClientProtocol = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class OracleWebSocketConfig:
|
|
30
|
+
"""Configuration for OracleWebSocket."""
|
|
31
|
+
ping_interval_ms: int = 10000
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OracleWebSocket:
|
|
35
|
+
"""
|
|
36
|
+
OracleWebSocket - Manages WebSocket connection to Oracle service.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, base_url: str, config: Optional[OracleWebSocketConfig] = None):
|
|
40
|
+
if not WEBSOCKETS_AVAILABLE:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
"websockets package is required for Oracle streaming. "
|
|
43
|
+
"Install with: pip install websockets"
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
self._base_url = base_url
|
|
47
|
+
self._config = config or OracleWebSocketConfig()
|
|
48
|
+
self._ws: Optional[WebSocketClientProtocol] = None
|
|
49
|
+
self._authenticated = False
|
|
50
|
+
self._ping_task: Optional[asyncio.Task] = None
|
|
51
|
+
self._receive_task: Optional[asyncio.Task] = None
|
|
52
|
+
|
|
53
|
+
# Callbacks
|
|
54
|
+
self.on_transcript: Optional[Callable[[str, bool, bool], None]] = None
|
|
55
|
+
self.on_error: Optional[Callable[[Exception], None]] = None
|
|
56
|
+
self.on_close: Optional[Callable[[], None]] = None
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_connected(self) -> bool:
|
|
60
|
+
"""Check if WebSocket is connected and authenticated."""
|
|
61
|
+
return (
|
|
62
|
+
self._ws is not None
|
|
63
|
+
and self._ws.open
|
|
64
|
+
and self._authenticated
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def connect(self, user_token: str) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Connect to Oracle WebSocket endpoint with first-message auth.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
user_token: User JWT for authentication
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
Exception: If connection or authentication fails
|
|
76
|
+
"""
|
|
77
|
+
# Convert HTTP URL to WebSocket URL
|
|
78
|
+
ws_url = self._build_websocket_url()
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
self._ws = await websockets.connect(ws_url)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
raise Exception(f"Failed to create WebSocket: {e}")
|
|
84
|
+
|
|
85
|
+
# Send first-message auth
|
|
86
|
+
auth_message = json.dumps({
|
|
87
|
+
"type": "auth",
|
|
88
|
+
"token": user_token,
|
|
89
|
+
})
|
|
90
|
+
await self._ws.send(auth_message)
|
|
91
|
+
|
|
92
|
+
# Wait for ready message
|
|
93
|
+
try:
|
|
94
|
+
response = await asyncio.wait_for(self._ws.recv(), timeout=10.0)
|
|
95
|
+
data = json.loads(response)
|
|
96
|
+
|
|
97
|
+
if data.get("type") == "ready":
|
|
98
|
+
self._authenticated = True
|
|
99
|
+
self._start_ping_interval()
|
|
100
|
+
self._start_receive_loop()
|
|
101
|
+
elif data.get("type") == "error":
|
|
102
|
+
raise Exception(f"Authentication failed: {data.get('message')}")
|
|
103
|
+
else:
|
|
104
|
+
raise Exception(f"Unexpected response type: {data.get('type')}")
|
|
105
|
+
|
|
106
|
+
except asyncio.TimeoutError:
|
|
107
|
+
await self._ws.close()
|
|
108
|
+
raise Exception("Authentication timeout")
|
|
109
|
+
|
|
110
|
+
def send_binary(self, pcm_frame: bytes) -> None:
|
|
111
|
+
"""
|
|
112
|
+
Send binary PCM audio frame.
|
|
113
|
+
Only called by VADGate - NOT exposed publicly.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
pcm_frame: Raw PCM audio data
|
|
117
|
+
"""
|
|
118
|
+
if self._ws and self._ws.open and self._authenticated:
|
|
119
|
+
asyncio.create_task(self._ws.send(pcm_frame))
|
|
120
|
+
|
|
121
|
+
async def send_ping(self) -> None:
|
|
122
|
+
"""Send a ping message for keepalive."""
|
|
123
|
+
if self._ws and self._ws.open and self._authenticated:
|
|
124
|
+
await self._ws.send(json.dumps({"type": "ping"}))
|
|
125
|
+
|
|
126
|
+
async def close(self) -> None:
|
|
127
|
+
"""Close the WebSocket connection gracefully."""
|
|
128
|
+
self._stop_ping_interval()
|
|
129
|
+
self._stop_receive_loop()
|
|
130
|
+
|
|
131
|
+
if self._ws:
|
|
132
|
+
# Send close message before closing
|
|
133
|
+
if self._ws.open:
|
|
134
|
+
try:
|
|
135
|
+
await self._ws.send(json.dumps({"type": "close"}))
|
|
136
|
+
except Exception:
|
|
137
|
+
pass # Ignore send errors during close
|
|
138
|
+
|
|
139
|
+
await self._ws.close()
|
|
140
|
+
self._ws = None
|
|
141
|
+
|
|
142
|
+
self._authenticated = False
|
|
143
|
+
|
|
144
|
+
def _build_websocket_url(self) -> str:
|
|
145
|
+
"""Build WebSocket URL from base URL."""
|
|
146
|
+
# Replace http(s) with ws(s)
|
|
147
|
+
if self._base_url.startswith("https"):
|
|
148
|
+
ws_protocol = "wss"
|
|
149
|
+
http_protocol = "https"
|
|
150
|
+
else:
|
|
151
|
+
ws_protocol = "ws"
|
|
152
|
+
http_protocol = "http"
|
|
153
|
+
|
|
154
|
+
return self._base_url.replace(http_protocol, ws_protocol) + "/api/oracle/stream"
|
|
155
|
+
|
|
156
|
+
def _start_receive_loop(self) -> None:
|
|
157
|
+
"""Start the receive loop for incoming messages."""
|
|
158
|
+
async def receive_loop():
|
|
159
|
+
try:
|
|
160
|
+
while self._ws and self._ws.open:
|
|
161
|
+
try:
|
|
162
|
+
message = await self._ws.recv()
|
|
163
|
+
await self._handle_message(message)
|
|
164
|
+
except websockets.exceptions.ConnectionClosed:
|
|
165
|
+
break
|
|
166
|
+
finally:
|
|
167
|
+
self._authenticated = False
|
|
168
|
+
if self.on_close:
|
|
169
|
+
self.on_close()
|
|
170
|
+
|
|
171
|
+
self._receive_task = asyncio.create_task(receive_loop())
|
|
172
|
+
|
|
173
|
+
def _stop_receive_loop(self) -> None:
|
|
174
|
+
"""Stop the receive loop."""
|
|
175
|
+
if self._receive_task:
|
|
176
|
+
self._receive_task.cancel()
|
|
177
|
+
self._receive_task = None
|
|
178
|
+
|
|
179
|
+
async def _handle_message(self, message) -> None:
|
|
180
|
+
"""Handle incoming WebSocket message."""
|
|
181
|
+
# Binary messages are not expected from server (audio is one-way)
|
|
182
|
+
if isinstance(message, bytes):
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
# Parse JSON message
|
|
186
|
+
try:
|
|
187
|
+
data = json.loads(message)
|
|
188
|
+
except json.JSONDecodeError:
|
|
189
|
+
print("[OracleSDK] Received invalid JSON from server")
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
msg_type = data.get("type")
|
|
193
|
+
|
|
194
|
+
if msg_type == "transcript":
|
|
195
|
+
text = data.get("text", "")
|
|
196
|
+
is_final = data.get("is_final", False)
|
|
197
|
+
speech_final = data.get("speech_final", False)
|
|
198
|
+
|
|
199
|
+
print(f"[OracleSDK] Transcript received: text={text[:40] if text else ''!r}, "
|
|
200
|
+
f"is_final={is_final}, speech_final={speech_final}")
|
|
201
|
+
|
|
202
|
+
if self.on_transcript:
|
|
203
|
+
self.on_transcript(text, is_final, speech_final)
|
|
204
|
+
|
|
205
|
+
elif msg_type == "pong":
|
|
206
|
+
# Keepalive response - no action needed
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
elif msg_type == "error":
|
|
210
|
+
error = Exception(data.get("message", "Unknown error"))
|
|
211
|
+
if self.on_error:
|
|
212
|
+
self.on_error(error)
|
|
213
|
+
|
|
214
|
+
else:
|
|
215
|
+
print(f"[OracleSDK] Unknown message type: {msg_type}")
|
|
216
|
+
|
|
217
|
+
def _start_ping_interval(self) -> None:
|
|
218
|
+
"""Start ping interval for keepalive."""
|
|
219
|
+
async def ping_loop():
|
|
220
|
+
while True:
|
|
221
|
+
await asyncio.sleep(self._config.ping_interval_ms / 1000.0)
|
|
222
|
+
await self.send_ping()
|
|
223
|
+
|
|
224
|
+
self._ping_task = asyncio.create_task(ping_loop())
|
|
225
|
+
|
|
226
|
+
def _stop_ping_interval(self) -> None:
|
|
227
|
+
"""Stop ping interval."""
|
|
228
|
+
if self._ping_task:
|
|
229
|
+
self._ping_task.cancel()
|
|
230
|
+
self._ping_task = None
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OracleSession - Main session class for streaming transcription.
|
|
3
|
+
|
|
4
|
+
Provides the public API for Oracle streaming:
|
|
5
|
+
- start() / stop() lifecycle
|
|
6
|
+
- Event callbacks for transcripts, VAD state, errors
|
|
7
|
+
- NO send_audio() exposed - VAD gates all audio automatically
|
|
8
|
+
|
|
9
|
+
The session handles everything internally:
|
|
10
|
+
- Microphone access and audio capture
|
|
11
|
+
- Resampling to 16kHz mono PCM
|
|
12
|
+
- VAD gating (only sends speech, not silence)
|
|
13
|
+
- WebSocket connection to Oracle
|
|
14
|
+
- Ping/pong keepalive during IDLE
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from typing import Callable, Optional, List
|
|
19
|
+
|
|
20
|
+
from .oracle_websocket import OracleWebSocket, OracleWebSocketConfig
|
|
21
|
+
from .vad_gate import VADGate
|
|
22
|
+
from .audio_capture import AudioCapture
|
|
23
|
+
from .types import VADState, ResolvedOracleSessionOptions
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class OracleSession:
|
|
27
|
+
"""
|
|
28
|
+
OracleSession - Real-time speech-to-text with VAD gating.
|
|
29
|
+
|
|
30
|
+
Usage:
|
|
31
|
+
session = dominus.oracle.create_session(user_jwt)
|
|
32
|
+
|
|
33
|
+
session.on_ready = lambda: print('Ready')
|
|
34
|
+
session.on_interim = lambda text: set_live_text(text)
|
|
35
|
+
session.on_utterance = lambda text: send_to_curator(text)
|
|
36
|
+
session.on_vad_state_change = lambda state: update_mic_icon(state)
|
|
37
|
+
|
|
38
|
+
await session.start()
|
|
39
|
+
# ... user speaks ...
|
|
40
|
+
await session.stop()
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: str,
|
|
46
|
+
user_token: str,
|
|
47
|
+
options: ResolvedOracleSessionOptions
|
|
48
|
+
):
|
|
49
|
+
self._user_token = user_token
|
|
50
|
+
self._is_active = False
|
|
51
|
+
self._vad_state: VADState = "idle"
|
|
52
|
+
|
|
53
|
+
# Track accumulated transcript for utterance detection
|
|
54
|
+
# Since VAD stops sending audio before Deepgram's utterance_end_ms,
|
|
55
|
+
# we use VAD TRAILING->IDLE transition as utterance boundary
|
|
56
|
+
self._pending_utterance = ""
|
|
57
|
+
|
|
58
|
+
# Initialize internal components
|
|
59
|
+
self._ws = OracleWebSocket(
|
|
60
|
+
base_url,
|
|
61
|
+
OracleWebSocketConfig(ping_interval_ms=options.ping_interval_ms)
|
|
62
|
+
)
|
|
63
|
+
self._vad = VADGate(options)
|
|
64
|
+
self._audio = AudioCapture()
|
|
65
|
+
|
|
66
|
+
# ========== EVENT CALLBACKS ==========
|
|
67
|
+
|
|
68
|
+
# Called when session is ready to receive speech
|
|
69
|
+
self.on_ready: Optional[Callable[[], None]] = None
|
|
70
|
+
|
|
71
|
+
# Called with interim transcripts (may change)
|
|
72
|
+
self.on_interim: Optional[Callable[[str], None]] = None
|
|
73
|
+
|
|
74
|
+
# Called when a transcript segment is finalized
|
|
75
|
+
self.on_final: Optional[Callable[[str], None]] = None
|
|
76
|
+
|
|
77
|
+
# Called when user finishes an utterance (trigger for Curator)
|
|
78
|
+
self.on_utterance: Optional[Callable[[str], None]] = None
|
|
79
|
+
|
|
80
|
+
# Called on any error
|
|
81
|
+
self.on_error: Optional[Callable[[Exception], None]] = None
|
|
82
|
+
|
|
83
|
+
# Called when session closes
|
|
84
|
+
self.on_close: Optional[Callable[[], None]] = None
|
|
85
|
+
|
|
86
|
+
# Called when VAD state changes (for UI indicators)
|
|
87
|
+
self.on_vad_state_change: Optional[Callable[[VADState], None]] = None
|
|
88
|
+
|
|
89
|
+
# ========== PUBLIC GETTERS ==========
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def is_active(self) -> bool:
|
|
93
|
+
"""Check if session is currently active."""
|
|
94
|
+
return self._is_active
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def vad_state(self) -> VADState:
|
|
98
|
+
"""Get current VAD state."""
|
|
99
|
+
return self._vad_state
|
|
100
|
+
|
|
101
|
+
# ========== LIFECYCLE METHODS ==========
|
|
102
|
+
|
|
103
|
+
async def start(self) -> None:
|
|
104
|
+
"""
|
|
105
|
+
Start the transcription session.
|
|
106
|
+
|
|
107
|
+
- Requests microphone permission
|
|
108
|
+
- Connects WebSocket to Oracle
|
|
109
|
+
- Begins VAD-gated audio streaming
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
Exception: If mic permission denied or connection fails
|
|
113
|
+
"""
|
|
114
|
+
if self._is_active:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# 1. Initialize VAD
|
|
119
|
+
await self._vad.initialize()
|
|
120
|
+
|
|
121
|
+
# 2. Connect WebSocket with first-message auth
|
|
122
|
+
await self._ws.connect(self._user_token)
|
|
123
|
+
|
|
124
|
+
# 3. Wire up transcript events
|
|
125
|
+
def on_transcript(text: str, is_final: bool, speech_final: bool):
|
|
126
|
+
if not is_final:
|
|
127
|
+
# Interim: show in live view (may change)
|
|
128
|
+
if self.on_interim:
|
|
129
|
+
self.on_interim(text)
|
|
130
|
+
else:
|
|
131
|
+
# Final: accumulate for utterance
|
|
132
|
+
if self.on_final:
|
|
133
|
+
self.on_final(text)
|
|
134
|
+
# Accumulate finals - space-separated
|
|
135
|
+
if self._pending_utterance:
|
|
136
|
+
self._pending_utterance += " " + text
|
|
137
|
+
else:
|
|
138
|
+
self._pending_utterance = text
|
|
139
|
+
|
|
140
|
+
# If Deepgram does send speech_final, honor it
|
|
141
|
+
if speech_final and text:
|
|
142
|
+
self._pending_utterance = "" # Clear since we're emitting
|
|
143
|
+
if self.on_utterance:
|
|
144
|
+
self.on_utterance(text)
|
|
145
|
+
|
|
146
|
+
self._ws.on_transcript = on_transcript
|
|
147
|
+
|
|
148
|
+
self._ws.on_error = lambda error: (
|
|
149
|
+
self.on_error(error) if self.on_error else None
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def on_ws_close():
|
|
153
|
+
self._is_active = False
|
|
154
|
+
if self.on_close:
|
|
155
|
+
self.on_close()
|
|
156
|
+
|
|
157
|
+
self._ws.on_close = on_ws_close
|
|
158
|
+
|
|
159
|
+
# 4. Start audio capture
|
|
160
|
+
await self._audio.start()
|
|
161
|
+
|
|
162
|
+
# 5. Wire audio through VAD gate
|
|
163
|
+
def on_audio_frame(pcm_frame: bytes):
|
|
164
|
+
self._vad.process_frame(pcm_frame)
|
|
165
|
+
|
|
166
|
+
self._audio.on_frame = on_audio_frame
|
|
167
|
+
|
|
168
|
+
# 6. VAD gate controls what gets sent
|
|
169
|
+
def on_send_audio(frames: List[bytes]):
|
|
170
|
+
for frame in frames:
|
|
171
|
+
self._ws.send_binary(frame)
|
|
172
|
+
|
|
173
|
+
self._vad.on_send_audio = on_send_audio
|
|
174
|
+
|
|
175
|
+
def on_vad_state_change(state: VADState):
|
|
176
|
+
prev_state = self._vad_state
|
|
177
|
+
self._vad_state = state
|
|
178
|
+
|
|
179
|
+
if self.on_vad_state_change:
|
|
180
|
+
self.on_vad_state_change(state)
|
|
181
|
+
|
|
182
|
+
# When VAD transitions to IDLE from TRAILING, emit accumulated utterance
|
|
183
|
+
if state == "idle" and (prev_state == "trailing" or prev_state == "speaking"):
|
|
184
|
+
if self._pending_utterance.strip():
|
|
185
|
+
print(f"[OracleSDK] VAD->IDLE: emitting utterance: "
|
|
186
|
+
f"{self._pending_utterance[:50]!r}")
|
|
187
|
+
if self.on_utterance:
|
|
188
|
+
self.on_utterance(self._pending_utterance.strip())
|
|
189
|
+
self._pending_utterance = ""
|
|
190
|
+
|
|
191
|
+
self._vad.on_state_change = on_vad_state_change
|
|
192
|
+
|
|
193
|
+
self._is_active = True
|
|
194
|
+
|
|
195
|
+
if self.on_ready:
|
|
196
|
+
self.on_ready()
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
# Clean up on failure
|
|
200
|
+
await self._cleanup()
|
|
201
|
+
raise e
|
|
202
|
+
|
|
203
|
+
async def stop(self) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Stop the transcription session.
|
|
206
|
+
|
|
207
|
+
- Stops microphone capture
|
|
208
|
+
- Closes WebSocket connection
|
|
209
|
+
- Cleans up resources
|
|
210
|
+
"""
|
|
211
|
+
if not self._is_active:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
self._is_active = False
|
|
215
|
+
await self._cleanup()
|
|
216
|
+
|
|
217
|
+
if self.on_close:
|
|
218
|
+
self.on_close()
|
|
219
|
+
|
|
220
|
+
# ========== PRIVATE METHODS ==========
|
|
221
|
+
|
|
222
|
+
async def _cleanup(self) -> None:
|
|
223
|
+
"""Clean up all resources."""
|
|
224
|
+
# Stop audio capture
|
|
225
|
+
await self._audio.stop()
|
|
226
|
+
|
|
227
|
+
# Close WebSocket
|
|
228
|
+
await self._ws.close()
|
|
229
|
+
|
|
230
|
+
# Reset VAD state
|
|
231
|
+
self._vad.reset()
|
|
232
|
+
self._vad_state = "idle"
|
|
233
|
+
|
|
234
|
+
# Clear pending utterance
|
|
235
|
+
self._pending_utterance = ""
|
|
236
|
+
|
|
237
|
+
def dispose(self) -> None:
|
|
238
|
+
"""Dispose of all resources (for cleanup on unmount)."""
|
|
239
|
+
asyncio.create_task(self.stop())
|
|
240
|
+
|
|
241
|
+
# Clear all callbacks
|
|
242
|
+
self.on_ready = None
|
|
243
|
+
self.on_interim = None
|
|
244
|
+
self.on_final = None
|
|
245
|
+
self.on_utterance = None
|
|
246
|
+
self.on_error = None
|
|
247
|
+
self.on_close = None
|
|
248
|
+
self.on_vad_state_change = None
|
|
249
|
+
|
|
250
|
+
# Dispose internal components
|
|
251
|
+
self._audio.dispose()
|
|
252
|
+
self._vad.dispose()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Oracle Namespace Types - Public interfaces for streaming transcription.
|
|
3
|
+
|
|
4
|
+
Oracle provides real-time speech-to-text via Deepgram streaming,
|
|
5
|
+
with built-in VAD (Voice Activity Detection) for cost optimization.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Literal, Optional, Callable, TypedDict
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# VAD (Voice Activity Detection) states
|
|
13
|
+
VADState = Literal["idle", "armed", "speaking", "trailing"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class OracleSessionOptions:
|
|
18
|
+
"""Configuration options for OracleSession."""
|
|
19
|
+
|
|
20
|
+
# Pre-roll buffer duration in ms (default: 320)
|
|
21
|
+
preroll_ms: int = 320
|
|
22
|
+
|
|
23
|
+
# Post-roll duration in ms (default: 800)
|
|
24
|
+
postroll_ms: int = 800
|
|
25
|
+
|
|
26
|
+
# VAD confirmation time in ms (default: 160)
|
|
27
|
+
armed_confirm_ms: int = 160
|
|
28
|
+
|
|
29
|
+
# VAD speech threshold 0-1 (default: 0.5) - used for energy-based fallback
|
|
30
|
+
vad_threshold: float = 0.5
|
|
31
|
+
|
|
32
|
+
# Energy threshold for fallback VAD (default: 800)
|
|
33
|
+
energy_threshold: int = 800
|
|
34
|
+
|
|
35
|
+
# Ping interval in ms for keepalive (default: 10000)
|
|
36
|
+
ping_interval_ms: int = 10000
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class ResolvedOracleSessionOptions:
|
|
41
|
+
"""Resolved session options with all defaults applied."""
|
|
42
|
+
|
|
43
|
+
preroll_ms: int
|
|
44
|
+
postroll_ms: int
|
|
45
|
+
armed_confirm_ms: int
|
|
46
|
+
vad_threshold: float
|
|
47
|
+
energy_threshold: int
|
|
48
|
+
ping_interval_ms: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Default configuration values
|
|
52
|
+
DEFAULT_OPTIONS = ResolvedOracleSessionOptions(
|
|
53
|
+
preroll_ms=320, # 16 frames x 20ms
|
|
54
|
+
postroll_ms=800, # Post-roll trailing duration
|
|
55
|
+
armed_confirm_ms=160, # 8 frames x 20ms to confirm speech
|
|
56
|
+
vad_threshold=0.5, # VAD threshold
|
|
57
|
+
energy_threshold=800, # Energy-based fallback threshold
|
|
58
|
+
ping_interval_ms=10000, # 10 second ping interval
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class AudioConfig:
|
|
64
|
+
"""Audio configuration constants."""
|
|
65
|
+
|
|
66
|
+
SAMPLE_RATE: int = 16000 # 16kHz
|
|
67
|
+
CHANNELS: int = 1 # Mono
|
|
68
|
+
FRAME_DURATION_MS: int = 20 # 20ms frames
|
|
69
|
+
FRAME_SIZE_BYTES: int = 640 # 320 samples x 2 bytes per sample
|
|
70
|
+
SAMPLES_PER_FRAME: int = 320 # 16kHz x 20ms
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
AUDIO_CONFIG = AudioConfig()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ServerReadyMessage(TypedDict):
|
|
77
|
+
"""Server ready message."""
|
|
78
|
+
type: Literal["ready"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ServerTranscriptMessage(TypedDict):
|
|
82
|
+
"""Server transcript message."""
|
|
83
|
+
type: Literal["transcript"]
|
|
84
|
+
text: str
|
|
85
|
+
is_final: bool
|
|
86
|
+
speech_final: bool
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class ServerPongMessage(TypedDict):
|
|
90
|
+
"""Server pong message."""
|
|
91
|
+
type: Literal["pong"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class ServerErrorMessage(TypedDict):
|
|
95
|
+
"""Server error message."""
|
|
96
|
+
type: Literal["error"]
|
|
97
|
+
message: str
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Union type for server messages
|
|
101
|
+
ServerMessage = ServerReadyMessage | ServerTranscriptMessage | ServerPongMessage | ServerErrorMessage
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class OracleSessionCallbacks:
|
|
106
|
+
"""Event callbacks for OracleSession."""
|
|
107
|
+
|
|
108
|
+
# Called when session is ready to receive speech
|
|
109
|
+
on_ready: Optional[Callable[[], None]] = None
|
|
110
|
+
|
|
111
|
+
# Called with interim transcripts (may change)
|
|
112
|
+
on_interim: Optional[Callable[[str], None]] = None
|
|
113
|
+
|
|
114
|
+
# Called when a transcript segment is finalized
|
|
115
|
+
on_final: Optional[Callable[[str], None]] = None
|
|
116
|
+
|
|
117
|
+
# Called when user finishes an utterance (trigger for Curator)
|
|
118
|
+
on_utterance: Optional[Callable[[str], None]] = None
|
|
119
|
+
|
|
120
|
+
# Called on any error
|
|
121
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
122
|
+
|
|
123
|
+
# Called when session closes
|
|
124
|
+
on_close: Optional[Callable[[], None]] = None
|
|
125
|
+
|
|
126
|
+
# Called when VAD state changes (for UI indicators)
|
|
127
|
+
on_vad_state_change: Optional[Callable[[VADState], None]] = None
|