-# -*- coding: utf-8 -*-\r
-"""语音对话 WebSocket / ASR / TTS 主业务。"""\r
-from common import Abstract\r
-try:\r
- from qp_log import qp_print as print\r
-except Exception:\r
- pass\r
-import EventMesh\r
-import uwebsocket\r
-import usys\r
-import gc\r
-import urandom\r
-import uhashlib\r
-import ubinascii\r
-import ujson\r
-import osTimer\r
-import _thread\r
-from queue import Queue\r
-import utime\r
-import audio\r
-import uos\r
-from machine import Pin\r
-import modem\r
-import sim\r
-import net\r
-import base64\r
-import request\r
-import ql_fs\r
-from misc import Power\r
-import ntptime\r
-import machine\r
-import dataCall\r
-import constants\r
-import versions\r
-import version_util\r
-try:\r
- import vc_config\r
-except Exception:\r
- vc_config = None\r
-try:\r
- import vc_text_intent\r
-except Exception:\r
- vc_text_intent = None\r
-try:\r
- import VAD\r
-except Exception:\r
- pass\r
-try:\r
- import usr.ai_alarm as _ai_alarm\r
-except Exception:\r
- import ai_alarm as _ai_alarm\r
-from vc_common import (\r
- dev_type,\r
- VC_HEARTBEAT_LOG,\r
- VC_PROTO_GUARDS,\r
- VC_WS_IO_LOG,\r
- _VC_KEEPALIVE_RX_EVENTS,\r
- _vc_out_json_event_meta,\r
- PRESS_AUDIO_MODE,\r
-)\r
-from vc_fsm import (\r
- VCSessionState,\r
- VCTurnState,\r
- VC_TtsRxPhase,\r
- VCMode,\r
- VCStateMachine,\r
- VC_record_TYPE_PCM,\r
- VC_record_TYPE_AMR,\r
- VC_play_TYPE_PCM,\r
- VC_play_TYPE_MP3,\r
- AI_client_VC_num,\r
- AI_client_asr_num,\r
-)\r
-from vc_ota import OTA\r
-from vc_utils import num_str_to_chinese\r
-\r
-class AI_VC(Abstract):\r
- def get_vc_tts_playback_active(self, *args, **kwargs):\r
- try:\r
- return 1 if self._vc_tts_playback_active() else 0\r
- except Exception:\r
- return 0\r
-\r
- def get_vc_stream_active(self, *args, **kwargs):\r
- # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。\r
- try:\r
- if self._vc_tts_playback_active():\r
- return 1\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self.fsm, "await_played", False):\r
- return 1\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:\r
- return 1\r
- except Exception:\r
- pass\r
- try:\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
- return 1\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "_tts_pending", 0) == 1:\r
- return 1\r
- except Exception:\r
- pass\r
- return 0\r
-\r
- def _vc_fsm_log_snip(self):\r
- try:\r
- f = self.fsm\r
- return "sess={} turn={} tts_rx={} await={} sp_up={}".format(\r
- int(getattr(f, "session", -1)),\r
- int(getattr(f, "turn", -1)),\r
- int(getattr(f, "tts_rx_phase", -1)),\r
- 1 if getattr(f, "await_played", False) else 0,\r
- 1 if getattr(f, "speech_uplink_open", 0) else 0,\r
- )\r
- except Exception:\r
- return "fsm=?"\r
-\r
- def _vc_tts_pipeline_busy(self):\r
- try:\r
- if getattr(self.fsm, "await_played", False):\r
- return True\r
- if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:\r
- return True\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
- return True\r
- if getattr(self, "_tts_pending", 0) == 1:\r
- return True\r
- if getattr(self, "VC_Play_flag", 0) == 1:\r
- return True\r
- if self._vc_local_play_busy():\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _vc_local_play_busy(self):\r
- try:\r
- if getattr(self, "_vc_local_play_active", 0) != 1:\r
- return False\r
- until = getattr(self, "_vc_local_play_until_ms", 0)\r
- if until and utime.ticks_diff(until, utime.ticks_ms()) > 0:\r
- return True\r
- self._vc_local_play_active = 0\r
- self._vc_local_play_until_ms = 0\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _vc_capture_allowed(self, reason=""):\r
- """采集/上行开麦前:下行播放或本地短音未结束则禁止。"""\r
- try:\r
- if self._vc_tts_pipeline_busy():\r
- return False\r
- if self._vc_local_play_busy():\r
- return False\r
- except Exception:\r
- return False\r
- return True\r
-\r
- def _vc_stop_downlink_for_interrupt(self, reason="interrupt"):\r
- """详设 DES-SCN-003:打断/取消时先本地停播清队列,再允许开麦。"""\r
- try:\r
- if getattr(self, "AI_audio_queue", None) is not None:\r
- if self.AI_audio_queue.empty() != True:\r
- self.AI_audio_queue.clear()\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_tts_pipeline_busy() or getattr(self, "is_ruppt_tts", 0) == 1:\r
- self._drop_tts_audio = 1\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_hard_mute_local_playback()\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
- self.VC_Play_End()\r
- except Exception:\r
- pass\r
- try:\r
- self._vc_local_play_active = 0\r
- self._vc_local_play_until_ms = 0\r
- except Exception:\r
- pass\r
- try:\r
- self.fsm.await_played = False\r
- self._need_audio_played_ack = 0\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- except Exception:\r
- pass\r
- try:\r
- pending_intr = getattr(self, "_vc_pending_interrupt_uplink", 0) == 1\r
- if (\r
- not pending_intr\r
- and getattr(self, "VC_record_state_flag", 0) != 0\r
- and getattr(self, "record_data_save_flag", 0) == 0\r
- ):\r
- self._uplink_request_stop("stop_downlink/" + str(reason))\r
- except Exception:\r
- pass\r
-\r
- def _vc_local_play(self, path, play_mode=1, loop=1, hold_ms=650):\r
- """本地 MP3 短音:播前停录/停流式 TTS,避免与麦克风采集叠用。"""\r
- try:\r
- if getattr(self, "record_data_save_flag", 0) == 0 and getattr(self, "VC_record_state_flag", 0) != 0:\r
- self._uplink_request_stop("local_play")\r
- utime.sleep_ms(30)\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_tts_playback_active():\r
- self._ptt_hard_mute_local_playback()\r
- try:\r
- self.VC_Play_End()\r
- except Exception:\r
- pass\r
- except Exception:\r
- pass\r
- try:\r
- hold = int(hold_ms)\r
- if hold < 200:\r
- hold = 200\r
- self._vc_local_play_active = 1\r
- self._vc_local_play_until_ms = utime.ticks_add(utime.ticks_ms(), hold)\r
- except Exception:\r
- pass\r
- try:\r
- self.aud.play(play_mode, loop, path)\r
- except Exception as e:\r
- try:\r
- self._vc_local_play_active = 0\r
- self._vc_local_play_until_ms = 0\r
- except Exception:\r
- pass\r
- usys.print_exception(e)\r
-\r
- def _vc_tts_playback_active(self):\r
- """仅服务端 TTS/PCM 流在播(VC_Play_Start);aud.play 本地短音不算。"""\r
- try:\r
- if getattr(self, "VC_Play_flag", 0) == 1:\r
- return True\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "play_pcm", None) is not None:\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _vc_vad_uplink_stop_eligible(self):\r
- # 对应旧 VC_state >= SpeechStart\r
- try:\r
- if getattr(self.fsm, "speech_uplink_open", 0):\r
- return True\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- return t not in (VCTurnState.Idle, VCTurnState.Listening)\r
- except Exception:\r
- return False\r
-\r
- def _vc_long_idle_active_speech_or_later(self):\r
- # 对应旧 VC_state >= SpeechIng\r
- try:\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- return t not in (VCTurnState.Idle, VCTurnState.Listening)\r
- except Exception:\r
- return False\r
-\r
- def _vc_is_business_idle(self):\r
- # Level2:LED ai_state 为 idle,且无录音/流播/音乐闹钟等(与 WS ping 无关)\r
- try:\r
- if getattr(self, "music_play_flag", 0) == 1:\r
- return False\r
- if getattr(self, "alarm_play_flag", 0) == 1:\r
- return False\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- return False\r
- if getattr(self, "VC_Play_flag", 0) != 0:\r
- return False\r
- except Exception:\r
- pass\r
- try:\r
- if EventMesh.publish("get_vc_stream_active") == 1:\r
- return False\r
- except Exception:\r
- pass\r
- try:\r
- st = EventMesh.publish("get_led_debug_state")\r
- if st and isinstance(st, dict):\r
- if (st.get("ai") or "") != "idle":\r
- return False\r
- except Exception:\r
- return False\r
- return True\r
-\r
- def _vc_idle_deep_sleep_impl(self):\r
- try:\r
- _need_close = (self.VC_satrt_flag == 1) or (getattr(self, "AI_client_VC_isconnect", 0) == 1)\r
- print("[VC][idle_L2] 触发休眠流程 need_Close={} VC_mode={}".format(\r
- 1 if _need_close else 0, getattr(self, "VC_mode", -1)))\r
- except Exception:\r
- pass\r
- try:\r
- if self.VC_satrt_flag == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:\r
- try:\r
- print("[VC][idle_L2] 调用 Close 断开会话/链路")\r
- except Exception:\r
- pass\r
- self.Close()\r
- except Exception:\r
- pass\r
- try:\r
- utime.sleep_ms(100)\r
- except Exception:\r
- pass\r
- try:\r
- print("[VC][idle_L2] 发布 Hsensor_set_sleep")\r
- EventMesh.publish("Hsensor_set_sleep")\r
- except Exception:\r
- pass\r
- try:\r
- print("[VC][idle_L2] 发布 SYSTEM_SLEEP ({})".format(constants.EVENT.SYSTEM_SLEEP))\r
- EventMesh.publish(constants.EVENT.SYSTEM_SLEEP)\r
- except Exception:\r
- pass\r
-\r
- def _vc_rx_resets_long_idle_timer(self, recv_data):\r
- """是否应因本帧下行清零 close_timerout(排除保活/心跳,含 V2.5 文本 ping 对端应答)。"""\r
- try:\r
- if recv_data is None:\r
- return False\r
- if isinstance(recv_data, (bytes, bytearray)):\r
- return True\r
- if not isinstance(recv_data, str):\r
- return False\r
- t = recv_data.strip()\r
- if not t:\r
- return False\r
- tl = t.lower()\r
- if tl in ("ping", "pong"):\r
- return False\r
- try:\r
- jd = ujson.loads(t)\r
- except Exception:\r
- return True\r
- ev = jd.get("event")\r
- if ev is None or ev == "":\r
- ev = jd.get("type")\r
- if ev is None:\r
- return True\r
- if str(ev).strip().lower() in _VC_KEEPALIVE_RX_EVENTS:\r
- return False\r
- return True\r
- except Exception:\r
- return True\r
-\r
- # -------------------------------------------------------------------------\r
- # 电源键 / AI 键:按 VC_mode 拆函数,便于阅读与修改(长对话 vs 单对话 PTT)\r
- # -------------------------------------------------------------------------\r
- def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):\r
- """电源键短按:读留言标志、退会议/翻译;否则 Close,或单键模式下走 AI_key_release。"""\r
- print("pwk_key_release")\r
- if EventMesh.publish("get_recv_voice") == 1:\r
- EventMesh.publish("update_play_new_voice_info")\r
- self.recv_voice = 0\r
- EventMesh.publish("set_history_voice_flag", 0)\r
- self.AI_new_voice_url = ""\r
- if self.trans_flag == 1 or self.meet_flag == 1:\r
- EventMesh.publish("exit_meet_mode")\r
- EventMesh.publish("exit_trans_mode")\r
- return\r
- if merged_ai_tail:\r
- self.AI_key_release(event, msg)\r
- else:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
- EventMesh.publish("stop_sos_call")\r
-\r
- def _ai_key_press_ptt_mode(self):\r
- """单对话 PTT:短按仅停 TTS(不换 sid);非播放态短按吞掉;长按由 AI_key_long_press→ptt_begin。"""\r
- if self.VC_mode != 1:\r
- return False\r
- try:\r
- playing = False\r
- try:\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- playing = t in (VCTurnState.TtsReceiving, VCTurnState.Playing)\r
- except Exception:\r
- playing = False\r
- if playing or self._vc_tts_pipeline_busy():\r
- self._ptt_short_press_stop = 1\r
- try:\r
- self._ptt_hard_mute_local_playback()\r
- except Exception:\r
- pass\r
- try:\r
- self.VC_Play_End()\r
- except Exception:\r
- pass\r
- try:\r
- self.fsm.await_played = False\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_led_lock_listening = 0\r
- self._ptt_interrupt_active = 0\r
- self.is_ruppt_tts = 0\r
- except Exception:\r
- pass\r
- try:\r
- self._vc_publish_led("idle", "ptt_short_press/stop_tts")\r
- except Exception:\r
- pass\r
- return True\r
- except Exception:\r
- pass\r
- return True\r
-\r
- def _ai_key_press_long_dialogue_interrupt(self):\r
- """长对话:TTS/播放忙或 turn==Thinking 时短按 → RequestToSpeak(先 cancel,canceled 后再开麦)。"""\r
- if self.VC_mode != 0:\r
- return False\r
- try:\r
- if getattr(self, "AI_client_VC_isconnect", 0) != 1 or getattr(self, "_vc_session_started", 0) != 1:\r
- return False\r
- except Exception:\r
- return False\r
- need_cancel = False\r
- if self._vc_tts_pipeline_busy():\r
- need_cancel = True\r
- else:\r
- try:\r
- if getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking:\r
- need_cancel = True\r
- except Exception:\r
- pass\r
- if not need_cancel:\r
- return False\r
- print("[KEY] long interrupt -> RequestToSpeak (cancel then uplink)")\r
- self.is_ruppt_tts = 1\r
- try:\r
- self._vc_pending_interrupt_uplink = 1\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})\r
- return True\r
-\r
- def _ai_key_press_long_dialogue_wake(self):\r
- """长对话未连接或未 started:直接 AI_VC_start,勿 Close+sleep。"""\r
- if self.VC_mode != 0:\r
- return False\r
- if getattr(self, "voice_flag", 0) == 1 or getattr(self, "meet_flag", 0) == 1 or getattr(self, "trans_flag", 0) == 1:\r
- return False\r
- try:\r
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
- return False\r
- except Exception:\r
- pass\r
- print("[KEY] long wake AI_VC_start (no Close)")\r
- try:\r
- self.Close_flag = 0\r
- except Exception:\r
- pass\r
- self.get_config_timer_count = 0\r
- self.key_state = 1\r
- self.AI_TTS_str = ""\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self.AI_VC_start()\r
- self.AI_key_long_press_flag = 0\r
- return True\r
-\r
- def _vc_led_alert_rb_flash(self):\r
- try:\r
- EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})\r
- except Exception:\r
- pass\r
-\r
- def _vc_clear_stale_record_state(self, reason=""):\r
- """record_state_flag 非 0 但可能无真实录音时先停录复位,避免 uplink 门控拒开麦。"""\r
- try:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- self.VC_Speech_End()\r
- except Exception:\r
- try:\r
- self.VC_record_state_flag = 0\r
- except Exception:\r
- pass\r
-\r
- def _ai_key_press_long_dialogue_when_session_ready(self):\r
- """长对话已连接且 session 已 started:非播放态直接开麦续聊(勿 idle 时发 cancel)。"""\r
- if self.VC_mode != 0:\r
- return False\r
- try:\r
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
- # 模式切换提示音会置 Close_flag=1;用户再短按须恢复可开麦\r
- try:\r
- self.Close_flag = 0\r
- except Exception:\r
- pass\r
- self._vc_clear_stale_record_state("key.press/continue")\r
- self.request_uplink_start("key.press/continue")\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _ai_key_release_handle_ptt(self):\r
- """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""\r
- if self.AI_key_long_press_flag == 1:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- try:\r
- gap = 200\r
- if "vc_config" in globals() and vc_config:\r
- gap = int(vc_config.get_int("PTT_RELEASE_END_SPEECH_DELAY_MS", 200))\r
- if gap > 0:\r
- utime.sleep_ms(gap)\r
- except Exception:\r
- pass\r
- print("[PTT] release end_speech asr_str={!r} rec={}".format(\r
- self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))\r
- self.AI_client_VC_end_speech()\r
- else:\r
- self.AI_key_long_press_flag = 0\r
- try:\r
- if getattr(self, "_ptt_short_press_stop", 0) == 1:\r
- self._ptt_short_press_stop = 0\r
- except Exception:\r
- pass\r
- print("[PTT] release_no_uplink_skip")\r
- return\r
- try:\r
- if getattr(self, "_ptt_short_press_stop", 0) == 1:\r
- self._ptt_short_press_stop = 0\r
- except Exception:\r
- pass\r
- print("[PTT] release_short_no_long")\r
-\r
- def _ai_key_long_press_route(self, event=None, msg=None):\r
- """长按:翻译先 Close 再开翻译录音;单对话→ptt_begin;长对话→与短按一致走 AI_key_press 链路。"""\r
- if self.trans_flag == 1:\r
- self.Close()\r
- EventMesh.publish("start_voice_record", 4)\r
- self.AI_key_long_press_flag = 1\r
- if self.VC_mode == 1:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.ptt_begin})\r
- else:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})\r
-\r
- # ===== Push-To-Talk(单对话模式)=====\r
- # 语义:\r
- # - 长按:无论当前处于 TTS/Thinking/任何中间态,立即打断并进入新一轮录音上行\r
- # - 松开:停止录音并等待服务端返回(由 AI_client_VC_end_speech 触发)\r
- def _ptt_end_led_suppress(self):\r
- # 保持 _vc_play_end_suppress_idle_led / _ptt_led_lock_listening 直至真开录(VC_Speech_Start)\r
- pass\r
-\r
- def _ptt_should_drop_inbound_tts(self):\r
- """单对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""\r
- try:\r
- if getattr(self, "VC_mode", -1) != 1:\r
- return False\r
- if getattr(self, "_drop_tts_audio", 0) == 1:\r
- return True\r
- if getattr(self, "is_ruppt_tts", 0) == 1:\r
- return True\r
- if getattr(self, "_ptt_interrupt_active", 0) == 1:\r
- return True\r
- if getattr(self, "_vc_serial_canceling", 0) == 1:\r
- return True\r
- if getattr(self, "_ptt_pending_arm", 0) == 1:\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _ptt_should_skip_remote_playback(self):\r
- return self._ptt_should_drop_inbound_tts()\r
-\r
- def _ptt_enter_interrupt_arm(self):\r
- try:\r
- self._ptt_arm_epoch = int(getattr(self, "_ptt_arm_epoch", 0)) + 1\r
- self._ptt_interrupt_active = 1\r
- self._ptt_led_lock_listening = 1\r
- self._vc_play_end_suppress_idle_led = 1\r
- self._drop_tts_audio = 1\r
- self.is_ruppt_tts = 1\r
- except Exception:\r
- pass\r
-\r
- def _ptt_abort_interrupt_arm(self):\r
- try:\r
- self._ptt_interrupt_active = 0\r
- self._ptt_led_lock_listening = 0\r
- self._vc_play_end_suppress_idle_led = 0\r
- self._drop_tts_audio = 0\r
- self.is_ruppt_tts = 0\r
- except Exception:\r
- pass\r
-\r
- def _ptt_on_record_hardware_started(self):\r
- """真开录后:允许新 TTS、放开灯锁;旧 TTS 已在打断期丢弃。"""\r
- try:\r
- self._ptt_interrupt_active = 0\r
- self._ptt_led_lock_listening = 0\r
- self._vc_play_end_suppress_idle_led = 0\r
- self._drop_tts_audio = 0\r
- self.is_ruppt_tts = 0\r
- except Exception:\r
- pass\r
-\r
- def _ptt_hard_mute_local_playback(self):\r
- try:\r
- self.VC_Play_flag = 0\r
- if getattr(self, "VC_play_TYPE", None) == VC_play_TYPE_PCM:\r
- if getattr(self, "play_pcm", None) is not None:\r
- try:\r
- self.play_pcm.stream_stop()\r
- self.play_pcm.close()\r
- except Exception:\r
- pass\r
- self.play_pcm = None\r
- try:\r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- except Exception:\r
- pass\r
- if getattr(self, "AI_audio_queue", None) is not None:\r
- try:\r
- if self.AI_audio_queue.empty() != True:\r
- self.AI_audio_queue.clear()\r
- except Exception:\r
- pass\r
- except Exception:\r
- pass\r
- try:\r
- gap = 0\r
- if "vc_config" in globals() and vc_config:\r
- gap = int(vc_config.get_int("PTT_POST_MUTE_MS", 30))\r
- if gap > 0:\r
- utime.sleep_ms(gap)\r
- except Exception:\r
- pass\r
-\r
- def battery_priority_mute_vc(self, event=None, msg=None):\r
- """低电/关机提示音:短暂停 VC TTS,避免被流式播放挡住。"""\r
- try:\r
- self._ptt_hard_mute_local_playback()\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
- self.VC_Play_End()\r
- except Exception:\r
- pass\r
-\r
- def _vc_enqueue_btn(self, func, data=None):\r
- """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""\r
- try:\r
- if data is not None:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})\r
- else:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": func})\r
- except Exception:\r
- pass\r
-\r
- def _vc_ptt_cancel_wait_ms(self):\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- return int(vc_config.get_int("PTT_CANCEL_WAIT_MS", 2500))\r
- except Exception:\r
- pass\r
- return 2500\r
-\r
- def _vc_ptt_need_interrupt_wait(self):\r
- """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。"""\r
- try:\r
- if getattr(self, "VC_mode", -1) != 1:\r
- return False\r
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- if self._vc_tts_pipeline_busy():\r
- return True\r
- if turn in (\r
- VCTurnState.Thinking, VCTurnState.TtsReceiving,\r
- VCTurnState.Playing, VCTurnState.AwaitPlayedAck,\r
- ):\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):\r
- try:\r
- self._ptt_pending_arm = 1\r
- self._vc_serial_canceling = 1\r
- try:\r
- self._vc_cancel_deadline_ms = utime.ticks_add(\r
- utime.ticks_ms(), self._vc_ptt_cancel_wait_ms())\r
- except Exception:\r
- self._vc_cancel_deadline_ms = 0\r
- json_data = {"event": "cancel", "payload": {}}\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- self._drop_tts_audio = 1\r
- print("[VC][serial] cancel_wait reason={} turn={}".format(\r
- reason, getattr(getattr(self, "fsm", None), "turn", "?")))\r
- except Exception:\r
- pass\r
-\r
- def _vc_on_ptt_cancel_settled(self, via="canceled"):\r
- """btn 线程:cancel 已收尾,执行挂起的 PTT 开录。"""\r
- try:\r
- if not getattr(self, "_vc_serial_canceling", 0) and not getattr(self, "_ptt_pending_arm", 0):\r
- return\r
- self._vc_serial_canceling = 0\r
- self._vc_cancel_deadline_ms = 0\r
- if getattr(self, "_ptt_pending_arm", 0):\r
- self._ptt_pending_arm = 0\r
- print("[VC][serial] ptt_arm after {}".format(via))\r
- self._ptt_arm_uplink()\r
- except Exception:\r
- pass\r
-\r
- def _vc_tick_serial_cancel_timeout(self):\r
- if not getattr(self, "_vc_serial_canceling", 0):\r
- return\r
- try:\r
- dl = getattr(self, "_vc_cancel_deadline_ms", 0)\r
- if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:\r
- print("[VC][serial] cancel_wait timeout -> arm")\r
- self._vc_on_ptt_cancel_settled(via="timeout")\r
- except Exception:\r
- pass\r
-\r
- def _vc_fsm_dispatch_cb(self, data):\r
- """btn 线程:处理服务端 FSM 事件(单对话串行)。"""\r
- try:\r
- ev, jsload = data\r
- except Exception:\r
- return\r
- try:\r
- actions = self.fsm.on_server_event(ev, jsload)\r
- self._fsm_apply_actions(actions)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- if ev == "canceled":\r
- self._vc_on_ptt_cancel_settled(via="canceled")\r
-\r
- def _vc_dispatch_server_event(self, ev, jsload_data=None):\r
- try:\r
- if ev == "canceled":\r
- self._vc_stop_downlink_for_interrupt("canceled")\r
- except Exception:\r
- pass\r
- if getattr(self, "VC_mode", -1) == 1:\r
- self._vc_enqueue_btn(self._vc_fsm_dispatch_cb, (ev, jsload_data))\r
- else:\r
- try:\r
- self._fsm_apply_actions(self.fsm.on_server_event(ev, jsload_data))\r
- except Exception:\r
- pass\r
-\r
- def _ptt_arm_uplink(self, t0=0):\r
- """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""\r
- try:\r
- self.is_ruppt_tts = 0\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_audio_created = 0\r
- except Exception:\r
- pass\r
- try:\r
- self.AI_ASR_flag = 0\r
- except Exception:\r
- pass\r
- try:\r
- self.AI_client_VC_send_speech()\r
- except Exception:\r
- pass\r
- try:\r
- self.VC_Speech_Start_now()\r
- except Exception:\r
- pass\r
- try:\r
- self._vc_publish_led("listening", "ptt_begin/recording")\r
- except Exception:\r
- pass\r
- self._ptt_end_led_suppress()\r
- try:\r
- if t0:\r
- print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))\r
- except Exception:\r
- pass\r
-\r
- def ptt_begin(self, reason="key.long_press"):\r
- try:\r
- if self.VC_mode != 1:\r
- return\r
- except Exception:\r
- return\r
-\r
- # PTT 新一轮开始:清理模式切换/提示音退出标志\r
- try:\r
- self.Close_flag = 0\r
- self._ptt_await_server = 0\r
- except Exception:\r
- pass\r
-\r
- t0 = 0\r
- try:\r
- t0 = utime.ticks_ms()\r
- except Exception:\r
- t0 = 0\r
-\r
- self._ptt_enter_interrupt_arm()\r
- try:\r
- self._vc_publish_led("listening", "ptt_begin/pre")\r
- except Exception:\r
- pass\r
-\r
- # 本地先硬静音再 VC_init,避免 DMA/队列尾包漏播\r
- try:\r
- self._ptt_hard_mute_local_playback()\r
- except Exception:\r
- pass\r
- try:\r
- self.VC_init()\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_Play_flag", 0) == 1 or getattr(self, "play_pcm", None) is not None:\r
- self.VC_Play_End()\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_hard_mute_local_playback()\r
- except Exception:\r
- pass\r
-\r
- # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错\r
- try:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- self.VC_Speech_End()\r
- except Exception:\r
- pass\r
-\r
- # 已在等 canceled:只刷新「待开录」,勿重复 audio.create\r
- try:\r
- if getattr(self, "_vc_serial_canceling", 0):\r
- self._ptt_pending_arm = 1\r
- print("[PTT] cancel in progress, refresh pending arm")\r
- self._ptt_end_led_suppress()\r
- return\r
- except Exception:\r
- pass\r
-\r
- # 会话未连接:先建链并发 Start,等待 started 后再自动开麦\r
- try:\r
- if getattr(self, "AI_client_VC_isconnect", 0) != 1:\r
- self._ptt_force_record_on_started = 1\r
- self.AI_VC_start()\r
- return\r
- except Exception:\r
- self._ptt_force_record_on_started = 1\r
- try:\r
- self.AI_VC_start()\r
- except Exception:\r
- pass\r
- return\r
-\r
- # 会话还没 initialized:先发 Start,请求服务端进入 started\r
- if getattr(self, "_vc_session_started", 0) != 1:\r
- self._ptt_force_record_on_started = 1\r
- try:\r
- try:\r
- if t0:\r
- print("[PTT] wait server.started (ms={})".format(utime.ticks_ms() - t0))\r
- except Exception:\r
- pass\r
- self.AI_client_VC_start_requset()\r
- except Exception:\r
- pass\r
- return\r
-\r
- # 服务端仍在思考/TTS:先 cancel,在 btn 队列收到 canceled 后再开录(方案甲串行)\r
- if self._vc_ptt_need_interrupt_wait():\r
- self._vc_begin_ptt_cancel_wait("ptt_begin")\r
- return\r
-\r
- self._ptt_arm_uplink(t0)\r
-\r
- def _vc_effective_asr_text(self):\r
- try:\r
- t = (getattr(self, "AI_ASR_str", "") or "").strip()\r
- if t:\r
- return t\r
- return (getattr(self, "_last_asr_result_text", "") or "").strip()\r
- except Exception:\r
- return ""\r
-\r
- def _vc_long_dialogue_auto_uplink_ok(self, reason):\r
- """长对话 canceled/completed 自动开麦:须有有效 ASR,且防抖/勿在 LLM 无 TTS 后抢开。"""\r
- try:\r
- if reason not in ("canceled", "completed", "local.play_end", "audio.played"):\r
- return True\r
- if getattr(self, "Close_flag", 0) == 1:\r
- return False\r
- if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:\r
- pass\r
- elif not self._vc_effective_asr_text():\r
- try:\r
- print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))\r
- except Exception:\r
- pass\r
- return False\r
- if reason == "completed":\r
- if getattr(self, "_turn_has_tts_started", 0) == 0:\r
- try:\r
- if getattr(self, "_llm_completed_ms", 0) or getattr(self.fsm, "turn", None) == VCTurnState.Thinking:\r
- print("[VC][uplink_ctrl] Deny START by completed cause=llm_without_tts")\r
- return False\r
- except Exception:\r
- pass\r
- try:\r
- now = utime.ticks_ms()\r
- last = getattr(self, "_vc_last_auto_uplink_ms", 0)\r
- if last and utime.ticks_diff(now, last) < 1200:\r
- print("[VC][uplink_ctrl] Deny START by {} cause=auto_uplink_debounce".format(reason))\r
- return False\r
- self._vc_last_auto_uplink_ms = now\r
- except Exception:\r
- pass\r
- except Exception:\r
- pass\r
- return True\r
-\r
- def _vc_led_state_debug_enabled(self):\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- return vc_config.get_bool("LED_STATE_DEBUG", True)\r
- except Exception:\r
- pass\r
- return True\r
-\r
- def _vc_led_should_standby_idle(self):\r
- """未录音且非 TTS 播中:待机/提示音/模式切换/单对话轮间应灭灯(DES-L3 idle),勿被 FSM.turn=Listening 误导。"""\r
- try:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- return False\r
- if self._vc_tts_playback_active():\r
- return False\r
- if getattr(self, "_ptt_await_server", 0) == 1:\r
- return False\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- if t in (\r
- VCTurnState.Thinking,\r
- VCTurnState.TtsReceiving,\r
- VCTurnState.Playing,\r
- VCTurnState.AwaitPlayedAck,\r
- ):\r
- return False\r
- if getattr(self, "Close_flag", 0) == 1:\r
- return True\r
- if getattr(self, "VC_mode", -1) == 1:\r
- return True\r
- k = getattr(self, "_last_tts_kind", "") or ""\r
- if k in ("prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt"):\r
- return True\r
- except Exception:\r
- pass\r
- return False\r
-\r
- def _vc_publish_led(self, ai_state, reason=""):\r
- try:\r
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
- if ai_state != "listening":\r
- if self._vc_led_state_debug_enabled():\r
- print("[LED] publish blocked ai={} reason={} (ptt_arm_lock)".format(\r
- ai_state, reason))\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_led_state_debug_enabled():\r
- print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(\r
- ai_state, reason, getattr(self, "VC_mode", -1),\r
- getattr(self, "VC_record_state_flag", -1),\r
- getattr(getattr(self, "fsm", None), "turn", "?")))\r
- except Exception:\r
- pass\r
- try:\r
- EventMesh.publish("set_led_state", {"ai_state": ai_state})\r
- except Exception:\r
- pass\r
-\r
- def _vc_led_sync_from_fsm(self):\r
- """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""\r
- try:\r
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
- if getattr(self, "VC_record_state_flag", 0) == 0:\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_led_should_standby_idle():\r
- self._vc_publish_led("idle", "fsm_sync/standby_idle")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:\r
- self._vc_publish_led("thinking", "fsm_sync/ptt_await")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_tts_playback_active():\r
- self._vc_publish_led("playing", "fsm_sync/tts")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- t = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- if t in (VCTurnState.Thinking, VCTurnState.TtsReceiving):\r
- self._vc_publish_led("thinking", "fsm_sync/thinking")\r
- elif t in (VCTurnState.Playing, VCTurnState.AwaitPlayedAck):\r
- # 服务端已标记播放态但本地 PCM 尚未起播:仍显示思考,勿误绿灯\r
- self._vc_publish_led("thinking", "fsm_sync/await_tts")\r
- elif t in (VCTurnState.Listening, VCTurnState.SpeechEnded, VCTurnState.SpeechStreaming):\r
- self._vc_publish_led("listening", "fsm_sync/listening")\r
- else:\r
- self._vc_publish_led("idle", "fsm_sync/idle")\r
- except Exception:\r
- try:\r
- self._vc_publish_led("idle", "fsm_sync/fallback")\r
- except Exception:\r
- pass\r
-\r
- def _uplink_request_start(self, reason="", *args, **kwargs):\r
- # 统一入口:避免多处同时 start 造成冲突(尤其是 TTS/播放阶段误开麦导致回声触发与状态紊乱)\r
- try:\r
- if self.Close_flag == 1:\r
- try:\r
- if reason in ("canceled", "local.play_end", "audio.played", "key.press/continue"):\r
- print("[VC][uplink_ctrl] Deny START by {} cause=Close_flag".format(reason))\r
- except Exception:\r
- pass\r
- return\r
- except Exception:\r
- return\r
-\r
- # 连续对话才允许“自动开新一轮上行”\r
- try:\r
- if self.VC_mode != 0:\r
- try:\r
- if reason in ("canceled", "local.play_end", "audio.played"):\r
- print("[VC][uplink_ctrl] Deny START by {} cause=VC_mode({})".format(reason, self.VC_mode))\r
- except Exception:\r
- pass\r
- return\r
- except Exception:\r
- return\r
-\r
- try:\r
- if self.VC_mode == 0 and not self._vc_long_dialogue_auto_uplink_ok(reason):\r
- return\r
- except Exception:\r
- pass\r
-\r
- try:\r
- if not self._vc_capture_allowed(reason):\r
- try:\r
- print("[VC][uplink_ctrl] Deny START by {} cause=playback_busy".format(reason))\r
- except Exception:\r
- pass\r
- return\r
- except Exception:\r
- return\r
-\r
- # ===== 业务门控(收敛到 FSM turn/session)=====\r
- try:\r
- if getattr(self.fsm, "session", VCSessionState.Disconnected) != VCSessionState.Started:\r
- try:\r
- if reason in ("canceled", "local.play_end", "audio.played"):\r
- print("[VC][uplink_ctrl] Deny START by {} cause=fsm.session".format(reason))\r
- except Exception:\r
- pass\r
- return\r
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- # 仅 Listening/SpeechEnded 允许开麦进入下一轮;其它阶段一律拒绝(避免抢跑打乱链路)\r
- if turn not in (VCTurnState.Listening, VCTurnState.SpeechEnded):\r
- try:\r
- if reason in ("canceled", "local.play_end", "audio.played"):\r
- print("[VC][uplink_ctrl] Deny START by {} cause=fsm.turn({})".format(reason, turn))\r
- except Exception:\r
- pass\r
- return\r
- except Exception:\r
- return\r
-\r
- # 若当前不在录音上行,才允许开新一轮\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- # canceled 打断后:可能是僵死 flag=2(started 未真开麦),先停录复位再开麦\r
- if reason == "canceled":\r
- try:\r
- self._vc_clear_stale_record_state("uplink_start/canceled")\r
- except Exception:\r
- pass\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- try:\r
- if reason in ("canceled", "local.play_end", "audio.played"):\r
- print("[VC][uplink_ctrl] Deny START by {} cause=VC_record_state_flag({})".format(\r
- reason, getattr(self, "VC_record_state_flag", 0)))\r
- except Exception:\r
- pass\r
- return\r
-\r
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
- if getattr(self, "record_data_save_flag", 0) == 0:\r
- self._server_vad_silence = 0\r
-\r
- print("[VC][uplink_ctrl] Request START by {}".format(reason))\r
- try:\r
- if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:\r
- self._vc_pending_interrupt_uplink = 0\r
- print("[VC][uplink_ctrl] interrupt_uplink armed -> start (canceled)")\r
- except Exception:\r
- pass\r
- self.VC_Speech_Start_now()\r
-\r
- def _uplink_request_stop(self, reason="", *args, **kwargs):\r
- # 统一入口:避免多处同时 stop 造成冲突\r
- if getattr(self, "record_data_save_flag", 0) == 0:\r
- self._server_vad_silence = 1\r
-\r
- if getattr(self, "VC_record_state_flag", 0) == 0:\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] _uplink_request_stop skip (record_state==0) reason={}".format(reason))\r
- except Exception:\r
- pass\r
- return\r
-\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] step=uplink_stop_exec reason={} VC_record_state_flag={}".format(\r
- reason, getattr(self, "VC_record_state_flag", -1)))\r
- except Exception:\r
- pass\r
- print("[VC][uplink_ctrl] Request STOP by {}".format(reason))\r
- self.VC_Speech_End()\r
-\r
- def _uplink_mark_start(self, first_len=0):\r
- try:\r
- if self._uplink_active == 1:\r
- return\r
- self._uplink_active = 1\r
- self._uplink_bytes = 0\r
- self._uplink_start_ms = utime.ticks_ms()\r
- print("[VC][uplink] START len={} {} VC_record_state_flag={}".format(\r
- first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag))\r
- except Exception:\r
- pass\r
-\r
- def _uplink_mark_end(self, reason=""):\r
- try:\r
- if self._uplink_active != 1:\r
- return\r
- dur = utime.ticks_diff(utime.ticks_ms(), self._uplink_start_ms) if self._uplink_start_ms else -1\r
- print("[VC][uplink] END reason={} bytes={} dur_ms={} {} VC_record_state_flag={}".format(\r
- reason, self._uplink_bytes, dur, self._vc_fsm_log_snip(), self.VC_record_state_flag))\r
- except Exception:\r
- pass\r
- self._uplink_active = 0\r
- self._uplink_bytes = 0\r
- self._uplink_start_ms = 0\r
-\r
- # 最近一次 TTS 的语义来源(dialog/prompt/mode_switch_long/mode_switch_ptt/...)\r
- self._last_tts_kind = ""\r
- # 下一个“无 dialog_id 的提示音 TTS”在 tts.started 时采用该 kind\r
- self._pending_tts_kind = ""\r
-\r
- # ===== 状态机动作接口(骨架)=====\r
- # 说明:统一把“副作用动作”收敛到这些方法,便于后续把 recv/timer 的 if/flag 逻辑迁移到 VCStateMachine。\r
- def request_send_start(self):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_start_requset})\r
-\r
- def request_send_audio_create(self):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_send_speech})\r
-\r
- def request_send_audio_completed(self):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_end_speech})\r
-\r
- def request_send_audio_played(self):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_audio_played})\r
-\r
- def request_send_synthesis(self, text):\r
- try:\r
- self.AI_TTS_str = text\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_synthesis})\r
-\r
- def request_uplink_start(self, reason=""):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_start, "data": reason})\r
-\r
- def request_uplink_stop(self, reason=""):\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self._uplink_request_stop, "data": reason})\r
-\r
- def _fsm_apply_actions(self, actions):\r
- try:\r
- for a in actions:\r
- if not a:\r
- continue\r
- if a[0] == "uplink_stop":\r
- self.request_uplink_stop(a[1] if len(a) > 1 else "")\r
- elif a[0] == "uplink_start":\r
- self.request_uplink_start(a[1] if len(a) > 1 else "")\r
- elif a[0] == "play_start":\r
- if getattr(self, "VC_mode", -1) == 1:\r
- if self._ptt_should_skip_remote_playback():\r
- if self._vc_led_state_debug_enabled():\r
- print("[VC][serial] skip play_start (ptt_interrupt)")\r
- continue\r
- try:\r
- self.fsm.on_local_play_start()\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_Start})\r
- elif a[0] == "send_audio_played":\r
- self.request_send_audio_played()\r
- except Exception:\r
- pass\r
-\r
- # AI\r
-\r
- def _print_hw_diag(self):\r
- # 仅诊断使用:打印当前分支能看到的麦克/音频相关配置和运行时状态。\r
- # JSWM2 老分支没有 device_profile/_PROFILE,这里宽容处理\r
- try:\r
- profile = _PROFILE if isinstance(_PROFILE, dict) else {}\r
- except Exception:\r
- profile = {}\r
- dev_type_profile = profile.get("dev_type", None)\r
- flags = profile.get("flags", {})\r
- hardware = profile.get("hardware", {})\r
- product_cfg = profile.get("product_cfg", {})\r
- vc_cfg = {}\r
- try:\r
- vc_cfg = product_cfg.get("vc", {})\r
- except Exception:\r
- vc_cfg = {}\r
-\r
- print("[hw-diag] ===== AI_VC hardware diag =====")\r
- print("[hw-diag] profile.dev_type =", dev_type_profile)\r
- print("[hw-diag] profile.flags =", flags)\r
- print("[hw-diag] profile.hardware =", hardware)\r
- print("[hw-diag] product_cfg.vc =", vc_cfg)\r
-\r
- try:\r
- print("[hw-diag] VC_record_samplerate =", self.VC_record_samplerate)\r
- print("[hw-diag] VC_play_samplerate =", self.VC_play_samplerate)\r
- except Exception:\r
- pass\r
- try:\r
- print("[hw-diag] vad_silence_runtime =", self.vad_silence)\r
- except Exception:\r
- pass\r
- try:\r
- print("[hw-diag] record_data_save_flag =", self.record_data_save_flag)\r
- print("[hw-diag] record_max_bytes =", self.record_max_bytes)\r
- except Exception:\r
- pass\r
-\r
- try:\r
- g = self.record.gain_get()\r
- except Exception as e:\r
- g = "gain_get_error: {}".format(e)\r
- print("[hw-diag] record.gain_get =", g)\r
- try:\r
- print("[hw-diag] type(record) =", type(self.record))\r
- print("[hw-diag] type(aud) =", type(self.aud))\r
- except Exception:\r
- pass\r
- print("[hw-diag] ===== end of hw-diag =====")\r
-\r
- def __init__(self):\r
- # import voiceCall\r
- # voiceCall.callStart("10086")\r
- # return\r
- self.http_get_config_flag = 0\r
- if self.http_get_config_flag == 1:\r
- try:\r
- self.http_get_config()#获取/更新配置参数\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.VC_30S_timer = osTimer()\r
- self.ai_key_press_audio = osTimer()\r
- self.VC_30S_timer.start(30000, 1, self.VC_30S_timer_cb)\r
- self.online_kws = 0#是否开启线上关键词\r
- self.get_config_timer_count = 0\r
- self.record_data_save_flag = 0 #是否保存录音文件\r
- self.play_data_save_flag = 0#是否保存tts数据到文件\r
- self.record_fd = None\r
- self.play_fd = None\r
- self.is_VC_ACK = 0 #是否开启ack应答模式\r
- self.is_long_record = 0 #是否开启长录音模式(语音打断模式)\r
- self.ai_OTA = OTA()\r
- self.is_auth = 1 #是否开启dmp鉴权\r
- #self.VC_mode = 1 #0:短按连续问答模式 1:长按录音模式\r
- self.VC_mode = EventMesh.publish("get_storage_config","VC_mode")#\r
- if self.VC_mode == None :\r
- self.VC_mode = 0\r
- self.is_up_error= 0 #是否上传错误id\r
- self.Close_flag =0 #=1时,连续对话播放完音频就退出\r
- # 服务端 VAD 结束标志:收到 vad.silence 后置 1,用于硬闸门禁止继续上行音频\r
- # (避免仍持续上行导致服务端推迟/抑制后续 TTS,从而一直停留在思考态蓝绿闪)\r
- self._server_vad_silence = 0\r
- # 单轮状态标志:用于判定 completed 是否应自动进入下一轮\r
- self._turn_has_asr_completed = 0\r
- self._turn_has_vad_silence = 0\r
- self._turn_has_tts_started = 0\r
- # VC 会话是否已进入 started(服务端 session initialized)\r
- # 重要:在收到 started 前发送音频帧会触发服务端 "session not initialized" → failed → 端侧重连死循环\r
- self._vc_session_started = 0\r
- # 开机/提示音:本轮 started 不自动开麦(避免问候 TTS 前误进 listening)\r
- self._skip_started_uplink_once = 0\r
- self._last_tts_kind = ""\r
- self._pending_tts_kind = ""\r
- # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create\r
- # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed\r
- self._ptt_audio_created = 0\r
- # PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听\r
- self._ptt_await_server = 0\r
- # 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create\r
- self._vc_serial_canceling = 0\r
- self._ptt_pending_arm = 0\r
- self._vc_cancel_deadline_ms = 0\r
- # PTT 打断:轮次号 + 开录前丢弃旧 TTS / 灯只显示聆听\r
- self._ptt_arm_epoch = 0\r
- self._ptt_interrupt_active = 0\r
- self._ptt_led_lock_listening = 0\r
- # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- # 各类兜底的节流时间戳(避免“兜底过激/连续触发”)\r
- self._fb_last_thinking_restart_ms = 0\r
- self._fb_last_tts_pending_timeout_ms = 0\r
- # thinking 兜底:部分情况下服务端只返回 llm.completed 而不触发 tts.started/completed,\r
- # 会导致一直停留在 thinking。此时超时自动回到聆听。\r
- self._llm_completed_ms = 0\r
- # 语音上行统计(真实发送到 VC 发送队列的开始/结束)\r
- self._uplink_active = 0\r
- self._uplink_bytes = 0\r
- self._uplink_start_ms = 0\r
-\r
- # 状态机骨架(后续逐步把旧逻辑迁移进来)\r
- self.fsm = VCStateMachine(self)\r
-\r
- self.play_new_voice_flag =0 #播放文字留言flag\r
-\r
- self.wait_first_flag = 0#是否进入未读判定(只播报一次)\r
- self.wait_first_timer = osTimer()#如果播放未读留言的时候未读则不要播放了\r
- self.wait_first_cnt = 0\r
-\r
- self.wait_sort = 0#未读留言正在播第几条\r
- EventMesh.subscribe("play_tts",self.play_tts)\r
- EventMesh.subscribe("play_new_voice",self.play_new_voice)\r
- EventMesh.subscribe("play_his_voice",self.play_his_voice)\r
- EventMesh.subscribe("play_wait_voice",self.play_wait_voice)\r
- EventMesh.subscribe("play_single_voice",self.play_single_voice)#不带tts的播放(闹钟)\r
- EventMesh.subscribe("play_music",self.play_music)#播放音乐\r
- EventMesh.subscribe("play_trans_tts",self.play_trans_tts)#接收翻译tts\r
- EventMesh.subscribe("set_vad_silence",self.set_vad_silence)\r
- EventMesh.subscribe("set_wait_first_flag",self.set_wait_first_flag)\r
- EventMesh.subscribe("kws_even",self.kws_even)\r
- EventMesh.subscribe("change_volume",self.change_volume) \r
- EventMesh.subscribe("find_cmd",self.find_cmd) #查询命令\r
- EventMesh.subscribe("NLP_set_vad_silence",self.NLP_set_vad_silence) #设置聆听间隔\r
- EventMesh.subscribe("change_voice",self.change_voice) #改变音色\r
- EventMesh.subscribe("Close_VC",self.Close_VC) #外部结束对话\r
- EventMesh.subscribe("get_recv_voice",self.get_recv_voice) #获取留言播放状态\r
- EventMesh.subscribe("set_recv_voice",self.set_recv_voice) #设置留言播放状态\r
- self.voice_timer = osTimer()#语音留言10S按下按键确定是否播放的判断定时器\r
- self.voice_cnt = 0 #留言定时器计数\r
- self.voicerupt_flag = 0#留言选择按键接听标志位\r
- self.wait_voicerupt_flag = 0#未读留言选择按键接听标志位\r
- self.recv_voice = 0 #是否收到留言\r
- self.voice_notice = 0 #收到留言并播放则置为1,接下来要播放提示词\r
- self.voice_choose_flag = 0#当tts和语音留言冲突时,为了防止语音地址被误清,做标志位在close做判断\r
-\r
- self.revoice_timer = osTimer()#语音留言10S按下按键确定是否回复的判断定时器\r
- self.revoice_cnt = 0 #回复留言定时器计数\r
- self.revoicerupt_flag = 0#留言选择按键接听标志位\r
-\r
- self.music_timer = osTimer()#音乐播放定时器,等待tts播完才放音乐\r
- self.music_cnt = 0 #留言定时器计数\r
-\r
- self.his_timer = osTimer()#历史留言播放定时器,等待tts播完再播留言\r
- self.his_cnt = 0#历史留言定时器技术\r
-\r
- self.wait_timer = osTimer()#未读留言播放定时器,等待tts播完再播留言\r
- self.wait_cnt = 0#未读留言定时器技术\r
-\r
- self.error_data = []\r
- self.asr_taskid = ""\r
- self.VC_taskid = ""\r
- self.VC_server = 2 #0柒牌正式服务器,1:测试服务器1,2:测试服务器2\r
- self.voice_flag = 0\r
-\r
- #会议记录标志\r
- self.meet_flag = 0\r
- #AI翻译标志\r
- self.trans_flag = 0\r
-\r
- #历史留言查看判断标志\r
- self.history_voice_flag = 0#公共变量判断 \r
- #60S等待留言判断标志\r
- self.wait_voice_flag = 0#公共变量判断 \r
- EventMesh.publish("AI_alarm_open")#\r
- self.server_ip = EventMesh.publish("get_storage_config","server_ip")#\r
- if self.server_ip == None:\r
- if self.VC_server == 1:# \r
- self.server_ip="118.145.158.207:22083"\r
- elif self.VC_server == 2:# \r
- self.server_ip="118.145.158.207:22082"\r
- elif self.VC_server == 4:# \r
- self.server_ip="118.145.158.207:22084"\r
- elif self.VC_server == 5:# \r
- self.server_ip="amp-wss.raisound.com"\r
- elif self.VC_server == 6:# \r
- self.server_ip="118.145.158.207:22080"\r
- elif self.VC_server == 7:# \r
- self.server_ip="118.145.158.207:22080"\r
- elif self.VC_server == 8:# \r
- self.server_ip="115.190.94.208:22080"\r
- elif self.VC_server == 86:# \r
- self.server_ip="118.145.158.207:22086" \r
- else:\r
- #self.server_ip="sevenpin.qipai.com.cn"\r
- self.server_ip="118.145.158.207:22082"\r
- # 默认走 constants.AMP_SERVER(由 default_srv.py 统一管理,可在 usr 覆盖)\r
- try:\r
- if constants.AMP_SERVER:\r
- self.server_ip = constants.AMP_SERVER\r
- except Exception:\r
- pass\r
-\r
- #开机提示音\r
- self.power_on_tone = EventMesh.publish("get_storage_config","power_on_tone")#\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- if not vc_config.get_bool("POWER_ON_TONE_ENABLED", True):\r
- self.power_on_tone = None\r
- except Exception:\r
- pass\r
- if self.power_on_tone == None:\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- self.power_on_tone = vc_config.get_attr("POWER_ON_TONE_DEFAULT", None)\r
- except Exception:\r
- pass\r
-\r
- self.VC_record_TYPE = VC_record_TYPE_PCM #0:PCM 1:AMR\r
- self.VC_play_TYPE = VC_play_TYPE_PCM #0:PCM 1:mp3 \r
- self.VC_record_samplerate = 16000\r
- self.VC_play_samplerate = 16000\r
- self.VAD_data_len = 640 #10ms数据\r
- self.is_VAD = 0#=1打开vad打断\r
- # ARG_sample_rate, // 采样率\r
- # ARG_channel, // 通道数\r
- # ARG_frame_size, // 每帧字节数\r
- # ARG_energy_threshold, // 能量阈值\r
- # ARG_voice_time, // 语音持续时间阈值(ms)\r
- # ARG_silence_time, // 静音持续时间阈值(ms)\r
- # ARG_silence_start_threshold, // 静音开始阈值(ms)\r
- self.vad = None\r
- if self.is_VAD == 1:\r
- self.vad = VAD.VAD(self.VC_record_samplerate ,1,self.VAD_data_len,600,100,300,100)\r
- self.speech_rate = EventMesh.publish("get_storage_config","speech_rate")#语速配置\r
- if self.speech_rate == None:\r
- self.speech_rate = 0\r
- \r
- self.vad_silence = EventMesh.publish("get_storage_config","vad_silence")#语速配置\r
- if self.vad_silence == None:\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- self.vad_silence = vc_config.get_int("VAD_SILENCE_DEFAULT", 300)\r
- else:\r
- self.vad_silence = 300\r
- except Exception:\r
- self.vad_silence = 300\r
-\r
- self.VC_TTS_voice_type = EventMesh.publish("get_storage_config","VC_TTS_voice_type")#音色配置\r
- if self.VC_TTS_voice_type == None :\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- cfg = vc_config.get_attr("VC_TTS_VOICE_TYPE_DEFAULT", None)\r
- if isinstance(cfg, dict):\r
- self.VC_TTS_voice_type = cfg\r
- except Exception:\r
- pass\r
- # 若配置未提供,沿用原逻辑内置兜底(保持兼容)\r
- if self.VC_TTS_voice_type == None:\r
- self.VC_TTS_voice_type = {\r
- "女":{"bot_name":"柒妹妹","voice":"zh_female_wanwanxiaohe_moon_bigtts","lang":"zh-cn"},\r
- "男":{"bot_name":"柒先生","voice":"zh_male_wennuanahu_moon_bigtts","lang":"zh-cn"},\r
- "童":{"bot_name":"柒乐乐","voice":"ICL_zh_female_huoponvhai_tob","lang":"zh-cn"},\r
- }\r
- \r
- # self.VC_TTS_voice_type = {0:"zh_female_xinlingjitang_moon_bigtts",1:"zh_male_wennuanahu_moon_bigtts",2:"ICL_zh_female_huoponvhai_tob",\r
- # 3:"aliyun::longjiaxin",4:"zh_female_daimengchuanmei_moon_bigtts"}\r
- #self.VC_TTS_voice_type = ["aliyun::longxiaoxia","aliyun::longnan","aliyun::longling"]\r
- \r
- #EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
-\r
- try:\r
- self.VC_TTS_voice_key = EventMesh.publish("get_storage_config","VC_TTS_voice_key")#音色配置\r
- if self.VC_TTS_voice_key == None :\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- self.VC_TTS_voice_key = vc_config.get_attr("VC_TTS_VOICE_KEY_DEFAULT", None)\r
- except Exception:\r
- pass\r
- if not self.VC_TTS_voice_key:\r
- self.VC_TTS_voice_key = "男"\r
- if self.VC_TTS_voice_key in self.VC_TTS_voice_type:\r
- pass\r
- else:\r
- self.VC_TTS_voice_key = list(self.VC_TTS_voice_type.keys())[0]\r
- print("音色配置:{}:{}\r\n\r\n".format(self.VC_TTS_voice_key,self.VC_TTS_voice_type[self.VC_TTS_voice_key]))\r
- self.VC_TTS_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置\r
- except Exception as e:\r
- usys.print_exception(e)\r
- #配置词\r
- self.commands_mode = 2 #=1用大模型判断命令,=0用服务器本地插件判断命令 =2用设备本地判断方式\r
- self.commands = []\r
- if self.VC_TTS_volume == None:\r
- self.VC_TTS_volume = 100\r
- #可ota配置的参数变量\r
- self.emotion = EventMesh.publish("get_storage_config","emotion")#情绪\r
- if self.emotion == None:\r
- self.emotion = ""\r
- self.ota_update_notice = EventMesh.publish("get_storage_config","ota_update_notice")#OTA升级提示语\r
- if self.ota_update_notice == None:\r
- self.ota_update_notice = "" \r
- self.AI_audio_queue_thread_id = 0\r
- self.AI_audio_queue_thread_lock = 0\r
- self.AI_VC_send_data_queue_thread_id = 0\r
- self.AI_VC_send_data_queue_thread_lock = 0\r
- self.AI_ASR_send_data_queue_thread_id = 0\r
- self.AI_ASR_send_data_queue_thread_lock = 0\r
- self.AI_record_tmp_queue = Queue(100)\r
- self.AI_ASR_send_data_queue = Queue(200)\r
- self.AI_VC_send_data_queue = Queue(100)\r
- self.AI_audio_queue = Queue(100)\r
- #self.AI_WS_connect_Queue = Queue(100) #释放用\r
- self.AI_TTS_str = ""\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_flag = 0\r
- self.AI_ASR_str_tmp = ""\r
- self.AI_ASR_send_buf_len = 0\r
- self.AI_ASR_text_tmp = ""\r
- self.AI_ASR_text_repet_count = 0\r
- # #开始线程\r
- # if self.AI_audio_queue_thread_id ==0:\r
- # self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
- # else:\r
- # print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
- # if self.AI_VC_send_data_queue_thread_id == 0: \r
- # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
- \r
- self.VC_dialog_id = ""#\r
- self.VC_message_id=None\r
- self.VC_task_id =None\r
- self.AI_client_VC = None\r
- self.AI_client_VC_recv_id = 0\r
- self.AI_client_VC_recv_lock = 0\r
- self.AI_client_VC_isconnect =0\r
- self.AI_client_VC_debug =0\r
-\r
- self.asr_state = 0 # 预留:ASR 侧状态(当前未参与分支)\r
- self.key_state = 0\r
-\r
- self.AI_client_asr = None\r
- self.AI_client_asr_recv_id = 0\r
- self.AI_client_asr_recv_lock = 0\r
- self.AI_client_asr_isconnect =0\r
- self.AI_ASR_timer = osTimer()\r
- self.AI_ASR_timer_count = 0\r
- self.AI_client_asr_debug =0\r
- self.AI_ASR_check_timeout = 0\r
- self.AI_ASR_error_timeout = 0\r
- self.VC_Thinking_timerout = 0\r
-\r
-\r
- self.record_pcm = None\r
- self.play_pcm = None\r
- self.VC_record_thread_id = 0\r
- self.VC_record_thread_lock = 0\r
- self.VC_pingping_timerout = 0\r
- self.asr_pingping_timerout = 0\r
- self.factory_timer = osTimer()\r
- self._1S_timer = osTimer()\r
- self.Audio_buf_size = 20*1024\r
- self.record = audio.Record()\r
- #self.record.end_callback(self.record_callback)\r
- self.record.gain_set(4,10)\r
- #self.record.gain_set(4,12)\r
- self.aud = audio.Audio(0)\r
- self.aud.setCallback(self.audio_cb)\r
- # 确保 record/aud 已创建后再打印硬件/音频诊断信息(需要 gain_get/type(record))\r
- try:\r
- self._print_hw_diag()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- #self.aud.setVolume(self.VC_TTS_volume //10)\r
- self.audio_play_state = 1 #音频播放完状态\r
- self.close_timerout = 0\r
- self._idle_deep_sec = 0\r
- self._vc_idle_deep_armed_suppress = 0\r
- self.LocalRespondingEnded_timerout = 0\r
-\r
- self.VC_Play_flag = 0\r
- self.play_flag = 0\r
- self.VC_record_state_flag = 0\r
- self.AI_key_long_press_flag = 0\r
- self._asr_completed_tone_done = 0 # asr.completed 本地 think 提示每轮仅一次\r
- self._last_asr_result_text = "" # 保留最近 asr.result,防 VC_Speech_End 早于 asr.completed 清空\r
- self._vc_last_auto_uplink_ms = 0 # 长对话自动开麦防抖\r
- self._vc_local_play_active = 0 # aud.play 本地短音占用(与采集互斥)\r
- self._vc_local_play_until_ms = 0\r
- self._vc_pending_interrupt_uplink = 0 # 长对话短按打断:等 canceled 再开麦\r
- self.websocket_error_count = 0\r
- self.AI_new_voice_url = "" #未读语音地址/音乐播放地址/闹钟播放标志\r
- self.music_play_flag = 0 #音乐播放标志\r
- self.alarm_play_flag = 0 #闹钟播放标志\r
- self.tts_in_data_len = 0\r
- self.tts_out_data_len = 0\r
- self._tts_first_write_ms = 0\r
- self._tts_play_check_log_ms = 0\r
- self._need_audio_played_ack = 0#tts.completed 后需在本地播报结束后上报 audio.played;勿用 tts_in/out:tts.completed 会再调 VC_Play_Start 把二者清零\r
- self.VC_satrt_flag = 0\r
- self.VC_stop_play_audio_flag = 0\r
- self.VC_satrt_kws_flag = 0\r
- self.session_id = ""\r
- self.stop_voice_timer=osTimer()\r
- self.pwk_cnt_voice = 0 #不听语音后,积攒的次数,需要一起播放\r
- self.pwk_notice_flag = 0#是否处于积攒留言状态(用来实现单次提醒)\r
- self.is_noread_voice = EventMesh.publish("get_storage_config","is_noread_voice")\r
- if self.is_noread_voice == None:\r
- self.is_noread_voice = 0#是否还有未读留言(开机判断)\r
- self.is_ruppt_tts = 0#是否已经打断tts播放\r
- EventMesh.subscribe("AI_key_press", self.AI_key_press)\r
- EventMesh.subscribe("BT_AI_key_press", self.BT_AI_key_press)\r
- EventMesh.subscribe("stop_AI_record", self.stop_AI_record)\r
- EventMesh.subscribe("AI_key_release", self.AI_key_release)\r
- EventMesh.subscribe("pwk_key_double_press", self.pwk_key_double_press)\r
- EventMesh.subscribe("AI_key_long_press", self.AI_key_long_press)\r
- EventMesh.subscribe("pwk_key_release", self.pwk_key_release)\r
- EventMesh.subscribe("get_AI_audio_queue", self.get_AI_audio_queue)\r
- EventMesh.subscribe("get_VC_satrt_flag", self.get_VC_satrt_flag)\r
- EventMesh.subscribe("get_vc_stream_active", self.get_vc_stream_active)\r
- EventMesh.subscribe("get_vc_tts_playback_active", self.get_vc_tts_playback_active)\r
- EventMesh.subscribe("battery_priority_mute_vc", self.battery_priority_mute_vc)\r
- EventMesh.subscribe("VC_error", self.VC_error)\r
- EventMesh.subscribe("audio_test", self.audio_test)\r
- EventMesh.subscribe("audio_test_close", self.audio_test_close)\r
- EventMesh.subscribe("set_record_data_save_flag", self.set_record_data_save_flag)\r
- EventMesh.subscribe("set_VC_TTS_voice_type", self.set_VC_TTS_voice_type)\r
- EventMesh.subscribe("SET_TTS_VOLUME", self.set_tts_volume)\r
- EventMesh.subscribe("set_VC_stop_play_audio_flag", self.set_VC_stop_play_audio_flag)\r
- EventMesh.subscribe("set_yuyinaudio_flag",self.set_yuyinaudio_flag)\r
- EventMesh.subscribe("get_yuyinaudio_flag",self.get_yuyinaudio_flag)\r
- EventMesh.subscribe("set_pwk_cnt_voice",self.set_pwk_cnt_voice)\r
- EventMesh.subscribe("get_pwk_cnt_voice",self.get_pwk_cnt_voice)\r
- EventMesh.subscribe("exit_voice_message_mode",self.exit_voice_message_mode) #超时未退出语音留言模式\r
- EventMesh.subscribe("come_meet_mode",self.come_meet_mode)#进入会议模式\r
- EventMesh.subscribe("exit_meet_mode",self.exit_meet_mode)#退出会议模式\r
- EventMesh.subscribe("come_trans_mode",self.come_trans_mode)#进入翻译模式\r
- EventMesh.subscribe("exit_trans_mode",self.exit_trans_mode)#退出翻译模式\r
- EventMesh.subscribe("set_trans_flag",self.set_trans_flag)#设置翻译状态\r
- EventMesh.subscribe("get_audio_play_state",self.get_audio_play_state)\r
- EventMesh.subscribe("set_voicerupt_flag",self.set_voicerupt_flag)#按键中断触发留言播放\r
- EventMesh.subscribe("set_wait_voicerupt_flag",self.set_wait_voicerupt_flag)#按键中断触发未读留言播放 \r
- EventMesh.subscribe("get_recv_voice",self.get_recv_voice)#获取是否收到留言\r
- EventMesh.subscribe("rate_change",self.rate_change)#直接修改语速\r
- EventMesh.subscribe("get_history_voice_flag",self.get_history_voice_flag)#获取查找历史留言状态\r
- EventMesh.subscribe("set_history_voice_flag",self.set_history_voice_flag)#修改查找历史留言状态\r
- EventMesh.subscribe("clean_unread_vocie",self.clean_unread_vocie)#修改查找历史留言状态\r
- EventMesh.subscribe("get_wait_voice_flag",self.get_wait_voice_flag)#获取60S等待留言播放定时器状态\r
- EventMesh.subscribe("set_wait_voice_flag",self.set_wait_voice_flag)#修改60S等待留言播放定时器状态\r
- EventMesh.subscribe("rate_move",self.rate_move)#NLP修改语速\r
- EventMesh.subscribe("find_rate",self.find_rate)#NLP查询语速\r
- EventMesh.subscribe("set_pwk_notice_flag",self.set_pwk_notice_flag)#设置未读状态\r
- # EventMesh.subscribe("get_voicerupt_flag",self.get_voicerupt_flag)#获取留言播放状态\r
- EventMesh.subscribe("chenggong_tone",self.chenggong_tone)\r
- \r
- pass\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- self.dialog_attributes_provider = EventMesh.publish("get_storage_config","dialog_attributes_provider")#智能体类型配置\r
- if self.dialog_attributes_provider == None:\r
- self.dialog_attributes_provider = "coze"\r
- self.outbound_attributess_provider = EventMesh.publish("get_storage_config","outbound_attributess_provider")#tts服务选择\r
- if self.outbound_attributess_provider == None:\r
- self.outbound_attributess_provider = "volcengine"\r
- self.inbound_attributess_provider = EventMesh.publish("get_storage_config","inbound_attributess_provider")#tts服务选择\r
- if self.inbound_attributess_provider == None:\r
- self.inbound_attributess_provider = "volcengine"\r
- \r
- self.bot_name = "柒先生"\r
- try:\r
- if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
- self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]\r
- else:\r
- print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try:\r
- bt_ota_flag=0\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("BT_OTA_ENABLED", False):\r
- bt_ota_flag = self.ai_OTA.bt_ota()#先检查蓝牙是否要升级\r
- except Exception:\r
- pass\r
-\r
- cat1_ota_flag = self.ai_OTA.min_sys_ota()\r
- if bt_ota_flag==1 or cat1_ota_flag == 1:\r
- #if 0: \r
- if self.ota_update_notice != "":\r
- self.AI_TTS_str = self.ota_update_notice\r
- else:\r
- self.AI_TTS_str = "检测到有新版本需要升级啦!升级时可千万别断电或乱按哦,升级完成后设备会自动重启,重启就代表升级成功"\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- self.Close_flag = 1\r
- # if self.ai_OTA.min_sys_ota()==2:\r
- # self.AI_TTS_str = "检测到有新版本需要升级啦!电量低于40%,无法保证升级正常执行,请充满电后重启设备再次升级!"\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- else:\r
- try:\r
- self.ai_OTA.ota_result()\r
- self.ai_OTA.pots_ICCID()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- #开机提示语,如果联网失败,直接提示信号弱\r
- \r
- \r
- #EventMesh.publish("get_storage_config",["powerRestart",1])\r
- powerRestart = EventMesh.publish("get_storage_config","powerRestart")#=1非正常重启\r
- if powerRestart == None:\r
- powerRestart = 0\r
- if powerRestart == 1:\r
- EventMesh.publish("set_storage_config",["powerRestart",0])\r
- \r
- else:\r
- pass\r
- self.AI_TTS_str = self.power_on_tone.replace("bot_name", self.bot_name)\r
- print("开机提示:{}".format(self.AI_TTS_str))\r
- try:\r
- self._pending_tts_kind = "boot_prompt"\r
- self._last_tts_kind = "boot_prompt"\r
- self._skip_started_uplink_once = 1\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
-\r
- except Exception as e:\r
- usys.print_exception(e)\r
- def set_VC_stop_play_audio_flag(self,event = None, msg = None):\r
- self.VC_stop_play_audio_flag = msg\r
- print("self.VC_stop_play_audio_flag:{}".format(self.VC_stop_play_audio_flag))\r
-\r
- def rate_change(self,event,msg):#直接设置语速(数字)\r
- self.speech_rate = msg\r
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
- print("set_speech_rate:{}".format(self.speech_rate))\r
- def rate_move(self,event,msg):#设置语速(模糊(快/慢一点))\r
- if msg == 1 and self.speech_rate<50:#增大\r
- self.speech_rate += 10\r
- else:#减小\r
- if self.speech_rate >-50:\r
- self.speech_rate -= 10\r
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
- print("set_speech_rate:{}".format(self.speech_rate))\r
- def find_rate(self,event,msg):#查询APP语速\r
- print("查询语速处理:原始数据为:{}".format(self.speech_rate))\r
- y = 1\r
- if self.speech_rate >= -50 and self.speech_rate < 0:#负数\r
- y = 1 - (-0.01 * self.speech_rate) \r
- elif self.speech_rate <= 50 and self.speech_rate >= 0:#正数\r
- y = 0.02 * self.speech_rate + 1.0\r
- print("处理后数据为{}".format(y))\r
- rate = str(y) + "倍"\r
- return rate\r
- def set_pwk_notice_flag(self,event,msg):#设置未读状态\r
- self.pwk_notice_flag = msg\r
- #获取60S等待留言播放定时器状态\r
- def get_wait_voice_flag(self,event,msg):\r
- return self.wait_voice_flag\r
- def set_wait_voice_flag(self,event,msg):\r
- # print("未读留言标记修改为:{}".format(msg))\r
- self.wait_voice_flag = msg\r
-\r
- #查看历史留言状态修改\r
- def get_history_voice_flag(self,event,msg):\r
- return self.history_voice_flag\r
- def set_history_voice_flag(self,event,msg):\r
- # print("历史留言标记修改为:{}".format(msg))\r
- self.history_voice_flag = msg\r
- def clean_unread_vocie(self,event,msg):\r
- self.pwk_cnt_voice = 0\r
- self.pwk_notice_flag = 0\r
- EventMesh.publish("set_app_new_voice_flag",0)\r
- def audio_test(self,event = None, msg = None):\r
- self._uplink_request_start("audio_test")\r
- self.VC_Play_Start() \r
- def audio_test_close(self,event = None, msg = None):\r
- self._uplink_request_stop("audio_test_close")\r
- self.VC_Play_End() \r
- if self.record_data_save_flag == 1:\r
- if self.record_fd :\r
- self.record_fd.close()\r
- self.record_fd = None\r
- def set_record_data_save_flag(self,event = None, msg = None):\r
- self.record_data_save_flag = msg\r
- def post_processor_after_instantiation(self):\r
- pass\r
- def get_AI_audio_queue(self, event, msg):\r
- return self.AI_audio_queue\r
- def get_VC_satrt_flag(self, event, msg):\r
- return self.VC_satrt_flag\r
- #检查有没有意外打开ai部分功能\r
- def check_vc_start(self):\r
- print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))\r
- if self.VC_satrt_flag == 0:\r
- if self.VC_record_state_flag != 0 or self.AI_client_VC_isconnect == 1 or self.VC_Play_flag == 1:\r
- self.Close()\r
- def VC_30S_timer_cb(self, *args): #\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_30S_timer_cb_}) \r
-\r
- def VC_30S_timer_cb_(self): \r
- self.get_config_timer_count += 1\r
- if self.get_config_timer_count >= 2*60*2:#2小时更新配置参数\r
- self.get_config_timer_count = 0\r
- if self.http_get_config_flag == 1:\r
- try:\r
- self.http_get_config()#获取/更新配置参数\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.check_vc_start()#30秒检查一下有没有关闭\r
- #提交错误id \r
- def ud_error(self,error_data):\r
- response = None\r
- if self.is_up_error == 1:\r
- try:\r
- # currentVersion = versions.Build_Time\r
- # currentVersion = "7PIN-"+currentVersion[:10]+"-"+currentVersion[11:-3]\r
- url = "http://{}/health?task_id={}".format(self.server_ip,error_data[0]+error_data[1])\r
- print(url)\r
- response = request.get(url,timeout=5)\r
- if response.status_code == 200:\r
-\r
- print("ud_error:ok")\r
- else:\r
- print("ud_error error!!!!")\r
- for i in response.text:\r
- print(i)\r
- response.close()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- if response != None:\r
- response.close()\r
- #获取配置参数\r
- def http_get_config(self, event=None, msg=None):\r
- print("http_get_config")\r
-\r
- response = None\r
- #http://180.184.206.142/bz-api/dev/config?deviceId=xxx\r
- try:\r
- url = (constants.DMP_BASE + "/dev/config/{}").format(modem.getDevImei())\r
- \r
- \r
- print(url)\r
- response = request.get(url,timeout=5)\r
- if response.status_code == 200:\r
- data = response.json()\r
- print("http_get_config:{}".format(data))\r
- \r
- if data:\r
- try:\r
- if data["code"] == 20000:#有配置\r
- if data["data"] :\r
- if "version" in data["data"]:\r
- config_version = EventMesh.publish("get_storage_config","config_version")#音量配置\r
- if config_version != data["data"]["version"]:\r
- config_version = data["data"]["version"]\r
- EventMesh.publish("set_storage_config",["config_version",config_version])\r
- if "VC_TTS_voice_type" in data["data"]:\r
- self.VC_TTS_voice_type = data["data"]["VC_TTS_voice_type"]\r
- EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
- print("配置VC_TTS_voice_type成功")\r
- if "server_ip" in data["data"]:\r
- self.server_ip = data["data"]["server_ip"]\r
- EventMesh.publish("set_storage_config",["server_ip",self.server_ip])\r
- print("配置server_ip成功")\r
- if "power_on_tone" in data["data"]:\r
- self.power_on_tone = data["data"]["power_on_tone"]\r
- EventMesh.publish("set_storage_config",["power_on_tone",self.power_on_tone])\r
- print("配置power_on_tone成功")\r
- if "dialog_attributes_provider" in data["data"]:\r
- self.dialog_attributes_provider = data["data"]["dialog_attributes_provider"]\r
- EventMesh.publish("set_storage_config",["dialog_attributes_provider",self.dialog_attributes_provider])\r
- if "outbound_attributess_provider" in data["data"]:\r
- self.outbound_attributess_provider = data["data"]["outbound_attributess_provider"]\r
- EventMesh.publish("set_storage_config",["outbound_attributess_provider",self.outbound_attributess_provider])\r
- print("配置outbound_attributess_provider成功")\r
- if "inbound_attributess_provider" in data["data"]:\r
- self.inbound_attributess_provider = data["data"]["inbound_attributess_provider"]\r
- EventMesh.publish("set_storage_config",["inbound_attributess_provider",self.inbound_attributess_provider])\r
- print("配置inbound_attributess_provider成功")\r
- if "speech_rate" in data["data"]:\r
- self.speech_rate = EventMesh.publish("SET_SPEECH_RATE",float(data["data"]["speech_rate"]))\r
- EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])\r
- print("配置speech_rate成功")\r
- if "emotion" in data["data"]:\r
- self.emotion = data["data"]["emotion"]\r
- EventMesh.publish("set_storage_config",["emotion",self.emotion])\r
- print("配置emotion成功")\r
- if "ota_update_notice" in data["data"]:\r
- self.ota_update_notice =data["data"]["ota_update_notice"]\r
- EventMesh.publish("set_storage_config",["ota_update_notice",self.ota_update_notice])\r
- print("配置ota_update_notice成功")\r
- if "VC_TTS_volume" in data["data"]:\r
- self.VC_TTS_volume =int(data["data"]["VC_TTS_volume"])\r
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
- print("配置VC_TTS_volume成功")\r
- \r
- except Exception as e:\r
- usys.print_exception(e)\r
- else:\r
- print("status_code:{}".format(response.status_code))\r
- print("http_get_config:error!!!!!!!!!!")\r
- response.close()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- if response != None:\r
- response.close()\r
-\r
- def audio_cb(self,event):\r
- if event == 0:\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- self.audio_play_state =1\r
- #EventMesh.publish("set_audio_state", 1)\r
- print('audio-play start.')\r
- # 本地 aud.play(开录/思考短音等)不改动 AI 灯,仅 TTS 流由 VC_Play_Start 控绿灯\r
- if self._vc_led_state_debug_enabled():\r
- print("[LED] audio_cb start skip (local_play)")\r
- return\r
- elif event == 7:\r
- self.audio_play_state = 0\r
- #EventMesh.publish("set_audio_state", 0) \r
- print('audio-play finish.!!!!!!')\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:\r
- self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_record_state_flag", 0) != 0:\r
- self._vc_publish_led("listening", "audio_cb/finish/recording")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if self._vc_tts_playback_active():\r
- if self._vc_led_state_debug_enabled():\r
- print("[LED] audio_cb finish skip (tts_playback)")\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "_ptt_led_lock_listening", 0) == 1:\r
- return\r
- except Exception:\r
- pass\r
- self._vc_led_sync_from_fsm()\r
- def set_tts_volume(self,event,msg):\r
- EventMesh.publish("set_storage_config",["VC_TTS_volume",msg])#音量配置\r
- self.VC_TTS_volume = msg\r
- print("set_tts_volume:{} {}".format(self.VC_TTS_volume,msg))\r
- if int(msg) <= 10: #最小一定要设置1要不然没有声音\r
- volume = 10 //10\r
- self.aud.setVolume(volume)\r
- else:\r
- self.aud.setVolume(self.VC_TTS_volume //10)\r
- pass\r
- def get_yuyinaudio_flag(self, event=None, msg = None):\r
- return self.voice_flag\r
-\r
- def set_yuyinaudio_flag(self, event=None, msg = None):\r
- self.voice_flag=0\r
-\r
- #留言主动拒听\r
- def get_pwk_cnt_voice(self, event=None, msg = None):\r
- return self.pwk_cnt_voice\r
-\r
- def set_pwk_cnt_voice(self, event=None, msg = None):\r
- self.pwk_cnt_voice=msg\r
- print("设置未读次数为:{}".format(self.pwk_cnt_voice))\r
-\r
- #会议\r
- def get_meetaudio_flag(self, event=None, msg = None):\r
- return self.meet_flag\r
- def set_meetaudio_flag(self, event=None, msg = None):\r
- self.meet_flag = 0\r
-\r
- def get_audio_play_state(self, event=None, msg = None):\r
- return self.audio_play_state\r
- \r
- \r
- \r
- def come_meet_mode(self, event=None, msg = None):\r
- if self.meet_flag == 0: #还是为0就提示进入会议纪要模式\r
- EventMesh.publish("send_meet_open_flag_to_iot")#创建会议\r
- self.meet_flag=1\r
- exit_tts ="开启会议纪要,正在录音"\r
- EventMesh.publish("play_tts",[exit_tts,1])\r
- utime.sleep_ms(4000)\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press}) \r
- pass\r
- def exit_meet_mode(self, event=None, msg = None):\r
- if self.meet_flag == 1: #还是为1就提示已退出会议纪要模式\r
- EventMesh.publish("send_meet_end_flag_to_iot")#结束会议\r
- self.meet_flag=0\r
- EventMesh.publish("stop_voice_record") #关闭语音录音\r
- exit_tts ="录音结束,已退出会议纪要模式"\r
- EventMesh.publish("play_tts",[exit_tts,1])\r
- pass\r
- def come_trans_mode(self, event=None, msg = None):\r
- if self.trans_flag == 0: #还是为0就提示进入AI翻译模式\r
- EventMesh.publish("send_trans_come_flag_to_iot")#进入翻译\r
- pass\r
- def exit_trans_mode(self, event=None, msg = None):\r
- if self.trans_flag == 1: #还是为1就提示已退出AI翻译模式 \r
- EventMesh.publish("stop_voice_record") #关闭语音录音\r
- exit_tts ="已退出AI翻译模式"\r
- EventMesh.publish("play_tts",[exit_tts,1])\r
- self.trans_flag=0\r
- EventMesh.publish("send_trans_end_flag_to_iot")#退出翻译\r
- pass\r
- def set_trans_flag(self,event,msg):\r
- self.trans_flag = msg\r
- def exit_voice_message_mode(self, event=None, msg = None):\r
- if self.voice_flag == 1: #还是为1就提示已退出会议纪要模式\r
- self.voice_flag=0\r
- exit_tts ="已退出语音留言模式"\r
- EventMesh.publish("play_tts",[exit_tts,1])\r
- pass\r
- def ai_key_press_audio_cb1(self, event=None, msg = None):\r
- print("播放按键音!!!!!!!!!!")\r
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
- # self.chenggong_tone()\r
- self.aud.aud_tone_play(10,400)#播放按键音\r
- def ai_key_press_audio_cb2(self, event=None, msg = None):\r
- EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
- # self.chenggong_tone() #播放按键音\r
-\r
- def AI_key_press(self, event=None, msg = None):#按键按下\r
- # 按键时打印 LED 聚合状态/最终模式(用于定位“绿灯不灭/闪一下”)\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
- try:\r
- led = EventMesh.publish("get_led_debug_state")\r
- except Exception:\r
- led = None\r
- try:\r
- t = getattr(self.fsm, "turn", None)\r
- except Exception:\r
- t = None\r
- print("[KEY] AI_key_press VC_mode={} {} turn={} led={}".format(\r
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
- except Exception:\r
- pass\r
- # 单对话 PTT / 长对话分支(按键短按提示音已关,避免与开录等提示音叠在一起)\r
- try:\r
- if self._ai_key_press_ptt_mode():\r
- return\r
- except Exception:\r
- pass\r
- if self._ai_key_press_long_dialogue_interrupt():\r
- return\r
- try:\r
- if self._ai_key_press_long_dialogue_when_session_ready():\r
- return\r
- except Exception:\r
- pass\r
- try:\r
- if self._ai_key_press_long_dialogue_wake():\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)\r
- return\r
- except Exception:\r
- pass\r
-\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- if EventMesh.publish("get_sos_flag")== 1:\r
- return\r
- if self.voice_flag == 1: #语音留言模式\r
- self.stop_voice_timer.stop() #关闭语音留言模式定时器\r
- # EventMesh.publish("stop_AI_record")\r
- self.Close()\r
- self.revoice_cnt = 0\r
- self.revoice_timer.stop()#回复定时器关闭,计数器清零\r
- EventMesh.publish("start_voice_record",2)#开始语音录音\r
- return\r
- elif self.meet_flag == 1:#会议纪要模式\r
- self.Close()\r
- EventMesh.publish("start_voice_record",3)#开始会议录音\r
- return\r
- elif self.trans_flag == 1:#AI翻译模式\r
- return\r
- \r
- EventMesh.publish("stop_voice_record") #关闭语音录音\r
- print("AI_key_press: unhandled mode branch")\r
- self.AI_key_long_press_flag = 0\r
- def BT_AI_key_press(self, event=None, msg = None):#按键按下\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- if EventMesh.publish("get_sos_flag")== 1:\r
- return\r
- if self.voice_flag == 1: #语音留言模式\r
- tts_str = "语音留言模式,长按按钮开始录音,松开按钮结束" \r
- EventMesh.publish("play_tts",[tts_str,1])\r
- return\r
- \r
- EventMesh.publish("stop_voice_record") #开启静默需要关闭\r
- print("AI_key_press")\r
- self.get_config_timer_count = 0\r
- self.key_state = 1\r
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
- # self.chenggong_tone() #播放按键音\r
- # 长对话:蓝牙按键同单按\r
- # - 对话进行中:打断/续聊(复用 dialog_id)\r
- # - 已停止/未 started:走新会话流程\r
- try:\r
- if self.VC_mode == 0:\r
- if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})\r
- return\r
- except Exception:\r
- pass\r
-\r
- self.Close()\r
- \r
- if self.VC_mode == 1: #长按对话模式 接收到蓝牙耳机按键事件\r
- \r
- tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
- EventMesh.publish("play_tts",[tts_str,1])\r
- return\r
- # if dev_type == "7PIN":\r
- # self.chenggong_tone()\r
- #self.aud.aud_tone_play(10,200)#播放按键音\r
- self.AI_TTS_str = ""#"你好,有什么吩咐"\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- # if self.VC_state != VC_State_None:\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})\r
- self._uplink_request_stop("BT_AI_key_press/compat2")\r
- self._uplink_request_start("BT_AI_key_press/compat2")\r
- \r
- self.AI_VC_start() \r
- \r
- \r
- # self.Close()\r
- # self.AI_VC_start()\r
- self.AI_key_long_press_flag = 0\r
- def stop_AI_record(self, event=None, msg=None):#停止AI对话\r
- if self.VC_satrt_flag == 1:\r
- self.get_config_timer_count = 0\r
- self.AI_TTS_str = ""#"你好,有什么吩咐"\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self.AI_key_long_press_flag = 0\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})\r
- self.request_uplink_stop("stop_AI_record")\r
- count = 0\r
- while True:\r
- if count >= 20 or self.VC_satrt_flag == 0:\r
- break\r
- count+=1\r
- utime.sleep_ms(500)\r
-\r
- def exit_voice_message_timer(self,*argc):\r
- EventMesh.publish("exit_voice_message_mode")\r
-\r
- def AI_key_release(self, event=None, msg=None):#按键弹起\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
- try:\r
- led = EventMesh.publish("get_led_debug_state")\r
- except Exception:\r
- led = None\r
- try:\r
- t = getattr(self.fsm, "turn", None)\r
- except Exception:\r
- t = None\r
- print("[KEY] AI_key_release VC_mode={} {} turn={} led={}".format(\r
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
- except Exception:\r
- pass\r
- print("AI_key_release123")\r
- self.key_state = 0\r
- if self.trans_flag == 1:#AI翻译模式\r
- EventMesh.publish("stop_voice_record") #关闭语音录音\r
- return\r
- if self.voice_flag == 1:\r
- self.stop_voice_timer.stop()\r
- self.voice_flag=0\r
- if EventMesh.publish("GET_USB_STATE") == 0:\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- EventMesh.publish("stop_voice_record")#停止静默录音\r
- tts_play_str = "留言已发送"\r
- self.voice_flag=0\r
- EventMesh.publish("play_tts",[tts_play_str,1])\r
- return\r
- if self.VC_mode == 1:\r
- self._ai_key_release_handle_ptt()\r
- return\r
- def pwk_key_double_press(self,event,msg):#pwk双击\r
- print("come 双击")\r
- # 切换模式:必须打断当前对话/录音/播放,回到 idle,等待按键触发新一轮\r
- try:\r
- self.Close_flag = 1\r
- except Exception:\r
- pass\r
- try:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
- except Exception:\r
- pass\r
- if self.VC_mode == 0:\r
- self.VC_mode = 1\r
- tts_str = "已切换到单对话模式" \r
- # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(单对话)\r
- try:\r
- self._pending_tts_kind = "mode_switch_ptt"\r
- self._last_tts_kind = "mode_switch_ptt"\r
- except Exception:\r
- pass\r
- else:\r
- self.VC_mode = 0 \r
- tts_str = "已切换到长对话模式" \r
- # 下一次“无 dialog_id 的提示音 TTS”归类为模式切换提示(长对话)\r
- try:\r
- self._pending_tts_kind = "mode_switch_long"\r
- self._last_tts_kind = "mode_switch_long"\r
- except Exception:\r
- pass\r
- # 提示音播完即退出(Close_flag=1),保持 idle,等待按键\r
- EventMesh.publish("play_tts",[tts_str,1])\r
- EventMesh.publish("set_storage_config",["VC_mode",self.VC_mode])\r
-\r
- def AI_key_long_press(self, event, msg):#按键长按\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):\r
- try:\r
- led = EventMesh.publish("get_led_debug_state")\r
- except Exception:\r
- led = None\r
- try:\r
- t = getattr(self.fsm, "turn", None)\r
- except Exception:\r
- t = None\r
- print("[KEY] AI_key_long_press VC_mode={} {} turn={} led={}".format(\r
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))\r
- except Exception:\r
- pass\r
- print("AI_key_long_press")\r
- self._ai_key_long_press_route(event, msg)\r
-\r
- def pwk_key_release(self, event, msg):#电源键短按,关闭\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- _m = bool(vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False))\r
- if not _m:\r
- _ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")\r
- _m = _ai is None or _ai == ""\r
- else:\r
- _m = False\r
- except Exception:\r
- _m = False\r
- if _m:\r
- self._handle_pwk_short_release(event, msg, merged_ai_tail=True)\r
- return\r
- self._handle_pwk_short_release(event, msg, merged_ai_tail=False)\r
-\r
-\r
- def key_prompt(self):\r
- EventMesh.publish("audio_montage_play",["key_prompt.mp3"])\r
- #self.aud.play(1,1,'U:/static/key_prompt.mp3')#提示请您按住中间的按钮开始讲话,讲完松开即可\r
- def VC_error(self, event, msg):#错误播报\r
- self.websocket_error()\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error}) \r
- def set_VC_record_state_flag(self,data):\r
- \r
- self.VC_record_state_flag = data\r
- print("[VC] record_state_flag={}".format(self.VC_record_state_flag))\r
- #开始录音,不缓存,直接发\r
- def VC_Speech_Start_now(self):\r
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
- self._server_vad_silence = 0\r
- self.VC_Speech_Start()\r
- if self.VC_record_state_flag == 1:\r
- self.VC_record_state_flag =2\r
- print("[VC] Speech_Start_now record_state={}".format(self.VC_record_state_flag))\r
- #开始录音,先缓存到buf,等服务器响应再发\r
- def VC_Speech_Start(self):\r
- try:\r
- \r
- #self.VC_state = VC_State_SpeechStart\r
- print("[VC] Speech_Start record_state={} AI_ASR_flag={}".format(self.VC_record_state_flag,self.AI_ASR_flag))\r
- if self.AI_ASR_flag != 0:\r
- return\r
- \r
- if self.VC_record_state_flag == 0:\r
- # 新一轮开始录音:允许上行(上一轮可能已置 silence 闸门)\r
- self._server_vad_silence = 0\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self._last_asr_result_text = ""\r
- try:\r
- self._asr_completed_tone_done = 0\r
- except Exception:\r
- pass\r
- \r
- self.VC_record_thread_close()#关闭录音线程\r
- _thread.stack_size(self.Audio_buf_size+8*1024)\r
- self.VC_record_thread_id = _thread.start_new_thread(self.VC_record_thread,())\r
- _thread.stack_size(8*1024)\r
- print("self.VC_record_thread_id:{}".format(self.VC_record_thread_id))\r
- try:\r
- \r
- if self.VC_record_TYPE == VC_record_TYPE_AMR :\r
- try :\r
- self.record.stream_start(audio.Record.AMRNB, self.VC_record_samplerate, 0)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- elif self.VC_record_TYPE == VC_record_TYPE_PCM:\r
- print("record_pcm.stream_read_start()!!!!!!!!!!!!")\r
- self.record.stream_start(audio.Record.WAV, self.VC_record_samplerate, 0)\r
- # if self.record_pcm != None:\r
- # try:\r
- # self.record_pcm.stream_stop()\r
- # self.record_pcm.close()\r
- # self.record_pcm = None\r
- # except Exception as e:\r
- # usys.print_exception(e)\r
- # if self.record_pcm == None: \r
- # ret = self.record_pcm = audio.Audio.PCM(0,1,16000,0,1,2)\r
- # print("audio.Audio.PCM:ret:{}".format(ret))\r
- # ret = self.record_pcm.stream_read_start()\r
- # print("self.record_pcm.stream_read_start:ret:{}".format(ret))\r
- except Exception as e:\r
- usys.print_exception(e) \r
- \r
- self.VC_record_state_flag = 1\r
- # 录音硬件已起:协议侧应对应 audio.create(PTT)或长对话直传 PCM\r
- try:\r
- print("[VC][proto] record_start sr={} pcm_type={} VC_mode={} session_started={} "\r
- "ptt_audio_created={} thread_id={}".format(\r
- self.VC_record_samplerate, self.VC_record_TYPE, self.VC_mode,\r
- getattr(self, "_vc_session_started", -1),\r
- getattr(self, "_ptt_audio_created", -1),\r
- self.VC_record_thread_id))\r
- except Exception:\r
- pass\r
- # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)\r
- try:\r
- self._vc_publish_led("listening", "VC_Speech_Start")\r
- except Exception:\r
- pass\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1:\r
- self._ptt_on_record_hardware_started()\r
- except Exception:\r
- pass\r
- \r
- except Exception as e:\r
- self.websocket_error()\r
- self.VC_record_state_flag = 1\r
- self.VC_Speech_End()#关闭录音\r
- usys.print_exception(e) \r
- #停止录音\r
- def VC_Speech_End(self):\r
- try:\r
- self._uplink_mark_end("VC_Speech_End")\r
- self.AI_ASR_flag = 0\r
- self.AI_ASR_str_tmp = ""\r
- # 勿在此清空 AI_ASR_str:vad.silence 停麦常早于 asr.completed,清空会导致 check_text:len:0 连播 think 音\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr})\r
- \r
- if self.VC_record_state_flag == 0:\r
- return\r
- #self.AI_client_asr_StopTranscription_requset()#请求停止\r
- if self.VC_record_TYPE == VC_record_TYPE_AMR:\r
- self.record.stream_stop()\r
- self.record.stop()#关闭录音\r
- Record_read_buf = bytearray(self.Audio_buf_size)\r
- for i in range(10):\r
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
- # if VC_AUDIO_FRAME_LOG:\r
- # print("Record_len:!!!!!!{}".format(Record_len))\r
- if Record_len <= 0:\r
- break\r
- elif self.VC_record_TYPE == VC_record_TYPE_PCM:\r
- self.record.stream_stop()\r
- self.record.stop()#关闭录音\r
- Record_read_buf = bytearray(self.Audio_buf_size)\r
- for i in range(10):\r
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
- # if VC_AUDIO_FRAME_LOG:\r
- # print("Record_len:!!!!!!{}".format(Record_len))\r
- if Record_len <= 0:\r
- break\r
- # if self.record_pcm != None:\r
- # try:\r
- # self.record_pcm.stream_stop()\r
- # self.record_pcm.close()\r
- # self.record_pcm = None\r
- # except Exception as e:\r
- # usys.print_exception(e)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.VC_record_thread_close()#关闭录音线程\r
- if self.AI_record_tmp_queue.empty() != True:\r
- self.AI_record_tmp_queue.clear()\r
- \r
- self.VC_record_state_flag = 0\r
- # PTT 一轮录音结束:清长按语义,避免后续短按松手误走 end_speech 而不播提示音\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1:\r
- self.AI_key_long_press_flag = 0\r
- except Exception:\r
- pass\r
- # 录音停止:单对话进入思考态(等服务器),勿继续蓝闪聆听\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1:\r
- self._ptt_await_server = 1\r
- self._vc_publish_led("thinking", "VC_Speech_End/ptt")\r
- else:\r
- self._ptt_await_server = 0\r
- self._vc_publish_led("idle", "VC_Speech_End")\r
- except Exception:\r
- pass\r
- print("[VC] Speech_End record_state={}".format(self.VC_record_state_flag))\r
- #关闭录音线程 \r
- def VC_record_thread_close(self): \r
- if self.VC_record_thread_id != 0:\r
- count =50\r
- while True:\r
- count -= 1\r
- if self.VC_record_thread_lock == 1:\r
- self.VC_record_thread_lock = 2\r
- if self.VC_record_thread_lock == 0 or count <=0:\r
- break\r
- else:\r
- print("VC_record_thread_thread going:{}...".format(count))\r
- utime.sleep_ms(1)\r
- self.VC_record_thread_lock = 0\r
- print("VC_record_thread_thread_close")\r
- if self.VC_record_thread_id != 0:\r
- try:\r
- _thread.stop_thread(self.VC_record_thread_id)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.VC_record_thread_id = 0\r
- #获取录音数据\r
-\r
- def VC_record_thread(self):\r
- ##return\r
- print("[VC] record_thread start")\r
- count = 0\r
- RequestToSpeak_flag = 0\r
- # try:\r
- # uos.remove('/usr/test.pcm')\r
- # except Exception as e:\r
- # usys.print_exception(e)\r
- #record_data_queue = Queue(100)\r
- #fd = open('/usr/test.pcm','w')\r
- # if self.VC_record_TYPE == 1:\r
- # h_data= [0x23,0x21 ,0x41 ,0x4D ,0x52 ,0x0A ]\r
- # h_data_b = bytes(h_data)\r
- # self.AI_client_VC_send(h_data_b)\r
- AI_record_tmp_queue_len = 0\r
- vad_buf = bytearray()\r
- VAD_data_count = 0\r
- vad_state = 0\r
- self.VAD_count = 0\r
- while True:\r
- try:\r
- if self.VC_record_thread_lock == 2:\r
- break\r
- self.VC_record_thread_lock = 1\r
- # 死循环接收数据\r
- #if self.VC_state == VC_State_SpeechStart:\r
- Record_read_buf = bytearray(self.Audio_buf_size)\r
- \r
- Record_len = 0\r
- if self.VC_record_TYPE == VC_record_TYPE_PCM:#pcm\r
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
- #utime.sleep_ms(60)\r
- \r
- else :\r
- \r
- Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)\r
- utime.sleep_ms(600)\r
-\r
- #Record_len = 1024*2\r
- #print("VC_record_thread:{}".format(Record_len))\r
- if Record_len > 0:\r
- read_buf = bytes(Record_read_buf[:Record_len])\r
- # if VC_AUDIO_FRAME_LOG:\r
- # print("Record_len:{}".format(Record_len))\r
- if self.record_data_save_flag == 1:\r
- if self.record_fd == None:\r
- try:\r
- uos.remove('/usr/record_data.pcm')\r
- except Exception as e:\r
- pass\r
- # 二进制写入,避免文本模式导致数据异常\r
- self.record_fd = open('/usr/record_data.pcm', 'wb')\r
- self.record_fd_len = 0\r
- if self.record_fd != None:\r
- try:\r
- self.record_fd.write(read_buf)\r
- print("write record:{}".format(len(read_buf)))\r
- self.record_fd_len = self.record_fd_len+len(read_buf)\r
- if self.record_fd_len >=500*1024:\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- except Exception as e:\r
- usys.print_exception(e)\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- # count+=Record_len\r
- # if count >150*1024:\r
- # self.record_fd.close()\r
- #self.record_fd = None\r
- #VAD检测\r
- if self.is_VAD == 1:\r
- vad_state = 0\r
- tmp_c = 1000\r
- while True:\r
- tmp_c -=1\r
- if tmp_c <=0:\r
- break\r
- #vad_buf_len = len(vad_buf)\r
- if VAD_data_count == 0:#没数据的时候\r
- vad_buf = Record_read_buf[:self.VAD_data_len]\r
- VAD_data_count = self.VAD_data_len\r
- self.vad.custom_vad_reset()#重置vad\r
- else:#有数据的时候\r
- if Record_len - VAD_data_count < self.VAD_data_len :#剩余长度不够一次填充,先保留,但不填算法\r
- vad_buf = Record_read_buf[VAD_data_count:Record_len]\r
- VAD_data_count = 0\r
- break\r
- else:#剩余长度够填一次数据\r
- vad_buf = Record_read_buf[VAD_data_count:VAD_data_count+self.VAD_data_len]\r
- VAD_data_count += len(vad_buf)\r
- #print("vad_buf_len:{},{}".format(len(vad_buf),VAD_data_count))\r
- \r
- if len(vad_buf) >= self.VAD_data_len:\r
- vad_state = self.vad.custom_vad_process(vad_buf,len(vad_buf))#给vad数据\r
- #print("vad_state:{}".format(vad_state))\r
- \r
- if VAD_data_count >= Record_len:\r
- VAD_data_count = 0\r
- break\r
- if vad_state == 1:\r
- print("检测到VAD=1!!!!!!!!!!!!!")\r
- \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断\r
- # RequestToSpeak_flag = 1\r
- \r
- self.VAD_count+=1\r
- if self.VAD_count >= 20: \r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断\r
- \r
- else:\r
- if self.VAD_count != 0:\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Pause,"data":0})#恢复音频播放\r
- self.VAD_count = 0\r
- if self._vc_tts_pipeline_busy():\r
- pass\r
- if vad_state == 1:\r
- self.AI_record_tmp_queue.put(read_buf)#入缓存队列\r
- AI_record_tmp_queue_len += len(read_buf)\r
- \r
- \r
- else:\r
- VAD_data_count = 0\r
- if self.VC_record_state_flag <=1 :#没发送前先存起来\r
- self.AI_record_tmp_queue.put(read_buf)\r
- AI_record_tmp_queue_len += len(read_buf)\r
- else:\r
- AI_record_tmp_queue_len = 0\r
- while True:\r
- if self.AI_record_tmp_queue.empty() == True:\r
- break\r
- else:\r
- pass\r
- # 协议门控:会话未 started 前禁止上行音频(否则 session not initialized)\r
- if VC_PROTO_GUARDS and self._vc_session_started != 1 and self.record_data_save_flag == 0:\r
- _ = self.AI_record_tmp_queue.get()\r
- AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(_))\r
- continue\r
- if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):\r
- self.AI_client_VC_send(self.AI_record_tmp_queue.get()) #发送之前存的数据\r
- else:\r
- self._uplink_mark_end("silence_gate(drop_queue)")\r
- _ = self.AI_record_tmp_queue.get()\r
- # 当前帧发送前协议门控(可用 VC_PROTO_GUARDS 临时关闭):\r
- # - session started 前禁止发帧\r
- # - Push-To-Talk 在 audio.create 前禁止发帧\r
- if VC_PROTO_GUARDS and (\r
- (self._vc_session_started != 1 and self.record_data_save_flag == 0) or\r
- (self.VC_mode == 1 and self._ptt_audio_created != 1 and self.record_data_save_flag == 0)\r
- ):\r
- # 未 ready:转入缓存,等待 started 再发(并做容量控制)\r
- try:\r
- self.AI_record_tmp_queue.put(read_buf)\r
- AI_record_tmp_queue_len += len(read_buf)\r
- while AI_record_tmp_queue_len > 128*1024 and self.AI_record_tmp_queue.empty() != True:\r
- d = self.AI_record_tmp_queue.get()\r
- AI_record_tmp_queue_len = max(0, AI_record_tmp_queue_len - len(d))\r
- except Exception:\r
- pass\r
- else:\r
- if not (self._server_vad_silence == 1 and self.record_data_save_flag == 0):\r
- self.AI_client_VC_send(read_buf)\r
- else:\r
- self._uplink_mark_end("silence_gate(drop_buf)")\r
- \r
- \r
-\r
- \r
- \r
-\r
- #print("VC_record_thread:{}".format(len(read_buf)))\r
- #count+=len(read_buf)\r
- # if count>=500*1024:#结束\r
- # self.AI_client_VC_end_speech()\r
- # break\r
- # count+=1\r
- # if count %10 == 0:\r
- # print("VC_record_thread on line {} asr recv :{} send:{}".format(self.VC_record_state_flag,_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)))\r
- \r
- # if self.VC_state >= VC_State_SpeechEnd:\r
- # break\r
- \r
- except Exception as e:\r
- usys.print_exception(e)\r
- #break\r
- self.VC_record_thread_lock = 0\r
- if self.record_data_save_flag == 1:\r
- utime.sleep_ms(1)\r
- else:\r
- utime.sleep_ms(100)\r
- #fd.close()\r
- print("VC_record_thread:exit")\r
- self.VC_record_thread_id = 0#线程退出\r
- self.VC_record_thread_lock = 0\r
-###################################################### VC ###################################################################################\r
- def websocket_connect_VC(self):\r
- print("websocket_connect_VC:{} e:{}!!!!!!!!!!!!!!!!!!!!!".format(self.AI_client_VC_isconnect,self.AI_client_VC_debug))\r
- if self.AI_client_VC_isconnect == 1:\r
- return\r
- # 创建VC客户端\r
- try:\r
- signal = net.csqQueryPoll()\r
- if signal == -1 or sim.getStatus() != 1:#无蜂窝/无卡等:弱网提示\r
- self.websocket_error(play_weak_signal_tone=True)\r
- return\r
- \r
- req_str = ""\r
- ack = ""\r
- if self.is_VC_ACK == 1:\r
- ack = "ack=1"\r
- lang = "lang=zh-cn"#普通话测试环境\r
- try:\r
- if "lang" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
- lang = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["lang"]\r
- else:\r
- print("not lang!!!!!!!!!!!!!!!!!!!!!!")\r
- except Exception as e:\r
- usys.print_exception(e)\r
- \r
- #女,男,童,粤,川\r
- self.bot_name = "柒先生"\r
- try:\r
- if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
- self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]\r
- else:\r
- print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")\r
- except Exception as e:\r
- usys.print_exception(e)\r
- \r
- # Headers = {'X-NLS-Token':'0c8b8f05a46f423096be64a9c4c46310'}\r
- # self.AI_client_VC = uwebsocket.Client.connect('wss://nls-gateway-cn-beijing.aliyuncs.com/ws/v1',headers=Headers, debug=False)\r
- token = EventMesh.publish("get_dev_token")\r
- print("token:{}".format(token))\r
- \r
- req_str = "vc/chat?is_auth={}".format(self.is_auth)\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- base = vc_config.get_attr("VC_WS_PATH", "vc/chat") or "vc/chat"\r
- req_str = "{}?is_auth={}".format(base, self.is_auth)\r
- except Exception:\r
- pass\r
- if(token):\r
- url = 'ws://{}/{}&dt={}'.format(self.server_ip,req_str,token)\r
- else:\r
- url = 'ws://{}/{}'.format(self.server_ip,req_str)\r
- #url = 'ws://115.190.94.208:22080/vc/coze?dt={}'.format(token)\r
- \r
- if VC_WS_IO_LOG:\r
- print("[VC] ws url {}".format(url))\r
- self.AI_client_VC = uwebsocket.Client.connect(url, debug=False)\r
- #self.AI_WS_connect_Queue.put(self.AI_client_VC)\r
-\r
- \r
- print("[VC] websocket_connect ok")\r
- except Exception as e: #网络连接异常(已通过 CSQ/SIM,多为 URL/握手/服务端问题,勿播「信号弱」)\r
- usys.print_exception(e)\r
- print("websocket_connect_VC error!!!\r\n")\r
- try:\r
- print("[VC] ws connect exc: {}".format(e))\r
- except Exception:\r
- pass\r
- str_error = str(e)\r
- if str_error.find("[Errno 22] EINVAL")!= -1: \r
- print("[VC] websocket EINVAL: {}".format(str_error))\r
- self.websocket_error(play_weak_signal_tone=False)\r
- EventMesh.publish("set_storage_config",["powerRestart",1])#调试临时关闭\r
- utime.sleep_ms(1000)\r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- Power.powerRestart()\r
- \r
- self.websocket_error(play_weak_signal_tone=False)\r
- return\r
- #print("AI_client_VC:{}".format(self.AI_client_VC))\r
- if self.AI_client_VC == None:\r
- EventMesh.publish("update_dev_token")#获取token\r
- self.websocket_error(play_weak_signal_tone=False)\r
- return\r
- self.AI_client_VC_debug+=1\r
- utime.sleep_ms(10)\r
- self.AI_client_VC.settimeout(5)\r
- #self.AI_VC_send_data_queue_thread_close()#关闭发送线程\r
- if self.AI_VC_send_data_queue_thread_id == 0: \r
- _thread.stack_size(self.Audio_buf_size+8*1024)\r
- self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
- _thread.stack_size(8*1024)\r
- print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))\r
- self.AI_client_VC_recv_thread_close()#关闭读线程\r
- # 线程接收数据\r
- _thread.stack_size(self.Audio_buf_size+8*1024)\r
- self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))\r
- _thread.stack_size(8*1024)\r
- self.AI_client_VC_isconnect = 1\r
- print("websocket_connect_VC end")\r
-\r
- def websocket_error(self, TTS_str=None, play_weak_signal_tone=False):\r
- """play_weak_signal_tone=True 时播 xinhaoruo1(弱网/无卡类);连接/协议失败请传 False,避免误报「信号弱」。"""\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] websocket_error enter VC_mode={} TTS_str={} weak_hint={} -> Close then uplink_stop(async)".format(\r
- getattr(self, "VC_mode", -1), TTS_str, play_weak_signal_tone))\r
- except Exception:\r
- pass\r
- print("websocket_error1!!!!")\r
- utime.sleep_ms(800)\r
- self.Close()\r
- print("websocket_error1.1!!!!")\r
- print("websocket_error!!!!")\r
- print("websocket_error!!!!")\r
- self.VC_Thinking_timerout = 0\r
- self.VC_satrt_flag = 0\r
- self.AI_TTS_str = ""\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self.AI_ASR_flag = 0\r
- # 可能由收包线程/其它线程触发:统一切回主线程执行 stop,避免“异常→重试→再异常”的循环\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] websocket_error schedule uplink_stop reason=websocket_error + set_led_idle")\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"websocket_error"})\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- if TTS_str == None:\r
- self.websocket_error_count+=1\r
- # if self.websocket_error_count <=3:\r
- # EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])\r
- if play_weak_signal_tone and self.websocket_error_count < 10:\r
- EventMesh.publish("audio_montage_play", ["xinhaoruo1.mp3"])\r
- elif self.websocket_error_count >=10:\r
- # EventMesh.publish("paly_iccid_imei") \r
- EventMesh.publish("audio_montage_play", ["xinhaoruo1.mp3"])\r
- # else:\r
- # EventMesh.publish("audio_montage_play",["R_SIM_FAILE.mp3"])\r
- #self.aud.play(1,1,'U:/static/R_SIM_FAILE.mp3')\r
- if self.websocket_error_count <3 :\r
- utime.sleep_ms(3000)\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})\r
- else:\r
- print("websocket_error:tts:{}".format(TTS_str))\r
- self.AI_TTS_str = TTS_str\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})\r
- self.Close_flag = 1\r
- print("websocket_error2{}".format(self.websocket_error_count))\r
- \r
- \r
- print("websocket_error4")\r
- \r
-\r
- def websocket_deconnect_VC(self):\r
- #print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))\r
- print("websocket_deconnect_VC!!!!!!!")\r
- self._vc_session_started = 0\r
- # 断链/模式切换时必须重置“上行录音状态”,否则会被 _uplink_request_start 的幂等门控挡住,\r
- # 表现为:切回长对话后无法自动开麦,按键 cancel/canceled 也无法恢复。\r
- try:\r
- self.VC_record_state_flag = 0\r
- except Exception:\r
- pass\r
- try:\r
- self._server_vad_silence = 0\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_audio_created = 0\r
- except Exception:\r
- pass\r
- try:\r
- # 若录音线程/录音设备仍在占用,先停掉\r
- self.VC_Speech_End()\r
- except Exception:\r
- try:\r
- self.VC_record_thread_close()\r
- except Exception:\r
- pass\r
- # 断链必须同步重置 FSM,否则 await_played/turn 残留会导致动态切换后“无法自动开麦/按键无效”\r
- try:\r
- self.fsm.session = VCSessionState.Disconnected\r
- self.fsm.turn = VCTurnState.Idle\r
- self.fsm.await_played = False\r
- self.fsm.last_tts_completed_ms = 0\r
- self.fsm.last_llm_ms = 0\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- self.AI_client_VC_recv_thread_close()#关闭读线程\r
- self.AI_client_VC_recv_id = 0\r
- if self.AI_client_VC_isconnect == 1 or self.AI_client_VC!=None:\r
- try:\r
- self.AI_client_VC_isconnect = 0\r
- count = 500\r
- while True:#等待发完数据\r
- count-=1\r
- if count <=0 or self.AI_VC_send_data_queue_thread_lock == 0:\r
- break\r
- utime.sleep_ms(1)\r
- \r
- try:\r
- self.AI_client_VC.close()\r
- self.AI_client_VC_debug -=1\r
- #self.AI_client_VC=None\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try :\r
- self.AI_client_VC.close()\r
- self.AI_client_VC_debug -=1\r
- #self.AI_client_VC=None\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try :\r
- self.AI_client_VC._close()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.AI_client_VC = None\r
- except Exception as e:\r
- usys.print_exception(e)\r
- print("websocket_deconnect_VC {}!!!!!!!".format(self.AI_client_VC_isconnect))\r
- #websocket发送ping\r
- def AI_client_VC_ping(self):\r
- pass\r
- if self.AI_client_VC_isconnect == 1:\r
- #self.AI_client_VC.write_frame(0x09,b'ping ping')\r
- self.AI_client_VC_send("ping")\r
- if VC_HEARTBEAT_LOG:\r
- print("VC: ping ping")\r
-\r
- #websocket发送数据 \r
- def AI_client_VC_send(self,data_str):\r
- # if isinstance(data_str, str):#是字符串\r
- # print("AI_client_VC_send:{}".format(data_str))\r
- # pass\r
- # else:\r
- # print("AI_client_VC_send:len{}".format(len(data_str)))\r
- # 统计“语音上行”真实开始/结束:这里只统计二进制音频(非 ping/字符串)\r
- try:\r
- if not isinstance(data_str, str):\r
- l = len(data_str)\r
- if l > 0:\r
- self._uplink_mark_start(l)\r
- self._uplink_bytes += l\r
- except Exception:\r
- pass\r
- self.AI_VC_send_data_queue.put([AI_client_VC_num,data_str]) \r
- #关闭数据发送线程\r
- def AI_VC_send_data_queue_thread_close(self):\r
- if self.AI_VC_send_data_queue_thread_id != 0:\r
- # 先清空待发队列,避免关闭过程持续堆积/刷屏\r
- if self.AI_VC_send_data_queue.empty() != True:\r
- self.AI_VC_send_data_queue.clear()\r
- # 通知发送线程退出;若发送线程正阻塞在底层 send,则只能等超时或强制 stop_thread\r
- self.AI_VC_send_data_queue_thread_lock = 2\r
- count = 500\r
- while True:\r
- count -= 1\r
- if self.AI_VC_send_data_queue_thread_lock == 0 or count <=0:\r
- break\r
- else:\r
- # 该打印只是“等待线程停下来”的调试心跳,默认关闭以免影响系统实时性\r
- if VC_HEARTBEAT_LOG and (count % 50 == 0 or count > 490):\r
- print("AI_VC_send_data_queue_thread going:{}...".format(count))\r
- utime.sleep_ms(5)\r
- self.AI_VC_send_data_queue_thread_lock = 0\r
- if VC_HEARTBEAT_LOG:\r
- print("AI_VC_send_data_queue_thread_close")\r
- if self.AI_VC_send_data_queue_thread_id != 0:\r
- try:\r
- # 线程可能已自然退出/被复用,stop_thread 会抛 ValueError\r
- try:\r
- running = _thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)\r
- except Exception:\r
- running = True\r
- if running:\r
- _thread.stop_thread(self.AI_VC_send_data_queue_thread_id)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.AI_VC_send_data_queue_thread_id = 0\r
- #发送数据队列\r
- def AI_VC_send_data_queue_thread(self):\r
- print("AI_VC_send_data_queue_thread:start")\r
- count_t = 0\r
- while True:\r
- if self.AI_VC_send_data_queue_thread_lock == 2:\r
- print("AI_VC_send_data_queue_thread,该退出了")\r
- break \r
- if self.AI_VC_send_data_queue.empty() :#不能去掉,去掉会卡死\r
- self.AI_VC_send_data_queue_thread_lock = 0 \r
- count_t+=1\r
- # if count_t%200 == 0:\r
- # print("self.AI_VC_send_data_queue.empty")\r
- utime.sleep_ms(1)\r
- else:\r
- #data[0]是websocket连接对象编号,data[1]是数据\r
- #print("AI_VC_send_data_queue_thread:get")\r
- \r
- self.AI_VC_send_data_queue_thread_lock = 1\r
- data = self.AI_VC_send_data_queue.get()\r
- \r
- \r
- #print("AI_VC_send_data_queue_thread:num{} len{}".format(data[0],len(data[1])))\r
- if data != None:\r
- try:\r
- #data["func"](data["data"])\r
- # 发送数据\r
- #bytes_data = bytes(data[1])\r
- #data[0].send(data[1])\r
- #print("AI_VC_send_data_queue_thread:len:{}".format(len(data[1])))\r
- #print("AI_VC_send_data_queue_thread on line asr recv :{} vc recv:{}".format(_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_client_VC_recv_id)))\r
- if data[0] == AI_client_VC_num:\r
- \r
- if isinstance(data[1], str):#是字符串\r
- self.VC_pingping_timerout = 0\r
- s = data[1]\r
- if s is None:\r
- s = ""\r
- # 保活 ping 不落 WS IO 日志(避免周期性刷屏)\r
- if VC_WS_IO_LOG and s != "ping":\r
- try:\r
- if len(s) > 240:\r
- preview = s[:160] + " ... " + s[-60:]\r
- else:\r
- preview = s\r
- print("[VC] WS send_txt len={} txt={}".format(len(s), preview))\r
- except Exception:\r
- try:\r
- print("[VC] WS send_txt {}".format(data[1]))\r
- except Exception:\r
- pass\r
- elif not VC_WS_IO_LOG:\r
- if s == "ping":\r
- pass\r
- else:\r
- try:\r
- ev, tid = _vc_out_json_event_meta(s)\r
- if ev:\r
- print("[VC][proto] tx event={} trace_id={} len={} ws_conn={}".format(\r
- ev, tid or "-", len(s), self.AI_client_VC_isconnect))\r
- else:\r
- print("[VC][proto] tx unknown_json len={} ws_conn={}".format(\r
- len(s), self.AI_client_VC_isconnect))\r
- except Exception:\r
- print("[VC][proto] tx parse_fail len={}".format(len(s)))\r
- else:\r
- pass\r
- # if VC_AUDIO_FRAME_LOG:\r
- # print("VC: send_data len:{}".format(len(data[1])))\r
- # print("self.AI_client_VC = {} , self.AI_client_VC_isconnect = {}".format(self.AI_client_VC,self.AI_client_VC_isconnect)) \r
- if self.AI_client_VC != None and self.AI_client_VC_isconnect == 1:\r
- self.AI_client_VC.send(data[1])\r
- else:\r
- try:\r
- if isinstance(data[1], str) and data[1] != "ping":\r
- _ev, _tid = _vc_out_json_event_meta(data[1])\r
- print("VC VC: send_data:ERROR len={} event={} trace_id={}".format(\r
- len(data[1]), _ev or "?", _tid or "-"))\r
- else:\r
- print("VC VC: send_data:ERROR:{}".format(len(data[1])))\r
- except Exception:\r
- print("VC VC: send_data:ERROR:{}".format(len(data[1])))\r
- #print("VC: send_data end")\r
- \r
- #print("VC: send_data end")\r
- except Exception as e:\r
- print("DeviceInfo_queue_thread:ERROR:{}".format(e))\r
- usys.print_exception(e)\r
- if data[0] == AI_client_VC_num:\r
- #self.websocket_error()\r
- pass\r
-\r
- \r
- try:\r
- if self.AI_client_VC_isconnect != 0:\r
- print("VC发送数据错误,退出")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC发送出错了,请重试"}) \r
- self.ud_error(["VC",self.VC_taskid])\r
- except Exception as e: #网络连接异常\r
- usys.print_exception(e)\r
-\r
- utime.sleep_ms(1)\r
- self.AI_VC_send_data_queue_thread_lock = 0 \r
- #VC关闭读线程\r
- def AI_client_VC_recv_thread_close(self):\r
- if self.AI_client_VC_recv_id != 0:\r
- count =500\r
- while True:\r
- count -= 1\r
- if self.AI_client_VC_recv_lock == 1:\r
- self.AI_client_VC_recv_lock = 2\r
- if self.AI_client_VC_recv_lock == 0 or count <=0:\r
- break\r
- else:\r
- print("AI_client_VC_recv_thread going:{}...".format(count))\r
- utime.sleep_ms(1)\r
- self.AI_client_VC_recv_lock = 0\r
- print("AI_client_VC_recv_thread_close")\r
- if self.AI_client_VC_recv_id!= 0:\r
- try:\r
- _thread.stop_thread(self.AI_client_VC_recv_id)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.AI_client_VC_recv_id = 0\r
- #Ai VC websocket返回数据 \r
- def AI_client_VC_recv(self,cli):\r
- count = 0\r
- asr_led_flag = 0\r
- tts_led_flag = 0\r
- while True:\r
- try:\r
- # 死循环接收数据\r
- self.AI_client_VC_recv_lock = 0\r
-\r
- count+=1\r
- if self._vc_tts_pipeline_busy() and (self.tts_in_data_len - self.tts_out_data_len > 64*1024 or count>5):\r
- count = 0\r
- utime.sleep_ms(1)\r
- if not self._vc_tts_pipeline_busy():\r
- utime.sleep_ms(1)\r
- recv_data = cli.recv()\r
- if self.AI_client_VC_recv_lock == 2:\r
- print("AI_client_VC_recv 该退出了")\r
- break\r
- self.AI_client_VC_recv_lock = 1\r
- if self._vc_rx_resets_long_idle_timer(recv_data):\r
- self.close_timerout = 0\r
- if recv_data == "websocket close":\r
- # 服务器关闭连接\r
- print("VC 服务器关闭连接!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- #self.AI_client_VC_isconnect = 0\r
- #self.websocket_deconnect_VC()\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr}) \r
- # #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(1500)})\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_connect_asr}) \r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"}) \r
- self.ud_error(["VC",self.VC_taskid])\r
- break\r
- elif recv_data == "":#客户端关闭连接\r
- print("VC VC :客户端关闭连接")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC连接断开了,请重试"}) \r
- self.ud_error(["VC",self.VC_taskid])\r
- break\r
- try:\r
- self.websocket_error_count = 0\r
- if isinstance(recv_data, str):#返回的是字符串\r
- jsload_data = ujson.loads(recv_data)\r
- if VC_WS_IO_LOG:\r
- print("[VC] recv raw:{}".format(recv_data))\r
- else:\r
- try:\r
- _rev = jsload_data.get("event", "")\r
- if _rev != "llm.delta":\r
- print("[VC] rx event={} sid={}".format(\r
- _rev, jsload_data.get("sid", "") or "-"))\r
- except Exception:\r
- pass\r
- #print("AI_client_VC_recv jsload_data = {}".format(jsload_data))\r
- try:\r
- self._cur_sid = jsload_data.get("sid", "") or ""\r
- except Exception:\r
- self._cur_sid = ""\r
- try:\r
- ev = jsload_data.get("event", "")\r
- if ev:\r
- self._vc_dispatch_server_event(ev, jsload_data)\r
- except Exception:\r
- pass\r
- \r
- if jsload_data["event"] == "started" :#会话开始\r
- #payload = jsload_data["payload"]\r
- # self.VC_dialog_id = payload["dialog_id"] \r
- # self.VC_task_id = jsload_data["task_id"] \r
- # started 仅表示链路就绪:不在这里提前切灯态;\r
- # 聆听灯只在“录音真正开始/停止”处更新(VC_Speech_Start/End)。\r
- # 会话开始:允许上行;连续对话模式下直接进入“可发送音频”状态\r
- self._vc_session_started = 1\r
- # started 时不要强制清 Close_flag:\r
- # - Close_flag==1:提示音/模式切换播完退出\r
- # - Close_flag==0:正常对话流程\r
- self._ptt_audio_created = 0\r
- self._server_vad_silence = 0\r
- self._turn_has_asr_completed = 0\r
- self._turn_has_vad_silence = 0\r
- self._turn_has_tts_started = 0\r
- self._llm_completed_ms = 0\r
- if self.VC_mode == 0 and getattr(self, "Close_flag", 0) == 0:\r
- _skip_uplink = 0\r
- try:\r
- if getattr(self, "_skip_started_uplink_once", 0) == 1:\r
- self._skip_started_uplink_once = 0\r
- _skip_uplink = 1\r
- print("[VC][uplink_ctrl] Deny START by server.started cause=boot_prompt")\r
- except Exception:\r
- pass\r
- if not _skip_uplink:\r
- # 会话已 started:若此时上行未启动,则在这里补一次开麦,避免“Start 前开麦被 Close 停掉”后\r
- # 出现首轮无音频上行导致“第一次对话没有”的情况。\r
- if self.record_data_save_flag == 0:\r
- if self.VC_record_state_flag == 0:\r
- self.request_uplink_start("server.started")\r
- else:\r
- self._vc_clear_stale_record_state("server.started")\r
- self.request_uplink_start("server.started/retry")\r
- \r
- print("VC:Started!!!!!!!!\r\n")\r
- print("TTS音频为:{},语音下发地址为:{}".format(self.AI_TTS_str,self.AI_new_voice_url))\r
- if self.AI_new_voice_url != "" and self.alarm_play_flag == 1 :#有闹钟,先响闹钟\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放铃声\r
- self.alarm_play_flag = 0\r
- self.voice_choose_flag = 1\r
- elif self.AI_TTS_str == "" and self.AI_new_voice_url != "" and self.music_play_flag == 0:\r
- # if EventMesh.publish("get_history_voice_flag") == 1:\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- # self.recv_voice = 0#当留言列表为空,留言播放标志清空\r
- # else:\r
- # self.voice_cnt = 0\r
- # print("定时器开启222222222")\r
- # self.voice_timer.start(1000,1,self.voice_judge)\r
- elif self.AI_TTS_str != "" and self.AI_new_voice_url == "":\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- \r
- elif self.AI_TTS_str != "" and self.AI_new_voice_url != "" and self.music_play_flag == 0:#遇到tts和语音留言冲突时(并且没有音乐播放)\r
- if EventMesh.publish("get_history_voice_flag") == 1:#播放历史留言\r
- if EventMesh.publish("get_wait_voice_flag") == 0:\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- self.his_cnt = 0\r
- self.his_timer.start(1000,1,self.history_play) \r
- self.voice_choose_flag = 1\r
- else:#等待留言\r
- if self.pwk_notice_flag == 0:\r
- self.wait_cnt = 0\r
- print("未听播放1")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- self.wait_timer.start(1000,1,self.wait_play) \r
- self.voice_choose_flag = 1\r
- \r
- else:#已确定播放\r
- print("未听播放2")\r
- self.wait_cnt = 0\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- self.wait_timer.start(1000,1,self.wait_single_play) \r
- self.voice_choose_flag = 1\r
- else:#播放正常留言\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- self.voice_cnt = 0\r
- print("定时器开启111111111")\r
- self.voice_timer.start(1000,1,self.voice_judge) \r
- self.voice_choose_flag = 1\r
- # self.AI_TTS_str = "" \r
- elif self.AI_TTS_str != "" and self.AI_new_voice_url != "" and self.music_play_flag == 1 :#遇到tts和语音留言冲突时(并且有音乐播放)\r
- self.music_play_flag = 0\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_synthesis})\r
- self.music_timer.start(1000,1,self.music_play) \r
- self.voice_choose_flag = 1\r
- else:\r
- #开始上传语音\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("UPLOAD_START_TONE_ENABLED", False):\r
- self.chenggong_tone()\r
- except Exception:\r
- pass\r
- if self.VC_mode == 0:\r
- self.request_uplink_start("server.started/idle")\r
- else:\r
- # Push-To-Talk:必须先同步发出 audio.create,再开麦。\r
- # request_send_audio_create() 走 EventMesh 异步队列,会导致录音线程先于\r
- # audio.create 上行,深度睡眠唤醒后首轮 ASR 空结果、第二轮才正常。\r
- try:\r
- self.AI_client_VC_send_speech()\r
- except Exception:\r
- self.request_send_audio_create()\r
- # 若长按发生在“未 started”窗口,收到 started 后立即开麦进入新一轮 PTT\r
- try:\r
- if getattr(self, "_ptt_force_record_on_started", 0) == 1:\r
- self._ptt_force_record_on_started = 0\r
- self.VC_Speech_Start_now()\r
- except Exception:\r
- pass\r
- \r
- \r
- # if self.AI_new_voice_url != "":\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})\r
-\r
- elif jsload_data["event"] == "config.update" :#配置更新中,需要停止录音\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- self.request_uplink_stop("config.update")\r
-\r
- elif jsload_data["event"] == "asr.started" :#asr开始 \r
- try:\r
- self.fsm.on_server_asr_started()\r
- except Exception:\r
- pass\r
- self._server_vad_silence = 0\r
- self._turn_has_asr_completed = 0\r
- self._turn_has_vad_silence = 0\r
- self._turn_has_tts_started = 0\r
- self._llm_completed_ms = 0\r
- try:\r
- self._asr_completed_tone_done = 0\r
- except Exception:\r
- pass\r
- # 单对话:_drop_tts_audio 在 VC_Speech_Start 真开录后再清 0,避免 canceled 后仍播旧 TTS\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- self._drop_tts_audio = 0\r
- except Exception:\r
- pass\r
- # 单对话:勿在 asr.started 置 flag=2(未真开麦会僵死);由 VC_Speech_Start 维护\r
- self.LocalRespondingEnded_timerout = 0\r
- elif jsload_data["event"] == "asr.result" :#asr结果\r
- try:\r
- self.AI_ASR_str = jsload_data["payload"]["content"]\r
- except Exception:\r
- try:\r
- self.AI_ASR_str = jsload_data.get("payload", {}).get("text", "") or ""\r
- except Exception:\r
- self.AI_ASR_str = ""\r
- print("[VC][ASR] {}".format(self.AI_ASR_str))\r
- try:\r
- if (self.AI_ASR_str or "").strip():\r
- self._last_asr_result_text = self.AI_ASR_str\r
- except Exception:\r
- pass\r
- if self.AI_ASR_str == "":#没识别到\r
- self.AI_TTS_str = "我没听清,请再说一遍"\r
- self._vc_led_alert_rb_flash()\r
- elif jsload_data["event"] == "asr.completed" :#asr 终止的标识 \r
- self._turn_has_asr_completed = 1\r
- try:\r
- if not (self.AI_ASR_str or "").strip():\r
- self.AI_ASR_str = getattr(self, "_last_asr_result_text", "") or ""\r
- except Exception:\r
- pass\r
- # 本地 think 提示:仅在有有效 ASR 文本时播,避免空轮次连播「滴」\r
- try:\r
- if getattr(self, "_asr_completed_tone_done", 0) == 1:\r
- pass\r
- elif not self._vc_effective_asr_text():\r
- try:\r
- print("[VC][ASR] completed empty, skip think tone")\r
- except Exception:\r
- pass\r
- else:\r
- stream_on = False\r
- try:\r
- stream_on = EventMesh.publish("get_vc_tts_playback_active") == 1\r
- except Exception:\r
- stream_on = False\r
- pcm_busy = getattr(self, "play_pcm", None) is not None\r
- playing = getattr(self, "VC_Play_flag", 0) == 1\r
- try:\r
- thinking = getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking\r
- except Exception:\r
- thinking = False\r
- if (not stream_on) and (not pcm_busy) and (not playing) and (not thinking):\r
- self._vc_local_play('U:/static/think.mp3', 1, 1, 650)\r
- self._asr_completed_tone_done = 1\r
- except Exception:\r
- pass\r
- print("self.commands_mode:{}".format(self.commands_mode))\r
- if self.commands_mode ==2:\r
- ret= self.check_text(self._vc_effective_asr_text())\r
- if ret == 0:#配置指令\r
- \r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- self.AI_ASR_send_buf_len = 0\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- try:\r
- \r
- self.request_uplink_stop("asr.completed/config_cmd")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e)\r
- continue\r
- elif ret == 1:#不处理\r
- pass \r
- #self.VC_Speech_End()\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
-\r
- # if self.record_data_save_flag == 1:\r
- # if self.record_fd :\r
- # self.record_fd.close()\r
- # self.record_fd = None\r
-\r
- # if dev_type != "7PIN" :\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.send_tone}) \r
- elif jsload_data["event"] == "vad.active" :#VAD 检测到说话→聆听\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("VAD_ACTIVE_BLUE_LED", True):\r
- self._server_vad_silence = 0\r
- except Exception:\r
- pass\r
- elif jsload_data["event"] == "vad.silence" :#VAD 终止的标识 \r
- # 说明服务端已判定“本轮说话结束”;此时继续上行只会干扰后续(例如推迟 TTS)。\r
- # 仅在“正在录音上行”的语音阶段才停止,避免误触发。\r
- self._server_vad_silence = 1\r
- self._turn_has_vad_silence = 1\r
- self._uplink_mark_end("vad.silence")\r
- if self.record_data_save_flag == 0:\r
- if self.VC_record_state_flag != 0 and self._vc_vad_uplink_stop_eligible():\r
- self.request_uplink_stop("vad.silence")\r
- elif jsload_data["event"] == "llm.created" :#思考(LLM)\r
- try:\r
- self._ptt_await_server = 0\r
- except Exception:\r
- pass\r
- try:\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- except Exception:\r
- pass\r
- self._llm_completed_ms = 0\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})\r
- pass\r
- elif jsload_data["event"] == "llm.completed" :#llm完成(可能无 tts)\r
- try:\r
- self._llm_completed_ms = utime.ticks_ms()\r
- except Exception:\r
- # 勿用 1:ticks_diff(now,1)≈开机至今,会瞬间满足 thinking 超时误以为该重启\r
- self._llm_completed_ms = 0\r
- try:\r
- self._cur_dialog_id = jsload_data.get("payload", {}).get("dialog_id", "") or ""\r
- except Exception:\r
- self._cur_dialog_id = ""\r
- try:\r
- self.fsm.log("server.llm.completed")\r
- except Exception:\r
- pass\r
- pass\r
- elif jsload_data["event"] == "llm.delta" :#llm返回数据\r
- if self.is_VAD == 0 and self.record_data_save_flag == 0:\r
- if self.VC_record_state_flag == 1:#llm返回数据还没停止,就先停止\r
- self.request_uplink_stop("llm.delta")\r
- elif jsload_data["event"] == "tts.started" :#tts开始\r
- try:\r
- self._ptt_await_server = 0\r
- except Exception:\r
- pass\r
- self._server_vad_silence = 1\r
- self._turn_has_tts_started = 1\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- self._uplink_mark_end("tts.started")\r
- # stop uplink + play_start 由 FSM action list 统一触发,避免多处重复调用\r
- # 标记本轮 TTS 来源:\r
- # - 有 payload.dialog_id:认为是“对话回复 TTS”\r
- # - 无 dialog_id:认为是“提示/提醒 TTS”(模式切换可通过 _pending_tts_kind 标记)\r
- try:\r
- did = jsload_data.get("payload", {}).get("dialog_id", "") or ""\r
- except Exception:\r
- did = ""\r
- # 注意:无 dialog_id 的 tts.started 很可能是提示音/提醒音;\r
- # 但“模式切换提示音”的 kind 可能已在 synthesis 时写入 _last_tts_kind,\r
- # 这里不应无条件覆盖为 prompt,否则播完无法触发下一轮开麦。\r
- try:\r
- if did:\r
- self._last_tts_kind = "dialog"\r
- else:\r
- k = getattr(self, "_pending_tts_kind", "") or ""\r
- if k:\r
- self._last_tts_kind = k\r
- else:\r
- cur = getattr(self, "_last_tts_kind", "") or ""\r
- if cur in (\r
- "mode_switch_long",\r
- "mode_switch_ptt",\r
- "boot_prompt",\r
- ):\r
- pass\r
- elif not cur:\r
- self._last_tts_kind = "prompt"\r
- except Exception:\r
- pass\r
- try:\r
- self._pending_tts_kind = ""\r
- except Exception:\r
- pass\r
- # 播放灯由阶段事件控制\r
- elif jsload_data["event"] == "tts.completed" :#tts结束\r
- # tts.completed 仅表示下发结束,本地可能仍在播放;绿灯关闭在本地播放结束时处理\r
- try:\r
- # server event 已在统一入口 on_server_event 触发 FSM,这里避免重复驱动\r
- pass\r
- except Exception:\r
- pass\r
- try:\r
- try:\r
- sid = getattr(self, "_cur_sid", "")\r
- did = getattr(self, "_cur_dialog_id", "")\r
- except Exception:\r
- sid, did = "", ""\r
- print("[TTS] completed in_len={} out_len={} q_empty={} play_pcm={} sid={} did={}".format(\r
- getattr(self, "tts_in_data_len", 0),\r
- getattr(self, "tts_out_data_len", 0),\r
- 1 if self.AI_audio_queue.empty() else 0,\r
- 1 if self.play_pcm != None else 0,\r
- sid,\r
- did,\r
- ))\r
- except Exception:\r
- pass\r
- # 播完上报完全由 FSM await_played + play_end 驱动,这里不再使用 LocalRespondingEnded_timerout/_need_audio_played_ack\r
- pass\r
-\r
- elif jsload_data["event"] == "canceled" :#打断\r
- # 停播/清队列已在 _vc_dispatch_server_event(canceled) 完成;此处仅收尾标志\r
- self.is_ruppt_tts = 0#设备上发中断tts,收到服务器返回的cancel后置为0\r
- self._need_audio_played_ack = 0\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- # 开麦由 FSM action list 统一触发(uplink_start:canceled),避免重复调用\r
- if self.AI_audio_queue_thread_id ==0:\r
- _thread.stack_size(24*1024)\r
- self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
- _thread.stack_size(8*1024)\r
- print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))\r
- else:\r
- print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
- elif jsload_data["event"] == "completed" :#会话结束(可能无 TTS,如 ASR 空)\r
- # 重要:服务端常在 tts.completed 后紧跟 completed。\r
- # 若本轮存在 TTS/待发 audio.played,则不能在此清零 LocalRespondingEnded_timerout/_need_audio_played_ack,\r
- # 否则会掐断「播完→audio.played→下一轮录音」链路,表现为最多 1~2 轮就停。\r
- if self._need_audio_played_ack == 1 or self._vc_tts_pipeline_busy() or self.VC_Play_flag == 1:\r
- pass\r
- else:\r
- # 无 TTS 的 completed:清状态并按需进入下一轮(见下方条件)\r
- self.LocalRespondingEnded_timerout = 0\r
- self._need_audio_played_ack = 0\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- # 连续对话:若本轮没有 TTS(或服务端直接 completed),需要立刻回到聆听状态\r
- # 否则会出现“有时 1 轮/2 轮后就停住”的现象(服务端 completed 但端侧未重新开录音上行)。\r
- if self.VC_mode == 0 and self.Close_flag == 0:\r
- # 仅当“本轮已结束一次语句(vad.silence 或 asr.completed)且从未进入 tts.started”\r
- # 才自动进入下一轮;避免把真正的“会话结束/异常结束”误当成下一轮入口。\r
- if (self._turn_has_tts_started == 0) and (self._turn_has_vad_silence == 1 or self._turn_has_asr_completed == 1):\r
- self._server_vad_silence = 0\r
- # 开麦由 FSM action list 统一触发(uplink_start:completed),这里不再重复调用\r
- pass\r
-\r
- elif jsload_data["event"] == "command" :#命令匹配\r
- ret = -1\r
- try:\r
- if self.commands_mode ==1:\r
- if len(jsload_data["payload"]["functions"])>0:\r
- \r
- cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]\r
- print("llm:cmd:{}".format(cmd))\r
- cmd = ujson.loads(cmd)\r
- ret = self.check_text_llm(cmd)\r
- elif self.commands_mode ==0:\r
-\r
- cmd = jsload_data["payload"]\r
- print("server:cmd:{}".format(cmd))\r
- ret = self.check_text_server(cmd)\r
- else:\r
- cmd = jsload_data["payload"]["functions"][0]["function"]["arguments"]\r
- print("llm:cmd:{}".format(cmd))\r
- cmd = ujson.loads(cmd)\r
- ret = self.check_text_llm(cmd)\r
- except Exception as e:\r
- usys.print_exception(e) \r
- if ret == -1:\r
- if self.AI_TTS_str =="":\r
- self.AI_TTS_str = "抱歉,没找到对应操作,可以换个说法试试" \r
- #EventMesh.publish("publish_AI_btnInfo",{"func":utime.sleep_ms,'data':(10)})\r
- self.request_uplink_stop("command")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- elif jsload_data["event"] == "failed":#接口返回错误\r
- if jsload_data["payload"]["error"] == "concurrency_limit_exceeded":\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.concurrency_limit_exceeded_tone}) \r
- \r
- #utime.sleep_ms(4000)\r
- \r
- else:\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":jsload_data["payload"]["message"]}) \r
- pass\r
- \r
- elif jsload_data["event"] == "audio.convert.completed":#音频转成完成没有出错 接口 \r
- # self.VC_state = VC_State_End\r
- print("未读留言初始数值:{}".format(self.pwk_cnt_voice))\r
- if self.pwk_notice_flag == 1:#只有在播放的时候才会去计数\r
- if self.pwk_cnt_voice > 0:\r
- self.pwk_cnt_voice -= 1#减1\r
- print("未读留言又-1,当前留言剩余数量为:{}".format(self.pwk_cnt_voice))\r
- else:\r
- print("未读列表为空")\r
- if self.pwk_cnt_voice == 0:#积累的留言已经全部播完\r
- self.pwk_notice_flag = 0\r
- self.wait_sort = 0\r
- self.wait_first_flag = 0#让下次未读播放提示恢复\r
- self.voicerupt_flag = 0 #标志位置0\r
- self.wait_voicerupt_flag = 0\r
- self.voice_notice = 1#开启回复\r
- if self.history_voice_flag == 1 and self.wait_voice_flag == 0:\r
- self.voice_notice = 0\r
- EventMesh.publish("set_storage_config",["is_noread_voice",0])\r
- \r
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
- self.voice_choose_flag = 0\r
- self.Close_flag = 1\r
- self.LocalRespondingEnded_timerout = 8 \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- if self.voice_notice == 1:#已收到留言,进行该留言回复逻辑处理\r
- tts_play_str = "如需回复,长按确认键开始录制,松开发送。十秒内无操作自动退出"\r
- EventMesh.publish("play_tts",[tts_play_str,1])\r
- self.voice_notice = 0#标志位清空\r
- self.voice_timer.stop()\r
- self.revoice_cnt = 0\r
- self.voice_flag = 1\r
- print("语音留言回复停止")\r
- self.revoice_timer.start(1000,1,self.revoice_judge)\r
- print("是否未读:{},是否播完:{}".format(EventMesh.publish("get_wait_voice_flag"),len(EventMesh.publish("get_play_new_voice_list"))))\r
- # if EventMesh.publish("get_wait_voice_flag") == 1 and len(EventMesh.publish("get_play_new_voice_list")) ==0:#还在未读并且列表为空\r
- # self.wait_first_flag = 0\r
- # print("未读留言播完,清空标志")\r
- # EventMesh.publish("set_wait_voice_flag",0)\r
- # EventMesh.publish("set_history_voice_flag",0)\r
- EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
- self.recv_voice = 0\r
- print("不开启定时器标志位置0")\r
- self.LocalRespondingEnded_timerout = 8\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- \r
- pass\r
- elif jsload_data["event"] == "audio.convert.failed": #音频转换失败了\r
- pass\r
- \r
- \r
- else:#下发音频数据\r
- #self.AI_ASR_str = ""#下发音频后清掉asr结果\r
- \r
- \r
- # PTT 打断后丢弃残留 TTS 音频帧,避免“断断续续继续播”\r
- try:\r
- if self._ptt_should_drop_inbound_tts():\r
- continue\r
- except Exception:\r
- pass\r
- if self.VC_stop_play_audio_flag == 0:\r
- print("新的AI对话不允许以前的播放数据:")\r
- break\r
-\r
- # if VC_AUDIO_FRAME_LOG:\r
- # print("下发音频数据1:{}".format(len(recv_data)))\r
-\r
- if self.play_data_save_flag == 1:\r
- if self.play_fd ==None:\r
- try:\r
- uos.remove('/usr/play_data.pcm')\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.play_fd = open('/usr/play_data.pcm','w')\r
- if self.play_fd !=None:\r
- self.play_fd.write(recv_data)\r
- # print("下发音频数据2:{}".format(len(recv_data)))\r
- #if self.VC_state >= VC_State_TTSStart:\r
- #self.put_record_data([recv_data,""])\r
- if self.is_ruppt_tts == 0:#被设备主动打断打断则不播放,知道收到服务器返回的cancel才修改标志位\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
- if self.play_pcm != None:\r
- try:\r
- #print("play_pcm.stream_write !!!")\r
- buf_len = self.play_pcm.stream_get_buf_len()\r
- count_tmp = 1000\r
- while buf_len < len(recv_data):\r
- utime.sleep_ms(1)\r
- count_tmp-=1\r
- if count_tmp<=0 or self.play_pcm == None:\r
- break\r
- buf_len = self.play_pcm.stream_get_buf_len()\r
- #print("buf已满,等待释放:buf_len:{},count_tmp:{}".format(buf_len,count_tmp))\r
- except Exception as e:\r
- usys.print_exception(e)\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.put_record_data,"data":[recv_data,""]}) \r
- else:\r
- print("被打断不播放") \r
- utime.sleep_ms(1)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- \r
- except Exception as e:\r
- error_str = str(e)\r
- #print("VC VC:e:{}".format(error_str))\r
- if error_str.find("[Errno 110] ETIMEDOUT")!= -1 or error_str.find("[Errno 115] ")!= -1:\r
- if VC_HEARTBEAT_LOG:\r
- print("VC VC:recv 正常报错:{}".format(error_str))\r
- # recv 超时仅表示本周期无帧,链路仍可能在等 LLM/TTS;清零避免误触发「接口无响应」\r
- self.close_timerout = 0\r
- pass\r
- else:\r
- usys.print_exception(e)\r
- #self.websocket_error()\r
- print("VC VC :读数据报错,退出")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"VC出错了,请重试"}) \r
- self.ud_error(["VC",self.VC_taskid])\r
- break\r
- self.AI_client_VC_isconnect = 0\r
- self.AI_client_VC_recv_id = 0 \r
- self.AI_client_VC_recv_lock = 0\r
- print("[VC] recv_thread exit")\r
- \r
- def voice_play_10S(self,event,msg):#按键开启留言播放等待10S定时器判断\r
- self.voice_timer.start(1000,1,self.voice_judge)\r
- #语音留言定时器\r
- def voice_judge(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.voice_judger_cb}) \r
- def voice_judger_cb(self):\r
- self.voice_cnt += 1\r
- # print("语音留言定时器cnt = {}".format(self.voice_cnt))\r
- if self.voicerupt_flag == 1:#按键被按下\r
- self.pwk_notice_flag = 1#表示进入收听未听留言\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- self.voice_cnt = 0\r
- # self.recv_voice = 0#进入留言定时器,留言清空\r
- print("进入留言定时器1,留言清空!!!!!")\r
- EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
- self.voice_timer.stop()\r
- if self.pwk_cnt_voice > 0:\r
- EventMesh.publish("get_new_voice_msg_S")\r
- if self.voice_cnt > 15:#10S后退出播放,5S是为了给语音提示留时间\r
- self.voice_cnt = 0\r
- self.recv_voice = 0#进入留言定时器,留言清空\r
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
- self.voice_choose_flag = 0\r
- print("进入留言定时器2,留言清空!!!!!")\r
- self.voice_timer.stop()\r
- #语音留言回复定时器\r
- def revoice_judge(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.revoice_judger_cb}) \r
- def revoice_judger_cb(self):\r
- self.revoice_cnt += 1\r
- # print("语音留言回复定时器cnt = {}".format(self.revoice_cnt))\r
- if self.revoice_cnt > 20:#10S后退出播放,10S是为了给语音提示留时间\r
- self.revoice_cnt = 0\r
- self.voice_flag=0\r
- EventMesh.publish("set_1M_nowplay_flag",0)\r
- print("语音留言回复停止")\r
- self.revoice_timer.stop()\r
- #音乐播放定时器\r
- def music_play(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.music_play_cb}) \r
- def music_play_cb(self):\r
- self.music_cnt += 1\r
- if self.music_cnt > 4:#4S后播放音乐,4S是为了给语音提示留时间\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- self.music_cnt = 0\r
- self.music_timer.stop() \r
- #历史留言播放定时器\r
- def history_play(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.history_play_cb}) \r
- def history_play_cb(self):\r
- self.his_cnt += 1\r
- if self.his_cnt > 4:#4S后播放,4S是为了给语音提示留时间\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- self.his_cnt = 0\r
- self.his_timer.stop() \r
- #未读留言是否播放定时器\r
- def wait_play(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_play_cb}) \r
- def wait_play_cb(self):\r
- self.wait_cnt += 1\r
- if self.wait_voicerupt_flag == 1:#按键被按下\r
- EventMesh.publish("set_is_listen_wait",1)#已听\r
- self.pwk_notice_flag = 1#表示进入收听未听留言\r
- self.AI_VC_start()\r
- self.wait_sort = 1\r
- self.AI_TTS_str = "第"+str(self.wait_sort)+"条" \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- # self.wait_cnt = 0\r
- # print("进入留言定时器1,留言清空!!!!!")\r
- # EventMesh.publish("update_play_new_voice_info")#更新已读状态\r
- self.wait_timer.stop()\r
- if self.wait_cnt > 10:#10S后退出播放,5S是为了给语音提示留时间\r
- self.wait_cnt = 0\r
- self.recv_voice = 0#进入留言定时器,留言清空\r
- EventMesh.publish("set_history_voice_flag",0)\r
- EventMesh.publish("set_wait_voice_flag",0)\r
- EventMesh.publish("set_open_device",0)#开机不听\r
- self.wait_first_flag = 0\r
- if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭\r
- self.voice_choose_flag = 0\r
- print("进入留言定时器2,留言清空!!!!!")\r
- self.wait_timer.stop()\r
- #未读留言一条条播放定时器\r
- def wait_single_play(self, *args, **kwargs):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_single_play_cb}) \r
- def wait_single_play_cb(self):\r
- self.wait_cnt += 1\r
- if self.wait_cnt > 2:#2S后播放,2S是为了给语音提示留时间\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- self.wait_cnt = 0\r
- self.wait_timer.stop() \r
- def set_voicerupt_flag(self,event,msg):#按键控制当前留言播放\r
- self.voicerupt_flag = msg\r
- def set_wait_voicerupt_flag(self,event,msg):#按键控制未读留言播放\r
- self.wait_voicerupt_flag = msg \r
- def get_voicerupt_flag(self,event,msg):#获取按键控制留言播放状态\r
- return self.voicerupt_flag\r
- def get_recv_voice(self,event,msg):\r
- return self.recv_voice\r
- def set_recv_voice(self,event,msg):\r
- return self.recv_voice\r
- def put_record_data(self,data):\r
- try:\r
- if self._ptt_should_drop_inbound_tts():\r
- return\r
- except Exception:\r
- pass\r
- #print("下发音频数据1:{}".format(len(data[0])))\r
- ret=self.AI_audio_queue.put(data)\r
- try:\r
- if self.tts_in_data_len == 0 and data and len(data[0]) > 0:\r
- try:\r
- sid = getattr(self, "_cur_sid", "")\r
- did = getattr(self, "_cur_dialog_id", "")\r
- except Exception:\r
- sid, did = "", ""\r
- print("[TTS] first_chunk len={} play_pcm={} VC_Play_flag={} sid={} did={}".format(\r
- len(data[0]),\r
- 1 if self.play_pcm != None else 0,\r
- getattr(self, "VC_Play_flag", 0),\r
- sid,\r
- did,\r
- ))\r
- except Exception:\r
- pass\r
- self.tts_in_data_len += len(data[0])\r
- #print("下发音频数据2:{},{}".format(len(data[0]),self.tts_in_data_len))\r
- #msg[0]是tts播放字符串,msg[1]是是否关闭当前tts\r
- def play_tts(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_tts_cb,"data":msg}) \r
- \r
- def play_tts_cb(self,data):\r
- try:\r
- tts_str = data[0]\r
- is_close_VC = data[1]\r
- if tts_str != "":\r
- self.AI_TTS_str = tts_str\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- try:\r
- if is_close_VC:\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- else:\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- except Exception:\r
- pass\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_tts_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e)\r
- pass\r
- def check_text_server(self,cmd):\r
- return -1\r
-\r
- def _apply_vc_text_intent(self, text, cmd, llm_style_tts):\r
- """表驱动文本意图(vc_text_intent)。未命中返回 None;命中返回 0/1/-1。"""\r
- if vc_text_intent is None:\r
- return None\r
- try:\r
- try:\r
- _btv = bool("vc_config" in globals() and vc_config and vc_config.get_bool("BT_VOICE_FEATURES_ENABLED", False))\r
- except Exception:\r
- _btv = False\r
- r = vc_text_intent.match_vc_command(text, {"bt_voice": _btv})\r
- except Exception as e:\r
- usys.print_exception(e)\r
- return None\r
- if not r:\r
- return None\r
- return self._apply_vc_text_intent_matched(r, cmd, llm_style_tts)\r
-\r
- def _apply_vc_text_intent_matched(self, r, cmd, llm_style_tts):\r
- if r.get("needs_payload") and cmd is None:\r
- return None\r
- it = r["intent"]\r
- if r.get("tts"):\r
- self.AI_TTS_str = r["tts"]\r
- if r.get("close_flag"):\r
- self.Close_flag = 1\r
- if it == "goodbye":\r
- return 0\r
- if it == "shutdown":\r
- if llm_style_tts:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
- EventMesh.publish("Power_off")\r
- return 1\r
- try:\r
- close_only = ("vc_config" in globals() and vc_config and vc_config.get_bool("POWEROFF_CMD_CLOSE_ONLY", True))\r
- except Exception:\r
- close_only = True\r
- if close_only:\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
- return 1\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})\r
- EventMesh.publish("Power_off")\r
- return 1\r
- if it == "toggle_dialog_mode":\r
- if self.VC_mode == 0:\r
- self.VC_mode = 1\r
- if llm_style_tts:\r
- self.AI_TTS_str = "了解,已切换到单对话模式"\r
- else:\r
- try:\r
- self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_PTT_TTS", "已切换到单对话模式")\r
- except Exception:\r
- self.AI_TTS_str = "已切换到单对话模式"\r
- else:\r
- self.VC_mode = 0\r
- if llm_style_tts:\r
- self.AI_TTS_str = "了解,已切换到长对话模式"\r
- else:\r
- try:\r
- self.AI_TTS_str = vc_config.get_attr("CMD_TOGGLE_MODE_TO_LONG_TTS", "已切换到长对话模式")\r
- except Exception:\r
- self.AI_TTS_str = "已切换到长对话模式"\r
- EventMesh.publish("set_storage_config", ["VC_mode", self.VC_mode])\r
- return 0\r
- if it == "volume_set":\r
- try:\r
- pj = ujson.loads(cmd["command"]["params_json"])\r
- volume = pj["n"]\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try:\r
- self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
- except Exception:\r
- self.AI_TTS_str = ""\r
- return -1\r
- if volume > 100 or volume < 0:\r
- self.AI_TTS_str = "音量调整超出范围了,只能0-100哦"\r
- else:\r
- self.VC_TTS_volume = volume\r
- self.aud.setVolume(self.VC_TTS_volume // 10)\r
- EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
- self.AI_TTS_str = "了解,音量已经调整,当前音量是{}".format(self.VC_TTS_volume)\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
- return 0\r
- if it == "volume_up":\r
- if llm_style_tts:\r
- try:\r
- print("self.VC_TTS_volume add:{}".format(self.VC_TTS_volume))\r
- except Exception:\r
- pass\r
- self.VC_TTS_volume += 10\r
- if self.VC_TTS_volume > 100:\r
- self.AI_TTS_str = "音量已经是最大啦" if llm_style_tts else "音量已最大"\r
- self.VC_TTS_volume = 100\r
- else:\r
- self.aud.setVolume(self.VC_TTS_volume // 10)\r
- EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
- self.AI_TTS_str = (\r
- "了解,音量已经调大了,当前音量是{}".format(self.VC_TTS_volume)\r
- if llm_style_tts\r
- else "配置成功,当前音量 {}".format(self.VC_TTS_volume)\r
- )\r
- return 0\r
- if it == "volume_down":\r
- if llm_style_tts:\r
- try:\r
- print("self.VC_TTS_volume end:{}".format(self.VC_TTS_volume))\r
- except Exception:\r
- pass\r
- self.VC_TTS_volume -= 10\r
- if self.VC_TTS_volume < 0:\r
- self.VC_TTS_volume = 0\r
- self.AI_TTS_str = "音量已经是最小啦" if llm_style_tts else "音量已最小"\r
- else:\r
- self.aud.setVolume(self.VC_TTS_volume // 10)\r
- EventMesh.publish("set_storage_config", ["VC_TTS_volume", self.VC_TTS_volume])\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle", self.VC_TTS_volume)\r
- self.AI_TTS_str = (\r
- "好嘞,音量已经调小了,当前音量是{}".format(self.VC_TTS_volume)\r
- if llm_style_tts\r
- else "配置成功,当前音量 {}".format(self.VC_TTS_volume)\r
- )\r
- return 0\r
- if it in ("speech_faster", "speech_rate_up"):\r
- self.speech_rate += 10\r
- if self.speech_rate > 50:\r
- self.AI_TTS_str = "语速已经是最快了"\r
- self.speech_rate = 50\r
- else:\r
- EventMesh.publish("set_storage_config", ["speech_rate", self.speech_rate])\r
- self.AI_TTS_str = "明白,语速已经调快了,当前语速是{}".format(self.speech_rate)\r
- return 0\r
- if it in ("speech_slower", "speech_rate_down"):\r
- self.speech_rate -= 10\r
- if self.speech_rate < -50:\r
- self.speech_rate = -50\r
- self.AI_TTS_str = "语速已经是最慢了"\r
- else:\r
- EventMesh.publish("set_storage_config", ["speech_rate", self.speech_rate])\r
- self.AI_TTS_str = "明白,语速已经调慢了,当前语速是{}".format(self.speech_rate)\r
- return 0\r
- if it == "vad_faster":\r
- self.vad_silence -= 200\r
- if self.vad_silence < 100:\r
- self.AI_TTS_str = "已经是最快了"\r
- self.vad_silence = 100\r
- else:\r
- self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)\r
- EventMesh.publish("set_storage_config", ["vad_silence", self.vad_silence])\r
- return 0\r
- if it == "vad_slower":\r
- self.vad_silence += 200\r
- if self.vad_silence > 2000:\r
- self.vad_silence = 2000\r
- self.AI_TTS_str = "已经是最慢了"\r
- else:\r
- self.AI_TTS_str = "配置成功,当前语音断句速度是{}毫秒".format(self.vad_silence)\r
- EventMesh.publish("set_storage_config", ["vad_silence", self.vad_silence])\r
- return 0\r
- if it == "query_signal":\r
- signal = net.csqQueryPoll()\r
- self.AI_TTS_str = "当前信号{}".format(signal)\r
- return 0\r
- if it == "query_battery":\r
- battery = EventMesh.publish("get_battery")\r
- try:\r
- print("电量查询:{}".format(battery))\r
- except Exception:\r
- pass\r
- self.AI_TTS_str = "电量剩余百分之{}".format(battery)\r
- return 0\r
- if it == "query_voltage":\r
- Vbatt = Power.getVbatt()\r
- self.AI_TTS_str = "当前电池电压{}".format(Vbatt)\r
- return 0\r
- if it == "query_iccid":\r
- iccid = sim.getIccid()\r
- if iccid != -1 and len(iccid) > 5:\r
- try:\r
- print("iccid{}".format(iccid))\r
- except Exception:\r
- pass\r
- self.AI_TTS_str = "当前卡号为:{}".format(iccid)\r
- else:\r
- self.AI_TTS_str = "卡号查询失败"\r
- return 0\r
- if it == "query_imei":\r
- imei = modem.getDevImei()\r
- if imei != -1 and len(imei) > 5:\r
- try:\r
- print("IMEI:{}".format(imei))\r
- except Exception:\r
- pass\r
- self.AI_TTS_str = "当前设备号为:{}".format(imei)\r
- else:\r
- self.AI_TTS_str = "设备号查询失败"\r
- return 0\r
- if it == "query_version":\r
- bt_versions = EventMesh.publish("get_bt_versions")\r
- if bt_versions is None or bt_versions == "":\r
- self.AI_TTS_str = "当前版本号:" + version_util.get_tts_version()\r
- else:\r
- self.AI_TTS_str = "当前版本号:" + version_util.get_tts_version(bt_versions)\r
- return 0\r
- if it == "query_server":\r
- self.AI_TTS_str = "服务器地址" + self.server_ip\r
- return 0\r
- if it == "ota_check":\r
- if self.ai_OTA.min_sys_ota() == 1:\r
- self.AI_TTS_str = "有新版本,准备开始升级,升级过程会多次重启设备,请保证流量充足及电量充足"\r
- else:\r
- self.AI_TTS_str = "当前已是最新版本"\r
- return 0\r
- if it == "factory_mode":\r
- self.factory_mode()\r
- return 1\r
- if it == "alarm_set":\r
- try:\r
- alarm = ujson.loads(cmd["command"]["params_json"])\r
- try:\r
- print("params_json:{}".format(cmd["command"]["params_json"]))\r
- except Exception:\r
- pass\r
- add_alarm_tmp = EventMesh.publish("AI_add_alarm", alarm["alarm"])\r
- if add_alarm_tmp[0] > 0:\r
- self.AI_TTS_str = add_alarm_tmp[1]\r
- return 1\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try:\r
- self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
- except Exception:\r
- self.AI_TTS_str = ""\r
- return -1\r
- if it == "alarm_clear":\r
- try:\r
- self.AI_TTS_str = EventMesh.publish("AI_delete_alarm")\r
- except Exception as e:\r
- usys.print_exception(e)\r
- return 0\r
- if it == "alarm_list":\r
- EventMesh.publish("read_alarm_list")\r
- return 0\r
- if it == "voice_message_mode":\r
- self.voice_flag = 1\r
- self.AI_TTS_str = "已进入语音留言模式,长按按钮开始录音,松开按钮结束"\r
- self.Close_flag = 1\r
- self.stop_voice_timer.stop()\r
- self.stop_voice_timer.start(30 * 1000, 0, self.exit_voice_message_timer)\r
- try:\r
- print("语音留言!!!!!!!!!!")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "bt_ble_adv_on":\r
- self.AI_TTS_str = "正在开蓝牙广播"\r
- EventMesh.publish("AI_send_bt_ble_switch_toble", 1)\r
- try:\r
- print("正在开蓝牙广播")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "bt_ble_adv_off":\r
- self.AI_TTS_str = "正在关蓝牙广播"\r
- EventMesh.publish("AI_send_bt_ble_switch_toble", 2)\r
- try:\r
- print("正在关蓝牙广播")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "bt_classic_on":\r
- self.AI_TTS_str = "正在开经典蓝牙"\r
- EventMesh.publish("AI_send_bt_ble_switch_toble", 3)\r
- try:\r
- print("正在开经典蓝牙")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "bt_classic_off":\r
- self.AI_TTS_str = "正在关经典蓝牙"\r
- EventMesh.publish("AI_send_bt_ble_switch_toble", 4)\r
- try:\r
- print("正在关经典蓝牙")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "unbind_confirm":\r
- self.AI_TTS_str = "设备解绑中...."\r
- self.Close_flag = 1\r
- EventMesh.publish("send_unbind_dev_to_iot")\r
- try:\r
- print("解绑设备!!!!!!!!!!!!!!")\r
- except Exception:\r
- pass\r
- return 0\r
- if it == "clear_earphone":\r
- self.AI_TTS_str = "正在删除"\r
- EventMesh.publish("clear_earphone_record")\r
- try:\r
- print("正在删除")\r
- except Exception:\r
- pass\r
- return 0\r
- return None\r
-\r
- def check_text_llm(self,cmd):\r
- try:\r
- desc = cmd["command"]["chinese_description"]\r
- if desc.find("切换音色") != -1:\r
- text = cmd["command"]["params_json"]\r
- try:\r
- for key in self.VC_TTS_voice_type:\r
- if text.find(key) != -1:\r
- self.VC_TTS_voice_key = key\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- except Exception as e:\r
- usys.print_exception(e)\r
- if text.find("男") != -1:\r
- if "男" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "男"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("女") != -1:\r
- if "女" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "女"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("同声") != -1 or text.find("童") != -1:\r
- if "童" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "童"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("粤") != -1 or text.find("越野") != -1 or text.find("广东话") != -1:\r
- if "粤" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "粤"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("四川") != -1:\r
- if "四川" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "四川"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("个性") != -1:\r
- if "个性" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "个性"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- if text.find("英语") != -1:\r
- if "英语" in self.VC_TTS_voice_type:\r
- self.VC_TTS_voice_key = "英语"\r
- EventMesh.publish("set_storage_config", ["VC_TTS_voice_key", self.VC_TTS_voice_key])\r
- self.AI_TTS_str = "了解,声音已切换"\r
- return 0\r
- self.AI_TTS_str = "很抱歉,没找到对应声音"\r
- return 0\r
- ret = self._apply_vc_text_intent(desc, cmd, True)\r
- if ret is not None:\r
- return ret\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try:\r
- self.AI_TTS_str = cmd["command"]["fail_prompt"]\r
- except Exception:\r
- self.AI_TTS_str = ""\r
- return -1\r
-\r
- def check_text(self, text):\r
- print("check_text:len:{}".format(len(text)))\r
- if len(text) > 60:\r
- return -1\r
- try:\r
- if not isinstance(text, str):\r
- return -1\r
- ret = self._apply_vc_text_intent(text, None, False)\r
- if ret is not None:\r
- return ret\r
- except Exception as e:\r
- usys.print_exception(e)\r
- return -1\r
-\r
- #获取trace_id\r
- def get_trace_id(self):\r
- t = utime.mktime(utime.localtime())\r
- t = t*1000+urandom.randint(0, 1000)\r
- md5_data = "{}".format(t)#md5签名\r
- sign_data = ""\r
- hash_obj = uhashlib.md5(md5_data)\r
- res = hash_obj.digest()\r
-\r
- # 转成十六进制表示\r
- sign_data = ubinascii.hexlify(res)\r
- sign_data = sign_data.decode('ascii')\r
- return sign_data\r
- \r
- def get_session_id(self,data = "session_id"):\r
- t = utime.mktime(utime.localtime())\r
- t = t*1000+urandom.randint(0, 1000)\r
- md5_data = "{}{}".format(data,t)#md5签名\r
- sign_data = ""\r
- hash_obj = uhashlib.md5(md5_data)\r
- res = hash_obj.digest()\r
-\r
- # 转成十六进制表示\r
- sign_data = ubinascii.hexlify(res)\r
- sign_data = sign_data.decode('ascii')\r
- return sign_data\r
- \r
- def get_new_message_id(self):\r
- message_id = self.get_trace_id()+"aaa"+self.get_trace_id()\r
- self.VC_message_id=message_id[:32]\r
- return self.VC_message_id \r
- def get_new_task_id(self):\r
- task_id = self.get_trace_id()+"aaa"+self.get_trace_id()\r
- self.VC_task_id=task_id[:32]\r
- return self.VC_task_id\r
- #开始播放声音\r
- def VC_Play_Start(self):\r
- try:\r
- if self._ptt_should_skip_remote_playback():\r
- if self._vc_led_state_debug_enabled():\r
- print("[VC] VC_Play_Start skip (ptt_interrupt)")\r
- return\r
- except Exception:\r
- pass\r
- self.tts_in_data_len = 0\r
- self.tts_out_data_len = 0\r
- self._tts_first_write_ms = 0\r
- if self.VC_Play_flag == 0:\r
- try:\r
- _ptt_fast_open = False\r
- try:\r
- if getattr(self, "VC_mode", -1) == 1:\r
- _ptt_fast_open = True\r
- except Exception:\r
- pass\r
- # 本地提示音/文件播放可能占用同一音频通道,导致 TTS 流播放“写入成功但无声”;\r
- # 在开启 TTS PCM 前先停掉本地播放,避免资源冲突。\r
- try:\r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- except Exception:\r
- pass\r
- # 资源互斥:播放前确保录音上行已停止(无 AEC 场景)\r
- try:\r
- if self.record_data_save_flag == 0 and self.VC_record_state_flag != 0:\r
- self._uplink_request_stop("VC_Play_Start")\r
- utime.sleep_ms(30)\r
- except Exception:\r
- pass\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
- if self.play_pcm == None:\r
- # 刚停采集时延后打开播音 PCM,且失败时重试,降低「pcm open write only fail」\r
- last_err = None\r
- _open_retries = 1 if _ptt_fast_open else 3\r
- for i in range(_open_retries):\r
- try:\r
- if not _ptt_fast_open:\r
- utime.sleep_ms(80 + i * 60)\r
- self.play_pcm = audio.Audio.PCM(0, 1, self.VC_play_samplerate, 1, 1, 2)\r
- ret = self.play_pcm.stream_write_start()\r
- print("self.record_pcm.stream_write_start:ret:{}".format(ret))\r
- last_err = None\r
- break\r
- except Exception as e:\r
- last_err = e\r
- try:\r
- if self.play_pcm != None:\r
- self.play_pcm.close()\r
- except Exception:\r
- pass\r
- self.play_pcm = None\r
- if last_err:\r
- raise last_err\r
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
- pass\r
- self.VC_Play_flag = 1\r
- try:\r
- if not self._ptt_should_skip_remote_playback():\r
- self._vc_publish_led("playing", "VC_Play_Start/tts")\r
- except Exception:\r
- pass\r
- except Exception as e:\r
- self.VC_Play_End()\r
- usys.print_exception(e)\r
- #暂停/继续声音播放 Pause_flag = 1暂停\r
- def VC_Play_Pause(self,Pause_flag):\r
- try:\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
- if self.play_pcm != None: \r
- self.play_pcm.stream_pause(Pause_flag)\r
- except Exception as e:\r
- self.VC_Play_End()\r
- usys.print_exception(e)\r
- #停止播放声音\r
- def VC_Play_End(self):\r
- self.VC_Play_flag = 0\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
- if self.VC_play_samplerate == 8000:\r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- elif self.VC_play_samplerate == 16000:\r
- if self.play_pcm != None:\r
- print("Close:play_pcm.close()!!!!!!!!!!!!!!!!!!")\r
- try:\r
- self.play_pcm.stream_stop()\r
- self.play_pcm.close()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.play_pcm=None\r
- \r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
- self.aud.stopPlayStream()\r
- self.aud.stopAll()\r
- if self.AI_audio_queue.empty()!=True:\r
- self.AI_audio_queue.clear()\r
-\r
- # TTS 播完:按真实 FSM/录音态恢复灯;本地短音不走此路径\r
- try:\r
- if getattr(self, "_vc_play_end_suppress_idle_led", 0):\r
- if self._vc_led_state_debug_enabled():\r
- print("[LED] VC_Play_End skip led (suppress)")\r
- elif getattr(self, "VC_record_state_flag", 0) != 0:\r
- self._vc_publish_led("listening", "VC_Play_End/recording")\r
- else:\r
- self._vc_led_sync_from_fsm()\r
- except Exception:\r
- pass\r
- # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出\r
- try:\r
- if "vc_config" in globals() and vc_config and (\r
- vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False) or vc_config.get_bool("HW_STATUS_DEBUG", False)\r
- ):\r
- try:\r
- led = EventMesh.publish("get_led_debug_state")\r
- except Exception:\r
- led = None\r
- print("[VC] VC_Play_End led={}".format(led))\r
- except Exception:\r
- pass\r
- \r
-\r
- def _vc_tts_bytes_per_sec(self):\r
- try:\r
- return int(self.VC_play_samplerate) * 2\r
- except Exception:\r
- return 32000\r
-\r
- def _vc_try_play_end_tick(self):\r
- """tts.completed 后判定本地 PCM 是否真正播完,避免过早 VC_Play_End 截断 TTS。"""\r
- try:\r
- if not getattr(self.fsm, "await_played", False):\r
- return False\r
- in_len = getattr(self, "tts_in_data_len", 0)\r
- out_len = getattr(self, "tts_out_data_len", 0)\r
- try:\r
- q_empty = self.AI_audio_queue.empty()\r
- except Exception:\r
- q_empty = True\r
- tts_done_ms = getattr(self.fsm, "last_tts_completed_ms", 0)\r
- if not (in_len > 0 and out_len >= in_len and q_empty and tts_done_ms):\r
- return False\r
-\r
- is_pcm_empty = (self.play_pcm is None) or (\r
- self.play_pcm is not None and self.play_pcm.stream_is_empty() == 1\r
- )\r
- pcm_buf = 0\r
- if self.play_pcm is not None:\r
- try:\r
- pcm_buf = self.play_pcm.stream_get_buf_len()\r
- except Exception:\r
- pcm_buf = -1\r
-\r
- now = utime.ticks_ms()\r
- bps = self._vc_tts_bytes_per_sec()\r
- total_ms = int(out_len * 1000 // bps) if bps > 0 else 0\r
- fw = getattr(self, "_tts_first_write_ms", 0) or tts_done_ms\r
- try:\r
- elapsed_at_done = utime.ticks_diff(tts_done_ms, fw)\r
- except Exception:\r
- elapsed_at_done = 0\r
- if elapsed_at_done < 0:\r
- elapsed_at_done = 0\r
- remain_ms = max(400, total_ms - elapsed_at_done) if total_ms > 0 else 400\r
- try:\r
- since_done = utime.ticks_diff(now, tts_done_ms)\r
- except Exception:\r
- since_done = 0\r
- tail_ok = since_done >= remain_ms\r
- pcm_ok = is_pcm_empty and (pcm_buf == 0 or pcm_buf == -1)\r
- too_long = since_done > max(remain_ms + 2000, 12000)\r
-\r
- ready = (pcm_ok and tail_ok) or too_long\r
- if not ready:\r
- try:\r
- if not getattr(self, "_tts_play_check_log_ms", 0) or utime.ticks_diff(\r
- now, self._tts_play_check_log_ms\r
- ) >= 800:\r
- self._tts_play_check_log_ms = now\r
- print(\r
- "[TTS] play_end_wait since_done={}ms remain~{}ms total~{}ms "\r
- "in={} out={} q_empty={} pcm_buf={} pcm_empty={}".format(\r
- since_done, remain_ms, total_ms, in_len, out_len,\r
- 1 if q_empty else 0, pcm_buf, 1 if is_pcm_empty else 0,\r
- )\r
- )\r
- except Exception:\r
- pass\r
- return False\r
-\r
- try:\r
- print(\r
- "[TTS] play_end_fire since_done={}ms remain~{}ms total~{}ms "\r
- "in={} out={} pcm_buf={} too_long={}".format(\r
- since_done, remain_ms, total_ms, in_len, out_len, pcm_buf, 1 if too_long else 0,\r
- )\r
- )\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})\r
- self._fsm_apply_actions(self.fsm.on_local_play_end())\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- self._tts_first_write_ms = 0\r
- return True\r
- except Exception as e:\r
- usys.print_exception(e)\r
- return False\r
-\r
- def _1S_timer_cb(self,*argc):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self._1S_timer_cb_}) \r
-\r
- def _fsm_timeout_table_tick(self, now_ms):\r
- # 兜底统一入口:按“状态机状态 + 超时阈值 + 节流”驱动,避免兜底抢跑/多入口打架。\r
- # 只在对应 turn/session 状态触发动作。\r
- try:\r
- turn = getattr(self.fsm, "turn", VCTurnState.Idle)\r
- except Exception:\r
- turn = VCTurnState.Idle\r
-\r
- # 1) synthesis→tts.started 超时:只在 Thinking 期允许清 pending 并重启会话\r
- # 根因:服务端未返回 tts.started 时,继续“回到聆听/开麦”会把本轮状态机拉扯得更乱;\r
- # 这里直接重启会话,让协议重新进入 started→下一轮流程。\r
- try:\r
- if turn == VCTurnState.Thinking and self._tts_pending == 1 and self._tts_pending_ms:\r
- try:\r
- if self._fb_last_tts_pending_timeout_ms:\r
- tts_gap_ok = utime.ticks_diff(now_ms, self._fb_last_tts_pending_timeout_ms) > 15000\r
- else:\r
- tts_gap_ok = True\r
- except Exception:\r
- tts_gap_ok = True\r
- if utime.ticks_diff(now_ms, self._tts_pending_ms) > 8000 and tts_gap_ok:\r
- self._fb_last_tts_pending_timeout_ms = now_ms\r
- print("[VC][tts_pending] timeout, clear pending and restart session")\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- if self.VC_mode == 0 and self.Close_flag == 0:\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})\r
- except Exception:\r
- pass\r
-\r
- # 2) thinking 超时:只在「llm.completed 已收到但 tts.started 长时间未到」时才重启会话\r
- # 注意:不能以 llm.created 的时间戳作为超时基准,否则会在 LLM 正常推理期间误判超时(notts 场景)。\r
- try:\r
- if self.VC_mode == 0 and self.Close_flag == 0:\r
- llm_completed_ms = getattr(self, "_llm_completed_ms", 0)\r
- # 只有在 llm.completed 之后才允许超时重启\r
- if turn == VCTurnState.Thinking and llm_completed_ms and self._turn_has_tts_started == 0:\r
- age_ms = utime.ticks_diff(now_ms, llm_completed_ms)\r
- restart_threshold_ms = 20000\r
- # _fb_last_thinking_restart_ms==0 时 ticks_diff(now,0) 非预期(间隔被误判极大),\r
- # 会在 llm.completed 后首批 tick 就触发 AI_VC_start → notts(无 tts.started);首次仅信任 age 门限\r
- try:\r
- if self._fb_last_thinking_restart_ms:\r
- gap_ok = utime.ticks_diff(now_ms, self._fb_last_thinking_restart_ms) > 15000\r
- else:\r
- gap_ok = True\r
- except Exception:\r
- gap_ok = True\r
- if (age_ms > restart_threshold_ms and gap_ok and\r
- self._tts_pending == 0 and self._need_audio_played_ack == 0 and self.VC_Play_flag == 0):\r
- self._fb_last_thinking_restart_ms = now_ms\r
- self._llm_completed_ms = 0\r
- self._server_vad_silence = 0\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- try:\r
- print("[VC][thinking_timeout] AI_VC_start age_ms={} thr={}".format(\r
- age_ms, restart_threshold_ms))\r
- except Exception:\r
- pass\r
- self._vc_led_alert_rb_flash()\r
- # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})\r
- except Exception:\r
- pass\r
-\r
- def _1S_timer_cb_(self):\r
- try:\r
- self._vc_tick_serial_cancel_timeout()\r
- except Exception:\r
- pass\r
-\r
- EventMesh.publish("system_sleep_rest")#防止黑屏\r
- # --- Level 1:仅长对话(VC_mode==0),无业务下行 idle_sec 后结束会话(需再次按键进入)\r
- # close_timerout:保活清除策略见 _vc_rx_resets_long_idle_timer(排除文本 ping/pong、heartbeat 等)\r
- # 与浅休眠/灭屏解耦;单对话不走本计时\r
- idle_sec = 20\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- idle_sec = int(vc_config.get_int("VC_IDLE_AUTO_CLOSE_SEC", 20))\r
- except Exception:\r
- idle_sec = 20\r
- if idle_sec < 5:\r
- idle_sec = 5\r
- if self.VC_mode == 0 and self.VC_satrt_flag == 1 and getattr(self, "AI_client_VC_isconnect", 0) == 1:\r
- self.close_timerout += 1\r
- else:\r
- self.close_timerout = 0\r
- if self.VC_mode == 0 and idle_sec > 0 and self.close_timerout > idle_sec:\r
- try:\r
- print("[VC][idle_L1] 长对话无业务下行超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={} {}".format(\r
- self.close_timerout, idle_sec, self.VC_mode,\r
- getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1),\r
- self._vc_fsm_log_snip()))\r
- except Exception:\r
- pass\r
- if self._vc_long_idle_active_speech_or_later():\r
- try:\r
- print("[VC][idle_L1] 处理=websocket_error(对话中无服务端响应)")\r
- except Exception:\r
- pass\r
- print("[VC][long_idle] branch=speech_no_rsp -> websocket_error + ud_error")\r
- print("接口无响应,超时退出")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"接口超时了。请重试"}) \r
- self.ud_error(["VC",self.VC_taskid])\r
- else:\r
- try:\r
- print("[VC][idle_L1] 处理=Close(结束对话,需再次按键进入)")\r
- except Exception:\r
- pass\r
- print("[VC][long_idle] branch=idle_timeout -> Close")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- #EventMesh.publish("sleep_BT")#休眠蓝牙\r
-\r
- # --- Level 2:长对话+单对话,业务 idle 累计 VC_IDLE_DEEP_SLEEP_SEC → 断开(若仍连)+ 深睡事件\r
- deep_sec = 60\r
- deep_en = 1\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- deep_sec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))\r
- deep_en = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))\r
- except Exception:\r
- deep_sec, deep_en = 60, 1\r
- active_deep = bool(deep_en) and deep_sec > 0\r
- if active_deep and deep_sec < 10:\r
- deep_sec = 10\r
- if not active_deep:\r
- self._idle_deep_sec = 0\r
- elif not self._vc_is_business_idle():\r
- self._idle_deep_sec = 0\r
- try:\r
- self._vc_idle_deep_armed_suppress = 0\r
- except Exception:\r
- pass\r
- elif getattr(self, "_vc_idle_deep_armed_suppress", 0):\r
- pass\r
- else:\r
- self._idle_deep_sec += 1\r
- if active_deep and self._idle_deep_sec > deep_sec:\r
- try:\r
- print("[VC][idle_L2] 业务idle累计超时: elapsed={}s thr={}s VC_mode={} VC_satrt={} ws_conn={}".format(\r
- self._idle_deep_sec, deep_sec, self.VC_mode,\r
- getattr(self, "VC_satrt_flag", -1), getattr(self, "AI_client_VC_isconnect", -1)))\r
- except Exception:\r
- pass\r
- try:\r
- self._vc_idle_deep_armed_suppress = 1\r
- except Exception:\r
- pass\r
- self._idle_deep_sec = 0\r
- EventMesh.publish("publish_AI_btnInfo", {"func": self._vc_idle_deep_sleep_impl})\r
-\r
- # 统一兜底表(按状态机状态触发)\r
- try:\r
- self._fsm_timeout_table_tick(utime.ticks_ms())\r
- except Exception:\r
- pass\r
-\r
- # (不做额外兜底)动态切换依赖 FSM 状态正确清理后,由 server.tts.completed→local.play_end 驱动自动开麦\r
-\r
- # print("get_audio_state !!!!!{} {} {}\r\n".format(self.play_end,EventMesh.publish("get_audio_state"),self.play_flag))\r
- # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====\r
- try:\r
- self._vc_try_play_end_tick()\r
- except Exception:\r
- pass\r
- if self.online_kws == 1:\r
- if self.VC_satrt_kws_flag == 1 and self.VC_satrt_flag == 0:#要打开录音上传数据实现kws\r
- pass\r
-\r
- # thinking/llm→tts 等兜底已在 _fsm_timeout_table_tick 内统一处理\r
- # VC WebSocket 文本 ping:间隔由 default_config 的 VC_WS_PING_INTERVAL_SEC 控制(秒),<=0 表示关闭 ping\r
- try:\r
- _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))\r
- except Exception:\r
- _ping_iv = 4\r
- if _ping_iv > 0:\r
- self.VC_pingping_timerout += 1\r
- if self.VC_pingping_timerout >= _ping_iv:\r
- self.VC_pingping_timerout = 0\r
- self.AI_client_VC_ping()\r
- # self.asr_pingping_timerout += 1\r
- # if self.asr_pingping_timerout>3:\r
- # self.asr_pingping_timerout=0\r
- # self.AI_client_asr_ping() \r
- # 历史遗留:VC_Thinking_timerout 的重启通道会在 llm.created 后抢跑触发 AI_VC_start,\r
- # 直接打断本轮 LLM→TTS(notts 日志:llm.created 后立刻 AI_VC_start,导致无 tts.started)。\r
- # 现在统一由 _fsm_timeout_table_tick(Thinking 超时) 负责重启会话,因此禁用该旧通道。\r
- if self.VC_Thinking_timerout > 0 and self.VC_Thinking_timerout < 100:\r
- self.VC_Thinking_timerout = 100\r
- # def AI_kws_start(self): \r
- # print("AI_kws_start")\r
- # EventMesh.publish("wakeup_BT")\r
- # EventMesh.publish("system_sleep_rest")#防止黑屏\r
- # url="ws://118.145.158.207:22080/wakeup/ws"\r
- # self.AI_client_kws = uwebsocket.Client.connect(url, debug=True)\r
- # self.AI_client_VC_recv_id = _thread.start_new_thread(self.AI_client_VC_recv, (self.AI_client_VC,))\r
- # self.VC_Speech_Start()#开启线程获取录音数据\r
- #VC开始\r
- def AI_VC_start(self):\r
- print("AI_VC_start1")\r
- \r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- self.VC_satrt_flag = 1\r
- self.VC_stop_play_audio_flag = 1\r
- self.VC_satrt_kws_flag = 1\r
- if self.VC_Thinking_timerout < 100:\r
- self.VC_Thinking_timerout = 0\r
- # 不在这里提前切灯态:播放/聆听灯只在“播放/录音真正开始/停止”处更新\r
- EventMesh.publish("wakeup_BT")\r
- #self.VC_Play_End()#关闭音频播放\r
- print("AI_VC_start2")\r
- #self.aud.aud_tone_play(16,200)#播放按键音\r
- self.LocalRespondingEnded_timerout = 0\r
- self.close_timerout = 0\r
- self._idle_deep_sec = 0\r
- self._vc_idle_deep_armed_suppress = 0\r
- \r
- #\r
- if self.AI_client_VC_isconnect == 0:#没连接的时候请求开始\r
- self.websocket_connect_VC()#连接服务器\r
- \r
- #utime.sleep_ms(50)\r
- else:\r
- print("AI_VC_start2 (already connected) {}".format(self._vc_fsm_log_snip()))\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- if self.AI_client_VC_isconnect == 0:\r
- #self.Close()\r
- return\r
- # 单对话 PTT:会话已 started 时勿重复发 Start(否则第二轮起上行/ASR 易乱)\r
- _ptt_skip_start = False\r
- try:\r
- if self.VC_mode == 1 and getattr(self, "_vc_session_started", 0) == 1:\r
- _ptt_skip_start = True\r
- print("[PTT] AI_VC_start skip duplicate Start (session active)")\r
- except Exception:\r
- pass\r
- if not _ptt_skip_start:\r
- self.AI_client_VC_start_requset()\r
- print("AI_VC_start3")\r
- \r
- if (not _ptt_skip_start) and self.AI_TTS_str == "" and self.AI_new_voice_url == "":\r
- self._uplink_request_start("AI_VC_start")\r
- \r
- \r
- # pass\r
- print("AI_VC_start4")\r
-\r
- #self.VC_Play_Start()\r
- #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})\r
- #开始线程\r
- \r
- # if self.AI_VC_send_data_queue_thread_id == 0: \r
- # _thread.stack_size(self.Audio_buf_size+8*1024)\r
- # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())\r
- # _thread.stack_size(8*1024)\r
- # print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))\r
- \r
- if self.AI_audio_queue_thread_id ==0:\r
- _thread.stack_size(24*1024)\r
- self.AI_audio_queue_thread_id = _thread.start_new_thread(self.AI_audio_queue_thread, ())\r
- _thread.stack_size(8*1024)\r
- print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))\r
- else:\r
- print("AI_VC_start :AI_audio_queue_thread_id != 0 ")\r
- EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠\r
- self._1S_timer.start(1000,1,self._1S_timer_cb)\r
- # self.VC_pingping_timerout = 0\r
- # self.asr_pingping_timerout = 0\r
- print("AI_VC_start5")\r
- #开始会话\r
- def AI_client_VC_start_requset(self):\r
- voice="zh_male_wennuanahu_moon_bigtts"\r
- try:\r
- voice = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["voice"]\r
- except Exception as e:\r
- usys.print_exception(e) \r
- #voice="zh_female_dongmanxiaoyang_moon_bigtts"\r
- token = EventMesh.publish("get_dev_token")\r
- if token ==None:\r
- token = ""\r
- # bot_option:优先从 default_config(usr 可覆盖)读取 VC_BOT_ID / VC_WORKFLOW_ID,允许空字符串\r
- _def_bot_id = "7462617881076351002"\r
- _def_workflow_id = "7462621757327196187"\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- _vc_bot_id = vc_config.get_attr("VC_BOT_ID", _def_bot_id)\r
- _vc_workflow_id = vc_config.get_attr("VC_WORKFLOW_ID", _def_workflow_id)\r
- if _vc_bot_id is None:\r
- _vc_bot_id = ""\r
- if _vc_workflow_id is None:\r
- _vc_workflow_id = ""\r
- else:\r
- _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id\r
- except Exception:\r
- _vc_bot_id, _vc_workflow_id = _def_bot_id, _def_workflow_id\r
- json_data = \\r
- {\r
- "header": {\r
- "event": "Start",\r
- "trace_id": self.get_trace_id(),\r
- },\r
- "payload": {\r
- "simple_response": True,#不返回llm过程文本\r
- "device_id": modem.getDevImei(),\r
- \r
- "user_agent": "AudioClient/1.0",\r
- "dialog_attributes": {\r
- #"provider": "doubao",\r
- # "provider": "coze",\r
-\r
- "cfg": {\r
- "disable_empty_prompt" : True,#拦截空字符请求大模型\r
- "custom_variables": {\r
- "name": self.bot_name,\r
- "devToken":token,\r
- },\r
- "greeting":{\r
- "enabled":False,\r
- "content":""\r
- },\r
- "openai_option":{\r
- },\r
-\r
- "bot_option": {\r
- "use_workflow": True,\r
- #"input_format": "text",\r
- "bot_id": _vc_bot_id,\r
- "workflow_id": _vc_workflow_id,\r
- \r
- }\r
- }\r
- },\r
- "outbound_attributes": {\r
- "provider": "volcengine",\r
- "cfg": {\r
- "format": "pcm",\r
- "sample_rate": self.VC_play_samplerate,\r
- "bit_rate": 16,\r
- "channel": 1,\r
- "volume": self.VC_TTS_volume,\r
- "emotion": self.emotion,\r
- #"voice": "7426720361732980745",\r
- "voice": voice,#音色\r
- "speech_rate": self.speech_rate,#语速\r
- "chunk_interval_ms": 10, # 配置每段音频入队间隔时间 100ms\r
- }\r
- },\r
- "inbound_attributes": {\r
- # "provider": "xun-fei",#\r
- "provider": "volcengine",\r
- "disable_vad":True,\r
- "vad_silence":self.vad_silence,\r
- "interrupt_options":{\r
- "concatenate_results":True,\r
- "thinking_time_only": True,\r
- #"keyword_interrupt": false, // 启用打断词功能 (打断词只有在思考以及之后才能打断)\r
- #"interrupt_keywords": ["停止", "打断","打断一下", "等一下", "别说了", "打住"] // 配置打断词列表\r
- },\r
- "cfg": {\r
- "format": "pcm",\r
- "sample_rate": self.VC_record_samplerate,\r
- "bit_rate": 16,\r
- "channel": 1,\r
- "enable_punctuation": True,\r
- "enable_itn":True,\r
- "keywords":[]\r
- }\r
- },\r
- "commands":self.commands,\r
- }\r
- }\r
- # "bot_id":"7481164663863459850",#鸭子\r
- # "workflow_id":"7480485093845991443",\r
- # "bot_id":"7462617881076351002",#语言助手\r
- # "workflow_id":"7462621757327196187",\r
- # "bot_id":"7469964258592931852",#深圳百事通\r
- # "workflow_id":"7469968083063603241",\r
- # "bot_id":"7411071327182438412",#柒牌\r
- # "workflow_id":"7469049680291610636",\r
- # if self.online_kws == 1:#打开\r
- # json_data["payload"]["inbound_attributes"]["interrupt_options"]["keyword_interrupt"] = True\r
- # json_data["payload"]["inbound_attributes"]["interrupt_options"]["interrupt_keywords"] = ["你好,柒先生","你好"]\r
- #如果\r
- json_data["payload"]["outbound_attributes"]["provider"]=self.outbound_attributess_provider#TTS音色服务选择,火山\r
- json_data["payload"]["inbound_attributes"]["provider"]=self.inbound_attributess_provider#ASR服务选择,火山\r
- #优先选择self.VC_TTS_voice_type[self.VC_TTS_voice_key]里得参数\r
- # if "inbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
- # if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"])>0:\r
- # json_data["payload"]["inbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]\r
- # print("inbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["inbound_attributess_provider"]))\r
- if "outbound_attributess_provider" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:\r
- if len(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"])>0:\r
- json_data["payload"]["outbound_attributes"]["provider"]=self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]\r
- print("outbound_attributess_provider是VC_TTS_voice_type里的:{}".format(self.VC_TTS_voice_type[self.VC_TTS_voice_key]["outbound_attributess_provider"]))\r
-\r
- # 根据配置设置默认 bot/workflow,并允许 VC_TTS_voice_type 覆盖\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- opt = vc_config.get_attr("DIALOG_BOT_OPTION", None)\r
- if isinstance(opt, dict):\r
- if "bot_id" in opt:\r
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = opt.get("bot_id") or ""\r
- if "workflow_id" in opt:\r
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = opt.get("workflow_id") or ""\r
- if "use_workflow" in opt:\r
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["use_workflow"] = bool(opt.get("use_workflow"))\r
- except Exception:\r
- pass\r
-\r
- try:\r
- # voice 配置可覆盖 bot_id/workflow_id/openai_option\r
- cur = self.VC_TTS_voice_type[self.VC_TTS_voice_key]\r
- if isinstance(cur, dict):\r
- if cur.get("bot_id"):\r
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["bot_id"] = cur.get("bot_id")\r
- if "workflow_id" in cur:\r
- json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]["workflow_id"] = cur.get("workflow_id") or ""\r
- if cur.get("openai_option"):\r
- json_data["payload"]["dialog_attributes"]["cfg"]["openai_option"] = cur.get("openai_option")\r
- except Exception:\r
- pass\r
-\r
- try:\r
- _bo = json_data["payload"]["dialog_attributes"]["cfg"]["bot_option"]\r
- if not _bo.get("bot_id", "") or not _bo.get("workflow_id", ""):\r
- _bo["use_workflow"] = False\r
- except Exception:\r
- pass\r
- try:\r
- if "vc_config" in globals() and vc_config:\r
- p = vc_config.get_attr("DIALOG_PROVIDER_DEFAULT", None)\r
- if p:\r
- json_data["payload"]["dialog_attributes"]["provider"] = p\r
- except Exception:\r
- pass\r
-\r
- try:\r
- if "vc_config" in globals() and vc_config and vc_config.get_bool("KEYWORDS_FROM_VOICE_TYPE", False):\r
- keywords = []\r
- for key in self.VC_TTS_voice_type:\r
- keywords.append(key)\r
- json_data["payload"]["inbound_attributes"]["cfg"]["keywords"] = keywords\r
- except Exception:\r
- pass\r
- if self.dialog_attributes_provider != None:\r
- json_data["payload"]["dialog_attributes"]["provider"] = self.dialog_attributes_provider\r
- if self.commands_mode !=0 :\r
- json_data["payload"]["commands"] = []\r
- if self.VC_mode == 0 :#and dev_type != "7PIN_BK_coze":#长对话模式打开vad\r
- json_data["payload"]["inbound_attributes"]["disable_vad"] = False\r
- # 新会话开始:在收到服务端 started 前,禁止发送音频帧(避免 session not initialized)\r
- self._vc_session_started = 0\r
- try:\r
- self.fsm.on_session_start_request()\r
- except Exception:\r
- pass\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- try:\r
- _tid = json_data.get("header", {}).get("trace_id", "")\r
- print("[VC][proto] Start enqueued trace_id={} VC_mode={} disable_vad={} ws_conn={}".format(\r
- _tid or "-",\r
- self.VC_mode,\r
- json_data.get("payload", {}).get("inbound_attributes", {}).get("disable_vad", "?"),\r
- self.AI_client_VC_isconnect))\r
- except Exception:\r
- pass\r
- \r
- #开始上传语音\r
- def AI_client_VC_send_speech(self):\r
- json_data = \\r
- {\r
- "header": {\r
- "event": "audio.create",\r
- \r
- },\r
- }\r
- \r
- if self.VC_mode == 1:#单对话模式\r
- # 协议门控(可用 VC_PROTO_GUARDS 临时关闭)\r
- if (not VC_PROTO_GUARDS) or (self._vc_session_started == 1):\r
- self._ptt_audio_created = 1\r
- try:\r
- print("[VC][proto] audio.create send session_started={} proto_guards={}".format(\r
- self._vc_session_started, VC_PROTO_GUARDS))\r
- except Exception:\r
- pass\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- else:\r
- try:\r
- print("[VC][proto] audio.create SKIP session_started={} VC_PROTO_GUARDS={} "\r
- "(等对端 started 后再开麦)".format(self._vc_session_started, VC_PROTO_GUARDS))\r
- except Exception:\r
- pass\r
- \r
-\r
- \r
- \r
- \r
- \r
- try:\r
- self.fsm.on_server_asr_started()\r
- except Exception:\r
- pass\r
- #结束上传语音\r
- def AI_client_VC_end_speech(self):\r
- json_data = \\r
- {\r
- "header": {\r
- "event": "audio.completed",\r
- \r
- },\r
- }\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- self._ptt_audio_created = 0\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"audio.completed"})\r
-\r
- \r
- #客户端打断对话\r
- def AI_client_VC_RequestToSpeak(self):\r
- json_data = \\r
- {\r
- "event": "cancel",\r
- "payload": {}\r
- }\r
- print("AI_client_VC_RequestToSpeak!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- # 模式切换等场景 play_tts(..., is_close_VC=1) 会置 Close_flag=1;用户再按 AI 键走 cancel→canceled→\r
- # uplink_start 时,FSM 与 _uplink_request_start 均会因 Close_flag 拒绝开麦,表现为“切回长对话后按键无上行”。\r
- try:\r
- if self.VC_mode == 0:\r
- self.Close_flag = 0\r
- self._vc_pending_interrupt_uplink = 1\r
- self._vc_clear_stale_record_state("RequestToSpeak")\r
- except Exception:\r
- pass\r
- # 只做“打断+清播放队列”,不要在这里关音频队列线程(会导致后续 TTS/提示音不稳定,且触发竞态)\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_init})\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_send,"data":ujson.dumps(json_data)})\r
- utime.sleep_ms(1)\r
- try:\r
- self.fsm.reset_rx_phases()\r
- except Exception:\r
- pass\r
- # #self.Close()\r
- # self.AI_VC_start() \r
- #TTS播放\r
- def AI_client_VC_synthesis(self):\r
- # 幂等保护:在上一条 synthesis 还未等到 tts.started 前,不重复发(防止多兜底/多入口重复触发)\r
- try:\r
- if self._tts_pending == 1:\r
- return\r
- except Exception:\r
- pass\r
- json_data = \\r
- {\r
- "event": "synthesis",\r
- "payload": {\r
- "text": self.AI_TTS_str\r
- }\r
- }\r
- # 语义标记只用于“是否自动开麦”的策略。\r
- # 当前策略:长对话不自动开麦(需按键开始),因此这里不对提示音做自动开麦标记。\r
- self.AI_TTS_str = ""\r
- # 标记为“等待 tts.started”,避免在 synthesis→tts.started 窗口误开下一轮录音上行\r
- self._tts_pending = 1\r
- try:\r
- self._tts_pending_ms = utime.ticks_ms()\r
- except Exception:\r
- self._tts_pending_ms = 0\r
- try:\r
- self.fsm.on_local_synthesis_sent()\r
- except Exception:\r
- pass\r
- print("AI_client_VC_synthesis!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- # if self.VC_state == VC_State_RequestToRespond:\r
- # print("AI_client_VC_RequestToRespond 重复 tts_str:{}\nasr_str:{}".format(self.AI_TTS_str,self.AI_ASR_str))\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- # 金三 V2.5:AudioPlayed — 端侧 TTS 播报结束后上报,服务端进入下一轮对话(轻度重同步,不断开 WebSocket)\r
- def AI_client_VC_audio_played(self):\r
- # V2.5:播完上报;与 VC_PROTO_GUARDS 无关(见文件头说明)\r
- try:\r
- if self.AI_client_VC_isconnect != 1 or self.AI_client_VC == None:\r
- return\r
- json_data = {"event": "audio.played", "payload": {}}\r
- print("AI_client_VC_audio_played")\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- except Exception as e:\r
- usys.print_exception(e)\r
- def kws_even(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.kws_even_cb}) \r
- def kws_even_cb(self):\r
- if EventMesh.publish("get_sos_flag") == 1:\r
- return\r
- print("kws_even_cb")\r
- if self.VC_mode == 1: \r
- try:\r
- tts_str = vc_config.get_attr("KWS_PTT_HINT", "请您按住圆形的按钮开始讲话,讲完松开即可 ")\r
- except Exception:\r
- tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "\r
- EventMesh.publish("play_tts",[tts_str,1]) \r
- return\r
- if self.VC_mode == 0:\r
- if EventMesh.publish("get_voice_record_flag") == 1:#停止静默录音\r
- if EventMesh.publish("get_voice_record_mode") == 2:#语音留言不能被打断\r
- return\r
- elif EventMesh.publish("get_voice_record_mode") == 1: #静默录音需要打断\r
- EventMesh.publish("stop_voice_record")#停止静默录音\r
- self.aud.aud_tone_play(10,400)\r
- # EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
- self.Close()\r
- # if dev_type == "7PIN" :\r
- # self.aud.aud_tone_play(10,400)#播放按键音\r
- #self.aud.aud_tone_play(10,200)#播放按键音\r
- self.AI_TTS_str = ""#"你好,有什么吩咐"\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self._uplink_request_stop("kws_even_cb")\r
- self._uplink_request_start("kws_even_cb")\r
- self.AI_VC_start() \r
-\r
- def set_vad_silence(self,event = None,msg= None):\r
- self.vad_silence = msg\r
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence]) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg}) \r
- #设置tts参数\r
- def set_wait_first_flag(self,event = None,msg= None):#设置未读语音播报初次记录flag\r
- self.wait_first_flag = msg\r
- def set_VC_TTS_voice_type(self,event = None,msg= None):\r
- self.VC_TTS_voice_type["APP_voice"] = msg\r
- #self.VC_TTS_voice_type[self.VC_TTS_voice_key]\r
- self.VC_TTS_voice_key = "APP_voice"\r
- EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])\r
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])\r
- print("[VC] TTS_voice key={} map={}".format(self.VC_TTS_voice_key, self.VC_TTS_voice_type))\r
- #修改音量大小 msg为1则为加大,msg为0则为减小,返回1成功,返回0失败\r
- def change_volume(self,event =None, msg =None):\r
- if msg == 0 :\r
- self.VC_TTS_volume -= 10\r
- if self.VC_TTS_volume < 0:\r
- self.VC_TTS_volume = 0\r
- return 0\r
- else:\r
- self.aud.setVolume(self.VC_TTS_volume //10)\r
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
- return 1\r
- elif msg == 1:\r
- self.VC_TTS_volume += 10\r
- if self.VC_TTS_volume > 100:\r
- self.VC_TTS_volume = 100\r
- return 0\r
- else:\r
- self.aud.setVolume(self.VC_TTS_volume //10)\r
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
- return 1\r
- elif msg >= 100:#修改为具体音量\r
- if msg > 200:\r
- self.VC_TTS_volume = 100\r
- return 0\r
- else:\r
- self.VC_TTS_volume = msg - 100#还原数据\r
- self.aud.setVolume(self.VC_TTS_volume //10)\r
- EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])\r
- EventMesh.publish("post_device_state")\r
- EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)\r
- return 1\r
- #调节聆听快慢(以秒为单位)\r
- def NLP_set_vad_silence(self,event,msg):\r
- if msg == 1:#聆听变快\r
- self.vad_silence -= 1000\r
- if self.vad_silence < 1000:\r
- AI_TTS_str = "已经是最快了" \r
- self.vad_silence = 1000\r
- else:\r
- AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)\r
- elif msg == 0:#聆听变慢\r
- self.vad_silence += 1000\r
- if self.vad_silence > 4000:\r
- self.vad_silence = 4000\r
- #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])\r
- #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小\r
- AI_TTS_str = "已经是最慢了"\r
- else:\r
- AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000) \r
- EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])\r
- return AI_TTS_str\r
- #查询指令判断\r
- def find_cmd(self,event ,msg):\r
- if "电量" in msg:\r
- battery = EventMesh.publish("get_battery")\r
- print("电量查询:{}".format(battery))\r
- tts_play_str = "电量剩余百分之{}".format(battery)\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- # print("电量!!!!!!!!!!!")\r
- elif "信号" in msg:\r
- signal = net.csqQueryPoll()\r
- tts_play_str = "当前信号{}".format(signal)\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- # print("信号!!!!!!!!!!!")\r
- elif "卡号" in msg:\r
- # print("卡号!!!!!!!!!!!")\r
- iccid = sim.getIccid()\r
- iccid = num_str_to_chinese(iccid)#转为中文\r
- if iccid != -1 and len(iccid) > 5:\r
- print("iccid{}".format(iccid))\r
- tts_play_str = "当前卡号为:{}".format(iccid)\r
- else:\r
- tts_play_str = "卡号查询失败"\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- elif "设备号" in msg:\r
- imei = modem.getDevImei()\r
- if imei != -1 and len(imei) > 5:\r
- print("IMEI:{}".format(imei))\r
- tts_play_str = "当前设备号为:{}".format(imei)\r
- else:\r
- tts_play_str = "设备号查询失败"\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- elif "音量" in msg :\r
- tts_play_str = "当前音量是{}".format(self.VC_TTS_volume)\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- elif "语速" in msg :\r
- tts_play_str = "当前语速是{}".format(EventMesh.publish("find_rate"))\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
- \r
- #改变音色判断\r
- def change_voice(self,event ,msg):\r
- voice = msg\r
- # if "男声" in msg or "普通话" in msg:\r
- # self.VC_TTS_voice_key = "男"\r
- # else:\r
- # self.VC_TTS_voice_key = voice\r
- self.outbound_attributess_provider = "xun-fei"\r
- if "男声" in msg or "普通话" in msg:\r
- self.VC_TTS_voice_key = "男"\r
- self.outbound_attributess_provider = "volcengine"\r
- elif "女声" in msg:\r
- self.VC_TTS_voice_key = "女"\r
- self.outbound_attributess_provider = "volcengine"\r
- elif "童声" in msg:\r
- self.VC_TTS_voice_key = "童" \r
- self.outbound_attributess_provider = "volcengine"\r
- elif "香港粤语" in msg:\r
- self.VC_TTS_voice_key = "香港粤语"\r
- elif "四川话" in msg:\r
- self.VC_TTS_voice_key = "四川" \r
- elif "上海话" in msg:\r
- self.VC_TTS_voice_key = "上海" \r
- elif "安徽合肥话" in msg:\r
- self.VC_TTS_voice_key = "安徽合肥" \r
- elif "山东话" in msg:\r
- self.VC_TTS_voice_key = "山东话" \r
- elif "陕西话" in msg:\r
- self.VC_TTS_voice_key = "陕西" \r
- elif "广东话" in msg:\r
- self.VC_TTS_voice_key = "广东" \r
- elif "成都话" in msg:\r
- self.VC_TTS_voice_key = "成都" \r
- elif "湖南话" in msg:\r
- self.VC_TTS_voice_key = "湖南" \r
- elif "苏州话" in msg:\r
- self.VC_TTS_voice_key = "苏州" \r
- elif "东北话" in msg:\r
- self.VC_TTS_voice_key = "东北" \r
- elif "河南话" in msg:\r
- self.VC_TTS_voice_key = "河南" \r
- elif "湖北话" in msg:\r
- self.VC_TTS_voice_key = "湖北" \r
- elif "闽南话" in msg:\r
- self.VC_TTS_voice_key = "闽南" \r
- elif "内蒙古话" in msg:\r
- self.VC_TTS_voice_key = "内蒙古话" \r
- EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])\r
- tts_play_str = "声音已改为{}".format(voice)\r
- EventMesh.publish("play_tts",[tts_play_str,0])\r
-\r
- #闹钟的url播放接口\r
- def play_single_voice(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_single_voice_cb,"data":msg}) \r
- def play_single_voice_cb(self,data):\r
- try:\r
- new_voice_url = data[0] #音频地址\r
- is_close_VC = data[1]\r
- if new_voice_url != "":\r
- self.AI_new_voice_url = new_voice_url\r
- self.alarm_play_flag = 1\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_single_voice_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
- pass \r
- #语音留言\r
- def play_new_voice(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg}) \r
- def play_new_voice_cb(self,data):\r
- try:\r
- new_voice_url = data[0] #音频地址\r
- is_close_VC = data[1]\r
- if new_voice_url != "":\r
- self.recv_voice = 1#收到留言\r
- print("已收到最新留言!!!!!")\r
- # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数\r
- self.AI_new_voice_url = new_voice_url\r
- # if self.pwk_cnt_voice == 0 and self.pwk_notice_flag == 0:#并且不在多条留言的状态\r
- # EventMesh.publish("play_tts",["您有一条语音留言,10秒内按下中间键可以收听",1])\r
- if self.pwk_notice_flag == 0 and EventMesh.publish("get_app_new_voice_flag"):#不处于播放状态并且是小程序下发的语音而不是请求的\r
- self.pwk_cnt_voice += 1#加上当前这条\r
- EventMesh.publish("set_storage_config",["is_noread_voice",1])\r
- EventMesh.publish("set_app_new_voice_flag",0)\r
- print("voice_add_to = {}".format(self.pwk_cnt_voice))\r
- print("self.pwk_notice_flag = {}".format(self.pwk_notice_flag))\r
- if self.pwk_cnt_voice >1:\r
- if self.pwk_notice_flag == 0:\r
- tts_str = "您有" + str(self.pwk_cnt_voice) + "条未读语音留言,10秒内按下中间键可以收听"\r
- EventMesh.publish("play_tts",[tts_str,1])\r
- elif self.pwk_cnt_voice == 1:\r
- if self.pwk_notice_flag == 0:\r
- tts_str = "您有1条语音留言,10秒内按下中间键可以收听"\r
- EventMesh.publish("play_tts",[tts_str,1])\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_new_voice_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
- pass\r
- #历史留言查看\r
- def play_his_voice(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_his_voice_cb,"data":msg}) \r
- def play_his_voice_cb(self,data):\r
- try:\r
- new_voice_url = data[0] #音频地址\r
- is_close_VC = data[1]\r
- time = data[2]\r
- tts_play_str = time + "收到" \r
- EventMesh.publish("play_tts",[tts_play_str,1])\r
- if new_voice_url != "":\r
- self.recv_voice = 1#收到留言\r
- print("历史留言查看")\r
- # tts_play_str = "收到" \r
- # EventMesh.publish("play_tts",[tts_play_str,1])\r
- # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数\r
- self.AI_new_voice_url = new_voice_url\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_his_voice_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
- pass\r
- #等待未读留言查看\r
- def play_wait_voice(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_wait_voice_cb,"data":msg}) \r
- def play_wait_voice_cb(self,data):\r
- try:\r
- new_voice_url = data[0] #音频地址\r
- is_close_VC = data[1]\r
- print("是否为第一次:{}".format(self.wait_first_flag))\r
- if self.wait_first_flag == 0:\r
- tts_play_str = "您有" + str(len(EventMesh.publish("get_play_new_voice_list"))) + "条未听留言,10秒内按下中间键可以收听"\r
- EventMesh.publish("play_tts",[tts_play_str,1]) \r
- # self.wait_first_timer.start(1000,1,self.noread_wait_vocie)\r
- self.wait_first_flag = 1\r
- elif self.wait_first_flag == 1:\r
- self.wait_sort += 1\r
- self.AI_TTS_str = "第"+str(self.wait_sort)+"条" \r
- if new_voice_url != "":\r
- self.recv_voice = 1#收到留言\r
- print("未读留言查看")\r
- self.AI_new_voice_url = new_voice_url\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_wait_voice_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
- pass\r
- def noread_wait_vocie(self,*args):#如果一直没读\r
- self.wait_first_cnt += 1\r
- print("未读留言等待收听中:{}".format(self.wait_first_cnt))\r
- if EventMesh.publish("get_recv_voice") == 1:#如果检测到留言播放\r
- self.wait_first_cnt = 0\r
- self.wait_first_timer.stop()\r
- if self.wait_first_cnt == 15:\r
- EventMesh.publish("set_wait_first_flag",0)\r
- EventMesh.publish("set_history_voice_flag",0)\r
- EventMesh.publish("set_wait_voice_flag",0)\r
- self.wait_first_cnt = 0\r
- self.wait_first_timer.stop()\r
- #音乐播放\r
- def play_music(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_music_cb,"data":msg}) \r
- def play_music_cb(self,data):\r
- try:\r
- music_name = data[0]\r
- new_voice_url = data[1] #音频地址\r
- is_close_VC = data[2]\r
- if new_voice_url != "":\r
- self.AI_new_voice_url = new_voice_url\r
- self.music_play_flag = 1#表示音乐播放开启\r
- EventMesh.publish("play_tts",["下面为您播放歌曲{}".format(music_name),1])\r
- if is_close_VC != None:\r
- self.Close_flag = is_close_VC\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_music_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
-\r
- #AI翻译接收TTS\r
- def play_trans_tts(self,event = None,msg= None):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.play_trans_tts_cb,"data":msg}) \r
- def play_trans_tts_cb(self,data):\r
- try:\r
- new_voice_url = data #音频地址\r
- if new_voice_url != "":\r
- self.AI_new_voice_url = new_voice_url\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- if self.AI_ASR_send_data_queue.empty()!=True:\r
- self.AI_ASR_send_data_queue.clear()\r
- EventMesh.publish("set_led_state", {"ai_state": "thinking"})\r
- self.AI_ASR_send_buf_len = 0\r
- self._uplink_request_stop("play_trans_tts_cb")\r
- self.websocket_deconnect_VC()\r
- self.AI_VC_start()\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言\r
- self.play_new_voice_flag = 1\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})\r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC}) \r
- # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start}) \r
- except Exception as e:\r
- usys.print_exception(e) \r
- def AI_client_new_voice(self): #上传未读的语音地址/包括音频地址也可以\r
- json_data = \\r
- {\r
- "event": "audio.convert",\r
- "payload": {\r
- "audio_convert_request": {\r
- "audio_url": self.AI_new_voice_url,\r
- "bit_depth": 16,\r
- "channels": 1,\r
- "sample_rate": 16000,\r
- "target_format": "pcm"\r
- }\r
- }\r
- }\r
- self.AI_new_voice_url = ""\r
- print("AI_client_new_voice!!!!!!!!!!!!!!!!!!!!!!!!!!!!!{}".format(json_data))\r
- self.AI_client_VC_send(ujson.dumps(json_data))\r
- \r
-\r
- def chenggong_tone(self):\r
- EventMesh.publish("audio_montage_play",["chenggong.mp3"]) \r
- # self.aud.aud_tone_play(10,400)\r
- def send_tone(self):\r
- EventMesh.publish("audio_montage_play",["send_tone.mp3"])\r
- def concurrency_limit_exceeded_tone(self):\r
- self.Close()\r
- EventMesh.publish("audio_montage_play",["chaobingfatis.mp3"]) #播放超并发提醒\r
- #给服务器回应ack\r
-\r
- \r
- ############################################\r
- #关闭音频播放线程\r
- def AI_audio_queue_thread_close(self):\r
- print("AI_audio_queue_thread_close1!!!!!!!!!!!!!!!!!!!!!!")\r
- if self.AI_audio_queue_thread_id != 0:\r
- count =500\r
- while True:\r
- count -= 1\r
- if self.AI_audio_queue_thread_lock == 0 or count <=0:\r
- break\r
- else:\r
- self.AI_audio_queue_thread_lock = 2\r
- print("AI_audio_queue_thread going:{}...".format(count))\r
- utime.sleep_ms(1)\r
- self.AI_audio_queue_thread_lock = 0\r
- print("AI_audio_queue_thread_close2!!!!!!!!!!!!!!!!!")\r
- if self.AI_audio_queue_thread_id != 0:\r
- try:\r
- _thread.stop_thread(self.AI_audio_queue_thread_id)\r
- except Exception as e:\r
- usys.print_exception(e)\r
- self.AI_audio_queue_thread_id = 0\r
- # 音频队列 \r
- def AI_audio_queue_thread(self):\r
- print("AI_audio_queue_thread start")\r
- count = 0\r
- if self.AI_audio_queue.empty()!=True:\r
- self.AI_audio_queue.clear()\r
- print("音频数据还有剩余!!!!!!!!!!!!")\r
- while True:\r
- try:\r
- if self.AI_audio_queue_thread_lock == 2 or self.VC_stop_play_audio_flag == 0:\r
- print("AI_audio_queue_thread 该退出了")\r
- break\r
- #\r
- count+=1\r
- if count%500 == 1 and VC_HEARTBEAT_LOG:\r
- print("AI_audio_queue_thread on")\r
- utime.sleep_ms(1)\r
- #print("AI_audio_queue_thread")\r
- if self.AI_audio_queue.empty() :\r
- self.AI_audio_queue_thread_lock = 0\r
- \r
- \r
- else:\r
- self.AI_audio_queue_thread_lock = 1\r
- data = self.AI_audio_queue.get()\r
- self.close_timerout = 0\r
- if self.play_flag == 0:\r
- self.play_flag =1\r
- #self.AI_client_tts_data_len=len(data)\r
- #print("audio_queue:len:{}".format(len(data[0])))\r
- \r
- if len(data[0]) > 10:\r
- try:\r
- if self._ptt_should_drop_inbound_tts():\r
- continue\r
- except Exception:\r
- pass\r
- for i in range(5): \r
- if self.VC_Play_flag == 0:\r
- self.VC_Play_Start()\r
- utime.sleep_ms(2)\r
- else:\r
- break\r
- # 打断后 PCM 偶发未打开:加重试,避免「队列有数据、play_pcm=None、无声」\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM and self.play_pcm is None and self.VC_stop_play_audio_flag == 1:\r
- for j in range(4):\r
- if self.VC_Play_flag == 0:\r
- self.VC_Play_Start()\r
- utime.sleep_ms(30 + j * 35)\r
- if self.play_pcm is not None:\r
- break\r
- try:\r
- if self.VC_play_TYPE == VC_play_TYPE_PCM:\r
- # if self.VC_play_samplerate == 8000:\r
- # tmp= self.aud.playStream(1,data[0] )\r
- # else:\r
- write_buf = bytearray(data[0])\r
- #print("play_pcm.stream_write1 len:{}".format(len(write_buf)))\r
- if self.play_pcm != None:\r
- #print("play_pcm.stream_write !!!")\r
- ret = self.play_pcm.stream_write(write_buf,len(write_buf))\r
- #ret= self.aud.playStream(1, data[0])#播放音频流\r
- if self.tts_out_data_len == 0:\r
- try:\r
- self._tts_first_write_ms = utime.ticks_ms()\r
- except Exception:\r
- self._tts_first_write_ms = 0\r
- try:\r
- sid = getattr(self, "_cur_sid", "")\r
- did = getattr(self, "_cur_dialog_id", "")\r
- except Exception:\r
- sid, did = "", ""\r
- print("[TTS] first_write ret={} len={} sid={} did={}".format(\r
- ret, len(write_buf), sid, did\r
- ))\r
- if ret <= 0:\r
- print("[TTS] stream_write ret={} len={} (no audio?)".format(ret, len(write_buf)))\r
- self.tts_out_data_len += ret\r
- try:\r
- if getattr(self.fsm, "await_played", False) and self.AI_audio_queue.empty():\r
- self._vc_try_play_end_tick()\r
- except Exception:\r
- pass\r
- #print("play_pcm.stream_write2:{}".format(self.tts_out_data_len))\r
- elif self.VC_play_TYPE == VC_play_TYPE_MP3:\r
- tmp= self.aud.playStream(3, data[0])#播放音频流\r
- \r
- if tmp != 0:\r
- print("2playStream:error tmp:{} data len:{}!!!!!!!!!!!!".format(tmp,len(data[0])))\r
- # # #播放失败的时候填充WAV头\r
- # data_len = len(data[0])\r
- # data_len_b = data_len.to_bytes(4,'little')#数据长度转16进制数组\r
- # h_data = [ 0x52 ,0x49 ,0x46 ,0x46 #,0x24 ,0x00 ,0x00 ,0x00 #文件标识、文件大小\r
- # ,0x57 ,0x41 ,0x56 ,0x45 ,0x66 ,0x6D ,0x74 ,0x20 #文件格式、格式块标识符\r
- # ,0x10 ,0x00 ,0x00 ,0x00 ,0x01 ,0x00 ,0x01 ,0x00 ,0x80 ,0x3E ,0x00 ,0x00 #格式块大小4、音频格式2、声道数2、采样率4\r
- # ,0x80 ,0x3E ,0x00 ,0x00 ,0x02 ,0x00 ,0x10 ,0x00 ,0x64 ,0x61 ,0x74 ,0x61]#每秒字节数4、数据块对齐2、位深度2、data4\r
- # h_data_b = bytes(h_data)\r
- # file_len = data_len+len(h_data_b)+4+4\r
- # file_len_b = file_len.to_bytes(4,'little')#文件长度转16进制数组\r
- # h_data_b = h_data_b[:4]+file_len_b+h_data_b[4:]+data_len_b\r
- # #self.aud.playStream(2, h_data_b)#播放音频流\r
- # tmp= self.aud.playStream(2, h_data_b+data[0])#播放音频流\r
- # print("3playStream:tmp:{} !!!!!!!!!!!!".format(tmp))\r
- \r
- \r
- mem = gc.mem_free()\r
- #print('1剩余内存RAM:{} KB'.format(mem / 1024))\r
- except Exception as e:\r
- print("AI_audio_queue_thread:ERROR:")\r
- usys.print_exception(e)\r
- \r
- # if self.VC_state == VC_State_TTSEnd and self.AI_audio_queue.empty() :#self.AI_audio_queue为空且tts下发结束\r
- # #通知服务器播放结束\r
- # self.AI_client_VC_LocalRespondingEnded()\r
- except Exception as e:\r
- print("AI_audio_queue_thread:ERROR:")\r
- usys.print_exception(e)\r
- self.AI_audio_queue_thread_lock = 0\r
- self.AI_audio_queue_thread_id = 0\r
- print("AI_audio_queue_thread:exit")\r
- def factory_mode(self):\r
- print("factory_mode")\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.factory_start}) \r
- def factory_start(self):\r
- self.factory_timer.start(3000, 1, self.factory_timer_cb)\r
- def factory_timer_cb(self, *args, **kwargs):\r
- print("factory_timer_cb play......")\r
- #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功\r
- EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])\r
- def Close_VC(self,event,msg):\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self.Close}) \r
- def Close(self):\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] Close() enter VC_mode={} {} VC_satrt_flag={} VC_record_state_flag={}".format(\r
- getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(),\r
- getattr(self, "VC_satrt_flag", -1), getattr(self, "VC_record_state_flag", -1)))\r
- except Exception:\r
- pass\r
- self.VC_stop_play_audio_flag = 0\r
- try:\r
- self.AI_key_long_press_flag = 0\r
- except Exception:\r
- pass\r
- self.VC_Thinking_timerout = 0\r
- self.AI_ASR_timer.stop()\r
- self._1S_timer.stop()\r
- self.close_timerout = 0\r
- self._idle_deep_sec = 0\r
- self.factory_timer.stop()\r
- self.AI_TTS_str = ""\r
- self.AI_ASR_str = ""\r
- self.AI_ASR_str_tmp = ""\r
- self.AI_ASR_flag = 0\r
- self.AI_ASR_check_timeout = 0\r
- self.AI_ASR_error_timeout = 0\r
- self.LocalRespondingEnded_timerout = 0\r
- self._need_audio_played_ack = 0\r
- self._tts_pending = 0\r
- self._tts_pending_ms = 0\r
- self._vc_session_started = 0\r
- self._ptt_audio_created = 0\r
- self._vc_pending_interrupt_uplink = 0\r
- try:\r
- self._skip_started_uplink_once = 0\r
- except Exception:\r
- pass\r
- try:\r
- self._ptt_abort_interrupt_arm()\r
- except Exception:\r
- pass\r
- # Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识\r
- try:\r
- self._cur_dialog_id = ""\r
- except Exception:\r
- pass\r
- try:\r
- self._cur_sid = ""\r
- except Exception:\r
- pass\r
- self.is_ruppt_tts = 0#打断标志位清零\r
- print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] step=set_led_state_idle (first)")\r
- except Exception:\r
- pass\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- self.tts_in_data_len = 0\r
- self.tts_out_data_len = 0\r
- #必须先销毁线程,不然线程会容易报错\r
- \r
- try:\r
- self.AI_audio_queue_thread_close()#销毁音频线程\r
- except Exception as e:\r
- usys.print_exception(e)\r
- try:\r
- self.AI_VC_send_data_queue_thread_close()#销毁数据发送线程\r
- except Exception as e: \r
- usys.print_exception(e)\r
- \r
- # Close 可能被不同线程触发:统一切回主线程执行 stop\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] step=schedule_uplink_stop reason=Close (async publish_AI_btnInfo)")\r
- except Exception:\r
- pass\r
- EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"Close"})\r
- \r
- print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- \r
- print("ai_VC Close1.2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- \r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] step=websocket_deconnect_VC")\r
- except Exception:\r
- pass\r
- try:\r
- self.websocket_deconnect_VC()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- \r
-\r
- \r
-\r
-\r
- print("ai_VC Close1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- \r
- print("ai_VC Close2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- \r
- \r
- \r
- \r
- if self.record_data_save_flag == 1:\r
- if self.record_fd :\r
- self.record_fd.close()\r
- self.record_fd = None\r
- if self.play_data_save_flag == 1:\r
- if self.play_fd !=None:\r
- self.play_fd.close()\r
- self.play_fd = None\r
- print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")\r
- \r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
- self.play_new_voice_flag = 0 #语音消息播放状态\r
- if self.voice_choose_flag == 0: # 如果没有未读语音\r
- self.AI_new_voice_url = "" # 语音播放地址\r
- print("关闭并重置语音标志位")\r
- else:\r
- print("关闭但是没有重置语音标志位")\r
- self.AI_ASR_send_buf_len = 0\r
- #self.AI_WS_connect_Queue.put(self.AI_client_VC)\r
- try:\r
- if getattr(self, "VC_mode", 0) != 1:\r
- print("[VC][long_idle] step=set_led_state_idle (second) -> VC_init VC_satrt_flag=0")\r
- except Exception:\r
- pass\r
- EventMesh.publish("set_led_state", {"ai_state": "idle"})\r
- self.VC_init()\r
- self.VC_satrt_flag = 0\r
- # Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)\r
- try:\r
- _dsec = 60\r
- _den = 1\r
- if "vc_config" in globals() and vc_config:\r
- _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))\r
- _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))\r
- if _den and _dsec > 0:\r
- self._1S_timer.start(1000, 1, self._1S_timer_cb)\r
- except Exception:\r
- pass\r
- # try:\r
- # while True:\r
- # if self.AI_WS_connect_Queue.empty()!=True:\r
- # AI_WS_connect = self.AI_WS_connect_Queue.get()\r
- # if AI_WS_connect :\r
- # try:\r
- # AI_WS_connect._close()\r
- # AI_WS_connect = None\r
- # except Exception as e:\r
- # usys.print_exception(e)\r
- # else:\r
- # print("websocket链接已为空")\r
- # else:\r
- # break\r
- # except Exception as e:\r
- # usys.print_exception(e)\r
-\r
- def VC_init2(self):\r
- self.VC_init()\r
- \r
- print("ai_VC VC_init2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")\r
- try:\r
- self.websocket_deconnect_VC()\r
- except Exception as e:\r
- usys.print_exception(e)\r
- print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")\r
- self._1S_timer.stop()\r
- if self.AI_VC_send_data_queue.empty()!=True:\r
- self.AI_VC_send_data_queue.clear()\r
-\r
- self.AI_ASR_send_buf_len = 0\r
- if self.AI_audio_queue.empty()!=True:\r
- self.AI_audio_queue.clear()\r
- def VC_init(self):\r
- _q0 = self.AI_audio_queue.size()\r
- self.VC_Play_End()\r
- if self.AI_audio_queue.empty()!=True:\r
- self.AI_audio_queue.clear()\r
- print("[VC] init audio_queue {} -> {}".format(_q0, self.AI_audio_queue.size()))\r
- # num = EventMesh.publish("get_volum_size")\r
- # if num == None:\r
- # num = 8\r
- # self.aud.setVolume(num)#恢复原始音量\r
+# -*- 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
+from vc_session_media import VCSessionMediaMixin
+
+class AI_VC(VCSessionIoMixin, VCSessionMediaMixin, Abstract):
+ def _vc_fsm_log_snip(self):
+ try:
+ f = self.fsm
+ return "sess={} turn={} tts_rx={} await={} sp_up={}".format(
+ int(getattr(f, "session", -1)),
+ int(getattr(f, "turn", -1)),
+ int(getattr(f, "tts_rx_phase", -1)),
+ 1 if getattr(f, "await_played", False) else 0,
+ 1 if getattr(f, "speech_uplink_open", 0) else 0,
+ )
+ except Exception:
+ return "fsm=?"
+ def _vc_capture_allowed(self, reason=""):
+ """采集/上行开麦前:下行播放或本地短音未结束则禁止。"""
+ try:
+ if self._vc_tts_pipeline_busy():
+ return False
+ if self._vc_local_play_busy():
+ return False
+ except Exception:
+ return False
+ return True
+ def _vc_stop_downlink_for_interrupt(self, reason="interrupt"):
+ """详设 DES-SCN-003:打断/取消时先本地停播清队列,再允许开麦。"""
+ try:
+ if getattr(self, "AI_audio_queue", None) is not None:
+ if self.AI_audio_queue.empty() != True:
+ self.AI_audio_queue.clear()
+ except Exception:
+ pass
+ try:
+ if self._vc_tts_pipeline_busy() or getattr(self, "is_ruppt_tts", 0) == 1:
+ self._drop_tts_audio = 1
+ except Exception:
+ pass
+ try:
+ self._ptt_hard_mute_local_playback()
+ 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()
+ except Exception:
+ pass
+ 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
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ except Exception:
+ pass
+ try:
+ pending_intr = getattr(self, "_vc_pending_interrupt_uplink", 0) == 1
+ if (
+ not pending_intr
+ and getattr(self, "VC_record_state_flag", 0) != 0
+ and getattr(self, "record_data_save_flag", 0) == 0
+ ):
+ self._uplink_request_stop("stop_downlink/" + str(reason))
+ except Exception:
+ pass
+ def _vc_long_idle_active_speech_or_later(self):
+ # 对应旧 VC_state >= SpeechIng
+ try:
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ return t not in (VCTurnState.Idle, VCTurnState.Listening)
+ except Exception:
+ return False
+ def _vc_is_business_idle(self):
+ # Level2:LED ai_state 为 idle,且无录音/流播/音乐闹钟等(与 WS ping 无关)
+ try:
+ if getattr(self, "music_play_flag", 0) == 1:
+ return False
+ if getattr(self, "alarm_play_flag", 0) == 1:
+ return False
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ return False
+ if getattr(self, "VC_Play_flag", 0) != 0:
+ return False
+ except Exception:
+ pass
+ try:
+ if EventMesh.publish("get_vc_stream_active") == 1:
+ return False
+ except Exception:
+ pass
+ try:
+ st = EventMesh.publish("get_led_debug_state")
+ if st and isinstance(st, dict):
+ if (st.get("ai") or "") != "idle":
+ return False
+ except Exception:
+ return False
+ return True
+ def _vc_idle_deep_sleep_impl(self):
+ try:
+ _need_close = (self.VC_satrt_flag == 1) or (getattr(self, "AI_client_VC_isconnect", 0) == 1)
+ print("[VC][idle_L2] 触发休眠流程 need_Close={} VC_mode={}".format(
+ 1 if _need_close else 0, getattr(self, "VC_mode", -1)))
+ except Exception:
+ pass
+ try:
+ if self.VC_satrt_flag == 1 or getattr(self, "AI_client_VC_isconnect", 0) == 1:
+ try:
+ print("[VC][idle_L2] 调用 Close 断开会话/链路")
+ except Exception:
+ pass
+ self.Close()
+ except Exception:
+ pass
+ try:
+ utime.sleep_ms(100)
+ except Exception:
+ pass
+ try:
+ print("[VC][idle_L2] 发布 Hsensor_set_sleep")
+ EventMesh.publish("Hsensor_set_sleep")
+ except Exception:
+ pass
+ try:
+ print("[VC][idle_L2] 发布 SYSTEM_SLEEP ({})".format(constants.EVENT.SYSTEM_SLEEP))
+ EventMesh.publish(constants.EVENT.SYSTEM_SLEEP)
+ except Exception:
+ pass
+ def _vc_rx_resets_long_idle_timer(self, recv_data):
+ """是否应因本帧下行清零 close_timerout(排除保活/心跳,含 V2.5 文本 ping 对端应答)。"""
+ try:
+ if recv_data is None:
+ return False
+ if isinstance(recv_data, (bytes, bytearray)):
+ return True
+ if not isinstance(recv_data, str):
+ return False
+ t = recv_data.strip()
+ if not t:
+ return False
+ tl = t.lower()
+ if tl in ("ping", "pong"):
+ return False
+ try:
+ jd = ujson.loads(t)
+ except Exception:
+ return True
+ ev = jd.get("event")
+ if ev is None or ev == "":
+ ev = jd.get("type")
+ if ev is None:
+ return True
+ if str(ev).strip().lower() in _VC_KEEPALIVE_RX_EVENTS:
+ return False
+ return True
+ except Exception:
+ return True
+ def _handle_pwk_short_release(self, event=None, msg=None, merged_ai_tail=False):
+ """电源键短按:读留言标志、退会议/翻译;否则 Close,或单键模式下走 AI_key_release。"""
+ print("pwk_key_release")
+ if EventMesh.publish("get_recv_voice") == 1:
+ EventMesh.publish("update_play_new_voice_info")
+ self.recv_voice = 0
+ EventMesh.publish("set_history_voice_flag", 0)
+ self.AI_new_voice_url = ""
+ if self.trans_flag == 1 or self.meet_flag == 1:
+ EventMesh.publish("exit_meet_mode")
+ EventMesh.publish("exit_trans_mode")
+ return
+ if merged_ai_tail:
+ self.AI_key_release(event, msg)
+ else:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
+ EventMesh.publish("stop_sos_call")
+ def _ai_key_press_ptt_mode(self):
+ """单对话 PTT:短按仅停 TTS(不换 sid);非播放态短按吞掉;长按由 AI_key_long_press→ptt_begin。"""
+ if self.VC_mode != 1:
+ return False
+ try:
+ playing = False
+ try:
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ playing = t in (VCTurnState.TtsReceiving, VCTurnState.Playing)
+ except Exception:
+ playing = False
+ if playing or self._vc_tts_pipeline_busy():
+ self._ptt_short_press_stop = 1
+ try:
+ self._ptt_hard_mute_local_playback()
+ except Exception:
+ pass
+ try:
+ self.VC_Play_End()
+ except Exception:
+ pass
+ try:
+ self.fsm.await_played = False
+ except Exception:
+ pass
+ try:
+ self._ptt_led_lock_listening = 0
+ self._ptt_interrupt_active = 0
+ self.is_ruppt_tts = 0
+ except Exception:
+ pass
+ try:
+ self._vc_publish_led("idle", "ptt_short_press/stop_tts")
+ except Exception:
+ pass
+ return True
+ except Exception:
+ pass
+ return True
+ def _ai_key_press_long_dialogue_interrupt(self):
+ """长对话:TTS/播放忙或 turn==Thinking 时短按 → RequestToSpeak(先 cancel,canceled 后再开麦)。"""
+ if self.VC_mode != 0:
+ return False
+ try:
+ if getattr(self, "AI_client_VC_isconnect", 0) != 1 or getattr(self, "_vc_session_started", 0) != 1:
+ return False
+ except Exception:
+ return False
+ need_cancel = False
+ if self._vc_tts_pipeline_busy():
+ need_cancel = True
+ else:
+ try:
+ if getattr(self.fsm, "turn", VCTurnState.Idle) == VCTurnState.Thinking:
+ need_cancel = True
+ except Exception:
+ pass
+ if not need_cancel:
+ return False
+ print("[KEY] long interrupt -> RequestToSpeak (cancel then uplink)")
+ self.is_ruppt_tts = 1
+ try:
+ self._vc_pending_interrupt_uplink = 1
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
+ return True
+ def _ai_key_press_long_dialogue_wake(self):
+ """长对话未连接或未 started:直接 AI_VC_start,勿 Close+sleep。"""
+ if self.VC_mode != 0:
+ return False
+ if getattr(self, "voice_flag", 0) == 1 or getattr(self, "meet_flag", 0) == 1 or getattr(self, "trans_flag", 0) == 1:
+ return False
+ try:
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+ return False
+ except Exception:
+ pass
+ print("[KEY] long wake AI_VC_start (no Close)")
+ try:
+ self.Close_flag = 0
+ except Exception:
+ pass
+ self.get_config_timer_count = 0
+ self.key_state = 1
+ self.AI_TTS_str = ""
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ self.AI_key_long_press_flag = 0
+ return True
+ def _vc_led_alert_rb_flash(self):
+ try:
+ EventMesh.publish("set_led_state", {"alert": "rb_flash_2"})
+ except Exception:
+ pass
+ def _ai_key_press_long_dialogue_when_session_ready(self):
+ """长对话已连接且 session 已 started:非播放态直接开麦续聊(勿 idle 时发 cancel)。"""
+ if self.VC_mode != 0:
+ return False
+ try:
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+ # 模式切换提示音会置 Close_flag=1;用户再短按须恢复可开麦
+ try:
+ self.Close_flag = 0
+ except Exception:
+ pass
+ self._vc_clear_stale_record_state("key.press/continue")
+ self.request_uplink_start("key.press/continue")
+ return True
+ except Exception:
+ pass
+ return False
+ def _ai_key_release_handle_ptt(self):
+ """单对话 PTT 松手:仅在上行录音中时 end_speech;禁止误触发 AI_VC_start(否则新 sid / cancel 竞态)。"""
+ if self.AI_key_long_press_flag == 1:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ try:
+ gap = 200
+ if "vc_config" in globals() and vc_config:
+ gap = int(vc_config.get_int("PTT_RELEASE_END_SPEECH_DELAY_MS", 200))
+ if gap > 0:
+ utime.sleep_ms(gap)
+ except Exception:
+ pass
+ print("[PTT] release end_speech asr_str={!r} rec={}".format(
+ self.AI_ASR_str, getattr(self, "VC_record_state_flag", -1)))
+ self.AI_client_VC_end_speech()
+ else:
+ self.AI_key_long_press_flag = 0
+ try:
+ if getattr(self, "_ptt_short_press_stop", 0) == 1:
+ self._ptt_short_press_stop = 0
+ except Exception:
+ pass
+ print("[PTT] release_no_uplink_skip")
+ return
+ try:
+ if getattr(self, "_ptt_short_press_stop", 0) == 1:
+ self._ptt_short_press_stop = 0
+ except Exception:
+ pass
+ print("[PTT] release_short_no_long")
+ def _ai_key_long_press_route(self, event=None, msg=None):
+ """长按:翻译先 Close 再开翻译录音;单对话→ptt_begin;长对话→与短按一致走 AI_key_press 链路。"""
+ if self.trans_flag == 1:
+ self.Close()
+ 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:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_key_press})
+ def _ptt_end_led_suppress(self):
+ # 保持 _vc_play_end_suppress_idle_led / _ptt_led_lock_listening 直至真开录(VC_Speech_Start)
+ pass
+ def _ptt_should_drop_inbound_tts(self):
+ """单对话打断窗口:丢弃迟到 TTS 帧 / 禁止起播。"""
+ try:
+ if getattr(self, "VC_mode", -1) != 1:
+ return False
+ if getattr(self, "_drop_tts_audio", 0) == 1:
+ return True
+ if getattr(self, "is_ruppt_tts", 0) == 1:
+ return True
+ if getattr(self, "_ptt_interrupt_active", 0) == 1:
+ return True
+ if getattr(self, "_vc_serial_canceling", 0) == 1:
+ return True
+ if getattr(self, "_ptt_pending_arm", 0) == 1:
+ return True
+ except Exception:
+ pass
+ return False
+ def _ptt_should_skip_remote_playback(self):
+ return self._ptt_should_drop_inbound_tts()
+ def _ptt_enter_interrupt_arm(self):
+ try:
+ self._ptt_arm_epoch = int(getattr(self, "_ptt_arm_epoch", 0)) + 1
+ self._ptt_interrupt_active = 1
+ self._ptt_led_lock_listening = 1
+ self._vc_play_end_suppress_idle_led = 1
+ self._drop_tts_audio = 1
+ self.is_ruppt_tts = 1
+ except Exception:
+ pass
+ def _ptt_abort_interrupt_arm(self):
+ try:
+ self._ptt_interrupt_active = 0
+ self._ptt_led_lock_listening = 0
+ self._vc_play_end_suppress_idle_led = 0
+ self._drop_tts_audio = 0
+ self.is_ruppt_tts = 0
+ except Exception:
+ pass
+ def _ptt_on_record_hardware_started(self):
+ """真开录后:允许新 TTS、放开灯锁;旧 TTS 已在打断期丢弃。"""
+ try:
+ self._ptt_interrupt_active = 0
+ self._ptt_led_lock_listening = 0
+ self._vc_play_end_suppress_idle_led = 0
+ self._drop_tts_audio = 0
+ self.is_ruppt_tts = 0
+ except Exception:
+ pass
+ def _ptt_hard_mute_local_playback(self):
+ try:
+ self.VC_Play_flag = 0
+ if getattr(self, "VC_play_TYPE", None) == VC_play_TYPE_PCM:
+ if getattr(self, "play_pcm", None) is not None:
+ try:
+ self.play_pcm.stream_stop()
+ self.play_pcm.close()
+ except Exception:
+ pass
+ self.play_pcm = None
+ 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:
+ self.AI_audio_queue.clear()
+ except Exception:
+ pass
+ except Exception:
+ pass
+ try:
+ gap = 0
+ if "vc_config" in globals() and vc_config:
+ gap = int(vc_config.get_int("PTT_POST_MUTE_MS", 30))
+ if gap > 0:
+ utime.sleep_ms(gap)
+ except Exception:
+ pass
+ def battery_priority_mute_vc(self, event=None, msg=None):
+ """低电/关机提示音:短暂停 VC TTS,避免被流式播放挡住。"""
+ try:
+ self._ptt_hard_mute_local_playback()
+ 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()
+ except Exception:
+ pass
+ def _vc_enqueue_btn(self, func, data=None):
+ """统一经 publish_AI_btnInfo 入队,在 btnInfo_queue_thread 串行执行。"""
+ try:
+ if data is not None:
+ EventMesh.publish("publish_AI_btnInfo", {"func": func, "data": data})
+ else:
+ EventMesh.publish("publish_AI_btnInfo", {"func": func})
+ except Exception:
+ pass
+ def _vc_ptt_cancel_wait_ms(self):
+ try:
+ if "vc_config" in globals() and vc_config:
+ return int(vc_config.get_int("PTT_CANCEL_WAIT_MS", 2500))
+ except Exception:
+ pass
+ return 2500
+ def _vc_ptt_need_interrupt_wait(self):
+ """单对话:服务端仍在思考/收 TTS/播报时,开录前须先 cancel 并等 canceled。"""
+ try:
+ if getattr(self, "VC_mode", -1) != 1:
+ return False
+ turn = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if self._vc_tts_pipeline_busy():
+ return True
+ if turn in (
+ VCTurnState.Thinking, VCTurnState.TtsReceiving,
+ VCTurnState.Playing, VCTurnState.AwaitPlayedAck,
+ ):
+ return True
+ except Exception:
+ pass
+ return False
+ def _vc_begin_ptt_cancel_wait(self, reason="ptt_interrupt"):
+ try:
+ self._ptt_pending_arm = 1
+ self._vc_serial_canceling = 1
+ try:
+ self._vc_cancel_deadline_ms = utime.ticks_add(
+ utime.ticks_ms(), self._vc_ptt_cancel_wait_ms())
+ except Exception:
+ self._vc_cancel_deadline_ms = 0
+ json_data = {"event": "cancel", "payload": {}}
+ self.AI_client_VC_send(ujson.dumps(json_data))
+ try:
+ self.fsm.reset_rx_phases()
+ except Exception:
+ pass
+ self._drop_tts_audio = 1
+ print("[VC][serial] cancel_wait reason={} turn={}".format(
+ reason, getattr(getattr(self, "fsm", None), "turn", "?")))
+ except Exception:
+ pass
+ def _vc_on_ptt_cancel_settled(self, via="canceled"):
+ """btn 线程:cancel 已收尾,执行挂起的 PTT 开录。"""
+ try:
+ if not getattr(self, "_vc_serial_canceling", 0) and not getattr(self, "_ptt_pending_arm", 0):
+ return
+ self._vc_serial_canceling = 0
+ self._vc_cancel_deadline_ms = 0
+ if getattr(self, "_ptt_pending_arm", 0):
+ self._ptt_pending_arm = 0
+ print("[VC][serial] ptt_arm after {}".format(via))
+ self._ptt_arm_uplink()
+ except Exception:
+ pass
+ def _vc_tick_serial_cancel_timeout(self):
+ if not getattr(self, "_vc_serial_canceling", 0):
+ return
+ try:
+ dl = getattr(self, "_vc_cancel_deadline_ms", 0)
+ if dl and utime.ticks_diff(utime.ticks_ms(), dl) >= 0:
+ print("[VC][serial] cancel_wait timeout -> arm")
+ self._vc_on_ptt_cancel_settled(via="timeout")
+ except Exception:
+ pass
+ def _ptt_arm_uplink(self, t0=0):
+ """btn 线程:cancel 已结束或无需 cancel 时,发 audio.create 并开麦。"""
+ try:
+ self.is_ruppt_tts = 0
+ except Exception:
+ pass
+ try:
+ self._ptt_audio_created = 0
+ except Exception:
+ pass
+ try:
+ self.AI_ASR_flag = 0
+ except Exception:
+ pass
+ try:
+ self.AI_client_VC_send_speech()
+ except Exception:
+ pass
+ try:
+ self.VC_Speech_Start_now()
+ except Exception:
+ pass
+ try:
+ self._vc_publish_led("listening", "ptt_begin/recording")
+ except Exception:
+ pass
+ self._ptt_end_led_suppress()
+ try:
+ if t0:
+ print("[PTT] record started (ms={})".format(utime.ticks_ms() - t0))
+ except Exception:
+ pass
+ def ptt_begin(self, reason="key.long_press"):
+ try:
+ if self.VC_mode != 1:
+ return
+ except Exception:
+ return
+
+ # PTT 新一轮开始:清理模式切换/提示音退出标志
+ try:
+ self.Close_flag = 0
+ self._ptt_await_server = 0
+ except Exception:
+ pass
+
+ t0 = 0
+ try:
+ t0 = utime.ticks_ms()
+ except Exception:
+ t0 = 0
+
+ self._ptt_enter_interrupt_arm()
+ try:
+ self._vc_publish_led("listening", "ptt_begin/pre")
+ except Exception:
+ pass
+
+ # 本地先硬静音再 VC_init,避免 DMA/队列尾包漏播
+ try:
+ self._ptt_hard_mute_local_playback()
+ except Exception:
+ pass
+ try:
+ self.VC_init()
+ 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()
+ except Exception:
+ pass
+ try:
+ self._ptt_hard_mute_local_playback()
+ except Exception:
+ pass
+
+ # 若上一轮录音尚未结束,先强制停掉,避免 record_thread 复用出错
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self.VC_Speech_End()
+ except Exception:
+ pass
+
+ # 已在等 canceled:只刷新「待开录」,勿重复 audio.create
+ try:
+ if getattr(self, "_vc_serial_canceling", 0):
+ self._ptt_pending_arm = 1
+ print("[PTT] cancel in progress, refresh pending arm")
+ self._ptt_end_led_suppress()
+ return
+ 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
+
+ # 服务端仍在思考/TTS:先 cancel,在 btn 队列收到 canceled 后再开录(方案甲串行)
+ if self._vc_ptt_need_interrupt_wait():
+ self._vc_begin_ptt_cancel_wait("ptt_begin")
+ return
+
+ self._ptt_arm_uplink(t0)
+ def _vc_led_state_debug_enabled(self):
+ try:
+ if "vc_config" in globals() and vc_config:
+ return vc_config.get_bool("LED_STATE_DEBUG", True)
+ except Exception:
+ pass
+ return True
+ def _vc_led_should_standby_idle(self):
+ """未录音且非 TTS 播中:待机/提示音/模式切换/单对话轮间应灭灯(DES-L3 idle),勿被 FSM.turn=Listening 误导。"""
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ return False
+ if self._vc_tts_playback_active():
+ return False
+ if getattr(self, "_ptt_await_server", 0) == 1:
+ return False
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (
+ VCTurnState.Thinking,
+ VCTurnState.TtsReceiving,
+ VCTurnState.Playing,
+ VCTurnState.AwaitPlayedAck,
+ ):
+ return False
+ if getattr(self, "Close_flag", 0) == 1:
+ return True
+ if getattr(self, "VC_mode", -1) == 1:
+ return True
+ k = getattr(self, "_last_tts_kind", "") or ""
+ if k in ("prompt", "boot_prompt", "mode_switch_long", "mode_switch_ptt"):
+ return True
+ except Exception:
+ pass
+ return False
+ def _vc_publish_led(self, ai_state, reason=""):
+ try:
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:
+ if ai_state != "listening":
+ if self._vc_led_state_debug_enabled():
+ print("[LED] publish blocked ai={} reason={} (ptt_arm_lock)".format(
+ ai_state, reason))
+ return
+ except Exception:
+ pass
+ try:
+ if self._vc_led_state_debug_enabled():
+ print("[LED] publish ai={} reason={} VC_mode={} rec={} turn={}".format(
+ ai_state, reason, getattr(self, "VC_mode", -1),
+ getattr(self, "VC_record_state_flag", -1),
+ getattr(getattr(self, "fsm", None), "turn", "?")))
+ except Exception:
+ pass
+ try:
+ EventMesh.publish("set_led_state", {"ai_state": ai_state})
+ except Exception:
+ pass
+ def _vc_led_sync_from_fsm(self):
+ """按 FSM/真实 TTS 播放恢复 AI 灯;本地 aud.play 短音不参与。"""
+ try:
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:
+ if getattr(self, "VC_record_state_flag", 0) == 0:
+ return
+ except Exception:
+ pass
+ try:
+ if self._vc_led_should_standby_idle():
+ self._vc_publish_led("idle", "fsm_sync/standby_idle")
+ return
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
+ self._vc_publish_led("thinking", "fsm_sync/ptt_await")
+ return
+ except Exception:
+ pass
+ try:
+ if self._vc_tts_playback_active():
+ self._vc_publish_led("playing", "fsm_sync/tts")
+ return
+ except Exception:
+ pass
+ try:
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (VCTurnState.Thinking, VCTurnState.TtsReceiving):
+ self._vc_publish_led("thinking", "fsm_sync/thinking")
+ elif t in (VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
+ # 服务端已标记播放态但本地 PCM 尚未起播:仍显示思考,勿误绿灯
+ self._vc_publish_led("thinking", "fsm_sync/await_tts")
+ elif t in (VCTurnState.Listening, VCTurnState.SpeechEnded, VCTurnState.SpeechStreaming):
+ self._vc_publish_led("listening", "fsm_sync/listening")
+ else:
+ self._vc_publish_led("idle", "fsm_sync/idle")
+ except Exception:
+ try:
+ self._vc_publish_led("idle", "fsm_sync/fallback")
+ except Exception:
+ pass
+ 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":
+ if getattr(self, "VC_mode", -1) == 1:
+ if self._ptt_should_skip_remote_playback():
+ if self._vc_led_state_debug_enabled():
+ print("[VC][serial] skip play_start (ptt_interrupt)")
+ continue
+ 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
+ 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")
+ # return
+ self.http_get_config_flag = 0
+ if self.http_get_config_flag == 1:
+ try:
+ self.http_get_config()#获取/更新配置参数
+ except Exception as e:
+ usys.print_exception(e)
+ self.VC_30S_timer = osTimer()
+ self.ai_key_press_audio = osTimer()
+ self.VC_30S_timer.start(30000, 1, self.VC_30S_timer_cb)
+ self.online_kws = 0#是否开启线上关键词
+ self.get_config_timer_count = 0
+ self.record_data_save_flag = 0 #是否保存录音文件
+ self.play_data_save_flag = 0#是否保存tts数据到文件
+ self.record_fd = None
+ self.play_fd = None
+ self.is_VC_ACK = 0 #是否开启ack应答模式
+ self.is_long_record = 0 #是否开启长录音模式(语音打断模式)
+ self.ai_OTA = OTA()
+ self.is_auth = 1 #是否开启dmp鉴权
+ #self.VC_mode = 1 #0:短按连续问答模式 1:长按录音模式
+ self.VC_mode = EventMesh.publish("get_storage_config","VC_mode")#
+ if self.VC_mode == None:
+ self.VC_mode = 0
+ try:
+ self.VC_mode = int(self.VC_mode)
+ except Exception:
+ 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
+ # 开机/提示音:本轮 started 不自动开麦(避免问候 TTS 前误进 listening)
+ self._skip_started_uplink_once = 0
+ self._last_tts_kind = ""
+ self._pending_tts_kind = ""
+ # Push-To-Talk(VAD关闭/单对话)是否已发送 audio.create
+ # 重要:协议要求 disable_vad=true 时必须 audio.create→音频帧→audio.completed
+ self._ptt_audio_created = 0
+ # PTT:松手等服务器期间保持 thinking 勿回蓝闪聆听
+ self._ptt_await_server = 0
+ # 方案甲 btnInfo 串行:打断时先 cancel,收到 canceled(或超时)后再 audio.create
+ self._vc_serial_canceling = 0
+ self._ptt_pending_arm = 0
+ self._vc_cancel_deadline_ms = 0
+ # PTT 打断:轮次号 + 开录前丢弃旧 TTS / 灯只显示聆听
+ self._ptt_arm_epoch = 0
+ self._ptt_interrupt_active = 0
+ self._ptt_led_lock_listening = 0
+ # 端侧主动发 synthesis 后到收到 tts.started 之间的窗口标志,用于阻止误开下一轮录音上行
+ self._tts_pending = 0
+ self._tts_pending_ms = 0
+ # 各类兜底的节流时间戳(避免“兜底过激/连续触发”)
+ self._fb_last_thinking_restart_ms = 0
+ self._fb_last_tts_pending_timeout_ms = 0
+ # thinking 兜底:部分情况下服务端只返回 llm.completed 而不触发 tts.started/completed,
+ # 会导致一直停留在 thinking。此时超时自动回到聆听。
+ self._llm_completed_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.wait_first_flag = 0#是否进入未读判定(只播报一次)
+ self.wait_first_timer = osTimer()#如果播放未读留言的时候未读则不要播放了
+ self.wait_first_cnt = 0
+
+ self.wait_sort = 0#未读留言正在播第几条
+ EventMesh.subscribe("play_tts",self.play_tts)
+ EventMesh.subscribe("play_new_voice",self.play_new_voice)
+ EventMesh.subscribe("play_his_voice",self.play_his_voice)
+ EventMesh.subscribe("play_wait_voice",self.play_wait_voice)
+ EventMesh.subscribe("play_single_voice",self.play_single_voice)#不带tts的播放(闹钟)
+ EventMesh.subscribe("play_music",self.play_music)#播放音乐
+ EventMesh.subscribe("play_trans_tts",self.play_trans_tts)#接收翻译tts
+ EventMesh.subscribe("set_vad_silence",self.set_vad_silence)
+ EventMesh.subscribe("set_wait_first_flag",self.set_wait_first_flag)
+ EventMesh.subscribe("kws_even",self.kws_even)
+ EventMesh.subscribe("change_volume",self.change_volume)
+ EventMesh.subscribe("find_cmd",self.find_cmd) #查询命令
+ EventMesh.subscribe("NLP_set_vad_silence",self.NLP_set_vad_silence) #设置聆听间隔
+ EventMesh.subscribe("change_voice",self.change_voice) #改变音色
+ EventMesh.subscribe("Close_VC",self.Close_VC) #外部结束对话
+ EventMesh.subscribe("get_recv_voice",self.get_recv_voice) #获取留言播放状态
+ EventMesh.subscribe("set_recv_voice",self.set_recv_voice) #设置留言播放状态
+ self.voice_timer = osTimer()#语音留言10S按下按键确定是否播放的判断定时器
+ self.voice_cnt = 0 #留言定时器计数
+ self.voicerupt_flag = 0#留言选择按键接听标志位
+ self.wait_voicerupt_flag = 0#未读留言选择按键接听标志位
+ self.recv_voice = 0 #是否收到留言
+ self.voice_notice = 0 #收到留言并播放则置为1,接下来要播放提示词
+ self.voice_choose_flag = 0#当tts和语音留言冲突时,为了防止语音地址被误清,做标志位在close做判断
+
+ self.revoice_timer = osTimer()#语音留言10S按下按键确定是否回复的判断定时器
+ self.revoice_cnt = 0 #回复留言定时器计数
+ self.revoicerupt_flag = 0#留言选择按键接听标志位
+
+ self.music_timer = osTimer()#音乐播放定时器,等待tts播完才放音乐
+ self.music_cnt = 0 #留言定时器计数
+
+ self.his_timer = osTimer()#历史留言播放定时器,等待tts播完再播留言
+ self.his_cnt = 0#历史留言定时器技术
+
+ self.wait_timer = osTimer()#未读留言播放定时器,等待tts播完再播留言
+ self.wait_cnt = 0#未读留言定时器技术
+
+ self.error_data = []
+ self.asr_taskid = ""
+ self.VC_taskid = ""
+ self.VC_server = 2 #0柒牌正式服务器,1:测试服务器1,2:测试服务器2
+ self.voice_flag = 0
+
+ #会议记录标志
+ self.meet_flag = 0
+ #AI翻译标志
+ self.trans_flag = 0
+
+ #历史留言查看判断标志
+ self.history_voice_flag = 0#公共变量判断
+ #60S等待留言判断标志
+ self.wait_voice_flag = 0#公共变量判断
+ EventMesh.publish("AI_alarm_open")#
+ self.server_ip = EventMesh.publish("get_storage_config","server_ip")#
+ if self.server_ip == None:
+ if self.VC_server == 1:#
+ self.server_ip="118.145.158.207:22083"
+ elif self.VC_server == 2:#
+ self.server_ip="118.145.158.207:22082"
+ elif self.VC_server == 4:#
+ self.server_ip="118.145.158.207:22084"
+ elif self.VC_server == 5:#
+ self.server_ip="amp-wss.raisound.com"
+ elif self.VC_server == 6:#
+ self.server_ip="118.145.158.207:22080"
+ elif self.VC_server == 7:#
+ self.server_ip="118.145.158.207:22080"
+ elif self.VC_server == 8:#
+ self.server_ip="115.190.94.208:22080"
+ elif self.VC_server == 86:#
+ self.server_ip="118.145.158.207:22086"
+ else:
+ #self.server_ip="sevenpin.qipai.com.cn"
+ self.server_ip="118.145.158.207:22082"
+ # 默认走 constants.AMP_SERVER(由 default_srv.py 统一管理,可在 usr 覆盖)
+ try:
+ if constants.AMP_SERVER:
+ self.server_ip = constants.AMP_SERVER
+ except Exception:
+ pass
+
+ #开机提示音
+ self.power_on_tone = EventMesh.publish("get_storage_config","power_on_tone")#
+ try:
+ if "vc_config" in globals() and vc_config:
+ if not vc_config.get_bool("POWER_ON_TONE_ENABLED", True):
+ self.power_on_tone = None
+ except Exception:
+ pass
+ if self.power_on_tone == None:
+ try:
+ if "vc_config" in globals() and vc_config:
+ self.power_on_tone = vc_config.get_attr("POWER_ON_TONE_DEFAULT", None)
+ except Exception:
+ pass
+
+ self.VC_record_TYPE = VC_record_TYPE_PCM #0:PCM 1:AMR
+ self.VC_play_TYPE = VC_play_TYPE_PCM #0:PCM 1:mp3
+ self.VC_record_samplerate = 16000
+ self.VC_play_samplerate = 16000
+ self.VAD_data_len = 640 #10ms数据
+ self.is_VAD = 0#=1打开vad打断
+ # ARG_sample_rate, // 采样率
+ # ARG_channel, // 通道数
+ # ARG_frame_size, // 每帧字节数
+ # ARG_energy_threshold, // 能量阈值
+ # ARG_voice_time, // 语音持续时间阈值(ms)
+ # ARG_silence_time, // 静音持续时间阈值(ms)
+ # ARG_silence_start_threshold, // 静音开始阈值(ms)
+ self.vad = None
+ if self.is_VAD == 1:
+ self.vad = VAD.VAD(self.VC_record_samplerate ,1,self.VAD_data_len,600,100,300,100)
+ self.speech_rate = EventMesh.publish("get_storage_config","speech_rate")#语速配置
+ if self.speech_rate == None:
+ self.speech_rate = 0
+
+ self.vad_silence = EventMesh.publish("get_storage_config","vad_silence")#语速配置
+ if self.vad_silence == None:
+ try:
+ if "vc_config" in globals() and vc_config:
+ self.vad_silence = vc_config.get_int("VAD_SILENCE_DEFAULT", 300)
+ else:
+ self.vad_silence = 300
+ except Exception:
+ self.vad_silence = 300
+
+ self.VC_TTS_voice_type = EventMesh.publish("get_storage_config","VC_TTS_voice_type")#音色配置
+ if self.VC_TTS_voice_type == None :
+ try:
+ if "vc_config" in globals() and vc_config:
+ cfg = vc_config.get_attr("VC_TTS_VOICE_TYPE_DEFAULT", None)
+ if isinstance(cfg, dict):
+ self.VC_TTS_voice_type = cfg
+ except Exception:
+ pass
+ # 若配置未提供,沿用原逻辑内置兜底(保持兼容)
+ if self.VC_TTS_voice_type == None:
+ self.VC_TTS_voice_type = {
+ "女":{"bot_name":"柒妹妹","voice":"zh_female_wanwanxiaohe_moon_bigtts","lang":"zh-cn"},
+ "男":{"bot_name":"柒先生","voice":"zh_male_wennuanahu_moon_bigtts","lang":"zh-cn"},
+ "童":{"bot_name":"柒乐乐","voice":"ICL_zh_female_huoponvhai_tob","lang":"zh-cn"},
+ }
+
+ # self.VC_TTS_voice_type = {0:"zh_female_xinlingjitang_moon_bigtts",1:"zh_male_wennuanahu_moon_bigtts",2:"ICL_zh_female_huoponvhai_tob",
+ # 3:"aliyun::longjiaxin",4:"zh_female_daimengchuanmei_moon_bigtts"}
+ #self.VC_TTS_voice_type = ["aliyun::longxiaoxia","aliyun::longnan","aliyun::longling"]
+
+ #EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
+
+ try:
+ self.VC_TTS_voice_key = EventMesh.publish("get_storage_config","VC_TTS_voice_key")#音色配置
+ if self.VC_TTS_voice_key == None :
+ try:
+ if "vc_config" in globals() and vc_config:
+ self.VC_TTS_voice_key = vc_config.get_attr("VC_TTS_VOICE_KEY_DEFAULT", None)
+ except Exception:
+ pass
+ if not self.VC_TTS_voice_key:
+ self.VC_TTS_voice_key = "男"
+ if self.VC_TTS_voice_key in self.VC_TTS_voice_type:
+ pass
+ else:
+ self.VC_TTS_voice_key = list(self.VC_TTS_voice_type.keys())[0]
+ print("音色配置:{}:{}\r\n\r\n".format(self.VC_TTS_voice_key,self.VC_TTS_voice_type[self.VC_TTS_voice_key]))
+ self.VC_TTS_volume = EventMesh.publish("get_storage_config","VC_TTS_volume")#音量配置
+ except Exception as e:
+ usys.print_exception(e)
+ #配置词
+ self.commands_mode = 2 #=1用大模型判断命令,=0用服务器本地插件判断命令 =2用设备本地判断方式
+ self.commands = []
+ if self.VC_TTS_volume == None:
+ self.VC_TTS_volume = 100
+ #可ota配置的参数变量
+ self.emotion = EventMesh.publish("get_storage_config","emotion")#情绪
+ if self.emotion == None:
+ self.emotion = ""
+ self.ota_update_notice = EventMesh.publish("get_storage_config","ota_update_notice")#OTA升级提示语
+ if self.ota_update_notice == None:
+ self.ota_update_notice = ""
+ self.AI_audio_queue_thread_id = 0
+ self.AI_audio_queue_thread_lock = 0
+ self.AI_VC_send_data_queue_thread_id = 0
+ self.AI_VC_send_data_queue_thread_lock = 0
+ self.AI_ASR_send_data_queue_thread_id = 0
+ self.AI_ASR_send_data_queue_thread_lock = 0
+ self.AI_record_tmp_queue = Queue(100)
+ self.AI_ASR_send_data_queue = Queue(200)
+ self.AI_VC_send_data_queue = Queue(100)
+ self.AI_audio_queue = Queue(100)
+ #self.AI_WS_connect_Queue = Queue(100) #释放用
+ self.AI_TTS_str = ""
+ self.AI_ASR_str = ""
+ self.AI_ASR_flag = 0
+ self.AI_ASR_str_tmp = ""
+ self.AI_ASR_send_buf_len = 0
+ self.AI_ASR_text_tmp = ""
+ self.AI_ASR_text_repet_count = 0
+ # #开始线程
+ # if self.AI_audio_queue_thread_id ==0:
+ # 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 ")
+ # if self.AI_VC_send_data_queue_thread_id == 0:
+ # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())
+
+ self.VC_dialog_id = ""#
+ self.VC_message_id=None
+ self.VC_task_id =None
+ self.AI_client_VC = None
+ self.AI_client_VC_recv_id = 0
+ self.AI_client_VC_recv_lock = 0
+ self.AI_client_VC_isconnect =0
+ self.AI_client_VC_debug =0
+
+ self.asr_state = 0 # 预留:ASR 侧状态(当前未参与分支)
+ self.key_state = 0
+
+ self.AI_client_asr = None
+ self.AI_client_asr_recv_id = 0
+ self.AI_client_asr_recv_lock = 0
+ self.AI_client_asr_isconnect =0
+ self.AI_ASR_timer = osTimer()
+ self.AI_ASR_timer_count = 0
+ self.AI_client_asr_debug =0
+ self.AI_ASR_check_timeout = 0
+ self.AI_ASR_error_timeout = 0
+ self.VC_Thinking_timerout = 0
+
+
+ self.record_pcm = None
+ self.play_pcm = None
+ self.VC_record_thread_id = 0
+ self.VC_record_thread_lock = 0
+ self.VC_pingping_timerout = 0
+ self.asr_pingping_timerout = 0
+ self.factory_timer = osTimer()
+ self._1S_timer = osTimer()
+ self.Audio_buf_size = 20*1024
+ self.record = audio.Record()
+ #self.record.end_callback(self.record_callback)
+ self.record.gain_set(4,10)
+ #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._idle_deep_sec = 0
+ self._vc_idle_deep_armed_suppress = 0
+ self.LocalRespondingEnded_timerout = 0
+
+ self.VC_Play_flag = 0
+ self.play_flag = 0
+ self.VC_record_state_flag = 0
+ self.AI_key_long_press_flag = 0
+ self._asr_completed_tone_done = 0 # asr.completed 本地 think 提示每轮仅一次
+ self._last_asr_result_text = "" # 保留最近 asr.result,防 VC_Speech_End 早于 asr.completed 清空
+ self._vc_last_auto_uplink_ms = 0 # 长对话自动开麦防抖
+ self._vc_local_play_active = 0 # aud.play 本地短音占用(与采集互斥)
+ self._vc_local_play_until_ms = 0
+ self._vc_pending_interrupt_uplink = 0 # 长对话短按打断:等 canceled 再开麦
+ self.websocket_error_count = 0
+ self.AI_new_voice_url = "" #未读语音地址/音乐播放地址/闹钟播放标志
+ self.music_play_flag = 0 #音乐播放标志
+ self.alarm_play_flag = 0 #闹钟播放标志
+ self.tts_in_data_len = 0
+ self.tts_out_data_len = 0
+ self._tts_first_write_ms = 0
+ self._tts_completed_wall_ms = 0
+ self._tts_play_check_log_ms = 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
+ self.session_id = ""
+ self.stop_voice_timer=osTimer()
+ self.pwk_cnt_voice = 0 #不听语音后,积攒的次数,需要一起播放
+ self.pwk_notice_flag = 0#是否处于积攒留言状态(用来实现单次提醒)
+ self.is_noread_voice = EventMesh.publish("get_storage_config","is_noread_voice")
+ if self.is_noread_voice == None:
+ self.is_noread_voice = 0#是否还有未读留言(开机判断)
+ self.is_ruppt_tts = 0#是否已经打断tts播放
+ EventMesh.subscribe("AI_key_press", self.AI_key_press)
+ EventMesh.subscribe("BT_AI_key_press", self.BT_AI_key_press)
+ EventMesh.subscribe("stop_AI_record", self.stop_AI_record)
+ EventMesh.subscribe("AI_key_release", self.AI_key_release)
+ EventMesh.subscribe("pwk_key_double_press", self.pwk_key_double_press)
+ EventMesh.subscribe("AI_key_long_press", self.AI_key_long_press)
+ 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("get_vc_tts_playback_active", self.get_vc_tts_playback_active)
+ EventMesh.subscribe("battery_priority_mute_vc", self.battery_priority_mute_vc)
+ EventMesh.subscribe("VC_error", self.VC_error)
+ EventMesh.subscribe("audio_test", self.audio_test)
+ EventMesh.subscribe("audio_test_close", self.audio_test_close)
+ EventMesh.subscribe("set_record_data_save_flag", self.set_record_data_save_flag)
+ EventMesh.subscribe("set_VC_TTS_voice_type", self.set_VC_TTS_voice_type)
+ EventMesh.subscribe("SET_TTS_VOLUME", self.set_tts_volume)
+ EventMesh.subscribe("set_VC_stop_play_audio_flag", self.set_VC_stop_play_audio_flag)
+ EventMesh.subscribe("set_yuyinaudio_flag",self.set_yuyinaudio_flag)
+ EventMesh.subscribe("get_yuyinaudio_flag",self.get_yuyinaudio_flag)
+ EventMesh.subscribe("set_pwk_cnt_voice",self.set_pwk_cnt_voice)
+ EventMesh.subscribe("get_pwk_cnt_voice",self.get_pwk_cnt_voice)
+ EventMesh.subscribe("exit_voice_message_mode",self.exit_voice_message_mode) #超时未退出语音留言模式
+ EventMesh.subscribe("come_meet_mode",self.come_meet_mode)#进入会议模式
+ EventMesh.subscribe("exit_meet_mode",self.exit_meet_mode)#退出会议模式
+ EventMesh.subscribe("come_trans_mode",self.come_trans_mode)#进入翻译模式
+ EventMesh.subscribe("exit_trans_mode",self.exit_trans_mode)#退出翻译模式
+ EventMesh.subscribe("set_trans_flag",self.set_trans_flag)#设置翻译状态
+ EventMesh.subscribe("get_audio_play_state",self.get_audio_play_state)
+ EventMesh.subscribe("set_voicerupt_flag",self.set_voicerupt_flag)#按键中断触发留言播放
+ EventMesh.subscribe("set_wait_voicerupt_flag",self.set_wait_voicerupt_flag)#按键中断触发未读留言播放
+ EventMesh.subscribe("get_recv_voice",self.get_recv_voice)#获取是否收到留言
+ EventMesh.subscribe("rate_change",self.rate_change)#直接修改语速
+ EventMesh.subscribe("get_history_voice_flag",self.get_history_voice_flag)#获取查找历史留言状态
+ EventMesh.subscribe("set_history_voice_flag",self.set_history_voice_flag)#修改查找历史留言状态
+ EventMesh.subscribe("clean_unread_vocie",self.clean_unread_vocie)#修改查找历史留言状态
+ EventMesh.subscribe("get_wait_voice_flag",self.get_wait_voice_flag)#获取60S等待留言播放定时器状态
+ EventMesh.subscribe("set_wait_voice_flag",self.set_wait_voice_flag)#修改60S等待留言播放定时器状态
+ EventMesh.subscribe("rate_move",self.rate_move)#NLP修改语速
+ EventMesh.subscribe("find_rate",self.find_rate)#NLP查询语速
+ EventMesh.subscribe("set_pwk_notice_flag",self.set_pwk_notice_flag)#设置未读状态
+ # EventMesh.subscribe("get_voicerupt_flag",self.get_voicerupt_flag)#获取留言播放状态
+ EventMesh.subscribe("chenggong_tone",self.chenggong_tone)
+
+ pass
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ self.dialog_attributes_provider = EventMesh.publish("get_storage_config","dialog_attributes_provider")#智能体类型配置
+ if self.dialog_attributes_provider == None:
+ self.dialog_attributes_provider = "coze"
+ self.outbound_attributess_provider = EventMesh.publish("get_storage_config","outbound_attributess_provider")#tts服务选择
+ if self.outbound_attributess_provider == None:
+ self.outbound_attributess_provider = "volcengine"
+ self.inbound_attributess_provider = EventMesh.publish("get_storage_config","inbound_attributess_provider")#tts服务选择
+ if self.inbound_attributess_provider == None:
+ self.inbound_attributess_provider = "volcengine"
+
+ self.bot_name = "柒先生"
+ try:
+ if "bot_name" in self.VC_TTS_voice_type[self.VC_TTS_voice_key]:
+ self.bot_name = self.VC_TTS_voice_type[self.VC_TTS_voice_key]["bot_name"]
+ else:
+ print("not bot_name!!!!!!!!!!!!!!!!!!!!!!")
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ bt_ota_flag=0
+ try:
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("BT_OTA_ENABLED", False):
+ bt_ota_flag = self.ai_OTA.bt_ota()#先检查蓝牙是否要升级
+ except Exception:
+ pass
+
+ cat1_ota_flag = self.ai_OTA.min_sys_ota()
+ if bt_ota_flag==1 or cat1_ota_flag == 1:
+ #if 0:
+ if self.ota_update_notice != "":
+ self.AI_TTS_str = self.ota_update_notice
+ else:
+ self.AI_TTS_str = "检测到有新版本需要升级啦!升级时可千万别断电或乱按哦,升级完成后设备会自动重启,重启就代表升级成功"
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ self.Close_flag = 1
+ # if self.ai_OTA.min_sys_ota()==2:
+ # self.AI_TTS_str = "检测到有新版本需要升级啦!电量低于40%,无法保证升级正常执行,请充满电后重启设备再次升级!"
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ else:
+ try:
+ self.ai_OTA.ota_result()
+ self.ai_OTA.pots_ICCID()
+ except Exception as e:
+ usys.print_exception(e)
+ #开机提示语,如果联网失败,直接提示信号弱
+
+
+ #EventMesh.publish("get_storage_config",["powerRestart",1])
+ powerRestart = EventMesh.publish("get_storage_config","powerRestart")#=1非正常重启
+ if powerRestart == None:
+ powerRestart = 0
+ if powerRestart == 1:
+ EventMesh.publish("set_storage_config",["powerRestart",0])
+
+ else:
+ pass
+ self.AI_TTS_str = self.power_on_tone.replace("bot_name", self.bot_name)
+ print("开机提示:{}".format(self.AI_TTS_str))
+ try:
+ self._pending_tts_kind = "boot_prompt"
+ self._last_tts_kind = "boot_prompt"
+ self._skip_started_uplink_once = 1
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+
+ except Exception as e:
+ usys.print_exception(e)
+ def set_pwk_notice_flag(self,event,msg):#设置未读状态
+ self.pwk_notice_flag = msg
+ def get_wait_voice_flag(self,event,msg):
+ return self.wait_voice_flag
+ def set_wait_voice_flag(self,event,msg):
+ # print("未读留言标记修改为:{}".format(msg))
+ self.wait_voice_flag = msg
+ def get_history_voice_flag(self,event,msg):
+ return self.history_voice_flag
+ def set_history_voice_flag(self,event,msg):
+ # print("历史留言标记修改为:{}".format(msg))
+ self.history_voice_flag = msg
+ def clean_unread_vocie(self,event,msg):
+ self.pwk_cnt_voice = 0
+ self.pwk_notice_flag = 0
+ EventMesh.publish("set_app_new_voice_flag",0)
+ def audio_test(self,event = None, msg = None):
+ self._uplink_request_start("audio_test")
+ self.VC_Play_Start()
+ def audio_test_close(self,event = None, msg = None):
+ self._uplink_request_stop("audio_test_close")
+ self.VC_Play_End()
+ if self.record_data_save_flag == 1:
+ if self.record_fd :
+ self.record_fd.close()
+ self.record_fd = None
+ def post_processor_after_instantiation(self):
+ pass
+ def get_VC_satrt_flag(self, event, msg):
+ return self.VC_satrt_flag
+ def check_vc_start(self):
+ print("check_vc_start:{} {} {} {}".format(self.VC_satrt_flag,self.VC_record_state_flag,self.AI_client_VC_isconnect,self.VC_Play_flag))
+ 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()
+ def VC_30S_timer_cb(self, *args): #
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_30S_timer_cb_})
+ def VC_30S_timer_cb_(self):
+ self.get_config_timer_count += 1
+ if self.get_config_timer_count >= 2*60*2:#2小时更新配置参数
+ self.get_config_timer_count = 0
+ if self.http_get_config_flag == 1:
+ try:
+ self.http_get_config()#获取/更新配置参数
+ except Exception as e:
+ usys.print_exception(e)
+ self.check_vc_start()#30秒检查一下有没有关闭
+ def ud_error(self,error_data):
+ response = None
+ if self.is_up_error == 1:
+ try:
+ # currentVersion = versions.Build_Time
+ # currentVersion = "7PIN-"+currentVersion[:10]+"-"+currentVersion[11:-3]
+ url = "http://{}/health?task_id={}".format(self.server_ip,error_data[0]+error_data[1])
+ print(url)
+ response = request.get(url,timeout=5)
+ if response.status_code == 200:
+
+ print("ud_error:ok")
+ else:
+ print("ud_error error!!!!")
+ for i in response.text:
+ print(i)
+ response.close()
+ except Exception as e:
+ usys.print_exception(e)
+ if response != None:
+ response.close()
+ def http_get_config(self, event=None, msg=None):
+ print("http_get_config")
+
+ response = None
+ #http://180.184.206.142/bz-api/dev/config?deviceId=xxx
+ try:
+ url = (constants.DMP_BASE + "/dev/config/{}").format(modem.getDevImei())
+
+
+ print(url)
+ response = request.get(url,timeout=5)
+ if response.status_code == 200:
+ data = response.json()
+ print("http_get_config:{}".format(data))
+
+ if data:
+ try:
+ if data["code"] == 20000:#有配置
+ if data["data"] :
+ if "version" in data["data"]:
+ config_version = EventMesh.publish("get_storage_config","config_version")#音量配置
+ if config_version != data["data"]["version"]:
+ config_version = data["data"]["version"]
+ EventMesh.publish("set_storage_config",["config_version",config_version])
+ if "VC_TTS_voice_type" in data["data"]:
+ self.VC_TTS_voice_type = data["data"]["VC_TTS_voice_type"]
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_type",self.VC_TTS_voice_type])
+ print("配置VC_TTS_voice_type成功")
+ if "server_ip" in data["data"]:
+ self.server_ip = data["data"]["server_ip"]
+ EventMesh.publish("set_storage_config",["server_ip",self.server_ip])
+ print("配置server_ip成功")
+ if "power_on_tone" in data["data"]:
+ self.power_on_tone = data["data"]["power_on_tone"]
+ EventMesh.publish("set_storage_config",["power_on_tone",self.power_on_tone])
+ print("配置power_on_tone成功")
+ if "dialog_attributes_provider" in data["data"]:
+ self.dialog_attributes_provider = data["data"]["dialog_attributes_provider"]
+ EventMesh.publish("set_storage_config",["dialog_attributes_provider",self.dialog_attributes_provider])
+ if "outbound_attributess_provider" in data["data"]:
+ self.outbound_attributess_provider = data["data"]["outbound_attributess_provider"]
+ EventMesh.publish("set_storage_config",["outbound_attributess_provider",self.outbound_attributess_provider])
+ print("配置outbound_attributess_provider成功")
+ if "inbound_attributess_provider" in data["data"]:
+ self.inbound_attributess_provider = data["data"]["inbound_attributess_provider"]
+ EventMesh.publish("set_storage_config",["inbound_attributess_provider",self.inbound_attributess_provider])
+ print("配置inbound_attributess_provider成功")
+ if "speech_rate" in data["data"]:
+ self.speech_rate = EventMesh.publish("SET_SPEECH_RATE",float(data["data"]["speech_rate"]))
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
+ print("配置speech_rate成功")
+ if "emotion" in data["data"]:
+ self.emotion = data["data"]["emotion"]
+ EventMesh.publish("set_storage_config",["emotion",self.emotion])
+ print("配置emotion成功")
+ if "ota_update_notice" in data["data"]:
+ self.ota_update_notice =data["data"]["ota_update_notice"]
+ EventMesh.publish("set_storage_config",["ota_update_notice",self.ota_update_notice])
+ print("配置ota_update_notice成功")
+ if "VC_TTS_volume" in data["data"]:
+ self.VC_TTS_volume =int(data["data"]["VC_TTS_volume"])
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
+ print("配置VC_TTS_volume成功")
+
+ except Exception as e:
+ usys.print_exception(e)
+ else:
+ print("status_code:{}".format(response.status_code))
+ print("http_get_config:error!!!!!!!!!!")
+ response.close()
+ except Exception as e:
+ usys.print_exception(e)
+ if response != None:
+ response.close()
+ def get_yuyinaudio_flag(self, event=None, msg = None):
+ return self.voice_flag
+ def set_yuyinaudio_flag(self, event=None, msg = None):
+ self.voice_flag=0
+ def get_pwk_cnt_voice(self, event=None, msg = None):
+ return self.pwk_cnt_voice
+ def set_pwk_cnt_voice(self, event=None, msg = None):
+ self.pwk_cnt_voice=msg
+ print("设置未读次数为:{}".format(self.pwk_cnt_voice))
+ def get_meetaudio_flag(self, event=None, msg = None):
+ return self.meet_flag
+ def set_meetaudio_flag(self, event=None, msg = None):
+ self.meet_flag = 0
+ def come_meet_mode(self, event=None, msg = None):
+ if self.meet_flag == 0: #还是为0就提示进入会议纪要模式
+ EventMesh.publish("send_meet_open_flag_to_iot")#创建会议
+ self.meet_flag=1
+ exit_tts ="开启会议纪要,正在录音"
+ EventMesh.publish("play_tts",[exit_tts,1])
+ utime.sleep_ms(4000)
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_key_press})
+ pass
+ def exit_meet_mode(self, event=None, msg = None):
+ if self.meet_flag == 1: #还是为1就提示已退出会议纪要模式
+ EventMesh.publish("send_meet_end_flag_to_iot")#结束会议
+ self.meet_flag=0
+ EventMesh.publish("stop_voice_record") #关闭语音录音
+ exit_tts ="录音结束,已退出会议纪要模式"
+ EventMesh.publish("play_tts",[exit_tts,1])
+ pass
+ def come_trans_mode(self, event=None, msg = None):
+ if self.trans_flag == 0: #还是为0就提示进入AI翻译模式
+ EventMesh.publish("send_trans_come_flag_to_iot")#进入翻译
+ pass
+ def exit_trans_mode(self, event=None, msg = None):
+ if self.trans_flag == 1: #还是为1就提示已退出AI翻译模式
+ EventMesh.publish("stop_voice_record") #关闭语音录音
+ exit_tts ="已退出AI翻译模式"
+ EventMesh.publish("play_tts",[exit_tts,1])
+ self.trans_flag=0
+ EventMesh.publish("send_trans_end_flag_to_iot")#退出翻译
+ pass
+ def set_trans_flag(self,event,msg):
+ self.trans_flag = msg
+ def exit_voice_message_mode(self, event=None, msg = None):
+ if self.voice_flag == 1: #还是为1就提示已退出会议纪要模式
+ self.voice_flag=0
+ exit_tts ="已退出语音留言模式"
+ EventMesh.publish("play_tts",[exit_tts,1])
+ pass
+ def ai_key_press_audio_cb1(self, event=None, msg = None):
+ print("播放按键音!!!!!!!!!!")
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
+ # self.chenggong_tone()
+ self.aud.aud_tone_play(10,400)#播放按键音
+ def ai_key_press_audio_cb2(self, event=None, msg = None):
+ EventMesh.publish("audio_montage_play",["chenggong.mp3"])
+ def AI_key_press(self, event=None, msg = None):#按键按下
+ # 按键时打印 LED 聚合状态/最终模式(用于定位“绿灯不灭/闪一下”)
+ try:
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
+ try:
+ led = EventMesh.publish("get_led_debug_state")
+ except Exception:
+ led = None
+ try:
+ t = getattr(self.fsm, "turn", None)
+ except Exception:
+ t = None
+ print("[KEY] AI_key_press VC_mode={} {} turn={} led={}".format(
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
+ except Exception:
+ pass
+ # 单对话 PTT / 长对话分支(按键短按提示音已关,避免与开录等提示音叠在一起)
+ try:
+ if self._ai_key_press_ptt_mode():
+ return
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ if self._ai_key_press_long_dialogue_interrupt():
+ return
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ if self._ai_key_press_long_dialogue_when_session_ready():
+ return
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ if self._ai_key_press_long_dialogue_wake():
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)
+ return
+ except Exception as e:
+ usys.print_exception(e)
+
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ if EventMesh.publish("get_sos_flag")== 1:
+ return
+ if self.voice_flag == 1: #语音留言模式
+ self.stop_voice_timer.stop() #关闭语音留言模式定时器
+ # EventMesh.publish("stop_AI_record")
+ self.Close()
+ self.revoice_cnt = 0
+ self.revoice_timer.stop()#回复定时器关闭,计数器清零
+ EventMesh.publish("start_voice_record",2)#开始语音录音
+ return
+ elif self.meet_flag == 1:#会议纪要模式
+ self.Close()
+ EventMesh.publish("start_voice_record",3)#开始会议录音
+ return
+ elif self.trans_flag == 1:#AI翻译模式
+ return
+
+ EventMesh.publish("stop_voice_record") #关闭语音录音
+ if self.VC_mode == 0:
+ print("[KEY] AI_key_press fallback -> AI_VC_start ws={} started={}".format(
+ getattr(self, "AI_client_VC_isconnect", -1),
+ getattr(self, "_vc_session_started", -1)))
+ try:
+ self.Close_flag = 0
+ except Exception:
+ pass
+ self.get_config_timer_count = 0
+ self.key_state = 1
+ self.AI_TTS_str = ""
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ self.AI_key_long_press_flag = 0
+ return
+ print("AI_key_press: unhandled mode branch VC_mode={}".format(
+ getattr(self, "VC_mode", -1)))
+ self.AI_key_long_press_flag = 0
+ def BT_AI_key_press(self, event=None, msg = None):#按键按下
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ if EventMesh.publish("get_sos_flag")== 1:
+ return
+ if self.voice_flag == 1: #语音留言模式
+ tts_str = "语音留言模式,长按按钮开始录音,松开按钮结束"
+ EventMesh.publish("play_tts",[tts_str,1])
+ return
+
+ EventMesh.publish("stop_voice_record") #开启静默需要关闭
+ print("AI_key_press")
+ self.get_config_timer_count = 0
+ self.key_state = 1
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
+ # self.chenggong_tone() #播放按键音
+ # 长对话:蓝牙按键同单按
+ # - 对话进行中:打断/续聊(复用 dialog_id)
+ # - 已停止/未 started:走新会话流程
+ try:
+ if self.VC_mode == 0:
+ if getattr(self, "AI_client_VC_isconnect", 0) == 1 and getattr(self, "_vc_session_started", 0) == 1:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_client_VC_RequestToSpeak})
+ return
+ except Exception:
+ pass
+
+ self.Close()
+
+ if self.VC_mode == 1: #长按对话模式 接收到蓝牙耳机按键事件
+
+ tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+ EventMesh.publish("play_tts",[tts_str,1])
+ return
+ # if dev_type == "7PIN":
+ # self.chenggong_tone()
+ #self.aud.aud_tone_play(10,200)#播放按键音
+ self.AI_TTS_str = ""#"你好,有什么吩咐"
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ # 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)})
+ self._uplink_request_stop("BT_AI_key_press/compat2")
+ self._uplink_request_start("BT_AI_key_press/compat2")
+
+ self.AI_VC_start()
+
+
+ # self.Close()
+ # self.AI_VC_start()
+ self.AI_key_long_press_flag = 0
+ def stop_AI_record(self, event=None, msg=None):#停止AI对话
+ if self.VC_satrt_flag == 1:
+ self.get_config_timer_count = 0
+ self.AI_TTS_str = ""#"你好,有什么吩咐"
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ self.AI_key_long_press_flag = 0
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ self.request_uplink_stop("stop_AI_record")
+ count = 0
+ while True:
+ if count >= 20 or self.VC_satrt_flag == 0:
+ break
+ count+=1
+ utime.sleep_ms(500)
+ def exit_voice_message_timer(self,*argc):
+ EventMesh.publish("exit_voice_message_mode")
+ def AI_key_release(self, event=None, msg=None):#按键弹起
+ try:
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
+ try:
+ led = EventMesh.publish("get_led_debug_state")
+ except Exception:
+ led = None
+ try:
+ t = getattr(self.fsm, "turn", None)
+ except Exception:
+ t = None
+ print("[KEY] AI_key_release VC_mode={} {} turn={} led={}".format(
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
+ except Exception:
+ pass
+ print("AI_key_release123")
+ self.key_state = 0
+ if self.trans_flag == 1:#AI翻译模式
+ EventMesh.publish("stop_voice_record") #关闭语音录音
+ return
+ if self.voice_flag == 1:
+ self.stop_voice_timer.stop()
+ self.voice_flag=0
+ if EventMesh.publish("GET_USB_STATE") == 0:
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ EventMesh.publish("stop_voice_record")#停止静默录音
+ tts_play_str = "留言已发送"
+ self.voice_flag=0
+ EventMesh.publish("play_tts",[tts_play_str,1])
+ return
+ if self.VC_mode == 1:
+ self._ai_key_release_handle_ptt()
+ return
+ def pwk_key_double_press(self,event,msg):#pwk双击
+ print("come 双击")
+ # 切换模式:必须打断当前对话/录音/播放,回到 idle,等待按键触发新一轮
+ try:
+ self.Close_flag = 1
+ except Exception:
+ pass
+ try:
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.Close})
+ except Exception:
+ pass
+ 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
+ # 提示音播完即退出(Close_flag=1),保持 idle,等待按键
+ 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):#按键长按
+ try:
+ if "vc_config" in globals() and vc_config and vc_config.get_bool("KEY_LED_DEBUG", False):
+ try:
+ led = EventMesh.publish("get_led_debug_state")
+ except Exception:
+ led = None
+ try:
+ t = getattr(self.fsm, "turn", None)
+ except Exception:
+ t = None
+ print("[KEY] AI_key_long_press VC_mode={} {} turn={} led={}".format(
+ getattr(self, "VC_mode", -1), self._vc_fsm_log_snip(), t, led))
+ except Exception:
+ pass
+ print("AI_key_long_press")
+ self._ai_key_long_press_route(event, msg)
+ def pwk_key_release(self, event, msg):#电源键短按,关闭
+ try:
+ if "vc_config" in globals() and vc_config:
+ _m = bool(vc_config.get_bool("SINGLE_POWER_KEY_AI_MERGED", False))
+ if not _m:
+ _ai = vc_config.get_attr("EXTINT_AI_GPIO", "GPIO22")
+ _m = _ai is None or _ai == ""
+ else:
+ _m = False
+ except Exception:
+ _m = False
+ if _m:
+ self._handle_pwk_short_release(event, msg, merged_ai_tail=True)
+ return
+ self._handle_pwk_short_release(event, msg, merged_ai_tail=False)
+ def key_prompt(self):
+ EventMesh.publish("audio_montage_play",["key_prompt.mp3"])
+ def VC_error(self, event, msg):#错误播报
+ self.websocket_error()
+ def set_voicerupt_flag(self,event,msg):#按键控制当前留言播放
+ self.voicerupt_flag = msg
+ def set_wait_voicerupt_flag(self,event,msg):#按键控制未读留言播放
+ self.wait_voicerupt_flag = msg
+ def get_voicerupt_flag(self,event,msg):#获取按键控制留言播放状态
+ return self.voicerupt_flag
+ 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:
+ try:
+ self.fsm.reset_rx_phases()
+ except Exception:
+ pass
+ 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)
+ restart_threshold_ms = 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
+ try:
+ self.fsm.reset_rx_phases()
+ except Exception:
+ pass
+ try:
+ print("[VC][thinking_timeout] AI_VC_start age_ms={} thr={}".format(
+ age_ms, restart_threshold_ms))
+ except Exception:
+ pass
+ self._vc_led_alert_rb_flash()
+ # thinking_timeout 策略:重启会话(而不是抢开麦),避免干扰服务端 LLM/TTS 流程
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.AI_VC_start})
+ except Exception:
+ pass
+ def _1S_timer_cb_(self):
+ try:
+ self._vc_tick_serial_cancel_timeout()
+ except Exception:
+ pass
+
+ EventMesh.publish("system_sleep_rest")#防止黑屏
+ # --- Level 1:仅长对话(VC_mode==0),无业务下行 idle_sec 后结束会话(需再次按键进入)
+ # close_timerout:保活清除策略见 _vc_rx_resets_long_idle_timer(排除文本 ping/pong、heartbeat 等)
+ # 与浅休眠/灭屏解耦;单对话不走本计时
+ idle_sec = 20
+ try:
+ if "vc_config" in globals() and vc_config:
+ idle_sec = int(vc_config.get_int("VC_IDLE_AUTO_CLOSE_SEC", 20))
+ except Exception:
+ idle_sec = 20
+ if idle_sec < 5:
+ idle_sec = 5
+ if self.VC_mode == 0 and self.VC_satrt_flag == 1 and getattr(self, "AI_client_VC_isconnect", 0) == 1:
+ self.close_timerout += 1
+ else:
+ self.close_timerout = 0
+ if self.VC_mode == 0 and idle_sec > 0 and self.close_timerout > idle_sec:
+ try:
+ print("[VC][idle_L1] 长对话无业务下行超时: 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()))
+ except Exception:
+ pass
+ if self._vc_long_idle_active_speech_or_later():
+ try:
+ print("[VC][idle_L1] 处理=websocket_error(对话中无服务端响应)")
+ except Exception:
+ pass
+ print("[VC][long_idle] branch=speech_no_rsp -> websocket_error + ud_error")
+ print("接口无响应,超时退出")
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_error,"data":"接口超时了。请重试"})
+ self.ud_error(["VC",self.VC_taskid])
+ else:
+ try:
+ print("[VC][idle_L1] 处理=Close(结束对话,需再次按键进入)")
+ except Exception:
+ pass
+ print("[VC][long_idle] branch=idle_timeout -> Close")
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ #EventMesh.publish("sleep_BT")#休眠蓝牙
+
+ # --- Level 2:长对话+单对话,业务 idle 累计 VC_IDLE_DEEP_SLEEP_SEC → 断开(若仍连)+ 深睡事件
+ deep_sec = 60
+ deep_en = 1
+ try:
+ if "vc_config" in globals() and vc_config:
+ deep_sec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
+ deep_en = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
+ except Exception:
+ deep_sec, deep_en = 60, 1
+ active_deep = bool(deep_en) and deep_sec > 0
+ if active_deep and deep_sec < 10:
+ deep_sec = 10
+ if not active_deep:
+ self._idle_deep_sec = 0
+ elif not self._vc_is_business_idle():
+ self._idle_deep_sec = 0
+ try:
+ self._vc_idle_deep_armed_suppress = 0
+ except Exception:
+ pass
+ elif getattr(self, "_vc_idle_deep_armed_suppress", 0):
+ pass
+ else:
+ 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)))
+ except Exception:
+ pass
+ try:
+ self._vc_idle_deep_armed_suppress = 1
+ except Exception:
+ pass
+ self._idle_deep_sec = 0
+ EventMesh.publish("publish_AI_btnInfo", {"func": self._vc_idle_deep_sleep_impl})
+
+ # 统一兜底表(按状态机状态触发)
+ 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))
+ # ===== 播放结束检测:用 FSM 驱动 play_end 动作链 =====
+ try:
+ self._vc_try_play_end_tick()
+ 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 内统一处理
+ # VC WebSocket 文本 ping:间隔由 default_config 的 VC_WS_PING_INTERVAL_SEC 控制(秒),<=0 表示关闭 ping
+ try:
+ _ping_iv = int(vc_config.get_int("VC_WS_PING_INTERVAL_SEC", 4))
+ except Exception:
+ _ping_iv = 4
+ if _ping_iv > 0:
+ self.VC_pingping_timerout += 1
+ if self.VC_pingping_timerout >= _ping_iv:
+ self.VC_pingping_timerout = 0
+ self.AI_client_VC_ping()
+ # self.asr_pingping_timerout += 1
+ # if self.asr_pingping_timerout>3:
+ # self.asr_pingping_timerout=0
+ # self.AI_client_asr_ping()
+ # 历史遗留: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_VC_start(self):
+ print("AI_VC_start1")
+
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ self.VC_satrt_flag = 1
+ self.VC_stop_play_audio_flag = 1
+ self.VC_satrt_kws_flag = 1
+ if self.VC_Thinking_timerout < 100:
+ self.VC_Thinking_timerout = 0
+ # 不在这里提前切灯态:播放/聆听灯只在“播放/录音真正开始/停止”处更新
+ EventMesh.publish("wakeup_BT")
+ #self.VC_Play_End()#关闭音频播放
+ print("AI_VC_start2")
+ #self.aud.aud_tone_play(16,200)#播放按键音
+ self.LocalRespondingEnded_timerout = 0
+ self.close_timerout = 0
+ self._idle_deep_sec = 0
+ self._vc_idle_deep_armed_suppress = 0
+
+ #
+ if self.AI_client_VC_isconnect == 0:#没连接的时候请求开始
+ self.websocket_connect_VC()#连接服务器
+
+ #utime.sleep_ms(50)
+ else:
+ print("AI_VC_start2 (already connected) {}".format(self._vc_fsm_log_snip()))
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ if self.AI_client_VC_isconnect == 0:
+ #self.Close()
+ return
+ # 单对话 PTT:会话已 started 时勿重复发 Start(否则第二轮起上行/ASR 易乱)
+ _ptt_skip_start = False
+ try:
+ if self.VC_mode == 1 and getattr(self, "_vc_session_started", 0) == 1:
+ _ptt_skip_start = True
+ print("[PTT] AI_VC_start skip duplicate Start (session active)")
+ except Exception:
+ pass
+ if not _ptt_skip_start:
+ self.AI_client_VC_start_requset()
+ print("AI_VC_start3")
+
+ if (not _ptt_skip_start) and self.AI_TTS_str == "" and self.AI_new_voice_url == "":
+ self._uplink_request_start("AI_VC_start")
+
+
+ # pass
+ print("AI_VC_start4")
+
+ #self.VC_Play_Start()
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Start})
+ #开始线程
+
+ # if self.AI_VC_send_data_queue_thread_id == 0:
+ # _thread.stack_size(self.Audio_buf_size+8*1024)
+ # self.AI_VC_send_data_queue_thread_id = _thread.start_new_thread(self.AI_VC_send_data_queue_thread, ())
+ # _thread.stack_size(8*1024)
+ # print("AI_VC_start :AI_VC_send_data_queue_thread_id :{} ".format(self.AI_VC_send_data_queue_thread_id))
+
+ 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, ())
+ _thread.stack_size(8*1024)
+ print("AI_VC_start :AI_audio_queue_thread_id :{} ".format(self.AI_audio_queue_thread_id))
+ else:
+ print("AI_VC_start :AI_audio_queue_thread_id != 0 ")
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ self._1S_timer.start(1000,1,self._1S_timer_cb)
+ # self.VC_pingping_timerout = 0
+ # self.asr_pingping_timerout = 0
+ print("AI_VC_start5")
+ def kws_even(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.kws_even_cb})
+ def kws_even_cb(self):
+ if EventMesh.publish("get_sos_flag") == 1:
+ return
+ print("kws_even_cb")
+ if self.VC_mode == 1:
+ try:
+ tts_str = vc_config.get_attr("KWS_PTT_HINT", "请您按住圆形的按钮开始讲话,讲完松开即可 ")
+ except Exception:
+ tts_str = "请您按住圆形的按钮开始讲话,讲完松开即可 "
+ EventMesh.publish("play_tts",[tts_str,1])
+ return
+ if self.VC_mode == 0:
+ if EventMesh.publish("get_voice_record_flag") == 1:#停止静默录音
+ if EventMesh.publish("get_voice_record_mode") == 2:#语音留言不能被打断
+ return
+ elif EventMesh.publish("get_voice_record_mode") == 1: #静默录音需要打断
+ EventMesh.publish("stop_voice_record")#停止静默录音
+ self.aud.aud_tone_play(10,400)
+ # EventMesh.publish("audio_montage_play",["chenggong.mp3"])
+ self.Close()
+ # if dev_type == "7PIN" :
+ # self.aud.aud_tone_play(10,400)#播放按键音
+ #self.aud.aud_tone_play(10,200)#播放按键音
+ self.AI_TTS_str = ""#"你好,有什么吩咐"
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ self._uplink_request_stop("kws_even_cb")
+ self._uplink_request_start("kws_even_cb")
+ self.AI_VC_start()
+ def set_wait_first_flag(self,event = None,msg= None):#设置未读语音播报初次记录flag
+ self.wait_first_flag = msg
+ def find_cmd(self,event ,msg):
+ if "电量" in msg:
+ battery = EventMesh.publish("get_battery")
+ print("电量查询:{}".format(battery))
+ tts_play_str = "电量剩余百分之{}".format(battery)
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ # print("电量!!!!!!!!!!!")
+ elif "信号" in msg:
+ signal = net.csqQueryPoll()
+ tts_play_str = "当前信号{}".format(signal)
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ # print("信号!!!!!!!!!!!")
+ elif "卡号" in msg:
+ # print("卡号!!!!!!!!!!!")
+ iccid = sim.getIccid()
+ iccid = num_str_to_chinese(iccid)#转为中文
+ if iccid != -1 and len(iccid) > 5:
+ print("iccid{}".format(iccid))
+ tts_play_str = "当前卡号为:{}".format(iccid)
+ else:
+ tts_play_str = "卡号查询失败"
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ elif "设备号" in msg:
+ imei = modem.getDevImei()
+ if imei != -1 and len(imei) > 5:
+ print("IMEI:{}".format(imei))
+ tts_play_str = "当前设备号为:{}".format(imei)
+ else:
+ tts_play_str = "设备号查询失败"
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ elif "音量" in msg :
+ tts_play_str = "当前音量是{}".format(self.VC_TTS_volume)
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ elif "语速" in msg :
+ tts_play_str = "当前语速是{}".format(EventMesh.publish("find_rate"))
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ def chenggong_tone(self):
+ EventMesh.publish("audio_montage_play",["chenggong.mp3"])
+ def concurrency_limit_exceeded_tone(self):
+ self.Close()
+ EventMesh.publish("audio_montage_play",["chaobingfatis.mp3"]) #播放超并发提醒
+ def factory_mode(self):
+ print("factory_mode")
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.factory_start})
+ def factory_start(self):
+ self.factory_timer.start(3000, 1, self.factory_timer_cb)
+ def factory_timer_cb(self, *args, **kwargs):
+ print("factory_timer_cb play......")
+ #self.aud.play(1,0,'U:/static/peizhichenggong.mp3')#切换成功
+ EventMesh.publish("audio_montage_play",["xinhaoruo1.mp3"])
+ def Close_VC(self,event,msg):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ def Close(self):
+ 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)))
+ except Exception:
+ pass
+ self.VC_stop_play_audio_flag = 0
+ try:
+ self.AI_key_long_press_flag = 0
+ except Exception:
+ pass
+ self.VC_Thinking_timerout = 0
+ self.AI_ASR_timer.stop()
+ self._1S_timer.stop()
+ self.close_timerout = 0
+ self._idle_deep_sec = 0
+ self.factory_timer.stop()
+ self.AI_TTS_str = ""
+ self.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ self.AI_ASR_flag = 0
+ 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._vc_pending_interrupt_uplink = 0
+ try:
+ self._skip_started_uplink_once = 0
+ except Exception:
+ pass
+ try:
+ self._ptt_abort_interrupt_arm()
+ except Exception:
+ pass
+ # Power 短按停止对话后,再次按 AI 需要新建 dialog_id:清理本轮 dialog/session 标识
+ try:
+ self._cur_dialog_id = ""
+ except Exception:
+ pass
+ try:
+ self._cur_sid = ""
+ except Exception:
+ pass
+ self.is_ruppt_tts = 0#打断标志位清零
+ print("ai_VC Close1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ try:
+ if getattr(self, "VC_mode", 0) != 1:
+ print("[VC][long_idle] step=set_led_state_idle (first)")
+ except Exception:
+ pass
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ self.tts_in_data_len = 0
+ self.tts_out_data_len = 0
+ #必须先销毁线程,不然线程会容易报错
+
+ try:
+ self.AI_audio_queue_thread_close()#销毁音频线程
+ except Exception as e:
+ usys.print_exception(e)
+ try:
+ self.AI_VC_send_data_queue_thread_close()#销毁数据发送线程
+ except Exception as e:
+ usys.print_exception(e)
+
+ # Close 可能被不同线程触发:统一切回主线程执行 stop
+ try:
+ if getattr(self, "VC_mode", 0) != 1:
+ print("[VC][long_idle] step=schedule_uplink_stop reason=Close (async publish_AI_btnInfo)")
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"Close"})
+
+ print("ai_VC Close1.1!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+
+ print("ai_VC Close1.2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+
+ try:
+ if getattr(self, "VC_mode", 0) != 1:
+ print("[VC][long_idle] step=websocket_deconnect_VC")
+ except Exception:
+ pass
+ try:
+ self.websocket_deconnect_VC()
+ except Exception as e:
+ usys.print_exception(e)
+
+
+
+
+
+ print("ai_VC Close1.3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+
+ print("ai_VC Close2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+
+
+
+
+ if self.record_data_save_flag == 1:
+ if self.record_fd :
+ self.record_fd.close()
+ self.record_fd = None
+ if self.play_data_save_flag == 1:
+ if self.play_fd !=None:
+ self.play_fd.close()
+ self.play_fd = None
+ print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")
+
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ self.play_new_voice_flag = 0 #语音消息播放状态
+ if self.voice_choose_flag == 0: # 如果没有未读语音
+ self.AI_new_voice_url = "" # 语音播放地址
+ print("关闭并重置语音标志位")
+ else:
+ print("关闭但是没有重置语音标志位")
+ self.AI_ASR_send_buf_len = 0
+ #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")
+ except Exception:
+ pass
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ self.VC_init()
+ self.VC_satrt_flag = 0
+ # Level2:会话结束后仍可继续 1s 节拍累计 idle 直至深睡(依赖 VC_IDLE_DEEP_SLEEP_SEC)
+ try:
+ _dsec = 60
+ _den = 1
+ if "vc_config" in globals() and vc_config:
+ _dsec = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_SEC", 60))
+ _den = int(vc_config.get_int("VC_IDLE_DEEP_SLEEP_ENABLE", 1))
+ if _den and _dsec > 0:
+ self._1S_timer.start(1000, 1, self._1S_timer_cb)
+ except Exception:
+ pass
+ def VC_init2(self):
+ self.VC_init()
+
+ print("ai_VC VC_init2!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
+ try:
+ self.websocket_deconnect_VC()
+ except Exception as e:
+ usys.print_exception(e)
+ print("ai_VC Close3!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\r\n")
+ self._1S_timer.stop()
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+
+ self.AI_ASR_send_buf_len = 0
+ if self.AI_audio_queue.empty()!=True:
+ self.AI_audio_queue.clear()
+ def VC_init(self):
+ _q0 = self.AI_audio_queue.size()
+ self.VC_Play_End()
+ if self.AI_audio_queue.empty()!=True:
+ self.AI_audio_queue.clear()
+ print("[VC] init audio_queue {} -> {}".format(_q0, self.AI_audio_queue.size()))
--- /dev/null
+# -*- coding: utf-8 -*-
+"""AI_VC mixin:ASR 上行/录音、TTS 下行、播放队列与本地采播。"""
+from vc_session_imports import * # noqa: F401,F403
+import _thread
+
+class VCSessionMediaMixin:
+ def get_vc_tts_playback_active(self, *args, **kwargs):
+ try:
+ return 1 if self._vc_tts_playback_active() else 0
+ except Exception:
+ return 0
+ def get_vc_stream_active(self, *args, **kwargs):
+ # 供本地提示音侧查询:TTS 流播放期间本地不可抢占。
+ try:
+ if self._vc_tts_playback_active():
+ return 1
+ except Exception:
+ pass
+ try:
+ if getattr(self.fsm, "await_played", False):
+ return 1
+ except Exception:
+ pass
+ try:
+ if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:
+ return 1
+ except Exception:
+ pass
+ try:
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
+ return 1
+ except Exception:
+ pass
+ try:
+ if getattr(self, "_tts_pending", 0) == 1:
+ return 1
+ except Exception:
+ pass
+ return 0
+ def _vc_tts_pipeline_busy(self):
+ try:
+ if getattr(self.fsm, "await_played", False):
+ return True
+ if getattr(self.fsm, "tts_rx_phase", VC_TtsRxPhase.Idle) != VC_TtsRxPhase.Idle:
+ return True
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ if t in (VCTurnState.TtsReceiving, VCTurnState.Playing, VCTurnState.AwaitPlayedAck):
+ return True
+ if getattr(self, "_tts_pending", 0) == 1:
+ return True
+ if getattr(self, "VC_Play_flag", 0) == 1:
+ return True
+ if self._vc_local_play_busy():
+ return True
+ except Exception:
+ pass
+ return False
+ def _vc_local_play_busy(self):
+ try:
+ if getattr(self, "_vc_local_play_active", 0) != 1:
+ return False
+ until = getattr(self, "_vc_local_play_until_ms", 0)
+ if until and utime.ticks_diff(until, utime.ticks_ms()) > 0:
+ return True
+ self._vc_local_play_active = 0
+ self._vc_local_play_until_ms = 0
+ except Exception:
+ pass
+ return False
+ def _vc_local_play(self, path, play_mode=1, loop=1, hold_ms=650):
+ """本地 MP3 短音:播前停录/停流式 TTS,避免与麦克风采集叠用。"""
+ try:
+ if getattr(self, "record_data_save_flag", 0) == 0 and getattr(self, "VC_record_state_flag", 0) != 0:
+ self._uplink_request_stop("local_play")
+ utime.sleep_ms(30)
+ except Exception:
+ pass
+ try:
+ if self._vc_tts_playback_active():
+ self._ptt_hard_mute_local_playback()
+ try:
+ self.VC_Play_End()
+ except Exception:
+ pass
+ except Exception:
+ pass
+ try:
+ hold = int(hold_ms)
+ if hold < 200:
+ hold = 200
+ self._vc_local_play_active = 1
+ self._vc_local_play_until_ms = utime.ticks_add(utime.ticks_ms(), hold)
+ except Exception:
+ pass
+ try:
+ self.aud.play(play_mode, loop, path)
+ except Exception as e:
+ try:
+ self._vc_local_play_active = 0
+ self._vc_local_play_until_ms = 0
+ except Exception:
+ pass
+ usys.print_exception(e)
+ def _vc_tts_playback_active(self):
+ """仅服务端 TTS/PCM 流在播(VC_Play_Start);aud.play 本地短音不算。"""
+ try:
+ if getattr(self, "VC_Play_flag", 0) == 1:
+ return True
+ except Exception:
+ pass
+ try:
+ if getattr(self, "play_pcm", None) is not None:
+ return True
+ except Exception:
+ pass
+ return False
+ def _vc_vad_uplink_stop_eligible(self):
+ # 对应旧 VC_state >= SpeechStart
+ try:
+ if getattr(self.fsm, "speech_uplink_open", 0):
+ return True
+ t = getattr(self.fsm, "turn", VCTurnState.Idle)
+ return t not in (VCTurnState.Idle, VCTurnState.Listening)
+ except Exception:
+ return False
+ def _vc_clear_stale_record_state(self, reason=""):
+ """record_state_flag 非 0 但可能无真实录音时先停录复位,避免 uplink 门控拒开麦。"""
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self.VC_Speech_End()
+ except Exception:
+ try:
+ self.VC_record_state_flag = 0
+ except Exception:
+ pass
+ def _vc_effective_asr_text(self):
+ try:
+ t = (getattr(self, "AI_ASR_str", "") or "").strip()
+ if t:
+ return t
+ return (getattr(self, "_last_asr_result_text", "") or "").strip()
+ except Exception:
+ return ""
+ def _vc_long_dialogue_auto_uplink_ok(self, reason):
+ """长对话 canceled/completed 自动开麦:须有有效 ASR,且防抖/勿在 LLM 无 TTS 后抢开。"""
+ try:
+ if reason not in ("canceled", "completed", "local.play_end", "audio.played"):
+ return True
+ if getattr(self, "Close_flag", 0) == 1:
+ return False
+ if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
+ pass
+ elif not self._vc_effective_asr_text():
+ k = getattr(self, "_last_tts_kind", "") or ""
+ if reason == "local.play_end" and k == "boot_prompt":
+ pass
+ else:
+ try:
+ print("[VC][uplink_ctrl] Deny START by {} cause=empty_asr".format(reason))
+ except Exception:
+ pass
+ return False
+ if reason == "completed":
+ if getattr(self, "_turn_has_tts_started", 0) == 0:
+ try:
+ 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")
+ return False
+ except Exception:
+ pass
+ try:
+ now = utime.ticks_ms()
+ last = getattr(self, "_vc_last_auto_uplink_ms", 0)
+ if last and utime.ticks_diff(now, last) < 1200:
+ print("[VC][uplink_ctrl] Deny START by {} cause=auto_uplink_debounce".format(reason))
+ return False
+ self._vc_last_auto_uplink_ms = now
+ except Exception:
+ pass
+ except Exception:
+ pass
+ return True
+ 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", "key.press/continue"):
+ 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
+
+ try:
+ if self.VC_mode == 0 and not self._vc_long_dialogue_auto_uplink_ok(reason):
+ return
+ except Exception:
+ pass
+
+ try:
+ if not self._vc_capture_allowed(reason):
+ try:
+ print("[VC][uplink_ctrl] Deny START by {} cause=playback_busy".format(reason))
+ 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:
+ # 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))
+ try:
+ if reason == "canceled" and getattr(self, "_vc_pending_interrupt_uplink", 0) == 1:
+ self._vc_pending_interrupt_uplink = 0
+ print("[VC][uplink_ctrl] interrupt_uplink armed -> start (canceled)")
+ except Exception:
+ pass
+ 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:
+ try:
+ if getattr(self, "VC_mode", 0) != 1:
+ print("[VC][long_idle] _uplink_request_stop skip (record_state==0) reason={}".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(
+ reason, getattr(self, "VC_record_state_flag", -1)))
+ except Exception:
+ pass
+ 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_record_state_flag={}".format(
+ first_len, self._vc_fsm_log_snip(), self.VC_record_state_flag))
+ except Exception:
+ pass
+ def _uplink_mark_end(self, reason=""):
+ 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_record_state_flag={}".format(
+ reason, self._uplink_bytes, dur, self._vc_fsm_log_snip(), 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 = ""
+ 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 set_VC_stop_play_audio_flag(self,event = None, msg = None):
+ self.VC_stop_play_audio_flag = msg
+ print("self.VC_stop_play_audio_flag:{}".format(self.VC_stop_play_audio_flag))
+ def rate_change(self,event,msg):#直接设置语速(数字)
+ self.speech_rate = msg
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
+ print("set_speech_rate:{}".format(self.speech_rate))
+ def rate_move(self,event,msg):#设置语速(模糊(快/慢一点))
+ if msg == 1 and self.speech_rate<50:#增大
+ self.speech_rate += 10
+ else:#减小
+ if self.speech_rate >-50:
+ self.speech_rate -= 10
+ EventMesh.publish("set_storage_config",["speech_rate",self.speech_rate])
+ print("set_speech_rate:{}".format(self.speech_rate))
+ def find_rate(self,event,msg):#查询APP语速
+ print("查询语速处理:原始数据为:{}".format(self.speech_rate))
+ y = 1
+ if self.speech_rate >= -50 and self.speech_rate < 0:#负数
+ y = 1 - (-0.01 * self.speech_rate)
+ elif self.speech_rate <= 50 and self.speech_rate >= 0:#正数
+ y = 0.02 * self.speech_rate + 1.0
+ print("处理后数据为{}".format(y))
+ rate = str(y) + "倍"
+ return rate
+ def set_record_data_save_flag(self,event = None, msg = None):
+ self.record_data_save_flag = msg
+ def get_AI_audio_queue(self, event, msg):
+ return self.AI_audio_queue
+ def audio_cb(self,event):
+ if event == 0:
+ EventMesh.publish(constants.EVENT.SYSTEM_UP)#唤醒休眠
+ self.audio_play_state =1
+ #EventMesh.publish("set_audio_state", 1)
+ print('audio-play start.')
+ # 本地 aud.play(开录/思考短音等)不改动 AI 灯,仅 TTS 流由 VC_Play_Start 控绿灯
+ if self._vc_led_state_debug_enabled():
+ print("[LED] audio_cb start skip (local_play)")
+ return
+ elif event == 7:
+ self.audio_play_state = 0
+ #EventMesh.publish("set_audio_state", 0)
+ print('audio-play finish.!!!!!!')
+ try:
+ if getattr(self, "VC_mode", -1) == 1 and getattr(self, "_ptt_await_server", 0) == 1:
+ self._vc_publish_led("thinking", "audio_cb/finish/ptt_await")
+ return
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_record_state_flag", 0) != 0:
+ self._vc_publish_led("listening", "audio_cb/finish/recording")
+ return
+ except Exception:
+ pass
+ try:
+ if self._vc_tts_playback_active():
+ if self._vc_led_state_debug_enabled():
+ print("[LED] audio_cb finish skip (tts_playback)")
+ return
+ except Exception:
+ pass
+ try:
+ if getattr(self, "_ptt_led_lock_listening", 0) == 1:
+ return
+ except Exception:
+ pass
+ self._vc_led_sync_from_fsm()
+ def set_tts_volume(self,event,msg):
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",msg])#音量配置
+ self.VC_TTS_volume = msg
+ print("set_tts_volume:{} {}".format(self.VC_TTS_volume,msg))
+ if int(msg) <= 10: #最小一定要设置1要不然没有声音
+ volume = 10 //10
+ self.aud.setVolume(volume)
+ else:
+ self.aud.setVolume(self.VC_TTS_volume //10)
+ pass
+ def get_audio_play_state(self, event=None, msg = None):
+ return self.audio_play_state
+ def set_VC_record_state_flag(self,data):
+
+ self.VC_record_state_flag = data
+ 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 record_state={}".format(self.VC_record_state_flag))
+ def VC_Speech_Start(self):
+ try:
+
+ #self.VC_state = VC_State_SpeechStart
+ 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.AI_ASR_str = ""
+ self.AI_ASR_str_tmp = ""
+ self._last_asr_result_text = ""
+ try:
+ self._asr_completed_tone_done = 0
+ except Exception:
+ pass
+
+ self.VC_record_thread_close()#关闭录音线程
+ _thread.stack_size(self.Audio_buf_size+8*1024)
+ self.VC_record_thread_id = _thread.start_new_thread(self.VC_record_thread,())
+ _thread.stack_size(8*1024)
+ print("self.VC_record_thread_id:{}".format(self.VC_record_thread_id))
+ try:
+
+ if self.VC_record_TYPE == VC_record_TYPE_AMR :
+ try :
+ self.record.stream_start(audio.Record.AMRNB, self.VC_record_samplerate, 0)
+ except Exception as e:
+ usys.print_exception(e)
+ elif self.VC_record_TYPE == VC_record_TYPE_PCM:
+ print("record_pcm.stream_read_start()!!!!!!!!!!!!")
+ self.record.stream_start(audio.Record.WAV, self.VC_record_samplerate, 0)
+ # if self.record_pcm != None:
+ # try:
+ # self.record_pcm.stream_stop()
+ # self.record_pcm.close()
+ # self.record_pcm = None
+ # except Exception as e:
+ # usys.print_exception(e)
+ # if self.record_pcm == None:
+ # ret = self.record_pcm = audio.Audio.PCM(0,1,16000,0,1,2)
+ # print("audio.Audio.PCM:ret:{}".format(ret))
+ # ret = self.record_pcm.stream_read_start()
+ # print("self.record_pcm.stream_read_start:ret:{}".format(ret))
+ except Exception as e:
+ usys.print_exception(e)
+
+ 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))
+ except Exception:
+ pass
+ # 录音开始:聆听灯(PTT 蓝快闪 / 长对话蓝慢闪)
+ try:
+ self._vc_publish_led("listening", "VC_Speech_Start")
+ except Exception:
+ pass
+ try:
+ if getattr(self, "VC_mode", -1) == 1:
+ self._ptt_on_record_hardware_started()
+ except Exception:
+ pass
+
+ except Exception as e:
+ self.websocket_error()
+ self.VC_record_state_flag = 1
+ self.VC_Speech_End()#关闭录音
+ usys.print_exception(e)
+ def VC_Speech_End(self):
+ try:
+ self._uplink_mark_end("VC_Speech_End")
+ self.AI_ASR_flag = 0
+ self.AI_ASR_str_tmp = ""
+ # 勿在此清空 AI_ASR_str:vad.silence 停麦常早于 asr.completed,清空会导致 check_text:len:0 连播 think 音
+ #EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_asr})
+
+ if self.VC_record_state_flag == 0:
+ return
+ #self.AI_client_asr_StopTranscription_requset()#请求停止
+ if self.VC_record_TYPE == VC_record_TYPE_AMR:
+ self.record.stream_stop()
+ self.record.stop()#关闭录音
+ 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)
+ # 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:
+ self.record.stream_stop()
+ self.record.stop()#关闭录音
+ 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)
+ # if VC_AUDIO_FRAME_LOG:
+ # print("Record_len:!!!!!!{}".format(Record_len))
+ if Record_len <= 0:
+ break
+ # if self.record_pcm != None:
+ # try:
+ # self.record_pcm.stream_stop()
+ # self.record_pcm.close()
+ # self.record_pcm = None
+ # except Exception as e:
+ # usys.print_exception(e)
+ except Exception as e:
+ usys.print_exception(e)
+ self.VC_record_thread_close()#关闭录音线程
+ if self.AI_record_tmp_queue.empty() != True:
+ self.AI_record_tmp_queue.clear()
+
+ self.VC_record_state_flag = 0
+ # PTT 一轮录音结束:清长按语义,避免后续短按松手误走 end_speech 而不播提示音
+ try:
+ if getattr(self, "VC_mode", -1) == 1:
+ self.AI_key_long_press_flag = 0
+ except Exception:
+ pass
+ # 录音停止:单对话进入思考态(等服务器),勿继续蓝闪聆听
+ try:
+ if getattr(self, "VC_mode", -1) == 1:
+ self._ptt_await_server = 1
+ self._vc_publish_led("thinking", "VC_Speech_End/ptt")
+ else:
+ self._ptt_await_server = 0
+ self._vc_publish_led("idle", "VC_Speech_End")
+ except Exception:
+ pass
+ 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:
+ count =50
+ while True:
+ count -= 1
+ if self.VC_record_thread_lock == 1:
+ self.VC_record_thread_lock = 2
+ if self.VC_record_thread_lock == 0 or count <=0:
+ break
+ else:
+ print("VC_record_thread_thread going:{}...".format(count))
+ utime.sleep_ms(1)
+ self.VC_record_thread_lock = 0
+ print("VC_record_thread_thread_close")
+ if self.VC_record_thread_id != 0:
+ try:
+ _thread.stop_thread(self.VC_record_thread_id)
+ except Exception as e:
+ usys.print_exception(e)
+ self.VC_record_thread_id = 0
+ def VC_record_thread(self):
+ ##return
+ print("[VC] record_thread start")
+ count = 0
+ RequestToSpeak_flag = 0
+ # try:
+ # uos.remove('/usr/test.pcm')
+ # except Exception as e:
+ # usys.print_exception(e)
+ #record_data_queue = Queue(100)
+ #fd = open('/usr/test.pcm','w')
+ # if self.VC_record_TYPE == 1:
+ # h_data= [0x23,0x21 ,0x41 ,0x4D ,0x52 ,0x0A ]
+ # h_data_b = bytes(h_data)
+ # self.AI_client_VC_send(h_data_b)
+ AI_record_tmp_queue_len = 0
+ vad_buf = bytearray()
+ VAD_data_count = 0
+ vad_state = 0
+ self.VAD_count = 0
+ while True:
+ try:
+ if self.VC_record_thread_lock == 2:
+ break
+ self.VC_record_thread_lock = 1
+ # 死循环接收数据
+ #if self.VC_state == VC_State_SpeechStart:
+ Record_read_buf = bytearray(self.Audio_buf_size)
+
+ Record_len = 0
+ if self.VC_record_TYPE == VC_record_TYPE_PCM:#pcm
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
+ #utime.sleep_ms(60)
+
+ else :
+
+ Record_len = self.record.stream_read(Record_read_buf, self.Audio_buf_size)
+ utime.sleep_ms(600)
+
+ #Record_len = 1024*2
+ #print("VC_record_thread:{}".format(Record_len))
+ if Record_len > 0:
+ read_buf = bytes(Record_read_buf[: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:
+ pass
+ # 二进制写入,避免文本模式导致数据异常
+ self.record_fd = open('/usr/record_data.pcm', 'wb')
+ self.record_fd_len = 0
+ if self.record_fd != None:
+ try:
+ self.record_fd.write(read_buf)
+ print("write record:{}".format(len(read_buf)))
+ self.record_fd_len = self.record_fd_len+len(read_buf)
+ if self.record_fd_len >=500*1024:
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ except Exception as e:
+ usys.print_exception(e)
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.Close})
+ # count+=Record_len
+ # if count >150*1024:
+ # self.record_fd.close()
+ #self.record_fd = None
+ #VAD检测
+ if self.is_VAD == 1:
+ vad_state = 0
+ tmp_c = 1000
+ while True:
+ tmp_c -=1
+ if tmp_c <=0:
+ break
+ #vad_buf_len = len(vad_buf)
+ if VAD_data_count == 0:#没数据的时候
+ vad_buf = Record_read_buf[:self.VAD_data_len]
+ VAD_data_count = self.VAD_data_len
+ self.vad.custom_vad_reset()#重置vad
+ else:#有数据的时候
+ if Record_len - VAD_data_count < self.VAD_data_len :#剩余长度不够一次填充,先保留,但不填算法
+ vad_buf = Record_read_buf[VAD_data_count:Record_len]
+ VAD_data_count = 0
+ break
+ else:#剩余长度够填一次数据
+ vad_buf = Record_read_buf[VAD_data_count:VAD_data_count+self.VAD_data_len]
+ VAD_data_count += len(vad_buf)
+ #print("vad_buf_len:{},{}".format(len(vad_buf),VAD_data_count))
+
+ if len(vad_buf) >= self.VAD_data_len:
+ vad_state = self.vad.custom_vad_process(vad_buf,len(vad_buf))#给vad数据
+ #print("vad_state:{}".format(vad_state))
+
+ if VAD_data_count >= Record_len:
+ VAD_data_count = 0
+ break
+ if vad_state == 1:
+ print("检测到VAD=1!!!!!!!!!!!!!")
+
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
+ # RequestToSpeak_flag = 1
+
+ self.VAD_count+=1
+ if self.VAD_count >= 20:
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_VC_RequestToSpeak})#打断
+
+ else:
+ if self.VAD_count != 0:
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Play_Pause,"data":0})#恢复音频播放
+ self.VAD_count = 0
+ if self._vc_tts_pipeline_busy():
+ pass
+ if vad_state == 1:
+ self.AI_record_tmp_queue.put(read_buf)#入缓存队列
+ AI_record_tmp_queue_len += len(read_buf)
+
+
+ else:
+ VAD_data_count = 0
+ if self.VC_record_state_flag <=1 :#没发送前先存起来
+ self.AI_record_tmp_queue.put(read_buf)
+ AI_record_tmp_queue_len += len(read_buf)
+ else:
+ AI_record_tmp_queue_len = 0
+ while True:
+ if self.AI_record_tmp_queue.empty() == True:
+ break
+ else:
+ pass
+ # 协议门控:会话未 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)")
+
+
+
+
+
+
+ #print("VC_record_thread:{}".format(len(read_buf)))
+ #count+=len(read_buf)
+ # if count>=500*1024:#结束
+ # self.AI_client_VC_end_speech()
+ # break
+ # count+=1
+ # if count %10 == 0:
+ # print("VC_record_thread on line {} asr recv :{} send:{}".format(self.VC_record_state_flag,_thread.threadIsRunning(self.AI_client_asr_recv_id),_thread.threadIsRunning(self.AI_VC_send_data_queue_thread_id)))
+
+ # if self.VC_state >= VC_State_SpeechEnd:
+ # break
+
+ except Exception as e:
+ usys.print_exception(e)
+ #break
+ self.VC_record_thread_lock = 0
+ if self.record_data_save_flag == 1:
+ utime.sleep_ms(1)
+ else:
+ utime.sleep_ms(100)
+ #fd.close()
+ print("VC_record_thread:exit")
+ self.VC_record_thread_id = 0#线程退出
+ self.VC_record_thread_lock = 0
+ def voice_play_10S(self,event,msg):#按键开启留言播放等待10S定时器判断
+ self.voice_timer.start(1000,1,self.voice_judge)
+ def voice_judge(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.voice_judger_cb})
+ def voice_judger_cb(self):
+ self.voice_cnt += 1
+ # print("语音留言定时器cnt = {}".format(self.voice_cnt))
+ if self.voicerupt_flag == 1:#按键被按下
+ self.pwk_notice_flag = 1#表示进入收听未听留言
+ self.AI_VC_start()
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ self.voice_cnt = 0
+ # self.recv_voice = 0#进入留言定时器,留言清空
+ print("进入留言定时器1,留言清空!!!!!")
+ EventMesh.publish("update_play_new_voice_info")#更新已读状态
+ self.voice_timer.stop()
+ if self.pwk_cnt_voice > 0:
+ EventMesh.publish("get_new_voice_msg_S")
+ if self.voice_cnt > 15:#10S后退出播放,5S是为了给语音提示留时间
+ self.voice_cnt = 0
+ self.recv_voice = 0#进入留言定时器,留言清空
+ if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭
+ self.voice_choose_flag = 0
+ print("进入留言定时器2,留言清空!!!!!")
+ self.voice_timer.stop()
+ def revoice_judge(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.revoice_judger_cb})
+ def revoice_judger_cb(self):
+ self.revoice_cnt += 1
+ # print("语音留言回复定时器cnt = {}".format(self.revoice_cnt))
+ if self.revoice_cnt > 20:#10S后退出播放,10S是为了给语音提示留时间
+ self.revoice_cnt = 0
+ self.voice_flag=0
+ EventMesh.publish("set_1M_nowplay_flag",0)
+ print("语音留言回复停止")
+ self.revoice_timer.stop()
+ def music_play(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.music_play_cb})
+ def music_play_cb(self):
+ self.music_cnt += 1
+ if self.music_cnt > 4:#4S后播放音乐,4S是为了给语音提示留时间
+ self.AI_VC_start()
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ self.music_cnt = 0
+ self.music_timer.stop()
+ def history_play(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.history_play_cb})
+ def history_play_cb(self):
+ self.his_cnt += 1
+ if self.his_cnt > 4:#4S后播放,4S是为了给语音提示留时间
+ self.AI_VC_start()
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ self.his_cnt = 0
+ self.his_timer.stop()
+ def wait_play(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_play_cb})
+ def wait_play_cb(self):
+ self.wait_cnt += 1
+ if self.wait_voicerupt_flag == 1:#按键被按下
+ EventMesh.publish("set_is_listen_wait",1)#已听
+ self.pwk_notice_flag = 1#表示进入收听未听留言
+ self.AI_VC_start()
+ self.wait_sort = 1
+ self.AI_TTS_str = "第"+str(self.wait_sort)+"条"
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ # self.wait_cnt = 0
+ # print("进入留言定时器1,留言清空!!!!!")
+ # EventMesh.publish("update_play_new_voice_info")#更新已读状态
+ self.wait_timer.stop()
+ if self.wait_cnt > 10:#10S后退出播放,5S是为了给语音提示留时间
+ self.wait_cnt = 0
+ self.recv_voice = 0#进入留言定时器,留言清空
+ EventMesh.publish("set_history_voice_flag",0)
+ EventMesh.publish("set_wait_voice_flag",0)
+ EventMesh.publish("set_open_device",0)#开机不听
+ self.wait_first_flag = 0
+ if self.voice_choose_flag == 1:#播放完语音后,有冲突的时候关闭
+ self.voice_choose_flag = 0
+ print("进入留言定时器2,留言清空!!!!!")
+ self.wait_timer.stop()
+ def wait_single_play(self, *args, **kwargs):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.wait_single_play_cb})
+ def wait_single_play_cb(self):
+ self.wait_cnt += 1
+ if self.wait_cnt > 2:#2S后播放,2S是为了给语音提示留时间
+ self.AI_VC_start()
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ self.wait_cnt = 0
+ self.wait_timer.stop()
+ def put_record_data(self,data):
+ try:
+ if self._ptt_should_drop_inbound_tts():
+ return
+ except Exception:
+ pass
+ #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])
+ def play_tts(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_tts_cb,"data":msg})
+ def play_tts_cb(self,data):
+ try:
+ tts_str = data[0]
+ is_close_VC = data[1]
+ if tts_str != "":
+ self.AI_TTS_str = tts_str
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ try:
+ if is_close_VC:
+ EventMesh.publish("set_led_state", {"ai_state": "idle"})
+ else:
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ except Exception:
+ pass
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_tts_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ pass
+ def VC_Play_Start(self):
+ try:
+ if self._ptt_should_skip_remote_playback():
+ if self._vc_led_state_debug_enabled():
+ print("[VC] VC_Play_Start skip (ptt_interrupt)")
+ return
+ except Exception:
+ pass
+ self.tts_in_data_len = 0
+ self.tts_out_data_len = 0
+ self._tts_first_write_ms = 0
+ try:
+ self._tts_completed_wall_ms = 0
+ except Exception:
+ pass
+ if self.VC_Play_flag == 0:
+ try:
+ _ptt_fast_open = False
+ try:
+ if getattr(self, "VC_mode", -1) == 1:
+ _ptt_fast_open = True
+ except Exception:
+ pass
+ # 本地提示音/文件播放可能占用同一音频通道,导致 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:
+ # 刚停采集时延后打开播音 PCM,且失败时重试,降低「pcm open write only fail」
+ last_err = None
+ _open_retries = 1 if _ptt_fast_open else 3
+ for i in range(_open_retries):
+ try:
+ if not _ptt_fast_open:
+ 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
+ try:
+ if not self._ptt_should_skip_remote_playback():
+ self._vc_publish_led("playing", "VC_Play_Start/tts")
+ except Exception:
+ pass
+ except Exception as e:
+ self.VC_Play_End()
+ usys.print_exception(e)
+ def VC_Play_Pause(self,Pause_flag):
+ try:
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:
+ if self.play_pcm != None:
+ self.play_pcm.stream_pause(Pause_flag)
+ except Exception as e:
+ self.VC_Play_End()
+ usys.print_exception(e)
+ def VC_Play_End(self):
+ self.VC_Play_flag = 0
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:
+ if self.VC_play_samplerate == 8000:
+ self.aud.stopPlayStream()
+ self.aud.stopAll()
+ elif self.VC_play_samplerate == 16000:
+ if self.play_pcm != None:
+ print("Close:play_pcm.close()!!!!!!!!!!!!!!!!!!")
+ try:
+ self.play_pcm.stream_stop()
+ self.play_pcm.close()
+ except Exception as e:
+ usys.print_exception(e)
+ self.play_pcm=None
+
+ self.aud.stopPlayStream()
+ self.aud.stopAll()
+ elif self.VC_play_TYPE == VC_play_TYPE_MP3:
+ self.aud.stopPlayStream()
+ self.aud.stopAll()
+ if self.AI_audio_queue.empty()!=True:
+ self.AI_audio_queue.clear()
+
+ # TTS 播完:按真实 FSM/录音态恢复灯;本地短音不走此路径
+ try:
+ if getattr(self, "_vc_play_end_suppress_idle_led", 0):
+ if self._vc_led_state_debug_enabled():
+ print("[LED] VC_Play_End skip led (suppress)")
+ elif getattr(self, "VC_record_state_flag", 0) != 0:
+ self._vc_publish_led("listening", "VC_Play_End/recording")
+ else:
+ self._vc_led_sync_from_fsm()
+ except Exception:
+ pass
+ # 关键日志:定位“tts 结束绿灯仍不灭”究竟是 base 态(如 charging/full)还是 playing 未退出
+ try:
+ if "vc_config" in globals() and vc_config and (
+ vc_config.get_bool("LED_DEBUG", False) or vc_config.get_bool("KEY_LED_DEBUG", False) or vc_config.get_bool("HW_STATUS_DEBUG", False)
+ ):
+ try:
+ led = EventMesh.publish("get_led_debug_state")
+ except Exception:
+ led = None
+ print("[VC] VC_Play_End led={}".format(led))
+ except Exception:
+ pass
+ def _vc_tts_bytes_per_sec(self):
+ try:
+ return int(self.VC_play_samplerate) * 2
+ except Exception:
+ return 32000
+
+ # audio_audio.c: ring_buf_len = 64*1024;stream_get_buf_len 返回「空闲可写」而非待播
+ _PCM_STREAM_RING_BYTES = 65536
+
+ def _vc_pcm_stream_pending_bytes(self, pcm_free):
+ if pcm_free < 0:
+ return -1
+ if pcm_free >= self._PCM_STREAM_RING_BYTES:
+ return 0
+ return self._PCM_STREAM_RING_BYTES - pcm_free
+
+ def _vc_try_play_end_tick(self):
+ """tts.completed 后判定本地 PCM 是否真正播完,避免过早 VC_Play_End 截断 TTS。"""
+ try:
+ if not getattr(self.fsm, "await_played", False):
+ return False
+ in_len = getattr(self, "tts_in_data_len", 0)
+ out_len = getattr(self, "tts_out_data_len", 0)
+ try:
+ q_empty = self.AI_audio_queue.empty()
+ except Exception:
+ q_empty = True
+ tts_done_ms = getattr(self, "_tts_completed_wall_ms", 0) or getattr(
+ self.fsm, "last_tts_completed_ms", 0)
+ if not (in_len > 0 and out_len >= in_len and q_empty and tts_done_ms):
+ return False
+
+ is_pcm_empty = (self.play_pcm is None) or (
+ self.play_pcm is not None and self.play_pcm.stream_is_empty() == 1
+ )
+ pcm_free = 0
+ if self.play_pcm is not None:
+ try:
+ pcm_free = self.play_pcm.stream_get_buf_len()
+ except Exception:
+ pcm_free = -1
+ pcm_pending = self._vc_pcm_stream_pending_bytes(pcm_free)
+ if pcm_pending < 0:
+ pcm_pending = max(0, in_len - out_len)
+
+ now = utime.ticks_ms()
+ bps = self._vc_tts_bytes_per_sec()
+ fw = getattr(self, "_tts_first_write_ms", 0)
+ # 丢弃明显陈旧的 completed 时间(上轮残留或 last_tts_completed_ms==1 等)
+ try:
+ if fw and tts_done_ms and utime.ticks_diff(tts_done_ms, fw) < 0:
+ tts_done_ms = now
+ if tts_done_ms and utime.ticks_diff(now, tts_done_ms) > 120000:
+ return False
+ except Exception:
+ pass
+
+ try:
+ since_done = utime.ticks_diff(now, tts_done_ms)
+ except Exception:
+ since_done = 0
+ debounce_ms = 150
+ if pcm_pending > 0 and bps > 0:
+ tail_ms = max(debounce_ms, int(pcm_pending * 1000 // bps) + 200)
+ else:
+ tail_ms = debounce_ms
+ tail_ok = since_done >= tail_ms
+ # 与旧 ai_VC LocalRespondingEnded 一致:stream 空且环形缓冲无待播数据
+ pcm_drained = is_pcm_empty and pcm_pending == 0
+ ready = pcm_drained and tail_ok
+
+ if not ready:
+ try:
+ if not getattr(self, "_tts_play_check_log_ms", 0) or utime.ticks_diff(
+ now, self._tts_play_check_log_ms
+ ) >= 800:
+ self._tts_play_check_log_ms = now
+ print(
+ "[TTS] play_end_wait since_done={}ms tail~{}ms "
+ "in={} out={} q_empty={} pcm_free={} pcm_pending={} pcm_empty={}".format(
+ since_done, tail_ms, in_len, out_len,
+ 1 if q_empty else 0, pcm_free, pcm_pending,
+ 1 if is_pcm_empty else 0,
+ )
+ )
+ except Exception:
+ pass
+ return False
+
+ try:
+ print(
+ "[TTS] play_end_fire since_done={}ms tail~{}ms "
+ "in={} out={} pcm_free={} pcm_pending={} pcm_empty={}".format(
+ since_done, tail_ms, in_len, out_len, pcm_free, pcm_pending,
+ 1 if is_pcm_empty else 0,
+ )
+ )
+ except Exception:
+ pass
+ EventMesh.publish("publish_AI_btnInfo", {"func": self.VC_Play_End})
+ self._fsm_apply_actions(self.fsm.on_local_play_end())
+ try:
+ self.fsm.reset_rx_phases()
+ except Exception:
+ pass
+ self._tts_first_write_ms = 0
+ return True
+ except Exception as e:
+ usys.print_exception(e)
+ return False
+ def AI_client_VC_send_speech(self):
+ json_data = \
+ {
+ "header": {
+ "event": "audio.create",
+
+ },
+ }
+
+ if self.VC_mode == 1:#单对话模式
+ # 协议门控(可用 VC_PROTO_GUARDS 临时关闭)
+ if (not VC_PROTO_GUARDS) or (self._vc_session_started == 1):
+ self._ptt_audio_created = 1
+ try:
+ print("[VC][proto] audio.create send session_started={} proto_guards={}".format(
+ self._vc_session_started, VC_PROTO_GUARDS))
+ except Exception:
+ pass
+ self.AI_client_VC_send(ujson.dumps(json_data))
+ else:
+ try:
+ print("[VC][proto] audio.create SKIP session_started={} VC_PROTO_GUARDS={} "
+ "(等对端 started 后再开麦)".format(self._vc_session_started, VC_PROTO_GUARDS))
+ except Exception:
+ pass
+
+
+
+
+
+
+ try:
+ self.fsm.on_server_asr_started()
+ except Exception:
+ pass
+ def AI_client_VC_end_speech(self):
+ json_data = \
+ {
+ "header": {
+ "event": "audio.completed",
+
+ },
+ }
+ self.AI_client_VC_send(ujson.dumps(json_data))
+ self._ptt_audio_created = 0
+ EventMesh.publish("publish_AI_btnInfo",{"func":self._uplink_request_stop, "data":"audio.completed"})
+ def AI_client_VC_synthesis(self):
+ # 幂等保护:在上一条 synthesis 还未等到 tts.started 前,不重复发(防止多兜底/多入口重复触发)
+ try:
+ if self._tts_pending == 1:
+ return
+ except Exception:
+ pass
+ json_data = \
+ {
+ "event": "synthesis",
+ "payload": {
+ "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))
+ 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 set_vad_silence(self,event = None,msg= None):
+ self.vad_silence = msg
+ EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
+ def set_VC_TTS_voice_type(self,event = None,msg= None):
+ self.VC_TTS_voice_type["APP_voice"] = msg
+ #self.VC_TTS_voice_type[self.VC_TTS_voice_key]
+ 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 key={} map={}".format(self.VC_TTS_voice_key, self.VC_TTS_voice_type))
+ def change_volume(self,event =None, msg =None):
+ if msg == 0 :
+ self.VC_TTS_volume -= 10
+ if self.VC_TTS_volume < 0:
+ self.VC_TTS_volume = 0
+ return 0
+ else:
+ self.aud.setVolume(self.VC_TTS_volume //10)
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
+ EventMesh.publish("post_device_state")
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
+ return 1
+ elif msg == 1:
+ self.VC_TTS_volume += 10
+ if self.VC_TTS_volume > 100:
+ self.VC_TTS_volume = 100
+ return 0
+ else:
+ self.aud.setVolume(self.VC_TTS_volume //10)
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
+ EventMesh.publish("post_device_state")
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
+ return 1
+ elif msg >= 100:#修改为具体音量
+ if msg > 200:
+ self.VC_TTS_volume = 100
+ return 0
+ else:
+ self.VC_TTS_volume = msg - 100#还原数据
+ self.aud.setVolume(self.VC_TTS_volume //10)
+ EventMesh.publish("set_storage_config",["VC_TTS_volume",self.VC_TTS_volume])
+ EventMesh.publish("post_device_state")
+ EventMesh.publish("adjust_volume_toBle",self.VC_TTS_volume)
+ return 1
+ def NLP_set_vad_silence(self,event,msg):
+ if msg == 1:#聆听变快
+ self.vad_silence -= 1000
+ if self.vad_silence < 1000:
+ AI_TTS_str = "已经是最快了"
+ self.vad_silence = 1000
+ else:
+ AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)
+ elif msg == 0:#聆听变慢
+ self.vad_silence += 1000
+ if self.vad_silence > 4000:
+ self.vad_silence = 4000
+ #EventMesh.publish("audio_montage_play",["yinliangyizuixiao.mp3"])
+ #self.aud.play(1,0,'U:/static/yinliangyizuixiao.mp3')#音量已最小
+ AI_TTS_str = "已经是最慢了"
+ else:
+ AI_TTS_str = "配置成功,当前语音断句速度是{}秒".format(self.vad_silence/1000)
+ EventMesh.publish("set_storage_config",["vad_silence",self.vad_silence])
+ return AI_TTS_str
+ def change_voice(self,event ,msg):
+ voice = msg
+ # if "男声" in msg or "普通话" in msg:
+ # self.VC_TTS_voice_key = "男"
+ # else:
+ # self.VC_TTS_voice_key = voice
+ self.outbound_attributess_provider = "xun-fei"
+ if "男声" in msg or "普通话" in msg:
+ self.VC_TTS_voice_key = "男"
+ self.outbound_attributess_provider = "volcengine"
+ elif "女声" in msg:
+ self.VC_TTS_voice_key = "女"
+ self.outbound_attributess_provider = "volcengine"
+ elif "童声" in msg:
+ self.VC_TTS_voice_key = "童"
+ self.outbound_attributess_provider = "volcengine"
+ elif "香港粤语" in msg:
+ self.VC_TTS_voice_key = "香港粤语"
+ elif "四川话" in msg:
+ self.VC_TTS_voice_key = "四川"
+ elif "上海话" in msg:
+ self.VC_TTS_voice_key = "上海"
+ elif "安徽合肥话" in msg:
+ self.VC_TTS_voice_key = "安徽合肥"
+ elif "山东话" in msg:
+ self.VC_TTS_voice_key = "山东话"
+ elif "陕西话" in msg:
+ self.VC_TTS_voice_key = "陕西"
+ elif "广东话" in msg:
+ self.VC_TTS_voice_key = "广东"
+ elif "成都话" in msg:
+ self.VC_TTS_voice_key = "成都"
+ elif "湖南话" in msg:
+ self.VC_TTS_voice_key = "湖南"
+ elif "苏州话" in msg:
+ self.VC_TTS_voice_key = "苏州"
+ elif "东北话" in msg:
+ self.VC_TTS_voice_key = "东北"
+ elif "河南话" in msg:
+ self.VC_TTS_voice_key = "河南"
+ elif "湖北话" in msg:
+ self.VC_TTS_voice_key = "湖北"
+ elif "闽南话" in msg:
+ self.VC_TTS_voice_key = "闽南"
+ elif "内蒙古话" in msg:
+ self.VC_TTS_voice_key = "内蒙古话"
+ EventMesh.publish("set_storage_config",["VC_TTS_voice_key",self.VC_TTS_voice_key])
+ tts_play_str = "声音已改为{}".format(voice)
+ EventMesh.publish("play_tts",[tts_play_str,0])
+ def play_single_voice(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_single_voice_cb,"data":msg})
+ def play_single_voice_cb(self,data):
+ try:
+ new_voice_url = data[0] #音频地址
+ is_close_VC = data[1]
+ if new_voice_url != "":
+ self.AI_new_voice_url = new_voice_url
+ self.alarm_play_flag = 1
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_single_voice_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ pass
+ def play_new_voice(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_new_voice_cb,"data":msg})
+ def play_new_voice_cb(self,data):
+ try:
+ new_voice_url = data[0] #音频地址
+ is_close_VC = data[1]
+ if new_voice_url != "":
+ self.recv_voice = 1#收到留言
+ print("已收到最新留言!!!!!")
+ # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数
+ self.AI_new_voice_url = new_voice_url
+ # if self.pwk_cnt_voice == 0 and self.pwk_notice_flag == 0:#并且不在多条留言的状态
+ # EventMesh.publish("play_tts",["您有一条语音留言,10秒内按下中间键可以收听",1])
+ if self.pwk_notice_flag == 0 and EventMesh.publish("get_app_new_voice_flag"):#不处于播放状态并且是小程序下发的语音而不是请求的
+ self.pwk_cnt_voice += 1#加上当前这条
+ EventMesh.publish("set_storage_config",["is_noread_voice",1])
+ EventMesh.publish("set_app_new_voice_flag",0)
+ print("voice_add_to = {}".format(self.pwk_cnt_voice))
+ print("self.pwk_notice_flag = {}".format(self.pwk_notice_flag))
+ if self.pwk_cnt_voice >1:
+ if self.pwk_notice_flag == 0:
+ tts_str = "您有" + str(self.pwk_cnt_voice) + "条未读语音留言,10秒内按下中间键可以收听"
+ EventMesh.publish("play_tts",[tts_str,1])
+ elif self.pwk_cnt_voice == 1:
+ if self.pwk_notice_flag == 0:
+ tts_str = "您有1条语音留言,10秒内按下中间键可以收听"
+ EventMesh.publish("play_tts",[tts_str,1])
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_new_voice_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ pass
+ def play_his_voice(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_his_voice_cb,"data":msg})
+ def play_his_voice_cb(self,data):
+ try:
+ new_voice_url = data[0] #音频地址
+ is_close_VC = data[1]
+ time = data[2]
+ tts_play_str = time + "收到"
+ EventMesh.publish("play_tts",[tts_play_str,1])
+ if new_voice_url != "":
+ self.recv_voice = 1#收到留言
+ print("历史留言查看")
+ # tts_play_str = "收到"
+ # EventMesh.publish("play_tts",[tts_play_str,1])
+ # EventMesh.publish("set_motor_time",[1,1,5,8,4])#设置马达参数
+ self.AI_new_voice_url = new_voice_url
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_his_voice_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ pass
+ def play_wait_voice(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_wait_voice_cb,"data":msg})
+ def play_wait_voice_cb(self,data):
+ try:
+ new_voice_url = data[0] #音频地址
+ is_close_VC = data[1]
+ print("是否为第一次:{}".format(self.wait_first_flag))
+ if self.wait_first_flag == 0:
+ tts_play_str = "您有" + str(len(EventMesh.publish("get_play_new_voice_list"))) + "条未听留言,10秒内按下中间键可以收听"
+ EventMesh.publish("play_tts",[tts_play_str,1])
+ # self.wait_first_timer.start(1000,1,self.noread_wait_vocie)
+ self.wait_first_flag = 1
+ elif self.wait_first_flag == 1:
+ self.wait_sort += 1
+ self.AI_TTS_str = "第"+str(self.wait_sort)+"条"
+ if new_voice_url != "":
+ self.recv_voice = 1#收到留言
+ print("未读留言查看")
+ self.AI_new_voice_url = new_voice_url
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_wait_voice_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ pass
+ def noread_wait_vocie(self,*args):#如果一直没读
+ self.wait_first_cnt += 1
+ print("未读留言等待收听中:{}".format(self.wait_first_cnt))
+ if EventMesh.publish("get_recv_voice") == 1:#如果检测到留言播放
+ self.wait_first_cnt = 0
+ self.wait_first_timer.stop()
+ if self.wait_first_cnt == 15:
+ EventMesh.publish("set_wait_first_flag",0)
+ EventMesh.publish("set_history_voice_flag",0)
+ EventMesh.publish("set_wait_voice_flag",0)
+ self.wait_first_cnt = 0
+ self.wait_first_timer.stop()
+ def play_music(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_music_cb,"data":msg})
+ def play_music_cb(self,data):
+ try:
+ music_name = data[0]
+ new_voice_url = data[1] #音频地址
+ is_close_VC = data[2]
+ if new_voice_url != "":
+ self.AI_new_voice_url = new_voice_url
+ self.music_play_flag = 1#表示音乐播放开启
+ EventMesh.publish("play_tts",["下面为您播放歌曲{}".format(music_name),1])
+ if is_close_VC != None:
+ self.Close_flag = is_close_VC
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_music_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ def play_trans_tts(self,event = None,msg= None):
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.play_trans_tts_cb,"data":msg})
+ def play_trans_tts_cb(self,data):
+ try:
+ new_voice_url = data #音频地址
+ if new_voice_url != "":
+ self.AI_new_voice_url = new_voice_url
+ if self.AI_VC_send_data_queue.empty()!=True:
+ self.AI_VC_send_data_queue.clear()
+ if self.AI_ASR_send_data_queue.empty()!=True:
+ self.AI_ASR_send_data_queue.clear()
+ EventMesh.publish("set_led_state", {"ai_state": "thinking"})
+ self.AI_ASR_send_buf_len = 0
+ self._uplink_request_stop("play_trans_tts_cb")
+ self.websocket_deconnect_VC()
+ self.AI_VC_start()
+ EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_client_new_voice})#播放留言
+ self.play_new_voice_flag = 1
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.VC_Speech_End})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.websocket_deconnect_VC})
+ # EventMesh.publish("publish_AI_btnInfo",{"func":self.AI_VC_start})
+ except Exception as e:
+ usys.print_exception(e)
+ def AI_audio_queue_thread_close(self):
+ print("AI_audio_queue_thread_close1!!!!!!!!!!!!!!!!!!!!!!")
+ if self.AI_audio_queue_thread_id != 0:
+ count =500
+ while True:
+ count -= 1
+ if self.AI_audio_queue_thread_lock == 0 or count <=0:
+ break
+ else:
+ self.AI_audio_queue_thread_lock = 2
+ print("AI_audio_queue_thread going:{}...".format(count))
+ utime.sleep_ms(1)
+ self.AI_audio_queue_thread_lock = 0
+ print("AI_audio_queue_thread_close2!!!!!!!!!!!!!!!!!")
+ if self.AI_audio_queue_thread_id != 0:
+ try:
+ _thread.stop_thread(self.AI_audio_queue_thread_id)
+ except Exception as e:
+ usys.print_exception(e)
+ self.AI_audio_queue_thread_id = 0
+ def AI_audio_queue_thread(self):
+ print("AI_audio_queue_thread start")
+ count = 0
+ if self.AI_audio_queue.empty()!=True:
+ self.AI_audio_queue.clear()
+ print("音频数据还有剩余!!!!!!!!!!!!")
+ while True:
+ try:
+ if self.AI_audio_queue_thread_lock == 2 or self.VC_stop_play_audio_flag == 0:
+ print("AI_audio_queue_thread 该退出了")
+ break
+ #
+ count+=1
+ if count%500 == 1 and VC_HEARTBEAT_LOG:
+ print("AI_audio_queue_thread on")
+ utime.sleep_ms(1)
+ #print("AI_audio_queue_thread")
+ if self.AI_audio_queue.empty() :
+ self.AI_audio_queue_thread_lock = 0
+
+
+ else:
+ self.AI_audio_queue_thread_lock = 1
+ data = self.AI_audio_queue.get()
+ self.close_timerout = 0
+ if self.play_flag == 0:
+ self.play_flag =1
+ #self.AI_client_tts_data_len=len(data)
+ #print("audio_queue:len:{}".format(len(data[0])))
+
+ if len(data[0]) > 10:
+ try:
+ if self._ptt_should_drop_inbound_tts():
+ continue
+ except Exception:
+ pass
+ for i in range(5):
+ if self.VC_Play_flag == 0:
+ self.VC_Play_Start()
+ utime.sleep_ms(2)
+ else:
+ break
+ # 打断后 PCM 偶发未打开:加重试,避免「队列有数据、play_pcm=None、无声」
+ if self.VC_play_TYPE == VC_play_TYPE_PCM and self.play_pcm is None and self.VC_stop_play_audio_flag == 1:
+ for j in range(4):
+ if self.VC_Play_flag == 0:
+ self.VC_Play_Start()
+ utime.sleep_ms(30 + j * 35)
+ if self.play_pcm is not None:
+ break
+ try:
+ if self.VC_play_TYPE == VC_play_TYPE_PCM:
+ # if self.VC_play_samplerate == 8000:
+ # tmp= self.aud.playStream(1,data[0] )
+ # else:
+ write_buf = bytearray(data[0])
+ #print("play_pcm.stream_write1 len:{}".format(len(write_buf)))
+ if self.play_pcm != None:
+ #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:
+ self._tts_first_write_ms = utime.ticks_ms()
+ except Exception:
+ self._tts_first_write_ms = 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
+ try:
+ if getattr(self.fsm, "await_played", False) and self.AI_audio_queue.empty():
+ self._vc_try_play_end_tick()
+ except Exception:
+ pass
+ #print("play_pcm.stream_write2:{}".format(self.tts_out_data_len))
+ elif self.VC_play_TYPE == VC_play_TYPE_MP3:
+ tmp= self.aud.playStream(3, data[0])#播放音频流
+
+ if tmp != 0:
+ print("2playStream:error tmp:{} data len:{}!!!!!!!!!!!!".format(tmp,len(data[0])))
+ # # #播放失败的时候填充WAV头
+ # data_len = len(data[0])
+ # data_len_b = data_len.to_bytes(4,'little')#数据长度转16进制数组
+ # h_data = [ 0x52 ,0x49 ,0x46 ,0x46 #,0x24 ,0x00 ,0x00 ,0x00 #文件标识、文件大小
+ # ,0x57 ,0x41 ,0x56 ,0x45 ,0x66 ,0x6D ,0x74 ,0x20 #文件格式、格式块标识符
+ # ,0x10 ,0x00 ,0x00 ,0x00 ,0x01 ,0x00 ,0x01 ,0x00 ,0x80 ,0x3E ,0x00 ,0x00 #格式块大小4、音频格式2、声道数2、采样率4
+ # ,0x80 ,0x3E ,0x00 ,0x00 ,0x02 ,0x00 ,0x10 ,0x00 ,0x64 ,0x61 ,0x74 ,0x61]#每秒字节数4、数据块对齐2、位深度2、data4
+ # h_data_b = bytes(h_data)
+ # file_len = data_len+len(h_data_b)+4+4
+ # file_len_b = file_len.to_bytes(4,'little')#文件长度转16进制数组
+ # h_data_b = h_data_b[:4]+file_len_b+h_data_b[4:]+data_len_b
+ # #self.aud.playStream(2, h_data_b)#播放音频流
+ # tmp= self.aud.playStream(2, h_data_b+data[0])#播放音频流
+ # print("3playStream:tmp:{} !!!!!!!!!!!!".format(tmp))
+
+
+ mem = gc.mem_free()
+ #print('1剩余内存RAM:{} KB'.format(mem / 1024))
+ except Exception as e:
+ print("AI_audio_queue_thread:ERROR:")
+ usys.print_exception(e)
+
+ # if self.VC_state == VC_State_TTSEnd and self.AI_audio_queue.empty() :#self.AI_audio_queue为空且tts下发结束
+ # #通知服务器播放结束
+ # self.AI_client_VC_LocalRespondingEnded()
+ except Exception as e:
+ print("AI_audio_queue_thread:ERROR:")
+ usys.print_exception(e)
+ self.AI_audio_queue_thread_lock = 0
+ self.AI_audio_queue_thread_id = 0
+ print("AI_audio_queue_thread:exit")