feat(short): narración — el grounding la cubre, el contrato la admite y el prompt la guía
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s

El comprobador va primero, antes que el campo (fase 2 §12): la narración es
prosa que el modelo redacta, no una etiqueta que copia, y es donde se cuela
una cifra sin fuente. De paso, la huella de una cifra pasa a ser número +
unidad canónica: con la voz repitiendo la pantalla, '35,000 FT' y '35,000
feet' son el mismo dato y contarlos dos veces inflaría el informe del que
depende la revisión humana.

editorial_notes estima la duración CON la voz: la declarada es un suelo y sin
esto el modelo escribiría 40 s de shots, les colgaría narración y se enteraría
del Short de 65 s cuando ya está pagado.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-08-06 15:38:34 +00:00
co-authored by Claude Fable 5
parent 93a506b636
commit a13c3062b6
9 changed files with 375 additions and 13 deletions
+34 -2
View File
@@ -168,6 +168,24 @@ class Claim:
def norm(self) -> str:
return normalize(self.text)
@property
def fingerprint(self) -> tuple:
"""Identidad del dato, no de su redacción — para no contar dos veces.
Una cifra se identifica por su número y su unidad CANÓNICA: "35,000 FT"
dibujado en pantalla y "35,000 feet" dicho en la narración son el mismo
dato, y con la narración esa coincidencia pasa a ser lo normal, no la
excepción. Contarlos por separado inflaría justo el informe del que
depende la revisión humana.
Lo demás se identifica por su forma normalizada: una cita reformulada
NO es la misma cita, y ahí la literalidad es el criterio correcto.
"""
if self.kind == "figure":
number = normalize(self.text.split()[0]) if self.text.split() else ""
return ("figure", number, self.unit)
return (self.kind, self.norm)
_QUOTED = re.compile(r'["“„‟«]([^"“”„‟«»]{3,})'
r'["”„‟»]')
@@ -315,6 +333,16 @@ def extract_claims(spec: dict) -> list[Claim]:
`meta` queda fuera a propósito: el título del spec no se dibuja en ningún
fotograma, es el nombre del fichero.
`narration` SÍ entra, y es de lo más importante que entra: lo que se dibuja
en pantalla son etiquetas cortas que el modelo copia, pero la narración es
prosa que redacta — el sitio natural para deslizar una cifra de más. Se
trata como cualquier prosa del spec: aporta sus citas, cifras y fechas. No
aporta nombres propios, por lo mismo que no los aportan `headline` o
`caption`: una frase entera no es una etiqueta identificadora, y sacar
nombres de dentro de la prosa exigiría adivinar por mayúsculas y llenaría
el informe de ruido. La cifra y la cita, que son lo que se fabrica, están
cubiertas.
"""
claims: list[Claim] = []
for i, shot in enumerate(spec.get("shots") or []):
@@ -328,12 +356,16 @@ def extract_claims(spec: dict) -> list[Claim]:
# Reconstruye "232 FT" a partir de {value: 232, unit: "FT"}.
for path_prefix, sub in _dicts_with_value_and_unit(props, base):
shot_claims = _attach_units(sub, shot_claims, path_prefix)
narration = shot.get("narration")
if isinstance(narration, str):
shot_claims.extend(
_claims_from_text(narration, f"shots.{i}.narration", "narration"))
claims.extend(shot_claims)
seen: set[tuple[str, str]] = set()
seen: set[tuple] = set()
unique: list[Claim] = []
for claim in claims:
fingerprint = (claim.kind, claim.norm)
fingerprint = claim.fingerprint
if not claim.norm or fingerprint in seen:
continue
seen.add(fingerprint)