fix(models): let the selected language decide the whisper.cpp variant (#795)
* fix(models): let the selected language decide the whisper.cpp variant whisper_cpp_model_size holds the id that loads the model, and for every size below "large" the multilingual variant id is the bare size name. A stored "medium" therefore could not say whether the user picked the multilingual variant or never picked anything, so the language-aware default never ran and selecting English left the picker on "Standard multilingual" while the panel recommended English-only right underneath. Record the specialization the user picks in its own key. Without that pin the variant is derived from the language; a value that is not a bare size name was written by an older build as a deliberate choice and is adopted as the pin, so existing configurations keep the variant they had. Closes #776 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TKVa2qTLQYMCHffjp97yqT * fix(models): keep language-derived whisper.cpp variants unpinned Auto-apply was writing the derived specialization into both the pin and whisper_cpp_model_size, so a later language change fought a stale pin and a leftover .en id. Only deliberate specializations are pinned now; unpinned saves store the bare size and resolve against the current language. * fix(models): keep multilingual whisper.cpp pins across languages Language auto-apply cleared any pin that matched the new language default, so an explicit multilingual choice made under English was lost when switching away and back. Preserve bare-size pins that still match the selection; keep language-derived and stale .en pins unpinned. Wrap the isort import for CI. --------- Co-authored-by: Kacper Paczos <23406555+kacperpaczos@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
co-authored by
Kacper Paczos
Claude Opus 5
Jatin K Malik
parent
f02a6a7c66
commit
43bf363f6b
@@ -12,6 +12,10 @@ import threading
|
||||
from typing import Any, Optional
|
||||
|
||||
from ..utils.paths import config_dir
|
||||
from ..utils.vosk_model_info import SUPPORTED_LANGUAGES
|
||||
from ..utils.whispercpp_model_info import MODEL_SIZES as WHISPERCPP_MODEL_SIZES
|
||||
from ..utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO, default_variant_for_size
|
||||
from ..utils.whispercpp_model_info import get_model_size as get_whispercpp_model_size
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -69,6 +73,10 @@ DEFAULT_CONFIG = {
|
||||
"vosk_model_size": "small", # Default model for VOSK engine
|
||||
"whisper_model_size": "tiny", # Default model for Whisper engine
|
||||
"whisper_cpp_model_size": "tiny", # Default model for whisper.cpp engine
|
||||
# Variant the user picked explicitly in Settings. Empty means "not pinned":
|
||||
# the variant is then derived from the selected language, because a bare size
|
||||
# name ("medium") is indistinguishable from the multilingual variant id.
|
||||
"whisper_cpp_model_variant": "",
|
||||
"vad_sensitivity": 3, # Voice Activity Detection sensitivity (1-5)
|
||||
"silence_timeout": 2.0, # Seconds of silence before stopping
|
||||
"stop_sound_guard_ms": 200, # Small tail trim to avoid the stop sound without clipping speech
|
||||
@@ -151,6 +159,38 @@ DEFAULT_CONFIG = {
|
||||
}
|
||||
|
||||
|
||||
def resolve_whispercpp_variant(saved_model: str, pinned_variant: str, language_id: str) -> str:
|
||||
"""Resolve the loadable whisper.cpp id for a saved size, pin, and language.
|
||||
|
||||
A pin outranks everything. When unpinned, a plain ``{size}.en`` English-only
|
||||
id is the language-derived default, not a legacy specialization, so a later
|
||||
language change can re-derive. True legacy specializations (quantized, turbo,
|
||||
versioned large, ``{size}.en-q*``) are still honoured.
|
||||
"""
|
||||
pinned = pinned_variant.lower() if isinstance(pinned_variant, str) else ""
|
||||
if pinned in WHISPERCPP_MODEL_INFO:
|
||||
return pinned
|
||||
|
||||
saved = saved_model.lower() if isinstance(saved_model, str) else ""
|
||||
size = saved if saved in WHISPERCPP_MODEL_SIZES else get_whispercpp_model_size(saved or "tiny")
|
||||
if size not in WHISPERCPP_MODEL_SIZES:
|
||||
size = get_whispercpp_model_size("tiny")
|
||||
|
||||
# Honour true leftover specializations, but not a plain English-only id.
|
||||
if (
|
||||
saved in WHISPERCPP_MODEL_INFO
|
||||
and saved not in WHISPERCPP_MODEL_SIZES
|
||||
and saved != f"{size}.en"
|
||||
):
|
||||
return saved
|
||||
|
||||
language_is_english = SUPPORTED_LANGUAGES.get(language_id, {}).get("whisper") == "en"
|
||||
derived = default_variant_for_size(size, language_is_english)
|
||||
if derived in WHISPERCPP_MODEL_INFO:
|
||||
return derived
|
||||
return saved if saved in WHISPERCPP_MODEL_INFO else "tiny"
|
||||
|
||||
|
||||
class ConfigManager:
|
||||
"""
|
||||
Manager for user configuration settings.
|
||||
@@ -432,23 +472,33 @@ class ConfigManager:
|
||||
return self.config
|
||||
|
||||
def get_model_size_for_engine(self, engine: str) -> str:
|
||||
"""Get the saved model size for a specific engine.
|
||||
"""Return the model id the engine should load for ``engine``.
|
||||
|
||||
Args:
|
||||
engine: The engine name ("vosk", "whisper", or "whisper_cpp")
|
||||
|
||||
Returns:
|
||||
The model size for the engine, or the default if not found
|
||||
For whisper.cpp an unpinned size is resolved against the saved
|
||||
language so a stored bare size still loads ``{size}.en`` under English,
|
||||
and a leftover plain ``{size}.en`` can re-derive after a language change.
|
||||
"""
|
||||
sr_config = self.config.get("speech_recognition", {})
|
||||
|
||||
# Try engine-specific model size first
|
||||
engine_key = f"{engine.lower()}_model_size"
|
||||
if engine_key in sr_config:
|
||||
return sr_config[engine_key]
|
||||
saved = sr_config[engine_key]
|
||||
else:
|
||||
# Fall back to generic model_size for backward compatibility
|
||||
saved = sr_config.get("model_size", "small" if engine == "vosk" else "tiny")
|
||||
|
||||
# Fall back to generic model_size for backward compatibility
|
||||
return sr_config.get("model_size", "small" if engine == "vosk" else "tiny")
|
||||
if engine.lower() != "whisper_cpp":
|
||||
return saved
|
||||
|
||||
# Unpinned configs store the bare size; the engine still needs the
|
||||
# language-derived loadable id (and leftover ``{size}.en`` must not
|
||||
# block a later language change).
|
||||
return resolve_whispercpp_variant(
|
||||
saved,
|
||||
self.get_model_variant_for_engine(engine),
|
||||
sr_config.get("language", "auto"),
|
||||
)
|
||||
|
||||
def set_model_size_for_engine(self, engine: str, model_size: str):
|
||||
"""Set the model size for a specific engine.
|
||||
@@ -466,6 +516,24 @@ class ConfigManager:
|
||||
self.config["speech_recognition"]["model_size"] = model_size
|
||||
logger.info(f"Set {engine} model size to: {model_size}")
|
||||
|
||||
def get_model_variant_for_engine(self, engine: str) -> str:
|
||||
"""Return the variant the user pinned for an engine, or "" when unpinned.
|
||||
|
||||
Only whisper.cpp has variants. A pinned value means the user chose that
|
||||
specialization in Settings; an empty value means the variant should be
|
||||
derived from the selected language.
|
||||
"""
|
||||
sr_config = self.config.get("speech_recognition", {})
|
||||
return sr_config.get(f"{engine.lower()}_model_variant", "") or ""
|
||||
|
||||
def set_model_variant_for_engine(self, engine: str, model_variant: str) -> None:
|
||||
"""Pin the variant the user chose for an engine ("" clears the pin)."""
|
||||
if "speech_recognition" not in self.config:
|
||||
self.config["speech_recognition"] = {}
|
||||
|
||||
self.config["speech_recognition"][f"{engine.lower()}_model_variant"] = model_variant
|
||||
logger.info(f"Set {engine} model variant to: {model_variant!r}")
|
||||
|
||||
def is_voice_commands_enabled(self) -> bool:
|
||||
"""Check if voice commands should be enabled.
|
||||
|
||||
@@ -484,7 +552,7 @@ class ConfigManager:
|
||||
|
||||
return enabled
|
||||
|
||||
def update_speech_recognition_settings(self, settings: dict[str, Any]):
|
||||
def update_speech_recognition_settings(self, settings: dict[str, Any]) -> None:
|
||||
"""Update multiple speech recognition settings at once."""
|
||||
if "speech_recognition" not in self.config:
|
||||
self.config["speech_recognition"] = {}
|
||||
@@ -493,10 +561,25 @@ class ConfigManager:
|
||||
if "engine" in settings and "model_size" in settings:
|
||||
engine = settings["engine"]
|
||||
model_size = settings["model_size"]
|
||||
# Unpinned whisper.cpp: persist the bare size so a later language
|
||||
# change can re-derive. Callers still pass the full loadable id as
|
||||
# settings["model_size"] for reconfigure/download.
|
||||
if engine == "whisper_cpp" and not settings.get("model_variant"):
|
||||
model_size = get_whispercpp_model_size(model_size)
|
||||
self.set_model_size_for_engine(engine, model_size)
|
||||
|
||||
# Update all other keys present in the provided settings dict
|
||||
# Empty means unpinned (derive from language). A non-empty value is a
|
||||
# deliberate specialization, including multilingual while English.
|
||||
if "engine" in settings and "model_variant" in settings:
|
||||
self.set_model_variant_for_engine(settings["engine"], settings["model_variant"])
|
||||
|
||||
# Update remaining keys. model_size / model_variant were already applied
|
||||
# through the engine-specific setters above (including bare-size persistence
|
||||
# for an unpinned whisper.cpp selection); writing them again would put the
|
||||
# full derived id back into the generic model_size key.
|
||||
for key, value in settings.items():
|
||||
if key in ("model_size", "model_variant"):
|
||||
continue
|
||||
self.config["speech_recognition"][key] = value
|
||||
logger.info(f"Updated speech recognition settings: {settings}")
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ from ..utils.whisper_model_info import ( # noqa: E402
|
||||
from ..utils.whispercpp_model_info import MODEL_SIZES as WHISPERCPP_MODEL_SIZES
|
||||
from ..utils.whispercpp_model_info import (
|
||||
WHISPERCPP_MODEL_INFO,
|
||||
default_variant_for_size,
|
||||
)
|
||||
from ..utils.whispercpp_model_info import delete_model as delete_whispercpp_model
|
||||
from ..utils.whispercpp_model_info import (
|
||||
@@ -80,6 +81,7 @@ from .config_manager import ( # noqa: E402
|
||||
DEFAULT_SOUND_EFFECT_TONE,
|
||||
PASTE_SHORTCUTS,
|
||||
SOUND_EFFECT_TONES,
|
||||
resolve_whispercpp_variant,
|
||||
)
|
||||
from .keyboard_backends import ( # noqa: E402
|
||||
DEFAULT_SHORTCUT,
|
||||
@@ -259,19 +261,7 @@ def _recommended_whispercpp_variant_for_language(
|
||||
|
||||
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]
|
||||
return default_variant_for_size(model_size, _language_is_english(language_id))
|
||||
|
||||
|
||||
def _is_language_paired_standard_variant(model_name: str) -> bool:
|
||||
@@ -4874,12 +4864,29 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self._populating_models = False
|
||||
self._refresh_unused_downloads()
|
||||
|
||||
def _resolve_saved_whispercpp_variant(self, saved_model_for_engine: str) -> str:
|
||||
"""Resolve which variant the saved configuration actually asks for.
|
||||
|
||||
``whisper_cpp_model_size`` holds the id that loads the model, but for every
|
||||
size below "large" the multilingual variant id *is* the bare size name, so
|
||||
a stored "medium" cannot say whether the user chose the multilingual variant
|
||||
or never chose anything. The explicit pin written by Settings answers that.
|
||||
Without a pin the variant follows the selected language, which is what makes
|
||||
picking English actually select an English-only model.
|
||||
|
||||
A leftover plain ``{size}.en`` English-only id is not treated as a permanent
|
||||
specialization: it is re-derived from the language currently in the combo.
|
||||
"""
|
||||
pinned = self.config_manager.get_model_variant_for_engine("whisper_cpp")
|
||||
language_id = self.language_combo.get_active_id() or self.language
|
||||
return resolve_whispercpp_variant(saved_model_for_engine, pinned, language_id)
|
||||
|
||||
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()
|
||||
saved_model = self._resolve_saved_whispercpp_variant(saved_model_for_engine)
|
||||
if saved_model not in WHISPERCPP_MODEL_INFO:
|
||||
saved_model = (
|
||||
recommended_model if recommended_model in WHISPERCPP_MODEL_INFO else "tiny"
|
||||
@@ -5637,7 +5644,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
except Exception as e: # pragma: no cover - a resync must never mask the real error
|
||||
logger.debug(f"Could not check the engine picker against the config: {e}")
|
||||
|
||||
def _save_selected_settings(self, settings: dict):
|
||||
def _save_selected_settings(self, settings: dict[str, Any]) -> None:
|
||||
"""Persist selected settings to their appropriate config sections."""
|
||||
sr_settings = {k: v for k, v in settings.items() if not k.startswith("whispercpp_")}
|
||||
advanced_settings = {k: v for k, v in settings.items() if k.startswith("whispercpp_")}
|
||||
@@ -5647,15 +5654,34 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.config_manager.set("advanced", key, value)
|
||||
self.config_manager.save_settings()
|
||||
|
||||
def get_selected_settings(self) -> dict:
|
||||
def get_selected_settings(self) -> dict[str, Any]:
|
||||
"""Return the currently selected settings from the UI."""
|
||||
engine_text = self.engine_combo.get_active_text()
|
||||
model_id = self.model_combo.get_active_id()
|
||||
language_id = self.language_combo.get_active_id()
|
||||
|
||||
engine = _engine_from_display(engine_text) if engine_text else "vosk"
|
||||
model_variant = ""
|
||||
if engine == "whisper_cpp":
|
||||
model_size = self._get_selected_whispercpp_model()
|
||||
selected = self._get_selected_whispercpp_model()
|
||||
size = get_whispercpp_model_size(selected)
|
||||
derived = self._get_default_whispercpp_variant_for_size(size)
|
||||
# Full id for reconfigure/download. Pin deliberate specializations
|
||||
# (including multilingual while English). A language-derived default
|
||||
# stays unpinned so a later language change can re-derive — except
|
||||
# when an existing bare-size pin already matches the selection: that
|
||||
# is a deliberate multilingual pin that became the new language's
|
||||
# default, and must survive so returning to English still honours it.
|
||||
model_size = selected
|
||||
existing_pin = (
|
||||
self.config_manager.get_model_variant_for_engine("whisper_cpp") or ""
|
||||
).lower()
|
||||
if selected != derived:
|
||||
model_variant = selected
|
||||
elif existing_pin == selected and selected in WHISPERCPP_MODEL_SIZES:
|
||||
model_variant = selected
|
||||
else:
|
||||
model_variant = ""
|
||||
else:
|
||||
model_size = model_id.lower() if model_id else "small"
|
||||
language = language_id if language_id else self._default_language_for_engine(engine)
|
||||
@@ -5666,6 +5692,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
settings = {
|
||||
"engine": engine,
|
||||
"model_size": model_size,
|
||||
"model_variant": model_variant,
|
||||
"language": language,
|
||||
"vad_sensitivity": vad,
|
||||
"silence_timeout": silence,
|
||||
|
||||
@@ -127,6 +127,23 @@ def get_model_variants(model_size: str) -> list[str]:
|
||||
return list(MODEL_VARIANTS_BY_SIZE.get(model_size.lower(), []))
|
||||
|
||||
|
||||
def default_variant_for_size(model_size: str, language_is_english: bool) -> Optional[str]:
|
||||
"""Return the default specialization for a size bucket and language."""
|
||||
variants = get_model_variants(model_size)
|
||||
if not variants:
|
||||
return None
|
||||
|
||||
english_variant = f"{model_size}.en"
|
||||
if language_is_english 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]
|
||||
|
||||
|
||||
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()
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
"""Picking a language must decide the whisper.cpp variant (#776).
|
||||
|
||||
``whisper_cpp_model_size`` stores the id that loads the model, and for every size
|
||||
below "large" the multilingual variant id is the bare size name. A stored "medium"
|
||||
therefore cannot say whether the user chose the multilingual variant or never chose
|
||||
anything, which is why selecting English used to leave the picker on multilingual.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
import vocalinux.ui
|
||||
from vocalinux.ui.config_manager import ConfigManager, resolve_whispercpp_variant
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def settings_dialog():
|
||||
"""Import settings_dialog with real base classes for its GTK subclasses.
|
||||
|
||||
Same reasoning as tests/test_settings_model_state.py: conftest swaps gi for a
|
||||
MagicMock, so the three bases the module subclasses have to be real classes for
|
||||
its methods to stay reachable. Both sys.modules and the package attribute are
|
||||
restored, otherwise later tests patching the module by name reach a second
|
||||
module object (#686).
|
||||
"""
|
||||
repository = sys.modules["gi.repository"]
|
||||
bases = {name: type(name, (), {}) for name in ("Box", "ListBoxRow", "Dialog")}
|
||||
saved_module = sys.modules.pop("vocalinux.ui.settings_dialog", None)
|
||||
saved_attribute = getattr(vocalinux.ui, "settings_dialog", None)
|
||||
try:
|
||||
with patch.object(repository, "Gtk", MagicMock(**bases)):
|
||||
module = importlib.import_module("vocalinux.ui.settings_dialog")
|
||||
yield module
|
||||
finally:
|
||||
sys.modules.pop("vocalinux.ui.settings_dialog", None)
|
||||
if saved_module is not None:
|
||||
sys.modules["vocalinux.ui.settings_dialog"] = saved_module
|
||||
if saved_attribute is not None:
|
||||
vocalinux.ui.settings_dialog = saved_attribute
|
||||
elif hasattr(vocalinux.ui, "settings_dialog"):
|
||||
del vocalinux.ui.settings_dialog
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dialog_class(settings_dialog):
|
||||
return settings_dialog.SettingsDialog
|
||||
|
||||
|
||||
def _dialog_stub(settings_dialog: Any, language: str, pinned_variant: str = "") -> Mock:
|
||||
"""A stand-in ``self`` whose language drives the real derivation helper."""
|
||||
dialog = Mock()
|
||||
dialog.language = language
|
||||
dialog.language_combo.get_active_id.return_value = language
|
||||
dialog.config_manager.get_model_variant_for_engine.return_value = pinned_variant
|
||||
# The derivation itself is the code under test's collaborator, not a mock:
|
||||
# it is the real module-level helper, driven by the language above.
|
||||
dialog._get_default_whispercpp_variant_for_size.side_effect = (
|
||||
lambda size: settings_dialog._default_whispercpp_variant_for_size(size, language)
|
||||
)
|
||||
return dialog
|
||||
|
||||
|
||||
def _selection_dialog(
|
||||
settings_dialog: Any,
|
||||
language: str,
|
||||
selected_variant: str,
|
||||
pinned_variant: str = "",
|
||||
) -> Mock:
|
||||
"""A stand-in ``self`` for ``get_selected_settings`` whisper.cpp pin tests."""
|
||||
dialog = Mock()
|
||||
dialog.engine_combo.get_active_text.return_value = "whisper.cpp"
|
||||
dialog.model_combo.get_active_id.return_value = "medium"
|
||||
dialog.language_combo.get_active_id.return_value = language
|
||||
dialog.language = language
|
||||
dialog.config_manager.get_model_variant_for_engine.return_value = pinned_variant
|
||||
dialog._get_selected_whispercpp_model.return_value = selected_variant
|
||||
dialog._get_default_whispercpp_variant_for_size.side_effect = (
|
||||
lambda size: settings_dialog._default_whispercpp_variant_for_size(size, language)
|
||||
)
|
||||
dialog.vad_spin.get_value.return_value = 3
|
||||
dialog.silence_spin.get_value.return_value = 2.0
|
||||
dialog.advanced_no_timestamps_switch.get_active.return_value = True
|
||||
dialog.advanced_no_context_switch.get_active.return_value = True
|
||||
dialog.advanced_initial_prompt_buffer.get_text.return_value = ""
|
||||
dialog.advanced_temperature_spin.get_value.return_value = 0.0
|
||||
dialog.advanced_temperature_inc_spin.get_value.return_value = -1.0
|
||||
dialog.advanced_entropy_thold_spin.get_value.return_value = 2.4
|
||||
dialog.advanced_logprob_thold_spin.get_value.return_value = -1.0
|
||||
dialog.advanced_no_speech_thold_spin.get_value.return_value = 0.6
|
||||
dialog.gpu_device_combo.get_active_id.return_value = None
|
||||
return dialog
|
||||
|
||||
|
||||
@pytest.mark.parametrize("size", ["tiny", "base", "small", "medium"])
|
||||
def test_bare_size_with_english_selected_resolves_to_the_english_variant(
|
||||
settings_dialog, dialog_class, size
|
||||
):
|
||||
"""The reported bug: a bare size plus English must not stay multilingual."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us")
|
||||
|
||||
resolved = dialog_class._resolve_saved_whispercpp_variant(dialog, size)
|
||||
|
||||
assert resolved == f"{size}.en"
|
||||
|
||||
|
||||
def test_bare_size_with_a_non_english_language_stays_multilingual(settings_dialog, dialog_class):
|
||||
"""No English-only weights exist for other languages, so multilingual is right."""
|
||||
dialog = _dialog_stub(settings_dialog, "pl")
|
||||
|
||||
assert dialog_class._resolve_saved_whispercpp_variant(dialog, "medium") == "medium"
|
||||
|
||||
|
||||
def test_an_explicit_multilingual_pin_survives_selecting_english(settings_dialog, dialog_class):
|
||||
"""A deliberate choice must outrank the language-based default."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us", pinned_variant="medium")
|
||||
|
||||
assert dialog_class._resolve_saved_whispercpp_variant(dialog, "medium") == "medium"
|
||||
|
||||
|
||||
def test_a_legacy_specialization_is_kept_without_a_pin(settings_dialog, dialog_class):
|
||||
"""Older builds stored only one value; a non-size id was a deliberate pick."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us")
|
||||
|
||||
assert dialog_class._resolve_saved_whispercpp_variant(dialog, "medium-q5_0") == "medium-q5_0"
|
||||
|
||||
|
||||
def test_large_has_no_english_variant_so_english_still_gets_multilingual(
|
||||
settings_dialog, dialog_class
|
||||
):
|
||||
"""large ships multilingual weights only; the derivation must not invent large.en."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us")
|
||||
|
||||
resolved = dialog_class._resolve_saved_whispercpp_variant(dialog, "large")
|
||||
|
||||
assert resolved == "large"
|
||||
assert resolved in settings_dialog.WHISPERCPP_MODEL_INFO
|
||||
|
||||
|
||||
def test_an_unknown_saved_value_falls_back_to_a_real_variant(settings_dialog, dialog_class):
|
||||
"""Garbage in the config must not propagate into a download attempt."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us")
|
||||
|
||||
resolved = dialog_class._resolve_saved_whispercpp_variant(dialog, "not-a-model")
|
||||
|
||||
assert resolved in settings_dialog.WHISPERCPP_MODEL_INFO
|
||||
|
||||
|
||||
def test_language_derived_selection_does_not_pin(settings_dialog: Any, dialog_class: Any) -> None:
|
||||
"""en-us + the derived English-only variant must stay unpinned on auto-save."""
|
||||
dialog = _selection_dialog(settings_dialog, "en-us", "medium.en")
|
||||
|
||||
settings = dialog_class.get_selected_settings(dialog)
|
||||
|
||||
assert settings["model_size"] == "medium.en"
|
||||
assert settings["model_variant"] == ""
|
||||
|
||||
|
||||
def test_deliberate_multilingual_while_english_pins_bare_size(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""Choosing multilingual while English is a specialization and must pin."""
|
||||
dialog = _selection_dialog(settings_dialog, "en-us", "medium")
|
||||
|
||||
settings = dialog_class.get_selected_settings(dialog)
|
||||
|
||||
assert settings["model_size"] == "medium"
|
||||
assert settings["model_variant"] == "medium"
|
||||
|
||||
|
||||
def test_explicit_multilingual_pin_survives_non_english_auto_save(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""A multilingual pin must not clear when it matches the new language default."""
|
||||
dialog = _selection_dialog(settings_dialog, "pl", "medium", pinned_variant="medium")
|
||||
|
||||
settings = dialog_class.get_selected_settings(dialog)
|
||||
|
||||
assert settings["model_size"] == "medium"
|
||||
assert settings["model_variant"] == "medium"
|
||||
|
||||
|
||||
def test_stale_english_only_pin_matching_derived_still_clears(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""A leftover ``{size}.en`` pin that equals the English default must clear."""
|
||||
dialog = _selection_dialog(settings_dialog, "en-us", "medium.en", pinned_variant="medium.en")
|
||||
|
||||
settings = dialog_class.get_selected_settings(dialog)
|
||||
|
||||
assert settings["model_size"] == "medium.en"
|
||||
assert settings["model_variant"] == ""
|
||||
|
||||
|
||||
def test_unpinned_non_english_derived_selection_stays_unpinned(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""Polish + multilingual with an empty pin must stay unpinned on auto-save."""
|
||||
dialog = _selection_dialog(settings_dialog, "pl", "medium")
|
||||
|
||||
settings = dialog_class.get_selected_settings(dialog)
|
||||
|
||||
assert settings["model_size"] == "medium"
|
||||
assert settings["model_variant"] == ""
|
||||
|
||||
|
||||
def test_unpinned_plain_en_id_rederives_for_a_new_language(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""A leftover ``medium.en`` with an empty pin must not block Polish."""
|
||||
dialog = _dialog_stub(settings_dialog, "pl")
|
||||
|
||||
assert dialog_class._resolve_saved_whispercpp_variant(dialog, "medium.en") == "medium"
|
||||
|
||||
|
||||
def test_unpinned_plain_en_id_still_follows_english(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""The same leftover ``medium.en`` still derives English-only under English."""
|
||||
dialog = _dialog_stub(settings_dialog, "en-us")
|
||||
|
||||
assert dialog_class._resolve_saved_whispercpp_variant(dialog, "medium.en") == "medium.en"
|
||||
|
||||
|
||||
def test_unpinned_quantized_en_id_is_kept_as_legacy(
|
||||
settings_dialog: Any, dialog_class: Any
|
||||
) -> None:
|
||||
"""``{size}.en-q*`` is a real leftover specialization, not a language default."""
|
||||
dialog = _dialog_stub(settings_dialog, "pl")
|
||||
|
||||
assert (
|
||||
dialog_class._resolve_saved_whispercpp_variant(dialog, "medium.en-q5_0") == "medium.en-q5_0"
|
||||
)
|
||||
|
||||
|
||||
def test_unpinned_save_of_derived_en_persists_bare_size_and_rederives() -> None:
|
||||
"""Auto-apply of a derived .en must not pin, and language can re-derive."""
|
||||
manager = ConfigManager()
|
||||
manager.update_speech_recognition_settings(
|
||||
{
|
||||
"engine": "whisper_cpp",
|
||||
"model_size": "medium.en",
|
||||
"model_variant": "",
|
||||
"language": "en-us",
|
||||
}
|
||||
)
|
||||
sr_config = manager.config["speech_recognition"]
|
||||
|
||||
assert sr_config["whisper_cpp_model_variant"] == ""
|
||||
assert sr_config["whisper_cpp_model_size"] == "medium"
|
||||
assert sr_config["model_size"] == "medium"
|
||||
# Bare size in the config, but English still loads the derived .en id.
|
||||
assert manager.get_model_size_for_engine("whisper_cpp") == "medium.en"
|
||||
assert resolve_whispercpp_variant("medium.en", "", "pl") == "medium"
|
||||
assert resolve_whispercpp_variant(sr_config["whisper_cpp_model_size"], "", "pl") == "medium"
|
||||
|
||||
manager.config["speech_recognition"]["language"] = "pl"
|
||||
assert manager.get_model_size_for_engine("whisper_cpp") == "medium"
|
||||
|
||||
|
||||
def test_leftover_en_in_model_size_does_not_block_language_switch() -> None:
|
||||
"""Configs that already stored .en with an empty pin must re-derive."""
|
||||
manager = ConfigManager()
|
||||
manager.config["speech_recognition"]["whisper_cpp_model_size"] = "medium.en"
|
||||
manager.config["speech_recognition"]["whisper_cpp_model_variant"] = ""
|
||||
manager.config["speech_recognition"]["language"] = "pl"
|
||||
|
||||
assert manager.get_model_size_for_engine("whisper_cpp") == "medium"
|
||||
|
||||
|
||||
def test_pinned_save_persists_the_full_variant() -> None:
|
||||
"""A deliberate pin still stores the loadable id so the engine loads it."""
|
||||
manager = ConfigManager()
|
||||
manager.update_speech_recognition_settings(
|
||||
{
|
||||
"engine": "whisper_cpp",
|
||||
"model_size": "medium",
|
||||
"model_variant": "medium",
|
||||
"language": "en-us",
|
||||
}
|
||||
)
|
||||
sr_config = manager.config["speech_recognition"]
|
||||
|
||||
assert sr_config["whisper_cpp_model_variant"] == "medium"
|
||||
assert sr_config["whisper_cpp_model_size"] == "medium"
|
||||
assert manager.get_model_size_for_engine("whisper_cpp") == "medium"
|
||||
Reference in New Issue
Block a user