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:
ChemaVX
2026-08-13 20:38:51 +00:00
co-authored by Claude Opus 5
parent 77029fa894
commit 8000ef2145
4 changed files with 163 additions and 26 deletions
+46
View File
@@ -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.