import { generateId } from "@designcombo/timeline"; import { ICaption } from "@designcombo/types"; interface Word { start: number; end: number; word: string; } interface ICaptionLine { text: string; words: Word[]; width: number; start: number; end: number; } export const generateCaption = ( captionLine: ICaptionLine, fontInfo: FontInfo, options: Options, sourceUrl: string ): ICaption => { const caption = { id: generateId(), type: "caption", name: "Caption", display: { from: options.displayFrom + captionLine.start * 1000, to: options.displayFrom + captionLine.end * 1000 }, metadata: { sourceUrl, parentId: options.parentId }, details: { // top: 100, appearedColor: "#FFFFFF", activeColor: "#50FF12", activeFillColor: "#7E12FF", color: "#DADADA", backgroundColor: "transparent", borderColor: "#000000", borderWidth: 5, text: captionLine.text, fontSize: fontInfo.fontSize, width: options.containerWidth, fontFamily: fontInfo.fontFamily, fontUrl: fontInfo.fontUrl, textAlign: "center", linesPerCaption: options.linesPerCaption, words: captionLine.words.map((w) => ({ ...w, start: w.start * 1000, end: w.end * 1000 })) } as unknown }; return caption as ICaption; }; interface Word { word: string; start: number; end: number; confidence: number; } interface CaptionsInput { sourceUrl: string; results: { main: { words: Word[]; }; }; } function createCaptionLines( input: CaptionsInput, fontInfo: FontInfo, options: Options ): ICaptionLine[] { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); if (!context) return []; context.font = `${fontInfo.fontSize}px ${fontInfo.fontFamily}`; const captionLines: ICaptionLine[] = []; const words: Word[] = input.results.main.words; let currentLine: ICaptionLine = { text: "", words: [], width: 0, start: words.length > 0 ? words[0].start : 0, end: 0 }; let linesCount = 0; words.forEach((wordObj, index) => { const wordWidth = context.measureText(wordObj.word).width; if ( currentLine.width + wordWidth > options.containerWidth - 100 || currentLine.text.endsWith(".") ) { const advance = currentLine.text.endsWith("."); // Check if it's time to start a new caption set if (linesCount + 1 === options.linesPerCaption || advance) { // Only push when lines count is correct captionLines.push(currentLine); linesCount = 0; // Reset currentLine for the next set of lines currentLine = { text: "", words: [], width: 0, start: wordObj.start, end: wordObj.end }; } else { linesCount += 1; // Reset currentLine.width but keep other details to continue accumulation currentLine.width = 0; } } // Accumulate words and width for the current line currentLine.text += (currentLine.text ? " " : "") + wordObj.word; currentLine.words.push(wordObj); currentLine.width += wordWidth; currentLine.end = wordObj.end; // Push the final line if it's the last word if (index === words.length - 1 && currentLine.text) { captionLines.push(currentLine); } }); return captionLines; } interface FontInfo { fontFamily: string; fontUrl: string; fontSize: number; } interface Options { containerWidth: number; linesPerCaption: number; parentId: string; displayFrom: number; } export function generateCaptions( input: CaptionsInput, fontInfo: FontInfo, options: Options ): ICaption[] { const captionLines = createCaptionLines(input, fontInfo, options); const captions = captionLines.map((line) => generateCaption(line, fontInfo, options, input.sourceUrl) ); return captions; }