* Voice UX: simple speak toggle, onboarding pick, chat voice change Replace Kokoro-branded settings with a Kann sprechen / Spricht nicht toggle, onboarding voice personalization, and local chat commands for voice changes. German voice presets (Eva, Bernd) surface as friendly names; Kokoro stays under the hood via KOKORO_BASE_URL. Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> * Fix import order in voice onboarding and settings Co-authored-by: cynarAI <cynarAI@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
58 lines
2.4 KiB
TypeScript
58 lines
2.4 KiB
TypeScript
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);
|
|
});
|
|
}
|