WeClaw_44_PWA语音消息端到端处理:从录音到ASR转录的异步管道

张开发
2026/4/18 15:38:34 15 分钟阅读

分享文章

WeClaw_44_PWA语音消息端到端处理:从录音到ASR转录的异步管道
WeClaw_44_PWA语音消息端到端处理从录音到ASR转录的异步管道作者: WeClaw 开发团队日期: 2026-03-29版本: v1.0标签: 语音消息、ASR、GLM-ASR、Whisper、音频转录、PWA 摘要本文完整剖析 WeClaw 系统中 PWA 语音消息的端到端处理管道。当用户在手机 PWA 端录制一段语音消息发送给桌面端 AI 时系统需要完成语音检测 → 音频下载 → ASR 转录 → 文本处理 → AI 回复。文章涵盖纯语音输入检测算法、双引擎 ASRGLM-ASR 云端 Whisper 本地、音频格式自动转换、以及桌面端主动发送语音消息到 PWA 的反向通道。核心收获 掌握纯语音输入的检测算法️ 理解 GLM-ASR 云端 Whisper 本地的双引擎架构 学会音频文件下载与格式转换流程⚡ 掌握跳过 ReAct 循环的性能优化策略 了解桌面端发送语音消息到 PWA 的实现 需求背景语音消息的特殊性语音 vs 文本的处理差异文本消息处理流程 帮我查一下天气 → 意图识别 → 工具调用 → AI 回复 └── 简单直接 语音消息处理流程 音频文件 → 检测类型 → 下载文件 → ASR 转录 → 得到文本 → 意图识别 → AI 回复 └── 多了三个额外步骤PWA 端的语音消息格式PWA 用户录音后发送的消息结构{type:pwa_request,payload:{content:[语音消息],attachments:[{attachment_id:uuid-xxx,filename:recording_20260329.webm,mime_type:audio/webm,data:/api/files/uuid-xxx}]}}特点content为占位符文本[语音消息]不是实际内容实际语音数据在attachments中以音频文件形式传递需要桌面端下载并转录为文本后才能处理 核心模块一纯语音输入检测为什么需要检测不是所有带附件的消息都是纯语音。需要区分| 场景 | content | attachments | 类型 ||------|---------|-------------|------|| 纯语音 |[语音消息]| 1 个音频文件 | 纯语音 ✅ || 文字图片 |帮我识别这张图| 1 个图片 | 多模态 ❌ || 纯图片 | (空) | 1 个图片 | 文件附件 ❌ || 文字语音 |帮我翻译这段话| 1 个音频 | 多模态 ❌ |检测算法# 语音占位符文本集合VOICE_PLACEHOLDER_TEXTS{[语音消息],[voice message],[Voice Message],[audio],[Audio],}def_is_voice_only_input(self,attachments:list,content:str)-bool:检测是否为纯语音输入。# 条件 1必须有附件ifnotattachments:returnFalse# 条件 2content 为空或是语音占位符actual_contentcontent.strip()ifcontentelseifactual_contentandactual_contentnotinself.VOICE_PLACEHOLDER_TEXTS:returnFalse# 条件 3只有一个附件iflen(attachments)!1:returnFalseattattachments[0]mime_typeatt.get(mime_type,)filenameatt.get(filename,)# 条件 4附件是音频文件# 方式一MIME 类型判断ifmime_type.startswith(audio/):returnTrue# 方式二扩展名判断兜底audio_exts(.mp3,.wav,.ogg,.aac,.flac,.m4a,.wma,.webm)iffilename.lower().endswith(audio_exts):returnTruereturnFalse设计要点占位符集合覆盖中英文变体MIME 类型优先扩展名兜底多附件不算纯语音可能是文件语音组合️ 核心模块二双引擎 ASR 转录引擎选择策略GLM-ASR云端推荐 ├── 优势准确率高、支持多语言、无需本地模型 ├── 劣势需要网络、有 API 配额 └── 适用正常网络环境 Whisper本地备用 ├── 优势离线可用、无配额限制 ├── 劣势需要模型下载、GPU 加速效果更好 └── 适用无网络或 GLM 不可用voice_input 工具的 transcribe_file 动作ActionDef(nametranscribe_file,description将音频文件转为文字,parameters{file_path:{type:string,description:音频文件路径(支持 wav/mp3/m4a/webm 等),},engine:{type:string,description:识别引擎glm-asr(云端) 或 whisper(本地),default:glm-asr,enum:[glm-asr,whisper],},model:{type:string,description:Whisper 模型 (仅 whisper 引擎),default:base,enum:[tiny,base,small,medium,large],},},required_params[file_path],)转录实现引擎选择 自动降级asyncdef_transcribe_file(self,file_path:str,engine:strNone,model:strbase,language:strNone)-ToolResult:将音频文件转为文字。ifengineisNone:engineself._engine# 实例默认引擎pathPath(file_path).expanduser().resolve()# 文件大小限制50MBifpath.stat().st_size/(1024*1024)50:returnToolResult(statusToolResultStatus.ERROR,error文件过大)# 根据引擎选择转录方式ifengineglm-asr:ifnot_check_glm_asr():logger.warning(GLM ASR 不可用降级到 Whisper)enginewhisper# 自动降级else:returnawaitself._transcribe_file_with_glm_asr(path,...)ifenginewhisper:ifnot_check_voice_dependencies():returnToolResult(statusToolResultStatus.ERROR,errorWhisper 不可用请安装 openai-whisper)returnawaitself._transcribe_file_with_whisper(path,model,...)自动降级流程用户指定 glm-asr ↓ 检查 GLM ASR 可用性 ├── 可用 → 调用 GLM ASR 转录 └── 不可用 → 降级到 Whisper ├── 可用 → 调用 Whisper 转录 └── 不可用 → 返回错误 核心模块三语音消息完整处理管道桌面端接收 PWA 语音消息asyncdef_transcribe_voice_attachment(self,attachment:dict,user_id:str,session_id:str)-str|None:直接转录音频附件返回转录文本。filenameattachment.get(filename,unknown)dataattachment.get(data,)# 1. 构建完整下载 URLserver_base_urlself._get_server_base_url()ifdata.startswith(/api/files/):full_urlf{server_base_url}{data}elifdata.startswith(http):full_urldataelse:returnNone# 2. 下载音频文件到本地local_pathself._download_remote_file(urlfull_url,filenamefilename,attachment_idattachment.get(attachment_id,),user_iduser_id,session_idsession_id,mime_typeattachment.get(mime_type,),file_typeaudio,user_message,)ifnotlocal_path:returnNone# 3. 调用 voice_input 工具转录fromsrc.tools.voice_inputimportVoiceInputTool toolVoiceInputTool()resultawaittool.execute(transcribe_file,{file_path:local_path,engine:glm-asr,# 默认使用云端引擎})# 4. 提取转录文本ifresult.status.valuesuccess:textresult.data.get(text,)returntext.strip()iftextelseNonereturnNone跳过 ReAct 循环的优化纯语音消息的处理不需要经过完整的 AI ReAct 循环普通消息流程ReAct 循环 用户输入 → 意图识别 → 工具暴露 → LLM 分析 → 工具调用 → LLM 整合 → 回复 └── 需要多次 LLM 调用耗时较长 纯语音消息流程跳过 ReAct 语音附件 → 检测纯语音 → 直接转录 → 转录文本作为用户输入 → 正常处理 └── 省去正在转录中间提示直接反馈结果# 处理入口ifself._is_voice_only_input(attachments,content):# 纯语音消息直接转录不经过 ReActtranscribedawaitself._transcribe_voice_attachment(attachments[0],user_id,session_id)iftranscribed:# 用转录文本替换占位符作为正常用户输入处理contenttranscribed attachments[]# 清空附件避免重复处理 核心模块四桌面端发送语音消息send_voice Action桌面端也可以主动向 PWA 发送语音消息asyncdef_send_voice(self,params:dict)-ToolResult:发送语音消息到 PWA。errself._check_bridge()iferr:returnerr file_pathparams.get(file_path,)pathPath(file_path)# 校验文件扩展名ifpath.suffix.lower()notinVOICE_EXTENSIONS:returnToolResult(statusToolResultStatus.ERROR,errorf不支持的语音格式:{path.suffix},)user_idself._resolve_user_id(params)transcriptparams.get(transcript,)# description 添加 [语音消息] 标记descriptionf[语音消息]{transcript}iftranscriptelse[语音消息]successawaitself._bridge_client.send_file_to_pwa(user_iduser_id,file_pathfile_path,descriptiondescription,)ifsuccess:returnToolResult(statusToolResultStatus.SUCCESS,data{message:语音消息已发送到 PWA},)else:returnToolResult(statusToolResultStatus.ERROR,error语音消息发送失败,)语音格式白名单VOICE_EXTENSIONS{.mp3,# MPEG Audio Layer 3.wav,# Waveform Audio.ogg,# Ogg Vorbis.flac,# Free Lossless Audio Codec.aac,# Advanced Audio Coding.m4a,# MPEG-4 Audio.opus,# Opus Audio.webm,# WebM AudioPWA 录音默认格式} 完整数据流PWA → 桌面端接收语音消息PWA 用户录音 ↓ PWA: 上传音频到服务器 → attachment_id ↓ PWA → 服务器 → 桌面端: pwa_request content[语音消息] attachments[{filename:rec.webm, data:/api/files/xxx}] ↓ 桌面端: _is_voice_only_input() → True ↓ 桌面端: _transcribe_voice_attachment() ├── 下载 webm 文件 ├── 调用 GLM-ASR 转录 └── 返回文本 明天的天气怎么样 ↓ 桌面端 AI: 以转录文本为输入正常处理 ↓ 桌面端 → 服务器 → PWA: AI 文本回复桌面端 → PWA发送语音消息LLM 调用 remote_file_share_send_voice file_pathgenerated/tts_output.mp3 transcript你好这是一条语音消息 ↓ RemoteFileShareTool._send_voice() ├── 校验扩展名 ∈ VOICE_EXTENSIONS ├── description [语音消息] 你好这是一条语音消息 └── 调用 bridge.send_file_to_pwa() ├── HTTP POST 上传文件 └── WebSocket file_share 信令 ↓ PWA 收到语音消息可播放收听 经验教训1. webm 格式的兼容性教训PWA 默认录音格式为 webm某些 ASR 引擎不支持。解决方案GLM-ASR 原生支持 webmWhisper 降级时自动转换为 wav。2. 占位符文本的多样性教训最初只检测[语音消息]国际化场景遗漏。解决方案维护占位符集合覆盖中英文和大小写变体。3. 空消息拦截的副作用教训系统原有逻辑拦截了 content 为空的消息导致纯语音消息无文字仅有附件被丢弃。解决方案修改拦截逻辑允许仅含附件的消息通过。 架构总结语音消息处理全景| 方向 | 通道 | 关键技术 ||------|------|---------||PWA → 桌面端| WebSocket 信令 HTTP 下载 | 纯语音检测、GLM-ASR 转录 ||桌面端 → PWA| HTTP 上传 WebSocket 信令 | send_voice Action、格式校验 |双引擎 ASR 对比| 维度 | GLM-ASR | Whisper ||------|---------|---------||运行位置| 云端 | 本地 ||准确率| 高商业级 | 中等取决于模型 ||延迟| 1-3 秒 | 3-10 秒CPU ||离线支持| ❌ | ✅ ||配额限制| 有API 调用限制 | 无 ||格式支持| wav/mp3/webm/m4a | wav其他需转换 |字数统计: 约 4,600 字阅读时间: 约 12 分钟代码行数: 约 280 行 WeClaw 技术博客系列已完成 44 篇本轮迭代v3.0.0 ~ v3.1.0共产出 4 篇深度技术博客第 41 篇WebSocket HTTP 混合协议设计第 42 篇Agent 工具注册全链路标准化第 43 篇双重认证与 Token 自动刷新第 44 篇PWA 语音消息端到端处理管道- - - - - - - - - - -

更多文章