| V2.5 字段路径 | V3 字段路径 | 备注 |\r
|---|---|---|\r
| `payload.device_id` | `data.device_id` | 字段名一致,位置不同 |\r
-| `payload.outbound_attributes.cfg.*` | `data.audio_config.output.*` | 输出音频参数归一 |\r
-| `payload.inbound_attributes.cfg.*` | `data.audio_config.input.*` | 输入音频参数归一 |\r
-| `payload.inbound_attributes.disable_vad` | `data.session_mode` | 语义变化:V3 用会话模式显式表达 |\r
-| `payload.simple_response` | `data.output_mode` + LLM 事件处理 | V3 不再用该开关表达“增量/完整”同一概念 |\r
+| `payload.outbound_attributes.cfg.*` | `data.audio_config.output.*` | 仅 `format/sample_rate/bit_rate/channel/speech_rate` |\r
+| `payload.outbound_attributes.cfg.voice` | `data.metadata.voice`(或 agent 绑定) | **V3 audio_config 无 voice** |\r
+| `payload.outbound_attributes.cfg.chunk_interval_ms` | `data.metadata.tts_cfg_extra` | **V3 audio_config 无此字段** |\r
+| `payload.outbound_attributes.cfg.emotion` | (V3 不传) | V3 无 emotion;端侧 V2.5 仍可在 outbound 使用 |\r
+| `payload.outbound_attributes.cfg.volume/chunk_interval_ms` | (V3 不传) | 同上 |\r
+| `payload.inbound_attributes.cfg.*` | `data.audio_config.input.*` | 仅 `format/sample_rate/bit_rate/channel` |\r
+| `payload.inbound_attributes.cfg.keywords` 等 | `data.metadata.asr_cfg_extra` | V3 input 无热词等字段 |\r
+| `payload.inbound_attributes.disable_vad=false` | `session_mode: "realtime"` | 长对话/VAD 自动断句;无需 audio.create |\r
+| `payload.inbound_attributes.disable_vad=true` | `session_mode: "push_to_talk"` | 按键说话;须 audio.create → 帧 → audio.completed |\r
+| `payload.simple_response: true` | `output_mode: "audio"` | 不订阅 LLM 文本流;与 `both`/`text` 区分 |\r
+| `payload.dialog_attributes` / `bot_option` | `agent_id` + `metadata` | V3 无 `dialog_attributes` 顶层;须服务端约定映射 |\r
\r
---\r
\r
2) **别再等 `asr.completed` 才 cancel**:V3 cancel 面向打断 LLM/TTS,但要看策略。 \r
3) **TTS 结束事件名不同**:V2.5 写 `tts.completed`;V3 是 `tts.done`。 \r
4) **ASR 字段名不同**:V2.5 `content`;V3 `text`,并有 `is_final`。 \r
-5) **V2.5 的某些扩展事件 V3 文档没写**:先确认再迁移,避免上线缺功能。\r
+5) **V2.5 的某些扩展事件 V3 文档没写**:先确认再迁移,避免上线缺功能。 \r
+6) **本工程 Start 出站形态与 V2.5 文档示例不完全一致**:代码用 `header.event="Start"` + `payload`,recv 侧按顶层 `event` 解析;切 V3 时须统一走适配层,勿只改 start 不改 cancel/synthesis。\r
+\r
+---\r
+\r
+## 14. 本工程(7PIN_1_5 / k8)现状与代码锚点\r
+\r
+当前固件实现的是 **金三 V2.5 语义**(`event`/`payload` 或 `header.event` + `payload`),与 `vc_fsm` / 按键 UX 已深度绑定。\r
+\r
+| 职责 | 文件 | 关键符号 |\r
+|------|------|----------|\r
+| WS 建链 URL | `vc_session_io.py` | `websocket_connect_VC()`:`VC_WS_PATH` + `?is_auth=&dt={token}` |\r
+| 会话 start | `vc_session_io.py` | `AI_client_VC_start_requset()`:`header.event="Start"` + `inbound_attributes` / `outbound_attributes` / `dialog_attributes` |\r
+| 下行分发 | `vc_session_io.py` | `AI_client_VC_recv()`:`jsload_data["event"]`、`payload` |\r
+| PTT 音频控制 | `vc_session_media.py` | `AI_client_VC_send_speech()` / `end_speech()`:`audio.create` / `audio.completed` |\r
+| TTS 合成 / 播完上报 | `vc_session_media.py` | `AI_client_VC_synthesis()`、`AI_client_VC_audio_played()` |\r
+| 状态机 | `vc_fsm.py` | `VC_mode` 0=长对话(VAD on) ↔ realtime;1=PTT ↔ push_to_talk |\r
+| 产品配置 | `products/configs/k8/default_config.py` | `VC_WS_PATH = "vc/agent"` |\r
+| 环境主机 | `default_srv.py` / `srv_configs/*` | `AMP_SERVER`(`ws://{AMP_SERVER}/...`) |\r
+\r
+**与 V2.5 文档的差异(实现层,非阻塞迁移):**\r
+\r
+- 出站 **Start** 使用 `header.event: "Start"`(大写),文档示例为顶层 `"event": "start"`。\r
+- 出站 **audio.create / audio.completed** 放在 `header.event` 下;**cancel / synthesis** 为顶层 `event` + `payload`(`_vc_out_json_event_meta` 已兼容两种形态)。\r
+- **长对话**(`VC_mode==0`)在 start 里设 `disable_vad=False`;**PTT** 默认 `disable_vad=True`,与 V3 的 `session_mode` 一一对应。\r
+- **`audio.played`** 在 `vc_common.py` 注释中明确为 V2.5 服务端门控;`vc_fsm` 的 `await_played` 依赖本地播完 + 上报。\r
+\r
+---\r
+\r
+## 15. 最简单可配置迁移方案(推荐)\r
+\r
+原则:**配置切换协议版本 + 单一适配层归一化事件名/字段**;`vc_fsm`、按键、`vc_session_media` 上行门控 **尽量不改**,避免拆东墙补西墙。\r
+\r
+### 15.1 仅增配置项(`default_config.py`,k8 量产可覆盖)\r
+\r
+```python\r
+# 协议版本:v2.5 | v3\r
+VC_PROTOCOL_VERSION = "v2.5"\r
+\r
+# WebSocket 路径段(无首尾斜杠)\r
+# v2.5: vc/agent | vc/chat\r
+# v3: rvc-service/api/v3/ws\r
+VC_WS_PATH = "vc/agent"\r
+\r
+# 鉴权 Query 风格(与 get_dev_token / DMP 链路配合)\r
+# dmp_dt → ...?dt={token}(现状)\r
+# api_key → ...?api_key={key}&device_id={imei}(V3 ApiKey 模式,需服务端发 key)\r
+VC_WS_AUTH_MODE = "dmp_dt"\r
+\r
+# V2.5 专有:播完上报 audio.played;V3 文档无此事件,应置 0,改由 tts.done + 本地播完驱动\r
+VC_AUDIO_PLAYED_ACK = 1\r
+\r
+# V3 start:agent_id(对应现网 bot);workflow 等放进 metadata(须与 RVC 服务端约定)\r
+VC_AGENT_ID = None # None 时用 VC_BOT_ID\r
+\r
+# 可选:首版不启用 V3 音频帧加密\r
+VC_AUDIO_ENCRYPTION = 0\r
+```\r
+\r
+`default_srv.py` 仍只负责 `AMP_SERVER`;**路径与协议版本放在 `default_config`**,与现有 `VC_WS_PATH` 分工一致。\r
+\r
+### 15.2 新增薄适配层(建议 `vc_proto.py`,约 120 行)\r
+\r
+只暴露 4 个函数,所有 JSON 进出 WS 经此层:\r
+\r
+| 函数 | 作用 |\r
+|------|------|\r
+| `vc_proto_build_ws_url(amp_server, path, auth_mode, token, device_id)` | 统一拼 `websocket_connect_VC` 的 URL |\r
+| `vc_proto_pack_client(event, payload)` | 出站:`v2.5` → `event/payload` 或 `header`;`v3` → `{type, data}` |\r
+| `vc_proto_unpack_server(raw_dict)` | 入站:解析后 **归一化为 V2.5 内部事件名 + payload 字典** |\r
+| `vc_proto_build_start(vc_session)` | 按版本生成 start/Start 整包 |\r
+\r
+**入站别名表(V3 → 内部仍用 V2.5 名,FSM/recv 零改动):**\r
+\r
+| V3 `type` | 内部 `event`(现有代码) |\r
+|-----------|-------------------------|\r
+| `error` | `failed` |\r
+| `tts.done` | `tts.completed` |\r
+| `llm.started` | `llm.created` |\r
+| 其余同名 | 不变(`started`、`asr.*`、`vad.*`、`cancel` 等) |\r
+\r
+**payload 字段归一(在 `unpack` 内完成):**\r
+\r
+- `asr.result`:`data.text` → `payload.content`(保留现有 `asr.result` 已尝试 `text` 的兼容逻辑)。\r
+- `llm.delta`:`data.delta` → 若业务读 `payload.content` 则同步写入。\r
+- `failed`/`error`:`data.code/message/details` → `payload.error` + `payload.message`(供 `websocket_error` 使用)。\r
+\r
+### 15.3 改动面最小化(仅 6 个 choke point)\r
+\r
+| # | 位置 | 改动 |\r
+|---|------|------|\r
+| 1 | `websocket_connect_VC` | URL 改为 `vc_proto_build_ws_url(...)` |\r
+| 2 | `AI_client_VC_start_requset` | 仍构建 V2.5 JSON;`AI_client_VC_send` 内 `vc_proto_pack_wire` 转 V3 start |\r
+| 3 | `AI_client_VC_send` | 统一 `vc_proto_pack_wire`(所有 JSON 出站,含 cancel/audio.*/synthesis) |\r
+| 4 | `AI_client_VC_recv` 解析后 | `ev, payload, env = vc_proto_unpack_server(jsload_data)`,后续仍用 `jsload_data["event"]` 可写回归一结果 |\r
+| 5 | `AI_client_VC_audio_played` | `if VC_AUDIO_PLAYED_ACK: ...`;V3 直接走现有 `_vc_fire_local_play_end` 链 |\r
+| 6 | `vc_common._vc_out_json_event_meta` | 同时识别 `type`(V3 出站日志) |\r
+\r
+**明确不先改:** `vc_fsm.py` 事件名、`vc_key_router`、录音上行门控(`VC_PROTO_GUARDS`)、二进制 PCM 发送逻辑。\r
+\r
+### 15.4 K8 产品:start 字段映射(V2.5 → V3)\r
+\r
+| 现网 `AI_client_VC_start_requset` | V3 `start.data` |\r
+|----------------------------------|-----------------|\r
+| `VC_mode==0` → `disable_vad=False` | `session_mode: "realtime"` |\r
+| `VC_mode==1` → `disable_vad=True` | `session_mode: "push_to_talk"` |\r
+| (固定音频对话) | `input_mode: "audio"`, `output_mode: "audio"` |\r
+| `simple_response: True` | `output_mode: "audio"`(不订阅 LLM 文本流;无需 `both`) |\r
+| `outbound_attributes.cfg.format/sr/...` | `audio_config.output.*`(仅 format/sample_rate/bit_rate/channel/speech_rate) |\r
+| `outbound_attributes.cfg.voice` | `metadata.voice` | 仅音色进 metadata |\r
+| `outbound_attributes.cfg.emotion/volume/chunk_interval_ms` | (不传) | V3 无这些字段 |\r
+| `inbound_attributes.cfg.*` + `vad_silence` | `audio_config.input.*`;静音阈值若 V3 无独立字段 → `metadata.vad_silence` **须服务端确认** |\r
+| `interrupt_options.thinking_time_only` | `interrupt_options.strategy: "thinking_time_only"`(V3 文档支持) |\r
+| `dialog_attributes.cfg.bot_option` | `agent_id` + `metadata`(`bot_id`/`workflow_id`/`custom_variables`) |\r
+| `payload.device_id` | `data.device_id` |\r
+| DMP `devToken` in `custom_variables` | `metadata` 或继续 DMP Query `dt` |\r
+\r
+### 15.5 分阶段上线建议\r
+\r
+| 阶段 | 配置 | 验证点 |\r
+|------|------|--------|\r
+| P0 联调 | `VC_PROTOCOL_VERSION=v3`,`VC_AUDIO_ENCRYPTION=0`,`VC_AUDIO_PLAYED_ACK=0` | 建链、start、started、长对话连续 3 轮、PTT 短按/长按 |\r
+| P1 行为对齐 | 同上 + 打断/cancel 策略 | thinking 期打断、PTT 过短取消、Power Close |\r
+| P2 可选 | `VC_AUDIO_ENCRYPTION=1` | 仅二进制帧加解密,JSON 仍明文 |\r
+| 保持 V2.5 | 默认不改配置 | 回归现有金三网关 |\r
+\r
+**上线前必须向 RVC 服务端确认:**\r
+\r
+1. K8 使用的 **Dify bot_id / workflow_id** 在 V3 下是 `agent_id` 还是 `metadata.inputs`。\r
+2. **`audio.played` 无等价事件** 时,关键词打断 / 连续对话是否仅依赖 `tts.done` + 本地播完。\r
+3. **`command` / `audio.convert.*`** 是否仍走旧 `/vc/agent` 或另有 HTTP/扩展 type。\r
+\r
+---\r
+\r
+## 16. 迁移改造 Checklist(端侧,含本仓库)\r
+\r
+在 §12 通用项基础上,按本工程勾选:\r
+\r
+- [x] `default_config.py` 增加 `VC_PROTOCOL_VERSION` / `VC_WS_AUTH_MODE` / `VC_AUDIO_PLAYED_ACK`(`configs/k8/default_config.py`)\r
+- [x] 新增 `vc_proto.py`(pack/unpack/url;start 经 `pack_wire` 转换)\r
+- [x] `vc_session_io.websocket_connect_VC` 使用 `vc_proto_build_ws_url`\r
+- [x] `AI_client_VC_start_requset`:仍构建 V2.5 结构,经 `AI_client_VC_send` → `vc_proto_pack_wire` 发 V3\r
+- [x] 所有 JSON 出站经 `AI_client_VC_send` → `vc_proto_pack_wire`(含 cancel/audio.*/synthesis)\r
+- [x] `AI_client_VC_recv` 入站 `vc_proto_normalize_server`\r
+- [x] `VC_AUDIO_PLAYED_ACK=0` 时 `play_end` 自动开麦(长对话):V3 无 `dialog_id` 时用 `_turn_has_asr_completed` / `_vc_expect_dialog_tts` 标 `kind=dialog`(`vc_session_io._vc_classify_llm_tts_kind`)\r
+- [ ] PTT:`session_mode=push_to_talk` 下 `audio.create`/`audio.completed` 行为不变 — **需实机 V3 联调**\r
+- [ ] 留言 / `audio.convert` / `command`:确认 V3 不支持则 **保持 v2.5 路径或单独开关**\r
+- [ ] 联调环境:`AMP_SERVER` + `VC_WS_PATH=rvc-service/api/v3/ws`(`default_srv` + k8 `default_config`)\r
+\r
+---\r
+\r
+## 17. 配置示例(k8 切 V3 联调)\r
+\r
+```python\r
+# products/configs/k8/default_config.py(或 usr 覆盖)\r
+VC_PROTOCOL_VERSION = "v3"\r
+VC_WS_PATH = "rvc-service/api/v3/ws"\r
+VC_WS_AUTH_MODE = "dmp_dt" # 仍用设备 token 时\r
+VC_AUDIO_PLAYED_ACK = 0\r
+VC_AGENT_ID = VC_BOT_ID # 或服务端分配的 agent_id\r
+VC_AUDIO_ENCRYPTION = 0\r
+```\r
+\r
+```python\r
+# products/srv_configs/beike_dev/default_srv.py(示例,按实际 RVC 主机修改)\r
+AMP_SERVER = "115.190.94.208:8087" # 文档示例 ws://host:8087/rvc-service/api/v3/ws\r
+```\r
+\r
+编译:`new_clean.bat k8 1`(与 `.cursor/rules/project.mdc` 一致)。\r
\r
import websocket_mask
LOGGER = log.getLogger(__name__)
+
+def _vc_ws_native_ping_tx_enabled():
+ """1=客户端主动发 RFC6455 OP_PING;0=不发。入站 OP_PING 始终回 OP_PONG(与配置无关)。"""
+ try:
+ import vc_config
+ return int(vc_config.get_int("VC_WS_NATIVE_PING_ENABLE", 0)) != 0
+ except Exception:
+ return False
+
+
# Opcodes
OP_CONT = 0x0
OP_TEXT = 0x1
# Ignore this frame, keep waiting for a data frame
continue
elif opcode == OP_PING:
- # We need to send a pong frame
- if self.debug: LOGGER.info("Sending PONG")
+ if self.debug:
+ LOGGER.info("Sending PONG")
self.write_frame(OP_PONG, data)
- # And then wait to receive
continue
elif opcode == OP_CONT:
# This is a continuation of a previous frame
self.write_frame(opcode, buf)
def ping(self):
- self.write_frame(OP_PING,b'ping')
+ if _vc_ws_native_ping_tx_enabled():
+ self.write_frame(OP_PING, b'ping')
def close(self, code=CLOSE_OK, reason=''):
"""Close the websocket."""
$(BUILD)/version_util.mpy \
$(BUILD)/motor.mpy \
$(BUILD)/vc_common.mpy \
+ $(BUILD)/vc_proto.mpy \
$(BUILD)/vc_utils.mpy \
$(BUILD)/vc_led.mpy \
$(BUILD)/vc_hw.mpy \
# -*- coding: utf-8 -*-
-# k8 产品 — 设备侧默认配置(编译期 freeze 进固件)
+# k8v3 产品 — 设备侧默认配置(RVC WebSocket V3;编译期 freeze 进固件)
#
# 加载优先级(vc_config.py):
# 1) usr/default_config.py(现场覆盖)
-# 2) 本文件(由 new_clean.bat 按 product_k8.ini 的 DEV_TYPE 拷贝到 core/default_config.py)
+# 2) 本文件(由 new_clean.bat 按 product_k8v3.ini 的 DEV_TYPE 拷贝到 core/default_config.py)
#
# 约定:仅放「机型/产品差异」常量,勿放运行态变量。
3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
}
+# 录音麦克增益:record.gain_set(codec_gain, dsp_gain)
+# codec_gain 0~4(编解码模拟增益 默认4);dsp_gain -36~12(数字增益,原工程常用 10)
+RECORD_CODEC_GAIN = 2
+RECORD_DSP_GAIN = 6
+
# =============================================================================
# 5. 可选外设与协议
# =============================================================================
UART_SUPPORTED = False
BT_OTA_ENABLED = False
UPLOAD_START_TONE_ENABLED = False # 开始上传语音时是否播成功提示音
+AI_ALARM_ENABLED = False # K8 吊坠无闹钟/日程硬件
# =============================================================================
# 6. 按键逻辑(AI_dev / btn_device)
# 7. VC 会话 / WebSocket / 空闲与休眠
# =============================================================================
-# 语音 WS 路径段(无首尾斜杠);完整 URL 见 default_srv.AMP_SERVER + 文首拼接说明
-VC_WS_PATH = "vc/agent"
-VC_WS_PING_INTERVAL_SEC = 55 # 文本 ping 间隔,过大影响保活,过小费电
+# 语音 WS 路径段(无首尾斜杠);主机见 srv_configs/beike_dev_v3/default_srv.py AMP_SERVER
+VC_WS_PATH = "rvc-service/api/v3/ws"
+VC_WS_NATIVE_PING_ENABLE = 0 # 0=不发标准 PING(入站 PING 仍回 PONG);1=客户端也发标准 PING
+VC_WS_PING_INTERVAL_SEC = 0 # V3 无自定义文本心跳;仅 V2.5 使用(>0 时发 "ping")
+
+# VC 协议:v3(RVC);环境 SRV_TYPE=beike_dev_v3
+VC_PROTOCOL_VERSION = "v3"
+VC_WS_AUTH_MODE = "dmp_dt" # dmp_dt | api_key
+VC_AUDIO_PLAYED_ACK = 0 # V2.5 播完上报 audio.played;V3 联调建议 0
+VC_AGENT_ID = None # V3 start.agent_id;None 时用 VC_BOT_ID
+VC_AUDIO_ENCRYPTION = 0 # V3 二进制帧加密(首版保持 0)
+# VC_API_KEY = "" # VC_WS_AUTH_MODE=api_key 时使用
+VC_PROTO_TX_LOG = 1 # 1=打印出站摘要 + 原始 JSON(过长分 json_partN 输出)
+
+# V3 start.welcome_options:0=关闭服务端自动欢迎 TTS(由设备 synthesis/notify 播报)
+VC_WELCOME_SERVER_ENABLED = 0
+# VC_WELCOME_SERVER_MESSAGE = "" # VC_WELCOME_SERVER_ENABLED=1 时可选文案
+
+# 协议 failed / websocket_error 是否 synthesis 播报错误原文(联调=1,量产=0)
+VC_ERROR_TTS_ENABLED = 1
-# 长对话:无有效业务下行累计达此秒数结束会话(代码最小 5)
+# 长对话 L1:轮次间隙无 TTS 下行累计达此秒 Close(听/想/播/录、保活帧不计;最小 5)
VC_IDLE_AUTO_CLOSE_SEC = 20
VC_IDLE_AUTO_CLOSE_SEC_PTT = 0 # 单对话预留,当前与 idle 统一处理
VC_IDLE_DEEP_SLEEP_SEC = 60
VC_IDLE_DEEP_SLEEP_ENABLE = 1
-VAD_SILENCE_DEFAULT = 300 # VAD 静音阈值(ms)
+# 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖
+VAD_SILENCE_DEFAULT = 300
# =============================================================================
# 8. 语音对话 / TTS / 智能体
POWER_ON_TONE_ENABLED = True
POWER_ON_TONE_DEFAULT = "你好,我来了"
-VC_TTS_VOICE_TYPE_DEFAULT = None # dict 或 None
-VC_TTS_VOICE_KEY_DEFAULT = None # 如 "男" / "童"
+# 默认音色(无 storage / usr 覆盖时生效)
+# - voice:start → audio_config.output.voice,须与 RVC V3 服务端已配置的 speaker 一致
+# - 联调若报 resource ID mismatched with speaker,改 voice 或让服务端绑定该音色
+VC_TTS_VOICE_KEY_DEFAULT = "男"
+VC_TTS_VOICE_TYPE_DEFAULT = {
+ "男": {
+ "bot_name": "柒先生",
+ "voice": "zh_male_wennuanahu_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "女": {
+ "bot_name": "柒妹妹",
+ "voice": "zh_female_wanwanxiaohe_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "童": {
+ "bot_name": "柒乐乐",
+ "voice": "ICL_zh_female_huoponvhai_tob",
+ "lang": "zh-cn",
+ },
+}
DIALOG_BOT_OPTION = None # {"bot_id", "workflow_id", "use_workflow"}
VC_BOT_ID = "7462617881076351002"
KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+ASR_EMPTY_HINT_TTS = "我没有听清,请再说一遍" # asr.completed 无有效识别时 Notify 播报
+
# =============================================================================
-# 10. 开录音 / PTT 时序(k8 量产:仅 start.mp3 开录提醒,无按键确认音)
+# 10. 开录音 / PTT 时序(对齐「录音上传与播放」设计稿)
# =============================================================================
+# 无按键确认音:不按 key_prompt;仅按键/PTT 开录时播 start.mp3(自动续聊不播)
+# PTT idle 短按:notify 话术 PTT_HINT;长按开录:start.mp3
-RECORD_START_TONE_FILE = "U:/static/start.mp3"
+RECORD_START_TONE_FILE = "U:/static/start.mp3" # 按键/PTT 开录提示音(非 server.started 自动开麦)
RECORD_START_TONE_GAP_MS = 180
+PTT_MIN_RECORD_MS = 400
+PTT_RECORD_TOO_SHORT_TTS = "录音时间太短,请按住圆形按钮说话" # notify
+
PTT_RELEASE_END_SPEECH_DELAY_MS = 200 # 松手后发 audio.completed 前延迟
PTT_CANCEL_WAIT_MS = 2500 # 打断后等 canceled 最长时间
PTT_POST_MUTE_MS = 30 # 本地硬静音后 codec 收尾
# =============================================================================\r
#\r
# 编译:\r
-# product_k8.ini 中 SRV_TYPE=beike_dev\r
+# product_k8v3.ini 中 SRV_TYPE=beike_dev_v3\r
# → new_clean.bat 拷贝本文件 → core/default_srv.py → 打进固件\r
#\r
# 运行时优先级(srv_config.py):\r
# 【语音对话 WebSocket — AI_VC.websocket_connect_VC】\r
# 主机:constants.AMP_SERVER(本文件 AMP_SERVER)\r
# → ai_VC 赋给 self.server_ip(可被存储 server_ip / 云端下发覆盖)\r
-# 路径:default_config.VC_WS_PATH(vc_config,如 k8 为 "vc/agent")\r
-# 查询:is_auth、设备 token(get_dev_token)\r
+# 路径:default_config.VC_WS_PATH(k8v3 为 "rvc-service/api/v3/ws")\r
+# 查询:DMP dt(get_dev_token)或 api_key(见 VC_WS_AUTH_MODE)\r
#\r
-# 拼接结果:\r
-# req_str = "{VC_WS_PATH}?is_auth={is_auth}"\r
-# 有 token: ws://{AMP_SERVER}/{req_str}&dt={token}\r
-# 无 token: ws://{AMP_SERVER}/{req_str}\r
-#\r
-# 示例(beike_dev + k8):\r
-# ws://118.145.158.207:22080/vc/agent?is_auth=...&dt=...\r
+# 示例(beike_dev_v3 + k8v3,RVC V3 API 文档):\r
+# ws://115.190.94.208:8087/rvc-service/api/v3/ws?is_auth=...&dt=...\r
#\r
# 【IoT 设备云 WebSocket — protocol.py】\r
# 完整 URL 已在本文件写死为 IOT_WS_URL,仅追加 token:\r
# =============================================================================\r
\r
# ---- 环境标识(仅注释/运维用;constants.SERVER_ENV 会读取)----\r
-SERVER_ENV = "beike_dev"\r
+SERVER_ENV = "beike_dev_v3"\r
\r
# ---- 三套「根地址」(constants.py 启动时加载,改环境必改)----\r
\r
# DMP 业务 HTTP 根路径(无尾斜杠)。用于 DMP_BASE + "/dev/..." 、"/ota/result?..."\r
DMP_BASE = "http://dmp-dev.raisound.com/dmp-api"\r
\r
-# 语音对话 AMP 主机 host:port(无 ws://)。用于 ws://{AMP_SERVER}/{VC_WS_PATH}...\r
-# 须与 default_config.VC_WS_PATH 联调;勿漏改 VC_WS_PATH(在 configs/k8/default_config.py)\r
-AMP_SERVER = "118.145.158.207:22080"\r
+# RVC V3 语音对话主机 host:port(无 ws://)。完整 WS:\r
+# ws://{AMP_SERVER}/{VC_WS_PATH} 例见 configs/k8v3/default_config.py\r
+AMP_SERVER = "115.190.94.208:8087"\r
\r
# IoT 长连接完整 WebSocket URL(含 ws://)。protocol 仅追加 ?bz-dt=token\r
IOT_WS_URL = "ws://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1"\r
res = list(res)
EventMesh.debug_log('总usr空间:{} MB,剩余空间:{} MB'.format((((res[0] * res[2]) / 1024 / 1024)),((res[0] * res[3]) / 1024 / 1024)))
EventMesh.debug_log("init1:{}".format(utime.localtime()))
-
-
+ try:
+ try:
+ import vc_config # noqa: F401 — 尽早加载产品 GPIO/时长
+ except Exception:
+ pass
+ try:
+ from usr.motor import motor_boot_vibrate_kick_early
+ except Exception:
+ from motor import motor_boot_vibrate_kick_early
+ motor_boot_vibrate_kick_early()
+ except Exception as e:
+ usys.print_exception(e)
+
EventMesh.debug_log("init2:{}".format(utime.localtime()))
EventMesh.debug_log("init3:{}".format(utime.localtime()))
\r
motor_pin = None\r
_motor_active_low = 0\r
+_boot_vibrate_kicked = 0\r
+_boot_vibrate_timer = None\r
\r
\r
def _cfg_motor_gpio_name():\r
motor_pin.write(1 if _motor_active_low else 0)\r
\r
\r
+def motor_boot_vibrate_kick_early():\r
+ """开机尽早震动(main_init 早期调用);与 motorEventManager 注册幂等。"""\r
+ global _boot_vibrate_kicked, _boot_vibrate_timer\r
+ ms = 0 if _boot_vibrate_kicked else _cfg_boot_vibrate_ms()\r
+ if ms <= 0:\r
+ return\r
+ try:\r
+ _ensure_motor_pin()\r
+ open_motor()\r
+ _boot_vibrate_kicked = 1\r
+\r
+ def _boot_vibe_stop(*args):\r
+ try:\r
+ close_motor()\r
+ except Exception:\r
+ pass\r
+\r
+ try:\r
+ _boot_vibrate_timer.stop()\r
+ except Exception:\r
+ pass\r
+ _boot_vibrate_timer = osTimer()\r
+ _boot_vibrate_timer.start(ms, 0, _boot_vibe_stop)\r
+ print("[motor] boot vibrate early {}ms".format(ms))\r
+ except Exception as e:\r
+ print("[motor] boot vibrate early fail:{}".format(e))\r
+\r
+\r
class motorEventManager(Abstract):\r
def __init__(self):\r
self.motor_timer = osTimer()\r
EventMesh.publish("publish_message_event_motorInfo",{"func":self.motor_fun_cb,"data":0}) \r
\r
def post_processor_after_instantiation(self):\r
- ms = _cfg_boot_vibrate_ms()\r
- if ms <= 0:\r
- return\r
- ticks = _motor_vibrate_ticks(ms)\r
- self.motor_count_tmp = 0\r
- self.set_motor_time(None, [1, 1, ticks, 0, 0])\r
+ motor_boot_vibrate_kick_early()\r
\r
# msg 0马达使能 1 马达振动时间100ms单位 2 震动次数 3 间隔时间振动 4 是否需要长振 5 长振时间 6间隔时间长振 7长振次数1一次 要为200ms的倍数\r
def set_motor_time(self,event,msg):\r
VC_WS_PATH = "vc/agent"
VC_WS_PING_INTERVAL_SEC = 55 # 文本 ping 间隔,过大影响保活,过小费电
+# VC 协议:v2.5(金三,默认)| v3(RVC);切 v3 时改 VC_WS_PATH 与 AMP_SERVER 联调
+VC_PROTOCOL_VERSION = "v2.5"
+VC_WS_AUTH_MODE = "dmp_dt" # dmp_dt | api_key
+VC_AUDIO_PLAYED_ACK = 1 # V2.5 播完上报 audio.played;V3 联调建议 0
+VC_AGENT_ID = None # V3 start.agent_id;None 时用 VC_BOT_ID
+VC_AUDIO_ENCRYPTION = 0 # V3 二进制帧加密(首版保持 0)
+# VC_API_KEY = "" # VC_WS_AUTH_MODE=api_key 时使用
+VC_PROTO_TX_LOG = 0 # 1=打印 start/synthesis 等出站 TTS 参数摘要
+
+# 协议 failed 是否 synthesis 播报错误原文(联调=1,量产=0)
+VC_ERROR_TTS_ENABLED = 0
+
# 长对话:无有效业务下行累计达此秒数结束会话(代码最小 5)
VC_IDLE_AUTO_CLOSE_SEC = 20
VC_IDLE_AUTO_CLOSE_SEC_PTT = 0 # 单对话预留,当前与 idle 统一处理
VC_IDLE_DEEP_SLEEP_ENABLE = 1
# 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖
-VAD_SILENCE_DEFAULT = 1500
+VAD_SILENCE_DEFAULT = 300
# =============================================================================
# 8. 语音对话 / TTS / 智能体
POWER_ON_TONE_ENABLED = True
POWER_ON_TONE_DEFAULT = "你好,我来了"
-VC_TTS_VOICE_TYPE_DEFAULT = None # dict 或 None
-VC_TTS_VOICE_KEY_DEFAULT = None # 如 "男" / "童"
+# 默认音色(无 storage / usr 覆盖时生效)
+# - voice:start → outbound_attributes.cfg.voice(金三 V2.5)
+VC_TTS_VOICE_KEY_DEFAULT = "男"
+VC_TTS_VOICE_TYPE_DEFAULT = {
+ "男": {
+ "bot_name": "柒先生",
+ "voice": "zh_male_wennuanahu_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "女": {
+ "bot_name": "柒妹妹",
+ "voice": "zh_female_wanwanxiaohe_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "童": {
+ "bot_name": "柒乐乐",
+ "voice": "ICL_zh_female_huoponvhai_tob",
+ "lang": "zh-cn",
+ },
+}
DIALOG_BOT_OPTION = None # {"bot_id", "workflow_id", "use_workflow"}
VC_BOT_ID = "7462617881076351002"
--- /dev/null
+# -*- coding: utf-8 -*-
+# k8v3 产品 — 设备侧默认配置(RVC WebSocket V3;编译期 freeze 进固件)
+#
+# 加载优先级(vc_config.py):
+# 1) usr/default_config.py(现场覆盖)
+# 2) 本文件(由 new_clean.bat 按 product_k8v3.ini 的 DEV_TYPE 拷贝到 core/default_config.py)
+#
+# 约定:仅放「机型/产品差异」常量,勿放运行态变量。
+
+# =============================================================================
+# 1. 产品与区域
+# =============================================================================
+
+# 硬件平台类型(与 products/configs/<DEV_TYPE>/ 目录名一致)
+DEV_TYPE = "7PIN"
+
+# 文案/逻辑用语言(如 zh / en)
+LANG = "zh"
+
+# 设备鉴权(device_res_author:HMAC 签名与 AES 加解密)产品名“小量博士” 、“浪漫挂坠”
+PRODUCT_PID = "cqxJZi5CNgzjS3mX21Zw"
+PRODUCT_KEY = "uKLrChwrxA1LklgBc3VwawYjUkbT3BgX" # 32 字节,运行时转为 bytes
+
+# =============================================================================
+# 2. GPIO 引脚
+# =============================================================================
+
+# --- 2.1 三色 LED(Pin 属性名字符串,vc_config 转为 Pin.GPIOxx)---
+LED_GPIO = ("GPIO36", "GPIO44", "GPIO43") # (R, G, B)
+
+# 点亮电平:False=高电平亮;True=低电平亮;仅某路反相用 LED_ACTIVE_LOW_RGB
+LED_ACTIVE_LOW = False
+LED_ACTIVE_LOW_RGB = None # 例 (True, False, False) 仅红灯反相
+
+# --- 2.2 按键 ExtInt ---
+EXTINT_AI_GPIO = "GPIO22" # AI 键
+PWK_KEY_GPIO = "GPIO26" # 电源键(短按/双击切模式等)
+
+# btn_device_ec800m:扩展按键(本机型未用)
+BTN_WECHAT_GPIO = None
+BTN_SOS_GPIO = None
+
+# --- 2.3 马达(motor.py / motorEventManager)---
+MOTOR_PIN_GPIO = "GPIO20" # None 表示无马达
+MOTOR_TICK_MS = 100
+MOTOR_ACTIVE_LOW = False
+MOTOR_BOOT_VIBRATE_MS = 200 # 开机震动时长(ms)
+
+# --- 2.4 蓝牙电源 / 音频 IIS(本机型关闭)---
+BT_POWER_MOTOR_ENABLED = False
+PIN_BT_ON_GPIO = None
+PIN_MOT_GPIO = None
+AUDIO_QDAI_CFG = None # 非 None 时调用 audio.set_qdai_cfg(...)
+
+# --- 2.5 蓝牙↔4G 唤醒(本机型关闭)---
+BT_WAKEUP_4G_ENABLED = False
+EXTINT_WAKEUP_CTA1_GPIO = None
+PIN_WAKEUP_BT_GPIO = None
+
+# =============================================================================
+# 3. LED 灯效策略(AI_led / ai_VC,对齐交互规范)
+# =============================================================================
+
+# 正常待机 idle:off=灭灯(REQ-BAT-01);green_blink=旧工程绿灯慢闪
+LED_IDLE_MODE = "off"
+
+# green_blink 模式下的亮/灭时长(ms);LED_IDLE_MODE=off 时不生效
+LED_IDLE_GREEN_BLINK_MS = 1000
+LED_IDLE_GREEN_BLINK_OFF_MS = 2000
+
+# LLM 思考阶段:flicker_blue_green | open_red_green
+THINKING_LED_MODE = "flicker_blue_green"
+
+# 收到 vad.active 是否强制回蓝灯(长对话)
+VAD_ACTIVE_BLUE_LED = True
+
+# 收音 listening:长对话蓝慢闪(1000ms)、单对话蓝快闪(250ms) 由 VC_mode 在 AI_led 内切换,无需本文件单独配置
+
+# =============================================================================
+# 4. 电池与硬件状态(AI_hw_status / AI_dev)
+# =============================================================================
+
+HW_STATUS_PERIOD_MS = 10000
+# 电池电压:全机约每 10s 采样 1 次 getVbatt,10 点滑动平均(见 vc_hw._BatteryCfg)
+VBATT_AVG_LEN = 10
+VBATT_PUSH_MIN_MS = 9000 # 同周期内 AI_hw / protocol 去重,避免重复读电压
+SIGNAL_WEAK_CSQ = 10 # 0≤CSQ<此值 → signal=weak(REQ-ERR-02)
+
+# SoC 门限:仅低电提示/LED 空电闪(REQ-BAT-02/03);关机见 VBATT_EMPTY_MV
+BATTERY_LOW_SOC_PERCENT = 25 # <25% 低电语音
+BATTERY_EMPTY_SOC_PERCENT = 5 # <5% LED 空电态;不单独触发关机
+BATTERY_LOW_WARN_INTERVAL_MS = 300000 # 低电语音间隔 5min
+BATTERY_EMPTY_FLASH_COUNT = 3
+BATTERY_EMPTY_FLASH_PERIOD_MS = 500 # 空电红快闪约 2 次/秒
+
+# 电压门限(mV):充满 / 低电 / 空电(与 get_battery、simple 模式共用)
+USE_SIMPLE_VBATT_THRESH = False # True 时忽略 BATTERY_OCV_MAP,仅用三档电压
+VBATT_CHARGE_FULL_MV = 4320 # 502631 ZCV 100% OCV≈4.322V;CV 4.35V
+VBATT_LOW_MV = 3630 # 约对应 ZCV 20%(4G 带载下不宜再抬高)
+VBATT_EMPTY_MV = 3400 # 未充电:滑动平均电压 < 此值 → 低电关机(唯一关机条件)
+
+# 502631-510 4.35V ZCV 放电 OCV 表(mV→SoC);0% 锚定 3400 而非实验室 3262
+BATTERY_OCV_MAP = {
+ 4322: 100, 4239: 95, 4177: 90, 4117: 85, 4059: 80, 4006: 75, 3942: 70, 3898: 65,
+ 3853: 60, 3814: 55, 3781: 50, 3755: 45, 3738: 40, 3724: 35, 3706: 30, 3678: 25,
+ 3634: 20, 3577: 15, 3522: 10, 3468: 5, 3400: 0,
+}
+
+# 录音麦克增益:record.gain_set(codec_gain, dsp_gain)
+# codec_gain 0~4(编解码模拟增益 默认4);dsp_gain -36~12(数字增益,原工程常用 10)
+RECORD_CODEC_GAIN = 2
+RECORD_DSP_GAIN = 6
+
+# =============================================================================
+# 5. 可选外设与协议
+# =============================================================================
+
+GNSS_ENABLED = False # 无 GNSS 硬件时 False,避免 import quecgnss
+UART_SUPPORTED = False
+BT_OTA_ENABLED = False
+UPLOAD_START_TONE_ENABLED = False # 开始上传语音时是否播成功提示音
+AI_ALARM_ENABLED = False # K8 吊坠无闹钟/日程硬件
+
+# =============================================================================
+# 6. 按键逻辑(AI_dev / btn_device)
+# =============================================================================
+
+KEY_RELEASE_DEBOUNCE_COUNT = 5
+AI_LONG_PRESS_POWER_OFF_ENABLED = False # AI 键长按 8s 关机
+SOS_ENABLED = False # AI 长按 3s SOS、板载 SOS 入口
+
+# =============================================================================
+# 7. VC 会话 / WebSocket / 空闲与休眠
+# =============================================================================
+
+# 语音 WS 路径段(无首尾斜杠);主机见 srv_configs/beike_dev_v3/default_srv.py AMP_SERVER
+VC_WS_PATH = "rvc-service/api/v3/ws"
+VC_WS_NATIVE_PING_ENABLE = 0 # 0=不发标准 PING(入站 PING 仍回 PONG);1=客户端也发标准 PING
+VC_WS_PING_INTERVAL_SEC = 0 # V3 无自定义文本心跳;仅 V2.5 使用(>0 时发 "ping")
+
+# VC 协议:v3(RVC);环境 SRV_TYPE=beike_dev_v3
+VC_PROTOCOL_VERSION = "v3"
+VC_WS_AUTH_MODE = "dmp_dt" # dmp_dt | api_key
+VC_AUDIO_PLAYED_ACK = 0 # V2.5 播完上报 audio.played;V3 联调建议 0
+VC_AGENT_ID = None # V3 start.agent_id;None 时用 VC_BOT_ID
+VC_AUDIO_ENCRYPTION = 0 # V3 二进制帧加密(首版保持 0)
+# VC_API_KEY = "" # VC_WS_AUTH_MODE=api_key 时使用
+VC_PROTO_TX_LOG = 1 # 1=打印出站摘要 + 原始 JSON(过长分 json_partN 输出)
+
+# V3 start.welcome_options:0=关闭服务端自动欢迎 TTS(由设备 synthesis/notify 播报)
+VC_WELCOME_SERVER_ENABLED = 0
+# VC_WELCOME_SERVER_MESSAGE = "" # VC_WELCOME_SERVER_ENABLED=1 时可选文案
+
+# 协议 failed / websocket_error 是否 synthesis 播报错误原文(联调=1,量产=0)
+VC_ERROR_TTS_ENABLED = 1
+
+# 长对话 L1:轮次间隙无 TTS 下行累计达此秒 Close(听/想/播/录、保活帧不计;最小 5)
+VC_IDLE_AUTO_CLOSE_SEC = 20
+VC_IDLE_AUTO_CLOSE_SEC_PTT = 0 # 单对话预留,当前与 idle 统一处理
+
+# 业务 idle 达此秒数触发深睡断开(最小 10;ENABLE=0 关闭)
+VC_IDLE_DEEP_SLEEP_SEC = 60
+VC_IDLE_DEEP_SLEEP_ENABLE = 1
+
+# 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖
+VAD_SILENCE_DEFAULT = 300
+
+# =============================================================================
+# 8. 语音对话 / TTS / 智能体
+# =============================================================================
+
+POWER_ON_TONE_ENABLED = True
+POWER_ON_TONE_DEFAULT = "你好,我来了"
+
+# 默认音色(无 storage / usr 覆盖时生效)
+# - voice:start → audio_config.output.voice,须与 RVC V3 服务端已配置的 speaker 一致
+# - 联调若报 resource ID mismatched with speaker,改 voice 或让服务端绑定该音色
+VC_TTS_VOICE_KEY_DEFAULT = "男"
+VC_TTS_VOICE_TYPE_DEFAULT = {
+ "男": {
+ "bot_name": "柒先生",
+ "voice": "zh_male_wennuanahu_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "女": {
+ "bot_name": "柒妹妹",
+ "voice": "zh_female_wanwanxiaohe_moon_bigtts",
+ "lang": "zh-cn",
+ },
+ "童": {
+ "bot_name": "柒乐乐",
+ "voice": "ICL_zh_female_huoponvhai_tob",
+ "lang": "zh-cn",
+ },
+}
+
+DIALOG_BOT_OPTION = None # {"bot_id", "workflow_id", "use_workflow"}
+VC_BOT_ID = "7462617881076351002"
+VC_WORKFLOW_ID = "7462621757327196187"
+DIALOG_PROVIDER_DEFAULT = None
+KEYWORDS_FROM_VOICE_TYPE = False
+
+BT_VOICE_FEATURES_ENABLED = False
+
+# =============================================================================
+# 9. 本地指令与固定话术
+# =============================================================================
+
+# protocol.ai_cmd_handle:云端 NLP 扩展(会议纪要 / 实时翻译 / 历史留言)
+NLP_EXTENDED_CMDS = False
+
+CMD_TOGGLE_MODE_TO_PTT_TTS = "已切换到单对话模式"
+CMD_TOGGLE_MODE_TO_LONG_TTS = "已切换到长对话模式"
+POWEROFF_CMD_CLOSE_ONLY = False # 「关机」指令仅 Close,不真断电
+
+KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+
+ASR_EMPTY_HINT_TTS = "我没有听清,请再说一遍" # asr.completed 无有效识别时 Notify 播报
+
+# =============================================================================
+# 10. 开录音 / PTT 时序(对齐「录音上传与播放」设计稿)
+# =============================================================================
+# 无按键确认音:不按 key_prompt;仅按键/PTT 开录时播 start.mp3(自动续聊不播)
+# PTT idle 短按:notify 话术 PTT_HINT;长按开录:start.mp3
+
+RECORD_START_TONE_FILE = "U:/static/start.mp3" # 按键/PTT 开录提示音(非 server.started 自动开麦)
+RECORD_START_TONE_GAP_MS = 180
+
+PTT_MIN_RECORD_MS = 400
+PTT_RECORD_TOO_SHORT_TTS = "录音时间太短,请按住圆形按钮说话" # notify
+
+PTT_RELEASE_END_SPEECH_DELAY_MS = 200 # 松手后发 audio.completed 前延迟
+PTT_CANCEL_WAIT_MS = 2500 # 打断后等 canceled 最长时间
+PTT_POST_MUTE_MS = 30 # 本地硬静音后 codec 收尾
+
+# =============================================================================
+# 11. 调试开关(量产可全 False)
+# =============================================================================
+
+LED_DEBUG = True
+LED_STATE_DEBUG = True
+KEY_LED_DEBUG = True
+HW_STATUS_DEBUG = True
\ No newline at end of file
--- /dev/null
+DEV_TYPE=k8v3
+SRV_TYPE=beike_dev_v3
+VERSION=V1.3
--- /dev/null
+# 设备侧 Server 配置(编译期由 product_*.ini 的 SRV_TYPE 拷贝为 core/default_srv.py 并 freeze)。\r
+#\r
+# =============================================================================\r
+# 加载与修改方式(改环境时按此检查,避免漏改)\r
+# =============================================================================\r
+#\r
+# 编译:\r
+# product_k8v3.ini 中 SRV_TYPE=beike_dev_v3\r
+# → new_clean.bat 拷贝本文件 → core/default_srv.py → 打进固件\r
+#\r
+# 运行时优先级(srv_config.py):\r
+# 1) usr/default_srv.py(现场覆盖,无需重编)\r
+# 2) freeze 内置 core/default_srv.py\r
+#\r
+# 启动时注入 constants(constants.py 仅读取下列 3 项到模块常量):\r
+# DMP_BASE, AMP_SERVER, IOT_WS_URL → constants.DMP_BASE / AMP_SERVER / IOT_WS_URL\r
+#\r
+# 与 default_config.py 的分工(勿混在同一文件改):\r
+# - 本文件:环境 / 域名 / 主机(随 SRV_TYPE 变,如 beike_dev ↔ beike)\r
+# - default_config:产品能力(随 DEV_TYPE/k8 变),含 VC_WS_PATH、VC_BOT_ID 等\r
+#\r
+# =============================================================================\r
+# URL 拼接总览(改 VC 联调时两项都要对)\r
+# =============================================================================\r
+#\r
+# 【语音对话 WebSocket — AI_VC.websocket_connect_VC】\r
+# 主机:constants.AMP_SERVER(本文件 AMP_SERVER)\r
+# → ai_VC 赋给 self.server_ip(可被存储 server_ip / 云端下发覆盖)\r
+# 路径:default_config.VC_WS_PATH(k8v3 为 "rvc-service/api/v3/ws")\r
+# 查询:DMP dt(get_dev_token)或 api_key(见 VC_WS_AUTH_MODE)\r
+#\r
+# 示例(beike_dev_v3 + k8v3,RVC V3 API 文档):\r
+# ws://115.190.94.208:8087/rvc-service/api/v3/ws?is_auth=...&dt=...\r
+#\r
+# 【IoT 设备云 WebSocket — protocol.py】\r
+# 完整 URL 已在本文件写死为 IOT_WS_URL,仅追加 token:\r
+# {IOT_WS_URL}?bz-dt={token}\r
+# 示例:ws://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1?bz-dt=...\r
+#\r
+# 【DMP HTTP — ai_VC 内认证/校时/ICCID/OTA 结果等】\r
+# 前缀:constants.DMP_BASE(本文件 DMP_BASE,无尾部斜杠)\r
+# 路径:ai_VC 内目前为字面量 "/dev/..."(与下方 DMP_*_PATH 取值应对齐)\r
+# 示例:{DMP_BASE}/dev/time 、{DMP_BASE}/dev/auth/active\r
+#\r
+# 【OTA 检查任务 — vc_ota.min_sys_ota / bt_ota】\r
+# constants.OTA_TASK_URL_FMT.format(imei, sourceVersion)\r
+# OTA 结果上报:{DMP_BASE} + OTA_RESULT_PATH_FMT(vc_ota.ota_result)\r
+#\r
+# 【AMP 健康检查 — ai_VC 错误上报】\r
+# 使用 self.server_ip(同 AMP_SERVER)+ 硬编码路径:\r
+# http://{server_ip}/health?task_id={...}\r
+# 与 AMP_HEALTH_URL_FMT 形式一致(第一参数为 host:port,无 http 前缀)\r
+#\r
+# 注意:下方 DMP_*_PATH / OTA_*_FMT / AMP_HEALTH_URL_FMT 为**集中文档与目标值**;\r
+# constants 启动时尚未自动读取这些路径常量;修改后若 ai_VC 仍写死字符串,需同步改代码或接 srv_config.get_attr。\r
+#\r
+# =============================================================================\r
+\r
+# ---- 环境标识(仅注释/运维用;constants.SERVER_ENV 会读取)----\r
+SERVER_ENV = "beike_dev_v3"\r
+\r
+# ---- 三套「根地址」(constants.py 启动时加载,改环境必改)----\r
+\r
+# DMP 业务 HTTP 根路径(无尾斜杠)。用于 DMP_BASE + "/dev/..." 、"/ota/result?..."\r
+DMP_BASE = "http://dmp-dev.raisound.com/dmp-api"\r
+\r
+# RVC V3 语音对话主机 host:port(无 ws://)。完整 WS:\r
+# ws://{AMP_SERVER}/{VC_WS_PATH} 例见 configs/k8v3/default_config.py\r
+AMP_SERVER = "115.190.94.208:8087"\r
+\r
+# IoT 长连接完整 WebSocket URL(含 ws://)。protocol 仅追加 ?bz-dt=token\r
+IOT_WS_URL = "ws://dmp-dev.raisound.com/iot-api/ws-hub/dev/v1"\r
+\r
+# ---- DMP 子路径(相对 DMP_BASE;与 ai_VC 内硬编码路径保持一致)----\r
+\r
+DMP_TIME_PATH = "/dev/time" # 校时 GET → dev_NTP\r
+DMP_ACTIVE_PATH = "/dev/auth/active" # 激活 POST\r
+DMP_SIGN_IN_PATH = "/dev/auth/sign_in" # 登录\r
+DMP_TOKEN_PATH = "/dev/auth/token" # 取 token → get_dev_token 链路\r
+DMP_ICCID_PATH = "/dev/iccid" # ICCID 上报(带 query)\r
+DMP_DEV_CONFIG_PATH_FMT = "/dev/config/{}" # 设备配置,format(deviceId)\r
+\r
+# ---- OTA ----\r
+\r
+# 完整 URL 模板;占位符 {} 依次为 deviceId(IMEI)、sourceVersion\r
+OTA_TASK_URL_FMT = "http://dmp-dev.raisound.com/dmp-api/ota/task?deviceId={}&sourceVersion={}"\r
+\r
+# 相对 DMP_BASE 的路径模板(上报 OTA 结果)\r
+OTA_RESULT_PATH_FMT = "/ota/result?deviceId={}¤tVersion={}"\r
+\r
+# ---- AMP 健康检查(host 用 AMP_SERVER / server_ip,非 DMP_BASE)----\r
+\r
+AMP_HEALTH_URL_FMT = "http://{}/health?task_id={}" # .format(server_ip, task_id)\r
# 若误关会导致问候结束后首轮用户说话无 vad/asr。
VC_PROTO_GUARDS = 0
# 1=打印 VC WebSocket 每条收发的完整文本(流量大、易刷屏);0 仅一行 event 摘要(llm.delta 省略)
-VC_WS_IO_LOG = 0
-# 长对话 20s 无 TTS idle:仅 tts.* / started / TTS 音频字节重置 close_timerout;asr/vad/llm 不重置。
-# 帧级 WebSocket PING/PONG 在 uwebsocket.recv() 内已处理,不会进入 AI_client_VC_recv。
-# 金三 V2.5 文档示例:WebSocket 文本帧 "ping" 保活;对端可能回 pong / heartbeat 类事件。
+VC_WS_IO_LOG = 1
+# 1=打印出站 JSON 摘要(start 音色/TTS 参数、synthesis 文本等);联调 TTS 建议开启
+VC_PROTO_TX_LOG = 1
+# 长对话 L1(VC_IDLE_AUTO_CLOSE_SEC):仅 tts.* / started / TTS 音频下行重置 close_timerout。
+# 保活不计入 L1:V2.5 文本 ping/pong、JSON heartbeat;RFC6455 OP_PING/OP_PONG(uwebsocket 内处理)。
+# V2.5:默认不发标准 PING,发文本 "ping"(VC_WS_PING_INTERVAL_SEC>0);须响应入站标准 PING→PONG。
+# V3:无自定义心跳;默认不发标准 PING;须响应入站标准 PING→PONG。
+# VC_WS_NATIVE_PING_ENABLE=1 时客户端才主动发标准 PING(两协议均默认 0)。
_VC_KEEPALIVE_RX_EVENTS = frozenset(("pong", "ping", "heartbeat", "keepalive"))
def _vc_out_json_event_meta(s):
- """解析 WS 出站 JSON:event 可能在 header 或顶层(Start/audio.create vs synthesis)。"""
+ """解析 WS 出站 JSON:event 可能在 header、顶层 event 或 V3 type。"""
try:
jd = ujson.loads(s)
h = jd.get("header")
ev = h.get("event") or ""
if ev:
return ev, h.get("trace_id") or ""
- ev = jd.get("event") or ""
- return ev, ""
+ ev = jd.get("event") or jd.get("type") or ""
+ tid = jd.get("trace_id") or ""
+ if not tid and isinstance(h, dict):
+ tid = h.get("trace_id") or ""
+ return ev, tid
except Exception:
return "", ""
--- /dev/null
+# -*- coding: utf-8 -*-
+"""VC WebSocket 协议适配:V2.5(金三)与 V3(RVC)可配置切换。"""
+import ujson
+
+PROTO_V25 = "v2.5"
+PROTO_V3 = "v3"
+
+# V3 下行 type → 内部仍使用的 V2.5 事件名(FSM/recv 无需改)
+_SERVER_TYPE_TO_EVENT = {
+ "error": "failed",
+ "tts.done": "tts.completed",
+ "llm.started": "llm.created",
+}
+
+
+def _cfg(name, default=None):
+ try:
+ import vc_config
+ return vc_config.get_attr(name, default)
+ except Exception:
+ return default
+
+
+def get_protocol_version():
+ v = _cfg("VC_PROTOCOL_VERSION", PROTO_V25)
+ if v is None:
+ return PROTO_V25
+ try:
+ v = str(v).lower().strip()
+ except Exception:
+ return PROTO_V25
+ if v in ("v3", "3", "rvc", "rvc_v3"):
+ return PROTO_V3
+ return PROTO_V25
+
+
+def is_v3():
+ return get_protocol_version() == PROTO_V3
+
+
+def vc_ws_custom_ping_enabled():
+ """V2.5:允许设备发 WebSocket 文本帧 \"ping\";V3:无自定义心跳。"""
+ if is_v3():
+ return False
+ try:
+ return int(_cfg("VC_WS_PING_INTERVAL_SEC", 55)) > 0
+ except Exception:
+ return True
+
+
+def vc_proto_audio_played_enabled():
+ default = 0 if is_v3() else 1
+ try:
+ return bool(_cfg("VC_AUDIO_PLAYED_ACK", default))
+ except Exception:
+ return bool(default)
+
+
+def vc_proto_build_ws_url(amp_server, path, is_auth, token=None, device_id=None):
+ """拼 VC WebSocket URL(无 ws:// 前缀)。"""
+ path = path or "vc/chat"
+ auth_mode = _cfg("VC_WS_AUTH_MODE", "dmp_dt")
+ try:
+ auth_mode = str(auth_mode or "dmp_dt").lower().strip()
+ except Exception:
+ auth_mode = "dmp_dt"
+
+ if is_v3():
+ if auth_mode == "api_key":
+ key = token or _cfg("VC_API_KEY", "") or ""
+ did = device_id or ""
+ if key and did:
+ return "ws://{}/{}?api_key={}&device_id={}".format(
+ amp_server, path, key, did)
+ if key:
+ return "ws://{}/{}?api_key={}".format(amp_server, path, key)
+ return "ws://{}/{}".format(amp_server, path)
+ # DMP:V3 文档 dt/ut;保留 is_auth 便于联调旧网关
+ if token:
+ return "ws://{}/{}?is_auth={}&dt={}".format(
+ amp_server, path, is_auth, token)
+ return "ws://{}/{}?is_auth={}".format(amp_server, path, is_auth)
+
+ # V2.5(现状)
+ req = "{}?is_auth={}".format(path, is_auth)
+ if token:
+ return "ws://{}/{}&dt={}".format(amp_server, req, token)
+ return "ws://{}/{}".format(amp_server, req)
+
+
+def _extract_client_event_payload(obj):
+ """从出站 JSON 对象解析 event 与 payload(兼容 header 与顶层 event)。"""
+ if not isinstance(obj, dict):
+ return None, {}
+ h = obj.get("header")
+ if isinstance(h, dict):
+ ev = h.get("event") or ""
+ if ev:
+ return ev, obj.get("payload") or {}
+ ev = obj.get("event") or obj.get("type") or ""
+ if ev:
+ pl = obj.get("payload")
+ if pl is None:
+ pl = obj.get("data")
+ if pl is None:
+ pl = {}
+ return ev, pl
+ return None, {}
+
+
+# V3 StartData.audio_config 文档字段(不含 V2.5 专有项)
+_V3_AUDIO_CFG_INPUT_KEYS = ("format", "sample_rate", "bit_rate", "channel")
+_V3_AUDIO_CFG_OUTPUT_KEYS = ("format", "sample_rate", "bit_rate", "channel", "speech_rate")
+# V2.5 outbound 专有:V3 不识别,不写入 start.data(仅 voice 放 metadata 供服务端选用)
+_V3_DROP_TTS_EXTRA_KEYS = frozenset(("emotion", "volume", "chunk_interval_ms", "codec"))
+
+
+def _pick_v3_audio_cfg(src, is_output=False):
+ """仅保留 V3 支持的 audio_config 字段;其余进 metadata 扩展。"""
+ allowed = _V3_AUDIO_CFG_OUTPUT_KEYS if is_output else _V3_AUDIO_CFG_INPUT_KEYS
+ cfg = {}
+ extras = {}
+ if not isinstance(src, dict):
+ return cfg, extras
+ for k, v in src.items():
+ if k in allowed:
+ cfg[k] = v
+ else:
+ extras[k] = v
+ return cfg, extras
+
+
+def _build_v3_welcome_options(dlg_cfg):
+ """V3 start.welcome_options:与 V2.5 dialog_attributes.cfg.greeting 及配置对齐。
+
+ 设备侧 notify/local/boot_prompt 等走 synthesis,默认关闭服务端自动欢迎 TTS。
+ """
+ greeting = {}
+ if isinstance(dlg_cfg, dict):
+ g = dlg_cfg.get("greeting")
+ if isinstance(g, dict):
+ greeting = g
+ enabled = False
+ message = ""
+ try:
+ if greeting.get("enabled"):
+ enabled = True
+ message = greeting.get("content") or ""
+ except Exception:
+ pass
+ cfg_val = _cfg("VC_WELCOME_SERVER_ENABLED", None)
+ if cfg_val is not None:
+ try:
+ enabled = bool(int(cfg_val))
+ except Exception:
+ enabled = bool(cfg_val)
+ if enabled:
+ message = _cfg("VC_WELCOME_SERVER_MESSAGE", message) or message
+ en = bool(enabled)
+ msg = message if en else ""
+ # 同时带 enabled/message(与 audio_encryption.enabled 命名一致)及 started 回包字段名
+ return {
+ "enabled": en,
+ "message": msg,
+ "welcome_enabled": en,
+ "welcome_message": msg,
+ }
+
+
+def _convert_start_payload_v25_to_v3(payload):
+ """V2.5 start payload → V3 start.data。"""
+ if not isinstance(payload, dict):
+ payload = {}
+ inb = payload.get("inbound_attributes") or {}
+ outb = payload.get("outbound_attributes") or {}
+ dlg = payload.get("dialog_attributes") or {}
+ dlg_cfg = dlg.get("cfg") if isinstance(dlg.get("cfg"), dict) else {}
+ bot = dlg_cfg.get("bot_option") if isinstance(dlg_cfg.get("bot_option"), dict) else {}
+
+ disable_vad = inb.get("disable_vad", True)
+ session_mode = "push_to_talk" if disable_vad else "realtime"
+
+ in_cfg, in_extra = _pick_v3_audio_cfg(inb.get("cfg") or {}, False)
+ out_cfg, out_extra = _pick_v3_audio_cfg(outb.get("cfg") or {}, True)
+
+ data = {
+ "device_id": payload.get("device_id") or "",
+ "session_mode": session_mode,
+ "input_mode": "audio",
+ "output_mode": "audio",
+ "audio_config": {
+ "input": in_cfg,
+ "output": out_cfg,
+ },
+ }
+
+ agent_id = _cfg("VC_AGENT_ID", None)
+ if agent_id is None or agent_id == "":
+ agent_id = bot.get("bot_id") or _cfg("VC_BOT_ID", "")
+ if agent_id:
+ data["agent_id"] = agent_id
+
+ io = dict(inb.get("interrupt_options") or {})
+ if io.get("thinking_time_only"):
+ io["strategy"] = "thinking_time_only"
+ elif io.get("keyword_interrupt"):
+ io["strategy"] = "keyword"
+ if io.get("interrupt_keywords"):
+ io["interrupt_keywords"] = io["interrupt_keywords"]
+ if io:
+ data["interrupt_options"] = io
+
+ data["welcome_options"] = _build_v3_welcome_options(dlg_cfg)
+
+ meta = {}
+ if bot:
+ meta["bot_option"] = bot
+ cv = dlg_cfg.get("custom_variables")
+ if isinstance(cv, dict) and cv:
+ meta["custom_variables"] = cv
+ vs = inb.get("vad_silence")
+ if vs is not None:
+ meta["vad_silence"] = vs
+ prov = dlg.get("provider")
+ if prov:
+ meta["dialog_provider"] = prov
+ # V2.5 专有字段勿进 audio_config;V3 仅保留文档认可的 metadata
+ if in_extra:
+ meta["asr_cfg_extra"] = in_extra
+ if out_extra:
+ voice = out_extra.get("voice")
+ if voice:
+ meta["voice"] = voice
+ # emotion/volume/chunk_interval_ms 等 V3 无对应项,不随 start 上报
+ tts_other = {}
+ for k, v in out_extra.items():
+ if k in _V3_DROP_TTS_EXTRA_KEYS or k == "voice":
+ continue
+ tts_other[k] = v
+ if tts_other:
+ meta["tts_cfg_extra"] = tts_other
+ if outb.get("provider"):
+ meta["tts_provider"] = outb.get("provider")
+ if inb.get("provider"):
+ meta["asr_provider"] = inb.get("provider")
+ if meta:
+ data["metadata"] = meta
+
+ enc = _cfg("VC_AUDIO_ENCRYPTION", 0)
+ try:
+ if int(enc):
+ data["audio_encryption"] = {"enabled": True}
+ except Exception:
+ pass
+
+ return data
+
+
+def vc_proto_pack_obj(obj):
+ """V3:将 V2.5 形态 dict 转为 Envelope dict;V2.5:原样返回。"""
+ if not is_v3():
+ return obj
+ if not isinstance(obj, dict):
+ return obj
+
+ ev, payload = _extract_client_event_payload(obj)
+ if not ev:
+ return obj
+
+ type_name = ev
+ if type_name == "Start":
+ type_name = "start"
+
+ if type_name == "start":
+ data = _convert_start_payload_v25_to_v3(payload)
+ elif type_name in ("cancel", "audio.create", "audio.completed"):
+ data = payload if isinstance(payload, dict) else {}
+ else:
+ data = payload if isinstance(payload, dict) else {}
+
+ return {"type": type_name, "data": data}
+
+
+def vc_proto_pack_wire(s):
+ """出站 JSON 字符串:V3 时改写为 type/data。"""
+ if not is_v3():
+ return s
+ if not isinstance(s, str) or not s or s == "ping":
+ return s
+ try:
+ obj = ujson.loads(s)
+ except Exception:
+ return s
+ try:
+ packed = vc_proto_pack_obj(obj)
+ return ujson.dumps(packed)
+ except Exception:
+ return s
+
+
+def _normalize_payload(event, data):
+ if not isinstance(data, dict):
+ data = {}
+ p = data
+ if event == "asr.result":
+ if "text" in p and "content" not in p:
+ p["content"] = p["text"]
+ elif event == "failed":
+ if "message" not in p:
+ if isinstance(p.get("error"), str):
+ p["message"] = p.get("error")
+ elif isinstance(p.get("msg"), str):
+ p["message"] = p.get("msg")
+ if "code" in p and "error" not in p:
+ p["error"] = p["code"]
+ elif event == "llm.delta":
+ if "delta" in p and "content" not in p:
+ p["content"] = p["delta"]
+ return p
+
+
+def _proto_tx_log_enabled():
+ try:
+ import vc_config
+ if vc_config:
+ v = vc_config.get_attr("VC_PROTO_TX_LOG", None)
+ if v is not None:
+ return bool(v)
+ except Exception:
+ pass
+ try:
+ from vc_common import VC_PROTO_TX_LOG
+ return bool(VC_PROTO_TX_LOG)
+ except Exception:
+ return True
+
+
+def _text_preview(s, n=48):
+ try:
+ t = (s or "").strip().replace("\n", " ")
+ except Exception:
+ return ""
+ if len(t) <= n:
+ return t
+ return t[:n] + "..."
+
+
+def _log_wire_json(tag, wire_str):
+ """打印 WebSocket 出站原始 JSON(过长时分段,避免串口截断)。"""
+ if not _proto_tx_log_enabled():
+ return
+ try:
+ s = wire_str if isinstance(wire_str, str) else ""
+ n = len(s)
+ chunk = 480
+ print("[VC][proto] {} json_len={}".format(tag, n))
+ if n <= chunk:
+ print("[VC][proto] {} json={}".format(tag, s))
+ return
+ pos = 0
+ part = 0
+ while pos < n:
+ print("[VC][proto] {} json_part{}={}".format(tag, part, s[pos:pos + chunk]))
+ pos += chunk
+ part += 1
+ except Exception:
+ pass
+
+
+def vc_proto_log_started_server(payload):
+ """入站 started:打印服务端 welcome 配置(联调 welcome_options 是否生效)。"""
+ if not is_v3() or not _proto_tx_log_enabled():
+ return
+ try:
+ p = payload if isinstance(payload, dict) else {}
+ we = p.get("welcome_enabled")
+ wm = p.get("welcome_message") or ""
+ agent = p.get("agent") or {}
+ if isinstance(agent, dict) and we is None:
+ we = agent.get("welcome_enabled")
+ if not wm:
+ wm = agent.get("welcome_message") or ""
+ print("[VC][proto] rx_started ver=v3 welcome_enabled={} welcome_msg={!r}".format(
+ we, _text_preview(str(wm), 48)))
+ except Exception:
+ pass
+
+
+def vc_proto_log_start_from_v25(json_data, vc_mode=-1):
+ """start/Start 发出前:打印 TTS/会话关键参数(不依赖大包 JSON 解析)。"""
+ if not _proto_tx_log_enabled():
+ return
+ try:
+ p = json_data.get("payload") or {}
+ inb = p.get("inbound_attributes") or {}
+ outb = p.get("outbound_attributes") or {}
+ ocfg = outb.get("cfg") or {}
+ icfg = inb.get("cfg") or {}
+ dlg = p.get("dialog_attributes") or {}
+ bot = (dlg.get("cfg") or {}).get("bot_option") or {}
+ disable_vad = inb.get("disable_vad", True)
+ sess = "push_to_talk" if disable_vad else "realtime"
+ tid = ""
+ try:
+ tid = (json_data.get("header") or {}).get("trace_id") or ""
+ except Exception:
+ pass
+ print("[VC][proto] start_tx ver={} trace={} vc_mode={} sess={} vad_silence={} "
+ "voice={} out_prov={} in_prov={} fmt={} sr={} speech_rate={} bot={} wf={}".format(
+ get_protocol_version(),
+ tid or "-",
+ vc_mode,
+ sess,
+ inb.get("vad_silence", "-"),
+ ocfg.get("voice", "-"),
+ outb.get("provider", "-"),
+ inb.get("provider", "-"),
+ ocfg.get("format", "-"),
+ ocfg.get("sample_rate", "-"),
+ ocfg.get("speech_rate", "-"),
+ bot.get("bot_id", "-"),
+ bot.get("workflow_id", "-"),
+ ))
+ try:
+ # 内部 V2.5 草稿,不经 pack_wire,不会上 WebSocket
+ _log_wire_json("start_draft_v25", ujson.dumps(json_data))
+ except Exception:
+ pass
+ if is_v3():
+ try:
+ d = _convert_start_payload_v25_to_v3(p)
+ ac_out = (d.get("audio_config") or {}).get("output") or {}
+ meta = d.get("metadata") or {}
+ wo = d.get("welcome_options") or {}
+ print("[VC][proto] start_preview_v3 agent={} session_mode={} voice_meta={} "
+ "welcome_enabled={} audio_out_keys={}".format(
+ d.get("agent_id", "-"),
+ d.get("session_mode", "-"),
+ meta.get("voice", "-"),
+ wo.get("welcome_enabled", "-"),
+ list(ac_out.keys()),
+ ))
+ # V3 pack 预览,未上链;链路上仅 start_wire_SENT 一条
+ _log_wire_json("start_preview_v3", ujson.dumps({"type": "start", "data": d}))
+ print("[VC][proto] start_log_note 链路上仅发 start_wire_SENT(type:start)")
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+
+def vc_proto_log_synthesis(text, pending_kind=""):
+ """synthesis 发出前:打印待合成文本摘要。"""
+ if not _proto_tx_log_enabled():
+ return
+ try:
+ t = text or ""
+ print("[VC][proto] synthesis_tx ver={} text_len={} kind={} text={!r}".format(
+ get_protocol_version(),
+ len(t),
+ pending_kind or "-",
+ _text_preview(t, 64),
+ ))
+ try:
+ if is_v3():
+ wire = ujson.dumps({"type": "synthesis", "data": {"text": t}})
+ else:
+ wire = ujson.dumps({"event": "synthesis", "payload": {"text": t}})
+ _log_wire_json("tx_synthesis", wire)
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+
+def vc_proto_log_tx_wire(wire_str, ws_conn=0):
+ """发送线程:对已 pack 的 JSON 打一行摘要(synthesis 等小报文)。"""
+ if not _proto_tx_log_enabled():
+ return
+ if not isinstance(wire_str, str) or not wire_str or wire_str == "ping":
+ return
+ ev = ""
+ try:
+ jd = ujson.loads(wire_str)
+ ev, pl = _extract_client_event_payload(jd)
+ except Exception:
+ print("[VC][proto] tx parse_fail len={} ws={}".format(len(wire_str), ws_conn))
+ _log_wire_json("tx_wire_raw", wire_str)
+ return
+ if not ev:
+ print("[VC][proto] tx unknown_json len={} ws={}".format(len(wire_str), ws_conn))
+ _log_wire_json("tx_wire_raw", wire_str)
+ return
+ el = str(ev).lower()
+ tag = "tx_" + el.replace(".", "_")
+ _log_wire_json(tag, wire_str)
+ if el == "synthesis":
+ txt = ""
+ try:
+ txt = pl.get("text") or ""
+ except Exception:
+ pass
+ print("[VC][proto] tx synthesis ver={} len={} ws={} text={!r}".format(
+ get_protocol_version(), len(wire_str), ws_conn, _text_preview(txt, 64)))
+ return
+ if el in ("start",):
+ print("[VC][proto] start_wire_SENT ver={} wire_len={} ws={}".format(
+ get_protocol_version(), len(wire_str), ws_conn))
+ return
+ if el in ("audio.create", "audio.completed", "cancel", "audio.played", "audio.convert"):
+ print("[VC][proto] tx event={} ver={} len={} ws={}".format(
+ ev, get_protocol_version(), len(wire_str), ws_conn))
+ return
+ print("[VC][proto] tx event={} ver={} len={} ws={}".format(
+ ev, get_protocol_version(), len(wire_str), ws_conn))
+
+
+def vc_proto_normalize_server(js):
+ """入站:V3 Envelope 归一为顶层 event + payload(就地修改并返回)。"""
+ if not isinstance(js, dict):
+ return js
+ if not is_v3():
+ return js
+
+ if js.get("type"):
+ ev = js.get("type") or ""
+ ev = _SERVER_TYPE_TO_EVENT.get(ev, ev)
+ data = js.get("data")
+ if data is None:
+ data = {}
+ js["event"] = ev
+ js["payload"] = _normalize_payload(ev, data)
+ sid = js.get("session_id") or js.get("sid")
+ if sid and not js.get("sid"):
+ js["sid"] = sid
+ return js
pass
return 8000
+ def _vc_ignore_late_tts_completed_event(self):
+ """打断后已停播/开麦:迟到的 tts.completed 不参与 await_played / play_end。"""
+ try:
+ return (
+ self._vc_is_long_dialogue_mode()
+ and not self._vc_tts_playback_active()
+ and (
+ getattr(self, "VC_record_state_flag", 0)
+ or getattr(self, "_vc_pending_interrupt_uplink", 0)
+ or getattr(self, "is_ruppt_tts", 0)
+ or getattr(self, "_drop_tts_audio", 0)
+ )
+ )
+ except Exception:
+ return False
+
def _long_dialogue_should_drop_inbound_tts(self):
"""长对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""
if not self._vc_is_long_dialogue_mode():
except Exception:
pass
def _vc_rx_resets_long_idle_timer(self, recv_data):
- """æ\98¯å\90¦æ¸\85é\9b¶ close_timeroutï¼\9aä»\85 TTS å\8f\8dé¦\88ï¼\88tts.* / started / é\9f³é¢\91å\97è\8a\82ï¼\89ï¼\8casr/vad/llm 不计。"""
+ """æ\98¯å\90¦æ¸\85é\9b¶ close_timeroutï¼\9aä»\85 TTS å\8f\8dé¦\88ï¼\9bä¿\9dæ´»ï¼\88æ\96\87æ\9c¬ ping/pongã\80\81heartbeatï¼\89不计。"""
try:
if recv_data is None:
return False
except Exception:
return False
def _vc_long_idle_timer_armed(self):
- """长对话 L1:会话已 started 且已连接时才开始计 20s 无 TTS。"""
+ """长对话 L1:轮次间隙无 TTS 下行才累计;听/想/播/录与 JSON 保活均不计。"""
try:
if getattr(self, "VC_mode", -1) != 0:
return False
return False
if getattr(self, "_vc_session_started", 0) != 1:
return False
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ return False
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (
+ VCTurnState.Listening,
+ VCTurnState.Thinking,
+ VCTurnState.TtsReceiving,
+ VCTurnState.Playing,
+ VCTurnState.AwaitPlayedAck,
+ ):
+ return False
return True
except Exception:
return False
self._vc_boot_standby = 0
self._last_tts_kind = ""
self._pending_tts_kind = ""
+ self._vc_expect_dialog_tts = 0
# Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
# 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
self._ptt_audio_created = 0
self._uplink_active = 0
self._uplink_bytes = 0
self._uplink_start_ms = 0
+ self._uplink_tx_fail_cnt = 0
+ self._uplink_tx_fail_bytes = 0
+ self._uplink_tx_fail_log_ms = 0
# 状态机骨架(后续逐步把旧逻辑迁移进来)
self.fsm = VCStateMachine(self)
self.Audio_buf_size = 20*1024
self.record = audio.Record()
#self.record.end_callback(self.record_callback)
- self.record.gain_set(4,10)
- #self.record.gain_set(4,12)
+ try:
+ _cg, _dg = 4, 10
+ if "vc_config" in globals() and vc_config:
+ _cg = int(vc_config.get_int("RECORD_CODEC_GAIN", 4))
+ _dg = int(vc_config.get_int("RECORD_DSP_GAIN", 10))
+ if _cg < 0:
+ _cg = 0
+ elif _cg > 4:
+ _cg = 4
+ if _dg < -36:
+ _dg = -36
+ elif _dg > 12:
+ _dg = 12
+ self.record.gain_set(_cg, _dg)
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ self.record.gain_set(4, 10)
+ except Exception:
+ pass
self.aud = audio.Audio(0)
self.aud.setCallback(self.audio_cb)
# 确保 record/aud 已创建后再打印硬件/音频诊断信息(需要 gain_get/type(record))
pass
if _bg_active:
EventMesh.publish("system_sleep_rest")
- # --- Level 1:仅长对话,idle_sec 内无 TTS 下行 → Close 进 idle(需再按 AI 进入)
- # close_timerout:仅 tts.* / started / TTS 音频重置;见 _vc_rx_resets_long_idle_timer
+ # --- Level 1:长对话轮次间隙 idle_sec 内无 TTS 下行 → Close(JSON ping/pong 不重置、不暂停累计)
+ # close_timerout:仅 tts.* / started / TTS 音频重置;听/想/播/录期 _vc_long_idle_timer_armed=False
idle_sec = 20
try:
if "vc_config" in globals() and vc_config:
if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:
pass
- # VC WebSocket 文本 ping
- try:
- _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))
- except Exception:
- _ping_iv = 4
- if _ping_iv > 0 and getattr(self, "AI_client_VC_isconnect", 0) == 1:
- self.VC_pingping_timerout += 1
- if self.VC_pingping_timerout >= _ping_iv:
- self.VC_pingping_timerout = 0
- self.AI_client_VC_ping()
+ # V2.5 自定义文本 ping;V3 无自定义心跳(入站标准 PING 由 uwebsocket 回 PONG)
+ if vc_ws_custom_ping_enabled() and getattr(self, "AI_client_VC_isconnect", 0) == 1:
+ try:
+ _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 55))
+ except Exception:
+ _ping_iv = 55
+ if _ping_iv > 0:
+ self.VC_pingping_timerout += 1
+ if self.VC_pingping_timerout >= _ping_iv:
+ self.VC_pingping_timerout = 0
+ self.AI_client_VC_ping()
# self.asr_pingping_timerout += 1
# if self.asr_pingping_timerout>3:
# self.asr_pingping_timerout=0
_vc_out_json_event_meta,\r
PRESS_AUDIO_MODE,\r
)\r
+from vc_proto import (\r
+ vc_proto_build_ws_url,\r
+ vc_proto_pack_wire,\r
+ vc_proto_normalize_server,\r
+ vc_proto_audio_played_enabled,\r
+ vc_proto_log_start_from_v25,\r
+ vc_proto_log_started_server,\r
+ vc_proto_log_synthesis,\r
+ vc_proto_log_tx_wire,\r
+ is_v3,\r
+ vc_ws_custom_ping_enabled,\r
+)\r
from vc_fsm import (\r
VCSessionState,\r
VCTurnState,\r
pass
return False
+ def _vc_error_tts_enabled(self):
+ """VC_ERROR_TTS_ENABLED=1:协议 failed 原文 synthesis 播报(联调用)。"""
+ try:
+ if "vc_config" in globals() and vc_config:
+ return vc_config.get_bool("VC_ERROR_TTS_ENABLED", False)
+ except Exception:
+ pass
+ return False
+
+ def _vc_error_tts_speakable(self, msg):
+ """协议 failed 是否走 synthesis;默认过滤技术错误,VC_ERROR_TTS_ENABLED=1 时全播。"""
+ try:
+ if msg is None:
+ return None
+ s = str(msg).strip()
+ if not s:
+ return None
+ if self._vc_error_tts_enabled():
+ return s
+ low = s.lower()
+ for pat in (
+ "failed to parse",
+ "parse message",
+ "json decode",
+ "invalid json",
+ "internal server",
+ "concurrency_limit",
+ ):
+ if pat in low:
+ return None
+ except Exception:
+ return None
+ return s
+
+ def _vc_classify_llm_tts_kind(self, payload_dialog_id=""):
+ """设计稿 llm tts:V3 下行常无 dialog_id,用轮次状态补判为 dialog。"""
+ try:
+ if payload_dialog_id:
+ return "dialog"
+ k = getattr(self, "_pending_tts_kind", "") or ""
+ if k:
+ return k
+ cur = getattr(self, "_last_tts_kind", "") or ""
+ if cur in ("mode_switch_long", "mode_switch_ptt", "boot_prompt"):
+ return cur
+ if getattr(self, "_vc_expect_dialog_tts", 0) == 1:
+ return "dialog"
+ if (
+ getattr(self, "VC_mode", -1) == 0
+ and getattr(self, "_turn_has_asr_completed", 0) == 1
+ ):
+ return "dialog"
+ if not cur:
+ return "prompt"
+ return cur
+ except Exception:
+ return "prompt"
+
def _vc_orchestrator_server_event_cb(self, data):
"""btn 编排线程:服务端协议事件唯一入口(长对话/PTT 统一)。"""
try:
self._vc_stop_downlink_for_interrupt("canceled")
except Exception:
pass
+ if ev == "tts.completed" and self._vc_ignore_late_tts_completed_event():
+ print("[ORCH] ignore late tts.completed (interrupt/recording)")
+ return
try:
actions = self.fsm.on_server_event(ev, jsload)
if ev == "canceled" and self._vc_is_long_dialogue_mode():
token = EventMesh.publish("get_dev_token")
print("token:{}".format(token))
- req_str = "vc/chat?is_auth={}".format(self.is_auth)
+ base = "vc/chat"
try:
if "vc_config" in globals() and vc_config:
base = vc_config.get_attr("VC_WS_PATH", "vc/chat") or "vc/chat"
- req_str = "{}?is_auth={}".format(base, self.is_auth)
except Exception:
pass
- if(token):
- url = 'ws://{}/{}&dt={}'.format(self.server_ip,req_str,token)
- else:
- url = 'ws://{}/{}'.format(self.server_ip,req_str)
+ try:
+ did = modem.getDevImei()
+ except Exception:
+ did = ""
+ url = vc_proto_build_ws_url(
+ self.server_ip, base, self.is_auth, token=token, device_id=did)
#url = 'ws://115.190.94.208:22080/vc/coze?dt={}'.format(token)
if VC_WS_IO_LOG:
utime.sleep_ms(3000)
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
else:
- print("websocket_error:tts:{}".format(TTS_str))
- self.AI_TTS_str = TTS_str
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- self.Close_flag = 1
+ speak = self._vc_error_tts_speakable(TTS_str)
+ print("websocket_error:tts:{} speakable={}".format(TTS_str, speak))
+ if speak:
+ self.AI_TTS_str = speak
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ self.Close_flag = 1
+ else:
+ self.websocket_error_count += 1
+ if self.websocket_error_count < 3:
+ utime.sleep_ms(3000)
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
print("websocket_error2{}".format(self.websocket_error_count))
usys.print_exception(e)
print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))
def AI_client_VC_ping(self):
- pass
+ """V2.5:WebSocket 文本帧 \"ping\";V3 不调用。不发 RFC6455 OP_PING(默认)。"""
+ if not vc_ws_custom_ping_enabled():
+ return
if self.AI_client_VC_isconnect == 1:
- #self.AI_client_VC.write_frame(0x09,b'ping ping')
self.AI_client_VC_send("ping")
if VC_HEARTBEAT_LOG:
print("VC: ping ping")
def AI_client_VC_send(self,data_str):
- # if isinstance(data_str, str):#是字符串
- # print("AI_client_VC_send:{}".format(data_str))
- # pass
- # else:
- # print("AI_client_VC_send:len{}".format(len(data_str)))
+ # V3:统一在发送队列前将 event/payload 转为 type/data
+ try:
+ if isinstance(data_str, str):
+ data_str = vc_proto_pack_wire(data_str)
+ except Exception:
+ pass
# 统计“语音上行”真实开始/结束:这里只统计二进制音频(非 ping/字符串)
+ is_pcm = not isinstance(data_str, str)
try:
- if not isinstance(data_str, str):
+ if is_pcm:
l = len(data_str)
if l > 0:
+ if self.AI_client_VC_isconnect != 1 or self.AI_client_VC is None:
+ self._uplink_note_tx_fail(l, "enqueue_ws_down")
+ return
self._uplink_mark_start(l)
self._uplink_bytes += l
except Exception:
pass
else:
try:
- ev, tid = _vc_out_json_event_meta(s)
- if ev:
- print("[VC][proto] tx event={} trace_id={} len={} ws_conn={}".format(
- ev, tid or "-", len(s), self.AI_client_VC_isconnect))
- else:
- print("[VC][proto] tx unknown_json len={} ws_conn={}".format(
- len(s), self.AI_client_VC_isconnect))
+ vc_proto_log_tx_wire(s, self.AI_client_VC_isconnect)
except Exception:
- print("[VC][proto] tx parse_fail len={}".format(len(s)))
+ print("[VC][proto] tx log_fail len={}".format(len(s)))
else:
pass
# if VC_AUDIO_FRAME_LOG:
print("VC VC: send_data:ERROR len={} event={} trace_id={}".format(
len(data[1]), _ev or "?", _tid or "-"))
else:
+ if not isinstance(data[1], str):
+ self._uplink_note_tx_fail(len(data[1]), "ws_down")
print("VC VC: send_data:ERROR:{}".format(len(data[1])))
except Exception:
+ if not isinstance(data[1], str):
+ try:
+ self._uplink_note_tx_fail(len(data[1]), "ws_down")
+ except Exception:
+ pass
print("VC VC: send_data:ERROR:{}".format(len(data[1])))
#print("VC: send_data end")
print("DeviceInfo_queue_thread:ERROR:{}".format(e))
usys.print_exception(e)
if data[0] == AI_client_VC_num:
- #self.websocket_error()
- pass
-
-
+ try:
+ if data[1] is not None and not isinstance(data[1], str):
+ self._uplink_note_tx_fail(len(data[1]), "send_exc")
+ except Exception:
+ pass
try:
if self.AI_client_VC_isconnect != 0:
print("VC发送数据错误,退出")
try:
self.websocket_error_count = 0
if isinstance(recv_data, str):#返回的是字符串
+ _tl = recv_data.strip().lower()
+ if _tl in ("ping", "pong"):
+ continue
jsload_data = ujson.loads(recv_data)
+ try:
+ jsload_data = vc_proto_normalize_server(jsload_data)
+ except Exception:
+ pass
if VC_WS_IO_LOG:
print("[VC] recv raw:{}".format(recv_data))
else:
self._turn_has_vad_silence = 0
self._turn_has_tts_started = 0
self._llm_completed_ms = 0
+ self._vc_expect_dialog_tts = 0
_skip_long_up = 0
if getattr(self, "_skip_started_uplink_once", 0) == 1:
try:
except Exception:
pass
print("VC:Started!!!!!!!!\r\n")
+ try:
+ vc_proto_log_started_server(jsload_data.get("payload") or {})
+ except Exception:
+ pass
print("TTS音频为:{},语音下发地址为:{}".format(
self.AI_TTS_str, self.AI_new_voice_url))
try:
except Exception:
pass
self._llm_completed_ms = 0
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
+ try:
+ if (
+ getattr(self, "VC_mode", 0) == 0
+ and getattr(self, "_turn_has_asr_completed", 0) == 1
+ ):
+ self._vc_expect_dialog_tts = 1
+ except Exception:
+ pass
pass
elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)
try:
# - 无 dialog_id:认为是“提示/提醒 TTS”(模式切换可通过 _pending_tts_kind 标记)
try:
did = jsload_data.get("payload", {}).get("dialog_id", "") or ""
+ if not did:
+ did = getattr(self, "_cur_dialog_id", "") or ""
except Exception:
did = ""
- # 注意:无 dialog_id 的 tts.started 很可能是提示音/提醒音;
- # 但“模式切换提示音”的 kind 可能已在 synthesis 时写入 _last_tts_kind,
- # 这里不应无条件覆盖为 prompt,否则播完无法触发下一轮开麦。
try:
- if did:
- self._last_tts_kind = "dialog"
- else:
- k = getattr(self, "_pending_tts_kind", "") or ""
- if k:
- self._last_tts_kind = k
- else:
- cur = getattr(self, "_last_tts_kind", "") or ""
- if cur in (
- "mode_switch_long",
- "mode_switch_ptt",
- "boot_prompt",
- ):
- pass
- elif not cur:
- self._last_tts_kind = "prompt"
+ self._last_tts_kind = self._vc_classify_llm_tts_kind(did)
+ if self._last_tts_kind == "dialog":
+ self._vc_expect_dialog_tts = 0
except Exception:
pass
try:
EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
elif jsload_data["event"] == "failed":#接口返回错误
- if jsload_data["payload"]["error"] == "concurrency_limit_exceeded":
+ pl = jsload_data.get("payload") or {}
+ err = pl.get("error") or ""
+ msg = pl.get("message") or pl.get("msg") or err or ""
+ try:
+ print("[VC] rx failed sid={} err={} msg={}".format(
+ jsload_data.get("sid", "") or "-", err, msg))
+ except Exception:
+ pass
+ if err == "concurrency_limit_exceeded":
EventMesh.publish("publish_AI_btnInfo",{"func":self.concurrency_limit_exceeded_tone})
-
- #utime.sleep_ms(4000)
-
else:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":jsload_data["payload"]["message"]})
+ speak = self._vc_error_tts_speakable(msg)
+ EventMesh.publish(
+ "publish_AI_btnInfo",
+ {"func": self.websocket_error, "data": speak},
+ )
pass
elif jsload_data["event"] == "audio.convert.completed":#音频转成完成没有出错 接口
self.fsm.on_session_start_request()
except Exception:
pass
- self.AI_client_VC_send(ujson.dumps(json_data))
try:
- _tid = json_data.get("header", {}).get("trace_id", "")
- print("[VC][proto] Start enqueued trace_id={} VC_mode={} disable_vad={} vad_silence={}ms ws_conn={}".format(
- _tid or "-",
- self.VC_mode,
- json_data.get("payload", {}).get("inbound_attributes", {}).get("disable_vad", "?"),
- getattr(self, "vad_silence", "?"),
- self.AI_client_VC_isconnect))
+ vc_proto_log_start_from_v25(json_data, getattr(self, "VC_mode", -1))
except Exception:
pass
+ self.AI_client_VC_send(ujson.dumps(json_data))
def AI_client_VC_RequestToSpeak(self):
"""兼容旧入口:长对话短按打断(勿 VC_init,避免灯态/fsm 进 thinking)。"""
print("AI_client_VC_RequestToSpeak!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
self._uplink_active = 1
self._uplink_bytes = 0
self._uplink_start_ms = utime.ticks_ms()
+ self._uplink_tx_fail_cnt = 0
+ self._uplink_tx_fail_bytes = 0
+ self._uplink_tx_fail_log_ms = 0
print("[VC][uplink] START len={} {} VC_record_state_flag={}".format(
first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag))
except Exception:
pass
+
+ def _uplink_note_tx_fail(self, nbytes, reason=""):
+ """上行 PCM 未成功发出(WS 未连、send 异常、协议门控丢弃等)。"""
+ try:
+ n = int(nbytes or 0)
+ self._uplink_tx_fail_cnt = getattr(self, "_uplink_tx_fail_cnt", 0) + 1
+ self._uplink_tx_fail_bytes = getattr(self, "_uplink_tx_fail_bytes", 0) + n
+ now = utime.ticks_ms()
+ last = getattr(self, "_uplink_tx_fail_log_ms", 0)
+ if not last or utime.ticks_diff(now, last) >= 800:
+ self._uplink_tx_fail_log_ms = now
+ print(
+ "[VC][uplink] TX_FAIL reason={} len={} fail_cnt={} fail_bytes={} {}".format(
+ reason or "?",
+ n,
+ self._uplink_tx_fail_cnt,
+ self._uplink_tx_fail_bytes,
+ self._vc_fsm_log_snip(),
+ )
+ )
+ except Exception:
+ pass
+
def _uplink_mark_end(self, reason=""):
try:
if self._uplink_active != 1:
return
dur = utime.ticks_diff(utime.ticks_ms(), self._uplink_start_ms) if self._uplink_start_ms else -1
- print("[VC][uplink] END reason={} bytes={} dur_ms={} {} VC_record_state_flag={}".format(
- reason, self._uplink_bytes, dur, self._vc_fsm_log_snip(), self.VC_record_state_flag))
+ fc = getattr(self, "_uplink_tx_fail_cnt", 0)
+ fb = getattr(self, "_uplink_tx_fail_bytes", 0)
+ print("[VC][uplink] END reason={} bytes={} dur_ms={} tx_fail_cnt={} tx_fail_bytes={} {} VC_record_state_flag={}".format(
+ reason, self._uplink_bytes, dur, fc, fb,
+ self._vc_fsm_log_snip(), self.VC_record_state_flag))
except Exception:
pass
self._uplink_active = 0
self._uplink_bytes = 0
self._uplink_start_ms = 0
+ self._uplink_tx_fail_cnt = 0
+ self._uplink_tx_fail_bytes = 0
+ self._uplink_tx_fail_log_ms = 0
# 最近一次 TTS 的语义来源(dialog/prompt/mode_switch_long/mode_switch_ptt/...)
self._last_tts_kind = ""
# 协议门控:会话未 started 前禁止上行音频(否则 session not initialized)
if VC_PROTO_GUARDS and self._vc_session_started != 1 and self.record_data_save_flag == 0:
_ = self.AI_record_tmp_queue.get()
+ try:
+ self._uplink_note_tx_fail(len(_), "not_started_drop")
+ except Exception:
+ pass
AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(_))
continue
if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):
AI_record_tmp_queue_len += len(read_buf)
while AI_record_tmp_queue_len > 128*1024 and self.AI_record_tmp_queue.empty() != True:
d = self.AI_record_tmp_queue.get()
+ try:
+ self._uplink_note_tx_fail(len(d), "prestart_buf_overflow")
+ except Exception:
+ pass
AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(d))
except Exception:
pass
return 0
return self._PCM_STREAM_RING_BYTES - pcm_free
+ def _vc_pcm_pending_for_play_end(self):
+ """play_pcm 已关闭时勿用 stream_get_buf_len(0) 误判整环待播。"""
+ pcm = getattr(self, "play_pcm", None)
+ if not pcm:
+ return 0
+ try:
+ return max(0, self._vc_pcm_stream_pending_bytes(pcm.stream_get_buf_len()))
+ except Exception:
+ return 0
+
def _vc_finish_play_end_close_if_needed(self):
"""旧版 LocalRespondingEnded:Close_flag==1 时提示音/模式切换播完应 Close 会话。"""
try:
is_pcm_empty_z = (self.play_pcm is None) or (
self.play_pcm is not None and self.play_pcm.stream_is_empty() == 1
)
- pcm_pending_z = 0
- if self.play_pcm is not None:
- try:
- pcm_pending_z = self._vc_pcm_stream_pending_bytes(
- self.play_pcm.stream_get_buf_len())
- except Exception:
- pcm_pending_z = 0
+ pcm_pending_z = self._vc_pcm_pending_for_play_end()
if (not is_pcm_empty_z) or pcm_pending_z > 0:
if since_done < 5000:
return False
slack = max(slack, in_len // 25)
except Exception:
pass
- try:
- if self.play_pcm is not None:
- pcm_free_pre = self.play_pcm.stream_get_buf_len()
- else:
- pcm_free_pre = 0
- except Exception:
- pcm_free_pre = 0
- pcm_pending_pre = self._vc_pcm_stream_pending_bytes(pcm_free_pre)
+ pcm_pending_pre = self._vc_pcm_pending_for_play_end()
if pcm_pending_pre < 0:
pcm_pending_pre = max(0, in_len - out_len)
played_ok = (out_len >= in_len) or (
is_pcm_empty = (self.play_pcm is None) or (
self.play_pcm is not None and self.play_pcm.stream_is_empty() == 1
)
- pcm_free = 0
+ pcm_free = -1
if self.play_pcm is not None:
try:
pcm_free = self.play_pcm.stream_get_buf_len()
except Exception:
pcm_free = -1
- pcm_pending = self._vc_pcm_stream_pending_bytes(pcm_free)
+ pcm_pending = self._vc_pcm_pending_for_play_end()
if pcm_pending < 0:
pcm_pending = max(0, in_len - out_len)
return
except Exception:
pass
+ _syn_text = self.AI_TTS_str or ""
+ try:
+ _syn_kind = getattr(self, "_pending_tts_kind", "") or getattr(self, "_last_tts_kind", "") or ""
+ except Exception:
+ _syn_kind = ""
+ try:
+ vc_proto_log_synthesis(_syn_text, _syn_kind)
+ except Exception:
+ pass
json_data = \
{
"event": "synthesis",
"payload": {
- "text": self.AI_TTS_str
+ "text": _syn_text
}
}
# 语义标记只用于“是否自动开麦”的策略。
# print("AI_client_VC_RequestToRespond 重复 tts_str:{}\nasr_str:{}".format(self.AI_TTS_str,self.AI_ASR_str))
self.AI_client_VC_send(ujson.dumps(json_data))
def AI_client_VC_audio_played(self):
- # V2.5:播完上报;与 VC_PROTO_GUARDS 无关(见文件头说明)
+ # V2.5:播完上报;V3 默认关闭(VC_AUDIO_PLAYED_ACK=0)
try:
+ if not vc_proto_audio_played_enabled():
+ return
if self.AI_client_VC_isconnect != 1 or self.AI_client_VC == None:
return
json_data = {"event": "audio.played", "payload": {}}