ojin-client 0.1.7.dev4__tar.gz → 0.1.7.dev5__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.dev5
4
4
  Summary: Ojin platform services
5
5
  Author: Journee
6
6
  License: Apache-2.0
@@ -88,6 +88,9 @@ class OjinPersonaClient(IOjinPersonaClient):
88
88
  self._inference_server_ready: bool = False
89
89
  self._cancelled: bool = False
90
90
  self._cancelled_interaction_id: str | None = None
91
+ self.active_interaction_id: str | None = None
92
+ self._split_audio_task: Optional[asyncio.Task] = None
93
+ self._audio_queue: asyncio.Queue[bytes] = asyncio.Queue()
91
94
 
92
95
  async def connect(self) -> None:
93
96
  """Establish WebSocket connection and authenticate with the service."""
@@ -109,6 +112,7 @@ class OjinPersonaClient(IOjinPersonaClient):
109
112
  )
110
113
  self._running = True
111
114
  self._receive_task = asyncio.create_task(self._receive_messages())
115
+ self._split_audio_task = asyncio.create_task(self._split_audio())
112
116
  logger.info("Successfully connected to OJIN Persona service")
113
117
  return
114
118
  except WebSocketException as e:
@@ -132,12 +136,7 @@ class OjinPersonaClient(IOjinPersonaClient):
132
136
  pass
133
137
 
134
138
  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
139
+ self.active_interaction_id = None
141
140
 
142
141
  if self._ws:
143
142
  try:
@@ -145,6 +144,19 @@ class OjinPersonaClient(IOjinPersonaClient):
145
144
  except Exception as e:
146
145
  logger.error("Error closing WebSocket connection: %s", e)
147
146
  self._ws = None
147
+
148
+ if self._split_audio_task:
149
+ self._split_audio_task.cancel()
150
+ with contextlib.suppress(asyncio.CancelledError):
151
+ await self._split_audio_task
152
+ self._split_audio_task = None
153
+
154
+ if self._receive_task:
155
+ self._receive_task.cancel()
156
+ with contextlib.suppress(asyncio.CancelledError):
157
+ await self._receive_task
158
+ self._receive_task = None
159
+
148
160
 
149
161
  logger.info("Disconnected from OJIN Persona service")
150
162
 
@@ -198,10 +210,19 @@ class OjinPersonaClient(IOjinPersonaClient):
198
210
  logger.error(e)
199
211
  raise
200
212
 
201
- if not isinstance(message, str):
202
- raise Exception("not a know Format")
203
-
213
+ # NOTE: str type
214
+ # TODO: clean when the proxy add structured logs for this error
204
215
  if message == "No backend servers available. Please try again later.":
216
+ await self._message_queue.put(
217
+ ErrorResponseMessage(
218
+ payload=ErrorResponse(
219
+ interaction_id=None,
220
+ code="NO_BACKEND_SERVER_AVAILABLE",
221
+ message=message,
222
+ timestamp=int(time.monotonic() * 1000),
223
+ )
224
+ )
225
+ )
205
226
  raise Exception(message)
206
227
 
207
228
  data = json.loads(message)
@@ -232,10 +253,11 @@ class OjinPersonaClient(IOjinPersonaClient):
232
253
  if isinstance(msg, OjinPersonaSessionReadyMessage):
233
254
  self._inference_server_ready = True
234
255
 
256
+ await self._message_queue.put(msg)
257
+
235
258
  if isinstance(msg, ErrorResponseMessage):
236
259
  raise RuntimeError(f"Error in Inference Server received: {msg}")
237
260
 
238
- await self._message_queue.put(msg)
239
261
  logger.info("Received message: %s", msg)
240
262
  else:
241
263
  logger.warning("Unknown message type: %s", msg_type)
@@ -263,6 +285,7 @@ class OjinPersonaClient(IOjinPersonaClient):
263
285
  if isinstance(message, OjinPersonaCancelInteractionMessage):
264
286
  logger.info("Interrupt")
265
287
 
288
+ self._cancelled = True
266
289
  cancel_input = CancelInteractionMessage(
267
290
  payload=message.to_proxy_message()
268
291
  )
@@ -284,10 +307,11 @@ class OjinPersonaClient(IOjinPersonaClient):
284
307
  return
285
308
 
286
309
  if isinstance(message, StartInteractionMessage):
287
- interaction_id = uuid.uuid4()
310
+ interaction_id = str(uuid.uuid4())
311
+ self.active_interaction_id = interaction_id
288
312
  logger.info("Generate UUID %s", interaction_id)
289
313
  interaction_response = StartInteractionResponseMessage(
290
- interaction_id=str(interaction_id)
314
+ interaction_id=interaction_id
291
315
  )
292
316
  while not self._message_queue.empty():
293
317
  await self._message_queue.get()
@@ -297,14 +321,71 @@ class OjinPersonaClient(IOjinPersonaClient):
297
321
  if isinstance(message, OjinPersonaInteractionInputMessage):
298
322
  logger.info("InteractionMessage")
299
323
  logger.info(f"Message sent {message.interaction_id}")
324
+ if message.interaction_id != self.active_interaction_id:
325
+ return
326
+
300
327
  if not message.audio_int16_bytes:
301
328
  raise ValueError("Audio cannot be empty")
302
329
 
330
+
331
+ await self._audio_queue.put(message.audio_int16_bytes)
303
332
  # Split audio bytes into chunks of max 3200 samples
333
+ # max_chunk_size = 3200 * 2
334
+ # audio_chunks = [
335
+ # message.audio_int16_bytes[i : i + max_chunk_size]
336
+ # for i in range(0, len(message.audio_int16_bytes), max_chunk_size)
337
+ # ]
338
+ # logger.info(
339
+ # "Split audio into %d chunks of max %d bytes",
340
+ # len(audio_chunks), max_chunk_size
341
+ # )
342
+
343
+ # for i, chunk in enumerate(audio_chunks):
344
+ # is_last = i == len(audio_chunks) - 1 and message.is_last_input
345
+ #
346
+ # interaction_input = InteractionInput(
347
+ # interaction_id=message.interaction_id,
348
+ # is_final_input=is_last,
349
+ # payload_type="audio",
350
+ # payload=chunk,
351
+ # timestamp=int(time.monotonic() * 1000),
352
+ # params=message.params if i == 0 else None,
353
+ # )
354
+ # proxy_message = InteractionInputMessage(payload=interaction_input)
355
+ # await self._ws.send(proxy_message.to_bytes())
356
+ return
357
+
358
+ logger.error("The message %s is Unknown", message)
359
+ # TODO: should we close the connection here?
360
+ await self.close()
361
+ error = ErrorResponseMessage(
362
+ payload=ErrorResponse(
363
+ interaction_id=message.interaction_id,
364
+ code="UNKNOWN",
365
+ message="The message is Unknown",
366
+ timestamp=int(time.monotonic() * 1000),
367
+ )
368
+ )
369
+ raise Exception(error)
370
+
371
+ async def _split_audio(self) -> None:
372
+ while True:
373
+ message_audio: bytes | None = None
374
+ if self._cancelled:
375
+ pass
376
+
377
+ try:
378
+ message_audio = self._audio_queue.get_nowait()
379
+ except asyncio.QueueEmpty:
380
+ pass
381
+
382
+ if message_audio is None:
383
+ pass
384
+
304
385
  max_chunk_size = 3200 * 2
305
386
  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)
387
+ message_audio[i : i + max_chunk_size]
388
+ for i in range(0, len(message_audio), max_chunk_size)
308
389
  ]
309
390
  logger.info(
310
391
  "Split audio into %d chunks of max %d bytes",
@@ -323,19 +404,9 @@ class OjinPersonaClient(IOjinPersonaClient):
323
404
  params=message.params if i == 0 else None,
324
405
  )
325
406
  proxy_message = InteractionInputMessage(payload=interaction_input)
407
+
326
408
  await self._ws.send(proxy_message.to_bytes())
327
- return
328
409
 
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
410
 
340
411
  async def receive_message(self) -> BaseModel | None:
341
412
  """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.dev5
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.7dev5"
4
4
  description = "Ojin platform services"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"