项目整理:重写主文档、归档过时文档、清理死代码、新增音频制作手册

文档整理:
- 重写 game/操作说明.md 为项目主文档(6游戏/6手势/旁白/模型生成音频/TTS两步流程)
- 归档过时文档至 other_docs/(林夏原始设计、旧执行文档)
- 新增 docs/音频制作执行手册.md(从零制作游戏音频的完整流程)
- docs/ 下保留旁白音色提示词、音频脚本

代码清理:
- 删除旧架构死代码: story.js / gestures.js / profile.js
- 删除耦合旧架构的过时测试目录 game/tests/

脚本配置修正:
- check_tts_env.sh: 启动脚本路径 AutoVideo → tts-server(实际位置)
- gen_narrator_samples.py: 硬编码地址改为环境变量,默认对齐 tts-server 端口 8000
This commit is contained in:
xsl
2026-06-19 22:29:04 +08:00
parent b3384499e3
commit 866f363591
46 changed files with 5061 additions and 2374 deletions
@@ -0,0 +1,593 @@
# 《林夏》音频素材本地制作执行文档
> **前置文档**:`林夏.md`(游戏设计)、`SOP_AI音频生产.md`(生产规范)
> **现行状态**`audio_2/项目状态说明.md`(v5 音色、目录、命令,**以此为准**)
> **执行环境**:本机 WSL2 · RTX 5090 32GB · FFmpeg 7.0
> **TTS 主链路****VoxCPM2**Voice Design 试听 → 克隆 → Style Control 表演),脚本 `audio_2/scripts/04_gen_tts_clone.py`
> **TTS 备用**CosyVoice2-0.5B(见附录 B
> **目标**:按 SOP 规范在本地生产全部音频素材
---
## 0. 本机资源清单
| 资源 | 路径 / 状态 | 用途 |
|---|---|---|
| **VoxCPM2** | `/home/xsl/tts-server/` · 端口 **8002** · `bash /home/xsl/tts-server/start-voxcpm.sh` | **TTS 主链路**Voice Design + 声音克隆 + Style Control |
| CosyVoice 2.0 (0.5B) | `/home/xsl/tools/CosyVoice` · conda env: `cosyvoice` | TTS 备用(见附录 B |
| ACE-Step 1.5 | `/home/xsl/tools/ACE-Step-1.5` · conda env: `acestep` | BGM / 铃声 / 叙事音效 / 环境音 |
| Stable Audio Open 1.0 | `/home/xsl/tools/stable-audio-tools` · conda env: `stableaudio` | 高品质音效补充 |
| FFmpeg 7.0 | `/usr/local/bin/ffmpeg` | 音频格式转换、响度归一化、听筒效果 |
| GPU | RTX 5090 32GB | 跑 VoxCPM2 / CosyVoice / ACE-Step 推理 |
| MuseTalk | `/home/xsl/work/MuseTalk` | **注意:这是唇形同步工具,不是 TTS** |
> **端口说明**8000 被 head3d 占用,TTS 固定使用 **8002**`VOXCPM_PORT=8002`)。
> **游戏运行时音频**`audio/mp3/`184 文件,仅保留 `story.js` / `engine.js` 引用的 `*_wav_v1.mp3` 等)。
> CosyVoice2 保留作为备用,仅在 VoxCPM2 表现不佳时切换。
---
## 1. 环境部署
### 1.1 创建项目目录结构
```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 voice/{linxia,mom,azhe,xiaomei,zhounan,hr,anan,dorm_a,dorm_b,dorm_c,delivery,system} # 各角色参考音频
```
### 1.2 启动 VoxCPM2 服务
```bash
VOXCPM_PORT=8002 VOXCPM_ENABLE_DENOISER=0 bash /home/xsl/tts-server/start-voxcpm.sh
# 服务: http://127.0.0.1:8002
```
验证服务可用:
```bash
curl -s http://127.0.0.1:8002/health
# 预期: {"status":"ok","model":"VoxCPM2","styled_speech":true}
```
**VoxCPM2 API 接口**(详见 `林夏.md` §7.2、`/home/xsl/tts-server/server.py`):
| 接口 | 用途 |
|------|------|
| `POST /v1/voices` | 注册参考音频(wav_base64)→ voice_id |
| `POST /v1/speech` | 纯克隆合成(text + voice_id)→ WAV |
| `POST /v1/speech/styled` | Style Control / Voice Design(见下) |
-`voice_id` → 克隆音色 + style 表演修饰
-`voice_id` → 纯 Voice Design(仅提示词,无参考音频)
- 输出格式:48kHz 16-bit PCM WAV
### 1.4 验证 FFmpeg
```bash
ffmpeg -version | head -1
# 预期输出: ffmpeg version 7.0.2-static
# 测试 loudnorm 滤镜
ffmpeg -f lavfi -i "sine=frequency=440:duration=1" -af "loudnorm=I=-16:TP=-1:LRA=11" -y /tmp/test_loudnorm.wav
echo "FFmpeg loudnorm 可用"
```
---
## 2. 角色参考音频准备(关键步骤)
VoxCPM2 的声音克隆需要**每个角色一段参考音频**(WAV)。**现行方案**:先用 Voice Design 生成试听变体,人工选定后作为永久参考。
### 2.1 现行流程(v4 → v5
```
1. 03_gen_voice_samples.py → 每角色 5 条约 10s 试听(不同提示词)
2. 人工试听,每角色保留 1 条 → audio_2/tts/v4_voice_samples/{role}/
3. 04_gen_tts_clone.py → 全量克隆 + 部署 audio/mp3/tts/*_wav_v1.mp3
4. 副本同步到 voice/{角色}/{角色}_ref.wav
```
**已选定参考**2026-05-23):
| 角色 | 文件 | 性别 |
|------|------|------|
| linxia | `linxia_v05.wav` | 女 |
| mom | `mom_v01.wav` | 女 |
| azhe | `azhe_v03.wav` | 男 |
| xiaomei | `xiaomei_v05.wav` | 女 |
| xiaomei_drunk | `xiaomei_drunk_v05.wav` | 女 |
| zhounan | `zhounan_v05.wav` | 男 |
| hr | `hr_v05.wav` | 女 |
| anan | `anan_v04.wav` | 女 |
| dorm_a/b/c | `v03` / `v02` / `v03` | 女 |
| delivery | `delivery_v04.wav` | **男** |
| self_3y | `self_3y_v03.wav` | 女 |
提示词配置:`audio_2/voice_sample_prompts.py`(试听)、`audio_2/voice_style_prompts.py`(表演修饰)。
### 2.2 参考音频规格(仍适用)
- **时长**:5-20 秒(推荐 10 秒左右)
- **格式**WAV16kHz mono(注册前由脚本自动转换)
- **内容**:完整中文句子
- **质量**:干声为主,避免底噪、混响、BGM
### 2.3 旧方案(人工录音 / 截取)
若 Voice Design 不满意,仍可按原方案准备参考音频放入 `voice/{角色}/`
| 角色 | 音色要求 | 参考音频获取方案 |
|---|---|---|
| `mom` 妈妈 | 50 岁,温柔克制 | 真人录音或影视截取 |
| `azhe` 阿哲 | 27 岁男声,理性偏冷 | 播客/有声书截取 |
| `delivery` 外卖小哥 | 30 岁北方男声 | 语速适中,非过快 |
| … | 见 `林夏.md` §7.3 | — |
---
## 3. TTS 生产流程
### 3.0 现行主力:v5 克隆流水线 ★
**前置**VoxCPM2 已启动(§1.2),参考音频已在 `v4_voice_samples/` 每角色 1 条。
```bash
cd /home/xsl/blind
# 全量克隆 + 自动部署游戏目录
VOXCPM_PORT=8002 python audio_2/scripts/04_gen_tts_clone.py --force
# 只重跑某角色
VOXCPM_PORT=8002 python audio_2/scripts/04_gen_tts_clone.py --role delivery --force
# 预览任务
python audio_2/scripts/04_gen_tts_clone.py --dry-run
```
| 步骤 | 脚本 | 输出 |
|------|------|------|
| 试听选音色 | `03_gen_voice_samples.py` | `audio_2/tts/v4_voice_samples/` |
| 全量克隆 | `04_gen_tts_clone.py` | `audio_2/tts/v5_cloned/*_wav_v5.mp3` |
| 游戏部署 | (04 脚本自动) | `audio/mp3/tts/*_wav_v1.mp3` |
- 台词来源:`audio/batch_tts_voxcpm.py``LINES`173 条)+ `EXTRA_LINES`self-3y ×3、系统 memo
- 表演修饰:`audio_2/voice_style_prompts.py``get_clone_style()`
- 清单:`audio_2/tts/v5_cloned/clone_manifest.tsv`
**游戏仅加载 `*_wav_v1.mp3`**,不加载 `*_mp3_v1.mp3`
### 3.1 VoxCPM2 声音克隆流程(底层原理)
**单条合成步骤**
1. 将角色参考音频注册为 voice_id:`POST /v1/voices`
2. 调用:`POST /v1/speech/styled`text + voice_id + style 表演修饰)
3. 获得 48kHz WAV → ffmpeg atempo 调速(`ROLES` 配置)→ MP3
4. 部署到 `audio/mp3/tts/`
**角色语速**`audio/batch_tts_voxcpm.py``ROLES`,与 `林夏.md` §7.3 一致):
```
voice/
├── linxia/ # 1.00
├── mom/ # 0.92
├── azhe/ # 1.05
├── xiaomei/ # 1.10(醉态 xiaomei_drunk 0.95
├── zhounan/ # 0.95
├── hr/ # 1.00
├── anan/ # 1.15
├── dorm_a/b/c # 1.10 / 1.00 / 0.95
├── delivery/ # 1.00(已从 1.20 调慢)
└── self_3y/ # 1.15(独立参考 self_3y_v03.wav
```
### 3.2 旧版批量脚本(参考,已被 v5 替代)
```bash
# 旧流程:直接读 voice/ 参考,无 Style Control 表演
python audio/batch_tts_voxcpm.py --source voice --dry-run
```
输出原为 `audio/00_raw/tts/`,现正式产物见 `audio_2/tts/v5_cloned/``audio/mp3/tts/`
### 3.3 长戏分段处理
SOP §5.3 规定:**超过 30 秒的台词必须分段录制**。
**妈妈 4'17" 长语音(第 16 条)分 8 段**
| 段号 | 文件名 | 台词 | 情绪提示 |
|---|---|---|---|
| 1 | `lv11_0200_mom_voice_01_seg1` | "夏夏。妈妈知道你今天电话不接。妈妈不催你,就是想跟你说一件事。" | gentle, hesitant |
| 2 | `lv11_0200_mom_voice_01_seg2` | "你24了对吧。妈妈24那年——你应该没听我说过——" | gentle, nostalgic |
| 3 | `lv11_0200_mom_voice_01_seg3` | "那时候妈妈在广州,给一个香港老板做秘书,1995年。那年我也被分手过。也被开除过。一模一样的春天。" | gentle, sad but holding back |
| 4 | `lv11_0200_mom_voice_01_seg4` | "那年我哭得特别难看。后来呢?后来就没什么后来了。我回了老家,认识了你爸,生了你。" | gentle, releasing |
| 5 | `lv11_0200_mom_voice_01_seg5` | "我跟你说这个不是想跟你说什么都会过去——我想跟你说,那年夏天我没死,是因为我外婆每天晚上都给我打一个电话。" | gentle, emotional |
| 6 | `lv11_0200_mom_voice_01_seg6` | "她不说什么,就问我今天吃饭了没。" | gentle, peaceful |
| 7 | `lv11_0200_mom_voice_01_seg7` | "我现在打给你,你不接也没事。妈妈给你发个语音,你什么时候想听就听。你不用回我。" | gentle, tender |
| 8 | `lv11_0200_mom_voice_01_seg8` | "你今天接了好多电话吧。妈妈不打扰你了。晚安。" | gentle, almost smiling |
每段单独合成后用 FFmpeg 拼接(见 §4.3)。
**小美醉语音(第 09 条)分 4 段**,speed 调低到 0.95,每段间加长停顿。
**周南 4 段长讲述(第 13 条)** 每段独立合成,段间留 800ms 静音。
### 3.4 系统 TTS
系统提示音(内容警告、暂停、热线、等待回复、memo 等)**现行方案**:
- 大部分系统语音用 **林夏音色**`linxia_ref.wav`)克隆,见 `LINES``v2_sys_*``lv11_sys_memo_01`
- 合成走 v5 流水线,与对白同一套参考
### 3.5 self-3y 三版预录
**现行**:独立参考 `self_3y_v03.wav`(非复用小美),角色 key 为 `self_3y`speed 1.15。
| 版本 | 文件名 | 情绪 |
|---|---|---|
| 明朗版 | `lv11_0030_self3y_voice_01` | 兴奋带紧张 |
| 迷茫版 | `lv11_0030_self3y_voice_02` | 疲惫假装坚强 |
| 温柔版 | `lv11_0030_self3y_voice_03` | 平静有力 |
台词见 `audio_2/scripts/04_gen_tts_clone.py``EXTRA_LINES`
---
## 4. 后期处理(FFmpeg
### 4.1 单条 TTS 标准处理链
```bash
# 变量
INPUT="audio/00_raw/tts/lv11_2103_mom_call_01_v1.wav"
OUTPUT="audio/01_processed/tts/lv11_2103_mom_call_01_v1.wav"
# 完整处理链(一条命令):
# [1] 高通 80Hz 去低频噪
# [2] 压缩器 4:1
# [3] 响度归一化到 -16 LUFS
# [4] 限制峰值 -1 dBTP
ffmpeg -i "$INPUT" \
-af "highpass=f=80,\
acompressor=threshold=-18dB:ratio=4:attack=20:release=250,\
loudnorm=I=-16:TP=-1:LRA=11,\
alimiter=limit=0.891" \
-ar 48000 -sample_fmt s16 -ac 1 \
-y "$OUTPUT"
```
### 4.2 加听筒效果(电话 / 微信语音形态)
```bash
INPUT="audio/01_processed/tts/lv11_2103_mom_call_01_v1.wav"
OUTPUT="audio/01_processed/tts/lv11_2103_mom_call_01_v1_phone.wav"
# 模拟手机听筒: HPF 300Hz + LPF 3.4kHz + 轻微饱和
ffmpeg -i "$INPUT" \
-af "highpass=f=300:poles=2,\
lowpass=f=3400:poles=2,\
equalizer=f=1500:t=q:w=1:g=1,\
acompressor=threshold=-20dB:ratio=3:attack=5:release=50" \
-y "$OUTPUT"
```
> **例外**`mom` 第 16 条 4'17" 长语音**不加听筒效果**——SOP 设计意图是"完整音质版"。
### 4.3 长戏分段拼接
```bash
# 1. 先给每段加 200ms 静音头尾
for seg in audio/00_raw/tts/lv11_0200_mom_voice_01_seg*.wav; do
ffmpeg -i "$seg" \
-af "adelay=200|200,apad=pad_dur=0.2" \
-y "${seg%.wav}_padded.wav"
done
# 2. 生成拼接文件列表
ls audio/00_raw/tts/lv11_0200_mom_voice_01_seg*_padded.wav | \
sed "s/^/file '/" | sed "s/$/'/" > /tmp/concat_list.txt
# 3. 拼接 + crossfade
ffmpeg -f concat -safe 0 -i /tmp/concat_list.txt \
-af "acrossfade=d=0.05:c1=tri:c2=tri" \
-y audio/00_raw/tts/lv11_0200_mom_voice_01_full.wav
# 注意:FFmpeg concat + acrossfade 只支持两个输入。
# 多段拼接需要逐段 crossfade,或直接 concat 后人工检查接缝。
# 推荐方案:先 concat 拼接,段间 200ms 静音已经足够自然。
ffmpeg -f concat -safe 0 -i /tmp/concat_list.txt \
-c copy \
-y audio/00_raw/tts/lv11_0200_mom_voice_01_full.wav
```
### 4.4 批量后期处理脚本
```bash
#!/bin/bash
# 批量处理 00_raw/tts/ → 01_processed/tts/
# 用法: bash audio/process_tts.sh
for f in audio/00_raw/tts/lv11_*.wav; do
basename=$(basename "$f")
output="audio/01_processed/tts/$basename"
# 跳过已处理的
[ -f "$output" ] && echo "跳过: $basename" && continue
echo "处理: $basename"
ffmpeg -i "$f" \
-af "highpass=f=80,\
acompressor=threshold=-18dB:ratio=4:attack=20:release=250,\
loudnorm=I=-16:TP=-1:LRA=11" \
-ar 48000 -sample_fmt s16 -ac 1 \
-y "$output" 2>/dev/null
echo "$output"
done
echo "TTS 后期处理完成"
```
### 4.5 最终打包(wav → ogg
```bash
#!/bin/bash
# 01_processed → 02_final (ogg Vorbis q=5)
for f in audio/01_processed/tts/*.wav; do
basename=$(basename "${f%.wav}.ogg")
output="audio/02_final/tts/$basename"
ffmpeg -i "$f" -c:a libvorbis -q:a 5 -y "$output" 2>/dev/null
done
echo "OGG 打包完成"
```
### 4.6 角色专属后期(按 SOP §4.2 配置卡)
```bash
# mom: 高频衰减 -3dB + 轻 reverb
ffmpeg -i input.wav -af "equalizer=f=5000:t=h:w=2000:g=-3" -y output.wav
# azhe: 中频衰减 -2dB"闷"感)
ffmpeg -i input.wav -af "equalizer=f=2000:t=q:w=1:g=-2" -y output.wav
# delivery: 低频衰减 -6dB(模拟听筒)
ffmpeg -i input.wav -af "equalizer=f=200:t=h:w=200:g=-6" -y output.wav
# self-3y: 高频衰减 -2dB + 底噪(录音笔质感)
ffmpeg -i input.wav \
-af "equalizer=f=5000:t=h:w=2000:g=-2" \
-y output.wav
# 底噪可以后续叠加一层轻微白噪声
```
---
## 5. 音乐素材(ACE-Step 本地 + Suno 备选)
**现行产出**:BGM 4 轨 + 铃声 5 条,均由 **ACE-Step 1.5** 本地生成,已部署至 `audio/mp3/music/`
Suno 仍可作为备选(按 SOP §6 操作):
### 5.1 操作步骤
1. 登录 Suno(需要 Pro 订阅)
2. 选 Custom Mode
3. 按 SOP §6.2 的 4 段曲 Prompt 生成
4. 每段跑 2 轮(出 4 首候选),选最佳
5. 用 Extend 延长到目标时长
6. 下载 WAV 格式到 `audio/00_raw/music/`
### 5.2 铃声生成
按 SOP §6.3 的 9 个角色铃声 Prompt 在 Suno 生成。
下载到 `audio/00_raw/ringtone/`
### 5.3 音乐后期处理
```bash
# 音乐处理: 削 AI 浊感 + 响度 -18 LUFS
for f in audio/00_raw/music/*.wav; do
basename=$(basename "$f")
ffmpeg -i "$f" \
-af "equalizer=f=350:t=q:w=1.5:g=-2,\
loudnorm=I=-18:TP=-1:LRA=11" \
-ar 48000 \
-y "audio/01_processed/music/$basename" 2>/dev/null
done
```
---
## 6. 音效素材(ACE-Step 本地 + ElevenLabs 备选)
**现行产出**:叙事音效 4 条(敲门、心跳、呼吸、水壶)在 `audio/mp3/sfx/`,由 ACE-Step 生成。
ElevenLabs SFX 仍可作为备选:
### 6.1 操作步骤
1. 登录 ElevenLabsCreator 订阅,含 Sound Effects
2. 按 SOP §7.2 的 Prompt 逐条生成
3. **每条跑 3 次**,挑最佳
4. 下载后转 WAV
```bash
# ElevenLabs 默认 mp3,转为 wav
for f in audio/00_raw/sfx/*.mp3; do
ffmpeg -i "$f" -ar 48000 -y "${f%.mp3}.wav" && rm "$f"
done
```
### 6.2 音效后期
```bash
# 短音效响度 -14 LUFS
for f in audio/00_raw/sfx/*.wav; do
basename=$(basename "$f")
ffmpeg -i "$f" \
-af "loudnorm=I=-14:TP=-1:LRA=11" \
-ar 48000 \
-y "audio/01_processed/sfx/$basename" 2>/dev/null
done
```
---
## 7. 环境底噪(Freesound
### 7.1 操作步骤
1. 按 SOP §8.1 的清单在 Freesound 搜索
2. 过滤:CC0 优先,≥ 44.1kHz,下载 WAV
3. 文件名加 Freesound ID:如 `amb_apartment_night_fs123456.wav`
4. **立刻登记到版权表**
### 7.2 环境底噪后期
```bash
# 环境底噪处理: HPF 60Hz + LPF 12kHz + 响度 -22 LUFS
for f in audio/00_raw/ambience/*.wav; do
basename=$(basename "$f")
ffmpeg -i "$f" \
-af "highpass=f=60,lowpass=f=12000,\
loudnorm=I=-22:TP=-3:LRA=11" \
-ar 48000 -ac 2 \
-y "audio/01_processed/ambience/$basename" 2>/dev/null
done
```
### 7.3 单声道转假性立体声
```bash
# 如果下载到的是单声道,做假性立体声
INPUT="audio/01_processed/ambience/amb_apartment_night.wav"
OUTPUT="audio/01_processed/ambience/amb_apartment_night_stereo.wav"
ffmpeg -i "$INPUT" \
-filter_complex "\
[0:a]asplit=2[L][R];\
[R]adelay=12|12,equalizer=f=8000:t=q:w=1:g=1[Rd];\
[L][Rd]amerge=inputs=2[out]" \
-map "[out]" -ac 2 \
-y "$OUTPUT"
```
---
## 8. QA 验收
### 8.1 响度检查脚本
```bash
#!/bin/bash
# 检查所有已处理音频的响度
echo "=== TTS 响度检查 (目标 -16 LUFS) ==="
for f in audio/01_processed/tts/*.wav; do
result=$(ffmpeg -i "$f" -af "loudnorm=print_format=json" -f null - 2>&1 | \
grep -A1 '"input_i"' | tail -1 | tr -d ' ",' | cut -d: -f2)
echo " $(basename "$f"): ${result} LUFS"
done
echo ""
echo "=== 音乐响度检查 (目标 -18 LUFS) ==="
for f in audio/01_processed/music/*.wav; do
result=$(ffmpeg -i "$f" -af "loudnorm=print_format=json" -f null - 2>&1 | \
grep -A1 '"input_i"' | tail -1 | tr -d ' ",' | cut -d: -f2)
echo " $(basename "$f"): ${result} LUFS"
done
echo ""
echo "=== 环境底噪响度检查 (目标 -22 LUFS) ==="
for f in audio/01_processed/ambience/*.wav; do
result=$(ffmpeg -i "$f" -af "loudnorm=print_format=json" -f null - 2>&1 | \
grep -A1 '"input_i"' | tail -1 | tr -d ' ",' | cut -d: -f2)
echo " $(basename "$f"): ${result} LUFS"
done
```
### 8.2 验收流程
1. **自检**:跑响度检查脚本,确认所有文件在目标范围 ±1 LUFS
2. **单条听感**:用耳机逐条听,标记不满意的(记录到 `03_qa_reports/`
3. **上下文听感**:把同一时间段的所有音频按时间轴拼起来完整听一遍
4. **不满意的重做**:回到 §3 重新合成,调参或换参考音频
---
## 9. 执行顺序总结(现行状态 2026-05-23
```
✅ 第 1 步 · 部署 VoxCPM2/home/xsl/tts-server,端口 8002
✅ 第 2 步 · Voice Design 试听 → 选定 13 角色参考 wav(v4)
✅ 第 3 步 · v5 全量克隆 + 部署 audio/mp3/tts/04_gen_tts_clone.py
✅ 第 4 步 · BGM / 铃声 / 音效 / 环境音(ACE-Step → audio/mp3/
✅ 第 5 步 · 清理未引用音频(184 文件与游戏对齐)
⬜ 第 6 步 · Playtest 听感迭代 + QA(§8
```
**重跑 TTS**(改参考或表演修饰后):
```bash
VOXCPM_PORT=8002 python audio_2/scripts/04_gen_tts_clone.py --force
```
详细目录与命令见 `audio_2/项目状态说明.md`。
---
## 附录 A · 常见问题
**Q: VoxCPM2 生成的语音有电流声/杂音?**
A: 检查参考音频质量。参考音频必须干净无噪,采样率 ≥ 16kHz。
**Q: 语速不自然?**
A: 用 FFmpeg atempo 调速(范围 0.5-2.0)。如果调速后音质下降,改为在合成文本中加标点控制节奏(句号=长停顿,逗号=短停顿)。
**Q: 怎样让同一个音色听起来"醉了"**
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: VoxCPM2 服务无法启动?**
A: 确认 `bash /home/xsl/AutoVideo/start-voxcpm.sh` 正常。确认 GPU 可用:`nvidia-smi`。
**Q: 生成很慢?**
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`
+942
View File
@@ -0,0 +1,942 @@
# 林夏
> **类型**:情感 / 叙事 / 轻悬疑 · 纯音频交互游戏
> **目标用户**:20–25 岁年轻女性(大学生 / 初入职场 / 一线城市独居)
> **时长**:约 3045 分钟
> **核心隐喻**:所有人都给你发语音,没有人发文字。今晚你必须一条一条听完,才能决定明天的自己长什么样。
> **核心原则**:每次选择都必须产生玩家能听到的、不同的后果。
---
## 0. 一句话概述
24 岁的林夏,被分手、被裁员、被妈妈催婚,全发生在同一个周五。她把手机调成静音、躲进合租屋,关上灯。屏幕坏了——只剩耳机能听、手指能摸。今晚会有 16 条电话和语音陆续到达。她接谁、不接谁、怎么回——每一个回应都会被对方听到,每一段对话都会走向不同的结局。
---
## 1. 玩家与角色
### 1.1 玩家是谁
- **林夏**,24 岁,西部小城来的姑娘,北漂第 3 年。
- 国贸某外企新媒体专员,今天上午刚被裁员(合同到期不续签)。
- 与男友"阿哲"恋爱 2 年 4 个月,今天下午刚被分手("我们性格真的不合")。
- 与一位大学闺蜜"小美"合租,但小美今晚去了酒吧。
- 老家妈妈让她"五一回来相亲"。
- 微信未读:99+。
- 屏幕:今天上午地铁挤碎了的那块屏,现在显示一片黑——但触控、震动、声音都还在。
### 1.2 林夏不是"听者"——她会说话
与传统音频叙事游戏不同,**林夏有自己的声音**。玩家的每次选择都会触发林夏的一句语音回应,对方会听到并做出反应。这构成了完整的对话:
```
NPC 说话 → 玩家选择方向 → 林夏说出回应 → NPC 对林夏的回应做出反应
```
林夏的声音设计:**疲惫但清醒、克制但有温度、年轻但不幼稚。**
---
## 2. 核心玩法
### 2.1 七种手势——零视觉依赖
| 手势 | 全局含义 | 情感色彩 | 触觉反馈 | 音效 |
|------|---------|---------|---------|------|
| **单击** | 接受 / 回应 / 继续 | 中性应答 | 轻触感 | 清脆短音 880Hz |
| **双击** | 跳过 / 忽略 | 不耐烦 / 赶时间 | 短促双震 | 下行滑音 587→392Hz |
| **长按(1s** | 根据状态:重听或播报 | 确认 | 缓慢长震 | 低沉确认音 330→440Hz |
| **右滑** | 温暖 / 靠近 | 接纳、关心 | 流畅长震 | 上行音 440→659Hz |
| **左滑** | 冷淡 / 推开 | 疏离、敷衍 | 干脆短震 | 下行音 523→349Hz |
| **上滑** | 好奇 / 追问 | 想了解更多 | 快速上弹震 | 快速上行 523→784Hz |
| **下滑** | 沉默 / 退缩 | 说不出口 | 极轻慢震 | 极轻低音 330Hz |
> **不需要记位置**。每个方向有固定的情感含义,贯穿全游戏。右滑永远是温暖,左滑永远是冷淡。
>
> **长按随时可用**:任何状态下长按 1 秒,系统根据当前状态做出响应——在 `playing` 或 `choosing` 状态下重播上一句 NPC 语音,其他状态下语音播报当前位置(例如"小美的消息,等待回复")。这是玩家在纯音频环境下的"安全网"。
### 2.2 对话流程(多轮交互)
```
┌─────────────────────────────────────────────────┐
│ [提示音 + 震动] │
│ 消息到达 │
│ → 单击播放 / 双击跳过 │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ NPC 语音播放(Round 1
│ "你被甩了我都听说了,明天陪你喝酒" │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ ★ 进入选择态 │
│ [三音上行提示音 + 明确震动模式(长-短-长)] │
│ 系统语音提示:"等待回复" │
│ 玩家操作:右滑 / 左滑 / 上滑 / 下滑 / 单击 │
│ 长按 → 重听 NPC 刚才说的话 │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ [打字音效 ~0.8s → 发送音效] │
│ 林夏的语音播放 │
│ 右滑 → "好啊,明天不见不散。" │
│ 左滑 → "再说吧。" │
│ 上滑 → "你怎么知道的啊?" │
│ 下滑 → [一秒沉默] │
│ 单击 → "嗯。" │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ NPC 反应播放(Round 2)—— 因选择而异 │
│ │
│ 右滑: "行!那我叫上安安!老地方见!" │
│ 左滑: "……好吧,你休息。" │
│ 上滑: "还不是阿哲那个大嘴巴告诉我的……" │
│ 下滑: "??你还活着吗哈哈。算了不打扰你了" │
│ 单击: "嗯是什么意思啊哈哈!行吧你休息" │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ [可选] Round 3: 继续对话(仅部分路径触发) │
│ NPC 追问 → 再次选择 → 林夏回应 → 对话结束 │
└──────────┬──────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ → 等待 → 下一条消息到达 │
└─────────────────────────────────────────────────┘
```
### 2.3 关键设计
| 方面 | 说明 |
|------|------|
| **林夏的声音** | 每次选择后都有语音回应(或有感知的沉默) |
| **NPC 反应** | 根据选择播放完全不同的反应语音 |
| **选择的感受** | 五个方向各自导向不同的音频内容 |
| **对话长度** | 温暖/追问路径更长、信息更多;冷淡/沉默路径更短 |
| **独家信息** | 追问路径可获得其他路径听不到的剧情细节 |
| **玩家驱动** | 没有时间限制,消息不会自动推进,完全由玩家操作控制节奏 |
| **双击跳过** | 在林夏回应或 NPC 反应播放中,双击可跳过当前语音,加速流程 |
### 2.4 关键机制:「正在输入…」
当林夏回应前,耳机里会播放微弱的**键盘敲击声** → 发送音效 → 然后才播放林夏的语音。这 ~1 秒的"打字等待"让选择感觉真实——你不是在"选菜单",你是在"发语音"。
---
## 3. 消息全列表(16 条)
| # | 来源 | 形式 | 关键内容 | 有无选择 |
|---|------|------|---------|---------|
| 01 | 妈妈 | 电话 | "今晚煮了你爱吃的排骨" | 接听 → 无选择(自动回应) |
| 02 | 闺蜜小美 | 语音 ×3 | "你被甩了我都听说了,明天陪你喝酒" | 5 方向 + 追问 Round 3 |
| 03 | 外卖小哥 | 电话 | "你的麻辣烫到了" | 接听 → 无选择(自动回应) |
| 04 | 前任阿哲 | 语音 | "我的 AirPods 落你那了" | 5 方向 + 温暖/追问各有 Round 3 |
| 05 | HR 王姐 | 语音 | "离职流程周三前办完就行" | 5 方向 |
| 06 | 室友安安 | 语音 | "我在 Helens!快来!" | 5 方向 |
| 07 | 寝室群 | 语音 ×5 | 室友 A 升职,@林夏 | 5 方向 + 温暖 Round 3 |
| 08 | 未知号码 | 电话 | 呼吸声 6 秒后挂断 | 无选择(悬疑钩子) |
| 09 | 小美(醉)| 语音 ×4 | "阿哲大三追过我" | 5 方向 + 追问 Round 3 |
| 10 | 妈妈 | 文字(系统语音朗读) | "睡了吗?" | 5 方向 + 温暖/追问各有 Round 3 |
| 11 | 阿哲 | 电话 | "我能过去拿吗?就 5 分钟" | 5 方向(关键分支锚点) |
| 12 | 周南 | 语音 | "初三坐你后桌那个" | 5 方向 |
| 13 | 周南 | 语音 ×4 | "今天我生日" | 5 方向 + 温暖 Round 3 |
| 14 | 妈妈 | 电话 | "你不接电话我有点担心" | 接听 → 自动回应,条件分支 |
| 15 | 自己 | 系统通知 | 3 年前录的备忘录 | 单击播放 / 双击跳过 |
| 16 | 妈妈 | 长语音 ×8 | 妈妈 24 岁那年的故事 | 无选择(结局前奏) |
---
## 4. 16 条消息详细设计
### MSG-01 妈妈 · 电话
**类型**:来电(铃声 + 长震动)
**Round 1**(接听后):妈妈说"今晚煮了你爱吃的排骨,你什么时候回来呀。"
**接听后自动**:林夏轻轻说"嗯……好。"(无选择)
**不接** → 直接跳到下一条。
**状态**:接听 → MOM_LINK = true
---
### MSG-02 小美 · 语音 ×3(教学关)
**Round 1**:小美连发三条语音:
- "林夏!你还活着吗!"
- "你被甩了我都听说了——别难过啊"
- "明天陪你喝酒,去不去"
**选择 + Round 2**
| 方向 | 林夏说 | 小美反应 | profile |
|------|--------|---------|---------|
| 右滑 | "好啊,明天不见不散。" | "行!那我叫上安安!老地方见!别放鸽子啊!" | engagement+2 |
| 左滑 | "再说吧。" | "……好吧。你要是改主意了随时说。" | avoidance+1 |
| 上滑 | "你怎么知道的啊?" | "还不是阿哲自己发朋友圈!你不知道吗?他发了又秒删……" | engagement+1**独家信息** |
| 下滑 | [沉默 2s] | "??你还活着吗哈哈。好了不烦你了,有事找我。" | avoidance+2 |
| 单击 | "嗯。" | "嗯是什么意思啊哈哈!行吧你休息。" | — |
**追问 Round 3**(上滑后,小美说完"他发了又秒删"):
- 右滑 → 林夏:"他……发了什么?" → 小美:"就发了一张你们以前的照片,配了一句'值得'。两分钟就删了" → **独家信息**
- 其他 → 对话结束
---
### MSG-03 外卖小哥 · 电话
**类型**:来电
**Round 1**:外卖小哥:"你好,你的麻辣烫到了,在楼下。"
**接听后自动**:林夏说"好,我下来拿。"(无选择)
**不接** → 漏接。
---
### MSG-04 阿哲 · 语音
**Round 1**:阿哲说"那个……我的 AirPods 好像落你那了,方便帮我寄一下吗?"
| 方向 | 林夏说 | 阿哲反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "你那边……还好吗。" | "我……挺好的。" [停顿] "你呢?" | engagement+2 |
| 左滑 | "知道了。明天寄。" | "嗯……谢了。" | HE_BACK=false, avoidance+1 |
| 上滑 | "你怎么不自己来拿。" | "我……不太方便过去。" [欲言又止] | engagement+2 |
| 下滑 | [沉默 3s] | [等 5 秒] "那……我晚点再说吧。" → 阿哲撤回消息 | HE_BACK=null, avoidance+2 |
| 单击 | "好。" | "谢谢。" | HE_BACK=false, engagement+1 |
**右滑 Round 3**(阿哲问"你呢?"后):
- 右滑 → 林夏:"不太好。" → 阿哲:"……对不起。" [沉默]
- 左滑 → 林夏:"挺好的。" → 阿哲:"那就好。"
- 下滑 → [沉默] → 阿哲:"……那,晚安。"
**上滑 Round 3**(阿哲说"不太方便"后):
- 上滑 → 林夏:"为什么。" → 阿哲:"我……" [长停顿] "没什么。你寄吧。" → HE_BACK=false
- 右滑 → 林夏:"算了,不问了。" → 对话结束
---
### MSG-05 HR 王姐 · 语音
**Round 1**:王姐说"小夏,离职流程周三前办完就行。别有压力。"
| 方向 | 林夏说 | 王姐反应 | profile |
|------|--------|---------|---------|
| 右滑 | "谢谢王姐。" | "别客气。你还年轻,出去看看也好。这行业你知道的。" | engagement+1 |
| 左滑 | "好的,知道了。" | [无反应,对话结束] | — |
| 上滑 | "王姐……有什么建议吗?" | "你想听真话?别在这行耗了。你有本事,去试试别的。" → **独家** | engagement+2 |
| 下滑 | [沉默] | [无反应,对话结束] | avoidance+1 |
| 单击 | "好。" | [无反应,对话结束] | — |
---
### MSG-06 安安 · 语音
**Round 1**:安安在酒吧里大喊:"我在 Helens!快来!今天 happy hour"(背景很吵)
| 方向 | 林夏说 | 安安反应 | profile |
|------|--------|---------|---------|
| 右滑 | "在哪儿啊?我看看吧。" | "对面那个!我给你留了位置!赶紧赶紧!" | engagement+2 |
| 左滑 | "不了,你们玩好。" | "好吧!那我给你寄一杯!" [大笑] | — |
| 上滑 | "你喝了多少了?" | "才两杯!我清醒得很!……好吧三杯。快来!" | engagement+1 |
| 下滑 | [沉默] | "喂?信号不好吗?好吧以后再约!" | avoidance+1 |
| 单击 | "嗯。" | "嗯是来还是不来啊!算了回头聊!" | — |
---
### MSG-07 寝室群 · 语音 ×5
**Round 1**:群消息连续播放:
- 室友 A"我升主管了!!!"
- 室友 A"请你们吃饭!!"
- 室友 B"我的天!恭喜!!"
- 室友 B"必须喝一个!"
- 室友 C"@林夏 你也来吧"
| 方向 | 林夏说 | 群里反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "恭喜恭喜!太厉害了!" | 室友 A:"哈哈谢谢!我请客!你最近怎么样啊?" | GROUP_REPLY, engagement+2 |
| 左滑 | "恭喜。" | [群里继续聊别的,没人注意到你的冷淡] | avoidance+1 |
| 上滑 | "怎么升的啊?讲讲!" | 室友 A:"说来话长!我们老板走了我就顶上了哈哈" → **独家** | GROUP_REPLY, engagement+2 |
| 下滑 | [沉默,不回群] | [群里继续热闹,但渐渐没人 @ 你了] | avoidance+2 |
| 单击 | [发一个表情] | 室友 B:"林夏发表情了哈哈" | GROUP_REPLY |
**右滑 Round 3**(室友 A 问"你最近怎么样"后):
- 右滑 → 林夏:"挺好的~你请客我去!" → 室友们欢呼
- 左滑 → 林夏:"还行。" → 室友 B:"你是不是有事啊?没事可以跟我们说"
- 下滑 → [沉默] → 对话自然结束
---
### MSG-08 未知号码 · 电话(悬疑)
**类型**:来电
**接听后**:只有呼吸声,6 秒后自动挂断。然后心跳音效。
**无选择**——悬疑钩子。
**不接** → 未接来电。
**回收**:MSG-16 妈妈长语音第 5 段中提到"我外婆每天晚上都给我打一个电话,她不说什么"——暗示未知号码就是妈妈从另一个号码拨出的"不说什么的电话",与妈妈线形成跨代呼应。不做明确揭示,留给玩家自己领悟。如果玩家接了 MSG-08 + 听完 MSG-16,两段呼吸/沉默会在记忆中形成对位。
---
### MSG-09 小美(醉)· 语音 ×4
**BGM 切换为悬疑风格**
**Round 1**(四段醉酒语音):
- "林夏……我跟你说个事"
- "阿哲那个人……大三的时候追过我"
- "但我没答应他……因为我知道你喜欢他"
- "我到现在都没跟你说……对不起"
| 方向 | 林夏说 | 小美反应 | profile |
|------|--------|---------|---------|
| 右滑 | "小美……你没事吧。" | "我没事……我就是喝多了想告诉你……你别怪我。" [哭] | engagement+2 |
| 左滑 | "我知道了。" | [沉默 3 秒] → 小美撤回了刚才的语音 | avoidance+1 |
| 上滑 | "你……为什么现在才说。" | "因为你今天……我怕你以后恨我……" | engagement+2 |
| 下滑 | [沉默] | [10 秒后] 小美撤回了刚才的语音 | avoidance+2, nostalgia+1 |
| 单击 | "嗯。" | "你不生气吗?" [等 3 秒] "算了……当我没说" | nostalgia+1 |
**上滑 Round 3**(小美说"怕你恨我"后):
- 右滑 → 林夏:"我不恨你。" → 小美 [大哭]:"谢谢你……谢谢你林夏"
- 左滑 → 林夏:"我得想想。" → 小美:"好……对不起" → 对话结束
- 下滑 → [沉默] → 小美:"你还在吗?" → [长沉默] → 小美撤回
---
### MSG-10 妈妈 · 文字消息
**Round 1**:消息到达提示音。系统语音朗读三个字——"睡了吗?"
| 方向 | 林夏说 | 妈妈反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "没有,怎么了?" | 妈妈发语音:"没事,就是想起来问一下。今天怎么样?" | MOM_LINK, engagement+2 |
| 左滑 | "嗯,要睡了。" | 系统语音朗读妈妈的文字:"好,早点睡。" | MOM_LINK, avoidance+1 |
| 上滑 | "妈……你能打电话给我吗?" | [电话铃响] 妈妈打来了!接听后:"怎么了夏夏?" | MOM_LINK, engagement+3 |
| 下滑 | [不回复] | [30 秒后] 系统语音朗读妈妈的文字:"看到了回一下。" | avoidance+2 |
| 单击 | "嗯。" | 系统语音朗读妈妈的文字:"嗯就是睡了?晚安。" | MOM_LINK |
**上滑路径**(妈妈打来电话后,说"怎么了夏夏?你声音不对。"):
- 右滑 → 林夏:"没什么……就是有点累。" → 妈妈:"累了就回家歇几天。妈煮了排骨。"
- 上滑 → 林夏:"妈,我辞职了。" → 妈妈 [沉默 2 秒]:"辞了就辞了。回来再说。"
- 下滑 → [沉默,只有呼吸] → 妈妈:"不想说就不说。妈在这呢。"
**右滑路径**(妈妈问"今天怎么样"后):
- 右滑 → 林夏:"还好。" → 妈妈:"那就好。别太晚了。"
- 上滑 → 林夏:"不太好。" → 妈妈:"怎么了?"
- 右滑 → 林夏:"工作没了。" → 妈妈:"……回来歇几天吧。妈不催你。"
- 下滑 → [沉默] → 妈妈:"不想说就不说。妈在这呢。" → 对话结束
---
### MSG-11 阿哲 · 电话(关键分支)
**类型**:来电
**Round 1**(接听后):阿哲说"那个……AirPods 的事,我能过去拿吗?就 5 分钟。"
| 方向 | 林夏说 | 阿哲反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "可以。但只有 5 分钟。" | "好,我马上过来。谢谢。" → [挂断] | HE_BACK=true |
| 左滑 | "不方便。我明天寄给你。" | "好吧……那谢了。" → [挂断] | HE_BACK=false |
| 上滑 | "为什么非要今天来?" | "我……明天要出差。" [停顿] "就 5 分钟。" | → 追问后再选 |
| 下滑 | [沉默 → 挂断音] | [电话断了] | HE_BACK=false |
| 单击 | "不了。" | "好。" → [挂断] | HE_BACK=false |
**上滑 Round 3**(阿哲说"就 5 分钟"后,再次进入选择):
- 右滑 → 林夏:"……来吧。" → 阿哲:"谢谢。我马上到。" → [挂断] → HE_BACK=true
- 左滑 → 林夏:"还是算了。" → 阿哲:"好吧。" → [挂断] → HE_BACK=false
- 下滑 → [沉默] → 阿哲:"那……你考虑一下,我等你消息。" → [挂断] → HE_BACK=false
**如果 HE_BACK=true** → 消息 12 之前插入门铃声 + 敲门声场景。
---
### MSG-12 周南 · 语音
**Round 1**:周南说"林夏,是我,周南。初三坐你后桌那个。好久没联系了,今天突然想到你。"
| 方向 | 林夏说 | 周南反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "周南!你还记得我啊!" | "当然记得。你是我们班最安静的那个。" [轻笑] | ZHOUNAN_DEPTH+1, engagement+2 |
| 左滑 | "嗯,你好。" | "嗯……好久不见。你现在在哪呢?" | ZHOUNAN_DEPTH+1, nostalgia+1 |
| 上滑 | "你怎么有我微信的?" | "你忘了吗?毕业那天你加的我。不过你可能早忘了哈哈。" | ZHOUNAN_DEPTH+1, nostalgia+2 |
| 下滑 | [沉默] | "你是不是不记得我了?没关系的。就是突然想说声嗨。" | nostalgia+1 |
| 单击 | "嗯……好久不见。" | "是啊,好久不见。" | ZHOUNAN_DEPTH+1 |
---
### MSG-13 周南 · 长语音 ×4
**BGM 切换为周南主题(温暖吉他)**
**Round 1**(四段语音):
- "高考那年我没考好,去复读了"
- "后来上了个二本,学了个不喜欢的专业"
- "毕业回了老家,在我爸店里帮忙"
- "今年刚来北京出差……今天是我生日"
| 方向 | 林夏说 | 周南反应 | 效果 |
|------|--------|---------|------|
| 右滑 | "一个人在北京,还好吗?" | "还行吧。习惯了。你呢?你在北京过得好吗?" | ZHOUNAN_DEPTH+1, engagement+2 |
| 左滑 | "你讲了好多。" | "是吗?不好意思,话太多了。" [轻笑] | ZHOUNAN_DEPTH+1, nostalgia+1 |
| 上滑 | "……生日快乐。" | [沉默 2 秒] "谢谢你。你是今天第一个跟我说的。" | ZHOUNAN_DEPTH+1, ZHOUNAN_SHARE, nostalgia+2 |
| 下滑 | [沉默] | "不好意思突然说这么多……那,晚安。" | avoidance+1 |
| 单击 | "嗯,我都听到了。" | "嗯。谢谢你听我说。" | ZHOUNAN_DEPTH+1 |
**右滑 Round 3**(周南问"你在北京过得好吗"后):
- 右滑 → 林夏:"不太好。今天也是我的坏日子。" → 周南:"那我们算是难兄难妹了。" [轻笑] → **ZHOUNAN_SHARE**
- 左滑 → 林夏:"还行吧。" → 周南:"那就好。"
- 上滑 → 林夏:"你为什么突然联系我?" → 周南:"因为……今天生日嘛,想起以前的事。想起你。"
---
### MSG-14 妈妈 · 第二次电话
**类型**:来电
**Round 1**:妈妈说"夏夏,你不接电话我有点担心。你还好吗?"
接听后 → 林夏自动说"嗯,我没事。"
妈妈:"真的没事?"
- 如果 MOM_LINK=true"好。那早点睡。妈爱你。" → 对话结束
- 如果 MOM_LINK=false"你今天怎么都不理妈妈?出什么事了吗?" → 进入选择:
- 右滑 → 林夏:"没事妈,就是手机坏了。" → 妈妈:"那你赶紧修啊。好了早点睡。" → MOM_LINK=true
- 左滑 → 林夏:"嗯,没事。" → 妈妈:"……好吧。晚安。" → MOM_LINK=true
- 下滑 → [沉默] → 妈妈:"你不说话我更担心。算了,不逼你了。晚安。" → MOM_LINK=true
**不接** → MOM_LINK 保持当前值(可能仍为 false)
**状态**:接听 → MOM_LINK=true
---
### MSG-15 时光胶囊 · 系统通知
**特殊消息**:系统提示音 + "3 年前的今晚,你给自己录过一段备忘录。"
- 单击播放 → 根据玩家画像播放不同版本(avoidance / engagement / nostalgia → SELF_RECORD=true
- 双击跳过 → avoidance+2
*无选择交互。这条消息是独白时刻。*
---
### MSG-16 妈妈 · 长语音 ×8(结局前奏)
**条件**MOM_LINK=true 时播放
妈妈讲她 24 岁那年的故事:
1. "夏夏。妈妈知道你今天电话不接。妈妈不催你,就是想跟你说一件事。"
2. "你 24 了对吧。妈妈 24 那年——你应该没听我说过——"
3. "那时候妈妈在广州,给一个香港老板做秘书,1995 年。那年我也被分手过。也被开除过。"
4. "那年我哭得特别难看。后来呢?后来就没什么后来了。我回了老家,认识了你爸,生了你。"
5. "我想跟你说,那年夏天我没死,是因为我外婆每天晚上都给我打一个电话。"
- **音频设计**:第 5 段开头,叠入与 MSG-08 相同的电话底噪质感(轻微线路嘶声),持续 2-3 秒后淡出。如果玩家之前接了 MSG-08 的未知号码,这一刻会在听觉上产生似曾相识的感觉——同样的电话底噪、同样的呼吸停顿,但这次有了妈妈的声音。
6. "她不说什么,就问我今天吃饭了没。"
- **音频设计**"不说什么"之后留 3 秒静默(仅电话底噪),呼应 MSG-08 的 6 秒呼吸声,形成跨消息的听觉对位。
7. "我现在打给你,你不接也没事。妈妈给你发个语音,你什么时候想听就听。你不用回我。"
8. "你今天接了好多电话吧。妈妈不打扰你了。晚安。"
*无选择交互。让玩家安静听完。播放结束 → 触发结局判定。*
**如果 MOM_LINK=false**MSG-16 不播放。MSG-15 结束后,静默 5 秒(环境底噪),然后直接进入结局判定。此时结局 A 的条件不可能满足(缺少 MOM_LINK),流程会落入结局 B/C/D。
---
## 5. 分支判定变量
| 变量 | 触发条件 | 影响 |
|------|---------|------|
| **MOM_LINK** | 是否接妈妈两次电话中的至少一次 / 是否回复"睡了吗?" | 决定 MSG-16 是否播放 |
| **HE_BACK** | 是否答应阿哲过来拿耳机(MSG-11 右滑) | 决定 MSG-12 前是否插入门铃场景 |
| **GROUP_REPLY** | 是否在寝室群里回复过(MSG-07 非沉默) | 决定彩蛋消息 |
| **ZHOUNAN_DEPTH** | 给周南的回应深度(累计) | 决定结局独白 + self-3y 版本 |
| **ZHOUNAN_SHARE** | 是否在周南对话中选择"分享自己"MSG-13 上滑/右滑 R3 右滑) | 决定隐藏结局 |
| **SELF_RECORD** | 是否听完 3 年前备忘录(MSG-15) | 决定结局类型 |
| **PROFILE** | 全场行为聚合画像:avoidance / engagement / nostalgia | 决定 self-3y 版本 + 环境音 |
| **UNREAD_COUNT** | 未播放/未接听的消息数(初始 16,每播放或接听 −1) | 决定结局 C/D |
| **REPLY_COUNT** | 林夏实际发出回应的消息条数 | 决定结局 C |
**REPLY_COUNT 计数规则**
- 选择右滑/左滑/上滑/单击(发出语音回应)→ +1
- 下滑(沉默)→ 不计
- 电话接听后自动回应(MSG-01/03/14)→ +1
- 双击跳过整条消息 → 不计
- 发表情(MSG-07 单击)→ +1
### 变量影响层级
并非所有选择都对结局产生等量影响。设计上区分**核心变量**与**氛围变量**:
| 层级 | 变量 | 影响 | 相关消息 |
|------|------|------|---------|
| **核心** | MOM_LINK | 决定 MSG-16 是否播放(妈妈长独白),直接影响结局 A | MSG-01, 10, 14 |
| **核心** | HE_BACK | 决定门铃场景是否触发,区分结局 A/B | MSG-11 |
| **核心** | ZHOUNAN_DEPTH + ZHOUNAN_SHARE | 决定隐藏结局 E | MSG-12, 13 |
| **辅助** | SELF_RECORD | 决定结局 A 是否可达 | MSG-15 |
| **氛围** | GROUP_REPLY | 影响彩蛋消息,但不改变结局走向 | MSG-07 |
| **氛围** | PROFILEavoidance/engagement/nostalgia | 影响环境音色调 + self-3y 版本,不直接决定结局 | 全局累计 |
| **统计** | UNREAD_COUNT / REPLY_COUNT | 决定结局 C/D 的触发条件 | 全局自动累计 |
> **设计原则**:HR、安安、寝室群、小美醉酒等消息的选择主要影响 PROFILE 氛围和当下的对话反馈,让玩家"听得出这一句不一样"。真正改变结局走向的是**妈妈线、阿哲线、周南线**三条核心关系。玩家在这三条线上的选择会产生明确的、跨消息的后果。
### PROFILE 计分细则
| 行为 | avoidance | engagement | nostalgia |
|------|-----------|------------|-----------|
| 漏接电话(不动) | +2 | 0 | 0 |
| 双击跳过消息 | +2 | 0 | 0 |
| 单击回"嗯" | +1 | 0 | 0 |
| 左滑冷淡回应 | +1 | 0 | 0 |
| 下滑沉默 | +2 | 0 | +1 |
| 右滑温暖回应 | 0 | +2 | 0 |
| 上滑追问 | 0 | +2 | 0 |
| Round 3 中选择"分享自己" | 0 | +1 | +1 |
最终取分数最高项作为 PROFILE 标签。
---
## 6. 结局系统(5 种)
### 结局 A · "明天就明天吧"
**触发**MOM_LINK=true + SELF_RECORD=true + 至少回了周南或群里 1 条
**优先级**2
- 妈妈语音播完。林夏起身,环境声:走到厨房、烧水、坐在小板凳上。
- 林夏轻声自语:"明天再说。"
- 烧水壶滴的一声 → 游戏结束。
### 结局 B · "AirPods 我寄给你"
**触发**HE_BACK=false + 回过周南至少 1 条
**优先级**3
- 林夏给阿哲发语音"我寄给你,地址发我"(打字音效 → 发送音效 → 林夏语音播放)。
- 林夏轻声自语:"寄出去了。"
- 一个"清醒地走"的结局。
### 结局 C · "全部已读"
**触发**UNREAD_COUNT=0 且 REPLY_COUNT≤2
**优先级**4
- 系统语音播报实际数值:"今晚共 16 条新消息,已读 {16UNREAD_COUNT},未回复 {16REPLY_COUNT}。"
- 长沉默。林夏轻声自语:"今晚就这样。"
### 结局 D · "明天再开机"
**触发**UNREAD_COUNT≥8**或**结局 E/A/B/C 均不满足(真正兜底)
**优先级**5(兜底结局)
- 系统语音提示电量 1%。触感震动 + 白噪 → 彻底黑。
- 第二天清晨鸟叫。林夏轻声自语:"早上好。"
### 结局 E · 隐藏 · "周南,我也记得你"
**触发**ZHOUNAN_DEPTH≥2 + ZHOUNAN_SHARE + PROFILE=engagement 或 nostalgia
**优先级**1(最高优先级)
- 周南秒回:"那我下次出差来北京,请你吃饭好不好。"
- 系统播放林夏的轻笑——这是全游戏唯一一次林夏主动笑出声。
- 林夏轻声说:"好。"
### 结局判定规则
当玩家同时满足多个结局条件时,按**优先级数字从小到大**判定,取第一个满足的结局:
```
E(隐藏,优先级 1)→ A(优先级 2)→ B(优先级 3)→ C(优先级 4)→ D(兜底,优先级 5)
```
- 结局 E 最稀有、最难触发,一旦满足则覆盖其他结局。
- 结局 D 是**真正的兜底结局**——无论玩家怎么玩,只要 E/A/B/C 都不满足就一定进入 D。UNREAD_COUNT≥8 是 D 的常见触发场景,但不是唯一入口。
- 结局 A 和 B 可能同时满足(例如 MOM_LINK=true + SELF_RECORD=true + HE_BACK=false + 回过周南),此时优先触发 A,因为 A 是更完整的情感闭合。
---
## 7. 音频设计
### 7.1 音频资产分类
| 类别 | 说明 | 数量 | 制作方案 |
|------|------|------|---------|
| **林夏语音** | 主角对话回应(通用 + 专属) | ~55 条, ~100s | VoxCPM2 声音克隆 |
| **NPC Round 1 语音** | 各角色主要台词 | ~30 条, ~300s | VoxCPM2 声音克隆 |
| **NPC Round 2/3 反应** | 对话分支反应 | ~54 条, ~205s | VoxCPM2 声音克隆 |
| **self-3y 三版** | 时光胶囊备忘录 ×3 | 3 条, ~150s | VoxCPM2`self_3y_v03` 独立参考) |
| **UI 短音效** | 提示音、选择反馈、打字/发送 | ~12 种 | Web Audio 合成(无需文件) |
| **BGM** | 4 段主题音乐 | 4 轨 | ACE-Step 1.5 本地生成 |
| **环境音** | 卧室夜噪、厨房、黎明等 | 3 段 | ACE-Step 1.5 本地生成 |
| **角色铃声** | 来电铃声 | 5 条(游戏引用 4 种场景) | ACE-Step 1.5 本地生成 |
| **叙事音效** | 敲门、心跳、呼吸、水壶等 | 4 条 | ACE-Step 1.5 本地生成 |
### 7.2 TTS 方案:VoxCPM2 本地克隆(现行 v5 链路)
> **注意**:本项目 TTS 统一使用 **VoxCPM2**。CosyVoice2 仅作备用,不作为主生产链路。
> **详细操作与目录**:见 `audio_2/项目状态说明.md`、`执行文档_音频素材制作.md`。
**现行流程**2026-05 落地):
```
Voice Design 试听(v4,每角色 5 条提示词变体)
→ 人工选定参考 wav
→ Style Control 克隆 + 表演修饰(v5
→ 部署到 audio/mp3/tts/*_wav_v1.mp3(游戏直接加载)
```
**服务启动**(端口 **8002**8000 被其他服务占用):
```bash
VOXCPM_PORT=8002 VOXCPM_ENABLE_DENOISER=0 bash /home/xsl/tts-server/start-voxcpm.sh
curl http://127.0.0.1:8002/health
# 预期: {"status":"ok","model":"VoxCPM2","styled_speech":true}
```
**API 接口**
| 接口 | 用途 |
|------|------|
| `POST /v1/voices` | 注册参考音频(wav_base64)→ voice_id |
| `POST /v1/speech` | 纯克隆合成(text + voice_id |
| `POST /v1/speech/styled` | Style Control:有 voice_id = 克隆+表演;无 voice_id = Voice Design |
- 输出格式:48kHz 16-bit PCM WAV
- 参数:cfg_value(默认 2.0)、inference_timesteps(默认 10
**批量生产(现行)**
```bash
# 全量克隆并部署游戏目录
VOXCPM_PORT=8002 python audio_2/scripts/04_gen_tts_clone.py --force
# 只重跑某角色
VOXCPM_PORT=8002 python audio_2/scripts/04_gen_tts_clone.py --role mom --force
# 重新生成试听样本(换提示词挑音色)
VOXCPM_PORT=8002 python audio_2/scripts/03_gen_voice_samples.py --role delivery --force
```
**游戏音频命名**:仅 `*_wav_v1.mp3``story.js` / `engine.js` 引用;`*_mp3_v1.mp3` 为旧流水线产物,已清理。
### 7.3 角色音色配置
参考音频来自 Voice Design 试听选定,存放在 `voice/{角色}/{角色}_ref.wav``audio_2/tts/v4_voice_samples/{角色}/`
| 角色 | 参考文件 | 目录 | 语速 | 音色描述 |
|------|----------|------|------|---------|
| **林夏** | `linxia_v05.wav` | `voice/linxia/` | 1.00 | 24 岁女生,疲惫但清醒,克制但有温度 |
| 妈妈 | `mom_v01.wav` | `voice/mom/` | 0.92 | 50 上下,温柔不黏腻,西部口音轻微 |
| 阿哲 | `azhe_v03.wav` | `voice/azhe/` | 1.05 | 27 岁男声,城市男生,理性偏冷 |
| 小美 | `xiaomei_v05.wav` | `voice/xiaomei/` | 1.10 | 24 岁,活泼,语速快 |
| 小美(醉) | `xiaomei_drunk_v05.wav` | `voice/xiaomei/` | 0.95 | 同闺蜜基底,降速模拟醉态 |
| 周南 | `zhounan_v05.wav` | `voice/zhounan/` | 0.95 | 26 岁男声,温和略拘谨 |
| HR 王姐 | `hr_v05.wav` | `voice/hr/` | 1.00 | 38 岁,职场化,疲惫的善意 |
| 安安 | `anan_v04.wav` | `voice/anan/` | 1.15 | 23 岁,活泼,比小美更嗲 |
| 室友 A | `dorm_a_v03.wav` | `voice/dorm_a/` | 1.10 | 年轻女声,兴奋 |
| 室友 B | `dorm_b_v02.wav` | `voice/dorm_b/` | 1.00 | 年轻女声,随和 |
| 室友 C | `dorm_c_v03.wav` | `voice/dorm_c/` | 0.95 | 年轻女声,温柔 |
| 外卖小哥 | `delivery_v04.wav` | `voice/delivery/` | **1.00** | 30 岁**男声**,北方腔,语速适中 |
| self-3y | `self_3y_v03.wav` | `voice/linxia/` | 1.15 | 3 年前的林夏,更年轻明亮(独立参考,非复用小美) |
表演修饰(warm/cold/分段等)由 `audio_2/voice_style_prompts.py``get_clone_style()` 控制;音色本身由参考 wav 锚定。
### 7.4 脚本与产物目录
| 脚本 | 用途 | 输出 |
|------|------|------|
| `audio_2/scripts/03_gen_voice_samples.py` | v4 试听(每角色 5 条) | `audio_2/tts/v4_voice_samples/` |
| `audio_2/scripts/04_gen_tts_clone.py` | **v5 全量克隆 + 部署** ★ | `audio_2/tts/v5_cloned/*_wav_v5.mp3``audio/mp3/tts/*_wav_v1.mp3` |
| `audio/batch_tts_voxcpm.py` | 台词表 `LINES`(173 条)+ 语速配置 | 旧版直连克隆,已被 v5 替代 |
| `audio_2/voice_style_prompts.py` | 表演修饰 / Voice Design 提示词 | — |
| `audio_2/voice_sample_prompts.py` | v4 试听变体与统一试听文本 | — |
**台词表**`audio/batch_tts_voxcpm.py``LINES` + `04_gen_tts_clone.py``EXTRA_LINES`self-3y ×3、系统 memo)。
**配置与文档**
```bash
# 旧版批量(仅参考,不再用于正式产出)
python audio/batch_tts_voxcpm.py --source voice --dry-run
```
### 7.5 UI 音效(Web Audio 合成,无需文件)
所有 UI 音效通过 `SFX_UI` 对象在浏览器端实时合成:
| 音效 | 用途 | 实现方式 |
|------|------|---------|
| `tap` | 单击确认 | 880Hz, 45ms |
| `swipe` | 滑动确认 | 660Hz, 60ms |
| `error` | 无效操作 | 200Hz square, 100ms |
| `notify` | 新消息到达 | 523Hz + 659Hz 双音 |
| `choiceReady` | 进入选择 | 440→523→659 三音上行 |
| `choiceWarm` | 右滑温暖 | 440→659Hz 上行 |
| `choiceCold` | 左滑冷淡 | 523→349Hz 下行 |
| `choiceCurious` | 上滑追问 | 523→784Hz 快速上行 |
| `choiceSilent` | 下滑沉默 | 330Hz 极轻 |
| `typing` | 打字中 | 6 次随机频率短脉冲 |
| `send` | 发送 | 400→900Hz 上扫 |
| `longPress` | 长按确认 | 330→440Hz 缓升 |
| `ending` | 结局 | 三音渐弱 |
### 7.6 BGM 与环境音
| 曲目 | 用途 | 描述 |
|------|------|------|
| M1 | 全程低氛围 Loop | lo-fi midnight bedroom, soft piano, light vinyl noise |
| M2 | MSG-08/09 悬疑 | subtle ambient suspense, low drone |
| M3 | MSG-13 周南段落 | gentle indie folk, fingerpicked guitar |
| M4 | MSG-16 结尾 | warm cinematic piano, single melody line |
环境音根据 PROFILE 累计分数动态调整:
- engagement 倾向:柔和雨声、远处隐约的音乐
- avoidance 倾向:安静、时钟滴答、远处车声
- nostalgia 倾向:梦幻混响、远处风铃
---
## 8. 技术实现
### 8.1 技术栈
纯前端 Web 应用(单 HTML + JS),无需后端服务器:
- **手势识别**`gestures.js` — 7 种手势(单击、双击、长按、四方向滑动)
- **音频管理**`audio.js` — Web Audio API 合成 + HTML5 Audio 播放
- **触觉反馈**`haptics.js` — Vibration API(增强反馈,非必需;见下方降级说明)
- **剧情数据**`story.js` — 16 条消息的完整对话树
- **游戏引擎**`engine.js` — 状态机 + 异步对话流程
- **行为画像**`profile.js` — PROFILE 计分 + 状态变量
### 8.2 状态机
```
idle → notification/ringing → playing → choosing → responding → reacting → [choosing | idle] → ending
```
| 状态 | 说明 | 可用手势 | 进入音效 |
|------|------|---------|---------|
| idle | 等待消息 | 长按(播报状态) | — |
| notification | 消息到达提示中 | 单击(播放)、双击(跳过)、长按(播报状态) | 提示音 + 震动 |
| ringing | 来电铃声中 | 单击(接听)、左滑(拒接)、长按(播报来电人) | 铃声 + 长震 |
| playing | NPC 语音播放中 | 双击(跳过语音)、长按(重听本句) | — |
| choosing | 等待玩家选择 | 右滑、左滑、上滑、下滑、单击、长按(重听 NPC 上一句) | **三音上行 + 长-短-长震动 + "等待回复"语音** |
| responding | 林夏语音播放中 | 双击(跳过) | 打字音效 |
| reacting | NPC 反应播放中 | 双击(跳过) | — |
| ending | 结局播放中 | 三指点击(退出) | 结局专属音效 |
> **关键**`playing → choosing` 的状态切换必须有**明确的听觉分界**(三音上行 + 震动模式变化 + "等待回复"语音提示),确保玩家在纯音频环境下不会误以为自己还在听消息。单击在 `notification` 状态是"播放",在 `choosing` 状态是"嗯"——两者之间有明确的进入选择态提示,不会混淆。
### 8.3 异步对话流程
```javascript
// 进入选择态时的明确确认
function enterChoosing(choices) {
state = 'choosing';
SFX_UI.choiceReady(); // 三音上行
haptics.pattern([200, 100, 80, 100, 200]); // 长-短-长 震动
speak('等待回复'); // 系统语音确认
currentChoices = choices;
}
// 长按处理:任何状态下可用
async _handleLongPress() {
SFX_UI.longPress();
if (state === 'choosing' || state === 'playing') {
// 重播上一句 NPC 语音
await playVoiceAsync([lastNpcAudio]);
} else {
// 播报当前状态
speak(getStatusText()); // 如:"小美的消息,等待回复"
}
}
async _handleChoice(dir) {
const choice = currentMsg.choices[dir];
// 1. 情感音效
SFX_UI[emotionSfx[dir]]();
// 2. 打字 → 发送
state = 'responding';
SFX_UI.typing();
await sleep(800);
SFX_UI.send();
await sleep(300);
// 3. 林夏的回应
if (choice.linxia) {
await playVoiceAsync([choice.linxia]);
} else {
await sleep(1500); // 沉默
}
// 4. NPC 反应
if (choice.npcReact) {
state = 'reacting';
await playVoiceAsync(choice.npcReact);
}
// 5. 后续对话(Round 3
if (choice.followUp) {
state = 'playing';
await playVoiceAsync(choice.followUp.audio);
if (choice.followUp.choices) {
enterChoosing(choice.followUp.choices);
return;
}
}
// 6. 推进到下一条
proceedToNext();
}
```
### 8.4 跳过机制
`responding``reacting` 状态中双击:
- 设置 `_skipRequested = true`
- 中断当前语音播放
- 异步函数在每个步骤检查 skip 标志,快速跳到下一阶段
### 8.5 全局安全手势
除 §2.1 的 7 种剧情手势外,有两种**全局安全手势**,它们的优先级高于任何剧情手势,在所有状态下均可触发:
| 手势 | 功能 | 优先级 |
|------|------|--------|
| **两指长按(1s** | 暂停游戏 | 最高——立即中断当前音频播放,进入暂停状态 |
| **三指点击** | 安全退出 | 最高——系统语音确认后跳转到支持资源页 |
**实现要求**
- `gestures.js` 在手势识别时先判断触摸点数量:≥3 → 安全退出,=2 且持续 ≥1s → 暂停,=1 → 进入剧情手势判断。
- 暂停状态下仅响应:单击(继续)、三指点击(退出)。
- 安全手势不影响任何游戏变量(PROFILE、REPLY_COUNT 等不变)。
### 8.6 触觉降级策略
触觉反馈(Vibration API)是增强体验,**不是必需通道**。iPhone Safari 不支持 Vibration API,部分 Android 浏览器也可能受限。设计原则:
- **所有状态切换必须有独立的音效/语音确认**,不能仅靠震动区分。
- `haptics.js` 在初始化时检测 `navigator.vibrate` 是否可用,不可用时静默降级。
- 测试时必须在关闭震动的情况下完整通关一遍,确认纯音频也能正常游玩。
### 8.6 隐私承诺
本作**不申请任何敏感权限**
- ❌ 不要麦克风、摄像头、位置、通讯录
- ❌ 不需要联网(全程离线可玩)
- ✅ 仅使用:触觉震动 + 触控输入 + 系统音量
- 玩家行为数据只在本地存储,从不上传。
---
## 9. 音频资产清单
### 林夏语音
| 类别 | 数量 | 平均时长 | 总时长 |
|------|------|---------|-------|
| 通用短句(嗯、好、知道了等) | ~10 条 | 1s | ~10s |
| 消息专属台词 | ~45 条 | 2s | ~90s |
| **合计** | **~55 条** | | **~100s** |
### NPC 语音(Round 1 主台词 + Round 2/3 反应)
| 角色 | Round 1 | Round 2/3 | 总条数 | 总时长 |
|------|---------|-----------|-------|-------|
| 妈妈 | 11 段 | 10 条 | ~21 条 | ~340s |
| 小美 | 7 段 | 10 条 | ~17 条 | ~120s |
| 阿哲 | 2 段 | 8 条 | ~10 条 | ~50s |
| 周南 | 5 段 | 10 条 | ~15 条 | ~80s |
| HR 王姐 | 1 段 | 3 条 | ~4 条 | ~20s |
| 安安 | 1 段 | 5 条 | ~6 条 | ~20s |
| 寝室群 | 5 段 | 8 条 | ~13 条 | ~50s |
| 外卖小哥 | 1 段 | 0 | ~1 条 | ~5s |
| self-3y | 3 版 | 0 | ~3 条 | ~150s |
| **合计** | | | **~90 条** | **~835s** |
### 总计(现行部署)
- 游戏引用 **184** 个 MP3`audio/mp3/`,已与 `story.js` / `engine.js` 对齐)
- TTS 对白 **169** 条(`tts/*_wav_v1.mp3`
- BGM + 铃声 **8** 条 · 音效 **4** 条 · 环境音 **3**
- v5 克隆产物归档 **177** 条(`audio_2/tts/v5_cloned/`,含台词表全量 + self-3y + 系统 memo
- UI 音效:13 种,全部 Web Audio 合成
---
## 10. 项目文件结构
```
blind/
├── game/
│ ├── index.html
│ └── js/
│ ├── story.js # 16 条消息 + 音频引用(*_wav_v1.mp3
│ ├── engine.js # 系统语音、结局、BGM/环境音
│ ├── audio.js # 音频管理 + UI 合成音效
│ └── …
├── audio/
│ ├── batch_tts_voxcpm.py # 台词表 LINES + ROLES 语速
│ └── mp3/ # 游戏运行时音频(184 文件)
│ ├── tts/ # 169 条对白
│ ├── music/ # BGM + 铃声
│ ├── sfx/ # 叙事音效
│ └── ambience/ # 环境音
├── voice/ # 各角色参考音频(v5 选定 *_ref.wav
├── audio_2/ # TTS v4/v5 流水线
│ ├── 项目状态说明.md
│ ├── voice_style_prompts.py
│ ├── voice_sample_prompts.py
│ ├── scripts/ # 03 试听 · 04 克隆部署 ★
│ └── tts/
│ ├── v4_voice_samples/ # 选定参考音色
│ └── v5_cloned/ # 正式产物 + clone_manifest.tsv
├── 林夏.md
├── 执行文档_音频素材制作.md
└── 技术报告_音频资源制作.md
```
TTS 服务(独立部署):`/home/xsl/tts-server/`(端口 **8002**
---
## 11. 风险与对策
| 风险 | 对策 |
|------|------|
| VoxCPM2 长情绪戏(妈妈 4 分钟)质感不够 | 分段录制 + 后期拼接,预留调参时间 |
| 声音克隆音色不够区分 | Voice Design 试听多变体 → 人工选定 → 克隆锚定;男声角色单独标注 |
| 60+ 分支语音管理复杂 | batch_tts_voxcpm.py 统一管理,命名规范 |
| 手势误判导致选错 | 滑动阈值 30px + 40°范围;双击间隔 250ms 内 |
| 玩家不理解手势含义 | MSG-02(小美教学关)强引导,每步正向反馈 |
| 女性玩家代入感 | 编剧组需有 24 岁左右女性参与 |
| 题材敏感 | 见下方"情绪安全设计"专节 |
### 情绪安全设计
本作涉及分手、裁员、独居、深夜来电、家庭创伤回忆等高情绪浓度内容,必须认真对待玩家的心理安全。
**开场内容提示**(游戏启动后、第一条消息前):
- 系统语音播报:"本游戏涉及情感压力、职场失落、家庭关系等主题。如果你当前情绪状态不佳,建议择日再玩。游戏过程中,两指长按可暂停,三指点击可直接退出。"
**游戏中安全机制**
- **随时暂停**:任何状态下两指同时长按 1 秒,游戏暂停,系统语音:"游戏已暂停。单击继续,三指点击退出。"(两指长按与单指双击完全不同,避免误触跳过)
- **安全退出**:三指同时点击,系统语音确认后直接跳转到结尾支持资源页。
- **长按状态播报**:让玩家随时确认自己在哪,减少焦虑感。
**结尾支持资源**(任何结局播放完毕后):
- 系统语音:"感谢你陪林夏度过这一夜。如果你现在需要和人说说话:全国 24 小时心理援助热线 400-161-9995;北京心理危机研究与干预中心 010-8295-1332。"
- 音频播放完毕后保持在屏幕上(即使是纯音频游戏,此处允许显示文字——用户此时可以看手机了,屏幕"修好了"是一个隐喻)。
**叙事安全原则**
- 林夏的自救不完全寄托在妈妈或男性角色身上——妈妈提供的是"被记住"的感觉,周南提供的是"被看见"的感觉,但最终让林夏起身烧水、给阿哲寄耳机的,是她自己的选择。
- MSG-16 妈妈语音中"那年夏天我没死"这句表述需谨慎处理:妈妈的语气是平静追忆而非戏剧化,TTS 合成时 emotion 设为 `gentle, peaceful`,避免制造突然的情绪冲击。
---
## 12. 制作流程
| 阶段 | 关键交付 | 状态 |
|------|---------|------|
| **剧本定稿** | 16 条全脚本 + 5 结局完整对白 + 所有分支台词 | ✅ |
| **参考音色准备** | v4 试听选定 13 角色参考 wav | ✅ |
| **VoxCPM2 v5 批量合成** | 177 条克隆 + 部署 `audio/mp3/tts/` | ✅ |
| **BGM + 环境音 + 音效** | ACE-Step 本地生成,游戏引用 15 条 | ✅ |
| **程序集成** | story.js 对接 `*_wav_v1.mp3` | ✅ |
| **测试** | 全 5 结局通关 + 各角色听感迭代 | 待 playtest |
---
## 附录 · 一句话给主创
> "这不是一个让女孩们更难过的游戏。这是一个让她们在难过的时候,知道有人也曾这样难过、并且活下来的游戏。"