mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-16 19:56:21 +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
|
LIVEFLOW_HOST=192.168.1.16
|
||||||
|
|
||||||
# Code langue ISO forcé pour la transcription ("fr", "en"...).
|
# Code langue ISO forcé pour la transcription ("fr", "en"...).
|
||||||
# Laisser vide pour la détection automatique de la langue.
|
# Par défaut : "fr" (français). Mettre "auto" pour la détection automatique.
|
||||||
ASR_LANGUAGE=
|
ASR_LANGUAGE=fr
|
||||||
|
|
||||||
# Clé API transmise au moteur ASR (sans importance en local).
|
# Clé API transmise au moteur ASR (sans importance en local).
|
||||||
ASR_API_KEY=sk-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_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_MODEL = os.environ.get("ASR_MODEL", "Qwen/Qwen3-ASR-1.7B")
|
||||||
ASR_API_KEY = os.environ.get("ASR_API_KEY", "sk-local")
|
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 = os.environ.get("DIARIZATION", "off").strip().lower() == "on"
|
||||||
DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.70"))
|
DIARIZATION_THRESHOLD = float(os.environ.get("DIARIZATION_THRESHOLD", "0.70"))
|
||||||
DIARIZATION_MAX_SPEAKERS = int(os.environ.get("DIARIZATION_MAX_SPEAKERS", "8"))
|
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)
|
queue.put_nowait(seg)
|
||||||
elif msg.get("text"):
|
elif msg.get("text"):
|
||||||
control = json.loads(msg["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:
|
if (last := segmenter.flush()) is not None:
|
||||||
queue.put_nowait(last)
|
queue.put_nowait(last)
|
||||||
queue.put_nowait(None)
|
queue.put_nowait(None)
|
||||||
@@ -205,6 +206,12 @@ async def ws_transcribe(ws: WebSocket):
|
|||||||
worker_task = None
|
worker_task = None
|
||||||
await ws.send_json({"type": "done", "meeting_id": meeting_id})
|
await ws.send_json({"type": "done", "meeting_id": meeting_id})
|
||||||
break
|
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:
|
except WebSocketDisconnect:
|
||||||
pass
|
pass
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
+53
-1
@@ -35,6 +35,7 @@ if (window.isSecureContext === false) {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
recording: false,
|
recording: false,
|
||||||
|
paused: false,
|
||||||
ws: null,
|
ws: null,
|
||||||
audioContext: null,
|
audioContext: null,
|
||||||
workletNode: null,
|
workletNode: null,
|
||||||
@@ -43,6 +44,8 @@ const state = {
|
|||||||
sendBufferSamples: 0,
|
sendBufferSamples: 0,
|
||||||
timerInterval: null,
|
timerInterval: null,
|
||||||
startedAt: null,
|
startedAt: null,
|
||||||
|
elapsedMs: 0,
|
||||||
|
lastTimerUpdate: null,
|
||||||
currentMeetingId: null,
|
currentMeetingId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -141,12 +144,21 @@ async function startRecording() {
|
|||||||
|
|
||||||
// 4. Mettre à jour l'interface
|
// 4. Mettre à jour l'interface
|
||||||
state.recording = true;
|
state.recording = true;
|
||||||
|
state.paused = false;
|
||||||
state.startedAt = Date.now();
|
state.startedAt = Date.now();
|
||||||
|
state.elapsedMs = 0;
|
||||||
|
state.lastTimerUpdate = Date.now();
|
||||||
state.timerInterval = setInterval(updateTimer, 500);
|
state.timerInterval = setInterval(updateTimer, 500);
|
||||||
|
|
||||||
if ($('record-btn')) {
|
if ($('record-btn')) {
|
||||||
$('record-btn').textContent = '■ Arrêter';
|
$('record-btn').textContent = '■ Arrêter';
|
||||||
$('record-btn').classList.add('recording');
|
$('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;
|
if ($('title')) $('title').disabled = true;
|
||||||
setStatus('rec', 'Enregistrement…');
|
setStatus('rec', 'Enregistrement…');
|
||||||
clearTranscript();
|
clearTranscript();
|
||||||
@@ -208,6 +220,10 @@ function stopRecording(abrupt = false) {
|
|||||||
$('record-btn').textContent = '● Démarrer';
|
$('record-btn').textContent = '● Démarrer';
|
||||||
$('record-btn').classList.remove('recording');
|
$('record-btn').classList.remove('recording');
|
||||||
}
|
}
|
||||||
|
if ($('pause-btn')) {
|
||||||
|
$('pause-btn').classList.add('hidden');
|
||||||
|
}
|
||||||
|
state.paused = false;
|
||||||
if ($('title')) $('title').disabled = false;
|
if ($('title')) $('title').disabled = false;
|
||||||
|
|
||||||
const bar = $('volume-bar');
|
const bar = $('volume-bar');
|
||||||
@@ -232,6 +248,7 @@ function updateVolumeIndicator(samples) {
|
|||||||
|
|
||||||
function queuePcm(samples) {
|
function queuePcm(samples) {
|
||||||
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
||||||
|
if (state.paused) return;
|
||||||
updateVolumeIndicator(samples);
|
updateVolumeIndicator(samples);
|
||||||
state.sendBuffer.push(samples);
|
state.sendBuffer.push(samples);
|
||||||
state.sendBufferSamples += samples.length;
|
state.sendBufferSamples += samples.length;
|
||||||
@@ -277,7 +294,11 @@ function setStatus(cls, text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateTimer() {
|
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 =
|
$('timer').textContent =
|
||||||
String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
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);
|
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
|
// --------------------------------------------------------------------- init
|
||||||
|
|
||||||
$('record-btn').onclick = () => (state.recording ? stopRecording() : startRecording());
|
$('record-btn').onclick = () => (state.recording ? stopRecording() : startRecording());
|
||||||
|
if ($('pause-btn')) $('pause-btn').onclick = togglePause;
|
||||||
$('copy-btn').onclick = copyTranscript;
|
$('copy-btn').onclick = copyTranscript;
|
||||||
$('delete-btn').onclick = deleteCurrentMeeting;
|
$('delete-btn').onclick = deleteCurrentMeeting;
|
||||||
loadMeetings();
|
loadMeetings();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<option value="">Micro par défaut</option>
|
<option value="">Micro par défaut</option>
|
||||||
</select>
|
</select>
|
||||||
<button id="record-btn" class="record">● Démarrer</button>
|
<button id="record-btn" class="record">● Démarrer</button>
|
||||||
|
<button id="pause-btn" class="pause hidden">⏸ Pause</button>
|
||||||
<span id="timer">00:00</span>
|
<span id="timer">00:00</span>
|
||||||
<div id="volume-container" class="volume-container" title="Volume du micro">
|
<div id="volume-container" class="volume-container" title="Volume du micro">
|
||||||
<div id="volume-bar" class="volume-bar"></div>
|
<div id="volume-bar" class="volume-bar"></div>
|
||||||
|
|||||||
+11
-1
@@ -206,7 +206,17 @@ button.record.recording { background: var(--rec); }
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: width 0.1s ease;
|
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) {
|
@media (max-width: 720px) {
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@ services:
|
|||||||
- ASR_MODEL=Qwen/Qwen3-ASR-1.7B
|
- ASR_MODEL=Qwen/Qwen3-ASR-1.7B
|
||||||
- ASR_API_KEY=${ASR_API_KEY:-sk-local}
|
- ASR_API_KEY=${ASR_API_KEY:-sk-local}
|
||||||
# Code langue ISO ("fr", "en"...) ou vide pour la détection automatique
|
# 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
|
- DB_PATH=/data/liveflow.db
|
||||||
# Diarisation des locuteurs (off par défaut pour rétrocompatibilité)
|
# Diarisation des locuteurs (off par défaut pour rétrocompatibilité)
|
||||||
- DIARIZATION=${DIARIZATION:-off}
|
- 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] 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] 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] 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
|
## Progress Log
|
||||||
- Création du module `diarizer.py` pour l'extraction des embeddings et le clustering.
|
- Création du module `diarizer.py` pour l'extraction des embeddings et le clustering.
|
||||||
|
|||||||
Reference in new issue
Block a user