feat(whisper): add guided whisper.cpp model variants (#465)

* feat(whisper): refine whisper.cpp model selection

* refactor(whisper): exclude diarization model from picker

* docs(whisper): document guided model variants

* fix(ui): cap settings dialog height at 600px to prevent excessive height on high-res displays
This commit is contained in:
Jatin K Malik
2026-06-11 22:22:43 -07:00
committed by GitHub
parent bcd5313a1e
commit e0ac5e409c
9 changed files with 789 additions and 100 deletions
+7 -2
View File
@@ -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
+21 -6
View File
@@ -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
+15 -1
View File
@@ -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:
+4 -2
View File
@@ -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",
+430 -51
View File
@@ -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(
"<span foreground='#e5a50a'>⚠ English-only model selected. "
"Language choices are limited to English.</span>"
)
self.language_warning.show()
elif lang_info.get("warning"):
self.language_warning.set_markup(
f"<span foreground='#e5a50a'>⚠ {lang_info['warning']}</span>"
)
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"<span foreground='#e5a50a'>⚠ {lang_info['warning']}</span>"
)
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"<b>{model_name.capitalize()}</b>: {info['desc']}")
self.model_info_title.set_markup(f"<b>{model_display_name}</b>: {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: <b>{recommended.capitalize()}</b> is recommended for your system ({reason})"
f"Tip: <b>{recommended_display_name}</b> 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})
+98 -36
View File
@@ -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:
+10 -2
View File
@@ -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"]):
+104
View File
@@ -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()
+100
View File
@@ -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."""