"""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")