mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(cli): preserve literal WebVTT cue text (#5236)
This commit is contained in:
@@ -281,6 +281,33 @@ Short format
|
||||
});
|
||||
|
||||
describe("caption formatting", () => {
|
||||
it.each([
|
||||
["R&D <config> next", "R&D <config> next"],
|
||||
["Literal < & references", "Literal &lt; &amp; references"],
|
||||
["3 < 4 and 5 > 2", "3 < 4 and 5 > 2"],
|
||||
["R&D --> next", "R&D --> next"],
|
||||
])("preserves literal WebVTT text %j on export and reimport", (text, payload) => {
|
||||
const words = [{ text, start: 1, end: 2, id: "w0" }];
|
||||
const output = formatVtt(words);
|
||||
expect(output).toBe(`WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n${payload}\n`);
|
||||
expect(loadTranscript(tmpFile("literal.vtt", output)).words).toEqual(words);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["R&D <config> next", "R&D <config> next"],
|
||||
[""quoted" 'text'", "\"quoted\" 'text'"],
|
||||
["& < <", "& < <"],
|
||||
["Literal &lt; &amp; references", "Literal < & references"],
|
||||
["<b>R&D</b> <config>", "R&D <config>"],
|
||||
["A B‎‏", "A\u00a0B\u200e\u200f"],
|
||||
["Keep &unknown;", "Keep &unknown;"],
|
||||
])("decodes the WebVTT cue payload %j once", (payload, text) => {
|
||||
const input = `WEBVTT\n\n00:00:01.000 --> 00:00:02.000\n${payload}\n`;
|
||||
expect(loadTranscript(tmpFile("entities.vtt", input)).words).toEqual([
|
||||
{ text, start: 1, end: 2, id: "w0" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("round-trips SRT cues through normalized words", () => {
|
||||
const srt = `1
|
||||
00:00:01,000 --> 00:00:03,500
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { extname, join } from "node:path";
|
||||
import { parseHTML } from "linkedom";
|
||||
|
||||
export interface Word {
|
||||
/** Stable identifier for referencing this word in overrides and compositions.
|
||||
@@ -217,6 +218,14 @@ function parseSrt(content: string): Word[] {
|
||||
return words;
|
||||
}
|
||||
|
||||
function decodeVttText(text: string): string {
|
||||
if (!text.includes("&")) return text;
|
||||
const { document } = parseHTML("<html><body></body></html>");
|
||||
const element = document.createElement("span");
|
||||
element.innerHTML = text.replace(/</g, "<");
|
||||
return element.textContent ?? "";
|
||||
}
|
||||
|
||||
function parseVtt(content: string): Word[] {
|
||||
const body = content.replace(/^WEBVTT[^\n]*\n/, "");
|
||||
// VTT is structurally similar to SRT (without numeric indices)
|
||||
@@ -239,7 +248,7 @@ function parseVtt(content: string): Word[] {
|
||||
if (!text) continue;
|
||||
|
||||
words.push({
|
||||
text,
|
||||
text: decodeVttText(text),
|
||||
start: parseVttTimestamp(startStr),
|
||||
end: parseVttTimestamp(endStr),
|
||||
});
|
||||
@@ -468,7 +477,8 @@ export function formatVtt(words: Word[], opts?: WordsToCuesOptions): string {
|
||||
"WEBVTT\n\n" +
|
||||
cues
|
||||
.map(
|
||||
(cue) => `${formatVttTimestamp(cue.start)} --> ${formatVttTimestamp(cue.end)}\n${cue.text}`,
|
||||
(cue) =>
|
||||
`${formatVttTimestamp(cue.start)} --> ${formatVttTimestamp(cue.end)}\n${cue.text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">")}`,
|
||||
)
|
||||
.join("\n\n") +
|
||||
"\n"
|
||||
|
||||
Reference in New Issue
Block a user