File size: 3,401 Bytes
0d37119
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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 };
}