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
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:
co-authored by
Claude Fable 5
parent
129f436fd2
commit
cfe4a6d754
@@ -10,33 +10,61 @@
|
|||||||
},
|
},
|
||||||
"audio": {
|
"audio": {
|
||||||
"preset": "sonar",
|
"preset": "sonar",
|
||||||
"silence": [[31.0, 36.0]]
|
"silence": [
|
||||||
|
[
|
||||||
|
33.5,
|
||||||
|
38.5
|
||||||
|
]
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"shots": [
|
"shots": [
|
||||||
{
|
{
|
||||||
"template": "radar_sweep",
|
"template": "radar_sweep",
|
||||||
"duration": 4.0,
|
"duration": 6.0,
|
||||||
"props": {
|
"props": {
|
||||||
"headline": "3 RADARS",
|
"headline": "3 RADARS",
|
||||||
"subline": "1 UNEXPLAINED RETURN",
|
"subline": "1 UNEXPLAINED RETURN",
|
||||||
"contact_bearing_deg": 210,
|
"contact_bearing_deg": 210,
|
||||||
"sweeps": 2
|
"sweeps": 2
|
||||||
}
|
},
|
||||||
|
"narration": "Something pulled alongside a seven forty-seven over Alaska, and stayed there."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"template": "track_map",
|
"template": "track_map",
|
||||||
"duration": 4.0,
|
"duration": 5.5,
|
||||||
"props": {
|
"props": {
|
||||||
"headline": "17 NOV 1986",
|
"headline": "17 NOV 1986",
|
||||||
"subline": "35,000 FT · 600 MPH",
|
"subline": "35,000 FT · 600 MPH",
|
||||||
"waypoints": [
|
"waypoints": [
|
||||||
{"label": "FORT YUKON", "lat": 66.57, "lon": -145.27},
|
{
|
||||||
{"label": "FAIRBANKS", "lat": 64.84, "lon": -147.72},
|
"label": "FORT YUKON",
|
||||||
{"label": "TALKEETNA", "lat": 62.32, "lon": -150.11},
|
"lat": 66.57,
|
||||||
{"label": "ANCHORAGE", "lat": 61.22, "lon": -149.90}
|
"lon": -145.27
|
||||||
],
|
},
|
||||||
"bounds": {"lat_min": 60.4, "lat_max": 67.4, "lon_min": -152.0, "lon_max": -143.5}
|
{
|
||||||
|
"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",
|
"template": "data_card",
|
||||||
@@ -44,13 +72,26 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"card_title": "FLIGHT CREW",
|
"card_title": "FLIGHT CREW",
|
||||||
"rows": [
|
"rows": [
|
||||||
{"key": "CAPT. KENJU TERAUCHI", "value": "PILOT IN COMMAND"},
|
{
|
||||||
{"key": "EX-FIGHTER PILOT", "value": "JASDF"},
|
"key": "CAPT. KENJU TERAUCHI",
|
||||||
{"key": "29 YEARS", "value": "FLYING EXPERIENCE"},
|
"value": "PILOT IN COMMAND"
|
||||||
{"key": "10,000+", "value": "FLIGHT HOURS"}
|
},
|
||||||
|
{
|
||||||
|
"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"
|
"footer": "REPORTS TWO LIGHTS PACING THE AIRCRAFT"
|
||||||
}
|
},
|
||||||
|
"narration": "The man reporting it flew fighters before he flew airliners."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"template": "scale_bars",
|
"template": "scale_bars",
|
||||||
@@ -58,7 +99,12 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"headline": "REPORTED SCALE",
|
"headline": "REPORTED SCALE",
|
||||||
"bars": [
|
"bars": [
|
||||||
{"label": "BOEING 747", "value": 232, "unit": "FT", "color": "ink"},
|
{
|
||||||
|
"label": "BOEING 747",
|
||||||
|
"value": 232,
|
||||||
|
"unit": "FT",
|
||||||
|
"color": "ink"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "ESTIMATED OBJECT",
|
"label": "ESTIMATED OBJECT",
|
||||||
"value": 2000,
|
"value": 2000,
|
||||||
@@ -67,7 +113,10 @@
|
|||||||
"value_label": "~1,600 – 2,000 FT"
|
"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"
|
"attribution": "— CAPT. TERAUCHI, ESTIMATE"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,37 +127,60 @@
|
|||||||
"headline": "EVASIVE MANEUVER",
|
"headline": "EVASIVE MANEUVER",
|
||||||
"subline": "360° TURN · −4,000 FT",
|
"subline": "360° TURN · −4,000 FT",
|
||||||
"legend": [
|
"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",
|
"caption": "CONTACT HOLDS RELATIVE POSITION",
|
||||||
"turn_deg": 360
|
"turn_deg": 360
|
||||||
}
|
},
|
||||||
|
"narration": "He tried to shake it. Full circle, steep descent, and it was still there."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"template": "signal_strips",
|
"template": "signal_strips",
|
||||||
"duration": 7.0,
|
"duration": 6.0,
|
||||||
"props": {
|
"props": {
|
||||||
"headline": "THREE INDEPENDENT SOURCES",
|
"headline": "THREE INDEPENDENT SOURCES",
|
||||||
"strips": [
|
"strips": [
|
||||||
{
|
{
|
||||||
"label": "ONBOARD RADAR",
|
"label": "ONBOARD RADAR",
|
||||||
"sublabel": "CONTACT 7–8 NM · 10 O'CLOCK",
|
"sublabel": "CONTACT 7–8 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",
|
"label": "ANCHORAGE CENTER",
|
||||||
"sublabel": "PRIMARY RETURNS THROUGH TURNS",
|
"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",
|
"label": "ELMENDORF ROCC",
|
||||||
"sublabel": "TRACKED “FLIGHT OF TWO”",
|
"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"
|
"footnote": "FAIRBANKS RADAR: NOTHING"
|
||||||
}
|
},
|
||||||
|
"narration": "He was not the only one seeing it. Though not everyone did."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"template": "document_quote",
|
"template": "document_quote",
|
||||||
@@ -128,10 +200,14 @@
|
|||||||
"props": {
|
"props": {
|
||||||
"count_to": 1500,
|
"count_to": 1500,
|
||||||
"count_label": "PAGES OF FAA DOCUMENTATION",
|
"count_label": "PAGES OF FAA DOCUMENTATION",
|
||||||
"lines": ["40 YEARS", "STILL OPEN"],
|
"lines": [
|
||||||
|
"40 YEARS",
|
||||||
|
"STILL OPEN"
|
||||||
|
],
|
||||||
"url": "THEEXCLUSIONZONE.COM",
|
"url": "THEEXCLUSIONZONE.COM",
|
||||||
"show_mark": true
|
"show_mark": true
|
||||||
}
|
},
|
||||||
|
"narration": "The file was never closed. It was filed, and left where anyone can read it."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
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 \
|
- **"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 \
|
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 \
|
type until it fits, so a string at twice its budget is drawn at a fraction of \
|
||||||
at a quarter of its size and ends up the smallest text on a frame it was \
|
its size and ends up the smallest text on a frame it was supposed to dominate. \
|
||||||
supposed to dominate. Stay at or under N. On a quote that means picking a \
|
Stay at or under N. On a quote that means picking a shorter verbatim span, \
|
||||||
shorter verbatim span, never squeezing the whole sentence in.
|
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 \
|
||||||
@@ -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 \
|
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 \
|
"17 NOV" — the voice reads exactly what you type, and it will say "one seven \
|
||||||
N-O-V" if you make it.
|
N-O-V" if you make it.
|
||||||
- Not every shot needs one. Silence over a single strong image is a choice, \
|
- **Most shots carry one.** The example narrates six of its eight and leaves \
|
||||||
and a wall of continuous talking is not.
|
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 \
|
- 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 \
|
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.
|
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
|
# 5. A worked example — FORMAT ONLY
|
||||||
|
|
||||||
This is a case_file that produced a good video. Read it for shape: how long a \
|
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, \
|
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 \
|
quotes or waypoints — not even if it covers the same case you were asked \
|
||||||
|
|||||||
@@ -113,6 +113,38 @@ def test_prompt_includes_the_worked_example_in_full():
|
|||||||
assert "counter_close" in prompt
|
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():
|
def test_prompt_says_out_loud_that_there_is_no_article_yet():
|
||||||
w, _ = writer("{}")
|
w, _ = writer("{}")
|
||||||
assert "No article URL yet" in w.build_prompt("X", "m", None, "X.TEST")
|
assert "No article URL yet" in w.build_prompt("X", "m", None, "X.TEST")
|
||||||
|
|||||||
Reference in New Issue
Block a user