High-throughput batch transcription and subtitle generation for large media libraries, built on faster-whisper.
This project is designed for people processing more than a handful of files at a time: channel archives, lecture folders, interview dumps, podcast backlogs, surveillance exports, and other media collections where throughput, resumability, and low babysitting matter.
Instead of treating transcription as a one-off script, Whisper Batch Subtitles treats it as a pipeline:
- discover media recursively
- extract and cache audio
- transcribe with hardware-aware defaults
- optionally translate into one or more languages
- write subtitle outputs
- resume safely after interruption
- Why This Project
- Features
- Status
- Requirements
- Installation
- Quick Start
- Example
- Configuration
- Multi-Track Workflows
- Diarization
- Runtime State
- Project Direction
- Development
- Contributing
- Security
- License
Most Whisper batch scripts work well for a quick test, then start to break down when the workload gets large.
This project aims to be more practical for real libraries:
- resumable runs with durable SQLite job state
- persistent caches so work is not repeated unnecessarily
- parallel pipeline stages for better hardware usage
- automatic hardware inspection and runtime recommendations
- CLI, config file, and environment-variable based control
- logs and progress reporting that are useful during long runs
- Recursive media scanning, with configurable scan order (newest/oldest/largest/smallest first)
- Subtitle generation in
.srtformat, with optional line wrapping, punctuation/whitespace cleanup, and repeated-segment (hallucination-loop) suppression - Optional translation output such as
.en.srt,.es.srt,.ja.srt, via a pluggable backend (Google by default, DeepL optional) - Faster-Whisper backend, with real multi-GPU routing (each transcription worker pinned to its own CUDA device)
- FFmpeg-based audio extraction
- Hardware-aware defaults for CPU/GPU systems, plus named presets (
fastest,balanced,archive-quality,low-vram,cpu-only) and an interactive setup wizard - Reusable job profiles (
--profile/--save-profile) for switching between named configurations - Watch/daemon mode for continuously-monitored folders
- Persistent audio, transcript, and translation caches
- Retry handling with type-aware classification (a corrupt file fails fast instead of wasting retries) and failure tracking
- Content-signature duplicate detection across differently-named/located copies of the same file
- YAML config support, with config file, named profile, and environment-variable layering
- Structured logs, periodic throughput reporting, an optional self-updating live status line, and a real multi-panel terminal (TUI) dashboard
- A built-in benchmarking mode that measures real transcription throughput on your hardware
- Dynamic auto-scaling of extraction (ffmpeg) workers based on observed queue backlog
- A read-only local web dashboard for checking pipeline status from a browser
- Multi-track audio inspection and per-directory role profiles
- Speaker-labeled merged transcripts when separate voice tracks are available
- Optional single-track diarization via
pyannote.audio - Optional split-environment diarization via subprocess + JSON handoff
- A Dockerfile for containerized deployment (CPU by default, adaptable for GPU)
The project is currently at v1.3.
It already includes the core pipeline architecture, resumability, caching, hardware detection with real multi-GPU routing and empirical benchmarking, queue-based orchestration with dynamic extraction-worker scaling, multi-track role handling, speaker-aware subtitle output, presets/wizard/profiles, watch mode, pluggable translation, duplicate detection, a real TUI dashboard, a read-only web dashboard, and a Dockerfile. Bigger roadmap items like distributed/multi-machine workers, a standalone executable, and a full job-submission API are still future work — see Project Direction below and CHANGELOG.md for what's shipped so far.
- Python 3.10+
ffmpegffprobe
You will also want a CUDA-capable GPU for the best throughput, but CPU-only operation is supported.
Developed and tested primarily on Windows; CI runs the full test suite on Linux (Python 3.10–3.13) on every
push and pull request, so the core pipeline is portable. There's nothing OS-specific in the codebase
outside of one Windows-specific RAM-detection code path in hardware.py (with a sysconf-based fallback
for other platforms).
Install the main transcription environment:
pip install -e .If you want everything in one environment, you can also install the optional diarization dependency:
pip install -e .[diarization]To use the DeepL translation backend instead of the default Google backend, install its extra too:
pip install -e .[deepl]For the --tui live terminal dashboard, install the tui extra:
pip install -e .[tui]If you are not installing as a package yet, the module entrypoint still works from the repo root.
A Dockerfile is also provided at the repo root for containerized runs (docker build -t wbs .); see the
comments in it for adapting to a GPU/CUDA base image. It ships with sensible defaults but hasn't been
build-tested in every environment, so treat it as a solid starting point to verify on your own setup.
For many Windows CUDA setups, the cleanest architecture is to keep transcription and diarization in separate environments:
- Main runtime environment
For
faster-whisper, FFmpeg extraction, batching, translation, and the normal pipeline - Diarization environment
For
pyannote.audio, newer Torch builds, and speaker models
Example:
# main environment
pip install -e .
# separate diarization environment
py -3.13 -m venv .venv-diarization
.venv-diarization\Scripts\activate
pip install pyannote.audio
pip install -e .Then point the main config at that diarization Python:
diarization_mode: auto
diarization_external_python: .venv-diarization\Scripts\python.exeWith that setup, the main pipeline hands cached audio to the diarization helper through a subprocess and reads back JSON speaker turns. This avoids Torch version conflicts between the transcription stack and the diarization stack.
Inspect the local machine and see the recommended runtime settings:
python -m whisper_batch_subtitles hardwareGenerate a starter config tuned to the current machine:
python -m whisper_batch_subtitles init-configOr answer a few questions interactively and let the wizard write the config for you:
python -m whisper_batch_subtitles wizardPrefer a one-shot named preset instead of the interactive wizard?
python -m whisper_batch_subtitles init-config --preset archive-qualityPresets: fastest, balanced, archive-quality, low-vram, cpu-only. You can also pass --preset
directly to run without writing a config file first.
Inspect a recording's audio streams and see the guessed or saved role mapping:
python -m whisper_batch_subtitles inspect-tracks process\example.mkvRun the pipeline against a media folder:
python -m whisper_batch_subtitles run --root-dir processThe legacy script entrypoint still works:
python whispertranscribetranslate.py --root-dir processSave a tuned run as a reusable named profile, then reuse it later:
python -m whisper_batch_subtitles run --root-dir process --preset fastest --save-profile podcast
python -m whisper_batch_subtitles run --root-dir process --profile podcastKeep watching a folder and pick up new files automatically instead of a one-shot scan:
python -m whisper_batch_subtitles run --root-dir process --watch --watch-interval-seconds 300Measure actual transcription throughput on your hardware instead of trusting static heuristics:
python -m whisper_batch_subtitles hardware --benchmarkWatch a live terminal dashboard instead of scrolling log lines (needs pip install -e .[tui]):
python -m whisper_batch_subtitles run --root-dir process --tuiLet the number of extraction workers grow and shrink automatically with the workload:
python -m whisper_batch_subtitles run --root-dir process --dynamic-ffmpeg-workers --ffmpeg-workers-max 8Check progress from a browser (read-only, no authentication, localhost by default):
python -m whisper_batch_subtitles servepython -m whisper_batch_subtitles run `
--root-dir process `
--device cuda `
--model small `
--batch-size 8 `
--ffmpeg-workers 2 `
--translation-workers 4 `
--target-language en `
--target-language es `
--resumeConfiguration can come from five places, lowest to highest precedence:
- built-in defaults
- a YAML config file (
--config, defaultwhisper-batch-subtitles.yamlin the current directory) - a named profile (
--profile <name>, loaded fromprofiles/<name>.yaml) - environment variables prefixed with
WBS_ - CLI flags
run --preset <name> is separate from profiles: it fills in defaults at CLI-flag precedence, so it beats
the config file/profile/env layers but still loses to anything you also type explicitly on the command
line ("run with this preset, but let me override this one setting").
Example:
WBS_MODEL=medium
WBS_TARGET_LANGUAGES=en,es
python -m whisper_batch_subtitles run --root-dir processCreate a machine-tuned config template with:
python -m whisper_batch_subtitles init-configSome notable options beyond the obvious ones:
gpu_device_indices: which CUDA device index each transcription worker routes to (repeat--gpu-deviceon the CLI); auto-populated from all detected GPUs if left unsetsubtitle_cleanup_text/subtitle_max_line_chars/subtitle_max_lines: subtitle text cleanup and balanced line-wrapping (on by default)suppress_repeated_segments: collapse runs of 3+ identical consecutive segments (a known Whisper looping failure mode); on by defaultduplicate_detection:off,warn, orskip— flag or skip files whose content matches an already-completed file elsewhere in the librarytranslation_backend:google(default) ordeepl(needspip install -e .[deepl]anddeepl_api_key/DEEPL_API_KEY)watch/watch_interval_seconds: keep rescanning the folder on an interval instead of exiting after one passtui: a live multi-panel terminal dashboard instead of log lines (needspip install -e .[tui]); takes precedence overlive_statusif both are setdynamic_ffmpeg_workers/ffmpeg_workers_min/ffmpeg_workers_max: auto-scale extraction workers within these bounds based on observed queue backlog, instead of a fixedffmpeg_workerscount
Important speaker-related options:
diarization_mode:off,auto,pyannote, orexternalspeaker_labels: include speaker labels in subtitle text when availablewrite_role_subtitles: emit extra files like.me.srtand.others.srtfor multi-track recordingsprompt_for_track_roles: ask once for unknown multi-track layouts and remember the answer per directorydiarization_external_python: Python executable for a separate diarization environmentdiarization_external_timeout_seconds: timeout for external diarization helper runs
Some recording setups produce multiple audio streams, for example:
- your microphone
- voice chat or remote participants
- game or desktop audio
When Whisper Batch Subtitles detects multiple audio streams, it can:
- inspect the stream metadata
- guess roles such as
me,others,mixed,system, orignore - prompt you to confirm the mapping
- remember that mapping for the directory so the next matching recording is automatic
When separate voice tracks are available, the pipeline transcribes them separately and merges them into a single speaker-labeled subtitle stream. This is often much more accurate than forcing multiple speakers through one mixed track.
There are two speaker-aware paths:
- Multi-track speaker separation: best when your recordings already store separate voice streams
- Single-track diarization: optional backend support through
pyannote.audio
If pyannote.audio is installed in the main environment, diarization_mode: pyannote or auto can label speakers for single mixed audio tracks directly.
If you want to keep diarization isolated in a different environment, set:
diarization_mode: auto
diarization_external_python: .venv-diarization\Scripts\python.exeIn that mode, the pipeline launches whisper_batch_subtitles.diarization_helper in the external environment, passes it the cached audio path, and reads back a cached JSON file of speaker turns.
If no diarization backend is available, the pipeline falls back gracefully and still works as a normal transcription run.
By default, the pipeline stores operational state in .whisper-batch-subtitles/:
state.sqlite3for durable job metadatacache/audio/for extracted audiocache/transcripts/for transcript cache datacache/translations/for translated subtitle cache datalogs/runtime.logfor human-readable logslogs/events.jsonlfor structured log events
This makes reruns much safer and faster on large datasets.
The long-term goal is to grow this into a reliable media ingestion and transcription platform rather than a simple batch script. The core pipeline architecture (queue-based stages, hardware-aware tuning with real multi-GPU routing and empirical benchmarking, resumable SQLite-backed state, caching, presets/wizard/profiles, watch mode, pluggable translation, duplicate detection, dynamic extraction-worker scaling, a real TUI dashboard, a read-only web dashboard, a Dockerfile) is already in place; what's left is mostly bigger, separate efforts rather than incremental additions:
- dynamic scheduling extended beyond the extraction stage (transcription workers stay statically sized since each pins a real model to a GPU)
- chunked/streaming audio extraction instead of always extracting a full cached file first
- distributed/multi-machine workers
- a standalone executable
- a job-submission REST API, a real web frontend, and a plugin system (the current web dashboard is deliberately read-only — status only, not job management)
Open an issue if you'd like to discuss priorities or pick one of these up.
Install the dev extras and run the test suite:
pip install -e .[dev]
pytestThe suite is hermetic (no GPU, no network) except for a handful of ffmpeg/ffprobe integration tests
that generate a synthetic tone via lavfi and auto-skip if those tools aren't on PATH.
Issues, ideas, performance reports, and architecture feedback are all welcome. If you test this on unusual hardware or large real-world datasets, that feedback is especially valuable. See CONTRIBUTING.md for development setup and pull request guidelines, and CHANGELOG.md for release history. This project follows the Contributor Covenant.
Please don't file public issues with vulnerability details — see SECURITY.md for the private disclosure process.
Licensed under the GNU Affero General Public License, version 3 or (at your option) any later version (AGPL-3.0-or-later). See LICENSE for the full text.