Files
SoundGame Dev cc0f8c008d HTML5 重写:6 款游戏 + 用户系统 + 全局设置 + 5 轮迭代重构
- 新增 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 文档
2026-08-29 21:55:44 +08:00

73 lines
2.7 KiB
Python
Raw Permalink 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.
"""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")