combo 配音流水线 · Goal2
老办法按字符数硬切,会把两句话粘一起、把没有空格的语言整句塌成一坨。 新办法让 codex 读原文决定每行收哪几个词,合成端产一份分行表,烧字端照着摆。 全程只在 -test 线,线上没动。
为什么要改
原来烧字幕的分组逻辑(burn.py group_into_chunks)只做一件事: 数字符数,够一行就切。只有遇到句末标点才强制换行。两个后果:
我们的配音脚本常常没有标点。 没标点 → 不触发强制换行 → 上一句的尾词和下一句的头词被切进同一行。
韩语、日语、泰语词之间不打空格。 ElevenLabs 把它们按单个字返回时间轴,纯字符分组要么把整句挤成一条、 要么从一个词的中间断开(把 시/작 拆两行)。
根因:分组只认字数和句末标点,而我们的输入既没标点、又有无空格语言——正好全踩中。
新方案 · 数据契约
烧字端原本吃两样:词级 SRT(每个词的时间)和视频。 现在多给它第三样——一份分行表 subtitle-line-groups.json。判断放在合成端做(那里有原文当依据), 烧字端只负责照着摆。
脑子在合成端(有原文),烧字端只消费。分行表是词级 SRT、视频之外的第三个输入,不替换前两个。
codex 在哪里、怎么介入
runner 合成完音频、建好词序列后,主动 exit 3 停下来, 在 deploy.json 写 status="needs_linebreak", 把原文 + 词序列交回工作流。codex 读这份请求, 只回答"哪几个词归一行"(一串 word_indices), 不许改字、不许动时间。runner 拿回分行重跑,缓存命中只补这一步。
codex 只在第 ③ 步出现,职责被压到极小:给行边界。改字、配速、SRT 这些它一律碰不到—— 所以它"答错"最多让分行不好看,绝不会损坏音画或时间轴。
# 收到 linebreak-request.json(节选) { "language":"hangul", "request_hash":"190ee7f6…", "segments":[{"script":"광고랑 똑같다 나무로 직접 짓는다"}, …], "words":[{"i":0,"text":"광"},{"i":1,"text":"고"}, …] } # 只回 word_indices —— 不许新增/删词/改时间 { "request_hash":"190ee7f6…", "lines":[{"word_indices":[0,1,2,3,4,5]}, {"word_indices":[6,7,…]}, …] }
确定性护栏
LLM 会抽风,所以它的回答要过三道确定性闸,任何一道不过就退回内置分组, 烧字永不阻塞。
| 护栏 | 挡什么 | 不过怎么办 |
|---|---|---|
| request_hash 绑定 | codex 答的是不是这次的词序列(防错配 / 串单) | 退 fallback_line_groups (老的确定性分组) 标 source=fallback |
| 覆盖校验 | 所有行拼起来必须正好=0..n-1,不漏词不越界不重叠 | |
| 不许篡改 | 词的文字、时间一律以 runner 建的 words[] 为准,codex 动不了 |
分行表里有个 source 字段:codex 真出了就是 codex, 走了兜底就是 fallback——验收时一眼看出断句到底是 LLM 干的还是退化了。
五语种规则
断句的硬规矩是不从一个词的中间断开。可"一个词"在不同语言里靠什么界定不一样:
| 语种 | 词边界靠什么 | 关键规矩 / 反例 |
|---|---|---|
| KO 韩 | 原文的空格=어절 | 同一 어절 的字不分两行 · 别拆 시/작 |
| JA 日 | 语义文节(无空格) | 助词/活用连一起 · 别拆 さっそ/く |
| AR 阿 | 空格分词 + RTL | 整词含变音不破 · 右起折行 · تدريجياً |
| RU 俄 | 空格分词 | 按词,短词不孤立成行 |
| EN 英 | 空格分词 | 按词 + 德语式复合词不硬拆 |
核心规矩:脚本没标点是常态,不是异常。 这时 segments[]——也就是配音逐句生成的那些段—— 就是所有语种的权威句子单位,默认 segment 边界=断句,标点只是次要信号。 codex 读 segments[].script 的语义来分,没标点照样分得出句。
无空格语言(KO/JA)的词级 SRT 本就是"每字一条 cue"——那是给逐字高亮用的时序层; 句子怎么成块,完全由分行表决定。
真机验 · test-only
同一条玩法视频、同一个 v3 多语种音色、无标点多句+带情绪 tag,跑了三单, 覆盖无空格 / 有空格 / RTL 三种最难的形态:
| KO 韩 无空格 | JA 日 无空格 | AR 阿 空格+RTL | |
|---|---|---|---|
| source | codex | codex | codex |
| 段 → 行 | 4 → 9 | 3 → 14 | 4 → 11 |
| 覆盖完整 | ✓ 0..57 | ✓ 0..68 | ✓ 0..24 |
| tag 泄漏 | ✓ 零 | ✓ 零 | ✓ 零 |
| 跨句粘 | ✓ 无 | ✓ 无 | ✓ 无 |
| 词内断 | ✓ 无(程序核 어절) | ✓ 无(语义核文节) | ✓ 无(整词不破) |
| 烧字成块 | line-groups 生效 | 生效 | 生效 |
| 字体 / 豆腐 | Noto CJK KR | Noto CJK JP | Noto Arabic |
烧字日志每单都打出 [line-groups] 生效 (N 行, source=codex) + fallback_used=false——烧字确实按句块摆,不是字符级逐字蹦。

L0 逻辑序 لعبة → حقيقية。画面里 لعبة(逻辑首词)在最右——正确 RTL。

L10 مريحة بلا توقف 超宽自动折两行,每行右起、词序不乱,字母连写正常。
runner: mai-combo-runner-test v6 burn: subtitle-burn-rendi-test v1 workflow: …_mai_test v35 prod 全未动