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:
iamdoubz
2026-07-13 19:25:27 -05:00
parent 319e7a31cc
commit db52b9e812
+46 -2
View File
@@ -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 }),
);
}
}
});
}