
WhisperLiveKit 转写与说话人分离对齐原理token 级 speaker 归属、滞后缓冲与翻译挂接机制【免费下载链接】WhisperLiveKitReal-time, local speech-to-text with streaming ASR, speaker diarization, translation, and OpenAI/Deepgram-compatible APIs.项目地址: https://gitcode.com/GitHub_Trending/wh/WhisperLiveKit导读WhisperLiveKit 是面向实时、本地语音转写的开源项目提供流式 ASR、说话人分离diarization、翻译以及 OpenAI/Deepgram 兼容 API。本文基于 docs/alignement_principles.md 展开系统讲解其核心机制——带时间戳的 ASR token 与带时间戳的说话人分离 span 之间的对齐算法包括按时间重叠度分配说话人、跨说话人边界的 token 处理、diarization 滞后时的缓冲策略、翻译文本的挂接归属规则以及沉默与模型通道上限的边界行为。读完本文你将掌握 WhisperLiveKit 如何在不依赖标点的情况下检测说话人切换、如何保证多说话人场景下输出行的稳定性并能结合源码定位对应实现。对齐的总体现路逐 token 度量重叠逐行切换说话人WhisperLiveKit 的对齐对象是两类带时间戳的数据流ASR token 流每个 token 携带start/end时间戳与文本由流式 ASR 后端提交见 whisperlivekit/timed_objects.py 中的ASRToken数据类。diarization span 流每个 span 携带时间区间与说话人编号由分离后端Sortformer 或 diart输出对应SpeakerSegment同样定义于 whisperlivekit/timed_objects.py。文档给出的对齐规则非常明确对每个已提交committed的 ASR token度量它与当前可用说话人 span 的重叠时长选择与其重叠最大的说话人当该说话人与前一个 token 不同时开启一行新的输出。关键点说话人切换检测完全不依赖标点。标点只用于行内文本的自然收束而不是判断说话人轮次的依据。这一点与传统的标点分割句子式流水线有本质区别。原文的图示清晰地表达了这一过程ASR tokens: [Hello ][there ][Goodbye ][now] Diarization: [ speaker 1 ][ speaker 2 ] Output line 1: [Hello there ] Output line 2: [Goodbye now ]在实现层面上述逻辑集中在 whisperlivekit/tokens_alignment.py 的TokensAlignment类中。build_token_speaker_segmentswhisperlivekit/tokens_alignment.py遍历全部 token对每个非沉默 token 调用_speaker_for_tokenwhisperlivekit/tokens_alignment.py求出其说话人当说话人发生变化时通过flush_pending()把当前累积的 token 组刷新成一条输出行_segment_from_token_group。行内连续、同一说话人的 token 会在随后的_merge_adjacent_segmentswhisperlivekit/tokens_alignment.py中被合并既保留文本也保留 token 列表。说话人编号约定内部 0-based对外 1-based一个容易踩坑的细节是编号约定Sortformer 内部使用 0-based 说话人编号其模型通道从 0 开始计数WhisperLiveKit 对外输出 1-based 说话人 ID。这一转换发生在_speaker_for_token中源码speaker diarization_segment.speaker 1whisperlivekit/tokens_alignment.py。此外序列化层Segment.to_dict会把未赋值-1的默认说话人也归一化为 1见 whisperlivekit/timed_objects.py。因此客户端拿到的行始终是speaker: 1/2/3/...这样的正编号无需关心后端模型的通道编号。边界 token 的归属规则文档明确了三个易错场景的判定标准跨边界的 token一个 token 的时间区间横跨两个说话人的边界时token 保持完整不拆分整段归属于重叠较大的一侧。实现上_speaker_for_token会遍历与 token 有交叠的所有 span用intersection_durationwhisperlivekit/tokens_alignment.py计算重叠时长并取最大者。零时长 tokenstart end的 token例如瞬间词或标点没有区间可度量直接归属于包含其时间戳的那个说话人 span。对应源码分支if diarization_segment.start token_start diarization_segment.endwhisperlivekit/tokens_alignment.py。独立标点 token一个单独的标点 token 总是关闭它前面的那条说话人行即使它的时间戳恰好落在下一个说话人边界上也不会因此开启一条新的标点行。_is_punctuation_onlywhisperlivekit/tokens_alignment.py按 Unicode 字符类别P*识别纯标点 tokenbuild_token_speaker_segments在遇到它时把它追加到当前累积的 token 组并结束该行。这些规则在测试中有直接验证tests/test_speaker_boundaries.py 中的test_zero_duration_token_uses_the_containing_speaker_segment与test_zero_duration_punctuation_at_turn_closes_the_previous_speakertests/test_speaker_boundaries.py分别覆盖了零时长 token 归属与标点关闭上一行两个场景后者还断言Hello there.归 speaker 1而 Next归 speaker 2且所有原始 token 在输出行中一个不丢、顺序不变.join(line.text ...) buffer_text .join(token.text ...)。无 diarization span 时的兜底行为当 diarization 尚未产生任何 span 时比如模型刚启动build_token_speaker_segments会回退到纯标点分段并保持默认说话人。源码中表现为if not diarization_segments:分支whisperlivekit/tokens_alignment.py测试test_missing_diarization_keeps_existing_default_speaker_behaviortests/test_speaker_boundaries.py验证了该行为。也就是说对齐层只是切分行 分配说话人它不会因为缺少分离数据而丢弃任何转写文本。Diarization 滞后buffer_diarization缓冲机制流式系统中转写与分离两个流水线的速度天然不同步——转写可以跑在 diarization 前面。文档明确凡是start时间戳落在最新 diarization 时间戳last_diarization_end之后、或恰好等于该时间戳的 token一律进入buffer_diarization不做投机性分配。ASR tokens: [first ][second] Diarization: [ speaker 1 ] Lines: [first ] Buffer: [second]为什么必须缓冲而非投机分配如果对尚未有 diarization 覆盖的文本强行分配说话人一旦后续分离结果到达之前已输出的行就得被改写导致前端出现文本跳动或说话人标签闪烁。缓冲策略保证了已输出行 缓冲文本永远是原始 token 顺序的前缀/后缀划分源码注释whisperlivekit/tokens_alignment.py明确写出了这一不变量Lines plus buffer must remain a prefix/suffix partition of the original token order。实现上build_token_speaker_segments维护last_diarization_end max(segment.end for segment in diarization_segments)whisperlivekit/tokens_alignment.py一旦某个 token 的start last_diarization_end就置位buffering_suffix此后该次刷新中的所有后续文本 token 一律进入buffer_partswhisperlivekit/tokens_alignment.py。buffering_suffix还会顺带处理后端可能发出的时间戳回退retrogradetoken——只要处于缓冲状态即使时间戳倒退也保持缓冲从而维护前缀/后缀划分不变量。刷新重算与一次性迁移文档强调每次刷新refresh都会根据已提交的 token 时间戳重建所有行。对应实现链路是results_formatter在 whisperlivekit/audio_processor.py 中每轮调用self.tokens_alignment.update()汲取状态缓冲再调用get_lines(...)whisperlivekit/tokens_alignment.py重建行。因为重建是幂等的全量计算当 diarization 追上后缓冲文本会一次性迁移进对应说话人的行内不丢文本、不丢词级时间戳token 对象本身被原样挂进Segment.tokens。缓冲文本通过FrontData.buffer_diarization字段随每次响应下发见 whisperlivekit/timed_objects.pydiff 协议与 Deepgram 兼容层都会透传该字段whisperlivekit/diff_protocol.py、whisperlivekit/deepgram_compat.py。Web 前端 whisperlivekit/web/live_transcription.js 在渲染时将其显示为待定归属文本并用remaining_time_diarization 0判断当前是否处于 diarization 滞后状态该文件中showDiaLag逻辑whisperlivekit/web/live_transcription.js配合 CSS 类.buffer_diarizationwhisperlivekit/web/live_transcription.css做视觉区分。性能保障前向游标而非全量两两比较_speaker_for_token接收一个search_start前向游标由于输入是按时间序排列的游标只前进不后退遇到end token_start的 span 直接跳过whisperlivekit/tokens_alignment.py从而让整次刷新保持近似线性复杂度而不是 token 数与 span 数的笛卡尔积。仅当出现时间戳回退时才重置游标为 0。测试test_speaker_resolution_uses_a_chronological_cursortests/test_speaker_boundaries.py通过 mockintersection_duration计数验证了游标确实把重叠计算压缩到了必要范围。Translation spans翻译文本的恰好一次挂接启用翻译后一条已确认validated的翻译可能覆盖一段源文本而这段源文本可能被 diarization 切分到多条说话人行。WhisperLiveKit 的规则是翻译文本恰好挂接一次挂到与它时间重叠最大的那条说话人行上落在内部边界上的点翻译start end归属于紧随其后的那一行若没有任何行与翻译重叠则挂到时间上最近的说话人行距离相等时按稳定的转写顺序选前面那一行。该逻辑位于add_translationswhisperlivekit/tokens_alignment.py对非点翻译遍历所有说话人行用intersection_duration求最大重叠者对点翻译用segment.start translated.start segment.end找包含该时间戳的行找不到重叠行时用temporal_distance求时间距离最近的行min(speech_segments, keytemporal_distance)。为什么不能全部挂或全不挂add_translations的 docstringwhisperlivekit/tokens_alignment.py解释得很清楚只挂完全包含的 span 会丢掉跨说话人的翻译挂给所有重叠行会导致同一译文重复出现。取最大时间重叠得到唯一的、确定性的归属者。重复快照不会重复挂接关键保证关联在每次 refresh 时重建因此客户端多次请求快照不会导致同一段翻译被追加两次。测试test_translation_segments_are_monotone_nonoverlapping与test_point_translation_at_final_endpoint_is_not_losttests/test_speaker_boundaries.py验证了连续两次get_lines得到的译文内容一致且累计只有一行携带翻译sum(bool(line.translation) for line in first) 1。这依赖一个实现细节add_translations每次先把所有说话人行的translation清零再重新计算归属whisperlivekit/tokens_alignment.py从根上避免了累加式重复。Silence 与模型通道上限显式沉默是独立行speaker: -2文档明确显式沉默explicit silence永远是独立的一行标记为speaker: -2它既不会被折叠进说话人行也不会进入 diarization 缓冲。这一约定贯穿全项目Segment.from_tokens(..., is_silenceTrue)会构造speaker-2的段whisperlivekit/timed_objects.pyis_silence()即判断speaker -2build_token_speaker_segments遇到token.is_silence()的 token 时先冲刷待定行再单独追加一条沉默段whisperlivekit/tokens_alignment.py输出侧同样遵守该约定Web 前端在 whisperlivekit/web/live_transcription.js 以speaker -2识别沉默行Deepgram 兼容层与 REST 输出则把speaker -2或空文本的行从说话人文本行中排除whisperlivekit/deepgram_compat.py、whisperlivekit/basic_server.py。注意区分两个概念-2是已结束/已确认的沉默段的标记而进行中的沉默由Silence对象与current_silence参数处理get_lines的current_silence分支whisperlivekit/tokens_alignment.py它同样以SilentSegmentspeaker-2落入行列表。对齐不改变模型支持的最大说话人数文档强调两点边界对齐层每个 token 只选一个说话人它不会改变所配置 diarization 模型支持的说话人数上限也无法表达一个 token 内同时有多人讲话——同一 token 内多说话人是模型通道能力之外的事情对齐层不负责、也不代表它存在。Sortformer 的--sortformer-max-speakers N只是声明式上界不是估计值。它按说话人到达顺序保留模型前 N 个通道如果录音里实际说话人超过 N 个后来的说话人可能被归并到已保留的标签之一。该设置不会改变转写文本、词时间戳或上述每 token 一个说话人的对齐逻辑。参数与源码佐证命令行参数定义于 whisperlivekit/parse_args.py--sortformer-max-speakers类型int取值 1–4默认None即使用检查点全部通道默认模型为 4 通道。帮助文本明确指出这是声明会话至多包含这么多说话人不是估计也不会拒绝多余说话人。相关配套参数还包括--sortformer-model-path本地.nemo文件/目录或模型 IDwhisperlivekit/parse_args.py与--diarization-backendsortformer/diart默认sortformerwhisperlivekit/parse_args.py。底层实现对声明上界的处理在 whisperlivekit/diarization/sortformer_backend.py 中_resolve_max_speakerswhisperlivekit/diarization/sortformer_backend.py校验max_speakers必须是 1 到检查点通道数之间的整数非法值直接抛错_process_predictionswhisperlivekit/diarization/sortformer_backend.py按到达顺序保留前 N 个通道retained_preds preds_np[:, :self.max_speakers]并对每帧取通道 argmax 得到当前活跃说话人再切分为SpeakerSegment片段。为什么是保留前 N 通道而不是逐帧取 top-N源码注释whisperlivekit/diarization/sortformer_backend.py说明流式 Sortformer 的通道按说话人到达顺序排列并在 speaker cache 中维持身份保留前 N 通道即可跨 chunk 保持说话人 ID 稳定而逐帧 top-N 会让成员关系不稳定且对独立 sigmoid 输出无效。该结论同样有集成测试支撑test_real_two_speaker_cap_is_stable_through_chunks_and_overlaptests/test_sortformer_real_fixture.py使用仓库内置的双说话人真实音频 fixturetests/fixtures/sortformer_2spk验证了跨 chunk、含重叠区间的说话人上限稳定性。数据流与调用链总览把上述机制串起来一次完整的对齐刷新周期如下ASR 后端产生带时间戳的 token进入State.new_tokensdiarization 后端diarize()见 whisperlivekit/diarization/sortformer_backend.py 或 whisperlivekit/diarization/diart_backend.py产生SpeakerSegment进入State.new_diarization翻译后端产生Translation进入State.new_translation状态类见 whisperlivekit/timed_objects.py。results_formatterwhisperlivekit/audio_processor.py每轮调用TokensAlignment.update()汲取三个缓冲whisperlivekit/tokens_alignment.py。get_lines(diarizationTrue, translation..., audio_time...)依次执行合并相邻同说话人 spanconcatenate_diar_segmentswhisperlivekit/tokens_alignment.py→ 按 token 归属切分输出行并产出buffer_diarizationbuild_token_speaker_segments→ 挂接翻译add_translations→ 按保留期裁剪历史_prunewhisperlivekit/tokens_alignment.py。结果封装为FrontData含lines、buffer_diarization、buffer_translation等字段下发到 WebSocket / REST / diff / Deepgram 各协议层。concatenate_diar_segments有个值得一提的细节它基于dataclasses.replace拷贝合并whisperlivekit/tokens_alignment.py而不是原地修改end因为该函数在每次get_lines刷新都会执行原地修改会令存储的 span 逐渐越合并越长造成累积性污染——源码 docstring 对此有明确警示whisperlivekit/tokens_alignment.py。实践要点与常见问题启用 diarization 对转写无副作用对齐层只为 token 分配说话人并切行不会改写 ASR 文本与词时间戳--sortformer-max-speakers也明确不影响转写内容。前端如何感知滞后当buffer_diarization非空且remaining_time_diarization 0时Web 前端将缓冲文本渲染为待归属样式whisperlivekit/web/live_transcription.js用户可据此判断文本已出、归属待定的中间状态。沉默行的语义speaker: -2的行在转写文本协议如basic_server.py的文本输出中会被过滤但在 JSON/前端协议中完整保留用于展示静音间隙。翻译与说话人行的关系一段跨说话人的译文只出现一次挂到重叠最大的行连续请求快照不会重复追加客户端可以放心地以全量刷新方式渲染。调试与测试仓库测试 tests/test_speaker_boundaries.py 是理解上述规则的极佳入口——它直接构造State与TokensAlignment逐条断言行内文本、说话人、时间戳与 token 完整性Sortformer 相关行为则有 tests/test_sortformer_real_fixture.py 基于真实音频验证。结语WhisperLiveKit 的转写–分离对齐设计围绕三个不变量展开输出行与缓冲永远是原始 token 序的前缀/后缀划分滞后场景不丢文本、每条翻译恰好一个归属重复刷新不重复追加、对齐层只分配不篡改不改变文本、时间戳与模型通道上限。把握住这三个不变量无论是排查说话人标签异常、理解 diarization 滞后时的文本去向还是为协议层做二次开发都能快速定位到 whisperlivekit/tokens_alignment.py 中对应的实现分支。【免费下载链接】WhisperLiveKitReal-time, local speech-to-text with streaming ASR, speaker diarization, translation, and OpenAI/Deepgram-compatible APIs.项目地址: https://gitcode.com/GitHub_Trending/wh/WhisperLiveKit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考