Files
council/scripts/audiopen-webhook/README.md
T
jpmschweitzerandClaude Opus 4.7 2c8df9425b add AudioPen ingest pipeline and sync architecture setup docs
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>
2026-04-19 20:17:48 +02:00

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:

  1. Generate a secret: head -c 24 /dev/urandom | base64 | tr -d '/+='.
  2. Configure AudioPen to POST JSON to https://<your-funnel>.ts.net/audiopen/<secret>.
  3. 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
    
  4. Expose via Tailscale Funnel:
    tailscale funnel --bg 8765
    
  5. 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.