Compare commits
30
Commits
b823259ab1
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad95f0728 | ||
|
|
fcc208e351 | ||
|
|
de238fb9c3 | ||
|
|
4378e3fd99 | ||
|
|
60feb778d3 | ||
|
|
0befcfe8a3 | ||
|
|
8b46091f9a | ||
|
|
cda0b6a4b8 | ||
|
|
50c9d831e9 | ||
|
|
1227c6e153 | ||
|
|
cf41fb0ce5 | ||
|
|
2290672d5b | ||
|
|
514f68ca2d | ||
|
|
693655d07d | ||
|
|
95e8d94298 | ||
|
|
c79451b373 | ||
|
|
373efac4af | ||
|
|
0665c8d73f | ||
|
|
4cc09d6369 | ||
|
|
61894433d9 | ||
|
|
553d0ea18d | ||
|
|
c8c5a5d75c | ||
|
|
45f1396769 | ||
|
|
9088022ef7 | ||
|
|
c057f804bf | ||
|
|
186daada82 | ||
|
|
ae7880af95 | ||
|
|
70b4b83be0 | ||
|
|
aaa293734e | ||
|
|
d1655075dc |
No files matched your search
+4
-3
@@ -33,7 +33,7 @@
|
||||
ffmpeg input, so any -Size / -LTX cover-scale-then-crop is applied to the upscaled image.
|
||||
Requires upscayl-bin.exe in PATH, the UPSCAYL_HOME environment variable pointing to the
|
||||
upscayl-bin install directory, and the NCNN_GPU_ID environment variable set to the desired
|
||||
GPU device ID (-1 for CPU, 0/1/2/... for GPU).
|
||||
GPU device ID (0/1/2/... for a GPU, or -1 to omit the -g flag so upscayl-bin uses its default device).
|
||||
.PARAMETER Model
|
||||
Upscayl model name (without extension) to use when -Upscale is specified. Defaults to
|
||||
'4xNomos8kSC'. The model files (<Model>.bin and <Model>.param) must exist under
|
||||
@@ -328,7 +328,7 @@ if ($Upscale -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
if ($gpuId -notmatch '^-?\d+$') {
|
||||
Write-Error ('NCNN_GPU_ID must be an integer (-1 for CPU, 0+ for GPU). Current value: {0}' -f $gpuId)
|
||||
Write-Error ('NCNN_GPU_ID must be an integer (-1 for the default device, 0+ for a GPU index). Current value: {0}' -f $gpuId)
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -337,7 +337,8 @@ if ($Upscale -gt 0) {
|
||||
Write-Host ('Upscaling source with upscayl-bin (x{0}, model={1}, gpu={2})...' -f $Upscale, $Model, $gpuId)
|
||||
Write-Host (' Upscayl temp file: {0}' -f $upscaledTempPath)
|
||||
|
||||
$upscaylArgs = @('-i', $SourcePath, '-o', $upscaledTempPath, '-s', $Upscale.ToString(), '-n', $Model, '-m', $modelsDir, '-g', $gpuId)
|
||||
$upscaylArgs = @('-i', $SourcePath, '-o', $upscaledTempPath, '-s', $Upscale.ToString(), '-n', $Model, '-m', $modelsDir)
|
||||
if ([int]$gpuId -ge 0) { $upscaylArgs += @('-g', $gpuId) }
|
||||
$upscaylCmdLine = '"{0}" {1}' -f $upscaylExePath, (($upscaylArgs | ForEach-Object {
|
||||
if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ }
|
||||
}) -join ' ')
|
||||
|
||||
+84
-21
@@ -14,11 +14,11 @@
|
||||
from the image sequence, ordered alphabetically (or reverse alphabetically if -Reverse is specified).
|
||||
- -Sequence and -Metadata are not allowed in this mode.
|
||||
- Defaults: 24 FPS, CRF 8, no audio (unless -Audio is provided).
|
||||
- -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse and -LTX are all honoured.
|
||||
- -Size, -FPS, -CRF, -Audio, -NoAudio, -StartFrame, -EndFrame, -Frames, -Reverse, -LTX and -Crop are all honoured.
|
||||
When Source is a supported image file (.png, .jpg, .jpeg, .webp, .bmp, .tiff, .tif, .gif) and Audio is provided, the script builds a video of the static image for the duration of the audio file.
|
||||
- -NoAudio is not allowed in this mode.
|
||||
- Defaults: 24 FPS, CRF 8, source image dimensions.
|
||||
- -Size, -FPS, -CRF and -LTX are honoured.
|
||||
- -Size, -FPS, -CRF, -LTX and -Crop are honoured.
|
||||
- -StartFrame, -EndFrame, -Frames, -Sequence, -Metadata and -Reverse are ignored.
|
||||
.PARAMETER Source
|
||||
Path to the input video file, or a directory containing PNG files with sequence numbers (e.g. frame_0000.png).
|
||||
@@ -36,7 +36,7 @@
|
||||
- start-end: Extract frames from start to end (e.g., "100-200")
|
||||
- positive_number: Extract frames from 0 to that number (e.g., "50")
|
||||
- -negative_number: Extract last N frames (e.g., "-10")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%", "-%")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%")
|
||||
- all: Extract all frames (same as "0-%")
|
||||
.PARAMETER Sequence
|
||||
Path to PNG file for extracted frame. Can be absolute or relative to current directory. .png extension is added if not provided. If EndFrame is -1, extracts the last frame and excludes it from the cloned video. If EndFrame is not -1, extracts the frame at EndFrame index and excludes it from the cloned video.
|
||||
@@ -74,6 +74,16 @@
|
||||
- 720p: 1280x704 (16:9), 704x1280 (9:16), 1088x576 (21:9), 576x1088 (9:21)
|
||||
- 480p: 832x448 (16:9), 448x832 (9:16), 896x512 (4:3), 512x896 (3:4)
|
||||
Forces a video re-encode (same as -Size).
|
||||
.PARAMETER Crop
|
||||
When present, the dimensions selected by -Size (including -LTX buckets) are obtained by center-cropping
|
||||
the source instead of resizing it, so the original pixel aspect ratio is kept (no stretching or scaling).
|
||||
Each dimension is cropped only when the source is bigger than the requested size; a dimension where the
|
||||
source is equal or smaller is left untouched (no upscaling or padding). Cropped dimensions are rounded
|
||||
down to an even number. Examples for a 1080x1080 source:
|
||||
- -Size 960:1080 -Crop: cropped horizontally to 960x1080.
|
||||
- -Size 720:720 -Crop: cropped horizontally and vertically to 720x720.
|
||||
Requires -Size. Without -Crop, the source is resized (default behaviour).
|
||||
Forces a video re-encode (same as -Size).
|
||||
.DESCRIPTION
|
||||
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.
|
||||
@@ -123,7 +133,10 @@ param(
|
||||
[switch]$Reverse,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$LTX
|
||||
[switch]$LTX,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$Crop
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -215,6 +228,14 @@ if (-not [string]::IsNullOrWhiteSpace($Frames)) {
|
||||
Write-Error '-Frames cannot be used together with -StartFrame, -EndFrame, or -Sequence'
|
||||
exit 1
|
||||
}
|
||||
if ($Frames -match '^-.*%') {
|
||||
Write-Error "Invalid Frames format '$Frames': '-%' is not supported. Use '%' for the last frame, or -N for the last N frames."
|
||||
exit 1
|
||||
}
|
||||
if ($Frames -match '^-0+$') {
|
||||
Write-Error "Invalid Frames format '$Frames': a negative count must be at least 1. Use -1 for the last frame."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$AudioPath = $null
|
||||
@@ -271,6 +292,31 @@ if (-not [string]::IsNullOrWhiteSpace($Size)) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($Crop -and [string]::IsNullOrWhiteSpace($Size)) {
|
||||
Write-Error '-Crop requires -Size'
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Builds the ffmpeg size filter for the requested output dimensions.
|
||||
# Without -Crop the source is resized (scale). With -Crop the source is center-cropped,
|
||||
# only on the dimensions where it is bigger than the requested size.
|
||||
function Get-SizeFilter {
|
||||
param([int]$SrcWidth, [int]$SrcHeight, [int]$OutWidth, [int]$OutHeight)
|
||||
if (-not $Crop) {
|
||||
return @{ Filter = ('scale={0}:{1}' -f $OutWidth, $OutHeight); Width = $OutWidth; Height = $OutHeight }
|
||||
}
|
||||
if ($SrcWidth -le 0 -or $SrcHeight -le 0) {
|
||||
Write-Error 'Could not determine source dimensions; -Crop cannot compute the crop area.'
|
||||
exit 1
|
||||
}
|
||||
$cropWidth = [Math]::Min($OutWidth, $SrcWidth)
|
||||
$cropHeight = [Math]::Min($OutHeight, $SrcHeight)
|
||||
$cropWidth -= $cropWidth % 2
|
||||
$cropHeight -= $cropHeight % 2
|
||||
return @{ Filter = ('crop={0}:{1}' -f $cropWidth, $cropHeight); Width = $cropWidth; Height = $cropHeight }
|
||||
}
|
||||
$cropNote = if ($Crop) { ' (cropped)' } else { '' }
|
||||
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
|
||||
# Image-to-video mode: when $Source is a supported image file and $Audio is provided.
|
||||
@@ -356,6 +402,12 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
|
||||
if ($imgOutWidth % 2) { $imgOutWidth++ }
|
||||
if ($imgOutHeight % 2) { $imgOutHeight++ }
|
||||
|
||||
if ($imgOutWidth -gt 0) {
|
||||
$imgSizeFilter = Get-SizeFilter $imgSrcWidth $imgSrcHeight $imgOutWidth $imgOutHeight
|
||||
$imgOutWidth = $imgSizeFilter.Width
|
||||
$imgOutHeight = $imgSizeFilter.Height
|
||||
}
|
||||
|
||||
$imgFfmpegArgs = @(
|
||||
'-y'
|
||||
'-loop', '1'
|
||||
@@ -365,7 +417,7 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
|
||||
)
|
||||
|
||||
if ($imgOutWidth -gt 0) {
|
||||
$imgFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $imgOutWidth, $imgOutHeight))
|
||||
$imgFfmpegArgs += @('-vf', $imgSizeFilter.Filter)
|
||||
}
|
||||
|
||||
$imgFfmpegArgs += $targetVideoEncodeArgs
|
||||
@@ -391,9 +443,9 @@ if (($supportedImageExts -contains $sourceExt) -and $AudioPath) {
|
||||
Write-Host (' FPS: {0}' -f $imgFPS)
|
||||
if ($imgOutWidth -gt 0) {
|
||||
if ($LTX) {
|
||||
Write-Host (' Size: {0}x{1} (LTX-2)' -f $imgOutWidth, $imgOutHeight)
|
||||
Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $imgOutWidth, $imgOutHeight, $cropNote)
|
||||
} else {
|
||||
Write-Host (' Size: {0}x{1}' -f $imgOutWidth, $imgOutHeight)
|
||||
Write-Host (' Size: {0}x{1}{2}' -f $imgOutWidth, $imgOutHeight, $cropNote)
|
||||
}
|
||||
}
|
||||
Write-Host (' CRF: {0}' -f $imgCRF)
|
||||
@@ -434,13 +486,13 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||
$maxPngIndex = $pngFiles.Count - 1
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Frames)) {
|
||||
$framesPattern = $Frames -replace '%', $maxPngIndex
|
||||
$framesPattern = if ($Frames -eq '%') { "$maxPngIndex-$maxPngIndex" } else { $Frames -replace '%', $maxPngIndex }
|
||||
if ($framesPattern -ieq 'all') { $framesPattern = "0-$maxPngIndex" }
|
||||
if ($framesPattern -match '^(\d+)-(\d+)$') {
|
||||
$StartFrame = [int]$Matches[1]
|
||||
$EndFrame = [int]$Matches[2]
|
||||
} elseif ($framesPattern -match '^-(\d+)$') {
|
||||
$StartFrame = [Math]::Max(0, $maxPngIndex - [int]$Matches[1])
|
||||
$StartFrame = [Math]::Max(0, $maxPngIndex - [int]$Matches[1] + 1)
|
||||
$EndFrame = $maxPngIndex
|
||||
} elseif ($framesPattern -match '^\d+$') {
|
||||
$StartFrame = 0
|
||||
@@ -542,6 +594,12 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||
if ($seqOutWidth % 2) { $seqOutWidth++ }
|
||||
if ($seqOutHeight % 2) { $seqOutHeight++ }
|
||||
|
||||
if ($seqOutWidth -gt 0) {
|
||||
$seqSizeFilter = Get-SizeFilter $seqSrcWidth $seqSrcHeight $seqOutWidth $seqOutHeight
|
||||
$seqOutWidth = $seqSizeFilter.Width
|
||||
$seqOutHeight = $seqSizeFilter.Height
|
||||
}
|
||||
|
||||
$concatTempFile = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
$frameDuration = (1.0 / $seqFPS).ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture)
|
||||
@@ -551,7 +609,6 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||
$concatLines += "file '$pfPath'"
|
||||
$concatLines += "duration $frameDuration"
|
||||
}
|
||||
$concatLines += "file '$($pngFiles[-1].FullName.Replace('\', '/'))'"
|
||||
[System.IO.File]::WriteAllLines($concatTempFile, $concatLines, [System.Text.UTF8Encoding]::new($false))
|
||||
|
||||
$seqFfmpegArgs = @(
|
||||
@@ -568,7 +625,7 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||
$seqFfmpegArgs += @('-r', $seqFPS.ToString('0.######', [System.Globalization.CultureInfo]::InvariantCulture))
|
||||
|
||||
if ($seqOutWidth -gt 0) {
|
||||
$seqFfmpegArgs += @('-vf', ('scale={0}:{1}' -f $seqOutWidth, $seqOutHeight))
|
||||
$seqFfmpegArgs += @('-vf', $seqSizeFilter.Filter)
|
||||
}
|
||||
|
||||
$seqFfmpegArgs += $targetVideoEncodeArgs
|
||||
@@ -593,9 +650,9 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
|
||||
Write-Host (' FPS: {0}' -f $seqFPS)
|
||||
if ($seqOutWidth -gt 0) {
|
||||
if ($LTX) {
|
||||
Write-Host (' Size: {0}x{1} (LTX-2)' -f $seqOutWidth, $seqOutHeight)
|
||||
Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $seqOutWidth, $seqOutHeight, $cropNote)
|
||||
} else {
|
||||
Write-Host (' Size: {0}x{1}' -f $seqOutWidth, $seqOutHeight)
|
||||
Write-Host (' Size: {0}x{1}{2}' -f $seqOutWidth, $seqOutHeight, $cropNote)
|
||||
}
|
||||
}
|
||||
Write-Host (' CRF: {0}' -f $seqCRF)
|
||||
@@ -668,13 +725,13 @@ $totalFramesInt = [int]$totalFrames
|
||||
$maxFrameIndex = $totalFramesInt - 1
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($Frames)) {
|
||||
$framesPattern = $Frames -replace '%', $maxFrameIndex
|
||||
$framesPattern = if ($Frames -eq '%') { "$maxFrameIndex-$maxFrameIndex" } else { $Frames -replace '%', $maxFrameIndex }
|
||||
if ($framesPattern -ieq 'all') { $framesPattern = "0-$maxFrameIndex" }
|
||||
if ($framesPattern -match '^(\d+)-(\d+)$') {
|
||||
$StartFrame = [int]$Matches[1]
|
||||
$EndFrame = [int]$Matches[2]
|
||||
} elseif ($framesPattern -match '^-(\d+)$') {
|
||||
$StartFrame = [Math]::Max(0, $maxFrameIndex - [int]$Matches[1])
|
||||
$StartFrame = [Math]::Max(0, $maxFrameIndex - [int]$Matches[1] + 1)
|
||||
$EndFrame = $maxFrameIndex
|
||||
} elseif ($framesPattern -match '^\d+$') {
|
||||
$StartFrame = 0
|
||||
@@ -758,6 +815,12 @@ if ($LTX) {
|
||||
$scaleHeight = $best.H
|
||||
}
|
||||
|
||||
if ($scaleWidth -gt 0) {
|
||||
$sizeFilter = Get-SizeFilter $sourceWidth $sourceHeight $scaleWidth $scaleHeight
|
||||
$scaleWidth = $sizeFilter.Width
|
||||
$scaleHeight = $sizeFilter.Height
|
||||
}
|
||||
|
||||
$ffprobeBitrateOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=bit_rate -of default=noprint_wrappers=1:nokey=1 -- $SourcePath 2>&1
|
||||
$ffprobeExit = $LASTEXITCODE
|
||||
|
||||
@@ -835,7 +898,7 @@ if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
|
||||
'-y'
|
||||
'-i', $SourcePath
|
||||
'-vf', $videoFilter
|
||||
'-vsync', 'vfr'
|
||||
'-fps_mode', 'vfr'
|
||||
'-frames:v', '1'
|
||||
'-q:v', '1'
|
||||
'--', $SequencePath
|
||||
@@ -882,9 +945,9 @@ if ($AudioPath -and $externalAudioCodec) {
|
||||
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $StartFrame, $actualEndFrame, $frameCount)
|
||||
if ($scaleWidth -gt 0) {
|
||||
if ($LTX) {
|
||||
Write-Host (' Size: {0}x{1} (LTX-2)' -f $scaleWidth, $scaleHeight)
|
||||
Write-Host (' Size: {0}x{1} (LTX-2){2}' -f $scaleWidth, $scaleHeight, $cropNote)
|
||||
} else {
|
||||
Write-Host (' Size: {0}x{1}' -f $scaleWidth, $scaleHeight)
|
||||
Write-Host (' Size: {0}x{1}{2}' -f $scaleWidth, $scaleHeight, $cropNote)
|
||||
}
|
||||
}
|
||||
if ($FPS -gt 0) {
|
||||
@@ -910,7 +973,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
||||
$filterParts = @('select=between(n\,{0}\,{1})' -f $StartFrame, $actualEndFrame)
|
||||
$filterParts += 'setpts=PTS-STARTPTS'
|
||||
if ($scaleWidth -gt 0) {
|
||||
$filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight)
|
||||
$filterParts += $sizeFilter.Filter
|
||||
}
|
||||
if ($FPS -gt 0) {
|
||||
$filterParts += ('fps={0}' -f $FPS)
|
||||
@@ -921,7 +984,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
||||
$videoFilter = $filterParts -join ','
|
||||
$ffmpegArgs += @(
|
||||
'-vf', $videoFilter
|
||||
'-vsync', 'vfr'
|
||||
'-fps_mode', 'vfr'
|
||||
)
|
||||
$ffmpegArgs += $targetVideoEncodeArgs
|
||||
if ($CRF -ge 0) {
|
||||
@@ -937,7 +1000,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
||||
} elseif ($needsReencode) {
|
||||
$filterParts = @()
|
||||
if ($scaleWidth -gt 0) {
|
||||
$filterParts += ('scale={0}:{1}' -f $scaleWidth, $scaleHeight)
|
||||
$filterParts += $sizeFilter.Filter
|
||||
}
|
||||
if ($FPS -gt 0) {
|
||||
$filterParts += ('fps={0}' -f $FPS)
|
||||
|
||||
+71
-42
@@ -8,6 +8,12 @@
|
||||
Each source video is scaled to fit its cell without stretching, with black letterbox/pillarbox
|
||||
padding applied as needed to preserve aspect ratios.
|
||||
|
||||
Exact fit (2 videos only): when S1 and S2 are both exactly as tall as the output
|
||||
(1080, or 2160 with -In4K) and their widths add up to the output width (1920, or 3840
|
||||
with -In4K), the two videos already tile the target frame perfectly. In that case they
|
||||
are stacked side by side untouched - no scaling, no padding, no quality loss.
|
||||
Example: 1280x1080 + 640x1080 -> 1920x1080. -Clip and -Sideways have no effect here.
|
||||
|
||||
The script detects the orientation of S1 (landscape or portrait) and applies appropriate scaling:
|
||||
|
||||
Landscape mode (width > height) - 1K HD:
|
||||
@@ -78,6 +84,9 @@
|
||||
Generate comparison video in 4K UHD (3840x2160) resolution instead of 1K HD (1920x1080).
|
||||
|
||||
.PARAMETER Clip
|
||||
Ignored when the two sources are an exact fit for the output frame (see DESCRIPTION),
|
||||
since such an output has no padding to remove.
|
||||
|
||||
Reduces the output height to match the actual scaled height of the sources, removing
|
||||
the top/bottom black letterbox padding. For landscape, only applies when exactly 2
|
||||
source videos are provided; ignored otherwise. For portrait, applies when 2-4 source
|
||||
@@ -135,6 +144,12 @@
|
||||
|
||||
Compare two 720p (1280x720) videos with no top/bottom letterboxing: output is 1920x540.
|
||||
|
||||
.EXAMPLE
|
||||
.\Compare-Videos.ps1 -S1 wide_1280x1080.mp4 -S2 narrow_640x1080.mp4 -Output exact.mp4
|
||||
|
||||
Exact fit: both sources are 1080 tall and 1280 + 640 = 1920, so they are stacked
|
||||
side by side with no scaling or padding at all.
|
||||
|
||||
.EXAMPLE
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -Output sideways.mp4 -Sideways
|
||||
|
||||
@@ -202,6 +217,20 @@ function Test-VideoExists {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-VideoDimensions {
|
||||
param([string]$Path, [string]$Name)
|
||||
|
||||
$probeOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $Path 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for ${Name}: $probeOutput"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$dimensions = $probeOutput -split ','
|
||||
return [pscustomobject]@{ Width = [int]$dimensions[0]; Height = [int]$dimensions[1] }
|
||||
}
|
||||
|
||||
Test-VideoExists -Path $S1 -Name "S1"
|
||||
Test-VideoExists -Path $S2 -Name "S2"
|
||||
|
||||
@@ -226,16 +255,9 @@ elseif ($S3) { $videoCount = 3 }
|
||||
# --- Detect orientation of S1 ---
|
||||
|
||||
Write-Host "Detecting orientation of S1..." -ForegroundColor Cyan
|
||||
$probeOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S1 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for S1: $probeOutput"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$dimensions = $probeOutput -split ','
|
||||
$width = [int]$dimensions[0]
|
||||
$height = [int]$dimensions[1]
|
||||
$dim1 = Get-VideoDimensions -Path $S1 -Name "S1"
|
||||
$width = $dim1.Width
|
||||
$height = $dim1.Height
|
||||
|
||||
$isPortrait = $height -gt $width
|
||||
|
||||
@@ -256,6 +278,34 @@ if ($isPortrait) {
|
||||
Write-Host "Landscape mode detected (${width}x${height}) - Output: $resolutionLabel" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# S2 dimensions are needed for the exact-fit check below and for -Clip.
|
||||
$w2 = 0
|
||||
$h2 = 0
|
||||
if ($videoCount -eq 2 -or $Clip) {
|
||||
$dim2 = Get-VideoDimensions -Path $S2 -Name "S2"
|
||||
$w2 = $dim2.Width
|
||||
$h2 = $dim2.Height
|
||||
}
|
||||
|
||||
# --- Exact fit: two sources that already tile the output perfectly ---
|
||||
# When both sources are exactly as tall as the output and their widths add up to the
|
||||
# output width (e.g. 1280x1080 + 640x1080 at 1K HD), stacking them side by side already
|
||||
# produces the target frame. Scaling or padding would only degrade them, so the sources
|
||||
# are passed straight to hstack untouched. -Clip and -Sideways have nothing to do in this
|
||||
# case (there is no padding to remove and the layout is already a single row).
|
||||
$exactFit = ($videoCount -eq 2 -and $height -eq $outputHeight -and $h2 -eq $outputHeight -and ($width + $w2) -eq $outputWidth)
|
||||
|
||||
if ($exactFit) {
|
||||
Write-Host ("Exact fit: {0}x{1} + {2}x{3} already tile {4}x{5}; sources used unscaled." -f $width, $height, $w2, $h2, $outputWidth, $outputHeight) -ForegroundColor Green
|
||||
if ($Clip) {
|
||||
Write-Host "Exact fit: -Clip has no effect (output contains no padding)." -ForegroundColor Yellow
|
||||
$Clip = $false
|
||||
}
|
||||
if ($Sideways) {
|
||||
Write-Host "Exact fit: -Sideways has no effect (sources are already a single row)." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if ($Clip -and -not $Sideways) {
|
||||
$clipAllowed = ($isPortrait -and $videoCount -le 4) -or (-not $isPortrait -and $videoCount -eq 2)
|
||||
if (-not $clipAllowed) {
|
||||
@@ -268,16 +318,6 @@ if ($Clip -and -not $Sideways) {
|
||||
$clipMixedLandscape = $false
|
||||
$clipMixedFilterComplex = $null
|
||||
if ($Clip -and -not $Sideways) {
|
||||
Write-Host "Probing S2 dimensions for -Clip..." -ForegroundColor Cyan
|
||||
$probeOutput2 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S2 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for S2: $probeOutput2"
|
||||
exit 1
|
||||
}
|
||||
$dim2 = $probeOutput2 -split ','
|
||||
$w2 = [int]$dim2[0]
|
||||
$h2 = [int]$dim2[1]
|
||||
|
||||
$s1IsLandscape = $width -gt $height
|
||||
$s2IsLandscape = $w2 -gt $h2
|
||||
$isMixedOrientation = ($s1IsLandscape -xor $s2IsLandscape)
|
||||
@@ -364,7 +404,11 @@ if ($Clip -and -not $Sideways) {
|
||||
$filterComplex = ""
|
||||
$inputArgs = @("-i", $S1, "-i", $S2)
|
||||
|
||||
if ($Sideways) {
|
||||
if ($exactFit) {
|
||||
# Both sources already have the exact output height and their widths sum to the output
|
||||
# width: hstack them as-is, with no scale and no pad filters in the graph.
|
||||
$filterComplex = "[0:v][1:v]hstack=inputs=2[outv]"
|
||||
} elseif ($Sideways) {
|
||||
Write-Host "Sideways mode: arranging $videoCount videos in a single row." -ForegroundColor Cyan
|
||||
|
||||
if ($isPortrait) {
|
||||
@@ -457,13 +501,8 @@ if ($Sideways) {
|
||||
$inputArgs += @("-i", $S3)
|
||||
$applyPortraitClip = $false
|
||||
if ($Clip) {
|
||||
$probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for S3: $probeOutput3"
|
||||
exit 1
|
||||
}
|
||||
$dim3 = $probeOutput3 -split ','
|
||||
$w3 = [int]$dim3[0]; $h3 = [int]$dim3[1]
|
||||
$dim3 = Get-VideoDimensions -Path $S3 -Name "S3"
|
||||
$w3 = $dim3.Width; $h3 = $dim3.Height
|
||||
if (($h2 -gt $w2) -and ($h3 -gt $w3)) {
|
||||
$applyPortraitClip = $true
|
||||
} else {
|
||||
@@ -490,20 +529,10 @@ if ($Sideways) {
|
||||
$inputArgs += @("-i", $S3, "-i", $S4)
|
||||
$applyPortraitClip4 = $false
|
||||
if ($Clip) {
|
||||
$probeOutput3 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S3 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for S3: $probeOutput3"
|
||||
exit 1
|
||||
}
|
||||
$dim3 = $probeOutput3 -split ','
|
||||
$w3 = [int]$dim3[0]; $h3 = [int]$dim3[1]
|
||||
$probeOutput4 = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0 $S4 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error "Failed to probe video dimensions for S4: $probeOutput4"
|
||||
exit 1
|
||||
}
|
||||
$dim4 = $probeOutput4 -split ','
|
||||
$w4 = [int]$dim4[0]; $h4 = [int]$dim4[1]
|
||||
$dim3 = Get-VideoDimensions -Path $S3 -Name "S3"
|
||||
$w3 = $dim3.Width; $h3 = $dim3.Height
|
||||
$dim4 = Get-VideoDimensions -Path $S4 -Name "S4"
|
||||
$w4 = $dim4.Width; $h4 = $dim4.Height
|
||||
if (($h2 -gt $w2) -and ($h3 -gt $w3) -and ($h4 -gt $w4)) {
|
||||
$applyPortraitClip4 = $true
|
||||
} else {
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ for /f "delims=" %%i in ('where Create-Clip.ps1 2^>nul') do set "_script=%%i" &
|
||||
echo Error: Create-Clip.ps1 not found in PATH
|
||||
exit /b 1
|
||||
:found
|
||||
set "_src=%~1"
|
||||
set "_video=%~1"
|
||||
set "_frames=%~2"
|
||||
shift
|
||||
shift
|
||||
@@ -14,4 +14,4 @@ set "_rest=%_rest% %1"
|
||||
shift
|
||||
goto loop
|
||||
:done
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%_script%" -Source "%_src%" -Frames "%_frames%"%_rest%
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%_script%" -Video "%_video%" -Frames "%_frames%"%_rest%
|
||||
+75
-24
@@ -1,26 +1,34 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a new clipped video file using Clone-Video.ps1, requiring the -Source and -Frames parameters from that script.
|
||||
The filename used for the -Target parameter is automatically derived from the -Source filename, with "-Clip" inserted before the video file extension (e.g., P01.mp4 becomes P01-Clip.mp4).
|
||||
Any extra parameters will be pass directly to Clone-Video.ps1, except for parameters -Source, -Frames and -Target.
|
||||
.PARAMETER Source
|
||||
Creates a new clipped video file using Clone-Video.ps1, requiring the -Video and -Frames parameters.
|
||||
The filename used for the -Target parameter is automatically derived from the -Video filename, with the -Suffix value (default "-Clip") inserted before the video file extension (e.g., P01.mp4 becomes P01-Clip.mp4).
|
||||
When the video name carries a '.skip' marker before its extension, the suffix is inserted before that marker so it stays last (e.g., P01.skip.mp4 becomes P01-Clip.skip.mp4).
|
||||
Any extra parameters will be pass directly to Clone-Video.ps1, except for parameters -Video, -Frames, -Suffix and -Target.
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory. If the path has no extension, .mp4, .webm and .mkv are tried in turn.
|
||||
.PARAMETER Frames
|
||||
0-based frames index to extract. Supports the following formats:
|
||||
- start-end: Extract frames from start to end (e.g., "100-200")
|
||||
- positive_number: Extract frames from 0 to that number (e.g., "50")
|
||||
- -negative_number: Extract last N frames (e.g., "-10")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%", "-%")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%")
|
||||
- all: Extract all frames (same as "0-%")
|
||||
.PARAMETER Suffix
|
||||
Text inserted into the derived -Target name before the file extension. Defaults to "-Clip" and cannot be empty.
|
||||
Example: -Suffix "-Clip-Shot-1" makes P01.mp4 become P01-Clip-Shot-1.mp4. A '.skip' marker still stays last.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
[string]$Video,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Frames,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Suffix = '-Clip',
|
||||
|
||||
[Parameter(ValueFromRemainingArguments)]
|
||||
[object[]]$ExtraArgs
|
||||
)
|
||||
@@ -28,36 +36,79 @@ param(
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$sourcePath = $Source
|
||||
$videoPath = $Video
|
||||
|
||||
# Resolve source path: accept .mp4, .webm or .mkv. If the path doesn't exist as-is,
|
||||
# Resolve video path: accept .mp4, .webm or .mkv. If the path doesn't exist as-is,
|
||||
# try appending each supported extension in turn (same logic as Clone-Video.ps1).
|
||||
$supportedSourceExts = @('.mp4', '.webm', '.mkv')
|
||||
if (-not (Test-Path -LiteralPath $sourcePath)) {
|
||||
foreach ($ext in $supportedSourceExts) {
|
||||
$candidate = "$sourcePath$ext"
|
||||
$supportedVideoExts = @('.mp4', '.webm', '.mkv')
|
||||
if (-not (Test-Path -LiteralPath $videoPath)) {
|
||||
foreach ($ext in $supportedVideoExts) {
|
||||
$candidate = "$videoPath$ext"
|
||||
if (Test-Path -LiteralPath $candidate) {
|
||||
$sourcePath = $candidate
|
||||
$videoPath = $candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Derive the -Target path: insert "-Clip" before the file extension (e.g., P01.mp4 -> P01-Clip.mp4)
|
||||
$sourceExt = [System.IO.Path]::GetExtension($sourcePath)
|
||||
$sourceBase = [System.IO.Path]::GetFileNameWithoutExtension($sourcePath)
|
||||
$sourceDir = [System.IO.Path]::GetDirectoryName($sourcePath)
|
||||
$targetName = "$sourceBase-Clip$sourceExt"
|
||||
# Derive the -Target path: insert -Suffix before the file extension (e.g., P01.mp4 -> P01-Clip.mp4)
|
||||
$sourceExt = [System.IO.Path]::GetExtension($videoPath)
|
||||
$sourceBase = [System.IO.Path]::GetFileNameWithoutExtension($videoPath)
|
||||
$sourceDir = [System.IO.Path]::GetDirectoryName($videoPath)
|
||||
|
||||
# A '.skip' marker must stay immediately before the container extension, so the suffix is
|
||||
# inserted before it rather than after (e.g., P01.skip.mp4 -> P01-Clip.skip.mp4).
|
||||
# The marker's original casing is preserved.
|
||||
$skipMarker = ''
|
||||
if ($sourceBase -match '^(.*)(\.skip)$') {
|
||||
$sourceBase = $Matches[1]
|
||||
$skipMarker = $Matches[2]
|
||||
}
|
||||
|
||||
$targetName = "$sourceBase$Suffix$skipMarker$sourceExt"
|
||||
$targetPath = if ([string]::IsNullOrEmpty($sourceDir)) { $targetName } else { Join-Path $sourceDir $targetName }
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$cloneVideoScript = Join-Path $scriptDir 'Clone-Video.ps1'
|
||||
|
||||
# Forward extra args plus common parameters to Clone-Video.ps1
|
||||
$passThrough = @()
|
||||
if ($ExtraArgs) { $passThrough += $ExtraArgs }
|
||||
if ($PSBoundParameters.ContainsKey('WhatIf')) { $passThrough += '-WhatIf' }
|
||||
if ($PSBoundParameters.ContainsKey('Confirm')) { $passThrough += '-Confirm' }
|
||||
# Forward extra args plus common parameters to Clone-Video.ps1.
|
||||
# The remaining arguments are turned into a hashtable and splatted by name: PowerShell
|
||||
# passes the elements of a splatted *array* as positional arguments, so forwarding
|
||||
# @('-Size', '720p') that way would bind '-Size' to Clone-Video.ps1's [int]$StartFrame
|
||||
# instead of naming its -Size parameter.
|
||||
# Assigned in two steps: an empty array returned from an if-statement block is enumerated
|
||||
# away by PowerShell, which would leave $extra as $null.
|
||||
$extra = @()
|
||||
if ($ExtraArgs) { $extra = @($ExtraArgs) }
|
||||
|
||||
& $cloneVideoScript -Source $sourcePath -Frames $Frames -Target $targetPath @passThrough
|
||||
$forwarded = @{}
|
||||
$index = 0
|
||||
while ($index -lt $extra.Count) {
|
||||
$token = [string]$extra[$index]
|
||||
|
||||
if ($token -notmatch '^-{1,2}([A-Za-z]\w*)$') {
|
||||
Write-Error ("Unexpected argument '{0}'. Extra parameters must be passed by name (e.g. -Size 720p)." -f $token)
|
||||
exit 1
|
||||
}
|
||||
$name = $Matches[1]
|
||||
|
||||
$next = if (($index + 1) -lt $extra.Count) { [string]$extra[$index + 1] } else { $null }
|
||||
|
||||
# The next token is this parameter's value unless it is itself a parameter name.
|
||||
# A '-' followed by a digit is a value, not a name (e.g. -CRF -1).
|
||||
if ($null -ne $next -and (($next -notmatch '^-{1,2}[A-Za-z]') -or ($next -match '^-\d'))) {
|
||||
$forwarded[$name] = $extra[$index + 1]
|
||||
$index += 2
|
||||
}
|
||||
else {
|
||||
# No value follows, so this is a switch (e.g. -NoAudio, -Reverse, -LTX).
|
||||
$forwarded[$name] = $true
|
||||
$index++
|
||||
}
|
||||
}
|
||||
|
||||
if ($PSBoundParameters.ContainsKey('WhatIf')) { $forwarded['WhatIf'] = [bool]$PSBoundParameters['WhatIf'] }
|
||||
if ($PSBoundParameters.ContainsKey('Confirm')) { $forwarded['Confirm'] = [bool]$PSBoundParameters['Confirm'] }
|
||||
|
||||
& $cloneVideoScript -Source $videoPath -Frames $Frames -Target $targetPath @forwarded
|
||||
exit $LASTEXITCODE
|
||||
@@ -7,6 +7,7 @@
|
||||
.DESCRIPTION
|
||||
This script creates a full subtitle file from a video file from three sources: a reference video and two SRT subtitle files.
|
||||
The output file contains all subtitles from the first and second subtitle files with proper timing and numbering.
|
||||
An existing file at -Output is overwritten without a prompt. Its folder is created if it does not exist.
|
||||
.PARAMETER Video
|
||||
Path to the video file.
|
||||
.PARAMETER Subtitle1
|
||||
@@ -193,7 +194,7 @@ function Export-SrtFile {
|
||||
#region Main Script
|
||||
|
||||
Write-Host "Parsing first subtitle file: $Subtitle1"
|
||||
$subtitle1Entries = Get-SrtContent -SrtPath $Subtitle1
|
||||
$subtitle1Entries = @(Get-SrtContent -SrtPath $Subtitle1)
|
||||
Write-Host " Found $($subtitle1Entries.Count) entries"
|
||||
|
||||
if ($subtitle1Entries.Count -eq 0) {
|
||||
@@ -209,7 +210,7 @@ $videoDurationFormatted = ConvertFrom-TimeSpan -TimeSpan ([TimeSpan]::FromSecond
|
||||
Write-Host " Video duration: $videoDurationFormatted (${videoDuration}s)"
|
||||
|
||||
Write-Host "Parsing second subtitle file: $Subtitle2"
|
||||
$subtitle2Entries = Get-SrtContent -SrtPath $Subtitle2
|
||||
$subtitle2Entries = @(Get-SrtContent -SrtPath $Subtitle2)
|
||||
Write-Host " Found $($subtitle2Entries.Count) entries"
|
||||
|
||||
if ($subtitle2Entries.Count -eq 0) {
|
||||
|
||||
+13
-13
@@ -1,7 +1,7 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a pillar-boxed video file from a video file.
|
||||
.PARAMETER Source
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
|
||||
.PARAMETER Target
|
||||
Path to the output video file. Can be absolute or relative to current directory. .mp4 extension is added if not provided.
|
||||
@@ -45,7 +45,7 @@
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
[string]$Video,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Target = $null,
|
||||
@@ -247,25 +247,25 @@ elseif ($effectLower -notin $validEffects) {
|
||||
throw "Invalid -Effect value: $Effect. Must be one of: $($validEffects -join ', '), or 'color:RRGGBB' (6 hex digits)."
|
||||
}
|
||||
|
||||
# Append .mp4 extension if missing on -Source
|
||||
if (-not [System.IO.Path]::GetExtension($Source)) {
|
||||
$Source = "$Source.mp4"
|
||||
# Append .mp4 extension if missing on -Video
|
||||
if (-not [System.IO.Path]::GetExtension($Video)) {
|
||||
$Video = "$Video.mp4"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Source)) {
|
||||
throw "Source video not found: $Source"
|
||||
if (-not (Test-Path -LiteralPath $Video)) {
|
||||
throw "Video file not found: $Video"
|
||||
}
|
||||
|
||||
$sourceFull = (Resolve-Path -LiteralPath $Source).Path
|
||||
$videoFull = (Resolve-Path -LiteralPath $Video).Path
|
||||
|
||||
# Default -Target: same directory and base name as -Source, with the effect appended
|
||||
# Default -Target: same directory and base name as -Video, with the effect appended
|
||||
# (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)
|
||||
$srcDir = [System.IO.Path]::GetDirectoryName($videoFull)
|
||||
$srcBase = [System.IO.Path]::GetFileNameWithoutExtension($videoFull)
|
||||
$effectForName = $effectLower -replace ':', '-'
|
||||
$verticalForName = if ($Vertical) { '-vertical' } else { '' }
|
||||
$Target = Join-Path $srcDir "$srcBase-$effectForName$verticalForName.mp4"
|
||||
@@ -296,7 +296,7 @@ if ($CRF -ne -1 -and ($CRF -lt 0 -or $CRF -gt 51)) {
|
||||
throw "Invalid -CRF value: $CRF. Must be in range 0-51."
|
||||
}
|
||||
|
||||
$inInfo = Get-VideoInfo -Path $sourceFull
|
||||
$inInfo = Get-VideoInfo -Path $videoFull
|
||||
|
||||
$inRes = "{0}x{1}" -f $inInfo.Width, $inInfo.Height
|
||||
$inFpsText = if ($inInfo.Fps) { "{0:0.###}" -f $inInfo.Fps } else { 'unknown' }
|
||||
@@ -370,7 +370,7 @@ $effFps = if ($FPS -gt 0) { $FPS } elseif ($inInfo.Fps) { $inInfo.Fps } else { $
|
||||
$effFpsText = if ($effFps) { "{0:0.###}" -f $effFps } else { 'unknown' }
|
||||
|
||||
# Build ffmpeg arguments
|
||||
$ffArgs = @('-y', '-i', $sourceFull)
|
||||
$ffArgs = @('-y', '-i', $videoFull)
|
||||
|
||||
if ($audioFull) {
|
||||
$ffArgs += @('-i', $audioFull)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Create-Scenes.ps1" %*
|
||||
@@ -0,0 +1,317 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Detects scenes in a video file with ffmpeg and extracts each scene as a separate video clip file.
|
||||
.DESCRIPTION
|
||||
Analyzes the input video with ffmpeg scene-change detection, using the -Level threshold, and computes
|
||||
the first and last frame (0-based) of each detected scene. Each scene is then extracted by calling
|
||||
Create-Clip.ps1 (located in the same directory as this script) with the scene frame range, which in
|
||||
turn calls Clone-Video.ps1.
|
||||
|
||||
Clip files are always saved in the same directory as the input video. The clip filename is the input
|
||||
video filename plus the scene suffix, with the scene number starting at 1
|
||||
(ex: P01.mp4 scene 1 becomes P01-Scene-01.mp4). For sources carrying a .skip marker, the marker stays
|
||||
last, as handled by Create-Clip.ps1 (ex: P01.skip.mp4 becomes P01-Scene-01.skip.mp4).
|
||||
|
||||
Encoding is performed by Clone-Video.ps1: by default each clip keeps the resolution, frame rate and
|
||||
compression characteristics of the input video, unless overridden with extra parameters (ex: -Size,
|
||||
-FPS, -CRF) forwarded to Create-Clip.ps1.
|
||||
|
||||
The list of scenes (index, start frame, end frame, frame count, start time, end time) is always displayed.
|
||||
If no scene change is detected, the whole video is treated as a single scene.
|
||||
With -Scenes, the level is adjusted automatically until the requested number of scenes is detected; every
|
||||
attempt is displayed. If that number cannot be reached, the script stops with exit code 1 and creates or
|
||||
deletes nothing.
|
||||
|
||||
Unless -List is specified, existing scene clip files from a previous run (same directory, same source
|
||||
name, same suffix pattern and same extension) are deleted before the new clips are created. Existing
|
||||
files are always replaced.
|
||||
|
||||
Scene detection and the scene list are read-only: they always run, including with -List and -WhatIf.
|
||||
Only the deletion and the creation of files are subject to -WhatIf and -Confirm. Both switches are
|
||||
also forwarded to Create-Clip.ps1.
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory. If the path has no
|
||||
extension, .mp4, .webm and .mkv are tried in turn.
|
||||
.PARAMETER Level
|
||||
Threshold level value between 0 and 1.0 to determine scene changes. Lower values mean more sensitive
|
||||
detection (more scenes). Default is 0.3. The value is rounded to 2 decimals.
|
||||
.PARAMETER Scenes
|
||||
Target number of scenes (a positive integer). The level is searched automatically, starting from -Level,
|
||||
until exactly this many scenes are detected. Stops with exit code 1 if the number cannot be reached.
|
||||
.PARAMETER Suffix
|
||||
Scene suffix to use for the output video clip files. Must contain a printf-style number placeholder
|
||||
such as %02d, which is replaced by the scene number starting at 1. By default, "-Scene-%02d" is used.
|
||||
.PARAMETER List
|
||||
Only displays the list of scenes. No video clip file is created and no existing file is deleted.
|
||||
.PARAMETER ExtraArgs
|
||||
Additional parameters passed as-is to Create-Clip.ps1 and from there to Clone-Video.ps1
|
||||
(ex: -Size 720p -NoAudio -CRF 18 -LTX). The parameters -Video, -Frames, -Suffix, -Target, -Level and
|
||||
-Scenes are managed by this script and must not be repeated.
|
||||
.EXAMPLE
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Level 0.4 -List
|
||||
Lists the scenes detected in P01.mp4 without creating or deleting any file.
|
||||
.EXAMPLE
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Scenes 4 -List
|
||||
Searches the level automatically and lists the scenes once exactly 4 scenes are detected.
|
||||
.EXAMPLE
|
||||
.\Create-Scenes.ps1 -Video P01 -Level 0.3 -Size 720p -NoAudio
|
||||
Creates P01-Scene-01.mp4, P01-Scene-02.mp4, ... scaled to 720p and without audio.
|
||||
.EXAMPLE
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Suffix "-Shot-%03d" -WhatIf
|
||||
Shows which files would be deleted and created, without changing anything.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string]$Video,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateRange(0.0, 1.0)]
|
||||
[double]$Level = 0.3,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateRange(1, [int]::MaxValue)]
|
||||
[int]$Scenes,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[ValidatePattern('%0?\d*d')]
|
||||
[string]$Suffix = '-Scene-%02d',
|
||||
|
||||
[switch]$List,
|
||||
|
||||
[Parameter(ValueFromRemainingArguments)]
|
||||
[object[]]$ExtraArgs
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$invariant = [Globalization.CultureInfo]::InvariantCulture
|
||||
|
||||
function Resolve-VideoPath {
|
||||
param([string]$Path)
|
||||
$candidates = @($Path) + @('.mp4', '.webm', '.mkv' | ForEach-Object { "$Path$_" })
|
||||
$found = $candidates | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1
|
||||
if (-not $found) { throw "Video file not found: $Path" }
|
||||
(Resolve-Path -LiteralPath $found).Path
|
||||
}
|
||||
|
||||
function ConvertTo-Number {
|
||||
param([string]$Text)
|
||||
# Parses plain numbers and ffprobe rationals (e.g. "30000/1001"). Returns $null for N/A or invalid values.
|
||||
$parts = $Text -split '/'
|
||||
$num = 0.0
|
||||
$den = 1.0
|
||||
if ($parts.Count -gt 2 -or -not [double]::TryParse($parts[0], [Globalization.NumberStyles]::Float, $invariant, [ref]$num)) { return $null }
|
||||
if ($parts.Count -eq 2 -and (-not [double]::TryParse($parts[1], [Globalization.NumberStyles]::Float, $invariant, [ref]$den) -or $den -eq 0)) { return $null }
|
||||
$num / $den
|
||||
}
|
||||
|
||||
function Get-VideoInfo {
|
||||
param([string]$Path)
|
||||
$ffArgs = @('-hide_banner', '-v', 'error', '-count_frames', '-select_streams', 'v:0',
|
||||
'-show_entries', 'stream=avg_frame_rate,r_frame_rate,start_time,nb_read_frames,nb_frames,duration',
|
||||
'-of', 'default=noprint_wrappers=1', $Path)
|
||||
Write-Verbose ('ffprobe ' + ($ffArgs -join ' '))
|
||||
$lines = & ffprobe @ffArgs
|
||||
if ($LASTEXITCODE -ne 0) { throw "ffprobe failed to read $Path" }
|
||||
|
||||
$fields = @{}
|
||||
foreach ($line in $lines) {
|
||||
if ("$line" -match '^(\w+)=(.*)$') { $fields[$Matches[1]] = $Matches[2] }
|
||||
}
|
||||
|
||||
$fps = ConvertTo-Number $fields['avg_frame_rate']
|
||||
if (-not $fps) { $fps = ConvertTo-Number $fields['r_frame_rate'] }
|
||||
if (-not $fps) { throw "Cannot read the frame rate of $Path" }
|
||||
|
||||
$startTime = ConvertTo-Number $fields['start_time']
|
||||
if ($null -eq $startTime) { $startTime = 0.0 }
|
||||
|
||||
$frames = ConvertTo-Number $fields['nb_read_frames']
|
||||
if ($null -eq $frames) { $frames = ConvertTo-Number $fields['nb_frames'] }
|
||||
if ($null -eq $frames) { $frames = [Math]::Round((ConvertTo-Number $fields['duration']) * $fps) }
|
||||
if ($frames -lt 1) { throw "Cannot determine the frame count of $Path" }
|
||||
|
||||
[pscustomobject]@{ Fps = $fps; StartTime = $startTime; TotalFrames = [int][Math]::Round($frames) }
|
||||
}
|
||||
|
||||
function Get-SceneChangeTime {
|
||||
param([string]$Path, [double]$Threshold)
|
||||
$filter = "select='gt(scene,{0})',showinfo" -f $Threshold.ToString($invariant)
|
||||
$ffArgs = @('-hide_banner', '-nostats', '-loglevel', 'info', '-i', $Path, '-vf', $filter, '-an', '-f', 'null', '-')
|
||||
Write-Verbose ('ffmpeg ' + ($ffArgs -join ' '))
|
||||
$output = & ffmpeg @ffArgs 2>&1 | Out-String
|
||||
if ($LASTEXITCODE -ne 0) { throw "ffmpeg scene detection failed on $Path" }
|
||||
[regex]::Matches($output, 'pts_time:([\d.]+)') | ForEach-Object { [double]::Parse($_.Groups[1].Value, $invariant) }
|
||||
}
|
||||
|
||||
function Format-SceneSuffix {
|
||||
param([string]$Template, [int]$Number)
|
||||
# Replaces only the first printf-style placeholder (%d, %2d, %02d, %003d) with the zero-padded number.
|
||||
$placeholder = [regex]::Match($Template, '%0*(\d*)d')
|
||||
$width = [Math]::Max(1, [int]('0' + $placeholder.Groups[1].Value))
|
||||
$Template.Substring(0, $placeholder.Index) + $Number.ToString('D' + $width, $invariant) +
|
||||
$Template.Substring($placeholder.Index + $placeholder.Length)
|
||||
}
|
||||
|
||||
function Get-SceneList {
|
||||
param($Info, [double[]]$ChangeTimes, [string]$Template)
|
||||
# pts_time * fps is exact only for constant frame rate videos; with variable frame rate,
|
||||
# scene boundaries can be off by one frame.
|
||||
$boundaries = @($ChangeTimes | ForEach-Object { [int][Math]::Round(($_ - $Info.StartTime) * $Info.Fps) } |
|
||||
Where-Object { $_ -gt 0 -and $_ -lt $Info.TotalFrames } | Sort-Object -Unique)
|
||||
$starts = @(0) + $boundaries
|
||||
for ($i = 0; $i -lt $starts.Count; $i++) {
|
||||
$start = [int]$starts[$i]
|
||||
$end = if ($i + 1 -lt $starts.Count) { $starts[$i + 1] - 1 } else { $Info.TotalFrames - 1 }
|
||||
[pscustomobject]@{
|
||||
Index = $i + 1
|
||||
StartFrame = $start
|
||||
EndFrame = $end
|
||||
Frames = $end - $start + 1
|
||||
StartTime = [Math]::Round($start / $Info.Fps, 3)
|
||||
EndTime = [Math]::Round($end / $Info.Fps, 3)
|
||||
TotalTime = [Math]::Round(($end - $start + 1) / $Info.Fps, 3)
|
||||
Suffix = Format-SceneSuffix -Template $Template -Number ($i + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Get-SceneFilePattern {
|
||||
param([string]$VideoPath, [string]$Template)
|
||||
# Regex matching the clip files this script creates for this source; used to find files of previous runs.
|
||||
$ext = [IO.Path]::GetExtension($VideoPath)
|
||||
$stem = [IO.Path]::GetFileNameWithoutExtension($VideoPath)
|
||||
$skipMarker = ''
|
||||
if ($stem -match '^(.*)(\.skip)$') {
|
||||
$stem = $Matches[1]
|
||||
$skipMarker = '\.skip'
|
||||
}
|
||||
$placeholder = [regex]::Match($Template, '%0*\d*d')
|
||||
$suffixPattern = [regex]::Escape($Template.Substring(0, $placeholder.Index)) + '\d+' +
|
||||
[regex]::Escape($Template.Substring($placeholder.Index + $placeholder.Length))
|
||||
'^' + [regex]::Escape($stem) + $suffixPattern + $skipMarker + [regex]::Escape($ext) + '$'
|
||||
}
|
||||
|
||||
function Remove-ExistingScene {
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param([string]$VideoPath, [string]$Pattern)
|
||||
$directory = [IO.Path]::GetDirectoryName($VideoPath)
|
||||
$existing = Get-ChildItem -LiteralPath $directory -File |
|
||||
Where-Object { $_.Name -match $Pattern -and $_.FullName -ne $VideoPath }
|
||||
foreach ($file in $existing) {
|
||||
if ($PSCmdlet.ShouldProcess($file.FullName, 'Delete existing scene file')) {
|
||||
Remove-Item -LiteralPath $file.FullName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertTo-ClipArgument {
|
||||
param([object[]]$Tokens)
|
||||
# Validates the extra arguments: every value must follow a parameter name, and parameter names without a value are switches.
|
||||
$params = @{}
|
||||
$pending = $null
|
||||
foreach ($token in $Tokens) {
|
||||
if ($token -is [string] -and $token -match '^-([A-Za-z]\w*)(:(.*))?$') {
|
||||
if ($pending) { $params[$pending] = $true }
|
||||
$pending = $Matches[1]
|
||||
if ($Matches[2]) {
|
||||
$params[$pending] = $Matches[3]
|
||||
$pending = $null
|
||||
}
|
||||
}
|
||||
elseif ($pending) {
|
||||
$params[$pending] = $token
|
||||
$pending = $null
|
||||
}
|
||||
else {
|
||||
throw "Unexpected argument '$token'. Extra parameters must be passed by name (e.g. -Size 720p)."
|
||||
}
|
||||
}
|
||||
if ($pending) { $params[$pending] = $true }
|
||||
$params
|
||||
}
|
||||
|
||||
foreach ($tool in 'ffmpeg', 'ffprobe') {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "$tool was not found in PATH." }
|
||||
}
|
||||
$createClipPath = Join-Path $PSScriptRoot 'Create-Clip.ps1'
|
||||
if (-not (Test-Path -LiteralPath $createClipPath -PathType Leaf)) {
|
||||
throw "Create-Clip.ps1 was not found next to this script: $createClipPath"
|
||||
}
|
||||
|
||||
$videoPath = Resolve-VideoPath -Path $Video
|
||||
[object[]]$tokens = @()
|
||||
if ($null -ne $ExtraArgs) { $tokens = @($ExtraArgs) }
|
||||
$extraParams = ConvertTo-ClipArgument -Tokens $tokens
|
||||
foreach ($reserved in 'Video', 'Frames', 'Suffix', 'Target', 'Level', 'Scenes') {
|
||||
if ($extraParams.ContainsKey($reserved)) {
|
||||
throw "-$reserved is managed by Create-Scenes.ps1 and must not be passed as an extra parameter."
|
||||
}
|
||||
}
|
||||
|
||||
$info = Get-VideoInfo -Path $videoPath
|
||||
# Level rounded to 2 decimals. The search rounds each new level to 2 decimals too; the step is not rounded.
|
||||
$sceneLevel = [Math]::Round($Level, 2, [MidpointRounding]::AwayFromZero)
|
||||
if ($PSBoundParameters.ContainsKey('Scenes')) {
|
||||
$sceneStep = 0.1
|
||||
$sceneDirection = 0
|
||||
$pass = 0
|
||||
while ($true) {
|
||||
$pass++
|
||||
$changeTimes = @(Get-SceneChangeTime -Path $videoPath -Threshold $sceneLevel)
|
||||
$sceneList = @(Get-SceneList -Info $info -ChangeTimes $changeTimes -Template $Suffix)
|
||||
'Pass {0}: level {1} -> {2} scene(s)' -f $pass, $sceneLevel, $sceneList.Count | Out-Host
|
||||
$sceneList | Format-Table -AutoSize | Out-Host
|
||||
if ($sceneList.Count -eq $Scenes) { break }
|
||||
if ($sceneList.Count -lt $Scenes) {
|
||||
# Fewer scenes than requested: a lower level detects more scenes.
|
||||
if ($sceneDirection -eq 0) { $sceneDirection = -1 }
|
||||
elseif ($sceneDirection -eq 1) { $sceneDirection = -1; $sceneStep = $sceneStep / 2.0 }
|
||||
}
|
||||
else {
|
||||
# More scenes than requested: a higher level detects fewer scenes.
|
||||
if ($sceneDirection -eq 0) { $sceneDirection = 1 }
|
||||
elseif ($sceneDirection -eq -1) { $sceneDirection = 1; $sceneStep = $sceneStep / 2.0 }
|
||||
}
|
||||
if ($sceneStep -lt 0.01) {
|
||||
throw "The number of scenes -Scenes $Scenes could not be found - Search stopped at scene level $sceneLevel."
|
||||
}
|
||||
$sceneLevel = [Math]::Round($sceneLevel + ($sceneDirection * $sceneStep), 2, [MidpointRounding]::AwayFromZero)
|
||||
if ($sceneLevel -lt 0 -or $sceneLevel -gt 1) {
|
||||
throw "The number of scenes -Scenes $Scenes could not be found - Search stopped at scene level $sceneLevel, which is outside the range 0 to 1.0."
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
$changeTimes = @(Get-SceneChangeTime -Path $videoPath -Threshold $sceneLevel)
|
||||
$sceneList = @(Get-SceneList -Info $info -ChangeTimes $changeTimes -Template $Suffix)
|
||||
$sceneList | Format-Table -AutoSize | Out-Host
|
||||
}
|
||||
'Scene level used: {0}' -f $sceneLevel | Out-Host
|
||||
if ($List) { return }
|
||||
|
||||
$confirm = [bool]$PSBoundParameters['Confirm']
|
||||
$pattern = Get-SceneFilePattern -VideoPath $videoPath -Template $Suffix
|
||||
Remove-ExistingScene -VideoPath $videoPath -Pattern $pattern -WhatIf:$WhatIfPreference -Confirm:$confirm
|
||||
|
||||
$failures = 0
|
||||
foreach ($clipScene in $sceneList) {
|
||||
$frames = '{0}-{1}' -f $clipScene.StartFrame, $clipScene.EndFrame
|
||||
Write-Verbose ('Scene {0}: frames {1}, suffix {2}' -f $clipScene.Index, $frames, $clipScene.Suffix)
|
||||
# Extra parameters are forwarded as the original token array, not as the hashtable: Create-Clip.ps1
|
||||
# collects unknown parameters through ValueFromRemainingArguments and parses them itself, while a
|
||||
# hashtable splat would hand it '-Name:value' tokens it does not understand.
|
||||
try {
|
||||
& $createClipPath -Video $videoPath -Frames $frames -Suffix $clipScene.Suffix -WhatIf:$WhatIfPreference -Confirm:$confirm @tokens
|
||||
if ($LASTEXITCODE -ne 0) { throw "Create-Clip.ps1 exited with code $LASTEXITCODE" }
|
||||
}
|
||||
catch {
|
||||
Write-Warning ('Scene {0} failed: {1}' -f $clipScene.Index, $_.Exception.Message)
|
||||
$failures++
|
||||
}
|
||||
}
|
||||
if ($failures -gt 0) { throw "$failures scene(s) failed to create." }
|
||||
@@ -25,6 +25,10 @@
|
||||
.DESCRIPTION
|
||||
Creates a short clip video file from a 16:9 video file using the specified plan.
|
||||
See Create-ShortClip.ini for details about the INI format and supported entries for the plan file.
|
||||
Existing files: with the default -Images (the 'Frames' folder next to the video), files directly in that folder are
|
||||
deleted before extraction (not with -Render, where they are the input). With an explicit -Images, existing frames matching that pattern are deleted before extraction,
|
||||
and the Frames folder is not touched. An existing output video is overwritten. The temporary extraction directory is
|
||||
always removed. The Frames folder is removed after the video is created unless -Keep is given.
|
||||
Supports -WhatIf and -Confirm for safe execution.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
@@ -498,7 +502,7 @@ try {
|
||||
if ($videoDir -eq '') { $videoDir = '.' }
|
||||
$framesDir = Join-Path $videoDir 'Frames'
|
||||
if (Test-Path $framesDir) {
|
||||
if ($PSCmdlet.ShouldProcess($framesDir, 'Clear existing frames directory')) {
|
||||
if (-not $Render -and $PSCmdlet.ShouldProcess($framesDir, 'Clear existing frames directory')) {
|
||||
Get-ChildItem -Path $framesDir -File | Remove-Item -Force
|
||||
}
|
||||
}
|
||||
@@ -719,11 +723,11 @@ try {
|
||||
}
|
||||
|
||||
# Clean up frames directory after video is successfully created
|
||||
if ((-not $Keep) -and (Test-Path $framesDir) -and ($PSCmdlet.ShouldProcess($framesDir, 'Remove frames directory'))) {
|
||||
if ((-not $Keep) -and $framesDir -and (Test-Path $framesDir) -and ($PSCmdlet.ShouldProcess($framesDir, 'Remove frames directory'))) {
|
||||
Remove-Item -Path $framesDir -Recurse -Force
|
||||
Write-Host "Frames directory removed: $framesDir"
|
||||
}
|
||||
elseif ($Keep -and (Test-Path $framesDir)) {
|
||||
elseif ($Keep -and $framesDir -and (Test-Path $framesDir)) {
|
||||
Write-Host "Frames directory preserved: $framesDir"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Create-Turnaround.ps1" %*
|
||||
@@ -0,0 +1,367 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Creates a turnaround image from a source of images using ffmpeg.
|
||||
Source images are arranged horizontally in the output, or in a grid with -Rows / -Columns.
|
||||
Images are never scaled: those smaller than the largest one are centred in their cell and
|
||||
padded with -Background.
|
||||
Supports PNG, JPEG, WEBP, BMP, AVIF and TIFF.
|
||||
.PARAMETER Source
|
||||
Path to the input directory containing image files with sequence numbers (e.g. Frame-00001.png). Can be absolute or relative to current directory.
|
||||
The path can have a file pattern to specify the image files (e.g. Img*.jpg), or point at a single image file.
|
||||
Image files are processed in numeric-aware (natural) name order, so Img2.jpg comes before Img10.jpg.
|
||||
.PARAMETER Target
|
||||
Path to the output turnaround image. Can be absolute or relative to current directory. .png extension is added if not provided.
|
||||
A recognised image extension (.png, .jpg, .jpeg, .webp, .bmp, .avif, .tif, .tiff) selects the output format.
|
||||
When omitted, a JPEG named after the last directory member of the source is written to the current directory:
|
||||
- -Source is a directory: the last directory member of that directory's full path (e.g. C:\renders\CharacterA -> CharacterA.jpg).
|
||||
- -Source is a file pattern or a single file: the last directory member of the current directory's full path.
|
||||
Throws when that path is a drive root (e.g. C:\), since it has no directory member to name the file after.
|
||||
.PARAMETER Rows
|
||||
Number of rows to arrange the images in. Defaults to 1 (all images in a single horizontal row).
|
||||
When only -Rows is given, the number of columns is computed as ceil(imageCount / Rows).
|
||||
.PARAMETER Columns
|
||||
Number of columns to arrange the images in. When only -Columns is given, the number of rows is
|
||||
computed as ceil(imageCount / Columns). When both -Rows and -Columns are given, Rows * Columns
|
||||
must be at least the number of images.
|
||||
.PARAMETER Background
|
||||
Colour used to fill the cells around images that are smaller than the largest image, and to fill
|
||||
the unused cells of an incomplete last row. Defaults to 'white'. Accepts any ffmpeg colour, e.g.
|
||||
a name (white, black, silver), 0xRRGGBB, or name@alpha ('white@0' for transparent).
|
||||
.PARAMETER Quality
|
||||
Quality factor value for lossy output formats (jpeg, webp and avif). Lower values mean better quality.
|
||||
- jpeg: passed to -q:v (typical range 1-31, lower is better). Defaults to 2.
|
||||
- webp: inverted internally and passed to libwebp's -quality (0-100, higher is better) so the
|
||||
"lower = better quality" semantic is consistent across formats. Defaults to 90.
|
||||
- avif: passed to libaom-av1's -crf (0-63, lower is better). Defaults to 25.
|
||||
Ignored for png, bmp and tiff (lossless).
|
||||
.PARAMETER Compression
|
||||
Compression level for png and webp formats. For png, this is libpng's -compression_level (0-9,
|
||||
higher means smaller files but slower encoding). For webp, this is libwebp's -compression_level
|
||||
(0-6, higher means smaller files but slower encoding). Ignored for jpeg, avif, bmp and tiff.
|
||||
.DESCRIPTION
|
||||
This script uses ffmpeg to create a turnaround image from a sequence of images.
|
||||
Source images are arranged horizontally in the output, or in a grid with -Rows / -Columns.
|
||||
Supports PNG, JPEG, WEBP, BMP, AVIF and TIFF.
|
||||
Supports -WhatIf and -Confirm for safe execution.
|
||||
|
||||
Images are read from -Source (a directory, a file pattern such as Img*.jpg, or a single image file)
|
||||
and ordered with a numeric-aware (natural) sort, so Img2.jpg comes before Img10.jpg.
|
||||
|
||||
Every image is placed in a cell as large as the largest source image and centred inside it, so
|
||||
images that are not all the same size are padded with -Background rather than being scaled or
|
||||
cropped: no source pixel is ever resized or lost. The output size is therefore
|
||||
(cellWidth * columns) x (cellHeight * rows).
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Source,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Target = $null,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$Rows = 0,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$Columns = 0,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$Background = 'white',
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$Quality = -1,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[int]$Compression = -1
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$supportedExts = @('.png', '.jpg', '.jpeg', '.webp', '.bmp', '.avif', '.tif', '.tiff')
|
||||
|
||||
function Get-NaturalSortKey {
|
||||
param([string]$Name)
|
||||
|
||||
# Zero-pad every digit run so an ordinal sort orders numbers numerically
|
||||
# (Img2 -> Img000000000002, Img10 -> Img000000000010).
|
||||
return (-join ($Name -split '(\d+)' | ForEach-Object {
|
||||
if ($_ -match '^\d+$') { $_.PadLeft(12, '0') } else { $_.ToLowerInvariant() }
|
||||
}))
|
||||
}
|
||||
|
||||
function Get-ImageDimensions {
|
||||
param([string]$Path)
|
||||
|
||||
$probeOutput = & ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=p=0:s=x -- $Path 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error ('ffprobe failed for: {0}{1}{2}' -f $Path, [Environment]::NewLine, ($probeOutput -join [Environment]::NewLine))
|
||||
exit 1
|
||||
}
|
||||
|
||||
$dimLine = ([string]($probeOutput | Select-Object -First 1)).Trim()
|
||||
if ($dimLine -notmatch '^(\d+)x(\d+)$') {
|
||||
Write-Error ('Could not determine image dimensions for: {0}' -f $Path)
|
||||
exit 1
|
||||
}
|
||||
|
||||
return [pscustomobject]@{ Width = [int]$Matches[1]; Height = [int]$Matches[2] }
|
||||
}
|
||||
|
||||
# --- Default -Target: '<last directory member>.jpg' in the current directory ---
|
||||
# The name comes from the source directory when -Source is a directory, and from the
|
||||
# current directory when -Source is a file pattern or a single file. Either way the file
|
||||
# is written to the current directory.
|
||||
if ([string]::IsNullOrWhiteSpace($Target)) {
|
||||
if (Test-Path -LiteralPath $Source -PathType Container) {
|
||||
$nameSourcePath = (Resolve-Path -LiteralPath $Source).Path
|
||||
} else {
|
||||
$nameSourcePath = (Get-Location).Path
|
||||
}
|
||||
|
||||
$nameSourcePath = [System.IO.Path]::GetFullPath($nameSourcePath)
|
||||
$trimmedPath = $nameSourcePath.TrimEnd('\', '/')
|
||||
$nameLeaf = Split-Path -Path $trimmedPath -Leaf
|
||||
|
||||
# 'C:\' trims to 'C:', which has no directory member to name the file after.
|
||||
if ([string]::IsNullOrWhiteSpace($nameLeaf) -or $trimmedPath -match '^[A-Za-z]:$') {
|
||||
Write-Error ('Cannot derive a target file name from ''{0}'': the path is a drive root with no directory member. Specify -Target explicitly.' -f $nameSourcePath)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$Target = Join-Path (Get-Location).Path ('{0}.jpg' -f $nameLeaf)
|
||||
Write-Host ('No -Target specified; using {0}' -f $Target)
|
||||
}
|
||||
|
||||
# --- Resolve -Target and the output format ---
|
||||
$TargetPath = $Target
|
||||
$targetExt = [System.IO.Path]::GetExtension($TargetPath).ToLowerInvariant()
|
||||
if ($supportedExts -notcontains $targetExt) {
|
||||
$TargetPath += '.png'
|
||||
$targetExt = '.png'
|
||||
}
|
||||
|
||||
switch ($targetExt) {
|
||||
'.jpg' { $targetFormat = 'jpeg' }
|
||||
'.jpeg' { $targetFormat = 'jpeg' }
|
||||
'.tif' { $targetFormat = 'tiff' }
|
||||
'.tiff' { $targetFormat = 'tiff' }
|
||||
default { $targetFormat = $targetExt.TrimStart('.') }
|
||||
}
|
||||
|
||||
$TargetDir = Split-Path -Parent $TargetPath
|
||||
if ($TargetDir -and -not (Test-Path -LiteralPath $TargetDir)) {
|
||||
Write-Error ('Target directory does not exist: {0}' -f $TargetDir)
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Absolute target path, used to keep the output out of its own input list when the
|
||||
# turnaround image is written into the source directory (so re-runs stay idempotent).
|
||||
if ([System.IO.Path]::IsPathRooted($TargetPath)) {
|
||||
$targetFullPath = [System.IO.Path]::GetFullPath($TargetPath)
|
||||
} else {
|
||||
$targetFullPath = [System.IO.Path]::GetFullPath((Join-Path (Get-Location).Path $TargetPath))
|
||||
}
|
||||
|
||||
# --- Resolve -Source into an ordered list of image files ---
|
||||
# -Source may be a directory, a path containing wildcards (Img*.jpg) or a single image file.
|
||||
if (Test-Path -LiteralPath $Source -PathType Container) {
|
||||
$candidates = Get-ChildItem -LiteralPath $Source -File
|
||||
} else {
|
||||
$candidates = Get-ChildItem -Path $Source -File -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (-not $candidates) {
|
||||
Write-Error ('No files found for source: {0}' -f $Source)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$images = $candidates |
|
||||
Where-Object { $supportedExts -contains $_.Extension.ToLowerInvariant() } |
|
||||
Where-Object { $_.FullName -ine $targetFullPath } |
|
||||
Sort-Object -Property @{ Expression = { Get-NaturalSortKey -Name $_.Name } }
|
||||
|
||||
if (-not $images) {
|
||||
Write-Error ('No supported image files ({0}) found for source: {1}' -f ($supportedExts -join ', '), $Source)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$imageCount = @($images).Count
|
||||
|
||||
# --- Grid layout ---
|
||||
if ($Rows -lt 0 -or $Columns -lt 0) {
|
||||
Write-Error '-Rows and -Columns must be positive values.'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Rows -gt 0 -and $Columns -gt 0) {
|
||||
if (($Rows * $Columns) -lt $imageCount) {
|
||||
Write-Error ('-Rows {0} x -Columns {1} = {2} cells is not enough for {3} images.' -f $Rows, $Columns, ($Rows * $Columns), $imageCount)
|
||||
exit 1
|
||||
}
|
||||
} elseif ($Columns -gt 0) {
|
||||
$Rows = [int][Math]::Ceiling($imageCount / [double]$Columns)
|
||||
} elseif ($Rows -gt 0) {
|
||||
$Columns = [int][Math]::Ceiling($imageCount / [double]$Rows)
|
||||
} else {
|
||||
# Documented default: a single horizontal row.
|
||||
$Rows = 1
|
||||
$Columns = $imageCount
|
||||
}
|
||||
|
||||
# --- Probe every image; the cell is as large as the largest source image ---
|
||||
$dimensions = @()
|
||||
foreach ($image in $images) {
|
||||
$dimensions += Get-ImageDimensions -Path $image.FullName
|
||||
}
|
||||
|
||||
$cellWidth = ($dimensions | Measure-Object -Property Width -Maximum).Maximum
|
||||
$cellHeight = ($dimensions | Measure-Object -Property Height -Maximum).Maximum
|
||||
|
||||
$mixedSizes = @($dimensions | Where-Object { $_.Width -ne $cellWidth -or $_.Height -ne $cellHeight }).Count
|
||||
|
||||
$outputWidth = $cellWidth * $Columns
|
||||
$outputHeight = $cellHeight * $Rows
|
||||
|
||||
Write-Host ('Creating turnaround image from {0} image(s): {1}' -f $imageCount, $Source)
|
||||
Write-Host (' Layout: {0} row(s) x {1} column(s)' -f $Rows, $Columns)
|
||||
Write-Host (' Cell size: {0}x{1}' -f $cellWidth, $cellHeight)
|
||||
if ($mixedSizes -gt 0) {
|
||||
Write-Host (' Note: {0} image(s) smaller than the largest image; centred and padded with {1}.' -f $mixedSizes, $Background)
|
||||
}
|
||||
for ($i = 0; $i -lt $imageCount; $i++) {
|
||||
Write-Host (' [{0}] {1} ({2}x{3})' -f ($i + 1), $images[$i].Name, $dimensions[$i].Width, $dimensions[$i].Height)
|
||||
}
|
||||
Write-Host (' Target format: {0}' -f $targetFormat)
|
||||
Write-Host (' Output size: {0}x{1}' -f $outputWidth, $outputHeight)
|
||||
if ($Quality -ge 0) {
|
||||
if ($targetFormat -in @('jpeg', 'webp', 'avif')) {
|
||||
Write-Host (' Quality: {0} (lower = better)' -f $Quality)
|
||||
} else {
|
||||
Write-Warning ('-Quality is ignored for the lossless {0} format.' -f $targetFormat)
|
||||
}
|
||||
}
|
||||
if ($Compression -ge 0) {
|
||||
if ($targetFormat -in @('png', 'webp')) {
|
||||
Write-Host (' Compression: {0}' -f $Compression)
|
||||
} else {
|
||||
Write-Warning ('-Compression is ignored for the {0} format.' -f $targetFormat)
|
||||
}
|
||||
}
|
||||
Write-Host (' Target: {0}' -f $TargetPath)
|
||||
|
||||
# --- Build the ffmpeg filter graph ---
|
||||
# Every input is normalised to rgba (hstack/vstack require matching pixel formats across
|
||||
# inputs) and centred in a cellWidth x cellHeight box. Rows are hstacked, then vstacked.
|
||||
# A short last row is centred horizontally inside the full row width.
|
||||
$filters = @()
|
||||
for ($i = 0; $i -lt $imageCount; $i++) {
|
||||
$filters += '[{0}:v]format=rgba,pad={1}:{2}:(ow-iw)/2:(oh-ih)/2:{3}[v{0}]' -f $i, $cellWidth, $cellHeight, $Background
|
||||
}
|
||||
|
||||
$rowLabels = @()
|
||||
for ($r = 0; $r -lt $Rows; $r++) {
|
||||
$first = $r * $Columns
|
||||
$last = [Math]::Min($first + $Columns, $imageCount) - 1
|
||||
$inRow = $last - $first + 1
|
||||
|
||||
$rowInputs = -join (($first..$last) | ForEach-Object { '[v{0}]' -f $_ })
|
||||
$rowLabel = 'r{0}' -f $r
|
||||
|
||||
if ($inRow -gt 1) {
|
||||
$filters += '{0}hstack=inputs={1}[{2}]' -f $rowInputs, $inRow, $rowLabel
|
||||
} else {
|
||||
$filters += '{0}null[{1}]' -f $rowInputs, $rowLabel
|
||||
}
|
||||
|
||||
if ($inRow -lt $Columns) {
|
||||
# Incomplete last row: centre it in the full output width.
|
||||
$filters += '[{0}]pad={1}:{2}:(ow-iw)/2:0:{3}[{0}p]' -f $rowLabel, $outputWidth, $cellHeight, $Background
|
||||
$rowLabel = '{0}p' -f $rowLabel
|
||||
}
|
||||
|
||||
$rowLabels += '[{0}]' -f $rowLabel
|
||||
}
|
||||
|
||||
if ($Rows -gt 1) {
|
||||
$filters += '{0}vstack=inputs={1}[outv]' -f (-join $rowLabels), $Rows
|
||||
} else {
|
||||
$filters += '{0}null[outv]' -f $rowLabels[0]
|
||||
}
|
||||
|
||||
$filterComplex = $filters -join ';'
|
||||
|
||||
# --- Per-format encoder arguments (same mapping and defaults as Clone-Image.ps1) ---
|
||||
# -Quality is always "lower = better" from the caller's point of view; the per-format
|
||||
# translation (including inverting it for libwebp) happens here.
|
||||
$encoderArgs = @()
|
||||
switch ($targetFormat) {
|
||||
'jpeg' {
|
||||
$encoderArgs += @('-c:v', 'mjpeg', '-pix_fmt', 'yuvj420p')
|
||||
if ($Quality -ge 0) {
|
||||
$encoderArgs += @('-q:v', $Quality.ToString())
|
||||
} else {
|
||||
$encoderArgs += @('-q:v', '2')
|
||||
}
|
||||
}
|
||||
'png' {
|
||||
$encoderArgs += @('-c:v', 'png')
|
||||
if ($Compression -ge 0) {
|
||||
$encoderArgs += @('-compression_level', $Compression.ToString())
|
||||
}
|
||||
}
|
||||
'webp' {
|
||||
$encoderArgs += @('-c:v', 'libwebp')
|
||||
if ($Quality -ge 0) {
|
||||
# Invert so that "lower = better" matches JPEG/AVIF behaviour.
|
||||
$webpQuality = 100 - $Quality
|
||||
if ($webpQuality -lt 0) { $webpQuality = 0 }
|
||||
if ($webpQuality -gt 100) { $webpQuality = 100 }
|
||||
$encoderArgs += @('-quality', $webpQuality.ToString())
|
||||
} else {
|
||||
$encoderArgs += @('-quality', '90')
|
||||
}
|
||||
if ($Compression -ge 0) {
|
||||
$encoderArgs += @('-compression_level', $Compression.ToString())
|
||||
}
|
||||
}
|
||||
'avif' {
|
||||
$encoderArgs += @('-c:v', 'libaom-av1', '-still-picture', '1', '-cpu-used', '4')
|
||||
if ($Quality -ge 0) {
|
||||
$encoderArgs += @('-crf', $Quality.ToString())
|
||||
} else {
|
||||
$encoderArgs += @('-crf', '25')
|
||||
}
|
||||
}
|
||||
'bmp' { $encoderArgs += @('-c:v', 'bmp') }
|
||||
'tiff' { $encoderArgs += @('-c:v', 'tiff') }
|
||||
}
|
||||
|
||||
$ffmpegArgs = @('-y')
|
||||
foreach ($image in $images) {
|
||||
$ffmpegArgs += @('-i', $image.FullName)
|
||||
}
|
||||
$ffmpegArgs += @('-filter_complex', $filterComplex, '-map', '[outv]')
|
||||
$ffmpegArgs += $encoderArgs
|
||||
$ffmpegArgs += @('-frames:v', '1', '-map_metadata', '-1', '--', $TargetPath)
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($TargetPath, 'Create turnaround image')) {
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
|
||||
$ffmpegOutput = & ffmpeg @ffmpegArgs 2>&1
|
||||
$ffmpegExit = $LASTEXITCODE
|
||||
|
||||
if ($ffmpegExit -ne 0) {
|
||||
Write-Host 'ffmpeg error output:' -ForegroundColor Red
|
||||
$ffmpegOutput | ForEach-Object { Write-Host $_ }
|
||||
Write-Error ('ffmpeg failed with exit code: {0}' -f $ffmpegExit)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$outInfo = Get-ImageDimensions -Path $TargetPath
|
||||
Write-Host ('Turnaround image created: {0} ({1}x{2})' -f $TargetPath, $outInfo.Width, $outInfo.Height) -ForegroundColor Green
|
||||
} else {
|
||||
$formattedArgs = $ffmpegArgs | ForEach-Object { if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ } }
|
||||
Write-Host ('WhatIf: Would create turnaround image with ffmpeg {0}' -f ($formattedArgs -join ' '))
|
||||
}
|
||||
+105
-20
@@ -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
|
||||
@@ -93,6 +106,7 @@ $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 = 1312; 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' }
|
||||
@@ -100,12 +114,14 @@ $cropProfiles = @(
|
||||
@{ 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 = 1920; Height = 1056; Filter = 'scale=-2:1080,crop=1920:1080:(in_w-1920)/2:(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 = 704; Height = 1312; 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' }
|
||||
@@ -113,6 +129,7 @@ $cropProfiles = @(
|
||||
@{ 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 = 1920; Filter = 'scale=1080:-2,crop=1080:1920:(in_w-1080)/2:(in_h-1920)/2' }
|
||||
@{ 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' }
|
||||
|
||||
@@ -129,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
|
||||
|
||||
@@ -158,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"
|
||||
@@ -240,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++
|
||||
}
|
||||
|
||||
+13
-21
@@ -1,13 +1,14 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts a specific frame of a video file to a PNG image using Get-LastFrame.ps1, requiring -Video and -Frame parameters from that script.
|
||||
The image filename used for the -Image parameter is automatically derived from the -Video filename, with a "-" followed by the "-Frame" value padded with zeros (maximum of 4 digits) inserted before the image file extension (e.g., P01.mp4 for frame 10 becomes P01-0010.png).
|
||||
Extracts a specific frame of a video file to a PNG image. The extraction is done by Get-LastFrame.ps1, which this script calls with an image path derived from -Video and -Frame.
|
||||
The image filename used for the -Image parameter is automatically derived from the -Video filename, with a "-" followed by the "-Frame" value padded with zeros (at least 4 digits, wider numbers are not truncated) inserted before the image file extension (e.g., P01.mp4 for frame 10 becomes P01-0010.png).
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory.
|
||||
.PARAMETER Frame
|
||||
0-based frame index to extract. Negative values count from the end:
|
||||
-1 extracts the last frame (default), -2 extracts second-to-last, etc.
|
||||
The resulting frame index is clamped to the valid range (0 to total frames - 1).
|
||||
-1 extracts the last frame, -2 extracts second-to-last, etc.
|
||||
The resulting frame index is clamped to the valid range (0 to total frames - 1): a positive value past the last frame
|
||||
gives the last frame, and a negative value before the first frame gives frame 0.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
@@ -35,8 +36,7 @@ $totalFramesOutput = & ffprobe -v error -select_streams v:0 -count_frames -show_
|
||||
$ffprobeExit = $LASTEXITCODE
|
||||
|
||||
if ($ffprobeExit -ne 0) {
|
||||
$totalFramesOutput | ForEach-Object { Write-Error $_ }
|
||||
Write-Error ('ffprobe failed to read video file: {0}' -f $VideoPath)
|
||||
Write-Error ('ffprobe failed to read video file: {0}{1}{2}' -f $VideoPath, [Environment]::NewLine, ($totalFramesOutput -join [Environment]::NewLine))
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -48,19 +48,15 @@ if (-not $totalFrames -or ([string]$totalFrames).Trim() -notmatch '^\d+$') {
|
||||
}
|
||||
|
||||
$totalFramesInt = [int]([string]$totalFrames).Trim()
|
||||
if ($totalFramesInt -lt 1) {
|
||||
Write-Error ('Video has no frames: {0}' -f $VideoPath)
|
||||
exit 1
|
||||
}
|
||||
$maxFrameIndex = $totalFramesInt - 1
|
||||
|
||||
# Resolve frame index (handles negative values that count from the end)
|
||||
$frameToExtract = if ($Frame -lt 0) {
|
||||
$calculatedFrame = $totalFramesInt + $Frame
|
||||
[Math]::Max(0, [Math]::Min($calculatedFrame, $maxFrameIndex))
|
||||
} else {
|
||||
if ($Frame -gt $maxFrameIndex) {
|
||||
Write-Error ('Frame index {0} is out of range. Valid range: 0 to {1}' -f $Frame, $maxFrameIndex)
|
||||
exit 1
|
||||
}
|
||||
$Frame
|
||||
}
|
||||
$requestedFrame = if ($Frame -lt 0) { $totalFramesInt + $Frame } else { $Frame }
|
||||
$frameToExtract = [Math]::Max(0, [Math]::Min($requestedFrame, $maxFrameIndex))
|
||||
|
||||
# Derive the output image path: insert "-XXXX" (zero-padded resolved frame index) before .png extension
|
||||
$videoBase = [System.IO.Path]::GetFileNameWithoutExtension($VideoPath)
|
||||
@@ -73,9 +69,5 @@ $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$getLastFrameScript = Join-Path $scriptDir 'Get-LastFrame.ps1'
|
||||
|
||||
# Forward common parameters to Get-LastFrame.ps1
|
||||
$passThrough = @()
|
||||
if ($PSBoundParameters.ContainsKey('WhatIf')) { $passThrough += '-WhatIf' }
|
||||
if ($PSBoundParameters.ContainsKey('Confirm')) { $passThrough += '-Confirm' }
|
||||
|
||||
& $getLastFrameScript -Video $VideoPath -Image $imagePath -Frame $frameToExtract @passThrough
|
||||
& $getLastFrameScript -Video $VideoPath -Image $imagePath -Frame $frameToExtract -WhatIf:$WhatIfPreference -Confirm:([bool]$PSBoundParameters['Confirm'])
|
||||
exit $LASTEXITCODE
|
||||
+16
-5
@@ -11,10 +11,11 @@
|
||||
- start-end: Extract frames from start to end (e.g., "100-200")
|
||||
- positive_number: Extract frames from 0 to that number (e.g., "50")
|
||||
- -negative_number: Extract last N frames (e.g., "-10")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%", "-%")
|
||||
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%")
|
||||
- all: Extract all frames (same as "0-%")
|
||||
.DESCRIPTION
|
||||
Extracts specific range of frames of a video file to PNG images using ffmpeg.
|
||||
An existing PNG file with the same name is replaced.
|
||||
Supports -WhatIf and -Confirm for safe execution.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
@@ -39,6 +40,15 @@ if (-not (Test-Path -LiteralPath $VideoPath)) {
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Frames -match '^-.*%') {
|
||||
Write-Error "Invalid Frames format '$Frames': '-%' is not supported. Use '%' for the last frame, or -N for the last N frames."
|
||||
exit 1
|
||||
}
|
||||
if ($Frames -match '^-0+$') {
|
||||
Write-Error "Invalid Frames format '$Frames': a negative count must be at least 1. Use -1 for the last frame."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
|
||||
$totalFramesOutput = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 -- $VideoPath 2>&1
|
||||
@@ -64,7 +74,7 @@ $startFrame = 0
|
||||
$endFrame = 0
|
||||
|
||||
# Replace % with actual last frame index, and handle 'all' keyword
|
||||
$framesPattern = $Frames -replace '%', $maxFrameIndex
|
||||
$framesPattern = if ($Frames -eq '%') { "$maxFrameIndex-$maxFrameIndex" } else { $Frames -replace '%', $maxFrameIndex }
|
||||
if ($framesPattern -ieq 'all') {
|
||||
$framesPattern = "0-$maxFrameIndex"
|
||||
}
|
||||
@@ -75,7 +85,7 @@ if ($framesPattern -match '^(\d+)-(\d+)$') {
|
||||
}
|
||||
elseif ($framesPattern -match '^-(\d+)$') {
|
||||
$negativeCount = [int]$matches[1]
|
||||
$startFrame = [Math]::Max(0, $maxFrameIndex - $negativeCount)
|
||||
$startFrame = [Math]::Max(0, $maxFrameIndex - $negativeCount + 1)
|
||||
$endFrame = $maxFrameIndex
|
||||
}
|
||||
elseif ($framesPattern -match '^\d+$') {
|
||||
@@ -102,7 +112,7 @@ $frameCount = $endFrame - $startFrame + 1
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Images)) {
|
||||
$videoBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Video)
|
||||
$outputDir = Split-Path -Parent $VideoPath
|
||||
$outputDir = Split-Path -Parent (Resolve-Path -LiteralPath $VideoPath).Path
|
||||
$Images = Join-Path $outputDir 'Frames' ($videoBaseName + '_%04d.png')
|
||||
} else {
|
||||
$outputPattern = $Images
|
||||
@@ -135,9 +145,10 @@ for ($frameIndex = $startFrame; $frameIndex -le $endFrame; $frameIndex++) {
|
||||
$videoFilter = 'select=eq(n\,{0})' -f $frameIndex
|
||||
|
||||
$ffmpegArgs = @(
|
||||
'-y'
|
||||
'-i', $VideoPath
|
||||
'-vf', $videoFilter
|
||||
'-vsync', 'vfr'
|
||||
'-fps_mode', 'vfr'
|
||||
'-frames:v', '1'
|
||||
'-q:v', '1'
|
||||
'--', $outputFile
|
||||
|
||||
+58
-21
@@ -5,13 +5,19 @@
|
||||
Path to the input video file. Can be absolute or relative to current directory.
|
||||
.PARAMETER Image
|
||||
Path to the output PNG image file. Can be absolute or relative to current directory.
|
||||
Supports a printf-style numbering token (e.g. frame_%04d.png or C:\Output\frame_%04d.png), which is
|
||||
replaced with the extracted 0-based frame index, zero-padded to the width given in the token
|
||||
(%d = no padding, %04d = 4 digits). Without a token the path is used literally.
|
||||
The output directory is created when it does not exist.
|
||||
.PARAMETER Frame
|
||||
0-based frame index to extract. Negative values count from the end:
|
||||
-1 extracts the last frame (default), -2 extracts second-to-last, etc.
|
||||
The resulting frame index is clamped to the valid range (0 to total frames - 1).
|
||||
The resulting frame index is clamped to the valid range (0 to total frames - 1): a positive value past the last frame
|
||||
gives the last frame, and a negative value before the first frame gives frame 0.
|
||||
.DESCRIPTION
|
||||
Extracts a specific frame of a video file to a PNG image using ffmpeg.
|
||||
Supports negative frame indices to count from the end of the video.
|
||||
Supports a printf-style numbering token in -Image, like Get-FramesRange.ps1.
|
||||
Supports -WhatIf and -Confirm for safe execution.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
@@ -31,14 +37,14 @@ $ErrorActionPreference = 'Stop'
|
||||
|
||||
$VideoPath = $Video
|
||||
|
||||
if (-not (Test-Path $VideoPath)) {
|
||||
if (-not (Test-Path -LiteralPath $VideoPath)) {
|
||||
Write-Error ('Video file not found: {0}' -f $VideoPath)
|
||||
exit 1
|
||||
}
|
||||
|
||||
$PSNativeCommandUseErrorActionPreference = $false
|
||||
|
||||
$totalFrames = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 $VideoPath 2>&1 | Select-Object -First 1
|
||||
$totalFrames = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 -- $VideoPath 2>&1 | Select-Object -First 1
|
||||
|
||||
if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
|
||||
Write-Error ('Could not determine frame count for: {0}' -f $VideoPath)
|
||||
@@ -46,33 +52,64 @@ if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
|
||||
}
|
||||
|
||||
$totalFramesInt = [int]$totalFrames
|
||||
if ($totalFramesInt -lt 1) {
|
||||
Write-Error ('Video has no frames: {0}' -f $VideoPath)
|
||||
exit 1
|
||||
}
|
||||
$maxFrameIndex = $totalFramesInt - 1
|
||||
|
||||
$frameToExtract = if ($Frame -lt 0) {
|
||||
# Negative values count from the end
|
||||
$calculatedFrame = $totalFramesInt + $Frame
|
||||
# Clamp to valid range [0, maxFrameIndex]
|
||||
[Math]::Max(0, [Math]::Min($calculatedFrame, $maxFrameIndex))
|
||||
} else {
|
||||
# Positive values are direct indices
|
||||
if ($Frame -gt $maxFrameIndex) {
|
||||
Write-Error ('Frame index {0} is out of range. Valid range: 0 to {1}' -f $Frame, $maxFrameIndex)
|
||||
exit 1
|
||||
}
|
||||
$Frame
|
||||
}
|
||||
# Negative values count from the end; the result is clamped to [0, maxFrameIndex]
|
||||
$requestedFrame = if ($Frame -lt 0) { $totalFramesInt + $Frame } else { $Frame }
|
||||
$frameToExtract = [Math]::Max(0, [Math]::Min($requestedFrame, $maxFrameIndex))
|
||||
|
||||
Write-Host ('Total frames: {0} - extracting frame index {1}' -f $totalFramesInt, $frameToExtract)
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($Image, ('Extract frame {0} to PNG' -f $frameToExtract))) {
|
||||
# Resolve any printf-style numbering token (%d, %04d) in -Image to the extracted frame
|
||||
# index. The substitution is done here rather than left to ffmpeg: ffmpeg would treat the
|
||||
# token as an image2 sequence counter starting at 1 instead of the actual video frame index.
|
||||
$ImagePath = $Image
|
||||
$tokenPattern = [regex]'%(\d*)d'
|
||||
while ($ImagePath -match $tokenPattern) {
|
||||
$padWidth = if ($Matches[1]) { [int]$Matches[1] } else { 0 }
|
||||
$ImagePath = $tokenPattern.Replace($ImagePath, ([string]$frameToExtract).PadLeft($padWidth, '0'), 1)
|
||||
}
|
||||
|
||||
if ($ImagePath -ne $Image) {
|
||||
Write-Host ('Output pattern resolved to: {0}' -f $ImagePath)
|
||||
}
|
||||
|
||||
$outputDir = Split-Path -Parent $ImagePath
|
||||
if ($outputDir -and -not (Test-Path -LiteralPath $outputDir)) {
|
||||
if ($PSCmdlet.ShouldProcess($outputDir, 'Create output directory')) {
|
||||
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
||||
Write-Host ('Created output directory: {0}' -f $outputDir)
|
||||
}
|
||||
}
|
||||
|
||||
if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToExtract))) {
|
||||
$videoFilter = 'select=eq(n\,{0})' -f $frameToExtract
|
||||
$ffmpegStderr = & ffmpeg -y -i $VideoPath -vf $videoFilter -vsync vfr -frames:v 1 -q:v 1 $Image 2>&1
|
||||
|
||||
$ffmpegArgs = @(
|
||||
'-y'
|
||||
'-i', $VideoPath
|
||||
'-vf', $videoFilter
|
||||
'-fps_mode', 'vfr'
|
||||
'-frames:v', '1'
|
||||
'-q:v', '1'
|
||||
'--', $ImagePath
|
||||
)
|
||||
|
||||
$ffmpegStderr = & ffmpeg @ffmpegArgs 2>&1
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$ffmpegStderr | ForEach-Object { Write-Error $_ }
|
||||
Write-Error 'ffmpeg failed to extract the last frame.'
|
||||
Write-Error ('ffmpeg failed to extract frame {0}: {1}' -f $frameToExtract, (($ffmpegStderr | Out-String).Trim()))
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ('Frame saved to: {0}' -f $Image)
|
||||
if (-not (Test-Path -LiteralPath $ImagePath)) {
|
||||
Write-Error ('ffmpeg reported success but no image was written for frame {0}: {1}' -f $frameToExtract, $ImagePath)
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host ('Frame saved to: {0}' -f $ImagePath)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Get-VideoSubtitles.ps1" %*
|
||||
@@ -0,0 +1,104 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Extracts the embedded subtitle tracks of a video file into the current directory.
|
||||
.PARAMETER Video
|
||||
Path to the input video file. Can be absolute or relative to current directory.
|
||||
.PARAMETER List
|
||||
When present, lists the subtitle tracks instead of extracting them. Creates no files.
|
||||
.DESCRIPTION
|
||||
This script uses ffprobe to list the subtitle streams of a video file and ffmpeg to copy each one out:
|
||||
- Text subtitles (SubRip, WebVTT and mov_text, found in MKV, WebM, MP4, MOV and M4V) are written as .srt.
|
||||
- ASS/SSA subtitles (MKV) are written as .ass, keeping their styling.
|
||||
- Blu-ray PGS subtitles (hdmv_pgs_subtitle, found in MKV, M2TS and TS) are copied unchanged to .sup.
|
||||
- Bitmap DVD subtitles (dvd_subtitle, found in MKV and VOB) and DVB subtitles (dvb_subtitle, found in TS) are listed but not extracted, because ffmpeg cannot write them as text or as a standalone file.
|
||||
- AVI files have no standard subtitle track, so nothing is extracted from them.
|
||||
Bitmap subtitles can only be turned into text with OCR, which this script does not do.
|
||||
Each file is named "<video name>.<language>.<extension>" in the current directory. The language is the 2-letter ISO 639-1 code when one exists, otherwise the 3-letter code from the file, or "und" when the track has no language tag. A repeated name gets a counter: "<video name>.<language>.2.<extension>".
|
||||
An existing file with the same name is overwritten.
|
||||
.NOTES
|
||||
Requires ffmpeg and ffprobe to be installed and available in the system PATH.
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Video,
|
||||
|
||||
[Parameter(Mandatory = $false)]
|
||||
[switch]$List
|
||||
)
|
||||
|
||||
function Get-LanguageCode([string]$Tag) {
|
||||
if (-not $Tag -or $Tag -eq 'und') { return 'und' }
|
||||
$culture = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::NeutralCultures) | Where-Object { $_.ThreeLetterISOLanguageName -eq $Tag } | Select-Object -First 1
|
||||
if ($culture) { return $culture.TwoLetterISOLanguageName }
|
||||
return $Tag
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $Video)) {
|
||||
throw "Video file not found: $Video"
|
||||
}
|
||||
$videoPath = (Resolve-Path -LiteralPath $Video).Path
|
||||
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($videoPath)
|
||||
$outputDir = (Get-Location).ProviderPath
|
||||
|
||||
$ffprobeJson = & ffprobe -v error -select_streams s -show_entries 'stream=index,codec_name:stream_tags=language' -of json -- $videoPath
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffprobe failed to read video file: $videoPath"
|
||||
}
|
||||
$streams = @((ConvertFrom-Json ($ffprobeJson -join "`n")).streams | Where-Object { $_ })
|
||||
|
||||
$plan = @()
|
||||
$used = @{}
|
||||
foreach ($s in $streams) {
|
||||
$ext = switch ($s.codec_name) {
|
||||
{ $_ -in 'subrip', 'mov_text', 'webvtt' } { '.srt' }
|
||||
{ $_ -in 'ass', 'ssa' } { '.ass' }
|
||||
'hdmv_pgs_subtitle' { '.sup' }
|
||||
default { $null }
|
||||
}
|
||||
$code = Get-LanguageCode $s.tags.language
|
||||
$fileName = "$baseName.$code$ext"
|
||||
$counter = 1
|
||||
while ($used.ContainsKey($fileName.ToLowerInvariant())) {
|
||||
$counter++
|
||||
$fileName = "$baseName.$code.$counter$ext"
|
||||
}
|
||||
$used[$fileName.ToLowerInvariant()] = $true
|
||||
$plan += [pscustomobject]@{ Index = [int]$s.index; Codec = $s.codec_name; Ext = $ext; File = Join-Path $outputDir $fileName }
|
||||
}
|
||||
|
||||
Write-Host "Subtitle tracks in: $videoPath"
|
||||
if ($streams.Count -eq 0) {
|
||||
Write-Host "No subtitle stream found in video file." -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($List) {
|
||||
$plan | Format-Table -Property @{ Label = 'Index'; Expression = { $_.Index } }, @{ Label = 'Codec'; Expression = { $_.Codec } }, @{ Label = 'Output'; Expression = { if ($_.Ext) { Split-Path -Leaf $_.File } else { 'not supported' } } } -AutoSize | Out-String | Write-Host
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($p in $plan) {
|
||||
if (-not $p.Ext) {
|
||||
Write-Warning "Subtitle stream $($p.Index) ($($p.Codec)) is not supported and was skipped."
|
||||
continue
|
||||
}
|
||||
if ($PSCmdlet.ShouldProcess($p.File, "Extract subtitle stream $($p.Index) ($($p.Codec))")) {
|
||||
$codecArgs = if ($p.Ext -eq '.srt') { @('-c:s', 'srt') } else { @('-c:s', 'copy') }
|
||||
if ($p.Ext -eq '.sup') { $codecArgs += @('-f', 'sup') }
|
||||
& ffmpeg -v error -y -i $videoPath -map "0:$($p.Index)" @codecArgs $p.File
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "ffmpeg failed to extract subtitle stream $($p.Index) with exit code $LASTEXITCODE"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $p.File)) {
|
||||
throw "Subtitle file was not created: $($p.File)"
|
||||
}
|
||||
Write-Host "Subtitle extracted: $($p.File)" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Error: $_"
|
||||
exit 1
|
||||
}
|
||||
@@ -5,17 +5,43 @@ A collection of PowerShell scripts for video processing tasks using ffmpeg and f
|
||||
## Prerequisites
|
||||
|
||||
- **PowerShell 7+** (pwsh.exe)
|
||||
- **ffmpeg** - Must be in system PATH
|
||||
- **ffprobe** - Must be in system PATH
|
||||
- **rife-ncnn-vulkan** - Must be in system PATH
|
||||
- **Platform**: tested on Windows 11 only. Linux and WSL are not tested and not supported.
|
||||
- **ffmpeg** - Must be in system PATH. Not needed by Get-FramesCount.ps1, Get-VideoInfo.ps1 and Create-FullSubtitle.ps1.
|
||||
- **ffprobe** - Must be in system PATH. Not needed by Stitch-InBetween.ps1.
|
||||
- **rife-ncnn-vulkan** - Must be in system PATH and also defined the environment variable **RIFE_HOME** to the same directory
|
||||
- **upscayl-bin** - Must be in system PATH and also defined the environment variable **UPSCAYL_HOME** to the same directory
|
||||
|
||||
## ffmpeg
|
||||
|
||||
To install ffmpeg on Windows, download it from [ffmpeg.org](https://ffmpeg.org/download.html) and add it to your system PATH.
|
||||
To install ffmpeg on Windows, download it from [ffmpeg.org](https://ffmpeg.org/) and add it to your system PATH. The latest release is available from [BtbN FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds/). The Windows build used here is the `win64-gpl` flavor.
|
||||
|
||||
## rife-ncnn-vulkan
|
||||
|
||||
To install rife-ncnn-vulkan on Windows, download it from [rife-ncnn-vulkan](https://github.com/nihui/rife-ncnn-vulkan) and add it to your system PATH.
|
||||
To install rife-ncnn-vulkan on Windows, see the [RIFE 4.26 installation guide](RIFE.md). Add it to your system PATH.
|
||||
|
||||
## upscayl-bin
|
||||
|
||||
To install upscayl-bin on Windows, see the [Upscalers installation guide](Upscalers.md). Add it to your system PATH.
|
||||
|
||||
## NCNN_GPU_ID
|
||||
|
||||
The environment variable **NCNN_GPU_ID** is used to specify the GPU device ID for ncnn-based applications.
|
||||
|
||||
This environment variable must be set for both `rife-ncnn-vulkan` and `upscayl-bin`.
|
||||
|
||||
The value stored in **NCNN_GPU_ID** will be either:
|
||||
|
||||
- **`0`** — use the first Vulkan-capable GPU (device index 0). If you have multiple GPUs, they'd be `0`, `1`, `2`, etc.
|
||||
- **`-1`** — use CPU only for `rife-ncnn-vulkan` (no Vulkan GPU is available). `upscayl-bin` rejects `-g -1` on the tested build, so the scripts omit its `-g` flag for `-1`, and it then uses its default device.
|
||||
|
||||
These map directly to the `-g` flag values accepted by both `rife-ncnn-vulkan` and `upscayl-bin`:
|
||||
|
||||
| `-g` value | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | First GPU |
|
||||
| `1` | Second GPU |
|
||||
| `2` | Third GPU |
|
||||
| `-1` | CPU for `rife-ncnn-vulkan`; no `-g` flag for `upscayl-bin` (its default device) |
|
||||
|
||||
---
|
||||
|
||||
@@ -23,17 +49,24 @@ To install rife-ncnn-vulkan on Windows, download it from [rife-ncnn-vulkan](http
|
||||
|
||||
| Script | Purpose |
|
||||
| :--- | :--- |
|
||||
| Clone-Image.ps1 | Clone/copy image files with optional resizing (cover-scale then crop), format conversion, and quality/compression settings |
|
||||
| Clone-Video.ps1 | Clone/copy video files with optional frame range and sequence extraction |
|
||||
| Compare-Videos.ps1 | Combine 2 to 4 video files into a single 1920x1080 side-by-side or grid comparison video |
|
||||
| Create-Clip.ps1 | Create a clip of a frame range from a video, named from the source with a suffix (default `-Clip`) |
|
||||
| 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-PillarBox.ps1 | Wrap a video in a 16:9 frame with selectable side-bar effects (black, white, blurred-background variants) |
|
||||
| Create-Scenes.ps1 | Detect scene changes with ffmpeg and extract each scene as a separate clip using Create-Clip.ps1 |
|
||||
| Create-ShortClip.ps1 | Create short clips from videos with cropping and overlay options |
|
||||
| Create-Turnaround.ps1 | Combine a sequence of images into a single turnaround (character reference sheet) image, in a row or a grid |
|
||||
| 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-Frame.ps1 | Extract a specific frame of a video to PNG, with the image name derived from the video and frame index |
|
||||
| Get-FramesCount.ps1 | Get total frame count of a video |
|
||||
| Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG |
|
||||
| Get-FramesRange.ps1 | Extract a range of frames to PNG images |
|
||||
| Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG, with an optional filename pattern |
|
||||
| Get-VideoAudio.ps1 | Extracts audio in either WAV or MP3 format from a video file |
|
||||
| Get-VideoInfo.ps1 | Extract and display video metadata from comment tags |
|
||||
| Stich-InBetween.ps1 | Generate interpolated in-between frames between two images using RIFE and encode to video |
|
||||
| Get-VideoSubtitles.ps1 | Extract the embedded subtitle tracks of a video to SRT, ASS or SUP files in the current directory, or list them with `-List` |
|
||||
| Stitch-InBetween.ps1 | Generate interpolated in-between frames between two images using RIFE and encode to video |
|
||||
|
||||
**Important**:
|
||||
|
||||
@@ -42,12 +75,45 @@ To install rife-ncnn-vulkan on Windows, download it from [rife-ncnn-vulkan](http
|
||||
|
||||
## Scripts Installation
|
||||
|
||||
The scripts are available from **git.gurusushi.dyndns.org**, repository: **guru/Video-Processing-Scripts**.
|
||||
The scripts are available from two locations:
|
||||
|
||||
- SVN: **public-svn**, project: **Video Processing Scripts**.
|
||||
- Git: **git.gurusushi.dyndns.org**, repository: **guru/Video-Processing-Scripts**.
|
||||
|
||||
```bash title="Clone the repository from Git"
|
||||
git clone https://git.gurusushi.dyndns.org/guru/Video-Processing-Scripts.git
|
||||
```
|
||||
|
||||
**Note**: there is a `Update-Video-Processing-Scripts.cmd` script that can be used to update the directory from Git to get the latest version.
|
||||
|
||||
Add the project folder to your PATH environment variable to access the scripts from any location.
|
||||
|
||||
---
|
||||
|
||||
## Agent Skill
|
||||
|
||||
[SKILL.md](SKILL.md) is an agent skill for AI coding agents (such as Claude Code, Devin, VS Code, etc.). It teaches the agent how to use these scripts safely, so the agent does not have to rediscover the documentation for each task.
|
||||
|
||||
What it covers:
|
||||
|
||||
- **Prerequisites and environment**: PowerShell 7+, ffmpeg, ffprobe, rife-ncnn-vulkan and upscayl-bin, and the `RIFE_HOME`, `UPSCAYL_HOME` and `NCNN_GPU_ID` variables. The agent checks them once before first use, and stops to ask you if something is missing instead of installing it.
|
||||
- **Safety rules**: it always previews with `-WhatIf` first, never passes `-Confirm`, and lists which scripts overwrite or delete existing files.
|
||||
- **Script catalog**: every script with a one-line purpose and a link to its page. The agent loads a page only when it needs the details.
|
||||
- **Workflows and troubleshooting**: common recipes, such as clipping a frame range or detecting scenes, and the documented error messages with their fixes.
|
||||
|
||||
To use it, copy the whole `video-processing-scripts` folder directly into your agent's skills directory, keeping the folder name. Do not copy only `SKILL.md`: it links to the documentation pages by relative path, so those pages must stay in the same folder. For example:
|
||||
|
||||
- Claude Code: `"%USERPROFILE%\.claudeskills\video-processing-scripts\"`
|
||||
- Devin: `"%USERPROFILE%\.codeium\windsurf\skills\"`
|
||||
- VS Code: `"%USERPROFILE%\.copilot\skills\"`
|
||||
|
||||
The `video-processing-scripts` folder is available in both repositories:
|
||||
|
||||
- **Git** (`guru/Video-Processing-Scripts`): the `video-processing-scripts` folder.
|
||||
- **SVN** (project `Video Processing Scripts`): the `video-processing-scripts` folder.
|
||||
|
||||
Each copy contains `SKILL.md` and the markdown documentation pages required by the skill.
|
||||
|
||||
## Notes
|
||||
|
||||
- All scripts require PowerShell 7+ (use `pwsh.exe`)
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
@echo off
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Stich-InBetween.ps1" %*
|
||||
@@ -0,0 +1,2 @@
|
||||
@echo off
|
||||
pwsh.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0Stitch-InBetween.ps1" %*
|
||||
@@ -34,10 +34,10 @@
|
||||
Frames per second for the output video. Default is 24.
|
||||
|
||||
.EXAMPLE
|
||||
.\Stich-InBetween.ps1 -First "frame1.png" -Last "frame2.png" -Frames 30 -Video "output.mp4"
|
||||
.\Stitch-InBetween.ps1 -First "frame1.png" -Last "frame2.png" -Frames 30 -Video "output.mp4"
|
||||
|
||||
.EXAMPLE
|
||||
.\Stich-InBetween.ps1 -First "D:\Images\start.png" -Last "D:\Images\end.png" -Frames 60 -Video "D:\Output\video.mp4" -FPS 30
|
||||
.\Stitch-InBetween.ps1 -First "D:\Images\start.png" -Last "D:\Images\end.png" -Frames 60 -Video "D:\Output\video.mp4" -FPS 30
|
||||
|
||||
.NOTES
|
||||
Requires:
|
||||
@@ -0,0 +1,224 @@
|
||||
# Clone-Image.ps1
|
||||
|
||||
Clones (copies) an image file using ffmpeg with optional resizing, quality and compression settings. Reads PNG, JPEG, WEBP, BMP, AVIF and TIFF sources and writes to any of those formats depending on the `-Target` file extension.
|
||||
|
||||
## Synopsis
|
||||
|
||||
The output format is chosen from the `-Target` extension (`.png`, `.jpg`/`.jpeg`, `.webp`, `.bmp`, `.avif`, `.tif`/`.tiff`; defaults to `.png` when no recognised extension is given). Each format has its own encoder profile — the script picks the appropriate ffmpeg encoder and applies any quality/compression settings. See [Output formats](#output-formats) for the full matrix.
|
||||
|
||||
When `-Size` is provided, the source image is resized using a **cover-then-crop** strategy:
|
||||
|
||||
1. The image is scaled (preserving its original pixel aspect ratio) so it fully covers the requested width *and* height — one dimension matches exactly, the other overflows.
|
||||
2. The overflowing dimension is then centre-cropped so the final image is exactly the requested size.
|
||||
|
||||
The source aspect ratio is preserved at all times (no warping/stretching).
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Source`** (required): Path to input image file. Accepted formats: `.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.avif`, `.tif`, `.tiff`. If the path has no extension, the script tries each supported extension in turn.
|
||||
- **`-Target`** (required): Path to output image file. The extension picks the output format (PNG, JPEG, WEBP, BMP, AVIF, or TIFF). If the extension is missing or unrecognised, `.png` is appended and the PNG profile is used.
|
||||
- **`-Size`** (optional): Size of the output image. Uses the source image's size if not specified. Possible values: `480p`, `720p`, `1080p`, `HD`, `1440p`, `2K`, `2160p`, `4K`, or custom size in format `width:height`. When specified, the source is **cover-scaled** (aspect ratio preserved) so it fully covers the requested dimensions, then **centre-cropped** to the exact target size. No image warping.
|
||||
- **`-Quality`** (optional): Quality factor for lossy output formats (JPEG, WEBP, AVIF). **Lower values mean better quality** (consistent across all three formats). Ignored for PNG, BMP and TIFF (lossless).
|
||||
- **JPEG**: passed to ffmpeg's `-q:v` (typical range `1-31`, lower = better).
|
||||
- **WEBP**: inverted internally and passed to libwebp's `-quality` (`0-100`, higher = better), so the "lower = better" semantic is preserved from the caller's perspective.
|
||||
- **AVIF**: passed to libaom-av1's `-crf` (`0-63`, lower = better).
|
||||
- **`-Compression`** (optional): Compression level for PNG and WEBP outputs.
|
||||
- **PNG**: libpng's `-compression_level` (`0-9`, higher = smaller files / slower encoding).
|
||||
- **WEBP**: libwebp's `-compression_level` (`0-6`, higher = smaller files / slower encoding).
|
||||
- Ignored for JPEG, AVIF, BMP and TIFF.
|
||||
- **`-Metadata`**: Copy available metadata from source image file to target image file.
|
||||
- **`-Upscale`** (optional): Upscale factor (`2`, `3` or `4`) applied to the source image with `upscayl-bin.exe` **before** ffmpeg runs. The upscaled image is written to a Windows temp PNG (under `%TEMP%`) and used as the ffmpeg input, so any `-Size` / `-LTX` cover-scale-then-crop is applied to the already-upscaled image. The temp PNG is deleted automatically when the script finishes. Requires `upscayl-bin.exe` in PATH and the `UPSCAYL_HOME` and `NCNN_GPU_ID` environment variables (see [index](index.md)). Omit (or pass `0`) to disable upscaling.
|
||||
- **`-Model`** (optional): Upscayl model name to use when `-Upscale` is specified. Defaults to `4xNomos8kSC`. The model files (`<Model>.bin` and `<Model>.param`) must exist under `%UPSCAYL_HOME%\models` (see the [Upscalers installation guide](Upscalers.md) for installing additional models). Ignored when `-Upscale` is not specified.
|
||||
- **`-Keep`** (optional): Path at which to save a copy of the upscayl-bin output (i.e. the upscaled image, **before** ffmpeg's cover-scale-then-crop). The file extension picks the format (`.png`, `.jpg`/`.jpeg`, `.webp`, `.bmp`, `.avif`, `.tif`/`.tiff`); if the extension is missing or unrecognised, `.png` is appended. When the requested format is `.png` (the upscayl native output) the temp file is simply copied; otherwise it is converted with ffmpeg using the same per-format encoders as `-Target` (with default quality settings). Only valid when `-Upscale` is specified — it is ignored (with a warning) otherwise.
|
||||
- **`-LTX`**: Override the dimensions chosen by `-Size` with the closest LTX-2 (Wan2GP by DeepBeepMeep) canonical resolution for the source's aspect ratio. Only valid when `-Size` is one of `HD`, `1080p`, `720p` or `480p`. The script probes the source's width/height, computes its aspect ratio, and picks the bucket whose ratio is numerically closest. Cover-scale-then-centre-crop still applies, so the source is fitted to the picked LTX dimensions without warping. The buckets per `-Size` are:
|
||||
- **`HD` / `1080p`**: `1920x1088` (16:9), `1088x1920` (9:16), `1664x960` (21:9), `960x1664` (9:21).
|
||||
- **`720p`**: `1280x704` (16:9), `704x1280` (9:16), `1088x576` (21:9), `576x1088` (9:21).
|
||||
- **`480p`**: `832x448` (16:9), `448x832` (9:16), `896x512` (4:3), `512x896` (3:4).
|
||||
|
||||
## Examples
|
||||
|
||||
**Clone entire image (same size, same format):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target output.png
|
||||
```
|
||||
|
||||
**Convert PNG to JPEG with default quality:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo.jpg
|
||||
```
|
||||
|
||||
**Convert with custom JPEG quality (lower = better):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo.jpg -Quality 3
|
||||
# Uses ffmpeg -q:v 3
|
||||
```
|
||||
|
||||
**Convert to WEBP at high quality:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo.webp -Quality 10
|
||||
# Internally maps to libwebp -quality 90 (100 - 10)
|
||||
```
|
||||
|
||||
**Convert to AVIF at near-visually-lossless:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo.avif -Quality 18
|
||||
# Uses libaom-av1 -crf 18 -still-picture 1
|
||||
```
|
||||
|
||||
**Upscale 4x with the default model, then crop to 4K:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source small.jpg -Target big_4k.png -Upscale 4 -Size 4K
|
||||
# small.jpg is upscaled x4 with 4xNomos8kSC into a temp PNG;
|
||||
# ffmpeg then cover-scales the upscaled image and centre-crops to 3840x2160.
|
||||
```
|
||||
|
||||
**Upscale 2x with a custom model:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo_2x.png -Upscale 2 -Model 4xUltraSharp
|
||||
# Requires 4xUltraSharp.bin and 4xUltraSharp.param under %UPSCAYL_HOME%\models.
|
||||
```
|
||||
|
||||
**Upscale 4x to a JPEG with custom quality:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source thumb.jpg -Target thumb_hi.jpg -Upscale 4 -Quality 3
|
||||
```
|
||||
|
||||
**Upscale 4x and also keep the full-resolution upscaled PNG:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source small.jpg -Target small_4k.png -Upscale 4 -Size 4K -Keep small_upscaled.png
|
||||
# small_upscaled.png is the raw upscayl output (no crop); small_4k.png is cover-scaled+cropped to 3840x2160.
|
||||
```
|
||||
|
||||
**Upscale and keep the upscaled image as JPEG (format conversion via ffmpeg):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo_2x.png -Upscale 2 -Keep photo_upscaled.jpg
|
||||
# upscayl writes a PNG; ffmpeg converts that PNG to photo_upscaled.jpg before the main clone step.
|
||||
```
|
||||
|
||||
**Resize using a named preset (cover-scale then crop):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo_720.png -Size 720p
|
||||
# Scales the source so it fully covers 1280x720, then centre-crops to exactly 1280x720.
|
||||
```
|
||||
|
||||
**Pick the closest LTX-2 720p bucket from a 16:9 source:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source landscape.png -Target landscape_ltx720.png -Size 720p -LTX
|
||||
# A 1920x1080 (16:9) source is fitted to 1280x704.
|
||||
```
|
||||
|
||||
**Pick the closest LTX-2 1080p bucket from a 9:16 phone shot:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source portrait.jpg -Target portrait_ltx1080.png -Size 1080p -LTX
|
||||
# A 1080x1920 (9:16) source is fitted to 1088x1920.
|
||||
```
|
||||
|
||||
**Pick the closest LTX-2 480p bucket from a 4:3 source:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source legacy.jpg -Target legacy_ltx480.png -Size 480p -LTX
|
||||
# A 1024x768 (4:3) source is fitted to 896x512.
|
||||
```
|
||||
|
||||
**Resize with a custom resolution:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target square.jpg -Size 1024:1024
|
||||
# Scales so the image fully covers 1024x1024, then centre-crops to a square.
|
||||
```
|
||||
|
||||
**Maximum PNG compression:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target small.png -Compression 9
|
||||
```
|
||||
|
||||
**WEBP with explicit quality and compression effort:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target photo.webp -Quality 20 -Compression 6
|
||||
# libwebp -quality 80 -compression_level 6
|
||||
```
|
||||
|
||||
**Preserve metadata when converting formats:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.jpg -Target photo.webp -Metadata
|
||||
# Copies EXIF/XMP/etc. that ffmpeg can transport into the WEBP container.
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source "C:\Photos\input.png" -Target "D:\Output\result.jpg"
|
||||
```
|
||||
|
||||
## Output formats
|
||||
|
||||
The output format is selected from the `-Target` file extension. Each format has its own ffmpeg encoder and the parameters that apply to it.
|
||||
|
||||
| Format | Target extension(s) | ffmpeg encoder | Lossy? | `-Quality` applies | `-Compression` applies |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| PNG | `.png` (default) | `png` | No | No | Yes (`0-9`, higher = smaller) |
|
||||
| JPEG | `.jpg`, `.jpeg` | `mjpeg` (yuvj420p) | Yes | Yes (`1-31`, lower = better) | No |
|
||||
| WEBP | `.webp` | `libwebp` | Yes | Yes (inverted: `0-100`, lower = better) | Yes (`0-6`, higher = smaller) |
|
||||
| AVIF | `.avif` | `libaom-av1` (still-picture) | Yes | Yes (`0-63`, lower = better) | No |
|
||||
| BMP | `.bmp` | `bmp` | No | No | No |
|
||||
| TIFF | `.tif`, `.tiff` | `tiff` | No | No | No |
|
||||
|
||||
### General rules
|
||||
|
||||
- The output format is determined entirely by the `-Target` extension. If `-Target` has no extension (or an unrecognised one), `.png` is appended and the PNG profile is used.
|
||||
- Resizing always uses the **cover-scale then centre-crop** strategy. The source pixel aspect ratio is preserved (no warping). When `-Size` is omitted, the source dimensions are kept as-is.
|
||||
- `-Quality` and `-Compression` are interpreted in a format-agnostic way from the caller's perspective: **lower `-Quality` always means better quality**, and **higher `-Compression` always means smaller files**. Per-format ffmpeg mappings (including the libwebp `-quality` inversion) happen inside the script.
|
||||
- Defaults when `-Quality`/`-Compression` are omitted: JPEG uses `-q:v 2`, WEBP uses `-quality 90`, AVIF uses `-crf 25`. PNG and lossless formats are encoded with ffmpeg's default settings.
|
||||
- AVIF output uses `libaom-av1` with `-still-picture 1` and `-cpu-used 4` for a reasonable speed/quality tradeoff. Your ffmpeg build must include `libaom-av1` for `.avif` output to work.
|
||||
- `-Metadata` toggles ffmpeg's `-map_metadata 0` (copy) vs `-map_metadata -1` (strip). Which metadata actually survives the conversion depends on what the source and target formats can carry.
|
||||
- `-LTX` only takes effect when `-Size` is `HD`, `1080p`, `720p` or `480p`. Combining it with any other `-Size` value (or omitting `-Size`) is rejected with an explicit error so the user is never silently downgraded to the regular preset.
|
||||
- `-Upscale` runs `upscayl-bin.exe` **before** ffmpeg. The script validates that `upscayl-bin.exe` is on PATH, that `UPSCAYL_HOME` is set and exists, that the `models` subfolder and the requested `<Model>.bin` / `<Model>.param` files are present, and that `NCNN_GPU_ID` is set to a valid integer (`0+` for a GPU index; `-1` omits the `-g` flag, so upscayl-bin uses its default device, which is not necessarily the CPU). The intermediate upscaled PNG is created under `%TEMP%` and deleted after the run (including on ffmpeg failure).
|
||||
- `-Keep` saves the raw upscayl output (no cropping/scaling). When the `-Keep` extension is `.png`, the temp file is copied directly (no re-encode); for any other supported extension a separate ffmpeg invocation converts the PNG to the requested format with default quality settings (JPEG `-q:v 2`, WEBP `-quality 90`, AVIF `-crf 25`). `-Keep` is ignored with a warning if `-Upscale` is not specified.
|
||||
|
||||
### Notes
|
||||
|
||||
- **AVIF encoding is slow.** `libaom-av1` is significantly slower than the other encoders even with `-cpu-used 4`. WebP at high `-Compression` is also slower than PNG or JPEG.
|
||||
- **JPEG output is always 4:2:0 chroma** (`yuvj420p`). If you need 4:4:4 JPEG or 16-bit output, edit the encoder args directly.
|
||||
- **TIFF and BMP** are stored uncompressed (TIFF can be configured for LZW/Deflate via additional ffmpeg options if needed).
|
||||
|
||||
## Existing Files
|
||||
|
||||
- An existing `-Target` image is **replaced** without a prompt. ffmpeg runs with `-y`, so the old file is lost. Use `-WhatIf` first, or write to a new name.
|
||||
- An existing file at `-Keep` is replaced in the same way.
|
||||
- The folders that contain `-Target` and `-Keep` must already exist. The script stops with an error instead of creating them.
|
||||
- The temporary upscaled PNG is deleted when the script finishes.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually creating files. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before each state-changing operation.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target output.jpg -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before creating files:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Image.ps1 -Source photo.png -Target output.avif -Confirm
|
||||
```
|
||||
@@ -0,0 +1,484 @@
|
||||
# Clone-Video.ps1
|
||||
|
||||
Clones (copies) a video file using ffmpeg with optional frame range selection and sequence extraction. Reads MP4 (H.264), WebM and Matroska (MKV) sources — including YouTube/Google DASH downloads encoded as VP9 + Opus/AAC — and writes MP4, WebM, or MKV output depending on the `-Target` file extension. Also builds a video from a directory of sequentially-named PNG files (e.g. `frame_0000.png`) when `-Source` points to a folder, or from a single still image (`.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tiff`, `.tif`, `.gif`) for the duration of a provided audio file when `-Source` is an image and `-Audio` is specified.
|
||||
|
||||
## Synopsis
|
||||
|
||||
When `Source` is a **directory** containing PNG files with sequence numbers:
|
||||
|
||||
- PNG files are sorted alphabetically (ascending) to form the frame list
|
||||
- `-StartFrame` / `-EndFrame` filter the list by 0-based index before encoding
|
||||
- If `-Reverse` is set, the list is sorted descending **after** any frame range filtering
|
||||
- If `-Size` is not specified, the dimensions of the first PNG are used
|
||||
- If `-FPS` is not specified, 24 fps is used
|
||||
- If `-CRF` is not specified, CRF 8 is used
|
||||
- If `-Audio` is not specified, the output has no audio
|
||||
- `-LTX` applies the same closest-bucket logic as for video sources
|
||||
- `-Crop` applies the same [crop rules](#crop-rules) as for video sources
|
||||
- `-Sequence` and `-Metadata` are **not** allowed in this mode and produce an error
|
||||
|
||||
When `Source` is a **supported image file** (`.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tiff`, `.tif`, `.gif`) and `-Audio` is provided:
|
||||
|
||||
- The image is looped as every frame for the duration of the audio file
|
||||
- `-NoAudio` is **not** allowed in this mode
|
||||
- If `-Size` is not specified, the source image dimensions are used
|
||||
- If `-FPS` is not specified, 24 fps is used
|
||||
- If `-CRF` is not specified, CRF 8 is used
|
||||
- `-Size`, `-FPS`, `-CRF`, `-LTX` and `-Crop` are honoured
|
||||
- `-StartFrame`, `-EndFrame`, `-Frames`, `-Sequence`, `-Metadata` and `-Reverse` are ignored
|
||||
|
||||
When `-Frames` is provided, it defines a frame range using a flexible format and is **mutually exclusive** with `-StartFrame`, `-EndFrame` and `-Sequence`.
|
||||
|
||||
When `Sequence` is provided (video source mode):
|
||||
|
||||
- If `EndFrame` is `-1`: Extracts the last frame to PNG, then clones video excluding the last frame
|
||||
- If `EndFrame` is not `-1`: Extracts the frame at `EndFrame` to PNG, then clones video from `StartFrame` to `EndFrame-1`
|
||||
|
||||
The output container is chosen from the `-Target` extension (`.mp4`, `.webm`, or `.mkv`; defaults to `.mp4` when no recognised extension is given). Each container has its own codec profile — the script stream-copies streams that are already compatible and transcodes anything else to the container's preferred codecs. See [Output containers](#output-containers) for the full matrix.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Source`** (required): Path to an input video file **or** a directory containing PNG files with sequence numbers (e.g. `frame_0000.png`) **or** a supported image file. For video files — accepted containers: `.mp4`, `.webm`, `.mkv`; if the path has no extension the script tries `.mp4`, `.webm`, then `.mkv` in turn. For PNG sequence directories — see [PNG Image Sequence Mode](#png-image-sequence-mode). For supported image files — see [Image-to-Video Mode](#image-to-video-mode).
|
||||
- **`-Target`** (required): Path to output video file. The extension picks the output container: `.mp4` (H.264/AAC), `.webm` (VP9/Opus), or `.mkv` (H.264/AAC, permissive stream-copy). If the extension is missing or unrecognised, `.mp4` is appended and the MP4 profile is used.
|
||||
- **`-StartFrame`** (optional): 0-based frame index to start cloning from (default: 0). Mutually exclusive with `-Frames`.
|
||||
- **`-EndFrame`** (optional): 0-based frame index to end cloning at, use -1 for end (default: -1). Mutually exclusive with `-Frames`.
|
||||
- **`-Frames`** (optional): 0-based frame range to clone. Mutually exclusive with `-StartFrame`, `-EndFrame`, and `-Sequence`. Supported formats:
|
||||
- `start-end`: Clone frames from `start` to `end` (e.g., `"100-500"`). Both ends must lie inside the video, and `start` must not be greater than `end`. Otherwise the script stops with exit code `1`.
|
||||
- `positive_number`: Clone frames from 0 to that number (e.g., `"50"`). A single number past the last frame is clamped to the last frame (`"150"` on a 100-frame video gives frames 0 to 99).
|
||||
- `-N`: Clone the last N frames (e.g., `"-10"` clones frames 90 to 99 of a 100-frame video; `"-1"` clones the last frame only)
|
||||
- `%`: The last frame index. Alone (`"%"`) it selects only the last frame. In a range (e.g., `"10-%"`) it is the end of the range
|
||||
- `-%` is not supported and is rejected with exit code `1`
|
||||
- `all`: Clone all frames (same as `"0-%"`)
|
||||
- **`-Sequence`** (optional): Path to PNG file for extracted frame (`.png` extension added if missing)
|
||||
- **`-Size`** (optional): Size of the output video. Uses the input video's size if not specified. Possible values: `320p`, `384p`, `480p`, `720p`, `900p`, `1080p`, `HD`, `1440p`, `2K`, `2160p`, `4K`, or custom size in format `width:height`. For standard named sizes the output **preserves the source aspect ratio**: the reference height is used as the short-side target and the other dimension is computed from the source AR — landscape/square sources use the reference as the output height (width computed), portrait sources use it as the output width (height computed). Custom sizes (`width:height`) are applied exactly as given. Forces a video re-encode, since scaling requires re-encoding.
|
||||
- **`-FPS`** (optional): Frames per second for the output video. Uses the input video's FPS if not specified. Forces a video re-encode, since changing frame rate requires re-encoding.
|
||||
- **`-NoAudio`**: Exclude audio from cloned video. Forces a video re-encode, since removing the audio stream requires re-muxing the output file.
|
||||
- **`-Audio`** (optional): Path to an alternate audio file to use for the target video. When specified, this audio replaces the source video's audio. Cannot be used with `-NoAudio`. When `-Source` is a supported image file, `-Audio` determines the output video duration.
|
||||
- **`-CRF`** (optional): 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
|
||||
- **`-Metadata`**: Copy available JSON metadata from source video file to target video file
|
||||
- **`-Reverse`**: Re-encode the video so it plays backward (last selected frame to first). Honours all other parameters (frame range, `-Size`, `-FPS`, `-CRF`, target container, etc.). Audio is **not** reversed — it is processed exactly as without `-Reverse` (trimmed/replaced/copied as applicable) and plays forward alongside the reversed video. Forces a video re-encode, since stream-copy is incompatible with the `reverse` filter.
|
||||
- **`-LTX`**: Override the dimensions chosen by `-Size` with the closest LTX-2 (Wan2GP by DeepBeepMeep) canonical resolution for the source's aspect ratio. Only valid when `-Size` is one of `HD`, `1080p`, `720p` or `480p`. The script probes the source's width/height, computes its aspect ratio, and picks the bucket whose ratio is numerically closest. Forces a video re-encode (same as `-Size`). The buckets per `-Size` are:
|
||||
- **`HD` / `1080p`**: `1920x1088` (16:9), `1088x1920` (9:16), `1664x960` (21:9), `960x1664` (9:21).
|
||||
- **`720p`**: `1280x704` (16:9), `704x1280` (9:16), `1088x576` (21:9), `576x1088` (9:21).
|
||||
- **`480p`**: `832x448` (16:9), `448x832` (9:16), `896x512` (4:3), `512x896` (3:4).
|
||||
- **`-Crop`**: Obtain the dimensions selected by `-Size` (including `-LTX` buckets) by **center-cropping** the source instead of resizing it, so the original aspect ratio of the picture content is kept (no stretching or scaling). Requires `-Size`. Without `-Crop` the source is resized (default behaviour). Honoured in all modes (video, PNG sequence and image-to-video). Forces a video re-encode (same as `-Size`). See [Crop rules](#crop-rules).
|
||||
|
||||
## Crop rules
|
||||
|
||||
When `-Crop` is present, the requested output size is first computed exactly as without `-Crop` (named presets apply the short-side AR rule, custom `width:height` is used as given, `-LTX` picks its bucket). The source is then cropped around its center instead of being scaled:
|
||||
|
||||
- A dimension is cropped only when the source is **bigger** than the requested size in that dimension.
|
||||
- A dimension where the source is equal or smaller is left untouched — no upscaling or padding is applied, so the output can be smaller than the requested size.
|
||||
- Cropped dimensions are rounded down to an even number (required by `yuv420p`).
|
||||
|
||||
| Source | `-Size` | Without `-Crop` (resize) | With `-Crop` |
|
||||
| --- | --- | --- | --- |
|
||||
| `1080x1080` | `960:1080` | `960x1080` (squeezed horizontally) | `960x1080` (cropped horizontally) |
|
||||
| `1080x1080` | `720:720` | `720x720` (scaled down) | `720x720` (cropped horizontally and vertically) |
|
||||
| `1080x1080` | `1920:720` | `1920x720` (stretched) | `1080x720` (cropped vertically only) |
|
||||
|
||||
## Image-to-Video Mode
|
||||
|
||||
When `-Source` is a supported image file and `-Audio` is provided, the script builds a video of the static image lasting for the duration of the audio file, bypassing all video-probing logic.
|
||||
|
||||
### Supported image formats
|
||||
|
||||
`.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tiff`, `.tif`, `.gif`
|
||||
|
||||
### Defaults vs. overrides
|
||||
|
||||
| Parameter | Default when omitted | Can be overridden? |
|
||||
| --- | --- | --- |
|
||||
| `-FPS` | 24 | Yes — any positive value |
|
||||
| `-CRF` | 8 | Yes — any value 0-51 |
|
||||
| `-Size` | Source image dimensions (even-padded) | Yes — any named or custom size |
|
||||
|
||||
### Dimension rules
|
||||
|
||||
- If `-Size` is a **named preset** (`720p`, `1080p`, etc.) without `-LTX`, the short-side AR rule from video mode applies using the source image's dimensions.
|
||||
- If `-Size` is a **custom** `width:height`, it is applied exactly.
|
||||
- If `-Size` is omitted, the source image's width/height are used directly (padded to an even number if needed).
|
||||
- `-LTX` picks the closest LTX-2 bucket from the source image's aspect ratio. Only valid with `-Size` `HD`, `1080p`, `720p` or `480p`.
|
||||
- `-Crop` center-crops the source image to the computed size instead of resizing it (see [Crop rules](#crop-rules)).
|
||||
|
||||
### Restrictions
|
||||
|
||||
- `-NoAudio` is not allowed; audio is required in this mode.
|
||||
- `-StartFrame`, `-EndFrame`, `-Frames`, `-Sequence`, `-Metadata` and `-Reverse` are ignored.
|
||||
|
||||
### Output container
|
||||
|
||||
The output container is determined by the `-Target` extension (`.mp4`, `.webm`, `.mkv`), using the same codec profiles as for video sources. For `libx264` targets (MP4/MKV), `-tune stillimage` is applied to optimise encoding for static content.
|
||||
|
||||
## PNG Image Sequence Mode
|
||||
|
||||
When `-Source` is a directory the script builds a video from the PNG files found inside, bypassing all video-probing logic. The behaviour is fully driven by the parameters below.
|
||||
|
||||
### Frame selection
|
||||
|
||||
`-StartFrame` and `-EndFrame` are **0-based indices** into the alphabetically-sorted PNG list (not time-codes). The default range covers all PNG files. `-Frames` can be used instead of `-StartFrame`/`-EndFrame` to specify the range using the flexible format described in the [Parameters](#parameters) section.
|
||||
|
||||
### Ordering
|
||||
|
||||
The list is always built in ascending alphabetical order first. `-Reverse` then reverses it **after** the frame range has been applied, so the first frame in the output is `list[EndFrame]` and the last is `list[StartFrame]`.
|
||||
|
||||
### Defaults vs. overrides
|
||||
|
||||
| Parameter | Default when omitted | Can be overridden? |
|
||||
| --- | --- | --- |
|
||||
| `-FPS` | 24 | Yes — any positive value |
|
||||
| `-CRF` | 8 | Yes — any value 0-51 |
|
||||
| `-Size` | First PNG dimensions (even-padded) | Yes — any named or custom size |
|
||||
| Audio | No audio (`-an`) | Yes — provide `-Audio` |
|
||||
|
||||
### Dimension rules
|
||||
|
||||
- If `-Size` is a **named preset** (`720p`, `1080p`, etc.) without `-LTX`, the short-side AR rule from video mode applies using the first PNG's dimensions.
|
||||
- If `-Size` is a **custom** `width:height`, it is applied exactly (as with video mode).
|
||||
- If `-Size` is omitted, the first PNG's width/height are used directly (padded to an even number if needed).
|
||||
- `-LTX` picks the closest LTX-2 bucket from the first PNG's aspect ratio. Only valid with `-Size` `HD`, `1080p`, `720p` or `480p`.
|
||||
- `-Crop` center-crops the PNG frames to the computed size instead of resizing them (see [Crop rules](#crop-rules)).
|
||||
|
||||
### Unsupported parameters
|
||||
|
||||
`-Sequence` and `-Metadata` are not valid in PNG sequence mode. Specifying either produces a terminating error before any file I/O occurs.
|
||||
|
||||
### Output container
|
||||
|
||||
The output container is still determined by the `-Target` extension (`.mp4`, `.webm`, `.mkv`), using the same codec profiles as for video sources.
|
||||
|
||||
## Examples
|
||||
|
||||
**Clone entire video:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4
|
||||
```
|
||||
|
||||
**Clone specific frame range:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -StartFrame 100 -EndFrame 500
|
||||
```
|
||||
|
||||
**Clone frame range using `-Frames`:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -Frames "100-500"
|
||||
```
|
||||
|
||||
**Clone from frame 0 to frame 50:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -Frames "50"
|
||||
```
|
||||
|
||||
**Clone last 10 frames:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -Frames "-10"
|
||||
```
|
||||
|
||||
**Clone from frame 10 to last frame:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -Frames "10-%"
|
||||
```
|
||||
|
||||
**Extract last frame and clone without it:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Sequence last_frame.png
|
||||
# Extracts last frame to last_frame.png
|
||||
# Clones all frames except the last one
|
||||
```
|
||||
|
||||
**Extract specific frame and clone up to it:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -EndFrame 100 -Sequence frame100.png
|
||||
# Extracts frame 100 to frame100.png
|
||||
# Clones frames 0-99 (excluding frame 100)
|
||||
```
|
||||
|
||||
**Clone with resized output:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Size 720p
|
||||
```
|
||||
|
||||
**Clone with custom resolution:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Size 1280:720
|
||||
```
|
||||
|
||||
**Clone with cropped output (no resizing):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source square.mp4 -Target output.mp4 -Size 960:1080 -Crop
|
||||
# A 1080x1080 source is center-cropped horizontally to 960x1080.
|
||||
```
|
||||
|
||||
**Clone with output cropped on both dimensions:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source square.mp4 -Target output.mp4 -Size 720:720 -Crop
|
||||
# A 1080x1080 source is center-cropped horizontally and vertically to 720x720.
|
||||
```
|
||||
|
||||
**Clone with different FPS:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -FPS 24
|
||||
```
|
||||
|
||||
**Clone with resized output and different FPS:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Size 1080p -FPS 30
|
||||
```
|
||||
|
||||
**Clone without audio:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target silent.mp4 -NoAudio
|
||||
```
|
||||
|
||||
**Clone with alternate audio:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Audio music.mp3
|
||||
# Replaces the video's audio with audio from music.mp3
|
||||
```
|
||||
|
||||
**Clone frame range with alternate audio:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip.mp4 -StartFrame 100 -EndFrame 500 -Audio soundtrack.wav
|
||||
# Clones frames 100-500 and uses audio from soundtrack.wav (trimmed to match video duration)
|
||||
```
|
||||
|
||||
**Clone using the closest LTX-2 720p bucket:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target ltx720.mp4 -Size 720p -LTX
|
||||
# A 16:9 source is rescaled to 1280x704, a 9:16 source to 704x1280,
|
||||
# a 21:9 source to 1088x576, a 9:21 source to 576x1088.
|
||||
```
|
||||
|
||||
**Clone using the closest LTX-2 1080p bucket:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source phone.mp4 -Target ltx1080.mp4 -Size 1080p -LTX
|
||||
# A 1080x1920 (9:16) phone clip is rescaled to 1088x1920.
|
||||
```
|
||||
|
||||
**Clone with custom CRF value:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Size 1080p -CRF 18
|
||||
# Uses CRF 18 instead of default CRF 8 or source bitrate
|
||||
```
|
||||
|
||||
**Clone with metadata:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Metadata
|
||||
# Copies all available JSON metadata from source to target
|
||||
```
|
||||
|
||||
**Reverse the video (audio stays forward):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target reversed.mp4 -Reverse
|
||||
# Video frames are played from last to first; audio plays forward as in the source.
|
||||
```
|
||||
|
||||
**Reverse a trimmed segment, rescaled and re-timed:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target clip_rev.mp4 -StartFrame 100 -EndFrame 500 -Size 720p -FPS 30 -Reverse
|
||||
# Selects frames 100-500, scales to 720p, retimes to 30 fps, then reverses the video.
|
||||
# Audio is trimmed to match the same segment but plays forward.
|
||||
```
|
||||
|
||||
**Build video from a still image with audio (image-to-video mode):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source photo.jpg -Target output.mp4 -Audio soundtrack.mp3
|
||||
# Creates a video of the static image for the duration of soundtrack.mp3
|
||||
```
|
||||
|
||||
**Build image-to-video at a specific resolution:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source photo.png -Target output.mp4 -Audio narration.wav -Size 720p
|
||||
```
|
||||
|
||||
**Build image-to-video with LTX-2 resolution:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source photo.jpg -Target ltx.mp4 -Audio narration.wav -Size 1080p -LTX
|
||||
# Picks the closest LTX-2 1080p bucket for the image's aspect ratio
|
||||
```
|
||||
|
||||
**Build video from PNG sequence (all defaults):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target output.mp4
|
||||
# All PNG files in frames\ sorted alphabetically, 24 fps, CRF 8, no audio
|
||||
```
|
||||
|
||||
**Build from a frame range:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target clip.mp4 -StartFrame 10 -EndFrame 49
|
||||
# Uses frame_0010.png through frame_0049.png (0-based indices into the sorted list)
|
||||
```
|
||||
|
||||
**Build reversed sequence:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target reversed.mp4 -Reverse
|
||||
# PNG files sorted descending: last frame plays first
|
||||
```
|
||||
|
||||
**Build reversed range:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target clip_rev.mp4 -StartFrame 10 -EndFrame 49 -Reverse
|
||||
# Frames 10-49 selected first, then reversed: frame_0049.png plays first
|
||||
```
|
||||
|
||||
**Build with custom FPS and CRF:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target output.mp4 -FPS 30 -CRF 18
|
||||
```
|
||||
|
||||
**Build with scaled output:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target output.mp4 -Size 720p
|
||||
# Scales to 720p preserving aspect ratio of first PNG
|
||||
```
|
||||
|
||||
**Build with audio:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target output.mp4 -Audio soundtrack.mp3
|
||||
```
|
||||
|
||||
**Build with LTX-2 canonical resolution:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target ltx.mp4 -Size 720p -LTX
|
||||
# Picks the closest LTX-2 720p bucket for the first PNG's aspect ratio
|
||||
```
|
||||
|
||||
**Build as WebM:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source frames\ -Target output.webm -FPS 24
|
||||
# VP9 + no audio; use -Audio to add a soundtrack
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source "C:\Videos\input.mp4" -Target "D:\Output\result.mp4"
|
||||
```
|
||||
|
||||
**Convert a YouTube/Google DASH download (VP9 + Opus in WebM) to standard H.264 MP4:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source download.webm -Target download.mp4
|
||||
# Video is transcoded from VP9 to H.264 (libx264, yuv420p, slow preset)
|
||||
# Audio is transcoded from Opus to AAC 192k
|
||||
```
|
||||
|
||||
**Convert an MKV source (e.g. yt-dlp default) to MP4:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source movie.mkv -Target movie.mp4
|
||||
```
|
||||
|
||||
**Convert and rescale a VP9 WebM in one step:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source download.webm -Target download.mp4 -Size 720p
|
||||
# Single re-encode pass: VP9 -> H.264 at 720p
|
||||
```
|
||||
|
||||
**Transcode an MP4 to WebM (VP9 + Opus):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target video.webm
|
||||
# Video transcoded H.264 -> VP9 (libvpx-vp9)
|
||||
# Audio transcoded AAC -> Opus (libopus, 160k)
|
||||
```
|
||||
|
||||
**Stream-copy a YouTube WebM into MKV (no re-encoding):**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source download.webm -Target download.mkv
|
||||
# MKV accepts VP9 and Opus directly; both streams are stream-copied.
|
||||
```
|
||||
|
||||
## Output containers
|
||||
|
||||
The output container is selected from the `-Target` file extension. Each container has its own codec profile, stream-copy list, and encoder settings.
|
||||
|
||||
| Container | Target video codec | Target audio codec | Source video codecs stream-copied | Source audio codecs stream-copied |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `.mp4` (default) | `libx264` (yuv420p, preset slow) | `aac` 192k | `h264` | `aac`, `mp3`, `ac3`, `eac3`, `alac` |
|
||||
| `.webm` | `libvpx-vp9` (yuv420p, deadline good, cpu-used 2, row-mt 1) | `libopus` 160k | `vp8`, `vp9`, `av1` | `opus`, `vorbis` |
|
||||
| `.mkv` | `libx264` (yuv420p, preset slow) | `aac` 192k | `h264`, `hevc`, `vp8`, `vp9`, `av1`, `mpeg4` | `aac`, `mp3`, `ac3`, `eac3`, `alac`, `flac`, `opus`, `vorbis` |
|
||||
|
||||
### General rules
|
||||
|
||||
- A source stream that's on the container's stream-copy list is copied without re-encoding when no trimming, resizing, frame rate change, or audio removal is requested.
|
||||
- A source stream that's **not** on the stream-copy list is transcoded to the container's target codec (e.g. VP9 → H.264 when the target is MP4, AAC → Opus when the target is WebM).
|
||||
- Trimming (`-StartFrame`/`-EndFrame` or `-Frames`) always forces an audio re-encode; in that case the audio is encoded with the target container's audio codec regardless of the source.
|
||||
- `-Size` always forces a video re-encode (scaling requires re-encoding). For standard named sizes (`320p`, `384p`, `480p`, `720p`, `900p`, `1080p`/`HD`, `1440p`/`2K`, `2160p`/`4K`) the output preserves the source's aspect ratio — the reference height is the short-side target (landscape: computed width; portrait: computed height). Custom sizes (`width:height`) are used exactly.
|
||||
- `-Crop` replaces the `scale` filter with a centered `crop` filter for the size computed from `-Size`/`-LTX`; it forces a video re-encode like `-Size`.
|
||||
- `-FPS` always forces a video re-encode (frame rate conversion requires re-encoding).
|
||||
- `-NoAudio` always forces a video re-encode (removing the audio stream requires re-muxing the output file).
|
||||
- `-Reverse` always forces a video re-encode (the `reverse` ffmpeg filter is appended at the end of the video filter chain). Audio is never reversed, so it plays forward over the reversed video and follows the same stream-copy/transcode rules it would have without `-Reverse`.
|
||||
- `-LTX` only takes effect when `-Size` is `HD`, `1080p`, `720p` or `480p`. Combining it with any other `-Size` value (or omitting `-Size`) is rejected with an explicit error so the user is never silently downgraded to the regular preset. The chosen bucket replaces the dimensions that `-Size` would normally produce; everything else (re-encode logic, frame range, FPS, CRF, audio handling) behaves exactly as with a plain `-Size`.
|
||||
- Video bitrate priority is the same for every container: `-CRF` (when specified) → source video bitrate (via `-b:v`) → `-crf 14` fallback. For `libvpx-vp9`, `-b:v 0` is automatically added alongside `-crf` so VP9's constant-quality mode is correctly enabled.
|
||||
- The `-Audio` replacement file is also probed; if its codec isn't on the target container's audio stream-copy list, it is transcoded to the container's audio codec.
|
||||
- If `-Target` has no extension (or an unrecognised one), `.mp4` is appended and the MP4 profile is used. This preserves the script's prior single-container behaviour.
|
||||
- The script logs the chosen target container, the detected source codecs, and any external `-Audio` codec, indicating whether each will be transcoded before running ffmpeg.
|
||||
|
||||
### Notes
|
||||
|
||||
- **WebM encoding is slow.** `libvpx-vp9` is much slower than `libx264`; the chosen `cpu-used 2 -row-mt 1` settings strike a reasonable balance, but expect significantly longer rendering times than MP4.
|
||||
- **MKV stream-copy is greedy.** Because MKV accepts almost any codec, MKV output usually involves no re-encoding at all and is the fastest target for re-containerising downloads.
|
||||
|
||||
## Existing Files
|
||||
|
||||
- An existing `-Target` video is **replaced** without a prompt. ffmpeg runs with `-y`, so the old file is lost. Use `-WhatIf` first, or write to a new name.
|
||||
- The same applies to the image-to-video and PNG-sequence modes, and to a PNG that `-Sequence` would overwrite.
|
||||
- The folder that contains `-Target` must already exist. The script stops with an error instead of creating it.
|
||||
- The temporary concat list used for PNG sequences is deleted after the run.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually creating files. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before each state-changing operation.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before creating files:**
|
||||
|
||||
```powershell
|
||||
.\Clone-Video.ps1 -Source video.mp4 -Target output.mp4 -Sequence frame.png -Confirm
|
||||
```
|
||||
@@ -0,0 +1,451 @@
|
||||
# Compare-Videos.ps1
|
||||
|
||||
Combines 2 to 4 video files into a single comparison video using ffmpeg.
|
||||
|
||||
<video controls width="100%"><source src="../../../images/Compare-Videos.mp4" type="video/mp4"></video>
|
||||
|
||||
*Left side: Original video, Right side: Outpainted video using LTX-2.3 and Wan2GP.*
|
||||
|
||||
*See [Create-PillarBox.ps1](Create-PillarBox.md) script for an example with 4 videos.*
|
||||
|
||||
## Synopsis
|
||||
|
||||
Creates side-by-side or grid comparison videos with automatic scaling and padding to preserve aspect ratios. By default, creates 1920x1080 (1K HD) output. Use `-In4K` for 3840x2160 (4K UHD) output. The script detects the orientation of the first video (S1) and applies appropriate scaling for either landscape or portrait mode. Each source video is scaled to fit its cell without stretching, with black letterbox/pillarbox padding applied as needed.
|
||||
|
||||
When exactly two sources already tile the output frame perfectly, they are passed through untouched — see [Exact Fit](#exact-fit).
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-S1`** (required): Path to source video 1
|
||||
- **`-S2`** (required): Path to source video 2
|
||||
- **`-S3`** (optional): Path to source video 3
|
||||
- **`-S4`** (optional): Path to source video 4
|
||||
- **`-Output`** (required): Path to the output video file
|
||||
- **`-NoAudio`**: Exclude audio from output (default: copies audio from S1)
|
||||
- **`-In4K`**: Generate comparison video in 4K UHD (3840x2160) resolution instead of 1K HD (1920x1080)
|
||||
- **`-Clip`**: Removes black padding from the output. Ignored when the two sources are an [exact fit](#exact-fit) for the output frame, since there is no padding to remove. For landscape, only applies when exactly **2** source videos are provided; ignored otherwise. For portrait, applies when **2–4** source videos are provided (all sources must be portrait); for 2–3 videos removes the outer side (left/right) padding, for 4 videos removes the outer top/bottom padding. With one landscape source and one portrait source (2-video only), switches to a **mixed-orientation** layout: the landscape video keeps its native pixel size (or 2× in `-In4K`) and the portrait video is centered in the remaining width with black pillar boxes. See [Clip Mode](#clip-mode) for details.
|
||||
- **`-Sideways`**: Arranges all videos in a single horizontal row. For landscape mode, replaces the default 2x2 grid layout (used for 3 and 4 videos) with a single row. For portrait mode, the layout is the same as the default (already side-by-side). Can be combined with `-Clip` to remove black padding. See [Sideways Mode](#sideways-mode) for details.
|
||||
|
||||
## Exact Fit
|
||||
|
||||
With exactly **2** source videos, when both are already as tall as the output frame and their widths add up to the output width, placing them side by side *is* the target frame. In this case the script applies **no scaling and no padding at all** — the two streams go straight into `hstack`, so neither source is resampled and no quality is lost.
|
||||
|
||||
The condition is checked against the selected output resolution:
|
||||
|
||||
| | Required height (both sources) | Required width (S1 + S2) |
|
||||
| --- | --- | --- |
|
||||
| Default (1K HD) | `1080` | `1920` |
|
||||
| With `-In4K` | `2160` | `3840` |
|
||||
|
||||
The widths do not have to be equal — any split that sums to the output width qualifies.
|
||||
|
||||
### Examples
|
||||
|
||||
| S1 | S2 | Output | Exact fit? |
|
||||
| --- | --- | --- | --- |
|
||||
| `1280x1080` | `640x1080` | `1920x1080` | Yes — 1280 + 640 = 1920, both 1080 tall |
|
||||
| `960x1080` | `960x1080` | `1920x1080` | Yes — equal halves |
|
||||
| `1440x1080` | `480x1080` | `1920x1080` | Yes — uneven split still sums to 1920 |
|
||||
| `2560x2160` + `-In4K` | `1280x2160` | `3840x2160` | Yes — 2560 + 1280 = 3840, both 2160 tall |
|
||||
| `2560x2160` | `1280x2160` | `1920x1080` | No — heights are not 1080 (add `-In4K`) |
|
||||
| `1280x720` | `640x1080` | `1920x1080` | No — S1 is not 1080 tall |
|
||||
| `1280x1080` | `680x1080` | `1920x1080` | No — widths sum to 1960, not 1920 |
|
||||
|
||||
Resulting filter graph (note the absence of `scale` and `pad`):
|
||||
|
||||
```text
|
||||
[0:v][1:v]hstack=inputs=2[outv]
|
||||
```
|
||||
|
||||
The console reports it as:
|
||||
|
||||
```text
|
||||
Exact fit: 1280x1080 + 640x1080 already tile 1920x1080; sources used unscaled.
|
||||
```
|
||||
|
||||
### Interaction with other switches
|
||||
|
||||
- **`-Clip`** is ignored: an exact-fit output contains no padding to remove. A notice is printed.
|
||||
- **`-Sideways`** is ignored: two sources side by side are already a single row. A notice is printed.
|
||||
- Applies to 2 sources only. With 3 or 4 videos the normal grid/row layouts are used.
|
||||
- Orientation detection still runs, but is irrelevant — the exact-fit check takes precedence over both the landscape and portrait layouts. (A pair such as two `960x1080` sources is detected as "portrait", yet is now passed through instead of being squeezed into 608-wide cells.)
|
||||
|
||||
## Orientation Detection
|
||||
|
||||
The script automatically detects the orientation of S1 by comparing its width and height:
|
||||
|
||||
- **Landscape mode:** Width > Height (e.g., 1920x1080)
|
||||
- **Portrait mode:** Height > Width (e.g., 1080x1920)
|
||||
|
||||
All videos are then scaled according to the detected orientation.
|
||||
|
||||
## Layout Rules
|
||||
|
||||
Each source video is scaled to fit its cell without stretching. Aspect ratio is always preserved using `force_original_aspect_ratio=decrease` combined with black padding to fill the cell exactly.
|
||||
|
||||
### Landscape Mode (width > height)
|
||||
|
||||
#### 1K HD (1920x1080) - Default
|
||||
|
||||
**2 videos:** Output is 1920x1080. Each video occupies a 960x1080 cell side-by-side.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
```
|
||||
|
||||
**3 videos:** Output is 1920x1080. Top row has two 960x540 cells (S1 left, S2 right). Bottom row has S3 in a 960x540 cell, horizontally centered with 480px black padding on each side.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
[ S3 ]
|
||||
```
|
||||
|
||||
**4 videos:** Output is 1920x1080. Four 960x540 cells in a 2x2 grid.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
[S3][S4]
|
||||
```
|
||||
|
||||
#### 4K UHD (3840x2160) - With -In4K
|
||||
|
||||
**2 videos:** Output is 3840x2160. Each video occupies a 1920x2160 cell side-by-side.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
```
|
||||
|
||||
**3 videos:** Output is 3840x2160. Top row has two 1920x1080 cells (S1 left, S2 right). Bottom row has S3 in a 1920x1080 cell, horizontally centered with 960px black padding on each side.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
[ S3 ]
|
||||
```
|
||||
|
||||
**4 videos:** Output is 3840x2160. Four 1920x1080 cells in a 2x2 grid.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
[S3][S4]
|
||||
```
|
||||
|
||||
### Portrait Mode (height > width)
|
||||
|
||||
#### 1K HD (1920x1080) - Default
|
||||
|
||||
All videos are displayed horizontally side-by-side, scaled and centered with black padding.
|
||||
|
||||
**2 videos:** Each video is scaled to 608x1080. Combined width is 1216x1080, centered horizontally in 1920x1080 output with 352px padding on each side. With `-Clip`: 1216x1080 (no side padding).
|
||||
|
||||
```text
|
||||
[ ][S1][S2][ ]
|
||||
```
|
||||
|
||||
**3 videos:** Each video is scaled to 608x1080. Combined width is 1824x1080, centered horizontally in 1920x1080 output with 48px padding on each side. With `-Clip`: 1824x1080 (all sources must be portrait; otherwise falls back to padded layout).
|
||||
|
||||
```text
|
||||
[ ][S1][S2][S3][ ]
|
||||
```
|
||||
|
||||
**4 videos:** Each video is scaled to 480x854. Combined width is 1920x854, centered vertically in 1920x1080 output with 113px padding on top and bottom. With `-Clip`: 1920x854 (all sources must be portrait; otherwise falls back to padded layout).
|
||||
|
||||
```text
|
||||
[S1][S2][S3][S4]
|
||||
```
|
||||
|
||||
#### 4K UHD (3840x2160) - With -In4K
|
||||
|
||||
**2 videos:** Each video is scaled to 1216x2160. Combined width is 2432x2160, centered horizontally in 3840x2160 output with 704px padding on each side. With `-Clip`: 2432x2160 (no side padding).
|
||||
|
||||
```text
|
||||
[ ][S1][S2][ ]
|
||||
```
|
||||
|
||||
**3 videos:** Each video is scaled to 1216x2160. Combined width is 3648x2160, centered horizontally in 3840x2160 output with 96px padding on each side. With `-Clip`: 3648x2160 (all sources must be portrait; otherwise falls back to padded layout).
|
||||
|
||||
```text
|
||||
[ ][S1][S2][S3][ ]
|
||||
```
|
||||
|
||||
**4 videos:** Each video is scaled to 960x1708. Combined width is 3840x1708, centered vertically in 3840x2160 output with 226px padding on top and bottom. With `-Clip`: 3840x1708 (all sources must be portrait; otherwise falls back to padded layout).
|
||||
|
||||
```text
|
||||
[S1][S2][S3][S4]
|
||||
```
|
||||
|
||||
## Sideways Mode
|
||||
|
||||
With `-Sideways`, all videos are placed in a single horizontal row regardless of video count. For landscape sources this is the key difference from the default layout, which uses a 2x2 grid for 3 and 4 videos.
|
||||
|
||||
Can be combined with `-Clip` to remove black padding: for landscape, the output height is reduced to the natural content height of the sources; for portrait, the outer centering pad is omitted and the output matches the raw hstack dimensions.
|
||||
|
||||
### Landscape Mode — Sideways
|
||||
|
||||
#### 1K HD (1920x1080)
|
||||
|
||||
**2 videos:** Each cell is 960x1080. Video content scales to 960x540, padded vertically. Total: 1920x1080. With `-Clip`: total 1920x540.
|
||||
|
||||
```text
|
||||
[S1][S2]
|
||||
```
|
||||
|
||||
**3 videos:** Each cell is 640x1080. Video content scales to 640x360, padded vertically. Total: 1920x1080. With `-Clip`: total 1920x360.
|
||||
|
||||
```text
|
||||
[S1][S2][S3]
|
||||
```
|
||||
|
||||
**4 videos:** Each cell is 480x1080. Video content scales to 480x270, padded vertically. Total: 1920x1080. With `-Clip`: total 1920x270.
|
||||
|
||||
```text
|
||||
[S1][S2][S3][S4]
|
||||
```
|
||||
|
||||
#### 4K UHD (3840x2160) — With -In4K
|
||||
|
||||
**2 videos:** Each cell is 1920x2160. Video content scales to 1920x1080, padded vertically. Total: 3840x2160. With `-Clip`: total 3840x1080.
|
||||
|
||||
**3 videos:** Each cell is 1280x2160. Video content scales to 1280x720, padded vertically. Total: 3840x2160. With `-Clip`: total 3840x720.
|
||||
|
||||
**4 videos:** Each cell is 960x2160. Video content scales to 960x540, padded vertically. Total: 3840x2160. With `-Clip`: total 3840x540.
|
||||
|
||||
### Portrait Mode — Sideways
|
||||
|
||||
Portrait videos are already displayed side-by-side by default. The `-Sideways` switch produces the same layout as the standard portrait mode. With `-Clip`, the outer black padding is omitted and the output matches the raw hstack dimensions.
|
||||
|
||||
#### 1K HD (1920x1080)
|
||||
|
||||
**2 videos:** Each cell 608x1080. Output 1920x1080 (352px padding per side). With `-Clip`: 1216x1080.
|
||||
|
||||
**3 videos:** Each cell 608x1080. Output 1920x1080 (48px padding per side). With `-Clip`: 1824x1080.
|
||||
|
||||
**4 videos:** Each cell 480x854. Output 1920x1080 (~113px padding top/bottom). With `-Clip`: 1920x854.
|
||||
|
||||
#### 4K UHD (3840x2160) — With -In4K
|
||||
|
||||
**2 videos:** Each cell 1216x2160. Output 3840x2160 (704px padding per side). With `-Clip`: 2432x2160.
|
||||
|
||||
**3 videos:** Each cell 1216x2160. Output 3840x2160 (96px padding per side). With `-Clip`: 3648x2160.
|
||||
|
||||
**4 videos:** Each cell 960x1706. Output 3840x2160 (~227px padding top/bottom). With `-Clip`: 3840x1706.
|
||||
|
||||
## Clip Mode
|
||||
|
||||
With only 2 source videos, the default behavior pads the output to the full target resolution (1920x1080 or 3840x2160). When the sources are shorter than 16:9 (e.g. two 1280x720 videos placed side-by-side at 1K HD), this leaves significant black bars on the top and bottom. The `-Clip` switch removes those bars. It works in two modes depending on the source orientations.
|
||||
|
||||
### Same-orientation mode (both landscape or both portrait)
|
||||
|
||||
The final output height is reduced to the actual height each source naturally scales to inside its half-width cell, removing the top/bottom letterbox entirely.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. Both `S1` and `S2` are probed for their original `width x height`.
|
||||
2. For each source, the script computes the height it would scale to when fitted by width into its cell (`outputWidth / 2` for landscape, `outputWidth * 0.6333 / 2` for portrait), preserving aspect ratio (`force_original_aspect_ratio=decrease`).
|
||||
3. The larger of the two scaled heights is used as the new output height (rounded up to an even number for `libx264`/`yuv420p`), capped at the original output height.
|
||||
4. All subsequent scaling/padding uses that reduced output height, so neither source is cropped and there is no top/bottom letterbox.
|
||||
|
||||
#### Examples
|
||||
|
||||
| Sources | Default output | With `-Clip` | With `-Clip -In4K` |
|
||||
| --- | --- | --- | --- |
|
||||
| Two `1280x720` (16:9) | `1920x1080` | `1920x540` | `3840x1080` |
|
||||
| Two `1920x1080` (16:9) | `1920x1080` | `1920x1080` (already full height) | `3840x2160` (already full height) |
|
||||
| One `1280x720` + one `1280x960` | `1920x1080` | `1920x720` (max scaled height) | `3840x1440` |
|
||||
|
||||
### Mixed-orientation mode (one landscape + one portrait)
|
||||
|
||||
When one source is landscape and the other is portrait, `-Clip` switches to a layout designed to display the landscape video at its native pixel size while pillar-boxing the portrait into the remaining horizontal space.
|
||||
|
||||
#### How it works
|
||||
|
||||
1. Both `S1` and `S2` are probed; the script detects that one is landscape (width > height) and the other is portrait.
|
||||
2. A `scaleFactor` is derived from the target resolution: `1.0` at 1K HD, `2.0` at 4K UHD (`outputWidth / 1920`).
|
||||
3. The landscape source is rendered at `landscapeNativeWidth * scaleFactor` × `landscapeNativeHeight * scaleFactor`. At 1K HD this is its native pixel size; at 4K UHD it's exactly 2× upscaled, matching the `-In4K` doubling used elsewhere.
|
||||
4. The output height is set to the landscape's scaled height.
|
||||
5. The portrait source is scaled to fit the remaining width (`outputWidth - landscapeScaledWidth`) × output height with `force_original_aspect_ratio=decrease`, then padded with black to fill that cell exactly (pillar-box bars).
|
||||
6. `S1` is placed on the left and `S2` on the right (the script preserves the same S1/S2 ordering as the same-orientation layout, regardless of which one is landscape).
|
||||
7. If the landscape source is already as wide as the target resolution (e.g. a 1080p source at `-In4K` would scale to 3840 wide, leaving no room for the portrait), the script falls back to the same-orientation logic and emits a console warning.
|
||||
|
||||
#### Examples
|
||||
|
||||
| Sources (S1, S2) | `-Clip` output | `-Clip -In4K` output |
|
||||
| --- | --- | --- |
|
||||
| `1280x720` landscape + `720x1280` portrait | `1920x720` (landscape `1280x720` on its side, portrait cell `640x720`) | `3840x1440` (landscape `2560x1440`, portrait cell `1280x1440`) |
|
||||
| `720x1280` portrait + `1280x720` landscape | `1920x720` (portrait cell `640x720` on left as S1, landscape `1280x720` on right as S2) | `3840x1440` (same with `-In4K` doubling) |
|
||||
| `854x480` landscape + `480x854` portrait | `1920x480` (landscape `854x480`, portrait cell `1066x480`) | `3840x960` |
|
||||
| `1920x1080` landscape + `1080x1920` portrait | falls back to same-orientation behaviour (landscape already at target width) | falls back to same-orientation behaviour |
|
||||
|
||||
### Portrait clip mode
|
||||
|
||||
When all source videos are portrait, `-Clip` removes the outer black padding rather than adjusting height:
|
||||
|
||||
- **2 or 3 videos:** The outer side (left/right) padding is omitted. Output width equals the raw hstack width (2× or 3× the cell width).
|
||||
- **4 videos:** The outer top/bottom padding is omitted. Output height equals the natural 9:16 cell height.
|
||||
|
||||
For 3 and 4 videos, S2 (and S3/S4 as needed) are probed to verify all sources are portrait. If any source is not portrait, the full padded layout is used and a warning is emitted.
|
||||
|
||||
| Sources | Default output | With `-Clip` | With `-Clip -In4K` |
|
||||
| --- | --- | --- | --- |
|
||||
| 2 portrait videos | 1920x1080 | 1216x1080 | 2432x2160 |
|
||||
| 3 portrait videos (all portrait) | 1920x1080 | 1824x1080 | 3648x2160 |
|
||||
| 4 portrait videos (all portrait) | 1920x1080 | 1920x854 | 3840x1708 |
|
||||
|
||||
### Restrictions
|
||||
|
||||
- For landscape, only applies when exactly **2** source videos are provided. With 3 or 4 landscape videos, the switch is ignored and a warning is emitted.
|
||||
- For portrait, applies when **2–4** source videos are provided. For 3 and 4 videos, all sources must be portrait; if any is not, the padded layout is used automatically.
|
||||
- Mixed-orientation mode (one landscape + one portrait) requires the landscape source's scaled width to be strictly less than the target width and its scaled height to fit within the target height; otherwise the script falls back to the same-orientation behaviour automatically.
|
||||
- The audio handling, codec, and CRF are unchanged.
|
||||
|
||||
## Examples
|
||||
|
||||
**Exact fit — two videos that already tile 1920x1080 (no scaling, no padding):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 wide_1280x1080.mp4 -S2 narrow_640x1080.mp4 -Output exact.mp4
|
||||
```
|
||||
|
||||
**Exact fit at 4K (2560x2160 + 1280x2160 → 3840x2160):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 wide_2560x2160.mp4 -S2 narrow_1280x2160.mp4 -Output exact_4k.mp4 -In4K
|
||||
```
|
||||
|
||||
**Two portrait videos clipped to remove side padding (1216x1080):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 portrait1.mp4 -S2 portrait2.mp4 -Output portrait_clip.mp4 -Clip
|
||||
```
|
||||
|
||||
**Same pair at 4K (2432x2160):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 portrait1.mp4 -S2 portrait2.mp4 -Output portrait_clip_4k.mp4 -Clip -In4K
|
||||
```
|
||||
|
||||
**Three portrait videos clipped (1824x1080 — all sources must be portrait):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 p1.mp4 -S2 p2.mp4 -S3 p3.mp4 -Output portrait3_clip.mp4 -Clip
|
||||
```
|
||||
|
||||
**Four portrait videos clipped to remove top/bottom padding (1920x854 — all sources must be portrait):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 p1.mp4 -S2 p2.mp4 -S3 p3.mp4 -S4 p4.mp4 -Output portrait4_clip.mp4 -Clip
|
||||
```
|
||||
|
||||
**Compare two videos:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison.mp4
|
||||
```
|
||||
|
||||
**Compare three videos:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 version1.mp4 -S2 version2.mp4 -S3 version3.mp4 -Output comparison.mp4
|
||||
```
|
||||
|
||||
**Compare four videos in grid:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -S4 d.mp4 -Output grid.mp4
|
||||
```
|
||||
|
||||
**Without audio:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 video1.mp4 -S2 video2.mp4 -Output comparison.mp4 -NoAudio
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 "C:\Videos\original.mp4" -S2 "D:\Processed\enhanced.mp4" -Output "E:\Output\comparison.mp4"
|
||||
```
|
||||
|
||||
**Generate 4K UHD comparison:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 original.mp4 -S2 enhanced.mp4 -Output comparison_4k.mp4 -In4K
|
||||
```
|
||||
|
||||
**Two 720p videos clipped to a true 16:9 strip (1920x540):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a_720p.mp4 -S2 b_720p.mp4 -Output side-by-side.mp4 -Clip
|
||||
```
|
||||
|
||||
**Same pair at 4K (3840x1080):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a_720p.mp4 -S2 b_720p.mp4 -Output side-by-side_4k.mp4 -Clip -In4K
|
||||
```
|
||||
|
||||
**Mixed orientation: 720p landscape + 720x1280 portrait (1920x720 output):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 landscape_720p.mp4 -S2 portrait_720x1280.mp4 -Output mixed.mp4 -Clip
|
||||
# Landscape occupies 1280x720 on the left, portrait fits in the right 640x720 cell with pillar boxes.
|
||||
```
|
||||
|
||||
**Same mixed pair at 4K (3840x1440 output):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 landscape_720p.mp4 -S2 portrait_720x1280.mp4 -Output mixed_4k.mp4 -Clip -In4K
|
||||
# Landscape is upscaled to 2560x1440, portrait cell is 1280x1440.
|
||||
```
|
||||
|
||||
**4K comparison with four videos:**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -S4 d.mp4 -Output grid_4k.mp4 -In4K
|
||||
```
|
||||
|
||||
**Three landscape videos in a single row (640x1080 cells):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -Output sideways.mp4 -Sideways
|
||||
```
|
||||
|
||||
**Four landscape videos in a single row at 4K (960x2160 cells):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -S4 d.mp4 -Output sideways_4k.mp4 -Sideways -In4K
|
||||
```
|
||||
|
||||
**Three landscape videos in a single row, clipped to content height (1920x360 output):**
|
||||
|
||||
```powershell
|
||||
.\Compare-Videos.ps1 -S1 a.mp4 -S2 b.mp4 -S3 c.mp4 -Output sideways_clip.mp4 -Sideways -Clip
|
||||
```
|
||||
|
||||
## Encoding Settings
|
||||
|
||||
- **Codec:** libx264
|
||||
- **Preset:** fast
|
||||
- **CRF:** 8 (high quality)
|
||||
- **Pixel Format:** yuv420p
|
||||
- **Audio:** Copied from S1 (unless `-NoAudio` specified)
|
||||
|
||||
## Existing Files
|
||||
|
||||
- An existing `-Output` video is **replaced** without a prompt. ffmpeg runs with `-y`, so the old file is lost. Use `-WhatIf` first, or write to a new name.
|
||||
- The folder that contains `-Output` must already exist. The script stops with an error instead of creating it.
|
||||
- The source videos are only read and are never modified.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffmpeg` and `ffprobe` in system PATH
|
||||
- All source video files and output directory must exist before execution
|
||||
- The script probes S1 to determine orientation (landscape vs. portrait)
|
||||
- All videos are scaled according to the detected orientation of S1
|
||||
- The full ffmpeg command is printed to console before execution
|
||||
- Supports `-WhatIf` for dry-run testing without creating output
|
||||
- Videos with different aspect ratios are automatically letterboxed/pillarboxed with black padding
|
||||
- Source videos are never stretched or distorted
|
||||
- With 2 sources that exactly tile the output frame, no `scale`/`pad` filter is applied at all; see [Exact Fit](#exact-fit)
|
||||
- Portrait mode videos are always displayed side-by-side horizontally, not in a grid
|
||||
- Use `-Sideways` to force all videos into a single horizontal row; this replaces the default 2×2 grid for landscape 3 and 4 videos
|
||||
- `-Sideways` and `-Clip` can be combined: for landscape, the cell height is reduced to the natural content height; for portrait, the outer centering padding is omitted
|
||||
@@ -0,0 +1,179 @@
|
||||
# Create-Clip.ps1
|
||||
|
||||
Creates a clipped video file by calling [Clone-Video.ps1](Clone-Video.md) with an automatically derived output filename. The `-Target` path is generated from the `-Video` filename by inserting the `-Suffix` value (default `-Clip`) before the file extension — for example, `P01.mp4` becomes `P01-Clip.mp4` saved alongside the source. Both `-Video` and `-Frames` are required; any additional parameters are forwarded directly to `Clone-Video.ps1`.
|
||||
|
||||
Sources carrying a `.skip` marker keep that marker last: `P01.skip.mp4` becomes `P01-Clip.skip.mp4`, so the clip remains excluded from the scripts that ignore `.skip` files.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to the input video file. Can be absolute or relative to current directory. Accepted containers: `.mp4`, `.webm`, `.mkv`. If the path has no extension the script tries `.mp4`, `.webm`, then `.mkv` in turn and uses the first match to determine the target filename extension.
|
||||
- **`-Frames`** (required): 0-based frame range to clip. Supported formats:
|
||||
- `start-end`: Clip frames from `start` to `end` (e.g., `"100-200"`). Both ends must lie inside the video, and `start` must not be greater than `end`. Otherwise the script stops with exit code `1`.
|
||||
- `positive_number`: Clip frames from 0 to that number (e.g., `"50"`). A single number past the last frame is clamped to the last frame (`"150"` on a 100-frame video gives frames 0 to 99).
|
||||
- `-N`: Clip the last N frames (e.g., `"-10"` clips frames 90 to 99 of a 100-frame video; `"-1"` clips the last frame only)
|
||||
- `%`: The last frame index. Alone (`"%"`) it selects only the last frame. In a range (e.g., `"10-%"`) it is the end of the range
|
||||
- `-%` is not supported and is rejected with exit code `1`
|
||||
- `all`: Clip all frames (same as `"0-%"`)
|
||||
- **`-Suffix`** (optional): Text inserted into the derived target name before the file extension. Defaults to `-Clip`, so `P01.mp4` becomes `P01-Clip.mp4`. Include any separator you want, such as the leading `-`. Must not be empty, because an empty suffix would produce a target with the same name as the source. For example, `-Suffix "-Clip-Shot-1"` turns `P01.mp4` into `P01-Clip-Shot-1.mp4`.
|
||||
- **Extra parameters**: Any additional parameters accepted by `Clone-Video.ps1` (e.g. `-Size`, `-FPS`, `-NoAudio`, `-Audio`, `-CRF`, `-Metadata`, `-Reverse`, `-LTX`) are forwarded directly. The parameters `-Video`, `-Frames`, `-Suffix` and `-Target` are handled by this script and must not be repeated.
|
||||
|
||||
Refer to [Clone-Video.ps1](Clone-Video.md) for a full description of all supported parameters.
|
||||
|
||||
## Target Filename Derivation
|
||||
|
||||
The output filename is derived automatically from the resolved source path:
|
||||
|
||||
| Source | Target |
|
||||
| --- | --- |
|
||||
| `P01.mp4` | `P01-Clip.mp4` |
|
||||
| `render/shot.webm` | `render/shot-Clip.webm` |
|
||||
| `C:\Videos\take01.mkv` | `C:\Videos\take01-Clip.mkv` |
|
||||
| `P01` (no extension, resolves to `P01.mp4`) | `P01-Clip.mp4` |
|
||||
| `P01.skip.mp4` | `P01-Clip.skip.mp4` |
|
||||
| `P04.skip.webm` | `P04-Clip.skip.webm` |
|
||||
| `P01.skip` (no extension, resolves to `P01.skip.mp4`) | `P01-Clip.skip.mp4` |
|
||||
| `P01.mp4` with `-Suffix "-Shot-1"` | `P01-Shot-1.mp4` |
|
||||
| `P01.mp4` with `-Suffix "-Clip-Shot-1"` | `P01-Clip-Shot-1.mp4` |
|
||||
| `P01.skip.mp4` with `-Suffix "-Clip-Shot-1"` | `P01-Clip-Shot-1.skip.mp4` |
|
||||
|
||||
### `.skip` sources
|
||||
|
||||
Several scripts in this collection ignore files whose name ends with `.skip.<ext>` (for example [Crop-Clips.ps1](Crop-Clips.md)). Inserting `-Clip` before the extension would produce `P01.skip-Clip.mp4`, which loses that meaning, so the marker is kept immediately before the container extension instead:
|
||||
|
||||
| Source | Target | |
|
||||
| --- | --- | --- |
|
||||
| `P01.skip.mp4` | `P01-Clip.skip.mp4` | correct — still a `.skip` file |
|
||||
| `P01.skip.mp4` | `P01.skip-Clip.mp4` | not used — marker no longer last |
|
||||
|
||||
This applies to every accepted container (`.mp4`, `.webm`, `.mkv`), matching is case-insensitive, and the marker's original casing is preserved (`P03.SKIP.mkv` → `P03-Clip.SKIP.mkv`).
|
||||
|
||||
## Examples
|
||||
|
||||
**Clip a specific frame range:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200"
|
||||
# Creates: P01-Clip.mp4 (frames 100 to 200)
|
||||
```
|
||||
|
||||
**Clip from frame 0 to frame 50:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "50"
|
||||
# Creates: P01-Clip.mp4 (frames 0 to 50)
|
||||
```
|
||||
|
||||
**Clip last 10 frames:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "-10"
|
||||
# Creates: P01-Clip.mp4 (last 10 frames)
|
||||
```
|
||||
|
||||
**Clip from frame 10 to last frame:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "10-%"
|
||||
# Creates: P01-Clip.mp4 (frames 10 to end)
|
||||
```
|
||||
|
||||
**Clip with resized output:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -Size 720p
|
||||
# Creates: P01-Clip.mp4 (frames 100-200 scaled to 720p)
|
||||
```
|
||||
|
||||
**Clip without audio:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -NoAudio
|
||||
```
|
||||
|
||||
**Clip with alternate audio:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "50-150" -Audio music.mp3
|
||||
```
|
||||
|
||||
**Clip with custom CRF:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-500" -CRF 18
|
||||
# Uses CRF 18 instead of default CRF 8 or source bitrate
|
||||
```
|
||||
|
||||
**Clip a reversed segment:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -Reverse
|
||||
# Creates: P01-Clip.mp4 playing frames 200 down to 100
|
||||
```
|
||||
|
||||
**Clip using the closest LTX-2 720p bucket:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -Size 720p -LTX
|
||||
```
|
||||
|
||||
**Source without extension:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01 -Frames "100-200"
|
||||
# Resolves P01 -> P01.mp4 (or .webm / .mkv, whichever exists first)
|
||||
# Creates: P01-Clip.mp4
|
||||
```
|
||||
|
||||
**Replace the default `-Clip` suffix:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -Suffix "-Clip-Shot-1"
|
||||
# Creates: P01-Clip-Shot-1.mp4 (frames 100 to 200)
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video "C:\Videos\P01.mp4" -Frames "100-200"
|
||||
# Creates: C:\Videos\P01-Clip.mp4
|
||||
```
|
||||
|
||||
**Clip a `.skip` source:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.skip.mp4 -Frames "100-200"
|
||||
# Creates: P01-Clip.skip.mp4 (the .skip marker stays before the extension)
|
||||
```
|
||||
|
||||
**Clip a `.skip` source with extra parameters:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.skip.mp4 -Frames "5-10" -Size 480p
|
||||
# Creates: P01-Clip.skip.mp4 scaled to 480p
|
||||
```
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing derived clip is **replaced** without a prompt. The clip is created by [Clone-Video.ps1](Clone-Video.md), which runs ffmpeg with `-y` and overwrites its target.
|
||||
|
||||
- The `.skip` marker stays last in the name that is replaced. For `P01.skip.mp4` the file replaced is `P01-Clip.skip.mp4`, or `P01-Clip<suffix>.skip.mp4` with `-Suffix`.
|
||||
- Use `-WhatIf` first, or use a different `-Suffix`.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution (forwarded to `Clone-Video.ps1`):
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without creating files. Shows what `Clone-Video.ps1` would do.
|
||||
- **`-Confirm`**: Prompt for confirmation before each state-changing operation.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before creating files:**
|
||||
|
||||
```powershell
|
||||
.\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -Confirm
|
||||
```
|
||||
@@ -0,0 +1,107 @@
|
||||
# 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.
|
||||
|
||||
## Synopsis
|
||||
|
||||
This script:
|
||||
|
||||
1. Extracts the video duration using ffprobe
|
||||
2. Parses the first subtitle file to find the last subtitle number
|
||||
3. Adjusts the second subtitle file's timing by adding the video duration
|
||||
4. Renumbers the second subtitle entries to continue from the first file
|
||||
5. Combines both into a single output SRT file
|
||||
|
||||
This is useful when you have two video parts with separate subtitle files and want to create a single combined subtitle file for the full video.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to the video file (used to determine duration for offset)
|
||||
- **`-Subtitle1`** (required): Path to the first subtitle file (SRT format)
|
||||
- **`-Subtitle2`** (required): Path to the second subtitle file (SRT format)
|
||||
- **`-Output`** (required): Path to the output combined subtitle file (SRT format)
|
||||
|
||||
## How It Works
|
||||
|
||||
**Timing Adjustment:**
|
||||
|
||||
The second subtitle file is assumed to start at 00:00:00,000 for its own video. The script adds the first video's duration to all timestamps in the second subtitle file.
|
||||
|
||||
Example: If the first video is exactly 3 minutes (00:03:00,000), then a subtitle entry from the second file at 00:00:05,000 becomes 00:03:05,000 in the output.
|
||||
|
||||
**Numbering:**
|
||||
|
||||
Subtitle entries are renumbered sequentially. If the first file ends with subtitle #45, the second file's entries start at #46.
|
||||
|
||||
**Format:**
|
||||
|
||||
All files use SRT (SubRip Subtitle) format:
|
||||
|
||||
```text
|
||||
1
|
||||
00:00:01,000 --> 00:00:04,000
|
||||
First subtitle text
|
||||
|
||||
2
|
||||
00:00:05,000 --> 00:00:08,000
|
||||
Second subtitle text
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Basic usage:**
|
||||
|
||||
```powershell
|
||||
.\Create-FullSubtitle.ps1 -Video "part1.mp4" -Subtitle1 "part1.srt" -Subtitle2 "part2.srt" -Output "combined.srt"
|
||||
```
|
||||
|
||||
**With absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Create-FullSubtitle.ps1 `
|
||||
-Video "C:\Videos\part1.mp4" `
|
||||
-Subtitle1 "C:\Subtitles\part1.srt" `
|
||||
-Subtitle2 "C:\Subtitles\part2.srt" `
|
||||
-Output "D:\Output\full.srt"
|
||||
```
|
||||
|
||||
**Combine multiple parts:**
|
||||
|
||||
```powershell
|
||||
# First combine part1 and part2
|
||||
.\Create-FullSubtitle.ps1 -Video "part1.mp4" -Subtitle1 "part1.srt" -Subtitle2 "part2.srt" -Output "temp.srt"
|
||||
|
||||
# Then combine result with part3
|
||||
.\Create-FullSubtitle.ps1 -Video "combined_1_2.mp4" -Subtitle1 "temp.srt" -Subtitle2 "part3.srt" -Output "final.srt"
|
||||
```
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually creating the output file. Shows what would be combined.
|
||||
- **`-Confirm`**: Prompt for confirmation before creating the output directory and writing the output file.
|
||||
|
||||
**Simulate without creating files:**
|
||||
|
||||
```powershell
|
||||
.\Create-FullSubtitle.ps1 -Video "video.mp4" -Subtitle1 "sub1.srt" -Subtitle2 "sub2.srt" -Output "out.srt" -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before writing:**
|
||||
|
||||
```powershell
|
||||
.\Create-FullSubtitle.ps1 -Video "video.mp4" -Subtitle1 "sub1.srt" -Subtitle2 "sub2.srt" -Output "out.srt" -Confirm
|
||||
```
|
||||
|
||||
## Existing Output
|
||||
|
||||
If the file given with `-Output` already exists, it is **overwritten** without a prompt. The folder is created when it does not exist. With `-WhatIf`, nothing is written.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffprobe` in system PATH to extract video duration
|
||||
- All subtitle files must be in SRT format
|
||||
- Output file is written with UTF-8 encoding
|
||||
- The output directory is created if it does not exist
|
||||
- Subtitle timing is precise to the millisecond
|
||||
@@ -0,0 +1,261 @@
|
||||
# Create-PillarBox.ps1
|
||||
|
||||
Creates a pillar-boxed (16:9) video file from a source video using ffmpeg, with selectable side-bar effects ranging from solid colors to blurred backgrounds derived from the video itself.
|
||||
|
||||
<video controls width="100%"><source src="../../../images/Create-PillarBox.mp4" type="video/mp4"></video>
|
||||
|
||||
*The video above was generated using the [Compare-Videos.ps1](Compare-Videos.md) script.*
|
||||
|
||||
Each video shows the same source material with a different effect as follows:
|
||||
|
||||
| Left side | Right side |
|
||||
| :-------: | :--------: |
|
||||
| black | standard |
|
||||
| gaussian | dark |
|
||||
|
||||
## Synopsis
|
||||
|
||||
Wraps the source video in a 16:9 frame so it can be played on standard widescreen displays without stretching:
|
||||
|
||||
- **Portrait-like sources** (e.g. 9:16, 1:1) get **vertical** bars on the left and right (true pillar-boxing).
|
||||
- **Sources wider than 16:9** get **horizontal** bars on the top and bottom (letter-boxing).
|
||||
- **Square (1:1) sources with `-Vertical`** are wrapped in a **9:16 portrait** frame instead, with **horizontal** bars on the top and bottom — for vertical platforms such as Shorts, Reels, or TikTok.
|
||||
- The source video is scaled to fit the chosen output frame while preserving its aspect ratio, then centered.
|
||||
- The bars themselves can be solid black/white or filled with a stylised, blurred copy of the video.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file (`.mp4` extension added if missing)
|
||||
- **`-Target`** (optional): Path to output video file (`.mp4` extension added if missing). If omitted, defaults to the source filename with the `-Effect` value (lowercased) appended after a dash and saved alongside the source — e.g. `video.mp4` + `-Effect Black` produces `video-black.mp4` next to the source. When `-Vertical` is used, `-vertical` is appended after the effect, just before the extension — e.g. `video-black-vertical.mp4`.
|
||||
- **`-Effect`** (required): Visual treatment of the side bars. One of `black`, `white`, `silver`, `gray`, `charcoal`, `standard`, `dark`, `gaussian`, or `color:RRGGBB` for an arbitrary hex color (case-insensitive on both the keyword and the hex value). See [Effects](#effects) for a detailed description.
|
||||
- **`-Vertical`**: Only valid when the source video is square (1:1). Produces a **9:16 portrait** output with the bars on the top and bottom instead of the default 16:9 landscape output with bars on the sides. All other parameters (`-Effect`, `-Size`, `-FPS`, `-Audio`, `-NoAudio`, `-CRF`) behave identically, just applied to a portrait frame. When `-Target` is omitted, the auto-generated filename ends with `-vertical` (e.g. `video-black-vertical.mp4`). Using it with a non-square source throws an error.
|
||||
- **`-Size`** (optional): Output video size. Possible values: `480p`, `720p`, `1080p`, `HD`, `1440p`, `2K`, `2160p`, `4K`, or custom `width:height`. If not specified, the smallest standard 16:9 size that fully contains the source is chosen automatically. With `-Vertical`, named presets are transposed to portrait (`1080p` → `1080x1920`, `4K` → `2160x3840`); a custom `width:height` is always used exactly as given.
|
||||
- **`-FPS`** (optional): Frames per second for the output video. Uses the input video's FPS if not specified.
|
||||
- **`-NoAudio`**: Exclude audio from the output video. Cannot be combined with `-Audio`.
|
||||
- **`-Audio`** (optional): Path to an alternate audio file to use for the target video. When specified, this audio replaces the source video's audio (re-encoded as AAC 192k, trimmed to the shortest stream).
|
||||
- **`-CRF`** (optional): Constant Rate Factor for video encoding (`0`-`51`, lower is better quality). When specified, overrides both the source video bitrate and the default `-crf 8` fallback.
|
||||
|
||||
## Effects
|
||||
|
||||
The `-Effect` parameter controls what fills the area around the source video inside the 16:9 frame. The eight effects fall into two groups:
|
||||
|
||||
### Solid colors
|
||||
|
||||
These produce flat, uniform bars. They are the cheapest to encode and are best when you want the viewer's eye to ignore the bars completely.
|
||||
|
||||
- **`black`** — Solid black bars (`#000000`). Standard television "matted" look. Maximum contrast against the picture; effectively invisible on OLED/dark-room playback.
|
||||
- **`white`** — Solid white bars (`#FFFFFF`). High-key look suitable for product/light backgrounds, slideshows, and graphic-design contexts.
|
||||
- **`silver`** — Solid silver bars (`#C0C0C0`). Light neutral gray; softer than `white` while staying bright. Useful when pure white feels too aggressive against the picture.
|
||||
- **`gray`** — Solid mid gray bars (`#808080`). 50% luminance neutral; sits between dark and bright content without leaning either way. A safe default for mixed/unknown content brightness.
|
||||
- **`charcoal`** — Solid charcoal bars (`#36454F`). Very dark, slightly blue-tinted gray. Almost as unobtrusive as `black` on OLED but with a more refined look in lit rooms or on LCD screens where pure black can look gray anyway.
|
||||
- **`color:RRGGBB`** — Solid bars in any hex color you specify. The six hex digits are case-insensitive, so `color:FFFF00`, `color:ffff00`, and `color:FfFf00` all produce the same bright yellow bars. Examples: `color:FFFF00` (yellow), `color:1E90FF` (dodger blue), `color:8B0000` (dark red). Use this when none of the named presets match your branding or scene palette.
|
||||
|
||||
### Blurred-background ("ambient") effects
|
||||
|
||||
These three effects all use the same fundamental technique: the source video is duplicated; one copy is **scaled to *fill* the 16:9 frame** (so the picture overflows the visible area in one dimension and is cropped) and then **blurred** to become the background. The original, un-blurred picture is then **centered on top** at its true aspect ratio. The result is that the bars are not empty space — they are a soft, motion-synchronised continuation of the video, similar to the "ambient backlight" treatment used on Apple TV and YouTube.
|
||||
|
||||
The three effects differ only in **how the background is treated** before the original picture is laid on top:
|
||||
|
||||
- **`standard`** — *Soft, neutral, slightly zoomed.*
|
||||
- **Scaling**: the background copy is scaled to roughly **110%** of the output frame and then cropped, giving the bars a subtle "pushed beyond the edges" feel rather than a flat fit. This avoids hard edges where the cropped border would otherwise sit exactly at the frame edge, and adds a barely-noticeable parallax depth.
|
||||
- **Blur**: a fast **box blur** of moderate radius. Box blur is computationally cheap; visually the background becomes recognisable smears of color and motion, but with slight box-shaped artefacts on sharp edges if you look closely.
|
||||
- **Brightness/color**: unchanged. The bars therefore retain the source's full brightness and saturation.
|
||||
- **What you'll see**: side bars filled with soft, recognisable shapes from the picture — sky color, skin tones, scenery — that move and shift in sync with the video. The center picture pops because it is sharp against a softer surround, but the overall image stays bright.
|
||||
- **Best for**: most general-purpose playback. Music videos, vlogs, product demos.
|
||||
|
||||
- **`dark`** — *Same blur as `standard`, but dimmed.*
|
||||
- **Scaling**: background fills the frame exactly (no extra zoom).
|
||||
- **Blur**: same box blur as `standard`.
|
||||
- **Brightness/color**: brightness is reduced (~ -25%) and saturation is reduced to ~80%, producing a darker, more muted background. The center video is **not** affected — only the bars.
|
||||
- **What you'll see**: side bars in the same shapes and motion as `standard`, but visibly darker and less colorful, so the eye is drawn more strongly to the original picture in the middle. Good for cinematic or moody content where the bars should "sink into" the screen.
|
||||
- **Best for**: film clips, dramatic content, dark-room viewing, or any time `standard` feels too bright/distracting.
|
||||
|
||||
- **`gaussian`** — *Smooth, premium-looking blur.*
|
||||
- **Scaling**: background fills the frame exactly (no extra zoom).
|
||||
- **Blur**: a **Gaussian blur** with a large sigma (~30). Gaussian blur is much more computationally expensive than box blur, but it produces a smoother fall-off with no visible artefacts at any zoom level. Sharp features in the original picture become unrecognisable color clouds.
|
||||
- **Brightness/color**: unchanged.
|
||||
- **What you'll see**: side bars that look like a smooth, abstract color wash that clearly comes from the video but where you can no longer make out shapes — only ambient color. Closest to the "broadcast TV" pillar-box look used by streaming services.
|
||||
- **Best for**: professional/broadcast deliverables, finished cuts where rendering time is acceptable, and any content where the `standard` blur looks too "blocky".
|
||||
|
||||
### Quick comparison
|
||||
|
||||
| Effect | Background source | Blur type | Brightness | Use when… |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `black` | none (solid `#000000`) | — | — | You want the bars to disappear visually. |
|
||||
| `white` | none (solid `#FFFFFF`) | — | — | You want a bright, graphic-design feel. |
|
||||
| `silver` | none (solid `#C0C0C0`) | — | — | You want a light bar that's softer than pure white. |
|
||||
| `gray` | none (solid `#808080`) | — | — | Neutral 50% gray that doesn't lean light or dark. |
|
||||
| `charcoal` | none (solid `#36454F`) | — | — | A refined near-black that holds up on LCD/lit rooms. |
|
||||
| `color:RRGGBB` | none (solid custom hex, case-insensitive) | — | — | You need an arbitrary brand or accent color, e.g. `color:FFFF00`. |
|
||||
| `standard` | video, ~110% zoom | box blur (fast) | normal | General use; you want context in the bars. |
|
||||
| `dark` | video | box blur (fast) | dimmed & desaturated | Cinematic look; you want focus on the picture. |
|
||||
| `gaussian` | video | Gaussian (slow, smooth) | normal | Premium/broadcast look; rendering time is OK. |
|
||||
|
||||
## Size selection
|
||||
|
||||
When `-Size` is omitted, the script picks the **smallest standard 16:9 preset that holds the source's bounding box** — i.e. the preset's long side must be at least the source's long side **and** its short side at least the source's short side:
|
||||
|
||||
- A 1080×1920 portrait source → `1920x1080` (1080p) — vertical bars on either side.
|
||||
- A 720×1280 portrait source → `1280x720` (720p) — vertical bars on either side.
|
||||
- A 1080×1080 square source → `1920x1080` (1080p) — vertical bars on either side, source kept at its native 1080×1080.
|
||||
- A 512×512 square source → `1280x720` (720p) — the 480p frame (854×480) is too short to hold 512 px.
|
||||
- A 2560×1080 ultra-wide source → `2560x1440` (1440p) — horizontal bars top and bottom.
|
||||
- A source too large for any preset → an even-rounded custom 16:9 frame that holds the source's bounding box (e.g. 4000×4000 → `7112x4002`).
|
||||
|
||||
Checking both sides matters most for square-ish sources: a 1080×1080 source would fit *inside* a 1280×720 frame only by being shrunk to 720×720, so the next preset up is used instead and no resolution is lost.
|
||||
|
||||
You can always force a specific size with `-Size 1080p`, `-Size 4K`, or a custom `-Size 1920:1080`.
|
||||
|
||||
### With `-Vertical` (square sources)
|
||||
|
||||
The same logic applies to a transposed 9:16 frame:
|
||||
|
||||
- A 1080×1080 square source → `1080x1920` (1080p transposed) — horizontal bars top and bottom, source kept at 1080×1080.
|
||||
- A 512×512 square source → `720x1280` (720p transposed) — horizontal bars top and bottom.
|
||||
- `-Size 1080p` → `1080x1920`, `-Size 720p` → `720x1280`, `-Size 4K` → `2160x3840`.
|
||||
- A custom `-Size 1080:1920` is used verbatim (no transposition).
|
||||
|
||||
## Examples
|
||||
|
||||
**Auto-named output (no `-Target` specified):**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Effect black
|
||||
# Produces video-black.mp4 next to video.mp4
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video vlog.mp4 -Effect Gaussian
|
||||
# Produces vlog-gaussian.mp4 (effect name is always lowercased)
|
||||
```
|
||||
|
||||
**Pillar-box a portrait video with black bars at 1080p:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video portrait.mp4 -Target portrait_16x9.mp4 -Effect black -Size 1080p
|
||||
```
|
||||
|
||||
**Turn a square video into a 9:16 vertical video (bars on top and bottom):**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video square.mp4 -Effect black -Vertical -Size 1080p
|
||||
# Produces square-black-vertical.mp4 at 1080x1920 with black bars above and below the picture
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video square.mp4 -Target reel.mp4 -Effect gaussian -Vertical
|
||||
# Blurred ambient bars on top and bottom; size derived from the source (1080x1080 -> 1080x1920)
|
||||
```
|
||||
|
||||
**Auto-size, white bars:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video clip.mp4 -Target clip_white.mp4 -Effect white
|
||||
# Output size derived from source dimensions
|
||||
```
|
||||
|
||||
**Custom yellow brand color (auto-named output):**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video clip.mp4 -Effect color:FFFF00
|
||||
# Produces clip-color-ffff00.mp4 with solid yellow side bars.
|
||||
# The hex value is case-insensitive: -Effect color:ffff00 works identically.
|
||||
```
|
||||
|
||||
**Soft ambient blur background (general purpose):**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video vlog.mp4 -Target vlog_16x9.mp4 -Effect standard -Size 1080p
|
||||
```
|
||||
|
||||
**Cinematic darkened blur:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video scene.mp4 -Target scene_16x9.mp4 -Effect dark -Size 1080p
|
||||
```
|
||||
|
||||
**Broadcast-quality Gaussian blur:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video music_video.mp4 -Target broadcast.mp4 -Effect gaussian -Size 1080p
|
||||
```
|
||||
|
||||
**Force 4K output:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video clip.mp4 -Target clip_4k.mp4 -Effect gaussian -Size 4K
|
||||
```
|
||||
|
||||
**Custom resolution:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video clip.mp4 -Target clip.mp4 -Effect black -Size 1920:1080
|
||||
```
|
||||
|
||||
**Replace audio with a soundtrack:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target output.mp4 -Effect standard -Audio soundtrack.mp3
|
||||
# Source audio is dropped; soundtrack.mp3 replaces it (AAC 192k, trimmed to shortest stream)
|
||||
```
|
||||
|
||||
**Strip audio entirely:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target silent.mp4 -Effect black -NoAudio
|
||||
```
|
||||
|
||||
**Force a specific FPS:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target output.mp4 -Effect standard -FPS 30
|
||||
```
|
||||
|
||||
**Override quality with CRF:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target output.mp4 -Effect gaussian -CRF 18
|
||||
# CRF 18 instead of default CRF 8 / source bitrate
|
||||
```
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing `-Target` video is **replaced** without a prompt. ffmpeg runs with `-y`.
|
||||
|
||||
- If ffmpeg fails, the target file is removed and the script stops with an error. This also removes a target that existed before the run, so the old file is lost. A partial file is not left behind.
|
||||
- Use `-WhatIf` first, or write to a new name.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without creating files. Prints the full ffmpeg command that would have been run.
|
||||
- **`-Confirm`**: Prompt for confirmation before each state-changing operation.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target output.mp4 -Effect standard -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before creating files:**
|
||||
|
||||
```powershell
|
||||
.\Create-PillarBox.ps1 -Video video.mp4 -Target output.mp4 -Effect gaussian -Confirm
|
||||
```
|
||||
|
||||
## Quality Preservation
|
||||
|
||||
- **Codec**: output is encoded with `libx264` (H.264) using `yuv420p` and the `slow` preset.
|
||||
- **Bitrate**: priority is `-CRF` (when specified) → source video bitrate (via `-b:v`) → fallback `-crf 8` (near-lossless).
|
||||
- **Audio**: when present and not replaced/excluded, audio is **stream-copied** with no quality loss. When `-Audio` is used, the replacement audio is re-encoded to AAC 192k.
|
||||
- **Aspect ratio**: source content is never stretched — only scaled proportionally and padded.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffmpeg` and `ffprobe` on the Windows `PATH`.
|
||||
- Output dimensions are forced to even pixels for `yuv420p` compatibility.
|
||||
- The `gaussian` effect is noticeably slower to encode than `standard`/`dark`; expect a real-time-or-slower rendering speed at 1080p on typical CPUs.
|
||||
- The `standard` and `dark` effects use box blur for speed; if you see visible block-like edges in the bars, switch to `gaussian` for a smoother result.
|
||||
- When the source aspect ratio is already 16:9, all five effects produce a visually identical output (the source fills the frame; the bars have zero size).
|
||||
- `-Vertical` is rejected for non-square sources: a 16:9 or 9:16 source already has an unambiguous bar orientation, so the switch would only be a source of confusion.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Create-Scenes.ps1
|
||||
|
||||
Detects scene changes in a video with ffmpeg and extracts each scene as a separate clip file. Each scene is created by calling [Create-Clip.ps1](Create-Clip.md) with the scene's frame range, which in turn calls [Clone-Video.ps1](Clone-Video.md).
|
||||
|
||||
The clips are saved next to the source video, named from the source filename and the scene number: `P01.mp4` produces `P01-Scene-01.mp4`, `P01-Scene-02.mp4`, and so on. A `.skip` marker stays last: `P01.skip.mp4` produces `P01-Scene-01.skip.mp4`.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to the input video file. Can be absolute or relative to current directory. If the file does not exist, the script tries appending `.mp4`, `.webm` and `.mkv` in turn, so `P01` resolves to `P01.mp4` when that file exists.
|
||||
- **`-Level`** (optional): Scene-change threshold between `0` and `1.0`, rounded to 2 decimals. Lower values are more sensitive and produce more scenes. Default: `0.3`.
|
||||
- **`-Scenes`** (optional): Target number of scenes, a positive integer. The level is searched automatically, starting from `-Level`, until exactly this many scenes are detected. Every attempt is displayed. If the number cannot be reached, the script stops with exit code `1` before any file is deleted or created. See [Automatic Level Search](#automatic-level-search).
|
||||
- **`-Suffix`** (optional): Suffix for the clip filenames. Must contain one printf-style number placeholder, which is replaced by the scene number starting at 1. Default: `-Scene-%02d`. See [Suffix Placeholders](#suffix-placeholders).
|
||||
- **`-List`** (optional): Only displays the scene list. No clip is created and no existing file is deleted.
|
||||
- **Extra parameters**: Any parameter accepted by `Create-Clip.ps1` and `Clone-Video.ps1` (e.g. `-Size`, `-FPS`, `-NoAudio`, `-Audio`, `-CRF`, `-Metadata`, `-Reverse`, `-LTX`) is forwarded to every clip. The parameters `-Video`, `-Frames`, `-Suffix`, `-Target`, `-Level` and `-Scenes` are managed by this script and must not be repeated. `-Level` and `-Scenes` are never forwarded to `Create-Clip.ps1`.
|
||||
|
||||
Refer to [Clone-Video.ps1](Clone-Video.md) for a full description of the encoding parameters.
|
||||
|
||||
## Requirements
|
||||
|
||||
- `ffmpeg` and `ffprobe` must be in the `PATH`.
|
||||
- `Create-Clip.ps1` must be in the same directory as `Create-Scenes.ps1`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Detection**: ffmpeg runs the `scene` score filter with the `-Level` threshold and reports the timestamp of each detected change.
|
||||
2. **Frame conversion**: each timestamp becomes a 0-based frame index using the video's frame rate and start time. Indices at or before frame 0, or at or after the last frame, are ignored.
|
||||
3. **Scene list**: scene 1 starts at frame 0, and each detected change starts a new scene. Each scene ends one frame before the next change, and the last scene ends at the last frame. With no detected change, the whole video is one scene.
|
||||
4. **Scene list display**: the list is always printed, including with `-List`, `-WhatIf` and `-Confirm`.
|
||||
5. **Cleanup**: unless `-List` is given, files from a previous run are deleted (see [Replacing Existing Clips](#replacing-existing-clips)).
|
||||
6. **Clip creation**: one `Create-Clip.ps1` call per scene, with frames `start-end` and the scene suffix.
|
||||
|
||||
Example scene list for a video with two hard cuts at frames 30 and 60:
|
||||
|
||||
```text
|
||||
Index StartFrame EndFrame Frames StartTime EndTime TotalTime Suffix
|
||||
----- ---------- -------- ------ --------- ------- --------- ------
|
||||
1 0 29 30 0.00 0.97 1.00 -Scene-01
|
||||
2 30 59 30 1.00 1.97 1.00 -Scene-02
|
||||
3 60 89 30 2.00 2.97 1.00 -Scene-03
|
||||
```
|
||||
|
||||
`StartTime` and `EndTime` are in seconds, computed as frame index divided by frame rate. `EndTime` is the time of the last frame of the scene, so it is one frame before the next scene starts. `TotalTime` is the scene's duration in seconds: `Frames` divided by frame rate.
|
||||
|
||||
After the list, the script prints `Scene level used: <level>`, the threshold that produced the list.
|
||||
|
||||
## Scene Threshold
|
||||
|
||||
`-Level` is passed to ffmpeg's `scene` score. The score measures how much a frame differs from the previous one, from `0` to `1`:
|
||||
|
||||
- `0.3` (default): a reasonable starting point for ordinary footage.
|
||||
- Lower values (e.g. `0.1`) detect gradual changes, such as slow camera moves, and produce more scenes.
|
||||
- Higher values (e.g. `0.6`) only detect hard cuts.
|
||||
- `1.0`: detects nothing, so the whole video becomes one scene.
|
||||
|
||||
## Automatic Level Search
|
||||
|
||||
With `-Scenes`, the script searches for a level that gives exactly the requested number of scenes:
|
||||
|
||||
1. It starts at `-Level` (rounded to 2 decimals) with a step of `0.1`.
|
||||
2. It detects the scenes and displays them, printed as `Pass N: level X -> M scene(s)` followed by the list.
|
||||
3. If the count matches `-Scenes`, the search stops.
|
||||
4. Too few scenes: the level moves down. Too many scenes: the level moves up. When the direction changes, the step is halved. Each new level is rounded to 2 decimals; the step is not rounded.
|
||||
5. The search stops with exit code `1` when the step falls below `0.01`, or when the level would leave the range `0` to `1.0`. Nothing is deleted or created in that case.
|
||||
|
||||
There is no limit on the number of passes. Each pass runs a full detection on the video, so long videos take longer. A count can be unreachable for a video: raising the level can skip a scene and jump past the target, so the search fails with exit code `1`.
|
||||
|
||||
The level used for the final list is shown on the last line, `Scene level used: <level>`.
|
||||
|
||||
**List exactly 4 scenes, starting at level 0.3:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Scenes 4 -List
|
||||
```
|
||||
|
||||
## Suffix Placeholders
|
||||
|
||||
The first printf-style placeholder in `-Suffix` is replaced by the scene number. The number is zero-padded to the width given in the placeholder:
|
||||
|
||||
| Placeholder | Scene 1 | Scene 12 |
|
||||
| --- | --- | --- |
|
||||
| `%d` | `1` | `12` |
|
||||
| `%02d` | `01` | `12` |
|
||||
| `%003d` | `001` | `012` |
|
||||
| `%03d` | `001` | `012` |
|
||||
|
||||
Other text in the suffix is used as written, so `-Suffix "-Shot-%03d"` produces `P01-Shot-001.mp4`. A suffix without a placeholder, such as `-Suffix "-Scene"`, is rejected. A literal `%%` is not supported.
|
||||
|
||||
## Replacing Existing Clips
|
||||
|
||||
Before the new clips are created, the script deletes earlier clips of the same source. A file is deleted when all of these match:
|
||||
|
||||
- It is in the same directory as the source video.
|
||||
- Its name is the source's base name, then the suffix pattern (any number in place of the placeholder), then `.skip` only if the source had one, then the source's extension.
|
||||
- It is not the source video itself.
|
||||
|
||||
Matching is case-insensitive. Files that do not match are never touched. Existing clips are always replaced, so you do not need to delete them first.
|
||||
|
||||
| Source | Matches | Does not match |
|
||||
| --- | --- | --- |
|
||||
| `P01.mp4` | `P01-Scene-01.mp4`, `P01-Scene-12.mp4` | `P01-Scene-01.skip.mp4`, `P01-Other.mp4` |
|
||||
| `P01.skip.mp4` | `P01-Scene-01.skip.mp4` | `P01-Scene-01.mp4` |
|
||||
|
||||
Because the deletion happens before the first clip is created, a second run with a higher `-Level` value removes the extra clips from the first run.
|
||||
|
||||
## Examples
|
||||
|
||||
**List the scenes without creating anything:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Level 0.4 -List
|
||||
```
|
||||
|
||||
**Create one clip per scene:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4
|
||||
# Creates: P01-Scene-01.mp4, P01-Scene-02.mp4, ...
|
||||
```
|
||||
|
||||
**Scale and remove audio from every clip:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01 -Level 0.3 -Size 720p -NoAudio
|
||||
```
|
||||
|
||||
**Custom suffix:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -Suffix "-Shot-%03d"
|
||||
# Creates: P01-Shot-001.mp4, P01-Shot-002.mp4, ...
|
||||
```
|
||||
|
||||
**Source with a `.skip` marker:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.skip.mp4
|
||||
# Creates: P01-Scene-01.skip.mp4, P01-Scene-02.skip.mp4, ...
|
||||
```
|
||||
|
||||
**Higher-quality clips:**
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -CRF 18
|
||||
```
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm`. Both apply to the deletion of old clips and to the creation of new ones, and are forwarded to `Create-Clip.ps1`.
|
||||
|
||||
- **`-WhatIf`**: Shows the scene list, the files that would be deleted, and the clips that would be created. Nothing is changed on disk.
|
||||
- **`-Confirm`**: Prompts before each deletion and each clip creation.
|
||||
|
||||
```powershell
|
||||
.\Create-Scenes.ps1 -Video P01.mp4 -WhatIf
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Frame indices are computed as `timestamp * frame rate`. This is exact for constant-frame-rate videos. With variable frame rate, a scene boundary can be off by one frame.
|
||||
- Clips are encoded by `Clone-Video.ps1`, so each clip keeps the source's resolution, frame rate and audio unless extra parameters change them.
|
||||
- Clips are written next to the source video, not to the current directory.
|
||||
- Scene detection is based on visual change only. A cut between two very similar shots may not be detected, and a fast camera move may be detected as a cut. Adjust `-Level` to suit the footage, or use `-Scenes` to search for a level that gives a chosen number of scenes.
|
||||
- Each clip is created with `Create-Clip.ps1`, so a failed clip is reported as a warning and the other scenes are still created. At the end, the script reports how many scenes failed.
|
||||
@@ -0,0 +1,346 @@
|
||||
# Create-ShortClip.ps1
|
||||
|
||||
Creates a short clip video file from a 16:9 video file with dynamic cropping, padding, and optional overlay.
|
||||
|
||||
## Synopsis
|
||||
|
||||
Transforms landscape videos into portrait (9:16) or square (1:1) format clips by:
|
||||
|
||||
1. Extracting frames with optional vertical padding
|
||||
2. Applying horizontal cropping with smooth transitions between keyframes
|
||||
3. Optionally overlaying a PNG image with transparency
|
||||
4. Encoding the final video with preserved quality settings
|
||||
|
||||
The output filename is automatically generated by appending `-ShortClip` before the file extension.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file (absolute or relative)
|
||||
- **`-Plan`** (required): Path to INI plan file defining clip specifications and cropping behavior
|
||||
- **`-Images`** (optional): Path to output PNG images file pattern. If not provided, frames are saved in subdirectory 'Frames' with video filename and 4-digit frame numbers
|
||||
- **`-Keep`**: Preserve temporary and frames directories after processing
|
||||
- **`-Render`**: Skip frame extraction/cropping and only create video from existing frames
|
||||
- **`-Color`** (optional): Override INI file Color value for padding color name or RGB value (default: `black`)
|
||||
- Named colors: `black`, `white`, `red`, `blue`, etc.
|
||||
- Hex format: `#RRGGBB` (e.g., `#1a1a1a`)
|
||||
- FFmpeg format: `0xRRGGBB`
|
||||
- **`-Overlay`** (optional): Override INI file Overlay value for path to PNG file with transparency to overlay on each cropped frame
|
||||
- **`-Smoothing`** (optional): Override INI file Smoothing value (`None`, `Up`, `Center`, `Down`)
|
||||
- **`-Speed`** (optional): Override INI file Speed value (minimum: 1)
|
||||
- **`-WhatIf`**: Simulate the operation without actually creating files. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before each state-changing operation (frame extraction, cropping, video creation).
|
||||
|
||||
## Plan File Format (INI)
|
||||
|
||||
### [Clip] Section
|
||||
|
||||
Defines output video specifications:
|
||||
|
||||
- **`Format`**: Output aspect ratio (default: `Portrait`)
|
||||
- `Portrait`: 9:16 aspect ratio
|
||||
- `Square`: 1:1 aspect ratio
|
||||
- **`Height`**: Vertical height in pixels (default: `1920`). Width is computed based on format
|
||||
- **`Padding`**: Vertical padding position when input height < output height (default: `Center`)
|
||||
- `Top`: All padding at top
|
||||
- `Bottom`: All padding at bottom
|
||||
- `Center`: Padding split evenly top and bottom
|
||||
- `None`: No padding; scale video to fill output height and crop horizontally to match format width
|
||||
- **'Color'**: Padding color name or RGB value (default: `black`)
|
||||
- Named colors: `black`, `white`, `red`, `blue`, etc.
|
||||
- Hex format: `#RRGGBB` (e.g., `#1a1a1a`)
|
||||
- FFmpeg format: `0xRRGGBB`
|
||||
- **`Overlay`**: Path to PNG file with transparency to overlay on each cropped frame
|
||||
- **`Audio`**: Copy audio from input video (default: `Yes`)
|
||||
- `Yes`: Include audio
|
||||
- `No`: Video only
|
||||
- **`Position`**: Reference point for numeric cropping offset values (default: `Center`)
|
||||
- `Left`: Numeric values represent absolute pixel offset from left edge
|
||||
- `Center`: Numeric values represent pixel offset from center (value - output_width/2)
|
||||
- `Right`: Numeric values represent pixel offset from right edge (value - output_width)
|
||||
- **`Smoothing`**: Mode for adjusting cropping transition length to control speed (default: `None`)
|
||||
- `None`: No smoothing, use calculated interpolation as-is
|
||||
- `Up`: Extend transition by incrementing upper frame until speed limit is met
|
||||
- `Center`: Extend transition by adjusting frame closest to center until speed limit is met
|
||||
- `Down`: Extend transition by decrementing lower frame until speed limit is met
|
||||
- **`Speed`**: Maximum cropping transition speed in pixels per frame (default: `10`, minimum: `1`)
|
||||
- Controls how fast the crop position can change between frames
|
||||
- If interpolated speed exceeds this value, smoothing extends the transition
|
||||
|
||||
### [Cropping] Section
|
||||
|
||||
Defines horizontal cropping keyframes with automatic interpolation:
|
||||
|
||||
**Format:** `frame_number=position`
|
||||
|
||||
**Position values:**
|
||||
|
||||
Each keyframe value is resolved to a **Left offset**: the X coordinate (in input-frame pixels) of the left edge of the crop window. The crop window has width `output_width` (or the intermediate width when downscaling).
|
||||
|
||||
- `Left`: Crop from left edge (Left offset = 0)
|
||||
- `Center`: Crop from center (Left offset = (input_width - output_width) / 2)
|
||||
- `Right`: Crop from right edge (Left offset = input_width - output_width)
|
||||
- `number`: Absolute pixel offset, interpreted according to the `Position` setting in the `[Clip]` section (see `Position`). Result is clamped to `[0, input_width - output_width]`.
|
||||
|
||||
From the Left offset, the script also derives:
|
||||
|
||||
- **Center offset** = Left offset + output_width / 2 (X coordinate of the crop window's center)
|
||||
- **Right offset** = Left offset + output_width (X coordinate of the crop window's right edge)
|
||||
|
||||
All three values are printed in the cropping offsets table at runtime to help diagnose framing.
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- Frame numbers must be in ascending order
|
||||
- Intermediate frames are automatically interpolated with smooth transitions
|
||||
- Frames after the last keyframe use the last defined offset
|
||||
- If section is empty or missing, defaults to `0=Center`
|
||||
|
||||
**Example:**
|
||||
|
||||
```ini
|
||||
[Cropping]
|
||||
0=Left
|
||||
84=Left
|
||||
85=Right
|
||||
140=Right
|
||||
159=Left
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
- Frames 0-84: Crop from left
|
||||
- Frame 84→85: Quick transition from left to right
|
||||
- Frames 85-140: Crop from right
|
||||
- Frames 140-159: Smooth pan from right to left
|
||||
- Frames 159+: Crop from left
|
||||
|
||||
## Examples
|
||||
|
||||
**Basic portrait clip:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video input.mp4 -Plan plan.ini
|
||||
# Creates: input-ShortClip.mp4 (9:16 format)
|
||||
```
|
||||
|
||||
**Square format with white padding:**
|
||||
|
||||
```ini
|
||||
# plan.ini
|
||||
[Clip]
|
||||
Format=Square
|
||||
Height=1920
|
||||
Padding=Center
|
||||
Audio=Yes
|
||||
[Cropping]
|
||||
0=Center
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Color white
|
||||
```
|
||||
|
||||
**With overlay graphic:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Overlay border.png
|
||||
# Applies border.png (with transparency) over each cropped frame
|
||||
```
|
||||
|
||||
**No padding (scale to fill height):**
|
||||
|
||||
```ini
|
||||
# plan.ini
|
||||
[Clip]
|
||||
Format=Portrait
|
||||
Height=1920
|
||||
Padding=None
|
||||
```
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini
|
||||
# Scales video to fill 1920px height, crops horizontally to 1080px width
|
||||
# No vertical padding bars added
|
||||
```
|
||||
|
||||
**Keep frames for inspection:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Keep
|
||||
# Preserves Frames/ directory for review
|
||||
```
|
||||
|
||||
**Re-render with different encoding:**
|
||||
|
||||
```powershell
|
||||
# First run: extract and crop frames
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Keep
|
||||
|
||||
# Modify encoding settings in script, then re-render
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Render
|
||||
# Skips extraction/cropping, only creates video from existing frames
|
||||
```
|
||||
|
||||
**Custom output location:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Images "C:\Output\frames\frame-%04d.png"
|
||||
```
|
||||
|
||||
**Custom padding color:**
|
||||
|
||||
```powershell
|
||||
# Dark gray padding
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Color #1a1a1a
|
||||
|
||||
# Navy blue padding
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Color navy
|
||||
```
|
||||
|
||||
**Position parameter with numeric values:**
|
||||
|
||||
```ini
|
||||
# plan.ini - Input: 1920x1088, Output: 1080x1920
|
||||
[Clip]
|
||||
Format=Portrait
|
||||
Position=Center
|
||||
[Cropping]
|
||||
0=960
|
||||
```
|
||||
|
||||
With `Position=Center` and output width 1080:
|
||||
|
||||
- Numeric value 960 is adjusted: 960 - (1080/2) = 960 - 540 = 420
|
||||
- Final offset: 420 pixels from left edge (centered in 1920px input)
|
||||
|
||||
With `Position=Left`:
|
||||
|
||||
- Numeric value 960 is used directly: 960 pixels from left edge
|
||||
|
||||
With `Position=Right` and output width 1080:
|
||||
|
||||
- Numeric value 960 is adjusted: 960 - 1080 = -120 → clamped to 0
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -WhatIf
|
||||
# Displays what would be done (cropping offsets, frame extraction, video creation) without actually processing
|
||||
```
|
||||
|
||||
**Prompt before each operation:**
|
||||
|
||||
```powershell
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Confirm
|
||||
# Will prompt for confirmation before frame extraction, each cropping operation, and video creation
|
||||
```
|
||||
|
||||
**Smoothing fast transitions:**
|
||||
|
||||
```ini
|
||||
# plan.ini - Control transition speed
|
||||
[Clip]
|
||||
Format=Portrait
|
||||
Smoothing=Center
|
||||
Speed=5
|
||||
[Cropping]
|
||||
0=Left
|
||||
10=Right
|
||||
```
|
||||
|
||||
Without smoothing, frames 0-10 would transition at 84 pixels/frame (too fast).
|
||||
With `Smoothing=Center` and `Speed=5`, the transition extends to more frames, keeping speed ≤ 5 pixels/frame.
|
||||
|
||||
**Override INI settings via command-line:**
|
||||
|
||||
```powershell
|
||||
# Override smoothing settings from command line
|
||||
.\Create-ShortClip.ps1 -Video video.mp4 -Plan plan.ini -Smoothing Up -Speed 3
|
||||
# Uses Up smoothing and speed limit of 3, ignoring INI file values
|
||||
```
|
||||
|
||||
## Cropping Interpolation Example
|
||||
|
||||
Input: 1920x1088 video → Output: 1080x1920 (Portrait)
|
||||
|
||||
```ini
|
||||
[Cropping]
|
||||
0=Left
|
||||
10=200
|
||||
50=Center
|
||||
91=Right
|
||||
```
|
||||
|
||||
**Calculated Left offsets:**
|
||||
|
||||
- **Frame 0**: 0 (Left)
|
||||
- **Frames 1-9**: Linear interpolation from 0 to 200
|
||||
- Frame 1: 20, Frame 2: 40, ..., Frame 9: 180
|
||||
- **Frame 10**: 200
|
||||
- **Frames 11-49**: Linear interpolation from 200 to 420 (Center = (1920-1080)/2)
|
||||
- Frame 11: 194.5, Frame 12: 189, ..., Frame 49: 425.5
|
||||
- **Frame 50**: 420 (Center)
|
||||
- **Frames 51-90**: Linear interpolation from 420 to 840 (Right = 1920-1080)
|
||||
- Frame 51: 430.5, Frame 52: 441, ..., Frame 90: 829.5
|
||||
- **Frame 91**: 840 (Right)
|
||||
- **Frames 92+**: 840 (last known value)
|
||||
|
||||
All fractional values are rounded to nearest pixel.
|
||||
|
||||
**Runtime output** prints Left, Center (= Left + 540), and Right (= Left + 1080) offsets only at frames where the Left offset changes:
|
||||
|
||||
```text
|
||||
Frame Left Center Right
|
||||
----------------------------------------
|
||||
0 0 540 1080
|
||||
1 20 560 1100
|
||||
2 40 580 1120
|
||||
...
|
||||
10 200 740 1280
|
||||
...
|
||||
50 420 960 1500
|
||||
...
|
||||
91 840 1380 1920
|
||||
```
|
||||
|
||||
## Quality Preservation
|
||||
|
||||
The script automatically preserves video quality from the input:
|
||||
|
||||
- Detects and uses same codec (H.264 → libx264, H.265 → libx265)
|
||||
- Preserves pixel format
|
||||
- Uses input bitrate when available
|
||||
- Falls back to CRF 8 (near-lossless) for VBR/CRF-encoded inputs
|
||||
- Audio encoded as AAC at 192k bitrate
|
||||
|
||||
## Processing Workflow
|
||||
|
||||
1. **Validation**: Check video and plan file existence
|
||||
2. **Analysis**: Extract video metadata (dimensions, frame count, FPS, codec)
|
||||
3. **Offset Calculation**: Compute per-frame cropping offsets with interpolation and optional smoothing
|
||||
4. **Frame Extraction**: Extract all frames with vertical padding applied
|
||||
5. **Cropping**: Apply horizontal crop to each frame based on computed offsets
|
||||
6. **Overlay** (optional): Apply PNG overlay during cropping step
|
||||
7. **Encoding**: Create output video from processed frames
|
||||
8. **Cleanup**: Remove temporary directories (unless `-Keep` specified)
|
||||
|
||||
> With `-WhatIf`, the workflow shows all planned operations without executing them.
|
||||
|
||||
## Existing Files and Folders
|
||||
|
||||
- **Default `-Images`** (the `Frames` folder next to the video): files directly in `Frames` are deleted before extraction. Subfolders are not touched. With `-Render`, nothing is deleted, because the existing frames are the input.
|
||||
- **Explicit `-Images`**: existing frames that match the `-Images` pattern are deleted before extraction. The `Frames` folder is not used or deleted.
|
||||
- **Output video**: an existing file at the output path is overwritten.
|
||||
- **Temporary extraction directory** (in the system temporary folder): always removed when the run ends.
|
||||
- **`Frames` folder** (default `-Images` only): removed after the video is created, unless `-Keep` is given.
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffmpeg` and `ffprobe` in system PATH
|
||||
- Frame numbers in INI file are 0-based
|
||||
- Overlay PNG should match output dimensions (width × height)
|
||||
- Transparent areas in overlay allow video content to show through
|
||||
- Use `-Render` to quickly re-encode with different settings without reprocessing frames
|
||||
- Use `-WhatIf` to preview the full workflow (cropping offsets, frame operations, video creation) before committing to a full run
|
||||
- Use `-Confirm` to be prompted before each state-changing operation
|
||||
- `Smoothing` only takes effect when the interpolated speed between two keyframes exceeds the `Speed` limit; otherwise offsets are used unchanged
|
||||
@@ -0,0 +1,269 @@
|
||||
# Create-Turnaround.ps1
|
||||
|
||||
Combines a sequence of images into a single **turnaround** (character reference sheet) image using ffmpeg.
|
||||
|
||||
A turnaround image shows the same subject from several angles — typically front, 3/4, profile and back — side by side at a consistent scale, in one wide image. It is the usual input for keeping a character consistent across image and video generation models.
|
||||
|
||||
## Synopsis
|
||||
|
||||
All images found at `-Source` are placed side by side, left to right, in the order given by a numeric-aware sort of their file names. `-Target` is optional: without it the sheet is saved as a JPEG named after the source directory, in the current directory. Every image keeps its original pixels: nothing is scaled, stretched or cropped. Images smaller than the largest one are centred in their cell and padded with `-Background` (white by default), so a set of slightly different sizes still produces a clean, aligned sheet.
|
||||
|
||||
Supports PNG, JPEG, WEBP, BMP, AVIF and TIFF for both input and output.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Source`** (required): Directory containing the images, a file pattern (e.g. `Img*.jpg`), or a single image file. Can be absolute or relative to the current directory. Non-recursive; only supported image extensions are considered.
|
||||
- **`-Target`** (optional): Path to the output turnaround image. Can be absolute or relative to the current directory. The extension selects the output format; `.png` is appended when the extension is missing or unrecognised. When omitted, the name is derived from the source — see [Default Target Name](#default-target-name).
|
||||
- **`-Rows`** (optional): Number of rows to arrange the images in. Defaults to `1` (a single horizontal row). When only `-Rows` is given, the column count is computed as `ceil(imageCount / Rows)`.
|
||||
- **`-Columns`** (optional): Number of columns to arrange the images in. When only `-Columns` is given, the row count is computed as `ceil(imageCount / Columns)`. When both are given, `Rows × Columns` must be at least the number of images.
|
||||
- **`-Background`** (optional): Colour used to pad images smaller than the largest one, and to fill the unused cells of an incomplete last row. Defaults to `white`. Accepts any ffmpeg colour: a name (`white`, `black`, `silver`), `0xRRGGBB`, or `name@alpha` (`white@0` for transparent).
|
||||
- **`-Quality`** (optional): Quality factor for the lossy output formats (jpeg, webp, avif). **Lower values mean better quality** in all three, see [Quality and Compression](#quality-and-compression). Ignored (with a warning) for png, bmp and tiff.
|
||||
- **`-Compression`** (optional): Compression level for png (`0`-`9`) and webp (`0`-`6`); higher means smaller files but slower encoding. Ignored (with a warning) for jpeg, avif, bmp and tiff.
|
||||
|
||||
## Default Target Name
|
||||
|
||||
When `-Target` is omitted, the script writes a **JPEG named after the last directory member of the source**, into the **current directory**:
|
||||
|
||||
| `-Source` | Name taken from | Result (run from `C:\work`) |
|
||||
| --- | --- | --- |
|
||||
| A directory, e.g. `C:\renders\CharacterA` | The last member of that directory's full path | `C:\work\CharacterA.jpg` |
|
||||
| A file pattern, e.g. `Img*.jpg` | The last member of the **current** directory's full path | `C:\work\work.jpg` |
|
||||
| A single image file | The last member of the current directory's full path | `C:\work\work.jpg` |
|
||||
|
||||
Relative sources are resolved to a full path first, so `-Source .` run from `C:\renders\CharacterA` produces `CharacterA.jpg` in that same directory. Trailing separators are ignored (`C:\renders\CharacterA\` behaves identically).
|
||||
|
||||
Since the derived name always ends in `.jpg`, the output uses the JPEG encoder and its default `-q:v 2`; add `-Quality` to change it, or pass `-Target` explicitly for a different format.
|
||||
|
||||
```powershell
|
||||
PS C:\work> .\Create-Turnaround.ps1 -Source C:\renders\CharacterA
|
||||
No -Target specified; using C:\work\CharacterA.jpg
|
||||
```
|
||||
|
||||
### Drive-root error
|
||||
|
||||
A path with no directory member cannot name the file, so a drive root is rejected:
|
||||
|
||||
```powershell
|
||||
PS C:\work> .\Create-Turnaround.ps1 -Source "C:\"
|
||||
Cannot derive a target file name from 'C:\': the path is a drive root with no directory member. Specify -Target explicitly.
|
||||
```
|
||||
|
||||
The same applies when `-Source` is a file pattern and the current directory is a drive root (e.g. running `Create-Turnaround.ps1 -Source *.png` from `C:\`). Supply `-Target` explicitly in that case.
|
||||
|
||||
## Image Order
|
||||
|
||||
File names are sorted with a **numeric-aware (natural) sort**, so digit runs compare as numbers:
|
||||
|
||||
```text
|
||||
Img1.png, Img2.png, Img10.png (not Img1, Img10, Img2)
|
||||
Frame-00001.png ... Frame-00012.png
|
||||
```
|
||||
|
||||
Zero-padded sequence names therefore sort identically to a plain alphabetical sort, while non-padded names (`Pose2`, `Pose10`) still come out in the intended order.
|
||||
|
||||
## Cell Size and Padding
|
||||
|
||||
The cell is as large as the **largest** source image in both dimensions:
|
||||
|
||||
- `cellWidth` = max width of all sources
|
||||
- `cellHeight` = max height of all sources
|
||||
- Output size = `cellWidth × columns` by `cellHeight × rows`
|
||||
|
||||
Each image is centred in its cell. Images that already match the cell size are passed through untouched; smaller ones gain `-Background` margins. No source image is ever resized, so the sheet is lossless with respect to the originals (verified at PSNR = ∞ for PNG in/PNG out).
|
||||
|
||||
When padding is applied, the console reports it:
|
||||
|
||||
```text
|
||||
Cell size: 600x768
|
||||
Note: 2 image(s) smaller than the largest image; centred and padded with white.
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
### Single row (default)
|
||||
|
||||
Four 512x768 images produce a 2048x768 sheet:
|
||||
|
||||
```text
|
||||
[1][2][3][4]
|
||||
```
|
||||
|
||||
### Grid
|
||||
|
||||
`-Rows` and/or `-Columns` wrap the images into a grid. With 4 images and `-Columns 2` (→ 2 rows), the output is 1024x1536:
|
||||
|
||||
```text
|
||||
[1][2]
|
||||
[3][4]
|
||||
```
|
||||
|
||||
An incomplete last row is **centred** inside the full output width and the remaining space filled with `-Background`. With 4 images and `-Columns 3` (→ 2 rows, 1536x1536):
|
||||
|
||||
```text
|
||||
[1][2][3]
|
||||
[4]
|
||||
```
|
||||
|
||||
| Images | Switches | Rows x Columns | Output (512x768 cells) |
|
||||
| --- | --- | --- | --- |
|
||||
| 4 | *(none)* | 1 x 4 | 2048x768 |
|
||||
| 4 | `-Columns 2` | 2 x 2 | 1024x1536 |
|
||||
| 4 | `-Rows 2` | 2 x 2 | 1024x1536 |
|
||||
| 4 | `-Columns 3` | 2 x 3 | 1536x1536 (last row centred) |
|
||||
| 4 | `-Rows 1 -Columns 2` | — | Error: 2 cells is not enough for 4 images |
|
||||
|
||||
## Output Format
|
||||
|
||||
The target extension selects the encoder, with the same mapping and defaults as [Clone-Image.ps1](Clone-Image.md):
|
||||
|
||||
| Extension | Encoder | Default settings | `-Quality` | `-Compression` |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `.png` | `png` | Lossless | — | `-compression_level` (0-9) |
|
||||
| `.jpg` / `.jpeg` | `mjpeg` | `-q:v 2`, `yuvj420p` | `-q:v` (1-31) | — |
|
||||
| `.webp` | `libwebp` | `-quality 90` | `-quality` (inverted) | `-compression_level` (0-6) |
|
||||
| `.avif` | `libaom-av1` | `-still-picture 1`, `-cpu-used 4`, `-crf 25` | `-crf` (0-63) | — |
|
||||
| `.bmp` | `bmp` | Lossless | — | — |
|
||||
| `.tif` / `.tiff` | `tiff` | Lossless | — | — |
|
||||
|
||||
PNG is recommended for a turnaround sheet that will be fed back into a generation model, since it is lossless and preserves transparency.
|
||||
|
||||
## Quality and Compression
|
||||
|
||||
`-Quality` is always interpreted as **lower = better quality**, regardless of format. For webp — whose native `-quality` option means the opposite — the value is inverted internally (`-Quality 20` becomes `-quality 80`), so a single convention holds across jpeg, webp and avif.
|
||||
|
||||
| Format | `-Quality` maps to | Range | Default when omitted |
|
||||
| --- | --- | --- | --- |
|
||||
| jpeg | `-q:v` | 1-31 (lower is better) | `2` |
|
||||
| webp | `-quality` = `100 - Quality` | 0-100 after inversion | `90` (i.e. `-Quality 10`) |
|
||||
| avif | `-crf` | 0-63 (lower is better) | `25` |
|
||||
|
||||
`-Compression` trades encoding time for file size on the two formats that support it, and does not affect image quality (both are lossless/independent of it):
|
||||
|
||||
| Format | `-Compression` maps to | Range |
|
||||
| --- | --- | --- |
|
||||
| png | `-compression_level` | 0-9 (higher = smaller, slower) |
|
||||
| webp | `-compression_level` | 0-6 (higher = smaller, slower) |
|
||||
|
||||
Measured on a 3840x480 sheet:
|
||||
|
||||
| Command | Output size |
|
||||
| --- | --- |
|
||||
| `-Target sheet.jpg` (default) | 73,744 B |
|
||||
| `-Target sheet.jpg -Quality 2` | 73,744 B (identical to the default) |
|
||||
| `-Target sheet.jpg -Quality 28` | 33,349 B |
|
||||
| `-Target sheet.webp -Quality 5` | 30,508 B |
|
||||
| `-Target sheet.webp -Quality 70` | 17,578 B |
|
||||
| `-Target sheet.png -Compression 0` | 7,395,829 B |
|
||||
| `-Target sheet.png -Compression 9` | 10,990 B |
|
||||
|
||||
Passing `-Quality` to a lossless format (or `-Compression` to a format that ignores it) is not an error; the value is dropped and a warning is printed:
|
||||
|
||||
```text
|
||||
WARNING: -Quality is ignored for the lossless png format.
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**No `-Target` — JPEG named after the source directory, written to the current directory:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source C:\renders\CharacterA
|
||||
# Writes .\CharacterA.jpg
|
||||
```
|
||||
|
||||
**No `-Target`, run from inside the frames directory:**
|
||||
|
||||
```powershell
|
||||
cd C:\renders\CharacterA
|
||||
.\Create-Turnaround.ps1 -Source "F*.png"
|
||||
# Writes C:\renders\CharacterA\CharacterA.jpg (name taken from the current directory)
|
||||
```
|
||||
|
||||
**Four frames in a directory into a single-row sheet:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png
|
||||
```
|
||||
|
||||
**Using a file pattern:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source ".\renders\Img*.jpg" -Target turnaround.png
|
||||
```
|
||||
|
||||
**Extension-less target (`.png` is appended):**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target character-sheet
|
||||
# Writes character-sheet.png
|
||||
```
|
||||
|
||||
**Eight angles as a 2x4 grid:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png -Rows 2
|
||||
```
|
||||
|
||||
**Three columns, incomplete last row centred:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png -Columns 3
|
||||
```
|
||||
|
||||
**Transparent background instead of white:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png -Background "white@0"
|
||||
```
|
||||
|
||||
**Mid-gray background, JPEG output:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.jpg -Background 0x808080
|
||||
```
|
||||
|
||||
**Smaller JPEG sheet (lower quality):**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.jpg -Quality 20
|
||||
```
|
||||
|
||||
**Maximum PNG compression (smallest lossless file, slowest encode):**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png -Compression 9
|
||||
```
|
||||
|
||||
**Near-lossless webp with maximum compression effort:**
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.webp -Quality 1 -Compression 6
|
||||
```
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing `-Target` image is **replaced** without a prompt. ffmpeg runs with `-y`.
|
||||
|
||||
- The folder that contains `-Target` must already exist. The script stops with an error instead of creating it.
|
||||
- Use `-WhatIf` first, or write to a new name.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Prints the image list, the computed layout and the full ffmpeg command without writing the output file.
|
||||
- **`-Confirm`**: Prompts for confirmation before creating the turnaround image.
|
||||
|
||||
```powershell
|
||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.png -WhatIf
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffmpeg` and `ffprobe` in the Windows `PATH`.
|
||||
- The target directory must exist; the script errors out rather than creating it.
|
||||
- Every input is normalised to `rgba` before stacking, so sources may mix formats (e.g. PNG + JPEG + WEBP) and colour models freely.
|
||||
- Source images are never scaled, stretched or cropped — differing sizes are reconciled with padding only.
|
||||
- A single image is valid input and produces a one-cell output (useful to normalise/convert a frame).
|
||||
- Files that are not recognised images are ignored; if none remain, the script errors with the list of supported extensions.
|
||||
- The output file is excluded from its own input list, so writing `-Target` into the `-Source` directory stays idempotent across re-runs (the previous sheet is not stacked into the new one).
|
||||
@@ -0,0 +1,190 @@
|
||||
# Crop-Clips.ps1
|
||||
|
||||
Batch crop videos generated by **LTX-2**, **Kling** and **Wan** to standard resolutions using ffmpeg.
|
||||
|
||||
Every non-standard resolution these generators produce is recognised by a single lookup table in the script, so one command handles a folder containing clips from any of them. Resolutions are unique per generator, so there is no ambiguity. Any resolution not listed below is skipped without modification.
|
||||
|
||||
Use `-Size` to ignore the table and convert every video to a specific resolution instead — see [Forcing a Size](#forcing-a-size).
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Source`** (optional): Source directory containing video files (default: current directory)
|
||||
- **`-Target`** (optional): Target directory for cropped videos (default: current directory)
|
||||
- **`-Size`** (optional): Output video size. Possible values: `320p`, `384p`, `480p`, `720p`, `900p`, `1080p`, `HD`, `1440p`, `2K`, `2160p`, `4K`, or a custom `width:height`. When specified it **overrides the resolution table** and converts every video to that size — see [Forcing a Size](#forcing-a-size). Without it, the table below is used.
|
||||
|
||||
## Supported Resolutions
|
||||
|
||||
### LTX-2
|
||||
|
||||
**Landscape:**
|
||||
|
||||
- **3744x2112** → **3840x2160**: Scale up to 3840 width, then vertical center crop
|
||||
- **2496x1408** → **2560x1440**: Scale up to 2560 width, then vertical center crop
|
||||
- **2160x1216** → **1920x1080**: Scale down to 1920 width, then vertical center crop
|
||||
- **1920x1088** → **1920x1080**: Center crop (removes 4px from top and bottom)
|
||||
- **1920x1056** → **1920x1080**: Scale up to 1080 height, then horizontal center crop
|
||||
- **1872x1056** → **1920x1080**: Scale up to 1920 width, then vertical center crop
|
||||
- **1600x896** → **1600x900**: Scale to 900 height, then center crop
|
||||
- **1312x704** → **1280x720**: Scale up to 720 height, then horizontal center crop
|
||||
- **1280x704** → **1280x720**: Scale to 720 height, then center crop
|
||||
- **1248x704** → **1280x720**: Scale up to 1280 width, then vertical center crop
|
||||
- **832x448** → **854x480**: Scale to 480 height, then center crop
|
||||
- **640x384** → **684x384**: Scale to 684 width, then center crop
|
||||
- **576x320** → **570x320**: Center crop (removes 3px from left and right)
|
||||
|
||||
**Portrait:**
|
||||
|
||||
- **2112x3744** → **2160x3840**: Scale up to 3840 height, then horizontal center crop
|
||||
- **1408x2496** → **1440x2560**: Scale up to 2560 height, then horizontal center crop
|
||||
- **1216x2160** → **1080x1920**: Scale down to 1920 height, then horizontal center crop
|
||||
- **1088x1920** → **1080x1920**: Center crop (removes 4px from left and right)
|
||||
- **1056x1920** → **1080x1920**: Scale up to 1080 width, then vertical center crop
|
||||
- **1056x1872** → **1080x1920**: Scale up to 1920 height, then horizontal center crop
|
||||
- **896x1600** → **900x1600**: Scale to 900 width, then center crop
|
||||
- **704x1312** → **720x1280**: Scale up to 720 width, then vertical center crop
|
||||
- **704x1280** → **720x1280**: Scale to 720 width, then center crop
|
||||
- **704x1248** → **720x1280**: Scale up to 1280 height, then horizontal center crop
|
||||
- **448x832** → **480x854**: Scale to 480 width, then center crop
|
||||
- **384x640** → **384x684**: Scale to 684 height, then center crop
|
||||
- **320x576** → **320x570**: Center crop (removes 3px from top and bottom)
|
||||
|
||||
### Kling
|
||||
|
||||
- **1928x1072** → **1920x1080**: Scale to 1080 height, then center crop width to 1920
|
||||
- **1920x1076** → **1920x1080**: Scale to 1080 height, then center crop width to 1920
|
||||
- **1920x1072** → **1920x1080**: Scale to 1080 height, then center crop width to 1920
|
||||
- **1916x1080** → **1920x1080**: Already 1080 tall, so the 4 missing columns are padded (centered) instead of rescaling
|
||||
|
||||
### Wan
|
||||
|
||||
- **1928x1076** → **1920x1080**: Scale to 1080 height, then center crop width to 1920
|
||||
- **1926x1076** → **1920x1080**: Scale to 1080 height, then center crop width to 1920
|
||||
- **1284x716** → **1280x720**: Scale to 720 height, then center crop width to 1280
|
||||
|
||||
### Square (1:1)
|
||||
|
||||
- **NxN** → **1080x1080**: Any square resolution other than 1080x1080 is scaled up or down to 1080x1080 (no crop needed)
|
||||
|
||||
## Forcing a Size
|
||||
|
||||
`-Size` bypasses the resolution table entirely. Every video is **cover-scaled** (`force_original_aspect_ratio=increase`, which preserves the pixel aspect ratio and guarantees the frame is fully covered) and then **centre-cropped** to the exact requested size — the same approach as [Clone-Image.ps1](Clone-Image.md). Because the table is not consulted, resolutions that would normally be skipped are converted too.
|
||||
|
||||
### Presets
|
||||
|
||||
Named presets are the landscape targets this script already produces:
|
||||
|
||||
| Preset | Size | Preset | Size |
|
||||
| --- | --- | --- | --- |
|
||||
| `320p` | 570x320 | `1080p` / `HD` | 1920x1080 |
|
||||
| `384p` | 684x384 | `1440p` / `2K` | 2560x1440 |
|
||||
| `480p` | 854x480 | `2160p` / `4K` | 3840x2160 |
|
||||
| `720p` | 1280x720 | | |
|
||||
| `900p` | 1600x900 | | |
|
||||
|
||||
Preset names are case-insensitive (`-Size hd` works).
|
||||
|
||||
### Portrait sources
|
||||
|
||||
Presets are **transposed for portrait sources**, so a 9:16 clip stays 9:16 instead of losing most of the frame to a landscape crop:
|
||||
|
||||
| Source | `-Size 1080p` | `-Size 720p` |
|
||||
| --- | --- | --- |
|
||||
| 1920x1088 (landscape) | 1920x1080 | 1280x720 |
|
||||
| 640x480 (landscape) | 1920x1080 | 1280x720 |
|
||||
| 512x512 (square) | 1920x1080 | 1280x720 |
|
||||
| 1080x1920 (portrait) | 1080x1920 *(already — skipped)* | 720x1280 |
|
||||
| 720x1280 (portrait) | 1080x1920 | 720x1280 *(already — skipped)* |
|
||||
|
||||
A custom `width:height` is **always used exactly as given** (never transposed), so `-Size 1080:1920` forces portrait output for every video, landscape sources included. Odd custom dimensions are rounded up to the next even number, as `libx264`/`yuv420p` requires even sizes (`-Size 571:321` becomes 572x322).
|
||||
|
||||
### Already-correct videos
|
||||
|
||||
A video that is already exactly the requested size is left untouched — no re-encode, no generation loss:
|
||||
|
||||
```text
|
||||
Skipping: 1920x1080.mp4 (already 1920x1080)
|
||||
```
|
||||
|
||||
An invalid `-Size` value fails immediately, before any file is processed.
|
||||
|
||||
## Behavior
|
||||
|
||||
- Processes all `.mp4` files in source directory
|
||||
- Skips files ending with `.bak.mp4` or `.skip.mp4`
|
||||
- Automatically detects resolution and applies appropriate crop filter (or uses `-Size` when given)
|
||||
- Preserves original quality and audio
|
||||
- **In-place mode** (source and target are the same directory). This is the default when `-Source` and `-Target` are both omitted, because both default to the current directory:
|
||||
- Each original `video.mp4` is copied to `video.bak.mp4`. An existing `.bak.mp4` is replaced.
|
||||
- Only files that are cropped are backed up. A file that is skipped, because it is already at the requested size or its resolution is not in the table, keeps its existing `.bak.mp4`. Re-running in place without `-Size` skips the files it already cropped, so the backup stays the original. A re-run with a different `-Size` crops them again, and replaces the backup with the first run's output. Keep your own copy of the originals before a re-run with a different `-Size`, or use a separate `-Target` folder.
|
||||
- The cropped result is written to `video.crop.mp4`. Only after that succeeds is the original `video.mp4` deleted and replaced by the cropped file. The `.bak.mp4` backup is kept.
|
||||
- Run with `-WhatIf` first, and do not run it on a folder you have not designated.
|
||||
- **Separate target folder**: the source files are not changed. A file with the same name already in the target folder is deleted and replaced.
|
||||
- If ffmpeg fails, the partial `.crop.mp4` file is deleted and the script stops with an error
|
||||
|
||||
## Examples
|
||||
|
||||
**Crop videos in current directory:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1
|
||||
```
|
||||
|
||||
**Crop videos from specific source:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Source "C:\Videos\Input"
|
||||
```
|
||||
|
||||
**Crop to different target directory:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Source "C:\Videos\Input" -Target "C:\Videos\Output"
|
||||
```
|
||||
|
||||
**Force every video to 1080p (table ignored):**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Source "C:\Videos\Input" -Size 1080p
|
||||
# Landscape clips -> 1920x1080, portrait clips -> 1080x1920
|
||||
```
|
||||
|
||||
**Force a small preset:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Size 480p
|
||||
# Landscape clips -> 854x480, portrait clips -> 480x854
|
||||
```
|
||||
|
||||
**Force a custom size (used verbatim, never transposed):**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Size "1080:1920"
|
||||
# Every video becomes 1080x1920, landscape sources included
|
||||
```
|
||||
|
||||
**Skip specific files:**
|
||||
|
||||
```powershell
|
||||
# Rename files to skip processing:
|
||||
# video.mp4 -> video.skip.mp4
|
||||
.\Crop-Clips.ps1
|
||||
```
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually modifying files. Shows what would be cropped.
|
||||
- **`-Confirm`**: Prompt for confirmation before processing each video file.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Source "C:\Videos\Input" -Target "C:\Videos\Output" -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before each file:**
|
||||
|
||||
```powershell
|
||||
.\Crop-Clips.ps1 -Source "C:\Videos\Input" -Confirm
|
||||
```
|
||||
@@ -0,0 +1,115 @@
|
||||
# Get-Frame.ps1
|
||||
|
||||
Extracts a specific frame of a video file to a PNG image by calling [Get-LastFrame.ps1](Get-LastFrame.md) with an automatically derived output filename. The image path is generated from the `-Video` filename by appending a dash and the resolved frame index zero-padded to at least four digits — for example, `P01.mp4` at frame 10 becomes `P01-0010.png` saved alongside the source video.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to the input video file. Can be absolute or relative to current directory.
|
||||
- **`-Frame`** (required): 0-based frame index to extract.
|
||||
- Positive values: Direct frame index (e.g., `0` = first frame, `100` = frame 100)
|
||||
- Negative values: Count from the end (e.g., `-1` = last frame, `-2` = second-to-last)
|
||||
- Values are automatically clamped to the valid range (0 to total frames − 1): a positive value past the last frame gives the last frame, and a negative value before the first frame gives frame 0
|
||||
|
||||
## Image Filename Derivation
|
||||
|
||||
The output filename is derived from the resolved (always positive) frame index:
|
||||
|
||||
| Video | `-Frame` | Total frames | Output image |
|
||||
| --- | --- | --- | --- |
|
||||
| `P01.mp4` | `10` | any | `P01-0010.png` |
|
||||
| `P01.mp4` | `0` | any | `P01-0000.png` |
|
||||
| `P01.mp4` | `-1` | 300 | `P01-0299.png` |
|
||||
| `P01.mp4` | `-2` | 300 | `P01-0298.png` |
|
||||
| `render/shot.mp4` | `42` | any | `render/shot-0042.png` |
|
||||
| `C:\Videos\take01.mp4` | `100` | any | `C:\Videos\take01-0100.png` |
|
||||
|
||||
Negative indices are resolved to the actual frame index before building the filename, so the output always reflects the exact frame that was extracted.
|
||||
|
||||
## Output Name Digits
|
||||
|
||||
The frame index is padded with zeros to **at least** four digits. Frames `0` to `9999` give exactly four digits (`0010`). Larger numbers are written in full: frame `12345` gives `P01-12345.png`.
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing PNG with the same name is **replaced**. The image is written by [Get-LastFrame.ps1](Get-LastFrame.md), which overwrites its output.
|
||||
|
||||
## Command Wrapper
|
||||
|
||||
`Get-Frame.cmd` takes the video and the frame as **positional** arguments. It calls `Get-Frame.ps1` found on `PATH`, so the script folder must be on `PATH`. Arguments after the frame are passed to `Get-Frame.ps1` unchanged. Only `-WhatIf` and `-Confirm` are accepted there. Other options are rejected.
|
||||
|
||||
```cmd
|
||||
Get-Frame.cmd "P01.mp4" 10
|
||||
```
|
||||
|
||||
This is the same call as `.\Get-Frame.ps1 -Video P01.mp4 -Frame 10`.
|
||||
|
||||
## Limitations
|
||||
|
||||
- `-Image` is not a parameter of `Get-Frame.ps1`, so it cannot choose the output name. To use a custom name or a numbering pattern, call [Get-LastFrame.ps1](Get-LastFrame.md) with `-Image`:
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video P01.mp4 -Frame 10 -Image "P01-%04d.png"
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Extract a specific frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame 10
|
||||
# Creates: P01-0010.png
|
||||
```
|
||||
|
||||
**Extract the first frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame 0
|
||||
# Creates: P01-0000.png
|
||||
```
|
||||
|
||||
**Extract the last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame -1
|
||||
# If video has 300 frames, creates: P01-0299.png
|
||||
```
|
||||
|
||||
**Extract the second-to-last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame -2
|
||||
# If video has 300 frames, creates: P01-0298.png
|
||||
```
|
||||
|
||||
**Extract the 10th frame from the end:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame -10
|
||||
# If video has 300 frames, creates: P01-0290.png
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video "C:\Videos\P01.mp4" -Frame 50
|
||||
# Creates: C:\Videos\P01-0050.png
|
||||
```
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution (forwarded to `Get-LastFrame.ps1`):
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without extracting the frame. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before extracting the frame.
|
||||
|
||||
**Simulate without extracting:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame 10 -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before extraction:**
|
||||
|
||||
```powershell
|
||||
.\Get-Frame.ps1 -Video P01.mp4 -Frame 10 -Confirm
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
# Get-FramesCount.ps1
|
||||
|
||||
Gets the total frame count of a video file using ffprobe.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file
|
||||
|
||||
## Examples
|
||||
|
||||
**Get frame count:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesCount.ps1 -Video video.mp4
|
||||
```
|
||||
|
||||
Output example:
|
||||
|
||||
```text
|
||||
500
|
||||
```
|
||||
|
||||
**Use in scripts:**
|
||||
|
||||
```powershell
|
||||
$frameCount = .\Get-FramesCount.ps1 -Video video.mp4
|
||||
Write-Host "Video has $frameCount frames"
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesCount.ps1 -Video "C:\Videos\sample.mp4"
|
||||
```
|
||||
@@ -0,0 +1,102 @@
|
||||
# Get-FramesRange.ps1
|
||||
|
||||
Extracts a specific range of frames from a video file to PNG images.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file
|
||||
- **`-Images`** (optional): Path to output PNG images file pattern. Can be absolute or relative to current directory. If not provided, the output files will be in subdirectory 'Frames' and named as the video file with a frame number suffix padded with zeros to 4 digits.
|
||||
- **`-Frames`** (required): Frame range specification (see formats below)
|
||||
|
||||
## Frame Range Formats
|
||||
|
||||
- **`start-end`**: Extract frames from start to end index (e.g., `100-200`). Both ends must lie inside the video, and `start` must not be greater than `end`. Otherwise the script stops with exit code `1`.
|
||||
- **`positive_number`**: Extract frames from 0 to that number (e.g., `50` extracts frames 0-50). A single number past the last frame is clamped to the last frame (`150` on a 100-frame video gives frames 0 to 99).
|
||||
- **`-negative_number`**: Extract the last N frames (e.g., `-10` extracts frames 90 to 99 of a 100-frame video; `-1` extracts the last frame only)
|
||||
- **`%`**: The last frame index. Alone (`%`) it extracts only the last frame. In a range it is the end of the range:
|
||||
- `%` - Extract only the last frame
|
||||
- `10-%` - Extract from frame 10 to the last frame
|
||||
- `-%` is not supported and is rejected with exit code `1`
|
||||
- **`all`**: Extract all frames (same as `0-%`)
|
||||
|
||||
## Examples
|
||||
|
||||
**Extract specific range:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "100-200"
|
||||
# Creates: Frames/video_0100.png, Frames/video_0101.png, ..., Frames/video_0200.png
|
||||
```
|
||||
|
||||
**Extract first 50 frames:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "50"
|
||||
# Creates: Frames/video_0000.png through Frames/video_0050.png
|
||||
```
|
||||
|
||||
**Extract last 10 frames:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "-10"
|
||||
# If video has 500 frames, creates: Frames/video_0490.png through Frames/video_0499.png
|
||||
```
|
||||
|
||||
**Extract only the last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "%"
|
||||
# If video has 500 frames, creates: Frames/video_0499.png
|
||||
```
|
||||
|
||||
**Extract from frame 10 to the last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "10-%"
|
||||
# If video has 500 frames, creates: Frames/video_0010.png through Frames/video_0499.png
|
||||
```
|
||||
|
||||
**Extract all frames:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "all"
|
||||
# If video has 500 frames, creates: Frames/video_0000.png through Frames/video_0499.png
|
||||
```
|
||||
|
||||
**Custom output pattern:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "0-100" -Images "output_frame"
|
||||
# Creates: output_frame_0000.png through output_frame_0100.png
|
||||
```
|
||||
|
||||
**Full output path:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "50-150" -Images "C:\Output\frame_%04d.png"
|
||||
```
|
||||
|
||||
**Note:** Frame numbers in output filenames reflect the actual video frame index (0-based).
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing PNG with the same name is **replaced**. Use `-WhatIf` to check the output names first.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually extracting frames. Shows which frames would be extracted.
|
||||
- **`-Confirm`**: Prompt for confirmation before extracting each frame.
|
||||
|
||||
**Simulate without extracting:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "100-200" -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before each frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-FramesRange.ps1 -Video video.mp4 -Frames "0-10" -Confirm
|
||||
```
|
||||
@@ -0,0 +1,132 @@
|
||||
# Get-LastFrame.ps1
|
||||
|
||||
Extracts a specific frame of a video file to a PNG image.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file
|
||||
- **`-Image`** (required): Path to output PNG image file. Supports a printf-style numbering token like [Get-FramesRange.ps1](Get-FramesRange.md) — see [Filename Pattern](#filename-pattern). The output directory is created when it does not exist.
|
||||
- **`-Frame`** (optional): 0-based frame index to extract (default: -1)
|
||||
- Positive values: Direct frame index (e.g., 0 = first frame, 100 = frame 100)
|
||||
- Negative values: Count from the end (e.g., -1 = last frame, -2 = second-to-last)
|
||||
- Values are automatically clamped to valid range (0 to total frames - 1): a positive value past the last frame gives the last frame, and a negative value before the first frame gives frame 0
|
||||
|
||||
## Filename Pattern
|
||||
|
||||
`-Image` may contain a printf-style numbering token, which is replaced with the **actual 0-based frame index that was extracted** — after `-Frame` has been resolved, so negative indices produce the real frame number:
|
||||
|
||||
| Token | Meaning | Frame 99 | Frame 7 |
|
||||
| --- | --- | --- | --- |
|
||||
| `%d` | No padding | `99` | `7` |
|
||||
| `%04d` | Zero-padded to 4 digits | `0099` | `0007` |
|
||||
| `%06d` | Zero-padded to 6 digits | `000099` | `000007` |
|
||||
|
||||
A path without a token is used **literally**, so existing commands such as `-Image last_frame.png` are unaffected (only one frame is produced, so no numbering is needed).
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image "frame_%04d.png"
|
||||
# 100-frame video -> frame_0099.png
|
||||
```
|
||||
|
||||
When the pattern resolves, the script reports the final name:
|
||||
|
||||
```text
|
||||
Total frames: 100 - extracting frame index 99
|
||||
Output pattern resolved to: frame_0099.png
|
||||
Frame saved to: frame_0099.png
|
||||
```
|
||||
|
||||
**Note:** the token is expanded by the script, not by ffmpeg. ffmpeg would treat `%04d` in an output path as an image-sequence counter starting at `1` (writing `frame_0001.png` regardless of which frame was selected); expanding it here guarantees the number is the real video frame index, matching [Get-FramesRange.ps1](Get-FramesRange.md).
|
||||
|
||||
## Examples
|
||||
|
||||
**Extract last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image last_frame.png
|
||||
```
|
||||
|
||||
**Extract specific frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image frame_100.png -Frame 100
|
||||
```
|
||||
|
||||
**Extract first frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image first_frame.png -Frame 0
|
||||
```
|
||||
|
||||
**Extract second-to-last frame:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image second_last.png -Frame -2
|
||||
```
|
||||
|
||||
**Extract 10th frame from the end:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image frame_minus_10.png -Frame -10
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video "C:\Videos\input.mp4" -Image "C:\Frames\output.png" -Frame 50
|
||||
```
|
||||
|
||||
**Filename pattern with the frame number:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image "frame_%04d.png"
|
||||
# If video has 100 frames, creates: frame_0099.png
|
||||
```
|
||||
|
||||
**Full output path with a pattern (directory is created if missing):**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image "C:\Output\frame_%04d.png" -Frame 50
|
||||
# Creates: C:\Output\frame_0050.png
|
||||
```
|
||||
|
||||
**Pattern with a negative frame index:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image "p_%06d.png" -Frame -2
|
||||
# If video has 100 frames, creates: p_000098.png
|
||||
```
|
||||
|
||||
**Pattern without padding:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image "f_%d.png" -Frame 7
|
||||
# Creates: f_7.png
|
||||
```
|
||||
|
||||
## Existing Files
|
||||
|
||||
An existing output PNG is **replaced** without a prompt. ffmpeg runs with `-y`.
|
||||
|
||||
- When `-Image` contains a numbering token such as `%04d`, the file replaced is the **resolved** name, not the pattern. For example, `-Image f_%04d.png -Frame 3` writes and replaces `f_0003.png`.
|
||||
- If the output folder does not exist, it is created. With `-WhatIf` it is only reported.
|
||||
- Use `-WhatIf` first, or write to a new name.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually extracting the frame. Shows the resolved output name and what would be extracted; no output directory is created.
|
||||
- **`-Confirm`**: Prompt for confirmation before extracting the frame.
|
||||
|
||||
**Simulate without extracting:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image frame.png -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before extraction:**
|
||||
|
||||
```powershell
|
||||
.\Get-LastFrame.ps1 -Video video.mp4 -Image frame.png -Frame 100 -Confirm
|
||||
```
|
||||
@@ -0,0 +1,84 @@
|
||||
# Get-VideoAudio.ps1
|
||||
|
||||
Extracts audio in either WAV or MP3 format from a video file.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file (absolute or relative)
|
||||
- **`-Output`** (optional): Path to output audio file (defaults to video filename with `.wav` or `.mp3` extension)
|
||||
- **`-MP3`**: Extract audio as MP3 at 320 kbps instead of WAV
|
||||
|
||||
## Examples
|
||||
|
||||
**Extract audio as WAV (default):**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4
|
||||
# Creates video.wav in the same directory
|
||||
```
|
||||
|
||||
**Extract audio as MP3:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4 -MP3
|
||||
# Creates video.mp3 in the same directory
|
||||
```
|
||||
|
||||
**Specify custom output path:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4 -Output audio\soundtrack.wav
|
||||
```
|
||||
|
||||
**Extract MP3 with custom output:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4 -Output music.mp3 -MP3
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video "C:\Videos\movie.mp4" -Output "D:\Audio\soundtrack.mp3" -MP3
|
||||
```
|
||||
|
||||
**Using relative paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video ..\videos\clip.mp4 -Output .\audio\clip.wav
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- If the video file has no audio stream, the script displays a message and exits without creating an output file
|
||||
- WAV format uses PCM 16-bit encoding (lossless)
|
||||
- MP3 format uses 320 kbps bitrate (highest quality)
|
||||
|
||||
## Existing Files
|
||||
|
||||
- An existing output file, the default `<video>.wav` or `<video>.mp3`, or the `-Output` path, is **replaced** without a prompt. ffmpeg runs with `-y`, so the old file is lost. Use `-WhatIf` first, or write to a new name.
|
||||
- If the output folder does not exist, ffmpeg fails and the script stops with an error.
|
||||
- If the video has no audio stream, the script prints a message and exits successfully. It creates no file and leaves any existing output untouched.
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually creating files. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before extracting audio.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4 -MP3 -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before extracting:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoAudio.ps1 -Video video.mp4 -Output soundtrack.wav -Confirm
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
Requires `ffmpeg` and `ffprobe` to be installed and available in the system PATH.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Get-VideoInfo.ps1
|
||||
|
||||
Extracts and displays video metadata from the comment tag in a video file's format metadata. The displayed fields are driven by `Get-VideoInfo.ini` (display name → JSON field name). The script also probes the video stream with ffprobe and reports the actual encoded resolution and frame rate alongside the metadata when they differ. Fields mapped to `frames_count` in the INI are always read directly from ffprobe rather than from the JSON metadata.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file. Supports Windows wildcards (`*` and `?`) to match multiple files (e.g., `*.mp4`, `video?.mov`). When wildcards are used, metadata is extracted and displayed for each matching file sequentially, with separators between files.
|
||||
- **`-Full`**: Output the parsed comment metadata as JSON (printed before the formatted block)
|
||||
|
||||
## Real vs metadata values
|
||||
|
||||
The script reads two pieces of information from ffprobe's video stream and uses them to enrich the formatted output:
|
||||
|
||||
- **Resolution**: any field whose value matches the `WxH` pattern (e.g. the metadata `resolution`) is shown with the actual encoded resolution appended when it differs — for example `Resolution: 720x1280 (real: 704x1280)`. WanGP / LTX-2 commonly record the requested resolution while the file is encoded at a slightly different size after the model's internal padding/cropping.
|
||||
- **Frame rate / duration**: the duration of the `video_length` field is always computed from ffprobe's real frame rate (`r_frame_rate` of the first video stream). The JSON metadata's `force_fps` is intentionally ignored because some WanGP/LTX presets store non-numeric mode markers there (e.g. `"control"`). If ffprobe doesn't return a usable frame rate, the script falls back to 24 fps.
|
||||
- **Actual frame count**: the `frames_count` field is always read directly from ffprobe (`nb_read_frames` of the first video stream) rather than from the JSON comment metadata. This gives the true encoded frame count of the file. Duration is computed using ffprobe's real frame rate, falling back to 24 fps if unavailable.
|
||||
|
||||
## Examples
|
||||
|
||||
**Display formatted metadata:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoInfo.ps1 -Video video.mp4
|
||||
```
|
||||
|
||||
Output example (metadata and actual resolution agree):
|
||||
|
||||
```text
|
||||
----------------------------------------
|
||||
File Name: video.mp4
|
||||
Text Prompt: Perform accurate continuous lip sync with the spoken audio. A young woman is talking. Her lips, mouth and jaw move precisely and continuously in sync with every word and syllable of the spoken audio. Her teeth and tongue are visible as she speaks. Her lips never stop or freeze while the audio is playing.
|
||||
Model: WanGP v10.9875 by DeepBeepMeep - LTX-2 2.3 Dev 22B
|
||||
Resolution: 1280x720
|
||||
Number of frames: 113 frames (4,7s, 24 fps)
|
||||
Video Length: 113 frames (4,7s, 24 fps)
|
||||
Seed: 153601012
|
||||
Guidance: 4
|
||||
Audio Guidance Scale: 7
|
||||
Modality Guidance: 3
|
||||
Guidance Rescale: 0,7
|
||||
Num Inference Steps: 40
|
||||
Creation Date: 2026-03-20 04:55:13
|
||||
Generation Time: 1209s (20m 9s)
|
||||
----------------------------------------
|
||||
```
|
||||
|
||||
Output example (metadata resolution differs from the actual encoded resolution):
|
||||
|
||||
```text
|
||||
----------------------------------------
|
||||
File Name: 2026-05-31-12h25m16s_seed777652600_Remove the text from the foreground..mp4
|
||||
Text Prompt: Remove the text from the foreground.
|
||||
Model: WanGP v11.88 by DeepBeepMeep - LTX-2 2.3 Distilled 1.1 22B
|
||||
Resolution: 720x1280 (real: 704x1280)
|
||||
Number of frames: 185 frames (6,2s, 30 fps)
|
||||
Video Length: 185 frames (6,2s, 30 fps)
|
||||
...
|
||||
----------------------------------------
|
||||
```
|
||||
|
||||
**Display full JSON metadata:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoInfo.ps1 -Video video.mp4 -Full
|
||||
```
|
||||
|
||||
**Using relative paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoInfo.ps1 -Video ..\videos\sample.mp4
|
||||
```
|
||||
|
||||
**Process multiple files with wildcards:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoInfo.ps1 -Video "*.mp4"
|
||||
```
|
||||
|
||||
**Process files matching a pattern:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoInfo.ps1 -Video "video?.mp4"
|
||||
.\Get-VideoInfo.ps1 -Video "C:\Videos\Project_*_v1.mp4"
|
||||
```
|
||||
@@ -0,0 +1,97 @@
|
||||
# Get-VideoSubtitles.ps1
|
||||
|
||||
Extracts the embedded subtitle tracks of a video file into the current directory, or lists them with `-List`.
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-Video`** (required): Path to input video file (absolute or relative)
|
||||
- **`-List`**: List the subtitle tracks (index, codec and output name) instead of extracting them. Creates no files
|
||||
|
||||
## Supported Formats
|
||||
|
||||
ffmpeg can copy text subtitles out of a container, and can copy Blu-ray bitmap subtitles to a `.sup` file. It cannot write DVD or DVB bitmap subtitles as a standalone file, and it cannot turn bitmaps into text without OCR.
|
||||
|
||||
| Container | Subtitle codec | Output | Notes |
|
||||
| --- | --- | --- | --- |
|
||||
| MKV | SubRip (`subrip`) | `.srt` | |
|
||||
| MKV, WebM | WebVTT (`webvtt`) | `.srt` | Converted to SubRip |
|
||||
| MP4, MOV, M4V | mov_text (`mov_text`) | `.srt` | Converted to SubRip |
|
||||
| MKV | ASS / SSA (`ass`) | `.ass` | Copied unchanged, styling is kept |
|
||||
| MKV, M2TS, TS | Blu-ray PGS (`hdmv_pgs_subtitle`) | `.sup` | Copied unchanged. Bitmap, not text |
|
||||
| MKV, VOB | DVD (`dvd_subtitle`) | not extracted | Listed as not supported |
|
||||
| TS | DVB (`dvb_subtitle`) | not extracted | Listed as not supported |
|
||||
| AVI | none | not extracted | AVI has no standard subtitle track |
|
||||
|
||||
Any other subtitle codec is listed as not supported and skipped with a warning.
|
||||
|
||||
## Output Names
|
||||
|
||||
Each extracted file is saved in the current directory, not next to the video. The name is built from the video name and the track language:
|
||||
|
||||
- `<video>.<language>.<extension>`, for example `movie.en.srt` for an English track
|
||||
- The language is the 2-letter ISO 639-1 code when one exists (`eng` becomes `en`, `fra` becomes `fr`). Otherwise it is the 3-letter code from the file (for example `fre`), or `und` when the track has no language tag
|
||||
- When two tracks produce the same name, the second and later ones get a counter: `movie.en.2.srt`, `movie.en.3.srt`
|
||||
|
||||
## Examples
|
||||
|
||||
**List the subtitle tracks without creating files:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video video.mkv -List
|
||||
```
|
||||
|
||||
**Extract all subtitle tracks to the current directory:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video video.mkv
|
||||
# Creates video.en.srt, video.fr.ass, ... in the current directory
|
||||
```
|
||||
|
||||
**Using absolute paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video "C:\Videos\movie.mkv"
|
||||
```
|
||||
|
||||
**Using relative paths:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video ..\videos\clip.mp4
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- If the video has no subtitle stream, the script displays a message and exits successfully. It creates no file
|
||||
- Each track is copied with ffmpeg in its own pass. Text tracks are written as SubRip (`.srt`), ASS tracks as `.ass`, and PGS tracks as `.sup`
|
||||
- Unsupported tracks produce a warning and the other tracks are still extracted
|
||||
- The video file itself is never changed
|
||||
|
||||
## Existing Files
|
||||
|
||||
- An existing file with the same name in the current directory is **replaced** without a prompt. ffmpeg runs with `-y`, so the old file is lost. Use `-WhatIf` first, or run from a different folder
|
||||
- If ffmpeg fails, or an expected output file is not created, the script stops with an error
|
||||
- `-List` creates and replaces nothing
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without creating files. Shows which files would be written.
|
||||
- **`-Confirm`**: Prompt for confirmation before each subtitle file is written.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video video.mkv -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before each file is written:**
|
||||
|
||||
```powershell
|
||||
.\Get-VideoSubtitles.ps1 -Video video.mkv -Confirm
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `ffmpeg` and `ffprobe` to be installed and available in the system PATH.
|
||||
- Language codes come from the file's language tags. Tracks without a tag are named with `und`.
|
||||
@@ -0,0 +1,98 @@
|
||||
# RIFE 4.26 on Windows 11 — Command-Line Installation Guide
|
||||
|
||||
There are two pieces you need: the **binary** (the `rife-ncnn-vulkan` executable) and the **model** (rife-v4.26 weights). The upstream nihui release is stuck at models up to v4.6, so you'll use **TNTwise's fork**, which maintains up-to-date model support including v4.26.
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Download the Binary
|
||||
|
||||
The actively maintained fork for newer models is TNTwise's `rife-ncnn-vulkan` at [https://github.com/TNTwise/rife-ncnn-vulkan](https://github.com/TNTwise/rife-ncnn-vulkan).
|
||||
|
||||
1. Go to: [https://github.com/TNTwise/rife-ncnn-vulkan/releases](https://github.com/TNTwise/rife-ncnn-vulkan/releases)
|
||||
2. Download the latest Windows zip, e.g. `rife-ncnn-vulkan-YYYYMMDD-windows.zip`
|
||||
3. Extract it to a folder, e.g. `C:\tools\rife-ncnn-vulkan\`
|
||||
|
||||
The package includes all binaries and models required — it is portable, so no CUDA or PyTorch runtime environment is needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add to PATH
|
||||
|
||||
To run `rife-ncnn-vulkan.exe` from any directory (instead of always `cd`-ing into the install folder), add the extraction folder to your system **PATH**:
|
||||
|
||||
```text
|
||||
C:\tools\rife-ncnn-vulkan\
|
||||
```
|
||||
|
||||
You must also define the environment variable **RIFE_HOME** and set it to the same folder:
|
||||
|
||||
```text
|
||||
RIFE_HOME=C:\tools\rife-ncnn-vulkan\
|
||||
```
|
||||
|
||||
> **Note:** `RIFE_HOME` must point to the same directory that is added to PATH. RIFE locates its model files via the `-m` command-line flag, not through environment variables, but `RIFE_HOME` is still required.
|
||||
|
||||
---
|
||||
|
||||
## Step 3 — Basic Usage (Interpolate Between Two Images)
|
||||
|
||||
To interpolate between two images and produce a middle frame:
|
||||
|
||||
```cmd
|
||||
rife-ncnn-vulkan.exe -0 frame0.png -1 frame1.png -o middle.png -m %RIFE_HOME%\rife-v4.26
|
||||
```
|
||||
|
||||
To process a whole directory of frames:
|
||||
|
||||
```cmd
|
||||
rife-ncnn-vulkan.exe -i input_frames\ -o output_frames\ -m %RIFE_HOME%\rife-v4.26
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Full CLI Reference
|
||||
|
||||
Key flags:
|
||||
|
||||
| Flag | Meaning |
|
||||
| ------ | --------- |
|
||||
| `-0 <path>` | First input image (jpg/png/webp) |
|
||||
| `-1 <path>` | Second input image |
|
||||
| `-o <path>` | Output image or directory |
|
||||
| `-m <path>` | Model path (point to your `rife-v4.26` folder) |
|
||||
| `-g <id>` | GPU device: `0`, `1`, `2` for GPU, **`-1` for CPU** |
|
||||
| `-j <n:n:n>` | Thread counts for load:proc:save |
|
||||
| `-n <count>` | Number of frames to generate between the two inputs (default: 1) |
|
||||
| `-v` | Verbose output |
|
||||
|
||||
---
|
||||
|
||||
## Running Without a GPU (CPU Mode)
|
||||
|
||||
You can run on CPU by setting `-g -1`.
|
||||
|
||||
```cmd
|
||||
rife-ncnn-vulkan.exe -0 frame0.png -1 frame1.png -o middle.png -m %RIFE_HOME%\rife-v4.26 -g -1
|
||||
```
|
||||
|
||||
You can also run on CPU, discrete GPU, and integrated GPU simultaneously:
|
||||
|
||||
```cmd
|
||||
rife-ncnn-vulkan.exe -i input_frames\ -o output_frames\ -g -1,-1,0,1 -j 2:4,4,2,1:4
|
||||
```
|
||||
|
||||
**Important caveat:** CPU mode works but is significantly slower — expect 10–30× slower processing compared to even an integrated GPU. For a single pair of 1080p frames it's tolerable; for video interpolation it will be very slow.
|
||||
|
||||
---
|
||||
|
||||
## Quick Test Checklist
|
||||
|
||||
```cmd
|
||||
# Verify the binary runs
|
||||
rife-ncnn-vulkan.exe -h
|
||||
|
||||
# Single frame interpolation test (CPU, no GPU needed)
|
||||
rife-ncnn-vulkan.exe -0 A.png -1 B.png -o middle.png -m %RIFE_HOME%\rife-v4.26 -g -1 -v
|
||||
```
|
||||
|
||||
If you get a Vulkan error with GPU mode, make sure your GPU drivers are up to date. CPU mode (`-g -1`) requires no Vulkan device at all.
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
name: video-processing-scripts
|
||||
description: Operates a collection of PowerShell 7 scripts that process video and image files with ffmpeg, ffprobe, rife-ncnn-vulkan and upscayl-bin. Covers clipping frame ranges, scene-based clip extraction, frame extraction to PNG, frame counts and video metadata, cropping and resizing to standard, LTX-2, Kling and Wan resolutions, pillar-boxing, side-by-side comparison, turnaround sheets, subtitle merging and subtitle extraction, audio extraction, AI image upscaling and RIFE frame interpolation. Use this skill when the user asks about video clipping, frame extraction, scene detection, cropping, upscaling, frame interpolation, subtitles, ffmpeg or RIFE workflows, the PowerShell video scripts, or preparing clips for Wan2GP or LTX models.
|
||||
---
|
||||
|
||||
# Video Processing PowerShell Scripts
|
||||
|
||||
A collection of PowerShell 7 scripts that wrap ffmpeg and ffprobe for video and image processing. Some scripts also call rife-ncnn-vulkan (frame interpolation) and upscayl-bin (AI upscaling).
|
||||
|
||||
The per-script pages sit in the same folder as this file. Load a page only when you need its details, using the links in the catalog below.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Platform**: Windows 11 only. The scripts are tested on Windows 11 and nowhere else. Linux and WSL are not supported. Run them with PowerShell 7 for Windows (`pwsh.exe`).
|
||||
- **PowerShell 7+** (`pwsh`). Required by every script.
|
||||
- **Core tools**: **ffmpeg** and **ffprobe** on `PATH`. Most scripts need both. [Stitch-InBetween.ps1](Stitch-InBetween.md) needs only ffmpeg. [Get-FramesCount.ps1](Get-FramesCount.md), [Get-VideoInfo.ps1](Get-VideoInfo.md) and [Create-FullSubtitle.ps1](Create-FullSubtitle.md) need only ffprobe.
|
||||
- **Optional tools**, needed only by specific scripts:
|
||||
- **rife-ncnn-vulkan** on `PATH`, with `RIFE_HOME` set. Required by [Stitch-InBetween.ps1](Stitch-InBetween.md). See the [RIFE installation guide](RIFE.md).
|
||||
- **upscayl-bin** on `PATH`, with `UPSCAYL_HOME` set. Required by [Clone-Image.ps1](Clone-Image.md) when `-Upscale` is used. See the [Upscalers installation guide](Upscalers.md).
|
||||
- **Tested with** ffmpeg n8.0.1-40 (GPL build, Windows 11). Do not claim that other versions work.
|
||||
|
||||
### Encoders and filters required
|
||||
|
||||
These encoders are used by the scripts. Check the ones a task needs before running it; see [Check the Environment](#check-the-environment).
|
||||
|
||||
| Encoder or filter | Used by | Needed for |
|
||||
| --- | --- | --- |
|
||||
| `libx264` | [Clone-Video.ps1](Clone-Video.md), [Compare-Videos.ps1](Compare-Videos.md), [Create-PillarBox.ps1](Create-PillarBox.md), [Create-ShortClip.ps1](Create-ShortClip.md), [Crop-Clips.ps1](Crop-Clips.md) | H.264 video in MP4 and MKV output |
|
||||
| `libx265` | [Create-ShortClip.ps1](Create-ShortClip.md) | Output of HEVC (H.265) sources |
|
||||
| `libvpx-vp9` | [Clone-Video.ps1](Clone-Video.md) | Video in WebM output |
|
||||
| `libopus` | [Clone-Video.ps1](Clone-Video.md) | Audio in WebM output |
|
||||
| `libmp3lame` | [Get-VideoAudio.ps1](Get-VideoAudio.md) | MP3 extraction (`-MP3`) |
|
||||
| `libaom-av1` | [Clone-Image.ps1](Clone-Image.md), [Create-Turnaround.ps1](Create-Turnaround.md) | AVIF images |
|
||||
| `libwebp` | [Clone-Image.ps1](Clone-Image.md), [Create-Turnaround.ps1](Create-Turnaround.md) | WebP images |
|
||||
|
||||
The filters used (for example `scale`, `crop`, `pad`, `hstack`, `vstack`, `select`, `concat`) are built into ffmpeg. No script needs `libass` or `libfreetype`.
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Purpose | Accepted values | Required by |
|
||||
| --- | --- | --- | --- |
|
||||
| `RIFE_HOME` | Install folder of rife-ncnn-vulkan (the same folder as its `PATH` entry). Models are read from `%RIFE_HOME%\rife-v4.26`. | Folder path, e.g. `C:\tools\rife-ncnn-vulkan\` | [Stitch-InBetween.ps1](Stitch-InBetween.md) |
|
||||
| `UPSCAYL_HOME` | Install folder of upscayl-bin (the same folder as its `PATH` entry). Models are read from `%UPSCAYL_HOME%\models`. | Folder path | [Clone-Image.ps1](Clone-Image.md) with `-Upscale` |
|
||||
| `NCNN_GPU_ID` | GPU device passed to the `-g` flag of rife-ncnn-vulkan and upscayl-bin. | `0`, `1`, `2`... (GPU index), `-1` (CPU for rife-ncnn-vulkan; upscayl-bin gets no `-g` flag and uses its default device) | [Stitch-InBetween.ps1](Stitch-InBetween.md), [Clone-Image.ps1](Clone-Image.md) with `-Upscale` |
|
||||
|
||||
These must be persistent user or system variables. An agent shell may not inherit variables set only in an interactive session.
|
||||
|
||||
## Check the Environment
|
||||
|
||||
Run the core checks once before the first use in a session:
|
||||
|
||||
```powershell
|
||||
pwsh --version
|
||||
ffmpeg -version
|
||||
ffprobe -version
|
||||
```
|
||||
|
||||
Run the task-specific checks only when the task uses the script that needs them:
|
||||
|
||||
- Stitch-InBetween.ps1: `Get-Command rife-ncnn-vulkan`, `$env:RIFE_HOME` and `$env:NCNN_GPU_ID`.
|
||||
- Clone-Image.ps1 with `-Upscale`: `Get-Command upscayl-bin`, `$env:UPSCAYL_HOME` and `$env:NCNN_GPU_ID`.
|
||||
- Encoder checks, only for the task that needs the encoder (see the table above). For example, before an MP3 extraction: `ffmpeg -hide_banner -encoders | Select-String libmp3lame`. A missing encoder stops only the task that needs it.
|
||||
|
||||
If a core check fails, or a check needed by the current task fails, **stop**. Tell the user exactly what is missing and point to [index.md](index.md) (prerequisites), [RIFE.md](RIFE.md) or [Upscalers.md](Upscalers.md). Do not install software or change environment variables without the user's approval. A missing optional tool must not block unrelated tasks such as clipping.
|
||||
|
||||
## Installation and Location
|
||||
|
||||
- Git: `git clone https://git.gurusushi.dyndns.org/guru/Video-Processing-Scripts.git` (repository `guru/Video-Processing-Scripts`).
|
||||
- SVN: `public-svn`, project `Video Processing Scripts`.
|
||||
- The script folder should be on `PATH` so that `Get-Command` finds the scripts and the `.cmd` wrappers work. This does not make `pwsh -File <bare name>` work. See [How to Run the Scripts](#how-to-run-the-scripts).
|
||||
- `Update-Video-Processing-Scripts.cmd` updates a Git copy to the latest version. Run it only with the user's approval.
|
||||
|
||||
## How to Run the Scripts
|
||||
|
||||
- Always call a script as `pwsh -NoProfile -File <full path to script>.ps1 <parameters>`. `pwsh -File` does not search `PATH`, so a bare name fails from any other folder. Find the script folder once with `Split-Path (Get-Command Create-Clip.ps1).Source`.
|
||||
- In the examples below, `.\` means the script folder. From any other folder, replace `.\` with the full script path.
|
||||
- The `.cmd` wrappers are Windows-only convenience files. Do not use them in agent commands.
|
||||
- Quote every path that contains spaces.
|
||||
- Paths can be absolute or relative to the current directory.
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Create-Clip.ps1 -Video "My Video.mp4" -Frames "100-200"
|
||||
```
|
||||
|
||||
From any other folder, use the full script path and an absolute video path:
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File "C:\path\to\Video Processing Scripts\Create-Clip.ps1" -Video "D:\clips\P01.mp4" -Frames "100-200"
|
||||
```
|
||||
|
||||
## Conventions
|
||||
|
||||
- Frame indices are **0-based**: the first frame is `0`.
|
||||
- **`-Frames` syntax** for [Clone-Video.ps1](Clone-Video.md), [Create-Clip.ps1](Create-Clip.md) and [Get-FramesRange.ps1](Get-FramesRange.md):
|
||||
|
||||
| Form | Meaning |
|
||||
| --- | --- |
|
||||
| `start-end` | Frames from `start` to `end`, e.g. `100-200`. Both ends must be inside the video. A reversed range or an end past the last frame is rejected with exit code `1`. |
|
||||
| `N` | Frames 0 to `N`, not only frame `N`. A number past the last frame is clamped to the last frame. |
|
||||
| `-N` | The last `N` frames. `-1` is the last frame. |
|
||||
| `%` | The last frame index. Alone it selects only the last frame. In a range, e.g. `10-%`, it is the end of the range. |
|
||||
| `-%` | Not supported. Rejected with exit code `1`. |
|
||||
| `all` | All frames, same as `0-%` |
|
||||
|
||||
- [Create-Scenes.ps1](Create-Scenes.md) takes no frame range. It finds scene boundaries itself.
|
||||
- **`-Frame` syntax** for [Get-Frame.ps1](Get-Frame.md) and [Get-LastFrame.ps1](Get-LastFrame.md): a single frame index. Negative values count from the end (`-1` is the last frame). [Get-Frame.ps1](Get-Frame.md) clamps values to the valid range: a positive value past the last frame gives the last frame, and a negative value before the first frame gives frame 0. [Get-LastFrame.ps1](Get-LastFrame.md) clamps the same way.
|
||||
- If the extension is omitted, `.mp4`, `.webm` and `.mkv` are tried in that order by [Clone-Video.ps1](Clone-Video.md), [Create-Clip.ps1](Create-Clip.md) and [Create-Scenes.ps1](Create-Scenes.md).
|
||||
- **`.skip` marker**: a file named `<name>.skip.<ext>` is excluded from batch processing, and the marker stays last in derived names.
|
||||
- [Create-Clip.ps1](Create-Clip.md) accepts `.mp4`, `.webm` and `.mkv`, case-insensitive. `P01.skip.mp4` gives `P01-Clip.skip.mp4`.
|
||||
- [Crop-Clips.ps1](Crop-Clips.md) processes only `.mp4` files and skips `*.skip.mp4` and `*.bak.mp4`.
|
||||
- [Create-Scenes.ps1](Create-Scenes.md) gives `P01-Scene-01.skip.mp4` for `P01.skip.mp4`.
|
||||
- On failure, scripts exit with `1`, print an error message to the error stream, and create no files. Check `$LASTEXITCODE` after each call.
|
||||
- **Forwarded parameters**: [Create-Clip.ps1](Create-Clip.md) and [Create-Scenes.ps1](Create-Scenes.md) accept the extra parameters of [Clone-Video.ps1](Clone-Video.md) and forward them, for example `-Size`, `-FPS`, `-NoAudio`, `-Audio`, `-CRF`, `-Metadata`, `-Reverse` and `-LTX`. Each reserves `-Video`, `-Frames`, `-Suffix` and `-Target` for itself, so do not repeat them in the extra parameters.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- **Never pass `-Confirm`.** Scripts run without interaction. Only `-Confirm` makes them prompt. No script sets a `ConfirmImpact` of `High`, so none prompts by default. [Create-Scenes.ps1](Create-Scenes.md) sets `Medium`.
|
||||
- Before any script that creates, overwrites or deletes files, run it with **`-WhatIf`**. Show the user the result, then run it for real only if the user agrees.
|
||||
- Never run a script on files the user has not designated. Never delete source videos.
|
||||
- Overwrite and delete behavior of every script, as stated on its page:
|
||||
|
||||
| Script | Behavior, as documented |
|
||||
| --- | --- |
|
||||
| [Clone-Image.ps1](Clone-Image.md) | An existing `-Target` or `-Keep` file is replaced. Both folders must exist. |
|
||||
| [Clone-Video.ps1](Clone-Video.md) | An existing `-Target` is replaced without a prompt. The folder of `-Target` must exist. |
|
||||
| [Compare-Videos.ps1](Compare-Videos.md) | An existing `-Output` is replaced. Its folder must exist. |
|
||||
| [Create-Clip.ps1](Create-Clip.md) | An existing derived clip is replaced without a prompt. The clip is written by Clone-Video.ps1, with the `.skip` marker kept last in the name. |
|
||||
| [Create-FullSubtitle.ps1](Create-FullSubtitle.md) | Overwrites `-Output` without a prompt. |
|
||||
| [Create-PillarBox.ps1](Create-PillarBox.md) | An existing `-Target` video is replaced without a prompt. If ffmpeg fails, the target is removed, including a file that existed before the run. |
|
||||
| [Create-Scenes.ps1](Create-Scenes.md) | Without `-List`, deletes the earlier scene clips of the same source in the source folder, then creates the new ones. `-List` creates and deletes nothing. |
|
||||
| [Create-ShortClip.ps1](Create-ShortClip.md) | Overwrites the output video. With the default `-Images` and without `-Render`, clears the `Frames` folder. With explicit `-Images`, deletes only matching frames. Removes its temporary folders, and the `Frames` folder unless `-Keep` is given. |
|
||||
| [Create-Turnaround.ps1](Create-Turnaround.md) | An existing `-Target` image is replaced without a prompt. The folder of `-Target` must exist. |
|
||||
| [Crop-Clips.ps1](Crop-Clips.md) | Works in place when `-Source` and `-Target` are omitted or equal. See the in-place rule below. |
|
||||
| [Get-Frame.ps1](Get-Frame.md) | An existing PNG with the same name is replaced. |
|
||||
| [Get-FramesCount.ps1](Get-FramesCount.md) | Reads only. Creates and deletes nothing. |
|
||||
| [Get-FramesRange.ps1](Get-FramesRange.md) | An existing PNG frame with the same name is replaced. |
|
||||
| [Get-LastFrame.ps1](Get-LastFrame.md) | An existing output PNG is replaced without a prompt. With a numbering token in `-Image`, the resolved name is replaced, not the pattern. |
|
||||
| [Get-VideoAudio.ps1](Get-VideoAudio.md) | The output file, default or from `-Output`, is replaced. |
|
||||
| [Get-VideoInfo.ps1](Get-VideoInfo.md) | Reads only. Creates and deletes nothing. |
|
||||
| [Get-VideoSubtitles.ps1](Get-VideoSubtitles.md) | An existing file with the same name in the current directory is replaced. `-List` creates nothing. |
|
||||
| [Stitch-InBetween.ps1](Stitch-InBetween.md) | Deletes an existing `-Video` before generating. Deletes everything inside an existing `-TempDir`. |
|
||||
|
||||
- **Crop-Clips in-place rule.** Crop-Clips works on the current folder when `-Source` and `-Target` are omitted or equal, and it backs up each file it crops to `<name>.bak.mp4`, replacing any existing backup without a prompt. Skipped files are not backed up and keep their backup. A re-run with a different `-Size` crops the already cropped files again, so the backup then holds the previous cropped output and the true original is lost. So:
|
||||
- Always pass explicit `-Source` and `-Target`, and prefer a separate `-Target` folder. Never pass the same folder for both.
|
||||
- Use in-place mode only when the user asks for it.
|
||||
- Warn the user before re-running in place with a different `-Size` on files that already have a `.bak.mp4`.
|
||||
- Never delete `.bak.mp4` files yourself unless the user asks.
|
||||
- **Stitch-InBetween `-TempDir` rule.** The script deletes everything inside an existing `-TempDir`. Never pass an existing folder. Omit `-TempDir` unless the user designates a dedicated empty folder.
|
||||
|
||||
## Script Catalog
|
||||
|
||||
| Script | Purpose | Typical use |
|
||||
| --- | --- | --- |
|
||||
| [Clone-Image.ps1](Clone-Image.md) | Resize (cover-scale then crop), convert formats, set quality and compression, copy metadata, AI-upscale with `-Upscale` and optionally keep the upscaled image with `-Keep`, use LTX-2 sizes with `-LTX` | `-Source small.jpg -Target big_4k.png -Upscale 4 -Size 4K` |
|
||||
| [Clone-Video.ps1](Clone-Video.md) | Re-encode or convert video (`.mp4`, `.webm`, `.mkv`) with frame range, size, FPS, audio, CRF, `-Crop`, `-Reverse`, `-LTX` and metadata options. Also builds a video from one image plus `-Audio`, builds a video from a folder of PNG files, and extracts one frame to PNG with `-Sequence` | `-Source in.mp4 -Target out.mp4 -Frames "0-100"` |
|
||||
| [Compare-Videos.ps1](Compare-Videos.md) | Side-by-side or grid comparison of 2 to 4 videos at 1080p or 4K, with `-Clip`, `-Sideways` and `-NoAudio` | `-S1 a.mp4 -S2 b.mp4 -Output cmp.mp4` |
|
||||
| [Create-Clip.ps1](Create-Clip.md) | Clip a frame range. The output name is the source name plus `-Suffix` (default `-Clip`). Forwards extra parameters to Clone-Video.ps1 | `-Video P01.mp4 -Frames "100-200"` |
|
||||
| [Create-FullSubtitle.ps1](Create-FullSubtitle.md) | Merge two SRT files into one, with timing taken from the video duration | `-Video v.mp4 -Subtitle1 a.srt -Subtitle2 b.srt -Output out.srt` |
|
||||
| [Create-PillarBox.ps1](Create-PillarBox.md) | Wrap a video in a 16:9 frame with side-bar effects (solid colours, or blurred backgrounds from the video). `-Vertical` gives a 9:16 frame for square sources | `-Video v.mp4 -Effect black` |
|
||||
| [Create-Scenes.ps1](Create-Scenes.md) | Detect scene changes and create one clip per scene. `-List` only shows the scenes. `-Scenes` searches the level for a given scene count. Forwards extra parameters to Create-Clip.ps1 and Clone-Video.ps1 | `-Video P01.mp4 -Level 0.3 -List` |
|
||||
| [Create-ShortClip.ps1](Create-ShortClip.md) | Build a vertical short clip from an INI plan, with optional overlay, colour, smoothing and speed. `-Render` builds the video from existing frames | `-Video v.mp4 -Plan plan.ini` |
|
||||
| [Create-Turnaround.ps1](Create-Turnaround.md) | Arrange same-sized images into a row or grid turnaround sheet, with padding colour, quality and compression options | `-Source "C:\renders\CharacterA" -Target sheet.png` |
|
||||
| [Crop-Clips.ps1](Crop-Clips.md) | Batch crop LTX-2, Kling and Wan videos to standard sizes. `-Size` fits every video to a chosen size. Square videos become 1080x1080 | `-Source in_dir -Target out_dir` |
|
||||
| [Get-Frame.ps1](Get-Frame.md) | Extract one frame to PNG with an automatic name (`<video>-<frame>.png`). Negative indices count from the end | `-Video P01.mp4 -Frame 10` |
|
||||
| [Get-FramesCount.ps1](Get-FramesCount.md) | Print the total frame count of a video | `-Video P01.mp4` |
|
||||
| [Get-FramesRange.ps1](Get-FramesRange.md) | Extract a frame range to PNG files, with a custom name pattern through `-Images` | `-Video P01.mp4 -Frames "0-50"` |
|
||||
| [Get-LastFrame.ps1](Get-LastFrame.md) | Extract one frame (default: the last) to a chosen PNG name. `-Image` can include a numbering token | `-Video P01.mp4 -Image last.png` |
|
||||
| [Get-VideoAudio.ps1](Get-VideoAudio.md) | Extract audio as WAV (default) or MP3 (`-MP3`) | `-Video P01.mp4 -MP3` |
|
||||
| [Get-VideoInfo.ps1](Get-VideoInfo.md) | Show metadata stored in video comment tags. Accepts wildcards. `-Full` adds JSON output | `-Video *.mp4` |
|
||||
| [Get-VideoSubtitles.ps1](Get-VideoSubtitles.md) | Extract embedded text (SRT, ASS) and Blu-ray PGS subtitle tracks to the current directory. DVD and DVB bitmap tracks are listed but not extracted. `-List` only shows the tracks | `-Video P01.mkv -List` |
|
||||
| [Stitch-InBetween.ps1](Stitch-InBetween.md) | Generate in-between frames between two images with RIFE, then encode them to video | `-First a.png -Last b.png -Frames 30 -Video out.mp4` |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
**Clip a frame range** (count frames, preview, then run):
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Get-FramesCount.ps1 -Video P01.mp4
|
||||
pwsh -NoProfile -File .\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200" -WhatIf
|
||||
pwsh -NoProfile -File .\Create-Clip.ps1 -Video P01.mp4 -Frames "100-200"
|
||||
```
|
||||
|
||||
**Split a video into scenes, with forwarded parameters** (list first, then create clips scaled to 720p without audio):
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Level 0.3 -List
|
||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Scenes 4 -Size 720p -NoAudio -WhatIf
|
||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Scenes 4 -Size 720p -NoAudio
|
||||
```
|
||||
|
||||
**Extract the last frame to a named PNG:**
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Get-LastFrame.ps1 -Video P01.mp4 -Image last.png
|
||||
```
|
||||
|
||||
**Upscale an image** (needs `UPSCAYL_HOME`, `NCNN_GPU_ID` and the model files):
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Clone-Image.ps1 -Source small.jpg -Target big_4k.png -Upscale 4 -Size 4K
|
||||
```
|
||||
|
||||
**Interpolate between two images with RIFE** (needs `RIFE_HOME` and `NCNN_GPU_ID`):
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\Stitch-InBetween.ps1 -First frame1.png -Last frame2.png -Frames 30 -Video output.mp4 -FPS 24 -WhatIf
|
||||
pwsh -NoProfile -File .\Stitch-InBetween.ps1 -First frame1.png -Last frame2.png -Frames 30 -Video output.mp4 -FPS 24
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **"Video file not found"**: check the path. Use an absolute path if relative paths fail. See [index.md](index.md).
|
||||
- **"ffmpeg failed" or "ffprobe failed"**: run `ffmpeg -version` and `ffprobe -version`. Check that the file is not corrupt and that the disk has space. See [index.md](index.md).
|
||||
- **"Frame index out of range"**: run [Get-FramesCount.ps1](Get-FramesCount.md). Indices are 0-based.
|
||||
- **`-Image` rejected by Get-Frame**: `-Image` is not a [Get-Frame.ps1](Get-Frame.md) parameter. Use [Get-LastFrame.ps1](Get-LastFrame.md) for custom names.
|
||||
- **Missing `RIFE_HOME`, `UPSCAYL_HOME` or `NCNN_GPU_ID`**: set the persistent variable as described above. Then start a new shell.
|
||||
- **Upscale fails with missing models**: the model files `<Model>.bin` and `<Model>.param` must be in `%UPSCAYL_HOME%\models`. See [Clone-Image.md](Clone-Image.md) and [Upscalers.md](Upscalers.md).
|
||||
@@ -0,0 +1,108 @@
|
||||
# Stitch-InBetween.ps1
|
||||
|
||||
Generates interpolated in-between frames between two images using RIFE and encodes them into a video.
|
||||
|
||||
## Synopsis
|
||||
|
||||
Uses `rife-ncnn-vulkan` to interpolate frames between a first and last image, then encodes the result into an H.264 video using ffmpeg.
|
||||
|
||||
The script:
|
||||
|
||||
1. Validates that the input image files exist
|
||||
2. Creates a dedicated temporary directory for input images
|
||||
3. Copies the first and last images to the dedicated temp directory
|
||||
4. Runs `rife-ncnn-vulkan.exe` to generate interpolated frames
|
||||
5. Filters the generated frames to keep only the desired in-between sequence
|
||||
6. Encodes the remaining frames into a video at the specified FPS
|
||||
|
||||
## Parameters
|
||||
|
||||
- **`-First`** (required): Full or relative path to the first image file (e.g., `First.png` or `D:\Images\First.png`)
|
||||
- **`-Last`** (required): Full or relative path to the last image file (e.g., `Last.png` or `D:\Images\Last.png`)
|
||||
- **`-TempDir`** (optional): Full absolute path to a temporary directory for intermediate PNG frames. Must end with `\`. Defaults to a system-generated temp subdirectory. This is separate from the dedicated input temp directory.
|
||||
- **`-Frames`** (required): Number of frames to generate in the output video. Must be greater than 0
|
||||
- **`-Video`** (required): Output video filename. If relative, resolved against current directory
|
||||
- **`-FPS`** (optional): Frames per second for the output video (default: `24`)
|
||||
|
||||
## Frame Generation
|
||||
|
||||
The number of frames passed to `rife-ncnn-vulkan` is computed as:
|
||||
|
||||
```text
|
||||
Count = 4 + (($Frames - 1) * 2)
|
||||
EndsAt = Count / 2
|
||||
```
|
||||
|
||||
After generation, `rife-ncnn-vulkan` outputs files named `%08d.png`. The script deletes:
|
||||
|
||||
- `00000001.png` (copy of the first input image)
|
||||
- All files with index greater than `EndsAt` (copy of the last input image and beyond)
|
||||
|
||||
Only the true interpolated frames (`00000002.png` through `00000{EndsAt}.png`) are passed to ffmpeg.
|
||||
|
||||
## Examples
|
||||
|
||||
**Basic interpolation:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "frame1.png" -Last "frame2.png" -Frames 30 -Video "output.mp4"
|
||||
```
|
||||
|
||||
**Full paths with custom FPS:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "D:\Images\start.png" -Last "D:\Images\end.png" -Frames 60 -Video "result.mp4" -FPS 30
|
||||
```
|
||||
|
||||
**Absolute paths for input and output:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "C:\Work\Frames\A.png" -Last "C:\Work\Frames\B.png" -Frames 24 -Video "D:\Output\clip.mp4"
|
||||
```
|
||||
|
||||
**Custom temp directory:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "A.png" -Last "B.png" -Frames 24 -Video "clip.mp4" -TempDir "D:\Temp\rife\"
|
||||
```
|
||||
|
||||
## Safe Execution
|
||||
|
||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||
|
||||
- **`-WhatIf`**: Simulate the operation without actually generating frames or creating the video. Shows what would be done.
|
||||
- **`-Confirm`**: Prompt for confirmation before clearing temp directory, generating frames, and creating the video.
|
||||
|
||||
**Simulate without making changes:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "frame1.png" -Last "frame2.png" -Frames 30 -Video "output.mp4" -WhatIf
|
||||
```
|
||||
|
||||
**Prompt before operations:**
|
||||
|
||||
```powershell
|
||||
.\Stitch-InBetween.ps1 -First "frame1.png" -Last "frame2.png" -Frames 30 -Video "output.mp4" -Confirm
|
||||
```
|
||||
|
||||
## Existing Files and Temporary Directory
|
||||
|
||||
- **`-Video`**: if a file already exists at this path, it is deleted before generation starts. The old file is gone even if the generation fails later. Use `-WhatIf` to see the deletion first.
|
||||
- **`-TempDir`**: if the folder already exists, **everything inside it is deleted recursively** before generation starts. Pass a dedicated folder that holds nothing you need. The default is a new folder under the system temporary directory, so no existing file is deleted.
|
||||
- The input frames are copied to a second temporary folder under the system temporary directory. This folder is deleted after rife-ncnn-vulkan finishes successfully.
|
||||
- Intermediate frames in `-TempDir` are deleted as described in [Frame Generation](#frame-generation).
|
||||
|
||||
## Notes
|
||||
|
||||
- Requires `rife-ncnn-vulkan.exe` (TNTwise fork, RIFE 4.26) in the system PATH
|
||||
- Requires `ffmpeg` in the system PATH
|
||||
- Requires the `RIFE_HOME` environment variable pointing to the rife-ncnn-vulkan install directory (the same folder that is on PATH). The script resolves the model directory from `%RIFE_HOME%\rife-v4.26`.
|
||||
- Requires the `NCNN_GPU_ID` environment variable set to the desired GPU device ID passed to rife-ncnn-vulkan via the `-g` flag:
|
||||
- `-1` — CPU only (no Vulkan GPU required).
|
||||
- `0` — First GPU.
|
||||
- `1`, `2`, etc. — Additional GPUs.
|
||||
- The script creates a dedicated temporary directory for input images, separate from the `-TempDir` parameter
|
||||
- Input images can be located anywhere on the filesystem and are copied to the dedicated temp directory
|
||||
- The dedicated input temp directory is automatically cleaned up after frame generation
|
||||
- The output temp directory (`-TempDir`) is created if it does not exist; its contents are always cleared before each run
|
||||
- Output codec: libx264, CRF 8, pixel format yuv420p
|
||||
@@ -0,0 +1,55 @@
|
||||
# Upscalers - upscayl-bin Windows 11 — Command-Line Installation Guide
|
||||
|
||||
There are two pieces you need: the **binary** (the `upscayl-bin` executable) and the **model** (upscayl models).
|
||||
|
||||
---
|
||||
|
||||
## Step 1 — Download the Binary
|
||||
|
||||
The upscayl-bin binary can be downloaded from the official upscayl repository or from the releases page.
|
||||
|
||||
1. Go to: [https://github.com/TNTwise/Universal-NCNN-Upscaler/releases](https://github.com/TNTwise/Universal-NCNN-Upscaler/releases)
|
||||
2. Download the latest Windows zip, e.g. `upscayl-bin-YYYYMMDD-windows.zip`
|
||||
3. Extract it to a folder, e.g. `C:\tools\upscayl-bin\`
|
||||
|
||||
The package includes all binaries required — it is portable, so no CUDA or PyTorch runtime environment is needed.
|
||||
|
||||
---
|
||||
|
||||
## Step 2 — Add to PATH
|
||||
|
||||
To run `upscayl-bin.exe` from any directory (instead of always `cd`-ing into the install folder), add the extraction folder to your system **PATH**:
|
||||
|
||||
```text
|
||||
C:\tools\upscayl-bin\
|
||||
```
|
||||
|
||||
Then open a new terminal and verify:
|
||||
|
||||
```powershell
|
||||
upscayl-bin -h
|
||||
```
|
||||
|
||||
## Step 3 — Download Models
|
||||
|
||||
The models can be downloaded from the links found in the `README.md` file of the upscayl-bin directory.
|
||||
|
||||
The default model used by the scripts is the `4xNomos8kSC` model.
|
||||
|
||||
1. Go to: [https://github.com/TNTwise/Upscaling-Models-NCNN/releases/tag/Realistic](https://github.com/TNTwise/Upscaling-Models-NCNN/releases/tag/Realistic)
|
||||
2. Download the `4xNomo8kSC.zip` file
|
||||
3. Extract the 2 files (`4xNomos8kSC.bin` and `4xNomos8kSC.param`) directly to the root of the **models** directory (create it if it doesn't exist) in `upscayl-bin`, e.g. `C:\tools\upscayl-bin\models\`
|
||||
|
||||
You can also download other models if you want to use them instead of the default `4xNomos8kSC` model. They should be extracted to the same `models` directory.
|
||||
|
||||
---
|
||||
|
||||
## Step 4 - Set UPSCAYL_HOME
|
||||
|
||||
The scripts that use upscayl-bin (for example `Clone-Image.ps1 -Upscale`) read the environment variable **UPSCAYL_HOME**. Set it to the same folder that is on PATH. The models are read from `%UPSCAYL_HOME%\models`:
|
||||
|
||||
```text
|
||||
UPSCAYL_HOME=C:\tools\upscayl-bin\
|
||||
```
|
||||
|
||||
Set the variable as a persistent user or system environment variable, then open a new terminal. The scripts also need `NCNN_GPU_ID`; see [NCNN_GPU_ID](index.md#ncnn_gpu_id).
|
||||
@@ -0,0 +1,142 @@
|
||||
# Video Processing PowerShell Scripts
|
||||
|
||||
A collection of PowerShell scripts for video processing tasks using ffmpeg and ffprobe.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **PowerShell 7+** (pwsh.exe)
|
||||
- **Platform**: tested on Windows 11 only. Linux and WSL are not tested and not supported.
|
||||
- **ffmpeg** - Must be in system PATH. Not needed by Get-FramesCount.ps1, Get-VideoInfo.ps1 and Create-FullSubtitle.ps1.
|
||||
- **ffprobe** - Must be in system PATH. Not needed by Stitch-InBetween.ps1.
|
||||
- **rife-ncnn-vulkan** - Must be in system PATH and also defined the environment variable **RIFE_HOME** to the same directory
|
||||
- **upscayl-bin** - Must be in system PATH and also defined the environment variable **UPSCAYL_HOME** to the same directory
|
||||
|
||||
## ffmpeg
|
||||
|
||||
To install ffmpeg on Windows, download it from [ffmpeg.org](https://ffmpeg.org/) and add it to your system PATH. The latest release is available from [BtbN FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds/). The Windows build used here is the `win64-gpl` flavor.
|
||||
|
||||
## rife-ncnn-vulkan
|
||||
|
||||
To install rife-ncnn-vulkan on Windows, see the [RIFE 4.26 installation guide](RIFE.md). Add it to your system PATH.
|
||||
|
||||
## upscayl-bin
|
||||
|
||||
To install upscayl-bin on Windows, see the [Upscalers installation guide](Upscalers.md). Add it to your system PATH.
|
||||
|
||||
## NCNN_GPU_ID
|
||||
|
||||
The environment variable **NCNN_GPU_ID** is used to specify the GPU device ID for ncnn-based applications.
|
||||
|
||||
This environment variable must be set for both `rife-ncnn-vulkan` and `upscayl-bin`.
|
||||
|
||||
The value stored in **NCNN_GPU_ID** will be either:
|
||||
|
||||
- **`0`** — use the first Vulkan-capable GPU (device index 0). If you have multiple GPUs, they'd be `0`, `1`, `2`, etc.
|
||||
- **`-1`** — use CPU only for `rife-ncnn-vulkan` (no Vulkan GPU is available). `upscayl-bin` rejects `-g -1` on the tested build, so the scripts omit its `-g` flag for `-1`, and it then uses its default device.
|
||||
|
||||
These map directly to the `-g` flag values accepted by both `rife-ncnn-vulkan` and `upscayl-bin`:
|
||||
|
||||
| `-g` value | Meaning |
|
||||
| --- | --- |
|
||||
| `0` | First GPU |
|
||||
| `1` | Second GPU |
|
||||
| `2` | Third GPU |
|
||||
| `-1` | CPU for `rife-ncnn-vulkan`; no `-g` flag for `upscayl-bin` (its default device) |
|
||||
|
||||
---
|
||||
|
||||
## Scripts Overview
|
||||
|
||||
| Script | Purpose |
|
||||
| :--- | :--- |
|
||||
| Clone-Image.ps1 | Clone/copy image files with optional resizing (cover-scale then crop), format conversion, and quality/compression settings |
|
||||
| Clone-Video.ps1 | Clone/copy video files with optional frame range and sequence extraction |
|
||||
| Compare-Videos.ps1 | Combine 2 to 4 video files into a single 1920x1080 side-by-side or grid comparison video |
|
||||
| Create-Clip.ps1 | Create a clip of a frame range from a video, named from the source with a suffix (default `-Clip`) |
|
||||
| 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-PillarBox.ps1 | Wrap a video in a 16:9 frame with selectable side-bar effects (black, white, blurred-background variants) |
|
||||
| Create-Scenes.ps1 | Detect scene changes with ffmpeg and extract each scene as a separate clip using Create-Clip.ps1 |
|
||||
| Create-ShortClip.ps1 | Create short clips from videos with cropping and overlay options |
|
||||
| Create-Turnaround.ps1 | Combine a sequence of images into a single turnaround (character reference sheet) image, in a row or a grid |
|
||||
| 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-Frame.ps1 | Extract a specific frame of a video to PNG, with the image name derived from the video and frame index |
|
||||
| Get-FramesCount.ps1 | Get total frame count of a video |
|
||||
| Get-FramesRange.ps1 | Extract a range of frames to PNG images |
|
||||
| Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG, with an optional filename pattern |
|
||||
| Get-VideoAudio.ps1 | Extracts audio in either WAV or MP3 format from a video file |
|
||||
| Get-VideoInfo.ps1 | Extract and display video metadata from comment tags |
|
||||
| Get-VideoSubtitles.ps1 | Extract the embedded subtitle tracks of a video to SRT, ASS or SUP files in the current directory, or list them with `-List` |
|
||||
| Stitch-InBetween.ps1 | Generate interpolated in-between frames between two images using RIFE and encode to video |
|
||||
|
||||
**Important**:
|
||||
|
||||
- All scripts require PowerShell 7+ (use `pwsh.exe`)
|
||||
- Each script has its own Windows command file (`.cmd`) that calls the PowerShell script
|
||||
|
||||
## Scripts Installation
|
||||
|
||||
The scripts are available from two locations:
|
||||
|
||||
- SVN: **public-svn**, project: **Video Processing Scripts**.
|
||||
- Git: **git.gurusushi.dyndns.org**, repository: **guru/Video-Processing-Scripts**.
|
||||
|
||||
```bash title="Clone the repository from Git"
|
||||
git clone https://git.gurusushi.dyndns.org/guru/Video-Processing-Scripts.git
|
||||
```
|
||||
|
||||
**Note**: there is a `Update-Video-Processing-Scripts.cmd` script that can be used to update the directory from Git to get the latest version.
|
||||
|
||||
Add the project folder to your PATH environment variable to access the scripts from any location.
|
||||
|
||||
---
|
||||
|
||||
## Agent Skill
|
||||
|
||||
[SKILL.md](SKILL.md) is an agent skill for AI coding agents (such as Claude Code, Devin, VS Code, etc.). It teaches the agent how to use these scripts safely, so the agent does not have to rediscover the documentation for each task.
|
||||
|
||||
What it covers:
|
||||
|
||||
- **Prerequisites and environment**: PowerShell 7+, ffmpeg, ffprobe, rife-ncnn-vulkan and upscayl-bin, and the `RIFE_HOME`, `UPSCAYL_HOME` and `NCNN_GPU_ID` variables. The agent checks them once before first use, and stops to ask you if something is missing instead of installing it.
|
||||
- **Safety rules**: it always previews with `-WhatIf` first, never passes `-Confirm`, and lists which scripts overwrite or delete existing files.
|
||||
- **Script catalog**: every script with a one-line purpose and a link to its page. The agent loads a page only when it needs the details.
|
||||
- **Workflows and troubleshooting**: common recipes, such as clipping a frame range or detecting scenes, and the documented error messages with their fixes.
|
||||
|
||||
To use it, copy the whole `video-processing-scripts` folder directly into your agent's skills directory, keeping the folder name. Do not copy only `SKILL.md`: it links to the documentation pages by relative path, so those pages must stay in the same folder. For example:
|
||||
|
||||
- Claude Code: `"%USERPROFILE%\.claudeskills\video-processing-scripts\"`
|
||||
- Devin: `"%USERPROFILE%\.codeium\windsurf\skills\"`
|
||||
- VS Code: `"%USERPROFILE%\.copilot\skills\"`
|
||||
|
||||
The `video-processing-scripts` folder is available in both repositories:
|
||||
|
||||
- **Git** (`guru/Video-Processing-Scripts`): the `video-processing-scripts` folder.
|
||||
- **SVN** (project `Video Processing Scripts`): the `video-processing-scripts` folder.
|
||||
|
||||
Each copy contains `SKILL.md` and the markdown documentation pages required by the skill.
|
||||
|
||||
## Notes
|
||||
|
||||
- All scripts require PowerShell 7+ (use `pwsh.exe`)
|
||||
- File extensions (`.mp4`, `.png`) are automatically added if not provided
|
||||
- Paths can be absolute or relative to current directory
|
||||
- Use `.skip.mp4` extension to exclude files from batch processing
|
||||
- Backup files (`.bak.mp4`) are automatically skipped in batch operations
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Error: "Video file not found"
|
||||
|
||||
- Check that the file path is correct
|
||||
- Ensure file exists at specified location
|
||||
- Use absolute paths if relative paths aren't working
|
||||
|
||||
### Error: "ffmpeg failed" or "ffprobe failed"
|
||||
|
||||
- Verify ffmpeg and ffprobe are installed and in PATH
|
||||
- Check that the video file is not corrupted
|
||||
- Ensure sufficient disk space for output
|
||||
|
||||
### Error: "Frame index out of range"
|
||||
|
||||
- Use `Get-FramesCount.ps1` to check valid frame range
|
||||
- Frame indices are 0-based (first frame is 0)
|
||||
Reference in new issue
Block a user