fix(cli): preserve subtitle cues across line endings (#5223)

* fix(cli): preserve subtitle cues across line endings

* refactor(cli): preserve newline handling while reconciling subtitle tests

* refactor(cli): normalize WebVTT line endings at the parser

* style(cli): format the retained subtitle test block
This commit is contained in:
Abdullah M. Nazif
2026-10-09 18:03:12 +00:00
committed by GitHub
parent 9e69821b8b
commit 9c1d3fbe62
2 changed files with 43 additions and 2 deletions
@@ -280,6 +280,47 @@ Short format
});
});
describe("subtitle line endings", () => {
it.each([
["srt", "\r\n"],
["srt", "\r"],
["vtt", "\r\n"],
["vtt", "\r"],
])("keeps separate %s cues with %j line endings", (ext, newline) => {
const timestamp = ext === "srt" ? "," : ".";
const lines = [
...(ext === "vtt" ? ["WEBVTT", ""] : []),
"1",
`00:00:01${timestamp}000 --> 00:00:03${timestamp}500`,
"<b>Hello</b> world",
"Again",
"",
"2",
`00:00:04${timestamp}000 --> 00:00:06${timestamp}000`,
"How are you",
"",
];
const { words, format } = loadTranscript(tmpFile(`captions.${ext}`, lines.join(newline)));
expect(format).toBe(ext);
expect(words).toEqual([
{ text: "Hello world Again", start: 1, end: 3.5, id: "w0" },
{ text: "How are you", start: 4, end: 6, id: "w1" },
]);
expect(formatSrt(words, { preGrouped: true })).toBe(
"1\n00:00:01,000 --> 00:00:03,500\nHello world Again\n\n2\n00:00:04,000 --> 00:00:06,000\nHow are you\n",
);
});
it("preserves WebVTT cues when newline forms are mixed", () => {
const source =
"WEBVTT\r\n\r\n00:01.000 --> 00:02.000\rFirst phrase\r\r00:03.000 --> 00:04.000\nSecond phrase\n";
expect(loadTranscript(tmpFile("mixed.vtt", source)).words).toEqual([
{ text: "First phrase", start: 1, end: 2, id: "w0" },
{ text: "Second phrase", start: 3, end: 4, id: "w1" },
]);
});
});
describe("caption formatting", () => {
it.each([
["R&D <config> next", "R&amp;D &lt;config&gt; next"],
+2 -2
View File
@@ -190,7 +190,7 @@ function parseOpenAI(data: Record<string, unknown>): Word[] {
function parseSrt(content: string): Word[] {
// SRT doesn't have word-level timestamps — parse as phrase-level entries.
// Each cue becomes one "word" entry (the full phrase).
const blocks = content.trim().split(/\n\n+/);
const blocks = content.replace(/\r\n?/g, "\n").trim().split(/\n\n+/);
const words: Word[] = [];
for (const block of blocks) {
@@ -227,7 +227,7 @@ function decodeVttText(text: string): string {
}
function parseVtt(content: string): Word[] {
const body = content.replace(/^WEBVTT[^\n]*\n/, "");
const body = content.replace(/\r\n?/g, "\n").replace(/^WEBVTT[^\n]*\n/, "");
// VTT is structurally similar to SRT (without numeric indices)
const blocks = body.trim().split(/\n\n+/);
const words: Word[] = [];