Files
2026-08-01 11:15:19 +08:00

181 lines
5.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
VoxCPM 流式 TTS 测试脚本。
用法:
python test_tts.py # 默认测试
python test_tts.py --text "你好世界" # 自定义文本
python test_tts.py --url http://x.x.x.x:8902 # 远程服务器
python test_tts.py --save test_output.wav # 保存为 WAV 文件
"""
import argparse
import base64
import json
import struct
import sys
import time
import requests
def main():
parser = argparse.ArgumentParser(description="VoxCPM 流式 TTS 测试")
parser.add_argument("--url", default="http://0.0.0.0:8902", help="TTS 服务地址")
parser.add_argument("--text", default="你好,这是一个流式语音合成测试。今天天气不错,适合出去散步。",
help="测试文本")
parser.add_argument("--save", default="", help="保存 WAV 文件路径(不指定则不保存)")
parser.add_argument("--play", action="store_true", help="本地播放(需要 pyaudio)")
args = parser.parse_args()
base_url = args.url.rstrip("/")
request_id = f"test-{int(time.time() * 1000)}"
# 1. 健康检查
print(f"→ 健康检查 {base_url}/health ...")
try:
r = requests.get(f"{base_url}/health", timeout=5)
r.raise_for_status()
print(f" 状态: {r.json()}")
except Exception as e:
print(f" ❌ 无法连接 {base_url}: {e}")
sys.exit(1)
# 2. 流式 TTS 请求
print(f"→ 流式 TTS 请求: \"{args.text}\"")
t_start = time.perf_counter()
sample_rate = 0
all_pcm = bytearray()
total_samples = 0
total_chunks = 0
try:
resp = requests.post(
f"{base_url}/stream_tts",
json={"text": args.text, "request_id": request_id},
stream=True,
timeout=300,
)
resp.raise_for_status()
for line in resp.iter_lines(decode_unicode=True):
if not line or not line.startswith("data: "):
continue
payload = json.loads(line[len("data: "):])
event_type = payload.get("type", "")
if event_type == "audio.start":
sample_rate = payload.get("sample_rate", 0)
num_segments = payload.get("num_segments", 0)
print(f" audio.start | 采样率={sample_rate}Hz 句数={num_segments}")
elif event_type == "audio.chunk":
b64_data = payload.get("data", "")
idx = payload.get("index", 0)
num_s = payload.get("num_samples", 0)
if b64_data:
pcm = base64.b64decode(b64_data)
all_pcm.extend(pcm)
total_samples += num_s
total_chunks += 1
# 进度提示
if idx % 5 == 0 and b64_data:
dur = total_samples / sample_rate if sample_rate else 0
print(f" audio.chunk | #{idx} 累计={dur:.1f}s")
elif event_type == "audio.done":
total = payload.get("total_segments", 0)
print(f" audio.done | 共{total}句 {total_samples}样本 {total_chunks}块")
elif event_type == "audio.error":
print(f" ❌ audio.error: {payload.get('message', '')}")
sys.exit(1)
elif event_type == "audio.cancelled":
print(f" ⚠ audio.cancelled")
break
elif event_type == "done":
pass # 流结束
except requests.exceptions.ConnectionError as e:
print(f" ❌ 连接失败: {e}")
sys.exit(1)
except Exception as e:
print(f" ❌ 错误: {e}")
sys.exit(1)
t_elapsed = time.perf_counter() - t_start
duration = total_samples / sample_rate if sample_rate else 0
rtf = t_elapsed / duration if duration > 0 else 0
print(f"\n{'='*50}")
print(f" 总耗时: {t_elapsed:.2f}s")
print(f" 音频时长: {duration:.2f}s")
print(f" RTF: {rtf:.2f} (越小越好,<1 表示比实时快)")
print(f" 采样率: {sample_rate} Hz")
print(f" 总样本数: {total_samples}")
print(f" PCM 大小: {len(all_pcm)} bytes")
print(f"{'='*50}")
if not all_pcm:
print(" ⚠ 未收到任何音频数据")
sys.exit(1)
# 3. 保存 WAV
if args.save:
_save_wav(args.save, all_pcm, sample_rate)
print(f"→ 已保存: {args.save}")
# 4. 本地播放
if args.play:
_play_pcm(all_pcm, sample_rate)
def _save_wav(filepath: str, pcm_f32: bytearray, sample_rate: int):
"""将 PCM f32le 转为 16-bit WAV 保存。"""
import io
import wave
import numpy as np
arr = np.frombuffer(pcm_f32, dtype=np.float32)
# 转为 int16
arr_i16 = (arr * 32767).clip(-32768, 32767).astype(np.int16)
with wave.open(filepath, "wb") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(arr_i16.tobytes())
def _play_pcm(pcm_f32: bytearray, sample_rate: int):
"""通过 pyaudio 播放 PCM 音频。"""
try:
import pyaudio
import numpy as np
except ImportError:
print(" ⚠ 需要安装 pyaudio: pip install pyaudio")
return
arr = np.frombuffer(pcm_f32, dtype=np.float32)
arr_i16 = (arr * 32767).clip(-32768, 32767).astype(np.int16)
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paInt16,
channels=1,
rate=sample_rate,
output=True,
)
print("→ 播放中...")
stream.write(arr_i16.tobytes())
stream.stop_stream()
stream.close()
p.terminate()
print("→ 播放完毕")
if __name__ == "__main__":
main()