Files
voculinux/src/vocalinux/ui/action_handler.py
T
Jatin K MalikandGitHub 1dc1d44665 feat: Alpha Launch v0.2.0 - Complete UX Overhaul 🚀🎤 (#39)
* 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
2025-12-28 15:29:51 -08:00

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")