Spaces:
Running
Running
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { Plugin } from './plugin.js'; | |
| import { FFMpegUtils } from 'common-utils'; | |
| export class OverlayPlugin extends Plugin { | |
| constructor(name, options) { | |
| super(name, options); | |
| } | |
| async applyPrerender(originalManuscript, jobId) { | |
| const transcript = originalManuscript.transcript || []; | |
| const overlayOpt = this.options.overlay; | |
| if (!overlayOpt) { | |
| this.log('No overlay option provided, skipping overlay plugin.'); | |
| return; | |
| } | |
| let overlayPath = null; | |
| // if option looks like a file path | |
| if (/\.(mp4|webm|png)$/i.test(overlayOpt)) { | |
| const flattened = this.mediaPathFlatten(overlayOpt); | |
| if (fs.existsSync(flattened)) overlayPath = flattened; | |
| else if (fs.existsSync(overlayOpt)) overlayPath = overlayOpt; | |
| else { | |
| this.log(`Overlay path does not exist: ${overlayOpt}`); | |
| return; | |
| } | |
| } else { | |
| // look inside public/assets/effects for mp4, webm, or png | |
| const effectsDir = path.join('public', 'assets', 'effects'); | |
| const mp4Path = path.join(effectsDir, `${overlayOpt}.mp4`); | |
| const webmPath = path.join(effectsDir, `${overlayOpt}.webm`); | |
| const pngPath = path.join(effectsDir, `${overlayOpt}.png`); | |
| if (fs.existsSync(mp4Path)) overlayPath = mp4Path; | |
| else if (fs.existsSync(webmPath)) overlayPath = webmPath; | |
| else if (fs.existsSync(pngPath)) overlayPath = pngPath; | |
| else { | |
| this.log(`No overlay found for '${overlayOpt}' in ${effectsDir}`); | |
| return; | |
| } | |
| } | |
| const overlayExt = path.extname(overlayPath).toLowerCase(); | |
| this.log(`Using overlay: ${overlayPath}`); | |
| for (let item of transcript) { | |
| if (!item.mediaAbsPaths || !item.mediaAbsPaths.length) continue; | |
| for (let mediaObj of item.mediaAbsPaths) { | |
| try { | |
| let mediaPath = mediaObj.path; | |
| if (!mediaPath || !fs.existsSync(mediaPath)) { | |
| const flattenedPath = this.mediaPathFlatten(mediaPath); | |
| if (fs.existsSync(flattenedPath)) { | |
| mediaObj.path = flattenedPath; | |
| mediaPath = flattenedPath; | |
| this.log(`Using flattened media path: ${flattenedPath}`); | |
| } else { | |
| this.log(`Media path does not exist: ${mediaPath}. Tried flattened: ${flattenedPath}`); | |
| continue; | |
| } | |
| } | |
| const meta = await FFMpegUtils.getMediaMetadata(mediaPath); | |
| if (!meta || !meta.video || !meta.video.width || !meta.video.height) { | |
| this.log(`No video stream found for ${mediaPath}`); | |
| continue; | |
| } | |
| const srcW = parseInt(meta.video.width); | |
| const srcH = parseInt(meta.video.height); | |
| const srcDuration = parseFloat(meta.duration || meta.format?.duration || 0); | |
| const durationArg = srcDuration > 0 ? `-t ${srcDuration}` : ''; | |
| const ext = path.extname(mediaPath); | |
| const base = path.basename(mediaPath, ext); | |
| const outPath = path.join(path.dirname(mediaPath), `${base}.overlay${ext}`); | |
| let cmd = null; | |
| if (overlayExt === '.mp4') { | |
| // mp4 uses green-screen (chroma key) -> remove green then overlay | |
| // chromakey params: color (green), similarity, blend | |
| cmd = `ffmpeg -i "${mediaPath}" -stream_loop -1 -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 ${durationArg} "${outPath}" -y`; | |
| } else if (overlayExt === '.webm') { | |
| // webm assumed to have alpha (transparent video) and should loop to match source duration | |
| cmd = `ffmpeg -i "${mediaPath}" -stream_loop -1 -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 ${durationArg} "${outPath}" -y`; | |
| } else if (overlayExt === '.png') { | |
| // png is static image overlay, loop to cover full video duration | |
| cmd = `ffmpeg -i "${mediaPath}" -loop 1 -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 ${durationArg} "${outPath}" -y`; | |
| } else { | |
| this.log(`Unsupported overlay extension: ${overlayExt}`); | |
| continue; | |
| } | |
| this.log(`Applying overlay ${overlayPath} -> ${outPath} on ${mediaPath}`); | |
| await FFMpegUtils.execute(cmd); | |
| mediaObj._originalPath = mediaObj.path; | |
| mediaObj.path = outPath; | |
| } catch (err) { | |
| this.log(`Error applying overlay to ${mediaObj?.path}: ${err}`); | |
| } | |
| } | |
| } | |
| } | |
| } | |