combo 配音流水线 · Goal2

智能断句:让字幕在该断的地方断

老办法按字符数硬切,会把两句话粘一起、把没有空格的语言整句塌成一坨。 新办法让 codex 读原文决定每行收哪几个词,合成端产一份分行表,烧字端照着摆。 全程只在 -test 线,线上没动。

3
烧字的输入(原本 2)
5
语种规则 KO·JA·AR·RU·EN
3 / 3
真机验通过 KO·JA·AR
0
线上改动

为什么要改

老办法:数着字数切,不看句子

原来烧字幕的分组逻辑(burn.py group_into_chunks)只做一件事: 数字符数,够一行就切。只有遇到句末标点才强制换行。两个后果:

① 跨句粘

我们的配音脚本常常没有标点。 没标点 → 不触发强制换行 → 上一句的尾词和下一句的头词被切进同一行。

② 无空格语言塌成一坨

韩语、日语、泰语词之间不打空格。 ElevenLabs 把它们按单个字返回时间轴,纯字符分组要么把整句挤成一条、 要么从一个词的中间断开(把 시/작 拆两行)。

根因:分组只认字数和句末标点,而我们的输入既没标点、又有无空格语言——正好全踩中。

新方案 · 数据契约

给烧字端加一个"分行表"输入

烧字端原本吃两样:词级 SRT(每个词的时间)和视频。 现在多给它第三样——一份分行表 subtitle-line-groups.json。判断放在合成端做(那里有原文当依据), 烧字端只负责照着摆。

SYNTH SIDE combo runner + voiceover-align LLM JUDGE codex 断句 读原文 → 还哪几个词一行 3RD INPUT line-groups .json BURN SIDE subtitle-burn 照分行表摆句块 exit3 交回原文+词 只还 word_indices --line-groups-url ■ 墨蓝 = 断句脑子在哪 ■ 暖灰 = 数据 / 消费端 原词级 SRT + 视频两条输入照旧,分行表是新加的第三条

脑子在合成端(有原文),烧字端只消费。分行表是词级 SRT、视频之外的第三个输入,不替换前两个。

codex 在哪里、怎么介入

合成跑到一半"交回"给 codex,只问一件事

runner 合成完音频、建好词序列后,主动 exit 3 停下来, 在 deploy.jsonstatus="needs_linebreak", 把原文 + 词序列交回工作流。codex 读这份请求, 只回答"哪几个词归一行"(一串 word_indices), 不许改字、不许动时间。runner 拿回分行重跑,缓存命中只补这一步。

① 合成 + 建词 build_words ② exit 3 交回 needs_linebreak 写 linebreak-request ③ codex 断句 读原文语义 还 word_indices ④ 校验 hash 对 + 覆盖全 不动字 / 时间 ⑤ 出分行表 source=codex 校验失败 退确定性分组 source=fallback ok → 喂烧字 墨蓝 = codex 真正介入的两步;实线 = 正常路;虚线 = codex 不可用/答错时的兜底

codex 只在第 ③ 步出现,职责被压到极小:给行边界。改字、配速、SRT 这些它一律碰不到—— 所以它"答错"最多让分行不好看,绝不会损坏音画或时间轴。

codex 收到什么、回什么

# 收到 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,…]}, …] }

确定性护栏

codex 是建议,不是裁判

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
sourcecodexcodexcodex
段 → 行4 → 93 → 144 → 11
覆盖完整✓ 0..57✓ 0..68✓ 0..24
tag 泄漏✓ 零✓ 零✓ 零
跨句粘✓ 无✓ 无✓ 无
词内断✓ 无(程序核 어절)✓ 无(语义核文节)✓ 无(整词不破)
烧字成块line-groups 生效生效生效
字体 / 豆腐Noto CJK KRNoto CJK JPNoto Arabic

烧字日志每单都打出 [line-groups] 生效 (N 行, source=codex) + fallback_used=false——烧字确实按句块摆,不是字符级逐字蹦

AR 抓帧:RTL 顺序对、连写正常、无豆腐

AR 单行 RTL

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

AR 折行 RTL

L10 مريحة بلا توقف 超宽自动折两行,每行右起、词序不乱,字母连写正常。

runner: mai-combo-runner-test v6 burn: subtitle-burn-rendi-test v1 workflow: …_mai_test v35 prod 全未动

test-only · prod(runner v14 / burn v18 / align v18)未动 · 待 RU+EN 补完五语后提 git · 生成于 combo 配音 Goal2 真机验