It works on my Mac: shipping ML in a frozen app without silent failures
The scariest bugs aren't crashes — those announce themselves. The scary ones are features that quietly do nothing. Packaging a Python ML stack into a double-clickable Mac app produced a whole family of them. Here's the bug class, and the test that put an end to it.
In development, MediaFind runs from source with a full Python environment — every library importable, everything works. Users don't get that. They get a frozen bundle: a PyInstaller-built .app with a snapshot of exactly the dependencies the packager decided to include. The gap between “my dev machine” and “the bundle” is a narrow, treacherous place — and it's where features went to die without a sound.
The bug class: lazy imports meet tree-shaking
PyInstaller bundles what it can see. It traces imports statically, and to keep the app from ballooning it leaves out what it thinks is unused. That collides with two completely reasonable habits:
- Lazy imports. Heavy ML libraries are often imported inside a function, on first use, to keep startup fast. The packager doesn't always see those.
- Graceful fallbacks. Good code wraps an optional dependency in
try/exceptand degrades if it's missing.
Put them together in a frozen app and you get the trap: the heavy dep gets tree-shaken out, the lazy import fails at runtime, the try/except catches it, and the feature falls back to a stub that returns empty — no crash, no error dialog, just a feature that mysteriously produces nothing.
The casualties
This one pattern took down four flagship features, each looking like an unrelated product bug:
| Feature | Dropped dependency | Silent symptom |
|---|---|---|
| Semantic search | sentence-transformers / sklearn | Hashing fallback → wrong vector size → “No matches” for everything |
| Face recognition | insightface | Stub backend → 0 faces detected |
| Speaker diarization | resemblyzer | Weak fallback embedder → everyone collapses into one speaker |
| Web downloader | yt-dlp | “Couldn't find any videos” on sites that clearly have them |
Every one of them worked perfectly in development. Every one of them was dead in the shipped app. And because the failure was silent, each got triaged as “the model is bad” or “the feature is broken” before anyone realized the real cause lived in the packaging spec.
The immediate fix: bundle what's actually needed
The direct remedy is to tell the packager about the deps it can't see — collect_all() the fragile libraries in the PyInstaller spec so their modules and their data files come along. Alongside that, a few related packaging decisions matter:
- Ship the ML libraries, not the weights. Whisper, CLIP and friends run from bundled libraries, but their model weights download once on first use — the one deliberate, opt-in network step. What can't be missing is the library code that loads them; that's what
collect_all()guarantees. - Ship a static ffmpeg. Media decode can't assume Homebrew is installed, so a static binary rides along.
- onedir, not onefile; a loopback-only server bind; bundled DB migrations. The unglamorous glue that makes a double-click “just work.”
The durable fix: a test that fails when a dep is dropped
Fixing four spec entries is easy. Making sure a fifth never silently breaks the same way — that's the real win. So there's a unit test that parses the PyInstaller specs themselves: it walks the spec files' syntax tree and asserts that every known-fragile dependency (the embeddings stack, insightface, resemblyzer, the ANN library, yt-dlp, and the rest) is still collected.
The lesson, beyond MediaFind
Two principles fall out of this. First: silent fallbacks are a liability — if a real backend is missing, say so loudly rather than quietly degrading, because a noisy failure gets fixed and a silent one ships. Second: guard the boundary you can't see across. The frozen app is a different world from your dev shell, and the only way to trust it is to test the thing that builds it. Once we did, “works on my machine” stopped being a punchline.
Download the build that actually ships
Every feature bundled, verified in CI, and notarized for macOS.
Download for macOS