Commit Graph

35 Commits

Author SHA1 Message Date
bfafc6a48c Le masque des modificateurs couvre enfin Command, et jamais Caps Lock
Deux defauts trouves en rendant la sonde injectable.

1. Le masque etait ecrit 0x000F0000 A LA MAIN. Il couvrait Shift, Control et Alt
   mais PAS Command (0x00100000) — pourtant la touche du raccourci de Zonza.
   Maintenir Cmd seul passait donc inapercu. Le masque est desormais COMPOSE
   depuis les constantes nommees de Quartz : un masque calcule de tete se trompe
   en silence, compose il ne peut plus.

2. Le test lisait le CLAVIER PHYSIQUE (`modificateurs_enfonces() is False`). Son
   resultat dependait des mains de l'utilisateur pendant la suite — un echec
   inreproductible a ete observe ce jour, puis 17 executions vertes. La sonde est
   rendue injectable, comme SystemeWindows l'avait deja, et les quatre
   modificateurs sont eprouves un par un. Un test verifie que la sonde reelle rend
   un booleen sans lever, SANS affirmer sa valeur.

Le repli hors Quartz excluait aussi le verrouillage majuscule : actif, il aurait
fait attendre 2 s avant CHAQUE collage, en permanence.

195 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 20:04:52 +02:00
aa63e94239 Un lanceur Qt pour Windows, et un assemblage partage
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 20:00:03 +02:00
3e269dfb5e Le diagnostic eprouve le micro au debit reel de l'application
16000 etait code en dur dans trois endroits de diagnostic.py alors que
CONFIG["sample_rate"] existe. La valeur coincide aujourd'hui, donc rien ne
cassait — mais si ce reglage changeait, le diagnostic aurait teste le micro et la
synthese a un debit different de celui du pipeline reel, et aurait cesse
SILENCIEUSEMENT d'etre representatif. Un diagnostic qui ne mesure pas ce qu'on
croit est pire qu'aucun diagnostic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 19:32:08 +02:00
205424f68a Ajoute la couche Windows et la commande de diagnostic 2026-08-23 19:26:12 +02:00
99376a8e12 Le test exerce enfin l'annulation de la garde, dans les deux interfaces
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:16:41 +02:00
82e0e16393 Ajoute l'overlay Qt, pour les plateformes sans Cocoa
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 19:05:25 +02:00
d7680227db Spec : remplacer mes estimations par les mesures reelles
L'estimation annoncait 15 a 30 s pour medium sur CPU et l'ecartait d'avance. La
mesure dit 4,4 s — et ce modele est PLUS JUSTE que le moteur GPU en service.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:45:42 +02:00
2ccb614649 A chaque moteur son modele, et un forcage manuel
app.py passait CONFIG["model"] — un depot MLX — quel que soit le moteur detecte.
Sur une machine sans Apple Silicon, faster-whisper aurait recu un depot qu'il ne
sait pas charger. Le defaut etait invisible ici, la detection rendant toujours
"mlx" sur ce Mac : aucun test ne pouvait le voir.

MODELES_PAR_MOTEUR associe chaque moteur a son depot. Les valeurs viennent de la
mesure du jour (trois echantillons, amorce active, CPU Apple Silicon) :

  faster-whisper base   0,52 s  10/13 termes techniques   142 Mo
  faster-whisper small  1,42 s  11/13                     464 Mo
  faster-whisper medium 4,40 s  13/13                    1460 Mo
  MLX medium (GPU)      0,75 s  11/13                     489 Mo

Le GPU vise donc la precision (medium, 13/13, il absorbe le cout) ; le CPU vise
la latence (small, 11/13 — AUTANT que le MLX utilise aujourd'hui — pour 1,4 s).
`medium` sur CPU serait plus juste, mais ces 4,4 s sont celles d'un CPU Apple
Silicon : sur un CPU Intel ou Windows, le meme modele peut demander trois a quatre
fois plus. La commande de diagnostic mesurera la machine reelle et pourra
promouvoir `medium`.

CONFIG["model"] passe a None et devient un FORCAGE manuel qui prime sur la table,
au lieu d'un reglage mort qu'on modifie sans effet.

115 tests. Comportement inchange sur ce Mac : moteur mlx, modele identique.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:45:14 +02:00
6974a3406c Ajoute le moteur faster-whisper, pour les machines sans Apple Silicon
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:38:09 +02:00
5cf8f448ed Un module plat par plateforme, pour les trois frontieres
interface/cocoa/overlay.py etait un sous-paquet quand systeme/macos.py et
moteur/mlx.py sont des modules plats. Rien n'expliquait la difference : la couche
Windows aurait eu a deviner lequel des deux motifs imiter, et aurait reproduit
l'incoherence dans un sens ou dans l'autre.

interface/cocoa.py est donc mis a plat, contenu inchange (diff vide), et la regle
est ecrite dans interface/__init__.py : un module plat par plateforme, sous-paquet
seulement le jour ou une plateforme exige plusieurs fichiers.

105 tests inchanges.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 18:33:35 +02:00
7661cbce36 Le menu Sons refonctionne, et le transcripteur porte son nom francais 2026-08-23 18:25:08 +02:00
dcec1e61d0 Le controleur passe par les contrats, la duplication disparait 2026-08-23 18:19:25 +02:00
3c3ea4444d Deplace l'overlay Cocoa derriere le contrat Overlay, sans le modifier
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:13:12 +02:00
5f83e4c6a3 La sonde CUDA reconnait aussi les DLL Windows
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:10:23 +02:00
734bba31da Extrait la transcription derriere le contrat Transcripteur
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 18:05:31 +02:00
5c4477fe1d Extrait les appels macOS derriere le contrat Systeme
- systeme/macos.py : SystemeMacOS (sons afplay, modificateurs Quartz, chemins,
  verrou d'instance fcntl) implementant le contrat Systeme.
- systeme/__init__.py : choisir_systeme() fabrique selon sys.platform,
  refuse clairement une plateforme inconnue.
- core.py : verrou d'instance (fcntl, LOCK_PATH, acquire_single_instance_lock)
  retire ; import os retire aussi, verifie inutilise ailleurs dans le fichier.
- app.py : passe par choisir_systeme().acquerir_verrou_instance(dossier_donnees()
  + instance.lock) au lieu de core.acquire_single_instance_lock. Seul changement
  dans ce fichier, nom de fichier inchange.
- tests/test_single_instance.py : reoriente vers choisir_systeme(), memes
  assertions et meme nombre de tests.
- tests/test_systeme.py (nouveau) : verrou du test_le_verrou_refuse_une_seconde_prise
  garde desormais la reference au premier verrou — sans variable, CPython le
  garbage-collectait entre les deux assert (refcount 0 -> fermeture -> verrou
  relache), le test passait pour la mauvaise raison.

89 tests verts (84 + 5).
2026-08-23 17:57:52 +02:00
434944def6 Renforce les contrats : types complets et docstrings de methode protegees
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:53:12 +02:00
54479d9af9 Declare les trois contrats de Zonza
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 17:47:02 +02:00
332afede0e Plan : corriger deux defauts releves a l'auto-relecture
dossier_donnees() etait utilise par app.py sans figurer dans le contrat Systeme —
l'implementation Windows aurait pu l'omettre et ne casser qu'a l'execution. Et les
comptes de tests annonces ne s'additionnaient pas depuis 80.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 17:44:39 +02:00
372df70c62 Plan : etape 1 du portage multiplateforme
Sept taches, chacune en TDD. Point de vigilance principal : app.py garde son nom
pour que le cdhash du stub reste identique et que l'autorisation d'Accessibilite
survive au refactor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 17:43:39 +02:00
6dea252013 Spec : preciser ou la detection est memorisee, et decouper en 4 etapes
L'auto-relecture a montre deux defauts : « memorisation » ne disait pas OU, et le
perimetre depassait ce qu'un seul plan peut porter. Les etapes 1 a 3 sont
verifiables sans machine Windows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-23 17:40:26 +02:00
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
19afbe0369 Zonza ne tient plus elle-meme le micro qu'elle essaie d'ouvrir
Trois dictees d'affilee sans le moindre son, chacune terminee par la fermeture
forcee du halo. Cause : Recorder.start() ecrasait self._stream sans fermer le
precedent. Des qu'un stop() se bloquait sur un peripherique accapare, le flux
restait ouvert POUR TOUJOURS ; l'essai suivant en empilait un autre par-dessus, et
Zonza finissait par tenir le micro qu'elle tentait d'ouvrir. 25 threads dans le
process en temoignaient.

start() libere donc le flux precedent avant d'en ouvrir un nouveau. La liberation
DETACHE la reference d'abord — meme ratee, elle ne doit pas laisser un flux mort
barrer le prochain demarrage — et utilise abort() plutot que stop(), ce dernier
drainant les tampons et pouvant se bloquer. Elle ne leve jamais.

Le flux est desormais injectable, ce qui rend Recorder testable sans peripherique.

Prouve sur le vrai micro, en enchainant deux demarrages sans arret entre les deux :
1066 blocs au premier, 1070 au second. Avant, le second en recevait zero.

80 tests passent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 23:57:47 +02:00
ebf3c8e749 Zonza previent quand le micro ne lui envoie rien
Loom tenait le micro : Zonza enregistrait dans le vide. La forme d'onde restait
plate et RIEN ne le signalait — on ne le decouvrait qu'apres avoir parle pour rien,
puis en cherchant pourquoi le halo se figeait.

Au bout de 1,8 s sans le moindre bloc audio, la bulle affiche « Aucun son — micro
pris ailleurs ? » et un son d'erreur previent. Le delai laisse le temps au flux
CoreAudio de s'ouvrir (quelques centaines de ms) sans laisser dicter une phrase
entiere pour rien.

Le garde `en_cours` evite le faux positif de la dictee tres courte : arreter avant
la fin du delai n'est pas une panne.

Verifie dans les deux sens : micro muet -> alerte, 0 bloc ; micro normal -> bulle
inchangee, 40 blocs. Pas de faux positif.

74 tests passent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 12:18:45 +02:00
68e1f336c8 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>
2026-08-21 12:14:47 +02:00
5710cb69ce 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>
2026-08-21 01:48:07 +02:00
58ed0d9e6c Attend le relachement des touches avant de coller
Deux dictees ont ete transcrites correctement puis n'ont jamais atteint la zone de
saisie, sans la moindre erreur : inject_text avait bien envoye ses touches. Cause :
la hotkey Cmd+Ctrl+D etait encore enfoncee quand Zonza a simule Cmd+V. Le systeme a
recu Cmd+Ctrl+V, que l'application cible ignore. Le code croyait avoir reussi.

inject_text patiente donc jusqu'a ce qu'aucun modificateur physique ne soit enfonce
(drapeaux Quartz), au plus 2 s. Au-dela, il colle quand meme : mieux vaut un collage
douteux qu'une transcription perdue. Le delai du presse-papier passe de 0,05 a 0,12 s,
0,05 etant juste.

Et surtout, le collage se TRACE desormais ("[collage] ok" ou l'avertissement) : sans
cela, un echec futur resterait invisible dans le log, comme celui-ci l'a ete.

L'attente est une unite pure et bornee (core.attendre, horloge et sommeil injectables).
Detecteur verifie en conditions reelles : False au repos, retour immediat.

55 tests passent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 01:42:44 +02:00
f8c30e0402 Overlay reduit d'un quart, et sa geometrie devient proportionnelle
Barre 440x52 -> 330x40. Les proportions internes (bouton stop, micro, marges,
ecarts) etaient en dur dans le dessin : les laisser telles quelles aurait donne
une grosse pastille dans une barre retrecie. Elles derivent desormais de la
hauteur de la barre via core.bar_metrics, donc une seule valeur commande toute
l'echelle — diviser bar_height par deux divise tout par deux (teste).

Rendu verifie par capture d'ecran apres reduction.

50 tests passent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:49:38 +02:00
55c860c5b9 L'overlay devient une barre de dictee au lieu d'un cercle
Reprend le design demande par Ralph, d'apres une capture : barre sombre
arrondie posee en bas de l'ecran, et dedans, de gauche a droite, un bouton stop
rond rouge-orange a carre blanc, une forme d'onde en barres verticales dont la
traine retombe en pointilles, une icone de micro. Au-dessus, une bulle sombre a
pointe.

La forme d'onde demandait un HISTORIQUE de niveaux, la ou l'ancien cercle ne
lisait que le niveau courant : push_level et bar_heights sont ajoutes a
audio_level.py comme unites pures (le plus recent a gauche, les positions vides
ou silencieuses retombent sur un plancher — c'est lui qui donne les pointilles).

La bulle ne peut pas afficher le texte en direct comme la reference : Zonza
transcrit APRES l'arret, il n'y a pas de flux continu. Elle porte donc l'etape
en cours, et la fenetre reste affichee jusqu'au collage au lieu de disparaitre
pendant les secondes de calcul — l'ecran ne se vide plus sans explication.

Rendu verifie par capture d'ecran, pas seulement par les tests : le micro a ete
redessine apres coup (il sortait en tache blanche) et une marge basse ajoutee,
l'arrondi de la barre etant rogne par le bord de la fenetre.

45 tests passent. Le cdhash du stub est inchange, donc les autorisations
d'Accessibilite survivent a cette mise a jour (verifie : accordee = True).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:46:56 +02:00
f1cff4239d Whisper recoit le vocabulaire metier en amorce
Sans contexte, le modele rendait « Playwright » en « PlayWreck », « iCloud » en
« Auriclaw », « Supabase » en « Supabass », « Vercel » en « Versell » : 4 termes
sur 5 faux, mesure sur audio reel. Avec l'amorce (initial_prompt), les 5 sont
corrects, sans changer de modele ni ralentir (1,3 s).

Passer a large-v3-turbo a ete teste et ecarte : PIRE sur le meme echantillon
(« commit sur Gitea » -> « commis sur JTA », « Stripe » -> « Trip »), pour 1,5 Go
de plus. C'est l'amorce qui paie, pas la taille du modele.

VOCABULAIRE est une simple liste a completer dans core.py. Whisper ne lit que
~224 tokens d'amorce et tronque le reste EN SILENCE : la longueur est donc
bornee explicitement, pour qu'un ajout de termes ne soit jamais sans effet a
l'insu de celui qui l'ecrit (47 termes = 416 caracteres sur 800).

34 tests passent (27 + 7 nouveaux).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:28:52 +02:00
3ad274a774 Zonza ne se lance plus qu'une fois a la fois
Deux bundles portant l'identifiant cloud.mrtechlab.zonza etaient enregistres
aupres de LaunchServices : /Applications/Zonza.app et un fantome laisse dans la
Corbeille le 13 juin, dont le stub visait l'ancien chemin du depot — valide
jusqu'a aujourd'hui. Les deux etaient donc lancables simultanement. Chaque
instance chargeait son propre modele MLX-Whisper medium et captait la MEME
hotkey globale : un seul appui declenchait deux transcriptions, la machine
saturait.

Le fantome est desenregistre, mais la classe de bug revient au moindre bundle
egare. D'ou un verrou exclusif (flock non bloquant) pris au demarrage : la
seconde instance renonce et sort. Le noyau libere le verrou meme apres un
plantage, donc pas d'orphelin.

Prouve avec le vrai binaire : 2e lancement direct -> "[garde] une autre instance
tourne deja", 1 seul process ; et controle negatif, seule elle demarre bien.

27 tests passent (22 + 5 nouveaux).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 00:25:05 +02:00
8a9a6de98c feat: demande d'accessibilité native au premier lancement
AXIsProcessTrustedWithOptions(prompt) : macOS affiche sa popup officielle
et crée l'entrée TCC correctement (l'ajout manuel via « + » ne prenait pas).
Diag de l'état de confiance dans le log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 01:03:09 +02:00
2236f2d11f fix: icône barre de menu sur Tahoe — stub libpython + abandon de rumps
Sur macOS Tahoe (26.5), le NSStatusItem n'est jamais placé (fenêtre h=0) si :
1. l'exécutable du processus n'appartient pas au bundle lancé (le python
   framework se ré-exécute → identité dépareillée) → stub C qui dlopen
   libpython et exécute app.py DANS le processus du bundle ;
2. l'item est créé par rumps (cause interne non isolée, l'API native
   fonctionne) → app.py réécrit en pyobjc pur (NSStatusItem + NSMenu).

Le stub transmet argv à python quand il y en a (workers multiprocessing
de MLX) — sinon bombe à fork (9 instances observées). PYTHONUNBUFFERED
pour que les print atteignent ~/Library/Logs/Zonza.log. Auto-diagnostic
du placement de l'icône au démarrage. rumps retiré des dépendances.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:54:44 +02:00
2b0d81796b fix: icône de barre de menu absente sur macOS Tahoe
setActivationPolicy_(Accessory) appelé avant rumps.run() empêche la
création du NSStatusItem sur Tahoe (vérifié par test A/B). Retiré :
LSUIElement dans l'Info.plist suffit pour masquer l'icône Dock.
Le lanceur redirige aussi stdout/stderr vers ~/Library/Logs/Zonza.log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-13 00:14:15 +02:00
22080eb997 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>
2026-06-12 23:57:59 +02:00