Files
Recordly/scripts/build-whisper-runtime.mjs
webadderall 9a5ff5c0a2 feat: ui overhaul — Phosphor icons, redesigned launcher, editor layout, and component refresh
- Replace all lucide-react imports with @phosphor-icons/react across every
  component (LaunchWindow, AnnotationSettingsPanel, ExtensionManager,
  ExtensionIcon, VideoPlayback, AnnotationOverlay, CropControl, TutorialHelp,
  PlaybackControls, all shadcn ui/ primitives, etc.)
- ExtensionIcon: new Phosphor-based resolver with Lucide name alias map for
  backwards-compatible extension manifests
- LaunchWindow / SourceSelector: full redesign — new icon rail layout,
  updated backgrounds, improved source selection UX, audio/video controls,
  countdown overlay
- VideoPlayback: overhaul playback controls and preview panel layout
- Timeline items: border-radius 16px→8px, cyan→blue unification, pill height
  85% for breathing room, resize handle vertical centering
- shadcn ui/: refresh accordion, dialog, dropdown, select, slider, switch,
  tabs, toggle, popover, button, card, input, label, sonner, content-clamp,
  audio-level-meter
- App.tsx / index.css / App.css / tailwind.config: global style updates
- types.ts, captionLayout.ts, captionStyle.ts, webcamOverlay.ts: data model
  updates to support new timeline and export features
- package.json: add @phosphor-icons/react, bump dependencies
- scripts/, .eslintrc, biome, tsconfig, vite.config: tooling alignment
- extension-sources: update built-in extension bundles
2026-04-16 18:01:04 +10:00

435 lines
11 KiB
JavaScript

import { execFileSync, execSync } from "node:child_process";
import { createWriteStream, existsSync } 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";
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}`);
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: [
"-G",
"Visual Studio 17 2022",
"-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 edition of vsEditions) {
const cmakePath = path.join(
"C:",
"Program Files",
"Microsoft Visual Studio",
"2022",
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 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) {
const args = [
"-S",
sourceDir,
"-B",
target.buildRoot,
"-DWHISPER_BUILD_TESTS=OFF",
"-DWHISPER_BUILD_SERVER=OFF",
"-DBUILD_SHARED_LIBS=OFF",
...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 cmake = findCmake();
if (!cmake) {
throw new Error(
"[build-whisper-runtime] CMake is required to build the bundled Whisper runtime.",
);
}
const sourceDir = await ensureSourceTree();
const targets = getTargetConfigs();
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}...`,
);
try {
execFileSync(cmake, getConfigureArgs(sourceDir, target), {
stdio: "inherit",
timeout: 300000,
});
} catch (error) {
if (target.platform === "win32" && target.arch !== "arm64") {
console.log(
"[build-whisper-runtime] VS 2022 generator unavailable, retrying with VS 2019...",
);
execFileSync(
cmake,
[
"-S",
sourceDir,
"-B",
target.buildRoot,
"-G",
"Visual Studio 16 2019",
"-A",
"x64",
"-DWHISPER_BUILD_TESTS=OFF",
"-DWHISPER_BUILD_SERVER=OFF",
"-DBUILD_SHARED_LIBS=OFF",
],
{ stdio: "inherit", timeout: 300000 },
);
} else {
throw error;
}
}
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();