evOo-local/packages/adapters/src/voice-factory.ts
cynarAI 8ca23ad609 Voice UX: Kann sprechen ohne Kokoro-Branding (#75)
* 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>
2026-09-22 14:35:36 +02:00

107 lines
3.2 KiB
TypeScript

import type { VoiceProvider } from "@rakazo/adapter-kit";
import { CartesiaVoiceProvider } from "./cartesia-voice.js";
import { ElevenLabsVoiceProvider } from "./elevenlabs-voice.js";
import { FishAudioVoiceProvider } from "./fish-audio-voice.js";
import { KokoroVoiceProvider } from "./kokoro-voice.js";
import { OpenAIVoiceProvider } from "./openai-voice.js";
import { SCRIPTED_VOICE_CATALOG_ENTRY, ScriptedVoiceProvider } from "./scripted-voice.js";
export const VOICE_CATALOG = [
{
id: "kokoro",
name: "Local voice",
description: "Free local speech on your server.",
transcribe: false,
},
{
id: "elevenlabs",
name: "ElevenLabs",
description: "Highest quality and cloning. Flash v2.5 for conversational calls.",
transcribe: true,
},
{
id: "openai",
name: "OpenAI",
description: "Simple TTS plus Whisper-class transcription. Reuse an OpenAI key.",
transcribe: true,
},
{
id: "cartesia",
name: "Cartesia",
description: "Lowest-latency Sonic voices for interruptible calls.",
transcribe: false,
},
{
id: "fish-audio",
name: "Fish Audio",
description: "Voice models, cloning, and expressive speech with optional transcription.",
transcribe: true,
},
] as const;
export { SCRIPTED_VOICE_CATALOG_ENTRY };
export type HostedVoiceProviderId = (typeof VOICE_CATALOG)[number]["id"];
export type VoiceProviderId = HostedVoiceProviderId | "scripted";
export function scriptedVoiceEnabled() {
return process.env.AGENT_RUNTIME === "scripted";
}
export function listVoiceCatalog() {
return scriptedVoiceEnabled()
? [...VOICE_CATALOG, SCRIPTED_VOICE_CATALOG_ENTRY]
: [...VOICE_CATALOG];
}
export function voiceCatalogEntry(id: string) {
if (id === SCRIPTED_VOICE_CATALOG_ENTRY.id) return SCRIPTED_VOICE_CATALOG_ENTRY;
return VOICE_CATALOG.find((entry) => entry.id === id);
}
export function isVoiceProviderId(value: string): value is VoiceProviderId {
if (value === "scripted") return scriptedVoiceEnabled();
return VOICE_CATALOG.some((entry) => entry.id === value);
}
/** Construct the provider adapter selected by a persisted voice credential. */
export function createVoiceProvider(kind: string): VoiceProvider {
switch (kind) {
case "kokoro":
return new KokoroVoiceProvider();
case "elevenlabs":
return new ElevenLabsVoiceProvider();
case "openai":
return new OpenAIVoiceProvider();
case "cartesia":
return new CartesiaVoiceProvider();
case "fish-audio":
return new FishAudioVoiceProvider();
case "scripted":
if (!scriptedVoiceEnabled()) break;
return new ScriptedVoiceProvider();
default:
break;
}
throw new Error(
`Unknown voice provider "${kind}". Use kokoro | elevenlabs | openai | cartesia | fish-audio.`,
);
}
export class NoVoiceConfigured extends Error {
readonly reason: "key" | "voice";
constructor(reason: "key" | "voice") {
super(
reason === "key"
? "Add a voice provider key in Voice settings to turn on speaking."
: "Pick a voice in Voice settings.",
);
this.reason = reason;
this.name = "NoVoiceConfigured";
}
}
export const MAX_SPEAK_CHARS = 2000;
export const MAX_TRANSCRIBE_BYTES = 8 * 1024 * 1024;