shiveshnavin commited on
Commit
3ec2e41
·
1 Parent(s): 8dd5bba

Optimize caption render into single command

Browse files
Files changed (1) hide show
  1. utils/CaptionRender.js +48 -65
utils/CaptionRender.js CHANGED
@@ -23,9 +23,15 @@ export class CaptionRenderer {
23
  fs.mkdirSync(outDir, { recursive: true });
24
  }
25
 
26
- const chunkFiles = [];
 
 
 
 
 
 
27
 
28
- // Process each transcript section
29
  for (let i = 0; i < originalManuscript.transcript.length; i++) {
30
  const section = originalManuscript.transcript[i];
31
  const audioDuration = section.durationInSeconds;
@@ -33,27 +39,24 @@ export class CaptionRenderer {
33
  const captionFile = section.audioCaptionFile;
34
  const mediaFiles = section.mediaAbsPaths || [];
35
 
36
- const chunkOutput = path.join(outDir, `chunk_${i}.mp4`);
37
-
38
  if (mediaFiles.length === 0) {
39
  console.log(`Skipping section ${i}: no media files`);
40
  continue;
41
  }
42
 
43
- // Build filter_complex for this section
44
- let filterComplex = '';
45
- let inputs = [];
46
-
47
- // Add all media files as inputs
48
  for (let j = 0; j < mediaFiles.length; j++) {
49
  inputs.push('-i', mediaFiles[j].path);
 
50
  }
51
 
52
- // Add audio input
53
  inputs.push('-i', audioPath);
54
- const audioIndex = mediaFiles.length;
 
55
 
56
- // Trim and concatenate video clips to match audio duration
57
  let videoFilters = [];
58
  let totalVideoDuration = 0;
59
 
@@ -61,73 +64,53 @@ export class CaptionRenderer {
61
  const clipDuration = Math.min(mediaFiles[j].durationSec, audioDuration - totalVideoDuration);
62
  if (clipDuration <= 0) break;
63
 
64
- videoFilters.push(`[${j}:v]trim=duration=${clipDuration},setpts=PTS-STARTPTS[v${j}]`);
 
65
  totalVideoDuration += clipDuration;
66
 
67
  if (totalVideoDuration >= audioDuration) break;
68
  }
69
 
70
- // Concatenate trimmed videos
71
  const videoCount = videoFilters.length;
72
- filterComplex = videoFilters.join(';') + ';';
73
- filterComplex += videoFilters.map((_, idx) => `[v${idx}]`).join('') + `concat=n=${videoCount}:v=1:a=0[vconcat]`;
74
-
75
- // Apply subtitles
76
- filterComplex += `;[vconcat]subtitles=${captionFile.replace(/\\/g, '/')}[vout]`;
77
-
78
- // Execute ffmpeg for this chunk
79
- await this.runFFmpeg([
80
- ...inputs,
81
- '-filter_complex', filterComplex,
82
- '-map', '[vout]',
83
- '-map', `${audioIndex}:a`,
84
- '-t', audioDuration.toString(),
85
- '-c:v', 'libx264',
86
- '-c:a', 'aac',
87
- '-y',
88
- chunkOutput
89
- ], controller, onLog);
90
-
91
- chunkFiles.push(chunkOutput);
92
- console.log(`Completed chunk ${i}: ${chunkOutput}`);
93
  }
94
 
95
- // Concatenate all chunks
96
- const outFile = path.join(outDir, `${jobId}_final.mp4`);
97
- const concatListPath = path.join(outDir, 'concat_list.txt');
98
- fs.writeFileSync(concatListPath, chunkFiles.map(f => `file '${path.basename(f)}'`).join('\n'));
 
 
99
 
 
100
  await this.runFFmpeg([
101
- '-f', 'concat',
102
- '-safe', '0',
103
- '-i', concatListPath,
104
- '-c', 'copy',
 
 
105
  '-y',
106
  outFile
107
  ], controller, onLog);
108
 
109
- // Clean up intermediate files
110
- console.log('Cleaning up intermediate files...');
111
- for (const chunkFile of chunkFiles) {
112
- try {
113
- if (fs.existsSync(chunkFile)) {
114
- fs.unlinkSync(chunkFile);
115
- console.log(`Deleted: ${chunkFile}`);
116
- }
117
- } catch (err) {
118
- console.error(`Failed to delete ${chunkFile}:`, err);
119
- }
120
- }
121
-
122
- try {
123
- if (fs.existsSync(concatListPath)) {
124
- fs.unlinkSync(concatListPath);
125
- console.log(`Deleted: ${concatListPath}`);
126
- }
127
- } catch (err) {
128
- console.error(`Failed to delete ${concatListPath}:`, err);
129
- }
130
-
131
  console.log(`Final output: ${outFile}`);
132
  return outFile;
133
  }
 
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;
 
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
 
 
64
  const clipDuration = Math.min(mediaFiles[j].durationSec, audioDuration - totalVideoDuration);
65
  if (clipDuration <= 0) break;
66
 
67
+ const idx = sectionVideoStart + j;
68
+ videoFilters.push(`[${idx}:v]trim=duration=${clipDuration},setpts=PTS-STARTPTS[v${i}_${j}]`);
69
  totalVideoDuration += clipDuration;
70
 
71
  if (totalVideoDuration >= audioDuration) break;
72
  }
73
 
74
+ // Concatenate trimmed videos for this section
75
  const videoCount = videoFilters.length;
76
+ if (filterComplex) filterComplex += ';';
77
+ filterComplex += videoFilters.join(';');
78
+
79
+ if (videoCount > 1) {
80
+ filterComplex += `;${videoFilters.map((_, idx) => `[v${i}_${idx}]`).join('')}concat=n=${videoCount}:v=1:a=0[vconcat${i}]`;
81
+ } else {
82
+ filterComplex += `;[v${i}_0]copy[vconcat${i}]`;
83
+ }
84
+
85
+ // Apply subtitles for this section
86
+ const escapedCaptionFile = captionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
87
+ filterComplex += `;[vconcat${i}]subtitles='${escapedCaptionFile}'[vsub${i}]`;
88
+
89
+ // Combine video with its audio and trim to audio duration
90
+ filterComplex += `;[vsub${i}][${audioIndex}:a]concat=n=1:v=1:a=1[vout${i}][aout${i}]`;
91
+
92
+ sectionOutputs.push(`[vout${i}][aout${i}]`);
 
 
 
 
93
  }
94
 
95
+ // Concatenate all sections
96
+ if (sectionOutputs.length > 1) {
97
+ filterComplex += `;${sectionOutputs.join('')}concat=n=${sectionOutputs.length}:v=1:a=1[finalv][finala]`;
98
+ } else {
99
+ filterComplex += `;[vout0]copy[finalv];[aout0]copy[finala]`;
100
+ }
101
 
102
+ // Execute single ffmpeg command
103
  await this.runFFmpeg([
104
+ ...inputs,
105
+ '-filter_complex', filterComplex,
106
+ '-map', '[finalv]',
107
+ '-map', '[finala]',
108
+ '-c:v', 'libx264',
109
+ '-c:a', 'aac',
110
  '-y',
111
  outFile
112
  ], controller, onLog);
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  console.log(`Final output: ${outFile}`);
115
  return outFile;
116
  }