Spaces:
Running
Running
File size: 5,744 Bytes
7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba ad6b549 3ec2e41 7504977 ad6b549 8dd5bba 7ad7585 ad6b549 7ad7585 3ec2e41 7ad7585 8dd5bba 7ad7585 3ec2e41 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 3ec2e41 7ad7585 3ec2e41 7ad7585 ad6b549 3ec2e41 ad6b549 7504977 ad6b549 8dd5bba ad6b549 8dd5bba ad6b549 7ad7585 ad6b549 7504977 bdfc7ce 7504977 3dcaad7 7504977 8dd5bba 7504977 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba 7ad7585 8dd5bba |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
// FULL FIXED VERSION — eliminates concat=n=1 and all copy filters
import { spawn } from 'child_process';
import os from 'os';
import path from 'path';
import fs from 'fs';
const ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
export class CaptionRenderer {
validateCaption(originalManuscript) {
let captionFiles = originalManuscript.transcript.map(item => item.audioCaptionFile);
// make sure the caption files are in ass format and exist
for (let captionFile of captionFiles) {
if (!captionFile || !captionFile.endsWith('.ass')) {
throw new Error('Invalid caption file format. Expected .ass files for item ' + (captionFiles.indexOf(captionFile) + 1) + '. Did you forget to use `caption` plugin?');
}
}
return true;
}
async doRender(jobId, originalManuscript, onLog, npmScript, options, controller) {
const outDir = path.join(process.cwd(), 'out');
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
const segmentFiles = [];
// Helper to clean up intermediate files
function cleanupFiles(files) {
for (const f of files) {
try { fs.unlinkSync(f); } catch (e) { /* ignore */ }
}
}
// Render each transcript section separately
for (const [i, section] of originalManuscript.transcript.entries()) {
const segmentFile = path.join(outDir, `${jobId}_segment_${i}.mp4`);
const inputs = [];
const filter = [];
let inputIndex = 0;
const media = section.mediaAbsPaths || [];
if (!media.length) continue;
media.forEach(m => {
inputs.push('-i', m.path);
inputIndex++;
});
inputs.push('-i', section.audioFullPath);
inputIndex++;
let clips = [];
let elapsed = 0;
media.forEach((m, j) => {
let d = m.prompt?.durationSec || m.durationSec;
d = Math.min(d, section.durationInSeconds - elapsed);
if (d <= 0) return;
const label = `v${i}_${j}`;
filter.push(`[${inputIndex - media.length - 1 + j}:v]trim=duration=${d},setpts=PTS-STARTPTS[${label}]`);
clips.push(`[${label}]`);
elapsed += d;
});
const vcat = `vc${i}`;
filter.push(`${clips.join('')}concat=n=${clips.length}:v=1:a=0[${vcat}]`);
const sub = `vs${i}`;
const esc = section.audioCaptionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
filter.push(`[${vcat}]subtitles='${esc}'[${sub}]`);
const vout = `vout${i}`;
const aout = `aout${i}`;
filter.push(`[${sub}]null[${vout}]`);
filter.push(`[${inputIndex - 1}:a]anull[${aout}]`);
const args = [
...inputs,
'-filter_complex', filter.join(';'),
'-map', `[${vout}]`,
'-map', `[${aout}]`,
'-c:v', 'libx264',
'-c:a', 'aac',
'-y', segmentFile
];
await this.runFFmpeg(args, controller, onLog);
segmentFiles.push(segmentFile);
}
// Concatenate all segments together
const resultFile = path.join(outDir, `${jobId}_final.mp4`);
const concatFile = path.join(outDir, `${jobId}_combined_final.mp4`);
const concatList = path.join(outDir, `${jobId}_concat_list.txt`);
fs.writeFileSync(concatList, segmentFiles.map(f => `file '${f}'`).join('\n'));
const concatArgs = [
'-f', 'concat',
'-safe', '0',
'-i', concatList,
'-c', 'copy',
'-y', concatFile
];
await this.runFFmpeg(concatArgs, controller, onLog);
// Add background music if present
const { bgMusic, bgMusicVolume = 0.25 } = originalManuscript;
if (bgMusic) {
const finalWithMusic = resultFile; // Overwrite the _final.mp4 file
const getDuration = (filePath) => {
try {
const ffprobeSync = require('child_process').spawnSync;
const result = ffprobeSync(ffmpegLocation.replace('ffmpeg', 'ffprobe'), [
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
filePath
]);
return parseFloat(result.stdout.toString().trim());
} catch (e) {
return null;
}
};
const videoDuration = getDuration(concatFile);
const atrimArg = (videoDuration && !isNaN(videoDuration)) ? `,atrim=end=${videoDuration}` : '';
const voiceVolume = options?.voiceVolume || 1.5;
const musicArgs = [
'-i', concatFile,
'-i', bgMusic,
'-filter_complex', `[0:a]volume=${voiceVolume}[voice];[1:a]volume=${bgMusicVolume}${atrimArg}[bgm];[voice][bgm]amix=inputs=2:duration=first:dropout_transition=2:normalize=0[mixeda]`,
'-map', '0:v',
'-map', '[mixeda]',
'-c:v', 'copy',
'-c:a', 'aac',
'-y', finalWithMusic
];
await this.runFFmpeg(musicArgs, controller, onLog);
segmentFiles.push(concatFile);
}
else {
fs.renameSync(concatFile, resultFile);
}
cleanupFiles([...segmentFiles, concatList]);
return resultFile;
}
runFFmpeg(args, controller, onLog) {
console.log('FFMPEG cmd:', args.join(' '));
return new Promise((resolve, reject) => {
const p = spawn(ffmpegLocation, args, { detached: true });
if (controller) controller.stop = () => p.kill('SIGKILL');
p.stderr.on('data', d => onLog && onLog(d.toString()));
p.stdout.on('data', d => onLog && onLog(d.toString()));
p.on('close', code => code === 0 ? resolve() : reject(new Error(`ffmpeg exited ${code}`)));
p.on('error', reject);
});
}
} |