[播报文本净化]:完成TTS表情包过滤,包含emoji清理、上下文净化和回归测试

This commit is contained in:
mkbk
2026-06-18 12:45:31 +08:00
parent 25255f178e
commit 3408a30e25
8 changed files with 285 additions and 22 deletions
+59 -1
View File
@@ -26,7 +26,7 @@ from owner_voice_pet.events import (
PipelineEventBus,
)
from owner_voice_pet.llm import MockLlmProvider
from owner_voice_pet.models import AudioFrame, AudioSegment, PlaybackResult, ReplyDelta, Transcript, TransportHealth
from owner_voice_pet.models import AudioFrame, AudioSegment, ErrorCode, PlaybackResult, ReplyDelta, Transcript, TransportHealth
from owner_voice_pet.assistant_pipeline import VoiceAssistantPipeline
from owner_voice_pet.runtime import build_live_runtime
from owner_voice_pet.stt import MetadataSttProvider, SherpaOnnxSttProvider
@@ -409,6 +409,64 @@ class LiveRuntimeTests(unittest.TestCase):
self.assertEqual(llm.calls[0][-1].content, "第一问")
self.assertNotIn("小杰小杰", llm.calls[0][-1].content)
def test_assistant_reply_sanitizes_tts_text_and_context(self) -> None:
frames = [wake_frame(0, 0)]
frames.extend(segment_frames(1, 20, partials=["第一问", "第一问"]))
transport = MemoryAudioTransport(frames, flush_clears_input=False)
stt = QueueSttProvider(["第一问"])
llm = QueueLlmProvider([["你好 😊。没问题[捂脸],我来帮你。"]])
context = ConversationContext()
runtime = VoiceAssistantPipeline(
config=AppConfig(llm_api_key="secret", speech_provider="cloud", wake_ack_text=""),
transport=transport,
wakeword=KeywordWakeWordProvider(),
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
stt=stt,
realtime_stt=MetadataSttProvider(),
llm=llm,
tts=SineTtsProvider(),
context=context,
reporter=RecordingReporter(),
event_bus=PipelineEventBus(),
)
summary = runtime.run(max_turns=1)
context_texts = [message.content for message in context.messages()]
self.assertEqual(summary.completed_turns, 1)
self.assertEqual(transport.played_segments[0].metadata["text"], "你好。没问题,我来帮你。")
self.assertIn("你好。没问题,我来帮你。", context_texts)
self.assertFalse(any("😊" in text or "[捂脸]" in text for text in context_texts))
def test_emoji_only_reply_recovers_without_tts_playback(self) -> None:
frames = [wake_frame(0, 0)]
frames.extend(segment_frames(1, 20, partials=["第一问", "第一问"]))
transport = MemoryAudioTransport(frames, flush_clears_input=False)
runtime = VoiceAssistantPipeline(
config=AppConfig(llm_api_key="secret", speech_provider="cloud", wake_ack_text=""),
transport=transport,
wakeword=KeywordWakeWordProvider(),
vad_recorder=VadRecorder(EnergyVadProvider(), min_duration_ms=40, end_silence_ms=40),
stt=QueueSttProvider(["第一问"]),
realtime_stt=MetadataSttProvider(),
llm=QueueLlmProvider([["😂😂"]]),
tts=SineTtsProvider(),
context=ConversationContext(),
reporter=RecordingReporter(),
event_bus=PipelineEventBus(),
)
summary = runtime.run(once=True)
event_types = [event.type for event in runtime.event_bus.events]
self.assertEqual(summary.completed_turns, 0)
self.assertEqual(summary.failed_turns, 1)
self.assertIsNotNone(summary.last_error)
self.assertEqual(summary.last_error.code, ErrorCode.TTS_EMPTY_AUDIO)
self.assertEqual(transport.played_segments, [])
self.assertNotIn(TTS_STARTED, event_types)
self.assertEqual(event_types[-1], STANDBY_RESUMED)
def test_live_runtime_uses_primary_speaker_endpoint_by_default(self) -> None:
runtime = build_live_runtime(AppConfig(llm_api_key="secret"))
self.assertIsInstance(runtime.vad_recorder, PrimarySpeakerVadRecorder)