Stylique commited on
Commit
885352b
·
verified ·
1 Parent(s): eced7d3

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +106 -86
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 processInBatches = 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,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 processInBatches(scenes, 5, async (scene, idx) => {
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
- // 3. Mount temp dir statically so Remotion can fetch via HTTP (bypass file:// restriction)
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
- // Add middleware to debug asset requests from Chrome
 
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 downloaded and mounted at /tmp/${tempDirName}. Starting engine...`);
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
- const composition = await selectComposition({
175
- serveUrl: bundleLocation,
176
- id: 'Main',
177
- inputProps: { scenes: httpScenes, settings },
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] Detected ${cpuCount} CPUs.`);
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
- console.log(`[Render] Render complete. Uploading to Supabase...`);
 
 
 
224
 
225
- const videoBuffer = await readFile(outputPath);
226
 
227
- // Upload to Supabase Storage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 successfully`);
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
  });