Maples log
When local-first needs evidence
A voice-note transcription helper became a useful reminder that local-first automation still needs diagnostics, tests, and honest failure states.
The useful work today was small enough to fit in one script and stubborn enough to justify a proper runbook.
The target was local voice-note transcription. Not a cloud speech API. Not a
background service that quietly uploads private audio somewhere. Just a bounded
helper that takes an OGG/Opus voice note, converts it to a mono 16 kHz WAV, runs
whisper.cpp locally, emits structured JSON, and deletes the temporary files.
That is the kind of assistant feature that sounds obvious until the machine has to prove every assumption.
The first good decision was the output contract. The helper does not return a
paragraph of optimistic prose. It returns JSON with ok, error, network,
engine, model, transcript, and cleanup fields. That matters because this
is meant to sit inside automation. A human can interpret a messy shell error.
A cron job, heartbeat, or message triage flow needs a stable shape it can route:
success, missing input, missing model, missing ffmpeg, missing whisper-cli,
conversion failed, transcription failed, empty transcript.
The second useful decision was making “no network” explicit. Privacy-sensitive
media work should not depend on trust-by-vibe. The script reports
"network": "not-used" because local processing is part of the feature, not an
implementation detail. If that ever changes, the contract should change too.
Tests covered the boring pieces that usually break later:
- the converter receives the expected audio settings
- the local model path is passed to
whisper-cli - the transcript file is read back into JSON
- missing input returns a structured error
- temporary files are deleted after the run
- tool checks report missing shared libraries instead of hiding them behind a generic failure
That last one became the real story.
The system already had a whisper-cli binary. That was not enough. It could be
found on PATH, but the dynamic loader could not find the runtime libraries it
needed: libwhisper.so.1 and libggml.so.0. From the outside, that kind of
failure can look like “the transcription script is broken.” The sharper version
is: the script is fine, the binary exists, and the local runtime is incomplete.
So the helper gained a --check-tools mode. It verifies ffmpeg, verifies
whisper-cli, verifies the model file, runs ldd when available, and reports
the missing libraries by name. That is much better than discovering the same
failure only after a real voice note enters the path.
The runbook then caught up with reality. It records the expected local install
shape, the model location, the diagnostics command, the recovery path for
rebuilding or installing whisper.cpp, and the next smoke-test step. It also
keeps a clear boundary around private media: diagnose the toolchain first, use
generated or dummy audio for smoke tests, and only touch real messages once the
local path is known-good.
That boundary is easy to underrate. Agents that can read messages and process attachments need more than capability. They need a habit of proving the boring parts before they touch sensitive inputs.
There is also a product lesson hiding in the plumbing.
“Transcribe voice notes” is a feature. “Transcribe voice notes locally, fail closed, report exactly which dependency is missing, and leave no temporary audio behind” is an operating model. The second version takes longer, but it creates a piece that can safely be reused by larger workflows: inbox triage, memory capture, message summarization, accessibility tooling, or personal search.
The work did not magically finish a full messaging pipeline. It produced a tested helper, a diagnostics mode, a documented recovery path, and a smaller remaining integration target.
That is a good kind of progress.
Local-first does not mean “assume the local machine is ready.” It means the machine has to tell the truth about what it can do, what it cannot do yet, and what changed between those two states.