diff options
Diffstat (limited to 'apps/web/src/subtitles/srt.ts')
| -rw-r--r-- | apps/web/src/subtitles/srt.ts | 98 |
1 files changed, 98 insertions, 0 deletions
diff --git a/apps/web/src/subtitles/srt.ts b/apps/web/src/subtitles/srt.ts new file mode 100644 index 00000000..197e96f9 --- /dev/null +++ b/apps/web/src/subtitles/srt.ts @@ -0,0 +1,98 @@ +import type { ParseSubtitleResult, SubtitleCue } from "./types"; + +const TIMESTAMP_SEPARATOR = /\s*-->\s*/; +const TIMESTAMP_PATTERN = + /^(\d{2}:\d{2}:\d{2}[,.]\d{1,3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{1,3})/; + +export function parseSrt({ input }: { input: string }): ParseSubtitleResult { + const normalized = input.replace(/\r\n?/g, "\n").trim(); + if (!normalized) { + return { + captions: [], + skippedCueCount: 0, + warnings: [], + }; + } + + const blocks = normalized.split(/\n{2,}/); + const cues: SubtitleCue[] = []; + let skippedCueCount = 0; + + for (const block of blocks) { + const lines = block + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); + + if (lines.length < 2) { + skippedCueCount += 1; + continue; + } + + const timestampIndex = TIMESTAMP_SEPARATOR.test(lines[0]) ? 0 : 1; + const timestampLine = lines[timestampIndex]; + if (!timestampLine || !TIMESTAMP_PATTERN.test(timestampLine)) { + skippedCueCount += 1; + continue; + } + + const textLines = lines.slice(timestampIndex + 1); + const text = textLines.join("\n").trim(); + if (!text) { + skippedCueCount += 1; + continue; + } + + const [rawStart, rawEnd] = timestampLine.split(TIMESTAMP_SEPARATOR); + if (!rawStart || !rawEnd) { + skippedCueCount += 1; + continue; + } + + const startTime = parseSrtTimestamp({ input: rawStart }); + const endTime = parseSrtTimestamp({ input: rawEnd }); + const duration = endTime - startTime; + + if ( + !Number.isFinite(startTime) || + !Number.isFinite(endTime) || + duration <= 0 + ) { + skippedCueCount += 1; + continue; + } + + cues.push({ + text, + startTime, + duration, + }); + } + + return { + captions: cues, + skippedCueCount, + warnings: [], + }; +} + +function parseSrtTimestamp({ input }: { input: string }): number { + const normalized = input.trim().replace(",", "."); + const match = normalized.match(/^(\d{2}):(\d{2}):(\d{2})\.(\d{1,3})$/); + if (!match) { + return Number.NaN; + } + + const [, hours, minutes, seconds, milliseconds] = match; + const parsedHours = Number.parseInt(hours, 10); + const parsedMinutes = Number.parseInt(minutes, 10); + const parsedSeconds = Number.parseInt(seconds, 10); + const parsedMilliseconds = Number.parseInt(milliseconds.padEnd(3, "0"), 10); + + return ( + parsedHours * 3600 + + parsedMinutes * 60 + + parsedSeconds + + parsedMilliseconds / 1000 + ); +} |
