UNPKG

piper-announce

Version:

AI-powered announcement generator using Piper TTS and OpenAI GPT models

269 lines (228 loc) 7.56 kB
#!/usr/bin/env node import { downloadVoices } from "./download-voices.js"; import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const colors = { reset: "\x1b[0m", bright: "\x1b[1m", green: "\x1b[32m", yellow: "\x1b[33m", blue: "\x1b[34m", cyan: "\x1b[36m", red: "\x1b[31m", }; function colorize(text, color) { return `${colors[color]}${text}${colors.reset}`; } // Force output to be visible during npm install function forceOutput(message) { // Write to stderr (less likely to be buffered by npm) process.stderr.write(message + "\n"); // Also try stdout process.stdout.write(message + "\n"); // Force flush if (process.stdout.flush) process.stdout.flush(); if (process.stderr.flush) process.stderr.flush(); } function findVoicesDir() { const possibleDirs = [ process.env.VOICES_DIR, path.join(process.cwd(), "voices"), path.join(__dirname, "..", "voices"), path.join(process.env.HOME || "~", ".piper", "voices"), ].filter(Boolean); for (const dir of possibleDirs) { if (fs.existsSync(dir)) return dir; } const defaultDir = path.join(__dirname, "..", "voices"); fs.mkdirSync(defaultDir, { recursive: true }); return defaultDir; } async function postInstallSetup() { // Force initial output forceOutput(""); forceOutput(colorize("🎵 Setting up Piper Announce...", "bright")); // Check if this is a CI environment or if user wants to skip download const isCI = process.env.CI || process.env.CONTINUOUS_INTEGRATION; const skipDownload = process.env.SKIP_VOICE_DOWNLOAD === "true"; if (isCI) { forceOutput( colorize( "⚠️ CI environment detected - skipping voice download", "yellow" ) ); forceOutput( colorize('💡 Run "piper-voices download" after installation', "cyan") ); return; } if (skipDownload) { forceOutput( colorize( "⚠️ Voice download skipped (SKIP_VOICE_DOWNLOAD=true)", "yellow" ) ); forceOutput( colorize('💡 Run "piper-voices download" after installation', "cyan") ); return; } const voicesDir = findVoicesDir(); forceOutput(colorize(`📂 Voices directory: ${voicesDir}`, "cyan")); try { forceOutput(colorize("🚀 Downloading voice models...", "blue")); forceOutput( colorize(" This may take a few minutes on first install.", "yellow") ); forceOutput(colorize(" Progress will be shown below:", "cyan")); forceOutput(""); // Create a custom download function with better npm-compatible progress await downloadVoicesForNpm(); forceOutput(""); forceOutput(colorize("✅ Piper Announce setup complete!", "green")); forceOutput( colorize( "💡 Use 'piper-voices status' to check voice availability", "cyan" ) ); } catch (error) { forceOutput(colorize(`❌ Setup failed: ${error.message}`, "red")); forceOutput( colorize("💡 You can download voices manually later:", "yellow") ); forceOutput(colorize(" piper-voices download", "cyan")); // Don't fail the installation - just warn the user process.exit(0); } } // Custom download function optimized for npm postinstall async function downloadVoicesForNpm() { const { checkExistingVoices, VOICE_MODELS } = await import( "./download-voices.js" ); const voicesDir = findVoicesDir(); const { missing } = checkExistingVoices(voicesDir); if (missing.length === 0) { forceOutput(colorize("✅ All voice models already available!", "green")); return; } // Prepare download list const filesToDownload = []; missing.forEach(({ voiceName, missingFiles }) => { missingFiles.forEach((file) => { filesToDownload.push({ ...file, voiceName }); }); }); let totalSizeMB = 0; filesToDownload.forEach((file) => { const sizeNum = parseFloat(file.size); if (file.size.includes("MB")) { totalSizeMB += sizeNum; } else if (file.size.includes("KB")) { totalSizeMB += sizeNum / 1024; } }); forceOutput( colorize( `📦 Downloading ${filesToDownload.length} voice files (~${Math.round( totalSizeMB )}MB)`, "cyan" ) ); let successCount = 0; let failCount = 0; // Download files with npm-friendly progress reporting for (let i = 0; i < filesToDownload.length; i++) { const file = filesToDownload[i]; forceOutput( colorize(`[${i + 1}/${filesToDownload.length}] ${file.filename}`, "blue") ); try { await downloadFileForNpm( file.url, file.filepath, file.filename, file.size ); successCount++; forceOutput(colorize(` ✅ Complete`, "green")); } catch (error) { failCount++; forceOutput(colorize(` ❌ Failed: ${error.message}`, "red")); // Clean up partial download try { if (fs.existsSync(file.filepath)) { fs.unlinkSync(file.filepath); } } catch {} } } // Final summary if (successCount > 0) { forceOutput( colorize(`✅ Successfully downloaded ${successCount} files`, "green") ); } if (failCount > 0) { forceOutput(colorize(`❌ Failed to download ${failCount} files`, "red")); } } // Download function optimized for npm postinstall visibility async function downloadFileForNpm(url, filepath, filename, expectedSize) { const { createWriteStream } = await import("fs"); const { pipeline } = await import("stream"); const { promisify } = await import("util"); const streamPipeline = promisify(pipeline); try { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const totalSize = parseInt(response.headers.get("content-length") || "0"); let downloadedSize = 0; let lastReportedPercent = 0; const fileStream = createWriteStream(filepath); // Create progress reporting stream const progressStream = new (await import("stream")).Transform({ transform(chunk, encoding, callback) { downloadedSize += chunk.length; if (totalSize > 0) { const percent = Math.floor((downloadedSize / totalSize) * 100); // Report every 25% to avoid spam but show progress if (percent >= lastReportedPercent + 25 && percent <= 100) { const downloaded = formatBytes(downloadedSize); const total = formatBytes(totalSize); forceOutput(` ${percent}% (${downloaded}/${total})`); lastReportedPercent = percent; } } callback(null, chunk); }, }); await streamPipeline(response.body, progressStream, fileStream); } catch (error) { throw new Error(`Download failed: ${error.message}`); } } function formatBytes(bytes) { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; } // Run the post-install setup postInstallSetup().catch((error) => { forceOutput(colorize(`❌ Post-install failed: ${error.message}`, "red")); forceOutput(colorize("💡 You can download voices manually later:", "yellow")); forceOutput(colorize(" piper-voices download", "cyan")); // Don't fail the installation process.exit(0); });