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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user