* feat: prepare for v0.2.0-alpha release * feat: prepare for v0.2.0-alpha release * chore: remove CHANGELOG.md, use GitHub Releases instead * refactor: use existing About dialog instead of Settings section * style: enhance About dialog UI readability * style: fix formatting with black and isort * feat: show VAD and silence timeout settings for Whisper engine - Renamed 'VOSK Settings' to 'Recognition Settings' - VAD Sensitivity and Silence Timeout now apply to both engines - Added tooltips explaining each setting - VOSK-specific model info only shown when VOSK selected * fix: resolve overlapping UI elements in settings dialog * fix: save VAD and silence timeout settings for both engines Previously these settings were only saved when VOSK was selected. Now they persist correctly for Whisper as well. * fix: show download progress when selecting undownloaded model Previously when auto-applying settings, models would download silently in the background without showing the progress dialog. Now: - Download progress dialog appears for undownloaded models - Model icons update correctly after download completes * feat: add download progress, speed, ETA and cancel button - Show real download progress percentage for VOSK models - Display download speed (MB/s) and estimated time remaining - Add Cancel button to abort downloads - Update status during extraction phase - Whisper downloads still pulse (uses internal downloader) * fix: defer model download until needed, don't block startup - App now starts instantly even if models aren't downloaded - Models are downloaded on-demand when selected in Settings - If user tries to dictate without a model, plays error sound - Added model_ready property to check if engine is usable * fix: improve first-run UX without downloaded models - Settings dialog now defaults to smallest available model, not saved config - Only show saved model if it's actually downloaded - Add desktop notification when trying to dictate without a model - Prevents auto-downloading large models on first launch * feat: add download progress tracking for Whisper models - Implement custom Whisper model download with progress/speed/ETA - Uses OpenAI's CDN URLs directly instead of whisper.load_model() - Progress callback works the same as VOSK downloads - Supports cancellation during download * fix: enable download progress callback for Whisper models Progress callback was only being set for VOSK downloads, not Whisper. Now both engines show real-time progress, speed, and ETA. * fix: update tests for defer_download and config manager mocking - test_main: Mock ConfigManager to prevent saved config from overriding test values - test_settings_dialog: Mock ModelDownloadDialog to prevent GTK init errors - test_speech_recognition: Use proper MockVoskModule class instead of MagicMock, pass defer_download=False for download test - test_whisper_support: Pass defer_download=False to trigger model loading in test * fix: resolve test failures for CI - test_main: Fix ConfigManager patch path to use source module - test_settings_dialog: Mock _is_vosk_model_downloaded and _is_whisper_model_downloaded to skip download flow, add missing dialog attributes - test_speech_recognition: Replace recognizer with properly configured mock that returns valid JSON, fixing test isolation issues when running all tests together * fix: mock _download_whisper_model in whisper test to avoid requests exception handling issue * fix: add gir1.2-girepository-2.0 dep for PyGObject on Ubuntu 24.04 - Add missing girepository-2.0 package for newer Ubuntu runners - Use --no-build-isolation for PyGObject to use system packages * fix: skip PyGObject pip install, use system python3-gi package - Use --no-deps for editable install to skip PyGObject - Install other deps explicitly without PyGObject - Add python3-gi-cairo for complete GTK support - Removes problematic girepository-2.0 build requirement * fix: install PyGObject via pip with proper build deps - Remove python3-gi/python3-gi-cairo (doesn't work with setup-python) - Install PyGObject from pip after installing build dependencies - Simplify venv setup without --system-site-packages hack * Fix GObject * fix: set GI_TYPELIB_PATH for pip-installed PyGObject to find AppIndicator3
131 lines
4.3 KiB
Python
131 lines
4.3 KiB
Python
"""
|
|
Action handler for Vocalinux voice commands.
|
|
|
|
This module handles special voice commands like "delete that", "undo", etc.
|
|
"""
|
|
|
|
import logging
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from ..text_injection.text_injector import TextInjector
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ActionHandler:
|
|
"""
|
|
Handles special voice command actions.
|
|
|
|
This class processes action commands from the speech recognition system
|
|
and performs the appropriate actions like deleting text, undoing, etc.
|
|
"""
|
|
|
|
def __init__(self, text_injector: "TextInjector"):
|
|
"""
|
|
Initialize the action handler.
|
|
|
|
Args:
|
|
text_injector: The text injector instance for performing actions
|
|
"""
|
|
self.text_injector = text_injector
|
|
self.last_injected_text = ""
|
|
|
|
# Map actions to handler methods
|
|
self.action_handlers = {
|
|
"delete_last": self._handle_delete_last,
|
|
"undo": self._handle_undo,
|
|
"redo": self._handle_redo,
|
|
"select_all": self._handle_select_all,
|
|
"select_line": self._handle_select_line,
|
|
"select_word": self._handle_select_word,
|
|
"select_paragraph": self._handle_select_paragraph,
|
|
"cut": self._handle_cut,
|
|
"copy": self._handle_copy,
|
|
"paste": self._handle_paste,
|
|
}
|
|
|
|
def handle_action(self, action: str) -> bool:
|
|
"""
|
|
Handle a voice command action.
|
|
|
|
Args:
|
|
action: The action to perform
|
|
|
|
Returns:
|
|
True if the action was handled successfully, False otherwise
|
|
"""
|
|
logger.debug(f"Handling action: {action}")
|
|
|
|
handler = self.action_handlers.get(action)
|
|
if handler:
|
|
try:
|
|
return handler()
|
|
except Exception as e:
|
|
logger.error(f"Error handling action '{action}': {e}")
|
|
return False
|
|
else:
|
|
logger.warning(f"Unknown action: {action}")
|
|
return False
|
|
|
|
def set_last_injected_text(self, text: str):
|
|
"""
|
|
Set the last injected text for undo/delete operations.
|
|
|
|
Args:
|
|
text: The last text that was injected
|
|
"""
|
|
self.last_injected_text = text
|
|
|
|
def _handle_delete_last(self) -> bool:
|
|
"""Handle 'delete that' command by sending backspace keys."""
|
|
if not self.last_injected_text:
|
|
logger.debug("No text to delete")
|
|
return True
|
|
|
|
# Send backspace keys for each character in the last injected text
|
|
backspaces = "\b" * len(self.last_injected_text)
|
|
success = self.text_injector.inject_text(backspaces)
|
|
|
|
if success:
|
|
logger.debug(f"Deleted {len(self.last_injected_text)} characters")
|
|
self.last_injected_text = ""
|
|
|
|
return success
|
|
|
|
def _handle_undo(self) -> bool:
|
|
"""Handle 'undo' command by sending Ctrl+Z."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+z")
|
|
|
|
def _handle_redo(self) -> bool:
|
|
"""Handle 'redo' command by sending Ctrl+Y."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+y")
|
|
|
|
def _handle_select_all(self) -> bool:
|
|
"""Handle 'select all' command by sending Ctrl+A."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+a")
|
|
|
|
def _handle_select_line(self) -> bool:
|
|
"""Handle 'select line' command by sending Home+Shift+End."""
|
|
return self.text_injector._inject_keyboard_shortcut("Home+shift+End")
|
|
|
|
def _handle_select_word(self) -> bool:
|
|
"""Handle 'select word' command by sending Ctrl+Shift+Right."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Right")
|
|
|
|
def _handle_select_paragraph(self) -> bool:
|
|
"""Handle 'select paragraph' command by sending Ctrl+Shift+Down."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+shift+Down")
|
|
|
|
def _handle_cut(self) -> bool:
|
|
"""Handle 'cut' command by sending Ctrl+X."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+x")
|
|
|
|
def _handle_copy(self) -> bool:
|
|
"""Handle 'copy' command by sending Ctrl+C."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+c")
|
|
|
|
def _handle_paste(self) -> bool:
|
|
"""Handle 'paste' command by sending Ctrl+V."""
|
|
return self.text_injector._inject_keyboard_shortcut("ctrl+v")
|