This repository / directory contains a VHS digitization pipeline with archival intent. 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.
~/Videos/
├── captures/
│ ├── archival/ # Raw captures (immutable)
│ ├── stabilized/ # Denoised / QTGMC intermediates
│ └── viewer/ # Plex‑ready derivatives
├── vhs-gui/ # Rust capture/playback GUI (egui + libmpv2)
├── vhs-env/
│ ├── archival/ # OBS/HandBrake slot: archival capture
│ ├── viewer/ # OBS/HandBrake slot: viewer capture
│ └── game/ # OBS/HandBrake slot: game capture
├── backups/ # Timestamped config backups
├── vhs_capture_ffmpeg.sh
├── vhs_denoise.sh
├── vhs_process.sh
├── vhs_edit_prep_pipeline.sh
├── vhs_bw_edit_prep_pipeline.sh
├── vhs_viewer_encode.sh
├── vhs_upscale.sh
├── vhs_upscale_bw.sh
├── vhs_upscale_anime.sh
├── vhs_anime_edit_prep_pipeline.sh
├── vhs_ivtc.sh
├── vhs_vdecimate.sh
├── vhs_field_align.sh
├── vhs_ivtc_decombed.sh
├── vhs_viewer_encode_bw_patched.sh
├── vhs_mode.sh
├── backup_vhs_env.sh
├── restore_vhs_env.sh
├── vhs_qtgmc.vpy
├── vhs-env/tools/ivtc.vpy
├── vhs-env/tools/ivtc_decombed.vpy
├── vhs-env/tools/field_align.vpy
└── vhs-env/tools/vdecimate.vpy
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)
A native Rust desktop application that wraps the full digitization pipeline — capture, processing, playback, and AI upscaling — in a single window. Intended as the primary interface for day-to-day tape digitization.
Build:
cd vhs-gui
cargo build # debug
cargo build --releaseLaunch:
DISPLAY=:0 ./vhs-gui/target/debug/vhs-guiStack: eframe 0.34 (glow/OpenGL via Wayland/winit) + egui for UI; libmpv2 render API for video (off-screen FBO, GLSL blit shader); nix for SIGINT/SIGSTOP/SIGCONT to process groups; image (JPEG decode) for upscale preview; trash for recoverable deletes.
What it does:
- Monitor — opens the V4L2 capture device directly in mpv for a live signal check before recording. The device is released cleanly once capture starts.
- Start Capture — spawns
vhs_capture_ffmpeg.sh. The V4L2 device is released from mpv first (Releasing state, 1 s timeout), then ffmpeg starts. As soon as the archival FFV1 file appears on disk it is opened immediately in the embedded player. When mpv reaches the current write position (EOF on the growing file) it automatically reopens atduration − 2 sto keep the preview near the live position — this repeats on every EOF, creating a rolling near-live window. - Stop Capture — sends SIGINT to the capture process group; exit 130 is treated as success.
- Library panel — five sections in display order: Viewer → Edit Master (VD) → Edit Master → Stabilized → Archival. Click any entry to open it in the player.
- Pipeline actions — per-section buttons launch Denoise, QTGMC, IVTC, VDecimate, Viewer Encode, and all upscale variants as background jobs. Only one job runs at a time; buttons are disabled while busy.
- Upscale jobs — always launched with
UPSCALE_BACKEND=rocm BATCH_SIZE=2. Show dual progress bars: total (completed segments / total) and segment (upscaled frames / extracted frames, driven by counting files inframes_up/vsframes/). Buttons: Pause (SIGSTOP), Resume (SIGCONT), Stop after Segment (sends SIGINT at next segment boundary; bypassed if all segments are already done so the concat/mux phase completes), Cancel (immediate SIGINT). Work dir is deleted automatically after the output file is confirmed on disk. - Upscale preview — while an upscale job runs the central panel shows a side-by-side comparison: "Original 720×480" (left) vs "Upscaled 4×" (right), updated every 4 seconds. Frames are matched by filename across
frames/andframes_up/. Arequest_repaint_after(1 s)heartbeat keeps the loop alive when mpv is idle. - Rename — Viewer files get a Rename… button that pre-fills an editable field with a human-readable title suggestion:
EDIT_MASTER-VHS_TRAILER-THE_GREAT_MOUSE_DETECTIVE_VD.upscale.mkv→VHS Trailer — The Great Mouse Detective.mkv. Strips pipeline suffixes/prefixes, converts underscores to spaces, title-cases with acronym preservation (VHS, TV, BBC, DVD, etc.), formats with em dash. - Delete — moves files to
~/.local/share/Trash/(FreeDesktop.org spec) via thetrashcrate; recoverable from any file manager. Confirmation prompt reads "Move to Trash?". - Playback controls — click-to-pause; time / remaining OSD; same mpv instance reused for all sources.
Capture state machine: Idle → Monitoring → Releasing (1 s timeout) → Capturing. V4L2 device is exclusive to ffmpeg during capture; mpv returns to idle during Releasing then switches to the growing archival file once capture begins.
Application icon: 256×256 RGBA VHS cassette embedded in the binary (assets/icon.png, decoded with the png crate).
System dependency note: Requires libmpv2 runtime (installed via dpkg --force-depends due to libcaca PPA conflict; libmpv-dev headers are not needed after the build).
- 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_edit_prep_pipeline.sh~/Videos/vhs_bw_edit_prep_pipeline.sh# Animation uses the standard QTGMC workflow — same as color
~/Videos/vhs_edit_prep_pipeline.sh
# or, to skip Kdenlive launch:
NO_LAUNCH=1 ~/Videos/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_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_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_process.sh VHS_ARCHIVAL_<timestamp>.mkv
# 2. Produce viewer copy (CRF 18 — clean enough for later upscaling)
~/Videos/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_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_field_align.sh ~/Videos/captures/stabilized/seg001_STABLE.mkv
# Then run IVTC (animation) or QTGMC (live action) on the aligned file
~/Videos/vhs_ivtc.sh ~/Videos/captures/stabilized/seg001_STABLE_ALIGNED.mkv~/Videos/vhs_process.sh ~/Videos/captures/archival/<file>.mkv# Color
~/Videos/vhs_upscale.sh input.mkv output_upscaled.mp4
# Black & white
~/Videos/vhs_upscale_bw.sh input_bw.mkv output_upscaled_bw.mp4
# Animation / anime
~/Videos/vhs_upscale_anime.sh input_anime.mkv output_upscaled_anime.mp4# Color
~/Videos/vhs_viewer_encode.sh
# Black & white
BW=1 ~/Videos/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_mode.sh archival
# Switch to game capture and launch OBS
~/Videos/vhs_mode.sh game --launch# Save current config to a slot
~/Videos/backup_vhs_env.sh game
# Restore a slot
~/Videos/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.
| Preset | Threshold | Brightness default | Use case |
|---|---|---|---|
none |
— | 0 | Animation default — hqdn3d only, no luma crush (avoids banding on cel art) |
small (live action default) |
16 | 0 | Most content — crushes below TV black level |
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 (small crush, no brightness)
~/Videos/vhs_upscale.sh input.mkv output.mkv
# Medium crush
CRUSH=medium ~/Videos/vhs_upscale_bw.sh input_bw.mkv output_bw.mkv
# Heavy crush with custom brightness override
CRUSH=heavy BRIGHTNESS=0.12 ~/Videos/vhs_upscale_anime.sh input_anime.mkv output_anime.mkv
# Small crush with brightness bump
BRIGHTNESS=0.08 ~/Videos/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_upscale.sh input.mkv output.mkv
# Disable all pre-filtering
PRE_VF="" ~/Videos/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 the Proxmox node shakyamuni.buddha.lan and is accessible from the files.buddha.lan VM. The drive is currently disconnected.
- 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: May 2026