From 45f1396769052e3a66c643e22ae9ff58a38d3766 Mon Sep 17 00:00:00 2001 From: Claude Toupin Date: Thu, 22 Oct 2026 08:30:52 -0400 Subject: [PATCH] Add -Size parameter to Crop-Clips.ps1 for custom output resolution with cover-scaling and center-cropping, supporting named presets (320p-4K) that transpose for portrait sources and custom "width:height" format, overriding the cropProfiles table to process videos of any resolution instead of skipping non-standard sizes, with even-number dimension rounding for yuv420p compatibility and skipping of videos already at requested size, plus fix output info reporting to skip when -WhatIf or declined -Confirm leaves --- Crop-Clips.ps1 | 121 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 101 insertions(+), 20 deletions(-) diff --git a/Crop-Clips.ps1 b/Crop-Clips.ps1 index 51f22de..7cf1cff 100644 --- a/Crop-Clips.ps1 +++ b/Crop-Clips.ps1 @@ -5,10 +5,20 @@ Source directory containing video files. .PARAMETER Target Target directory for cropped video files. +.PARAMETER Size + Size of the output video. Uses the known non-standard generator resolutions table if not specified. + Possible values: 320p, 384p, 480p, 720p, 900p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height". + When specified, the resolutions table is overridden and every video is scaled (preserving aspect + ratio) so it fully covers the requested size, then centre-cropped to it, so videos of any + resolution are processed instead of being skipped. + Named presets are landscape (e.g. 1080p = 1920x1080) and are transposed for portrait sources + (a 1080x1920 source with -Size 1080p stays 1080x1920); a custom "width:height" is used exactly as + given. Videos already at the requested size are skipped. .DESCRIPTION Crops video clips to standard resolutions using ffmpeg. Handles the non-standard resolutions produced by LTX-2, Kling and Wan (see the $cropProfiles table), plus any square (1:1) source, - which is scaled to 1080x1080. Videos with any other resolution are skipped. + which is scaled to 1080x1080. Videos with any other resolution are skipped, unless -Size is + used, which overrides the table and converts every video to the requested size. Supports -WhatIf and -Confirm for safe execution. #> [CmdletBinding(SupportsShouldProcess)] @@ -17,7 +27,10 @@ param( [string]$Source = '.', [Parameter(Mandatory = $false)] - [string]$Target = '.' + [string]$Target = '.', + + [Parameter(Mandatory = $false)] + [string]$Size = $null ) Set-StrictMode -Version Latest @@ -133,6 +146,39 @@ $cropProfiles = @( @{ Width = 1284; Height = 716; Filter = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2' } ) +# --- Optional -Size: overrides the $cropProfiles lookup for every video --- +# Named presets are stored in landscape form and transposed for portrait sources, so a 9:16 +# clip stays 9:16. A custom 'width:height' is always used exactly as given. Parsing happens +# once here so an invalid value fails before any video is touched. +$sizePresets = @{ + '320p' = @{ Width = 570; Height = 320 } + '384p' = @{ Width = 684; Height = 384 } + '480p' = @{ Width = 854; Height = 480 } + '720p' = @{ Width = 1280; Height = 720 } + '900p' = @{ Width = 1600; Height = 900 } + '1080p' = @{ Width = 1920; Height = 1080 } + 'HD' = @{ Width = 1920; Height = 1080 } + '1440p' = @{ Width = 2560; Height = 1440 } + '2K' = @{ Width = 2560; Height = 1440 } + '2160p' = @{ Width = 3840; Height = 2160 } + '4K' = @{ Width = 3840; Height = 2160 } +} + +$sizePreset = $null +$sizeExact = $null + +if (-not [string]::IsNullOrWhiteSpace($Size)) { + if ($sizePresets.ContainsKey($Size)) { + $sizePreset = $sizePresets[$Size] + } + elseif ($Size -match '^(\d+):(\d+)$') { + $sizeExact = @{ Width = [int]$Matches[1]; Height = [int]$Matches[2] } + } + else { + throw "Invalid -Size value: $Size. Possible values: 320p, 384p, 480p, 720p, 900p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format ""width:height""." + } +} + $sourceFull = (Resolve-Path -LiteralPath $Source).Path $targetFull = (Resolve-Path -LiteralPath $Target).Path @@ -162,22 +208,53 @@ foreach ($video in $videos) { $inQuality = Format-Quality -BitRate $inInfo.BitRate $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } - $cropProfile = $cropProfiles | Where-Object { $_.Width -eq $inInfo.Width -and $_.Height -eq $inInfo.Height } | Select-Object -First 1 + if ($sizePreset -or $sizeExact) { + # -Size given: ignore the resolution table and fit every video to the requested size by + # cover-scaling (force_original_aspect_ratio=increase keeps the pixel aspect ratio and + # guarantees full coverage) then centre-cropping to the exact size. + if ($sizeExact) { + $outW = $sizeExact.Width + $outH = $sizeExact.Height + } + elseif ($inInfo.Height -gt $inInfo.Width) { + # Portrait source: use the portrait form of the preset. + $outW = $sizePreset.Height + $outH = $sizePreset.Width + } + else { + $outW = $sizePreset.Width + $outH = $sizePreset.Height + } - if ($cropProfile) { - $vf = $cropProfile.Filter - } - elseif ($inInfo.Width -eq $inInfo.Height -and $inInfo.Width -ne 1080) { - # Any square (1:1) source other than 1080x1080 is scaled to 1080x1080; no crop needed. - $vf = 'scale=1080:1080' + # libx264/yuv420p requires even dimensions. + if ($outW % 2) { $outW++ } + if ($outH % 2) { $outH++ } + + if ($inInfo.Width -eq $outW -and $inInfo.Height -eq $outH) { + Write-Host "Skipping: $($inInfo.FileName) (already ${outW}x${outH})" + continue + } + + $vf = 'scale={0}:{1}:force_original_aspect_ratio=increase,crop={0}:{1}' -f $outW, $outH } else { - $vf = $null - } + $cropProfile = $cropProfiles | Where-Object { $_.Width -eq $inInfo.Width -and $_.Height -eq $inInfo.Height } | Select-Object -First 1 - if (-not $vf) { - Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" - continue + if ($cropProfile) { + $vf = $cropProfile.Filter + } + elseif ($inInfo.Width -eq $inInfo.Height -and $inInfo.Width -ne 1080) { + # Any square (1:1) source other than 1080x1080 is scaled to 1080x1080; no crop needed. + $vf = 'scale=1080:1080' + } + else { + $vf = $null + } + + if (-not $vf) { + Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" + continue + } } Write-Host "Input : Filename=$($inInfo.FileName) Resolution=$inRes FPS=$inFpsText Quality=$inQuality Audio=$inAudioText" @@ -244,13 +321,17 @@ foreach ($video in $videos) { Write-Host "WhatIf: Would crop video with ffmpeg $($formattedArgs -join ' ')" } - $outInfo = Get-VideoInfo -Path $outFinalPath - $outRes = "{0}x{1}" -f $outInfo.Width, $outInfo.Height - $outFpsText = if ($outInfo.Fps) { "{0:0.###}" -f $outInfo.Fps } else { 'unknown' } - $outQuality = Format-Quality -BitRate $outInfo.BitRate - $outAudioText = if ($outInfo.HasAudio) { 'yes' } else { 'no' } + # Under -WhatIf (or -Confirm declined) nothing was written, so there is no output file to + # probe; reporting is skipped rather than failing on the missing file. + if (Test-Path -LiteralPath $outFinalPath) { + $outInfo = Get-VideoInfo -Path $outFinalPath + $outRes = "{0}x{1}" -f $outInfo.Width, $outInfo.Height + $outFpsText = if ($outInfo.Fps) { "{0:0.###}" -f $outInfo.Fps } else { 'unknown' } + $outQuality = Format-Quality -BitRate $outInfo.BitRate + $outAudioText = if ($outInfo.HasAudio) { 'yes' } else { 'no' } - Write-Host "Output: Filename=$($outInfo.FileName) Resolution=$outRes FPS=$outFpsText Quality=$outQuality Audio=$outAudioText" + Write-Host "Output: Filename=$($outInfo.FileName) Resolution=$outRes FPS=$outFpsText Quality=$outQuality Audio=$outAudioText" + } $processedCount++ }