- 新增 web/ 纯音频 HTML5 平台:共享引擎(手势/WebAudio 三层/设置/震动)、 Node 零依赖服务器(静态 + 注册登录 + 进度存档 API) - 游戏:剑与声(RPG 新作)、声命线、暗室(重设计)、音序回响(重设计)、 节拍行者(跟随领奏重构)、盲航(同向闪避重构);声东击西删除 - 全局功能:男声/女声双套 TTS 资产、三档语速、BGM 音量三档、 震动反馈语言(10 种震型)、服务器进度同步 - 资产:899 ogg + 899 m4a(Qwen3-TTS 双性别语音、Stable Audio 3 音效、 Mureka BGM 4+3 首),终检零缺失 - 文档:docs/design/ 13 篇设计审查文档、docs/audio/ 资产管线、 web/server/README.md API 文档
73 lines
2.7 KiB
Python
73 lines
2.7 KiB
Python
"""web/simon 语音批量生成(Qwen3-TTS CustomVoice,Serena 音色)。
|
||
|
||
生成后写入 audio/voice/wav_tmp/*.wav,由 shell 统一 ffmpeg 转 ogg/m4a。
|
||
每条断言:时长 > 0.3s(不足时尾部补静音到 0.35s)、峰值 > 0.05(统一峰值归一 0.8)。
|
||
"""
|
||
import os
|
||
import numpy as np
|
||
import soundfile as sf
|
||
import torch
|
||
from qwen_tts import Qwen3TTSModel
|
||
|
||
OUT = os.path.join(os.path.dirname(__file__), "..", "audio", "voice", "wav_tmp")
|
||
os.makedirs(OUT, exist_ok=True)
|
||
|
||
LINES = {
|
||
# 开场 + 教学
|
||
"intro": "欢迎来到音序回响。我会用六种声音演奏一段序列,请你记住顺序,再用同样的手势复现出来。先来认识这六种声音。",
|
||
"teach_up": "向上滑,是上行琶音。",
|
||
"teach_down": "向下滑,是低沉的下行音。",
|
||
"teach_left": "向左滑,声音在你的左边。",
|
||
"teach_right": "向右滑,声音在你的右边。",
|
||
"teach_tap": "单击,是短促的鼓点。",
|
||
"teach_doubletap": "双击,是两声连响。",
|
||
"teach_end": "好了,记住这些声音。序列会越来越长,看你能坚持几轮。",
|
||
# 流程提示
|
||
"listen": "听好了。",
|
||
"your_turn": "轮到你了。",
|
||
# 反馈
|
||
"correct": "答对了!",
|
||
"wrong": "错了。",
|
||
"new_record": "新纪录!",
|
||
"gameover": "游戏结束。",
|
||
"replay": "单击屏幕,再来一局。",
|
||
# 播报拼接片段
|
||
"score_pre": "你坚持了",
|
||
"best_pre": "历史最高纪录,",
|
||
"di": "第",
|
||
"lun": "轮",
|
||
"num_shi": "十",
|
||
"num_0": "零", "num_1": "一", "num_2": "二", "num_3": "三", "num_4": "四",
|
||
"num_5": "五", "num_6": "六", "num_7": "七", "num_8": "八", "num_9": "九",
|
||
}
|
||
|
||
model = Qwen3TTSModel.from_pretrained(
|
||
"Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
|
||
device_map="cuda:0",
|
||
dtype=torch.bfloat16,
|
||
)
|
||
|
||
names = list(LINES.keys())
|
||
texts = [LINES[n] for n in names]
|
||
wavs, sr = model.generate_custom_voice(
|
||
text=texts,
|
||
language=["Chinese"] * len(names),
|
||
speaker="Serena",
|
||
instruct="用清晰、沉稳而有引导感的声音,语速适中,像一个耐心的游戏主持人",
|
||
)
|
||
|
||
for name, wav in zip(names, wavs):
|
||
wav = np.asarray(wav, dtype=np.float32)
|
||
peak = np.max(np.abs(wav))
|
||
assert peak > 0.05, f"{name} 峰值过小: {peak}"
|
||
wav = wav / peak * 0.8
|
||
dur = len(wav) / sr
|
||
if dur < 0.35: # 单字片段补静音,保证可听清且满足时长断言
|
||
wav = np.concatenate([wav, np.zeros(int((0.35 - dur) * sr), dtype=np.float32)])
|
||
dur = len(wav) / sr
|
||
assert dur > 0.3, f"{name} 时长过短: {dur}"
|
||
path = os.path.join(OUT, name + ".wav")
|
||
sf.write(path, wav, sr)
|
||
print(f"{name}: {dur:.2f}s peak={np.max(np.abs(wav)):.2f}")
|
||
print("done", len(names), "clips")
|