[全双工依赖接入]:完成真实APM依赖安装和接线,包含aec-audio-processing、音频自测和最终门禁

This commit is contained in:
mkbk
2026-06-19 13:26:25 +08:00
parent 98ae711971
commit caf35783f8
5 changed files with 199 additions and 17 deletions
+18 -11
View File
@@ -85,9 +85,10 @@ class CliAcceptanceTests(unittest.TestCase):
self.assertTrue(data["success"])
self.assertEqual(data["command"], "run-agent-live")
self.assertEqual(data["assistant_mode"], "full_duplex_agent")
self.assertFalse(data["full_duplex_runtime_ready"])
self.assertTrue(data["full_duplex_runtime_ready"])
self.assertEqual(data["audio_apm_provider"], "webrtc")
self.assertEqual(data["audio_apm_error_code"], "AUDIO_APM_UNAVAILABLE")
self.assertEqual(data["audio_apm_error_code"], "")
self.assertTrue(data["audio_apm_available"])
self.assertTrue(data["llm_streaming_enabled"])
self.assertEqual(data["turn_based_entry"], "run-live")
@@ -215,16 +216,22 @@ class CliAcceptanceTests(unittest.TestCase):
self.assertTrue(data["checks"]["barge_in"])
self.assertTrue(data["checks"]["tool_router"])
def test_audio_self_test_reports_missing_required_apm(self) -> None:
def test_audio_self_test_reports_missing_required_apm_when_probe_fails(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
code, data = self.call(
"--env-file",
str(Path(tmp) / ".env"),
"audio-self-test",
"--duration",
"1",
"--check-echo",
)
with patch("owner_voice_pet.full_duplex_audio.webrtc_apm_probe") as probe:
probe.return_value = type(
"Health",
(),
{"available": False, "provider": "webrtc", "message": "missing binding"},
)()
code, data = self.call(
"--env-file",
str(Path(tmp) / ".env"),
"audio-self-test",
"--duration",
"1",
"--check-echo",
)
self.assertEqual(code, 1)
self.assertFalse(data["success"])
+28 -1
View File
@@ -5,6 +5,7 @@ from unittest.mock import patch
from owner_voice_pet.config import AppConfig
from owner_voice_pet.full_duplex_audio import (
AecAudioProcessingProvider,
AudioHub,
CaptureRingBuffer,
FakeWebRtcAudioProcessingProvider,
@@ -181,12 +182,26 @@ class FullDuplexAudioTests(unittest.TestCase):
self.assertIsInstance(provider, NoopAudioProcessingProvider)
self.assertTrue(provider.health_check().fallback_active)
def test_webrtc_provider_uses_aec_audio_processing_when_available(self) -> None:
with patch("owner_voice_pet.full_duplex_audio.webrtc_apm_probe") as probe:
probe.return_value = type(
"Health",
(),
{"available": True, "provider": "aec_audio_processing", "message": "found binding"},
)()
provider = build_audio_processing_provider(
AppConfig(audio_apm_provider="webrtc", audio_apm_required=True)
)
self.assertIsInstance(provider, AecAudioProcessingProvider)
self.assertTrue(provider.health_check().available)
def test_detected_but_unwired_webrtc_provider_fails_when_required(self) -> None:
with patch("owner_voice_pet.full_duplex_audio.webrtc_apm_probe") as probe:
probe.return_value = type(
"Health",
(),
{"available": True, "message": "found binding"},
{"available": True, "provider": "webrtc_audio_processing", "message": "found binding"},
)()
with self.assertRaises(ProviderError) as raised:
build_audio_processing_provider(
@@ -195,6 +210,18 @@ class FullDuplexAudioTests(unittest.TestCase):
self.assertEqual(raised.exception.code, ErrorCode.AUDIO_APM_UNAVAILABLE)
def test_aec_audio_processing_provider_processes_capture_and_render(self) -> None:
provider = AecAudioProcessingProvider(sample_rate=16000, channels=1)
provider.process_render(frame(1, 0, pcm=b"\x00\x00" * 160))
processed = provider.process_capture(frame(2, 20, pcm=b"\x01\x00" * 320, duration_ms=20))
self.assertEqual(processed.sample_rate, 16000)
self.assertEqual(processed.channels, 1)
self.assertEqual(len(processed.pcm), 640)
self.assertEqual(processed.metadata["apm_provider"], "aec_audio_processing")
self.assertEqual(len(provider.render_frames), 1)
def test_webrtc_probe_reports_unavailable_without_binding(self) -> None:
with patch("importlib.util.find_spec", return_value=None):
health = webrtc_apm_probe()