1
0
Fork 0
polaris-task-force/src/lib/voiceover/transcode.ts
Z8MB1E 962607b90e feat(voiceover): transcode submissions to mp3 via ffmpeg
Normalize every submission to mono 96 kbps MP3 so takes play everywhere regardless of the recording codec; falls back to storing the original file when ffmpeg is unavailable or fails.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-07 04:37:10 -04:00

117 lines
2.6 KiB
TypeScript

import { spawn } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import path from "node:path";
export const MP3_MIMETYPE = "audio/mpeg";
/** Voice-appropriate MP3 settings: mono, 44.1 kHz, 96 kbps. */
const FFMPEG_ARGS = [
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
"input",
"-vn",
"-ac",
"1",
"-ar",
"44100",
"-codec:a",
"libmp3lame",
"-b:a",
"96k",
"-f",
"mp3",
"output.mp3",
];
const FFMPEG_TIMEOUT_MS = 30_000;
export interface Mp3Result {
buffer: Buffer;
name: string;
mimetype: string;
size: number;
}
/**
* Convert an uploaded audio recording (WebM/Opus, MP4/AAC, etc.) to MP3 using
* the system ffmpeg binary. Returns null when ffmpeg is unavailable or fails,
* so callers can fall back to storing the original file untouched.
*/
export async function transcodeToMp3(
buffer: Buffer,
originalName: string,
): Promise<Mp3Result | null> {
let dir: string | null = null;
try {
dir = await mkdtemp(path.join(tmpdir(), "voiceover-"));
const inputPath = path.join(dir, "input");
const outputPath = path.join(dir, "output.mp3");
await writeFile(inputPath, buffer);
const ok = await runFfmpeg(inputPath, outputPath);
if (!ok) {
return null;
}
const mp3 = await readFile(outputPath);
if (mp3.length === 0) {
return null;
}
return {
buffer: mp3,
name: `${stripExtension(originalName)}.mp3`,
mimetype: MP3_MIMETYPE,
size: mp3.length,
};
} catch {
return null;
} finally {
if (dir) {
await rm(dir, { recursive: true, force: true });
}
}
}
function stripExtension(name: string): string {
const dot = name.lastIndexOf(".");
return dot > 0 ? name.slice(0, dot) : name;
}
function runFfmpeg(inputPath: string, outputPath: string): Promise<boolean> {
// Rewrite the two path placeholders into the real temp paths.
const args = FFMPEG_ARGS.map((arg) =>
arg === "input" ? inputPath : arg === "output.mp3" ? outputPath : arg,
);
return new Promise((resolve) => {
let child: ReturnType<typeof spawn>;
try {
child = spawn("ffmpeg", args, { stdio: "ignore" });
} catch {
resolve(false);
return;
}
let settled = false;
const finish = (result: boolean) => {
if (!settled) {
settled = true;
clearTimeout(timer);
resolve(result);
}
};
const timer = setTimeout(() => {
child.kill("SIGKILL");
finish(false);
}, FFMPEG_TIMEOUT_MS);
child.on("error", () => finish(false));
child.on("close", (code) => finish(code === 0));
});
}