Files
researchowl/k8s/deployment.yaml
T
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

121 lines
3.3 KiB
YAML

---
apiVersion: v1
kind: Namespace
metadata:
name: researchowl
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: researchowl-data
namespace: researchowl
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: researchowl
namespace: researchowl
labels:
app: researchowl
spec:
replicas: 1
selector:
matchLabels:
app: researchowl
template:
metadata:
labels:
app: researchowl
spec:
containers:
- name: researchowl
image: git.chemavx.xyz/chemavx/researchowl:latest
imagePullPolicy: Always
env:
- name: TELEGRAM_BOT_TOKEN
valueFrom:
secretKeyRef:
name: researchowl-secrets
key: telegram-bot-token
- name: TELEGRAM_ALLOWED_USERS
valueFrom:
secretKeyRef:
name: researchowl-secrets
key: telegram-allowed-users
- name: OLLAMA_URL
value: "http://ollama.chemavx.xyz"
- name: OLLAMA_MODEL
value: "qwen2.5:7b"
- name: OLLAMA_EMBED_MODEL
value: "bge-m3"
- name: DB_PATH
value: "/data/researchowl.db"
- name: MAX_SOURCES
value: "150"
- name: MAX_DEPTH
value: "3"
- name: QUALITY_THRESHOLD
value: "0.4"
# YouTube (/upload_short). `optional: true` a propósito: sin las
# claves el pod arranca igual y el comando contesta "no configurado".
# Sin optional, una clave que aún no existe deja el Deployment en
# CreateContainerConfigError y tira el bot entero.
- name: YOUTUBE_CLIENT_ID
valueFrom:
secretKeyRef:
name: researchowl-secrets
key: youtube-client-id
optional: true
- name: YOUTUBE_CLIENT_SECRET
valueFrom:
secretKeyRef:
name: researchowl-secrets
key: youtube-client-secret
optional: true
- name: YOUTUBE_REFRESH_TOKEN
valueFrom:
secretKeyRef:
name: researchowl-secrets
key: youtube-refresh-token
optional: true
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "1Gi"
cpu: "500m"
volumes:
- name: data
persistentVolumeClaim:
claimName: researchowl-data
imagePullSecrets:
- name: gitea-registry
---
# Secret template — fill with real values and apply manually
# kubectl create secret generic researchowl-secrets \
# --from-literal=telegram-bot-token=YOUR_TOKEN \
# --from-literal=telegram-allowed-users=YOUR_USER_ID \
# -n researchowl
apiVersion: v1
kind: Secret
metadata:
name: researchowl-secrets
namespace: researchowl
type: Opaque
stringData:
telegram-bot-token: "REPLACE_ME"
telegram-allowed-users: "REPLACE_ME"