Files
Video-Processing-Scripts/Create-FullSubtitle.ps1
2026-05-19 14:55:09 -04:00

246 lines
7.2 KiB
PowerShell

<#
.SYNOPSIS
Creates a full subtitle file from a video file from three sources: a reference video and two SRT subtitle files.
This script will extract the video time length and the last subtitle number used in the first subtitle file,
then will apply them to the second subtitle file for proper timing and numbering.
The output file will contain all subtitles from the first and second subtitle files with proper timing and numbering.
.DESCRIPTION
This script creates a full subtitle file from a video file from three sources: a reference video and two SRT subtitle files.
The output file contains all subtitles from the first and second subtitle files with proper timing and numbering.
.PARAMETER Video
Path to the video file.
.PARAMETER Subtitle1
Path to the first subtitle file.
.PARAMETER Subtitle2
Path to the second subtitle file.
.PARAMETER Output
Path to the output subtitle file.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
[string]$Video,
[Parameter(Mandatory = $true)]
[string]$Subtitle1,
[Parameter(Mandatory = $true)]
[string]$Subtitle2,
[Parameter(Mandatory = $true)]
[string]$Output
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
#region Helper Functions
function Get-VideoDuration {
param([string]$VideoPath)
if (-not (Test-Path -LiteralPath $VideoPath -PathType Leaf)) {
throw "Video file not found: $VideoPath"
}
$frameCountOutput = ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 "$VideoPath" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "ffprobe failed to get video frame count"
}
$fpsOutput = ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of csv=p=0 "$VideoPath" 2>&1
if ($LASTEXITCODE -ne 0) {
throw "ffprobe failed to get video fps"
}
$frameCount = [int]$frameCountOutput.Trim()
# r_frame_rate is returned as a fraction e.g. "24/1" or "30000/1001"
$fpsParts = $fpsOutput.Trim() -split '/'
$fps = [double]$fpsParts[0] / [double]$fpsParts[1]
return $frameCount / $fps
}
function ConvertTo-TimeSpan {
param([string]$SrtTime)
# SRT format: HH:MM:SS,mmm
$pattern = '^(\d{2}):(\d{2}):(\d{2}),(\d{3})$'
if (-not ($SrtTime -match $pattern)) {
throw "Invalid SRT time format: $SrtTime"
}
$hours = [int]$matches[1]
$minutes = [int]$matches[2]
$seconds = [int]$matches[3]
$milliseconds = [int]$matches[4]
return New-TimeSpan -Hours $hours -Minutes $minutes -Seconds $seconds -Milliseconds $milliseconds
}
function ConvertFrom-TimeSpan {
param([TimeSpan]$TimeSpan)
$hours = [int][Math]::Floor($TimeSpan.TotalHours)
$minutes = $TimeSpan.Minutes
$seconds = $TimeSpan.Seconds
$milliseconds = $TimeSpan.Milliseconds
return '{0:D2}:{1:D2}:{2:D2},{3:D3}' -f $hours, $minutes, $seconds, $milliseconds
}
function Add-TimeToSrt {
param(
[string]$SrtTime,
[double]$OffsetSeconds
)
$timeSpan = ConvertTo-TimeSpan -SrtTime $SrtTime
$offsetSpan = [TimeSpan]::FromSeconds($OffsetSeconds)
$newTimeSpan = $timeSpan.Add($offsetSpan)
return ConvertFrom-TimeSpan -TimeSpan $newTimeSpan
}
function Get-SrtContent {
param([string]$SrtPath)
if (-not (Test-Path -LiteralPath $SrtPath -PathType Leaf)) {
throw "Subtitle file not found: $SrtPath"
}
$content = Get-Content -LiteralPath $SrtPath -Raw
$entries = @()
# Split by double newline to separate subtitle entries
$rawEntries = $content -split "`r?`n`r?`n"
foreach ($rawEntry in $rawEntries) {
$lines = $rawEntry.Trim() -split "`r?`n"
if ($lines.Count -lt 2) {
continue
}
# First line should be the subtitle number
$number = 0
if (-not ([int]::TryParse($lines[0].Trim(), [ref]$number))) {
continue
}
# Second line should be the timing
$timingLine = $lines[1].Trim()
$timingPattern = '^(.+?) --> (.+?)$'
if (-not ($timingLine -match $timingPattern)) {
continue
}
$startTime = $matches[1]
$endTime = $matches[2]
# Remaining lines are the text
$textLines = $lines[2..($lines.Count - 1)]
$text = ($textLines -join "`n").Trim()
$entries += [PSCustomObject]@{
Number = $number
StartTime = $startTime
EndTime = $endTime
Text = $text
}
}
return $entries
}
function Export-SrtFile {
[CmdletBinding(SupportsShouldProcess)]
param(
[array]$Entries,
[string]$OutputPath
)
$outputDir = Split-Path -Parent $OutputPath
if ($outputDir -and -not (Test-Path -LiteralPath $outputDir)) {
if ($PSCmdlet.ShouldProcess($outputDir, 'Create output directory')) {
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
}
}
$srtContent = @()
for ($i = 0; $i -lt $Entries.Count; $i++) {
$entry = $Entries[$i]
$srtContent += $entry.Number
$srtContent += "$($entry.StartTime) --> $($entry.EndTime)"
$srtContent += $entry.Text
if ($i -lt $Entries.Count - 1) {
$srtContent += ''
}
}
if ($PSCmdlet.ShouldProcess($OutputPath, 'Write combined SRT file')) {
$srtContent -join "`r`n" | Out-File -LiteralPath $OutputPath -Encoding UTF8 -NoNewline
}
}
#endregion
#region Main Script
Write-Host "Parsing first subtitle file: $Subtitle1"
$subtitle1Entries = Get-SrtContent -SrtPath $Subtitle1
Write-Host " Found $($subtitle1Entries.Count) entries"
if ($subtitle1Entries.Count -eq 0) {
throw "No valid subtitle entries found in: $Subtitle1"
}
$lastNumber = $subtitle1Entries[-1].Number
Write-Host " Last subtitle number: $lastNumber"
Write-Host "Extracting video duration from: $Video"
$videoDuration = Get-VideoDuration -VideoPath $Video
$videoDurationFormatted = ConvertFrom-TimeSpan -TimeSpan ([TimeSpan]::FromSeconds($videoDuration))
Write-Host " Video duration: $videoDurationFormatted (${videoDuration}s)"
Write-Host "Parsing second subtitle file: $Subtitle2"
$subtitle2Entries = Get-SrtContent -SrtPath $Subtitle2
Write-Host " Found $($subtitle2Entries.Count) entries"
if ($subtitle2Entries.Count -eq 0) {
throw "No valid subtitle entries found in: $Subtitle2"
}
Write-Host "Adjusting second subtitle timing and numbering..."
$adjustedSubtitle2Entries = @()
for ($i = 0; $i -lt $subtitle2Entries.Count; $i++) {
$entry = $subtitle2Entries[$i]
$newNumber = $lastNumber + $i + 1
$newStartTime = Add-TimeToSrt -SrtTime $entry.StartTime -OffsetSeconds $videoDuration
$newEndTime = Add-TimeToSrt -SrtTime $entry.EndTime -OffsetSeconds $videoDuration
$adjustedSubtitle2Entries += [PSCustomObject]@{
Number = $newNumber
StartTime = $newStartTime
EndTime = $newEndTime
Text = $entry.Text
}
}
Write-Host " Adjusted $($adjustedSubtitle2Entries.Count) entries"
$combinedEntries = $subtitle1Entries + $adjustedSubtitle2Entries
Write-Host "Combined total: $($combinedEntries.Count) entries"
Write-Host "Writing output file: $Output"
Export-SrtFile -Entries $combinedEntries -OutputPath $Output
Write-Host "Output file created successfully: $Output"
#endregion