File size: 2,311 Bytes
ef4c36f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | import { v4 as uuidv4 } from "uuid";
import type { Caption } from "@/types";
const BASE_WORD_MS = 180;
const CHAR_MS = 40;
const GAP_MS = 60;
// ponytail: phrases of 6 read far better than one word at a time, and each cue
// stays on screen ~6x longer without touching playback speed
export const WORDS_PER_CAPTION = 6;
function wordDuration(text: string): number {
return BASE_WORD_MS + text.length * CHAR_MS;
}
function groupWords(text: string, perGroup = WORDS_PER_CAPTION): string[] {
const words = text.trim().split(/\s+/).filter(Boolean);
const groups: string[] = [];
for (let i = 0; i < words.length; i += perGroup) {
groups.push(words.slice(i, i + perGroup).join(" "));
}
// ponytail: a 1-2 word orphan at the end flashes by — fold it into the previous phrase
if (groups.length > 1 && groups[groups.length - 1].split(" ").length <= 2) {
const orphan = groups.pop() as string;
groups[groups.length - 1] = `${groups[groups.length - 1]} ${orphan}`;
}
return groups;
}
export function autoSyncCaptions(text: string, speed = 1): Caption[] {
const phrases = groupWords(text);
let cursor = 0;
return phrases.map((phrase) => {
const duration =
phrase.split(" ").reduce((sum, w) => sum + wordDuration(w), 0) / speed;
const caption: Caption = {
id: uuidv4(),
text: phrase,
startMs: cursor,
endMs: cursor + duration,
};
cursor += duration + GAP_MS / speed;
return caption;
});
}
export function syncCaptionsToDuration(text: string, totalDurationMs: number): Caption[] {
const phrases = groupWords(text);
if (phrases.length === 0) return [];
// weight by character count so a long phrase holds the screen longer
const weights = phrases.map((p) => Math.max(p.length, 1));
const totalWeight = weights.reduce((a, b) => a + b, 0);
let cursor = 0;
return phrases.map((phrase, index) => {
const duration = (weights[index] / totalWeight) * totalDurationMs;
const caption: Caption = {
id: uuidv4(),
text: phrase.replace(/[,;:]+$/, ""),
startMs: Math.round(cursor),
endMs: Math.round(cursor + duration),
};
cursor += duration;
return caption;
});
}
export function captionsToText(captions: Caption[]): string {
return captions.map((c) => c.text).join(" ");
}
|