Overlay reduit d'un quart, et sa geometrie devient proportionnelle
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) <noreply@anthropic.com>
This commit is contained in:
parent
55c860c5b9
commit
f8c30e0402
32
core.py
32
core.py
@ -17,19 +17,19 @@ CONFIG = {
|
|||||||
"sample_rate": 16000,
|
"sample_rate": 16000,
|
||||||
"fps": 30,
|
"fps": 30,
|
||||||
# --- overlay « barre de dictee » (bas de l'ecran) ---
|
# --- overlay « barre de dictee » (bas de l'ecran) ---
|
||||||
"bar_width": 440.0,
|
"bar_width": 330.0,
|
||||||
"bar_height": 52.0,
|
"bar_height": 40.0,
|
||||||
"bar_radius": 16.0,
|
"bar_radius": 12.0,
|
||||||
"bar_bg_rgba": (0.09, 0.09, 0.10, 0.95),
|
"bar_bg_rgba": (0.09, 0.09, 0.10, 0.95),
|
||||||
"stop_button_rgb": (0.89, 0.31, 0.23), # rond rouge-orange
|
"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
|
"stop_glyph_rgb": (1.0, 1.0, 1.0), # carre arrondi blanc au centre
|
||||||
"waveform_rgb": (0.95, 0.95, 0.97),
|
"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)
|
"waveform_floor": 0.07, # plancher : dessine en petit point (traine pointillee)
|
||||||
"mic_rgb": (0.78, 0.78, 0.82),
|
"mic_rgb": (0.78, 0.78, 0.82),
|
||||||
"bubble_text_rgb": (1.0, 1.0, 1.0),
|
"bubble_text_rgb": (1.0, 1.0, 1.0),
|
||||||
"bubble_font_size": 13.0,
|
"bubble_font_size": 12.0,
|
||||||
"bubble_max_width": 300.0,
|
"bubble_max_width": 240.0,
|
||||||
"level_gain": 8.0, # amplification du niveau brut pour l'animation
|
"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_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)
|
"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) + "."
|
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):
|
def validate_config(config):
|
||||||
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
"""Valide la config ; lève ValueError si une valeur est aberrante."""
|
||||||
if config["min_duration_s"] < 0:
|
if config["min_duration_s"] < 0:
|
||||||
|
|||||||
@ -29,16 +29,13 @@ from AppKit import (
|
|||||||
from Foundation import NSMakeRect, NSMakeSize, NSObject
|
from Foundation import NSMakeRect, NSMakeSize, NSObject
|
||||||
|
|
||||||
from audio_level import bar_heights, push_level
|
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
|
_BOTTOM_MARGIN = 80.0 # au-dessus du Dock
|
||||||
_BUBBLE_GAP = 12.0 # espace entre la bulle et la barre
|
_BUBBLE_GAP = 12.0 # espace entre la bulle et la barre
|
||||||
_BUBBLE_PAD = 12.0 # marge intérieure de la bulle
|
_BUBBLE_PAD = 12.0 # marge intérieure de la bulle
|
||||||
_TAIL = 7.0 # demi-largeur de la pointe 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
|
_BOTTOM_INSET = 8.0 # evite que l'arrondi bas de la barre touche le bord
|
||||||
_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
|
|
||||||
|
|
||||||
|
|
||||||
def _rounded(x, y, w, h, radius):
|
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()
|
_rgba(CONFIG["bar_bg_rgba"], CONFIG["bar_bg_rgba"][3]).set()
|
||||||
_rounded(x, y, w, h, CONFIG["bar_radius"]).fill()
|
_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()
|
_rgba(CONFIG["stop_button_rgb"]).set()
|
||||||
NSBezierPath.bezierPathWithOvalInRect_(
|
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()
|
).fill()
|
||||||
# carré arrondi blanc au centre = « arrêter »
|
# carré arrondi blanc au centre = « arrêter »
|
||||||
side = _STOP_D * 0.34
|
side = diametre * 0.34
|
||||||
_rgba(CONFIG["stop_glyph_rgb"]).set()
|
_rgba(CONFIG["stop_glyph_rgb"]).set()
|
||||||
_rounded(cx - side / 2, cy - side / 2, side, side, 2.0).fill()
|
_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
|
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()
|
_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 = _rgba(CONFIG["mic_rgb"])
|
||||||
col.set()
|
col.set()
|
||||||
body_h = 13.0
|
largeur = m["mic_largeur"]
|
||||||
body_y = cy - body_h / 2 + 3.0
|
body_h = m["mic_corps_hauteur"]
|
||||||
|
body_y = cy - body_h / 2 + body_h * 0.23
|
||||||
# capsule (le micro lui-meme)
|
# 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
|
# arceau en U qui passe SOUS la capsule
|
||||||
arc = NSBezierPath.bezierPath()
|
arc = NSBezierPath.bezierPath()
|
||||||
arc.appendBezierPathWithArcWithCenter_radius_startAngle_endAngle_(
|
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.setLineCapStyle_(1) # bouts arrondis
|
||||||
arc.stroke()
|
arc.stroke()
|
||||||
# pied vertical sous l'arceau
|
# 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):
|
def _draw_bubble(self, bar_x, bar_top, bar_w):
|
||||||
if not self._status:
|
if not self._status:
|
||||||
@ -128,7 +127,7 @@ class PulseView(NSView):
|
|||||||
bounds = text.boundingRectWithSize_options_(avail, 1 << 0) # WordWrap
|
bounds = text.boundingRectWithSize_options_(avail, 1 << 0) # WordWrap
|
||||||
tw, th = bounds.size.width, bounds.size.height
|
tw, th = bounds.size.width, bounds.size.height
|
||||||
w, h = tw + 2 * _BUBBLE_PAD, th + 2 * _BUBBLE_PAD
|
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
|
y = bar_top + _BUBBLE_GAP
|
||||||
_rgba(CONFIG["bar_bg_rgba"], CONFIG["bar_bg_rgba"][3]).set()
|
_rgba(CONFIG["bar_bg_rgba"], CONFIG["bar_bg_rgba"][3]).set()
|
||||||
_rounded(x, y, w, h, 10.0).fill()
|
_rounded(x, y, w, h, 10.0).fill()
|
||||||
@ -151,11 +150,14 @@ class PulseView(NSView):
|
|||||||
y = _BOTTOM_INSET
|
y = _BOTTOM_INSET
|
||||||
cy = y + h / 2.0
|
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_bar_background(x, y, w, h)
|
||||||
self._draw_stop_button(x + _SIDE_PAD + _STOP_D / 2, cy)
|
self._draw_stop_button(x + pad + stop_d / 2, cy, stop_d)
|
||||||
self._draw_mic(x + w - _SIDE_PAD - _MIC_W / 2, cy)
|
self._draw_mic(x + w - pad - mic_w / 2, cy, m)
|
||||||
wave_x = x + _SIDE_PAD + _STOP_D + 18.0
|
wave_x = x + pad + stop_d + ecart
|
||||||
wave_w = (x + w - _SIDE_PAD - _MIC_W - 18.0) - wave_x
|
wave_w = (x + w - pad - mic_w - ecart) - wave_x
|
||||||
self._draw_waveform(wave_x, cy, wave_w)
|
self._draw_waveform(wave_x, cy, wave_w)
|
||||||
self._draw_bubble(x, y + h, w)
|
self._draw_bubble(x, y + h, w)
|
||||||
|
|
||||||
|
|||||||
41
tests/test_geometrie.py
Normal file
41
tests/test_geometrie.py
Normal file
@ -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"
|
||||||
Loading…
Reference in New Issue
Block a user