From: TimekeeperPL <1624952103@qq.com> Date: Sat, 30 May 2026 08:28:47 +0000 (+0800) Subject: 增加拼接字段 增加长对话25秒超时机制 X-Git-Url: http://git.snicker.com.cn/?a=commitdiff_plain;h=refs%2Fheads%2FJSWMV3;p=mirrors%2FASR1603_G5.git 增加拼接字段 增加长对话25秒超时机制 --- diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py index 39827e79..eed875f4 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/products/configs/k8/default_config.py @@ -158,6 +158,9 @@ VAD_SILENCE_DEFAULT = 1000 # 长对话:单轮最长录音(ms)、asr.completed 后续说 merge 窗(ms);0=不限制 LONG_DIALOGUE_MAX_TURN_MS = 25000 LONG_DIALOGUE_ASR_CONTINUE_MS = 2000 +# 与 start.interrupt_options 一致 +INTERRUPT_CONCATENATE_RESULTS = 1 +INTERRUPT_STRATEGY_ALWAYS_ALLOW = 1 # llm 已起后连续 canceled 达此次数则冻结续说、停麦等回复(避免一直录无 TTS) LONG_DIALOGUE_LLM_CANCEL_MAX = 2 diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_fsm.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_fsm.py index 7432d435..0beec43f 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_fsm.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_fsm.py @@ -232,7 +232,7 @@ class VCStateMachine: return actions if event == "canceled": self.on_server_canceled() - # canceled:仅长对话允许“立刻回到聆听并尝试开麦”(由 _uplink_request_start 门控保证安全幂等) + # canceled 开麦由 _uplink_request_start 门控(拼接/打断/merge) try: if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0: actions.append(("uplink_start", "canceled")) @@ -241,10 +241,15 @@ class VCStateMachine: return actions if event == "completed": self.on_server_completed() - # completed:若不在等待 audio.played(说明本轮无 TTS),仅长对话可直接进入下一轮聆听 + # completed:无 TTS 轮次才自动开麦;等对话 TTS 时由 local.play_end 开麦 try: if self.mode() == VCMode.Realtime and getattr(self.vc, "Close_flag", 0) == 0: - if (not getattr(self, "await_played", False)) and self.turn != VCTurnState.AwaitPlayedAck: + expect_tts = getattr(self.vc, "_vc_expect_dialog_tts", 0) == 1 + if ( + (not expect_tts) + and (not getattr(self, "await_played", False)) + and self.turn != VCTurnState.AwaitPlayedAck + ): actions.append(("uplink_start", "completed")) except Exception: pass diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_proto.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_proto.py index cf9e6662..d5e150d0 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_proto.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_proto.py @@ -201,12 +201,13 @@ def _convert_start_payload_v25_to_v3(payload): data["agent_id"] = agent_id io = dict(inb.get("interrupt_options") or {}) - if io.get("thinking_time_only"): - io["strategy"] = "thinking_time_only" - elif io.get("keyword_interrupt"): - io["strategy"] = "keyword" - if io.get("interrupt_keywords"): - io["interrupt_keywords"] = io["interrupt_keywords"] + if io.get("strategy") not in ("always_allow", "thinking_time_only", "keyword", "never_allow"): + if io.get("always_allow"): + io["strategy"] = "always_allow" + elif io.get("thinking_time_only"): + io["strategy"] = "thinking_time_only" + elif io.get("keyword_interrupt"): + io["strategy"] = "keyword" if io: data["interrupt_options"] = io @@ -455,12 +456,7 @@ def vc_proto_log_synthesis(text, pending_kind=""): return try: t = text or "" - print("[VC][proto] synthesis_tx ver={} text_len={} kind={} text={!r}".format( - get_protocol_version(), - len(t), - pending_kind or "-", - _text_preview(t, 64), - )) + print("[VC][p] syn tx l={} k={}".format(len(t), pending_kind or "-")) try: if is_v3(): wire = ujson.dumps({"type": "synthesis", "data": {"text": t}}) @@ -500,19 +496,15 @@ def vc_proto_log_tx_wire(wire_str, ws_conn=0): txt = pl.get("text") or "" except Exception: pass - print("[VC][proto] tx synthesis ver={} len={} ws={} text={!r}".format( - get_protocol_version(), len(wire_str), ws_conn, _text_preview(txt, 64))) + print("[VC][p] tx syn l={}".format(len(wire_str))) return if el in ("start",): - print("[VC][proto] start_wire_SENT ver={} wire_len={} ws={}".format( - get_protocol_version(), len(wire_str), ws_conn)) + print("[VC][p] tx start l={}".format(len(wire_str))) return if el in ("audio.create", "audio.completed", "cancel", "audio.played", "audio.convert"): - print("[VC][proto] tx event={} ver={} len={} ws={}".format( - ev, get_protocol_version(), len(wire_str), ws_conn)) + print("[VC][p] tx {} l={}".format(ev, len(wire_str))) return - print("[VC][proto] tx event={} ver={} len={} ws={}".format( - ev, get_protocol_version(), len(wire_str), ws_conn)) + print("[VC][p] tx {} l={}".format(ev, len(wire_str))) def vc_proto_normalize_server(js): diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py index 43b032da..76208d01 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"""语音对话编排:按键/PTT、AI_VC_start/Close、定时器与 FSM 编排。""" from vc_session_imports import * # noqa: F401,F403 import _thread from vc_session_io import VCSessionIoMixin @@ -20,7 +19,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): def _vc_fsm_log_snip(self): try: f = self.fsm - return "sess={} turn={} tts_rx={} await={} sp_up={}".format( + return "s={} t={} rx={} ap={} up={}".format( int(getattr(f, "session", -1)), int(getattr(f, "turn", -1)), int(getattr(f, "tts_rx_phase", -1)), @@ -128,6 +127,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): try: if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: return True + if ( + reason in ("canceled/continue_speech", "canceled") + and getattr(self, "_vc_intr_always", 1) + and getattr(self, "_vc_intr_concat", 1) + ): + return True # TTS 播完进入下一轮:play_end 时 PCM 尚未 close,勿用 pipeline_busy 拒开麦 if reason in ("local.play_end", "audio.played"): if getattr(self, "_vc_session_started", 0) == 1: @@ -142,7 +147,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): return True def _vc_reset_long_idle_timer(self, tag=""): - """长对话 L1:用户开麦/有效轮次活动则清零无 TTS 超时计数。""" try: if getattr(self, "VC_mode", -1) != 0: return @@ -150,15 +154,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass - def _vc_long_dialogue_defer_vad_stop(self): - try: - return ( - self._vc_is_long_dialogue_mode() - and getattr(self, "record_data_save_flag", 0) == 0 - ) - except Exception: - return False - def _vc_asr_continue_merge_ms(self): try: if "vc_config" in globals() and vc_config: @@ -167,14 +162,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): pass return 2000 - def _vc_ld_cancel_storm_max(self): - try: - if "vc_config" in globals() and vc_config: - return int(vc_config.get_int("LONG_DIALOGUE_LLM_CANCEL_MAX", 2)) - except Exception: - pass - return 2 - def _vc_ld_reset_merge_freeze(self): try: self._vc_ld_merge_frozen = 0 @@ -184,17 +171,30 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass - def _vc_ld_on_tts_started(self): + def _vc_ld_begin_new_speech_turn(self, reason=""): + if not self._vc_is_long_dialogue_mode(): + return try: - self._vc_ld_merge_frozen = 0 - self._vc_ld_cancel_in_wall = 0 - self._turn_has_llm_created = 0 - self._vc_ld_awaiting_reply = 0 + self._turn_has_asr_completed = 0 + self._turn_has_vad_silence = 0 + self._turn_has_asr_result = 0 + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 + self._vc_asr_completed_ms = 0 + self._vc_last_asr_result_ms = 0 + self._vc_defer_vad_uplink_stop = 0 + self._vc_vad_had_active_after_silence = 0 + self._server_vad_silence = 0 + self._vc_turn_cap_fired = 0 + self._vc_turn_cap_await_asr_deadline_ms = 0 + self._vc_turn_wall_start_ms = 0 + self._vc_turn_wall_had_asr_completed = 0 + self._vc_ld_reset_merge_freeze() + print("[VC][turn] new r={}".format(reason or "?")) except Exception: pass def _vc_ld_bump_cancel_in_wall(self): - """单轮墙钟内累计 canceled(不随 llm.created 清零);达上限强制提交。""" if not self._vc_is_long_dialogue_mode(): return False if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: @@ -204,13 +204,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): try: n = int(getattr(self, "_vc_ld_cancel_in_wall", 0)) + 1 self._vc_ld_cancel_in_wall = n - mx = self._vc_ld_cancel_storm_max() - if mx > 0 and n >= mx: + if n >= 2: self._vc_ld_merge_frozen = 1 self._vc_asr_continue_merge = 0 self._vc_pending_asr_completed_stop = 0 try: - print("[VC][cancel_storm] n={} max={} -> force commit".format(n, mx)) + print("[VC][cst] n={}/{}".format(n, mx)) except Exception: pass return True @@ -218,17 +217,89 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): pass return False + def _vc_turn_cap_await_asr_tick(self): + try: + if not self._vc_is_long_dialogue_mode(): + return + dl = getattr(self, "_vc_turn_cap_await_asr_deadline_ms", 0) + if not dl: + return + if ( + getattr(self, "_turn_has_asr_completed", 0) == 1 + and getattr(self, "_vc_turn_cap_fired", 0) != 1 + ): + self._vc_turn_cap_await_asr_deadline_ms = 0 + return + if utime.ticks_diff(utime.ticks_ms(), dl) < 0: + return + self._vc_turn_cap_await_asr_deadline_ms = 0 + try: + print("[VC][cap] asr wait to") + except Exception: + pass + if getattr(self, "VC_record_state_flag", 0) != 0: + try: + self._vc_uplink_send_silence_tail_ms(1000) + except Exception: + pass + self._server_vad_silence = 1 + try: + self._uplink_mark_end("max_turn_ms/asr_wait_to") + except Exception: + pass + self.request_uplink_stop("max_turn_ms/asr_wait_to") + except Exception: + pass + def _vc_ld_force_commit_turn(self, reason="force_commit"): - """停止续说开麦,关上行/停录,等云端用已有 ASR 走完 LLM→TTS。""" if not self._vc_is_long_dialogue_mode(): return try: self._vc_asr_continue_merge = 0 self._vc_pending_asr_completed_stop = 0 self._vc_defer_vad_uplink_stop = 0 + self._vc_ld_merge_frozen = 1 + try: + self._vc_uplink_flush_record_queue() + except Exception: + pass + rec = int(getattr(self, "VC_record_state_flag", 0)) + is_cap = str(reason or "") == "max_turn_ms" + wall_comp = getattr(self, "_vc_turn_wall_had_asr_completed", 0) == 1 + if is_cap: + self._vc_ld_awaiting_reply = 1 + if wall_comp: + self._turn_has_asr_completed = 0 + try: + print("[VC][cap] rechad") + except Exception: + pass + else: + try: + print("[VC][cap] stream_only") + except Exception: + pass + wait_asr = is_cap and rec >= 1 and wall_comp + if wait_asr: + try: + cap_wait = 3500 + self._vc_turn_cap_await_asr_deadline_ms = utime.ticks_add( + utime.ticks_ms(), cap_wait) + print("[VC][cap] wait_asr ms={}".format(cap_wait)) + except Exception: + self._vc_turn_cap_await_asr_deadline_ms = 0 + wait_asr = False + if is_cap and rec >= 1 and not wait_asr: + try: + self._vc_uplink_send_silence_tail_ms(1000) + except Exception: + pass self._server_vad_silence = 1 - self._vc_ld_awaiting_reply = 1 - if getattr(self, "VC_record_state_flag", 0) != 0: + try: + self._vc_reset_long_idle_timer(reason) + except Exception: + pass + if rec >= 1 and not wait_asr: try: self._uplink_mark_end(reason) except Exception: @@ -239,11 +310,14 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_publish_led("thinking", reason) except Exception: pass + try: + print("[VC][cap] vad_stop r={}".format(reason or "?")) + except Exception: + pass except Exception: pass def _vc_asr_continue_merge_active(self): - """asr.completed 后短窗内仍有 asr.result → 同轮续说 merge。""" if not self._vc_is_long_dialogue_mode(): return False try: @@ -262,37 +336,33 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: return False - def _vc_should_hold_mic_for_asr_continue(self): - # llm 已起:云端 barge-in 由 canceled+ASR 承接,端侧不再 hold 开麦 - if getattr(self, "_turn_has_llm_created", 0) == 1: - return False - if getattr(self, "_vc_vad_had_active_after_silence", 0) == 1: - return True - return self._vc_asr_continue_merge_active() - def _vc_note_asr_result_activity(self): try: - if getattr(self, "_vc_ld_merge_frozen", 0) == 1: + if ( + getattr(self, "_vc_ld_merge_frozen", 0) == 1 + or getattr(self, "_vc_turn_cap_fired", 0) == 1 + ): return self._vc_last_asr_result_ms = utime.ticks_ms() - # llm 已起后迟到的 asr.result 只更新文本,禁止 continue merge 再开上行 if getattr(self, "_turn_has_llm_created", 0) == 1: return if getattr(self, "_turn_has_asr_completed", 0) == 1: + if not getattr(self, "_vc_intr_concat", 1): + return self._vc_asr_continue_merge = 1 self._vc_pending_asr_completed_stop = 0 if getattr(self, "record_data_save_flag", 0) == 0: + rec = int(getattr(self, "VC_record_state_flag", 0)) + if rec == 0: + print("[VC][ASR] rx post stop") + return self._server_vad_silence = 0 self._vc_defer_vad_uplink_stop = 0 - try: - print("[VC][ASR] continue after completed -> resume uplink gate") - except Exception: - pass + print("[VC][ASR] cont rs={}".format(rec)) except Exception: pass def _vc_flush_deferred_vad_uplink_stop(self, reason="asr.completed"): - """长对话 asr.completed:vad.silence 句末立停;无 vad 则留 merge 窗再停麦。""" try: if not self._vc_is_long_dialogue_mode(): return @@ -300,7 +370,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): return if getattr(self, "_vc_vad_had_active_after_silence", 0) == 1: try: - print("[VC][VAD] asr.completed defer stop (vad pause merged)") + print("[VC][VAD] completed defer vad merge") except Exception: pass return @@ -313,7 +383,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_defer_vad_uplink_stop = 0 elif not self._vc_effective_asr_text(): return - if had_defer: + if had_defer or not getattr(self, "_vc_intr_concat", 1): self._server_vad_silence = 1 try: self._uplink_mark_end(reason) @@ -322,7 +392,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible(): self.request_uplink_stop(reason) return - # 无 vad.silence:先 silence_gate,merge 窗内若有新 asr.result 则恢复;超时再关麦 self._vc_pending_asr_completed_stop = 1 self._server_vad_silence = 1 try: @@ -333,7 +402,6 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): pass def _vc_asr_completed_stop_tick(self): - """merge 窗结束仍无续说 → 执行延后关麦。""" try: if not self._vc_is_long_dialogue_mode(): return @@ -349,7 +417,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_pending_asr_completed_stop = 0 if getattr(self, "VC_record_state_flag", 0) != 0 and self._vc_vad_uplink_stop_eligible(): try: - print("[VC][ASR] merge window end -> stop uplink") + print("[VC][ASR] merge end") except Exception: pass self.request_uplink_stop("asr.completed/merge_timeout") @@ -357,8 +425,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): pass def _long_dialogue_cancel_preserve_speech(self): - """长对话 canceled:非用户打断时,录音中或 asr 续说窗内均保持/恢复上行。""" - if not self._vc_is_long_dialogue_mode(): + if not self._vc_is_long_dialogue_mode() or not getattr(self, "_vc_intr_concat", 1): return False try: if getattr(self, "_vc_ld_merge_frozen", 0) == 1: @@ -367,53 +434,38 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): return False if getattr(self, "is_ruppt_tts", 0) == 1: return False - # llm 已起:达 cancel 上限后不再保录(由 cancel_storm 提交) - if getattr(self, "_turn_has_llm_created", 0) == 1: - if int(getattr(self, "_vc_ld_cancel_in_wall", 0)) >= self._vc_ld_cancel_storm_max(): - return False + if int(getattr(self, "_vc_ld_cancel_in_wall", 0)) >= 2: + return False if getattr(self, "VC_record_state_flag", 0) != 0: return True - if self._vc_asr_continue_merge_active(): - return True - if getattr(self, "_vc_asr_continue_merge", 0) == 1: + if self._vc_asr_continue_merge_active() or getattr(self, "_vc_asr_continue_merge", 0) == 1: return True + if getattr(self, "_vc_intr_always", 1) and self._vc_effective_asr_text(): + if ( + getattr(self, "_turn_has_asr_completed", 0) == 1 + or getattr(self, "_turn_has_llm_created", 0) == 1 + or getattr(self, "_turn_has_tts_started", 0) == 1 + ): + return True except Exception: pass return False - def _vc_ld_first_post_llm_cancel_barge(self): - """llm 已起后第 1 次 canceled(补半句):即使 merge 未置位也允许保录开麦。""" - if not self._vc_is_long_dialogue_mode(): - return False - if getattr(self, "_vc_ld_merge_frozen", 0) == 1: - return False - if getattr(self, "_turn_has_llm_created", 0) != 1: - return False - if int(getattr(self, "_vc_ld_cancel_in_wall", 0)) != 1: - return False - return bool(self._vc_effective_asr_text()) - - def _vc_ld_should_continue_speech_on_cancel(self): - if getattr(self, "_vc_ld_merge_frozen", 0) == 1: - return False - if self._vc_asr_continue_merge_active(): - return True - if getattr(self, "_vc_asr_continue_merge", 0) == 1: - return True - return self._vc_ld_first_post_llm_cancel_barge() - - def _long_dialogue_overlap_cancel_keep_recording(self): - return self._long_dialogue_cancel_preserve_speech() - def _long_dialogue_handle_continue_speech_cancel(self): - """canceled 但用户仍在说:清 TTS,保持/恢复录音与上行。""" try: self._drop_tts_audio = 1 + try: + self._vc_hold_tts_consumer_for_interrupt() + except Exception: + pass 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: + preserve_tone = self._vc_should_preserve_local_tone_on_mute() + self._ptt_hard_mute_local_playback(skip_aud_stop=preserve_tone) + if preserve_tone: + self.VC_Play_flag = 0 + elif 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 @@ -425,85 +477,87 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._server_vad_silence = 0 self._vc_pending_asr_completed_stop = 0 try: - print("[KEY] long_dialogue canceled -> keep/restore speech") + print("[KEY] ld canceled keep speech") except Exception: pass except Exception: pass - def _vc_long_dialogue_max_turn_ms(self): - """长对话单轮录音墙钟上限(ms);0=不限制。merge 续说期间累计,不因 silence_gate 清零。""" - try: - if "vc_config" in globals() and vc_config: - return int(vc_config.get_int("LONG_DIALOGUE_MAX_TURN_MS", 25000)) - except Exception: - pass - return 25000 - - def _vc_turn_wall_clear(self): + def _vc_turn_wall_clear(self, tag="", reset_merge=0): try: + if tag and not self._vc_is_long_dialogue_mode(): + return + had = tag and ( + getattr(self, "_vc_turn_wall_start_ms", 0) + or getattr(self, "_vc_turn_cap_fired", 0) + ) self._vc_turn_wall_start_ms = 0 + self._vc_turn_wall_had_asr_completed = 0 self._vc_turn_cap_fired = 0 - if hasattr(self, "_vc_ld_reset_merge_freeze"): + self._vc_turn_cap_await_asr_deadline_ms = 0 + if had and tag: + print("[VC][cap] clr r={}".format(tag)) + if reset_merge: + if getattr(self, "_vc_ld_awaiting_reply", 0) == 1: + self._vc_ld_merge_frozen = 0 + return self._vc_ld_reset_merge_freeze() except Exception: pass - def _vc_turn_wall_arm_if_needed(self): - """真开录时启动单轮墙钟;句中 vad.active 续说不重置。""" - try: - if not self._vc_is_long_dialogue_mode(): - return - if getattr(self, "_vc_turn_wall_start_ms", 0): - return - self._vc_turn_wall_start_ms = utime.ticks_ms() - self._vc_turn_cap_fired = 0 - except Exception: - pass - def _vc_long_dialogue_turn_cap_tick(self): - """录音线程周期检查:超 LONG_DIALOGUE_MAX_TURN_MS 强制停录提交(方案2+上限)。""" try: if not self._vc_is_long_dialogue_mode(): return + self._vc_turn_cap_await_asr_tick() if getattr(self, "record_data_save_flag", 0) != 0: return - if getattr(self, "VC_record_state_flag", 0) < 2: - return if getattr(self, "_vc_turn_cap_fired", 0) == 1: return - max_ms = self._vc_long_dialogue_max_turn_ms() + if getattr(self, "_turn_has_llm_created", 0) == 1: + return + rec = int(getattr(self, "VC_record_state_flag", 0)) + if rec < 1: + return + max_ms = 25000 + try: + if "vc_config" in globals() and vc_config: + max_ms = int(vc_config.get_int("LONG_DIALOGUE_MAX_TURN_MS", 25000)) + except Exception: + pass if max_ms <= 0: return start = getattr(self, "_vc_turn_wall_start_ms", 0) + if not start and rec >= 1: + self._vc_turn_wall_start_ms = utime.ticks_ms() + self._vc_turn_wall_had_asr_completed = 0 + start = self._vc_turn_wall_start_ms if not start: return - if utime.ticks_diff(utime.ticks_ms(), start) < max_ms: + elapsed = utime.ticks_diff(utime.ticks_ms(), start) + if elapsed < max_ms: + return + # 真开上行后才提交;仅硬件缓冲(state==1)则继续等到 uplink 或超时 + if rec < 2 and getattr(self, "_uplink_active", 0) != 1: return self._vc_turn_cap_fired = 1 - self._vc_pending_asr_completed_stop = 0 - self._vc_asr_continue_merge = 0 - self._vc_ld_merge_frozen = 1 + self._vc_vad_had_active_after_silence = 0 + up_b_seg = int(getattr(self, "_uplink_bytes", 0) or 0) + no_comp = getattr(self, "_turn_has_asr_completed", 0) != 1 try: - print("[VC][turn_cap] force stop elapsed_ms>={} max_ms={}".format( - utime.ticks_diff(utime.ticks_ms(), start), max_ms)) + print("[VC][cap] stop el>={}".format(elapsed)) except Exception: pass - self._vc_defer_vad_uplink_stop = 0 - self._vc_vad_had_active_after_silence = 0 - self._server_vad_silence = 1 + self._vc_ld_force_commit_turn("max_turn_ms") try: - self._uplink_mark_end("max_turn_ms") + print("[VC][cap] el={} no={} b={}".format( + elapsed, 1 if no_comp else 0, up_b_seg)) except Exception: pass - self.request_uplink_stop("max_turn_ms") - if hasattr(self, "_vc_ld_force_commit_turn"): - self._vc_ld_force_commit_turn("max_turn_ms") except Exception: pass def _long_dialogue_media_busy(self): - """长对话:TTS/思考/播报中(按键须走打断,勿误走 continue)。""" try: if self._vc_tts_playback_active() or self._vc_tts_pipeline_busy(): return True @@ -555,8 +609,21 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: return False + def _long_dialogue_is_stale_tts_started(self): + """打断等 canceled 期间的迟到 tts.started(不含 settled 尾包窗)。""" + if not self._vc_is_long_dialogue_mode(): + return False + try: + if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: + return True + if getattr(self, "is_ruppt_tts", 0) == 1 and getattr(self, "_drop_tts_audio", 0) == 1: + return True + except Exception: + pass + return False + def _long_dialogue_should_drop_inbound_tts(self): - """长对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。""" + """长对话打断窗口:丢弃迟到 TTS PCM(含 settle 后尾包窗)。""" if not self._vc_is_long_dialogue_mode(): return False try: @@ -574,9 +641,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): return False def _long_dialogue_on_server_tts_started(self): - """新一轮服务端 TTS:结束打断丢弃窗口,允许 VC_Play_Start 与下行写入。""" if not self._vc_is_long_dialogue_mode(): return + try: + self._vc_turn_wall_clear("tts.started") + except Exception: + pass try: if getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: return @@ -609,11 +679,18 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): """仅清 TTS/播放下行,保持录音与蓝慢闪。""" try: self._drop_tts_audio = 1 + try: + self._vc_hold_tts_consumer_for_interrupt() + except Exception: + pass 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: + preserve_tone = self._vc_should_preserve_local_tone_on_mute() + self._ptt_hard_mute_local_playback(skip_aud_stop=preserve_tone) + if preserve_tone: + self.VC_Play_flag = 0 + elif 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 @@ -684,8 +761,35 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass return True + + def _vc_hold_tts_consumer_for_interrupt(self): + try: + if getattr(self, "AI_audio_queue_thread_id", 0) == 0: + return + self.AI_audio_queue_thread_lock = 2 + for _ in range(80): + if getattr(self, "AI_audio_queue_thread_id", 0) == 0: + break + utime.sleep_ms(1) + except Exception: + pass + + def _vc_should_preserve_local_tone_on_mute(self): + try: + if getattr(self, "VC_record_state_flag", 0) == 0: + return False + if not self._vc_local_play_busy(): + return False + return True + except Exception: + return False + def _vc_stop_downlink_for_interrupt(self, reason="interrupt"): """详设 DES-SCN-003:打断/取消时先本地停播清队列,再允许开麦。""" + try: + self._vc_hold_tts_consumer_for_interrupt() + except Exception: + pass try: if getattr(self, "AI_audio_queue", None) is not None: if self.AI_audio_queue.empty() != True: @@ -697,20 +801,28 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._drop_tts_audio = 1 except Exception: pass + preserve_tone = False try: - self._ptt_hard_mute_local_playback() + preserve_tone = self._vc_should_preserve_local_tone_on_mute() except Exception: pass try: - if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None: - self.VC_Play_End() + self._ptt_hard_mute_local_playback(skip_aud_stop=preserve_tone) except Exception: pass try: - self._vc_local_play_active = 0 - self._vc_local_play_until_ms = 0 + if preserve_tone: + self.VC_Play_flag = 0 + elif getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None: + self.VC_Play_End() except Exception: pass + if not preserve_tone: + try: + self._vc_local_play_active = 0 + self._vc_local_play_until_ms = 0 + except Exception: + pass try: self.fsm.await_played = False self._need_audio_played_ack = 0 @@ -1419,6 +1531,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): try: if getattr(self, "_vc_ld_awaiting_reply", 0) == 1: return False + if getattr(self, "_vc_turn_cap_fired", 0) == 1: + return False if getattr(self, "_vc_defer_listening_led", 0) == 1: return False if getattr(self, "VC_mode", -1) != 0: @@ -1437,6 +1551,10 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): """长对话 TTS/思考中短按:停播 + cancel;canceled(或 cancel 超时)后开麦。""" if not self._vc_is_long_dialogue_mode(): return + try: + self._vc_turn_wall_clear("key.interrupt") + except Exception: + pass try: self._vc_turn_bump("long_interrupt") self.Close_flag = 0 @@ -1597,10 +1715,41 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass def _vc_restart_record_same_dialog(self): - """长对话 recording+短按:停录后立即重开(旧 sid,对齐设计稿)。""" if not self._vc_is_long_dialogue_mode(): return False try: + now = utime.ticks_ms() + last = getattr(self, "_vc_ld_restart_last_ms", 0) + if last and utime.ticks_diff(now, last) < 1500: + try: + print("[KEY] restart debounce") + except Exception: + pass + return True + self._vc_ld_restart_last_ms = now + rec = int(getattr(self, "VC_record_state_flag", 0)) + if getattr(self, "AI_client_VC_isconnect", 0) == 1: + try: + self.AI_client_VC_send( + ujson.dumps({"event": "cancel", "payload": {}})) + print("[KEY] restart cancel") + except Exception: + pass + if rec >= 1: + try: + self._vc_uplink_flush_record_queue() + except Exception: + pass + try: + self._vc_uplink_send_silence_tail_ms(1000) + except Exception: + pass + self._vc_ld_begin_new_speech_turn("key.restart") + self.AI_ASR_str = "" + self.AI_ASR_str_tmp = "" + self.fsm.reset_rx_phases() + if getattr(self.fsm, "session", None) == VCSessionState.Started: + self.fsm.turn = VCTurnState.Listening self.Close_flag = 0 self._skip_started_uplink_once = 0 self._vc_reset_long_idle_timer("key.restart") @@ -1814,7 +1963,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._intr.ruppt_tts = 0 except Exception: pass - def _ptt_hard_mute_local_playback(self): + def _ptt_hard_mute_local_playback(self, skip_aud_stop=False): try: self.VC_Play_flag = 0 if getattr(self, "VC_play_TYPE", None) == VC_play_TYPE_PCM: @@ -1825,11 +1974,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass self.play_pcm = None - try: - self.aud.stopPlayStream() - self.aud.stopAll() - except Exception: - pass + if not skip_aud_stop: + try: + self.aud.stopPlayStream() + self.aud.stopAll() + except Exception: + pass if getattr(self, "AI_audio_queue", None) is not None: try: if self.AI_audio_queue.empty() != True: @@ -2222,7 +2372,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): """ imperative 灯态意图;由 Presenter 与 latch 合并后单次下发。""" try: if self._vc_led_state_debug_enabled(): - print("[LED] publish intent ai={} reason={} VC_mode={} turn={}".format( + print("[LED] ai={} r={}".format( ai_state, reason, getattr(self, "VC_mode", -1), getattr(getattr(self, "fsm", None), "turn", "?"))) except Exception: @@ -2257,7 +2407,15 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): if not a: continue if a[0] == "uplink_stop": - self._uplink_request_stop(a[1] if len(a) > 1 else "") + _stop_reason = a[1] if len(a) > 1 else "" + if ( + _stop_reason == "tts.started" + and self._vc_is_long_dialogue_mode() + and hasattr(self, "_long_dialogue_should_drop_inbound_tts") + and self._long_dialogue_should_drop_inbound_tts() + ): + continue + self._uplink_request_stop(_stop_reason) elif a[0] == "uplink_start": self._uplink_request_start(a[1] if len(a) > 1 else "") elif a[0] == "play_start": @@ -2266,6 +2424,12 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): if self._vc_led_state_debug_enabled(): print("[VC][serial] skip play_start (ptt_interrupt)") continue + if ( + self._vc_is_long_dialogue_mode() + and hasattr(self, "_long_dialogue_should_drop_inbound_tts") + and self._long_dialogue_should_drop_inbound_tts() + ): + continue try: self.fsm.on_local_play_start() except Exception: @@ -2396,13 +2560,13 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_ms_close_pending = 0 self._vc_ms_key_block_until_ms = 0 self._vc_close_reason = "" - # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频 - # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪) self._server_vad_silence = 0 self._vc_defer_vad_uplink_stop = 0 self._vc_vad_had_active_after_silence = 0 self._vc_turn_wall_start_ms = 0 + self._vc_turn_wall_had_asr_completed = 0 self._vc_turn_cap_fired = 0 + self._vc_turn_cap_await_asr_deadline_ms = 0 self._vc_asr_completed_ms = 0 self._vc_last_asr_result_ms = 0 self._vc_asr_continue_merge = 0 @@ -2764,6 +2928,14 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_local_play_until_ms = 0 self._vc_pending_interrupt_uplink = 0 # 长对话短按打断:等 canceled 再开麦 self._long_dialogue_interrupt_settled_ms = 0 # 打断开麦完成时刻(过滤迟到 canceled) + self._vc_intr_concat = 1 + self._vc_intr_always = 1 + try: + if "vc_config" in globals() and vc_config: + self._vc_intr_concat = vc_config.get_int("INTERRUPT_CONCATENATE_RESULTS", 1) + self._vc_intr_always = vc_config.get_int("INTERRUPT_STRATEGY_ALWAYS_ALLOW", 1) + except Exception: + pass self.websocket_error_count = 0 self.AI_new_voice_url = "" #未读语音地址/音乐播放地址/闹钟播放标志 self.music_play_flag = 0 #音乐播放标志 @@ -3524,6 +3696,11 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._vc_tick_cancel_deadline() except Exception: pass + try: + if self._vc_is_long_dialogue_mode(): + self._vc_long_dialogue_turn_cap_tick() + except Exception: + pass _bg_active = False try: @@ -3554,17 +3731,15 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): except Exception: pass try: - print("[VC][idle_L1] 长对话无TTS反馈超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={} {}".format( - self.close_timerout, idle_sec, self.VC_mode, - getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1), - self._vc_fsm_log_snip())) + print("[VC][L1] no_tts {}s/{} {}".format( + self.close_timerout, idle_sec, self._vc_fsm_log_snip())) except Exception: pass try: print("[VC][idle_L1] 处理=Close(无TTS反馈进idle,需再次按键进入)") except Exception: pass - print("[VC][long_idle] branch=no_tts_timeout -> Close") + print("[VC][L1] Close") try: self._vc_idle_l1_close = 1 except Exception: @@ -3597,9 +3772,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): self._idle_deep_sec += 1 if active_deep and self._idle_deep_sec > deep_sec: try: - print("[VC][idle_L2] 业务idle累计超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={}".format( - self._idle_deep_sec, deep_sec, self.VC_mode, - getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1))) + print("[VC][L2] deep {}s/{}".format(self._idle_deep_sec, deep_sec)) except Exception: pass try: @@ -3838,9 +4011,8 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): pass try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] Close() enter VC_mode={} {} VC_satrt_flag={} VC_record_state_flag={}".format( - getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), - getattr(self, "VC_satrt_flag", -1), getattr(self, "VC_record_state_flag", -1))) + print("[VC][li] Close m={} {}".format( + getattr(self, "VC_mode", -1), self._vc_fsm_log_snip())) except Exception: pass self.VC_stop_play_audio_flag = 0 @@ -3898,7 +4070,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] step=set_led_state_idle (first)") + print("[VC][li] led idle1") except Exception: pass EventMesh.publish("set_led_state", {"ai_state": "idle", "prefer_off": 1}) @@ -3918,7 +4090,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): # Close 可能被不同线程触发:统一切回主线程执行 stop try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] step=schedule_uplink_stop reason=Close (async publish_AI_btnInfo)") + print("[VC][li] uplink stop") except Exception: pass try: @@ -3932,7 +4104,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] step=websocket_deconnect_VC") + print("[VC][li] ws dc") except Exception: pass try: @@ -3973,7 +4145,7 @@ class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract): #self.AI_WS_connect_Queue.put(self.AI_client_VC) try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] step=set_led_state_idle (second) -> VC_init VC_satrt_flag=0") + print("[VC][li] led idle2") except Exception: pass EventMesh.publish("set_led_state", {"ai_state": "idle"}) diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py index 31b96dba..fbfe7112 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_io.py @@ -1,5 +1,4 @@ # -*- coding: utf-8 -*- -"""AI_VC mixin:VC WebSocket 建链、收发线程、协议 JSON 分发。""" from vc_session_imports import * # noqa: F401,F403 from vc_notify import NotifyScheduler # import * 不导入以下划线开头的符号;_thread 必须显式引入(否则 start_new_thread NameError) @@ -30,13 +29,19 @@ class VCSessionIoMixin: pass return False + def _vc_is_silent_protocol_error(self, msg): + try: + low = str(msg or "").lower() + return ("idle too long" in low) or ("transcribe" in low) or ("asr error" in low) + except Exception: + return False + def _vc_error_tts_speakable(self, msg): - """协议 failed 是否走 synthesis;默认过滤技术错误,VC_ERROR_TTS_ENABLED=1 时全播。""" try: if msg is None: return None s = str(msg).strip() - if not s: + if not s or self._vc_is_silent_protocol_error(s): return None if self._vc_error_tts_enabled(): return s @@ -97,14 +102,26 @@ class VCSessionIoMixin: pass return if ev == "tts.started": + try: + if ( + self._vc_is_long_dialogue_mode() + and hasattr(self, "_long_dialogue_is_stale_tts_started") + and self._long_dialogue_is_stale_tts_started() + ): + try: + self._vc_stop_downlink_for_interrupt("stale_tts.started") + except Exception: + pass + return + except Exception: + pass try: self._vc_tts_rx_counters_reset() except Exception: pass if self._vc_is_long_dialogue_mode(): try: - if hasattr(self, "_vc_ld_on_tts_started"): - self._vc_ld_on_tts_started() + self._vc_ld_reset_merge_freeze() if hasattr(self, "_long_dialogue_on_server_tts_started"): self._long_dialogue_on_server_tts_started() except Exception: @@ -116,22 +133,15 @@ class VCSessionIoMixin: storm = False if hasattr(self, "_vc_ld_bump_cancel_in_wall"): storm = self._vc_ld_bump_cancel_in_wall() - if storm and hasattr(self, "_vc_ld_force_commit_turn"): + if storm: self._vc_ld_force_commit_turn("cancel_storm") elif ( hasattr(self, "_long_dialogue_cancel_preserve_speech") and self._long_dialogue_cancel_preserve_speech() - ) or ( - hasattr(self, "_vc_ld_first_post_llm_cancel_barge") - and self._vc_ld_first_post_llm_cancel_barge() ): keep_speech = True if keep_speech: - if ( - hasattr(self, "_vc_ld_should_continue_speech_on_cancel") - and self._vc_ld_should_continue_speech_on_cancel() - and hasattr(self, "_long_dialogue_handle_continue_speech_cancel") - ): + if hasattr(self, "_long_dialogue_handle_continue_speech_cancel"): self._long_dialogue_handle_continue_speech_cancel() elif hasattr(self, "_long_dialogue_handle_stale_cancel"): self._long_dialogue_handle_stale_cancel() @@ -180,7 +190,6 @@ class VCSessionIoMixin: self._vc_on_ptt_cancel_settled(via="canceled") def _vc_dispatch_server_event(self, ev, jsload_data=None): - """recv 线程:仅入编排队列,不在此修改 FSM。""" gen = self._vc_turn_current() self._vc_enqueue_btn( self._vc_orchestrator_server_event_cb, @@ -320,8 +329,8 @@ class VCSessionIoMixin: """play_weak_signal_tone=True 时播 xinhaoruo1(弱网/无卡类);连接/协议失败请传 False,避免误报「信号弱」。""" try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] websocket_error enter VC_mode={} TTS_str={} weak_hint={} -> Close then uplink_stop(async)".format( - getattr(self, "VC_mode", -1), TTS_str, play_weak_signal_tone)) + print("[VC][li] ws_err m={} w={}".format( + getattr(self, "VC_mode", -1), play_weak_signal_tone)) except Exception: pass print("websocket_error1!!!!") @@ -339,7 +348,7 @@ class VCSessionIoMixin: # 可能由收包线程/其它线程触发:统一切回主线程执行 stop,避免“异常→重试→再异常”的循环 try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] websocket_error schedule uplink_stop reason=websocket_error + set_led_idle") + print("[VC][li] ws_err stop") except Exception: pass EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"}) @@ -682,28 +691,32 @@ class VCSessionIoMixin: jsload_data = vc_proto_normalize_server(jsload_data) except Exception: pass + try: + _rev = jsload_data.get("event", "") + except Exception: + _rev = "" + _cap = int(getattr(self, "_vc_turn_cap_fired", 0)) + _await = int(getattr(self, "_vc_ld_awaiting_reply", 0)) if VC_WS_IO_LOG: print("[VC] recv raw:{}".format(recv_data)) - else: + elif _rev != "llm.delta": + try: + print("[VC] rx event={} sid={}".format( + _rev, jsload_data.get("sid", "") or "-")) + except Exception: + pass + if _cap or _await: try: - _rev = jsload_data.get("event", "") - if _rev != "llm.delta": - print("[VC] rx event={} sid={}".format( - _rev, jsload_data.get("sid", "") or "-")) + print("[VC][rx] ev={} n={} cap={} aw={}".format( + _rev or "?", len(recv_data), _cap, _await)) except Exception: pass - #print("AI_client_VC_recv jsload_data = {}".format(jsload_data)) try: self._cur_sid = jsload_data.get("sid", "") or "" except Exception: self._cur_sid = "" - try: - ev = jsload_data.get("event", "") - if ev: - self._vc_dispatch_server_event(ev, jsload_data) - except Exception: - pass - + if _rev: + self._vc_dispatch_server_event(_rev, jsload_data) if jsload_data["event"] == "started" :#会话开始 #payload = jsload_data["payload"] # self.VC_dialog_id = payload["dialog_id"] @@ -728,7 +741,7 @@ class VCSessionIoMixin: try: self._skip_started_uplink_once = 0 _skip_long_up = 1 - print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt") + print("[VC][up] Deny started boot_prompt") except Exception: pass print("VC:Started!!!!!!!!\r\n") @@ -799,8 +812,7 @@ class VCSessionIoMixin: self._turn_has_asr_result = 1 # 有效 ASR 后勿再执行空 ASR 提示音播完开麦 self._vc_reopen_uplink_after_empty_hint = 0 - if hasattr(self, "_vc_note_asr_result_activity"): - self._vc_note_asr_result_activity() + self._vc_note_asr_result_activity() except Exception: pass if self.AI_ASR_str == "":#没识别到 @@ -809,13 +821,18 @@ class VCSessionIoMixin: elif jsload_data["event"] == "asr.completed" :#asr 终止的标识 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") + if ( + self._vc_is_long_dialogue_mode() + and getattr(self, "_vc_turn_wall_start_ms", 0) + ): + self._vc_turn_wall_had_asr_completed = 1 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._vc_turn_cap_await_asr_deadline_ms = 0 + if getattr(self, "_vc_turn_cap_fired", 0) == 1: + self._vc_ld_merge_frozen = 0 + self._vc_ld_awaiting_reply = 1 except Exception: pass try: @@ -823,6 +840,11 @@ class VCSessionIoMixin: self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or "" 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") + except Exception: + pass # 本地 think 提示:仅在有有效 ASR 文本时播,避免空轮次连播「滴」 try: if getattr(self, "_asr_completed_tone_done", 0) == 1: @@ -856,11 +878,15 @@ class VCSessionIoMixin: self._asr_completed_tone_done = 1 elif vad_cont: try: - print("[VC][ASR] skip think tone (vad pause merged)") + print("[VC][ASR] skip think vad merge") except Exception: pass except Exception: pass + try: + self._vc_flush_deferred_vad_uplink_stop("asr.completed") + except Exception: + pass print("self.commands_mode:{}".format(self.commands_mode)) if self.commands_mode ==2: ret= self.check_text(self._vc_effective_asr_text()) @@ -905,11 +931,12 @@ class VCSessionIoMixin: except Exception: pass elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识 - # 长对话:句中停顿仅标记,等 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 上行 + if ( + self._vc_is_long_dialogue_mode() + and self.record_data_save_flag == 0 + ): self._vc_defer_vad_uplink_stop = 1 self._server_vad_silence = 1 try: @@ -932,6 +959,15 @@ class VCSessionIoMixin: elif jsload_data["event"] == "llm.created" :#思考(LLM) try: self._turn_has_llm_created = 1 + self._vc_ld_awaiting_reply = 0 + self._vc_turn_cap_fired = 0 + except Exception: + pass + try: + if self._vc_is_long_dialogue_mode(): + self._vc_asr_continue_merge = 0 + self._vc_pending_asr_completed_stop = 0 + self._vc_vad_had_active_after_silence = 0 except Exception: pass try: @@ -1164,6 +1200,23 @@ class VCSessionIoMixin: pass if err == "concurrency_limit_exceeded": EventMesh.publish("publish_AI_btnInfo",{"func":self.concurrency_limit_exceeded_tone}) + elif ( + getattr(self, "VC_mode", -1) == 0 + and ("idletimeout" in str(msg).lower() + or "idle timeout" in str(msg).lower()) + and ( + getattr(self, "_vc_ld_awaiting_reply", 0) == 1 + or getattr(self, "_vc_turn_cap_fired", 0) == 1 + or self._vc_effective_asr_text() + ) + ): + self._vc_ld_awaiting_reply = 0 + self._vc_turn_cap_fired = 0 + print("[VC][ASR] idle to skip Close") + try: + self._vc_publish_led("idle", "asr/idle_timeout") + except Exception: + pass else: speak = self._vc_error_tts_speakable(msg) EventMesh.publish( @@ -1287,6 +1340,11 @@ class VCSessionIoMixin: print("被打断不播放") utime.sleep_ms(1) except Exception as e: + if isinstance(recv_data, str): + print("[VC][rx] json_err n={} cap={} aw={}".format( + len(recv_data), + getattr(self, "_vc_turn_cap_fired", 0), + getattr(self, "_vc_ld_awaiting_reply", 0))) usys.print_exception(e) except Exception as e: @@ -1820,10 +1878,8 @@ class VCSessionIoMixin: "disable_vad":True, "vad_silence":self.vad_silence, "interrupt_options":{ - "concatenate_results":True, - "thinking_time_only": True, - #"keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断) - #"interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表 + "strategy": "always_allow", + "concatenate_results": True, }, "cfg": { "format": "pcm", diff --git a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py index 3915bb8b..aa79d547 100644 --- a/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py +++ b/2.sdk/project/7PIN_1_5/sdk/services/microPython/ports/quectel/core/vc_session_media.py @@ -143,50 +143,15 @@ class VCSessionMediaMixin: usys.print_exception(e) return False - def _vc_play_key_press_tone(self, via=""): - """已废弃:K8 无按键确认音;仅 RECORD_START_TONE(start.mp3)。配置缺省则不播。""" - try: - path = None - hold_ms = 300 - if "vc_config" in globals() and vc_config: - cfg = vc_config.get_attr("KEY_PRESS_TONE_FILE", None) - if cfg is not None and str(cfg).strip(): - path = str(cfg).strip() - hold_ms = int(vc_config.get_int("KEY_PRESS_TONE_DELAY_MS", hold_ms)) - if not path: - return False - print("[VC] key_press_tone via={} path={}".format(via or "-", path)) - self._vc_local_play(path, hold_ms=hold_ms) - return True - except Exception as e: - usys.print_exception(e) - return False - - def _vc_arm_key_press_tone(self): - try: - self._vc_pending_key_press_tone = 1 - except Exception: - pass - - def _vc_try_play_key_press_tone_when_ready(self, via=""): - try: - if not getattr(self, "_vc_pending_key_press_tone", 0): - return False - if getattr(self, "Close_flag", 0) == 1: - return False - if not self._vc_play_key_press_tone(via=via): - return False - self._vc_pending_key_press_tone = 0 - return True - except Exception: - return False - _vc_play_ai_key_record_start_tone = _vc_play_record_start_tone 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") + or r.startswith("BT_AI_key_press") + ): return True if r.startswith("ptt_begin") or r == "asr.started/defer": return True @@ -240,6 +205,19 @@ class VCSessionMediaMixin: self._vc_skip_local_play_stop_record = 0 except Exception: pass + try: + if getattr(self, "VC_mode", 0) == 0: + r = str(reason or "") + if r in ("local.play_end", "audio.played", "server.started"): + self._vc_ld_begin_new_speech_turn(r) + elif ( + r.startswith("key.") + and getattr(self, "_vc_pending_interrupt_uplink", 0) != 1 + and getattr(self, "is_ruppt_tts", 0) != 1 + ): + self._vc_ld_begin_new_speech_turn(r) + except Exception: + pass try: if getattr(self, "VC_record_state_flag", 0) == 1: self.VC_Speech_Start_now() @@ -257,8 +235,15 @@ class VCSessionMediaMixin: try: if getattr(self, "VC_mode", 0) == 0: self._long_dialogue_key_arm = 0 + # 打断开麦:须等 server.canceled 再 settle;否则迟到 tts.started 会抢播 if hasattr(self, "_long_dialogue_end_interrupt"): - self._long_dialogue_end_interrupt() + _intr_open = ( + getattr(self, "_vc_pending_interrupt_uplink", 0) == 1 + or getattr(self, "is_ruppt_tts", 0) == 1 + or getattr(self, "_drop_tts_audio", 0) == 1 + ) + if not _intr_open: + self._long_dialogue_end_interrupt() elif getattr(self, "VC_mode", -1) == 1: if hasattr(self, "_ptt_on_record_hardware_started"): self._ptt_on_record_hardware_started() @@ -280,11 +265,6 @@ class VCSessionMediaMixin: self._vc_user_record_entry_long(reason) def _vc_user_record_entry_long(self, reason=""): - """ - 长对话开麦: - - 按键/打断续聊:先 start.mp3,滴声结束后再上行+蓝慢闪 - - 自动续聊(server.started/play_end 等):不播 start,直接上行 - """ try: self._vc_user_record_open_reason = str(reason or "") self._vc_defer_listening_led = 1 @@ -331,9 +311,6 @@ class VCSessionMediaMixin: pass def _vc_user_record_entry_ptt(self, reason=""): - """ - 单对话 PTT 长按:立即录音+上行;按键开录时并行播 start.mp3;松手 end_speech 停传。 - """ try: self._vc_user_record_open_reason = str(reason or "") self._vc_defer_listening_led = 0 @@ -538,7 +515,14 @@ class VCSessionMediaMixin: return True except Exception: pass - print("[VC][uplink_ctrl] Deny START by canceled cause=not_user_interrupt") + print("[VC][up] Deny START by canceled cause=not_user_interrupt") + return False + if ( + reason == "completed" + and getattr(self, "_vc_expect_dialog_tts", 0) == 1 + and getattr(self, "_turn_has_tts_started", 0) == 0 + ): + print("[VC][up] Deny START by completed cause=await_tts") return False if ( reason == "canceled" @@ -563,13 +547,13 @@ class VCSessionMediaMixin: # 长对话:有有效 ASR 的 completed 才自动下一轮(空 ASR 不抢开麦) pass else: - print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason)) + print("[VC][up] Deny START by {} cause=empty_asr".format(reason)) 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") + print("[VC][up] Deny START by completed cause=llm_without_tts") 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") @@ -592,7 +576,7 @@ class VCSessionMediaMixin: last = getattr(self, last_attr, 0) if last and utime.ticks_diff(now, last) < debounce_ms: tag = "interrupt_debounce" if user_intr else "auto_debounce" - print("[VC][uplink_ctrl] Deny START by {} cause={} ({}ms)".format( + print("[VC][up] Deny START by {} cause={} ({}ms)".format( reason, tag, debounce_ms)) return False setattr(self, last_attr, now) @@ -615,7 +599,7 @@ class VCSessionMediaMixin: else: try: if reason in ("canceled", "local.play_end", "audio.played", "key.press/continue"): - print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason)) + print("[VC][up] Deny START by {} cause=Close_flag".format(reason)) except Exception: pass return @@ -646,7 +630,7 @@ class VCSessionMediaMixin: try: if not self._vc_capture_allowed(reason): try: - print("[VC][uplink_ctrl] Deny START by {} cause=playback_busy".format(reason)) + print("[VC][up] deny {} busy".format(reason)) except Exception: pass return @@ -658,9 +642,9 @@ class VCSessionMediaMixin: if getattr(self.fsm, "session", VCSessionState.Disconnected) != VCSessionState.Started: try: if reason in _key_reasons: - print("[VC][uplink_ctrl] Defer START by {} (wait server.started)".format(reason)) + print("[VC][up] defer {}".format(reason)) elif reason in ("canceled", "local.play_end", "audio.played"): - print("[VC][uplink_ctrl] Deny START by {} cause=fsm.session".format(reason)) + print("[VC][up] deny {} sess".format(reason)) except Exception: pass return @@ -669,46 +653,22 @@ class VCSessionMediaMixin: if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded): try: if reason in _key_reasons or reason in ("canceled", "local.play_end", "audio.played"): - print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn)) + print("[VC][up] deny {} t={}".format(reason, turn)) except Exception: pass return except Exception: return - # 若当前不在录音上行,才允许开新一轮 - if getattr(self, "VC_record_state_flag", 0) != 0: - # canceled 打断后:可能是僵死 flag=2(started 未真开麦),先停录复位再开麦 - if reason == "canceled": - try: - self._vc_clear_stale_record_state("uplink_start/canceled") - except Exception: - pass - if getattr(self, "VC_record_state_flag", 0) != 0: - try: - if reason in ("canceled", "local.play_end", "audio.played"): - print("[VC][uplink_ctrl] Deny START by {} cause=VC_record_state_flag({})".format( - reason, getattr(self, "VC_record_state_flag", 0))) - except Exception: - pass - return - - # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门) - if getattr(self, "record_data_save_flag", 0) == 0: - self._server_vad_silence = 0 - - print("[VC][uplink_ctrl] Request START by {}".format(reason)) + # 打断 cancel_wait 超时已开麦:迟到的 server.canceled 勿停录重开(避免双 start.mp3) try: if ( reason == "canceled" and getattr(self, "VC_mode", 0) == 0 - and ( - getattr(self, "_vc_pending_interrupt_uplink", 0) == 1 - or getattr(self, "is_ruppt_tts", 0) == 1 - ) + and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1 and getattr(self, "VC_record_state_flag", 0) != 0 ): - print("[VC][uplink_ctrl] interrupt -> already recording, skip restart") + print("[VC][up] intr skip") try: self._long_dialogue_key_arm = 0 if hasattr(self, "_long_dialogue_end_interrupt"): @@ -722,9 +682,32 @@ class VCSessionMediaMixin: pass try: if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1: - print("[VC][uplink_ctrl] interrupt -> start (canceled)") + print("[VC][up] intr start") except Exception: pass + + # 若当前不在录音上行,才允许开新一轮 + if getattr(self, "VC_record_state_flag", 0) != 0: + # canceled 打断后:可能是僵死 flag=2(started 未真开麦),先停录复位再开麦 + if reason == "canceled": + try: + self._vc_clear_stale_record_state("uplink_start/canceled") + except Exception: + pass + if getattr(self, "VC_record_state_flag", 0) != 0: + try: + if reason in ("canceled", "local.play_end", "audio.played"): + print("[VC][up] deny {} rs={}".format( + reason, getattr(self, "VC_record_state_flag", 0))) + except Exception: + pass + return + + # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门) + if getattr(self, "record_data_save_flag", 0) == 0: + self._server_vad_silence = 0 + + print("[VC][up] Request START by {}".format(reason)) self._vc_uplink_start_recording(reason) def _uplink_request_stop(self, reason="", *args, **kwargs): # 统一入口:避免多处同时 stop 造成冲突 @@ -734,18 +717,18 @@ class VCSessionMediaMixin: if getattr(self, "VC_record_state_flag", 0) == 0: try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] _uplink_request_stop skip (record_state==0) reason={}".format(reason)) + print("[VC][idle] stop skip rs=0 r={}".format(reason)) except Exception: pass return try: if getattr(self, "VC_mode", 0) != 1: - print("[VC][long_idle] step=uplink_stop_exec reason={} VC_record_state_flag={}".format( + print("[VC][idle] uplink_stop r={} rs={}".format( reason, getattr(self, "VC_record_state_flag", -1))) except Exception: pass - print("[VC][uplink_ctrl] Request STOP by {}".format(reason)) + print("[VC][up] Request STOP by {}".format(reason)) self.VC_Speech_End() def _uplink_mark_start(self, first_len=0): try: @@ -757,8 +740,17 @@ class VCSessionMediaMixin: self._uplink_tx_fail_cnt = 0 self._uplink_tx_fail_bytes = 0 self._uplink_tx_fail_log_ms = 0 - print("[VC][uplink] START len={} {} VC_record_state_flag={}".format( - first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag)) + try: + if ( + getattr(self, "VC_mode", -1) == 0 + and not getattr(self, "_vc_turn_wall_start_ms", 0) + ): + self._vc_turn_wall_start_ms = utime.ticks_ms() + self._vc_turn_wall_had_asr_completed = 0 + self._vc_turn_cap_fired = 0 + except Exception: + pass + print("[VC][up] START len={} rs={}".format(first_len, self.VC_record_state_flag)) except Exception: pass @@ -772,28 +764,31 @@ class VCSessionMediaMixin: last = getattr(self, "_uplink_tx_fail_log_ms", 0) if not last or utime.ticks_diff(now, last) >= 800: self._uplink_tx_fail_log_ms = now - print( - "[VC][uplink] TX_FAIL reason={} len={} fail_cnt={} fail_bytes={} {}".format( - reason or "?", - n, - self._uplink_tx_fail_cnt, - self._uplink_tx_fail_bytes, - self._vc_fsm_log_snip(), - ) - ) + print("[VC][up] TX_FAIL r={} n={} fc={}".format( + reason or "?", n, self._uplink_tx_fail_cnt)) except Exception: pass + def _vc_uplink_send_silence_tail_ms(self, ms=1000): + try: + if getattr(self, "AI_client_VC_isconnect", 0) != 1: + return 0 + n = 32 * int(ms) + if n <= 0: + return 0 + self.AI_client_VC_send(bytes(n)) + print("[VC][up] silence_tail ms={} b={}".format(int(ms), n)) + return n + except Exception: + return 0 + def _uplink_mark_end(self, reason=""): try: if self._uplink_active != 1: return dur = utime.ticks_diff(utime.ticks_ms(), self._uplink_start_ms) if self._uplink_start_ms else -1 - fc = getattr(self, "_uplink_tx_fail_cnt", 0) - fb = getattr(self, "_uplink_tx_fail_bytes", 0) - print("[VC][uplink] END reason={} bytes={} dur_ms={} tx_fail_cnt={} tx_fail_bytes={} {} VC_record_state_flag={}".format( - reason, self._uplink_bytes, dur, fc, fb, - self._vc_fsm_log_snip(), self.VC_record_state_flag)) + print("[VC][up] END r={} b={} d={} rs={}".format( + reason, self._uplink_bytes, dur, self.VC_record_state_flag)) except Exception: pass self._uplink_active = 0 @@ -802,11 +797,29 @@ class VCSessionMediaMixin: self._uplink_tx_fail_cnt = 0 self._uplink_tx_fail_bytes = 0 self._uplink_tx_fail_log_ms = 0 - - # 最近一次 TTS 的语义来源(dialog/prompt/mode_switch_long/mode_switch_ptt/...) self._last_tts_kind = "" - # 下一个“无 dialog_id 的提示音 TTS”在 tts.started 时采用该 kind self._pending_tts_kind = "" + def _vc_uplink_flush_record_queue(self): + try: + if getattr(self, "record_data_save_flag", 0) != 0: + return + q = getattr(self, "AI_record_tmp_queue", None) + if not q: + return + ps = int(getattr(self, "_server_vad_silence", 0)) + self._server_vad_silence = 0 + n = 0 + while q.empty() != True: + b = q.get() + if b: + self.AI_client_VC_send(b) + n += len(b) + self._server_vad_silence = ps + if n: + print("[VC][up] flush n={}".format(n)) + except Exception: + pass + def request_uplink_start(self, reason=""): EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_start, "data": reason}) def request_uplink_stop(self, reason=""): @@ -873,6 +886,12 @@ class VCSessionMediaMixin: return except Exception: pass + try: + if getattr(self, "_vc_ld_awaiting_reply", 0) == 1: + self._vc_publish_led("thinking", "audio_cb/finish/await_llm") + return + except Exception: + pass try: if self._vc_should_hold_listening_led(): self._vc_publish_listening_for_mode("audio_cb/finish/listen_hold") @@ -903,8 +922,10 @@ class VCSessionMediaMixin: if self.VC_record_state_flag == 1: self.VC_record_state_flag =2 try: - if hasattr(self, "_vc_turn_wall_arm_if_needed"): - self._vc_turn_wall_arm_if_needed() + if getattr(self, "VC_mode", -1) == 0 and not getattr(self, "_vc_turn_wall_start_ms", 0): + self._vc_turn_wall_start_ms = utime.ticks_ms() + self._vc_turn_wall_had_asr_completed = 0 + self._vc_turn_cap_fired = 0 except Exception: pass try: @@ -913,6 +934,31 @@ class VCSessionMediaMixin: except Exception: pass print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag)) + + def _vc_release_play_pcm_for_record(self): + try: + if getattr(self, "VC_play_TYPE", None) != VC_play_TYPE_PCM: + return + if getattr(self, "play_pcm", None) is None: + return + try: + self.play_pcm.stream_stop() + self.play_pcm.close() + except Exception: + pass + self.play_pcm = None + self.VC_Play_flag = 0 + gap = 30 + try: + if "vc_config" in globals() and vc_config: + gap = int(vc_config.get_int("PTT_POST_MUTE_MS", gap)) + except Exception: + pass + if gap > 0: + utime.sleep_ms(gap) + except Exception: + pass + def VC_Speech_Start(self): try: @@ -925,8 +971,7 @@ class VCSessionMediaMixin: # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门) self._server_vad_silence = 0 try: - if hasattr(self, "_vc_turn_wall_clear"): - self._vc_turn_wall_clear() + self._vc_turn_wall_clear(reset_merge=1) except Exception: pass self.AI_ASR_str = "" @@ -951,6 +996,7 @@ class VCSessionMediaMixin: usys.print_exception(e) elif self.VC_record_TYPE == VC_record_TYPE_PCM: print("record_pcm.stream_read_start()!!!!!!!!!!!!") + self._vc_release_play_pcm_for_record() _rec_started = 0 for _ri in range(2): try: @@ -990,12 +1036,8 @@ class VCSessionMediaMixin: self.VC_record_state_flag = 1 # 录音硬件已起:协议侧应对应 audio.create(PTT)或长对话直传 PCM try: - print("[VC][proto] record_start sr={} pcm_type={} VC_mode={} session_started={} " - "ptt_audio_created={} thread_id={}".format( - self.VC_record_samplerate, self.VC_record_TYPE, self.VC_mode, - getattr(self, "_vc_session_started", -1), - getattr(self, "_ptt_audio_created", -1), - self.VC_record_thread_id)) + print("[VC][p] rec sr={} m={}".format( + self.VC_record_samplerate, self.VC_mode)) except Exception: pass # 聆听灯与上行一并开启(见 _vc_user_record_uplink_open) @@ -1020,10 +1062,20 @@ class VCSessionMediaMixin: usys.print_exception(e) def VC_Speech_End(self): try: + ld_commit = 0 + try: + if getattr(self, "VC_mode", -1) == 0: + ld_commit = ( + getattr(self, "_vc_turn_cap_fired", 0) == 1 + or getattr(self, "_vc_ld_awaiting_reply", 0) == 1 + or getattr(self, "_vc_ld_merge_frozen", 0) == 1 + ) + except Exception: + ld_commit = 0 self._uplink_mark_end("VC_Speech_End") try: - if hasattr(self, "_vc_turn_wall_clear"): - self._vc_turn_wall_clear() + if not ld_commit: + self._vc_turn_wall_clear(reset_merge=1) except Exception: pass self.AI_ASR_flag = 0 @@ -1090,12 +1142,14 @@ class VCSessionMediaMixin: and getattr(self, "Close_flag", 0) == 0 and getattr(self, "AI_client_VC_isconnect", 0) == 1 ): - if ( - getattr(self, "_vc_turn_cap_fired", 0) == 1 - or getattr(self, "_vc_ld_merge_frozen", 0) == 1 - or getattr(self, "_vc_ld_awaiting_reply", 0) == 1 - ) and self._vc_effective_asr_text(): + if ld_commit and self._vc_effective_asr_text(): self._vc_publish_led("thinking", "VC_Speech_End/force_commit") + elif self._vc_effective_asr_text() and ( + getattr(self, "_turn_has_asr_completed", 0) == 1 + or getattr(self, "_vc_expect_dialog_tts", 0) == 1 + or getattr(self, "_turn_has_llm_created", 0) == 1 + ): + self._vc_publish_led("thinking", "VC_Speech_End/await_llm") else: self._vc_publish_listening_for_mode("VC_Speech_End/long_wait") else: @@ -1244,10 +1298,8 @@ class VCSessionMediaMixin: else: try: - if hasattr(self, "_vc_long_dialogue_turn_cap_tick"): - self._vc_long_dialogue_turn_cap_tick() - if hasattr(self, "_vc_asr_completed_stop_tick"): - self._vc_asr_completed_stop_tick() + self._vc_long_dialogue_turn_cap_tick() + self._vc_asr_completed_stop_tick() except Exception: pass VAD_data_count = 0 @@ -1425,6 +1477,14 @@ class VCSessionMediaMixin: return except Exception: pass + try: + if ( + hasattr(self, "_long_dialogue_should_drop_inbound_tts") + and self._long_dialogue_should_drop_inbound_tts() + ): + return + except Exception: + pass #print("下发音频数据1:{}".format(len(data[0]))) ret=self.AI_audio_queue.put(data) try: @@ -1862,9 +1922,6 @@ class VCSessionMediaMixin: if not ready and q_empty and tts_done_ms: try: if since_done >= 15000 and pcm_pending <= slack: - print( - "[TTS] play_end_force since_done={}ms pcm_pending={}".format( - since_done, pcm_pending)) ready = True except Exception: pass