Add -Reverse parameter to reencode video backward while keeping audio forward, and add 1284x716 to 1280x720 crop support in Crop-ClipsWan

This commit is contained in:
2026-06-07 16:44:21 -04:00
parent 201cc2625f
commit fcfa99dddc
2 changed files with 55 additions and 13 deletions
+39 -8
View File
@@ -33,6 +33,11 @@
Constant Rate Factor for video encoding (0-51, lower is better quality). When specified, overrides both the video bitrate and the default -crf 8 value. Only used when re-encoding is required. Constant Rate Factor for video encoding (0-51, lower is better quality). When specified, overrides both the video bitrate and the default -crf 8 value. Only used when re-encoding is required.
.PARAMETER Metadata .PARAMETER Metadata
When present, copies available JSON metadata from the source video file to the target video file. When present, copies available JSON metadata from the source video file to the target video file.
.PARAMETER Reverse
When present, re-encodes the video so it plays backward (from the last selected frame to the first).
Honours all other parameters (StartFrame/EndFrame range, Size, FPS, CRF, container choice, etc.).
Audio is NOT reversed; it is processed exactly as it would be without -Reverse (trimmed/replaced/copied as applicable) and plays forward alongside the reversed video.
Forces a video re-encode, since the reverse filter cannot be applied to a stream-copied video.
.DESCRIPTION .DESCRIPTION
This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion. This script uses ffmpeg to clone (copy) a video file with optional frame range and audio exclusion.
When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly. When Sequence is provided, it extracts a specific frame to PNG and then clones the video accordingly.
@@ -71,7 +76,10 @@ param(
[int]$CRF = -1, [int]$CRF = -1,
[Parameter(Mandatory = $false)] [Parameter(Mandatory = $false)]
[switch]$Metadata [switch]$Metadata,
[Parameter(Mandatory = $false)]
[switch]$Reverse
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
@@ -173,7 +181,10 @@ if (-not [string]::IsNullOrWhiteSpace($Audio)) {
# rather than stream-copied. # rather than stream-copied.
$ffprobeExtAudioOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $AudioPath 2>&1 $ffprobeExtAudioOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $AudioPath 2>&1
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
$externalAudioCodec = ($ffprobeExtAudioOutput | Select-Object -First 1).Trim().ToLowerInvariant() $extAudioLine = $ffprobeExtAudioOutput | Select-Object -First 1
if ($extAudioLine) {
$externalAudioCodec = ([string]$extAudioLine).Trim().ToLowerInvariant()
}
} }
} }
@@ -215,7 +226,8 @@ if ($ffprobeExit -ne 0) {
exit 1 exit 1
} }
$fpsRatio = ($ffprobeFpsOutput | Select-Object -First 1).Trim() $fpsRatioLine = $ffprobeFpsOutput | Select-Object -First 1
$fpsRatio = if ($fpsRatioLine) { ([string]$fpsRatioLine).Trim() } else { '' }
if (-not $fpsRatio -or $fpsRatio -notmatch '^\d+/\d+$') { if (-not $fpsRatio -or $fpsRatio -notmatch '^\d+/\d+$') {
Write-Error ('Could not determine frame rate for: {0}' -f $SourcePath) Write-Error ('Could not determine frame rate for: {0}' -f $SourcePath)
@@ -235,7 +247,8 @@ if ($ffprobeExit -ne 0) {
exit 1 exit 1
} }
$totalFrames = ($ffprobeFramesOutput | Select-Object -First 1).Trim() $totalFramesLine = $ffprobeFramesOutput | Select-Object -First 1
$totalFrames = if ($totalFramesLine) { ([string]$totalFramesLine).Trim() } else { '' }
if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') { if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
Write-Error ('Could not determine frame count for: {0}' -f $SourcePath) Write-Error ('Could not determine frame count for: {0}' -f $SourcePath)
@@ -250,10 +263,13 @@ $ffprobeExit = $LASTEXITCODE
$videoBitrate = $null $videoBitrate = $null
if ($ffprobeExit -eq 0) { if ($ffprobeExit -eq 0) {
$bitrateValue = ($ffprobeBitrateOutput | Select-Object -First 1).Trim() $bitrateLine = $ffprobeBitrateOutput | Select-Object -First 1
if ($bitrateLine) {
$bitrateValue = ([string]$bitrateLine).Trim()
if ($bitrateValue -and $bitrateValue -match '^\d+$') { if ($bitrateValue -and $bitrateValue -match '^\d+$') {
$videoBitrate = $bitrateValue $videoBitrate = $bitrateValue
} }
}
} }
# Probe source video and audio codecs. When the source codec isn't on the # Probe source video and audio codecs. When the source codec isn't on the
@@ -262,13 +278,19 @@ if ($ffprobeExit -eq 0) {
$ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1 $ffprobeVideoCodecOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
$sourceVideoCodec = '' $sourceVideoCodec = ''
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
$sourceVideoCodec = ($ffprobeVideoCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant() $vCodecLine = $ffprobeVideoCodecOutput | Select-Object -First 1
if ($vCodecLine) {
$sourceVideoCodec = ([string]$vCodecLine).Trim().ToLowerInvariant()
}
} }
$ffprobeAudioCodecOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1 $ffprobeAudioCodecOutput = & ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
$sourceAudioCodec = '' $sourceAudioCodec = ''
if ($LASTEXITCODE -eq 0) { if ($LASTEXITCODE -eq 0) {
$sourceAudioCodec = ($ffprobeAudioCodecOutput | Select-Object -First 1).Trim().ToLowerInvariant() $aCodecLine = $ffprobeAudioCodecOutput | Select-Object -First 1
if ($aCodecLine) {
$sourceAudioCodec = ([string]$aCodecLine).Trim().ToLowerInvariant()
}
} }
# A source codec not on the target container's stream-copy list forces a re-encode # A source codec not on the target container's stream-copy list forces a re-encode
@@ -364,6 +386,9 @@ if ($scaleWidth -gt 0) {
if ($FPS -gt 0) { if ($FPS -gt 0) {
Write-Host (' FPS: {0}' -f $FPS) Write-Host (' FPS: {0}' -f $FPS)
} }
if ($Reverse) {
Write-Host ' Reverse: video reversed, audio unchanged'
}
Write-Host (' Target: {0}' -f $TargetPath) Write-Host (' Target: {0}' -f $TargetPath)
$ffmpegArgs = @( $ffmpegArgs = @(
@@ -375,7 +400,7 @@ if ($AudioPath) {
$ffmpegArgs += @('-i', $AudioPath) $ffmpegArgs += @('-i', $AudioPath)
} }
$needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0 -or $videoCodecMismatch $needsReencode = $scaleWidth -gt 0 -or $FPS -gt 0 -or $videoCodecMismatch -or $Reverse
if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) { if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
$filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame) $filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame)
@@ -386,6 +411,9 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
if ($FPS -gt 0) { if ($FPS -gt 0) {
$filterParts += ('fps={0}' -f $FPS) $filterParts += ('fps={0}' -f $FPS)
} }
if ($Reverse) {
$filterParts += 'reverse'
}
$videoFilter = $filterParts -join ',' $videoFilter = $filterParts -join ','
$ffmpegArgs += @( $ffmpegArgs += @(
'-vf', $videoFilter '-vf', $videoFilter
@@ -410,6 +438,9 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
if ($FPS -gt 0) { if ($FPS -gt 0) {
$filterParts += ('fps={0}' -f $FPS) $filterParts += ('fps={0}' -f $FPS)
} }
if ($Reverse) {
$filterParts += 'reverse'
}
if ($filterParts.Count -gt 0) { if ($filterParts.Count -gt 0) {
$videoFilter = $filterParts -join ',' $videoFilter = $filterParts -join ','
$ffmpegArgs += @('-vf', $videoFilter) $ffmpegArgs += @('-vf', $videoFilter)
+14 -3
View File
@@ -6,7 +6,7 @@
.PARAMETER Target .PARAMETER Target
Target directory for cropped video files. Target directory for cropped video files.
.DESCRIPTION .DESCRIPTION
Crops video clips from 1928x1076 or 1926x1076 to 1920x1080 using ffmpeg. Crops video clips from 1928x1076 or 1926x1076 to 1920x1080, or from 1284x716 to 1280x720 using ffmpeg.
Supports -WhatIf and -Confirm for safe execution. Supports -WhatIf and -Confirm for safe execution.
#> #>
[CmdletBinding(SupportsShouldProcess)] [CmdletBinding(SupportsShouldProcess)]
@@ -113,7 +113,8 @@ foreach ($video in $videos) {
$inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' } $inAudioText = if ($inInfo.HasAudio) { 'yes' } else { 'no' }
$isValidResolution = ($inInfo.Width -eq 1928 -and $inInfo.Height -eq 1076) -or $isValidResolution = ($inInfo.Width -eq 1928 -and $inInfo.Height -eq 1076) -or
($inInfo.Width -eq 1926 -and $inInfo.Height -eq 1076) ($inInfo.Width -eq 1926 -and $inInfo.Height -eq 1076) -or
($inInfo.Width -eq 1284 -and $inInfo.Height -eq 716)
if (-not $isValidResolution) { if (-not $isValidResolution) {
Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)" Write-Host "Skipping: $($inInfo.FileName) (resolution $inRes)"
@@ -132,10 +133,20 @@ foreach ($video in $videos) {
$outCropPath = Join-Path $targetFull ($video.BaseName + '.crop.mp4') $outCropPath = Join-Path $targetFull ($video.BaseName + '.crop.mp4')
$outFinalPath = Join-Path $targetFull $video.Name $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 = @( $ffArgs = @(
'-y', '-y',
'-i', $inputPath, '-i', $inputPath,
'-vf', 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(in_h-1080)/2' '-vf', $videoFilter
) )
if ($inInfo.HasAudio) { if ($inInfo.HasAudio) {