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.
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/PKG-INFO +2 -2
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/realtime.py +138 -89
- livekit_plugins_volcengine-1.2.2/livekit/plugins/volcengine/version.py +1 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/pyproject.toml +1 -1
- livekit_plugins_volcengine-1.2.0/livekit/plugins/volcengine/version.py +0 -1
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/.gitignore +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/README.md +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/__init__.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/bigmodel_stt.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/llm.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/log.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/py.typed +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/stt.py +0 -0
- {livekit_plugins_volcengine-1.2.0 → livekit_plugins_volcengine-1.2.2}/livekit/plugins/volcengine/tts.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
256
|
-
|
|
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=
|
|
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 =
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
461
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.2.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|