* fix(settings): make model downloads explicit and cancel the whole intent "Use it" on the recommendation card applied a different model than the one shown, a cancelled or failed apply left the staged model behind so the next Test Dictation downloaded it, and cancelling a download tore down and then reloaded the previous model while the dialog waited. - Recommendation is now passive text, compared by size tier, with a link to docs/MODEL_SELECTION.md. The "Use it" button is gone. - Every Settings download asks first and shows size and source host. Declining, cancelling or failing drops the staged model spec and puts the pickers back on the saved config. - Test Dictation refuses a model that is not on disk instead of applying it. - reconfigure() fetches a missing model before releasing the loaded one, so a cancel restores state without reloading or rehashing the old model. Cancellation state is reset only when a download claim is taken or ended. - Follow-layout mode shows the resolved language in the simple Main language control, read-only, and simple edits keep the "layout" setting. Fixes #979 Fixes #980 Fixes #981 Fixes #982 Fixes #985 Fixes #988 * fix(settings): restore follow-layout mode when a model change is declined * fix(settings): keep follow-layout and dictation consistent during model changes * style(tests): format follow-layout regression tests * fix(recognition): skip resume reinit while a model download is running * fix(recognition): keep a lazy reload pending when resume is skipped during a download --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
797 lines
33 KiB
Python
797 lines
33 KiB
Python
"""Simple mode asks what the user knows and derives the rest (#779)."""
|
|
|
|
import importlib
|
|
import sys
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
import vocalinux.ui
|
|
from vocalinux.utils import whispercpp_model_info
|
|
from vocalinux.utils.model_choice import (
|
|
ACCURATE,
|
|
BALANCED,
|
|
FASTEST,
|
|
priority_for_size,
|
|
size_for_priority,
|
|
)
|
|
|
|
|
|
@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; both sys.modules and the
|
|
package attribute are restored so later tests patching the module by name do
|
|
not 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(language="pl", multi=False, priority=BALANCED, recommended="small", second=None):
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_syncing = False
|
|
dialog._applying_settings = False
|
|
# A Mock attribute is truthy, which would trip the steering guard.
|
|
dialog._simple_driving = False
|
|
dialog.language = language
|
|
dialog.simple_language_combo.get_active_id.return_value = language
|
|
dialog.simple_multi_switch.get_active.return_value = multi
|
|
dialog.simple_second_language_combo.get_active_id.return_value = second
|
|
dialog.simple_priority_combo.get_active_id.return_value = priority
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = (recommended, "reason")
|
|
dialog._get_selected_engine.return_value = "whisper_cpp"
|
|
# The language resolution is the code under test's own helper, not a mock.
|
|
dialog._simple_decoding_language.side_effect = (
|
|
lambda: _sd().SettingsDialog._simple_decoding_language(dialog)
|
|
)
|
|
# Disk lookups are covered separately; here the derived variant stands.
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: variant
|
|
return dialog
|
|
|
|
|
|
def _sd():
|
|
import vocalinux.ui.settings_dialog as module
|
|
|
|
return module
|
|
|
|
|
|
# --- the size half of the derivation -------------------------------------
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"recommended,priority,expected",
|
|
[
|
|
("small", FASTEST, "base"),
|
|
("small", BALANCED, "small"),
|
|
("small", ACCURATE, "medium"),
|
|
("tiny", FASTEST, "tiny"), # clamped at the bottom
|
|
("large", ACCURATE, "large"), # clamped at the top
|
|
],
|
|
)
|
|
def test_priority_moves_relative_to_the_hardware_recommendation(recommended, priority, expected):
|
|
assert size_for_priority(recommended, priority) == expected
|
|
|
|
|
|
def test_priority_reads_back_from_an_already_chosen_size():
|
|
"""Opening simple mode must describe the current model, not reset it."""
|
|
assert priority_for_size("small", "base") == FASTEST
|
|
assert priority_for_size("small", "small") == BALANCED
|
|
assert priority_for_size("small", "medium") == ACCURATE
|
|
|
|
|
|
# --- simple mode driving the advanced controls ---------------------------
|
|
|
|
|
|
def test_a_single_language_pins_it_and_picks_the_matching_variant(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub(language="pl", multi=False, priority=BALANCED, recommended="small")
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.engine_combo.set_active_id.assert_called_once_with("whisper_cpp")
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "pl")
|
|
dialog.model_combo.set_active_id.assert_called_once_with("small")
|
|
# Polish has no English-only weights, so the multilingual variant is correct.
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("small")
|
|
|
|
|
|
def test_english_gets_the_english_only_variant(settings_dialog, dialog_class):
|
|
"""Same size, better recognition, so simple mode must not leave it on multilingual."""
|
|
dialog = _dialog_stub(language="en-us", multi=False, priority=BALANCED, recommended="small")
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("small.en")
|
|
|
|
|
|
def test_naming_a_second_language_turns_on_auto_detect(settings_dialog, dialog_class):
|
|
"""Two languages cannot be pinned, so the engine gets detection instead."""
|
|
dialog = _dialog_stub(language="pl", multi=True, second="en-us", recommended="small")
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "auto")
|
|
assert dialog.language == "auto"
|
|
# Detection cannot use English-only weights.
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("small")
|
|
|
|
|
|
def test_most_accurate_moves_a_size_up(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub(language="en-us", multi=False, priority=ACCURATE, recommended="small")
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.model_combo.set_active_id.assert_called_once_with("medium")
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("medium.en")
|
|
|
|
|
|
def test_most_accurate_on_large_falls_back_to_multilingual_for_english(
|
|
settings_dialog, dialog_class
|
|
):
|
|
"""large ships no English-only weights, so the variant must stay real."""
|
|
dialog = _dialog_stub(language="en-us", multi=False, priority=ACCURATE, recommended="large")
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.model_combo.set_active_id.assert_called_once_with("large")
|
|
chosen = dialog.model_variant_combo.set_active_id.call_args[0][0]
|
|
assert chosen in settings_dialog.WHISPERCPP_MODEL_INFO
|
|
assert not settings_dialog.is_english_only_whispercpp_model(chosen)
|
|
|
|
|
|
# --- guards ---------------------------------------------------------------
|
|
|
|
|
|
def test_syncing_the_widgets_does_not_count_as_a_user_edit(settings_dialog, dialog_class):
|
|
"""Otherwise opening the page would re-apply and trigger a download."""
|
|
dialog = _dialog_stub()
|
|
dialog._simple_syncing = True
|
|
|
|
dialog_class._on_simple_choice_changed(dialog)
|
|
|
|
dialog._apply_simple_choice.assert_not_called()
|
|
dialog._auto_apply_settings.assert_not_called()
|
|
|
|
|
|
def test_a_real_edit_applies_and_saves(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub()
|
|
|
|
dialog_class._on_simple_choice_changed(dialog)
|
|
|
|
dialog._apply_simple_choice.assert_called_once()
|
|
dialog._auto_apply_settings.assert_called_once()
|
|
|
|
|
|
def test_the_second_language_list_appears_only_when_asked_for(settings_dialog, dialog_class):
|
|
"""show_all() is a no-op on a no_show_all widget: the flag must go first.
|
|
|
|
Reported from the installed build — the switch turned on and nothing
|
|
appeared, because the row was shown with the flag still set.
|
|
"""
|
|
dialog = Mock()
|
|
dialog.simple_multi_switch.get_active.return_value = True
|
|
row = dialog.simple_second_language_row
|
|
order = []
|
|
row.set_no_show_all.side_effect = lambda flag: order.append(("no_show_all", flag))
|
|
row.show_all.side_effect = lambda: order.append(("show_all", None))
|
|
|
|
dialog_class._update_simple_visibility(dialog)
|
|
|
|
assert order == [("no_show_all", False), ("show_all", None)]
|
|
|
|
|
|
def test_the_second_language_list_is_hidden_by_default(settings_dialog, dialog_class):
|
|
dialog = Mock()
|
|
dialog.simple_multi_switch.get_active.return_value = False
|
|
row = dialog.simple_second_language_row
|
|
order = []
|
|
row.hide.side_effect = lambda: order.append("hide")
|
|
row.set_no_show_all.side_effect = lambda flag: order.append(("no_show_all", flag))
|
|
|
|
dialog_class._update_simple_visibility(dialog)
|
|
|
|
assert order == ["hide", ("no_show_all", True)]
|
|
|
|
|
|
def test_any_language_as_the_second_answer_means_detection(settings_dialog, dialog_class):
|
|
"""The explicit multilingual option."""
|
|
dialog = _dialog_stub(language="pl", multi=True, second="auto")
|
|
|
|
assert dialog_class._simple_decoding_language(dialog) == "auto"
|
|
|
|
|
|
def test_turning_the_switch_on_defaults_the_second_list_to_any_language(
|
|
settings_dialog, dialog_class
|
|
):
|
|
"""Otherwise flipping the switch alone would visibly do nothing."""
|
|
dialog = _dialog_stub(language="pl", multi=True, second=None)
|
|
|
|
dialog_class._on_simple_choice_changed(dialog)
|
|
|
|
dialog.simple_second_language_combo.set_active_id.assert_called_once_with("auto")
|
|
dialog._apply_simple_choice.assert_called_once()
|
|
|
|
|
|
def test_the_info_card_stays_on_the_page(settings_dialog):
|
|
"""It is the only feedback that a priority change did anything, and its cost."""
|
|
import inspect
|
|
|
|
source = inspect.getsource(settings_dialog.SettingsDialog._build_engine_section)
|
|
|
|
assert "self.simple_page.pack_start(self.model_info_card" in source
|
|
assert "self.advanced_box.pack_start(self.model_info_card" not in source
|
|
|
|
|
|
def test_a_second_language_switches_decoding_to_detection(settings_dialog, dialog_class):
|
|
"""whisper takes one language or none, so two means automatic detection."""
|
|
dialog = _dialog_stub(language="pl", multi=True, second="en-us")
|
|
|
|
assert dialog_class._simple_decoding_language(dialog) == "auto"
|
|
|
|
|
|
def test_two_english_entries_still_pin_english(settings_dialog, dialog_class):
|
|
"""en-US plus en-IN is still English, so the .en weights stay usable."""
|
|
dialog = _dialog_stub(language="en-us", multi=True, second="en-in")
|
|
|
|
assert dialog_class._simple_decoding_language(dialog) == "en-us"
|
|
|
|
|
|
def test_the_switch_without_a_second_language_keeps_the_main_one(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub(language="pl", multi=True, second=None)
|
|
|
|
assert dialog_class._simple_decoding_language(dialog) == "pl"
|
|
|
|
|
|
def test_no_switch_pins_the_main_language(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub(language="pl", multi=False, second="en-us")
|
|
|
|
assert dialog_class._simple_decoding_language(dialog) == "pl"
|
|
|
|
|
|
def test_expanding_the_island_shows_the_rows_for_the_active_engine(settings_dialog, dialog_class):
|
|
"""show_all() first, then the per-engine pass, or rows the engine does not
|
|
use would be revealed."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
expander = Mock()
|
|
expander.get_expanded.return_value = True
|
|
order = []
|
|
dialog.advanced_box.show_all.side_effect = lambda: order.append("show_all")
|
|
dialog._update_engine_specific_ui.side_effect = lambda: order.append("engine_ui")
|
|
|
|
dialog_class._on_advanced_expanded(dialog, expander, None)
|
|
|
|
assert order == ["show_all", "engine_ui"]
|
|
dialog.config_manager.set.assert_called_once_with("speech_recognition", "show_advanced", True)
|
|
|
|
|
|
def test_collapsing_the_island_rereads_the_live_model(settings_dialog, dialog_class):
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
expander = Mock()
|
|
expander.get_expanded.return_value = False
|
|
|
|
dialog_class._on_advanced_expanded(dialog, expander, None)
|
|
|
|
dialog._sync_simple_from_advanced.assert_called_once()
|
|
dialog.config_manager.set.assert_called_once_with("speech_recognition", "show_advanced", False)
|
|
|
|
|
|
def test_restoring_the_island_on_open_does_not_write_the_config(settings_dialog, dialog_class):
|
|
"""Setting the saved state during init must not count as a user choice."""
|
|
dialog = Mock()
|
|
dialog._initializing = True
|
|
expander = Mock()
|
|
expander.get_expanded.return_value = True
|
|
|
|
dialog_class._on_advanced_expanded(dialog, expander, None)
|
|
|
|
dialog.config_manager.set.assert_not_called()
|
|
|
|
|
|
def test_a_change_in_the_advanced_rows_shows_up_in_the_simple_answers(
|
|
settings_dialog, dialog_class
|
|
):
|
|
"""Both cards are on screen; they must not contradict each other."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_driving = False
|
|
dialog._simple_syncing = False
|
|
|
|
dialog_class._refresh_simple_readout(dialog)
|
|
|
|
dialog._sync_simple_from_advanced.assert_called_once()
|
|
|
|
|
|
@pytest.mark.parametrize("flag", ["_initializing", "_simple_driving", "_simple_syncing"])
|
|
def test_the_readout_stays_quiet_while_simple_mode_is_steering(settings_dialog, dialog_class, flag):
|
|
"""Re-syncing mid-steer would read half-set controls back into the answers."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_driving = False
|
|
dialog._simple_syncing = False
|
|
setattr(dialog, flag, True)
|
|
|
|
dialog_class._refresh_simple_readout(dialog)
|
|
|
|
dialog._sync_simple_from_advanced.assert_not_called()
|
|
|
|
|
|
def test_no_second_toplevel_is_created_for_advanced(settings_dialog, dialog_class):
|
|
"""A second window failed twice on KWin/Wayland: transient for the dialog it
|
|
crashed the compositor (tag kwin-crash-repro); standing alone it could not
|
|
be raised above the dialog. The island lives in the same window."""
|
|
import inspect
|
|
|
|
source = inspect.getsource(dialog_class._build_simple_model_section)
|
|
helper = inspect.getsource(settings_dialog._make_expander_card)
|
|
|
|
assert "Gtk.Window(" not in source
|
|
assert "set_transient_for" not in source
|
|
assert "_make_expander_card" in source
|
|
assert "Gtk.Expander" in helper
|
|
|
|
|
|
def test_opening_simple_mode_describes_the_current_model_instead_of_resetting_it(
|
|
settings_dialog, dialog_class
|
|
):
|
|
"""Switching to simple must not silently change which model is loaded."""
|
|
dialog = Mock()
|
|
dialog.language = "en-us"
|
|
dialog.language_combo.get_active_id.return_value = "en-us"
|
|
dialog.config_manager.get.return_value = ""
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small.en", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "medium.en"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(False)
|
|
dialog.simple_language_combo.set_active_id.assert_called_once_with("en-us")
|
|
# medium sits one step above the recommended small, which is "most accurate".
|
|
dialog.simple_priority_combo.set_active_id.assert_called_once_with(ACCURATE)
|
|
assert dialog._simple_syncing is False
|
|
|
|
|
|
def test_auto_detect_shows_up_as_the_other_languages_switch(settings_dialog, dialog_class):
|
|
dialog = Mock()
|
|
dialog.language = "auto"
|
|
dialog.language_combo.get_active_id.return_value = "auto"
|
|
dialog.simple_language_combo.get_active_id.return_value = None
|
|
dialog.config_manager.get.return_value = ""
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "small"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(True)
|
|
|
|
|
|
def test_follow_layout_shows_and_disables_the_resolved_simple_language(
|
|
settings_dialog: Any, dialog_class: Any
|
|
) -> None:
|
|
dialog = _dialog_stub(language="de")
|
|
dialog.language_combo.get_active_id.return_value = "de"
|
|
dialog._get_selected_whispercpp_model.return_value = "small"
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small", "reason")
|
|
|
|
with patch.object(settings_dialog, "_is_following_layout", return_value=True):
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_language_combo.set_active_id.assert_called_once_with("de")
|
|
dialog.simple_language_row.set_sensitive.assert_called_once_with(False)
|
|
dialog.simple_language_row.set_tooltip_text.assert_called_once_with(
|
|
"Follows your keyboard layout. Turn off Follow keyboard layout "
|
|
"in Advanced to pick a language."
|
|
)
|
|
|
|
|
|
def test_follow_layout_priority_change_does_not_write_resolved_language(
|
|
settings_dialog: Any, dialog_class: Any
|
|
) -> None:
|
|
dialog = _dialog_stub(language="de", priority=ACCURATE, recommended="small")
|
|
dialog._get_selected_engine.return_value = "whisper_cpp"
|
|
|
|
with patch.object(settings_dialog, "_is_following_layout", return_value=True):
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_not_called()
|
|
assert dialog.language == "de"
|
|
dialog.model_combo.set_active_id.assert_called_once_with("medium")
|
|
|
|
|
|
def test_follow_layout_priority_change_keeps_multilingual_variant_and_mode(
|
|
settings_dialog: Any, dialog_class: Any
|
|
) -> None:
|
|
dialog = _dialog_stub(language="en-us", priority=FASTEST, recommended="small")
|
|
dialog._follow_layout_active = True
|
|
dialog.follow_layout_switch.get_active.return_value = True
|
|
dialog._follow_layout_supported.return_value = True
|
|
selected_variant: dict[str, Any] = {"id": None}
|
|
dialog.model_variant_combo.set_active_id.side_effect = lambda variant: selected_variant.update(
|
|
id=variant
|
|
)
|
|
dialog._get_selected_whispercpp_model.side_effect = lambda: selected_variant["id"] or "small"
|
|
dialog._is_selected_whispercpp_model_english_only.side_effect = lambda: (
|
|
settings_dialog.is_english_only_whispercpp_model(dialog._get_selected_whispercpp_model())
|
|
)
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
dialog_class._sync_follow_layout_controls(dialog)
|
|
|
|
assert selected_variant["id"] == "base"
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("base")
|
|
dialog._on_disk_stand_in.assert_called_once_with(
|
|
"base", "base", settings_dialog.LANGUAGE_FOLLOWS_LAYOUT
|
|
)
|
|
assert dialog.follow_layout_switch.get_active() is True
|
|
assert dialog._follow_layout_active is True
|
|
|
|
|
|
@pytest.mark.parametrize("stale_second", ["auto", "en-us"])
|
|
def test_pinned_advanced_language_plus_stale_stored_second_does_not_replace_the_pin(
|
|
settings_dialog, dialog_class, stale_second
|
|
):
|
|
"""A leftover simple second answer must not override a pinned Advanced language.
|
|
|
|
Sync used to re-enable the multi switch from persisted ``simple_second_language``.
|
|
The next simple edit then decoded to auto and silently replaced the pin.
|
|
"""
|
|
dialog = Mock()
|
|
dialog.language = "pl"
|
|
dialog.language_combo.get_active_id.return_value = "pl"
|
|
dialog.config_manager.get.return_value = stale_second
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "small"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(False)
|
|
dialog.simple_language_combo.set_active_id.assert_called_once_with("pl")
|
|
dialog.config_manager.set.assert_called_with("speech_recognition", "simple_second_language", "")
|
|
dialog.simple_second_language_combo.set_active_id.assert_called_with("auto")
|
|
|
|
# Widgets after sync: multi off, main language pinned. A later simple apply
|
|
# must keep the pin rather than silently writing auto.
|
|
dialog.simple_multi_switch.get_active.return_value = False
|
|
dialog.simple_language_combo.get_active_id.return_value = "pl"
|
|
dialog.simple_second_language_combo.get_active_id.return_value = "auto"
|
|
dialog.simple_priority_combo.get_active_id.return_value = BALANCED
|
|
dialog._simple_decoding_language.side_effect = lambda: dialog_class._simple_decoding_language(
|
|
dialog
|
|
)
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: variant
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "pl")
|
|
assert dialog.language == "pl"
|
|
|
|
|
|
def test_two_english_second_language_survives_sync_from_a_pin(settings_dialog, dialog_class):
|
|
"""en-US plus en-IN still pins English; sync must not treat that as leftover auto.
|
|
|
|
Dropping the switch and wiping ``simple_second_language`` on every pinned
|
|
readout would forget the second English answer the next time settings open.
|
|
"""
|
|
dialog = Mock()
|
|
dialog.language = "en-us"
|
|
dialog.language_combo.get_active_id.return_value = "en-us"
|
|
dialog.config_manager.get.return_value = "en-in"
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small.en", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "small.en"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(True)
|
|
dialog.simple_language_combo.set_active_id.assert_called_once_with("en-us")
|
|
dialog.simple_second_language_combo.set_active_id.assert_called_once_with("en-in")
|
|
dialog.config_manager.set.assert_not_called()
|
|
|
|
dialog.simple_multi_switch.get_active.return_value = True
|
|
dialog.simple_language_combo.get_active_id.return_value = "en-us"
|
|
dialog.simple_second_language_combo.get_active_id.return_value = "en-in"
|
|
dialog.simple_priority_combo.get_active_id.return_value = BALANCED
|
|
dialog._simple_decoding_language.side_effect = lambda: dialog_class._simple_decoding_language(
|
|
dialog
|
|
)
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: variant
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "en-us")
|
|
assert dialog.language == "en-us"
|
|
|
|
|
|
@pytest.mark.parametrize("stale_second", ["en-in", "en-us"])
|
|
def test_two_english_second_language_does_not_override_advanced_auto(
|
|
settings_dialog, dialog_class, stale_second
|
|
):
|
|
"""en-US plus another English still pins English, so it cannot sit on auto.
|
|
|
|
Restoring that leftover over Advanced auto would make the next simple edit
|
|
silently replace auto with en-us. Duplicate en-US is the same pin.
|
|
"""
|
|
dialog = Mock()
|
|
dialog.language = "auto"
|
|
dialog.language_combo.get_active_id.return_value = "auto"
|
|
dialog.simple_language_combo.get_active_id.return_value = "en-us"
|
|
dialog.config_manager.get.return_value = stale_second
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "small"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(True)
|
|
dialog.simple_language_combo.set_active_id.assert_not_called()
|
|
dialog.simple_second_language_combo.set_active_id.assert_called_once_with("auto")
|
|
dialog.config_manager.set.assert_called_with("speech_recognition", "simple_second_language", "")
|
|
|
|
dialog.simple_multi_switch.get_active.return_value = True
|
|
dialog.simple_language_combo.get_active_id.return_value = "en-us"
|
|
dialog.simple_second_language_combo.get_active_id.return_value = "auto"
|
|
dialog.simple_priority_combo.get_active_id.return_value = BALANCED
|
|
dialog._simple_decoding_language.side_effect = lambda: dialog_class._simple_decoding_language(
|
|
dialog
|
|
)
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: variant
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "auto")
|
|
assert dialog.language == "auto"
|
|
|
|
|
|
def test_named_second_language_that_still_means_auto_survives_auto_sync(
|
|
settings_dialog, dialog_class
|
|
):
|
|
"""English plus Polish is detection; Advanced auto must keep that second pick."""
|
|
dialog = Mock()
|
|
dialog.language = "auto"
|
|
dialog.language_combo.get_active_id.return_value = "auto"
|
|
dialog.simple_language_combo.get_active_id.return_value = "en-us"
|
|
dialog.config_manager.get.return_value = "pl"
|
|
dialog._get_recommended_whispercpp_model_for_language.return_value = ("small", "reason")
|
|
dialog._get_selected_whispercpp_model.return_value = "small"
|
|
|
|
dialog_class._sync_simple_from_advanced(dialog)
|
|
|
|
dialog.simple_multi_switch.set_active.assert_called_once_with(True)
|
|
dialog.simple_second_language_combo.set_active_id.assert_called_once_with("pl")
|
|
dialog.config_manager.set.assert_not_called()
|
|
|
|
dialog.simple_multi_switch.get_active.return_value = True
|
|
dialog.simple_language_combo.get_active_id.return_value = "en-us"
|
|
dialog.simple_second_language_combo.get_active_id.return_value = "pl"
|
|
dialog.simple_priority_combo.get_active_id.return_value = BALANCED
|
|
dialog._simple_decoding_language.side_effect = lambda: dialog_class._simple_decoding_language(
|
|
dialog
|
|
)
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: variant
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "auto")
|
|
assert dialog.language == "auto"
|
|
|
|
|
|
def test_changing_advanced_language_refreshes_the_simple_readout(settings_dialog, dialog_class):
|
|
"""Otherwise a pin in Advanced leaves a stale multi switch on screen."""
|
|
dialog = Mock()
|
|
dialog._processing_language_change = False
|
|
dialog._initializing = False
|
|
dialog._applying_settings = False
|
|
dialog._simple_driving = False
|
|
dialog.language_combo.get_active_id.return_value = "pl"
|
|
dialog.engine_combo.get_active_text.return_value = "Local (whisper.cpp)"
|
|
|
|
dialog_class._on_language_changed(dialog, None)
|
|
|
|
dialog._refresh_simple_readout.assert_called_once()
|
|
|
|
|
|
# --- regressions reported from the installed build -----------------------
|
|
|
|
|
|
def test_the_steering_guard_is_held_for_the_whole_pick(settings_dialog, dialog_class):
|
|
"""Picking a language froze the window: each steered combo re-applied.
|
|
|
|
_apply_simple_choice sets engine, language, size and variant, and every one of
|
|
those handlers ends in _auto_apply_settings, so one pick reconfigured the engine
|
|
up to four times on the UI thread. The guard has to be held for the whole of the
|
|
steering and released before the single apply that follows.
|
|
"""
|
|
dialog = _dialog_stub()
|
|
held = []
|
|
dialog._apply_simple_choice.side_effect = lambda: held.append(dialog._simple_driving)
|
|
|
|
dialog_class._on_simple_choice_changed(dialog)
|
|
|
|
assert held == [True], "guard was not held while the controls were being steered"
|
|
assert dialog._simple_driving is False, "guard was not released afterwards"
|
|
dialog._auto_apply_settings.assert_called_once()
|
|
|
|
|
|
def test_auto_apply_is_suppressed_while_simple_mode_is_steering(settings_dialog, dialog_class):
|
|
"""The guard itself: nothing runs while the controls are being pointed."""
|
|
dialog = Mock()
|
|
dialog._applying_settings = False
|
|
dialog._initializing = False
|
|
dialog._test_active = False
|
|
dialog._populating_models = False
|
|
dialog._simple_driving = True
|
|
|
|
dialog_class._auto_apply_settings(dialog)
|
|
|
|
dialog.get_selected_settings.assert_not_called()
|
|
|
|
|
|
def test_the_simple_language_box_accepts_typing(settings_dialog, dialog_class):
|
|
"""A list of thirty-plus languages you cannot type into is not usable."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_syncing = False
|
|
dialog.simple_language_combo.get_active_id.return_value = None
|
|
dialog.simple_language_combo.get_child.return_value.get_text.return_value = "Pol"
|
|
|
|
with patch.object(settings_dialog, "_combo_text_rows", return_value=[("pl", "Polish")]):
|
|
dialog_class._commit_or_restore_simple_language_entry(dialog)
|
|
|
|
dialog.simple_language_combo.set_active_id.assert_called_once_with("pl")
|
|
|
|
|
|
def test_unresolvable_typing_restores_the_previous_language(settings_dialog, dialog_class):
|
|
"""Typing nonsense must not leave the box in a state the config never had."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_syncing = False
|
|
dialog.language = "pl"
|
|
dialog.simple_language_combo.get_active_id.return_value = None
|
|
dialog.simple_language_combo.get_child.return_value.get_text.return_value = "zzzz"
|
|
|
|
with patch.object(settings_dialog, "_combo_text_rows", return_value=[("pl", "Polish")]):
|
|
dialog_class._commit_or_restore_simple_language_entry(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.simple_language_combo, "pl")
|
|
|
|
|
|
def test_restoring_after_auto_detect_falls_back_to_a_real_language(settings_dialog, dialog_class):
|
|
""" "auto" is the switch, not an entry in this list, so it cannot be restored."""
|
|
dialog = Mock()
|
|
dialog._initializing = False
|
|
dialog._simple_syncing = False
|
|
dialog.language = "auto"
|
|
dialog.simple_language_combo.get_active_id.return_value = None
|
|
dialog.simple_language_combo.get_child.return_value.get_text.return_value = ""
|
|
|
|
with patch.object(settings_dialog, "_combo_text_rows", return_value=[("pl", "Polish")]):
|
|
dialog_class._commit_or_restore_simple_language_entry(dialog)
|
|
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(
|
|
dialog.simple_language_combo, "en-us"
|
|
)
|
|
|
|
|
|
# --- reuse what is on disk instead of downloading a sibling ---------------
|
|
|
|
|
|
def _with_disk(settings_dialog, downloaded):
|
|
# The stand-in helper lives in whispercpp_model_info and reads its own
|
|
# module globals, so the disk state is patched there, not on the dialog.
|
|
return (
|
|
patch.object(
|
|
whispercpp_model_info,
|
|
"is_model_downloaded",
|
|
side_effect=lambda name: name in downloaded,
|
|
),
|
|
patch.object(
|
|
whispercpp_model_info,
|
|
"get_model_variants",
|
|
return_value=["base", "base.en", "base-q5_1", "base.en-q5_1", "base-q8_0"],
|
|
),
|
|
)
|
|
|
|
|
|
def test_a_same_size_weight_on_disk_stands_in_for_a_missing_sibling(settings_dialog, dialog_class):
|
|
"""Reported: switching the other-languages switch off went from base to
|
|
base.en and sat through a 141 MB modal download for the same size."""
|
|
on_disk, variants = _with_disk(settings_dialog, ["base"])
|
|
with on_disk, variants:
|
|
chosen = dialog_class._on_disk_stand_in(Mock(), "base.en", "base", "en-us")
|
|
assert chosen == "base"
|
|
|
|
|
|
def test_the_derived_variant_is_kept_when_it_is_already_on_disk(settings_dialog, dialog_class):
|
|
on_disk, variants = _with_disk(settings_dialog, ["base", "base.en"])
|
|
with on_disk, variants:
|
|
assert dialog_class._on_disk_stand_in(Mock(), "base.en", "base", "en-us") == "base.en"
|
|
|
|
|
|
def test_english_only_weights_never_stand_in_for_another_language(settings_dialog, dialog_class):
|
|
on_disk, variants = _with_disk(settings_dialog, ["base.en"])
|
|
with on_disk, variants:
|
|
assert dialog_class._on_disk_stand_in(Mock(), "base", "base", "pl") == "base"
|
|
|
|
|
|
def test_english_only_weights_never_stand_in_for_detection(settings_dialog, dialog_class):
|
|
on_disk, variants = _with_disk(settings_dialog, ["base.en"])
|
|
with on_disk, variants:
|
|
assert dialog_class._on_disk_stand_in(Mock(), "base", "base", "auto") == "base"
|
|
|
|
|
|
def test_nothing_of_that_size_on_disk_means_the_derived_variant_and_a_download(
|
|
settings_dialog, dialog_class
|
|
):
|
|
on_disk, variants = _with_disk(settings_dialog, ["medium.en"])
|
|
with on_disk, variants:
|
|
assert dialog_class._on_disk_stand_in(Mock(), "base.en", "base", "en-us") == "base.en"
|
|
|
|
|
|
def test_a_plain_weight_is_preferred_over_a_quantized_one(settings_dialog, dialog_class):
|
|
on_disk, variants = _with_disk(settings_dialog, ["base-q5_1", "base"])
|
|
with on_disk, variants:
|
|
assert dialog_class._on_disk_stand_in(Mock(), "base.en", "base", "en-us") == "base"
|
|
|
|
|
|
@pytest.mark.parametrize("engine", ["faster_whisper", "parakeet"])
|
|
def test_simple_choice_leaves_faster_whisper_and_parakeet_alone(
|
|
settings_dialog, dialog_class, engine
|
|
):
|
|
"""Editing a simple question must not yank Advanced off Faster Whisper or Parakeet."""
|
|
dialog = _dialog_stub(language="en-us", multi=False, priority=BALANCED, recommended="small")
|
|
dialog._get_selected_engine.return_value = engine
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.engine_combo.set_active_id.assert_not_called()
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "en-us")
|
|
assert dialog.language == "en-us"
|
|
dialog.model_combo.set_active_id.assert_not_called()
|
|
dialog.model_variant_combo.set_active_id.assert_not_called()
|
|
|
|
|
|
@pytest.mark.parametrize("engine", ["vosk", "remote_api", "whisper", "unknown"])
|
|
def test_simple_choice_steers_incompatible_engines_to_whisper_cpp(
|
|
settings_dialog, dialog_class, engine
|
|
):
|
|
dialog = _dialog_stub(language="pl", multi=False, priority=BALANCED, recommended="small")
|
|
dialog._get_selected_engine.return_value = engine
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog.engine_combo.set_active_id.assert_called_once_with("whisper_cpp")
|
|
dialog._set_combo_active_id_or_first.assert_called_once_with(dialog.language_combo, "pl")
|
|
dialog.model_combo.set_active_id.assert_called_once_with("small")
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("small")
|
|
|
|
|
|
def test_simple_mode_consults_the_disk_before_settling_on_a_variant(settings_dialog, dialog_class):
|
|
dialog = _dialog_stub(language="en-us", multi=False, priority=BALANCED, recommended="small")
|
|
dialog._on_disk_stand_in.side_effect = lambda variant, size, language: "small"
|
|
|
|
dialog_class._apply_simple_choice(dialog)
|
|
|
|
dialog._on_disk_stand_in.assert_called_once_with("small.en", "small", "en-us")
|
|
dialog.model_variant_combo.set_active_id.assert_called_once_with("small")
|