// The caption-track rule (lib/videoStatus.ts): which English VTT a video's // transcript is read from, by name and then by content. // // Run with: node_modules/.bin/tsx --test common/lib/captionTrack.test.ts import { after, test } from "node:test"; import assert from "node:assert/strict"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { CAPTION_TRACK_RULE_VERSION, TRANSCRIPT_PIN_FILENAME, captionInputs, englishVttsByPreference, readEnglishVttCues, readSubTracks, resolvePrimaryVtt, } from "./videoStatus"; const ROOT = mkdtempSync(path.join(tmpdir(), "caption-track-")); after(() => rmSync(ROOT, { recursive: true, force: true })); const fixture = (name: string) => readFileSync(path.join(import.meta.dirname, "__fixtures__", name), "utf8"); const ROLLING = fixture("vtt-rolling.vtt"); const CUE_BLOCKS = fixture("vtt-cue-blocks.vtt"); const EMPTY = "WEBVTT\nKind: captions\nLanguage: en\n\n"; let n = 0; function videoDir(files: Record): string { const dir = path.join(ROOT, `v${n++}`); mkdirSync(dir, { recursive: true }); for (const [name, body] of Object.entries(files)) writeFileSync(path.join(dir, name), body); return dir; } test("en-orig ranks above en; regional above auto-translated; translations are not English", () => { const entries = [ "transcript.en-en-US.vtt", "transcript.en.vtt", "transcript.es-en-US.vtt", "transcript.en-GB.vtt", "transcript.en-orig.vtt", "transcript.json", ]; assert.deepEqual(englishVttsByPreference(entries), [ "transcript.en-orig.vtt", "transcript.en.vtt", "transcript.en-GB.vtt", "transcript.en-en-US.vtt", ]); assert.equal(resolvePrimaryVtt(entries), "transcript.en-orig.vtt"); assert.equal(resolvePrimaryVtt(["transcript.en.vtt", "transcript.en-US.vtt"]), "transcript.en.vtt"); assert.equal(resolvePrimaryVtt(["transcript.es.vtt"]), null); }); test("the operator's pin puts transcript.en.vtt first, and is a caption input", () => { const entries = ["transcript.en-orig.vtt", "transcript.en.vtt", TRANSCRIPT_PIN_FILENAME]; assert.equal(resolvePrimaryVtt(entries), "transcript.en.vtt"); assert.deepEqual(captionInputs(entries), [ "transcript.en.vtt", "transcript.en-orig.vtt", TRANSCRIPT_PIN_FILENAME, ]); // A pin with no English VTT is no input. assert.deepEqual(captionInputs([TRANSCRIPT_PIN_FILENAME]), []); }); test("readEnglishVttCues reads en-orig when both tracks have text", async () => { const dir = videoDir({ "transcript.en.vtt": CUE_BLOCKS, "transcript.en-orig.vtt": ROLLING, }); const got = await readEnglishVttCues(dir); assert.equal(got?.filename, "transcript.en-orig.vtt"); assert.equal(got?.cues[0].text, "are talking about the harbor"); }); test("readEnglishVttCues falls back past a track with no cues", async () => { const dir = videoDir({ "transcript.en-orig.vtt": EMPTY, "transcript.en.vtt": CUE_BLOCKS, }); const got = await readEnglishVttCues(dir); assert.equal(got?.filename, "transcript.en.vtt"); assert.equal(got?.cues.length, 7); }); test("readEnglishVttCues: every track empty is the first track with no cues; none is null", async () => { const dir = videoDir({ "transcript.en-orig.vtt": EMPTY, "transcript.en.vtt": EMPTY, }); assert.deepEqual(await readEnglishVttCues(dir), { filename: "transcript.en-orig.vtt", cues: [] }); assert.equal(await readEnglishVttCues(videoDir({ "transcript.es.vtt": ROLLING })), null); }); test("readSubTracks lists no English VTT: those are caption tracks (lib/captionTracks.ts)", async () => { const dir = videoDir({ "transcript.en.vtt": CUE_BLOCKS, "transcript.en-orig.vtt": ROLLING, "transcript.es.vtt": ROLLING, }); const tracks = (await readSubTracks(dir)).map((t) => t.track).sort(); assert.deepEqual(tracks, ["es"]); }); test("umtool's copy of the caption-track rule version matches", () => { const src = readFileSync( path.join(import.meta.dirname, "..", "..", "umtool", "report-to-video", "cues.mjs"), "utf8", ); const m = src.match(/export const CAPTION_TRACK_RULE_VERSION = (\d+);/); assert.equal(Number(m?.[1]), CAPTION_TRACK_RULE_VERSION); });