Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 10 additions & 32 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ chrono = "0.4"
rusqlite = { version = "0.37", features = ["bundled"] }
tar = "0.4.44"
flate2 = "1.0"
transcribe-rs = "0.1.4"
transcribe-rs = { version="0.2.1", features = ["whisper", "parakeet", "whisperfile"] }
ferrous-opencc = "0.2.3"
specta = "=2.0.0-rc.22"
specta-typescript = "0.0.9"
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod audio;
pub mod history;
pub mod models;
pub mod transcription;
pub mod whisperfile;

use crate::settings::{get_settings, write_settings, AppSettings, LogLevel};
use crate::utils::cancel_current_operation;
Expand Down
18 changes: 18 additions & 0 deletions src-tauri/src/commands/whisperfile.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use crate::managers::whisperfile;
use tauri::AppHandle;

#[tauri::command]
#[specta::specta]
pub async fn download_whisperfile_binary(app: AppHandle) -> Result<String, String> {
let path = whisperfile::download_whisperfile(&app)
.await
.map_err(|e| format!("Failed to download whisperfile: {}", e))?;

Ok(path.to_string_lossy().to_string())
}

#[tauri::command]
#[specta::specta]
pub fn is_whisperfile_binary_downloaded(app: AppHandle) -> bool {
whisperfile::is_whisperfile_downloaded(&app)
}
3 changes: 3 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ pub fn run() {
shortcut::change_selected_language_setting,
shortcut::change_overlay_position_setting,
shortcut::change_debug_mode_setting,
shortcut::change_whisper_runtime_setting,
shortcut::change_word_correction_threshold_setting,
shortcut::change_paste_method_setting,
shortcut::change_clipboard_handling_setting,
Expand Down Expand Up @@ -305,6 +306,8 @@ pub fn run() {
commands::history::delete_history_entry,
commands::history::update_history_limit,
commands::history::update_recording_retention_period,
commands::whisperfile::download_whisperfile_binary,
commands::whisperfile::is_whisperfile_binary_downloaded,
helpers::clamshell::is_laptop,
]);

Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/managers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod audio;
pub mod history;
pub mod model;
pub mod transcription;
pub mod whisperfile;
140 changes: 122 additions & 18 deletions src-tauri/src/managers/transcription.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::audio_toolkit::apply_custom_words;
use crate::managers::model::{EngineType, ModelManager};
use crate::settings::{get_settings, ModelUnloadTimeout};
use crate::managers::whisperfile::get_whisperfile_path;
use crate::settings::{get_settings, ModelUnloadTimeout, WhisperRuntime};
use anyhow::Result;
use log::{debug, error, info, warn};
use serde::Serialize;
Expand All @@ -15,6 +16,7 @@ use transcribe_rs::{
ParakeetEngine, ParakeetInferenceParams, ParakeetModelParams, TimestampGranularity,
},
whisper::{WhisperEngine, WhisperInferenceParams},
whisperfile::{WhisperfileEngine, WhisperfileInferenceParams, WhisperfileModelParams},
},
TranscriptionEngine,
};
Expand All @@ -30,6 +32,7 @@ pub struct ModelStateEvent {
enum LoadedEngine {
Whisper(WhisperEngine),
Parakeet(ParakeetEngine),
Whisperfile(WhisperfileEngine),
}

#[derive(Clone)]
Expand Down Expand Up @@ -136,15 +139,24 @@ impl TranscriptionManager {
let unload_start = std::time::Instant::now();
debug!("Starting to unload model");

{
let old_engine = {
let mut engine = self.engine.lock().unwrap();
if let Some(ref mut loaded_engine) = *engine {
match loaded_engine {
LoadedEngine::Whisper(ref mut whisper) => whisper.unload_model(),
LoadedEngine::Parakeet(ref mut parakeet) => parakeet.unload_model(),
LoadedEngine::Whisperfile(ref mut whisperfile) => whisperfile.unload_model(),
}
}
*engine = None; // Drop the engine to free memory
engine.take() // Take the engine out to drop it safely
};

// Drop the old engine in a separate thread to avoid Tokio runtime drop panic
// This is necessary because WhisperfileEngine contains a Tokio runtime
if old_engine.is_some() {
thread::spawn(move || {
drop(old_engine);
});
}
{
let mut current_model = self.current_model_id.lock().unwrap();
Expand Down Expand Up @@ -205,25 +217,79 @@ impl TranscriptionManager {
}

let model_path = self.model_manager.get_model_path(model_id)?;
let settings = get_settings(&self.app_handle);

// Create appropriate engine based on model type
let loaded_engine = match model_info.engine_type {
EngineType::Whisper => {
let mut engine = WhisperEngine::new();
engine.load_model(&model_path).map_err(|e| {
let error_msg = format!("Failed to load whisper model {}: {}", model_id, e);
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
anyhow::anyhow!(error_msg)
})?;
LoadedEngine::Whisper(engine)
// Check if we should use Whisperfile runtime
if settings.whisper_runtime == WhisperRuntime::Whisperfile {
let binary_path = get_whisperfile_path(&self.app_handle).map_err(|e| {
let error_msg = format!("Failed to get whisperfile path: {}", e);
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
anyhow::anyhow!(error_msg)
})?;

if !binary_path.exists() {
let error_msg =
"Whisperfile binary not found. Please download it in Settings > Debug."
.to_string();
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
return Err(anyhow::anyhow!(error_msg));
}

info!("Using Whisperfile runtime with binary at {:?}", binary_path);
let mut engine = WhisperfileEngine::new(binary_path);
let params = WhisperfileModelParams::default();
engine.load_model_with_params(&model_path, params).map_err(|e| {
let error_msg =
format!("Failed to load whisperfile model {}: {}", model_id, e);
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
anyhow::anyhow!(error_msg)
})?;
LoadedEngine::Whisperfile(engine)
} else {
// Standard Whisper runtime
let mut engine = WhisperEngine::new();
engine.load_model(&model_path).map_err(|e| {
let error_msg = format!("Failed to load whisper model {}: {}", model_id, e);
let _ = self.app_handle.emit(
"model-state-changed",
ModelStateEvent {
event_type: "loading_failed".to_string(),
model_id: Some(model_id.to_string()),
model_name: Some(model_info.name.clone()),
error: Some(error_msg.clone()),
},
);
anyhow::anyhow!(error_msg)
})?;
LoadedEngine::Whisper(engine)
}
}
EngineType::Parakeet => {
let mut engine = ParakeetEngine::new();
Expand All @@ -248,9 +314,21 @@ impl TranscriptionManager {
};

// Update the current engine and model ID
// First, take the old engine out to drop it safely in a separate thread
// This prevents "Cannot drop a runtime in a context where blocking is not allowed" panic
{
let mut engine = self.engine.lock().unwrap();
let old_engine = engine.take();
*engine = Some(loaded_engine);

// Drop the old engine in a separate thread to avoid Tokio runtime drop panic
// WhisperfileEngine contains a Tokio runtime, and dropping it from within
// an async context (when called from set_active_model) causes a panic
if old_engine.is_some() {
thread::spawn(move || {
drop(old_engine);
});
}
}
{
let mut current_model = self.current_model_id.lock().unwrap();
Expand Down Expand Up @@ -384,6 +462,32 @@ impl TranscriptionManager {
.transcribe_samples(audio, Some(params))
.map_err(|e| anyhow::anyhow!("Parakeet transcription failed: {}", e))?
}
LoadedEngine::Whisperfile(whisperfile_engine) => {
// Normalize language code (same as Whisper)
let whisperfile_language = if settings.selected_language == "auto" {
None
} else {
let normalized = if settings.selected_language == "zh-Hans"
|| settings.selected_language == "zh-Hant"
{
"zh".to_string()
} else {
settings.selected_language.clone()
};
Some(normalized)
};

let params = WhisperfileInferenceParams {
language: whisperfile_language,
translate: settings.translate_to_english,
temperature: None,
response_format: Some("verbose_json".to_string()),
};

whisperfile_engine
.transcribe_samples(audio, Some(params))
.map_err(|e| anyhow::anyhow!("Whisperfile transcription failed: {}", e))?
}
}
};

Expand Down
Loading
Loading