import { z } from "zod"; import { type Caption, type CaptionPage, type CaptionLine, type OrientationEnum, MusicVolumeEnum, } from "../types/shorts"; import { AvailableComponentsEnum, type OrientationConfig } from "./types"; export const shortVideoSchema = z.object({ scenes: z.array( z.object({ captions: z.custom(), audio: z.object({ url: z.string(), duration: z.number(), }), video: z.string(), }), ), config: z.object({ paddingBack: z.number().optional(), captionPosition: z.enum(["top", "center", "bottom"]).optional(), captionBackgroundColor: z.string().optional(), durationMs: z.number(), musicVolume: z.nativeEnum(MusicVolumeEnum).optional(), }), music: z.object({ file: z.string(), url: z.string(), start: z.number(), end: z.number(), }), }); export function createCaptionPages({ captions, lineMaxLength, lineCount, maxDistanceMs, }: { captions: Caption[]; lineMaxLength: number; lineCount: number; maxDistanceMs: number; }) { const pages = []; let currentPage: CaptionPage = { startMs: 0, endMs: 0, lines: [], }; let currentLine: CaptionLine = { texts: [], }; captions.forEach((caption, i) => { // Check if we need to start a new page due to time gap if (i > 0 && caption.startMs - currentPage.endMs > maxDistanceMs) { // Add current line if not empty if (currentLine.texts.length > 0) { currentPage.lines.push(currentLine); } // Add current page if not empty if (currentPage.lines.length > 0) { pages.push(currentPage); } // Start new page currentPage = { startMs: caption.startMs, endMs: caption.endMs, lines: [], }; currentLine = { texts: [], }; } // Check if adding this caption exceeds the line length const currentLineText = currentLine.texts.map((t) => t.text).join(" "); if ( currentLine.texts.length > 0 && currentLineText.length + 1 + caption.text.length > lineMaxLength ) { // Line is full, add it to current page currentPage.lines.push(currentLine); currentLine = { texts: [], }; // Check if page is full if (currentPage.lines.length >= lineCount) { // Page is full, add it to pages pages.push(currentPage); // Start new page currentPage = { startMs: caption.startMs, endMs: caption.endMs, lines: [], }; } } // Add caption to current line currentLine.texts.push({ text: caption.text, startMs: caption.startMs, endMs: caption.endMs, }); // Update page timing currentPage.endMs = caption.endMs; if (i === 0 || currentPage.startMs === 0) { currentPage.startMs = caption.startMs; } else { currentPage.startMs = Math.min(currentPage.startMs, caption.startMs); } }); // Don't forget to add the last line and page if (currentLine.texts.length > 0) { currentPage.lines.push(currentLine); } if (currentPage.lines.length > 0) { pages.push(currentPage); } return pages; } export function getOrientationConfig(orientation: OrientationEnum) { const config: Record = { portrait: { width: 1080, height: 1920, component: AvailableComponentsEnum.PortraitVideo, }, }; return config[orientation]; } export function calculateVolume( level: MusicVolumeEnum = MusicVolumeEnum.high, ): [number, boolean] { switch (level) { case "muted": return [0, true]; case "low": return [0.2, false]; case "medium": return [0.45, false]; case "high": return [0.7, false]; default: return [0.7, false]; } }