mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
563 lines
16 KiB
JavaScript
563 lines
16 KiB
JavaScript
import { execFileSync, execSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import { createWriteStream, existsSync, rmSync } from "node:fs";
|
|
import { chmod, cp, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
import { get as httpsGet } from "node:https";
|
|
import path from "node:path";
|
|
|
|
import {
|
|
configureWithWindowsCmakeGenerator,
|
|
WINDOWS_VISUAL_STUDIO_INSTALL_DIRS,
|
|
} from "./windows-cmake-generators.mjs";
|
|
|
|
const projectRoot = process.cwd();
|
|
const whisperVersion = "v1.8.4";
|
|
const nativeRoot = path.join(projectRoot, "electron", "native");
|
|
const cacheRoot = path.join(projectRoot, ".tmp", "whisper-runtime");
|
|
const archivePath = path.join(cacheRoot, `${whisperVersion}.tar.gz`);
|
|
const extractRoot = path.join(cacheRoot, `src-${whisperVersion}`);
|
|
const windowsX64ArchivePath = path.join(cacheRoot, `${whisperVersion}-windows-x64.zip`);
|
|
const windowsX64ArchiveSha256 = "74f973345cb52ef5ba3ec9e7e7af8e48cc8c71722d1528603b80588a11f82e3e";
|
|
|
|
function getHostArch() {
|
|
return process.arch === "arm64" ? "arm64" : "x64";
|
|
}
|
|
|
|
function getNativeArchTag(platform, arch) {
|
|
if (platform === "darwin") {
|
|
return arch === "arm64" ? "darwin-arm64" : "darwin-x64";
|
|
}
|
|
|
|
if (platform === "win32") {
|
|
return arch === "arm64" ? "win32-arm64" : "win32-x64";
|
|
}
|
|
|
|
if (platform === "linux") {
|
|
return arch === "arm64" ? "linux-arm64" : "linux-x64";
|
|
}
|
|
|
|
throw new Error(`[build-whisper-runtime] Unsupported platform: ${platform}/${arch}`);
|
|
}
|
|
|
|
function getRequestedArchitectures(platform) {
|
|
const hostArch = getHostArch();
|
|
const configured = process.env.WHISPER_RUNTIME_ARCHS?.trim();
|
|
|
|
if (!configured) {
|
|
return [hostArch];
|
|
}
|
|
|
|
if (configured === "all") {
|
|
return ["arm64", "x64"];
|
|
}
|
|
|
|
const supported = new Set(["arm64", "x64"]);
|
|
const requested = configured
|
|
.split(",")
|
|
.map((entry) => entry.trim())
|
|
.filter(Boolean);
|
|
|
|
if (requested.length === 0) {
|
|
return [hostArch];
|
|
}
|
|
|
|
const invalid = requested.filter((arch) => !supported.has(arch));
|
|
if (invalid.length > 0) {
|
|
throw new Error(
|
|
`[build-whisper-runtime] Unsupported ${platform} target architecture request: ${invalid.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
return [...new Set(requested)];
|
|
}
|
|
|
|
function createDarwinTarget(arch) {
|
|
const targetArch = arch === "arm64" ? "arm64" : "x64";
|
|
const isCrossCompile = targetArch !== getHostArch();
|
|
const configureArgs = [
|
|
"-DCMAKE_BUILD_TYPE=Release",
|
|
`-DCMAKE_OSX_ARCHITECTURES=${targetArch === "arm64" ? "arm64" : "x86_64"}`,
|
|
];
|
|
|
|
if (isCrossCompile) {
|
|
configureArgs.push("-DGGML_NATIVE=OFF");
|
|
}
|
|
|
|
return {
|
|
platform: "darwin",
|
|
arch: targetArch,
|
|
archTag: getNativeArchTag("darwin", targetArch),
|
|
buildRoot: path.join(cacheRoot, `build-darwin-${targetArch}`),
|
|
outputDir: path.join(nativeRoot, "bin", getNativeArchTag("darwin", targetArch)),
|
|
configureArgs,
|
|
};
|
|
}
|
|
|
|
function getTargetConfigs() {
|
|
if (process.platform === "darwin") {
|
|
return getRequestedArchitectures("darwin").map((arch) => createDarwinTarget(arch));
|
|
}
|
|
|
|
const arch = getHostArch();
|
|
const archTag = getNativeArchTag(process.platform, arch);
|
|
|
|
if (process.platform === "win32") {
|
|
return [
|
|
{
|
|
platform: "win32",
|
|
arch,
|
|
archTag,
|
|
buildRoot: path.join(cacheRoot, `build-${archTag}`),
|
|
outputDir: path.join(nativeRoot, "bin", archTag),
|
|
configureArgs: ["-A", arch === "arm64" ? "ARM64" : "x64"],
|
|
},
|
|
];
|
|
}
|
|
|
|
if (process.platform === "linux") {
|
|
return [
|
|
{
|
|
platform: "linux",
|
|
arch,
|
|
archTag,
|
|
buildRoot: path.join(cacheRoot, `build-${archTag}`),
|
|
outputDir: path.join(nativeRoot, "bin", archTag),
|
|
configureArgs: ["-DCMAKE_BUILD_TYPE=Release"],
|
|
},
|
|
];
|
|
}
|
|
|
|
throw new Error(
|
|
`[build-whisper-runtime] Unsupported platform: ${process.platform}/${process.arch}`,
|
|
);
|
|
}
|
|
|
|
function getSourceArchiveUrl() {
|
|
return `https://github.com/ggml-org/whisper.cpp/archive/refs/tags/${whisperVersion}.tar.gz`;
|
|
}
|
|
|
|
function findCmake() {
|
|
try {
|
|
execSync("cmake --version", { stdio: "pipe" });
|
|
return "cmake";
|
|
} catch {
|
|
// not on PATH
|
|
}
|
|
|
|
if (process.platform === "win32") {
|
|
const vsEditions = ["Community", "Professional", "Enterprise", "BuildTools"];
|
|
for (const version of WINDOWS_VISUAL_STUDIO_INSTALL_DIRS) {
|
|
for (const edition of vsEditions) {
|
|
const cmakePath = path.join(
|
|
"C:",
|
|
"Program Files",
|
|
"Microsoft Visual Studio",
|
|
version,
|
|
edition,
|
|
"Common7",
|
|
"IDE",
|
|
"CommonExtensions",
|
|
"Microsoft",
|
|
"CMake",
|
|
"CMake",
|
|
"bin",
|
|
"cmake.exe",
|
|
);
|
|
if (existsSync(cmakePath)) {
|
|
return cmakePath;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function ensureTarAvailable() {
|
|
try {
|
|
execSync("tar --version", { stdio: "pipe" });
|
|
} catch {
|
|
throw new Error("[build-whisper-runtime] tar is required to unpack whisper.cpp sources.");
|
|
}
|
|
}
|
|
|
|
async function downloadFile(url, destinationPath) {
|
|
await mkdir(path.dirname(destinationPath), { recursive: true });
|
|
|
|
await new Promise((resolve, reject) => {
|
|
const request = (currentUrl, redirectCount = 0) => {
|
|
const req = httpsGet(currentUrl, (response) => {
|
|
const statusCode = response.statusCode ?? 0;
|
|
const location = response.headers.location;
|
|
|
|
if (statusCode >= 300 && statusCode < 400 && location) {
|
|
response.resume();
|
|
if (redirectCount >= 5) {
|
|
reject(
|
|
new Error(
|
|
"[build-whisper-runtime] Too many redirects while downloading whisper.cpp source.",
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
request(new URL(location, currentUrl).toString(), redirectCount + 1);
|
|
return;
|
|
}
|
|
|
|
if (statusCode < 200 || statusCode >= 300) {
|
|
response.resume();
|
|
reject(
|
|
new Error(
|
|
`[build-whisper-runtime] Failed to download whisper.cpp source: HTTP ${statusCode}`,
|
|
),
|
|
);
|
|
return;
|
|
}
|
|
|
|
const fileStream = createWriteStream(destinationPath);
|
|
fileStream.on("finish", resolve);
|
|
fileStream.on("error", reject);
|
|
response.on("error", reject);
|
|
response.pipe(fileStream);
|
|
});
|
|
|
|
req.on("error", reject);
|
|
};
|
|
|
|
request(url);
|
|
});
|
|
}
|
|
|
|
async function getFileSha256(filePath) {
|
|
return createHash("sha256")
|
|
.update(await readFile(filePath))
|
|
.digest("hex");
|
|
}
|
|
|
|
async function findDirectoryContaining(rootPath, fileName) {
|
|
const pending = [rootPath];
|
|
while (pending.length > 0) {
|
|
const currentPath = pending.shift();
|
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
if (entries.some((entry) => entry.isFile() && entry.name === fileName)) {
|
|
return currentPath;
|
|
}
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
pending.push(path.join(currentPath, entry.name));
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
async function stageWindowsX64PrebuiltRuntime(target) {
|
|
if (target.platform !== "win32" || target.arch !== "x64") {
|
|
return false;
|
|
}
|
|
|
|
await mkdir(cacheRoot, { recursive: true });
|
|
const archiveUrl = `https://github.com/ggml-org/whisper.cpp/releases/download/${whisperVersion}/whisper-bin-x64.zip`;
|
|
let archiveIsValid =
|
|
existsSync(windowsX64ArchivePath) &&
|
|
(await getFileSha256(windowsX64ArchivePath)) === windowsX64ArchiveSha256;
|
|
if (!archiveIsValid) {
|
|
await rm(windowsX64ArchivePath, { force: true });
|
|
console.log(
|
|
`[build-whisper-runtime] Downloading official whisper.cpp ${whisperVersion} Windows x64 runtime...`,
|
|
);
|
|
await downloadFile(archiveUrl, windowsX64ArchivePath);
|
|
archiveIsValid = (await getFileSha256(windowsX64ArchivePath)) === windowsX64ArchiveSha256;
|
|
}
|
|
|
|
if (!archiveIsValid) {
|
|
throw new Error(
|
|
"[build-whisper-runtime] Windows x64 runtime archive failed its SHA-256 integrity check.",
|
|
);
|
|
}
|
|
|
|
const prebuiltExtractRoot = path.join(cacheRoot, `prebuilt-${target.archTag}`);
|
|
await rm(prebuiltExtractRoot, { recursive: true, force: true });
|
|
await mkdir(prebuiltExtractRoot, { recursive: true });
|
|
execFileSync("tar", ["-xf", windowsX64ArchivePath, "-C", prebuiltExtractRoot], {
|
|
stdio: "inherit",
|
|
});
|
|
|
|
const runtimeDir = await findDirectoryContaining(prebuiltExtractRoot, "whisper-cli.exe");
|
|
if (!runtimeDir) {
|
|
throw new Error(
|
|
"[build-whisper-runtime] Official Windows archive did not contain whisper-cli.exe.",
|
|
);
|
|
}
|
|
|
|
const runtimeEntries = (await readdir(runtimeDir)).filter(
|
|
(entry) => /^(whisper|ggml)/i.test(entry) || entry.toLowerCase().endsWith(".dll"),
|
|
);
|
|
await stageRuntimeArtifacts(target, runtimeDir, runtimeEntries);
|
|
return true;
|
|
}
|
|
|
|
async function ensureSourceTree() {
|
|
const extractedSourceDir = path.join(
|
|
extractRoot,
|
|
`whisper.cpp-${whisperVersion.replace(/^v/, "")}`,
|
|
);
|
|
if (existsSync(path.join(extractedSourceDir, "CMakeLists.txt"))) {
|
|
return extractedSourceDir;
|
|
}
|
|
|
|
await rm(extractRoot, { recursive: true, force: true });
|
|
await mkdir(extractRoot, { recursive: true });
|
|
|
|
if (!existsSync(archivePath)) {
|
|
console.log(`[build-whisper-runtime] Downloading whisper.cpp ${whisperVersion} source...`);
|
|
await downloadFile(getSourceArchiveUrl(), archivePath);
|
|
}
|
|
|
|
ensureTarAvailable();
|
|
execFileSync("tar", ["-xzf", archivePath, "-C", extractRoot], { stdio: "inherit" });
|
|
|
|
if (!existsSync(path.join(extractedSourceDir, "CMakeLists.txt"))) {
|
|
throw new Error(
|
|
`[build-whisper-runtime] Extracted whisper.cpp source not found at ${extractedSourceDir}`,
|
|
);
|
|
}
|
|
|
|
return extractedSourceDir;
|
|
}
|
|
|
|
async function shouldSkipBuild(target) {
|
|
const manifestPath = path.join(target.outputDir, "whisper-runtime.json");
|
|
if (!existsSync(manifestPath)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
const binaryName = target.platform === "win32" ? "whisper-cli.exe" : "whisper-cli";
|
|
const binaryPath = path.join(target.outputDir, binaryName);
|
|
return (
|
|
manifest.version === whisperVersion &&
|
|
manifest.arch === target.arch &&
|
|
existsSync(binaryPath)
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function getConfigureArgs(sourceDir, target, generator, toolset) {
|
|
const args = [
|
|
"-S",
|
|
sourceDir,
|
|
"-B",
|
|
target.buildRoot,
|
|
"-DWHISPER_BUILD_TESTS=OFF",
|
|
"-DWHISPER_BUILD_SERVER=OFF",
|
|
"-DBUILD_SHARED_LIBS=OFF",
|
|
...(generator ? ["-G", generator] : []),
|
|
...(toolset ? ["-T", toolset] : []),
|
|
...target.configureArgs,
|
|
];
|
|
|
|
return args;
|
|
}
|
|
|
|
function getBuildArgs(target) {
|
|
const args = ["--build", target.buildRoot, "--config", "Release"];
|
|
|
|
if (target.platform !== "win32") {
|
|
args.push("--parallel");
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
async function findRuntimeArtifacts(target) {
|
|
const candidateDirs =
|
|
target.platform === "win32"
|
|
? [path.join(target.buildRoot, "bin", "Release"), path.join(target.buildRoot, "bin")]
|
|
: [path.join(target.buildRoot, "bin")];
|
|
|
|
for (const candidateDir of candidateDirs) {
|
|
if (!existsSync(candidateDir)) {
|
|
continue;
|
|
}
|
|
|
|
const entries = await readdir(candidateDir);
|
|
const runtimeEntries = entries.filter((entry) =>
|
|
/^(whisper|ggml|libwhisper|libggml)/i.test(entry),
|
|
);
|
|
if (runtimeEntries.length > 0) {
|
|
return {
|
|
candidateDir,
|
|
runtimeEntries,
|
|
};
|
|
}
|
|
}
|
|
|
|
throw new Error("[build-whisper-runtime] Built whisper runtime artifacts were not found.");
|
|
}
|
|
|
|
async function stageRuntimeArtifacts(target, candidateDir, runtimeEntries) {
|
|
await mkdir(target.outputDir, { recursive: true });
|
|
|
|
for (const entry of runtimeEntries) {
|
|
const sourcePath = path.join(candidateDir, entry);
|
|
const destinationPath = path.join(target.outputDir, entry);
|
|
const entryStats = await stat(sourcePath);
|
|
|
|
if (entryStats.isDirectory()) {
|
|
await rm(destinationPath, { recursive: true, force: true });
|
|
await cp(sourcePath, destinationPath, { recursive: true });
|
|
continue;
|
|
}
|
|
|
|
await cp(sourcePath, destinationPath, { force: true });
|
|
if (target.platform !== "win32") {
|
|
await chmod(destinationPath, 0o755).catch(() => undefined);
|
|
}
|
|
}
|
|
|
|
const manifestPath = path.join(target.outputDir, "whisper-runtime.json");
|
|
await writeFile(
|
|
manifestPath,
|
|
JSON.stringify(
|
|
{
|
|
version: whisperVersion,
|
|
platform: target.platform,
|
|
arch: target.arch,
|
|
binary: target.platform === "win32" ? "whisper-cli.exe" : "whisper-cli",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"utf8",
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
const targets = getTargetConfigs();
|
|
|
|
// Official whisper.cpp releases include a signed, portable Windows x64
|
|
// runtime. Prefer it so developers and packaged builds do not require a full
|
|
// Visual Studio C++ installation just to enable captions.
|
|
for (const target of targets) {
|
|
if (!(await shouldSkipBuild(target))) {
|
|
try {
|
|
await stageWindowsX64PrebuiltRuntime(target);
|
|
} catch (error) {
|
|
console.warn(
|
|
"[build-whisper-runtime] Failed to stage the official Windows runtime; falling back to a source build:",
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const stagedChecks = await Promise.all(targets.map((target) => shouldSkipBuild(target)));
|
|
if (stagedChecks.every(Boolean)) {
|
|
console.log(
|
|
`[build-whisper-runtime] Whisper runtime ${whisperVersion} is staged for ${targets.map((target) => target.archTag).join(", ")}.`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
const cmake = findCmake();
|
|
|
|
if (!cmake) {
|
|
// Soft-fail only when this script runs as part of `npm install`/`npm ci`,
|
|
// in CI, or when the developer explicitly opted in. Direct invocations
|
|
// (e.g. via `npm run build`, `build:win`, `build:mac`, `build:linux`)
|
|
// must still fail loudly so we never ship a release build that is
|
|
// missing the whisper runtime and silently ships broken auto-captions.
|
|
const isPostinstall = process.env.npm_lifecycle_event === "postinstall";
|
|
const isCI = process.env.CI === "true";
|
|
const allowMissing = process.env.WHISPER_RUNTIME_ALLOW_MISSING === "1";
|
|
const softFailAllowed = isPostinstall || isCI || allowMissing;
|
|
|
|
const skipChecks = await Promise.all(targets.map((target) => shouldSkipBuild(target)));
|
|
const allTargetsStaged = skipChecks.every(Boolean);
|
|
|
|
if (allTargetsStaged) {
|
|
console.log(
|
|
"[build-whisper-runtime] CMake not found; using bundled whisper runtime artifacts.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
const missing = targets
|
|
.filter((_target, index) => !skipChecks[index])
|
|
.map((target) => target.archTag)
|
|
.join(", ");
|
|
|
|
if (softFailAllowed) {
|
|
console.warn(
|
|
`[build-whisper-runtime] CMake not found and no bundled runtime is staged for: ${missing}. ` +
|
|
"Auto-caption features that rely on whisper.cpp will be unavailable until you install CMake " +
|
|
"and rerun `npm run build:whisper-runtime`.",
|
|
);
|
|
return;
|
|
}
|
|
|
|
throw new Error(
|
|
`[build-whisper-runtime] CMake is required to stage the whisper runtime for: ${missing}. ` +
|
|
"Install CMake and retry, or set WHISPER_RUNTIME_ALLOW_MISSING=1 to build without auto-caption support.",
|
|
);
|
|
}
|
|
|
|
const sourceDir = await ensureSourceTree();
|
|
|
|
console.log(
|
|
`[build-whisper-runtime] Target architectures for ${process.platform}: ${targets.map((target) => target.archTag).join(", ")}`,
|
|
);
|
|
|
|
for (const target of targets) {
|
|
if (await shouldSkipBuild(target)) {
|
|
console.log(
|
|
`[build-whisper-runtime] Whisper runtime ${whisperVersion} already staged for ${target.archTag}.`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
await mkdir(target.buildRoot, { recursive: true });
|
|
|
|
console.log(
|
|
`[build-whisper-runtime] Configuring whisper.cpp ${whisperVersion} for ${target.archTag}...`,
|
|
);
|
|
if (target.platform === "win32") {
|
|
configureWithWindowsCmakeGenerator({
|
|
prefix: "build-whisper-runtime",
|
|
clearCache: () => {
|
|
rmSync(path.join(target.buildRoot, "CMakeCache.txt"), { force: true });
|
|
rmSync(path.join(target.buildRoot, "CMakeFiles"), {
|
|
recursive: true,
|
|
force: true,
|
|
});
|
|
},
|
|
configure: (generator, toolset) =>
|
|
execFileSync(cmake, getConfigureArgs(sourceDir, target, generator, toolset), {
|
|
stdio: "inherit",
|
|
timeout: 300000,
|
|
}),
|
|
});
|
|
} else {
|
|
execFileSync(cmake, getConfigureArgs(sourceDir, target), {
|
|
stdio: "inherit",
|
|
timeout: 300000,
|
|
});
|
|
}
|
|
|
|
console.log(
|
|
`[build-whisper-runtime] Building bundled whisper runtime for ${target.archTag}...`,
|
|
);
|
|
execFileSync(cmake, getBuildArgs(target), { stdio: "inherit", timeout: 900000 });
|
|
|
|
const { candidateDir, runtimeEntries } = await findRuntimeArtifacts(target);
|
|
await stageRuntimeArtifacts(target, candidateDir, runtimeEntries);
|
|
console.log(`[build-whisper-runtime] Staged whisper runtime -> ${target.outputDir}`);
|
|
}
|
|
}
|
|
|
|
await main();
|