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>
70 lines
3.0 KiB
Markdown
70 lines
3.0 KiB
Markdown
# 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`](../../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**:
|
|
```sh
|
|
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**:
|
|
```sh
|
|
tailscale funnel --bg 8765
|
|
```
|
|
5. **Test**:
|
|
```sh
|
|
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.
|