dominus-sdk-python-dev 2.4.0__py3-none-any.whl → 2.5.1__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,276 @@
1
+ """
2
+ VADGate - Voice Activity Detection Gate (INTERNAL)
3
+
4
+ Implements a 4-state machine for VAD gating:
5
+ IDLE -> ARMED -> SPEAKING -> TRAILING -> IDLE
6
+
7
+ Features:
8
+ - Pre-roll buffer (320ms) to capture word onsets
9
+ - Armed confirmation (160ms) to reject false triggers
10
+ - Post-roll/trailing (800ms) to avoid chopped endings
11
+ - Circular buffer for pre-roll storage
12
+
13
+ This module is INTERNAL and should NOT be exported publicly.
14
+ """
15
+
16
+ import asyncio
17
+ import struct
18
+ import math
19
+ from typing import List, Callable, Optional
20
+ from dataclasses import dataclass
21
+
22
+ from .types import VADState, ResolvedOracleSessionOptions
23
+
24
+
25
+ @dataclass
26
+ class VADGateConfig:
27
+ """Configuration for VADGate."""
28
+
29
+ # Number of frames in pre-roll buffer
30
+ preroll_frames: int
31
+
32
+ # Number of frames to confirm armed state
33
+ armed_confirm_frames: int
34
+
35
+ # Number of consecutive silence frames to confirm trailing (hysteresis)
36
+ silence_confirm_frames: int
37
+
38
+ # Post-roll timeout in ms
39
+ postroll_ms: int
40
+
41
+ # VAD threshold (0-1)
42
+ threshold: float
43
+
44
+ # Energy threshold for fallback VAD
45
+ energy_threshold: int
46
+
47
+
48
+ def detect_speech_by_energy(pcm_frame: bytes, threshold: int = 500) -> bool:
49
+ """
50
+ Energy-based speech detection fallback.
51
+ Calculates RMS energy and compares against threshold.
52
+
53
+ Args:
54
+ pcm_frame: Raw PCM16 audio data
55
+ threshold: RMS energy threshold
56
+
57
+ Returns:
58
+ True if speech detected, False otherwise
59
+ """
60
+ # Convert bytes to int16 samples
61
+ num_samples = len(pcm_frame) // 2
62
+ if num_samples == 0:
63
+ return False
64
+
65
+ samples = struct.unpack(f"<{num_samples}h", pcm_frame)
66
+
67
+ # Calculate RMS energy
68
+ sum_squares = sum(s * s for s in samples)
69
+ rms = math.sqrt(sum_squares / num_samples)
70
+
71
+ return rms > threshold
72
+
73
+
74
+ class EnergyVAD:
75
+ """
76
+ Energy-based VAD fallback.
77
+ Uses RMS energy detection when webrtcvad is unavailable.
78
+ """
79
+
80
+ def __init__(self, energy_threshold: int):
81
+ self.energy_threshold = energy_threshold
82
+
83
+ def is_speech(self, pcm_frame: bytes, _threshold: float) -> bool:
84
+ return detect_speech_by_energy(pcm_frame, self.energy_threshold)
85
+
86
+ def dispose(self) -> None:
87
+ pass
88
+
89
+
90
+ class WebRTCVAD:
91
+ """
92
+ WebRTC VAD wrapper.
93
+ Falls back to energy-based detection if webrtcvad is unavailable.
94
+ """
95
+
96
+ def __init__(self, threshold: float, energy_threshold: int, sample_rate: int = 16000):
97
+ self.threshold = threshold
98
+ self.sample_rate = sample_rate
99
+ self.fallback = EnergyVAD(energy_threshold)
100
+ self.vad = None
101
+ self.use_fallback = False
102
+
103
+ try:
104
+ import webrtcvad
105
+ self.vad = webrtcvad.Vad()
106
+ # Aggressiveness level 0-3 (3 = most aggressive filtering)
107
+ # Map threshold (0-1) to aggressiveness (0-3)
108
+ aggressiveness = max(0, min(3, int((1 - threshold) * 3)))
109
+ self.vad.set_mode(aggressiveness)
110
+ except ImportError:
111
+ print("[OracleSDK] webrtcvad not available, using energy-based fallback")
112
+ self.use_fallback = True
113
+
114
+ def is_speech(self, pcm_frame: bytes, threshold: float) -> bool:
115
+ if self.use_fallback or self.vad is None:
116
+ return self.fallback.is_speech(pcm_frame, threshold)
117
+
118
+ try:
119
+ # webrtcvad requires specific frame sizes: 10, 20, or 30 ms
120
+ # For 16kHz, 20ms = 320 samples = 640 bytes
121
+ return self.vad.is_speech(pcm_frame, self.sample_rate)
122
+ except Exception:
123
+ # Fall back to energy-based on any error
124
+ return self.fallback.is_speech(pcm_frame, threshold)
125
+
126
+ def dispose(self) -> None:
127
+ self.vad = None
128
+
129
+
130
+ class VADGate:
131
+ """
132
+ VADGate - 4-state Voice Activity Detection machine.
133
+
134
+ State transitions:
135
+ - IDLE -> ARMED: When VAD detects speech
136
+ - ARMED -> SPEAKING: After armed_confirm_frames of continuous speech
137
+ - ARMED -> IDLE: If speech stops before confirmation
138
+ - SPEAKING -> TRAILING: When VAD detects silence
139
+ - TRAILING -> SPEAKING: If speech resumes
140
+ - TRAILING -> IDLE: After postroll_ms timeout
141
+ """
142
+
143
+ def __init__(
144
+ self,
145
+ options: ResolvedOracleSessionOptions,
146
+ frame_duration_ms: int = 20,
147
+ loop: Optional[asyncio.AbstractEventLoop] = None
148
+ ):
149
+ self._state: VADState = "idle"
150
+ self._pre_roll_buffer: List[bytes] = []
151
+ self._armed_frame_count = 0
152
+ self._silence_frame_count = 0
153
+ self._trailing_task: Optional[asyncio.Task] = None
154
+ self._loop = loop
155
+
156
+ self._config = VADGateConfig(
157
+ preroll_frames=math.ceil(options.preroll_ms / frame_duration_ms),
158
+ armed_confirm_frames=math.ceil(options.armed_confirm_ms / frame_duration_ms),
159
+ silence_confirm_frames=8, # 160ms of silence before trailing
160
+ postroll_ms=options.postroll_ms,
161
+ threshold=options.vad_threshold,
162
+ energy_threshold=options.energy_threshold,
163
+ )
164
+
165
+ # Use energy-based VAD as the primary implementation
166
+ self._vad_model = EnergyVAD(self._config.energy_threshold)
167
+
168
+ # Callbacks
169
+ self.on_send_audio: Optional[Callable[[List[bytes]], None]] = None
170
+ self.on_state_change: Optional[Callable[[VADState], None]] = None
171
+
172
+ @property
173
+ def state(self) -> VADState:
174
+ """Get current VAD state."""
175
+ return self._state
176
+
177
+ async def initialize(self) -> None:
178
+ """Initialize the VAD model (call before processing frames)."""
179
+ # Energy VAD doesn't need initialization
180
+ pass
181
+
182
+ def process_frame(self, pcm_frame: bytes) -> None:
183
+ """
184
+ Process an audio frame through the VAD gate.
185
+
186
+ Args:
187
+ pcm_frame: 20ms PCM16 audio frame (640 bytes)
188
+ """
189
+ # Maintain circular pre-roll buffer
190
+ self._pre_roll_buffer.append(pcm_frame)
191
+ if len(self._pre_roll_buffer) > self._config.preroll_frames:
192
+ self._pre_roll_buffer.pop(0)
193
+
194
+ is_speech = self._vad_model.is_speech(pcm_frame, self._config.threshold)
195
+
196
+ if self._state == "idle":
197
+ if is_speech:
198
+ self._set_state("armed")
199
+ self._armed_frame_count = 1
200
+
201
+ elif self._state == "armed":
202
+ if is_speech:
203
+ self._armed_frame_count += 1
204
+ if self._armed_frame_count >= self._config.armed_confirm_frames:
205
+ # Confirmed speech - flush pre-roll buffer
206
+ if self.on_send_audio:
207
+ self.on_send_audio(list(self._pre_roll_buffer))
208
+ self._set_state("speaking")
209
+ else:
210
+ # False trigger - back to idle
211
+ self._set_state("idle")
212
+ self._armed_frame_count = 0
213
+
214
+ elif self._state == "speaking":
215
+ if self.on_send_audio:
216
+ self.on_send_audio([pcm_frame])
217
+
218
+ if not is_speech:
219
+ # Hysteresis: require multiple consecutive silence frames
220
+ self._silence_frame_count += 1
221
+ if self._silence_frame_count >= self._config.silence_confirm_frames:
222
+ self._set_state("trailing")
223
+ self._start_trailing_timer()
224
+ self._silence_frame_count = 0
225
+ else:
226
+ # Speech detected - reset silence counter
227
+ self._silence_frame_count = 0
228
+
229
+ elif self._state == "trailing":
230
+ if self.on_send_audio:
231
+ self.on_send_audio([pcm_frame])
232
+
233
+ if is_speech:
234
+ self._cancel_trailing_timer()
235
+ self._set_state("speaking")
236
+
237
+ def reset(self) -> None:
238
+ """Reset VAD state to idle."""
239
+ self._cancel_trailing_timer()
240
+ self._state = "idle"
241
+ self._pre_roll_buffer = []
242
+ self._armed_frame_count = 0
243
+ self._silence_frame_count = 0
244
+
245
+ def dispose(self) -> None:
246
+ """Clean up resources."""
247
+ self._cancel_trailing_timer()
248
+ self._vad_model.dispose()
249
+ self.on_send_audio = None
250
+ self.on_state_change = None
251
+
252
+ def _set_state(self, new_state: VADState) -> None:
253
+ """Update VAD state and fire callback."""
254
+ if self._state != new_state:
255
+ self._state = new_state
256
+ if self.on_state_change:
257
+ self.on_state_change(new_state)
258
+
259
+ def _start_trailing_timer(self) -> None:
260
+ """
261
+ Start the trailing timeout.
262
+ After postroll_ms, transition back to idle.
263
+ """
264
+ async def trailing_timeout():
265
+ await asyncio.sleep(self._config.postroll_ms / 1000.0)
266
+ self._set_state("idle")
267
+ self._trailing_task = None
268
+
269
+ loop = self._loop or asyncio.get_event_loop()
270
+ self._trailing_task = loop.create_task(trailing_timeout())
271
+
272
+ def _cancel_trailing_timer(self) -> None:
273
+ """Cancel the trailing timeout."""
274
+ if self._trailing_task is not None:
275
+ self._trailing_task.cancel()
276
+ self._trailing_task = None
dominus/start.py CHANGED
@@ -342,6 +342,13 @@ class Dominus:
342
342
  # Secure namespace with proper audit context support
343
343
  self.db_secure = SecureNS(self)
344
344
 
345
+ # Oracle namespace (Speech-to-Text streaming)
346
+ from .namespaces.oracle import OracleNamespace
347
+ self.oracle = OracleNamespace(self)
348
+
349
+ # Alias for Node.js SDK parity (stt = oracle)
350
+ self.stt = self.oracle
351
+
345
352
  # Cache for JWT public key
346
353
  self._public_key_cache = None
347
354
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python-dev
3
- Version: 2.4.0
3
+ Version: 2.5.1
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -25,9 +25,20 @@ Requires-Dist: bcrypt>=4.0.0
25
25
  Requires-Dist: cryptography>=41.0.0
26
26
  Provides-Extra: jwt
27
27
  Requires-Dist: PyJWT>=2.8.0; extra == "jwt"
28
+ Provides-Extra: oracle
29
+ Requires-Dist: websockets>=12.0; extra == "oracle"
30
+ Requires-Dist: sounddevice>=0.4.6; extra == "oracle"
31
+ Requires-Dist: numpy>=1.24.0; extra == "oracle"
32
+ Requires-Dist: webrtcvad>=2.0.10; extra == "oracle"
28
33
  Provides-Extra: dev
29
34
  Requires-Dist: pytest>=7.0.0; extra == "dev"
30
35
  Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
36
+ Provides-Extra: all
37
+ Requires-Dist: PyJWT>=2.8.0; extra == "all"
38
+ Requires-Dist: websockets>=12.0; extra == "all"
39
+ Requires-Dist: sounddevice>=0.4.6; extra == "all"
40
+ Requires-Dist: numpy>=1.24.0; extra == "all"
41
+ Requires-Dist: webrtcvad>=2.0.10; extra == "all"
31
42
 
32
43
  # CB Dominus SDK for Python
33
44
 
@@ -1,16 +1,14 @@
1
- dominus/__init__.py,sha256=_gX9fwk5BlNvP1YEEmOGG7pftBiJYnY8PRZ9sHAoZL4,4841
1
+ dominus/__init__.py,sha256=fBZgMu7J7o7DCUNH9PStQ873LW5n9IREXsuu6EsXO70,5132
2
2
  dominus/errors.py,sha256=Sfpg_4zlraE6fprdSu7RDNmSDv0Eg5OZRcPhNUQhNGs,5761
3
- dominus/start.py,sha256=H6QOEYw5_YK-lFZ7AhQpQPACvKPlb8R5sw_MA0rcsmQ,32503
3
+ dominus/start.py,sha256=F_Z9qu8m5YLKk9uCeM9S2vnSfArDJVojoi6MDe8pNqE,32743
4
4
  dominus/config/__init__.py,sha256=nvLLNh4dM_MCqcfF3XFS2NnVn44q0RYHEfAPP-2G5To,434
5
5
  dominus/config/endpoints.py,sha256=fsV5c7vaicpJlF735o46FP4Xlu2Ap4rSvnNa4F0TOYg,1574
6
6
  dominus/helpers/__init__.py,sha256=cyoBvw1QJugtGt0gYitIz1yxOaMmBa-wMXqbh-fi4PU,61
7
7
  dominus/helpers/auth.py,sha256=IVZLelikaGIAZvyc-q7EoPd8fBNVlGffGXQkTP4x5Fk,633
8
8
  dominus/helpers/cache.py,sha256=RgCTPLmHoOXGkxF-pLP9buKPkvZDYHtsLUxbTFjheJI,6028
9
9
  dominus/helpers/core.py,sha256=DeCo2sCmXJLnitJSDzRKcQGwYXyQOw1KRvzxDI6DQsA,24174
10
- dominus/helpers/crypto.py,sha256=XvP9ud5jJ9KPvIQmtAOeXZS4WUyCNb8X-RL_eOB7WNE,2890
10
+ dominus/helpers/crypto.py,sha256=R1yZittVykTQzGVJdb_uMQuF3Y1uGuQRJHZ1BTWT9lU,3014
11
11
  dominus/namespaces/__init__.py,sha256=Kh7tw-5LmY-CpZL2BdqbHc93QLjxTOCGLUVPwAqqDnE,659
12
- dominus/namespaces/_deprecated_crossover.py,sha256=NY73vatvc4CDgML9QMqmvoEjRr_t-Vq6FHtbby0M2-M,385
13
- dominus/namespaces/_deprecated_sql.py,sha256=U57p5hGFbsPEcNEl12m7tYTWux7j2rcCHbCjmNMJ8oY,38318
14
12
  dominus/namespaces/admin.py,sha256=qkGoteAJ6cqdNub4zL1mtyxAQtsM3-nUplRSgw2fMZY,1640
15
13
  dominus/namespaces/auth.py,sha256=Nqy4EOA1qMINvVuvf2-RnKPwN6TyLiD3vmz1G4J7TQg,55462
16
14
  dominus/namespaces/courier.py,sha256=WAhHpswhO8S_eEzn5zgBGhDqhgGxfDR_gs4i1rnotNw,6810
@@ -25,10 +23,14 @@ dominus/namespaces/portal.py,sha256=6T0ul2spnhcDORh1JN2v7zDbkdWqk0FblWKFIeUjSsA,
25
23
  dominus/namespaces/redis.py,sha256=WctuQdUddifDiato6QHntv1ulvAQVVC01iq_k2WUh9A,9384
26
24
  dominus/namespaces/secrets.py,sha256=6xhUJVeDBb5-AiwrwjozarwAc7SoiJ-2Rq6ByBAx684,3572
27
25
  dominus/namespaces/secure.py,sha256=9idGY5R6k7b-JM1SAF_1Gt-YV5OempyxFBflgLJieLo,7168
26
+ dominus/namespaces/oracle/__init__.py,sha256=XiJ8P-vFln73p4r1_ZXgmNQvawkNUt5htuGD51JCoBA,4584
27
+ dominus/namespaces/oracle/audio_capture.py,sha256=1-QcoAHYT1idEBGAJ7g4qpu5QeKdmwiyWciB6K0WkNM,11201
28
+ dominus/namespaces/oracle/oracle_websocket.py,sha256=VsKKPTZmtdchoXj9bFel3hrWUVELS-5UD7LNFWlDdfg,7540
29
+ dominus/namespaces/oracle/session.py,sha256=1As6q-bs0Ynoz3fsNk18E7CuouPROnVadCY_HeHRTkw,7998
30
+ dominus/namespaces/oracle/types.py,sha256=YGwb-PfKikRGNk4af5Av2DD9YPLJpogM21Co9X4T_cU,3517
31
+ dominus/namespaces/oracle/vad_gate.py,sha256=Gpb5q72-MSGYm4PVK64E4w6kawx5AMb1SsJ2uOJrtFw,9010
28
32
  dominus/services/__init__.py,sha256=LQl5sx6DHhX1xCN3MRoUgffxqwm3Mfm525hijyFods8,43
29
- dominus/services/_deprecated_architect.py,sha256=EAMhUK8C_4DLgYyKGVqcXrroEoAenWeuKWnPMowGMQk,14985
30
- dominus/services/_deprecated_sovereign.py,sha256=is9SQVCcmcGUEFKULqitHlk67pfpfkQE-ApU-gVjIEE,3511
31
- dominus_sdk_python_dev-2.4.0.dist-info/METADATA,sha256=yKb6wAvg7axw31k6XZ20krPsnoJCknhMo1JQMcoFvbg,16918
32
- dominus_sdk_python_dev-2.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- dominus_sdk_python_dev-2.4.0.dist-info/top_level.txt,sha256=515zxMIbX0DpheRbjvNqKIt_AFqdFjX41jtyp_SqLf4,8
34
- dominus_sdk_python_dev-2.4.0.dist-info/RECORD,,
33
+ dominus_sdk_python_dev-2.5.1.dist-info/METADATA,sha256=MeDbZyg_VXsneDDnh7vZ_4X7qe6CpaEwOFr1-X5QjPM,17401
34
+ dominus_sdk_python_dev-2.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ dominus_sdk_python_dev-2.5.1.dist-info/top_level.txt,sha256=515zxMIbX0DpheRbjvNqKIt_AFqdFjX41jtyp_SqLf4,8
36
+ dominus_sdk_python_dev-2.5.1.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- """Crossover (admin) operations namespace - DEPRECATED
2
-
3
- Note: This namespace has been removed from the SDK.
4
- Provisioning and admin operations should be done via Architect's
5
- /api/crossover endpoint directly, not through the SDK.
6
-
7
- This file is kept as a placeholder to prevent import errors in
8
- any code that may still reference it.
9
- """
10
- # Empty file - all crossover functionality removed