Files
openwhispr/scripts/download-whisper-cpp.js
fad475c6f4 fix(whisper): extract MSVC runtime DLLs beside win32 whisper-server (#1687)
* fix(whisper): extract MSVC runtime DLLs beside win32 whisper-server

The win32 CPU whisper-server.exe links the dynamic MSVC runtime
(msvcp140/vcruntime140/vcruntime140_1/vcomp140) but shipped as a bare exe:
download-whisper-cpp.js copied only the binary out of the release zip, so
on machines without the VC++ 2015-2022 redistributable the Windows loader
killed every spawn with 0xC0000135 (STATUS_DLL_NOT_FOUND) before main(),
with empty stderr — local transcription dead on arrival (CUS-113).

- pin whisper.cpp 0.0.10, the first release whose win32 zips bundle the DLLs
- give the win32 config the same libPattern DLL-companion extraction the
  llama/sherpa download scripts already use, consolidating their duplicated
  findLibrariesInDir/matchesPattern helpers into lib/download-utils.js
- prep whisperVulkanManager with libPattern so the 0.0.10 vulkan zip's CRT
  DLLs are extracted too (its "statically linked" comment was wrong re CRT)

electron-builder's extraResources filter already passes *.dll and
cleanupFiles already spares libraries, so no packaging-config change needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(downloads): validate cached binaries and traversal errors

* fix(whisper): Preserve migration and propagate download failures

Keep legacy Windows Vulkan packs migratable until the DLL-bearing release and digest update lands. Make all-platform downloads exit nonzero after any platform failure and remove the unused numeric library-search options shim.

* fix(gpu): Require Windows whisper runtime libraries

Pin CUDA and Vulkan packs to verified 0.0.10 assets and reject incomplete caches or staged installs. Preserve the existing pack when validation fails.

* fix(deps): Upgrade Tiptap past attribute injection advisory

Align the full Tiptap package family at 3.30.4, the first patched release for GHSA-cp6q-959q-f8rh. Restore the npm audit gate without unrelated dependency upgrades.

* fix(deps): Patch xmldom serialization advisory

Upgrade the existing xmldom override to 0.8.15, the first patched 0.8.x release for GHSA-6gmq-8vp8-gcm6, so the npm audit gate remains clean.

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Chadpiha <chadpiha23@gmail.com>
2026-09-02 18:46:44 +02:00

240 lines
7.7 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const {
copyLibraries,
downloadFile,
extractZip,
fetchLatestRelease,
findBinaryInDir,
parseArgs,
setExecutable,
cleanupFiles,
} = require("./lib/download-utils");
const {
WHISPER_CPP_TAG,
WINDOWS_MSVC_RUNTIME_LIBRARIES,
} = require("../src/helpers/whisperCppRelease");
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.
// 0.0.10 is the first release whose win32 zips bundle the MSVC runtime DLLs (CUS-113).
const BINARIES = {
"darwin-arm64": {
zipName: "whisper-server-darwin-arm64.zip",
binaryName: "whisper-server-darwin-arm64",
outputName: "whisper-server-darwin-arm64",
},
"darwin-x64": {
zipName: "whisper-server-darwin-x64.zip",
binaryName: "whisper-server-darwin-x64",
outputName: "whisper-server-darwin-x64",
},
"win32-x64": {
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: WINDOWS_MSVC_RUNTIME_LIBRARIES,
},
"linux-x64": {
zipName: "whisper-server-linux-x64-cpu.zip",
binaryName: "whisper-server-linux-x64-cpu",
outputName: "whisper-server-linux-x64",
},
};
const BIN_DIR = path.join(__dirname, "..", "resources", "bin");
// Cache the release info to avoid multiple API calls
let cachedRelease = null;
async function getRelease() {
if (cachedRelease) return cachedRelease;
cachedRelease = await fetchLatestRelease(WHISPER_CPP_REPO, { tag: WHISPER_CPP_TAG });
return cachedRelease;
}
function getDownloadUrl(release, zipName) {
const asset = release?.assets?.find((a) => a.name === 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 (!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) {
console.error(` [server] ${platformArch}: Asset ${config.zipName} not found in release`);
return false;
}
console.log(` [server] ${platformArch}: Downloading from ${url}`);
const zipPath = path.join(BIN_DIR, config.zipName);
try {
await downloadFile(url, zipPath);
const extractDir = path.join(BIN_DIR, `temp-whisper-${platformArch}`);
fs.mkdirSync(extractDir, { recursive: true });
await extractZip(zipPath, extractDir);
const binaryPath = findBinaryInDir(extractDir, config.binaryName);
if (binaryPath) {
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`
);
return false;
}
fs.rmSync(extractDir, { recursive: true, force: true });
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
return true;
} catch (error) {
console.error(` [server] ${platformArch}: Failed - ${error.message}`);
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
return false;
}
}
async function downloadAllBinaries(release, isForce, download = downloadBinary) {
let allSucceeded = true;
for (const platformArch of Object.keys(BINARIES)) {
const succeeded = await download(platformArch, BINARIES[platformArch], release, isForce);
if (!succeeded) allSucceeded = false;
}
return allSucceeded;
}
async function main() {
console.log(`\n[whisper-server] Using pinned version: ${WHISPER_CPP_TAG}`);
const release = await getRelease();
if (!release) {
console.error(`[whisper-server] Could not fetch release from ${WHISPER_CPP_REPO}`);
console.log(`\nMake sure release exists: https://github.com/${WHISPER_CPP_REPO}/releases`);
process.exitCode = 1;
return;
}
console.log(`\nDownloading whisper-server binaries (${release.tag})...\n`);
fs.mkdirSync(BIN_DIR, { recursive: true });
const args = parseArgs();
if (args.isCurrent) {
if (!BINARIES[args.platformArch]) {
console.error(`Unsupported platform/arch: ${args.platformArch}`);
process.exitCode = 1;
return;
}
console.log(`Downloading for target platform (${args.platformArch}):`);
const ok = await downloadBinary(
args.platformArch,
BINARIES[args.platformArch],
release,
args.isForce
);
if (!ok) {
console.error(`Failed to download binaries for ${args.platformArch}`);
process.exitCode = 1;
return;
}
if (args.shouldCleanup) {
cleanupFiles(BIN_DIR, "whisper-server", `whisper-server-${args.platformArch}`);
}
} else {
console.log("Downloading binaries for all platforms:");
const allSucceeded = await downloadAllBinaries(release, args.isForce);
if (!allSucceeded) process.exitCode = 1;
}
console.log("\n---");
const files = fs.readdirSync(BIN_DIR).filter((f) => f.startsWith("whisper-server"));
if (files.length > 0) {
console.log("Available whisper-server binaries:\n");
files.forEach((f) => {
const stats = fs.statSync(path.join(BIN_DIR, f));
console.log(` - ${f} (${Math.round(stats.size / 1024 / 1024)}MB)`);
});
} else {
console.log("No binaries downloaded yet.");
console.log(`\nMake sure release exists: https://github.com/${WHISPER_CPP_REPO}/releases`);
}
}
if (require.main === module) {
main().catch(console.error);
}
module.exports = { BINARIES, WHISPER_CPP_TAG, downloadAllBinaries, isCompleteInstall };