From: SnickerSun Date: Wed, 27 May 2026 23:55:29 +0000 (+0800) Subject: 修复长对话模式下,一直播放音频,会造成一直在收录模式的bug,现在超时会自动停止。 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=075982de0df3ef4b137ef347f27e6f894f5749bc;p=mirrors%2FASR1603_G5.git 修复长对话模式下,一直播放音频,会造成一直在收录模式的bug,现在超时会自动停止。 --- diff --git a/.cursor/rules/002-design.mdc b/.cursor/rules/002-design.mdc index 9e9e79f6..dcf28b58 100644 --- a/.cursor/rules/002-design.mdc +++ b/.cursor/rules/002-design.mdc @@ -1,72 +1,106 @@ --- -description: K8吊坠 VC 录音/上传/播放/LED/按键设计动作矩阵(7PIN_1_5) +description: K8吊坠 VC 录音/上传/播放/LED/按键设计思路(7PIN_1_5) alwaysApply: true globs: 2.sdk/project/7PIN_1_5/**/vc_*.py,2.sdk/project/7PIN_1_5/**/protocol.py --- -# VC 录音上传与播放 — 设计动作(7PIN_1_5) +# VC 录音上传与播放 — 设计思路(7PIN_1_5) -**权威设计稿**:`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` +详细演示稿:`1.doc/K8_diaozhui/design/录音上传与播放-演示文稿.md` +量产配置:`products/configs/k8v3/default_config.py`(`product_k8v3.ini`) +主代码:`vc_session` / `vc_session_io` / `vc_session_media`,按键走 `vc_key_router` -## 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 | 无闹钟 | +- **对话 TTS**:云端 LLM 回复,走 `play_pcm` +- **提示 TTS**:notify(模式切换、PTT 教操作等),走 `play_tts` +- **本地音**:`start.mp3`、`think.mp3` 等,走 `_vc_local_play` -**不配置**:`KEY_PRESS_TONE_FILE`(`key_prompt.mp3` 非 K8 需求;按键本身无确认音)。 +同时只能占一路:谁响谁优先;本地提示音播放时**不要停录音**。 + +--- -**PTT 两条 notify 勿混**:`PTT_HINT` = 没长按、教操作;`PTT_RECORD_TOO_SHORT_TTS` = 已长按但松太快、取消录音。 +## 灯怎么亮 -## 术语 ↔ 代码 +- 正在上传/录音 → 蓝闪(listening) +- 等云端想回复 → 蓝绿交替(thinking) +- 正在播对话 → 绿(playing) +- 没事 → 灭或 idle + +长对话 TTS 播完后可以自动再开麦;Power 短按结束会话后会清对话 id。 + +--- -| 设计列 | 实现 | -|--------|------| -| **llm tts** | `play_pcm` / `tts.started` | -| **notify tts** | `play_tts`(`vc_notify` 种类) | -| **local play** | `_vc_local_play`、`audio_montage_play` | +## 长对话:用户一直在说,端侧怎么想 -**仲裁**:llm 播 → notify/local skip;notify 播 → 关 llm+local;local 播 → 关 llm+notify;边录边播 local **不停录**。 +云端逻辑是:你停一下它认为一句结束,就开始想怎么答(LLM);你若马上又开口,它会打断(canceled)并把前后文字拼在一起。 -## 录音 / LED +**端侧要跟云端配合,不能对着干**:LLM 已经开始了,就不要再无限开麦续传,否则 LLM 永远答不完,灯一直蓝闪却没有声音。 -- **开始上传**:以首帧 PCM 上行(`VC_Speech_Start_now`)为起点;此前三路 media end。 -- **结束**:VAD → `asr.completed`;PTT 松手 `audio.completed`。 -- **灯**:上传→listening;ASR 后 LLM→thinking;`tts.started`→playing;否则 idle。 +### 正常说一句(中间有停顿) -## 按键 — AI 键(`VC_mode` 0/1) +说完 → 云端 `asr.completed` → 先暂停上行,等约 2 秒: -| UX | 长对话单击 | PTT 单击 | PTT 长按 | -|----|------------|----------|----------| -| idle | 开录 + `start.mp3`;**仅 Power 短按后再开用新 sid**,否则旧 sid | notify `PTT_HINT`;idle | press 开录 + `start.mp3` / release 结束 | -| recording | 停录立即重开,旧 sid | (无) | 同单击 | -| thinking | 打断后重开,旧 sid | 停对话→idle | 停对话后开录 | -| tts | 停播后重开,旧 sid | 停播→idle | 停播后开录 | +- 2 秒内没再说话 → 关麦,蓝闪等 LLM +- 2 秒内又说了几个字 → 当作同一句继续,合并后再断句 -**PTT 异常**:录音 < `PTT_MIN_RECORD_MS` → **notify** `PTT_RECORD_TOO_SHORT_TTS` + cancel + idle。 +### LLM 已经开始之后 -## 按键 — Power 键 +- **收到 LLM 就关麦**(不再边想边传新音频) +- 若你还接着说了,云端会 canceled;**第一次** canceled 允许再开麦把尾巴接上(补半句) +- **同一轮里第二次** canceled → 不再续说,停麦,切 thinking,等云端用已有文字答完 +- **一轮最长约 25 秒**(`LONG_DIALOGUE_MAX_TURN_MS`),到了强制提交,同样停麦等回复 + +### 别踩的坑(改代码时记住) + +- LLM 之后还无限 `continue merge` → 和云端打断打架 +- 每次 LLM 都把 canceled 计数清零 → 保护永远触发不了 +- 停麦了灯还蓝闪 → 用户以为还在录 +- 停麦后会话挂着不动 → 云端可能 IdleTimeout + +### 联调看这几条日志 + +- 正常:`continue after completed`、第一次 canceled 后有 `keep/restore speech` +- 提交:`[VC][cancel_storm]`、`[VC][turn_cap]` +- 异常:`Deny START by canceled` 后长时间无 TTS + +--- + +## k8v3 相关配置(默认值) + +- `VAD_SILENCE_DEFAULT=1000` — 云端断句静音,storage 可覆盖 +- `LONG_DIALOGUE_ASR_CONTINUE_MS=2000` — 句末 2 秒内续说算同一句 +- `LONG_DIALOGUE_LLM_CANCEL_MAX=2` — 一轮里第二次 canceled 强制提交 +- `LONG_DIALOGUE_MAX_TURN_MS=25000` — 单轮最长 25 秒 +- `RECORD_START_TONE_FILE` — 开录提示 `start.mp3`(不是按键滴声) + +--- + +## AI 键(长对话 VC_mode=0) + +- **idle 短按**:开录 + start 音;Power 短按结束会话后,下次才是新对话 id +- **recording 短按**:停录并立刻重开,同一对话 +- **thinking / tts 短按**:打断播报,再开录 + +## AI 键(PTT VC_mode=1) + +- idle 短按 → 播「请长按说话」提示,不进录音 +- 长按 → start 音 + 录音,松手结束 +- 按太短 → 播「说话时间太短」并取消 + +## Power 键 + +- 短按:结束当前对话(Close WS),清 sid,可进休眠 +- 长按 3 秒:关机 + +--- -| UX | 短按 | -|----|------| -| idle | cancel(如需)→ Close WS → idle/休眠 → **清 sid** | -| recording | cancel 本次 ASR 上传 → Close → 清 sid | -| thinking / tts | cancel 对话/播报 → Close → 清 sid | +## 改代码常去的函数 -**长按 Power 3s**:关机提示音 + 断电(`vc_buttons`)。 +长对话连续说:`_vc_note_asr_result_activity`、`_long_dialogue_handle_continue_speech_cancel`、`_vc_ld_force_commit_turn`、`_vc_long_dialogue_turn_cap_tick` -## 代码锚点 +按键:`KeyRouter.dispatch_press`、`handle_power_short`、`_vc_play_record_start_tone` -- `KeyRouter.dispatch_press` / `handle_ptt_release` / `handle_power_short` -- `_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 为新对话) +会话结束:`Close()` 清 `_cur_dialog_id` diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py index 5bacccb2..b6d2e720 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/default_config.py @@ -163,7 +163,13 @@ VC_IDLE_DEEP_SLEEP_SEC = 60 VC_IDLE_DEEP_SLEEP_ENABLE = 1 # 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖 -VAD_SILENCE_DEFAULT = 300 +VAD_SILENCE_DEFAULT = 1000 + +# 长对话:单轮最长录音(ms)、asr.completed 后续说 merge 窗(ms);0=不限制 +LONG_DIALOGUE_MAX_TURN_MS = 25000 +LONG_DIALOGUE_ASR_CONTINUE_MS = 2000 +# llm 已起后连续 canceled 达此次数则冻结续说、停麦等回复(避免一直录无 TTS) +LONG_DIALOGUE_LLM_CANCEL_MAX = 2 # ============================================================================= # 8. 语音对话 / TTS / 智能体 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py index d5d13c4f..4bdff7d5 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/7PIN_1_5/default_config.py @@ -34,6 +34,9 @@ AI_LONG_PRESS_POWER_OFF_ENABLED = False POWER_ON_TONE_ENABLED = True POWER_ON_TONE_DEFAULT = "你好,我来了" VAD_SILENCE_DEFAULT = 1000 +# 长对话单轮最长录音(ms):句中 vad merge 期间累计;0=不限制 +LONG_DIALOGUE_MAX_TURN_MS = 25000 +LONG_DIALOGUE_ASR_CONTINUE_MS = 2000 VC_TTS_VOICE_TYPE_DEFAULT = None VC_TTS_VOICE_KEY_DEFAULT = None diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py index 06facaea..39827e79 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py @@ -153,7 +153,13 @@ VC_IDLE_DEEP_SLEEP_SEC = 60 VC_IDLE_DEEP_SLEEP_ENABLE = 1 # 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖 -VAD_SILENCE_DEFAULT = 300 +VAD_SILENCE_DEFAULT = 1000 + +# 长对话:单轮最长录音(ms)、asr.completed 后续说 merge 窗(ms);0=不限制 +LONG_DIALOGUE_MAX_TURN_MS = 25000 +LONG_DIALOGUE_ASR_CONTINUE_MS = 2000 +# llm 已起后连续 canceled 达此次数则冻结续说、停麦等回复(避免一直录无 TTS) +LONG_DIALOGUE_LLM_CANCEL_MAX = 2 # ============================================================================= # 8. 语音对话 / TTS / 智能体 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8v3/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8v3/default_config.py index 5bacccb2..b6d2e720 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8v3/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8v3/default_config.py @@ -163,7 +163,13 @@ VC_IDLE_DEEP_SLEEP_SEC = 60 VC_IDLE_DEEP_SLEEP_ENABLE = 1 # 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖 -VAD_SILENCE_DEFAULT = 300 +VAD_SILENCE_DEFAULT = 1000 + +# 长对话:单轮最长录音(ms)、asr.completed 后续说 merge 窗(ms);0=不限制 +LONG_DIALOGUE_MAX_TURN_MS = 25000 +LONG_DIALOGUE_ASR_CONTINUE_MS = 2000 +# llm 已起后连续 canceled 达此次数则冻结续说、停麦等回复(避免一直录无 TTS) +LONG_DIALOGUE_LLM_CANCEL_MAX = 2 # ============================================================================= # 8. 语音对话 / TTS / 智能体 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py index c4542f1c..43b032da 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py @@ -159,15 +159,346 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: return False + def _vc_asr_continue_merge_ms(self): + try: + if "vc_config" in globals() and vc_config: + return int(vc_config.get_int("LONG_DIALOGUE_ASR_CONTINUE_MS", 2000)) + except Exception: + pass + return 2000 + + def _vc_ld_cancel_storm_max(self): + try: + if "vc_config" in globals() and vc_config: + return int(vc_config.get_int("LONG_DIALOGUE_LLM_CANCEL_MAX", 2)) + except Exception: + pass + return 2 + + def _vc_ld_reset_merge_freeze(self): + try: + self._vc_ld_merge_frozen = 0 + self._vc_ld_cancel_in_wall = 0 + self._turn_has_llm_created = 0 + self._vc_ld_awaiting_reply = 0 + except Exception: + pass + + def _vc_ld_on_tts_started(self): + try: + self._vc_ld_merge_frozen = 0 + self._vc_ld_cancel_in_wall = 0 + self._turn_has_llm_created = 0 + self._vc_ld_awaiting_reply = 0 + except Exception: + pass + + def _vc_ld_bump_cancel_in_wall(self): + """单轮墙钟内累计 canceled(不随 llm.created 清零);达上限强制提交。""" + if not self._vc_is_long_dialogue_mode(): + return False + if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: + return False + if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + return True + try: + n = int(getattr(self, "_vc_ld_cancel_in_wall", 0)) + 1 + self._vc_ld_cancel_in_wall = n + mx = self._vc_ld_cancel_storm_max() + if mx > 0 and n >= mx: + self._vc_ld_merge_frozen = 1 + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 + try: + print("[VC][cancel_storm] n={} max={} -> force commit".format(n, mx)) + except Exception: + pass + return True + except Exception: + pass + return False + + def _vc_ld_force_commit_turn(self, reason="force_commit"): + """停止续说开麦,关上行/停录,等云端用已有 ASR 走完 LLM→TTS。""" + if not self._vc_is_long_dialogue_mode(): + return + try: + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 + self._vc_defer_vad_uplink_stop = 0 + self._server_vad_silence = 1 + self._vc_ld_awaiting_reply = 1 + if getattr(self, "VC_record_state_flag", 0) != 0: + try: + self._uplink_mark_end(reason) + except Exception: + pass + self.request_uplink_stop(reason) + if self._vc_effective_asr_text(): + try: + self._vc_publish_led("thinking", reason) + except Exception: + pass + except Exception: + pass + + def _vc_asr_continue_merge_active(self): + """asr.completed 后短窗内仍有 asr.result → 同轮续说 merge。""" + if not self._vc_is_long_dialogue_mode(): + return False + try: + if getattr(self, "_vc_asr_continue_merge", 0) != 1: + return False + comp = getattr(self, "_vc_asr_completed_ms", 0) + last = getattr(self, "_vc_last_asr_result_ms", 0) + if not comp or not last: + return False + win = self._vc_asr_continue_merge_ms() + now = utime.ticks_ms() + if utime.ticks_diff(now, comp) > win or utime.ticks_diff(now, last) > win: + self._vc_asr_continue_merge = 0 + return False + return True + except Exception: + return False + + def _vc_should_hold_mic_for_asr_continue(self): + # llm 已起:云端 barge-in 由 canceled+ASR 承接,端侧不再 hold 开麦 + if getattr(self, "_turn_has_llm_created", 0) == 1: + return False + if getattr(self, "_vc_vad_had_active_after_silence", 0) == 1: + return True + return self._vc_asr_continue_merge_active() + + def _vc_note_asr_result_activity(self): + try: + if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + return + self._vc_last_asr_result_ms = utime.ticks_ms() + # llm 已起后迟到的 asr.result 只更新文本,禁止 continue merge 再开上行 + if getattr(self, "_turn_has_llm_created", 0) == 1: + return + if getattr(self, "_turn_has_asr_completed", 0) == 1: + self._vc_asr_continue_merge = 1 + self._vc_pending_asr_completed_stop = 0 + if getattr(self, "record_data_save_flag", 0) == 0: + self._server_vad_silence = 0 + self._vc_defer_vad_uplink_stop = 0 + try: + print("[VC][ASR] continue after completed -> resume uplink gate") + except Exception: + pass + except Exception: + pass + def _vc_flush_deferred_vad_uplink_stop(self, reason="asr.completed"): - """长对话:asr.completed 再停麦,避免句中 vad.silence 把一句话切成两段。""" + """长对话 asr.completed:vad.silence 句末立停;无 vad 则留 merge 窗再停麦。""" try: - if not getattr(self, "_vc_defer_vad_uplink_stop", 0): + if not self._vc_is_long_dialogue_mode(): return - self._vc_defer_vad_uplink_stop = 0 + if getattr(self, "record_data_save_flag", 0) != 0: + return + if getattr(self, "_vc_vad_had_active_after_silence", 0) == 1: + try: + print("[VC][VAD] asr.completed defer stop (vad pause merged)") + except Exception: + pass + return + try: + self._vc_asr_completed_ms = utime.ticks_ms() + except Exception: + pass + had_defer = getattr(self, "_vc_defer_vad_uplink_stop", 0) + if had_defer: + self._vc_defer_vad_uplink_stop = 0 + elif not self._vc_effective_asr_text(): + return + if had_defer: + self._server_vad_silence = 1 + try: + self._uplink_mark_end(reason) + except Exception: + pass + if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible(): + self.request_uplink_stop(reason) + return + # 无 vad.silence:先 silence_gate,merge 窗内若有新 asr.result 则恢复;超时再关麦 + self._vc_pending_asr_completed_stop = 1 self._server_vad_silence = 1 + try: + self._uplink_mark_end("asr.completed/pending_stop") + except Exception: + pass + except Exception: + pass + + def _vc_asr_completed_stop_tick(self): + """merge 窗结束仍无续说 → 执行延后关麦。""" + try: + if not self._vc_is_long_dialogue_mode(): + return + if getattr(self, "_vc_pending_asr_completed_stop", 0) != 1: + return + if self._vc_asr_continue_merge_active(): + return + comp = getattr(self, "_vc_asr_completed_ms", 0) + if not comp: + return + if utime.ticks_diff(utime.ticks_ms(), comp) < self._vc_asr_continue_merge_ms(): + return + self._vc_pending_asr_completed_stop = 0 if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible(): - self.request_uplink_stop(reason) + try: + print("[VC][ASR] merge window end -> stop uplink") + except Exception: + pass + self.request_uplink_stop("asr.completed/merge_timeout") + except Exception: + pass + + def _long_dialogue_cancel_preserve_speech(self): + """长对话 canceled:非用户打断时,录音中或 asr 续说窗内均保持/恢复上行。""" + if not self._vc_is_long_dialogue_mode(): + return False + try: + if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + return False + if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: + return False + if getattr(self, "is_ruppt_tts", 0) == 1: + return False + # llm 已起:达 cancel 上限后不再保录(由 cancel_storm 提交) + if getattr(self, "_turn_has_llm_created", 0) == 1: + if int(getattr(self, "_vc_ld_cancel_in_wall", 0)) >= self._vc_ld_cancel_storm_max(): + return False + if getattr(self, "VC_record_state_flag", 0) != 0: + return True + if self._vc_asr_continue_merge_active(): + return True + if getattr(self, "_vc_asr_continue_merge", 0) == 1: + return True + except Exception: + pass + return False + + def _vc_ld_first_post_llm_cancel_barge(self): + """llm 已起后第 1 次 canceled(补半句):即使 merge 未置位也允许保录开麦。""" + if not self._vc_is_long_dialogue_mode(): + return False + if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + return False + if getattr(self, "_turn_has_llm_created", 0) != 1: + return False + if int(getattr(self, "_vc_ld_cancel_in_wall", 0)) != 1: + return False + return bool(self._vc_effective_asr_text()) + + def _vc_ld_should_continue_speech_on_cancel(self): + if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + return False + if self._vc_asr_continue_merge_active(): + return True + if getattr(self, "_vc_asr_continue_merge", 0) == 1: + return True + return self._vc_ld_first_post_llm_cancel_barge() + + def _long_dialogue_overlap_cancel_keep_recording(self): + return self._long_dialogue_cancel_preserve_speech() + + def _long_dialogue_handle_continue_speech_cancel(self): + """canceled 但用户仍在说:清 TTS,保持/恢复录音与上行。""" + try: + self._drop_tts_audio = 1 + if getattr(self, "AI_audio_queue", None) is not None: + if self.AI_audio_queue.empty() != True: + self.AI_audio_queue.clear() + self._ptt_hard_mute_local_playback() + if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None: + self.VC_Play_End() + self.fsm.await_played = False + self._need_audio_played_ack = 0 + self.fsm.on_server_canceled() + self._long_dialogue_publish_listening("canceled/continue_speech") + if getattr(self, "VC_record_state_flag", 0) == 0: + self.request_uplink_start("canceled/continue_speech") + else: + self._server_vad_silence = 0 + self._vc_pending_asr_completed_stop = 0 + try: + print("[KEY] long_dialogue canceled -> keep/restore speech") + except Exception: + pass + except Exception: + pass + + def _vc_long_dialogue_max_turn_ms(self): + """长对话单轮录音墙钟上限(ms);0=不限制。merge 续说期间累计,不因 silence_gate 清零。""" + try: + if "vc_config" in globals() and vc_config: + return int(vc_config.get_int("LONG_DIALOGUE_MAX_TURN_MS", 25000)) + except Exception: + pass + return 25000 + + def _vc_turn_wall_clear(self): + try: + self._vc_turn_wall_start_ms = 0 + self._vc_turn_cap_fired = 0 + if hasattr(self, "_vc_ld_reset_merge_freeze"): + self._vc_ld_reset_merge_freeze() + except Exception: + pass + + def _vc_turn_wall_arm_if_needed(self): + """真开录时启动单轮墙钟;句中 vad.active 续说不重置。""" + try: + if not self._vc_is_long_dialogue_mode(): + return + if getattr(self, "_vc_turn_wall_start_ms", 0): + return + self._vc_turn_wall_start_ms = utime.ticks_ms() + self._vc_turn_cap_fired = 0 + except Exception: + pass + + def _vc_long_dialogue_turn_cap_tick(self): + """录音线程周期检查:超 LONG_DIALOGUE_MAX_TURN_MS 强制停录提交(方案2+上限)。""" + try: + if not self._vc_is_long_dialogue_mode(): + return + if getattr(self, "record_data_save_flag", 0) != 0: + return + if getattr(self, "VC_record_state_flag", 0) < 2: + return + if getattr(self, "_vc_turn_cap_fired", 0) == 1: + return + max_ms = self._vc_long_dialogue_max_turn_ms() + if max_ms <= 0: + return + start = getattr(self, "_vc_turn_wall_start_ms", 0) + if not start: + return + if utime.ticks_diff(utime.ticks_ms(), start) < max_ms: + return + self._vc_turn_cap_fired = 1 + self._vc_pending_asr_completed_stop = 0 + self._vc_asr_continue_merge = 0 + self._vc_ld_merge_frozen = 1 + try: + print("[VC][turn_cap] force stop elapsed_ms>={} max_ms={}".format( + utime.ticks_diff(utime.ticks_ms(), start), max_ms)) + except Exception: + pass + self._vc_defer_vad_uplink_stop = 0 + self._vc_vad_had_active_after_silence = 0 + self._server_vad_silence = 1 + try: + self._uplink_mark_end("max_turn_ms") + except Exception: + pass + self.request_uplink_stop("max_turn_ms") + if hasattr(self, "_vc_ld_force_commit_turn"): + self._vc_ld_force_commit_turn("max_turn_ms") except Exception: pass @@ -1086,6 +1417,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): def _long_dialogue_should_hold_listening_led(self): """长对话打断等待/录音:保持蓝慢闪(含 cancel→开麦 窗口)。""" try: + if getattr(self, "_vc_ld_awaiting_reply", 0) == 1: + return False if getattr(self, "_vc_defer_listening_led", 0) == 1: return False if getattr(self, "VC_mode", -1) != 0: @@ -2068,6 +2401,16 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._server_vad_silence = 0 self._vc_defer_vad_uplink_stop = 0 self._vc_vad_had_active_after_silence = 0 + self._vc_turn_wall_start_ms = 0 + self._vc_turn_cap_fired = 0 + self._vc_asr_completed_ms = 0 + self._vc_last_asr_result_ms = 0 + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 + self._turn_has_llm_created = 0 + self._vc_ld_cancel_in_wall = 0 + self._vc_ld_merge_frozen = 0 + self._vc_ld_awaiting_reply = 0 # 单轮状态标志:用于判定 completed 是否应自动进入下一轮 self._turn_has_asr_completed = 0 self._turn_has_asr_result = 0 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py index b1fa492f..31b96dba 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py @@ -103,18 +103,38 @@ class VCSessionIoMixin: pass if self._vc_is_long_dialogue_mode(): try: + if hasattr(self, "_vc_ld_on_tts_started"): + self._vc_ld_on_tts_started() if hasattr(self, "_long_dialogue_on_server_tts_started"): self._long_dialogue_on_server_tts_started() except Exception: pass if ev == "canceled": try: - if ( - self._vc_is_long_dialogue_mode() - and hasattr(self, "_long_dialogue_is_stale_server_cancel") - and self._long_dialogue_is_stale_server_cancel() - ): - self._long_dialogue_handle_stale_cancel() + keep_speech = False + if self._vc_is_long_dialogue_mode(): + storm = False + if hasattr(self, "_vc_ld_bump_cancel_in_wall"): + storm = self._vc_ld_bump_cancel_in_wall() + if storm and hasattr(self, "_vc_ld_force_commit_turn"): + self._vc_ld_force_commit_turn("cancel_storm") + elif ( + hasattr(self, "_long_dialogue_cancel_preserve_speech") + and self._long_dialogue_cancel_preserve_speech() + ) or ( + hasattr(self, "_vc_ld_first_post_llm_cancel_barge") + and self._vc_ld_first_post_llm_cancel_barge() + ): + keep_speech = True + if keep_speech: + if ( + hasattr(self, "_vc_ld_should_continue_speech_on_cancel") + and self._vc_ld_should_continue_speech_on_cancel() + and hasattr(self, "_long_dialogue_handle_continue_speech_cancel") + ): + self._long_dialogue_handle_continue_speech_cancel() + elif hasattr(self, "_long_dialogue_handle_stale_cancel"): + self._long_dialogue_handle_stale_cancel() return except Exception: pass @@ -139,9 +159,13 @@ class VCSessionIoMixin: self._fsm_apply_actions(actions) if ev == "canceled": try: + preserve = False + if hasattr(self, "_long_dialogue_cancel_preserve_speech"): + preserve = self._long_dialogue_cancel_preserve_speech() if ( self._vc_is_long_dialogue_mode() and hasattr(self, "_long_dialogue_end_interrupt") + and not preserve and not ( hasattr(self, "_long_dialogue_is_stale_server_cancel") and self._long_dialogue_is_stale_server_cancel() @@ -745,6 +769,9 @@ class VCSessionIoMixin: self._llm_completed_ms = 0 self._vc_defer_vad_uplink_stop = 0 self._vc_vad_had_active_after_silence = 0 + self._vc_asr_completed_ms = 0 + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 try: self._asr_completed_tone_done = 0 except Exception: @@ -772,6 +799,8 @@ class VCSessionIoMixin: self._turn_has_asr_result = 1 # 有效 ASR 后勿再执行空 ASR 提示音播完开麦 self._vc_reopen_uplink_after_empty_hint = 0 + if hasattr(self, "_vc_note_asr_result_activity"): + self._vc_note_asr_result_activity() except Exception: pass if self.AI_ASR_str == "":#没识别到 @@ -901,6 +930,19 @@ class VCSessionIoMixin: self._server_vad_silence = 1 self._uplink_mark_end("vad.silence") elif jsload_data["event"] == "llm.created" :#思考(LLM) + try: + self._turn_has_llm_created = 1 + except Exception: + pass + try: + if ( + self._vc_is_long_dialogue_mode() + and getattr(self, "VC_record_state_flag", 0) != 0 + and getattr(self, "record_data_save_flag", 0) == 0 + ): + self.request_uplink_stop("llm.created") + except Exception: + pass try: if hasattr(self, "_vc_reset_long_idle_timer"): self._vc_reset_long_idle_timer("llm.created") diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py index 5763b7f5..3915bb8b 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py @@ -528,6 +528,16 @@ class VCSessionMediaMixin: and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1 ) if reason == "canceled" and not user_intr: + try: + if hasattr(self, "_vc_asr_continue_merge_active") and self._vc_asr_continue_merge_active(): + return True + if ( + hasattr(self, "_long_dialogue_cancel_preserve_speech") + and self._long_dialogue_cancel_preserve_speech() + ): + return True + except Exception: + pass print("[VC][uplink_ctrl] Deny START by canceled cause=not_user_interrupt") return False if ( @@ -892,6 +902,11 @@ class VCSessionMediaMixin: self.VC_Speech_Start() if self.VC_record_state_flag == 1: self.VC_record_state_flag =2 + try: + if hasattr(self, "_vc_turn_wall_arm_if_needed"): + self._vc_turn_wall_arm_if_needed() + except Exception: + pass try: if int(getattr(self, "VC_mode", 0)) == 1: self._ptt_record_open_ms = utime.ticks_ms() @@ -909,6 +924,11 @@ class VCSessionMediaMixin: if self.VC_record_state_flag == 0: # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门) self._server_vad_silence = 0 + try: + if hasattr(self, "_vc_turn_wall_clear"): + self._vc_turn_wall_clear() + except Exception: + pass self.AI_ASR_str = "" self.AI_ASR_str_tmp = "" self._last_asr_result_text = "" @@ -1001,6 +1021,11 @@ class VCSessionMediaMixin: def VC_Speech_End(self): try: self._uplink_mark_end("VC_Speech_End") + try: + if hasattr(self, "_vc_turn_wall_clear"): + self._vc_turn_wall_clear() + except Exception: + pass self.AI_ASR_flag = 0 self.AI_ASR_str_tmp = "" # 勿在此清空 AI_ASR_str:vad.silence 停麦常早于 asr.completed,清空会导致 check_text:len:0 连播 think 音 @@ -1065,7 +1090,14 @@ class VCSessionMediaMixin: and getattr(self, "Close_flag", 0) == 0 and getattr(self, "AI_client_VC_isconnect", 0) == 1 ): - self._vc_publish_listening_for_mode("VC_Speech_End/long_wait") + if ( + getattr(self, "_vc_turn_cap_fired", 0) == 1 + or getattr(self, "_vc_ld_merge_frozen", 0) == 1 + or getattr(self, "_vc_ld_awaiting_reply", 0) == 1 + ) and self._vc_effective_asr_text(): + self._vc_publish_led("thinking", "VC_Speech_End/force_commit") + else: + self._vc_publish_listening_for_mode("VC_Speech_End/long_wait") else: self._vc_publish_led("idle", "VC_Speech_End") except Exception: @@ -1211,6 +1243,13 @@ class VCSessionMediaMixin: else: + try: + if hasattr(self, "_vc_long_dialogue_turn_cap_tick"): + self._vc_long_dialogue_turn_cap_tick() + if hasattr(self, "_vc_asr_completed_stop_tick"): + self._vc_asr_completed_stop_tick() + except Exception: + pass VAD_data_count = 0 if self.VC_record_state_flag <=1 :#没发送前先存起来 self.AI_record_tmp_queue.put(read_buf)