openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
168 lines (167 loc) • 5.34 kB
JavaScript
import { A as writeExternalFileWithinRoot } from "./fs-safe-B6pvPGnf.js";
import { n as resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir-DnyL0lW9.js";
import { n as tempWorkspaceSync, r as withTempWorkspace } from "./private-temp-workspace-Dh7teFP0.js";
import { r as runCommandWithTimeout } from "./exec-BIE-3oLG.js";
import { t as basenameFromAnyPath } from "./file-name-CKnWacTs.js";
import "./image-ops-DU1SIRgh.js";
import { s as runFfmpeg } from "./media-probe-Dbs998-L.js";
import path from "node:path";
//#region src/media/audio-transcode.ts
const DEFAULT_OPUS_SAMPLE_RATE_HZ = 48e3;
const DEFAULT_OPUS_BITRATE = "64k";
const DEFAULT_OPUS_CHANNELS = 1;
const DEFAULT_TEMP_PREFIX = "audio-opus-";
const DEFAULT_OUTPUT_FILE_NAME = "voice.opus";
function normalizeAudioExtension(params) {
const fromExtension = params.inputExtension?.trim();
const normalized = (fromExtension ? fromExtension.startsWith(".") ? fromExtension : `.${fromExtension}` : path.extname(params.inputFileName ?? "")).toLowerCase();
return /^\.[a-z0-9]{1,12}$/.test(normalized) ? normalized : ".audio";
}
function normalizeTempPrefix(value) {
const sanitized = value?.trim().replace(/[^a-zA-Z0-9._-]/g, "-");
if (!sanitized || sanitized === "." || sanitized === "..") return DEFAULT_TEMP_PREFIX;
return sanitized.endsWith("-") ? sanitized : `${sanitized}-`;
}
function normalizeOutputFileName(value) {
const baseName = basenameFromAnyPath(value?.trim() || DEFAULT_OUTPUT_FILE_NAME);
if (/^[a-zA-Z0-9._-]{1,80}$/.test(baseName) && baseName !== "." && baseName !== "..") return baseName;
return DEFAULT_OUTPUT_FILE_NAME;
}
function resolveMaxDurationSeconds(value) {
if (value === void 0) return;
if (!Number.isFinite(value) || value <= 0) throw new Error("maxDurationSeconds must be a positive finite number");
return value;
}
/** Transcodes arbitrary audio input into mono Opus using a scoped temp workspace. */
async function transcodeAudioBufferToOpus(params) {
const maxDurationSeconds = resolveMaxDurationSeconds(params.maxDurationSeconds);
return await withTempWorkspace({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: normalizeTempPrefix(params.tempPrefix)
}, async (workspace) => {
const inputPath = await workspace.write(`input${normalizeAudioExtension(params)}`, params.audioBuffer);
const outputFileName = normalizeOutputFileName(params.outputFileName);
await writeExternalFileWithinRoot({
rootDir: workspace.dir,
path: outputFileName,
write: async (outputPath) => {
await runFfmpeg([
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
inputPath,
"-vn",
"-sn",
"-dn",
...maxDurationSeconds === void 0 ? [] : ["-t", String(maxDurationSeconds)],
"-c:a",
"libopus",
"-b:a",
params.bitrate ?? DEFAULT_OPUS_BITRATE,
"-ar",
String(params.sampleRateHz ?? DEFAULT_OPUS_SAMPLE_RATE_HZ),
"-ac",
String(params.channels ?? DEFAULT_OPUS_CHANNELS),
"-f",
"opus",
outputPath
], { timeoutMs: params.timeoutMs });
}
});
return await workspace.read(outputFileName);
});
}
/** Transcodes known audio container pairs, currently using macOS afconvert recipes where needed. */
async function transcodeAudioBuffer(params) {
const source = normalizeContainerExt(params.sourceExtension);
const target = normalizeContainerExt(params.targetExtension);
if (!source || !target) return {
ok: false,
reason: "invalid-extension"
};
if (source === target) return {
ok: false,
reason: "noop-same-container"
};
const recipe = pickAfconvertRecipe(source, target);
if (!recipe) return {
ok: false,
reason: "no-recipe"
};
if (process.platform !== "darwin") return {
ok: false,
reason: "platform-unsupported"
};
const tmp = tempWorkspaceSync({
rootDir: resolvePreferredOpenClawTmpDir(),
prefix: "tts-transcode-"
});
try {
const inPath = tmp.write(`in.${source}`, params.audioBuffer);
const outPath = tmp.path(`out.${target}`);
const result = await runAfconvert({
args: [
...recipe,
inPath,
outPath
],
timeoutMs: params.timeoutMs ?? 5e3
});
if (!result.ok) return {
ok: false,
reason: "transcoder-failed",
detail: result.detail
};
return {
ok: true,
buffer: tmp.read(`out.${target}`)
};
} catch (err) {
return {
ok: false,
reason: "transcoder-failed",
detail: err.message
};
} finally {
tmp.cleanup();
}
}
function normalizeContainerExt(ext) {
const trimmed = ext.trim().toLowerCase().replace(/^\./, "");
return /^[a-z0-9]{1,12}$/.test(trimmed) ? trimmed : void 0;
}
function pickAfconvertRecipe(_source, target) {
if (target === "caf") return [
"-f",
"caff",
"-d",
"opus@24000",
"-c",
"1"
];
}
async function runAfconvert(params) {
try {
const result = await runCommandWithTimeout(["/usr/bin/afconvert", ...params.args], {
maxOutputBytes: 1024,
timeoutMs: params.timeoutMs
});
if (result.termination === "timeout") return {
ok: false,
detail: `timeout-${params.timeoutMs}ms`
};
return result.code === 0 ? { ok: true } : {
ok: false,
detail: `exit-${result.code ?? "unknown"}`
};
} catch (err) {
return {
ok: false,
detail: err instanceof Error ? err.message : String(err)
};
}
}
//#endregion
export { transcodeAudioBufferToOpus as n, transcodeAudioBuffer as t };