feat(npu): engine log names the decoder EP alongside the encoder EP

This commit is contained in:
iamdoubz committed 2026-07-15 19:48:15 -05:00
1 parent 402ffe4586
commit 7f7de48ecc
1 file changed
+83 -47
+83 -47
View File
@@ -194,17 +194,17 @@ fn load_transcriber(
match OnnxTranscriber::load(&dir, backend, language) {
Ok(t) => {
// The definitive "is the accelerator actually engaged" line
// (visible with `npm run tauri dev`): the encoder runs on
// the EP below; the autoregressive decoder always runs on
// the CPU EP, which is why Task Manager shows only short
// periodic NPU spikes alongside sustained CPU load.
// (visible with `npm run tauri dev`): encoder EP from the
// accel path, decoder EP from wherever its session landed
// (OpenVINO/GPU on NPU systems, CPU otherwise/on fallback).
tracing::info!(
"transcription engine: ONNX {} (encoder EP: {}, decoder: CPU)",
"transcription engine: ONNX {} (encoder EP: {}, decoder EP: {})",
onnx_models::DEFAULT_ONNX_MODEL,
match path {
AccelPath::OnnxOpenVino => "OpenVINO/NPU",
_ => "DirectML/GPU",
}
},
t.decoder_ep()
);
return Ok((Box::new(t), backend));
}
@@ -347,7 +347,9 @@ async fn attribute_split(
spans.sort_by_key(|s| s.start_ms);
// Uniform naming: "You" -> "You", far speakers -> "Speaker 2", "Speaker 3"….
let labels = crate::diarization::voiceprint::first_appearance_order(&spans);
Some(crate::diarization::voiceprint::build_name_map(&labels, "You"))
Some(crate::diarization::voiceprint::build_name_map(
&labels, "You",
))
}
/// The distinct speakers seen in `segments` so far, in first-appearance order,
@@ -655,28 +657,23 @@ pub async fn start_recording(
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone();
if attribute_split(
diarizer.clone(),
wav_path_for_diar.clone(),
&mut snapshot,
)
.await
.map(|auto_names| {
let mut names = names_for_diar.lock().unwrap_or_else(|e| e.into_inner());
// Never overwrite a name already set (user rename or a
// prior pass) — same guard as the post-stop pass.
for (label, name) in auto_names {
names.entry(label).or_insert(name);
}
})
.is_none()
if attribute_split(diarizer.clone(), wav_path_for_diar.clone(), &mut snapshot)
.await
.map(|auto_names| {
let mut names =
names_for_diar.lock().unwrap_or_else(|e| e.into_inner());
// Never overwrite a name already set (user rename or a
// prior pass) — same guard as the post-stop pass.
for (label, name) in auto_names {
names.entry(label).or_insert(name);
}
})
.is_none()
{
continue; // pass failed (already logged); retry next tick
}
let relabeled: HashMap<u64, String> = snapshot
.into_iter()
.map(|s| (s.id, s.speaker))
.collect();
let relabeled: HashMap<u64, String> =
snapshot.into_iter().map(|s| (s.id, s.speaker)).collect();
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;
@@ -891,7 +888,8 @@ pub async fn stop_recording(
if already_named {
continue;
}
if let Err(e) = state.store.rename_speaker(&meeting_id, &label, &name).await {
if let Err(e) = state.store.rename_speaker(&meeting_id, &label, &name).await
{
tracing::warn!("failed to persist auto speaker name: {e}");
continue;
}
@@ -1757,8 +1755,11 @@ fn pick_realtime_recommendation(
.max_by(|a, b| {
let sa = sizes.get(&a.model).copied().unwrap_or(0);
let sb = sizes.get(&b.model).copied().unwrap_or(0);
sa.cmp(&sb)
.then(b.rtf.partial_cmp(&a.rtf).unwrap_or(std::cmp::Ordering::Equal))
sa.cmp(&sb).then(
b.rtf
.partial_cmp(&a.rtf)
.unwrap_or(std::cmp::Ordering::Equal),
)
})
.map(|r| StressRecommendation {
backend: r.backend.clone(),
@@ -2525,10 +2526,17 @@ async fn run_import_pipeline(
})
.await;
match transcode {
Ok(Ok(())) => {
emit_import_progress(&app, &meeting_id, "prepare", "done", Some(t.elapsed().as_millis()), None)
Ok(Ok(())) => emit_import_progress(
&app,
&meeting_id,
"prepare",
"done",
Some(t.elapsed().as_millis()),
None,
),
Ok(Err(e)) => {
return fail_import(&app, &store, &meeting_id, "prepare", &e.to_string()).await
}
Ok(Err(e)) => return fail_import(&app, &store, &meeting_id, "prepare", &e.to_string()).await,
Err(e) => return fail_import(&app, &store, &meeting_id, "prepare", &e.to_string()).await,
}
@@ -2552,11 +2560,22 @@ async fn run_import_pipeline(
.await;
let (mut segments, resolved_language) = match transcribed {
Ok(Ok(v)) => {
emit_import_progress(&app, &meeting_id, "transcribe", "done", Some(t.elapsed().as_millis()), None);
emit_import_progress(
&app,
&meeting_id,
"transcribe",
"done",
Some(t.elapsed().as_millis()),
None,
);
v
}
Ok(Err(e)) => return fail_import(&app, &store, &meeting_id, "transcribe", &e.to_string()).await,
Err(e) => return fail_import(&app, &store, &meeting_id, "transcribe", &e.to_string()).await,
Ok(Err(e)) => {
return fail_import(&app, &store, &meeting_id, "transcribe", &e.to_string()).await
}
Err(e) => {
return fail_import(&app, &store, &meeting_id, "transcribe", &e.to_string()).await
}
};
// Phase 3: diarize — one pass if the models are installed, else every line
@@ -2578,7 +2597,14 @@ async fn run_import_pipeline(
Err(e) => tracing::warn!("import diarization task failed: {e}"),
}
}
emit_import_progress(&app, &meeting_id, "diarize", "done", Some(t.elapsed().as_millis()), None);
emit_import_progress(
&app,
&meeting_id,
"diarize",
"done",
Some(t.elapsed().as_millis()),
None,
);
// Phase 4: finalize — persist segments, notes, seal audio.
emit_import_progress(&app, &meeting_id, "finalize", "active", None, None);
@@ -2626,7 +2652,14 @@ async fn run_import_pipeline(
}
}
}
emit_import_progress(&app, &meeting_id, "finalize", "done", Some(t.elapsed().as_millis()), None);
emit_import_progress(
&app,
&meeting_id,
"finalize",
"done",
Some(t.elapsed().as_millis()),
None,
);
let _ = app.emit(
"transcript://finalized",
@@ -2643,7 +2676,9 @@ async fn fail_import(
phase: &str,
message: &str,
) {
let _ = store.set_meeting_status(&meeting_id.to_string(), "error").await;
let _ = store
.set_meeting_status(&meeting_id.to_string(), "error")
.await;
emit_import_progress(app, meeting_id, phase, "error", None, Some(message));
}
@@ -2926,8 +2961,8 @@ fn fold_wav(wav: &[u8], out_channels: u16) -> Option<Vec<u8>> {
(hound::SampleFormat::Int, 16) => {
let s: Vec<i16> = reader.samples::<i16>().map_while(Result::ok).collect();
for frame in s.chunks(ch) {
let avg = (frame.iter().map(|&x| x as i32).sum::<i32>()
/ frame.len() as i32) as i16;
let avg =
(frame.iter().map(|&x| x as i32).sum::<i32>() / frame.len() as i32) as i16;
for _ in 0..out_channels {
writer.write_sample(avg).ok()?;
}
@@ -3125,10 +3160,7 @@ pub async fn bulk_export_meetings(
/// so re-importing never collides with existing meetings. Returns the count
/// imported.
#[tauri::command]
pub async fn import_meeting_bundle(
state: State<'_, AppState>,
dir: String,
) -> WaResult<u32> {
pub async fn import_meeting_bundle(state: State<'_, AppState>, dir: String) -> WaResult<u32> {
let root = PathBuf::from(&dir);
let mut bundles: Vec<PathBuf> = Vec::new();
if root.join("meeting.json").exists() {
@@ -3977,7 +4009,11 @@ only, with no preamble or commentary.";
let notes = notes.trim();
let user = format!(
"My rough notes:\n{}\n\nTranscript:\n{}",
if notes.is_empty() { "(none yet)" } else { notes },
if notes.is_empty() {
"(none yet)"
} else {
notes
},
prompt.transcript
);
provider
@@ -4895,8 +4931,8 @@ pub(crate) fn spawn_auto_sync(
let app = app.clone();
tauri::async_runtime::spawn(async move {
match enqueue_meeting_sync(store.as_ref(), &meeting_id, None, true).await {
Ok(0) => {} // nothing changed
Ok(_) => pump_sync(&app, store.as_ref()).await, // upload the changes
Ok(0) => {} // nothing changed
Ok(_) => pump_sync(&app, store.as_ref()).await, // upload the changes
Err(e) => tracing::warn!("auto-sync enqueue failed: {e:?}"),
}
});