Initial ai typewriter implementation
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from threading import Lock
|
||||
|
||||
import keyboard
|
||||
|
||||
from .ai_client import AIClientError, ask_ai
|
||||
from .clipboard_capture import capture_selection
|
||||
from .config import AppConfig, load_config
|
||||
from .key_stepper import KeyStepper
|
||||
|
||||
LOG = logging.getLogger("ai_typewriter")
|
||||
|
||||
|
||||
class AITypewriterApp:
|
||||
def __init__(self, config: AppConfig) -> None:
|
||||
self.config = config
|
||||
self._busy = Lock()
|
||||
self._stepper: KeyStepper | None = None
|
||||
|
||||
def run(self) -> None:
|
||||
keyboard.add_hotkey(self.config.hotkey, self._handle_hotkey, suppress=False)
|
||||
LOG.info("Prêt. Raccourci: %s. Quitter: Ctrl+C dans ce terminal.", self.config.hotkey)
|
||||
keyboard.wait()
|
||||
|
||||
def _handle_hotkey(self) -> None:
|
||||
if not self._busy.acquire(blocking=False):
|
||||
LOG.warning("Requête déjà en cours, raccourci ignoré.")
|
||||
return
|
||||
try:
|
||||
self._capture_ask_and_step()
|
||||
finally:
|
||||
self._busy.release()
|
||||
|
||||
def _capture_ask_and_step(self) -> None:
|
||||
try:
|
||||
selected = capture_selection(self.config.copy_wait_seconds, self.config.restore_clipboard)
|
||||
LOG.info("Texte capturé: %d caractères.", len(selected))
|
||||
answer = ask_ai(selected, self.config)
|
||||
LOG.info("Réponse reçue: %d caractères. Mode dactylographie actif.", len(answer))
|
||||
if self._stepper is not None:
|
||||
self._stepper.stop()
|
||||
self._stepper = KeyStepper(answer, delay=self.config.type_delay_seconds)
|
||||
self._stepper.start()
|
||||
except (AIClientError, FileNotFoundError, ValueError) as exc:
|
||||
LOG.error("%s", exc)
|
||||
except Exception:
|
||||
LOG.exception("Erreur inattendue")
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description="AI Typewriter")
|
||||
parser.add_argument("--config", default="config.json", help="Chemin du fichier config.json")
|
||||
parser.add_argument("--debug", action="store_true", help="Logs détaillés")
|
||||
parser.add_argument("--ask", help="Mode test: envoie ce texte à l'IA et imprime la réponse, sans hook clavier")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = build_parser().parse_args(argv)
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if args.debug else logging.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s",
|
||||
)
|
||||
try:
|
||||
config = load_config(args.config)
|
||||
except Exception as exc:
|
||||
LOG.error("%s", exc)
|
||||
return 2
|
||||
|
||||
if args.ask is not None:
|
||||
try:
|
||||
print(ask_ai(args.ask, config))
|
||||
return 0
|
||||
except AIClientError as exc:
|
||||
LOG.error("%s", exc)
|
||||
return 1
|
||||
|
||||
AITypewriterApp(config).run()
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
Reference in New Issue
Block a user