MinusPod learns a sponsor from the first correction you make
MinusPod is a self-hosted server that removes ads before you ever hit play
At a glance
- What is it?
- A self-hosted server that transcribes podcast episodes with Whisper, has an LLM find the ad segments, and cuts them with FFmpeg. The interesting part is that your rejections become patterns, so the same sponsor is caught next time without another model call.
- Who is it for?
- Adopt MinusPod if you listen to a fixed set of feeds, will host it, and want ad removal that improves as you correct it rather than one you re-run from scratch. Do not adopt it if you need it to work without a language model and a Whisper backend, since detection is an LLM step and transcription needs either a GPU or a remote backend.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 3 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 8, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Whisper, then a model, then FFmpeg
The pipeline is four components and it is described without hedging. Whisper transcribes the episode. A language model finds the ad segments in that transcript. FFmpeg cuts them out of the audio. A Flask application serves the modified feed and the modified files.
Detection happens in passes rather than in one shot. A first pass runs the model over sliding windows of the transcript, and then a verification pass runs automatically against the re-cut audio, which is a check the model could not have done from the transcript alone. An optional ad-reviewer stage then confirms, adjusts or rejects each cut, and it can resurrect a borderline detection that the earlier passes had dropped.
Audio-side signals sit alongside the transcript work: loudness analysis, detection of dynamic audio insertion transitions, pre-roll and post-roll handling, and a voice-activity gap detector aimed at the spans where Whisper drops speech. There is also per-feed audio cue detection, which snaps a cut to a show's own jingle or stinger.
Bring your own model. Anthropic, OpenRouter, Ollama or any OpenAI-compatible endpoint, switchable at runtime, with per-stage tuning of model, temperature, token budget and reasoning, and prompts you can edit per pass.
The transcription side has the same shape: local Whisper on GPU or CPU through faster-whisper, or a remote OpenAI-compatible API, with whisper.cpp, Groq, OpenAI Whisper and OpenVINO on Intel hardware named as backends.
A correction becomes a pattern at three scopes
The feature that separates this from a one-shot ad remover is that your corrections compound.
When you reject or adjust a cut, that feedback becomes a pattern, and patterns are learned across episodes rather than within one. Scope matters, and there are three levels: the individual podcast, the wider network, and global. A sponsor that only ever appears on one show is caught at podcast scope; an agency that runs ads across several shows is caught higher up.
The mechanism is described as catching repeat sponsors without asking the model again, which is the economic argument. Without pattern learning, every episode pays for a fresh set of model calls over the same sponsor read. With it, the second and third appearance is a lookup.
There is a sponsor list alongside the patterns, holding aliases and normalizations so that the same advertiser named three different ways resolves to one entry.
Community pattern sync is opt-in, and the contribution path back is unusually specific: one pull request. So the shared pattern set is a repository of text rather than a live service, which means it can be reviewed, forked or ignored without trusting a server.
Every marker carries a category and a disposition
Ad removal is not binary, and the marker model says so. Every marker carries a segment category, and there are five: sponsor, cross-promo, self-promo, interaction, and an opt-in intro, outro or recap category.
Each category resolves to one of three dispositions: remove, beep, or keep. Removal is silent cutting. A beep leaves the audio in place with a tone over it, which is useful when you want to hear that something was there. Keep leaves the segment alone.
Those dispositions are configurable globally or per feed, and the default is remove until you configure something. That default is the right one for a tool whose whole purpose is cutting ads, but it means an interaction prompt or a show's own recap is cut on the first run unless you say otherwise. That is what the opt-in intro, outro and recap category is for.
What plays in the gap is configurable too. The Compose file shows an optional asset mount for your own replacement audio file, the sound played where the ads were, and notes that uploading one through Settings writes it into the data directory instead, which takes precedence and survives a rebuild.
Rejected cuts stay visible for auditing
Confidence scoring with a review queue is the mechanism behind that opt-in step, and the queue behaves in a way worth copying.
A detection gets a score, low-scoring ones go to a queue, and a human decides. What is unusual is what happens to the decisions you make: rejected detections stay visible for auditing rather than disappearing. So when a later episode cuts something you did not want cut, the history of what was proposed and what you said about it is still there.
The user interface follows from that. There is a web UI with a waveform-based ad editor, alongside views for feeds, episodes, patterns, sponsors, history and stats. A waveform editor is the right shape for this problem: the unit of judgement is a span of audio, and reading a start and end timestamp is not the same as hearing it.
Cost is tracked in the same view. The stats page covers ads cut, time saved, token usage and spend, which is the number to watch if you are on a paid provider, since detection is a per-episode model call and the whole value proposition of pattern learning is that those calls get cheaper over time.
Retention is also configurable, globally or per feed, with a never-delete archive mode if you would rather MinusPod keep everything.
Without the master passphrase, keys sit in plaintext
The security posture is stated in the quick start rather than buried, and it is the first thing to read.
Provider keys are encrypted with AES-256-GCM behind a master passphrase, and Compose starts without one. Without it, provider API keys go into the database as plaintext. For a home setup that is a modest risk; for anyone exposing the server it is not.
The quick start is an environment file and one command:
# 1. Create environment file
cat > .env << EOF
ANTHROPIC_API_KEY=your-key-here
BASE_URL=http://localhost:8000
MINUSPOD_MASTER_PASSPHRASE=long-random-string-you-will-not-lose
OPENAI_MODEL=claude-haiku-4-5
EOF
# 2. Create data directory
mkdir -p data
# 3. Run
docker-compose up -dSetting the passphrase later is supported and does not require a reinstall. On the next boot, existing plaintext rows are migrated to encrypted storage marked with an `enc:v1:` prefix, and a SQLite snapshot is taken into `data/backups/` before the migration runs. The snapshot is mandatory, which is the right default for an in-place rewrite of your key store.
The catch is on the other side. An encrypted database download can only be restored with the same passphrase that created it, so the value has to be long, random and stored somewhere other than next to the database. Backups are listed among the operational features alongside webhooks and a full REST API with an OpenAPI document, and the repository carries `openapi.yaml` at the root.
Configuration itself has a precedence rule worth knowing: environment variables seed settings that are unset, and stored values win once they exist. So an env var will not override something you changed in the interface.
Compose blocks anonymous requests that would spend your tokens
A podcast feed reader is an unauthenticated client by nature, and this is the part of MinusPod that has to reconcile that with a paid API behind it.
Compose requires an application password before it serves the normal API. Set the first one from the same host. For remote setup there is a temporary token: put `MINUSPOD_SETUP_TOKEN` in the environment file, send it in the `X-MinusPod-Setup-Token` header when setting that first password, then remove it.
The second guard is the one that protects your provider bill. Compose prevents unauthenticated feed requests from starting paid processing. If you want just-in-time processing of a feed without credentials, you have to say so, either by enabling Authenticated Feeds or by setting `MINUSPOD_ALLOW_PUBLIC_PROCESSING=true`.
That is a good default because the failure mode is invisible otherwise: an unguarded feed request is an LLM call, and a podcast app polling a new feed can make several before you have looked at anything.
Authentication itself is a single shared password rather than per-user accounts, which suits a household server. For anything public the deployment guidance is a reverse proxy or a Cloudflare tunnel in front.
latest moves several times a day, stable does not
Release cadence is unusually high, and the project tells you how to opt out of it. The three most recent tags are v2.97.15, v2.97.38 and v2.97.45, the last from 2026-10-01.
The default tags, `:latest` for the GPU image and `:cpu` for the CPU one, follow every release, and the README says plainly that several can land in a day. The Compose file reads that as a variable, `MINUSPOD_VERSION:-latest`, so pinning is a one-line change.
For a slower track you set `MINUSPOD_VERSION=stable` or `MINUSPOD_VERSION=stable-cpu`, which resolves to the `stable` and `stable-cpu` tags. Those only move to releases that have soaked in production, and the releases page carries curated notes per version.
There is a third Compose file as well, `docker-compose.whisper.yml`, alongside the main one and `docker-compose.cpu.yml`.
The CPU path is not an afterthought: the image is multi-arch and runs natively on both amd64 and arm64, so a machine without an NVIDIA GPU can still run the full server by offloading transcription to a remote API. Memory and VRAM tables are kept in the installation document rather than in the README.
CUDA arrives as pip wheels, not as a CUDA base image
The Dockerfile explains its own architecture, and the two choices are both deliberate.
The frontend is built in a first stage on an Alpine Node image, then the application stage is a plain Ubuntu image with no NVIDIA or CUDA base at all. The reason is in the comment: ctranslate2 statically links the CUDA runtime, and cuDNN and cuBLAS come from the pip `nvidia-*` wheels that arrive as dependencies of torch, made visible through `LD_LIBRARY_PATH`. GPU access is injected by the NVIDIA container runtime using the `NVIDIA_*` environment variables.
The consequence is that the runtime image is a normal Ubuntu image. It does not carry a CUDA toolchain, and it is not tied to a driver-matched CUDA release the way a CUDA base image is.
Python 3.12 is installed from the deadsnakes PPA rather than taken from the base, because Ubuntu 26.04 ships 3.14 and the project needs 3.12: the comment names the reason as the numpy pin, since wheels for numpy below 2.0 stop at cp312.
Two smaller decisions are worth noting. The Swagger UI assets are copied into the built static directory so the docs route serves them locally instead of pulling from a third-party CDN. And the container runs as a non-root `minuspod` user at UID and GID 1000, with the entrypoint using setpriv to drop root after the chown step that migrates the data volume on first boot.
Editorial conclusion
Adopt MinusPod if you listen to a fixed set of feeds, will host it, and want ad removal that improves as you correct it rather than one you re-run from scratch. Do not adopt it if you need it to work without a language model and a Whisper backend, since detection is an LLM step and transcription needs either a GPU or a remote backend. Verify two things first. Set MINUSPOD_MASTER_PASSPHRASE before the first real run, because without it your provider keys sit in the database as plaintext. Then decide between the floating tags and the stable ones, since several releases can land in a day on the default track.
Frequently asked questions
How does MinusPod remove ads from podcasts?
Whisper transcribes the episode, a language model finds the ad segments in the transcript, and FFmpeg cuts them out. A verification pass then runs against the re-cut audio, and an optional reviewer stage can confirm, adjust, reject or resurrect each cut.
Which language models can MinusPod use?
You bring your own: Anthropic, OpenRouter, Ollama, or any OpenAI-compatible endpoint, switchable at runtime. Model, temperature, token budget and reasoning are tuned per stage, and prompts can be edited with per-pass overrides. No model ships by default.
Do I need a GPU to run MinusPod?
Not for the server itself. Docker with NVIDIA GPU support is only needed for local Whisper. Without a GPU you can pull the CPU variant, which is multi-arch and runs natively on amd64 and arm64, and offload transcription to a remote backend such as whisper.cpp, Groq, OpenAI Whisper or OpenVINO.
Are my provider API keys stored in plaintext?
They are unless you set MINUSPOD_MASTER_PASSPHRASE, in which case they are encrypted with AES-256-GCM. Compose starts without a passphrase, and setting one later migrates existing plaintext rows on the next boot after taking a mandatory SQLite snapshot into data/backups/.
How do I pin a MinusPod version?
Set MINUSPOD_VERSION=stable for the GPU image or stable-cpu for the CPU one, which resolves to the tags that only move to releases that have soaked in production. The default latest and cpu tags follow every release, and several can land in a day.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ttlequals0-minuspod)