// ============================================================ // ENDPOINT PRINCIPAL — POST /api/analizar // Orquesta el pipeline completo: // URL → Audio → Whisper → GPT-4o → Validar → Embedding → Supabase // ============================================================ 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' export default async function handler(req, res) { // Solo aceptar POST if (req.method !== 'POST') { return res.status(405).json({ error: 'Método no permitido' }) } const inicio = Date.now() // ── Validar body de entrada ─────────────────────────────── const { url, niche, sub_niche, mercado_objetivo, idioma = 'es', cliente_id = null, proyecto_nombre = null, competidor_referente = false, // Métricas manuales (la API no las devuelve) vistas = null, likes = null, compartidos = null, fecha_publicacion = null, contexto_video = '', } = 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' }) 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)) { return res.status(400).json({ error: 'URL no soportada. Solo se aceptan TikTok, Instagram Reels y YouTube Shorts.' }) } let paso = 'inicio' try { // ── PASO 1: Extraer audio ───────────────────────────── paso = 'extraccion' const { audioUrl, duracion, titulo, thumbnail, plataforma } = await extraerAudio(url) // ── PASO 2: Transcribir con Whisper ─────────────────── paso = 'transcripcion' const transcript = await transcribir(audioUrl, idioma) // ── PASO 3: Analizar con GPT-4o ─────────────────────── paso = 'analisis' const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracion, contexto_video) // ── PASO 4: Validar con Zod ─────────────────────────── paso = 'validacion' const analisis = validarAnalisis(analisisRaw) // ── PASO 5: Generar embedding vectorial ─────────────── paso = 'embedding' const vector = await generarEmbedding(transcript, analisis) // ── PASO 6: Guardar en Supabase ─────────────────────── paso = 'guardado' const payload = { cliente_id, niche, sub_niche, mercado_objetivo, idioma, proyecto_nombre, competidor_referente, url_origen: url, plataforma, duracion_segundos: duracion, vistas: vistas ? Number(vistas) : null, likes: likes ? Number(likes) : null, compartidos: compartidos ? Number(compartidos) : null, fecha_publicacion, contexto_video: contexto_video || null, ...analisis, transcript, embedding_vector: vector, // Array nativo para pgvector procesado_ok: true, version_prompt: 'v1.0', } const { data: guion, error: errorSupabase } = await supabase .from('guiones') .insert(payload) .select('id, niche, score_virabilidad, resumen_patron') .single() if (errorSupabase) { console.error('[Supabase Error]:', errorSupabase) throw new Error(`Supabase error: ${errorSupabase.message}`) } const duracionTotal = ((Date.now() - inicio) / 1000).toFixed(1) return res.status(200).json({ ok: true, guion_id: guion.id, niche: guion.niche, score_virabilidad: guion.score_virabilidad, resumen_patron: guion.resumen_patron, tiempo_total_seg: parseFloat(duracionTotal), }) } catch (err) { console.error(`[analizar] Error en paso "${paso}":`, err.message) // Guardar el error en Supabase para diagnóstico if (paso !== 'inicio') { 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(() => {}) // silencioso si falla el insert de error } return res.status(500).json({ ok: false, paso, error: err.message, }) } }