A voice-journaling PWA. Phone records, a GPU at home transcribes, an Obsidian vault is the only place the words end up.
Private, self-hosted only. It's an actual journal, so there's no public link and no demo instance. Everything below is what you'd need to run your own.
I wanted to journal by talking, one-thumb on my phone, without a third-party app holding
my diary or a database I'd have to keep in sync with my notes by hand. The frontend is an
installable iOS PWA: it records audio, shows a calendar of which days already have an
entry, and queues a failed upload for retry so a bad connection on the road doesn't lose
what I just said. The backend is FastAPI running faster-whisper on GPU, and it also
serves the built frontend as static files, so the whole thing is one origin with no CORS
to reason about.
A finished recording becomes two files: a plain-text transcript and its audio, named
NN - H.MMpm.txt and paired, dropped straight into the day's folder in the vault. There
is no database. Those files are the only source of truth, indexed into memory when the
server starts and rebuilt on demand through POST /api/reindex. I moved off an earlier
SQLite design to get here, on purpose: a database that can drift from the vault is a
second place for the truth to live, and I'd rather lose the index and rebuild it in a
second than reconcile two stores by hand.
Access control lives in the network, not the app. tailscale serve proxies the backend
to an HTTPS hostname on a private tailnet, on its own sidecar node so nothing else on the
host can hijack the URL. There's no login screen and no API key: being a device on the
tailnet is what grants access.
The GPU is a GTX 1080 Ti, and Pascal doesn't support efficient float16. Loading Whisper
in the wrong precision crashes on model load, not later, so
WHISPER_COMPUTE_TYPE=int8_float32 is pinned as a hard invariant, not a default someone
can quietly bump. A watchdog scheduled task keeps the container up unattended and a daily
job backs up the transcript index, because the point of a journal is that it's there the
next time I open my phone.
CLAUDE.md holds the guidance I give Claude Code when it works in this repo, including
the vault schema and that GPU invariant. I develop with agents heavily, and it's the
project's memory for this codebase.
You'll need a machine with an NVIDIA GPU (or CPU, with a smaller model and slower transcription) and Docker, a Tailscale account with HTTPS certificates enabled for your tailnet, and an Obsidian vault folder to bind-mount as the recordings directory.
cp backend/.env.example backend/.env
cd frontend && npm install && npm run build && cd ..
docker compose up -d
tailscale serve --bg 8000
curl http://localhost:8000/healthbackend/.env.example carries every setting that matters:
WHISPER_MODEL,WHISPER_DEVICE,WHISPER_COMPUTE_TYPE— the model size and the compute type for your GPU generation (float16on RTX 20-series and newer,int8_float32on a Pascal card like mine,int8on CPU).WHISPER_INITIAL_PROMPT,WHISPER_SUBSTITUTIONS— optional priming and a narrow find/replace list for names Whisper still mishears.VAULT_RECORDINGS_DIR— where inside the container your vault's recordings folder is mounted; point it somewhere empty and Vesper starts empty.VAULT_TZ— filenames use local wall-clock time while timestamps are handled in UTC, so the recording timezone has to be explicit.MAX_UPLOAD_MB,RATE_LIMIT_API_PER_MIN,RATE_LIMIT_TRANSCRIBE_PER_MIN— hardening knobs, sane defaults included; no API key is required anywhere.
Open the tailnet HTTPS URL in Safari on your phone and Add to Home Screen. The Windows
watchdog and backup task setup is in ops/windows/.
frontend/ Vite + React + Mantine PWA (build output -> frontend/dist)
backend/ FastAPI + faster-whisper; data lives in the Obsidian vault, not a DB
scripts/ generate_icons.py, programmatic app icons
ops/windows/ watchdog + backup scheduled tasks
docker-compose.yml
SECURITY.md threat model and hardening notes
Live on my own tailnet, used daily. This is a snapshot of a private working repo, history isn't published here.
MIT