diff --git a/src/core/services/subtitle-cue-parser.ts b/src/core/services/subtitle-cue-parser.ts index d118cfd..3f0cd4c 100644 --- a/src/core/services/subtitle-cue-parser.ts +++ b/src/core/services/subtitle-cue-parser.ts @@ -53,11 +53,76 @@ export function parseSrtCues(content: string): SubtitleCue[] { return cues; } -// Stub exports — will be implemented in subsequent tasks -export function parseAssCues(_content: string): SubtitleCue[] { - throw new Error('parseAssCues not yet implemented'); +const ASS_OVERRIDE_TAG_PATTERN = /\{[^}]*\}/g; + +const ASS_TIMING_PATTERN = /^(\d+):(\d{2}):(\d{2})\.(\d{1,2})$/; + +function parseAssTimestamp(raw: string): number | null { + const match = ASS_TIMING_PATTERN.exec(raw.trim()); + if (!match) { + return null; + } + const hours = Number(match[1]); + const minutes = Number(match[2]); + const seconds = Number(match[3]); + const centiseconds = Number(match[4]!.padEnd(2, '0')); + return hours * 3600 + minutes * 60 + seconds + centiseconds / 100; } +export function parseAssCues(content: string): SubtitleCue[] { + const cues: SubtitleCue[] = []; + const lines = content.split(/\r?\n/); + let inEventsSection = false; + + for (const line of lines) { + const trimmed = line.trim(); + + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + inEventsSection = trimmed.toLowerCase() === '[events]'; + continue; + } + + if (!inEventsSection) { + continue; + } + + if (!trimmed.startsWith('Dialogue:')) { + continue; + } + + // Split on first 9 commas (ASS v4+ has 10 fields; last is Text which can contain commas) + const afterPrefix = trimmed.slice('Dialogue:'.length); + const fields: string[] = []; + let remaining = afterPrefix; + for (let fieldIndex = 0; fieldIndex < 9; fieldIndex += 1) { + const commaIndex = remaining.indexOf(','); + if (commaIndex < 0) { + break; + } + fields.push(remaining.slice(0, commaIndex)); + remaining = remaining.slice(commaIndex + 1); + } + + if (fields.length < 9) { + continue; + } + + const startTime = parseAssTimestamp(fields[1]!); + const endTime = parseAssTimestamp(fields[2]!); + if (startTime === null || endTime === null) { + continue; + } + + const rawText = remaining.replace(ASS_OVERRIDE_TAG_PATTERN, '').trim(); + if (rawText) { + cues.push({ startTime, endTime, text: rawText }); + } + } + + return cues; +} + +// Stub export — will be implemented in Task 6 export function parseSubtitleCues(_content: string, _filename: string): SubtitleCue[] { throw new Error('parseSubtitleCues not yet implemented'); }