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:
Ralph Mayola 2026-08-21 01:48:07 +02:00
parent 58ed0d9e6c
commit 5710cb69ce
3 changed files with 77 additions and 1 deletions

24
core.py
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):

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:

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