ojin-client 0.1.7.dev11__tar.gz → 0.1.7.dev12__tar.gz
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.
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/PKG-INFO +1 -1
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/ojin_persona_client.py +119 -21
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/PKG-INFO +1 -1
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/pyproject.toml +1 -1
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/README.md +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/__init__.py +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/cacert.pem +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/entities/interaction_messages.py +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/entities/session_messages.py +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin/ojin_persona_messages.py +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/SOURCES.txt +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/dependency_links.txt +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/requires.txt +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/top_level.txt +0 -0
- {ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""WebSocket client for OJIN Persona service with
|
|
1
|
+
"""WebSocket client for OJIN Persona service with send buffer management."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
@@ -91,8 +91,10 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
91
91
|
self._split_audio_task: Optional[asyncio.Task] = None
|
|
92
92
|
self._audio_queue: asyncio.Queue[OjinPersonaInteractionInputMessage] = asyncio.Queue()
|
|
93
93
|
|
|
94
|
-
# Add cancellation event for
|
|
94
|
+
# Add cancellation event and send queue for managing buffer backpressure
|
|
95
95
|
self._cancel_event = asyncio.Event()
|
|
96
|
+
self._send_queue: asyncio.Queue = asyncio.Queue(maxsize=100) # Limit queue size
|
|
97
|
+
self._send_task: Optional[asyncio.Task] = None
|
|
96
98
|
|
|
97
99
|
async def connect(self) -> None:
|
|
98
100
|
"""Establish WebSocket connection and authenticate with the service."""
|
|
@@ -109,12 +111,23 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
109
111
|
|
|
110
112
|
# Add query parameters for API key and config ID
|
|
111
113
|
url = f"{self.ws_url}?config_id={self.config_id}"
|
|
114
|
+
# Configure WebSocket with smaller buffers to reduce latency
|
|
112
115
|
self._ws = await websockets.connect(
|
|
113
|
-
url,
|
|
116
|
+
url,
|
|
117
|
+
additional_headers=headers,
|
|
118
|
+
ping_interval=30,
|
|
119
|
+
ping_timeout=10,
|
|
120
|
+
# Reduce write buffer size to minimize send blocking
|
|
121
|
+
write_limit=16384, # 16KB instead of default 64KB
|
|
122
|
+
# Reduce max message size to prevent large messages from blocking
|
|
123
|
+
max_size=1024*1024, # 1MB max message size
|
|
124
|
+
# Reduce queue size to minimize buffering delay
|
|
125
|
+
max_queue=8 # Smaller queue than default 32
|
|
114
126
|
)
|
|
115
127
|
self._running = True
|
|
116
128
|
self._receive_task = asyncio.create_task(self._receive_messages())
|
|
117
129
|
self._split_audio_task = asyncio.create_task(self._split_audio())
|
|
130
|
+
self._send_task = asyncio.create_task(self._send_worker())
|
|
118
131
|
logger.info("Successfully connected to OJIN Persona service")
|
|
119
132
|
return
|
|
120
133
|
except WebSocketException as e:
|
|
@@ -141,6 +154,13 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
141
154
|
self._active_interaction_id = None
|
|
142
155
|
self._cancel_event.set() # Signal cancellation to all tasks
|
|
143
156
|
|
|
157
|
+
# Stop send worker first to prevent new messages
|
|
158
|
+
if self._send_task:
|
|
159
|
+
self._send_task.cancel()
|
|
160
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
161
|
+
await self._send_task
|
|
162
|
+
self._send_task = None
|
|
163
|
+
|
|
144
164
|
if self._ws:
|
|
145
165
|
try:
|
|
146
166
|
await self._ws.close()
|
|
@@ -162,6 +182,56 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
162
182
|
|
|
163
183
|
logger.info("Disconnected from OJIN Persona service")
|
|
164
184
|
|
|
185
|
+
async def _send_worker(self) -> None:
|
|
186
|
+
"""Dedicated worker to send messages without blocking the main thread."""
|
|
187
|
+
while self._running:
|
|
188
|
+
try:
|
|
189
|
+
# Wait for either a message to send or cancellation
|
|
190
|
+
send_task = asyncio.create_task(self._send_queue.get())
|
|
191
|
+
cancel_task = asyncio.create_task(self._cancel_event.wait())
|
|
192
|
+
|
|
193
|
+
done, pending = await asyncio.wait(
|
|
194
|
+
[send_task, cancel_task],
|
|
195
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
196
|
+
timeout=0.1
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Cancel pending tasks
|
|
200
|
+
for task in pending:
|
|
201
|
+
task.cancel()
|
|
202
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
203
|
+
await task
|
|
204
|
+
|
|
205
|
+
if self._cancelled or self._cancel_event.is_set():
|
|
206
|
+
# Clear send queue during cancellation
|
|
207
|
+
while not self._send_queue.empty():
|
|
208
|
+
try:
|
|
209
|
+
self._send_queue.get_nowait()
|
|
210
|
+
except asyncio.QueueEmpty:
|
|
211
|
+
break
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
if done and send_task in done:
|
|
215
|
+
message_data = send_task.result()
|
|
216
|
+
|
|
217
|
+
if self._ws and not self._ws.closed:
|
|
218
|
+
try:
|
|
219
|
+
# Use asyncio.wait_for to prevent indefinite blocking
|
|
220
|
+
await asyncio.wait_for(
|
|
221
|
+
self._ws.send(message_data),
|
|
222
|
+
timeout=5.0 # 5 second timeout for sends
|
|
223
|
+
)
|
|
224
|
+
except asyncio.TimeoutError:
|
|
225
|
+
logger.error("Send timeout - WebSocket send buffer may be full")
|
|
226
|
+
# Don't break the connection, just log and continue
|
|
227
|
+
except Exception as e:
|
|
228
|
+
logger.error(f"Error sending message: {e}")
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error in send worker: {e}")
|
|
233
|
+
break
|
|
234
|
+
|
|
165
235
|
async def _receive_messages(self) -> None:
|
|
166
236
|
"""Continuously receive and process incoming messages."""
|
|
167
237
|
if not self._ws:
|
|
@@ -286,27 +356,35 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
286
356
|
raise ConnectionError("Infernece Server is not ready to receive messsages")
|
|
287
357
|
|
|
288
358
|
if isinstance(message, OjinPersonaCancelInteractionMessage):
|
|
289
|
-
logger.
|
|
359
|
+
logger.info("Interrupt - Processing cancellation immediately")
|
|
290
360
|
|
|
291
361
|
# Set cancellation flag and event immediately
|
|
292
362
|
self._cancelled = True
|
|
293
363
|
self._cancel_event.set()
|
|
294
364
|
|
|
295
|
-
#
|
|
365
|
+
# Clear all queues immediately
|
|
366
|
+
self._clear_all_queues()
|
|
367
|
+
|
|
368
|
+
# Send cancellation message directly, bypassing the send queue
|
|
296
369
|
cancel_input = CancelInteractionMessage(
|
|
297
370
|
payload=message.to_proxy_message()
|
|
298
371
|
)
|
|
299
372
|
|
|
300
|
-
# Send immediately
|
|
373
|
+
# Send immediately with timeout to prevent blocking
|
|
301
374
|
try:
|
|
302
|
-
|
|
303
|
-
|
|
375
|
+
if self._ws and not self._ws.closed:
|
|
376
|
+
await asyncio.wait_for(
|
|
377
|
+
self._ws.send(cancel_input.model_dump_json()),
|
|
378
|
+
timeout=1.0 # Short timeout for immediate sending
|
|
379
|
+
)
|
|
380
|
+
logger.warning(f"Cancellation message sent immediately for {message.interaction_id}")
|
|
381
|
+
except asyncio.TimeoutError:
|
|
382
|
+
logger.warning("Cancellation send timeout - forcing connection close")
|
|
383
|
+
await self.close()
|
|
384
|
+
return
|
|
304
385
|
except Exception as e:
|
|
305
386
|
logger.error(f"Failed to send cancellation message: {e}")
|
|
306
387
|
|
|
307
|
-
# Clear queues quickly without blocking
|
|
308
|
-
self._clear_queues_non_blocking()
|
|
309
|
-
|
|
310
388
|
# Reset cancellation state
|
|
311
389
|
self._cancelled = False
|
|
312
390
|
self._cancel_event.clear()
|
|
@@ -321,7 +399,7 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
321
399
|
interaction_id=interaction_id
|
|
322
400
|
)
|
|
323
401
|
# Clear queues non-blocking
|
|
324
|
-
self.
|
|
402
|
+
self._clear_message_queue()
|
|
325
403
|
self._message_queue.put_nowait(interaction_response)
|
|
326
404
|
return
|
|
327
405
|
|
|
@@ -334,7 +412,11 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
334
412
|
if not message.audio_int16_bytes:
|
|
335
413
|
raise ValueError("Audio cannot be empty")
|
|
336
414
|
|
|
337
|
-
|
|
415
|
+
# Use put_nowait to prevent blocking - drop messages if queue is full
|
|
416
|
+
try:
|
|
417
|
+
self._audio_queue.put_nowait(message)
|
|
418
|
+
except asyncio.QueueFull:
|
|
419
|
+
logger.warning("Audio queue full - dropping audio message")
|
|
338
420
|
return
|
|
339
421
|
|
|
340
422
|
logger.error("The message %s is Unknown", message)
|
|
@@ -350,22 +432,36 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
350
432
|
)
|
|
351
433
|
raise Exception(error)
|
|
352
434
|
|
|
353
|
-
def
|
|
435
|
+
def _clear_all_queues(self) -> None:
|
|
354
436
|
"""Clear all queues without blocking."""
|
|
355
|
-
|
|
437
|
+
self._clear_message_queue()
|
|
438
|
+
self._clear_audio_queue()
|
|
439
|
+
self._clear_send_queue()
|
|
440
|
+
|
|
441
|
+
def _clear_message_queue(self) -> None:
|
|
442
|
+
"""Clear message queue without blocking."""
|
|
356
443
|
while True:
|
|
357
444
|
try:
|
|
358
445
|
self._message_queue.get_nowait()
|
|
359
446
|
except asyncio.QueueEmpty:
|
|
360
447
|
break
|
|
361
|
-
|
|
362
|
-
|
|
448
|
+
|
|
449
|
+
def _clear_audio_queue(self) -> None:
|
|
450
|
+
"""Clear audio queue without blocking."""
|
|
363
451
|
while True:
|
|
364
452
|
try:
|
|
365
453
|
self._audio_queue.get_nowait()
|
|
366
454
|
except asyncio.QueueEmpty:
|
|
367
455
|
break
|
|
368
456
|
|
|
457
|
+
def _clear_send_queue(self) -> None:
|
|
458
|
+
"""Clear send queue without blocking."""
|
|
459
|
+
while True:
|
|
460
|
+
try:
|
|
461
|
+
self._send_queue.get_nowait()
|
|
462
|
+
except asyncio.QueueEmpty:
|
|
463
|
+
break
|
|
464
|
+
|
|
369
465
|
async def _split_audio(self) -> None:
|
|
370
466
|
"""Split audio into chunks and send them, with cancellation support."""
|
|
371
467
|
while self._running:
|
|
@@ -446,10 +542,12 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
446
542
|
proxy_message = InteractionInputMessage(payload=interaction_input)
|
|
447
543
|
|
|
448
544
|
try:
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
545
|
+
# Use put_nowait to prevent blocking on full send queue
|
|
546
|
+
self._send_queue.put_nowait(proxy_message.to_bytes())
|
|
547
|
+
except asyncio.QueueFull:
|
|
548
|
+
logger.warning("Send queue full - dropping audio chunk")
|
|
549
|
+
# Continue with next chunk instead of breaking
|
|
550
|
+
continue
|
|
453
551
|
|
|
454
552
|
async def receive_message(self) -> BaseModel | None:
|
|
455
553
|
"""Receive the next message from the OJIN Persona service.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ojin_client-0.1.7.dev11 → ojin_client-0.1.7.dev12}/ojin_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|