import { spawn } from "child_process"; import { StoryConfig } from "./config"; import * as path from "path"; import ffmpeg from "ffmpeg-static"; function escapeForFilter(filePath: string): string { return filePath.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/,/g, "\\,").replace(/'/g, "\\'"); } export async function createVideo( storyName: string, storyConfig: StoryConfig, imageFiles: string[], chunkDurations: number[], srtPath: string ): Promise { const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg"; const audioPath = path.resolve("stories", storyName, "final_audio", "final.mp3"); const videoPath = path.resolve("stories", storyName, "video", "final.mp4"); const totalDuration = chunkDurations.reduce((a, b) => a + b, 0); const resolution = storyConfig.config.export_settings?.resolution || "1024x1024"; const inputs = imageFiles.map((file) => ["-loop", "1", "-i", file]).flat(); inputs.push("-i", audioPath); const filterGraph = imageFiles .map((_, i) => { const duration = chunkDurations[i]; const zoompan = `zoompan=z='min(zoom+0.0015,1.5)':d=${25 * duration}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=${resolution}`; return `[${i}:v]${zoompan},fade=t=out:st=${duration - 1}:d=1[v${i}]`; }) .join(";"); const streamSpecifiers = imageFiles.map((_, i) => `[v${i}]`).join(""); const escapedSrt = escapeForFilter(srtPath); const concatGraph = `${filterGraph};${streamSpecifiers}concat=n=${imageFiles.length}:v=1:a=0,format=yuv420p[v0]`; const finalFilterGraph = `${concatGraph};[v0]subtitles='${escapedSrt}'[v]`; const args = [ "-y", ...inputs, "-filter_complex", finalFilterGraph, "-map", "[v]", "-map", `${imageFiles.length}:a`, "-c:v", "libx264", "-tune", "stillimage", "-c:a", "aac", "-b:a", "192k", "-pix_fmt", "yuv420p", "-t", totalDuration.toString(), videoPath, ]; const ffmpegProcess = spawn(ffmpegPath, args); return new Promise((resolve, reject) => { ffmpegProcess.on("close", (code: number | null) => { if (code === 0) { resolve(); } else { reject(new Error(`ffmpeg process exited with code ${code}`)); } }); }); }