- Python 81%
- JavaScript 11%
- CSS 6%
- Docker 1%
- HTML <1%
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:
- 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). - 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.
- 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:episodetag, else a leading number in the title, else assigned sequentially by publish date. - Guest names: parsed from
NNN – Guesttitles 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
@handlein 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.