Spaces:
Sleeping
Sleeping
Commit
·
3ec2e41
1
Parent(s):
8dd5bba
Optimize caption render into single command
Browse files- 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 44 |
-
|
| 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 =
|
|
|
|
| 55 |
|
| 56 |
-
//
|
| 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 |
-
|
|
|
|
| 65 |
totalVideoDuration += clipDuration;
|
| 66 |
|
| 67 |
if (totalVideoDuration >= audioDuration) break;
|
| 68 |
}
|
| 69 |
|
| 70 |
-
// Concatenate trimmed videos
|
| 71 |
const videoCount = videoFilters.length;
|
| 72 |
-
|
| 73 |
-
filterComplex += videoFilters.
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
], controller, onLog);
|
| 90 |
-
|
| 91 |
-
chunkFiles.push(chunkOutput);
|
| 92 |
-
console.log(`Completed chunk ${i}: ${chunkOutput}`);
|
| 93 |
}
|
| 94 |
|
| 95 |
-
// Concatenate all
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
|
|
|
|
|
|
| 99 |
|
|
|
|
| 100 |
await this.runFFmpeg([
|
| 101 |
-
|
| 102 |
-
'-
|
| 103 |
-
'-
|
| 104 |
-
'-
|
|
|
|
|
|
|
| 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 |
}
|