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:
@@ -651,8 +651,27 @@ class OutputGenerator:
|
||||
# never buried inside the long .md document). None on the flag-off path.
|
||||
self.last_publish_notice: str | None = None
|
||||
|
||||
async def _remember_article_url(self, ghost: "GhostPublisher", post: dict,
|
||||
output_id: int | None) -> None:
|
||||
"""Guarda en la fila de `outputs` la URL que tendrá el artículo.
|
||||
|
||||
Se construye desde el slug (`{sitio}/{slug}/`) y no desde el `url` que
|
||||
devuelve Ghost, porque el post es un DRAFT y ese campo trae la URL de
|
||||
previsualización. Si Jose cambia el slug al publicar, el enlace habrá
|
||||
que rehacerlo — el Short lo enseña en la descripción, no en el vídeo.
|
||||
|
||||
Best-effort: nunca bloquea la publicación.
|
||||
"""
|
||||
if output_id is None or not post.get("slug"):
|
||||
return
|
||||
try:
|
||||
await self.db.set_output_url(output_id, f"{ghost.url}/{post['slug']}/")
|
||||
except Exception as e:
|
||||
logger.warning("No se pudo guardar la URL del artículo", error=str(e))
|
||||
|
||||
async def _publish_blog_to_ghost(self, lang: str, full_output: str, topic: str,
|
||||
session_id: int, seo_override: str | None) -> str:
|
||||
session_id: int, seo_override: str | None,
|
||||
output_id: int | None = None) -> str:
|
||||
"""Publish a blog DRAFT to Ghost, gated by the SEO autofill mode.
|
||||
|
||||
Returns the ghost_notice to APPEND to the returned document (flag-off /
|
||||
@@ -703,6 +722,7 @@ class OutputGenerator:
|
||||
self.last_publish_notice = (
|
||||
_seo_live_message(ghost, post, seo, inserted_pairs)
|
||||
+ collision_note)
|
||||
await self._remember_article_url(ghost, post, output_id)
|
||||
logger.info("Auto-published blog to Ghost",
|
||||
mode=mode, post_id=post["id"], links=len(inserted_pairs))
|
||||
return ""
|
||||
@@ -720,6 +740,7 @@ class OutputGenerator:
|
||||
try:
|
||||
result = await ghost.publish_draft(title, full_output)
|
||||
post = result["posts"][0]
|
||||
await self._remember_article_url(ghost, post, output_id)
|
||||
logger.info("Auto-published blog to Ghost (bare)", post_id=post["id"])
|
||||
return _bare_ghost_notice(ghost, post) + collision_note
|
||||
except Exception as e:
|
||||
@@ -797,13 +818,13 @@ class OutputGenerator:
|
||||
full_output = header + "\n\n" + output
|
||||
|
||||
# Save to DB
|
||||
await self.db.save_output(session_id, output_type, full_output)
|
||||
output_id = await self.db.save_output(session_id, output_type, full_output)
|
||||
|
||||
# Auto-publish to Ghost for blog outputs (autofill mode gated inside helper).
|
||||
ghost_notice = ""
|
||||
if output_type in (OutputType.BLOG, OutputType.BLOG_EXTENDED):
|
||||
ghost_notice = await self._publish_blog_to_ghost(
|
||||
lang, full_output, topic, session_id, seo_override)
|
||||
lang, full_output, topic, session_id, seo_override, output_id)
|
||||
|
||||
logger.info("Output generated", type=output_type, length=len(full_output))
|
||||
return full_output + ghost_notice
|
||||
@@ -960,13 +981,13 @@ class OutputGenerator:
|
||||
header = self._build_header(topic, output_type, session, stats)
|
||||
full_output = header + "\n\n" + full_content
|
||||
|
||||
await self.db.save_output(session_id, output_type, full_output)
|
||||
output_id = await self.db.save_output(session_id, output_type, full_output)
|
||||
|
||||
# Auto-publish to Ghost for extended blog outputs (autofill mode gated inside).
|
||||
ghost_notice = ""
|
||||
if output_type == OutputType.BLOG_EXTENDED:
|
||||
ghost_notice = await self._publish_blog_to_ghost(
|
||||
lang, full_output, topic, session_id, seo_override)
|
||||
lang, full_output, topic, session_id, seo_override, output_id)
|
||||
|
||||
logger.info("Extended output generated", type=output_type,
|
||||
sections=len(sections), length=len(full_output))
|
||||
|
||||
Reference in New Issue
Block a user