Spaces:
Running
Running
| import path from 'path'; | |
| import fs from 'fs'; | |
| import { Plugin } from './plugin.js'; | |
| import { FFMpegUtils } from 'common-utils'; | |
| export class CropPlugin extends Plugin { | |
| constructor(name, options) { | |
| super(name, options); | |
| } | |
| mediaPathFlatten(mediaPath) { | |
| return path.join('public', path.basename(mediaPath)); | |
| } | |
| async applyPrerender(originalManuscript, jobId) { | |
| const transcript = originalManuscript.transcript || []; | |
| const targetWidth = +this.options.width || +this.options.targetWidth || 1080; | |
| const targetHeight = +this.options.height || +this.options.targetHeight || 1920; | |
| const targetAspect = targetWidth / targetHeight; | |
| 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}. Trying at flattened path: ${flattenedPath} failed.`); | |
| 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 srcAspect = srcW / srcH; | |
| let cropW = srcW, cropH = srcH, cropX = 0, cropY = 0; | |
| if (Math.abs(srcAspect - targetAspect) < 0.0001) { | |
| this.log(`Aspect ratio matches target for ${mediaPath}, skipping crop.`); | |
| continue; | |
| } | |
| if (srcAspect > targetAspect) { | |
| // source is too wide -> reduce width, keep height | |
| cropW = Math.round(targetAspect * srcH); | |
| cropX = Math.round((srcW - cropW) / 2); | |
| cropH = srcH; | |
| cropY = 0; | |
| } else { | |
| // source is too tall -> reduce height, keep width | |
| cropH = Math.round(srcW / targetAspect); | |
| cropY = Math.round((srcH - cropH) / 2); | |
| cropW = srcW; | |
| cropX = 0; | |
| } | |
| if (cropW === srcW && cropH === srcH) { | |
| continue; | |
| } | |
| const ext = path.extname(mediaPath); | |
| const base = path.basename(mediaPath, ext); | |
| const outPath = path.join(path.dirname(mediaPath), `${base}.cropped${ext}`); | |
| const cmd = `ffmpeg -i "${mediaPath}" -filter:v "crop=${cropW}:${cropH}:${cropX}:${cropY}" -c:v libx264 -preset veryfast -crf 23 -c:a copy "${outPath}" -y`; | |
| this.log(`Cropping ${mediaPath} -> ${outPath} crop=${cropW}x${cropH}+${cropX}+${cropY}`); | |
| await FFMpegUtils.execute(cmd); | |
| // keep original reference and replace path | |
| mediaObj._originalPath = mediaObj.path; | |
| mediaObj.path = outPath; | |
| } catch (err) { | |
| this.log(`Error cropping media ${mediaObj?.path}: ${err}`); | |
| } | |
| } | |
| } | |
| } | |
| } | |