migrate to root dir

This commit is contained in:
2025-08-08 19:26:21 +02:00
parent cf8219691b
commit 8720500442
41 changed files with 2478 additions and 4440 deletions

125
lib/pipeline/audio.ts Normal file
View File

@@ -0,0 +1,125 @@
import { spawn } from "child_process";
import { StoryConfig } from "./config";
import * as path from "path";
import * as fs from "fs";
import ffmpeg from "ffmpeg-static";
import ffprobe from "ffprobe-static";
export function getDuration(file: string): Promise<number> {
const ffprobePath: string = (ffprobe as unknown as { path: string }).path;
const args = ["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", file];
const ffprobeProcess = spawn(ffprobePath, args);
let duration = "";
ffprobeProcess.stdout.on("data", (data: Buffer) => {
duration += data.toString();
});
return new Promise<number>((resolve, reject) => {
ffprobeProcess.on("close", (code: number | null) => {
if (code === 0) {
resolve(parseFloat(duration));
} else {
reject(new Error(`ffprobe process exited with code ${code}`));
}
});
});
}
export async function getChunkDuration(chunkPath: string): Promise<number> {
return getDuration(chunkPath);
}
export async function generateSilence(duration: number, outputFile: string): Promise<void> {
const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg";
const args = ["-y", "-f", "lavfi", "-i", `anullsrc=r=44100:cl=stereo:d=${duration}`, outputFile];
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}`));
}
});
});
}
export async function combineAudio(storyName: string, storyConfig: StoryConfig, audioFiles: string[]): Promise<void> {
const ffmpegPath: string = (ffmpeg as unknown as string) || "ffmpeg";
const introFile = path.resolve("stories", storyName, storyConfig.config.intro_audio_file);
const outroFile = path.resolve("stories", storyName, storyConfig.config.outro_audio_file);
const finalAudioDir = path.resolve("stories", storyName, "final_audio");
const tempAudioFile = path.join(finalAudioDir, "temp.mp3");
const finalAudioFile = path.join(finalAudioDir, "final.mp3");
const backgroundMusicFile = path.resolve("stories", storyName, storyConfig.config.background_music_file);
fs.mkdirSync(finalAudioDir, { recursive: true });
const allFiles = [introFile, ...audioFiles.map((f) => path.resolve(f)), outroFile];
const fileList = allFiles.map((f) => `file '${f.replace(/'/g, "'\\''")}'`).join("\n");
const listFile = path.resolve("stories", storyName, "filelist.txt");
fs.writeFileSync(listFile, fileList);
const concatArgs = ["-y", "-f", "concat", "-safe", "0", "-i", listFile, "-c", "copy", tempAudioFile];
const concatProcess = spawn(ffmpegPath, concatArgs);
await new Promise<void>((resolve, reject) => {
concatProcess.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`ffmpeg concat process exited with code ${code}`));
}
});
});
const duration = await getDuration(tempAudioFile);
if (!fs.existsSync(backgroundMusicFile)) {
await new Promise<void>((resolve, reject) => {
const args = ["-y", "-i", tempAudioFile, "-c:a", "libmp3lame", "-q:a", "4", finalAudioFile];
const p = spawn(ffmpegPath, args);
p.on("close", (code: number | null) =>
code === 0 ? resolve() : reject(new Error(`ffmpeg copy failed ${code}`))
);
});
return;
}
const bgVolume = "0.2";
const mixArgs = [
"-y",
"-i",
tempAudioFile,
"-stream_loop",
"-1",
"-i",
backgroundMusicFile,
"-filter_complex",
`[1:a]volume=${bgVolume},atrim=0:${duration},asetpts=N/SR/TB[bg];[0:a][bg]amix=inputs=2:duration=first:dropout_transition=0[a]`,
"-map",
"[a]",
"-c:a",
"libmp3lame",
"-q:a",
"4",
finalAudioFile,
];
const mixProcess = spawn(ffmpegPath, mixArgs);
return new Promise<void>((resolve, reject) => {
mixProcess.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else {
reject(new Error(`ffmpeg mix process exited with code ${code}`));
}
});
});
}