记录一下那天晚上的排查过程。FastAPI 0.111 的接口用 UploadFile 接收客服发来的 WebP 动图,交给 Pillow 12.0 的 Image.open 读出来再送视觉模型识别,结果每次都只认出封面帧,用户说的是第 4 帧里的报错弹窗。同一个文件用浏览器打开是正常播放的,试过把文件后缀改成 gif 依然只读首帧。

刚开始盯着「转给模型时按 RGBA 直接送,透明区域被压成黑块遮住文字」,折腾了半个多小时,排掉了。

原因不止一个,得分开看。Image.open 默认只解码首帧,取后续帧要显式 seek。还有一种是n_frames 属性在 WebP 上要读完文件头才准,提前读会得到 1。

少数情况下是前端上传前做过一轮压缩,动图被转成了静态图。

动手时先确认这个 WebP 到底有几帧

python -c "from PIL import Image;im=Image.open('a.webp');print(im.n_frames, im.size, im.mode)"

然后按帧号取到用户说的那一帧并落盘

python -c "from PIL import Image;im=Image.open('a.webp');im.seek(3);im.convert('RGB').save('f4.jpg',quality=90)"

随后把落盘的帧直接喂给视觉模型验证

curl http://127.0.0.1:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{"model":"qwen2.5-vl","messages":[{"role":"user","content":[{"type":"image_url","image_url":{"url":"file:///tmp/f4.jpg"}},{"type":"text","text":"读出弹窗文字"}]}]}'

剩下的就是在接口里按帧数判断是否走抽帧分支

python -c "from PIL import Image;print(Image.open('a.webp').n_frames > 1)"

怎么算真的好了,看的是同一批现象。第 4 帧抽出来能读到完整弹窗文字,接口返回的识别结果与用户描述一致,静态图链路的结果没有变化。

补一句。动图不是「一张图」,seek 之后必须重新 convert,RGBA 直接存 JPEG 会报错,透明区域变成黑块,文字识别基本就废了。如果同一现象隔几天又出现,说明根因没找到,别当偶发处理。

作者 9823189