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) <noreply@anthropic.com>
This commit is contained in:
Ralph Mayola 2026-08-21 01:42:44 +02:00
parent f8c30e0402
commit 58ed0d9e6c
3 changed files with 106 additions and 3 deletions

21
core.py
View File

@ -13,6 +13,8 @@ CONFIG = {
"hotkey": "<cmd>+<ctrl>+d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent) "hotkey": "<cmd>+<ctrl>+d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent)
"sounds": True, "sounds": True,
"min_duration_s": 0.3, "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) "max_duration_s": 120.0, # sécurité : arrêt auto après 2 min (fenêtre jamais figée)
"sample_rate": 16000, "sample_rate": 16000,
"fps": 30, "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): def bar_metrics(bar_height):
"""Mesures de l'overlay pour une hauteur de barre donnee, en points.""" """Mesures de l'overlay pour une hauteur de barre donnee, en points."""
if bar_height <= 0: if bar_height <= 0:

View File

@ -11,7 +11,7 @@ import pyperclip
import sounddevice as sd import sounddevice as sd
from pynput.keyboard import Controller, Key from pynput.keyboard import Controller, Key
from core import CONFIG, clean_transcript from core import CONFIG, attendre, clean_transcript
SOUNDS = { SOUNDS = {
"start": "/System/Library/Sounds/Pop.aiff", "start": "/System/Library/Sounds/Pop.aiff",
@ -35,15 +35,42 @@ def play_sound(name):
pass # le son ne doit jamais casser le daemon 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): 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: if not text:
return return
pyperclip.copy(text) 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): with _kbd.pressed(Key.cmd):
_kbd.press("v") _kbd.press("v")
_kbd.release("v") _kbd.release("v")
print("[collage] ok")
class Recorder: class Recorder:

55
tests/test_attente.py Normal file
View File

@ -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