Zonza : dictée vocale locale macOS (MLX-Whisper + cercle pulsant)
Reconstruction propre de l'ancien projet Dictée. Bundle id neuf (cloud.mrtechlab.zonza), chemin de build unique vers /Applications (plus jamais de builds /tmp enregistrés dans LaunchServices). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""Point d'entrée : application macOS de barre de menu (rumps).
|
||||
|
||||
Possède la boucle d'événements Cocoa. Enregistre la hotkey globale, gère l'état du menu,
|
||||
lance le warmup du modèle en fond, et instancie le contrôleur + la fenêtre de pulsation.
|
||||
|
||||
La politique d'activation est forcée en « accessory » : Zonza vit dans la barre de menu,
|
||||
jamais dans le Dock — même quand la fenêtre de pulsation Cocoa s'affiche.
|
||||
"""
|
||||
import threading
|
||||
|
||||
import rumps
|
||||
from AppKit import NSApplication, NSApplicationActivationPolicyAccessory
|
||||
from PyObjCTools import AppHelper
|
||||
from pynput import keyboard
|
||||
|
||||
from controller import ZonzaController
|
||||
from core import CONFIG, validate_config
|
||||
from pulse_window import PulseWindow
|
||||
|
||||
_STATE_TITLES = {
|
||||
"idle": "🎙️",
|
||||
"recording": "🔴",
|
||||
"transcribing": "⏳",
|
||||
}
|
||||
|
||||
|
||||
class ZonzaApp(rumps.App):
|
||||
def __init__(self):
|
||||
super().__init__("🎙️", quit_button=None)
|
||||
self.pulse = PulseWindow.alloc().init()
|
||||
self.controller = ZonzaController(
|
||||
CONFIG, self.pulse, on_state=self._set_state,
|
||||
run_on_main=AppHelper.callAfter,
|
||||
)
|
||||
self.menu = [
|
||||
rumps.MenuItem("Sons", callback=self._toggle_sounds),
|
||||
None,
|
||||
rumps.MenuItem("Quitter", callback=self._quit),
|
||||
]
|
||||
self.menu["Sons"].state = CONFIG["sounds"]
|
||||
self._start_hotkey()
|
||||
self._warmup_async()
|
||||
|
||||
def _set_state(self, state):
|
||||
# appelé depuis des threads de fond (transcription) ; toucher le titre du menu
|
||||
# (AppKit) doit se faire sur le thread principal → on marshale via callAfter.
|
||||
AppHelper.callAfter(self._apply_title, state)
|
||||
|
||||
def _apply_title(self, state):
|
||||
self.title = _STATE_TITLES.get(state, "🎙️")
|
||||
|
||||
def _toggle_sounds(self, sender):
|
||||
CONFIG["sounds"] = not CONFIG["sounds"]
|
||||
sender.state = CONFIG["sounds"]
|
||||
|
||||
def _quit(self, _):
|
||||
rumps.quit_application()
|
||||
|
||||
def _start_hotkey(self):
|
||||
# pynput exécute ce callback sur SON thread d'écoute, pas le thread principal.
|
||||
# toggle() touche NSWindow/NSTimer (Cocoa) → on marshale sur le thread principal.
|
||||
def on_activate():
|
||||
AppHelper.callAfter(self.controller.toggle)
|
||||
self._listener = keyboard.GlobalHotKeys({CONFIG["hotkey"]: on_activate})
|
||||
self._listener.start()
|
||||
|
||||
def _warmup_async(self):
|
||||
def run():
|
||||
try:
|
||||
self.controller.transcriber.warmup()
|
||||
except Exception as e:
|
||||
print(f"[erreur] warmup modèle : {e}")
|
||||
rumps.notification("Zonza", "Modèle", f"Échec du chargement : {e}")
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
|
||||
def main():
|
||||
validate_config(CONFIG)
|
||||
# barre de menu uniquement, jamais d'icône Dock — même en lançant depuis les sources
|
||||
NSApplication.sharedApplication().setActivationPolicy_(
|
||||
NSApplicationActivationPolicyAccessory
|
||||
)
|
||||
ZonzaApp().run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user