πŸ¦€

The include_str!() Pattern β€” Embedding Python ML in a Rust Binary

Nightingale's pragmatic compromise β€” single binary distribution, persistent process communication, zero version mismatch

A pattern discovered in Nightingale (Rust karaoke app). A practical architecture for handling Python ML stacks from Rust.

The Problem: Running Python ML from Rust

Nightingale needs UVR/Demucs (vocal separation) and WhisperX (speech recognition). Both are PyTorch-based and only work properly in Python.

Typical options and their problems:

Depend on system Python β€” version conflicts, package collisions, "works on my machine" syndrome.

Ship .py files separately β€” file loss, path issues, users accidentally deleting scripts.

PyO3 Rust↔Python bindings β€” fine for simple functions, but binding heavy ML stacks (PyTorch + WhisperX + audio-separator) is dependency hell.

Rewrite everything in Rust β€” impractical. ML ecosystem (PyTorch, Hugging Face, ONNX Runtime) is Python-centric.

Nightingale's Solution: include_str!() + Persistent Process

const STEMS_PY: &str = include_str!("../analyzer/stems.py");
const SERVER_PY: &str = include_str!("../analyzer/server.py");
// ... 10 Python files

Rust's include_str!() macro includes file contents as string constants at compile time. Not runtime file reading.

How It Works

Build time: cargo build embeds 10 .py files as strings β†’ single binary output.

First launch (bootstrap): Create ~/.nightingale/vendor/ β†’ extract .py strings to files β†’ install Python 3.10 via uv β†’ create venv β†’ install PyTorch, WhisperX β†’ write .ready marker.

Analysis: Rust spawns server.py as child process β†’ Python loads WhisperX model β†’ waits β†’ Rust sends JSON via stdin β†’ Python responds via stdout β†’ server stays alive for next song.

5 Benefits

1. Single binary distribution β€” one file to ship.

2. Zero version mismatch β€” Rust and Python code from same commit.

3. Tamper-proof β€” users can't accidentally modify embedded scripts.

4. Persistent Python server β€” ML models stay loaded between songs. CUDA OOM? Kill and respawn without crashing the app.

5. Simple JSON communication β€” no FFI complexity, easy debugging.

When This Pattern Fits

  • Python-dominant domains (ML inference)

  • Low call frequency (per-song, not per-millisecond)

  • Negligible IPC overhead

  • Desktop apps where deployment simplicity matters

When It Doesn't Fit

  • Thousands of calls per second β†’ use PyO3 or ONNX Runtime Rust bindings

  • Hundreds of Python files β†’ binary bloat

  • Bidirectional streaming β†’ use gRPC or sockets

Core Insight

"Let Python do what Python does best (ML), but let Rust control deployment and orchestration." Instead of cramming both languages into one process via FFI, separate processes but bundle deployment. Low coupling, low deployment complexity.

Key Concepts

1

Embed 10 Python scripts into Rust binary at compile time via include_str!()

2

Extract as .py files to ~/.nightingale/vendor/analyzer/ on first run

3

Auto-install Python 3.10 + PyTorch + ML packages via uv (bootstrap)

4

Spawn server.py as persistent child process β†’ WhisperX model stays loaded

5

Communicate analysis requests, progress, completion via stdin/stdout JSON protocol

6

On CUDA OOM: kill server β†’ clean GPU β†’ respawn (app stays alive)

Use Cases

ML inference desktop apps β€” need Python ML but must ship as single binary Lightweight Rust ↔ Python communication β€” when stdin/stdout JSON suffices over PyO3 Self-contained app bootstrap β€” uv + auto-install so users never need to install Python