Skip to content

feat(diarization): 支持字幕说话者区分 - #414

Open
nightt5879 wants to merge 3 commits into
buxuku:mainfrom
nightt5879:nightt5879/feat-352-speaker-diarization
Open

feat(diarization): 支持字幕说话者区分#414
nightt5879 wants to merge 3 commits into
buxuku:mainfrom
nightt5879:nightt5879/feat-352-speaker-diarization

Conversation

@nightt5879

@nightt5879 nightt5879 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

变更内容

  • 新增任务级“区分说话者”开关,默认关闭;支持自动检测或指定 2–8 位说话者
  • 使用 sherpa-onnx 的 pyannote segmentation + 3D-Speaker embedding 在独立 utility process 中执行本地说话者分离,支持逐文件取消与进程隔离
  • 按时间重叠把分离结果对齐到 SRT/VTT/ASS 字幕,并生成 [Speaker 1] / [Speaker 1 + Speaker 2] 标签,保留原时间轴
  • 在翻译完成后、校对数据生成前统一标注源字幕与译文,避免说话者标签污染翻译提示
  • 配音/TTS 最终输入会剥离本功能生成的说话者前缀,展示与交付字幕继续保留标签;其它方括号正文不受影响
  • 开启说话者分离的普通任务会固定创建时配置快照,任务重开、重试和续跑不会被之后修改的全局开关或人数覆盖
  • 新增资源中心下载、镜像回退、导入、删除、路径展示,以及任务快照、中英文 i18n 和纯函数测试

模型与降级策略

  • 官方模型资源合计约 44.4 MiB:pyannote segmentation 压缩包 6,958,444 字节,3D-Speaker embedding 39,593,761 字节
  • 模型缺失、音频不可用、结果为空或推理失败时记录原因并保持原字幕不变,不阻断既有转写/翻译流水线
  • 任务取消仍严格终止独立 worker;多文件并发时每个文件拥有独立进程
  • 功能默认关闭,不改变现有任务和默认输出

验证

  • npm run test:speaker-diarization:配置测试 10/10,对齐与 TTS 边界测试 10/10
  • npm run test:engines:730 passed,0 failed(含任务快照回归)
  • npm run check:i18n
  • npm run build
  • git diff --check
  • 通过 GitHub Release API 核对两个官方模型资源的文件名与字节大小

尚未覆盖

  • 未在本地下载完整模型并用真实多人音频执行端到端推理;模型配置、worker 协议、字幕对齐、TTS 文本边界与失败回退已由定向测试及生产构建覆盖

Closes #352

@buxuku buxuku left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

非常感谢该 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 命名空间下不存在(仅存在于 modelsControldubbingBlock.openModelsFolder),导致界面显示 raw key openModelsFolder

同类问题还有 'Open folder failed' 硬编码英文,应改为 i18n key。

建议:

  • renderer/public/locales/zh/resources.jsonen/resources.json 根级补充 openModelsFolderopenFolderFailed
  • 或改为与 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: truespeakerDiarizationCount: 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 代码层面的原因

  1. stageUtils.tsStageKey 没有 speakerDiarization
  2. getFileStages() 不会渲染该阶段
  3. 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 问题

  1. 很多用户不希望交付的 SRT/VTT 带 [Speaker 1](播放器、剪辑软件、下游工具不识别)
  2. 配音需要说话者信息,但 TTS 不应朗读标签(已有 normalizeDubbingSpeechText strip 逻辑)
  3. 未来 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)

  1. speakerDiarizationCount 上限:UI 限制 2–8,但 stage.ts 未在运行时 cap,建议与 UI 一致
  2. 进度回调:分离耗时较长,worker 若支持 progress callback,可回写到 speakerDiarizationProgress
  3. 测试覆盖:现有 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,含 speakers roster + 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 克隆参考音
  • 这是流水线重新支持说话者区分的前置条件

@nightt5879

Copy link
Copy Markdown
Contributor Author

已按本次 review 的阻塞项完成修改,追加提交:f74ee0d

  1. 文案 / i18n / 就绪状态

    • 补齐 resources 根级 openModelsFolderopenFolderFailed,移除打开目录失败的硬编码英文。
    • 中文产品文案统一为「角色分离」,英文统一为 Speaker diarization;hint 已改为区分「应用内 metadata」与「写入字幕」。
    • 高级设置中的就绪 Badge 现在同时检查 sherpa 运行库和角色分离模型。
  2. 独立流程阶段与竞态修复

    • 新增 speakerDiarization 阶段状态、进度和错误字段,并在任务轨道中放在翻译之后、附加阶段之前。
    • 执行顺序现在是:阶段 loading → 分离推理 → 写入 proofread sidecar → 阶段 done;校对入口会等待该阶段完成,不再提前读取旧 sidecar。
    • 模型缺失、音频不可用或推理失败仍按非阻断策略处理为 done + warning;取消继续走原有严格中止逻辑。
    • 当前 utility worker 没有增量 progress 回调,因此先提供明确的阶段 loading 与 0/100 进度;后续 worker 支持回调后可直接细化。
  3. 字幕嵌入策略

    • 新增 speakerDiarizationEmbedInSubtitle,默认 false
    • 关闭时不修改 SRT/VTT/TXT 等交付字幕,将一基编号 speakerIds 写入 cue 级 sidecar metadata,并在校对数据更新时保留。
    • 显式开启时才写入 [Speaker N] 前缀;翻译仍在分离之前完成,不会污染 prompt。
    • 运行时人数限制与 UI 对齐为 2–8,越界统一回退自动聚类。
  4. 产品边界

    • 角色分离只在标准「转写 / 转写 + 翻译」任务展示和运行。
    • 一条龙向导 payload、配方保存/读取/回填均剥离全部角色分离字段;主进程对旧快照和含 dub/compose/recipeName 的任务再次强制门禁。
    • 文档已补充支持范围、模型/运行库要求和默认导出策略。

验证通过:

  • npm run check:i18n
  • npm run test:speaker-diarization(config 10 + alignment/workflow 21)
  • npm run test:recipes
  • npm run test:engines(730)
  • npm run test:dubbing(151)
  • git diff --check

另外执行了 npm run test:pipeline;Windows 下现有 4 个路径分隔符断言失败,在干净 main 上可同样复现,与本次改动无关。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

功能建議:區分說話者

2 participants