[流式回复播放]:完成LLM到TTS流式播报,包含句子切分和可取消播放队列
This commit is contained in:
@@ -88,6 +88,7 @@ class CliAcceptanceTests(unittest.TestCase):
|
||||
self.assertFalse(data["full_duplex_runtime_ready"])
|
||||
self.assertEqual(data["audio_apm_provider"], "webrtc")
|
||||
self.assertEqual(data["audio_apm_error_code"], "AUDIO_APM_UNAVAILABLE")
|
||||
self.assertTrue(data["llm_streaming_enabled"])
|
||||
self.assertEqual(data["turn_based_entry"], "run-live")
|
||||
|
||||
def test_run_agent_live_once_invokes_agent_runtime(self) -> None:
|
||||
|
||||
@@ -134,6 +134,22 @@ class FullDuplexIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(playback.spoken.text, "你好。")
|
||||
self.assertEqual(render.frame_count, 1)
|
||||
|
||||
def test_full_duplex_runtime_streaming_response_writes_render_reference_and_spoken_text(self) -> None:
|
||||
runtime = FullDuplexAgentRuntime(
|
||||
config=AppConfig(audio_apm_provider="fake", audio_apm_required=False),
|
||||
llm_provider=FakeStreamingLlmProvider(
|
||||
[LlmStreamEvent("delta", "第一句。第二句。"), LlmStreamEvent("finish", finish_reason="stop")]
|
||||
),
|
||||
tts_provider=FakeStreamingTtsProvider(),
|
||||
)
|
||||
|
||||
spoken = runtime.run_streaming_response_fixture([Message("user", "你好", 1.0)])
|
||||
|
||||
self.assertEqual(spoken, "第一句。第二句。")
|
||||
self.assertIsNotNone(runtime.audio_hub)
|
||||
self.assertEqual(runtime.audio_hub.render_reference.frame_count, 2)
|
||||
self.assertEqual(runtime.playback_queue.pending_items, 0)
|
||||
|
||||
def test_memory_restart_and_tool_search_integration(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
db_path = Path(tmp) / "memory.sqlite3"
|
||||
|
||||
@@ -2,9 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from owner_voice_pet.full_duplex_audio import RenderReferenceRingBuffer
|
||||
from owner_voice_pet.full_duplex_audio import AudioHub, FakeWebRtcAudioProcessingProvider, RenderReferenceRingBuffer
|
||||
from owner_voice_pet.full_duplex_control import CancellationGraph
|
||||
from owner_voice_pet.full_duplex_response import (
|
||||
AudioSegmentStreamingTtsProvider,
|
||||
FakeStreamingLlmProvider,
|
||||
FakeStreamingTtsProvider,
|
||||
InterruptiblePlaybackQueue,
|
||||
@@ -13,6 +14,7 @@ from owner_voice_pet.full_duplex_response import (
|
||||
prepare_tts_sentence,
|
||||
)
|
||||
from owner_voice_pet.models import AudioFrame, Message
|
||||
from owner_voice_pet.tts import SineTtsProvider
|
||||
|
||||
|
||||
class FullDuplexResponseTests(unittest.TestCase):
|
||||
@@ -74,6 +76,17 @@ class FullDuplexResponseTests(unittest.TestCase):
|
||||
self.assertEqual(frames, [])
|
||||
self.assertEqual(session.accepted_text, [])
|
||||
|
||||
def test_audio_segment_streaming_tts_wraps_sync_tts_as_pcm_chunks(self) -> None:
|
||||
provider = AudioSegmentStreamingTtsProvider(SineTtsProvider(), chunk_ms=30)
|
||||
session = provider.start_stream(voice="default", sample_rate=16000)
|
||||
|
||||
frames = session.accept_text("你好。")
|
||||
|
||||
self.assertEqual(provider.started, [("default", 16000)])
|
||||
self.assertEqual(session.accepted_text, ["你好。"])
|
||||
self.assertGreater(len(frames), 1)
|
||||
self.assertTrue(all(frame.metadata["tts_text"] == "你好。" for frame in frames))
|
||||
|
||||
def test_interruptible_playback_commits_only_fully_played_text(self) -> None:
|
||||
queue = InterruptiblePlaybackQueue()
|
||||
render = RenderReferenceRingBuffer(capacity_ms=1000)
|
||||
@@ -92,6 +105,20 @@ class FullDuplexResponseTests(unittest.TestCase):
|
||||
self.assertEqual(queue.spoken.text, "完整句。")
|
||||
self.assertEqual(render.frame_count, 2)
|
||||
|
||||
def test_interruptible_playback_writes_audio_hub_render_reference(self) -> None:
|
||||
queue = InterruptiblePlaybackQueue()
|
||||
graph = CancellationGraph("turn")
|
||||
processor = FakeWebRtcAudioProcessingProvider()
|
||||
hub = AudioHub(processor=processor, render_capacity_ms=1000)
|
||||
frames = [AudioFrame(b"1", 16000, 1, 0, 1, {"duration_ms": 20})]
|
||||
queue.enqueue("一句。", frames)
|
||||
|
||||
result = queue.play_next(audio_hub=hub, cancellation=graph.root)
|
||||
|
||||
self.assertFalse(result.interrupted)
|
||||
self.assertEqual(hub.render_reference.frame_count, 1)
|
||||
self.assertEqual([item.frame_id for item in processor.render_frames], [1])
|
||||
|
||||
def test_interruptible_playback_does_not_commit_interrupted_text(self) -> None:
|
||||
queue = InterruptiblePlaybackQueue()
|
||||
render = RenderReferenceRingBuffer(capacity_ms=1000)
|
||||
|
||||
Reference in New Issue
Block a user