Compare commits

...
2 Commits
7 changed files with 111 additions and 502 deletions

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.
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
+55 -67
View File
@@ -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)"
-2
View File
@@ -1,2 +0,0 @@
@echo off
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Crop-ClipsKling.ps1" %*
-202
View File
@@ -1,202 +0,0 @@
<#
.SYNOPSIS
Crops video clips using ffmpeg.
.PARAMETER Source
Source directory containing video files.
.PARAMETER Target
Target directory for cropped video files.
.DESCRIPTION
Crops video clips using ffmpeg.
Supports -WhatIf and -Confirm for safe execution.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $false)]
[string]$Source = '.',
[Parameter(Mandatory = $false)]
[string]$Target = '.'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-VideoInfo {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$json = & ffprobe -v error -print_format json -show_streams -show_format -- "$Path"
if ($LASTEXITCODE -ne 0 -or -not $json) {
throw "ffprobe failed for: $Path"
}
$info = $json | ConvertFrom-Json
$videoStream = $info.streams | Where-Object { $_.codec_type -eq 'video' } | Select-Object -First 1
if (-not $videoStream) {
throw "No video stream found in: $Path"
}
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
$fps = $num / $den
}
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
FileName = [System.IO.Path]::GetFileName($Path)
Width = [int]$videoStream.width
Height = [int]$videoStream.height
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoBitRate
}
}
function Format-Quality {
param(
[Parameter(Mandatory = $false)]
$BitRate
)
if (-not $BitRate -or $BitRate -notmatch '^\d+$') {
return 'unknown'
}
$kbps = [math]::Round(([double]$BitRate) / 1000.0)
return "${kbps}kbps"
}
$sourceFull = (Resolve-Path -LiteralPath $Source).Path
$targetFull = (Resolve-Path -LiteralPath $Target).Path
$BackupRequired = ($sourceFull.TrimEnd('\') -ieq $targetFull.TrimEnd('\'))
$processedCount = 0
$videos = Get-ChildItem -LiteralPath $sourceFull -Filter '*.mp4' -File
foreach ($video in $videos) {
if ($video.Name -like '*.bak.mp4' -or $video.Name -like '*.skip.mp4') {
continue
}
$inputPath = $video.FullName
$inInfo = $null
try {
$inInfo = Get-VideoInfo -Path $inputPath
}
catch {
Write-Error $_
throw
}
$inRes = "{0}x{1}" -f $inInfo.Width, $inInfo.Height
$is1072 = ($inInfo.Height -eq 1072 -and ($inInfo.Width -eq 1920 -or $inInfo.Width -eq 1928))
$is1916x1080 = ($inInfo.Width -eq 1916 -and $inInfo.Height -eq 1080)
$is1920x1076 = ($inInfo.Width -eq 1920 -and $inInfo.Height -eq 1076)
if (-not $is1072 -and -not $is1916x1080 -and -not $is1920x1076) {
Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)"
continue
}
if ($BackupRequired) {
$backupPath = Join-Path $video.DirectoryName ($video.BaseName + '.bak' + $video.Extension)
if ($PSCmdlet.ShouldProcess($backupPath, 'Create backup of original video')) {
Copy-Item -LiteralPath $inputPath -Destination $backupPath -Force
}
}
$outCropPath = Join-Path $targetFull ($video.BaseName + '.crop.mp4')
$outFinalPath = Join-Path $targetFull $video.Name
if ($is1916x1080) {
$vfFilter = 'pad=1920:1082:2:1:black,crop=1920:1080:0:1'
}
else {
$vfFilter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2'
}
$ffArgs = @(
'-y',
'-i', $inputPath,
'-vf', $vfFilter
)
if ($inInfo.HasAudio) {
$ffArgs += @('-c:a', 'copy')
}
else {
$ffArgs += @('-an')
}
if ($inInfo.VideoBitRate -and $inInfo.VideoBitRate -match '^\d+$') {
$ffArgs += @('-b:v', $inInfo.VideoBitRate)
}
else {
$ffArgs += @('-crf', '8', '-preset', 'slow')
}
$ffArgs += @($outCropPath)
if ($PSCmdlet.ShouldProcess($outCropPath, 'Crop video with ffmpeg')) {
$ffmpegOutput = & ffmpeg @ffArgs 2>&1
$ffmpegExit = $LASTEXITCODE
if ($ffmpegExit -ne 0) {
if (Test-Path -LiteralPath $outCropPath) {
Remove-Item -LiteralPath $outCropPath -Force -ErrorAction SilentlyContinue
}
$ffmpegOutput | ForEach-Object { Write-Error $_ }
Write-Error "ffmpeg failed while cropping: $($inInfo.FileName)"
Write-Host "ffmpeg $($ffArgs -join ' ')"
exit 1
}
if (($BackupRequired) -and ($PSCmdlet.ShouldProcess($inputPath, 'Remove original video after backup'))) {
Remove-Item -LiteralPath $inputPath -Force
}
if ((Test-Path -LiteralPath $outFinalPath) -and ($PSCmdlet.ShouldProcess($outFinalPath, 'Remove existing output file'))) {
Remove-Item -LiteralPath $outFinalPath -Force
}
if ($PSCmdlet.ShouldProcess("$outCropPath -> $outFinalPath", 'Move cropped video to final location')) {
Move-Item -LiteralPath $outCropPath -Destination $outFinalPath -Force
}
} else {
$formattedArgs = $ffArgs | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } }
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' }
Write-Host "Output: Filename=$($outInfo.FileName) Resolution=$outRes FPS=$outFpsText Quality=$outQuality Audio=$outAudioText"
$processedCount++
}
Write-Host "Processed (cropped) videos: $processedCount"
-2
View File
@@ -1,2 +0,0 @@
@echo off
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Crop-ClipsWan.ps1" %*
-210
View File
@@ -1,210 +0,0 @@
<#
.SYNOPSIS
Crops video clips using ffmpeg.
.PARAMETER Source
Source directory containing video files.
.PARAMETER Target
Target directory for cropped video files.
.DESCRIPTION
Crops video clips from 1928x1076 or 1926x1076 to 1920x1080, or from 1284x716 to 1280x720 using ffmpeg.
Supports -WhatIf and -Confirm for safe execution.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $false)]
[string]$Source = '.',
[Parameter(Mandatory = $false)]
[string]$Target = '.'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Get-VideoInfo {
param(
[Parameter(Mandatory = $true)]
[string]$Path
)
$json = & ffprobe -v error -print_format json -show_streams -show_format -- "$Path"
if ($LASTEXITCODE -ne 0 -or -not $json) {
throw "ffprobe failed for: $Path"
}
$info = $json | ConvertFrom-Json
$videoStream = $info.streams | Where-Object { $_.codec_type -eq 'video' } | Select-Object -First 1
if (-not $videoStream) {
throw "No video stream found in: $Path"
}
$audioStream = $info.streams | Where-Object { $_.codec_type -eq 'audio' } | Select-Object -First 1
# Safe property accessor: returns $null when the property is absent (strict-mode friendly).
$get = { param($obj, $name) if ($obj -and ($obj.PSObject.Properties.Name -contains $name)) { $obj.$name } else { $null } }
$rFrameRate = & $get $videoStream 'r_frame_rate'
$fps = $null
if ($rFrameRate -and $rFrameRate -match '^(\d+)/(\d+)$') {
$num = [double]$Matches[1]
$den = [double]$Matches[2]
if ($den -ne 0) {
$fps = $num / $den
}
}
$videoBitRate = & $get $videoStream 'bit_rate'
$formatBitRate = & $get $info.format 'bit_rate'
$bitRate = if ($videoBitRate) { $videoBitRate } else { $formatBitRate }
[pscustomobject]@{
Path = $Path
FileName = [System.IO.Path]::GetFileName($Path)
Width = [int]$videoStream.width
Height = [int]$videoStream.height
Fps = $fps
BitRate = $bitRate
HasAudio = [bool]$audioStream
VideoBitRate = $videoBitRate
}
}
function Format-Quality {
param(
[Parameter(Mandatory = $false)]
$BitRate
)
if (-not $BitRate -or $BitRate -notmatch '^\d+$') {
return 'unknown'
}
$kbps = [math]::Round(([double]$BitRate) / 1000.0)
return "${kbps}kbps"
}
$sourceFull = (Resolve-Path -LiteralPath $Source).Path
$targetFull = (Resolve-Path -LiteralPath $Target).Path
$BackupRequired = ($sourceFull.TrimEnd('\') -ieq $targetFull.TrimEnd('\'))
$processedCount = 0
$videos = Get-ChildItem -LiteralPath $sourceFull -Filter '*.mp4' -File
foreach ($video in $videos) {
if ($video.Name -like '*.bak.mp4' -or $video.Name -like '*.skip.mp4') {
continue
}
$inputPath = $video.FullName
$inInfo = $null
try {
$inInfo = Get-VideoInfo -Path $inputPath
}
catch {
Write-Error $_
throw
}
$inRes = "{0}x{1}" -f $inInfo.Width, $inInfo.Height
$inFpsText = if ($inInfo.Fps) { "{0:0.###}" -f $inInfo.Fps } else { 'unknown' }
$inQuality = Format-Quality -BitRate $inInfo.BitRate
$inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' }
$isValidResolution = ($inInfo.Width -eq 1928 -and $inInfo.Height -eq 1076) -or
($inInfo.Width -eq 1926 -and $inInfo.Height -eq 1076) -or
($inInfo.Width -eq 1284 -and $inInfo.Height -eq 716)
if (-not $isValidResolution) {
Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)"
continue
}
Write-Host "Input : Filename=$($inInfo.FileName) Resolution=$inRes FPS=$inFpsText Quality=$inQuality Audio=$inAudioText"
if ($BackupRequired) {
$backupPath = Join-Path $video.DirectoryName ($video.BaseName + '.bak' + $video.Extension)
if ($PSCmdlet.ShouldProcess($backupPath, 'Create backup of original video')) {
Copy-Item -LiteralPath $inputPath -Destination $backupPath -Force
}
}
$outCropPath = Join-Path $targetFull ($video.BaseName + '.crop.mp4')
$outFinalPath = Join-Path $targetFull $video.Name
# Determine crop parameters based on input resolution
if ($inInfo.Width -eq 1284 -and $inInfo.Height -eq 716) {
# 1284x716: expand to 720 height, then crop to 1280x720
$videoFilter = 'scale=-2:720,crop=1280:720:(in_w-1280)/2:(in_h-720)/2'
}
else {
# 1928x1076 or 1926x1076: expand to 1080 height, then crop to 1920x1080
$videoFilter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2'
}
$ffArgs = @(
'-y',
'-i', $inputPath,
'-vf', $videoFilter
)
if ($inInfo.HasAudio) {
$ffArgs += @('-c:a', 'copy')
}
else {
$ffArgs += @('-an')
}
if ($inInfo.VideoBitRate -and $inInfo.VideoBitRate -match '^\d+$') {
$ffArgs += @('-b:v', $inInfo.VideoBitRate)
}
else {
$ffArgs += @('-crf', '8', '-preset', 'slow')
}
$ffArgs += @($outCropPath)
if ($PSCmdlet.ShouldProcess($outCropPath, 'Crop video with ffmpeg')) {
$ffmpegOutput = & ffmpeg @ffArgs 2>&1
$ffmpegExit = $LASTEXITCODE
if ($ffmpegExit -ne 0) {
if (Test-Path -LiteralPath $outCropPath) {
Remove-Item -LiteralPath $outCropPath -Force -ErrorAction SilentlyContinue
}
$ffmpegOutput | ForEach-Object { Write-Error $_ }
Write-Error "ffmpeg failed while cropping: $($inInfo.FileName)"
Write-Host "ffmpeg $($ffArgs -join ' ')"
exit 1
}
if (($BackupRequired) -and ($PSCmdlet.ShouldProcess($inputPath, 'Remove original video after backup'))) {
Remove-Item -LiteralPath $inputPath -Force
}
if ((Test-Path -LiteralPath $outFinalPath) -and ($PSCmdlet.ShouldProcess($outFinalPath, 'Remove existing output file'))) {
Remove-Item -LiteralPath $outFinalPath -Force
}
if ($PSCmdlet.ShouldProcess("$outCropPath -> $outFinalPath", 'Move cropped video to final location')) {
Move-Item -LiteralPath $outCropPath -Destination $outFinalPath -Force
}
} else {
$formattedArgs = $ffArgs | ForEach-Object { if ($_ -match '\s') { "`"$_`"" } else { $_ } }
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' }
Write-Host "Output: Filename=$($outInfo.FileName) Resolution=$outRes FPS=$outFpsText Quality=$outQuality Audio=$outAudioText"
$processedCount++
}
Write-Host "Processed (cropped) videos: $processedCount"
+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 |
| 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 |