feat: usar métricas reales para ordenar referencias del generador

- Vistas y Likes son ahora obligatorios al analizar un video
- El generador ordena referencias por likes/vistas reales en lugar del score_virabilidad estimado por GPT-4o
- Agrega CLAUDE.md con guía de arquitectura y comandos

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-01 09:00:20 -05:00
parent a453b87c6c
commit 14372b5b29
5 changed files with 101 additions and 11 deletions

80
CLAUDE.md Normal file
View File

@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
AI-powered video script analysis and generation system. Analyzes TikTok/Reels/Shorts via a 5-step pipeline (extract audio → Whisper transcription → GPT-4o 49-field analysis → vector embeddings → Supabase storage) and generates new scripts using top-performing references.
## Development Commands
**Start both services (two terminals required):**
```bash
# Terminal 1 — Backend (port 3001)
cd backend && npm install && npm run dev
# Terminal 2 — Frontend (port 5173)
cd frontend && npm install && npm run dev
```
The frontend Vite dev server proxies `/api/*` to `http://localhost:3001`, so no CORS issues in development.
There are no test or lint scripts configured.
## Architecture
This is a **monorepo with three layers**:
### `/frontend` — Vue 3 + Vite + Tailwind
- Single-page app with 7 views (Dashboard, Analysis list/detail/create, Scripts, Generate, Login)
- Routes defined in [frontend/src/router/index.js](frontend/src/router/index.js)
- API calls centralized in [frontend/src/lib/api.js](frontend/src/lib/api.js)
- Auth is a mock Pinia store ([frontend/src/stores/auth.js](frontend/src/stores/auth.js)) with hardcoded credentials — not production-ready
- Design system: dark Obsidian theme. Colors defined in [frontend/tailwind.config.js](frontend/tailwind.config.js) as semantic tokens (`canvas`, `surface`, `ink`, `accent`, etc.). Fonts: Bricolage Grotesque (headlines) + Outfit (body)
### `/backend` — Express.js (local) + `/api` (Vercel serverless)
Two parallel sets of endpoint files exist:
- `/backend/api/` — used by Express server locally
- `/api/` (root) — Vercel serverless functions for production
When modifying API logic, **both files must be kept in sync** (or changes made to the root `/api/` file if targeting production).
Core pipeline modules in `/backend/lib/`:
| Module | Role |
|--------|------|
| `extractor.js` | RapidAPI Social Download → audio URL (TikTok/Reels/Shorts) |
| `transcriptor.js` | Whisper-1 → text transcript |
| `analizador.js` | GPT-4o → 49-field JSON analysis (storytelling, Cialdini, neuromarketing) |
| `validador.js` | Zod schema validation of GPT-4o output |
| `embeddings.js` | OpenAI embeddings → pgvector |
| `generador.js` | GPT-4o script generation from top-scoring references |
| `supabase.js` | Supabase client (SERVICE_ROLE_KEY — bypasses RLS) |
### `/database` — Supabase PostgreSQL + pgvector
Migrations must be applied in order in the Supabase SQL console:
`01_schema → 02_funciones → 03_rls → 04_datos_prueba → 05_analisis_extendido → 06_guiones_generados → 07_diagnostico_contexto`
Two primary tables:
- **`guiones`** — analyzed scripts, ~49 fields including enums, Cialdini booleans, psychographic scores (1-100), and a `embedding_vector` pgvector column
- **`guiones_generados`** — AI-generated scripts linked to `guiones` references via `referencias_ids UUID[]`
## Environment Variables
Create `/backend/.env`:
```
OPENAI_API_KEY=...
RAPIDAPI_KEY=... # Social Download All In One API
SUPABASE_URL=...
SUPABASE_SERVICE_ROLE_KEY=...
PORT=3001
```
For Vercel production, these same variables must be set in the Vercel project dashboard (the `/api/*.js` functions read from `process.env`).
## Key Constraints
- **Dual file sync**: The `/api/*.js` (Vercel) and `/backend/api/*.js` (Express) files implement the same logic — they diverged in past fixes. Always check both when debugging endpoint behavior.
- **No auth on backend endpoints**: API routes have no authentication middleware. Security relies on Supabase RLS + CORS. The service role key bypasses RLS, so backend lib files must never be exposed client-side.
- **Vercel function timeout**: Set to 60s in `vercel.json`. The full analysis pipeline (extract + transcribe + GPT-4o) can take 30-50s on long videos.
- **Node 24.x** required for backend (`--watch` flag in dev script).

View File

@ -38,6 +38,8 @@ export default async function handler(req, res) {
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
if (!niche) return res.status(400).json({ error: 'El campo "niche" es requerido' })
if (!vistas || Number(vistas) <= 0) return res.status(400).json({ error: 'El campo "vistas" es requerido y debe ser mayor a 0' })
if (!likes || Number(likes) <= 0) return res.status(400).json({ error: 'El campo "likes" es requerido y debe ser mayor a 0' })
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(url)) {

View File

@ -48,7 +48,8 @@ export default async function handler(req, res) {
`)
.eq('procesado_ok', true)
.eq('niche', niche)
.order('score_virabilidad', { ascending: false })
.order('likes', { ascending: false })
.order('vistas', { ascending: false })
.limit(num_referencias)
if (plataforma) query = query.eq('plataforma', plataforma)

View File

@ -116,6 +116,8 @@ app.post('/api/analizar', async (req, res) => {
if (!url) return res.status(400).json({ error: 'El campo "url" es requerido' })
if (!niche) return res.status(400).json({ error: 'El campo "niche" es requerido' })
if (!vistas || Number(vistas) <= 0) return res.status(400).json({ error: 'El campo "vistas" es requerido y debe ser mayor a 0' })
if (!likes || Number(likes) <= 0) return res.status(400).json({ error: 'El campo "likes" es requerido y debe ser mayor a 0' })
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(url)) {
@ -260,7 +262,8 @@ app.post('/api/generar', async (req, res) => {
`)
.eq('procesado_ok', true)
.eq('niche', niche)
.order('score_virabilidad', { ascending: false })
.order('likes', { ascending: false })
.order('vistas', { ascending: false })
.limit(num_referencias)
if (plataforma) query = query.eq('plataforma', plataforma)

View File

@ -109,16 +109,16 @@
<div class="grid grid-cols-3 gap-4">
<div class="space-y-1.5">
<label class="text-xs font-semibold text-ink-2 uppercase tracking-wide">Vistas</label>
<input v-model.number="form.vistas" type="number" placeholder="0" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
<label class="text-xs font-semibold text-ink-2 uppercase tracking-wide">Vistas <span class="text-error normal-case font-normal">*</span></label>
<input v-model.number="form.vistas" type="number" min="1" placeholder="Ej. 250000" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-ink-2 uppercase tracking-wide">Likes</label>
<input v-model.number="form.likes" type="number" placeholder="0" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
<label class="text-xs font-semibold text-ink-2 uppercase tracking-wide">Likes <span class="text-error normal-case font-normal">*</span></label>
<input v-model.number="form.likes" type="number" min="1" placeholder="Ej. 18000" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
</div>
<div class="space-y-1.5">
<label class="text-xs font-semibold text-ink-2 uppercase tracking-wide">Compartidos</label>
<input v-model.number="form.compartidos" type="number" placeholder="0" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
<input v-model.number="form.compartidos" type="number" min="0" placeholder="Ej. 3200" class="w-full bg-canvas border border-border rounded-lg px-3 py-2.5 text-sm text-ink text-center focus:outline-none focus:ring-2 focus:ring-accent/30" :disabled="analizando"/>
</div>
</div>
@ -288,6 +288,10 @@ async function iniciarAnalisis() {
error.value = "URL y Nicho son obligatorios para iniciar el pipeline."
return
}
if (!form.value.vistas || form.value.vistas <= 0 || !form.value.likes || form.value.likes <= 0) {
error.value = "Vistas y Likes son obligatorios. Cópialos directamente del video antes de analizar."
return
}
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(form.value.url)) {