Terminal version
+----+ > VHS-CLI +------------------------------------------------------------+
| |
| ##. ##. ##. ##. #######. ######. ##. ##. |
| ##: ##: ##: ##: ##...... ##...... ##: ##: |
| ##: ##: #######: #######. #####. ##: ##: ##: |
| .##. ##.. ##...##: .....##: ...... ##: ##: ##: |
| .####.. ##: ##: #######: .######. #######. ##: |
| ..... ... ... ........ ....... ........ ... |
| |
| .:######################################################:. |
| > PLAY - SP 0:00:01 TRK ===.. * REC |
+-----------------------------------------------------------------------------+
cat banner.ans in a 256-color terminal for the vibrant ANSI-colored version.
This repository contains the bash-script VHS digitization pipeline with archival intent — capture, denoise, deinterlace, viewer-encode, and AI-upscale. It's the terminal/standalone way to run the pipeline. There's also a companion GUI, vhs-gui, which wraps most of this same workflow in a desktop app (reimplemented natively in Rust rather than calling these scripts) — the two are independent, sibling projects; this repo has no dependency on that one or vice versa.
Local layout: this repo is expected to live at ~/Videos/vhs-cli/, as a sibling
of the data directories (~/Videos/captures/, ~/Videos/backups/, ~/Videos/logs/)
which are not part of this repo (or any repo) — they're local working directories
the scripts read from and write to by absolute path.
The pipeline is designed to:
- Preserve bit‑exact capture data
- Produce lossless masters (FFV1 + PCM) and retain them when storage permits
- Allow repeatable re‑processing (denoise, QTGMC) without recapture
- Produce clean, space‑efficient viewer derivatives for Plex
When storage is constrained, only the viewer/access copy is kept after editing. Scripts and codec policy are written to always produce a lossless master — whether it is retained afterward is a storage decision, not a pipeline one.
The scripts are intentionally small, single‑purpose, and composable.
This pipeline is built and run on a single Linux workstation:
- CPU: AMD Ryzen 9 5900X (12-core) — QTGMC/IVTC (VapourSynth), denoise (SoX/ffmpeg), and viewer encodes (libx264) are all CPU-bound.
- GPU: AMD Radeon RX 7800 XT (RDNA 3, gfx1101) — drives both upscale backends:
- ROCm (PyTorch, via
realesrgan-rocm) — required for the community VHS-specific models (2x_VHS-Film,ToonVHS-1x,VHS-Sharpen-1x). - Vulkan (
realesrgan-ncnn-vulkan) — limited to the model families the binary hardcodes support for (realesrgan-x4plus,-x4plus-anime,realesrnet-x4plus,realesr-animevideov3*); anything else segfaults it regardless of what's on disk — see "Choosing a model" below.
- ROCm (PyTorch, via
- VHS capture device (the flakiest link in the pipeline): MacroSilicon
MS210x USB video grabber — a low-cost USB2.0 analog capture dongle (USB ID
534d:0021, chipset commonly sold under "EasierCAP" and similar generic-brand names). Specifics that matter when something goes wrong here:- Video and audio are two independent interfaces on the same dongle,
not a synced A/V pair — video via V4L2 (720×480 YUYV422 @ 30fps), audio
via a separate onboard USB-audio ALSA card (
hw:CARD=MS210x,DEV=0, 48kHz stereo). They will drift relative to each other over a long capture; that's exactly whatvhs_fix_sync.shexists to correct after the fact. - Referenced only by its persistent
/dev/v4l/by-id/path (usb-MACROSIL_AV_TO_USB2.0-video-index0), never a bare/dev/videoN— this machine has other V4L2-capable devices (a second, unrelated USB webcam), so plain device numbering isn't stable across replugs/reboots. - If a capture looks stuck, garbled, or silently drops frames, suspect this dongle (loose USB connection, a flaky USB hub in the chain, or the device needing a replug) before the CPU/GPU or ffmpeg config.
- Video and audio are two independent interfaces on the same dongle,
not a synced A/V pair — video via V4L2 (720×480 YUYV422 @ 30fps), audio
via a separate onboard USB-audio ALSA card (
- Game/console capture (separate
gameenv slot — not used by the VHS pipeline itself): a Blackmagic Design Intensity Pro PCIe card, via OBS's DeckLink plugin. Worth noting given the MS210x's own flakiness above: the cheap USB dongle is the one that actually works. The "proper" dedicated capture card currently does not — every logged OBS session (vhs-env/game/obs-studio/logs/) showsdecklink: Starting capture...immediately followed bydecklink: Failed to enable video input, 100% of attempts across two months of logs. The likely cause visible in those same logs: OBS's decklink plugin was compiled against Desktop Video SDK 12.0, but the installed Blackmagic Desktop Video driver is 15.3/15.3.1 — a large API version gap. Not yet root-caused or fixed. - Upscale scratch storage: a secondary drive mounted at
/media/<user>/<external-drive>/Videos/—vhs_upscale_work/(chunked upscale checkpoints) lives there by default (WORK_ROOTin the upscale scripts).
None of this is hardcoded beyond the defaults above — device paths, WORK_ROOT,
UPSCALE_BACKEND, and MODEL are all environment-variable overridable per script.
~/Videos/ # data root — not part of this or any repo
├── captures/
│ ├── archival/ # Raw captures (immutable)
│ ├── stabilized/ # Denoised / QTGMC intermediates
│ └── viewer/ # Plex‑ready derivatives
├── backups/ # Timestamped config backups
├── logs/ # Per-run logs + PGID files
├── vhs-gui/ # sibling repo — github.com/RyanEiri/vhs-gui
├── plex-reencoder/ # sibling repo — github.com/RyanEiri/plex-reencoder
└── vhs-cli/ # this repo
├── vhs_capture_ffmpeg.sh
├── vhs_denoise.sh
├── denoise.sh
├── vhs_process.sh
├── vhs_edit_prep_pipeline.sh
├── vhs_bw_edit_prep_pipeline.sh
├── vhs_anime_edit_prep_pipeline.sh
├── vhs_obs_edit_prep_pipeline.sh
├── vhs_viewer_encode.sh
├── vhs_viewer_encode_bw_patched.sh
├── vhs_viewer_probe_all.sh
├── vhs_upscale.sh
├── vhs_upscale_bw.sh
├── vhs_upscale_anime.sh
├── vhs_ivtc.sh
├── vhs_ivtc_decombed.sh
├── vhs_vdecimate.sh
├── vhs_field_align.sh
├── vhs_fix_sync.sh
├── vhs_probe_crush.sh
├── vhs_mode.sh
├── backup_vhs_env.sh
├── restore_vhs_env.sh
├── vhs_qtgmc.vpy # legacy; scripts use vhs-env/tools/qtgmc.vpy
└── vhs-env/
├── archival/ffmpeg/capture.env
├── viewer/ffmpeg/capture.env
├── viewer/handbrake/viewer-presets.json
└── tools/
├── qtgmc.vpy
├── ivtc.vpy
├── ivtc_decombed.vpy
├── field_align.vpy
└── vdecimate.vpy
Two roots are used throughout: VIDEOS_DIR (data — ~/Videos, unchanged by this
split) and SCRIPTS_DIR (this repo — ~/Videos/vhs-cli, defaults baked into the
orchestrator scripts, both overridable via environment).
Capture only.
- Captures VHS from hardware
- Writes FFV1 + PCM MKV files
- Output:
captures/archival/*.mkv - No denoise, no QTGMC, no editing
- Hard duration cap via ffmpeg
-tto prevent runaway captures filling the drive
Key environment variables:
MAX_CAPTURE_DURATION— auto-stop time (default:04:00:00, T-120 LP). Set06:00:00for a full EP tape.
This is the ground truth source and should never be modified.
Audio denoise primitive.
- Removes VHS line hum / broadband noise
- Uses a short noise sample from the beginning of the tape
- Video is copied bit‑exact
- Audio rebuilt as PCM
Input: any MKV with PCM audio
Output: captures/stabilized/*_STABLE.mkv
This script is safe to re‑run at any time.
Prep from an existing file (no capture).
Pipeline:
- Takes an existing MKV (archival or stabilized)
- Runs
vhs_denoise.sh(unless skipped) - Runs QTGMC if needed or forced
- Ensures FFV1 + PCM output only
- Selects the correct edit input
- Hands off to Kdenlive
This script is used when:
- Adjusting denoise parameters
- Re‑running QTGMC
- Preparing non‑captured source files
Capture + prep convenience wrapper.
Pipeline:
- Runs
vhs_capture_ffmpeg.sh - Identifies the newly captured file
- Calls
vhs_process.shon it
This is the normal entry point for digitizing a new tape.
Viewer derivative for Plex.
- Reads:
captures/stabilized/EDIT_MASTER.mkv - Writes:
captures/viewer/EDIT_MASTER.viewer.mkv - H.264 video, AAC audio
- VHS mode default: CRF 18 single-pass — clean enough to use as an upscale source
- HD mode default: CRF 20
- Set
V_BK=2000kand unsetV_CRFto revert to 2-pass ABR (Plex-only, no upscale planned)
This output is disposable and can be regenerated at any time.
AI upscaling via Real‑ESRGAN.
- Chunked, resumable processing (default 30s segments)
- Uses
realesrgan-ncnn-vulkan(Vulkan, default) orrealesrgan-rocm(ROCm/PyTorch, opt‑in) - Internal upscale at 4× then downscale to 2× final resolution
- Segment checkpoints allow resume after interruption
- Safety guard prevents mixing segments with different settings or backends
Input: any video file Output: upscaled H.264 + AAC (viewer‑quality)
Usage:
./vhs_upscale.sh INPUT OUTPUT [segment_seconds] [crf]Key environment variables:
MODEL— Real‑ESRGAN model (default:realesrgan-x4plus)INTERNAL_SCALE/FINAL_SCALE— scale factors (default: 4 / 2)CRF— H.264 quality (default: 21)WORK_ROOT— working directory for segmentsCRUSH— crush preset (see Crush Presets below)BRIGHTNESS— brightness adjustment; accepts named levels (none=0,low=0.02,medium=0.05,high=0.095) or a raw floatPRE_VF— explicit filter chain, overridesCRUSHif set. UsePRE_VF=""to disable all pre‑filtering.UPSCALE_BACKEND—vulkan(default) orrocm;rocmuses~/bin/realesrgan-rocm(PyTorch+ROCm, supportsrealesrgan-x4plusandrealesrgan-x4plus-anime). ROCm defaults toTILE_SIZE=0(full‑frame batched inference).BATCH_SIZE— frames per GPU batch for ROCm backend (default: 8; use2for RealESRGAN x4plus on 16 GB — deep RRDB intermediates OOM above that)
Black‑and‑white capture + prep pipeline.
Same workflow as vhs_edit_prep_pipeline.sh, plus grayscale conversion:
- Switch to archival mode
- Capture archival master (FFV1/PCM)
- Stabilize (audio denoise)
- QTGMC deinterlace (if needed)
- Create B&W edit master (desaturate → FFV1/PCM)
- Print Kdenlive command
Output: captures/stabilized/*_STABLE[_QTGMC]_BW.mkv
The grayscale filter (hue=s=0) removes color while preserving the archival FFV1 + PCM codec policy.
Key environment variables:
BW_FORCE=1— overwrite existing B&W outputBW_FILTER— custom filter (default:hue=s=0)
Animation/anime capture + IVTC pipeline.
Note: IVTC is no longer the recommended animation workflow. Animation now uses the standard QTGMC pipeline (
vhs_process.sh), withvhs_vdecimate.shrun after Kdenlive editing before upscaling. This script is retained as a utility for cases where IVTC is specifically needed.
Same workflow as vhs_edit_prep_pipeline.sh, but replaces QTGMC deinterlacing with inverse telecine (IVTC) for animated content that was originally 24fps film telecined to 30fps NTSC:
- Switch to archival mode
- Capture archival master (FFV1/PCM)
- Stabilize (audio denoise)
- IVTC (vivtc VFM + VDecimate) — recovers original 24fps cadence
- Print Kdenlive command
Output: captures/stabilized/*_STABLE_IVTC.mkv
Key environment variables:
VS_TFF— field order (1=TFF default, 0=BFF)
Standalone IVTC runner.
Runs inverse telecine on an existing stabilized file (no capture, no denoise):
- Uses
vhs-env/tools/ivtc.vpy(vivtc VFM + VDecimate) - Output: FFV1 + PCM (archival codec policy)
- Converts 30fps telecined → 24fps progressive
Input: any *_STABLE.mkv
Output: *_IVTC.mkv
Usage:
./vhs_ivtc.sh INPUT_STABLE.mkv [OUTPUT_IVTC.mkv]IVTC with selective QTGMC decombing for combed frames.
Runs inverse telecine (VFM field matching) followed by selective QTGMC deinterlacing on frames that VFM couldn't cleanly field‑match. VDecimate then removes duplicate frames (30fps → 24fps).
- Uses
vhs-env/tools/ivtc_decombed.vpy - Output: FFV1 + PCM (archival codec policy)
- More aggressive than plain IVTC — handles per‑frame combing artifacts
Note: For best results on animation, prefer the IVTC → QTGMC → VDecimate pipeline (see Typical Workflows) which runs faster and produces cleaner output.
Input: any *_STABLE.mkv
Output: *_IVTC_DECOMBED.mkv
Usage:
./vhs_ivtc_decombed.sh INPUT_STABLE.mkv [OUTPUT_IVTC_DECOMBED.mkv]Key environment variables:
VS_TFF— field order (1=TFF default, 0=BFF)VS_DECOMB_PRESET— QTGMC preset for decombing (default:Fast)
Remove telecine duplicate frames (30fps → 24fps).
Runs VapourSynth VDecimate on a progressive file to remove the duplicate frames introduced by the 3:2 pulldown telecine process. Required for any VHS tape sourced from 24fps film — both animation and commercial film releases.
- Uses
vhs-env/tools/vdecimate.vpy - Output: FFV1 + PCM, 24fps
- Run after QTGMC, before upscaling
Input: QTGMC-processed progressive MKV
Output: *_VD.mkv
Usage:
./vhs_vdecimate.sh EDIT_MASTER.mkv EDIT_MASTER_VD.mkvNote: QTGMC deinterlaces but does not remove telecine pulldown — VDecimate is always a separate required step for film-sourced content.
Correct interlaced field misalignment (horizontal stepping).
VHS playback hardware can introduce a static horizontal offset between the two interlaced fields, producing a stair‑step pattern on vertical edges. This script corrects the misalignment by separating the fields, applying a sub‑pixel horizontal shift to one field via high‑quality resampling (Spline36), then re‑weaving.
- Uses
vhs-env/tools/field_align.vpy - Output: FFV1 + PCM (archival codec policy)
- Should be run before QTGMC or IVTC (on the interlaced denoised file)
Input: any *_STABLE.mkv (interlaced)
Output: *_ALIGNED.mkv
Usage:
# Default: shift bottom field 1.0px rightward
./vhs_field_align.sh INPUT_STABLE.mkv [OUTPUT_ALIGNED.mkv]
# Adjust shift amount (positive = right, negative = left)
VS_FIELD_SHIFT=1.5 ./vhs_field_align.sh INPUT_STABLE.mkv
# Shift top field instead
VS_FIELD_SHIFT=-0.5 VS_SHIFT_FIELD=top ./vhs_field_align.sh INPUT_STABLE.mkvKey environment variables:
VS_FIELD_SHIFT— pixels to shift (float, default:1.0). Typical VHS values: 0.5–3.0VS_SHIFT_FIELD— which field to shift:toporbottom(default:bottom)VS_TFF— field order (1=TFF default, 0=BFF)
Tip: Try a short clip with different VS_FIELD_SHIFT values to find the right offset for your deck. The offset is usually consistent across all tapes from the same VCR.
Black‑and‑white AI upscaling via Real‑ESRGAN.
Same chunked, resumable pipeline as vhs_upscale.sh, adapted for B&W content:
- Extracts frames as grayscale (stored as RGB JPG for Real‑ESRGAN compatibility)
- Upscales with neutral chroma preserved throughout
- Same resume/safety‑guard behavior as the color variant
Input: any B&W video file Output: upscaled H.264 + AAC (viewer‑quality)
Usage:
./vhs_upscale_bw.sh INPUT OUTPUT [segment_seconds] [crf]Additional environment variables:
CRUSH— crush preset (see Crush Presets below); all presets includehue=s=0for grayscaleBW_FILTER— explicit filter chain, overridesCRUSHif set
Animation/anime AI upscaling via Real‑ESRGAN.
Same chunked, resumable pipeline as vhs_upscale.sh, using the realesrgan-x4plus-anime model which is trained on drawn/cel content (cartoons, anime, hand‑drawn material).
Input: any animation/anime video file (ideally 24fps progressive from IVTC → QTGMC → VDecimate) Output: upscaled H.264 + AAC (viewer‑quality)
Usage:
./vhs_upscale_anime.sh INPUT OUTPUT [segment_seconds] [crf]Key differences from vhs_upscale.sh:
MODELdefaults torealesrgan-x4plus-animeinstead ofrealesrgan-x4plusCRUSHdefaults tonone(hqdn3d only, no luma crush) — luma crush causes banding on flat-color cel artBRIGHTNESSdefaults to0(no uplift) — override with named levels or raw float if neededDECOMB=1— optional per‑segment IVTC + QTGMC decombing before frame extraction (slow; prefer the IVTC → QTGMC → VDecimate workflow instead)
B&W‑aware viewer derivative for Plex.
Enhanced version of vhs_viewer_encode.sh with B&W support and auto mode detection:
BW=1forces grayscale output viahue=s=0(orBW_FILTERoverride)- Auto‑detects SD (≤576p → 2‑pass ABR @ 2000 kb/s, 640×480) vs HD (>576p → CRF encode)
- Optional deinterlace control (
DEINTERLACE=auto|on|off) - Upgraded audio: AAC 320 kb/s, twoloop coder, 20 kHz cutoff
Usage:
BW=1 ./vhs_viewer_encode_bw_patched.sh [INPUT [OUTPUT.mkv]]Falls back to the newest .mkv in captures/stabilized/ if no input is specified.
Environment switcher.
Switches the active OBS, HandBrake, and ffmpeg configuration to a named mode:
./vhs_mode.sh {archival|viewer|game} [--launch]- archival / viewer — Restores the OBS + HandBrake slot via
restore_vhs_env.shand repoints~/Videos/ffmpeg-currentto the slot'scapture.env - game — Restores the game OBS slot (if it exists) and optionally launches OBS with the configured game‑capture profile/collection (
--launch) - Archival mode also verifies the active ffmpeg has the FFV1 encoder
Key environment variables:
OBS_PROFILE_GAME/OBS_COLLECTION_GAME— OBS profile and scene collection for game mode
Save current OBS + HandBrake configuration.
Creates a timestamped backup and optionally updates a named slot snapshot:
./backup_vhs_env.sh # timestamped backup only
./backup_vhs_env.sh archival # + update archival slot
./backup_vhs_env.sh viewer # + update viewer slot
./backup_vhs_env.sh game # + update game slot- Backs up
~/.config/obs-studioand~/.var/app/fr.handbrake.ghb - Timestamped backups go to
~/Videos/backups/vhs-env-<timestamp>[-slot]/ - Slot snapshots go to
~/Videos/vhs-env/<slot>/(used byrestore_vhs_env.sh)
Restore OBS + HandBrake configuration.
Restores from a named slot or a timestamped backup:
./restore_vhs_env.sh archival # restore from slot
./restore_vhs_env.sh viewer
./restore_vhs_env.sh game
./restore_vhs_env.sh <path> # restore from specific backup directory
./restore_vhs_env.sh # restore from most recent backup- Moves the current config aside (
.PRE-RESTORE.*) before overwriting - Refuses to run if OBS or HandBrake are currently open
- The
gameslot is treated as optional (no error if missing)
- Video: FFV1
- Audio: PCM (pcm_s16le)
- Container: MKV
- Video: H.264
- Audio: AAC
- Container: MKV
- Bitrate‑controlled (not archival)
No ProRes, no HandBrake in the master pipeline.
~/Videos/vhs-cli/vhs_edit_prep_pipeline.sh~/Videos/vhs-cli/vhs_bw_edit_prep_pipeline.sh# Animation uses the standard QTGMC workflow — same as color
~/Videos/vhs-cli/vhs_edit_prep_pipeline.sh
# or, to skip Kdenlive launch:
NO_LAUNCH=1 ~/Videos/vhs-cli/vhs_process.sh VHS_ARCHIVAL_<timestamp>.mkv
# After Kdenlive editing, run VDecimate before upscaling (see Animation Upscale Pipeline)Animation EDIT_MASTERs have already been through QTGMC at denoise+QTGMC time (FORCE_QTGMC=1 default in vhs_process.sh). The post-edit upscale pipeline is two steps:
# 1. VDecimate — remove 3:2 pulldown duplicate frames (30fps → 24fps)
~/Videos/vhs-cli/vhs_vdecimate.sh EDIT_MASTER-TITLE.mkv EDIT_MASTER-TITLE_VD.mkv
# 2. Upscale with anime model (ROCm backend, no luma crush)
UPSCALE_BACKEND=rocm BATCH_SIZE=2 \
~/Videos/vhs-cli/vhs_upscale_anime.sh EDIT_MASTER-TITLE_VD.mkv \
~/Videos/captures/viewer/VHS\ Trailer\ —\ Title.mkvVDecimate is also required for commercial film VHS tapes (live action films on VHS were telecined from 24fps just like animation). Run vhs_vdecimate.sh on the EDIT_MASTER before upscaling any film-sourced content. Native 30fps video (home video, TV news) does not need it.
When working through a backlog of tapes, produce a viewer copy immediately and defer upscaling:
# 1. Capture → denoise + QTGMC as normal
NO_LAUNCH=1 ~/Videos/vhs-cli/vhs_process.sh VHS_ARCHIVAL_<timestamp>.mkv
# 2. Produce viewer copy (CRF 18 — clean enough for later upscaling)
~/Videos/vhs-cli/vhs_viewer_encode.sh EDIT_MASTER-TITLE.mkv
# → captures/viewer/EDIT_MASTER-TITLE.viewer.mkv (Plex-ready immediately)
# 3. Later: upscale from viewer encode
~/Videos/vhs-cli/vhs_upscale.sh captures/viewer/EDIT_MASTER-TITLE.viewer.mkv \
captures/viewer/TITLE.upscale.mkv# Correct stepping before IVTC or QTGMC
VS_FIELD_SHIFT=1.5 ~/Videos/vhs-cli/vhs_field_align.sh ~/Videos/captures/stabilized/seg001_STABLE.mkv
# Then run IVTC (animation) or QTGMC (live action) on the aligned file
~/Videos/vhs-cli/vhs_ivtc.sh ~/Videos/captures/stabilized/seg001_STABLE_ALIGNED.mkv~/Videos/vhs-cli/vhs_process.sh ~/Videos/captures/archival/<file>.mkv# Color
~/Videos/vhs-cli/vhs_upscale.sh input.mkv output_upscaled.mp4
# Black & white
~/Videos/vhs-cli/vhs_upscale_bw.sh input_bw.mkv output_upscaled_bw.mp4
# Animation / anime
~/Videos/vhs-cli/vhs_upscale_anime.sh input_anime.mkv output_upscaled_anime.mp4# Color
~/Videos/vhs-cli/vhs_viewer_encode.sh
# Black & white
BW=1 ~/Videos/vhs-cli/vhs_viewer_encode_bw_patched.shUse an em dash (—) as the separator between the type prefix and the title:
VHS Trailer — Dances with Wolves.mkv
VHS Commercial — Disney World.mkv
VHS Commercials — Walt Disney Home Video.mkv
VHS Special Features — Fun and Fancy Free.mkv
Drop the .upscale pipeline suffix from the final filename. Hyphen-minus (-) is stripped by YouTube's upload sanitization; em dash survives. Internal pipeline files (EDIT_MASTERs, _VD.mkv, in-progress outputs) keep the all-caps underscore convention.
# Switch to archival capture settings
~/Videos/vhs-cli/vhs_mode.sh archival
# Switch to game capture and launch OBS
~/Videos/vhs-cli/vhs_mode.sh game --launch# Save current config to a slot
~/Videos/vhs-cli/backup_vhs_env.sh game
# Restore a slot
~/Videos/vhs-cli/restore_vhs_env.sh gameAI upscaling is the slowest step in the pipeline — a single 80‑minute tape can take hours of GPU time through Real‑ESRGAN. Most upscaling tools (including GUI applications like chaiNNer) treat the entire job as a single atomic operation: if the process crashes, the GPU driver resets, or you simply need to shut down, you lose all progress and start from scratch.
The vhs_upscale*.sh scripts solve this with a chunked, segment‑based checkpoint
system:
- The input video is split into short segments (default 30 seconds).
- Each segment is fully processed (frame extraction → Real‑ESRGAN → H.264 encode)
and written as an independent checkpoint file (
segments/seg_XXX.mp4). - On the next run, any segment whose checkpoint file already exists is skipped.
- After all segments complete, they are concatenated and the original audio is muxed in.
This means:
- Interruption is free. Kill the process at any time; completed segments are preserved.
- Resume is automatic. Re‑run the same command and it picks up where it left off.
- Progress is visible. Each segment logs independently, and you can count checkpoint files to gauge completion.
A configuration fingerprint (run_config.txt) is written alongside the segments.
If you change settings (model, CRF, scale factor, etc.) between runs, the script refuses
to continue rather than silently mixing segments from different configurations. Override
with ALLOW_MIXED=1 if intentional.
All upscale scripts (vhs_upscale.sh, vhs_upscale_bw.sh, vhs_upscale_anime.sh) support a CRUSH environment variable that selects a pre‑filtering preset applied during frame extraction, before Real‑ESRGAN sees the frames. The filters denoise shadow noise and crush dark values so the upscaler doesn't hallucinate texture in noisy black regions.
Default for all scripts: CRUSH=none — hqdn3d denoising only, no luma remap, no brightness adjustment. Use small/medium/heavy only when a specific tape's noise floor needs correction.
| Preset | Threshold | Brightness default | Use case |
|---|---|---|---|
none (default) |
— | 0 | hqdn3d only — no luma crush, no brightness uplift |
small |
16 | 0 | Crushes below TV black level; tapes with noisy blacks |
medium |
50 | 0.05 | Darker/noisier tapes needing moderate cleanup |
heavy |
70 | 0.095 | Very noisy tapes, heavy shadow noise |
BRIGHTNESS accepts named levels: none=0, low=0.02, medium=0.05, high=0.095, or a raw float. Named levels override the preset default without changing the crush threshold.
All presets include hqdn3d=3:2:4:3 denoise and a ramped luma crush: values below the threshold are zeroed, and the remaining range is smoothly remapped to 0–255 (no hard clipping). The B&W script additionally includes hue=s=0 for grayscale conversion in every preset.
The BRIGHTNESS environment variable overrides the preset's default brightness without changing the crush level. This is useful for fine‑tuning per‑tape without building a full custom PRE_VF.
Usage:
# Default (no crush, no brightness — hqdn3d only)
~/Videos/vhs-cli/vhs_upscale.sh input.mkv output.mkv
# Medium crush
CRUSH=medium ~/Videos/vhs-cli/vhs_upscale_bw.sh input_bw.mkv output_bw.mkv
# Heavy crush with custom brightness override
CRUSH=heavy BRIGHTNESS=0.12 ~/Videos/vhs-cli/vhs_upscale_anime.sh input_anime.mkv output_anime.mkv
# Small crush with brightness bump
BRIGHTNESS=0.08 ~/Videos/vhs-cli/vhs_upscale.sh input.mkv output.mkv
# Fully custom (overrides CRUSH entirely)
PRE_VF="hqdn3d=3:2:4:3,lutyuv=y='if(lt(val,35),0,min(255,(val-35)*255/220))',eq=brightness=0.1" \
~/Videos/vhs-cli/vhs_upscale.sh input.mkv output.mkv
# Disable all pre-filtering
PRE_VF="" ~/Videos/vhs-cli/vhs_upscale.sh input.mkv output.mkvAll pipeline scripts write a process group ID (PGID) file on startup and clean it up on exit. This enables reliable pause, resume, and stop of any running pipeline step from an external shell or automation tool.
| Script | PGID file |
|---|---|
vhs_capture_ffmpeg.sh |
logs/capture.pgid |
vhs_qtgmc_only.sh |
logs/qtgmc.pgid |
vhs_ivtc.sh |
logs/ivtc.pgid |
vhs_ivtc_decombed.sh |
logs/ivtc_decombed.pgid |
vhs_field_align.sh |
logs/field_align.pgid |
vhs_upscale*.sh |
<work_dir>/upscale.pgid |
Usage:
# Pause a running capture
kill -STOP -$(cat ~/Videos/logs/capture.pgid)
# Resume
kill -CONT -$(cat ~/Videos/logs/capture.pgid)
# Gracefully stop
kill -INT -$(cat ~/Videos/logs/capture.pgid)The negative PID in kill targets the entire process group, ensuring child processes (ffmpeg, vspipe, realesrgan) are also signaled.
A Blu‑ray ripping and re‑encoding pipeline is planned to complement the VHS workflow, producing the same archival and viewer derivative structure.
Hardware: The Blu‑ray drive is physically installed on a Proxmox host and is accessible from an NFS VM on the local network. The drive is currently disconnected.
Model files are not included in this repository due to size. The table below lists every model used in production, where to obtain it, and which backend it requires.
| Model | Scale | Get it from | Notes |
|---|---|---|---|
realesrgan-x4plus |
4× | xinntao/Real-ESRGAN releases | General live-action VHS |
realesrgan-x4plus-anime |
4× | xinntao/Real-ESRGAN releases | Animation and cel art |
realesrgan-x2plus |
2× | xinntao/Real-ESRGAN releases | When 4× is too aggressive |
License: BSD 3-Clause. Both .pth (ROCm) and ncnn .param/.bin variants are on the releases page.
Sourced from OpenModelDB. Verify each model's license on its OpenModelDB page before redistributing.
| Model | Scale | OpenModelDB | Notes |
|---|---|---|---|
2x_VHS-Film |
2× | 2x VHS upscale and denoise Film | Live-action at 2× with integrated denoise |
ToonVHS-1x |
1× | ToonVHS 1x | Sharpen/denoise animation without resizing |
VHS-Sharpen-1x |
1× | VHS Sharpen 1x | Sharpen/denoise live-action without resizing |
ROCm backend (.pth): ~/opt/realesrgan-rocm/models/. Each model also needs an entry in MODEL_MAP in ~/opt/realesrgan-rocm/driver.py.
Vulkan/ncnn backend (.param + .bin): ~/opt/realesrgan-ncnn/models/. realesrgan-ncnn-vulkan hardcodes its network architecture by matching the model name against realesrgan-x4plus, realesrgan-x4plus-anime, realesrnet-x4plus, or realesr-animevideov3* — any other name segfaults it, even with valid .param/.bin files present. The upscale scripts and vhs-gui only discover/offer names in that set for the Vulkan backend; the community VHS models below are ROCm-only regardless of what files exist in this directory.
Don't want to hunt these down individually? All of the above — official
and community models, both .pth and ncnn .param/.bin formats — are
packaged as ready-to-extract archives on the
models-v1 release,
with a MODEL-CREDITS.md documenting each model's license and original
author.
| Scenario | Model | Int. Scale | Final Scale | Script |
|---|---|---|---|---|
| Live-action VHS → 2× (default) | realesrgan-x2plus |
2× | 2× | vhs_upscale.sh |
| Live-action → 2×, more aggressive | realesrgan-x4plus |
4× | 2× | vhs_upscale.sh |
| Live-action with warm style | 2x_VHS-Film |
2× | 2× | vhs_upscale.sh (UPSCALE_BACKEND=rocm) |
| Animation after VDecimate | realesrgan-x4plus-anime |
4× | 2× | vhs_upscale_anime.sh |
| Animation, keep resolution | ToonVHS-1x |
1× | 1× | vhs_upscale_anime.sh (UPSCALE_BACKEND=rocm) |
| B&W live-action → 2× (default) | realesrgan-x2plus |
2× | 2× | vhs_upscale_bw.sh |
| Sharpen only, no resize | VHS-Sharpen-1x |
1× | 1× | vhs_upscale.sh (UPSCALE_BACKEND=rocm) |
realesrgan-x2plus is the preferred neutral model for 2× output. It runs faster than routing through realesrgan-x4plus at Int 4× / Final 2× and produces equivalent quality at the target resolution. Use realesrgan-x4plus when you want the extra sharpening headroom of a 4× internal pass.
2x_VHS-Film introduces a warm/amber color shift that is a model artifact, not source-accurate. Suitable when the master will be retained; avoid when the viewer copy may become the only surviving copy.
- Capture once — raw archival masters are ground truth and should never be modified.
- Process many times — denoise, QTGMC, and viewer encodes are repeatable from the archival master.
- Retain masters when storage allows — the pipeline always produces a lossless master; whether it is kept afterward depends on available storage. When space is constrained, only the viewer copy is retained.
- Viewer copies are processed for watchability — deinterlacing, AI upscaling, luma conditioning, and brightness adjustment are applied to the access copy only. These are not part of the archival record.
- Editing is cuts‑only — no color grading, dropout repair, or image stabilization. VHS artifacts are preserved, not corrected.
- Viewer files are disposable; masters are the goal.
Last updated: June 2026
