Skip to content

ADR-0004: Stash + LLM local sobre RTX 3080 — auto-captioning y búsqueda semántica

Campo Valor
Status Proposed
Date 2026-05-17
Decision by ramon
Reviewers claude (Opus 4.7 1M)
Supersedes
Builds on ADR-0003 (saneamiento + crisis NVMe), memoria ai_tagger_windows_setup.md, media_pipeline.md

Contexto

Stash (VM 208 media-208, lazy via Sablier en nopor.monxas.casa) gestiona ~4.6 TB de librería en /mnt/nfs_media/nopor/ (NAS TerraMaster F4-423, montado NFS). El metadata actual viene de:

  1. Scrapers Stash estándar (StashDB, ThePornDB) — cubre material reconocido, mediocre para amateur/uploads sueltos.
  2. AI Tagger (NSFW AI Model Server, 192.168.0.115:8000, WSL2 Ubuntu en sobremesa Windows con RTX 3080) — clasificador discriminativo: bodyparts (fearless_terrain) y actions (gentler_river). Modelos Patreon Member tier, output = tags fijos de un vocabulario cerrado.

Lo que NO existe: descripción libre de la escena (mood, escenario, narrativa, intensidad), búsqueda semántica ("escenas en cocina por la mañana", "ambiente tenso silencioso", "POV con conversación previa"), ni similarity search.

La hipótesis es que un VLM (vision-language model) local + embeddings sobre la RTX 3080 puede cerrar ese gap sin enviar nada fuera de LAN (requisito no negociable por contenido sensible) y reutilizando hardware ya pagado.

El problema operativo de la plataforma (peso real en la decisión)

La queja literal del usuario:

"El mayor problema es la absoluta guerra para usar Windows y WSL para trabajar con ella: que no se duerma, llegar con SSH, etc."

Detalle de cada fricción medida en los últimos meses:

  1. Sleep/hibernate Windows mata WSL → GPU inaccesible. Caffeine/presentationsettings funciona a medias; tras Windows Update, settings se resetean.
  2. SSH a WSL desde LAN requiere netsh portproxy con IP WSL dinámica (cambia tras cada reboot). ai_tagger_windows_setup.md documenta el hack: regenerar portproxy a mano.
  3. WoL inconsistente porque Windows pasa por Modern Standby (S0ix) en vez de S3 — el NIC a veces no responde al magic packet.
  4. Windows Update reinicia sin avisar → AI Tagger cae hasta que alguien hace login físico (no hay autologin documentado).
  5. Drivers NVIDIA en WSL se desincronizan (libcuda.so.1 mismatch) tras updates. Fix manual cada vez.
  6. Doble systemd: el de Windows (Services) + el de WSL (systemd inside) → debugging requiere wsl -d Ubuntu --user root systemctl ... con quoting hell de cmd.exe español.
  7. Locale español del Windows host rompe scripts (separador decimal coma, fechas dd/mm/yyyy en logs).

Esto NO es coste despreciable: el AI Tagger ya funciona y aún así requiere intervención manual ~1× cada 2-3 semanas. Añadir encima un VLM 13B + pipeline de captioning multiplica la superficie de fallos.

Por tanto, este ADR decide platform-first, idea-después. Si la plataforma sigue siendo Windows+WSL, el proyecto vivirá con la misma deuda operativa que el AI Tagger; si se cambia a Linux bare-metal, hay una inversión one-shot que paga durante años.

Decisiones

D1. Plataforma RTX 3080: Linux bare-metal (wipe Windows) — Opción C

Decisión: reformatear el desktop sobremesa (192.168.0.115) a Ubuntu Server 24.04 LTS + nvidia-driver-550 + Docker + Ollama nativo. Eliminar Windows.

Alternativas evaluadas:

Opción Esfuerzo Reversibilidad Pain points resueltos Pierde
A. Windows+WSL + paliativos (autologin, Caffeine, WG nativo Win) Bajo (días) Total Sleep parcial, SSH parcial. Updates y drivers SIGUEN rotos. Nada
B. Ollama nativo Windows (sin WSL) Bajo (1 día) Total Quita la capa WSL (drivers, IP dinámica, portproxy). Sleep/updates/locale siguen. Nada
C. Linux bare-metal (wipe) Medio (1-2 días, backup previo) Baja (reinstall Win es 1 día más) Todos los 7 pain points Gaming Windows nativo, Stable Diffusion GUI (Auto1111 corre igual en Linux), AI Tagger Patreon .pt2.encverificar que carga en Linux (es PyTorch CUDA, debería)
D. GPU passthrough a VM Proxmox Alto (mover GPU físicamente a pmx-50, vfio, IOMMU, ATX→cluster, refrigeración) Muy baja (desmontar PC) Todos + integración cluster Gaming, slot PCIe del PC, exige cambiar caja/PSU del pmx-50 (Mini-PC, no acepta 3080: ya descartado en contexto del ADR)
E. Dual boot + remote selector Medio Total Solo si boot correcto; complejidad WoL+grub-reboot Tiempo de switching, mental overhead
F. Mini-PC + eGPU futuro Muy alto (compra >€800) N/A Todos Dinero, espacio

Por qué C y no B: Opción B es tentadora ("mínimo cambio, máximo retorno") pero solo resuelve el 30% del dolor (drivers WSL, portproxy). El 70% restante (sleep, updates, locale) es Windows el problema, no WSL. Pagar el coste de un wipe una sola vez y operar el host como cualquier otro LXC del homelab (SSH key-based, systemd, apt, prom-node-exporter, integrado en Loki/Grafana) es estratégicamente superior.

Por qué C y no D: passthrough a Proxmox sería ideal arquitectónicamente, pero pmx-50 es un Mini-PC (Ryzen 7735HS) que físicamente no admite una RTX 3080 (sin slot PCIe x16, sin espacio, sin PSU). Está descartado por hardware.

Riesgos asumidos en C: - Modelos Patreon .pt2.enc: cargan via PyTorch+CUDA. Linux es el target nativo de PyTorch; riesgo bajo. Verificar antes del wipe con un boot live USB Ubuntu + test del nsfw_ai_model_server. - Pérdida de Windows gaming: confirmar con el usuario; si juega regularmente, opción E (dual boot) es plan B. - Stable Diffusion: Auto1111 / ComfyUI corren mejor en Linux. No-loss. - Tarde de instalación: ~4-6h reales (backup config WSL, wipe, install Ubuntu, drivers, recrear ai-server + nuevo stack VLM).

D2. Stack VLM: LLaVA-NeXT 7B (no 13B) + Ollama, con fallback Qwen2-VL 7B

Decisión: usar LLaVA-NeXT 7B (alias llava-llama3 o llava:7b-v1.6 en Ollama) como modelo principal de captioning. NO 13B.

Justificación VRAM RTX 3080 10GB:

Modelo VRAM Q4_K_M VRAM FP16 Tokens/s estimados RTX 3080
LLaVA-NeXT 7B ~5.5 GB ~14 GB (no cabe FP16) 40-60 tok/s
LLaVA-NeXT 13B ~9.5 GB ~26 GB 15-25 tok/s, margen 0.5 GB = OOM con cualquier otro tenant en GPU
Qwen2-VL 7B ~6 GB ~14 GB 35-50 tok/s, mejor en reasoning visual
InternVL2 8B ~7 GB n/a 30-45 tok/s, top en benchmarks abiertos

La RTX 3080 con 10 GB es muy justa para 13B Q4 si además hay que coexistir con: - nsfw_ai_model_server (~2 GB residente) - Embeddings e5-large (~1.5 GB) - ffmpeg con NVDEC (~500 MB context)

Total presupuesto realista: ~9.5 GB de GPU disponible para VLM → 7B holgado, 13B en tensión permanente.

Trade-off aceptado: 7B describe escenas con menos detalle narrativo que 13B; para tags semánticos y mood es suficiente. Si más adelante quality of captions resulta pobre, ruta upgrade es swap a un mini-PC con 4060 Ti 16GB o esperar 5070 (12-16GB). NO meter una segunda GPU al sobremesa actual (PSU/térmica).

D3. Pipeline de procesamiento: ffmpeg → keyframes → VLM → caption → embedding → Stash custom fields

Flujo definitivo:

[Stash plugin trigger: "AI_CaptionMe" tag] 
    → POST scene_id a caption-worker (FastAPI en sobremesa Linux)
    → ffmpeg -skip_frame nokey -vf "select=eq(pict_type\,I),scale=512:-1"
       extrae 6 keyframes (heurística: 1 de los primeros 10%, 4 distribuidos, 1 de los últimos 10%)
    → para cada frame: LLaVA-NeXT 7B con prompt:
       "Describe this scene briefly: setting, mood, number of subjects, 
        camera angle, intensity (low/medium/high). One sentence per axis."
    → caption merger: concatenación + LLM pass final (mismo LLaVA en text-only)
       que produce JSON {setting, mood, subjects, angle, intensity, summary}
    → embeddings e5-large del summary → pgvector (nueva tabla en postgres LXC 251)
    → POST GraphQL Stash: sceneUpdate(details, custom_fields)
       custom fields: ai_caption_setting, ai_caption_mood, ai_caption_intensity
    → tag remove "AI_CaptionMe", tag add "AI_Captioned"

Latencia esperada por escena (6 frames, 7B Q4 a 50 tok/s, ~80 tokens output/frame): - ffmpeg extract: 2-4s - 6× VLM inferencia: 6 × (10s prompt processing + 1.6s generation) ≈ 70s - Merge LLM pass: 5s - Embeddings: <1s - Stash GraphQL: <500ms - Total: ~80-90s por escena

Con la librería actual (~4.6 TB, asumamos ~8000 escenas a media 600 MB) → ~190 horas de cómputo = 8 días 24/7 o 3-4 semanas a ritmo nocturno. Aceptable como batch inicial one-shot.

Después, marginal: escenas nuevas se procesan al ingest (Stash event hook).

D4. Búsqueda semántica via pgvector (NO Qdrant nuevo)

Decisión: usar PostgreSQL 16 + pgvector existente en LXC 251 (rag, ya tiene postgres para asturias-rag con e5-large). Crear schema stash_semantic con tablas scene_embeddings (scene_id, embedding vector(1024), caption_summary text, indexed_at).

Por qué no Qdrant/Milvus: ya hay pgvector funcionando, e5-large cargado, backups del LXC 251 incluidos en PBS. Añadir Qdrant = 1 servicio más, 1 backup más, 1 dashboard más. ANN de pgvector con HNSW es sobradamente suficiente para 8000-50000 vectores.

Trade-off: si la librería crece a >500K scenes o se quiere multi-tenant, migrar a Qdrant es plan futuro (no en este ADR).

D5. UI: Stash plugin frontend + endpoint /similar

Decisión: extender el plugin ai_tagger existente (o crear ai_caption paralelo) con:

  1. Nuevos filtros en Scenes page: dropdowns mood, setting, intensity (alimentados por custom fields).
  2. Botón "Find similar" en cada scene → llama /similar?scene_id=X&k=20 del caption-worker → devuelve scene_ids ranked por cosine similarity → frontend redirige a Stash filter id IN (...).
  3. Search bar libre: "escenas con ambiente íntimo nocturno" → embed query con e5-large → ANN top-k.

No tocar Stash core (es upstream). Plugin frontend usa Stash plugin API + custom GraphQL endpoint.

D6. Coexistencia con AI Tagger existente

Decisión: AI Tagger (nsfw_ai_model_server) sigue corriendo en paralelo, ahora como systemd nativo Ubuntu (no WSL). Caption-worker es servicio separado :8001. Ambos cargan modelos a demanda; GPU memory manager de Ollama hace swap cuando hay presión.

Trade-off: si los 3 procesos están activos simultáneamente, el primero en pedir VRAM gana y los otros encolan. Para el batch inicial de captioning, parar AI Tagger temporalmente vía systemd.

D7. Observabilidad

Decisión: el host sobremesa entra al stack centralizado como cualquier otro: - node_exporter :9100 → Prometheus VM 208 - nvidia_gpu_exporter :9835 → métricas VRAM, temperatura, power, utilization - Logs caption-worker + ai-server → Loki via promtail - Dashboard Grafana "GPU host sobremesa" (VRAM split, queue depth, latencia caption p50/p95) - Alertas: nvidia_gpu_temperature_celsius > 82 for 5m, caption_worker_queue_depth > 100 for 10m, up{instance="sobremesa:9100"} == 0 for 2m

Trade-offs globales

  • Privacidad: 100% local, ningún byte sale de LAN. ✅ Requisito cumplido.
  • Coste hardware: €0 incremental (hardware ya pagado).
  • Coste operativo: -70% friction vs Windows+WSL (estimado). Linux host se integra al runbook estándar del homelab.
  • Coste tiempo: ~6h wipe+reinstall + ~1-2 semanas tuning pipeline + 1-3 semanas batch inicial.
  • Vendor lock-in: cero. Ollama, LLaVA, e5-large, pgvector son todos OSS.
  • Riesgo: el wipe es la operación más arriesgada (irreversible-ish). Mitigación: backup completo de la config WSL ai-server + modelos Patreon .pt2.enc a NAS antes del wipe, y test boot live USB Ubuntu + carga del modelo Patreon antes de formatear.

Alternativas descartadas (resumen)

  • Opción A (paliar Windows): pone tiritas, no soluciona updates ni locale. Deuda perpetua.
  • Opción B (Ollama nativo Win): resuelve solo capa WSL, no Windows. Coste/beneficio peor que C a medio plazo.
  • Opción D (passthrough Proxmox): bloqueado por hardware (Mini-PC pmx-50 no admite GPU full-size).
  • Opción E (dual boot): complejidad WoL+grub, mental overhead. Solo si usuario confirma necesidad gaming.
  • Opción F (eGPU futuro): deferred, no resuelve hoy.
  • VLM 13B: VRAM-bound en 10 GB, riesgo OOM permanente.
  • Qdrant standalone: overkill para 8K scenes; pgvector cubre.
  • Cloud APIs (GPT-4V/Claude/Gemini): privacidad NO negociable, descartado de entrada.
  • Auto-caption sin keyframes (cada N segundos): 10× más cómputo, ganancia marginal.

Consecuencias

  • Sobremesa pasa a ser un LXC más (conceptualmente): SSH key-based, systemd, integrado en Loki/Grafana, sin sesiones interactivas requeridas. Adiós Windows como vector de incidencias.
  • GPU compartida entre ai-server (NSFW classifier), caption-worker (VLM), e5-large (embeddings), ocasional Stable Diffusion CLI. Gestión via Ollama y systemd ordering.
  • Stash gana búsqueda semántica + similarity sin tocar upstream.
  • pgvector en LXC 251 crece con schema nuevo; ya está en backups PBS.
  • Riesgo residual: si el wipe descubre incompatibilidad con modelos Patreon .pt2.enc no detectada en test live USB, hay que reinstalar Windows. Plan B = opción E (dual boot) en ese caso.
  • Energía: sobremesa H24 (~150W idle estimado) en vez de modo on-demand actual. Considerar suspend-to-RAM agresivo en Linux + WoL fiable (en Linux funciona bien, S3 real, no Modern Standby).

Pendientes inmediatos (si se aprueba)

  • Confirmar con usuario: ¿gaming Windows necesario? Si sí, escalar a opción E (dual boot) o aceptar pérdida.
  • Boot live USB Ubuntu en sobremesa + test carga modelo Patreon fearless_terrain.pt.enc con PyTorch+CUDA Linux. GO/NO-GO del wipe depende de este test.
  • Backup completo /opt/ai-server/ + /root/miniconda3/envs/ai_model_server/ + claves OAuth Patreon (licenseV1.0.lic, licenseV1.1.lic) a /mnt/nfs_media/backups/sobremesa-prewipe/.
  • Inventario qué hay en Windows que se pierde (Stable Diffusion checkpoints, Steam games, datos personales).
  • Diseñar schema pgvector stash_semantic (DDL).
  • Probar LLaVA-NeXT 7B con 10 frames reales de la librería antes de comprometerse al pipeline (calidad de captions = make-or-break).
  • Definir prompt template tras tuning manual con ~50 escenas.

Lecciones (anticipadas)

  • Reusar hardware existente pagando un coste one-shot (wipe) suele batir comprar nuevo, pero requiere honestidad sobre el dolor operativo que ya existe.
  • VRAM 10 GB es la línea roja real para VLMs 13B; planificar 7B y dejar 13B como upgrade futuro con 16GB+.
  • Stash extensibilidad via plugin + custom fields + GraphQL es suficiente para features avanzadas sin fork.