Files
SoundGame/web/space/gen_voice.py
T

84 lines
3.3 KiB
Python
Raw 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.
"""盲航(space)TTS 对白批量生成:教学旁白 + 游戏内提示 + 数字播报。
用法:flock /home/xsl/SoundGame/.gpu.lock .venv-tts/bin/python gen_voice.py
输出 24kHz wav 到 web/space/audio/build/voice/,随后由 ffmpeg 转 ogg/m4a。
"""
import os
os.environ.setdefault("HF_HOME", "/home/xsl/SoundGame/models/hf")
os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
import numpy as np
import soundfile as sf
import torch
from qwen_tts import Qwen3TTSModel
OUT = "/home/xsl/SoundGame/web/space/audio/build/voice"
os.makedirs(OUT, exist_ok=True)
CALM = "冷静、沉稳的飞船导航合成语音,吐字清晰"
URGENT = "急促、紧张的警报语气,音量偏大"
# (文件名, 文本, instruct)
LINES = [
("intro1", "欢迎登舰,飞行员。这里是盲航号——没有舷窗,没有仪表,你的双眼已经没用了。从现在起,只靠耳朵飞。", CALM),
("intro2", "陨石会从四个方向逼近。呼啸声在左边,就向右滑;在右边,就向左滑;上方传来尖锐呼啸,就向下滑;下方传来低沉轰隆,就向上滑。永远朝声音的反面躲。", CALM),
("intro3", "单击屏幕,引擎加速,可以冲过空隙;双击展开护盾,挡下一次撞击,之后三十秒才能再次充能。警报声越响,陨石越近。", CALM),
("intro4", "船体只能承受三次撞击。教学完毕,盲航开始。祝你好运。", CALM),
("hit1", "船体受损!还能承受两次撞击。", URGENT),
("hit2", "船体严重受损!再挨一次就完了!", URGENT),
("shield_block", "护盾挡下了撞击。", CALM),
("shield_ready", "护盾充能完毕。", CALM),
("gameover", "飞船坠毁。", "沉重、惋惜的语气"),
("restart", "单击屏幕,再次启航。", CALM),
("n_time", "存活时间", CALM),
("n_sec", "秒", CALM),
("n_dodge", "共躲过陨石", CALM),
("n_ge", "颗", CALM),
("n_milestone", "已躲过", CALM),
("num0", "零", CALM),
("num1", "一", CALM),
("num2", "二", CALM),
("num3", "三", CALM),
("num4", "四", CALM),
("num5", "五", CALM),
("num6", "六", CALM),
("num7", "七", CALM),
("num8", "八", CALM),
("num9", "九", CALM),
("num_shi", "十", CALM),
("num_bai", "百", CALM),
]
model = Qwen3TTSModel.from_pretrained(
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
device_map="cuda:0",
dtype=torch.bfloat16,
)
print("model loaded")
# 已有非空 wav 的行跳过(支持失败续跑)
todo = [(n, t, i) for n, t, i in LINES
if not (os.path.exists(f"{OUT}/{n}.wav") and os.path.getsize(f"{OUT}/{n}.wav") > 1024)]
print(f"todo: {len(todo)} / {len(LINES)}")
BATCH = 6 # 小批量,失败时损失可控
for i in range(0, len(todo), BATCH):
chunk = todo[i:i + BATCH]
wavs, sr = model.generate_custom_voice(
text=[c[1] for c in chunk],
language=["Chinese"] * len(chunk),
speaker=["Serena"] * len(chunk),
instruct=[c[2] for c in chunk],
)
for (name, _, _), wav in zip(chunk, wavs):
path = f"{OUT}/{name}.wav"
sf.write(path, wav, sr)
dur = len(wav) / sr
peak = float(np.max(np.abs(wav)))
print(f"{name}: {dur:.2f}s peak={peak:.3f}")
assert dur > 0.3, f"{name} too short ({dur:.2f}s)"
assert peak > 0.05, f"{name} too quiet (peak={peak:.3f})"
print("ALL_DONE")