v2收尾:手势升级 + 暂停退出 + 结局语音化 + 分支补全 + 12条新音频

gestures.js: 新增长按(1s)、两指长按(暂停)、三指点击(退出),支持多指检测
engine.js: REPLY_COUNT变量、长按重播/状态广播、暂停恢复退出机制、
  选择提示音、结局台词预录音频替换TTS、内容预警+心理热线
story.js: MSG-10/11 follow-up补全down(沉默)选项
index.html: 操作指南添加长按/安全手势、内容预警文字
batch_tts_voxcpm.py: 新增12条台词(结局旁白+系统提示+分支NPC反应)
audio/mp3/tts: 12个VoxCPM2生成的林夏声线MP3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
xsl
2026-05-19 23:22:04 +08:00
co-authored by Claude Opus 4.6
parent e4d1141b4f
commit d03bcb14e4
21 changed files with 929 additions and 326 deletions
+126 -260
View File
@@ -1,7 +1,9 @@
# 《林夏》音频素材本地制作执行文档
> **前置文档**:`林夏.md`(游戏设计)、`SOP_AI音频生产.md`(生产规范)
> **执行环境**:本机 WSL2 · RTX 5090 32GB · CosyVoice2-0.5B · FFmpeg 7.0
> **执行环境**:本机 WSL2 · RTX 5090 32GB · FFmpeg 7.0
> **TTS 主链路****VoxCPM2**(见 `林夏.md` §7.2),批量脚本 `audio/batch_tts_voxcpm.py`
> **TTS 备用**CosyVoice2-0.5B(见附录 B
> **目标**:按 SOP 规范在本地生产全部音频素材
---
@@ -10,12 +12,14 @@
| 资源 | 路径 / 状态 | 用途 |
|---|---|---|
| CosyVoice 2.0 (0.5B) | `/home/xsl/tools/CosyVoice` · conda env: `cosyvoice` | TTS 主力(替代 SOP 中 MiniMax/豆包方案 |
| **VoxCPM2** | `bash /home/xsl/AutoVideo/start-voxcpm.sh``http://127.0.0.1:8000` | **TTS 主链路**(声音克隆 + 合成 |
| CosyVoice 2.0 (0.5B) | `/home/xsl/tools/CosyVoice` · conda env: `cosyvoice` | TTS 备用(见附录 B |
| FFmpeg 7.0 | `/usr/local/bin/ffmpeg` | 音频格式转换、响度归一化、听筒效果 |
| GPU | RTX 5090 32GB | 跑 CosyVoice 推理 |
| GPU | RTX 5090 32GB | 跑 VoxCPM2 / CosyVoice 推理 |
| MuseTalk | `/home/xsl/work/MuseTalk` | **注意:这是唇形同步工具,不是 TTS** |
> **SOP 中的 MiniMax / 豆包 API 全部替换为本地 CosyVoice**。音乐(Suno)、音效(ElevenLabs SFX)、环境采样(Freesound)仍走云端。
> **SOP 中的 MiniMax / 豆包 API 全部替换为本地 VoxCPM2**。音乐(Suno)、音效(ElevenLabs SFX)、环境采样(Freesound)仍走云端。
> CosyVoice2 保留作为备用方案(见附录 B),仅在 VoxCPM2 表现不佳时切换。
---
@@ -26,51 +30,27 @@
```bash
cd /home/xsl/blind
mkdir -p audio/{00_raw/{tts,music,sfx,ambience,ringtone},01_processed/{tts,music,sfx,ambience,ringtone},02_final/{tts,music,sfx,ambience,ringtone},03_qa_reports,99_legal}
mkdir -p audio/00_raw/tts/prompt_ref # 存放角色参考音频
mkdir -p voice/{linxia,mom,azhe,xiaomei,zhounan,hr,anan,dorm_a,dorm_b,dorm_c,delivery,system} # 角色参考音频
```
### 1.2 验证 CosyVoice 环境
### 1.2 启动 VoxCPM2 服务
```bash
# 激活 conda 环境
conda activate cosyvoice
# 验证依赖
cd /home/xsl/tools/CosyVoice
python -c "
import sys
sys.path.append('third_party/Matcha-TTS')
from cosyvoice.cli.cosyvoice import CosyVoice2
model = CosyVoice2('pretrained_models/CosyVoice2-0.5B')
print('可用预训练音色:', model.list_available_spks())
print('采样率:', model.sample_rate)
print('CosyVoice2 加载成功')
"
# 启动 VoxCPM2 TTS 服务
bash /home/xsl/AutoVideo/start-voxcpm.sh
# uvicorn 服务: http://127.0.0.1:8000
```
如果报错 `ModuleNotFoundError`,先装依赖
验证服务可用
```bash
conda activate cosyvoice
cd /home/xsl/tools/CosyVoice
pip install -r requirements.txt
curl -s http://127.0.0.1:8000/docs | head -5
# 应返回 API 文档页面
```
### 1.3 启动 CosyVoice WebUI
```bash
conda activate cosyvoice
cd /home/xsl/tools/CosyVoice
python webui.py --port 8000
```
浏览器访问 `http://localhost:8000`。WebUI 提供 4 种推理模式:
| 模式 | 说明 | 本项目用途 |
|---|---|---|
| **预训练音色** | 从内置音色列表选择 | 快速试听、系统 TTS |
| **3s极速复刻** | 提供 3-30s 参考音频 + 对应文字 | **主力模式**:为每个角色准备参考音频 |
| **跨语种复刻** | 跨语言克隆 | 暂不需要 |
| **自然语言控制** | 用文字描述控制语气风格 | CosyVoice2 不支持此模式,改用 instruct2 API |
**VoxCPM2 API 接口**(详见 `林夏.md` §7.2):
- `POST /v1/voices` — 注册参考音频(wav_base64)→ 返回 voice_id
- `POST /v1/speech` — 合成语音(text + voice_id)→ 返回 WAV
- 输出格式:48kHz 16-bit PCM WAV
### 1.4 验证 FFmpeg
@@ -87,7 +67,7 @@ echo "FFmpeg loudnorm 可用"
## 2. 角色参考音频准备(关键步骤)
CosyVoice2 的"3s极速复刻"需要**每个角色一段 5-20 秒的参考音频**。参考音频的音色决定了最终 TTS 的音色
VoxCPM2 的声音克隆需要**每个角色一段参考音频**(WAV 格式)。参考音频的音色决定了最终 TTS 的音色。参考音频存放在 `voice/{角色}/` 目录下(详见 `林夏.md` §7.3 角色音色配置)
### 2.1 参考音频来源方案
@@ -101,7 +81,7 @@ CosyVoice2 的"3s极速复刻"需要**每个角色一段 5-20 秒的参考音频
| `anan` 安安 | 23 岁,可爱 | 年轻女性活泼语调截取 |
| `dorm-a/b/c` | 三个不同年轻女声 | 需要 3 段风格不同的女声参考 |
| `delivery` 外卖小哥 | 30 岁,北方腔 | 短视频/影视截取 |
| `system` 系统 | 中性 AI 女声 | 用 CosyVoice 预训练音色即可 |
| `system` 系统 | 中性 AI 女声 | VoxCPM2 注册一个中性女声参考音频 |
| `self-3y` 3年前自己 | 同 xiaomei 基底 | 复用 xiaomei 参考音频,调速度和音调 |
### 2.2 参考音频制作规格
@@ -111,229 +91,80 @@ CosyVoice2 的"3s极速复刻"需要**每个角色一段 5-20 秒的参考音频
- **内容**:完整的中文句子(需同时记录对应文字)
- **质量**:干声为主,避免底噪、混响、BGM
存放路径:`audio/00_raw/tts/prompt_ref/{角色代号}_ref.wav`
存放路径:`voice/{角色代号}/`(与 `林夏.md` §7.3 保持一致)
```bash
# 如果参考音频不是 wav 或采样率不对,用 FFmpeg 转换
ffmpeg -i input.mp3 -ar 16000 -ac 1 audio/00_raw/tts/prompt_ref/mom_ref.wav
ffmpeg -i input.mp3 -ar 16000 -ac 1 voice/mom/ref.wav
```
### 2.3 创建参考音频记录表
`audio/00_raw/tts/prompt_ref/` 下创建 `ref_manifest.txt`,记录每条参考音频对应的文字:
`voice/` 下创建 `ref_manifest.txt`,记录每条参考音频对应的文字:
```
mom_ref.wav | 今天天气不错,妈妈给你做了红烧排骨,等你回来吃饭。
azhe_ref.wav | 好的,我知道了,那件事明天再说吧。
xiaomei_ref.wav | 哎你猜怎么着,今天路上遇到一个超级搞笑的事!
zhounan_ref.wav | 嗯,其实我也不太确定,就是觉得应该跟你说一声。
hr_ref.wav | 好的,那我们下周三前把流程走完,有什么问题随时联系我。
anan_ref.wav | 天呐天呐你快来!这边超级好玩的!
dorm_a_ref.wav | 嘿你们看看我,今天升职了!请大家吃饭!
dorm_b_ref.wav | 恭喜恭喜啊,太厉害了,我就知道你行的。
dorm_c_ref.wav | 嗯,不错,对了夏夏怎么不说话啊。
delivery_ref.wav | 你好你的外卖到了,放门口了啊,麻辣烫注意别洒了。
mom/ref.wav | 今天天气不错,妈妈给你做了红烧排骨,等你回来吃饭。
azhe/ref.wav | 好的,我知道了,那件事明天再说吧。
xiaomei/ref.wav | 哎你猜怎么着,今天路上遇到一个超级搞笑的事!
zhounan/ref.wav | 嗯,其实我也不太确定,就是觉得应该跟你说一声。
hr/ref.wav | 好的,那我们下周三前把流程走完,有什么问题随时联系我。
anan/ref.wav | 天呐天呐你快来!这边超级好玩的!
dorm_a/ref.wav | 嘿你们看看我,今天升职了!请大家吃饭!
dorm_b/ref.wav | 恭喜恭喜啊,太厉害了,我就知道你行的。
dorm_c/ref.wav | 嗯,不错,对了夏夏怎么不说话啊。
delivery/ref.wav | 你好你的外卖到了,放门口了啊,麻辣烫注意别洒了。
```
---
## 3. TTS 生产流程
### 3.1 用 WebUI 逐条生产(推荐新手)
### 3.1 VoxCPM2 声音克隆流程
1. 浏览器打开 `http://localhost:8000`
2. 选择模式:**3s极速复刻**
3. 上传对应角色的参考音频
4. 在"prompt文本"中输入参考音频对应的文字
5. 在"合成文本"中输入要合成的台词
6. 调整速度(对应 SOP §4.2 各角色 Speed 值)
7. 点击"生成音频"
8. 试听满意后,右键保存音频
**前置**:确认 VoxCPM2 服务已启动(§1.2)。
### 3.2 用 Python 脚本批量生产(推荐熟练后)
**单条合成步骤**
1. 将角色参考音频注册为 voice_id:`POST /v1/voices`wav_base64
2. 调用合成接口:`POST /v1/speech`text + voice_id
3. 获得 48kHz 16-bit PCM WAV
4. 用 FFmpeg atempo 调速(按 `林夏.md` §7.3 各角色语速配置)
5. 保存到 `audio/00_raw/tts/`
创建批量生产脚本 `audio/batch_tts.py`
```python
#!/usr/bin/env python3
"""
《林夏》TTS 批量生产脚本
用法: conda activate cosyvoice && python audio/batch_tts.py
"""
import os, sys, json, torch, torchaudio
# 设置路径
COSYVOICE_DIR = "/home/xsl/tools/CosyVoice"
PROJECT_DIR = "/home/xsl/blind"
sys.path.insert(0, COSYVOICE_DIR)
sys.path.insert(0, f"{COSYVOICE_DIR}/third_party/Matcha-TTS")
from cosyvoice.cli.cosyvoice import CosyVoice2
from cosyvoice.utils.file_utils import load_wav
# 加载模型
print("正在加载 CosyVoice2 模型...")
model = CosyVoice2(f"{COSYVOICE_DIR}/pretrained_models/CosyVoice2-0.5B")
print(f"模型加载完成,采样率: {model.sample_rate}")
# ============================================================
# 角色配置 — 按 SOP §4.2 配置卡填写
# speed: 对应 SOP 中的 Speed 值
# ref_wav: 参考音频路径
# ref_text: 参考音频对应文字
# ============================================================
ROLES = {
"mom": {
"speed": 0.92,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/mom_ref.wav",
"ref_text": "今天天气不错,妈妈给你做了红烧排骨,等你回来吃饭。",
},
"azhe": {
"speed": 1.05,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/azhe_ref.wav",
"ref_text": "好的,我知道了,那件事明天再说吧。",
},
"xiaomei": {
"speed": 1.10,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/xiaomei_ref.wav",
"ref_text": "哎你猜怎么着,今天路上遇到一个超级搞笑的事!",
},
"zhounan": {
"speed": 0.95,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/zhounan_ref.wav",
"ref_text": "嗯,其实我也不太确定,就是觉得应该跟你说一声。",
},
"hr": {
"speed": 1.00,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/hr_ref.wav",
"ref_text": "好的,那我们下周三前把流程走完,有什么问题随时联系我。",
},
"anan": {
"speed": 1.15,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/anan_ref.wav",
"ref_text": "天呐天呐你快来!这边超级好玩的!",
},
"delivery": {
"speed": 1.20,
"ref_wav": f"{PROJECT_DIR}/audio/00_raw/tts/prompt_ref/delivery_ref.wav",
"ref_text": "你好你的外卖到了,放门口了啊,麻辣烫注意别洒了。",
},
# system 用预训练音色,不走 zero-shot
}
# ============================================================
# 台词表 — 从林夏.md §3 / §5 抄写
# 每条格式: (文件名前缀, 角色, 台词, speed_override=None)
# ============================================================
LINES = [
# === 第 01 条 · 21:03 妈妈电话 ===
("lv11_2103_mom_call_01", "mom",
"今晚煮了你爱吃的排骨,给你拍张照啊。下周五回来一趟,你高中物理老师的儿子,挺好的。"),
# === 第 02 条 · 21:15 小美三连 ===
("lv11_2115_xiaomei_voice_01", "xiaomei", "在吗"),
("lv11_2115_xiaomei_voice_02", "xiaomei", "你被甩了我都听说了"),
("lv11_2115_xiaomei_voice_03", "xiaomei", "明天晚上喝酒,海底捞旁边那家,不许放鸽子"),
# === 第 03 条 · 21:20 外卖小哥 ===
("lv11_2120_delivery_call_01", "delivery",
"你好你的麻辣烫到了,放门口可以吗?"),
# === 第 04 条 · 21:30 阿哲语音 ===
("lv11_2130_azhe_voice_01", "azhe",
"嗯……夏夏。我那个AirPods充电盒,好像落你抽屉里了。你方便的时候帮我寄一下吗,工位还要用。地址我等下发你。"),
# === 第 05 条 · 21:45 HR 王姐 ===
("lv11_2145_hr_voice_01", "hr",
"夏夏啊,离职流程你下周三之前办完哈,公积金那边记得跟进。辛苦啦,外面有合适的工作我帮你留意。"),
# === 第 06 条 · 22:00 安安 ===
("lv11_2200_anan_voice_01", "anan",
"夏夏!我在helens!要不要过来!我请客!"),
# === 第 10 条 · 23:00 妈妈文字 ===
("lv11_2300_mom_text_01", "mom", "睡了吗?"),
# === 第 11 条 · 23:15 阿哲电话 ===
("lv11_2315_azhe_call_01", "azhe",
"我能过去拿吗?就5分钟"),
# === 第 12 条 · 23:30 周南 ===
("lv11_2330_zhounan_voice_01", "zhounan",
"林夏,是我,周南。你应该不记得我了吧,初三坐你后桌那个"),
# === 第 14 条 · 00:15 妈妈电话 ===
("lv11_0015_mom_call_01", "mom",
"夏夏,你不接电话我有点担心。妈妈知道你工作忙,你早点睡。"),
# ... 更多台词按需添加
# 长戏(妈妈 4'17"、小美醉语音、周南 4 段)需分段处理,见 §3.3
]
def synthesize_line(filename, role, text, speed_override=None):
"""合成单条台词"""
cfg = ROLES[role]
speed = speed_override or cfg["speed"]
ref_wav = cfg["ref_wav"]
ref_text = cfg["ref_text"]
if not os.path.exists(ref_wav):
print(f" [跳过] 参考音频不存在: {ref_wav}")
return
# 加载参考音频
prompt_speech = load_wav(ref_wav, 16000)
# 裁剪到合理长度
if prompt_speech.shape[1] > 16000 * 30:
prompt_speech = prompt_speech[:, :16000 * 30]
output_path = f"{PROJECT_DIR}/audio/00_raw/tts/{filename}_v1.wav"
print(f" 合成中: {filename} [{role}] speed={speed}")
print(f" 台词: {text[:50]}...")
speeches = []
for chunk in model.inference_zero_shot(
tts_text=text,
prompt_text=ref_text,
prompt_speech_16k=prompt_speech,
stream=False,
speed=speed,
):
speeches.append(chunk["tts_speech"])
if speeches:
full_speech = torch.concat(speeches, dim=1)
torchaudio.save(output_path, full_speech, model.sample_rate)
duration = full_speech.shape[1] / model.sample_rate
print(f" 已保存: {output_path} ({duration:.1f}s)")
else:
print(f" [失败] 未生成音频: {filename}")
def main():
print(f"\n{'='*60}")
print(f" 《林夏》TTS 批量生产")
print(f"{len(LINES)} 条台词")
print(f"{'='*60}\n")
for i, line in enumerate(LINES):
filename = line[0]
role = line[1]
text = line[2]
speed_override = line[3] if len(line) > 3 else None
print(f"\n[{i+1}/{len(LINES)}] {filename}")
synthesize_line(filename, role, text, speed_override)
print(f"\n{'='*60}")
print(" 批量合成完成!请到 audio/00_raw/tts/ 检查结果")
print(f"{'='*60}")
if __name__ == "__main__":
main()
**角色音色目录**(与 `林夏.md` §7.3 一致)
```
voice/
├── linxia/ # 林夏(speed 1.00
├── mom/ # 妈妈(speed 0.92
├── azhe/ # 阿哲(speed 1.05
├── xiaomei/ # 小美(speed 1.10,醉态 0.95
├── zhounan/ # 周南(speed 0.95
├── hr/ # HR 王姐(speed 1.00
├── anan/ # 安安(speed 1.15
├── dorm_a/ # 室友 Aspeed 1.10
├── dorm_b/ # 室友 Bspeed 1.00
├── dorm_c/ # 室友 Cspeed 0.95
└── delivery/ # 外卖小哥(speed 1.20
```
### 3.2 用批量脚本生产(主力方式)
使用项目自带的 VoxCPM2 批量合成脚本:
```bash
# 确保 VoxCPM2 服务已启动
bash /home/xsl/AutoVideo/start-voxcpm.sh
# 批量合成所有台词
python audio/batch_tts_voxcpm.py --source voice
# 只合成特定角色
python audio/batch_tts_voxcpm.py --source voice --role linxia
# 干跑查看任务列表
python audio/batch_tts_voxcpm.py --source voice --dry-run
```
脚本读取 `voice/` 目录下的参考音频,按 `林夏.md` §7.3 角色配置自动注册 voice_id 并批量合成。输出到 `audio/00_raw/tts/`
### 3.3 长戏分段处理
@@ -360,11 +191,11 @@ SOP §5.3 规定:**超过 30 秒的台词必须分段录制**。
### 3.4 系统 TTS
系统提示音(播报未读消息、念选项等)用 CosyVoice **预训练音色**模式
系统提示音(播报状态、念选项等)用 VoxCPM2 注册一个中性女声参考音频
1. WebUI 选"预训练音色"
2. 从下拉菜单选一个清晰的中性女声
3. 合成所有系统语句
1. 准备一段清晰的中性女声参考音频(如新闻播报片段),存入 `voice/system/`
2. 通过 `POST /v1/voices` 注册为 system voice_id
3.`POST /v1/speech` 合成所有系统语句
系统语句示例:
- "阿哲,发来1条语音。8秒。"
@@ -663,13 +494,13 @@ done
## 9. 执行顺序总结
```
第 1 步 · 部署环境 (§1)
第 1 步 · 启动 VoxCPM2 + 部署环境 (§1)
↓ 约 30 分钟
第 2 步 · 准备参考音频 (§2) ← 这一步最关键,音色质量取决于此
第 2 步 · 准备参考音频到 voice/ (§2) ← 这一步最关键,音色质量取决于此
↓ 约 1-3 天(需要找人录音或搜集素材)
第 3 步 · 系统 TTS 先行 (§3.4) ← 最简单的先跑通流程
第 3 步 · 系统 TTS 先行 (§3.4) ← 最简单的先跑通 VoxCPM2 流程
↓ 约 1 小时
第 4 步 · 短台词批量生产 (§3.1/3.2)
第 4 步 · 短台词批量生产 (§3.2 batch_tts_voxcpm.py)
↓ 约 3-4 小时
第 5 步 · 长戏分段录制 (§3.3)
↓ 约 2-3 小时
@@ -692,22 +523,57 @@ done
## 附录 A · 常见问题
**Q: CosyVoice 生成的语音有电流声/杂音?**
**Q: VoxCPM2 生成的语音有电流声/杂音?**
A: 检查参考音频质量。参考音频必须干净无噪,采样率 ≥ 16kHz。
**Q: 语速不自然?**
A: speed 参数范围 0.5-2.0。如果调速后音质下降,改为在合成文本中加标点控制节奏(句号=长停顿,逗号=短停顿)。
A: 用 FFmpeg atempo 调速(范围 0.5-2.0。如果调速后音质下降,改为在合成文本中加标点控制节奏(句号=长停顿,逗号=短停顿)。
**Q: 怎样让同一个音色听起来"醉了"**
A: CosyVoice 没有情绪控制参数。替代方案:
A: TTS 通常没有情绪控制参数。替代方案:
1. 在文本中插入省略号、重复字来模拟口齿不清
2. 后期用 FFmpeg 轻微变速+加混响:
```bash
ffmpeg -i drunk.wav -af "atempo=0.95,aecho=0.8:0.7:40:0.3" -y drunk_fx.wav
```
**Q: WebUI 启动报 CUDA 错误**
A: 确认 conda env 正确:`conda activate cosyvoice`。确认 GPU 可用:`nvidia-smi`。
**Q: VoxCPM2 服务无法启动**
A: 确认 `bash /home/xsl/AutoVideo/start-voxcpm.sh` 正常。确认 GPU 可用:`nvidia-smi`。
**Q: 生成很慢?**
A: RTX 5090 应该很快(远超实时)。如果慢,检查是否误用了 CPU。在 Python 中加 `print(torch.cuda.is_available())` 确认。
A: RTX 5090 应该很快(远超实时)。如果慢,检查是否误用了 CPU。
---
## 附录 B · CosyVoice2 备用方案
> **仅在 VoxCPM2 表现不佳或不可用时使用。** 正常生产流程请走 §1-§3 的 VoxCPM2 主链路。
### B.1 环境验证
```bash
conda activate cosyvoice
cd /home/xsl/tools/CosyVoice
python -c "
import sys
sys.path.append('third_party/Matcha-TTS')
from cosyvoice.cli.cosyvoice import CosyVoice2
model = CosyVoice2('pretrained_models/CosyVoice2-0.5B')
print('CosyVoice2 加载成功,采样率:', model.sample_rate)
"
```
### B.2 WebUI 启动
```bash
conda activate cosyvoice
cd /home/xsl/tools/CosyVoice
python webui.py --port 8001 # 注意:用 8001 避免与 VoxCPM2 的 8000 端口冲突
```
模式选择:**3s极速复刻**(上传参考音频 + 对应文字 → 合成)。
### B.3 批量脚本
CosyVoice2 版批量脚本为 `audio/batch_tts.py`(区别于主力脚本 `audio/batch_tts_voxcpm.py`)。
用法:`conda activate cosyvoice && python audio/batch_tts.py`