shiveshnavin commited on
Commit
7ad7585
·
1 Parent(s): 9b7faf0

Change to work with single line

Browse files
server-plugins/split-render.js CHANGED
@@ -7,6 +7,9 @@ import { PerformanceRecorder } from 'common-utils';
7
 
8
  /**
9
  * SplitRenderPlugin must be used with transparent-background
 
 
 
10
  */
11
  export class SplitRenderPlugin extends Plugin {
12
  constructor(name, options) {
 
7
 
8
  /**
9
  * SplitRenderPlugin must be used with transparent-background
10
+ * when the Remotion renders the captions on a black background.
11
+ * This plugin will then composite the transparent video over
12
+ * the actual media files thus combining them into the final output.
13
  */
14
  export class SplitRenderPlugin extends Plugin {
15
  constructor(name, options) {
utils/CaptionRender.js CHANGED
@@ -1,9 +1,11 @@
 
 
1
  import { spawn } from 'child_process';
2
  import os from 'os';
3
  import path from 'path';
4
  import fs from 'fs';
5
 
6
- var ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
7
 
8
  export class CaptionRenderer {
9
  validateCaption(originalManuscript) {
@@ -19,159 +21,96 @@ export class CaptionRenderer {
19
 
20
  async doRender(jobId, originalManuscript, onLog, npmScript, options, controller) {
21
  const outDir = path.join(process.cwd(), 'out');
22
- if (!fs.existsSync(outDir)) {
23
- fs.mkdirSync(outDir, { recursive: true });
24
- }
25
 
26
  const outFile = path.join(outDir, `${jobId}_final.mp4`);
27
 
28
- // Build a single complex filter chain for all sections
29
- let filterComplex = '';
30
  let inputs = [];
31
- let sectionOutputs = [];
32
  let inputIndex = 0;
 
 
33
 
34
- // Process each transcript section into the filter graph
35
- for (let i = 0; i < originalManuscript.transcript.length; i++) {
36
- const section = originalManuscript.transcript[i];
37
- const audioDuration = section.durationInSeconds;
38
- const audioPath = section.audioFullPath;
39
- const captionFile = section.audioCaptionFile;
40
- const mediaFiles = section.mediaAbsPaths || [];
41
-
42
- if (mediaFiles.length === 0) {
43
- console.log(`Skipping section ${i}: no media files`);
44
- continue;
45
- }
46
 
47
- // Add all media files as inputs for this section
48
- const sectionVideoStart = inputIndex;
49
- for (let j = 0; j < mediaFiles.length; j++) {
50
- inputs.push('-i', mediaFiles[j].path);
51
  inputIndex++;
52
- }
53
 
54
- // Add audio input for this section
55
- inputs.push('-i', audioPath);
56
- const audioIndex = inputIndex;
57
  inputIndex++;
58
 
59
- // Build video filter chain for this section
60
- let videoFilters = [];
61
- let totalVideoDuration = 0;
62
 
63
- for (let j = 0; j < mediaFiles.length; j++) {
64
- let mediaFileDuration = mediaFiles[j].prompt?.durationSec || mediaFiles[j].durationSec
65
- if (j == mediaFiles.length - 1 && mediaFileDuration + totalVideoDuration < audioDuration) {
66
- mediaFileDuration = audioDuration - totalVideoDuration;
67
- }
68
- const clipDuration = Math.min(mediaFileDuration, audioDuration - totalVideoDuration);
69
- if (clipDuration <= 0) break;
70
 
71
- const idx = sectionVideoStart + j;
72
- videoFilters.push(`[${idx}:v]trim=duration=${clipDuration},setpts=PTS-STARTPTS[v${i}_${j}]`);
73
- totalVideoDuration += clipDuration;
 
 
74
 
75
- if (totalVideoDuration >= audioDuration) break;
76
- }
77
 
78
- // Concatenate trimmed videos for this section
79
- const videoCount = videoFilters.length;
80
- if (filterComplex) filterComplex += ';';
81
- filterComplex += videoFilters.join(';');
82
 
83
- if (videoCount > 1) {
84
- filterComplex += `;${videoFilters.map((_, idx) => `[v${i}_${idx}]`).join('')}concat=n=${videoCount}:v=1:a=0[vconcat${i}]`;
85
- } else {
86
- filterComplex += `;[v${i}_0]copy[vconcat${i}]`;
87
- }
88
 
89
- // Apply subtitles for this section
90
- const escapedCaptionFile = captionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
91
- filterComplex += `;[vconcat${i}]subtitles='${escapedCaptionFile}'[vsub${i}]`;
92
 
93
- // Combine video with its audio and trim to audio duration
94
- filterComplex += `;[vsub${i}][${audioIndex}:a]concat=n=1:v=1:a=1[vout${i}][aout${i}]`;
 
95
 
96
- sectionOutputs.push(`[vout${i}][aout${i}]`);
97
- }
98
 
99
- // Concatenate all sections
100
- if (sectionOutputs.length > 1) {
101
- filterComplex += `;${sectionOutputs.join('')}concat=n=${sectionOutputs.length}:v=1:a=1[finalv][finala]`;
102
- } else {
103
- filterComplex += `;[vout0]copy[finalv];[aout0]copy[finala]`;
104
  }
105
 
106
- // Add background music if present
107
- const { bgMusic, bgMusicVolume = 0.25, bgMusicDuration = 3 * 60 } = originalManuscript;
108
- let bgMusicInputIndex = null;
109
- if (bgMusic) {
110
- inputs.push('-i', bgMusic);
111
- bgMusicInputIndex = inputIndex;
112
- inputIndex++;
113
- // Mix bgMusic with finala
114
- filterComplex += `;[${bgMusicInputIndex}:a]volume=${bgMusicVolume}[bgmvol];[finala][bgmvol]amix=inputs=2:duration=first:dropout_transition=2[mixeda]`;
115
- }
116
-
117
- await this.runFFmpeg([
118
  ...inputs,
119
- '-filter_complex', filterComplex,
120
  '-map', '[finalv]',
121
- '-map', bgMusic ? '[mixeda]' : '[finala]',
122
  '-c:v', 'libx264',
123
  '-c:a', 'aac',
124
- '-y',
125
- outFile
126
- ], controller, onLog);
127
 
128
- console.log(`Final output: ${outFile}`);
129
- return outFile;
130
  }
131
 
132
- async runFFmpeg(args, controller, onLog) {
133
- console.log('FFMPEG cmd: ', args.join(' '));
134
- return new Promise((resolve, reject) => {
135
- const ffmpegProcess = spawn(ffmpegLocation, [...args], {
136
- detached: true,
137
- cwd: process.cwd()
138
- });
139
-
140
- if (controller) {
141
- controller.stop = () => {
142
- console.log('Stopping ffmpeg process');
143
- try {
144
- process.kill(-ffmpegProcess.pid, 'SIGKILL');
145
- } catch (e) {
146
- console.error(`Failed to kill process group ${-ffmpegProcess.pid}`, e);
147
- ffmpegProcess.kill('SIGKILL');
148
- }
149
- };
150
- }
151
 
152
- ffmpegProcess.stdout.on('data', (data) => {
153
- const msg = data.toString();
154
- console.log(msg);
155
- if (onLog) onLog(msg);
156
- });
157
 
158
- ffmpegProcess.stderr.on('data', (data) => {
159
- const msg = data.toString();
160
- console.error(msg);
161
- if (onLog) onLog(msg);
162
- });
163
 
164
- ffmpegProcess.on('close', (code) => {
165
- if (code === 0) {
166
- resolve();
167
- } else {
168
- reject(new Error(`FFmpeg process exited with code ${code}`));
169
- }
170
- });
171
 
172
- ffmpegProcess.on('error', (err) => {
173
- reject(err);
174
- });
175
  });
176
  }
177
  }
 
1
+ // FULL FIXED VERSION — eliminates concat=n=1 and all copy filters
2
+
3
  import { spawn } from 'child_process';
4
  import os from 'os';
5
  import path from 'path';
6
  import fs from 'fs';
7
 
8
+ const ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
9
 
10
  export class CaptionRenderer {
11
  validateCaption(originalManuscript) {
 
21
 
22
  async doRender(jobId, originalManuscript, onLog, npmScript, options, controller) {
23
  const outDir = path.join(process.cwd(), 'out');
24
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
 
 
25
 
26
  const outFile = path.join(outDir, `${jobId}_final.mp4`);
27
 
 
 
28
  let inputs = [];
29
+ let filter = [];
30
  let inputIndex = 0;
31
+ let videoLabels = [];
32
+ let audioLabels = [];
33
 
34
+ originalManuscript.transcript.forEach((section, i) => {
35
+ const media = section.mediaAbsPaths || [];
36
+ if (!media.length) return;
 
 
 
 
 
 
 
 
 
37
 
38
+ const audioIndex = inputIndex + media.length;
39
+
40
+ media.forEach(m => {
41
+ inputs.push('-i', m.path);
42
  inputIndex++;
43
+ });
44
 
45
+ inputs.push('-i', section.audioFullPath);
 
 
46
  inputIndex++;
47
 
48
+ let clips = [];
49
+ let elapsed = 0;
 
50
 
51
+ media.forEach((m, j) => {
52
+ let d = m.prompt?.durationSec || m.durationSec;
53
+ d = Math.min(d, section.durationInSeconds - elapsed);
54
+ if (d <= 0) return;
 
 
 
55
 
56
+ const label = `v${i}_${j}`;
57
+ filter.push(`[${inputIndex - media.length - 1 + j}:v]trim=duration=${d},setpts=PTS-STARTPTS[${label}]`);
58
+ clips.push(`[${label}]`);
59
+ elapsed += d;
60
+ });
61
 
62
+ const vcat = `vc${i}`;
63
+ filter.push(`${clips.join('')}concat=n=${clips.length}:v=1:a=0[${vcat}]`);
64
 
65
+ const sub = `vs${i}`;
66
+ const esc = section.audioCaptionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
67
+ filter.push(`[${vcat}]subtitles='${esc}'[${sub}]`);
 
68
 
69
+ const vout = `vout${i}`;
70
+ const aout = `aout${i}`;
 
 
 
71
 
72
+ filter.push(`[${sub}]null[${vout}]`);
73
+ filter.push(`[${audioIndex}:a]anull[${aout}]`);
 
74
 
75
+ videoLabels.push(`[${vout}]`);
76
+ audioLabels.push(`[${aout}]`);
77
+ });
78
 
79
+ filter.push(`${videoLabels.join('')}concat=n=${videoLabels.length}:v=1:a=0[finalv]`);
80
+ filter.push(`${audioLabels.join('')}concat=n=${audioLabels.length}:v=0:a=1[finala]`);
81
 
82
+ if (originalManuscript.bgMusic) {
83
+ inputs.push('-i', originalManuscript.bgMusic);
84
+ filter.push(`[${inputIndex}:a]volume=${originalManuscript.bgMusicVolume || 0.25}[bgm]`);
85
+ filter.push(`[finala][bgm]amix=inputs=2:duration=first:dropout_transition=2[mixeda]`);
 
86
  }
87
 
88
+ const args = [
 
 
 
 
 
 
 
 
 
 
 
89
  ...inputs,
90
+ '-filter_complex', filter.join(';'),
91
  '-map', '[finalv]',
92
+ '-map', originalManuscript.bgMusic ? '[mixeda]' : '[finala]',
93
  '-c:v', 'libx264',
94
  '-c:a', 'aac',
95
+ '-y', outFile
96
+ ];
 
97
 
98
+ return this.runFFmpeg(args, controller, onLog);
 
99
  }
100
 
101
+ runFFmpeg(args, controller, onLog) {
102
+ console.log('FFMPEG cmd:', args.join(' '));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
+ return new Promise((resolve, reject) => {
105
+ const p = spawn(ffmpegLocation, args, { detached: true });
 
 
 
106
 
107
+ if (controller) controller.stop = () => p.kill('SIGKILL');
 
 
 
 
108
 
109
+ p.stderr.on('data', d => onLog && onLog(d.toString()));
110
+ p.stdout.on('data', d => onLog && onLog(d.toString()));
 
 
 
 
 
111
 
112
+ p.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}`)));
113
+ p.on('error', reject);
 
114
  });
115
  }
116
  }