Repository Public
1import { describe, expect, it } from "vitest";2import { cuesFromText, cuesFromWords, parseVtt, toVtt, wordsFromChars } from "../src/pipeline/captions";34const chars = (text: string, per = 0.06) => [...text].map((char, i) => ({ char, start: i * per, end: (i + 1) * per }));56describe("captions", () => {7 it("groups character timings into words", () => {8 const w = wordsFromChars(chars("La montre s’arrête."));9 expect(w.map((x) => x.text)).toEqual(["La", "montre", "s’arrête."]);10 expect(w[1].start).toBeCloseTo(0.18);11 });12 it("builds short cues that never overlap and respects the offset", () => {13 const cues = cuesFromWords(wordsFromChars(chars("It had measured not hours but the years he lived after pulling a sailor from the sea.")), 10);14 expect(cues.length).toBeGreaterThan(1);15 expect(cues[0].start).toBeGreaterThanOrEqual(10);16 for (const c of cues) expect(c.text.length).toBeLessThanOrEqual(34);17 for (let i = 1; i < cues.length; i++) expect(cues[i].start).toBeGreaterThanOrEqual(cues[i - 1].end);18 });19 it("spreads translated text over the spoken interval", () => {20 const cues = cuesFromText("Elle ne mesurait pas les heures, mais les années vécues après avoir sauvé un marin.", 2, 8);21 expect(cues[0].start).toBe(2);22 expect(cues.at(-1)!.end).toBeLessThanOrEqual(8.001);23 expect(cuesFromText("", 0, 5)).toEqual([]);24 });25 it("round-trips through WebVTT", () => {26 const cues = [{ start: 0.4, end: 2.05, text: "First line" }, { start: 2.1, end: 61.5, text: "Second --> line" }];27 const vtt = toVtt(cues);28 expect(vtt.startsWith("WEBVTT\n")).toBe(true);29 expect(vtt).toContain("00:00:00.400 --> 00:00:02.050");30 expect(vtt).toContain("00:01:01.500");31 const back = parseVtt(vtt);32 expect(back).toHaveLength(2);33 expect(back[1].text).toBe("Second → line");34 });35});36