跳到内容

实时语音转文本,延迟低于 200 毫秒:架构指南

发布时间
最近更新

收听收听本文

实时语音转文本(STT)会在说话时即时转录音频,几百毫秒内输出文本。但要保持低延迟,除了模型本身,架构同样关键。工程师需要整体规划传输、分片、端点检测和采集路径,每一步都会增加延迟。任何环节效率低下,都可能导致超出 200 毫秒预算。

本指南提供了一套实用系统,帮助你从传输层开始搭建实时语音转文本流程。我们将以 Scribe v2 实时版 为例,该模型延迟约 150 毫秒,可输出部分转录,支持 90 多种语言,兼容 PCM(8kHz-48kHz)和 mu-law 音频,并提供语音活动检测和手动提交控制用于分段。

我们将梳理音频如何到达服务器、假设如何转为最终文本、流内功能的延迟成本,以及如何正确采集和转发音频。

要点总结

  • 构建实时语音转文本系统需要精细调整架构,确保全流程延迟始终较低。
  • 大多数流程推荐使用 WebSocket,WebRTC 虽有优势但更复杂。
  • 语音活动检测可实现免手动分段,手动提交则让应用在已知说话结束时主动分段。
  • 部分转录为临时结果,最终转录为已确认结果,建议区分展示。
  • 约 100 毫秒的小 PCM 分片可最大限度降低首次部分转录的延迟。

实时语音转文本:WebSocket 与 WebRTC

在转录前,音频需从源头传输到识别端。所选通道决定了后续流程的最低延迟。音频到达转录层有两种可行方案。

WebSocket 是基于 TCP 的长连接、可靠、双向有序通道。只需建立连接,上行推送二进制音频帧,下行接收转录事件。客户端和服务端实现都很简单,可穿透允许 HTTPS 的企业代理和防火墙,所有主流浏览器和服务端环境均支持。

WebSocket 的限制在于依赖 TCP。如果丢包,TCP 会重传并阻塞后续数据,直到补齐缺口。网络良好时无感知,丢包时会出现短暂卡顿,音频堆积后集中到达。

WebRTC 专为实时媒体设计,采用 UDP(通过 SRTP)传输,丢包不会阻塞流,流程可持续。内置抖动缓冲,吸收包到达时间的波动;通过 ICE/STUN/TURN 协议实现 NAT 穿透,便于路由器后的设备互联,并自带音频采集和编码功能。

对于无法直连的客户端,通常需要 TURN 服务器,服务端需终止媒体流而非读取字节流。

简要对比如下:

WebSocket
Transport
TCP (reliable, ordered)
Behavior under packet loss
Head-of-line blocking, bursty recovery
Jitter handling
Your responsibility
NAT traversal
Not needed (client-initiated)
Browser support
Universal, trivial
Server complexity
Low
WebRTC
Transport
UDP/SRTP (real-time, loss-tolerant)
Behavior under packet loss
Graceful degradation
Jitter handling
Built-in jitter buffer
NAT traversal
Requires ICE/STUN/TURN
Browser support
Universal, but more API surface
Server complexity
High (media server or SFU)

大多数场景推荐 WebSocket。适用于客户端网络良好且采集路径可控的情况:如服务器间流程、桌面应用、宽带环境下的浏览器应用,以及大多数音频已到达服务器的呼叫中心后端。

如需直接采集不稳定移动网络上的消费级设备,或已用 WebRTC 实现双向音频(如可回话的语音智能体),或对低丢包实时性要求高于实现简单性时,建议选择 WebRTC。

本指南其余部分均以 WebSocket 作为识别连接的传输方式,便于流程可见且适合大多数团队入门。流程本身不依赖 WebSocket,后续可在前端加 WebRTC 媒体层,服务端解码为 PCM,再按相同分片推入流程。

部分转录与最终转录:中间结果说明

实时识别器不会等完整句子才输出,而是持续输出猜测,随着音频增加不断修正,最终锁定。理解两者区别,才能让转录体验流畅而非卡顿。

部分转录(临时结果)是模型基于当前音频的最佳猜测,设计上本就不稳定。音频增加时,模型会修正前面内容:如 “I want to” 后续可变为 “I want two tickets”。部分转录速度快(约 150 毫秒延迟),且会被后续结果覆盖。

最终转录为已确认分段,不再更改。分段完成后,识别器进入下一个分段,后续猜测对应后续音频。最终转录适合保存、发送给 LLM 或作为正式转录存储。

区分部分与最终转录很重要,混淆会导致以下三类问题:

  • 用户体验: 展示部分转录让转录过程更实时,用户能看到说话内容即时出现,确认麦克风和系统正常工作。
  • 端点检测: 部分转录可持续反映语音活动,结合 VAD 可判断说话是否结束。
  • 下游时序:语音智能体流程 步骤为音频输入、语音转文本、LLM 处理,接着

部分与最终转录建议区分展示。常见做法是用一个可变“当前行”绑定最新部分转录,收到最终转录时追加到转录列表:

type TranscriptState = {
  committed: string[]; // finalized segments, never rewritten
  current: string;     // latest partial, overwritten on each update
};

const onPartial = (s: TranscriptState, text: string): TranscriptState =>
  ({ ...s, current: text });

const onFinal = (s: TranscriptState, text: string): TranscriptState =>
  ({ committed: [...s.committed, text], current: "" });

视觉上,已确认文本用常规样式,当前文本用浅色或斜体,提示用户内容可能变化。

端点检测与语音活动检测(VAD)

识别内容只是第一步,还需判断说话何时结束。这决定了何时分段,以及智能体何时开始响应。

端点检测即判断一句话是否结束。过早分段会打断用户,过晚则让智能体在用户已说完后迟迟不响应。

Scribe v2 Realtime 提供两种互补机制:

  • 语音活动检测基于静音自动分段: 识别器检测语音转为持续静音时自动分段。VAD 适合对话场景,无需手动计时,能适应自然语速。
  • 手动提交控制:手动提交让应用自主决定何时分段,无需依赖静音。发送提交信号后,识别器关闭当前分段并输出最终转录。适用于应用已知说话结束的场景,如松开按键、点击“发送”或有外部轮流策略。

两者可结合使用。典型语音智能体用 VAD 实现免手动操作,同时提供手动提交作为补充,用户思考时不会被打断,主动操作时可立即分段。

静音阈值没有绝对标准,需权衡:

  • 较短的静音超时(如 200-400 毫秒)响应更快,但易把自然停顿分成多段,导致智能体过早响应。
  • 较长超时(如 800-1200 毫秒)可容忍自然停顿,保持语句完整,但系统反应会有明显延迟。

没有通用常量,需根据场景调整阈值:

  • 听写和笔记类应用可容忍较长停顿,建议延长超时并依赖 VAD。
  • 指令类和事务型智能体适合短超时加手动提交,因说话简短明了。
  • 多语种或非母语用户停顿更多,建议延长静音阈值。

参考以上建议,可帮助你构建高效端点检测系统,迈向实时语音转文本。

流内功能:语言检测与说话人分离

流式识别不仅能输出文本,还可提供更多信号。但每增加一项功能,都会影响延迟和稳定性。建议只启用实时体验必需的功能,其余留给批量处理。

自动语言识别让 Scribe v2 Realtime 能在支持的 90 多种语言中自动识别语种,无需提前指定。代价是模型需一小段音频才能判断,流前几条部分转录可能不稳定。若已知语种,建议直接指定,可提升前期稳定性。

说话人分离可区分不同说话者,标记谁说了什么。批量转录时模型能看到全文件,较易实现;流式转录则需根据当前音频实时分配说话人标签,前期标签可能需后续修正。流式说话人标签应视为临时,直到分段完成。

词级时序和实体信息同理。请求越多元数据,模型和传输压力越大。大多数实时界面只需文本和分段边界,细粒度元数据可在通话后批量处理。

流式音频格式:PCM 与 mu-law

传输和识别逻辑常被关注,但实际问题往往出在音频编码和分片。选对格式和分片大小,是降低语音转文本延迟的最简单方法。

PCM(线性、16 位有符号、小端)适合自控采集场景。采样率越高,音频细节越多:16kHz 是语音识别标准下限,通常足够;8kHz 为电话级,丢失高频。应选与源音频一致的采样率。8kHz 电话音频无需升采样到 48kHz,因高频信息已丢失。

8kHz mu-law 是电话音频格式。如从 Twilio 等服务商接入通话,音频为 8kHz mu-law,建议直接转发,无需二次转码。保持源格式可避免重采样失真和多余转换。

分片大小直接影响感知延迟。音频按分片发送,识别器收到分片即输出部分转录。分片越小,更新越频繁,首次部分转录延迟越低;分片越大,消息更少,推理上下文略多。实用范围为每片 20-250 毫秒音频。举例:16kHz 单声道 16 位 PCM,1 秒音频约 32,000 字节,100 毫秒分片约 3,200 字节。

浏览器中采集麦克风输入

浏览器端推荐用 Web Audio API 搭配 AudioWorklet。Worklet 运行在音频渲染线程,接收小帧音频,不受主线程卡顿影响,优于旧的 ScriptProcessorNode。其任务是将浏览器原生 float 采样转为 16 位 PCM,交给主线程,通过 WebSocket 发送。

Worklet 处理器核心为 float 到 PCM 的转换:

// pcm-worklet.ts - registered via audioContext.audioWorklet.addModule()
class PCMWorklet extends AudioWorkletProcessor {
  process(inputs: Float32Array[][]) {
    const channel = inputs[0]?.[0]; // mono; Float32, range [-1, 1]
    if (!channel) return true;
    const pcm = new Int16Array(channel.length);
    for (let i = 0; i < channel.length; i++) {
      const s = Math.max(-1, Math.min(1, channel[i]));
      pcm[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
    }
    // Transfer the buffer to the main thread without copying.
    this.port.postMessage(pcm.buffer, [pcm.buffer]);
    return true;
  }
}
registerProcessor("pcm-worklet", PCMWorklet);

流程代码示例

流程包含三部分:浏览器客户端采集麦克风并流式发送 PCM 到服务器,Node 服务器转发音频到 Scribe v2 Realtime 并返回转录,脚本化客户端可从文件或电话桥接流式发送 PCM。

服务器转发而非让浏览器直连识别器,主要是因为 ElevenLabs API 密钥属于敏感信息,绝不能出现在前端代码。密钥应保存在服务器。如需浏览器直连识别器,应由服务器生成一次性短效 token 交给客户端,而非 API 密钥。

浏览器客户端

客户端通过 WebSocket 连接服务器,利用上述 worklet 采集麦克风,并实时转发每帧 PCM。服务器已将事件标准化为 { type, text },驱动前述部分/最终状态。

// client.ts - runs in the browser. ws is an open WebSocket to your server.
const audioContext = new AudioContext({ sampleRate: 16000 });
await audioContext.audioWorklet.addModule("pcm-worklet.js");

const mediaStream = await navigator.mediaDevices.getUserMedia({
  audio: { channelCount: 1, echoCancellation: true, noiseSuppression: true },
});

const source = audioContext.createMediaStreamSource(mediaStream);
const worklet = new AudioWorkletNode(audioContext, "pcm-worklet");

// Forward each PCM frame to the server the moment it is produced.
worklet.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
  if (ws.readyState === WebSocket.OPEN) ws.send(e.data);
};
source.connect(worklet);

// Manual commit: tell the server to finalize the current segment.
const commit = () => ws.send(JSON.stringify({ type: "commit" }));

服务器转发

服务器为每个客户端建立识别连接,API 密钥仅保存在服务器,二进制 PCM 直接转发,并将识别事件标准化为客户端消费的 { type, text } 格式:

// server.ts - Node, using the `ws` library. ELEVENLABS_API_KEY and the
// recognizer URL come from the environment; see the Speech to Text reference
// for the exact path and query parameters.
import { WebSocketServer, WebSocket } from "ws";

new WebSocketServer({ port: 8080 }).on("connection", (client) => {
  // The API key stays on the server, never on the wire to the browser.
  const recognizer = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
    headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
  });

  // Browser -> recognizer: forward binary PCM, translate control messages.
  client.on("message", (data, isBinary) => {
    if (recognizer.readyState !== WebSocket.OPEN) return;
    if (isBinary) recognizer.send(data); // raw PCM bytes
    else if (JSON.parse(data.toString()).type === "commit")
      recognizer.send(sendCommit());
  });

  // Recognizer -> browser: normalize events into a stable shape.
  recognizer.on("message", (raw) => {
    const event = parseRecognizerEvent(raw.toString());
    if (event && client.readyState === WebSocket.OPEN)
      client.send(JSON.stringify(event));
  });

  // ... open handshake, queueing pre-open audio, and teardown on close/error
});

所有端点相关逻辑仅在下方两个适配器函数中。字段名请替换为 Speech to Text 参考文档中的实际名称,其余流程无需更改:

// The single place that knows the recognizer's wire format.
const sendCommit = (): string => JSON.stringify({ type: "commit" });

type NormalizedEvent =
  | { type: "partial"; text: string }
  | { type: "final"; text: string }
  | { type: "vad"; speaking: boolean };

function parseRecognizerEvent(raw: string): NormalizedEvent | null {
  const msg = JSON.parse(raw);
  if (msg.is_final === true || msg.type === "final")
    return { type: "final", text: msg.text ?? "" };
  if (msg.type === "vad") return { type: "vad", speaking: !!msg.speaking };
  if (typeof msg.text === "string")
    return { type: "partial", text: msg.text };
  return null;
}

脚本化后端客户端

后端流程及下方基准测试同样可用此识别连接,无需浏览器:从任意来源读取 PCM,按实时分片节奏发送,读取返回事件。API 密钥和 URL 从环境变量获取,和服务器一致。

// stream-stt.ts - pace ~100ms chunks at real time, then commit the tail.
const SAMPLE_RATE = 16000, CHUNK_MS = 100;
const CHUNK_BYTES = (SAMPLE_RATE * 2 * CHUNK_MS) / 1000; // 3200 bytes
const ws = new WebSocket(process.env.RECOGNIZER_WSS_URL!, {
  headers: { "xi-api-key": process.env.ELEVENLABS_API_KEY! },
});

// Send: walk the PCM buffer in 100ms chunks, sleeping between to mimic a
// live source. For audio that already arrives in real time, drop the sleep.
async function sendAudio(pcm: Buffer) {
  for (let off = 0; off < pcm.length; off += CHUNK_BYTES) {
    ws.send(pcm.subarray(off, off + CHUNK_BYTES));
    await new Promise((r) => setTimeout(r, CHUNK_MS));
  }
  ws.send(JSON.stringify({ type: "commit" })); // finalize the trailing segment
}

// Receive: print partials in place, append finals.
ws.on("message", (raw) => {
  const e = parseRecognizerEvent(raw.toString());
  if (e?.type === "final") console.log(`[final]   ${e.text}`);
  else if (e?.type === "partial") process.stdout.write(`[partial] ${e.text}\r`);
});

语音转文本延迟与词错误率基准测试

延迟和词错误率会因说话人、语言、音频环境、音频长度、网络路径及服务当前负载而异。

单台笔记本在某城市测得的结果,并不代表其他环境。应在接近生产环境的基础设施上,用真实音频测试,并报告区间和分布,而非单一数值。

唯一有意义的延迟和准确率,是你在自有音频和生产环境下测得的数据。以下为语音转文本延迟基准测试指南。

语音转文本延迟应测哪些指标

基准测试实时语音转文本延迟时,建议关注以下主要指标:

  • 首次部分转录时间: 从发送首个音频分片到收到首个非空部分转录的时间。
  • 部分转录到最终转录延迟: 从一句话最后一个音频分片到最终转录的时间。
  • 词错误率(WER):最终转录与人工参考的 WER,所有系统统一计算方式。
  • 稳定性变化: 最终分段前部分转录被重写的次数,反映实时界面变化频率。

控制变量

为避免数据不可靠,建议在实验中加入多项控制,确保一致性。

语音转文本延迟基准测试应关注的主要控制变量:

  • 音频一致: 所有系统用相同文件、采样率和编码。
  • 节奏一致:所有系统按相同实时分片节奏流式发送(如每片 100 毫秒)。
  • 重复并报告分布: 每个文件全天多次测试,报告中位数和尾部(p50/p95)。
  • 参考与评分一致: 计算 WER 前统一文本规范化(大小写、标点、数字)。
  • 披露区域与网络: 说明测试运行地点及到各服务商的网络路径。

保持以上要素一致,可获得更精准的指标。

测试框架结构

测量核心接收服务商适配器,记录首次部分转录时间、最终延迟和部分转录变化次数:

// benchmark.ts - measurement core; one StreamFn adapter per provider.
type StreamFn = (
  audioPath: string,
  onEvent: (kind: "partial" | "final", text: string) => void,
  result: RunResult
) => Promise<void>; // adapter sets result.lastChunkSentAt on the final chunk

interface RunResult {
  firstPartialMs?: number;
  finalLagMs?: number;
  hypothesis: string;
  partialEdits: number;
  lastChunkSentAt: number;
  startedAt: number;
}

async function measure(streamFn: StreamFn, audioPath: string): Promise<RunResult> {
  const result: RunResult = {
    hypothesis: "", partialEdits: 0, lastChunkSentAt: 0,
    startedAt: performance.now(),
  };
  let prevPartial = "";

  await streamFn(audioPath, (kind, text) => {
    const now = performance.now();
    if (kind === "partial") {
      if (text && result.firstPartialMs === undefined)
        result.firstPartialMs = now - result.startedAt;
      if (text !== prevPartial) { result.partialEdits++; prevPartial = text; }
    } else { // final
      result.hypothesis = result.hypothesis ? `${result.hypothesis} ${text}` : text;
      if (result.lastChunkSentAt)
        result.finalLagMs = now - result.lastChunkSentAt;
    }
  }, result);

  return result;
}

词错误率采用标准的 Levenshtein 距离,基于规范化文本逐词计算。计算前应统一小写并去除标点,否则测量的是规范化器而非模型。建议循环每个文件约 10 次,报告中位首次部分转录时间和中位 WER(p50/p95),因单次结果易受网络波动影响。

运行时需准备两项:每套系统写一个 StreamFn 适配器,上述脚本化客户端已是一个,其他适配器遵循(audioPath, onEvent, result)协议,并在最后一片音频发送时设置 result.lastChunkSentAt。其次加载音频文件和参考文本,批量调用测量函数。建议在接近生产环境的机器上、用真实音频运行,确保结果可复现。

实时语音转文本实现要点回顾

本文介绍了多项架构优化,帮助你逐步完善系统,迈向实时语音转文本。

生产级实时 STT 系统主要涉及以下决策:

  • 传输方式:网络可控且追求简单时选 WebSocket,需容忍丢包且采集自消费级设备时选 WebRTC。
  • 部分与最终转录:部分转录视为临时,最终转录为已确认,建议区分展示,提升用户信任。
  • 端点检测:免手动分段用 VAD,手动提交作补充,静音阈值按场景调整。
  • 流内功能:仅在实时体验需要时启用流内功能,其余可用 Scribe v2 批量处理。
  • 音频格式:采集小帧 PCM,分片约 100 毫秒,电话音频保持源格式。
  • 基准测试:根据自有音频和目标指标,实际调整准确率与延迟参数。
  • API 安全:API 密钥保存在服务器,或为直连客户端生成一次性 token。

如需了解如何 优化语音智能体延迟,我们也为你准备了相关指南。

用 Scribe v2 Realtime 构建实时语音转文本系统

Scribe v2 Realtime 模型延迟约 150 毫秒,实际体验还取决于你搭建的架构。参考本文策略,可优化流程架构,降低延迟,提升客户体验。

想进一步了解,请查看 语音转文本功能 概览,阅读我们的 模型参考文档 获取完整功能和语种列表,并访问实时产品页面:实时语音转文本 API实时语音转文本.

准备好开始时,免费创建 ElevenLabs 账户,立即体验流式转录。

实时语音转文本延迟常见问题

相关内容

用高质量 AI 音频创作