From 9e3f6327b752601bf68dd826b846bad25660f51a Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Thu, 16 Jul 2026 21:06:35 -0500 Subject: [PATCH 01/74] feat(models): add Q8_0 variants + large-v3-turbo q5_0/q8_0 to catalog (FR-MODEL-1) --- src-tauri/src/transcription/models.rs | 110 ++++++++++++++++++++++++-- 1 file changed, 103 insertions(+), 7 deletions(-) diff --git a/src-tauri/src/transcription/models.rs b/src-tauri/src/transcription/models.rs index dcb68db..8c956e0 100644 --- a/src-tauri/src/transcription/models.rs +++ b/src-tauri/src/transcription/models.rs @@ -20,6 +20,12 @@ struct Catalog { multilingual: bool, } +// Each size ships two quantizations: a q5 variant (recommended — smaller, +// near-identical quality) and q8_0 (less quantized, larger). The UI groups +// the pair into one row with a quantization dropdown, relying on the q5 +// variant being listed first within its family. Labels are intentionally +// identical across a family's variants so the row label doesn't change when +// the dropdown flips; the id/size carry the quantization difference. const CATALOG: &[Catalog] = &[ Catalog { id: "tiny.en-q5_1", @@ -27,51 +33,112 @@ const CATALOG: &[Catalog] = &[ size_mb: 32, multilingual: false, }, + Catalog { + id: "tiny.en-q8_0", + label: "Tiny (English, quantized) — fastest, least accurate", + size_mb: 42, + multilingual: false, + }, Catalog { id: "base.en-q5_1", label: "Base (English, quantized) — balanced default", size_mb: 60, multilingual: false, }, + Catalog { + id: "base.en-q8_0", + label: "Base (English, quantized) — balanced default", + size_mb: 78, + multilingual: false, + }, Catalog { id: "small.en-q5_1", label: "Small (English, quantized) — more accurate, slower", size_mb: 190, multilingual: false, }, + Catalog { + id: "small.en-q8_0", + label: "Small (English, quantized) — more accurate, slower", + size_mb: 252, + multilingual: false, + }, Catalog { // ggerganov/whisper.cpp only ships a q5_0 quantization for medium // (q5_1 doesn't exist upstream for this size) — q5_1 here 404s. id: "medium.en-q5_0", - label: "Medium (English, quantized) — best accuracy, slowest", + label: "Medium (English, quantized) — high accuracy, slowest", size_mb: 540, multilingual: false, }, + Catalog { + id: "medium.en-q8_0", + label: "Medium (English, quantized) — high accuracy, slowest", + size_mb: 785, + multilingual: false, + }, Catalog { id: "tiny-q5_1", label: "Tiny (multilingual, quantized) — fastest, least accurate", size_mb: 32, multilingual: true, }, + Catalog { + id: "tiny-q8_0", + label: "Tiny (multilingual, quantized) — fastest, least accurate", + size_mb: 42, + multilingual: true, + }, Catalog { id: "base-q5_1", label: "Base (multilingual, quantized) — balanced default", size_mb: 60, multilingual: true, }, + Catalog { + id: "base-q8_0", + label: "Base (multilingual, quantized) — balanced default", + size_mb: 78, + multilingual: true, + }, Catalog { id: "small-q5_1", label: "Small (multilingual, quantized) — more accurate, slower", size_mb: 190, multilingual: true, }, + Catalog { + id: "small-q8_0", + label: "Small (multilingual, quantized) — more accurate, slower", + size_mb: 252, + multilingual: true, + }, Catalog { // Same upstream-availability caveat as medium.en above. id: "medium-q5_0", - label: "Medium (multilingual, quantized) — best accuracy, slowest", + label: "Medium (multilingual, quantized) — high accuracy, slowest", size_mb: 540, multilingual: true, }, + Catalog { + id: "medium-q8_0", + label: "Medium (multilingual, quantized) — high accuracy, slowest", + size_mb: 785, + multilingual: true, + }, + // large-v3-turbo has no `.en` variant upstream — multilingual only. + Catalog { + id: "large-v3-turbo-q5_0", + label: "Large v3 Turbo (multilingual, quantized) — most accurate", + size_mb: 547, + multilingual: true, + }, + Catalog { + id: "large-v3-turbo-q8_0", + label: "Large v3 Turbo (multilingual, quantized) — most accurate", + size_mb: 833, + multilingual: true, + }, ]; fn model_url(id: &str) -> String { @@ -199,18 +266,47 @@ mod tests { // 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 { + // (The reverse doesn't hold: large-v3-turbo is multilingual-only.) + for en in CATALOG.iter().filter(|m| !m.multilingual) { assert!( - multilingual.iter().any(|m| m.size_mb == en.size_mb), + CATALOG + .iter() + .any(|m| m.multilingual && m.size_mb == en.size_mb), "no multilingual sibling for {}", en.id ); } } + #[test] + fn every_family_lists_q5_then_q8() { + // The Settings UI groups the catalog into one row per family + // (id minus the `-q*` suffix) with a quantization dropdown, and + // treats the family's FIRST variant as the recommended default — + // so each family must be exactly [q5, q8_0], in that order. + let mut families: Vec<(&str, Vec<&str>)> = Vec::new(); + for m in CATALOG { + let (family, _) = m.id.rsplit_once("-q").unwrap(); + match families.iter_mut().find(|(f, _)| *f == family) { + Some((_, ids)) => ids.push(m.id), + None => families.push((family, vec![m.id])), + } + } + for (family, ids) in &families { + assert_eq!(ids.len(), 2, "family {family} must have exactly 2 variants"); + assert!( + ids[0].contains("-q5_"), + "family {family} must list its q5 variant first, got {}", + ids[0] + ); + assert!( + ids[1].ends_with("-q8_0"), + "family {family} must list q8_0 second, got {}", + ids[1] + ); + } + } + #[test] fn is_multilingual_matches_the_catalog_flag() { assert!(!is_multilingual("base.en-q5_1")); From 88adce152b1ab441ffad0561a91a7ca636930f7a Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Thu, 16 Jul 2026 21:09:50 -0500 Subject: [PATCH 02/74] feat(ui): group whisper models per family with quantization dropdown (FR-MODEL-1) --- src/lib/views/Settings.svelte | 52 ++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/lib/views/Settings.svelte b/src/lib/views/Settings.svelte index 1c916f4..a293852 100644 --- a/src/lib/views/Settings.svelte +++ b/src/lib/views/Settings.svelte @@ -14,6 +14,7 @@ import { api, errorMessage, events } from "../api"; import type { BackendId, + ModelInfo, StressTestResult, SyncKind, SyncTargetConfig, @@ -394,6 +395,31 @@ if (!p || !p.total) return null; return Math.round((p.received / p.total) * 100); } + // Whisper models grouped one row per family (id minus the `-q*` suffix) + // with a quantization dropdown, instead of a separate row per quantization. + // The catalog guarantees each family lists its recommended q5 variant first. + const familyOf = (id: string) => id.replace(/-q\d_\d$/, ""); + const quantOf = (id: string) => (id.match(/-(q\d_\d)$/)?.[1] ?? "").toUpperCase(); + let modelFamilies = $derived.by(() => { + const fams: { key: string; variants: ModelInfo[] }[] = []; + for (const m of settings.models) { + const k = familyOf(m.id); + const f = fams.find((x) => x.key === k); + if (f) f.variants.push(m); + else fams.push({ key: k, variants: [m] }); + } + return fams; + }); + // Per-family dropdown choice; unset falls back to active > installed > recommended. + let quantChoice = $state>({}); + function shownVariant(key: string, variants: ModelInfo[]): ModelInfo { + return ( + variants.find((v) => v.id === quantChoice[key]) ?? + variants.find((v) => v.active) ?? + variants.find((v) => v.installed) ?? + variants[0] + ); + } let showConsent = $state(false); let testResult = $state<{ ok: boolean; message: string } | null>(null); @@ -1129,7 +1155,8 @@

{t("settings.hardware.models")}