Add Auto micro

This commit is contained in:
2026-04-05 10:49:43 +02:00
parent 73a78fd7a5
commit 6ee5296a82

View File

@@ -2,7 +2,8 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'
const API_BASE = '/api' const API_BASE = '/api'
const AUTO_STOP_SILENCE_MS = 2500 const AUTO_STOP_SILENCE_MS = 2500
const SPEECH_START_THRESHOLD = 0.05 const SPEECH_START_THRESHOLD = 0.02
const SILENCE_THRESHOLD = 0.012
async function parseApiResponse(res) { async function parseApiResponse(res) {
const contentType = res.headers.get('content-type') || '' const contentType = res.headers.get('content-type') || ''
@@ -90,6 +91,7 @@ export default function App() {
const [isTranscribing, setIsTranscribing] = useState(false) const [isTranscribing, setIsTranscribing] = useState(false)
const [voiceStatus, setVoiceStatus] = useState('') const [voiceStatus, setVoiceStatus] = useState('')
const [errorMessage, setErrorMessage] = useState('') const [errorMessage, setErrorMessage] = useState('')
const [micLevel, setMicLevel] = useState(0)
const [voices, setVoices] = useState([]) const [voices, setVoices] = useState([])
const [selectedVoiceURI, setSelectedVoiceURI] = useState('') const [selectedVoiceURI, setSelectedVoiceURI] = useState('')
const mediaRecorderRef = useRef(null) const mediaRecorderRef = useRef(null)
@@ -101,6 +103,7 @@ export default function App() {
const sourceNodeRef = useRef(null) const sourceNodeRef = useRef(null)
const animationFrameRef = useRef(null) const animationFrameRef = useRef(null)
const silenceStartedAtRef = useRef(null) const silenceStartedAtRef = useRef(null)
const hasSpeechInSegmentRef = useRef(false)
const isRecordingRef = useRef(false) const isRecordingRef = useRef(false)
const isAutoListeningRef = useRef(false) const isAutoListeningRef = useRef(false)
const isTranscribingRef = useRef(false) const isTranscribingRef = useRef(false)
@@ -285,6 +288,8 @@ export default function App() {
} }
analyserRef.current = null analyserRef.current = null
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
hasSpeechInSegmentRef.current = false
setMicLevel(0)
if (mediaStreamRef.current) { if (mediaStreamRef.current) {
mediaStreamRef.current.getTracks().forEach((track) => track.stop()) mediaStreamRef.current.getTracks().forEach((track) => track.stop())
mediaStreamRef.current = null mediaStreamRef.current = null
@@ -370,6 +375,7 @@ export default function App() {
recordedChunksRef.current = [] recordedChunksRef.current = []
recordingMimeTypeRef.current = mimeType || recorder.mimeType || 'audio/webm' recordingMimeTypeRef.current = mimeType || recorder.mimeType || 'audio/webm'
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
hasSpeechInSegmentRef.current = false
recorder.ondataavailable = (event) => { recorder.ondataavailable = (event) => {
if (event.data && event.data.size > 0) { if (event.data && event.data.size > 0) {
@@ -384,11 +390,13 @@ export default function App() {
mediaRecorderRef.current = null mediaRecorderRef.current = null
setIsRecording(false) setIsRecording(false)
if (audioBlob.size > 0) { if (audioBlob.size > 0 && hasSpeechInSegmentRef.current) {
await transcribeRecording(audioBlob, finalMimeType, { autoSend: true }) await transcribeRecording(audioBlob, finalMimeType, { autoSend: true })
} else if (isAutoListeningRef.current) { } else if (isAutoListeningRef.current) {
setVoiceStatus('Micro actif. Parle quand tu veux.') setVoiceStatus('Micro actif. Parle quand tu veux.')
} }
hasSpeechInSegmentRef.current = false
} }
recorder.onerror = () => { recorder.onerror = () => {
@@ -415,18 +423,19 @@ export default function App() {
const volume = calculateVolume(analyserRef.current) const volume = calculateVolume(analyserRef.current)
const now = Date.now() const now = Date.now()
setMicLevel(volume)
if (volume >= SPEECH_START_THRESHOLD) { if (volume >= SPEECH_START_THRESHOLD) {
hasSpeechInSegmentRef.current = true
silenceStartedAtRef.current = null silenceStartedAtRef.current = null
if (!isRecordingRef.current && !isTranscribingRef.current) { } else if (isRecordingRef.current && hasSpeechInSegmentRef.current && volume <= SILENCE_THRESHOLD) {
startSegmentRecording()
}
} else if (isRecordingRef.current) {
if (!silenceStartedAtRef.current) { if (!silenceStartedAtRef.current) {
silenceStartedAtRef.current = now silenceStartedAtRef.current = now
} else if (now - silenceStartedAtRef.current >= AUTO_STOP_SILENCE_MS) { } else if (now - silenceStartedAtRef.current >= AUTO_STOP_SILENCE_MS) {
stopSegmentRecording() stopSegmentRecording()
} }
} else if (volume > SILENCE_THRESHOLD) {
silenceStartedAtRef.current = null
} }
animationFrameRef.current = requestAnimationFrame(monitorMicrophone) animationFrameRef.current = requestAnimationFrame(monitorMicrophone)
@@ -459,6 +468,7 @@ 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.')
await startSegmentRecording()
monitorMicrophone() monitorMicrophone()
} catch { } catch {
setVoiceStatus('Accès au micro refusé ou indisponible.') setVoiceStatus('Accès au micro refusé ou indisponible.')
@@ -603,6 +613,11 @@ export default function App() {
<button type="submit">Envoyer</button> <button type="submit">Envoyer</button>
</form> </form>
{isAutoListening && (
<div className="voice-status">
Niveau micro: {Math.round(Math.min(micLevel * 1200, 100))}%
</div>
)}
{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>}
</main> </main>