From f0bf89bc86f7045b9fa098b8ff95d5785779f4e3 Mon Sep 17 00:00:00 2001 From: BillionClaw <267901332+BillionClaw@users.noreply.github.com> Date: Tue, 24 Mar 2026 05:39:42 +0800 Subject: [PATCH] fix(core): fix Record.convert_to_file_path() saving audio as .jpg When Record.audio file is downloaded via HTTP URL, save_temp_img() was used which always saves with .jpg extension. This corrupts non-JPG audio files (MP3, OGG, AAC, etc.). Add download_audio_by_url() and save_temp_audio() that save audio data without image processing, using .audio extension to avoid format corruption. Fixes: Record component convert_to_file_path for non-WAV audio URLs. --- astrbot/core/message/components.py | 4 ++-- astrbot/core/utils/io.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/astrbot/core/message/components.py b/astrbot/core/message/components.py index 6311681cd6..23362ff42c 100644 --- a/astrbot/core/message/components.py +++ b/astrbot/core/message/components.py @@ -36,7 +36,7 @@ from astrbot.core import astrbot_config, file_token_service, logger from astrbot.core.utils.astrbot_path import get_astrbot_temp_path -from astrbot.core.utils.io import download_file, download_image_by_url, file_to_base64 +from astrbot.core.utils.io import download_audio_by_url, download_file, download_image_by_url, file_to_base64 class ComponentType(str, Enum): @@ -157,7 +157,7 @@ async def convert_to_file_path(self) -> str: if self.file.startswith("file:///"): return self.file[8:] if self.file.startswith("http"): - file_path = await download_image_by_url(self.file) + file_path = await download_audio_by_url(self.file) return os.path.abspath(file_path) if self.file.startswith("base64://"): bs64_data = self.file.removeprefix("base64://") diff --git a/astrbot/core/utils/io.py b/astrbot/core/utils/io.py index b565926749..d63d116392 100644 --- a/astrbot/core/utils/io.py +++ b/astrbot/core/utils/io.py @@ -63,6 +63,16 @@ def save_temp_img(img: Image.Image | bytes) -> str: return p +def save_temp_audio(audio_data: bytes) -> str: + """Save audio data to a temporary file with a proper extension.""" + temp_dir = get_astrbot_temp_path() + timestamp = f"{int(time.time())}_{uuid.uuid4().hex[:8]}" + p = os.path.join(temp_dir, f"recordseg_{timestamp}.audio") + with open(p, "wb") as f: + f.write(audio_data) + return p + + async def download_image_by_url( url: str, post: bool = False, @@ -123,6 +133,34 @@ async def download_image_by_url( raise e +async def download_audio_by_url(url: str) -> str: + """Download audio from URL, preserving extension. Returns local file path.""" + try: + ssl_context = ssl.create_default_context(cafile=certifi.where()) + connector = aiohttp.TCPConnector(ssl=ssl_context) + async with aiohttp.ClientSession( + trust_env=True, + connector=connector, + ) as session: + async with session.get(url) as resp: + data = await resp.read() + return save_temp_audio(data) + except (aiohttp.ClientConnectorSSLError, aiohttp.ClientConnectorCertificateError): + logger.warning( + f"SSL certificate verification failed for {url}. " + "Disabling SSL verification (CERT_NONE) as a fallback." + ) + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + async with aiohttp.ClientSession() as session: + async with session.get(url, ssl=ssl_context) as resp: + data = await resp.read() + return save_temp_audio(data) + except Exception as e: + raise e + + async def download_file(url: str, path: str, show_progress: bool = False) -> None: """从指定 url 下载文件到指定路径 path""" try: