feat(short): la paleta de audio viva — GET /audio en el contrato, el prompt y el bucle de edición
Build & Deploy ResearchOwl / build-and-push (push) Successful in 9s

shortsmith ya compone la banda sonora (sonar); ahora publica una paleta
(pulse, static) y este repo la consume en vivo: el prompt la ofrece con sus
notas de mood, validate_spec la usa como fuente de verdad para audio.preset,
y editar el preset en /short_spec es la manera gratis de escucharlas. Sin
/audio (404 o caída) todo cae a la paleta base y nada deja de renderizar.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-08-06 14:41:27 +00:00
co-authored by Claude Fable 5
parent 1fd0c1b3d6
commit 93a506b636
11 changed files with 259 additions and 34 deletions
+21 -4
View File
@@ -28,7 +28,8 @@ from src.config import settings
from src.db.database import OutputType, ResearchDB
from src.generator.grounding import GroundingReport, check_grounding
from src.generator.shortsmith import (
ShortsmithClient, ShortsmithError, ShortsmithRejected, ShortsmithUnavailable,
BASELINE_PRESETS, ShortsmithClient, ShortsmithError, ShortsmithRejected,
ShortsmithUnavailable,
)
from src.generator.shortspec import ShortSpecWriter, SpecWriteFailed
from src.generator.spec_contract import SpecInvalid, editorial_notes, validate_spec
@@ -138,6 +139,15 @@ class ShortProducer:
directory.mkdir(parents=True, exist_ok=True)
return directory / f"{session_id}.mp4"
async def _presets(self) -> dict[str, str]:
"""La paleta de audio, en vivo. Nunca tumba nada: sin ella se ofrece la
base y el Short sale con sonar, que es lo que salía siempre."""
try:
return await self.client.audio_presets()
except Exception as e:
logger.warning("GET /audio falló — paleta base", error=str(e))
return dict(BASELINE_PRESETS)
# --- pipeline -----------------------------------------------------------
async def produce(self, session_id: int,
@@ -171,15 +181,19 @@ class ShortProducer:
logger.warning("Short sin URL de artículo — se sigue con el dominio pelado",
session_id=session_id)
# 2. El contrato, en vivo. Sin él no hay prompt que escribir.
# 2. El contrato, en vivo. Sin él no hay prompt que escribir. La paleta
# de audio mejora el prompt pero no lo define: si /audio falla, se
# ofrece la paleta base y el render sale igual.
templates = await self.client.templates()
presets = await self._presets()
# 3. El spec.
started = time.monotonic()
llm_call = self.llm_override or self._llm_call(session_id)
writer = ShortSpecWriter(
llm_call, templates,
refresh_templates=lambda: self.client.templates(refresh=True))
refresh_templates=lambda: self.client.templates(refresh=True),
presets=presets)
try:
written = await writer.write(
topic, context, article_url=result.article_url,
@@ -251,9 +265,12 @@ class ShortProducer:
result = ShortResult(topic=topic, spec=spec)
# 1. El contrato, en vivo — las mismas rutas verbatim que ve el modelo.
# La paleta también: editar audio.preset a "pulse" es justo el tipo
# de retoque para el que existe este camino.
templates = await self.client.templates()
presets = await self._presets()
try:
validate_spec(spec, templates)
validate_spec(spec, templates, presets=presets)
except SpecInvalid as e:
result.failure = ("El spec editado no pasa el contrato: "
+ "; ".join(e.errors[:6]))