diff --git a/app/diarizer.py b/app/diarizer.py index 45da4e4..f027650 100644 --- a/app/diarizer.py +++ b/app/diarizer.py @@ -114,14 +114,14 @@ class SpeakerDiarizer: # sous le seuil (zone morte) ET que le segment est assez long pour donner # une empreinte fiable. Sinon le segment est rattaché au meilleur profil. # Cela évite qu'un même locuteur soit éclaté en Locuteur 3, 4, 5... - NEW_SPEAKER_MARGIN = 0.20 # écart sous le seuil pour autoriser un nouveau - NEW_SPEAKER_MIN_S = 1.2 # durée mini d'un segment pour créer un locuteur + NEW_SPEAKER_MARGIN = 0.05 # écart sous le seuil pour autoriser un nouveau + NEW_SPEAKER_MIN_S = 1.0 # durée mini d'un segment pour créer un locuteur STICKY_MARGIN = 0.05 # adhérence au dernier locuteur si scores proches def __init__( self, model: EmbeddingModel, - threshold: float = 0.35, + threshold: float = 0.40, max_speakers: int = 8, ) -> None: self._model = model diff --git a/app/main.py b/app/main.py index 7b8f690..e403b7a 100644 --- a/app/main.py +++ b/app/main.py @@ -44,7 +44,7 @@ def clean_transcript(text: str) -> str: # Diarisation (identification des locuteurs) : embeddings ECAPA-TDNN sur CPU DIARIZATION = os.environ.get("DIARIZATION", "off").strip().lower() == "on" -DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.35")) +DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.40")) DIARIZATION_MAX_SPEAKERS = int(os.environ.get("DIARIZATION_MAX_SPEAKERS", "8")) LIVEFLOW_USER = os.environ.get("LIVEFLOW_USER", "admin") diff --git a/app/static/app.js b/app/static/app.js index 62d5d83..cc546a9 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -16,6 +16,7 @@ const state = { silentWarned: false, sourceNode: null, paused: false, + wakeLock: null, }; // couleurs des étiquettes de locuteurs (classes .speaker-0 à .speaker-7) @@ -137,6 +138,7 @@ async function startRecording() { state.heardSound = false; state.silentWarned = false; state.timerInterval = setInterval(updateTimer, 500); + acquireWakeLock(); // garde l'écran allumé (téléphone) $('record-btn').textContent = '■ Arrêter'; $('record-btn').classList.add('recording'); $('pause-btn').classList.remove('hidden', 'paused'); @@ -186,6 +188,7 @@ function flushPcm() { function stopRecording(abrupt = false) { state.recording = false; clearInterval(state.timerInterval); + releaseWakeLock(); if (state.sourceNode) state.sourceNode.disconnect(); if (state.workletNode) state.workletNode.disconnect(); if (state.mediaStream) state.mediaStream.getTracks().forEach((t) => t.stop()); @@ -366,6 +369,30 @@ async function copyTranscript() { // --------------------------------------------------------------------- init +// Verrou d'écran : empêche le téléphone de se mettre en veille pendant +// l'enregistrement (sinon micro/WebSocket suspendus). iOS 16.4+, Android. +async function acquireWakeLock() { + try { + if ('wakeLock' in navigator) { + state.wakeLock = await navigator.wakeLock.request('screen'); + } + } catch (e) { /* refusé / non supporté : tant pis */ } +} +function releaseWakeLock() { + try { state.wakeLock && state.wakeLock.release(); } catch (e) {} + state.wakeLock = null; +} +// iOS relâche le verrou quand l'onglet repasse au premier plan : on le reprend +document.addEventListener('visibilitychange', () => { + if (document.visibilityState === 'visible') { + if (state.recording) acquireWakeLock(); + else loadMeetings(); // rafraîchit la liste en revenant sur l'onglet + } +}); +window.addEventListener('focus', () => { if (!state.recording) loadMeetings(); }); +// rafraîchit régulièrement la liste (réunions faites depuis un autre appareil) +setInterval(() => { if (!state.recording) loadMeetings(); }, 20000); + $('record-btn').onclick = () => (state.recording ? stopRecording() : startRecording()); $('pause-btn').onclick = togglePause; $('logout-btn').onclick = async () => { await fetch('/api/logout', { method: 'POST' }); location.href = '/login'; };