From f8c30e04021cfb278fa7873dc92a8940bdc8472b Mon Sep 17 00:00:00 2001 From: Ralph Mayola Date: Fri, 21 Aug 2026 00:49:38 +0200 Subject: [PATCH] Overlay reduit d'un quart, et sa geometrie devient proportionnelle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- core.py | 32 +++++++++++++++++++++++++------ pulse_window.py | 42 +++++++++++++++++++++-------------------- tests/test_geometrie.py | 41 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 89 insertions(+), 26 deletions(-) create mode 100644 tests/test_geometrie.py diff --git a/core.py b/core.py index 1c87982..0bca809 100644 --- a/core.py +++ b/core.py @@ -17,19 +17,19 @@ CONFIG = { "sample_rate": 16000, "fps": 30, # --- overlay « barre de dictee » (bas de l'ecran) --- - "bar_width": 440.0, - "bar_height": 52.0, - "bar_radius": 16.0, + "bar_width": 330.0, + "bar_height": 40.0, + "bar_radius": 12.0, "bar_bg_rgba": (0.09, 0.09, 0.10, 0.95), "stop_button_rgb": (0.89, 0.31, 0.23), # rond rouge-orange "stop_glyph_rgb": (1.0, 1.0, 1.0), # carre arrondi blanc au centre "waveform_rgb": (0.95, 0.95, 0.97), - "waveform_bars": 34, # nombre de barres de la forme d'onde + "waveform_bars": 28, # nombre de barres de la forme d'onde "waveform_floor": 0.07, # plancher : dessine en petit point (traine pointillee) "mic_rgb": (0.78, 0.78, 0.82), "bubble_text_rgb": (1.0, 1.0, 1.0), - "bubble_font_size": 13.0, - "bubble_max_width": 300.0, + "bubble_font_size": 12.0, + "bubble_max_width": 240.0, "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) @@ -92,6 +92,26 @@ def build_initial_prompt(termes=None): return prefixe + ", ".join(retenus) + "." +# Proportions internes de la barre, exprimees en fraction de sa HAUTEUR : reduire +# `bar_height` reduit alors le bouton, le micro et les marges dans le meme +# mouvement, au lieu de laisser une grosse pastille dans une barre retrecie. +_PROPORTIONS = { + "stop_diametre": 0.538, + "mic_largeur": 0.173, + "marge_laterale": 0.423, + "mic_corps_hauteur": 0.250, + "mic_arceau_rayon": 0.135, + "ecart_interne": 0.346, +} + + +def bar_metrics(bar_height): + """Mesures de l'overlay pour une hauteur de barre donnee, en points.""" + if bar_height <= 0: + raise ValueError("bar_height doit etre > 0") + return {cle: bar_height * f for cle, f in _PROPORTIONS.items()} + + def validate_config(config): """Valide la config ; lève ValueError si une valeur est aberrante.""" if config["min_duration_s"] < 0: diff --git a/pulse_window.py b/pulse_window.py index 7fcb1f1..cc427db 100644 --- a/pulse_window.py +++ b/pulse_window.py @@ -29,16 +29,13 @@ from AppKit import ( from Foundation import NSMakeRect, NSMakeSize, NSObject from audio_level import bar_heights, push_level -from core import CONFIG +from core import CONFIG, bar_metrics _BOTTOM_MARGIN = 80.0 # au-dessus du Dock _BUBBLE_GAP = 12.0 # espace entre la bulle et la barre _BUBBLE_PAD = 12.0 # marge intérieure de la bulle _TAIL = 7.0 # demi-largeur de la pointe de la bulle -_SIDE_PAD = 22.0 # marge intérieure gauche/droite de la barre -_STOP_D = 28.0 # diamètre du bouton stop -_MIC_W = 9.0 # largeur de la capsule du micro -_BOTTOM_INSET = 10.0 # evite que l'arrondi bas de la barre touche le bord +_BOTTOM_INSET = 8.0 # evite que l'arrondi bas de la barre touche le bord def _rounded(x, y, w, h, radius): @@ -74,13 +71,13 @@ class PulseView(NSView): _rgba(CONFIG["bar_bg_rgba"], CONFIG["bar_bg_rgba"][3]).set() _rounded(x, y, w, h, CONFIG["bar_radius"]).fill() - def _draw_stop_button(self, cx, cy): + def _draw_stop_button(self, cx, cy, diametre): _rgba(CONFIG["stop_button_rgb"]).set() NSBezierPath.bezierPathWithOvalInRect_( - NSMakeRect(cx - _STOP_D / 2, cy - _STOP_D / 2, _STOP_D, _STOP_D) + NSMakeRect(cx - diametre / 2, cy - diametre / 2, diametre, diametre) ).fill() # carré arrondi blanc au centre = « arrêter » - side = _STOP_D * 0.34 + side = diametre * 0.34 _rgba(CONFIG["stop_glyph_rgb"]).set() _rounded(cx - side / 2, cy - side / 2, side, side, 2.0).fill() @@ -98,23 +95,25 @@ class PulseView(NSView): half = max(bw / 2.0, level * half_max) # jamais moins qu'un point rond _rounded(cxi - bw / 2, cy - half, bw, half * 2, bw / 2).fill() - def _draw_mic(self, cx, cy): + def _draw_mic(self, cx, cy, m): col = _rgba(CONFIG["mic_rgb"]) col.set() - body_h = 13.0 - body_y = cy - body_h / 2 + 3.0 + largeur = m["mic_largeur"] + body_h = m["mic_corps_hauteur"] + body_y = cy - body_h / 2 + body_h * 0.23 # capsule (le micro lui-meme) - _rounded(cx - _MIC_W / 2, body_y, _MIC_W, body_h, _MIC_W / 2).fill() + _rounded(cx - largeur / 2, body_y, largeur, body_h, largeur / 2).fill() # arceau en U qui passe SOUS la capsule arc = NSBezierPath.bezierPath() arc.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_( - (cx, body_y + 3.0), 7.0, 195.0, 345.0 + (cx, body_y + body_h * 0.23), m["mic_arceau_rayon"], 195.0, 345.0 ) - arc.setLineWidth_(1.6) + arc.setLineWidth_(max(1.0, body_h * 0.12)) arc.setLineCapStyle_(1) # bouts arrondis arc.stroke() # pied vertical sous l'arceau - _rounded(cx - 0.8, body_y - 8.0, 1.6, 4.0, 0.8).fill() + pied = max(1.2, largeur * 0.18) + _rounded(cx - pied / 2, body_y - body_h * 0.62, pied, body_h * 0.31, pied / 2).fill() def _draw_bubble(self, bar_x, bar_top, bar_w): if not self._status: @@ -128,7 +127,7 @@ class PulseView(NSView): bounds = text.boundingRectWithSize_options_(avail, 1 << 0) # WordWrap tw, th = bounds.size.width, bounds.size.height w, h = tw + 2 * _BUBBLE_PAD, th + 2 * _BUBBLE_PAD - x = bar_x + 24.0 # alignée à gauche, comme la référence + x = bar_x + CONFIG["bar_height"] * 0.46 # alignée à gauche, comme la référence y = bar_top + _BUBBLE_GAP _rgba(CONFIG["bar_bg_rgba"], CONFIG["bar_bg_rgba"][3]).set() _rounded(x, y, w, h, 10.0).fill() @@ -151,11 +150,14 @@ class PulseView(NSView): y = _BOTTOM_INSET cy = y + h / 2.0 + m = bar_metrics(h) + pad, stop_d, mic_w, ecart = (m["marge_laterale"], m["stop_diametre"], + m["mic_largeur"], m["ecart_interne"]) self._draw_bar_background(x, y, w, h) - self._draw_stop_button(x + _SIDE_PAD + _STOP_D / 2, cy) - self._draw_mic(x + w - _SIDE_PAD - _MIC_W / 2, cy) - wave_x = x + _SIDE_PAD + _STOP_D + 18.0 - wave_w = (x + w - _SIDE_PAD - _MIC_W - 18.0) - wave_x + self._draw_stop_button(x + pad + stop_d / 2, cy, stop_d) + self._draw_mic(x + w - pad - mic_w / 2, cy, m) + wave_x = x + pad + stop_d + ecart + wave_w = (x + w - pad - mic_w - ecart) - wave_x self._draw_waveform(wave_x, cy, wave_w) self._draw_bubble(x, y + h, w) diff --git a/tests/test_geometrie.py b/tests/test_geometrie.py new file mode 100644 index 0000000..fe096bf --- /dev/null +++ b/tests/test_geometrie.py @@ -0,0 +1,41 @@ +"""Geometrie de l'overlay : tout derive de la hauteur de la barre. + +Sinon, reduire la barre laisse le bouton stop, le micro et les marges a leur +taille d'origine, et la composition se deseequilibre. Une seule valeur commande +l'echelle. +""" +import pytest + +from core import CONFIG, bar_metrics + + +def test_toutes_les_mesures_sont_fournies(): + m = bar_metrics(52.0) + for cle in ("stop_diametre", "mic_largeur", "marge_laterale", + "mic_corps_hauteur", "mic_arceau_rayon", "ecart_interne"): + assert cle in m + + +def test_les_mesures_tiennent_dans_la_barre(): + h = 52.0 + m = bar_metrics(h) + assert m["stop_diametre"] < h + assert m["mic_corps_hauteur"] < h + assert 2 * m["marge_laterale"] < CONFIG["bar_width"] + + +def test_diviser_la_hauteur_par_deux_divise_tout_par_deux(): + grand, petit = bar_metrics(52.0), bar_metrics(26.0) + for cle, valeur in grand.items(): + assert petit[cle] == pytest.approx(valeur / 2.0) + + +def test_une_hauteur_absurde_est_refusee(): + with pytest.raises(ValueError): + bar_metrics(0) + + +def test_la_config_reste_coherente_avec_ses_mesures(): + m = bar_metrics(CONFIG["bar_height"]) + largeur_occupee = 2 * m["marge_laterale"] + m["stop_diametre"] + m["mic_largeur"] + assert largeur_occupee < CONFIG["bar_width"], "plus de place pour la forme d'onde"