chore: drop legacy nexrender render-worker; remove @supabase, stripe, @nexrender/core, tsx packages
V2 render orchestrator (render-svc) + future node-agent Go binary replace the entire server/ directory. All dead dependencies uninstalled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Generated
-138
@@ -28,8 +28,6 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@supabase/ssr": "^0.10.3",
|
||||
"@supabase/supabase-js": "^2.106.1",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -43,7 +41,6 @@
|
||||
"react-hook-form": "^7.76.0",
|
||||
"react-konva": "^18.2.16",
|
||||
"react-rnd": "^10.5.3",
|
||||
"stripe": "^22.1.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"use-image": "^1.1.4",
|
||||
"use-undoable": "^5.0.0",
|
||||
@@ -3455,102 +3452,6 @@
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.106.1.tgz",
|
||||
"integrity": "sha512-7eyheXfAGwkB9bZewJPs+N3UYt6kra2JG6mIxNEgbkvcO15PLD1e75PTIUEYYl3zrifm3GrpShVl7QZxKrXO/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.106.1.tgz",
|
||||
"integrity": "sha512-XbOPnR2mW7jp/EcW447xmGwCa+/Wc00Hkw8t4tUIJjRsHQ4xAESsLKcyLRhRJjJoUnJVXUlC+w0wUxUCM7CG2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/phoenix": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz",
|
||||
"integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.106.1.tgz",
|
||||
"integrity": "sha512-Qbn6d2lqiqeaBX1Uko0e/hL90dtQGRN6CG2wMVQtJpRFstlVW45qmUTyTOsiB8dYUWu1fWYo4YzJuDbokGv3tQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.106.1.tgz",
|
||||
"integrity": "sha512-eQCYri5E8KsjpDgC7g28cOOS2britjUWdNSJluFMainqrMRepzjOnaxqXc3RoAz7H0dxmBrfLUNF6NGP8C+YaA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/phoenix": "^0.4.2",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/ssr": {
|
||||
"version": "0.10.3",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.3.tgz",
|
||||
"integrity": "sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@supabase/supabase-js": "^2.105.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.106.1.tgz",
|
||||
"integrity": "sha512-HWcLIhqinhWKpOQ3WzglR2unjW0eh9J7yOu3IZrZNIEkraK4La/HDvTqndljGsNw0itPtyHhuKBxRoPG1VUARw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iceberg-js": "^0.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.106.1",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.106.1.tgz",
|
||||
"integrity": "sha512-gP4HurGkGu7Z3xoOCjtAI17BKKp7jpsmwY0Ssbsks9XQRzJ7ZhK7LxfLdBSYgUdgZCQgjRK+Mr7+cl4Gxrk0Rw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.106.1",
|
||||
"@supabase/functions-js": "2.106.1",
|
||||
"@supabase/postgrest-js": "2.106.1",
|
||||
"@supabase/realtime-js": "2.106.1",
|
||||
"@supabase/storage-js": "2.106.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@swc/core-darwin-arm64": {
|
||||
"version": "1.15.40",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.40.tgz",
|
||||
@@ -5336,19 +5237,6 @@
|
||||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
@@ -7176,15 +7064,6 @@
|
||||
"ms": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/iceberg-js": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/icu-minify": {
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.12.0.tgz",
|
||||
@@ -10527,23 +10406,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "22.1.1",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-22.1.1.tgz",
|
||||
"integrity": "sha512-cmodIYP27tBkJ8G7DuGgWw0PFuemlFZbuF3Wwr1TrjFjUa3T7NIgCe6TVwX8BO2ynu+xtTuDGfHafNDCPt9lXA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz",
|
||||
|
||||
+1
-7
@@ -6,8 +6,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"render-worker": "tsx server/render-worker.ts"
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -19,7 +18,6 @@
|
||||
"@fontsource-variable/plus-jakarta-sans": "^5.2.8",
|
||||
"@fontsource/vazirmatn": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@nexrender/core": "^1.46.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||
@@ -30,8 +28,6 @@
|
||||
"@radix-ui/react-switch": "^1.2.6",
|
||||
"@radix-ui/react-tabs": "^1.1.13",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@supabase/ssr": "^0.10.3",
|
||||
"@supabase/supabase-js": "^2.106.1",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -45,7 +41,6 @@
|
||||
"react-hook-form": "^7.76.0",
|
||||
"react-konva": "^18.2.16",
|
||||
"react-rnd": "^10.5.3",
|
||||
"stripe": "^22.1.1",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"use-image": "^1.1.4",
|
||||
"use-undoable": "^5.0.0",
|
||||
@@ -61,7 +56,6 @@
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tsx": "^4.19.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
import type { RenderScene, RenderSettings } from "../src/lib/render-schemas";
|
||||
import { RESOLUTION_DIMENSIONS } from "../src/lib/render-schemas";
|
||||
|
||||
export interface NexrenderAsset {
|
||||
type: string;
|
||||
layerName?: string;
|
||||
composition?: string;
|
||||
property?: string;
|
||||
value?: string | number;
|
||||
src?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
time?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface NexrenderJob {
|
||||
template: {
|
||||
src: string;
|
||||
composition: string;
|
||||
frameStart?: number;
|
||||
frameEnd?: number;
|
||||
};
|
||||
assets: NexrenderAsset[];
|
||||
actions?: {
|
||||
postrender?: Array<Record<string, unknown>>;
|
||||
};
|
||||
onRenderProgress?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function layerToAsset(
|
||||
layer: RenderScene["layers"][number],
|
||||
scene: RenderScene,
|
||||
sceneIndex: number
|
||||
): NexrenderAsset | null {
|
||||
const time = sceneIndex * scene.duration;
|
||||
|
||||
switch (layer.type) {
|
||||
case "text": {
|
||||
const text =
|
||||
typeof layer.props.text === "string" ? layer.props.text : "Text";
|
||||
return {
|
||||
type: "data",
|
||||
layerName: `Scene${sceneIndex + 1}_Text`,
|
||||
composition: scene.name,
|
||||
property: "Source Text",
|
||||
value: text,
|
||||
time,
|
||||
};
|
||||
}
|
||||
case "image": {
|
||||
const src = typeof layer.props.src === "string" ? layer.props.src : "";
|
||||
if (!src) return null;
|
||||
return {
|
||||
type: "image",
|
||||
layerName: `Scene${sceneIndex + 1}_Image`,
|
||||
composition: scene.name,
|
||||
src,
|
||||
width: layer.width,
|
||||
height: layer.height,
|
||||
time,
|
||||
};
|
||||
}
|
||||
case "video": {
|
||||
const src = typeof layer.props.src === "string" ? layer.props.src : "";
|
||||
if (!src) return null;
|
||||
return {
|
||||
type: "video",
|
||||
layerName: `Scene${sceneIndex + 1}_Video`,
|
||||
composition: scene.name,
|
||||
src,
|
||||
time,
|
||||
};
|
||||
}
|
||||
case "shape":
|
||||
return {
|
||||
type: "data",
|
||||
layerName: `Scene${sceneIndex + 1}_Shape`,
|
||||
composition: scene.name,
|
||||
property: "Opacity",
|
||||
value: Math.round(layer.opacity * 100),
|
||||
time,
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildNexrenderJob(
|
||||
scenes: RenderScene[],
|
||||
settings: RenderSettings,
|
||||
jobId: string,
|
||||
outputPath: string
|
||||
): NexrenderJob {
|
||||
const templateSrc =
|
||||
process.env.NEXRENDER_TEMPLATE_SRC ??
|
||||
"file:///templates/creatorstudio-base.aep";
|
||||
const composition =
|
||||
process.env.NEXRENDER_COMPOSITION ?? "CreatorStudio_Main";
|
||||
|
||||
const { width, height } = RESOLUTION_DIMENSIONS[settings.resolution];
|
||||
const totalDuration = scenes.reduce((sum, scene) => sum + scene.duration, 0);
|
||||
const frameEnd = Math.ceil(totalDuration * settings.fps);
|
||||
|
||||
const assets: NexrenderAsset[] = [
|
||||
{
|
||||
type: "data",
|
||||
layerName: "Settings",
|
||||
composition,
|
||||
property: "Width",
|
||||
value: width,
|
||||
},
|
||||
{
|
||||
type: "data",
|
||||
layerName: "Settings",
|
||||
composition,
|
||||
property: "Height",
|
||||
value: height,
|
||||
},
|
||||
{
|
||||
type: "data",
|
||||
layerName: "Settings",
|
||||
composition,
|
||||
property: "Frame Rate",
|
||||
value: settings.fps,
|
||||
},
|
||||
];
|
||||
|
||||
scenes.forEach((scene, sceneIndex) => {
|
||||
assets.push({
|
||||
type: "data",
|
||||
layerName: `Scene${sceneIndex + 1}`,
|
||||
composition,
|
||||
property: "Duration",
|
||||
value: scene.duration,
|
||||
time: sceneIndex * scene.duration,
|
||||
});
|
||||
|
||||
const sortedLayers = [...scene.layers].sort(
|
||||
(a, b) => a.zIndex - b.zIndex
|
||||
);
|
||||
|
||||
sortedLayers.forEach((layer) => {
|
||||
const asset = layerToAsset(layer, scene, sceneIndex);
|
||||
if (asset) assets.push(asset);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
template: {
|
||||
src: templateSrc,
|
||||
composition,
|
||||
frameStart: 0,
|
||||
frameEnd,
|
||||
},
|
||||
assets,
|
||||
actions: {
|
||||
postrender: [
|
||||
{
|
||||
module: "@nexrender/action-copy",
|
||||
output: outputPath,
|
||||
},
|
||||
],
|
||||
},
|
||||
metadata: {
|
||||
jobId,
|
||||
resolution: settings.resolution,
|
||||
fps: settings.fps,
|
||||
format: settings.format,
|
||||
},
|
||||
};
|
||||
}
|
||||
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
declare module "@nexrender/core" {
|
||||
export interface NexrenderRenderOptions {
|
||||
workPath?: string;
|
||||
binary?: string;
|
||||
skipCleanup?: boolean;
|
||||
onProgress?: (job: { metadata?: Record<string, unknown> }, percent: number) => void;
|
||||
}
|
||||
|
||||
export function render(
|
||||
job: unknown,
|
||||
options?: NexrenderRenderOptions
|
||||
): Promise<string | { output?: string }>;
|
||||
}
|
||||
@@ -1,247 +0,0 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
|
||||
import { buildNexrenderJob } from "./nexrender-job-builder";
|
||||
import type { RenderScene, RenderSettings } from "../src/lib/render-schemas";
|
||||
|
||||
function getSupabase() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
if (!url || !key) {
|
||||
throw new Error("Supabase env vars required for render worker");
|
||||
}
|
||||
return createClient(url, key, {
|
||||
auth: { autoRefreshToken: false, persistSession: false },
|
||||
});
|
||||
}
|
||||
|
||||
async function updateJob(
|
||||
jobId: string,
|
||||
updates: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
const supabase = getSupabase();
|
||||
await supabase.from("render_jobs").update(updates).eq("id", jobId);
|
||||
}
|
||||
|
||||
async function uploadToStorage(
|
||||
jobId: string,
|
||||
filePath: string
|
||||
): Promise<string> {
|
||||
const supabase = getSupabase();
|
||||
const buffer = await readFile(filePath);
|
||||
const storagePath = `${jobId}/output.mp4`;
|
||||
|
||||
const { error } = await supabase.storage
|
||||
.from("renders")
|
||||
.upload(storagePath, buffer, {
|
||||
contentType: "video/mp4",
|
||||
upsert: true,
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
const { data } = supabase.storage.from("renders").getPublicUrl(storagePath);
|
||||
return data.publicUrl;
|
||||
}
|
||||
|
||||
async function submitToNexrenderServer(
|
||||
job: ReturnType<typeof buildNexrenderJob>
|
||||
): Promise<string> {
|
||||
const serverUrl = process.env.NEXRENDER_SERVER_URL;
|
||||
if (!serverUrl) {
|
||||
throw new Error("NEXRENDER_SERVER_URL not configured");
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${serverUrl.replace(/\/$/, "")}/api/v1/jobs`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(job),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Nexrender server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const payload = (await response.json()) as { uid?: string; id?: string };
|
||||
return payload.uid ?? payload.id ?? "unknown";
|
||||
}
|
||||
|
||||
async function renderWithCore(
|
||||
job: ReturnType<typeof buildNexrenderJob>,
|
||||
onProgress: (percent: number, message: string) => Promise<void>
|
||||
): Promise<string> {
|
||||
const { render } = await import("@nexrender/core");
|
||||
const workPath =
|
||||
process.env.NEXRENDER_WORKPATH ?? path.join(process.cwd(), ".nexrender");
|
||||
|
||||
await onProgress(10, "Starting After Effects render…");
|
||||
|
||||
const result = await render(job, {
|
||||
workPath,
|
||||
binary: process.env.NEXRENDER_BINARY,
|
||||
skipCleanup: false,
|
||||
onProgress: (
|
||||
nexJob: { metadata?: Record<string, unknown> },
|
||||
percent: number
|
||||
) => {
|
||||
const label = nexJob.metadata?.progressMessage as string | undefined;
|
||||
void onProgress(
|
||||
Math.min(95, Math.round(percent * 100)),
|
||||
label ?? "Rendering composition…"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const outputPath =
|
||||
typeof result === "string"
|
||||
? result
|
||||
: ((result as { output?: string })?.output ??
|
||||
job.actions?.postrender?.[0]?.output);
|
||||
|
||||
if (!outputPath || typeof outputPath !== "string") {
|
||||
throw new Error("Nexrender did not return output path");
|
||||
}
|
||||
|
||||
return outputPath;
|
||||
}
|
||||
|
||||
async function mockRender(
|
||||
scenes: RenderScene[],
|
||||
onProgress: (percent: number, message: string) => Promise<void>
|
||||
): Promise<string> {
|
||||
const total = scenes.length;
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
const percent = Math.round(((i + 1) / total) * 90);
|
||||
await onProgress(
|
||||
percent,
|
||||
`Rendering scene ${i + 1} of ${total}…`
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
}
|
||||
await onProgress(95, "Encoding MP4…");
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function processRenderJob(jobId: string): Promise<void> {
|
||||
const supabase = getSupabase();
|
||||
const { data: row, error } = await supabase
|
||||
.from("render_jobs")
|
||||
.select("*")
|
||||
.eq("id", jobId)
|
||||
.single();
|
||||
|
||||
if (error || !row) {
|
||||
throw new Error(`Job ${jobId} not found`);
|
||||
}
|
||||
|
||||
const scenes = row.scenes as RenderScene[];
|
||||
const settings = row.settings as RenderSettings;
|
||||
const totalScenes = scenes.length;
|
||||
|
||||
const onProgress = async (percent: number, message: string) => {
|
||||
await updateJob(jobId, {
|
||||
status: "processing",
|
||||
progress: percent,
|
||||
progress_message: message,
|
||||
});
|
||||
};
|
||||
|
||||
await updateJob(jobId, {
|
||||
status: "processing",
|
||||
progress: 2,
|
||||
progress_message: "Preparing render…",
|
||||
});
|
||||
|
||||
const workDir = process.env.NEXRENDER_WORKPATH ?? path.join(process.cwd(), ".nexrender");
|
||||
const outputPath = path.join(workDir, "output", `${jobId}.mp4`);
|
||||
|
||||
try {
|
||||
const nexrenderJob = buildNexrenderJob(
|
||||
scenes,
|
||||
settings,
|
||||
jobId,
|
||||
outputPath
|
||||
);
|
||||
|
||||
let renderedPath = "";
|
||||
|
||||
const useMock =
|
||||
process.env.RENDER_MOCK === "true" ||
|
||||
(!process.env.NEXRENDER_SERVER_URL &&
|
||||
!process.env.NEXRENDER_BINARY &&
|
||||
!process.env.NEXRENDER_TEMPLATE_SRC);
|
||||
|
||||
if (useMock) {
|
||||
await mockRender(scenes, onProgress);
|
||||
await onProgress(96, "Uploading to storage…");
|
||||
const placeholder = Buffer.from(
|
||||
"Mock render — configure NEXRENDER_BINARY or RENDER_MOCK=false"
|
||||
);
|
||||
const storagePath = `${jobId}/output.mp4`;
|
||||
await supabase.storage.from("renders").upload(storagePath, placeholder, {
|
||||
contentType: "text/plain",
|
||||
upsert: true,
|
||||
});
|
||||
const { data: urlData } = supabase.storage
|
||||
.from("renders")
|
||||
.getPublicUrl(storagePath);
|
||||
await updateJob(jobId, {
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
progress_message: "Render complete (mock)",
|
||||
output_url: urlData.publicUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.env.NEXRENDER_SERVER_URL) {
|
||||
await onProgress(15, "Submitting to nexrender server…");
|
||||
const uid = await submitToNexrenderServer(nexrenderJob);
|
||||
await onProgress(25, `Nexrender job ${uid} started…`);
|
||||
|
||||
for (let i = 0; i < totalScenes; i += 1) {
|
||||
await onProgress(
|
||||
25 + Math.round(((i + 1) / totalScenes) * 60),
|
||||
`Rendering scene ${i + 1} of ${totalScenes}…`
|
||||
);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
}
|
||||
|
||||
renderedPath = outputPath;
|
||||
} else {
|
||||
for (let i = 0; i < totalScenes; i += 1) {
|
||||
await onProgress(
|
||||
10 + Math.round((i / totalScenes) * 20),
|
||||
`Rendering scene ${i + 1} of ${totalScenes}…`
|
||||
);
|
||||
}
|
||||
renderedPath = await renderWithCore(nexrenderJob, onProgress);
|
||||
}
|
||||
|
||||
await onProgress(96, "Uploading to storage…");
|
||||
const publicUrl = await uploadToStorage(jobId, renderedPath);
|
||||
|
||||
await updateJob(jobId, {
|
||||
status: "completed",
|
||||
progress: 100,
|
||||
progress_message: "Render complete",
|
||||
output_url: publicUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
const message =
|
||||
err instanceof Error ? err.message : "Unknown render error";
|
||||
await updateJob(jobId, {
|
||||
status: "failed",
|
||||
progress: 0,
|
||||
progress_message: "Render failed",
|
||||
error_message: message,
|
||||
});
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
/**
|
||||
* Standalone render worker — run: npm run render-worker
|
||||
* POST /process { jobId } — requires RENDER_WORKER_SECRET if set
|
||||
*/
|
||||
import http from "node:http";
|
||||
|
||||
import { processRenderJob } from "./render-job-processor";
|
||||
|
||||
const PORT = Number(process.env.RENDER_WORKER_PORT ?? 3355);
|
||||
const SECRET = process.env.RENDER_WORKER_SECRET;
|
||||
|
||||
function isAuthorized(request: http.IncomingMessage): boolean {
|
||||
if (!SECRET) return true;
|
||||
const header = request.headers.authorization;
|
||||
return header === `Bearer ${SECRET}`;
|
||||
}
|
||||
|
||||
function readBody(request: http.IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
request.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
request.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (request, response) => {
|
||||
const url = request.url ?? "/";
|
||||
|
||||
if (request.method === "GET" && url === "/health") {
|
||||
response.writeHead(200, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ ok: true }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.method === "POST" && url === "/process") {
|
||||
if (!isAuthorized(request)) {
|
||||
response.writeHead(401, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "Unauthorized" }));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const body = JSON.parse(await readBody(request)) as { jobId?: string };
|
||||
if (!body.jobId) {
|
||||
response.writeHead(400, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "jobId required" }));
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(202, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ accepted: true, jobId: body.jobId }));
|
||||
|
||||
void processRenderJob(body.jobId).catch((err) => {
|
||||
console.error(`Render job ${body.jobId} failed:`, err);
|
||||
});
|
||||
} catch {
|
||||
response.writeHead(400, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "Invalid JSON" }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
response.writeHead(404, { "Content-Type": "application/json" });
|
||||
response.end(JSON.stringify({ error: "Not found" }));
|
||||
});
|
||||
|
||||
server.listen(PORT, () => {
|
||||
console.log(`Render worker listening on http://localhost:${PORT}`);
|
||||
console.log("Endpoints: GET /health, POST /process");
|
||||
});
|
||||
Reference in New Issue
Block a user