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

This commit is contained in:
guru committed 2026-09-20 16:16:55 -04:00
1 parent 274dac5786
commit b823259ab1
3 files changed
+111 -86

No files matched your search

+55 -16
View File
@@ -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. 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, 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). 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 .PARAMETER Effect
The effect to apply to the side bars. Possible values are: The effect to apply to the side bars. Possible values are:
black: The side bars will be solid black (#000000). 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. 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. 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. 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 .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". Possible values: 480p, 720p, 1080p, HD, 1440p, 2K, 2160p, 4K, or custom size in format "width:height".
.PARAMETER FPS .PARAMETER FPS
Frames per second for the output video. Uses the input video's FPS if not specified. 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. 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). 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. 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)] [CmdletBinding(SupportsShouldProcess)]
param( param(
@@ -45,6 +53,9 @@ param(
[Parameter(Mandatory = $true)] [Parameter(Mandatory = $true)]
[string]$Effect, [string]$Effect,
[Parameter(Mandatory = $false)]
[switch]$Vertical,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[string]$Size = $null, [string]$Size = $null,
@@ -136,7 +147,12 @@ function Resolve-Size {
[int]$SourceWidth, [int]$SourceWidth,
[Parameter(Mandatory = $true)] [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) # Standard 16:9 sizes (largest first only matters for closest-fit lookup)
@@ -154,6 +170,9 @@ function Resolve-Size {
if ($Value) { if ($Value) {
$named = $standards | Where-Object { $_.Name -ieq $Value } | Select-Object -First 1 $named = $standards | Where-Object { $_.Name -ieq $Value } | Select-Object -First 1
if ($named) { if ($named) {
if ($Vertical) {
return [pscustomobject]@{ Width = $named.Height; Height = $named.Width }
}
return [pscustomobject]@{ Width = $named.Width; Height = $named.Height } 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'." 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 # Pick the smallest standard 16:9 preset that holds the source's bounding box
# the source's longer side. The foreground is scaled down with # once that box is laid out in the frame's orientation: the preset's long side
# 'force_original_aspect_ratio=decrease', so the source is allowed to shrink # (width) must be at least the source's long side AND the preset's short side
# to fit; we only need the preset to be "big enough" relative to the source's # (height) must be at least the source's short side. Checking both sides matters
# largest dimension. This produces the documented behavior: # 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) # 1080x1920 portrait -> 1920x1080 (vertical bars on either side)
# 720x1280 portrait -> 1280x720 # 720x1280 portrait -> 1280x720
# 1080x1080 square -> 1920x1080 (vertical bars, source kept at 1080x1080)
# 2560x1080 landscape -> 2560x1440 (horizontal bars top/bottom) # 2560x1080 landscape -> 2560x1440 (horizontal bars top/bottom)
# With -Vertical the same preset is used transposed (e.g. 1080x1080 -> 1080x1920).
$longSide = [math]::Max($SourceWidth, $SourceHeight) $longSide = [math]::Max($SourceWidth, $SourceHeight)
$shortSide = [math]::Min($SourceWidth, $SourceHeight)
$unique = $standards | Sort-Object { $_.Width } -Unique $unique = $standards | Sort-Object { $_.Width } -Unique
foreach ($s in $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 } return [pscustomobject]@{ Width = $s.Width; Height = $s.Height }
} }
} }
# Source larger than any standard preset: build a 16:9 frame whose width # Source larger than any standard preset: build a 16:9 (or 9:16) frame that
# matches the source's longer side (rounded to even). # holds the source's bounding box, rounded to even dimensions.
$w = $longSide + ($longSide % 2) $long = [math]::Max($longSide, [int][math]::Ceiling($shortSide * 16.0 / 9.0))
$h = [int][math]::Ceiling($w * 9.0 / 16.0) $short = [math]::Max($shortSide, [int][math]::Ceiling($long * 9.0 / 16.0))
if ($h % 2) { $h++ } if ($long % 2) { $long++ }
return [pscustomobject]@{ Width = $w; Height = $h } 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. # 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). # (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 # For 'color:RRGGBB', the colon is replaced with a dash to keep the filename valid
# (e.g. -Effect color:FFFF00 -> video-color-ffff00.mp4). # (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)) { if ([string]::IsNullOrWhiteSpace($Target)) {
$srcDir = [System.IO.Path]::GetDirectoryName($sourceFull) $srcDir = [System.IO.Path]::GetDirectoryName($sourceFull)
$srcBase = [System.IO.Path]::GetFileNameWithoutExtension($sourceFull) $srcBase = [System.IO.Path]::GetFileNameWithoutExtension($sourceFull)
$effectForName = $effectLower -replace ':', '-' $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)) { elseif (-not [System.IO.Path]::GetExtension($Target)) {
$Target = "$Target.mp4" $Target = "$Target.mp4"
@@ -271,7 +304,13 @@ $inQuality = Format-Quality -BitRate $inInfo.BitRate
$inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' }
Write-Host "Input : Filename=$($inInfo.FileName) Resolution=$inRes FPS=$inFpsText Quality=$inQuality Audio=$inAudioText" 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 $outWidth = $outSize.Width
$outHeight = $outSize.Height $outHeight = $outSize.Height
+55 -67
View File
@@ -6,7 +6,9 @@
.PARAMETER Target .PARAMETER Target
Target directory for cropped video files. Target directory for cropped video files.
.DESCRIPTION .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. Supports -WhatIf and -Confirm for safe execution.
#> #>
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
@@ -83,6 +85,50 @@ function Format-Quality {
return "${kbps}kbps" 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 $sourceFull = (Resolve-Path -LiteralPath $Source).Path
$targetFull = (Resolve-Path -LiteralPath $Target).Path $targetFull = (Resolve-Path -LiteralPath $Target).Path
@@ -112,76 +158,18 @@ foreach ($video in $videos) {
$inQuality = Format-Quality -BitRate $inInfo.BitRate $inQuality = Format-Quality -BitRate $inInfo.BitRate
$inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' }
$vf = $null $cropProfile = $cropProfiles | Where-Object { $_.Width -eq $inInfo.Width -and $_.Height -eq $inInfo.Height } | Select-Object -First 1
if ($inInfo.Width -eq 1920 -and $inInfo.Height -eq 1088) {
$vf = 'crop=1920:1080:0:4' if ($cropProfile) {
} $vf = $cropProfile.Filter
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'
} }
elseif ($inInfo.Width -eq $inInfo.Height -and $inInfo.Width -ne 1080) { 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' $vf = 'scale=1080:1080'
} }
else {
$vf = $null
}
if (-not $vf) { if (-not $vf) {
Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)"
+1 -3
View File
@@ -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 | | 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-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 | | 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-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) |
| 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 |
| Get-Frames.ps1 | Extract a range of frames to PNG images | | Get-Frames.ps1 | Extract a range of frames to PNG images |
| Get-FramesCount.ps1 | Get total frame count of a video | | Get-FramesCount.ps1 | Get total frame count of a video |
| Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG | | Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG |