Stylique commited on
Commit
7f69117
·
verified ·
1 Parent(s): 6e0b3c5

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +428 -257
server.js CHANGED
@@ -1,8 +1,8 @@
1
  import express from 'express';
2
  import cors from 'cors';
3
  import { bundle } from '@remotion/bundler';
4
- import { renderFrames, stitchFramesToVideo } from '@remotion/renderer';
5
- import { readFile, mkdir, rm, writeFile } from 'fs/promises';
6
  import { tmpdir } from 'os';
7
  import { join, dirname } from 'path';
8
  import { randomUUID } from 'crypto';
@@ -17,362 +17,533 @@ const PORT = process.env.PORT || 7860;
17
  app.use(cors());
18
  app.use(express.json({ limit: '50mb' }));
19
 
20
- // Global cache for bundle
21
- let cachedBundle = null;
22
- let lastBundleTime = 0;
23
- const BUNDLE_CACHE_TTL = 5 * 60 * 1000;
 
 
 
24
 
25
  // Health check
26
  app.get('/', (req, res) => {
27
  res.json({
28
  status: 'ok',
29
  service: 'FacelessFlowAI Video Renderer',
30
- note: 'Custom rendering pipeline with NVENC'
 
31
  });
32
  });
33
 
34
- // Get bundle with caching
35
- async function getCachedBundle() {
36
  const now = Date.now();
37
 
38
- if (cachedBundle && (now - lastBundleTime) < BUNDLE_CACHE_TTL) {
39
- console.log('[Performance] Using cached bundle');
40
- return cachedBundle;
41
  }
42
 
43
- console.log('[Performance] Creating new bundle');
44
 
45
  try {
46
- cachedBundle = await bundle({
47
  entryPoint: join(__dirname, 'remotion', 'index.tsx'),
 
48
  webpackOverride: (config) => {
49
- config.output = {
50
- ...config.output,
51
- filename: 'bundle.js',
52
- publicPath: '/',
53
- };
54
 
 
55
  config.optimization = {
56
- ...config.optimization,
57
- splitChunks: false,
58
- runtimeChunk: false,
 
 
 
 
59
  };
60
 
61
- config.mode = 'production';
62
- config.devtool = false;
 
 
 
 
63
 
64
  return config;
65
  },
66
  });
67
 
68
- console.log(`[Bundle] Bundle created at: ${cachedBundle}`);
69
- lastBundleTime = now;
70
- return cachedBundle;
 
 
71
  } catch (error) {
72
- console.error('[Bundle] Error creating bundle:', error);
73
  throw error;
74
  }
75
  }
76
 
77
- // Custom video stitching with system FFmpeg
78
- async function stitchFramesWithNVENC(frameDir, fps, outputPath, width = 1920, height = 1080) {
79
- const { exec } = await import('child_process');
80
- const { promisify } = await import('util');
81
- const execAsync = promisify(exec);
82
-
83
- try {
84
- console.log('[NVENC] Starting hardware encoding with system FFmpeg');
85
 
86
- // First, check if NVENC is available
87
- const { stdout: encoderCheck } = await execAsync('ffmpeg -encoders 2>&1 | grep -i nvenc');
88
- const hasNVENC = encoderCheck.includes('h264_nvenc');
 
89
 
90
- if (hasNVENC) {
91
- console.log('[NVENC] Using hardware encoding');
92
-
93
- // Build FFmpeg command for NVENC
94
- const ffmpegCmd = [
95
- 'ffmpeg',
96
- '-y', // Overwrite output
97
- '-framerate', String(fps),
98
- '-i', `${frameDir}/%d.png`, // Input frames
99
- '-c:v', 'h264_nvenc', // NVENC encoder
100
- '-preset', 'p1', // Fastest quality preset
101
- '-cq', '20', // Constant quality
102
- '-movflags', '+faststart',
103
- '-pix_fmt', 'yuv420p',
104
- '-vf', `scale=${width}:${height}:flags=lanczos`,
105
- '-c:a', 'aac',
106
- '-b:a', '192k',
107
- outputPath
108
- ].join(' ');
109
-
110
- console.log(`[NVENC] Command: ${ffmpegCmd}`);
111
-
112
- const { stdout, stderr } = await execAsync(ffmpegCmd, { maxBuffer: 1024 * 1024 * 50 });
113
- console.log('[NVENC] Encoding completed');
114
-
115
- } else {
116
- console.log('[NVENC] Falling back to software encoding');
117
-
118
- // Fallback to software encoding
119
- const ffmpegCmd = [
120
- 'ffmpeg',
121
- '-y',
122
- '-framerate', String(fps),
123
- '-i', `${frameDir}/%d.png`,
124
- '-c:v', 'libx264',
125
- '-preset', 'fast',
126
- '-crf', '23',
127
- '-movflags', '+faststart',
128
- '-pix_fmt', 'yuv420p',
129
- '-vf', `scale=${width}:${height}:flags=lanczos`,
130
- '-c:a', 'aac',
131
- '-b:a', '192k',
132
- outputPath
133
- ].join(' ');
134
-
135
- const { stdout, stderr } = await execAsync(ffmpegCmd, { maxBuffer: 1024 * 1024 * 50 });
136
- console.log('[Software] Encoding completed');
137
- }
138
 
139
- return true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- } catch (error) {
142
- console.error('[Encoding] Error:', error.message);
143
- throw error;
 
 
 
 
 
 
 
144
  }
145
  }
146
 
147
- // Custom render pipeline
148
- async function customRender(composition, bundleLocation, scenes, settings, outputPath) {
149
- const startTime = Date.now();
150
-
151
- console.log('[Custom Render] Starting frame rendering...');
152
-
153
- // Step 1: Render frames
154
- const frameDir = join(tmpdir(), `frames-${randomUUID()}`);
155
- await mkdir(frameDir, { recursive: true });
156
-
157
- const renderStart = Date.now();
158
 
159
- await renderFrames({
160
- config: composition,
161
- serveUrl: bundleLocation,
162
- onStart: ({ frameCount }) => {
163
- console.log(`[Frames] Rendering ${frameCount} frames`);
164
- },
165
- onFrameUpdate: ({ renderedFrames, encodedFrames }) => {
166
- const percent = Math.round((renderedFrames / composition.durationInFrames) * 100);
167
- const elapsed = (Date.now() - renderStart) / 1000;
168
- const fps = elapsed > 0 ? Math.round(renderedFrames / elapsed) : 0;
169
- console.log(`[Frames] ${percent}% | FPS: ${fps} | Rendered: ${renderedFrames}/${composition.durationInFrames}`);
170
- },
171
- outputDir: frameDir,
172
- inputProps: { scenes, settings },
173
- imageFormat: 'png',
174
- compositionId: composition.id,
175
- concurrency: 6,
176
- chromiumOptions: {
177
- executablePath: '/usr/bin/google-chrome-stable',
178
- args: ['--no-sandbox', '--disable-dev-shm-usage']
179
- },
180
  });
181
 
182
- const frameTime = Date.now() - renderStart;
183
- console.log(`[Frames] Completed in ${Math.round(frameTime / 1000)}s`);
184
 
185
- // Step 2: Stitch frames with NVENC
186
- console.log('[Custom Render] Stitching frames to video...');
187
- const stitchStart = Date.now();
188
 
189
- await stitchFramesWithNVENC(
190
- frameDir,
191
- composition.fps,
192
- outputPath,
193
- composition.width,
194
- composition.height
195
- );
196
 
197
- const stitchTime = Date.now() - stitchStart;
198
- console.log(`[Stitching] Completed in ${Math.round(stitchTime / 1000)}s`);
199
 
200
- // Step 3: Cleanup frames
201
- await rm(frameDir, { recursive: true, force: true });
202
-
203
- const totalTime = Date.now() - startTime;
204
- console.log(`[Custom Render] Total time: ${Math.round(totalTime / 1000)}s`);
205
 
206
- return totalTime;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  }
208
 
209
- // Render endpoint
210
  app.post('/render', async (req, res) => {
211
  const { projectId, scenes, settings } = req.body;
212
-
 
213
  if (!projectId || !scenes || !settings) {
214
- return res.status(400).json({ error: 'Missing projectId, scenes, or settings' });
 
 
 
 
215
  }
216
-
217
- // Initialize Supabase Admin Client
218
  const SUPABASE_URL = process.env.SUPABASE_URL;
219
  const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
220
-
221
  if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
222
- console.error('[Config] Missing Supabase credentials');
223
- return res.status(500).json({ error: 'Renderer configuration error' });
 
 
224
  }
225
-
226
  const { createClient } = await import('@supabase/supabase-js');
227
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
228
-
229
- // 1. Respond immediately
230
- res.json({
231
- success: true,
232
- message: 'Rendering started in background',
233
- note: 'Using custom render pipeline with hardware acceleration'
 
234
  });
235
-
236
- // 2. Start Background Process
237
  (async () => {
238
- const startTime = Date.now();
239
-
240
  try {
241
- console.log(`[Render] Starting render for project ${projectId}`);
242
-
243
- const bundleLocation = await getCachedBundle();
244
- console.log('[Bundle] Bundle ready');
245
-
246
- const composition = await import('@remotion/renderer').then(mod =>
247
- mod.selectComposition({
248
- serveUrl: bundleLocation,
249
- id: 'Main',
250
- inputProps: { scenes, settings },
251
- chromiumOptions: {
252
- executablePath: '/usr/bin/google-chrome-stable',
253
- args: ['--no-sandbox', '--disable-dev-shm-usage']
254
- },
255
  })
256
- );
257
 
258
- console.log(`[Composition] Loaded: ${composition.durationInFrames} frames at ${composition.fps}fps`);
259
-
260
- const tempDir = join(tmpdir(), `remotion-${randomUUID()}`);
261
- await mkdir(tempDir, { recursive: true });
262
- const outputPath = join(tempDir, 'output.mp4');
263
-
264
- console.log('[Render] Starting custom render pipeline...');
 
 
 
 
 
 
265
 
266
- const renderTime = await customRender(
267
- composition,
268
- bundleLocation,
269
- scenes,
270
- settings,
271
- outputPath
272
- );
273
-
274
- console.log(`[Render] Completed in ${Math.round(renderTime / 1000)}s`);
275
-
276
- console.log(`[Upload] Reading video file...`);
277
- const videoBuffer = await readFile(outputPath);
278
- console.log(`[Upload] Video size: ${Math.round(videoBuffer.length / (1024 * 1024))}MB`);
279
-
280
  // Upload to Supabase Storage
281
- const fileName = `${projectId}/video-${Date.now()}.mp4`;
282
- const { data: uploadData, error: uploadError } = await supabase
283
- .storage
 
 
 
284
  .from('projects')
285
- .upload(fileName, videoBuffer, {
286
- contentType: 'video/mp4',
287
- upsert: true
 
288
  });
289
-
290
- if (uploadError) {
291
- console.error('[Upload] Error:', uploadError);
292
- throw new Error(`Upload failed: ${uploadError.message}`);
293
- }
294
-
295
- // Get Public URL
296
- const { data: { publicUrl } } = supabase
297
- .storage
298
  .from('projects')
299
  .getPublicUrl(fileName);
300
-
301
- console.log(`[Upload] Uploaded to ${publicUrl}`);
302
-
303
- // Update Project in DB
304
- const { error: dbError } = await supabase
305
  .from('projects')
306
  .update({
307
- status: 'done',
308
  video_url: publicUrl,
309
- render_time: renderTime,
310
- rendered_at: new Date().toISOString()
 
 
 
 
 
 
 
311
  })
312
  .eq('id', projectId);
313
-
314
- if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
315
-
316
- console.log(`[Render] Project ${projectId} updated successfully`);
317
-
318
- // Cleanup
319
- await rm(tempDir, { recursive: true, force: true });
320
-
321
  } catch (error) {
322
- console.error(`[Render] Error for ${projectId}:`, error.message);
323
 
324
- // Update project status to 'error'
325
  try {
326
  await supabase
327
  .from('projects')
328
  .update({
329
- status: 'error',
330
- error_message: error.message
 
 
331
  })
332
  .eq('id', projectId);
333
  } catch (dbError) {
334
- console.error('[DB] Failed to update error status:', dbError);
335
  }
336
  }
337
  })();
338
  });
339
 
340
- // Status endpoint
341
  app.get('/status', async (req, res) => {
342
  const os = await import('os');
343
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
344
  res.json({
345
- status: 'running',
346
- cpu_cores: os.cpus().length,
347
- total_memory: Math.round(os.totalmem() / (1024 * 1024 * 1024)) + 'GB',
348
- free_memory: Math.round(os.freemem() / (1024 * 1024 * 1024)) + 'GB',
349
- bundle_cached: cachedBundle !== null,
350
- uptime: Math.round(process.uptime()) + 's',
351
- render_method: 'Custom pipeline with hardware acceleration'
352
  });
353
  });
354
 
355
  // Start server
356
- app.listen(PORT, async () => {
357
- console.log(`🎬 FacelessFlowAI Renderer running on port ${PORT}`);
 
 
 
 
 
358
 
359
- const os = await import('os');
360
- console.log(`💾 Memory: ${Math.round(os.totalmem() / (1024 * 1024 * 1024))}GB`);
361
- console.log(`⚡ CPU Cores: ${os.cpus().length}`);
 
 
 
362
 
363
- console.log('--- Configuration ---');
364
- console.log('RENDER: Custom pipeline (renderFrames + FFmpeg)');
365
- console.log('ENCODING: Hardware NVENC when available');
366
- console.log('FRAMES: PNG format for quality');
367
- console.log('---------------------');
368
 
369
- // Auto-warmup
370
  setTimeout(async () => {
371
  try {
372
- await getCachedBundle();
373
- console.log('[Auto-Warmup] Bundle ready');
 
374
  } catch (error) {
375
- console.error('[Auto-Warmup] Failed:', error.message);
376
  }
377
- }, 3000);
378
  });
 
1
  import express from 'express';
2
  import cors from 'cors';
3
  import { bundle } from '@remotion/bundler';
4
+ import { renderMedia, selectComposition } from '@remotion/renderer';
5
+ import { readFile, mkdir, rm } from 'fs/promises';
6
  import { tmpdir } from 'os';
7
  import { join, dirname } from 'path';
8
  import { randomUUID } from 'crypto';
 
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' as const,
96
+ pixelFormat: 'yuv420p' as const,
97
+ imageFormat: 'jpeg' as const,
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
+ numberOfGifLoops: 0,
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, public code: string, public details?: any) {
139
+ super(message);
140
+ this.name = 'RenderError';
141
  }
142
  }
143
 
144
+ // Main render function (professional pattern)
145
+ async function renderVideo(params: {
146
+ compositionId: string;
147
+ serveUrl: string;
148
+ inputProps: any;
149
+ onProgress?: (progress: number) => void;
150
+ }) {
151
+ const { compositionId, serveUrl, inputProps, onProgress } = params;
 
 
 
152
 
153
+ console.log('[Render] Selecting composition...');
154
+ const composition = await selectComposition({
155
+ serveUrl,
156
+ id: compositionId,
157
+ inputProps,
158
+ chromiumOptions: getRenderConfig().chromiumOptions,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  });
160
 
161
+ console.log(`[Render] Composition: ${composition.durationInFrames} frames @ ${composition.fps}fps`);
 
162
 
163
+ const tempDir = join(tmpdir(), `render-${randomUUID()}`);
164
+ await mkdir(tempDir, { recursive: true });
165
+ const outputPath = join(tempDir, 'output.mp4');
166
 
167
+ const renderConfig = getRenderConfig();
 
 
 
 
 
 
168
 
169
+ console.log('[Render] Starting render...');
 
170
 
171
+ const startTime = Date.now();
172
+ let lastProgress = 0;
 
 
 
173
 
174
+ try {
175
+ await renderMedia({
176
+ composition,
177
+ serveUrl,
178
+ outputLocation: outputPath,
179
+ inputProps,
180
+ onProgress: ({ progress }) => {
181
+ const percent = Math.round(progress * 100);
182
+ if (percent !== lastProgress && percent % 5 === 0) {
183
+ console.log(`[Progress] ${percent}%`);
184
+ lastProgress = percent;
185
+ onProgress?.(progress);
186
+ }
187
+ },
188
+ ...renderConfig,
189
+ });
190
+
191
+ const renderTime = Date.now() - startTime;
192
+ const fps = Math.round(composition.durationInFrames / (renderTime / 1000));
193
+
194
+ console.log(`[Render] Completed in ${Math.round(renderTime / 1000)}s (${fps} FPS)`);
195
+
196
+ const videoBuffer = await readFile(outputPath);
197
+ const fileSizeMB = Math.round(videoBuffer.length / (1024 * 1024));
198
+
199
+ console.log(`[Render] Output: ${fileSizeMB}MB`);
200
+
201
+ // Cleanup temp directory
202
+ await rm(tempDir, { recursive: true, force: true });
203
+
204
+ return {
205
+ buffer: videoBuffer,
206
+ duration: renderTime,
207
+ fps,
208
+ fileSizeMB,
209
+ composition,
210
+ };
211
+
212
+ } catch (error) {
213
+ // Cleanup on error
214
+ try {
215
+ await rm(tempDir, { recursive: true, force: true });
216
+ } catch (cleanupError) {
217
+ console.warn('[Cleanup] Failed to cleanup temp dir:', cleanupError);
218
+ }
219
+
220
+ throw new RenderError(
221
+ `Render failed: ${error.message}`,
222
+ 'RENDER_FAILED',
223
+ { originalError: error.message }
224
+ );
225
+ }
226
  }
227
 
228
+ // Render endpoint (professional API design)
229
  app.post('/render', async (req, res) => {
230
  const { projectId, scenes, settings } = req.body;
231
+
232
+ // Input validation
233
  if (!projectId || !scenes || !settings) {
234
+ return res.status(400).json({
235
+ error: 'VALIDATION_ERROR',
236
+ message: 'Missing required fields: projectId, scenes, or settings',
237
+ required: ['projectId', 'scenes', 'settings']
238
+ });
239
  }
240
+
241
+ // Initialize Supabase
242
  const SUPABASE_URL = process.env.SUPABASE_URL;
243
  const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
244
+
245
  if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
246
+ return res.status(500).json({
247
+ error: 'CONFIGURATION_ERROR',
248
+ message: 'Server configuration error'
249
+ });
250
  }
251
+
252
  const { createClient } = await import('@supabase/supabase-js');
253
  const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
254
+
255
+ // Accept request immediately (async processing)
256
+ res.json({
257
+ success: true,
258
+ message: 'Render job accepted',
259
+ jobId: projectId,
260
+ estimatedTime: 'Processing...'
261
  });
262
+
263
+ // Start async processing
264
  (async () => {
 
 
265
  try {
266
+ console.log(`[Job ${projectId}] Starting render job`);
267
+
268
+ // Update status to processing
269
+ await supabase
270
+ .from('projects')
271
+ .update({
272
+ status: 'processing',
273
+ started_at: new Date().toISOString()
 
 
 
 
 
 
274
  })
275
+ .eq('id', projectId);
276
 
277
+ // Get bundle
278
+ const bundleLocation = await getBundle();
279
+
280
+ // Render video
281
+ const result = await renderVideo({
282
+ compositionId: 'Main',
283
+ serveUrl: bundleLocation,
284
+ inputProps: { scenes, settings },
285
+ onProgress: (progress) => {
286
+ // Optional: Send progress updates via WebSocket or store in DB
287
+ console.log(`[Job ${projectId}] Progress: ${Math.round(progress * 100)}%`);
288
+ }
289
+ });
290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
291
  // Upload to Supabase Storage
292
+ const fileName = `${projectId}/${Date.now()}-${randomUUID().slice(0, 8)}.mp4`;
293
+ const contentType = 'video/mp4';
294
+
295
+ console.log(`[Job ${projectId}] Uploading to storage...`);
296
+
297
+ const { error: uploadError } = await supabase.storage
298
  .from('projects')
299
+ .upload(fileName, result.buffer, {
300
+ contentType,
301
+ cacheControl: '3600',
302
+ upsert: false
303
  });
304
+
305
+ if (uploadError) throw uploadError;
306
+
307
+ // Get public URL
308
+ const { data: { publicUrl } } = supabase.storage
 
 
 
 
309
  .from('projects')
310
  .getPublicUrl(fileName);
311
+
312
+ // Update project with success
313
+ await supabase
 
 
314
  .from('projects')
315
  .update({
316
+ status: 'completed',
317
  video_url: publicUrl,
318
+ rendered_at: new Date().toISOString(),
319
+ render_metadata: {
320
+ duration_ms: result.duration,
321
+ file_size_mb: result.fileSizeMB,
322
+ fps: result.fps,
323
+ total_frames: result.composition.durationInFrames,
324
+ resolution: `${result.composition.width}x${result.composition.height}`,
325
+ timestamp: new Date().toISOString()
326
+ }
327
  })
328
  .eq('id', projectId);
329
+
330
+ console.log(`[Job ${projectId}] Completed successfully`);
331
+
 
 
 
 
 
332
  } catch (error) {
333
+ console.error(`[Job ${projectId}] Failed:`, error);
334
 
335
+ // Update with error
336
  try {
337
  await supabase
338
  .from('projects')
339
  .update({
340
+ status: 'failed',
341
+ error_message: error.message,
342
+ error_details: error.details || {},
343
+ failed_at: new Date().toISOString()
344
  })
345
  .eq('id', projectId);
346
  } catch (dbError) {
347
+ console.error(`[Job ${projectId}] Failed to update error status:`, dbError);
348
  }
349
  }
350
  })();
351
  });
352
 
353
+ // Status endpoint with detailed info
354
  app.get('/status', async (req, res) => {
355
  const os = await import('os');
356
 
357
+ const status = {
358
+ service: 'FacelessFlowAI Renderer',
359
+ version: '2.0.0',
360
+ status: 'operational',
361
+ uptime: Math.round(process.uptime()),
362
+
363
+ // System info
364
+ system: {
365
+ cpu_cores: os.cpus().length,
366
+ total_memory_gb: Math.round(os.totalmem() / (1024 ** 3)),
367
+ free_memory_gb: Math.round(os.freemem() / (1024 ** 3)),
368
+ platform: os.platform(),
369
+ arch: os.arch()
370
+ },
371
+
372
+ // Service info
373
+ service_info: {
374
+ bundle_cached: cachedBundle.location !== null,
375
+ bundle_version: cachedBundle.version,
376
+ cache_age_minutes: cachedBundle.timestamp ?
377
+ Math.round((Date.now() - cachedBundle.timestamp) / 60000) : null
378
+ },
379
+
380
+ // Configuration
381
+ config: {
382
+ max_concurrency: 6,
383
+ codec: 'h264',
384
+ default_resolution: '1920x1080',
385
+ default_fps: 30
386
+ },
387
+
388
+ timestamp: new Date().toISOString()
389
+ };
390
+
391
+ res.json(status);
392
+ });
393
+
394
+ // Metrics endpoint (for monitoring)
395
+ app.get('/metrics', async (req, res) => {
396
+ const os = await import('os');
397
+ const { execSync } = await import('child_process');
398
+
399
+ try {
400
+ // System metrics
401
+ const loadAvg = os.loadavg();
402
+ const memoryUsage = process.memoryUsage();
403
+
404
+ // GPU metrics if available
405
+ let gpuInfo = null;
406
+ try {
407
+ const gpuOutput = execSync('nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total --format=csv,noheader,nounits 2>/dev/null || echo ""', { encoding: 'utf8' });
408
+ if (gpuOutput.trim()) {
409
+ const [gpuUtil, memUsed, memTotal] = gpuOutput.trim().split(', ').map(Number);
410
+ gpuInfo = {
411
+ utilization_percent: gpuUtil,
412
+ memory_used_mb: memUsed,
413
+ memory_total_mb: memTotal,
414
+ memory_usage_percent: Math.round((memUsed / memTotal) * 100)
415
+ };
416
+ }
417
+ } catch {
418
+ // GPU not available or nvidia-smi not installed
419
+ }
420
+
421
+ const metrics = {
422
+ timestamp: new Date().toISOString(),
423
+ system: {
424
+ load_avg: loadAvg,
425
+ memory: {
426
+ rss_mb: Math.round(memoryUsage.rss / 1024 / 1024),
427
+ heap_total_mb: Math.round(memoryUsage.heapTotal / 1024 / 1024),
428
+ heap_used_mb: Math.round(memoryUsage.heapUsed / 1024 / 1024),
429
+ external_mb: Math.round(memoryUsage.external / 1024 / 1024)
430
+ },
431
+ uptime: process.uptime()
432
+ },
433
+ gpu: gpuInfo,
434
+ service: {
435
+ bundle_cache_hit: cachedBundle.location !== null,
436
+ active_requests: 0 // You could track this
437
+ }
438
+ };
439
+
440
+ res.json(metrics);
441
+ } catch (error) {
442
+ res.status(500).json({ error: error.message });
443
+ }
444
+ });
445
+
446
+ // System check endpoint
447
+ app.get('/health', async (req, res) => {
448
+ const checks = [];
449
+
450
+ // Check 1: System memory
451
+ try {
452
+ const os = await import('os');
453
+ const freeMemoryGB = Math.round(os.freemem() / (1024 ** 3));
454
+ checks.push({
455
+ name: 'system_memory',
456
+ status: freeMemoryGB > 2 ? 'healthy' : 'warning',
457
+ message: `${freeMemoryGB}GB free memory`,
458
+ value: freeMemoryGB
459
+ });
460
+ } catch (error) {
461
+ checks.push({
462
+ name: 'system_memory',
463
+ status: 'error',
464
+ message: error.message
465
+ });
466
+ }
467
+
468
+ // Check 2: Chrome availability
469
+ try {
470
+ const { execSync } = await import('child_process');
471
+ execSync('/usr/bin/google-chrome-stable --version', { stdio: 'pipe' });
472
+ checks.push({
473
+ name: 'chrome',
474
+ status: 'healthy',
475
+ message: 'Chrome is available'
476
+ });
477
+ } catch (error) {
478
+ checks.push({
479
+ name: 'chrome',
480
+ status: 'warning',
481
+ message: 'Chrome not found, will use headless shell'
482
+ });
483
+ }
484
+
485
+ // Check 3: FFmpeg
486
+ try {
487
+ const { execSync } = await import('child_process');
488
+ execSync('ffmpeg -version', { stdio: 'pipe' });
489
+ checks.push({
490
+ name: 'ffmpeg',
491
+ status: 'healthy',
492
+ message: 'FFmpeg is available'
493
+ });
494
+ } catch (error) {
495
+ checks.push({
496
+ name: 'ffmpeg',
497
+ status: 'error',
498
+ message: 'FFmpeg not found'
499
+ });
500
+ }
501
+
502
+ // Check 4: Bundle status
503
+ checks.push({
504
+ name: 'bundle',
505
+ status: cachedBundle.location ? 'healthy' : 'warming',
506
+ message: cachedBundle.location ? 'Bundle cached' : 'Bundle warming up',
507
+ version: cachedBundle.version
508
+ });
509
+
510
+ const allHealthy = checks.every(c => c.status === 'healthy' || c.status === 'warning');
511
+ const anyError = checks.some(c => c.status === 'error');
512
+
513
  res.json({
514
+ status: anyError ? 'degraded' : (allHealthy ? 'healthy' : 'warning'),
515
+ checks,
516
+ timestamp: new Date().toISOString()
 
 
 
 
517
  });
518
  });
519
 
520
  // Start server
521
+ app.listen(PORT, () => {
522
+ console.log(`
523
+ 🎬 FacelessFlowAI Professional Renderer
524
+ ======================================
525
+ Port: ${PORT}
526
+ Mode: Production
527
+ Hardware: L4 GPU (8 vCPU, 30GB RAM)
528
 
529
+ Endpoints:
530
+ - GET / Service status
531
+ - GET /health System health check
532
+ - GET /status Detailed service info
533
+ - GET /metrics System metrics
534
+ - POST /render Render video
535
 
536
+ Initializing...
537
+ `);
 
 
 
538
 
539
+ // Warm up the system
540
  setTimeout(async () => {
541
  try {
542
+ console.log('[Init] Warming up renderer...');
543
+ await getBundle();
544
+ console.log('[Init] Renderer ready');
545
  } catch (error) {
546
+ console.error('[Init] Warmup failed:', error.message);
547
  }
548
+ }, 2000);
549
  });