import fs from "fs/promises"; import path from "path"; import { createCanvas, loadImage } from "canvas"; import { renderFrameCaptions } from "@/lib/caption-renderer"; import { encodeVideo, extractFrames } from "@/lib/ffmpeg"; import { loadFonts } from "@/lib/fonts"; import { clearProgress, setProgress } from "@/lib/progress"; import { exportPath, renderDir, saveProject, videoPath } from "@/lib/storage"; import type { Project } from "@/types"; // ponytail: moved verbatim out of app/api/render/route.ts so /api/reel can reuse it export async function renderProject(project: Project) { const { id, captions, style, layout, animation, videoMeta, audioPath: ttsAudio } = project; const inputVideo = videoPath(id); const framesDirectory = renderDir(id); const outputVideo = exportPath(id); await fs.mkdir(framesDirectory, { recursive: true }); loadFonts(); const targetWidth = Math.min(videoMeta.width, 1080); const fps = Math.min(Math.round(videoMeta.fps) || 30, 30); const framePattern = path.join(framesDirectory, "frame_%06d.png"); setProgress(id, { percent: 5, stage: "extracting", message: "Extracting video frames...", }); await extractFrames(inputVideo, framePattern, fps, targetWidth); const frameFiles = (await fs.readdir(framesDirectory)) .filter((f) => f.endsWith(".png")) .sort(); const totalFrames = frameFiles.length; const frameDurationMs = 1000 / fps; const batchSize = 50; setProgress(id, { percent: 15, stage: "rendering", message: `Rendering captions on ${totalFrames} frames...`, }); for (let i = 0; i < totalFrames; i++) { const currentMs = i * frameDurationMs; const framePath = path.join(framesDirectory, frameFiles[i]); const img = await loadImage(framePath); const canvas = createCanvas(img.width, img.height); const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); renderFrameCaptions( ctx, captions, currentMs, style, layout, animation, img.width, img.height, ); await fs.writeFile(framePath, canvas.toBuffer("image/png")); if (i % batchSize === 0 || i === totalFrames - 1) { const percent = 15 + (i / totalFrames) * 70; setProgress(id, { percent, stage: "rendering", message: `Rendering frame ${i + 1} of ${totalFrames}...`, etaSeconds: Math.round(((totalFrames - i) / fps) * 0.5), }); } } setProgress(id, { percent: 90, stage: "encoding", message: "Merging video, captions, and voiceover...", }); await encodeVideo(inputVideo, framePattern, outputVideo, fps, ttsAudio); await fs.rm(framesDirectory, { recursive: true, force: true }); project.status = "done"; project.exportPath = outputVideo; await saveProject(project); setProgress(id, { percent: 100, stage: "done", message: "Render complete!", }); setTimeout(() => clearProgress(id), 60_000); }