# 长对话:单轮最长录音(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
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"))
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
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
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}})
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):
# -*- 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
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)),
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:
return True
def _vc_reset_long_idle_timer(self, tag=""):
- """长对话 L1:用户开麦/有效轮次活动则清零无 TTS 超时计数。"""
try:
if getattr(self, "VC_mode", -1) != 0:
return
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:
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
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:
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
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:
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:
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
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
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)
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:
pass
def _vc_asr_completed_stop_tick(self):
- """merge 窗结束仍无续说 → 执行延后关麦。"""
try:
if not self._vc_is_long_dialogue_mode():
return
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")
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:
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
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
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:
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
"""仅清 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
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:
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
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:
"""长对话 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
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")
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:
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:
""" 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:
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":
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:
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
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 #音乐播放标志
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:
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:
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:
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
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})
# 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:
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:
#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"})
# -*- 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)
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
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:
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()
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,
"""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!!!!")
# 可能由收包线程/其它线程触发:统一切回主线程执行 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"})
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"]
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")
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 == "":#没识别到
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:
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:
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())
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:
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:
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(
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:
"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",
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
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()
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()
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
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
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"
# 长对话:有有效 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")
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)
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
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
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
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"):
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 造成冲突
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:
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
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
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=""):
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")
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:
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:
# 新一轮开始录音:允许上行(上一轮可能已置 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 = ""
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:
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)
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
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:
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
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:
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