mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-11 17:27:19 +02:00
feat: gestion de pause/reprise de reunion et correction du bug de sigles chinois (ASR francais par defaut)
This commit is contained in:
1 parent
6d1419492f
commit
52fda48e5f
7 files changed
+82
-8
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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();
|
||||
|
||||
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in new issue
Block a user