feat(audio): add downloadable extra Piper voices with accents
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>
This commit is contained in:
@@ -16,14 +16,25 @@ const PIPER_ARCHIVES = {
|
||||
"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. Genders were checked by measuring each
|
||||
// voice's median pitch (nst 81 Hz, ryan 152 Hz; lisa 184 Hz, lessac 208 Hz), not assumed
|
||||
// from the name. `default` is the voice used until someone picks another in Settings.
|
||||
// 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: "en_US-ryan-medium", path: "en/en_US/ryan/medium", language: "en", gender: "male", label: "Ryan", default: true },
|
||||
{ name: "en_US-lessac-medium", path: "en/en_US/lessac/medium", language: "en", gender: "female", label: "Lessac" },
|
||||
{ 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) {
|
||||
@@ -47,9 +58,11 @@ async function installPiperBinary({ projectDir, log }) {
|
||||
if (result.status !== 0) throw new Error(`Unpacking failed: ${result.stderr || result.error?.message || "tar error"}`);
|
||||
}
|
||||
|
||||
async function installVoices({ voicesDir, log }) {
|
||||
// Installs the standard voices, or exactly the named catalog voices (`names`).
|
||||
async function installVoices({ voicesDir, log, names = null }) {
|
||||
fs.mkdirSync(voicesDir, { recursive: true });
|
||||
for (const voice of VOICES) {
|
||||
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;
|
||||
@@ -60,10 +73,12 @@ async function installVoices({ voicesDir, log }) {
|
||||
}
|
||||
|
||||
// Skips anything already present, so re-running is cheap and safe.
|
||||
async function installAll({ projectDir, hasBinary, log = () => {} }) {
|
||||
// `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 });
|
||||
await installVoices({ voicesDir: path.join(projectDir, "piper", "voices"), log, names: voice ? [voice] : null });
|
||||
log("Done.");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user