Compare commits
13
Commits
693655d07d
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dad95f0728 | ||
|
|
fcc208e351 | ||
|
|
de238fb9c3 | ||
|
|
4378e3fd99 | ||
|
|
60feb778d3 | ||
|
|
0befcfe8a3 | ||
|
|
8b46091f9a | ||
|
|
cda0b6a4b8 | ||
|
|
50c9d831e9 | ||
|
|
1227c6e153 | ||
|
|
cf41fb0ce5 | ||
|
|
2290672d5b | ||
|
|
514f68ca2d |
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.
|
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
|
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
|
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
|
.PARAMETER Model
|
||||||
Upscayl model name (without extension) to use when -Upscale is specified. Defaults to
|
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
|
'4xNomos8kSC'. The model files (<Model>.bin and <Model>.param) must exist under
|
||||||
@@ -328,7 +328,7 @@ if ($Upscale -gt 0) {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
if ($gpuId -notmatch '^-?\d+$') {
|
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
|
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 ('Upscaling source with upscayl-bin (x{0}, model={1}, gpu={2})...' -f $Upscale, $Model, $gpuId)
|
||||||
Write-Host (' Upscayl temp file: {0}' -f $upscaledTempPath)
|
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 {
|
$upscaylCmdLine = '"{0}" {1}' -f $upscaylExePath, (($upscaylArgs | ForEach-Object {
|
||||||
if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ }
|
if ($_ -match '\s') { '"{0}"' -f $_ } else { $_ }
|
||||||
}) -join ' ')
|
}) -join ' ')
|
||||||
|
|||||||
+15
-8
@@ -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
|
||||||
@@ -891,7 +898,7 @@ if (-not [string]::IsNullOrWhiteSpace($Sequence)) {
|
|||||||
'-y'
|
'-y'
|
||||||
'-i', $SourcePath
|
'-i', $SourcePath
|
||||||
'-vf', $videoFilter
|
'-vf', $videoFilter
|
||||||
'-vsync', 'vfr'
|
'-fps_mode', 'vfr'
|
||||||
'-frames:v', '1'
|
'-frames:v', '1'
|
||||||
'-q:v', '1'
|
'-q:v', '1'
|
||||||
'--', $SequencePath
|
'--', $SequencePath
|
||||||
@@ -977,7 +984,7 @@ if ($StartFrame -gt 0 -or $actualEndFrame -lt $maxFrameIndex) {
|
|||||||
$videoFilter = $filterParts -join ','
|
$videoFilter = $filterParts -join ','
|
||||||
$ffmpegArgs += @(
|
$ffmpegArgs += @(
|
||||||
'-vf', $videoFilter
|
'-vf', $videoFilter
|
||||||
'-vsync', 'vfr'
|
'-fps_mode', 'vfr'
|
||||||
)
|
)
|
||||||
$ffmpegArgs += $targetVideoEncodeArgs
|
$ffmpegArgs += $targetVideoEncodeArgs
|
||||||
if ($CRF -ge 0) {
|
if ($CRF -ge 0) {
|
||||||
|
|||||||
+1
-1
@@ -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.
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ function Export-SrtFile {
|
|||||||
#region Main Script
|
#region Main Script
|
||||||
|
|
||||||
Write-Host "Parsing first subtitle file: $Subtitle1"
|
Write-Host "Parsing first subtitle file: $Subtitle1"
|
||||||
$subtitle1Entries = Get-SrtContent -SrtPath $Subtitle1
|
$subtitle1Entries = @(Get-SrtContent -SrtPath $Subtitle1)
|
||||||
Write-Host " Found $($subtitle1Entries.Count) entries"
|
Write-Host " Found $($subtitle1Entries.Count) entries"
|
||||||
|
|
||||||
if ($subtitle1Entries.Count -eq 0) {
|
if ($subtitle1Entries.Count -eq 0) {
|
||||||
@@ -210,7 +210,7 @@ $videoDurationFormatted = ConvertFrom-TimeSpan -TimeSpan ([TimeSpan]::FromSecond
|
|||||||
Write-Host " Video duration: $videoDurationFormatted (${videoDuration}s)"
|
Write-Host " Video duration: $videoDurationFormatted (${videoDuration}s)"
|
||||||
|
|
||||||
Write-Host "Parsing second subtitle file: $Subtitle2"
|
Write-Host "Parsing second subtitle file: $Subtitle2"
|
||||||
$subtitle2Entries = Get-SrtContent -SrtPath $Subtitle2
|
$subtitle2Entries = @(Get-SrtContent -SrtPath $Subtitle2)
|
||||||
Write-Host " Found $($subtitle2Entries.Count) entries"
|
Write-Host " Found $($subtitle2Entries.Count) entries"
|
||||||
|
|
||||||
if ($subtitle2Entries.Count -eq 0) {
|
if ($subtitle2Entries.Count -eq 0) {
|
||||||
|
|||||||
+63
-14
@@ -2,7 +2,7 @@
|
|||||||
.SYNOPSIS
|
.SYNOPSIS
|
||||||
Detects scenes in a video file with ffmpeg and extracts each scene as a separate video clip file.
|
Detects scenes in a video file with ffmpeg and extracts each scene as a separate video clip file.
|
||||||
.DESCRIPTION
|
.DESCRIPTION
|
||||||
Analyzes the input video with ffmpeg scene-change detection, using the -Scene threshold, and computes
|
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
|
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
|
Create-Clip.ps1 (located in the same directory as this script) with the scene frame range, which in
|
||||||
turn calls Clone-Video.ps1.
|
turn calls Clone-Video.ps1.
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
|
|
||||||
The list of scenes (index, start frame, end frame, frame count, start time, end time) is always displayed.
|
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.
|
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
|
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
|
name, same suffix pattern and same extension) are deleted before the new clips are created. Existing
|
||||||
@@ -29,9 +32,12 @@
|
|||||||
.PARAMETER Video
|
.PARAMETER Video
|
||||||
Path to the input video file. Can be absolute or relative to current directory. If the path has no
|
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.
|
extension, .mp4, .webm and .mkv are tried in turn.
|
||||||
.PARAMETER Scene
|
.PARAMETER Level
|
||||||
Threshold value between 0 and 1.0 to determine scene changes. Lower values mean more sensitive
|
Threshold level value between 0 and 1.0 to determine scene changes. Lower values mean more sensitive
|
||||||
detection (more scenes). Default is 0.3.
|
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
|
.PARAMETER Suffix
|
||||||
Scene suffix to use for the output video clip files. Must contain a printf-style number placeholder
|
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.
|
such as %02d, which is replaced by the scene number starting at 1. By default, "-Scene-%02d" is used.
|
||||||
@@ -39,13 +45,16 @@
|
|||||||
Only displays the list of scenes. No video clip file is created and no existing file is deleted.
|
Only displays the list of scenes. No video clip file is created and no existing file is deleted.
|
||||||
.PARAMETER ExtraArgs
|
.PARAMETER ExtraArgs
|
||||||
Additional parameters passed as-is to Create-Clip.ps1 and from there to Clone-Video.ps1
|
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 and -Target are
|
(ex: -Size 720p -NoAudio -CRF 18 -LTX). The parameters -Video, -Frames, -Suffix, -Target, -Level and
|
||||||
managed by this script and must not be repeated.
|
-Scenes are managed by this script and must not be repeated.
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Create-Scenes.ps1 -Video P01.mp4 -Scene 0.4 -List
|
.\Create-Scenes.ps1 -Video P01.mp4 -Level 0.4 -List
|
||||||
Lists the scenes detected in P01.mp4 without creating or deleting any file.
|
Lists the scenes detected in P01.mp4 without creating or deleting any file.
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Create-Scenes.ps1 -Video P01 -Scene 0.3 -Size 720p -NoAudio
|
.\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.
|
Creates P01-Scene-01.mp4, P01-Scene-02.mp4, ... scaled to 720p and without audio.
|
||||||
.EXAMPLE
|
.EXAMPLE
|
||||||
.\Create-Scenes.ps1 -Video P01.mp4 -Suffix "-Shot-%03d" -WhatIf
|
.\Create-Scenes.ps1 -Video P01.mp4 -Suffix "-Shot-%03d" -WhatIf
|
||||||
@@ -59,7 +68,11 @@ param(
|
|||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[ValidateRange(0.0, 1.0)]
|
[ValidateRange(0.0, 1.0)]
|
||||||
[double]$Scene = 0.3,
|
[double]$Level = 0.3,
|
||||||
|
|
||||||
|
[Parameter(Mandatory = $false)]
|
||||||
|
[ValidateRange(1, [int]::MaxValue)]
|
||||||
|
[int]$Scenes,
|
||||||
|
|
||||||
[Parameter(Mandatory = $false)]
|
[Parameter(Mandatory = $false)]
|
||||||
[ValidateNotNullOrEmpty()]
|
[ValidateNotNullOrEmpty()]
|
||||||
@@ -161,6 +174,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,16 +247,51 @@ $videoPath = Resolve-VideoPath -Path $Video
|
|||||||
[object[]]$tokens = @()
|
[object[]]$tokens = @()
|
||||||
if ($null -ne $ExtraArgs) { $tokens = @($ExtraArgs) }
|
if ($null -ne $ExtraArgs) { $tokens = @($ExtraArgs) }
|
||||||
$extraParams = ConvertTo-ClipArgument -Tokens $tokens
|
$extraParams = ConvertTo-ClipArgument -Tokens $tokens
|
||||||
foreach ($reserved in 'Video', 'Frames', 'Suffix', 'Target') {
|
foreach ($reserved in 'Video', 'Frames', 'Suffix', 'Target', 'Level', 'Scenes') {
|
||||||
if ($extraParams.ContainsKey($reserved)) {
|
if ($extraParams.ContainsKey($reserved)) {
|
||||||
throw "-$reserved is managed by Create-Scenes.ps1 and must not be passed as an extra parameter."
|
throw "-$reserved is managed by Create-Scenes.ps1 and must not be passed as an extra parameter."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$info = Get-VideoInfo -Path $videoPath
|
$info = Get-VideoInfo -Path $videoPath
|
||||||
$changeTimes = @(Get-SceneChangeTime -Path $videoPath -Threshold $Scene)
|
# Level rounded to 2 decimals. The search rounds each new level to 2 decimals too; the step is not rounded.
|
||||||
$scenes = @(Get-SceneList -Info $info -ChangeTimes $changeTimes -Template $Suffix)
|
$sceneLevel = [Math]::Round($Level, 2, [MidpointRounding]::AwayFromZero)
|
||||||
$scenes | Format-Table -AutoSize | Out-Host
|
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 }
|
if ($List) { return }
|
||||||
|
|
||||||
$confirm = [bool]$PSBoundParameters['Confirm']
|
$confirm = [bool]$PSBoundParameters['Confirm']
|
||||||
@@ -250,7 +299,7 @@ $pattern = Get-SceneFilePattern -VideoPath $videoPath -Template $Suffix
|
|||||||
Remove-ExistingScene -VideoPath $videoPath -Pattern $pattern -WhatIf:$WhatIfPreference -Confirm:$confirm
|
Remove-ExistingScene -VideoPath $videoPath -Pattern $pattern -WhatIf:$WhatIfPreference -Confirm:$confirm
|
||||||
|
|
||||||
$failures = 0
|
$failures = 0
|
||||||
foreach ($clipScene in $scenes) {
|
foreach ($clipScene in $sceneList) {
|
||||||
$frames = '{0}-{1}' -f $clipScene.StartFrame, $clipScene.EndFrame
|
$frames = '{0}-{1}' -f $clipScene.StartFrame, $clipScene.EndFrame
|
||||||
Write-Verbose ('Scene {0}: frames {1}, suffix {2}' -f $clipScene.Index, $frames, $clipScene.Suffix)
|
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
|
# Extra parameters are forwarded as the original token array, not as the hashtable: Create-Clip.ps1
|
||||||
|
|||||||
+11
-15
@@ -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)
|
||||||
|
|||||||
+14
-5
@@ -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+$') {
|
||||||
@@ -103,7 +112,7 @@ $frameCount = $endFrame - $startFrame + 1
|
|||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Images)) {
|
if ([string]::IsNullOrWhiteSpace($Images)) {
|
||||||
$videoBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Video)
|
$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')
|
$Images = Join-Path $outputDir 'Frames' ($videoBaseName + '_%04d.png')
|
||||||
} else {
|
} else {
|
||||||
$outputPattern = $Images
|
$outputPattern = $Images
|
||||||
@@ -139,7 +148,7 @@ for ($frameIndex = $startFrame; $frameIndex -le $endFrame; $frameIndex++) {
|
|||||||
'-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'
|
||||||
'--', $outputFile
|
'--', $outputFile
|
||||||
|
|||||||
+18
-19
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +5,15 @@ A collection of PowerShell scripts for video processing tasks using ffmpeg and f
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- **PowerShell 7+** (pwsh.exe)
|
- **PowerShell 7+** (pwsh.exe)
|
||||||
- **ffmpeg** - Must be in system PATH
|
- **Platform**: tested on Windows 11 only. Linux and WSL are not tested and not supported.
|
||||||
- **ffprobe** - Must be in system PATH
|
- **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
|
- **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
|
- **upscayl-bin** - Must be in system PATH and also defined the environment variable **UPSCAYL_HOME** to the same directory
|
||||||
|
|
||||||
## ffmpeg
|
## 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
|
## rife-ncnn-vulkan
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ This environment variable must be set for both `rife-ncnn-vulkan` and `upscayl-b
|
|||||||
The value stored in **NCNN_GPU_ID** will be either:
|
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.
|
- **`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 (no Vulkan GPU is available).
|
- **`-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`:
|
These map directly to the `-g` flag values accepted by both `rife-ncnn-vulkan` and `upscayl-bin`:
|
||||||
|
|
||||||
@@ -40,7 +41,7 @@ These map directly to the `-g` flag values accepted by both `rife-ncnn-vulkan` a
|
|||||||
| `0` | First GPU |
|
| `0` | First GPU |
|
||||||
| `1` | Second GPU |
|
| `1` | Second GPU |
|
||||||
| `2` | Third GPU |
|
| `2` | Third GPU |
|
||||||
| `-1` | CPU |
|
| `-1` | CPU for `rife-ncnn-vulkan`; no `-g` flag for `upscayl-bin` (its default device) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ These map directly to the `-g` flag values accepted by both `rife-ncnn-vulkan` a
|
|||||||
| Get-LastFrame.ps1 | Extract a specific frame or last frame to PNG, with an optional filename pattern |
|
| 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-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-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 |
|
| Stitch-InBetween.ps1 | Generate interpolated in-between frames between two images using RIFE and encode to video |
|
||||||
|
|
||||||
**Important**:
|
**Important**:
|
||||||
@@ -90,7 +92,7 @@ Add the project folder to your PATH environment variable to access the scripts f
|
|||||||
|
|
||||||
## Agent Skill
|
## Agent Skill
|
||||||
|
|
||||||
[SKILL.md](SKILL.md) is an agent skill for AI coding agents (such as Claude Code or Devin). It teaches the agent how to use these scripts safely, so the agent does not have to rediscover the documentation for each task.
|
[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:
|
What it covers:
|
||||||
|
|
||||||
@@ -99,15 +101,18 @@ What it covers:
|
|||||||
- **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.
|
- **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.
|
- **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 file into your agent's skills folder in a folder named `video-processing-scripts`. For example:
|
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:
|
||||||
|
|
||||||
- Devin: `.devin/skills/video-processing-scripts/SKILL.md`
|
- Claude Code: `"%USERPROFILE%\.claudeskills\video-processing-scripts\"`
|
||||||
- Claude Code: `.claude/skills/video-processing-scripts/SKILL.md`
|
- Devin: `"%USERPROFILE%\.codeium\windsurf\skills\"`
|
||||||
|
- VS Code: `"%USERPROFILE%\.copilot\skills\"`
|
||||||
|
|
||||||
The skill is available in both repositories:
|
The `video-processing-scripts` folder is available in both repositories:
|
||||||
|
|
||||||
- **SVN** (project `Video Processing Scripts`): `mkdocs/SKILL.md`
|
- **Git** (`guru/Video-Processing-Scripts`): the `video-processing-scripts` folder.
|
||||||
- **Git** (`guru/Video-Processing-Scripts`): `SKILL.md` at the repository root. `Update-Git-Publish.ps1` copies it from `mkdocs/SKILL.md`.
|
- **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
|
## Notes
|
||||||
|
|
||||||
|
|||||||
@@ -1,162 +0,0 @@
|
|||||||
---
|
|
||||||
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, 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). Each script has its own page in this documentation, linked from the catalog below.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- **PowerShell 7+** (`pwsh`). Required by every script.
|
|
||||||
- **ffmpeg** and **ffprobe** on `PATH`. Required by every video script.
|
|
||||||
- **rife-ncnn-vulkan** on `PATH`, with `RIFE_HOME` set. Required by [Stitch-InBetween.ps1](Stitch-InBetween.md). See [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 [Upscalers installation guide](Upscalers.md).
|
|
||||||
|
|
||||||
## 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 only) | [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 these once before the first use in a session:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh --version
|
|
||||||
ffmpeg -version
|
|
||||||
ffprobe -version
|
|
||||||
Get-Command rife-ncnn-vulkan
|
|
||||||
Get-Command upscayl-bin
|
|
||||||
"RIFE_HOME=$env:RIFE_HOME"
|
|
||||||
"UPSCAYL_HOME=$env:UPSCAYL_HOME"
|
|
||||||
"NCNN_GPU_ID=$env:NCNN_GPU_ID"
|
|
||||||
```
|
|
||||||
|
|
||||||
If anything is missing or empty, **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.
|
|
||||||
|
|
||||||
## 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 must be on `PATH` so scripts can be called from any directory.
|
|
||||||
- `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 <script>.ps1 <parameters>`.
|
|
||||||
- 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"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Conventions
|
|
||||||
|
|
||||||
- Frame indices are **0-based**: the first frame is `0`.
|
|
||||||
- If the extension is omitted, `.mp4`, `.webm` and `.mkv` are tried in that order (for example [Create-Clip.ps1](Create-Clip.md), [Create-Scenes.ps1](Create-Scenes.md)).
|
|
||||||
- Files ending in `.skip.mp4` (for example `P01.skip.mp4`) are excluded from batch processing. Batch scripts also skip `.bak.mp4` backups.
|
|
||||||
- A `.skip` marker stays last in derived output names: `P01.skip.mp4` gives `P01-Clip.skip.mp4` ([Create-Clip.ps1](Create-Clip.md)) and `P01-Scene-01.skip.mp4` ([Create-Scenes.ps1](Create-Scenes.md)).
|
|
||||||
|
|
||||||
## 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` 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.
|
|
||||||
|
|
||||||
Scripts that replace or delete existing files:
|
|
||||||
|
|
||||||
| Script | Behavior |
|
|
||||||
| --- | --- |
|
|
||||||
| [Clone-Video.ps1](Clone-Video.md) | Overwrites the `-Target` file (ffmpeg `-y`). |
|
|
||||||
| [Create-Clip.ps1](Create-Clip.md) | Overwrites the derived clip, through Clone-Video. |
|
|
||||||
| [Create-Scenes.ps1](Create-Scenes.md) | Deletes earlier clips of the same source in the source folder, then overwrites them. |
|
|
||||||
| [Clone-Image.ps1](Clone-Image.md) | Overwrites the `-Target` image (ffmpeg `-y`). |
|
|
||||||
| [Compare-Videos.ps1](Compare-Videos.md) | Overwrites the `-Output` file (ffmpeg `-y`). |
|
|
||||||
| [Create-PillarBox.ps1](Create-PillarBox.md) | Removes an existing target before writing it. |
|
|
||||||
| [Create-Turnaround.ps1](Create-Turnaround.md) | Overwrites the `-Target` image. |
|
|
||||||
| [Crop-Clips.ps1](Crop-Clips.md) | In-place when source and target are the same folder, which is the default when both are omitted: writes a `.bak.mp4` backup, then removes the original after a successful crop. A separate target folder replaces same-named files. |
|
|
||||||
| [Create-FullSubtitle.ps1](Create-FullSubtitle.md) | Overwrites the `-Output` subtitle file without a prompt. |
|
|
||||||
| [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. |
|
|
||||||
| [Get-FramesRange.ps1](Get-FramesRange.md) | Overwrites existing PNG frames with the same name (ffmpeg `-y`). |
|
|
||||||
| [Get-LastFrame.ps1](Get-LastFrame.md) | Overwrites the output PNG (ffmpeg `-y`). [Get-Frame.ps1](Get-Frame.md) uses it. |
|
|
||||||
| [Get-VideoAudio.ps1](Get-VideoAudio.md) | Overwrites the output audio file. |
|
|
||||||
| [Stitch-InBetween.ps1](Stitch-InBetween.md) | Deletes an existing `-Video` output before generating. Deletes everything inside an existing `-TempDir`. Deletes intermediate PNG frames. |
|
|
||||||
|
|
||||||
Run `-WhatIf` first on every script in this table, and check the output paths before running it for real.
|
|
||||||
|
|
||||||
## Script Catalog
|
|
||||||
|
|
||||||
| Script | Purpose | Typical use |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| [Clone-Image.ps1](Clone-Image.md) | Resize, convert and optionally AI-upscale images | `-Source small.jpg -Target big_4k.png -Upscale 4 -Size 4K` |
|
|
||||||
| [Clone-Video.ps1](Clone-Video.md) | Re-encode a video with frame range, size, FPS, audio and CRF options | `-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 | `-S1 a.mp4 -S2 b.mp4 -Output cmp.mp4` |
|
|
||||||
| [Create-Clip.ps1](Create-Clip.md) | Clip a frame range; output name is the source name plus `-Suffix` (default `-Clip`) | `-Video P01.mp4 -Frames "100-200"` |
|
|
||||||
| [Create-FullSubtitle.ps1](Create-FullSubtitle.md) | Merge two SRT files into one with adjusted timing | `-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 | `-Video v.mp4 -Effect black` |
|
|
||||||
| [Create-Scenes.ps1](Create-Scenes.md) | Detect scene changes and create one clip per scene | `-Video P01.mp4 -Scene 0.3 -List` |
|
|
||||||
| [Create-ShortClip.ps1](Create-ShortClip.md) | Build a vertical short clip from an INI plan | `-Video v.mp4 -Plan plan.ini` |
|
|
||||||
| [Create-Turnaround.ps1](Create-Turnaround.md) | Arrange same-named images into a turnaround sheet | `-Source "C:\renders\CharacterA" -Target sheet.png` |
|
|
||||||
| [Crop-Clips.ps1](Crop-Clips.md) | Batch crop LTX-2, Kling and Wan videos to standard sizes | `-Source in_dir -Target out_dir` |
|
|
||||||
| [Get-Frame.ps1](Get-Frame.md) | Extract one frame to PNG with an automatic name | `-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 range of frames to PNG files | `-Video P01.mp4 -Frames "0-50"` |
|
|
||||||
| [Get-LastFrame.ps1](Get-LastFrame.md) | Extract one frame (default: the last) to a chosen PNG name | `-Video P01.mp4 -Image last.png` |
|
|
||||||
| [Get-VideoAudio.ps1](Get-VideoAudio.md) | Extract audio as WAV (default) or MP3 | `-Video P01.mp4 -MP3` |
|
|
||||||
| [Get-VideoInfo.ps1](Get-VideoInfo.md) | Show metadata stored in video comment tags | `-Video *.mp4` |
|
|
||||||
| [Stitch-InBetween.ps1](Stitch-InBetween.md) | Generate in-between frames with RIFE and 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** (list first, then create clips):
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Scene 0.3 -List
|
|
||||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Scene 0.3 -WhatIf
|
|
||||||
pwsh -NoProfile -File .\Create-Scenes.ps1 -Video P01.mp4 -Scene 0.3
|
|
||||||
```
|
|
||||||
|
|
||||||
**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:**
|
|
||||||
|
|
||||||
```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).
|
|
||||||
@@ -188,7 +188,7 @@ The output format is selected from the `-Target` file extension. Each format has
|
|||||||
- 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.
|
- 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.
|
- `-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.
|
- `-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 (`-1` for CPU, `0+` for GPU). The intermediate upscaled PNG is created under `%TEMP%` and deleted after the run (including on ffmpeg failure).
|
- `-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.
|
- `-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
|
### Notes
|
||||||
@@ -197,6 +197,13 @@ The output format is selected from the `-Target` file extension. Each format has
|
|||||||
- **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.
|
- **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).
|
- **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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||||
@@ -43,10 +43,11 @@ The output container is chosen from the `-Target` extension (`.mp4`, `.webm`, or
|
|||||||
- **`-StartFrame`** (optional): 0-based frame index to start cloning from (default: 0). Mutually exclusive with `-Frames`.
|
- **`-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`.
|
- **`-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:
|
- **`-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"`). 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"`)
|
- `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 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.
|
||||||
@@ -456,6 +457,13 @@ The output container is selected from the `-Target` file extension. Each contain
|
|||||||
- **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.
|
- **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.
|
- **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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||||
@@ -429,6 +429,12 @@ For 3 and 4 videos, S2 (and S3/S4 as needed) are probed to verify all sources ar
|
|||||||
- **Pixel Format:** yuv420p
|
- **Pixel Format:** yuv420p
|
||||||
- **Audio:** Copied from S1 (unless `-NoAudio` specified)
|
- **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
|
## Notes
|
||||||
|
|
||||||
- Requires `ffmpeg` and `ffprobe` in system PATH
|
- Requires `ffmpeg` and `ffprobe` in system PATH
|
||||||
@@ -8,10 +8,11 @@ Sources carrying a `.skip` marker keep that marker last: `P01.skip.mp4` becomes
|
|||||||
|
|
||||||
- **`-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.
|
- **`-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:
|
- **`-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"`). 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"`)
|
- `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 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.
|
||||||
@@ -151,6 +152,13 @@ This applies to every accepted container (`.mp4`, `.webm`, `.mkv`), matching is
|
|||||||
# Creates: P01-Clip.skip.mp4 scaled to 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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution (forwarded to `Clone-Video.ps1`):
|
This script supports `-WhatIf` and `-Confirm` for safe execution (forwarded to `Clone-Video.ps1`):
|
||||||
File renamed without changes.
@@ -218,6 +218,13 @@ The same logic applies to a transposed 9:16 frame:
|
|||||||
# CRF 18 instead of default CRF 8 / source bitrate
|
# 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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||||
@@ -7,10 +7,11 @@ The clips are saved next to the source video, named from the source filename and
|
|||||||
## Parameters
|
## 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.
|
- **`-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.
|
||||||
- **`-Scene`** (optional): Scene-change threshold between `0` and `1.0`. Lower values are more sensitive and produce more scenes. Default: `0.3`.
|
- **`-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).
|
- **`-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.
|
- **`-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` and `-Target` are managed by this script and must not be repeated.
|
- **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.
|
Refer to [Clone-Video.ps1](Clone-Video.md) for a full description of the encoding parameters.
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@ Refer to [Clone-Video.ps1](Clone-Video.md) for a full description of the encodin
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
1. **Detection**: ffmpeg runs the `scene` score filter with the `-Scene` threshold and reports the timestamp of each detected change.
|
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.
|
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.
|
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`.
|
4. **Scene list display**: the list is always printed, including with `-List`, `-WhatIf` and `-Confirm`.
|
||||||
@@ -31,24 +32,46 @@ 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.
|
||||||
|
|
||||||
|
After the list, the script prints `Scene level used: <level>`, the threshold that produced the list.
|
||||||
|
|
||||||
## Scene Threshold
|
## Scene Threshold
|
||||||
|
|
||||||
`-Scene` is passed to ffmpeg's `scene` score. The score measures how much a frame differs from the previous one, from `0` to `1`:
|
`-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.
|
- `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.
|
- 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.
|
- Higher values (e.g. `0.6`) only detect hard cuts.
|
||||||
- `1.0`: detects nothing, so the whole video becomes one scene.
|
- `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
|
## 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:
|
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:
|
||||||
@@ -77,14 +100,14 @@ Matching is case-insensitive. Files that do not match are never touched. Existin
|
|||||||
| `P01.mp4` | `P01-Scene-01.mp4`, `P01-Scene-12.mp4` | `P01-Scene-01.skip.mp4`, `P01-Other.mp4` |
|
| `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` |
|
| `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 `-Scene` value removes the extra clips from the first run.
|
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
|
## Examples
|
||||||
|
|
||||||
**List the scenes without creating anything:**
|
**List the scenes without creating anything:**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Create-Scenes.ps1 -Video P01.mp4 -Scene 0.4 -List
|
.\Create-Scenes.ps1 -Video P01.mp4 -Level 0.4 -List
|
||||||
```
|
```
|
||||||
|
|
||||||
**Create one clip per scene:**
|
**Create one clip per scene:**
|
||||||
@@ -97,7 +120,7 @@ Because the deletion happens before the first clip is created, a second run with
|
|||||||
**Scale and remove audio from every clip:**
|
**Scale and remove audio from every clip:**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\Create-Scenes.ps1 -Video P01 -Scene 0.3 -Size 720p -NoAudio
|
.\Create-Scenes.ps1 -Video P01 -Level 0.3 -Size 720p -NoAudio
|
||||||
```
|
```
|
||||||
|
|
||||||
**Custom suffix:**
|
**Custom suffix:**
|
||||||
@@ -136,5 +159,5 @@ This script supports `-WhatIf` and `-Confirm`. Both apply to the deletion of old
|
|||||||
- 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.
|
- 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 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.
|
- 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 `-Scene` to suit the footage.
|
- 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.
|
- 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.
|
||||||
File renamed without changes.
@@ -240,6 +240,13 @@ cd C:\renders\CharacterA
|
|||||||
.\Create-Turnaround.ps1 -Source .\renders -Target turnaround.webp -Quality 1 -Compression 6
|
.\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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||||
@@ -115,6 +115,7 @@ An invalid `-Size` value fails immediately, before any file is processed.
|
|||||||
- Preserves original quality and audio
|
- 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:
|
- **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.
|
- 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.
|
- 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.
|
- 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.
|
- **Separate target folder**: the source files are not changed. A file with the same name already in the target folder is deleted and replaced.
|
||||||
@@ -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
|
||||||
|
|
||||||
File renamed without changes.
@@ -10,13 +10,13 @@ Extracts a specific range of frames from a video file to PNG images.
|
|||||||
|
|
||||||
## Frame Range Formats
|
## Frame Range Formats
|
||||||
|
|
||||||
- **`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`). 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)
|
- **`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 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
|
||||||
```
|
```
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -104,6 +104,14 @@ Frame saved to: frame_0099.png
|
|||||||
# Creates: f_7.png
|
# 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
|
## Safe Execution
|
||||||
|
|
||||||
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
This script supports `-WhatIf` and `-Confirm` for safe execution:
|
||||||
@@ -53,7 +53,12 @@ Extracts audio in either WAV or MP3 format from a video file.
|
|||||||
- If the video file has no audio stream, the script displays a message and exits without creating an output file
|
- 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)
|
- WAV format uses PCM 16-bit encoding (lossless)
|
||||||
- MP3 format uses 320 kbps bitrate (highest quality)
|
- MP3 format uses 320 kbps bitrate (highest quality)
|
||||||
- If output file already exists, it will be overwritten
|
|
||||||
|
## 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
|
## Safe Execution
|
||||||
|
|
||||||
File renamed without changes.
@@ -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`.
|
||||||
File renamed without changes.
@@ -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).
|
||||||
File renamed without changes.
File renamed without changes.
@@ -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