"""`zonza --diagnostic` : éprouve toute la chaîne sur la machine cible et dit où elle casse. Aucune machine Windows n'étant disponible pour ce portage, cette commande est le SEUL moyen pour l'utilisateur de valider `systeme/windows.py` en conditions réelles. D'où deux règles strictes suivies ici : 1. Chaque épreuve attrape SES PROPRES erreurs : une panne (micro absent, moteur introuvable, `say` manquant...) ne doit jamais faire tomber les épreuves suivantes ni le rapport entier. 2. Une épreuve qui n'a pas pu être menée est marquée `reussite=None` (« non concluante ») — JAMAIS `True`. Mentir sur une réussite serait pire que ne rien dire : mieux vaut « je ne sais pas » qu'un faux vert. Deux fonctions séparées, pour rester testable : `diagnostiquer()` rend une liste de `ResultatEpreuve` (aucun `print`) ; `mettre_en_forme()` la traduit en texte affichable. Toutes les dépendances matérielles/plateforme sont injectables, ce qui permet d'éprouver l'ENCHAÎNEMENT des épreuves depuis ce Mac sans jamais toucher un vrai micro ni un vrai moteur de transcription. """ import os import platform import shutil import subprocess import sys import tempfile import time import uuid import wave from dataclasses import dataclass import numpy as np import core import moteur from moteur import _cuda_disponible from systeme import choisir_systeme @dataclass(frozen=True) class ResultatEpreuve: """Le compte-rendu d'une seule épreuve. `reussite` : True (menée, et tout va bien), False (menée, et quelque chose est cassé), ou None (n'a PAS pu être menée — non concluante, jamais présentée comme une réussite). """ nom: str reussite: bool | None detail: str # --- épreuve 1 : plateforme ------------------------------------------------- def epreuve_plateforme(plateforme=None, machine=None, cuda_disponible=None): """Décrit la machine : plateforme Python, architecture, présence de CUDA.""" plateforme = plateforme if plateforme is not None else sys.platform machine = machine if machine is not None else platform.machine() sonder_cuda = cuda_disponible if cuda_disponible is not None else _cuda_disponible try: cuda = sonder_cuda() except Exception as e: return ResultatEpreuve("Plateforme", None, f"sonde CUDA en échec : {e}") cuda_txt = "présent" if cuda else "absent" return ResultatEpreuve("Plateforme", True, f"{plateforme} / {machine} / CUDA {cuda_txt}") # --- épreuve 2 : moteur retenu ---------------------------------------------- def epreuve_moteur(detecter_moteur=None, resoudre_modele=None, creer_transcripteur=None): """Décide du moteur, résout son modèle, instancie le transcripteur. Rend `(resultat, transcripteur_ou_None)` : le transcripteur sert à l'épreuve Transcription sans être reconstruit — mais seulement si cette épreuve a réussi ; sinon la Transcription sera honnêtement sautée. """ detecter_moteur = detecter_moteur or moteur.detecter_moteur resoudre_modele = resoudre_modele or core.modele_retenu creer_transcripteur = creer_transcripteur or moteur.choisir_transcripteur try: nom = detecter_moteur() nom_modele = resoudre_modele(nom, core.CONFIG["model"]) transcripteur = creer_transcripteur( nom=nom, modele=nom_modele, langue=core.CONFIG["language"], amorce=core.CONFIG["initial_prompt"], ) except Exception as e: return ResultatEpreuve("Moteur retenu", None, f"détection impossible : {e}"), None return ResultatEpreuve("Moteur retenu", True, f"{nom} + {nom_modele}"), transcripteur # --- épreuve 3 : micro ------------------------------------------------------- def epreuve_micro(ouvrir_flux=None, sample_rate=16000, duree_s=1.0, dormir=None): """Ouvre VRAIMENT le micro pendant `duree_s` et compte les blocs reçus. `ouvrir_flux` suit le même contrat que `Recorder._ouvrir_flux` dans `capture.py` : injectable, pour que les tests ne touchent à aucun périphérique réel. """ dormir = dormir or time.sleep if ouvrir_flux is None: try: import sounddevice as sd except Exception as e: return ResultatEpreuve("Micro", None, f"sounddevice indisponible : {e}") ouvrir_flux = lambda **kw: sd.InputStream(**kw) # noqa: E731 compte = {"n": 0} def on_block(indata, frames, time_info, status): compte["n"] += 1 try: flux = ouvrir_flux(samplerate=sample_rate, channels=1, dtype="float32", callback=on_block) flux.start() except Exception as e: return ResultatEpreuve("Micro", None, f"ouverture du micro impossible : {e}") dormir(duree_s) for action in ("stop", "close"): try: getattr(flux, action)() except Exception: pass # meilleur effort : le compte déjà reçu reste valable n = compte["n"] return ResultatEpreuve("Micro", n > 0, f"{n} blocs reçus en {duree_s:g} s") # --- épreuve 4 : transcription ----------------------------------------------- def _generer_echantillon_parole(texte="Bonjour, ceci est un test de dictée Zonza.", sample_rate=16000): """Synthétise un court échantillon de parole via la commande `say` de macOS. LÈVE si `say` est absent, échoue, ou si le WAV produit est illisible — c'est volontaire : l'appelant retombe alors sur du silence plutôt que d'inventer un résultat. Sur une machine sans `say` (Windows, Linux), c'est TOUJOURS ce chemin qui est emprunté. """ if shutil.which("say") is None: raise RuntimeError("commande `say` absente (probablement pas macOS)") chemin = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name try: subprocess.run( ["say", "-o", chemin, f"--data-format=LEI16@{sample_rate}", texte], check=True, capture_output=True, timeout=15, ) with wave.open(chemin, "rb") as w: brut = w.readframes(w.getnframes()) return np.frombuffer(brut, dtype="