Spaces:
Runtime error
Runtime error
File size: 4,879 Bytes
e43a4a9 740e55f e43a4a9 740e55f e43a4a9 416cae6 e43a4a9 8b5482f 740e55f 8b5482f 740e55f e43a4a9 740e55f e43a4a9 740e55f 8b5482f 740e55f 8b5482f 740e55f 416cae6 740e55f 8b5482f 740e55f 416cae6 740e55f 8b5482f 740e55f 416cae6 740e55f 8b5482f 740e55f 8b5482f e43a4a9 | 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 | import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawn } from "node:child_process";
import ffmpegPath from "ffmpeg-static";
import { HttpError } from "../utils/httpError.js";
function runFfmpeg(inputPath, outputPath, outputArgs) {
return new Promise((resolve, reject) => {
const child = spawn(ffmpegPath, [
"-y",
"-i",
inputPath,
"-vn",
...outputArgs,
outputPath
]);
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", (error) => {
reject(new HttpError(400, "Failed to convert audio to mp3.", error.message));
});
child.on("close", (code) => {
if (code === 0) {
resolve();
return;
}
reject(new HttpError(400, "Failed to convert audio to mp3.", stderr.trim() || undefined));
});
});
}
export function createAudioConversionService({ fetchImpl = fetch, maxAudioDownloadMb = 25 } = {}) {
const maxBytes = maxAudioDownloadMb * 1024 * 1024;
return {
async downloadAndConvertToMp3Base64(url) {
const parsedUrl = new URL(url);
if (!["http:", "https:"].includes(parsedUrl.protocol)) {
throw new HttpError(400, "Audio URL must use http or https.");
}
const response = await fetchImpl(url);
if (!response.ok) {
throw new HttpError(400, `Failed to download audio URL: ${response.status} ${response.statusText}`);
}
const audioBuffer = Buffer.from(await response.arrayBuffer());
if (audioBuffer.length > maxBytes) {
throw new HttpError(413, `Audio URL exceeded ${maxAudioDownloadMb}MB download limit.`);
}
const inputFormat = inferAudioFormatFromUrl(url) || inferAudioFormatFromMimeType(response.headers.get("content-type")) || "unknown";
return transcodeAudioBuffer(audioBuffer, inputFormat);
},
async normalizeBase64Audio({ data, format }) {
const audioBuffer = Buffer.from(data, "base64");
if (audioBuffer.length === 0) {
throw new HttpError(400, "Audio input must include base64 data.");
}
if (audioBuffer.length > maxBytes) {
throw new HttpError(413, `Audio input exceeded ${maxAudioDownloadMb}MB upload limit.`);
}
if (!["mp3", "wav", "m4a"].includes(format)) {
throw new HttpError(400, "Audio input format must be mp3, wav, or m4a.");
}
try {
return await transcodeAudioBuffer(audioBuffer, format);
} catch (error) {
if (error instanceof HttpError) {
throw error;
}
throw new HttpError(400, `Failed to normalize audio input as ${format}.`, error.message);
}
}
};
async function transcodeAudioBuffer(audioBuffer, inputFormat) {
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "oapix-audio-"));
const normalizedInputFormat = normalizeInputFormat(inputFormat);
const inputPath = path.join(tempDir, `input-media.${normalizedInputFormat === "unknown" ? "bin" : normalizedInputFormat}`);
const outputPath = path.join(tempDir, "output.mp3");
try {
console.log(`audio conversion started: format=${normalizedInputFormat}, target=mp3`);
await fs.writeFile(inputPath, audioBuffer);
await runFfmpeg(inputPath, outputPath, ffmpegOutputArgs());
const convertedBuffer = await fs.readFile(outputPath);
console.log(`audio conversion successful: format=${normalizedInputFormat}->mp3`);
return {
data: convertedBuffer.toString("base64"),
format: "mp3"
};
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
console.error(`audio conversion failed: format=${normalizedInputFormat}, target=mp3, error=${detail}`);
throw error;
} finally {
await fs.rm(tempDir, { force: true, recursive: true });
}
}
}
function ffmpegOutputArgs() {
return ["-acodec", "libmp3lame", "-q:a", "4"];
}
function inferAudioFormatFromUrl(url) {
try {
const pathname = new URL(url).pathname.toLowerCase();
if (pathname.endsWith(".m4a")) {
return "m4a";
}
if (pathname.endsWith(".wav")) {
return "wav";
}
if (pathname.endsWith(".mp3")) {
return "mp3";
}
} catch (_error) {
return "unknown";
}
return "unknown";
}
function inferAudioFormatFromMimeType(mimeType) {
const value = String(mimeType || "").split(";")[0].trim().toLowerCase();
if (value === "audio/mp4" || value === "audio/x-m4a") {
return "m4a";
}
if (value === "audio/wav" || value === "audio/x-wav") {
return "wav";
}
if (value === "audio/mpeg" || value === "audio/mp3") {
return "mp3";
}
return "unknown";
}
function normalizeInputFormat(format) {
return ["m4a", "wav", "mp3"].includes(format) ? format : "unknown";
}
|