whung99
feat: deploy Oppy with Google API integration
0d37119
import { useEffect, useRef, useState, useCallback } from 'react';
// Very broad matching — French STT transcribes "Oppy" in many unexpected ways
function matchesWakeWord(transcript) {
const t = transcript.toLowerCase().replace(/[.,!?'"\-]/g, ' ').trim();
return /opp?[iey]|au?pp?[iey]|oh? p[iey]|au pi|o[bp]+ ?[iey]|hop[iey]|op[iey]|hey op|ok op|aux? pi/.test(t);
}
export default function useWakeWord({ enabled, onDetected }) {
const recognitionRef = useRef(null);
const isRunningRef = useRef(false);
const enabledRef = useRef(enabled);
const [micActive, setMicActive] = useState(false);
const [micError, setMicError] = useState(false);
enabledRef.current = enabled;
const startListening = useCallback(() => {
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition || !enabledRef.current || isRunningRef.current) return;
const recognition = new SpeechRecognition();
recognition.lang = 'fr-FR';
recognition.continuous = true;
recognition.interimResults = true;
recognition.onstart = () => {
setMicActive(true);
setMicError(false);
};
recognition.onresult = (event) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (matchesWakeWord(transcript)) {
recognition.stop();
isRunningRef.current = false;
setMicActive(false);
onDetected();
return;
}
}
};
recognition.onend = () => {
isRunningRef.current = false;
setMicActive(false);
// Only restart if still enabled AND this recognition instance is still current
if (enabledRef.current && recognitionRef.current === recognition) {
setTimeout(() => {
if (enabledRef.current) startListening();
}, 300);
}
};
recognition.onerror = (event) => {
isRunningRef.current = false;
setMicActive(false);
if (event.error === 'not-allowed') {
setMicError(true);
return;
}
if (enabledRef.current) {
setTimeout(() => startListening(), 500);
}
};
recognitionRef.current = recognition;
isRunningRef.current = true;
try {
recognition.start();
} catch {
isRunningRef.current = false;
setMicActive(false);
}
}, [onDetected]);
// Manual trigger — needed because browsers block auto-start without user gesture
const requestMic = useCallback(() => {
if (!isRunningRef.current) {
setMicError(false);
startListening();
}
}, [startListening]);
useEffect(() => {
if (enabled) {
// Try auto-start (works if mic was already granted)
startListening();
} else {
if (recognitionRef.current) {
try { recognitionRef.current.abort(); } catch {}
try { recognitionRef.current.stop(); } catch {}
recognitionRef.current = null;
isRunningRef.current = false;
}
setMicActive(false);
}
return () => {
if (recognitionRef.current) {
try { recognitionRef.current.abort(); } catch {}
try { recognitionRef.current.stop(); } catch {}
recognitionRef.current = null;
isRunningRef.current = false;
}
};
}, [enabled, startListening]);
return { micActive, micError, requestMic };
}