OpenMontage 实战:基于 ElevenLabs Scribe 的浏览器端实时语音转写(Client-Side Real-Time Streaming)完整指南
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
导读
本文聚焦 OpenMontage 仓库中 speech-to-text 技能 的客户端实时流式转写能力:如何从浏览器直接采集麦克风音频,以约 150ms 级别的低延迟流式送往 ElevenLabs Scribe v2 Realtime 模型进行实时转写,并处理"部分转写(partial)"与"提交转写(committed)"两类结果。读完本文你将掌握单次令牌(single-use token)签发、ReactuseScribeHook 与原生 JavaScriptScribe.connect两条接入路径、scribe.status状态机、VAD 自动提交策略以及手动 PCM 分块推送的完整实战方案,并了解该能力在 OpenMontage 视频生产流水线中的落点。
本文以仓库内 realtime-client-side.md 为主体骨架,同时结合 SKILL.md、realtime-events.md、realtime-commit-strategies.md 等姊妹文档与仓库源码进行纵深展开,确保配置、代码、参数均可直接复制运行。
一、能力定位:在 OpenMontage 中用于什么
OpenMontage 是一个开源的 Agent 化视频生产系统,其.agents/skills/与skills/目录沉淀了大量可供 AI 编码助手调用的技能文件。speech-to-text技能的整体定位是"使用 ElevenLabs Scribe v2 将音频/视频转换为文本",用于生成字幕、转写会议、处理口播内容等场景。该技能包含两类模型(详见 SKILL.md):
| 模型 ID | 特点 | 适用场景 |
|---|---|---|
scribe_v2 | 高精度、支持 90+ 语言 | 批量转写、字幕生成、长音频 |
scribe_v2_realtime | 低延迟(约 150ms) | 实时转写、语音 Agent |
本文讨论的scribe_v2_realtime即属于第二条路线。在仓库中,该技能与具体的转写工具形成配套:tools/analysis/transcriber.py中的Transcriber工具声明了agent_skills = ["speech-to-text"],这意味着 Agent 在触发转写任务时可以查阅本文档对应的技能知识。该工具默认基于 faster-whisper / WhisperX 提供本地离线转写(支持词级时间戳、说话人分离、语言检测,见 transcriber.py),而 ElevenLabs Scribe 则提供了云端低延迟实时的另一条路径——两者互补:离线批处理交给 Whisper,浏览器实时对话场景则交给 Scribe Realtime。
仓库中同为 ElevenLabs 生态的 elevenlabs 技能 还展示了语音生成侧(TTS、声音克隆、音效、音乐)与 Remotion 的集成工作流(语音旁白脚本 → MP3 → Remotion<Audio>组件逐场景同步)。本文的实时转写能力与该工作流配合,即可构建"实时聆听 → 生成逐词时间戳 → 驱动字幕/歌词/口型同步"的完整闭环。
二、安装:必须使用 @elevenlabs/* 命名空间
根据 installation.md 与本文档,客户端实时转写需要安装对应的官方包:
# React npm install @elevenlabs/react @elevenlabs/elevenlabs-js # JavaScript npm install @elevenlabs/client @elevenlabs/elevenlabs-jsWarning:客户端包必须使用
@elevenlabs/*命名空间。旧版elevenlabs(v1.x)npm 包已废弃,不应再使用;如果项目里残留旧包,先执行npm uninstall elevenlabs再安装新包。
各包的分工如下(对应 installation.md 的迁移说明):
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; // 服务端/Node:客户端实例与令牌签发 import { Scribe } from "@elevenlabs/client"; // 浏览器端:底层流式连接 import { useScribe } from "@elevenlabs/react"; // React:Hook 封装背景补充:OpenMontage 的语音生成侧同样遵循"直连与托管路由并存"的原则。elevenlabs 技能 指出,TTS 场景优先路由到
fal_elevenlabs_tts(通过 fal.ai 集中管理凭证),仅当注册表中直接 Provider 可用时才使用elevenlabs_tts。实时转写侧的密钥管理(ELEVENLABS_API_KEY)与之一致:API Key 只应存在于服务端环境变量中,绝不下发浏览器。
三、Token 生成:用单次令牌保护 API Key
浏览器端流式转写必须使用"单次使用令牌(single-use token)"来保护你的 API Key——绝不能让浏览器直接持有或拼接 API Key。令牌在你的后端服务中生成,并暴露为一个受鉴权保护的安全接口:
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; const elevenlabs = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_API_KEY, }); app.get("/scribe-token", yourAuthMiddleware, async (req, res) => { const token = await elevenlabs.tokens.singleUse.create("realtime_scribe"); res.json(token); });Note:单次使用令牌的有效期只有 15 分钟,过期后必须重新签发。这要求前端在每次建立连接前实时获取令牌,而不是缓存复用。
四、React 实现:useScribe Hook 全流程
React 应用通过@elevenlabs/react提供的useScribeHook 接入实时转写。核心思路:Hook 负责管理连接生命周期与事件回调,你只需要在点击"开始"时获取令牌并connect():
import { useScribe, CommitStrategy } from "@elevenlabs/react"; function TranscriptionComponent() { const [transcript, setTranscript] = useState(""); const scribe = useScribe({ modelId: "scribe_v2_realtime", commitStrategy: CommitStrategy.VAD, // Auto-commit on silence for mic input onPartialTranscript: (data) => { // Show live feedback as user speaks console.log("Partial:", data.text); }, onCommittedTranscript: (data) => { // Final transcript for this segment setTranscript((prev) => prev + data.text); }, }); const startRecording = async () => { const tokenResponse = await fetch("/scribe-token"); const { token } = await tokenResponse.json(); await scribe.connect({ token, microphone: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); }; const stopRecording = () => { scribe.disconnect(); }; return ( <div> <div>Status: {scribe.status}</div> <button onClick={startRecording}>Start</button> <button onClick={stopRecording}>Stop</button> <p>{transcript}</p> </div> ); }4.1 Commit 策略:麦克风输入必须用 VAD
Important:
useScribe的默认提交策略是CommitStrategy.MANUAL,即必须显式调用scribe.commit()才会产出最终转写。对麦克风实时输入,务必设置CommitStrategy.VAD,让服务端在检测到静音时自动提交。如果不设置,committed转写永远不会触发,连接甚至可能因长期无提交而断开。
两种策略的取舍在 realtime-commit-strategies.md 中有完整说明:
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Manual | 由你调用commit()完成段提交 | 文件处理、由你控制音频分段 |
| VAD | 检测到静音自动提交 | 实时麦克风输入、对话式应用 |
VAD 还有一组可调参数(React 侧以 Hook 选项传入):
const scribe = useScribe({ modelId: "scribe_v2_realtime", commitStrategy: CommitStrategy.VAD, // Optional VAD tuning: vadSilenceThresholdSecs: 1.5, // Silence duration before commit(默认 1.5s) vadThreshold: 0.4, // Speech detection sensitivity 0-1(默认 0.4,越小越灵敏) minSpeechDurationMs: 100, // Minimum speech length required(默认 100ms) minSilenceDurationMs: 100, // Minimum silence length required(默认 100ms) });4.2 两类转写结果:partial 与 committed
理解实时转写,首先要分清两类输出(详见 SKILL.md 与 realtime-commit-strategies.md):
| 类型 | 说明 | 用途 |
|---|---|---|
| Partial(部分转写) | 随音频处理高频更新的"当前最佳猜测" | 边说边显示的字幕反馈;不要落库,随时可能被修正 |
| Committed(提交转写) | 提交后稳定不变的最终结果 | 应用的"事实来源(source of truth)",可安全拼接与保存 |
| Committed + Timestamps | 带词级时间戳的最终结果 | 字幕、卡拉 OK、口型同步 |
4.3 scribe.status 状态机
| Status | Meaning |
|---|---|
"disconnected" | 无活动连接 |
"connecting" | 正在建立连接 |
"connected" | 已连接,可接收音频 |
"transcribing" | 正在处理语音(检测到音频或 VAD 提交时由"connected"转入) |
"error" | 发生错误 |
Important:判断会话是否处于活动状态时,必须同时检查
"connected"与"transcribing"。因为在语音处理期间状态会切到"transcribing",只检查"connected"会导致按钮、波形、指示灯等 UI 元素在 VAD 提交时被错误重置。
// Correct - handles both active states const isListening = scribe.status === "connected" || scribe.status === "transcribing"; // Wrong - will flicker/reset when VAD commits const isListening = scribe.status === "connected";4.4 VAD 的底层原理
VAD(Voice Activity Detection)监听静音并在说话人停顿时自动提交,从而产出贴合人类自然说话节奏(句间、思想间停顿)的转写分段。其推荐使用场景是实时麦克风输入与对话式应用;而 Manual 提交则适合文件处理、已知分段边界、需要最大时间控制的场景(对应 realtime-commit-strategies.md)。若采用 Manual 策略,官方还给出最佳实践:每 20–30 秒提交一次、在静音或逻辑断点(句末、说话人切换)提交、若 90 秒无手动提交则自动提交。
五、JavaScript 实现:Scribe.connect 事件驱动
不依赖 React 的场景(原生 JS、Vue、小程序等)使用@elevenlabs/client的Scribe.connect。与 React Hook 的回调注册方式不同,底层客户端采用事件监听模型,事件名与 WebSocket 消息类型一一对应:
import { Scribe, RealtimeEvents } from "@elevenlabs/client"; async function startTranscription() { const tokenResponse = await fetch("/scribe-token"); const { token } = await tokenResponse.json(); const connection = Scribe.connect({ token, modelId: "scribe_v2_realtime", includeTimestamps: true, microphone: { echoCancellation: true, noiseSuppression: true, autoGainControl: true, }, }); connection.on(RealtimeEvents.OPEN, () => { console.log("Connected"); }); connection.on(RealtimeEvents.PARTIAL_TRANSCRIPT, (data) => { console.log("Partial:", data.text); }); connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT, (data) => { console.log("Committed:", data.text); }); connection.on(RealtimeEvents.COMMITTED_TRANSCRIPT_WITH_TIMESTAMPS, (data) => { for (const word of data.words) { console.log(`${word.text}: ${word.start}s - ${word.end}s`); } }); connection.on(RealtimeEvents.ERROR, (error) => { console.error("Error:", error); }); connection.on(RealtimeEvents.CLOSE, () => { console.log("Disconnected"); }); return connection; }5.1 事件协议:底层消息格式
理解事件驱动模型,推荐通读 realtime-events.md。该文档完整定义了实时转写的 wire protocol:
客户端 → 服务端(Sent Events):
input_audio_chunk:发送音频数据。关键字段包括message_type(恒为"input_audio_chunk")、audio_base_64(Base64 编码的 PCM 音频)、commit(此块后是否提交)、sample_rate(采样率,8000–48000)、previous_text(仅首块可携带,最长 50 字符,用于断线重连后的上下文续接):
{ "message_type": "input_audio_chunk", "audio_base_64": "<base64-encoded-pcm-audio>", "commit": false, "sample_rate": 16000 }commit:终结当前转写段:
{ "message_type": "commit" }服务端 → 客户端(Received Events,均以message_type作为判别字段):
| 事件 | 含义 |
|---|---|
session_started | 连接建立成功,返回session_id与回显的会话配置(采样率、音频格式、模型 ID、提交策略、是否含时间戳) |
partial_transcript | 部分转写,随音频处理频繁更新 |
committed_transcript | 提交后的最终转写 |
committed_transcript_with_timestamps | 含词级时间戳的最终转写(include_timestamps=true时在 committed 之后发送),每个词包含text、start、end、type(word/spacing/audio_event)与可选speaker_id |
错误事件与错误码:服务端通过error事件上报失败原因,常见错误码包括auth_error(Key 或令牌无效)、quota_exceeded(用量超限)、input_error(不支持的音频格式或非法输入)、rate_limited(请求过频)、commit_throttled(提交过于频繁)、session_time_limit_exceeded(会话超时)、chunk_size_exceeded(音频块过大)、insufficient_audio_activity(未检测到足够语音)、transcriber_error(内部处理错误)等。
六、手动音频分块:处理文件与自定义音频源
对于文件上传或自定义音频源(例如不是麦克风,而是从本地文件解码出的 PCM 流),需要将音频编码为 PCM-16 并按块推送给服务端:
const chunkSize = 4096; for (let offset = 0; offset < pcmData.length; offset += chunkSize) { const chunk = pcmData.slice(offset, offset + chunkSize); const bytes = new Uint8Array(chunk.buffer); const base64 = btoa(String.fromCharCode(...bytes)); scribe.sendAudio(base64); // Simulate real-time streaming await new Promise((resolve) => setTimeout(resolve, 50)); } // Finalize transcription scribe.commit();6.1 音频格式要求
服务端对音频有明确要求(详见 realtime-server-side.md 的 Audio Requirements):
| 参数 | 推荐值 |
|---|---|
| 格式 | PCM 16-bit |
| 采样率 | 16000 Hz(推荐),支持 8kHz–48kHz |
| 声道 | 单声道(Mono) |
| 分块大小 | 32,000 字节 ≈ 16kHz 下的 1 秒音频(服务端侧示例);客户端手动分块示例使用 4096 字节 |
提示:处理多声道或非 16kHz 音频时,应先做预处理。服务端示例给出了 Python 侧用 pydub 的转换思路:多声道
set_channels(1)、重采样set_frame_rate(16000)、位深set_sample_width(2),再按块 Base64 编码发送。JavaScript 侧可用fs.readFileSync读取.pcm文件按chunkSize切分后toString("base64")发送,最后connection.commit()收尾。
6.2 提供上下文(previous_text)
如果需要在断线重连后续接对话,或在转写开头为模型提供语境,可在首个音频块中携带previous_text(不超过 50 字符)。这有助于:续接重连后的对话、提升上下文准确性、处理句子碎片(对应 realtime-commit-strategies.md)。
七、麦克风选项:浏览器采集参数
connect时传入的microphone配置直接映射到浏览器getUserMedia约束,用于改善采集质量:
| Option | Description |
|---|---|
echoCancellation | 消除扬声器回声 |
noiseSuppression | 过滤背景噪声 |
autoGainControl | 归一化音量水平 |
在"说话人 A 对着扬声器讲话、扬声器同时播放对方声音"这类视频会议场景中,echoCancellation尤其重要——否则转写会把回放的对方语音也当作输入。
八、安全底线
浏览器端实时转写存在天然的密钥泄露风险,必须遵守以下三条红线(对应原文档 Security 章节):
- 绝不在客户端暴露你的 API Key:API Key 只存在于服务端环境变量(如
ELEVENLABS_API_KEY),任何前端代码、构建产物、网络请求中都不应出现。 - 始终在后端生成单次使用令牌:通过
elevenlabs.tokens.singleUse.create("realtime_scribe")签发,前端只持有一次性、15 分钟有效的令牌。 - 用鉴权中间件保护令牌接口:
/scribe-token这类端点必须挂在yourAuthMiddleware之后,防止匿名用户刷取令牌造成额度消耗。
九、在 OpenMontage 中的落地与延伸
9.1 与仓库转写工具链的配合
OpenMontage 的转写能力呈现"本地离线 + 云端实时"双轨结构:
- 本地离线:Transcriber 工具 基于 faster-whisper 提供确定性的批量转写(词级时间戳、VAD 过滤、GPU 探测与 CPU 回退、WhisperX 说话人分离),输出
{文件名}_transcript.json,适合音视频文件的后期字幕生成。 - 云端实时:本文的 Scribe v2 Realtime 客户端流式方案,适合交互式、低延迟场景(直播字幕、会议实时记录、语音助手)。
两种路径产出相同的"文本 + 词级时间戳"数据结构,可统一喂给下游的 字幕同步技能 或 subtitle-sync 技能 完成 SRT/ASS 字幕生成,再交给 Remotion 合成器 渲染为带字幕的视频成品。
9.2 延伸:WebSocket 直连
在不便使用 SDK 的场景(例如非 JS 语言、嵌入式环境),可以绕过 SDK 直连 WebSocket 端点(详见 realtime-server-side.md):
wss://api.elevenlabs.io/v1/speech-to-text/realtime?model_id=scribe_v2_realtime直连时的消息格式与上文事件协议完全一致:上行input_audio_chunk/commit,下行以message_type区分的各类转写事件。这意味着本文描述的令牌签发、音频格式、事件语义在直连场景下同样适用——你完全可以用同一套后端令牌体系驱动任意语言的实时转写客户端。
十、快速自检清单
落地前对照检查以下要点,可避免绝大多数"连接建了但没结果"的坑:
- ✅ 包名是否为
@elevenlabs/react/@elevenlabs/client/@elevenlabs/elevenlabs-js(拒绝旧版elevenlabs)。 - ✅ API Key 是否仅存在于服务端,令牌接口是否挂了鉴权中间件。
- ✅ 麦克风输入是否设置了
CommitStrategy.VAD(默认 Manual 会导致 committed 永不触发)。 - ✅ UI 活动态判断是否同时覆盖
"connected"与"transcribing"。 - ✅ 手动推流时音频是否为 PCM-16 / 单声道 / 16kHz,块大小是否在服务端接受范围内。
- ✅ 是否处理了
error事件并映射错误码(auth_error、rate_limited、chunk_size_exceeded等)。
【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考