# 自动化配音（FishAudio TTS）

把文案丢进去，自动按**自然段**整段合成（段级长请求保音色），再拼成一整条 MP3。
这套系统是怎么一步步调出来的 → `创造全流程.md`。
写文案前先看 → `文案写作规范.md`（怎么写能一次过 0.95，不浪费 roll）。

## 用法

1. 把文案粘贴进 `文案.txt`（不用自己断句）。
2. 先预览分句（**不调用 API、不耗额度**）：
   ```
   python 配音.py --preview
   ```
   分句结果会打印出来，也写到 `output/分句预览.txt`。同时自动跑两道预检：**版式**（段<40 有效字 / 段内切碎=句号间均<8 字——天花板定律：只有长段保质量）+ **读音风险**。觉得没问题再继续。
3. 正式合成：
   ```
   python 配音.py
   ```
   - 每句一个文件：`output/001.mp3`、`002.mp3` …
   - 整段拼好的：`output/合并.mp3`

指定别的文案文件：`python 配音.py 我的稿子.txt`
格言体/短段密集的稿子：`python 配音.py 文案.txt --minchars 55`——全文并成 90~140 字大段保 0.95（之后重拼也带同参数：`python 合并.py --minchars 55`）

## 合成粒度

默认**整段合成**（`synth_unit: paragraph`，段级长请求 + 行尾句号化——短文本请求是乱码之源，见创造全流程）：每个自然段是一个合成单元，段内换行＝句号停顿（~0.31s），段间空行＝0.45s，章标题后＝1.0s；不足 25 有效字的段自动并入相邻段。句号/逗号级的细切只用于 SRT 字幕分行和预览，不影响合成请求。
数字里的小数点 / 千分位（如 `3.14`、`1,000`）不会被误断。

## 配置 `config.json`

| 字段 | 说明 |
|---|---|
| `api_key` / `voice_id` | FishAudio 密钥 / 你的声音模型 ID（已填好） |
| `model` | 模型，当前 `s2-pro` |
| `temperature` / `top_p` / `repetition_penalty` | 稳定性，越低越稳、越保守 |
| `prosody.speed` | 语速，1=正常，1.1 略快，0.9 略慢 |
| `prosody.volume` | 音量增益（dB） |
| `mp3_bitrate` / `sample_rate` | 输出音质，当前 192k / 44100 |
| `merge` | 是否拼成一条（true/false） |
| `gap` | 句与句之间插入的静音秒数，0=不插。想要句间停顿就设 0.3 之类 |
| `parallel` | 并行施工：`workers` 同时干几段（3），`roll_width` 一波并发 roll 几版（2），`api_concurrency` 在途请求上限（4） |

## 声纹质检（像本人分数）

每段合成后，本地声纹模型自动算与你音色锚点的相似度（**像本人分数**），判级：

- **绿**：机器替你听过了，不用复听
- **黄**：机器拿不准（含 <2.5 秒短段——短段声纹天然测不准，永不自动重roll），建议人工听
- **红**：明显跑偏 → **当场自动重roll**（上限 `qc.auto_reroll` 次，roll一版检一版，保留分数最高的），有总额度熔断（`qc.reroll_budget`）

质量优先模式（当前配置）：`qc.target_sim=0.95`，**一律 0.95 无例外**（2026-06-11 定）——凡低于 0.95 的非短段都会自动 roll（上限 10 次/段，预算=全文字符×5），到 0.95 就停，到不了就取最高分版并警示。**段够不着 0.95 的正解是并段加长（`--minchars`），不是接受低分**。自动 roll 的所有版本（含初版）都留档在 `output/roll留档_NNN/`，文件名带分数，可随时审计机器的取舍。

两条看分数时要知道的事：
- **⚠预算尽**：roll 预算被先完成的段烧光时，没拿到足额 roll 的段会在段尾和收尾汇总里标 ⚠预算尽——这种段的分数是"凑合"不是"天花板"，补法 `python 重roll.py <段号> 6`（单段不占批次预算）
- **分数突然整体掉档先查版式和段长**：实案《命运的排除法》——把短句逐行切碎（句号密度翻倍）导致 5/7 段掉出 0.95 且耳测明显掉档；恢复"原文标点=停顿地图"的流动行后 5/7 段回 0.95+；剩余低分段用 `--minchars 55` 并成 90~140 字大段后**全部 0.95+**（"文体天花板"被证伪：0.918 顶死的格言段并入 134 字大段直接 0.960——是段长效应，不是文体效应）

另两个稳定性机制：
- **短句并段**（`min_unit_chars=25`，按**有效发音字数**计、标点不算）：不足 25 字的段链式并入下一段（末段并入上一段；章节标题两侧都不可并）。实验依据：声纹分数天花板与段长强相关，<25 字段怎么 roll 都到不了 0.95，并段是治本
- **目标分一律 0.95**（`qc.target_tiers=[[0,0.95]]`，2026-06-11 用户定：0.93 绝对不行，金句也不例外）；章节标题只在判红时 roll。短段够不着 0.95 的唯一正解是**并段加长**（`--minchars`），不是降目标
- **读音替换**（`pronounce`）：如 `{"AI": "诶爱"}`，只改送给 TTS 的发音，字幕/预览仍是原文
- **ASR 错读闸门**：每版合成自动转写、对照发音文本，漏读/多读/读成别的音报"错读嫌疑"并参与 roll 挑版（罚分 0.02/处，比韵律 0.01 重：带嫌疑的版本要明显更像才能赢）。同音异字（教验↔校验）、儿化音、的/地/得（ASR 书面化归一）已自动宽恕。注意：声调型多音字错读会被 ASR 的语言模型"纠正"回正字，只能部分捕获——读音的最终裁决仍是你的耳朵
- **声调哨兵**（错读闸门的补盲插件）：对风险库 `tone_sentinels` 列管的字（如"不一定"的"不"），按字级时间戳截出该字的音频，直接测**基频降幅**验收四声——判据由对照实验定标：**降幅不足 10%（浅降/平调/上升）都算嫌疑**，因为耳朵把浅降就听成二声（用户定罪版实测仅 -5%/-7%，置信四声 -20%~-28%）。ASR 看不见的声调错读，从此逐候选被声学验收、带罚分挑版自动避开（实案 2026-06-12："不一定"读 bú 且 roll 不愈，催生此哨兵）。新声调错读经耳确认后往 `tone_sentinels` 加一条即列管
- **听点**：预检命中 + 错读嫌疑统一存到 `output/听点.json`，跑完只听这几处即可（见下"听点反馈"）

跑完会打印 🟡🔴 复听清单并写 `output/质检报告.txt`。锚点已冻结（音色基准锁定）：绿段不再滚入锚点，判级基准恒定、与批次顺序无关。

- 锚点初始化（用 output/ 里已认可的段）：`python 质检.py --init`
- 给任意文件打分：`python 质检.py output/033.mp3`
- 锚点存在 `models/锚点.npz`，声纹模型 `models/campplus_zh-cn_16k.onnx`（本地运行，零费用）

## 反馈训练（自迭代：越用越懂你的耳朵）

每次听完，用一条命令把你的判断喂给系统：

```
python 反馈.py 4 最像     # 认可第4段 → 记台账（锚点已冻结，不动音色基准）
python 反馈.py 4 不像     # 否决 → 记台账（冻结期不入负样本库；台账=日后解冻/重训依据）判罚
python 反馈.py 4 读错     # 读音问题 → 只记台账（读错≠音色不像，不污染声纹库）
python 反馈.py 4 听点对   # 该段听点听过没问题 → 规则误报统计+1（误报≥5次且零真错的规则自动静默）
python 反馈.py 4 听点错 行业  # 听点真错（词可省略=该段全部）→ 计真错；ASR实锤案例自动写入风险库
python 反馈.py <mp3路径> 最像   # 对比集里的任意文件也行
python 反馈.py --status   # 看锚点/负样本/台账规模和当前阈值
```

听点反馈是预检系统的回流通道：你裁决得越多，预检越准、越安静——这是风险库的自收敛机制。

机制：判级用冻结时点的锚点+负样本库（候选若"更像负样本而非锚点"直接降级，阈值由锚点分布自适应）。**锚点已冻结**：新的认可/否决只进台账（`models/反馈台账.jsonl`），作为日后解冻或重训音色时的依据。听点反馈不受冻结影响，照常回流风险库。

## 重roll（已升级：自动挑最像）

```
python 重roll.py 2        # 第2段生成4版 → 自动按像本人分数排序 → 最高分直接覆盖 002.mp3
python 重roll.py 2 6      # 6版
python 重roll.py 2 --keep # 只生成打分不覆盖，自己耳朵挑
```
覆盖后 `python 合并.py` 重拼。屡roll不绿通常是文本拗口，改一两个字比再roll十次管用。

## 呼吸处理（三层，全自动）

1. **源头**：默认参考音频已净化（呼吸剪为静音，`models\参考音频.mp3`；原版在 `_原始备份`）——合成呼吸基线减半，音色不损
2. **检测**：每段自动检测"连喘模式"（相邻呼吸间隔 <0.45s，实案"呵、呵"）→ 韵律⚠ → 触发 roll 换版
3. **成品**：`python 加BGM.py`（标准浓度 0.18，`--vol 0.12` 轻）→ `合并_带BGM.mp3`，掩盖剩余呼吸/轻微电音尾。曲目由 `--bgm <路径>` 提供后自动收进 `models\BGM标准.mp3` 永久保管（**当前 models 里没有曲目**，首次跑会提示给曲）

⛔ 永远不要对合成音频做去呼吸/衰减处理（实测违和，已废弃）。

## 章节标题（全自动）

标题自动并入下文凑足 40+ 字合成（音色 0.95+），1.0s 章节停顿由 `悬停.py` 自动对标题尾拉伸补回——无需任何标记或手工步骤。

## 金句悬停（停顿拉伸术）

**金句永远埋在长段里，不为金句分行/分段**（0.95 红线优先，2026-06-11 定案）。文案里给金句段开头标 `!`（每批至多 1~2 处）——该段照常被并入大段合成，悬停会自动在大段内部定位金句尾做手术（段中拉伸已实战验证）。正常跑配音后执行：

```
python 悬停.py     # 定位金句尾句号停顿, 拉伸到 0.65s 悬停(音频手术, 零API成本)
python 合并.py     # 重拼
```

原理：金句并在长段里合成保住音色（0.95±），悬停由音频层拉伸句号静音实现——音色与节奏解耦。

## 字幕

合成 / 重拼时自动产出 `output/合并.srt`：段级时间精确打点（无需 Whisper），段内按字数比例细分到句，行尾不留标点。

## 并行施工（提速 ~2.7×）

段与段互不依赖（每段独立请求、锚点冻结后打分与顺序无关），所以默认**3 个工人抢队列**（谁空谁领下一段，长段先开工）+ 段内**一波 2 路并发 roll**（roll 纵深变横宽），全局限流闸控制在途请求数、撞 429 自动加倍退避。干跑实测：同样 44 次调用、同样字符成本，131s → 48s。

- 控制台完成顺序是乱的（段号都在，看号不看序）；产物（报告/听点/台账/合并）与串行**逐字节一致**
- 排查问题想回老路：`python 配音.py 文案.txt --serial`
- 防呆：锚点未冻结时自动退回单工人（绿段滚锚点会随完成顺序漂移）

## 批次指标（复利仪表盘）

每批合成自动追加一行 `models\批次指标.jsonl`：送 TTS 字符数（≈成本）、roll 次数、**一次过率**、**复听负担分钟数**、像本人均值/最低值；质检报告末尾同步附一行。按 `文案写作规范.md` 写稿 → 一次过率应该越来越高、复听负担越来越低——这两条曲线就是系统在复利的证据。

## 语料导出（平台无关的对冲资产）

```
python 语料.py            # 扫描全部成品夹 -> models\语料清单.jsonl + 汇总表
```

把所有（段文本、段音频、像本人分数）三元组汇总成数据集清单。重训 voice、换 TTS 引擎、评测新模型时，拿这份清单立刻取用全部已认可语料——像本人 ≥0.95 的段就是重训级数据。

## 说明

- 某句失败会自动重试 3 次，仍失败就跳过并继续，最后告诉你哪几句没成；可改完再单独重跑。
- 合并依赖 ffmpeg（本机已装）。
- `config.json` 里有密钥，别外传 / 别上传到公开仓库。
- 质检依赖 `pip install sherpa-onnx numpy`（已装）。`config.json` 的 `qc.enable=false` 可整体关闭质检。
