Adds Alma (sv), Alan, Northern English, Cori, Alba (British/Scottish) and Joe, John, Norman (American) as optional voices downloaded one at a time from Settings, with gender (measured pitch) and accent shown in the voice lists. The standard install is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
86 lines
5.3 KiB
JavaScript
86 lines
5.3 KiB
JavaScript
// Downloads the standalone Piper release for this OS and the Swedish + English voices
|
|
// into <project>/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";
|
|
// One male and one female voice per language are installed by default; `extra` voices
|
|
// (~63 MB each) are only downloaded when picked under Settings -> Audio. Genders were
|
|
// checked by measuring each voice's median pitch, not assumed from the name (nst 81 Hz,
|
|
// ryan 152, alan 103, northern_english_male 110, joe 98, john 117, norman 103; lisa 184,
|
|
// lessac 208, alma 185, alba 199, cori 202). `default` is the voice used until someone picks
|
|
// another in Settings. `accent` is shown next to the name.
|
|
const VOICES = [
|
|
{ name: "sv_SE-nst-medium", path: "sv/sv_SE/nst/medium", language: "sv", gender: "male", label: "NST", default: true },
|
|
{ name: "sv_SE-lisa-medium", path: "sv/sv_SE/lisa/medium", language: "sv", gender: "female", label: "Lisa" },
|
|
{ name: "sv_SE-alma-medium", path: "sv/sv_SE/alma/medium", language: "sv", gender: "female", label: "Alma", extra: true },
|
|
{ name: "en_US-ryan-medium", path: "en/en_US/ryan/medium", language: "en", gender: "male", label: "Ryan", accent: "us", default: true },
|
|
{ name: "en_US-lessac-medium", path: "en/en_US/lessac/medium", language: "en", gender: "female", label: "Lessac", accent: "us" },
|
|
{ name: "en_GB-alan-medium", path: "en/en_GB/alan/medium", language: "en", gender: "male", label: "Alan", accent: "gb", extra: true },
|
|
{ name: "en_GB-northern_english_male-medium", path: "en/en_GB/northern_english_male/medium", language: "en", gender: "male", label: "Northern English", accent: "gb_north", extra: true },
|
|
{ name: "en_GB-cori-medium", path: "en/en_GB/cori/medium", language: "en", gender: "female", label: "Cori", accent: "gb", extra: true },
|
|
{ name: "en_GB-alba-medium", path: "en/en_GB/alba/medium", language: "en", gender: "female", label: "Alba", accent: "scottish", extra: true },
|
|
{ name: "en_US-joe-medium", path: "en/en_US/joe/medium", language: "en", gender: "male", label: "Joe", accent: "us", extra: true },
|
|
{ name: "en_US-john-medium", path: "en/en_US/john/medium", language: "en", gender: "male", label: "John", accent: "us", extra: true },
|
|
{ name: "en_US-norman-medium", path: "en/en_US/norman/medium", language: "en", gender: "male", label: "Norman", accent: "us", extra: true },
|
|
];
|
|
|
|
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"}`);
|
|
}
|
|
|
|
// Installs the standard voices, or exactly the named catalog voices (`names`).
|
|
async function installVoices({ voicesDir, log, names = null }) {
|
|
fs.mkdirSync(voicesDir, { recursive: true });
|
|
const wanted = names ? VOICES.filter((voice) => names.includes(voice.name)) : VOICES.filter((voice) => !voice.extra);
|
|
for (const voice of wanted) {
|
|
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.
|
|
// `voice` set: install Piper if missing plus only that extra voice.
|
|
async function installAll({ projectDir, hasBinary, log = () => {}, voice = "" }) {
|
|
if (voice && !VOICES.some((item) => item.name === voice)) throw new Error(`Unknown voice: ${voice}`);
|
|
if (!hasBinary) await installPiperBinary({ projectDir, log });
|
|
else log("Piper already installed.");
|
|
await installVoices({ voicesDir: path.join(projectDir, "piper", "voices"), log, names: voice ? [voice] : null });
|
|
log("Done.");
|
|
}
|
|
|
|
module.exports = { installAll, installVoices, installPiperBinary, VOICES, PIPER_ARCHIVES };
|