CognxSafeTrack commited on
Commit
4fd3a34
·
1 Parent(s): 3165bfa

feat: implement real-time streaming transcription using Web Speech API

Browse files
apps/admin/src/hooks/useAudioRecorder.ts CHANGED
@@ -1,86 +1,76 @@
1
- import { useState, useRef } from 'react';
2
 
3
  interface UseAudioRecorderProps {
4
  onTranscriptionComplete: (text: string) => void;
5
- apiUrl: string;
6
- token: string;
7
- organizationId: string;
8
  }
9
 
10
- export const useAudioRecorder = ({ onTranscriptionComplete, apiUrl, token, organizationId }: UseAudioRecorderProps) => {
11
  const [isRecording, setIsRecording] = useState(false);
12
- const [isTranscribing, setIsTranscribing] = useState(false);
13
- const mediaRecorderRef = useRef<MediaRecorder | null>(null);
14
- const audioChunksRef = useRef<Blob[]>([]);
15
 
16
- const startRecording = async () => {
17
- try {
18
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
19
- const mediaRecorder = new MediaRecorder(stream);
20
- mediaRecorderRef.current = mediaRecorder;
21
- audioChunksRef.current = [];
 
22
 
23
- mediaRecorder.ondataavailable = (event) => {
24
- if (event.data.size > 0) {
25
- audioChunksRef.current.push(event.data);
 
26
  }
 
 
 
 
 
 
27
  };
28
 
29
- mediaRecorder.onstop = async () => {
30
- const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/webm' });
31
- await sendAudioToTranscription(audioBlob);
32
- stream.getTracks().forEach(track => track.stop());
33
  };
34
 
35
- mediaRecorder.start();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  setIsRecording(true);
37
  } catch (err) {
38
- console.error("Failed to start recording:", err);
39
- alert("Impossible d'accéder au micro.");
 
40
  }
41
  };
42
 
43
  const stopRecording = () => {
44
- if (mediaRecorderRef.current && isRecording) {
45
- mediaRecorderRef.current.stop();
46
  setIsRecording(false);
47
  }
48
  };
49
 
50
- const sendAudioToTranscription = async (blob: Blob) => {
51
- if (!token || !organizationId) return;
52
-
53
- setIsTranscribing(true);
54
- const formData = new FormData();
55
- formData.append('file', blob, 'recording.webm');
56
-
57
- try {
58
- const res = await fetch(`${apiUrl}/v1/organizations/${organizationId}/campaigns/transcribe`, {
59
- method: 'POST',
60
- headers: {
61
- 'Authorization': `Bearer ${token}`
62
- },
63
- body: formData
64
- });
65
-
66
- if (res.ok) {
67
- const data = await res.json();
68
- if (data.text) {
69
- onTranscriptionComplete(data.text);
70
- }
71
- } else {
72
- console.error("Transcription failed");
73
- }
74
- } catch (err) {
75
- console.error("Transcription error:", err);
76
- } finally {
77
- setIsTranscribing(false);
78
- }
79
- };
80
-
81
  return {
82
  isRecording,
83
- isTranscribing,
84
  startRecording,
85
  stopRecording
86
  };
 
1
+ import { useState, useRef, useEffect } from 'react';
2
 
3
  interface UseAudioRecorderProps {
4
  onTranscriptionComplete: (text: string) => void;
5
+ apiUrl?: string;
6
+ token?: string;
7
+ organizationId?: string;
8
  }
9
 
10
+ export const useAudioRecorder = ({ onTranscriptionComplete }: UseAudioRecorderProps) => {
11
  const [isRecording, setIsRecording] = useState(false);
12
+ const recognitionRef = useRef<any>(null);
 
 
13
 
14
+ useEffect(() => {
15
+ const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
16
+ if (SpeechRecognition) {
17
+ const recognition = new SpeechRecognition();
18
+ recognition.lang = 'fr-FR';
19
+ recognition.interimResults = true;
20
+ recognition.continuous = false;
21
 
22
+ recognition.onresult = (event: any) => {
23
+ let currentTranscript = "";
24
+ for (let i = 0; i < event.results.length; i++) {
25
+ currentTranscript += event.results[i][0].transcript;
26
  }
27
+ onTranscriptionComplete(currentTranscript);
28
+ };
29
+
30
+ recognition.onerror = (event: any) => {
31
+ console.error("Speech recognition error:", event.error);
32
+ setIsRecording(false);
33
  };
34
 
35
+ recognition.onend = () => {
36
+ setIsRecording(false);
 
 
37
  };
38
 
39
+ recognitionRef.current = recognition;
40
+ }
41
+
42
+ return () => {
43
+ if (recognitionRef.current) {
44
+ recognitionRef.current.stop();
45
+ }
46
+ };
47
+ }, [onTranscriptionComplete]);
48
+
49
+ const startRecording = () => {
50
+ if (!recognitionRef.current) {
51
+ alert("La dictée vocale instantanée n'est pas supportée sur ce navigateur.");
52
+ return;
53
+ }
54
+ try {
55
+ recognitionRef.current.start();
56
  setIsRecording(true);
57
  } catch (err) {
58
+ console.error("Failed to start speech recognition:", err);
59
+ // If already started, just set state
60
+ setIsRecording(true);
61
  }
62
  };
63
 
64
  const stopRecording = () => {
65
+ if (recognitionRef.current && isRecording) {
66
+ recognitionRef.current.stop();
67
  setIsRecording(false);
68
  }
69
  };
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  return {
72
  isRecording,
73
+ isTranscribing: false, // No longer needed as it's real-time
74
  startRecording,
75
  stopRecording
76
  };
apps/admin/src/pages/CrmConversationalDashboard.tsx CHANGED
@@ -42,10 +42,8 @@ export default function CrmConversationalDashboard() {
42
  const [uploadedFile, setUploadedFile] = useState<{ name: string, listId: string, listName: string } | null>(null);
43
 
44
  // Recording & Transcription logic moved to useAudioRecorder hook
 
45
  const { isRecording, startRecording, stopRecording } = useAudioRecorder({
46
- apiUrl: import.meta.env.VITE_API_URL,
47
- token: token || '',
48
- organizationId: selectedOrgId || '',
49
  onTranscriptionComplete: (text) => setInput(text)
50
  });
51
 
 
42
  const [uploadedFile, setUploadedFile] = useState<{ name: string, listId: string, listName: string } | null>(null);
43
 
44
  // Recording & Transcription logic moved to useAudioRecorder hook
45
+ // Real-time streaming transcription logic
46
  const { isRecording, startRecording, stopRecording } = useAudioRecorder({
 
 
 
47
  onTranscriptionComplete: (text) => setInput(text)
48
  });
49