[主说话人端点]:完成音色消失结束录音,包含临时音色画像、端点配置和回归测试

This commit is contained in:
mkbk
2026-06-17 21:33:33 +08:00
parent f9da304568
commit da91be6e5c
8 changed files with 372 additions and 12 deletions
+77 -1
View File
@@ -7,7 +7,7 @@ import unittest
from owner_voice_pet.models import AudioFrame, AudioSegment, ErrorCode, ProviderError
from owner_voice_pet.config import AppConfig
from owner_voice_pet.stt import CloudAsrSttProvider, MetadataSttProvider, SherpaOnnxSttProvider, is_valid_transcript_text
from owner_voice_pet.vad import EnergyVadProvider, HybridVadProvider, VadRecorder
from owner_voice_pet.vad import EnergyVadProvider, HybridVadProvider, PrimarySpeakerVadRecorder, VadRecorder
from owner_voice_pet.wakeword import (
KeywordWakeWordProvider,
MissingWakeWordModelProvider,
@@ -111,6 +111,82 @@ class WakeVadSttTests(unittest.TestCase):
self.assertTrue(result.is_speech)
self.assertEqual(result.speech_ms, 20)
def test_primary_speaker_endpoint_stops_on_background_noise(self) -> None:
provider = EnergyVadProvider()
provider.load()
recorder = PrimarySpeakerVadRecorder(
provider,
min_duration_ms=40,
end_silence_ms=1000,
speaker_profile_ms=40,
speaker_absent_ms=40,
)
frames = [
make_frame(1, 0, speech=True, metadata={"speaker_id": "owner", "transcript": "你是谁"}),
make_frame(2, 20, speech=True, metadata={"speaker_id": "owner"}),
make_frame(3, 40, speech=True, metadata={"speaker_id": "background"}),
make_frame(4, 60, speech=True, metadata={"speaker_id": "background"}),
make_frame(5, 80, speech=True, metadata={"speaker_id": "owner", "transcript": "你是谁第二次"}),
]
segment = None
consumed = 0
for item in frames:
consumed += 1
result = recorder.feed(item)
if isinstance(result, AudioSegment):
segment = result
break
self.assertIsNotNone(segment)
assert segment is not None
self.assertEqual(segment.metadata["end_reason"], "primary_speaker_absent")
self.assertEqual(consumed, 4)
self.assertEqual(segment.metadata["transcript"], "你是谁")
def test_primary_speaker_endpoint_allows_short_pause(self) -> None:
provider = EnergyVadProvider()
provider.load()
recorder = PrimarySpeakerVadRecorder(
provider,
min_duration_ms=40,
end_silence_ms=1000,
speaker_profile_ms=40,
speaker_absent_ms=60,
)
frames = [
make_frame(1, 0, speech=True, metadata={"speaker_id": "owner"}),
make_frame(2, 20, speech=True, metadata={"speaker_id": "owner"}),
make_frame(3, 40, speech=False),
make_frame(4, 60, speech=True, metadata={"speaker_id": "owner"}),
make_frame(5, 80, speech=False),
make_frame(6, 100, speech=False),
make_frame(7, 120, speech=False),
]
results = [recorder.feed(item) for item in frames]
self.assertIsNone(results[2])
self.assertIsNone(results[3])
self.assertIsInstance(results[-1], AudioSegment)
assert isinstance(results[-1], AudioSegment)
self.assertEqual(results[-1].metadata["end_reason"], "primary_speaker_absent")
def test_primary_speaker_endpoint_falls_back_to_vad_when_profile_missing(self) -> None:
provider = EnergyVadProvider()
provider.load()
recorder = PrimarySpeakerVadRecorder(provider, min_duration_ms=40, end_silence_ms=40)
frames = [
AudioFrame(b"", 16000, 1, 0, 1, {"duration_ms": 20, "speech": True, "transcript": "你好"}),
AudioFrame(b"", 16000, 1, 20, 2, {"duration_ms": 20, "speech": True}),
make_frame(3, 40, speech=False),
make_frame(4, 60, speech=False),
]
segment = None
for item in frames:
result = recorder.feed(item)
if isinstance(result, AudioSegment):
segment = result
self.assertIsNotNone(segment)
assert segment is not None
self.assertEqual(segment.metadata["end_reason"], "silence")
def test_metadata_stt_transcribes_fixture_text(self) -> None:
provider = MetadataSttProvider()
provider.load()