Le peripherique audio ne peut plus geler l'interface
Le halo s'est fige une seconde fois, bloque sur « Je vous ecoute… », forme d'onde plate, process a 0 % CPU. Une autre application (Loom) tenait le micro : le stream.stop() de PortAudio s'est bloque. Or toggle() s'executait sur le thread principal Cocoa — toute l'interface a gele avec lui. Et le garde-fou pose le matin meme etait inoperant PAR CONSTRUCTION : c'est un NSTimer, il vit sur la boucle d'evenements que le blocage venait justement d'arreter. Un garde-fou qui depend de ce qu'il est cense sauver ne sauve rien. Desormais, le chemin appele depuis le thread principal ne touche PLUS JAMAIS au peripherique : ouverture et fermeture du flux partent en tache de fond, le thread principal ne fait que changer l'etat et rafraichir l'overlay. Deux tests verrouillent la regle avec un recorder qui leve si on l'appelle depuis le mauvais endroit. S'ajoute une garde COURTE (20 s) armee des l'arret : passe ce point il ne reste que la transcription, quelques secondes ; laisser 210 s de halo, c'est « fige » pour l'utilisateur meme si le code finit par se rattraper. Prouve en reproduisant la panne — un stop() qui ne rend jamais la main : l'interface continue de battre (6, 12, 24 battements) et le halo se ferme seul. Avant, le compteur serait reste fige. 69 tests passent. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+36
-27
@@ -18,13 +18,21 @@ from engine import Recorder, Transcriber, inject_text, play_sound
|
||||
class ZonzaController:
|
||||
"""Machine à états pilotée par toggle(). on_state : callback(str) pour l'UI (menu)."""
|
||||
|
||||
def __init__(self, config, pulse_window, on_state=None, run_on_main=None):
|
||||
def __init__(self, config, pulse_window, on_state=None, run_on_main=None,
|
||||
lancer_tache=None):
|
||||
self.config = config
|
||||
self.pulse = pulse_window
|
||||
self.on_state = on_state or (lambda s: None)
|
||||
# run_on_main : marshale un appel sur le thread principal Cocoa (requis pour l'UI).
|
||||
# Par défaut exécute directement (utile pour les tests sans boucle Cocoa).
|
||||
self.run_on_main = run_on_main or (lambda fn: fn())
|
||||
# lancer_tache : execute un travail HORS du thread principal. Tout ce qui
|
||||
# touche au peripherique audio passe par la — PortAudio peut bloquer
|
||||
# indefiniment si une autre application accapare le micro, et un blocage
|
||||
# sur le thread principal gele TOUTE l'interface Cocoa (halo compris).
|
||||
self.lancer_tache = lancer_tache or (
|
||||
lambda fn: threading.Thread(target=fn, daemon=True).start()
|
||||
)
|
||||
self.recorder = Recorder(config["sample_rate"], on_block=self._on_audio_block)
|
||||
self.transcriber = Transcriber(config["model"], config["language"])
|
||||
self.recording = False
|
||||
@@ -52,20 +60,25 @@ class ZonzaController:
|
||||
self._stop_and_process()
|
||||
|
||||
def _start(self):
|
||||
self._smoothed_level = 0.0
|
||||
self.recording = True
|
||||
self.pulse.show()
|
||||
self.pulse.set_status("Je vous écoute…")
|
||||
play_sound("start")
|
||||
self.on_state("recording")
|
||||
self._arm_safety_timer()
|
||||
self.lancer_tache(self._ouvrir_flux)
|
||||
|
||||
def _ouvrir_flux(self):
|
||||
"""Ouvre le micro. Hors thread principal : peut bloquer (cf __init__)."""
|
||||
try:
|
||||
self._smoothed_level = 0.0
|
||||
self.recorder.start()
|
||||
self.recording = True
|
||||
self.pulse.show()
|
||||
self.pulse.set_status("Je vous écoute…")
|
||||
play_sound("start")
|
||||
self.on_state("recording")
|
||||
self._arm_safety_timer()
|
||||
except Exception as e:
|
||||
print(f"[erreur] démarrage enregistrement : {e}")
|
||||
play_sound("error")
|
||||
self.recording = False
|
||||
self.pulse.hide()
|
||||
self._cancel_safety_timer()
|
||||
self.run_on_main(self.pulse.hide)
|
||||
self.on_state("idle")
|
||||
|
||||
def _arm_safety_timer(self):
|
||||
@@ -90,29 +103,25 @@ class ZonzaController:
|
||||
self.run_on_main(self.toggle)
|
||||
|
||||
def _stop_and_process(self):
|
||||
"""Appelé depuis le thread principal : ne touche PAS au périphérique audio."""
|
||||
self.recording = False
|
||||
self._cancel_safety_timer()
|
||||
self.pulse.set_status("Transcription…")
|
||||
self.pulse.rearmer_garde(self.config["duree_max_transcription_s"])
|
||||
self.on_state("transcribing")
|
||||
self.lancer_tache(self._fermer_et_transcrire)
|
||||
|
||||
def _fermer_et_transcrire(self):
|
||||
"""Ferme le flux, transcrit, colle. Hors thread principal : `recorder.stop()`
|
||||
peut bloquer si le micro a été accaparé par une autre application — c'est
|
||||
précisément ce qui figeait le halo quand cet appel vivait sur le thread
|
||||
principal (2026-08-21)."""
|
||||
try:
|
||||
audio = self.recorder.stop()
|
||||
play_sound("done")
|
||||
if not is_long_enough(len(audio), self.config["sample_rate"], self.config["min_duration_s"]):
|
||||
self.pulse.hide()
|
||||
self.on_state("idle")
|
||||
if not is_long_enough(len(audio), self.config["sample_rate"],
|
||||
self.config["min_duration_s"]):
|
||||
return
|
||||
# la fenetre reste affichee : la bulle porte l'etape en cours, sinon
|
||||
# l'ecran se vide pendant les secondes de calcul et on croit a un plantage.
|
||||
self.pulse.set_status("Transcription…")
|
||||
self.on_state("transcribing")
|
||||
threading.Thread(target=self._transcribe_and_inject, args=(audio,), daemon=True).start()
|
||||
except Exception as e:
|
||||
print(f"[erreur] arrêt enregistrement : {e}")
|
||||
play_sound("error")
|
||||
self.pulse.hide()
|
||||
self.on_state("idle")
|
||||
|
||||
def _transcribe_and_inject(self, audio):
|
||||
# thread de fond : transcription lente + collage
|
||||
try:
|
||||
text = self.transcriber.transcribe(audio)
|
||||
if not text:
|
||||
return
|
||||
@@ -123,7 +132,7 @@ class ZonzaController:
|
||||
print(f"[erreur] collage échoué ({e}) — texte dans le presse-papier, fais Cmd+V")
|
||||
play_sound("error")
|
||||
except Exception as e:
|
||||
print(f"[erreur] transcription : {e}")
|
||||
print(f"[erreur] arrêt ou transcription : {e}")
|
||||
play_sound("error")
|
||||
finally:
|
||||
self.run_on_main(self.pulse.hide)
|
||||
|
||||
Reference in New Issue
Block a user