Created
July 21, 2026 09:11
-
-
Save h8rt3rmin8r/2773b55fc12bb0a666f2f903195f8c57 to your computer and use it in GitHub Desktop.
Parse a SubRip (.srt) subtitle file into structured JSON (default) or CSV.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Parse a SubRip (.srt) subtitle file into structured JSON (default) or CSV. | |
| .DESCRIPTION | |
| Reads a SubRip Text (.srt) file, parses every subtitle cue into a uniform | |
| record, and emits the collection as JSON (default) or CSV. The output is | |
| intended for downstream tooling: search, diffing, transcript analysis, or | |
| reshaping into other subtitle formats. | |
| Each cue becomes one object with a fixed, standard set of fields: | |
| index the cue's sequence number as it appears in the file | |
| time_start normalized start timecode, HH:MM:SS,mmm | |
| time_end normalized end timecode, HH:MM:SS,mmm | |
| ms_start start time in whole milliseconds (derived) | |
| ms_end end time in whole milliseconds (derived) | |
| ms_duration ms_end minus ms_start (derived) | |
| speaker extracted speaker label, or null (heuristic; see below) | |
| content the cue text, multiple lines joined with LF, tags preserved | |
| x1, x2, y1, y2 optional SubRip display coordinates, or null | |
| SubRip coverage. The parser follows the de facto SubRip specification: a | |
| file has no header or footer; cues are separated by a blank line; each cue | |
| is an integer index line, a timecode line of the form | |
| "HH:MM:SS,mmm --> HH:MM:SS,mmm", and one or more text lines. It tolerates | |
| the real-world variations the format accumulated over the years: a leading | |
| UTF-8 byte-order mark, CRLF or lone-CR line endings, trailing whitespace on | |
| lines, a period used in place of the millisecond comma, short (under three | |
| digit) millisecond fields, and the optional bounding-box coordinate suffix | |
| on the timecode line ("X1:left X2:right Y1:top Y2:bottom"). Inline styling | |
| tags (bold, italic, underline, font color) are preserved verbatim in the | |
| content field rather than stripped, since they are part of the cue. | |
| Speaker extraction. The "Name: dialogue" prefix seen in Zoom, Teams, and | |
| Otter transcripts is NOT part of the SubRip format; it is a transcript | |
| convention. This script detects it heuristically and, when found, lifts the | |
| label into the speaker field and removes it from content. The heuristic | |
| requires the colon to be followed by whitespace (so time-like text such as | |
| "at 3:30" is not misread) and rejects labels containing "?" or "!". It can | |
| still misfire on genuine lines like "Note: ..."; pass -NoSpeaker to turn it | |
| off entirely and keep every cue's text intact. | |
| Output destination. By default the result is written to a file beside the | |
| input with the same base name and a .json (or .csv) extension. Writing will | |
| NOT overwrite an existing output file unless -Force is given. Pass | |
| -StreamOutput to skip the file and send the payload to standard output for | |
| piping. All files this script writes are UTF-8 with no BOM and LF endings. | |
| Constraints. Requires PowerShell 7 or later (it uses ConvertTo-Json | |
| -AsArray to guarantee array-shaped output even for a single cue). Supports | |
| -WhatIf and -Confirm for the file-write step. Exit codes: 0 success, | |
| 1 a parse or write check failed (for example, no valid cues, or the output | |
| exists and -Force was not supplied), 2 an environment precondition failed | |
| (the input file is missing or is not a .srt file). | |
| .PARAMETER Path | |
| Path to the input subtitle file. Must exist and end with the .srt | |
| extension. The path is read literally, so names containing wildcard | |
| metacharacters or awkward whitespace are handled verbatim. | |
| Alias: p | |
| .PARAMETER Csv | |
| Emit CSV instead of the default JSON. The columns are the same fixed field | |
| set as the JSON objects, quoted per RFC 4180 so that commas and embedded | |
| line breaks in the content survive intact. | |
| Alias: c | |
| .PARAMETER StreamOutput | |
| Skip writing a file and send the payload to standard output for pipeline | |
| processing (for example, piping into ConvertFrom-Json). Suppresses the | |
| informational progress messages so only the payload reaches the stream. | |
| Alias: stdout | |
| .PARAMETER Force | |
| Overwrite the output file if it already exists. Without this switch the | |
| script refuses to clobber an existing file and exits 1. Ignored when | |
| -StreamOutput is used, since no file is written. | |
| Alias: f | |
| .PARAMETER NoSpeaker | |
| Disable the "Name: dialogue" speaker-extraction heuristic. Every cue's text | |
| is kept verbatim in content and the speaker field is null throughout. | |
| Alias: n | |
| .PARAMETER Quiet | |
| Suppress informational and success messages. Warnings and errors still | |
| emit. Implied while streaming to standard output. | |
| Alias: q | |
| .PARAMETER Help | |
| Print this help text to the terminal. | |
| Alias: h | |
| .EXAMPLE | |
| .\ConvertFrom-Srt.ps1 interview.srt | |
| Parses interview.srt and writes interview.json beside it (the most common | |
| invocation). | |
| .EXAMPLE | |
| .\ConvertFrom-Srt.ps1 -Path interview.srt -Csv -Force | |
| Writes interview.csv, overwriting any existing file of that name. | |
| .EXAMPLE | |
| .\ConvertFrom-Srt.ps1 interview.srt -StreamOutput | ConvertFrom-Json | | |
| Where-Object speaker -eq 'William Thompson' | |
| Streams the JSON to the pipeline and filters cues down to a single speaker | |
| without ever touching disk. | |
| .EXAMPLE | |
| .\ConvertFrom-Srt.ps1 captions.srt -NoSpeaker -stdout | |
| Streams JSON with speaker detection disabled, so a leading "Word:" in any | |
| cue is left as part of the text. | |
| .NOTES | |
| Exit codes: 0 success, 1 parse or write check failed, 2 environment | |
| precondition failed. Companion contract: srt-json.schema.json. | |
| #> | |
| [CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium',DefaultParameterSetName='Default')] | |
| Param( | |
| [Parameter(Mandatory=$true,Position=0,ParameterSetName='Default')] | |
| [Alias("p")] | |
| [string]$Path, | |
| [Parameter(Mandatory=$false,ParameterSetName='Default')] | |
| [Alias("c")] | |
| [Switch]$Csv, | |
| [Parameter(Mandatory=$false,ParameterSetName='Default')] | |
| [Alias("stdout")] | |
| [Switch]$StreamOutput, | |
| [Parameter(Mandatory=$false,ParameterSetName='Default')] | |
| [Alias("f")] | |
| [Switch]$Force, | |
| [Parameter(Mandatory=$false,ParameterSetName='Default')] | |
| [Alias("n")] | |
| [Switch]$NoSpeaker, | |
| [Parameter(Mandatory=$false,ParameterSetName='Default')] | |
| [Alias("q")] | |
| [Switch]$Quiet, | |
| [Parameter(Mandatory=$true,ParameterSetName='HelpText')] | |
| [Alias("h")] | |
| [Switch]$Help | |
| ) | |
| #_______________________________________________________________________________ | |
| ## Declare Functions | |
| function Assert-PSVersion { | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$false)] | |
| [version]$Minimum = '7.0' | |
| ) | |
| $current = $PSVersionTable.PSVersion | |
| if ($current -lt $Minimum) { | |
| Write-Host ("ALERT: PowerShell {0}+ required; running {1}. Relaunch with 'pwsh'." -f $Minimum, $current) -ForegroundColor Red | |
| exit 2 | |
| } | |
| } | |
| function Write-Log { | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$true,Position=0)] | |
| [string]$Message, | |
| [Parameter(Mandatory=$false)] | |
| [ValidateSet('Info','Success','Warn','Error','Debug')] | |
| [string]$Level = 'Info', | |
| [Parameter(Mandatory=$false)] | |
| [string]$Source = $null | |
| ) | |
| if ($script:LogSilent -and $Level -ne 'Error') { return } | |
| if ($script:LogQuiet -and (@('Info','Success','Debug') -contains $Level)) { return } | |
| $stamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss.fff') | |
| $tag = if ($Source) { "[$Source] " } else { '' } | |
| $label = $Level.ToUpper().PadRight(7) | |
| $color = switch ($Level) { | |
| 'Info' { 'Gray' } | |
| 'Success' { 'Green' } | |
| 'Warn' { 'Yellow' } | |
| 'Error' { 'Red' } | |
| 'Debug' { 'DarkGray' } | |
| } | |
| Write-Host ("{0} {1}{2} {3}" -f $stamp, $tag, $label, $Message) -ForegroundColor $color | |
| } | |
| function ConvertTo-SrtMilliseconds { | |
| # Parse a single SubRip timecode (HH:MM:SS,mmm, comma or period, with | |
| # a tolerant 1-to-3 digit millisecond field) into whole milliseconds. | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$true)] | |
| [string]$Timecode | |
| ) | |
| $m = [regex]::Match($Timecode, '(\d{1,3}):(\d{1,2}):(\d{1,2})[,.](\d{1,3})') | |
| if (-not $m.Success) { | |
| throw "Unparseable timecode: '$Timecode'" | |
| } | |
| $h = [int]$m.Groups[1].Value | |
| $mn = [int]$m.Groups[2].Value | |
| $s = [int]$m.Groups[3].Value | |
| $ms = [int]($m.Groups[4].Value.PadRight(3, '0')) | |
| return ($h * 3600000) + ($mn * 60000) + ($s * 1000) + $ms | |
| } | |
| function Format-SrtTimecode { | |
| # Render whole milliseconds back to the canonical HH:MM:SS,mmm form. | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$true)] | |
| [int]$Milliseconds | |
| ) | |
| $h = [math]::Floor($Milliseconds / 3600000) | |
| $rem = $Milliseconds % 3600000 | |
| $mn = [math]::Floor($rem / 60000) | |
| $rem = $rem % 60000 | |
| $s = [math]::Floor($rem / 1000) | |
| $ms = $rem % 1000 | |
| return ('{0:00}:{1:00}:{2:00},{3:000}' -f $h, $mn, $s, $ms) | |
| } | |
| function Split-SrtSpeaker { | |
| # Heuristically lift a leading "Name: dialogue" transcript label out of | |
| # the cue text. Returns a hashtable with Speaker (string or $null) and | |
| # Content (the text with the label removed when one was found). | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$true)] | |
| [AllowEmptyCollection()] | |
| [string[]]$TextLines | |
| ) | |
| $tail = @() | |
| if ($TextLines.Count -gt 1) { $tail = @($TextLines[1..($TextLines.Count - 1)]) } | |
| $first = if ($TextLines.Count -ge 1) { $TextLines[0] } else { '' } | |
| # Same-line label: colon must be followed by whitespace, dialogue after. | |
| $a = [regex]::Match($first, '^(?<sp>[\p{L}][^:\r\n]{0,48}?):[ \t]+(?<rest>\S.*)$') | |
| if ($a.Success -and ($a.Groups['sp'].Value -notmatch '[?!]')) { | |
| $lines = @($a.Groups['rest'].Value) + $tail | |
| return @{ Speaker = $a.Groups['sp'].Value.Trim(); Content = ($lines -join "`n") } | |
| } | |
| # Label alone on the first line, dialogue on the following lines. | |
| $b = [regex]::Match($first, '^(?<sp>[\p{L}][^:\r\n]{0,48}?):[ \t]*$') | |
| if ($b.Success -and ($b.Groups['sp'].Value -notmatch '[?!]') -and ($tail.Count -gt 0)) { | |
| return @{ Speaker = $b.Groups['sp'].Value.Trim(); Content = ($tail -join "`n") } | |
| } | |
| return @{ Speaker = $null; Content = ($TextLines -join "`n") } | |
| } | |
| function ConvertFrom-SrtText { | |
| # Parse raw SubRip text into an ordered array of cue objects. | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory=$true)] | |
| [string]$Text, | |
| [Parameter(Mandatory=$false)] | |
| [switch]$ExtractSpeaker | |
| ) | |
| # Strip a UTF-8 BOM and normalize all line endings to LF. | |
| if ($Text.Length -gt 0 -and $Text[0] -eq [char]0xFEFF) { | |
| $Text = $Text.Substring(1) | |
| } | |
| $Text = $Text -replace "`r`n", "`n" | |
| $Text = $Text -replace "`r", "`n" | |
| # Drop trailing whitespace per line so blank-line splitting is reliable. | |
| $Text = (($Text -split "`n") | ForEach-Object { $_.TrimEnd() }) -join "`n" | |
| $blocks = $Text.Trim("`n") -split "`n`n+" | |
| $cues = New-Object System.Collections.Generic.List[object] | |
| $auto = 0 | |
| foreach ($block in $blocks) { | |
| if ([string]::IsNullOrWhiteSpace($block)) { continue } | |
| $auto++ | |
| $lines = $block -split "`n" | |
| # Locate the timecode line (the one carrying the arrow separator). | |
| $arrowIdx = -1 | |
| for ($i = 0; $i -lt $lines.Count; $i++) { | |
| if ($lines[$i] -match '-->') { $arrowIdx = $i; break } | |
| } | |
| if ($arrowIdx -lt 0) { | |
| throw "Cue $auto has no timecode line (missing '-->'): '$($lines -join ' / ')'" | |
| } | |
| $tc = $lines[$arrowIdx] | |
| $tm = [regex]::Match($tc, '(\d{1,3}:\d{1,2}:\d{1,2}[,.]\d{1,3})\s*-->\s*(\d{1,3}:\d{1,2}:\d{1,2}[,.]\d{1,3})') | |
| if (-not $tm.Success) { | |
| throw "Cue $auto has an unparseable timecode line: '$tc'" | |
| } | |
| $startMs = ConvertTo-SrtMilliseconds -Timecode $tm.Groups[1].Value | |
| $endMs = ConvertTo-SrtMilliseconds -Timecode $tm.Groups[2].Value | |
| # Index: the last numeric line before the timecode, else auto number. | |
| $index = $auto | |
| if ($arrowIdx -ge 1) { | |
| $before = @($lines[0..($arrowIdx - 1)] | Where-Object { $_ -match '^\s*\d+\s*$' }) | |
| if ($before.Count -gt 0) { $index = [int]($before[-1].Trim()) } | |
| } | |
| # Optional display coordinates on the timecode line. | |
| $x1 = $x2 = $y1 = $y2 = $null | |
| if ($tc -match 'X1:\s*(\d+)') { $x1 = [int]$Matches[1] } | |
| if ($tc -match 'X2:\s*(\d+)') { $x2 = [int]$Matches[1] } | |
| if ($tc -match 'Y1:\s*(\d+)') { $y1 = [int]$Matches[1] } | |
| if ($tc -match 'Y2:\s*(\d+)') { $y2 = [int]$Matches[1] } | |
| # Text is every line after the timecode line. | |
| $textLines = @() | |
| if ($arrowIdx -lt ($lines.Count - 1)) { | |
| $textLines = @($lines[($arrowIdx + 1)..($lines.Count - 1)]) | |
| } | |
| $speaker = $null | |
| $content = ($textLines -join "`n") | |
| if ($ExtractSpeaker) { | |
| $split = Split-SrtSpeaker -TextLines $textLines | |
| $speaker = $split.Speaker | |
| $content = $split.Content | |
| } | |
| $cues.Add([pscustomobject][ordered]@{ | |
| index = $index | |
| time_start = (Format-SrtTimecode -Milliseconds $startMs) | |
| time_end = (Format-SrtTimecode -Milliseconds $endMs) | |
| ms_start = $startMs | |
| ms_end = $endMs | |
| ms_duration = ($endMs - $startMs) | |
| speaker = $speaker | |
| content = $content | |
| x1 = $x1 | |
| x2 = $x2 | |
| y1 = $y1 | |
| y2 = $y2 | |
| }) | |
| } | |
| return $cues.ToArray() | |
| } | |
| #_______________________________________________________________________________ | |
| ## Declare Variables and Arrays | |
| $script:LogQuiet = $false | |
| $script:LogSilent = $false | |
| $ThisScriptPath = $MyInvocation.MyCommand.Path | |
| #_______________________________________________________________________________ | |
| ## Execute Operations | |
| # Catch help text requests | |
| if (($Help) -or ($PSCmdlet.ParameterSetName -eq 'HelpText')) { | |
| Get-Help $ThisScriptPath -Detailed | |
| exit 0 | |
| } | |
| Assert-PSVersion -Minimum '7.0' | |
| # Streaming to stdout implies quiet so only the payload reaches the stream. | |
| if ($Quiet -or $StreamOutput) { $script:LogQuiet = $true } | |
| # Environment preconditions: the input must exist and be a .srt file. | |
| if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { | |
| Write-Host "ALERT: input file not found: $Path" -ForegroundColor Red | |
| exit 2 | |
| } | |
| $fullInput = (Get-Item -LiteralPath $Path).FullName | |
| if ([System.IO.Path]::GetExtension($fullInput).ToLowerInvariant() -ne '.srt') { | |
| Write-Host "ALERT: input must be a .srt file: $fullInput" -ForegroundColor Red | |
| exit 2 | |
| } | |
| $raw = Get-Content -LiteralPath $fullInput -Raw -Encoding utf8 | |
| if ([string]::IsNullOrWhiteSpace($raw)) { | |
| Write-Error "Input file is empty: $fullInput" | |
| exit 1 | |
| } | |
| Write-Log "Parsing $fullInput" -Level Info -Source 'srt' | |
| $cues = ConvertFrom-SrtText -Text $raw -ExtractSpeaker:(-not $NoSpeaker) | |
| if ($cues.Count -lt 1) { | |
| Write-Error "No valid subtitle cues found in: $fullInput" | |
| exit 1 | |
| } | |
| Write-Log ("Parsed {0} cue(s)" -f $cues.Count) -Level Success -Source 'srt' | |
| # Build the payload in the requested format. | |
| if ($Csv) { | |
| $payload = (($cues | ConvertTo-Csv -NoTypeInformation) -join "`n") | |
| } else { | |
| $payload = ($cues | ConvertTo-Json -Depth 5 -AsArray) | |
| } | |
| # Emit: stream to stdout, or write a file beside the input. | |
| if ($StreamOutput) { | |
| Write-Output $payload | |
| exit 0 | |
| } | |
| $ext = if ($Csv) { 'csv' } else { 'json' } | |
| $outDir = [System.IO.Path]::GetDirectoryName($fullInput) | |
| $outBase = [System.IO.Path]::GetFileNameWithoutExtension($fullInput) | |
| $outPath = [System.IO.Path]::Combine($outDir, "$outBase.$ext") | |
| if ((Test-Path -LiteralPath $outPath) -and (-not $Force)) { | |
| Write-Error "Output already exists: $outPath. Re-run with -Force to overwrite." | |
| exit 1 | |
| } | |
| if ($PSCmdlet.ShouldProcess($outPath, 'Write output file')) { | |
| $utf8NoBom = New-Object System.Text.UTF8Encoding($false) | |
| [System.IO.File]::WriteAllText($outPath, ($payload + "`n"), $utf8NoBom) | |
| Write-Log ("Wrote {0} cue(s) to {1}" -f $cues.Count, $outPath) -Level Success -Source 'srt' | |
| } | |
| exit 0 | |
| #_______________________________________________________________________________ | |
| ## End of script |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "$id": "https://schemas.shruggie.tech/data/srt-json.schema.json", | |
| "title": "SubRip cue array (ConvertFrom-Srt.ps1 output)", | |
| "description": "Describes the JSON produced by ConvertFrom-Srt.ps1: the top-level value is an array, and each element is one parsed SubRip (.srt) subtitle cue. Every cue carries the same fixed set of fields, so the array is a uniform table that maps one-to-one onto the CSV output. Timecodes are normalized to the canonical HH:MM:SS,mmm form, and each cue also exposes its times pre-converted to whole milliseconds for arithmetic and sorting. Fields that a given cue does not carry (a speaker label, display coordinates) are present as JSON null rather than being omitted, so the field set is identical for every element.", | |
| "type": "array", | |
| "items": { | |
| "$ref": "#/$defs/cue" | |
| }, | |
| "$defs": { | |
| "cue": { | |
| "type": "object", | |
| "title": "SubRip cue", | |
| "description": "A single subtitle entry from the source file: one sequence number, one start/end time pair, and the text shown during that interval, plus derived and optional fields.", | |
| "additionalProperties": false, | |
| "required": [ | |
| "index", | |
| "time_start", | |
| "time_end", | |
| "ms_start", | |
| "ms_end", | |
| "ms_duration", | |
| "speaker", | |
| "content", | |
| "x1", | |
| "x2", | |
| "y1", | |
| "y2" | |
| ], | |
| "properties": { | |
| "index": { | |
| "type": "integer", | |
| "minimum": 0, | |
| "description": "The cue's sequence number, taken verbatim from the numeric line at the top of the block in the source file. In a well-formed SubRip file these run 1, 2, 3, ... in order, but the value here is whatever the file actually stated; the parser does not renumber. If a block had no readable numeric line, this falls back to the cue's ordinal position in the file (1 for the first cue, and so on).", | |
| "examples": [1, 2, 47] | |
| }, | |
| "time_start": { | |
| "type": "string", | |
| "pattern": "^\\d{2,}:[0-5]\\d:[0-5]\\d,\\d{3}$", | |
| "description": "The moment the subtitle appears, normalized to the canonical SubRip timecode form HH:MM:SS,mmm (hours, minutes, seconds, then a comma and exactly three digits of milliseconds). Normalization means: the millisecond separator is always a comma even if the source used a period, hours are zero-padded to at least two digits, and short millisecond fields in the source are right-padded to three digits. Note the comma decimal separator, which is what distinguishes SubRip from WebVTT (which uses a period).", | |
| "examples": ["00:00:01,000", "01:23:04,000"] | |
| }, | |
| "time_end": { | |
| "type": "string", | |
| "pattern": "^\\d{2,}:[0-5]\\d:[0-5]\\d,\\d{3}$", | |
| "description": "The moment the subtitle disappears, in the same normalized HH:MM:SS,mmm form as time_start. In a well-formed cue this is at or after time_start.", | |
| "examples": ["00:00:04,000", "01:23:07,500"] | |
| }, | |
| "ms_start": { | |
| "type": "integer", | |
| "minimum": 0, | |
| "description": "time_start expressed as a single integer count of milliseconds from zero (hours*3600000 + minutes*60000 + seconds*1000 + milliseconds). Provided so downstream code can sort, compare, offset, or difference cue times without re-parsing the timecode string.", | |
| "examples": [1000, 4984000] | |
| }, | |
| "ms_end": { | |
| "type": "integer", | |
| "minimum": 0, | |
| "description": "time_end expressed as whole milliseconds from zero, computed the same way as ms_start.", | |
| "examples": [4000, 4987500] | |
| }, | |
| "ms_duration": { | |
| "type": "integer", | |
| "description": "How long the subtitle is on screen, in milliseconds: ms_end minus ms_start. This is normally non-negative. It can be negative only if the source cue's end time precedes its start time, which is a defect in the source file rather than in the conversion; the value is reported as-is so such defects remain visible.", | |
| "examples": [3000, 3750] | |
| }, | |
| "speaker": { | |
| "type": ["string", "null"], | |
| "description": "The speaker label when the cue text began with a 'Name: dialogue' prefix, with the label lifted out and the 'Name: ' removed from content. This prefix is a transcript convention (Zoom, Teams, Otter, and similar), NOT part of the SubRip format itself, so extraction is heuristic: it fires only when a colon is immediately followed by whitespace (so time-of-day text such as 'meet at 3:30' is not misread) and the candidate label contains no '?' or '!'. It is null when no label was detected, and null for every cue when the converter was run with -NoSpeaker. Because the heuristic can still misfire on genuine lines like 'Note: ...', treat this field as best-effort rather than authoritative.", | |
| "examples": ["Narrator", "Speaker 1", null] | |
| }, | |
| "content": { | |
| "type": "string", | |
| "description": "The subtitle text for this cue. When the source cue spanned several lines, those lines are joined with a single line feed (\\n) in this one string. Inline SubRip styling tags (<b>, <i>, <u>, and <font color=\"...\">) are preserved verbatim rather than stripped, since they are part of the displayed text. When a speaker label was extracted into the speaker field, the leading 'Name: ' is removed from here; otherwise the text is left exactly as written. May be an empty string if the source cue had no text lines.", | |
| "examples": [ | |
| "Plain subtitle text.", | |
| "A caption that wraps onto\na second line.", | |
| "<i>Emphasized text.</i>" | |
| ] | |
| }, | |
| "x1": { | |
| "type": ["integer", "null"], | |
| "minimum": 0, | |
| "description": "Left edge, in pixels, of the optional SubRip display bounding box. SubRip allows a timecode line to carry a position suffix of the form 'X1:left X2:right Y1:top Y2:bottom'; when present, its four values populate x1/x2/y1/y2. This is an optional and rarely used part of the format, so the value is null for the common case where the cue had no coordinates.", | |
| "examples": [40, null] | |
| }, | |
| "x2": { | |
| "type": ["integer", "null"], | |
| "minimum": 0, | |
| "description": "Right edge, in pixels, of the optional SubRip display bounding box (the X2 value of an 'X1:.. X2:.. Y1:.. Y2:..' suffix). Null when the cue carried no coordinates.", | |
| "examples": [600, null] | |
| }, | |
| "y1": { | |
| "type": ["integer", "null"], | |
| "minimum": 0, | |
| "description": "Top edge, in pixels, of the optional SubRip display bounding box (the Y1 value). Null when the cue carried no coordinates.", | |
| "examples": [20, null] | |
| }, | |
| "y2": { | |
| "type": ["integer", "null"], | |
| "minimum": 0, | |
| "description": "Bottom edge, in pixels, of the optional SubRip display bounding box (the Y2 value). Null when the cue carried no coordinates.", | |
| "examples": [50, null] | |
| } | |
| } | |
| } | |
| }, | |
| "examples": [ | |
| [ | |
| { | |
| "index": 1, | |
| "time_start": "00:00:01,000", | |
| "time_end": "00:00:04,000", | |
| "ms_start": 1000, | |
| "ms_end": 4000, | |
| "ms_duration": 3000, | |
| "speaker": "Narrator", | |
| "content": "The sample file begins here.", | |
| "x1": null, | |
| "x2": null, | |
| "y1": null, | |
| "y2": null | |
| }, | |
| { | |
| "index": 2, | |
| "time_start": "00:00:04,500", | |
| "time_end": "00:00:08,250", | |
| "ms_start": 4500, | |
| "ms_end": 8250, | |
| "ms_duration": 3750, | |
| "speaker": null, | |
| "content": "A second line of subtitle text,\nspanning two rows.", | |
| "x1": null, | |
| "x2": null, | |
| "y1": null, | |
| "y2": null | |
| } | |
| ] | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment