Concat · 中文配音扩展(DUB)
施工图纸:在哪儿加什么,怎么实现
基线 v0.2.8 · 分支 amd @ fd415e0
A 区 · 新路径 3 个 · ~1560 行
B 区 · 上游接线 4 个文件 / 8 行
图纸 7 张
一句话:业务逻辑和界面全部装进上游不存在的三个路径(crates/concat-dub/、ui/dialogs/dub.slint、src/dub_glue.rs),
上游源码只动 4 个文件、8 行,每行带 // DUB: 尾巴。升级时新路径永远不冲突,8 行重放一次即可。
操作模型是「以字幕条为单元」:右键一条字幕就能 译 / 配 / 对;框选多条后右键 = 批量版;整条字幕轨由面板「范围」下拉指定(上游轨道头没有右键菜单)。
A 区 · 新增文件(不冲突)
B 区 · 上游 8 行接线(重放)
① ② ③ 红色圈号 = 图上标注,下文表格给实现
图 1两区分离:改哪儿、加哪儿
左边是必须动的上游文件(琥珀色),右边是全新路径(青色)。中间的箭头就是 8 行的全部意义:上游只留两个"插座"——注入菜单行、接收菜单点击。
图 1 · 左边 4 个文件 8 行是唯一需要"重放"的改动;右边三个路径上游永远不会有,git 三方合并只会看到"我方新增"。
图 2界面草图:六个落点全在主窗口里
不改停靠布局、不加新面板(PaneKind 只有 5 个值,牵一发动全身)。所有操作从时间线上右键一条字幕发起(选一条 = 单条,框选多条 = 批量),设置面板是浮层。
图 2 · 关键点:① ② 都在"右键"上,不需要工具栏按钮;③ 配音落在新建的「配音轨」;⑥ 面板是浮层,不占停靠位。
| 标注 | 界面位置 | 怎么实现 |
| ① | 时间线上单条字幕右键 |
上游已有 on_clip_context → clip_action(id, action)。A5 在 clip_action 的 match 里拦 "dub." 前缀,交给 dub_glue::act(studio, id, action)。菜单行由 A4 注入(menu_rows 只对 ClipKind::Text 返回)。 |
| ② | 框选 / Shift 点选多条字幕后右键 |
上游右键只挂在 clip 上(lanes.slint:555),轨道头没有右键菜单——不做轨头菜单,整轨改由面板「范围」下拉指定。act() 读 studio.selection: Vec<String>(studio.rs:845,pub 字段),右键的那条不在选中集里就只取它自己;多条结果包一个 Command::Batch(一次 undo)。 |
| ③ | 新建的「配音轨」(名字不叫 A1,避免和锚点编号混淆) |
AddMedia(origin = MediaOrigin::Speech)+ AddClip{ track_id, start: clip.start, ripple: false };单条重配走 ReplaceClipMedia,不新增 clip。 |
| ④ | 检查器·文字字段 |
第 5 步手工修正,现有能力,一行不改。注意:dub 侧改字必须走 text.rs::with_content,否则样式被重置(图 5 ①)。 |
| ⑤ | V1 轨道头的静音 |
SetTrackFlag { track_id, flag: TrackFlag::Mute, value: true }。对齐完成后由面板上的「静音原声」勾选框触发。 |
| ⑥ | 配音设置浮层 |
A7/A8 挂 DubDialog,可见性由 app.dub.open 控制;菜单底部「配音设置…」把它置 true。不加新菜单栏项、不加新 PaneKind。 |
回到你给的 6 步:只有第 2 步是从零写的
把上表翻成你的操作顺序,就能看清工作量落点 —— 第 1、4、5 步一行新代码都不用写,第 3 步和第 6 步是同一套编排的两档范围。
| 步 | 你要的操作 | 谁提供 | 落地命令 / 说明 |
| 1 | 外语素材 → 语音转字幕 |
已有 Transcriber::transcribe(transcribe.rs:435)+ srt_commands(captions.rs:587) |
Batch{ AddTextClip{duration} } |
| 2 | 字幕译成中文 / 指定语言 |
新增 dub.translate → translate.rs(在线 ureq) |
UpdateClip{patch.text},多条包 Batch。唯一从零开始的模块。 |
| 3 | 整轨字幕 → 中文配音 |
已有 Speech::speak(tts.rs:726);新增只是编排 + 语速估算 |
AddMedia + AddClip(范围 = Target::Lane) |
| 4 | 配音对接画面(时长贴合) |
已有 AddClip / ReplaceClipMedia / TrimClip / MoveClips |
同上 + TrimClip;策略在 fit.rs 里三选一 |
| 5 | 手工修正字幕(改字 / 拖时间) |
已有 检查器 / MoveClips / SplitClips |
用户操作,零新代码。dub 侧改字要走 text.rs 保住样式 |
| 6 | 对单条已修正的字幕单独配音 |
新增 dub.speak 的 Target::One |
ReplaceClipMedia(不新增 clip)+ TrimClip。与第 3 步同一套代码,只差范围。 |
图 3右键菜单:三项动词,一个前缀
菜单是 Rust 侧数据驱动的(editor.slint:435 的 menu-items 由 studio.rs:10964 fn menu() 生成),所以加菜单项不改任何 slint、不新增 Glyph。所有新菜单项的 id 统一 dub. 前缀,A5 就靠这个前缀拦下来。
图 3 · 前缀 dub. 是全部接线点:A5 用 a.starts_with("dub.") 一次性拦下所有新菜单项,上游 match 分支一个不改。
图 4设置面板:dub.slint 线框
这是唯一需要手写的界面文件,约 240 行 slint。四项能力:范围、翻译、配音、对齐,加一条进度条。
图 4 · Ⓐ 范围(One / Selection / Lane 三态)、Ⓑ API Key、Ⓒ 语速区间(clamp 0.85–1.20)、Ⓓ 对齐三选一。
图 5数据流:一次右键之后发生什么
上半是「译」,下半是「配 + 对」。两条链路都在后台线程(spawn_in_project),UI 不卡;落地时再把命令一次性交给 studio.apply。
图 5 · 上半 4 个上游命令全是现成的;真正新写的只有 target / translate / speak / fit 四个模块。
图 68 个锚点落在文件树的哪里
琥珀小牌 = 上游改动(8 行);青色 ★ = 新文件,上游永不出现。左边的 [ ] 牌和右边的代码行是一一对应的。
图 6 · 8 个锚点里有 6 个是单行、2 个是两行成对;全部收在 4 个文件内。
图 7升级时为什么不会打架
不是靠"小心",是靠三条可验证的路径事实:新路径上游不存在、通配符让 crate 自动入列、8 行全部带标记。
图 7 · 冲突面被压到 8 行,而且每行都能用 grep "DUB:" 找回来——不靠记忆。
附三个里程碑与当前状态
| 阶段 | 做完什么 | 验收(能被眼睛看见) |
| M1 |
concat-dub 骨架 + text.rs(带单测)+ 单句翻译 + 单句 TTS;A1~A6 接上 |
一条外语视频 → 转字幕 → 右键某条「译成中文」→ 变中文且字号/颜色/描边没变 → 右键「配音这条」→ 多出一条音频 |
| M2 |
多选走 Batch + 三策略 + 面板(A7/A8)+ 原声静音 |
整片框选一次译完,Ctrl+Z 一次全撤;输出一条完整中文配音成片 |
| M3 |
原文/译文双行、失败报告、.env 导入、重试提示 |
断网跑一次,报告说清哪几条没译上,其余照常落地 |
两个需要你定的点(其余都已按上游实际代码定死)
- 翻译服务商:现在按 OpenAI 兼容的
/chat/completions 写,dub.json 里换 base_url + model 就能换家(DeepSeek / 通义 / 智谱都兼容)。若你想用非兼容协议的,translate.rs 要单独适配。
- 配音落在哪条轨:默认"记住上次用的音轨,判空后再放,否则找首个空位"(照抄
voiceover.rs 的做法)。若你希望固定一条专用「配音轨」、没有就自动建,说一声,M2 里改。
本文与 Concat中文配音扩展_施工方案.md 是同一套方案:文档给文字细节与命令,图纸给位置。当前代码一行未动(git status 只多出未跟踪的 transub/ 目录)。