feat(short): generación y render de Shorts vía shortsmith
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:
ChemaVX
2026-08-01 21:55:42 +00:00
co-authored by Claude Opus 5
parent 8b81ef87e4
commit 20c8d03aa7
27 changed files with 4350 additions and 23 deletions
+21 -11
View File
@@ -442,6 +442,24 @@ class ContentProcessor:
"""
Retrieve most relevant chunks for a query using embeddings + keyword fallback
"""
top_chunks = await self.rag_chunks(session_id, query, top_k)
# Build context
context_parts = []
for chunk in top_chunks:
source_label = f"[{chunk.get('source_type', 'web').upper()}] {chunk.get('title', 'Unknown')}"
context_parts.append(f"{source_label}:\n{chunk['content']}")
return "\n\n---\n\n".join(context_parts)
async def rag_chunks(self, session_id: int, query: str,
top_k: int = 20) -> list[dict]:
"""Los chunks en sí, no el contexto ya montado.
Mismo ranking que `rag_query` (que ahora llama aquí). Hace falta para el
comprobador de fundamento: comprueba contra EXACTAMENTE el material que
se le pasó al modelo, y para eso necesita las filas, con su `url`.
"""
# Get query embedding
query_embedding = await self.ollama.embed(query)
@@ -462,15 +480,7 @@ class ContentProcessor:
scored.append((sim * 0.7 + chunk["quality_score"] * 0.3, chunk))
scored.sort(key=lambda x: x[0], reverse=True)
top_chunks = [c for _, c in scored[:top_k]]
else:
# Fallback: just use quality score
top_chunks = chunks[:top_k]
return [c for _, c in scored[:top_k]]
# Build context
context_parts = []
for chunk in top_chunks:
source_label = f"[{chunk.get('source_type', 'web').upper()}] {chunk.get('title', 'Unknown')}"
context_parts.append(f"{source_label}:\n{chunk['content']}")
return "\n\n---\n\n".join(context_parts)
# Fallback: just use quality score
return chunks[:top_k]