Hosted app to convert papers and articles to a personal podcast feed
  • Rust 85.1%
  • Svelte 10.6%
  • TypeScript 2.6%
  • Shell 1%
  • Dockerfile 0.5%
  • Other 0.2%
Find a file
Tej Chajed 46926ff4b4 Avoid sentence breaks on numbers, abbreviations, and acronyms
The SSML splitter was treating every `.` as a sentence boundary, so
"3.5", "Dr. Smith", and "U.S." each produced an audible mid-sentence
pause in the rendered audio. Skip the split on digit-period-digit, on
known abbreviations (Dr., Mr., etc., vs., Inc., ...), and on single-
letter words (covers U.S., e.g., a.m.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 14:12:48 -05:00
.github/workflows Fix clippy warnings; drop --depot=false from CI deploy 2026-04-23 20:49:32 -05:00
backend Avoid sentence breaks on numbers, abbreviations, and acronyms 2026-04-27 14:12:48 -05:00
frontend Switch default voice to Chirp3-HD-Puck; add per-episode voice selector 2026-04-23 20:36:09 -05:00
scripts Split Google API key into TTS and Studio keys; remove old design docs 2026-04-12 23:33:28 -07:00
.dockerignore Add dockerignore 2026-04-12 23:47:36 -07:00
.env.example Split Google API key into TTS and Studio keys; remove old design docs 2026-04-12 23:33:28 -07:00
.gitignore Gitignore .claude/ local state 2026-04-14 11:19:11 -05:00
AGENTS.md Add AGENTS.md 2026-04-15 10:39:29 -05:00
DEPLOYMENT.md Send SSML to Google TTS; switch default voice to Neural2-D 2026-04-23 20:10:46 -05:00
DESIGN.md Send SSML to Google TTS; switch default voice to Neural2-D 2026-04-23 20:10:46 -05:00
Dockerfile UI cleanups: Summary suffix, mobile, warnings, ps in image 2026-04-14 18:55:57 -05:00
fly.toml Increase the amount of memory in fly instance 2026-04-15 07:06:57 -05:00
litestream.yml Single Fly.io deployment: Axum serves frontend, Litestream backs up SQLite 2026-04-12 16:47:08 -07:00
llms.txt AI usage tracking, admin endpoints, and ingest improvements 2026-04-14 20:07:35 -05:00
NAMES.md Add NAMES.md with project rename brainstorm 2026-04-14 08:59:36 -05:00
README.md Restore migration 006 content; move fix into 007 2026-04-14 11:17:10 -05:00
start.sh Single Fly.io deployment: Axum serves frontend, Litestream backs up SQLite 2026-04-12 16:47:08 -07:00
TIGRIS_INVESTIGATION.md Restore migration 006 content; move fix into 007 2026-04-14 11:17:10 -05:00
TODO.md Fix TTS duration and section timings by summing per-chunk durations 2026-04-14 20:37:12 -05:00

TTS Podcast

TODO (2026-04-14): Clean up the Tigris public-hostname handling. During debugging we tried three hostnames (fly.storage.tigris.dev, t3.storage.dev, t3.tigrisfiles.io) and churned on ACLs, bucket policies, and the --public flag before landing on t3.tigrisfiles.io. Confidence in the current code is low — see TIGRIS_INVESTIGATION.md for the full log. Open items: verify playback end-to-end, hoist the public hostname into config, decide whether the acl(public-read) on uploads is actually needed, and consider switching to presigned URLs.

A self-hosted web app that turns web articles, arXiv papers, and PDFs into a private podcast feed. Submit a URL or upload a PDF; the backend scrapes or extracts the text, cleans it with an LLM, optionally summarizes it, synthesizes speech with Google Cloud TTS, generates cover art with Gemini, and publishes an RSS feed that any podcast client can subscribe to.

There is no user login. Access is controlled by a per-feed secret token embedded in the RSS URL and API calls, plus an admin token for feed management.

Repository Layout

backend/          Rust workspace
  tts-lib/          Shared library: providers (Claude/Gemini), TTS, scrape, PDF, cleanup, summarize
  tts-cli/          Command-line tool for running the pipeline locally
  server/           Axum HTTP server + background worker (the binary that ships)
frontend/         SvelteKit (Svelte 5) SPA, built static and served by the backend
scripts/          Helper scripts (sync-secrets.sh)
Dockerfile        Multi-stage build (Bun frontend, cargo-chef backend, Litestream runtime)
fly.toml          Fly.io app config
litestream.yml    Continuous SQLite backup to Tigris (S3)
start.sh          Runtime entrypoint: restore DB, run Litestream supervising the backend

Architecture

  • Backend: Rust / Axum. Single binary serving the API, the RSS feed, the SvelteKit static build, and running the job worker inline.
  • Database: SQLite on a Fly volume, WAL mode, embedded migrations.
  • Backups: Litestream replicates the SQLite WAL to Tigris continuously.
  • Storage: Tigris (S3-compatible) for audio MP3s, cover images, and DB backups.
  • Hosting: A single Fly.io app — no Vercel, no separate worker.

Local Development

Prerequisites

  • Rust 1.94+ (matches the Dockerfile)
  • Bun
  • poppler-utils (for PDF handling)
  • API keys from Anthropic and Google (see DEPLOYMENT.md for how to obtain them)

Setup

cp .env.example .env
# Fill in API keys and generate an admin token: openssl rand -hex 32
set -a; source .env; set +a

Backend

cd backend
cargo run -p tts-podcast-backend

The server listens on http://localhost:8080, creates the SQLite database if missing, and runs migrations on startup.

Frontend

cd frontend
bun install
bun run dev      # dev server on :5173 — set VITE_API_BASE_URL=http://localhost:8080
# or
bun run build    # produces frontend/build, which the backend serves via STATIC_DIR

CLI

The tts-cli crate runs the pipeline end-to-end without the server — handy for debugging cleanup prompts or trying a new provider against a single URL:

cd backend
cargo run -p tts-cli -- --help

Pipeline Overview

URL  → scrape → clean → [summarize] → tts → done → image
PDF  → pdf    → clean → [summarize] → tts → done → image
  • scrape: readability for articles; arXiv API + ar5iv.org HTML for papers.
  • pdf: Claude vision reads the PDF as a document (handles two-column layouts).
  • clean: Claude (or Gemini, configurable) removes navigation junk, fixes encoding, rewrites equations as spoken English for academic sources.
  • summarize (optional, per-episode flag): LLM produces a shorter spoken version stored as transcript. If present, TTS uses this instead of the cleaned text.
  • tts: Google Cloud TTS, chunked at sentence boundaries, MP3 frames concatenated.
  • image: Gemini 2.5 Flash Image generates cover art from a short summary. Runs after the episode is already done — failures are non-fatal.

Each stage is a job row with exponential-backoff retry.

API

See DESIGN.md for the full surface. The useful endpoints:

  • POST /api/v1/feeds — create a feed (admin)
  • GET /api/v1/feeds/:token — feed + episodes (token-auth)
  • POST /api/v1/feeds/:token/episodes — submit a URL. Body: { url, summarize?, tts_provider? }
  • POST /api/v1/feeds/:token/episodes/pdf — upload a PDF (multipart; accepts source_url to preserve the original link)
  • GET /feed/:token/rss.xml — the RSS feed to hand to a podcast client

Deployment

Everything runs on one Fly.io app with a mounted volume and a Tigris bucket. See DEPLOYMENT.md for the full setup, including how to create and rotate each API key.

License

Private / personal use.