fix(photo-exif): shared HEIC-capable image decode for both AI passes#282
Conversation
Add lib/decode-image.js (sharp-primary, heic-decode/libheif-WASM fallback, orientation-safe) so the face and caption workers can read pixels from the ~47% of this library's photos sharp's bundled libheif can't decode. Caption worker also survives an isolated VLM failure now, stopping only after VLM_MAX_CONSECUTIVE_FAILURES in a row instead of on the first one. Closes #280
Found in pre-PR review: a value of 0 (or negative) broke the caption worker before its first VLM call, since the failure counter starts at 0 and 0 >= 0 is already true. Clamp to the default (5) below 1. Refs #280
There was a problem hiding this comment.
Pull request overview
This PR introduces a shared, HEIC-capable image decode helper for the photo-exif connector so both AI enrichment passes (face detection + captioning) can reliably obtain pixel data, including for HEIC/HEIF files that sharp can’t decode on this environment.
Changes:
- Adds
lib/decode-image.jsto centralize image opening with sharp-first / heic-decode fallback and orientation handling. - Updates
lib/face-detect.jsandcaption-worker.jsto use the shared decode helper (including caption-worker downscale/re-encode for VLM calls). - Changes caption-worker failure handling to stop only after
VLM_MAX_CONSECUTIVE_FAILURESconsecutive VLM failures (config + tests + docs).
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| connectors/photo-exif/lib/decode-image.js | New shared openImage helper with sharp/heic-decode dispatch + orientation policy. |
| connectors/photo-exif/lib/face-detect.js | Switches face detection to use openImage for a shared decode path. |
| connectors/photo-exif/caption-worker.js | Uses openImage, downscales/re-encodes for VLM, and adds consecutive-failure stopping logic. |
| connectors/photo-exif/test.mjs | Adds unit tests for decode-image dispatch/fallback/rotation and caption-worker failure behavior. |
| connectors/photo-exif/README.md | Documents HEIC decoding behavior and updated caption-worker semantics. |
| connectors/photo-exif/.env.example | Documents VLM_MAX_CONSECUTIVE_FAILURES. |
| connectors/photo-exif/package.json | Adds heic-decode dependency and includes an allowScripts entry for onnxruntime-node. |
| connectors/photo-exif/package-lock.json | Locks heic-decode and libheif-js transitive dependency details. |
Files not reviewed (1)
- connectors/photo-exif/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…ly fall back Copilot review on PR #282: sharp defers real decode errors until an output op runs, so sharp(absPath).rotate() never threw for an undecodable file — the fallback to heic-decode for a mislabeled extension (e.g. a real HEIC saved as .jpg) never actually triggered, the same trap as sharp.metadata(). Force the decode on a clone before returning. Also fixes a misleading "fallback" log line for the .heic/.heif dispatch-first case. Refs #280
|
Thanks for the review — went through all three:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 8 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- connectors/photo-exif/package-lock.json: Generated file
Comments suppressed due to low confidence (1)
connectors/photo-exif/lib/decode-image.js:73
- The log message says "fallback decoder" even in the common case where
.heic/.heifdispatch selects heic-decode as the primary path. This can be misleading in logs; consider wording it neutrally (still noting the performance cost).
console.error('photo-exif: using heic-decode (the slower ~1s/photo fallback decoder) for', absPath);
… heic-decode path Second Copilot review round on PR #282: - caption-worker.js's VLM-failure catch continue'd past the throttle sleep, so repeated failures hammered the VLM endpoint back-to-back instead of pacing like a success does. Throttle applies on failure too now, verified by a timing-gap test. - decode-image.js's heic-decode path copied the whole decoded RGBA buffer via Buffer.from(typedArray); use a zero-copy Buffer view over the same ArrayBuffer instead (a 12MP RGBA decode is already ~48MB). Refs #280
|
Second round — both confirmed and fixed in 5a9213a:
The suppressed "using heic-decode fallback" wording comment was already addressed in the first round (3d97fda). |
Closes #280
Summary
connectors/photo-exif/lib/decode-image.js— a sharedopenImage(absPath)helper used by both AI enrichment passes.sharpstays the primary decoder for every format;heic-decode(pure-JS libde265/WASM, no native build) is the fallback for the ~47% of this library's HEICs that sharp's bundled libheif can't open (two distinct failure modes, both invisible tosharp.metadata(), which never actually decodes).lib/face-detect.jsandcaption-worker.jstoopenImage, replacing their own rawsharp(absPath)/readFilecalls..rotate()(EXIF); the heic-decode path deliberately does not — libheif already applies the HEIC container's own rotation during decode, so a second rotate would double-rotate every portrait iPhone photo.caption-worker.js's VLM failure handling changes from "abort the whole run on the first failure" to a consecutive-failure counter (VLM_MAX_CONSECUTIVE_FAILURES, default 5, resets on any success) — one bad photo at 2am shouldn't waste an overnight run.content_hashis still computed from the original file bytes, never the re-encoded JPEG sent to the VLM.allowScriptsentry foronnxruntime-nodethat was sitting uncommitted onmain(already committed tomaindirectly ahead of this branch, per the issue's own note).Changes
connectors/photo-exif/lib/decode-image.js(new) —openImage/createOpenImagewith an injectable decoder seam for testsconnectors/photo-exif/lib/face-detect.js— usesopenImage, drops the now-unused inline sharp import/pipeline builderconnectors/photo-exif/caption-worker.js— usesopenImage+ resize/re-encode for the VLM call; consecutive-failure counterconnectors/photo-exif/package.json/package-lock.json— addsheic-decode(noallowScriptsneeded, pure JS/WASM)connectors/photo-exif/.env.example— documentsVLM_MAX_CONSECUTIVE_FAILURESconnectors/photo-exif/README.md— new "HEIC decoding" section, updated caption-worker/setup/limitations/files sectionsconnectors/photo-exif/test.mjs— decode-image dispatch/fallback/no-rotation tests, caption-worker consecutive-failure testsVerification
npm test(root): 186/186 passnpm run check:boundary: green (nosrc/import from the connector)connectors/photo-exif:npm test— 37/39 pass in the full run; the 2 failures are a pre-existing Node v26fetch+process.exit()native-crash race on this dev box (reproduced with plainfetch()calls and zero sharp/heic-decode involvement, and it already hits the same untouchedface-workertest on unmodifiedmain) — both new tests pass reliably in isolation (3/3). This connector's suite isn't part of CI (manual/on-device only, perCLAUDE.md).POST /api/remember→POST /api/recallreturned the memory with a distance score, 0 server errors.VLM_MAX_CONSECUTIVE_FAILURES=0would stop the worker before its first VLM call) — fixed and covered by a new test in the same branch.Test plan
openImagedecodes the two known-failure-mode HEIC shapes (covered structurally via injected-decoder dispatch tests; real-file decode is a manual, on-device check per the issue — no HEIC fixture exists in this repo)VLM_MAX_CONSECUTIVE_FAILURESconsecutive failures (and doesn't misbehave at the0edge case)npm run check:boundarygreennpm testgreen🤖 Generated with Claude Code
https://claude.ai/code/session_01LHTyidSMEdQyNZmHvHYdfP