From 58ed0d9e6c8a263f3fbeffb7e69ff06405fcd8bb Mon Sep 17 00:00:00 2001 From: Ralph Mayola Date: Fri, 21 Aug 2026 01:42:44 +0200 Subject: [PATCH] Attend le relachement des touches avant de coller Deux dictees ont ete transcrites correctement puis n'ont jamais atteint la zone de saisie, sans la moindre erreur : inject_text avait bien envoye ses touches. Cause : la hotkey Cmd+Ctrl+D etait encore enfoncee quand Zonza a simule Cmd+V. Le systeme a recu Cmd+Ctrl+V, que l'application cible ignore. Le code croyait avoir reussi. inject_text patiente donc jusqu'a ce qu'aucun modificateur physique ne soit enfonce (drapeaux Quartz), au plus 2 s. Au-dela, il colle quand meme : mieux vaut un collage douteux qu'une transcription perdue. Le delai du presse-papier passe de 0,05 a 0,12 s, 0,05 etant juste. Et surtout, le collage se TRACE desormais ("[collage] ok" ou l'avertissement) : sans cela, un echec futur resterait invisible dans le log, comme celui-ci l'a ete. L'attente est une unite pure et bornee (core.attendre, horloge et sommeil injectables). Detecteur verifie en conditions reelles : False au repos, retour immediat. 55 tests passent. Co-Authored-By: Claude Opus 5 (1M context) --- core.py | 21 +++++++++++++++++ engine.py | 33 +++++++++++++++++++++++--- tests/test_attente.py | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 tests/test_attente.py diff --git a/core.py b/core.py index 0bca809..8f4747e 100644 --- a/core.py +++ b/core.py @@ -13,6 +13,8 @@ CONFIG = { "hotkey": "++d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent) "sounds": True, "min_duration_s": 0.3, + "attente_modificateurs_s": 2.0, # on patiente avant de coller (cf inject_text) + "delai_presse_papier_s": 0.12, # 0.05 s etait juste : le presse-papier n'etait pas pret "max_duration_s": 120.0, # sécurité : arrêt auto après 2 min (fenêtre jamais figée) "sample_rate": 16000, "fps": 30, @@ -105,6 +107,25 @@ _PROPORTIONS = { } +def attendre(condition, delai_max_s, pas_s, dormir=None, horloge=None): + """Attend que `condition()` soit vraie, au plus `delai_max_s`. Rend True/False. + + Bornee volontairement : mieux vaut coller avec un modificateur encore enfonce + que perdre la transcription si une touche reste bloquee. `dormir` et `horloge` + sont injectables pour tester sans attendre reellement. + """ + import time as _t + dormir = dormir or _t.sleep + horloge = horloge or _t.monotonic + debut = horloge() + while True: + if condition(): + return True + if horloge() - debut >= delai_max_s: + return False + dormir(pas_s) + + def bar_metrics(bar_height): """Mesures de l'overlay pour une hauteur de barre donnee, en points.""" if bar_height <= 0: diff --git a/engine.py b/engine.py index e271189..806a873 100644 --- a/engine.py +++ b/engine.py @@ -11,7 +11,7 @@ import pyperclip import sounddevice as sd from pynput.keyboard import Controller, Key -from core import CONFIG, clean_transcript +from core import CONFIG, attendre, clean_transcript SOUNDS = { "start": "/System/Library/Sounds/Pop.aiff", @@ -35,15 +35,42 @@ def play_sound(name): pass # le son ne doit jamais casser le daemon +# Masque des modificateurs (ctrl, shift, alt, cmd) dans les drapeaux Quartz. +_MASQUE_MODIFICATEURS = 0x000F0000 + + +def modificateurs_enfonces(): + """True si au moins un modificateur physique est actuellement enfoncé.""" + try: + from Quartz import CGEventSourceFlagsState + return bool(CGEventSourceFlagsState(1) & _MASQUE_MODIFICATEURS) + except Exception: + return False # dans le doute, ne jamais bloquer le collage + + def inject_text(text): - """Copie le texte dans le presse-papier puis colle avec Cmd+V dans l'app active.""" + """Copie le texte dans le presse-papier puis colle avec Cmd+V dans l'app active. + + On attend d'abord que l'utilisateur ait relâché ses touches : s'il tient encore + Cmd+Ctrl+D quand on envoie Cmd+V, le système reçoit Cmd+Ctrl+V et l'application + cible ne colle rien — sans la moindre erreur de notre côté (constaté le + 2026-08-21 : deux dictées transcrites, jamais collées). + """ if not text: return pyperclip.copy(text) - time.sleep(0.05) # laisser le presse-papier se mettre à jour + libre = attendre( + lambda: not modificateurs_enfonces(), + CONFIG["attente_modificateurs_s"], + 0.03, + ) + if not libre: + print("[collage] modificateurs encore enfoncés — collage tenté quand même") + time.sleep(CONFIG["delai_presse_papier_s"]) # laisser le presse-papier se mettre à jour with _kbd.pressed(Key.cmd): _kbd.press("v") _kbd.release("v") + print("[collage] ok") class Recorder: diff --git a/tests/test_attente.py b/tests/test_attente.py new file mode 100644 index 0000000..172ac02 --- /dev/null +++ b/tests/test_attente.py @@ -0,0 +1,55 @@ +"""Attente d'une condition avant le collage. + +Bug du 2026-08-21 : deux dictees transcrites correctement n'ont jamais atteint la +zone de saisie. Aucune exception — les touches AVAIENT ete envoyees. Cause : la +hotkey Cmd+Ctrl+D etait encore enfoncee quand Zonza a simule Cmd+V ; le systeme a +recu Cmd+Ctrl+V, que l'application cible ignore. + +D'ou une attente du relachement des modificateurs avant de coller. Bornee : si une +touche reste enfoncee (clavier bloque), on colle quand meme plutot que de perdre le +texte. +""" +from core import attendre + + +def test_condition_deja_vraie_ne_dort_pas(): + sommeils = [] + assert attendre(lambda: True, 1.0, 0.05, dormir=sommeils.append, horloge=lambda: 0.0) is True + assert sommeils == [] + + +def test_condition_devient_vraie_apres_quelques_tours(): + tours = {"n": 0} + def cond(): + tours["n"] += 1 + return tours["n"] >= 3 + t = {"v": 0.0} + def horloge(): return t["v"] + def dormir(s): t["v"] += s + assert attendre(cond, 1.0, 0.05, dormir=dormir, horloge=horloge) is True + assert tours["n"] == 3 + + +def test_rend_False_au_bout_du_temps_imparti(): + t = {"v": 0.0} + def horloge(): return t["v"] + def dormir(s): t["v"] += s + assert attendre(lambda: False, 0.2, 0.05, dormir=dormir, horloge=horloge) is False + + +def test_le_temps_imparti_est_respecte(): + t = {"v": 0.0} + dormis = [] + def horloge(): return t["v"] + def dormir(s): t["v"] += s; dormis.append(s) + attendre(lambda: False, 0.2, 0.05, dormir=dormir, horloge=horloge) + assert sum(dormis) <= 0.25, "l'attente ne doit pas deborder" + + +def test_un_delai_nul_tente_quand_meme_une_fois(): + essais = {"n": 0} + def cond(): + essais["n"] += 1 + return False + attendre(cond, 0.0, 0.05, dormir=lambda s: None, horloge=lambda: 0.0) + assert essais["n"] >= 1