feat(short): generación y render de Shorts vía shortsmith
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s
Añade /generate short_en y /short_spec. El pipeline genera un shot spec con Haiku, verifica cada cifra, fecha y cita contra los chunks de la sesión, lo renderiza en shortsmith y entrega el MP4 por Telegram junto a un informe de claims. - ShortsmithClient con sondeo y fallback al spec JSON si el render falla - Contrato de plantillas obtenido de GET /templates, no codificado - Comprobación de fundamento determinista, sin LLM - outputs.published_url para enlazar el artículo de Ghost - Normalización de comillas rectas a tipográficas (ver KNOWN-ISSUES.md) Lo que no aparece en los chunks se contrasta contra el ejemplo del prompt: si casa ahí es fuga, no invención, y se informa como tal. El purgado de sesiones se lleva también su MP4. La subida a YouTube queda fuera a propósito: fase 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""El mensaje de revisión del Short.
|
||||
|
||||
Es la puerta humana: si este mensaje no sale, o sale sin los avisos, se está
|
||||
publicando lo que el modelo recuerde en vez de lo que dicen las fuentes. Por eso
|
||||
tiene test propio aparte del pipeline.
|
||||
"""
|
||||
import json
|
||||
|
||||
from src.bot.bot import _claims_message
|
||||
from src.generator.grounding import check_grounding
|
||||
from src.generator.short import ShortResult
|
||||
|
||||
SPEC = {
|
||||
"version": 1,
|
||||
"meta": {"id": "x", "title": "X"},
|
||||
"shots": [{"template": "scale_bars", "duration": 30.0, "props": {
|
||||
"headline": "REPORTED SCALE",
|
||||
"bars": [{"label": "BOEING 747", "value": 232, "unit": "FT"}]}}],
|
||||
}
|
||||
CHUNKS = [{"content": "A Boeing 747 is 232 ft long.", "url": "https://a.test/1"}]
|
||||
|
||||
|
||||
def result_with(**kw):
|
||||
base = dict(topic="Caso X", spec=SPEC, title="X", attempts=1,
|
||||
cost_usd=0.0042, duration_s=30.0,
|
||||
article_url="https://www.theexclusionzone.com/caso-x/",
|
||||
grounding=check_grounding(SPEC, CHUNKS))
|
||||
base.update(kw)
|
||||
return ShortResult(**base)
|
||||
|
||||
|
||||
def test_a_clean_report_still_says_so():
|
||||
"""Un éxito silencioso enseña al lector a dejar de mirar."""
|
||||
text = _claims_message(result_with())
|
||||
assert "0 sin encontrar" in text
|
||||
assert "1 chunks de 1 URLs" in text
|
||||
assert "Coste: $0.0042" in text
|
||||
|
||||
|
||||
def test_ungrounded_claims_are_listed_one_by_one():
|
||||
invented = json.loads(json.dumps(SPEC))
|
||||
invented["shots"][0]["props"]["headline"] = "41,000 FT"
|
||||
text = _claims_message(result_with(spec=invented,
|
||||
grounding=check_grounding(invented, CHUNKS)))
|
||||
assert "1 sin encontrar" in text
|
||||
assert "41,000 FT" in text
|
||||
|
||||
|
||||
def test_a_session_without_an_article_url_says_what_to_run():
|
||||
text = _claims_message(result_with(article_url=None))
|
||||
assert "/generate blog en" in text
|
||||
|
||||
|
||||
def test_render_warnings_reach_the_human():
|
||||
text = _claims_message(result_with(render_warnings=[
|
||||
{"template": "data_card", "text": "UNA FILA DEMASIADO LARGA",
|
||||
"requested": 44, "size": 38}]))
|
||||
assert "recortados" in text and "data_card" in text
|
||||
|
||||
|
||||
def test_there_is_a_report_even_when_there_was_no_spec():
|
||||
text = _claims_message(ShortResult(topic="Caso X"))
|
||||
assert "Sin comprobación de fundamento" in text
|
||||
assert "Coste:" in text
|
||||
Reference in New Issue
Block a user