AI-powered podcast generation: extract content from any source, generate multi-role dialogue scripts via Gemini LLM, synthesize speech with Edge TTS — now with word-level subtitles for karaoke-style highlighting.
New subtitle-gen CLI and podcast/subtitle_generator.py module that uploads the final podcast mp3 to Gemini, requests schema-bound per-token timing, and writes 4 subtitle artifacts alongside the audio:
| File | Use case |
|---|---|
<stem>.words.json |
Front-end / video player per-word rendering (segments[{speaker,text,start,end,words:[...]}]) |
<stem>.words.vtt |
WebVTT with inline <00:00:00.500> karaoke time tags |
<stem>.words.lrc |
Enhanced LRC (A2 extension) with per-token <mm:ss.xx> timestamps |
<stem>.srt |
Classic sentence-level SRT |
Tokenization adapts to language:
- CJK (
zh/ja/ko): 2–5-character natural phrases - Space-separated (
en/es/fr/ …): one token per word
Long audio is automatically split into chunks (default 180 s, SUBTITLE_CHUNK_SEC / --chunk-sec) to avoid Gemini MAX_TOKENS truncation; chunk timestamps are re-offset and merged. max_output_tokens=65535 is set explicitly, and any chunk that still hits MAX_TOKENS is automatically halved and retried down to a 30 s floor.
Standalone use:
subtitle-gen generate audios/podcast/foo.mp3 --language zh
# or via make
make subtitle-gen AUDIO=audios/podcast/foo.mp3 LANG=zhIntegrated into gen-podcast run as opt-in via --subtitles (default off, safe for databases that have not yet been migrated). Override the model with --subtitle-model.
Four new columns on podcast to store R2 URLs of the generated subtitle artifacts:
ALTER TABLE podcast ADD COLUMN subtitle_json_url text DEFAULT "";
ALTER TABLE podcast ADD COLUMN subtitle_vtt_url text DEFAULT "";
ALTER TABLE podcast ADD COLUMN subtitle_lrc_url text DEFAULT "";
ALTER TABLE podcast ADD COLUMN subtitle_srt_url text DEFAULT "";Full schema + migration lives in podcast/sql/podcast.sql and the comment block at the top of podcast/insert_podcast.py. New insert-podcast update-podcast-subtitles-to-d1 CLI backfills URLs on existing rows.
podcast/_llm_retry.py centralizes Gemini transient-error handling (is_transient_llm_error, invoke_with_transient_retry, stream_with_transient_retry); text (content_parser/table/podcast.py) and audio (subtitle_generator.py) paths now share identical backoff semantics. Upload and generate_content are both wrapped so ConnectTimeout during file upload retries cleanly.
- Last role of a podcast was silently dropped when the Gemini partial stream ended on an empty-content role (
content_parser_tts.py). Theduplicate-overwrite mechanism relies on the next role's yield to refresh the previous one's content — the last role never had a "next," so a truncated or empty final utterance (e.g.Weedge: 再见,下次见!) was lost. Added a post-loop fallback that either appends ([final-append]) or overwrites ([final-fix]) the final role using the stream's final state.
New environment variables (both optional, sensible defaults):
| Variable | Default | Purpose |
|---|---|---|
GEMINI_SUBTITLE_MODEL |
gemini-2.5-flash |
Audio-capable model for the subtitle pass |
SUBTITLE_CHUNK_SEC |
180 |
Chunk length (s) for long-audio splitting; chunks that still hit MAX_TOKENS auto-halve to a 30 s floor |
CLAUDE.mdadded — architecture, pipeline, commands, D1 schema. Read first when operating this repo with Claude Code.README.md/README_CN.mdupdated — new feature row, env table row,runoptions, standalone Word-Level Subtitles section, make target, manual backfill CLI.makefile— newmake subtitle-gen AUDIO=... [LANG=...] [ARGS="..."]target.
pip install gen-podcast==0.1.3Existing D1 databases need the four ALTER TABLE statements above before running gen-podcast run with subtitles enabled (or pass --no-subtitles).
| File | Change |
|---|---|
podcast/subtitle_generator.py |
new — Gemini audio → word-level subtitle generation + CLI |
podcast/_llm_retry.py |
new — shared Gemini retry helpers |
podcast/sql/podcast.sql |
new — D1 schema + migration SQL |
CLAUDE.md |
new — Claude Code guidance |
podcast/gen_podcast.py |
--subtitles / --no-subtitles, --subtitle-model; auto subtitle generation + R2 upload |
podcast/insert_podcast.py |
4 subtitle URL columns; update-podcast-subtitles-to-d1 CLI |
podcast/content_parser_tts.py |
post-loop final-role fallback |
podcast/content_parser/table/podcast.py |
retry logic extracted to _llm_retry |
makefile, README.md, README_CN.md, .env.example, pyproject.toml |
version bump, docs, new target, new env vars, new CLI entry |