Stylique commited on
Commit
368b96d
·
verified ·
1 Parent(s): 0165696

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +124 -492
server.js CHANGED
@@ -17,530 +17,162 @@ const PORT = process.env.PORT || 7860;
17
  app.use(cors());
18
  app.use(express.json({ limit: '50mb' }));
19
 
20
- // Professional bundle caching with versioning
21
- let cachedBundle = {
22
- location: null,
23
- version: 0,
24
- timestamp: 0
25
- };
26
- const BUNDLE_CACHE_TTL = 10 * 60 * 1000; // 10 minutes
27
-
28
  // Health check
29
  app.get('/', (req, res) => {
30
- res.json({
31
- status: 'ok',
32
- service: 'FacelessFlowAI Video Renderer',
33
- version: '2.0.0',
34
- uptime: Math.round(process.uptime())
35
- });
36
  });
37
 
38
- // Professional bundle management
39
- async function getBundle() {
40
- const now = Date.now();
41
-
42
- if (cachedBundle.location && (now - cachedBundle.timestamp) < BUNDLE_CACHE_TTL) {
43
- console.log('[Bundle] Using cached bundle');
44
- return cachedBundle.location;
45
- }
46
-
47
- console.log('[Bundle] Creating production bundle');
48
-
49
- try {
50
- cachedBundle.location = await bundle({
51
- entryPoint: join(__dirname, 'remotion', 'index.tsx'),
52
- // Professional webpack configuration
53
- webpackOverride: (config) => {
54
- // Production optimizations
55
- config.mode = 'production';
56
- config.devtool = false;
57
-
58
- // Bundle optimizations
59
- config.optimization = {
60
- minimize: true,
61
- moduleIds: 'deterministic',
62
- splitChunks: {
63
- chunks: 'async',
64
- minSize: 20000,
65
- maxSize: 70000,
66
- },
67
- };
68
-
69
- // Performance hints
70
- config.performance = {
71
- hints: false,
72
- maxEntrypointSize: 512000,
73
- maxAssetSize: 512000,
74
- };
75
-
76
- return config;
77
- },
78
- });
79
-
80
- cachedBundle.timestamp = now;
81
- cachedBundle.version++;
82
-
83
- console.log(`[Bundle] Bundle v${cachedBundle.version} ready at ${cachedBundle.location}`);
84
- return cachedBundle.location;
85
- } catch (error) {
86
- console.error('[Bundle] Failed to create bundle:', error);
87
- throw error;
88
- }
89
- }
90
-
91
- // Professional rendering configuration
92
- function getRenderConfig() {
93
- return {
94
- // Video settings
95
- codec: 'h264',
96
- pixelFormat: 'yuv420p',
97
- imageFormat: 'jpeg',
98
- jpegQuality: 80,
99
-
100
- // Performance settings (optimized for L4: 8 vCPU, 30GB RAM)
101
- concurrency: 6, // Optimal for 8-core system
102
- disallowParallelEncoding: false,
103
- // REMOVED: numberOfGifLoops: 0, // Only for GIF codec
104
-
105
- // Audio settings
106
- audioBitrate: '192k',
107
- videoBitrate: '8M',
108
- muted: false,
109
-
110
- // Chromium settings (professional defaults)
111
- chromiumOptions: {
112
- executablePath: '/usr/bin/google-chrome-stable',
113
- enableMultiProcessRendering: true,
114
- ignoreDefaultArgs: ['--disable-gpu'],
115
- args: [
116
- '--no-sandbox',
117
- '--disable-dev-shm-usage',
118
- '--disable-setuid-sandbox',
119
- '--disable-accelerated-2d-canvas',
120
- '--disable-gpu',
121
- '--hide-scrollbars',
122
- '--disable-notifications',
123
- '--disable-background-timer-throttling',
124
- '--disable-backgrounding-occluded-windows',
125
- '--disable-renderer-backgrounding',
126
- '--disable-features=ScriptStreaming',
127
- '--disable-features=VizDisplayCompositor',
128
- ],
129
- },
130
-
131
- // FFmpeg settings (professional defaults)
132
- ffmpegOverride: undefined, // Let Remotion handle FFmpeg
133
- };
134
- }
135
-
136
- // Professional error handling
137
- class RenderError extends Error {
138
- constructor(message, code, details) {
139
- super(message);
140
- this.name = 'RenderError';
141
- this.code = code;
142
- this.details = details;
143
- }
144
- }
145
-
146
- // Main render function (professional pattern)
147
- async function renderVideo(params) {
148
- const { compositionId, serveUrl, inputProps, onProgress } = params;
149
-
150
- console.log('[Render] Selecting composition...');
151
- const composition = await selectComposition({
152
- serveUrl,
153
- id: compositionId,
154
- inputProps,
155
- chromiumOptions: getRenderConfig().chromiumOptions,
156
- });
157
-
158
- console.log(`[Render] Composition: ${composition.durationInFrames} frames @ ${composition.fps}fps`);
159
-
160
- const tempDir = join(tmpdir(), `render-${randomUUID()}`);
161
- await mkdir(tempDir, { recursive: true });
162
- const outputPath = join(tempDir, 'output.mp4');
163
-
164
- const renderConfig = getRenderConfig();
165
-
166
- console.log('[Render] Starting render...');
167
-
168
- const startTime = Date.now();
169
- let lastProgress = 0;
170
-
171
- try {
172
- await renderMedia({
173
- composition,
174
- serveUrl,
175
- outputLocation: outputPath,
176
- inputProps,
177
- onProgress: ({ progress }) => {
178
- const percent = Math.round(progress * 100);
179
- if (percent !== lastProgress && percent % 5 === 0) {
180
- console.log(`[Progress] ${percent}%`);
181
- lastProgress = percent;
182
- if (onProgress) onProgress(progress);
183
- }
184
- },
185
- ...renderConfig,
186
- });
187
-
188
- const renderTime = Date.now() - startTime;
189
- const fps = Math.round(composition.durationInFrames / (renderTime / 1000));
190
-
191
- console.log(`[Render] Completed in ${Math.round(renderTime / 1000)}s (${fps} FPS)`);
192
-
193
- const videoBuffer = await readFile(outputPath);
194
- const fileSizeMB = Math.round(videoBuffer.length / (1024 * 1024));
195
-
196
- console.log(`[Render] Output: ${fileSizeMB}MB`);
197
-
198
- // Cleanup temp directory
199
- await rm(tempDir, { recursive: true, force: true });
200
-
201
- return {
202
- buffer: videoBuffer,
203
- duration: renderTime,
204
- fps: fps,
205
- fileSizeMB: fileSizeMB,
206
- composition: composition,
207
- };
208
-
209
- } catch (error) {
210
- // Cleanup on error
211
- try {
212
- await rm(tempDir, { recursive: true, force: true });
213
- } catch (cleanupError) {
214
- console.warn('[Cleanup] Failed to cleanup temp dir:', cleanupError);
215
- }
216
-
217
- throw new RenderError(
218
- `Render failed: ${error.message}`,
219
- 'RENDER_FAILED',
220
- { originalError: error.message }
221
- );
222
- }
223
- }
224
-
225
- // Render endpoint (professional API design)
226
  app.post('/render', async (req, res) => {
227
  const { projectId, scenes, settings } = req.body;
228
-
229
- // Input validation
230
  if (!projectId || !scenes || !settings) {
231
- return res.status(400).json({
232
- error: 'VALIDATION_ERROR',
233
- message: 'Missing required fields: projectId, scenes, or settings',
234
- required: ['projectId', 'scenes', 'settings']
235
- });
236
  }
237
-
238
- // Initialize Supabase
239
  const SUPABASE_URL = process.env.SUPABASE_URL;
240
  const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
241
-
242
  if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
243
- return res.status(500).json({
244
- error: 'CONFIGURATION_ERROR',
245
- message: 'Server configuration error'
246
- });
247
  }
248
-
249
  const { createClient } = await import('@supabase/supabase-js');
250
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
251
-
252
- // Accept request immediately (async processing)
253
- res.json({
254
- success: true,
255
- message: 'Render job accepted',
256
- jobId: projectId,
257
- estimatedTime: 'Processing...'
258
- });
259
-
260
- // Start async processing
261
  (async () => {
262
  try {
263
- console.log(`[Job ${projectId}] Starting render job`);
264
-
265
- // Update status to processing
266
- await supabase
267
- .from('projects')
268
- .update({
269
- status: 'processing',
270
- started_at: new Date().toISOString()
271
- })
272
- .eq('id', projectId);
273
-
274
- // Get bundle
275
- const bundleLocation = await getBundle();
276
-
277
- // Render video
278
- const result = await renderVideo({
279
- compositionId: 'Main',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
280
  serveUrl: bundleLocation,
 
 
 
 
 
281
  inputProps: { scenes, settings },
282
- onProgress: (progress) => {
283
- // Optional: Send progress updates via WebSocket or store in DB
284
- console.log(`[Job ${projectId}] Progress: ${Math.round(progress * 100)}%`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  }
286
  });
287
-
 
 
 
 
288
  // Upload to Supabase Storage
289
- const fileName = `${projectId}/${Date.now()}-${randomUUID().slice(0, 8)}.mp4`;
290
- const contentType = 'video/mp4';
291
-
292
- console.log(`[Job ${projectId}] Uploading to storage...`);
293
-
294
- const { error: uploadError } = await supabase.storage
295
- .from('projects')
296
- .upload(fileName, result.buffer, {
297
- contentType,
298
- cacheControl: '3600',
299
- upsert: false
300
  });
301
-
302
- if (uploadError) throw uploadError;
303
-
304
- // Get public URL
305
- const { data: { publicUrl } } = supabase.storage
 
306
  .from('projects')
307
  .getPublicUrl(fileName);
308
-
309
- // Update project with success
310
- await supabase
 
 
311
  .from('projects')
312
  .update({
313
- status: 'completed',
314
- video_url: publicUrl,
315
- rendered_at: new Date().toISOString(),
316
- render_metadata: {
317
- duration_ms: result.duration,
318
- file_size_mb: result.fileSizeMB,
319
- fps: result.fps,
320
- total_frames: result.composition.durationInFrames,
321
- resolution: `${result.composition.width}x${result.composition.height}`,
322
- timestamp: new Date().toISOString()
323
- }
324
  })
325
  .eq('id', projectId);
326
-
327
- console.log(`[Job ${projectId}] Completed successfully`);
328
-
 
 
 
 
 
329
  } catch (error) {
330
- console.error(`[Job ${projectId}] Failed:`, error.message);
331
-
332
- // Update with error
333
- try {
334
- await supabase
335
- .from('projects')
336
- .update({
337
- status: 'failed',
338
- error_message: error.message,
339
- error_details: error.details || {},
340
- failed_at: new Date().toISOString()
341
- })
342
- .eq('id', projectId);
343
- } catch (dbError) {
344
- console.error(`[Job ${projectId}] Failed to update error status:`, dbError.message);
345
- }
346
  }
347
  })();
348
  });
349
 
350
- // Status endpoint with detailed info
351
- app.get('/status', async (req, res) => {
352
- const os = await import('os');
353
-
354
- const status = {
355
- service: 'FacelessFlowAI Renderer',
356
- version: '2.0.0',
357
- status: 'operational',
358
- uptime: Math.round(process.uptime()),
359
-
360
- // System info
361
- system: {
362
- cpu_cores: os.cpus().length,
363
- total_memory_gb: Math.round(os.totalmem() / (1024 ** 3)),
364
- free_memory_gb: Math.round(os.freemem() / (1024 ** 3)),
365
- platform: os.platform(),
366
- arch: os.arch()
367
- },
368
-
369
- // Service info
370
- service_info: {
371
- bundle_cached: cachedBundle.location !== null,
372
- bundle_version: cachedBundle.version,
373
- cache_age_minutes: cachedBundle.timestamp ?
374
- Math.round((Date.now() - cachedBundle.timestamp) / 60000) : null
375
- },
376
-
377
- // Configuration
378
- config: {
379
- max_concurrency: 6,
380
- codec: 'h264',
381
- default_resolution: '1920x1080',
382
- default_fps: 30
383
- },
384
-
385
- timestamp: new Date().toISOString()
386
- };
387
-
388
- res.json(status);
389
- });
390
-
391
- // Metrics endpoint (for monitoring)
392
- app.get('/metrics', async (req, res) => {
393
- const os = await import('os');
394
- const { execSync } = await import('child_process');
395
-
396
- try {
397
- // System metrics
398
- const loadAvg = os.loadavg();
399
- const memoryUsage = process.memoryUsage();
400
-
401
- // GPU metrics if available
402
- let gpuInfo = null;
403
- try {
404
- const gpuOutput = execSync('nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits 2>/dev/null || echo ""', { encoding: 'utf8' });
405
- if (gpuOutput.trim()) {
406
- const [gpuUtil, memUsed, memTotal] = gpuOutput.trim().split(', ').map(Number);
407
- gpuInfo = {
408
- utilization_percent: gpuUtil,
409
- memory_used_mb: memUsed,
410
- memory_total_mb: memTotal,
411
- memory_usage_percent: Math.round((memUsed / memTotal) * 100)
412
- };
413
- }
414
- } catch {
415
- // GPU not available or nvidia-smi not installed
416
- }
417
-
418
- const metrics = {
419
- timestamp: new Date().toISOString(),
420
- system: {
421
- load_avg: loadAvg,
422
- memory: {
423
- rss_mb: Math.round(memoryUsage.rss / 1024 / 1024),
424
- heap_total_mb: Math.round(memoryUsage.heapTotal / 1024 / 1024),
425
- heap_used_mb: Math.round(memoryUsage.heapUsed / 1024 / 1024),
426
- external_mb: Math.round(memoryUsage.external / 1024 / 1024)
427
- },
428
- uptime: process.uptime()
429
- },
430
- gpu: gpuInfo,
431
- service: {
432
- bundle_cache_hit: cachedBundle.location !== null,
433
- active_requests: 0 // You could track this
434
- }
435
- };
436
-
437
- res.json(metrics);
438
- } catch (error) {
439
- res.status(500).json({ error: error.message });
440
- }
441
- });
442
 
443
- // System check endpoint
444
- app.get('/health', async (req, res) => {
445
- const checks = [];
446
-
447
- // Check 1: System memory
448
- try {
449
- const os = await import('os');
450
- const freeMemoryGB = Math.round(os.freemem() / (1024 ** 3));
451
- checks.push({
452
- name: 'system_memory',
453
- status: freeMemoryGB > 2 ? 'healthy' : 'warning',
454
- message: `${freeMemoryGB}GB free memory`,
455
- value: freeMemoryGB
456
- });
457
- } catch (error) {
458
- checks.push({
459
- name: 'system_memory',
460
- status: 'error',
461
- message: error.message
462
- });
463
- }
464
-
465
- // Check 2: Chrome availability
466
- try {
467
- const { execSync } = await import('child_process');
468
- execSync('/usr/bin/google-chrome-stable --version', { stdio: 'pipe' });
469
- checks.push({
470
- name: 'chrome',
471
- status: 'healthy',
472
- message: 'Chrome is available'
473
- });
474
- } catch (error) {
475
- checks.push({
476
- name: 'chrome',
477
- status: 'warning',
478
- message: 'Chrome not found, will use headless shell'
479
- });
480
- }
481
-
482
- // Check 3: FFmpeg
483
- try {
484
- const { execSync } = await import('child_process');
485
- execSync('ffmpeg -version', { stdio: 'pipe' });
486
- checks.push({
487
- name: 'ffmpeg',
488
- status: 'healthy',
489
- message: 'FFmpeg is available'
490
- });
491
- } catch (error) {
492
- checks.push({
493
- name: 'ffmpeg',
494
- status: 'error',
495
- message: 'FFmpeg not found'
496
- });
497
- }
498
-
499
- // Check 4: Bundle status
500
- checks.push({
501
- name: 'bundle',
502
- status: cachedBundle.location ? 'healthy' : 'warming',
503
- message: cachedBundle.location ? 'Bundle cached' : 'Bundle warming up',
504
- version: cachedBundle.version
505
- });
506
-
507
- const allHealthy = checks.every(c => c.status === 'healthy' || c.status === 'warning');
508
- const anyError = checks.some(c => c.status === 'error');
509
-
510
- res.json({
511
- status: anyError ? 'degraded' : (allHealthy ? 'healthy' : 'warning'),
512
- checks: checks,
513
- timestamp: new Date().toISOString()
514
- });
515
  });
516
-
517
- // Start server
518
- app.listen(PORT, () => {
519
- console.log(`
520
- 🎬 FacelessFlowAI Professional Renderer
521
- ======================================
522
- Port: ${PORT}
523
- Mode: Production
524
- Hardware: L4 GPU (8 vCPU, 30GB RAM)
525
-
526
- Endpoints:
527
- - GET / Service status
528
- - GET /health System health check
529
- - GET /status Detailed service info
530
- - GET /metrics System metrics
531
- - POST /render Render video
532
-
533
- Initializing...
534
- `);
535
-
536
- // Warm up the system
537
- setTimeout(async () => {
538
- try {
539
- console.log('[Init] Warming up renderer...');
540
- await getBundle();
541
- console.log('[Init] Renderer ready');
542
- } catch (error) {
543
- console.error('[Init] Warmup failed:', error.message);
544
- }
545
- }, 2000);
546
- });
 
17
  app.use(cors());
18
  app.use(express.json({ limit: '50mb' }));
19
 
 
 
 
 
 
 
 
 
20
  // Health check
21
  app.get('/', (req, res) => {
22
+ res.json({ status: 'ok', service: 'FacelessFlowAI Video Renderer' });
 
 
 
 
 
23
  });
24
 
25
+ // Render endpoint
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  app.post('/render', async (req, res) => {
27
  const { projectId, scenes, settings } = req.body;
28
+
 
29
  if (!projectId || !scenes || !settings) {
30
+ return res.status(400).json({ error: 'Missing projectId, scenes, or settings' });
 
 
 
 
31
  }
32
+
33
+ // Initialize Supabase Admin Client
34
  const SUPABASE_URL = process.env.SUPABASE_URL;
35
  const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
36
+
37
  if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
38
+ console.error('[Config] Missing Supabase credentials');
39
+ return res.status(500).json({ error: 'Renderer configuration error' });
 
 
40
  }
41
+
42
  const { createClient } = await import('@supabase/supabase-js');
43
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
44
+
45
+ // 1. Respond immediately
46
+ res.json({ success: true, message: 'Rendering started in background' });
47
+
48
+ // 2. Start Background Process
 
 
 
 
 
49
  (async () => {
50
  try {
51
+ console.log(`[Render] Starting background render for project ${projectId}`);
52
+
53
+ const bundleLocation = await bundle({
54
+ entryPoint: join(__dirname, 'remotion', 'index.tsx'),
55
+ webpackOverride: (config) => config,
56
+ });
57
+
58
+ const composition = await selectComposition({
59
+ serveUrl: bundleLocation,
60
+ id: 'Main',
61
+ inputProps: { scenes, settings },
62
+ chromiumOptions: { executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable' },
63
+ });
64
+
65
+ const tempDir = join(tmpdir(), `remotion-${randomUUID()}`);
66
+ await mkdir(tempDir, { recursive: true });
67
+ const outputPath = join(tempDir, 'output.mp4');
68
+
69
+ const os = await import('os');
70
+ const cpuCount = os.cpus().length;
71
+ console.log(`[Render] Detected ${cpuCount} CPUs. Using all cores for rendering.`);
72
+
73
+ // Tuning for L4 (8 vCPU / 30GB RAM / 24GB VRAM)
74
+ // 8 vCPUs means going above 8 concurrency won't help (context switching).
75
+ // 30GB RAM / 8 = ~3.7GB per instance. Chrome can exceed this.
76
+ // Safe sweet spot is likely 6.
77
+ // Enabling Chrome GPU (gl: 'angle') moves load to VRAM, helping stability.
78
+ const safeConcurrency = Math.min(cpuCount, 6);
79
+ console.log(`[Render] Requesting concurrency: ${safeConcurrency} (Host has ${cpuCount})`);
80
+
81
+ let lastLog = 0;
82
+
83
+ await renderMedia({
84
+ composition,
85
  serveUrl: bundleLocation,
86
+ codec: 'h264',
87
+ pixelFormat: 'yuv420p',
88
+ outputLocation: outputPath,
89
+ imageFormat: 'jpeg',
90
+ jpegQuality: 80,
91
  inputProps: { scenes, settings },
92
+ chromiumOptions: {
93
+ executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
94
+ enableMultiProcessRendering: true,
95
+ // Enable GPU acceleration for Chrome compositing (HUGE for L40)
96
+ gl: 'angle',
97
+ ignoreDefaultArgs: ['--disable-gpu'],
98
+ args: ['--use-gl=angle', '--use-angle=gl-egl', '--no-sandbox']
99
+ },
100
+ concurrency: safeConcurrency,
101
+ disallowParallelEncoding: false,
102
+ onProgress: ({ progress }) => {
103
+ const percent = Math.round(progress * 100);
104
+ // Log every 2%
105
+ if (percent - lastLog >= 2 || percent === 100) {
106
+ console.log(`[Render] Progress: ${percent}%`);
107
+ lastLog = percent;
108
+ }
109
+ },
110
+ ffmpegOverride: ({ args }) => {
111
+ // NVENC Preset p1 = Fastest (lowest latency/quality trade-off for speed)
112
+ return [...args, '-c:v', 'h264_nvenc', '-preset', 'p1'];
113
  }
114
  });
115
+
116
+ console.log(`[Render] Render complete. Uploading to Supabase...`);
117
+
118
+ const videoBuffer = await readFile(outputPath);
119
+
120
  // Upload to Supabase Storage
121
+ const fileName = `${projectId}/video-${Date.now()}.mp4`;
122
+ const { data: uploadData, error: uploadError } = await supabase
123
+ .storage
124
+ .from('projects') // Assuming 'projects' bucket exists
125
+ .upload(fileName, videoBuffer, {
126
+ contentType: 'video/mp4',
127
+ upsert: true
 
 
 
 
128
  });
129
+
130
+ if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`);
131
+
132
+ // Get Public URL
133
+ const { data: { publicUrl } } = supabase
134
+ .storage
135
  .from('projects')
136
  .getPublicUrl(fileName);
137
+
138
+ console.log(`[Render] Uploaded to ${publicUrl}`);
139
+
140
+ // Update Project in DB
141
+ const { error: dbError } = await supabase
142
  .from('projects')
143
  .update({
144
+ status: 'done',
145
+ video_url: publicUrl
 
 
 
 
 
 
 
 
 
146
  })
147
  .eq('id', projectId);
148
+
149
+ if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
150
+
151
+ console.log(`[Render] Project ${projectId} updated successfully`);
152
+
153
+ // Cleanup
154
+ await rm(tempDir, { recursive: true, force: true });
155
+
156
  } catch (error) {
157
+ console.error(`[Render] Background Error for ${projectId}:`, error);
158
+ // Update project status to 'error'
159
+ await supabase
160
+ .from('projects')
161
+ .update({
162
+ status: 'error',
163
+ })
164
+ .eq('id', projectId);
 
 
 
 
 
 
 
 
165
  }
166
  })();
167
  });
168
 
169
+ app.listen(PORT, () => {
170
+ console.log(`🎬 FacelessFlowAI Renderer running on port ${PORT}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ // Debug: Check for Secrets on Startup
173
+ console.log('--- Environment Check ---');
174
+ console.log('SUPABASE_URL exists:', !!process.env.SUPABASE_URL);
175
+ console.log('SUPABASE_SERVICE_ROLE_KEY exists:', !!process.env.SUPABASE_SERVICE_ROLE_KEY);
176
+ console.log('Available Keys (starts with SUPA):', Object.keys(process.env).filter(k => k.startsWith('SUPA')));
177
+ console.log('-------------------------');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  });