From 04ba2614179b97256abde3ec343b238f7be641a6 Mon Sep 17 00:00:00 2001 From: tjc66666666 <3428979959@qq.com> Date: Sat, 6 Jun 2026 13:39:30 +0800 Subject: [PATCH 1/4] Implement data URI handling for audio references Add support for data URI audio references by decoding base64 audio data and saving it to a temporary file. --- .../core/provider/sources/openai_source.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index 8aa2778f1b..c8af63e7b5 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -367,6 +367,28 @@ async def _audio_ref_to_local_path(self, audio_ref: str) -> tuple[str, list[Path return str(target_path), cleanup_paths if audio_ref.startswith("file://"): return self._file_uri_to_path(audio_ref), cleanup_paths + if audio_ref.startswith("data:"): + # data URI 格式: data:audio/wav;base64, + # 或 data:audio/mp3;base64, + try: + header, base64_data = audio_ref.split(",", 1) + # 从 data URI header 中提取 MIME 类型和格式 + # 例如 "data:audio/wav;base64" -> "wav" + mime_parts = header.removeprefix("data:").split(";") + mime_type = mime_parts[0] if mime_parts else "audio/wav" + suffix = "." + mime_type.split("/")[-1] if "/" in mime_type else ".wav" + if suffix not in (".wav", ".mp3", ".ogg", ".m4a", ".aac", ".flac"): + suffix = ".wav" + audio_bytes = base64.b64decode(base64_data) + temp_dir = Path(get_astrbot_temp_path()) + temp_dir.mkdir(parents=True, exist_ok=True) + target_path = temp_dir / f"provider_audio_{uuid.uuid4().hex}{suffix}" + target_path.write_bytes(audio_bytes) + cleanup_paths.append(target_path) + return str(target_path), cleanup_paths + except Exception as exc: + logger.warning("解析 data URI 音频失败: %s,错误: %s", audio_ref[:100], exc) + return audio_ref, cleanup_paths return audio_ref, cleanup_paths async def _resolve_audio_part(self, audio_ref: str) -> dict | None: From 01a9e557ab9de0eee700b8512750f6892a376188 Mon Sep 17 00:00:00 2001 From: tjc66666666 <3428979959@qq.com> Date: Sat, 6 Jun 2026 14:17:27 +0800 Subject: [PATCH 2/4] Enhance data URI handling for audio formats --- .../core/provider/sources/openai_source.py | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/astrbot/core/provider/sources/openai_source.py b/astrbot/core/provider/sources/openai_source.py index c8af63e7b5..34fbf398b5 100644 --- a/astrbot/core/provider/sources/openai_source.py +++ b/astrbot/core/provider/sources/openai_source.py @@ -369,16 +369,37 @@ async def _audio_ref_to_local_path(self, audio_ref: str) -> tuple[str, list[Path return self._file_uri_to_path(audio_ref), cleanup_paths if audio_ref.startswith("data:"): # data URI 格式: data:audio/wav;base64, - # 或 data:audio/mp3;base64, + # 或 data:audio/mpeg;base64, try: + # 防止过大的 base64 payload 导致内存耗尽 + if len(audio_ref) > 10 * 1024 * 1024: # 10 MB + logger.warning( + "data URI 音频过大 (%.1f MB),将忽略", + len(audio_ref) / (1024 * 1024), + ) + return audio_ref, cleanup_paths + header, base64_data = audio_ref.split(",", 1) - # 从 data URI header 中提取 MIME 类型和格式 - # 例如 "data:audio/wav;base64" -> "wav" + + # 从 data URI header 中提取 MIME 类型 mime_parts = header.removeprefix("data:").split(";") mime_type = mime_parts[0] if mime_parts else "audio/wav" - suffix = "." + mime_type.split("/")[-1] if "/" in mime_type else ".wav" - if suffix not in (".wav", ".mp3", ".ogg", ".m4a", ".aac", ".flac"): - suffix = ".wav" + + # 使用显式映射避免 audio/mpeg 被错误转换为 .mpeg 而非 .mp3 + mime_to_suffix = { + "audio/wav": ".wav", + "audio/x-wav": ".wav", + "audio/mpeg": ".mp3", + "audio/mp3": ".mp3", + "audio/ogg": ".ogg", + "audio/m4a": ".m4a", + "audio/x-m4a": ".m4a", + "audio/aac": ".aac", + "audio/flac": ".flac", + "audio/x-flac": ".flac", + } + suffix = mime_to_suffix.get(mime_type, ".wav") + audio_bytes = base64.b64decode(base64_data) temp_dir = Path(get_astrbot_temp_path()) temp_dir.mkdir(parents=True, exist_ok=True) @@ -386,8 +407,10 @@ async def _audio_ref_to_local_path(self, audio_ref: str) -> tuple[str, list[Path target_path.write_bytes(audio_bytes) cleanup_paths.append(target_path) return str(target_path), cleanup_paths - except Exception as exc: - logger.warning("解析 data URI 音频失败: %s,错误: %s", audio_ref[:100], exc) + except (ValueError, binascii.Error, OSError) as exc: + logger.warning( + "解析 data URI 音频失败: %s,错误: %s", audio_ref[:100], exc + ) return audio_ref, cleanup_paths return audio_ref, cleanup_paths From 54fb50b32c34f25cd0abbcc4a77c1d7f786fb000 Mon Sep 17 00:00:00 2001 From: tjc66666666 <3428979959@qq.com> Date: Sat, 6 Jun 2026 14:21:55 +0800 Subject: [PATCH 3/4] Implement audio data URI tests for WAV, MP3, OGG Add tests for handling audio data URIs in various formats. --- tests/test_openai_source.py | 161 ++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index b5587ffb14..0991a8cdfb 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -1809,3 +1809,164 @@ async def fake_create(**kwargs): assert messages[1] == {"role": "user", "content": "again"} finally: await provider.terminate() + + +# --------------------------------------------------------------------------- +# _audio_ref_to_local_path - data URI 处理测试 +# --------------------------------------------------------------------------- + + +def _minimal_wav_bytes() -> bytes: + """生成一个最小的有效 WAV 头(44 字节空数据)。""" + import struct + + data_size = 0 + file_size = 36 + data_size # 4 + 24 + 8 + data_size + header = bytearray(44) + header[0:4] = b"RIFF" + struct.pack_into(" Date: Sat, 6 Jun 2026 14:26:32 +0800 Subject: [PATCH 4/4] Refactor assertion message for audio path test --- tests/test_openai_source.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_openai_source.py b/tests/test_openai_source.py index 0991a8cdfb..5fb7a60482 100644 --- a/tests/test_openai_source.py +++ b/tests/test_openai_source.py @@ -1,6 +1,7 @@ import base64 import builtins from io import BytesIO +from pathlib import Path from types import SimpleNamespace import pytest @@ -1859,7 +1860,9 @@ async def test_audio_ref_data_uri_wav(monkeypatch, tmp_path): assert cleanup, "应该返回需要清理的临时文件路径" assert audio_path.endswith(".wav"), f"后缀应为 .wav,实际为 {audio_path}" assert Path(audio_path).parent == tmp_path - assert Path(audio_path).read_bytes() == wav_data, "解码后的内容应与原始 WAV 数据一致" + assert Path(audio_path).read_bytes() == wav_data, ( + "解码后的内容应与原始 WAV 数据一致" + ) # 清理 for p in cleanup: p.unlink(missing_ok=True)