Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 2 additions & 38 deletions scripts/download-llama-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const fs = require("fs");
const path = require("path");
const {
copyLibraries,
downloadFile,
extractArchive,
fetchLatestRelease,
Expand Down Expand Up @@ -62,36 +63,6 @@ function findAsset(release, pattern) {
return release?.assets?.find((a) => pattern.test(a.name));
}

function findLibrariesInDir(dir, pattern, maxDepth = 5, currentDepth = 0) {
if (currentDepth >= maxDepth) return [];

const results = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
results.push(...findLibrariesInDir(fullPath, pattern, maxDepth, currentDepth + 1));
} else if (matchesPattern(entry.name, pattern)) {
results.push(fullPath);
}
}

return results;
}

function matchesPattern(filename, pattern) {
if (pattern === "*.dylib") {
return filename.endsWith(".dylib");
} else if (pattern === "*.dll") {
return filename.endsWith(".dll");
} else if (pattern === "*.so*") {
return /\.so(\.\d+)*$/.test(filename) || filename.endsWith(".so");
}
return false;
}

async function downloadBinary(key, config, release, isForce = false) {
if (!config) {
console.log(` ${key}: Not supported`);
Expand Down Expand Up @@ -135,14 +106,7 @@ async function downloadBinary(key, config, release, isForce = false) {
console.log(` ${key}: Extracted to ${config.outputName}`);

if (config.libPattern) {
const libraries = findLibrariesInDir(extractDir, config.libPattern);

for (const libPath of libraries) {
const libName = path.basename(libPath);
const destPath = path.join(BIN_DIR, libName);

fs.copyFileSync(libPath, destPath);
setExecutable(destPath);
for (const libName of copyLibraries(extractDir, BIN_DIR, config.libPattern)) {
console.log(` ${key}: Copied library ${libName}`);
}
}
Expand Down
44 changes: 8 additions & 36 deletions scripts/download-sherpa-onnx.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const {
cleanupFiles,
downloadFile,
findBinaryInDir,
findLibrariesInDir,
parseArgs,
setExecutable,
} = require("./lib/download-utils");
Expand Down Expand Up @@ -84,40 +85,6 @@ function extractTarBz2(archivePath, destDir) {
});
}

function findLibrariesInDir(dir, pattern, maxDepth = 5, currentDepth = 0) {
if (currentDepth >= maxDepth) return [];

const results = [];
try {
const entries = fs.readdirSync(dir, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
results.push(...findLibrariesInDir(fullPath, pattern, maxDepth, currentDepth + 1));
} else if (matchesPattern(entry.name, pattern)) {
results.push(fullPath);
}
}
} catch {
// Ignore permission errors
}

return results;
}

function matchesPattern(filename, pattern) {
if (pattern === "*.dylib") {
return filename.endsWith(".dylib");
} else if (pattern === "*.dll") {
return filename.endsWith(".dll");
} else if (pattern === "*.so*") {
return /\.so(\.\d+)*$/.test(filename) || filename.endsWith(".so");
}
return false;
}

function copyBinary(extractDir, binaryName, outputPath, platformArch) {
const foundPath = findBinaryInDir(extractDir, binaryName);

Expand Down Expand Up @@ -160,7 +127,10 @@ async function downloadBinary(platformArch, config, isForce = false) {
const diarizeOutputPath = path.join(BIN_DIR, config.diarizeOutputName);
const installMarkerPath = path.join(BIN_DIR, `.sherpa-onnx-${platformArch}.json`);

if (!isForce && isCompleteInstall(installMarkerPath, [outputPath, onlineOutputPath, diarizeOutputPath])) {
if (
!isForce &&
isCompleteInstall(installMarkerPath, [outputPath, onlineOutputPath, diarizeOutputPath])
) {
console.log(` ${platformArch}: Already exists (use --force to re-download)`);
return true;
}
Expand Down Expand Up @@ -189,7 +159,9 @@ async function downloadBinary(platformArch, config, isForce = false) {
// Copy shared libraries
const copiedLibraries = [];
if (config.libPattern) {
const libraries = findLibrariesInDir(extractDir, config.libPattern);
const libraries = findLibrariesInDir(extractDir, config.libPattern, {
ignoreReadErrors: true,
});

// Separate versioned and unversioned libraries to create symlinks where possible
// e.g. libonnxruntime.dylib -> libonnxruntime.1.23.2.dylib (saves ~71MB)
Expand Down
60 changes: 57 additions & 3 deletions scripts/download-whisper-cpp.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const fs = require("fs");
const path = require("path");
const {
copyLibraries,
downloadFile,
extractZip,
fetchLatestRelease,
Expand All @@ -15,7 +16,8 @@ const WHISPER_CPP_REPO = "OpenWhispr/whisper.cpp";

// Pinned to a tested build. Tracking the latest release let an upstream whisper.cpp bump
// change transcription output between app releases with no diff to review. See #1348.
const WHISPER_CPP_TAG = process.env.WHISPER_CPP_VERSION || "0.0.8";
// 0.0.10 is the first release whose win32 zips bundle the MSVC runtime DLLs (CUS-113).
const WHISPER_CPP_TAG = process.env.WHISPER_CPP_VERSION || "0.0.10";

const BINARIES = {
"darwin-arm64": {
Expand All @@ -32,6 +34,10 @@ const BINARIES = {
zipName: "whisper-server-win32-x64-cpu.zip",
binaryName: "whisper-server-win32-x64-cpu.exe",
outputName: "whisper-server-win32-x64.exe",
// MSVC runtime DLLs the exe links dynamically; without them beside the exe,
// machines lacking the VC++ redistributable die at load with 0xC0000135 (CUS-113)
libPattern: "*.dll",
requiredLibraries: ["msvcp140.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcomp140.dll"],
},
"linux-x64": {
zipName: "whisper-server-linux-x64-cpu.zip",
Expand All @@ -57,18 +63,41 @@ function getDownloadUrl(release, zipName) {
return asset?.url || null;
}

function isCompleteInstall(markerPath, binaryPath, config) {
if (!fs.existsSync(binaryPath)) return false;

try {
const marker = JSON.parse(fs.readFileSync(markerPath, "utf8"));
if (marker.version !== WHISPER_CPP_TAG || !Array.isArray(marker.libraries)) return false;

const binDir = path.dirname(markerPath);
if (marker.libraries.some((library) => !fs.existsSync(path.join(binDir, library)))) {
return false;
}

const installedLibraries = new Set(marker.libraries.map((library) => library.toLowerCase()));
return (config.requiredLibraries || []).every((library) =>
installedLibraries.has(library.toLowerCase())
);
} catch {
return false;
}
}

async function downloadBinary(platformArch, config, release, isForce = false) {
if (!config) {
console.log(` [server] ${platformArch}: Not supported`);
return false;
}

const outputPath = path.join(BIN_DIR, config.outputName);
const installMarkerPath = path.join(BIN_DIR, `.whisper-cpp-${platformArch}.json`);

if (fs.existsSync(outputPath) && !isForce) {
if (!isForce && isCompleteInstall(installMarkerPath, outputPath, config)) {
console.log(` [server] ${platformArch}: Already exists (use --force to re-download)`);
return true;
}
if (isForce && fs.existsSync(installMarkerPath)) fs.unlinkSync(installMarkerPath);

const url = getDownloadUrl(release, config.zipName);
if (!url) {
Expand All @@ -91,6 +120,27 @@ async function downloadBinary(platformArch, config, release, isForce = false) {
fs.copyFileSync(binaryPath, outputPath);
setExecutable(outputPath);
console.log(` [server] ${platformArch}: Extracted to ${config.outputName}`);

let copiedLibraries = [];
if (config.libPattern) {
copiedLibraries = copyLibraries(extractDir, BIN_DIR, config.libPattern);
for (const libName of copiedLibraries) {
console.log(` [server] ${platformArch}: Copied library ${libName}`);
}
}

const copiedLibraryNames = new Set(copiedLibraries.map((library) => library.toLowerCase()));
const missingLibraries = (config.requiredLibraries || []).filter(
(library) => !copiedLibraryNames.has(library.toLowerCase())
);
if (missingLibraries.length > 0) {
throw new Error(`Archive missing required libraries: ${missingLibraries.join(", ")}`);
}

fs.writeFileSync(
installMarkerPath,
JSON.stringify({ version: WHISPER_CPP_TAG, libraries: copiedLibraries })
);
} else {
console.error(
` [server] ${platformArch}: Binary "${config.binaryName}" not found in archive`
Expand Down Expand Up @@ -170,4 +220,8 @@ async function main() {
}
}

main().catch(console.error);
if (require.main === module) {
main().catch(console.error);
}

module.exports = { BINARIES, WHISPER_CPP_TAG, isCompleteInstall };
53 changes: 53 additions & 0 deletions scripts/lib/download-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,56 @@ function setExecutable(filePath) {
}
}

function matchesPattern(filename, pattern) {
if (pattern === "*.dylib") {
return filename.endsWith(".dylib");
} else if (pattern === "*.dll") {
return filename.endsWith(".dll");
} else if (pattern === "*.so*") {
return /\.so(\.\d+)*$/.test(filename) || filename.endsWith(".so");
}
return false;
}

function findLibrariesInDir(dir, pattern, options = {}, currentDepth = 0) {
const normalizedOptions = typeof options === "number" ? { maxDepth: options } : options;
const { maxDepth = 5, ignoreReadErrors = false } = normalizedOptions;
if (currentDepth >= maxDepth) return [];

const results = [];
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch (error) {
if (ignoreReadErrors) return [];
throw error;
}

for (const entry of entries) {
const fullPath = path.join(dir, entry.name);

if (entry.isDirectory()) {
results.push(...findLibrariesInDir(fullPath, pattern, normalizedOptions, currentDepth + 1));
} else if (matchesPattern(entry.name, pattern)) {
results.push(fullPath);
}
}

return results;
}

function copyLibraries(extractDir, destDir, pattern) {
const copied = [];
for (const libPath of findLibrariesInDir(extractDir, pattern)) {
const libName = path.basename(libPath);
const destPath = path.join(destDir, libName);
fs.copyFileSync(libPath, destPath);
setExecutable(destPath);
copied.push(libName);
}
return copied;
}

function cleanupFiles(binDir, prefix, keepPrefix) {
const keepPrefixes = Array.isArray(keepPrefix) ? keepPrefix : [keepPrefix];
// Never delete shared libraries; only platform binaries. The b9763 split ships
Expand All @@ -335,11 +385,14 @@ function cleanupFiles(binDir, prefix, keepPrefix) {
}

module.exports = {
copyLibraries,
downloadFile,
extractArchive,
extractZip,
fetchLatestRelease,
findBinaryInDir,
findLibrariesInDir,
matchesPattern,
parseArgs,
setExecutable,
cleanupFiles,
Expand Down
5 changes: 4 additions & 1 deletion src/helpers/whisperVulkanManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ const EXPECTED_DIGESTS = {

const BIN_SUBDIR = "whisper-vulkan";

// Statically linked — no companion libs to copy
// Statically linked against ggml-vulkan (no Vulkan companion libs), but the exe
// still links the dynamic MSVC runtime — from release 0.0.10 the win32 zip bundles
// those DLLs and they must be extracted beside the exe (CUS-113)
class WhisperVulkanManager extends GpuBinaryManager {
constructor() {
super({
Expand All @@ -35,6 +37,7 @@ class WhisperVulkanManager extends GpuBinaryManager {
assetName: "whisper-server-win32-x64-vulkan.zip",
binaryName: "whisper-server-win32-x64-vulkan.exe",
outputName: "whisper-server-win32-x64-vulkan.exe",
libPattern: /\.dll$/i,
},
"linux-x64": {
assetName: "whisper-server-linux-x64-vulkan.zip",
Expand Down
Loading
Loading