跳到主要内容

实时语音与视觉输入(前端视角)

目录

核心场景

  • 语音助手:边说边转写、边生成
  • 截图问答:上传图像后定位区域并解释
  • 视频片段分析:按时间段提问并引用帧信息

两种语音架构:级联 vs 端到端语音(2026 关键)

这是当前实时语音最重要的认知,面试常考:

1)级联管线(Cascaded:ASR → LLM → TTS)

把语音拆成三段独立处理:语音识别(ASR)→ 文本送 LLM → 文本转语音(TTS)

  • 优点:每段可单独替换/优化、文本可审计、便于接 RAG/工具。
  • 缺点:延迟叠加(一问一答常 >1.5s)、丢失语气/情感/打断的自然感。

2)端到端语音模型(Speech-to-Speech / Realtime)

直接“语音进、语音出”,模型原生处理音频,无需中间转文本。代表:OpenAI Realtime API(GPT Realtime 系列)、Gemini Live API、各家原生语音模型。

  • 优点:超低延迟(数百毫秒)、支持自然打断(barge-in)、保留语气情感、体验接近真人对话。
  • 缺点:可审计性弱一些、接复杂工具/RAG 时工程更讲究。
  • 工程特征:通过 WebRTC 或 WebSocket 建立持久双向连接,支持 VAD(语音活动检测)自动判断说话结束function calling(边对话边调工具)

面试一句话:“要可控、要接知识库就用级联(ASR+LLM+TTS);要低延迟、要自然打断的语音对话就用端到端 Realtime API(走 WebRTC/WebSocket)。

详见 WebRTC 协议与实时音视频实践 中的 Realtime over WebRTC 落地。

原生多模态:图像/音频/视频直接进模型

当代旗舰模型(GPT-5、Claude 4.x、Gemini 2.5/3 等)多为原生多模态——图像、音频、甚至视频可直接作为输入,不必先转成文字描述:

  • 图像理解:截图问答、UI 识别、文档/表格 OCR+理解、看图写代码。
  • 视频理解:Gemini 等支持长视频输入,按时间段提问。
  • 前端要做的:压缩与格式适配(见下文)、多模态消息体组装(文本 + 图像 URL/base64 混排)、超大文件分段。

实时链路设计

这张链路图想说明三件事:

  • 前端不仅是“展示层”,还承担采集、编码和状态编排
  • 实时服务的核心目标是低延迟回传,而不是一次性大结果
  • UI 要把各阶段可视化,否则用户会误以为卡死

当你按这个链路实现后,用户感知会明显改善:从“等结果”变成“看到过程”,这对实时交互非常关键。

关键点:

  • 采集、编码、上传必须可中断
  • 前端显示实时阶段(录音中/识别中/生成中)
  • 所有阶段统一 traceId 方便排障

前端状态管理建议

  • idle:未开始
  • capturing:采集中
  • streaming:实时返回中
  • failed:失败
  • done:完成

同时保存:

  • 最近 10 秒缓存片段(用于断线重试)
  • 当前输入模态(text/audio/image)
  • 延迟指标(首包时间、完整耗时)

代码示例

浏览器录音并切片上传

export async function startAudioStream(onChunk: (blob: Blob) => void) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });

recorder.ondataavailable = (e) => {
if (e.data.size > 0) onChunk(e.data);
};

// 每 500ms 产生一片,适合实时转写
recorder.start(500);

return () => {
recorder.stop();
stream.getTracks().forEach((t) => t.stop());
};
}

视觉输入预处理

export async function compressImage(file: File, maxSize = 1280) {
const img = await createImageBitmap(file);
const scale = Math.min(1, maxSize / Math.max(img.width, img.height));
const w = Math.round(img.width * scale);
const h = Math.round(img.height * scale);

const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d")!;
ctx.drawImage(img, 0, 0, w, h);

return await new Promise<Blob>((resolve) =>
canvas.toBlob((blob) => resolve(blob as Blob), "image/jpeg", 0.85)
);
}

状态图

最小验收标准

  • 实时输入可在 1 秒内看到反馈
  • 用户可随时中断,不残留后台任务
  • 错误时可重试并保留已生成内容