Initial commit: PodcastYes — AI podcast platform
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { promises as fs } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { runFfmpeg, ffprobeDuration } from "../ffmpeg";
|
||||
|
||||
/**
|
||||
* Concatenate per-segment MP3 buffers into one normalized episode MP3.
|
||||
*
|
||||
* Segments are re-encoded (not stream-copied) through a single libmp3lame pass
|
||||
* with loudness normalization, which guarantees a uniform codec/bitrate and
|
||||
* avoids the header/timestamp glitches that `-c copy` concat can produce.
|
||||
*/
|
||||
export async function stitchMp3(
|
||||
segments: Buffer[]
|
||||
): Promise<{ data: Buffer; durationSec: number | null }> {
|
||||
if (segments.length === 0) throw new Error("No audio segments to stitch");
|
||||
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "podcastyes-"));
|
||||
try {
|
||||
const files: string[] = [];
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const file = path.join(dir, `seg_${String(i).padStart(4, "0")}.mp3`);
|
||||
await fs.writeFile(file, segments[i]);
|
||||
files.push(file);
|
||||
}
|
||||
|
||||
// concat demuxer list — forward slashes so it parses on Windows and Linux.
|
||||
const listPath = path.join(dir, "list.txt");
|
||||
const listBody = files
|
||||
.map((f) => `file '${f.split(path.sep).join("/").replace(/'/g, "'\\''")}'`)
|
||||
.join("\n");
|
||||
await fs.writeFile(listPath, listBody);
|
||||
|
||||
const outPath = path.join(dir, "episode.mp3");
|
||||
await runFfmpeg([
|
||||
"-y",
|
||||
"-f",
|
||||
"concat",
|
||||
"-safe",
|
||||
"0",
|
||||
"-i",
|
||||
listPath,
|
||||
"-af",
|
||||
"loudnorm=I=-16:TP=-1.5:LRA=11",
|
||||
"-c:a",
|
||||
"libmp3lame",
|
||||
"-b:a",
|
||||
"128k",
|
||||
"-ar",
|
||||
"44100",
|
||||
outPath,
|
||||
]);
|
||||
|
||||
const data = await fs.readFile(outPath);
|
||||
const durationSec = await ffprobeDuration(outPath);
|
||||
return { data, durationSec };
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user