From f02a6a7c66bf6632b9ec6f0575fb8ce679e1e5fd Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Mon, 7 Sep 2026 14:11:44 -0700 Subject: [PATCH] fix(settings): flip whisper.cpp specialization when language is English (#780) Bare size names like tiny/small/medium are also the multilingual variant ids, so a fresh config looks like a deliberate multilingual pick and the .en preference never runs. Retarget those ambiguous ids (and matching quantized pairs) when language implies English-only vs multilingual. Fixes #776. --- src/vocalinux/ui/settings_dialog.py | 60 +++++++++++++++++++++++++++++ tests/test_settings_dialog.py | 29 ++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/vocalinux/ui/settings_dialog.py b/src/vocalinux/ui/settings_dialog.py index f898688..1b1df2f 100644 --- a/src/vocalinux/ui/settings_dialog.py +++ b/src/vocalinux/ui/settings_dialog.py @@ -274,6 +274,59 @@ def _default_whispercpp_variant_for_size(model_size: str, language_id: str) -> O return variants[0] +def _is_language_paired_standard_variant(model_name: str) -> bool: + """Return whether a model id is the bare multilingual / .en pair for its size. + + Size bucket names (tiny, base, small, medium) are also the multilingual + specialization ids. Config often stores those ambiguous size-level defaults, + so they must not be treated as a locked multilingual choice. + """ + model_name = model_name.lower() + size = get_whispercpp_model_size(model_name) + if size == "large": + return model_name == "large" + return model_name in {size, f"{size}.en"} + + +def _mirror_whispercpp_english_variant(model_name: str) -> str: + """Map medium <-> medium.en and medium-q5_0 <-> medium.en-q5_0 style ids.""" + model_name = model_name.lower() + size = get_whispercpp_model_size(model_name) + english_prefix = f"{size}.en" + if model_name.startswith(english_prefix): + return size + model_name[len(english_prefix) :] + if model_name.startswith(size): + return english_prefix + model_name[len(size) :] + return model_name + + +def _whispercpp_variant_for_language(selected_model: str, language_id: str) -> str: + """Retarget a saved specialization when language implies .en vs multilingual.""" + selected_model = selected_model.lower() + size = get_whispercpp_model_size(selected_model) + variants = get_whispercpp_model_variants(size) + if selected_model not in variants: + return selected_model + + wants_english = _language_is_english(language_id) + is_english_only = is_english_only_whispercpp_model(selected_model) + + if _is_language_paired_standard_variant(selected_model): + default = _default_whispercpp_variant_for_size(size, language_id) + return default if default in variants else selected_model + + if wants_english != is_english_only: + mirrored = _mirror_whispercpp_english_variant(selected_model) + if mirrored in variants: + return mirrored + if not wants_english and is_english_only: + default = _default_whispercpp_variant_for_size(size, language_id) + if default in variants: + return default + + return selected_model + + # Uniform width for right-hand row controls so they align down a page. # ComboBoxText sizes itself to the longest item unless the cell is ellipsized, # which is why Shortcut Mode used to dwarf Shortcut Key. @@ -4866,7 +4919,14 @@ class SettingsDialog(Gtk.Dialog): ) self.model_variant_combo.append(model_name, display_text) + language_id = self.language_combo.get_active_id() or self.language model_to_set = selected_model if selected_model in variants else None + if model_to_set: + # Bare size ids double as multilingual variants; retarget for language + # so English picks .en instead of leaving Standard multilingual stuck. + model_to_set = _whispercpp_variant_for_language(model_to_set, language_id) + if model_to_set not in variants: + model_to_set = None if not model_to_set and recommended_model in variants: model_to_set = recommended_model if not model_to_set: diff --git a/tests/test_settings_dialog.py b/tests/test_settings_dialog.py index f385d2a..c1b3cae 100644 --- a/tests/test_settings_dialog.py +++ b/tests/test_settings_dialog.py @@ -713,6 +713,35 @@ class TestSettingsDialogHelperFunctions(unittest.TestCase): ) self.assertEqual(_default_whispercpp_variant_for_size("medium", "auto"), "medium") + def test_whispercpp_variant_for_language_retargets_ambiguous_size_ids(self): + """Bare size ids must follow language, not stick on multilingual.""" + from vocalinux.ui.settings_dialog import _whispercpp_variant_for_language + + # Default config stores "tiny"/"medium" which are also multilingual ids. + self.assertEqual(_whispercpp_variant_for_language("medium", "en-us"), "medium.en") + self.assertEqual(_whispercpp_variant_for_language("tiny", "en-us"), "tiny.en") + self.assertEqual(_whispercpp_variant_for_language("small", "en-in"), "small.en") + self.assertEqual(_whispercpp_variant_for_language("medium", "auto"), "medium") + self.assertEqual(_whispercpp_variant_for_language("medium.en", "auto"), "medium") + self.assertEqual(_whispercpp_variant_for_language("medium.en", "fr"), "medium") + + # Quantized pairs mirror with language when a match exists. + self.assertEqual( + _whispercpp_variant_for_language("medium-q5_0", "en-us"), + "medium.en-q5_0", + ) + self.assertEqual( + _whispercpp_variant_for_language("medium.en-q5_0", "auto"), + "medium-q5_0", + ) + + # No English pair: leave the deliberate specialization alone. + self.assertEqual( + _whispercpp_variant_for_language("large-v3-turbo", "en-us"), + "large-v3-turbo", + ) + self.assertEqual(_whispercpp_variant_for_language("large", "en-us"), "large") + def test_whisper_delete_unknown_model(self): """Test deleting an unknown Whisper model raises ValueError.""" from vocalinux.ui.settings_dialog import _delete_whisper_model