migrate to root dir
This commit is contained in:
74
lib/pipeline/video.ts
Normal file
74
lib/pipeline/video.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { spawn } from "child_process";
|
||||
import { StoryConfig } from "./config";
|
||||
import * as path from "path";
|
||||
import ffmpeg from "ffmpeg-static";
|
||||
|
||||
function escapeForFilter(filePath: string): string {
|
||||
return filePath.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/,/g, "\\,").replace(/'/g, "\\'");
|
||||
}
|
||||
|
||||
export async function createVideo(
|
||||
storyName: string,
|
||||
storyConfig: StoryConfig,
|
||||
imageFiles: string[],
|
||||
chunkDurations: number[],
|
||||
srtPath: string
|
||||
): Promise<void> {
|
||||
const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg";
|
||||
const audioPath = path.resolve("stories", storyName, "final_audio", "final.mp3");
|
||||
const videoPath = path.resolve("stories", storyName, "video", "final.mp4");
|
||||
const totalDuration = chunkDurations.reduce((a, b) => a + b, 0);
|
||||
const resolution = storyConfig.config.export_settings?.resolution || "1024x1024";
|
||||
|
||||
const inputs = imageFiles.map((file) => ["-loop", "1", "-i", file]).flat();
|
||||
inputs.push("-i", audioPath);
|
||||
|
||||
const filterGraph = imageFiles
|
||||
.map((_, i) => {
|
||||
const duration = chunkDurations[i];
|
||||
const zoompan = `zoompan=z='min(zoom+0.0015,1.5)':d=${25 * duration}:x='iw/2-(iw/zoom/2)':y='ih/2-(ih/zoom/2)':s=${resolution}`;
|
||||
return `[${i}:v]${zoompan},fade=t=out:st=${duration - 1}:d=1[v${i}]`;
|
||||
})
|
||||
.join(";");
|
||||
|
||||
const streamSpecifiers = imageFiles.map((_, i) => `[v${i}]`).join("");
|
||||
const escapedSrt = escapeForFilter(srtPath);
|
||||
const concatGraph = `${filterGraph};${streamSpecifiers}concat=n=${imageFiles.length}:v=1:a=0,format=yuv420p[v0]`;
|
||||
const finalFilterGraph = `${concatGraph};[v0]subtitles='${escapedSrt}'[v]`;
|
||||
|
||||
const args = [
|
||||
"-y",
|
||||
...inputs,
|
||||
"-filter_complex",
|
||||
finalFilterGraph,
|
||||
"-map",
|
||||
"[v]",
|
||||
"-map",
|
||||
`${imageFiles.length}:a`,
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-tune",
|
||||
"stillimage",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-b:a",
|
||||
"192k",
|
||||
"-pix_fmt",
|
||||
"yuv420p",
|
||||
"-t",
|
||||
totalDuration.toString(),
|
||||
videoPath,
|
||||
];
|
||||
|
||||
const ffmpegProcess = spawn(ffmpegPath, args);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
ffmpegProcess.on("close", (code: number | null) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`ffmpeg process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user