diff --git a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py index 2096237ce..42b0b6cec 100644 --- a/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py +++ b/astrbot/core/platform/sources/qqofficial/qqofficial_message_event.py @@ -6,7 +6,7 @@ import base64 import aiofiles from astrbot.core.utils.io import file_to_base64, download_image_by_url -from astrbot.core.utils.tencent_record_helper import wav_to_tencent_silk +from astrbot.core.utils.tencent_record_helper import audio_to_tencent_silk from astrbot.core.utils.astrbot_path import get_astrbot_data_path from astrbot.api.event import AstrMessageEvent, MessageChain from astrbot.api.platform import AstrBotMessage, PlatformMetadata @@ -271,40 +271,49 @@ async def _parse_to_qqofficial(message: MessageChain): image_base64 = None # only one img supported image_file_path = None record_file_path = None - for i in message.chain: - if isinstance(i, Plain): - plain_text += i.text - elif isinstance(i, Image) and not image_base64: - if i.file and i.file.startswith("file:///"): - image_base64 = file_to_base64(i.file[8:]) - image_file_path = i.file[8:] - elif i.file and i.file.startswith("http"): - image_file_path = await download_image_by_url(i.file) + + for element in message.chain: + if isinstance(element, Plain): + plain_text += element.text + + elif isinstance(element, Image) and not image_base64: + if element.file and element.file.startswith("file:///"): + image_base64 = file_to_base64(element.file[8:]) + image_file_path = element.file[8:] + elif element.file and element.file.startswith("http"): + image_file_path = await download_image_by_url(element.file) image_base64 = file_to_base64(image_file_path) - elif i.file and i.file.startswith("base64://"): - image_base64 = i.file + elif element.file and element.file.startswith("base64://"): + image_base64 = element.file[9:] # 直接去掉前缀 else: - image_base64 = file_to_base64(i.file) - image_base64 = image_base64.removeprefix("base64://") - elif isinstance(i, Record): - if i.file: - record_wav_path = await i.convert_to_file_path() # wav 路径 + image_base64 = file_to_base64(element.file) + # 确保去掉 base64 前缀 + if image_base64 and image_base64.startswith("base64://"): + image_base64 = image_base64[9:] + + elif isinstance(element, Record): + if element.file: + record_wav_path = await element.convert_to_file_path() # wav 路径 temp_dir = os.path.join(get_astrbot_data_path(), "temp") - record_tecent_silk_path = os.path.join( + os.makedirs(temp_dir, exist_ok=True) # 确保目录存在 + + record_tencent_silk_path = os.path.join( temp_dir, f"{uuid.uuid4()}.silk" ) try: - duration = await wav_to_tencent_silk( - record_wav_path, record_tecent_silk_path + duration = await audio_to_tencent_silk( + record_wav_path, record_tencent_silk_path ) if duration > 0: - record_file_path = record_tecent_silk_path + record_file_path = record_tencent_silk_path else: record_file_path = None logger.error("转换音频格式时出错:音频时长不大于0") except Exception as e: logger.error(f"处理语音时出错: {e}") record_file_path = None + else: - logger.debug(f"qq_official 忽略 {i.type}") + logger.debug(f"qq_official 忽略 {element.type}") + return plain_text, image_base64, image_file_path, record_file_path diff --git a/astrbot/core/utils/tencent_record_helper.py b/astrbot/core/utils/tencent_record_helper.py index 2c97a01ed..63575d0dc 100644 --- a/astrbot/core/utils/tencent_record_helper.py +++ b/astrbot/core/utils/tencent_record_helper.py @@ -117,7 +117,9 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]: try: import pilk except ImportError as e: - raise Exception("未安装 pilk: pip install pilk") from e + raise Exception( + "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库" + ) from e temp_dir = os.path.join(get_astrbot_data_path(), "temp") os.makedirs(temp_dir, exist_ok=True) @@ -158,3 +160,75 @@ async def audio_to_tencent_silk_base64(audio_path: str) -> tuple[str, float]: os.remove(wav_path) if os.path.exists(silk_path): os.remove(silk_path) + + +async def audio_to_tencent_silk(audio_path: str, output_path: str) -> float: + """ + 将 MP3/WAV 文件转为 Tencent Silk 并返回时长(秒)。 + + 参数: + - audio_path: 输入音频文件路径(.mp3 或 .wav) + - output_path: 输出的音频路径-> silk + + 返回: + - duration: 音频时长(秒) + """ + try: + import pilk + except ImportError as e: + raise Exception( + "pilk 模块未安装,请前往管理面板->控制台->安装pip库 安装 pilk 这个库" + ) from e + + # 确保输入文件存在 + if not os.path.exists(audio_path): + raise FileNotFoundError(f"音频文件不存在: {audio_path}") + + temp_dir = os.path.join(get_astrbot_data_path(), "temp") + os.makedirs(temp_dir, exist_ok=True) + + # 检查文件扩展名 + ext = os.path.splitext(audio_path)[1].lower() + + # 创建临时 WAV 文件 + temp_wav = tempfile.NamedTemporaryFile( + suffix=".wav", delete=False, dir=temp_dir + ).name + + wav_path = audio_path # 默认使用原文件路径 + + # 如果不是 WAV 格式,需要转换 + if ext != ".wav": + try: + await convert_to_pcm_wav(audio_path, temp_wav) + wav_path = temp_wav + except Exception as e: + # 如果转换失败,清理临时文件 + if os.path.exists(temp_wav): + os.remove(temp_wav) + raise Exception(f"音频格式转换失败: {e}") from e + + try: + with wave.open(wav_path, "rb") as wav_file: + rate = wav_file.getframerate() + + # 转换为 Silk 格式 + silk_duration = await asyncio.to_thread( + pilk.encode, wav_path, output_path, pcm_rate=rate, tencent=True + ) + + return silk_duration + + except Exception as e: + # 如果转换失败,删除可能已创建的部分输出文件 + if os.path.exists(output_path): + os.remove(output_path) + raise Exception(f"Silk 格式转换失败: {e}") from e + + finally: + # 清理临时 WAV 文件(如果是新创建的) + if wav_path != audio_path and os.path.exists(wav_path): + try: + os.remove(wav_path) + except Exception: + pass # 忽略清理错误