diff --git a/apps/desktop/src-tauri/src/captions.rs b/apps/desktop/src-tauri/src/captions.rs index e8c397cf12..cd0fd5a023 100644 --- a/apps/desktop/src-tauri/src/captions.rs +++ b/apps/desktop/src-tauri/src/captions.rs @@ -55,6 +55,7 @@ impl Default for CaptionData { lazy_static::lazy_static! { static ref WHISPER_CONTEXT: Arc>>> = Arc::new(Mutex::new(None)); static ref MODEL_DOWNLOADS: Mutex> = Mutex::new(HashMap::new()); + static ref TRANSCRIPTION_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); } #[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] @@ -643,8 +644,14 @@ async fn extract_audio_from_video(video_path: &str, output_path: &PathBuf) -> Re } } -async fn get_whisper_context(model_path: &str) -> Result, String> { - let mut context_guard = WHISPER_CONTEXT.lock().await; +fn lock_transcription_worker_slot() -> std::sync::MutexGuard<'static, ()> { + TRANSCRIPTION_LOCK + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn get_whisper_context_blocking(model_path: &str) -> Result, String> { + let mut context_guard = WHISPER_CONTEXT.blocking_lock(); if let Some(ref existing) = *context_guard { log::info!("Reusing cached Whisper context"); @@ -661,6 +668,15 @@ async fn get_whisper_context(model_path: &str) -> Result, St Ok(ctx_arc) } +#[cfg(all(target_os = "macos", target_arch = "aarch64"))] +fn release_whisper_context_after_transcription() { + let mut ctx = WHISPER_CONTEXT.blocking_lock(); + *ctx = None; +} + +#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))] +fn release_whisper_context_after_transcription() {} + fn is_special_token(token_text: &str) -> bool { let trimmed = token_text.trim(); if trimmed.is_empty() { @@ -1314,22 +1330,14 @@ pub async fn transcribe_audio( TranscriptionEngine::Parakeet => { log::info!("Using Parakeet TDT engine"); let model_dir = model_path.clone(); - tokio::task::spawn_blocking(move || process_with_parakeet(&audio_path, &model_dir)) - .await - .map_err(|e| format!("Parakeet task panicked: {e}"))? + tokio::task::spawn_blocking(move || { + let _guard = lock_transcription_worker_slot(); + process_with_parakeet(&audio_path, &model_dir) + }) + .await + .map_err(|e| format!("Parakeet task panicked: {e}"))? } TranscriptionEngine::Whisper => { - let context = match get_whisper_context(&model_path).await { - Ok(ctx) => { - log::info!("Whisper context ready"); - ctx - } - Err(e) => { - log::error!("Failed to initialize Whisper context: {e}"); - return Err(format!("Failed to initialize transcription model: {e}")); - } - }; - let transcription_hints = GeneralSettingsStore::get(&app) .ok() .flatten() @@ -1338,7 +1346,21 @@ pub async fn transcribe_audio( log::info!("Starting Whisper transcription in blocking task..."); tokio::task::spawn_blocking(move || { - process_with_whisper(&audio_path, context, &language, &transcription_hints) + let _guard = lock_transcription_worker_slot(); + let context = match get_whisper_context_blocking(&model_path) { + Ok(ctx) => { + log::info!("Whisper context ready"); + ctx + } + Err(e) => { + log::error!("Failed to initialize Whisper context: {e}"); + return Err(format!("Failed to initialize transcription model: {e}")); + } + }; + let result = + process_with_whisper(&audio_path, context, &language, &transcription_hints); + release_whisper_context_after_transcription(); + result }) .await .map_err(|e| format!("Whisper task panicked: {e}"))? @@ -2311,7 +2333,7 @@ async fn parakeet_model_file_sizes( let part_size = resp .content_length() .filter(|size| *size > 0) - .or_else(|| parakeet_known_part_size(*url)) + .or_else(|| parakeet_known_part_size(url)) .unwrap_or(0); file_size = file_size.saturating_add(part_size); } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index 7270a65055..08bd55a0eb 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -47,8 +47,8 @@ use std::{ time::Duration, }; use tauri::{AppHandle, Manager, path::BaseDirectory}; -use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_dialog::{DialogExt, MessageDialogBuilder}; +use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_specta::Event; use tracing::*; @@ -3015,9 +3015,7 @@ async fn handle_recording_end( // so they don't reappear when the main window comes back. let focus_manager = handle.try_state::(); for (label, window) in handle.webview_windows() { - if let Ok(CapWindowId::TargetSelectOverlay { display_id }) = - CapWindowId::from_str(&label) - { + if let Ok(CapWindowId::TargetSelectOverlay { display_id }) = CapWindowId::from_str(&label) { hide_overlay(&window); if let Some(ref fm) = focus_manager { fm.destroy(&display_id, handle.global_shortcut()); diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index 2d97c446fb..51cceb978e 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -2378,7 +2378,6 @@ impl ShowCapWindow { )); } - #[cfg(target_os = "macos")] crate::platform::set_window_level( window.as_ref().window(),