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>
This commit is contained in:
2026-08-21 00:46:56 +02:00
co-authored by Claude Opus 5
parent f1cff4239d
commit 55c860c5b9
7 changed files with 327 additions and 86 deletions
+7 -4
View File
@@ -50,12 +50,15 @@ def test_clean_transcript_empty_stays_empty():
def test_config_has_ui_keys():
for key in ("fps", "circle_base_radius", "circle_amplitude", "circle_color_rgb"):
for key in ("fps", "bar_width", "bar_height", "bar_radius", "bar_bg_rgba",
"stop_button_rgb", "waveform_rgb", "waveform_bars", "mic_rgb"):
assert key in CONFIG
def test_config_ui_defaults():
assert CONFIG["fps"] == 30
assert CONFIG["circle_base_radius"] > 0
assert CONFIG["circle_amplitude"] > 0
assert len(CONFIG["circle_color_rgb"]) == 3
assert CONFIG["bar_width"] > CONFIG["bar_height"] # barre couchee, pas carree
assert CONFIG["bar_radius"] <= CONFIG["bar_height"] / 2
assert len(CONFIG["bar_bg_rgba"]) == 4
assert CONFIG["waveform_bars"] >= 8
assert 0.0 < CONFIG["waveform_floor"] < 1.0
+72
View File
@@ -0,0 +1,72 @@
"""La bulle de l'overlay porte l'etat en cours.
Zonza ne transcrit pas en continu : le texte n'existe qu'apres l'arret. La bulle
affiche donc l'etape ou l'on en est — « Je vous ecoute… » pendant la capture,
« Transcription… » pendant le calcul — et la fenetre reste visible jusqu'au
collage, au lieu de disparaitre en laissant l'utilisateur sans retour.
"""
import numpy as np
from controller import ZonzaController
from core import CONFIG
class FauxPulse:
def __init__(self):
self.statuts = []
self.visible = False
def show(self):
self.visible = True
def hide(self):
self.visible = False
def set_level(self, level):
pass
def set_status(self, text):
self.statuts.append(text)
class FauxRecorder:
def __init__(self, audio):
self.audio = audio
def start(self):
pass
def stop(self):
return self.audio
def _ctrl(audio):
pulse = FauxPulse()
c = ZonzaController(CONFIG, pulse)
c.recorder = FauxRecorder(audio)
c.transcriber = type("T", (), {"transcribe": staticmethod(lambda a: "bonjour")})()
return c, pulse
def test_la_bulle_annonce_l_ecoute_au_demarrage():
c, pulse = _ctrl(np.zeros(16000, dtype="float32"))
c.toggle()
assert pulse.visible
assert any("écoute" in s.lower() for s in pulse.statuts)
def test_la_fenetre_reste_visible_pendant_la_transcription():
audio = np.ones(16000, dtype="float32") * 0.1 # 1 s : au-dessus du seuil
c, pulse = _ctrl(audio)
c.toggle()
pulse.statuts.clear()
c._stop_and_process()
assert pulse.visible, "la fenetre ne doit pas disparaitre avant la transcription"
assert any("transcription" in s.lower() for s in pulse.statuts)
def test_un_enregistrement_trop_court_referme_tout_de_suite():
c, pulse = _ctrl(np.zeros(10, dtype="float32")) # bien sous min_duration_s
c.toggle()
c._stop_and_process()
assert not pulse.visible
+53
View File
@@ -0,0 +1,53 @@
"""Historique de niveaux -> hauteurs de barres de la forme d'onde.
L'overlay affiche une forme d'onde facon dictee : le niveau le plus recent a
GAUCHE, les plus anciens vers la droite. Les positions sans donnee (debut
d'enregistrement) ou silencieuses retombent sur un plancher, dessine comme un
petit point plutot qu'une barre — c'est ce qui donne la traine pointillee.
"""
import pytest
from audio_level import bar_heights, push_level
def test_push_ajoute_en_tete():
assert push_level([], 0.5, capacity=3) == [0.5]
assert push_level([0.5], 0.8, capacity=3) == [0.8, 0.5]
def test_push_borne_la_capacite_en_jetant_le_plus_ancien():
h = [0.3, 0.2, 0.1]
assert push_level(h, 0.9, capacity=3) == [0.9, 0.3, 0.2]
def test_push_ne_modifie_pas_l_historique_recu():
h = [0.3]
push_level(h, 0.9, capacity=3)
assert h == [0.3]
def test_push_borne_le_niveau_dans_zero_un():
assert push_level([], 5.0, capacity=2) == [1.0]
assert push_level([], -3.0, capacity=2) == [0.0]
def test_bar_heights_complete_avec_le_plancher():
b = bar_heights([1.0], n_bars=4, floor=0.08)
assert len(b) == 4
assert b[0] == 1.0
assert b[1:] == [0.08, 0.08, 0.08]
def test_bar_heights_tronque_si_historique_trop_long():
b = bar_heights([0.9, 0.8, 0.7, 0.6, 0.5], n_bars=3, floor=0.05)
assert b == [0.9, 0.8, 0.7]
def test_bar_heights_applique_le_plancher_aux_silences():
b = bar_heights([0.0, 0.5], n_bars=2, floor=0.1)
assert b == [0.1, 0.5]
def test_bar_heights_refuse_un_nombre_de_barres_absurde():
with pytest.raises(ValueError):
bar_heights([0.5], n_bars=0, floor=0.1)