README.md

podscribe #

Point it at a podcast RSS feed. Every episode gets a speaker-labeled, copy-edited transcript on a browsable web page, with click-a-timestamp-to-listen audio. New episodes are picked up automatically within the hour.

Built for the Unnamed Simcluster Podcast (live at simcluster.cee.wtf), configurable for any show with a similar shape: RSS 2.0 feed, direct audio enclosures, a recurring host talking to one guest per episode.

How it works #

Three models, each doing only what it's best at:

  1. ElevenLabs Scribe v2 transcribes the full episode in one request with acoustic diarization (speaker_0/speaker_1), word timestamps, and audio tags like [laughs]. Its speaker attribution is authoritative — in manual verification against ground truth it won every single contested attribution against role-based inference (8/8).
  2. Gemini hears only the first 6 minutes and transcribes it with real names, knowing who the host is (from config) and who the guest is (from the feed). Scribe's anonymous ids are then mapped onto names by word-weighted vote. Gemini never attributes the final transcript — it only votes; individual ballots can be wrong, the election is decisive. If the vote is ambiguous (< 0.75 agreement) the episode falls back to generic "Speaker N" labels with a visible warning rather than guessing.
  3. A stronger Gemini (text only, no audio) copy-edits the result: proper nouns from a roster built out of the whole feed, certain mishearings, punctuation, transcription stutters. Never paraphrases, never touches speaker labels. The pre-edit turns are kept alongside (raw_turns).

Costs per episode (~40 min): about $0.15 of Scribe, ~$0.05 of naming Gemini, $0.15–0.70 of review Gemini depending on how hard it thinks. Episodes are keyed by feed guid and only ever paid for once; failures park in error status for manual retry instead of auto-retrying into a bill.

Quickstart #

cp deploy/.env.example .env    # fill in ELEVEN_LABS_API_TOKEN + GEMINI_API_KEY
docker compose -f compose.example.yml up -d --build
open http://localhost:8080

With no other config it transcribes the Simcluster podcast. To point it at your show, set (see deploy/.env.example for the full list):

var meaning
FEED_URL any RSS 2.0 feed with audio enclosures
SHOW_NAME / SHOW_URL branding for the UI + model prompts
HOST_NAME / HOST_NOTES who the recurring host is; prose that helps identify them
EXTRA_CONTEXT extra names/spellings for the copy-editor, one per line
NUM_SPEAKERS default 2; other values disable host/guest naming
BACKFILL_LIMIT default 10 — on first run only the newest N episodes are transcribed; the rest show as "not transcribed" and can be run on demand (POST /api/episodes/{n}/retry). Pointing at a 10-year feed will not buy you a surprise $500 backfill.
ACCENT_COLOR UI accent

Feed assumptions (and their fallbacks) #

  • Episode numbers: itunes:episode tag, else a leading number in the title, else assigned sequentially by publish date.
  • Guest names: parsed from NNN – Guest titles when possible; otherwise Gemini infers the name from the episode metadata + how the guest is introduced in the audio, and declares it explicitly.
  • Guest handles: first @handle in the episode description, if any.
  • Some media hosts 403 non-browser user agents; all feed/audio requests send a browser UA.

API #

endpoint
GET /api/episodes list with status/cost/word counts
GET /api/episodes/{n} full transcript
GET /api/meta show branding
POST /api/poll poll the feed now (cost-safe, dedupes)
POST /api/episodes/{n}/retry re-run a failed episode
POST /api/episodes/{n}/review run the copy-edit pass once on a done episode

Development #

cd backend && pip install -r requirements.txt
uvicorn main:app --port 8914          # API (put keys in backend/.env)
cd frontend && npm install && npm run dev   # UI on :5174, proxies /api

deploy/ holds the original cee.wtf deployment config; ignore it for your own installs.