fix(short): los textos ilegibles se avisaban con el render ya pagado
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m18s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m18s
La nota que teníamos era falsa: los avisos `severe` de shortsmith SÍ llegan a
un humano — el informe de Telegram los saca en rojo. Lo que pasa es que llegan
colgados del render TERMINADO, así que hacerles caso significa editar el spec a
mano y pagar un segundo. Por eso nadie actuó nunca.
`x-fits` tampoco podía ser la comprobación, y a propósito: es guía blanda que el
propio ejemplo de referencia se salta por uno o tres caracteres viéndose bien.
Parar ahí sería gritar con specs buenos, y un aviso que grita se ignora — que es
justo cómo sobrevivieron los graves. Así que shortsmith publica desde 289d50e un
segundo número medido por campo, `x-fits-hard`, y esto lo comprueba antes de
gastar el render: cuesta un reintento del modelo en vez de un render. El mismo
movimiento que hizo `MAX_CUE_CHARS` con los captions en e32c59f.
Auditado contra los 17 short_en de producción: OCHO llevan al menos un texto que
se dibuja ilegible.
- El ya conocido, Cash-Landrum: `ALL THREE DEVELOPED SYMPTOMS CONSISTENT WITH
RADIATION EXPOSURE`, 36 px pedidos y 20 dibujados. Salta con 63 caracteres
contra un presupuesto de 61 — así de vertical es la curva pegada al muro.
- El que nadie había visto es peor y más común: el CARTEL DE CIERRE.
`counter_close.lines` pide 110 px y se dibujó a 28 en el peor caso, y por
debajo de 64 en cinco de los ocho. Es la llamada a la acción, y en un tercio
del catálogo es el texto más pequeño del fotograma.
- Y la tolerancia aguanta sobre datos reales: `STILL UNEXPLAINED` encoge de 110
a 84 px y no se avisa, que es lo correcto.
El desempate entre intentos pasa a contar averías —gancho y texto ilegible—
antes que segundos, por la misma razón que en 6d9b602: una reescritura que
arregla un rótulo pero se pasa un segundo perdía contra la que no lo arreglaba.
Un campo sin presupuesto propio (`Bar.unit`, que se dibuja dentro de la cadena
de `value_label`) se salta a conciencia: reconstruir esa cadena aquí pediría
conocer el formato de la plantilla, que es lo que este repo no sabe ni debe.
Y contra un shortsmith anterior a 289d50e esto se calla — no puede inventarse el
número —, así que de que el contrato traiga el campo se encarga
test_shortsmith_live.py, que es quien habla con el servicio.
Suite: 274 pasan.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+118
-11
@@ -13,7 +13,7 @@ import pytest
|
||||
|
||||
from src.generator.spec_contract import (
|
||||
SpecInvalid, describe_templates, editorial_notes, opening_notes,
|
||||
validate_spec,
|
||||
unreadable_notes, validate_spec,
|
||||
)
|
||||
|
||||
EXAMPLE = Path(__file__).resolve().parents[1] / "src/generator/examples/jal1628.json"
|
||||
@@ -23,8 +23,10 @@ TEMPLATES = {
|
||||
"type": "object", "additionalProperties": False,
|
||||
"required": ["headline"],
|
||||
"properties": {
|
||||
"headline": {"type": "string", "minLength": 1},
|
||||
"subline": {"type": "string", "default": ""},
|
||||
"headline": {"type": "string", "minLength": 1,
|
||||
"x-fits": 13, "x-fits-hard": 21},
|
||||
"subline": {"type": "string", "default": "",
|
||||
"x-fits": 27, "x-fits-hard": 42},
|
||||
"contact_bearing_deg": {"type": "number", "minimum": 0,
|
||||
"exclusiveMaximum": 360, "default": 210.0},
|
||||
"sweeps": {"type": "number", "exclusiveMinimum": 0, "maximum": 10,
|
||||
@@ -38,20 +40,26 @@ TEMPLATES = {
|
||||
"type": "object", "additionalProperties": False,
|
||||
"required": ["label", "value"],
|
||||
"properties": {
|
||||
"label": {"type": "string", "minLength": 1},
|
||||
"label": {"type": "string", "minLength": 1,
|
||||
"x-fits": 32, "x-fits-hard": 53},
|
||||
"value": {"type": "number", "exclusiveMinimum": 0},
|
||||
"unit": {"type": "string", "default": ""},
|
||||
"unit": {"type": "string", "default": "",
|
||||
"x-fits-part-of": "value_label"},
|
||||
"color": {"enum": ["ink", "amber", "amber_dark", "muted", "dim", "red"],
|
||||
"type": "string", "default": "ink"},
|
||||
"value_label": {"type": "string", "default": ""},
|
||||
"value_label": {"type": "string", "default": "",
|
||||
"x-fits": 30, "x-fits-hard": 49},
|
||||
},
|
||||
}},
|
||||
"properties": {
|
||||
"headline": {"type": "string", "minLength": 1},
|
||||
"headline": {"type": "string", "minLength": 1,
|
||||
"x-fits": 16, "x-fits-hard": 26},
|
||||
"bars": {"type": "array", "items": {"$ref": "#/$defs/Bar"},
|
||||
"minItems": 1, "maxItems": 3},
|
||||
"quote": {"type": "array", "items": {"type": "string"}, "maxItems": 2},
|
||||
"attribution": {"type": "string", "default": ""},
|
||||
"quote": {"type": "array", "items": {"type": "string"}, "maxItems": 2,
|
||||
"x-fits": 33, "x-fits-hard": 49},
|
||||
"attribution": {"type": "string", "default": "",
|
||||
"x-fits": 46, "x-fits-hard": 73},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -353,10 +361,16 @@ def test_a_spec_that_is_not_even_a_dict():
|
||||
def test_describe_templates_is_driven_by_what_the_service_publishes():
|
||||
text = describe_templates(TEMPLATES)
|
||||
assert "radar_sweep:" in text and "scale_bars:" in text
|
||||
assert "headline: string, no vacío, OBLIGATORIO" in text
|
||||
assert "1-3 elementos" in text # los límites llegan al prompt
|
||||
assert "ink, amber, amber_dark, muted, dim, red" in text
|
||||
assert "label: string, no vacío, OBLIGATORIO" in text # despliega los objetos anidados
|
||||
# Los dos presupuestos, y el que no tiene por dibujarse dentro de otro: el
|
||||
# modelo apunta al primero, y el segundo es el que se le comprueba.
|
||||
assert "headline: string, no vacío, CABE ~13 caracteres dibujados, " \
|
||||
"ILEGIBLE por encima de 21, OBLIGATORIO" in text
|
||||
assert "unit: string, se dibuja dentro de value_label, comparte su sitio" in text
|
||||
# Y despliega los objetos anidados, con sus presupuestos de `$defs`.
|
||||
assert "label: string, no vacío, CABE ~32 caracteres dibujados, " \
|
||||
"ILEGIBLE por encima de 53, OBLIGATORIO" in text
|
||||
|
||||
|
||||
def test_a_template_nobody_wrote_here_still_gets_described():
|
||||
@@ -644,6 +658,99 @@ def test_the_hook_note_comes_before_the_duration_one():
|
||||
assert "fecha" in notes[0] and "objetivo" in notes[1]
|
||||
|
||||
|
||||
# --- textos que se van a dibujar ilegibles -----------------------------------
|
||||
# `x-fits` es guía blanda y tiene que serlo: el ejemplo de referencia se pasa de
|
||||
# varios de sus propios presupuestos por uno o tres caracteres y se ve bien.
|
||||
# `x-fits-hard` es la otra línea, y esa sí se comprueba antes de gastar el
|
||||
# render — que es donde el aviso llegaba antes, con el vídeo ya pagado.
|
||||
|
||||
def one_shot(template="radar_sweep", **props):
|
||||
return spec_with({"template": template, "duration": 25.0, "props": props})
|
||||
|
||||
|
||||
def test_a_text_past_the_hard_budget_is_flagged():
|
||||
"""El caso real, medido sobre Cash-Landrum: un texto pidió 36 px y se dibujó
|
||||
a 20 en un fotograma de 1080 de ancho."""
|
||||
largo = "ALL THREE DEVELOPED SYMPTOMS CONSISTENT WITH RADIATION EXPOSURE"
|
||||
|
||||
note = unreadable_notes(one_shot(headline=largo), TEMPLATES)[0]
|
||||
|
||||
assert "ILEGIBLE" in note
|
||||
assert "shots.0.headline" in note and f"{len(largo)} caracteres" in note
|
||||
assert "caben 21" in note
|
||||
|
||||
|
||||
def test_a_text_between_the_two_budgets_is_left_alone():
|
||||
"""El control, y es la mitad del diseño: entre `x-fits` y `x-fits-hard` el
|
||||
texto sale un poco más pequeño y se ve bien. Avisar ahí sería gritar con
|
||||
specs buenos, y un aviso que grita se acaba ignorando — que es exactamente
|
||||
cómo el de verdad grave se pasó meses sin que nadie actuara."""
|
||||
assert len("3 RADARS TRACKING") > 13 # por encima del x-fits
|
||||
assert len("3 RADARS TRACKING") < 21 # por debajo del ilegible
|
||||
|
||||
assert unreadable_notes(one_shot(headline="3 RADARS TRACKING"), TEMPLATES) == []
|
||||
|
||||
|
||||
def test_the_budget_of_a_list_of_lines_is_per_line():
|
||||
"""Una cita se dibuja partida en líneas, así que el presupuesto es por línea.
|
||||
Medirlo sobre el texto unido avisaría de una cita bien partida en dos."""
|
||||
dos = ["A QUOTE SPLIT WHERE IT HAS TO", "BREAK SO THAT IT FITS ON SCREEN"]
|
||||
assert sum(len(x) for x in dos) > 49 and all(len(x) < 49 for x in dos)
|
||||
|
||||
ok = spec_with(shot("scale_bars", quote=dos))
|
||||
assert unreadable_notes(ok, TEMPLATES) == []
|
||||
|
||||
larga = spec_with(shot("scale_bars", quote=["X" * 60]))
|
||||
assert "shots.0.quote[0]" in unreadable_notes(larga, TEMPLATES)[0]
|
||||
|
||||
|
||||
def test_the_budget_inside_a_list_of_objects_is_found():
|
||||
"""Los presupuestos de un submodelo viven en `$defs`, y saltárselos fue justo
|
||||
el agujero por el que shortsmith se pasó meses sin medir nueve campos."""
|
||||
doc = spec_with(shot("scale_bars",
|
||||
bars=[{"label": "BOEING 747", "value": 232},
|
||||
{"label": "X" * 60, "value": 100}]))
|
||||
|
||||
note = unreadable_notes(doc, TEMPLATES)[0]
|
||||
|
||||
assert "shots.0.bars[1].label" in note
|
||||
|
||||
|
||||
def test_a_field_drawn_inside_another_is_not_judged_alone():
|
||||
"""`unit` no tiene presupuesto propio: se dibuja dentro de la cadena de
|
||||
`value_label`. Juzgarlo solo sería inventarse un límite que el contrato dice
|
||||
expresamente que no existe."""
|
||||
doc = spec_with(shot("scale_bars",
|
||||
bars=[{"label": "BOEING 747", "value": 232,
|
||||
"unit": "X" * 60}]))
|
||||
|
||||
assert unreadable_notes(doc, TEMPLATES) == []
|
||||
|
||||
|
||||
def test_an_old_contract_without_the_hard_budget_says_nothing():
|
||||
"""shortsmith publicó `x-fits-hard` en 289d50e. Contra uno anterior esto no
|
||||
puede inventarse el número: se calla, y de que el contrato lo traiga se
|
||||
encarga `test_shortsmith_live.py`, que es quien habla con el servicio."""
|
||||
viejo = {"radar_sweep": {"type": "object", "required": ["headline"],
|
||||
"properties": {"headline": {"type": "string",
|
||||
"x-fits": 13}}}}
|
||||
|
||||
assert unreadable_notes(one_shot(headline="X" * 90), viejo) == []
|
||||
|
||||
|
||||
def test_the_defects_come_before_the_duration():
|
||||
"""Los tres avisos pueden salir juntos y quien los lee coge `[0]`. Primero lo
|
||||
que está roto, después lo que está fuera de objetivo."""
|
||||
doc = spec_with({"template": "radar_sweep", "duration": 90.0,
|
||||
"props": {"headline": "8 JAN 1981",
|
||||
"subline": "X" * 60}})
|
||||
|
||||
notes = editorial_notes(doc, TEMPLATES)
|
||||
|
||||
assert len(notes) == 3
|
||||
assert "fecha" in notes[0] and "ILEGIBLE" in notes[1] and "objetivo" in notes[2]
|
||||
|
||||
|
||||
def test_the_prompt_carries_how_much_text_actually_fits():
|
||||
"""`x-fits` es el único límite que nada rechaza: si no llega al prompt, el
|
||||
modelo escribe una cita de 58 caracteres para un hueco de 16."""
|
||||
|
||||
Reference in New Issue
Block a user