import type { SpeechClip, VoiceInfo } from "@rakazo/adapter-kit"; import { GERMAN_VOICE_PRESETS } from "@rakazo/core"; import { readVoiceAudio, readVoiceJson, requireOk, voiceDeadline } from "./voice-http.js"; /** The Piper model behind each German voice; Kokoro has no German voices of its own. */ export const PIPER_MODELS: Record = { dm_thorsten: "de_DE-thorsten-high", df_kerstin: "de_DE-kerstin-low", df_eva: "de_DE-eva_k-x_low", df_ramona: "de_DE-ramona-low", dm_karlsson: "de_DE-karlsson-low", }; /** Piper runs only where configured, so a server without it skips the request entirely. */ export function piperBaseUrl(): string | undefined { const raw = process.env.PIPER_BASE_URL?.trim(); return raw ? raw.replace(/\/+$/, "") : undefined; } export function piperUnreachable(): string { return "Deutsche Stimme nicht erreichbar. Prüfe, ob der Piper-Server läuft."; } /** The German voices whose Piper model the server has downloaded. */ export async function listPiperVoices(signal: AbortSignal): Promise { const baseUrl = piperBaseUrl(); if (!baseUrl) return []; try { const res = await fetch(`${baseUrl}/voices`, { signal: voiceDeadline(signal, 20_000) }); if (!res.ok) return []; const body = await readVoiceJson(res, { requireValid: true }); const models = new Set(body && typeof body === "object" ? Object.keys(body) : []); return GERMAN_VOICE_PRESETS.filter((voice) => { const model = PIPER_MODELS[voice.id]; return model !== undefined && models.has(model); }); } catch { return []; } } /** Speaks through Piper when the voice is one of its models, otherwise returns undefined. */ export async function synthesizePiper( voiceId: string | undefined, text: string, signal: AbortSignal, ): Promise { const model = voiceId ? PIPER_MODELS[voiceId] : undefined; if (!model) return undefined; const baseUrl = piperBaseUrl(); if (!baseUrl) throw new Error(piperUnreachable()); let res: Response; try { res = await fetch(`${baseUrl}/synthesize`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ text, voice: model }), signal, }); } catch { throw new Error(piperUnreachable()); } await requireOk(res, "Local voice", "speaking"); return { bytes: await readVoiceAudio(res, signal), mimeType: "audio/wav" }; }