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>
This commit is contained in:
2026-06-12 23:57:59 +02:00
co-authored by Claude Fable 5
commit 22080eb997
18 changed files with 955 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
"""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": "<cmd>+<ctrl>+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()