import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { provenanceFromMetadata, readVttProvenance, resolveVttProvenance, sniffVttProvenance, vttTrackName, } from "./subtitleProvenance"; // Run with: pnpm --filter yt-dlp-transcript-common exec tsx --test lib/subtitleProvenance.test.ts // Shaped after real yt-dlp --write-auto-subs output: every cue carries // `align:start position:0%` and inline word timings. const ASR_HEAD = `WEBVTT Kind: captions Language: en 00:00:00.030 --> 00:00:03.919 align:start position:0% so<00:00:00.719> today<00:00:01.199> we're<00:00:01.439> going<00:00:01.680> to 00:00:03.919 --> 00:00:03.929 align:start position:0% so today we're going to 00:00:03.929 --> 00:00:07.070 align:start position:0% so today we're going to talk<00:00:04.320> about<00:00:04.639> the<00:00:04.879> whole<00:00:05.199> thing `; // Shaped after a human-uploaded/manual track: plain cues, no cue settings, no // inline word timings. const MANUAL_HEAD = `WEBVTT Kind: captions Language: en 00:00:01.000 --> 00:00:04.000 So today we're going to talk about the whole thing. 00:00:04.000 --> 00:00:08.500 It's a long story, but bear with me. 00:00:08.500 --> 00:00:12.000 Here we go. `; test("sniffVttProvenance classifies YouTube ASR output as asr", () => { assert.equal(sniffVttProvenance(ASR_HEAD), "asr"); }); test("sniffVttProvenance classifies a manual caption track as manual", () => { assert.equal(sniffVttProvenance(MANUAL_HEAD), "manual"); }); test("sniffVttProvenance detects asr from word timings alone", () => { // Some ASR tracks are served without the align/position cue settings but keep // the inline word timings — still machine-generated. const head = ASR_HEAD.replace(/ align:start position:\d+%/g, ""); assert.equal(sniffVttProvenance(head), "asr"); }); test("sniffVttProvenance returns unknown for too little evidence", () => { assert.equal(sniffVttProvenance(""), "unknown"); assert.equal(sniffVttProvenance("not a vtt at all"), "unknown"); assert.equal( sniffVttProvenance("WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nOnly one cue.\n"), "unknown", ); }); test("sniffVttProvenance returns unknown when markers are present but sparse", () => { const head = `WEBVTT 00:00:00.000 --> 00:00:02.000 align:start position:0% one 00:00:02.000 --> 00:00:04.000 two 00:00:04.000 --> 00:00:06.000 three 00:00:06.000 --> 00:00:08.000 four `; assert.equal(sniffVttProvenance(head), "unknown"); }); test("provenanceFromMetadata prefers subtitles over automatic_captions", () => { const meta = { subtitles: { en: [{ ext: "vtt" }] }, automatic_captions: { en: [{ ext: "vtt" }], "en-orig": [{ ext: "vtt" }] }, }; assert.equal(provenanceFromMetadata(meta, "en"), "manual"); assert.equal(provenanceFromMetadata(meta, "en-orig"), "asr"); assert.equal(provenanceFromMetadata(meta, "fr"), "unknown"); assert.equal(provenanceFromMetadata(null, "en"), "unknown"); assert.equal(provenanceFromMetadata({}, "en"), "unknown"); }); test("vttTrackName extracts the language segment", () => { assert.equal(vttTrackName("transcript.en.vtt"), "en"); assert.equal(vttTrackName("transcript.en-US.vtt"), "en-US"); assert.equal(vttTrackName("transcript.json"), null); assert.equal(vttTrackName("transcript.cues.json"), null); }); test("readVttProvenance sniffs only the head of a file on disk", async () => { const dir = await mkdtemp(path.join(tmpdir(), "subprov-")); await writeFile(path.join(dir, "transcript.en.vtt"), ASR_HEAD); await writeFile(path.join(dir, "transcript.en-US.vtt"), MANUAL_HEAD); assert.equal(await readVttProvenance(dir, "transcript.en.vtt"), "asr"); assert.equal(await readVttProvenance(dir, "transcript.en-US.vtt"), "manual"); assert.equal(await readVttProvenance(dir, "missing.vtt"), "unknown"); }); test("resolveVttProvenance falls back to metadata for an undecidable file", async () => { const dir = await mkdtemp(path.join(tmpdir(), "subprov-")); // One cue only: the sniff can't judge it. await writeFile( path.join(dir, "transcript.en.vtt"), "WEBVTT\n\n00:00:00.000 --> 00:00:02.000\nhi\n", ); assert.equal(await resolveVttProvenance(dir, "transcript.en.vtt"), "unknown"); await writeFile( path.join(dir, "metadata.info.json"), JSON.stringify({ automatic_captions: { en: [{ ext: "vtt" }] } }), ); assert.equal(await resolveVttProvenance(dir, "transcript.en.vtt"), "asr"); });