livekit-plugins-volcengine 1.2.0__tar.gz → 1.2.2__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.
Files changed (15) hide show
  1. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/PKG-INFO +2 -2
  2. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/realtime.py +138 -89
  3. livekit_plugins_volcengine-1.2.2/livekit/plugins/volcengine/version.py +1 -0
  4. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/pyproject.toml +1 -1
  5. livekit_plugins_volcengine-1.2.0/livekit/plugins/volcengine/version.py +0 -1
  6. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/.gitignore +0 -0
  7. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/README.md +0 -0
  8. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/__init__.py +0 -0
  9. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/bigmodel_stt.py +0 -0
  10. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/llm.py +0 -0
  11. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/log.py +0 -0
  12. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/py.typed +0 -0
  13. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/stt.py +0 -0
  14. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/tts.py +0 -0
  15. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: livekit-plugins-volcengine
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: LiveKit Agent Plugins for Volcengine
5
5
  Author-email: wangmengdi <790990241@qq.com>
6
6
  Keywords: audio,livekit,realtime,video,webrtc
@@ -14,7 +14,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio
14
14
  Classifier: Topic :: Multimedia :: Video
15
15
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
16
  Requires-Python: >=3.9
17
- Requires-Dist: livekit-agents>=1.2.0
17
+ Requires-Dist: livekit-agents>=1.2.2
18
18
  Requires-Dist: numpy
19
19
  Requires-Dist: openai>=1.75.0
20
20
  Requires-Dist: osc-data>=0.2.2
@@ -15,7 +15,6 @@ from typing import Literal
15
15
 
16
16
  import aiohttp
17
17
  import numpy as np
18
-
19
18
  from livekit import rtc
20
19
  from livekit.agents import llm, utils
21
20
  from livekit.agents.types import (
@@ -25,22 +24,8 @@ from livekit.agents.types import (
25
24
  NotGivenOr,
26
25
  )
27
26
 
28
-
29
27
  from .log import logger
30
28
 
31
- # When a response is created with the OpenAI Realtime API, those events are sent in this order:
32
- # 1. response.created (contains resp_id)
33
- # 2. response.output_item.added (contains item_id)
34
- # 3. conversation.item.created
35
- # 4. response.content_part.added (type audio/text)
36
- # 5. response.audio_transcript.delta (x2, x3, x4, etc)
37
- # 6. response.audio.delta (x2, x3, x4, etc)
38
- # 7. response.content_part.done
39
- # 8. response.output_item.done (contains item_status: "completed/incomplete")
40
- # 9. response.done (contains status_details for cancelled/failed/turn_detected/content_filter)
41
- #
42
- # Ourcode assumes a response will generate only one item with type "message"
43
-
44
29
 
45
30
  PROTOCOL_VERSION = 0b0001
46
31
  DEFAULT_HEADER_SIZE = 0b0001
@@ -204,7 +189,7 @@ class _RealtimeOptions:
204
189
  }
205
190
  return headers
206
191
 
207
- def get_start_session_reqs(self):
192
+ def get_start_session_reqs(self, dialog_id: str | None) -> dict:
208
193
  start_session_req = {
209
194
  "tts": {
210
195
  "audio_config": {
@@ -216,6 +201,7 @@ class _RealtimeOptions:
216
201
  "dialog": {
217
202
  "bot_name": self.bot_name,
218
203
  "system_role": self.system_role,
204
+ "dialog_id": dialog_id or str(utils.shortuuid()),
219
205
  "speaking_style": self.speaking_style,
220
206
  "extra": {"strict_audit": False},
221
207
  },
@@ -249,11 +235,12 @@ class RealtimeModel(llm.RealtimeModel):
249
235
  def __init__(
250
236
  self,
251
237
  bot_name: str = "豆包",
252
- system_role: str = "你是一个语音助手。",
253
- opening: str = "你好啊,今天过得怎么样?",
254
238
  speaking_style: str = "你的说话风格简洁明了,语速适中,语调自然。",
255
- app_id: str = None,
256
- access_token: str = None,
239
+ opening: str | None = None,
240
+ app_id: str | None = None,
241
+ access_token: str | None = None,
242
+ system_role: str | None = None,
243
+ audio_output: bool = True,
257
244
  http_session: aiohttp.ClientSession | None = None,
258
245
  max_session_duration: NotGivenOr[float | None] = NOT_GIVEN,
259
246
  conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
@@ -264,7 +251,7 @@ class RealtimeModel(llm.RealtimeModel):
264
251
  turn_detection=True,
265
252
  user_transcription=True,
266
253
  auto_tool_reply_generation=False,
267
- audio_output=True,
254
+ audio_output=audio_output,
268
255
  )
269
256
  )
270
257
 
@@ -349,8 +336,10 @@ class RealtimeSession(
349
336
  self._current_generation: _ResponseGeneration | None = None
350
337
  self._current_item: _MessageGeneration | None = None
351
338
  self._remote_chat_ctx = llm.remote_chat_context.RemoteChatContext()
352
- self._is_opening = True
339
+ self._is_opening = False
353
340
  self._first_tts_response = True
341
+ self._first_llm_response = True
342
+ self._first_llm_sentence = True
354
343
 
355
344
  self._update_chat_ctx_lock = asyncio.Lock()
356
345
  self._update_fnc_ctx_lock = asyncio.Lock()
@@ -398,6 +387,7 @@ class RealtimeSession(
398
387
 
399
388
  async def _run_ws(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
400
389
  closing = False
390
+ logger.info("start connection")
401
391
  start_connection_request = bytearray(generate_header())
402
392
  start_connection_request.extend(int(1).to_bytes(4, "big"))
403
393
  payload_bytes = str.encode("{}")
@@ -406,64 +396,55 @@ class RealtimeSession(
406
396
  start_connection_request.extend(payload_bytes)
407
397
  await ws_conn.send_bytes(start_connection_request)
408
398
  _ = await ws_conn.receive_bytes()
409
- logger.info("startConnection response")
410
399
 
411
- request_params = self._realtime_model._opts.get_start_session_reqs()
412
- payload_bytes = str.encode(json.dumps(request_params))
413
- payload_bytes = gzip.compress(payload_bytes)
414
- start_session_request = bytearray(generate_header())
415
- start_session_request.extend(int(100).to_bytes(4, "big"))
416
- start_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
417
- start_session_request.extend(str.encode(self.session_id))
418
- start_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
419
- start_session_request.extend(payload_bytes)
420
- await ws_conn.send_bytes(start_session_request)
421
- _ = await ws_conn.receive_bytes()
422
- logger.info("startSession response")
423
-
424
- payload = {
425
- "content": self._realtime_model._opts.opening,
426
- }
427
- hello_request = bytearray(generate_header())
428
- hello_request.extend(int(300).to_bytes(4, "big"))
429
- payload_bytes = str.encode(json.dumps(payload))
430
- payload_bytes = gzip.compress(payload_bytes)
431
- hello_request.extend((len(self.session_id)).to_bytes(4, "big"))
432
- hello_request.extend(str.encode(self.session_id))
433
- hello_request.extend((len(payload_bytes)).to_bytes(4, "big"))
434
- hello_request.extend(payload_bytes)
435
- await ws_conn.send_bytes(hello_request)
436
- self._is_opening = True
437
- logger.info("send hello request")
438
-
439
- self._current_generation = _ResponseGeneration(
440
- message_ch=utils.aio.Chan(),
441
- function_ch=utils.aio.Chan(),
442
- messages={},
443
- _created_timestamp=time.time(),
444
- _done_fut=asyncio.Future(),
445
- )
446
-
447
- generation_ev = llm.GenerationCreatedEvent(
448
- message_stream=self._current_generation.message_ch,
449
- function_stream=self._current_generation.function_ch,
450
- user_initiated=False,
451
- )
452
- self.emit("generation_created", generation_ev)
453
- item_id = utils.shortuuid()
454
- self._current_item = _MessageGeneration(
455
- message_id=item_id,
456
- text_ch=utils.aio.Chan(),
457
- audio_ch=utils.aio.Chan(),
458
- )
400
+ logger.info("start session")
401
+ await self._start_session(ws_conn=ws_conn, dialog_id=self.session_id)
402
+
403
+ if self._realtime_model._opts.opening is not None:
404
+ self._is_opening = True
405
+ payload = {
406
+ "content": self._realtime_model._opts.opening,
407
+ }
408
+ hello_request = bytearray(generate_header())
409
+ hello_request.extend(int(300).to_bytes(4, "big"))
410
+ payload_bytes = str.encode(json.dumps(payload))
411
+ payload_bytes = gzip.compress(payload_bytes)
412
+ hello_request.extend((len(self.session_id)).to_bytes(4, "big"))
413
+ hello_request.extend(str.encode(self.session_id))
414
+ hello_request.extend((len(payload_bytes)).to_bytes(4, "big"))
415
+ hello_request.extend(payload_bytes)
416
+ await ws_conn.send_bytes(hello_request)
417
+ self._is_opening = True
418
+ logger.info("send hello request")
419
+
420
+ self._current_generation = _ResponseGeneration(
421
+ message_ch=utils.aio.Chan(),
422
+ function_ch=utils.aio.Chan(),
423
+ messages={},
424
+ _created_timestamp=time.time(),
425
+ _done_fut=asyncio.Future(),
426
+ )
459
427
 
460
- self._current_generation.message_ch.send_nowait(
461
- llm.MessageGeneration(
428
+ generation_ev = llm.GenerationCreatedEvent(
429
+ message_stream=self._current_generation.message_ch,
430
+ function_stream=self._current_generation.function_ch,
431
+ user_initiated=False,
432
+ )
433
+ self.emit("generation_created", generation_ev)
434
+ item_id = utils.shortuuid()
435
+ self._current_item = _MessageGeneration(
462
436
  message_id=item_id,
463
- text_stream=self._current_item.text_ch,
464
- audio_stream=self._current_item.audio_ch,
437
+ text_ch=utils.aio.Chan(),
438
+ audio_ch=utils.aio.Chan(),
439
+ )
440
+
441
+ self._current_generation.message_ch.send_nowait(
442
+ llm.MessageGeneration(
443
+ message_id=item_id,
444
+ text_stream=self._current_item.text_ch,
445
+ audio_stream=self._current_item.audio_ch,
446
+ )
465
447
  )
466
- )
467
448
 
468
449
  @utils.log_exceptions(logger=logger)
469
450
  async def _send_task() -> None:
@@ -504,7 +485,7 @@ class RealtimeSession(
504
485
  event = response.get("event")
505
486
  if event == 450: # ASRInfo
506
487
  self.emit("input_speech_started", llm.InputSpeechStartedEvent())
507
- logger.info("speech start")
488
+ logger.info("transcription start")
508
489
  elif event == 451: # ASRResponse
509
490
  response = response["payload_msg"]
510
491
  transcription = response["results"][0]["alternatives"][0][
@@ -552,16 +533,19 @@ class RealtimeSession(
552
533
  )
553
534
 
554
535
  elif event == 459: # ASREnd
555
- logger.info("speech end")
536
+ logger.info("transcription end")
556
537
  self.emit(
557
538
  "input_speech_stopped",
558
539
  llm.InputSpeechStoppedEvent(
559
540
  user_transcription_enabled=False
560
541
  ),
561
542
  )
543
+ logger.info("llm start")
544
+ logger.info("tts start")
562
545
 
563
546
  elif event == 352: # TTSResponse
564
547
  if self._first_tts_response:
548
+ logger.info("llm first sentence")
565
549
  logger.info("tts first response")
566
550
  self._first_tts_response = False
567
551
  audio_bytes = response[
@@ -580,7 +564,12 @@ class RealtimeSession(
580
564
  samples_per_channel=len(audio_bytes) // 2,
581
565
  )
582
566
  )
567
+ elif event == 350: # TTSSentenceStart
568
+ pass
569
+ elif event == 351: # TTSSentenceEnd
570
+ pass
583
571
  elif event == 359: # TTSEnded
572
+ logger.info("tts end")
584
573
  self._current_item.audio_ch.close()
585
574
  if self._is_opening:
586
575
  self._current_item.text_ch.send_nowait(
@@ -593,10 +582,15 @@ class RealtimeSession(
593
582
  self._current_generation = None
594
583
  self._first_tts_response = True
595
584
  elif event == 550: # 模型回复的文本内容
585
+ if self._first_llm_response:
586
+ logger.info("llm first response")
587
+ self._first_llm_response = False
596
588
  text = response["payload_msg"]["content"]
597
589
  self._current_item.text_ch.send_nowait(text)
598
590
  elif event == 559: # 模型回复文本结束事件
591
+ logger.info("llm end")
599
592
  self._current_item.text_ch.close()
593
+ self._first_llm_response = True
600
594
  else:
601
595
  pass
602
596
  except Exception:
@@ -607,20 +601,10 @@ class RealtimeSession(
607
601
  asyncio.create_task(_recv_task(), name="_recv_task"),
608
602
  asyncio.create_task(_send_task(), name="_send_task"),
609
603
  ]
610
- wait_reconnect_task: asyncio.Task | None = None
611
- # if self._realtime_model._opts.max_session_duration is not None:
612
- # wait_reconnect_task = asyncio.create_task(
613
- # asyncio.sleep(self._realtime_model._opts.max_session_duration),
614
- # name="_timeout_task",
615
- # )
616
- # tasks.append(wait_reconnect_task)
617
604
  try:
618
605
  done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
619
-
620
- # propagate exceptions from completed tasks
621
606
  for task in done:
622
- if task != wait_reconnect_task:
623
- task.result()
607
+ task.result()
624
608
 
625
609
  finally:
626
610
  await utils.aio.cancel_and_wait(*tasks)
@@ -629,6 +613,71 @@ class RealtimeSession(
629
613
  def _create_session_update_event(self):
630
614
  pass
631
615
 
616
+ async def chat_tts_text(
617
+ self,
618
+ start: bool,
619
+ end: bool,
620
+ content: str,
621
+ ws_conn: aiohttp.ClientWebSocketResponse,
622
+ ) -> None:
623
+ """发送Chat TTS Text消息"""
624
+ payload = {
625
+ "start": start,
626
+ "end": end,
627
+ "content": content,
628
+ }
629
+ logger.info("ChatTTSTextRequest")
630
+ payload_bytes = str.encode(json.dumps(payload))
631
+ payload_bytes = gzip.compress(payload_bytes)
632
+
633
+ chat_tts_text_request = bytearray(generate_header())
634
+ chat_tts_text_request.extend(int(500).to_bytes(4, "big"))
635
+ chat_tts_text_request.extend((len(self.session_id)).to_bytes(4, "big"))
636
+ chat_tts_text_request.extend(str.encode(self.session_id))
637
+ chat_tts_text_request.extend((len(payload_bytes)).to_bytes(4, "big"))
638
+ chat_tts_text_request.extend(payload_bytes)
639
+ await ws_conn.send_bytes(chat_tts_text_request)
640
+
641
+ async def _start_session(
642
+ self, ws_conn: aiohttp.ClientWebSocketResponse, dialog_id: str
643
+ ) -> None:
644
+ request_params = self._realtime_model._opts.get_start_session_reqs(
645
+ dialog_id=dialog_id
646
+ )
647
+ payload_bytes = str.encode(json.dumps(request_params))
648
+ payload_bytes = gzip.compress(payload_bytes)
649
+ start_session_request = bytearray(generate_header())
650
+ start_session_request.extend(int(100).to_bytes(4, "big"))
651
+ start_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
652
+ start_session_request.extend(str.encode(self.session_id))
653
+ start_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
654
+ start_session_request.extend(payload_bytes)
655
+ await ws_conn.send_bytes(start_session_request)
656
+ _ = await ws_conn.receive_bytes()
657
+
658
+ async def _finish_session(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
659
+ finish_session_request = bytearray(generate_header())
660
+ finish_session_request.extend(int(102).to_bytes(4, "big"))
661
+ payload_bytes = str.encode("{}")
662
+ payload_bytes = gzip.compress(payload_bytes)
663
+ finish_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
664
+ finish_session_request.extend(str.encode(self.session_id))
665
+ finish_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
666
+ finish_session_request.extend(payload_bytes)
667
+ await ws_conn.send_bytes(finish_session_request)
668
+
669
+ async def _finish_connection(
670
+ self, ws_conn: aiohttp.ClientWebSocketResponse
671
+ ) -> None:
672
+ finish_connection_request = bytearray(generate_header())
673
+ finish_connection_request.extend(int(2).to_bytes(4, "big"))
674
+ payload_bytes = str.encode("{}")
675
+ payload_bytes = gzip.compress(payload_bytes)
676
+ finish_connection_request.extend((len(payload_bytes)).to_bytes(4, "big"))
677
+ finish_connection_request.extend(payload_bytes)
678
+ await ws_conn.send_bytes(finish_connection_request)
679
+ _ = await ws_conn.receive_bytes()
680
+
632
681
  @property
633
682
  def chat_ctx(self) -> llm.ChatContext:
634
683
  return self._remote_chat_ctx.to_chat_ctx()
@@ -0,0 +1 @@
1
+ __version__ = "1.2.2"
@@ -9,7 +9,7 @@ authors = [
9
9
  keywords = ["webrtc", "realtime", "audio", "video", "livekit"]
10
10
  requires-python = ">=3.9"
11
11
  dependencies = [
12
- "livekit-agents>=1.2.0",
12
+ "livekit-agents>=1.2.2",
13
13
  "openai>=1.75.0",
14
14
  "osc-data>=0.2.2",
15
15
  "pydantic",
@@ -1 +0,0 @@
1
- __version__ = "1.2.0"