diff --git a/README.md b/README.md
index ec2a7fa..4ba3f41 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,7 @@ curl -fsSL raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /
The installer will:
- **Auto-detect your hardware** (GPU, RAM, Vulkan support)
- **Recommend the best engine** for your system
-- **Download the appropriate model** (~74MB for whisper.cpp tiny)
+- **Download the appropriate model** (~74MB for the default whisper.cpp tiny model)
- **Install neural VAD support** when ONNX Runtime is available
- **Install in ~1-2 minutes** (vs 5-10 min with old Whisper)
@@ -243,6 +243,8 @@ vocalinux --engine whisper_cpp # Use whisper.cpp engine (default)
vocalinux --engine whisper # Use OpenAI Whisper engine
vocalinux --engine vosk # Use VOSK engine
vocalinux --model medium # Use medium-sized model
+vocalinux --model medium.en-q5_0 # Use exact whisper.cpp model variant
+vocalinux --model large-v3-turbo # Use large-v3 Turbo with whisper.cpp
vocalinux --wayland # Force Wayland mode
vocalinux --start-minimized # Start without first-run modal prompts
```
@@ -283,7 +285,10 @@ Configuration is stored in `~/.config/vocalinux/config.json`:
}
```
-You can also configure settings through the graphical Settings dialog (right-click the tray icon).
+For whisper.cpp, `model_size` may be a size such as `tiny` or an exact ggml model ID
+such as `medium.en-q5_0` or `large-v3-turbo`. You can also configure this through
+the graphical Settings dialog, where whisper.cpp models are split into **Model Size**
+and **Specialization** controls.
### Neural Voice Activity Detection
diff --git a/docs/INSTALL.md b/docs/INSTALL.md
index 1c2e7d4..7eccf8f 100644
--- a/docs/INSTALL.md
+++ b/docs/INSTALL.md
@@ -16,7 +16,7 @@ That's it! The installer handles everything automatically:
- ✅ Installs whisper.cpp (~1-2 minutes, no heavy dependencies!)
- ✅ Auto-detects your GPU (AMD, Intel, NVIDIA all supported)
- ✅ Installs neural VAD support when ONNX Runtime is available
-- ✅ Downloads the tiny model (~74MB)
+- ✅ Downloads the default whisper.cpp tiny model (~74MB)
- ✅ Configures everything automatically
> ⏱️ **Installation Time**: ~1-2 minutes (vs 5-10 minutes with old Whisper AI)
@@ -205,7 +205,7 @@ Examples:
- **VOSK**: Lightweight engine for older systems
5. **Installs neural VAD support** when ONNX Runtime is available, with a safe amplitude-VAD fallback otherwise
6. **Downloads speech recognition models**:
- - whisper.cpp: ~74MB tiny model (or larger if selected)
+ - whisper.cpp: default ~74MB tiny model; selectable variants range from smaller quantized models to large models
- Whisper: ~75MB tiny model + PyTorch dependencies (~2.3GB with CUDA)
- VOSK: ~40MB small model
7. **Installs desktop integration** (icons, .desktop file)
@@ -215,7 +215,7 @@ Examples:
| Engine | Download Size | Install Time | GPU Support |
|--------|---------------|--------------|-------------|
-| **whisper.cpp** (default) | ~74-500MB | ~1-2 min | AMD, Intel, NVIDIA (Vulkan) |
+| **whisper.cpp** (default) | ~74MB default; variants from ~15MB-3GB | ~1-2 min | AMD, Intel, NVIDIA (Vulkan) |
| Whisper (OpenAI) | ~2.3GB+ | ~5-10 min | NVIDIA only (CUDA) |
| VOSK | ~40MB | ~30 sec | CPU only |
@@ -247,6 +247,8 @@ vocalinux --model tiny # Use tiny model (default, fastest)
vocalinux --model small # Use small model
vocalinux --model medium # Use medium model
vocalinux --model large # Use large model
+vocalinux --model medium.en-q5_0 # Use exact whisper.cpp model variant
+vocalinux --model large-v3-turbo # Use large-v3 Turbo with whisper.cpp
vocalinux --wayland # Force Wayland compatibility mode
vocalinux --start-minimized # Start without first-run modal prompts
```
@@ -449,13 +451,25 @@ gtk-update-icon-cache -f -t ~/.local/share/icons/hicolor
- **No special drivers** - Just needs standard Vulkan support
**Models:**
-whisper.cpp uses the same models as OpenAI Whisper, converted to `ggml` format:
+whisper.cpp uses OpenAI Whisper models converted to `ggml` format. Vocalinux lets you
+pick a top-level size and, for whisper.cpp, a specialization:
+
- **tiny** (~74MB) - Fastest, good for real-time dictation
- **base** (~141MB) - Good balance of speed and accuracy
- **small** (~465MB) - Better accuracy, still fast
- **medium** (~1.5GB) - High accuracy
- **large** (~3.0GB) - Best accuracy, slower
+Available whisper.cpp specializations include:
+- **Standard multilingual** - Best default for auto-detect or non-English dictation
+- **English-only** - Choose when you dictate only in English
+- **Quantized** - Lower memory and smaller downloads with a possible accuracy tradeoff
+- **Turbo** - Faster large-v3 option with strong accuracy
+- **Legacy large** - Use only if you specifically need an older large model version
+
+Examples of exact whisper.cpp model IDs are `tiny.en`, `small.en-q5_1`,
+`medium-q5_0`, `medium.en-q5_0`, `large-v3-turbo`, and `large-v3-turbo-q8_0`.
+
### Checking GPU Support
To verify your GPU is detected:
@@ -494,12 +508,13 @@ Change the `engine` field:
{
"speech_recognition": {
"engine": "whisper_cpp", // Options: whisper_cpp, whisper, vosk
- "model_size": "tiny"
+ "model_size": "tiny" // Or an exact whisper.cpp ID like medium.en-q5_0
}
}
```
-Or use the GUI: Right-click tray icon → Settings → Speech Engine
+Or use the GUI: Right-click tray icon → Settings → Speech Engine. For whisper.cpp,
+the GUI splits this into **Model Size** and **Specialization**.
## Troubleshooting
diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md
index 19b0646..9387a77 100644
--- a/docs/USER_GUIDE.md
+++ b/docs/USER_GUIDE.md
@@ -69,7 +69,9 @@ Vocalinux supports several commands that you can speak to control formatting:
6. **Use GPU acceleration**: If you have a GPU (AMD, Intel, or NVIDIA), whisper.cpp will automatically use it for faster transcription
7. **Choose the right model**:
- For real-time dictation: Use `tiny` or `base` (fastest)
- - For better accuracy: Use `vocalinux --model medium` or `--model large`
+ - For better accuracy: Use `small`, `medium`, or `large`
+ - For English-only dictation: Choose an `.en` specialization
+ - For lower-memory systems: Choose a quantized specialization such as `q5_0` or `q5_1`
8. **Check debug logs**: Run `vocalinux --debug` to see which backend is being used (Vulkan, CUDA, or CPU)
## Customization
@@ -121,6 +123,14 @@ Vocalinux now offers **three speech recognition engines**:
- **small** (~465MB) - Better accuracy
- **medium** (~1.5GB) - High accuracy
- **large** (~3.0GB) - Best accuracy, slower
+5. For whisper.cpp, select a **Specialization**:
+ - **Standard multilingual** - Best default for auto-detect or non-English dictation
+ - **English-only** - Choose when you dictate only in English
+ - **Quantized** - Lower memory and smaller downloads with a possible accuracy tradeoff
+ - **Turbo** - Faster large-v3 option with strong accuracy
+ - **Legacy large** - Use only if you specifically need an older large model version
+
+English-only whisper.cpp specializations limit the language selector to English.
### When to Use Each Model
@@ -130,6 +140,10 @@ Vocalinux now offers **three speech recognition engines**:
**For maximum accuracy:** Use **large** - best results but requires more RAM and GPU power.
+**For lower-memory systems:** Use a quantized whisper.cpp specialization such as **Q5** or **Q8**.
+
+**For English-only dictation:** Use an **English-only** specialization and keep the language set to English.
+
### GPU Acceleration
**whisper.cpp** automatically uses GPU acceleration when available:
diff --git a/src/vocalinux/main.py b/src/vocalinux/main.py
index f691e78..3164cdd 100644
--- a/src/vocalinux/main.py
+++ b/src/vocalinux/main.py
@@ -28,8 +28,10 @@ def parse_arguments():
parser.add_argument(
"--model",
type=str,
- choices=["small", "medium", "large"],
- help="Speech recognition model size (small, medium, large)",
+ help=(
+ "Speech recognition model ID. Examples: small, medium, large, "
+ "medium.en-q5_0, large-v3-turbo"
+ ),
)
parser.add_argument(
"--language",
diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py
index e03c876..9ec00cd 100644
--- a/src/vocalinux/ui/settings_dialog.py
+++ b/src/vocalinux/ui/settings_dialog.py
@@ -18,7 +18,7 @@ import logging
import os
import threading
import time
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
import gi
@@ -30,12 +30,16 @@ from gi.repository import Gdk, GLib, Gtk, Pango # noqa: E402
from ..common_types import RecognitionState # noqa: E402
from ..speech_recognition.silero_vad import is_silero_available # noqa: E402
from ..utils.vosk_model_info import SUPPORTED_LANGUAGES, VOSK_MODEL_INFO # noqa: E402
+from ..utils.whispercpp_model_info import MODEL_SIZES as WHISPERCPP_MODEL_SIZES
from ..utils.whispercpp_model_info import (
WHISPERCPP_MODEL_INFO,
detect_compute_backend,
get_backend_display_name,
)
+from ..utils.whispercpp_model_info import get_model_size as get_whispercpp_model_size
+from ..utils.whispercpp_model_info import get_model_variants as get_whispercpp_model_variants
from ..utils.whispercpp_model_info import get_recommended_model as get_recommended_whispercpp_model
+from ..utils.whispercpp_model_info import is_english_only_model as is_english_only_whispercpp_model
from ..utils.whispercpp_model_info import is_model_downloaded as is_whispercpp_model_downloaded
from .config_manager import DEFAULT_CONFIG # noqa: E402
from .keyboard_backends import ( # noqa: E402
@@ -67,12 +71,8 @@ ENGINE_MODELS = {
"large",
], # Add more whisper sizes if needed
"whisper_cpp": [
- "tiny",
- "base",
- "small",
- "medium",
- "large",
- ], # whisper.cpp models (ggml format)
+ *WHISPERCPP_MODEL_SIZES,
+ ], # whisper.cpp top-level size buckets; variants are selected separately
"remote_api": [], # Remote API does not need local models
}
@@ -98,6 +98,165 @@ def _engine_from_display(display_name: str) -> str:
return display_name.lower()
+def _model_display_name(model_name: str) -> str:
+ """Get a user-friendly model display name."""
+ if model_name == "large":
+ return "Large v3"
+
+ display_parts = []
+ for part in model_name.split("-"):
+ if part.endswith(".en"):
+ display_parts.append(part[:-3].capitalize())
+ display_parts.append("EN")
+ elif part.startswith("q"):
+ display_parts.append(part.upper())
+ elif part == "turbo":
+ display_parts.append("Turbo")
+ elif part.startswith("v") and part[1:].isdigit():
+ display_parts.append(part)
+ else:
+ display_parts.append(part.capitalize())
+
+ return " ".join(display_parts)
+
+
+def _model_specialization_display_name(model_name: str) -> str:
+ """Get a concise label for a whisper.cpp model variant."""
+ if model_name == "large":
+ return "Standard v3"
+
+ quantization = None
+ for part in model_name.split("-"):
+ if part.startswith("q"):
+ quantization = part.upper()
+ break
+
+ if model_name.startswith("large-v") and "turbo" not in model_name:
+ version = next(
+ (part for part in model_name.split("-") if part.startswith("v")),
+ "large",
+ )
+ if quantization:
+ return f"{version} {quantization}"
+ return version
+
+ if is_english_only_whispercpp_model(model_name):
+ return f"English-only {quantization}" if quantization else "English-only"
+
+ if "turbo" in model_name:
+ if quantization:
+ return f"Turbo {quantization}"
+ return "Turbo"
+
+ if quantization:
+ return f"Quantized {quantization}"
+
+ return "Standard multilingual"
+
+
+def _language_is_english(language_id: str) -> bool:
+ """Return whether a language ID maps to English for Whisper."""
+ return SUPPORTED_LANGUAGES.get(language_id, {}).get("whisper") == "en"
+
+
+def _recommended_whispercpp_variant_for_language(
+ recommended_model: str,
+ reason: str,
+ language_id: str,
+) -> tuple[str, str]:
+ """Adjust a hardware recommendation to the selected language."""
+ recommended_size = get_whispercpp_model_size(recommended_model)
+ english_variant = f"{recommended_size}.en"
+
+ if _language_is_english(language_id) and english_variant in WHISPERCPP_MODEL_INFO:
+ return english_variant, f"{reason}; English language selected"
+
+ return recommended_model, reason
+
+
+def _default_whispercpp_variant_for_size(model_size: str, language_id: str) -> Optional[str]:
+ """Return the default specialization for a user-selected size and language."""
+ variants = get_whispercpp_model_variants(model_size)
+ if not variants:
+ return None
+
+ english_variant = f"{model_size}.en"
+ if _language_is_english(language_id) and english_variant in variants:
+ return english_variant
+
+ standard_variant = "large" if model_size == "large" else model_size
+ if standard_variant in variants:
+ return standard_variant
+
+ return variants[0]
+
+
+MODEL_SIZE_TOOLTIP = (
+ "Choose the largest model your computer can run comfortably. Tiny/Base are fastest, "
+ "Small is balanced, and Medium/Large can be more accurate but need more memory."
+)
+MODEL_SPECIALIZATION_TOOLTIP = (
+ "Choose Standard multilingual unless you specifically need English-only accuracy, "
+ "lower-memory quantized models, Turbo speed, or a legacy large model."
+)
+LANGUAGE_TOOLTIP = (
+ "Choose the language you dictate in. English-only model specializations limit this "
+ "list to English."
+)
+
+
+def _model_specialization_tooltip(model_name: str) -> str:
+ """Return hover guidance for a whisper.cpp specialization."""
+ is_english_only = is_english_only_whispercpp_model(model_name)
+ is_quantized = any(part.startswith("q") for part in model_name.split("-"))
+
+ if "turbo" in model_name:
+ if is_quantized:
+ return (
+ "Choose this for a faster large-v3 Turbo model with lower disk and memory use; "
+ "expect a small accuracy tradeoff from quantization."
+ )
+ return (
+ "Choose Turbo when you want high accuracy from a large model with less memory use "
+ "and faster inference than full large v3."
+ )
+
+ if model_name.startswith("large-v") and model_name != "large":
+ return (
+ "Choose this only if you specifically want that legacy large model version; "
+ "Standard v3 or Turbo is the better default for most users."
+ )
+
+ if is_english_only and is_quantized:
+ return (
+ "Choose this for English-only dictation on lower-memory systems; choose "
+ "multilingual if you use auto-detect or any non-English language."
+ )
+
+ if is_english_only:
+ return (
+ "Choose this when you dictate only in English. It can be better for English, "
+ "but it will not work for other languages."
+ )
+
+ if is_quantized:
+ return (
+ "Choose this on lower-memory systems or when download size matters; it uses "
+ "less disk and RAM with a possible accuracy tradeoff."
+ )
+
+ if model_name == "large":
+ return (
+ "Choose Standard v3 when you want the highest default accuracy and have enough "
+ "memory for a large model."
+ )
+
+ return (
+ "Choose Standard multilingual for most users, auto-detect, or any supported "
+ "non-English language."
+ )
+
+
# Whisper model metadata for display
WHISPER_MODEL_INFO = {
"tiny": {"size_mb": 75, "desc": "Fastest, lowest accuracy", "params": "39M"},
@@ -635,7 +794,7 @@ class ModelDownloadDialog(Gtk.Dialog):
language: str = "en-us",
):
super().__init__(
- title=f"Downloading {model_name.capitalize()} Model",
+ title=f"Downloading {_model_display_name(model_name)} Model",
transient_for=parent,
flags=Gtk.DialogFlags.MODAL,
)
@@ -657,7 +816,10 @@ class ModelDownloadDialog(Gtk.Dialog):
# Info label
self.info_label = Gtk.Label(
- label=f"Downloading {engine_display} {model_name} model (~{_format_size(model_size_mb)})...",
+ label=(
+ f"Downloading {engine_display} {_model_display_name(model_name)} model "
+ f"(~{_format_size(model_size_mb)})..."
+ ),
wrap=True,
justify=Gtk.Justification.CENTER,
)
@@ -811,7 +973,7 @@ class SettingsDialog(Gtk.Dialog):
else:
screen_height = 1080 # Default fallback
screen_width = 1920
- dialog_height = int(screen_height * 0.4)
+ dialog_height = min(600, int(screen_height * 0.4))
dialog_width = min(700, int(screen_width * 0.8))
self.set_default_size(dialog_width, dialog_height)
self.get_style_context().add_class("settings-dialog")
@@ -1120,24 +1282,40 @@ class SettingsDialog(Gtk.Dialog):
# Model size selection
self.model_combo = Gtk.ComboBoxText()
self.model_combo.set_size_request(180, -1)
+ self.model_combo.set_tooltip_text(MODEL_SIZE_TOOLTIP)
_prevent_scroll_on_hover(self.model_combo)
self.model_row = PreferenceRow(
title="Model Size",
subtitle="Larger models are more accurate but slower",
widget=self.model_combo,
)
+ self.model_row.set_tooltip_text(MODEL_SIZE_TOOLTIP)
group.add_row(self.model_row)
+ # whisper.cpp specialization selection
+ self.model_variant_combo = Gtk.ComboBoxText()
+ self.model_variant_combo.set_size_request(220, -1)
+ self.model_variant_combo.set_tooltip_text(MODEL_SPECIALIZATION_TOOLTIP)
+ _prevent_scroll_on_hover(self.model_variant_combo)
+ self.model_variant_row = PreferenceRow(
+ title="Specialization",
+ subtitle="Variant for language, speed, or memory use",
+ widget=self.model_variant_combo,
+ )
+ self.model_variant_row.set_tooltip_text(MODEL_SPECIALIZATION_TOOLTIP)
+ group.add_row(self.model_variant_row)
+
# Language selection
self.language_combo = Gtk.ComboBoxText()
self.language_combo.set_size_request(180, -1)
- self.language_combo.set_tooltip_text("Primary language for speech recognition")
+ self.language_combo.set_tooltip_text(LANGUAGE_TOOLTIP)
_prevent_scroll_on_hover(self.language_combo)
self.language_row = PreferenceRow(
title="Language",
subtitle="Primary language for recognition",
widget=self.language_combo,
)
+ self.language_row.set_tooltip_text(LANGUAGE_TOOLTIP)
group.add_row(self.language_row)
self.content_box.pack_start(group, False, False, 0)
@@ -1188,6 +1366,7 @@ class SettingsDialog(Gtk.Dialog):
# Connect signals
self.engine_combo.connect("changed", self._on_engine_changed)
self.model_combo.connect("changed", self._on_model_changed)
+ self.model_variant_combo.connect("changed", self._on_model_variant_changed)
self.language_combo.connect("changed", self._on_language_changed)
def _on_remote_api_settings_changed(self, widget):
@@ -2063,16 +2242,9 @@ class SettingsDialog(Gtk.Dialog):
if self.engine_combo.get_model():
self.engine_combo.set_active(0)
- # Populate model options for the selected engine
+ # Populate model and language options for the selected engine
self._populate_model_options()
-
- # Populate language options
- self._populate_language_options()
- if self.language:
- if not self.language_combo.set_active_id(self.language):
- logger.warning(f"Language '{self.language}' not found in options, using auto")
- self.language_combo.set_active_id("auto")
- self.language = "auto"
+ self._sync_language_options_for_selected_model(self.language)
# Set spin button values
self.vad_spin.set_value(self.current_vad)
@@ -2136,11 +2308,110 @@ class SettingsDialog(Gtk.Dialog):
"silence_timeout": silence_timeout,
}
+ def _get_selected_engine(self) -> str:
+ """Return the currently selected engine ID."""
+ engine_text = self.engine_combo.get_active_text()
+ return _engine_from_display(engine_text) if engine_text else "vosk"
+
+ def _get_selected_whispercpp_model(self) -> str:
+ """Return the selected whisper.cpp model variant."""
+ variant_id = self.model_variant_combo.get_active_id()
+ if variant_id:
+ return variant_id
+
+ size_id = self.model_combo.get_active_id()
+ model_size = size_id.lower() if size_id else "small"
+ variants = get_whispercpp_model_variants(model_size)
+ return variants[0] if variants else "small"
+
+ def _get_recommended_whispercpp_model_for_language(self) -> tuple[str, str]:
+ """Return the recommended whisper.cpp variant for the selected language."""
+ recommended_model, reason = get_recommended_whispercpp_model()
+ language_id = self.language_combo.get_active_id() or self.language
+ return _recommended_whispercpp_variant_for_language(
+ recommended_model,
+ reason,
+ language_id,
+ )
+
+ def _get_default_whispercpp_variant_for_size(self, model_size: str) -> Optional[str]:
+ """Return the default specialization for a user-selected size."""
+ language_id = self.language_combo.get_active_id() or self.language
+ return _default_whispercpp_variant_for_size(model_size, language_id)
+
+ def _is_selected_whispercpp_model_english_only(self) -> bool:
+ """Return whether the selected model is a whisper.cpp English-only variant."""
+ if self._get_selected_engine() != "whisper_cpp":
+ return False
+ return is_english_only_whispercpp_model(self._get_selected_whispercpp_model())
+
+ def _set_combo_active_id_or_first(self, combo, active_id: Optional[str]) -> bool:
+ """Set a combo to an ID, falling back to the first row."""
+ if active_id and combo.set_active_id(active_id):
+ return True
+
+ model = combo.get_model()
+ if model:
+ combo.set_active(0)
+ return True
+ return False
+
+ def _default_language_for_engine(self, engine: str) -> str:
+ """Return a safe default language for the selected engine and model."""
+ if engine == "vosk" or self._is_selected_whispercpp_model_english_only():
+ return "en-us"
+ return "auto"
+
+ def _update_model_picker_tooltips(self):
+ """Refresh model picker hover guidance for the current selection."""
+ self.model_combo.set_tooltip_text(MODEL_SIZE_TOOLTIP)
+ self.model_row.set_tooltip_text(MODEL_SIZE_TOOLTIP)
+ self.language_combo.set_tooltip_text(LANGUAGE_TOOLTIP)
+ self.language_row.set_tooltip_text(LANGUAGE_TOOLTIP)
+
+ if self._get_selected_engine() == "whisper_cpp":
+ specialization_tooltip = _model_specialization_tooltip(
+ self._get_selected_whispercpp_model()
+ )
+ else:
+ specialization_tooltip = MODEL_SPECIALIZATION_TOOLTIP
+
+ self.model_variant_combo.set_tooltip_text(specialization_tooltip)
+ self.model_variant_row.set_tooltip_text(specialization_tooltip)
+
+ def _sync_language_options_for_selected_model(self, preferred_language: Optional[str] = None):
+ """Refresh language options and keep the current model/language pair valid."""
+ engine = self._get_selected_engine()
+ language_to_keep = (
+ preferred_language or self.language_combo.get_active_id() or self.language
+ )
+
+ self._processing_language_change = True
+ try:
+ self._populate_language_options()
+ if not self._set_combo_active_id_or_first(self.language_combo, language_to_keep):
+ return
+
+ active_language = self.language_combo.get_active_id()
+ if active_language != language_to_keep:
+ fallback_language = self._default_language_for_engine(engine)
+ self._set_combo_active_id_or_first(self.language_combo, fallback_language)
+
+ self.language = (
+ self.language_combo.get_active_id() or self._default_language_for_engine(engine)
+ )
+ finally:
+ self._processing_language_change = False
+
+ self._update_language_warning()
+ self._update_model_picker_tooltips()
+
def _populate_model_options(self):
"""Populate model options based on the current engine selection."""
self._populating_models = True
try:
self.model_combo.remove_all()
+ self.model_variant_combo.remove_all()
engine_text = self.engine_combo.get_active_text()
if not engine_text:
@@ -2158,12 +2429,14 @@ class SettingsDialog(Gtk.Dialog):
saved_model_for_engine = self.config_manager.get_model_size_for_engine(engine)
logger.info(f"Saved model for {engine}: {saved_model_for_engine}")
+ if engine == "whisper_cpp":
+ self._populate_whispercpp_model_options(saved_model_for_engine)
+ return
+
downloaded_models = []
smallest_model = None
if engine == "whisper":
recommended_model, _ = _get_recommended_whisper_model()
- elif engine == "whisper_cpp":
- recommended_model, _ = get_recommended_whispercpp_model()
else:
recommended_model, _ = _get_recommended_vosk_model()
@@ -2172,9 +2445,6 @@ class SettingsDialog(Gtk.Dialog):
if engine == "whisper" and size in WHISPER_MODEL_INFO:
info = WHISPER_MODEL_INFO[size]
is_downloaded = _is_whisper_model_downloaded(size)
- elif engine == "whisper_cpp" and size in WHISPERCPP_MODEL_INFO:
- info = WHISPERCPP_MODEL_INFO[size]
- is_downloaded = is_whispercpp_model_downloaded(size)
elif engine == "vosk" and size in VOSK_MODEL_INFO:
info = VOSK_MODEL_INFO[size]
is_downloaded = _is_vosk_model_downloaded(size, self.language)
@@ -2182,9 +2452,13 @@ class SettingsDialog(Gtk.Dialog):
is_downloaded = False
info = {"size_mb": 0}
+ model_display_name = _model_display_name(size)
status = "✓" if is_downloaded else "↓"
star = " ★" if size == recommended_model else ""
- display_text = f"{size.capitalize()} ({_format_size(info.get('size_mb', 0))}) {status}{star}"
+ display_text = (
+ f"{model_display_name} ({_format_size(info.get('size_mb', 0))}) "
+ f"{status}{star}"
+ )
if is_downloaded:
downloaded_models.append(size)
@@ -2221,6 +2495,62 @@ class SettingsDialog(Gtk.Dialog):
finally:
self._populating_models = False
+ def _populate_whispercpp_model_options(self, saved_model_for_engine: str):
+ """Populate whisper.cpp size and specialization selectors."""
+ recommended_model, _ = self._get_recommended_whispercpp_model_for_language()
+ recommended_size = get_whispercpp_model_size(recommended_model)
+
+ saved_model = saved_model_for_engine.lower()
+ if saved_model not in WHISPERCPP_MODEL_INFO:
+ saved_model = (
+ recommended_model if recommended_model in WHISPERCPP_MODEL_INFO else "tiny"
+ )
+
+ saved_size = get_whispercpp_model_size(saved_model)
+
+ for model_size in ENGINE_MODELS["whisper_cpp"]:
+ display_text = _model_display_name(model_size)
+ if model_size == recommended_size:
+ display_text += " ★"
+ self.model_combo.append(model_size, display_text)
+
+ self._set_combo_active_id_or_first(self.model_combo, saved_size)
+ active_size = self.model_combo.get_active_id() or saved_size
+ self._populate_whispercpp_variant_options(active_size, saved_model)
+
+ def _populate_whispercpp_variant_options(
+ self,
+ model_size: str,
+ selected_model: Optional[str] = None,
+ ):
+ """Populate the whisper.cpp specialization selector for a size."""
+ self.model_variant_combo.remove_all()
+
+ variants = get_whispercpp_model_variants(model_size.lower())
+ recommended_model, _ = self._get_recommended_whispercpp_model_for_language()
+
+ for model_name in variants:
+ info = WHISPERCPP_MODEL_INFO[model_name]
+ is_downloaded = is_whispercpp_model_downloaded(model_name)
+ status = "✓" if is_downloaded else "↓"
+ star = " ★" if model_name == recommended_model else ""
+ display_text = (
+ f"{_model_specialization_display_name(model_name)} "
+ f"({_format_size(info['size_mb'])}) {status}{star}"
+ )
+ self.model_variant_combo.append(model_name, display_text)
+
+ model_to_set = selected_model if selected_model in variants else None
+ if not model_to_set and recommended_model in variants:
+ model_to_set = recommended_model
+ if not model_to_set:
+ model_to_set = self._get_default_whispercpp_variant_for_size(model_size.lower())
+ if not model_to_set and variants:
+ model_to_set = variants[0]
+
+ self._set_combo_active_id_or_first(self.model_variant_combo, model_to_set)
+ self._update_model_picker_tooltips()
+
def _on_engine_changed(self, widget):
"""Handle changes in the selected engine."""
engine_text = self.engine_combo.get_active_text()
@@ -2239,8 +2569,7 @@ class SettingsDialog(Gtk.Dialog):
self.language = "auto"
self._populate_model_options()
- self._populate_language_options()
- self.language_combo.set_active_id(self.language)
+ self._sync_language_options_for_selected_model(self.language)
self._update_engine_specific_ui()
self._update_model_info()
self._update_voice_commands_for_engine()
@@ -2261,6 +2590,25 @@ class SettingsDialog(Gtk.Dialog):
if self._populating_models:
return
+ if self._get_selected_engine() == "whisper_cpp":
+ model_size = self.model_combo.get_active_id()
+ if model_size:
+ self._populating_models = True
+ try:
+ self._populate_whispercpp_variant_options(model_size)
+ finally:
+ self._populating_models = False
+ self._sync_language_options_for_selected_model()
+
+ self._update_model_info()
+ self._auto_apply_settings()
+
+ def _on_model_variant_changed(self, widget):
+ """Handle changes in the selected whisper.cpp specialization."""
+ if self._populating_models:
+ return
+
+ self._sync_language_options_for_selected_model()
self._update_model_info()
self._auto_apply_settings()
@@ -2297,6 +2645,7 @@ class SettingsDialog(Gtk.Dialog):
return
engine = _engine_from_display(engine)
+ english_only_whispercpp = self._is_selected_whispercpp_model_english_only()
for lang_code, lang_info in SUPPORTED_LANGUAGES.items():
display_text = lang_info["name"]
@@ -2308,6 +2657,8 @@ class SettingsDialog(Gtk.Dialog):
is_downloaded = _is_vosk_model_downloaded("small", lang_code)
display_text += " ✓" if is_downloaded else " ↓"
elif engine in ["whisper", "whisper_cpp", "remote_api"]:
+ if english_only_whispercpp and lang_info.get("whisper") != "en":
+ continue
# Both Whisper and whisper.cpp support auto-detect
if lang_code == "auto":
display_text += " ⚠"
@@ -2316,6 +2667,26 @@ class SettingsDialog(Gtk.Dialog):
self.language_combo.append(lang_code, display_text)
+ def _update_language_warning(self):
+ """Update language help text for the selected engine/model/language."""
+ lang_code = self.language_combo.get_active_id()
+ lang_info = SUPPORTED_LANGUAGES.get(lang_code, {})
+
+ if self._is_selected_whispercpp_model_english_only():
+ self.language_warning.set_markup(
+ "⚠ English-only model selected. "
+ "Language choices are limited to English."
+ )
+ self.language_warning.show()
+ elif lang_info.get("warning"):
+ self.language_warning.set_markup(
+ f"⚠ {lang_info['warning']}"
+ )
+ self.language_warning.show()
+ else:
+ self.language_warning.set_markup("")
+ self.language_warning.hide()
+
def _on_language_changed(self, widget):
"""Handle language selection change."""
if self._processing_language_change:
@@ -2331,20 +2702,9 @@ class SettingsDialog(Gtk.Dialog):
self._processing_language_change = True
try:
- engine = _engine_from_display(engine)
- lang_info = SUPPORTED_LANGUAGES.get(lang_code, {})
-
- if lang_info.get("warning"):
- self.language_warning.set_markup(
- f"⚠ {lang_info['warning']}"
- )
- self.language_warning.show()
- else:
- self.language_warning.set_markup("")
- self.language_warning.hide()
-
self.language = lang_code
self._populate_model_options()
+ self._update_language_warning()
self._auto_apply_settings()
finally:
self._processing_language_change = False
@@ -2357,17 +2717,24 @@ class SettingsDialog(Gtk.Dialog):
if is_remote:
self.model_row.hide()
+ self.model_variant_row.hide()
self.model_info_card.hide()
self.legend_box.hide()
self.remote_server_group.show_all()
self.remote_status_label.show()
else:
self.model_row.show_all()
+ if engine == "whisper_cpp":
+ self.model_variant_row.show_all()
+ else:
+ self.model_variant_row.hide()
self.legend_box.show_all()
self.remote_server_group.hide()
self.remote_status_label.hide()
self._update_model_info()
+ self._update_language_warning()
+ self._update_model_picker_tooltips()
self._update_advanced_tab_sensitivity()
def _update_advanced_tab_sensitivity(self):
@@ -2405,12 +2772,14 @@ class SettingsDialog(Gtk.Dialog):
engine = _engine_from_display(engine_text)
- model_id = self.model_combo.get_active_id()
- if not model_id:
- self.model_info_card.hide()
- return
-
- model_name = model_id.lower()
+ if engine == "whisper_cpp":
+ model_name = self._get_selected_whispercpp_model()
+ else:
+ model_id = self.model_combo.get_active_id()
+ if not model_id:
+ self.model_info_card.hide()
+ return
+ model_name = model_id.lower()
if engine == "whisper":
if model_name not in WHISPER_MODEL_INFO:
@@ -2426,7 +2795,7 @@ class SettingsDialog(Gtk.Dialog):
return
info = WHISPERCPP_MODEL_INFO[model_name]
is_downloaded = is_whispercpp_model_downloaded(model_name)
- recommended, reason = get_recommended_whispercpp_model()
+ recommended, reason = self._get_recommended_whispercpp_model_for_language()
backend, backend_info = detect_compute_backend()
extra_info = (
f"Parameters: {info['params']} • Backend: {get_backend_display_name(backend)}"
@@ -2443,8 +2812,11 @@ class SettingsDialog(Gtk.Dialog):
self.model_info_card.hide()
return
+ model_display_name = _model_display_name(model_name)
+ recommended_display_name = _model_display_name(recommended)
+
# Update title
- self.model_info_title.set_markup(f"{model_name.capitalize()}: {info['desc']}")
+ self.model_info_title.set_markup(f"{model_display_name}: {info['desc']}")
# Update subtitle with status
if is_downloaded:
@@ -2460,9 +2832,10 @@ class SettingsDialog(Gtk.Dialog):
)
else:
self.model_recommendation.set_markup(
- f"Tip: {recommended.capitalize()} is recommended for your system ({reason})"
+ f"Tip: {recommended_display_name} is recommended for your system ({reason})"
)
+ self._update_model_picker_tooltips()
self.model_info_card.show_all()
def _auto_apply_settings(self):
@@ -2585,8 +2958,11 @@ class SettingsDialog(Gtk.Dialog):
language_id = self.language_combo.get_active_id()
engine = _engine_from_display(engine_text) if engine_text else "vosk"
- model_size = model_id.lower() if model_id else "small"
- language = language_id if language_id else "auto"
+ if engine == "whisper_cpp":
+ model_size = self._get_selected_whispercpp_model()
+ else:
+ model_size = model_id.lower() if model_id else "small"
+ language = language_id if language_id else self._default_language_for_engine(engine)
vad = int(self.vad_spin.get_value())
silence = self.silence_spin.get_value()
@@ -2770,6 +3146,9 @@ For now, the engine has been reverted to VOSK."""
if engine == "whisper" and not _is_whisper_model_downloaded(model_name):
needs_download = True
model_info = WHISPER_MODEL_INFO.get(model_name, {"size_mb": 500})
+ elif engine == "whisper_cpp" and not is_whispercpp_model_downloaded(model_name):
+ needs_download = True
+ model_info = WHISPERCPP_MODEL_INFO.get(model_name, {"size_mb": 39})
elif engine == "vosk" and not _is_vosk_model_downloaded(model_name, self.language):
needs_download = True
model_info = VOSK_MODEL_INFO.get(model_name, {"size_mb": 50})
diff --git a/src/vocalinux/utils/whispercpp_model_info.py b/src/vocalinux/utils/whispercpp_model_info.py
index dcfe027..6617353 100644
--- a/src/vocalinux/utils/whispercpp_model_info.py
+++ b/src/vocalinux/utils/whispercpp_model_info.py
@@ -15,42 +15,104 @@ logger = logging.getLogger(__name__)
# Whisper.cpp model information
# Models are downloaded from Hugging Face (ggml format)
+_WHISPERCPP_REPO_URL = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main"
+
+
+def _model_url(model_name: str) -> str:
+ """Build the Hugging Face URL for a ggml whisper.cpp model."""
+ file_model_name = "large-v3" if model_name == "large" else model_name
+ return f"{_WHISPERCPP_REPO_URL}/ggml-{file_model_name}.bin"
+
+
+_WHISPERCPP_MODEL_SPECS = [
+ ("tiny", 74, "39M", "Fastest, lowest accuracy"),
+ ("tiny.en", 74, "39M", "English-only tiny model"),
+ ("tiny-q5_1", 15, "39M", "Quantized tiny model, lowest memory"),
+ ("tiny.en-q5_1", 15, "39M", "Quantized English-only tiny model"),
+ ("tiny-q8_0", 32, "39M", "Q8 quantized tiny model"),
+ ("base", 141, "74M", "Fast, good for basic use"),
+ ("base.en", 141, "74M", "English-only base model"),
+ ("base-q5_1", 60, "74M", "Quantized base model, lower memory"),
+ ("base.en-q5_1", 60, "74M", "Quantized English-only base model"),
+ ("base-q8_0", 82, "74M", "Q8 quantized base model"),
+ ("small", 465, "244M", "Balanced speed/accuracy"),
+ ("small.en", 465, "244M", "English-only small model"),
+ ("small-q5_1", 163, "244M", "Quantized small model, lower memory"),
+ ("small.en-q5_1", 163, "244M", "Quantized English-only small model"),
+ ("small-q8_0", 190, "244M", "Q8 quantized small model"),
+ ("medium", 1463, "769M", "High accuracy, slower"),
+ ("medium.en", 1463, "769M", "English-only medium model"),
+ ("medium-q5_0", 568, "769M", "Quantized medium model, lower memory"),
+ ("medium.en-q5_0", 568, "769M", "Quantized English-only medium model"),
+ ("medium-q8_0", 823, "769M", "Q8 quantized medium model"),
+ ("large-v1", 2952, "1550M", "Legacy large v1 model"),
+ ("large-v2", 2952, "1550M", "Legacy large v2 model"),
+ ("large-v2-q5_0", 1170, "1550M", "Quantized large v2 model, lower memory"),
+ ("large-v2-q8_0", 1660, "1550M", "Q8 quantized large v2 model"),
+ ("large", 2952, "1550M", "Highest accuracy, maps to large v3"),
+ ("large-v3-q5_0", 1170, "1550M", "Quantized large v3 model, lower memory"),
+ ("large-v3-turbo", 1620, "809M", "High accuracy, lower memory than large"),
+ ("large-v3-turbo-q5_0", 574, "809M", "Quantized large v3 Turbo model"),
+ ("large-v3-turbo-q8_0", 874, "809M", "Q8 quantized large v3 Turbo model"),
+]
+
WHISPERCPP_MODEL_INFO = {
- "tiny": {
- "size_mb": 74,
- "params": "39M",
- "desc": "Fastest, lowest accuracy",
- "url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin",
- },
- "base": {
- "size_mb": 141,
- "params": "74M",
- "desc": "Fast, good for basic use",
- "url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-base.bin",
- },
- "small": {
- "size_mb": 465,
- "params": "244M",
- "desc": "Balanced speed/accuracy",
- "url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small.bin",
- },
- "medium": {
- "size_mb": 1463,
- "params": "769M",
- "desc": "High accuracy, slower",
- "url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-medium.bin",
- },
- "large": {
- "size_mb": 2952,
- "params": "1550M",
- "desc": "Highest accuracy, slowest",
- "url": "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3.bin",
- },
+ spec[0]: {
+ "size_mb": spec[1],
+ "params": spec[2],
+ "desc": spec[3],
+ "url": _model_url(spec[0]),
+ }
+ for spec in _WHISPERCPP_MODEL_SPECS
}
# Available models list
AVAILABLE_MODELS = list(WHISPERCPP_MODEL_INFO.keys())
+MODEL_SIZES = ["tiny", "base", "small", "medium", "large"]
+
+MODEL_VARIANTS_BY_SIZE = {
+ "tiny": ["tiny", "tiny.en", "tiny-q5_1", "tiny.en-q5_1", "tiny-q8_0"],
+ "base": ["base", "base.en", "base-q5_1", "base.en-q5_1", "base-q8_0"],
+ "small": [
+ "small",
+ "small.en",
+ "small-q5_1",
+ "small.en-q5_1",
+ "small-q8_0",
+ ],
+ "medium": ["medium", "medium.en", "medium-q5_0", "medium.en-q5_0", "medium-q8_0"],
+ "large": [
+ "large",
+ "large-v3-q5_0",
+ "large-v3-turbo",
+ "large-v3-turbo-q5_0",
+ "large-v3-turbo-q8_0",
+ "large-v2",
+ "large-v2-q5_0",
+ "large-v2-q8_0",
+ "large-v1",
+ ],
+}
+
+
+def get_model_size(model_name: str) -> str:
+ """Return the top-level whisper.cpp size bucket for a model variant."""
+ model_name = model_name.lower()
+ if model_name.startswith("large"):
+ return "large"
+ return model_name.split(".", 1)[0].split("-", 1)[0]
+
+
+def get_model_variants(model_size: str) -> list[str]:
+ """Return available whisper.cpp variants for a size bucket."""
+ return list(MODEL_VARIANTS_BY_SIZE.get(model_size.lower(), []))
+
+
+def is_english_only_model(model_name: str) -> bool:
+ """Return whether a whisper.cpp model variant is English-only."""
+ return ".en" in model_name.lower()
+
# Compute backend types
class ComputeBackend:
@@ -244,7 +306,7 @@ def get_model_path(model_name: str) -> str:
Get the path where a model should be stored.
Args:
- model_name: Name of the model (tiny, base, small, medium, large)
+ model_name: Name of the model (for example tiny, base, small, medium, large)
Returns:
Path to the model file
@@ -252,11 +314,11 @@ def get_model_path(model_name: str) -> str:
models_dir = os.path.expanduser("~/.local/share/vocalinux/models/whispercpp")
os.makedirs(models_dir, exist_ok=True)
- if model_name == "large":
- # Large model uses v3 variant
- return os.path.join(models_dir, "ggml-large-v3.bin")
- else:
- return os.path.join(models_dir, f"ggml-{model_name}.bin")
+ model_info = WHISPERCPP_MODEL_INFO.get(model_name)
+ if model_info and model_info.get("url"):
+ return os.path.join(models_dir, os.path.basename(model_info["url"]))
+
+ return os.path.join(models_dir, f"ggml-{model_name}.bin")
def is_model_downloaded(model_name: str) -> bool:
diff --git a/tests/test_main.py b/tests/test_main.py
index 50587d0..53e9d95 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -57,8 +57,8 @@ class TestMainModule(unittest.TestCase):
self.assertTrue(args.wayland)
self.assertTrue(args.start_minimized)
- def test_parse_arguments_model_choices(self):
- """Test that model only accepts valid choices."""
+ def test_parse_arguments_model_values(self):
+ """Test model parsing for base and exact whisper.cpp model IDs."""
with patch("sys.argv", ["vocalinux", "--model", "small"]):
args = parse_arguments()
self.assertEqual(args.model, "small")
@@ -71,6 +71,14 @@ class TestMainModule(unittest.TestCase):
args = parse_arguments()
self.assertEqual(args.model, "large")
+ with patch("sys.argv", ["vocalinux", "--model", "medium.en-q5_0"]):
+ args = parse_arguments()
+ self.assertEqual(args.model, "medium.en-q5_0")
+
+ with patch("sys.argv", ["vocalinux", "--model", "large-v3-turbo"]):
+ args = parse_arguments()
+ self.assertEqual(args.model, "large-v3-turbo")
+
def test_parse_arguments_engine_choices(self):
"""Test that engine only accepts valid choices."""
with patch("sys.argv", ["vocalinux", "--engine", "vosk"]):
diff --git a/tests/test_settings_dialog.py b/tests/test_settings_dialog.py
index 17e7e1d..3f81933 100644
--- a/tests/test_settings_dialog.py
+++ b/tests/test_settings_dialog.py
@@ -586,6 +586,110 @@ class TestSettingsDialogHelperFunctions(unittest.TestCase):
self.assertTrue(callable(_get_recommended_vosk_model))
+ def test_whispercpp_settings_use_size_buckets(self):
+ """Test that whisper.cpp settings split size from specialization."""
+ from vocalinux.ui.settings_dialog import ENGINE_MODELS, WHISPERCPP_MODEL_INFO
+
+ self.assertEqual(
+ ENGINE_MODELS["whisper_cpp"],
+ ["tiny", "base", "small", "medium", "large"],
+ )
+ self.assertIn("large-v3-turbo", WHISPERCPP_MODEL_INFO)
+ self.assertIn("large-v3-turbo-q5_0", WHISPERCPP_MODEL_INFO)
+ self.assertNotIn("small.en-tdrz", WHISPERCPP_MODEL_INFO)
+
+ def test_model_display_name_large_v3_turbo(self):
+ """Test display labels for whisper.cpp model variants."""
+ from vocalinux.ui.settings_dialog import _model_display_name
+
+ self.assertEqual(_model_display_name("large"), "Large v3")
+ self.assertEqual(_model_display_name("large-v3-turbo"), "Large v3 Turbo")
+ self.assertEqual(_model_display_name("large-v3-turbo-q5_0"), "Large v3 Turbo Q5_0")
+ self.assertEqual(_model_display_name("tiny.en-q5_1"), "Tiny EN Q5_1")
+
+ def test_model_specialization_display_name(self):
+ """Test concise specialization labels for the second whisper.cpp dropdown."""
+ from vocalinux.ui.settings_dialog import _model_specialization_display_name
+
+ self.assertEqual(_model_specialization_display_name("medium"), "Standard multilingual")
+ self.assertEqual(_model_specialization_display_name("medium.en"), "English-only")
+ self.assertEqual(
+ _model_specialization_display_name("medium.en-q5_0"),
+ "English-only Q5_0",
+ )
+ self.assertEqual(_model_specialization_display_name("large"), "Standard v3")
+ self.assertEqual(_model_specialization_display_name("large-v3-turbo"), "Turbo")
+ self.assertEqual(_model_specialization_display_name("large-v2-q5_0"), "v2 Q5_0")
+ self.assertEqual(_model_specialization_display_name("large-v3-q5_0"), "v3 Q5_0")
+
+ def test_model_picker_tooltips_explain_when_to_choose_variants(self):
+ """Test hover guidance for model picker choices."""
+ from vocalinux.ui.settings_dialog import (
+ LANGUAGE_TOOLTIP,
+ MODEL_SIZE_TOOLTIP,
+ MODEL_SPECIALIZATION_TOOLTIP,
+ _model_specialization_tooltip,
+ )
+
+ self.assertIn("largest model", MODEL_SIZE_TOOLTIP)
+ self.assertIn("Standard multilingual", MODEL_SPECIALIZATION_TOOLTIP)
+ self.assertIn("English-only", LANGUAGE_TOOLTIP)
+ self.assertIn("only in English", _model_specialization_tooltip("medium.en"))
+ self.assertIn("lower-memory systems", _model_specialization_tooltip("medium-q5_0"))
+ self.assertIn("Turbo", _model_specialization_tooltip("large-v3-turbo"))
+ self.assertIn("legacy large model", _model_specialization_tooltip("large-v2"))
+ self.assertIn("most users", _model_specialization_tooltip("small"))
+
+ def test_model_picker_rows_have_hover_tooltips(self):
+ """Test that model picker rows expose the guidance as hover text."""
+ import os
+
+ source_path = os.path.join(
+ os.path.dirname(__file__),
+ "..",
+ "src",
+ "vocalinux",
+ "ui",
+ "settings_dialog.py",
+ )
+ with open(source_path, "r") as f:
+ source_code = f.read()
+
+ self.assertIn("self.model_combo.set_tooltip_text(MODEL_SIZE_TOOLTIP)", source_code)
+ self.assertIn("self.model_row.set_tooltip_text(MODEL_SIZE_TOOLTIP)", source_code)
+ self.assertIn(
+ "self.model_variant_row.set_tooltip_text(specialization_tooltip)",
+ source_code,
+ )
+ self.assertIn("self.language_row.set_tooltip_text(LANGUAGE_TOOLTIP)", source_code)
+
+ def test_whispercpp_recommendation_uses_language_for_specialization(self):
+ """Test that English language nudges recommendations to .en variants."""
+ from vocalinux.ui.settings_dialog import (
+ _default_whispercpp_variant_for_size,
+ _recommended_whispercpp_variant_for_language,
+ )
+
+ self.assertEqual(
+ _recommended_whispercpp_variant_for_language(
+ "medium",
+ "mock hardware reason",
+ "en-us",
+ ),
+ ("medium.en", "mock hardware reason; English language selected"),
+ )
+ self.assertEqual(_default_whispercpp_variant_for_size("medium", "en-us"), "medium.en")
+
+ self.assertEqual(
+ _recommended_whispercpp_variant_for_language(
+ "medium",
+ "mock hardware reason",
+ "auto",
+ ),
+ ("medium", "mock hardware reason"),
+ )
+ self.assertEqual(_default_whispercpp_variant_for_size("medium", "auto"), "medium")
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/test_whispercpp_model_info_ext.py b/tests/test_whispercpp_model_info_ext.py
index 307e8e7..fd9406e 100644
--- a/tests/test_whispercpp_model_info_ext.py
+++ b/tests/test_whispercpp_model_info_ext.py
@@ -74,6 +74,12 @@ class TestModelInfo(unittest.TestCase):
self.assertIn("large", WHISPERCPP_MODEL_INFO)
+ def test_whispercpp_model_info_contains_large_v3_turbo(self):
+ """Test that WHISPERCPP_MODEL_INFO contains large-v3-turbo model."""
+ from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
+
+ self.assertIn("large-v3-turbo", WHISPERCPP_MODEL_INFO)
+
def test_model_info_has_size_mb(self):
"""Test that each model has size_mb field."""
from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
@@ -124,6 +130,80 @@ class TestModelInfo(unittest.TestCase):
self.assertEqual(set(AVAILABLE_MODELS), set(WHISPERCPP_MODEL_INFO.keys()))
+ def test_available_models_tracks_upstream_dictation_models(self):
+ """Test that AVAILABLE_MODELS covers upstream dictation models."""
+ from vocalinux.utils.whispercpp_model_info import AVAILABLE_MODELS
+
+ expected_models = [
+ "tiny",
+ "tiny.en",
+ "tiny-q5_1",
+ "tiny.en-q5_1",
+ "tiny-q8_0",
+ "base",
+ "base.en",
+ "base-q5_1",
+ "base.en-q5_1",
+ "base-q8_0",
+ "small",
+ "small.en",
+ "small-q5_1",
+ "small.en-q5_1",
+ "small-q8_0",
+ "medium",
+ "medium.en",
+ "medium-q5_0",
+ "medium.en-q5_0",
+ "medium-q8_0",
+ "large-v1",
+ "large-v2",
+ "large-v2-q5_0",
+ "large-v2-q8_0",
+ "large", # Vocalinux alias for upstream large-v3
+ "large-v3-q5_0",
+ "large-v3-turbo",
+ "large-v3-turbo-q5_0",
+ "large-v3-turbo-q8_0",
+ ]
+
+ self.assertEqual(AVAILABLE_MODELS, expected_models)
+
+ def test_model_sizes_group_whispercpp_variants_for_ui(self):
+ """Test that whisper.cpp models are grouped into size buckets."""
+ from vocalinux.utils.whispercpp_model_info import (
+ MODEL_SIZES,
+ MODEL_VARIANTS_BY_SIZE,
+ WHISPERCPP_MODEL_INFO,
+ )
+
+ self.assertEqual(MODEL_SIZES, ["tiny", "base", "small", "medium", "large"])
+ self.assertEqual(MODEL_VARIANTS_BY_SIZE["medium"][0], "medium")
+ self.assertIn("medium.en-q5_0", MODEL_VARIANTS_BY_SIZE["medium"])
+ self.assertIn("large-v3-turbo-q8_0", MODEL_VARIANTS_BY_SIZE["large"])
+ self.assertNotIn("small.en-tdrz", MODEL_VARIANTS_BY_SIZE["small"])
+
+ grouped_models = {
+ model_name for variants in MODEL_VARIANTS_BY_SIZE.values() for model_name in variants
+ }
+ self.assertEqual(grouped_models, set(WHISPERCPP_MODEL_INFO.keys()))
+ self.assertFalse(
+ any(model_name.endswith("-tdrz") for model_name in grouped_models),
+ "TinyDiarize needs diarization-specific runtime support, not the base dictation picker",
+ )
+
+ def test_model_variant_helpers_identify_size_and_english_only_variants(self):
+ """Test helpers that drive the split whisper.cpp model selectors."""
+ from vocalinux.utils.whispercpp_model_info import (
+ get_model_size,
+ get_model_variants,
+ is_english_only_model,
+ )
+
+ self.assertEqual(get_model_size("medium.en-q5_0"), "medium")
+ self.assertEqual(get_model_size("large-v3-turbo-q8_0"), "large")
+ self.assertTrue(is_english_only_model("tiny.en-q5_1"))
+ self.assertFalse(is_english_only_model("large-v3-turbo"))
+
class TestDetectVulkanSupport(unittest.TestCase):
"""Test cases for detect_vulkan_support function."""
@@ -559,6 +639,26 @@ class TestGetModelPath(unittest.TestCase):
path = get_model_path("large")
self.assertIn("ggml-large-v3.bin", path)
+ def test_large_model_url_uses_v3(self):
+ """Test large model metadata points to upstream large-v3."""
+ from vocalinux.utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO
+
+ self.assertIn("ggml-large-v3.bin", WHISPERCPP_MODEL_INFO["large"]["url"])
+
+ def test_get_model_path_large_v3_turbo(self):
+ """Test get_model_path for large-v3-turbo model."""
+ from vocalinux.utils.whispercpp_model_info import get_model_path
+
+ path = get_model_path("large-v3-turbo")
+ self.assertIn("ggml-large-v3-turbo.bin", path)
+
+ def test_get_model_path_quantized_model(self):
+ """Test get_model_path for quantized model."""
+ from vocalinux.utils.whispercpp_model_info import get_model_path
+
+ path = get_model_path("large-v3-turbo-q5_0")
+ self.assertIn("ggml-large-v3-turbo-q5_0.bin", path)
+
class TestIsModelDownloaded(unittest.TestCase):
"""Test cases for is_model_downloaded function."""