recordings transcribes one or more audio files directly, or watches one
directory tree as a daemon. It cleans each transcript with an LLM, publishes a
.txt file, and optionally archives audio.
It replaces a 1225-line shell transaction system and a 591-line Python
transcriber with one ordering argument: the audio file is the only source
of truth, so the audio is never moved out of the watch tree until its
cleaned transcript has been durably published (write to temp, fsync, atomic
rename). A crash at any point leaves either the audio still in place — the
next sweep redoes the work, reusing the spooled raw transcript when the file
is unchanged — or the work complete. The rare crash exactly between
publishing the transcript and archiving the audio yields a visible duplicate
(name-2.txt) on the next sweep, never a loss.
# Process files once; transcript goes in current directory and audio stays put.
recordings memo.m4a interview.wav \
--prompt ~/doc/post-process.md \
--asr-url https://hera.lan:8443/v1 \
--asr-model cohere-transcribe-03-2026-mlx-fp16 \
--llm-url http://localhost:8000/v1 \
--llm-model GLM-5.3-Flash-oQ4e
# One directory remains daemon mode; archive is optional.
recordings ~/Recordings \
--output ~/Documents/Inbox \
--archive ~/Documents/Recordings \
--prompt ~/doc/post-process.md \
--asr-url https://hera.lan:8443/v1 \
--asr-model cohere-transcribe-03-2026-mlx-fp16 \
--llm-url http://localhost:8000/v1 \
--llm-model GLM-5.3-Flash-oQ4e- Supplying audio file paths processes that exact set then exits. Supplying one
directory preserves watch mode.
--onceruns one directory sweep. --outputdefaults to current directory.--archiveis optional; without it, successfully processed audio remains in place.- Native filesystem events (FSEvents on macOS, inotify on Linux) start a
sweep the moment the watch tree changes, with an interval poll (default
30 s,
--poll) as fallback; a file is processed only once its size and mtime are stable across two scans, so half-synced uploads are left alone. - Audio is first converted to 16 kHz mono WAV (the model's native input) with
macOS-native
afconvert. Transcription uses an OpenAI-compatible/audio/transcriptionsendpoint (--asr-url/--asr-model) or a local command whose stdout is transcript and receives WAV path last:--asr-command "mlx-speech asr --model <model-dir> --language en --audio". - Cleanup posts raw transcript with prompt file contents as system message to
/chat/completions(--llm-url,--llm-model). SSL_CERT_FILE, when set, is CA bundle used to validate TLS endpoints.--api-key(defaultdummy-key) is sent as Bearer token.- A recording that keeps failing is retried on later sweeps and abandoned after 5 attempts until file changes or daemon restarts; audio stays put on failure.
- Empty subdirectories of watch tree (including
.DS_Store-only ones) are pruned after each sweep, but only once untouched for 24 hours. The watch tree is typically a sync folder, where a directory that reads as empty locally may still be the destination of an upload in flight from another device. State lives in~/.local/state/recordings.
The daemon subsumes the old fswatch pipeline; the service reduces to a
single exec (here with local mlx-speech transcription; swap the
--asr-command line for --asr-url/--asr-model, and export
SSL_CERT_FILE, once the server endpoint transcribes this model
correctly):
flatten-recordings = {
script = ''
# mlx-speech comes from the per-user profile; afconvert from macOS.
export PATH="/etc/profiles/per-user/johnw/bin:$PATH"
exec ${pkgs.recordings}/bin/recordings "${home}/Recordings" \
--output "${home}/Recordings" \
--archive "${home}/Documents/Recordings" \
--prompt "${home}/doc/post-process.md" \
--asr-command "mlx-speech asr --model ${home}/src/mlx-speech/models/cohere/cohere_transcribe/mlx-int8 --language en --audio" \
--llm-url http://localhost:8000/v1 \
--llm-model GLM-5.3-Flash-oQ4e
'';
serviceConfig = {
RunAtLoad = true;
KeepAlive = true; # long-running daemon; relaunch if it ever exits
LowPriorityIO = false;
LowPriorityBackgroundIO = false;
ProcessType = "Standard";
StandardOutPath = "${home}/Library/Logs/flatten-recordings.log";
StandardErrorPath = "${home}/Library/Logs/flatten-recordings.log";
};
};nix build # binary and test suite
nix develop # cargo, rustc, rustfmt, clippy
cargo test # inside the dev shell