CLI (podtx) to pull podcast episodes from an RSS feed and transcribe them locally on Apple Silicon.
Default engine: NVIDIA Parakeet TDT v3 via parakeet-mlx.
Optional: OpenAI Whisper via mlx-whisper.
uv sync --extra allOr install a single engine:
uv sync --extra parakeet # default engine
uv sync --extra whisperActivate / run:
uv run podtx --help# Register a feed and sync the latest 5 episodes (default)
podtx add https://example.com/podcast.rss
podtx sync
# One-shot: latest episode from a feed → files in the current directory
podtx transcribe https://example.com/podcast.rss
# Local file or direct audio URL
podtx transcribe ./episode.mp3
podtx transcribe https://example.com/audio/ep01.mp3 --engine whisper| Command | Description |
|---|---|
podtx add <rss-url> |
Register a feed |
podtx remove <feed> |
Unregister by slug or URL |
podtx feeds |
List registered feeds |
podtx show <feed> |
Episode status for a feed |
podtx doctor |
Library health: failed / stuck episodes, empty feeds, missing outputs |
podtx sync [feed] |
Transcribe new episodes |
podtx transcribe <target> |
One-shot RSS / URL / file |
podtx format <json|--feed|--all> |
Re-format existing transcript JSON (no ASR) |
podtx rename --from-title --feed|--all |
Fix _000_ filenames from title episode numbers |
podtx summarize <json|--feed|--all> |
Summarize existing transcripts (no ASR) to episode.summary.json / .md |
podtx search <query> [--feed] [--limit] [--since] [--until] [--reindex] |
Offline FTS5 search over transcripts |
--engine parakeet|whisper— ASR backend (default:parakeet)--model <hf-repo>— override model id--limit N/--all— how many pending episodes (syncdefault limit: 5)--format srt/--format vtt— add subtitle outputs (.txt+.jsonalways by default)--keep-audio— retain downloaded audio--local-attention/--full-attention— Parakeet attention mode (default: local; required for long episodes)--local-attention-context-size N— local attention window (default: 256)--readable— paragraph breaks for human reading: silence gaps, then sentence boundaries after ~20s, with a max ~45s / ~120 words so long Parakeet runs don’t stay as one wall of text (default is raw continuous text; JSON timestamps are always rounded)--cleanup— stripuh/umand collapse consecutive word/phrase doubles (1–4 words, e.g.the the,I think I think) in text outputs (JSON segments stay raw)--out-dir/--data-dir— override output or app data paths--quiet— less terminal noise
Re-format an existing transcript without re-running ASR:
podtx format path/to/episode.json --readable --cleanup
podtx format --feed corecursive-coding-stories --readable --cleanup
podtx format --all --readable --cleanupRename already-transcribed files whose episode number was missing (_000_)
when the title embeds a clear number (same rules as filename inference above):
podtx rename --from-title --feed syntax-tasty-web-development-treats --dry-run
podtx rename --from-title --feed syntax-tasty-web-development-treats
podtx rename --from-title --allCheck library health — failed or still-pending episodes, feeds with no recorded episodes, and done episodes whose transcript files no longer exist (read-only):
podtx doctorCreate short summaries from existing transcript JSON without re-running ASR:
podtx summarize path/to/episode.json
podtx summarize path/to/episode.json --format md # also write a markdown render
podtx summarize --feed corecursive-coding-stories --limit 5
podtx summarize --all --limit 10Output is a stable sidecar written next to the transcript (or --out-dir):
episode.summary.json (default) and/or episode.summary.md with --format md,
containing a short overview, key points/takeaways, and optional timestamped quotes.
Backends — explicitly opt-in, never silent network calls:
| Backend | Description | Credentials |
|---|---|---|
fake (default) |
Offline extractive summary — no network, no model | none |
openrouter |
Hosted LLM via OpenRouter | podtx auth set openrouter, or OPENROUTER_API_KEY, or --api-key |
opencode |
OpenCode Zen Go / Meta-hosted models | podtx auth set opencode, or OPENCODE_API_KEY, or --api-key |
lmstudio / local |
Local LM Studio (OpenAI-compatible) | none — defaults to http://localhost:1234/v1 |
--backendselects the backend;--modeloverrides the default model id;--base-urloverrides the endpoint (e.g. a non-standard LM Studio port).--temperature(default 0.3),--timeout(default 60s), and--max-input-chars(default: no truncation) tune the LLM calls.- Same precedence as transcription: CLI flags > environment (
PODCAST_TRANSCRIBER_SUMMARIZE_*) >config.toml(summarize_backend,summarize_model, …) > defaults.
- Data (SQLite state, transcripts, temp audio):
~/.local/share/podcast-transcriber/ - Optional config:
~/.config/podcast-transcriber/config.toml - Transcript filenames:
{YYYY-MM-DD}_{episode:03d}_{slug}. Episode comes from RSSitunes:episodewhen present; otherwise a clear leading number in the title is used (860 - …,#860 …,Episode 860: …). Section-style ids like1.1 - …are ignored (fallback000).
engine = "parakeet"
# model = "mlx-community/parakeet-tdt-0.6b-v3"
limit = 5
formats = ["txt", "json"]
keep_audio = false
local_attention = true
# local_attention_context_size = 256
# readable = true
# cleanup = true
# summarize_backend = "fake" # fake (offline) | openrouter | opencode | lmstudio
# summarize_model = "<model-id>"
# summarize_timeout = 60.0
# summarize_temperature = 0.3Precedence: CLI flags > environment (PODCAST_TRANSCRIBER_*) > config.toml > defaults.
| Engine | Default model | Extra |
|---|---|---|
parakeet |
mlx-community/parakeet-tdt-0.6b-v3 |
--extra parakeet |
whisper |
mlx-community/whisper-large-v3-turbo |
--extra whisper |
New engines: implement TranscriptionEngine in src/podtx/engines/ and register in registry.py.
- Parakeet TDT v3 weights are released under CC BY 4.0 (attribution required). See nvidia/parakeet-tdt-0.6b-v3.
- This project’s code is GPL-3.0-only (see
LICENSE).
uv sync --extra all --extra dev
uv run pytestCI runs the unit suite with coverage on pull requests via jobs test and coverage-ratchet (see CONTRIBUTING.md). Project statement/branch floors come from repo variables COVERAGE_RATCHET_MIN (default 65%) and COVERAGE_RATCHET_MIN_BRANCHES (default 45%); combined coverage is informational. PRs also get a sticky coverage comment (whole package, not patch).
See CONTRIBUTING.md for how to file bugs and feature requests.