Files
SoundGame/docs/asset-pipeline.md
T
xsl f5eae6d37c test+docs: 「对白没读完」教训固化到框架(引擎回归测试 + 审计工具 + 四处文档)
- web/harness/engine-voice.test.mjs:引擎 voice 句尾保真三不变式(I1 源未耗尽不得结算 /
  I2 耗尽后排空窗才断开 / I3 零消耗走旧路径),可驱动 SPN mock 模拟真实消耗,
  15 断言全绿;防止未来重构回潮
- tools/audit_voice_tails.py:voice 资产尾部静默审计工具入库(含判读口径头注:
  尾静默紧=裁剪工艺≠剪字,句尾完整性验收在引擎层)
- 文档:HANDOVER(§1.4 回归清单 / §5.3 质量手段 / §6 已解决账)、roadmap §4
  新增全游戏「句尾完整性」真机听测项、asset-pipeline QC 口径、methodology §5
  度量盲区教训(媒体播完类问题转写层无效,必须真人耳测+引擎断言)、
  web-engine-api voice 自然结束语义
- 修正:结束判定改用 filter.sourcePosition(SimpleFilter 单调精确计数器);
  10 套件全绿(8 playtest + guide + engine-voice)
2026-09-06 16:08:18 +08:00

143 lines
7.1 KiB
Markdown
Raw 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.
# SoundGame — 资产生产流水线
三类音频资产(对白 voice / 音效 sfx / 音乐 bgm)的完整生成与再生成流程。
所有脚本都在 `app/src/<flavor>/gen/` 下,可随时复现全部资产。
通用约定:
- **GPU 任务必须串行**:`flock /home/ubuntu/SoundGame/.gpu.lock <命令>`
(TTS 与 Stable Audio 3 共用一块 RTX 3090)。
- 环境脚本:`source env.d/tts.sh`(HF_HOME + hf-mirror 镜像)、
`source env.d/android.sh`(ffmpeg 在 PATH)、`source env.d/sfx.sh`(HF_TOKEN,
含密钥不入库,模板见 `env.d/sfx.example.sh`)。
- 中间产物 wav 输出到各 gen 目录或 `/tmp/slg/`(不入库),最终 ogg 进
`app/src/<flavor>/assets/audio/{bgm,voice,sfx}/`。
## 1. TTS 对白(Qwen3-TTS CustomVoice)
引擎与用法详见 `env.d/tts.md`(CustomVoice 预置音色 + instruct 情感指令、
VoiceDesign、预置音色表)。流水线层面:
```bash
source env.d/tts.sh
# 《声命线》:48 条,6 条一批;输出 /tmp/slg/wav/,可传文件名参数只重生指定条目
flock .gpu.lock .venv-tts/bin/python app/src/storyline/gen/gen_voice.py [文件id...]
bash app/src/storyline/gen/convert.sh # wav -> ogg 进 assets(含电台带通效果)
# 《声东击西》:33 条,逐条生成;输出 gen/wav_voice/
cd app/src/reflex/gen
flock /home/ubuntu/SoundGame/.gpu.lock /home/ubuntu/SoundGame/.venv-tts/bin/python gen_voice.py
# 《暗室》:41 条,8 条一批,统一峰值归一 0.89;输出 gen/wav/voice/
flock .gpu.lock .venv-tts/bin/python app/src/escape/gen/gen_voice.py
```
- 每条生成后自动校验:**时长 > 0.3s 且峰值 > 0.05**,不达标直接 assert 失败。
- **尾部裁剪与句尾完整性(2026-09 口径)**:管线峰值归一 + 静音尾裁剪很紧,
绝大多数 voice 文件尾部静默 <120ms 属预期,**不等于剪字**。「句子是否读完」
的验收在引擎层(源消耗位置判定 + 300ms 排空窗,见 `web/engine/audio.js
_finishVoice` 与 `web/harness/engine-voice.test.mjs`);资产侧怀疑剪字时用
ASR 抽查末字/语气词核对,量化工具 `tools/audit_voice_tails.py`(判读口径
见其文件头注)。
- 音色分配见各游戏文档(docs/games/*.md):旁白 Uncle_Fu、林晚 Serena、
猎狗 Dylan、小满/广播 Vivian;escape 全员 Uncle_Fu 靠 instruct 区分语气。
- 已知不稳条目:reflex 的 `num_6`("六"),单独用
`app/src/reflex/gen/fix_num6.py` 重试生成(最多 4 次取时长合格的一条)。
- 改台词只需改 gen_voice.py 里的 `LINES` 表再重跑;文件名即游戏代码引用的 id。
## 2. 音效(Stable Audio 3 + 程序合成)
### 2a. Stable Audio 3 small-sfx(主力,AI 生成)
```bash
source env.d/sfx.sh # HF_TOKEN(gated 模型;本机直连不通,实测 gated+token 走 hf-mirror 可用,2026-09-02)
cd tools/stable-audio-3
uv run stable-audio --model small-sfx \
-p "footsteps on gravel, slow walking" --duration 3 \
-o /home/ubuntu/SoundGame/tools/samples/sfx_test.wav
```
- 提示词必须英文;`--duration` ≤ 120s;`--steps` 默认 8;small 模型峰值显存
约 2.4GB。批量:`-p "p1" "p2" --duration 3 5`。
- 首次使用需完成 gated 许可 + token 流程,详见 `env.d/sfx.md` / `docs/setup.md`。
- 生成后用 ffmpeg 转 ogg 进 assets(规格见下文"资产规格")。
### 2b. 程序合成(保底/已在线方案,numpy + soundfile,纯 CPU 无需 GPU 锁)
| 游戏 | 脚本 | 输出 |
|---|---|---|
| storyline | `app/src/storyline/gen/gen_sfx.py` | `/tmp/slg/wav/sfx/`(24kHz),`convert.sh` 转 ogg |
| reflex | `app/src/reflex/gen/gen_sfx_bgm.py` | `gen/wav_sfx/`(44.1kHz),`convert_audio.py` 转 ogg |
| escape | `app/src/escape/gen/gen_sfx.py` | `gen/wav/sfx/`(44.1kHz),手动 ffmpeg 转 ogg |
当前线上资产全部为程序合成版;Stable Audio 3 环境已就绪,可按
README 待办逐步替换(替换时保持同名同目录,游戏代码零改动)。
## 3. BGM
### 3a. Mureka API(storyline / escape)
```bash
source env.d/mureka.sh # MUREKA_API_KEY(含密钥不入库,模板 env.d/mureka.example.sh)
python3 tools/mureka_gen.py \
--prompt "dark ambient horror game background music, low drone, ..., seamless loop, instrumental" \
--out tools/mureka_out/storyline # 提交→轮询→下载全部候选 flac
python3 tools/mureka_gen.py --task-id <id> --out tools/mureka_out/xxx # 只轮询已有任务
```
后处理(两条已上线 BGM 相同参数,记录见 `tools/mureka_log.md`):
```
裁剪前 120s → 首尾 3s acrossfade(tri) 无缝循环化 → loudnorm I=-20/TP=-2/LRA=11
→ aresample 44.1kHz 立体声 → libvorbis -q:a 3
```
成品 117.00s,保持原文件名(`dark_drone.ogg` / `drone.ogg`)直接替换 assets。
原程序合成版备份在 `tools/bgm_backup/`。
**成本参考**:`POST /v1/instrumental/generate` 每条任务 **$0.03**(一次出 2 个
候选),项目预算上限 $0.50,已用 $0.06(2 条任务均一次成功)。每次调用必须
登记到 `tools/mureka_log.md`。
### 3b. 程序合成(reflex,有意保留)
`app/src/reflex/gen/gen_sfx_bgm.py` 的 `make_pulse_bgm(bpm, bars=8)`:
心跳底鼓 + 低音脉冲音序 + 镲噪声 + 不协和 pad,100/128/152 BPM 三档各 8 小节。
玩法要求 BPM 随波次精确切换,AI 生成曲目 BPM 不精确会破坏机制,**不要用
Mureka 替换 reflex 的 BGM**。
## 资产规格
| 项 | 标准 |
|---|---|
| 容器/编码 | ogg vorbis(`-c:a libvorbis`) |
| 码率 | voice/sfx `-q:a 4`;bgm `-q:a 3`(控制体积) |
| 采样率 | TTS 源 24kHz;程序合成 reflex/escape 44.1kHz;BGM 统一 aresample 44.1kHz |
| BGM 响度 | loudnorm **I=-20 LUFS / TP=-2 / LRA=11** |
| BGM 循环 | 首尾交叉淡化无缝循环(Mureka 版 3s acrossfade;合成版 2s/4s 等功率交叉) |
| 时长校验 | 每条 > 0.3s(数字语音等短条目除外,fix_num6 放宽到 0.3~1.5s 区间判定) |
| 峰值校验 | 生成后 assert 峰值 > 0.05(防静音/失败条目混入) |
| 峰值归一 | storyline 合成 sfx 0.85;escape voice 0.89 / sfx 0.85(环境音 0.55-0.7);reflex voice 0.6 / sfx 0.7-0.95 |
## 目录约定
```
app/src/<flavor>/
assets/audio/{bgm,voice,sfx}/*.ogg # 最终资产(入库)
gen/ # 生成脚本(入库)+ 中间 wav/字体(不入库)
tools/
mureka_gen.py / mureka_log.md # Mureka 调用脚本与台账(入库)
mureka_out/ bgm_backup/ samples/ # 原始下载/备份/冒烟输出(不入库)
stable-audio-3/ # SFX 仓库(代码入库,.venv 不入库)
```
引擎按 id 前缀(`bgm/` `voice/` `sfx/`)分发到三个播放层,文件名即代码中的
引用 id;新增资产保持"同名替换、新增先改代码引用"的原则。
## 封面图
- storyline:`gen/make_cover.py`(PIL,字体 /tmp/slg/NotoSansSC.ttf)
- escape:`gen/gen_cover.py`(PIL,字体 gen/font.otf,不入库需自备)
- reflex:封面无独立生成脚本(骨架渐变图 + 后期替换)
- 统一输出 `app/src/<flavor>/res/drawable/cover.png`,1080x1920,文件名固定。