Platform

Native on every platform: how MediaFind ships for Mac, Windows, and Linux

MediaFind started as a Mac-only app. Getting to native desktop installers on all three platforms required untangling three separate bugs — one per OS — and rethinking what "native" means for a Python ML app wrapped in a Tauri shell.

For a long time, the download page was simple: one button, one .dmg, one platform. That was a deliberate choice — Mac-first let us move fast, focus on the native app experience, and ship features without worrying about three operating systems at once. But "Mac-only" left a lot of people out. Video editors, researchers, and archivists on Windows and Linux were running MediaFind in a browser pointed at a terminal process, which is fine for power users and not fine for anyone else.

The goal was a real installer on every platform. Not a Docker container, not a Python package you pip-install — a double-click installer that puts a native app in the right place, launches it from the dock or start menu, and feels like software. Here is how we got there.

The architecture: engine + shell

MediaFind is two things stitched together: a Python ML engine that indexes your media, runs models, and serves a JSON API; and a native desktop shell that wraps the web UI in a proper window with menus, a dock icon, and OS integration. These two layers have different distribution needs, which is why they are packaged separately.

Tauri shell native window · menus dock icon · OS integration spawns PyInstaller sidecar mediafind serve · hidden process ML engine · SQLite · API serves Web UI localhost · Tauri webview same on all platforms
The Tauri shell spawns the PyInstaller binary as a hidden sidecar and points a webview at its localhost API. The user sees one native app; the two processes talk via HTTP on loopback.

The engine is PyInstaller-frozen: a self-contained binary that ships every Python dependency, every ML library data file, and a bundled static ffmpeg — no Python installation required on the user's machine. The shell is a Tauri app: a small Rust binary that manages the window, menus, and OS chrome. On first launch, the shell starts the sidecar and opens the webview. From the user's perspective, it is one application.

This split has a useful property: the Python engine is platform-agnostic by design. If you can freeze a Python binary for a given OS, you have an engine that works there. The challenge was that for the first eighteen months, we only ever tried freezing it for macOS.

The first tagged build that worked on all three

The CI workflow triggers on version tags. When we pushed v0.1.5, it built the PyInstaller server binary on a macOS, Ubuntu, and Windows matrix, then published the Ubuntu and Windows artifacts to a GitHub release. That sounds simple. The reality was that every tagged build from v0.1.0 through v0.1.4 had failed on all three platforms. There were three bugs, each hidden behind the last.

Bug 1: the typing backport (all platforms)

resemblyzer — the library that computes speaker embeddings for voice diarization — has a transitive dependency on the obsolete typing package from PyPI. This package predates Python 3.5, exists only for compatibility with ancient code, and in modern Python it shadows the standard library's typing module. PyInstaller sees the import, finds the backport, and then fails to build because the real typing module is gone.

The fix is to uninstall the backport before freezing. Our build script now does exactly that: pip uninstall -y typing before invoking PyInstaller. It re-installs itself during pip install of the next package that depends on it, so the step has to run late in the build — right before the freeze — but it reliably clears the conflict.

Bug 2: Linux disk overflow

PyInstaller's --onefile mode assembles everything into a single executable. On Linux, the default PyPI torch wheel includes CUDA: roughly 2.5 GB of GPU libraries that MediaFind does not use. Trying to objcopy a 2.5 GB blob into a single binary on a GitHub Actions runner — which has around 14 GB of free space shared across the entire build job — hits the disk wall.

The fix is to install the CPU-only torch wheel before anything else:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu

That drops the torch footprint from ~2.5 GB to ~250 MB. The macOS and Windows PyPI wheels are already CPU-only, so this step only matters on Linux — but on Linux it is the difference between a build that finishes and one that dies with No space left on device partway through linking.

Bug 3: Windows help output crash

MediaFind's CLI has a --help flag. The help text contains a glyph to label a path step. On Windows, the default console code page is cp1252, which does not include that character. Python's attempt to print it raises a UnicodeEncodeError before the app ever starts. From a user's perspective: they double-click the installer, the app opens, and immediately crashes on the very first output.

The fix is a one-time UTF-8 reconfiguration in the entry point:

import sys
if sys.stdout and hasattr(sys.stdout, 'reconfigure'):
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')
if sys.stderr and hasattr(sys.stderr, 'reconfigure'):
    sys.stderr.reconfigure(encoding='utf-8', errors='replace')

This runs before any output is produced. The hasattr guard handles the frozen app case where stdout is None (no console attached). After this, every glyph in the app's output renders correctly regardless of the system locale.

The Tauri shell and what "native" actually means

Getting the engine to build on three platforms was a prerequisite. The second layer of the problem was making the experience feel native — not just "runs on Windows" but "feels like a Windows app." The Tauri shell is where that work lives.

The core of the shell is small: a Rust binary that reads a config for the sidecar path, spawns the engine process, waits for it to be ready (polling the /health endpoint), then opens a WebviewWindow pointing at localhost. The web UI inside the window is the same HTML, CSS, and JavaScript that works in a browser — no platform-specific UI code at all. Tauri provides the native window chrome, and the web UI fills it.

A few things make this feel genuinely native rather than just "a browser window in a box":

The installer formats

Different platforms have different conventions for what an "installer" looks like, and deviating from those conventions is a fast way to make users distrust your software.

The release pipeline

Because macOS signing is owner-gated (the certificate lives on one machine), the release process is split in two. CI handles Linux and Windows: push a version tag, the matrix build runs, and the artifacts publish to a GitHub release. macOS is built locally with a dedicated script that handles the signing, packaging, notarization, and stapling sequence, then the .dmg is manually uploaded alongside the CI artifacts.

The finished release then mirrors to a separate public repository. The primary repository is private; GitHub releases on a private repo require authentication to download, which makes the URL useless for a website download button. The public mirror holds the same assets with public read access, and the site's config.js points each platform's download button at the right asset there.

The site now shows three download buttons: macOS (signed, primary), Windows (beta), and Linux (beta). Each links to the format most appropriate for that platform. The macOS button has always been there; the Windows and Linux buttons are new. That's what v0.1.7 shipped.

What "beta" means here

The Windows and Linux installers are labeled beta for two concrete reasons, not as a hedge. First, the Windows binary is unsigned, which means SmartScreen will warn on first run. Second, the Linux build is tested on Ubuntu 22.04 — other distributions, especially those with older WebKitGTK or different glibc versions, have not been validated. Neither of these is a fundamental problem; both are addressable with a code-signing certificate (Windows) and broader distribution testing (Linux). The Mac build has neither limitation, which is why it is not labeled beta.

The engine itself — the indexing, search, ML, and everything under the hood — is identical on all three platforms. The beta label is about the installer and integration story, not the feature set.

What is next

The three-platform installer story unlocked a set of follow-on work that wasn't practical before:

The goal throughout has been the same as the Mac experience: you download an installer, run it, and MediaFind is on your computer. No Python environment, no terminal, no configuration. Everything runs locally, all models stay on your machine, and search is fast because nothing is going to a server. That experience is now available on all three platforms.

Download for your platform

Native installers for macOS, Windows, and Linux — all on-device, all private.

Download MediaFind