Commit Graph
35 Commits
Author SHA1 Message Date
ChemaVXandClaude Opus 5 77029fa894 feat(youtube): la visibilidad se comprueba desde fuera, no se le cree a la API
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
`/upload_short` contaba lo que la respuesta de la subida decía del vídeo. Eso
es la palabra de la API sobre sí misma, y todo el flujo de revisión descansa en
ella: el informe de fundamento se lee ANTES de publicar sólo si subir no
publica. El 2026-08-12, mirando un vídeo recién subido, la respuesta decía
`privacyStatus: private` y el vídeo se veía sin sesión — resultó ser un clic
humano en Studio y no un fallo, pero el episodio dejó claro que no había forma
de distinguir un caso del otro.

Ahora se contrasta: oEmbed contesta 200 a un vídeo que se ve sin sesión y 404 a
uno que no. Sin credenciales, sin tocar el scope — `youtube.upload` no puede
preguntar por el estado de un vídeo, y ampliarlo a uno que sí pueda significa
darle a un token de subida permiso para vaciar el canal.

Dos decisiones que van con esto:

- **Sólo el 200 es una prueba.** Un 404 no demuestra que el vídeo sea privado:
  también lo devuelve uno que YouTube aún no ha indexado. Por eso el negativo
  se mira dos veces y, si sigue negativo, se cuenta como "no se ve desde
  fuera", no como "es privado".
- **No haber podido comprobar no es haber comprobado que no.** Un fallo de red
  deja `reachable=None` y el parte lo dice, en vez de heredar la garantía que
  no tiene.

Cuando la API dice privado y el vídeo se ve, el aviso va en la PRIMERA línea
del mensaje de Telegram: enterarse tiene que costar cero atención.

Verificado contra la realidad — el mismo vídeo daba True antes de ocultarlo y
False después; un vídeo borrado y uno privado dan False, y uno público True.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 22:09:39 +00:00
ChemaVXandClaude Fable 5 129f436fd2 feat(short): el prompt sabe cuánto texto cabe dibujado, y el informe grita si quedó ilegible
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
x-fits llega vivo del contrato y se pone delante del modelo: es el único
límite que nada rechaza, porque el renderizador encoge en vez de fallar. Sin
esto el modelo escribe una cita de 58 caracteres para un hueco de 16 y se
entera cuando ya está renderizada.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 16:19:48 +00:00
ChemaVXandClaude Fable 5 a13c3062b6 feat(short): narración — el grounding la cubre, el contrato la admite y el prompt la guía
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
El comprobador va primero, antes que el campo (fase 2 §12): la narración es
prosa que el modelo redacta, no una etiqueta que copia, y es donde se cuela
una cifra sin fuente. De paso, la huella de una cifra pasa a ser número +
unidad canónica: con la voz repitiendo la pantalla, '35,000 FT' y '35,000
feet' son el mismo dato y contarlos dos veces inflaría el informe del que
depende la revisión humana.

editorial_notes estima la duración CON la voz: la declarada es un suelo y sin
esto el modelo escribiría 40 s de shots, les colgaría narración y se enteraría
del Short de 65 s cuando ya está pagado.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 15:38:34 +00:00
ChemaVXandClaude Fable 5 93a506b636 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>
2026-08-06 14:41:27 +00:00
ChemaVXandClaude Fable 5 cc1f0cab93 fix(short): cerrar el bucle de edición y el desfase spec/vídeo en la subida
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m17s
Dos huecos de la revisión:

1. /short_spec prometía re-renderizar el spec editado pero no existía el
   camino de vuelta. Ahora un .json adjunto se valida contra el contrato
   vivo (errores con su ruta verbatim), se re-comprueba el fundamento (la
   edición pudo meter una cifra nueva), se guarda como output nuevo y se
   renderiza. Sin LLM: este camino es gratis. La sesión sale del nombre
   del fichero (short_{id}_spec.json), que Telegram conserva al reenviar.

2. /upload_short podía subir un vídeo viejo con metadatos nuevos: produce
   guarda el spec ANTES de renderizar, así que un re-intento con render
   fallido deja en disco el MP4 de la vuelta anterior. Ahora se compara el
   mtime del vídeo con el created_at del spec y se niega (force lo salta).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:46:06 +00:00
ChemaVXandClaude Opus 5 ec3e6d05c0 feat(youtube): subir Shorts al canal con /upload_short
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>
2026-08-03 14:43:53 +00:00
ChemaVXandClaude Opus 5 20c8d03aa7 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>
2026-08-01 21:55:42 +00:00
ChemaVXandClaude Fable 5 187d29a372 feat(bot): marcar sesiones 'running' huérfanas como 'interrupted' al arrancar
Las tareas de research viven solo en memoria (_active_tasks): un reinicio
del pod las mata sin tocar la DB y sus sesiones quedan en 'running' para
siempre — parecen activas en /status y get_active_session. Nuevo estado
ResearchStatus.INTERRUPTED y barrido en _on_startup antes de la purga.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:38:43 +00:00
ChemaVXandClaude Fable 5 2cb0cf841c feat(deps): python-telegram-bot 21.5 -> 22.8
Build & Deploy ResearchOwl / build-and-push (push) Successful in 49s
Salto mayor revisado contra el changelog oficial:
- La 22.0 elimina lo deprecado en v20.x. De esa lista el bot solo usaba
  disable_web_page_preview (3 sitios), que ademas resulta seguir vivo
  como shim en 22.8 — se migra igualmente a link_preview_options, que
  es la API canonica desde Bot API 7.0.
- No usamos quote=, proxy_url, ni *_timeout en run_polling (el resto
  de eliminaciones). Python >=3.10 (imagen 3.12) y httpx >=0.27,<0.29
  (tenemos 0.28.1) OK.
- Verificado aislado: import de src.bot.bot sin DeprecationWarnings,
  8 tests en verde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:17:21 +00:00
ChemaVXandClaude Opus 4.8 b1c5bc737d feat(news): F2 — scheduler automático del monitor RSS (inerte)
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Engancha el monitor de noticias al scheduler loop EXISTENTE de
watched_topics (sin segunda task asyncio). Cada tick, si NEWS_ENABLED,
y si ha pasado NEWS_POLL_INTERVAL_HOURS desde el último poll (estado en
memoria), pollea los feeds, empuja los pendientes (notified=0) al chat
destino (NEWS_CHAT_ID o 1er TELEGRAM_ALLOWED_USERS) y los marca.

Best-effort: la rama de noticias va en su propio try/except — un fallo
del news-poll NUNCA tumba el scheduler de watched_topics. Deploy inerte:
NEWS_ENABLED sigue False; el flip se hará aparte en k8s-manifests.

- db: get_unnotified(limit=None) — pendientes, recientes primero (NULLS LAST)
- bot: rama news al final del tick del scheduler, reusa poll_feeds /
  item_from_row / format_digest de F1; "…y N más" si excede NEWS_MAX_ITEMS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 10:16:07 +00:00
ChemaVXandClaude Opus 4.8 c0b0c1ba0a feat(news): F1 — monitor RSS best-effort + comando /news manual
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
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>
2026-06-26 20:57:26 +00:00
ChemaVXandClaude Opus 4.8 7a995da88f feat(seo): wire autofill into publish path behind SEO_AUTOFILL (default off, dry-run support)
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 14:14:39 +00:00
ChemaVXandClaude Opus 4.8 cd557a2d71 perf(db): conexión SQLite compartida por proceso
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
get_db() devuelve un proxy sobre una única conexión real reutilizada durante
toda la vida del proceso, en vez de abrir una conexión nueva y re-ejecutar
todo el SCHEMA en cada comando/scoring.

- _SharedConnection: proxy con close() no-op → los handlers conservan el
  patrón get_db()/finally close() sin cambios
- aiosqlite serializa las operaciones en el hilo de la conexión: compartirla
  entre coroutines es seguro y elimina la contención del lock de escritura a
  nivel de fichero (scheduler solapado, /compare con 2 sesiones)
- close_db() vía post_shutdown para checkpoint WAL limpio al apagar

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 15:16:24 +00:00
ChemaVXandClaude Opus 4.8 bf275b7f82 fix: correcciones de scraping/DB y mejoras de robustez
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>
2026-06-15 14:38:03 +00:00
ChemaVXandClaude Opus 4.8 fd9aaa193b fix: watch diff — envío con fallback a texto plano y task no silencioso
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
El resumen de diff generado por Claude se enviaba con parse_mode=MARKDOWN;
texto libre con entidades desbalanceadas provocaba BadRequest y el mensaje
no llegaba. Además el send_message estaba fuera del try/except de _task y el
task se retenía en _active_tasks sin await, así que la excepción se tragaba
sin log alguno.

- _safe_send(): intenta Markdown y reintenta en texto plano si falla
- _task: except de nivel superior que loguea cualquier fallo

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 18:39:55 +00:00
ChemaVXandClaude Opus 4.8 41e4e3f5d6 feat: /watch --at HH:MM — hora absoluta en Europe/Madrid
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:55:52 +00:00
ChemaVX f577ac4712 feat: Ghost EN — /generate blog en publica en inglés en theexclusionzone.com
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m19s
2026-05-18 16:49:09 +00:00
ChemaVXandClaude Sonnet 4.6 747b9605c0 fix: cmd_publish usa _active_sessions para la sesión correcta
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Mismo patrón que cmd_generate: si hay sesión activa registrada
para el chat, consulta por id; si no, fallback a created_at DESC.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:40:52 +00:00
ChemaVXandClaude Sonnet 4.6 caf763c23e fix: cmd_generate usa _active_sessions para la sesión correcta
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Si hay una sesión activa registrada para el chat, se consulta
directamente por id en lugar de por created_at DESC, evitando
que /generate use la sesión más reciente en vez de la actual.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:38:06 +00:00
ChemaVXandClaude Sonnet 4.6 bdea12e6f2 fix: nombre de archivo .md usa topic del output, no del session pre-cacheado
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
El session dict se obtiene antes de generator.generate() y puede
contener datos de la sesión anterior. Ahora se extrae el topic
directamente de la línea "Topic:" del header del output generado,
que siempre refleja la sesión actual usada en la generación.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 15:18:09 +00:00
ChemaVXandClaude Sonnet 4.6 83eb2359be feat: Ghost CMS integration — auto-publish blog + /publish command
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-08 10:26:22 +00:00
ChemaVX 7a156e2af1 fix: mover alerta de coste a /generate donde está el gasto real
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
2026-05-06 07:49:29 +00:00
ChemaVX 279475a175 feat: alerta de coste — aviso si sesión supera COST_ALERT_THRESHOLD
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
2026-05-06 07:23:11 +00:00
ChemaVX e8034f3f37 feat: /compare — análisis comparativo de dos temas en paralelo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 34s
2026-05-06 06:40:31 +00:00
ChemaVX 53cf7a04a8 feat: modo diff para /watch — notifica solo si hay novedades reales
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
2026-05-05 07:43:41 +00:00
ChemaVX 4c7f5b521b feat: fase 3 — export PDF con reportlab + /export command
Build & Deploy ResearchOwl / build-and-push (push) Successful in 1m2s
2026-05-04 12:57:21 +00:00
ChemaVX a47d7b26ca feat: fase 2 — generación por secciones report_extended, blog_extended, podcast_extended
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
2026-05-04 10:58:06 +00:00
ChemaVX b5518ac95a feat: scheduler /watch — watched_topics + scheduler loop + /watch /unwatch /watches
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
2026-05-04 07:48:05 +00:00
ChemaVX b33ae202b8 feat: trackeo de coste por llamada Claude — tabla api_usage + /costs
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
2026-05-03 20:06:06 +00:00
ChemaVX a681627d2e feat: TTL purge — purge_old_sessions + /purge command + startup hook
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
2026-05-03 16:56:37 +00:00
ChemaVX 7704f071d6 feat: retry+backoff en scraper, ProgressReporter en bot
Build & Deploy ResearchOwl / build-and-push (push) Successful in 6s
2026-05-03 16:40:37 +00:00
ChemaVXandClaude Sonnet 4.6 5feff6073e fix: send new message if edit_text fails silently in /process
Build & Deploy ResearchOwl / build-and-push (push) Successful in 7s
If the bot restarted between sending the progress message and the
completion callback, edit_text may fail silently (Conflict/stale ref).
Store completion text and reply_text as fallback so the user always
sees the result.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:53:59 +00:00
ChemaVXandClaude Sonnet 4.6 c4fb33fbf5 fix: WAL mode for concurrent reads, skipped stats, anti-repetition prompts
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
database.py: enable PRAGMA journal_mode=WAL + synchronous=NORMAL so
  /status reads from concurrent connections see committed data without
  blocking behind the scraper's writes; add 'skipped' to get_session_stats

bot.py: show skipped count in fmt_progress and cmd_status; use 'or 0'
  to guard against NULL from SUM(); label active research in /status

processor.py: raise generate() temperature default to 0.7 + add
  repeat_penalty=1.15/repeat_last_n=128 to Ollama options to stop
  qwen2.5:3b from looping; scoring prompt keeps temperature=0.1

generator.py: rewrite all prompts with explicit "NEVER repeat"
  constraints and distinct-content rules per section; podcast prompt
  now asks for spoken-word style (no formal headers); reduce thread
  to 12-18 tweets (was 15-25) to fit model context; pass temperature=0.7

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 10:15:30 +00:00
ChemaVXandClaude Sonnet 4.6 0c7176dd0b fix: add /process command, log quality filtering, improve Reddit headers
Build & Deploy ResearchOwl / build-and-push (push) Successful in 5s
- bot.py: add cmd_process handler to manually trigger chunk processing
  on the last session; register CommandHandler("process")
- processor.py: log exceptions from asyncio.gather instead of silently
  dropping them; add per-chunk quality score debug logging; warn when
  all chunks filtered by quality threshold with actionable hint;
  raise fallback score to 0.6 so Ollama failures don't filter chunks
- exhaustive.py: replace bot User-Agent with full browser UA + headers
  for REDDIT_HEADERS; downgrade Reddit 403 from warning to info since
  server IPs are routinely blocked; use content_type=None on json()
  to avoid aiohttp content-type mismatch errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 20:37:39 +00:00
ChemaVX ba08536337 feat: initial ResearchOwl
Build & Deploy ResearchOwl / build (push) Failing after 1m38s
2026-04-27 13:49:07 +00:00