// Downloads the standalone Piper release for this OS and the Swedish + English voices // into /piper/. Needs internet once; everything runs offline afterwards. const fs = require("fs"); const os = require("os"); const path = require("path"); const { Readable } = require("stream"); const { pipeline } = require("stream/promises"); const { spawnSync } = require("child_process"); const PIPER_RELEASE_BASE = "https://github.com/rhasspy/piper/releases/download/2023.11.14-2"; const PIPER_ARCHIVES = { "linux-x64": "piper_linux_x86_64.tar.gz", "linux-arm64": "piper_linux_aarch64.tar.gz", "win32-x64": "piper_windows_amd64.zip", "darwin-x64": "piper_macos_x64.tar.gz", "darwin-arm64": "piper_macos_aarch64.tar.gz", }; const VOICE_BASE = "https://huggingface.co/rhasspy/piper-voices/resolve/main"; const VOICES = [ { name: "sv_SE-nst-medium", path: "sv/sv_SE/nst/medium" }, { name: "en_US-lessac-medium", path: "en/en_US/lessac/medium" }, ]; async function download(url, target) { const response = await fetch(url, { redirect: "follow" }); if (!response.ok || !response.body) throw new Error(`Download failed (${response.status}): ${url}`); const partial = `${target}.part`; await pipeline(Readable.fromWeb(response.body), fs.createWriteStream(partial)); fs.renameSync(partial, target); } async function installPiperBinary({ projectDir, log }) { const archive = PIPER_ARCHIVES[`${process.platform}-${process.arch}`]; if (!archive) throw new Error(`No standalone Piper release for ${process.platform}-${process.arch}`); const tmpFile = path.join(os.tmpdir(), `racecore-${archive}`); log(`Downloading Piper (${archive})...`); await download(`${PIPER_RELEASE_BASE}/${archive}`, tmpFile); log("Unpacking Piper..."); // tar ships with Linux, macOS and Windows 10+, and the Windows one also unpacks .zip. const result = spawnSync("tar", ["-xf", tmpFile, "-C", projectDir], { encoding: "utf8", windowsHide: true }); fs.rmSync(tmpFile, { force: true }); if (result.status !== 0) throw new Error(`Unpacking failed: ${result.stderr || result.error?.message || "tar error"}`); } async function installVoices({ voicesDir, log }) { fs.mkdirSync(voicesDir, { recursive: true }); for (const voice of VOICES) { for (const file of [`${voice.name}.onnx.json`, `${voice.name}.onnx`]) { const target = path.join(voicesDir, file); if (fs.existsSync(target)) continue; log(`Downloading voice ${file}...`); await download(`${VOICE_BASE}/${voice.path}/${file}`, target); } } } // Skips anything already present, so re-running is cheap and safe. async function installAll({ projectDir, hasBinary, log = () => {} }) { if (!hasBinary) await installPiperBinary({ projectDir, log }); else log("Piper already installed."); await installVoices({ voicesDir: path.join(projectDir, "piper", "voices"), log }); log("Done."); } module.exports = { installAll, installVoices, installPiperBinary, VOICES, PIPER_ARCHIVES };