fix: prevent idle watcher from unloading model during recording (#1085)
* fix: prevent idle watcher from unloading model during recording
the idle watcher treats ModelUnloadTimeout::Immediately as a 0s
timeout, causing idle_ms > 0 to always be true. this unloads the
model on the next 10s tick — even while the user is still recording.
the Immediately variant is already handled correctly by
maybe_unload_immediately() which fires after each transcription
completes. the idle watcher should skip it entirely.
fixes regression introduced in d1da935.
* fix race condition
* cleanup
* format
---------
Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
parent
0b3322faf3
commit
095f4ac455
21 changed files with 60 additions and 50 deletions
|
|
@ -1,4 +1,5 @@
|
|||
use crate::audio_toolkit::{apply_custom_words, filter_transcription_output};
|
||||
use crate::managers::audio::AudioRecordingManager;
|
||||
use crate::managers::model::{EngineType, ModelManager};
|
||||
use crate::settings::{
|
||||
get_settings, ModelUnloadTimeout, OrtAcceleratorSetting, WhisperAcceleratorSetting,
|
||||
|
|
@ -12,7 +13,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|||
use std::sync::{Arc, Condvar, Mutex, MutexGuard};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use transcribe_rs::{
|
||||
onnx::{
|
||||
canary::CanaryModel,
|
||||
|
|
@ -79,12 +80,7 @@ impl TranscriptionManager {
|
|||
model_manager,
|
||||
app_handle: app_handle.clone(),
|
||||
current_model_id: Arc::new(Mutex::new(None)),
|
||||
last_activity: Arc::new(AtomicU64::new(
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
)),
|
||||
last_activity: Arc::new(AtomicU64::new(Self::now_ms())),
|
||||
shutdown_signal: Arc::new(AtomicBool::new(false)),
|
||||
watcher_handle: Arc::new(Mutex::new(None)),
|
||||
is_loading: Arc::new(Mutex::new(false)),
|
||||
|
|
@ -107,14 +103,28 @@ impl TranscriptionManager {
|
|||
}
|
||||
|
||||
let settings = get_settings(&app_handle_cloned);
|
||||
let timeout_seconds = settings.model_unload_timeout.to_seconds();
|
||||
let timeout = settings.model_unload_timeout;
|
||||
|
||||
if let Some(limit_seconds) = timeout_seconds {
|
||||
// Skip Immediately — that variant is handled by
|
||||
// maybe_unload_immediately() after each transcription.
|
||||
// Treating it as 0s here would unload the model mid-recording.
|
||||
if timeout == ModelUnloadTimeout::Immediately {
|
||||
continue;
|
||||
}
|
||||
|
||||
// While recording, keep the idle timer fresh so the
|
||||
// model is never unloaded mid-session.
|
||||
let is_recording = app_handle_cloned
|
||||
.try_state::<Arc<AudioRecordingManager>>()
|
||||
.map_or(false, |a| a.is_recording());
|
||||
if is_recording {
|
||||
manager_cloned.touch_activity();
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(limit_seconds) = timeout.to_seconds() {
|
||||
let last = manager_cloned.last_activity.load(Ordering::Relaxed);
|
||||
let now_ms = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64;
|
||||
let now_ms = TranscriptionManager::now_ms();
|
||||
let idle_ms = now_ms.saturating_sub(last);
|
||||
let limit_ms = limit_seconds * 1000;
|
||||
|
||||
|
|
@ -213,6 +223,18 @@ impl TranscriptionManager {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
/// Reset the idle timer to now.
|
||||
fn touch_activity(&self) {
|
||||
self.last_activity.store(Self::now_ms(), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Unloads the model immediately if the setting is enabled and the model is loaded
|
||||
pub fn maybe_unload_immediately(&self, context: &str) {
|
||||
let settings = get_settings(&self.app_handle);
|
||||
|
|
@ -402,13 +424,7 @@ impl TranscriptionManager {
|
|||
}
|
||||
|
||||
// Reset idle timer so the watcher doesn't immediately unload a just-loaded model
|
||||
self.last_activity.store(
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.touch_activity();
|
||||
|
||||
// Emit loading completed event
|
||||
let _ = self.app_handle.emit(
|
||||
|
|
@ -457,13 +473,7 @@ impl TranscriptionManager {
|
|||
|
||||
pub fn transcribe(&self, audio: Vec<f32>) -> Result<String> {
|
||||
// Update last activity timestamp
|
||||
self.last_activity.store(
|
||||
SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.touch_activity();
|
||||
|
||||
let st = std::time::Instant::now();
|
||||
|
||||
|
|
|
|||
|
|
@ -123,7 +123,7 @@ pub enum ModelUnloadTimeout {
|
|||
Min10,
|
||||
Min15,
|
||||
Hour1,
|
||||
Sec5, // Debug mode only
|
||||
Sec15, // Debug mode only
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Type)]
|
||||
|
|
@ -216,7 +216,7 @@ impl ModelUnloadTimeout {
|
|||
ModelUnloadTimeout::Min10 => Some(10),
|
||||
ModelUnloadTimeout::Min15 => Some(15),
|
||||
ModelUnloadTimeout::Hour1 => Some(60),
|
||||
ModelUnloadTimeout::Sec5 => Some(0), // Special case for debug - handled separately
|
||||
ModelUnloadTimeout::Sec15 => Some(0), // Special case for debug - handled separately
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -224,7 +224,7 @@ impl ModelUnloadTimeout {
|
|||
match self {
|
||||
ModelUnloadTimeout::Never => None,
|
||||
ModelUnloadTimeout::Immediately => Some(0), // Special case for immediate unloading
|
||||
ModelUnloadTimeout::Sec5 => Some(5),
|
||||
ModelUnloadTimeout::Sec15 => Some(15),
|
||||
_ => self.to_minutes().map(|m| m * 60),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -809,7 +809,7 @@ export type LLMPrompt = { id: string; name: string; prompt: string }
|
|||
export type LogLevel = "trace" | "debug" | "info" | "warn" | "error"
|
||||
export type ModelInfo = { id: string; name: string; description: string; filename: string; url: string | null; size_mb: number; is_downloaded: boolean; is_downloading: boolean; partial_size: number; is_directory: boolean; engine_type: EngineType; accuracy_score: number; speed_score: number; supports_translation: boolean; is_recommended: boolean; supported_languages: string[]; supports_language_selection: boolean; is_custom: boolean }
|
||||
export type ModelLoadStatus = { is_loaded: boolean; current_model: string | null }
|
||||
export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_5"
|
||||
export type ModelUnloadTimeout = "never" | "immediately" | "min_2" | "min_5" | "min_10" | "min_15" | "hour_1" | "sec_15"
|
||||
export type OrtAcceleratorSetting = "auto" | "cpu" | "cuda" | "directml" | "rocm"
|
||||
export type OverlayPosition = "none" | "top" | "bottom"
|
||||
export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_shift_v" | "external_script"
|
||||
|
|
|
|||
|
|
@ -51,8 +51,8 @@ export const ModelUnloadTimeoutSetting: React.FC<ModelUnloadTimeoutProps> = ({
|
|||
const debugTimeoutOptions = [
|
||||
...timeoutOptions,
|
||||
{
|
||||
value: "sec5" as ModelUnloadTimeout,
|
||||
label: t("settings.advanced.modelUnload.options.sec5"),
|
||||
value: "sec15" as ModelUnloadTimeout,
|
||||
label: t("settings.advanced.modelUnload.options.sec15"),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -309,7 +309,7 @@
|
|||
"min10": "بعد 10 دقائق",
|
||||
"min15": "بعد 15 دقيقة",
|
||||
"hour1": "بعد ساعة واحدة",
|
||||
"sec5": "بعد 5 ثوانٍ (تصحيح أخطاء)"
|
||||
"sec15": "بعد 15 ثوانٍ (تصحيح أخطاء)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Po 10 minutách",
|
||||
"min15": "Po 15 minutách",
|
||||
"hour1": "Po 1 hodině",
|
||||
"sec5": "Po 5 sekundách (Debug)"
|
||||
"sec15": "Po 15 sekundách (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Nach 10 Minuten",
|
||||
"min15": "Nach 15 Minuten",
|
||||
"hour1": "Nach 1 Stunde",
|
||||
"sec5": "Nach 5 Sekunden (Debug)"
|
||||
"sec15": "Nach 15 Sekunden (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "After 10 minutes",
|
||||
"min15": "After 15 minutes",
|
||||
"hour1": "After 1 hour",
|
||||
"sec5": "After 5 seconds (Debug)"
|
||||
"sec15": "After 15 seconds (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Después de 10 minutos",
|
||||
"min15": "Después de 15 minutos",
|
||||
"hour1": "Después de 1 hora",
|
||||
"sec5": "Después de 5 segundos (Depuración)"
|
||||
"sec15": "Después de 15 segundos (Depuración)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Après 10 minutes",
|
||||
"min15": "Après 15 minutes",
|
||||
"hour1": "Après 1 heure",
|
||||
"sec5": "Après 5 secondes (Débogage)"
|
||||
"sec15": "Après 15 secondes (Débogage)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Dopo 10 minuti",
|
||||
"min15": "Dopo 15 minuti",
|
||||
"hour1": "Dopo 1 ora",
|
||||
"sec5": "Dopo 5 secondi (Debug)"
|
||||
"sec15": "Dopo 15 secondi (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "10分後",
|
||||
"min15": "15分後",
|
||||
"hour1": "1時間後",
|
||||
"sec5": "5秒後(デバッグ)"
|
||||
"sec15": "15秒後(デバッグ)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "10분 후",
|
||||
"min15": "15분 후",
|
||||
"hour1": "1시간 후",
|
||||
"sec5": "5초 후 (디버그)"
|
||||
"sec15": "15초 후 (디버그)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Po 10 minutach",
|
||||
"min15": "Po 15 minutach",
|
||||
"hour1": "Po 1 godzinie",
|
||||
"sec5": "Po 5 sekundach (Debug)"
|
||||
"sec15": "Po 15 sekundach (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Após 10 minutos",
|
||||
"min15": "Após 15 minutos",
|
||||
"hour1": "Após 1 hora",
|
||||
"sec5": "Após 5 segundos (Depuração)"
|
||||
"sec15": "Após 15 segundos (Depuração)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Через 10 минут",
|
||||
"min15": "Через 15 минут",
|
||||
"hour1": "Через 1 час",
|
||||
"sec5": "Через 5 секунд (отладка)"
|
||||
"sec15": "Через 15 секунд (отладка)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "10 dakika sonra",
|
||||
"min15": "15 dakika sonra",
|
||||
"hour1": "1 saat sonra",
|
||||
"sec5": "5 saniye sonra (Debug)"
|
||||
"sec15": "15 saniye sonra (Debug)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Через 10 хвилин",
|
||||
"min15": "Через 15 хвилин",
|
||||
"hour1": "Через 1 годину",
|
||||
"sec5": "Через 5 секунд (Дебаг)"
|
||||
"sec15": "Через 15 секунд (Дебаг)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "Sau 10 phút",
|
||||
"min15": "Sau 15 phút",
|
||||
"hour1": "Sau 1 giờ",
|
||||
"sec5": "Sau 5 giây (Gỡ lỗi)"
|
||||
"sec15": "Sau 15 giây (Gỡ lỗi)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "10 分鐘後",
|
||||
"min15": "15 分鐘後",
|
||||
"hour1": "1 小時後",
|
||||
"sec5": "5 秒後(偵錯)"
|
||||
"sec15": "15 秒後(偵錯)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@
|
|||
"min10": "10 分钟后",
|
||||
"min15": "15 分钟后",
|
||||
"hour1": "1 小时后",
|
||||
"sec5": "5 秒后(调试)"
|
||||
"sec15": "15 秒后(调试)"
|
||||
}
|
||||
},
|
||||
"customWords": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue