The first model load runs on a background thread after the tray is up. While it loads the tray shows the processing icon and 'Loading model…', and a dictation request gets a 'still loading' notification. A failed load leaves the tray in the error state instead of exiting. Fixes #50
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""Tray rendering while the speech model loads in the background (#50).
|
|
|
|
The tray is built before the model load starts, so it must show a loading
|
|
state, re-render on the GTK main loop when the load finishes, and keep the
|
|
idle keep-alive from unloading a model that is still being loaded.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from vocalinux.common_types import RecognitionState
|
|
from vocalinux.ui import tray_indicator
|
|
|
|
LOADING_TITLE = "Loading model…"
|
|
ICONS = {
|
|
"default": "default-icon",
|
|
"active": "active-icon",
|
|
"processing": "processing-icon",
|
|
}
|
|
|
|
|
|
def _engine(state=RecognitionState.IDLE, loading=False):
|
|
engine = MagicMock()
|
|
engine.state = state
|
|
engine.model_loading = loading
|
|
engine.is_auto_paused = False
|
|
return engine
|
|
|
|
|
|
def _make_tray(engine):
|
|
"""Build a TrayIndicator without running GTK setup; only rendering helpers are real."""
|
|
tray = tray_indicator.TrayIndicator.__new__(tray_indicator.TrayIndicator)
|
|
tray.speech_engine = engine
|
|
tray.indicator = MagicMock()
|
|
tray._icon_keys = dict(ICONS)
|
|
tray._indicator_title = tray_indicator.TRAY_TITLE
|
|
tray._language_shortcuts_refresh_pending = False
|
|
tray.overlay = None
|
|
tray.shortcut_manager = MagicMock()
|
|
tray._model_keepalive = MagicMock()
|
|
tray._auto_pause_monitor = MagicMock(paused=False)
|
|
tray._set_indicator_icon = MagicMock()
|
|
tray._set_indicator_title = MagicMock()
|
|
tray._set_menu_item_enabled = MagicMock()
|
|
tray._set_cancel_menu_item_visible = MagicMock()
|
|
return tray
|
|
|
|
|
|
def test_loading_shows_processing_icon_and_loading_title_while_idle():
|
|
tray = _make_tray(_engine(loading=True))
|
|
|
|
tray._update_ui(RecognitionState.IDLE)
|
|
|
|
tray._set_indicator_icon.assert_called_with(ICONS["processing"], "Loading speech model")
|
|
tray._set_indicator_title.assert_called_with(LOADING_TITLE)
|
|
tray._set_menu_item_enabled.assert_any_call("Start Voice Typing", False)
|
|
tray._set_menu_item_enabled.assert_any_call("Stop Voice Typing", False)
|
|
|
|
|
|
def test_finished_load_returns_to_idle_icon_and_enables_start():
|
|
tray = _make_tray(_engine(loading=False))
|
|
|
|
tray._update_ui(RecognitionState.IDLE)
|
|
|
|
tray._set_indicator_icon.assert_called_with(ICONS["default"], "Microphone off")
|
|
tray._set_menu_item_enabled.assert_any_call("Start Voice Typing", True)
|
|
|
|
|
|
def test_loading_state_does_not_mask_an_active_session():
|
|
tray = _make_tray(_engine(state=RecognitionState.LISTENING, loading=True))
|
|
|
|
tray._update_ui(RecognitionState.LISTENING)
|
|
|
|
tray._set_indicator_icon.assert_called_with(ICONS["active"], "Microphone on")
|
|
|
|
|
|
def test_load_change_is_marshalled_onto_the_gtk_main_loop():
|
|
tray = _make_tray(_engine(loading=False))
|
|
|
|
with patch.object(tray_indicator, "GLib") as glib:
|
|
tray._on_model_load_changed()
|
|
|
|
glib.idle_add.assert_called_once_with(tray._apply_model_load_state)
|
|
tray._set_indicator_icon.assert_not_called()
|
|
|
|
|
|
def test_finishing_the_load_restarts_the_keep_alive_timer():
|
|
engine = _engine(loading=False)
|
|
tray = _make_tray(engine)
|
|
|
|
assert tray._apply_model_load_state() is False
|
|
|
|
tray._model_keepalive.bump.assert_called_once_with()
|
|
tray._set_indicator_icon.assert_called_with(ICONS["default"], "Microphone off")
|
|
|
|
|
|
def test_loading_in_progress_does_not_restart_the_keep_alive_timer():
|
|
tray = _make_tray(_engine(loading=True))
|
|
|
|
tray._apply_model_load_state()
|
|
|
|
tray._model_keepalive.bump.assert_not_called()
|
|
|
|
|
|
def test_keep_alive_never_unloads_while_the_model_is_loading():
|
|
loading = _make_tray(_engine(loading=True))
|
|
loaded = _make_tray(_engine(loading=False))
|
|
|
|
assert loading._is_safe_for_keepalive_unload() is False
|
|
assert loaded._is_safe_for_keepalive_unload() is True
|