Finds episodes and films that arrived with an audio description track — the narrator who describes what is happening on screen — and replaces them.
Some releases carry the visually-impaired narration mix instead of the normal soundtrack. You do not find out until you sit down to watch, and by then the "good" copy has been deleted as a duplicate.
Nothing in a normal *arr setup can catch this:
- The release title need not say so. Many are named identically to a clean release, so no regex, quality profile or release filter can see it.
- The file metadata need not say so either. A real described file measured
during development had a single AAC track with
visual_impaired=0and no title tag. Everything Sonarr can inspect looked perfect. - The narration is mixed into the only audio track, so there is no clean track to switch to. The file has to be replaced, not repaired.
Only the audio itself knows. So Voice Ovarr listens.
Subtitles transcribe the programme's dialogue. Audio description is inserted exactly where that dialogue is not, and is never itself subtitled. That gives a free, precise map of where narration would have to live.
Voice Ovarr pulls out the dialogue-free gaps, transcribes only those, and applies two tests. Both must pass:
| Gate | Question | Clean | Narrated |
|---|---|---|---|
| Gap speech | Is anyone talking where nobody should be? | 0–2 words/min | 125–143 words/min |
| Narration | Is what they say description, not conversation? | 0.00 | 0.29–0.48 |
The second gate is the one that makes this safe to act on. Gap speech alone false-positives on unscripted television whose subtitles miss dialogue — a cooking show scored 85.8 words/min, and a children's show scored 127, higher than genuinely narrated episodes. Both scored 0.00 on narration, because contestants and puppets speak in the first and second person and ask questions. A describer never does.
About one file in six has no subtitle track at all. Voice Ovarr handles those three ways, cheapest first:
- Sidecar subtitles next to the file, if present.
- Any text track — and it picks the fullest one, not the first. Many releases put a tiny forced/signs-only track first; taking that one makes a file look like it has no subtitles at all.
- Bitmap subtitles (PGS/VobSub) with no OCR. Voice Ovarr never reads subtitle text, only when dialogue starts and stops, so a picture-based track works just as well: in PGS a cue is a large "display" packet followed by a tiny "clear" packet, which gives the interval exactly.
If there is genuinely no subtitle track, it falls back to sampling the whole soundtrack and counting narration lines per minute — an absolute rate rather than a ratio, so a talkative show does not dilute it. Reference: narrated 0.60–1.87/min, clean 0.00–0.20/min. This is weaker evidence, so such detections are flagged "needs a human" and are never deleted automatically.
Files that cannot be judged are reported as "could not check". They are never quietly counted as clean.
Deletes the file, blocklists the release so the identical copy cannot be grabbed back, and lets Sonarr/Radarr search for a replacement.
Order matters: the file is deleted before the release is failed. The other way round, the *arr still sees a satisfied episode and never searches for a replacement.
ACTION_MODE defaults to manual — it finds and reports, and you press the button. Switch it to auto once it has proved itself on your own library.
git clone https://github.com/katalyst88/voice-ovarr
cd voice-ovarr
docker compose up -d --buildThere is no prebuilt image yet, so it builds from source. First build takes a few minutes: it installs ffmpeg and bakes the speech model into the image so the container needs no network access afterwards.
services:
voice-ovarr:
build: .
container_name: voice-ovarr
restart: unless-stopped
ports: ["8099:8099"]
environment:
SONARR_URL: "http://sonarr:8989"
SONARR_API_KEY: "your-key"
RADARR_URL: "http://radarr:7878"
RADARR_API_KEY: "your-key"
PATH_MAPS: "/data/media=>/media"
ACTION_MODE: "manual"
volumes:
- ./config:/config
- /path/to/your/media:/media:roThen open http://localhost:8099.
| Variable | Default | What it does |
|---|---|---|
SONARR_URL / SONARR_API_KEY |
— | Your Sonarr. Omit for a film-only setup. |
RADARR_URL / RADARR_API_KEY |
— | Your Radarr. Omit for a TV-only setup. |
PATH_MAPS |
— | arr path=>container path, semicolon separated. Fixes "file not found". |
VOICEOVARR_PIN |
unset | Unset means no login. Set it if this is reachable from the internet — the UI can delete media. |
ACTION_MODE |
manual |
manual reports and waits for you; auto deletes the file and blocklists the release. |
MAX_ACTIONS_PER_RUN |
6 |
Cap on deletions per run. Anything beyond is reported instead. |
WPM_GATE |
40 |
Gap-speech threshold. |
NARR_GATE |
0.15 |
Narration threshold. The safety catch. |
NO_SUBS_RATE_GATE |
0.5 |
Fallback threshold for files with no subtitles. |
MAX_GAP_AUDIO |
300 |
Seconds of audio transcribed per file (~40s of CPU). |
SCHEDULE_ENABLED |
true |
Run daily over recent imports. |
SCAN_CRON_HOUR / SCAN_CRON_MINUTE |
3 / 45 |
When. |
WHISPER_MODEL |
base.en |
Speech model. small.en is slower and more accurate. |
The most common problem. Sonarr reports a file as
/data/media/TV/Show/Season 01/ep.mkv; inside this container it might be
/media/TV/Show/Season 01/ep.mkv. Then:
PATH_MAPS=/data/media=>/media
Windows-style *arr paths work too: PATH_MAPS=\\nas\public=>/media.
It transcribes about five minutes of audio per episode, not the whole file, and
speech recognition runs locally (faster-whisper, baked into the image) — no
API, no account, nothing leaves your network.
The cost is dominated by reading the file, not by listening to it. Extracting the subtitle timings and slicing the audio both mean pulling the file through ffmpeg, so where your media lives matters far more than your CPU:
| Media location | Measured, per episode |
|---|---|
| Local disk | ~1 minute |
| Network share (SMB/NFS), via Docker | ~4 to 5 minutes |
Plan the library sweep around your own number rather than an advertised one — start it, watch the History tab, and set the batch size from what you see.
Recorded so nobody repeats them:
ffprobedisposition flags (visual_impaired) or track titles — clean on genuinely described files.- Release-title matching — the title carries no marker.
- Silence or loudness density — a described episode measured 14.0% quiet against 4.2% for a clean one. Backwards, because clean episodes run near-continuous score. Loudness cannot separate narration from music.
- Subtitle-gap energy with no transcription (a fully signal-processing approach, no ML). Tested across 6 described and 7 clean files: described scored 0.560–0.891, clean scored 0.485–0.613. Overlapping — no threshold exists. Narration and dialogue are both "a person talking in a quiet mix"; only the words tell them apart.
- A couple of fixed sample windows — caught one described episode and missed two others in the same release. Aim at the gaps instead.
MIT.