A self-hosted ad remover that uses your Pocket Casts account as the sync fabric. Built on top of MinusPod: downloads each episode, removes the ads with a local LLM, and puts the clean version back into your Pocket Casts Up Next queue on every device.
Important
This app requires an active Pocket Casts Plus
subscription. The cleaned .mp3 files are uploaded back to Pocket Casts as
custom files, which is a Plus-only feature. The free tier will accept your
login but reject the uploads.
- Why this exists
- How it works
- Quick start
- First-time setup
- Web UI
- CLI
- Configuration reference
- Architecture
- MinusPod patches
- Troubleshooting
- Logs
- Tests
- Contributing
- License & credits
Premium podcast subscriptions cut ads, but only for a handful of shows you pay for individually. This project sits between Pocket Casts and your podcasts and strips ads from everything you already subscribe to — transcription runs locally on your machine; ad detection can use local Ollama or a cloud LLM API.
The hard parts (transcription, ad detection, audio surgery) come from MinusPod. This repo adds:
- Pocket Casts integration — auth, episode listing, custom-file uploads, Up Next sync, played/archived state, auto-reconciliation of stale queues.
- Local-first orchestration — a Flask dashboard that drives MinusPod from a podcast-centric (not feed-centric) view.
- Portable transcription backends — use
whisper.cppnatively on macOS (Metal), Linux (CUDA/CPU), or the vendor Docker image on anything else. - History & accounting — every cleaned episode lands in a searchable, exportable history with time saved and ad counts.
┌─────────────────────────────────────────────┐
│ Pocket Casts (your subscriptions) │
└───────────────┬────────────────┬────────────┘
│ │
1. List │ │ 6. Upload + sync
▼ │
┌────────────────────────┐ │
│ Dashboard (Flask) │ │
│ this repo │───────┘
└────┬───────────────────┘
│ 2. Hand off feed
▼
┌────────────────────────┐
│ MinusPod │
│ (port 8000, patched) │
└────┬─────────┬─────────┘
│ │
3. Whisper 4. LLM ad detection
│ │
▼ ▼
┌──────────┐ ┌──────────┐
│ whisper │ │ LLM │
│ .cpp │ │ Ollama / │
│ :8765 │ │ API │
└──────────┘ └──────────┘
│
5. FFmpeg cuts the ads, re-embeds metadata
│
▼
cleaned `.mp3` → uploaded to Pocket Casts
When Pocket Casts already has its own AI transcript for an episode, the pipeline still uses Whisper locally — the PC transcript is checked against the audio as a confidence check, but it is never used as the ad-detector input. PC transcripts are produced from the show's own content and omit dynamically-inserted host-read ads; feeding one to the ad detector would guarantee it finds nothing to cut.
Already installed whisper.cpp and MinusPod (and Ollama, if you use local LLM)? One command.
cp .env.example .env # add Pocket Casts credentials + LLM settings
source .venv/bin/activate && source .env && python3 pocketcasts_adfree.py ui
# Open http://localhost:5050The UI auto-starts Whisper and MinusPod in the background on every launch.
Ollama is started only when LLM_PROVIDER=ollama (the default); if you
use a cloud LLM API instead, Ollama is skipped entirely. No separate
./start_services.sh step is required. MinusPod is also auto-updated from
upstream on each start, and the
LLM cost-optimisation patch is re-applied on top.
Required everywhere:
- An active Pocket Casts Plus subscription — the pipeline uploads cleaned files to Pocket Casts Cloud, which is a Plus-only feature.
- Python 3.10+
ffmpeg- An ad-detection LLM — either:
- Ollama running locally (default), or
- A cloud / remote API (OpenRouter, OpenAI, Groq, Together, a self-hosted vLLM/LiteLLM endpoint, etc.) — see Choosing an LLM backend
- 16 GB of RAM minimum for local Whisper transcription; 32 GB+ recommended only if you also run a large local LLM (e.g. the default 35B Ollama model). With a cloud LLM, RAM pressure is much lower — you mainly need headroom for Whisper.
- About 10 GB of disk for vendored models + transcripts (Whisper weights; Ollama models are additional if you run locally).
Platform-specific notes:
| Platform | Transcription backend | Notes |
|---|---|---|
| macOS (Apple Silicon) | whisper.cpp native with -DWHISPER_METAL=ON |
Fastest path; scripts/setup_whisper.sh handles it. |
| macOS (Intel) / Linux | whisper.cpp native (CPU or CUDA) |
Same script; set WHISPER_CUDA=1 before running if you have an NVIDIA GPU. |
| Windows / other | Docker image | Use the whisper.cpp server Docker container. The Services panel warns when Docker is in use because it's much slower on ARM/Apple. |
Install the toolchain (macOS example — substitute your OS's package manager):
brew install ffmpeg cmake
# Only if using local Ollama (LLM_PROVIDER=ollama, the default):
brew install ollamagit clone https://github.com/<your-fork>/pocket-casts-mod.git
cd pocket-casts-modcp .env.example .env
$EDITOR .env # add POCKETCASTS_EMAIL and POCKETCASTS_PASSWORDpython3 -m venv venv && source venv/bin/activate
pip install -r requirements.txtThe MinusPod/ and whisper.cpp/ checkouts are deliberately not
committed; the helper scripts re-create them at known-good commits and apply
the local patches in patches/.
./scripts/setup_minuspod.sh # clone, pin commit, apply patches/minuspod-local.patch
./scripts/setup_whisper.sh # clone, build with WHISPER_METAL=ON, fetch modelAd detection classifies transcript windows with an LLM — one call per ~8 min
of audio, so a 4-hour episode is ~30 calls. Set LLM_PROVIDER in .env to
pick how MinusPod reaches that model.
LLM_PROVIDER |
When to use | Key variables |
|---|---|---|
ollama (default) |
Free, private, runs on your GPU/RAM | OPENAI_MODEL, optional OPENAI_BASE_URL |
openrouter |
One API key, 200+ models (Claude, GPT, DeepSeek, …) | OPENROUTER_API_KEY, OPENAI_MODEL |
openai-compatible |
Any OpenAI-compatible endpoint | OPENAI_BASE_URL, OPENAI_API_KEY, OPENAI_MODEL |
anthropic |
Direct Claude API (no OpenAI shim) | ANTHROPIC_API_KEY |
When LLM_PROVIDER is anything other than ollama, the dashboard does not
start Ollama and hides it from the Services panel — only Whisper and
MinusPod need to run locally.
Out of the box the pipeline expects Ollama with qwen3.5:35b-a3b;
start_services.sh derives a tuned variant named qwen3.5-addetect with a
16 K context.
Pick a model that fits your machine. A too-large model is both slow (the "45 min per episode" complaint) and a hard-crash risk on machines with less than ~48 GB RAM (model + Whisper + KV cache + your other apps can panic the kernel).
| Free RAM | Recommended model | Why |
|---|---|---|
| ≥ 48 GB | qwen3.5:35b-a3b |
Best accuracy. MoE so generation is fast. ~22 GB resident. |
| 24-48 GB | qwen3:14b |
Solid accuracy, ~9 GB resident, ~2× the windows/min. Default for ≤ 36 GB Macs. |
| 8-24 GB | llama3.1:8b |
Acceptable for short shows; misses the occasional native-read sponsor. ~5 GB. |
# .env — local Ollama (default; LLM_PROVIDER=ollama can be omitted)
export LLM_PROVIDER=ollama
export OPENAI_MODEL=qwen3:14b
export OPENAI_BASE_URL=http://localhost:11434/v1
ollama pull qwen3:14bIf you change the model after start_services.sh already created the
qwen3.5-addetect alias, set OPENAI_MODEL explicitly so MinusPod stops
asking for the alias.
Use this when you don't want a multi-GB model resident on your machine. Transcription still runs locally via whisper.cpp; only ad detection goes to the API.
OpenRouter — one key, many models. Model IDs use the provider/model
form from openrouter.ai/models:
# .env — OpenRouter
export LLM_PROVIDER=openrouter
export OPENROUTER_API_KEY=sk-or-v1-your-key-here
export OPENAI_MODEL=deepseek/deepseek-v4-flash
# Optional: pin discounted infrastructure hosts (see the model's Providers tab
# on openrouter.ai for exact slugs). Without this, OpenRouter load-balances at
# blended pricing — often ~3× more than hosts like GMICloud or Novita.
export OPENROUTER_PROVIDER_ORDER=GMICloud,Novita,Alibaba
export OPENROUTER_ALLOW_FALLBACKS=false
# Or auto-pick cheapest: export OPENROUTER_PROVIDER_SORT=priceAny other OpenAI-compatible API — OpenAI, Groq, Together, Fireworks, a
local MLX/vLLM/LiteLLM proxy, etc. Set LLM_PROVIDER=openai-compatible and
point OPENAI_BASE_URL at the provider's /v1 root:
# .env — OpenAI
export LLM_PROVIDER=openai-compatible
export OPENAI_BASE_URL=https://api.openai.com/v1
export OPENAI_API_KEY=sk-your-openai-key
export OPENAI_MODEL=gpt-4o-mini
# .env — Groq (example)
export LLM_PROVIDER=openai-compatible
export OPENAI_BASE_URL=https://api.groq.com/openai/v1
export OPENAI_API_KEY=gsk_your-groq-key
export OPENAI_MODEL=llama-3.3-70b-versatile
# .env — self-hosted or custom proxy (example)
export LLM_PROVIDER=openai-compatible
export OPENAI_BASE_URL=http://localhost:8800/v1
export OPENAI_API_KEY=not-needed
export OPENAI_MODEL=qwen3:14bRestart the UI (or MinusPod from the Services panel) after changing LLM settings so MinusPod picks up the new environment.
source .env && python3 pocketcasts_adfree.py uiOpen http://localhost:5050. The UI starts Whisper and MinusPod automatically
(Ollama too, when LLM_PROVIDER=ollama) — watch the floating log panel for
progress.
First launch takes ~60 s for MinusPod to initialise; subsequent starts are
faster because services are already running.
Manual service control:
./start_services.shis still available if you want to pre-warm services before launching the UI, or start them without the UI at all (e.g. CLI use). It also handles the--mlxflag for MLX-based LLM inference.
The dashboard at http://localhost:5050 has two views:
- Stat cards — Subscriptions, Eligible, Patreon (skipped), Processed Episodes. Click any card to filter the list below.
- In Up Next — every episode currently queued in Pocket Casts (including uploaded custom files), grouped by podcast. Custom files are inline and editable: rename, mark played/unplayed, remove from Up Next, delete. The dashboard also auto-reconciles stale originals: whenever an ad-free upload exists, the original episode is silently removed from Up Next and marked played.
- All Podcasts — every subscription. Expand a row to see episodes.
- Episodes are tagged
unplayed/in progress/played/archived/processed, with play status pulled directly from your Pocket Casts account. - Each episode has inline Queue / Un-queue and Mark played / Mark unplayed buttons.
- Header checkbox (with
indeterminatestate) selects all eligible episodes for the podcast. - Per-podcast Reset processed button if you want to re-process older episodes.
- Episodes are tagged
- Auto-refresh — the list refreshes every ~20 seconds while the tab is visible and you haven't selected anything. No manual refresh button needed.
- Toolbar — Search, Process Selected, Services, Clean up played Ad-Free files.
- Floating log panel — colored real-time log. Auto-expands on any new log entry so progress, Whisper/LLM messages, and errors are always visible; collapse manually from the header. Skip and Stop appear here when a job is running.
Every processed episode, with timestamp, podcast, episode title, ads removed, and time saved. Sortable, filterable, exportable as CSV.
Click Services in the toolbar.
Each row shows: status dot (healthy / running but unhealthy / down), backend
pill (native / docker / brew), pid, port, and a docs link that jumps
to the relevant section of this README. When LLM_PROVIDER=ollama, the footer
also has a model picker for MinusPod ad detection; with a cloud provider,
Ollama is hidden and the active LLM provider is shown instead.
| Service | Port | Managed via | Configured by |
|---|---|---|---|
| Ollama | 11434 | brew services (preferred); skipped when using a cloud LLM |
LLM_PROVIDER, OPENAI_MODEL |
| Whisper | 8765 | Native binary or Docker | scripts/setup_whisper.sh, models in whisper.cpp/models/ |
| MinusPod | 8000 | Flask under MinusPod/venv/ — auto-updated on every start |
LLM_PROVIDER and related vars in .env |
| Pipeline UI | 5050 | This repo | python3 pocketcasts_adfree.py ui |
The panel won't let you stop the UI itself (it'd kill the panel that's hosting it).
source .env && source venv/bin/activate
# Launch the dashboard (auto-starts all services)
python3 pocketcasts_adfree.py ui
# Test the pipeline end-to-end on a single feed (services must be running)
python3 pocketcasts_adfree.py test --rss-url 'https://feeds.simplecast.com/54nAGcIl'
# Process every feed registered in MinusPod
python3 pocketcasts_adfree.py auto
# Filter by podcast name (case-insensitive substring)
python3 pocketcasts_adfree.py auto --filter 'daily'All configuration lives in .env. Copy .env.example to start, then override
only what you need.
| Variable | Purpose |
|---|---|
POCKETCASTS_EMAIL |
Pocket Casts account email. |
POCKETCASTS_PASSWORD |
Pocket Casts account password. |
Set LLM_PROVIDER to choose how MinusPod runs ad detection. See
Choosing an LLM backend for full examples.
| Variable | Default | Effect |
|---|---|---|
LLM_PROVIDER |
ollama |
ollama (local), openrouter, openai-compatible, or anthropic. Non-ollama values skip starting Ollama. |
OPENAI_MODEL |
qwen3.5-addetect |
Model name / ID passed to MinusPod. For OpenRouter use provider/model slugs; for Ollama use ollama list names. |
OPENAI_BASE_URL |
http://localhost:11434/v1 |
Base URL for Ollama or openai-compatible providers (must end in /v1). Ignored for openrouter and anthropic. |
OPENAI_API_KEY |
not-needed |
API key for openai-compatible endpoints. Set to your provider's key (OpenAI, Groq, Together, etc.). |
OPENROUTER_API_KEY |
— | Required when LLM_PROVIDER=openrouter. |
OPENROUTER_PROVIDER_ORDER |
— | Comma-separated OpenRouter host slugs to try in order (e.g. GMICloud,Novita,Alibaba). Pins discounted providers instead of blended pricing. |
OPENROUTER_ALLOW_FALLBACKS |
false when order is set |
Whether OpenRouter may use hosts outside OPENROUTER_PROVIDER_ORDER. |
OPENROUTER_PROVIDER_SORT |
— | Auto-rank hosts: price, throughput, or latency. Alternative to an explicit order list. |
ANTHROPIC_API_KEY |
— | Required when LLM_PROVIDER=anthropic. |
| Variable | Default | Effect |
|---|---|---|
AD_START_PAD |
1.5 |
Seconds to extend each ad earlier. |
AD_END_PAD |
2.0 |
Seconds to extend each ad later. |
AD_END_PAD_TAIL |
5.0 |
Extra padding for ads that end at the very end of the file. Catches musical outros that Whisper truncates. |
TAIL_GAP_MIN_SECONDS |
60 |
If Whisper's transcript ends this many seconds before the audio file does, treat the gap as an untranscribed post-roll ad and cut it. |
| Variable | Default | Effect |
|---|---|---|
WINDOW_SIZE_SECONDS |
600 |
Transcript window size handed to the LLM. |
WINDOW_OVERLAP_SECONDS |
120 |
Overlap between consecutive windows. |
LARGE_WINDOW_SECONDS |
1200 |
Window size used in place of WINDOW_SIZE_SECONDS for 1 M-context models (DeepSeek V4, Gemini Flash, Qwen Long, Llama 4 / 3.1-405B) on episodes longer than 2 × WINDOW_SIZE_SECONDS. Cuts the per-episode LLM call count roughly in half on long episodes. |
SKIP_VERIFICATION_UNDER_SECONDS |
1200 (20 min) |
Skip the verification pass on episodes shorter than this. Short episodes rarely have ads that survive pass 1, and verification doubles the LLM cost for near-zero yield. Set to 0 to disable. |
ENABLE_PROMPT_CACHING |
true |
Annotate the system prompt with cache_control: ephemeral so the provider can cache it across the ~22 windows of a long episode. Cached input tokens are reported in the response log; the cost calculator stays cache-unaware. |
AD_DETECTION_MAX_TOKENS |
4096 |
Token budget per LLM call. |
OLLAMA_NUM_PARALLEL |
1 |
(Ollama only) Concurrent requests. Each in-flight slot duplicates the KV cache. Increase only on machines with ≥48 GB free RAM. |
OLLAMA_MAX_LOADED_MODELS |
1 |
(Ollama only) How many models Ollama keeps resident. Bumping this silently doubles memory if MinusPod swaps detection ↔ verification ↔ chapters models. |
OLLAMA_KEEP_ALIVE |
30s |
(Ollama only) How long Ollama keeps the model loaded after the last request. Short values quiet the fans between episodes; longer values save the ~30 s reload cost. |
EPISODE_MAX_WALLCLOCK_SECONDS |
5400 (90 min) |
Hard cap on a single episode. If exceeded the orchestrator gives up and moves to the next one so the queue stays unblocked. |
EPISODE_STALL_THRESHOLD_SECONDS |
900 (15 min) |
If MinusPod's stage doesn't change this long during transcription, restart whisper-server. Same threshold twice aborts the episode. |
EPISODE_STALL_THRESHOLD_LLM_SECONDS |
2700 (45 min) |
Higher cap during ad detection / verify / review — each LLM window can take many minutes on large models. |
LLM_TIMEOUT_LOCAL |
1200 (20 min) |
MinusPod per-window timeout for local Ollama (was 10 min; too tight for qwen3.5-addetect). Cloud APIs use MinusPod's shorter default. |
| Component | Path | Port | Role |
|---|---|---|---|
| Pipeline orchestrator | pocketcasts_adfree.py |
— | CLI + Pocket Casts API client + sync engine |
| Web server | ui_server.py |
5050 | Flask app exposing the dashboard and REST API |
| Service control plane | services_manager.py |
— | Start/stop/restart/health for the four backends |
| Templates | templates/ |
— | index.html, readme.html |
| Static assets | static/ |
— | css/app.css, js/app.js |
| Tests | tests.py |
— | unittest-based suite |
| MinusPod (vendored) | MinusPod/ |
8000 | Ad detection + audio processing engine. Re-cloned via scripts/setup_minuspod.sh. |
| whisper.cpp (vendored) | whisper.cpp/ |
8765 | Local Metal-accelerated ASR. Re-cloned via scripts/setup_whisper.sh. |
| Ollama | (system) | 11434 | Local LLM inference when LLM_PROVIDER=ollama. |
| Cloud LLM | (remote) | — | OpenRouter, OpenAI, Groq, Anthropic, or any openai-compatible endpoint when LLM_PROVIDER is set accordingly. |
MinusPod classifies transcript segments as ad / non-ad using whichever backend
LLM_PROVIDER selects:
ollama— runs on your machine. Managed viabrew services start ollama; the dashboard's Services panel can also start/stop/restart it. Model is selectable at runtime in the panel footer (picks any model inollama list).openrouter— routes to any OpenRouter model viaOPENROUTER_API_KEY+OPENAI_MODEL.openai-compatible— works with any provider that speaks the OpenAI Chat Completions API: OpenAI, Groq, Together, Fireworks, self-hosted vLLM, MLX proxies, etc. SetOPENAI_BASE_URLto the provider's/v1root andOPENAI_API_KEYto your key (not-neededfor local proxies that don't require auth).anthropic— direct Claude API viaANTHROPIC_API_KEY.
Whisper transcription always runs locally (or via your configured whisper.cpp backend); cloud LLM providers do not replace transcription.
Two backends supported:
- Native (Metal, recommended) — built by
scripts/setup_whisper.shwith-DWHISPER_METAL=ON. Runs on the GPU, ~10× faster than Docker on Apple Silicon. - Docker — provided as a fallback for non-macOS hosts. The Services panel warns when this path is in use.
Models live in whisper.cpp/models/ (ggml-large-v3-turbo.bin is preferred
when present).
Local modifications to MinusPod live as patches in
patches/. The pinned upstream commit is recorded in
patches/MINUSPOD_BASE.txt. setup_minuspod.sh
applies them in order with git apply --3way; if a patch no longer applies
cleanly against the pinned commit, the script warns and continues rather than
failing the install.
| Patch | Purpose |
|---|---|
minuspod-local.patch |
Honour DATA_DIR, env-tunable window sizes, detect_tail_gap, ad padding, SKIP_VERIFICATION=true. |
llm-cost-optimizations.patch |
The three LLM cost tunables documented under MinusPod runtime: large-window override for 1 M-context models, configurable SKIP_VERIFICATION_UNDER_SECONDS, and OpenRouter prompt caching on the system prompt. Adds the "Ad detection" panel in this UI. |
The three LLM cost tunables are exposed in the dashboard under Ad
detection. Changes land in the MinusPod database immediately and take effect
on the next episode processed — no service restart required. The dashboard
GETs /api/v1/settings to read the current values (showing whether each is at
the default, set in the DB, or overridden by an env var) and PUTs
/api/v1/settings/ad-detection to write. Unknown keys are rejected before the
request reaches MinusPod, and MinusPod's own cross-field validation
(LARGE_WINDOW_SECONDS >= WINDOW_SIZE_SECONDS) is surfaced back to the UI.
| Symptom | Likely cause / fix |
|---|---|
No module named httpx |
source venv/bin/activate && pip install -r requirements.txt |
| Upload fails with 403 / "subscription required" | Your Pocket Casts account is on the free tier. Custom-file upload is a Plus feature. |
Could not find RSS for: [name] |
The pipeline resolves feeds via the iTunes Search API. Pass --rss-url directly or add the feed manually in MinusPod. |
| MinusPod "Circuit breaker OPEN" | The LLM endpoint failed repeatedly. With Ollama, check ollama list; with a cloud API, verify LLM_PROVIDER, API key, and OPENAI_MODEL. The UI auto-restarts MinusPod on next launch. |
| Fans still spinning after a job | (Ollama only) The pipeline auto-unloads Ollama. Force it: curl -s -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{"model":"<your-model>","keep_alive":"0s"}' |
| Ollama missing from Services panel | Expected when LLM_PROVIDER is not ollama. The panel shows which cloud provider is active instead. |
| Cloud LLM works but transcription is slow | Normal — Whisper still runs locally. Cloud LLM only speeds up ad detection, not transcription. |
start_services.sh not needed? |
Correct — the UI auto-starts everything. start_services.sh is still useful for pre-warming services before the UI, or for --mlx mode. |
| MinusPod patch failed to apply after auto-update | A new upstream release changed a file our patch touches. Run cd MinusPod && git diff > ../patches/minuspod-local.patch to regenerate after manually resolving. |
| Stuck on "Starting transcription" for a long time | Normal for 2+ hour episodes (many 5–10 min Whisper chunks). Check /tmp/minuspod.log for pass1:transcribing N/M or Chunk N complete. If the log stops mid-chunk for 15+ min, restart Whisper via the Services panel. start_services.sh now uses 5-min chunks and skips loudnorm preprocessing. |
| Transcription much slower than expected | You're probably on the Docker whisper image. Switch to the native binary via the Services panel (Metal on macOS, CPU/CUDA elsewhere). |
| One episode takes 30+ minutes | A 4-hour show = ~30 LLM windows. With qwen3.5:35b-a3b that's ~30 × 1.5 min = 45 min. Switch to qwen3:14b (echo 'OPENAI_MODEL=qwen3:14b' >> .env) — same 30 windows, ~3 × faster. |
| Mac kernel panics or hard freezes during a job | The default model is ~22 GB resident. Combined with Whisper Metal buffers (~2 GB), browser, IDE, etc. it can OOM the GPU on a 36 GB machine. The dashboard now shows a memory warning before each job; heed it, switch to qwen3:14b, or set OLLAMA_NUM_PARALLEL=1 (already the default). |
Whisper crash with kIOGPUCommandBufferCallbackErrorInnocentVictim |
Metal has a hard 8-command-buffer limit. The launcher now forces --processors 1 --threads ≤8; if you customised it, lower those numbers. |
Aborted on pass1:detecting:N/M |
Ad detection uses your configured LLM (Ollama or API), not Whisper. With local Ollama, large models (qwen3.5-addetect) can exceed 10 min per window — use OPENAI_MODEL=qwen3:14b on ≤36 GB Macs, or raise LLM_TIMEOUT_LOCAL. The stall watchdog restarts Ollama (not Whisper) for detecting stages and waits up to 45 min (EPISODE_STALL_THRESHOLD_LLM_SECONDS). With a cloud API, check rate limits and model availability. |
| Queue stalls on one episode forever | Wallclock cap 90 min (EPISODE_MAX_WALLCLOCK_SECONDS). Transcription stalls bounce whisper; LLM stalls bounce Ollama. See EPISODE_STALL_THRESHOLD_* above. |
| Ad still partially in outro | Increase TAIL_GAP_MIN_SECONDS (smaller threshold = more aggressive) or AD_END_PAD_TAIL. See patches/README.md. |
| Custom-file thumbnail stuck on the generic icon | Pocket Casts caches the colour fallback for ~1 minute after upload. The image does eventually render on every device — it's cosmetic only. |
| Service | File |
|---|---|
| MinusPod | /tmp/minuspod.log |
| whisper.cpp | /tmp/whisper-server.log |
| Pipeline UI | /tmp/pocketcasts-ui.log (and the floating log panel) |
| Ollama | ~/Library/Logs/Homebrew/ollama/ollama.log |
The Services panel can tail any of these inline (Log button per row).
source venv/bin/activate
python -m unittest tests -vThe suite covers artwork normalization, date validation, state management,
Patreon detection, transcript parsing, skip/stop semantics, upload ordering,
Up Next queue safety, Pocket Casts iOS-parity (hasCustomImage / colour),
RSS resolution, processed-podcast detection, the failed-episode abort path,
the services_manager helpers, and every /api/* endpoint.
PRs that make it more portable, improve ad-detection quality, or broaden platform support are welcome.
When you change anything in MinusPod/, regenerate the patch:
cd MinusPod
git diff > ../patches/minuspod-local.patchThis patch is automatically reapplied on every startup after a MinusPod upstream pull, so your local changes survive auto-updates. If a new upstream release conflicts with the patch, you'll see a warning in the startup log — resolve manually and regenerate.
MIT.
Built on top of:
- MinusPod by ttlequals0 — ad detection + audio processing engine.
- whisper.cpp by Georgi Gerganov — Metal-accelerated transcription.
- Ollama — optional local LLM inference (when
LLM_PROVIDER=ollama).
The unofficial Pocket Casts API client is reverse-engineered from public iOS client traffic; use accordingly.