Add Micro auto
This commit is contained in:
@@ -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 l’enregistrement.')
|
setVoiceStatus('Le navigateur a rencontré une erreur pendant l’enregistrement.')
|
||||||
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 n’est pas pris en charge par ce navigateur.')
|
setVoiceStatus('Le micro automatique n’est 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, j’enverrai après 2,5 s de silence.')
|
setVoiceStatus('Micro actif. Parle quand tu veux, j’enverrai 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>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user