mirror of
https://github.com/R0m1k3/LiveFlow.git
synced 2026-10-14 02:38:44 +02:00
Diarisation des locuteurs et pause/reprise (intégration des travaux de main)
- diarizer.py (ECAPA-TDNN + clustering incrémental) repris tel quel de main - Hooks dans le pipeline : colonne speaker, identification par segment dans un thread, locuteur dans l'interface (étiquettes colorées) et les exports - Pause/reprise : bouton ⏸, le client coupe l'envoi, le serveur clôt le segment en cours - Case « Locuteurs » pour désactiver la diarisation par réunion - DIARIZATION=on par défaut dans les compose, PyTorch CPU dans l'image https://claude.ai/code/session_01YHMp3EKzr4s6o8w1ygxuUe
This commit is contained in:
10 files changed
+352
-20
No files matched your search
+58
-5
@@ -16,8 +16,18 @@ const state = {
|
||||
sourceNode: null,
|
||||
autoMicQueue: null,
|
||||
switching: false,
|
||||
paused: false,
|
||||
};
|
||||
|
||||
// couleurs des étiquettes de locuteurs (classes .speaker-0 à .speaker-7)
|
||||
const speakerColors = {};
|
||||
function speakerColorIndex(speaker) {
|
||||
if (!(speaker in speakerColors)) {
|
||||
speakerColors[speaker] = Object.keys(speakerColors).length % 8;
|
||||
}
|
||||
return speakerColors[speaker];
|
||||
}
|
||||
|
||||
const BATCH_SAMPLES = 4096; // ~256 ms de PCM 16 kHz par message WebSocket
|
||||
|
||||
// fetch avec redirection vers la page de connexion si la session a expiré
|
||||
@@ -124,7 +134,11 @@ async function startRecording() {
|
||||
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
state.ws = new WebSocket(`${proto}://${location.host}/ws`);
|
||||
state.ws.onopen = () => state.ws.send(JSON.stringify({ type: 'start', title: $('title').value }));
|
||||
state.ws.onopen = () => state.ws.send(JSON.stringify({
|
||||
type: 'start',
|
||||
title: $('title').value,
|
||||
diarization: $('diarization-cb').checked,
|
||||
}));
|
||||
state.ws.onmessage = onServerMessage;
|
||||
state.ws.onclose = (e) => {
|
||||
if (e.code === 4401) { location.href = '/login'; return; }
|
||||
@@ -142,6 +156,7 @@ async function startRecording() {
|
||||
listMics();
|
||||
|
||||
state.recording = true;
|
||||
state.paused = false;
|
||||
state.startedAt = Date.now();
|
||||
state.lastLoudAt = Date.now();
|
||||
state.silentWarned = false;
|
||||
@@ -149,12 +164,18 @@ async function startRecording() {
|
||||
state.timerInterval = setInterval(updateTimer, 500);
|
||||
$('record-btn').textContent = '■ Arrêter';
|
||||
$('record-btn').classList.add('recording');
|
||||
$('pause-btn').classList.remove('hidden', 'paused');
|
||||
$('pause-btn').textContent = '⏸ Pause';
|
||||
$('title').disabled = true;
|
||||
setStatus('rec', 'Enregistrement…');
|
||||
clearTranscript();
|
||||
}
|
||||
|
||||
function queuePcm(samples) {
|
||||
if (state.paused) {
|
||||
state.lastLoudAt = Date.now(); // pas de chasse au micro pendant la pause
|
||||
return;
|
||||
}
|
||||
updateVu(samples);
|
||||
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
||||
state.sendBuffer.push(samples);
|
||||
@@ -200,10 +221,32 @@ function stopRecording(abrupt = false) {
|
||||
|
||||
$('record-btn').textContent = '● Démarrer';
|
||||
$('record-btn').classList.remove('recording');
|
||||
$('pause-btn').classList.add('hidden');
|
||||
state.paused = false;
|
||||
$('title').disabled = false;
|
||||
$('vu-bar').style.width = '0%';
|
||||
}
|
||||
|
||||
function togglePause() {
|
||||
if (!state.recording || !state.ws || state.ws.readyState !== WebSocket.OPEN) return;
|
||||
state.paused = !state.paused;
|
||||
const btn = $('pause-btn');
|
||||
if (state.paused) {
|
||||
flushPcm();
|
||||
state.ws.send(JSON.stringify({ type: 'pause' }));
|
||||
btn.textContent = '▶ Reprendre';
|
||||
btn.classList.add('paused');
|
||||
setStatus('busy', 'En pause');
|
||||
$('vu-bar').style.width = '0%';
|
||||
} else {
|
||||
state.ws.send(JSON.stringify({ type: 'resume' }));
|
||||
state.lastLoudAt = Date.now();
|
||||
btn.textContent = '⏸ Pause';
|
||||
btn.classList.remove('paused');
|
||||
setStatus('rec', 'Enregistrement…');
|
||||
}
|
||||
}
|
||||
|
||||
function onServerMessage(event) {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === 'ready') {
|
||||
@@ -238,7 +281,7 @@ function updateTimer() {
|
||||
String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
|
||||
|
||||
// micro muet depuis 5 s : on essaie automatiquement les autres micros
|
||||
if (!state.recording) return;
|
||||
if (!state.recording || state.paused) return;
|
||||
const silent = Date.now() - state.lastLoudAt > 5000;
|
||||
if (silent) {
|
||||
state.silentWarned = true;
|
||||
@@ -270,7 +313,12 @@ function clearTranscript() {
|
||||
function appendSegment(seg) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'segment';
|
||||
div.innerHTML = `<span class="ts">${fmtTs(seg.t0)}</span><span class="text"></span>`;
|
||||
let speakerHtml = '';
|
||||
if (seg.speaker) {
|
||||
speakerHtml = `<span class="speaker speaker-${speakerColorIndex(seg.speaker)}"></span>`;
|
||||
}
|
||||
div.innerHTML = `<span class="ts">${fmtTs(seg.t0)}</span>${speakerHtml}<span class="text"></span>`;
|
||||
if (seg.speaker) div.querySelector('.speaker').textContent = seg.speaker;
|
||||
div.querySelector('.text').textContent = seg.text;
|
||||
$('transcript').appendChild(div);
|
||||
$('transcript').scrollTop = $('transcript').scrollHeight;
|
||||
@@ -328,8 +376,12 @@ async function deleteCurrentMeeting() {
|
||||
}
|
||||
|
||||
async function copyTranscript() {
|
||||
const text = [...document.querySelectorAll('#transcript .segment .text')]
|
||||
.map((el) => el.textContent).join('\n');
|
||||
const text = [...document.querySelectorAll('#transcript .segment')]
|
||||
.map((el) => {
|
||||
const speaker = el.querySelector('.speaker');
|
||||
const t = el.querySelector('.text').textContent;
|
||||
return speaker ? `[${speaker.textContent}] ${t}` : t;
|
||||
}).join('\n');
|
||||
await navigator.clipboard.writeText(text);
|
||||
$('copy-btn').textContent = '✓ Copié';
|
||||
setTimeout(() => ($('copy-btn').textContent = '📋 Copier'), 1500);
|
||||
@@ -338,6 +390,7 @@ async function copyTranscript() {
|
||||
// --------------------------------------------------------------------- init
|
||||
|
||||
$('record-btn').onclick = () => (state.recording ? stopRecording() : startRecording());
|
||||
$('pause-btn').onclick = togglePause;
|
||||
$('logout-btn').onclick = async () => { await fetch('/api/logout', { method: 'POST' }); location.href = '/login'; };
|
||||
$('mic-select').onchange = async () => {
|
||||
const id = $('mic-select').value;
|
||||
|
||||
@@ -27,8 +27,12 @@
|
||||
<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="vu" title="Niveau du micro"><div id="vu-bar"></div></div>
|
||||
<label id="diarization-toggle" class="toggle" title="Identifier qui parle (Locuteur 1, 2...)">
|
||||
<input type="checkbox" id="diarization-cb" checked> Locuteurs
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section id="transcript-panel">
|
||||
|
||||
@@ -109,6 +109,38 @@ button, .controls a {
|
||||
}
|
||||
button.record { background: var(--accent); font-weight: 600; min-width: 130px; }
|
||||
button.record.recording { background: var(--rec); }
|
||||
button.pause { background: var(--panel-2); font-weight: 600; }
|
||||
button.pause.paused { background: #2a2410; color: #ffd166; }
|
||||
button.pause.hidden { display: none; }
|
||||
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.82rem;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.toggle input { accent-color: var(--accent); }
|
||||
|
||||
.speaker {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.speaker-0 { background: #1d3a5f; color: #7cb8ff; }
|
||||
.speaker-1 { background: #1f4a2c; color: #6fdc8c; }
|
||||
.speaker-2 { background: #4a2a1f; color: #ffab70; }
|
||||
.speaker-3 { background: #3d1f4a; color: #d8a6ff; }
|
||||
.speaker-4 { background: #4a1f2e; color: #ff9bb3; }
|
||||
.speaker-5 { background: #1f4a47; color: #6fd9d2; }
|
||||
.speaker-6 { background: #4a431f; color: #e3d56f; }
|
||||
.speaker-7 { background: #2e2e4a; color: #a3a8ff; }
|
||||
#timer { font-variant-numeric: tabular-nums; color: var(--muted); min-width: 48px; }
|
||||
|
||||
#vu {
|
||||
|
||||
Reference in new issue
Block a user