#dev_type = 'jswm'#金三完美
#dev_type = 'jswm_BOX'#金三完美柒牌盒子
#dev_type = 'csh'#慈善会
+
+# 1=打印 VC 周期性保活专用行「VC: ping ping」;默认 0(WS send 明细里已跳过 ping,无需再开此项)
+VC_HEARTBEAT_LOG = 0
+# 1=打印音频帧级日志:Record_len / send_data len / 下发音频长度;默认 0(只看 uplink START/END)
+# VC_AUDIO_FRAME_LOG = 0
+# 1=启用我们新增的协议层“严格门控/缓冲/兜底”行为;0=临时排除这些改动(用于定位协议影响)
+# 受控项(与对端 V2.5+ 能力对齐;关=回退旧端行为):
+# - 上行:收到 started 前缓冲/丢弃 PCM;PTT 模式下 audio.create 前禁发 PCM
+# - 兜底:llm.completed 后若久无 tts.started,端侧 force_tts 主动发 synthesis
+# 注意:audio.played 不受此开关控制——金三 V2.5 服务端依赖端侧播完后上报才能进入下一轮 ASR;
+# 若误关会导致问候结束后首轮用户说话无 vad/asr。
+VC_PROTO_GUARDS = 0
+# 1=打印 VC WebSocket 每条收发的完整文本(流量大、易刷屏);0 仅一行 event 摘要(llm.delta 省略)
+VC_WS_IO_LOG = 1
+
+# 用于现场确认设备实际运行的 ai_VC 版本(只打印一次,不影响业务)
+VC_BUILD_TAG = "fsm_tts_actions_2026-05-06"
+
+
class AI_led(Abstract):
def __init__(self):
if dev_type == "G10" or dev_type == 'jswm':
- self.led_red = Pin(Pin.GPIO36, Pin.OUT, Pin.PULL_PD, 0)
- self.led_green = Pin(Pin.GPIO44, Pin.OUT, Pin.PULL_PD, 1)
- self.led_blue = Pin(Pin.GPIO21, Pin.OUT, Pin.PULL_PD, 1)
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO44, Pin.GPIO21
elif dev_type == "7PIN_BK_EU":
- self.led_red = Pin(Pin.GPIO36, Pin.OUT, Pin.PULL_PD, 0)
- #self.led_green = Pin(Pin.GPIO21, Pin.OUT, Pin.PULL_PD, 1)
- self.led_green = Pin(Pin.GPIO8, Pin.OUT, Pin.PULL_PD, 1)
- self.led_blue = Pin(Pin.GPIO19, Pin.OUT, Pin.PULL_PD, 1)
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO8, Pin.GPIO19
elif dev_type == "7PIN_1_5":
- self.led_red = Pin(Pin.GPIO28, Pin.OUT, Pin.PULL_PD, 0)
- #self.led_green = Pin(Pin.GPIO21, Pin.OUT, Pin.PULL_PD, 1)
- self.led_green = Pin(Pin.GPIO44, Pin.OUT, Pin.PULL_PD, 1)
- self.led_blue = Pin(Pin.GPIO30, Pin.OUT, Pin.PULL_PD, 1)
+ r_gpio, g_gpio, b_gpio = Pin.GPIO28, Pin.GPIO44, Pin.GPIO30
+ elif dev_type == "7PIN":
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO44, Pin.GPIO43
else:
- self.led_red = Pin(Pin.GPIO36, Pin.OUT, Pin.PULL_PD, 0)
- self.led_green = Pin(Pin.GPIO21, Pin.OUT, Pin.PULL_PD, 1)
- self.led_blue = Pin(Pin.GPIO19, Pin.OUT, Pin.PULL_PD, 1)
-
- #=1 红灯闪,=2白灯闪,=3绿灯闪,=4全部灯闪
+ r_gpio, g_gpio, b_gpio = Pin.GPIO36, Pin.GPIO21, Pin.GPIO19
+ self._led_gpio_r = r_gpio
+ self._led_gpio_g = g_gpio
+ self._led_gpio_b = b_gpio
+ self.led_red = Pin(r_gpio, Pin.OUT, Pin.PULL_PD, 0)
+ self.led_green = Pin(g_gpio, Pin.OUT, Pin.PULL_PD, 1)
+ self.led_blue = Pin(b_gpio, Pin.OUT, Pin.PULL_PD, 1)
+ print("AI_led GPIO dev_type={} red={} green={} blue={}".format(
+ dev_type, r_gpio, g_gpio, b_gpio))
+
+ #=1 红灯闪,=2蓝灯闪,=3绿灯闪,=4全部灯闪,=5 思考:蓝绿交替
self.led_flicker_flag = 0
self.led_flicker_on_off_flag = 0
+ self.led_think_phase = 0
self.led_flicker_timer = osTimer()
EventMesh.subscribe("open_led_red",self.open_led_red)
EventMesh.subscribe("flicker_led_blue",self.flicker_led_blue)
EventMesh.subscribe("flicker_led_green",self.flicker_led_green)
EventMesh.subscribe("flicker_led_all",self.flicker_led_all)
+ EventMesh.subscribe("flicker_led_blue_green",self.flicker_led_blue_green)
EventMesh.subscribe("close_led",self.close_led)
EventMesh.publish("open_led_red")
+ # 开机演示:每秒切换一次 LED,红->绿->蓝 为一轮,共 2 轮后停止
+ self._boot_led_demo_timer = osTimer()
+ self._boot_led_demo_step = 0
+ self._boot_led_demo_left = 3 * 2 # 3 色 * 2 轮
+ self._boot_led_demo_timer.start(1000, 1, self._boot_led_demo_timer_cb)
+
+ def _boot_led_demo_timer_cb(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._boot_led_demo_timer_cb_})
+
+ def _boot_led_demo_timer_cb_(self):
+ try:
+ if self._boot_led_demo_left <= 0:
+ self._boot_led_demo_timer.stop()
+ return
+ # 不与闪烁抢占:演示期强制关掉闪烁
+ self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
+
+ if (self._boot_led_demo_step % 3) == 0:
+ print("boot_led_demo: RED (gpio={})".format(
+ self._led_gpio_r))
+ self.open_led_red(None, None)
+ elif (self._boot_led_demo_step % 3) == 1:
+ print("boot_led_demo: GREEN (gpio={})".format(
+ self._led_gpio_g))
+ self.open_led_green(None, None)
+ else:
+ print("boot_led_demo: BLUE (gpio={})".format(
+ self._led_gpio_b))
+ self.open_led_blue(None, None)
+
+ self._boot_led_demo_step += 1
+ self._boot_led_demo_left -= 1
+ if self._boot_led_demo_left <= 0:
+ self._boot_led_demo_timer.stop()
+ except Exception as e:
+ try:
+ self._boot_led_demo_timer.stop()
+ except Exception:
+ pass
+ print("_boot_led_demo_timer_cb_ fail: {}".format(e))
+
+ def _led_stop_flicker_timer(self):
+ try:
+ self.led_flicker_timer.stop()
+ except Exception:
+ pass
+
def flicker_led_red(self,event,msg):
+ self._led_stop_flicker_timer()
self.led_flicker_flag = 1
self.led_flicker_timer.start(1500, 1, self.led_flicker_timer_cb)
def flicker_led_blue(self,event,msg):
+ self._led_stop_flicker_timer()
self.led_flicker_flag = 2
self.led_flicker_timer.start(1500, 1, self.led_flicker_timer_cb)
def flicker_led_green(self,event,msg):
+ self._led_stop_flicker_timer()
self.led_flicker_flag = 3
self.led_flicker_timer.start(1500, 1, self.led_flicker_timer_cb)
def flicker_led_all(self,event,msg):
+ self._led_stop_flicker_timer()
self.led_flicker_flag = 4
self.led_flicker_timer.start(1500, 1, self.led_flicker_timer_cb)
+ def flicker_led_blue_green(self,event,msg):
+ # 思考态:蓝/绿交替亮(各占半个周期),红灯关闭
+ self._led_stop_flicker_timer()
+ self.led_flicker_flag = 5
+ self.led_think_phase = 0
+ self.led_red.write(0)
+ self.led_blue.write(1)
+ self.led_green.write(0)
+ self.led_think_phase = 1
+ self.led_flicker_timer.start(400, 1, self.led_flicker_timer_cb)
+
def led_flicker_timer_cb(self, *args, **kwargs):
EventMesh.publish("publish_AI_btnInfo",{"func":self.led_flicker_timer_cb_})
def led_flicker_timer_cb_(self):
if self.led_flicker_flag == 0:
- self.led_flicker_timer.stop()
+ self._led_stop_flicker_timer()
+ return
+ if self.led_flicker_flag == 5:
+ self.led_red.write(0)
+ if self.led_think_phase == 0:
+ self.led_blue.write(1)
+ self.led_green.write(0)
+ self.led_think_phase = 1
+ else:
+ self.led_blue.write(0)
+ self.led_green.write(1)
+ self.led_think_phase = 0
return
if self.led_flicker_on_off_flag == 0:
self.led_red.write(0)
def open_led_red(self,event,msg):
#print("open_led_red")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
#self.led_red.write(0)
self.led_blue.write(0)
self.led_green.write(0)
def open_led_blue(self,event,msg):
#print("open_led_blue")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
self.led_red.write(0)
#self.led_blue.write(0)
self.led_green.write(0)
def open_led_green(self,event,msg):
#print("open_led_green")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
self.led_red.write(0)
self.led_blue.write(0)
#self.led_green.write(0)
def open_led_red_blue(self,event,msg):
#print("open_led_blue")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
self.led_red.write(1)
#self.led_blue.write(0)
self.led_green.write(0)
def open_led_red_green(self,event,msg):
#print("open_led_blue")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
self.led_red.write(1)
#self.led_blue.write(0)
self.led_green.write(1)
def open_led_all(self,event,msg):
#print("open_led_all")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
#self.led_red.write(0)
self.led_blue.write(1)
self.led_green.write(1)
def close_led(self,event,msg):
#print("close_led")
self.led_flicker_flag = 0
+ self._led_stop_flicker_timer()
+ try:
+ self._boot_led_demo_timer.stop()
+ except Exception:
+ pass
self.led_red.write(0)
self.led_blue.write(0)
self.led_green.write(0)
self.read_uart_data = ""
self.uart_timer.stop()
def split_combined_json(self,combined_str):
- """
- 将连续的JSON字符串分割为多个独立的JSON字符串列表
-
- 参数:
- combined_str: 多个JSON拼接的字符串
- 返回:
- 分割后的
- """
+ # 将连续的JSON字符串分割为多个独立的JSON字符串列表
+ #
+ # 参数:
+ # combined_str: 多个JSON拼接的字符串
+ # 返回:
+ # 分割后的
json_list = []
start_index = 0
brace_count = 0 # 用于匹配{}括号
"event": "get_versions",
}
json_str =ujson.dumps(json_data)
- print("send_get_versions{}".format(json_str))
- print("send_get_versions{}".format(json_str))
- print("send_get_versions{}".format(json_str))
+ print("send_get_versions {}".format(json_str))
self.send_data(json_str,1)
#self.AI_uart.write(json_str)
count =1000
if dev_type != "7PIN_1_5": #不是1.5代不能发串口数据
return
print("sys_sleep_cb!!!!!!!!!!!!!!!")
- print("sys_sleep_cb!!!!!!!!!!!!!!!")
- print("sys_sleep_cb!!!!!!!!!!!!!!!")
EventMesh.publish("sleep_BT")#休眠蓝牙
self.PIN_wakeup_bt.write(0)
def sys_up_cb(self,event=None,msg=None):#唤醒前回调
if dev_type != "7PIN_1_5": #不是1.5代不能发串口数据
return
print("sys_up_cb!!!!!!!!!!!!!!!")
- print("sys_up_cb!!!!!!!!!!!!!!!")
- print("sys_up_cb!!!!!!!!!!!!!!!")
self.wakeup_BT()
self.PIN_wakeup_bt.write(1)
def rest_TB(self,event=None,msg=None):
- print("rest_TB!!!!!!!!!!!!!!!")
- print("rest_TB!!!!!!!!!!!!!!!")
print("rest_TB!!!!!!!!!!!!!!!")
self.PIN_BT_ON.write(0)
utime.sleep_ms(100)
if dev_type != "7PIN_1_5": #不是1.5代不能发串口数据
return
print("wakeup_BT!!!!!!!!!!!!!!!")
- print("wakeup_BT!!!!!!!!!!!!!!!")
wakeup_cta1_io = self.extint_wakeup_cta1.read_level()
if wakeup_cta1_io == 1:
return 1
return 10
def audio_montage_play(self,event,msg):
self.wakeup_BT()
-
+ # 抢占规则:流播放(TTS)可打断本地;本地提示音不可打断流播放
+ try:
+ if EventMesh.publish("get_vc_stream_active") == 1:
+ print("[audio-arb] skip local montage_play during stream")
+ return
+ except Exception:
+ pass
self.aud.stopPlayStream()
self.aud.stopAll()
str_data='mp3files='
AI_client_asr_num = 4
+class VCSessionState:
+ Disconnected = 0
+ Connecting = 1
+ Starting = 2
+ Started = 3
+
+
+class VCTurnState:
+ Idle = 0
+ Listening = 1
+ SpeechStreaming = 2
+ SpeechEnded = 3
+ Thinking = 4
+ TtsReceiving = 5
+ Playing = 6
+ AwaitPlayedAck = 7
+
+
+class VCMode:
+ Realtime = 0 # VAD on(长对话,协议:无需 audio.create/completed)
+ PushToTalk = 1 # VAD off(单对话,协议:必须 audio.create/completed 包裹音频帧)
+
+
+class VCStateMachine:
+ # 轻量状态机骨架(先落地骨架与动作接口,后续逐步把旧 if/flag 逻辑收敛进来)。
+ #
+ # 约束来自《金三服务-语音对话V2.5》:
+ # - 收到 started 后才允许发音频帧(否则 session not initialized)
+ # - VAD 开启:不需要 audio.create/completed
+ # - VAD 关闭:必须 audio.create → frames → audio.completed
+ # - 播放结束 audio.played 是进入下一轮的门控信号之一(尤其关键词打断场景)
+
+ def __init__(self, vc):
+ self.vc = vc
+ self.session = VCSessionState.Disconnected
+ self.turn = VCTurnState.Idle
+ self.last_llm_ms = 0
+ self.last_tts_pending_ms = 0
+ self.last_force_tts_ms = 0
+ self.await_played = False
+ self.last_tts_completed_ms = 0
+
+ def _turn_name(self, t):
+ try:
+ return {
+ VCTurnState.Idle: "Idle",
+ VCTurnState.Listening: "Listening",
+ VCTurnState.SpeechStreaming: "SpeechStreaming",
+ VCTurnState.SpeechEnded: "SpeechEnded",
+ VCTurnState.Thinking: "Thinking",
+ VCTurnState.TtsReceiving: "TtsReceiving",
+ VCTurnState.Playing: "Playing",
+ VCTurnState.AwaitPlayedAck: "AwaitPlayedAck",
+ }.get(t, str(t))
+ except Exception:
+ return str(t)
+
+ def _session_name(self, s):
+ try:
+ return {
+ VCSessionState.Disconnected: "Disconnected",
+ VCSessionState.Connecting: "Connecting",
+ VCSessionState.Starting: "Starting",
+ VCSessionState.Started: "Started",
+ }.get(s, str(s))
+ except Exception:
+ return str(s)
+
+ def log(self, reason=""):
+ # 只用于关键节点的单行状态日志,便于串口快速定位状态漂移
+ try:
+ print("[FSM] {} session={} turn={} await_played={}".format(
+ reason,
+ self._session_name(self.session),
+ self._turn_name(self.turn),
+ 1 if self.await_played else 0,
+ ))
+ except Exception:
+ pass
+
+ def mode(self):
+ try:
+ return VCMode.Realtime if self.vc.VC_mode == 0 else VCMode.PushToTalk
+ except Exception:
+ return VCMode.Realtime
+
+ def on_session_start_request(self):
+ self.session = VCSessionState.Starting
+ self.turn = VCTurnState.Idle
+ # 新会话开始时必须清空上一轮“待播完”状态,否则会出现:
+ # - 切换模式/重连后 await_played 仍为 True → play_end 逻辑抢跑/不触发 → 无法自动开麦
+ self.await_played = False
+ self.last_tts_completed_ms = 0
+ self.last_llm_ms = 0
+
+ def on_server_started(self):
+ self.session = VCSessionState.Started
+ # 新会话 started:确保进入 Listening,且清理上轮播放门控
+ self.await_played = False
+ self.last_tts_completed_ms = 0
+ if self.turn in (VCTurnState.Idle, VCTurnState.AwaitPlayedAck):
+ self.turn = VCTurnState.Listening
+ self.log("server.started")
+
+ def on_server_vad_silence(self):
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.SpeechEnded
+ self.log("server.vad.silence")
+
+ def on_server_llm_created(self):
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.Thinking
+ try:
+ self.last_llm_ms = utime.ticks_ms()
+ except Exception:
+ self.last_llm_ms = 1
+ self.log("server.llm.created")
+
+ def on_server_tts_started(self):
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.TtsReceiving
+ self.log("server.tts.started")
+
+ def on_server_tts_completed(self):
+ if self.session == VCSessionState.Started:
+ # tts.completed 只表示服务端音频下发结束,本地可能仍在播放;保持 Playing,由 await_played 门控播完上报
+ self.await_played = True
+ try:
+ self.last_tts_completed_ms = utime.ticks_ms()
+ except Exception:
+ self.last_tts_completed_ms = 1
+ self.log("server.tts.completed")
+
+ def on_server_canceled(self):
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.Listening
+ self.log("server.canceled")
+
+ def on_server_completed(self):
+ if self.session == VCSessionState.Started:
+ # 本轮结束:默认回到 Listening;是否自动开麦由 action 门控决定
+ # 若仍在等待本地播完(await_played),completed 不应把 turn 拉回 Listening,
+ # 否则会触发 completed→uplink_start 抢跑,打断播放链路。
+ if not self.await_played and self.turn != VCTurnState.AwaitPlayedAck:
+ self.turn = VCTurnState.Listening
+ self.log("server.completed")
+
+ def on_server_event(self, event, jsload_data=None):
+ # 事件→动作(action list)适配层。
+ # 现阶段只产出“可幂等的控制动作”(uplink start/stop),避免和旧逻辑重复触发播放等副作用。
+ # 后续会逐步把更多行为(play_start/audio.played/ptt create/completed)迁移到这里。
+ actions = []
+ try:
+ # LLM 阶段必须驱动 FSM.turn,否则 turn 会停在 SpeechEnded,导致:
+ # - _fsm_timeout_table_tick 里「Thinking 超时 / synthesis 超时」永不命中
+ # - 与注释和 force_tts 前置条件不一致
+ if event == "llm.created":
+ self.on_server_llm_created()
+ return actions
+ if event == "llm.completed":
+ # 若对端偶发跳过 llm.created,用 completed 把 turn 补到 Thinking,避免兜底全失效
+ try:
+ if self.session == VCSessionState.Started:
+ if self.turn in (VCTurnState.SpeechEnded, VCTurnState.Listening):
+ self.turn = VCTurnState.Thinking
+ except Exception:
+ pass
+ return actions
+ if event == "started":
+ self.on_server_started()
+ return actions
+ if event == "vad.silence":
+ self.on_server_vad_silence()
+ actions.append(("uplink_stop", "vad.silence"))
+ return actions
+ if event == "tts.started":
+ self.on_server_tts_started()
+ actions.append(("uplink_stop", "tts.started"))
+ actions.append(("play_start", "tts.started"))
+ return actions
+ if event == "tts.completed":
+ self.on_server_tts_completed()
+ # 播放可能已在 started 时开启,这里不再强制 play_start,避免反复 open PCM
+ return actions
+ if event == "canceled":
+ self.on_server_canceled()
+ # canceled:若处于长对话,立刻回到聆听并尝试开麦(由 _uplink_request_start 的门控保证安全)
+ actions.append(("uplink_start", "canceled"))
+ return actions
+ if event == "completed":
+ self.on_server_completed()
+ # completed:若不在等待 audio.played(说明本轮无 TTS),长对话可直接进入下一轮聆听
+ if (not getattr(self, "await_played", False)) and self.turn != VCTurnState.AwaitPlayedAck:
+ actions.append(("uplink_start", "completed"))
+ return actions
+ except Exception:
+ pass
+ return actions
+
+ def on_local_audio_played(self):
+ # 本地播报完成后:长对话回到 Listening,等待下一轮输入
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.Listening
+ self.await_played = False
+ self.last_tts_completed_ms = 0
+ self.log("local.audio_played")
+
+ def on_local_play_start(self):
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.Playing
+ self.log("local.play_start")
+
+ def on_local_synthesis_sent(self):
+ # synthesis 发出后到 tts.started 之间:视为 TTS 接收阶段,禁止开麦
+ if self.session == VCSessionState.Started:
+ self.turn = VCTurnState.TtsReceiving
+ self.log("local.synthesis_sent")
+
+ def on_local_play_end(self):
+ actions = []
+ try:
+ if self.session != VCSessionState.Started:
+ return actions
+ if self.await_played:
+ actions.append(("send_audio_played", "local.play_end"))
+ # 当前策略:长对话模式不做“播完自动开麦”,避免静音时持续占用麦克/上行、误触发和隐私争议。
+ # 因此这里不追加 uplink_start。
+ try:
+ k = getattr(self.vc, "_last_tts_kind", "") or ""
+ except Exception:
+ k = ""
+ self.on_local_audio_played()
+ except Exception:
+ pass
+ self.log("local.play_end")
+ return actions
+
+
class AI_VC(Abstract):
+ def get_vc_stream_active(self, *args, **kwargs):
+ # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。
+ try:
+ if getattr(self, "VC_Play_flag", 0) == 1:
+ return 1
+ except Exception:
+ pass
+ try:
+ if getattr(self.fsm, "await_played", False):
+ return 1
+ except Exception:
+ pass
+ try:
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (VCTurnState.TtsReceiving, VCTurnState.Playing):
+ return 1
+ except Exception:
+ pass
+ try:
+ if getattr(self, "_tts_pending", 0) == 1:
+ return 1
+ except Exception:
+ pass
+ return 0
+
+ # ===== Push-To-Talk(单对话模式)=====
+ # 语义:
+ # - 长按:无论当前处于 TTS/Thinking/任何中间态,立即打断并进入新一轮录音上行
+ # - 松开:停止录音并等待服务端返回(由 AI_client_VC_end_speech 触发)
+ def ptt_begin(self, reason="key.long_press"):
+ try:
+ if self.VC_mode != 1:
+ return
+ except Exception:
+ return
+
+ t0 = 0
+ try:
+ t0 = utime.ticks_ms()
+ except Exception:
+ t0 = 0
+
+ # 本地先停播并清空队列,避免“边播边录/回声触发/状态机乱跳”
+ try:
+ self.VC_init()
+ except Exception:
+ pass
+
+ # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self.VC_Speech_End()
+ except Exception:
+ pass
+
+ # 会话未连接:先建链并发 Start,等待 started 后再自动开麦
+ try:
+ if getattr(self, "AI_client_VC_isconnect", 0) != 1:
+ self._ptt_force_record_on_started = 1
+ self.AI_VC_start()
+ return
+ except Exception:
+ self._ptt_force_record_on_started = 1
+ try:
+ self.AI_VC_start()
+ except Exception:
+ pass
+ return
+
+ # 会话还没 initialized:先发 Start,请求服务端进入 started
+ if getattr(self, "_vc_session_started", 0) != 1:
+ self._ptt_force_record_on_started = 1
+ try:
+ try:
+ if t0:
+ print("[PTT] wait server.started (ms={})".format(utime.ticks_ms() - t0))
+ except Exception:
+ pass
+ self.AI_client_VC_start_requset()
+ except Exception:
+ pass
+ return
+
+ # 打断服务端当前 turn(如正在 tts/思考),然后开启新一轮 PTT
+ try:
+ cancel = {"event": "cancel", "payload": {}}
+ self.AI_client_VC_send(ujson.dumps(cancel))
+ except Exception:
+ pass
+
+ # 新一轮:先发 audio.create,再开麦(record_thread 以 _ptt_audio_created 门控)
+ try:
+ self._ptt_audio_created = 0
+ except Exception:
+ pass
+ try:
+ self.AI_client_VC_send_speech()
+ except Exception:
+ pass
+ try:
+ self.VC_Speech_Start_now()
+ except Exception:
+ pass
+ try:
+ if t0:
+ print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
+ except Exception:
+ pass
+
+ def _uplink_request_start(self, reason="", *args, **kwargs):
+ # 统一入口:避免多处同时 start 造成冲突(尤其是 TTS/播放阶段误开麦导致回声触发与状态紊乱)
+ try:
+ if self.Close_flag == 1:
+ try:
+ if reason in ("canceled", "local.play_end", "audio.played"):
+ print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason))
+ except Exception:
+ pass
+ return
+ except Exception:
+ return
+
+ # 连续对话才允许“自动开新一轮上行”
+ try:
+ if self.VC_mode != 0:
+ try:
+ if reason in ("canceled", "local.play_end", "audio.played"):
+ print("[VC][uplink_ctrl] Deny START by {} cause=VC_mode({})".format(reason, self.VC_mode))
+ except Exception:
+ pass
+ return
+ except Exception:
+ return
+
+ # ===== 业务门控(收敛到 FSM turn/session)=====
+ try:
+ if getattr(self.fsm, "session", VCSessionState.Disconnected) != VCSessionState.Started:
+ try:
+ if reason in ("canceled", "local.play_end", "audio.played"):
+ print("[VC][uplink_ctrl] Deny START by {} cause=fsm.session".format(reason))
+ except Exception:
+ pass
+ return
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)
+ # 仅 Listening/SpeechEnded 允许开麦进入下一轮;其它阶段一律拒绝(避免抢跑打乱链路)
+ if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded):
+ try:
+ if reason in ("canceled", "local.play_end", "audio.played"):
+ print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))
+ except Exception:
+ pass
+ return
+ except Exception:
+ return
+
+ # 若当前不在录音上行,才允许开新一轮
+ 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))
+ self.VC_Speech_Start_now()
+
+ def _uplink_request_stop(self, reason="", *args, **kwargs):
+ # 统一入口:避免多处同时 stop 造成冲突
+ if getattr(self, "record_data_save_flag", 0) == 0:
+ self._server_vad_silence = 1
+
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ return
+
+ print("[VC][uplink_ctrl] Request STOP by {}".format(reason))
+ self.VC_Speech_End()
+
+ def _uplink_mark_start(self, first_len=0):
+ try:
+ if self._uplink_active == 1:
+ return
+ self._uplink_active = 1
+ self._uplink_bytes = 0
+ self._uplink_start_ms = utime.ticks_ms()
+ print("[VC][uplink] START len={} VC_state={} VC_record_state_flag={}".format(
+ first_len, self.VC_state, self.VC_record_state_flag))
+ except Exception:
+ pass
+
+ 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
+ print("[VC][uplink] END reason={} bytes={} dur_ms={} VC_state={} VC_record_state_flag={}".format(
+ reason, self._uplink_bytes, dur, self.VC_state, self.VC_record_state_flag))
+ except Exception:
+ pass
+ self._uplink_active = 0
+ self._uplink_bytes = 0
+ self._uplink_start_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 = ""
+
+ # ===== 状态机动作接口(骨架)=====
+ # 说明:统一把“副作用动作”收敛到这些方法,便于后续把 recv/timer 的 if/flag 逻辑迁移到 VCStateMachine。
+ def request_send_start(self):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_start_requset})
+
+ def request_send_audio_create(self):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_send_speech})
+
+ def request_send_audio_completed(self):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_end_speech})
+
+ def request_send_audio_played(self):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_audio_played})
+
+ def request_send_synthesis(self, text):
+ try:
+ self.AI_TTS_str = text
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})
+
+ def request_uplink_start(self, reason=""):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_start, "data": reason})
+
+ def request_uplink_stop(self, reason=""):
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_stop, "data": reason})
+
+ def _fsm_apply_actions(self, actions):
+ try:
+ for a in actions:
+ if not a:
+ continue
+ if a[0] == "uplink_stop":
+ self.request_uplink_stop(a[1] if len(a) > 1 else "")
+ elif a[0] == "uplink_start":
+ self.request_uplink_start(a[1] if len(a) > 1 else "")
+ elif a[0] == "play_start":
+ try:
+ self.fsm.on_local_play_start()
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_Start})
+ elif a[0] == "send_audio_played":
+ self.request_send_audio_played()
+ except Exception:
+ pass
+
# AI
-
+
+ def _print_hw_diag(self):
+ # 仅诊断使用:打印当前分支能看到的麦克/音频相关配置和运行时状态。
+ # JSWM2 老分支没有 device_profile/_PROFILE,这里宽容处理
+ try:
+ profile = _PROFILE if isinstance(_PROFILE, dict) else {}
+ except Exception:
+ profile = {}
+ dev_type_profile = profile.get("dev_type", None)
+ flags = profile.get("flags", {})
+ hardware = profile.get("hardware", {})
+ product_cfg = profile.get("product_cfg", {})
+ vc_cfg = {}
+ try:
+ vc_cfg = product_cfg.get("vc", {})
+ except Exception:
+ vc_cfg = {}
+
+ print("[hw-diag] ===== AI_VC hardware diag =====")
+ print("[hw-diag] profile.dev_type =", dev_type_profile)
+ print("[hw-diag] profile.flags =", flags)
+ print("[hw-diag] profile.hardware =", hardware)
+ print("[hw-diag] product_cfg.vc =", vc_cfg)
+
+ try:
+ print("[hw-diag] VC_record_samplerate =", self.VC_record_samplerate)
+ print("[hw-diag] VC_play_samplerate =", self.VC_play_samplerate)
+ except Exception:
+ pass
+ try:
+ print("[hw-diag] vad_silence_runtime =", self.vad_silence)
+ except Exception:
+ pass
+ try:
+ print("[hw-diag] record_data_save_flag =", self.record_data_save_flag)
+ print("[hw-diag] record_max_bytes =", self.record_max_bytes)
+ except Exception:
+ pass
+
+ try:
+ g = self.record.gain_get()
+ except Exception as e:
+ g = "gain_get_error: {}".format(e)
+ print("[hw-diag] record.gain_get =", g)
+ try:
+ print("[hw-diag] type(record) =", type(self.record))
+ print("[hw-diag] type(aud) =", type(self.aud))
+ except Exception:
+ pass
+ print("[hw-diag] ===== end of hw-diag =====")
+
def __init__(self):
# import voiceCall
# voiceCall.callStart("10086")
self.VC_mode = 0
self.is_up_error= 0 #是否上传错误id
self.Close_flag =0 #=1时,连续对话播放完音频就退出
+ # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频
+ # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪)
+ self._server_vad_silence = 0
+ # 单轮状态标志:用于判定 completed 是否应自动进入下一轮
+ self._turn_has_asr_completed = 0
+ self._turn_has_vad_silence = 0
+ self._turn_has_tts_started = 0
+ # VC 会话是否已进入 started(服务端 session initialized)
+ # 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环
+ self._vc_session_started = 0
+ # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
+ # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
+ self._ptt_audio_created = 0
+ # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ # 各类兜底的节流时间戳(避免“兜底过激/连续触发”)
+ self._fb_last_thinking_restart_ms = 0
+ self._fb_last_force_tts_ms = 0
+ self._fb_last_tts_pending_timeout_ms = 0
+ # thinking 兜底:部分情况下服务端只返回 llm.completed 而不触发 tts.started/completed,
+ # 会导致一直停留在 thinking。此时超时自动回到聆听。
+ self._llm_completed_ms = 0
+ # llm→tts 兜底:若服务端只回 llm.completed 没有自动触发 tts,则由端侧主动发 synthesis 触发 TTS
+ self._llm_text_pending_tts = ""
+ self._llm_force_tts_ms = 0
+ # 语音上行统计(真实发送到 VC 发送队列的开始/结束)
+ self._uplink_active = 0
+ self._uplink_bytes = 0
+ self._uplink_start_ms = 0
+
+ # 状态机骨架(后续逐步把旧逻辑迁移进来)
+ self.fsm = VCStateMachine(self)
self.play_new_voice_flag =0 #播放文字留言flag
#self.record.gain_set(4,12)
self.aud = audio.Audio(0)
self.aud.setCallback(self.audio_cb)
+ # 确保 record/aud 已创建后再打印硬件/音频诊断信息(需要 gain_get/type(record))
+ try:
+ self._print_hw_diag()
+ except Exception as e:
+ usys.print_exception(e)
#self.aud.setVolume(self.VC_TTS_volume //10)
self.audio_play_state = 1 #音频播放完状态
self.close_timerout = 0
self.alarm_play_flag = 0 #闹钟播放标志
self.tts_in_data_len = 0
self.tts_out_data_len = 0
+ self._need_audio_played_ack = 0#tts.completed 后需在本地播报结束后上报 audio.played;勿用 tts_in/out:tts.completed 会再调 VC_Play_Start 把二者清零
self.VC_satrt_flag = 0
self.VC_stop_play_audio_flag = 0
self.VC_satrt_kws_flag = 0
EventMesh.subscribe("pwk_key_release", self.pwk_key_release)
EventMesh.subscribe("get_AI_audio_queue", self.get_AI_audio_queue)
EventMesh.subscribe("get_VC_satrt_flag", self.get_VC_satrt_flag)
+ EventMesh.subscribe("get_vc_stream_active", self.get_vc_stream_active)
EventMesh.subscribe("VC_error", self.VC_error)
EventMesh.subscribe("audio_test", self.audio_test)
EventMesh.subscribe("audio_test_close", self.audio_test_close)
self.pwk_notice_flag = 0
EventMesh.publish("set_app_new_voice_flag",0)
def audio_test(self,event = None, msg = None):
- self.VC_Speech_Start()
+ self._uplink_request_start("audio_test")
self.VC_Play_Start()
def audio_test_close(self,event = None, msg = None):
- self.VC_Speech_End()
+ self._uplink_request_stop("audio_test_close")
self.VC_Play_End()
if self.record_data_save_flag == 1:
if self.record_fd :
def get_VC_satrt_flag(self, event, msg):
return self.VC_satrt_flag
#检查有没有意外打开ai部分功能
- def chack_vc_start(self):
- print("chack_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))
+ def check_vc_start(self):
+ print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))
if self.VC_satrt_flag == 0:
if self.VC_record_state_flag != 0 or self.AI_client_VC_isconnect == 1 or self.VC_Play_flag == 1:
self.Close()
self.http_get_config()#获取/更新配置参数
except Exception as e:
usys.print_exception(e)
- self.chack_vc_start()#30秒检查一下有没有关闭
+ self.check_vc_start()#30秒检查一下有没有关闭
#提交错误id
def ud_error(self,error_data):
response = None
if dev_type == "7PIN": #2代产品要延时播放按键音 为了连接蓝牙后不播放按键音的问题
# print("1111111111播放按键音!!!!!!!!!!")
# self.aud.aud_tone_play(10,400)#播放按键音
- self.aud.play(1, 1, 'U:/static/start.mp3') #播放按键音
+ # 抢占规则:本地提示音不可打断流播放
+ try:
+ if EventMesh.publish("get_vc_stream_active") != 1:
+ self.aud.play(1, 1, 'U:/static/start.mp3') #播放按键音
+ except Exception:
+ pass
# 播放状态判定
if self.VC_state >= VC_State_TTSStart and self.VC_state <= VC_State_TTSEnd:
print("【打断播放】设备正在说话,仅中断播放,不发起新对话")
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
return
+ # 长对话:单按键语义为“打断当前对话,然后继续开麦上行”
+ # - 若服务端返回 canceled,则会在 canceled 分支自动触发开麦(见 recv 里的 request_uplink_start("canceled"))
+ try:
+ if self.VC_mode == 0:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
+ return
+ except Exception:
+ pass
+
EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
if EventMesh.publish("get_sos_flag")== 1:
return
pass
# self.aud.aud_tone_play(10,200)#播放按键音
utime.sleep_ms(500)
-
self.Close()
if PRESS_AUDIO_MODE == 0:
if dev_type == "7PIN" or dev_type == "7PIN_1_5":
# if self.VC_state != VC_State_None:
# EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
# EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
- #if self.VC_record_state_flag >1:#如果在录音,就先停止录音
- self.VC_Speech_End()
-
- self.VC_Speech_Start()
+ self._uplink_request_stop("BT_AI_key_press/compat")
+ self._uplink_request_start("BT_AI_key_press/compat")
self.AI_VC_start()
if dev_type == "7PIN" or dev_type == "7PIN_1_5":
pass
# self.aud.aud_tone_play(10,400)#播放按键音
+ # 长对话:蓝牙按键同单按——打断并继续开麦上行
+ try:
+ if self.VC_mode == 0:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
+ return
+ except Exception:
+ pass
+
self.Close()
if PRESS_AUDIO_MODE == 0:
if dev_type == "7PIN" or dev_type == "7PIN_1_5":
# if self.VC_state != VC_State_None:
# EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
# EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
- #if self.VC_record_state_flag >1:#如果在录音,就先停止录音
- self.VC_Speech_End()
-
- self.VC_Speech_Start()
+ self._uplink_request_stop("BT_AI_key_press/compat2")
+ self._uplink_request_start("BT_AI_key_press/compat2")
self.AI_VC_start()
self.AI_ASR_str_tmp = ""
self.AI_key_long_press_flag = 0
EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self.request_uplink_stop("stop_AI_record")
count = 0
while True:
if count >= 20 or self.VC_satrt_flag == 0:
if self.VC_mode == 0:
self.VC_mode = 1
tts_str = "已切换到单对话模式"
+ # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(单对话)
+ try:
+ self._pending_tts_kind = "mode_switch_ptt"
+ self._last_tts_kind = "mode_switch_ptt"
+ except Exception:
+ pass
else:
self.VC_mode = 0
tts_str = "已切换到长对话模式"
+ # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(长对话)
+ try:
+ self._pending_tts_kind = "mode_switch_long"
+ self._last_tts_kind = "mode_switch_long"
+ except Exception:
+ pass
EventMesh.publish("play_tts",[tts_str,1])
EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])
def AI_key_long_press(self, event, msg):#按键长按
print("AI_key_long_press")
- if dev_type == 'jswm' or dev_type == 'jswm_BOX' :
+ # 单对话(PTT):任何中间态都允许打断并开启新一轮录音上行
+ if self.trans_flag == 1:#AI翻译模式
self.Close()
+ EventMesh.publish("open_led_green")
+ EventMesh.publish("start_voice_record",4)#开始翻译录音
+ self.AI_key_long_press_flag = 1
+ if self.VC_mode == 1:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.ptt_begin})
else:
- if self.trans_flag == 1:#AI翻译模式
- self.Close()
- EventMesh.publish("open_led_green")
- EventMesh.publish("start_voice_record",4)#开始翻译录音
- self.AI_key_long_press_flag = 1
+ # 长对话:长按与单按一致——打断当前并继续对话(开麦上行)
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})
def pwk_key_release(self, event, msg):#电源键短按,关闭
print("pwk_key_release")
def set_VC_record_state_flag(self,data):
self.VC_record_state_flag = data
- print("set_VC_record_state_flag:{}".format(self.VC_record_state_flag))
+ print("[VC] record_state_flag={}".format(self.VC_record_state_flag))
#开始录音,不缓存,直接发
def VC_Speech_Start_now(self):
+ # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
+ self._server_vad_silence = 0
self.VC_Speech_Start()
if self.VC_record_state_flag == 1:
self.VC_record_state_flag =2
- print("VC_Speech_Start_now:{}".format(self.VC_record_state_flag))
+ print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag))
#开始录音,先缓存到buf,等服务器响应再发
def VC_Speech_Start(self):
try:
#self.VC_state = VC_State_SpeechStart
- print("VC_Speech_Start VC_record_state_flag:{} self.AI_ASR_flag:{}!!!!!!!!".format(self.VC_record_state_flag,self.AI_ASR_flag))
+ print("[VC] Speech_Start record_state={} AI_ASR_flag={}".format(self.VC_record_state_flag,self.AI_ASR_flag))
if self.AI_ASR_flag != 0:
return
if self.VC_record_state_flag == 0:
+ # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)
+ self._server_vad_silence = 0
self.VC_record_thread_close()#关闭录音线程
_thread.stack_size(self.Audio_buf_size+8*1024)
#停止录音
def VC_Speech_End(self):
try:
+ self._uplink_mark_end("VC_Speech_End")
self.AI_ASR_flag = 0
self.AI_ASR_str_tmp = ""
self.AI_ASR_str = ""
Record_read_buf = bytearray(self.Audio_buf_size)
for i in range(10):
Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- print("Record_len:!!!!!!{}".format(Record_len))
+ # if VC_AUDIO_FRAME_LOG:
+ # print("Record_len:!!!!!!{}".format(Record_len))
if Record_len <= 0:
break
elif self.VC_record_TYPE == VC_record_TYPE_PCM:
Record_read_buf = bytearray(self.Audio_buf_size)
for i in range(10):
Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
- print("Record_len:!!!!!!{}".format(Record_len))
+ # if VC_AUDIO_FRAME_LOG:
+ # print("Record_len:!!!!!!{}".format(Record_len))
if Record_len <= 0:
break
# if self.record_pcm != None:
self.AI_record_tmp_queue.clear()
self.VC_record_state_flag = 0
- print("VC_Speech_End VC_record_state_flag:{}!!!!!!!!".format(self.VC_record_state_flag))
+ print("[VC] Speech_End record_state={}".format(self.VC_record_state_flag))
#关闭录音线程
def VC_record_thread_close(self):
if self.VC_record_thread_id != 0:
def VC_record_thread(self):
##return
- print("VC_record_thread start!!!!!!!!!!!!!!!!!!!!\r\n")
+ print("[VC] record_thread start")
count = 0
RequestToSpeak_flag = 0
# try:
#print("VC_record_thread:{}".format(Record_len))
if Record_len > 0:
read_buf = bytes(Record_read_buf[:Record_len])
- print("Record_len:{}".format(Record_len))
+ # if VC_AUDIO_FRAME_LOG:
+ # print("Record_len:{}".format(Record_len))
if self.record_data_save_flag == 1:
if self.record_fd == None:
try:
uos.remove('/usr/record_data.pcm')
except Exception as e:
- usys.print_exception(e)
- self.record_fd = open('/usr/record_data.pcm','w')
+ pass
+ # 二进制写入,避免文本模式导致数据异常
+ self.record_fd = open('/usr/record_data.pcm', 'wb')
self.record_fd_len = 0
if self.record_fd != None:
try:
break
if vad_state == 1:
print("检测到VAD=1!!!!!!!!!!!!!")
- print("检测到VAD=1!!!!!!!!!!!!!")
- print("检测到VAD=1!!!!!!!!!!!!!")
# EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
# RequestToSpeak_flag = 1
break
else:
pass
- self.AI_client_VC_send(self.AI_record_tmp_queue.get()) #发送之前存的数据
- self.AI_client_VC_send(read_buf)
+ # 协议门控:会话未 started 前禁止上行音频(否则 session not initialized)
+ if VC_PROTO_GUARDS and self._vc_session_started != 1 and self.record_data_save_flag == 0:
+ _ = self.AI_record_tmp_queue.get()
+ AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(_))
+ continue
+ if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):
+ self.AI_client_VC_send(self.AI_record_tmp_queue.get()) #发送之前存的数据
+ else:
+ self._uplink_mark_end("silence_gate(drop_queue)")
+ _ = self.AI_record_tmp_queue.get()
+ # 当前帧发送前协议门控(可用 VC_PROTO_GUARDS 临时关闭):
+ # - session started 前禁止发帧
+ # - Push-To-Talk 在 audio.create 前禁止发帧
+ if VC_PROTO_GUARDS and (
+ (self._vc_session_started != 1 and self.record_data_save_flag == 0) or
+ (self.VC_mode == 1 and self._ptt_audio_created != 1 and self.record_data_save_flag == 0)
+ ):
+ # 未 ready:转入缓存,等待 started 再发(并做容量控制)
+ try:
+ self.AI_record_tmp_queue.put(read_buf)
+ AI_record_tmp_queue_len += len(read_buf)
+ while AI_record_tmp_queue_len > 128*1024 and self.AI_record_tmp_queue.empty() != True:
+ d = self.AI_record_tmp_queue.get()
+ AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(d))
+ except Exception:
+ pass
+ else:
+ if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):
+ self.AI_client_VC_send(read_buf)
+ else:
+ self._uplink_mark_end("silence_gate(drop_buf)")
url = 'ws://{}/{}'.format(self.server_ip,req_str)
#url = 'ws://115.190.94.208:22080/vc/coze?dt={}'.format(token)
- print("VC VC:url{}".format(url))
+ if VC_WS_IO_LOG:
+ print("[VC] ws url {}".format(url))
self.AI_client_VC = uwebsocket.Client.connect(url, debug=True)
#self.AI_WS_connect_Queue.put(self.AI_client_VC)
- print("websocket_connect_VC:OK!!!!!!!!!!!!!!!!!!!!!")
+ print("[VC] websocket_connect ok")
except Exception as e: #网络连接异常
usys.print_exception(e)
print("websocket_connect_VC error!!!\r\n")
str_error = str(e)
if str_error.find("[Errno 22] EINVAL")!= -1:
- print("websocket_connect_VC:str_error:{}".format(str_error))
- print("[Errno 22] EINVAL!!!\r\n")
- print("[Errno 22] EINVAL!!!\r\n")
- print("[Errno 22] EINVAL!!!\r\n")
- print("[Errno 22] EINVAL!!!\r\n")
+ print("[VC] websocket EINVAL: {}".format(str_error))
self.websocket_error()
EventMesh.publish("set_storage_config",["powerRestart",1])#调试临时关闭
utime.sleep_ms(1000)
self.AI_ASR_str = ""
self.AI_ASR_str_tmp = ""
self.AI_ASR_flag = 0
- self.VC_Speech_End()
+ # 可能由收包线程/其它线程触发:统一切回主线程执行 stop,避免“异常→重试→再异常”的循环
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"})
EventMesh.publish("close_led")
if TTS_str == None:
self.websocket_error_count+=1
def websocket_deconnect_VC(self):
#print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))
print("websocket_deconnect_VC!!!!!!!")
+ self._vc_session_started = 0
+ # 断链/模式切换时必须重置“上行录音状态”,否则会被 _uplink_request_start 的幂等门控挡住,
+ # 表现为:切回长对话后无法自动开麦,按键 cancel/canceled 也无法恢复。
+ try:
+ self.VC_record_state_flag = 0
+ except Exception:
+ pass
+ try:
+ self._server_vad_silence = 0
+ except Exception:
+ pass
+ try:
+ self._ptt_audio_created = 0
+ except Exception:
+ pass
+ try:
+ # 若录音线程/录音设备仍在占用,先停掉
+ self.VC_Speech_End()
+ except Exception:
+ try:
+ self.VC_record_thread_close()
+ except Exception:
+ pass
+ # 断链必须同步重置 FSM,否则 await_played/turn 残留会导致动态切换后“无法自动开麦/按键无效”
+ try:
+ self.fsm.session = VCSessionState.Disconnected
+ self.fsm.turn = VCTurnState.Idle
+ self.fsm.await_played = False
+ self.fsm.last_tts_completed_ms = 0
+ self.fsm.last_llm_ms = 0
+ except Exception:
+ pass
self.AI_client_VC_recv_thread_close()#关闭读线程
self.AI_client_VC_recv_id = 0
if self.AI_client_VC_isconnect == 1 or self.AI_client_VC!=None:
if self.AI_client_VC_isconnect == 1:
#self.AI_client_VC.write_frame(0x09,b'ping ping')
self.AI_client_VC_send("ping")
- print("VC: ping ping")
+ if VC_HEARTBEAT_LOG:
+ print("VC: ping ping")
#websocket发送数据
def AI_client_VC_send(self,data_str):
# pass
# else:
# print("AI_client_VC_send:len{}".format(len(data_str)))
+ # 统计“语音上行”真实开始/结束:这里只统计二进制音频(非 ping/字符串)
+ try:
+ if not isinstance(data_str, str):
+ l = len(data_str)
+ if l > 0:
+ self._uplink_mark_start(l)
+ self._uplink_bytes += l
+ except Exception:
+ pass
self.AI_VC_send_data_queue.put([AI_client_VC_num,data_str])
#关闭数据发送线程
def AI_VC_send_data_queue_thread_close(self):
if self.AI_VC_send_data_queue_thread_id != 0:
- count =500
+ # 先清空待发队列,避免关闭过程持续堆积/刷屏
+ if self.AI_VC_send_data_queue.empty() != True:
+ self.AI_VC_send_data_queue.clear()
+ # 通知发送线程退出;若发送线程正阻塞在底层 send,则只能等超时或强制 stop_thread
+ self.AI_VC_send_data_queue_thread_lock = 2
+ count = 500
while True:
count -= 1
if self.AI_VC_send_data_queue_thread_lock == 0 or count <=0:
break
else:
- self.AI_VC_send_data_queue_thread_lock = 2
- print("AI_VC_send_data_queue_thread going:{}...".format(count))
- utime.sleep_ms(1)
- if self.AI_VC_send_data_queue.empty()!=True:
- self.AI_VC_send_data_queue.clear()
+ # 该打印只是“等待线程停下来”的调试心跳,默认关闭以免影响系统实时性
+ if VC_HEARTBEAT_LOG and (count % 50 == 0 or count > 490):
+ print("AI_VC_send_data_queue_thread going:{}...".format(count))
+ utime.sleep_ms(5)
self.AI_VC_send_data_queue_thread_lock = 0
- print("AI_VC_send_data_queue_thread_close")
+ if VC_HEARTBEAT_LOG:
+ print("AI_VC_send_data_queue_thread_close")
if self.AI_VC_send_data_queue_thread_id != 0:
try:
- _thread.stop_thread(self.AI_VC_send_data_queue_thread_id)
+ # 线程可能已自然退出/被复用,stop_thread 会抛 ValueError
+ try:
+ running = _thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)
+ except Exception:
+ running = True
+ if running:
+ _thread.stop_thread(self.AI_VC_send_data_queue_thread_id)
except Exception as e:
usys.print_exception(e)
self.AI_VC_send_data_queue_thread_id = 0
if isinstance(data[1], str):#是字符串
self.VC_pingping_timerout = 0
- print("VC VC: send_data:{}".format(data[1]))
+ s = data[1]
+ if s is None:
+ s = ""
+ # 保活 ping 不落 WS IO 日志(避免周期性刷屏)
+ if VC_WS_IO_LOG and s != "ping":
+ try:
+ if len(s) > 240:
+ preview = s[:160] + " ... " + s[-60:]
+ else:
+ preview = s
+ print("[VC] WS send_txt len={} txt={}".format(len(s), preview))
+ except Exception:
+ try:
+ print("[VC] WS send_txt {}".format(data[1]))
+ except Exception:
+ pass
+ elif not VC_WS_IO_LOG:
+ try:
+ if s == "ping":
+ pass
+ else:
+ jd = ujson.loads(s)
+ print("[VC] tx event={}".format(jd.get("event", "?")))
+ except Exception:
+ print("[VC] tx len={} (non-json)".format(len(s)))
else:
- print("VC: send_data len:{}".format(len(data[1])))
+ pass
+ # if VC_AUDIO_FRAME_LOG:
+ # print("VC: send_data len:{}".format(len(data[1])))
# print("self.AI_client_VC = {} , self.AI_client_VC_isconnect = {}".format(self.AI_client_VC,self.AI_client_VC_isconnect))
if self.AI_client_VC != None and self.AI_client_VC_isconnect == 1:
self.AI_client_VC.send(data[1])
try:
self.websocket_error_count = 0
if isinstance(recv_data, str):#返回的是字符串
- print("VC VC:recv_data:{}".format(recv_data))
- jsload_data = ujson.loads(recv_data)
+ jsload_data = ujson.loads(recv_data)
+ if VC_WS_IO_LOG:
+ print("[VC] recv raw:{}".format(recv_data))
+ else:
+ try:
+ _rev = jsload_data.get("event", "")
+ if _rev != "llm.delta":
+ print("[VC] rx event={} sid={}".format(
+ _rev, jsload_data.get("sid", "") or "-"))
+ 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._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))
+ except Exception:
+ pass
if jsload_data["event"] == "started" :#会话开始
#payload = jsload_data["payload"]
# self.VC_dialog_id = payload["dialog_id"]
# self.VC_task_id = jsload_data["task_id"]
EventMesh.publish("open_led_blue")
+ # 会话开始:允许上行;连续对话模式下直接进入“可发送音频”状态
+ self._vc_session_started = 1
+ # 新会话开始必须允许连续对话自动开麦(防止上一轮 Close_flag 残留导致 Deny START)
+ self.Close_flag = 0
+ self._ptt_audio_created = 0
+ self._server_vad_silence = 0
+ self._turn_has_asr_completed = 0
+ self._turn_has_vad_silence = 0
+ self._turn_has_tts_started = 0
+ self._llm_completed_ms = 0
+ self._llm_text_pending_tts = ""
+ self._llm_force_tts_ms = 0
+ if self.VC_mode == 0:
+ # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后
+ # 出现首轮无音频上行导致“第一次对话没有”的情况。
+ if self.record_data_save_flag == 0 and self.VC_record_state_flag == 0:
+ self.request_uplink_start("server.started")
+ else:
+ self.VC_record_state_flag = 2
print("VC:Started!!!!!!!!\r\n")
print("TTS音频为:{},语音下发地址为:{}".format(self.AI_TTS_str,self.AI_new_voice_url))
if self.VC_mode == 0 :
self.VC_record_state_flag =2
else:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send_speech})
+ # Push-To-Talk:会话 started 后再发 audio.create(协议要求)
+ self.request_send_audio_create()
+ # 若长按发生在“未 started”窗口,收到 started 后立即开麦进入新一轮 PTT
+ try:
+ if getattr(self, "_ptt_force_record_on_started", 0) == 1:
+ self._ptt_force_record_on_started = 0
+ self.VC_Speech_Start_now()
+ except Exception:
+ pass
# if self.AI_new_voice_url != "":
self.VC_state = VC_State_None
elif jsload_data["event"] == "config.update" :#配置更新中,需要停止录音
self.VC_state = VC_State_End
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self.request_uplink_stop("config.update")
elif jsload_data["event"] == "asr.started" :#asr开始
self.VC_state = VC_State_SpeechStart
+ self._server_vad_silence = 0
+ self._turn_has_asr_completed = 0
+ self._turn_has_vad_silence = 0
+ self._turn_has_tts_started = 0
+ self._llm_completed_ms = 0
+ self._llm_text_pending_tts = ""
+ self._llm_force_tts_ms = 0
if self.VC_mode == 1 :
self.VC_record_state_flag = 2
self.LocalRespondingEnded_timerout = 0
self.AI_TTS_str = "我没听清,请再说一遍"
EventMesh.publish("open_led_blue")
elif jsload_data["event"] == "asr.completed" :#asr 终止的标识
- self.aud.play(1, 1, 'U:/static/think.mp3') #播放按键音
+ self._turn_has_asr_completed = 1
+ # 抢占规则:本地提示音不可打断流播放
+ try:
+ if EventMesh.publish("get_vc_stream_active") != 1:
+ self.aud.play(1, 1, 'U:/static/think.mp3') #播放按键音
+ except Exception:
+ pass
print("self.commands_mode:{}".format(self.commands_mode))
if self.commands_mode ==2:
ret= self.check_text(self.AI_ASR_str)
EventMesh.publish("open_led_all")
try:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self.request_uplink_stop("asr.completed/config_cmd")
EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
except Exception as e:
# if dev_type != "7PIN" :
#EventMesh.publish("publish_AI_btnInfo",{"func":self.send_tone})
+ elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听
+ if dev_type in ("7PIN_1_5", "7PIN", "7PIN_coze"):
+ self._server_vad_silence = 0
+ EventMesh.publish("open_led_blue")
elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识
- pass
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- elif jsload_data["event"] == "llm.created" :#会话开始
- EventMesh.publish("open_led_red_green")#
+ # 说明服务端已判定“本轮说话结束”;此时继续上行只会干扰后续(例如推迟 TTS)。
+ # 仅在“正在录音上行”的语音阶段才停止,避免误触发。
+ self._server_vad_silence = 1
+ self._turn_has_vad_silence = 1
+ self._uplink_mark_end("vad.silence")
+ if self.record_data_save_flag == 0:
+ if self.VC_record_state_flag != 0 and self.VC_state >= VC_State_SpeechStart:
+ self.request_uplink_stop("vad.silence")
+ elif jsload_data["event"] == "llm.created" :#思考(LLM)
+ if dev_type in ("7PIN_1_5", "7PIN", "7PIN_coze"):
+ EventMesh.publish("flicker_led_blue_green")
+ else:
+ EventMesh.publish("open_led_red_green")
self.VC_state = VC_State_Thinking
+ self._llm_completed_ms = 0
+ self._llm_text_pending_tts = ""
+ self._llm_force_tts_ms = 0
# EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
pass
+ elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)
+ try:
+ self._llm_completed_ms = utime.ticks_ms()
+ except Exception:
+ # 勿用 1:ticks_diff(now,1)≈开机至今,会瞬间满足 thinking 超时误以为该重启
+ self._llm_completed_ms = 0
+ # 若服务端没有自动触发 tts.started,则准备由端侧主动发 synthesis
+ try:
+ txt = jsload_data.get("payload", {}).get("content", "")
+ except Exception:
+ txt = ""
+ if isinstance(txt, str) and txt.strip() and VC_PROTO_GUARDS:
+ self._llm_text_pending_tts = txt.strip()
+ self._llm_force_tts_ms = self._llm_completed_ms
+ try:
+ self._cur_dialog_id = jsload_data.get("payload", {}).get("dialog_id", "") or ""
+ except Exception:
+ self._cur_dialog_id = ""
+ try:
+ self.fsm.log("server.llm.completed")
+ except Exception:
+ pass
+ pass
elif jsload_data["event"] == "llm.delta" :#llm返回数据
if self.is_VAD == 0 and self.record_data_save_flag == 0:
if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self.request_uplink_stop("llm.delta")
elif jsload_data["event"] == "tts.started" :#tts开始
self.VC_state = VC_State_TTSStart
- if self.is_VAD == 0 and self.record_data_save_flag == 0:
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
-
- #self.VC_Play_Start()
+ self._server_vad_silence = 1
+ self._turn_has_tts_started = 1
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ self._llm_text_pending_tts = ""
+ self._llm_force_tts_ms = 0
+ self._uplink_mark_end("tts.started")
+ # stop uplink + play_start 由 FSM action list 统一触发,避免多处重复调用
+ # 标记本轮 TTS 来源:
+ # - 有 payload.dialog_id:认为是“对话回复 TTS”
+ # - 无 dialog_id:认为是“提示/提醒 TTS”(模式切换可通过 _pending_tts_kind 标记)
+ try:
+ did = jsload_data.get("payload", {}).get("dialog_id", "") or ""
+ except Exception:
+ did = ""
+ # 注意:无 dialog_id 的 tts.started 很可能是提示音/提醒音;
+ # 但“模式切换提示音”的 kind 可能已在 synthesis 时写入 _last_tts_kind,
+ # 这里不应无条件覆盖为 prompt,否则播完无法触发下一轮开麦。
+ try:
+ if did:
+ self._last_tts_kind = "dialog"
+ else:
+ k = getattr(self, "_pending_tts_kind", "") or ""
+ if k:
+ self._last_tts_kind = k
+ else:
+ cur = getattr(self, "_last_tts_kind", "") or ""
+ if cur in ("mode_switch_long", "mode_switch_ptt"):
+ pass
+ elif not cur:
+ self._last_tts_kind = "prompt"
+ except Exception:
+ pass
+ try:
+ self._pending_tts_kind = ""
+ except Exception:
+ pass
EventMesh.publish("open_led_green")#绿色
elif jsload_data["event"] == "tts.completed" :#tts结束
EventMesh.publish("open_led_green")#绿色
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
- self.LocalRespondingEnded_timerout = 8
self.VC_state = VC_State_TTSEnd
+ try:
+ # server event 已在统一入口 on_server_event 触发 FSM,这里避免重复驱动
+ pass
+ except Exception:
+ pass
+ try:
+ try:
+ sid = getattr(self, "_cur_sid", "")
+ did = getattr(self, "_cur_dialog_id", "")
+ except Exception:
+ sid, did = "", ""
+ print("[TTS] completed in_len={} out_len={} q_empty={} play_pcm={} sid={} did={}".format(
+ getattr(self, "tts_in_data_len", 0),
+ getattr(self, "tts_out_data_len", 0),
+ 1 if self.AI_audio_queue.empty() else 0,
+ 1 if self.play_pcm != None else 0,
+ sid,
+ did,
+ ))
+ except Exception:
+ pass
+ # 播完上报完全由 FSM await_played + play_end 驱动,这里不再使用 LocalRespondingEnded_timerout/_need_audio_played_ack
pass
elif jsload_data["event"] == "canceled" :#打断
self.is_ruppt_tts = 0#设备上发中断tts,收到服务器返回的cancel后置为0
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_Start_now})#开始录音
+ self._need_audio_played_ack = 0
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ # 开麦由 FSM action list 统一触发(uplink_start:canceled),避免重复调用
if self.AI_audio_queue_thread_id ==0:
_thread.stack_size(24*1024)
self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())
else:
print("AI_VC_start :AI_audio_queue_thread_id != 0 ")
self.VC_state = VC_State_None
- elif jsload_data["event"] == "completed" :#会话结束 "event":"audio.convert.completed"
- #EventMesh.publish("open_led_green")#绿色
- #if self.VC_state >= VC_State_TTSStart:
- self.LocalRespondingEnded_timerout = 8
- self.VC_state = VC_State_End
+ elif jsload_data["event"] == "completed" :#会话结束(可能无 TTS,如 ASR 空)
+ # 重要:服务端常在 tts.completed 后紧跟 completed。
+ # 若本轮存在 TTS/待发 audio.played,则不能在此清零 LocalRespondingEnded_timerout/_need_audio_played_ack,
+ # 否则会掐断「播完→audio.played→下一轮录音」链路,表现为最多 1~2 轮就停。
+ if self._need_audio_played_ack == 1 or self.VC_state >= VC_State_TTSStart or self.VC_Play_flag == 1:
+ pass
+ else:
+ # 无 TTS 的 completed:清状态并按需进入下一轮(见下方条件)
+ self.LocalRespondingEnded_timerout = 0
+ self._need_audio_played_ack = 0
+ self.VC_state = VC_State_None
+ # 连续对话:若本轮没有 TTS(或服务端直接 completed),需要立刻回到聆听状态
+ # 否则会出现“有时 1 轮/2 轮后就停住”的现象(服务端 completed 但端侧未重新开录音上行)。
+ if self.VC_mode == 0 and self.Close_flag == 0:
+ # 仅当“本轮已结束一次语句(vad.silence 或 asr.completed)且从未进入 tts.started”
+ # 才自动进入下一轮;避免把真正的“会话结束/异常结束”误当成下一轮入口。
+ if (self._turn_has_tts_started == 0) and (self._turn_has_vad_silence == 1 or self._turn_has_asr_completed == 1):
+ self._server_vad_silence = 0
+ # 开麦由 FSM action list 统一触发(uplink_start:completed),这里不再重复调用
pass
elif jsload_data["event"] == "command" :#命令匹配
if self.AI_TTS_str =="":
self.AI_TTS_str = "抱歉,没找到对应操作,可以换个说法试试"
#EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self.request_uplink_stop("command")
EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
elif jsload_data["event"] == "failed":#接口返回错误
print("新的AI对话不允许以前的播放数据:")
break
- print("下发音频数据1:{}".format(len(recv_data)))
+ # if VC_AUDIO_FRAME_LOG:
+ # print("下发音频数据1:{}".format(len(recv_data)))
if self.play_data_save_flag == 1:
if self.play_fd ==None:
error_str = str(e)
#print("VC VC:e:{}".format(error_str))
if error_str.find("[Errno 110] ETIMEDOUT")!= -1 or error_str.find("[Errno 115] ")!= -1:
- print("VC VC:recv 正常报错:{}".format(error_str))
+ if VC_HEARTBEAT_LOG:
+ print("VC VC:recv 正常报错:{}".format(error_str))
+ # recv 超时仅表示本周期无帧,链路仍可能在等 LLM/TTS;清零避免误触发「接口无响应」
+ self.close_timerout = 0
pass
else:
usys.print_exception(e)
self.AI_client_VC_isconnect = 0
self.AI_client_VC_recv_id = 0
self.AI_client_VC_recv_lock = 0
- print("AI_client_VC_recv END!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ print("[VC] recv_thread exit")
def voice_play_10S(self,event,msg):#按键开启留言播放等待10S定时器判断
self.voice_timer.start(1000,1,self.voice_judge)
#print("下发音频数据1:{}".format(len(data[0])))
ret=self.AI_audio_queue.put(data)
+ try:
+ if self.tts_in_data_len == 0 and data and len(data[0]) > 0:
+ try:
+ sid = getattr(self, "_cur_sid", "")
+ did = getattr(self, "_cur_dialog_id", "")
+ except Exception:
+ sid, did = "", ""
+ print("[TTS] first_chunk len={} play_pcm={} VC_Play_flag={} sid={} did={}".format(
+ len(data[0]),
+ 1 if self.play_pcm != None else 0,
+ getattr(self, "VC_Play_flag", 0),
+ sid,
+ did,
+ ))
+ except Exception:
+ pass
self.tts_in_data_len += len(data[0])
#print("下发音频数据2:{},{}".format(len(data[0]),self.tts_in_data_len))
#msg[0]是tts播放字符串,msg[1]是是否关闭当前tts
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_tts_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.tts_out_data_len = 0
if self.VC_Play_flag == 0:
try:
+ # 本地提示音/文件播放可能占用同一音频通道,导致 TTS 流播放“写入成功但无声”;
+ # 在开启 TTS PCM 前先停掉本地播放,避免资源冲突。
+ try:
+ self.aud.stopPlayStream()
+ self.aud.stopAll()
+ except Exception:
+ pass
+ # 资源互斥:播放前确保录音上行已停止(无 AEC 场景)
+ try:
+ if self.record_data_save_flag == 0 and self.VC_record_state_flag != 0:
+ self._uplink_request_stop("VC_Play_Start")
+ utime.sleep_ms(30)
+ except Exception:
+ pass
if self.VC_play_TYPE == VC_play_TYPE_PCM:
- if self.play_pcm == None:
- self.play_pcm = audio.Audio.PCM(0,1,self.VC_play_samplerate,1,1,2)
- #self.play_pcm.setVolume(11)
- ret = self.play_pcm.stream_write_start()
- print("self.record_pcm.stream_write_start:ret:{}".format(ret))
+ if self.play_pcm == None:
+ # 刚停采集时延后打开播音 PCM,且失败时重试,降低「pcm open write only fail」
+ last_err = None
+ for i in range(3):
+ try:
+ utime.sleep_ms(80 + i * 60)
+ self.play_pcm = audio.Audio.PCM(0, 1, self.VC_play_samplerate, 1, 1, 2)
+ ret = self.play_pcm.stream_write_start()
+ print("self.record_pcm.stream_write_start:ret:{}".format(ret))
+ last_err = None
+ break
+ except Exception as e:
+ last_err = e
+ try:
+ if self.play_pcm != None:
+ self.play_pcm.close()
+ except Exception:
+ pass
+ self.play_pcm = None
+ if last_err:
+ raise last_err
elif self.VC_play_TYPE == VC_play_TYPE_MP3:
pass
self.VC_Play_flag = 1
self.aud.stopAll()
if self.AI_audio_queue.empty()!=True:
self.AI_audio_queue.clear()
+
+ # 播放结束后:退出“播报中”绿灯态。默认回到蓝灯(可说/待机态)。
+ try:
+ EventMesh.publish("open_led_blue")
+ except Exception:
+ pass
def _1S_timer_cb(self,*argc):
EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_})
+ def _fsm_timeout_table_tick(self, now_ms):
+ # 兜底统一入口:按“状态机状态 + 超时阈值 + 节流”驱动,避免兜底抢跑/多入口打架。
+ # 只在对应 turn/session 状态触发动作。
+ try:
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)
+ except Exception:
+ turn = VCTurnState.Idle
+
+ # 1) synthesis→tts.started 超时:只在 Thinking 期允许清 pending 并重启会话
+ # 根因:服务端未返回 tts.started 时,继续“回到聆听/开麦”会把本轮状态机拉扯得更乱;
+ # 这里直接重启会话,让协议重新进入 started→下一轮流程。
+ try:
+ if turn == VCTurnState.Thinking and self._tts_pending == 1 and self._tts_pending_ms:
+ try:
+ if self._fb_last_tts_pending_timeout_ms:
+ tts_gap_ok = utime.ticks_diff(now_ms, self._fb_last_tts_pending_timeout_ms) > 15000
+ else:
+ tts_gap_ok = True
+ except Exception:
+ tts_gap_ok = True
+ if utime.ticks_diff(now_ms, self._tts_pending_ms) > 8000 and tts_gap_ok:
+ self._fb_last_tts_pending_timeout_ms = now_ms
+ print("[VC][tts_pending] timeout, clear pending and restart session")
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ if self.VC_mode == 0 and self.Close_flag == 0:
+ self.VC_state = VC_State_None
+ EventMesh.publish("open_led_blue")
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ except Exception:
+ pass
+
+ # 2) thinking 超时:只在「llm.completed 已收到但 tts.started 长时间未到」时才重启会话
+ # 注意:不能以 llm.created 的时间戳作为超时基准,否则会在 LLM 正常推理期间误判超时(notts 场景)。
+ try:
+ if self.VC_mode == 0 and self.Close_flag == 0:
+ llm_completed_ms = getattr(self, "_llm_completed_ms", 0)
+ # 只有在 llm.completed 之后才允许超时重启
+ if turn == VCTurnState.Thinking and llm_completed_ms and self._turn_has_tts_started == 0:
+ age_ms = utime.ticks_diff(now_ms, llm_completed_ms)
+ # 有 force_tts pending 时不可用 6s 短超时:会与下方 force_tts(约 5s)抢跑 AI_VC_start,
+ # 打断 synthesis/会话;改为长超时兜底,仍以 throttle 为保护。
+ pending_for_force_tts = VC_PROTO_GUARDS and (
+ self._llm_text_pending_tts or self._llm_force_tts_ms)
+ restart_threshold_ms = 40000 if pending_for_force_tts else 20000
+ # _fb_last_thinking_restart_ms==0 时 ticks_diff(now,0) 非预期(间隔被误判极大),
+ # 会在 llm.completed 后首批 tick 就触发 AI_VC_start → notts(无 tts.started);首次仅信任 age 门限
+ try:
+ if self._fb_last_thinking_restart_ms:
+ gap_ok = utime.ticks_diff(now_ms, self._fb_last_thinking_restart_ms) > 15000
+ else:
+ gap_ok = True
+ except Exception:
+ gap_ok = True
+ if (age_ms > restart_threshold_ms and gap_ok and
+ self._tts_pending == 0 and self._need_audio_played_ack == 0 and self.VC_Play_flag == 0):
+ self._fb_last_thinking_restart_ms = now_ms
+ self._llm_completed_ms = 0
+ self._server_vad_silence = 0
+ self.VC_state = VC_State_None
+ try:
+ print("[VC][thinking_timeout] AI_VC_start age_ms={} thr={}".format(
+ age_ms, restart_threshold_ms))
+ except Exception:
+ pass
+ EventMesh.publish("open_led_blue")
+ # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ except Exception:
+ pass
+
+ # 3) llm→tts 兜底:仅 Thinking 期且足够等待/节流时触发;若仍在录音则先 stop,下一次 tick 再判断(受 VC_PROTO_GUARDS)
+ try:
+ if VC_PROTO_GUARDS and self._llm_text_pending_tts and self._llm_force_tts_ms and self._turn_has_tts_started == 0:
+ # 注意:服务端可能出现 llm.completed 后直接 completed(无 tts 事件)的情况,
+ # 此时 FSM turn 可能已回到 Listening;兜底不能硬绑 Thinking,否则会“只见 llm 不见 tts”。
+ if (turn not in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck) and
+ utime.ticks_diff(now_ms, self._llm_force_tts_ms) > 5000 and
+ utime.ticks_diff(now_ms, self._fb_last_force_tts_ms) > 15000 and
+ self._tts_pending == 0 and self._need_audio_played_ack == 0 and self.VC_Play_flag == 0):
+ self._fb_last_force_tts_ms = now_ms
+ if self.record_data_save_flag == 0 and self.VC_record_state_flag != 0:
+ self.request_uplink_stop("force_tts_pre_stop")
+ return
+ self._llm_force_tts_ms = 0
+ self.AI_TTS_str = self._llm_text_pending_tts
+ self._llm_text_pending_tts = ""
+ self._server_vad_silence = 1
+ if self.record_data_save_flag == 0:
+ self.request_uplink_stop("force_tts")
+ try:
+ sid = getattr(self, "_cur_sid", "")
+ did = getattr(self, "_cur_dialog_id", "")
+ except Exception:
+ sid, did = "", ""
+ try:
+ print("[VC][force_tts] send synthesis sid={} did={} turn={}".format(sid, did, turn))
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})
+ except Exception:
+ pass
+
def _1S_timer_cb_(self):
EventMesh.publish("system_sleep_rest")#防止黑屏
EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
#EventMesh.publish("sleep_BT")#休眠蓝牙
+ # 统一兜底表(按状态机状态触发)
+ try:
+ self._fsm_timeout_table_tick(utime.ticks_ms())
+ except Exception:
+ pass
+
+ # (不做额外兜底)动态切换依赖 FSM 状态正确清理后,由 server.tts.completed→local.play_end 驱动自动开麦
+
# print("get_audio_state !!!!!{} {} {}\r\n".format(self.play_end,EventMesh.publish("get_audio_state"),self.play_flag))
- if self.LocalRespondingEnded_timerout>0 :#
- self.LocalRespondingEnded_timerout -= 1
- print("LocalRespondingEnded_timerout:{}".format(self.LocalRespondingEnded_timerout))
- if self.LocalRespondingEnded_timerout <= 0 or self.play_pcm == None or(self.play_pcm != None and self.play_pcm.stream_is_empty() == 1) :#超时或者音频播放完,
- self.LocalRespondingEnded_timerout = 0
- if self.VC_mode == 1 :#长按单次问答模式
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- #EventMesh.publish("sleep_BT")#休眠蓝牙
- else:
- EventMesh.publish("open_led_blue")
- if self.Close_flag == 1:
- self.Close_flag = 0
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
- #EventMesh.publish("sleep_BT")#休眠蓝牙
- else:
- #utime.sleep_ms(1000)
-
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_End})
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.chenggong_tone})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_Start_now})#开始录音
+ # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====
+ try:
+ if self.VC_mode != 1:
+ if getattr(self.fsm, "await_played", False):
+ # 不能只用 stream_is_empty 判定“播完”,否则可能在队列仍有待播数据/统计尚未对齐时提前关播,出现“没声/一闪而过”。
+ is_pcm_empty = (self.play_pcm == None) or (self.play_pcm != None and self.play_pcm.stream_is_empty() == 1)
+ try:
+ q_empty = self.AI_audio_queue.empty()
+ except Exception:
+ q_empty = True
+ try:
+ in_len = getattr(self, "tts_in_data_len", 0)
+ out_len = getattr(self, "tts_out_data_len", 0)
+ except Exception:
+ in_len, out_len = 0, 0
+ drained = (in_len > 0 and out_len >= in_len and q_empty)
+ # 8s 超时:兜底认为播放结束(避免播放状态卡死)
+ too_long = False
+ try:
+ t0 = getattr(self.fsm, "last_tts_completed_ms", 0)
+ if t0:
+ too_long = utime.ticks_diff(utime.ticks_ms(), t0) > 8000
+ except Exception:
+ pass
+ if (is_pcm_empty and drained) or too_long:
+ EventMesh.publish("open_led_blue")
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})
+ self._fsm_apply_actions(self.fsm.on_local_play_end())
self.VC_state = VC_State_None
- #通知服务器播放结束
- #self.AI_client_VC_LocalRespondingEnded()
+ except Exception:
+ pass
if self.online_kws == 1:
if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:#要打开录音上传数据实现kws
pass
+
+ # thinking/llm→tts 等兜底已在 _fsm_timeout_table_tick 内统一处理
self.VC_pingping_timerout+=1
if self.VC_pingping_timerout>3:
self.VC_pingping_timerout=0
# if self.asr_pingping_timerout>3:
# self.asr_pingping_timerout=0
# self.AI_client_asr_ping()
- if self.VC_Thinking_timerout > 0 and self.VC_Thinking_timerout < 100:#VC Thinking超时后重新开始
- print("VC_Thinking_timerout:{}!!!!!!!!!!!!!!!!!!!!!".format(self.VC_Thinking_timerout))
- self.VC_Thinking_timerout -=1
- if self.VC_Thinking_timerout == 0:
- self.VC_Thinking_timerout = 100
- print("Thinking重试,重试!!!!!!!!!!!!!!!!!!!!!")
- self.VC_state = VC_State_None
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
- pass
+ # 历史遗留:VC_Thinking_timerout 的重启通道会在 llm.created 后抢跑触发 AI_VC_start,
+ # 直接打断本轮 LLM→TTS(notts 日志:llm.created 后立刻 AI_VC_start,导致无 tts.started)。
+ # 现在统一由 _fsm_timeout_table_tick(Thinking 超时) 负责重启会话,因此禁用该旧通道。
+ if self.VC_Thinking_timerout > 0 and self.VC_Thinking_timerout < 100:
+ self.VC_Thinking_timerout = 100
# def AI_kws_start(self):
# print("AI_kws_start")
# EventMesh.publish("wakeup_BT")
#VC开始
def AI_VC_start(self):
print("AI_VC_start1")
+ try:
+ print("[VC] build:{}".format(VC_BUILD_TAG))
+ except Exception:
+ pass
EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
self.VC_satrt_flag = 1
self.VC_state = VC_State_None
if self.AI_TTS_str == "" and self.AI_new_voice_url == "":#没有tts要播放的时候才开始录音 并音频路径为空
#开始asr录音
- self.VC_Speech_Start()#开启线程获取录音数据
+ self._uplink_request_start("AI_VC_start")#开启线程获取录音数据(统一入口)
# pass
if self.VC_mode == 0 :#and dev_type != "7PIN_BK_coze":#长对话模式打开vad
json_data["payload"]["inbound_attributes"]["disable_vad"] = False
self.VC_state = VC_State_None
+ # 新会话开始:在收到服务端 started 前,禁止发送音频帧(避免 session not initialized)
+ self._vc_session_started = 0
+ try:
+ self.fsm.on_session_start_request()
+ except Exception:
+ pass
self.AI_client_VC_send(ujson.dumps(json_data))
#开始上传语音
}
if self.VC_mode == 1:#单对话模式
- self.AI_client_VC_send(ujson.dumps(json_data))
+ # 协议门控(可用 VC_PROTO_GUARDS 临时关闭)
+ if (not VC_PROTO_GUARDS) or (self._vc_session_started == 1):
+ self._ptt_audio_created = 1
+ self.AI_client_VC_send(ujson.dumps(json_data))
},
}
self.AI_client_VC_send(ujson.dumps(json_data))
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ self._ptt_audio_created = 0
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"audio.completed"})
#客户端打断对话
"payload": {}
}
print("AI_client_VC_RequestToSpeak!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_init})
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send,"data":ujson.dumps(json_data)})
+ # 只做“打断+清播放队列”,不要在这里关音频队列线程(会导致后续 TTS/提示音不稳定,且触发竞态)
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_init})
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send,"data":ujson.dumps(json_data)})
utime.sleep_ms(1)
- # self.websocket_deconnect_VC()
- self.AI_audio_queue_thread_close()
self.VC_state = VC_State_None
# #self.Close()
# self.AI_VC_start()
#TTS播放
def AI_client_VC_synthesis(self):
+ # 幂等保护:在上一条 synthesis 还未等到 tts.started 前,不重复发(防止多兜底/多入口重复触发)
+ try:
+ if self._tts_pending == 1:
+ return
+ except Exception:
+ pass
json_data = \
{
"event": "synthesis",
"text": self.AI_TTS_str
}
}
+ # 语义标记只用于“是否自动开麦”的策略。
+ # 当前策略:长对话不自动开麦(需按键开始),因此这里不对提示音做自动开麦标记。
self.AI_TTS_str = ""
+ # 标记为“等待 tts.started”,避免在 synthesis→tts.started 窗口误开下一轮录音上行
+ self._tts_pending = 1
+ try:
+ self._tts_pending_ms = utime.ticks_ms()
+ except Exception:
+ self._tts_pending_ms = 0
+ try:
+ self.fsm.on_local_synthesis_sent()
+ except Exception:
+ pass
print("AI_client_VC_synthesis!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
# if self.VC_state == VC_State_RequestToRespond:
# print("AI_client_VC_RequestToRespond 重复 tts_str:{}\nasr_str:{}".format(self.AI_TTS_str,self.AI_ASR_str))
self.AI_client_VC_send(ujson.dumps(json_data))
+ # 金三 V2.5:AudioPlayed — 端侧 TTS 播报结束后上报,服务端进入下一轮对话(轻度重同步,不断开 WebSocket)
+ def AI_client_VC_audio_played(self):
+ # V2.5:播完上报;与 VC_PROTO_GUARDS 无关(见文件头说明)
+ try:
+ if self.AI_client_VC_isconnect != 1 or self.AI_client_VC == None:
+ return
+ json_data = {"event": "audio.played", "payload": {}}
+ print("AI_client_VC_audio_played")
+ self.AI_client_VC_send(ujson.dumps(json_data))
+ except Exception as e:
+ usys.print_exception(e)
def kws_even(self,event = None,msg= None):
EventMesh.publish("publish_AI_btnInfo",{"func":self.kws_even_cb})
def kws_even_cb(self):
self.AI_ASR_str = ""
self.AI_ASR_str_tmp = ""
self.VC_state = VC_State_None
- self.VC_Speech_End()
- self.VC_Speech_Start()
+ self._uplink_request_stop("kws_even_cb")
+ self._uplink_request_start("kws_even_cb")
self.AI_VC_start()
def set_vad_silence(self,event = None,msg= None):
self.VC_TTS_voice_key = "APP_voice"
EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
- print("VC_TTS_voice_type:{}".format(self.VC_TTS_voice_type))
- print("VC_TTS_voice_key:{}".format(self.VC_TTS_voice_key))
+ print("[VC] TTS_voice key={} map={}".format(self.VC_TTS_voice_key, self.VC_TTS_voice_type))
#修改音量大小 msg为1则为加大,msg为0则为减小,返回1成功,返回0失败
def change_volume(self,event =None, msg =None):
if msg == 0 :
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_single_voice_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.play_new_voice_flag = 1
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_new_voice_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.play_new_voice_flag = 1
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_his_voice_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.play_new_voice_flag = 1
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_wait_voice_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.play_new_voice_flag = 1
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_music_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
self.play_new_voice_flag = 1
self.AI_ASR_send_data_queue.clear()
EventMesh.publish("open_led_all")
self.AI_ASR_send_buf_len = 0
- self.VC_Speech_End()
+ self._uplink_request_stop("play_trans_tts_cb")
self.websocket_deconnect_VC()
self.AI_VC_start()
EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
break
#
count+=1
- if count%500 == 1:
+ if count%500 == 1 and VC_HEARTBEAT_LOG:
print("AI_audio_queue_thread on")
utime.sleep_ms(1)
#print("AI_audio_queue_thread")
#print("play_pcm.stream_write !!!")
ret = self.play_pcm.stream_write(write_buf,len(write_buf))
#ret= self.aud.playStream(1, data[0])#播放音频流
+ if self.tts_out_data_len == 0:
+ try:
+ sid = getattr(self, "_cur_sid", "")
+ did = getattr(self, "_cur_dialog_id", "")
+ except Exception:
+ sid, did = "", ""
+ print("[TTS] first_write ret={} len={} sid={} did={}".format(
+ ret, len(write_buf), sid, did
+ ))
+ if ret <= 0:
+ print("[TTS] stream_write ret={} len={} (no audio?)".format(ret, len(write_buf)))
self.tts_out_data_len += ret
#print("play_pcm.stream_write2:{}".format(self.tts_out_data_len))
elif self.VC_play_TYPE == VC_play_TYPE_MP3:
self.AI_ASR_check_timeout = 0
self.AI_ASR_error_timeout = 0
self.LocalRespondingEnded_timerout = 0
+ self._need_audio_played_ack = 0
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ self._vc_session_started = 0
+ self._ptt_audio_created = 0
self.is_ruppt_tts = 0#打断标志位清零
print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
EventMesh.publish("close_led")
except Exception as e:
usys.print_exception(e)
- self.VC_Speech_End()
+ # Close 可能被不同线程触发:统一切回主线程执行 stop
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"Close"})
print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
if self.AI_audio_queue.empty()!=True:
self.AI_audio_queue.clear()
def VC_init(self):
-
- print("VC_init:AI_VC_send_data_queue:size:{}".format(self.AI_audio_queue.size()))
-
-
- print("VC_init2")
- #self.VC_Speech_End()
- print("VC_init3")
+ _q0 = self.AI_audio_queue.size()
self.VC_Play_End()
- print("VC_init4")
-
if self.AI_audio_queue.empty()!=True:
self.AI_audio_queue.clear()
- print("VC_init:AI_VC_send_data_queue:end size:{}".format(self.AI_audio_queue.size()))
+ print("[VC] init audio_queue {} -> {}".format(_q0, self.AI_audio_queue.size()))
# num = EventMesh.publish("get_volum_size")
# if num == None:
# num = 8