"""Unités pures de Zonza — aucune dépendance matérielle. Tout ce qui est ici est testable sans micro, sans clavier, sans modèle. """ import re CONFIG = { "model": "mlx-community/whisper-medium", "language": "fr", "hotkey": "++d", # Cmd+Ctrl+D (libre : ni macOS ni VS Code ne l'utilisent) "sounds": True, "min_duration_s": 0.3, "max_duration_s": 120.0, # sécurité : arrêt auto après 2 min (fenêtre jamais figée) "sample_rate": 16000, "fps": 30, "circle_base_radius": 20.0, "circle_amplitude": 30.0, "circle_color_rgb": (0.40, 0.50, 0.95), "level_gain": 8.0, # amplification du niveau brut pour l'animation "level_attack": 0.6, # lissage à la montée (0–1, proche de 1 = très réactif) "level_release": 0.15, # lissage à la descente (plus petit = retombée douce) "wave_rings": 3, # nombre d'anneaux d'onde concentriques "wave_speed": 0.04, # vitesse de propagation des anneaux par frame } def validate_config(config): """Valide la config ; lève ValueError si une valeur est aberrante.""" if config["min_duration_s"] < 0: raise ValueError("min_duration_s doit être >= 0") if config["sample_rate"] <= 0: raise ValueError("sample_rate doit être > 0") if not config["hotkey"]: raise ValueError("hotkey ne peut pas être vide") def is_long_enough(num_samples, sample_rate, min_duration_s): """Renvoie True si l'enregistrement dépasse le seuil de durée minimal.""" duration_s = num_samples / sample_rate return duration_s >= min_duration_s def clean_transcript(text): """Nettoie le texte de Whisper : trim + espaces/retours multiples écrasés.""" return re.sub(r"\s+", " ", text).strip()