* 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
1.9 KiB
TypeScript
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;
|
|
}
|