feat(short): el prompt sabe cuánto texto cabe dibujado, y el informe grita si quedó ilegible
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
x-fits llega vivo del contrato y se pone delante del modelo: es el único límite que nada rechaza, porque el renderizador encoge en vez de fallar. Sin esto el modelo escribe una cita de 58 caracteres para un hueco de 16 y se entera cuando ya está renderizada. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a13c3062b6
commit
129f436fd2
+9
-1
@@ -473,9 +473,17 @@ def _claims_message(result) -> str:
|
|||||||
|
|
||||||
if trimmed:
|
if trimmed:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
# Un recorte grave no es un titular más pequeño, es uno ilegible: se
|
||||||
|
# separa para que no se pierda entre los cosméticos.
|
||||||
|
severe = [w for w in trimmed if w.get("severe")]
|
||||||
lines.append(f"✂️ {len(trimmed)} textos recortados al dibujar:")
|
lines.append(f"✂️ {len(trimmed)} textos recortados al dibujar:")
|
||||||
for w in trimmed[:5]:
|
for w in trimmed[:5]:
|
||||||
lines.append(f" • [{w.get('template', '?')}] {str(w.get('text', ''))[:60]}")
|
mark = "🔴 " if w.get("severe") else ""
|
||||||
|
lines.append(f" • {mark}[{w.get('template', '?')}] "
|
||||||
|
f"{str(w.get('text', ''))[:60]}")
|
||||||
|
if severe:
|
||||||
|
lines.append(f" 🔴 {len(severe)} quedaron ILEGIBLES (dibujados a menos "
|
||||||
|
"de la mitad): acorta ese texto y reenvía el spec.")
|
||||||
|
|
||||||
if spoken:
|
if spoken:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|||||||
@@ -90,6 +90,12 @@ not a target. Aim for {target_min:.0f}-{target_max:.0f}.
|
|||||||
a card with four rows needs longer than a headline.
|
a card with four rows needs longer than a headline.
|
||||||
- Every string is drawn as given. Write them the way they should appear: \
|
- Every string is drawn as given. Write them the way they should appear: \
|
||||||
SHORT, UPPERCASE, no trailing punctuation. A headline is 2-5 words.
|
SHORT, UPPERCASE, no trailing punctuation. A headline is 2-5 words.
|
||||||
|
- **"CABE ~N caracteres dibujados" is a width, and it is the one limit nothing \
|
||||||
|
will catch for you.** Nothing rejects a longer string: the renderer shrinks the \
|
||||||
|
type until it fits, so a quote of 58 characters in a 16-character slot is drawn \
|
||||||
|
at a quarter of its size and ends up the smallest text on a frame it was \
|
||||||
|
supposed to dominate. Stay at or under N. On a quote that means picking a \
|
||||||
|
shorter verbatim span, never squeezing the whole sentence in.
|
||||||
- Respect every max length and list-length limit above. They are enforced.
|
- Respect every max length and list-length limit above. They are enforced.
|
||||||
- Colours are palette names ({colors}) — never hex.
|
- Colours are palette names ({colors}) — never hex.
|
||||||
- Quotes carry the typographic quote marks: “SPLIT RADAR IMAGE”, with U+201C \
|
- Quotes carry the typographic quote marks: “SPLIT RADAR IMAGE”, with U+201C \
|
||||||
|
|||||||
@@ -406,6 +406,12 @@ def _describe_field(name: str, schema: dict, required: bool, defs: dict,
|
|||||||
bits.append("no vacío")
|
bits.append("no vacío")
|
||||||
if "maxLength" in schema:
|
if "maxLength" in schema:
|
||||||
bits.append(f"máx {schema['maxLength']} caracteres")
|
bits.append(f"máx {schema['maxLength']} caracteres")
|
||||||
|
# `x-fits` es cuánto texto cabe DIBUJADO al tamaño de diseño, medido por
|
||||||
|
# shortsmith contra sus propias fuentes. No se valida — los caracteres son
|
||||||
|
# un proxy de los píxeles — pero es lo único que evita que el modelo escriba
|
||||||
|
# una cita de 58 caracteres en un hueco de 16 y salga dibujada ilegible.
|
||||||
|
if "x-fits" in schema:
|
||||||
|
bits.append(f"CABE ~{schema['x-fits']} caracteres dibujados")
|
||||||
for key, text in (("minimum", "≥"), ("maximum", "≤"),
|
for key, text in (("minimum", "≥"), ("maximum", "≤"),
|
||||||
("exclusiveMinimum", ">"), ("exclusiveMaximum", "<")):
|
("exclusiveMinimum", ">"), ("exclusiveMaximum", "<")):
|
||||||
if key in schema:
|
if key in schema:
|
||||||
|
|||||||
@@ -165,3 +165,21 @@ class TestNarrationWarnings:
|
|||||||
{"kind": "narration", "text": "no voice installed"}]))
|
{"kind": "narration", "text": "no voice installed"}]))
|
||||||
assert "1 textos recortados" in text # sólo cuenta el de dibujo
|
assert "1 textos recortados" in text # sólo cuenta el de dibujo
|
||||||
assert "🔇 no voice installed" in text
|
assert "🔇 no voice installed" in text
|
||||||
|
|
||||||
|
|
||||||
|
class TestSevereShrink:
|
||||||
|
"""Un recorte leve es cosmético; uno grave deja el texto ilegible justo
|
||||||
|
donde importaba. Mezclarlos entrena al lector a ignorar los dos."""
|
||||||
|
|
||||||
|
def test_a_severe_shrink_is_called_out(self):
|
||||||
|
text = _claims_message(result_with(render_warnings=[
|
||||||
|
{"template": "document_quote", "text": "“UNA CITA MUY LARGA”",
|
||||||
|
"requested": 84, "size": 20, "severe": True}]))
|
||||||
|
assert "🔴" in text and "ILEGIBLES" in text
|
||||||
|
|
||||||
|
def test_a_cosmetic_shrink_stays_quiet(self):
|
||||||
|
text = _claims_message(result_with(render_warnings=[
|
||||||
|
{"template": "scale_bars", "text": "PHYSICAL EVIDENCE",
|
||||||
|
"requested": 92, "size": 84, "severe": False}]))
|
||||||
|
assert "recortados" in text
|
||||||
|
assert "ILEGIBLES" not in text and "🔴" not in text
|
||||||
|
|||||||
@@ -323,3 +323,27 @@ def test_narration_that_overshoots_the_target_is_flagged_as_narration():
|
|||||||
def test_a_spec_without_narration_keeps_the_old_wording():
|
def test_a_spec_without_narration_keeps_the_old_wording():
|
||||||
note = editorial_notes(spec_with(*[shot(duration=10.0) for _ in range(6)]))[0]
|
note = editorial_notes(spec_with(*[shot(duration=10.0) for _ in range(6)]))[0]
|
||||||
assert "duración total" in note and "estimada" not in note
|
assert "duración total" in note and "estimada" not in note
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
templates = {"document_quote": {
|
||||||
|
"type": "object", "required": ["quote_a"],
|
||||||
|
"properties": {"quote_a": {"type": "string", "minLength": 1, "x-fits": 16}}}}
|
||||||
|
|
||||||
|
described = describe_templates(templates)
|
||||||
|
|
||||||
|
assert "CABE ~16 caracteres" in described
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_string_longer_than_it_fits_is_still_valid():
|
||||||
|
"""Los caracteres son un proxy de los píxeles: rechazar por ancho estimado
|
||||||
|
tiraría specs que se dibujan perfectamente."""
|
||||||
|
templates = {"radar_sweep": {
|
||||||
|
"type": "object", "additionalProperties": False, "required": ["headline"],
|
||||||
|
"properties": {"headline": {"type": "string", "minLength": 1, "x-fits": 13}}}}
|
||||||
|
doc = spec_with({"template": "radar_sweep", "duration": 25.0,
|
||||||
|
"props": {"headline": "UN TITULAR BASTANTE MAS LARGO QUE ESO"}})
|
||||||
|
|
||||||
|
validate_spec(doc, templates)
|
||||||
|
|||||||
Reference in New Issue
Block a user