Files
Generadordeguiones/backend/server.js
Hanzo_dev 7695dd0be6 Initial commit — Sistema Generador de Guiones V4.0
Pipeline completo: URL → Whisper → GPT-4o → pgvector → Supabase
Frontend Vue 3 + Tailwind, Backend Express + Vercel serverless functions
2026-03-28 16:02:59 -05:00

191 lines
7.2 KiB
JavaScript

import 'dotenv/config'
import express from 'express'
import cors from 'cors'
import { extraerAudio } from './lib/extractor.js'
import { transcribir } from './lib/transcriptor.js'
import { analizarTranscript } from './lib/analizador.js'
import { validarAnalisis } from './lib/validador.js'
import { generarEmbedding } from './lib/embeddings.js'
import { supabase } from './lib/supabase.js'
// ── Validar variables de entorno requeridas ──────────────────
const REQUIRED_ENV = ['OPENAI_API_KEY', 'SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'RAPIDAPI_KEY']
const missingVars = REQUIRED_ENV.filter(k => !process.env[k])
if (missingVars.length > 0) {
console.error(`\n❌ Variables de entorno faltantes: ${missingVars.join(', ')}\n Configura el archivo backend/.env antes de iniciar.\n`)
process.exit(1)
}
const app = express()
const PORT = process.env.PORT || 3001
app.use(cors({ origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173' }))
app.use(express.json())
// ── GET /api/guiones ────────────────────────────────────────
// Lista todos los guiones con paginación y filtros
app.get('/api/guiones', async (req, res) => {
const { niche, cliente_id, plataforma, page = 1, limit = 20 } = req.query
const offset = (page - 1) * limit
let query = supabase
.from('guiones')
.select(`
id, niche, sub_niche, plataforma, url_origen,
gancho_texto, estructura_narrativa, trigger_emocional,
tono, score_engagement, score_virabilidad, score_cialdini,
fecha_analisis, procesado_ok, vistas, likes, compartidos,
tema_principal, resumen_patron
`, { count: 'exact' })
.eq('procesado_ok', true)
.order('fecha_analisis', { ascending: false })
.range(offset, offset + limit - 1)
if (niche) query = query.eq('niche', niche)
if (cliente_id) query = query.eq('cliente_id', cliente_id)
if (plataforma) query = query.eq('plataforma', plataforma)
const { data, error, count } = await query
if (error) return res.status(500).json({ error: error.message })
res.json({ guiones: data, total: count, page: Number(page), limit: Number(limit) })
})
// ── GET /api/guiones/:id ────────────────────────────────────
// Detalle completo de un guion
app.get('/api/guiones/:id', async (req, res) => {
const { data, error } = await supabase
.from('guiones')
.select('*')
.eq('id', req.params.id)
.single()
if (error) return res.status(404).json({ error: 'Guion no encontrado' })
res.json(data)
})
// ── GET /api/nichos ─────────────────────────────────────────
// Lista de nichos distintos para el selector del formulario
app.get('/api/nichos', async (req, res) => {
const { data, error } = await supabase
.from('guiones')
.select('niche')
.eq('procesado_ok', true)
if (error) return res.status(500).json({ error: error.message })
const nichos = [...new Set(data.map(r => r.niche))].sort()
res.json(nichos)
})
// ── GET /api/clientes ───────────────────────────────────────
app.get('/api/clientes', async (req, res) => {
const { data, error } = await supabase
.from('clientes')
.select('id, nombre, industria')
.eq('activo', true)
.order('nombre')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
})
// ── GET /api/stats ──────────────────────────────────────────
// Estadísticas para el dashboard
app.get('/api/stats', async (req, res) => {
const { data, error } = await supabase
.from('vista_resumen_nichos')
.select('*')
if (error) return res.status(500).json({ error: error.message })
res.json(data)
})
// ── POST /api/analizar ──────────────────────────────────────
// Pipeline completo: URL → Audio → Whisper → GPT-4o → Supabase
app.post('/api/analizar', async (req, res) => {
const {
url, niche, sub_niche, mercado_objetivo,
idioma = 'es', cliente_id = null, proyecto_nombre = null,
competidor_referente = false,
vistas = null, likes = null, compartidos = null,
fecha_publicacion = null,
} = req.body
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' })
const URL_SOPORTADAS = /^https?:\/\/(www\.)?(tiktok\.com|vm\.tiktok\.com|instagram\.com|youtube\.com|youtu\.be)/
if (!URL_SOPORTADAS.test(url)) {
return res.status(400).json({ error: 'URL no soportada. Solo se aceptan TikTok, Instagram Reels y YouTube Shorts.' })
}
const inicio = Date.now()
let paso = 'inicio'
try {
paso = 'extraccion'
console.log(`[1/5] Extrayendo audio de: ${url}`)
const { audioUrl, duracion, plataforma } = await extraerAudio(url)
paso = 'transcripcion'
console.log(`[2/5] Transcribiendo audio (${duracion}s)...`)
const transcript = await transcribir(audioUrl, idioma)
paso = 'analisis'
console.log(`[3/5] Analizando con GPT-4o...`)
const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion)
paso = 'validacion'
console.log(`[4/5] Validando schema...`)
const analisis = validarAnalisis(analisisRaw)
paso = 'embedding'
console.log(`[5/5] Generando embedding y guardando...`)
const vector = await generarEmbedding(transcript, analisis)
const { data: guion, error: errorSupabase } = await supabase
.from('guiones')
.insert({
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
proyecto_nombre, competidor_referente,
url_origen: url, plataforma, duracion_segundos: duracion,
vistas, likes, compartidos, fecha_publicacion,
...analisis,
transcript,
embedding_vector: `[${vector.join(',')}]`,
procesado_ok: true,
version_prompt: 'v1.0',
})
.select('id, niche, score_virabilidad, resumen_patron')
.single()
if (errorSupabase) throw new Error(`Supabase: ${errorSupabase.message}`)
const segundos = ((Date.now() - inicio) / 1000).toFixed(1)
console.log(`✓ Completado en ${segundos}s — ID: ${guion.id}`)
res.json({
ok: true,
guion_id: guion.id,
niche: guion.niche,
score_virabilidad: guion.score_virabilidad,
resumen_patron: guion.resumen_patron,
tiempo_total_seg: parseFloat(segundos),
})
} catch (err) {
console.error(`✗ Error en paso "${paso}":`, err.message)
await supabase.from('guiones').insert({
url_origen: url, niche, idioma, cliente_id,
procesado_ok: false,
error_detalle: `[${paso}] ${err.message}`,
version_prompt: 'v1.0',
}).catch(() => {})
res.status(500).json({ ok: false, paso, error: err.message })
}
})
app.listen(PORT, () => console.log(`Backend local corriendo en http://localhost:${PORT}`))