Spaces:
Running
Running
Commit ·
23a704f
1
Parent(s): 2887549
Render with bubbles working
Browse files- utils/AvatarRender.js +34 -4
- utils/bubble/Bubble.js +3 -3
- utils/bubble/bubble-templates.js +1 -1
utils/AvatarRender.js
CHANGED
|
@@ -2,6 +2,7 @@ import { spawn } from 'child_process';
|
|
| 2 |
import os from 'os';
|
| 3 |
import path from 'path';
|
| 4 |
import fs from 'fs';
|
|
|
|
| 5 |
|
| 6 |
const ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
| 7 |
|
|
@@ -61,6 +62,7 @@ export class AvatarRenderer {
|
|
| 61 |
|
| 62 |
// Burn captions for this section
|
| 63 |
const captionFile = section.audioCaptionFile;
|
|
|
|
| 64 |
if (captionFile && captionFile.endsWith('.ass')) {
|
| 65 |
const subbedFile = path.join(outDir, `${jobId}_section${i}_subbed.mp4`);
|
| 66 |
const esc = captionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
|
|
@@ -73,9 +75,25 @@ export class AvatarRenderer {
|
|
| 73 |
onLog && onLog(`Burning captions for section ${i}...\n`);
|
| 74 |
await this.runFFmpeg(subArgs, controller, onLog);
|
| 75 |
tempFiles.push(subbedFile);
|
| 76 |
-
|
| 77 |
} else {
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
}
|
| 80 |
}
|
| 81 |
|
|
@@ -128,9 +146,21 @@ export class AvatarRenderer {
|
|
| 128 |
}
|
| 129 |
}
|
| 130 |
|
| 131 |
-
// Cleanup intermediate files
|
|
|
|
| 132 |
for (const f of tempFiles) {
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
}
|
| 135 |
|
| 136 |
return resultFile;
|
|
|
|
| 2 |
import os from 'os';
|
| 3 |
import path from 'path';
|
| 4 |
import fs from 'fs';
|
| 5 |
+
import Bubble from './bubble/Bubble.js';
|
| 6 |
|
| 7 |
const ffmpegLocation = os.platform() === 'win32' ? 'ffmpeg.exe' : 'ffmpeg';
|
| 8 |
|
|
|
|
| 62 |
|
| 63 |
// Burn captions for this section
|
| 64 |
const captionFile = section.audioCaptionFile;
|
| 65 |
+
let sourceForBubbles;
|
| 66 |
if (captionFile && captionFile.endsWith('.ass')) {
|
| 67 |
const subbedFile = path.join(outDir, `${jobId}_section${i}_subbed.mp4`);
|
| 68 |
const esc = captionFile.replace(/\\/g, '/').replace(/:/g, '\\:');
|
|
|
|
| 75 |
onLog && onLog(`Burning captions for section ${i}...\n`);
|
| 76 |
await this.runFFmpeg(subArgs, controller, onLog);
|
| 77 |
tempFiles.push(subbedFile);
|
| 78 |
+
sourceForBubbles = subbedFile;
|
| 79 |
} else {
|
| 80 |
+
sourceForBubbles = sectionVideo;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
// Apply bubbles if present on this section
|
| 84 |
+
if (section.bubbles && Array.isArray(section.bubbles) && section.bubbles.length) {
|
| 85 |
+
const bubbledFile = path.join(outDir, `${jobId}_section${i}_bubbled.mp4`);
|
| 86 |
+
try {
|
| 87 |
+
onLog && onLog(`Applying ${section.bubbles.length} bubble(s) to section ${i}...\n`);
|
| 88 |
+
await Bubble.makeBubble(sourceForBubbles, section.bubbles, bubbledFile, onLog);
|
| 89 |
+
tempFiles.push(bubbledFile);
|
| 90 |
+
sectionCaptionedVideos.push(bubbledFile);
|
| 91 |
+
} catch (e) {
|
| 92 |
+
onLog && onLog(`Bubble application failed for section ${i}, using original: ${e}\n`);
|
| 93 |
+
sectionCaptionedVideos.push(sourceForBubbles);
|
| 94 |
+
}
|
| 95 |
+
} else {
|
| 96 |
+
sectionCaptionedVideos.push(sourceForBubbles);
|
| 97 |
}
|
| 98 |
}
|
| 99 |
|
|
|
|
| 146 |
}
|
| 147 |
}
|
| 148 |
|
| 149 |
+
// Cleanup intermediate files — keep only the final resultFile and the manuscript JSON
|
| 150 |
+
const manuscriptPath = path.join(process.cwd(), 'public', 'original_manuscript.json');
|
| 151 |
for (const f of tempFiles) {
|
| 152 |
+
if (!f) continue;
|
| 153 |
+
try {
|
| 154 |
+
const abs = path.resolve(f);
|
| 155 |
+
// never delete the final result file or the manuscript file
|
| 156 |
+
if (abs === path.resolve(resultFile)) continue;
|
| 157 |
+
if (abs === path.resolve(manuscriptPath)) continue;
|
| 158 |
+
if (fs.existsSync(abs)) {
|
| 159 |
+
try { fs.unlinkSync(abs); onLog && onLog(`Deleted temp file: ${abs}\n`); } catch (e) { onLog && onLog(`Failed to delete temp file ${abs}: ${e}\n`); }
|
| 160 |
+
}
|
| 161 |
+
} catch (e) {
|
| 162 |
+
onLog && onLog(`Error while cleaning temp file ${f}: ${e}\n`);
|
| 163 |
+
}
|
| 164 |
}
|
| 165 |
|
| 166 |
return resultFile;
|
utils/bubble/Bubble.js
CHANGED
|
@@ -553,9 +553,9 @@ export async function test() {
|
|
| 553 |
toSec: 5.0
|
| 554 |
};
|
| 555 |
|
| 556 |
-
// Additional sample: simple-top-center-
|
| 557 |
const imageSample = {
|
| 558 |
-
templateName: 'simple-top-center-
|
| 559 |
// provide mediaAbsPath (what makeBubble expects) pointing to an image
|
| 560 |
mediaAbsPath: path.join(cwd, 'public', 'media2.png'),
|
| 561 |
bubbleText: { text: 'IMAGE ABOVE, TEXT CENTERED' },
|
|
@@ -614,4 +614,4 @@ export async function test() {
|
|
| 614 |
}
|
| 615 |
}
|
| 616 |
|
| 617 |
-
test()
|
|
|
|
| 553 |
toSec: 5.0
|
| 554 |
};
|
| 555 |
|
| 556 |
+
// Additional sample: simple-top-center-media uses an image or video asset centered at top with text
|
| 557 |
const imageSample = {
|
| 558 |
+
templateName: 'simple-top-center-media',
|
| 559 |
// provide mediaAbsPath (what makeBubble expects) pointing to an image
|
| 560 |
mediaAbsPath: path.join(cwd, 'public', 'media2.png'),
|
| 561 |
bubbleText: { text: 'IMAGE ABOVE, TEXT CENTERED' },
|
|
|
|
| 614 |
}
|
| 615 |
}
|
| 616 |
|
| 617 |
+
// test()
|
utils/bubble/bubble-templates.js
CHANGED
|
@@ -26,7 +26,7 @@ export const BaseBubbleTemplates = {
|
|
| 26 |
// image-only variant: the caller must still provide the path when using this
|
| 27 |
// template. animation defaults are pulled up to the top level so they survive
|
| 28 |
// the case where the user specifies `mediaAbsPath` as a simple string.
|
| 29 |
-
'simple-top-center-
|
| 30 |
borderRadius: 20,
|
| 31 |
normalizeAudio: true,
|
| 32 |
audioEffectFile: 'woosh',
|
|
|
|
| 26 |
// image-only variant: the caller must still provide the path when using this
|
| 27 |
// template. animation defaults are pulled up to the top level so they survive
|
| 28 |
// the case where the user specifies `mediaAbsPath` as a simple string.
|
| 29 |
+
'simple-top-center-media': {
|
| 30 |
borderRadius: 20,
|
| 31 |
normalizeAudio: true,
|
| 32 |
audioEffectFile: 'woosh',
|