- Native Swift ML engine, the new default (with automatic Python-venv fallback). The ML service (CLIP visual and text, face detection and recognition, OCR) can now run as a single native Swift binary instead of the ~1.5 GB Python venv (torch, mlx, onnxruntime, opencv, insightface) and the mlx-pin crash surface that came with it (#38, #103). CLIP runs on
mlx-swiftwith the same mlx-community weights; face embedding runs the identical InsightFace ArcFace model through onnxruntime's C API; detection and OCR use Apple's Vision framework. Same models and weights, so embeddings stay in the same space as an existing Immich index and face clusters: no re-index, no re-cluster. Validated against the Python service on real library photos (CLIP visual cosine ~0.996, CLIP text 1.0, face embedding ~0.999 bbox-matched, OCR identical). The native engine is preferred automatically; if its bundle or models are missing, or it fails to start or become healthy, the accelerator falls back to the Python venv so ML is never left down. On a fresh install the models (~740MB) download once in the background on first native start, so ML runs on the Python engine for a few minutes until they arrive. To switch back to the Python engine, set"ml_engine": "python"in~/.immich-accelerator/config.jsonand restart the accelerator (brew services restart epheterson/immich-accelerator/immich-accelerator); set it back to"native"or remove the key to return. Distributed as an ad-hoc-signed relocatable bundle over Homebrew, so there is no notarization step and no Gatekeeper friction.
- Install could silently ship a broken ML venv (#17, #105): the dashboard (
uvicorn) and the ML service (fastapi) both run out of one venv built in the formula'spost_install. If thatpip installpartially failed (typically a flaky connection on the largetorchdownload thatmlx_clippulls in), the venv existed but was missing packages, and nothing caught it, so the CLI reported the new version while the dashboard and ML crashed at runtime withModuleNotFoundError: No module named 'uvicorn'/'fastapi'. The formula now (1) retries the dependency install a couple of times so a transient network blip self-recovers, and (2) verifiesfastapi,uvicorn,mlx, and the dashboard app actually import after install, failing the install loudly (pointing atbrew reinstall immich-accelerator) instead of shipping a half-working setup. If you hit this on 1.5.31,brew reinstall immich-acceleratorrebuilds the venv. Reported by @jhoogeboom and @yz47.
- Reverted the mlx pin lift: mlx 0.32.0 also crashes CLIP (#103). v1.5.29 lifted
mlx>=0.22.0,<0.31.2to allow 0.32.0 after a re-test that only exercisedmlx_clip.image_encoderdirectly. That was insufficient: the real ML service/predictpath hard-crashes under 0.32.0 withstd::runtime_error: There is no Stream(cpu, 1) in current thread(the CPU-stream analogue of 0.31.2's #38 gpu-stream crash), on multiple Macs. The pin is back tomlx>=0.22.0,<0.31.2(resolves to 0.31.1, the last known-good). Runbrew upgrade immich-accelerator; if you were on 1.5.29/1.5.30 with ML failing, this restores it. Reported by @shtefko.
scripts/ml-preflight.py, a real-model gate for mlx/ML changes. It boots the actual ML service withSTUB_MODE=falseand hammers/predictwith concurrent real CLIP inference, detecting the SIGABRT that aSTUB_MODEtest or animage_encoder-only loop misses. Verified it fails on mlx 0.32.0 (captures theStream(cpu, 1)abort) and passes on 0.31.1. The mlx pin and themlsubmodule are now gated on this on Apple Silicon (see CLAUDE.md), and the weekly mlx-pin check points re-tests at it. This is the gap that let the v1.5.29 regression ship.
- Camera RAW thumbnails failed (#99): Canon CR2 (and other camera RAW such as Nikon NEF, Sony ARW, Adobe DNG, Fujifilm RAF) died in
AssetGenerateThumbnailswithtiff2vips: Old-style JPEG compression support is not configured. Sharp's prebuilt libvips on macOS lacks old-style-JPEG support and has no dcraw/libraw loader, so it cannot read a RAW file's embedded image. The HEIC decode shim now also detects RAW by extension and pre-decodes it via Homebrewvips(fuller libtiff/libjpeg for TIFF-based RAW like CR2/NEF/ARW/DNG, plus libraw for the rest), the same libvips Docker Immich uses, before handing the result to Sharp. Works headless; OCR and face are untouched (RAW decode affects thumbnail generation only). Verified on an M4 Mac Mini with a real Canon CR2: bundled Sharp alone fails with the tiff2vips error, and with the shim it produces a valid thumbnail. Closes another macOS-only decode gap after the HEIC fix (v1.5.28). Reported by @shtefko. - Headless HEIC decode was silently falling back to
sips: Sharp's prebuilt libvips exports a bogusVIPSHOME(the GitHub-runner path it was built in, e.g./Users/runner/work/sharp-libvips/...) into the process environment when it loads. The decode shim spawned the Homebrewvipswith that inheritedVIPSHOME, sovipslooked for its loader modules (libheif for HEIC, the RAW loaders) in a nonexistent directory and failed withVipsForeignLoad: ... is not a known file format, falling back to Apple'ssips. Becausesipsneeds a logged-in GUI session, on a headless Mac the primaryvipspath (the entire point of the v1.5.28 HEIC fix) never actually ran, so HEIC thumbnails quietly depended on a desktop session. The shim now drops aVIPSHOMEthat points at a nonexistent directory before spawning a decoder, sovipsuses its own compiled-in module path and decodes HEIC and RAW headless as intended; an intentional, existingVIPSHOMEis preserved. Surfaced while adding RAW support and verified on a headless M4 Mac Mini (HEIC now decodes viavips, notsips).
- The
vipsdecode intermediate is now written as a lossless deflate-compressed TIFF (roughly 40% smaller than uncompressed) to trim peak memory and temp I/O when decoding large RAW frames. Pixels are unchanged; Sharp reads the deflate TIFF the same way.
- Lifted the mlx pin now that 0.31.2's CLIP crash is fixed (#38): the bundled
immich-ml-metalfork pinnedmlx>=0.22.0,<0.31.2because mlx 0.31.2 hard-crashed CLIP inference in the worker's thread pool (std::runtime_error: There is no Stream(gpu, 0) in current thread, SIGABRT). mlx 0.32.0 is verified to fix it: 1,800 concurrent CLIP inferences from a 4/8-workerThreadPoolExecutoragainst realclip-vit-base-patch32weights on Apple Silicon, zero aborts. The pin is nowmlx>=0.22.0,!=0.31.2,<0.33.0, which excludes only the known-bad 0.31.2, allows the verified 0.32.x (currently the sole in-range build), and caps below the untested 0.33 minor. The weekly mlx-pin check now tracks that 0.33.0 ceiling instead of 0.31.2, so it stops nagging about 0.32.0. Blast radius is CLIP only: OCR runs on Apple Vision, face on onnxruntime, neither imports mlx.
- HEIC thumbnails failed on headless Macs (
Input file contains unsupported image formatonAssetGenerateThumbnails): the HEIC decode shim transcoded via Apple'ssips, butsipsneeds a logged-in GUI (Aqua/WindowServer) session, so in a headless launchd service it silently produced empty output and no HEIC thumbnail was generated. The shim now decodes via libheif (vips, already a dependency, thenheif-convert) as the primary path, which works with no GUI session and matches Docker Immich's own libvips+libde265 output;sipsremains a last-resort fallback for a logged-in desktop. Each decoder's output is validated before use, and the shim logs which decoders it resolved at startup.
- Immich 3.0 core plugin failed to import (
Failed to import plugin from /build/plugins/immich-plugin-core): the v1.5.24 layer-download early-exit treatedplugins/<name>/dist/plugin.wasmas "plugin complete", but 3.0 ships the plugin'smanifest.json(at the plugin root) in a separate image layer. Extraction stopped after the wasm layer and dropped the manifest, so the worker could not import the plugin and workflow features were unavailable. The fix has two parts:_build_has_core_pluginnow requires BOTH the manifest and the wasm before the early-exit fires; and the cached-server check now verifies the plugin actually belongs to the version being served before trusting the cache, so a broken 3.0 install self-heals on the next start after upgrading. Becausebuild-datais a single shared directory that only reflects the last-extracted version, that check is keyed on a version stamp written tobuild-dataonce it is complete, which avoids both trusting a stale cross-version plugin and needlessly re-downloading a cache that is fine. Existing installs (whose completebuild-datapredates the stamp) and offline--import-serverusers are adopted in place rather than re-downloaded, by matching the plugin against the requested version's layout, so upgrading is free. Applies to the ghcr download, local-Docker extraction, and offline import paths. Found by dogfooding 3.0.
- Motion-photo / video metadata errors on Immich 3.0 (#95):
probePacketsfailed withTypeError: Cannot read properties of undefined (reading 'split'). The pg_dump shim wrapschild_process.execFilebut dropped itsutil.promisify.customsymbol, so Immich 3.0'sconst execFile = promisify(execFileCb)fell back to the default promisifier and resolved to a bare stdout string instead of{ stdout, stderr }; destructuring{ stdout }then yieldedundefined. The shim now reinstalls a custom promisifier sopromisify(execFile)keeps the{ stdout, stderr }contract. A 3.0-only codepath, latent until now. Reported by @shtefko.
- Worker crash from file-descriptor exhaustion (#89): Immich leaks file handles while processing some media (a reporter hit it on Sony A7IV XAVC files on an external drive), opening each source file and never closing it. His diagnostic (from v1.5.23) showed ~49,000 open descriptors, all pointing at the source files; once the fd table is that large macOS fails
spawn()withEBADFand the worker crashes. The watcher now monitors the worker's live open-fd count (summed across worker processes, via libproc) and restarts the worker before the table gets dangerous (default 10,000; a healthy worker sits around 150), with a cooldown to avoid thrash. Tunable viaIMMICH_ACCEL_FD_RESTART_THRESHOLD(0 disables) andIMMICH_ACCEL_FD_RESTART_COOLDOWN. This is a safety net for an upstream Immich leak, not a fix for the leak itself.
- Plugin-path detection now recognizes both the 2.7 layout (
corePlugin/) and the 3.0 layout (plugins/immich-plugin-core/). Immich 3.0 renamed the WASM core-plugin directory, which made the layer-download early-exit never fire on 3.0, so setup downloaded every image layer instead of stopping once the small plugin layer arrived. Setup is fast again on 3.0. The rest of the 3.0 worker (extraction, native Sharp, ffmpeg wrapper, plugin load, worker start) was verified end to end. - README notes the worker tracks your Immich version, verified with 2.7.x and 3.0.x.
Note: moving your own Docker stack from 2.7 to 3.0 requires Immich's database migration (pgvecto-rs to VectorChord); that is on the Immich side, not the accelerator.
spawn EBADFinstrumentation (#89): when a child-process spawn throws (the crash a reporter hits during ffmpeg capability detection, not reproducible on any environment we can build), the shim now logs the file-descriptor state at the point of failure, which of fd 0/1/2 is invalid, the requested stdio, and the process's open fds, then re-throws unchanged. Fires only on the error path; silent in normal operation. This is to capture the root cause on the affected machine.
- pg_dump shim wrapped
child_processspawn twice (#89): the shim calledinstall()for bothchild_processandnode:child_process, which are the same object on modern Node, so every spawn ran through two nested wrappers (twopg_dump_shim.jsframes in stack traces). Now guarded to patch a given module object once. This is hardening, not a confirmed fix for the reportedspawn EBADF, which could not be reproduced across Node 22/24, launchd-style stdio, high concurrency, or starved fd limits, and rewrites nothing for ffprobe. Reported by @KoenM9264.
brew cleanupfailing on root-owned.pyc(#86): the CLI wrapper now setsPYTHONDONTWRITEBYTECODE=1, so Python no longer writes__pycache__into the Homebrew Cellar. Previously, running the CLI as root (e.g.sudo immich-accelerator ...) left root-owned bytecode in the keg thatbrew cleanupcouldn't remove. Reported by @shtefko. After upgrading, clear any existing root-owned files once:sudo chown -R $(whoami):admin /opt/homebrew/Cellar/immich-accelerator.
- README house style: added badges, a Star History chart, and License/Credits sections, and aligned the footer to the canonical form. Documented that Immich's
Transcoding video ... without hardware accelerationlog is expected and benign on macOS (the ffmpeg wrapper does the VideoToolbox encode; Immich has no VideoToolbox option to report). Reported by @shtefko (#84). - README slimmed: Troubleshooting is now a collapsible symptom index (
<details>), and the Split deployment guide moved todocs/split-deployment.mdwith a short pointer. The common path reads top-to-bottom without scrolling past the long tail. - README polish: promoted the status from Alpha to Beta (in daily use), de-duplicated the Commands table (the
immich-acceleratorprefix is stated once, not on every row), and removed all em-dashes.
brew services stop/restartnow stop ML and the dashboard too, not just the worker (#81 follow-up). v1.5.18's stop handler used the normal sequential shutdown, which waits up to 5s per service; launchd SIGKILLs the watcher only a few seconds after signalling it, so the handler was cut off after the worker and left ML + dashboard running. The handler now signals all services up front, so they all stop even under launchd's short grace.
- Media-readiness gate no longer refuses a writable split setup (#80). The gate checked writability at the media root, but Immich only writes to the subdirs (
upload,thumbs, …) — a split deployment can have a root-owned, non-writable media root (e.g. a/datasynthetic link) while the subdirs are fully writable. The gate now places/verifies its marker across the media root and Immich's standard subdirs (root first, for back-compat), testing writability where Immich actually writes. A genuinely missing/unmounted root still correctly refuses. Reported by @shtefko. brew services stop/restartnow actually stop the worker (#81). The worker, ML, and dashboard run detached, so they survived when launchd signalled only the watcher — a stop or restart left them running. The watcher now traps SIGTERM/SIGINT and stops its services before exiting, so the service lifecycle behaves as expected. Reported by @shtefko.immich-accelerator startnow also starts the dashboard. Previously onlywatch/brew-services brought it up. Reported by @shtefko.
brew upgradenow actually takes effect. The worker and ML service run detached, so they survived abrew services restart— a fresh watcher would adopt the still-running old-code worker, and an upgrade silently didn't apply until a full manual stop/start. The watcher now stamps the version each worker starts with, restarts a worker found running stale code, and (in service mode) notices when the installed version on disk changes and relaunches itself so the new code — shims and fixes — comes up on a fresh worker within ~30s. No manual restart needed when running as a service. README updated.
- Media-readiness gate: don't start until the real media root is mounted (default on). On a network-mounted media root, the worker could start before the mount came up and write thumbnails into a local placeholder directory that the mount later masks (silent data loss). The worker now drops a small marker file in the media root on first start and verifies it on every subsequent start; if the marker is missing — a placeholder, or the mount isn't up yet — it refuses to start (the watch loop retries, so it comes up as soon as the mount appears). Mount-agnostic (works for local, NFS, and SMB), runs before the ML service so a not-ready mount never orphans it, and probes in a timeout-bounded subprocess so a hung mount can't wedge startup. Opt out with
"require_media_ready": false.
- Keep remote Postgres connections alive in split deployments (#74). When the worker and the database sit on different network segments, a stateful firewall or NAT can silently reap an idle connection; the worker then hangs on the next read until
read ETIMEDOUTand doesn't recover (intermittent, since only idle connections get reaped). Immich doesn't expose a keepalive setting, so the worker now preloads a small shim that enables TCP keepAlive on everypgconnection, keeping the socket warm so it isn't dropped. No-op for same-host setups; Immich's source is untouched. Tunable/disable viaIMMICH_ACCEL_PG_KEEPALIVE/IMMICH_ACCEL_PG_KEEPALIVE_MS. Reported by @shtefko.
- Actionable error when a fresh split deployment can't import geodata (#73). On a split setup whose frontend ran api-only from the start, the accelerator is the first microservices worker to run, so it attempts Immich's one-time reverse-geocoding (geodata) import — a large bulk insert that can break over a network DB connection (
write EPIPE), leaving the worker in a silent crash-restart loop. The worker log is now scanned on failure: when this signature is detected, the accelerator prints the cause and the fix (initialize geodata once on the frontend, then switch back toIMMICH_WORKERS_INCLUDE=api) instead of a raw stack trace. Documented in the split-deployment guide. Reported by @shtefko.
- Fix misplaced OCR bounding boxes; sync the ML service to upstream. OCR boxes were emitted in pixel coordinates, but Immich expects normalized 0–1 coordinates, so recognized-text boxes rendered in the wrong place. The bundled
immich-ml-metalis updated to upstreammain(carrying ourmlx<0.31.2pin), which fixes the bbox calculation and brings ~2 months of upstream improvements: ViT-L-14 auto-conversion to MLX with an open_clip fallback, face-recognition CoreML provider tuning (MLProgram/ANE), CLIP model load moved off the event loop, and new model mappings. Reported by @shtefko (immich-ml-metal#2).
- Cap service logs so they can't grow unbounded.
worker.log/ml.logwere opened in append mode and never rotated — a worker spewing stack traces for unsupported files (e.g. videos mislabeled.heic) had grown one log to 10GB. Each log is now capped at 200MB: thewatchloop rotates in place every cycle, andstartrotates before (re)opening. Rotation truncates the existing file rather than renaming it, so it works while the worker holds the file open (append mode), and the last 2000 lines of context are preserved.
- Dashboard progress now matches Immich's own counts (#68). Per-stage completion could read over 100% (e.g. "Neural Engine — 102.1%") and was generally inaccurate: the done-counts were taken from side tables (
smart_search,asset_job_status) unfiltered — including rows left behind by deleted/hidden assets — and divided by a single asset total. Each bar now counts over the same population Immich itself uses (perasset-job.repository.js): thumbnails/OCR over live non-hidden assets, CLIP/faces over assets-that-have-a-preview, video over live non-hidden videos. Done can no longer exceed total, and the percentages line up with Immich's Jobs page. (A 100% clamp remains as a safety net.) Reported by @Rustymage.
- Generate thumbnails for HEIC photos (#62). Sharp's prebuilt libvips on macOS bundles libheif without an HEVC decoder (AVIF only), so iPhone HEICs failed to decode (
bad seek/ "compression format has not been built in") and never got thumbnails. The native worker now routes HEVC-HEIC files through Apple's ImageIO (sips) before Sharp, via a preloaded module that wrapssharp— Immich's source is untouched. Handles tiled iPhone HEICs that even system libheif rejects (its iref security limit). AVIF, JPEG, and everything else are unaffected. Reported by @goldhandconsultancy.
- Fix a regression in 1.5.8's upload detection (#62, #64). 1.5.8 returned the resolved media location as the detected value, which combined with the start-time path check to block
startwith a false "path mismatch" even after a correct setup. Detection now keeps the rawIMMICH_MEDIA_LOCATIONvalue (the effective path is still used internally to pick the mount), and only bind mounts are considered. Upgrading from 1.5.8 is recommended.
- Detect the upload mount when uploads live at
/data(#62). Detection only matched a Docker mount whose destination contained/upload, so the modern Immich default —${UPLOAD_LOCATION}:/datawith noIMMICH_MEDIA_LOCATIONset — reported "Upload: not detected". Detection now resolves the media location the way Immich itself does (storage.service.js: explicitIMMICH_MEDIA_LOCATION, else whichever of/dataor/usr/src/app/uploadis mounted) and reports the matching mount. Reported by @goldhandconsultancy.
- Detect the media root correctly when a storage label is set (#61). The split-deployment path check assumed Immich's
upload/<uuid>/layout and mis-detected a date-nested folder (e.g.…/library/Anthony/2026/06) for anyone using a storage label or custom storage template — the suggestedupload_mountwould have gone stale next month. Detection now follows Immich's own path builder (<MEDIA>/library/<label|uuid>/…) and resolves the true media root. - Don't suggest an impossible synthetic link (#61). macOS synthetic links can only create a single top-level name, so the previous guidance to mirror Docker's container-default
/usr/src/app/uploadcould never work. The mismatch warning now offers achievable routes: a synthetic link when the media root is top-level, or re-pointingIMMICH_MEDIA_LOCATIONfirst when it isn't. Suggested commands useprintfso the tab separator is portable across bash and zsh. - Validate the
/buildsynthetic entry by content, not file existence (#61). A hand-edited/etc/synthetic.d/immich-accelerator(e.g. a manual upload-path entry) was treated as "build link configured," so the required/buildentry was silently skipped and Microservices came up red after reboot. Setup now checks for the actualbuildentry and appends it if missing, preserving any foreign lines. Reported by @Rustymage.
- Default local DB/Redis host to 127.0.0.1 instead of localhost (#59). On macOS,
localhostcan resolve to::1(IPv6) first while the generated Docker stack publishes Postgres/Redis on127.0.0.1only, so connections could fail or stall. Auto-detected local setups now use127.0.0.1; remote and manual configs are unchanged. Contributed by @hiisukun.
- Bump GitHub Actions to Node 24 runtimes (#60).
checkoutv4 → v6,setup-pythonv5 → v6, ahead of GitHub's June 16 forced Node 24 migration.
- Fix install and upgrade flow for Homebrew 5.1.15+ tap trust enforcement. Homebrew now refuses to load formulas from untrusted third-party taps:
brew install immich-acceleratorafter a barebrew taphard-fails for new users, and existing taps are silently skipped bybrew outdated/brew upgrade— installs go stale with no warning. Quick start now uses the fully-qualifiedbrew install epheterson/immich-accelerator/immich-accelerator(which Homebrew accepts without prior trust) plusbrew trustso upgrades keep working. The formula caveats now mention the trust step, and the README documents both failure modes and how to update the accelerator itself.
- Support authenticated Redis (#56). The native worker now forwards
REDIS_PASSWORDandREDIS_USERNAMEto Immich, so deployments that require a Redis password (e.g. TrueNAS) or ACL auth work. Contributed by @exkuretrol.
- Fix root-owned files from Docker containers (#52). Fresh installs now offer to run the Immich server container as the current user, preventing root-owned files in bind-mounted directories. Contributed by @hiisukun.
- Graceful uninstall when root-owned files exist.
uninstallnow explains the problem and suggestssudo rm -rfinstead of crashing with a raw PermissionError. - Fix
/etc/synthetic.dpermissions. A tight root umask could leave the directory unsearchable by non-root users, breaking the/buildfirmlink check. - Compose project named "immich". Was defaulting to "docker" (the parent directory name).
- Fix watchdog spawning duplicate workers (#51). Immich 2.7+ sets
process.title='immich', so the parent node PID exits while child processes keep running. PID tracking now falls back to scanning for liveimmichprocesses, preventing the watchdog from spawning duplicates. Stop also kills all orphaned immich processes.
- HEVC VideoToolbox output now always uses Apple-compatible hvc1 tag (#48). ffmpeg defaults to hev1 for HEVC, which Apple's decoder rejects. The wrapper now injects
-tag:v hvc1when encoding HEVC via VideoToolbox if the caller doesn't specify it.
- One-command fresh install.
immich-accelerator setupon a bare Mac now sets up everything — installs OrbStack if no Docker is found, creates the Immich Docker stack, configures the native worker, and starts all services. Two questions: where your photos are, where Immich should store its data. No manual docker-compose editing. - Managed Docker stack. The generated compose file lives at
~/.immich-accelerator/docker/. Re-running setup detects it and restarts if stopped, without re-prompting.
- UHDR JPEG thumbnails failing (#44). Sharp was building from source against system libvips which has a UHDR loader that breaks JPEG auto-detect. Now installs the official pre-built darwin-arm64 binary from npm, which bundles vips 8.17.3 without the UHDR loader. Matches stock Immich Docker behavior. Existing installs:
immich-accelerator setupto trigger a rebuild.
- Startup validates database and Redis connections with real queries (#42). Instead of just checking TCP connectivity, the preflight now runs
psql SELECT 1andredis-cli PING. Surfaces the actual error (auth failure, port conflict, connection reset) with actionable guidance instead of letting the worker crash with a raw ECONNRESET. - Auto-creates missing media subdirectories on startup (#43). If
upload/,thumbs/,encoded-video/, etc. are missing underIMMICH_MEDIA_LOCATION, the preflight creates them with.immichmarkers so Immich's StorageService doesn't crash.
- Environment preflight checks on start. Auto-detects and fixes common issues before launching the worker: ImageMagick HEIC codec (auto-reinstalls), NFS mount accessibility, DB/Redis connectivity for split setups. Runs silently when everything is healthy.
- CLIP/smart search crashes on MLX 0.31.2 (#38). MLX 0.31.2 introduced a threading regression that crashes with "no Stream(gpu, 0)" during CLIP inference. Pinned
mlx<0.31.2until upstream fixes it. Existing installs:brew reinstall immich-accelerator. - ML submodule updated with face embedding batch-dim fallback, CLIP model-swap retry, and gpu_lock clarification from upstream code review.
- iPhone 15+ Ultra HDR images failed thumbnail generation (#36). Sharp was using a pre-packaged binary without libultrahdr support. Now builds from source against Homebrew's libvips which includes it. Existing installs: run
immich-accelerator setupto trigger a rebuild.
- Dashboard shows worker memory usage. "Microservices Worker (450 MB)" next to the service status so memory growth during long thumbnail runs is visible (#33).
- Human-readable API error messages. Dashboard now shows specific messages for common failures (auth rejected, connection refused, timeout, empty response) instead of raw Python exceptions.
- Homebrew formula failed to install on every release since v1.4.4 (#31). The CI workflow that generates the formula used a bash heredoc containing backticks and em-dashes. Backticks triggered command substitution on the runner, em-dashes got mangled through the locale. Brew's Ruby installer rejected the resulting invalid UTF-8. Replaced with plain ASCII.
- ML service silently failed to start after brew upgrade (#29). Config stored a versioned Cellar path for
ml_dirwhich gets deleted on upgrade.cmd_startandcmd_watchnow auto-resolveml_dirvia the stable/opt/homebrew/opt/symlink on every run. Warns loudly when the ML venv is missing instead of silently skipping. - Dashboard showed "Idle" while Immich was processing (#28). The
/api/jobscall had a 2s timeout that caused intermittent failures under load, and errors were silently swallowed. Dashboard now shows the actual error when the API is unreachable, displays queue counts matching Immich's admin panel, and bumps the timeout to 5s.
- Database backups were silently 0 bytes (#24). Immich pipes
pg_dumpthroughgzip --rsyncable, which Apple's BSD gzip doesn't support.pg_dump_shim.jsnow reroutes those calls to Homebrew's GNU gzip (or strips the flag as fallback). The formula nowdepends_on "gzip"so fresh installs get GNU gzip automatically. - Watchdog could kill unrelated processes.
_kill_stale_processesmatched any command line containing the substringimmich. Replaced with precise patterns for the canonical worker (node … dist/main.js) and ML service (python -m src.main) launch shapes.
- Isolated E2E Immich stack (
scripts/e2e-stack.yml+e2e-stack.sh). Dedicated postgres / redis / api-only Immich server on port-shifted loopback addresses with throwaway state. The VM harness now requires this stack and refuses to run against prod Immich. - Real-data backup integration test that runs
pg_dump | gzip --rsyncablethrough the shim against the isolated Postgres and asserts the output is a valid non-empty pg_dump.
- Homebrew formula pulled node 25, breaking sharp on every fresh install: v1.4.x shipped with
depends_on "node"in the generated formula. Homebrew's defaultnodeformula tracks mainline (currently 25.x), but Immich 2.7.x pinsengines.node = 24.14.1andsharp@0.34.5's native addons fail to load on node 25 with aNODE_MODULE_VERSIONmismatch. Every freshbrew install immich-accelerator+immich-accelerator starthit an opaque worker crash atrequire('sharp')mid-Nest-bootstrap that looked like an Immich bug. Fix: pindepends_on "node@22"in the formula template and teachfind_node()to look under/opt/homebrew/opt/node@22/bin/node(keg-only — no/opt/homebrew/binsymlink). find_node()accepted unsupported node majors silently: previously it returned the firstnodebinary it found, regardless of version. Now filters toSUPPORTED_NODE_MAJORS = (22, 24)and installsnode@22if nothing compatible is present._rebuild_sharp()swallowed rebuild failures: loggedlog.errorand returned, letting the worker start and crash later with an unrelated-looking stack. Now raisesRuntimeErrorwith the rebuild stderr tail and a remediation pointing atbrew install node@22.
- Node preflight in
start: everyimmich-accelerator startnow re-resolves node viafind_node(), updatesconfig["node"]if the path changed, and compares against Immich'sengines.nodeparsed frompackage.json. Catches thebrew upgradedrift pattern where node silently jumps majors and breaks sharp, with a clear "install node@22" error before the worker ever spawns. - Sharp load preflight in
start: spawnsnode -e "require('sharp')"against the server dir. If it fails, auto-attempts a rebuild and retries. If the retry still fails, hard-errors with remediation. This turns "opaque worker crash 10+ seconds into Nest bootstrap" into a 1-second clearly-labeled check.
tests/test_fresh_install.py::TestNodeVersionPreflight— 11 new unit tests covering_node_major_version,find_nodeversion filtering,_check_node_engines_compatwith real stubbed node binaries,_verify_sharp_loadserror reporting, and a static check that the CI-generated Homebrew formula pinsnode@22.scripts/e2e-fresh-install.shstep 1b — assertsfind_node()on a real VM returns a node inSUPPORTED_NODE_MAJORS. Would have caught the v1.4.x regression on the first E2E run.scripts/e2e-fresh-install.shstep 3b — runs the full sharp preflight (_check_node_engines_compat+_verify_sharp_loads) against the extracted Immich server in the VM.scripts/e2e-fresh-install.shsteps 9–16 — real execution coverage: start ML inSTUB_MODE, hit/ping+/health+/predictfor actual JSON render (catches anyORJSONResponseregression), runimmich-accelerator ml-testagainst the stub, verify theNODE_OPTIONSpg_dump shim actually loads in a real node process, start the real worker and wait for theImmich Microservices is runningNest-bootstrap marker, run the dashboard against the live worker and confirmworker.alive=true, verify thestatussubcommand reports the running PID, verifystopcleanly terminates + is idempotent, verify a secondstartafterstopreaches Nest bootstrap again.scripts/e2e-bootstrap-vm.sh— installsnode@22instead of Homebrew-defaultnode, sets up the/buildsynthetic firmlink via dual/etc/synthetic.d/+/etc/synthetic.confentries (some macOS VM images only honor the legacy location), reboots the VM to activate the firmlink, and verifies/buildresolves post-reboot before saving the base snapshot. pip install now retries 3× on VM DNS flakes.
- NODE_OPTIONS shim path broken in v1.4.2 (#24 follow-up): The v1.4.2 commit that "polished" the pg_dump shim wrapping wrapped the path in single quotes, thinking Node would unquote them. Node doesn't — it splits
NODE_OPTIONSon whitespace and takes the literal characters. The quotes became part of the filename and every worker start failed withCannot find module ''/opt/homebrew/Cellar/…/pg_dump_shim.js''. Fix: wrap the path in double quotes, which is the only form Node'sNODE_OPTIONStokenizer honors universally (single quotes and backslash escapes both fail — both verified empirically against Node 25.2). Verified end-to-end against a real v1.4.2 brew install on an M4 Mac — shim loads,pg_dump (PostgreSQL) 18.3runs. - ORJSONResponse crash in ML service (#20 tail): The v1.3.4 commit dropped
orjsonfromml/requirements.txtwith an incorrect commit message claiming "not imported by our code."main.pyimportsORJSONResponsefromfastapi.responsesand uses it in 3 places, and FastAPI'sORJSONResponse.render()assertsorjson is not Noneat render time — every/predictrequest crashed withAssertionError: orjson must be installed to use ORJSONResponse. Fix: swap to stdlib-backedJSONResponse, keeping theorjsondrop (which was correct — its wheel broke Homebrew's dylib fixup). Verified end-to-end: ml-test passes 4/4 on real CLIP + OCR requests with no orjson in the venv.
- Static check:
ml/srcusesORJSONResponseonly ifml/requirements.txtpinsorjson— would have caught the v1.3.4 half-fix instantly. - Real-node integration: generate the exact
NODE_OPTIONSstringcmd_startbuilds, spawnnode --requireagainst a sentinel shim, assert it loads. Would have caught the v1.4.2 quoting bug before merge. - Static check:
cmd_start's shim-path escaping uses backslash form, not shell quoting.
- Path-mismatch probe false positive (#19 follow-up): The v1.4.1 split-setup probe queried
/api/librariesas its primary signal, but that endpoint only returns external libraries in Immich 2.7+ — the upload library (where web-UI uploads land) is implicit atIMMICH_MEDIA_LOCATIONand doesn't appear there. Result: any install with an external library plus a correctly-setupload_mountgot blocked atimmich-accelerator startwith a false "path mismatch" error. The probe now parses an upload-library asset'soriginalPath(filteringlibraryId: null) and skips external-library assets entirely. If no upload assets exist yet, the check is skipped — nothing to compare against. - External library path validation: The probe now also checks every external library's
importPathagainst the Mac filesystem. Missing paths produce a non-fatal warning per library with the library name and guidance to mount or synthetic-link them. Upload-library mismatch remains fatal (thumbnails WILL 404), external-library inaccessibility is advisory (worker can still process uploads + any libraries whose paths do resolve). - pg_dump ENOENT on database backup (#24): Immich's
DatabaseBackupServicehardcodes/usr/lib/postgresql/${version}/bin/pg_dumpin its dist, and on macOS that path doesn't exist, so the native microservices worker fails every backup cycle with ENOENT./usr/lib/is SIP-protected and there's no env-var escape hatch in Immich's code. Fix: a tiny Node runtime shim (immich_accelerator/hooks/pg_dump_shim.js) that monkey-patcheschild_process.spawn/spawnSync/execFileto rewrite the Linux postgres client path to/opt/homebrew/opt/libpq/bin/at call time. The shim is preloaded viaNODE_OPTIONS=--require …when we launch the worker. Immich's JS source on disk is never touched — the same interposition pattern we already use for the ffmpeg wrapper, applied at the Node module layer. Verified end-to-end on a real Mac with node + libpq 18.3.
- Split deployment clarity: Reworked the Split deployment section of the README to lead with the one requirement everyone needs to get right — both machines see the same files at the same absolute paths via a shared filesystem (NFS/SMB). There is no HTTP transport of thumbnails between hosts; the worker reads/writes directly to disk. Surfaced because a v1.4.1 user interpreted "match paths" as string matching. Also dropped the long-stale v0.x and v1.2.x migration sections.
- VM bootstrap script: Fixed three edge cases that kept the tart-based E2E harness from running cleanly — trap composition bug that left stale VMs behind, stdin-race on heredoc-over-ssh that silently dropped pip install commands, and
brew updatenetwork flakiness aborting bootstrap. The harness is now proven end-to-end against live Immich.
- Dashboard ModuleNotFoundError (#17): Homebrew formula wrapper now runs the CLI under the ML venv's Python instead of stock
python@3.11. The dashboard's lazyfastapi/uvicornimports now resolve on any fresh Mac — previously they only worked if the user happened to have the packages installed globally. Also added abrew testblock that force-loadsdashboard.create_appso this class of bug gets caught at audit time, not in the wild. - Missing corePlugin on OCI download (#18): Re-fix of a regression from the v1.3.3 fix. The layer loop had a
size_mb < 1early-break shortcut that fired before examining the current layer — stranding the tiny corePlugin COPY layer. Replaced with a version-aware break (_has_everything) that requirescorePlugin/manifest.jsonfor Immich 2.7+. Verified end-to-end against a live ghcr.io pull.
- Added
tests/test_fresh_install.py— unit tests for the layer-break logic, a static check thatfastapi/uvicornstay pinned inml/requirements.txt, an AST check thatdashboard.pyhas no top-level third-party imports, and a slow integration test that builds a pristine venv and confirmscreate_appworks with onlyfastapi+uvicorninstalled. - CI now runs on macOS 14 (Apple Silicon) in addition to Ubuntu. The
fresh-install-macosjob catches "works on my machine" bugs before shipping.
- libpq as Homebrew dependency:
psqlnow installed automatically viadepends_on "libpq"in the formula — works for both new installs and upgrades, not just setup. - Published GitHub releases: Releases are now published automatically (not draft).
- Draft GitHub releases: Merging to main now creates a draft GitHub release with changelog notes and upgrade instructions. Review and publish when ready.
- Dashboard DB connectivity: Dashboard now logs clear errors when it can't reach Postgres (missing psql, wrong port binding, unreachable host) instead of silently showing empty data.
- Setup installs libpq:
psqlclient installed automatically for dashboard DB queries. Previously only worked on same-machine setups via docker exec. - Port instructions: Setup now suggests open port binding (
5432:5432) instead of localhost-only (127.0.0.1:5432:5432), with a note to restrict if same-machine.
- Fully automated releases: Merging to main now auto-tags and updates Homebrew formula — no manual steps.
- Homebrew formula fix: Move ML pip install to
post_installphase — fixes dylib fixup errors on all Rust-compiled Python extensions (pydantic_core, tokenizers, etc.), not just orjson. - CI workflow: Formula template now generates
post_installcorrectly so future releases don't regress. - Docs: Clarify NAS+Mac path mapping with two concrete options — match Mac paths in Docker, or use macOS synthetic links to match Docker paths on Mac (zero Docker changes).
- jellyfin-ffmpeg auto-detect: No longer hardcodes a specific version URL that 404s when upstream updates. Now parses the repo directory listing for the latest build.
- Homebrew formula: Move ML venv pip install to
post_installphase to avoid Homebrew's dylib fixup errors on Rust-compiled Python extensions. - Cleanup: Remove stale local formula copy (real formula lives in tap repo, auto-updated by CI).
- Homebrew install fix: Drop
orjsondependency — binary wheel broke Homebrew's dylib fixup. Not imported by our code; FastAPI uses stdlib json as fallback. Fixes #7. - Synthetic link migration: Migrate legacy
/etc/synthetic.confentry to/etc/synthetic.d/immich-accelerator. Uninstall now cleans both locations.
- Plugin WASM paths: Fix Immich 2.7+ crash in split-worker setups. Uses macOS synthetic firmlink (
/build→~/.immich-accelerator/build-data) so both Docker and native workers resolve the same plugin paths. No database modifications. Setup prompts for sudo once;uninstallcleans it up. - OCI extraction: Fix build data landing in wrong directory (
build/instead ofbuild-data/). Tar member paths are now rewritten during extraction. - Missing corePlugin: OCI download no longer skips small image layers, so the corePlugin WASM (in its own Docker COPY layer) is always extracted.
- Yanked — used direct DB manipulation for plugin path fix. Replaced by v1.3.3 firmlink approach.
- Homebrew install:
brew tap epheterson/immich-accelerator && brew install immich-accelerator. Installs deps, createsimmich-acceleratorcommand in PATH, supportsbrew services startfor auto-launch. - Formula at epheterson/homebrew-immich-accelerator.
python3 -m immich_accelerator(waspython3 -m accelerator). Clearer what it is.
git clone --recursive && cd && python3 -m immich_accelerator setupdoes everything.- Auto-installs: Homebrew, Node.js, libvips, Python 3.11, ML venv + dependencies, jellyfin-ffmpeg.
- Extracts server from Docker, rebuilds Sharp for macOS.
- Shows docker-compose changes, opens editor, retries connection until Docker is configured.
- Auto-starts worker + ML service after setup.
- Offers to install launchd services (worker + dashboard) for auto-start on login.
- Quick start reduced from 4 manual steps to: clone, setup, done.
uninstallcommand: cleanly removes services, launchd config, accelerator data, and ML venv. Immich data and Docker untouched.
- Setup offers to install Homebrew, Node.js, and libvips if missing. Zero manual prerequisites beyond Python 3.11.
- OrbStack Docker path detected automatically.
- jellyfin-ffmpeg: Setup now downloads the same ffmpeg binary Immich uses in Docker (jellyfin-ffmpeg, macOS arm64). Includes
tonemapxnatively — HDR video thumbnails are now identical to Docker output. No more Homebrew ffmpeg patching or formula editing. - Simplified ffmpeg wrapper: With jellyfin-ffmpeg handling
tonemapxnatively, the wrapper only remaps encoders to VideoToolbox. Reduced from 120 lines to 50. - Requirements simplified: No longer need
brew install ffmpegor libwebp formula patching. Just Node.js, libvips, and Python 3.11+. - Known differences reduced: ffmpeg row in the differences table now shows "Identical" — same binary, same filters, same output.
- Chip-as-card layout: each card represents a silicon unit (CPU, GPU, Neural Engine, VideoToolbox) with its tasks inside. Neural Engine shows Face Detection and OCR with individual progress bars and counts.
- Excludes hidden assets (Live Photo motion files) from progress — matches Immich's actual processing scope.
- Shows 100% when all processable assets are complete. Unprocessable files (corrupt, stubs) tracked as "skipped" instead of dragging percentage below 100%.
- Video Transcode shows "N transcoded" instead of misleading ratio against total videos.
- PLAN.md removed from repo (local planning doc, gitignored)
- Pre-push hook enforces VERSION > remote + CHANGELOG entry
- Remote Immich (
setup --url http://nas:2283 --api-key KEY) — queries the Immich API for version, prompts for DB/Redis connection details. Docker on the Mac is optional — if available it pulls the image; if not, it guides you through extracting the server on your NAS and importing with--import-server. - Manual config (
setup --manual) — creates a config template at~/.immich-accelerator/config.jsonfor direct editing. Full control, zero auto-discovery. - Server import (
setup --import-server PATH) — import server files from a directory or tarball extracted on a remote host. No Docker needed on the Mac.
- Real-time monitoring at
http://your-mac:8420with live processing rates and ETAs - Service health indicators (worker, ML, Docker)
- Re-queue Missing button (triggers all job queues via Immich API)
- Apple Silicon hardware utilization display
- Mobile-friendly layout
- libwebp encoder validation — setup now checks for
libwebp,h264_videotoolbox, andhevc_videotoolboxencoders. Without libwebp, all video thumbnails fail silently. - tonemapx → tonemap remap — Immich's Docker ffmpeg (jellyfin-ffmpeg) has a custom
tonemapxfilter for HDR→SDR. Homebrew ffmpeg doesn't. The wrapper now remaps to upstreamtonemap+formatfilters for HDR video thumbnails. - Two-pass -preset handling —
-presetis now correctly stripped for VideoToolbox regardless of argument order. - Debug logging — ffmpeg wrapper logs rewritten commands to
~/.immich-accelerator/logs/ffmpeg-wrapper.log.
- Sharp rebuilt against system libvips (Homebrew) instead of prebuilt darwin binaries. Handles corrupt HEIF files correctly, matching Docker's behavior.
- Stale process cleanup on
start— kills orphaned immich/ML processes from previous runs. - Dashboard uses config values for DB queries instead of hardcoding container/user/db names.
cmd_startalways uses config DB password (not stale Docker detection).cmd_watchusesextract_immich_serverreturn value instead of reconstructing path.- Rate calculations use
time.monotonic()consistently (immune to NTP/DST clock adjustments). - API key preserved across
setupre-runs. - Submodule pointer fixed (clone --recursive works).
- "Known differences from Docker" section in README — comprehensive table of every deviation from stock Immich.
- Split deployment (NAS + Mac) documentation updated for new setup modes.
- Dashboard section with screenshot.
- Dashboard header shows both "Accelerator v1.1.0" and "Immich v2.6.3" badges.
- VERSION file is single source of truth, read by both CLI and dashboard.
Major rewrite. The project is now Immich Accelerator — runs Immich's own microservices worker natively on macOS instead of custom thumbnail/ffmpeg components.
- Native microservices worker — Immich's own code extracted from Docker, run bare metal on macOS with full hardware access
- VideoToolbox video transcoding — ffmpeg wrapper remaps software encoders to hardware (h264_videotoolbox, hevc_videotoolbox). No Immich patches needed.
- Accelerator CLI —
setup,start,stop,status,watch,update,logs - Container extract approach — server copied from Docker image, always version-matched. Sharp native binary + libvips for HEIF swapped for macOS. No source builds.
- IMMICH_MEDIA_LOCATION — Immich's official env var for path mapping. Zero sudo.
- Auto-update — detects Immich version changes on start and re-extracts
- Watchdog —
watchcommand monitors and auto-restarts crashed services - Metal concurrency — shared gpu_lock serializes MLX CLIP (thread-safety bug in MLX), Vision framework runs lock-free on ANE
- HEIF/HEIC support — Sharp libvips darwin binary includes full format support
| Silicon | Used for | Rate |
|---|---|---|
| Metal GPU | CLIP embeddings (MLX) | ~700/min |
| Neural Engine | Face detection + OCR (Vision) | ~200 + 700/min |
| VideoToolbox | Video encode/decode | Hardware-accelerated |
| CPU (NEON SIMD) | Thumbnails (Sharp/libvips) | ~400/min |
| CPU / CoreML | Face embedding (ONNX) | Lock-free, parallel |
- Custom thumbnail worker (replaced by Immich's own Sharp running natively)
- FFmpeg proxy and wrapper scripts (replaced by lightweight ffmpeg wrapper)
- Direct database writes for thumbnails (native worker uses Immich's own job pipeline)
- ML service (immich-ml-metal) — native ML via MACHINE_LEARNING_URL
- Concurrent inference: CLIP, face detection, OCR run simultaneously across GPU/ANE/CPU
- Batched face embeddings: single ONNX call for N faces
- In-memory CLIP: eliminated temp file I/O
- Metal concurrency fix: shared gpu_lock prevents MLX + Vision crashes
- 29 tests
Initial release and iterations. Custom thumbnail worker, ffmpeg proxy, ML service. See git history for details.