Merge pull request #55 from QuentinFuxa/diart_integration_improvements

Diart integration improvements : Correct bugs
This commit is contained in:
Quentin Fuxa 2025-02-23 23:16:10 +01:00 committed by GitHub
commit d4096e7e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 407 additions and 415 deletions

View file

@ -1,26 +1,27 @@
import asyncio
import re
import threading
import numpy as np
from diart import SpeakerDiarization from diart import SpeakerDiarization
from diart.inference import StreamingInference from diart.inference import StreamingInference
from diart.sources import AudioSource from diart.sources import AudioSource
from rx.subject import Subject
import threading
import numpy as np
import asyncio
import re
def extract_number(s):
match = re.search(r'\d+', s) def extract_number(s: str) -> int:
return int(match.group()) if match else None m = re.search(r'\d+', s)
return int(m.group()) if m else None
class WebSocketAudioSource(AudioSource): class WebSocketAudioSource(AudioSource):
""" """
Simple custom AudioSource that blocks in read() Custom AudioSource that blocks in read() until close() is called.
until close() is called. Use push_audio() to inject PCM chunks.
push_audio() is used to inject new PCM chunks.
""" """
def __init__(self, uri: str = "websocket", sample_rate: int = 16000): def __init__(self, uri: str = "websocket", sample_rate: int = 16000):
super().__init__(uri, sample_rate) super().__init__(uri, sample_rate)
self._close_event = threading.Event()
self._closed = False self._closed = False
self._close_event = threading.Event()
def read(self): def read(self):
self._close_event.wait() self._close_event.wait()
@ -32,99 +33,59 @@ class WebSocketAudioSource(AudioSource):
self._close_event.set() self._close_event.set()
def push_audio(self, chunk: np.ndarray): def push_audio(self, chunk: np.ndarray):
chunk = np.expand_dims(chunk, axis=0)
if not self._closed: if not self._closed:
self.stream.on_next(chunk) self.stream.on_next(np.expand_dims(chunk, axis=0))
def create_pipeline(SAMPLE_RATE):
diar_pipeline = SpeakerDiarization()
ws_source = WebSocketAudioSource(uri="websocket_source", sample_rate=SAMPLE_RATE)
inference = StreamingInference(
pipeline=diar_pipeline,
source=ws_source,
do_plot=False,
show_progress=False,
)
return inference, ws_source
def init_diart(SAMPLE_RATE, diar_instance):
diar_pipeline = SpeakerDiarization()
ws_source = WebSocketAudioSource(uri="websocket_source", sample_rate=SAMPLE_RATE)
inference = StreamingInference(
pipeline=diar_pipeline,
source=ws_source,
do_plot=False,
show_progress=False,
)
l_speakers_queue = asyncio.Queue()
def diar_hook(result):
"""
Hook called each time Diart processes a chunk.
result is (annotation, audio).
For each detected speaker segment, push its info to the queue and update processed_time.
"""
annotation, audio = result
if annotation._labels:
for speaker in annotation._labels:
segments_beg = annotation._labels[speaker].segments_boundaries_[0]
segments_end = annotation._labels[speaker].segments_boundaries_[-1]
if segments_end > diar_instance.processed_time:
diar_instance.processed_time = segments_end
asyncio.create_task(
l_speakers_queue.put({"speaker": speaker, "beg": segments_beg, "end": segments_end})
)
else:
audio_duration = audio.extent.end
if audio_duration > diar_instance.processed_time:
diar_instance.processed_time = audio_duration
inference.attach_hooks(diar_hook)
loop = asyncio.get_event_loop()
diar_future = loop.run_in_executor(None, inference)
return inference, l_speakers_queue, ws_source
class DiartDiarization: class DiartDiarization:
def __init__(self, SAMPLE_RATE): def __init__(self, sample_rate: int):
self.processed_time = 0 self.processed_time = 0
self.inference, self.l_speakers_queue, self.ws_source = init_diart(SAMPLE_RATE, self)
self.segment_speakers = [] self.segment_speakers = []
self.speakers_queue = asyncio.Queue()
self.pipeline = SpeakerDiarization()
self.source = WebSocketAudioSource(uri="websocket_source", sample_rate=sample_rate)
self.inference = StreamingInference(
pipeline=self.pipeline,
source=self.source,
do_plot=False,
show_progress=False,
)
# Attache la fonction hook et démarre l'inférence en arrière-plan.
self.inference.attach_hooks(self._diar_hook)
asyncio.get_event_loop().run_in_executor(None, self.inference)
async def diarize(self, pcm_array): def _diar_hook(self, result):
self.ws_source.push_audio(pcm_array) annotation, audio = result
self.segment_speakers = [] if annotation._labels:
while not self.l_speakers_queue.empty(): for speaker, label in annotation._labels.items():
self.segment_speakers.append(await self.l_speakers_queue.get()) beg = label.segments_boundaries_[0]
end = label.segments_boundaries_[-1]
if end > self.processed_time:
self.processed_time = end
asyncio.create_task(self.speakers_queue.put({
"speaker": speaker,
"beg": beg,
"end": end
}))
else:
dur = audio.extent.end
if dur > self.processed_time:
self.processed_time = dur
async def diarize(self, pcm_array: np.ndarray):
self.source.push_audio(pcm_array)
self.segment_speakers.clear()
while not self.speakers_queue.empty():
self.segment_speakers.append(await self.speakers_queue.get())
def close(self): def close(self):
self.ws_source.close() self.source.close()
def assign_speakers_to_chunks(self, chunks): def assign_speakers_to_chunks(self, chunks: list) -> list:
""" end_attributed_speaker = 0
For each chunk (a dict with keys "beg" and "end"), assign a speaker label. for chunk in chunks:
for segment in self.segment_speakers:
- If a chunk overlaps with a detected speaker segment, assign that label. if not (segment["end"] <= chunk["beg"] or segment["beg"] >= chunk["end"]):
- If the chunk's end time is within the processed time and no speaker was assigned, chunk["speaker"] = extract_number(segment["speaker"]) + 1
mark it as "No speaker". end_attributed_speaker = chunk["end"]
- If the chunk's time hasn't been fully processed yet, leave it (or mark as "Processing"). return end_attributed_speaker
"""
for ch in chunks:
ch["speaker"] = ch.get("speaker", -1)
for segment in self.segment_speakers:
seg_beg = segment["beg"]
seg_end = segment["end"]
speaker = segment["speaker"]
for ch in chunks:
if seg_end <= ch["beg"] or seg_beg >= ch["end"]:
continue
ch["speaker"] = extract_number(speaker) + 1
if self.processed_time > 0:
for ch in chunks:
if ch["end"] <= self.processed_time and ch["speaker"] == -1:
ch["speaker"] = -2
return chunks

View file

@ -1,339 +1,361 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Audio Transcription</title> <title>Audio Transcription</title>
<style> <style>
body { body {
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
margin: 20px; margin: 20px;
text-align: center; text-align: center;
} }
#recordButton {
width: 80px; #recordButton {
height: 80px; width: 80px;
font-size: 36px; height: 80px;
border: none; font-size: 36px;
border-radius: 50%; border: none;
background-color: white; border-radius: 50%;
cursor: pointer; background-color: white;
box-shadow: 0 0px 10px rgba(0, 0, 0, 0.2); cursor: pointer;
transition: background-color 0.3s ease, transform 0.2s ease; box-shadow: 0 0px 10px rgba(0, 0, 0, 0.2);
} transition: background-color 0.3s ease, transform 0.2s ease;
#recordButton.recording { }
background-color: #ff4d4d;
color: white; #recordButton.recording {
} background-color: #ff4d4d;
#recordButton:active { color: white;
transform: scale(0.95); }
}
#status { #recordButton:active {
margin-top: 20px; transform: scale(0.95);
font-size: 16px; }
color: #333;
} #status {
.settings-container { margin-top: 20px;
display: flex; font-size: 16px;
justify-content: center; color: #333;
align-items: center; }
gap: 15px;
margin-top: 20px; .settings-container {
} display: flex;
.settings { justify-content: center;
display: flex; align-items: center;
flex-direction: column; gap: 15px;
align-items: flex-start; margin-top: 20px;
gap: 5px; }
}
#chunkSelector, .settings {
#websocketInput { display: flex;
font-size: 16px; flex-direction: column;
padding: 5px; align-items: flex-start;
border-radius: 5px; gap: 5px;
border: 1px solid #ddd; }
background-color: #f9f9f9;
} #chunkSelector,
#websocketInput { #websocketInput {
width: 200px; font-size: 16px;
} padding: 5px;
#chunkSelector:focus, border-radius: 5px;
#websocketInput:focus { border: 1px solid #ddd;
outline: none; background-color: #f9f9f9;
border-color: #007bff; }
}
label { #websocketInput {
font-size: 14px; width: 200px;
} }
/* Speaker-labeled transcript area */
#linesTranscript { #chunkSelector:focus,
margin: 20px auto; #websocketInput:focus {
max-width: 600px; outline: none;
text-align: left; border-color: #007bff;
font-size: 16px; }
}
#linesTranscript p { label {
margin: 5px 0; font-size: 14px;
} }
#linesTranscript strong {
color: #333; /* Speaker-labeled transcript area */
} #linesTranscript {
#speaker { margin: 20px auto;
background-color: #dcefff; max-width: 600px;
border-radius: 30px; text-align: left;
padding: 2px 10px; font-size: 16px;
font-size: 14px; }
}
#timeInfo { #linesTranscript p {
color: #666; margin: 5px 0;
margin-left: 10px; }
}
.textcontent { #linesTranscript strong {
font-size: 16px; color: #333;
margin-left: 10px; }
padding-left: 10px;
border-left: 2px solid #dcefff; #speaker {
margin-bottom: 10px; background-color: #dcefff;
} border-radius: 30px;
.buffer { padding: 2px 10px;
color: rgb(180, 180, 180); font-size: 14px;
font-style: italic; }
margin-left: 4px;
} #timeInfo {
.spinner { color: #666;
display: inline-block; margin-left: 10px;
width: 8px; }
height: 8px;
border: 2px solid rgba(0, 0, 0, 0.2); .textcontent {
border-top: 2px solid #333; font-size: 16px;
border-radius: 50%; margin-left: 10px;
animation: spin 0.6s linear infinite; padding-left: 10px;
vertical-align: middle; border-left: 2px solid #dcefff;
margin-bottom: 2px; margin-bottom: 10px;
}
.buffer {
color: rgb(180, 180, 180);
font-style: italic;
margin-left: 4px;
}
.spinner {
display: inline-block;
width: 8px;
height: 8px;
border: 2px solid rgba(0, 0, 0, 0.2);
border-top: 2px solid #333;
border-radius: 50%;
animation: spin 0.6s linear infinite;
vertical-align: middle;
margin-bottom: 2px;
} }
@keyframes spin { @keyframes spin {
to { to {
transform: rotate(360deg); transform: rotate(360deg);
}
} }
.silence {
color: #666;
background-color: #f3f3f3;
font-size: 13px;
border-radius: 30px;
padding: 2px 10px;
} }
.silence {
color: #666; .loading {
background-color: #f3f3f3; color: #666;
font-size: 13px; background-color: #eff9ff;
border-radius: 30px; font-size: 14px;
padding: 2px 10px; border-radius: 30px;
} padding: 2px 10px;
.loading { }
color: #666; </style>
background-color: #eff9ff;
font-size: 14px;
border-radius: 30px;
padding: 2px 10px;
}
</style>
</head> </head>
<body> <body>
<div class="settings-container"> <div class="settings-container">
<button id="recordButton">🎙️</button> <button id="recordButton">🎙️</button>
<div class="settings"> <div class="settings">
<div> <div>
<label for="chunkSelector">Chunk size (ms):</label> <label for="chunkSelector">Chunk size (ms):</label>
<select id="chunkSelector"> <select id="chunkSelector">
<option value="500">500 ms</option> <option value="500">500 ms</option>
<option value="1000" selected>1000 ms</option> <option value="1000" selected>1000 ms</option>
<option value="2000">2000 ms</option> <option value="2000">2000 ms</option>
<option value="3000">3000 ms</option> <option value="3000">3000 ms</option>
<option value="4000">4000 ms</option> <option value="4000">4000 ms</option>
<option value="5000">5000 ms</option> <option value="5000">5000 ms</option>
</select> </select>
</div> </div>
<div> <div>
<label for="websocketInput">WebSocket URL:</label> <label for="websocketInput">WebSocket URL:</label>
<input id="websocketInput" type="text" value="ws://localhost:8000/asr" /> <input id="websocketInput" type="text" value="ws://localhost:8000/asr" />
</div> </div>
</div>
</div> </div>
</div>
<p id="status"></p> <p id="status"></p>
<!-- Speaker-labeled transcript --> <!-- Speaker-labeled transcript -->
<div id="linesTranscript"></div> <div id="linesTranscript"></div>
<script> <script>
let isRecording = false; let isRecording = false;
let websocket = null; let websocket = null;
let recorder = null; let recorder = null;
let chunkDuration = 1000; let chunkDuration = 1000;
let websocketUrl = "ws://localhost:8000/asr"; let websocketUrl = "ws://localhost:8000/asr";
let userClosing = false; let userClosing = false;
const statusText = document.getElementById("status"); const statusText = document.getElementById("status");
const recordButton = document.getElementById("recordButton"); const recordButton = document.getElementById("recordButton");
const chunkSelector = document.getElementById("chunkSelector"); const chunkSelector = document.getElementById("chunkSelector");
const websocketInput = document.getElementById("websocketInput"); const websocketInput = document.getElementById("websocketInput");
const linesTranscriptDiv = document.getElementById("linesTranscript"); const linesTranscriptDiv = document.getElementById("linesTranscript");
chunkSelector.addEventListener("change", () => { chunkSelector.addEventListener("change", () => {
chunkDuration = parseInt(chunkSelector.value); chunkDuration = parseInt(chunkSelector.value);
}); });
websocketInput.addEventListener("change", () => { websocketInput.addEventListener("change", () => {
const urlValue = websocketInput.value.trim(); const urlValue = websocketInput.value.trim();
if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) { if (!urlValue.startsWith("ws://") && !urlValue.startsWith("wss://")) {
statusText.textContent = "Invalid WebSocket URL (must start with ws:// or wss://)"; statusText.textContent = "Invalid WebSocket URL (must start with ws:// or wss://)";
return; return;
}
websocketUrl = urlValue;
statusText.textContent = "WebSocket URL updated. Ready to connect.";
});
function setupWebSocket() {
return new Promise((resolve, reject) => {
try {
websocket = new WebSocket(websocketUrl);
} catch (error) {
statusText.textContent = "Invalid WebSocket URL. Please check and try again.";
reject(error);
return;
}
websocket.onopen = () => {
statusText.textContent = "Connected to server.";
resolve();
};
websocket.onclose = () => {
if (userClosing) {
statusText.textContent = "WebSocket closed by user.";
} else {
statusText.textContent =
"Disconnected from the WebSocket server. (Check logs if model is loading.)";
}
userClosing = false;
};
websocket.onerror = () => {
statusText.textContent = "Error connecting to WebSocket.";
reject(new Error("Error connecting to WebSocket"));
};
// Handle messages from server
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
/*
The server might send:
{
"lines": [
{"speaker": 0, "text": "Hello.", "beg": "00:00", "end": "00:01"},
{"speaker": -2, "text": "Hi, no speaker here.", "beg": "00:01", "end": "00:02"},
{"speaker": -1, "text": "...", "beg": "00:02", "end": "00:03" },
...
],
"buffer": "..."
} }
*/ websocketUrl = urlValue;
const { lines = [], buffer = "" } = data; statusText.textContent = "WebSocket URL updated. Ready to connect.";
renderLinesWithBuffer( lines, buffer); });
};
});
}
function renderLinesWithBuffer(lines, buffer) { function setupWebSocket() {
// Clears if no lines return new Promise((resolve, reject) => {
if (!Array.isArray(lines) || lines.length === 0) { try {
linesTranscriptDiv.innerHTML = ""; websocket = new WebSocket(websocketUrl);
return; } catch (error) {
} statusText.textContent = "Invalid WebSocket URL. Please check and try again.";
reject(error);
return;
}
websocket.onopen = () => {
statusText.textContent = "Connected to server.";
resolve();
};
websocket.onclose = () => {
if (userClosing) {
statusText.textContent = "WebSocket closed by user.";
} else {
statusText.textContent =
"Disconnected from the WebSocket server. (Check logs if model is loading.)";
}
userClosing = false;
};
const linesHtml = lines.map((item, idx) => { websocket.onerror = () => {
let timeInfo = ""; statusText.textContent = "Error connecting to WebSocket.";
if (item.beg !== undefined && item.end !== undefined) { reject(new Error("Error connecting to WebSocket"));
timeInfo = ` ${item.beg} - ${item.end}`; };
// Handle messages from server
websocket.onmessage = (event) => {
const data = JSON.parse(event.data);
/*
The server might send:
{
"lines": [
{"speaker": 0, "text": "Hello.", "beg": "00:00", "end": "00:01"},
{"speaker": -2, "text": "Hi, no speaker here.", "beg": "00:01", "end": "00:02"},
{"speaker": -1, "text": "...", "beg": "00:02", "end": "00:03" },
...
],
"buffer": "..."
}
*/
const { lines = [], buffer = "" } = data;
renderLinesWithBuffer(lines, buffer);
};
});
} }
let speakerLabel = ""; function renderLinesWithBuffer(lines, buffer) {
if (item.speaker === -2) { if (!Array.isArray(lines) || lines.length === 0) {
speakerLabel = `<span class="silence">Silence<span id='timeInfo'>${timeInfo}</span></span>`; if (buffer) {
} else if (item.speaker == -1) { linesTranscriptDiv.innerHTML = `<span class="buffer">${buffer}</span>`;
speakerLabel = `<span class='loading'> <span class="spinner"></span><span id='timeInfo'>${item.diff} second(s) of audio are undergoing diarization</span></span>`; } else {
} else if (item.speaker == -3) { linesTranscriptDiv.innerHTML = "";
speakerLabel = `<span id="speaker"><span id='timeInfo'>${timeInfo}</span>`; }
} else if (item.speaker !== -1) { return;
speakerLabel = `<span id="speaker">Speaker ${item.speaker}<span id='timeInfo'>${timeInfo}</span></span>`; }
}
const linesHtml = lines.map((item, idx) => {
let textContent = item.text; let timeInfo = "";
if (idx === lines.length - 1 && buffer) { if (item.beg !== undefined && item.end !== undefined) {
textContent += `<span class="buffer">${buffer}</span>`; timeInfo = ` ${item.beg} - ${item.end}`;
}
let speakerLabel = "";
if (item.speaker === -2) {
speakerLabel = `<span class="silence">Silence<span id='timeInfo'>${timeInfo}</span></span>`;
} else if (item.speaker == 0) {
speakerLabel = `<span class='loading'><span class="spinner"></span><span id='timeInfo'>${item.diff} second(s) of audio are undergoing diarization</span></span>`;
} else if (item.speaker == -1) {
speakerLabel = `<span id="speaker"><span id='timeInfo'>${timeInfo}</span>`;
} else if (item.speaker !== -1) {
speakerLabel = `<span id="speaker">Speaker ${item.speaker}<span id='timeInfo'>${timeInfo}</span></span>`;
}
let textContent = item.text;
if (idx === lines.length - 1 && buffer) {
textContent += `<span class="buffer">${buffer}</span>`;
}
return textContent
? `<p>${speakerLabel}<br/><div class='textcontent'>${textContent}</div></p>`
: `<p>${speakerLabel}<br/></p>`;
}).join("");
linesTranscriptDiv.innerHTML = linesHtml;
} }
return textContent
? `<p>${speakerLabel}<br/><div class='textcontent'>${textContent}</div></p>`
: `<p >${speakerLabel}<br/></p>`;
}).join("");
linesTranscriptDiv.innerHTML = linesHtml; async function startRecording() {
} try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
async function startRecording() { recorder = new MediaRecorder(stream, { mimeType: "audio/webm" });
try { recorder.ondataavailable = (e) => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); if (websocket && websocket.readyState === WebSocket.OPEN) {
recorder = new MediaRecorder(stream, { mimeType: "audio/webm" }); websocket.send(e.data);
recorder.ondataavailable = (e) => { }
if (websocket && websocket.readyState === WebSocket.OPEN) { };
websocket.send(e.data); recorder.start(chunkDuration);
} isRecording = true;
}; updateUI();
recorder.start(chunkDuration); } catch (err) {
isRecording = true; statusText.textContent = "Error accessing microphone. Please allow microphone access.";
updateUI(); }
} catch (err) {
statusText.textContent = "Error accessing microphone. Please allow microphone access.";
}
}
function stopRecording() {
userClosing = true;
if (recorder) {
recorder.stop();
recorder = null;
}
isRecording = false;
if (websocket) {
websocket.close();
websocket = null;
}
updateUI();
}
async function toggleRecording() {
if (!isRecording) {
linesTranscriptDiv.innerHTML = "";
try {
await setupWebSocket();
await startRecording();
} catch (err) {
statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
} }
} else {
stopRecording();
}
}
function updateUI() { function stopRecording() {
recordButton.classList.toggle("recording", isRecording); userClosing = true;
statusText.textContent = isRecording ? "Recording..." : "Click to start transcription"; if (recorder) {
} recorder.stop();
recorder = null;
}
isRecording = false;
recordButton.addEventListener("click", toggleRecording); if (websocket) {
</script> websocket.close();
websocket = null;
}
updateUI();
}
async function toggleRecording() {
if (!isRecording) {
linesTranscriptDiv.innerHTML = "";
try {
await setupWebSocket();
await startRecording();
} catch (err) {
statusText.textContent = "Could not connect to WebSocket or access mic. Aborted.";
}
} else {
stopRecording();
}
}
function updateUI() {
recordButton.classList.toggle("recording", isRecording);
statusText.textContent = isRecording ? "Recording..." : "Click to start transcription";
}
recordButton.addEventListener("click", toggleRecording);
</script>
</body> </body>
</html> </html>

View file

@ -208,6 +208,7 @@ async def websocket_endpoint(websocket: WebSocket):
"beg": transcription.start, "beg": transcription.start,
"end": transcription.end, "end": transcription.end,
"text": transcription.text, "text": transcription.text,
"speaker": -1
}) })
full_transcription += transcription.text if transcription else "" full_transcription += transcription.text if transcription else ""
buffer = online.get_buffer() buffer = online.get_buffer()
@ -218,23 +219,32 @@ async def websocket_endpoint(websocket: WebSocket):
"beg": time() - beg_loop, "beg": time() - beg_loop,
"end": time() - beg_loop + 1, "end": time() - beg_loop + 1,
"text": '', "text": '',
"speaker": -1
}) })
sleep(1) sleep(1)
buffer = '' buffer = ''
if args.diarization: if args.diarization:
await diarization.diarize(pcm_array) await diarization.diarize(pcm_array)
diarization.assign_speakers_to_chunks(chunk_history) end_attributed_speaker = diarization.assign_speakers_to_chunks(chunk_history)
current_speaker = 0 current_speaker = -10
lines = [] lines = []
last_end_diarized = 0 last_end_diarized = 0
previous_speaker = -1
for ind, ch in enumerate(chunk_history): for ind, ch in enumerate(chunk_history):
speaker = ch.get("speaker", -3) speaker = ch.get("speaker")
if speaker == -1 and ind < len(chunk_history) - 1: if args.diarization:
continue if speaker == -1 or speaker == 0:
elif speaker != current_speaker: if ch['end'] < end_attributed_speaker:
speaker = previous_speaker
else:
speaker = 0
else:
last_end_diarized = max(ch['end'], last_end_diarized)
if speaker != current_speaker:
lines.append( lines.append(
{ {
"speaker": speaker, "speaker": speaker,
@ -245,12 +255,11 @@ async def websocket_endpoint(websocket: WebSocket):
} }
) )
current_speaker = speaker current_speaker = speaker
elif speaker != -1: else:
lines[-1]["text"] += ch['text'] lines[-1]["text"] += ch['text']
lines[-1]["end"] = format_time(ch['end']) lines[-1]["end"] = format_time(ch['end'])
if speaker != -1: lines[-1]["diff"] = round(ch['end'] - last_end_diarized, 2)
last_end_diarized = max(ch['end'], last_end_diarized)
response = {"lines": lines, "buffer": buffer} response = {"lines": lines, "buffer": buffer}
await websocket.send_json(response) await websocket.send_json(response)