UNPKG

swictation

Version:

Cross-platform voice-to-text dictation for Linux and macOS with GPU acceleration (NVIDIA CUDA/CoreML), Secretary Mode (60+ natural language commands), Context-Aware Meta-Learning, and pure Rust performance. Meta-package that automatically installs platfor

1,372 lines (1,191 loc) 117 kB
#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); const os = require('os'); const https = require('https'); const crypto = require('crypto'); const { checkNvidiaHibernationStatus } = require('./src/nvidia-hibernation-setup'); const { getIpcSocketPath } = require('./src/socket-paths'); // Environment variable support for model test-loading // By default, model testing runs when GPU is detected // Set SKIP_MODEL_TEST=1 to disable (useful for CI/headless environments) const SKIP_MODEL_TEST = process.env.SKIP_MODEL_TEST === '1'; // Colors for console output (basic implementation without chalk dependency) const colors = { reset: '\x1b[0m', green: '\x1b[32m', yellow: '\x1b[33m', cyan: '\x1b[36m', red: '\x1b[31m' }; function log(color, message) { console.log(`${colors[color]}${message}${colors.reset}`); } function checkPlatform() { const platform = process.platform; const arch = process.arch; // Support both Linux and macOS if (platform === 'linux') { // Linux-specific checks if (arch !== 'x64') { log('yellow', 'Note: Swictation on Linux currently only supports x64 architecture'); process.exit(0); } // Check GLIBC version try { const glibcVersion = execSync('ldd --version 2>&1 | head -1', { encoding: 'utf8' }); const versionMatch = glibcVersion.match(/(\d+)\.(\d+)/); if (versionMatch) { const major = parseInt(versionMatch[1]); const minor = parseInt(versionMatch[2]); if (major < 2 || (major === 2 && minor < 39)) { log('red', '\n⚠ INCOMPATIBLE GLIBC VERSION'); log('yellow', `Detected GLIBC ${major}.${minor} (need 2.39+)`); log('yellow', 'Swictation requires Ubuntu 24.04 LTS or newer'); log('yellow', 'Ubuntu 22.04 is NOT supported due to GLIBC 2.35'); log('yellow', '\nSupported distributions:'); log('cyan', ' - Ubuntu 24.04 LTS (Noble Numbat) or newer'); log('cyan', ' - Debian 13+ (Trixie)'); log('cyan', ' - Fedora 39+'); log('yellow', '\nInstallation will continue but binaries may not work.'); } } } catch (err) { log('yellow', 'Warning: Could not check GLIBC version'); } } else if (platform === 'darwin') { // macOS-specific checks if (arch !== 'arm64') { log('red', '\n⚠ UNSUPPORTED ARCHITECTURE'); log('yellow', `Detected architecture: ${arch}`); log('yellow', 'Swictation on macOS requires Apple Silicon (M1/M2/M3/M4)'); log('yellow', 'Intel Macs are not supported'); process.exit(1); } // Check macOS version (require Sonoma 14.0+ or Sequoia 15.0+) try { const osVersion = execSync('sw_vers -productVersion', { encoding: 'utf8' }).trim(); const versionMatch = osVersion.match(/(\d+)\.(\d+)/); if (versionMatch) { const major = parseInt(versionMatch[1]); const minor = parseInt(versionMatch[2]); if (major < 14) { log('red', '\n⚠ UNSUPPORTED MACOS VERSION'); log('yellow', `Detected macOS ${osVersion}`); log('yellow', 'Swictation requires macOS 14.0 (Sonoma) or newer'); log('yellow', '\nSupported versions:'); log('cyan', ' - macOS 14.x (Sonoma)'); log('cyan', ' - macOS 15.x (Sequoia)'); log('yellow', '\nInstallation will continue but may not work correctly.'); } else { log('green', `✓ macOS ${osVersion} (Apple Silicon)`); } } } catch (err) { log('yellow', 'Warning: Could not check macOS version'); } } else { log('yellow', `Note: Swictation currently only supports Linux and macOS`); log('yellow', `Detected platform: ${platform}`); log('yellow', 'Skipping postinstall for unsupported platform'); process.exit(0); } } /** * Phase 1: Clean up old/conflicting service files from previous installations * This prevents conflicts between old Python-based services and new Node.js services */ /** * Stop currently running services before upgrade to prevent CUDA state corruption * This must run BEFORE any file modifications happen */ async function stopExistingServices() { log('cyan', '\n🛑 Stopping currently running services...'); let stopped = false; try { // Method 1: Try using swictation CLI if available try { execSync('which swictation 2>/dev/null', { stdio: 'ignore' }); execSync('swictation stop 2>/dev/null', { stdio: 'ignore' }); log('green', '✓ Stopped swictation services via CLI'); stopped = true; // Give services time to fully stop and release CUDA await new Promise(resolve => setTimeout(resolve, 2000)); } catch (cliErr) { // swictation CLI not available, try systemctl try { execSync('systemctl --user stop swictation-daemon.service 2>/dev/null', { stdio: 'ignore' }); execSync('systemctl --user stop swictation-ui.service 2>/dev/null', { stdio: 'ignore' }); log('green', '✓ Stopped services via systemctl'); stopped = true; await new Promise(resolve => setTimeout(resolve, 2000)); } catch (systemctlErr) { log('cyan', 'ℹ No existing services to stop'); } } } catch (err) { log('cyan', 'ℹ No existing services to stop'); } return stopped; } /** * Clean up old ONNX Runtime libraries from Python pip installations * These cause version conflicts (1.20.1 vs 1.22.x) */ function cleanupOldOnnxRuntime() { log('cyan', '\n🧹 Checking for old ONNX Runtime libraries...'); try { const homeDir = os.homedir(); const pythonLibDirs = [ path.join(homeDir, '.local', 'lib', 'python3.13', 'site-packages', 'onnxruntime'), path.join(homeDir, '.local', 'lib', 'python3.12', 'site-packages', 'onnxruntime'), path.join(homeDir, '.local', 'lib', 'python3.11', 'site-packages', 'onnxruntime'), path.join(homeDir, '.local', 'lib', 'python3.10', 'site-packages', 'onnxruntime'), ]; let removedAny = false; for (const ortDir of pythonLibDirs) { if (fs.existsSync(ortDir)) { try { // Check if it's an old version that conflicts const capiDir = path.join(ortDir, 'capi'); if (fs.existsSync(capiDir)) { const ortFiles = fs.readdirSync(capiDir).filter(f => f.includes('libonnxruntime.so')); if (ortFiles.length > 0 && ortFiles[0].includes('1.20')) { log('yellow', `⚠️ Found old ONNX Runtime 1.20.x at ${ortDir}`); log('cyan', ` Removing to prevent version conflicts...`); execSync(`rm -rf "${ortDir}"`, { stdio: 'ignore' }); log('green', `✓ Removed old ONNX Runtime installation`); removedAny = true; } } } catch (err) { // Can't determine version, leave it alone } } } if (!removedAny) { log('green', '✓ No conflicting ONNX Runtime installations found'); } } catch (err) { log('yellow', `⚠️ Error checking ONNX Runtime installations: ${err.message}`); } } /** * Remove old npm installations that conflict with new installation * Handles both system-wide and nvm installations */ function cleanupOldNpmInstallations() { log('cyan', '\n🧹 Checking for old npm installations...'); const oldInstallPaths = [ '/usr/local/lib/node_modules/swictation', '/usr/local/nodejs/lib/node_modules/swictation', '/usr/lib/node_modules/swictation', ]; let removedAny = false; for (const oldPath of oldInstallPaths) { if (fs.existsSync(oldPath) && oldPath !== __dirname) { log('yellow', `⚠️ Found old npm installation at ${oldPath}`); log('cyan', ` Removing to prevent conflicts...`); try { execSync(`sudo rm -rf "${oldPath}" 2>/dev/null || rm -rf "${oldPath}"`, { stdio: 'ignore' }); log('green', `✓ Removed old installation`); removedAny = true; } catch (err) { log('yellow', `⚠️ Could not remove ${oldPath}: ${err.message}`); log('yellow', ` You may need to run: sudo rm -rf "${oldPath}"`); } } } if (!removedAny) { log('green', '✓ No conflicting npm installations found'); } } async function cleanOldServices() { log('cyan', '\n🧹 Checking for old service files...'); const oldServiceLocations = [ // Old system-wide service files (from Python version) '/usr/lib/systemd/user/swictation.service', '/usr/lib/systemd/system/swictation.service', // Old user service files that might conflict path.join(os.homedir(), '.config', 'systemd', 'user', 'swictation.service') ]; let foundOldServices = false; for (const servicePath of oldServiceLocations) { if (fs.existsSync(servicePath)) { foundOldServices = true; log('yellow', `⚠️ Found old service file: ${servicePath}`); // Extract service name from path const serviceName = path.basename(servicePath); const isSystemService = servicePath.includes('/system/'); try { // Try to stop the service if it's running const stopCmd = isSystemService ? `sudo systemctl stop ${serviceName} 2>/dev/null || true` : `systemctl --user stop ${serviceName} 2>/dev/null || true`; execSync(stopCmd, { stdio: 'ignore' }); log('cyan', ` ✓ Stopped service: ${serviceName}`); // Disable the service const disableCmd = isSystemService ? `sudo systemctl disable ${serviceName} 2>/dev/null || true` : `systemctl --user disable ${serviceName} 2>/dev/null || true`; execSync(disableCmd, { stdio: 'ignore' }); log('cyan', ` ✓ Disabled service: ${serviceName}`); // Remove the service file (requires sudo for system services) if (isSystemService) { try { execSync(`sudo rm -f "${servicePath}"`, { stdio: 'ignore' }); log('green', ` ✓ Removed old service file: ${servicePath}`); } catch (err) { log('yellow', ` ⚠️ Could not remove ${servicePath} (may need manual cleanup)`); } } else { try { fs.unlinkSync(servicePath); log('green', ` ✓ Removed old service file: ${servicePath}`); } catch (err) { log('yellow', ` ⚠️ Could not remove ${servicePath}: ${err.message}`); } } } catch (err) { log('yellow', ` ⚠️ Error cleaning up ${serviceName}: ${err.message}`); } } } if (foundOldServices) { // Reload systemd to pick up changes try { execSync('systemctl --user daemon-reload 2>/dev/null', { stdio: 'ignore' }); execSync('sudo systemctl daemon-reload 2>/dev/null || true', { stdio: 'ignore' }); log('green', '✓ Reloaded systemd daemon'); } catch (err) { log('yellow', '⚠️ Could not reload systemd daemon'); } } else { log('green', '✓ No old service files found'); } return foundOldServices; } function ensureBinaryPermissions() { const binaries = []; // CLI wrapper in main package const cliBinary = path.join(__dirname, 'bin', 'swictation'); if (fs.existsSync(cliBinary)) { binaries.push(cliBinary); } // Platform package binaries try { const { resolveBinaryPaths } = require('./src/resolve-binary'); const binaryPaths = resolveBinaryPaths(); // Add daemon and UI from platform package if (fs.existsSync(binaryPaths.daemon)) { binaries.push(binaryPaths.daemon); } if (fs.existsSync(binaryPaths.ui)) { binaries.push(binaryPaths.ui); } } catch (err) { // Platform package not installed yet - skip platform binaries log('yellow', ` ⚠️ Platform package binaries not found (will be checked later)`); } // Legacy binaries (if they exist from old installations) const legacyBinaries = [ path.join(__dirname, 'lib', 'native', 'swictation-daemon.bin'), path.join(__dirname, 'bin', 'swictation-daemon'), path.join(__dirname, 'bin', 'swictation-ui') ]; for (const binary of legacyBinaries) { if (fs.existsSync(binary)) { binaries.push(binary); } } // Set execute permissions for (const binary of binaries) { try { fs.chmodSync(binary, '755'); log('green', `✓ Set execute permissions for ${path.basename(binary)}`); } catch (err) { log('yellow', `Warning: Could not set permissions for ${path.basename(binary)}: ${err.message}`); } } } function createDirectories() { const dirs = [ path.join(os.homedir(), '.config', 'swictation'), path.join(os.homedir(), '.local', 'share', 'swictation'), path.join(os.homedir(), '.local', 'share', 'swictation', 'models'), path.join(os.homedir(), '.cache', 'swictation') ]; for (const dir of dirs) { if (!fs.existsSync(dir)) { try { fs.mkdirSync(dir, { recursive: true }); log('green', `✓ Created directory: ${dir}`); } catch (err) { log('yellow', `Warning: Could not create ${dir}: ${err.message}`); } } } } /** * Detect system package manager * @returns {object} Package manager info with install command */ function detectPackageManager() { const managers = [ { cmd: 'apt', name: 'apt', installCmd: 'sudo apt update && sudo apt install -y' }, { cmd: 'dnf', name: 'dnf', installCmd: 'sudo dnf install -y' }, { cmd: 'pacman', name: 'pacman', installCmd: 'sudo pacman -S --noconfirm' }, { cmd: 'zypper', name: 'zypper', installCmd: 'sudo zypper install -y' } ]; for (const manager of managers) { try { execSync(`which ${manager.cmd}`, { stdio: 'ignore' }); return manager; } catch { // Try next manager } } return null; } /** * Install a package using the system package manager * @param {string} packageName - Package name to install * @param {string} displayName - Display name for logging * @returns {boolean} Success status */ function installPackage(packageName, displayName) { const pkgManager = detectPackageManager(); if (!pkgManager) { log('yellow', ` ⚠️ No supported package manager found (apt/dnf/pacman/zypper)`); log('cyan', ` Please install ${displayName} manually`); return false; } log('cyan', ` Installing ${displayName} via ${pkgManager.name}...`); try { execSync(`${pkgManager.installCmd} ${packageName}`, { stdio: 'inherit' }); log('green', ` ✓ ${displayName} installed successfully`); return true; } catch (err) { log('yellow', ` ⚠️ Failed to install ${displayName}: ${err.message}`); log('cyan', ` Install manually: ${pkgManager.installCmd} ${packageName}`); return false; } } function checkDependencies() { const optional = []; const required = []; // Check for required tools const tools = [ { name: 'systemctl', type: 'optional', package: 'systemd' }, { name: 'nc', type: 'optional', package: 'netcat' }, { name: 'wtype', type: 'optional', package: 'wtype (for Wayland)' }, { name: 'xdotool', type: 'optional', package: 'xdotool (for X11)' }, { name: 'hf', type: 'optional', package: 'huggingface_hub[cli] (pip install huggingface_hub[cli])' } ]; for (const tool of tools) { try { execSync(`which ${tool.name}`, { stdio: 'ignore' }); } catch { if (tool.type === 'required') { required.push(tool); } else { optional.push(tool); } } } if (required.length > 0) { log('red', '\n⚠ Required dependencies missing:'); for (const tool of required) { log('yellow', ` - ${tool.name} (install: ${tool.package})`); } log('red', '\nPlease install required dependencies before using Swictation'); process.exit(1); } if (optional.length > 0) { log('yellow', '\n📦 Optional dependencies for full functionality:'); for (const tool of optional) { log('cyan', ` - ${tool.name} (${tool.package})`); } } } function detectNvidiaGPU() { try { execSync('nvidia-smi', { stdio: 'ignore' }); return true; } catch { return false; } } /** * Detect GPU compute capability (sm_XX architecture) * Returns { hasGPU: boolean, computeCap: string, smVersion: number } * @returns {object} GPU compute capability info */ function detectGPUComputeCapability() { const result = { hasGPU: false, computeCap: null, // e.g., "5.2", "8.6", "12.0" smVersion: null, // e.g., 52, 86, 120 gpuName: null }; if (!detectNvidiaGPU()) { return result; } result.hasGPU = true; try { // Get compute capability and GPU name const output = execSync( 'nvidia-smi --query-gpu=compute_cap,name --format=csv,noheader', { encoding: 'utf8' } ).trim(); const [computeCap, gpuName] = output.split(',').map(s => s.trim()); result.computeCap = computeCap; result.gpuName = gpuName; // Convert "5.2" -> 52, "8.6" -> 86, "12.0" -> 120 const [major, minor] = computeCap.split('.').map(n => parseInt(n)); result.smVersion = major * 10 + minor; log('green', `✓ Detected GPU: ${gpuName}`); log('cyan', ` Compute Capability: ${computeCap} (sm_${result.smVersion})`); } catch (err) { log('yellow', `⚠️ Could not detect compute capability: ${err.message}`); } return result; } /** * Select appropriate GPU library package variant based on compute capability * @param {number} smVersion - Compute capability as integer (e.g., 52, 86, 120) * @returns {object} Package variant info */ function selectGPUPackageVariant(smVersion) { // Architecture mapping based on RELEASE_NOTES.md if (smVersion >= 50 && smVersion <= 70) { // sm_50-70: Maxwell, Pascal, Volta (2014-2017) return { variant: 'legacy', architectures: 'sm_50-70', description: 'Maxwell, Pascal, Volta GPUs (2014-2017)', examples: 'GTX 750/900/1000, Quadro M/P series, Titan V, V100' }; } else if (smVersion >= 75 && smVersion <= 86) { // sm_75-86: Turing, Ampere (2018-2021) return { variant: 'modern', architectures: 'sm_75-86', description: 'Turing, Ampere GPUs (2018-2021)', examples: 'GTX 16/RTX 20/30 series, A100, RTX A1000-A6000' }; } else if (smVersion >= 89 && smVersion <= 121) { // sm_89-120: Ada Lovelace, Hopper, Blackwell (2022-2024) return { variant: 'latest', architectures: 'sm_89-120', description: 'Ada Lovelace, Hopper, Blackwell GPUs (2022-2024)', examples: 'RTX 4090, H100, B100/B200, RTX PRO 6000 Blackwell, RTX 50 series' }; } else { // Unsupported architecture return { variant: null, architectures: `sm_${smVersion}`, description: 'Unsupported GPU architecture', examples: 'GPU too old (<sm_50) or unknown architecture' }; } } /** * Detect CUDA and cuDNN library paths dynamically * Returns an array of directories to include in LD_LIBRARY_PATH * Now includes ~/.local/share/swictation/gpu-libs as PRIMARY source */ /** * Detect actual npm installation path (handles nvm vs system-wide) * This is critical for service files to find the correct libraries */ function detectActualNpmInstallPath() { // __dirname is where this script is running from // For system-wide: /usr/local/lib/node_modules/swictation // For nvm: /home/user/.nvm/versions/node/vX.Y.Z/lib/node_modules/swictation // Return the actual installation directory return __dirname; } /** * Detect where npm global packages are installed * This helps find the native library path for LD_LIBRARY_PATH */ function detectNpmNativeLibPath() { const installDir = detectActualNpmInstallPath(); return path.join(installDir, 'lib', 'native'); } function detectCudaLibraryPaths() { const paths = []; // PRIORITY 1: User's GPU libs directory (our multi-architecture packages) const gpuLibsDir = path.join(os.homedir(), '.local', 'share', 'swictation', 'gpu-libs'); if (fs.existsSync(gpuLibsDir)) { paths.push(gpuLibsDir); } // PRIORITY 2: Check common CUDA installation directories (system-wide fallback) const cudaDirs = [ '/usr/local/cuda/lib64', '/usr/local/cuda/lib', '/usr/local/cuda-13/lib64', '/usr/local/cuda-13/lib', '/usr/local/cuda-12.9/lib64', '/usr/local/cuda-12.9/lib', '/usr/local/cuda-12/lib64', '/usr/local/cuda-12/lib', ]; // Find directories that contain cuDNN or CUDA runtime for (const dir of cudaDirs) { try { if (fs.existsSync(dir)) { const files = fs.readdirSync(dir); // Check for cuDNN or CUDA runtime libraries if (files.some(f => f.startsWith('libcudnn.so') || f.startsWith('libcudart.so'))) { if (!paths.includes(dir)) { paths.push(dir); } } } } catch (err) { // Ignore errors from directories we can't read } } return paths; } async function downloadFile(url, dest) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(dest); https.get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { // Follow redirect https.get(response.headers.location, (redirectResponse) => { redirectResponse.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }).on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); } else { response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); } }).on('error', (err) => { fs.unlink(dest, () => {}); reject(err); }); }); } /** * Verify SHA256 checksum of a downloaded file * @param {string} filePath - Path to file to verify * @param {string} expectedChecksum - Expected SHA256 hash (lowercase hex) * @returns {boolean} - True if checksum matches */ function verifyChecksum(filePath, expectedChecksum) { const fileBuffer = fs.readFileSync(filePath); const hashSum = crypto.createHash('sha256'); hashSum.update(fileBuffer); const actualChecksum = hashSum.digest('hex'); return actualChecksum === expectedChecksum.toLowerCase(); } /** * Get SHA256 checksum of a file (for debugging/generating checksums) * @param {string} filePath - Path to file * @returns {string} - SHA256 hash as lowercase hex */ function getFileChecksum(filePath) { const fileBuffer = fs.readFileSync(filePath); const hashSum = crypto.createHash('sha256'); hashSum.update(fileBuffer); return hashSum.digest('hex'); } // SHA256 checksums for macOS release binaries (SECURITY: verify integrity of downloads) const MACOS_CHECKSUMS = { // Daemon v0.7.4 - ARM64 macOS binary 'daemon-0.7.4': 'd992f8c424448eedbc902d81377d1079b7af62798a1e29fe089895838c7ab6b7', // UI v0.1.0 - ARM64 macOS Tauri app 'ui-0.1.0': '5ce09af9942c5b11683380621fce2937ea1e2beefa3842a8ec6a1a698ce6b319', }; /** * Load expected checksums from checksums.txt * @returns {Map<string, string>} Map of filename -> sha512 hash */ function loadChecksums() { const checksumsPath = path.join(__dirname, 'checksums.txt'); if (!fs.existsSync(checksumsPath)) { throw new Error('checksums.txt not found - package may be corrupted'); } const content = fs.readFileSync(checksumsPath, 'utf8'); const checksums = new Map(); for (const line of content.split('\n')) { // Skip comments and empty lines if (line.trim().startsWith('#') || line.trim() === '') { continue; } // Parse "hash filename" format const match = line.match(/^([a-f0-9]{128})\s+(.+)$/); if (match) { const [, hash, filename] = match; checksums.set(filename, hash); } } return checksums; } /** * Calculate SHA-512 checksum of a file * @param {string} filePath - Path to file * @returns {Promise<string>} SHA-512 hash in hex format */ function calculateChecksum(filePath) { return new Promise((resolve, reject) => { const hash = crypto.createHash('sha512'); const stream = fs.createReadStream(filePath); stream.on('data', (chunk) => { hash.update(chunk); }); stream.on('end', () => { resolve(hash.digest('hex')); }); stream.on('error', (err) => { reject(err); }); }); } /** * Verify downloaded file checksum matches expected value * @param {string} filePath - Path to downloaded file * @param {string} filename - Original filename for lookup * @param {Map<string, string>} checksums - Expected checksums map * @throws {Error} If checksum doesn't match or file is missing from checksums */ async function verifyChecksum(filePath, filename, checksums) { const expectedChecksum = checksums.get(filename); if (!expectedChecksum) { throw new Error(`No checksum found for ${filename} - package may be corrupted`); } log('cyan', ' Verifying file integrity...'); const actualChecksum = await calculateChecksum(filePath); if (actualChecksum !== expectedChecksum) { throw new Error( `SECURITY: Checksum mismatch for ${filename}!\n` + ` Expected: ${expectedChecksum}\n` + ` Actual: ${actualChecksum}\n` + ` This could indicate a corrupted download or supply chain attack.\n` + ` DO NOT extract this file. Please report this issue.` ); } log('green', ` ✓ Checksum verified (SHA-512)`); } async function downloadGPULibraries() { const hasGPU = detectNvidiaGPU(); if (!hasGPU) { log('cyan', '\nℹ No NVIDIA GPU detected - skipping GPU library download'); log('cyan', ' CPU-only mode will be used'); return; } log('green', '\n✓ NVIDIA GPU detected!'); log('cyan', '📦 Detecting GPU architecture and downloading optimized libraries...\n'); // Get platform package lib directory for GPU libraries const { resolveBinaryPaths } = require('./src/resolve-binary'); let gpuLibsDir; try { const binaryPaths = resolveBinaryPaths(); gpuLibsDir = binaryPaths.libDir; log('cyan', ` GPU libraries will be installed to platform package: ${gpuLibsDir}\n`); } catch (err) { log('red', ' ✗ Platform package not found - cannot install GPU libraries'); log('cyan', ' This should not happen as platform package was verified earlier'); throw new Error('Platform package lib directory not found'); } // Detect GPU compute capability const gpuInfo = detectGPUComputeCapability(); if (!gpuInfo.smVersion) { log('yellow', '⚠️ Could not detect GPU compute capability'); log('cyan', ' Skipping GPU library download'); log('cyan', ' You can manually download from:'); log('cyan', ' https://github.com/robertelee78/swictation/releases/tag/gpu-libs-v1.2.0'); return; } // Select appropriate package variant const packageInfo = selectGPUPackageVariant(gpuInfo.smVersion); if (!packageInfo.variant) { log('yellow', `⚠️ GPU architecture ${packageInfo.architectures} is not supported`); log('cyan', ` ${packageInfo.description}`); log('cyan', ' Supported architectures: sm_50 through sm_121'); log('cyan', ' Your GPU may be too old or require a newer ONNX Runtime build'); return; } log('cyan', `📦 Selected Package: ${packageInfo.variant.toUpperCase()}`); log('cyan', ` Architectures: ${packageInfo.architectures}`); log('cyan', ` Description: ${packageInfo.description}`); log('cyan', ` Examples: ${packageInfo.examples}\n`); // GPU libs v1.1.0: Multi-architecture CUDA support (sm_50-120) // ONNX Runtime 1.23.2, CUDA 12.9, cuDNN 9.15.1 const GPU_LIBS_VERSION = '1.2.0'; const variant = packageInfo.variant; const releaseUrl = `https://github.com/robertelee78/swictation/releases/download/gpu-libs-v${GPU_LIBS_VERSION}/cuda-libs-${variant}.tar.gz`; const tmpDir = path.join(os.tmpdir(), 'swictation-gpu-install'); const tarPath = path.join(tmpDir, `cuda-libs-${variant}.tar.gz`); try { // Load checksums for verification let checksums; try { checksums = loadChecksums(); log('green', ' ✓ Loaded integrity checksums'); } catch (err) { log('red', ` ✗ Failed to load checksums: ${err.message}`); throw new Error('Cannot proceed without checksums - package integrity cannot be verified'); } // Create directories if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } if (!fs.existsSync(gpuLibsDir)) { fs.mkdirSync(gpuLibsDir, { recursive: true }); } // Check if GPU libs are already installed by checking metadata file const configDir = path.join(os.homedir(), '.config', 'swictation'); const gpuPackageInfoPath = path.join(configDir, 'gpu-package-info.json'); let skipDownload = false; if (fs.existsSync(gpuPackageInfoPath)) { try { const existingMetadata = JSON.parse(fs.readFileSync(gpuPackageInfoPath, 'utf8')); if (existingMetadata.version === GPU_LIBS_VERSION && existingMetadata.variant === variant) { skipDownload = true; log('green', ` ✓ GPU libraries v${GPU_LIBS_VERSION} (${variant}) already installed`); log('cyan', ` Location: ${gpuLibsDir}`); log('cyan', ` Installed: ${existingMetadata.installedAt}`); log('cyan', ` Skipping download to save time and bandwidth`); } } catch (err) { log('yellow', ` Warning: Could not read GPU package metadata: ${err.message}`); } } if (!skipDownload) { // Download tarball log('cyan', ` Downloading ${variant} package...`); log('cyan', ` URL: ${releaseUrl}`); await downloadFile(releaseUrl, tarPath); log('green', ` ✓ Downloaded ${variant} package (~1.5GB)`); // Verify cryptographic checksum before extraction const filename = `cuda-libs-${variant}.tar.gz`; try { await verifyChecksum(tarPath, filename, checksums); } catch (err) { // Delete potentially malicious file fs.unlinkSync(tarPath); throw err; } // Extract tarball to gpu-libs directory log('cyan', ' Extracting libraries...'); execSync(`tar -xzf "${tarPath}" -C "${tmpDir}"`, { stdio: 'inherit' }); // Move libraries from extracted ${variant}/libs/ to gpu-libs directory const extractedLibsDir = path.join(tmpDir, variant, 'libs'); if (fs.existsSync(extractedLibsDir)) { const libFiles = fs.readdirSync(extractedLibsDir); for (const file of libFiles) { const srcPath = path.join(extractedLibsDir, file); const destPath = path.join(gpuLibsDir, file); fs.copyFileSync(srcPath, destPath); } log('green', ` ✓ Extracted ${libFiles.length} libraries to ${gpuLibsDir}`); } else { throw new Error(`Expected directory not found: ${extractedLibsDir}`); } // Cleanup fs.unlinkSync(tarPath); execSync(`rm -rf "${path.join(tmpDir, variant)}"`, { stdio: 'ignore' }); // Save GPU package info for systemd service generation const packageMetadata = { variant: packageInfo.variant, architectures: packageInfo.architectures, smVersion: gpuInfo.smVersion, computeCap: gpuInfo.computeCap, gpuName: gpuInfo.gpuName, version: GPU_LIBS_VERSION, libsPath: gpuLibsDir, installedAt: new Date().toISOString() }; try { fs.writeFileSync(gpuPackageInfoPath, JSON.stringify(packageMetadata, null, 2)); log('green', ` ✓ Saved package metadata to ${gpuPackageInfoPath}`); } catch (err) { log('yellow', ` ⚠️ Could not save package metadata: ${err.message}`); } } log('green', '\n✅ GPU acceleration enabled!'); log('cyan', ` Architecture: ${packageInfo.architectures}`); log('cyan', ` Libraries: ${gpuLibsDir}`); log('cyan', ' Your system will use CUDA for faster transcription\n'); } catch (err) { log('yellow', `\n⚠️ Failed to download GPU libraries: ${err.message}`); log('cyan', ' Continuing with CPU-only mode'); log('cyan', ' You can manually download from:'); log('cyan', ` ${releaseUrl}`); log('cyan', '\n Manual installation:'); log('cyan', ` 1. Download: curl -L -o /tmp/cuda-libs-${variant}.tar.gz ${releaseUrl}`); log('cyan', ` 2. Extract: tar -xzf /tmp/cuda-libs-${variant}.tar.gz -C /tmp`); log('cyan', ` 3. Install: cp /tmp/${variant}/libs/*.so ${gpuLibsDir}/`); } } /** * Download ONNX Runtime CoreML dylib for macOS * CoreML is Apple's GPU acceleration framework for neural networks */ async function downloadONNXRuntimeCoreML() { log('cyan', '\n📦 Setting up ONNX Runtime CoreML library for macOS...'); // Version info - must match build-macos-release.sh expectations // NOTE: Using 1.22.0 due to ORT 1.23.x regression with external data + CoreML // See: https://github.com/microsoft/onnxruntime/issues/26261 // TODO: Upgrade to 1.23.x+ when fix (PR #26263) is released const ORT_VERSION = '1.22.0'; // ONNX Runtime version with CoreML support const releaseUrl = `https://github.com/robertelee78/swictation/releases/download/onnx-runtime-macos-v${ORT_VERSION}/libonnxruntime.dylib`; const tmpDir = path.join(os.tmpdir(), 'swictation-macos-install'); const dylibPath = path.join(tmpDir, 'libonnxruntime.dylib'); // Target directory in npm package const nativeDir = path.join(__dirname, 'lib', 'native'); const targetDylibPath = path.join(nativeDir, 'libonnxruntime.dylib'); try { // Check if already downloaded if (fs.existsSync(targetDylibPath)) { log('green', ` ✓ ONNX Runtime CoreML dylib already present`); log('cyan', ` Location: ${targetDylibPath}`); log('cyan', ` Skipping download`); return; } // Check for pre-signed library from platform package (CI-signed with Developer ID) // This is critical for macOS hardened runtime compatibility const platformPkgLib = path.join(__dirname, 'node_modules', '@agidreams', 'darwin-arm64', 'lib', 'libonnxruntime.dylib'); if (fs.existsSync(platformPkgLib)) { log('green', ` ✓ Found Developer ID signed ONNX Runtime from platform package`); log('cyan', ` Source: ${platformPkgLib}`); // Create target directory if needed if (!fs.existsSync(nativeDir)) { fs.mkdirSync(nativeDir, { recursive: true }); } // Copy the pre-signed library fs.copyFileSync(platformPkgLib, targetDylibPath); log('green', ` ✓ Copied signed library to ${targetDylibPath}`); // Verify signature (informational) try { const sigInfo = execSync(`codesign -dv "${targetDylibPath}" 2>&1 | head -5`, { encoding: 'utf8' }); if (sigInfo.includes('TeamIdentifier')) { log('green', ` ✓ Library has valid Developer ID signature`); } } catch (err) { // Signature check is informational only } return; } // Create directories if (!fs.existsSync(tmpDir)) { fs.mkdirSync(tmpDir, { recursive: true }); } if (!fs.existsSync(nativeDir)) { fs.mkdirSync(nativeDir, { recursive: true }); } // Download dylib log('cyan', ` Downloading CoreML-enabled ONNX Runtime...`); log('cyan', ` URL: ${releaseUrl}`); await downloadFile(releaseUrl, dylibPath); log('green', ` ✓ Downloaded CoreML dylib (~80MB)`); // Verify it's a valid Mach-O library try { const fileOutput = execSync(`file "${dylibPath}"`, { encoding: 'utf8' }); if (!fileOutput.includes('Mach-O') || !fileOutput.includes('arm64')) { throw new Error(`Invalid Mach-O library: ${fileOutput}`); } log('green', ` ✓ Verified Mach-O ARM64 library`); } catch (err) { log('red', ` ✗ Library verification failed: ${err.message}`); throw err; } // Copy to npm package native directory fs.copyFileSync(dylibPath, targetDylibPath); log('green', ` ✓ Installed to ${targetDylibPath}`); // Check for CoreML support try { const symbols = execSync(`nm -g "${targetDylibPath}" | grep -i coreml | head -5`, { encoding: 'utf8' }); if (symbols) { log('green', ` ✓ CoreML symbols detected in library`); } } catch (err) { log('yellow', ` ⚠️ Could not verify CoreML symbols (may be normal)`); } // Cleanup temp file try { fs.unlinkSync(dylibPath); } catch (err) { // Cleanup is optional } log('green', `✅ CoreML-enabled ONNX Runtime ready for GPU acceleration`); } catch (err) { log('red', `\n❌ Failed to download ONNX Runtime CoreML library`); log('yellow', ` Error: ${err.message}`); log('cyan', '\n Manual installation:'); log('cyan', ` 1. Download: ${releaseUrl}`); log('cyan', ` 2. Copy to: ${targetDylibPath}`); throw err; } } /** * Download macOS ARM64 daemon binary from GitHub releases * Required for Apple Silicon Macs - cannot use bundled Linux ELF binaries */ /** * Download macOS ARM64 UI application from GitHub releases * Required for Apple Silicon Macs - the Tauri-based UI application */ function detectOrtLibrary() { log('cyan', '\n🔍 Detecting ONNX Runtime library path...'); // PRIORITY 1: Check platform package library (new architecture) // Platform packages (@agidreams/linux-x64, @agidreams/darwin-arm64) include GPU-enabled ORT try { const { resolveBinaryPaths, isPlatformPackageInstalled } = require('./src/resolve-binary'); if (isPlatformPackageInstalled()) { const binaryPaths = resolveBinaryPaths(); const isMacOS = process.platform === 'darwin'; const ortFileName = isMacOS ? 'libonnxruntime.dylib' : 'libonnxruntime.so'; const platformOrtLib = path.join(binaryPaths.libDir, ortFileName); if (fs.existsSync(platformOrtLib)) { log('green', `✓ Found ONNX Runtime (platform package): ${platformOrtLib}`); log('cyan', ` Using GPU-enabled library from ${binaryPaths.packageName}`); return platformOrtLib; } } } catch (err) { // Platform package not installed yet - continue to fallbacks } // PRIORITY 2: Check legacy bundled library path (deprecated, for backwards compatibility) const npmOrtLib = path.join(__dirname, 'lib', 'native', 'libonnxruntime.so'); if (fs.existsSync(npmOrtLib)) { log('green', `✓ Found ONNX Runtime (bundled): ${npmOrtLib}`); log('cyan', ' Using bundled GPU-enabled library with CUDA provider support'); return npmOrtLib; } // PRIORITY 3: Fall back to Python installation (usually CPU-only) log('yellow', '⚠️ Platform package ORT not found - checking Python fallback...'); try { // Try to find ONNX Runtime through Python (fallback, usually CPU-only) const ortPath = execSync( 'python3 -c "import onnxruntime; import os; print(os.path.join(os.path.dirname(onnxruntime.__file__), \'capi\'))"', { encoding: 'utf-8' } ).trim(); if (!fs.existsSync(ortPath)) { log('yellow', '⚠️ Warning: ONNX Runtime capi directory not found at ' + ortPath); log('yellow', ' Daemon may not work correctly without onnxruntime-gpu'); log('cyan', ' Install with: pip3 install onnxruntime-gpu'); return null; } // Find the actual .so file const ortFiles = fs.readdirSync(ortPath).filter(f => f.startsWith('libonnxruntime.so')); if (ortFiles.length === 0) { log('yellow', '⚠️ Warning: Could not find libonnxruntime.so in ' + ortPath); log('yellow', ' Daemon may not work correctly'); log('cyan', ' Install with: pip3 install onnxruntime-gpu'); return null; } // Use the first (or only) .so file found const ortLibPath = path.join(ortPath, ortFiles[0]); log('yellow', `⚠️ Using Python ONNX Runtime: ${ortLibPath}`); log('yellow', ' Note: This may be CPU-only and lack CUDA support'); // Store in a config file for systemd service generation const configDir = path.join(__dirname, 'config'); const envFilePath = path.join(configDir, 'detected-environment.json'); const envConfig = { ORT_DYLIB_PATH: ortLibPath, detected_at: new Date().toISOString(), onnxruntime_version: execSync('python3 -c "import onnxruntime; print(onnxruntime.__version__)"', { encoding: 'utf-8' }).trim(), warning: 'Using Python pip installation - may be CPU-only' }; try { fs.writeFileSync(envFilePath, JSON.stringify(envConfig, null, 2)); log('green', `✓ Saved environment config to ${envFilePath}`); } catch (err) { log('yellow', `Warning: Could not save environment config: ${err.message}`); } return ortLibPath; } catch (err) { log('yellow', '\n⚠️ Could not detect ONNX Runtime:'); log('yellow', ` ${err.message}`); log('cyan', '\n📦 Please install onnxruntime-gpu for optimal performance:'); log('cyan', ' pip3 install onnxruntime-gpu'); log('cyan', '\n The daemon will not work correctly without this library!'); return null; } } function generateSystemdService(ortLibPath) { log('cyan', '\n⚙️ Generating systemd service files...'); try { // Get platform package binary paths const { resolveBinaryPaths } = require('./src/resolve-binary'); let binaryPaths; try { binaryPaths = resolveBinaryPaths(); log('cyan', ` Using platform package: ${binaryPaths.packageName}`); log('cyan', ` Daemon binary: ${binaryPaths.daemon}`); log('cyan', ` Platform lib directory: ${binaryPaths.libDir}`); } catch (err) { log('red', ` ✗ Could not resolve platform package binaries: ${err.message}`); log('yellow', ' Service generation cannot proceed without platform package'); return; } const systemdDir = path.join(os.homedir(), '.config', 'systemd', 'user'); // Create systemd directory if it doesn't exist if (!fs.existsSync(systemdDir)) { fs.mkdirSync(systemdDir, { recursive: true }); log('green', `✓ Created ${systemdDir}`); } // Detect display environment variables (used by both daemon and UI services) const runtimeDir = process.env.XDG_RUNTIME_DIR || `/run/user/${process.getuid()}`; let waylandDisplay = null; let xDisplay = process.env.DISPLAY || null; try { const sockets = fs.readdirSync(runtimeDir).filter(f => f.startsWith('wayland-')); if (sockets.length > 0) { // Use the first wayland socket (usually wayland-0 or wayland-1) waylandDisplay = sockets[0]; log('cyan', ` Detected Wayland display: ${waylandDisplay}`); } } catch (err) { // Wayland socket not found, may be X11-only system } // 1. Generate daemon service from template const templatePath = path.join(__dirname, 'templates', 'swictation-daemon.service.template'); if (!fs.existsSync(templatePath)) { log('yellow', `⚠️ Warning: Template not found at ${templatePath}`); log('yellow', ' Skipping daemon service generation'); } else { let template = fs.readFileSync(templatePath, 'utf8'); // Replace placeholders with platform package paths // __INSTALL_DIR__ in template refers to the bin directory containing daemon binary // Template has: __INSTALL_DIR__/swictation-daemon // We need to replace __INSTALL_DIR__ with actual binDir from platform package template = template.replace(/__INSTALL_DIR__/g, binaryPaths.binDir); // CRITICAL: Detect GPU variant to determine which ONNX Runtime to use const configDir = path.join(os.homedir(), '.config', 'swictation'); const gpuPackageInfoPath = path.join(configDir, 'gpu-package-info.json'); let finalOrtLibPath, finalLdLibraryPath; let variant = 'latest'; // default // Try to read GPU package info to get variant if (fs.existsSync(gpuPackageInfoPath)) { try { const metadata = JSON.parse(fs.readFileSync(gpuPackageInfoPath, 'utf8')); variant = metadata.variant || 'latest'; log('cyan', ` Detected GPU package variant: ${variant}`); } catch (err) { log('yellow', ` Warning: Could not read GPU package metadata: ${err.message}`); } } // Detect CUDA paths upfront (needed for logging) const detectedCudaPaths = detectCudaLibraryPaths(); // Check multiple possible ONNX Runtime locations in priority order: // 1. gpu-libs directory (downloaded GPU libraries) // 2. Platform package lib directory (bundled with package) const gpuLibsDir = path.join(os.homedir(), '.local', 'share', 'swictation', 'gpu-libs'); const gpuLibsOrtPath = path.join(gpuLibsDir, 'libonnxruntime.so'); const platformOrtPath = path.join(binaryPaths.libDir, 'libonnxruntime.so'); if (fs.existsSync(gpuLibsOrtPath)) { // Use gpu-libs ONNX Runtime (downloaded GPU libraries) finalOrtLibPath = gpuLibsOrtPath; // LD_LIBRARY_PATH: gpu-libs first (has CUDA providers), then platform lib, then CUDA paths finalLdLibraryPath = [gpuLibsDir, binaryPaths.libDir, ...detectedCudaPaths].join(':'); log('cyan', ` Using downloaded ONNX Runtime: ${finalOrtLibPath}`); log('cyan', ` GPU libraries directory: ${gpuLibsDir}`); } else if (fs.existsSync(platformOrtPath)) { // Use platform package ONNX Runtime (bundled) finalOrtLibPath = platformOrtPath; finalLdLibraryPath = [...detectedCudaPaths, binaryPaths.libDir].join(':'); log('cyan', ` Using platform package ONNX Runtime: ${finalOrtLibPath}`); log('cyan', ` Platform lib directory: ${binaryPaths.libDir}`); } else { // Neither exists - use platform path but warn log('yellow', ` ⚠️ ONNX Runtime not found in gpu-libs: ${gpuLibsOrtPath}`); log('yellow', ` ⚠️ ONNX Runtime not found in platform: ${platformOrtPath}`); finalOrtLibPath = platformOrtPath; // Use platform path as placeholder finalLdLibraryPath = [...detectedCudaPaths, binaryPaths.libDir].join(':'); log('yellow', ' ⚠️ Service may fail - run GPU library download manually'); } // CRITICAL: Trim all whitespace and newlines from paths to prevent malformed service file if (finalOrtLibPath) { const cleanPath = finalOrtLibPath.trim().replace(/[\r\n]/g, ''); template = template.replace(/__ORT_DYLIB_PATH__/g, cleanPath); log('cyan', ` ORT_DYLIB_PATH set to: ${cleanPath}`); } else { log('yellow', '⚠️ Warning: ORT_DYLIB_PATH not detected'); log('yellow', ' Service file will contain placeholder - you must set it manually'); } // CRITICAL: Trim and clean LD_LIBRARY_PATH const cleanLdPath = finalLdLibraryPath.trim().replace(/[\r\n]/g, ''); template = template.replace(/__LD_LIBRARY_PATH__/g, cleanLdPath); log('cyan', ` LD_LIBRARY_PATH set to: ${cleanLdPath}`); if (detectedCudaPaths.length > 0) { log('cyan', ` Detected ${detectedCudaPaths.length} CUDA library path(s):`); detectedCudaPaths.forEach(p => log('cyan', ` ${p}`)); } else { log('yellow', ' ⚠️ No CUDA libraries detected (CPU-only mode)'); } // Add display environment variables before ImportEnvironment if (waylandDisplay || xDisplay) { const envVars = []; if (waylandDisplay) envVars.push(`Environment="WAYLAND_DISPLAY=${waylandDisplay}"`); if (xDisplay) envVars.push(`Environment="DISPLAY=${xDisplay}"`); // Insert before ImportEnvironment= line template = template.replace( /ImportEnvironment=/, `${envVars.join('\n')}\n\n# Import full user environment for PulseAudio/PipeWire session\n# This ensures all audio devices are detected properly (4 devices instead of 1)\n# Required for microphone access in user session\nImportEnvironment=` ); } // VALIDATION: Check for malformed Environment variables const validateServiceFile = (content) => { const lines = content.split('\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].includes('Environment=')) { const quoteCnt = (lines[i].match(/"/g) || []).length; if (quoteCnt % 2 !== 0) { throw new Error(`Malformed Environment variable at line ${i+1}: ${lines[i]}`); } } } }; try { validateServiceFile(template); log('green', ' ✓ Service file validation passed'); } catch (err) { log('red', ` ✗ Service file validation failed: ${err.message}`); throw err; } // Write daemon service file const daemonServicePath = path.join(systemdDir, 'swictation-daemon.service'); fs.writeFileSync(daemonServicePath, template); log('green', `✓ Generated daemon service: ${daemonServicePath}`); if (ortLibPath) { log('cyan', ' Service configured with detected ONNX Runtime path'); } else { log('yellow', ' ⚠️ Please edit the service file to set ORT_DYLIB_PATH manually'); } } // 2. Install UI service - detect environment and use appropriate UI // Sway/wlroots compositors: Use Python Qt tray (better Wayland support) // GNOME/KDE/X11: Use Tauri UI binary // CRITICAL: Environment variables may not be set during npm postinstall // So we use multiple detection methods that work regardless of environment: // 1. Check running compositor processes (most reliable) // 2. Check for compositor sockets // 3. Fall back to environment variables if available let isSway = false; let isHyprland = false; let isRiver = false; let detectedCompositor = 'unknown'; // Method 1: Check running processes (works even without env vars) try { const pgrepSway = execSync('pgrep -x sway 2>/dev/null || true', { encoding: 'utf8' }).trim(); if (pgrepSway) { isSway = true;