Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 5,598 Bytes
3165afb dde59e8 3165afb 2ed47da a25b190 2ed47da a25b190 4914f1f 2ed47da a25b190 2ed47da f26d0ef 2ed47da f26d0ef 2ed47da f26d0ef 15f63f6 f26d0ef 059a558 f26d0ef 059a558 f26d0ef 2ed47da dde59e8 f26d0ef 2ed47da f26d0ef cde398b f26d0ef 2ed47da cde398b 565de77 a6123d1 8aa24de 565de77 a6123d1 cde398b 059a558 565de77 cde398b 565de77 cde398b 565de77 059a558 cde398b f26d0ef 8aa24de 2ed47da f26d0ef 2ed47da cde398b f26d0ef 2ed47da f26d0ef 2ed47da |
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 |
import { rm, writeFile, readFile } from "node:fs/promises"
import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import ffmpeg from "fluent-ffmpeg"
import { getRandomDirectory } from "@aitube/io"
/**
* Converts an image in Base64 format to a video encoded in Base64.
*
* @param inputImageInBase64 - The input image encoded in Base64.
* @param outputVideoFormat - Optional. Format of the output video (default is "mp4").
* @param outputVideoDurationInMs - Optional. Duration of the video in milliseconds (default is 1000ms).
* @param codec - Optional. Codec used for video coding. Defaults differ based on `outputVideoFormat`.
* @param width - Optional. Width of the output video.
* @param height - Optional. Height of the output video.
* @param fps - Optional. Frame rate of the output video.
* @param zoomInRatePerSecond - Optional. Zoom-in rate (by default 0.6, or which would zoom by 3% over 5 seconds)
*
* @returns - A promise that resolves to the video as a Base64 encoded string.
*/
export async function imageToVideoBase64({
inputImageInBase64,
outputFilePath,
outputDir,
clearOutputDirAtTheEnd = true,
outputVideoFormat = "mp4",
outputVideoDurationInMs = 1000,
codec = outputVideoFormat === "webm" ? "libvpx-vp9" : "libx264",
width = 1920,
height = 1080,
fps = 25,
zoomInRatePerSecond = 0.02
}: {
inputImageInBase64: string
outputFilePath?: string
outputDir?: string
clearOutputDirAtTheEnd?: boolean
outputVideoFormat?: string
outputVideoDurationInMs?: number
codec?: string
width?: number
height?: number
fps?: number
zoomInRatePerSecond?: number
}): Promise<string> {
outputDir = outputDir || (await getRandomDirectory())
outputDir = outputDir || await getRandomDirectory();
// Decode the Base64 image and write it to a temporary file.
const base64Data = inputImageInBase64.substring(inputImageInBase64.indexOf(',') + 1);
const buffer = Buffer.from(base64Data, 'base64');
const inputImagePath = path.join(outputDir, `${uuidv4()}.png`);
await writeFile(inputImagePath, buffer);
const inputImageDetails = await new Promise<ffmpeg.FfprobeData>((resolve, reject) => {
ffmpeg.ffprobe(inputImagePath, (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
const originalWidth = inputImageDetails.streams[0].width || width;
const originalHeight = inputImageDetails.streams[0].height || height;
const originalAspect = originalWidth / originalHeight;
const targetAspect = width / height;
let cropWidth, cropHeight;
if (originalAspect > targetAspect) {
// Crop width to match target aspect
// console.log("imageToVideoBase64 case 1")
cropHeight = originalHeight;
cropWidth = Math.floor(cropHeight * targetAspect);
} else {
// Crop height to match target aspect
// console.log("imageToVideoBase64 case 2")
cropWidth = originalWidth;
cropHeight = Math.floor(cropWidth / targetAspect);
}
// Set the path for the output video.
outputFilePath = outputFilePath || path.join(outputDir, `output_${uuidv4()}.${outputVideoFormat}`);
// we want to create a smooth Ken Burns effect
const durationInSeconds = outputVideoDurationInMs / 1000;
const framesTotal = durationInSeconds * fps;
const xCenter = `iw/2-(iw/zoom/2)`;
const yCenter = `ih/2-(ih/zoom/2)`;
const startZoom = 1;
const endZoom = 1 + zoomInRatePerSecond * durationInSeconds;
const zoomDurationFrames = Math.ceil(durationInSeconds * fps); // Total frames for the video
const videoFilters = [
`crop=${cropWidth}:${cropHeight}:${(originalWidth - cropWidth) / 2}:${(originalHeight - cropHeight) / 2}`,
`zoompan=z='min(zoom+${(endZoom - startZoom) / framesTotal}, ${endZoom})':x='${xCenter}':y='${yCenter}':d=${zoomDurationFrames}`
].join(',');
/*
console.log(`imagetoVideoBase64 called with: ${JSON.stringify({
inputImageInBase64: inputImageInBase64?.slice(0, 50),
outputFilePath,
width,
height,
outputVideoDurationInMs,
outputDir,
clearOutputDirAtTheEnd,
outputVideoFormat,
originalWidth,
originalHeight,
originalAspect,
targetAspect,
cropHeight,
cropWidth,
durationInSeconds,
framesTotal,
xCenter,
yCenter,
startZoom,
endZoom,
zoomDurationFrames,
videoFilters,
}, null, 2)}`)
*/
// Process the image to video conversion using ffmpeg.
await new Promise<void>((resolve, reject) => {
ffmpeg(inputImagePath)
// this is disabled to avoid repeating the zoom-in multiple times
// .inputOptions(['-loop 1'])
.outputOptions([
`-t ${durationInSeconds}`,
`-r ${fps}`,
`-s ${width}x${height}`,
`-c:v ${codec}`,
'-tune stillimage',
'-pix_fmt yuv420p'
])
.videoFilters(videoFilters)
.on('start', commandLine => console.log('imageToVideoBase64: Spawned Ffmpeg with command: ' + commandLine))
.on('end', () => resolve())
.on('error', err => reject(err))
.save(outputFilePath);
});
// Read the video file, encode it to Base64, and format it as a data URI.
const videoBuffer = await readFile(outputFilePath);
const videoBase64 = videoBuffer.toString('base64');
const resultAsBase64DataUri = `data:video/${outputVideoFormat};base64,${videoBase64}`;
// Attempt to clean up temporary work files.
if (clearOutputDirAtTheEnd) {
try {
await rm(outputDir, { recursive: true, force: true });
} catch (error) {
console.error('Error removing temporary files:', error);
}
}
return resultAsBase64DataUri;
} |