wangzhibo
2026-09-11 659c470eba542383987aad227b7f918100f131fa
xiaozhi-esp32-server/main/xiaozhi-server/core/handle/receiveAudioHandle.py
@@ -17,16 +17,21 @@
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)
@@ -44,6 +49,7 @@
    # 检查输入是否是JSON格式(包含说话人信息)
    speaker_name = None
    actual_text = text
    conn.last_asr_language = getattr(conn, "last_asr_language", None)
    try:
        # FunASR 等会把结果包成 {"content","language","emotion"};声纹场景还有 speaker
@@ -52,6 +58,8 @@
            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,让模型自然称呼一次;
@@ -86,15 +94,28 @@
            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)
@@ -105,39 +126,50 @@
            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)
@@ -149,6 +181,16 @@
        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)