Do you have Movies or TV shows in your media player for which the audio and/or subtitle tracks are labeled as "undefined" or "unknown"?
ULDAS (Undefined Language Detector for Audio and Subtitles) solves that problem by:
- Scanning your video and subtitle files for undefined tracks
- Extracting audio and subtitle samples
- Using AI speech recognition to detect the audio language
- Detecting subtitle language (can also detect if subtitles are [FORCED] and/or [SDH])
- Updating the file metadata with the correct language codes and flags
The script optionally remuxes non MKV video formats to MKV first.
Requires
- Python >=3.11
- FFmpeg
- MKVToolNix
- Tesseract-OCR for image based subtitles (e.g. PGS)
- 🛠️ Installation
- ⚙️ Configuration
- 🌐 WebUI
- 📄 Supported File Formats
- ⌨️ CLI Reference
- 🏞️ Example screenshots
⚠️ Need Help or have Feedback?- ❤️ Support the Project
- Download Docker Desktop from docker.com
- Install and start Docker Desktop on your computer
- Verify installation: Open a terminal/command prompt and type
docker --version- you should see a version number
- Create a new folder for ULDAS on your computer (e.g.,
C:\ULDASor/home/user/ULDAS) - Download the
docker-compose.ymland place it in that folder, or manually create it by copy pasting this content:
version: "3.8"
services:
uldas:
container_name: uldas
image: netplexflix/uldas:latest
environment:
- PUID=1000
- PGID=1000
- TZ=America/New_York # Set your timezone
- CRON_SCHEDULE=0 5 * * 5
# Examples:
# "0 3 * * *" = every day at 3:00 AM
# "0 */6 * * *" = every 6 hours
# "0 5 * * 5" = every Friday at 5:00 AM
# Alternative: use a simple hours interval instead of cron:
# - SCHEDULE_HOURS=24
# Leave all scheduler env vars empty or remove them to run once and exit.
#
# NOTE: CRON_SCHEDULE / SCHEDULE_HOURS are only used as initial defaults
# on first launch. Once saved to config.yml they can be edited live from
# the webUI Settings page (Scheduler section) without a container restart.
ports:
- "2119:2119"
volumes:
- ./config:/app/config
- /path/to/folder1:/folder1
- /path/to/folder2:/folder2
# Optional: mount custom temp directory:
# - /path/to/temp:/tmp/uldas
restart: unless-stopped- Volume Mounts:
| Mount | Description |
|---|---|
/app/config |
Config file and tracking data (required) |
/folder1 |
e.g. Your movies library |
/folder2 |
e.g. Your TV shows library |
/tmp/uldas |
optional mount for custom tmp directory |
Note
Your config.yml paths need to match the paths inside your container
example:
volumes:
- ./config:/app/config
- /media/movies:/folder1
- /media/tv:/folder2Extra paths can be added as long as they are also listed in the config.
Important
The format is: your-actual-path:container-path
- Set the timezone (
TZ) e.g.Europe/Brussels,America/New_York,Asia/Tokyo,... - Update the CRON Schedule Tip: Crontab.Guru
CRON_SCHEDULE(andSCHEDULE_HOURS) are only consulted on first start. After that, edit the schedule live from the webUI Settings page → Scheduler section (between General and Advanced). You can switch between a simple hours interval and a CRON expression without restarting the container.- By default ULDAS does not run immediately when the container starts — it waits for the next scheduled time (or a manual Run Now from the dashboard), so you have a chance to review your settings in the webUI first. If you prefer ULDAS to start processing as soon as the container is up, enable Run on Startup in the webUI Settings → Scheduler section (or set
run_on_startup: trueinconfig.yml).
- Update port to xxxx:2119 if you want to run the webUI on a different port than 2119.
- Create a subfolder named
config - Download
config/config.example.ymland save it asconfig.ymlin your config folder
- See ⚙️ Configuration
- Open a terminal/command prompt in your ULDAS folder
- Type this command and press Enter:
docker-compose up -d
- That's it! The latest docker container will be pulled from Dockerhub.
- Update
docker-compose pull
ULDAS is available in Community Applications. Search for "ULDAS" or install manually using the template in unraid/.
Rename config.example.yml to config.yml and change the values where needed:
- path: Main Paths for your media.
- ignore_tags: List of substrings. Any file whose name contains one of these (case-insensitive, matched against the name without extension) is skipped. Useful for trailers, samples, featurettes. Example:
[-trailer, sample] - remux_to_mkv:
trueremuxes non-MKV files so they can be processed too - show_details:
truewill show you more details of what's happening - dry_run:
truewill do a dry run (will show what it would do, without actually altering any files) - run_on_startup:
false(default) — ULDAS will not run immediately when the container starts, giving you time to review settings in the webUI first. Set totrueto run straight away on container start; subsequent runs then follow the configured schedule in either case. - process_subtitles:
truewill process undefined subtitle tracks - process_external_subtitles:
truewill process external subtitle files - analyze_forced_subtitles:
truewill analyze whether a subtitle track has "Forced Subtitles" or not - detect_sdh_subtitles:
truewill analyze whether a subtitle track has 'hearing impaired' support. (e.g.: [Dogs barking], [Narrator:],... )
Only Change these if you know what you're doing.
- vad_filter: Enables Voice Activity Detection to filter out silence and background noise before language analysis (Default: True)
- vad_min_speech_duration_ms: Minimum speech segment length (in milliseconds) to consider as valid speech (Default: 250)
- vad_max_speech_duration_s: Maximum continuous speech segment length (in seconds) before splitting (Default: 30)
- whisper_model: See Model Size Guide below
- device: Hardware acceleration preference (auto, cpu, or cuda). Auto-detects CUDA GPU if available, falls back to CPU (Default: "auto")
- compute_type: Precision/performance trade-off (auto, int8, float16, float32). Auto-selects optimal type based on device (Default: "auto")
- cpu_threads:Number of CPU threads to use. 0 = automatic detection based on system cores (Default: 0)
- confidence_threshold: Minimum confidence level (0.0-1.0) required to accept language detection from audio samples. If sample-based detection falls below this threshold, the entire audio track is analyzed for improved accuracy. Higher values are more conservative but reduce false positives. (Default: 0.9)
- subtitle_confidence_threshold: If subtitle detection confidence falls below confidence, the track is skipped
- reprocess_all :
truewill reprocess ALL audio tracks, even if they already have a language tag. (Default:false) - reprocess_all_subtitles:
truewill reprocess ALL subtitle tracks, even if they already have a language tag. (Default:false) - operation_timeout_seconds: 600, # 10 minutes
- temp_dir: Change temporary directory for audio/subtitle extraction. Leave empty to use system default (/tmp)
Forced subtitle detection thresholds.
Density-based:
- forced_subtitle_low_density_threshold: Below = likely forced
- forced_subtitle_high_density_threshold: Above = likely full
Coverage-based (secondary factor):
- forced_subtitle_low_coverage_threshold: Below = likely forced
- forced_subtitle_high_coverage_threshold: Above = likely full
Absolute count thresholds:
- forced_subtitle_min_count_threshold: Below = likely forced
- forced_subtitle_max_count_threshold: Above = likely full
- tiny: Fastest, least accurate
- base: Good balance
- small: More accurate, slower (used during development tests)
- medium: Very accurate, much slower
- large: Most accurate, very slow
You can access the webui via localhost:2119 Here you can edit settings and check your log. Screenshots
Always Processed:
- MKV files: Primary target format
With remux_to_mkv: true
- MP4, AVI, MOV, WMV, FLV, WebM, M4V, M2TS, MTS, TS, VOB
- Note: Original files are deleted after successful conversion
External subtitle File Formats:
- SRT, ASS, SSA, SUB, VTT, IDX
| Flag | Description |
|---|---|
--version |
Show the current ULDAS version and exit |
--config PATH |
Path to the configuration file (default: config/config.yml) |
--create-config |
Create a sample configuration file and exit |
--find-mkv |
Locate MKVToolNix installation (Windows only) |
--clear-tracking |
Clear all file processing tracking data and exit |
--skip-update-check |
Skip checking for updates on startup |
| Flag | Description |
|---|---|
--verbose, -v |
Enable verbose/debug output (sets show_details to true) |
--quiet, -q |
Suppress most console output (sets show_details to false) |
--show-details |
Show detailed processing information |
--no-show-details |
Hide detailed processing information |
| Flag | Description |
|---|---|
--directory DIR [DIR ...] |
Override directory/directories to scan. Accepts multiple paths |
--remux-to-mkv |
Remux non-MKV video files to MKV before processing |
--no-remux-to-mkv |
Disable remuxing non-MKV files to MKV |
--model {tiny,base,small,medium,large} |
Whisper model size to use for audio language detection |
--dry-run |
Simulate all changes without modifying any files |
--temp-dir DIR |
Override the temporary directory used for intermediate files |
| Flag | Description |
|---|---|
--vad |
Enable VAD filter (default) |
--no-vad |
Disable VAD filter |
--vad-min-speech-duration-ms MS |
Minimum speech duration in milliseconds for VAD |
--vad-max-speech-duration-s S |
Maximum speech duration in seconds for VAD |
| Flag | Description |
|---|---|
--device {auto,cpu,cuda} |
Device for Whisper inference |
--compute-type {auto,int8,int8_float16,int16,float16,float32} |
Compute type for Whisper inference |
--cpu-threads N |
Number of CPU threads (0 = auto) |
| Flag | Description |
|---|---|
--confidence-threshold F |
Confidence threshold for audio language detection (0.0��1.0) |
--reprocess-all |
Reprocess all audio tracks, even those already tagged |
--force-reprocess |
Force reprocessing, ignoring the tracking cache entirely |
| Flag | Description |
|---|---|
--tracking |
Enable file processing tracking (default) |
--no-tracking |
Disable file processing tracking |
| Flag | Description |
|---|---|
--process-subtitles |
Process embedded subtitle tracks |
--no-process-subtitles |
Disable processing of embedded subtitle tracks |
--process-external-subtitles |
Process external subtitle files |
--no-process-external-subtitles |
Disable processing of external subtitle files |
--analyze-forced |
Analyze and tag forced subtitles |
--no-analyze-forced |
Disable forced subtitle analysis |
--detect-sdh |
Detect and tag SDH (Subtitles for the Deaf and Hard of Hearing) |
--no-sdh-detection |
Disable SDH subtitle detection |
--subtitle-confidence-threshold F |
Confidence threshold for subtitle language detection (0.0–1.0) |
--reprocess-all-subtitles |
Reprocess all subtitle tracks, even those already tagged |
| Flag | Description |
|---|---|
--operation-timeout S |
Timeout in seconds for long operations such as full audio track extraction |
| Flag | Description |
|---|---|
--forced-sub-low-coverage F |
Low coverage threshold (%) — below this, subtitles are likely forced |
--forced-sub-high-coverage F |
High coverage threshold (%) — above this, subtitles are likely full |
--forced-sub-low-density F |
Low density threshold (subtitles/min) — below this, likely forced |
--forced-sub-high-density F |
High density threshold (subtitles/min) — above this, likely full |
--forced-sub-min-count N |
Minimum subtitle count — below this, likely forced |
--forced-sub-max-count N |
Maximum subtitle count — above this, likely full |
Every CLI option corresponds to a key in the YAML configuration file. CLI arguments always take precedence over config file values. For boolean options with toggle pairs (--flag / --no-flag), the positive flag wins if both are specified.
| CLI Flag | Config Key | Default |
|---|---|---|
--directory |
path |
["."] |
--remux-to-mkv |
remux_to_mkv |
false |
--show-details |
show_details |
true |
--model |
whisper_model |
base |
--dry-run |
dry_run |
false |
--temp-dir |
temp_dir |
"" |
--vad / --no-vad |
vad_filter |
true |
--vad-min-speech-duration-ms |
vad_min_speech_duration_ms |
250 |
--vad-max-speech-duration-s |
vad_max_speech_duration_s |
30 |
--device |
device |
auto |
--compute-type |
compute_type |
auto |
--cpu-threads |
cpu_threads |
0 |
--confidence-threshold |
confidence_threshold |
0.9 |
--reprocess-all |
reprocess_all |
false |
--force-reprocess |
force_reprocess |
false |
--tracking / --no-tracking |
use_tracking |
true |
--process-subtitles |
process_subtitles |
false |
--process-external-subtitles |
process_external_subtitles |
false |
--analyze-forced |
analyze_forced_subtitles |
false |
--detect-sdh / --no-sdh-detection |
detect_sdh_subtitles |
true |
--subtitle-confidence-threshold |
subtitle_confidence_threshold |
0.85 |
--reprocess-all-subtitles |
reprocess_all_subtitles |
false |
--operation-timeout |
operation_timeout_seconds |
600 |
--forced-sub-low-coverage |
forced_subtitle_low_coverage_threshold |
25.0 |
--forced-sub-high-coverage |
forced_subtitle_high_coverage_threshold |
50.0 |
--forced-sub-low-density |
forced_subtitle_low_density_threshold |
3.0 |
--forced-sub-high-density |
forced_subtitle_high_density_threshold |
8.0 |
--forced-sub-min-count |
forced_subtitle_min_count_threshold |
50 |
--forced-sub-max-count |
forced_subtitle_max_count_threshold |
300 |
- example run with reprocess_all: true: Samsara is indeed a documentary without spoken dialogue.
Note
A warning will be given at the end of a run for any files that were marked as 'zxx' (no linguistic content).
While it is perfectly possible for a video file to have no linguistic content (silent movies, old Disney cartoons, etc), these could also indicate AI 'hallucinations'.
You may want to manually check these files.
Tracks with confidence below the subtitle_confidence_threshold are automatically skipped and shown in the summary.
For image-based (PGS) subtitles without OCR support, language detection will be skipped.
If a file is marked as failed, it is likely corrupt. Manually remux or replace it. Renaming of external subtitle files can fail if the targetted name is already in use by another file, or if the file is set to read-only.
- Join our Discord
If you find this project useful, starring the repository is appreciated! ⭐
Big thanks to DaLeberkasPepi for extensive testing.





