feat(diarization): live tick re-emits relabeled segments + live 'You' (FR-SPK)
Phase 2 backend. The 15s provisional pass now (1) runs the mic voiceprint match every tick and merges 'You'/'Speaker N' into the session name map without overwriting user-set names, and (2) re-emits every committed segment whose speaker changed via transcript://segment (replace-by-id) so the live transcript reflects the refined labels instead of a frozen 'S1'.
This commit is contained in:
@@ -512,6 +512,7 @@ pub async fn start_recording(
|
||||
let wav_path_for_diar = wav_path.clone();
|
||||
let segments_for_diar = segments.clone();
|
||||
let names_for_diar = speaker_names.clone();
|
||||
let voice_sample_for_diar = mic_voice_sample.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// ponytail: reprocesses the whole recording-so-far each tick
|
||||
// rather than incremental/windowed segmentation — sherpa-onnx's
|
||||
@@ -552,16 +553,59 @@ pub async fn start_recording(
|
||||
}
|
||||
};
|
||||
|
||||
let speakers = {
|
||||
let (speakers, changed) = {
|
||||
let mut segs = segments_for_diar.lock().unwrap_or_else(|e| e.into_inner());
|
||||
// Snapshot prior labels so only segments whose speaker
|
||||
// actually changed this pass get re-emitted (ids are stable;
|
||||
// the frontend replaces by id).
|
||||
let before: HashMap<u64, String> =
|
||||
segs.iter().map(|s| (s.id, s.speaker.clone())).collect();
|
||||
diarizer.assign(&mut segs, &spans);
|
||||
|
||||
// Live "You": match the mic voiceprint against this pass's
|
||||
// clusters. Clusters re-shuffle every tick so match every
|
||||
// tick; never overwrite a name already set (user rename or a
|
||||
// prior pass) — same guard as the post-stop pass.
|
||||
if let Some(voice_sample) = &voice_sample_for_diar {
|
||||
let mic_samples = voice_sample.samples();
|
||||
match crate::diarization::voiceprint::match_mic_speaker(
|
||||
&diarization_embedding_model_file(),
|
||||
&mic_samples,
|
||||
&wav_path_for_diar,
|
||||
&spans,
|
||||
) {
|
||||
Ok(auto_names) => {
|
||||
let mut names =
|
||||
names_for_diar.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for (label, name) in auto_names {
|
||||
names.entry(label).or_insert(name);
|
||||
}
|
||||
}
|
||||
Err(e) => tracing::warn!("live voiceprint match failed: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let names = names_for_diar.lock().unwrap_or_else(|e| e.into_inner());
|
||||
speaker_infos_from_segments(&segs, &names)
|
||||
let changed: Vec<TranscriptSegment> = segs
|
||||
.iter()
|
||||
.filter(|s| before.get(&s.id) != Some(&s.speaker))
|
||||
.cloned()
|
||||
.collect();
|
||||
(speaker_infos_from_segments(&segs, &names), changed)
|
||||
};
|
||||
let _ = app_for_diar.emit(
|
||||
"diarization://updated",
|
||||
serde_json::json!({ "meetingId": meeting_id_for_diar, "speakers": speakers }),
|
||||
);
|
||||
// Re-emit relabeled committed segments so the live transcript
|
||||
// reflects the refined speakers without a new event type
|
||||
// (docs/04-api-contracts.md: transcript://segment is replace-by-id).
|
||||
for segment in &changed {
|
||||
let _ = app_for_diar.emit(
|
||||
"transcript://segment",
|
||||
serde_json::json!({ "meetingId": meeting_id_for_diar, "segment": segment }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user