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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ojin-client
3
- Version: 0.1.7.dev9
3
+ Version: 0.1.7.dev10
4
4
  Summary: Ojin platform services
5
5
  Author: Journee
6
6
  License: Apache-2.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
- await self._ws.send(cancel_input.model_dump_json())
294
-
295
- logger.info(f"Message sent {message.interaction_id}")
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
- while not self._message_queue.empty():
298
- try:
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
- while not self._message_queue.empty():
315
- await self._message_queue.get()
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
- async def _split_audio(self) -> None:
353
+ def _clear_queues_non_blocking(self) -> None:
354
+ """Clear all queues without blocking."""
355
+ # Clear message queue
369
356
  while True:
370
- message_audio: OjinPersonaInteractionInputMessage| None = None
371
- if self._cancelled:
372
- continue
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
- message_audio = self._audio_queue.get_nowait()
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
- await self._ws.send(proxy_message.to_bytes())
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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ojin-client
3
- Version: 0.1.7.dev9
3
+ Version: 0.1.7.dev10
4
4
  Summary: Ojin platform services
5
5
  Author: Journee
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ojin-client"
3
- version = "0.1.7dev9"
3
+ version = "0.1.7dev10"
4
4
  description = "Ojin platform services"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"