Voice-note capture as the primary mobile input channel. AudioPen polishes on their cloud, POSTs to a self-hosted receiver on the desktop (exposed via Tailscale Funnel so no router ports open and nothing behind Authentik). The receiver writes to inbox/raw/; a bash normaliser wraps each drop in frontmatter and moves it to inbox/; /triage-inbox routes them from there. scripts/audiopen-ingest.sh — 80-line bash normaliser. Idempotent; safe to re-run. Extracts title, slugifies, computes capture timestamp from mtime, rewrites with fleeting-note frontmatter. scripts/audiopen-webhook/main.py — stdlib-only Python HTTP server. Zero pip deps; binds to 127.0.0.1 by default. Accepts flexible payload shapes (title/name + body/output/summary/polished/ orig_transcript) so AudioPen version drift is logged rather than silently dropped. scripts/audiopen-webhook/*.example — systemd user-unit templates for the receiver and the path/service pair that fires the ingest wrapper on inbox/raw/ changes. scripts/pyproject.toml + README.md — Python venv convention. Zero deps today; venv location reserved at scripts/.venv/ (gitignored), manifest at scripts/pyproject.toml, bootstrap documented for both plain pip and uv. Optional-dependencies groups let individual tools pull what they need without bloating the whole env. docs/setup/audiopen.md — full setup guide: generating the shared secret, installing the systemd units, pairing with Tailscale Funnel, configuring AudioPen's webhook, and the IMAP-fallback path for setups that can't run a public-reachable receiver. docs/setup/sync.md — companion guide: Gitea SSH remote (works even with Authentik gating HTTPS), obsidian-git plugin configuration, Syncthing desktop↔phone pairing with the critical .stignore patterns, and the three phone-role tiers so the user can pick Tier 1 / 2 / 3 at their own pace. .gitignore gains **/.venv/, **/venv/, **/__pycache__/, and *.pyc so nobody accidentally commits a materialised environment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3.0 KiB
audiopen-webhook
Zero-dependency Python HTTP server that receives AudioPen webhook POSTs and
writes bare markdown files to inbox/raw/. The separate scripts/audiopen-ingest.sh
wrapper normalises those files into proper fleeting notes under inbox/.
Why this shape
- Receiver is minimal (stdlib-only http.server). No Flask, no runtime to keep updated, ~150 LOC. Reads AudioPen's JSON payload, writes one file, logs to stderr. Anything fancier belongs in the ingest wrapper.
- Two-step raw → normalised. Keeps the receiver stupid; all schema decisions (frontmatter, slug format, filename) live in one shell script you can read in 30 seconds.
- Designed for Tailscale Funnel. Binds to 127.0.0.1 by default. Tailscale Funnel (or Cloudflare Tunnel, or an SSH remote forward, or ngrok) terminates the public endpoint and proxies to 127.0.0.1:8765. No ports opened on your router; nothing behind Authentik.
Setup
Full step-by-step lives in docs/setup/audiopen.md.
Quick version:
- Generate a secret:
head -c 24 /dev/urandom | base64 | tr -d '/+='. - Configure AudioPen to POST JSON to
https://<your-funnel>.ts.net/audiopen/<secret>. - Install the systemd user units:
mkdir -p ~/.config/audiopen-webhook ~/.config/systemd/user echo "AUDIOPEN_WEBHOOK_SECRET=<secret>" > ~/.config/audiopen-webhook/env chmod 600 ~/.config/audiopen-webhook/env cp audiopen-webhook.service.example ~/.config/systemd/user/audiopen-webhook.service # create two more systemd files for the ingest path/service per # audiopen-ingest.path.example systemctl --user daemon-reload systemctl --user enable --now audiopen-webhook.service audiopen-ingest.path - Expose via Tailscale Funnel:
tailscale funnel --bg 8765 - Test:
curl -X POST -H 'Content-Type: application/json' \ -d '{"title":"Test","body":"Hello from curl"}' \ https://<your-funnel>.ts.net/audiopen/<secret> # → OK ls ~/path/to/vault/inbox/raw # should see the test file # wait a beat for the inotify path unit to fire ls ~/path/to/vault/inbox # should see the normalised file
Payload shape
The receiver accepts any of these JSON keys for the title: title, name.
For the body: body, output, summary, polished, orig_transcript. Unknown
keys are ignored; the raw payload is logged to stderr so you can see what
AudioPen actually sent if shape drifts.
If AudioPen's webhook format changes in a way the current extractor misses,
check journalctl --user -u audiopen-webhook.service for the logged shape and
update the key list in extract_content() at the top of main.py.
Alternative: email + IMAP fetch
If running a public-reachable receiver isn't viable, the plan has an email-path
fallback documented in docs/setup/audiopen.md. It's entirely outbound: AudioPen
emails each note to a dedicated address, and a cron on the desktop pulls via
IMAP and drops files in inbox/raw/. Same ingest wrapper handles the rest.