scrape / lib /render-pipeline.ts
kjbytes's picture
push code
ef4c36f
Raw
History Blame Contribute Delete
2.96 kB
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);
}