diff --git a/src-tauri/src/transcription/models.rs b/src-tauri/src/transcription/models.rs index f1de058..f360052 100644 --- a/src-tauri/src/transcription/models.rs +++ b/src-tauri/src/transcription/models.rs @@ -12,6 +12,12 @@ struct Catalog { id: &'static str, label: &'static str, size_mb: u32, + /// `false` for the `.en` (English-only) ggml variants; `true` for the + /// multilingual variants, which whisper.cpp ships as the same filename + /// minus the `.en` infix (e.g. `ggml-base.bin` vs `ggml-base.en.bin`) — + /// same download/install machinery, just a different id/URL (T8.7, + /// FR-TRX-4, M4.2). + multilingual: bool, } const CATALOG: &[Catalog] = &[ @@ -19,21 +25,49 @@ const CATALOG: &[Catalog] = &[ id: "tiny.en-q5_1", label: "Tiny (English, quantized) — fastest, least accurate", size_mb: 32, + multilingual: false, }, Catalog { id: "base.en-q5_1", label: "Base (English, quantized) — balanced default", size_mb: 60, + multilingual: false, }, Catalog { id: "small.en-q5_1", label: "Small (English, quantized) — more accurate, slower", size_mb: 190, + multilingual: false, }, Catalog { id: "medium.en-q5_1", label: "Medium (English, quantized) — best accuracy, slowest", size_mb: 540, + multilingual: false, + }, + Catalog { + id: "tiny-q5_1", + label: "Tiny (multilingual, quantized) — fastest, least accurate", + size_mb: 32, + multilingual: true, + }, + Catalog { + id: "base-q5_1", + label: "Base (multilingual, quantized) — balanced default", + size_mb: 60, + multilingual: true, + }, + Catalog { + id: "small-q5_1", + label: "Small (multilingual, quantized) — more accurate, slower", + size_mb: 190, + multilingual: true, + }, + Catalog { + id: "medium-q5_1", + label: "Medium (multilingual, quantized) — best accuracy, slowest", + size_mb: 540, + multilingual: true, }, ]; @@ -50,10 +84,19 @@ pub fn list(active_id: &str) -> Vec { size_mb: m.size_mb, installed: whisper_model_file(m.id).exists(), active: m.id == active_id, + multilingual: m.multilingual, }) .collect() } +/// Whether `id` names a multilingual (non-`.en`) catalog model — an unknown +/// id (shouldn't happen; callers validate against the catalog first) is +/// conservatively treated as English-only rather than granting language +/// selection it can't honor. +pub fn is_multilingual(id: &str) -> bool { + CATALOG.iter().any(|m| m.id == id && m.multilingual) +} + #[derive(Debug, thiserror::Error)] pub enum ModelError { #[error("unknown model id: {0}")] @@ -147,4 +190,38 @@ mod tests { let err = remove(active, active).unwrap_err(); assert!(matches!(err, ModelError::Invalid(_))); } + + #[test] + fn catalog_has_a_multilingual_counterpart_for_every_english_only_size() { + // T8.7/M4.2: every `.en` model must have a same-size multilingual + // sibling so the Settings picker always has a language-capable + // option at whatever accuracy/speed tier the user already chose. + let en_only: Vec<_> = CATALOG.iter().filter(|m| !m.multilingual).collect(); + let multilingual: Vec<_> = CATALOG.iter().filter(|m| m.multilingual).collect(); + assert_eq!(en_only.len(), multilingual.len()); + for en in &en_only { + assert!( + multilingual.iter().any(|m| m.size_mb == en.size_mb), + "no multilingual sibling for {}", + en.id + ); + } + } + + #[test] + fn is_multilingual_matches_the_catalog_flag() { + assert!(!is_multilingual("base.en-q5_1")); + assert!(is_multilingual("base-q5_1")); + // Unknown ids are conservatively English-only (no model to check). + assert!(!is_multilingual("nonexistent-id")); + } + + #[test] + fn list_reports_multilingual_flag_per_model() { + let models = list("base.en-q5_1"); + let base_en = models.iter().find(|m| m.id == "base.en-q5_1").unwrap(); + let base_multi = models.iter().find(|m| m.id == "base-q5_1").unwrap(); + assert!(!base_en.multilingual); + assert!(base_multi.multilingual); + } }