fix(short): el prompt pedía tres cosas que no podían cumplirse a la vez
El modelo declaraba menos segundos de los que su propia línea necesitaba en 3 o 4 de cada 5 planos narrados — entre 3,8 y 8,2 s de deriva por Short. El vídeo salía con la duración correcta porque shortsmith estira, pero el ritmo visual que el spec escribía no era el que se renderizaba. No era pereza del modelo: era un juego de reglas insatisfacible. El prompt pedía a la vez líneas de hasta 18 palabras, planos de 6 s como mucho, y tiempo declarado suficiente para la propia voz. 18 palabras piden 7,3 s, así que las tres juntas son imposibles y el modelo rompía la única que nadie comprobaba. Tres capas, las tres deterministas y sin gastar una generación: - La regla enseñada llevaba el mismo error de clase que el estimador tenía antes del 2026-08-12: palabras por segundo a secas, sin el término de las pausas. Medida contra las 28 líneas que el bot ha narrado de verdad, `words/2.75 + 0.5` se quedaba corta en 14 y hasta 2,27 s — obedecerla al pie de la letra seguía infradeclarando. Ahora es de dos términos, como la voz, y el peor caso baja a 1,27 s en 5 de 28. - El tope de palabras por línea se DERIVA del plano más largo (`max_words_in`) en vez de escribirse a mano. La contradicción no puede volver. - El ejemplo de referencia incumplía su propia regla en 2 de sus 6 líneas, y el ejemplo es la señal más fuerte del prompt: le estábamos enseñando el fallo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,7 +139,7 @@
|
||||
"caption": "CONTACT HOLDS RELATIVE POSITION",
|
||||
"turn_deg": 360
|
||||
},
|
||||
"narration": "He tried to shake it. Full circle, steep descent, and it was still there."
|
||||
"narration": "He tried to shake it. Full circle, steep descent, and it stayed there."
|
||||
},
|
||||
{
|
||||
"template": "signal_strips",
|
||||
@@ -207,7 +207,7 @@
|
||||
"url": "THEEXCLUSIONZONE.COM",
|
||||
"show_mark": true
|
||||
},
|
||||
"narration": "The file was never closed. It was filed, and left where anyone can read it."
|
||||
"narration": "The file was never closed. It was left where anyone can read it."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+41
-19
@@ -23,7 +23,8 @@ import structlog
|
||||
|
||||
from src.generator.spec_contract import (
|
||||
SpecInvalid, describe_templates, editorial_notes, estimated_duration,
|
||||
validate_spec, NARRATION_WORDS_PER_SECOND,
|
||||
max_words_in, validate_spec, NARRATION_ROUNDED_PAD,
|
||||
NARRATION_SENTENCE_SILENCE, NARRATION_WORDS_PER_SECOND,
|
||||
TARGET_MAX_DURATION, TARGET_MIN_DURATION,
|
||||
)
|
||||
|
||||
@@ -49,20 +50,26 @@ NOTE_ATTEMPTS = 1
|
||||
#: El ejemplo de referencia habla 74. La sesión 167 habló 97 y salió a 47,5 s.
|
||||
NARRATION_WORD_BUDGET = 80
|
||||
|
||||
#: Lo que mide una línea. No es preferencia de estilo: son las líneas del
|
||||
#: ejemplo (11, 12, 10, 14, 12, 15 palabras). El tope anterior — "menos de 25" —
|
||||
#: no describía nada que el canal hubiera publicado, y el modelo escribió líneas
|
||||
#: de 25 y 27 palabras sin saltarse ninguna regla.
|
||||
NARRATION_WORDS_PER_LINE = 12
|
||||
NARRATION_WORDS_PER_LINE_MAX = 18
|
||||
|
||||
#: Lo que dura un plano como mucho. También del ejemplo (6,0 s el más largo,
|
||||
#: 5,6 de media). Sin este tope el modelo declaraba 42 s en seis planos de siete
|
||||
#: Lo que dura un plano como mucho. Del ejemplo (6,0 s el más largo, 5,6 de
|
||||
#: media). Sin este tope el modelo declaraba 42 s en seis planos de siete
|
||||
#: segundos y LUEGO les colgaba la narración encima: el primer borrador salía a
|
||||
#: 50 s las tres veces que se midió, y hacía falta una reescritura entera para
|
||||
#: bajarlo.
|
||||
MAX_SHOT_DURATION = 6.0
|
||||
|
||||
#: Lo que mide una línea. No es preferencia de estilo: es la media del ejemplo.
|
||||
NARRATION_WORDS_PER_LINE = 12
|
||||
|
||||
#: Y el tope duro NO se escribe a mano: es cuántas palabras caben en el plano
|
||||
#: más largo que se permite declarar. Escribirlos por separado fue el fallo que
|
||||
#: hizo que el modelo infradeclarase casi todos sus planos, y no por pereza: el
|
||||
#: prompt le pedía a la vez líneas de hasta 18 palabras, planos de 6 s como
|
||||
#: mucho y tiempo declarado suficiente para su propia voz. Las tres a la vez son
|
||||
#: imposibles — 18 palabras piden 7,3 s — así que el modelo rompía la única que
|
||||
#: nadie comprobaba, la duración declarada. Derivando el tope de la duración, la
|
||||
#: contradicción no puede volver.
|
||||
NARRATION_WORDS_PER_LINE_MAX = max_words_in(MAX_SHOT_DURATION, sentences=2)
|
||||
|
||||
EXAMPLE_PATH = Path(__file__).parent / "examples" / "jal1628.json"
|
||||
|
||||
__all__ = ["ShortSpecWriter", "SpecResult", "SpecWriteFailed", "NARRATIVE_SHAPES"]
|
||||
@@ -156,11 +163,12 @@ aloud by the renderer and burned in as captions. Write it for the ear.
|
||||
watches the rest, and the opening shot's narration is those two seconds. Lead \
|
||||
with the strangest true thing you have, not with a preamble.
|
||||
- **Keep a line to {words_per_line} words, hard stop at {words_per_line_max}.** \
|
||||
That is the example's own average, and it is not a style preference: a 25-word \
|
||||
line is three seconds of your whole budget spent on one shot. Long sentences lose the listener and \
|
||||
stretch the shot; the renderer will not cut your voice off, it will make the \
|
||||
shot longer instead, and a Short that drifts past {target_max:.0f} seconds is a \
|
||||
Short people leave.
|
||||
That is the example's own average, and the hard stop is not a style preference \
|
||||
either — it is exactly as much as fits in the longest shot you are allowed to \
|
||||
declare. {words_per_line_max} words is {max_shot:.0f} seconds; the same limit, \
|
||||
written twice. Go past it and the shot has to grow, because the renderer will \
|
||||
not cut your voice off — it makes the shot longer instead, and a Short that \
|
||||
drifts past {target_max:.0f} seconds is a Short people leave.
|
||||
- **Do not read the screen aloud.** The captions already show your words and \
|
||||
the template already shows its own. If the shot draws "35,000 FT", the voice \
|
||||
says what that altitude meant, not the number again.
|
||||
@@ -176,12 +184,24 @@ with one narrated shot out of eight is not a Short with a voice, it is a Short \
|
||||
that forgot to speak.
|
||||
- **Give every narrated shot enough time for its own line, and work it out \
|
||||
rather than guessing.** The voice reads about {words_per_second:.1f} words a \
|
||||
second and pauses a quarter second at every full stop, so:
|
||||
second and pauses a quarter second at every full stop, so **count the words AND \
|
||||
count the sentences**:
|
||||
|
||||
duration ≥ words ÷ {words_per_second:.1f} + half a second
|
||||
duration ≥ words ÷ {words_per_second:.1f} + {sentence_pause} × sentences + \
|
||||
{rounded_pad}
|
||||
|
||||
A twelve-word line needs five seconds; give that shot 5.0, not 4.0. This is \
|
||||
the one rule that makes your own arithmetic true: a shot runs for the LONGER of \
|
||||
The second term is the one that catches people out. "Witness identities. \
|
||||
Sensor details. Locations redacted." is six words and three full stops: it is \
|
||||
not a fast line, it is three quarters of a second of silence on top. Two lines \
|
||||
of the same length do not take the same time if one of them is chopped.
|
||||
|
||||
Worked, on the example below: shot 1 speaks twelve words in one sentence, so \
|
||||
12 ÷ {words_per_second:.1f} + {sentence_pause} + {rounded_pad} = 5.1, and it \
|
||||
declares 5.5. Shot 4 speaks thirteen words in two sentences, so 13 ÷ \
|
||||
{words_per_second:.1f} + 0.5 + {rounded_pad} = 5.7, and it declares 6.0. Round \
|
||||
up, never down.
|
||||
|
||||
This is the one rule that makes your own arithmetic true: a shot runs for the LONGER of \
|
||||
its declared duration and its line — never shorter, the voice is never cut off \
|
||||
— so a shot that declares less than its line silently grows, and the video ends \
|
||||
up longer than the durations you wrote. Hold this rule and the total you \
|
||||
@@ -466,6 +486,8 @@ class ShortSpecWriter:
|
||||
words_per_line=NARRATION_WORDS_PER_LINE,
|
||||
words_per_line_max=NARRATION_WORDS_PER_LINE_MAX,
|
||||
max_shot=MAX_SHOT_DURATION,
|
||||
sentence_pause=f"{NARRATION_SENTENCE_SILENCE:g}",
|
||||
rounded_pad=f"{NARRATION_ROUNDED_PAD:g}",
|
||||
example=_load_example(),
|
||||
article=article,
|
||||
context=context,
|
||||
|
||||
@@ -29,6 +29,9 @@ __all__ = [
|
||||
"editorial_notes",
|
||||
"estimated_duration",
|
||||
"spoken_seconds",
|
||||
"sentence_count",
|
||||
"teachable_seconds",
|
||||
"max_words_in",
|
||||
"describe_templates",
|
||||
"TARGET_MIN_DURATION",
|
||||
"TARGET_MAX_DURATION",
|
||||
@@ -350,11 +353,54 @@ NARRATION_PAD = 0.45
|
||||
#: le pide que recorte texto, no tiempo.
|
||||
NARRATION_WORDS_PER_SECOND = 2.75
|
||||
|
||||
#: El respiro redondeado hacia arriba, para la regla que se le enseña al modelo.
|
||||
#: `NARRATION_PAD` son 0,45 s; "medio segundo" se sostiene en la cabeza y va
|
||||
#: sobrado, que es la dirección correcta en la que equivocarse.
|
||||
NARRATION_ROUNDED_PAD = 0.5
|
||||
|
||||
#: Final de frase: un punto pegado a la palabra y seguido de espacio o de nada.
|
||||
#: El decimal de "1.5" no cuenta, y por eso mira lo que va detrás.
|
||||
_SENTENCE_END = re.compile(r"[.!?](?=\s|$)")
|
||||
|
||||
|
||||
def sentence_count(line: str) -> int:
|
||||
"""Frases de una línea, contadas como las cuenta Piper para sus pausas."""
|
||||
return max(1, len(_SENTENCE_END.findall(line))) if line.strip() else 0
|
||||
|
||||
|
||||
def teachable_seconds(words: int, sentences: int = 1) -> float:
|
||||
"""Lo que hay que DECLARAR para una línea, en las unidades que el modelo cuenta.
|
||||
|
||||
Es `spoken_seconds` traducido de caracteres a palabras. La traducción hace
|
||||
falta porque un LLM no cuenta caracteres de fiar, pero sí cuenta palabras y
|
||||
puntos — y la regla tiene que ser computable por quien debe obedecerla, o no
|
||||
es una regla, es un deseo.
|
||||
|
||||
Los dos términos son los mismos que los de la voz. La versión anterior del
|
||||
prompt colapsaba el segundo en un "+ medio segundo" fijo, y ese es el mismo
|
||||
error de clase que tenía el estimador antes del 2026-08-12: sin pausa por
|
||||
frase, una línea troceada en frases cortas se da por rápida. Medido contra
|
||||
las 28 líneas reales, aquella regla se quedaba corta en 14 y hasta 2,27 s —
|
||||
o sea que un modelo que la obedeciera al pie de la letra seguiría
|
||||
infradeclarando la mitad de sus planos. Con el término por frase el peor
|
||||
caso baja a 1,27 s y sólo en 5 de 28.
|
||||
"""
|
||||
return (words / NARRATION_WORDS_PER_SECOND
|
||||
+ sentences * NARRATION_SENTENCE_SILENCE
|
||||
+ NARRATION_ROUNDED_PAD)
|
||||
|
||||
|
||||
def max_words_in(seconds: float, sentences: int = 1) -> int:
|
||||
"""Cuántas palabras caben en un plano de esa duración, según la regla de arriba.
|
||||
|
||||
Existe para que el tope de palabras por línea y el tope de duración de plano
|
||||
no puedan volver a contradecirse: se deriva uno del otro en vez de escribir
|
||||
los dos a mano.
|
||||
"""
|
||||
room = seconds - sentences * NARRATION_SENTENCE_SILENCE - NARRATION_ROUNDED_PAD
|
||||
return max(1, int(room * NARRATION_WORDS_PER_SECOND))
|
||||
|
||||
|
||||
def spoken_seconds(line: str) -> float:
|
||||
"""Lo que tarda la voz en decir una línea, sin el respiro final.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user