当前位置: 首页 > news >正文

CosyVoice 双向流式 streamingCall() — 前后端总体方案

CosyVoice 双向流式streamingCall()— 前后端总体方案

在保留现有LLM 流式type=content的前提下,把 TTS 从「整段call()+ OSS URL」升级为CosyVoice 双向 WebSocket + 音频帧直推前端,并保证语音失败不影响文字


一、现状 vs 目标

维度现状目标
文字百炼streamCalltype=content不变
TTS APIcall(整段)阻塞streamingCall(delta)+streamingComplete()
分句本地VoiceStreamingSegmentBufferCosyVoice 服务端自动分句(可去掉本地缓冲)
推前端voiceChunk.voiceUrl(OSS)音频帧(Base64 或 WS Binary)+ 可选短 URL 降级
落库段级 OSS + merge →ext.voiceUrl内存攒帧 → 结束 merge 上传一次
complete等 TTS 队列finishAndAwait文字结束即发 complete,TTS 异步收尾

二、总体架构

CosyVoice WSS streamingCall百炼 Agent streamCallMobileImControllerask-ai WebSocket小程序/H5CosyVoice WSS streamingCall百炼 Agent streamCallMobileImControllerask-ai WebSocket小程序/H5loop[LLM 流式]promptprocessWebSocketAskAi创建 SpeechSynthesizer+callbackchatStreamForWebSocketdeltatype=contentstreamingCall(delta)onEvent audioFrametype=voiceFrame全文结束type=textComplete 或 complete(无语音)streamingComplete()onCompletemerge帧→WAV→OSS→exttype=voiceComplete

原则

  1. 文字与语音解耦:content 先推;TTS 异常只 catch 打日志,不中断 LLM。
  2. 一问一 CosyVoice 连接:每问答一个SpeechSynthesizer,禁止单例共享。
  3. 文本单线程喂 TTS:LLM delta 入队,单线程streamingCall,避免多线程同实例。
  4. 出站串行:同一WebSocketSession的 content / voiceFrame 走per-connection 发送队列

三、后端方案

3.1 模块划分

模块职责
AliyunAgentServiceImpl不变:推content+onContentDelta
CosyVoiceStreamingSession(新)管理一条 WSS:streamingCall/streamingComplete/ callback / close
VoiceSynthesisService新增openStreamingSession(voiceId, callback),配置 WSS 端点
ImStreamingVoicePushSession(重构)去掉call()+段级 OSS;改为转发 delta + 收帧推前端 + 攒帧 merge
WebSocketOutboundQueue(新)同一连接 Text/Binary 串行 send
MobileImController创建/销毁 session;LLM 结束后先发 complete,TTS 异步 finalize

3.2 CosyVoice 会话生命周期

// 问答开始(有 doctorId + voiceId + 连接可用)SpeechSynthesisParamparam=builder().apiKey(...).model("cosyvoice-v2")// 与复刻音色一致.voice(voiceId).format(PCM_22050HZ_MONO_16BIT)// 或 MP3,流式推荐 PCM/MP3.build();SpeechSynthesizersynthesizer=newSpeechSynthesizer(param,callback);// LLM 每个 delta(onContentDelta,try-catch)textFeederQueue.offer(delta);// 单线程 consumer → synthesizer.streamingCall(delta)// LLM 结束synthesizer.streamingComplete();await onComplete/latch;// merge + 落库 + closesynthesizer.getDuplexApi().close(1000,"bye");

配置新增

aliyun:voice:api-key:sk-xxxmodel:cosyvoice-v2websocket-url:wss://{workspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference

SDK 建议≥ 2.22.0getOutput()句子事件);当前 2.19.4 可先 PoC。

3.3 与 LLM 的衔接

onContentDelta(delta): 1. outboundQueue.send(content) // 已在 Agent 层完成 2. cosyVoiceSession.feedText(delta) // 非阻塞入队 chatStreamForWebSocket 返回后: 1. updateBotMessageAfterAiReply 2. outboundQueue.send({ type: "textComplete" }) // 或带 complete 3. cosyVoiceSession.finishAsync() // streamingComplete + 不阻塞主线程 4. 环信 / 告警 等(不依赖 TTS)

3.4 推前端协议(建议)

文字(不变)

{"type":"content","content":"增量","messageId":"...","timestamp":123}

音频帧(新增,推荐 JSON+Base64 便于小程序)

{"type":"voiceFrame","messageId":"429476821505122304","seq":12,"format":"pcm","sampleRate":22050,"channels":1,"bitDepth":16,"sentenceIndex":0,"event":"sentence-synthesis","data":"base64...","timestamp":123}

可选:句子边界(来自result.getOutput()

{"type":"voiceSentence","messageId":"...","sentenceIndex":0,"text":"...","event":"sentence-end"}

结束

{"type":"voiceComplete","messageId":"...","hasVoice":true,"voiceUrl":"https://.../merged.wav"}{"type":"complete","messageId":"...","timestamp":123}
消息时机
contentLLM 流式
voiceGeneratingTTS 连接建立(可选)
voiceFrameCosyVoiceonEventaudioFrame
textCompleteLLM 结束(不等 TTS
voiceCompleteTTSonComplete+ merge 落库后
completetextComplete同发,或 voice 可选

降级:帧推送失败或小程序不支持流式播放时,保留短 MP3 分片 URLvoiceChunk兼容。

3.5 落库

onEvent: audioFrames.add(frame) onComplete: bytes = concat(frames) 或 decode PCM → WAV mergedUrl = upload OSS voice/merged ext: { hasVoice, voiceUrl, voiceFormat, voiceId }

段级 OSS 可取消,只保留最终 merge 一次

3.6 失败与隔离

失败处理
无音色 / 无 doctorId不建 TTS,仅 content + complete
streamingCall/ CosyVoice 报错日志 +voiceComplete(hasVoice=false)不影响已推 content
单帧推送失败日志,继续后续帧
merge/OSS 失败ext.voiceUrl,实时播放仍可能完整
23s 无新文本超时关 TTS;LLM 若仍输出需续连或整问重建 session
同连接连发两问in-flight 锁或拒绝第二问

3.7 并发

  • 每用户每问:1× SpeechSynthesizer + 1× WSS
  • 全局:有界 TTS 连接池(如 32~64),超出排队
  • 禁止Spring 单例SpeechSynthesizer
  • WebSocketConnectionManager:增加sendBinary(connectionId, bytes)+ 与 Text 共用 outbound 队列

四、前端方案

4.1 状态机

IDLE → CONNECTED → AI_STREAMING → TEXT_DONE → VOICE_STREAMING → DONE ↓ content ↓ voiceFrame ↓ ↓ 追加播放队列
  • 文字:按序拼接content
  • 语音:按messageId+seq维护播放队列

4.2 播放(微信小程序)

方案做法适用
A. Base64 → 临时文件每句/每 N 帧攒成 WAV →wx.getFileSystemManager写 temp →InnerAudioContext.src改造小,延迟略高于 H5
B. 句子级 WAVsentence-end后拼帧写文件再播与 CosyVoice 分句对齐,推荐
C. 降级 URL仍收voiceChunk.voiceUrl兼容旧版

不建议小程序裸 PCM 逐帧直播(无 Web Audio,实现成本高)。

4.3 前端伪代码

consttextBuf={};constaudioQueue=[];// { messageId, seq, pcmChunks[] }letplaying=false;onMessage(msg){switch(msg.type){case'content':appendText(msg.messageId,msg.content);break;case'voiceFrame':enqueueFrame(msg);tryPlayNext();break;case'textComplete':markTextDone(msg.messageId);break;case'voiceComplete':case'complete':finishSession(msg.messageId);break;}}

4.4 与旧协议兼容

  • 检测首包:有voiceFrame走流式;仅有voiceChunk走 URL 队列
  • 版本号:连接时?voiceProtocol=2connected里带features: ["voiceStream"]

五、分阶段实施

阶段内容风险
P0SDK 升级 +CosyVoiceStreamingSessionPoC;服务端收帧落日志
P1voiceFrameBase64 推 WS;小程序句子级 temp 文件播放
P2去掉段级 OSS + 本地VoiceStreamingSegmentBuffer;complete 与 TTS 解耦
P3outbound 队列、有界连接池、in-flight 锁
P4可选 Binary 帧、H5 Web Audio 低延迟路径

六、和现有voiceChunk对比

现在voiceChunk+ OSS双向streamingCall
首包延迟整句合成 + 上传更低(帧级)
带宽客户端拉 OSSWS 直推(Base64 更大)
小程序InnerAudio + URL成熟需 temp 文件或句子 WAV
服务端简单WSS 长连接 + 队列 + 协议
历史回放段级 + merge仅 merge 一次即可

七、推荐结论

推荐路径LLM 文字协议不动 + CosyVoice 双向流式 +voiceFrame(句子边界拼 WAV)+ 结束 merge 一次 OSS + 文字结束立即complete+ 语音失败可降级无 voice。

  • 后端核心CosyVoiceStreamingSession+ 文本单线程 feeder + outbound 串行队列
  • 前端核心:按messageId/seq攒帧,按句写 temp WAV 播放
  • 兼容:保留voiceChunk作降级开关

若要落地 P0/P1,切Agent 模式可从VoiceSynthesisService+ImStreamingVoicePushSession改造起笔。

http://www.jsqmd.com/news/1100132/

相关文章:

  • 【JAVA八股文第一章-JVM内存模型】
  • HDFS的文件的读写流程及常用命令
  • 01 · 当 AI 学会“按规矩办事“——规范驱动 Agent 工作流总览
  • 终极指南:如何快速上手MoeKoe Music开源酷狗音乐客户端
  • 从零到一:如何用Citizens2打造沉浸式Minecraft服务器体验
  • 基于改进YOLOv8与无人机的电动自行车违规行为智能检测系统
  • GitLab架构演进:应对AI时代代码分析与高并发挑战
  • 胜券助手已进化为SenClaw:百胜智能中台自带的“免费数字员工”
  • 按位取反是对补码的取反,和之前的求反码的规则类似,但是首位的符号位是改变的,剩下的位数0和1互换,说白了就是每一位都取反
  • 谈谈 2026 年 Altera 的 FPGA 产品线
  • 为何建议等Wi-Fi 8?
  • AI 驱动智能合约漏洞检测:从静态模式匹配到图神经网络的深度审计
  • STL文件太大怎么办?3D模型轻量化实战分享
  • 基于改进YOLOv8的无人机航拍电动自行车违规行为检测实践指南
  • AI Agent实战指南:从核心能力到本地部署的完整路径
  • 基于YOLOv8的轻量化船舶检测:实现可见光与红外图像的高精度识别
  • OpenClaw:让 AI 拥有执行能力的开源本地智能体框架
  • 叉车采购选哪家?这几点帮你精准锁定
  • 2024年HTTP协议安全实战:从头部配置到HTTP/3攻防
  • 数据质量不过关,数据中台就是垃圾进垃圾出:从评价指标到治理闭环的技术拆解
  • 影刀RPA新手教程:电商评论挖掘完全指南——批量采集用户评论、情感分析与词云生成
  • AI Agent本地部署实战:从零构建具备规划与工具调用能力的智能体
  • 终极指南:3分钟上手!零基础文本分析工具KH Coder让数据分析像刷朋友圈一样简单
  • vivo X Fold6开售:稳健策略下,能否跨越折叠屏与AI生态门槛?
  • WorkBuddy AI助手:自然语言查询数据库实战指南与安全实践
  • DTSS认证咨询机构哪家值得推荐
  • 轻量化YOLOv8船舶检测模型:跨模态鲁棒性与边缘部署实战
  • Linux strip 命令 | 详解及在 Linaro 交叉编译工具链中的使用
  • 指夹式脉搏血氧仪PCBA整体方案
  • YOLOv8知识蒸馏实战:让小模型获得大模型的精度