openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
315 lines (314 loc) • 12.9 kB
JavaScript
import { i as getOrCreatePromise } from "./lazy-promise-DGqyc4Y4.js";
import { d as pathExists } from "./fs-safe-B6pvPGnf.js";
import { r as resolveEnvironmentValue } from "./process-env-DlZFJzq6.js";
import { n as runExec } from "./exec-BIE-3oLG.js";
import { constants } from "node:fs";
import path from "node:path";
import fs$1 from "node:fs/promises";
//#region src/media-understanding/fs.ts
/** Safely checks optional media file paths without throwing on empty input. */
async function optionalPathExists(filePath) {
return filePath ? await pathExists(filePath) : false;
}
//#endregion
//#region src/media-understanding/local-audio.ts
const WHISPER_CPP_MODEL_DIRS = [
"/opt/homebrew/share/whisper-cpp",
"/usr/local/share/whisper-cpp",
"/usr/share/whisper-cpp"
];
async function listDirectoryEntries(dirPath) {
try {
return await fs$1.readdir(dirPath);
} catch {
return [];
}
}
/**
* Picks an installed ggml whisper.cpp model without hardcoding a filename.
* Larger models transcribe better, so prefer non-tiny when several exist;
* WHISPER_CPP_MODEL remains the explicit override.
*/
async function discoverWhisperCppModel(listDirectory) {
for (const dir of WHISPER_CPP_MODEL_DIRS) {
const models = (await listDirectory(dir)).filter((name) => name.startsWith("ggml-") && name.endsWith(".bin")).toSorted((a, b) => {
return (a.includes("tiny") ? 1 : 0) - (b.includes("tiny") ? 1 : 0) || a.localeCompare(b);
});
if (models[0]) return path.join(dir, models[0]);
}
return null;
}
const binaryCache = /* @__PURE__ */ new Map();
const libraryCache = /* @__PURE__ */ new Map();
const observedBackendCache = /* @__PURE__ */ new Map();
function clearLocalAudioInspectionCacheForTests() {
binaryCache.clear();
libraryCache.clear();
observedBackendCache.clear();
}
function commandId(command) {
return path.basename(command.trim()).toLowerCase();
}
function resolveRequestedLocalAudioBackend(params) {
const command = commandId(params.command);
if (command === "sherpa-onnx-offline") {
const providerIndex = params.args.findIndex((arg) => arg === "--provider");
return ((providerIndex >= 0 ? params.args[providerIndex + 1] : void 0) ?? params.args.find((arg) => arg.startsWith("--provider="))?.slice(11) ?? "cpu").trim().toLowerCase();
}
if (command === "whisper-cli") {
if (params.args.includes("-ng") || params.args.includes("--no-gpu")) return "cpu";
const deviceIndex = params.args.findIndex((arg) => arg === "-dev" || arg === "--device");
const device = (deviceIndex >= 0 ? params.args[deviceIndex + 1] : void 0) ?? params.args.find((arg) => arg.startsWith("--device="))?.slice(9);
return device?.trim() ? `device:${device.trim()}` : void 0;
}
}
function observationKey(params) {
return `${params.command.trim()}\0${resolveRequestedLocalAudioBackend(params) ?? "default"}`;
}
function recordLocalAudioBackendObservation(params) {
if (commandId(params.command) !== "whisper-cli") return;
const backend = /failed to initialize\s+(?:MTL\d+|Metal|CUDA\d*)\s+backend/i.test(params.output) ? "cpu" : /using\s+(?:MTL\d+|Metal)\s+backend/i.test(params.output) ? "metal" : /using\s+CUDA\d*\s+backend/i.test(params.output) ? "cuda" : /using\s+CPU\s+backend|no GPU found/i.test(params.output) ? "cpu" : void 0;
if (backend) observedBackendCache.set(observationKey(params), backend);
return backend;
}
function getObservedBackend(params) {
return observedBackendCache.get(observationKey(params));
}
async function isExecutable(filePath, platform) {
try {
if (!(await fs$1.stat(filePath)).isFile()) return false;
if (platform !== "win32") await fs$1.access(filePath, constants.X_OK);
return true;
} catch {
return false;
}
}
function binaryNames(name, platform, pathExtensions) {
if (platform !== "win32" || path.extname(name)) return [name];
return [name, ...(pathExtensions ?? ".EXE;.CMD;.BAT;.COM").split(";").map((extension) => extension.trim()).filter(Boolean).map((extension) => `${name}${extension}`)];
}
function expandHomeDir(value, env) {
const trimmed = value.trim().replace(/^"(.*)"$/, "$1");
if (trimmed === "~") return env.HOME ?? trimmed;
if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) return env.HOME ? path.join(env.HOME, trimmed.slice(2)) : trimmed;
return trimmed;
}
async function findBinary(name, env, platform, checkExecutable = isExecutable) {
const pathValue = resolveEnvironmentValue(env, "PATH", platform) ?? "";
const pathExtensions = resolveEnvironmentValue(env, "PATHEXT", platform);
const key = `${platform}\0${pathValue}\0${pathExtensions ?? ""}\0${name}`;
return await getOrCreatePromise(binaryCache, key, async () => {
const direct = name.trim();
const candidates = binaryNames(direct, platform, pathExtensions);
if (direct.includes("/") || direct.includes("\\")) {
for (const candidate of candidates) {
const expanded = candidate === "~" || candidate.startsWith("~/") || candidate.startsWith("~\\") ? path.join(env.HOME ?? "~", candidate.slice(candidate === "~" ? 1 : 2)) : candidate;
if (await checkExecutable(expanded, platform)) return expanded;
}
return null;
}
for (const directory of pathValue.split(path.delimiter)) {
const expandedDirectory = expandHomeDir(directory, env);
if (!expandedDirectory) continue;
for (const candidate of candidates) {
const fullPath = path.join(expandedDirectory, candidate);
if (await checkExecutable(fullPath, platform)) return fullPath;
}
}
return null;
}, { cacheRejections: false });
}
async function inspectLinkedLibraries(filePath, platform) {
const key = `${platform}\0${filePath}`;
return await getOrCreatePromise(libraryCache, key, async () => {
const command = platform === "darwin" ? "otool" : platform === "linux" ? "readelf" : null;
if (!command) return null;
try {
const result = await runExec(command, platform === "darwin" ? ["-L", filePath] : ["-d", filePath], { timeoutMs: 1500 });
return `${result.stdout}\n${result.stderr ?? ""}`;
} catch {
return null;
}
}, { cacheRejections: false });
}
async function inspectWhisperBackend(params) {
const libraries = await params.libraries(params.command, params.platform);
if (/(?:ggml[-_]?cuda|libcuda|libcudart)/i.test(libraries ?? "")) return {
capableBackend: "cuda",
evidence: "whisper-cli links a CUDA ggml runtime"
};
if (params.platform === "darwin" && params.arch === "arm64") {
const realCommand = await params.realpath(params.command).catch(() => params.command);
if (/(?:ggml[-_]?metal|Metal\.framework)/i.test(libraries ?? "") || /\/Cellar\/whisper-cpp\/[^/]+\/bin\/whisper-cli$/.test(realCommand)) return {
capableBackend: "metal",
evidence: "Apple Silicon Homebrew whisper-cpp runtime with Metal support"
};
}
return { evidence: "whisper-cli backend cannot be proven without loading a model" };
}
function rank(candidate) {
if (candidate.id === "whisper-cli" && (candidate.observedBackend === "metal" || candidate.observedBackend === "cuda")) return 0;
if (candidate.id === "sherpa-onnx-offline") return 1;
if (candidate.id === "whisper-cli") return 2;
return candidate.id === "parakeet-mlx" ? 3 : 4;
}
async function inspectLocalAudioSelection(options = {}) {
const env = options.env ?? process.env;
const platform = options.platform ?? process.platform;
const arch = options.arch ?? process.arch;
const resolveBinary = async (name) => options.resolveBinary ? await options.resolveBinary(name, env) : await findBinary(name, env, platform, options.checkExecutable);
const [parakeetCommand, whisperCommand, sherpaCommand, pythonCommand] = await Promise.all([
"parakeet-mlx",
"whisper-cli",
"sherpa-onnx-offline",
"whisper"
].map(resolveBinary));
const envModel = env.WHISPER_CPP_MODEL?.trim();
const whisperModel = envModel && await optionalPathExists(envModel) ? envModel : await discoverWhisperCppModel(options.listDirectory ?? listDirectoryEntries);
const whisperReady = Boolean(whisperCommand) && Boolean(whisperModel);
const whisperBackend = whisperCommand ? await inspectWhisperBackend({
command: whisperCommand,
platform,
arch,
realpath: options.resolveRealpath ?? fs$1.realpath,
libraries: options.inspectLinkedLibraries ?? inspectLinkedLibraries
}) : { evidence: "whisper-cli command not found" };
const sherpaDir = env.SHERPA_ONNX_MODEL_DIR?.trim();
const sherpaFiles = sherpaDir ? [
"tokens.txt",
"encoder.onnx",
"decoder.onnx",
"joiner.onnx"
].map((file) => path.join(sherpaDir, file)) : [];
const sherpaReady = Boolean(sherpaCommand) && sherpaFiles.length === 4 && (await Promise.all(sherpaFiles.map(optionalPathExists))).every(Boolean);
const parakeetReady = Boolean(parakeetCommand) && platform === "darwin" && arch === "arm64";
const parakeetArgs = [
"{{AttachmentPath}}",
"--output-format",
"txt",
"--output-dir",
"{{OutputDir}}",
"--output-template",
"{filename}"
];
const whisperArgs = [
"-m",
whisperModel ?? "",
"-otxt",
"-of",
"{{OutputBase}}",
"-nt",
"{{AttachmentPath}}"
];
const sherpaArgs = [
`--tokens=${sherpaFiles[0]}`,
`--encoder=${sherpaFiles[1]}`,
`--decoder=${sherpaFiles[2]}`,
`--joiner=${sherpaFiles[3]}`,
"{{AttachmentPath}}"
];
const pythonArgs = [
"--model",
"turbo",
"--output_format",
"txt",
"--output_dir",
"{{OutputDir}}",
"--verbose",
"False",
"{{AttachmentPath}}"
];
const candidates = [
{
id: "parakeet-mlx",
command: "parakeet-mlx",
resolvedCommand: parakeetCommand ?? void 0,
available: Boolean(parakeetCommand),
ready: parakeetReady,
capableBackend: parakeetReady ? "mlx" : void 0,
evidence: parakeetReady ? "parakeet-mlx is an MLX runtime on Apple Silicon; device use is unobserved" : "parakeet-mlx acceleration is only supported on Apple Silicon",
selected: false,
reason: parakeetCommand ? parakeetReady ? void 0 : "unsupported platform for MLX acceleration" : "command not found",
entry: parakeetReady ? {
type: "cli",
command: "parakeet-mlx",
args: parakeetArgs
} : void 0
},
{
id: "whisper-cli",
command: "whisper-cli",
resolvedCommand: whisperCommand ?? void 0,
available: Boolean(whisperCommand),
ready: whisperReady,
...whisperBackend,
requestedBackend: resolveRequestedLocalAudioBackend({
command: "whisper-cli",
args: whisperArgs
}),
observedBackend: getObservedBackend({
command: "whisper-cli",
args: whisperArgs
}),
selected: false,
reason: whisperCommand ? whisperReady ? void 0 : "model file not found" : "command not found",
entry: whisperReady ? {
type: "cli",
command: "whisper-cli",
args: whisperArgs
} : void 0
},
{
id: "sherpa-onnx-offline",
command: "sherpa-onnx-offline",
resolvedCommand: sherpaCommand ?? void 0,
available: Boolean(sherpaCommand),
ready: sherpaReady,
requestedBackend: "cpu",
evidence: "OpenClaw auto args omit --provider, so sherpa-onnx uses its CPU default",
selected: false,
reason: sherpaCommand ? sherpaReady ? void 0 : "SHERPA_ONNX_MODEL_DIR is missing required model files" : "command not found",
entry: sherpaReady ? {
type: "cli",
command: "sherpa-onnx-offline",
args: sherpaArgs
} : void 0
},
{
id: "whisper",
command: "whisper",
resolvedCommand: pythonCommand ?? void 0,
available: Boolean(pythonCommand),
ready: Boolean(pythonCommand),
evidence: "Python Whisper chooses its runtime device when the model loads",
selected: false,
reason: pythonCommand ? void 0 : "command not found",
entry: pythonCommand ? {
type: "cli",
command: "whisper",
args: pythonArgs
} : void 0
}
];
candidates.sort((left, right) => rank(left) - rank(right));
const selected = candidates.find((candidate) => candidate.ready && candidate.entry);
if (selected) selected.selected = true;
return {
candidates,
entries: candidates.flatMap((candidate) => candidate.ready && candidate.entry ? [candidate.entry] : []),
selected
};
}
function formatLocalAudioSelection(selection) {
const selected = selection.selected;
if (!selected) return null;
const describeBackend = (candidate) => [
candidate.capableBackend ? `capable=${candidate.capableBackend}` : null,
candidate.requestedBackend ? `requested=${candidate.requestedBackend}` : null,
`observed=${candidate.observedBackend ?? "unknown"}`
].filter(Boolean).join(", ");
const fallbacks = selection.candidates.filter((candidate) => candidate.ready && candidate !== selected).map((candidate) => `${candidate.command} (${describeBackend(candidate)})`);
return `${selected.command} (${describeBackend(selected)}); ${selected.evidence}${fallbacks.length > 0 ? `; fallbacks: ${fallbacks.join(", ")}` : ""}`;
}
//#endregion
export { resolveRequestedLocalAudioBackend as a, recordLocalAudioBackendObservation as i, formatLocalAudioSelection as n, inspectLocalAudioSelection as r, clearLocalAudioInspectionCacheForTests as t };