Fase 3, con una corrección sobre lo que decía la §11 de la spec de fase 2.
El bloqueo no es OAuth. Los vídeos subidos por videos.insert desde un
proyecto de API sin auditar quedan restringidos a privado, y el candado es
del proyecto, no del vídeo: no se abre desde Studio, se abre pasando la
auditoría de cumplimiento de Google. Así que esto no publica. Deja el vídeo
en el canal con los metadatos puestos y devuelve el enlace de Studio para
que una persona lo revise y le dé a publicar — la misma forma que /publish
con los borradores de Ghost, y por la misma razón: el informe de fundamento
no sirve de nada si el vídeo ya está subido cuando lo lees.
Comando aparte, no un paso de /generate short_en.
- src/generator/youtube.py: refresco de token contra oauth2.googleapis.com,
subida resumable en dos pasos y metadatos derivados del shot spec ya
guardado (título, enlace al artículo, fuentes que el Short cita en
pantalla, etiquetas del tema). Sin google-api-python-client: es síncrono
y bloquearía el loop del bot; son dos peticiones HTTP y el repo ya firma
los JWT de Ghost a mano. aiohttp con SAFE_ACCEPT_ENCODING como todo lo
demás.
- Scope youtube.upload y nada más: un token filtrado no puede leer ni
borrar nada del canal, sólo subir.
- forced_private detecta que YouTube devolvió "private" cuando se pidió
otra cosa, y el aviso lo dice. Es la firma del candado, y tragárselo
haría creer que salió publicado.
- invalid_grant se traduce a su causa real: la pantalla de consentimiento
quedó en "Testing" y Google revoca esos tokens a los siete días. Es el
fallo que menos se adivina y el que más probable es encontrarse.
- get_article_url ahora excluye las filas short_en. Su published_url pasa a
ser la URL de YouTube, y sin el filtro el siguiente Short de la sesión
enlazaría al Short anterior: un bucle silencioso, porque la URL es válida
y nadie la mira dos veces.
- scripts/youtube_oauth.py, sólo stdlib: corre en el portátil, no en el
contenedor, y no debería exigir instalar nada.
- Las tres claves van optional:true en el Deployment. Sin eso, una clave que
aún no está en Infisical deja el pod en CreateContainerConfigError y tira
el bot entero por una función que nadie ha pedido todavía.
30 tests nuevos contra un servidor falso. No hay test en vivo a propósito:
cualquier ejecución real sube un vídeo a un canal de verdad, y eso no es
algo que deba pasar por teclear pytest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Generación 3B→7B y embeddings a bge-m3 en README, CLAUDE.md, .env.example y
k8s/deployment.yaml. Corregido el default buggy de config.py: ollama_embed_model
era qwen2.5:3b (un modelo de chat) → bge-m3. Añadido OLLAMA_EMBED_MODEL donde
faltaba. Nota del host público de ollama ahora tras Authentik.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Un batch de 20 fuentes concurrentes con un documento de 98k palabras y
varios PDFs grandes mató el pod (OOMKilled, límite 1Gi) el 2026-07-10 en
plena investigación.
- _extract_pdf: cap bajado de 50MB a 15MB, verificado también sobre el
body real (Content-Length puede faltar); pdfplumber movido a
run_in_executor (es síncrono y congelaba el event loop, misma clase de
bug que DDGS) con flush_cache() por página.
- _mark_scraped: contenido truncado a settings.max_content_length
(300k chars) antes de guardarlo en source_contents — libros enteros
inflan RAM y DB sin aportar al RAG.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
La sesión aiohttp de _search_searxng (camino primario de búsqueda) construía
sus headers sin Accept-Encoding: heredaba el default de aiohttp, que volvería
a anunciar br si algún día se reinstala un backend brotli. El valor vive ahora
en src/config.py (SAFE_ACCEPT_ENCODING) en vez de copiado literal por sitio.
También corrige el comentario de HEADERS que afirmaba que brotlicffi seguía
instalado como fallback — 397546a lo quitó: un br no anunciado hoy falla sin
recuperación y la fuente se pierde.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Nuevo _seed_news: registra el feed de Bing News como fuente rss y
_extract_rss (F3) siembra sus entries — sin lógica de parseo duplicada.
_unwrap_news_link extrae la URL real del publisher del ?url= de
apiclick.aspx.
Bing y no Google News como pedía el plan: verificado en vivo que los
links de news.google.com/rss/articles/ acaban en muro de consent (UE) y
el id AU_yqL solo se resuelve vía batchexecute interno — habría sembrado
URLs muertas. Los de Bing llevan la URL real en texto plano.
Flag off por defecto: se activará deliberadamente vía deployment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_search_searxng hardcodeaba engines=duckduckgo,google,bing,brave, lo
que ignoraba cualquier motor extra de la instancia (y dejaba la
búsqueda coja cuando brave cae en rate limit y DDG en CAPTCHA, ambos
observados en vivo). Ahora la lista vive en settings.searxng_engines
(env SEARXNG_ENGINES, tunable por GitOps sin rebuild).
Default ampliado con motores verificados contra la instancia el
2026-07-03: +startpage +mojeek +presearch +google news +bing news
+internet archive scholar. Medido con queries reales: ~2x resultados
y ~2x dominios únicos por ~+2s/query. Excluidos qwant (denied),
yahoo (roto), wikidata (0 en texto) y wayback machine (duplicados).
Sin cambios en k8s-manifests: use_default_settings:true ya trae todos
estos motores configurados. El fallback a DDG queda intacto.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Elimina las dos deprecaciones de Pydantic V2 (se rompían en V3):
- class Config -> model_config = SettingsConfigDict(env_file='.env')
- Field(env='X') en los 33 campos -> resolución automática por nombre
de campo (case-insensitive en pydantic-settings); verificado que los
33 nombres coinciden con su variable, así que el comportamiento es
idéntico. pytest pasa de 34 warnings a 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
src/news/monitor.py: 7 feeds UAP/OVNI, matching normalizado (NFKD sin tildes)
con word-boundary para tokens cortos (uap/ufo/nhi/aaro/ovni) y substring para
frases. poll_feeds aísla cada feed (uno caído nunca tumba el run) y parsea en
hilo aparte para no bloquear el event loop; cold-start por fuente (siembra sin
notificar). format_digest agrupa por fuente y trocea <4000 chars. /news refresca
y muestra novedades 24h. Capa desacoplada: src/news NO importa de src/bot.
No toca el scheduler (F2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
news_seen table (CREATE TABLE IF NOT EXISTS, sin migración) + índices.
Config NEWS_* (NEWS_ENABLED=false por defecto) + propiedad news_chat_id
(fallback al 1er TELEGRAM_ALLOWED_USERS). Métodos ResearchDB: source_seeded,
record_news_item, mark_news_notified, get_recent_news. Todo inerte: nada
lo invoca aún. feedparser ya estaba en requirements (6.0.12 >= 6.0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#1 batch scoring (processor):
- _score_quality_batch puntúa hasta 25 chunks por llamada a Claude en vez
de una por chunk (una fuente de 19 chunks pasaba de 19 llamadas a 1)
- parser robusto (último número por línea, padding neutro si faltan)
- fallback por-chunk con Ollama si el batch falla
#2 fuentes opcionales (config + scraper):
- ENABLE_YOUTUBE / ENABLE_REDDIT, default False: la IP del homelab está
bloqueada por Reddit (403) y YouTube (transcripts vacíos), eran peso muerto
- se saltan también las URLs de yt/reddit descubiertas dentro de webs, sin
gastar petición de red
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sección crítica:
- is_blacklisted: match por dominio/subdominio exacto (antes "x.com" como
substring bloqueaba netflix.com, phoenix.com, etc.)
- normalize_url: conserva el query string (rompía YouTube watch?v= y URLs
con ?id=); solo borra el fragment
- get_db: PRAGMA busy_timeout=5000 para evitar "database is locked" en
/compare y watches solapados
- OllamaClient.embed: usa OLLAMA_EMBED_MODEL en vez del modelo de chat
- log_api_call: coste por modelo (opus/sonnet/haiku) en vez de Haiku fijo
Mejoras:
- src/llm.py: cliente Anthropic compartido y cacheado (antes se instanciaba
uno por cada llamada/chunk)
- SEARXNG_URL configurable via env
- get_running_loop() en vez de get_event_loop() (deprecado)
- soup.title.get_text() robusto ante <title> con tags anidados
- limpieza: import muerto, total_words duplicado, w_id no usado
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>