修正最新修改到k8/default_config.py
authorsnickersun <snickersun@163.com>
Mon, 25 May 2026 08:20:32 +0000 (16:20 +0800)
committersnickersun <snickersun@163.com>
Mon, 25 May 2026 08:20:32 +0000 (16:20 +0800)
.cursor/rules/002-design.mdc
.cursor/rules/project.mdc
1.doc/K8_diaozhui/design/VC实现审查与简化架构.md
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8s/default_config.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_key_router.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py
2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py

index 0d59d0d..9e9e79f 100644 (file)
@@ -6,7 +6,26 @@ globs: 2.sdk/project/7PIN_1_5/**/vc_*.py,2.sdk/project/7PIN_1_5/**/protocol.py
 
 # VC 录音上传与播放 — 设计动作(7PIN_1_5)
 
-**权威设计稿**:`1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md`。实现:`vc_ux` / `vc_key_router` / `vc_media_arb`。
+**权威设计稿**:`1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md`  
+**量产配置**:`products/configs/k8/default_config.py`(`product_k8.ini` → `DEV_TYPE=k8`)  
+**实现骨架**:`vc_ux` / `vc_key_router` / `vc_media_arb`
+
+## k8 配置摘要(与代码绑定)
+
+| 配置项 | k8 值 | 用途 |
+|--------|-------|------|
+| `RECORD_START_TONE_FILE` | `U:/static/start.mp3` | **开始录音** local 提醒(非按键滴声) |
+| `RECORD_START_TONE_GAP_MS` | 180 | 长对话:start 播完再开上行 |
+| `VAD_SILENCE_DEFAULT` | 1500 | 长对话 VAD 断句 |
+| `PTT_HINT` | 长按说话话术 | PTT **idle 短按** → notify(`_ptt_play_idle_hint`) |
+| `PTT_MIN_RECORD_MS` | 400 | PTT 长按过短判定 |
+| `PTT_RECORD_TOO_SHORT_TTS` | 见配置 | PTT **长按过短** → notify(`_ptt_abort_short_record`) |
+| `POWEROFF_CMD_CLOSE_ONLY` | False | 语音「关机」可走真断电 |
+| `AI_ALARM_ENABLED` | False | 无闹钟 |
+
+**不配置**:`KEY_PRESS_TONE_FILE`(`key_prompt.mp3` 非 K8 需求;按键本身无确认音)。
+
+**PTT 两条 notify 勿混**:`PTT_HINT` = 没长按、教操作;`PTT_RECORD_TOO_SHORT_TTS` = 已长按但松太快、取消录音。
 
 ## 术语 ↔ 代码
 
@@ -28,12 +47,12 @@ globs: 2.sdk/project/7PIN_1_5/**/vc_*.py,2.sdk/project/7PIN_1_5/**/protocol.py
 
 | UX | 长对话单击 | PTT 单击 | PTT 长按 |
 |----|------------|----------|----------|
-| idle | 开录;**仅 Power 短按后再开用新 sid**,否则旧 sid | 提醒长按;idle | press 开录 / release 结束 |
+| idle | 开录 + `start.mp3`;**仅 Power 短按后再开用新 sid**,否则旧 sid | notify `PTT_HINT`;idle | press 开录 + `start.mp3` / release 结束 |
 | recording | 停录立即重开,旧 sid | (无) | 同单击 |
 | thinking | 打断后重开,旧 sid | 停对话→idle | 停对话后开录 |
 | tts | 停播后重开,旧 sid | 停播→idle | 停播后开录 |
 
-**PTT 异常**:录音时长不足 → **notify TTS** + idle(`PTT_RECORD_TOO_SHORT_TTS`)。
+**PTT 异常**:录音 < `PTT_MIN_RECORD_MS` → **notify** `PTT_RECORD_TOO_SHORT_TTS` + cancel + idle。
 
 ## 按键 — Power 键
 
@@ -48,5 +67,6 @@ globs: 2.sdk/project/7PIN_1_5/**/vc_*.py,2.sdk/project/7PIN_1_5/**/protocol.py
 ## 代码锚点
 
 - `KeyRouter.dispatch_press` / `handle_ptt_release` / `handle_power_short`
-- `_vc_restart_record_same_dialog`、`_ptt_stop_dialog_to_idle`、`_ptt_abort_short_record`
+- `_vc_play_record_start_tone`、`_vc_restart_record_same_dialog`
+- `_ptt_play_idle_hint`、`_ptt_abort_short_record`、`_ptt_stop_dialog_to_idle`
 - `Close()` 清 `_cur_dialog_id`(Power 后下次 AI 为新对话)
index 2499777..914588d 100644 (file)
@@ -6,6 +6,7 @@ alwaysApply: true
 - 你是高级嵌入式工程师,代码必须遵循最佳实践和最佳设计原则\r
 - 本工程是micropython工程,\r
 - 2.sdk/Helios_SDK是编译路径,2.sdk/project/7PIN_1_5是项目路径 ,代码优先在项目路径里找,没有的通用代码才在编译路径里。\r
+- 2.sdk\project\7PIN_1_5\sdk\services\microPython\ports\quectel\core\products\configs\k8\default_config.py 是默认k8 的配置参数。\r
 - 编译指令是 new_clean.bat k8 1 \r
 - 分析日志解决问题时,要考虑根因,不要只是靠简单判断条件进行兜底修复。\r
 - 每次修改都要考虑副作用,不能拆东墙补西墙
\ No newline at end of file
index 512f43b..7f9c713 100644 (file)
@@ -1,54 +1,95 @@
-# VC 实现审查与简化架构(7PIN_1_5)\r
-\r
-> 权威:`录音上传与播放-演示文稿.md`(含 **Power 键**、**sid 规则**、**PTT 录音过短 notify**)\r
-\r
-## 1. 设计稿要点(2025 更新)\r
-\r
-### 媒体三车道\r
-\r
-| 车道 | 抢占 |\r
-|------|------|\r
-| LLM PCM | notify/local defer |\r
-| Notify TTS | 关 llm + local |\r
-| Local MP3 | 关 llm + notify;录音中可并行 |\r
-\r
-### sid 规则\r
-\r
-- **AI  idle 开录**:默认 **旧 sid**(同 WS 会话续 `dialog_id`)。\r
-- **仅 Power 短按 Close 后**:清 `_cur_dialog_id`,下次 `AI_VC_start` 为 **新 sid**。\r
-\r
-### Power 短按\r
-\r
-各 UX 态均:**协议 cancel(按需)→ Close WS → idle/休眠 → 清 sid**。\r
-\r
-### PTT 异常\r
-\r
-长按松手且录音 < `PTT_MIN_RECORD_MS`:cancel、**play_tts notify**、idle。\r
-\r
-## 2. 代码结构(三件套)\r
-\r
-| 模块 | 文件 |\r
-|------|------|\r
-| UX 四态 | `vc_ux.py` → `derive_ux_state` |\r
-| 按键表 | `vc_key_router.py` → AI / Power / PTT release |\r
-| 媒体仲裁 | `vc_media_arb.py` → `before_local` / `before_notify` / `before_llm_pcm` |\r
-\r
-`vc_mode_strategy` 仅转发 `KeyRouter`;`vc_present` 用 UX 映 LED。\r
-\r
-## 3. 符合度(当前)\r
-\r
-| 项 | 状态 |\r
-|----|------|\r
-| UX + KeyRouter + MediaArbiter 骨架 | ✅ |\r
-| 长对话 recording 重开 | ✅ |\r
-| PTT idle/thinking/tts 短按 | ✅ |\r
-| Power 短按按 UX cancel+Close | ✅ |\r
-| PTT 录音过短 notify | ✅ |\r
-| notify `play_tts` 仲裁入口 | ✅ |\r
-| `vc_session_io` 仅 FSM actions | 🔲 后续 |\r
-\r
-## 4. 迁移剩余\r
-\r
-- 收敛 `play_tts_cb` 断 WS:区分 takeover notify 与会话内 synthesis\r
-- 录音灯挂 `VC_Speech_Start_now` 首帧\r
-- 删与 `InterruptContext` 重复的 `_ptt_*` latch\r
+# VC 实现审查与简化架构(7PIN_1_5 / k8 量产)
+
+> 权威:`录音上传与播放-演示文稿.md`(含 **Power 键**、**sid 规则**、**PTT 录音过短 notify**)  
+> **配置唯一入口**:`products/configs/k8/default_config.py`(`product_k8.ini`)
+
+## 1. 设计稿要点
+
+### 媒体三车道
+
+| 车道 | 抢占 |
+|------|------|
+| LLM PCM | notify/local defer |
+| Notify TTS | 关 llm + local |
+| Local MP3 | 关 llm + notify;录音中可并行 |
+
+### sid 规则
+
+- **AI idle 开录**:默认 **旧 sid**(同 WS 会话续 `dialog_id`)。
+- **仅 Power 短按 Close 后**:清 `_cur_dialog_id`,下次 `AI_VC_start` 为 **新 sid**。
+
+### Power 短按
+
+各 UX 态均:**协议 cancel(按需)→ Close WS → idle/休眠 → 清 sid**。
+
+### PTT 两类 notify(均需保留)
+
+| 场景 | 用户行为 | 配置 | 代码 |
+|------|----------|------|------|
+| idle **短按** | 未长按,需教操作 | `PTT_HINT` | `_ptt_play_idle_hint` → `play_tts` |
+| **长按过短** | 已开录但 < `PTT_MIN_RECORD_MS` 松手 | `PTT_RECORD_TOO_SHORT_TTS` | `KeyRouter.ptt_release_after_long_press` → `_ptt_abort_short_record` |
+
+### PTT 异常(过短)
+
+长按松手且录音 < `PTT_MIN_RECORD_MS`:cancel、**play_tts notify**、idle。
+
+## 2. 提示音产品定义(K8,2025 澄清)
+
+| 类型 | 是否需要 | 实现 |
+|------|----------|------|
+| 开始录音提醒 | ✅ | `RECORD_START_TONE_FILE` → `U:/static/start.mp3` |
+| 按键确认音 / `key_prompt.mp3` | ❌ | k8 已删除 `KEY_PRESS_TONE_*`;非「滴」 |
+| PTT idle 教学 | ✅ notify | `PTT_HINT` |
+| PTT 录音过短 | ✅ notify | `PTT_RECORD_TOO_SHORT_TTS` |
+| think 提醒 | ✅ local | `vc_session_io` 硬编码 `think.mp3`(待配置化) |
+
+## 3. 代码结构(三件套)
+
+| 模块 | 文件 |
+|------|------|
+| UX 四态 | `vc_ux.py` → `derive_ux_state` |
+| 按键表 | `vc_key_router.py` → AI / Power / PTT release |
+| 媒体仲裁 | `vc_media_arb.py` → `before_local` / `before_notify` / `before_llm_pcm` |
+
+`vc_mode_strategy` 仅转发 `KeyRouter`;`vc_present` 用 UX 映 LED。
+
+## 4. k8 配置 ↔ 设计
+
+| 设计项 | k8 配置 | 代码 |
+|--------|---------|------|
+| Local 开录 start | `RECORD_START_TONE_FILE=start.mp3` | `_vc_play_record_start_tone` |
+| Local 长对话短按「滴」 | **无**(非需求) | `_vc_play_key_press_tone` 配置缺省即 return |
+| Notify PTT idle 提醒 | `PTT_HINT` | `_ptt_play_idle_hint` |
+| Notify 录音过短 | `PTT_RECORD_TOO_SHORT_TTS` | `ptt_release_after_long_press` |
+| Notify 开机/模式切换 | `POWER_ON_*` / `CMD_TOGGLE_*` | `play_tts` |
+| VAD 断句 | `VAD_SILENCE_DEFAULT=1500` | `Start` payload |
+| Power 短按清 sid | (行为) | `KeyRouter.handle_power_short` → `Close` |
+| 语音关机 | `POWEROFF_CMD_CLOSE_ONLY=False` | 允许真断电分支 |
+
+## 5. 符合度(当前)
+
+| 项 | 状态 |
+|----|------|
+| UX + KeyRouter + MediaArbiter 骨架 | ✅ |
+| 长对话 recording 重开 + start 开录音 | ✅ |
+| PTT idle/thinking/tts 短按 | ✅ |
+| Power 短按按 UX cancel+Close | ✅ |
+| PTT 录音过短 notify | ✅ |
+| notify `play_tts` 仲裁入口 | ✅ |
+| 无 KEY_PRESS / key_prompt 短按音 | ✅ |
+| `vc_session_io` 仅 FSM actions | 🔲 后续 |
+
+## 6. 迁移剩余
+
+- 收敛 `play_tts_cb` 断 WS:区分 takeover notify 与会话内 synthesis
+- 录音灯挂 `VC_Speech_Start_now` 首帧
+- 删与 `InterruptContext` 重复的 `_ptt_*` latch
+
+## 7. 实机检查
+
+1. 编译:`new_clean.bat k8 1` 后确认 `core/default_config.py` 与 k8 一致  
+2. 日志:`vad_silence=1500`(非 Flash 300)  
+3. 长对话短按续聊:无滴声,有 **start** 开录音  
+4. PTT idle **短按**:播报 `PTT_HINT`(教长按)  
+5. PTT **长按 <400ms 松手**:播报 `PTT_RECORD_TOO_SHORT_TTS`(过短)  
+6. `usr/static/start.mp3` 存在  
index aefa493..d7723d3 100644 (file)
@@ -174,13 +174,10 @@ KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
 PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
 
 # =============================================================================
-# 10. 按键音 / 开录音 / PTT 时序(k8:开录仅以灯提示,不播专用开录音)
+# 10. 开录音 / PTT 时序(k8 量产:仅 start.mp3 开录提醒,无按键确认音)
 # =============================================================================
 
-KEY_PRESS_TONE_FILE = None         # AI 短按操作音;None=不播
-KEY_PRESS_TONE_DELAY_MS = 300
-
-RECORD_START_TONE_FILE = None      # 单对话长按开录提示音;None=仅蓝快闪
+RECORD_START_TONE_FILE = "U:/static/start.mp3"
 RECORD_START_TONE_GAP_MS = 180
 
 PTT_RELEASE_END_SPEECH_DELAY_MS = 200   # 松手后发 audio.completed 前延迟
index e7c2e84..d5d13c4 100644 (file)
@@ -33,8 +33,7 @@ AI_LONG_PRESS_POWER_OFF_ENABLED = False
 # ---- 开机提示/语音参数默认值 ----
 POWER_ON_TONE_ENABLED = True
 POWER_ON_TONE_DEFAULT = "你好,我来了"
-# 服务端 VAD 断句静音时长(ms):句中短暂停顿勿小于 1200;可 storage vad_silence 覆盖
-VAD_SILENCE_DEFAULT = 1500
+VAD_SILENCE_DEFAULT = 1000
 VC_TTS_VOICE_TYPE_DEFAULT = None
 VC_TTS_VOICE_KEY_DEFAULT = None
 
@@ -55,10 +54,8 @@ DIALOG_BOT_OPTION = None
 DIALOG_PROVIDER_DEFAULT = None
 KEYWORDS_FROM_VOICE_TYPE = False
 
-# ---- 按键音/提示音策略 ----
-KEY_PRESS_TONE_FILE = "U:/static/key_prompt.mp3"  # REQ-AI-01:短按操作确认「滴」
-KEY_PRESS_TONE_DELAY_MS = 300
-RECORD_START_TONE_FILE = "U:/static/start.mp3"  # 开录/打断续聊提示音
+# ---- 提示音(K8 量产见 products/configs/k8/default_config.py)----
+RECORD_START_TONE_FILE = "U:/static/start.mp3"
 RECORD_START_TONE_GAP_MS = 180
 # VC 空闲/超时(ai_VC._1S_timer_cb_);长对话无下行超时
 VC_IDLE_AUTO_CLOSE_SEC = 20
@@ -67,6 +64,4 @@ VC_IDLE_AUTO_CLOSE_SEC_PTT = 0
 VC_IDLE_DEEP_SLEEP_SEC = 60
 VC_IDLE_DEEP_SLEEP_ENABLE = 1
 PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
-PTT_MIN_RECORD_MS = 400
-PTT_RECORD_TOO_SHORT_TTS = "录音时间太短,请按住圆形按钮说话"
 
index aefa493..71ac4da 100644 (file)
@@ -114,6 +114,7 @@ GNSS_ENABLED = False           # 无 GNSS 硬件时 False,避免 import quecgn
 UART_SUPPORTED = False
 BT_OTA_ENABLED = False
 UPLOAD_START_TONE_ENABLED = False  # 开始上传语音时是否播成功提示音
+AI_ALARM_ENABLED = False         # K8 吊坠无闹钟/日程硬件
 
 # =============================================================================
 # 6. 按键逻辑(AI_dev / btn_device)
@@ -139,7 +140,8 @@ 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 = 1500
 
 # =============================================================================
 # 8. 语音对话 / TTS / 智能体
@@ -174,15 +176,17 @@ KWS_PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
 PTT_HINT = "请您按住圆形的按钮开始讲话,讲完松开即可 "
 
 # =============================================================================
-# 10. 按键音 / 开录音 / PTT 时序(k8:开录仅以灯提示,不播专用开录音)
+# 10. 开录音 / PTT 时序(对齐「录音上传与播放」设计稿)
 # =============================================================================
+# 无按键确认音:不按 key_prompt;仅「开始录音」播 start.mp3(非滴声)
+# PTT idle 短按:notify 话术 PTT_HINT;长按开录:start.mp3
 
-KEY_PRESS_TONE_FILE = None         # AI 短按操作音;None=不播
-KEY_PRESS_TONE_DELAY_MS = 300
-
-RECORD_START_TONE_FILE = None      # 单对话长按开录提示音;None=仅蓝快闪
+RECORD_START_TONE_FILE = "U:/static/start.mp3"    # 开录/打断续聊提醒(唯一本地按键相关短音)
 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 收尾
index 2567a6e..73ca356 100644 (file)
@@ -177,12 +177,9 @@ KWS_PTT_HINT = "请您按住电源键开始讲话,讲完松开即可 "
 PTT_HINT = "请您按住电源键开始讲话,讲完松开即可 "
 
 # =============================================================================
-# 10. 按键音 / 开录音 / PTT 时序(k8s:开录仅以灯提示,不播专用开录音)
+# 10. 开录音 / PTT 时序(k8s:无按键确认音;开录见 RECORD_START_*)
 # =============================================================================
 
-KEY_PRESS_TONE_FILE = None         # AI 短按操作音;None=不播
-KEY_PRESS_TONE_DELAY_MS = 300
-
 RECORD_START_TONE_FILE = None      # 单对话长按开录提示音;None=仅蓝快闪
 RECORD_START_TONE_GAP_MS = 180
 
index d9436cb..98e0863 100644 (file)
-# -*- coding: utf-8 -*-\r
-"""按键表驱动:mode × UX × 事件 → 既有 vc_session 方法。"""\r
-try:\r
-    import EventMesh\r
-    import constants\r
-    import utime\r
-except Exception:\r
-    EventMesh = None\r
-    constants = None\r
-    utime = None\r
-\r
-from vc_ux import derive_ux_state, UX_IDLE, UX_RECORDING, UX_THINKING, UX_TTS, ux_name\r
-\r
-try:\r
-    import vc_config\r
-except Exception:\r
-    vc_config = None\r
-\r
-\r
-class KeyRouter(object):\r
-    @staticmethod\r
-    def dispatch_press(vc):\r
-        try:\r
-            if int(getattr(vc, "VC_mode", 0)) == 1:\r
-                return KeyRouter._ptt_press(vc)\r
-            return KeyRouter._long_ai_press(vc)\r
-        except Exception as e:\r
-            try:\r
-                import usys\r
-                usys.print_exception(e)\r
-            except Exception:\r
-                pass\r
-            return False\r
-\r
-    @staticmethod\r
-    def handle_ptt_release(vc):\r
-        if int(getattr(vc, "VC_mode", 0)) != 1:\r
-            return False\r
-        if getattr(vc, "AI_key_long_press_flag", 0) == 1:\r
-            return False\r
-        ux = derive_ux_state(vc)\r
-        try:\r
-            print("[KEY][router] ptt_release ux={}".format(ux_name(ux)))\r
-        except Exception:\r
-            pass\r
-        if ux == UX_TTS:\r
-            try:\r
-                if vc._ptt_is_active_tts_playback():\r
-                    vc._ptt_stop_tts_on_short_press()\r
-                    return True\r
-            except Exception:\r
-                pass\r
-        if ux == UX_THINKING:\r
-            try:\r
-                vc._ptt_stop_dialog_to_idle()\r
-                return True\r
-            except Exception:\r
-                pass\r
-        if ux == UX_IDLE:\r
-            try:\r
-                vc.key_prompt()\r
-                return True\r
-            except Exception:\r
-                pass\r
-        return False\r
-\r
-    @staticmethod\r
-    def handle_power_short(vc):\r
-        """Power 短按:cancel → Close → 清 sid(设计稿幻灯片 6)。"""\r
-        ux = derive_ux_state(vc)\r
-        try:\r
-            print("[KEY][router] power_short ux={}".format(ux_name(ux)))\r
-        except Exception:\r
-            pass\r
-        try:\r
-            vc._vc_send_protocol_cancel("power_short/ux={}".format(ux_name(ux)))\r
-        except Exception:\r
-            pass\r
-        try:\r
-            vc._vc_close_reason = "power_short"\r
-        except Exception:\r
-            pass\r
-        try:\r
-            if EventMesh:\r
-                EventMesh.publish("publish_AI_btnInfo", {"func": vc.Close})\r
-        except Exception:\r
-            pass\r
-        try:\r
-            if EventMesh and constants:\r
-                EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
-        except Exception:\r
-            pass\r
-        return True\r
-\r
-    @staticmethod\r
-    def _long_ai_press(vc):\r
-        ux = derive_ux_state(vc)\r
-        try:\r
-            print("[KEY][router] ai_press ux={} {}".format(\r
-                ux_name(ux), getattr(vc, "_vc_fsm_log_snip", lambda: "")()))\r
-        except Exception:\r
-            pass\r
-        if ux == UX_RECORDING:\r
-            try:\r
-                return vc._vc_restart_record_same_dialog()\r
-            except Exception:\r
-                pass\r
-        if ux in (UX_THINKING, UX_TTS):\r
-            try:\r
-                vc._long_dialogue_interrupt_on_short_press()\r
-                return True\r
-            except Exception:\r
-                pass\r
-        if vc._ai_key_press_long_dialogue_session():\r
-            return True\r
-        if vc._ai_key_press_long_dialogue_wake():\r
-            if EventMesh and constants:\r
-                EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
-            return True\r
-        return KeyRouter._long_fallback_start(vc)\r
-\r
-    @staticmethod\r
-    def _long_fallback_start(vc):\r
-        try:\r
-            vc.Close_flag = 0\r
-            vc._skip_started_uplink_once = 0\r
-            vc._vc_long_dialogue_key_arm_session()\r
-            vc._long_dialogue_publish_listening("key.press/fallback")\r
-            vc.get_config_timer_count = 0\r
-            vc.key_state = 1\r
-            vc.AI_TTS_str = ""\r
-            vc.AI_ASR_str = ""\r
-            vc.AI_ASR_str_tmp = ""\r
-            EventMesh.publish("publish_AI_btnInfo", {"func": vc.AI_VC_start})\r
-            vc.AI_key_long_press_flag = 0\r
-            return True\r
-        except Exception:\r
-            return False\r
-\r
-    @staticmethod\r
-    def _ptt_press(vc):\r
-        return True\r
-\r
-    @staticmethod\r
-    def ptt_release_after_long_press(vc):\r
-        """PTT 长按松手:录音过短 → notify TTS + idle。"""\r
-        if getattr(vc, "VC_record_state_flag", 0) == 0:\r
-            return False\r
-        min_ms = 400\r
-        tts = "录音时间太短,请按住圆形按钮说话"\r
-        try:\r
-            if vc_config:\r
-                min_ms = int(vc_config.get_int("PTT_MIN_RECORD_MS", min_ms))\r
-                tts = vc_config.get_attr("PTT_RECORD_TOO_SHORT_TTS", tts) or tts\r
-        except Exception:\r
-            pass\r
-        t_open = int(getattr(vc, "_ptt_record_open_ms", 0) or 0)\r
-        if not t_open or utime is None:\r
-            return False\r
-        try:\r
-            if utime.ticks_diff(utime.ticks_ms(), t_open) >= min_ms:\r
-                return False\r
-        except Exception:\r
-            return False\r
-        try:\r
-            vc._ptt_abort_short_record(tts)\r
-        except Exception:\r
-            pass\r
-        return True\r
+# -*- coding: utf-8 -*-
+"""按键表驱动:mode × UX × 事件 → 既有 vc_session 方法。"""
+try:
+    import EventMesh
+    import constants
+    import utime
+except Exception:
+    EventMesh = None
+    constants = None
+    utime = None
+
+from vc_ux import derive_ux_state, UX_IDLE, UX_RECORDING, UX_THINKING, UX_TTS, ux_name
+
+try:
+    import vc_config
+except Exception:
+    vc_config = None
+
+
+class KeyRouter(object):
+    @staticmethod
+    def dispatch_press(vc):
+        try:
+            if int(getattr(vc, "VC_mode", 0)) == 1:
+                return KeyRouter._ptt_press(vc)
+            return KeyRouter._long_ai_press(vc)
+        except Exception as e:
+            try:
+                import usys
+                usys.print_exception(e)
+            except Exception:
+                pass
+            return False
+
+    @staticmethod
+    def handle_ptt_release(vc):
+        if int(getattr(vc, "VC_mode", 0)) != 1:
+            return False
+        if getattr(vc, "AI_key_long_press_flag", 0) == 1:
+            return False
+        ux = derive_ux_state(vc)
+        try:
+            print("[KEY][router] ptt_release ux={}".format(ux_name(ux)))
+        except Exception:
+            pass
+        if ux == UX_TTS:
+            try:
+                if vc._ptt_is_active_tts_playback():
+                    vc._ptt_stop_tts_on_short_press()
+                    return True
+            except Exception:
+                pass
+        if ux == UX_THINKING:
+            try:
+                vc._ptt_stop_dialog_to_idle()
+                return True
+            except Exception:
+                pass
+        if ux == UX_IDLE:
+            try:
+                if hasattr(vc, "_ptt_play_idle_hint"):
+                    vc._ptt_play_idle_hint()
+                    return True
+            except Exception:
+                pass
+        return False
+
+    @staticmethod
+    def handle_power_short(vc):
+        """Power 短按:cancel → Close → 清 sid(设计稿幻灯片 6)。"""
+        ux = derive_ux_state(vc)
+        try:
+            print("[KEY][router] power_short ux={}".format(ux_name(ux)))
+        except Exception:
+            pass
+        try:
+            vc._vc_send_protocol_cancel("power_short/ux={}".format(ux_name(ux)))
+        except Exception:
+            pass
+        try:
+            vc._vc_close_reason = "power_short"
+        except Exception:
+            pass
+        try:
+            if EventMesh:
+                EventMesh.publish("publish_AI_btnInfo", {"func": vc.Close})
+        except Exception:
+            pass
+        try:
+            if EventMesh and constants:
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)
+        except Exception:
+            pass
+        return True
+
+    @staticmethod
+    def _long_ai_press(vc):
+        ux = derive_ux_state(vc)
+        try:
+            print("[KEY][router] ai_press ux={} {}".format(
+                ux_name(ux), getattr(vc, "_vc_fsm_log_snip", lambda: "")()))
+        except Exception:
+            pass
+        if ux == UX_RECORDING:
+            try:
+                return vc._vc_restart_record_same_dialog()
+            except Exception:
+                pass
+        if ux in (UX_THINKING, UX_TTS):
+            try:
+                vc._long_dialogue_interrupt_on_short_press()
+                return True
+            except Exception:
+                pass
+        if vc._ai_key_press_long_dialogue_session():
+            return True
+        if vc._ai_key_press_long_dialogue_wake():
+            if EventMesh and constants:
+                EventMesh.publish(constants.EVENT.SYSTEM_UP)
+            return True
+        return KeyRouter._long_fallback_start(vc)
+
+    @staticmethod
+    def _long_fallback_start(vc):
+        try:
+            vc.Close_flag = 0
+            vc._skip_started_uplink_once = 0
+            vc._vc_long_dialogue_key_arm_session()
+            vc._long_dialogue_publish_listening("key.press/fallback")
+            vc.get_config_timer_count = 0
+            vc.key_state = 1
+            vc.AI_TTS_str = ""
+            vc.AI_ASR_str = ""
+            vc.AI_ASR_str_tmp = ""
+            EventMesh.publish("publish_AI_btnInfo", {"func": vc.AI_VC_start})
+            vc.AI_key_long_press_flag = 0
+            return True
+        except Exception:
+            return False
+
+    @staticmethod
+    def _ptt_press(vc):
+        return True
+
+    @staticmethod
+    def ptt_release_after_long_press(vc):
+        """PTT 长按松手:录音过短 → notify TTS + idle。"""
+        if getattr(vc, "VC_record_state_flag", 0) == 0:
+            return False
+        min_ms = 400
+        tts = "录音时间太短,请按住圆形按钮说话"
+        try:
+            if vc_config:
+                min_ms = int(vc_config.get_int("PTT_MIN_RECORD_MS", min_ms))
+                tts = vc_config.get_attr("PTT_RECORD_TOO_SHORT_TTS", tts) or tts
+        except Exception:
+            pass
+        t_open = int(getattr(vc, "_ptt_record_open_ms", 0) or 0)
+        if not t_open or utime is None:
+            return False
+        try:
+            if utime.ticks_diff(utime.ticks_ms(), t_open) >= min_ms:
+                return False
+        except Exception:
+            return False
+        try:
+            vc._ptt_abort_short_record(tts)
+        except Exception:
+            pass
+        return True
index f93fd6d..e41fd3a 100644 (file)
@@ -2854,7 +2854,25 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
             return
         self._handle_pwk_short_release(event, msg, merged_ai_tail=False)
     def key_prompt(self):
-        EventMesh.publish("audio_montage_play",["key_prompt.mp3"])
+        """遗留 montage(audio.bin 内 key_prompt);K8 量产不用,请用 RECORD_START_TONE。"""
+        EventMesh.publish("audio_montage_play", ["key_prompt.mp3"])
+
+    def _ptt_play_idle_hint(self):
+        """PTT idle 短按:notify 话术提醒(对齐 k8 PTT_HINT,非 KEY_PRESS 本地音)。"""
+        try:
+            tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+            if "vc_config" in globals() and vc_config:
+                tts_str = (
+                    vc_config.get_attr("PTT_HINT", None)
+                    or vc_config.get_attr("KWS_PTT_HINT", None)
+                    or tts_str
+                )
+            tts_str = str(tts_str or "").strip()
+            if not tts_str:
+                return
+            EventMesh.publish("play_tts", [tts_str, 0, "prompt"])
+        except Exception as e:
+            usys.print_exception(e)
     def VC_error(self, event, msg):#错误播报
         self.websocket_error()
     def set_voicerupt_flag(self,event,msg):#按键控制当前留言播放
index b60d3b8..9c126e7 100644 (file)
@@ -144,7 +144,7 @@ class VCSessionMediaMixin:
             return False
 
     def _vc_play_key_press_tone(self, via=""):
-        """长对话短按操作确认音(REQ-AI-01);KEY_PRESS_TONE_FILE=None 时不播。"""
+        """已废弃:K8 无按键确认音;仅 RECORD_START_TONE(start.mp3)。配置缺省则不播。"""
         try:
             path = None
             hold_ms = 300