feat(short): el ejemplo del prompt habla, y por eso los specs vuelven a hablar
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s

La primera generación con narración viva narró 5 de 7 planos. La siguiente, ya
con la sección de presupuestos de ancho en el prompt, narró 1 de 8.

La causa no era la sección 3b sino la 5: el ejemplo trabajado — un spec completo
de 8 planos presentado como "el que produjo un buen vídeo" — no llevaba ni una
narración, porque es anterior a la voz. Contra esa señal de formato, una regla
en prosa que además dice "no todos los planos necesitan una" pierde siempre.

Ahora el ejemplo narra 6 de sus 8 y calla en los dos que dibujan una cita, donde
la voz sólo competiría con palabras que ya están en pantalla. Sus duraciones
suben para pagar lo que habla: un plano que se queda corto para su propia voz
enseñaría a infradeclarar, y el render no corta la voz, alarga el plano. La
ventana de silencio se recoloca porque seguía a document_quote por tiempo.

El aviso de ancho ya no cita el hueco de 16 caracteres de quote_a: shortsmith
envuelve esas citas en dos líneas y el presupuesto vivo es 31.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-08-06 21:32:49 +00:00
co-authored by Claude Fable 5
parent 129f436fd2
commit cfe4a6d754
3 changed files with 150 additions and 34 deletions
+103 -27
View File
@@ -10,33 +10,61 @@
},
"audio": {
"preset": "sonar",
"silence": [[31.0, 36.0]]
"silence": [
[
33.5,
38.5
]
]
},
"shots": [
{
"template": "radar_sweep",
"duration": 4.0,
"duration": 6.0,
"props": {
"headline": "3 RADARS",
"subline": "1 UNEXPLAINED RETURN",
"contact_bearing_deg": 210,
"sweeps": 2
}
},
"narration": "Something pulled alongside a seven forty-seven over Alaska, and stayed there."
},
{
"template": "track_map",
"duration": 4.0,
"duration": 5.5,
"props": {
"headline": "17 NOV 1986",
"subline": "35,000 FT · 600 MPH",
"waypoints": [
{"label": "FORT YUKON", "lat": 66.57, "lon": -145.27},
{"label": "FAIRBANKS", "lat": 64.84, "lon": -147.72},
{"label": "TALKEETNA", "lat": 62.32, "lon": -150.11},
{"label": "ANCHORAGE", "lat": 61.22, "lon": -149.90}
],
"bounds": {"lat_min": 60.4, "lat_max": 67.4, "lon_min": -152.0, "lon_max": -143.5}
{
"label": "FORT YUKON",
"lat": 66.57,
"lon": -145.27
},
{
"label": "FAIRBANKS",
"lat": 64.84,
"lon": -147.72
},
{
"label": "TALKEETNA",
"lat": 62.32,
"lon": -150.11
},
{
"label": "ANCHORAGE",
"lat": 61.22,
"lon": -149.9
}
],
"bounds": {
"lat_min": 60.4,
"lat_max": 67.4,
"lon_min": -152.0,
"lon_max": -143.5
}
},
"narration": "Nothing should have been able to hold station beside them up there."
},
{
"template": "data_card",
@@ -44,13 +72,26 @@
"props": {
"card_title": "FLIGHT CREW",
"rows": [
{"key": "CAPT. KENJU TERAUCHI", "value": "PILOT IN COMMAND"},
{"key": "EX-FIGHTER PILOT", "value": "JASDF"},
{"key": "29 YEARS", "value": "FLYING EXPERIENCE"},
{"key": "10,000+", "value": "FLIGHT HOURS"}
{
"key": "CAPT. KENJU TERAUCHI",
"value": "PILOT IN COMMAND"
},
{
"key": "EX-FIGHTER PILOT",
"value": "JASDF"
},
{
"key": "29 YEARS",
"value": "FLYING EXPERIENCE"
},
{
"key": "10,000+",
"value": "FLIGHT HOURS"
}
],
"footer": "REPORTS TWO LIGHTS PACING THE AIRCRAFT"
}
},
"narration": "The man reporting it flew fighters before he flew airliners."
},
{
"template": "scale_bars",
@@ -58,7 +99,12 @@
"props": {
"headline": "REPORTED SCALE",
"bars": [
{"label": "BOEING 747", "value": 232, "unit": "FT", "color": "ink"},
{
"label": "BOEING 747",
"value": 232,
"unit": "FT",
"color": "ink"
},
{
"label": "ESTIMATED OBJECT",
"value": 2000,
@@ -67,7 +113,10 @@
"value_label": "~1,600 2,000 FT"
}
],
"quote": ["“TWICE THE SIZE OF", "AN AIRCRAFT CARRIER”"],
"quote": [
"“TWICE THE SIZE OF",
"AN AIRCRAFT CARRIER”"
],
"attribution": "— CAPT. TERAUCHI, ESTIMATE"
}
},
@@ -78,37 +127,60 @@
"headline": "EVASIVE MANEUVER",
"subline": "360° TURN · 4,000 FT",
"legend": [
{"label": "JAL 1628", "color": "ink"},
{"label": "UNIDENTIFIED CONTACT", "color": "amber"}
{
"label": "JAL 1628",
"color": "ink"
},
{
"label": "UNIDENTIFIED CONTACT",
"color": "amber"
}
],
"caption": "CONTACT HOLDS RELATIVE POSITION",
"turn_deg": 360
}
},
"narration": "He tried to shake it. Full circle, steep descent, and it was still there."
},
{
"template": "signal_strips",
"duration": 7.0,
"duration": 6.0,
"props": {
"headline": "THREE INDEPENDENT SOURCES",
"strips": [
{
"label": "ONBOARD RADAR",
"sublabel": "CONTACT 78 NM · 10 O'CLOCK",
"markers": [0.26, 0.48, 0.63, 0.81]
"markers": [
0.26,
0.48,
0.63,
0.81
]
},
{
"label": "ANCHORAGE CENTER",
"sublabel": "PRIMARY RETURNS THROUGH TURNS",
"markers": [0.26, 0.48, 0.63, 0.81]
"markers": [
0.26,
0.48,
0.63,
0.81
]
},
{
"label": "ELMENDORF ROCC",
"sublabel": "TRACKED “FLIGHT OF TWO”",
"markers": [0.26, 0.48, 0.63, 0.81]
"markers": [
0.26,
0.48,
0.63,
0.81
]
}
],
"footnote": "FAIRBANKS RADAR: NOTHING"
}
},
"narration": "He was not the only one seeing it. Though not everyone did."
},
{
"template": "document_quote",
@@ -128,10 +200,14 @@
"props": {
"count_to": 1500,
"count_label": "PAGES OF FAA DOCUMENTATION",
"lines": ["40 YEARS", "STILL OPEN"],
"lines": [
"40 YEARS",
"STILL OPEN"
],
"url": "THEEXCLUSIONZONE.COM",
"show_mark": true
}
},
"narration": "The file was never closed. It was filed, and left where anyone can read it."
}
]
}
+15 -7
View File
@@ -92,10 +92,10 @@ a card with four rows needs longer than a headline.
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.
type until it fits, so a string at twice its budget is drawn at a fraction 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.
- Colours are palette names ({colors}) — never hex.
- Quotes carry the typographic quote marks: “SPLIT RADAR IMAGE”, with U+201C \
@@ -127,8 +127,15 @@ punctuation, whole words. The on-screen props are terse and uppercase; the \
narration is a person talking. Write "seventeenth of November" rather than \
"17 NOV" — the voice reads exactly what you type, and it will say "one seven \
N-O-V" if you make it.
- Not every shot needs one. Silence over a single strong image is a choice, \
and a wall of continuous talking is not.
- **Most shots carry one.** The example narrates six of its eight and leaves \
silent exactly the two that draw a quotation, where the voice would only be \
competing with words already on the frame. Chosen silence is an edit; a spec \
with one narrated shot out of eight is not a Short with a voice, it is a Short \
that forgot to speak.
- Narration costs seconds. A shot is never cut short to fit the voice — it \
grows instead — so a line that needs six seconds in a four-second shot pushes \
your whole total past the target. Write the line, then give the shot the time \
the line actually takes.
- Everything in section 4 applies to narration word for word. It is prose you \
compose rather than a label you copy, which makes it the easiest place to \
slip in a figure no source gave you — and it is checked exactly like the rest.
@@ -155,7 +162,8 @@ state the fact plainly.
# 5. A worked example — FORMAT ONLY
This is a case_file that produced a good video. Read it for shape: how long a \
shot runs, how a headline is worded, how the shots build.
shot runs, how a headline is worded, which shots speak and which stay silent, \
how the shots build.
It is not source material. Do not reuse its strings, figures, coordinates, \
quotes or waypoints — not even if it covers the same case you were asked \
+32
View File
@@ -113,6 +113,38 @@ def test_prompt_includes_the_worked_example_in_full():
assert "counter_close" in prompt
def test_the_worked_example_narrates_most_of_its_shots():
"""El ejemplo es la señal de formato más fuerte del prompt, más que cualquier
regla en prosa. Sin narración en él, el modelo escribía specs mudos aunque la
sección 3b le dijera lo contrario: en la primera generación tras añadir la voz
narró 1 de 8 planos. Si alguien vuelve a dejar el ejemplo mudo, esto salta.
"""
example = json.loads(EXAMPLE.read_text())
shots = example["shots"]
spoken = [s for s in shots if s.get("narration")]
assert len(spoken) >= len(shots) * 0.6, "el ejemplo enseña a no narrar"
# Y el silencio del ejemplo es una decisión, no un olvido: calla justo donde
# la plantilla ya dibuja una cita.
silent = {s["template"] for s in shots if not s.get("narration")}
assert silent == {"scale_bars", "document_quote"}
def test_the_worked_example_declares_time_for_its_own_narration():
"""Un plano que se queda corto para su propia voz enseña a infradeclarar: el
render no corta la voz, alarga el plano, y el total se va del objetivo."""
from src.generator.spec_contract import NARRATION_CHARS_PER_SECOND
example = json.loads(EXAMPLE.read_text())
for i, shot in enumerate(example["shots"]):
narration = shot.get("narration", "")
if not narration:
continue
needs = len(narration) / NARRATION_CHARS_PER_SECOND
assert shot["duration"] >= needs, f"shot {i} declara menos de lo que habla"
def test_prompt_says_out_loud_that_there_is_no_article_yet():
w, _ = writer("{}")
assert "No article URL yet" in w.build_prompt("X", "m", None, "X.TEST")