Stylique commited on
Commit
aa606c4
·
verified ·
1 Parent(s): cec4e29

Update server.js

Browse files
Files changed (1) hide show
  1. server.js +62 -198
server.js CHANGED
@@ -4,15 +4,12 @@ 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, 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
 
@@ -24,7 +21,7 @@ app.use(express.json({ limit: '50mb' }));
24
 
25
  // Health check
26
  app.get('/', (req, res) => {
27
- res.json({ status: 'ok', service: 'FacelessFlowAI Video Renderer' });
28
  });
29
 
30
  // Render endpoint
@@ -55,206 +52,67 @@ app.post('/render', async (req, res) => {
55
  try {
56
  console.log(`[Render] Starting background render for project ${projectId}`);
57
 
 
 
 
 
 
 
58
  const bundleLocation = await bundle({
59
  entryPoint: join(__dirname, 'remotion', 'index.tsx'),
60
  webpackOverride: (config) => config,
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');
67
-
68
- // Helper: Robust Downloader with Retries
69
- const downloadAsset = async (url, destPath, retries = 3) => {
70
- for (let i = 0; i < retries; i++) {
71
- try {
72
- const response = await fetch(url);
73
- if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
74
- if (!response.body) throw new Error(`No body for ${url}`);
75
-
76
- await pipeline(response.body, createWriteStream(destPath));
77
- return; // Success
78
- } catch (err) {
79
- console.warn(`[Download] Attempt ${i + 1} failed for ${url}: ${err.message}`);
80
- if (i === retries - 1) throw err; // Throw on last failure
81
- await new Promise(r => setTimeout(r, 2000 * (i + 1))); // Backoff
82
- }
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 });
90
-
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);
98
- const batchResults = await Promise.all(batch.map((item, batchLocalIndex) => {
99
- const globalIndex = i + batchLocalIndex;
100
- return fn(item, globalIndex);
101
- }));
102
- results.push(...batchResults);
103
- }
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 => {
159
- const s2 = { ...s };
160
- if (s2.audio_url && s2.audio_url.startsWith('file://')) {
161
- const filename = s2.audio_url.split('/').pop();
162
- s2.audio_url = `http://localhost:${PORT}/tmp/${tempDirName}/${filename}`;
163
- }
164
- if (s2.image_url && s2.image_url.startsWith('file://')) {
165
- const filename = s2.image_url.split('/').pop();
166
- s2.image_url = `http://localhost:${PORT}/tmp/${tempDirName}/${filename}`;
167
- }
168
- return s2;
 
 
 
 
 
 
 
 
 
 
 
 
169
  });
170
 
171
- // --------------------------------------
172
- // SEGMENTED BATCH RENDERING
173
- // --------------------------------------
174
- // CRASH FIX: Reduce batch size to 10 to prevent OOM
175
- const BATCH_SIZE = 10;
176
- const totalBatches = Math.ceil(httpScenes.length / BATCH_SIZE);
177
- const partFiles = [];
178
-
179
- const os = await import('os');
180
- const cpuCount = os.cpus().length;
181
- console.log(`[Render] Splitting ${httpScenes.length} scenes into ${totalBatches} parts. CPU: ${cpuCount}`);
182
-
183
- for (let i = 0; i < totalBatches; i++) {
184
- const startIdx = i * BATCH_SIZE;
185
- const endIdx = Math.min((i + 1) * BATCH_SIZE, httpScenes.length);
186
- const batchScenes = httpScenes.slice(startIdx, endIdx);
187
-
188
- console.log(`[Render] Processing Part ${i + 1}/${totalBatches} (Scenes ${startIdx}-${endIdx - 1})...`);
189
-
190
- const composition = await selectComposition({
191
- serveUrl: bundleLocation,
192
- id: 'Main',
193
- inputProps: { scenes: batchScenes, settings },
194
- chromiumOptions: { executablePath: process.env.CHROME_BIN },
195
- });
196
-
197
- const partPath = join(tempDir, `part_${i}.mp4`);
198
- partFiles.push(partPath);
199
 
200
- let lastLoggedPercent = -1;
201
 
202
- await renderMedia({
203
- composition,
204
- serveUrl: bundleLocation,
205
- codec: 'h264',
206
- pixelFormat: 'yuv420p',
207
- outputLocation: partPath,
208
- imageFormat: 'jpeg',
209
- jpegQuality: 80,
210
- inputProps: { scenes: batchScenes, settings },
211
- chromiumOptions: {
212
- executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
213
- enableMultiProcessRendering: true, // SPEED: Enable since we have 32GB RAM
214
- args: [
215
- '--no-sandbox',
216
- '--disable-dev-shm-usage',
217
- '--disable-gpu', // CPU Mode
218
- '--mute-audio',
219
- ]
220
- },
221
- concurrency: 2, // SPEED: 2x Parallel Frames
222
- disallowParallelEncoding: false, // SPEED: Allow parallel encoding
223
- onProgress: ({ progress }) => {
224
- const percent = Math.round(progress * 100);
225
- if (percent !== lastLoggedPercent && percent % 10 === 0) {
226
- console.log(`[Render] Part ${i + 1} Progress: ${percent}%`);
227
- lastLoggedPercent = percent;
228
- }
229
- },
230
- });
231
-
232
- console.log(`[Render] Part ${i + 1} Done.`);
233
- // Clean composition object to help GC?
234
- }
235
-
236
- // --------------------------------------
237
- // STITCHING
238
- // --------------------------------------
239
- console.log(`[Render] All parts rendered. Stitching ${partFiles.length} files...`);
240
-
241
- const concatListPath = join(tempDir, 'concat_list.txt');
242
- const concatContent = partFiles.map(p => `file '${p}'`).join('\n');
243
- await writeFile(concatListPath, concatContent);
244
-
245
- const finalOutputPath = join(tempDir, 'output_final.mp4');
246
- const ffmpegCmd = `ffmpeg -f concat -safe 0 -i "${concatListPath}" -c copy "${finalOutputPath}"`;
247
-
248
- await exec(ffmpegCmd);
249
- console.log(`[Render] Stitching complete.`);
250
-
251
- // --------------------------------------
252
- // UPLOAD
253
- // --------------------------------------
254
- console.log(`[Render] Uploading to Supabase...`);
255
- const videoBuffer = await readFile(finalOutputPath);
256
  const fileName = `${projectId}/video-${Date.now()}.mp4`;
257
-
258
  const { data: uploadData, error: uploadError } = await supabase
259
  .storage
260
  .from('projects')
@@ -265,6 +123,7 @@ app.post('/render', async (req, res) => {
265
 
266
  if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`);
267
 
 
268
  const { data: { publicUrl } } = supabase
269
  .storage
270
  .from('projects')
@@ -275,21 +134,26 @@ app.post('/render', async (req, res) => {
275
  // Update Project in DB
276
  const { error: dbError } = await supabase
277
  .from('projects')
278
- .update({ status: 'done', video_url: publicUrl })
 
 
 
279
  .eq('id', projectId);
280
 
281
  if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
282
 
283
- console.log(`[Render] Project ${projectId} completed and updated.`);
284
 
285
  // Cleanup
286
- await rm(tempDir, { recursive: true, force: true });
287
 
288
  } catch (error) {
289
  console.error(`[Render] Background Error for ${projectId}:`, error);
290
  await supabase
291
  .from('projects')
292
- .update({ status: 'error' })
 
 
293
  .eq('id', projectId);
294
  }
295
  })();
 
4
  import cors from 'cors';
5
  import { bundle } from '@remotion/bundler';
6
  import { renderMedia, selectComposition } from '@remotion/renderer';
7
+ import { readFile, rm } from 'fs/promises';
8
  import { tmpdir } from 'os';
9
  import { join, dirname } from 'path';
 
10
  import { fileURLToPath } from 'url';
11
+ import os from 'os';
 
12
 
 
13
  const __filename = fileURLToPath(import.meta.url);
14
  const __dirname = dirname(__filename);
15
 
 
21
 
22
  // Health check
23
  app.get('/', (req, res) => {
24
+ res.json({ status: 'ok', service: 'FacelessFlowAI Video Renderer (Simple)' });
25
  });
26
 
27
  // Render endpoint
 
52
  try {
53
  console.log(`[Render] Starting background render for project ${projectId}`);
54
 
55
+ // Available resources
56
+ const cpuCount = os.cpus().length;
57
+ const freeMem = (os.freemem() / 1024 / 1024).toFixed(0);
58
+ const totalMem = (os.totalmem() / 1024 / 1024).toFixed(0);
59
+ console.log(`[Resources] vCPUs: ${cpuCount} | RAM: ${freeMem}/${totalMem} MB Free`);
60
+
61
  const bundleLocation = await bundle({
62
  entryPoint: join(__dirname, 'remotion', 'index.tsx'),
63
  webpackOverride: (config) => config,
64
  });
65
 
66
+ // Clean Composition Params
67
+ const composition = await selectComposition({
68
+ serveUrl: bundleLocation,
69
+ id: 'Main',
70
+ inputProps: { scenes, settings }, // Direct Cloud URLs
71
+ chromiumOptions: { executablePath: process.env.CHROME_BIN },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  });
73
 
74
+ const outputLocation = join(tmpdir(), `out-${projectId}.mp4`);
75
+
76
+ let lastLoggedPercent = -1;
77
+
78
+ // Simple Render
79
+ await renderMedia({
80
+ composition,
81
+ serveUrl: bundleLocation,
82
+ codec: 'h264',
83
+ pixelFormat: 'yuv420p',
84
+ outputLocation: outputLocation,
85
+ imageFormat: 'jpeg',
86
+ jpegQuality: 80,
87
+ inputProps: { scenes, settings },
88
+ chromiumOptions: {
89
+ executablePath: process.env.CHROME_BIN || '/usr/bin/google-chrome-stable',
90
+ enableMultiProcessRendering: true, // Requested: Enable Multi-process
91
+ args: [
92
+ '--no-sandbox',
93
+ '--disable-dev-shm-usage',
94
+ '--disable-gpu',
95
+ '--mute-audio',
96
+ ]
97
+ },
98
+ // Use all available cores (or set specific number)
99
+ concurrency: 4,
100
+ disallowParallelEncoding: false,
101
+ onProgress: ({ progress }) => {
102
+ const percent = Math.round(progress * 100);
103
+ if (percent !== lastLoggedPercent && percent % 5 === 0) {
104
+ console.log(`[Render] Progress: ${percent}%`);
105
+ lastLoggedPercent = percent;
106
+ }
107
+ },
108
  });
109
 
110
+ console.log(`[Render] Render complete. Uploading to Supabase...`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
+ const videoBuffer = await readFile(outputLocation);
113
 
114
+ // Upload to Supabase Storage
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  const fileName = `${projectId}/video-${Date.now()}.mp4`;
 
116
  const { data: uploadData, error: uploadError } = await supabase
117
  .storage
118
  .from('projects')
 
123
 
124
  if (uploadError) throw new Error(`Upload failed: ${uploadError.message}`);
125
 
126
+ // Get Public URL
127
  const { data: { publicUrl } } = supabase
128
  .storage
129
  .from('projects')
 
134
  // Update Project in DB
135
  const { error: dbError } = await supabase
136
  .from('projects')
137
+ .update({
138
+ status: 'done',
139
+ video_url: publicUrl
140
+ })
141
  .eq('id', projectId);
142
 
143
  if (dbError) throw new Error(`DB Update failed: ${dbError.message}`);
144
 
145
+ console.log(`[Render] Project ${projectId} updated successfully`);
146
 
147
  // Cleanup
148
+ await rm(outputLocation, { force: true });
149
 
150
  } catch (error) {
151
  console.error(`[Render] Background Error for ${projectId}:`, error);
152
  await supabase
153
  .from('projects')
154
+ .update({
155
+ status: 'error',
156
+ })
157
  .eq('id', projectId);
158
  }
159
  })();