import type { VoiceInfo } from "@rakazo/contracts"; /** Friendly German presets when Kokoro exposes df_/dm_ voices. */ export const GERMAN_VOICE_PRESETS: VoiceInfo[] = [ { id: "df_eva", label: "Eva", description: "Deutsch · warm" }, { id: "dm_bernd", label: "Bernd", description: "Deutsch · klar" }, { id: "df_victoria", label: "Victoria", description: "Deutsch · freundlich" }, { id: "dm_martin", label: "Martin", description: "Deutsch · ruhig" }, ]; const PRESET_BY_ID = new Map(GERMAN_VOICE_PRESETS.map((voice) => [voice.id, voice])); /** Strip engine prefix (df_eva → Eva) for display when no preset exists. */ export function formatVoiceLabel(id: string): string { const preset = PRESET_BY_ID.get(id); if (preset) return preset.label; const suffix = id.replace(/^[a-z]{2}_/, ""); if (!suffix) return id; return suffix.charAt(0).toUpperCase() + suffix.slice(1).replace(/_/g, " "); } export function voiceDescription(id: string, fallback?: string): string | undefined { return PRESET_BY_ID.get(id)?.description ?? fallback; } /** Prefer German voices when UI locale is de. */ export function preferVoicesForLocale(voices: VoiceInfo[], locale: string): VoiceInfo[] { if (locale !== "de") return voices; const german = voices.filter((voice) => /^d[fm]_/.test(voice.id)); if (german.length === 0) return voices; const rest = voices.filter((voice) => !/^d[fm]_/.test(voice.id)); return [...german, ...rest]; } export function defaultVoiceForLocale(voices: VoiceInfo[], locale: string): VoiceInfo | undefined { const ordered = preferVoicesForLocale(voices, locale); if (locale === "de") { return ordered.find((voice) => voice.id === "df_eva") ?? ordered[0]; } return ordered[0]; } /** Match a user-provided name (Eva, bernd, df_eva) against available voices. */ export function matchVoiceByName(voices: VoiceInfo[], query: string): VoiceInfo | undefined { const needle = query.trim().toLowerCase(); if (!needle) return undefined; const byId = voices.find((voice) => voice.id.toLowerCase() === needle); if (byId) return byId; const byLabel = voices.find((voice) => voice.label.toLowerCase() === needle); if (byLabel) return byLabel; const suffix = needle.replace(/^[a-z]{2}_/, ""); return voices.find((voice) => { const label = voice.label.toLowerCase(); const idSuffix = voice.id.replace(/^[a-z]{2}_/, "").toLowerCase(); return label.includes(suffix) || idSuffix.includes(suffix) || suffix.includes(idSuffix); }); }