initial open-source release
This commit is contained in:
commit
01c4605498
224 files changed
+55124
No files matched your search
@@ -0,0 +1,180 @@
|
||||
#!/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()
|
||||
Reference in new issue
Block a user