最新哦,分词策略放到cli了
This commit is contained in:
1 parent
fcc94ffd73
commit
f9f9645fa1
9 files changed
+270
-305
No files matched your search
+144
-62
@@ -1,71 +1,153 @@
|
||||
import requests
|
||||
import argparse
|
||||
import os
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
真实 TTS API 测试(增强版):验证分块合并 + 并发耗时。
|
||||
"""
|
||||
|
||||
import io
|
||||
import wave
|
||||
import re
|
||||
import time
|
||||
import os
|
||||
import sys
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import requests
|
||||
|
||||
# 默认服务地址
|
||||
BASE_URL = "http://127.0.0.1:8000"
|
||||
# ======================== 配置 ========================
|
||||
TTS_BASE_URL = "http://192.168.137.164:8902"
|
||||
OUTPUT_WAV = "test_output.wav"
|
||||
REQUEST_TIMEOUT = 120
|
||||
MAX_PARALLEL = 4
|
||||
MIN_EFFECTIVE_CHARS = 20
|
||||
# =====================================================
|
||||
|
||||
def test_health():
|
||||
"""检查服务是否在线"""
|
||||
resp = requests.get(f"{BASE_URL}/health")
|
||||
print(f"Health check: {resp.status_code} {resp.json()}")
|
||||
SENT_SPLIT_PATTERN = re.compile(r'(?<=[。!?;,、])')
|
||||
REMOVE_PUNCT_PATTERN = re.compile(r'[。!?;,、:;“”()《》【】\-\_\—\~\.\,\!\?\s·]')
|
||||
|
||||
def test_tts(text, speaker=None, speed=1.0, output="output.wav"):
|
||||
"""
|
||||
调用 TTS 接口并保存音频文件
|
||||
"""
|
||||
params = {"text": text, "speed": speed}
|
||||
if speaker:
|
||||
params["speaker"] = speaker
|
||||
def effective_length(text: str) -> int:
|
||||
return len(REMOVE_PUNCT_PATTERN.sub('', text))
|
||||
|
||||
print(f"Requesting TTS...")
|
||||
start = time.time()
|
||||
resp = requests.get(f"{BASE_URL}/tts", params=params)
|
||||
elapsed = time.time() - start
|
||||
def split_text(text: str, min_effective_chars: int = MIN_EFFECTIVE_CHARS) -> list[str]:
|
||||
raw_chunks = [c.strip() for c in SENT_SPLIT_PATTERN.split(text) if c.strip()]
|
||||
if not raw_chunks:
|
||||
return [text]
|
||||
|
||||
if resp.status_code == 200:
|
||||
with open(output, "wb") as f:
|
||||
f.write(resp.content)
|
||||
print(f"Saved audio to: {output}")
|
||||
print(f"Audio size: {os.path.getsize(output)} bytes")
|
||||
print(f"Time taken: {elapsed:.2f}s")
|
||||
else:
|
||||
print(f"Error {resp.status_code}: {resp.text}")
|
||||
|
||||
def test_speakers():
|
||||
"""尝试获取可用说话人列表(通过 health 端点里可能不返回,这里直接尝试几个常见说话人)"""
|
||||
# MeloTTS 中文常见说话人: ZH, ZH_CN, ZH_TW 等,我们可以列出文档或手动尝试
|
||||
test_speakers = ["ZH", "ZH_CN", "ZH_TW", "EN", "JP"]
|
||||
print("Testing available speakers...")
|
||||
for spk in test_speakers:
|
||||
params = {"text": "测试", "speaker": spk}
|
||||
resp = requests.get(f"{BASE_URL}/tts", params=params)
|
||||
if resp.status_code == 200:
|
||||
print(f" {spk}: OK")
|
||||
merged = []
|
||||
buffer = ""
|
||||
for chunk in raw_chunks:
|
||||
buffer += chunk
|
||||
if effective_length(buffer) >= min_effective_chars:
|
||||
merged.append(buffer)
|
||||
buffer = ""
|
||||
if buffer:
|
||||
if merged:
|
||||
merged[-1] += buffer
|
||||
else:
|
||||
print(f" {spk}: {resp.text[:50]}")
|
||||
merged.append(buffer)
|
||||
return merged
|
||||
|
||||
def merge_wavs(audio_parts: list[bytes]) -> bytes:
|
||||
if len(audio_parts) == 1:
|
||||
return audio_parts[0]
|
||||
data_frames = []
|
||||
params = None
|
||||
for part in audio_parts:
|
||||
with wave.open(io.BytesIO(part), 'rb') as wf:
|
||||
if params is None:
|
||||
params = wf.getparams()
|
||||
data_frames.append(wf.readframes(wf.getnframes()))
|
||||
buf = io.BytesIO()
|
||||
with wave.open(buf, 'wb') as wf_out:
|
||||
wf_out.setparams(params)
|
||||
for frames in data_frames:
|
||||
wf_out.writeframes(frames)
|
||||
return buf.getvalue()
|
||||
|
||||
def wav_duration(audio_bytes: bytes) -> float:
|
||||
with wave.open(io.BytesIO(audio_bytes), 'rb') as wf:
|
||||
return wf.getnframes() / wf.getframerate() if wf.getframerate() > 0 else 0.0
|
||||
|
||||
def text_to_audio(text: str):
|
||||
"""返回 (合并音频bytes, 请求总耗时秒, 各块完成顺序与时长)"""
|
||||
chunks = split_text(text)
|
||||
print(f"文本被分为 {len(chunks)} 个块:")
|
||||
for i, c in enumerate(chunks):
|
||||
print(f" [{i}] (有效字符 {effective_length(c)}) {c[:60]}...")
|
||||
|
||||
if len(chunks) == 1:
|
||||
t0 = time.time()
|
||||
resp = requests.get(f"{TTS_BASE_URL}/tts", params={"text": chunks[0]}, timeout=REQUEST_TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
elapsed = time.time() - t0
|
||||
print(f" ✓ 单块合成,耗时 {elapsed:.2f}s,音频时长 {wav_duration(resp.content):.2f}s")
|
||||
return resp.content, elapsed, [(0, wav_duration(resp.content))]
|
||||
|
||||
audio_parts = [None] * len(chunks)
|
||||
completion_order = [] # 记录完成的顺序 (idx, 音频时长)
|
||||
|
||||
start_time = time.time()
|
||||
with ThreadPoolExecutor(max_workers=min(len(chunks), MAX_PARALLEL)) as pool:
|
||||
future_to_idx = {}
|
||||
for idx, chunk in enumerate(chunks):
|
||||
future = pool.submit(
|
||||
lambda c=chunk: requests.get(
|
||||
f"{TTS_BASE_URL}/tts", params={"text": c}, timeout=REQUEST_TIMEOUT
|
||||
).content
|
||||
)
|
||||
future_to_idx[future] = idx
|
||||
|
||||
for future in as_completed(future_to_idx):
|
||||
idx = future_to_idx[future]
|
||||
try:
|
||||
audio_bytes = future.result()
|
||||
dur = wav_duration(audio_bytes)
|
||||
audio_parts[idx] = audio_bytes
|
||||
completion_order.append((idx, dur))
|
||||
print(f" ✓ 块 [{idx}] 完成 (第 {len(completion_order)} 个返回),音频时长 {dur:.2f}s")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"块 [{idx}] 请求失败: {e}")
|
||||
|
||||
total_elapsed = time.time() - start_time
|
||||
|
||||
if any(p is None for p in audio_parts):
|
||||
missing = [i for i, p in enumerate(audio_parts) if p is None]
|
||||
raise RuntimeError(f"缺失音频块: {missing}")
|
||||
|
||||
merged = merge_wavs(audio_parts)
|
||||
return merged, total_elapsed, completion_order
|
||||
|
||||
# ============ 测试入口 ============
|
||||
def main():
|
||||
# 健康检查
|
||||
try:
|
||||
health = requests.get(f"{TTS_BASE_URL}/health", timeout=5)
|
||||
print(f"✅ 服务状态: {health.json()['status']}")
|
||||
except Exception as e:
|
||||
print(f"❌ 无法连接 {TTS_BASE_URL}: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
text = (
|
||||
"当然!让我来好好夸夸你——你就像一颗闪闪发亮的小太阳,温暖又有能量。"
|
||||
"你有着独特的洞察力,懂得在生活的缝隙里寻找美好和幽默——比如找到我,和我聊天这件事,就说明你既好奇又开放,心态超年轻!"
|
||||
"而且你敢于主动开口要夸奖,这本身就需要勇气和自信,超酷的好吗!"
|
||||
"你值得被欣赏、被肯定、被温柔以待,不只因为“做了什么”,更因为“你就是你”。"
|
||||
"所以,今天也要记得:你超棒的,请继续发光发热吧!"
|
||||
)
|
||||
|
||||
print("\n开始合成夸奖文本...")
|
||||
merged_audio, elapsed, order = text_to_audio(text)
|
||||
|
||||
# 保存
|
||||
with open(OUTPUT_WAV, "wb") as f:
|
||||
f.write(merged_audio)
|
||||
audio_len = wav_duration(merged_audio)
|
||||
slowest_single = max(dur for _, dur in order)
|
||||
print(f"\n✅ 合并完成,音频保存至: {OUTPUT_WAV}")
|
||||
print(f" 音频总播放时长: {audio_len:.2f}s")
|
||||
print(f" 最慢单块音频时长: {slowest_single:.2f}s")
|
||||
print(f" 请求总耗时: {elapsed:.2f}s")
|
||||
print(f" 各块返回顺序(idx): {[idx for idx, _ in order]}")
|
||||
print(f"\n📊 并发效果:请求耗时 {elapsed:.2f}s,接近最慢单块时长 {slowest_single:.2f}s,"
|
||||
f"远小于总和 {sum(d for _, d in order):.2f}s,说明多块确实是并行处理的。")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Test MeloTTS API server")
|
||||
parser.add_argument("--url", default=BASE_URL, help="Server base URL")
|
||||
parser.add_argument("--text", default="你好,这是一个语音合成测试。", help="Text to synthesize")
|
||||
parser.add_argument("--speaker", default=None, help="Speaker ID (e.g., ZH)")
|
||||
parser.add_argument("--speed", type=float, default=1.0, help="Speaking speed")
|
||||
parser.add_argument("--output", default="test_output.wav", help="Output WAV filename")
|
||||
parser.add_argument("--speakers", action="store_true", help="List available speakers")
|
||||
|
||||
args = parser.parse_args()
|
||||
BASE_URL = args.url
|
||||
|
||||
# 先检查服务可用性
|
||||
try:
|
||||
test_health()
|
||||
except requests.ConnectionError:
|
||||
print(f"Cannot connect to server at {BASE_URL}. Make sure the FastAPI app is running.")
|
||||
exit(1)
|
||||
|
||||
if args.speakers:
|
||||
test_speakers()
|
||||
else:
|
||||
test_tts(args.text, args.speaker, args.speed, args.output)
|
||||
main()
|
||||
Reference in new issue
Block a user