SparkOS人机交互平台
SparkOS人机交互平台文档导览
SparkOS产品介绍
SparkOS人机交互平台隐私协议
SparkOS人机交互平台用户协议
SparkOS Android SDK接入文档
SparkOS iOS SDK接入文档
SparkOS Harmony NEXT接入文档
SparkOS 平台新手指南
SparkOS进阶教程
SparkOS API 接入文档
本文档使用 MrDoc 发布
-
+
首页
SparkOS API 接入文档
# SparkOS API 接入文档 > **适用版本:全双工 + 单工** > 本文重点描述 SparkOS 全双工及单工接入的请求参数、响应参数和相关注意事项。 ## 技术支持 API 接入问题可联系: - `zhhuang18` - `授权与咨询工单:https://yf2ljykclb.xfchat.iflytek.com/docx/doxrzDaqP7sepNAdXbi6J9gzt1g?from=from_copylink` --- # 前言 ## WebSocket 接口说明 1. 客户端必须符合 WebSocket 协议规范,即 RFC 6455。 2. WebSocket 握手成功后,如果客户端在 **10 秒内未发送请求数据**,服务端会主动断开连接。 3. 本接口默认采用全双工交互模式。每次建立连接后,客户端可以持续发送音频数据,云端会持续处理音频,并将处理结果返回给客户端。 4. 单个 WebSocket 连接最长可持续使用 **30 分钟**。 5. 服务端下发错误码后,客户端应断开并重新建立连接,否则后续处理过程可能连续报错。 ## 内容审核说明(重要) 接口会对用户输入内容和 AI 输出内容进行文本审核。 审核内容包括但不限于: 1. 涉及国家安全的信息; 2. 涉及政治与宗教类的信息; 3. 涉及暴力与恐怖主义的信息; 4. 涉及黄、赌、毒类的信息; 5. 涉及不文明的信息。 当输入或输出未通过审核时,接口会返回相应错误码: - `10014`:输入审核错误; - `10019`:大模型生成结果审核错误。 详细说明参见[错误码说明](#五错误码说明)。 --- # 一、API 介绍 SparkOS API是一套端到端语音交互解决方案。 客户端持续输入用户侧音频,云端完成语音识别、语义理解、回复生成以及音频或视频合成,并返回合成音频或视频流。 ## 开通授权 1. API 会进行时长和并发鉴权。 2. 可在相关平台的应用管理页面领取或购买授权包。 3. 授权包购买成功后,方可调用 API 服务。 4. 也可联系技术支持,由后台协助开通授权。 5. DeepSeek 版本需要额外申请 DeepSeek 授权。 --- # 二、API 接入说明 ## 2.1 请求说明 ### 2.1.1 请求地址 ```text ws(s)://sparkcons.cn-huabei-1.xf-yun.com/v3/sos ``` ### 2.1.2 接口鉴权 请参考通用接口鉴权文档: <https://aiui-doc-admin.xf-yun.com/project-4/doc-375/> 如有需要,可提供以下语言的鉴权代码: - Java - Python - Go - JavaScript ### 2.1.3 请求协议 请求协议示例如下: ```json { "header": { "app_id": "", // 应用id "uid": "", // 用户唯一标识,用户关联用户交互历史, "status": 0, // 客户端发送数据的状态 0:开始;1:中间状态;2:结束 "stmid": "1", // 交互轮数,客户端发起的交互计数。使用continuous_vad单工模式时,一次ws连接内的每一轮对话【必须】递增更新stmid。 使用continuous双工模式时,stmid固定不变。 "scene": "main", // 平台上,appid下面创建的情景模式 "agent_id":"xxx", // sparkos平台创建生成的是agentid,agentid与scene 选其一即可 "bot_id": "xxx", "msc.lat": 19.65309164062, "msc.lng": 109.259056086, "ver_type": "intelligent_hdw", "os_sys": "android", "interact_mode":"continuous" //交互模式选择,支持continuous(双工)和 continuous_vad(单工)可选,默认continuous }, "parameter": { "iat": { "iat": { "encoding": "utf8", "compress": "raw", "format": "json" }, "vgap":50 }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "true", "personal":"人设 id", "prompt":"prompt 信息, 例如:你是小明,一个小学学生,热爱画画" }, "tts": { "vcn": "x5_lingfeiyi_flow", "res_id": "xxxx", "res_gender":"", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 } }, "avatar": { "avatar_id": "", // "res_id": "", "image": "", "encoding": "", "width": 512, "height": 512 } }, "payload": { "audio": { "status": 0, "audio": "base64的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 } } } ``` ### 请求参数 > 参数名称中的层级使用 `.` 表示,例如 `parameter.iat.vgap`。 | **参数名称** | **类型** | **是否必传** | **含义** | **备注** | **限制** | |---|---|---:|---|---|---| | `header` | object | 是 | 用户参数 | | | | `header.app_id` | string | 是 | AppID | | `maxLength: 15` | | `header.uid` | string | 是 | 授权的用户 ID | AppID 下用户唯一 ID | `maxLength: 64` | | `header.status` | int | 是 | 会话的状态 | `0` 表示第一帧,`1` 表示中间帧,`2` 表示最后一帧 | 可选值:`0`、`1`、`2`、`3` | | `header.stmid` | string | 是 | 会话 ID | 交互轮数,客户端发起的交互计数。使用 `continuous_vad` 单工模式时,一次 WebSocket 连接内的每轮交互计数需要更新;使用 `continuous` 模式时固定不变 | `maxLength: 32` | | `header.scene` | string | 是 | 情景模式(场景) | 从 AIUI 开放平台或飞云平台创建并使用 | `maxLength: 16` | | `header.msc.lat` | float | 否 | 纬度 | | `[-90, 90]` | | `header.msc.lng` | float | 否 | 经度 | | `[-180, 180]` | | `header.ver_type` | string | 否 | 请求来源 | 内部参数,用户请求标记 | `maxLength: 16` | | `header.os_sys` | string | 否 | 设备的系统类型 | | | | `header.interact_mode` | string | 是 | 交互模式 | 默认值为 `continuous` | `continuous`:全双工;`continuous_vad`:单工兼容模式 | | `header.bot_id` | string | 否 | 指定请求的 Bot ID(链路 ID) | 当前可选值包括:`sos_app_deepseekv3`(DeepSeek 回复版本)等 | `maxLength: 64` | | `parameter` | object | 是 | 功能特性参数 | | | | `parameter.iat` | object | 是 | 识别参数部分 | 功能启用时必传 | | | `parameter.iat.iat` | object | 是 | 识别结果描述 | | | | `parameter.iat.iat.encoding` | string | 是 | 识别结果的编码要求 | 当前仅支持 `utf8` | | | `parameter.iat.iat.compress` | string | 是 | 识别结果的压缩类型 | 当前仅支持 `raw` | | | `parameter.iat.iat.format` | string | 是 | 识别结果的格式 | 当前仅支持 `json` | | | `parameter.iat.vgap` | int | 否 | 音频的 Gap 值,即多长时间没有有效音频内容时判定为停顿 | 默认值 `80`,对应 `800 ms`;最小值 `40`,对应 `400 ms`;最大值 `1000`,对应 `10000 ms` | | | `parameter.iat.mode` | int | 否 | 识别模式 | | | | `parameter.nlp` | object | 是 | NLP 参数部分 | 功能启用时必传 | | | `parameter.nlp.nlp` | object | 是 | NLP 结果描述 | | | | `parameter.nlp.nlp.encoding` | string | 是 | NLP 结果的编码要求 | 当前仅支持 `utf8` | | | `parameter.nlp.nlp.compress` | string | 是 | NLP 结果的压缩类型 | 当前仅支持 `raw` | | | `parameter.nlp.nlp.format` | string | 是 | NLP 结果的格式 | 当前仅支持 `json` | | | `parameter.nlp.new_session` | string | 否 | 是否开启新的会话;新会话会清除对话历史 | 传 `"true"` 或 `"global"` 会清空会话历史并重新交互;传 `"false"` 会携带之前的会话历史 | | | `parameter.nlp.prompt` | string | 否 | Prompt 描述,可用于修改系统默认人设 | 当传入 `personal` 参数时,`prompt` 参数不生效;当前不支持占位符 | | | `parameter.tts` | object | 否 | TTS 功能参数 | | | | `parameter.tts.tts` | object | 是 | 合成音频参数 | 功能启用时必传 | | | `parameter.tts.tts.encoding` | string | 是 | 合成音频编码 | 支持 `raw`、`opus-wb`、`opus-swb`;默认值为 `raw` | | | `parameter.tts.tts.sample_rate` | int | 是 | 合成音频采样率 | 支持 `16000`、`24000`;默认值为 `16000`,单位 Hz | | | `parameter.tts.tts.channels` | int | 是 | 合成音频通道数 | 默认值为 `1`,表示单声道 | | | `parameter.tts.tts.bit_depth` | int | 是 | 合成音频位深 | 默认值为 `16` | | | `parameter.tts.vcn` | string | 是 | 发音人 | `1`:支持 SparkOS 发音人,需要对应发音人授权;`2`:支持声音复刻,传入 `"x5_clone"` 时还需传入对应的 `res_id` | | | `parameter.tts.res_id` | string | 否 | 复刻发音人的资源 ID(音色 ID) | 声音复刻相关文档:<https://www.xfyun.cn/doc/spark/reproduction.html> | | | `parameter.tts.res_gender` | string | 否 | 复刻发音人的性别 | | | | `parameter.tts.speed` | int | 否 | 语速 | 取值范围 `0~100`,默认值为 `50` | | | `parameter.tts.volume` | int | 否 | 音量 | 取值范围 `0~100`,默认值为 `50` | | | `parameter.tts.pitch` | int | 否 | 音调 | 取值范围 `0~100`,默认值为 `50` | | | `parameter.avatar` | object | 否 | 虚拟人参数 | 功能启用时必传;仅在虚拟人相关场景下生效 | | | `parameter.avatar.avatar_id` | string | 是 | 虚拟人形象 ID | 平台创建的虚拟人形象 ID | | | `parameter.avatar.image` | string | 否 | 自定义虚拟人形象照片 | 填写图片下载链接;传入该字段时,`avatar_id` 字段无效 | 需满足虚拟人自定义形象照片要求 | | `parameter.avatar.encoding` | string | 否 | 图片类型 | 支持 `jpg`、`png`、`jpeg`、`bmp` | | | `parameter.avatar.width` | int | 否 | 视频分辨率宽度 | 默认值为 `512` | | | `parameter.avatar.height` | int | 否 | 视频分辨率高度 | 默认值为 `512` | | | `payload` | object | 是 | 请求数据载荷 | | | | `payload.audio` | object | 条件必传 | 音频数据部分 | 使用音频交互功能时必传 | | | `payload.audio.audio` | string | 是 | 音频数据 | 音频数据的 Base64 编码。部分语言在序列化时会自动对二进制数据进行 Base64 编码 | | | `payload.audio.status` | int | 是 | 音频数据状态 | `0` 表示首帧音频,`1` 表示中间帧音频,`2` 表示最后一帧音频 | 可选值:`0`、`1`、`2` | | `payload.audio.frame_size` | int | 是 | 音频编码对应的帧大小 | 在二进制状态下,表示音频数据压缩后对应的帧大小;未压缩时传 `0` | | | `payload.audio.encoding` | string | 是 | 音频数据编码格式 | 支持 `raw`、`opus`;默认值为 `raw`。其他格式可联系技术支持 | | | `payload.audio.sample_rate` | int | 是 | 音频数据采样率 | 支持 `8000`、`16000`;默认值为 `16000`,单位 Hz | | | `payload.audio.channels` | int | 是 | 音频数据声道数 | 支持 `1`、`2`;默认值为 `1` | | | `payload.audio.bit_depth` | int | 是 | 音频数据位深 | 支持 `8`、`16`;默认值为 `16`,单位 bit | | --- ## 2.1.5 简化请求数据示例 ### 单工兼容模式:`continuous_vad` 接入要求: - 每轮对话必须更新 `header.stmid`,且不能使用以前使用过的值。 - 例如:`"0"`、`"1"`、`"2"`。 - `header.status` 表示整个会话状态: - 第一个数据包为 `0`; - 后续数据包为 `1`; - 结束会话时为 `2`。 - `payload.audio.status` 表示单轮音频状态: - 每轮从 `0` 开始; - 中间为 `1`; - 本轮结束为 `2`。 - 每轮对话的第一个数据包需要发送完整字段。 - 后续数据包可以省略 `parameter`。 - 后续数据包的 `header` 只需发送关键字段。 ### 第一轮对话 第一轮使用: ```text header.stmid = "0" ``` #### 第一轮首包 ```json { "header": { "app_id": "xxxxxxx", "uid": "uidxxxx", "status": 0, "stmid": "0", "scene": "ai-personality-1", "os_sys": "android", "interact_mode": "continuous_vad", "pers_param": "{\"appid\":\"xxxxxxx\",\"uid\":\"xxxxxxx\"}" }, "parameter": { "iat": { "vgap": 60, "dwa": "wpgs", "iat": { "encoding": "utf8", "compress": "raw", "format": "json" }, "eos": "800", "domain": "sms" }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "false" }, "tts": { "vcn": "x5_lingxiaoyue_flow", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } }, "payload": { "audio": { "status": 0, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` #### 第一轮中间包 ```json { "header": { "app_id": "xxxxxxx", "uid": "xxxxxxx", "status": 1, "stmid": "0", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 1, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` #### 第一轮结束包 ```json { "header": { "app_id": "xxxxxxx", "uid": "xxxxxxx", "status": 1, "stmid": "0", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 2, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` ### 第二轮对话 第二轮必须更新: ```text header.stmid = "1" ``` 第二轮首包状态为: ```text header.status = 1 payload.audio.status = 0 ``` 示例: ```json { "header": { "app_id": "xxxxxxx", "uid": "xxxxxxx", "status": 1, "stmid": "1", "scene": "ai-personality-1", "os_sys": "android", "interact_mode": "continuous_vad", "pers_param": "{\"appid\":\"xxxxxxx\",\"uid\":\"xxxxxxx\"}" }, "parameter": { "iat": { "vgap": 60, "dwa": "wpgs", "iat": { "encoding": "utf8", "compress": "raw", "format": "json" }, "eos": "800", "domain": "sms" }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "false" }, "tts": { "vcn": "x5_lingxiaoyue_flow", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } }, "payload": { "audio": { "status": 0, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` 后续中间包: ```json { "header": { "app_id": "879230fc", "uid": "thzhang4", "status": 1, "stmid": "1", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 1, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` 第二轮结束包: ```json { "header": { "app_id": "xxxxxxx", "uid": "xxxxxxx", "status": 1, "stmid": "1", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 2, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` 第三轮及后续轮次按照相同规则处理: ```text 第三轮:header.stmid = "2" 第四轮:header.stmid = "3" …… ``` 结束整个会话时: ```text header.status = 2 ``` 然后关闭 WebSocket: ```python ws.close() ``` --- ### 双工模式:`continuous` 双工模式要求如下: - `header.stmid` 的值【固定不变】。 - `header.status` 会话的状态: - 第一个数据包为 `0`; - 后续数据包为 `1`; - 结束会话时为 `2`。 - `payload.audio.status`音频的状态: - 第一个音频包为 `0`; - 后续音频包为 `1`; - 结束时为 `2`。 - 第一个数据包发送完整字段。 - 后续数据包可以省略 `parameter`。 - 【持续不断的发送音频】:即使用户没有说话,也要一直发送音频。服务侧会自动判断用户有没有说话。 #### 双工模式首包 ```json { "header": { "app_id": "879230fc", "uid": "thzhang4", "status": 0, "stmid": "0", "scene": "ai-personality-1", "os_sys": "android", "interact_mode": "continuous", "test": "孙悟空", "pers_param": "{\"appid\":\"879230fc\",\"uid\":\"thzhang4\"}" }, "parameter": { "iat": { "vgap": 60, "dwa": "wpgs", "iat": { "encoding": "utf8", "compress": "raw", "format": "json" } }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "false" }, "tts": { "vcn": "x5_lingxiaoyue_flow", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } }, "payload": { "audio": { "status": 0, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` #### 双工模式中间包 ```json { "header": { "app_id": "879230fc", "uid": "thzhang4", "status": 1, "stmid": "0", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 1, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` > 即使用户没有说话,也必须持续发送音频数据。 #### 双工模式结束包 ```json { "header": { "app_id": "879230fc", "uid": "thzhang4", "status": 2, "stmid": "0", "scene": "ai-personality-1" }, "payload": { "audio": { "status": 2, "audio": "Base64 编码的音频数据", "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16 } } } ``` 关闭 WebSocket: ```python ws.close() ``` --- ## 2.2 响应说明 ### 首帧成功响应 首帧数据发送成功后,服务端可能返回以下响应。 在没有错误的情况下,该响应可能省略。通常该响应主要用于返回参数错误、连接错误或鉴权错误。 ```json { "header": { "code": 0, "message": "success", "sid": "xgo00010205@dx192743899eb0001822-audio-1", "status": 0, "stmid": "1" } } ``` ### 结果示例 ```json { "header": { "code": 0, "message": "success", "sid": "xgo00010205@dx192743899eb0001822-audio-1", "status": 0, "stmid": "1" }, "payload": { "event": { "compress": "", "encoding": "", "format": "", "seq": 0, "status": 0, "text": "eyJ0eXBlIjoiVmFkIiwiZGF0YSI6IiIsImtleSI6IkJvcyIsImRlc2MiOnt9fQ==" }, "iat": { "compress": "raw", "encoding": "utf8", "format": "json", "seq": 1, "status": 2, "text": "Base64 编码的识别结果" }, "nlp": { "compress": "", "encoding": "", "format": "", "seq": 0, "status": 0, "text": "Base64 编码的模型回复文本" }, "tts": { "compress": "", "encoding": "", "format": "", "seq": 0, "status": 0, "audio": "Base64 编码的音频数据" }, "cbm_vms": { "compress": "", "encoding": "", "format": "", "seq": 0, "status": 0, "text": "Base64 编码的 VMS connect/start/stop info" } } } ``` ### 异常结果示例 ```json { "header": { "code": 10110, "message": "server licence error", "sid": "xgo00010205@dx192743899eb0001822-audio-1", "status": 2, "stmid": "1" } } ``` ### 2.2.1 响应参数 | **参数名称** | **参数类型** | **含义** | **备注** | |---|---|---|---| | `header` | object | 响应头信息 | | | `header.code` | int | 服务错误码 | `0` 表示成功,其他值表示失败 | | `header.sid` | string | 会话的 SID | | | `header.status` | int | 会话的状态 | 返回顺序通常为 `0 → 1 → … → 1 → 2` | | `header.stmid` | string | 会话 ID | 一次长连接中,每个会话对应一个会话 ID;一次连接中的多次对话…… | | `parameter` | object | 暂无 | | | `payload` | object | 数据字段 | | | `payload.event` | object | 事件数据 | | | `payload.event.text` | base64string | 事件数据内容 | 云端识别服务会判断用户音频的断点信息,并下发相应事件。该字段需要进行 Base64 解码 | | `payload.event.text.type` | string | 事件类型 | 例如:`Vad` | | `payload.event.text.data` | string | 事件附加数据 | | | `payload.event.text.key` | string | 事件标识 | 例如:`Bos`、`Eos`、`Silence` | | `payload.event.text.desc` | object | 事件描述信息 | | | `payload.event.status` | int | 数据状态 | 可选值为 `0`、`1`、`2`:`0` 表示首帧结果,`1` 表示中间帧结果,`2` 表示最后一帧结果 | | `payload.event.encoding` | string | 数据编码格式 | | | `payload.event.seq` | int | 数据段编号 | | | `payload.event.format` | string | 数据内容格式 | | | `payload.event.compress` | string | 数据内容压缩方式 | | | `payload.iat` | object | 识别结果 | 详见下方参数示例 | | `payload.nlp` | object | 回复文本 | 详见下方参数示例 | | `payload.tts` | object | 合成结果 | 详见下方参数示例 | | `payload.cbm_vms` | object | 虚拟人结果 | 详见下方参数示例 | | `payload.cbm_semantic` | object | 语义结果 | 详见下方参数示例 | | `payload.cbm_knowledge` | object | 知识结果 | 详见下方参数示例 | --- ## 事件数据 云端识别服务会判断用户音频的断点信息,并通过 `payload.event.text` 下发事件。 `payload.event.text` 为 Base64 编码数据,客户端需要先执行 Base64 解码。 主要事件包括: | **事件** | **含义** | **处理建议** | |---|---|---| | `Bos` | 检测到用户开始说话 | 可标记一轮用户语音开始 | | `Eos` | 检测到当前子句结束 | 可用于辅助判断单轮语音结束 | | `Silence` | 检测到长时间无人说话或会话结束 | 通常可结束当前会话 | ### Bos 事件 检测到音频中有人说话时触发。 ```json { "type": "Vad", "data": "", "key": "Bos", "desc": {} } ``` ### Eos 事件 检测到音频中的子句说完时触发。 ```json { "type": "Vad", "data": "", "key": "Eos", "desc": {} } ``` > **注意:** `Bos` 和 `Eos` 不保证严格成对出现。VAD 误触发时,可能出现只有 `Bos`、没有 `Eos` 的情况。 ### Silence 事件 检测到长时间无人说话时触发。上传 `status = 2` 时,也可能触发该事件。 ```json { "type": "Vad", "data": "", "key": "Silence", "desc": {} } ``` 收到 `Silence` 后,可根据业务需要结束会话。 --- ## 语义 VAD 结果 新增语义 VAD 结果下发。 | **type** | **key** | **data** | **meta** | **I/O** | **说明** | |---|---|---|---|---|---| | `ability` | `svad` | `<REJECT>` | | `OUT` | 语义 VAD 拒绝了该回复。绝对人人 | --- ## 识别数据 和传统的识别结果少于区别,极速场下的sn和ls字段没有实际意义 识别结果位于:payload.iat.text ```json //以下结果已经 base64decode text的内容 {"sn":1,"ls":false,"bg":0,"ed":0,"pgs":"apd","ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]}]} {"sn":2,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,1],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"1385"}]}]} {"sn":3,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,2],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856"}]}]} {"sn":4,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,3],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"138569"}]}]} {"sn":5,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,4],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"1385690"}]}]} {"sn":6,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,5],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901"}]}]} {"sn":7,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,6],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"138569012"}]}]} {"sn":8,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,7],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"1385690123"}]}]} {"sn":9,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,8],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]}]} {"sn":10,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,9],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]}]} {"sn":11,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,10],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"重"}]},{"bg":0,"cw":[{"sc":0.00,"w":"六"}]}]} {"sn":12,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,11],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"六"}]}]} {"sn":13,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,12],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"六"}]},{"bg":0,"cw":[{"sc":0.00,"w":"千"}]}]} {"sn":14,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,13],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6800"}]}]} {"sn":15,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,14],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6880"}]}]} {"sn":16,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,15],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]}]} {"sn":17,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,16],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]},{"bg":0,"cw":[{"sc":0.00,"w":"点"}]}]} {"sn":18,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,17],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]},{"bg":0,"cw":[{"sc":0.00,"w":"."}]},{"bg":0,"cw":[{"sc":0.00,"w":"8"}]}]} {"sn":19,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,18],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]},{"bg":0,"cw":[{"sc":0.00,"w":"."}]},{"bg":0,"cw":[{"sc":0.00,"w":"8"}]},{"bg":0,"cw":[{"sc":0.00,"w":"元"}]}]} {"sn":20,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,19],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]},{"bg":0,"cw":[{"sc":0.00,"w":"."}]},{"bg":0,"cw":[{"sc":0.00,"w":"8"}]},{"bg":0,"cw":[{"sc":0.00,"w":"元"}]},{"bg":0,"cw":[{"sc":0.00,"w":"话费"}]}]} {"sn":21,"ls":false,"bg":0,"ed":0,"pgs":"rpl","rg":[1,20],"ws":[{"bg":0,"cw":[{"sc":0.00,"w":"给"}]},{"bg":0,"cw":[{"sc":0.00,"w":"13856901234"}]},{"bg":0,"cw":[{"sc":0.00,"w":"充"}]},{"bg":0,"cw":[{"sc":0.00,"w":"6888"}]},{"bg":0,"cw":[{"sc":0.00,"w":"."}]},{"bg":0,"cw":[{"sc":0.00,"w":"8"}]},{"bg":0,"cw":[{"sc":0.00,"w":"元"}]},{"bg":0,"cw":[{"sc":0.00,"w":"话费"}]}]} {"sn":22,"ls":true,"bg":0,"ed":0,"pgs":"apd","ws":[{"bg":0,"cw":[{"sc":0.00,"w":"。"}]}]} ``` ### 字段说明 | **JSON 字段** | **类型** | **描述** | |---|---|---| | `pgs` | string | 开启 WPGS 后会出现此字段。取值为 `"apd"` 时,表示该片结果追加到前面的最终结果;取值为 `"rpl"` 时,表示替换前面的部分结果,替换范围由 `rg` 字段指定。 | | `rg` | array | 替换范围。开启 WPGS 后会出现此字段。假设值为 `[2, 5]`,表示替换第 2 次至第 5 次返回的结果。 | 后台保证返回pgs结果无交叉嵌套。根据参数说明得到上述示例的最终听写结果为 “给13856901234充6888.8元话费。” ### 识别结果解析规则 1. 普通结果,每个识别结果均是独立结果,直接解析即可 2. 事件数据与识别数据无严格的先后顺序。 - 开启pgs后,sn递增,ls=true代表当前子句完成(仅代表子句结束,不建议作为识别完整标记) - 识别结果遵循iat 0 1 2 的顺序,iat.status=2时,代表识别完整 3. PGS结果,需要解析BOS和EOS之前的所有结果进行拼接,最后的结果一般在rst=rlt中 --- ## 回复文本数据 模型回复文本位于: ```text payload.nlp.text ``` 该字段以 Base64 编码,并以流式方式下发。 解码后的文本示例: ```text 今天 天气晴朗 ,气温 10~12℃, 东北风微风 ``` 客户端需要按照返回顺序进行拼接。 --- ## 工具结果数据 工具结果是生成模型回复文本之前,工具或插件执行过程产生的中间结果。 目前可能下发的工具结果包括: | **类型** | **说明** | |---|---| | `cbm_tidy` | 识别文本的改写结果 | | `cbm_retrieval_classify` | 知识分类结果 | | `cbm_semantic` | 语义理解结果,通常包含 `answer`、`semantic`、`data` | | `cbm_knowledge` | 知识检索结果,知识分类命中时返回 | ### `cbm_tidy` ```json { "query": "告诉我明天北京天气怎么样", "intent": [ { "index": 0, "value": "明天北京天气怎么样" } ] } ``` ### `cbm_retrieval_classify` 未命中分类: ```json { "type": 0 } ``` 命中分类: ```json { "type": 1, "keyword": "北京的历史悠久的建筑" } ``` ### `cbm_semantic` ```json { "answer": { "text": "明天北京全天晴转多云,气温-2***********" } } ``` ### `cbm_knowledge` ```json [ { "score": 0, "repoId": "agg_knowledge", "summary": "北京的历史太厚重,为了控制******" } ] ``` --- ## 合成音频数据 合成音频数据位于: ```text payload.tts.audio ``` 客户端需要对该字段执行 Base64 解码,并按照返回的音频参数播放。 需要关注的参数包括: - `encoding` - `sample_rate` - `bit_depth` - `channels` 示例: ```json { "payload": { "tts": { "audio": "Base64 编码的音频数据", "bit_depth": 16, "channels": 1, "encoding": "raw", "frame_size": 0, "sample_rate": 24000, "seq": 1, "status": 0 } } } ``` --- ## 虚拟人数据 使用虚拟人模式时,云端会下发虚拟人视频流地址。 客户端可根据协议类型播放视频流。 目前支持: | **协议** | **说明** | |---|---| | `xrtc` | 讯飞自研协议,需要搭配特定 SDK 使用 | | `rtmp` | 开源协议,暂不开放 | 虚拟人数据包括: - 初始化拉流信息; - 虚拟人阶段性开始播放信息; - 虚拟人阶段性播放结束信息。 ### 初始化视频流地址 ```json { "event_type": "stream_info", "stream_extend": { "appid": "11223****", "user_sign": "Bearer *******" }, "stream_url": "xrtcx******", "vmr_status": 0 } ``` ### 阶段性开始播放 ```json { "event_type": "driver_status", "vmr_status": 0 } ``` ### 阶段性播放结束 ```json { "event_type": "driver_status", "vmr_status": 2 } ``` --- # 三、注意事项 ## 3.1 音频上传注意事项 ### 全双工场景:`interact_mode = continuous` - 上传音频不能中断。 - 必须实时上传录音设备采集到的音频。 - 不可额外添加无关音频。 - 不可间断性丢弃音频。 - 建议每隔 **40 ms** 上传一次音频。 - 当采样率为 `16000 Hz`、位深为 `16 bit`、单声道时,每次建议上传 **1280 字节**。 - 即使用户没有说话,也需要持续发送音频。 - 在嘈杂环境下,可结合识别结果中的 `Eos` 事件,临时关闭录音设备。 - 临时关闭录音设备期间,应继续每隔 40 ms 上传 1280 字节的空白音频字节流。 ### 单工兼容场景:`interact_mode = continuous_vad` - 必须确保每轮 `payload.audio.status` 完整经历 `0 → 1 → 2`。 - 在完成 `0 → 2` 后,音频上传可以中断。 - 云端会在 60 秒内下发本轮音频的交互结果。 - 如果 60 秒内没有上传新一轮音频,连接会被断开。 - 可借助识别结果的 `Eos` 事件主动中断音频上传。 - 主动结束本轮时,需要发送: ```text payload.audio.status = 2 ``` > `continuous_vad` 已在 1.9 版本中合并到 `continuous`,仅保留兼容能力。 ## 3.2 虚拟人交互注意事项 - 虚拟人视频流播放结束数据的下发可能存在约 **100 ms** 延迟。 - 使用虚拟人自定义形象时,应仔细阅读图片格式要求。 - 如果图片不满足要求,会话可能启动失败。 ## 3.3 其他注意事项 - 如果上传的音频过于嘈杂,可能多次触发 VAD 事件。 - 当多次触发 VAD 时,下发的识别文本、回复文本和音频以最后一次 VAD 事件对应的结果为准。 - 云端下发非零错误码后,客户端必须断开连接。 - 出错后的 WebSocket 连接不能继续复用。 - 为节省上传带宽,首帧必须严格按照协议上传完整参数。 - 中间帧可以省略部分字段。 - 中间帧必须保证 `header` 必传参数存在。 - 中间帧可以省略 `parameter`。 --- # 四、Demo 示例 ## 4.1 纯语音交互接入demo(Python代码) 使用已经保存好的音频测试功能 ``` import base64 import hashlib import hmac import json import os import threading import time from datetime import datetime from time import mktime from urllib.parse import urlencode from wsgiref.handlers import format_date_time from websocket import create_connection app_id = "" Key = "" Secret = "" scene="" #音频文件位置 # 音频需要保留足够长的静音段,比如 10s 的音频中,说话部分只在前 2s,用来模拟全双工持续不断的传音频场景 # 音频最好是原始的 raw 格式,wav 也可;不可是其他压缩格式,demo 代码未做压缩编码适配 input_file = r"./wether.wav" audio_file_path = r"./" base_url = "wss://********/v1/openapi/chat" audio_array = "" class AssembleHeaderException(Exception): def __init__(self, msg): self.message = msg class Url: def __init__(this, host, path, schema): this.host = host this.path = path this.schema = schema pass # calculate sha256 and encode to base64 def sha256base64(data): sha256 = hashlib.sha256() sha256.update(data) digest = base64.b64encode(sha256.digest()).decode(encoding='utf-8') return digest def parse_url(requset_url): stidx = requset_url.index("://") host = requset_url[stidx + 3:] schema = requset_url[:stidx + 3] edidx = host.index("/") if edidx <= 0: raise AssembleHeaderException("invalid request url:" + requset_url) path = host[edidx:] host = host[:edidx] u = Url(host, path, schema) return u # build auth request url def assemble_auth_url(requset_url, method="GET", api_key="", api_secret=""): u = parse_url(requset_url) host = u.host path = u.path now = datetime.now() date = format_date_time(mktime(now.timetuple())) # print(date) # date = "Thu, 12 Dec 2019 01:57:27 GMT" signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1".format(host, date, method, path) # print(signature_origin) signature_sha = hmac.new(api_secret.encode('utf-8'), signature_origin.encode('utf-8'), digestmod=hashlib.sha256).digest() signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( api_key, "hmac-sha256", "host date request-line", signature_sha) authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') # print(authorization_origin) values = { "host": host, "date": date, "authorization": authorization } return requset_url + "?" + urlencode(values) def send_data(ws): with open(input_file,"rb") as f: count = 0 status = 0 while True: data = f.read(1280) # 每次读取1280字节 if not data: break # 如果没有数据了,跳出循环,# 暂不考虑音频大小是 1280 的倍数 if count == 0: status = 0 elif len(data) == 1280: # 暂不考虑音频大小是 1280 的倍数 status = 1 else: status = 2 count += 1 content_b64 = base64.b64encode(data).decode() data = { "header": { "app_id": app_id, "uid": "youtestuid", "status": status, "stmid": "1", "scene": scene, "msc.lat": 19.65309164062, "msc.lng": 109.259056086, "ver_type": "test_demo", "os_sys": "android", "interact_mode":"continuous" }, "parameter": { "iat": { "iat": { "encoding": "utf8", "compress": "raw", "format": "json" }, }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "global" }, "tts": { "vcn": "x5_lingfeiyi_flow", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 } }, }, "payload": { "audio": { "status": status, "audio": content_b64, "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 }, } } data_js = json.dumps(data) ws.send(data_js) time.sleep(0.04) def recv_data(ws): # 加载音频文件 audio_array = [] with open('output.pcm', 'wb') as f: while True: result = ws.recv() result_js = json.loads(result) if result_js.__contains__("payload") : if 'event' in result_js['payload']: decoded_bytes = base64.b64decode(result_js['payload']['event']['text']) decoded_str = decoded_bytes.decode('utf-8') result_js['payload']['event']['text'] = decoded_str elif 'iat' in result_js['payload']: decoded_bytes = base64.b64decode(result_js['payload']['iat']['text']) decoded_str = decoded_bytes.decode('utf-8') result_js['payload']['iat']['text'] = decoded_str elif 'nlp' in result_js['payload']: decoded_bytes = base64.b64decode(result_js['payload']['nlp']['text']) decoded_str = decoded_bytes.decode('utf-8') result_js['payload']['nlp']['text'] = decoded_str elif 'tts' in result_js['payload']: if len(result_js['payload']['tts']['audio']) > 0: audio_bytes = base64.b64decode(result_js['payload']['tts']['audio']) audio_array.append(audio_bytes) try: status = result_js["payload"]["tts"]["status"] if status == 2: print("开始写音频") writeAudioFile(audio_array) break except: pass print(str(result_js)) # 将解码后的数据写入到本地文件 def writeAudioFile(audio_array): time_now = str(int(time.time())) file_name = audio_file_path+time_now+".pcm" print(file_name) fp = open(file_name, "wb+") try: for audio in audio_array: fp.write(audio) finally: fp.flush() fp.close() if __name__ == '__main__': path = assemble_auth_url(requset_url=base_url, method="GET", api_key=Key, api_secret=Secret) ws = create_connection(path) thread_send = threading.Thread(target=send_data, args=(ws,)) thread_recv = threading.Thread(target=recv_data, args=(ws,)) thread_recv.start() thread_send.start() thread_recv.join() thread_send.join() ``` ### 直接使用电脑的麦与播放器体验 ```import base64 import hashlib import hmac import json import threading import time from datetime import datetime from time import mktime from urllib.parse import urlencode from wsgiref.handlers import format_date_time from websocket import create_connection import pyaudio app_id = "" Key = "" Secret = "" scene="sos_app" # 默认固定值 base_url = "wss://sparkos.xfyun.cn/v1/openapi/chat" audio_array = [] inPlay = False notFirstVad = False # 音频录制配置 RATE = 16000 # 采样率 CHANNELS = 1 # 单声道 CHUNK = 1280 # 每次读取的音频字节大小 FORMAT = pyaudio.paInt16 # 音频格式 lock = threading.Lock() # 创建线程锁 # 将str转为base64编码格式 def strToBase64(s): strEncode = base64.b64encode(s.encode('utf8')) return str(strEncode, encoding='utf8') def send_data(ws): """ 从麦克风录制音频并实时发送。 """ audio = pyaudio.PyAudio() # 打开音频流 stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) print("开始录音...") count = 0 try: while True: data = stream.read(CHUNK) status = 0 if count == 0 else 1 content_b64 = base64.b64encode(data).decode() # 发送 WebSocket 数据包 data = { "header": { "app_id": app_id, "uid": "xhchen", "status": status, "stmid": "1", "scene": scene, "ver_type": "intelligent_hdw", "os_sys": "android", "interact_mode": "continuous" }, "parameter": { "iat": { "iat": { "encoding": "utf8", "compress": "raw", "format": "json" }, "sem_vad": 1, "vgap": 50 }, "nlp": { "nlp": { "encoding": "utf8", "compress": "raw", "format": "json" }, "new_session": "global", }, "tts": { "vcn": "x5_lingfeiyi_flow", "speed": 50, "volume": 50, "pitch": 50, "tts": { "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 } }, }, "payload": { "audio": { "status": status, "audio": content_b64, "encoding": "raw", "sample_rate": 16000, "channels": 1, "bit_depth": 16, "frame_size": 0 }, } } ws.send(json.dumps(data)) count += 1 time.sleep(0.04) except Exception as e: print("录音发送中断:", e) finally: stream.stop_stream() stream.close() audio.terminate() print("录音已停止") def recv_data(ws): global audio_array, notFirstVad full_decoded_text = "" inTTS = False alreadyEos = False try: while True: result = ws.recv() # 接收 WebSocket 消息 result_js = json.loads(result) #print(result) if result_js.__contains__("header") and result_js["header"].__contains__("code"): if result_js["header"]["code"] != 0 : print(result) if result_js.__contains__("payload"): if 'iat' in result_js["payload"]: decoded_bytes = base64.b64decode(result_js['payload']['iat']['text']) word = json.loads( decoded_bytes.decode('utf-8')) try: word = word["text"]["ws"][0]["cw"][0]["w"] except (KeyError, IndexError): word = "字段不存在" print("question:", word) elif 'nlp' in result_js["payload"]: # 解码 NLP 文本 decoded_bytes = base64.b64decode(result_js['payload']['nlp']['text']) decoded_str = decoded_bytes.decode('utf-8') full_decoded_text += decoded_str # 拼接流式输出的文本 print(f"\r{full_decoded_text}", end="") # 实现单行输出,不换行打印 elif 'event' in result_js["payload"]: decoded_bytes = base64.b64decode(result_js['payload']['event']['text']) vad = json.loads(decoded_bytes).get("key") if vad == "Eos": alreadyEos = True if (vad == "Bos") & alreadyEos: full_decoded_text = "" print("") print("====================新一轮交互=================") if inTTS | inPlay: audio_array = [] elif 'tts' in result_js['payload']: if len(result_js['payload']['tts']['audio']) > 0: inTTS = True audio_bytes = base64.b64decode(result_js['payload']['tts']['audio']) # 解码音频数据并写入到播放流 audio_array.append(audio_bytes) # 检查 TTS 的状态,是否为结束状态 if result_js["payload"]["tts"]["status"] == 2: inTTS = False except Exception as e: print(f"接收数据出错: {e}") def play_audio(): """ 从 audio_array 中读取数据并实时播放。 """ global audio_array,inPlay audio = pyaudio.PyAudio() stream = audio.open(format=pyaudio.paInt16, channels=1, rate=16000, output=True) try: while True: if audio_array: inPlay = True audio_chunk = audio_array.pop(0) # 读取并移除最早的音频数据 if audio_chunk: stream.write(audio_chunk) # 播放音频 except Exception as e: print(f"播放音频出错: {e}") finally: stream.stop_stream() stream.close() audio.terminate() def write_audio_file(audio_array): """ 将接收的音频保存为 PCM 文件。 """ time_now = str(int(time.time())) file_name = f"output_{time_now}.pcm" print("写入音频文件:", file_name) with open(file_name, "wb") as f: for audio in audio_array: f.write(audio) print("音频保存完成") class AssembleHeaderException(Exception): def __init__(self, msg): self.message = msg class Url: def __init__(this, host, path, schema): this.host = host this.path = path this.schema = schema pass # calculate sha256 and encode to base64 def sha256base64(data): sha256 = hashlib.sha256() sha256.update(data) digest = base64.b64encode(sha256.digest()).decode(encoding='utf-8') return digest def parse_url(requset_url): stidx = requset_url.index("://") host = requset_url[stidx + 3:] schema = requset_url[:stidx + 3] edidx = host.index("/") if edidx <= 0: raise AssembleHeaderException("invalid request url:" + requset_url) path = host[edidx:] host = host[:edidx] u = Url(host, path, schema) return u # build auth request url def assemble_auth_url(requset_url, method="GET", api_key="", api_secret=""): u = parse_url(requset_url) host = u.host path = u.path now = datetime.now() date = format_date_time(mktime(now.timetuple())) # print(date) # date = "Thu, 12 Dec 2019 01:57:27 GMT" signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1".format(host, date, method, path) # print(signature_origin) signature_sha = hmac.new(api_secret.encode('utf-8'), signature_origin.encode('utf-8'), digestmod=hashlib.sha256).digest() signature_sha = base64.b64encode(signature_sha).decode(encoding='utf-8') authorization_origin = "api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"" % ( api_key, "hmac-sha256", "host date request-line", signature_sha) authorization = base64.b64encode(authorization_origin.encode('utf-8')).decode(encoding='utf-8') # print(authorization_origin) values = { "host": host, "date": date, "authorization": authorization } return requset_url + "?" + urlencode(values) if __name__ == '__main__': path = assemble_auth_url(requset_url=base_url, method="GET", api_key=Key, api_secret=Secret) ws = create_connection(path) thread_send = threading.Thread(target=send_data, args=(ws,)) thread_recv = threading.Thread(target=recv_data, args=(ws,)) thread_play = threading.Thread(target=play_audio) thread_recv.start() thread_send.start() thread_play.start() thread_play.join() ``` --- ## 4.3 Java 接入注意事项 ### WebSocket 消息类型 WebSocket 标准中的消息类型包括: - Text; - Binary。 使用 Java 接入时,请同时在以下回调中打印服务端返回结果: ```text onMessage onBinaryMessage ``` 通过日志确认服务端结果实际进入了哪个回调。 ### 多轮交互读超时 使用多轮交互时,如果长时间没有开启下一轮会话,WebSocket 连接可能断开。 需要注意客户端设置的: - WebSocket 读超时时间; - 连接保活时间; - 下一轮会话启动间隔。 --- ## 4.4 Go 语言 Demo > 如有需要,可额外提供 Go 语言接入示例。 --- # 五、错误码说明 服务端返回非零错误码后,除非错误说明另有规定,否则客户端应停止发送数据、关闭当前连接并重新建立连接。 | **错误码** | **描述** | **处理方式** | |---:|---|---| | `0` | 成功 | 正常处理 | | `10001` | WebSocket 读取客户端消息失败 | 按照 `status = 0 → 1 → 2` 发送请求体,并在发送 WebSocket Close 消息后关闭连接 | | `10002` | WebSocket 向客户端发送消息失败,一般不会收到此消息 | 同一个 `stmid` 按照 `status = 0 → 1 → 2` 发送,且不要来回切换 `stmid` | | `10008` | AI 能力连接复用错误 | 云端报错后不要复用当前连接 | | `10010` | 识别能力连接失败 | 稍后重试 | | `10012` | 能力连接断开,请勿复用连接 | 关闭当前连接并重新建立连接 | | `10014` | 输入审核错误 | 输入音频内容未通过审核 | | `10019` | 大模型生成结果审核错误 | 输出未通过审核;若能确认输入正常,请清除上下文历史 | | `10024` | 识别结果为空 | 音频中未识别到有效内容 | | `10110` | AI 能力资源不足 | 云端服务异常,请稍后重试 | | `10222` | AI 能力超时 | 云端服务异常,请稍后重试 | | `10223` | AI 能力节点异常 | 云端服务异常,请稍后重试 | | `11201` | 请求时长超限 | 申请或购买相应授权 | | `11202` | 请求 QPS 超限 | 申请或购买相应授权 | | `11203` | 请求并发超限 | 申请或购买相应授权 | | `20000` | 缺少必要参数 | 根据错误消息检查请求体 | | `20001` | 虚拟人连接异常,无法初始化 | 云端服务异常,请稍后重试 | | `20002` | 缺少应用配置 | 确认 AppID 合法且已开通 SparkOS | | `20016` | 请求的 AI 能力不存在 | 确认是否已申请特殊 AI 能力,并确保能力可用 | | `20019` | AI 能力连接失败,包括合成、虚拟人等 | 云端服务异常,请稍后重试 | | `20029` | 自定义虚拟人图片渲染失败,图片不满足格式要求 | 检查图片内容和格式 | | `20098` | 能力调度异常 | 联系技术支持 | | `20099` | 未知错误 | 联系技术支持 | | `500` | 能力调度崩溃或连接时间过长 | 重新建立连接 | | `504` | 能力调度异常,下游服务超时 | 联系技术支持 | ---
aiuiadmin
2026年9月22日 11:52
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码