Add Micro auto

This commit is contained in:
2026-04-05 10:56:06 +02:00
parent a8102e1f8c
commit 88fed85a13

View File

@@ -4,6 +4,7 @@ const API_BASE = '/api'
const AUTO_STOP_SILENCE_MS = 2500 const AUTO_STOP_SILENCE_MS = 2500
const SPEECH_START_THRESHOLD = 0.02 const SPEECH_START_THRESHOLD = 0.02
const SILENCE_THRESHOLD = 0.012 const SILENCE_THRESHOLD = 0.012
const DEBUG_AUDIO = true
async function parseApiResponse(res) { async function parseApiResponse(res) {
const contentType = res.headers.get('content-type') || '' const contentType = res.headers.get('content-type') || ''
@@ -92,6 +93,7 @@ export default function App() {
const [voiceStatus, setVoiceStatus] = useState('') const [voiceStatus, setVoiceStatus] = useState('')
const [errorMessage, setErrorMessage] = useState('') const [errorMessage, setErrorMessage] = useState('')
const [micLevel, setMicLevel] = useState(0) const [micLevel, setMicLevel] = useState(0)
const [audioDebug, setAudioDebug] = useState([])
const [voices, setVoices] = useState([]) const [voices, setVoices] = useState([])
const [selectedVoiceURI, setSelectedVoiceURI] = useState('') const [selectedVoiceURI, setSelectedVoiceURI] = useState('')
const mediaRecorderRef = useRef(null) const mediaRecorderRef = useRef(null)
@@ -122,6 +124,12 @@ export default function App() {
loadStudents() loadStudents()
}, []) }, [])
function pushAudioDebug(message) {
if (!DEBUG_AUDIO) return
const timestamp = new Date().toLocaleTimeString('fr-FR', { hour12: false })
setAudioDebug((prev) => [`${timestamp} ${message}`, ...prev].slice(0, 12))
}
useEffect(() => { useEffect(() => {
isRecordingRef.current = isRecording isRecordingRef.current = isRecording
}, [isRecording]) }, [isRecording])
@@ -275,6 +283,7 @@ export default function App() {
} }
function stopMediaStream() { function stopMediaStream() {
pushAudioDebug('Arrêt et nettoyage du flux micro')
if (animationFrameRef.current) { if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current)
animationFrameRef.current = null animationFrameRef.current = null
@@ -324,6 +333,7 @@ export default function App() {
const extension = mimeType.includes('mp4') ? 'mp4' : mimeType.includes('ogg') ? 'ogg' : 'webm' const extension = mimeType.includes('mp4') ? 'mp4' : mimeType.includes('ogg') ? 'ogg' : 'webm'
const formData = new FormData() const formData = new FormData()
formData.append('file', new File([audioBlob], `voice-input.${extension}`, { type: mimeType || 'audio/webm' })) formData.append('file', new File([audioBlob], `voice-input.${extension}`, { type: mimeType || 'audio/webm' }))
pushAudioDebug(`Transcription demandée, taille=${audioBlob.size}, mime=${mimeType || 'inconnu'}`)
setIsTranscribing(true) setIsTranscribing(true)
setVoiceStatus('Transcription en cours...') setVoiceStatus('Transcription en cours...')
@@ -335,6 +345,7 @@ export default function App() {
}) })
const transcript = (data.text || '').trim() const transcript = (data.text || '').trim()
setInput(transcript) setInput(transcript)
pushAudioDebug(`Transcription reçue, longueur=${transcript.length}`)
if (!transcript) { if (!transcript) {
setVoiceStatus('Aucun texte reconnu.') setVoiceStatus('Aucun texte reconnu.')
@@ -343,12 +354,14 @@ export default function App() {
if (autoSend && selectedStudentId) { if (autoSend && selectedStudentId) {
setVoiceStatus('Texte reconnu, envoi automatique...') setVoiceStatus('Texte reconnu, envoi automatique...')
pushAudioDebug('Envoi automatique du message transcrit')
await submitUserMessage(transcript) await submitUserMessage(transcript)
setVoiceStatus('Message vocal envoyé automatiquement.') setVoiceStatus('Message vocal envoyé automatiquement.')
} else { } else {
setVoiceStatus('Texte dicté prêt à être envoyé.') setVoiceStatus('Texte dicté prêt à être envoyé.')
} }
} catch (error) { } catch (error) {
pushAudioDebug(`Erreur transcription: ${error.message || 'inconnue'}`)
setVoiceStatus(error.message || 'Impossible de transcrire cet enregistrement.') setVoiceStatus(error.message || 'Impossible de transcrire cet enregistrement.')
} finally { } finally {
setIsTranscribing(false) setIsTranscribing(false)
@@ -381,10 +394,12 @@ export default function App() {
recordingMimeTypeRef.current = mimeType || recorder.mimeType || 'audio/webm' recordingMimeTypeRef.current = mimeType || recorder.mimeType || 'audio/webm'
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
hasSpeechInSegmentRef.current = false hasSpeechInSegmentRef.current = false
pushAudioDebug(`Démarrage enregistrement segment, mime=${recordingMimeTypeRef.current}`)
recorder.ondataavailable = (event) => { recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) { if (event.data && event.data.size > 0) {
recordedChunksRef.current.push(event.data) recordedChunksRef.current.push(event.data)
pushAudioDebug(`Chunk audio reçu, taille=${event.data.size}`)
} }
} }
@@ -394,6 +409,7 @@ export default function App() {
recordedChunksRef.current = [] recordedChunksRef.current = []
mediaRecorderRef.current = null mediaRecorderRef.current = null
setIsRecording(false) setIsRecording(false)
pushAudioDebug(`Enregistrement arrêté, taille totale=${audioBlob.size}, parole détectée=${hasSpeechInSegmentRef.current ? 'oui' : 'non'}`)
if (audioBlob.size > 0 && hasSpeechInSegmentRef.current) { if (audioBlob.size > 0 && hasSpeechInSegmentRef.current) {
await transcribeRecording(audioBlob, finalMimeType, { autoSend: true }) await transcribeRecording(audioBlob, finalMimeType, { autoSend: true })
@@ -405,6 +421,7 @@ export default function App() {
} }
recorder.onerror = () => { recorder.onerror = () => {
pushAudioDebug('Erreur MediaRecorder')
setVoiceStatus('Le navigateur a rencontré une erreur pendant lenregistrement.') setVoiceStatus('Le navigateur a rencontré une erreur pendant lenregistrement.')
setIsRecording(false) setIsRecording(false)
} }
@@ -418,6 +435,7 @@ export default function App() {
const recorder = mediaRecorderRef.current const recorder = mediaRecorderRef.current
if (recorder && recorder.state !== 'inactive') { if (recorder && recorder.state !== 'inactive') {
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
pushAudioDebug('Arrêt du segment demandé après silence')
recorder.stop() recorder.stop()
setVoiceStatus(statusMessage) setVoiceStatus(statusMessage)
} }
@@ -431,12 +449,17 @@ export default function App() {
setMicLevel(volume) setMicLevel(volume)
if (volume >= SPEECH_START_THRESHOLD) { if (volume >= SPEECH_START_THRESHOLD) {
if (!hasSpeechInSegmentRef.current) {
pushAudioDebug(`Parole détectée, niveau=${volume.toFixed(4)}`)
}
hasSpeechInSegmentRef.current = true hasSpeechInSegmentRef.current = true
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
} else if (isRecordingRef.current && hasSpeechInSegmentRef.current && volume <= SILENCE_THRESHOLD) { } else if (isRecordingRef.current && hasSpeechInSegmentRef.current && volume <= SILENCE_THRESHOLD) {
if (!silenceStartedAtRef.current) { if (!silenceStartedAtRef.current) {
pushAudioDebug(`Début silence, niveau=${volume.toFixed(4)}`)
silenceStartedAtRef.current = now silenceStartedAtRef.current = now
} else if (now - silenceStartedAtRef.current >= AUTO_STOP_SILENCE_MS) { } else if (now - silenceStartedAtRef.current >= AUTO_STOP_SILENCE_MS) {
pushAudioDebug('Silence confirmé, arrêt du segment')
stopSegmentRecording() stopSegmentRecording()
} }
} else if (volume > SILENCE_THRESHOLD) { } else if (volume > SILENCE_THRESHOLD) {
@@ -449,10 +472,12 @@ export default function App() {
async function activateAutoListening() { async function activateAutoListening() {
if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') { if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
setVoiceStatus('Le micro automatique nest pas pris en charge par ce navigateur.') setVoiceStatus('Le micro automatique nest pas pris en charge par ce navigateur.')
pushAudioDebug('Navigateur incompatible micro auto')
return return
} }
try { try {
pushAudioDebug('Demande accès micro')
const stream = await navigator.mediaDevices.getUserMedia({ const stream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
echoCancellation: true, echoCancellation: true,
@@ -465,6 +490,7 @@ export default function App() {
if (audioContext.state === 'suspended') { if (audioContext.state === 'suspended') {
await audioContext.resume() await audioContext.resume()
} }
pushAudioDebug(`Micro autorisé, AudioContext=${audioContext.state}`)
const analyser = audioContext.createAnalyser() const analyser = audioContext.createAnalyser()
analyser.fftSize = 2048 analyser.fftSize = 2048
analyser.smoothingTimeConstant = 0.85 analyser.smoothingTimeConstant = 0.85
@@ -482,15 +508,18 @@ export default function App() {
mediaStreamRef.current = stream mediaStreamRef.current = stream
setIsAutoListening(true) setIsAutoListening(true)
setVoiceStatus('Micro actif. Parle quand tu veux, jenverrai après 2,5 s de silence.') setVoiceStatus('Micro actif. Parle quand tu veux, jenverrai après 2,5 s de silence.')
pushAudioDebug(`Piste micro active=${stream.getAudioTracks()[0]?.readyState || 'inconnue'}`)
await startSegmentRecording() await startSegmentRecording()
monitorMicrophone() monitorMicrophone()
} catch { } catch {
pushAudioDebug('Accès micro refusé ou indisponible')
setVoiceStatus('Accès au micro refusé ou indisponible.') setVoiceStatus('Accès au micro refusé ou indisponible.')
stopMediaStream() stopMediaStream()
} }
} }
function deactivateAutoListening(resetStatus = true) { function deactivateAutoListening(resetStatus = true) {
pushAudioDebug('Désactivation micro auto')
setIsAutoListening(false) setIsAutoListening(false)
if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') { if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
mediaRecorderRef.current.onstop = null mediaRecorderRef.current.onstop = null
@@ -634,6 +663,11 @@ export default function App() {
)} )}
{voiceStatus && <p className="muted voice-status">{voiceStatus}</p>} {voiceStatus && <p className="muted voice-status">{voiceStatus}</p>}
{errorMessage && <p className="muted voice-status">{errorMessage}</p>} {errorMessage && <p className="muted voice-status">{errorMessage}</p>}
{DEBUG_AUDIO && (
<div className="voice-status">
{audioDebug.length === 0 ? 'Debug audio: aucune trace' : `Debug audio: ${audioDebug.join(' | ')}`}
</div>
)}
</main> </main>
</div> </div>
) )