Configuration¶
VBC can be configured via YAML files and CLI arguments. CLI arguments always override config file settings.
Configuration File¶
Default location: conf/vbc.yaml
Demo Configuration¶
Demo mode uses a separate simulation file: conf/demo.yaml.
This file controls simulated file counts, size distribution, processing speed, and error mix.
Full Example¶
input_dirs:
- path: /path/to/videos
enabled: true
- path: /path/to/folder with spaces
enabled: true
- path: /path/to/disabled-folder
enabled: false
# When using output_dirs, set suffix_output_dirs to null.
output_dirs:
- /path/to/videos_out
- /path/to/folder with spaces_out
suffix_output_dirs: null
# When using errors_dirs, set suffix_errors_dirs to null.
errors_dirs:
- /path/to/videos_err
- /path/to/folder with spaces_err
suffix_errors_dirs: null
general:
# === Core Settings ===
threads: 8 # Max concurrent compression threads (>0; executor max_workers=16)
prefetch_factor: 1 # Submit-on-demand multiplier (>=1)
preflight_in_worker: false # Run metadata preparation in active worker slots
gpu: true # Use GPU (NVENC) vs CPU (SVT-AV1)
queue_sort: name # Also supports source-mtime-desc (newest source first)
queue_seed: null # Optional seed for deterministic rand order
log_path: /tmp/vbc/compression.log # Log file location
cpu_fallback: false # Retry on CPU if NVENC hits HW cap error
ffmpeg_cpu_threads: null # Max CPU threads per ffmpeg worker (null = auto)
# === Input/Output ===
extensions: # File extensions to process
- ".mp4"
- ".mov"
- ".avi"
- ".flv"
- ".webm"
min_size_bytes: 1048576 # Minimum file size (1 MiB)
# === Metadata ===
copy_metadata: true # Copy EXIF/XMP tags from source
use_exif: true # Use ExifTool for deep metadata analysis
# === Filtering ===
skip_av1: false # Skip files already encoded in AV1
filter_cameras: [] # Only process specific camera models (empty = all)
# Example: ["ILCE-7RM5", "DJI OsmoPocket3"]
# === Quality Control ===
dynamic_quality: # Camera-specific quality rules
"ILCE-7RM5":
cq: 38 # CQ mode override
rate: # Optional rate override
bps: "0.8"
minrate: "0.7"
maxrate: "0.9"
"DC-GH7":
cq: 40
"DJI OsmoPocket3":
cq: 45
rate:
bps: "180M"
quality_mode: cq # cq (default) or rate
bps: null # e.g. 200Mbps, 200M, 200000k, 0.8
minrate: null # optional; same class as bps
maxrate: null # optional; same class as bps
rate_target_max_bps: null # optional hard cap for resolved target bitrate (absolute only)
min_compression_ratio: 0.1 # Minimum savings required (0.0-1.0)
# If compression < 10%, keep original
# === Error Handling ===
clean_errors: false # Remove .err markers on startup
verify_fail_action: false # false | log | pause | exit
source_policy: keep # keep | delete_after_success
auto_repair_errors: true # Repair current-session failures before WAITING/exit
# === UI/Display ===
strip_unicode_display: true # Replace emoji/unicode with '?' in UI
debug: false # Enable verbose debug logging
# === Manual Rotation ===
manual_rotation: null # Global rotation (null, 0, 90, 180, 270)
gpu_config:
enabled: true
sample_interval_s: 5.0
history_window_s: 300.0
nvtop_device_index: 0
gpu_encoder:
advanced: false
common_args:
- "-c:v av1_nvenc"
- "-preset p7"
- "-tune hq"
- "-b:v 0"
- "-cq 45"
- "-f mp4"
advanced_args:
- "-c:v av1_nvenc"
- "-preset p7"
- "-tune hq"
- "-b:v 0"
- "-cq 45"
- "-rc vbr"
- "-multipass fullres"
- "-rc-lookahead 32"
- "-spatial-aq 1"
- "-temporal-aq 1"
- "-aq-strength 8"
- "-b_ref_mode middle"
- "-f mp4"
cpu_encoder:
advanced: false
common_args:
- "-c:v libsvtav1"
- "-preset 6"
- "-crf 32"
- "-svtav1-params tune=0:enable-overlays=1"
- "-f mp4"
advanced_enforce_input_pix_fmt: true
advanced_args:
- "-c:v libaom-av1"
- "-crf 30"
- "-b:v 0"
- "-cpu-used 0"
- "-tune ssim"
- "-lag-in-frames 35"
- "-aq-mode 1"
- "-row-mt 1"
- "-threads 0"
- "-f matroska"
ui:
activity_feed_max_items: 5
panel_height_scale: 0.7
autorotate:
patterns: # Regex -> Rotation angle
"DJI_.*\\.MP4": 0 # DJI drones - no rotation
"GOPR\\d+\\.MP4": 180 # GoPro pattern - 180° flip
"IMG_\\d{4}\\.MOV": 90 # iPhone pattern - 90° rotation
Configuration Reference¶
General Settings¶
threads¶
- Type: Integer (>0)
- Default: 1
- Description: Maximum number of concurrent compression threads
- Note: Runtime keyboard adjustment (
</>) clamps to 1-8 threads - Implementation detail: Worker pool is created with
max_workers=16, so practical upper parallelism is 16
Quality Defaults¶
- Source: Encoder args (
gpu_encoder/cpu_encoder) via-cq(GPU) or-crf(CPU) - Override:
--qualityCLI flag orgeneral.dynamic_qualitymappings - Recommendation ranges (0-63, lower = better quality):
- 35-38: Archival quality
- 40-45: High quality daily use
- 48-52: Good quality, smaller files
- 55+: Low quality, very small files
prefetch_factor¶
- Type: Integer (>=1)
- Default: 1
- Description: Submit-on-demand queue multiplier. Higher values = more files queued.
- Formula:
max_queued = prefetch_factor × threads
preflight_in_worker¶
- Type: Boolean
- Default:
false - Description: When enabled, disables proactive metadata probing of the pending queue. Each task performs its preflight after taking a normal worker slot and appears in ACTIVE JOBS with the animated arc icon before compression begins.
- Trade-off: A slow
ffprobeoccupies only one worker slot instead of blocking preparation of the entire queue. Queue rows remain lightweight until their task starts. - Scope: Global; applies to regular video files and JSON metadata tasks.
queue_sort¶
- Type: String
- Default:
name - Description: Processing order for files in the queue
- Values:
name,rand,dir,size,size-asc,size-desc,ext,source-mtime-desc - Notes:
sizeis an alias forsize-ascrandcan be made deterministic withqueue_seedextuses the order ofextensionsand requires a non-empty listsource-mtime-descplaces the newest source recording first. Manifest jobs useproducer.source_latest_mtime_ns; regular video jobs use the source file's filesystemmtime_ns. The manifest JSON file's own mtime is never used, so importing old manifests does not make archival recordings look new.
queue_seed¶
- Type: Integer or null
- Default:
null - Description: Seed for deterministic
randqueue order - Example:
42(ensure same random order across runs)
log_path¶
- Type: String or null
- Default:
/tmp/vbc/compression.log - Description: Path to log file (overrides the output directory default)
- Note: If set to
null, logs are written to<output_dir>/compression.log. - Rotation: At startup, a non-empty existing log is archived with a local timestamp, for example
compression_2026-07-18_00-22-15.log. The new session keeps usingcompression.log.
cpu_fallback¶
- Type: Boolean
- Default: false
- Description: Retry on CPU when GPU encoding fails with hardware capability errors
- Note: Useful when NVENC runs out of sessions; pair with
ffmpeg_cpu_threads - Behavior: HW cap
.errmarkers are cleared on startup so files re-enter the queue
ffmpeg_cpu_threads¶
- Type: Integer or null
- Default:
null - Description: Max CPU threads per ffmpeg worker when using CPU encoding (including fallback)
- Note: Limits per-worker CPU usage; does not change
threads(worker count)
gpu¶
- Type: Boolean
- Default: true
- Description: Use GPU (NVENC) instead of CPU (SVT-AV1)
- GPU (NVENC):
- Pros: Very fast, good for 1080p/1440p
- Cons: Quality ceiling at ~CQ35-38, session limits
- CPU (SVT-AV1):
- Pros: Excellent quality, no session limits
- Cons: Much slower
gpu_refresh_rate¶
- Type: Integer
- Default:
5 - Description: (Deprecated) Use
gpu_config.sample_interval_sinstead. Kept for backwards compatibility. - Note: See deprecation notice in GPU Monitoring section
GPU Monitoring (gpu_config)¶
Advanced settings for GPU monitoring sparklines.
enabled¶
- Type: Boolean
- Default:
true - Description: Enable GPU monitoring and dashboard sparklines.
- Note: Requires NVIDIA GPU and
nvtop(VBC callsnvtop -s).nvidia-smiis only for driver checks.
sample_interval_s¶
- Type: Float
- Default:
5.0 - Description: How often to sample GPU metrics (seconds).
history_window_s¶
- Type: Float
- Default:
300.0 - Description: Total time window shown in sparklines (default 5 minutes).
nvtop_device_index¶
- Type: Integer
- Default:
0 - Description: Index of the GPU to monitor when multiple GPUs are present.
nvtop_device_name¶
- Type: String or null
- Default:
null - Description: Override device selection by name instead of index (e.g., "NVIDIA GeForce RTX 4090").
- Note: When set, takes precedence over
nvtop_device_index.
nvtop_path¶
- Type: String or null
- Default:
null - Description: Custom path to nvtop binary (e.g., "/usr/local/bin/nvtop"). Auto-detected if not set.
- Note: Useful when multiple nvtop versions are installed or nvtop is in a non-standard location.
refresh_rate¶
- Type: Integer
- Default:
5 - Description: (Deprecated) Use
sample_interval_sinstead. Kept for backwards compatibility.
Deprecated Fields
gpu_config.refresh_rateis deprecated in favor ofsample_interval_sgeneral.gpu_refresh_rateis deprecated in favor ofgpu_config.sample_interval_s
For backwards compatibility, VBC still accepts both old fields, but new configurations should use gpu_config.sample_interval_s.
GPU Encoder (gpu_encoder)¶
Encoder args are full FFmpeg argument lists. VBC uses one list:
common_args when advanced=false, otherwise advanced_args. Lists are not merged.
advanced¶
- Type: Boolean
- Default: false
- Description: Use
advanced_argsinstead ofcommon_args.
common_args¶
- Type: List of strings
- Default: NVENC baseline (
-c:v av1_nvenc,-preset p7,-tune hq,-b:v 0,-cq 45,-f mp4) - Description: Full FFmpeg arg list for standard GPU encoding.
advanced_args¶
- Type: List of strings
- Default: NVENC HQ set (adds
-rc vbr,-multipass fullres, AQ, lookahead, etc.) - Description: Full FFmpeg arg list for advanced GPU encoding.
CPU Encoder (cpu_encoder)¶
advanced¶
- Type: Boolean
- Default: false
- Description: Use
advanced_argsinstead ofcommon_args.
common_args¶
- Type: List of strings
- Default: SVT-AV1 baseline (
-c:v libsvtav1,-preset 6,-crf 32,-f mp4) - Description: Full FFmpeg arg list for standard CPU encoding.
advanced_enforce_input_pix_fmt¶
- Type: Boolean
- Default: true
- Description: When
advanced=true, force output-pix_fmtto match the input (from ffprobe).
advanced_args¶
- Type: List of strings
- Default: AOM AV1 HQ set (
-c:v libaom-av1,-crf 30,-cpu-used 0,-tune ssim, etc.) - Description: Full FFmpeg arg list for advanced CPU encoding (often outputs
-f matroska, so.mkv).
UI Configuration (ui)¶
Dashboard display settings.
activity_feed_max_items¶
- Type: Integer (1-20)
- Default:
5 - Description: Maximum number of events shown in the activity feed panel.
active_jobs_max_display¶
- Type: Integer (1-16)
- Default:
8 - Description: Maximum number of concurrent jobs to display in the active panel.
panel_height_scale¶
- Type: Float (0.3-1.0)
- Default:
0.7 - Description: Vertical scaling factor for panels (0.7 = 30% reduction in height).
Input/Output¶
input_dirs¶
- Type: List of objects (
{path: string, enabled: bool, metadata?: bool, watch?: bool, idle_interval?: int}) - Default:
[](empty) - Description: Ordered input directory entries used when no CLI input is provided
- Behavior:
- CLI input overrides config input (no merge)
- Only entries with
enabled: trueare processed - UI order is exactly the same as YAML order
- Duplicate
pathvalues are rejected (startup validation error) - Missing or inaccessible directories are skipped
- Startup fails if no valid directories remain
- Limits: max 50 enabled directories, max 150 characters per path
metadata: truescans strict*.jsoncompression manifests instead of video extensionswatch: trueuses Linux inotify to trigger a refresh when a final*.jsonis closed after writing or atomically moved into a metadata directory. It is rejected for regular video directories and defaults tofalse.idle_intervalenables an automatic re-scan after that many idle seconds whenwait_on_finish: true; omitted means manual refresh only
Manifest-driven multipart input¶
input_dirs:
- path: /path/to/metadata
enabled: true
metadata: true
watch: true
suffix_output_dirs: _out
suffix_errors_dirs: _err
metadata:
audio_only: ignore
# Maximum missing output video frames accepted for the complete logical job.
# Default: 0 (strict).
max_dropped_frames: 0
# Maximum effective duration of one part and of the complete request.
# Default: 86400 (24 hours).
max_duration_seconds: 86400
# Optional hot-reloaded overrides:
# source_policy: move_all
# move_after_success_dir: /path/to/source-archive
# compression_profile: tiktok
# error_policy:
# missing_input: fail
Each final *.json file represents one logical queue item. Initial discovery validates
the JSON and input paths, then publishes a lightweight queue proxy immediately. VBC
probes every path in inputs through the rolling 25-item metadata window, preserves the
listed order, and splits consecutive parts whenever their orientation changes. Compatible
resolutions within one orientation group are normalized to that group's largest frame,
transcoded sequentially, and stream-copied into one MP4. The first group uses the exact
manifest output_path; later groups use the first available numbered name (_1, _2,
and so on). Existing MP4 files are never overwritten and share the same numbered namespace,
so an untagged file protected as _1 makes the next orientation group use _2. The
split is based only on probed orientation; VBC does not fingerprint or trim repeated
content with a compatible orientation. The directory suffixes route the JSON itself:
Each unchanged physical part is probed once per VBC run. That single probe supplies stream properties, packet counts, and the normalized video duration derived from packet timestamps; refreshes reuse the cached result. Output stream verification is likewise cached, while the post-write VBC-tag check uses ExifTool without probing the video again.
With watch: true, completed JSON events are coalesced during a one-second quiet
period and then only the reported JSON paths are added to discovery. This works both
while jobs are active and in WAITING mode; it does not preempt jobs already running
or rescan an existing metadata backlog. Hidden temporary files and all non-JSON names
are ignored. If inotify reports an event-queue overflow, VBC shows a warning and falls
back to a full refresh. A manually copied JSON that is observed before its final write
has settled is retried after a one-second stability check before it can be routed to
_err. A path that disappears before incremental discovery reaches it is treated as an
already-routed task and is ignored without creating or overwriting an error marker.
- success:
/metadata_out/YYYYMMDD/request.json - any probe, compression, verification, or cleanup error for a valid manifest whose
inputs are under
/recordings/<username>/:/recordings_err/<username>/request.json,/recordings_err/<username>/request.err, and every existing source input - invalid manifest JSON whose producer and source list cannot be trusted:
/metadata_err/request.jsonand/metadata_err/request.err
metadata.audio_only is fail by default. ignore removes parts without usable video
packets from the effective concat list while retaining them for source_policy handling.
delete removes those parts from both the concat list and the filesystem during
preflight, independently of source_policy, and logs each removal as
MANIFEST_SOURCE_DELETED with reason=audio_only. If no usable video remains, VBC
creates no output and moves the unchanged manifest to _out as an ignored task.
Optional policy overrides are reloaded before each manifest job; an invalid edit keeps
the last valid metadata policy. metadata.max_dropped_frames defaults to strict 0;
each generated output accepts only that many missing frames, logs a warning, and never
accepts extra frames. metadata.max_duration_seconds defaults to 24 hours. A packet
duration above the limit triggers a decoded-frame count only for that anomalous part.
VBC accepts it and rebuilds its timestamps when frames / fps is within the limit;
otherwise preflight fails. An aggregate duration above the limit also fails. The failure
is shown in the activity feed and routes the JSON to _err. copy_metadata remains a
video-to-video setting and uses the first effective video part from each generated
orientation group.
The success date directory comes from the timestamp suffix in
producer.recording_id (..._YYYYMMDD_HHMMSS). If that identifier has no valid date,
VBC uses the manifest's required created_at value. The compressed output filename does
not need to contain a date. Existing flat success manifests remain supported by cleanup
tools.
Every generated output must pass frame, ffprobe, and VBC-tag verification before the JSON
moves to _out or an output-dependent source policy runs. A request below
general.min_size_bytes is also a successful no-output result:
delete_after_success deletes all of its sources and move_all archives them. Ctrl+C
leaves the manifest and sources in place. On restart, VBC reuses verified VBC outputs in
numbered order, skips occupied untagged numbers, and continues with the first missing
group.
After successful finalization, every generated output receives the manifest's exact
producer.source_latest_mtime_ns. The same rule applies to regular video jobs using the
source video's filesystem mtime. Output directory timestamps are not modified. Actual
file changes are recorded in the application log as OUTPUT_MTIME_CHANGED entries.
Manifest schema version 1 requires operation: concat_transcode, absolute unique input
paths, compression_profile: tiktok, error_policy.missing_input: fail, and one of
source_policy: keep, delete_after_success, move_after_success, or move_all.
Deletion and move_after_success normally run only after atomic output finalization and
successful ffprobe/VBC-tag verification. The exception is a request below
general.min_size_bytes: it is considered successfully handled without an output, so
delete_after_success deletes all inputs. Each deletion is recorded as
MANIFEST_SOURCE_DELETED with the source path, size in bytes, manifest path, and reason.
move_after_success uses metadata.move_after_success_dir and preserves the producer
username as the first destination directory. A missing destination setting, a missing or
unwritable directory, insufficient free space for all inputs, or an existing destination
file makes the policy fall back to keep for the complete request. move_all additionally
archives inputs for completed ignored requests that produced no output. A terminal
preflight, compression, verification, or cleanup failure instead derives the real source
root from the manifest inputs and quarantines every existing input together with the JSON
and .err under <source_root>_err/<producer.username>/, regardless of the success
policy. For example, /mnt/1/TT/recordings/user/file.mp4 is quarantined under
/mnt/1/TT/recordings_err/user/. The watched metadata directory is only a request
transport. Destination collisions fail closed and leave the original request in place.
Invalid JSON cannot move sources because its producer and input list are not trusted, so
only that JSON and its marker use metadata_err. Ctrl+C is not terminal and never moves
sources or the manifest.
Generating manifests for legacy recordings¶
scripts/generate_metadata_manifests.py scans a recordings tree without modifying it and
writes strict manifests into a separate metadata directory. Files ending in
_partNNN.mp4 are grouped and numerically ordered; a plain MP4 without a matching part
group becomes a single-input request. A read-only ExifTool tag scan first excludes prior
outputs carrying the VBCEncoder tag. The scan runs in bounded batches and displays
progress. A file that ExifTool reports as an unreadable format is warned about separately
and remains eligible for normal manifest discovery; it does not abort the remaining scan.
If an untagged plain file is followed by
_part002.mp4, it is treated as the legacy first part. Otherwise a complete part group
wins over a same-name untagged plain file, which is reported as shadowed. Groups with
unresolved missing part numbers, symlinks, VBC staging artifacts, zero total size,
and duplicate manifest names are never generated. On a repeated scan, an existing
manifest with the same logical content is counted and skipped. Its created_at timestamp
is ignored for this comparison. An existing same-name manifest with any other difference
is reported as a conflict and is never overwritten.
uv run python scripts/generate_metadata_manifests.py \
/path/to/recordings \
/path/to/generated_metadata \
--compressed-dir /path/to/compressed \
--modified-before 2026-07-19T00:00:00+02:00 \
--dry-run
Remove --dry-run to create the JSON files. The required --modified-before value must
include a timezone offset. A task is generated only when every input has an mtime strictly
older than the boundary, which prevents currently written parts from entering the migration.
--compressed-dir defaults to a sibling directory named compressed; --source-policy
defaults to keep. A configured metadata.source_policy still overrides the value generated
in each manifest. An existing output is still represented by a manifest. VBC validates
ffprobe readability and required VBC tags: a valid tagged output is reused, while an untagged
or invalid primary output is preserved under the next _N.mp4 name before VBC creates a
verified replacement.
Repairing timestamps from filenames¶
scripts/repair_output_mtimes.py recursively scans a directory without using metadata
manifests. For every regular file whose name contains a valid YYYYMMDD_HHMMSS value, it
sets the file mtime to that local date and time. The file may have any extension or no
extension. Each directory containing matching files directly receives the newest timestamp
found among those files. Symlinks are ignored.
# Inspect the planned changes
uv run python scripts/repair_output_mtimes.py \
/path/to/compressed \
--dry-run
# Apply the changes
uv run python scripts/repair_output_mtimes.py \
/path/to/compressed
Use --files-only to modify matching files without touching directories. Use
--user-dirs-only to modify only user directories that are direct children of the supplied
directory, without touching files. These two switches are mutually exclusive; without
either switch, the script updates both files and their direct parent directories.
Because filenames do not contain a timezone offset, timestamps are interpreted in the machine's local timezone. Rich output reports scanned and changed files and directories, invalid date-like names, ignored symlinks, and filesystem errors.
Restoring a failed manifest and moved sources¶
scripts/restore_failed_manifest.py restores one JSON from a source error package or the
legacy configured metadata error directory. It reads input_dirs, errors_dirs or
suffix_errors_dirs, and metadata.move_after_success_dir from the VBC configuration.
Every source listed in the manifest must either already exist at its original path, be
next to the failed manifest in its producer error package, or be present under the legacy
archived producer directory. The script validates the complete plan before moving
anything, refuses all destination collisions, restores source files first, and moves the
JSON into the metadata input directory last so inotify cannot enqueue an incomplete
request.
uv run python scripts/restore_failed_manifest.py \
/path/to/recordings_err/username/request.json \
--dry-run
The default configuration is conf/vbc.yaml; use --config /path/to/vbc.yaml to select
another file. Remove --dry-run to perform the restore. Missing sources are printed with
both their original and expected archive paths, and no files are moved when any source is
missing. Sources that have already been manually restored are accepted when no duplicate
archive copy exists. The existing .err marker remains in the error directory as failure
history.
Repairing supported failed manifests¶
scripts/repair_failed_manifests.py performs a controlled recompression for known failure
classes while leaving the original .err marker in place. It accepts one .json/.err
file or recursively scans *.err files in a manifest error directory. Unsupported errors
are reported and left unchanged. Repairs run sequentially and reuse the normal VBC
preflight, output verification, VBC tagging, manifest routing, and source policy.
The first supported handler is FFmpeg exit code 244 / error -12. It restores archived
sources when needed and recompresses video without its problematic audio stream. This is a
runtime-only override: the manifest JSON is not modified. Every FFmpeg child started by
the repair tool has a fixed 50 GiB address-space limit.
# Inspect one task without changing files
uv run python scripts/repair_failed_manifests.py \
/path/to/recordings_err/username/request.err \
--dry-run
# Repair every supported task in one configured error directory
uv run python scripts/repair_failed_manifests.py \
/path/to/recordings_err
The default configuration is conf/vbc.yaml; use --config /path/to/vbc.yaml when
needed. For a successful repair, the JSON moves to its configured metadata output
directory, source files follow the effective source_policy, and the old .err remains
in the error directory. A failed or interrupted repair does not publish the JSON back into
the watched input queue.
Analyzing and clearing a manifest error directory¶
scripts/video_error_analyzer.py recursively classifies every .err file in a manifest
error directory such as recordings_err. A plain invocation is read-only and does not
require the corresponding JSON to exist:
Known categories have independent action flags. --repair-ffmpeg-244 delegates to the
bounded repair described above. Deletion flags include --delete-orphans,
--delete-moov-missing, --delete-missing-input, --delete-invalid-dimensions,
--delete-no-video, --delete-invalid-bitstream, --delete-hardware-capability,
--delete-ffmpeg-abort, --delete-ffmpeg-segfault, --delete-ffmpeg-234, and
--delete-unknown. Multiple category flags may be combined. Add --dry-run to inspect the
selected actions without changing files.
Deletion is deliberately limited to the selected .json and .err metadata pair. It
never deletes or moves source videos or compressed outputs. For an orphan marker, only the
.err file is removed. There is no catch-all deletion switch.
Verifying and cleaning the moved source archive¶
scripts/cleanup_source_archive.py independently compares files in a moved source archive
with files in the compressed output tree. Manifest contents are not trusted for output
verification; completed manifest filenames are used only to recognize requests that VBC
already routed successfully. Multipart names such as recording_part001.mp4 are normalized
to recording.mp4 in the matching relative user directory.
The archive scan uses general.extensions from the same configuration as VBC. Files with
other extensions are outside its scope, remain untouched, and are not reported as errors.
# Read-only inventory
uv run python scripts/cleanup_source_archive.py \
/path/to/sources_compressed \
/path/to/compressed
# The paths and size floor may be omitted. The source archive and min_size_bytes
# come from conf/vbc.yaml; the compressed root is inferred from active, completed,
# or legacy error manifests in the configured metadata directories.
uv run python scripts/cleanup_source_archive.py
# Preview safe deletion
uv run python scripts/cleanup_source_archive.py \
/path/to/sources_compressed \
/path/to/compressed \
--verify-vbc-tags \
--delete-verified \
--dry-run
# Delete at most 10 eligible sources and print every deleted path
uv run python scripts/cleanup_source_archive.py \
--verify-vbc-tags \
--delete-verified \
--delete-limit 10
New outputs contain VBCSourceParts, for example 1,2,4,5. Each output created from an
orientation group lists only the physical parts that actually contributed to that output.
The cleaner deletes listed parts. For an omitted part, it additionally runs a bounded
ffprobe without a packet timeline. A part independently confirmed to have no usable
video packets is reported as IGNORED_NO_VIDEO and is deletion-eligible. An omitted part
with video, or one that cannot be probed, remains UNMAPPED_SOURCE. Older outputs without
VBCSourceParts are reported as LEGACY_MATCH and use filename matching. With
--verify-vbc-tags, both precise and legacy matches also require VBCEncoder.
If an output is missing but the request is present in the configured metadata success
directory, the cleaner runs bounded ffprobe -count_packets checks without a packet
timeline. A group where every source has zero usable video packets is reported as
DONE_NO_VIDEO and is deletion-eligible. The cleaner also repeats VBC's effective size
calculation after excluding audio-only parts. If the remaining video-bearing sources are
below general.min_size_bytes, the group is reported as DONE_BELOW_MIN_SIZE and is also
deletion-eligible. A completed group with enough usable video keeps the conservative
OUTPUT_MISSING status.
Known terminal failures are quarantined instead of deleted during --delete-verified:
moov atom not found (CORRUPT_MOOV), FFmpeg exit -6/SIGABRT, FFmpeg exit
-11/SIGSEGV, unsupported hardware capabilities, invalid video dimensions, and invalid
input data. Classification prefers the matching .err marker; the missing-moov case can
also be recognized directly by the bounded source probe. The cleaner reads the matching
manifest and derives <original source root>_err/<producer.username> from its input
paths. It moves the archived source together with any matching
ttracker-<recording_id>.json and .err found in the configured metadata input,
success, or error directories. A missing, invalid, or inconsistent manifest disables
quarantine for that source. Destination collisions fail closed, and a partial move is
rolled back.
For a multipart failure, the cleaner can isolate a part that independently fails bounded
probing with End of file, invalid input data, or a missing moov atom. If the sum of
valid video parts and the complete sizes of those corrupt parts is still below
general.min_size_bytes, only the corrupt parts are quarantined. Confirmed audio-only
parts are excluded from that upper bound, and the remaining sources are deletion-eligible
as ignored no-video or below-minimum data. An unresolved probe failure, or an upper bound
at or above the configured floor, keeps the complete group as OUTPUT_MISSING.
Unknown failures remain in the source archive as OUTPUT_MISSING. This is intentional:
for example, FFmpeg exit code 234 is not assigned a meaning merely from the numeric code.
After a non-dry-run cleanup, remaining source files therefore identify recordings whose
missing compression output still needs explanation.
Logical source groups below general.min_size_bytes are reported as BELOW_MIN_SIZE.
Their individual and combined sizes are shown, and --delete-verified also deletes these
sources even when no compressed output exists. Override the configured floor with
--min-size-bytes. Remove --dry-run to apply deletion. Outputs, sources at or above the
floor without a verified match, unmapped sources, invalid tags, symlinks, and files outside
general.extensions are never deleted.
The detailed table shows only sources requiring attention by default, so verified legacy
matches and below-minimum entries do not hide unresolved cases. Their counts remain in the
inventory summary. Use --show-all to print every source.
--delete-limit N (alias --limit-delete N) limits a --delete-verified run to
at most N actual deletions. Without the option, deletion remains unlimited. When the limit
is present, the report includes a separate DELETED table; with --dry-run, it instead
lists the limited WOULD_DELETE paths.
Scanning and deletion have separate progress bars. The deletion stage reports
Deleting sources N/N, or Previewing deletions N/N with --dry-run.
When either positional path is omitted, the script loads conf/vbc.yaml (or --config).
The source archive comes from metadata.move_after_success_dir. Because the compressed
video root is stored in manifests rather than directly in VBC configuration, the script
infers it from up to 100 newest manifests in enabled metadata directories and stops if the
root is missing or ambiguous.
output_dirs¶
- Type: List of strings
- Default:
[](empty) - Description: Explicit output directories (one per enabled input directory, in order)
- Rules:
- Must exist and be writable
- Count must match enabled input directories
- Cannot be used with
suffix_output_dirs(set it tonull)
suffix_output_dirs¶
- Type: String or null
- Default:
_out - Description: Output directory suffix appended to each input directory name
- Notes:
- Set to
nullwhen usingoutput_dirs - Example:
/videos→/videos_out
errors_dirs¶
- Type: List of strings
- Default:
[](empty) - Description: Explicit directories for failed files (one per enabled input directory, in order)
- Rules:
- Must exist and be writable
- Count must match enabled input directories
- Cannot be used with
suffix_errors_dirs(set it tonull)
suffix_errors_dirs¶
- Type: String or null
- Default:
_err - Description: Suffix appended to each input directory name for failed files
- Behavior: After processing, failed source files and their
.errmarkers are moved here - Safety: If more than 100
.errfiles are found, VBC asks before moving them - Notes:
- Set to
nullwhen usingerrors_dirs - Example:
/videos→/videos_err
extensions¶
- Type: List of strings
- Default:
[".mp4", ".mov", ".avi", ".flv", ".webm"] - Description: File extensions to scan and process
- Note: Case-insensitive, can include or omit leading dot
min_size_bytes¶
- Type: Integer
- Default: 1048576 (1 MiB)
- Description: Minimum input file size to process
- Use case: Skip corrupted/incomplete files
- Manifest behavior: Compared with the sum of effective video-part sizes after audio-only filtering. A smaller request creates no video and moves its JSON to
_outas a completed ignored task.delete_after_successdeletes every remaining source (audio_only: deletehas already removed filtered inputs),move_allarchives the remaining sources, and the remaining policies keep them.
Metadata¶
copy_metadata¶
- Type: Boolean
- Default: true
- Description: Copy EXIF/XMP/GPS tags from source to output
- Method: Uses ExifTool to preserve all metadata including GPS
- Manifest jobs: Copies from the first effective video part; JSON metadata is never copied to the video
use_exif¶
- Type: Boolean
- Default: true
- Description: Enable deep metadata analysis with ExifTool
- Required for:
dynamic_quality(camera-specific quality)filter_cameras(camera filtering)- GPS and camera model extraction
Filtering¶
skip_av1¶
- Type: Boolean
- Default: false
- Description: Skip files already encoded in AV1 codec
- Use case: Mixed libraries with some AV1 files already compressed
filter_cameras¶
- Type: List of strings
- Default:
[](empty = process all cameras) - Description: Only process files from specific camera models
- Example:
["ILCE-7RM5", "DJI OsmoPocket3"] - Matching: Case-insensitive substring against extracted camera metadata; exact model strings are most reliable.
Quality Control¶
dynamic_quality¶
- Type: Dictionary (string -> object)
- Default:
{}(empty) - Description: Camera model -> quality rule mapping.
- Matching: Full-text search in all EXIF metadata
- Example:
- Priority: First match wins (order matters in YAML)
- Required schema: each entry must be an object with
cq; legacy scalar format ("Sony": 40) is rejected.
quality_mode¶
- Type: String
- Default:
cq - Values:
cq,rate - Description:
cq: use encoder quality args (-cqon GPU,-crfon CPU)rate: use bitrate target (bps) with optionalminrate/maxrate- CLI validation rules:
--qualityis valid only forquality_mode=cq--bps/--minrate/--maxraterequirequality_mode=rate
bps¶
- Type: String or null
- Default:
null - Description: Target bitrate for
quality_mode=rate. - Accepted formats:
- Absolute:
200000000,200000k,200M,200Mbps - Relative:
0.8(input bitrate × 0.8)
minrate¶
- Type: String or null
- Default:
null - Description: Optional lower bitrate bound in
ratemode. - Rule: Must use the same numeric class as
bps(all absolute or all relative).
maxrate¶
- Type: String or null
- Default:
null - Description: Optional upper bitrate bound in
ratemode. - Rule: Must use the same numeric class as
bpsandminrate.
rate_target_max_bps¶
- Type: String or null
- Default:
null - Description: Optional hard cap applied after
bps/minrate/maxrateresolution inratemode. - Accepted formats: Absolute only (
95M,100Mbps,100000k,100000000). - Scope:
- Global:
general.rate_target_max_bps - Per camera override:
general.dynamic_quality.<pattern>.rate.rate_target_max_bps - Note: This caps target bitrate; it does not guarantee peak bitrate.
min_compression_ratio¶
- Type: Float (0.0-1.0)
- Default: 0.1 (10%)
- Description: Minimum compression savings required
- Behavior: If
(1 - output_size/input_size) < threshold, keep original file instead of compressed version - Use case: Prevent "compression" that makes files larger
Error Handling¶
clean_errors¶
- Type: Boolean
- Default: false
- Description: Remove existing
.errmarkers on startup and retry those files - Behavior:
false: Skip files with.errmarkerstrue: Delete.errfiles and retry compression
verify_fail_action¶
- Type: String
- Default:
false - CLI:
--verify-fail-action - Values:
false: disable output verificationlog: mark job as failed and continuepause: mark job as failed and pause queue withERRORstatus (wait forR/S)exit: mark job as failed and terminate processing with error- Verification checks (when mode is not
false): ffprobecan read output without errors- VBC tags exist:
VBCOriginalName,VBCOriginalSize,VBCQuality,VBCOriginalBitrate,VBCEncoder,VBCFinishedAt
source_policy¶
- Type: String
- Default:
keep - Values:
keep: retain the original source filedelete_after_success: delete the source only after a successful compression and verification- Safety: This option forces verification even when
verify_fail_action: false. VBC checks that ffprobe can read the output, that it contains video packets, and that all required VBC tags are present. If compression did not meet the minimum savings ratio, VBC keeps the original source.
auto_repair_errors¶
- Type: Boolean
- Default: true
- Description: Automatically attempt one in-TUI repair pass for files that failed during the current session
- Behavior:
true: After the queue is empty, VBC enters REPAIR, moves current-session failures to the errors folder, repairs them once, then queues repaired files for compressionfalse: VBC skips the automatic repair pass- Repaired
.mkvfiles are processed directly after repair even when.mkvis not listed ingeneral.extensions - After graceful shutdown (
S), VBC performs the repair pass and exits without compressing repaired files
repair_corrupted_flv¶
- Type: Boolean
- Default: false
- Description: Legacy/special FLV repair setting. Automatic current-session repair is controlled by
auto_repair_errors.
wait_on_finish¶
- Type: Boolean
- Default: false
- CLI:
--wait/--no-wait - Description: After all tasks complete, wait for user input instead of auto-exiting
- Behavior:
true: Displays WAITING status; press R to restart scan or S/Ctrl+C to exit- With per-directory
idle_interval, VBC automatically re-scans due directories while WAITING - A metadata directory with
watch: truetriggers refreshes in both ACTIVE and WAITING states.wait_on_finish: trueis still required to keep an empty VBC process alive for future events. false: VBC exits automatically when processing finishes
bell_on_finish¶
- Type: Boolean
- Default: false
- CLI:
--bell/--no-bell - Description: Play a terminal bell (BEL character) once the full cycle finishes (compression → automatic repair → compression of repaired files)
- Triggers:
- When entering wait state (if
wait_on_finish=true) - Just before exit (if
wait_on_finish=false) - The automatic repair pass itself does not emit a bell, because repaired files are queued and compressed immediately afterwards
UI/Display¶
strip_unicode_display¶
- Type: Boolean
- Default: true
- Description: Replace non-ASCII characters (emoji, special Unicode) with '?' in UI
- Reason: Prevents table alignment issues with emoji in filenames
debug¶
- Type: Boolean
- Default: false
- Description: Enable verbose debug logging
- Logs: FFmpeg timing, ExifTool calls, compression stages
manual_rotation¶
- Type: Integer or null
- Default: null
- Values: null, 0, 90, 180, 270
- Description: Global rotation override (takes precedence over
autorotate)
Auto-Rotation¶
sidecar¶
- Type: Boolean
- Default:
false - Description: Read a per-video rotation from a same-stem
.rotfile - Format: One directive:
--video-rotate=-1,0,90,180, or270 - Example:
clip.part.mp4usesclip.part.rot - Priority: Manual CLI rotation →
.rotsidecar → filename patterns
When disabled, VBC does not inspect .rot files. A missing .rot file falls
through to patterns; an invalid or unreadable sidecar fails that video job.
The value -1 means that rotation could not be determined: VBC applies no
custom rotation and does not fall through to filename patterns.
When a sidecar supplies a rotation, VBC ignores rotation metadata embedded in
the input and applies only the sidecar value.
patterns¶
- Type: Dictionary (regex -> angle)
- Default:
{} - Description: Filename regex patterns mapped to rotation angles
- Example:
- Note: First match wins (order matters)
CLI Overrides¶
Only a subset of config keys can be overridden via CLI flags:
general.threads→--threads- encoder CQ/CRF value →
--quality(only whenquality_mode=cq) general.quality_mode→--quality-modegeneral.bps,general.minrate,general.maxrate→--bps,--minrate,--maxrate(only whenquality_mode=rate)general.gpu→--gpu/--cpugeneral.queue_sort,general.queue_seed→--queue-sort,--queue-seedgeneral.log_path→--log-pathgeneral.clean_errors→--clean-errorsgeneral.verify_fail_action→--verify-fail-actiongeneral.skip_av1→--skip-av1general.min_size_bytes→--min-sizegeneral.min_compression_ratio→--min-ratiogeneral.filter_cameras→--camerageneral.manual_rotation→--rotate-180general.debug→--debuggeneral.wait_on_finish→--wait/--no-waitgeneral.bell_on_finish→--bell/--no-bellgeneral.auto_repair_errors→ config onlygeneral.repair_corrupted_flv→ config only
Other settings (for example prefetch_factor, dynamic_quality, encoder arg lists, directory mappings, and gpu_config) must be set in YAML.
# Override threads and quality
uv run vbc /videos --threads 8 --quality 38
# Override GPU setting
uv run vbc /videos --cpu # Force CPU mode
# Override camera filtering
uv run vbc /videos --camera "ILCE-7RM5,DJI"
# Override multiple settings
uv run vbc /videos \
--config custom.yaml \
--threads 8 \
--quality 40 \
--gpu \
--skip-av1 \
--clean-errors \
--min-size 5242880 \
--rotate-180 \
--debug
Local Overrides (VBC.YAML)¶
VBC scans input directories for VBC.YAML and applies the nearest ancestor file per job.
Priority: Global config → Local VBC.YAML → CLI.
Allowed root keys: general, gpu_encoder, cpu_encoder, autorotate, cq.
For example, this local config enables .rot sidecars only for videos below its
directory:
Allowed general keys: gpu, cpu_fallback, ffmpeg_cpu_threads, copy_metadata,
use_exif, filter_cameras, dynamic_quality, quality_mode, bps, minrate, maxrate, rate_target_max_bps,
extensions, min_size_bytes, clean_errors, verify_fail_action, skip_av1, manual_rotation,
min_compression_ratio, source_policy, debug.
Special key: cq (int 0–63) overrides quality for both GPU and CPU encoder args.
Environment-Specific Configs¶
You can maintain multiple config files:
# Production (high quality, slow)
uv run vbc /videos --config conf/production.yaml
# Fast preview (low quality, fast)
uv run vbc /videos --config conf/preview.yaml
# Archival (maximum quality)
uv run vbc /videos --config conf/archive.yaml
Example conf/archive.yaml:
general:
threads: 4
gpu: false # CPU for best quality
copy_metadata: true
use_exif: true
min_compression_ratio: 0.05 # Must save at least 5%
cpu_encoder:
common_args:
- "-crf 30" # Very high quality
Validation¶
VBC uses Pydantic for config validation. Invalid model values raise ValidationError; CLI startup catches validation and helper ValueErrors and prints them as Typer error messages:
# Invalid threads (must be > 0)
Error: threads must be greater than 0
# Invalid rotation angle
Error: Invalid rotation angle 45. Must be 0, 90, 180, or 270.
Next Steps¶
- Runtime Controls - Keyboard shortcuts
- Advanced Features - Dynamic Quality, auto-rotation
- Architecture Overview - How config is loaded