livekit-plugins-volcengine 1.2.0__tar.gz → 1.2.3__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.3}/PKG-INFO +2 -2
  2. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/realtime.py +142 -89
  3. livekit_plugins_volcengine-1.2.3/livekit/plugins/volcengine/version.py +1 -0
  4. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/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.3}/.gitignore +0 -0
  7. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/README.md +0 -0
  8. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/__init__.py +0 -0
  9. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/bigmodel_stt.py +0 -0
  10. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/llm.py +0 -0
  11. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/log.py +0 -0
  12. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/py.typed +0 -0
  13. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/stt.py +0 -0
  14. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/livekit/plugins/volcengine/tts.py +0 -0
  15. {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.3}/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.3
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.3
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
@@ -186,6 +171,7 @@ class _RealtimeOptions:
186
171
  conn_options: APIConnectOptions
187
172
  opening: str = "你好啊,今天过得怎么样?"
188
173
  speaking_style: str = "你的说话风格简洁明了,语速适中,语调自然。"
174
+ speaker: str = "zh_female_vv_jupiter_bigtts"
189
175
  sample_rate: int = 24000
190
176
  num_channels: int = 1
191
177
  format: str = "pcm"
@@ -204,7 +190,7 @@ class _RealtimeOptions:
204
190
  }
205
191
  return headers
206
192
 
207
- def get_start_session_reqs(self):
193
+ def get_start_session_reqs(self, dialog_id: str | None) -> dict:
208
194
  start_session_req = {
209
195
  "tts": {
210
196
  "audio_config": {
@@ -212,10 +198,12 @@ class _RealtimeOptions:
212
198
  "format": self.format,
213
199
  "sample_rate": self.sample_rate,
214
200
  },
201
+ "speaker": self.speaker,
215
202
  },
216
203
  "dialog": {
217
204
  "bot_name": self.bot_name,
218
205
  "system_role": self.system_role,
206
+ "dialog_id": dialog_id or str(utils.shortuuid()),
219
207
  "speaking_style": self.speaking_style,
220
208
  "extra": {"strict_audit": False},
221
209
  },
@@ -249,11 +237,13 @@ class RealtimeModel(llm.RealtimeModel):
249
237
  def __init__(
250
238
  self,
251
239
  bot_name: str = "豆包",
252
- system_role: str = "你是一个语音助手。",
253
- opening: str = "你好啊,今天过得怎么样?",
254
240
  speaking_style: str = "你的说话风格简洁明了,语速适中,语调自然。",
255
- app_id: str = None,
256
- access_token: str = None,
241
+ speaker: str = "zh_female_vv_jupiter_bigtts",
242
+ opening: str | None = None,
243
+ app_id: str | None = None,
244
+ access_token: str | None = None,
245
+ system_role: str | None = None,
246
+ audio_output: bool = True,
257
247
  http_session: aiohttp.ClientSession | None = None,
258
248
  max_session_duration: NotGivenOr[float | None] = NOT_GIVEN,
259
249
  conn_options: APIConnectOptions = DEFAULT_API_CONNECT_OPTIONS,
@@ -264,7 +254,7 @@ class RealtimeModel(llm.RealtimeModel):
264
254
  turn_detection=True,
265
255
  user_transcription=True,
266
256
  auto_tool_reply_generation=False,
267
- audio_output=True,
257
+ audio_output=audio_output,
268
258
  )
269
259
  )
270
260
 
@@ -283,6 +273,7 @@ class RealtimeModel(llm.RealtimeModel):
283
273
  conn_options=conn_options,
284
274
  bot_name=bot_name,
285
275
  system_role=system_role,
276
+ speaker=speaker,
286
277
  opening=opening,
287
278
  speaking_style=speaking_style,
288
279
  )
@@ -349,8 +340,10 @@ class RealtimeSession(
349
340
  self._current_generation: _ResponseGeneration | None = None
350
341
  self._current_item: _MessageGeneration | None = None
351
342
  self._remote_chat_ctx = llm.remote_chat_context.RemoteChatContext()
352
- self._is_opening = True
343
+ self._is_opening = False
353
344
  self._first_tts_response = True
345
+ self._first_llm_response = True
346
+ self._first_llm_sentence = True
354
347
 
355
348
  self._update_chat_ctx_lock = asyncio.Lock()
356
349
  self._update_fnc_ctx_lock = asyncio.Lock()
@@ -398,6 +391,7 @@ class RealtimeSession(
398
391
 
399
392
  async def _run_ws(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
400
393
  closing = False
394
+ logger.info("start connection")
401
395
  start_connection_request = bytearray(generate_header())
402
396
  start_connection_request.extend(int(1).to_bytes(4, "big"))
403
397
  payload_bytes = str.encode("{}")
@@ -406,64 +400,55 @@ class RealtimeSession(
406
400
  start_connection_request.extend(payload_bytes)
407
401
  await ws_conn.send_bytes(start_connection_request)
408
402
  _ = await ws_conn.receive_bytes()
409
- logger.info("startConnection response")
410
403
 
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
- )
404
+ logger.info("start session")
405
+ await self._start_session(ws_conn=ws_conn, dialog_id=self.session_id)
406
+
407
+ if self._realtime_model._opts.opening is not None:
408
+ self._is_opening = True
409
+ payload = {
410
+ "content": self._realtime_model._opts.opening,
411
+ }
412
+ hello_request = bytearray(generate_header())
413
+ hello_request.extend(int(300).to_bytes(4, "big"))
414
+ payload_bytes = str.encode(json.dumps(payload))
415
+ payload_bytes = gzip.compress(payload_bytes)
416
+ hello_request.extend((len(self.session_id)).to_bytes(4, "big"))
417
+ hello_request.extend(str.encode(self.session_id))
418
+ hello_request.extend((len(payload_bytes)).to_bytes(4, "big"))
419
+ hello_request.extend(payload_bytes)
420
+ await ws_conn.send_bytes(hello_request)
421
+ self._is_opening = True
422
+ logger.info("send hello request")
423
+
424
+ self._current_generation = _ResponseGeneration(
425
+ message_ch=utils.aio.Chan(),
426
+ function_ch=utils.aio.Chan(),
427
+ messages={},
428
+ _created_timestamp=time.time(),
429
+ _done_fut=asyncio.Future(),
430
+ )
459
431
 
460
- self._current_generation.message_ch.send_nowait(
461
- llm.MessageGeneration(
432
+ generation_ev = llm.GenerationCreatedEvent(
433
+ message_stream=self._current_generation.message_ch,
434
+ function_stream=self._current_generation.function_ch,
435
+ user_initiated=False,
436
+ )
437
+ self.emit("generation_created", generation_ev)
438
+ item_id = utils.shortuuid()
439
+ self._current_item = _MessageGeneration(
462
440
  message_id=item_id,
463
- text_stream=self._current_item.text_ch,
464
- audio_stream=self._current_item.audio_ch,
441
+ text_ch=utils.aio.Chan(),
442
+ audio_ch=utils.aio.Chan(),
443
+ )
444
+
445
+ self._current_generation.message_ch.send_nowait(
446
+ llm.MessageGeneration(
447
+ message_id=item_id,
448
+ text_stream=self._current_item.text_ch,
449
+ audio_stream=self._current_item.audio_ch,
450
+ )
465
451
  )
466
- )
467
452
 
468
453
  @utils.log_exceptions(logger=logger)
469
454
  async def _send_task() -> None:
@@ -504,7 +489,7 @@ class RealtimeSession(
504
489
  event = response.get("event")
505
490
  if event == 450: # ASRInfo
506
491
  self.emit("input_speech_started", llm.InputSpeechStartedEvent())
507
- logger.info("speech start")
492
+ logger.info("transcription start")
508
493
  elif event == 451: # ASRResponse
509
494
  response = response["payload_msg"]
510
495
  transcription = response["results"][0]["alternatives"][0][
@@ -552,16 +537,19 @@ class RealtimeSession(
552
537
  )
553
538
 
554
539
  elif event == 459: # ASREnd
555
- logger.info("speech end")
540
+ logger.info("transcription end")
556
541
  self.emit(
557
542
  "input_speech_stopped",
558
543
  llm.InputSpeechStoppedEvent(
559
544
  user_transcription_enabled=False
560
545
  ),
561
546
  )
547
+ logger.info("llm start")
548
+ logger.info("tts start")
562
549
 
563
550
  elif event == 352: # TTSResponse
564
551
  if self._first_tts_response:
552
+ logger.info("llm first sentence")
565
553
  logger.info("tts first response")
566
554
  self._first_tts_response = False
567
555
  audio_bytes = response[
@@ -580,7 +568,12 @@ class RealtimeSession(
580
568
  samples_per_channel=len(audio_bytes) // 2,
581
569
  )
582
570
  )
571
+ elif event == 350: # TTSSentenceStart
572
+ pass
573
+ elif event == 351: # TTSSentenceEnd
574
+ pass
583
575
  elif event == 359: # TTSEnded
576
+ logger.info("tts end")
584
577
  self._current_item.audio_ch.close()
585
578
  if self._is_opening:
586
579
  self._current_item.text_ch.send_nowait(
@@ -593,10 +586,15 @@ class RealtimeSession(
593
586
  self._current_generation = None
594
587
  self._first_tts_response = True
595
588
  elif event == 550: # 模型回复的文本内容
589
+ if self._first_llm_response:
590
+ logger.info("llm first response")
591
+ self._first_llm_response = False
596
592
  text = response["payload_msg"]["content"]
597
593
  self._current_item.text_ch.send_nowait(text)
598
594
  elif event == 559: # 模型回复文本结束事件
595
+ logger.info("llm end")
599
596
  self._current_item.text_ch.close()
597
+ self._first_llm_response = True
600
598
  else:
601
599
  pass
602
600
  except Exception:
@@ -607,20 +605,10 @@ class RealtimeSession(
607
605
  asyncio.create_task(_recv_task(), name="_recv_task"),
608
606
  asyncio.create_task(_send_task(), name="_send_task"),
609
607
  ]
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
608
  try:
618
609
  done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
619
-
620
- # propagate exceptions from completed tasks
621
610
  for task in done:
622
- if task != wait_reconnect_task:
623
- task.result()
611
+ task.result()
624
612
 
625
613
  finally:
626
614
  await utils.aio.cancel_and_wait(*tasks)
@@ -629,6 +617,71 @@ class RealtimeSession(
629
617
  def _create_session_update_event(self):
630
618
  pass
631
619
 
620
+ async def chat_tts_text(
621
+ self,
622
+ start: bool,
623
+ end: bool,
624
+ content: str,
625
+ ws_conn: aiohttp.ClientWebSocketResponse,
626
+ ) -> None:
627
+ """发送Chat TTS Text消息"""
628
+ payload = {
629
+ "start": start,
630
+ "end": end,
631
+ "content": content,
632
+ }
633
+ logger.info("ChatTTSTextRequest")
634
+ payload_bytes = str.encode(json.dumps(payload))
635
+ payload_bytes = gzip.compress(payload_bytes)
636
+
637
+ chat_tts_text_request = bytearray(generate_header())
638
+ chat_tts_text_request.extend(int(500).to_bytes(4, "big"))
639
+ chat_tts_text_request.extend((len(self.session_id)).to_bytes(4, "big"))
640
+ chat_tts_text_request.extend(str.encode(self.session_id))
641
+ chat_tts_text_request.extend((len(payload_bytes)).to_bytes(4, "big"))
642
+ chat_tts_text_request.extend(payload_bytes)
643
+ await ws_conn.send_bytes(chat_tts_text_request)
644
+
645
+ async def _start_session(
646
+ self, ws_conn: aiohttp.ClientWebSocketResponse, dialog_id: str
647
+ ) -> None:
648
+ request_params = self._realtime_model._opts.get_start_session_reqs(
649
+ dialog_id=dialog_id
650
+ )
651
+ payload_bytes = str.encode(json.dumps(request_params))
652
+ payload_bytes = gzip.compress(payload_bytes)
653
+ start_session_request = bytearray(generate_header())
654
+ start_session_request.extend(int(100).to_bytes(4, "big"))
655
+ start_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
656
+ start_session_request.extend(str.encode(self.session_id))
657
+ start_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
658
+ start_session_request.extend(payload_bytes)
659
+ await ws_conn.send_bytes(start_session_request)
660
+ _ = await ws_conn.receive_bytes()
661
+
662
+ async def _finish_session(self, ws_conn: aiohttp.ClientWebSocketResponse) -> None:
663
+ finish_session_request = bytearray(generate_header())
664
+ finish_session_request.extend(int(102).to_bytes(4, "big"))
665
+ payload_bytes = str.encode("{}")
666
+ payload_bytes = gzip.compress(payload_bytes)
667
+ finish_session_request.extend((len(self.session_id)).to_bytes(4, "big"))
668
+ finish_session_request.extend(str.encode(self.session_id))
669
+ finish_session_request.extend((len(payload_bytes)).to_bytes(4, "big"))
670
+ finish_session_request.extend(payload_bytes)
671
+ await ws_conn.send_bytes(finish_session_request)
672
+
673
+ async def _finish_connection(
674
+ self, ws_conn: aiohttp.ClientWebSocketResponse
675
+ ) -> None:
676
+ finish_connection_request = bytearray(generate_header())
677
+ finish_connection_request.extend(int(2).to_bytes(4, "big"))
678
+ payload_bytes = str.encode("{}")
679
+ payload_bytes = gzip.compress(payload_bytes)
680
+ finish_connection_request.extend((len(payload_bytes)).to_bytes(4, "big"))
681
+ finish_connection_request.extend(payload_bytes)
682
+ await ws_conn.send_bytes(finish_connection_request)
683
+ _ = await ws_conn.receive_bytes()
684
+
632
685
  @property
633
686
  def chat_ctx(self) -> llm.ChatContext:
634
687
  return self._remote_chat_ctx.to_chat_ctx()
@@ -0,0 +1 @@
1
+ __version__ = "1.2.3"
@@ -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.3",
13
13
  "openai>=1.75.0",
14
14
  "osc-data>=0.2.2",
15
15
  "pydantic",
@@ -1 +0,0 @@
1
- __version__ = "1.2.0"