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,68 @@
|
||||
"""Prueba de fontanería contra un shortsmith VIVO.
|
||||
|
||||
Se salta salvo que se le dé una URL alcanzable desde donde corren los tests:
|
||||
|
||||
SHORTSMITH_LIVE_URL=http://10.43.86.57:8080 pytest tests/test_shortsmith_live.py -v
|
||||
|
||||
(dentro del cluster es `http://shortsmith-svc.shortsmith.svc.cluster.local:8080`;
|
||||
desde el nodo, la ClusterIP de `kubectl get svc -n shortsmith`).
|
||||
|
||||
Renderiza el ejemplo de referencia entero — ~30 s de CPU en el pod — y comprueba
|
||||
que vuelve un MP4. Es el paso 1 del §12 del spec de fase 2: probar el transporte
|
||||
antes de generar nada.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from src.generator.shortsmith import ShortsmithClient
|
||||
|
||||
LIVE_URL = os.environ.get("SHORTSMITH_LIVE_URL")
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not LIVE_URL, reason="define SHORTSMITH_LIVE_URL para probar contra el servicio vivo")
|
||||
|
||||
EXAMPLE = Path(__file__).resolve().parents[1] / "src/generator/examples/jal1628.json"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthz_and_templates():
|
||||
client = ShortsmithClient(LIVE_URL)
|
||||
|
||||
health = await client.health()
|
||||
assert health["status"] == "ok"
|
||||
assert health["templates"] >= 1
|
||||
|
||||
templates = await client.templates(refresh=True)
|
||||
# No se comprueban nombres concretos a propósito: el contrato es de
|
||||
# shortsmith y añadir plantillas allí no debe romper aquí.
|
||||
assert templates, "GET /templates devolvió vacío"
|
||||
for name, schema in templates.items():
|
||||
assert schema.get("type") == "object", f"{name} no publica un esquema de objeto"
|
||||
assert "properties" in schema
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_render_the_reference_example_end_to_end(tmp_path):
|
||||
client = ShortsmithClient(LIVE_URL)
|
||||
spec = json.loads(EXAMPLE.read_text())
|
||||
|
||||
job_id = await client.render(spec)
|
||||
seen = []
|
||||
|
||||
async def on_progress(fraction, status):
|
||||
seen.append(fraction)
|
||||
|
||||
result = await client.poll(job_id, on_progress=on_progress)
|
||||
assert result.ok, f"el job terminó en {result.status}: {result.error}"
|
||||
assert seen and max(seen) == 1.0
|
||||
|
||||
video = await client.fetch_video(job_id)
|
||||
# ftyp en los primeros bytes: es un MP4 de verdad, no una página de error.
|
||||
assert b"ftyp" in video[:32]
|
||||
assert len(video) > 100_000, f"solo {len(video)} bytes — sospechosamente corto"
|
||||
|
||||
out = tmp_path / "jal1628.mp4"
|
||||
out.write_bytes(video)
|
||||
print(f"\nrenderizado {len(video)/1e6:.2f} MB en {out}")
|
||||
Reference in New Issue
Block a user