- Python 46.8%
- JavaScript 44.2%
- HTML 5.3%
- Shell 2.8%
- Just 0.5%
- Other 0.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| app | ||
| article-listener | ||
| charts/article-listener | ||
| docs/protocol | ||
| scripts | ||
| tests | ||
| .dockerignore | ||
| .gitignore | ||
| article-listener-v0.1.zip | ||
| Dockerfile | ||
| justfile | ||
| pyproject.toml | ||
| README.md | ||
| uv.lock | ||
Article Listener
A small self-hosted article-to-speech PWA. Share a link from Android Chrome, extract the article, and play Kokoro 82M narration synthesized by the shared CPU-only Kokoro service. Built with FastAPI, SQLite, a persistent speech-job worker, Trafilatura/Readability, static HTML/CSS/JS, and Helm. No Node build step or SaaS account required.
Features in v0.1
- Installable Android PWA (Chrome → Install app) with Share → Article Listener integration. Also accepts pasted URLs.
- Public article catalogue: anyone can browse and listen to ready narration. A contributor password is required to submit links, generate missing speech, change voices, or remove an article.
- Article cards use Open Graph titles, descriptions, site names, and images; missing images use the
shots.waylonwalker.comscreenshot service. - Retains meaningful article HTML (headings, bold/strong, highlights, sub/sup, blockquotes, code, admonitions). Converts it into narration script with
(((emphasis))),[stern],[contemplative], etc. Not every web page preserves markup; extraction falls back to Readability and Trafilatura. - Kokoro voices with saved per-article voice selection and regeneration.
- One synthesized paragraph/section at a time, caches WAV segments, begins playback when the first is ready while generating the rest. Play, pause, skip, speed, click a paragraph to start there or click a word to seek to it, live word highlighting, position resume and Android Media Session support.
- Shared SQLite catalogue and persistent audio cache. Reader access is public; article-management actions are protected by the app password.
just publishbuilds and pushes an image to the homelab's private registry. Argo CD deploys the Helm chart fromhomelab-argo.
Publish and deploy
The production image is stored in the homelab's private registry at
registry.wayl.one/article-listener. Authenticate your local container tool
with an account that can push:
podman login registry.wayl.one
just publish
just publish tags the image with the current Git commit and UTC build time,
then pushes it. It prints the full image tag. Set that tag in
~/git/homelab-argo/k8s/article-listener/helm-chart/values.yaml, commit and
push the Argo repo, and let the 1-apps parent Application reconcile the
article-listener child Application. Argo manages the namespace, workload,
PVC, service, and Traefik ingress.
The chart and child Application live in homelab-argo so Argo can read them
from its configured repository. Keep the chart there in sync with this
repository's charts/article-listener chart when changing deployment
templates. The cluster's registry credentials are installed as a SealedSecret
in the app namespace; do not commit an unsealed Secret or registry password.
The ingress serves both https://listen.waylonwalker.com and
https://listen.wayl.one. Keep ingress.auth.enabled: true: the public
Ingress strips client-supplied X-Hlab-* headers, while a higher-priority
Traefik IngressRoute applies ForwardAuth to sign-in, contributor-session,
and write endpoints. Guests can browse articles and listen to ready audio;
submissions, narration controls, and contributor progress require the
article-listener:access Hlab-auth scope. The database is shared, so submitted
articles and their text/audio are public to anyone who can reach either
hostname. GET /healthz remains available to Kubernetes probes. ExternalDNS
creates both DNS records from the Ingress.
Production speech requests use the internal kokoro Kubernetes service at
http://kokoro.kokoro.svc.cluster.local. Listen sends authenticated synthesis
requests and stores the returned WAV and word timings in its own persistent
audio cache. The model runs only in the shared service, so Listen does not need
the model's memory or CPU allocation.
just deploy remains available for a standalone imperative install, but do
not use it for the Argo-managed article-listener release.
After deployment
- Open either hostname and browse or listen without signing in.
- Sign in with the contributor password before submitting a URL; the submission field appears in the top navigation.
- In Android Chrome, use menu → Install app. Then share a blog URL → Article Listener. If signed out, the shared URL waits until you sign in.
- Press Play on an article with ready audio. Contributors can generate missing audio. Check
just logsfor speech-service failures.
If no share target appears, uninstall and reinstall with Chrome after HTTPS and the manifest are accessible. Share-target support varies between browsers. Android background playback is best-effort: it uses Media Session and a single reused audio element, but long screen-off transitions need testing on a real phone.
Local development (no big model download)
just dev # http://127.0.0.1:8008, Kokoro speech on CPU
just test # uv dev dependencies; includes API integration tests
To run the actual model locally on Linux:
uv sync --extra dev --extra tts
DATA_DIR=.data COOKIE_SECURE=0 uv run --extra tts uvicorn app.main:app --reload
.data is gitignored. Local development uses Kokoro 82M on CPU unless TTS_SERVICE_URL and TTS_API_TOKEN are set to use the shared service. Production uses the shared service and does not install the model dependencies. Local development can use the app password by setting APP_PASSWORD and COOKIE_SECURE=0; production reads APP_PASSWORD from the article-listener-auth Kubernetes Secret. Keep global ingress ForwardAuth disabled while public guest access is enabled.
Speech & semantics
Typical conversions:
| Article markup | Spoken script |
|---|---|
<strong>very important</strong> |
(((very important))) |
<mark>never</mark> |
(((never))) |
<aside class="warning">...</aside> |
[stern] Warning. ... |
<div class="admonition tip">...</div> |
[contemplative] Note. ... |
<h2>Results</h2> |
[contemplative] Results. |
x<sup>2</sup> |
x squared |
<blockquote>...</blockquote> |
[tender] ... |
Kokoro word timings are saved with each generated audio segment. The reader highlights the current word and lets you click a word to seek within its paragraph. Timing is model-predicted, so occasional small offsets are expected. Some pages lose formatting during extraction; HTML content is sanitized before display.
Security, licensing, operations
- Production uses homelab-auth through Traefik ForwardAuth. Traefik strips client-supplied
X-Hlab-*headers and injects the authenticated identity and scopes; a NetworkPolicy limits app traffic to Traefik. The app denies protected requests without a valid user ID andarticle-listener:access. Local password login is retained only during the staged rollout and for development. - SSRF mitigation checks URL schemes, DNS resolution, redirects, private and loopback IP addresses, ports and download size. DNS rebinding between validation and connection is not fully prevented. Do not expose an unauthenticated endpoint; deploy a network egress policy if hostile users may gain access.
- Only a single app replica should use this SQLite database/worker. Longhorn
ReadWriteOncePVC; backup the volume. SQLite WAL files and model cache live under/data. - WAV segments use much more space than Opus/AAC; version one favors straightforward browser playback and fidelity. Plan an audio compression/garbage-collection pass.
- The Kokoro model downloads on first use and uses CPU and memory during synthesis. See the Kokoro project for model details and licensing.
Architecture
Android Chrome / Brave ──share URL──> installed PWA /share (POST)
│
▼
FastAPI + homelab-auth
│
resolve, fetch, extract and sanitize
│
semantic HTML blocks + speech markup
│
SQLite article library
│
single CPU TTS worker
│
cached WAV files on PVC
│
browser Audio player
Development priorities
- Real-device benchmark on Pixel 6 and falcon3: cold model loading, first audio time, real-time factor and screen-off transitions.
- Stronger article extraction fallback for JS-rendered pages and custom Markata admonitions.
- Richer superscript/subscript/math rendering and per-admonition voice overrides.
- Opus output, cached audio GC, more robust TTS-job queue and ArgoCD GitOps integration.