"""Assemblage commun aux deux lanceurs (`app.py` Cocoa, `lanceur_qt.py` Qt/Windows). Avant ce module, `app.py` était le SEUL endroit qui savait choisir le système, prendre le verrou d'instance, construire le transcripteur et l'overlay, puis câbler le tout dans un `ZonzaController`. Un second lanceur qui recopierait cette séquence créerait deux copies vouées à diverger en silence — exactement le défaut déjà résorbé entre `engine.py` et `systeme/macos.py` (cf journal du projet). Les deux lanceurs appellent donc les deux fonctions d'ici ; ce qui reste propre à chacun (boucle d'événements, icône de barre, menu) reste dans `app.py` et `lanceur_qt.py`. Tout est injectable (`plateforme`, `run_on_main`...) pour rester testable sans jamais ouvrir de fenêtre ni de micro : la construction du transcripteur charge un vrai modèle, donc les tests injectent un `moteur.detecter_moteur`/ `moteur.choisir_transcripteur` doublés plutôt que de laisser tourner le vrai. """ import os import moteur from controller import ZonzaController from core import CONFIG, modele_retenu, validate_config from interface import classe_overlay_pour from systeme import choisir_systeme def acquerir_verrou(systeme, chemin_verrou=None): """Prend le verrou d'instance unique ; rend le handle, ou None si pris. Une seule instance à la fois : sinon deux modèles en RAM et la même hotkey captée deux fois (cf systeme.acquerir_verrou_instance). Le chemin est injectable : les tests et les essais manuels d'un lanceur doivent pouvoir viser un fichier qui n'est PAS celui de l'instance déjà en cours de l'utilisateur. """ chemin = chemin_verrou or os.path.join(systeme.dossier_donnees(), "instance.lock") return systeme.acquerir_verrou_instance(chemin) def construire(plateforme=None, run_on_main=None, on_state=None, detecter_moteur=None, choisir_transcripteur=None): """Construit le système, le transcripteur, l'overlay et le contrôleur. Ne prend PAS le verrou (c'est le rôle d'`acquerir_verrou`, appelée à part par le lanceur, qui décide seul de ce qu'il fait d'un refus). Ne valide la config qu'une fois, ici, pour que les deux lanceurs en bénéficient sans avoir à y penser. `detecter_moteur`/`choisir_transcripteur` sont injectables : ils permettent aux tests de construire l'assemblage entier sans charger un vrai modèle de transcription (opération lourde, et qui échouerait sans matériel/poids téléchargés). """ detecter_moteur = detecter_moteur or moteur.detecter_moteur choisir_transcripteur = choisir_transcripteur or moteur.choisir_transcripteur validate_config(CONFIG) systeme = choisir_systeme(plateforme) moteur_nom = detecter_moteur() transcripteur = choisir_transcripteur( nom=moteur_nom, modele=modele_retenu(moteur_nom, CONFIG["model"]), langue=CONFIG["language"], amorce=CONFIG["initial_prompt"], ) overlay = classe_overlay_pour(plateforme)() controller = ZonzaController( CONFIG, overlay, transcripteur, systeme, on_state=on_state, run_on_main=run_on_main, ) return { "systeme": systeme, "transcripteur": transcripteur, "overlay": overlay, "controller": controller, }