Minus is an open-source HDMI box that sits between your streaming device and your TV. It watches the video feed, detects ads with ML models running locally on its own NPUs, then mutes and covers them until your show comes back. Instead of the ad you get something useful, like a Spanish vocabulary card.
Nothing leaves your living room. There is no cloud service, no subscription, and no account. The OCR, the vision model, and the speech recognition all run on the device. The one exception is something you have to switch on yourself: you can choose to share anonymous snapshots of blocked ads to help train the next model. It's off by default; see docs/PRIVACY.md.
An ad break as seen through Minus. The ad is muted and covered by a vocabulary card, with a small greyscale preview in the corner so you can tell when it's over.
How it works
Minus is a 4K@60fps HDMI passthrough. Your Roku, Fire TV, or Google TV plugs into Minus, and Minus plugs into your TV. Three detectors watch the stream and vote on whether an ad is playing:
┌──────────────┐ ┌────────────────────┐ ┌─────────────────────┐
│ HDMI IN │────▶│ ustreamer │────▶│ HDMI OUT to TV │
│ (Roku, etc.) │ │ (MPP hw encoding) │ │ (DRM/KMS, 60fps) │
└──────────────┘ └────────┬───────────┘ └─────────────────────┘
│ snapshots + audio
┌───────────────┼────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌──────────────┐ ┌──────────────┐
│ PaddleOCR │ │ minus-v0.1 │ │ Moonshine │
│ RK3588 NPU │ │ (VLM) │ │ ASR (CPU) │
│ reads ad UI │ │ Axera NPU │ │ hears ad │
│ text ~200ms │ │ ~370ms │ │ language │
└────────────────┘ └──────────────┘ └──────────────┘
└───────────────┼────────────────┘
▼
blocking decision engine
(mute + overlay, typically < 2s to react)
- OCR (PaddleOCR PP-OCRv6 on the RK3588's NPU, ~200ms a frame) reads on-screen ad UI like "Skip in 5", "Ad 2 of 3", and countdown timers, including the character misreads OCR makes on real TVs. When an ad shows its own countdown, Minus reads the number and counts it down in real time, so it knows the ad is still running even while OCR briefly loses the text. That clock only ever helps hold a block that the detectors already started. On its own it never starts one.
- minus-v0.1, a 450M-parameter vision-language model fine-tuned for this exact job, looks at the raw frame and answers "is this an advertisement?" in about 370ms on an Axera AX8850 NPU. It needs no text on screen to work.
- ASR (Moonshine tiny-en on CPU) transcribes the audio and listens for marketing language as a confirmation signal.
When the votes say "ad", audio mutes immediately and the screen switches to an overlay rendered at 60fps inside the hardware JPEG encoder (no desktop stack, no X11). When the ad ends the show comes back after a few seconds, about 5 in practice. Minus waits for several clean frames and a short floor before it lets go, because OCR routinely loses an ad's text for a few seconds mid-ad, and releasing on the first gap would flash the ad back on screen.
The model
minus-v0.1 is published on Hugging Face: TheGarageDev/Minus-v0.1.
It's a fine-tune of LiquidAI's LFM2.5-VL-450M trained on frames captured from real TV streams and compiled into fused-layer models for the Axera NPU. Inference is prefill-only: the yes/no answer is read straight from the logits without generating any tokens, so latency is deterministic.
| Metric | Value |
|---|---|
| Accuracy (800-image holdout) | 97.0% |
| Ad recall | 94.8% |
| Non-ad recall (false-positive resistance) | 99.2% |
| Inference latency | ~0.37s per frame |
| Parameters | 450M |
The same model also classifies screen state (playing / paused / menu / dialog / screensaver) for autonomous mode.
What ships where
The two models are distributed differently, which trips people up when setting up a new board:
| Model | Runs on | Where it comes from |
|---|---|---|
| PaddleOCR PP-OCRv6 detection + recognition (default), plus PP-OCRv3 as a rollback | RK3588 NPU | In this repository, models/paddleocr/. ~17 MB for both generations, nothing to download. |
| minus-v0.1 (450M VLM) | Axera AX-M1 | Hugging Face, the axera/ folder (~740 MB). install.sh downloads it into /home/radxa/axera_models/minus-v0.1/; which model and where are set in models/vlm_model.json |
OCR needs three files per generation, all present in the repo: a detection model that finds text regions, a recognition model that reads each region, and the character dictionary the output is decoded through. Each generation has its own dictionary, and the two are not interchangeable. MINUS_OCR_MODEL_VERSION picks one: v6 (the default) or v3. The loader keeps each model paired with its own dictionary and thresholds, so they can't be mixed up. v6 was chosen on latency: 183ms p50 and 274ms p90 in production, and a tighter tail that keeps text-heavy ad frames under the hard timeout. A rotated-text classifier is also included, but Minus doesn't load it, since TV overlay text is never upside down. models/paddleocr/README.md has the details and checksums.
Features
- Real-time ad blocking via OCR + VLM + ASR triangulation. The decision engine handles the ugly cases: flicker between ads, paused screens that look like ads, black transition frames, frozen streams.
- Reads an ad's own countdown ("Skip in 12", "Ad 0:15") and counts it down to know how much of the ad is left. It's an extra signal on top of the detectors, never a trigger by itself. It lets go if the countdown freezes or the audio goes silent, which is what a paused video looks like.
- Ads get replaced, not just blacked out. Choose Spanish vocabulary cards (750+ entries), trivia facts, or a photo screensaver of your own pictures.
- 4K@60fps passthrough with a zero-copy VPU/RGA pipeline in a patched ustreamer, around 5% CPU.
- Audio mutes the instant a block starts, with watchdogs that recover the pipeline if anything wedges, including re-initialising the HDMI output when the TV comes back so sound isn't left silent.
- Thermal-adaptive: if the SoC holds at 83 °C, Minus caps the display at a steady 30fps and makes blocking stickier until it cools below 75 °C, rather than letting the frame rate collapse under throttling.
- Web UI with the live feed, pause controls, detection history, screenshot review, and settings, usable from any device on your network.
- Remote control integration for Fire TV (ADB), Roku (ECP), and Google TV (ADB), including pressing "Skip Ad" for you.
- Autonomous mode keeps YouTube playing unattended to collect training data, midnight to 9am by default, using the VLM to navigate. It stands down the moment the TV is turned on so it never fights you for the remote, and if the streaming stick has put itself to sleep it presses Home to wake it. A music-video mode steers toward music, which carries far more ads per hour.
- Odds and ends: IR blaster for HDMI switches, WS2812B status LED strip, WiFi captive portal for first-time setup, HDCP 1.4 capture support.
Web UI
| Desktop | Mobile |
|---|---|
![]() |
![]() |
The web UI is served by the device itself (Flask). Live feed, block/pause controls, per-detection history, a swipe-based screenshot review flow for cleaning up training data, device remotes, and every setting. A status row shows OCR, VLM and ASR state alongside CPU, memory, temperature, disk and HDMI signal, and turns amber or red as things get tight. No app to install.
Hardware
Roughly $350-$450 in parts. Full bill of materials, part links, and sizing notes: docs/HARDWARE.md.
| Component | Notes |
|---|---|
| Radxa ROCK 5B+ or ROCK 5B, 8 GB RAM recommended (4 GB minimum) | RK3588 SoC. The 4K@60 HDMI input is the part that matters: boards without one can't do passthrough. Also provides the NPU for OCR and the VPU for 4K60 JPEG encoding. |
| Radxa AICore AX-M1 (Axera AX8850, M.2 2280 M-key, 24 TOPS, 8 GB) | Runs minus-v0.1 at ~370ms per inference |
| Radxa Heatsink 6240B, heatsink and fan (docs) | Required. This is Radxa's cooler for the ROCK 5B / 5B+ / 5T. For the 5B+, get V2.0: V1.0 has fins that hit the 5B+. 4K60 passthrough runs around 80 °C with the fan going; past 83 °C Minus drops to 30fps until it cools. Passive cooling alone will throttle. |
| Radxa Power PD 30W, or any 30 W+ USB-C PD supply | Required. Radxa's own supply is the one they test the ROCK 5B against. An underpowered supply (a 5 V/2 A phone charger, say) causes random pipeline restarts. |
| 2x certified High Speed HDMI cables, 18 Gbps | Source → Minus → TV. Keep them short; marginal cables show up as "No Signal" at 4K60. |
| Optional: 64 GB+ microSD card (A2-rated) | Holds the OS if your board has no onboard eMMC. The ROCK 5B+ can be ordered with eMMC built in; the ROCK 5B boots from microSD or an eMMC module. |
| Optional: IR LED (940 nm) on GPIO | Controls an HDMI switch for multi-device setups, see docs/IR_TRANSMITTER.md |
| Optional: WS2812B 8-LED strip | Status indicator (idle / blocking / error / ...), see docs/STATUS_LEDS.md |
| Optional: HDCP 1.4 sink key | For capturing HDCP-protected sources, see hdcp/ |
Operating system: Debian 12 (bookworm) on Radxa's BSP 6.1 kernel, which is what their official ROCK 5B/5B+ images ship. This is not interchangeable with a mainline kernel: the HDMI receiver (rk_hdmirx), the Rockchip MPP encoder, RGA, and the RKNN NPU runtime all live in Rockchip's 6.1 tree. On a mainline kernel the board boots fine and /dev/video0 simply never appears. Either the KDE or the CLI image works; there is no display server or desktop environment involved at runtime, since Minus talks straight to DRM/KMS and the hardware encoders.
Getting started
git clone https://github.com/garagehq/Minus-streaming-stick.git /home/radxa/Minus cd /home/radxa/Minus # Install system + Python dependencies (GStreamer, fonts, rknnlite, axengine, ...) # then build the patched ustreamer: git clone https://github.com/garagehq/ustreamer.git /home/radxa/ustreamer-garagehq cd /home/radxa/ustreamer-garagehq && make WITH_MPP=1 cp ustreamer /home/radxa/ustreamer-patched # Install as a service that starts on boot. This also downloads the vision # model (~740 MB from Hugging Face; the OCR models already ship in this repo): cd /home/radxa/Minus && sudo ./install.sh # ...or run directly (fetch the model first if install.sh hasn't): sudo python3 tools/minus_update.py --install-model sudo python3 minus.py
Staying up to date
Settings → Minus Updates in the web UI shows whether this box is current and updates it with one button:
- Minus — on a published release, compared with the latest
GitHub release;
running from source, compared with the latest commit on your branch (normally
main). - ustreamer — the
/home/radxa/ustreamer-garagehqcheckout against its branch; rebuilt when it changes. - Vision model — the files in
/home/radxa/axera_models/against the model the code asks for on Hugging Face (models/vlm_model.json). A new model (for example Minus v0.2) arrives the same way: after the code update, the new model is downloaded, checked and swapped in.
Update now pulls what's new (fast-forward only — it never overwrites local
changes; a branch with local commits or no upstream is reported but left alone),
rebuilds ustreamer if needed, downloads the model if needed, and restarts Minus,
about a minute without picture. Minus has to come back up within four minutes
and stay up (30 seconds on the same process, so a crash loop doesn't pass), and
if the vision model was loaded before the update it has to load again; otherwise
the code, ustreamer and model are all rolled back and it restarts on the previous
version. Only one update can run at a time, and an update that was cut off (power
cut) shows as interrupted instead of blocking the button. The previous model is kept next to the new one as
<model dir>.prev. A check runs five minutes after startup and at most hourly
after that while the page is open; nothing updates without the button. From a
shell: sudo python3 tools/minus_update.py does the same thing.
Releases are cut by hand with tools/release.sh — see
docs/RELEASING.md. A box on a release is only ever moved to a
newer release; a box running from source follows its branch.
Automatic security updates
install.sh installs Debian's unattended-upgrades, and Minus turns it on by
default (Settings → System Updates in the web UI toggles it, or set
"unattended_upgrades": false in ~/.minus_system_settings.json). Only
bookworm-security fixes are applied, it never reboots by itself, and the
packages the video path depends on (kernel, u-boot, Rockchip MPP/RGA,
GStreamer, Mesa, RKNN, Tailscale) are blacklisted so a routine run can't swap
a driver the pipeline was validated against — update those deliberately with
apt when you choose to. Policy lives in
/etc/apt/apt.conf.d/52minus-unattended-upgrades (written by the service);
history is in /var/log/unattended-upgrades/. If the web UI says the
package is missing: sudo apt-get install unattended-upgrades.
The full dependency list and deployment walkthrough are in docs/DEPLOYMENT.md. Model paths, the OCR generation and detection thresholds are set with environment variables (MINUS_VLM_MODEL_DIR, MINUS_OCR_MODEL_DIR, MINUS_OCR_MODEL_VERSION, etc.); CLAUDE.md documents all of them.
Minus auto-detects the connected HDMI output, resolution, DRM plane, and audio device at startup, and works with both 4K and 1080p displays.
Documentation
| Document | Description |
|---|---|
| docs/HARDWARE.md | Bill of materials, OS and kernel requirements |
| docs/FEATURES.md | Complete feature list |
| docs/ARCHITECTURE.md | System architecture and data flow |
| docs/DEPLOYMENT.md | Setting up a new device from scratch |
| docs/API.md | Web UI REST API |
| docs/PRIVACY.md | What stays on the box, and the opt-in anonymous ad-snapshot contribution |
| docs/RELEASING.md | Cutting a release, shipping a new vision model |
| docs/ASR.md | Audio-based ad confirmation |
| docs/AUDIO.md | Audio passthrough pipeline |
| docs/AESTHETICS.md | Visual design guide |
| docs/IR_TRANSMITTER.md | IR blaster for HDMI switches |
| docs/STATUS_LEDS.md | WS2812B status strip |
| hdcp/README.md | HDCP 1.4 setup for the HDMI input |
| CLAUDE.md | Engineering notes: detection tuning and the full bug history |
Related projects
- Minus-chrome-extension: the same idea for your browser, as a Chrome extension.
- garagehq/ustreamer: our ustreamer fork with RK3588 MPP hardware encoding, NV12/NV24/BGR24 capture, and the 60fps blocking-overlay compositor.
- TheGarageDev/Minus-v0.1: the model, on Hugging Face.
Support
Minus is free, open-source, and runs entirely on your own hardware. No ads, no tracking, no server bills passed on to you. If it has saved you from a few unskippable ad breaks, you can buy me a coffee ☕:
License
- Code: GPL-3.0
- Documentation: CC BY-SA 4.0
- Hardware designs: CERN-OHL-S-2.0




