Download lib/timing.ts from d3evil4/scrape: direct link, hf CLI and curl.
- Browser
- Download file 2.31 kB
-
https://huggingface.co/spaces/d3evil4/scrape/resolve/main/lib/timing.ts
- Command line
-
hf download hf://spaces/d3evil4/scrape/lib/timing.ts
-
curl -L -o timing.ts https://huggingface.co/spaces/d3evil4/scrape/resolve/main/lib/timing.ts
2.31 kB
| 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(" "); | |
| } | |