Voice memos that file themselves
I think out loud. My phone and watch dutifully record it, and the recordings then sit in a library I never open. This is the background job that turns each memo into a searchable note in my Obsidian vault, with the audio attached and no manual step.
The problem
Voice Memos is great at capturing and bad at finding. A recording that says “remember to ask about that” is useless if the only way to search it is to play it back. I wanted every memo transcribed, filed in my notes and searchable, on my own hardware, for free, with no step I have to remember.
How it works
- Sync. Voice Memos on the phone and watch syncs through iCloud to a Mac that stays on.
- Trigger. A launchd agent wakes when the recordings folder changes, and every five minutes as a backstop.
- Copy, never move. For each recording it hasn't seen, it makes sure the audio is fully downloaded, copies it, and transcribes it locally.
- Route. The first spoken word picks the destination. Say a keyword first and the memo goes where that keyword says; the keyword is stripped from the note.
- File. It writes a Markdown note with frontmatter, the transcript and the embedded audio, then records the recording in a ledger so it is never imported twice.
---
created: 2026-01-01T09:30:00-0800
source: voice-memo
audio: 2026-01-01-093000-example-title.m4a
route: personal
original: 20260101 093000-ABC123.m4a
---
Transcript text goes here.
---
![[2026-01-01-093000-example-title.m4a]]
The agent definition is short. These are the lines that matter:
<key>ProgramArguments</key>
<array><string>/bin/bash</string><string>/path/to/router.sh</string></array>
<key>WatchPaths</key>
<array><string>~/Library/Group Containers/group.com.apple.VoiceMemos.shared/Recordings</string></array>
<key>StartInterval</key><integer>300</integer>
<key>ThrottleInterval</key><integer>10</integer>
Transcription
The first version, in June, used mlx-whisper, which runs well on Apple silicon and costs nothing. The version running now hands that step to a macOS Shortcut whose transcription runs on Apple Foundation Models (AFM), on the Mac itself (there is a separate write-up on AFM), so the script only has to call it:
shortcuts run "Transcribe Memo" --input-path "$m4a" --output-path "$out" --output-type public.plain-text
An empty transcript means the recording wasn't speech, so it is skipped for good. A failed run is simply retried five minutes later.
The hard part wasn't the transcription
Most of the work went into making a background job that touches iCloud files behave. Each of these was a real failure that got a fix:
- The file might not really be there. A synced recording can be a placeholder that hasn't downloaded. The router asks iCloud for it and waits until the size stops changing before touching it.
wait_for_download() {
local f="$1" prev=-1 now i
brctl download "$f" 2>/dev/null # ask iCloud for the real file
for i in $(seq 15); do
now=$(stat -f%z "$f" 2>/dev/null || echo -1)
[ "$now" = "$prev" ] && [ "$now" -gt 0 ] && return 0 # size stopped changing
prev=$now; sleep 2
done
return 1 # try again on the next run
}
- An empty ledger looks like “nothing imported yet.” The ledger lives in iCloud, and an evicted or truncated copy once made the job want to re-import the whole back catalogue. Three fixes: refuse to run unless the ledger is downloaded and writable; derive “already imported” from the notes themselves, since each one records its source recording in frontmatter; and if the ledger is ever empty, reseed it from the notes and the current library instead of importing everything.
- Two Macs sharing one library can double-import. A lock only protects one machine. A shared ranking file plus heartbeat files in iCloud means only the highest-ranked awake Mac does the work, and the next one takes over if it falls asleep. A last guard skips any note whose title matches one written in the previous 15 minutes.
- Single instance. macOS has no
flock, so the lock is an atomicmkdir, and a lock older than ten minutes is treated as stale.
# macOS has no flock: mkdir is atomic, so use it as the lock
if [ -d "$LOCK" ] && [ -n "$(find "$LOCK" -prune -mmin +10)" ]; then
rmdir "$LOCK" # stale lock: steal it
fi
mkdir "$LOCK" 2>/dev/null || exit 0 # someone else is running
trap 'rmdir "$LOCK"' EXIT
- Full Disk Access. Without it the agent runs happily and reads zero files. Grant it to
/bin/bash. If nothing shows up, check this first. - New audio format. Recent iOS versions record spatial audio as
.qta. It is converted withafconvertso ordinary players can open it.
Result
Recordings from the phone and watch turn into searchable notes a few minutes later, with no manual step. It depends on iCloud sync timing, needs a Mac that stays awake, and the transcript is only as good as the recording. I'm not publishing the whole script, since much of it is housekeeping specific to my machines, but the pieces above are the parts worth copying.
This is entry 07 of AI Lab Work.