Spaces:
Running
Running
Commit ·
4021252
1
Parent(s): 8b0803b
WUP
Browse files- server-plugins/apply.js +2 -0
- server-plugins/crop.js +0 -3
- server-plugins/overlay.js +98 -0
- server-plugins/plugin.js +5 -0
server-plugins/apply.js
CHANGED
|
@@ -6,6 +6,7 @@ import { CaptionPlugin } from './generate-captions.js';
|
|
| 6 |
import { CropPlugin } from './crop.js';
|
| 7 |
import { FlattenPathsPlugin } from './flatten-paths.js';
|
| 8 |
import { SpeedPlugin } from './speed.js';
|
|
|
|
| 9 |
|
| 10 |
const pluginRegistry = {
|
| 11 |
'split-media-render': SplitRenderPlugin,
|
|
@@ -14,6 +15,7 @@ const pluginRegistry = {
|
|
| 14 |
'crop': CropPlugin,
|
| 15 |
'flatten-paths': FlattenPathsPlugin,
|
| 16 |
'speed': SpeedPlugin,
|
|
|
|
| 17 |
};
|
| 18 |
|
| 19 |
export async function applyPluginsPrerender(plugins, originalManuscript, originalManuscriptPath, jobId) {
|
|
|
|
| 6 |
import { CropPlugin } from './crop.js';
|
| 7 |
import { FlattenPathsPlugin } from './flatten-paths.js';
|
| 8 |
import { SpeedPlugin } from './speed.js';
|
| 9 |
+
import { OverlayPlugin } from './overlay.js';
|
| 10 |
|
| 11 |
const pluginRegistry = {
|
| 12 |
'split-media-render': SplitRenderPlugin,
|
|
|
|
| 15 |
'crop': CropPlugin,
|
| 16 |
'flatten-paths': FlattenPathsPlugin,
|
| 17 |
'speed': SpeedPlugin,
|
| 18 |
+
'overlay': OverlayPlugin,
|
| 19 |
};
|
| 20 |
|
| 21 |
export async function applyPluginsPrerender(plugins, originalManuscript, originalManuscriptPath, jobId) {
|
server-plugins/crop.js
CHANGED
|
@@ -8,9 +8,6 @@ export class CropPlugin extends Plugin {
|
|
| 8 |
super(name, options);
|
| 9 |
}
|
| 10 |
|
| 11 |
-
mediaPathFlatten(mediaPath) {
|
| 12 |
-
return path.join('public', path.basename(mediaPath));
|
| 13 |
-
}
|
| 14 |
|
| 15 |
async applyPrerender(originalManuscript, jobId) {
|
| 16 |
const transcript = originalManuscript.transcript || [];
|
|
|
|
| 8 |
super(name, options);
|
| 9 |
}
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
async applyPrerender(originalManuscript, jobId) {
|
| 13 |
const transcript = originalManuscript.transcript || [];
|
server-plugins/overlay.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import path from 'path';
|
| 2 |
+
import fs from 'fs';
|
| 3 |
+
import { Plugin } from './plugin.js';
|
| 4 |
+
import { FFMpegUtils } from 'common-utils';
|
| 5 |
+
|
| 6 |
+
export class OverlayPlugin extends Plugin {
|
| 7 |
+
constructor(name, options) {
|
| 8 |
+
super(name, options);
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
async applyPrerender(originalManuscript, jobId) {
|
| 12 |
+
const transcript = originalManuscript.transcript || [];
|
| 13 |
+
const overlayOpt = this.options.overlay;
|
| 14 |
+
|
| 15 |
+
if (!overlayOpt) {
|
| 16 |
+
this.log('No overlay option provided, skipping overlay plugin.');
|
| 17 |
+
return;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
let overlayPath = null;
|
| 21 |
+
// if option looks like a file path
|
| 22 |
+
if (/\.(mp4|webm)$/i.test(overlayOpt)) {
|
| 23 |
+
const flattened = this.mediaPathFlatten(overlayOpt);
|
| 24 |
+
if (fs.existsSync(flattened)) overlayPath = flattened;
|
| 25 |
+
else if (fs.existsSync(overlayOpt)) overlayPath = overlayOpt;
|
| 26 |
+
else {
|
| 27 |
+
this.log(`Overlay path does not exist: ${overlayOpt}`);
|
| 28 |
+
return;
|
| 29 |
+
}
|
| 30 |
+
} else {
|
| 31 |
+
// look inside public/assets/effects for mp4 or webm
|
| 32 |
+
const effectsDir = path.join('public', 'assets', 'effects');
|
| 33 |
+
const mp4Path = path.join(effectsDir, `${overlayOpt}.mp4`);
|
| 34 |
+
const webmPath = path.join(effectsDir, `${overlayOpt}.webm`);
|
| 35 |
+
if (fs.existsSync(mp4Path)) overlayPath = mp4Path;
|
| 36 |
+
else if (fs.existsSync(webmPath)) overlayPath = webmPath;
|
| 37 |
+
else {
|
| 38 |
+
this.log(`No overlay found for '${overlayOpt}' in ${effectsDir}`);
|
| 39 |
+
return;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
const overlayExt = path.extname(overlayPath).toLowerCase();
|
| 44 |
+
this.log(`Using overlay: ${overlayPath}`);
|
| 45 |
+
|
| 46 |
+
for (let item of transcript) {
|
| 47 |
+
if (!item.mediaAbsPaths || !item.mediaAbsPaths.length) continue;
|
| 48 |
+
for (let mediaObj of item.mediaAbsPaths) {
|
| 49 |
+
try {
|
| 50 |
+
let mediaPath = mediaObj.path;
|
| 51 |
+
if (!mediaPath || !fs.existsSync(mediaPath)) {
|
| 52 |
+
const flattenedPath = this.mediaPathFlatten(mediaPath);
|
| 53 |
+
if (fs.existsSync(flattenedPath)) {
|
| 54 |
+
mediaObj.path = flattenedPath;
|
| 55 |
+
mediaPath = flattenedPath;
|
| 56 |
+
this.log(`Using flattened media path: ${flattenedPath}`);
|
| 57 |
+
} else {
|
| 58 |
+
this.log(`Media path does not exist: ${mediaPath}. Tried flattened: ${flattenedPath}`);
|
| 59 |
+
continue;
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const meta = await FFMpegUtils.getMediaMetadata(mediaPath);
|
| 64 |
+
if (!meta || !meta.video || !meta.video.width || !meta.video.height) {
|
| 65 |
+
this.log(`No video stream found for ${mediaPath}`);
|
| 66 |
+
continue;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
const srcW = parseInt(meta.video.width);
|
| 70 |
+
const srcH = parseInt(meta.video.height);
|
| 71 |
+
|
| 72 |
+
const ext = path.extname(mediaPath);
|
| 73 |
+
const base = path.basename(mediaPath, ext);
|
| 74 |
+
const outPath = path.join(path.dirname(mediaPath), `${base}.overlay${ext}`);
|
| 75 |
+
|
| 76 |
+
let cmd = null;
|
| 77 |
+
|
| 78 |
+
if (overlayExt === '.mp4') {
|
| 79 |
+
// mp4 uses green-screen (chroma key) -> remove green then overlay
|
| 80 |
+
// chromakey params: color (green), similarity, blend
|
| 81 |
+
cmd = `ffmpeg -i "${mediaPath}" -i "${overlayPath}" -filter_complex "[1:v]scale=${srcW}:${srcH},chromakey=0x00FF00:0.3:0.1[ovr];[0:v][ovr]overlay=0:0:format=auto,format=yuv420p" -c:v libx264 -preset veryfast -crf 23 -c:a copy -shortest "${outPath}" -y`;
|
| 82 |
+
} else {
|
| 83 |
+
// webm assumed to have alpha (transparent video)
|
| 84 |
+
cmd = `ffmpeg -i "${mediaPath}" -i "${overlayPath}" -filter_complex "[1:v]scale=${srcW}:${srcH}[ovr];[0:v][ovr]overlay=0:0:format=auto,format=yuv420p" -c:v libx264 -preset veryfast -crf 23 -c:a copy -shortest "${outPath}" -y`;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
this.log(`Applying overlay ${overlayPath} -> ${outPath} on ${mediaPath}`);
|
| 88 |
+
await FFMpegUtils.execute(cmd);
|
| 89 |
+
|
| 90 |
+
mediaObj._originalPath = mediaObj.path;
|
| 91 |
+
mediaObj.path = outPath;
|
| 92 |
+
} catch (err) {
|
| 93 |
+
this.log(`Error applying overlay to ${mediaObj?.path}: ${err}`);
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
}
|
| 98 |
+
}
|
server-plugins/plugin.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
|
|
| 1 |
|
| 2 |
export class Plugin {
|
| 3 |
constructor(name, options) {
|
|
@@ -15,5 +16,9 @@ export class Plugin {
|
|
| 15 |
log(...args) {
|
| 16 |
console.log(`[Plugin: ${this.name}]`, ...args);
|
| 17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
}
|
| 19 |
|
|
|
|
| 1 |
+
import path from "path";
|
| 2 |
|
| 3 |
export class Plugin {
|
| 4 |
constructor(name, options) {
|
|
|
|
| 16 |
log(...args) {
|
| 17 |
console.log(`[Plugin: ${this.name}]`, ...args);
|
| 18 |
}
|
| 19 |
+
|
| 20 |
+
mediaPathFlatten(mediaPath) {
|
| 21 |
+
return path.join('public', path.basename(mediaPath));
|
| 22 |
+
}
|
| 23 |
}
|
| 24 |
|