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:
parent
58ed0d9e6c
commit
5710cb69ce
24
core.py
24
core.py
@ -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):
|
||||
|
||||
@ -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
tests/test_garde_overlay.py
Normal file
40
tests/test_garde_overlay.py
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user