zonza/docs/superpowers/specs/2026-08-23-zonza-multiplateforme-design.md
Ralph Mayola 711eace2bb Spec : rendre Zonza multiplateforme
Conception validee avec Ralph : noyau portable + une couche par systeme derriere
trois contrats explicites. L'implementation Cocoa est deplacee sans etre modifiee.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 17:39:56 +02:00

6.8 KiB
Raw Blame History

Zonza multiplateforme — conception

Date : 2026-08-23 But : faire tourner Zonza sur Mac Apple Silicon, Mac Intel, Windows avec NVIDIA et Windows sans GPU, sans rien casser de la version macOS en service.


1. Point de départ

Audit du 2026-08-23 : 55 % du code est déjà portable.

Fichier Lignes État
core.py 228 portable
controller.py 164 portable — aucun import Cocoa
audio_level.py 55 portable
engine.py 160 1 import Quartz, 4 appels afplay, et MLX
app.py 156 100 % Cocoa
pulse_window.py 268 100 % Cocoa

Trois obstacles réels :

  1. MLX cible le GPU Apple Silicon. Aucun équivalent ailleurs.
  2. L'interface est entièrement Cocoa (424 lignes).
  3. fcntl.flock n'existe pas sur Windows — le verrou d'instance unique, qui empêche le gel constaté le 2026-08-21, disparaîtrait en silence.

2. Approche retenue

Noyau portable + une couche par système, derrière des contrats explicites.

Écartées : réécrire toute l'interface en Qt (perd les contournements Tahoe durement acquis, risque de régression sur la machine en service) ; maintenir deux applications séparées (double surface de bugs, alors que deux jours viennent d'être passés à en corriger une seule).

L'approche retenue ne sacrifie rien : l'implémentation Cocoa est déplacée sans être modifiée, et le contrat qu'elle respecte existe déjà — c'est celui que FauxPulse implémente dans les tests.

3. Structure

zonza/
├── core.py           inchangé — pur
├── audio_level.py    inchangé — pur
├── controller.py     inchangé — ne parle qu'aux contrats
├── moteur/           choisir_transcripteur() · mlx.py · faster.py
├── interface/        choisir_interface() · cocoa/ · qt/
├── systeme/          choisir_systeme() · macos.py · windows.py
└── lanceur.py        point d'entrée commun

Contrats

Contrat Méthodes
Transcripteur warmup() · transcrire(audio) -> str
Overlay show() · hide() · set_level() · set_status() · rearmer_garde()
Systeme jouer_son() · modificateurs_enfonces() · raccourci_defaut() · chemin_log() · acquerir_verrou_instance()

acquerir_verrou_instance rejoint la couche système : fcntl.flock sur macOS, msvcrt.locking sur Windows.

4. Moteur de transcription

Détection, puis mémorisation :

Apple Silicon ?          → MLX
sinon, CUDA disponible ? → faster-whisper GPU
sinon                    → faster-whisper CPU

La décision est une fonction pure recevant (plateforme, processeur, cuda_disponible) ; l'interrogation de la machine est séparée. Les quatre configurations sont donc testables depuis un seul Mac.

Configuration Moteur Modèle Latence
Mac Apple Silicon MLX whisper-medium ~1,3 s (mesuré)
Windows + NVIDIA faster-whisper CUDA medium float16 12 s (à mesurer)
Mac Intel · Windows CPU faster-whisper CPU small int8 38 s (à mesurer)

medium est écarté sur CPU : 15 à 30 s tuerait l'usage.

Première étape de l'implémentation : mesurer réellement les deux dernières lignes. Le 2026-08-21, une mesure analogue a renversé l'intuition — large-v3-turbo s'est révélé PIRE que medium.

faster-whisper accepte initial_prompt avec la même sémantique : l'amorce de 47 termes survit sur les quatre configurations. Sur small, elle compense partiellement une moindre précision sur les noms propres — à mesurer, pas à espérer.

Réglages manuels : CONFIG["moteur"] et CONFIG["modele"] forcent le choix.

5. Interface

L'overlay Qt reprend le dessin à l'identique — QPainter pour NSBezierPath, QTimer pour NSTimer, fenêtre frameless, toujours au-dessus, translucide, WindowTransparentForInput. Icône de barre par QSystemTrayIcon.

La géométrie reste partagée : core.bar_metrics() ne connaît aucun système, les deux interfaces la lisent. Un changement de taille s'applique aux deux.

6. Couche système

macOS Windows
Son afplay winsound.PlaySound
Modificateurs Quartz CGEventSourceFlagsState GetAsyncKeyState (ctypes)
Journal ~/Library/Logs/Zonza.log %LOCALAPPDATA%\Zonza\Zonza.log
Raccourci Cmd+Ctrl+D Ctrl+Alt+D
Verrou fcntl.flock msvcrt.locking

pynput, pyperclip, sounddevice et numpy fonctionnent tels quels sur Windows : raccourci global, presse-papier et capture micro ne demandent aucun travail.

7. Installation

install.sh et install.ps1 créent l'environnement et n'installent que ce qui sert à la machine (mlx-whisper seulement sur Apple Silicon, PySide6 seulement où Qt est utilisé), puis posent le lancement au démarrage. build_app.sh reste inchangé pour le bundle Mac.

Un installeur double-clic est écarté : produire un .exe exige une machine Windows, absente ; et py2app/PyInstaller ont déjà été retirés de ce projet en juin parce qu'empaqueter les shaders Metal de MLX échouait.

8. Vérification

Contrainte assumée : aucune machine Windows n'est disponible pour vérifier.

Deux réponses.

Tests par contrat. Chaque contrat a UNE suite, rejouée contre TOUTES ses implémentations. Si l'overlay Qt oublie rearmer_garde, la suite rougit sans Windows. La détection matérielle étant pure, les quatre configurations sont couvertes depuis un seul Mac.

Commande de diagnostic, exécutée par Ralph sur la machine cible :

zonza --diagnostic

Sortie attendue :

Plateforme      : Windows 11 / x86_64 / CUDA absent
Moteur retenu   : faster-whisper CPU, modèle small int8
Micro           : ✓ 1064 blocs reçus en 1 s
Transcription   : ✓ 2,8 s — « Pousse le commit sur Gitea »
Presse-papier   : ✓
Raccourci       : ✓ Ctrl+Alt+D capté
Overlay         : ✓ affiché puis refermé
Verrou          : ✓ seconde instance refusée

Engagement : il ne sera jamais affirmé que Zonza fonctionne sous Windows. Seulement que les tests passent, que les contrats sont respectés et que le diagnostic est prêt. La preuve vient de la machine cible.

9. Acquis à ne pas perdre

Les correctifs des 21 et 22 août doivent survivre au port, et être testés dans chaque implémentation :

  • aucune opération audio sur le thread d'interface (le gel venait de là) ;
  • libération du flux audio avant toute réouverture (Zonza s'affamait elle-même) ;
  • attente du relâchement des modificateurs avant le collage ;
  • durée de vie maximale de l'overlay (210 s, 20 s en transcription) ;
  • alerte micro muet à 1,8 s ;
  • verrou d'instance unique.

10. Hors périmètre

Linux ; transcription en continu pendant la parole ; installeur double-clic ; synchronisation du vocabulaire entre machines.