fix(settings): speech model follow-ups from QA (#836)
* fix(settings): speech model follow-ups from #834 Keep the Unused downloads expander in sync with search, recommend Faster Whisper .en weights for English, parse nvidia-smi MiB VRAM, stop simple mode from clobbering Faster Whisper/Parakeet, and ceil RAM so ~7.4 GiB counts as 8. * fix(settings): keep FW .en picker ids and test unused search Represent Faster Whisper English catalog variants in the Settings size combo so tray-saved small.en is not rejected and overwritten. Add behavioral unused-downloads search coverage for match/nonmatch/clear.
This commit is contained in:
@@ -18,6 +18,7 @@ UX Design Notes:
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
@@ -175,11 +176,15 @@ ENGINE_MODELS = {
|
||||
], # Parakeet TDT 0.6B int8 bundles
|
||||
"faster_whisper": [
|
||||
"tiny",
|
||||
"tiny.en",
|
||||
"base",
|
||||
"base.en",
|
||||
"small",
|
||||
"small.en",
|
||||
"medium",
|
||||
"medium.en",
|
||||
"large-v3",
|
||||
], # faster-whisper models mirror OpenAI Whisper sizes
|
||||
], # catalog sizes plus English-only .en variants (no large-v3.en)
|
||||
"remote_api": [], # Remote API does not need local models
|
||||
}
|
||||
|
||||
@@ -330,6 +335,20 @@ def _recommended_whispercpp_variant_for_language(
|
||||
return recommended_model, reason
|
||||
|
||||
|
||||
def _recommended_faster_whisper_variant_for_language(
|
||||
recommended_model: str,
|
||||
reason: str,
|
||||
language_id: str,
|
||||
) -> tuple[str, str]:
|
||||
"""Adjust a Faster Whisper hardware recommendation to the selected language."""
|
||||
english_variant = (
|
||||
recommended_model if recommended_model.endswith(".en") else f"{recommended_model}.en"
|
||||
)
|
||||
if _language_is_english(language_id) and english_variant in FASTER_WHISPER_MODEL_INFO:
|
||||
return english_variant, reason
|
||||
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."""
|
||||
return default_variant_for_size(model_size, _language_is_english(language_id))
|
||||
@@ -1461,7 +1480,7 @@ def _get_recommended_whisper_model() -> tuple:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
ram_gb = psutil.virtual_memory().total // (1024**3)
|
||||
ram_gb = math.ceil(psutil.virtual_memory().total / (1024**3))
|
||||
|
||||
# Check for CUDA - suppress warnings during detection
|
||||
has_cuda = False
|
||||
@@ -1521,7 +1540,7 @@ def _get_recommended_vosk_model() -> tuple:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
ram_gb = psutil.virtual_memory().total // (1024**3)
|
||||
ram_gb = math.ceil(psutil.virtual_memory().total / (1024**3))
|
||||
|
||||
# VOSK models are CPU-based, so we recommend based on RAM and disk space
|
||||
if ram_gb >= 4:
|
||||
@@ -1567,7 +1586,10 @@ def recommended_model_for_engine(
|
||||
reason = parakeet.RECOMMENDED_REASON
|
||||
size_mb = parakeet.PARAKEET_MODEL_INFO.get(model_id, {}).get("size_mb", 0)
|
||||
elif engine == "faster_whisper":
|
||||
model_id, reason = get_recommended_faster_whisper_model()
|
||||
recommended_model, reason = get_recommended_faster_whisper_model()
|
||||
model_id, reason = _recommended_faster_whisper_variant_for_language(
|
||||
recommended_model, reason, language
|
||||
)
|
||||
size_mb = FASTER_WHISPER_MODEL_INFO.get(model_id, {}).get("size_mb", 0)
|
||||
else:
|
||||
# Remote API transcribes server-side; there is nothing to download.
|
||||
@@ -2624,7 +2646,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
page_names = {page.name for page in self._pages}
|
||||
self._search_previous_page = visible_page if visible_page in page_names else "dictation"
|
||||
|
||||
baseline = {"rows": {}, "groups": {}, "extras": {}}
|
||||
baseline = {"rows": {}, "groups": {}, "extras": {}, "islands": {}}
|
||||
for page in self._pages:
|
||||
for group in page.groups:
|
||||
visible = group.get_visible()
|
||||
@@ -2640,6 +2662,10 @@ class SettingsDialog(Gtk.Dialog):
|
||||
baseline["rows"][row] = row.get_visible()
|
||||
for extra in page.extras:
|
||||
baseline["extras"][extra] = extra.get_visible()
|
||||
# Nested unused_models_group is the searchable unit; the expander card
|
||||
# that wraps it is not a PreferencesGroup, so record it separately.
|
||||
if self.unused_island is not None:
|
||||
baseline["islands"][self.unused_island] = self.unused_island.get_visible()
|
||||
self._search_baseline = baseline
|
||||
|
||||
def _restore_search_baseline(self):
|
||||
@@ -2725,6 +2751,12 @@ class SettingsDialog(Gtk.Dialog):
|
||||
page.match_count_label.hide()
|
||||
page.sidebar_row.set_sensitive(False)
|
||||
|
||||
# collect_children finds unused_models_group inside unused_island, so the
|
||||
# expander card is not an extra. Hide the header when the nested group
|
||||
# has no search hits; restore puts the island back from the snapshot.
|
||||
if self.unused_island is not None and hasattr(self, "unused_models_group"):
|
||||
self.unused_island.set_visible(self.unused_models_group.get_visible())
|
||||
|
||||
if first_match_page is not None:
|
||||
self.sidebar_listbox.select_row(first_match_page.sidebar_row)
|
||||
self.settings_stack.set_visible_child_name(first_match_page.name)
|
||||
@@ -3575,6 +3607,10 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.unused_models_group = PreferencesGroup(
|
||||
keywords=("delete", "remove", "unused", "disk", "storage", "downloaded"),
|
||||
)
|
||||
# Title lives on the expander card. Copy it onto the nested group so
|
||||
# search matches "Unused downloads" without drawing a second header.
|
||||
self.unused_models_group.title = "Unused downloads"
|
||||
self.unused_models_group.description = "Downloaded, but not the one in use"
|
||||
# The island is the card; this group only holds rows for search/delete.
|
||||
self.unused_models_group.get_style_context().remove_class("preferences-group")
|
||||
|
||||
@@ -5703,6 +5739,11 @@ class SettingsDialog(Gtk.Dialog):
|
||||
recommended_model = parakeet.RECOMMENDED_MODEL
|
||||
elif engine == "faster_whisper":
|
||||
recommended_model, _ = get_recommended_faster_whisper_model()
|
||||
recommended_model, _ = _recommended_faster_whisper_variant_for_language(
|
||||
recommended_model,
|
||||
"",
|
||||
self.language_combo.get_active_id() or self.language,
|
||||
)
|
||||
else:
|
||||
recommended_model, _ = _get_recommended_vosk_model()
|
||||
|
||||
@@ -5737,18 +5778,26 @@ class SettingsDialog(Gtk.Dialog):
|
||||
if smallest_model is None:
|
||||
smallest_model = size
|
||||
|
||||
self.model_combo.append(size.capitalize(), display_text)
|
||||
# whisper.cpp already appends the catalog id; Faster Whisper
|
||||
# ids include dots (`small.en`) which capitalize() mangles.
|
||||
combo_id = size if engine == "faster_whisper" else size.capitalize()
|
||||
self.model_combo.append(combo_id, display_text)
|
||||
|
||||
# Determine which model to select
|
||||
saved_model = saved_model_for_engine.lower()
|
||||
valid_models = [m.lower() for m in ENGINE_MODELS.get(engine, [])]
|
||||
|
||||
if saved_model in valid_models:
|
||||
model_to_set = saved_model.capitalize()
|
||||
selected = saved_model
|
||||
elif downloaded_models:
|
||||
model_to_set = downloaded_models[0].capitalize()
|
||||
selected = downloaded_models[0]
|
||||
else:
|
||||
model_to_set = smallest_model.capitalize() if smallest_model else "Small"
|
||||
selected = smallest_model
|
||||
|
||||
if engine == "faster_whisper":
|
||||
model_to_set = selected or "tiny"
|
||||
else:
|
||||
model_to_set = selected.capitalize() if selected else "Small"
|
||||
|
||||
logger.info(f"Setting active model to: {model_to_set}")
|
||||
|
||||
@@ -6459,14 +6508,25 @@ class SettingsDialog(Gtk.Dialog):
|
||||
Simple mode deliberately steers the existing widgets instead of writing the
|
||||
configuration itself, so applying, downloading and the info card keep going
|
||||
through exactly one code path.
|
||||
|
||||
Faster Whisper and Parakeet stay selected: language still follows the
|
||||
simple questions, but the engine is not forced to whisper.cpp. Other
|
||||
engines are steered onto whisper.cpp and get the full size/variant derive.
|
||||
"""
|
||||
language = self._simple_decoding_language()
|
||||
priority = self.simple_priority_combo.get_active_id() or BALANCED
|
||||
|
||||
self.engine_combo.set_active_id("whisper_cpp")
|
||||
engine = self._get_selected_engine()
|
||||
if engine not in ("faster_whisper", "parakeet"):
|
||||
self.engine_combo.set_active_id("whisper_cpp")
|
||||
engine = "whisper_cpp"
|
||||
|
||||
self._set_combo_active_id_or_first(self.language_combo, language)
|
||||
self.language = language
|
||||
|
||||
if engine != "whisper_cpp":
|
||||
return
|
||||
|
||||
recommended, _ = self._get_recommended_whispercpp_model_for_language()
|
||||
size = size_for_priority(get_whispercpp_model_size(recommended), priority)
|
||||
variant = _default_whispercpp_variant_for_size(size, language) or size
|
||||
@@ -6706,6 +6766,11 @@ class SettingsDialog(Gtk.Dialog):
|
||||
info = FASTER_WHISPER_MODEL_INFO[model_name]
|
||||
is_downloaded = is_faster_whisper_model_downloaded(model_name)
|
||||
recommended, reason = get_recommended_faster_whisper_model()
|
||||
recommended, reason = _recommended_faster_whisper_variant_for_language(
|
||||
recommended,
|
||||
reason,
|
||||
self.language_combo.get_active_id() or self.language,
|
||||
)
|
||||
extra_info = f"Parameters: {info['params']}"
|
||||
else:
|
||||
self.model_info_card.hide()
|
||||
|
||||
@@ -7,6 +7,7 @@ loads it, the same way Parakeet and whisper.cpp models are.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
from functools import lru_cache
|
||||
@@ -238,7 +239,7 @@ def get_recommended_model() -> tuple[str, str]:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
ram_gb = int(psutil.virtual_memory().total) // (1024**3)
|
||||
ram_gb = math.ceil(int(psutil.virtual_memory().total) / (1024**3))
|
||||
except Exception:
|
||||
ram_gb = 4
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
@@ -426,6 +427,24 @@ def detect_cpu_info() -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def _parse_cuda_vram_gib(backend_info: str) -> Optional[float]:
|
||||
"""Parse VRAM in GiB from nvidia-smi-style backend info.
|
||||
|
||||
Accepts MiB/MB/GiB/GB (case-insensitive). MiB and MB are converted by
|
||||
dividing by 1024. Returns None when no size can be parsed.
|
||||
"""
|
||||
if not isinstance(backend_info, str) or not backend_info:
|
||||
return None
|
||||
match = re.search(r"(\d+(?:\.\d+)?)\s*(gi?b|mi?b)", backend_info, flags=re.IGNORECASE)
|
||||
if not match:
|
||||
return None
|
||||
amount = float(match.group(1))
|
||||
unit = match.group(2).lower()
|
||||
if unit in {"mb", "mib"}:
|
||||
amount /= 1024.0
|
||||
return amount
|
||||
|
||||
|
||||
def get_recommended_model() -> tuple[str, str]:
|
||||
"""
|
||||
Get the recommended whisper.cpp model based on system configuration.
|
||||
@@ -436,7 +455,7 @@ def get_recommended_model() -> tuple[str, str]:
|
||||
try:
|
||||
import psutil
|
||||
|
||||
ram_gb = psutil.virtual_memory().total // (1024**3)
|
||||
ram_gb = math.ceil(psutil.virtual_memory().total / (1024**3))
|
||||
|
||||
# Detect available compute backends
|
||||
backend, backend_info = detect_compute_backend()
|
||||
@@ -448,19 +467,14 @@ def get_recommended_model() -> tuple[str, str]:
|
||||
else:
|
||||
return "base", f"Vulkan GPU with {ram_gb}GB RAM"
|
||||
elif backend == ComputeBackend.CUDA:
|
||||
# CUDA has more VRAM typically
|
||||
if "GB" in backend_info:
|
||||
try:
|
||||
vram_gb = int(backend_info.split("GB")[0].split("(")[-1].strip())
|
||||
if vram_gb >= 8:
|
||||
return "medium", f"CUDA GPU with {vram_gb}GB VRAM"
|
||||
elif vram_gb >= 4:
|
||||
return "small", f"CUDA GPU with {vram_gb}GB VRAM"
|
||||
else:
|
||||
return "base", f"CUDA GPU with limited VRAM"
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
return "small", f"CUDA GPU detected"
|
||||
vram_gb = _parse_cuda_vram_gib(backend_info or "")
|
||||
if vram_gb is not None:
|
||||
if vram_gb >= 8:
|
||||
return "medium", f"CUDA GPU with {vram_gb:g}GB VRAM"
|
||||
if vram_gb >= 4:
|
||||
return "small", f"CUDA GPU with {vram_gb:g}GB VRAM"
|
||||
return "base", "CUDA GPU with limited VRAM"
|
||||
return "small", "CUDA GPU detected"
|
||||
else:
|
||||
# CPU-only recommendations based on RAM
|
||||
if ram_gb >= 16:
|
||||
|
||||
Reference in New Issue
Block a user