iPhone 照片终于能读了:vision_analyze 现在解码 HEIC,视觉工具也不会再凭空消失


你在手机上截了一张报错图,顺手 AirDrop 到跑着 Hermes 的服务器上,然后跟 agent 说“看看这张图哪里错了”。它回你一句:source is not a recognized image。文件明明在那儿,尺寸也对,你双击能看,怎么到 agent 手里就成了“不认识的东西”?更让人摸不着头脑的是另一件事:某个长跑的进程里,视觉工具上午还能用,下午调用 vision_analyze 却告诉你视觉能力不可用——你什么都没改。

这两个症状其实来自两个独立的老问题,都在最近被修掉了:一个是图片格式嗅探漏掉了 iPhone 的 HEIC,另一个是启动探测把代理缓存写脏,导致视觉工具“自我消失”。本文把两件事讲清楚,并给出你自己排查的办法。

症状一:iPhone 照片被当成“不是图片”

先说清楚 vision_analyze 是什么:它是 Hermes 里让 agent“看图”的工具——你把图片路径或附件给它,它把图交给视觉模型,返回一段描述或问答结果。

iPhone 默认拍照格式是 HEIC(也写作 HEIF),它和 JPEG 不是一个家族:JPEG 是“一张图直接是像素”的老格式,HEIC 是装在一个叫 ISO-BMFF 的容器里的——容器好比快递箱,箱子上写的“品牌”决定了里面装的是 HEVC 编码的静图、AVIF 图,还是一段视频。Hermes 判断“这是不是图片、是什么图片”靠的是读文件头几个字节(称作 magic bytes 嗅探),而不是看扩展名——因为上传链路经常把 HEIC 错标成 .jpg,扩展名根本不值得信任。

旧代码里这段嗅探只认识 PNG / JPEG / GIF / WebP 这几张“名片”,没有任何处理 ISO-BMFF 容器的分支。于是 HEIC 文件读到嗅探环节就返回“未知”,后面那条本来能把 SVG / BMP / TIFF 转成受支持格式的重编码通道根本没机会执行。一句话:不是不能转,是连门都没进。

修复一:从 ftyp box 里认品牌(PR #106408)

修复的做法很直接:在嗅探里补上 ISO-BMFF 的 ftyp box 分支。容器第 4 到第 8 个字节固定是 ftyp,紧随其后的是 major brand(主品牌)与一串 compatible brand(兼容品牌列表)——就像箱子外面的品名和“也适用于”清单。

从 tools/vision_tools_image_prep.py 的实现能看出三个关键点:

  1. 主品牌和兼容品牌都要看。 AVIF 经常把主品牌写成通用的 mif1,真正的 AV1 标识只出现在兼容品牌列表里。只看主品牌会把 AVIF 认成“未知”。
  2. 扫描范围受 box 声明的长度限制,并且遇到非法长度时“失败关闭”(fail closed)。 文件头里的长度字段是攻击者可控的,如果放任它把扫描延伸进后面的 box,嗅探结果就可能被构造出来。所以长度不合法时直接判定失败,而不是“尽力猜”。
  3. 别的 ftyp 品牌照旧不认。 mp42(MP4 视频)这类非图片品牌不会被误判成图片——否则你的视频文件会被塞给视觉模型。

认出来之后,文件会走那条已经存在的 _normalize_to_supported_image 归一化通道,转成 PNG 再交给模型。解码 HEIC 靠的是 pillow-heif(预编译 wheel 里自带 libheif,不需要你装系统库),它作为一种“软依赖”注册:装了就启用,没装只会给出可执行的提示,而不是让整个视觉功能崩掉。

AVIF 的待遇稍微不同:Pillow 11.3 及以上已经原生支持 AVIF,而很多 pillow-heif 的 wheel 并不带 AV1 解码器,所以 AVIF 不被 pillow-heif 卡住——能解就直接解。

现在 pillow-heif 已经进了核心依赖(pillow-heif>=1.4.0,<2),也就是说新版本装好即用。如果你是从旧版本升级上来,或者跑在一个自己裁剪依赖的环境里,缺解码器时的报错会明确告诉你需要装什么:

# 缺解码器时的补救(多数情况下新装环境已内置)
pip install pillow-heif

# 升级到包含该修复的版本
hermes update

顺带一提,这个改动是一次 salvage(把社区贡献者的提交重新落到重构后的代码上):原始 PR 来自 @samfoy,同一类问题在 MoonshotAI/kimi-code 那边也被复现并修掉过——他们的 ReadMediaFile 同样用一份写死的 PNG/JPEG/GIF/WebP 白名单,没有任何 HEIF 分支。两边的结论一致:格式白名单必须包含容器嗅探,否则“来自手机的照片”这一类输入会稳定失败。

症状二:视觉工具用着用着就不见了

第二个症状更隐蔽。在长驻进程(gateway、桌面端后端、长时间开着的会话)里,vision_analyze 可能在某个时刻开始报“视觉能力不可用”,重启之后又好了。你大概率会怀疑是 provider 抽风或者配额用尽,但真正的元凶是启动探测把代理客户端缓存写脏了。

背景是这么回事:Hermes 启动时会做一次“视觉能力探测”,用的是辅助模型通道(auxiliary client)。为了不让探测本身的网络往返拖慢启动,探测阶段会用一个桩客户端(stub)代替真实客户端,探测结束后再换成真的。问题是这个桩被塞进了真正的运行时缓存键下面。

后果在 PR #110136 里写得很清楚:下一次探测命中缓存,走到 _compat_model(),对桩对象取 _client 属性时抛了 RuntimeError(getattr 的默认值只能吞掉 AttributeError,吞不掉别的异常),而 check_vision_requirements() 把这个异常当成“视觉不可用”直接咽下去了——于是这个进程往后的余生都认为视觉不能用。更要命的是,只有带斜杠的模型 ID(比如 vendor/model 这种写法)才会走到那条分支,所以报这个 bug 的人几乎清一色是配了 NVIDIA、LM Studio、koboldcpp、litellm 这类 provider/model 形式的用户。

修复的要点是:探测模式下解析出的客户端不写缓存。而且判断条件用的是“当前处于探测模式”,而不是“这个对象是不是桩”——因为桩外面可能还包了一层适配器(CodexAuxiliaryClient 之类的包装),用类型判断会漏掉这种情况。

需要说清楚的是:这个修复不会自动治好已经在运行的进程。 桩已经被写进那个进程的缓存了,升级后需要重启一次(升级本身通常就包含重启),重启之后缓存是干净的。

自己动手指一次

想确认你的机器上是哪种情况,不用猜,直接跑:

# 1) 看 file 命令怎么认这个文件(很多 iPhone 照片会被认成 HEIC)
file ~/Downloads/IMG_1234.HEIC

# 2) 看看 Hermes 侧的解码能力:装上 pillow-heif 后,Pillow 能打开 HEIC
python3 -c "import pillow_heif, PIL; pillow_heif.register_heif_opener(); im=PIL.Image.open('/path/to/IMG_1234.HEIC'); print(im.format, im.size)"

# 3) 在会话里直接让 agent 看图(路径换成你的真实文件)
#    > 用 vision_analyze 看一下 /path/to/IMG_1234.HEIC,告诉我里面有什么

第 2 步如果报“没有 pillow_heif 模块”,那就是解码器没装;如果第 2 步能打开、第 3 步仍然说“不是可识别的图片”,那基本可以确定你跑的是修复之前的代码,先升级。

另外,如果你经常要处理手机拍的媒体文件,可以顺手看看站内这两篇:处理沙箱里的图片与视频投递见 沙箱媒体投递指南,文档类文件的读取(PDF、Word、Excel、EPUB)见 read_file 文档提取。图片本身太小、需要放大再喂给模型的场景,见 媒体放大指南。

注意事项与版本状态

  • 归一化是“转码”,不是“原地支持”。 HEIC 会被转成 PNG 再交给视觉模型。这意味着一次大图转换会占用额外的 CPU 与临时空间;如果你要处理一批照片,别一次全丢进去。
  • 不是所有“看起来能打开”的文件都过关。 你的桌面看图工具能开,是因为它自带解码器;Hermes 走的是服务端解码路径,两者不是一回事。
  • 两个修复互相独立。 格式嗅探解决“这个文件认不认识”,探测缓存解决“这个进程还愿不愿意看图”。只升一次级、两个都会带上。
  • 发布状态:这两个修复(#106408、#110136)合并时间都在 v0.21.2 打标签之后,写作时处于 main 分支上、尚未进入已发布的版本;跑一次 hermes update 取最新代码,或等下一个版本发布即可带上。

小结

一句话总结这两件事:格式嗅探要认容器,不只看扩展名;启动探测不该把桩写进真缓存。

放到日常使用上就三条:

  1. iPhone / 安卓手机拍的照片,现在可以放心直接丢给 vision_analyze。
  2. 升级后如果视觉工具还是“不认识”,检查一下 pillow-heif 是否装好。
  3. 如果某个长跑进程突然说视觉不可用,先重启它,再看是不是踩了探测缓存的旧 bug。

下次 agent 再说“这不是一张图片”,你至少知道该往哪查了。