92 lines
2.9 KiB
Python
92 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""传输适配层的无硬件回归测试。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import tempfile
|
|
import time
|
|
import wave
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
import numpy as np
|
|
from av import AudioFrame
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from client.test_client import FileAudioTrack, ReplyRecorder
|
|
from services.bot_server import SimpleVAD, extract_user_text
|
|
|
|
|
|
def _write_test_wav(path: Path) -> None:
|
|
samples = np.ones(1600, dtype=np.int16) * 1000 # 100ms 语音
|
|
with wave.open(str(path), "wb") as output:
|
|
output.setnchannels(1)
|
|
output.setsampwidth(2)
|
|
output.setframerate(16000)
|
|
output.writeframes(samples.tobytes())
|
|
|
|
|
|
async def test_file_track_clock() -> None:
|
|
with tempfile.TemporaryDirectory(prefix="transport_test_") as directory:
|
|
path = Path(directory) / "input.wav"
|
|
_write_test_wav(path)
|
|
track = FileAudioTrack(path, tail_silence_s=0.8)
|
|
track._t0 = time.perf_counter() - 10 # 测试不需要真实等待
|
|
pts = []
|
|
while not track.finished:
|
|
pts.append((await track.recv()).pts)
|
|
|
|
assert len(pts) == 90, len(pts) # 100ms 音频 + 800ms 静音
|
|
assert all(after > before for before, after in zip(pts, pts[1:])), pts[-5:]
|
|
|
|
|
|
def test_vad_tail_silence() -> None:
|
|
vad = SimpleVAD()
|
|
events = [vad.feed(np.ones(320, dtype=np.int16) * 1000, 20) for _ in range(20)]
|
|
events += [vad.feed(np.zeros(320, dtype=np.int16), 20) for _ in range(40)]
|
|
assert "end" in events
|
|
|
|
|
|
def test_reply_requires_non_silent_audio() -> None:
|
|
recorder = ReplyRecorder(Path("/tmp/unused-transport-test.wav"))
|
|
silence = AudioFrame(format="s16", layout="mono", samples=320)
|
|
silence.planes[0].update(np.zeros(320, dtype=np.int16).tobytes())
|
|
silence.sample_rate = 48000
|
|
recorder.feed(silence)
|
|
assert not recorder.has_speech
|
|
assert recorder.sample_rate == 48000
|
|
|
|
speech = AudioFrame(format="s16", layout="mono", samples=320)
|
|
speech.planes[0].update((np.ones(320, dtype=np.int16) * 500).tobytes())
|
|
speech.sample_rate = 48000
|
|
recorder.feed(speech)
|
|
assert recorder.has_speech
|
|
|
|
|
|
def test_extract_rtvi_user_text() -> None:
|
|
assert extract_user_text([
|
|
{"role": "assistant", "content": "旧回复"},
|
|
{"role": "user", "content": " 文字问题 "},
|
|
]) == "文字问题"
|
|
assert extract_user_text([{
|
|
"role": "user",
|
|
"content": [{"type": "text", "text": "第一段"}, {"text": "第二段"}],
|
|
}]) == "第一段 第二段"
|
|
assert extract_user_text([{"role": "assistant", "content": "没有用户消息"}]) == ""
|
|
|
|
|
|
async def main() -> int:
|
|
await test_file_track_clock()
|
|
test_vad_tail_silence()
|
|
test_reply_requires_non_silent_audio()
|
|
test_extract_rtvi_user_text()
|
|
print("传输适配层回归测试:4/4 PASS")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(asyncio.run(main()))
|