ojin-client 0.1.7.dev4__tar.gz → 0.1.7.dev6__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.dev4
3
+ Version: 0.1.7.dev6
4
4
  Summary: Ojin platform services
5
5
  Author: Journee
6
6
  License: Apache-2.0
@@ -87,7 +87,9 @@ class OjinPersonaClient(IOjinPersonaClient):
87
87
  self._receive_task: Optional[asyncio.Task] = None
88
88
  self._inference_server_ready: bool = False
89
89
  self._cancelled: bool = False
90
- self._cancelled_interaction_id: str | None = None
90
+ self.active_interaction_id: str | None = None
91
+ self._split_audio_task: Optional[asyncio.Task] = None
92
+ self._audio_queue: asyncio.Queue[OjinPersonaInteractionInputMessage] = asyncio.Queue()
91
93
 
92
94
  async def connect(self) -> None:
93
95
  """Establish WebSocket connection and authenticate with the service."""
@@ -109,6 +111,7 @@ class OjinPersonaClient(IOjinPersonaClient):
109
111
  )
110
112
  self._running = True
111
113
  self._receive_task = asyncio.create_task(self._receive_messages())
114
+ self._split_audio_task = asyncio.create_task(self._split_audio())
112
115
  logger.info("Successfully connected to OJIN Persona service")
113
116
  return
114
117
  except WebSocketException as e:
@@ -132,12 +135,7 @@ class OjinPersonaClient(IOjinPersonaClient):
132
135
  pass
133
136
 
134
137
  self._running = False
135
-
136
- if self._receive_task:
137
- self._receive_task.cancel()
138
- with contextlib.suppress(asyncio.CancelledError):
139
- await self._receive_task
140
- self._receive_task = None
138
+ self.active_interaction_id = None
141
139
 
142
140
  if self._ws:
143
141
  try:
@@ -145,6 +143,19 @@ class OjinPersonaClient(IOjinPersonaClient):
145
143
  except Exception as e:
146
144
  logger.error("Error closing WebSocket connection: %s", e)
147
145
  self._ws = None
146
+
147
+ if self._split_audio_task:
148
+ self._split_audio_task.cancel()
149
+ with contextlib.suppress(asyncio.CancelledError):
150
+ await self._split_audio_task
151
+ self._split_audio_task = None
152
+
153
+ if self._receive_task:
154
+ self._receive_task.cancel()
155
+ with contextlib.suppress(asyncio.CancelledError):
156
+ await self._receive_task
157
+ self._receive_task = None
158
+
148
159
 
149
160
  logger.info("Disconnected from OJIN Persona service")
150
161
 
@@ -198,10 +209,19 @@ class OjinPersonaClient(IOjinPersonaClient):
198
209
  logger.error(e)
199
210
  raise
200
211
 
201
- if not isinstance(message, str):
202
- raise Exception("not a know Format")
203
-
212
+ # NOTE: str type
213
+ # TODO: clean when the proxy add structured logs for this error
204
214
  if message == "No backend servers available. Please try again later.":
215
+ await self._message_queue.put(
216
+ ErrorResponseMessage(
217
+ payload=ErrorResponse(
218
+ interaction_id=None,
219
+ code="NO_BACKEND_SERVER_AVAILABLE",
220
+ message=message,
221
+ timestamp=int(time.monotonic() * 1000),
222
+ )
223
+ )
224
+ )
205
225
  raise Exception(message)
206
226
 
207
227
  data = json.loads(message)
@@ -232,10 +252,11 @@ class OjinPersonaClient(IOjinPersonaClient):
232
252
  if isinstance(msg, OjinPersonaSessionReadyMessage):
233
253
  self._inference_server_ready = True
234
254
 
255
+ await self._message_queue.put(msg)
256
+
235
257
  if isinstance(msg, ErrorResponseMessage):
236
258
  raise RuntimeError(f"Error in Inference Server received: {msg}")
237
259
 
238
- await self._message_queue.put(msg)
239
260
  logger.info("Received message: %s", msg)
240
261
  else:
241
262
  logger.warning("Unknown message type: %s", msg_type)
@@ -263,6 +284,7 @@ class OjinPersonaClient(IOjinPersonaClient):
263
284
  if isinstance(message, OjinPersonaCancelInteractionMessage):
264
285
  logger.info("Interrupt")
265
286
 
287
+ self._cancelled = True
266
288
  cancel_input = CancelInteractionMessage(
267
289
  payload=message.to_proxy_message()
268
290
  )
@@ -277,17 +299,16 @@ class OjinPersonaClient(IOjinPersonaClient):
277
299
  except asyncio.QueueEmpty:
278
300
  break
279
301
 
280
-
281
302
  self._cancelled = False
282
- self._cancelled_interaction_id = message.interaction_id
283
303
 
284
304
  return
285
305
 
286
306
  if isinstance(message, StartInteractionMessage):
287
- interaction_id = uuid.uuid4()
307
+ interaction_id = str(uuid.uuid4())
308
+ self.active_interaction_id = interaction_id
288
309
  logger.info("Generate UUID %s", interaction_id)
289
310
  interaction_response = StartInteractionResponseMessage(
290
- interaction_id=str(interaction_id)
311
+ interaction_id=interaction_id
291
312
  )
292
313
  while not self._message_queue.empty():
293
314
  await self._message_queue.get()
@@ -297,14 +318,71 @@ class OjinPersonaClient(IOjinPersonaClient):
297
318
  if isinstance(message, OjinPersonaInteractionInputMessage):
298
319
  logger.info("InteractionMessage")
299
320
  logger.info(f"Message sent {message.interaction_id}")
321
+ if message.interaction_id != self.active_interaction_id:
322
+ return
323
+
300
324
  if not message.audio_int16_bytes:
301
325
  raise ValueError("Audio cannot be empty")
302
326
 
327
+
328
+ await self._audio_queue.put(message)
303
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
+ return
354
+
355
+ logger.error("The message %s is Unknown", message)
356
+ # TODO: should we close the connection here?
357
+ await self.close()
358
+ error = ErrorResponseMessage(
359
+ payload=ErrorResponse(
360
+ interaction_id=message.interaction_id,
361
+ code="UNKNOWN",
362
+ message="The message is Unknown",
363
+ timestamp=int(time.monotonic() * 1000),
364
+ )
365
+ )
366
+ raise Exception(error)
367
+
368
+ async def _split_audio(self) -> None:
369
+ while True:
370
+ message_audio: OjinPersonaInteractionInputMessage| None = None
371
+ if self._cancelled:
372
+ pass
373
+
374
+ try:
375
+ message_audio = self._audio_queue.get_nowait()
376
+ except asyncio.QueueEmpty:
377
+ pass
378
+
379
+ if message_audio is None:
380
+ pass
381
+
304
382
  max_chunk_size = 3200 * 2
305
383
  audio_chunks = [
306
- message.audio_int16_bytes[i : i + max_chunk_size]
307
- for i in range(0, len(message.audio_int16_bytes), max_chunk_size)
384
+ message_audio.audio_int16_bytes[i : i + max_chunk_size]
385
+ for i in range(0, len(message_audio.audio_int16_bytes), max_chunk_size)
308
386
  ]
309
387
  logger.info(
310
388
  "Split audio into %d chunks of max %d bytes",
@@ -312,30 +390,20 @@ class OjinPersonaClient(IOjinPersonaClient):
312
390
  )
313
391
 
314
392
  for i, chunk in enumerate(audio_chunks):
315
- is_last = i == len(audio_chunks) - 1 and message.is_last_input
393
+ is_last = i == len(audio_chunks) - 1 and message_audio.is_last_input
316
394
 
317
395
  interaction_input = InteractionInput(
318
- interaction_id=message.interaction_id,
396
+ interaction_id=message_audio.interaction_id,
319
397
  is_final_input=is_last,
320
398
  payload_type="audio",
321
399
  payload=chunk,
322
400
  timestamp=int(time.monotonic() * 1000),
323
- params=message.params if i == 0 else None,
401
+ params=message_audio.params if i == 0 else None,
324
402
  )
325
403
  proxy_message = InteractionInputMessage(payload=interaction_input)
404
+
326
405
  await self._ws.send(proxy_message.to_bytes())
327
- return
328
406
 
329
- logger.error("The message %s is Unknown", message)
330
- await self.close()
331
- error = ErrorResponseMessage(
332
- payload=ErrorResponse(
333
- code="UNKNOWN",
334
- message="The message is Unknown",
335
- timestamp=int(time.monotonic() * 1000),
336
- )
337
- )
338
- raise Exception(error)
339
407
 
340
408
  async def receive_message(self) -> BaseModel | None:
341
409
  """Receive the next message from the OJIN Persona service.
@@ -168,26 +168,6 @@ class OjinPersonaInteractionInputMessage(OjinPersonaMessage):
168
168
  )
169
169
 
170
170
 
171
- class ErrorResponsePayload(BaseModel):
172
- """Response message informing the client there was an error.
173
-
174
- contains details about the error
175
- """
176
-
177
- error: str
178
- code: Optional[str] = None
179
- timestamp: Optional[int] = None
180
-
181
-
182
- class ErrorResponseMessage(BaseModel):
183
- """Response message informing the client there was an error.
184
-
185
- contains details about the error
186
- """
187
-
188
- type: str
189
- payload: ErrorResponsePayload
190
-
191
171
 
192
172
  class IOjinPersonaClient(ABC):
193
173
  """Interface for Ojin Persona client communication.
@@ -210,7 +190,7 @@ class IOjinPersonaClient(ABC):
210
190
  """
211
191
 
212
192
  @abstractmethod
213
- async def receive_message(self) -> BaseModel:
193
+ async def receive_message(self) -> BaseModel|None:
214
194
  """Receive a message from the server.
215
195
 
216
196
  Returns:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ojin-client
3
- Version: 0.1.7.dev4
3
+ Version: 0.1.7.dev6
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.7dev4"
3
+ version = "0.1.7dev6"
4
4
  description = "Ojin platform services"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"