From 153421f00e2d3a4cf2dad6f6e1fd9d61452c34ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elberte=20Pl=C3=ADnio?= Date: Thu, 23 Jul 2026 22:34:37 -0300 Subject: [PATCH] fix(stt): auto-detect .en and other ggml model variants --- docs/releases/UNRELEASED.md | 5 +++ src/engine/stt.rs | 83 ++++++++++++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/docs/releases/UNRELEASED.md b/docs/releases/UNRELEASED.md index fdc8534..78f964e 100644 --- a/docs/releases/UNRELEASED.md +++ b/docs/releases/UNRELEASED.md @@ -21,6 +21,8 @@ then reset this file. preserves completed segments and only re-transcribes the unfinished tail. - PickScribe now finds `whisper-cli` in `~/.local/bin` when desktop sessions omit that directory from `PATH`. +- Whisper model auto-detection now recognizes English-only `.en` models and + other `ggml-*.bin` variants in the standard local model directory. - Double-clicking empty titlebar space now maximizes or restores the window. - On Linux, the float capsule now keeps its glow visible without capturing clicks in the transparent margin. Other platforms keep a snug window to @@ -101,6 +103,9 @@ then reset this file. ### Tested +- Whisper model auto-detection fix: `cargo test` and `cargo clippy --workspace + --all-targets -- -D warnings`. Live-verified the installed + `~/.local/share/whisper.cpp/models/ggml-base.en.bin` model is selected. - pickscribe#66 PR 2 (macOS audio capture via ffmpeg/avfoundation): `cargo test` (workspace root, 122 tests including new `recorder_args` and `platform` coverage), `cargo test --manifest-path src-tauri/Cargo.toml` (37 diff --git a/src/engine/stt.rs b/src/engine/stt.rs index 103fb1e..50fda77 100644 --- a/src/engine/stt.rs +++ b/src/engine/stt.rs @@ -43,17 +43,8 @@ pub fn detect_model_path() -> Option { } let home = std::env::var("HOME").ok()?; let model_dir = PathBuf::from(&home).join(".local/share/whisper.cpp/models"); - for name in [ - "ggml-large-v3-turbo.bin", - "ggml-large-v3-turbo-q5_0.bin", - "ggml-small.bin", - "ggml-base.bin", - "ggml-tiny.bin", - ] { - let candidate = model_dir.join(name); - if candidate.is_file() { - return Some(candidate); - } + if let Some(path) = detect_model_in_dir(&model_dir) { + return Some(path); } // Arch whisper.cpp-model-* packages if let Ok(entries) = fs::read_dir("/usr/share") { @@ -75,6 +66,39 @@ pub fn detect_model_path() -> Option { None } +fn detect_model_in_dir(model_dir: &Path) -> Option { + for name in [ + "ggml-large-v3-turbo.bin", + "ggml-large-v3-turbo-q5_0.bin", + "ggml-small.bin", + "ggml-small.en.bin", + "ggml-base.bin", + "ggml-base.en.bin", + "ggml-tiny.bin", + "ggml-tiny.en.bin", + ] { + let candidate = model_dir.join(name); + if candidate.is_file() { + return Some(candidate); + } + } + + let mut models = fs::read_dir(model_dir) + .ok()? + .flatten() + .map(|entry| entry.path()) + .filter(|path| { + path.is_file() + && path + .file_name() + .is_some_and(|name| name.to_string_lossy().starts_with("ggml-")) + && path.extension().is_some_and(|extension| extension == "bin") + }) + .collect::>(); + models.sort(); + models.into_iter().next() +} + /// List models available in the default model directory (for the settings UI). pub fn available_models() -> Vec { let mut out = Vec::new(); @@ -303,6 +327,43 @@ fn parse_progress_percentage(line: &str) -> Option { mod tests { use super::*; + #[test] + fn detects_english_model_from_preference_list() { + let dir = tempfile::tempdir().unwrap(); + let model = dir.path().join("ggml-base.en.bin"); + fs::write(&model, []).unwrap(); + + assert_eq!(detect_model_in_dir(dir.path()), Some(model)); + } + + #[test] + fn detects_unknown_model_by_sorted_filename() { + let dir = tempfile::tempdir().unwrap(); + let first = dir.path().join("ggml-foo.bin"); + fs::write(dir.path().join("ggml-zeta.bin"), []).unwrap(); + fs::write(&first, []).unwrap(); + + assert_eq!(detect_model_in_dir(dir.path()), Some(first)); + } + + #[test] + fn returns_none_for_empty_model_directory() { + let dir = tempfile::tempdir().unwrap(); + + assert_eq!(detect_model_in_dir(dir.path()), None); + } + + #[test] + fn respects_model_preference_order() { + let dir = tempfile::tempdir().unwrap(); + let preferred = dir.path().join("ggml-small.bin"); + fs::write(dir.path().join("ggml-base.bin"), []).unwrap(); + fs::write(dir.path().join("ggml-small.en.bin"), []).unwrap(); + fs::write(&preferred, []).unwrap(); + + assert_eq!(detect_model_in_dir(dir.path()), Some(preferred)); + } + #[test] fn parses_whisper_progress_percentages() { assert_eq!(