fix(settings): stop Test Dictation calling a missing model "no speech" (#702)
* fix(settings): stop Test Dictation calling a missing model "no speech" start_recognition now returns whether listening actually began. Test Dictation checks that before starting its timer, so a missing or auto-paused model gets a real error instead of "No speech detected". * fix(settings): reconfigure Test Dictation against the live engine UI matching config.json is not enough when speech_engine is still on another engine or model size. Compare those too before the listen timer. * fix(tests): keep settings_dialog lookups on one module _load_settings_dialog() restored sys.modules but left vocalinux.ui.settings_dialog pointing at the private reload. Python 3.9/3.10 mock follows that attribute, so test_whisper_list_and_delete patched a different copy than it imported and the expanduser stub hid the real cache. Point the package attribute back at the restored module, and patch the imported module object in the deletion test.
This commit is contained in:
@@ -21,8 +21,8 @@ class SpeechRecognitionManagerProtocol(Protocol):
|
||||
|
||||
state: RecognitionState
|
||||
|
||||
def start_recognition(self, mode: str = "toggle") -> None:
|
||||
"""Start the speech recognition process."""
|
||||
def start_recognition(self, mode: str = "toggle") -> bool:
|
||||
"""Start the speech recognition process. Returns True if listening began."""
|
||||
...
|
||||
|
||||
def stop_recognition(self) -> None:
|
||||
|
||||
@@ -2470,11 +2470,16 @@ class SpeechRecognitionManager:
|
||||
chunk_duration_ms = (1024 / 16000) * 1000
|
||||
return int(guard_ms / chunk_duration_ms)
|
||||
|
||||
def start_recognition(self, mode: str = "toggle"):
|
||||
"""Start the speech recognition process."""
|
||||
def start_recognition(self, mode: str = "toggle") -> bool:
|
||||
"""Start the speech recognition process.
|
||||
|
||||
Returns:
|
||||
True if recognition actually started (state is LISTENING), False if
|
||||
blocked (wrong state, auto-paused, or model not ready).
|
||||
"""
|
||||
if self.state != RecognitionState.IDLE:
|
||||
logger.warning(f"Cannot start recognition in current state: {self.state}")
|
||||
return
|
||||
return False
|
||||
|
||||
if self._auto_paused:
|
||||
logger.warning(
|
||||
@@ -2487,7 +2492,7 @@ class SpeechRecognitionManager:
|
||||
"Close that app or remove it from Auto-Pause settings to resume.",
|
||||
"dialog-information",
|
||||
)
|
||||
return
|
||||
return False
|
||||
|
||||
# Check if model is ready (lazy-reload after idle keep-alive unload)
|
||||
if not self.model_ready:
|
||||
@@ -2501,7 +2506,7 @@ class SpeechRecognitionManager:
|
||||
"Open Settings to check your engine and try again.",
|
||||
"dialog-warning",
|
||||
)
|
||||
return
|
||||
return False
|
||||
else:
|
||||
logger.warning(
|
||||
"Cannot start recognition: model not downloaded. "
|
||||
@@ -2514,7 +2519,7 @@ class SpeechRecognitionManager:
|
||||
"to use dictation.",
|
||||
"dialog-warning",
|
||||
)
|
||||
return
|
||||
return False
|
||||
|
||||
logger.info("Starting speech recognition")
|
||||
self._update_state(RecognitionState.LISTENING)
|
||||
@@ -2537,6 +2542,7 @@ class SpeechRecognitionManager:
|
||||
self.recognition_thread = threading.Thread(target=self._perform_recognition)
|
||||
self.recognition_thread.daemon = True
|
||||
self.recognition_thread.start()
|
||||
return True
|
||||
|
||||
def stop_recognition(self):
|
||||
"""Stop the speech recognition process."""
|
||||
|
||||
@@ -4962,13 +4962,22 @@ class SettingsDialog(Gtk.Dialog):
|
||||
|
||||
current_config = self.config_manager.get_settings().get("speech_recognition", {})
|
||||
selected_settings = self.get_selected_settings()
|
||||
selected_engine = selected_settings.get("engine")
|
||||
selected_model = selected_settings.get("model_size")
|
||||
# Compare to the live engine too: UI can match the file while the
|
||||
# in-memory manager is still on another engine/size (sidecar, pending apply).
|
||||
live_engine = getattr(self.speech_engine, "engine", None)
|
||||
live_model = getattr(self.speech_engine, "model_size", None)
|
||||
|
||||
settings_differ = False
|
||||
if current_config.get("engine") != selected_settings.get("engine") or current_config.get(
|
||||
"model_size"
|
||||
) != selected_settings.get("model_size"):
|
||||
if (
|
||||
current_config.get("engine") != selected_engine
|
||||
or current_config.get("model_size") != selected_model
|
||||
or live_engine != selected_engine
|
||||
or live_model != selected_model
|
||||
):
|
||||
settings_differ = True
|
||||
elif selected_settings.get("engine") == "vosk":
|
||||
elif selected_engine == "vosk":
|
||||
if current_config.get("vad_sensitivity") != selected_settings.get(
|
||||
"vad_sensitivity"
|
||||
) or current_config.get("silence_timeout") != selected_settings.get("silence_timeout"):
|
||||
@@ -4981,21 +4990,40 @@ class SettingsDialog(Gtk.Dialog):
|
||||
return
|
||||
self.test_buffer.set_text("Settings applied. Starting test...")
|
||||
|
||||
self.connect_to_recognition_manager()
|
||||
|
||||
self._saved_text_callbacks = self.speech_engine.get_text_callbacks()
|
||||
self.speech_engine.set_text_callbacks([self._test_text_callback])
|
||||
|
||||
if not self.speech_engine.start_recognition():
|
||||
self.speech_engine.set_text_callbacks(self._saved_text_callbacks)
|
||||
del self._saved_text_callbacks
|
||||
self.test_output_revealer.set_reveal_child(True)
|
||||
if getattr(self.speech_engine, "is_auto_paused", False):
|
||||
self.test_buffer.set_text(
|
||||
"Dictation is paused. Close the listed app or remove it from "
|
||||
"Auto-Pause settings."
|
||||
)
|
||||
elif not getattr(self.speech_engine, "model_ready", True):
|
||||
self.test_buffer.set_text(
|
||||
"No speech model downloaded. Open the Speech Model page and "
|
||||
"download a model to use Test Dictation."
|
||||
)
|
||||
else:
|
||||
self.test_buffer.set_text("Could not start recognition test.")
|
||||
return
|
||||
|
||||
self._test_active = True
|
||||
self.test_button.set_sensitive(False)
|
||||
self.test_button.set_label("Testing… Speak Now!")
|
||||
self.test_output_revealer.set_reveal_child(True)
|
||||
self.test_buffer.set_text("")
|
||||
self._test_result = ""
|
||||
|
||||
self.connect_to_recognition_manager()
|
||||
self.update_recognition_progress("Listening", info="Starting recognition test...")
|
||||
|
||||
self._saved_text_callbacks = self.speech_engine.get_text_callbacks()
|
||||
self.speech_engine.set_text_callbacks([self._test_text_callback])
|
||||
|
||||
self.speech_engine.start_recognition()
|
||||
threading.Thread(target=self._stop_test_after_delay, args=(3,)).start()
|
||||
# Enough room for one utterance plus the configured silence window.
|
||||
delay = float(selected_settings.get("silence_timeout", 2.0)) + 2.0
|
||||
threading.Thread(target=self._stop_test_after_delay, args=(delay,)).start()
|
||||
|
||||
def _test_text_callback(self, text: str):
|
||||
"""Callback specifically for the test recognition."""
|
||||
@@ -5011,7 +5039,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.test_textview.scroll_to_mark(mark, 0.0, True, 0.0, 1.0)
|
||||
return False
|
||||
|
||||
def _stop_test_after_delay(self, delay: int):
|
||||
def _stop_test_after_delay(self, delay: float):
|
||||
"""Stops the recognition test after a specified delay."""
|
||||
time.sleep(delay)
|
||||
GLib.idle_add(self._finalize_test)
|
||||
|
||||
Reference in New Issue
Block a user