Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

vhs-cli — VHS Digitization Pipeline

vhs-cli banner

License: GPLv3 Language: Bash Platform: Linux

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.


Hardware

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.
  • 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 what vhs_fix_sync.sh exists 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.
  • Game/console capture (separate game env 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/) shows decklink: Starting capture... immediately followed by decklink: 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_ROOT in 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.


Directory Layout

~/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).


Script Roles (Authoritative)

1. vhs_capture_ffmpeg.sh

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 -t to prevent runaway captures filling the drive

Key environment variables:

  • MAX_CAPTURE_DURATION — auto-stop time (default: 04:00:00, T-120 LP). Set 06:00:00 for a full EP tape.

This is the ground truth source and should never be modified.


2. vhs_denoise.sh

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.


3. vhs_process.sh

Prep from an existing file (no capture).

Pipeline:

  1. Takes an existing MKV (archival or stabilized)
  2. Runs vhs_denoise.sh (unless skipped)
  3. Runs QTGMC if needed or forced
  4. Ensures FFV1 + PCM output only
  5. Selects the correct edit input
  6. Hands off to Kdenlive

This script is used when:

  • Adjusting denoise parameters
  • Re‑running QTGMC
  • Preparing non‑captured source files

4. vhs_edit_prep_pipeline.sh

Capture + prep convenience wrapper.

Pipeline:

  1. Runs vhs_capture_ffmpeg.sh
  2. Identifies the newly captured file
  3. Calls vhs_process.sh on it

This is the normal entry point for digitizing a new tape.


5. vhs_viewer_encode.sh

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=2000k and unset V_CRF to revert to 2-pass ABR (Plex-only, no upscale planned)

This output is disposable and can be regenerated at any time.


6. vhs_upscale.sh

AI upscaling via Real‑ESRGAN.

  • Chunked, resumable processing (default 30s segments)
  • Uses realesrgan-ncnn-vulkan (Vulkan, default) or realesrgan-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 segments
  • CRUSH — 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 float
  • PRE_VF — explicit filter chain, overrides CRUSH if set. Use PRE_VF="" to disable all pre‑filtering.
  • UPSCALE_BACKENDvulkan (default) or rocm; rocm uses ~/bin/realesrgan-rocm (PyTorch+ROCm, supports realesrgan-x4plus and realesrgan-x4plus-anime). ROCm defaults to TILE_SIZE=0 (full‑frame batched inference).
  • BATCH_SIZE — frames per GPU batch for ROCm backend (default: 8; use 2 for RealESRGAN x4plus on 16 GB — deep RRDB intermediates OOM above that)

7. vhs_bw_edit_prep_pipeline.sh

Black‑and‑white capture + prep pipeline.

Same workflow as vhs_edit_prep_pipeline.sh, plus grayscale conversion:

  1. Switch to archival mode
  2. Capture archival master (FFV1/PCM)
  3. Stabilize (audio denoise)
  4. QTGMC deinterlace (if needed)
  5. Create B&W edit master (desaturate → FFV1/PCM)
  6. 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 output
  • BW_FILTER — custom filter (default: hue=s=0)

8. vhs_anime_edit_prep_pipeline.sh

Animation/anime capture + IVTC pipeline.

Note: IVTC is no longer the recommended animation workflow. Animation now uses the standard QTGMC pipeline (vhs_process.sh), with vhs_vdecimate.sh run 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:

  1. Switch to archival mode
  2. Capture archival master (FFV1/PCM)
  3. Stabilize (audio denoise)
  4. IVTC (vivtc VFM + VDecimate) — recovers original 24fps cadence
  5. Print Kdenlive command

Output: captures/stabilized/*_STABLE_IVTC.mkv

Key environment variables:

  • VS_TFF — field order (1=TFF default, 0=BFF)

9. vhs_ivtc.sh

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]

10. vhs_ivtc_decombed.sh

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)

11. vhs_vdecimate.sh

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.mkv

Note: QTGMC deinterlaces but does not remove telecine pulldown — VDecimate is always a separate required step for film-sourced content.


12. vhs_field_align.sh

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.mkv

Key environment variables:

  • VS_FIELD_SHIFT — pixels to shift (float, default: 1.0). Typical VHS values: 0.5–3.0
  • VS_SHIFT_FIELD — which field to shift: top or bottom (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.


13. vhs_upscale_bw.sh

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 include hue=s=0 for grayscale
  • BW_FILTER — explicit filter chain, overrides CRUSH if set

14. vhs_upscale_anime.sh

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:

  • MODEL defaults to realesrgan-x4plus-anime instead of realesrgan-x4plus
  • CRUSH defaults to none (hqdn3d only, no luma crush) — luma crush causes banding on flat-color cel art
  • BRIGHTNESS defaults to 0 (no uplift) — override with named levels or raw float if needed
  • DECOMB=1 — optional per‑segment IVTC + QTGMC decombing before frame extraction (slow; prefer the IVTC → QTGMC → VDecimate workflow instead)

15. vhs_viewer_encode_bw_patched.sh

B&W‑aware viewer derivative for Plex.

Enhanced version of vhs_viewer_encode.sh with B&W support and auto mode detection:

  • BW=1 forces grayscale output via hue=s=0 (or BW_FILTER override)
  • 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.


16. vhs_mode.sh

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.sh and repoints ~/Videos/ffmpeg-current to the slot's capture.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

17. backup_vhs_env.sh

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-studio and ~/.var/app/fr.handbrake.ghb
  • Timestamped backups go to ~/Videos/backups/vhs-env-<timestamp>[-slot]/
  • Slot snapshots go to ~/Videos/vhs-env/<slot>/ (used by restore_vhs_env.sh)

18. restore_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 game slot is treated as optional (no error if missing)

Codec Policy (Strict)

Archival & Intermediates

  • Video: FFV1
  • Audio: PCM (pcm_s16le)
  • Container: MKV

Viewer / Plex

  • Video: H.264
  • Audio: AAC
  • Container: MKV
  • Bitrate‑controlled (not archival)

No ProRes, no HandBrake in the master pipeline.


Typical Workflows

Digitize a New Tape (Color)

~/Videos/vhs-cli/vhs_edit_prep_pipeline.sh

Digitize a New Tape (Black & White)

~/Videos/vhs-cli/vhs_bw_edit_prep_pipeline.sh

Digitize a New Tape (Animation / Anime)

# 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 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.mkv

VDecimate 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.

Backlog Workflow (Quick Capture, Upscale Later)

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

Fix Field Misalignment (Horizontal Stepping)

# 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

Re‑run Prep on an Existing Capture

~/Videos/vhs-cli/vhs_process.sh ~/Videos/captures/archival/<file>.mkv

Upscale a Video (2× with AI)

# 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

Export Viewer Copy After Editing

# Color
~/Videos/vhs-cli/vhs_viewer_encode.sh

# Black & white
BW=1 ~/Videos/vhs-cli/vhs_viewer_encode_bw_patched.sh

Naming Final YouTube / Viewer Files

Use 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 OBS Environment

# 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 / Restore OBS Configuration

# Save current config to a slot
~/Videos/vhs-cli/backup_vhs_env.sh game

# Restore a slot
~/Videos/vhs-cli/restore_vhs_env.sh game

Resumable Upscaling

AI 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:

  1. The input video is split into short segments (default 30 seconds).
  2. Each segment is fully processed (frame extraction → Real‑ESRGAN → H.264 encode) and written as an independent checkpoint file (segments/seg_XXX.mp4).
  3. On the next run, any segment whose checkpoint file already exists is skipped.
  4. 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.


Crush Presets

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.mkv

Process Group Management (PGID)

All 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.


Blu‑ray Pipeline (Planned)

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.


Recommended Upscale Models

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.

Real-ESRGAN — xinntao (ncnn + ROCm)

Model Scale Get it from Notes
realesrgan-x4plus xinntao/Real-ESRGAN releases General live-action VHS
realesrgan-x4plus-anime xinntao/Real-ESRGAN releases Animation and cel art
realesrgan-x2plus 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.

Community VHS models — ROCm only (PyTorch .pth)

Sourced from OpenModelDB. Verify each model's license on its OpenModelDB page before redistributing.

Model Scale OpenModelDB Notes
2x_VHS-Film 2x VHS upscale and denoise Film Live-action at 2× with integrated denoise
ToonVHS-1x ToonVHS 1x Sharpen/denoise animation without resizing
VHS-Sharpen-1x VHS Sharpen 1x Sharpen/denoise live-action without resizing

Where to place models

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.

Choosing a model

Scenario Model Int. Scale Final Scale Script
Live-action VHS → 2× (default) realesrgan-x2plus vhs_upscale.sh
Live-action → 2×, more aggressive realesrgan-x4plus vhs_upscale.sh
Live-action with warm style 2x_VHS-Film vhs_upscale.sh (UPSCALE_BACKEND=rocm)
Animation after VDecimate realesrgan-x4plus-anime vhs_upscale_anime.sh
Animation, keep resolution ToonVHS-1x vhs_upscale_anime.sh (UPSCALE_BACKEND=rocm)
B&W live-action → 2× (default) realesrgan-x2plus vhs_upscale_bw.sh
Sharpen only, no resize VHS-Sharpen-1x 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.


Philosophy

  • 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

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages