From fb4f37145027560c51d53ca9e1c36e3ed3cba621 Mon Sep 17 00:00:00 2001 From: Claude Toupin Date: Wed, 10 Jun 2026 17:36:45 -0400 Subject: [PATCH] Add -Upscale parameter to Clone-Image.ps1 for AI upscaling with upscayl-bin before ffmpeg processing, add -Model and -Keep parameters for model selection and output preservation, and update Stich-InBetween.ps1 to use RIFE_HOME and NCNN_GPU_ID environment variables for TNTwise fork compatibility --- Clone-Image.ps1 | 170 +++++++++++++++++++++++++++++++++++++++++++- Stich-InBetween.ps1 | 43 +++++++++-- 2 files changed, 205 insertions(+), 8 deletions(-) diff --git a/Clone-Image.ps1 b/Clone-Image.ps1 index 171b2f7..e323934 100644 --- a/Clone-Image.ps1 +++ b/Clone-Image.ps1 @@ -27,6 +27,23 @@ (0-6, higher means smaller files but slower encoding). Ignored for jpeg, avif, bmp and tiff. .PARAMETER Metadata When present, copies available metadata from the source image file to the target image file. +.PARAMETER Upscale + Optional upscaling factor (2, 3 or 4) applied to the source image with upscayl-bin.exe + before ffmpeg runs. The upscaled image is written to a Windows temp PNG and used as the + ffmpeg input, so any -Size / -LTX cover-scale-then-crop is applied to the upscaled image. + Requires upscayl-bin.exe in PATH, the UPSCAYL_HOME environment variable pointing to the + upscayl-bin install directory, and the NCNN_GPU_ID environment variable set to the desired + GPU device ID (-1 for CPU, 0/1/2/... for GPU). +.PARAMETER Model + Upscayl model name (without extension) to use when -Upscale is specified. Defaults to + '4xNomos8kSC'. The model files (.bin and .param) must exist under + %UPSCAYL_HOME%\models. Ignored when -Upscale is not specified. +.PARAMETER Keep + Optional path to save a copy of the upscayl-bin output (i.e. the upscaled image, before + ffmpeg's cover-scale-then-crop). The file extension determines the format; if it differs + from .png (the upscayl output format) the temp PNG is converted with ffmpeg using the + appropriate per-format encoder. If the extension is missing or unrecognised, .png is + appended. Only valid when -Upscale is specified. .PARAMETER LTX When present, overrides the dimensions selected by -Size with the closest LTX-2 (Wan2GP by DeepBeepMeep) canonical resolution for the source's aspect ratio. Only valid when -Size is one of HD, 1080p, 720p or 480p. @@ -61,7 +78,17 @@ param( [switch]$Metadata, [Parameter(Mandatory = $false)] - [switch]$LTX + [switch]$LTX, + + [Parameter(Mandatory = $false)] + [ValidateSet(0, 2, 3, 4)] + [int]$Upscale = 0, + + [Parameter(Mandatory = $false)] + [string]$Model = '4xNomos8kSC', + + [Parameter(Mandatory = $false)] + [string]$Keep = $null ) Set-StrictMode -Version Latest @@ -259,6 +286,142 @@ switch ($targetFormat) { } } +# --- Optional upscayl-bin upscaling step (runs before ffmpeg). --- +# When -Upscale is given (2/3/4) the source image is upscaled with upscayl-bin +# into a temp PNG; that temp PNG then becomes the ffmpeg input so any -Size / +# -LTX cover-scale-then-crop is applied to the already-upscaled image. +$upscaledTempPath = $null +if ($Upscale -gt 0) { + $upscaylExe = Get-Command -Name 'upscayl-bin.exe' -ErrorAction SilentlyContinue + if (-not $upscaylExe) { + Write-Error 'upscayl-bin.exe not found in PATH. Install upscayl-bin and add it to the system PATH (see mkdocs/Upscalers.md).' + exit 1 + } + $upscaylExePath = $upscaylExe.Source + + $upscaylHome = $env:UPSCAYL_HOME + if (-not $upscaylHome) { + Write-Error 'UPSCAYL_HOME environment variable is not set. Set it to the upscayl-bin install directory.' + exit 1 + } + $upscaylHome = $upscaylHome.TrimEnd('\', '/') + if (-not (Test-Path -LiteralPath $upscaylHome -PathType Container)) { + Write-Error ('UPSCAYL_HOME directory does not exist: {0}' -f $upscaylHome) + exit 1 + } + + $modelsDir = Join-Path $upscaylHome 'models' + if (-not (Test-Path -LiteralPath $modelsDir -PathType Container)) { + Write-Error ('Upscayl models directory not found: {0}' -f $modelsDir) + exit 1 + } + $modelBin = Join-Path $modelsDir ('{0}.bin' -f $Model) + $modelParam = Join-Path $modelsDir ('{0}.param' -f $Model) + if (-not (Test-Path -LiteralPath $modelBin -PathType Leaf) -or -not (Test-Path -LiteralPath $modelParam -PathType Leaf)) { + Write-Error ('Upscayl model files not found for model ''{0}'' under {1} (expected {0}.bin and {0}.param).' -f $Model, $modelsDir) + exit 1 + } + + $gpuId = $env:NCNN_GPU_ID + if (-not $gpuId) { + Write-Error 'NCNN_GPU_ID environment variable is not set. Set it to -1 (CPU) or 0/1/2/... (GPU device index).' + exit 1 + } + if ($gpuId -notmatch '^-?\d+$') { + Write-Error ('NCNN_GPU_ID must be an integer (-1 for CPU, 0+ for GPU). Current value: {0}' -f $gpuId) + exit 1 + } + + $upscaledTempPath = Join-Path ([System.IO.Path]::GetTempPath()) (('upscayl-{0}.png') -f ([System.Guid]::NewGuid().ToString())) + + Write-Host ('Upscaling source with upscayl-bin (x{0}, model={1}, gpu={2})...' -f $Upscale, $Model, $gpuId) + Write-Host (' Upscayl temp file: {0}' -f $upscaledTempPath) + + if ($PSCmdlet.ShouldProcess($SourcePath, ('Upscale x{0} with upscayl-bin into temp file' -f $Upscale))) { + $upscaylOutput = & $upscaylExePath -i $SourcePath -o $upscaledTempPath -s $Upscale.ToString() -n $Model -m $modelsDir -g $gpuId 2>&1 + $upscaylExit = $LASTEXITCODE + if ($upscaylExit -ne 0) { + Write-Host 'upscayl-bin error output:' -ForegroundColor Red + $upscaylOutput | ForEach-Object { Write-Host $_ } + Write-Error ('upscayl-bin failed with exit code: {0}' -f $upscaylExit) + if (Test-Path -LiteralPath $upscaledTempPath) { Remove-Item -LiteralPath $upscaledTempPath -Force -ErrorAction SilentlyContinue } + exit 1 + } + + # Switch ffmpeg's input to the upscaled image. + $SourcePath = $upscaledTempPath + + # Refresh informational source dimensions from the upscaled file. + $ffprobeDimOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $SourcePath 2>&1 + if ($LASTEXITCODE -eq 0) { + $dimLine2 = $ffprobeDimOutput2 | Select-Object -First 1 + if ($dimLine2) { + $sourceDimensions = ([string]$dimLine2).Trim() + (' (upscaled x{0})' -f $Upscale) + } + } + + # --- Optional -Keep: save a copy of the upscaled image. --- + if (-not [string]::IsNullOrWhiteSpace($Keep)) { + $KeepPath = $Keep + $keepExt = [System.IO.Path]::GetExtension($KeepPath).ToLowerInvariant() + if ($supportedTargetExts -notcontains $keepExt) { + $KeepPath += '.png' + $keepExt = '.png' + } + $KeepDir = Split-Path -Parent $KeepPath + if ($KeepDir -and -not (Test-Path -LiteralPath $KeepDir)) { + Write-Error ('Keep target directory does not exist: {0}' -f $KeepDir) + if (Test-Path -LiteralPath $upscaledTempPath) { Remove-Item -LiteralPath $upscaledTempPath -Force -ErrorAction SilentlyContinue } + exit 1 + } + + if ($keepExt -eq '.png') { + # Upscayl already wrote a PNG; just copy. + if ($PSCmdlet.ShouldProcess($KeepPath, 'Copy upscaled image (PNG)')) { + Copy-Item -LiteralPath $upscaledTempPath -Destination $KeepPath -Force + Write-Host ('Upscaled image saved to: {0}' -f $KeepPath) + } + } else { + # Convert PNG -> requested format with ffmpeg, using sane defaults. + switch ($keepExt) { + '.jpg' { $keepFormat = 'jpeg' } + '.jpeg' { $keepFormat = 'jpeg' } + '.tif' { $keepFormat = 'tiff' } + '.tiff' { $keepFormat = 'tiff' } + default { $keepFormat = $keepExt.TrimStart('.') } + } + $keepEncoderArgs = @() + switch ($keepFormat) { + 'jpeg' { $keepEncoderArgs += @('-c:v', 'mjpeg', '-pix_fmt', 'yuvj420p', '-q:v', '2') } + 'webp' { $keepEncoderArgs += @('-c:v', 'libwebp', '-quality', '90') } + 'avif' { $keepEncoderArgs += @('-c:v', 'libaom-av1', '-still-picture', '1', '-cpu-used', '4', '-crf', '25') } + 'bmp' { $keepEncoderArgs += @('-c:v', 'bmp') } + 'tiff' { $keepEncoderArgs += @('-c:v', 'tiff') } + 'png' { $keepEncoderArgs += @('-c:v', 'png') } + } + Write-Host ('Converting upscaled image to {0}: {1}' -f $keepFormat, $KeepPath) + if ($PSCmdlet.ShouldProcess($KeepPath, ('Save upscaled image as {0}' -f $keepFormat))) { + $keepFfmpegArgs = @('-y', '-i', $upscaledTempPath) + $keepEncoderArgs + @('-frames:v', '1', '-map_metadata', '-1', '--', $KeepPath) + $keepFfmpegOutput = & ffmpeg @keepFfmpegArgs 2>&1 + $keepFfmpegExit = $LASTEXITCODE + if ($keepFfmpegExit -ne 0) { + Write-Host 'ffmpeg error output (Keep step):' -ForegroundColor Red + $keepFfmpegOutput | ForEach-Object { Write-Host $_ } + Write-Error ('ffmpeg failed to save upscaled image (-Keep) with exit code: {0}' -f $keepFfmpegExit) + if (Test-Path -LiteralPath $upscaledTempPath) { Remove-Item -LiteralPath $upscaledTempPath -Force -ErrorAction SilentlyContinue } + exit 1 + } + Write-Host ('Upscaled image saved to: {0}' -f $KeepPath) + } + } + } + } +} + +if (-not [string]::IsNullOrWhiteSpace($Keep) -and $Upscale -le 0) { + Write-Warning '-Keep was specified without -Upscale; ignoring (nothing to save).' +} + Write-Host ('Cloning image: {0}' -f $SourcePath) Write-Host (' Target format: {0}' -f $targetFormat) if ($sourceDimensions) { @@ -316,3 +479,8 @@ if ($PSCmdlet.ShouldProcess($TargetPath, 'Clone image file')) { Write-Host ('Image cloned successfully to: {0}' -f $TargetPath) } + +# --- Clean up the upscayl temp file (if any). --- +if ($upscaledTempPath -and (Test-Path -LiteralPath $upscaledTempPath)) { + Remove-Item -LiteralPath $upscaledTempPath -Force -ErrorAction SilentlyContinue +} diff --git a/Stich-InBetween.ps1 b/Stich-InBetween.ps1 index c02bf3a..ece3df2 100644 --- a/Stich-InBetween.ps1 +++ b/Stich-InBetween.ps1 @@ -41,12 +41,17 @@ .NOTES Requires: - - rife-ncnn-vulkan.exe in the system PATH + - rife-ncnn-vulkan.exe (TNTwise fork, RIFE 4.26) in the system PATH + - The RIFE_HOME environment variable set to the rife-ncnn-vulkan install directory + (the same directory that is on PATH; it must contain the rife-v4.26 model folder) + - The NCNN_GPU_ID environment variable set to the desired GPU device ID + (-1 for CPU, 0/1/2/etc. for GPU) passed to rife-ncnn-vulkan via the -g flag - ffmpeg in the system PATH - PowerShell 7 or later - The script automatically locates rife-ncnn-vulkan.exe from PATH and expects - the rife-v4 model directory to be in the same directory as the executable. + The script locates rife-ncnn-vulkan.exe from PATH and resolves the rife-v4.26 + model directory from %RIFE_HOME%\rife-v4.26. The GPU device is selected from + %NCNN_GPU_ID%. Supports -WhatIf and -Confirm for safe execution. #> @@ -161,15 +166,39 @@ if (-not $rifeExe) { } $rifeExePath = $rifeExe.Source -$rifeDir = Split-Path -Parent $rifeExePath -$rifeModelPath = Join-Path $rifeDir "rife-v4" + +# Resolve the model directory via RIFE_HOME (required by the TNTwise fork's +# install layout, see mkdocs/RIFE.md). RIFE_HOME must point to the install +# folder containing the rife-v4.26 model directory. +$rifeHome = $env:RIFE_HOME +if (-not $rifeHome) { + throw "RIFE_HOME environment variable is not set. Set it to the rife-ncnn-vulkan install directory (the same folder that is on PATH)." +} +$rifeHome = $rifeHome.TrimEnd('\', '/') + +if (-not (Test-Path -LiteralPath $rifeHome -PathType Container)) { + throw "RIFE_HOME directory does not exist: $rifeHome" +} + +$rifeModelPath = Join-Path $rifeHome "rife-v4.26" if (-not (Test-Path -LiteralPath $rifeModelPath -PathType Container)) { - throw "RIFE model directory not found at: $rifeModelPath" + throw "RIFE model directory not found at: $rifeModelPath. Ensure the rife-v4.26 model folder ships under %RIFE_HOME%." +} + +# Resolve the GPU device ID for the -g flag from NCNN_GPU_ID. +$gpuId = $env:NCNN_GPU_ID +if (-not $gpuId) { + throw "NCNN_GPU_ID environment variable is not set. Set it to -1 (CPU) or 0/1/2/etc. (GPU device index)." +} +if ($gpuId -notmatch '^-?\d+$') { + throw "NCNN_GPU_ID must be an integer (-1 for CPU, 0+ for GPU). Current value: $gpuId" } Write-Host "Found rife-ncnn-vulkan.exe at: $rifeExePath" +Write-Host "Using RIFE_HOME: $rifeHome" Write-Host "Using model path: $rifeModelPath" +Write-Host "Using GPU device (-g): $gpuId" # --- Prepare TempDir --- @@ -190,7 +219,7 @@ if (Test-Path -LiteralPath $tempDirPath) { if ($PSCmdlet.ShouldProcess($TempDir, "Generate interpolated frames with rife-ncnn-vulkan (Count=$Count)")) { Write-Host "Executing rife-ncnn-vulkan.exe (Count=$Count, EndsAt=$EndsAt)..." - & $rifeExePath -0 $FirstFilename -1 $LastFilename -i $InputTempDir -o $TempDir -n $Count -m $rifeModelPath + & $rifeExePath -0 $FirstFilename -1 $LastFilename -i $InputTempDir -o $TempDir -n $Count -m $rifeModelPath -g $gpuId if ($LASTEXITCODE -ne 0) { throw "rife-ncnn-vulkan.exe failed with exit code $LASTEXITCODE"