L'overlay ne peut plus rester fige a l'ecran

Le halo est reste affiche indefiniment. Depuis la refonte du design, la fenetre ne
se ferme plus a l'arret de l'enregistrement mais apres le collage : tout blocage
dans cette chaine — transcription qui traine, thread mort, bug en amont — la laisse
a l'ecran, et l'utilisateur n'a aucun moyen de s'en debarrasser.

La fenetre porte desormais sa propre duree de vie : une minuterie armee dans show()
la ferme quoi qu'il arrive, et hide() l'annule au passage normal. Le garde-fou ne
depend d'AUCUN code en amont, c'est tout l'interet.

La limite doit couvrir le pire cas reel — un enregistrement de duree maximale SUIVI
de sa transcription — sinon on remplacerait un halo fige par un halo qui s'evapore
pendant que Zonza travaille. validate_config refuse desormais une valeur trop courte
(210 s pour 120 s d'enregistrement + 30 s de marge minimale).

Prouve en conditions reelles, limite abaissee a 3 s : visible a 1,5 s (pas de
fermeture prematuree), fermee a 4 s et 5,5 s.

60 tests passent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-21 01:48:07 +02:00
co-authored by Claude Opus 5
parent 58ed0d9e6c
commit 5710cb69ce
3 changed files with 77 additions and 1 deletions
+23 -1
View File
@@ -15,7 +15,10 @@ CONFIG = {
"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,
# Duree de vie MAXIMALE de l'overlay, garde-fou contre un halo fige (2026-08-21).
# Doit couvrir un enregistrement maximal PUIS sa transcription.
"duree_max_overlay_s": 210.0, # sécurité : arrêt auto après 2 min (fenêtre jamais figée)
"sample_rate": 16000,
"fps": 30,
# --- overlay « barre de dictee » (bas de l'ecran) ---
@@ -133,6 +136,20 @@ def bar_metrics(bar_height):
return {cle: bar_height * f for cle, f in _PROPORTIONS.items()}
# Marge minimale a laisser pour transcrire apres un enregistrement de duree maximale.
MARGE_TRANSCRIPTION_S = 30.0
def duree_vie_overlay_coherente(config):
"""La limite de vie de l'overlay couvre-t-elle le pire cas reel ?
Pire cas : un enregistrement qui va jusqu'a max_duration_s, suivi de sa
transcription. Une limite trop courte fermerait le halo PENDANT que Zonza
travaille encore — on remplacerait un halo fige par un halo qui s'evapore.
"""
return config["duree_max_overlay_s"] >= config["max_duration_s"] + MARGE_TRANSCRIPTION_S
def validate_config(config):
"""Valide la config ; lève ValueError si une valeur est aberrante."""
if config["min_duration_s"] < 0:
@@ -141,6 +158,11 @@ def validate_config(config):
raise ValueError("sample_rate doit être > 0")
if not config["hotkey"]:
raise ValueError("hotkey ne peut pas être vide")
if not duree_vie_overlay_coherente(config):
raise ValueError(
"duree_max_overlay_s doit couvrir max_duration_s + "
f"{MARGE_TRANSCRIPTION_S} s de transcription"
)
def is_long_enough(num_samples, sample_rate, min_duration_s):
+14
View File
@@ -175,6 +175,7 @@ class PulseWindow(NSObject):
self._window = None
self._view = None
self._timer = None
self._garde = None # ferme l'overlay quoi qu'il arrive (cf show)
return self
def _window_size(self):
@@ -229,12 +230,25 @@ class PulseWindow(NSObject):
self._timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
1.0 / float(CONFIG["fps"]), self, "_tick:", None, True
)
# Garde-fou : la fenêtre porte sa propre durée de vie. Si le contrôleur
# échoue à la refermer — transcription bloquée, thread mort, bug en amont —
# elle se ferme seule plutôt que de rester figée à l'écran (2026-08-21).
self._garde = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(
float(CONFIG["duree_max_overlay_s"]), self, "_expiration:", None, False
)
def _expiration_(self, timer):
print("[garde] overlay ouvert trop longtemps — fermeture forcée")
self.hide()
def hide(self):
"""Arrête l'animation et masque la fenêtre. Thread principal."""
if self._timer is not None:
self._timer.invalidate()
self._timer = None
if self._garde is not None:
self._garde.invalidate()
self._garde = None
if self._window is not None:
self._window.orderOut_(None)
with self._lock:
+40
View File
@@ -0,0 +1,40 @@
"""Durée de vie maximale de l'overlay.
Bug du 2026-08-21 : le halo est resté figé à l'écran. Depuis la refonte du design,
la fenêtre ne se ferme plus à l'arrêt de l'enregistrement mais après le collage —
donc tout blocage dans cette chaîne la laisse affichée indéfiniment.
La fenêtre porte désormais sa propre limite : quoi qu'il arrive en amont, elle se
referme seule. Ce test verrouille la COHÉRENCE de la limite, qui doit couvrir le
pire cas réel — un enregistrement de durée maximale SUIVI d'une transcription.
"""
import pytest
from core import CONFIG, duree_vie_overlay_coherente
def test_la_limite_couvre_un_enregistrement_maximal_plus_la_transcription():
assert duree_vie_overlay_coherente(CONFIG) is True
def test_une_limite_plus_courte_que_l_enregistrement_est_refusee():
mauvais = dict(CONFIG, duree_max_overlay_s=CONFIG["max_duration_s"] - 1)
assert duree_vie_overlay_coherente(mauvais) is False
def test_une_limite_sans_marge_pour_transcrire_est_refusee():
# juste egale a la duree d'enregistrement : la transcription serait coupee
serre = dict(CONFIG, duree_max_overlay_s=CONFIG["max_duration_s"])
assert duree_vie_overlay_coherente(serre) is False
def test_validate_config_rejette_une_limite_incoherente():
from core import validate_config
mauvais = dict(CONFIG, duree_max_overlay_s=10.0)
with pytest.raises(ValueError):
validate_config(mauvais)
def test_la_config_livree_est_coherente():
from core import validate_config
validate_config(CONFIG) # ne doit pas lever