[打断低延迟修复]:完成真实播报打断门控修正,包含短保护窗口、用户音色优先和诊断输出

This commit is contained in:
mkbk
2026-06-19 11:27:34 +08:00
parent f1f4885cc5
commit 7c6797aeb0
8 changed files with 222 additions and 32 deletions
+43
View File
@@ -197,6 +197,49 @@ class BargeInTests(unittest.TestCase):
self.assertFalse(monitor.interrupted)
def test_user_profile_match_can_override_render_echo_candidate(self) -> None:
gate = BargeInSpeakerGate(
enabled=True,
user_similarity_threshold=0.62,
assistant_reject_threshold=0.72,
min_rms=0.001,
)
gate.remember_user_segment(segment_for_speaker("owner"))
assistant = AudioSegment(b"\x01\x20\x02\x20" * 320, 16000, 1, 0, 40, {"speaker_id": "assistant"})
frames = [
AudioFrame(
assistant.pcm[: 640 * 2],
16000,
1,
20 * idx,
idx,
{"duration_ms": 20, "speech": True, "speaker_id": "owner"},
)
for idx in (1, 2)
]
transport = MemoryAudioTransport(frames)
transport.start_input()
monitor = AsyncBargeInMonitor(
transport=transport,
vad_provider=EnergyVadProvider(threshold=1),
realtime_stt=None,
speaker_gate=gate,
assistant_profile=gate.assistant_profile(assistant),
assistant_reference=assistant,
echo_guard_ms=0,
min_speech_ms=40,
listen_interval_ms=1,
)
monitor.vad_provider.load()
monitor.start()
deadline = time.monotonic() + 1
while not monitor.interrupted and time.monotonic() < deadline:
time.sleep(0.005)
monitor.stop()
self.assertTrue(monitor.interrupted)
if __name__ == "__main__":
unittest.main()
+33 -7
View File
@@ -204,7 +204,12 @@ def make_runtime(
reporter = RecordingReporter()
event_bus = PipelineEventBus()
runtime = VoiceAssistantPipeline(
config=AppConfig(llm_api_key="secret", speech_provider="cloud", wake_ack_text=wake_ack_text),
config=AppConfig(
llm_api_key="secret",
speech_provider="cloud",
wake_ack_text=wake_ack_text,
barge_in_enabled=False,
),
transport=transport,
wakeword=KeywordWakeWordProvider(),
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
@@ -345,7 +350,12 @@ class LiveRuntimeTests(unittest.TestCase):
transport = MemoryAudioTransport(frames, flush_clears_input=False)
ack_tts = CountingTtsProvider()
runtime = VoiceAssistantPipeline(
config=AppConfig(llm_api_key="secret", speech_provider="cloud", wake_ack_text="我在"),
config=AppConfig(
llm_api_key="secret",
speech_provider="cloud",
wake_ack_text="我在",
barge_in_enabled=False,
),
transport=transport,
wakeword=KeywordWakeWordProvider(),
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
@@ -363,8 +373,8 @@ class LiveRuntimeTests(unittest.TestCase):
self.assertEqual(summary.completed_turns, 2)
self.assertEqual(ack_tts.synthesized_texts, ["我在"])
self.assertEqual(transport.played_segments[0].metadata["text"], "我在")
self.assertEqual(transport.played_segments[3].metadata["text"], "我在")
ack_segments = [segment for segment in transport.played_segments if segment.metadata.get("text") == "我在"]
self.assertEqual(len(ack_segments), 2)
def test_zero_post_playback_drain_flushes_without_dropping_prefilled_question(self) -> None:
runtime, stt, _, transport, reporter = make_runtime(["第一问"])
@@ -387,7 +397,7 @@ class LiveRuntimeTests(unittest.TestCase):
self.assertEqual(summary.completed_turns, 1)
self.assertEqual(reporter.transcripts, ["第一问"])
self.assertEqual(len(stt.calls), 1)
self.assertEqual(len(transport.played_segments), 2)
self.assertGreaterEqual(len(transport.played_segments), 2)
self.assertEqual(transport.flush_count, 2)
self.assertEqual(transport.played_segments[-1].metadata["chime"], "end")
self.assertEqual(transport.played_segments[-1].metadata["source"], "file")
@@ -400,8 +410,8 @@ class LiveRuntimeTests(unittest.TestCase):
summary = runtime.run(max_turns=1)
self.assertEqual(summary.completed_turns, 1)
self.assertEqual(len(transport.played_segments), 1)
self.assertNotIn("chime", transport.played_segments[-1].metadata)
self.assertGreaterEqual(len(transport.played_segments), 1)
self.assertFalse(any("chime" in segment.metadata for segment in transport.played_segments))
def test_temporary_context_is_sent_to_second_llm_call(self) -> None:
runtime, _, llm, _, _ = make_runtime(["第一问", "第二问"])
@@ -587,6 +597,7 @@ class LiveRuntimeTests(unittest.TestCase):
speech_provider="cloud",
wake_ack_text="",
followup_listen_timeout_ms=3000,
barge_in_enabled=False,
),
transport=transport,
wakeword=KeywordWakeWordProvider(),
@@ -627,6 +638,7 @@ class LiveRuntimeTests(unittest.TestCase):
speech_provider="cloud",
wake_ack_text="",
followup_listen_timeout_ms=3000,
barge_in_enabled=False,
),
transport=transport,
wakeword=KeywordWakeWordProvider(),
@@ -662,6 +674,7 @@ class LiveRuntimeTests(unittest.TestCase):
speech_provider="cloud",
wake_ack_text="",
followup_listen_timeout_ms=3000,
barge_in_enabled=False,
),
transport=transport,
wakeword=KeywordWakeWordProvider(),
@@ -771,6 +784,19 @@ class LiveRuntimeTests(unittest.TestCase):
self.assertEqual(reporter.transcripts[-1], "打断问题")
self.assertEqual(len(llm.calls), 2)
def test_barge_in_echo_guard_is_capped_and_only_used_once_per_reply(self) -> None:
runtime, _, _, _, _ = make_runtime(["第一问"])
runtime.controller._barge_in_reply_guard_used = False
segment = AudioSegment(b"\x01\x00" * 16000, 16000, 1, 0, 1000)
first_guard = runtime.controller._effective_barge_in_echo_guard_ms(segment)
second_guard = runtime.controller._effective_barge_in_echo_guard_ms(segment)
min_speech = runtime.controller._effective_barge_in_min_speech_ms()
self.assertEqual(first_guard, 120)
self.assertEqual(second_guard, 0)
self.assertLessEqual(min_speech, runtime.config.interrupt_target_latency_ms)
if __name__ == "__main__":
unittest.main()