Update server.js
Browse files
server.js
CHANGED
|
@@ -4,12 +4,15 @@ import express from 'express';
|
|
| 4 |
import cors from 'cors';
|
| 5 |
import { bundle } from '@remotion/bundler';
|
| 6 |
import { renderMedia, selectComposition } from '@remotion/renderer';
|
| 7 |
-
import { readFile, mkdir, rm } from 'fs/promises';
|
| 8 |
import { tmpdir } from 'os';
|
| 9 |
import { join, dirname } from 'path';
|
| 10 |
import { randomUUID } from 'crypto';
|
| 11 |
import { fileURLToPath } from 'url';
|
|
|
|
|
|
|
| 12 |
|
|
|
|
| 13 |
const __filename = fileURLToPath(import.meta.url);
|
| 14 |
const __dirname = dirname(__filename);
|
| 15 |
|
|
@@ -58,9 +61,6 @@ app.post('/render', async (req, res) => {
|
|
| 58 |
});
|
| 59 |
|
| 60 |
// --- STABILITY: Pre-download Assets ---
|
| 61 |
-
// To prevent "server sent no data" timeouts and reduce RAM usage,
|
| 62 |
-
// we download all assets to disk BEFORE starting the render.
|
| 63 |
-
|
| 64 |
const fs = await import('fs');
|
| 65 |
const { pipeline } = await import('stream/promises');
|
| 66 |
const { createWriteStream } = await import('fs');
|
|
@@ -83,7 +83,7 @@ app.post('/render', async (req, res) => {
|
|
| 83 |
}
|
| 84 |
};
|
| 85 |
|
| 86 |
-
// process.cwd() is disk-backed (prevents RAM OOM)
|
| 87 |
const tempDir = join(process.cwd(), `temp_assets_${projectId}`);
|
| 88 |
try { await rm(tempDir, { recursive: true, force: true }); } catch { }
|
| 89 |
await mkdir(tempDir, { recursive: true });
|
|
@@ -91,7 +91,7 @@ app.post('/render', async (req, res) => {
|
|
| 91 |
console.log(`[Render] Pre-downloading assets to ${tempDir} (Disk-backed)...`);
|
| 92 |
|
| 93 |
// Helper: Batch Processor for Concurrency Control
|
| 94 |
-
const
|
| 95 |
const results = [];
|
| 96 |
for (let i = 0; i < items.length; i += batchSize) {
|
| 97 |
const batch = items.slice(i, i + batchSize);
|
|
@@ -104,56 +104,55 @@ app.post('/render', async (req, res) => {
|
|
| 104 |
return results;
|
| 105 |
};
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
// Iterate scenes and download assets (Concurrency Limit: 5)
|
| 108 |
-
const localScenes = await
|
| 109 |
const newScene = { ...scene };
|
| 110 |
-
// Calculate global index if needed, but 'scene' object is what matters
|
| 111 |
|
| 112 |
-
// 1. Audio
|
| 113 |
if (scene.audio_url) {
|
| 114 |
const audioExt = scene.audio_url.split('.').pop().split('?')[0] || 'mp3';
|
| 115 |
const localAudioPath = join(tempDir, `audio_${idx}.${audioExt}`);
|
| 116 |
try {
|
| 117 |
await downloadAsset(scene.audio_url, localAudioPath);
|
|
|
|
| 118 |
newScene.audio_url = `file://${localAudioPath}`;
|
| 119 |
} catch (e) {
|
| 120 |
console.error(`[Download] Failed audio for scene ${idx}: ${e.message}`);
|
| 121 |
-
// fallback to original URL if download fails? or fail hard?
|
| 122 |
-
// For now, let's keep original URL so Remotion *might* try,
|
| 123 |
-
// but it will likely fail later. Better to log.
|
| 124 |
}
|
| 125 |
}
|
| 126 |
|
| 127 |
-
// 2. Video / Image
|
| 128 |
if (scene.image_url) {
|
| 129 |
-
// Check if it's a video (likely from Pexels/Supabase)
|
| 130 |
const isVideo = scene.media_type === 'video' || scene.image_url.includes('.mp4');
|
| 131 |
-
|
| 132 |
-
const ext = isVideo ? 'mp4' : 'jpg'; // Default extension if unknown
|
| 133 |
const localMediaPath = join(tempDir, `visual_${idx}.${ext}`);
|
| 134 |
-
|
| 135 |
try {
|
| 136 |
await downloadAsset(scene.image_url, localMediaPath);
|
|
|
|
| 137 |
newScene.image_url = `file://${localMediaPath}`;
|
| 138 |
} catch (e) {
|
| 139 |
console.error(`[Download] Failed visual for scene ${idx}: ${e.message}`);
|
| 140 |
}
|
| 141 |
}
|
| 142 |
-
|
| 143 |
return newScene;
|
| 144 |
});
|
| 145 |
|
| 146 |
-
|
| 147 |
-
// We use a unique route based on the tempDir name to avoid collisions
|
| 148 |
-
const tempDirName = tempDir.split('/').pop(); // e.g., "remotion-1234..."
|
| 149 |
|
| 150 |
-
//
|
|
|
|
| 151 |
app.use(`/tmp/${tempDirName}`, (req, res, next) => {
|
| 152 |
-
// console.log(`[Asset Server] Serving: ${req.path}`); // Uncomment for verbose debug
|
| 153 |
next();
|
| 154 |
}, express.static(tempDir));
|
| 155 |
|
| 156 |
-
console.log(`[Render] Assets
|
| 157 |
|
| 158 |
// Rewrite scenes to use LOCALHOST HTTP URLs
|
| 159 |
const httpScenes = localScenes.map(s => {
|
|
@@ -170,62 +169,93 @@ app.post('/render', async (req, res) => {
|
|
| 170 |
});
|
| 171 |
|
| 172 |
// --------------------------------------
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
chromiumOptions: { executablePath: process.env.CHROME_BIN },
|
| 179 |
-
});
|
| 180 |
-
|
| 181 |
-
const outputPath = join(tempDir, 'output.mp4');
|
| 182 |
|
| 183 |
const os = await import('os');
|
| 184 |
const cpuCount = os.cpus().length;
|
| 185 |
-
console.log(`[Render]
|
| 186 |
-
|
| 187 |
-
let lastLoggedPercent = -1;
|
| 188 |
-
|
| 189 |
-
await renderMedia({
|
| 190 |
-
composition,
|
| 191 |
-
serveUrl: bundleLocation,
|
| 192 |
-
codec: 'h264',
|
| 193 |
-
pixelFormat: 'yuv420p',
|
| 194 |
-
outputLocation: outputPath,
|
| 195 |
-
imageFormat: 'jpeg',
|
| 196 |
-
jpegQuality: 80,
|
| 197 |
-
inputProps: { scenes: httpScenes, settings },
|
| 198 |
-
chromiumOptions: {
|
| 199 |
-
executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
|
| 200 |
-
enableMultiProcessRendering: true, // Re-enable for speed (we have 30GB RAM now)
|
| 201 |
-
// GPU Optimizations for NVIDIA L4
|
| 202 |
-
args: [
|
| 203 |
-
'--no-sandbox',
|
| 204 |
-
'--disable-dev-shm-usage',
|
| 205 |
-
'--enable-gpu', // Force GPU
|
| 206 |
-
'--use-gl=egl', // Use EGL for hardware accel
|
| 207 |
-
'--enable-zero-copy',
|
| 208 |
-
'--ignore-gpu-blocklist',
|
| 209 |
-
// '--mute-audio', // Optional: keep muted if audio is handled by FFmpeg
|
| 210 |
-
]
|
| 211 |
-
},
|
| 212 |
-
concurrency: 4, // 8 vCPUs -> Start with 4 workers
|
| 213 |
-
disallowParallelEncoding: false, // Allow parallel encoding for speed
|
| 214 |
-
onProgress: ({ progress }) => {
|
| 215 |
-
const percent = Math.round(progress * 100);
|
| 216 |
-
if (percent !== lastLoggedPercent) {
|
| 217 |
-
console.log(`[Render] Progress: ${percent}%`);
|
| 218 |
-
lastLoggedPercent = percent;
|
| 219 |
-
}
|
| 220 |
-
},
|
| 221 |
-
});
|
| 222 |
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
-
|
| 226 |
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
const fileName = `${projectId}/video-${Date.now()}.mp4`;
|
|
|
|
| 229 |
const { data: uploadData, error: uploadError } = await supabase
|
| 230 |
.storage
|
| 231 |
.from('projects')
|
|
@@ -236,7 +266,6 @@ app.post('/render', async (req, res) => {
|
|
| 236 |
|
| 237 |
if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`);
|
| 238 |
|
| 239 |
-
// Get Public URL
|
| 240 |
const { data: { publicUrl } } = supabase
|
| 241 |
.storage
|
| 242 |
.from('projects')
|
|
@@ -247,27 +276,21 @@ app.post('/render', async (req, res) => {
|
|
| 247 |
// Update Project in DB
|
| 248 |
const { error: dbError } = await supabase
|
| 249 |
.from('projects')
|
| 250 |
-
.update({
|
| 251 |
-
status: 'done',
|
| 252 |
-
video_url: publicUrl
|
| 253 |
-
})
|
| 254 |
.eq('id', projectId);
|
| 255 |
|
| 256 |
if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
|
| 257 |
|
| 258 |
-
console.log(`[Render] Project ${projectId} updated
|
| 259 |
|
| 260 |
// Cleanup
|
| 261 |
await rm(tempDir, { recursive: true, force: true });
|
| 262 |
|
| 263 |
} catch (error) {
|
| 264 |
console.error(`[Render] Background Error for ${projectId}:`, error);
|
| 265 |
-
// Update project status to 'error'
|
| 266 |
await supabase
|
| 267 |
.from('projects')
|
| 268 |
-
.update({
|
| 269 |
-
status: 'error',
|
| 270 |
-
})
|
| 271 |
.eq('id', projectId);
|
| 272 |
}
|
| 273 |
})();
|
|
@@ -275,11 +298,8 @@ app.post('/render', async (req, res) => {
|
|
| 275 |
|
| 276 |
app.listen(PORT, () => {
|
| 277 |
console.log(`🎬 FacelessFlowAI Renderer running on port ${PORT}`);
|
| 278 |
-
|
| 279 |
// Debug: Check for Secrets on Startup
|
| 280 |
console.log('--- Environment Check ---');
|
| 281 |
console.log('SUPABASE_URL exists:', !!process.env.SUPABASE_URL);
|
| 282 |
console.log('SUPABASE_SERVICE_ROLE_KEY exists:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
|
| 283 |
-
console.log('Available Keys (starts with SUPA):', Object.keys(process.env).filter(k => k.startsWith('SUPA')));
|
| 284 |
-
console.log('-------------------------');
|
| 285 |
});
|
|
|
|
| 4 |
import cors from 'cors';
|
| 5 |
import { bundle } from '@remotion/bundler';
|
| 6 |
import { renderMedia, selectComposition } from '@remotion/renderer';
|
| 7 |
+
import { readFile, mkdir, rm, writeFile } from 'fs/promises';
|
| 8 |
import { tmpdir } from 'os';
|
| 9 |
import { join, dirname } from 'path';
|
| 10 |
import { randomUUID } from 'crypto';
|
| 11 |
import { fileURLToPath } from 'url';
|
| 12 |
+
import { exec as execCallback } from 'child_process';
|
| 13 |
+
import { promisify } from 'util';
|
| 14 |
|
| 15 |
+
const exec = promisify(execCallback);
|
| 16 |
const __filename = fileURLToPath(import.meta.url);
|
| 17 |
const __dirname = dirname(__filename);
|
| 18 |
|
|
|
|
| 61 |
});
|
| 62 |
|
| 63 |
// --- STABILITY: Pre-download Assets ---
|
|
|
|
|
|
|
|
|
|
| 64 |
const fs = await import('fs');
|
| 65 |
const { pipeline } = await import('stream/promises');
|
| 66 |
const { createWriteStream } = await import('fs');
|
|
|
|
| 83 |
}
|
| 84 |
};
|
| 85 |
|
| 86 |
+
// process.cwd() is disk-backed (prevents RAM OOM on HF Spaces)
|
| 87 |
const tempDir = join(process.cwd(), `temp_assets_${projectId}`);
|
| 88 |
try { await rm(tempDir, { recursive: true, force: true }); } catch { }
|
| 89 |
await mkdir(tempDir, { recursive: true });
|
|
|
|
| 91 |
console.log(`[Render] Pre-downloading assets to ${tempDir} (Disk-backed)...`);
|
| 92 |
|
| 93 |
// Helper: Batch Processor for Concurrency Control
|
| 94 |
+
const batchProcessArgs = async (items, batchSize, fn) => {
|
| 95 |
const results = [];
|
| 96 |
for (let i = 0; i < items.length; i += batchSize) {
|
| 97 |
const batch = items.slice(i, i + batchSize);
|
|
|
|
| 104 |
return results;
|
| 105 |
};
|
| 106 |
|
| 107 |
+
// Calculate total downloaded size using fs.stat
|
| 108 |
+
let totalBytes = 0;
|
| 109 |
+
const trackSize = async (file) => {
|
| 110 |
+
try {
|
| 111 |
+
const stats = await fs.promises.stat(file);
|
| 112 |
+
totalBytes += stats.size;
|
| 113 |
+
} catch { }
|
| 114 |
+
};
|
| 115 |
+
|
| 116 |
// Iterate scenes and download assets (Concurrency Limit: 5)
|
| 117 |
+
const localScenes = await batchProcessArgs(scenes, 5, async (scene, idx) => {
|
| 118 |
const newScene = { ...scene };
|
|
|
|
| 119 |
|
|
|
|
| 120 |
if (scene.audio_url) {
|
| 121 |
const audioExt = scene.audio_url.split('.').pop().split('?')[0] || 'mp3';
|
| 122 |
const localAudioPath = join(tempDir, `audio_${idx}.${audioExt}`);
|
| 123 |
try {
|
| 124 |
await downloadAsset(scene.audio_url, localAudioPath);
|
| 125 |
+
await trackSize(localAudioPath);
|
| 126 |
newScene.audio_url = `file://${localAudioPath}`;
|
| 127 |
} catch (e) {
|
| 128 |
console.error(`[Download] Failed audio for scene ${idx}: ${e.message}`);
|
|
|
|
|
|
|
|
|
|
| 129 |
}
|
| 130 |
}
|
| 131 |
|
|
|
|
| 132 |
if (scene.image_url) {
|
|
|
|
| 133 |
const isVideo = scene.media_type === 'video' || scene.image_url.includes('.mp4');
|
| 134 |
+
const ext = isVideo ? 'mp4' : 'jpg';
|
|
|
|
| 135 |
const localMediaPath = join(tempDir, `visual_${idx}.${ext}`);
|
|
|
|
| 136 |
try {
|
| 137 |
await downloadAsset(scene.image_url, localMediaPath);
|
| 138 |
+
await trackSize(localMediaPath);
|
| 139 |
newScene.image_url = `file://${localMediaPath}`;
|
| 140 |
} catch (e) {
|
| 141 |
console.error(`[Download] Failed visual for scene ${idx}: ${e.message}`);
|
| 142 |
}
|
| 143 |
}
|
|
|
|
| 144 |
return newScene;
|
| 145 |
});
|
| 146 |
|
| 147 |
+
console.log(`[Render] Download complete. Total asset size: ${(totalBytes / 1024 / 1024).toFixed(2)} MB`);
|
|
|
|
|
|
|
| 148 |
|
| 149 |
+
// 3. Mount temp dir statically so Remotion can fetch via HTTP (bypass file:// restriction)
|
| 150 |
+
const tempDirName = tempDir.split('/').pop();
|
| 151 |
app.use(`/tmp/${tempDirName}`, (req, res, next) => {
|
|
|
|
| 152 |
next();
|
| 153 |
}, express.static(tempDir));
|
| 154 |
|
| 155 |
+
console.log(`[Render] Assets mounted at /tmp/${tempDirName}. Starting Segmented Rendering...`);
|
| 156 |
|
| 157 |
// Rewrite scenes to use LOCALHOST HTTP URLs
|
| 158 |
const httpScenes = localScenes.map(s => {
|
|
|
|
| 169 |
});
|
| 170 |
|
| 171 |
// --------------------------------------
|
| 172 |
+
// SEGMENTED BATCH RENDERING
|
| 173 |
+
// --------------------------------------
|
| 174 |
+
const BATCH_SIZE = 40; // Render 40 scenes at a time
|
| 175 |
+
const totalBatches = Math.ceil(httpScenes.length / BATCH_SIZE);
|
| 176 |
+
const partFiles = [];
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
const os = await import('os');
|
| 179 |
const cpuCount = os.cpus().length;
|
| 180 |
+
console.log(`[Render] Splitting ${httpScenes.length} scenes into ${totalBatches} parts. CPU: ${cpuCount}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
|
| 182 |
+
for (let i = 0; i < totalBatches; i++) {
|
| 183 |
+
const startIdx = i * BATCH_SIZE;
|
| 184 |
+
const endIdx = Math.min((i + 1) * BATCH_SIZE, httpScenes.length);
|
| 185 |
+
const batchScenes = httpScenes.slice(startIdx, endIdx);
|
| 186 |
|
| 187 |
+
console.log(`[Render] Processing Part ${i + 1}/${totalBatches} (Scenes ${startIdx}-${endIdx - 1})...`);
|
| 188 |
|
| 189 |
+
const composition = await selectComposition({
|
| 190 |
+
serveUrl: bundleLocation,
|
| 191 |
+
id: 'Main',
|
| 192 |
+
inputProps: { scenes: batchScenes, settings },
|
| 193 |
+
chromiumOptions: { executablePath: process.env.CHROME_BIN },
|
| 194 |
+
});
|
| 195 |
+
|
| 196 |
+
const partPath = join(tempDir, `part_${i}.mp4`);
|
| 197 |
+
partFiles.push(partPath);
|
| 198 |
+
|
| 199 |
+
let lastLoggedPercent = -1;
|
| 200 |
+
|
| 201 |
+
await renderMedia({
|
| 202 |
+
composition,
|
| 203 |
+
serveUrl: bundleLocation,
|
| 204 |
+
codec: 'h264',
|
| 205 |
+
pixelFormat: 'yuv420p',
|
| 206 |
+
outputLocation: partPath,
|
| 207 |
+
imageFormat: 'jpeg',
|
| 208 |
+
jpegQuality: 80,
|
| 209 |
+
inputProps: { scenes: batchScenes, settings },
|
| 210 |
+
chromiumOptions: {
|
| 211 |
+
executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
|
| 212 |
+
enableMultiProcessRendering: true,
|
| 213 |
+
args: [
|
| 214 |
+
'--no-sandbox',
|
| 215 |
+
'--disable-dev-shm-usage',
|
| 216 |
+
'--enable-gpu',
|
| 217 |
+
'--use-gl=egl',
|
| 218 |
+
'--enable-zero-copy',
|
| 219 |
+
'--ignore-gpu-blocklist',
|
| 220 |
+
]
|
| 221 |
+
},
|
| 222 |
+
concurrency: 4,
|
| 223 |
+
disallowParallelEncoding: false,
|
| 224 |
+
onProgress: ({ progress }) => {
|
| 225 |
+
const percent = Math.round(progress * 100);
|
| 226 |
+
if (percent !== lastLoggedPercent && percent % 10 === 0) {
|
| 227 |
+
console.log(`[Render] Part ${i + 1} Progress: ${percent}%`);
|
| 228 |
+
lastLoggedPercent = percent;
|
| 229 |
+
}
|
| 230 |
+
},
|
| 231 |
+
});
|
| 232 |
+
|
| 233 |
+
console.log(`[Render] Part ${i + 1} Done.`);
|
| 234 |
+
// Clean composition object to help GC?
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
// --------------------------------------
|
| 238 |
+
// STITCHING
|
| 239 |
+
// --------------------------------------
|
| 240 |
+
console.log(`[Render] All parts rendered. Stitching ${partFiles.length} files...`);
|
| 241 |
+
|
| 242 |
+
const concatListPath = join(tempDir, 'concat_list.txt');
|
| 243 |
+
const concatContent = partFiles.map(p => `file '${p}'`).join('\n');
|
| 244 |
+
await writeFile(concatListPath, concatContent);
|
| 245 |
+
|
| 246 |
+
const finalOutputPath = join(tempDir, 'output_final.mp4');
|
| 247 |
+
const ffmpegCmd = `ffmpeg -f concat -safe 0 -i "${concatListPath}" -c copy "${finalOutputPath}"`;
|
| 248 |
+
|
| 249 |
+
await exec(ffmpegCmd);
|
| 250 |
+
console.log(`[Render] Stitching complete.`);
|
| 251 |
+
|
| 252 |
+
// --------------------------------------
|
| 253 |
+
// UPLOAD
|
| 254 |
+
// --------------------------------------
|
| 255 |
+
console.log(`[Render] Uploading to Supabase...`);
|
| 256 |
+
const videoBuffer = await readFile(finalOutputPath);
|
| 257 |
const fileName = `${projectId}/video-${Date.now()}.mp4`;
|
| 258 |
+
|
| 259 |
const { data: uploadData, error: uploadError } = await supabase
|
| 260 |
.storage
|
| 261 |
.from('projects')
|
|
|
|
| 266 |
|
| 267 |
if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`);
|
| 268 |
|
|
|
|
| 269 |
const { data: { publicUrl } } = supabase
|
| 270 |
.storage
|
| 271 |
.from('projects')
|
|
|
|
| 276 |
// Update Project in DB
|
| 277 |
const { error: dbError } = await supabase
|
| 278 |
.from('projects')
|
| 279 |
+
.update({ status: 'done', video_url: publicUrl })
|
|
|
|
|
|
|
|
|
|
| 280 |
.eq('id', projectId);
|
| 281 |
|
| 282 |
if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
|
| 283 |
|
| 284 |
+
console.log(`[Render] Project ${projectId} completed and updated.`);
|
| 285 |
|
| 286 |
// Cleanup
|
| 287 |
await rm(tempDir, { recursive: true, force: true });
|
| 288 |
|
| 289 |
} catch (error) {
|
| 290 |
console.error(`[Render] Background Error for ${projectId}:`, error);
|
|
|
|
| 291 |
await supabase
|
| 292 |
.from('projects')
|
| 293 |
+
.update({ status: 'error' })
|
|
|
|
|
|
|
| 294 |
.eq('id', projectId);
|
| 295 |
}
|
| 296 |
})();
|
|
|
|
| 298 |
|
| 299 |
app.listen(PORT, () => {
|
| 300 |
console.log(`🎬 FacelessFlowAI Renderer running on port ${PORT}`);
|
|
|
|
| 301 |
// Debug: Check for Secrets on Startup
|
| 302 |
console.log('--- Environment Check ---');
|
| 303 |
console.log('SUPABASE_URL exists:', !!process.env.SUPABASE_URL);
|
| 304 |
console.log('SUPABASE_SERVICE_ROLE_KEY exists:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
|
|
|
|
|
|
|
| 305 |
});
|