# 服务端断句静音(ms);句中短暂停顿建议 ≥1200;可被 storage vad_silence 覆盖
VAD_SILENCE_DEFAULT = 1500
+# 长对话打断等:等 canceled 超时后再开麦(ms)
+LONG_DIALOGUE_CANCEL_WAIT_MS = 800
+LONG_DIALOGUE_DROP_TTS_MS = 8000
+
+# asr.completed 滴声(think.mp3):同轮须先有 asr.result;防抖避免残留 completed 连播
+ASR_COMPLETED_THINK_DEBOUNCE_MS = 2500
+# llm 完成但云端未下 tts.started:用 llm.completed 正文发 synthesis 兜底播报
+LLM_WITHOUT_TTS_SYNTHESIS_FALLBACK = True
+# completed 后云端迟到 tts.started 等待窗(此期间勿发 cancel)
+LLM_LATE_TTS_WAIT_MS = 3500
+# 打断 cancel 开麦后:短 uplink/空 ASR 仅重开麦;vad.silence 宽限(ms)
+INTERRUPT_ASR_RECOVER_MS = 8000
+INTERRUPT_VAD_SILENCE_GRACE_MS = 3000
+INTERRUPT_ASR_MIN_UPLINK_MS = 2000
+
# =============================================================================
# 8. 语音对话 / TTS / 智能体
# =============================================================================
RECORD_START_TONE_FILE = "U:/static/start.mp3" # 按键/PTT 开录提示音(非 server.started 自动开麦)
RECORD_START_TONE_GAP_MS = 180
+RECORD_START_TONE_UPLINK_DELAY_MS = 700 # 滴声结束后再启录音/上行(须≥MP3 时长,避免滞后 ASR)
+ASR_EMPTY_MIN_UPLINK_MS = 1500 # 上行短于此的空 ASR:按键开麦仅重开麦,不播「没听清」
+RECORD_RESTART_STALE_DROP_MS = 1500 # key.restart 后至 uplink_open 前丢弃迟到 cancel/ASR/TTS
PTT_MIN_RECORD_MS = 400
PTT_RECORD_TOO_SHORT_TTS = "录音时间太短,请按住圆形按钮说话" # notify
-# -*- coding: utf-8 -*-\r
-"""长对话/PTT 打断上下文:绑定 turn_gen,收敛 is_ruppt_tts 等 legacy 标志。"""\r
-try:\r
- import utime\r
-except Exception:\r
- utime = None\r
-\r
-\r
-class InterruptContext(object):\r
- KIND_NONE = ""\r
- KIND_LONG_SHORT = "long_short"\r
- KIND_PTT_SHORT = "ptt_short"\r
- KIND_PTT_LONG = "ptt_long"\r
-\r
- def __init__(self):\r
- self.active = 0\r
- self.kind = self.KIND_NONE\r
- self.gen = 0\r
- self.pending_uplink = 0\r
- self.drop_tts = 0\r
- self.ruppt_tts = 0\r
- self.settled_ms = 0\r
- self.cancel_deadline_ms = 0\r
-\r
- def clear(self, vc=None):\r
- self.active = 0\r
- self.kind = self.KIND_NONE\r
- self.gen = 0\r
- self.pending_uplink = 0\r
- self.drop_tts = 0\r
- self.ruppt_tts = 0\r
- self.settled_ms = 0\r
- self.cancel_deadline_ms = 0\r
- if vc is not None:\r
- self._sync_legacy(vc)\r
-\r
- def begin(self, vc, kind, pending_uplink=0, cancel_wait_ms=800):\r
- try:\r
- gen = int(getattr(vc, "_vc_turn_gen", 0))\r
- except Exception:\r
- gen = 0\r
- self.active = 1\r
- self.kind = kind or self.KIND_NONE\r
- self.gen = gen\r
- self.pending_uplink = 1 if pending_uplink else 0\r
- self.drop_tts = 1\r
- self.ruppt_tts = 1\r
- self.settled_ms = 0\r
- if utime and cancel_wait_ms:\r
- try:\r
- self.cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), int(cancel_wait_ms))\r
- except Exception:\r
- self.cancel_deadline_ms = 0\r
- else:\r
- self.cancel_deadline_ms = 0\r
- self._sync_legacy(vc)\r
- return gen\r
-\r
- def settle(self, vc):\r
- """打断开麦完成:清 is_ruppt/drop 以允许新 TTS;保留 settled_ms 供迟到 canceled 判 stale。"""\r
- if utime:\r
- try:\r
- self.settled_ms = utime.ticks_ms()\r
- except Exception:\r
- self.settled_ms = 0\r
- self.pending_uplink = 0\r
- self.active = 0\r
- self.drop_tts = 0\r
- self.ruppt_tts = 0\r
- self._sync_legacy(vc)\r
-\r
- def end_drop_window(self, vc):\r
- """新一轮 TTS 开始:结束丢弃窗口。"""\r
- self.drop_tts = 0\r
- self.ruppt_tts = 0\r
- self.settled_ms = 0\r
- self.pending_uplink = 0\r
- self.active = 0\r
- self._sync_legacy(vc)\r
-\r
- def is_stale_gen(self, vc):\r
- try:\r
- return int(getattr(vc, "_vc_turn_gen", 0)) != int(self.gen)\r
- except Exception:\r
- return False\r
-\r
- def _sync_legacy(self, vc):\r
- """与 vc_session_io / media 中既有 getattr 兼容。"""\r
- try:\r
- vc._vc_pending_interrupt_uplink = 1 if self.pending_uplink else 0\r
- vc.is_ruppt_tts = 1 if self.ruppt_tts else 0\r
- vc._drop_tts_audio = 1 if self.drop_tts else 0\r
- vc._long_dialogue_interrupt_settled_ms = int(self.settled_ms or 0)\r
- vc._vc_cancel_deadline_ms = int(self.cancel_deadline_ms or 0)\r
- except Exception:\r
- pass\r
-\r
- def load_legacy(self, vc):\r
- try:\r
- self.pending_uplink = 1 if getattr(vc, "_vc_pending_interrupt_uplink", 0) else 0\r
- self.ruppt_tts = 1 if getattr(vc, "is_ruppt_tts", 0) else 0\r
- self.drop_tts = 1 if getattr(vc, "_drop_tts_audio", 0) else 0\r
- self.settled_ms = int(getattr(vc, "_long_dialogue_interrupt_settled_ms", 0) or 0)\r
- self.cancel_deadline_ms = int(getattr(vc, "_vc_cancel_deadline_ms", 0) or 0)\r
- self.active = 1 if (self.pending_uplink or self.ruppt_tts or self.drop_tts) else 0\r
- except Exception:\r
- pass\r
+# -*- coding: utf-8 -*-
+"""长对话/PTT 打断上下文:绑定 turn_gen,收敛 is_ruppt_tts 等 legacy 标志。"""
+try:
+ import utime
+except Exception:
+ utime = None
+
+
+class InterruptContext(object):
+ KIND_NONE = ""
+ KIND_LONG_SHORT = "long_short"
+ KIND_PTT_SHORT = "ptt_short"
+ KIND_PTT_LONG = "ptt_long"
+
+ def __init__(self):
+ self.active = 0
+ self.kind = self.KIND_NONE
+ self.gen = 0
+ self.pending_uplink = 0
+ self.drop_tts = 0
+ self.ruppt_tts = 0
+ self.settled_ms = 0
+ self.cancel_deadline_ms = 0
+
+ def clear(self, vc=None):
+ self.active = 0
+ self.kind = self.KIND_NONE
+ self.gen = 0
+ self.pending_uplink = 0
+ self.drop_tts = 0
+ self.ruppt_tts = 0
+ self.settled_ms = 0
+ self.cancel_deadline_ms = 0
+ if vc is not None:
+ self._sync_legacy(vc)
+
+ def begin(self, vc, kind, pending_uplink=0, cancel_wait_ms=800):
+ try:
+ gen = int(getattr(vc, "_vc_turn_gen", 0))
+ except Exception:
+ gen = 0
+ self.active = 1
+ self.kind = kind or self.KIND_NONE
+ self.gen = gen
+ self.pending_uplink = 1 if pending_uplink else 0
+ self.drop_tts = 1
+ self.ruppt_tts = 1
+ self.settled_ms = 0
+ if utime and cancel_wait_ms:
+ try:
+ self.cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), int(cancel_wait_ms))
+ except Exception:
+ self.cancel_deadline_ms = 0
+ else:
+ self.cancel_deadline_ms = 0
+ self._sync_legacy(vc)
+ return gen
+
+ def settle(self, vc):
+ """打断开麦完成:清 pending;录音进行中继续丢弃上一轮迟到 TTS。"""
+ if utime:
+ try:
+ self.settled_ms = utime.ticks_ms()
+ except Exception:
+ self.settled_ms = 0
+ self.pending_uplink = 0
+ self.active = 0
+ keep_drop = False
+ try:
+ keep_drop = getattr(vc, "VC_record_state_flag", 0) != 0
+ except Exception:
+ keep_drop = False
+ if keep_drop:
+ self.drop_tts = 1
+ self.ruppt_tts = 1
+ else:
+ self.drop_tts = 0
+ self.ruppt_tts = 0
+ self._sync_legacy(vc)
+
+ def end_drop_window(self, vc):
+ """新一轮 TTS 开始:结束丢弃窗口。"""
+ self.drop_tts = 0
+ self.ruppt_tts = 0
+ self.settled_ms = 0
+ self.pending_uplink = 0
+ self.active = 0
+ self._sync_legacy(vc)
+
+ def is_stale_gen(self, vc):
+ try:
+ return int(getattr(vc, "_vc_turn_gen", 0)) != int(self.gen)
+ except Exception:
+ return False
+
+ def _sync_legacy(self, vc):
+ """与 vc_session_io / media 中既有 getattr 兼容。"""
+ try:
+ vc._vc_pending_interrupt_uplink = 1 if self.pending_uplink else 0
+ vc.is_ruppt_tts = 1 if self.ruppt_tts else 0
+ vc._drop_tts_audio = 1 if self.drop_tts else 0
+ vc._long_dialogue_interrupt_settled_ms = int(self.settled_ms or 0)
+ vc._vc_cancel_deadline_ms = int(self.cancel_deadline_ms or 0)
+ except Exception:
+ pass
+
+ def load_legacy(self, vc):
+ try:
+ self.pending_uplink = 1 if getattr(vc, "_vc_pending_interrupt_uplink", 0) else 0
+ self.ruppt_tts = 1 if getattr(vc, "is_ruppt_tts", 0) else 0
+ self.drop_tts = 1 if getattr(vc, "_drop_tts_audio", 0) else 0
+ self.settled_ms = int(getattr(vc, "_long_dialogue_interrupt_settled_ms", 0) or 0)
+ self.cancel_deadline_ms = int(getattr(vc, "_vc_cancel_deadline_ms", 0) or 0)
+ self.active = 1 if (self.pending_uplink or self.ruppt_tts or self.drop_tts) else 0
+ except Exception:
+ pass
print("[KEY][router] ptt_release ux={}".format(ux_name(ux)))
except Exception:
pass
- if ux == UX_TTS:
+ if ux in (UX_TTS, UX_THINKING):
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()
+ vc._ptt_short_press_interrupt_restart()
return True
except Exception:
pass
@staticmethod
def handle_power_short(vc):
- """Power 短按:cancel → Close → 清 sid(设计稿幻灯片 6)。"""
+ """Power 短按:停录 + cancel + Close(force) 清 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
+ if hasattr(vc, "_vc_power_short_shutdown"):
+ vc._vc_power_short_shutdown()
+ else:
+ vc._vc_send_protocol_cancel("power_short/ux={}".format(ux_name(ux)))
+ vc._vc_close_reason = "power_short"
+ if EventMesh:
+ EventMesh.publish("publish_AI_btnInfo", {"func": vc.Close})
+ except Exception as e:
+ try:
+ import usys
+ usys.print_exception(e)
+ except Exception:
+ pass
try:
if EventMesh and constants:
EventMesh.publish(constants.EVENT.SYSTEM_UP)
-# -*- coding: utf-8 -*-\r
-"""通知通道:server.started 媒体排队、play_tts 与对话会话分流。"""\r
-try:\r
- from vc_fsm import VCTurnState\r
-except Exception:\r
- VCTurnState = None\r
-try:\r
- import vc_config\r
-except Exception:\r
- vc_config = None\r
-\r
-# play_tts 通道\r
-CHANNEL_TAKEOVER = "takeover" # 断连重连 WS synthesis(IoT 查找设备/模式切换/无对话)\r
-CHANNEL_DEFER = "defer" # 对话进行中排队,空闲后播放(is_close_VC=0 的 IoT 查询等)\r
-\r
-# TTS 业务种类(_pending_tts_kind / play_tts 第三参数)\r
-TTS_KIND_FIND_DEVICE = "find_device"\r
-TTS_KIND_MODE_SWITCH_LONG = "mode_switch_long"\r
-TTS_KIND_MODE_SWITCH_PTT = "mode_switch_ptt"\r
-TTS_KIND_BOOT_PROMPT = "boot_prompt"\r
-TTS_KIND_PROMPT = "prompt"\r
-\r
-# 须立即占用会话、不可 defer\r
-_TAKEOVER_KINDS = (\r
- TTS_KIND_FIND_DEVICE,\r
- TTS_KIND_MODE_SWITCH_LONG,\r
- TTS_KIND_MODE_SWITCH_PTT,\r
- TTS_KIND_BOOT_PROMPT,\r
- TTS_KIND_PROMPT,\r
-)\r
-\r
-# server.started plan op\r
-OP_LONG_UPLINK = "long_uplink"\r
-OP_ALARM_VOICE = "alarm_voice"\r
-OP_VOICE_URL = "voice_url"\r
-OP_SYNTHESIS = "synthesis"\r
-OP_SYNTHESIS_HIS_TIMER = "synthesis_his_timer"\r
-OP_SYNTHESIS_WAIT_TIMER = "synthesis_wait_timer"\r
-OP_SYNTHESIS_WAIT_SINGLE = "synthesis_wait_single"\r
-OP_SYNTHESIS_VOICE_TIMER = "synthesis_voice_timer"\r
-OP_SYNTHESIS_MUSIC_TIMER = "synthesis_music_timer"\r
-OP_IDLE_UPLINK = "idle_uplink"\r
-OP_PTT_ROUND = "ptt_round"\r
-OP_UPLOAD_TONE = "upload_tone"\r
-\r
-\r
-def dialog_session_active(vc):\r
- """对话占用中:通知类 play_tts 应 defer。"""\r
- try:\r
- if getattr(vc, "Close_flag", 0) == 1:\r
- return False\r
- if getattr(vc, "VC_satrt_flag", 0) != 1:\r
- return False\r
- if getattr(vc, "AI_client_VC_isconnect", 0) != 1:\r
- return False\r
- if getattr(vc, "VC_Play_flag", 0) == 1:\r
- return True\r
- if getattr(vc, "VC_record_state_flag", 0) != 0:\r
- return True\r
- # 7PIN_1_5 无闹钟业务;alarm_play_flag 仅 legacy(如 play_single_voice)\r
- if getattr(vc, "music_play_flag", 0) == 1:\r
- return True\r
- if VCTurnState is not None and getattr(vc, "fsm", None) is not None:\r
- t = getattr(vc.fsm, "turn", VCTurnState.Idle)\r
- if t not in (VCTurnState.Idle, VCTurnState.Listening):\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
-\r
-class NotifyScheduler(object):\r
- @staticmethod\r
- def classify_play_tts(vc, is_close_vc):\r
- try:\r
- if is_close_vc:\r
- return CHANNEL_TAKEOVER\r
- except Exception:\r
- pass\r
- try:\r
- k = getattr(vc, "_pending_tts_kind", "") or getattr(vc, "_last_tts_kind", "") or ""\r
- if k in _TAKEOVER_KINDS:\r
- return CHANNEL_TAKEOVER\r
- except Exception:\r
- pass\r
- if dialog_session_active(vc):\r
- return CHANNEL_DEFER\r
- return CHANNEL_TAKEOVER\r
-\r
- @staticmethod\r
- def plan_on_started(vc, skip_long_uplink=0):\r
- """替代 vc_session_io started 内 if-elif 媒体分支。"""\r
- plan = []\r
- try:\r
- import EventMesh\r
- except Exception:\r
- EventMesh = None\r
-\r
- # 长对话:started 补开麦(原 597-623,与媒体队列正交)\r
- try:\r
- if (\r
- not skip_long_uplink\r
- and getattr(vc, "VC_mode", 0) == 0\r
- and getattr(vc, "Close_flag", 0) == 0\r
- and not getattr(vc, "_vc_boot_standby", 0)\r
- ):\r
- plan.append({"op": OP_LONG_UPLINK})\r
- except Exception:\r
- pass\r
-\r
- tts = getattr(vc, "AI_TTS_str", "") or ""\r
- url = getattr(vc, "AI_new_voice_url", "") or ""\r
- alarm = getattr(vc, "alarm_play_flag", 0) == 1\r
- music = getattr(vc, "music_play_flag", 0) == 1\r
-\r
- # 闹钟铃声(其它机型);7PIN_1_5 无闹钟,alarm_play_flag 通常为 0\r
- if url and alarm:\r
- plan.append({"op": OP_ALARM_VOICE})\r
- elif not tts and url and not music:\r
- plan.append({"op": OP_VOICE_URL})\r
- elif tts and not url:\r
- plan.append({"op": OP_SYNTHESIS})\r
- elif tts and url and not music:\r
- try:\r
- his = EventMesh.publish("get_history_voice_flag") == 1 if EventMesh else False\r
- except Exception:\r
- his = False\r
- if his:\r
- try:\r
- wait = EventMesh.publish("get_wait_voice_flag") == 0 if EventMesh else True\r
- except Exception:\r
- wait = True\r
- if wait:\r
- plan.append({"op": OP_SYNTHESIS_HIS_TIMER})\r
- else:\r
- try:\r
- notice = getattr(vc, "pwk_notice_flag", 0) == 0\r
- except Exception:\r
- notice = True\r
- if notice:\r
- plan.append({"op": OP_SYNTHESIS_WAIT_TIMER})\r
- else:\r
- plan.append({"op": OP_SYNTHESIS_WAIT_SINGLE})\r
- else:\r
- plan.append({"op": OP_SYNTHESIS_VOICE_TIMER})\r
- elif tts and url and music:\r
- plan.append({"op": OP_SYNTHESIS_MUSIC_TIMER})\r
- else:\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool(\r
- "UPLOAD_START_TONE_ENABLED", False\r
- ):\r
- plan.append({"op": OP_UPLOAD_TONE})\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(vc, "VC_mode", 0) == 0:\r
- plan.append({"op": OP_IDLE_UPLINK})\r
- else:\r
- plan.append({"op": OP_PTT_ROUND})\r
- except Exception:\r
- plan.append({"op": OP_IDLE_UPLINK})\r
- return plan\r
+# -*- coding: utf-8 -*-
+"""通知通道:server.started 媒体排队、play_tts 与对话会话分流。"""
+try:
+ from vc_fsm import VCTurnState
+except Exception:
+ VCTurnState = None
+try:
+ import vc_config
+except Exception:
+ vc_config = None
+
+# play_tts 通道
+CHANNEL_TAKEOVER = "takeover" # 断连重连 WS synthesis(IoT 查找设备/模式切换/无对话)
+CHANNEL_DEFER = "defer" # 对话进行中排队,空闲后播放(is_close_VC=0 的 IoT 查询等)
+
+# TTS 业务种类(_pending_tts_kind / play_tts 第三参数)
+TTS_KIND_FIND_DEVICE = "find_device"
+TTS_KIND_MODE_SWITCH_LONG = "mode_switch_long"
+TTS_KIND_MODE_SWITCH_PTT = "mode_switch_ptt"
+TTS_KIND_BOOT_PROMPT = "boot_prompt"
+TTS_KIND_PROMPT = "prompt"
+
+# 须立即占用会话、不可 defer
+_TAKEOVER_KINDS = (
+ TTS_KIND_FIND_DEVICE,
+ TTS_KIND_MODE_SWITCH_LONG,
+ TTS_KIND_MODE_SWITCH_PTT,
+ TTS_KIND_BOOT_PROMPT,
+ TTS_KIND_PROMPT,
+)
+
+# server.started plan op
+OP_LONG_UPLINK = "long_uplink"
+OP_ALARM_VOICE = "alarm_voice"
+OP_VOICE_URL = "voice_url"
+OP_SYNTHESIS = "synthesis"
+OP_SYNTHESIS_HIS_TIMER = "synthesis_his_timer"
+OP_SYNTHESIS_WAIT_TIMER = "synthesis_wait_timer"
+OP_SYNTHESIS_WAIT_SINGLE = "synthesis_wait_single"
+OP_SYNTHESIS_VOICE_TIMER = "synthesis_voice_timer"
+OP_SYNTHESIS_MUSIC_TIMER = "synthesis_music_timer"
+OP_IDLE_UPLINK = "idle_uplink"
+OP_PTT_ROUND = "ptt_round"
+OP_UPLOAD_TONE = "upload_tone"
+
+
+def dialog_session_active(vc):
+ """对话占用中:通知类 play_tts 应 defer。"""
+ try:
+ if getattr(vc, "Close_flag", 0) == 1:
+ return False
+ if getattr(vc, "VC_satrt_flag", 0) != 1:
+ return False
+ if getattr(vc, "AI_client_VC_isconnect", 0) != 1:
+ return False
+ if getattr(vc, "VC_Play_flag", 0) == 1:
+ return True
+ if getattr(vc, "VC_record_state_flag", 0) != 0:
+ return True
+ # 7PIN_1_5 无闹钟业务;alarm_play_flag 仅 legacy(如 play_single_voice)
+ if getattr(vc, "music_play_flag", 0) == 1:
+ return True
+ if VCTurnState is not None and getattr(vc, "fsm", None) is not None:
+ t = getattr(vc.fsm, "turn", VCTurnState.Idle)
+ if t not in (VCTurnState.Idle, VCTurnState.Listening):
+ return True
+ except Exception:
+ pass
+ return False
+
+
+class NotifyScheduler(object):
+ @staticmethod
+ def classify_play_tts(vc, is_close_vc):
+ try:
+ if is_close_vc:
+ return CHANNEL_TAKEOVER
+ except Exception:
+ pass
+ try:
+ k = getattr(vc, "_pending_tts_kind", "") or getattr(vc, "_last_tts_kind", "") or ""
+ if k in _TAKEOVER_KINDS:
+ return CHANNEL_TAKEOVER
+ except Exception:
+ pass
+ if dialog_session_active(vc):
+ return CHANNEL_DEFER
+ return CHANNEL_TAKEOVER
+
+ @staticmethod
+ def plan_on_started(vc, skip_long_uplink=0):
+ """替代 vc_session_io started 内 if-elif 媒体分支。"""
+ plan = []
+ try:
+ import EventMesh
+ except Exception:
+ EventMesh = None
+
+ tts = getattr(vc, "AI_TTS_str", "") or ""
+ # 模式切换提示音播放期间才抑制 started 自动开麦;Close 后 kind 残留不得挡 PTT 长按
+ ms_hint_only = False
+ try:
+ k0 = getattr(vc, "_pending_tts_kind", "") or getattr(vc, "_last_tts_kind", "") or ""
+ if k0 in (TTS_KIND_MODE_SWITCH_LONG, TTS_KIND_MODE_SWITCH_PTT):
+ if getattr(vc, "Close_flag", 0) == 1:
+ ms_hint_only = True
+ else:
+ ph = int(getattr(vc, "_vc_ms_phase", 0) or 0)
+ if ph in (1, 2, 3):
+ ms_hint_only = True
+ except Exception:
+ ms_hint_only = False
+
+ # 长对话:started 补开麦(模式切换提示音播完前勿自动开录)
+ try:
+ if (
+ not skip_long_uplink
+ and not ms_hint_only
+ and getattr(vc, "VC_mode", 0) == 0
+ and getattr(vc, "Close_flag", 0) == 0
+ and not getattr(vc, "_vc_boot_standby", 0)
+ ):
+ plan.append({"op": OP_LONG_UPLINK})
+ except Exception:
+ pass
+ url = getattr(vc, "AI_new_voice_url", "") or ""
+ alarm = getattr(vc, "alarm_play_flag", 0) == 1
+ music = getattr(vc, "music_play_flag", 0) == 1
+
+ # 闹钟铃声(其它机型);7PIN_1_5 无闹钟,alarm_play_flag 通常为 0
+ if url and alarm:
+ plan.append({"op": OP_ALARM_VOICE})
+ elif not tts and url and not music:
+ plan.append({"op": OP_VOICE_URL})
+ elif tts and not url:
+ plan.append({"op": OP_SYNTHESIS})
+ elif tts and url and not music:
+ try:
+ his = EventMesh.publish("get_history_voice_flag") == 1 if EventMesh else False
+ except Exception:
+ his = False
+ if his:
+ try:
+ wait = EventMesh.publish("get_wait_voice_flag") == 0 if EventMesh else True
+ except Exception:
+ wait = True
+ if wait:
+ plan.append({"op": OP_SYNTHESIS_HIS_TIMER})
+ else:
+ try:
+ notice = getattr(vc, "pwk_notice_flag", 0) == 0
+ except Exception:
+ notice = True
+ if notice:
+ plan.append({"op": OP_SYNTHESIS_WAIT_TIMER})
+ else:
+ plan.append({"op": OP_SYNTHESIS_WAIT_SINGLE})
+ else:
+ plan.append({"op": OP_SYNTHESIS_VOICE_TIMER})
+ elif tts and url and music:
+ plan.append({"op": OP_SYNTHESIS_MUSIC_TIMER})
+ else:
+ try:
+ if "vc_config" in globals() and vc_config and vc_config.get_bool(
+ "UPLOAD_START_TONE_ENABLED", False
+ ):
+ plan.append({"op": OP_UPLOAD_TONE})
+ except Exception:
+ pass
+ try:
+ if ms_hint_only:
+ pass
+ elif getattr(vc, "VC_mode", 0) == 0:
+ plan.append({"op": OP_IDLE_UPLINK})
+ else:
+ plan.append({"op": OP_PTT_ROUND})
+ except Exception:
+ if not ms_hint_only:
+ plan.append({"op": OP_IDLE_UPLINK})
+ return plan
"""返回传给 set_led_state 的 dict;不发布。"""\r
if present is None:\r
present = build_present_state(vc)\r
- if present.get("override_ai"):\r
- msg = {"ai_state": present["override_ai"]}\r
- if present.get("override_prefer_off") or present.get("prefer_off"):\r
- msg["prefer_off"] = 1\r
- return msg\r
if present.get("force_idle"):\r
msg = {"ai_state": "idle"}\r
if present.get("prefer_off"):\r
msg["prefer_off"] = 1\r
return msg\r
+ # 录音/开麦锁:优先于陈旧 thinking override(单对话长按须保持蓝快闪)\r
if present.get("hold_listening"):\r
return {"ai_state": "listening"}\r
+ if present.get("override_ai"):\r
+ msg = {"ai_state": present["override_ai"]}\r
+ if present.get("override_prefer_off") or present.get("prefer_off"):\r
+ msg["prefer_off"] = 1\r
+ return msg\r
if present.get("ptt_await_server") and int(present.get("mode", 0)) == 1:\r
return {"ai_state": "thinking"}\r
if present.get("stream_active"):\r
)
from vc_mode_strategy import strategy_for_mode
+# 长对话轮次守卫(内嵌,勿拆独立 .py,避免烧录漏文件导致 ai_VC 导入失败、无 token)
+_POST_INTERRUPT_DROP_EVENTS = (
+ "vad.silence", "vad.active", "asr.result", "asr.completed",
+ "llm.created", "llm.completed", "llm.done", "llm.delta", "completed",
+)
+# 录音中 key.restart:仅 cancel→滴声→开麦 窗口内丢弃(勿用 post_interrupt,否则会误杀新一句 ASR)
+_RECORD_RESTART_STALE_DROP_EVENTS = (
+ "vad.silence", "vad.active", "asr.started", "asr.result", "asr.completed",
+ "llm.created", "llm.completed", "llm.done", "llm.delta",
+ "tts.started", "tts.completed", "completed", "canceled",
+)
+
+# 单对话 PTT 轮次:0=idle 1=arming(等 started)
+PTT_ROUND_IDLE = 0
+PTT_ROUND_ARMING = 1
+
+
+def _vc_guard_cfg_int(key, default):
+ try:
+ if vc_config:
+ return int(vc_config.get_int(key, default))
+ except Exception:
+ pass
+ return default
+
+
+def _vc_guard_cfg_bool(key, default):
+ try:
+ if vc_config:
+ return vc_config.get_bool(key, default)
+ except Exception:
+ pass
+ return default
+
+
+class VCTurnGuard(object):
+ def __init__(self, vc):
+ self.vc = vc
+
+ def late_tts_wait_ms(self):
+ return _vc_guard_cfg_int("LLM_LATE_TTS_WAIT_MS", 3500)
+
+ def interrupt_recover_ms(self):
+ return _vc_guard_cfg_int("INTERRUPT_ASR_RECOVER_MS", 8000)
+
+ def vad_silence_grace_ms(self):
+ return _vc_guard_cfg_int("INTERRUPT_VAD_SILENCE_GRACE_MS", 3000)
+
+ def min_uplink_ms(self):
+ return _vc_guard_cfg_int("INTERRUPT_ASR_MIN_UPLINK_MS", 2000)
+
+ def _post_interrupt_active(self):
+ v = self.vc
+ return (
+ getattr(v, "_vc_post_interrupt_until_asr", 0) == 1
+ or getattr(v, "_vc_suppress_stale_server_turn", 0) == 1
+ or getattr(v, "_vc_stale_tts_until_asr_started", 0) == 1
+ )
+
+ def enter_post_interrupt(self):
+ v = self.vc
+ v._vc_post_interrupt_until_asr = 1
+ v._vc_suppress_stale_server_turn = 1
+ v._vc_stale_tts_until_asr_started = 1
+
+ def clear_post_interrupt(self):
+ v = self.vc
+ v._vc_post_interrupt_until_asr = 0
+ v._vc_suppress_stale_server_turn = 0
+ v._vc_stale_tts_until_asr_started = 0
+
+ def _record_restart_stale_drop_ms(self):
+ return _vc_guard_cfg_int("RECORD_RESTART_STALE_DROP_MS", 1500)
+
+ def arm_record_restart_stale_drop(self):
+ v = self.vc
+ try:
+ v._vc_record_restart_drop_until_ms = utime.ticks_add(
+ utime.ticks_ms(), self._record_restart_stale_drop_ms())
+ print("[VC][guard] record_restart stale_drop_ms={}".format(
+ self._record_restart_stale_drop_ms()))
+ except Exception:
+ v._vc_record_restart_drop_until_ms = 0
+
+ def clear_record_restart_stale_drop(self, tag=""):
+ v = self.vc
+ if not getattr(v, "_vc_record_restart_drop_until_ms", 0):
+ return
+ v._vc_record_restart_drop_until_ms = 0
+ if tag:
+ try:
+ print("[VC][guard] record_restart stale_drop clear ({})".format(tag))
+ except Exception:
+ pass
+
+ def record_restart_stale_drop_active(self):
+ until = int(getattr(self.vc, "_vc_record_restart_drop_until_ms", 0) or 0)
+ if not until:
+ return False
+ try:
+ if utime.ticks_diff(until, utime.ticks_ms()) > 0:
+ return True
+ except Exception:
+ pass
+ self.clear_record_restart_stale_drop("expired")
+ return False
+
+ def should_drop_record_restart_stale(self, ev):
+ if not self.record_restart_stale_drop_active():
+ return False
+ return ev in _RECORD_RESTART_STALE_DROP_EVENTS
+
+ def should_drop_server_event(self, ev):
+ if self.should_drop_record_restart_stale(ev):
+ return True
+ if self._post_interrupt_active() and ev in _POST_INTERRUPT_DROP_EVENTS:
+ return True
+ if ev == "vad.silence" and self.should_ignore_vad_silence():
+ return True
+ return False
+
+ def should_stale_tts(self):
+ if not self._is_long():
+ return False
+ v = self.vc
+ if self._post_interrupt_active():
+ return True
+ if getattr(v, "_vc_pending_interrupt_uplink", 0) == 1:
+ return True
+ return False
+
+ def should_drop_inbound_tts_pcm(self):
+ v = self.vc
+ if not self._is_long():
+ return False
+ if self._post_interrupt_active():
+ return True
+ if getattr(v, "_drop_tts_audio", 0) == 1:
+ return True
+ if getattr(v, "_vc_pending_interrupt_uplink", 0) == 1:
+ return True
+ if getattr(v, "is_ruppt_tts", 0) == 1:
+ return True
+ settled = getattr(v, "_long_dialogue_interrupt_settled_ms", 0)
+ if settled:
+ try:
+ win = _vc_guard_cfg_int("LONG_DIALOGUE_DROP_TTS_MS", 8000)
+ if utime.ticks_diff(utime.ticks_ms(), settled) < win:
+ return True
+ except Exception:
+ pass
+ return False
+
+ def arm_interrupt_recover(self, tag=""):
+ v = self.vc
+ now = utime.ticks_ms()
+ v._vc_interrupt_recover_until_ms = utime.ticks_add(
+ now, self.interrupt_recover_ms())
+ v._vc_ignore_vad_silence_until_ms = utime.ticks_add(
+ now, self.vad_silence_grace_ms())
+ v._vc_interrupt_asr_min_uplink_ms = self.min_uplink_ms()
+ def in_interrupt_recover(self):
+ until = int(getattr(self.vc, "_vc_interrupt_recover_until_ms", 0) or 0)
+ return bool(until and utime.ticks_diff(until, utime.ticks_ms()) > 0)
+
+ def should_ignore_vad_silence(self):
+ until = int(getattr(self.vc, "_vc_ignore_vad_silence_until_ms", 0) or 0)
+ return bool(until and utime.ticks_diff(until, utime.ticks_ms()) > 0)
+
+ def should_play_record_start_tone(self, reason):
+ r = str(reason or "")
+ if r.startswith("asr.empty"):
+ return False
+ # 用户打断 TTS 后 cancel 开麦:播 start.mp3(与 key.press 一致);迟到 canceled 不播
+ if r == "canceled" or r.startswith("canceled/"):
+ if getattr(self.vc, "_vc_interrupt_record_start_tone", 0) == 1:
+ return True
+ if getattr(self.vc, "_vc_pending_interrupt_uplink", 0) == 1:
+ return True
+ return False
+ if self.vc._vc_uplink_reason_is_user_key(reason):
+ return True
+ if r.startswith("server.started") and getattr(self.vc, "_long_dialogue_key_arm", 0) == 1:
+ return True
+ return False
+
+ def asr_empty_use_soft_recover(self):
+ if not self.in_interrupt_recover():
+ return False
+ v = self.vc
+ min_ms = int(getattr(v, "_vc_interrupt_asr_min_uplink_ms", 0) or self.min_uplink_ms())
+ dur = int(getattr(v, "_vc_last_uplink_dur_ms", -1))
+ if dur >= 0 and dur < min_ms:
+ return True
+ if getattr(v, "_turn_has_asr_result", 0) != 1:
+ return True
+ return False
+
+ def clear_interrupt_recover(self):
+ v = self.vc
+ v._vc_interrupt_recover_until_ms = 0
+ v._vc_ignore_vad_silence_until_ms = 0
+
+ def arm_await_late_tts(self, reason="completed"):
+ self.vc._vc_await_late_tts_until_ms = utime.ticks_add(
+ utime.ticks_ms(), self.late_tts_wait_ms())
+ try:
+ print("[VC][guard] late_tts arm reason={} wait_ms={}".format(
+ reason, self.late_tts_wait_ms()))
+ except Exception:
+ pass
+
+ def awaiting_late_tts(self, now_ms=None):
+ until = int(getattr(self.vc, "_vc_await_late_tts_until_ms", 0) or 0)
+ if not until:
+ return False
+ if now_ms is None:
+ now_ms = utime.ticks_ms()
+ return utime.ticks_diff(until, now_ms) > 0
+
+ def clear_late_tts(self, tag=""):
+ v = self.vc
+ v._vc_await_late_tts_until_ms = 0
+ v._vc_llm_without_tts_round = 0
+ if tag:
+ try:
+ print("[VC][guard] late_tts clear {}".format(tag))
+ except Exception:
+ pass
+
+ def on_llm_without_tts_completed(self):
+ self.vc._vc_llm_without_tts_round = 1
+ if not self.try_llm_synthesis_fallback("completed/llm_without_tts"):
+ self.arm_await_late_tts("llm_without_tts")
+
+ def try_llm_synthesis_fallback(self, reason="completed"):
+ v = self.vc
+ if not self._is_long():
+ return False
+ if getattr(v, "_turn_has_tts_started", 0) == 1:
+ return False
+ if getattr(v, "_tts_pending", 0) == 1:
+ return False
+ if not _vc_guard_cfg_bool("LLM_WITHOUT_TTS_SYNTHESIS_FALLBACK", True):
+ return False
+ now = utime.ticks_ms()
+ last = getattr(v, "_vc_llm_synth_fallback_ms", 0) or 0
+ if last and utime.ticks_diff(now, last) < 3000:
+ return False
+ text = (getattr(v, "_llm_reply_text", "") or "").strip()
+ if not text:
+ self.arm_await_late_tts(reason)
+ return False
+ v._vc_llm_synth_fallback_ms = now
+ v._vc_llm_without_tts_round = 0
+ try:
+ print("[VC][guard] llm_fallback synthesis len={} reason={}".format(
+ len(text), reason))
+ except Exception:
+ pass
+ v.AI_TTS_str = text
+ v._pending_tts_kind = "dialog"
+ v._last_tts_kind = "dialog"
+ v.request_send_synthesis(text)
+ return True
+
+ def late_tts_wait_tick(self, now_ms):
+ until = int(getattr(self.vc, "_vc_await_late_tts_until_ms", 0) or 0)
+ if not until:
+ return
+ if utime.ticks_diff(now_ms, until) < 0:
+ return
+ self.vc._vc_await_late_tts_until_ms = 0
+ if getattr(self.vc, "_turn_has_tts_started", 0) == 1:
+ return
+ if getattr(self.vc, "_tts_pending", 0) == 1:
+ return
+ if getattr(self.vc, "_turn_has_asr_result", 0) == 1:
+ return
+ try:
+ if hasattr(self.vc, "_vc_effective_asr_text") and self.vc._vc_effective_asr_text():
+ return
+ except Exception:
+ pass
+ hint = "我没有听清,请再说一遍"
+ try:
+ if vc_config:
+ hint = str(vc_config.get_attr("ASR_EMPTY_HINT_TTS", hint) or hint).strip()
+ except Exception:
+ pass
+ if not hint:
+ return
+ try:
+ print("[VC][guard] late_tts timeout -> synthesis hint")
+ except Exception:
+ pass
+ v = self.vc
+ v._pending_tts_kind = "asr_empty_hint"
+ v._last_tts_kind = "asr_empty_hint"
+ v.request_send_synthesis(hint)
+
+ def reset_turn_flags(self, reason=""):
+ v = self.vc
+ v._turn_has_asr_completed = 0
+ v._turn_has_vad_silence = 0
+ v._turn_has_tts_started = 0
+ v._turn_has_asr_result = 0
+ v._llm_completed_ms = 0
+ v._llm_reply_text = ""
+ v._vc_defer_vad_uplink_stop = 0
+ v._server_vad_silence = 0
+ v._vc_vad_had_active_after_silence = 0
+ v._vc_llm_without_tts_round = 0
+ v._vc_await_late_tts_until_ms = 0
+ self.clear_interrupt_recover()
+ v._asr_completed_tone_done = 0
+ try:
+ print("[VC][guard] reset_turn_flags reason={}".format(reason))
+ except Exception:
+ pass
+
+ def on_long_interrupt(self):
+ self.reset_turn_flags("long_interrupt")
+ self.enter_post_interrupt()
+ self.arm_interrupt_recover("long_interrupt")
+ try:
+ self.vc._vc_interrupt_record_start_tone = 1
+ self.vc._vc_interrupt_cancel_uplink_armed = 0
+ self.vc._vc_defer_record_for_tone = 0
+ except Exception:
+ pass
+
+ def on_record_restart(self):
+ """录音中重录:turn 已 bump;短窗口丢 cancel 前迟到下行;开麦后正常收 ASR。"""
+ v = self.vc
+ self.reset_turn_flags("key.restart")
+ self.arm_record_restart_stale_drop()
+ self.arm_interrupt_recover("key.restart")
+ self.purge_stale_tts("key.restart")
+ try:
+ v.AI_ASR_str = ""
+ v.AI_ASR_str_tmp = ""
+ v._last_asr_result_text = ""
+ except Exception:
+ pass
+
+ def should_preserve_tts_drop_window(self):
+ if self._post_interrupt_active():
+ return True
+ v = self.vc
+ if getattr(v, "_vc_pending_interrupt_uplink", 0) == 1:
+ return True
+ if getattr(v, "is_ruppt_tts", 0) == 1:
+ return True
+ if getattr(v, "_drop_tts_audio", 0) == 1:
+ return True
+ return False
+
+ def on_asr_started(self):
+ self.clear_post_interrupt()
+ self.clear_interrupt_recover()
+
+ def on_asr_result(self, text):
+ if (text or "").strip():
+ self.clear_interrupt_recover()
+
+ def purge_stale_tts(self, tag=""):
+ v = self.vc
+ try:
+ v._drop_tts_audio = 1
+ q = getattr(v, "AI_audio_queue", None)
+ if q is not None and q.empty() != True:
+ q.clear()
+ if getattr(v, "VC_Play_flag", 0) == 1 or getattr(v, "play_pcm", None) is not None:
+ try:
+ v.VC_Play_End()
+ except Exception:
+ pass
+ if getattr(v, "play_pcm", None) is not None:
+ try:
+ v.play_pcm.stream_stop()
+ v.play_pcm.close()
+ except Exception:
+ pass
+ v.play_pcm = None
+ v.fsm.await_played = False
+ v._need_audio_played_ack = 0
+ try:
+ v.fsm.mark_tts_rx_idle()
+ if getattr(v.fsm, "session", None) == VCSessionState.Started:
+ v.fsm.turn = VCTurnState.Listening
+ except Exception:
+ pass
+ v._turn_has_tts_started = 0
+ try:
+ print("[VC][guard] purge_stale_tts ({})".format(tag))
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ def _is_long(self):
+ try:
+ return getattr(self.vc, "VC_mode", -1) == 0
+ except Exception:
+ return False
+
+
class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
def _vc_fsm_log_snip(self):
try:
except Exception:
return False
+ def _ptt_in_interrupt_wait(self):
+ if not self._vc_is_ptt_mode():
+ return False
+ try:
+ return (
+ getattr(self, "_ptt_interrupt_active", 0) == 1
+ or getattr(self, "_vc_serial_canceling", 0) == 1
+ or getattr(self, "_ptt_pending_arm", 0) == 1
+ )
+ except Exception:
+ return False
+
+ def _ptt_release_interrupt_settle(self):
+ """TTS 打断长按后松手且未开录:停播、单对话待机,不 Close WS。"""
+ try:
+ self.AI_key_long_press_flag = 0
+ self._ptt_force_record_on_started = 0
+ self._ptt_round_phase = PTT_ROUND_IDLE
+ self._ptt_skip_arm_on_cancel = 1
+ self._ptt_pending_arm = 0
+ self._need_audio_played_ack = 0
+ try:
+ self.fsm.await_played = False
+ except Exception:
+ pass
+ self._ptt_hard_mute_local_playback()
+ self._ptt_led_lock_listening = 0
+ self._vc_play_end_suppress_idle_led = 0
+ self._ptt_idle_latch = 1
+ self._vc_publish_led("idle", "ptt_intr_rel", prefer_off=1)
+ except Exception:
+ pass
+
+ def _ptt_round_should_arm_on_started(self):
+ if not self._vc_is_ptt_mode():
+ return False
+ try:
+ if getattr(self, "AI_key_long_press_flag", 0) != 1:
+ return False
+ if getattr(self, "_ptt_force_record_on_started", 0) != 1:
+ return False
+ ph = int(getattr(self, "_ptt_round_phase", PTT_ROUND_IDLE))
+ if ph == PTT_ROUND_ARMING:
+ return True
+ if ph == PTT_ROUND_IDLE:
+ self._ptt_round_phase = PTT_ROUND_ARMING
+ return True
+ return False
+ except Exception:
+ return False
+
+ def _ptt_round_abort(self, reason="", close_session=False, publish_idle=True):
+ if not self._vc_is_ptt_mode():
+ return
+ try:
+ self._ptt_round_phase = PTT_ROUND_IDLE
+ self.AI_key_long_press_flag = 0
+ self._ptt_force_record_on_started = 0
+ self._ptt_defer_speech_until_asr = 0
+ self._ptt_audio_created = 0
+ self._ptt_record_open_ms = 0
+ self._ptt_pending_arm = 0
+ self._ptt_short_press_stop = 0
+ self._ptt_abort_interrupt_arm()
+ self._ptt_idle_latch = 0
+ self._ptt_await_server = 0
+ except Exception:
+ pass
+ if publish_idle:
+ try:
+ self._vc_publish_led("idle", "ptt_abort")
+ except Exception:
+ pass
+ if close_session and getattr(self, "VC_record_state_flag", 0) == 0:
+ try:
+ if getattr(self, "VC_satrt_flag", 0) == 1 or getattr(
+ self, "AI_client_VC_isconnect", 0
+ ) == 1:
+ self._vc_enqueue_btn(self._ptt_abort_close_btn, priority=1)
+ except Exception:
+ pass
+
+ def _ptt_abort_close_btn(self, event=None, msg=None):
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ return
+ try:
+ self._vc_close_reason = "ptt_abort"
+ self.Close()
+ except Exception:
+ pass
+
def _vc_reset_dialogue_mode_state(self):
"""Close/模式切换时清零双模式按键与灯锁,避免残留互相影响。"""
try:
self._ptt_force_record_on_started = 0
self._ptt_await_server = 0
self._ptt_audio_created = 0
+ self._ptt_round_phase = PTT_ROUND_IDLE
+ self._ptt_skip_arm_on_cancel = 0
except Exception:
pass
try:
self._led_override = None
except Exception:
pass
+ try:
+ self._last_tts_kind = ""
+ self._pending_tts_kind = ""
+ except Exception:
+ pass
try:
self._drop_tts_audio = 0
self.is_ruppt_tts = 0
if not self._vc_is_long_dialogue_mode():
return True
try:
+ r = str(reason or "")
+ if r.startswith("asr.empty/recover"):
+ return True
if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
return True
# TTS 播完进入下一轮:play_end 时 PCM 尚未 close,勿用 pipeline_busy 拒开麦
return False
def _vc_flush_deferred_vad_uplink_stop(self, reason="asr.completed"):
- """长对话:asr.completed 再停麦,避免句中 vad.silence 把一句话切成两段。"""
+ """长对话:vad.silence 已 silence_gate 止 PCM;asr.completed 再关麦/停录线程。"""
try:
- if not getattr(self, "_vc_defer_vad_uplink_stop", 0):
+ had_defer = getattr(self, "_vc_defer_vad_uplink_stop", 0) == 1
+ if had_defer:
+ self._vc_defer_vad_uplink_stop = 0
+ if getattr(self, "_server_vad_silence", 0) != 1:
+ self._server_vad_silence = 1
+ if getattr(self, "VC_record_state_flag", 0) == 0:
return
- self._vc_defer_vad_uplink_stop = 0
- self._server_vad_silence = 1
- if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible():
- self.request_uplink_stop(reason)
+ if not self._vc_vad_uplink_stop_eligible():
+ return
+ if had_defer or self._vc_is_long_dialogue_mode():
+ # 编排线程内同步停麦;勿 request_uplink_stop 入队(与 llm.created/canceled 竞态)
+ self._uplink_request_stop(reason)
+ except Exception:
+ pass
+
+ def _vc_capture_llm_reply_text(self, jsload_data, append=False):
+ try:
+ payload = jsload_data.get("payload", {}) if jsload_data else {}
+ c = (payload.get("content") or payload.get("text") or "").strip()
+ if not c:
+ return
+ if append:
+ self._llm_reply_text = (getattr(self, "_llm_reply_text", "") or "") + c
+ else:
+ self._llm_reply_text = c
except Exception:
pass
+ def _vc_try_llm_reply_synthesis_fallback(self, reason="completed"):
+ return self._turn_guard.try_llm_synthesis_fallback(reason)
+
+ def _vc_arm_await_late_tts(self, reason="completed"):
+ self._turn_guard.arm_await_late_tts(reason)
+
+ def _vc_awaiting_late_tts(self, now_ms=None):
+ return self._turn_guard.awaiting_late_tts(now_ms)
+
+ def _vc_clear_await_late_tts(self, tag=""):
+ self._turn_guard.clear_late_tts(tag)
+
+ def _vc_arm_interrupt_asr_recover(self, tag=""):
+ self._turn_guard.arm_interrupt_recover(tag)
+
+ def _vc_in_interrupt_asr_recover(self):
+ return self._turn_guard.in_interrupt_recover()
+
+ def _vc_should_ignore_vad_silence(self):
+ return self._turn_guard.should_ignore_vad_silence()
+
+ def _vc_asr_empty_soft_recover(self):
+ return self._turn_guard.asr_empty_use_soft_recover()
+
+ def _vc_should_drop_record_restart_stale(self, ev):
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ return g.should_drop_record_restart_stale(ev)
+ return False
+
+ def _vc_late_tts_wait_tick(self, now_ms):
+ self._turn_guard.late_tts_wait_tick(now_ms)
+
def _long_dialogue_media_busy(self):
"""长对话:TTS/思考/播报中(按键须走打断,勿误走 continue)。"""
try:
pass
return 8000
+ def _long_dialogue_is_stale_server_tts(self):
+ return self._turn_guard.should_stale_tts()
+
+ def _vc_purge_stale_tts_round(self, tag=""):
+ self._turn_guard.purge_stale_tts(tag)
+
def _long_dialogue_should_drop_inbound_tts(self):
- """长对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""
- if not self._vc_is_long_dialogue_mode():
- return False
- try:
- if getattr(self, "_drop_tts_audio", 0) == 1:
- return True
- if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
- return True
- if getattr(self, "is_ruppt_tts", 0) == 1:
- return True
- settled = getattr(self, "_long_dialogue_interrupt_settled_ms", 0)
- if settled and utime.ticks_diff(utime.ticks_ms(), settled) < self._long_dialogue_drop_tts_window_ms():
- return True
- except Exception:
- pass
- return False
+ return self._turn_guard.should_drop_inbound_tts_pcm()
def _long_dialogue_on_server_tts_started(self):
"""新一轮服务端 TTS:结束打断丢弃窗口,允许 VC_Play_Start 与下行写入。"""
if not self._vc_is_long_dialogue_mode():
return
try:
+ self._vc_clear_await_late_tts("")
+ except Exception:
+ pass
+ try:
+ if hasattr(self, "_long_dialogue_is_stale_server_tts") and self._long_dialogue_is_stale_server_tts():
+ try:
+ print("[VC][long] tts.started stale (recording/interrupt) -> ignore")
+ except Exception:
+ pass
+ return
if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
return
if getattr(self, "_intr", None) is not None:
except Exception:
pass
+ def _ptt_is_stale_server_cancel(self):
+ """PTT 真开录后迟到的 canceled(上一轮 cancel/提示音 TTS 回声),勿停录。"""
+ if not self._vc_is_ptt_mode():
+ return False
+ try:
+ if getattr(self, "_vc_serial_canceling", 0) == 1:
+ return False
+ if getattr(self, "_ptt_pending_arm", 0) == 1:
+ return False
+ if getattr(self, "_ptt_interrupt_active", 0) == 1:
+ return False
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ return False
+ return True
+ except Exception:
+ return False
+
+ def _ptt_handle_stale_cancel(self):
+ """仅清迟到 TTS/播放下行,保持 PTT 录音与蓝快闪。"""
+ if not self._vc_is_ptt_mode():
+ return
+ 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._ptt_publish_listening_fast("canceled/stale")
+ print("[PTT] stale canceled -> keep recording")
+ except Exception:
+ pass
+
def _long_dialogue_is_stale_server_cancel(self):
"""打断已开麦后迟到的 canceled:勿停录/勿灭灯(多次短按常见)。"""
if not self._vc_is_long_dialogue_mode():
try:
if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
return False
+ # 已 asr.completed、等 LLM/TTS 期间的迟到 cancel 视为残留
+ if (
+ getattr(self, "_turn_has_asr_completed", 0) == 1
+ and getattr(self, "_turn_has_tts_started", 0) == 0
+ ):
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (
+ VCTurnState.SpeechEnded,
+ VCTurnState.Thinking,
+ VCTurnState.TtsReceiving,
+ ):
+ return True
+ if getattr(self, "_llm_completed_ms", 0):
+ return True
if getattr(self, "VC_record_state_flag", 0) == 0:
return False
settled = getattr(self, "_long_dialogue_interrupt_settled_ms", 0)
"""PTT 长按:等 started / asr.started 后开录,无开录提示音(REQ-AI-07)。"""
try:
self._ptt_force_record_on_started = 1
+ self._ptt_round_phase = PTT_ROUND_ARMING
except Exception:
pass
and str(reason).find("canceled") >= 0
and self._long_dialogue_is_stale_server_cancel()
)
- if stale_cancel:
+ ptt_stale_cancel = (
+ self._vc_is_ptt_mode()
+ and str(reason).find("canceled") >= 0
+ and self._ptt_is_stale_server_cancel()
+ )
+ if stale_cancel or ptt_stale_cancel:
pass
elif (
not pending_intr
pass
try:
if getattr(self, "AI_client_VC_isconnect", 0) == 1:
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
except Exception:
pass
try:
pass
elif op == OP_IDLE_UPLINK:
self.request_uplink_start("server.started/idle")
- elif op == OP_PTT_ROUND:
- _ptt_rec = getattr(self, "_ptt_force_record_on_started", 0) == 1
- if _ptt_rec:
- try:
- self._ptt_force_record_on_started = 0
- except Exception:
- pass
- try:
- self.AI_client_VC_send_speech()
- except Exception:
- self.request_send_audio_create()
- try:
- if _ptt_rec:
- self._vc_user_record_entry("ptt_begin/started")
- except Exception:
- pass
+
+ def _vc_on_server_started_plan(self, plan):
+ if not plan:
+ return
+ rest = []
+ ptt_arm = 0
+ for item in plan:
+ try:
+ op = item.get("op") if isinstance(item, dict) else item
+ except Exception:
+ continue
+ if op == OP_PTT_ROUND:
+ ptt_arm = 1
+ else:
+ rest.append(item)
+ if rest:
+ self._vc_apply_notify_started_plan(rest)
+ if ptt_arm and self._vc_is_ptt_mode():
+ self._vc_enqueue_btn(self._ptt_started_arm_cb, priority=1)
+
+ def _ptt_started_arm_cb(self, data=None):
+ if not self._ptt_round_should_arm_on_started():
+ return
+ self._ptt_force_record_on_started = 0
+ try:
+ self.AI_client_VC_send_speech()
+ except Exception:
+ self.request_send_audio_create()
+ try:
+ self._vc_user_record_entry("ptt_begin/started")
+ except Exception:
+ pass
def _vc_send_protocol_cancel(self, reason=""):
"""协议 cancel(Power / 打断)。"""
try:
if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
self.AI_client_VC_send(ujson.dumps({"event": "cancel", "payload": {}}))
- print("[VC] cancel reason={}".format(reason))
except Exception:
pass
- def _ptt_abort_short_record(self, tts_str):
- """PTT 录音过短:cancel + notify TTS + idle。"""
+ def _vc_asr_empty_end_and_cancel(self):
try:
- self.AI_key_long_press_flag = 0
- self._ptt_defer_speech_until_asr = 0
+ if getattr(self, "VC_record_state_flag", 0):
+ self.VC_Speech_End()
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(
+ self, "_vc_session_started", 0
+ ) == 1:
+ self._vc_send_protocol_cancel("asr.empty")
+ except Exception:
+ pass
+
+ def _vc_asr_empty_play_hint(self, hint):
+ try:
+ self._pending_tts_kind = self._last_tts_kind = "asr_empty_hint"
+ self.request_send_synthesis(hint)
+ except Exception:
+ pass
+
+ def _ptt_reset_empty_flags(self, soft):
+ try:
+ if soft:
+ self._ptt_abort_interrupt_arm()
+ self._ptt_round_phase = PTT_ROUND_IDLE
+ self._turn_guard.clear_interrupt_recover()
+ self._ptt_await_server = 0
self._ptt_audio_created = 0
+ self._ptt_defer_speech_until_asr = 0
self._ptt_record_open_ms = 0
+ self.AI_key_long_press_flag = 0
+ self._ptt_idle_latch = 1 if soft else 0
+ if not soft:
+ self._vc_reopen_uplink_after_empty_hint = 0
except Exception:
pass
+
+ def _ptt_abort_short_record(self, tts_str):
+ """PTT 录音过短:cancel + notify TTS + idle。"""
+ self._ptt_round_abort("short_record", close_session=False, publish_idle=False)
try:
self._vc_send_protocol_cancel("ptt_short_record")
except Exception:
hint = str(cfg).strip()
except Exception:
pass
- try:
- print("[VC][ASR] completed empty -> clear + notify ({})".format(reason))
- except Exception:
- pass
try:
self.AI_ASR_str = ""
self.AI_ASR_str_tmp = ""
self._last_asr_result_text = ""
- self._asr_completed_tone_done = 0
except Exception:
pass
if self._vc_is_long_dialogue_mode():
return
self._vc_handle_asr_empty_ptt(hint, reason)
+ def _vc_should_soft_recover_empty_asr(self):
+ """过短/打断续聊空 ASR:仅重开麦,勿 cancel+synthesis 播「没听清」。"""
+ if self._vc_asr_empty_soft_recover():
+ return True
+ try:
+ if self._vc_is_ptt_mode() and getattr(self, "_turn_has_asr_result", 0) != 1:
+ return True
+ except Exception:
+ pass
+ min_ms = 1500
+ try:
+ if "vc_config" in globals() and vc_config:
+ min_ms = int(vc_config.get_int("ASR_EMPTY_MIN_UPLINK_MS", min_ms))
+ except Exception:
+ pass
+ dur = int(getattr(self, "_vc_last_uplink_dur_ms", -1))
+ if dur >= 0 and dur < min_ms:
+ r = str(getattr(self, "_vc_user_record_open_reason", "") or "")
+ if hasattr(self, "_vc_uplink_reason_is_user_key") and self._vc_uplink_reason_is_user_key(r):
+ return True
+ if r.startswith("key."):
+ return True
+ return False
+
def _vc_handle_asr_empty_long(self, hint, reason="asr.completed"):
"""长对话:空 ASR 提示播完后复用 sid 重新开麦(蓝慢闪)。"""
+ if self._vc_should_soft_recover_empty_asr():
+ try:
+ self._server_vad_silence = 0
+ self._vc_defer_vad_uplink_stop = 0
+ self._vc_vad_had_active_after_silence = 0
+ self._vc_reopen_uplink_after_empty_hint = 0
+ if getattr(self, "VC_record_state_flag", 0):
+ self.VC_Speech_End()
+ self._vc_clear_stale_record_state("asr.empty/recover")
+ self._long_dialogue_publish_listening("asr.empty/recover")
+ self.request_uplink_start("asr.empty/recover")
+ except Exception:
+ pass
+ return
try:
self._vc_reopen_uplink_after_empty_hint = 0
if (
self._vc_vad_had_active_after_silence = 0
except Exception:
pass
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- self.VC_Speech_End()
- except Exception:
- pass
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(
- self, "_vc_session_started", 0
- ) == 1:
- self._vc_send_protocol_cancel("asr.empty")
- except Exception:
- pass
- try:
- self._pending_tts_kind = "asr_empty_hint"
- self._last_tts_kind = "asr_empty_hint"
- self.request_send_synthesis(hint)
- print("[VC][ASR] long empty hint -> synthesis, reopen_after=1")
- except Exception as e:
- usys.print_exception(e)
+ self._vc_asr_empty_end_and_cancel()
+ self._vc_asr_empty_play_hint(hint)
def _vc_handle_asr_empty_ptt(self, hint, reason="asr.completed"):
- """单对话:idle + Notify(不断线重连的轻量路径仍走 play_tts)。"""
- try:
- self._ptt_await_server = 0
- self._ptt_audio_created = 0
- self._ptt_defer_speech_until_asr = 0
- self._ptt_record_open_ms = 0
- self.AI_key_long_press_flag = 0
- self._ptt_idle_latch = 1
- self._vc_reopen_uplink_after_empty_hint = 0
- except Exception:
- pass
- try:
- if getattr(self, "VC_record_state_flag", 0) != 0:
- self.VC_Speech_End()
- except Exception:
- pass
- try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(
- self, "_vc_session_started", 0
- ) == 1:
- self._vc_send_protocol_cancel("asr.empty")
- except Exception:
- pass
- try:
- EventMesh.publish("play_tts", [hint, 0, "prompt"])
- except Exception:
- pass
+ soft = self._vc_should_soft_recover_empty_asr()
+ self._ptt_reset_empty_flags(soft)
+ self._vc_asr_empty_end_and_cancel()
+ if soft:
+ try:
+ self._vc_publish_led("idle", "asr.empty", prefer_off=1)
+ except Exception:
+ pass
+ return
+ self._vc_asr_empty_play_hint(hint)
try:
- self._vc_publish_led("idle", "asr.empty")
+ self._vc_publish_led("thinking", "asr.empty")
except Exception:
pass
usys.print_exception(e)
return False
+ def _vc_power_short_shutdown(self):
+ """电源短按:停录 + cancel + 强制 Close(PWR 须能退出,不受模式切换 phase 挡)。"""
+ try:
+ from vc_overlay import overlay_set, OVERLAY_IDLE
+ overlay_set(self, OVERLAY_IDLE)
+ except Exception:
+ try:
+ self._vc_ms_phase = 0
+ self._vc_ms_close_pending = 0
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self.VC_Speech_End()
+ except Exception:
+ pass
+ try:
+ ux = None
+ try:
+ from vc_ux import derive_ux_state, ux_name
+ ux = ux_name(derive_ux_state(self))
+ except Exception:
+ ux = "?"
+ self._vc_send_protocol_cancel("power_short/ux={}".format(ux))
+ except Exception:
+ pass
+ try:
+ self._vc_close_reason = "power_short"
+ except Exception:
+ pass
+ try:
+ print("[KEY] power_short -> Close(force) rec={}".format(
+ getattr(self, "VC_record_state_flag", -1)))
+ except Exception:
+ pass
+ self.Close(force=True)
+
def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):
"""电源键短按:读留言/会议;否则按 UX 表 cancel+Close(清 sid)。"""
if self._vc_ms_keys_blocked("pwk_release"):
pass
return False
- def _long_dialogue_interrupt_on_short_press(self):
- """长对话 TTS/思考中短按:停播 + cancel;canceled(或 cancel 超时)后开麦。"""
+ def _intr_discard_pending_uplink(self):
+ """取消「等 canceled 再开麦」;保留 TTS 丢弃窗口(与 on_record_restart 配合)。"""
+ intr = getattr(self, "_intr", None)
+ keep_drop = False
+ try:
+ g = getattr(self, "_turn_guard", None)
+ keep_drop = g.should_preserve_tts_drop_window() if g else False
+ except Exception:
+ keep_drop = bool(getattr(self, "_drop_tts_audio", 0))
+ if intr is not None:
+ intr.pending_uplink = 0
+ intr.active = 0
+ intr.cancel_deadline_ms = 0
+ if keep_drop:
+ intr.drop_tts = 1
+ intr.ruppt_tts = 1
+ intr._sync_legacy(self)
+ return
+ try:
+ self._vc_pending_interrupt_uplink = 0
+ self._vc_cancel_deadline_ms = 0
+ except Exception:
+ pass
+
+ def _long_dialogue_invalidate_server_turn(self, tag, wait_canceled_uplink=False):
+ """作废云端当前轮:turn_bump + 守卫 + 停播 + cancel。wait=True 时等 canceled 再开麦。"""
if not self._vc_is_long_dialogue_mode():
return
try:
- self._vc_turn_bump("long_interrupt")
+ self._vc_turn_bump(tag)
self.Close_flag = 0
self.fsm.await_played = False
- wait_ms = 800
- if "vc_config" in globals() and vc_config:
- wait_ms = int(vc_config.get_int("LONG_DIALOGUE_CANCEL_WAIT_MS", wait_ms))
- if getattr(self, "_intr", None) is not None:
- self._intr.begin(
- self, InterruptContext.KIND_LONG_SHORT,
- pending_uplink=1, cancel_wait_ms=wait_ms,
- )
+ if wait_canceled_uplink:
+ self._turn_guard.on_long_interrupt()
+ wait_ms = 800
+ if "vc_config" in globals() and vc_config:
+ wait_ms = int(vc_config.get_int("LONG_DIALOGUE_CANCEL_WAIT_MS", wait_ms))
+ if getattr(self, "_intr", None) is not None:
+ self._intr.begin(
+ self, InterruptContext.KIND_LONG_SHORT,
+ pending_uplink=1, cancel_wait_ms=wait_ms,
+ )
+ else:
+ self.is_ruppt_tts = 1
+ self._drop_tts_audio = 1
+ self._vc_pending_interrupt_uplink = 1
+ self._vc_cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), wait_ms)
else:
- self.is_ruppt_tts = 1
- self._drop_tts_audio = 1
- self._vc_pending_interrupt_uplink = 1
- self._vc_cancel_deadline_ms = utime.ticks_add(utime.ticks_ms(), wait_ms)
+ self._turn_guard.on_record_restart()
+ self._intr_discard_pending_uplink()
+ self._vc_interrupt_cancel_uplink_armed = 1
except Exception:
pass
try:
- self._vc_stop_downlink_for_interrupt("long_press/interrupt")
+ self._vc_stop_downlink_for_interrupt(tag)
except Exception:
pass
try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1:
- self.AI_client_VC_send(ujson.dumps({"event": "cancel", "payload": {}}))
- print("[KEY] long_dialogue interrupt -> cancel")
+ self._vc_send_protocol_cancel(tag)
except Exception:
pass
try:
except Exception:
pass
- def _ptt_stop_tts_on_short_press(self):
- """单对话短按:停本地+下行 TTS、灭灯;发 cancel,不换 sid、不开录。"""
+ def _long_dialogue_interrupt_on_short_press(self):
+ """长对话 TTS/思考中短按:停播 + cancel;canceled(或超时)后再开麦。"""
+ self._long_dialogue_invalidate_server_turn(
+ "long_interrupt", wait_canceled_uplink=True)
+ try:
+ print("[KEY] long_dialogue interrupt -> cancel")
+ except Exception:
+ pass
+
+ def _ptt_short_press_interrupt_restart(self):
+ """单对话 TTS/思考中短按松手:打断下行并进入新一轮录音(同长按打断 cancel→开麦)。"""
if not self._vc_is_ptt_mode():
return
- self._ptt_short_press_stop = 1
- self._ptt_idle_latch = 1
try:
- if self._vc_ptt_need_interrupt_wait() or self._vc_tts_pipeline_busy():
- self._vc_turn_bump("ptt_short_stop")
- self.is_ruppt_tts = 1
- self._drop_tts_audio = 1
- self._ptt_interrupt_active = 0
- self._ptt_led_lock_listening = 0
+ self._ptt_short_press_stop = 0
+ self._ptt_idle_latch = 0
+ except Exception:
+ pass
+ try:
self.fsm.await_played = False
except Exception:
pass
try:
- self._vc_stop_downlink_for_interrupt("ptt_short_press")
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self.VC_Speech_End()
except Exception:
pass
try:
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
- json_data = {"event": "cancel", "payload": {}}
- self.AI_client_VC_send(ujson.dumps(json_data))
- print("[PTT] short_press stop_tts -> cancel")
+ self._ptt_enter_interrupt_arm(bump_gen=True)
+ except Exception:
+ pass
+ try:
+ self._vc_stop_downlink_for_interrupt("ptt_short_interrupt")
+ except Exception:
+ pass
+ try:
+ if getattr(self, "AI_client_VC_isconnect", 0) != 1:
+ self._vc_ptt_mark_record_after_started()
+ self.AI_VC_start()
+ print("[PTT] short_interrupt -> AI_VC_start (no ws)")
+ return
+ except Exception:
+ pass
+ try:
+ if getattr(self, "_vc_session_started", 0) != 1:
+ self._vc_ptt_mark_record_after_started()
+ self.AI_client_VC_start_requset()
+ print("[PTT] short_interrupt -> wait started")
+ return
except Exception:
pass
try:
- self._vc_publish_led("idle", "ptt_short_press/stop_tts")
+ if self._vc_ptt_need_interrupt_wait():
+ self._vc_begin_ptt_cancel_wait("ptt_short_interrupt")
+ print("[PTT] short_interrupt -> cancel_wait arm")
+ return
except Exception:
pass
+ try:
+ self._ptt_arm_uplink()
+ print("[PTT] short_interrupt -> arm uplink")
+ except Exception as e:
+ usys.print_exception(e)
+
+ def _ptt_stop_tts_on_short_press(self):
+ """兼容旧名:单对话短按打断 TTS 并重新开录。"""
+ self._ptt_short_press_interrupt_restart()
def _ai_key_press_ptt_mode(self):
"""单对话 PTT:按下仅吞掉;短按停播在松手且未长按时;长按→ptt_begin。"""
except Exception:
pass
def _vc_restart_record_same_dialog(self):
- """é\95¿å¯¹è¯\9d recording+ç\9fæ\8c\89ï¼\9aå\81\9cå½\95å\90\8eç«\8bå\8d³é\87\8då¼\80ï¼\88æ\97§ sidï¼\8c对é½\90设计稿)。"""
+ """é\95¿å¯¹è¯\9d recording+ç\9fæ\8c\89ï¼\9aåº\9fä¸\8aä¸\80è½®ä¸\8bè¡\8c + å\81\9cå½\95ç«\8bå\8d³é\87\8då¼\80ï¼\88æ\97§ sid)。"""
if not self._vc_is_long_dialogue_mode():
return False
try:
- self.Close_flag = 0
self._skip_started_uplink_once = 0
+ self._long_dialogue_invalidate_server_turn(
+ "key.restart", wait_canceled_uplink=False)
self._vc_reset_long_idle_timer("key.restart")
self._vc_long_dialogue_key_arm_session()
self._long_dialogue_publish_listening("key.restart")
self.request_uplink_stop("key.restart/stop")
self._vc_clear_stale_record_state("key.restart/post")
self.request_uplink_start("key.restart/start")
+ try:
+ print("[KEY] key.restart invalidate+cancel -> uplink restart")
+ except Exception:
+ pass
return True
except Exception as e:
usys.print_exception(e)
except Exception:
pass
try:
+ ux = None
+ try:
+ from vc_ux import derive_ux_state, UX_TTS, UX_THINKING
+ ux = derive_ux_state(self)
+ if ux in (UX_TTS, UX_THINKING) and hasattr(
+ self, "_ptt_short_press_interrupt_restart"
+ ):
+ self._ptt_short_press_interrupt_restart()
+ return
+ except Exception:
+ pass
if self._ptt_is_active_tts_playback():
- self._ptt_stop_tts_on_short_press()
+ self._ptt_short_press_interrupt_restart()
return
except Exception:
pass
utime.sleep_ms(gap)
except Exception:
pass
- print("[PTT] release end_speech asr_str={!r} rec={}".format(
- self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))
self.AI_client_VC_end_speech()
try:
self._ptt_record_open_ms = 0
except Exception:
pass
+ elif self._ptt_in_interrupt_wait():
+ self._ptt_release_interrupt_settle()
+ elif getattr(self, "VC_record_state_flag", 0) == 0:
+ try:
+ self._ptt_skip_arm_on_cancel = 1
+ self._ptt_pending_arm = 0
+ except Exception:
+ pass
+ self._ptt_round_abort("", close_session=False, publish_idle=True)
+ try:
+ self._ptt_idle_latch = 1
+ self._vc_publish_led("idle", "ptt_rel_pre", prefer_off=1)
+ except Exception:
+ pass
elif getattr(self, "_ptt_audio_created", 0) == 1 or getattr(
self, "_ptt_defer_speech_until_asr", 0
) == 1:
- self.AI_key_long_press_flag = 0
try:
- self._ptt_defer_speech_until_asr = 0
- self._ptt_audio_created = 0
- self._ptt_abort_interrupt_arm()
if getattr(self, "AI_client_VC_isconnect", 0) == 1:
self.AI_client_VC_send(
ujson.dumps({"event": "cancel", "payload": {}}))
- print("[PTT] release cancel pending audio.create (no record)")
except Exception:
pass
+ self._ptt_round_abort("", close_session=False)
else:
+ self._ptt_round_abort("", close_session=False)
+ try:
self.AI_key_long_press_flag = 0
- try:
- if getattr(self, "_ptt_short_press_stop", 0) == 1:
- self._ptt_short_press_stop = 0
- except Exception:
- pass
- print("[PTT] release_no_uplink_skip")
+ except Exception:
+ pass
return
try:
if getattr(self, "_ptt_short_press_stop", 0) == 1:
try:
if bump_gen:
self._vc_turn_bump("ptt_interrupt")
+ self._vc_arm_interrupt_asr_recover("ptt_interrupt")
self._ptt_interrupt_active = 1
self._ptt_led_lock_listening = 1
self._vc_play_end_suppress_idle_led = 1
except Exception:
return False
+ def _vc_reset_turn_rx_flags(self, reason=""):
+ self._turn_guard.reset_turn_flags(reason)
+
def _vc_turn_bump(self, reason=""):
"""打断/新轮次:递增 turn_gen 并 purge 队列中过期 VC 项。"""
try:
self._vc_turn_gen = int(getattr(self, "_vc_turn_gen", 0)) + 1
self._ptt_arm_epoch = self._vc_turn_gen
- print("[ORCH] turn_gen={} reason={}".format(self._vc_turn_gen, reason))
EventMesh.publish("vc_orchestrator_purge", self._vc_turn_gen)
except Exception:
pass
except Exception:
pass
self._drop_tts_audio = 1
- print("[VC][serial] cancel_wait reason={} turn={}".format(
- reason, getattr(getattr(self, "fsm", None), "turn", "?")))
self._ptt_publish_listening_fast("ptt_cancel_wait")
except Exception:
pass
return
self._vc_serial_canceling = 0
self._vc_cancel_deadline_ms = 0
+ if getattr(self, "_ptt_skip_arm_on_cancel", 0):
+ self._ptt_skip_arm_on_cancel = 0
+ self._ptt_pending_arm = 0
+ self._ptt_interrupt_active = 0
+ self._drop_tts_audio = 0
+ self.is_ruppt_tts = 0
+ return
if getattr(self, "_ptt_pending_arm", 0):
self._ptt_pending_arm = 0
- print("[VC][serial] ptt_arm after {}".format(via))
self._ptt_arm_uplink()
except Exception:
pass
if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:
self._vc_cancel_deadline_ms = 0
if getattr(self, "VC_record_state_flag", 0) == 0:
+ if (
+ hasattr(self, "_vc_interrupt_cancel_uplink_busy")
+ and self._vc_interrupt_cancel_uplink_busy()
+ ):
+ return
print("[KEY] long_dialogue cancel_wait timeout -> uplink")
try:
self._vc_clear_stale_record_state("canceled/timeout")
pass
def _ptt_arm_uplink(self, t0=0):
"""btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""
+ try:
+ if getattr(self, "_ptt_skip_arm_on_cancel", 0) == 1:
+ return
+ if getattr(self, "AI_key_long_press_flag", 0) != 1:
+ return
+ except Exception:
+ pass
try:
self._ptt_await_server = 0
self.is_ruppt_tts = 0
except Exception:
pass
self._ptt_end_led_suppress()
- try:
- if t0:
- print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
- except Exception:
- pass
def ptt_begin(self, reason="key.long_press"):
if not self._vc_is_ptt_mode():
return
# PTT 新一轮开始:清理模式切换/提示音退出标志
try:
self.Close_flag = 0
+ self._last_tts_kind = ""
+ self._pending_tts_kind = ""
except Exception:
pass
# 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错
try:
if getattr(self, "VC_record_state_flag", 0) != 0:
- self.VC_Speech_End()
+ silent = getattr(self, "AI_key_long_press_flag", 0) == 1
+ self.VC_Speech_End(ptt_silent=silent)
except Exception:
pass
self._turn_has_asr_completed = 0
self._turn_has_vad_silence = 0
self._turn_has_tts_started = 0
+ self._turn_has_asr_result = 0 # 本轮是否收到过非空 asr.result(防残留 completed 双滴)
+ self._vc_last_think_tone_ms = 0
+ self._vc_llm_without_tts_round = 0 # llm 无 TTS 轮次,禁止 completed 误开麦
+ self._llm_reply_text = "" # llm.completed/delta 正文(simple_response 时用于 synthesis 兜底)
+ self._vc_llm_synth_fallback_ms = 0
+ self._vc_await_late_tts_until_ms = 0
+ self._vc_post_interrupt_until_asr = 0 # 与 guard 同步:打断后至 asr.started
+ self._vc_suppress_stale_server_turn = 0 # 兼容旧读法,由 VCTurnGuard 维护
+ self._vc_stale_tts_until_asr_started = 0
+ self._vc_interrupt_recover_until_ms = 0
+ self._vc_ignore_vad_silence_until_ms = 0
+ self._vc_interrupt_asr_min_uplink_ms = 2000
+ self._vc_last_uplink_dur_ms = -1
self._vc_reopen_uplink_after_empty_hint = 0
+ self._vc_interrupt_record_start_tone = 0
+ self._vc_defer_record_for_tone = 0
+ self._vc_interrupt_cancel_uplink_armed = 0
+ self._turn_guard = VCTurnGuard(self)
# VC 会话是否已进入 started(服务端 session initialized)
# 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环
self._vc_session_started = 0
# Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
# 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
self._ptt_audio_created = 0
+ self._ptt_round_phase = PTT_ROUND_IDLE
+ self._ptt_skip_arm_on_cancel = 0
# PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听
self._ptt_await_server = 0
# 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create
except Exception as e:
usys.print_exception(e)
+
+ def _vc_asr_started_should_reset_think_tone(self):
+ """未开麦的 asr.started(llm 无 TTS 后的残留)勿重置滴声标志。"""
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ return True
+ if getattr(self, "_turn_has_asr_result", 0) == 1:
+ return True
+ if getattr(self, "_llm_completed_ms", 0) and getattr(self, "_turn_has_tts_started", 0) == 0:
+ return False
+ except Exception:
+ pass
+ return False
+
def set_pwk_notice_flag(self,event,msg):#设置未读状态
self.pwk_notice_flag = msg
def get_wait_voice_flag(self,event,msg):
return self.VC_satrt_flag
def check_vc_start(self):
print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))
+ try:
+ if (
+ self._vc_is_ptt_mode()
+ and int(getattr(self, "_ptt_round_phase", PTT_ROUND_IDLE))
+ == PTT_ROUND_IDLE
+ and getattr(self, "VC_satrt_flag", 0) == 1
+ and getattr(self, "VC_record_state_flag", 0) == 0
+ and getattr(self, "AI_key_long_press_flag", 0) == 0
+ and not getattr(self, "_ptt_await_server", 0)
+ and not self._vc_tts_playback_active()
+ and getattr(self, "AI_client_VC_isconnect", 0) == 1
+ ):
+ self._vc_close_reason = "ptt_orphan"
+ self.Close()
+ return
+ except Exception:
+ pass
if self.VC_satrt_flag == 0:
if self.VC_record_state_flag != 0 or self.AI_client_VC_isconnect == 1 or self.VC_Play_flag == 1:
self.Close()
except Exception:
pass
def _vc_mode_switch_prep(self, gen):
- """模å¼\8få\88\87æ\8d¢ PREPï¼\9a软å\81\9cæ\92/æ\96 WS/å\81\9c线ç¨\8bï¼\8cä¸\8dè°\83ç\94¨å®\8cæ\95´ Closeï¼\88é\81¿å\85\8d play_pcm.close æ\8e\90æ\8f\90示é\9f³ï¼\89。"""
+ """模å¼\8få\88\87æ\8d¢ PREPï¼\9aè½»é\87\8f Closeï¼\88ä¸\8dæ\89\93 VC_Play_Endï¼\89ï¼\8cå\86\8dæ\92模å¼\8fæ\8f\90示é\9f³。"""
try:
print("[VC][ms] prep gen={} {}".format(gen, self._vc_fsm_log_snip()))
except Exception:
pass
try:
if getattr(self, "VC_satrt_flag", 0) == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:
- self.websocket_deconnect_VC()
- except Exception:
- pass
- try:
- if getattr(self, "VC_satrt_flag", 0) == 1:
- self.AI_audio_queue_thread_close()
- self.AI_VC_send_data_queue_thread_close()
+ self.Close_reconnect()
except Exception:
pass
try:
tts_str = getattr(self, "_vc_ms_pending_tts_str", "") or ""
if not tts_str:
tts_str = "已切换到单对话模式" if getattr(self, "VC_mode", 0) == 1 else "已切换到长对话模式"
- print("[VC][ms] play_tts gen={} mode={}".format(gen, getattr(self, "VC_mode", -1)))
- EventMesh.publish("play_tts", [tts_str, 1])
+ kind = "mode_switch_ptt" if getattr(self, "VC_mode", 0) == 1 else "mode_switch_long"
+ print("[VC][ms] play_tts gen={} mode={} kind={}".format(
+ gen, getattr(self, "VC_mode", -1), kind))
+ EventMesh.publish("play_tts", [tts_str, 1, kind])
def pwk_key_double_press(self,event,msg):#pwk双击
print("come 双击")
EventMesh.publish("publish_AI_btnInfo", {"func": self._vc_idle_deep_sleep_impl})
if _bg_active:
+ try:
+ self._vc_late_tts_wait_tick(utime.ticks_ms())
+ except Exception:
+ pass
# 统一兜底表(按状态机状态触发)
try:
self._fsm_timeout_table_tick(utime.ticks_ms())
EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])
def Close_VC(self,event,msg):
EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- def Close(self):
+ def Close_reconnect(self):
+ """断链后重连/接管:轻量 Close,不打断本地 TTS、保留 dialog、不经 idle_shutdown。"""
+ self.Close(light=True, force=True, skip_idle_shutdown=True, preserve_dialog=True)
+ def Close(self, light=False, force=False, skip_idle_shutdown=False, preserve_dialog=False):
try:
self._vc_persist_vc_mode()
except Exception:
pass
try:
ms_phase = int(getattr(self, "_vc_ms_phase", 0))
- if ms_phase in (1, 2, 3) and getattr(self, "_vc_close_reason", "") != "mode_switch_play_end":
+ cr = str(getattr(self, "_vc_close_reason", "") or "")
+ if (
+ not force
+ and ms_phase in (1, 2, 3)
+ and cr != "mode_switch_play_end"
+ and not cr.startswith("power_short")
+ ):
print("[VC][ms] Close blocked phase={} reason={}".format(
- ms_phase, getattr(self, "_vc_close_reason", "")))
+ ms_phase, cr))
return
except Exception:
pass
try:
- self._vc_close_reason = ""
+ if not light:
+ self._vc_close_reason = ""
except Exception:
pass
try:
- self._vc_idle_close_in_progress = 1
+ if not light:
+ self._vc_idle_close_in_progress = 1
except Exception:
pass
try:
- if getattr(self, "VC_mode", 0) == 0:
+ if not light and getattr(self, "VC_mode", 0) == 0:
self._vc_prepare_for_idle_close()
except Exception:
pass
getattr(self, "VC_satrt_flag", -1), getattr(self, "VC_record_state_flag", -1)))
except Exception:
pass
- self.VC_stop_play_audio_flag = 0
+ if not light:
+ self.VC_stop_play_audio_flag = 0
try:
- self.AI_key_long_press_flag = 0
+ if not light:
+ self.AI_key_long_press_flag = 0
except Exception:
pass
self.VC_Thinking_timerout = 0
except Exception:
pass
try:
- self._vc_boot_standby = 0
+ if not light:
+ self._vc_boot_standby = 0
except Exception:
pass
try:
- self._vc_reset_dialogue_mode_state()
+ if not light:
+ self._vc_reset_dialogue_mode_state()
except Exception:
pass
# Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识
- try:
- self._cur_dialog_id = ""
- except Exception:
- pass
- try:
- self._cur_sid = ""
- except Exception:
- pass
- self.is_ruppt_tts = 0#打断标志位清零
+ if not preserve_dialog:
+ try:
+ self._cur_dialog_id = ""
+ except Exception:
+ pass
+ try:
+ self._cur_sid = ""
+ except Exception:
+ pass
+ if not light:
+ self.is_ruppt_tts = 0#打断标志位清零
print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
try:
if getattr(self, "VC_mode", 0) != 1:
print("[VC][long_idle] step=set_led_state_idle (first)")
except Exception:
pass
- EventMesh.publish("set_led_state", {"ai_state": "idle", "prefer_off": 1})
+ if not light:
+ EventMesh.publish("set_led_state", {"ai_state": "idle", "prefer_off": 1})
self.tts_in_data_len = 0
self.tts_out_data_len = 0
#必须先销毁线程,不然线程会容易报错
except Exception:
pass
try:
+ self._vc_close_ws_internal = 1
self.websocket_deconnect_VC()
except Exception as e:
usys.print_exception(e)
-
-
-
-
+ finally:
+ try:
+ self._vc_close_ws_internal = 0
+ except Exception:
+ pass
print("ai_VC Close1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
print("[VC][long_idle] step=set_led_state_idle (second) -> VC_init VC_satrt_flag=0")
except Exception:
pass
- EventMesh.publish("set_led_state", {"ai_state": "idle"})
- self.VC_init()
+ if not light:
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ self.VC_init()
self.VC_satrt_flag = 0
try:
- self._vc_shutdown_idle_services("Close")
+ if not skip_idle_shutdown:
+ self._vc_shutdown_idle_services("Close")
except Exception:
pass
# Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)
+ if not light:
+ try:
+ _dsec = 60
+ _den = 1
+ if "vc_config" in globals() and vc_config:
+ _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
+ _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
+ if _den and _dsec > 0:
+ self._1S_timer.start(1000, 1, self._1S_timer_cb)
+ except Exception:
+ pass
try:
- _dsec = 60
- _den = 1
- if "vc_config" in globals() and vc_config:
- _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
- _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
- if _den and _dsec > 0:
- self._1S_timer.start(1000, 1, self._1S_timer_cb)
- except Exception:
- pass
- try:
- self._vc_idle_close_in_progress = 0
- self._vc_idle_l1_close = 0
+ if not light:
+ self._vc_idle_close_in_progress = 0
+ self._vc_idle_l1_close = 0
except Exception:
pass
try:
- self._vc_ms_phase = 0
- self._vc_ms_play_gen = 0
- self._vc_ms_close_pending = 0
+ if not light:
+ self._vc_ms_phase = 0
+ self._vc_ms_play_gen = 0
+ self._vc_ms_close_pending = 0
except Exception:
pass
def VC_init2(self):
- self.VC_init()
-
- print("ai_VC VC_init2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- try:
- self.websocket_deconnect_VC()
- except Exception as e:
- usys.print_exception(e)
+ print("ai_VC VC_init2 -> Close_reconnect")
+ self.Close_reconnect()
print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")
self._1S_timer.stop()
if self.AI_VC_send_data_queue.empty()!=True:
class VCSessionIoMixin:
_VC_STALE_DROP_EVENTS = (
- "tts.started", "tts.completed", "llm.created", "llm.completed", "completed",
+ "tts.started", "tts.completed", "llm.created", "llm.completed", "llm.done",
+ "completed", "asr.result", "asr.completed", "vad.silence", "vad.active",
)
def _vc_server_event_drop_stale(self, ev, gen):
except Exception:
pass
return
+ if self._vc_suppress_post_interrupt_server_turn(ev):
+ try:
+ print("[ORCH] drop post-interrupt ev={}".format(ev))
+ except Exception:
+ pass
+ return
+ try:
+ if ev == "asr.completed":
+ self._turn_has_asr_completed = 1
+ elif ev == "vad.silence":
+ self._turn_has_vad_silence = 1
+ except Exception:
+ pass
+ if ev in ("tts.started", "tts.completed") and self._vc_is_long_dialogue_mode():
+ try:
+ if (
+ hasattr(self, "_long_dialogue_is_stale_server_tts")
+ and self._long_dialogue_is_stale_server_tts()
+ ):
+ if hasattr(self, "_vc_purge_stale_tts_round"):
+ self._vc_purge_stale_tts_round("stale/{}".format(ev))
+ return
+ except Exception:
+ pass
if ev == "tts.started":
try:
self._vc_tts_rx_counters_reset()
except Exception:
pass
if ev == "canceled":
+ try:
+ if (
+ self._vc_is_ptt_mode()
+ and hasattr(self, "_ptt_is_stale_server_cancel")
+ and self._ptt_is_stale_server_cancel()
+ ):
+ self._ptt_handle_stale_cancel()
+ return
+ except Exception:
+ pass
+ try:
+ if (
+ self._vc_is_long_dialogue_mode()
+ and getattr(self, "_vc_pending_interrupt_uplink", 0) != 1
+ and (
+ getattr(self, "VC_Play_flag", 0) == 1
+ or getattr(self.fsm, "turn", None)
+ in (VCTurnState.Playing, VCTurnState.TtsReceiving)
+ )
+ ):
+ 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()
+ if getattr(self, "VC_Play_flag", 0) == 1 or getattr(
+ self, "play_pcm", None
+ ) is not None:
+ self.VC_Play_End()
+ try:
+ self.fsm.await_played = False
+ self._need_audio_played_ack = 0
+ except Exception:
+ pass
+ try:
+ self._long_dialogue_publish_listening(
+ "canceled/during_play")
+ except Exception:
+ pass
+ print("[VC] canceled during play -> stop TTS")
+ return
+ except Exception:
+ pass
try:
if (
self._vc_is_long_dialogue_mode()
self._vc_stop_downlink_for_interrupt("canceled")
except Exception:
pass
+ if ev == "asr.completed":
+ try:
+ if hasattr(self, "_vc_flush_deferred_vad_uplink_stop"):
+ self._vc_flush_deferred_vad_uplink_stop("asr.completed/vad")
+ except Exception:
+ pass
try:
actions = self.fsm.on_server_event(ev, jsload)
if ev == "canceled" and self._vc_is_long_dialogue_mode():
a for a in (actions or [])
if a and a[0] != "uplink_start"
]
+ elif (
+ hasattr(self, "_vc_interrupt_cancel_uplink_busy")
+ and self._vc_interrupt_cancel_uplink_busy()
+ ):
+ actions = [
+ a for a in (actions or [])
+ if a and a[0] != "uplink_start"
+ ]
+ try:
+ print("[VC] orch skip uplink_start (canceled dup)")
+ except Exception:
+ pass
+ except Exception:
+ pass
+ if ev == "vad.silence":
+ try:
+ _skip_vad_stop = False
+ if getattr(self, "_vc_defer_vad_uplink_stop", 0):
+ _skip_vad_stop = True
+ elif (
+ getattr(self, "_turn_has_asr_completed", 0) == 1
+ and getattr(self, "_turn_has_tts_started", 0) == 0
+ ):
+ _skip_vad_stop = True
+ if _skip_vad_stop:
+ actions = [
+ a for a in (actions or [])
+ if a and a[0] != "uplink_stop"
+ ]
+ print("[VC][VAD] orch skip uplink_stop (defer/post-asr)")
except Exception:
pass
self._fsm_apply_actions(actions)
+ if ev == "asr.completed":
+ try:
+ self._vc_try_play_think_tone()
+ except Exception:
+ pass
if ev == "canceled":
try:
if (
if ev == "canceled" and self._vc_is_ptt_mode():
self._vc_on_ptt_cancel_settled(via="canceled")
+ def _vc_suppress_post_interrupt_server_turn(self, ev):
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ return g.should_drop_server_event(ev)
+ try:
+ if getattr(self, "_vc_suppress_stale_server_turn", 0) != 1:
+ return False
+ return ev in (
+ "vad.silence", "vad.active", "asr.result", "asr.completed",
+ "llm.created", "llm.completed", "llm.done", "llm.delta", "completed",
+ )
+ except Exception:
+ return False
+
def _vc_dispatch_server_event(self, ev, jsload_data=None):
"""recv 线程:仅入编排队列,不在此修改 FSM。"""
+ try:
+ if hasattr(self, "_vc_should_drop_record_restart_stale") and self._vc_should_drop_record_restart_stale(ev):
+ print("[VC] drop record_restart stale ev={}".format(ev))
+ return
+ except Exception:
+ pass
+ if self._vc_suppress_post_interrupt_server_turn(ev):
+ try:
+ print("[VC] drop post-interrupt ev={}".format(ev))
+ except Exception:
+ pass
+ return
+ try:
+ if (
+ ev == "vad.silence"
+ and hasattr(self, "_vc_should_ignore_vad_silence")
+ and self._vc_should_ignore_vad_silence()
+ ):
+ print("[VC] drop vad.silence (interrupt recover grace)")
+ return
+ except Exception:
+ pass
gen = self._vc_turn_current()
self._vc_enqueue_btn(
self._vc_orchestrator_server_event_cb,
pass
self.AI_client_VC_isconnect = 0
try:
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
except Exception:
pass
def websocket_error(self, TTS_str=None, play_weak_signal_tone=False):
print("websocket_error4")
def websocket_deconnect_VC(self):
+ if not getattr(self, "_vc_close_ws_internal", 0):
+ try:
+ print("[VC] websocket_deconnect_VC -> Close_reconnect (unified)")
+ except Exception:
+ pass
+ try:
+ self.Close_reconnect()
+ except Exception as e:
+ usys.print_exception(e)
+ return
#print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))
print("websocket_deconnect_VC!!!!!!!")
self._vc_session_started = 0
self._turn_has_asr_completed = 0
self._turn_has_vad_silence = 0
self._turn_has_tts_started = 0
+ self._turn_has_asr_result = 0
self._llm_completed_ms = 0
_skip_long_up = 0
if getattr(self, "_skip_started_uplink_once", 0) == 1:
try:
_plan = NotifyScheduler.plan_on_started(
self, skip_long_uplink=_skip_long_up)
- self._vc_apply_notify_started_plan(_plan)
+ self._vc_on_server_started_plan(_plan)
except Exception as e:
usys.print_exception(e)
self.request_uplink_stop("config.update")
elif jsload_data["event"] == "asr.started" :#asr开始
+ if (
+ hasattr(self, "_vc_should_drop_record_restart_stale")
+ and self._vc_should_drop_record_restart_stale("asr.started")
+ ):
+ continue
+ try:
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ g.on_asr_started()
+ else:
+ self._vc_suppress_stale_server_turn = 0
+ self._vc_stale_tts_until_asr_started = 0
+ except Exception:
+ pass
try:
self.fsm.on_server_asr_started()
except Exception:
self._turn_has_asr_completed = 0
self._turn_has_vad_silence = 0
self._turn_has_tts_started = 0
+ self._turn_has_asr_result = 0
+ try:
+ self._llm_reply_text = ""
+ self._vc_llm_without_tts_round = 0
+ except Exception:
+ pass
self._llm_completed_ms = 0
self._vc_defer_vad_uplink_stop = 0
self._vc_vad_had_active_after_silence = 0
try:
- self._asr_completed_tone_done = 0
+ if hasattr(self, "_vc_asr_started_should_reset_think_tone"):
+ if self._vc_asr_started_should_reset_think_tone():
+ self._asr_completed_tone_done = 0
+ else:
+ self._asr_completed_tone_done = 0
except Exception:
pass
# 单对话:_drop_tts_audio 在 VC_Speech_Start 真开录后再清 0,避免 canceled 后仍播旧 TTS
# 单对话:勿在 asr.started 置 flag=2(未真开麦会僵死);由 VC_Speech_Start 维护
self.LocalRespondingEnded_timerout = 0
elif jsload_data["event"] == "asr.result" :#asr结果
+ if (
+ hasattr(self, "_vc_should_drop_record_restart_stale")
+ and self._vc_should_drop_record_restart_stale("asr.result")
+ ):
+ continue
try:
self.AI_ASR_str = jsload_data["payload"]["content"]
except Exception:
try:
if (self.AI_ASR_str or "").strip():
self._last_asr_result_text = self.AI_ASR_str
+ self._turn_has_asr_result = 1
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ g.on_asr_result(self.AI_ASR_str)
+ else:
+ self._vc_interrupt_recover_until_ms = 0
+ self._vc_ignore_vad_silence_until_ms = 0
except Exception:
pass
if self.AI_ASR_str == "":#没识别到
self.AI_TTS_str = "我没听清,请再说一遍"
self._vc_led_alert_rb_flash()
elif jsload_data["event"] == "asr.completed" :#asr 终止的标识
+ if (
+ hasattr(self, "_vc_should_drop_record_restart_stale")
+ and self._vc_should_drop_record_restart_stale("asr.completed")
+ ):
+ continue
+ if self._vc_suppress_post_interrupt_server_turn("asr.completed"):
+ continue
self._turn_has_asr_completed = 1
- try:
- if hasattr(self, "_vc_flush_deferred_vad_uplink_stop"):
- self._vc_flush_deferred_vad_uplink_stop("asr.completed/vad")
- except Exception:
- pass
try:
if self._vc_effective_asr_text() and hasattr(self, "_vc_reset_long_idle_timer"):
self._vc_reset_long_idle_timer("asr.completed")
self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or ""
except Exception:
pass
- # 本地 think 提示:仅在有有效 ASR 文本时播,避免空轮次连播「滴」
+ # think 滴声:编排线程 _vc_try_play_think_tone(asr.completed 唯一播放入口)
try:
- if getattr(self, "_asr_completed_tone_done", 0) == 1:
- pass
- elif not self._vc_effective_asr_text():
- try:
- self._vc_handle_asr_empty("asr.completed")
- except Exception as e:
- usys.print_exception(e)
- else:
- stream_on = False
- try:
- stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1
- except Exception:
- stream_on = False
- pcm_busy = getattr(self, "play_pcm", None) is not None
- playing = getattr(self, "VC_Play_flag", 0) == 1
- try:
- thinking = getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking
- except Exception:
- thinking = False
- vad_cont = getattr(self, "_vc_vad_had_active_after_silence", 0) == 1
- if (
- (not stream_on)
- and (not pcm_busy)
- and (not playing)
- and (not thinking)
- and (not vad_cont)
- ):
- self._vc_local_play('U:/static/think.mp3', 1, 1, 650)
- self._asr_completed_tone_done = 1
- elif vad_cont:
- try:
- print("[VC][ASR] skip think tone (vad pause merged)")
- except Exception:
- pass
- except Exception:
- pass
+ if not self._vc_effective_asr_text():
+ self._vc_handle_asr_empty("asr.completed")
+ except Exception as e:
+ usys.print_exception(e)
print("self.commands_mode:{}".format(self.commands_mode))
if self.commands_mode ==2:
ret= self.check_text(self._vc_effective_asr_text())
try:
self.request_uplink_stop("asr.completed/config_cmd")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close_reconnect})
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
except Exception as e:
usys.print_exception(e)
elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听
try:
self._vc_vad_had_active_after_silence = 1
- self._vc_defer_vad_uplink_stop = 0
- self._server_vad_silence = 0
+ if getattr(self, "_vc_defer_vad_uplink_stop", 0):
+ self._vc_defer_vad_uplink_stop = 0
+ self._server_vad_silence = 0
+ try:
+ print("[VC][VAD] active cancel defer, resume uplink")
+ except Exception:
+ pass
+ else:
+ self._server_vad_silence = 0
except Exception:
pass
try:
except Exception:
pass
elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识
+ if self._vc_suppress_post_interrupt_server_turn("vad.silence"):
+ continue
+ try:
+ if (
+ hasattr(self, "_vc_should_ignore_vad_silence")
+ and self._vc_should_ignore_vad_silence()
+ ):
+ print("[VC][VAD] ignore silence (interrupt recover grace)")
+ continue
+ except Exception:
+ pass
# 长对话:句中停顿仅标记,等 asr.completed 再停麦(配合服务端 vad_silence 加长)
self._turn_has_vad_silence = 1
try:
if hasattr(self, "_vc_long_dialogue_defer_vad_stop") and self._vc_long_dialogue_defer_vad_stop():
+ # 仅 defer 停麦(VC_Speech_End);须立刻 silence_gate 止 PCM,
+ # 否则云端已 vad.silence 端侧仍上行 → LLM 无 tts / 残留 asr 轮次
self._vc_defer_vad_uplink_stop = 1
+ self._server_vad_silence = 1
try:
- print("[VC][VAD] silence defer uplink_stop (long_dialogue)")
+ self._uplink_mark_end("vad.silence/defer")
+ except Exception:
+ pass
+ try:
+ print("[VC][VAD] silence defer stop_mic_only (silence_gate on)")
except Exception:
pass
else:
self._server_vad_silence = 1
self._uplink_mark_end("vad.silence")
elif jsload_data["event"] == "llm.created" :#思考(LLM)
+ try:
+ self._llm_reply_text = ""
+ except Exception:
+ pass
try:
if hasattr(self, "_vc_reset_long_idle_timer"):
self._vc_reset_long_idle_timer("llm.created")
self._llm_completed_ms = 0
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
pass
+ elif jsload_data["event"] == "llm.done":
+ try:
+ if hasattr(self, "_vc_capture_llm_reply_text"):
+ self._vc_capture_llm_reply_text(jsload_data, append=False)
+ except Exception:
+ pass
elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)
+ try:
+ if hasattr(self, "_vc_capture_llm_reply_text"):
+ self._vc_capture_llm_reply_text(jsload_data, append=False)
+ except Exception:
+ pass
try:
self._llm_completed_ms = utime.ticks_ms()
except Exception:
pass
pass
elif jsload_data["event"] == "llm.delta" :#llm返回数据
+ try:
+ if hasattr(self, "_vc_capture_llm_reply_text"):
+ self._vc_capture_llm_reply_text(jsload_data, append=True)
+ except Exception:
+ pass
if self.is_VAD == 0 and self.record_data_save_flag == 0:
if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止
self.request_uplink_stop("llm.delta")
elif jsload_data["event"] == "tts.started" :#tts开始
+ if (
+ self._vc_is_long_dialogue_mode()
+ and hasattr(self, "_long_dialogue_is_stale_server_tts")
+ and self._long_dialogue_is_stale_server_tts()
+ ):
+ try:
+ if hasattr(self, "_vc_purge_stale_tts_round"):
+ self._vc_purge_stale_tts_round("recv/tts.started")
+ except Exception:
+ pass
+ continue
try:
self._ptt_await_server = 0
except Exception:
pass
self._server_vad_silence = 1
self._turn_has_tts_started = 1
+ try:
+ self._vc_llm_without_tts_round = 0
+ except Exception:
+ pass
self._tts_pending = 0
self._tts_pending_ms = 0
self._uplink_mark_end("tts.started")
except Exception:
pass
elif jsload_data["event"] == "tts.completed" :#tts结束
+ if (
+ self._vc_is_long_dialogue_mode()
+ and hasattr(self, "_long_dialogue_is_stale_server_tts")
+ and self._long_dialogue_is_stale_server_tts()
+ ):
+ try:
+ if hasattr(self, "_vc_purge_stale_tts_round"):
+ self._vc_purge_stale_tts_round("recv/tts.completed")
+ except Exception:
+ pass
+ continue
# tts.completed 仅表示下发结束,本地可能仍在播放;绿灯关闭在本地播放结束时处理
try:
# server event 已在统一入口 on_server_event 触发 FSM,这里避免重复驱动
self.AI_TTS_str = "抱歉,没找到对应操作,可以换个说法试试"
#EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
self.request_uplink_stop("command")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close_reconnect})
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
elif jsload_data["event"] == "failed":#接口返回错误
if jsload_data["payload"]["error"] == "concurrency_limit_exceeded":
usys.print_exception(e)
EventMesh.publish("publish_AI_btnInfo",{"func":self.put_record_data,"data":[recv_data,""]})
else:
- print("被打断不播放")
+ if VC_WS_IO_LOG:
+ print("被打断不播放")
utime.sleep_ms(1)
except Exception as e:
usys.print_exception(e)
def _vc_uplink_reason_is_user_key(self, reason):
"""按键/PTT 开录(非 server.started、play_end、asr.empty 等自动开麦)。"""
r = str(reason or "")
- if r.startswith("key.press") or r.startswith("BT_AI_key_press"):
+ if r.startswith("key.press") or r.startswith("key.restart"):
+ return True
+ if r.startswith("BT_AI_key_press"):
return True
if r.startswith("ptt_begin") or r == "asr.started/defer":
return True
return False
def _vc_should_play_record_start_tone(self, reason):
- """按键开录播 start.mp3;idle 按 AI 建链后 server.started 首开麦也算按键开录。"""
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ return g.should_play_record_start_tone(reason)
+ r = str(reason or "")
+ if r == "canceled" or r.startswith("canceled/") or r.startswith("asr.empty"):
+ return False
if self._vc_uplink_reason_is_user_key(reason):
return True
- r = str(reason or "")
- if (
- r.startswith("server.started")
- and getattr(self, "_long_dialogue_key_arm", 0) == 1
- ):
+ if r.startswith("server.started") and getattr(self, "_long_dialogue_key_arm", 0) == 1:
return True
return False
usys.print_exception(e)
def _vc_user_record_uplink_open(self, reason=""):
+ try:
+ r = str(reason or "")
+ if r.startswith("key.restart"):
+ g = getattr(self, "_turn_guard", None)
+ if g is not None:
+ g.clear_record_restart_stale_drop("uplink_open")
+ except Exception:
+ pass
+ try:
+ self._vc_interrupt_record_start_tone = 0
+ except Exception:
+ pass
+ try:
+ if getattr(self, "_vc_defer_record_for_tone", 0) == 1:
+ self._vc_defer_record_for_tone = 0
+ self._vc_clear_record_tmp_queue("tone_done")
+ self._vc_interrupt_cancel_uplink_armed = 0
+ except Exception:
+ pass
try:
self._vc_defer_listening_led = 0
self._vc_skip_local_play_stop_record = 0
except Exception:
pass
try:
+ self._server_vad_silence = 0
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ self.VC_Speech_Start()
if getattr(self, "VC_record_state_flag", 0) == 1:
self.VC_Speech_Start_now()
- elif getattr(self, "VC_record_state_flag", 0) == 0:
- self.VC_Speech_Start()
- if getattr(self, "VC_record_state_flag", 0) == 1:
- self.VC_Speech_Start_now()
except Exception as e:
usys.print_exception(e)
try:
else:
self._vc_user_record_entry_long(reason)
+ def _vc_interrupt_cancel_uplink_busy(self):
+ """打断 cancel 开麦是否已在进行(防 timeout 与 server.canceled 双触发双滴声)。"""
+ try:
+ if getattr(self, "_vc_defer_record_for_tone", 0) == 1:
+ return True
+ if getattr(self, "_vc_interrupt_cancel_uplink_armed", 0) == 1:
+ return True
+ except Exception:
+ pass
+ return False
+
+ def _vc_clear_record_tmp_queue(self, tag=""):
+ """丢弃开录前缓存的 PCM(避免滴声/空档滞后送达云端 ASR)。"""
+ try:
+ q = getattr(self, "AI_record_tmp_queue", None)
+ if q is not None and q.empty() != True:
+ q.clear()
+ try:
+ print("[VC][user_record] clear_pcm_queue ({})".format(tag or "-"))
+ except Exception:
+ pass
+ except Exception:
+ pass
+
+ def _vc_record_start_tone_uplink_delay_ms(self):
+ """滴声后再启录音/上行:须覆盖 start.mp3 实际时长(勿仅用 180ms GAP)。"""
+ delay_ms = 700
+ gap_ms = 180
+ try:
+ if "vc_config" in globals() and vc_config:
+ delay_ms = int(vc_config.get_int(
+ "RECORD_START_TONE_UPLINK_DELAY_MS", delay_ms))
+ gap_ms = int(vc_config.get_int("RECORD_START_TONE_GAP_MS", gap_ms))
+ except Exception:
+ pass
+ if delay_ms < gap_ms + 400:
+ delay_ms = gap_ms + 400
+ if delay_ms < 500:
+ delay_ms = 500
+ return delay_ms
+
def _vc_user_record_entry_long(self, reason=""):
"""
长对话开麦:
- - 按键/打断续聊:先 start.mp3,滴声结束后再上行+蓝慢闪
+ - 按键/打断续聊:先 start.mp3,滴声结束后再启录音+上行(勿提前 VC_Speech_Start)
- 自动续聊(server.started/play_end 等):不播 start,直接上行
"""
try:
self._vc_user_record_open_reason = str(reason or "")
self._vc_defer_listening_led = 1
self._vc_skip_local_play_stop_record = 1
- except Exception:
- pass
- try:
- if getattr(self, "VC_record_state_flag", 0) == 0:
- self.VC_Speech_Start()
- elif getattr(self, "VC_record_state_flag", 0) > 1:
- pass
- except Exception as e:
- usys.print_exception(e)
- hold_ms = 180
- try:
- if "vc_config" in globals() and vc_config:
- hold_ms = int(vc_config.get_int("RECORD_START_TONE_GAP_MS", hold_ms))
+ self._vc_defer_record_for_tone = 0
except Exception:
pass
played = False
except Exception:
played = False
if not played:
+ try:
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ self.VC_Speech_Start()
+ elif getattr(self, "VC_record_state_flag", 0) > 1:
+ pass
+ except Exception as e:
+ usys.print_exception(e)
try:
self._vc_user_record_uplink_open(reason)
except Exception:
pass
return
try:
- total_ms = int(hold_ms) + 40
- if total_ms < 200:
- total_ms = 200
+ self._vc_defer_record_for_tone = 1
+ self._vc_interrupt_cancel_uplink_armed = 1
+ self._server_vad_silence = 1
+ except Exception:
+ pass
+ try:
+ total_ms = self._vc_record_start_tone_uplink_delay_ms()
self._vc_user_record_tone_timer.stop()
self._vc_user_record_tone_timer.start(total_ms, 0, self._vc_user_record_tone_timer_cb)
- print("[VC][user_record] entry_long key_tone reason={} tone_ms={}".format(reason, total_ms))
+ print("[VC][user_record] entry_long key_tone reason={} uplink_after_ms={}".format(
+ reason, total_ms))
except Exception as e:
usys.print_exception(e)
try:
+ self._vc_defer_record_for_tone = 0
self._vc_user_record_uplink_open(reason)
except Exception:
pass
return (getattr(self, "_last_asr_result_text", "") or "").strip()
except Exception:
return ""
+
+ def _vc_try_play_think_tone(self):
+ """asr.completed 本地 think(滴)每轮仅一次;编排线程唯一入口,避免 recv/空 ASR 重置导致连播。"""
+ try:
+ if getattr(self, "_asr_completed_tone_done", 0) == 1:
+ return
+ if getattr(self, "_turn_has_asr_result", 0) != 1:
+ try:
+ print("[VC][ASR] skip think tone (no asr.result this turn)")
+ except Exception:
+ pass
+ return
+ debounce_ms = 2500
+ try:
+ if "vc_config" in globals() and vc_config:
+ debounce_ms = int(vc_config.get_int("ASR_COMPLETED_THINK_DEBOUNCE_MS", debounce_ms))
+ except Exception:
+ pass
+ try:
+ last_ms = int(getattr(self, "_vc_last_think_tone_ms", 0) or 0)
+ if last_ms and utime.ticks_diff(utime.ticks_ms(), last_ms) < debounce_ms:
+ print("[VC][ASR] skip think tone (debounce {}ms)".format(debounce_ms))
+ return
+ except Exception:
+ pass
+ if self._vc_local_play_busy():
+ try:
+ print("[VC][ASR] skip think tone (local_play busy)")
+ except Exception:
+ pass
+ return
+ if not self._vc_effective_asr_text():
+ return
+ stream_on = False
+ try:
+ stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1
+ except Exception:
+ stream_on = False
+ pcm_busy = getattr(self, "play_pcm", None) is not None
+ playing = getattr(self, "VC_Play_flag", 0) == 1
+ try:
+ thinking = getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking
+ except Exception:
+ thinking = False
+ vad_cont = getattr(self, "_vc_vad_had_active_after_silence", 0) == 1
+ if stream_on or pcm_busy or playing or thinking or vad_cont:
+ return
+ self._asr_completed_tone_done = 1
+ try:
+ self._vc_last_think_tone_ms = utime.ticks_ms()
+ except Exception:
+ self._vc_last_think_tone_ms = 0
+ self._vc_local_play("U:/static/think.mp3", 1, 1, 650)
+ except Exception as e:
+ usys.print_exception(e)
+
def _vc_long_dialogue_auto_uplink_ok(self, reason):
"""长对话自动/打断开麦:ASR/LLM 门控 + 双轨防抖(自动 1200ms / 用户打断 450ms)。"""
try:
self._vc_is_long_dialogue_mode()
and reason == "completed"
and self._vc_effective_asr_text()
+ and getattr(self, "_turn_has_asr_result", 0) == 1
and (
getattr(self, "_turn_has_vad_silence", 0) == 1
or getattr(self, "_turn_has_asr_completed", 0) == 1
)
):
- # é\95¿å¯¹è¯\9dï¼\9aæ\9c\89æ\9c\89æ\95\88 ASR ç\9a\84 completed æ\89\8dè\87ªå\8a¨ä¸\8bä¸\80è½®ï¼\88空 ASR ä¸\8dæ\8a¢å¼\80麦ï¼\89
+ # é\95¿å¯¹è¯\9dï¼\9aæ\9c¬è½®æ\9c\89 asr.result ç\9a\84 completed æ\89\8dè\87ªå\8a¨ä¸\8bä¸\80è½®
pass
else:
print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))
return False
+ if reason == "completed":
+ if getattr(self, "_vc_llm_without_tts_round", 0) == 1:
+ print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts_pending")
+ return False
+ if getattr(self, "_turn_has_asr_result", 0) != 1 and self._vc_effective_asr_text():
+ print("[VC][uplink_ctrl] Deny START by completed cause=stale_asr_text")
+ return False
if reason == "completed" and getattr(self, "_turn_has_tts_started", 0) == 0:
if getattr(self, "_llm_completed_ms", 0) or getattr(
self.fsm, "turn", None
) == VCTurnState.Thinking:
print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts")
+ try:
+ if hasattr(self, "_turn_guard"):
+ self._turn_guard.on_llm_without_tts_completed()
+ else:
+ self._vc_arm_await_late_tts(reason)
+ except Exception:
+ pass
try:
if self._vc_is_long_dialogue_mode() and getattr(self, "_vc_session_started", 0) == 1:
self._long_dialogue_publish_listening("completed/llm_without_tts")
if getattr(self, "record_data_save_flag", 0) == 0:
self._server_vad_silence = 0
+ try:
+ if (
+ reason == "canceled"
+ and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
+ and hasattr(self, "_vc_interrupt_cancel_uplink_busy")
+ and self._vc_interrupt_cancel_uplink_busy()
+ ):
+ print("[VC][uplink_ctrl] skip duplicate canceled uplink (tone/open in progress)")
+ return
+ except Exception:
+ pass
print("[VC][uplink_ctrl] Request START by {}".format(reason))
try:
if (
try:
if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
print("[VC][uplink_ctrl] interrupt -> start (canceled)")
+ try:
+ self._vc_interrupt_record_start_tone = 1
+ self._vc_interrupt_cancel_uplink_armed = 1
+ except Exception:
+ pass
+ except Exception:
+ pass
+ try:
+ r = str(reason or "")
+ if self._vc_is_long_dialogue_mode() and (
+ r == "canceled"
+ or r.startswith("canceled/")
+ or r.startswith("asr.empty/recover")
+ ):
+ if not self._vc_in_interrupt_asr_recover():
+ self._vc_arm_interrupt_asr_recover(r)
except Exception:
pass
self._vc_uplink_start_recording(reason)
self._uplink_active = 1
self._uplink_bytes = 0
self._uplink_start_ms = utime.ticks_ms()
- 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_mark_end(self, reason=""):
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))
+ try:
+ self._vc_last_uplink_dur_ms = int(dur) if dur is not None and dur >= 0 else -1
+ except Exception:
+ self._vc_last_uplink_dur_ms = -1
except Exception:
pass
self._uplink_active = 0
elif event == 7:
self.audio_play_state = 0
#EventMesh.publish("set_audio_state", 0)
- print('audio-play finish.!!!!!!')
try:
- if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
- self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")
- return
+ if getattr(self, "VC_mode", -1) == 1:
+ t = getattr(self.fsm, "turn", None)
+ if getattr(self, "_ptt_await_server", 0) or t in (
+ VCTurnState.Thinking, VCTurnState.TtsReceiving,
+ ):
+ self._vc_publish_led("thinking", "audio_cb/ptt")
+ return
except Exception:
pass
try:
def VC_Speech_Start_now(self):
# 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
self._server_vad_silence = 0
- self.VC_Speech_Start()
+ # intr_recover 已在 cancel 开麦 uplink_request_start 时 arm,勿重复
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ self.VC_Speech_Start()
if self.VC_record_state_flag == 1:
self.VC_record_state_flag =2
try:
self.VC_record_state_flag = 1
self.VC_Speech_End()#关闭录音
usys.print_exception(e)
- def VC_Speech_End(self):
+ def VC_Speech_End(self, ptt_silent=False):
try:
self._uplink_mark_end("VC_Speech_End")
self.AI_ASR_flag = 0
self.AI_record_tmp_queue.clear()
self.VC_record_state_flag = 0
+ try:
+ if getattr(self.fsm, "speech_uplink_open", 0) == 1:
+ self.fsm.speech_uplink_open = 0
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_mode", 0) == 0:
+ keep_drop = False
+ try:
+ g = getattr(self, "_turn_guard", None)
+ keep_drop = g.should_preserve_tts_drop_window() if g else False
+ except Exception:
+ keep_drop = bool(
+ getattr(self, "_drop_tts_audio", 0)
+ or getattr(self, "is_ruppt_tts", 0)
+ )
+ if not keep_drop:
+ self.is_ruppt_tts = 0
+ self._drop_tts_audio = 0
+ if getattr(self, "_intr", None) is not None:
+ try:
+ self._intr.drop_tts = 0
+ self._intr.ruppt_tts = 0
+ except Exception:
+ pass
+ except Exception:
+ pass
# PTT 一轮录音结束:清长按语义,避免后续短按松手误走 end_speech 而不播提示音
try:
- if getattr(self, "VC_mode", -1) == 1:
+ if getattr(self, "VC_mode", -1) == 1 and not ptt_silent:
self.AI_key_long_press_flag = 0
except Exception:
pass
if getattr(self, "VC_mode", -1) == 1:
self._ptt_led_lock_listening = 0
self._ptt_interrupt_active = 0
- self._ptt_await_server = 1
- self._vc_publish_led("thinking", "VC_Speech_End/ptt")
+ if ptt_silent:
+ # ptt_begin 清理残留录音:用户仍按住,勿进思考态/勿清长按
+ self._ptt_await_server = 0
+ else:
+ self._ptt_await_server = 1
+ self._vc_publish_led("thinking", "VC_Speech_End/ptt")
else:
self._ptt_await_server = 0
if (
return
except Exception:
pass
- if tts_str != "":
- self.AI_TTS_str = tts_str
+ k = ""
+ try:
+ if len(data) > 2 and data[2]:
+ k = str(data[2])
+ self._pending_tts_kind = k
+ self._last_tts_kind = k
+ except Exception:
+ pass
+ if not k:
+ k = str(getattr(self, "_pending_tts_kind", "") or "")
+ ms_hint = (tts_str or "").strip()
+ if k in ("mode_switch_long", "mode_switch_ptt"):
+ if not ms_hint:
+ ms_hint = str(getattr(self, "_vc_ms_pending_tts_str", "") or "").strip()
+ if not ms_hint:
+ ms_hint = (
+ "已切换到单对话模式" if int(getattr(self, "VC_mode", 0)) == 1
+ else "已切换到长对话模式"
+ )
+ if ms_hint:
+ self.AI_TTS_str = ms_hint
if is_close_VC != None:
self.Close_flag = is_close_VC
try:
- k = getattr(self, "_pending_tts_kind", "") or ""
if k in ("mode_switch_long", "mode_switch_ptt"):
self._vc_ms_phase = 2
self._vc_ms_play_gen = int(getattr(self, "_vc_ms_active_gen", 0))
+ self._skip_started_uplink_once = 1
except Exception:
pass
if self.AI_VC_send_data_queue.empty()!=True:
if self.AI_ASR_send_data_queue.empty()!=True:
self.AI_ASR_send_data_queue.clear()
try:
- if is_close_VC:
- self._vc_publish_led("idle", "play_tts_cb")
+ ptt_mode = int(getattr(self, "VC_mode", 0)) == 1
+ notify_kind = k in (
+ "prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt",
+ "find_device", "asr_empty_hint",
+ )
+ # 单对话 notify:设计稿仅 ASR complete 后亮思考灯;提示音/模式切换保持 idle
+ if is_close_VC or ptt_mode or notify_kind:
+ prefer_off = 1 if (ptt_mode or k in ("prompt", "boot_prompt")) else 0
+ self._vc_publish_led(
+ "idle", "play_tts_cb/notify", prefer_off=prefer_off)
else:
self._vc_publish_led("thinking", "play_tts_cb")
except Exception:
pass
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_tts_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
+ try:
+ if k in ("mode_switch_long", "mode_switch_ptt") and ms_hint:
+ self.AI_TTS_str = ms_hint
+ self._pending_tts_kind = k
+ self._last_tts_kind = k
+ except Exception:
+ pass
self.AI_VC_start()
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
if self._vc_led_state_debug_enabled():
print("[VC] VC_Play_Start skip (ptt_interrupt)")
return
+ if hasattr(self, "_long_dialogue_is_stale_server_tts") and self._long_dialogue_is_stale_server_tts():
+ try:
+ print("[VC] VC_Play_Start skip (long_recording/stale_tts)")
+ except Exception:
+ pass
+ return
if hasattr(self, "_long_dialogue_should_drop_inbound_tts") and self._long_dialogue_should_drop_inbound_tts():
if self._vc_led_state_debug_enabled():
print("[VC] VC_Play_Start skip (long_interrupt)")
pass
if not ready:
+ try:
+ if (
+ getattr(self.fsm, "await_played", False)
+ and getattr(self, "play_pcm", None) is None
+ and getattr(self, "VC_Play_flag", 0) == 0
+ and (
+ self._long_dialogue_is_stale_server_tts()
+ or getattr(self, "VC_record_state_flag", 0) != 0
+ )
+ ):
+ self.fsm.await_played = False
+ try:
+ self.fsm.mark_tts_rx_idle()
+ if getattr(self.fsm, "session", None) == VCSessionState.Started:
+ self.fsm.turn = VCTurnState.Listening
+ except Exception:
+ pass
+ print("[TTS] play_end_skip (no play_pcm, recording/stale)")
+ return False
+ except Exception:
+ pass
try:
if not getattr(self, "_tts_play_check_log_ms", 0) or utime.ticks_diff(
now, self._tts_play_check_log_ms
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_single_voice_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
self.play_new_voice_flag = 1
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_new_voice_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
self.play_new_voice_flag = 1
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_his_voice_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
self.play_new_voice_flag = 1
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_wait_voice_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
self.play_new_voice_flag = 1
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_music_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
self.play_new_voice_flag = 1
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
EventMesh.publish("set_led_state", {"ai_state": "thinking"})
self.AI_ASR_send_buf_len = 0
self._uplink_request_stop("play_trans_tts_cb")
- self.websocket_deconnect_VC()
+ self.Close_reconnect()
self.AI_VC_start()
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
self.play_new_voice_flag = 1
-# -*- coding: utf-8 -*-\r
-"""持久化配置读写。"""\r
-from common import Abstract\r
-import EventMesh\r
-import ujson\r
-import ql_fs\r
-\r
-class getStorageDataManager(Abstract):#获取内存数据\r
- def __init__(self):\r
- self.config_data = {}\r
- check = ql_fs.path_exists("/usr/static/storage_file/config.json")\r
- if check == True:\r
- with open("/usr/static/storage_file/config.json","r") as fd:\r
- self.config_data = ujson.load(fd)\r
- else :\r
- self.config_data = {'old_time': 0, 'flip_sw': 0}\r
- pass\r
-\r
- def post_processor_after_instantiation(self):\r
- EventMesh.subscribe("set_storage_data", self.set_storage_data)\r
- EventMesh.subscribe("set_storage_config", self.set_storage_config)\r
- EventMesh.subscribe("get_storage_data", self.get_storage_data)\r
- EventMesh.subscribe("get_storage_config", self.get_storage_config)\r
-\r
- def set_storage_data(self, event, msg): \r
- # EventMesh.debug_log("set_storage_data:{}".format(msg))\r
- if msg != None:\r
- self.config_data = msg\r
- with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:\r
- ujson.dump(self.config_data,fd)\r
- \r
-\r
- def set_storage_config(self,event,msg): \r
- # EventMesh.debug_log("set_storage_key_data1:{}".format(msg))\r
- if len(msg) >= 2:\r
- self.config_data[msg[0]]=msg[1]\r
- # EventMesh.debug_log("set_storage_key_data2:{}".format(self.config_data[msg[0]]))\r
- EventMesh.publish("publish_message_event_ui",{"func":self.set_storage_save,"data":0})\r
- def set_storage_save(self, data): \r
- with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:\r
- ujson.dump(self.config_data,fd)\r
- def get_storage_data(self, event, msg): \r
- # EventMesh.debug_log("get_storage_data:{}".format(self.config_data))\r
- return self.config_data \r
- def get_storage_config(self,event,msg):\r
- # EventMesh.debug_log("get_storage_key_data1:{}".format(msg))\r
- b = msg in self.config_data.keys()\r
- if b == True:\r
- # EventMesh.debug_log("get_storage_key_data2:{}".format(self.config_data[msg]))\r
- return self.config_data[msg]\r
- else :\r
- return None\r
- # try:\r
- # return self.config_data[msg]\r
- # except:\r
- # EventMesh.debug_log("get_storage_key_data:none key:{}",msg)\r
- # return None\r
+# -*- coding: utf-8 -*-
+"""持久化配置读写。"""
+from common import Abstract
+import EventMesh
+import ujson
+import ql_fs
+
+class getStorageDataManager(Abstract):#获取内存数据
+ def __init__(self):
+ self.config_data = {}
+ check = ql_fs.path_exists("/usr/static/storage_file/config.json")
+ if check == True:
+ with open("/usr/static/storage_file/config.json","r") as fd:
+ self.config_data = ujson.load(fd)
+ else :
+ self.config_data = {'old_time': 0, 'flip_sw': 0}
+ pass
+
+ def post_processor_after_instantiation(self):
+ EventMesh.subscribe("set_storage_data", self.set_storage_data)
+ EventMesh.subscribe("set_storage_config", self.set_storage_config)
+ EventMesh.subscribe("get_storage_data", self.get_storage_data)
+ EventMesh.subscribe("get_storage_config", self.get_storage_config)
+
+ def set_storage_data(self, event, msg):
+ # EventMesh.debug_log("set_storage_data:{}".format(msg))
+ if msg != None:
+ self.config_data = msg
+ with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:
+ ujson.dump(self.config_data,fd)
+
+
+ def set_storage_config(self,event,msg):
+ # EventMesh.debug_log("set_storage_key_data1:{}".format(msg))
+ if len(msg) >= 2:
+ self.config_data[msg[0]]=msg[1]
+ if msg[0] in ("VC_mode", "powerRestart"):
+ self.set_storage_save(0)
+ else:
+ EventMesh.publish("publish_message_event_ui",{"func":self.set_storage_save,"data":0})
+ def set_storage_save(self, data):
+ with open("/usr/static/storage_file/config.json",'w+',encoding="utf-8") as fd:
+ ujson.dump(self.config_data,fd)
+ def get_storage_data(self, event, msg):
+ # EventMesh.debug_log("get_storage_data:{}".format(self.config_data))
+ return self.config_data
+ def get_storage_config(self,event,msg):
+ # EventMesh.debug_log("get_storage_key_data1:{}".format(msg))
+ b = msg in self.config_data.keys()
+ if b == True:
+ # EventMesh.debug_log("get_storage_key_data2:{}".format(self.config_data[msg]))
+ return self.config_data[msg]
+ else :
+ return None
+ # try:
+ # return self.config_data[msg]
+ # except:
+ # EventMesh.debug_log("get_storage_key_data:none key:{}",msg)
+ # return None