Files
blind/audio/gen_tts_missing.py
2026-05-18 18:39:49 +08:00

159 lines
6.2 KiB
Python

#!/usr/bin/env python3
"""
生成缺失的 TTS 资产:
- self-3y 三个版本(明朗/迷茫/温柔)
- system 系统提示音 TTS
使用 VoxCPM2 API (http://127.0.0.1:8000)
"""
import os, sys, time, glob, base64, subprocess, tempfile, shutil
import requests
PROJECT_DIR = "/home/xsl/blind"
OUT_DIR = f"{PROJECT_DIR}/audio/00_raw/tts"
VOICE_DIR = f"{PROJECT_DIR}/voice"
BASE_URL = "http://127.0.0.1:8000"
os.makedirs(OUT_DIR, exist_ok=True)
# ── 台词定义 ──────────────────────────────────────────────────
MISSING_LINES = [
# self-3y 明朗版(23岁刚来北京,对一切新鲜)
("lv11_0030_self3y_voice_01", # filename(不含后缀)
"xiaomei", # 音色来源目录(同xiaomei基底+调整)
0.95, # speed(比正常略快,体现23岁轻盈)
"哦对,今天是我来北京的第一百天。妈,我没跟你说,我自己数的。"
"这个城市好大。我走了很多路,见了很多人。我觉得我还没准备好,"
"但我又觉得好像准备了很久很久了。你们不用担心我。我很好。"
"以后的我,如果你听到这条,记得要好好的。"),
# self-3y 迷茫版(其实已经很累了,只是没说)
("lv11_0030_self3y_voice_02",
"xiaomei",
1.00,
"我今天又没睡好。不是失眠,就是睡不踏实。"
"北京这个地方,它会让你觉得你随时都要更好,随时都要更努力。"
"我在努力。但我不知道在努力什么。"
"我就想留下一条记录。今天,今天是……算了,就今天吧。"),
# self-3y 温柔版(对未来的自己说撑住)
("lv11_0030_self3y_voice_03",
"xiaomei",
1.00,
"是我。23岁的我。我设了三年后打开。"
"我不知道那时候的你怎么样了。如果你很好,就当我多虑了。"
"如果你不好——你看,你熬过来了对不对。"
"你23岁的时候,也觉得很难。但你还是过来了。"
"所以……撑住啊。"),
# system TTS — 游戏系统提示
("lv11_sys_battery_01", "xiaomei", 1.00, "电量百分之一"),
("lv11_sys_battery_02", "xiaomei", 1.00, "电量不足,请及时充电"),
("lv11_sys_unread_01", "xiaomei", 1.00, "您有一条未读消息"),
("lv11_sys_unread_multi", "xiaomei", 1.00, "您有多条未读消息"),
("lv11_sys_memo_01", "xiaomei", 1.00, "三年前今天,一条未听的录音"),
("lv11_sys_goodnight", "xiaomei", 1.00, "晚安"),
]
# ── 工具函数 ──────────────────────────────────────────────────
def find_ref(voice_dir_name):
d = os.path.join(VOICE_DIR, voice_dir_name)
for ext in ("*.wav", "*.mp3", "*.flac"):
files = sorted(glob.glob(os.path.join(d, ext)))
if files:
return files[0]
return None
def to_wav(src, dst):
subprocess.run(
["ffmpeg", "-y", "-i", src, "-ar", "16000", "-ac", "1", "-acodec", "pcm_s16le", dst],
check=True, capture_output=True
)
def apply_speed(src, speed, dst):
if abs(speed - 1.0) < 0.01:
shutil.copy2(src, dst)
return
subprocess.run(
["ffmpeg", "-y", "-i", src, "-filter:a", f"atempo={speed}", "-acodec", "pcm_s16le", dst],
check=True, capture_output=True
)
def register_voice(wav_path):
with open(wav_path, "rb") as f:
b64 = base64.b64encode(f.read()).decode()
r = requests.post(f"{BASE_URL}/v1/voices", json={"wav_base64": b64}, timeout=60)
r.raise_for_status()
return r.json()["voice_id"]
def synthesize(voice_id, text):
r = requests.post(f"{BASE_URL}/v1/speech",
json={"text": text, "voice_id": voice_id, "cfg_value": 2.0, "inference_timesteps": 10},
timeout=180)
r.raise_for_status()
return r.content
# ── 主流程 ────────────────────────────────────────────────────
def main():
# 健康检查
try:
requests.get(f"{BASE_URL}/health", timeout=5).raise_for_status()
print(f"VoxCPM 服务就绪: {BASE_URL}")
except Exception as e:
print(f"[错误] VoxCPM 服务不可用: {e}")
sys.exit(1)
tmp_dir = tempfile.mkdtemp(prefix="tts_missing_")
voice_cache = {} # dir_name → voice_id
print(f"\n{len(MISSING_LINES)} 条待合成\n{'='*50}")
ok, skip, fail = 0, 0, 0
for filename, voice_dir, speed, text in MISSING_LINES:
out_path = os.path.join(OUT_DIR, f"{filename}_wav_v1.wav")
if os.path.exists(out_path):
print(f" [跳过已存在] {filename}")
skip += 1
continue
# 注册音色(缓存)
if voice_dir not in voice_cache:
ref = find_ref(voice_dir)
if not ref:
print(f" [✗无参考] {voice_dir}: {filename}")
fail += 1
continue
wav_ref = os.path.join(tmp_dir, f"{voice_dir}_ref.wav")
if not ref.endswith(".wav"):
to_wav(ref, wav_ref)
else:
shutil.copy2(ref, wav_ref)
print(f" 注册音色 [{voice_dir}] ...", end=" ", flush=True)
vid = register_voice(wav_ref)
voice_cache[voice_dir] = vid
print(f"ok ({vid[:8]}…)")
print(f" 合成: {filename} ...", end=" ", flush=True)
t0 = time.time()
try:
raw_bytes = synthesize(voice_cache[voice_dir], text)
tmp_raw = os.path.join(tmp_dir, f"{filename}.raw.wav")
with open(tmp_raw, "wb") as f:
f.write(raw_bytes)
apply_speed(tmp_raw, speed, out_path)
print(f"✓ {time.time()-t0:.1f}s → {os.path.basename(out_path)}")
ok += 1
except Exception as e:
print(f"✗ {e}")
fail += 1
shutil.rmtree(tmp_dir, ignore_errors=True)
print(f"\n{'='*50}")
print(f"完成 {ok} 跳过 {skip} 失败 {fail}")
print(f"输出: {OUT_DIR}")
if __name__ == "__main__":
main()