feat: gestion de pause/reprise de reunion et correction du bug de sigles chinois (ASR francais par defaut)

This commit is contained in:
Michael committed 2026-06-12 16:53:24 +02:00
1 parent 6d1419492f
commit 52fda48e5f
7 files changed
+82 -8

No files matched your search

+2 -2
View File
@@ -3,8 +3,8 @@
LIVEFLOW_HOST=192.168.1.16
# Code langue ISO forcé pour la transcription ("fr", "en"...).
# Laisser vide pour la détection automatique de la langue.
ASR_LANGUAGE=
# Par défaut : "fr" (français). Mettre "auto" pour la détection automatique.
ASR_LANGUAGE=fr
# Clé API transmise au moteur ASR (sans importance en local).
ASR_API_KEY=sk-local
+9 -2
View File
@@ -18,7 +18,7 @@ DB_PATH = os.environ.get("DB_PATH", "/data/liveflow.db")
ASR_BASE_URL = os.environ.get("ASR_BASE_URL", "http://asr:8000/v1").rstrip("/")
ASR_MODEL = os.environ.get("ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
ASR_API_KEY = os.environ.get("ASR_API_KEY", "sk-local")
ASR_LANGUAGE = os.environ.get("ASR_LANGUAGE", "").strip()
ASR_LANGUAGE = os.environ.get("ASR_LANGUAGE", "").strip() or "fr"
DIARIZATION = os.environ.get("DIARIZATION", "off").strip().lower() == "on"
DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.70"))
DIARIZATION_MAX_SPEAKERS = int(os.environ.get("DIARIZATION_MAX_SPEAKERS", "8"))
@@ -197,7 +197,8 @@ async def ws_transcribe(ws: WebSocket):
queue.put_nowait(seg)
elif msg.get("text"):
control = json.loads(msg["text"])
if control.get("type") == "stop":
cmd = control.get("type")
if cmd == "stop":
if (last := segmenter.flush()) is not None:
queue.put_nowait(last)
queue.put_nowait(None)
@@ -205,6 +206,12 @@ async def ws_transcribe(ws: WebSocket):
worker_task = None
await ws.send_json({"type": "done", "meeting_id": meeting_id})
break
elif cmd == "pause":
# Mettre en pause : forcer la finalisation du segment audio en cours
if (last := segmenter.flush()) is not None:
queue.put_nowait(last)
elif cmd == "resume":
pass
except WebSocketDisconnect:
pass
finally:
+53 -1
View File
@@ -35,6 +35,7 @@ if (window.isSecureContext === false) {
const state = {
recording: false,
paused: false,
ws: null,
audioContext: null,
workletNode: null,
@@ -43,6 +44,8 @@ const state = {
sendBufferSamples: 0,
timerInterval: null,
startedAt: null,
elapsedMs: 0,
lastTimerUpdate: null,
currentMeetingId: null,
};
@@ -141,12 +144,21 @@ async function startRecording() {
// 4. Mettre à jour l'interface
state.recording = true;
state.paused = false;
state.startedAt = Date.now();
state.elapsedMs = 0;
state.lastTimerUpdate = Date.now();
state.timerInterval = setInterval(updateTimer, 500);
if ($('record-btn')) {
$('record-btn').textContent = '■ Arrêter';
$('record-btn').classList.add('recording');
}
if ($('pause-btn')) {
$('pause-btn').classList.remove('hidden');
$('pause-btn').classList.remove('paused');
$('pause-btn').textContent = '⏸ Pause';
}
if ($('title')) $('title').disabled = true;
setStatus('rec', 'Enregistrement…');
clearTranscript();
@@ -208,6 +220,10 @@ function stopRecording(abrupt = false) {
$('record-btn').textContent = '● Démarrer';
$('record-btn').classList.remove('recording');
}
if ($('pause-btn')) {
$('pause-btn').classList.add('hidden');
}
state.paused = false;
if ($('title')) $('title').disabled = false;
const bar = $('volume-bar');
@@ -232,6 +248,7 @@ function updateVolumeIndicator(samples) {
function queuePcm(samples) {
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
if (state.paused) return;
updateVolumeIndicator(samples);
state.sendBuffer.push(samples);
state.sendBufferSamples += samples.length;
@@ -277,7 +294,11 @@ function setStatus(cls, text) {
}
function updateTimer() {
const s = Math.floor((Date.now() - state.startedAt) / 1000);
if (state.paused) return;
const now = Date.now();
state.elapsedMs += now - state.lastTimerUpdate;
state.lastTimerUpdate = now;
const s = Math.floor(state.elapsedMs / 1000);
$('timer').textContent =
String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
}
@@ -372,9 +393,40 @@ async function copyTranscript() {
setTimeout(() => ($('copy-btn').textContent = '📋 Copier'), 1500);
}
function togglePause() {
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
const btn = $('pause-btn');
if (!state.paused) {
// Mettre en pause
state.paused = true;
state.elapsedMs += Date.now() - state.lastTimerUpdate;
try { state.ws.send(JSON.stringify({ type: 'pause' })); } catch (e) {}
setStatus('busy', 'Pause'); // Indicateur visuel "Pause"
if (btn) {
btn.textContent = '▶ Reprendre';
btn.classList.add('paused');
}
// Remettre la barre de volume à 0% pendant la pause
const bar = $('volume-bar');
if (bar) bar.style.width = '0%';
} else {
// Reprendre
state.paused = false;
state.lastTimerUpdate = Date.now();
try { state.ws.send(JSON.stringify({ type: 'resume' })); } catch (e) {}
setStatus('rec', 'Enregistrement…');
if (btn) {
btn.textContent = '⏸ Pause';
btn.classList.remove('paused');
}
}
}
// --------------------------------------------------------------------- init
$('record-btn').onclick = () => (state.recording ? stopRecording() : startRecording());
if ($('pause-btn')) $('pause-btn').onclick = togglePause;
$('copy-btn').onclick = copyTranscript;
$('delete-btn').onclick = deleteCurrentMeeting;
loadMeetings();
+1
View File
@@ -26,6 +26,7 @@
<option value="">Micro par défaut</option>
</select>
<button id="record-btn" class="record">● Démarrer</button>
<button id="pause-btn" class="pause hidden">⏸ Pause</button>
<span id="timer">00:00</span>
<div id="volume-container" class="volume-container" title="Volume du micro">
<div id="volume-bar" class="volume-bar"></div>
+11 -1
View File
@@ -206,7 +206,17 @@ button.record.recording { background: var(--rec); }
border-radius: 2px;
transition: width 0.1s ease;
}
/* Bouton Pause */
button.pause {
background: #2a2410;
color: #ffd166;
font-weight: 600;
min-width: 110px;
}
button.pause.paused {
background: var(--accent);
color: var(--text);
}
@media (max-width: 720px) {
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- ASR_MODEL=Qwen/Qwen3-ASR-1.7B
- ASR_API_KEY=${ASR_API_KEY:-sk-local}
# Code langue ISO ("fr", "en"...) ou vide pour la détection automatique
- ASR_LANGUAGE=${ASR_LANGUAGE:-}
- ASR_LANGUAGE=${ASR_LANGUAGE:-fr}
- DB_PATH=/data/liveflow.db
# Diarisation des locuteurs (off par défaut pour rétrocompatibilité)
- DIARIZATION=${DIARIZATION:-off}
+5 -1
View File
@@ -22,7 +22,11 @@ Vérification de la cohérence de tous les fichiers (backend et frontend) et tes
- [x] Rendre configurable le nombre maximal de locuteurs (DIARIZATION_MAX_SPEAKERS, défaut 8) dans main.py, .env.example et docker-compose.yml
- [x] Créer l'afficheur de volume micro en temps réel (visualiseur audio) dans index.html, style.css et app.js
- [x] Mettre en place la capture et l'affichage des erreurs JS non gérées à l'écran pour faciliter le diagnostic
- [x] Modifier `app/static/index.html` pour intégrer le bouton de pause
- [x] Modifier `app/static/style.css` pour styliser le bouton de pause
- [x] Modifier `app/static/app.js` pour gérer l'état de pause (timer, flux audio et boutons)
- [x] Modifier `app/main.py` pour traiter les commandes WebSocket `pause` et `resume` (flush VAD)
- [x] Résoudre le problème d'apparition des sigles chinois en forçant le français (ASR_LANGUAGE=fr) par défaut dans le code, le docker-compose et l'environnement.
## Progress Log
- Création du module `diarizer.py` pour l'extraction des embeddings et le clustering.