diff --git a/astrbot/core/pipeline/preprocess_stage/stage.py b/astrbot/core/pipeline/preprocess_stage/stage.py index 0f75dfd15..0265973b9 100644 --- a/astrbot/core/pipeline/preprocess_stage/stage.py +++ b/astrbot/core/pipeline/preprocess_stage/stage.py @@ -4,7 +4,7 @@ import traceback from collections.abc import AsyncGenerator from astrbot.core import logger -from astrbot.core.message.components import Image, Plain, Record +from astrbot.core.message.components import Image, Plain, Record, Reply from astrbot.core.platform.astr_message_event import AstrMessageEvent from astrbot.core.utils.media_utils import ensure_wav @@ -80,6 +80,24 @@ class PreProcessStage(Stage): except Exception as e: logger.warning(f"Voice processing failed: {e}") + # Also process Record components inside Reply chains (wav conversion) + for component in event.get_messages(): + if isinstance(component, Reply) and component.chain: + for idx, reply_comp in enumerate(component.chain): + if isinstance(reply_comp, Record): + try: + original_path = await reply_comp.convert_to_file_path() + record_path = await ensure_wav(original_path) + if record_path != original_path: + event.track_temporary_local_file(record_path) + reply_comp.file = record_path + reply_comp.path = record_path + component.chain[idx] = reply_comp + except Exception as e: + logger.warning( + f"Voice processing in reply chain failed: {e}" + ) + # STT if self.stt_settings.get("enable", False): # TODO: 独立 @@ -90,27 +108,53 @@ class PreProcessStage(Stage): f"会话 {event.unified_msg_origin} 未配置语音转文本模型。", ) return + + async def _stt_record(record_comp: Record, is_reply: bool = False): + """对单个 Record 组件执行语音转文本,成功返回 Plain,失败返回 None。""" + prefix = "引用消息" if is_reply else "" + try: + path = await record_comp.convert_to_file_path() + except Exception as e: + logger.warning(f"获取{prefix}语音路径失败: {e}") + return None + + retry = 5 + for i in range(retry): + try: + result = await stt_provider.get_text(audio_url=path) + if result: + suffix = "(引用消息)" if is_reply else "" + logger.info(f"语音转文本{suffix}结果: " + result) + return Plain(result) + break + except FileNotFoundError: + # napcat workaround: file may not be ready immediately + logger.debug(f"文件尚未就绪 ({path}),重试 {i + 1}/{retry}") + await asyncio.sleep(0.5) + continue + except BaseException as e: + logger.error(traceback.format_exc()) + suffix = "(引用消息)" if is_reply else "" + logger.error(f"语音转文本{suffix}失败: {e}") + break + return None + message_chain = event.get_messages() for idx, component in enumerate(message_chain): if isinstance(component, Record): - path = await component.convert_to_file_path() - retry = 5 - for i in range(retry): - try: - result = await stt_provider.get_text(audio_url=path) - if result: - logger.info("语音转文本结果: " + result) - message_chain[idx] = Plain(result) - event.message_str += result - event.message_obj.message_str += result - break - except FileNotFoundError as e: - # napcat workaround - logger.warning(e) - logger.warning(f"重试中: {i + 1}/{retry}") - await asyncio.sleep(0.5) - continue - except BaseException as e: - logger.error(traceback.format_exc()) - logger.error(f"语音转文本失败: {e}") - break + plain_comp = await _stt_record(component) + if plain_comp: + message_chain[idx] = plain_comp + event.message_str += plain_comp.text + event.message_obj.message_str += plain_comp.text + + # Also STT for Record components inside Reply chains + for component in event.get_messages(): + if isinstance(component, Reply) and component.chain: + for idx, reply_comp in enumerate(component.chain): + if isinstance(reply_comp, Record): + plain_comp = await _stt_record(reply_comp, is_reply=True) + if plain_comp: + component.chain[idx] = plain_comp + event.message_str += plain_comp.text + event.message_obj.message_str += plain_comp.text