feat(diarization): 支持字幕说话者区分 - #414
Conversation
buxuku
left a comment
There was a problem hiding this comment.
非常感谢该 PR ,它是本工具的功能规划之一。之前对该功能有过一个比较完整的产品规划,它可以基于角色分离数据来增加字幕校对和配音的能力。
总体评价
优点
- 说话者分离作为引擎无关的后处理,兼容 whisper / faster-whisper / sherpa / 云端 ASR,比只在 sherpa 听写路径内集成更务实
- 挂载在翻译之后、sidecar 写入之前,避免
[Speaker N]污染翻译 prompt,顺序正确 alignment.ts纯函数设计清晰,重叠说话者处理(Speaker 1 + Speaker 2)有考虑- 独立
utilityProcess+ 取消信号传递,与现有 sherpa worker 模式一致 - 失败降级策略合理:模型缺失 / 推理失败时保留原字幕,任务继续
主要问题
当前实现更像「能跑通的技术 MVP」,在流程可见性、文案完整性、导出策略、产品边界上还需要补齐,否则用户容易遇到「功能开了但看不到效果」或「交付物被污染」的体验问题。
一、当前 PR 需要调整的内容
1. 文案与 i18n 问题
1.1 已发现的 i18n bug
SpeakerDiarizationModelSection.tsx 中「打开模型目录」按钮使用了 t('openModelsFolder'),但该 key 在 resources 命名空间下不存在(仅存在于 modelsControl 或 dubbingBlock.openModelsFolder),导致界面显示 raw key openModelsFolder。
同类问题还有 'Open folder failed' 硬编码英文,应改为 i18n key。
建议:
- 在
renderer/public/locales/zh/resources.json和en/resources.json根级补充openModelsFolder、openFolderFailed - 或改为与
ModelLibrarySection一致的命名空间用法 - 合并前跑
npm run check:i18n确保通过
1.2 产品用词不统一
当前中英文案混用多种表述:
| 位置 | 当前文案 | 问题 |
|---|---|---|
resources.json |
「说话者分离(可选后处理)」 | 与任务页、行业习惯不一致 |
tasks.json |
「区分说话者」 | 与资源页不一致 |
| 英文 | Speaker diarization / Distinguish speakers | 尚可,但中英文内部需对齐 |
产品术语决策:统一使用「角色分离」
与讯飞听见、讯飞开放平台等国内转写产品的叫法对齐(API 参数 roleType = 角色分离)。技术实现仍是 Speaker Diarization,但面向用户的文案建议统一为:
| 层级 | 中文 | 英文 |
|---|---|---|
| 功能名 / 模块名 | 角色分离 | Speaker diarization |
| 任务开关标签 | 角色分离 或 开启角色分离 | Speaker diarization |
| 阶段名(任务轨道) | 角色分离 | Speaker diarization |
| 输出物 | 角色标签(如 [Speaker 1]) |
Speaker labels |
| 配置摘要 | 「2 人」/「自动判断人数」 | 沿用现有 summary 结构 |
需严格区分的术语(文案中避免混用):
| 不要用 | 原因 |
|---|---|
| 指伴奏/背景音剥离(UVR、Spleeter),完全不同 | |
| 指声纹注册后认人(Speaker Identification),不是匿名编号 | |
| 与「角色分离」并存会造成同一功能多个名字 |
hint 示例(角色分离):
转写与翻译完成后,在本地分析整段音频,按不同角色分离发言并标注到字幕。可选择是否将角色标签写入导出的字幕文件。
英文 hint 可继续用 “Distinguish speakers” 或 “Speaker diarization”,与中文「角色分离」语义对应即可。
1.3 提示文案与即将新增的行为不一致
当前 tasks.json 的 hint 写死了「为源字幕和译文添加 [Speaker 1] 等标签」。如果后面加「是否嵌入字幕」选项(见第 3 点),这段 hint 需要改成条件描述,例如:
转写与翻译完成后,在本地分析整段音频,识别不同说话者并按时间轴对齐到每条字幕。可选择是否将标签写入导出的字幕文件。
1.4 模型就绪状态检查不完整
AdvancedSheet 目前只检查 speakerDiarizationModelInstalled,没有检查 sherpa 运行库是否安装(isSherpaLibInstalled())。可能出现「模型显示已下载,但运行时仍跳过」的情况,用户难以理解。
建议: 模型状态 Badge 同时反映「运行库 + 模型」是否就绪,或在 hint 里分开说明。
2. 说话者区分需要作为独立流程阶段
2.1 问题现象(已复现)
实际测试(Terence Tao / Lex Fridman 访谈,speakerDiarization: true,speakerDiarizationCount: 2,builtin whisper + AI 精修):
- 日志在
refine stage done(18:13:58)后结束,没有speaker diarization相关日志 - 磁盘上 SRT 在 18:16 才被写入,全部 323 条 cue 含
[Speaker 1]/[Speaker 2] - 用户在 18:14 进入校对 → 看不到标签;重新进入 → 标签出现
根因是竞态,不是功能失效:
转写 → AI 精修 → [隐形说话者区分 ~2min] → 写 sidecar → 收尾
↑ ↑
UI 认为任务完成 / 校对可进 标签才真正写入
2.2 代码层面的原因
stageUtils.ts的StageKey没有speakerDiarizationgetFileStages()不会渲染该阶段isProofreadReady()在refineSubtitle === 'done'后即返回true(generateOnly),不会等待说话者区分
isProofreadReady 当前逻辑(renderer/components/tasks/stageUtils.ts):
if (typeDef.taskType === 'generateOnly') {
if (file?.extractSubtitle !== 'done') return false;
const stages = getFileStages(file, typeDef, formData);
if (stages.some((s) => s.key === 'refineSubtitle')) {
return file?.refineSubtitle === 'done';
}
return true; // ← 说话者分离开启时也会在这里提前返回 true
}用户看到「精修完成」就认为可以校对,但说话者区分还在后台跑(21 分钟音频约 2 分钟,主要吃 CPU),体验上像功能坏了。
2.3 期望行为
开启 speakerDiarization: true 时:
提取音频 → 转写 → [AI精修] → 翻译 → 说话者区分 → 写 sidecar → 任务完成
↑
独立 UI 阶段(loading / done / error)
校对在此阶段完成后解锁
2.4 实现建议
(1)扩展阶段模型
// IFiles 新增
speakerDiarization?: 'pending' | 'loading' | 'done' | 'error';
speakerDiarizationProgress?: number;
speakerDiarizationError?: string;
// StageKey 新增
'speakerDiarization'(2)getFileStages() 插入条件
当 formData?.speakerDiarization === true 且任务需要转写(非纯字幕输入、非配对模式)时,在 translateSubtitle 之后、dubbing 之前插入该阶段。
(3)fileProcessor.ts 状态回写
调用 runSpeakerDiarizationStage 前后通过 taskFileChange 推送:
- 进入前:
loading - 成功:
done - 降级跳过(模型缺失等):建议
done+ 任务级 warning(与现有「不阻断任务」语义一致) - 用户取消:走现有取消逻辑
(4)修改 isProofreadReady()
若轨道含 speakerDiarization 阶段,须等 file.speakerDiarization === 'done' 才解锁校对。
(5)与 pipeline gate 的关系
一条龙默认开启 subtitleGate。需确认 gate 进入 review 的时机在 sidecar 写入之后;若 gate 更早触发,停靠时读到的 sidecar 仍可能无说话者信息。
(6)耗时预期
说话者区分主要跑 CPU(社区反馈:21 分钟音频约 5 分钟)。独立阶段 + loading 状态能避免用户误以为卡死。可在阶段 hint 中说明「耗时与音频长度成正比」。
3. 说话者是否嵌入字幕:应为用户可选项
3.1 现状
runSpeakerDiarizationStage 无条件将 [Speaker N] 写入所有字幕文件:
const paths = [
file.srtFile,
file.tempSrtFile,
file.translatedSrtFile,
file.tempTranslatedSrtFile,
].filter(...);
await applySubtitleAnnotations(plans);随后 writeProofreadDataFromFiles 从已标注 SRT 解析进 sidecar,说话者信息被固化在 source/target 文本里。
3.2 问题
- 很多用户不希望交付的 SRT/VTT 带
[Speaker 1](播放器、剪辑软件、下游工具不识别) - 配音需要说话者信息,但 TTS 不应朗读标签(已有
normalizeDubbingSpeechTextstrip 逻辑) - 未来 metadata 化后,文本前缀应是「渲染结果」而非唯一数据源
3.3 建议方案
新增配置项:
speakerDiarizationEmbedInSubtitle?: boolean;| 选项 | 字幕文件 | sidecar / 应用内 | 导出交付物 |
|---|---|---|---|
嵌入(true) |
含 [Speaker 1] |
文本含前缀 | 含前缀 |
仅应用内(false) |
不含前缀 | 含 speaker metadata | 不含前缀 |
UI: 在 AdvancedSheet 说话者区分开关下方,开启时显示子选项:
☑ 区分说话者
└─ ☑ 将说话者标签写入字幕文件
关闭后,标签不会出现在导出的 SRT/TXT 中,
说话者信息仅用于应用内校对与配音。
默认值: 建议 false(不污染交付物),更符合专业字幕工作流;若更看重开箱即用,可默认 true 并在 release note 说明。
本 PR 折中(若不想等 metadata PR): 可在 ProofreadDataCue 增加可选 speakerIds?: number[],分离后写入 sidecar 但不改 SRT——这样「不嵌入」模式下校对台已能区分说话者。
3.4 各下游影响
| 下游 | 嵌入开启 | 嵌入关闭 |
|---|---|---|
| 翻译 | 无影响(分离在翻译后) | 无影响 |
| 校对 sidecar | 文本含前缀 | 文本不含;metadata 含 speaker |
| 配音 TTS | strip 前缀 | 从 metadata 取 speaker |
| 烧录字幕 | 含前缀 | 不含前缀 |
4. 自定义流水线:不支持配置说话者区分
4.1 范围定义
以下入口不应暴露说话者区分配置:
| 入口 | 说明 |
|---|---|
| 一条龙向导 | builtin-pipeline → /tasks/new?recipe=builtin-pipeline |
| 用户保存的配方 | taskRecipes 持久化的自定义配方 |
| 含 dub/compose 的向导任务 | recipeHasExtraStages(recipe) === true |
应保留能力的入口:
- 仅转写:
/tasks/generate - 转写+翻译:
/tasks/generate-translate
(仅翻译不涉及转写,本就不应出现该选项。)
4.2 原因
(1)阶段顺序强依赖
说话者区分要求:完整转写时间轴 + 提取音频 + 翻译完成后执行 + sidecar 生成前完成。自定义流水线阶段组合多变,可能出现:
- gate 停靠时 sidecar 尚未写入说话者
- 配音与分离时序冲突
- 用户配置出无效组合
(2)配音链路尚未对接
一条龙默认 dub: true。当前配音从 sidecar/SRT 取文本,voiceId 尚未与 speaker 关联。流水线中开启分离但不嵌字幕时,配音无法按说话人分音色——半成品体验劣于「暂不支持」。
(3)配方复用风险
用户配方 config 浅合并到向导。若允许保存 speakerDiarization: true,一键启动一条龙会带入该配置,但向导 UI 无对应开关,用户无法感知或修改。
(4)产品边界
v1 先在标准字幕任务验证;流水线集成留到 metadata + 配音映射成熟后。
4.3 实现建议
- UI: 向导 / 配方编辑不展示说话者区分;
AdvancedSheet在流水线上下文不渲染该区块 - 保存配方时: strip
speakerDiarization/speakerDiarizationCount/speakerDiarizationEmbedInSubtitle - 运行时: 向导创建任务时,若
recipeHasExtraStages(recipe)或recipe.id === 'builtin-pipeline',强制speakerDiarization = false - 文档: 说明说话者区分仅在「转写」「转写+翻译」任务可用
二、小项建议(非阻塞,可本 PR 或 follow-up)
speakerDiarizationCount上限:UI 限制 2–8,但stage.ts未在运行时 cap,建议与 UI 一致- 进度回调:分离耗时较长,worker 若支持 progress callback,可回写到
speakerDiarizationProgress - 测试覆盖:现有
test:speaker-diarization(config + alignment)不错;可补充「嵌入开/关」和「阶段状态回写」相关用例
三、建议继续增强或者单独开 PR 的增强
3.1 说话者数据写入 Metadata (重点基建,建议优先考虑)
动机: 当前唯一载体是文本前缀 [Speaker 1],编辑字幕易破坏标签,重命名需全文替换,导出/配音/ASS 分色无法共用数据源。
方向:
interface SpeakerInfo {
id: string; // "speaker-1"
displayName: string; // "Speaker 1" | "主持人"
}
interface ProofreadDataCue {
// 现有字段...
speakerIds?: string[];
}- sidecar 升到
version: 2,含speakersroster + cue 级speakerIds - SRT 文本保持纯净;
embedInSubtitle: true时前缀仅作导出渲染 - v1 sidecar 启动时 best-effort 从文本前缀反向解析
3.2 字幕校对界面按说话人区分
依赖: 3.1 Metadata
- 按
speakerId分色 - 筛选 / 高亮某一说话人
- 重命名
displayName - 单条 cue 手动修正说话人
- 可选:发言时长统计
3.3 配音界面按说话人指定音色
依赖: 3.1 Metadata
DubbingCue.voiceId 已预留(types/dubbing.ts 注释 D2)。方向:
- 任务级
speakerId → voiceId映射表 - 生成
DubbingCue时按 speaker 填充voiceId - 可选:每说话人绑定 zipvoice 克隆参考音
- 这是流水线重新支持说话者区分的前置条件
|
已按本次 review 的阻塞项完成修改,追加提交:
验证通过:
另外执行了 |
变更内容
[Speaker 1]/[Speaker 1 + Speaker 2]标签,保留原时间轴模型与降级策略
验证
npm run test:speaker-diarization:配置测试 10/10,对齐与 TTS 边界测试 10/10npm run test:engines:730 passed,0 failed(含任务快照回归)npm run check:i18nnpm run buildgit diff --check尚未覆盖
Closes #352