diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 11867ee..e7dc258 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -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 = snapshot - .into_iter() - .map(|s| (s.id, s.speaker)) - .collect(); + let relabeled: HashMap = + 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> { (hound::SampleFormat::Int, 16) => { let s: Vec = reader.samples::().map_while(Result::ok).collect(); for frame in s.chunks(ch) { - let avg = (frame.iter().map(|&x| x as i32).sum::() - / frame.len() as i32) as i16; + let avg = + (frame.iter().map(|&x| x as i32).sum::() / 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 { +pub async fn import_meeting_bundle(state: State<'_, AppState>, dir: String) -> WaResult { let root = PathBuf::from(&dir); let mut bundles: Vec = 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:?}"), } });