be able to change models from the tray (#1002)
* be able to change models from the tray * persist at the right times * fixes * fix tests
This commit is contained in:
parent
79f28f515c
commit
30a940629a
22 changed files with 181 additions and 29 deletions
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::managers::model::{ModelInfo, ModelManager};
|
use crate::managers::model::{ModelInfo, ModelManager};
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::{ModelStateEvent, TranscriptionManager};
|
||||||
use crate::settings::{get_settings, write_settings};
|
use crate::settings::{get_settings, write_settings, ModelUnloadTimeout};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::{AppHandle, State};
|
use tauri::{AppHandle, Emitter, Manager, State};
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
|
|
@ -58,36 +58,85 @@ pub async fn delete_model(
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
/// Shared logic for switching the active model, used by both the Tauri command
|
||||||
#[specta::specta]
|
/// and the tray menu handler.
|
||||||
pub async fn set_active_model(
|
///
|
||||||
app_handle: AppHandle,
|
/// Validates the model, updates the persisted setting, and loads the model
|
||||||
model_manager: State<'_, Arc<ModelManager>>,
|
/// unless the unload timeout is set to "Immediately" (in which case the model
|
||||||
transcription_manager: State<'_, Arc<TranscriptionManager>>,
|
/// will be loaded on-demand during the next transcription).
|
||||||
model_id: String,
|
pub fn switch_active_model(app: &AppHandle, model_id: &str) -> Result<(), String> {
|
||||||
) -> Result<(), String> {
|
let model_manager = app.state::<Arc<ModelManager>>();
|
||||||
|
let transcription_manager = app.state::<Arc<TranscriptionManager>>();
|
||||||
|
|
||||||
|
// Atomically claim the loading slot — prevents concurrent model loads
|
||||||
|
// from tray double-clicks or overlapping commands. The guard resets the
|
||||||
|
// flag on drop (including early returns, errors, and panics).
|
||||||
|
let _loading_guard = transcription_manager
|
||||||
|
.try_start_loading()
|
||||||
|
.ok_or_else(|| "Model load already in progress".to_string())?;
|
||||||
|
|
||||||
// Check if model exists and is available
|
// Check if model exists and is available
|
||||||
let model_info = model_manager
|
let model_info = model_manager
|
||||||
.get_model_info(&model_id)
|
.get_model_info(model_id)
|
||||||
.ok_or_else(|| format!("Model not found: {}", model_id))?;
|
.ok_or_else(|| format!("Model not found: {}", model_id))?;
|
||||||
|
|
||||||
if !model_info.is_downloaded {
|
if !model_info.is_downloaded {
|
||||||
return Err(format!("Model not downloaded: {}", model_id));
|
return Err(format!("Model not downloaded: {}", model_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the model in the transcription manager
|
let settings = get_settings(app);
|
||||||
transcription_manager
|
let unload_timeout = settings.model_unload_timeout;
|
||||||
.load_model(&model_id)
|
let old_model = settings.selected_model.clone();
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
// Update settings
|
// Persist the new selection early so the frontend sees the correct model
|
||||||
let mut settings = get_settings(&app_handle);
|
// when it reacts to events emitted by load_model.
|
||||||
settings.selected_model = model_id.clone();
|
let mut settings = settings;
|
||||||
write_settings(&app_handle, settings);
|
settings.selected_model = model_id.to_string();
|
||||||
|
write_settings(app, settings);
|
||||||
|
|
||||||
|
// Skip eager loading if unload is set to "Immediately" — the model
|
||||||
|
// will be loaded on-demand during the next transcription.
|
||||||
|
if unload_timeout == ModelUnloadTimeout::Immediately {
|
||||||
|
// Notify frontend — load_model won't be called so no events
|
||||||
|
// would otherwise be emitted.
|
||||||
|
let _ = app.emit(
|
||||||
|
"model-state-changed",
|
||||||
|
ModelStateEvent {
|
||||||
|
event_type: "selection_changed".to_string(),
|
||||||
|
model_id: Some(model_id.to_string()),
|
||||||
|
model_name: Some(model_info.name.clone()),
|
||||||
|
error: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
log::info!(
|
||||||
|
"Model selection changed to {} (not loading — unload set to Immediately).",
|
||||||
|
model_id
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the model. On failure, revert the persisted selection.
|
||||||
|
if let Err(e) = transcription_manager.load_model(model_id) {
|
||||||
|
let mut settings = get_settings(app);
|
||||||
|
settings.selected_model = old_model;
|
||||||
|
write_settings(app, settings);
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub async fn set_active_model(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
_model_manager: State<'_, Arc<ModelManager>>,
|
||||||
|
_transcription_manager: State<'_, Arc<TranscriptionManager>>,
|
||||||
|
model_id: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
switch_active_model(&app_handle, &model_id)
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
#[specta::specta]
|
#[specta::specta]
|
||||||
pub async fn get_current_model(app_handle: AppHandle) -> Result<String, String> {
|
pub async fn get_current_model(app_handle: AppHandle) -> Result<String, String> {
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,25 @@ fn initialize_core_logic(app_handle: &AppHandle) {
|
||||||
"quit" => {
|
"quit" => {
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
|
id if id.starts_with("model_select:") => {
|
||||||
|
let model_id = id.strip_prefix("model_select:").unwrap().to_string();
|
||||||
|
let current_model = settings::get_settings(app).selected_model;
|
||||||
|
if model_id == current_model {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let app_clone = app.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
match commands::models::switch_active_model(&app_clone, &model_id) {
|
||||||
|
Ok(()) => {
|
||||||
|
log::info!("Model switched to {} via tray.", model_id);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to switch model via tray: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tray::update_tray_menu(&app_clone, &tray::TrayIconState::Idle, None);
|
||||||
|
});
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
})
|
})
|
||||||
.build(app_handle)
|
.build(app_handle)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,21 @@ enum LoadedEngine {
|
||||||
GigaAM(GigaAMEngine),
|
GigaAM(GigaAMEngine),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RAII guard that clears the `is_loading` flag and notifies waiters on drop.
|
||||||
|
/// Ensures the loading flag is always reset, even on early returns or panics.
|
||||||
|
pub struct LoadingGuard {
|
||||||
|
is_loading: Arc<Mutex<bool>>,
|
||||||
|
loading_condvar: Arc<Condvar>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for LoadingGuard {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let mut is_loading = self.is_loading.lock().unwrap();
|
||||||
|
*is_loading = false;
|
||||||
|
self.loading_condvar.notify_all();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TranscriptionManager {
|
pub struct TranscriptionManager {
|
||||||
engine: Arc<Mutex<Option<LoadedEngine>>>,
|
engine: Arc<Mutex<Option<LoadedEngine>>>,
|
||||||
|
|
@ -96,11 +111,6 @@ impl TranscriptionManager {
|
||||||
let timeout_seconds = settings.model_unload_timeout.to_seconds();
|
let timeout_seconds = settings.model_unload_timeout.to_seconds();
|
||||||
|
|
||||||
if let Some(limit_seconds) = timeout_seconds {
|
if let Some(limit_seconds) = timeout_seconds {
|
||||||
// Skip polling-based unloading for immediate timeout since it's handled directly in transcribe()
|
|
||||||
if settings.model_unload_timeout == ModelUnloadTimeout::Immediately {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let last = manager_cloned.last_activity.load(Ordering::Relaxed);
|
let last = manager_cloned.last_activity.load(Ordering::Relaxed);
|
||||||
let now_ms = SystemTime::now()
|
let now_ms = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
|
@ -154,6 +164,22 @@ impl TranscriptionManager {
|
||||||
engine.is_some()
|
engine.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Atomically check whether a model load is in progress and, if not, mark
|
||||||
|
/// one as starting. Returns a [`LoadingGuard`] whose [`Drop`] impl will
|
||||||
|
/// clear the flag and wake waiters. Returns `None` if a load is already in
|
||||||
|
/// progress.
|
||||||
|
pub fn try_start_loading(&self) -> Option<LoadingGuard> {
|
||||||
|
let mut is_loading = self.is_loading.lock().unwrap();
|
||||||
|
if *is_loading {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
*is_loading = true;
|
||||||
|
Some(LoadingGuard {
|
||||||
|
is_loading: self.is_loading.clone(),
|
||||||
|
loading_condvar: self.loading_condvar.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unload_model(&self) -> Result<()> {
|
pub fn unload_model(&self) -> Result<()> {
|
||||||
let unload_start = std::time::Instant::now();
|
let unload_start = std::time::Instant::now();
|
||||||
debug!("Starting to unload model");
|
debug!("Starting to unload model");
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ pub struct ModelStateEvent {
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RAII guard that is a no-op in the mock — mirrors the real `LoadingGuard`.
|
||||||
|
pub struct LoadingGuard;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TranscriptionManager {
|
pub struct TranscriptionManager {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -33,6 +36,10 @@ impl TranscriptionManager {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn try_start_loading(&self) -> Option<LoadingGuard> {
|
||||||
|
Some(LoadingGuard)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn unload_model(&self) -> Result<()> {
|
pub fn unload_model(&self) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
use crate::managers::history::{HistoryEntry, HistoryManager};
|
use crate::managers::history::{HistoryEntry, HistoryManager};
|
||||||
|
use crate::managers::model::ModelManager;
|
||||||
use crate::managers::transcription::TranscriptionManager;
|
use crate::managers::transcription::TranscriptionManager;
|
||||||
use crate::settings;
|
use crate::settings;
|
||||||
use crate::tray_i18n::get_tray_translations;
|
use crate::tray_i18n::get_tray_translations;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::image::Image;
|
use tauri::image::Image;
|
||||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
use tauri::menu::{CheckMenuItem, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
||||||
use tauri::tray::TrayIcon;
|
use tauri::tray::TrayIcon;
|
||||||
use tauri::{AppHandle, Manager, Theme};
|
use tauri::{AppHandle, Manager, Theme};
|
||||||
use tauri_plugin_clipboard_manager::ClipboardExt;
|
use tauri_plugin_clipboard_manager::ClipboardExt;
|
||||||
|
|
@ -125,6 +126,40 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<&
|
||||||
)
|
)
|
||||||
.expect("failed to create copy last transcript item");
|
.expect("failed to create copy last transcript item");
|
||||||
let model_loaded = app.state::<Arc<TranscriptionManager>>().is_model_loaded();
|
let model_loaded = app.state::<Arc<TranscriptionManager>>().is_model_loaded();
|
||||||
|
let quit_i = MenuItem::with_id(app, "quit", &strings.quit, true, quit_accelerator)
|
||||||
|
.expect("failed to create quit item");
|
||||||
|
let separator = || PredefinedMenuItem::separator(app).expect("failed to create separator");
|
||||||
|
|
||||||
|
// Build model submenu — label is the active model name
|
||||||
|
let model_manager = app.state::<Arc<ModelManager>>();
|
||||||
|
let models = model_manager.get_available_models();
|
||||||
|
let current_model_id = &settings.selected_model;
|
||||||
|
|
||||||
|
let mut downloaded: Vec<_> = models.into_iter().filter(|m| m.is_downloaded).collect();
|
||||||
|
downloaded.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
let submenu_label = downloaded
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.id == *current_model_id)
|
||||||
|
.map(|m| m.name.clone())
|
||||||
|
.unwrap_or_else(|| strings.model.clone());
|
||||||
|
|
||||||
|
let model_submenu = {
|
||||||
|
let submenu = Submenu::with_id(app, "model_submenu", &submenu_label, true)
|
||||||
|
.expect("failed to create model submenu");
|
||||||
|
|
||||||
|
for model in &downloaded {
|
||||||
|
let is_active = model.id == *current_model_id;
|
||||||
|
let item_id = format!("model_select:{}", model.id);
|
||||||
|
let item =
|
||||||
|
CheckMenuItem::with_id(app, &item_id, &model.name, true, is_active, None::<&str>)
|
||||||
|
.expect("failed to create model item");
|
||||||
|
let _ = submenu.append(&item);
|
||||||
|
}
|
||||||
|
|
||||||
|
submenu
|
||||||
|
};
|
||||||
|
|
||||||
let unload_model_i = MenuItem::with_id(
|
let unload_model_i = MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
"unload_model",
|
"unload_model",
|
||||||
|
|
@ -133,9 +168,6 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<&
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
)
|
)
|
||||||
.expect("failed to create unload model item");
|
.expect("failed to create unload model item");
|
||||||
let quit_i = MenuItem::with_id(app, "quit", &strings.quit, true, quit_accelerator)
|
|
||||||
.expect("failed to create quit item");
|
|
||||||
let separator = || PredefinedMenuItem::separator(app).expect("failed to create separator");
|
|
||||||
|
|
||||||
let menu = match state {
|
let menu = match state {
|
||||||
TrayIconState::Recording | TrayIconState::Transcribing => {
|
TrayIconState::Recording | TrayIconState::Transcribing => {
|
||||||
|
|
@ -164,6 +196,8 @@ pub fn update_tray_menu(app: &AppHandle, state: &TrayIconState, locale: Option<&
|
||||||
&version_i,
|
&version_i,
|
||||||
&separator(),
|
&separator(),
|
||||||
©_last_transcript_i,
|
©_last_transcript_i,
|
||||||
|
&separator(),
|
||||||
|
&model_submenu,
|
||||||
&unload_model_i,
|
&unload_model_i,
|
||||||
&separator(),
|
&separator(),
|
||||||
&settings_i,
|
&settings_i,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "...التحقق من وجود تحديثات",
|
"checkUpdates": "...التحقق من وجود تحديثات",
|
||||||
"copyLastTranscript": "نسخ آخر نص تم تفريغه",
|
"copyLastTranscript": "نسخ آخر نص تم تفريغه",
|
||||||
"unloadModel": "تفريغ النموذج",
|
"unloadModel": "تفريغ النموذج",
|
||||||
|
"model": "النموذج",
|
||||||
"quit": "إنهاء",
|
"quit": "إنهاء",
|
||||||
"cancel": "إلغاء"
|
"cancel": "إلغاء"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Zkontrolovat aktualizace...",
|
"checkUpdates": "Zkontrolovat aktualizace...",
|
||||||
"copyLastTranscript": "Zkopírovat poslední přepis",
|
"copyLastTranscript": "Zkopírovat poslední přepis",
|
||||||
"unloadModel": "Uvolnit model",
|
"unloadModel": "Uvolnit model",
|
||||||
|
"model": "Model",
|
||||||
"quit": "Ukončit",
|
"quit": "Ukončit",
|
||||||
"cancel": "Zrušit"
|
"cancel": "Zrušit"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Nach Updates suchen...",
|
"checkUpdates": "Nach Updates suchen...",
|
||||||
"copyLastTranscript": "Letzte Transkription kopieren",
|
"copyLastTranscript": "Letzte Transkription kopieren",
|
||||||
"unloadModel": "Modell entladen",
|
"unloadModel": "Modell entladen",
|
||||||
|
"model": "Modell",
|
||||||
"quit": "Beenden",
|
"quit": "Beenden",
|
||||||
"cancel": "Abbrechen"
|
"cancel": "Abbrechen"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Check for Updates...",
|
"checkUpdates": "Check for Updates...",
|
||||||
"copyLastTranscript": "Copy Last Transcript",
|
"copyLastTranscript": "Copy Last Transcript",
|
||||||
"unloadModel": "Unload Model",
|
"unloadModel": "Unload Model",
|
||||||
|
"model": "Model",
|
||||||
"quit": "Quit",
|
"quit": "Quit",
|
||||||
"cancel": "Cancel"
|
"cancel": "Cancel"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Buscar actualizaciones...",
|
"checkUpdates": "Buscar actualizaciones...",
|
||||||
"copyLastTranscript": "Copiar la última transcripción",
|
"copyLastTranscript": "Copiar la última transcripción",
|
||||||
"unloadModel": "Descargar modelo",
|
"unloadModel": "Descargar modelo",
|
||||||
|
"model": "Modelo",
|
||||||
"quit": "Salir",
|
"quit": "Salir",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Rechercher des mises à jour...",
|
"checkUpdates": "Rechercher des mises à jour...",
|
||||||
"copyLastTranscript": "Copier la dernière transcription",
|
"copyLastTranscript": "Copier la dernière transcription",
|
||||||
"unloadModel": "Décharger le modèle",
|
"unloadModel": "Décharger le modèle",
|
||||||
|
"model": "Modèle",
|
||||||
"quit": "Quitter",
|
"quit": "Quitter",
|
||||||
"cancel": "Annuler"
|
"cancel": "Annuler"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Verifica aggiornamenti...",
|
"checkUpdates": "Verifica aggiornamenti...",
|
||||||
"copyLastTranscript": "Copia l'ultima trascrizione",
|
"copyLastTranscript": "Copia l'ultima trascrizione",
|
||||||
"unloadModel": "Scarica modello",
|
"unloadModel": "Scarica modello",
|
||||||
|
"model": "Modello",
|
||||||
"quit": "Esci",
|
"quit": "Esci",
|
||||||
"cancel": "Annulla"
|
"cancel": "Annulla"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "アップデートを確認...",
|
"checkUpdates": "アップデートを確認...",
|
||||||
"copyLastTranscript": "最新の文字起こしをコピー",
|
"copyLastTranscript": "最新の文字起こしをコピー",
|
||||||
"unloadModel": "モデルをアンロード",
|
"unloadModel": "モデルをアンロード",
|
||||||
|
"model": "モデル",
|
||||||
"quit": "終了",
|
"quit": "終了",
|
||||||
"cancel": "キャンセル"
|
"cancel": "キャンセル"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "업데이트 확인...",
|
"checkUpdates": "업데이트 확인...",
|
||||||
"copyLastTranscript": "마지막 녹음 내용 복사",
|
"copyLastTranscript": "마지막 녹음 내용 복사",
|
||||||
"unloadModel": "모델 언로드",
|
"unloadModel": "모델 언로드",
|
||||||
|
"model": "모델",
|
||||||
"quit": "종료",
|
"quit": "종료",
|
||||||
"cancel": "취소"
|
"cancel": "취소"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Sprawdź aktualizacje...",
|
"checkUpdates": "Sprawdź aktualizacje...",
|
||||||
"copyLastTranscript": "Kopiuj ostatnią transkrypcję",
|
"copyLastTranscript": "Kopiuj ostatnią transkrypcję",
|
||||||
"unloadModel": "Zwolnij model",
|
"unloadModel": "Zwolnij model",
|
||||||
|
"model": "Model",
|
||||||
"quit": "Zamknij",
|
"quit": "Zamknij",
|
||||||
"cancel": "Anuluj"
|
"cancel": "Anuluj"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Verificar Atualizações...",
|
"checkUpdates": "Verificar Atualizações...",
|
||||||
"copyLastTranscript": "Copiar última transcrição",
|
"copyLastTranscript": "Copiar última transcrição",
|
||||||
"unloadModel": "Descarregar modelo",
|
"unloadModel": "Descarregar modelo",
|
||||||
|
"model": "Modelo",
|
||||||
"quit": "Sair",
|
"quit": "Sair",
|
||||||
"cancel": "Cancelar"
|
"cancel": "Cancelar"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Проверить обновления...",
|
"checkUpdates": "Проверить обновления...",
|
||||||
"copyLastTranscript": "Скопировать последнюю транскрипцию",
|
"copyLastTranscript": "Скопировать последнюю транскрипцию",
|
||||||
"unloadModel": "Выгрузить модель",
|
"unloadModel": "Выгрузить модель",
|
||||||
|
"model": "Модель",
|
||||||
"quit": "Выход",
|
"quit": "Выход",
|
||||||
"cancel": "Отмена"
|
"cancel": "Отмена"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Güncellemeleri Kontrol Et...",
|
"checkUpdates": "Güncellemeleri Kontrol Et...",
|
||||||
"copyLastTranscript": "Son transkripti kopyala",
|
"copyLastTranscript": "Son transkripti kopyala",
|
||||||
"unloadModel": "Modeli boşalt",
|
"unloadModel": "Modeli boşalt",
|
||||||
|
"model": "Model",
|
||||||
"quit": "Çıkış",
|
"quit": "Çıkış",
|
||||||
"cancel": "İptal"
|
"cancel": "İptal"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Перевірити оновлення...",
|
"checkUpdates": "Перевірити оновлення...",
|
||||||
"copyLastTranscript": "Скопіювати останню транскрипцію",
|
"copyLastTranscript": "Скопіювати останню транскрипцію",
|
||||||
"unloadModel": "Вивантажити модель",
|
"unloadModel": "Вивантажити модель",
|
||||||
|
"model": "Модель",
|
||||||
"quit": "Вийти",
|
"quit": "Вийти",
|
||||||
"cancel": "Скасувати"
|
"cancel": "Скасувати"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "Kiểm tra cập nhật...",
|
"checkUpdates": "Kiểm tra cập nhật...",
|
||||||
"copyLastTranscript": "Sao chép bản chép lời mới nhất",
|
"copyLastTranscript": "Sao chép bản chép lời mới nhất",
|
||||||
"unloadModel": "Dỡ mô hình",
|
"unloadModel": "Dỡ mô hình",
|
||||||
|
"model": "Mô hình",
|
||||||
"quit": "Thoát",
|
"quit": "Thoát",
|
||||||
"cancel": "Hủy"
|
"cancel": "Hủy"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "檢查更新...",
|
"checkUpdates": "檢查更新...",
|
||||||
"copyLastTranscript": "複製最新轉錄",
|
"copyLastTranscript": "複製最新轉錄",
|
||||||
"unloadModel": "卸載模型",
|
"unloadModel": "卸載模型",
|
||||||
|
"model": "模型",
|
||||||
"quit": "結束",
|
"quit": "結束",
|
||||||
"cancel": "取消"
|
"cancel": "取消"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"checkUpdates": "检查更新...",
|
"checkUpdates": "检查更新...",
|
||||||
"copyLastTranscript": "复制最新转录",
|
"copyLastTranscript": "复制最新转录",
|
||||||
"unloadModel": "卸载模型",
|
"unloadModel": "卸载模型",
|
||||||
|
"model": "模型",
|
||||||
"quit": "退出",
|
"quit": "退出",
|
||||||
"cancel": "取消"
|
"cancel": "取消"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue