chore: bump minimum Python from 3.8 to 3.9 (#336)
* chore: bump minimum Python from 3.8 to 3.9 and adopt PEP 585 builtins - Update requires-python, classifiers, mypy target, and black target in pyproject.toml - Replace Python 3.8 with 3.9 in CI matrix and nightly workflow - Replace typing.List/Dict/Tuple/Set with builtin list/dict/tuple/set (PEP 585) - Update all 3.8 version references in README, docs, website, and install.sh - Fix test_wayland_no_tools to mock IBus availability - Remove List from common_types re-exports and update test * style: reformat test files for Black 26.x and pin version in CI Black 26.x introduced parenthesized context manager formatting. Reformat 7 test files and pin Black>=26,<27 in CI to prevent future drift from unpinned version upgrades.
This commit is contained in:
@@ -175,7 +175,7 @@ jobs:
|
||||
| **Version** | ${{ steps.version.outputs.version }} |
|
||||
| **Base Version** | ${{ steps.version.outputs.base_version }} |
|
||||
| **Build Date** | ${{ steps.version.outputs.date }} |
|
||||
| **Python** | 3.8+ |
|
||||
| **Python** | 3.9+ |
|
||||
| **OS** | Linux (X11 & Wayland) |
|
||||
|
||||
---
|
||||
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
- name: Install linting tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 black isort
|
||||
pip install flake8 "black>=26,<27" isort
|
||||
|
||||
- name: Lint with flake8 (critical errors)
|
||||
run: |
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.8', '3.10', '3.11', '3.13']
|
||||
python-version: ['3.9', '3.10', '3.11', '3.13']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<!-- Build & Quality -->
|
||||
[](https://github.com/jatinkrmalik/vocalinux/actions)
|
||||
[](https://github.com/jatinkrmalik/vocalinux)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://www.python.org/downloads/)
|
||||
[](https://www.gtk.org/)
|
||||
[](https://codecov.io/gh/jatinkrmalik/vocalinux)
|
||||
|
||||
@@ -204,7 +204,7 @@ Or launch it from your application menu!
|
||||
## 📋 Requirements
|
||||
|
||||
- **OS**: Linux (tested on Ubuntu 22.04+, Debian 11+, Fedora 39+, Arch Linux, openSUSE Tumbleweed)
|
||||
- **Python**: 3.8 or newer
|
||||
- **Python**: 3.9 or newer
|
||||
- **Display**: X11 or Wayland
|
||||
- **Hardware**: Microphone for voice input
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ sudo zypper install -y python3-gobject python3-gobject-cairo gtk3 \
|
||||
### Gentoo
|
||||
```bash
|
||||
sudo emerge dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libappindicator:3 \
|
||||
media-libs/portaudio dev-lang/python:3.8
|
||||
media-libs/portaudio dev-lang/python:3.9
|
||||
```
|
||||
|
||||
**Note:** Gentoo compiles packages from source, which will take longer.
|
||||
|
||||
+1
-1
@@ -31,7 +31,7 @@ git clone https://github.com/jatinkrmalik/vocalinux.git && cd vocalinux && ./ins
|
||||
| Requirement | Details |
|
||||
|-------------|---------|
|
||||
| **Operating System** | Ubuntu 22.04+ (recommended), Debian 11+, Fedora 38+, Arch Linux |
|
||||
| **Python** | 3.8 or newer |
|
||||
| **Python** | 3.9 or newer |
|
||||
| **Display Server** | X11 or Wayland |
|
||||
| **Hardware** | Microphone for speech input |
|
||||
| **Disk Space** | ~200MB (including whisper.cpp model) |
|
||||
|
||||
+2
-2
@@ -1152,7 +1152,7 @@ install_system_dependencies() {
|
||||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config wget curl unzip base-devel vulkan-tools vulkan-headers glslang xclip wl-clipboard"
|
||||
local ZYPPER_PACKAGES="python3-pip python3-gobject python3-gobject-cairo gtk3 libappindicator-gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config wget curl unzip vulkan-tools vulkan-devel glslang xclip wl-clipboard"
|
||||
# Gentoo uses Portage and different package naming convention
|
||||
local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.8 pkgconf dev-util/glslang x11-misc/xclip gui-apps/wl-clipboard"
|
||||
local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.9 pkgconf dev-util/glslang x11-misc/xclip gui-apps/wl-clipboard"
|
||||
# Alpine Linux uses apk and has musl libc
|
||||
local APK_PACKAGES="py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev py3-virtualenv pkgconf wget curl unzip glslang vulkan-tools xclip wl-clipboard"
|
||||
# Void Linux uses xbps
|
||||
@@ -1721,7 +1721,7 @@ mkdir -p "$ICON_DIR"
|
||||
|
||||
# Check Python version
|
||||
check_python_version() {
|
||||
local MIN_VERSION="3.8"
|
||||
local MIN_VERSION="3.9"
|
||||
local PYTHON_CMD="python3"
|
||||
|
||||
# Check if python3 command exists
|
||||
|
||||
+3
-4
@@ -14,7 +14,7 @@ authors = [
|
||||
maintainers = [
|
||||
{name = "Jatin K Malik", email = "jatinkrmalik@gmail.com"}
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
requires-python = ">=3.9"
|
||||
keywords = [
|
||||
"voice", "dictation", "speech", "recognition",
|
||||
"linux", "accessibility", "voice-typing", "speech-to-text"
|
||||
@@ -26,7 +26,6 @@ classifiers = [
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
@@ -99,7 +98,7 @@ version = {attr = "vocalinux.version.__version__"}
|
||||
|
||||
[tool.black]
|
||||
line-length = 100
|
||||
target-version = ['py38', 'py39', 'py310', 'py311', 'py312', 'py313']
|
||||
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
|
||||
include = '\.pyi?$'
|
||||
exclude = '''
|
||||
/(
|
||||
@@ -157,7 +156,7 @@ exclude_lines = [
|
||||
show_missing = true
|
||||
|
||||
[tool.mypy]
|
||||
python_version = "3.8"
|
||||
python_version = "3.9"
|
||||
warn_return_any = true
|
||||
warn_unused_configs = true
|
||||
ignore_missing_imports = true
|
||||
|
||||
@@ -4,7 +4,7 @@ This module provides type definitions to avoid circular imports.
|
||||
"""
|
||||
|
||||
from enum import Enum, auto
|
||||
from typing import Callable, List, Optional, Protocol # noqa: F401
|
||||
from typing import Callable, Optional, Protocol # noqa: F401
|
||||
|
||||
|
||||
class RecognitionState(Enum):
|
||||
|
||||
@@ -7,7 +7,6 @@ This module processes text commands from speech recognition, such as
|
||||
|
||||
import logging
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -99,7 +98,7 @@ class CommandProcessor:
|
||||
)
|
||||
self.format_cmd_regex = re.compile(format_cmd_pattern, re.IGNORECASE)
|
||||
|
||||
def process_text(self, text: str) -> Tuple[str, List[str]]:
|
||||
def process_text(self, text: str) -> tuple[str, list[str]]:
|
||||
"""
|
||||
Process text commands in the recognized text.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import queue
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable, List, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
from ..common_types import RecognitionState
|
||||
from ..ui.audio_feedback import play_error_sound, play_start_sound, play_stop_sound
|
||||
@@ -518,9 +518,9 @@ class SpeechRecognitionManager:
|
||||
self._voice_commands_preference = kwargs.get("voice_commands_enabled")
|
||||
self._voice_commands_enabled = self._resolve_voice_commands_enabled()
|
||||
|
||||
self.text_callbacks: List[Callable[[str], None]] = []
|
||||
self.state_callbacks: List[Callable[[RecognitionState], None]] = []
|
||||
self.action_callbacks: List[Callable[[str], None]] = []
|
||||
self.text_callbacks: list[Callable[[str], None]] = []
|
||||
self.state_callbacks: list[Callable[[RecognitionState], None]] = []
|
||||
self.action_callbacks: list[Callable[[str], None]] = []
|
||||
|
||||
# Download progress tracking
|
||||
self._download_progress_callback: Optional[Callable[[float, float, str], None]] = None
|
||||
@@ -537,7 +537,7 @@ class SpeechRecognitionManager:
|
||||
|
||||
# Audio diagnostics tracking
|
||||
self._last_audio_level = 0.0
|
||||
self._audio_level_callbacks: List[Callable[[float], None]] = []
|
||||
self._audio_level_callbacks: list[Callable[[float], None]] = []
|
||||
|
||||
# Recording control flags
|
||||
self.should_record = False
|
||||
@@ -699,7 +699,7 @@ class SpeechRecognitionManager:
|
||||
self.state = RecognitionState.ERROR
|
||||
raise
|
||||
|
||||
def _transcribe_with_whisper(self, audio_buffer: List[bytes]) -> str:
|
||||
def _transcribe_with_whisper(self, audio_buffer: list[bytes]) -> str:
|
||||
"""
|
||||
Transcribe audio buffer using Whisper.
|
||||
|
||||
@@ -911,7 +911,7 @@ class SpeechRecognitionManager:
|
||||
self.state = RecognitionState.ERROR
|
||||
raise
|
||||
|
||||
def _transcribe_with_whispercpp(self, audio_buffer: List[bytes]) -> str:
|
||||
def _transcribe_with_whispercpp(self, audio_buffer: list[bytes]) -> str:
|
||||
"""
|
||||
Transcribe audio buffer using whisper.cpp.
|
||||
|
||||
@@ -1397,11 +1397,11 @@ class SpeechRecognitionManager:
|
||||
except ValueError:
|
||||
logger.warning(f"Callback {callback} not found in text_callbacks.")
|
||||
|
||||
def get_text_callbacks(self) -> List[Callable[[str], None]]:
|
||||
def get_text_callbacks(self) -> list[Callable[[str], None]]:
|
||||
"""Get a copy of the current text callbacks list."""
|
||||
return list(self.text_callbacks)
|
||||
|
||||
def set_text_callbacks(self, callbacks: List[Callable[[str], None]]):
|
||||
def set_text_callbacks(self, callbacks: list[Callable[[str], None]]):
|
||||
"""Set the text callbacks list (used for temporarily replacing callbacks)."""
|
||||
self.text_callbacks = list(callbacks)
|
||||
|
||||
@@ -1829,7 +1829,7 @@ class SpeechRecognitionManager:
|
||||
|
||||
self._process_audio_buffer(audio_buffer)
|
||||
|
||||
def _process_audio_buffer(self, audio_buffer: List[bytes]):
|
||||
def _process_audio_buffer(self, audio_buffer: list[bytes]):
|
||||
"""Process an immutable audio segment for transcription and commands."""
|
||||
if not audio_buffer:
|
||||
return
|
||||
@@ -1959,7 +1959,7 @@ class SpeechRecognitionManager:
|
||||
self._update_state(RecognitionState.LISTENING)
|
||||
logger.info("DEBUG: _perform_recognition thread exiting")
|
||||
|
||||
def _enqueue_audio_segment(self, audio_buffer: List[bytes]):
|
||||
def _enqueue_audio_segment(self, audio_buffer: list[bytes]):
|
||||
"""Queue an audio segment for asynchronous transcription."""
|
||||
segment = audio_buffer.copy()
|
||||
if not segment:
|
||||
|
||||
@@ -7,7 +7,7 @@ This module handles loading, saving, and accessing user preferences.
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -109,7 +109,7 @@ class ConfigManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
|
||||
def _check_needs_migration(self, user_config: Dict) -> bool:
|
||||
def _check_needs_migration(self, user_config: dict) -> bool:
|
||||
"""Check if the user config needs migration to add per-engine model sizes."""
|
||||
sr_config = user_config.get("speech_recognition", {})
|
||||
# Need migration if we have model_size but not the per-engine keys
|
||||
@@ -117,7 +117,7 @@ class ConfigManager:
|
||||
"vosk_model_size" not in sr_config or "whisper_model_size" not in sr_config
|
||||
)
|
||||
|
||||
def _migrate_config(self, user_config: Dict):
|
||||
def _migrate_config(self, user_config: dict):
|
||||
"""Migrate old config formats to the current format."""
|
||||
sr_config = self.config.get("speech_recognition", {})
|
||||
user_sr_config = user_config.get("speech_recognition", {})
|
||||
@@ -210,7 +210,7 @@ class ConfigManager:
|
||||
logger.error(f"Failed to set config value: {e}")
|
||||
return False
|
||||
|
||||
def get_settings(self) -> Dict[str, Any]:
|
||||
def get_settings(self) -> dict[str, Any]:
|
||||
"""Get the entire configuration dictionary."""
|
||||
return self.config
|
||||
|
||||
@@ -267,7 +267,7 @@ class ConfigManager:
|
||||
|
||||
return enabled
|
||||
|
||||
def update_speech_recognition_settings(self, settings: Dict[str, Any]):
|
||||
def update_speech_recognition_settings(self, settings: dict[str, Any]):
|
||||
"""Update multiple speech recognition settings at once."""
|
||||
if "speech_recognition" not in self.config:
|
||||
self.config["speech_recognition"] = {}
|
||||
@@ -293,7 +293,7 @@ class ConfigManager:
|
||||
self.config["sound_effects"] = {}
|
||||
self.config["sound_effects"]["enabled"] = enabled
|
||||
|
||||
def _update_dict_recursive(self, target: Dict, source: Dict):
|
||||
def _update_dict_recursive(self, target: dict, source: dict):
|
||||
"""
|
||||
Update a dictionary recursively.
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import os
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, List, Optional, Set
|
||||
from typing import Optional
|
||||
|
||||
# Try to import evdev
|
||||
try:
|
||||
@@ -41,7 +41,7 @@ KEY_LEFTMETA = 125 # Super/Windows key
|
||||
KEY_RIGHTMETA = 126
|
||||
|
||||
# Map modifier key names to evdev key codes
|
||||
MODIFIER_KEY_CODES: Dict[str, Set[int]] = {
|
||||
MODIFIER_KEY_CODES: dict[str, set[int]] = {
|
||||
"ctrl": {KEY_LEFTCTRL, KEY_RIGHTCTRL},
|
||||
"alt": {KEY_LEFTALT, KEY_RIGHTALT},
|
||||
"shift": {KEY_LEFTSHIFT, KEY_RIGHTSHIFT},
|
||||
@@ -55,7 +55,7 @@ MODIFIER_KEY_CODES: Dict[str, Set[int]] = {
|
||||
}
|
||||
|
||||
|
||||
def find_keyboard_devices() -> List[str]:
|
||||
def find_keyboard_devices() -> list[str]:
|
||||
"""
|
||||
Find all keyboard input devices.
|
||||
|
||||
@@ -149,22 +149,22 @@ class EvdevKeyboardBackend(KeyboardBackend):
|
||||
mode: The shortcut mode ("toggle" or "push_to_talk")
|
||||
"""
|
||||
super().__init__(shortcut, mode)
|
||||
self.devices: List[InputDevice] = []
|
||||
self.device_fds: List[int] = []
|
||||
self.devices: list[InputDevice] = []
|
||||
self.device_fds: list[int] = []
|
||||
self.running = False
|
||||
self.monitor_thread: Optional[threading.Thread] = None
|
||||
|
||||
self.last_trigger_time = 0
|
||||
self.last_key_press_time = 0
|
||||
self.double_tap_threshold = 0.3 # seconds
|
||||
self.key_pressed_devices: Set[int] = set()
|
||||
self.key_pressed_devices: set[int] = set()
|
||||
|
||||
self._devices_lock = threading.Lock()
|
||||
|
||||
if not EVDEV_AVAILABLE:
|
||||
logger.error("python-evdev not available")
|
||||
|
||||
def _get_target_key_codes(self) -> Set[int]:
|
||||
def _get_target_key_codes(self) -> set[int]:
|
||||
"""Get the evdev key codes for the configured modifier."""
|
||||
return MODIFIER_KEY_CODES.get(self._modifier_key, set())
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, List, Optional
|
||||
from typing import Callable, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -58,8 +58,8 @@ class LoggingManager:
|
||||
max_records: Maximum number of log records to keep in memory
|
||||
"""
|
||||
self.max_records = max_records
|
||||
self.log_records: List[LogRecord] = []
|
||||
self.log_callbacks: List[Callable[[LogRecord], None]] = []
|
||||
self.log_records: list[LogRecord] = []
|
||||
self.log_callbacks: list[Callable[[LogRecord], None]] = []
|
||||
self.lock = threading.Lock()
|
||||
|
||||
# Create logs directory
|
||||
@@ -130,7 +130,7 @@ class LoggingManager:
|
||||
level_filter: Optional[str] = None,
|
||||
module_filter: Optional[str] = None,
|
||||
last_n: Optional[int] = None,
|
||||
) -> List[LogRecord]:
|
||||
) -> list[LogRecord]:
|
||||
"""
|
||||
Get log records with optional filtering.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ for whisper.cpp, supporting Vulkan, CUDA, and CPU backends.
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from typing import Dict, Optional, Tuple
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -60,7 +60,7 @@ class ComputeBackend:
|
||||
CPU = "cpu"
|
||||
|
||||
|
||||
def detect_vulkan_support() -> Tuple[bool, Optional[str]]:
|
||||
def detect_vulkan_support() -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Detect if Vulkan is available and get device info.
|
||||
|
||||
@@ -91,7 +91,7 @@ def detect_vulkan_support() -> Tuple[bool, Optional[str]]:
|
||||
return False, None
|
||||
|
||||
|
||||
def detect_cuda_support() -> Tuple[bool, Optional[str]]:
|
||||
def detect_cuda_support() -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
Detect if NVIDIA CUDA is available and get device info.
|
||||
|
||||
@@ -119,7 +119,7 @@ def detect_cuda_support() -> Tuple[bool, Optional[str]]:
|
||||
return False, None
|
||||
|
||||
|
||||
def detect_compute_backend() -> Tuple[str, str]:
|
||||
def detect_compute_backend() -> tuple[str, str]:
|
||||
"""
|
||||
Detect the best available compute backend.
|
||||
|
||||
@@ -177,7 +177,7 @@ def detect_cpu_info() -> str:
|
||||
return "CPU"
|
||||
|
||||
|
||||
def get_recommended_model() -> Tuple[str, str]:
|
||||
def get_recommended_model() -> tuple[str, str]:
|
||||
"""
|
||||
Get the recommended whisper.cpp model based on system configuration.
|
||||
|
||||
|
||||
@@ -175,8 +175,9 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback, "_get_audio_player", return_value=(None, [])
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(audio_feedback, "_get_audio_player", return_value=(None, [])),
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
@@ -189,11 +190,15 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("paplay", ["wav"]),
|
||||
), patch.object(audio_feedback.subprocess, "Popen") as mock_popen:
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("paplay", ["wav"]),
|
||||
),
|
||||
patch.object(audio_feedback.subprocess, "Popen") as mock_popen,
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
|
||||
@@ -209,11 +214,15 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("aplay", ["wav"]),
|
||||
), patch.object(audio_feedback.subprocess, "Popen") as mock_popen:
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("aplay", ["wav"]),
|
||||
),
|
||||
patch.object(audio_feedback.subprocess, "Popen") as mock_popen,
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
|
||||
@@ -230,11 +239,15 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("mplayer", ["wav"]),
|
||||
), patch.object(audio_feedback.subprocess, "Popen") as mock_popen:
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("mplayer", ["wav"]),
|
||||
),
|
||||
patch.object(audio_feedback.subprocess, "Popen") as mock_popen,
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
|
||||
@@ -251,9 +264,11 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback, "_get_audio_player", return_value=("play", ["wav"])
|
||||
), patch.object(audio_feedback.subprocess, "Popen") as mock_popen:
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(audio_feedback, "_get_audio_player", return_value=("play", ["wav"])),
|
||||
patch.object(audio_feedback.subprocess, "Popen") as mock_popen,
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
|
||||
@@ -270,14 +285,18 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("paplay", ["wav"]),
|
||||
), patch.object(
|
||||
audio_feedback.subprocess,
|
||||
"Popen",
|
||||
side_effect=Exception("Mock error"),
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("paplay", ["wav"]),
|
||||
),
|
||||
patch.object(
|
||||
audio_feedback.subprocess,
|
||||
"Popen",
|
||||
side_effect=Exception("Mock error"),
|
||||
),
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
@@ -326,11 +345,15 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.os.path, "exists", return_value=True), patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("ci_test_player", ["wav"]),
|
||||
), patch.object(audio_feedback.subprocess, "Popen") as mock_popen:
|
||||
with (
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.object(
|
||||
audio_feedback,
|
||||
"_get_audio_player",
|
||||
return_value=("ci_test_player", ["wav"]),
|
||||
),
|
||||
patch.object(audio_feedback.subprocess, "Popen") as mock_popen,
|
||||
):
|
||||
# Call the function
|
||||
result = audio_feedback._play_sound_file("test.wav")
|
||||
|
||||
@@ -346,9 +369,11 @@ class TestAudioFeedback(unittest.TestCase):
|
||||
# Import the module first
|
||||
import vocalinux.ui.audio_feedback as audio_feedback
|
||||
|
||||
with patch.object(audio_feedback.shutil, "which", return_value=None), patch.object(
|
||||
audio_feedback.os.path, "exists", return_value=True
|
||||
), patch.dict("os.environ", {"GITHUB_ACTIONS": "true"}):
|
||||
with (
|
||||
patch.object(audio_feedback.shutil, "which", return_value=None),
|
||||
patch.object(audio_feedback.os.path, "exists", return_value=True),
|
||||
patch.dict("os.environ", {"GITHUB_ACTIONS": "true"}),
|
||||
):
|
||||
# First verify _get_audio_player returns None
|
||||
player, formats = audio_feedback._get_audio_player()
|
||||
self.assertIsNone(player)
|
||||
|
||||
@@ -21,17 +21,22 @@ class TestAutostartManager(unittest.TestCase):
|
||||
self.assertEqual(command, "/usr/bin/vocalinux --start-minimized")
|
||||
|
||||
def test_get_exec_command_falls_back_to_python_module(self):
|
||||
with patch("vocalinux.ui.autostart_manager.shutil.which", return_value=None), patch(
|
||||
"vocalinux.ui.autostart_manager.sys.executable", "/usr/bin/python3"
|
||||
), patch("vocalinux.ui.autostart_manager.sys.frozen", new=False, create=True):
|
||||
with (
|
||||
patch("vocalinux.ui.autostart_manager.shutil.which", return_value=None),
|
||||
patch("vocalinux.ui.autostart_manager.sys.executable", "/usr/bin/python3"),
|
||||
patch("vocalinux.ui.autostart_manager.sys.frozen", new=False, create=True),
|
||||
):
|
||||
command = autostart_manager.get_exec_command()
|
||||
self.assertEqual(command, "/usr/bin/python3 -m vocalinux.main --start-minimized")
|
||||
|
||||
def test_enable_disable_autostart_creates_and_removes_entry(self):
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
with patch.dict("os.environ", {"XDG_CONFIG_HOME": tmp_dir}, clear=False), patch(
|
||||
"vocalinux.ui.autostart_manager.shutil.which",
|
||||
return_value="/usr/bin/vocalinux",
|
||||
with (
|
||||
patch.dict("os.environ", {"XDG_CONFIG_HOME": tmp_dir}, clear=False),
|
||||
patch(
|
||||
"vocalinux.ui.autostart_manager.shutil.which",
|
||||
return_value="/usr/bin/vocalinux",
|
||||
),
|
||||
):
|
||||
enabled = autostart_manager.enable_autostart()
|
||||
self.assertTrue(enabled)
|
||||
|
||||
@@ -106,7 +106,6 @@ class TestImports:
|
||||
"""Test that all types can be imported."""
|
||||
from vocalinux.common_types import (
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
Protocol,
|
||||
RecognitionState,
|
||||
@@ -120,7 +119,6 @@ class TestImports:
|
||||
SpeechRecognitionManagerProtocol,
|
||||
TextInjectorProtocol,
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
Protocol,
|
||||
]
|
||||
|
||||
@@ -803,11 +803,14 @@ class TestIBusEngineMainEntrypoint(unittest.TestCase):
|
||||
"""Test --xml path prints engines XML and exits early."""
|
||||
from vocalinux.text_injection import ibus_engine
|
||||
|
||||
with patch.object(sys, "argv", ["ibus_engine.py", "--xml"]), patch.object(
|
||||
ibus_engine, "_get_engines_xml", return_value="<engines></engines>"
|
||||
) as mock_get_xml, patch("builtins.print") as mock_print, patch.object(
|
||||
ibus_engine.IBus, "init"
|
||||
) as mock_init:
|
||||
with (
|
||||
patch.object(sys, "argv", ["ibus_engine.py", "--xml"]),
|
||||
patch.object(
|
||||
ibus_engine, "_get_engines_xml", return_value="<engines></engines>"
|
||||
) as mock_get_xml,
|
||||
patch("builtins.print") as mock_print,
|
||||
patch.object(ibus_engine.IBus, "init") as mock_init,
|
||||
):
|
||||
result = ibus_engine.main()
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
@@ -820,11 +823,14 @@ class TestIBusEngineMainEntrypoint(unittest.TestCase):
|
||||
from vocalinux.text_injection import ibus_engine
|
||||
|
||||
mock_app = MagicMock()
|
||||
with patch.object(sys, "argv", ["ibus_engine.py", "--ibus"]), patch.object(
|
||||
ibus_engine, "IBUS_AVAILABLE", True
|
||||
), patch.object(ibus_engine.IBus, "init") as mock_init, patch.object(
|
||||
ibus_engine, "VocalinuxEngineApplication", return_value=mock_app
|
||||
) as mock_app_cls:
|
||||
with (
|
||||
patch.object(sys, "argv", ["ibus_engine.py", "--ibus"]),
|
||||
patch.object(ibus_engine, "IBUS_AVAILABLE", True),
|
||||
patch.object(ibus_engine.IBus, "init") as mock_init,
|
||||
patch.object(
|
||||
ibus_engine, "VocalinuxEngineApplication", return_value=mock_app
|
||||
) as mock_app_cls,
|
||||
):
|
||||
result = ibus_engine.main()
|
||||
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
@@ -583,9 +583,10 @@ class TestBackendFactory(unittest.TestCase):
|
||||
"""Test forcing pynput backend."""
|
||||
from vocalinux.ui.keyboard_backends import create_backend
|
||||
|
||||
with patch("vocalinux.ui.keyboard_backends.PYNPUT_AVAILABLE", True), patch(
|
||||
"vocalinux.ui.keyboard_backends.PynputKeyboardBackend"
|
||||
) as MockPynput:
|
||||
with (
|
||||
patch("vocalinux.ui.keyboard_backends.PYNPUT_AVAILABLE", True),
|
||||
patch("vocalinux.ui.keyboard_backends.PynputKeyboardBackend") as MockPynput,
|
||||
):
|
||||
mock_backend = MagicMock()
|
||||
MockPynput.return_value = mock_backend
|
||||
|
||||
@@ -598,9 +599,10 @@ class TestBackendFactory(unittest.TestCase):
|
||||
"""Test forcing evdev backend."""
|
||||
from vocalinux.ui.keyboard_backends import create_backend
|
||||
|
||||
with patch("vocalinux.ui.keyboard_backends.EVDEV_AVAILABLE", True), patch(
|
||||
"vocalinux.ui.keyboard_backends.EvdevKeyboardBackend"
|
||||
) as MockEvdev:
|
||||
with (
|
||||
patch("vocalinux.ui.keyboard_backends.EVDEV_AVAILABLE", True),
|
||||
patch("vocalinux.ui.keyboard_backends.EvdevKeyboardBackend") as MockEvdev,
|
||||
):
|
||||
mock_backend = MagicMock()
|
||||
MockEvdev.return_value = mock_backend
|
||||
|
||||
|
||||
@@ -109,8 +109,11 @@ class TestCheckDependencies(unittest.TestCase):
|
||||
|
||||
obj = _make_injector(DesktopEnvironment.WAYLAND)
|
||||
with patch("shutil.which", return_value=None):
|
||||
with self.assertRaises(RuntimeError):
|
||||
obj._check_dependencies()
|
||||
with patch(
|
||||
"vocalinux.text_injection.text_injector.is_ibus_available", return_value=False
|
||||
):
|
||||
with self.assertRaises(RuntimeError):
|
||||
obj._check_dependencies()
|
||||
|
||||
|
||||
class TestInjectText(unittest.TestCase):
|
||||
|
||||
@@ -69,13 +69,12 @@ class TestTrayIndicatorInitialization(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
# Set up mocks
|
||||
mock_speech_engine = MagicMock()
|
||||
@@ -124,13 +123,12 @@ class TestTrayIndicatorInitialization(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -192,13 +190,12 @@ class TestTrayIndicatorMenuOperations(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -232,13 +229,12 @@ class TestTrayIndicatorMenuOperations(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -294,13 +290,12 @@ class TestTrayIndicatorStateHandling(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -334,13 +329,12 @@ class TestTrayIndicatorStateHandling(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -374,13 +368,12 @@ class TestTrayIndicatorStateHandling(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -436,13 +429,12 @@ class TestTrayIndicatorSignalHandling(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
@@ -480,13 +472,12 @@ class TestTrayIndicatorSignalHandling(unittest.TestCase):
|
||||
from vocalinux.common_types import RecognitionState
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
with patch("vocalinux.ui.tray_indicator.logging"), patch(
|
||||
"vocalinux.ui.tray_indicator._resource_manager"
|
||||
) as mock_resource_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.ConfigManager"
|
||||
) as mock_config_manager, patch(
|
||||
"vocalinux.ui.tray_indicator.KeyboardShortcutManager"
|
||||
) as mock_keyboard_manager:
|
||||
with (
|
||||
patch("vocalinux.ui.tray_indicator.logging"),
|
||||
patch("vocalinux.ui.tray_indicator._resource_manager") as mock_resource_manager,
|
||||
patch("vocalinux.ui.tray_indicator.ConfigManager") as mock_config_manager,
|
||||
patch("vocalinux.ui.tray_indicator.KeyboardShortcutManager") as mock_keyboard_manager,
|
||||
):
|
||||
|
||||
from vocalinux.ui.tray_indicator import TrayIndicator
|
||||
|
||||
|
||||
@@ -90,10 +90,14 @@ class TestWhisperSupport:
|
||||
whisper_mock.load_model.return_value = model_mock
|
||||
|
||||
# Patch modules and mock the download method to avoid network/file operations
|
||||
with patch.dict(sys.modules, {"whisper": whisper_mock, "torch": torch_mock}), patch(
|
||||
"vocalinux.speech_recognition.recognition_manager."
|
||||
"SpeechRecognitionManager._download_whisper_model"
|
||||
) as _mock_download, patch("os.path.exists", return_value=True):
|
||||
with (
|
||||
patch.dict(sys.modules, {"whisper": whisper_mock, "torch": torch_mock}),
|
||||
patch(
|
||||
"vocalinux.speech_recognition.recognition_manager."
|
||||
"SpeechRecognitionManager._download_whisper_model"
|
||||
) as _mock_download,
|
||||
patch("os.path.exists", return_value=True),
|
||||
):
|
||||
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
|
||||
|
||||
# Create manager with Whisper engine and defer_download=False to trigger model loading
|
||||
@@ -132,23 +136,31 @@ class TestWhisperSupport:
|
||||
):
|
||||
from vocalinux.speech_recognition import recognition_manager as rm
|
||||
|
||||
with patch("os.makedirs"), patch("os.path.exists", return_value=True), patch(
|
||||
"os.path.getsize", return_value=40 * 1024 * 1024
|
||||
), patch("multiprocessing.cpu_count", return_value=4), patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("vulkan", "Intel GPU"),
|
||||
), patch(
|
||||
"vocalinux.utils.whispercpp_model_info.get_backend_display_name",
|
||||
return_value="Vulkan",
|
||||
), patch(
|
||||
"vocalinux.speech_recognition.recognition_manager.get_model_path",
|
||||
return_value="/tmp/mock-ggml-tiny.bin",
|
||||
), patch(
|
||||
"vocalinux.speech_recognition.recognition_manager._show_notification"
|
||||
) as notify_mock, patch.dict(
|
||||
os.environ,
|
||||
{"GGML_VULKAN": "1", "GGML_CUDA": "1"},
|
||||
clear=False,
|
||||
with (
|
||||
patch("os.makedirs"),
|
||||
patch("os.path.exists", return_value=True),
|
||||
patch("os.path.getsize", return_value=40 * 1024 * 1024),
|
||||
patch("multiprocessing.cpu_count", return_value=4),
|
||||
patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_compute_backend",
|
||||
return_value=("vulkan", "Intel GPU"),
|
||||
),
|
||||
patch(
|
||||
"vocalinux.utils.whispercpp_model_info.get_backend_display_name",
|
||||
return_value="Vulkan",
|
||||
),
|
||||
patch(
|
||||
"vocalinux.speech_recognition.recognition_manager.get_model_path",
|
||||
return_value="/tmp/mock-ggml-tiny.bin",
|
||||
),
|
||||
patch(
|
||||
"vocalinux.speech_recognition.recognition_manager._show_notification"
|
||||
) as notify_mock,
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{"GGML_VULKAN": "1", "GGML_CUDA": "1"},
|
||||
clear=False,
|
||||
),
|
||||
):
|
||||
manager = rm.SpeechRecognitionManager(
|
||||
engine="whisper_cpp", model_size="tiny", defer_download=False
|
||||
|
||||
@@ -261,9 +261,10 @@ class TestDetectCPUInfo(unittest.TestCase):
|
||||
|
||||
def test_detect_cpu_info_fallback_to_nproc(self):
|
||||
"""Test CPU detection falls back to nproc."""
|
||||
with patch("builtins.open", create=True, side_effect=Exception("No /proc/cpuinfo")), patch(
|
||||
"subprocess.run"
|
||||
) as mock_run:
|
||||
with (
|
||||
patch("builtins.open", create=True, side_effect=Exception("No /proc/cpuinfo")),
|
||||
patch("subprocess.run") as mock_run,
|
||||
):
|
||||
|
||||
mock_run.return_value = MagicMock(returncode=0, stdout="8\n")
|
||||
|
||||
@@ -275,8 +276,9 @@ class TestDetectCPUInfo(unittest.TestCase):
|
||||
|
||||
def test_detect_cpu_info_default_fallback(self):
|
||||
"""Test CPU detection returns default when all methods fail."""
|
||||
with patch("builtins.open", create=True, side_effect=Exception()), patch(
|
||||
"subprocess.run", side_effect=Exception()
|
||||
with (
|
||||
patch("builtins.open", create=True, side_effect=Exception()),
|
||||
patch("subprocess.run", side_effect=Exception()),
|
||||
):
|
||||
|
||||
from vocalinux.utils.whispercpp_model_info import detect_cpu_info
|
||||
@@ -299,11 +301,10 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
|
||||
def test_detect_compute_backend_prefers_vulkan(self):
|
||||
"""Test that detect_compute_backend prefers Vulkan."""
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_vulkan_support"
|
||||
) as mock_vulkan, patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_cuda_support"
|
||||
) as mock_cuda:
|
||||
with (
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
||||
):
|
||||
|
||||
mock_vulkan.return_value = (True, "Intel Arc GPU")
|
||||
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
||||
@@ -319,11 +320,10 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
|
||||
def test_detect_compute_backend_falls_back_to_cuda(self):
|
||||
"""Test that detect_compute_backend falls back to CUDA."""
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_vulkan_support"
|
||||
) as mock_vulkan, patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_cuda_support"
|
||||
) as mock_cuda:
|
||||
with (
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
||||
):
|
||||
|
||||
mock_vulkan.return_value = (False, None)
|
||||
mock_cuda.return_value = (True, "NVIDIA RTX 3080 (10GB)")
|
||||
@@ -339,13 +339,11 @@ class TestDetectComputeBackend(unittest.TestCase):
|
||||
|
||||
def test_detect_compute_backend_falls_back_to_cpu(self):
|
||||
"""Test that detect_compute_backend falls back to CPU."""
|
||||
with patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_vulkan_support"
|
||||
) as mock_vulkan, patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_cuda_support"
|
||||
) as mock_cuda, patch(
|
||||
"vocalinux.utils.whispercpp_model_info.detect_cpu_info"
|
||||
) as mock_cpu:
|
||||
with (
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_vulkan_support") as mock_vulkan,
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_cuda_support") as mock_cuda,
|
||||
patch("vocalinux.utils.whispercpp_model_info.detect_cpu_info") as mock_cpu,
|
||||
):
|
||||
|
||||
mock_vulkan.return_value = (False, None)
|
||||
mock_cuda.return_value = (False, None)
|
||||
|
||||
@@ -20,7 +20,7 @@ const faqCategories = [
|
||||
},
|
||||
{
|
||||
q: "What are the system requirements?",
|
||||
a: "You need Python 3.8+, a working microphone, and about 500MB of disk space. For GPU acceleration with whisper.cpp, you need Vulkan support (AMD, Intel, or NVIDIA).",
|
||||
a: "You need Python 3.9+, a working microphone, and about 500MB of disk space. For GPU acceleration with whisper.cpp, you need Vulkan support (AMD, Intel, or NVIDIA).",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -37,8 +37,8 @@ const distroConfig: Record<DistroSlug, DistroConfig> = {
|
||||
intro:
|
||||
"This Ubuntu guide is tuned for practical desktop use: terminals, browsers, IDEs, office suites, and chat apps. Vocalinux runs locally so your voice data never leaves your machine.",
|
||||
prerequisites: [
|
||||
"Ubuntu 22.04+ (or Ubuntu-based distro)",
|
||||
"Python 3.8+",
|
||||
"Ubuntu 22.04+ (or Ubuntu-based distro)",
|
||||
"Python 3.9+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo apt update && sudo apt install -y curl)",
|
||||
],
|
||||
@@ -68,8 +68,8 @@ const distroConfig: Record<DistroSlug, DistroConfig> = {
|
||||
intro:
|
||||
"Fedora is an excellent match for Vocalinux thanks to current Python stacks and up-to-date system libraries. This guide focuses on reliable setup and minimal friction.",
|
||||
prerequisites: [
|
||||
"Fedora Workstation 39+ (or Fedora-based distro)",
|
||||
"Python 3.8+",
|
||||
"Fedora Workstation 39+ (or Fedora-based distro)",
|
||||
"Python 3.9+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo dnf install -y curl)",
|
||||
],
|
||||
@@ -99,8 +99,8 @@ const distroConfig: Record<DistroSlug, DistroConfig> = {
|
||||
intro:
|
||||
"Arch users usually care about control and performance. This guide keeps setup lean while preserving compatibility across rolling-release desktop environments.",
|
||||
prerequisites: [
|
||||
"Arch Linux / Manjaro / EndeavourOS",
|
||||
"Python 3.8+",
|
||||
"Arch Linux / Manjaro / EndeavourOS",
|
||||
"Python 3.9+",
|
||||
"Working microphone",
|
||||
"curl installed (sudo pacman -Sy --needed curl)",
|
||||
],
|
||||
|
||||
@@ -858,7 +858,7 @@ export default function HomePage() {
|
||||
</h4>
|
||||
<ul className="space-y-2 text-sm text-muted-foreground">
|
||||
<li>• Ubuntu 22.04+ (or equivalent)</li>
|
||||
<li>• Python 3.8 or newer</li>
|
||||
<li>• Python 3.9 or newer</li>
|
||||
<li>• 4GB RAM (8GB for large models)</li>
|
||||
<li>• Microphone</li>
|
||||
<li>• X11 or Wayland display server</li>
|
||||
@@ -1134,7 +1134,7 @@ export default function HomePage() {
|
||||
question: "What are the system requirements?",
|
||||
answer: (
|
||||
<>
|
||||
Minimum: 4GB RAM, Python 3.8+, ~200MB disk space. 8GB+ RAM recommended for larger Whisper models. The default whisper.cpp tiny model (~39MB) works great on modest hardware. GPU acceleration is available via Vulkan for AMD, Intel, and NVIDIA GPUs.{" "}
|
||||
Minimum: 4GB RAM, Python 3.9+, ~200MB disk space. 8GB+ RAM recommended for larger Whisper models. The default whisper.cpp tiny model (~39MB) works great on modest hardware. GPU acceleration is available via Vulkan for AMD, Intel, and NVIDIA GPUs.{" "}
|
||||
<Link href="/install/" className="text-primary hover:underline">View full installation requirements</Link>.
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -11,7 +11,7 @@ const troubleshootingItems = [
|
||||
solutions: [
|
||||
"Ensure you ran the installer: curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash",
|
||||
"Check if the virtual environment is activated: source ~/.local/share/vocalinux/venv/bin/activate",
|
||||
"Verify Python version (3.8+ required): python3 --version",
|
||||
"Verify Python version (3.9+ required): python3 --version",
|
||||
"Try reinstalling: ./uninstall.sh && ./install.sh",
|
||||
],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user