Fix -Frames negative range calculation to extract last N frames correctly (off-by-one error), add validation to reject invalid '-%' and '-0' formats with exit code 1, change '%' alone to extract single last frame instead of range, remove duplicate last frame from PNG sequence concat list, update Get-LastFrame.ps1 and Get-Frame.ps1 to clamp out-of-range frame indices instead of erroring and improve error messages with inline stderr output, add -y flag and -- separator to ffprobe calls, replace deprecated

This commit is contained in:
guru committed 2026-11-02 06:30:38 -05:00
1 parent cf41fb0ce5
commit 1227c6e153
13 files changed
+100 -86

No files matched your search

+13 -6
View File
@@ -36,7 +36,7 @@
- start-end: Extract frames from start to end (e.g., "100-200") - start-end: Extract frames from start to end (e.g., "100-200")
- positive_number: Extract frames from 0 to that number (e.g., "50") - positive_number: Extract frames from 0 to that number (e.g., "50")
- -negative_number: Extract last N frames (e.g., "-10") - -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-%") - all: Extract all frames (same as "0-%")
.PARAMETER Sequence .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. 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.
@@ -228,6 +228,14 @@ if (-not [string]::IsNullOrWhiteSpace($Frames)) {
Write-Error '-Frames cannot be used together with -StartFrame, -EndFrame, or -Sequence' Write-Error '-Frames cannot be used together with -StartFrame, -EndFrame, or -Sequence'
exit 1 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 $AudioPath = $null
@@ -478,13 +486,13 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
$maxPngIndex = $pngFiles.Count - 1 $maxPngIndex = $pngFiles.Count - 1
if (-not [string]::IsNullOrWhiteSpace($Frames)) { 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 -ieq 'all') { $framesPattern = "0-$maxPngIndex" }
if ($framesPattern -match '^(\d+)-(\d+)$') { if ($framesPattern -match '^(\d+)-(\d+)$') {
$StartFrame = [int]$Matches[1] $StartFrame = [int]$Matches[1]
$EndFrame = [int]$Matches[2] $EndFrame = [int]$Matches[2]
} elseif ($framesPattern -match '^-(\d+)$') { } elseif ($framesPattern -match '^-(\d+)$') {
$StartFrame = [Math]::Max(0, $maxPngIndex - [int]$Matches[1]) $StartFrame = [Math]::Max(0, $maxPngIndex - [int]$Matches[1] + 1)
$EndFrame = $maxPngIndex $EndFrame = $maxPngIndex
} elseif ($framesPattern -match '^\d+$') { } elseif ($framesPattern -match '^\d+$') {
$StartFrame = 0 $StartFrame = 0
@@ -601,7 +609,6 @@ if (Test-Path -LiteralPath $SourcePath -PathType Container) {
$concatLines += "file '$pfPath'" $concatLines += "file '$pfPath'"
$concatLines += "duration $frameDuration" $concatLines += "duration $frameDuration"
} }
$concatLines += "file '$($pngFiles[-1].FullName.Replace('\', '/'))'"
[System.IO.File]::WriteAllLines($concatTempFile, $concatLines, [System.Text.UTF8Encoding]::new($false)) [System.IO.File]::WriteAllLines($concatTempFile, $concatLines, [System.Text.UTF8Encoding]::new($false))
$seqFfmpegArgs = @( $seqFfmpegArgs = @(
@@ -718,13 +725,13 @@ $totalFramesInt = [int]$totalFrames
$maxFrameIndex = $totalFramesInt - 1 $maxFrameIndex = $totalFramesInt - 1
if (-not [string]::IsNullOrWhiteSpace($Frames)) { 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 -ieq 'all') { $framesPattern = "0-$maxFrameIndex" }
if ($framesPattern -match '^(\d+)-(\d+)$') { if ($framesPattern -match '^(\d+)-(\d+)$') {
$StartFrame = [int]$Matches[1] $StartFrame = [int]$Matches[1]
$EndFrame = [int]$Matches[2] $EndFrame = [int]$Matches[2]
} elseif ($framesPattern -match '^-(\d+)$') { } elseif ($framesPattern -match '^-(\d+)$') {
$StartFrame = [Math]::Max(0, $maxFrameIndex - [int]$Matches[1]) $StartFrame = [Math]::Max(0, $maxFrameIndex - [int]$Matches[1] + 1)
$EndFrame = $maxFrameIndex $EndFrame = $maxFrameIndex
} elseif ($framesPattern -match '^\d+$') { } elseif ($framesPattern -match '^\d+$') {
$StartFrame = 0 $StartFrame = 0
+1 -1
View File
@@ -11,7 +11,7 @@
- start-end: Extract frames from start to end (e.g., "100-200") - start-end: Extract frames from start to end (e.g., "100-200")
- positive_number: Extract frames from 0 to that number (e.g., "50") - positive_number: Extract frames from 0 to that number (e.g., "50")
- -negative_number: Extract last N frames (e.g., "-10") - -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-%") - all: Extract all frames (same as "0-%")
.PARAMETER Suffix .PARAMETER Suffix
Text inserted into the derived -Target name before the file extension. Defaults to "-Clip" and cannot be empty. Text inserted into the derived -Target name before the file extension. Defaults to "-Clip" and cannot be empty.
+1
View File
@@ -161,6 +161,7 @@ function Get-SceneList {
Frames = $end - $start + 1 Frames = $end - $start + 1
StartTime = [Math]::Round($start / $Info.Fps, 3) StartTime = [Math]::Round($start / $Info.Fps, 3)
EndTime = [Math]::Round($end / $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) Suffix = Format-SceneSuffix -Template $Template -Number ($i + 1)
} }
} }
+11 -15
View File
@@ -1,13 +1,14 @@
<# <#
.SYNOPSIS .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. 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). 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 .PARAMETER Video
Path to the input video file. Can be absolute or relative to current directory. Path to the input video file. Can be absolute or relative to current directory.
.PARAMETER Frame .PARAMETER Frame
0-based frame index to extract. Negative values count from the end: 0-based frame index to extract. Negative values count from the end:
-1 extracts the last frame (default), -2 extracts second-to-last, etc. -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). 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)] [CmdletBinding(SupportsShouldProcess)]
param( param(
@@ -35,8 +36,7 @@ $totalFramesOutput = & ffprobe -v error -select_streams v:0 -count_frames -show_
$ffprobeExit = $LASTEXITCODE $ffprobeExit = $LASTEXITCODE
if ($ffprobeExit -ne 0) { if ($ffprobeExit -ne 0) {
$totalFramesOutput | ForEach-Object { Write-Error $_ } Write-Error ('ffprobe failed to read video file: {0}{1}{2}' -f $VideoPath, [Environment]::NewLine, ($totalFramesOutput -join [Environment]::NewLine))
Write-Error ('ffprobe failed to read video file: {0}' -f $VideoPath)
exit 1 exit 1
} }
@@ -48,19 +48,15 @@ if (-not $totalFrames -or ([string]$totalFrames).Trim() -notmatch '^\d+$') {
} }
$totalFramesInt = [int]([string]$totalFrames).Trim() $totalFramesInt = [int]([string]$totalFrames).Trim()
if ($totalFramesInt -lt 1) {
Write-Error ('Video has no frames: {0}' -f $VideoPath)
exit 1
}
$maxFrameIndex = $totalFramesInt - 1 $maxFrameIndex = $totalFramesInt - 1
# Resolve frame index (handles negative values that count from the end) # Resolve frame index (handles negative values that count from the end)
$frameToExtract = if ($Frame -lt 0) { $requestedFrame = if ($Frame -lt 0) { $totalFramesInt + $Frame } else { $Frame }
$calculatedFrame = $totalFramesInt + $Frame $frameToExtract = [Math]::Max(0, [Math]::Min($requestedFrame, $maxFrameIndex))
[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
}
# Derive the output image path: insert "-XXXX" (zero-padded resolved frame index) before .png extension # Derive the output image path: insert "-XXXX" (zero-padded resolved frame index) before .png extension
$videoBase = [System.IO.Path]::GetFileNameWithoutExtension($VideoPath) $videoBase = [System.IO.Path]::GetFileNameWithoutExtension($VideoPath)
+12 -3
View File
@@ -11,7 +11,7 @@
- start-end: Extract frames from start to end (e.g., "100-200") - start-end: Extract frames from start to end (e.g., "100-200")
- positive_number: Extract frames from 0 to that number (e.g., "50") - positive_number: Extract frames from 0 to that number (e.g., "50")
- -negative_number: Extract last N frames (e.g., "-10") - -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-%") - all: Extract all frames (same as "0-%")
.DESCRIPTION .DESCRIPTION
Extracts specific range of frames of a video file to PNG images using ffmpeg. Extracts specific range of frames of a video file to PNG images using ffmpeg.
@@ -40,6 +40,15 @@ if (-not (Test-Path -LiteralPath $VideoPath)) {
exit 1 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 $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 $totalFramesOutput = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 -- $VideoPath 2>&1
@@ -65,7 +74,7 @@ $startFrame = 0
$endFrame = 0 $endFrame = 0
# Replace % with actual last frame index, and handle 'all' keyword # 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') { if ($framesPattern -ieq 'all') {
$framesPattern = "0-$maxFrameIndex" $framesPattern = "0-$maxFrameIndex"
} }
@@ -76,7 +85,7 @@ if ($framesPattern -match '^(\d+)-(\d+)$') {
} }
elseif ($framesPattern -match '^-(\d+)$') { elseif ($framesPattern -match '^-(\d+)$') {
$negativeCount = [int]$matches[1] $negativeCount = [int]$matches[1]
$startFrame = [Math]::Max(0, $maxFrameIndex - $negativeCount) $startFrame = [Math]::Max(0, $maxFrameIndex - $negativeCount + 1)
$endFrame = $maxFrameIndex $endFrame = $maxFrameIndex
} }
elseif ($framesPattern -match '^\d+$') { elseif ($framesPattern -match '^\d+$') {
+18 -19
View File
@@ -12,7 +12,8 @@
.PARAMETER Frame .PARAMETER Frame
0-based frame index to extract. Negative values count from the end: 0-based frame index to extract. Negative values count from the end:
-1 extracts the last frame (default), -2 extracts second-to-last, etc. -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 .DESCRIPTION
Extracts a specific frame of a video file to a PNG image using ffmpeg. 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 negative frame indices to count from the end of the video.
@@ -36,14 +37,14 @@ $ErrorActionPreference = 'Stop'
$VideoPath = $Video $VideoPath = $Video
if (-not (Test-Path $VideoPath)) { if (-not (Test-Path -LiteralPath $VideoPath)) {
Write-Error ('Video file not found: {0}' -f $VideoPath) Write-Error ('Video file not found: {0}' -f $VideoPath)
exit 1 exit 1
} }
$PSNativeCommandUseErrorActionPreference = $false $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+$') { if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
Write-Error ('Could not determine frame count for: {0}' -f $VideoPath) Write-Error ('Could not determine frame count for: {0}' -f $VideoPath)
@@ -51,21 +52,15 @@ if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
} }
$totalFramesInt = [int]$totalFrames $totalFramesInt = [int]$totalFrames
$maxFrameIndex = $totalFramesInt - 1 if ($totalFramesInt -lt 1) {
Write-Error ('Video has no frames: {0}' -f $VideoPath)
$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 exit 1
} }
$Frame $maxFrameIndex = $totalFramesInt - 1
}
# 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) Write-Host ('Total frames: {0} - extracting frame index {1}' -f $totalFramesInt, $frameToExtract)
@@ -98,7 +93,7 @@ if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToE
'-y' '-y'
'-i', $VideoPath '-i', $VideoPath
'-vf', $videoFilter '-vf', $videoFilter
'-vsync', 'vfr' '-fps_mode', 'vfr'
'-frames:v', '1' '-frames:v', '1'
'-q:v', '1' '-q:v', '1'
'--', $ImagePath '--', $ImagePath
@@ -107,8 +102,12 @@ if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToE
$ffmpegStderr = & ffmpeg @ffmpegArgs 2>&1 $ffmpegStderr = & ffmpeg @ffmpegArgs 2>&1
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
$ffmpegStderr | ForEach-Object { Write-Error $_ } Write-Error ('ffmpeg failed to extract frame {0}: {1}' -f $frameToExtract, (($ffmpegStderr | Out-String).Trim()))
Write-Error 'ffmpeg failed to extract the last frame.' exit 1
}
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 exit 1
} }
+3 -2
View File
@@ -45,8 +45,9 @@ The output container is chosen from the `-Target` extension (`.mp4`, `.webm`, or
- **`-Frames`** (optional): 0-based frame range to clone. Mutually exclusive with `-StartFrame`, `-EndFrame`, and `-Sequence`. Supported formats: - **`-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"`) - `start-end`: Clone frames from `start` to `end` (e.g., `"100-500"`)
- `positive_number`: Clone frames from 0 to that number (e.g., `"50"`) - `positive_number`: Clone frames from 0 to that number (e.g., `"50"`)
- `-N`: Clone last N frames (e.g., `"-10"`) - `-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)
- `%`: Represents the last frame index; can be used alone or in ranges (e.g., `"%"`, `"10-%"`, `"-%"`) - `%`: 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-%"`) - `all`: Clone all frames (same as `"0-%"`)
- **`-Sequence`** (optional): Path to PNG file for extracted frame (`.png` extension added if missing) - **`-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. - **`-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.
+3 -2
View File
@@ -10,8 +10,9 @@ Sources carrying a `.skip` marker keep that marker last: `P01.skip.mp4` becomes
- **`-Frames`** (required): 0-based frame range to clip. Supported formats: - **`-Frames`** (required): 0-based frame range to clip. Supported formats:
- `start-end`: Clip frames from `start` to `end` (e.g., `"100-200"`) - `start-end`: Clip frames from `start` to `end` (e.g., `"100-200"`)
- `positive_number`: Clip frames from 0 to that number (e.g., `"50"`) - `positive_number`: Clip frames from 0 to that number (e.g., `"50"`)
- `-N`: Clip last N frames (e.g., `"-10"`) - `-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)
- `%`: Represents the last frame index; can be used alone or in ranges (e.g., `"%"`, `"10-%"`, `"-%"`) - `%`: 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-%"`) - `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`. - **`-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. - **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.
+6 -6
View File
@@ -31,14 +31,14 @@ Refer to [Clone-Video.ps1](Clone-Video.md) for a full description of the encodin
Example scene list for a video with two hard cuts at frames 30 and 60: Example scene list for a video with two hard cuts at frames 30 and 60:
```text ```text
Index StartFrame EndFrame Frames StartTime EndTime Suffix Index StartFrame EndFrame Frames StartTime EndTime TotalTime Suffix
----- ---------- -------- ------ --------- ------- ------ ----- ---------- -------- ------ --------- ------- --------- ------
1 0 29 30 0.00 0.97 -Scene-01 1 0 29 30 0.00 0.97 1.00 -Scene-01
2 30 59 30 1.00 1.97 -Scene-02 2 30 59 30 1.00 1.97 1.00 -Scene-02
3 60 89 30 2.00 2.97 -Scene-03 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. `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.
## Scene Threshold ## Scene Threshold
+1 -1
View File
@@ -8,7 +8,7 @@ Extracts a specific frame of a video file to a PNG image by calling [Get-LastFra
- **`-Frame`** (required): 0-based frame index to extract. - **`-Frame`** (required): 0-based frame index to extract.
- Positive values: Direct frame index (e.g., `0` = first frame, `100` = frame 100) - 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) - 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) - 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 ## Image Filename Derivation
+13 -20
View File
@@ -12,11 +12,11 @@ Extracts a specific range of frames from a video file to PNG images.
- **`start-end`**: Extract frames from start to end index (e.g., `100-200`) - **`start-end`**: Extract frames from start to end index (e.g., `100-200`)
- **`positive_number`**: Extract frames from 0 to that number (e.g., `50` extracts frames 0-50) - **`positive_number`**: Extract frames from 0 to that number (e.g., `50` extracts frames 0-50)
- **`-negative_number`**: Extract last N frames (e.g., `-10` extracts last 10 frames) - **`-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)
- **`%`**: Represents the last frame index. Can be used alone or in ranges: - **`%`**: 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 - `%` - Extract only the last frame
- `10-%` - Extract from frame 10 to the last frame - `10-%` - Extract from frame 10 to the last frame
- `-%` - Extract only the first frame (frame 0) - `-%` is not supported and is rejected with exit code `1`
- **`all`**: Extract all frames (same as `0-%`) - **`all`**: Extract all frames (same as `0-%`)
## Examples ## Examples
@@ -24,63 +24,56 @@ Extracts a specific range of frames from a video file to PNG images.
**Extract specific range:** **Extract specific range:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "100-200" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "100-200"
# Creates: Frames/video_0100.png, Frames/video_0101.png, ..., Frames/video_0200.png # Creates: Frames/video_0100.png, Frames/video_0101.png, ..., Frames/video_0200.png
``` ```
**Extract first 50 frames:** **Extract first 50 frames:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "50" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "50"
# Creates: Frames/video_0000.png through Frames/video_0050.png # Creates: Frames/video_0000.png through Frames/video_0050.png
``` ```
**Extract last 10 frames:** **Extract last 10 frames:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "-10" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "-10"
# If video has 500 frames, creates: Frames/video_0490.png through Frames/video_0499.png # If video has 500 frames, creates: Frames/video_0490.png through Frames/video_0499.png
``` ```
**Extract only the last frame:** **Extract only the last frame:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "%" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "%"
# If video has 500 frames, creates: Frames/video_0499.png # If video has 500 frames, creates: Frames/video_0499.png
``` ```
**Extract from frame 10 to the last frame:** **Extract from frame 10 to the last frame:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "10-%" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "10-%"
# If video has 500 frames, creates: Frames/video_0010.png through Frames/video_0499.png # If video has 500 frames, creates: Frames/video_0010.png through Frames/video_0499.png
``` ```
**Extract only the first frame:**
```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "-%"
# Creates: Frames/video_0000.png
```
**Extract all frames:** **Extract all frames:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "all" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "all"
# If video has 500 frames, creates: Frames/video_0000.png through Frames/video_0499.png # If video has 500 frames, creates: Frames/video_0000.png through Frames/video_0499.png
``` ```
**Custom output pattern:** **Custom output pattern:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "0-100" -Images "output_frame" .\Get-FramesRange.ps1 -Video video.mp4 -Frames "0-100" -Images "output_frame"
# Creates: output_frame_0000.png through output_frame_0100.png # Creates: output_frame_0000.png through output_frame_0100.png
``` ```
**Full output path:** **Full output path:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "50-150" -Images "C:\Output\frame_%04d.png" .\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). **Note:** Frame numbers in output filenames reflect the actual video frame index (0-based).
@@ -99,11 +92,11 @@ This script supports `-WhatIf` and `-Confirm` for safe execution:
**Simulate without extracting:** **Simulate without extracting:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "100-200" -WhatIf .\Get-FramesRange.ps1 -Video video.mp4 -Frames "100-200" -WhatIf
``` ```
**Prompt before each frame:** **Prompt before each frame:**
```powershell ```powershell
.\Get-Frames.ps1 -Video video.mp4 -Frames "0-10" -Confirm .\Get-FramesRange.ps1 -Video video.mp4 -Frames "0-10" -Confirm
``` ```
+1 -1
View File
@@ -9,7 +9,7 @@ Extracts a specific frame of a video file to a PNG image.
- **`-Frame`** (optional): 0-based frame index to extract (default: -1) - **`-Frame`** (optional): 0-based frame index to extract (default: -1)
- Positive values: Direct frame index (e.g., 0 = first frame, 100 = frame 100) - 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) - 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) - 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 ## Filename Pattern
+17 -10
View File
@@ -11,7 +11,7 @@ The per-script pages sit in the same folder as this file. Load a page only when
## Prerequisites ## Prerequisites
- **Platform**: Windows 11 only. The scripts are tested on Windows 11 and nowhere else. Linux and WSL are not supported. Run them with Windows PowerShell 7 (`pwsh.exe`). - **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. - **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. - **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: - **Optional tools**, needed only by specific scripts:
@@ -49,7 +49,7 @@ If a core check fails, or a check needed by the current task fails, **stop**. Te
- Git: `git clone https://git.gurusushi.dyndns.org/guru/Video-Processing-Scripts.git` (repository `guru/Video-Processing-Scripts`). - 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`. - SVN: `public-svn`, project `Video Processing Scripts`.
- The script folder must be on `PATH` so scripts can be called from any directory. - 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. - `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 ## How to Run the Scripts
@@ -64,6 +64,12 @@ If a core check fails, or a check needed by the current task fails, **stop**. Te
pwsh -NoProfile -File .\Create-Clip.ps1 -Video "My Video.mp4" -Frames "100-200" 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 ## Conventions
- Frame indices are **0-based**: the first frame is `0`. - Frame indices are **0-based**: the first frame is `0`.
@@ -73,18 +79,19 @@ pwsh -NoProfile -File .\Create-Clip.ps1 -Video "My Video.mp4" -Frames "100-200"
| --- | --- | | --- | --- |
| `start-end` | Frames from `start` to `end`, e.g. `100-200` | | `start-end` | Frames from `start` to `end`, e.g. `100-200` |
| `N` | Frames 0 to `N`, not only frame `N` | | `N` | Frames 0 to `N`, not only frame `N` |
| `-N` | The last `N` frames | | `-N` | The last `N` frames. `-1` is the last frame. |
| `%` | The last frame index, alone or in a range, e.g. `10-%` | | `%` | 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-%` | | `all` | All frames, same as `0-%` |
- **`-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). Out-of-range values are clamped. - [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). - 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. - **`.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`. - [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`. - [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`. - [Create-Scenes.ps1](Create-Scenes.md) gives `P01-Scene-01.skip.mp4` for `P01.skip.mp4`.
- Batch operations skip `.bak.mp4` backup files. - On failure, scripts exit with `1`, print an error message to the error stream, and create no files. Check `$LASTEXITCODE` after each call.
- Scripts exit with `0` on success and `1` on failure. 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. - **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 ## Safety Rules
@@ -102,7 +109,7 @@ pwsh -NoProfile -File .\Create-Clip.ps1 -Video "My Video.mp4" -Frames "100-200"
| [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-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-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-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) | Only when `-List` is not given. Deletes earlier clips of the same source in the source folder, then creates the new ones, which replace the old ones. | | [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-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. | | [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. | | [Crop-Clips.ps1](Crop-Clips.md) | Works in place when `-Source` and `-Target` are omitted or equal. See the in-place rule below. |
@@ -114,8 +121,8 @@ pwsh -NoProfile -File .\Create-Clip.ps1 -Video "My Video.mp4" -Frames "100-200"
| [Get-VideoInfo.ps1](Get-VideoInfo.md) | Reads only. Creates and deletes nothing. | | [Get-VideoInfo.ps1](Get-VideoInfo.md) | Reads only. Creates and deletes nothing. |
| [Stitch-InBetween.ps1](Stitch-InBetween.md) | Deletes an existing `-Video` before generating. Deletes everything inside an existing `-TempDir`. | | [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, 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: - **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. - 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. - 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`. - 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. - Never delete `.bak.mp4` files yourself unless the user asks.