import { useEffect } from "react"; import type { VoiceController } from "../realtime/controller"; interface Recognition { lang: string; continuous: boolean; interimResults: boolean; onresult: ((event: { resultIndex: number; results: ArrayLike> }) => void) | null; onerror: ((event: { error: string }) => void) | null; onend: (() => void) | null; start: () => void; abort: () => void; } declare global { interface Window { SpeechRecognition?: new () => Recognition; webkitSpeechRecognition?: new () => Recognition; } } /** Optional browser wake word; never owns the microphone during a voice session. */ export function useWakeWord(enabled: boolean, controller: VoiceController) { useEffect(() => { const Constructor = window.SpeechRecognition ?? window.webkitSpeechRecognition; if (!enabled || !Constructor) { return; } const recognition = new Constructor(); let stopped = false; let timer: ReturnType | undefined; recognition.lang = "es-ES"; recognition.continuous = true; recognition.interimResults = true; const start = () => { if (!stopped) { try { recognition.start(); } catch { /* A browser may require a click first. */ } } }; recognition.onresult = (event) => { for (let index = event.resultIndex; index < event.results.length; index++) { const text = event.results[index]?.[0]?.transcript .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, "") ?? ""; if (/\b(?:hola|ola)\s+(?:vocero|bocero|bosero|vocerro)\b/.test(text)) { stopped = true; recognition.abort(); void controller.start(false, undefined, "hola vocero"); return; } } }; recognition.onerror = (event) => { if (["not-allowed", "service-not-allowed", "audio-capture"].includes(event.error)) { stopped = true; } }; recognition.onend = () => { if (!stopped) { timer = setTimeout(start, 1000); } }; start(); return () => { stopped = true; clearTimeout(timer); recognition.onresult = null; recognition.onerror = null; recognition.onend = null; recognition.abort(); }; }, [enabled, controller]); }