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