No description
  • Python 46.8%
  • JavaScript 44.2%
  • HTML 5.3%
  • Shell 2.8%
  • Just 0.5%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-10-22 15:08:54 -05:00
app feat: add llms and agents aliases 2026-10-22 15:08:54 -05:00
article-listener Make Article Listener public with contributor access 2026-10-10 13:52:52 -05:00
charts/article-listener Improve original article previews and lock container builds 2026-10-10 18:53:13 -05:00
docs/protocol feat: publish URL mentions index 2026-10-22 13:40:15 -05:00
scripts Make Article Listener public with contributor access 2026-10-10 13:52:52 -05:00
tests feat: add llms and agents aliases 2026-10-22 15:08:54 -05:00
.dockerignore Improve original article previews and lock container builds 2026-10-10 18:53:13 -05:00
.gitignore init 2026-10-09 19:43:42 -05:00
article-listener-v0.1.zip init 2026-10-09 19:43:42 -05:00
Dockerfile Use shared Kokoro service for production speech 2026-10-11 12:22:07 -05:00
justfile Make Article Listener public with contributor access 2026-10-10 13:52:52 -05:00
pyproject.toml Improve original article previews and lock container builds 2026-10-10 18:53:13 -05:00
README.md Use shared Kokoro service for production speech 2026-10-11 12:22:07 -05:00
uv.lock Improve original article previews and lock container builds 2026-10-10 18:53:13 -05:00

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.com screenshot 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 publish builds and pushes an image to the homelab's private registry. Argo CD deploys the Helm chart from homelab-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

  1. Open either hostname and browse or listen without signing in.
  2. Sign in with the contributor password before submitting a URL; the submission field appears in the top navigation.
  3. 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.
  4. Press Play on an article with ready audio. Contributors can generate missing audio. Check just logs for 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 and article-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 ReadWriteOnce PVC; 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

  1. Real-device benchmark on Pixel 6 and falcon3: cold model loading, first audio time, real-time factor and screen-off transitions.
  2. Stronger article extraction fallback for JS-rendered pages and custom Markata admonitions.
  3. Richer superscript/subscript/math rendering and per-admonition voice overrides.
  4. Opus output, cached audio GC, more robust TTS-job queue and ArgoCD GitOps integration.