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:
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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
@@ -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"]):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user