ojin-client 0.1.7.dev9__tar.gz → 0.1.7.dev10__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.dev9 → ojin_client-0.1.7.dev10}/PKG-INFO +1 -1
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/ojin_persona_client.py +92 -44
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/PKG-INFO +1 -1
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/pyproject.toml +1 -1
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/README.md +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/__init__.py +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/cacert.pem +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/entities/interaction_messages.py +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/entities/session_messages.py +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin/ojin_persona_messages.py +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/SOURCES.txt +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/dependency_links.txt +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/requires.txt +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/top_level.txt +0 -0
- {ojin_client-0.1.7.dev9 → ojin_client-0.1.7.dev10}/setup.cfg +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""WebSocket client for OJIN Persona service."""
|
|
1
|
+
"""WebSocket client for OJIN Persona service with optimized cancellation handling."""
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import contextlib
|
|
@@ -90,6 +90,9 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
90
90
|
self._active_interaction_id: str | None = None
|
|
91
91
|
self._split_audio_task: Optional[asyncio.Task] = None
|
|
92
92
|
self._audio_queue: asyncio.Queue[OjinPersonaInteractionInputMessage] = asyncio.Queue()
|
|
93
|
+
|
|
94
|
+
# Add cancellation event for immediate stopping
|
|
95
|
+
self._cancel_event = asyncio.Event()
|
|
93
96
|
|
|
94
97
|
async def connect(self) -> None:
|
|
95
98
|
"""Establish WebSocket connection and authenticate with the service."""
|
|
@@ -136,6 +139,7 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
136
139
|
|
|
137
140
|
self._running = False
|
|
138
141
|
self._active_interaction_id = None
|
|
142
|
+
self._cancel_event.set() # Signal cancellation to all tasks
|
|
139
143
|
|
|
140
144
|
if self._ws:
|
|
141
145
|
try:
|
|
@@ -156,7 +160,6 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
156
160
|
await self._receive_task
|
|
157
161
|
self._receive_task = None
|
|
158
162
|
|
|
159
|
-
|
|
160
163
|
logger.info("Disconnected from OJIN Persona service")
|
|
161
164
|
|
|
162
165
|
async def _receive_messages(self) -> None:
|
|
@@ -283,24 +286,30 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
283
286
|
raise ConnectionError("Infernece Server is not ready to receive messsages")
|
|
284
287
|
|
|
285
288
|
if isinstance(message, OjinPersonaCancelInteractionMessage):
|
|
286
|
-
logger.info("Interrupt")
|
|
289
|
+
logger.info("Interrupt - Processing cancellation immediately")
|
|
287
290
|
|
|
291
|
+
# Set cancellation flag and event immediately
|
|
288
292
|
self._cancelled = True
|
|
293
|
+
self._cancel_event.set()
|
|
294
|
+
|
|
295
|
+
# Send cancellation message with high priority
|
|
289
296
|
cancel_input = CancelInteractionMessage(
|
|
290
297
|
payload=message.to_proxy_message()
|
|
291
298
|
)
|
|
292
299
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
300
|
+
# Send immediately without waiting
|
|
301
|
+
try:
|
|
302
|
+
await self._ws.send(cancel_input.model_dump_json())
|
|
303
|
+
logger.info(f"Cancellation message sent immediately for {message.interaction_id}")
|
|
304
|
+
except Exception as e:
|
|
305
|
+
logger.error(f"Failed to send cancellation message: {e}")
|
|
296
306
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
self._message_queue.get_nowait()
|
|
300
|
-
except asyncio.QueueEmpty:
|
|
301
|
-
break
|
|
307
|
+
# Clear queues quickly without blocking
|
|
308
|
+
self._clear_queues_non_blocking()
|
|
302
309
|
|
|
310
|
+
# Reset cancellation state
|
|
303
311
|
self._cancelled = False
|
|
312
|
+
self._cancel_event.clear()
|
|
304
313
|
|
|
305
314
|
return
|
|
306
315
|
|
|
@@ -311,8 +320,8 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
311
320
|
interaction_response = StartInteractionResponseMessage(
|
|
312
321
|
interaction_id=interaction_id
|
|
313
322
|
)
|
|
314
|
-
|
|
315
|
-
|
|
323
|
+
# Clear queues non-blocking
|
|
324
|
+
self._clear_queues_non_blocking()
|
|
316
325
|
self._message_queue.put_nowait(interaction_response)
|
|
317
326
|
return
|
|
318
327
|
|
|
@@ -326,30 +335,6 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
326
335
|
raise ValueError("Audio cannot be empty")
|
|
327
336
|
|
|
328
337
|
await self._audio_queue.put(message)
|
|
329
|
-
# Split audio bytes into chunks of max 3200 samples
|
|
330
|
-
# max_chunk_size = 3200 * 2
|
|
331
|
-
# audio_chunks = [
|
|
332
|
-
# message.audio_int16_bytes[i : i + max_chunk_size]
|
|
333
|
-
# for i in range(0, len(message.audio_int16_bytes), max_chunk_size)
|
|
334
|
-
# ]
|
|
335
|
-
# logger.info(
|
|
336
|
-
# "Split audio into %d chunks of max %d bytes",
|
|
337
|
-
# len(audio_chunks), max_chunk_size
|
|
338
|
-
# )
|
|
339
|
-
|
|
340
|
-
# for i, chunk in enumerate(audio_chunks):
|
|
341
|
-
# is_last = i == len(audio_chunks) - 1 and message.is_last_input
|
|
342
|
-
#
|
|
343
|
-
# interaction_input = InteractionInput(
|
|
344
|
-
# interaction_id=message.interaction_id,
|
|
345
|
-
# is_final_input=is_last,
|
|
346
|
-
# payload_type="audio",
|
|
347
|
-
# payload=chunk,
|
|
348
|
-
# timestamp=int(time.monotonic() * 1000),
|
|
349
|
-
# params=message.params if i == 0 else None,
|
|
350
|
-
# )
|
|
351
|
-
# proxy_message = InteractionInputMessage(payload=interaction_input)
|
|
352
|
-
# await self._ws.send(proxy_message.to_bytes())
|
|
353
338
|
return
|
|
354
339
|
|
|
355
340
|
logger.error("The message %s is Unknown", message)
|
|
@@ -365,18 +350,73 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
365
350
|
)
|
|
366
351
|
raise Exception(error)
|
|
367
352
|
|
|
368
|
-
|
|
353
|
+
def _clear_queues_non_blocking(self) -> None:
|
|
354
|
+
"""Clear all queues without blocking."""
|
|
355
|
+
# Clear message queue
|
|
369
356
|
while True:
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
357
|
+
try:
|
|
358
|
+
self._message_queue.get_nowait()
|
|
359
|
+
except asyncio.QueueEmpty:
|
|
360
|
+
break
|
|
361
|
+
|
|
362
|
+
# Clear audio queue
|
|
363
|
+
while True:
|
|
364
|
+
try:
|
|
365
|
+
self._audio_queue.get_nowait()
|
|
366
|
+
except asyncio.QueueEmpty:
|
|
367
|
+
break
|
|
373
368
|
|
|
369
|
+
async def _split_audio(self) -> None:
|
|
370
|
+
"""Split audio into chunks and send them, with cancellation support."""
|
|
371
|
+
while self._running:
|
|
372
|
+
message_audio: OjinPersonaInteractionInputMessage | None = None
|
|
373
|
+
|
|
374
374
|
try:
|
|
375
|
-
|
|
375
|
+
# Use wait_for with cancellation event to make this interruptible
|
|
376
|
+
wait_tasks = [
|
|
377
|
+
asyncio.create_task(self._audio_queue.get()),
|
|
378
|
+
asyncio.create_task(self._cancel_event.wait())
|
|
379
|
+
]
|
|
380
|
+
|
|
381
|
+
done, pending = await asyncio.wait(
|
|
382
|
+
wait_tasks,
|
|
383
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
384
|
+
timeout=0.1 # Short timeout to check cancellation frequently
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Cancel pending tasks
|
|
388
|
+
for task in pending:
|
|
389
|
+
task.cancel()
|
|
390
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
391
|
+
await task
|
|
392
|
+
|
|
393
|
+
# Check if cancellation was triggered
|
|
394
|
+
if self._cancelled or self._cancel_event.is_set():
|
|
395
|
+
logger.info("Audio splitting cancelled")
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
# Check if we got a message
|
|
399
|
+
if done:
|
|
400
|
+
completed_task = done.pop()
|
|
401
|
+
if completed_task == wait_tasks[0]: # Audio queue task completed
|
|
402
|
+
message_audio = completed_task.result()
|
|
403
|
+
else: # Cancellation event was set
|
|
404
|
+
continue
|
|
405
|
+
else:
|
|
406
|
+
# Timeout occurred, continue loop
|
|
407
|
+
continue
|
|
408
|
+
|
|
376
409
|
except asyncio.QueueEmpty:
|
|
377
410
|
await asyncio.sleep(0.01)
|
|
378
411
|
continue
|
|
412
|
+
except Exception as e:
|
|
413
|
+
logger.error(f"Error getting audio message: {e}")
|
|
414
|
+
continue
|
|
379
415
|
|
|
416
|
+
if not message_audio:
|
|
417
|
+
continue
|
|
418
|
+
|
|
419
|
+
# Process audio chunks with cancellation checks
|
|
380
420
|
max_chunk_size = 3200 * 2
|
|
381
421
|
audio_chunks = [
|
|
382
422
|
message_audio.audio_int16_bytes[i : i + max_chunk_size]
|
|
@@ -388,6 +428,11 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
388
428
|
)
|
|
389
429
|
|
|
390
430
|
for i, chunk in enumerate(audio_chunks):
|
|
431
|
+
# Check for cancellation before each chunk
|
|
432
|
+
if self._cancelled or self._cancel_event.is_set():
|
|
433
|
+
logger.info("Audio chunk sending cancelled")
|
|
434
|
+
break
|
|
435
|
+
|
|
391
436
|
is_last = i == len(audio_chunks) - 1 and message_audio.is_last_input
|
|
392
437
|
|
|
393
438
|
interaction_input = InteractionInput(
|
|
@@ -400,8 +445,11 @@ class OjinPersonaClient(IOjinPersonaClient):
|
|
|
400
445
|
)
|
|
401
446
|
proxy_message = InteractionInputMessage(payload=interaction_input)
|
|
402
447
|
|
|
403
|
-
|
|
404
|
-
|
|
448
|
+
try:
|
|
449
|
+
await self._ws.send(proxy_message.to_bytes())
|
|
450
|
+
except Exception as e:
|
|
451
|
+
logger.error(f"Failed to send audio chunk: {e}")
|
|
452
|
+
break
|
|
405
453
|
|
|
406
454
|
async def receive_message(self) -> BaseModel | None:
|
|
407
455
|
"""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.dev9 → ojin_client-0.1.7.dev10}/ojin_client.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|