File size: 2,960 Bytes
ef4c36f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
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);
}