Add Micro auto
This commit is contained in:
@@ -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 l’enregistrement.')
|
||||
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 n’est 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, j’enverrai 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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user