first commit

This commit is contained in:
workD12
2026-09-11 19:47:15 +03:00
commit 32754a1fe2
56 changed files with 3836 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
"""Модуль 5: hotkeys — клавиши управления (событийные бэкенды).
Использование:
from modules.hotkeys import KeyEvents, get_backend
events = KeyEvents(on_talk_down=..., on_talk_up=..., on_stop=...,
on_repeat=..., on_quit=...)
backend = get_backend("console", events) # или "keys" (Windows / root-Linux)
backend.start()
"""
from __future__ import annotations
import os
from .base import KeyEvents
from .console import ConsoleBackend
from .keyboard_hook import KeyboardHookBackend
__all__ = ["KeyEvents", "ConsoleBackend", "KeyboardHookBackend", "get_backend"]
_BACKENDS = {
ConsoleBackend.name: ConsoleBackend,
KeyboardHookBackend.name: KeyboardHookBackend,
}
def get_backend(name: str, events: KeyEvents, **kwargs):
"""Фабрика бэкендов клавиш."""
if name == KeyboardHookBackend.name:
# Клавиши можно переопределить через переменные окружения/.env:
# HOTKEY_DIALOG (пробел-тумблер), HOTKEY_TALK, HOTKEY_STOP, HOTKEY_REPEAT
kwargs.setdefault("dialog", os.environ.get("HOTKEY_DIALOG", "space"))
kwargs.setdefault("talk", os.environ.get("HOTKEY_TALK", "num 0"))
kwargs.setdefault("stop_key", os.environ.get("HOTKEY_STOP", "esc"))
kwargs.setdefault("repeat", os.environ.get("HOTKEY_REPEAT", "enter"))
try:
backend_cls = _BACKENDS[name]
except KeyError:
raise ValueError(
f"Неизвестный бэкенд клавиш: {name!r}. Доступны: {sorted(_BACKENDS)}"
) from None
return backend_cls(events, **kwargs)
+30
View File
@@ -0,0 +1,30 @@
"""События клавиш — контракт между бэкендом ввода и ассистентом.
Бэкенд (console / Windows-хук) только ПЕРЕДАЁТ события; вся логика
реакции — в машине состояний (modules/assistant).
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable, Optional
@dataclass
class KeyEvents:
"""Набор колбэков ассистента.
on_dialog_toggle — ПРОБЕЛ: начать диалог (куи «готов к диалогу») или
завершить его (куи «до новых встреч»);
on_talk_down — клавиша «Слушай» НАЖАТА (push-to-talk, резервный режим);
on_talk_up — клавиша «Слушай» ОТПУЩЕНА;
on_stop — «Замолчи»: заглушить речь / отменить;
on_repeat — «Повтори»: последнюю фразу;
on_quit — выход из программы.
"""
on_talk_down: Callable[[], None]
on_talk_up: Callable[[], None]
on_stop: Callable[[], None]
on_repeat: Callable[[], None]
on_quit: Callable[[], None]
on_dialog_toggle: Optional[Callable[[], None]] = None
+100
View File
@@ -0,0 +1,100 @@
"""Консольный бэкенд клавиш — для разработки и теста в Linux.
Глобальный хук клавиатуры (библиотека keyboard) на Linux требует root,
поэтому здесь клавиши имитируются вводом в терминале:
Enter — нажать/отпустить «Слушай» (первое нажатие = начать запись,
второе = отпустить и обработать; аналог удержания кнопки);
s + Enter — «Замолчи»;
r + Enter — «Повтори»;
q + Enter — выход.
На Windows-ПК отец получит настоящие глобальные клавиши (keyboard_hook.py).
"""
from __future__ import annotations
import threading
from .base import KeyEvents
class ConsoleBackend:
name = "console"
def __init__(self, events: KeyEvents) -> None:
self._events = events
self._talk_held = False
self._running = False
def start(self) -> None:
import threading
self._running = True
thread = threading.Thread(target=self._loop, daemon=True)
thread.start()
def stop(self) -> None:
self._running = False
# --- внутреннее ------------------------------------------------------
def _loop(self) -> None:
# Одиночные клавиши БЕЗ Enter (readchar): работает без root, когда
# окно терминала в фокусе. Пробел = настоящий пробел, как на Windows.
try:
import readchar
use_readchar = True
except ImportError:
use_readchar = False # фолбэк: строки + Enter (старое поведение)
if use_readchar:
self._loop_readchar(readchar)
else:
self._loop_lines()
def _loop_readchar(self, readchar) -> None:
while self._running:
key = readchar.readkey()
if key in ("\r", "\n"):
continue # Enter игнорируем (не part of управления)
if key == " ":
self._events.on_dialog_toggle() # ПРОБЕЛ = тумблер диалога
elif key == "s":
self._events.on_stop()
elif key == "r":
self._events.on_repeat()
elif key in ("q", "\x03"): # q или Ctrl+C
self._events.on_quit()
return
# остальные клавиши игнорируются
def _loop_lines(self) -> None:
"""Фолбэк: строки через Enter (если readchar не установлен)."""
if self._events.on_dialog_toggle:
print(" (readchar не установлен: пробел имитируй 'd' + Enter; "
"pip install readchar — включит настоящий пробел)")
while self._running:
try:
line = input()
except EOFError: # stdin закрылся — выходим
self._events.on_quit()
return
cmd = line.strip().lower()
if cmd == "q":
self._events.on_quit()
return
if cmd in ("d", "space", "пробел"):
if self._events.on_dialog_toggle:
self._events.on_dialog_toggle()
elif cmd == "s":
self._events.on_stop()
elif cmd == "r":
self._events.on_repeat()
elif cmd == "":
# Enter без текста = нажатие/отпускание «Слушай» (push-to-talk)
if self._talk_held:
self._talk_held = False
self._events.on_talk_up()
else:
self._talk_held = True
self._events.on_talk_down()
+57
View File
@@ -0,0 +1,57 @@
"""Бэкенд глобальных клавиш на библиотеке keyboard — целевой для Windows.
Перехватывает клавиши ГЛОБАЛЬНО (окно может быть свёрнуто) — именно то,
что нужно отцу. Требования/ограничения библиотеки keyboard:
- Windows: работает без админ-прав;
- Linux: требует root (поэтому в разработке используем console-бэкенд);
- проект библиотеки не развивается с 2020 — если на Windows появятся
проблемы, заменяем на pynput (интерфейс KeyEvents менять не придётся).
Клавиши задаются в .env (HOTKEY_TALK / HOTKEY_STOP / HOTKEY_REPEAT);
по умолчанию: space / esc / enter. Имена — как в библиотеке keyboard
(numpad-клавиши подберём при настройке на Windows, напр. «num 0», «num enter»).
"""
from __future__ import annotations
from .base import KeyEvents
class KeyboardHookBackend:
name = "keys"
def __init__(self, events: KeyEvents, talk: str = "num 0",
stop_key: str = "esc", repeat: str = "enter",
dialog: str = "space") -> None:
self._events = events
self._talk = talk
self._stop_key = stop_key
self._repeat = repeat
self._dialog = dialog # ПРОБЕЛ: тумблер диалога
self._started = False
def start(self) -> None:
import keyboard # ленивый импорт: на Linux без root упадёт — см. console
if self._dialog and self._events.on_dialog_toggle:
keyboard.add_hotkey(self._dialog, self._events.on_dialog_toggle, suppress=True)
if self._talk:
keyboard.on_press_key(self._talk, self._talk_press)
keyboard.on_release_key(self._talk, self._talk_release)
keyboard.add_hotkey(self._stop_key, self._events.on_stop, suppress=True)
keyboard.add_hotkey(self._repeat, self._events.on_repeat, suppress=True)
self._started = True
def stop(self) -> None:
if self._started:
import keyboard
keyboard.unhook_all()
self._started = False
# --- внутреннее ------------------------------------------------------
def _talk_press(self, event) -> None: # noqa: ANN001
# Библиотека шлёт повторные down-события при удержании; лишние вызовы
# безопасны: машина состояний игнорирует on_talk_down в состоянии listening.
self._events.on_talk_down()
def _talk_release(self, event) -> None: # noqa: ANN001
self._events.on_talk_up()