Prhub

#49056 [Bugfix] Emit a valid media type from encode_{audio,image,video}_url

原始 PR 作者 vineethsaivs 合并时间 2026-08-04 03:02 文件变更 2 提交数 1 评论 7 代码增减 +21 / -4

执行摘要

修复多模态 encode_*_url 生成非法媒体类型的问题

PR body 指出:三个 encode_*_url 帮助函数通过 mimetypes.types_map.get("." + format.lower(), "image"/"audio"/"video") 解析媒体类型,当格式扩展名未注册时回退为裸顶层类型,而缺少 subtype 的媒体类型不是良好定义的(not well-formed),会生成 data:image;base64,... 这类非法 data URL。该场景可被 Pillow/soundfile 可编码但 Python mimetypes 未注册的格式(TGA、PCX、CAF、W64)真实触达。PR body 还声明修改仅影响请求构造辅助函数的 media type 字符串,不涉及模型输出、精度与服务数值,因此不适用模型评测结果;同时作者排查了 #43638、#46947、#44474、#29034 等开放 PR 确认非重复,且无对应 open issue。

该 PR 值得快速阅读:它展示了在公共工具函数层用 type/subtype 兜底、而非在调用方修补的最小成本修复方式,并配套了针对未注册格式的回归测试。对多模态维护者,建议确认 closed 状态下修复是否已通过其他途径合入 main;对贡献者,可参考其 body 中主动声明不适用模型评测、排查重复 PR、以及首次贡献者 CI 沟通的流程。

讨论亮点
  • import 位置(风格):DarkLight1337 唯一一条代码评审意见是 "Please put the imports at the top",要求 from PIL import Image 不要放在测试函数内部;作者移到顶部第三方导入块,并说明与 tests/multimodal/test_image.py 的写法一致。
  • CI 门槛沟通:作者说明首次贡献者(0 个已合并 PR)无法自行添加 ready 标签,CI 从未运行;获批后指出 pre-run-check 的失败是标签添加(2026-07-27)之前的陈旧结果,重跑即可通过。
  • 合并冲突:mergify[bot] 提示冲突,作者 rebase 到 main 解决,冲突仅在 tests/multimodal/test_utils.py 的测试插入锚点处。维护者最终批准("Thanks")。

实现拆解

  1. 变更入口:vllm/multimodal/utils.py 中三个 data URL 构造辅助函数 encode_audio_url、encode_image_url、encode_video_url,各自只有一行 fallback 默认值变化。
  2. 核心改动:mimetypes.types_map.get 的默认值由裸类型改为 type/subtype 形式:

    • encode_audio_url:fallback "audio" → f"audio/{format.lower()}";
    • encode_image_url:fallback "image" → f"image/{format.lower()}";
    • encode_video_url:fallback "video" → f"video/{format.lower()}",同时保留 jpeg → video/jpeg 的特判分支。
      mimetypes.types_map 已注册的格式(如 PNG → image/png、WAV → audio/x-wav)走原路径,行为完全不变。
  3. 测试配套:tests/multimodal/test_utils.py 新增 test_encode_image_url_uncommon_format_has_valid_mimetype,构造 2×2 RGB 的 Pillow 图片并以 format="TGA" 编码,断言 data URL 前缀为 image/tga(.tga 未注册于 mimetypes.types_map,恰好覆盖 fallback 分支);按评审意见将 from PIL import Image 移到文件顶部导入块。

  4. 流程配套:该 PR 经历一次 rebase,解决与新增测试 test_group_and_batch_mm_items_splits_shared_data_by_dtype 在文件同一锚点产生的合并冲突;首次贡献者因 pre-run-check 门槛(0 个已合并 PR)无法自行启动 CI,由维护者添加 ready 标签后放行。作者说明本轮提交仅固化了 image 分支的回归测试,audio/video 分支通过本地 CPU 摘出函数体验证(media type 逻辑与 base64 编码无关)。
文件 模块 状态 重要度
vllm/multimodal/utils.py 多模态工具 modified 5.27
tests/multimodal/test_utils.py 单元测试 modified 4.84

关键符号

encode_audio_url encode_image_url encode_video_url test_encode_image_url_uncommon_format_has_valid_mimetype

关键源码片段

vllm/multimodal/utils.py core-logic

三个 encode_*_url 函数的 media type 回退逻辑所在,是本次修复的核心文件。

def encode_audio_url(
    audio: np.ndarray,
    sampling_rate: int,
    *,
    format: str = "WAV",
) -> str:
    """Encode audio as a data URL."""
    audio_b64 = encode_audio_base64(audio, sampling_rate, format=format)
    # 优先在 mimetypes.types_map 中查询已注册的媒体类型;
    # 未注册(如 CAF、W64 等 soundfile 可编码格式)时回退为 "audio/<format>",
    # 保证生成的 data URL 始终是合法的 type/subtype 结构。
    mimetype = mimetypes.types_map.get("." + format.lower(), f"audio/{format.lower()}")
    return f"data:{mimetype};base64,{audio_b64}"
​
​
def encode_image_url(
    image: Image.Image,
    *,
    image_mode: str | None = "RGB",
    format: str = "PNG",
) -> str:
    """
    将 Pillow 图片编码为 data URL。
​
    默认先转换为 RGB;传入 image_mode=None 可保留原始 image mode。
    """
    image_b64 = encode_image_base64(image, image_mode=image_mode, format=format)
    # TGA、PCX 等 Pillow 支持但 mimetypes 未注册的格式会命中此回退分支,
    # 修复前会生成 "data:image;base64,..." 这类缺少 subtype 的非法媒体类型。
    mimetype = mimetypes.types_map.get("." + format.lower(), f"image/{format.lower()}")
    return f"data:{mimetype};base64,{image_b64}"
​
​
def encode_video_url(
    frames: npt.NDArray,
    *,
    format: str = "JPEG",
) -> str:
    video_b64 = encode_video_base64(frames, format=format)
​
    # 帧序列按 JPEG 编码时沿用既定的 video/jpeg 映射;
    # 其他未注册格式(如 MKV)回退为 "video/<format>",同样保证结构合法。
    if format.lower() == "jpeg":
        mimetype = "video/jpeg"
    else:
        mimetype = mimetypes.types_map.get(
            "." + format.lower(), f"video/{format.lower()}"
        )
​
    return f"data:{mimetype};base64,{video_b64}"
tests/multimodal/test_utils.py test-coverage

新增回归测试覆盖未注册格式的 fallback 路径,并按评审意见调整 PIL 导入位置。

def test_encode_image_url_uncommon_format_has_valid_mimetype():
    """未注册在 mimetypes.types_map 中的格式也必须生成合法的
    type/subtype 媒体类型,而不是裸的 image。"""
    # ".tga" 不在 mimetypes.types_map 中,恰好覆盖 fallback 分支;
    # 而 Pillow 仍可编码 TGA 格式,说明该场景可被真实用户触达。
    url = encode_image_url(Image.new("RGB", (2, 2)), format="TGA")
    # 从 data URL 中取出 media type 前缀:去掉 "data:" 后取分号前部分
    mimetype = url.removeprefix("data:").split(";", 1)[0]
    assert mimetype == "image/tga"

评论区精华

测试文件的 import 组织方式 style

DarkLight1337 在 review 中要求 `from PIL import Image` 放到文件顶部,而不是测试函数内部导入。

结论:作者将导入移到顶部第三方导入块,并说明与 tests/multimodal/test_image.py 的既有写法保持一致。 · 已解决

首次贡献者 CI 门槛(pre-run-check) question

作者因 0 个已合并 PR、无 ready 标签导致 CI 从未运行,请求维护者加标签;获批后说明旧的 pre-run-check 失败是标签添加前的陈旧结果。

结论:DarkLight1337 添加 ready 标签并批准 PR;CI 需重跑 pre-run-check。 · 已解决

合并冲突与 rebase 处理 other

mergify[bot] 提示存在合并冲突;作者 rebase 到 main,冲突位于 tests/multimodal/test_utils.py 的测试插入锚点。

结论:Rebase 后 PR 恢复可合并状态,同时保留 DarkLight1337 的 import 修复。 · 已解决

风险与影响

  • 回归风险低:已注册格式完全不变化,改动仅涉及三个函数各一行的默认值,且 image 分支有新增测试覆盖。
  • 兼容性:未注册格式的 fallback 会输出 image/tga、video/mkv 这类可能不在 IANA 注册表中的媒体类型,某些严格校验的下游解析器仍可能拒绝,但相比修复前缺少 subtype 的非法前缀已转为结构合法。
  • 测试覆盖缺口:提交的测试只覆盖 encode_image_url 的 TGA 分支;audio、video 的 fallback 未固化为回归测试。
  • 状态不确定:PR 最终状态为 closed,材料未披露是否合入 main;若未合入,该修复需要后续 PR 承接。
  • 用户影响:使用 encode_audio_url、encode_image_url、encode_video_url 构造 data URL 请求的用户,在使用 TGA、PCX、CAF、W64、MKV 等非注册格式时会得到结构合法的 media type;已注册格式用户无感知。
  • 系统影响:不进入推理、调度或模型执行路径,无数值与性能影响;仅影响请求构造辅助函数输出的字符串。
  • 团队影响:典型的小型正确性修复 + 回归测试样板,对多模态模块的后续类似 fallback 修复有参考价值。
测试仅覆盖 image 分支 回退 MIME 可能非 IANA 注册 PR 状态为 closed 未确认合入

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论