From b823259ab1e7a8c261cffac255b12b1aee59eb22 Mon Sep 17 00:00:00 2001 From: Claude Toupin Date: Sun, 20 Sep 2026 16:16:55 -0400 Subject: [PATCH] Refactor Crop-Clips.ps1 to use table-driven crop profiles for LTX-2, Kling and Wan resolutions, add -Vertical parameter to Create-PillarBox.ps1 for 9:16 portrait output from square sources with horizontal bars, update Resolve-Size to check both dimensions when selecting presets and transpose named sizes in portrait mode, and update README to reflect consolidated cropping functionality --- Create-PillarBox.ps1 | 71 +++++++++++++++++++------ Crop-Clips.ps1 | 122 +++++++++++++++++++------------------------ README.md | 4 +- 3 files changed, 111 insertions(+), 86 deletions(-) diff --git a/Create-PillarBox.ps1 b/Create-PillarBox.ps1 index 25a4645..eabd717 100644 --- a/Create-PillarBox.ps1 +++ b/Create-PillarBox.ps1 @@ -7,6 +7,7 @@ Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided. If omitted, defaults to the source filename with the -Effect value (lowercased) appended after a dash, saved alongside the source (e.g. video.mp4 + -Effect black -> video-black.mp4). + When -Vertical is used, '-vertical' is appended after the effect (e.g. video-black-vertical.mp4). .PARAMETER Effect The effect to apply to the side bars. Possible values are: black: The side bars will be solid black (#000000). @@ -18,8 +19,14 @@ standard: Creates softly blurred side bars generated from the video itself, with a subtle zoom to fill the 16:9 frame. dark: Produces blurred side bars with reduced brightness, creating a darker, more contrasted background that enhances focus on the main video. gaussian: Generates smooth, high‑quality Gaussian‑blurred side bars for a clean, professional broadcast-style background. +.PARAMETER Vertical + Only valid when the source video has a square (1:1) resolution. When present, the output is a 9:16 portrait + video with the bars on the top and bottom instead of the default 16:9 landscape output with bars on the sides. + Named -Size presets are interpreted as portrait sizes (e.g. 1080p -> 1080x1920, 4K -> 2160x3840). + Throws when the source video is not square. .PARAMETER Size - Size of the output video. Uses the input video's closest size that will fit the 16:9 aspect ratio if not specified. + Size of the output video. Uses the input video's closest size that will fit the 16:9 aspect ratio if not specified + (9:16 when -Vertical is used). Possible values: 480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height". .PARAMETER FPS Frames per second for the output video. Uses the input video's FPS if not specified. @@ -33,6 +40,7 @@ Creates a pillar-boxed video file from a video file. Will add black bars to the sides of the video to match 16:9 aspect ratio. The black bars will be verticals on each side of the output video if the source video is more portrait-like (e.g. 9:16). Otherwise, the black bars will be horizontal on the top and bottom of the output video. + With -Vertical (square sources only), the output frame is 9:16 instead and the bars are horizontal on the top and bottom. #> [CmdletBinding(SupportsShouldProcess)] param( @@ -45,6 +53,9 @@ param( [Parameter(Mandatory = $true)] [string]$Effect, + [Parameter(Mandatory = $false)] + [switch]$Vertical, + [Parameter(Mandatory = $false)] [string]$Size = $null, @@ -136,7 +147,12 @@ function Resolve-Size { [int]$SourceWidth, [Parameter(Mandatory = $true)] - [int]$SourceHeight + [int]$SourceHeight, + + # When set, the output frame is 9:16 (portrait) instead of 16:9: named presets + # are transposed (1080p -> 1080x1920) and the computed frame is portrait. + [Parameter(Mandatory = $false)] + [switch]$Vertical ) # Standard 16:9 sizes (largest first only matters for closest-fit lookup) @@ -154,6 +170,9 @@ function Resolve-Size { if ($Value) { $named = $standards | Where-Object { $_.Name -ieq $Value } | Select-Object -First 1 if ($named) { + if ($Vertical) { + return [pscustomobject]@{ Width = $named.Height; Height = $named.Width } + } return [pscustomobject]@{ Width = $named.Width; Height = $named.Height } } @@ -164,29 +183,41 @@ function Resolve-Size { throw "Invalid -Size value: $Value. Use a named preset (480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K) or 'width:height'." } - # Pick the smallest standard 16:9 preset whose longer side (width) is at least - # the source's longer side. The foreground is scaled down with - # 'force_original_aspect_ratio=decrease', so the source is allowed to shrink - # to fit; we only need the preset to be "big enough" relative to the source's - # largest dimension. This produces the documented behavior: + # Pick the smallest standard 16:9 preset that holds the source's bounding box + # once that box is laid out in the frame's orientation: the preset's long side + # (width) must be at least the source's long side AND the preset's short side + # (height) must be at least the source's short side. Checking both sides matters + # for square-ish sources: a 1080x1080 source fits 1920x1080 at full resolution, + # while 1280x720 would force it down to 720x720. + # This produces the documented behavior: # 1080x1920 portrait -> 1920x1080 (vertical bars on either side) # 720x1280 portrait -> 1280x720 + # 1080x1080 square -> 1920x1080 (vertical bars, source kept at 1080x1080) # 2560x1080 landscape -> 2560x1440 (horizontal bars top/bottom) + # With -Vertical the same preset is used transposed (e.g. 1080x1080 -> 1080x1920). $longSide = [math]::Max($SourceWidth, $SourceHeight) + $shortSide = [math]::Min($SourceWidth, $SourceHeight) $unique = $standards | Sort-Object { $_.Width } -Unique foreach ($s in $unique) { - if ($s.Width -ge $longSide) { + if ($s.Width -ge $longSide -and $s.Height -ge $shortSide) { + if ($Vertical) { + return [pscustomobject]@{ Width = $s.Height; Height = $s.Width } + } return [pscustomobject]@{ Width = $s.Width; Height = $s.Height } } } - # Source larger than any standard preset: build a 16:9 frame whose width - # matches the source's longer side (rounded to even). - $w = $longSide + ($longSide % 2) - $h = [int][math]::Ceiling($w * 9.0 / 16.0) - if ($h % 2) { $h++ } - return [pscustomobject]@{ Width = $w; Height = $h } + # Source larger than any standard preset: build a 16:9 (or 9:16) frame that + # holds the source's bounding box, rounded to even dimensions. + $long = [math]::Max($longSide, [int][math]::Ceiling($shortSide * 16.0 / 9.0)) + $short = [math]::Max($shortSide, [int][math]::Ceiling($long * 9.0 / 16.0)) + if ($long % 2) { $long++ } + if ($short % 2) { $short++ } + if ($Vertical) { + return [pscustomobject]@{ Width = $short; Height = $long } + } + return [pscustomobject]@{ Width = $long; Height = $short } } # Validate -Effect first so it can be used in the default -Target name. @@ -231,11 +262,13 @@ $sourceFull = (Resolve-Path -LiteralPath $Source).Path # (e.g. C:\clips\video.mp4 + -Effect Black -> C:\clips\video-black.mp4). # For 'color:RRGGBB', the colon is replaced with a dash to keep the filename valid # (e.g. -Effect color:FFFF00 -> video-color-ffff00.mp4). +# With -Vertical, '-vertical' is appended last (e.g. video-black-vertical.mp4). if ([string]::IsNullOrWhiteSpace($Target)) { $srcDir = [System.IO.Path]::GetDirectoryName($sourceFull) $srcBase = [System.IO.Path]::GetFileNameWithoutExtension($sourceFull) $effectForName = $effectLower -replace ':', '-' - $Target = Join-Path $srcDir "$srcBase-$effectForName.mp4" + $verticalForName = if ($Vertical) { '-vertical' } else { '' } + $Target = Join-Path $srcDir "$srcBase-$effectForName$verticalForName.mp4" } elseif (-not [System.IO.Path]::GetExtension($Target)) { $Target = "$Target.mp4" @@ -271,7 +304,13 @@ $inQuality = Format-Quality -BitRate $inInfo.BitRate $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } Write-Host "Input : Filename=$($inInfo.FileName) Resolution=$inRes FPS=$inFpsText Quality=$inQuality Audio=$inAudioText" -$outSize = Resolve-Size -Value $Size -SourceWidth $inInfo.Width -SourceHeight $inInfo.Height +# -Vertical produces a 9:16 output frame (bars on top and bottom) and is only +# meaningful for square sources, where the 16:9 / 9:16 choice is otherwise arbitrary. +if ($Vertical -and $inInfo.Width -ne $inInfo.Height) { + throw "-Vertical requires a square (1:1) source video, but '$($inInfo.FileName)' is $inRes." +} + +$outSize = Resolve-Size -Value $Size -SourceWidth $inInfo.Width -SourceHeight $inInfo.Height -Vertical:$Vertical $outWidth = $outSize.Width $outHeight = $outSize.Height diff --git a/Crop-Clips.ps1 b/Crop-Clips.ps1 index 3e5ed75..5c38ed4 100644 --- a/Crop-Clips.ps1 +++ b/Crop-Clips.ps1 @@ -6,7 +6,9 @@ .PARAMETER Target Target directory for cropped video files. .DESCRIPTION - Crops video clips using ffmpeg. + 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. Supports -WhatIf and -Confirm for safe execution. #> [CmdletBinding(SupportsShouldProcess)] @@ -83,6 +85,50 @@ function Format-Quality { return "${kbps}kbps" } +# Known non-standard generator resolutions and the ffmpeg filter that brings each one +# back to a standard size. Resolutions are unique across all generators, so a single +# lookup is unambiguous. Any resolution not listed here is skipped, except square +# sources, which are handled by a generic rule in the loop below. +$cropProfiles = @( + # LTX-2 landscape + @{ Width = 1920; Height = 1088; Filter = 'crop=1920:1080:0:4' } + @{ Width = 1280; Height = 704; Filter = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2' } + @{ Width = 1600; Height = 896; Filter = 'scale=-2:900,crop=1600:900:(in_w-1600)/2:(in_h-900)/2' } + @{ Width = 832; Height = 448; Filter = 'scale=-2:480,crop=854:480:(in_w-854)/2:(in_h-480)/2' } + @{ Width = 640; Height = 384; Filter = 'scale=684:-2,crop=684:384:(in_w-684)/2:(in_h-384)/2' } + @{ Width = 576; Height = 320; Filter = 'crop=570:320:3:0' } + @{ Width = 3744; Height = 2112; Filter = 'scale=3840:-2,crop=3840:2160:0:(in_h-2160)/2' } + @{ Width = 2496; Height = 1408; Filter = 'scale=2560:-2,crop=2560:1440:0:(in_h-1440)/2' } + @{ Width = 2160; Height = 1216; Filter = 'scale=1920:-2,crop=1920:1080:0:(in_h-1080)/2' } + @{ Width = 1872; Height = 1056; Filter = 'scale=1920:-2,crop=1920:1080:0:(in_h-1080)/2' } + @{ Width = 1248; Height = 704; Filter = 'scale=1280:-2,crop=1280:720:0:(in_h-720)/2' } + + # LTX-2 portrait + @{ Width = 1088; Height = 1920; Filter = 'crop=1080:1920:4:0' } + @{ Width = 704; Height = 1280; Filter = 'scale=720:-2,crop=720:1280:(in_w-720)/2:(in_h-1280)/2' } + @{ Width = 896; Height = 1600; Filter = 'scale=900:-2,crop=900:1600:(in_w-900)/2:(in_h-1600)/2' } + @{ Width = 448; Height = 832; Filter = 'scale=480:-2,crop=480:854:(in_w-480)/2:(in_h-854)/2' } + @{ Width = 384; Height = 640; Filter = 'scale=-2:684,crop=384:684:(in_w-384)/2:(in_h-684)/2' } + @{ Width = 320; Height = 576; Filter = 'crop=320:570:0:3' } + @{ Width = 2112; Height = 3744; Filter = 'scale=-2:3840,crop=2160:3840:(in_w-2160)/2:0' } + @{ Width = 1408; Height = 2496; Filter = 'scale=-2:2560,crop=1440:2560:(in_w-1440)/2:0' } + @{ Width = 1216; Height = 2160; Filter = 'scale=-2:1920,crop=1080:1920:(in_w-1080)/2:0' } + @{ Width = 1056; Height = 1872; Filter = 'scale=-2:1920,crop=1080:1920:(in_w-1080)/2:0' } + @{ Width = 704; Height = 1248; Filter = 'scale=-2:1280,crop=720:1280:(in_w-720)/2:0' } + + # Kling + @{ Width = 1920; Height = 1072; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' } + @{ Width = 1928; Height = 1072; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' } + @{ Width = 1920; Height = 1076; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' } + # 1916x1080 is already 1080 tall: pad the missing 4 columns instead of rescaling. + @{ Width = 1916; Height = 1080; Filter = 'pad=1920:1082:2:1:black,crop=1920:1080:0:1' } + + # Wan + @{ Width = 1928; Height = 1076; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' } + @{ Width = 1926; Height = 1076; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' } + @{ Width = 1284; Height = 716; Filter = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2' } +) + $sourceFull = (Resolve-Path -LiteralPath $Source).Path $targetFull = (Resolve-Path -LiteralPath $Target).Path @@ -112,76 +158,18 @@ foreach ($video in $videos) { $inQuality = Format-Quality -BitRate $inInfo.BitRate $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } - $vf = $null - if ($inInfo.Width -eq 1920 -and $inInfo.Height -eq 1088) { - $vf = 'crop=1920:1080:0:4' - } - elseif ($inInfo.Width -eq 1280 -and $inInfo.Height -eq 704) { - $vf = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2' - } - elseif ($inInfo.Width -eq 1088 -and $inInfo.Height -eq 1920) { - $vf = 'crop=1080:1920:4:0' - } - elseif ($inInfo.Width -eq 704 -and $inInfo.Height -eq 1280) { - $vf = 'scale=720:-2,crop=720:1280:(in_w-720)/2:(in_h-1280)/2' - } - elseif ($inInfo.Width -eq 832 -and $inInfo.Height -eq 448) { - $vf = 'scale=-2:480,crop=854:480:(in_w-854)/2:(in_h-480)/2' - } - elseif ($inInfo.Width -eq 448 -and $inInfo.Height -eq 832) { - $vf = 'scale=480:-2,crop=480:854:(in_w-480)/2:(in_h-854)/2' - } - elseif ($inInfo.Width -eq 1600 -and $inInfo.Height -eq 896) { - $vf = 'scale=-2:900,crop=1600:900:(in_w-1600)/2:(in_h-900)/2' - } - elseif ($inInfo.Width -eq 640 -and $inInfo.Height -eq 384) { - $vf = 'scale=684:-2,crop=684:384:(in_w-684)/2:(in_h-384)/2' - } - elseif ($inInfo.Width -eq 576 -and $inInfo.Height -eq 320) { - $vf = 'crop=570:320:3:0' - } - elseif ($inInfo.Width -eq 896 -and $inInfo.Height -eq 1600) { - $vf = 'scale=900:-2,crop=900:1600:(in_w-900)/2:(in_h-1600)/2' - } - elseif ($inInfo.Width -eq 384 -and $inInfo.Height -eq 640) { - $vf = 'scale=-2:684,crop=384:684:(in_w-384)/2:(in_h-684)/2' - } - elseif ($inInfo.Width -eq 320 -and $inInfo.Height -eq 576) { - $vf = 'crop=320:570:0:3' - } - elseif ($inInfo.Width -eq 1248 -and $inInfo.Height -eq 704) { - $vf = 'scale=1280:-2,crop=1280:720:0:(in_h-720)/2' - } - elseif ($inInfo.Width -eq 1872 -and $inInfo.Height -eq 1056) { - $vf = 'scale=1920:-2,crop=1920:1080:0:(in_h-1080)/2' - } - elseif ($inInfo.Width -eq 2160 -and $inInfo.Height -eq 1216) { - $vf = 'scale=1920:-2,crop=1920:1080:0:(in_h-1080)/2' - } - elseif ($inInfo.Width -eq 2496 -and $inInfo.Height -eq 1408) { - $vf = 'scale=2560:-2,crop=2560:1440:0:(in_h-1440)/2' - } - elseif ($inInfo.Width -eq 3744 -and $inInfo.Height -eq 2112) { - $vf = 'scale=3840:-2,crop=3840:2160:0:(in_h-2160)/2' - } - elseif ($inInfo.Width -eq 704 -and $inInfo.Height -eq 1248) { - $vf = 'scale=-2:1280,crop=720:1280:(in_w-720)/2:0' - } - elseif ($inInfo.Width -eq 1056 -and $inInfo.Height -eq 1872) { - $vf = 'scale=-2:1920,crop=1080:1920:(in_w-1080)/2:0' - } - elseif ($inInfo.Width -eq 1216 -and $inInfo.Height -eq 2160) { - $vf = 'scale=-2:1920,crop=1080:1920:(in_w-1080)/2:0' - } - elseif ($inInfo.Width -eq 1408 -and $inInfo.Height -eq 2496) { - $vf = 'scale=-2:2560,crop=1440:2560:(in_w-1440)/2:0' - } - elseif ($inInfo.Width -eq 2112 -and $inInfo.Height -eq 3744) { - $vf = 'scale=-2:3840,crop=2160:3840:(in_w-2160)/2:0' + $cropProfile = $cropProfiles | Where-Object { $_.Width -eq $inInfo.Width -and $_.Height -eq $inInfo.Height } | Select-Object -First 1 + + 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)" diff --git a/README.md b/README.md index 915e279..681d926 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,7 @@ To install rife-ncnn-vulkan on Windows, download it from [rife-ncnn-vulkan](http | Compare-Videos.ps1 | Combine 2 to 4 video files into a single 1920x1080 side-by-side or grid comparison video | | Create-FullSubtitle.ps1 | Creates a full subtitle file from a video file and two SRT subtitle files by combining them with adjusted timing and sequential numbering | | Create-ShortClip.ps1 | Create short clips from videos with cropping and overlay options | -| Crop-Clips.ps1 | Batch crop videos generated by LTX-2 from 1920x1088 or 1280x704 resolution | -| Crop-ClipsKling.ps1 | Batch crop videos generated by Kling from 1920x1072 or 1928x1072 resolution | -| Crop-ClipsWan.ps1 | Batch crop videos generated by Wan from 1928x1076 resolution | +| Crop-Clips.ps1 | Batch crop videos generated by LTX-2, Kling and Wan from their non-standard resolutions to standard ones (also scales any square video to 1080x1080) | | Get-Frames.ps1 | Extract a range of frames to PNG images | | Get-FramesCount.ps1 | Get total frame count of a video | | Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG |