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.
This commit is contained in:
Jatin K Malik
2026-09-07 14:11:44 -07:00
committed by GitHub
parent 688d9e9bd2
commit f02a6a7c66
2 changed files with 89 additions and 0 deletions
+60
View File
@@ -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:
+29
View File
@@ -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