V2TSR icon

V2TSR

Remasters recordings to 4K. Here is everything it does to a video, in order, and every setting that changes it.

What it is

V2TSR (short for Vid2Text-SR) takes a podcast recording and makes a cleaner, sharper 4K version of it. It cleans up sensor noise, can correct exposure, applies a colour look, sharpens, can raise the frame rate, and encodes on the graphics card.

It comes in two layers. The pipeline is a command-line script built on FFmpeg, and it is the single source of truth for how a video gets rendered. The desktop app is a small window on top of it: pick videos, pick a look, press Start. The app builds the exact command you would type yourself, so anything the app makes, the command line can make too.

It runs on a Windows laptop with an RTX 4050 Laptop GPU, an 8-core Intel Core Ultra 7 and 16 GB of RAM. Every number on this page was measured on that machine.

The same video frame twice: on the left a plain crop and upscale, on the right the same frame after the V2TSR Unprompted chain, with less blotchy noise and slightly more contrast.
Plain crop, scale and light sharpenV2TSR Unprompted chain
The same frame from an Unprompted episode, cropped close and blown up about 2.4x. The right side has less blotchy noise in the skin and the grey wall and a touch more contrast. The difference is deliberately small: the goal is a natural camera look, not a filter.

The chain

Before anything renders, V2TSR reads the file (size, frame rate, duration, codec, rotation, so portrait stays portrait) and proves which encoder works by test-encoding 10 frames on each candidate: NVIDIA first, then Intel, then AMD, then the CPU. It does the same for GPU decoding and GPU scaling. Nothing is assumed.

Default order

  1. 1
    Format8-bit 4:2:0, so every filter after it sees the same kind of frame.
  2. 2
    Denoisehqdn3d, which smooths noise within a frame and across neighbouring frames.
  3. 3
    ExposureA brightness curve from a whole-clip measurement (only with --grade auto).
  4. 4
    LookThe colour grade you picked.
  5. 5
    Sharpenunsharp, at the strength the detail preset sets.
  6. 6
    Frame rateUp to 60 fps with RIFE or minterpolate, if the source is slower.
  7. 7
    UpscaleTo fit inside 3840×2160 (or 2160×3840 for portrait), on the GPU when it can.

Unprompted profile order (scale first)

  1. 1
    FormatSame as the default.
  2. 2
    Denoise at source sizehqdn3d=0.5:0.5:8:6: light within a frame, medium across frames.
  3. 3
    Upscale to 4KCPU Lanczos, so the next two steps can still run after it.
  4. 4
    Gradeeq=contrast=1.04:saturation=1.05, a gentle lift.
  5. 5
    Sharpen lastunsharp=5:5:0.6:5:5:0.0 on the 4K frame.
  6. 6
    Keep the source frame rateNo interpolation: 30 fps in, 30 fps out.

Why this order

  • Clean before anything else. Denoising, exposure and sharpening all run at the source size in the default chain. That's far cheaper than at 4K, and the upscaler then fills in already-clean pixels instead of enlarging noise.
  • Exposure before sharpening, so the sharpen amount applies to the final contrast, not to a frame that's about to get brighter.
  • Frame rate before the upscale. Interpolating at 4K runs at about 0.35 fps. Moving it before the upscale took a 4-second test clip from 10:17 to 1:38, then to 1:04 once the upscale moved to the GPU.
  • The GPU scaler has to be last. Once frames are on the graphics card, FFmpeg's other filters can't touch them. That's why the Unprompted profile, which grades and sharpens after scaling, uses the CPU Lanczos scaler instead.

Getting the video in

By default the source is streamed through a text (Base64) copy in 50 MB chunks and decoded back on the fly. Base64 is about 1.33x the size of the video. If the file's index sits at the start it's piped straight into FFmpeg; if not, it's decoded to a temporary file first. --direct-input skips the round trip and reads the file directly, which saves about 2.3x the source size on disk. The app always uses the round trip.

The exposure measurement (--grade auto)

It builds one brightness histogram for the whole clip from frames shrunk to 320 px wide, 4 per second. The black point is the darkest 0.5% and the white point the brightest 0.5%, aiming the middle brightness at 0.42 with the gain capped at 1.6x and the curve's gamma held between 0.60 and 1.15. Default strength is 0.8.

Guards keep it from doing harm. A clip with almost no tonal range (a title card or a fade) is left alone. The black point never goes past 0.25. The correction backs off if it would make the middle worse. A clip that's already well exposed isn't touched.

Looks

The look is the colour grade, set with --look (default Clean bright). There are 15. --look also accepts a path to a .cube LUT file. A LUT is global, so it can't brighten a face without also brightening the wall behind it.

NameWhat it doesFilter
naturalNo grade at all.none
camera_naturalThe Unprompted look: a touch more contrast and colour, otherwise the camera as it is.eq=contrast=1.04:saturation=1.05
clean_brightThe default: brighter mids, slightly warm, a little more colour.curves=master='0/0 0.25/0.27 0.5/0.55 0.75/0.80 1/1', colorbalance=rm=0.02:bm=-0.02, eq=saturation=1.08
doc_neutralDocumentary neutral: lifted black, lowered white, colour down to 0.88.curves + saturation 0.88
punchyS-curve contrast, colour up to 1.22.curves + saturation 1.22
vivid_popBuilt for YouTube thumbnails: strong colour, slightly brighter.curve, saturation 1.35, brightness +0.02
teal_orangeTeal shadows, orange highlights.colorbalance + saturation 1.12
cool_modernShifted blue, slightly muted.blue shift, saturation 0.95
film_warmWarm, with lifted blacks like film.per-channel curves, saturation 0.95
matte_fadeBlacks lifted to 0.08, muted colour.curves, saturation 0.85
golden_softWarm mids.curves, saturation 1.05
vintage_70sPer-channel curves for a faded 70s print.curves, saturation 0.85
bleach_bypassLow colour, hard contrast.saturation 0.55 + contrast curve
noir_coolCrushed, blue-tinted, with a vignette.curve, saturation 0.7, vignette=angle=PI/4.5
bw_contrastBlack and white with contrast.hue=s=0 + contrast curve

Detail presets

A detail preset is a pair: how much noise to remove and how much to sharpen. Set it with --detail (default Crisp). --denoise and --sharpen replace either half on its own, or turn it off.

How to read hqdn3d=a:b:c:d: the first two numbers are cleanup within one frame (brightness, colour), the last two are cleanup across frames. Across-frame cleanup removes grain that flickers frame to frame without softening edges much, which is why the Unprompted preset leans on it.

NameDenoiseSharpen
lighthqdn3d=1:1:3:3unsharp=5:5:0.4:5:5:0.0
unpromptedhqdn3d=0.5:0.5:8:6 (medium, mostly across frames)unsharp=5:5:0.6:5:5:0.0
balancedhqdn3d=1.5:1.5:6:6unsharp=5:5:0.7:5:5:0.0
crisphqdn3d=1:1:4:4unsharp=5:5:1.1:5:5:0.0
max_detailnonecas=0.6, unsharp=5:5:0.5:5:5:0.0
smooth_skinhqdn3d=4:3:8:6unsharp=5:5:0.4:5:5:0.0
film_grainhqdn3d=1.5:1.5:6:6unsharp=5:5:0.6:5:5:0.0, then noise=alls=6:allf=t+u (adds grain back)

The Unprompted profile

--profile unprompted is the setting every Unprompted video uses. It was picked from a side-by-side test on 5 November 2026: the darker, camera-natural version with medium across-frame cleanup won. The brighter, more heavily denoised 60 fps version lost. Since 6 November the same chain is used on every reel and every YouTube video, not only full episodes. Reels run it at 1080×1920 instead of 4K.

What it sets

  • Look: camera_natural
  • Detail: unprompted
  • Exposure grade: off
  • Separate denoise and sharpen: cleared (the detail preset decides)
  • Order: scale first (CPU Lanczos)
  • Quality: CQ 16, about 93 Mb/s

How it overrides other settings

  • It's applied after everything else is read, so it wins over any --look, --detail, --grade, --denoise, --sharpen or --quality you also pass.
  • --codec still counts: H.264 or HEVC is your choice.
  • --fps, --interp and --gpu-scale do nothing under it, because scale-first keeps the source frame rate and scales on the CPU.

The full chain it produces: format → hqdn3d=0.5:0.5:8:6 → Lanczos to 4K → eq=contrast=1.04:saturation=1.05 → unsharp=5:5:0.6:5:5:0.0 → yuv420p, at the source frame rate, CQ 16. A render from the profile matched the approved test sample at 44.9 dB PSNR; the remaining difference is encoding.

Frame rate

The default target is 60 fps (--fps 60). What happens depends on the source:

SourceWhat V2TSR does
Exactly half the target (30 → 60, 29.97 → 59.94)RIFE, a neural network on the GPU that draws the in-between frame. It only works when it can exactly double the rate.
Any other rate below 60minterpolate (FFmpeg's motion-compensated interpolation, mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1). Correct, but slow on the CPU.
Within 2% of 60 (59.94 counts)Nothing: the frames are used as they are.
Above 60Frames are dropped down to 60.

--interp auto (default) uses RIFE when its environment works, and otherwise warns and falls back to minterpolate. Asking for --interp rife when it isn't available stops with an error instead of silently changing method. RIFE computes motion at half resolution by default (--rife-scale 0.5), which is about 3x faster at the same quality.

In the RIFE path, one FFmpeg decodes and cleans, RIFE generates the frames in between, and a second FFmpeg upscales and encodes. The audio is copied aside first and joined back during the encode.

RIFE is the RIFE v4 model (MIT licence) running in half precision on CUDA. There is no AI upscaler: scaling is Lanczos on the CPU or bicubic on the GPU. Neural upscaling was tried and is only practical on short clips with this GPU.

Encoding

Output is H.264 by default. HEVC is about 40% smaller, and AV1 is available on the command line. Quality is constant-quality (CQ): lower is better. The default is 19, and the Unprompted profile uses 16.

EncoderSettings
NVIDIA (NVENC)-preset p5 -tune hq -rc vbr -cq Q -b:v 0 -spatial-aq 1 -temporal-aq 1 -rc-lookahead 32, plus -bf 3 -b_ref_mode middle except for AV1
Intel (QSV)-preset slower -global_quality Q -look_ahead_depth 40
AMD (AMF)Constant QP: I = Q, P = Q+2, B = Q+4
CPUlibx264 / libx265 -preset medium -crf Q-1; libsvtav1 -preset 6 -crf Q+10

H.264 uses the High profile. HEVC is tagged hvc1 so Apple devices play it. Audio is copied as-is when it fits in an MP4, otherwise it's re-encoded to AAC at 192 kb/s. Files get +faststart so they play while they download. Renders are written to *.partial.mp4 and only renamed once they finish, so a half-written file never looks done.

The app

Opening it: the desktop or Start menu shortcut. Opening it twice brings the open window to the front, and videos dropped on the icon go into the open window's queue. When the app's own files change, it shows "V2TSR was updated" with a Restart now button and keeps your queue. Changes to the render engine need no restart at all. The title bar shows which version is running.

What's on screen

  • Queue: Add videos (mp4, mov, mkv, m4v, avi, webm), Remove, and a table with length, estimated time and size, and status.
  • Colour: every look. Default Clean bright.
  • Detail: every detail preset. Default Crisp.
  • Format: H.264 (plays everywhere) or HEVC (about 40% smaller).
  • Save to: next to each video (default) or a folder you choose.
  • Unprompted episode look: on by default. Turns off Colour and Detail and uses the profile.
  • Move finished renders to Google Drive: on by default.
  • Buttons: Preview 10 s, Start, Pause/Resume, Cancel, Open output folder.

While it runs

  • Live metrics: progress, frames per second, speed against real time, time left, GPU temperature, load, power and clock, video memory, CPU, RAM, free disk, power source, elapsed time, hottest temperature, current stage.
  • Stages: Checking the video, Preparing the video, Measuring brightness, Rendering, Checking the result.
  • History: every past render, including whether it reached Drive. Double-click one that's only on Drive to open it there.
  • Details: the raw log.

Where things go

  • Finished render: <name>_4k60.mp4 in a remaster folder next to the source, or in the folder you chose. The name says 4k60 even when the Unprompted profile keeps 30 fps.
  • Preview: 10 seconds starting at a third of the way in (at most 60 s; from the start for clips of 20 s or less). It opens in your video player.
  • Drive move: uploads one at a time, in their own process so they keep going after the window closes, and keep the laptop awake. Each upload retries up to 8 times, 10 minutes apart. The local file is deleted only after the copy on Drive has the same checksum. Any failure keeps the local file.

Safety checks

  1. Disk space before starting. Output size is estimated from measured rates (16.5 MB/s for 4K60 H.264, 10 MB/s for HEVC or AV1, both with 10% headroom) plus 1 GB. The temporary working copies are counted too. If the working folder and the output share a drive, both are checked against that one drive. Not enough space stops the render before it starts. The app adds its own check (estimate × 1.3 + 5 GB) and asks before going ahead.
  2. No overwriting. The app asks before replacing a file, and the pipeline refuses unless told to.
  3. The finished file is checked for the right frame size, a frame rate within 0.5, a duration within 1 second, and audio present. It also needs at least 98% of the frames it should have. That last check exists because a damaged recording once came out with the right duration but thousands of frames missing.
  4. A watchdog on the RIFE path. If any step errors, or no new frame arrives for 3 minutes (5 for the first frame), it stops all of them instead of hanging. Paused or asleep time doesn't count.
  5. Retries in the app. A GPU failure is retried once after 10 seconds. Any other failure marks that video Failed and the queue carries on.
  6. Keeping the machine on. The laptop stays awake while rendering. The app warns about running on battery (the GPU drops to about 20 W and renders run 2-5x slower), another render already running, and uploads still in progress. Cancel cleans up the partial file.

Measured numbers

"Real time" means a 10-minute video takes 10 minutes. 0.5x real time means it takes 20.

JobResult
1080p 60 fps → 4K60 H.264, 2:03 clip, no interpolation needed56.7 fps, 0.94x real time, 1.47 GB
Unprompted profile (1080p30 → 4K30, CQ 16)1.23x real time, about 11.6 MB/s of output
Real episode, 25.3 min, 4K60 H.26422.8 GB (15.0 MB/s)
1080p30 → 60 fps, RIFE (motion at half resolution)28.8 fps, 0.48x real time
Same, RIFE at full resolution9.3 fps
Same, minterpolate4.2 fps (RIFE is 6.9x faster)
Same, rife-ncnn-vulkan2.7-5.8 fps
10-minute RIFE stress test, 1080p30 → 4K6027.1 fps, 0.45x, 27:39 total, 6.82 GB. No slowdown over the run (27.3 fps first 10%, 29.2 fps last 10%). GPU peaked at 68 °C.
30 fps stress test with minterpolate2.4 fps, 0.04x: 12:38 for 30 seconds. All 8 CPU cores at 90-100%, GPU mostly idle. A 60-minute video would take about 25 hours, which is why RIFE became the default.
60-minute episode with RIFE (projected)About 2 h 15 min, about 41 GB H.264

The app's estimates come from these: 0.45x real time for 30 fps sources, 0.94x for 60 fps sources, about 0.68 GB per minute for H.264 and 0.41 GB per minute for HEVC. Quality of RIFE's made-up frames against real 60 fps frames: 44.6 dB PSNR (minterpolate 45.8 dB). Both are close to the real thing.

Limits and decisions

Limits

  • RIFE only doubles the frame rate. Any other rate falls back to minterpolate, which is slow.
  • The GPU scaler uses bicubic, not Lanczos. FFmpeg's GPU Lanczos wraps its overshoot around (pure white turns black), which left dotted outlines on hard edges: 612 pixels off by more than 60 levels on a test frame, against a maximum of 39 with bicubic. 4K renders made before the switch have the artifact.
  • A LUT is global. It can't light a face without lighting the wall too.
  • The app's Unprompted checkbox text undersells it. It says "light grain cleanup" and 30 fps. The profile actually uses medium across-frame cleanup and keeps the source's frame rate, so a 60 fps source stays 60.

Decisions

DateDecision
29 Oct 2026A very bright "LIT" grade was rejected: too bright, and it looked low quality.
3 Nov 2026Defaults set to Clean bright + Crisp. A darker, warmer "moody" look matched to a reference reel was turned down.
3 Nov 2026RIFE became the default interpolator after a clean 10-minute test. Interpolation moved before the upscale.
5 Nov 2026From a side-by-side test of full episodes, the darker camera-natural look with medium across-frame cleanup, sharpening last, the source's 30 fps and a high bitrate became the Unprompted profile. The brighter, more heavily denoised 60 fps version lost: the plain version looked more natural, but its grain showed, so medium cleanup was added to it.
5 Nov 2026After a GPU error hung a render, the watchdog was added. After a damaged file passed the duration check, the frame-count check was added.
6 Nov 2026The Unprompted chain became the look for every reel and every YouTube video.

Flag reference

FlagDefaultWhat it does
--profile unpromptedoffThe Unprompted chain. Overrides look, detail, grade, denoise, sharpen and quality.
--lookclean_brightColour grade by name, or a .cube file.
--detailcrispDenoise + sharpen pair.
--denoise / --sharpenfrom presetReplace one half of the detail preset, or off.
--gradeautoauto (whole-clip exposure), dynamic (per frame), off, or fixed eq values.
--strength, --black-pct, --white-pct, --target-median0.8, 0.5, 99.5, 0.42Tune the auto exposure.
--fps60Target frame rate.
--interpautoauto, rife or minterpolate.
--rife-scale0.5Resolution RIFE measures motion at.
--interp-firstonInterpolate before the upscale (turning it off is about 6x slower).
--gpu-scaleonBicubic on the GPU. --no-gpu-scale uses CPU Lanczos.
--scale-firstoffUpscale right after denoising, then grade and sharpen at 4K (what the profile uses).
--codech264h264, hevc or av1.
--encoderautoauto, nvenc, qsv, amf or cpu.
--quality19Constant quality. Lower is better and bigger.
--audio-bitrate192kOnly used when audio has to be re-encoded.
--verify-exposure (--check)offRe-measures the finished brightness. Costs about 40% more render time.
--direct-inputoffRead the source directly instead of through the Base64 copy.
--base64-outputoffAlso write a Base64 copy of the result.
--start, --durationwhole fileRender part of a video.
--dry-run (--detect-only)offProbe and print the plan without rendering.
-yoffAllow overwriting an existing output.