evOo-local/packages/core/src/voice-chat-command.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

58 lines
1.9 KiB
TypeScript

export type VoiceChatCommand =
| { kind: "set_voice"; voiceName: string }
| { kind: "speak_on" }
| { kind: "speak_off" };
const SET_VOICE_PATTERNS = [
/^(?:\/stimme|\/voice)\s+(.+)$/i,
/^(?:ändere|aendere|wechsel|nimm)\s+(?:deine\s+)?stimme\s+(?:zu|auf|in)\s+(.+)$/i,
/^(?:change|switch|use)\s+(?:your\s+)?voice\s+(?:to\s+)?(.+)$/i,
/^sprich\s+(?:mit\s+)?(?:der\s+)?stimme\s+(.+)$/i,
/^(?:nutze|verwende|nimm)\s+(?:die\s+)?stimme\s+(.+)$/i,
] as const;
const SPEAK_ON_PATTERNS = [
/^(?:\/sprechen|\/speak)$/i,
/^(?:kann\s+wieder\s+)?sprechen$/i,
/^(?:lies|lese)\s+(?:antworten\s+)?(?:wieder\s+)?(?:laut\s+)?vor$/i,
/^(?:turn\s+)?speak(?:ing)?\s+on$/i,
/^read\s+(?:replies\s+)?aloud$/i,
] as const;
const SPEAK_OFF_PATTERNS = [
/^(?:\/schweigen|\/mute)$/i,
/^(?:sprich\s+)?(?:nicht\s+mehr|nicht\s+wieder)$/i,
/^sprich\s+nicht$/i,
/^(?:turn\s+)?speak(?:ing)?\s+off$/i,
/^stop\s+speaking$/i,
/^schweig(?:e)?$/i,
] as const;
function firstCapture(patterns: readonly RegExp[], text: string): string | undefined {
for (const pattern of patterns) {
const match = pattern.exec(text.trim());
const captured = match?.[1]?.trim();
if (captured) return captured;
}
return undefined;
}
function matchesAny(patterns: readonly RegExp[], text: string): boolean {
const trimmed = text.trim();
return patterns.some((pattern) => pattern.test(trimmed));
}
/** Parse a chat message for local voice control before sending to the agent. */
export function parseVoiceChatCommand(text: string): VoiceChatCommand | null {
const trimmed = text.trim();
if (!trimmed) return null;
if (matchesAny(SPEAK_OFF_PATTERNS, trimmed)) return { kind: "speak_off" };
if (matchesAny(SPEAK_ON_PATTERNS, trimmed)) return { kind: "speak_on" };
const voiceName = firstCapture(SET_VOICE_PATTERNS, trimmed);
if (voiceName) return { kind: "set_voice", voiceName };
return null;
}