Tus fotos del iPhone por fin funcionan: decodificación HEIC en vision_analyze y herramientas de visión que dejan de desaparecer


Haces una captura de pantalla de un error en el móvil, se la pasas por AirDrop a la máquina donde corre Hermes y le pides al agente que le eche un vistazo. Te responde: source is not a recognized image. El archivo está ahí, tú lo abres sin problema en local, pero para el agente no es una imagen en absoluto. Mientras tanto, en un proceso de larga duración pasa algo más raro: vision_analyze funcionaba esta mañana y esta tarde te dice que la visión no está disponible, y tú no has tocado nada.

Esos dos síntomas vienen de dos bugs independientes, ambos corregidos hace poco. Uno es un sniffer de formatos de imagen que nunca aprendió a reconocer el HEIC del iPhone; el otro es una sonda de arranque que envenenaba la caché de un cliente y hacía que las herramientas de visión desaparecieran por su cuenta. Esto es lo que pasó y cómo comprobar tu propia instalación.

Síntoma uno: una foto del iPhone “no es una imagen”

Primero, la versión corta de la herramienta implicada: vision_analyze es como el agente mira una imagen. Le das una ruta de archivo o un adjunto, envía la imagen a un modelo de visión y te devuelve una descripción o una respuesta.

Las fotos del iPhone vienen por defecto en HEIC (también llamado HEIF) y no pertenecen a la misma familia que JPEG. JPEG son píxeles con una cabecera; HEIC va envuelto en un formato contenedor llamado ISO-BMFF. Piensa en el contenedor como una caja de envío: la marca impresa en la caja decide si lo que hay dentro es una imagen fija HEVC, una imagen AVIF o un fragmento de vídeo. Hermes decide si un archivo es una imagen —y de qué tipo— leyendo los primeros bytes (magic-byte sniffing) en lugar de fiarse de la extensión, porque los pipelines de subida renombran HEIC como .jpg continuamente. Las extensiones mienten; las cabeceras no.

El sniffer antiguo solo reconocía las tarjetas de presentación de PNG / JPEG / GIF / WebP. No tenía ninguna rama para contenedores ISO-BMFF. Así que un archivo HEIC salía del sniffer como “desconocido” y nunca llegaba a la ruta de recodificación que ya se ocupaba de SVG, BMP y TIFF. La imagen no era inconvertible. Es que nunca cruzó la puerta.

Solución uno: leer la marca de la caja ftyp (PR #106408)

La corrección añade una rama ISO-BMFF ftyp al sniffer. Los bytes 4 a 8 del contenedor son siempre ftyp, seguidos de una marca principal (major brand) y una lista de marcas compatibles: la etiqueta de la caja más su lista de “también conocido como”.

Tres detalles de tools/vision_tools_image_prep.py que importan:

  1. Se consultan tanto la marca principal como la lista de marcas compatibles. AVIF suele declarar mif1 como marca principal y solo identifica AV1 en la lista de compatibles. Una comprobación que mirase únicamente la marca principal declararía AVIF como desconocido.
  2. El escaneo queda acotado por el tamaño de caja declarado y falla en cerrado (fail closed) ante un tamaño malformado. Ese campo de longitud es entrada controlada por un atacante; dejar que ensanche el escaneo hacia las cajas siguientes significa que el resultado del sniff puede construirse en lugar de leerse. Un tamaño inválido es un fallo, no una conjetura.
  3. Otras marcas ftyp siguen sin reconocerse. mp42 (un vídeo MP4) no es una imagen y no se entrega a un modelo de visión.

Una vez reconocido, el archivo toma la ruta existente _normalize_to_supported_image y se convierte a PNG antes de llegar al modelo. La decodificación de HEIC viene de pillow-heif, registrado como dependencia soft: los wheels precompilados incluyen libheif, así que no hay librerías del sistema que instalar. Si falta el decodificador, obtienes un mensaje accionable en lugar de un stack de visión roto.

AVIF se trata un poco distinto a propósito: Pillow 11.3 y posteriores decodifican AVIF de forma nativa, mientras que muchos wheels de pillow-heif no incluyen ningún códec AV1, así que AVIF no depende de pillow-heif.

pillow-heif es ahora una dependencia core (pillow-heif>=1.4.0,<2), así que una instalación nueva funciona sin más:

# Alternativa si corres en un entorno reducido sin el decodificador
pip install pillow-heif

# Trae la versión que incluye ambas correcciones
hermes update

Conviene saberlo: esto llegó como rescate de una contribución de la comunidad (@samfoy), y la misma clase de bug se corrigió upstream en MoonshotAI/kimi-code — su ReadMediaFile también rechazaba los .heic de iPhone contra una allowlist fija de PNG/JPEG/GIF/WebP sin rama HEIF. La conclusión es la misma en ambos lados: una allowlist de formatos necesita sniffing de contenedor, o “las fotos hechas con el móvil” fallan para siempre.

Síntoma dos: herramientas de visión que desaparecen sin hacer ruido

El segundo síntoma es más traicionero. En un proceso de larga vida —un gateway, un backend de escritorio, una sesión abierta durante horas— vision_analyze puede empezar a responder que la visión no está disponible, y reiniciar lo arregla. Los sospechosos naturales son el provider o tu cuota. El culpable real era una sonda de arranque que escribía un cliente stub en la caché del runtime.

Este es el mecanismo. Al arrancar, Hermes lanza una sonda de capacidad de visión sobre el cliente del modelo auxiliar. Para mantener el round trip de red de esa sonda fuera de la ruta de arranque, la sonda usa un cliente stub y luego lo sustituye por el real. El bug: el stub se guardaba bajo la clave real de la caché del runtime.

El PR #110136 detalla la consecuencia. La siguiente sonda encuentra el stub en caché, llega a _compat_model() y hace getattr(stub, "_client", None), lo que lanza RuntimeError, porque el valor por defecto de getattr solo absorbe AttributeError. check_vision_requirements() se lo tragaba convirtiéndolo en False durante el resto de la vida del proceso. Solo los ids de modelo con barra llegan a esa rama, y por eso los reportes se concentraban en configuraciones vendor/model: NVIDIA, LM Studio, koboldcpp, litellm.

La corrección mantiene los clientes en modo sonda totalmente fuera de la caché y basa la guarda en estar en modo sonda en lugar del tipo del objeto, porque los wrappers de adaptador (CodexAuxiliaryClient y compañía) envuelven al stub y anulan cualquier comprobación isinstance.

Una advertencia honesta: esto no cura un proceso que ya está corriendo. El stub está en la caché de ese proceso; la corrección surte efecto tras un reinicio, que de todos modos te da una actualización.

Comprueba tu caso tú mismo

No adivines en qué estado está tu instalación. Tres comandos:

# 1) Qué es el archivo en realidad
file ~/Downloads/IMG_1234.HEIC

# 2) ¿Puede decodificarlo el lado de Python? (con pillow-heif instalado)
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) Pídeselo directamente al agente, en una sesión
#    > use vision_analyze on /path/to/IMG_1234.HEIC and tell me what it shows

Si el paso 2 lanza “no module named pillow_heif”, te falta el decodificador. Si el paso 2 funciona pero el paso 3 sigue diciendo que la fuente no es una imagen reconocible, casi con seguridad estás ejecutando código anterior a la corrección: actualiza.

Si trabajas mucho con medios del móvil, dos artículos relacionados de este sitio te interesan: la entrega de imágenes y vídeo en sandbox en la guía de entrega de medios en sandbox y la extracción de documentos para PDF, Word, Excel y EPUB en extracción de documentos con read_file. Para imágenes que simplemente son demasiado pequeñas, mira la guía de escalado de medios.

Advertencias y estado del release

  • Normalizar significa transcodificar, no soporte nativo. El HEIC se convierte en PNG antes de llegar al modelo, así que un lote de fotos grandes cuesta CPU y disco temporal. No le tires mil archivos de golpe.
  • “Mi escritorio sí lo abre” no demuestra nada. Tu visor trae su propio decodificador; Hermes decodifica en el servidor por una ruta distinta.
  • Las dos correcciones son independientes. El sniffing de formato responde a “¿reconozco este archivo?”; la caché de la sonda responde a “¿este proceso sigue queriendo mirar imágenes?”. Una sola actualización trae ambas.
  • Estado del release: los dos cambios (#106408, #110136) se fusionaron después del tag v0.21.2, así que a día de hoy viven en main y todavía no están en una versión etiquetada. hermes update toma main, o espera al siguiente release.

La conclusión

Dos reglas cubren ambos bugs: haz sniffing del contenedor, no de la extensión, y nunca guardes en caché un stub de sonda como si fuera un cliente real.

En el día a día eso significa tres cosas:

  1. Las fotos de iPhone y Android pueden ir directas a vision_analyze desde ya.
  2. Si la visión sigue negándose después de actualizar, comprueba que pillow-heif esté presente.
  3. Si un proceso de larga duración dice de repente que la visión no está disponible, reinícialo primero y luego mira si estabas con el bug antiguo de la caché de la sonda.

La próxima vez que un agente insista en que tu imagen no es una imagen, sabrás exactamente dónde mirar.