diff --git a/Clone-Video.ps1 b/Clone-Video.ps1 index 922650f..72265d2 100644 --- a/Clone-Video.ps1 +++ b/Clone-Video.ps1 @@ -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. @@ -228,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 @@ -478,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 @@ -601,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 = @( @@ -718,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 diff --git a/Create-Clip.ps1 b/Create-Clip.ps1 index ba10919..77b0cee 100644 --- a/Create-Clip.ps1 +++ b/Create-Clip.ps1 @@ -11,7 +11,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 Suffix Text inserted into the derived -Target name before the file extension. Defaults to "-Clip" and cannot be empty. diff --git a/Create-Scenes.ps1 b/Create-Scenes.ps1 index 17d4ef9..b9c7e68 100644 --- a/Create-Scenes.ps1 +++ b/Create-Scenes.ps1 @@ -161,6 +161,7 @@ function Get-SceneList { 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) } } diff --git a/Get-Frame.ps1 b/Get-Frame.ps1 index 0ca0b7d..cd2e21e 100644 --- a/Get-Frame.ps1 +++ b/Get-Frame.ps1 @@ -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. + 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) diff --git a/Get-FramesRange.ps1 b/Get-FramesRange.ps1 index dcf0e80..150c3c4 100644 --- a/Get-FramesRange.ps1 +++ b/Get-FramesRange.ps1 @@ -11,7 +11,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-%") .DESCRIPTION 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 } +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 @@ -65,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" } @@ -76,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+$') { diff --git a/Get-LastFrame.ps1 b/Get-LastFrame.ps1 index 028b0cb..55f05fe 100644 --- a/Get-LastFrame.ps1 +++ b/Get-LastFrame.ps1 @@ -12,7 +12,8 @@ .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. @@ -36,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) @@ -51,21 +52,15 @@ 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) @@ -98,7 +93,7 @@ if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToE '-y' '-i', $VideoPath '-vf', $videoFilter - '-vsync', 'vfr' + '-fps_mode', 'vfr' '-frames:v', '1' '-q:v', '1' '--', $ImagePath @@ -107,8 +102,12 @@ if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToE $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 + } + + 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 } diff --git a/skill/Clone-Video.md b/skill/Clone-Video.md index b05fcd9..2b8ce7a 100644 --- a/skill/Clone-Video.md +++ b/skill/Clone-Video.md @@ -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: - `start-end`: Clone frames from `start` to `end` (e.g., `"100-500"`) - `positive_number`: Clone frames from 0 to that number (e.g., `"50"`) - - `-N`: Clone last N frames (e.g., `"-10"`) - - `%`: Represents the last frame index; can be used alone or in ranges (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) + - `%`: 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. diff --git a/skill/Create-Clip.md b/skill/Create-Clip.md index f605869..ad0662b 100644 --- a/skill/Create-Clip.md +++ b/skill/Create-Clip.md @@ -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: - `start-end`: Clip frames from `start` to `end` (e.g., `"100-200"`) - `positive_number`: Clip frames from 0 to that number (e.g., `"50"`) - - `-N`: Clip last N frames (e.g., `"-10"`) - - `%`: Represents the last frame index; can be used alone or in ranges (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) + - `%`: 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. diff --git a/skill/Create-Scenes.md b/skill/Create-Scenes.md index 88f1f8c..932a1ea 100644 --- a/skill/Create-Scenes.md +++ b/skill/Create-Scenes.md @@ -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: ```text -Index StartFrame EndFrame Frames StartTime EndTime Suffix ------ ---------- -------- ------ --------- ------- ------ - 1 0 29 30 0.00 0.97 -Scene-01 - 2 30 59 30 1.00 1.97 -Scene-02 - 3 60 89 30 2.00 2.97 -Scene-03 +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. +`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 diff --git a/skill/Get-Frame.md b/skill/Get-Frame.md index 52fa97b..afd8127 100644 --- a/skill/Get-Frame.md +++ b/skill/Get-Frame.md @@ -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. - 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) + - 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 diff --git a/skill/Get-FramesRange.md b/skill/Get-FramesRange.md index 680e532..d660dc5 100644 --- a/skill/Get-FramesRange.md +++ b/skill/Get-FramesRange.md @@ -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`) - **`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) -- **`%`**: Represents the last frame index. Can be used alone or in ranges: +- **`-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 - - `-%` - Extract only the first frame (frame 0) +- `-%` is not supported and is rejected with exit code `1` - **`all`**: Extract all frames (same as `0-%`) ## Examples @@ -24,63 +24,56 @@ Extracts a specific range of frames from a video file to PNG images. **Extract specific range:** ```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 ``` **Extract first 50 frames:** ```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 ``` **Extract last 10 frames:** ```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 ``` **Extract only the last frame:** ```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 ``` **Extract from frame 10 to the last frame:** ```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 ``` -**Extract only the first frame:** - -```powershell -.\Get-Frames.ps1 -Video video.mp4 -Frames "-%" -# Creates: Frames/video_0000.png -``` - **Extract all frames:** ```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 ``` **Custom output pattern:** ```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 ``` **Full output path:** ```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). @@ -99,11 +92,11 @@ This script supports `-WhatIf` and `-Confirm` for safe execution: **Simulate without extracting:** ```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:** ```powershell -.\Get-Frames.ps1 -Video video.mp4 -Frames "0-10" -Confirm +.\Get-FramesRange.ps1 -Video video.mp4 -Frames "0-10" -Confirm ``` diff --git a/skill/Get-LastFrame.md b/skill/Get-LastFrame.md index 9f99143..e62294b 100644 --- a/skill/Get-LastFrame.md +++ b/skill/Get-LastFrame.md @@ -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) - 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) + - 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 diff --git a/skill/SKILL.md b/skill/SKILL.md index 13bdba4..0f313e3 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -11,7 +11,7 @@ The per-script pages sit in the same folder as this file. Load a page only when ## 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. - **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: @@ -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`). - 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 ` 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 @@ -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" ``` +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`. @@ -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` | | `N` | Frames 0 to `N`, not only frame `N` | - | `-N` | The last `N` frames | - | `%` | The last frame index, alone or in a range, e.g. `10-%` | + | `-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-%` | -- **`-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). - **`.skip` marker**: a file named `.skip.` 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`. -- Batch operations skip `.bak.mp4` backup files. -- Scripts exit with `0` on success and `1` on failure. Check `$LASTEXITCODE` after each call. +- 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 @@ -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-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) | 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-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. | @@ -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. | | [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 `.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. +- **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 `.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.