| | |
| | | async def handleAudioMessage(conn: "ConnectionHandler", pcm_frame): |
| | | # 当前片段是否有人说话 |
| | | have_voice = conn.vad.is_vad(conn, pcm_frame) |
| | | # 如果设备刚刚被唤醒,短暂忽略VAD检测 |
| | | if hasattr(conn, "just_woken_up") and conn.just_woken_up: |
| | | mode = getattr(conn, "interaction_mode", "CHAT") |
| | | # 跟读听窗必须收齐 PCM。just_woken_up 的 2s 丢帧只用于对话,避免跟读前几句落成空 wav。 |
| | | if mode != "SHADOWING" and hasattr(conn, "just_woken_up") and conn.just_woken_up: |
| | | have_voice = False |
| | | # 设置一个短暂延迟后恢复VAD检测 |
| | | if not hasattr(conn, "vad_resume_task") or conn.vad_resume_task.done(): |
| | | conn.vad_resume_task = asyncio.create_task(resume_vad_detection(conn)) |
| | | return |
| | | # 服务端AEC功能需要实时触发打断 |
| | | # 服务端AEC:仅对话类模式允许打断。复读/跟读/翻译时喇叭回声不能当成插话。 |
| | | if conn.client_aec and have_voice: |
| | | if conn.client_is_speaking and conn.client_listen_mode != "manual": |
| | | if ( |
| | | conn.client_is_speaking |
| | | and conn.client_listen_mode != "manual" |
| | | and mode in ("CHAT", "BILINGUAL") |
| | | ): |
| | | await handleAbortMessage(conn) |
| | | # 设备长时间空闲检测,用于say goodbye |
| | | await no_voice_close_connect(conn, have_voice) |
| | |
| | | # 检查输入是否是JSON格式(包含说话人信息) |
| | | speaker_name = None |
| | | actual_text = text |
| | | conn.last_asr_language = getattr(conn, "last_asr_language", None) |
| | | |
| | | try: |
| | | # FunASR 等会把结果包成 {"content","language","emotion"};声纹场景还有 speaker |
| | |
| | | if isinstance(data, dict) and "content" in data: |
| | | speaker_name = data.get("speaker") |
| | | actual_content = data.get("content") or "" |
| | | if data.get("language"): |
| | | conn.last_asr_language = data.get("language") |
| | | if speaker_name: |
| | | conn.logger.bind(tag=TAG).info(f"解析到说话人信息: {speaker_name}") |
| | | # 仅在该说话人首次出现时保留 {"speaker":...} JSON,让模型自然称呼一次; |
| | |
| | | return |
| | | |
| | | from core.app_runtime.runtime import ( |
| | | is_exit_translation, |
| | | detect_user_language, |
| | | handle_shadowing_turn, |
| | | is_exit_to_chat, |
| | | match_voice_switch, |
| | | run_translation, |
| | | start_app_task, |
| | | VOICE_MENU, |
| | | wrap_bilingual_user_text, |
| | | voice_menu_text, |
| | | ) |
| | | from core.handle.intentHandler import speak_txt |
| | | |
| | | mode = getattr(conn, "interaction_mode", "CHAT") |
| | | |
| | | # 菜单 TTS 里的「跟读/复读机/翻译」不能当成切 App;退出聊天口令仍要生效 |
| | | if conn.client_is_speaking and getattr(conn, "awaiting_voice_app_choice", False): |
| | | mapped = match_voice_switch(actual_text) |
| | | if mapped and mapped[0] == "chat": |
| | | conn.awaiting_voice_app_choice = False |
| | | await start_app_task(conn, "chat", "conversation", {}, source="voice") |
| | | return |
| | | conn.logger.bind(tag=TAG).info("语音菜单播报中,忽略切 App") |
| | | return |
| | | |
| | | if getattr(conn, "awaiting_voice_app_choice", False): |
| | | mapped = match_voice_switch(actual_text) |
| | |
| | | return |
| | | if mapped and mapped[0] == "__menu__": |
| | | conn.awaiting_voice_app_choice = True |
| | | speak_txt(conn, VOICE_MENU) |
| | | speak_txt(conn, voice_menu_text()) |
| | | return |
| | | if mapped and mapped[0] != "__menu__": |
| | | params = {} |
| | | await start_app_task(conn, mapped[0], mapped[1], params, source="voice") |
| | | if mapped and mapped[0] == "chat": |
| | | await start_app_task(conn, "chat", "conversation", {}, source="voice") |
| | | return |
| | | speak_txt(conn, VOICE_MENU) |
| | | conn.awaiting_voice_app_choice = True |
| | | if mapped: |
| | | await start_app_task(conn, mapped[0], mapped[1], {}, source="voice") |
| | | return |
| | | conn.logger.bind(tag=TAG).info("语音菜单未匹配,保持 chat") |
| | | return |
| | | |
| | | mapped = match_voice_switch(actual_text) |
| | | if mapped and mapped[0] == "__menu__": |
| | | conn.awaiting_voice_app_choice = True |
| | | speak_txt(conn, VOICE_MENU) |
| | | speak_txt(conn, voice_menu_text()) |
| | | return |
| | | if mapped and mapped[0] == "__reserved__": |
| | | speak_txt(conn, "这个还没开放,先继续当前内容吧。") |
| | | return |
| | | if mapped and mapped[0] in ("english_translation", "english_learning", "chat"): |
| | | if mapped[0] == "english_learning" and mapped[1] == "repeater": |
| | | speak_txt(conn, "复读需要指定 SD 卡上的文件名,或用小程序开始。") |
| | | return |
| | | if conn.client_is_speaking and mapped and mapped[0] not in ("chat",): |
| | | conn.logger.bind(tag=TAG).info(f"TTS 播放中忽略切 App: {mapped}") |
| | | return |
| | | if mapped and mapped[0] in ( |
| | | "english_translation", |
| | | "english_learning", |
| | | "english_conversation", |
| | | "chat", |
| | | ): |
| | | await start_app_task(conn, mapped[0], mapped[1], {}, source="voice") |
| | | return |
| | | |
| | | if mode != "CHAT" and is_exit_to_chat(actual_text): |
| | | await start_app_task(conn, "chat", "conversation", {}, source="voice") |
| | | return |
| | | |
| | | if mode == "PLAY_AUDIO": |
| | | conn.logger.bind(tag=TAG).info("PLAY_AUDIO 模式忽略上行对话") |
| | | conn.logger.bind(tag=TAG).info("复读机模式忽略上行对话") |
| | | return |
| | | |
| | | if mode == "SHADOWING": |
| | | await handle_shadowing_turn(conn, actual_text) |
| | | return |
| | | |
| | | if mode == "TRANSLATION": |
| | | if is_exit_translation(actual_text): |
| | | await start_app_task(conn, "chat", "conversation", {}, source="voice") |
| | | return |
| | | await send_stt_message(conn, actual_text) |
| | | conn.client_abort = False |
| | | translated = await run_translation(conn, actual_text) |
| | |
| | | speak_txt(conn, translated) |
| | | return |
| | | |
| | | if mode == "BILINGUAL": |
| | | await send_stt_message(conn, actual_text) |
| | | conn.client_abort = False |
| | | user_lang = detect_user_language( |
| | | actual_text, getattr(conn, "last_asr_language", None) |
| | | ) |
| | | wrapped = wrap_bilingual_user_text(actual_text, user_lang) |
| | | conn.executor.submit(conn.chat, wrapped) |
| | | return |
| | | |
| | | # manual 模式下不打断正在播放的内容 |
| | | if conn.client_is_speaking and conn.client_listen_mode != "manual": |
| | | await handleAbortMessage(conn) |