DroneMind/voicellmcloud/test/test_streaming_smoke.py
2026-04-14 10:08:41 +08:00

167 lines
5.3 KiB
Python

"""
流式 LLM + TTS 烟测(不播放音频、不依赖交互)。
用法(在项目根目录):
python test/test_streaming_smoke.py
依赖:服务端已启动,且 .env 中 LLM/TTS 可正常调用。
环境变量 VOICE_TEST_WS_URL 可覆盖 WebSocket URL。
"""
from __future__ import annotations
import asyncio
import json
import sys
import time
from pathlib import Path
import websockets
sys.path.insert(0, str(Path(__file__).parent))
from test_config import SERVER_URL, AUTH_TOKEN, DEVICE_ID
async def one_turn(
ws,
*,
text: str,
expect_routing: str,
) -> dict:
turn_id = f"smoke-{int(time.time() * 1000)}"
deltas: list[str] = []
dialog_order: list[str] = []
pcm = bytearray()
saw_dialog = False
first_tts_before_dialog: bool | None = None
await ws.send(
json.dumps(
{
"type": "turn.text",
"proto_version": "1.0",
"transport_profile": "text_uplink",
"turn_id": turn_id,
"text": text,
"is_final": True,
"source": "device_stt",
},
ensure_ascii=False,
)
)
while True:
msg = await asyncio.wait_for(ws.recv(), timeout=120)
if isinstance(msg, bytes):
pcm.extend(msg)
continue
data = json.loads(msg)
t = data.get("type")
dialog_order.append(t)
if t == "llm.text_delta":
if data.get("delta"):
deltas.append(data["delta"])
continue
if t == "dialog_result":
saw_dialog = True
routing = data.get("routing")
if routing != expect_routing:
return {
"ok": False,
"error": f"routing 期望 {expect_routing} 实际 {routing}",
"order": dialog_order,
}
continue
if t == "tts_audio_chunk":
if first_tts_before_dialog is None:
first_tts_before_dialog = not saw_dialog
continue
if t == "turn.complete":
reply_concat = "".join(deltas)
chat = None
# 重放顺序里没有 dialog_result 的完整对象,仅校验收尾
return {
"ok": True,
"turn_id": turn_id,
"delta_chunks": len(deltas),
"reply_preview": reply_concat[:120],
"pcm_bytes": len(pcm),
"order_tail": dialog_order[-8:],
"first_tts_before_dialog": first_tts_before_dialog,
"metrics": data.get("metrics"),
}
if t == "error":
return {
"ok": False,
"error": f"{data.get('code')}: {data.get('message')}",
"order": dialog_order,
}
return {"ok": False, "error": "unreachable"}
async def main() -> int:
print(f"连接 {SERVER_URL}")
try:
async with websockets.connect(SERVER_URL) as ws:
await ws.send(
json.dumps(
{
"type": "session.start",
"proto_version": "1.0",
"transport_profile": "text_uplink",
"session_id": "smoke-session",
"auth_token": AUTH_TOKEN,
"client": {
"device_id": DEVICE_ID,
"locale": "zh-CN",
"capabilities": {
"playback_sample_rate_hz": 24000,
"prefer_tts_codec": "pcm_s16le",
},
"protocol": {
"dialog_result": "cloud_voice_dialog_v1"
},
},
},
ensure_ascii=False,
)
)
raw = await asyncio.wait_for(ws.recv(), timeout=30)
ready = json.loads(raw)
if ready.get("type") != "session.ready":
print("FAIL: 非 session.ready", ready)
return 1
print("OK session.ready")
# 闲聊
r1 = await one_turn(ws, text="你好,用一句话介绍你自己。", expect_routing="chitchat")
print("\n[闲聊]", json.dumps(r1, ensure_ascii=False, indent=2))
if not r1.get("ok"):
return 1
if r1["delta_chunks"] < 1:
print("WARN: 未收到 llm.text_delta 增量(可能模型一次块较大)")
if r1["pcm_bytes"] < 1000:
print("FAIL: PCM 过短", r1["pcm_bytes"])
return 1
# 飞控
r2 = await one_turn(ws, text="返航", expect_routing="flight_intent")
print("\n[飞控]", json.dumps(r2, ensure_ascii=False, indent=2))
if not r2.get("ok"):
return 1
if r2["pcm_bytes"] < 500:
print("FAIL: 飞控 PCM 过短", r2["pcm_bytes"])
return 1
print("\nPASS: 流式 + TTS 烟测通过")
return 0
except Exception as e:
print("FAIL:", e)
import traceback
traceback.print_exc()
return 1
if __name__ == "__main__":
sys.exit(asyncio.run(main()))