La seule personne qui a commenté mes articles ici l'a fait sur exactement ça : « le coup du F6 enfoncé, je parle, je relâche, ça colle direct ». Cinq articles publiés, un seul commentateur, et il tombe pile sur le mécanisme que je pensais le plus trivial du produit. Fa qu'on va le creuser.
Parce que c'est justement le morceau que j'ai le plus sous-estimé au début. Un raccourci global sur Windows, ça sonne comme un if keydown: enregistre suivi d'un if keyup: arrête. Dans les faits, entre le moment où tu tiens une touche et le moment où le texte atterrit au bon endroit, il y a une demi-douzaine de pièges que j'ai découverts un par un, en production, pas dans un tutoriel.
Toutes les touches ne font pas un bon raccourci global
Le premier piège, c'est de choisir la touche. Une lettre seule (« j » par exemple) vole littéralement la frappe à toutes les applications ouvertes, parce que le raccourci est enregistré en mode suppression (suppress=True) pour que la touche ne tape pas un caractère parasite dans ton éditeur : tape « j » n'importe où pendant que l'app tourne, et plus rien ne s'écrit à cet endroit.
Les touches de fonction (F1 à F24) sont le compromis le moins pire : elles n'écrivent aucun caractère, donc elles ne détruisent pas la frappe. Libres pour autant, non : F1 ouvre l'aide, F5 rafraîchit, F11 passe en plein écran, F12 ouvre les outils de développement. Et comme le raccourci est enregistré en mode suppression, la touche choisie est réellement volée aux autres applications tant que l'app tourne. F6 est le défaut parce que c'est une des moins chargées, pas parce qu'elle est libre, et elle se change avec --hotkey. Le code refuse une config bancale au démarrage plutôt que d'échouer en silence trois clics plus tard :
single_function_key = len(hotkey_keys) == 1 and hotkey_keys[0] in FUNCTION_KEYS
if len(hotkey_keys) < 2 and not single_function_key:
raise ValueError(f"Hotkey must be a combo like ctrl+space or a function key like f6, got: {hotkey!r}")
if len(hotkey_keys) >= 2 and not any(key in MODIFIER_KEYS for key in hotkey_keys):
raise ValueError(f"Hotkey combo must include a modifier key (ctrl/alt/shift/windows), got: {hotkey!r}")
if cancel in hotkey_keys:
raise ValueError(f"Cancel key must not overlap the push-to-talk hotkey, got cancel={cancel!r} hotkey={hotkey!r}")
Trois règles, trois catégories de bug évitées avant même que l'app démarre : une touche seule qui n'est pas une touche de fonction, une combinaison sans modificateur (ctrl, alt, shift ou windows), et une touche d'annulation qui chevauche le raccourci lui-même. Ce dernier cas, je l'ai vécu en test : si la touche d'annulation partage une touche avec le hotkey, appuyer pour annuler peut redéclencher l'enregistrement au lieu de le couper. Mieux vaut planter au démarrage avec un message clair que planter en silence pendant que tu dictes.
Le relâchement de touche ment, dans les deux sens
J'ai déjà raconté ici le bug qui m'a pris deux soirs : l'événement de relâchement de Windows qui ne se déclenche carrément jamais, à cause d'un changement de focus ou d'un autre hook clavier actif en même temps. Le fix, c'était de ne jamais faire confiance à l'événement et de revérifier l'état physique du clavier avec un timer.
Ce que je n'avais pas encore raconté, c'est le problème inverse : il arrive qu'un relâchement parasite passe alors que la touche est encore enfoncée, typiquement pendant un changement de focus qui dure une fraction de seconde, ou quand un autre hook clavier réinjecte des événements. Sans anti-rebond, ce faux signal coupe l'enregistrement en plein milieu d'un mot, et la phrase ressort en deux clips séparés. Le même mécanisme règle les deux sens du problème : un timer qui attend un court délai, puis qui re-questionne le clavier avant de vraiment couper.
Le timer, je l'ai déjà montré dans l'article sur ce bug. Ce qui compte ici, c'est ce qu'il va vérifier avant de couper :
def _stop_if_hotkey_inactive(self):
if any(keyboard.is_pressed(key) for key in self.hotkey_keys):
return
self.stop_recording()
Concrètement : le relâchement ne déclenche pas l'arrêt, il déclenche une question posée un peu plus tard. Si la touche est encore enfoncée, on continue. Sinon, on coupe. L'événement Windows n'est pas la vérité, juste une invitation à aller la vérifier.
L'indicateur visuel peut voler ta fenêtre active
Le piège le plus vicieux, c'est celui-là, parce qu'il ne casse rien de façon bruyante : il colle juste le texte au mauvais endroit. Quand Tania enregistre, une petite fenêtre d'état apparaît à l'écran. Si cette fenêtre devient la fenêtre active au moment du collage, le raccourci de collage part vers elle. Et comme elle n'a aucune zone de saisie, le texte ne va nulle part : ton éditeur n'a rien reçu et ta transcription est perdue sans un seul message d'erreur. Sur Windows, ça se règle au niveau du style de la fenêtre, pas dans la logique applicative :
GWL_EXSTYLE = -20
WS_EX_NOACTIVATE = 0x08000000
WS_EX_TOOLWINDOW = 0x00000080
cur = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, cur | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW)
WS_EX_NOACTIVATE dit à Windows que cette fenêtre n'a pas le droit de prendre le focus, jamais. WS_EX_TOOLWINDOW la sort en plus de l'Alt+Tab et de la barre des tâches. Sans ces drapeaux, l'indicateur a l'air anodin à l'œil, mais plus rien ne l'empêche de prendre le focus quand il apparaît. Détail qui m'a coûté un moment de doute : le style étendu se pose sur une fenêtre déjà créée, alors je le réapplique à chaque réaffichage plutôt qu'une seule fois à l'initialisation.
Les modificateurs fantômes
Dernier piège, plus discret : si tu tiens ctrl ou shift en parlant (un réflexe si t'étais en train de sélectionner du texte juste avant), ce modificateur peut encore être considéré comme enfoncé au moment où le code envoie ctrl+v pour coller. Résultat : le collage se transforme en autre chose selon l'application active. Le fix, c'est de relâcher explicitement les trois modificateurs avant d'envoyer le collage :
keyboard.release("ctrl")
keyboard.release("shift")
keyboard.release("alt")
keyboard.send("ctrl+v")
Trois lignes qui coûtent rien, dans le chemin de collage par presse-papier. Ça ne défait pas la physique, si tu tiens vraiment la touche elle peut se ré-annoncer aussitôt, mais ça règle le cas courant du modificateur resté collé dans l'état du clavier.
Ce que le push-to-talk règle vraiment, et ce qu'il règle pas
Il y a un gain qui n'est dans aucune des lignes ci-dessus, mais dans la forme même du geste. Un clip borné par un humain qui tient une touche n'a pas de longue traîne de silence au début ni à la fin. C'est un des terrains où les modèles de transcription hallucinent le plus, largement documenté sur Whisper, ces bouts de phrase (« merci », « sous-titres réalisés par... ») qui sortent de nulle part sur du silence pur. Un mode toggle ou une écoute continue avale forcément du silence et du bruit ambiant, donc ça a besoin d'un détecteur d'activité vocale pour compenser. Le push-to-talk transfère cette décision à l'utilisateur, qui la prend mieux qu'un seuil automatique.
Ça reste un arbitrage, pas une victoire absolue. Tenir une touche pendant dix minutes de dictée continue, personne fait ça, fa que ce mode ne convient juste pas à la dictée longue. Et je vais le dire clairement parce que ça fait plusieurs articles que je le repousse : j'ai un protocole de banc d'essai pour mesurer un vrai taux d'erreur de transcription, et il n'a toujours pas tourné. Zéro chiffre de précision à montrer aujourd'hui, ici comme ailleurs dans la série. Le jour où ça tourne, ce sera un article à part, pas une ligne glissée dans celui-ci.
Le code des pièges ci-dessus est dans app.py, ouvert en MIT sur GitHub. Si vous avez déjà câblé un raccourci global sur Windows ou ailleurs, qu'est-ce qui a fini par piétiner un raccourci existant chez vous ? Je cherche encore les cas que j'ai pas prévus.
Top comments (0)