diff --git a/backend/api/analizar.js b/backend/api/analizar.js index 7265dd4..5a42d9c 100644 --- a/backend/api/analizar.js +++ b/backend/api/analizar.js @@ -53,13 +53,16 @@ export default async function handler(req, res) { paso = 'extraccion' const { audioUrl, duracion, titulo, thumbnail, plataforma } = await extraerAudio(url) + // Normalizar duración: TikTok devuelve ms (ej. 47416), Instagram devuelve s float (ej. 49.062) + const duracionSeg = duracion ? Math.round(duracion > 1000 ? duracion / 1000 : duracion) : null + // ── 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) + const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracionSeg, contexto_video) // ── PASO 4: Validar con Zod ─────────────────────────── paso = 'validacion' @@ -74,7 +77,7 @@ export default async function handler(req, res) { const payload = { cliente_id, niche, sub_niche, mercado_objetivo, idioma, proyecto_nombre, competidor_referente, - url_origen: url, plataforma, duracion_segundos: duracion, + url_origen: url, plataforma, duracion_segundos: duracionSeg, vistas: vistas ? Number(vistas) : null, likes: likes ? Number(likes) : null, compartidos: compartidos ? Number(compartidos) : null, @@ -114,15 +117,17 @@ export default async function handler(req, res) { // 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 + try { + 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({ diff --git a/backend/lib/transcriptor.js b/backend/lib/transcriptor.js index a3dcdf0..4fc47fc 100644 --- a/backend/lib/transcriptor.js +++ b/backend/lib/transcriptor.js @@ -42,7 +42,8 @@ export async function transcribir(audioUrl, idioma = 'es') { const ext = audioUrl.split('?')[0].split('.').pop()?.toLowerCase() || 'mp3' const mimeMap = { mp3: 'audio/mpeg', m4a: 'audio/mp4', mp4: 'audio/mp4', webm: 'audio/webm', ogg: 'audio/ogg', wav: 'audio/wav' } const mimeType = mimeMap[ext] || 'audio/mpeg' - const audioFile = toFile(audioResponse, `audio.${ext}`, { type: mimeType }) + // toFile es async en el SDK de OpenAI — await es necesario aunque el IDE lo marque como hint + const audioFile = await toFile(audioResponse, `audio.${ext}`, { type: mimeType }) const transcripcion = await openai.audio.transcriptions.create({ file: audioFile, diff --git a/backend/lib/validador.js b/backend/lib/validador.js index 5aef69e..96f2b5e 100644 --- a/backend/lib/validador.js +++ b/backend/lib/validador.js @@ -40,7 +40,12 @@ function normalizarEnum(valor, validos, aliases = {}) { const matchU = validos.find(v => v.toLowerCase() === underscored) if (matchU) return matchU - return sinAcentos // sin match → Zod reportará el error + // 6. Fallback de último recurso: "otra" > "ninguna" > "ninguno" > último valor del enum + // GPT-4o a veces devuelve "ninguno" para campos donde no corresponde + if (validos.includes('otra')) return 'otra' + if (validos.includes('ninguna')) return 'ninguna' + if (validos.includes('ninguno')) return 'ninguno' + return validos[validos.length - 1] } /** Enum flexible: tolera acentos, mayúsculas, espacios y aliases explícitos */ diff --git a/backend/server.js b/backend/server.js index c730a84..9df97a0 100644 --- a/backend/server.js +++ b/backend/server.js @@ -132,13 +132,16 @@ app.post('/api/analizar', async (req, res) => { console.log(`[1/5] Extrayendo audio de: ${url}`) const { audioUrl, duracion, plataforma } = await extraerAudio(url) + // Normalizar duración: TikTok devuelve ms (ej. 47416), Instagram devuelve s float (ej. 49.062) + const duracionSeg = duracion ? Math.round(duracion > 1000 ? duracion / 1000 : duracion) : null + paso = 'transcripcion' - console.log(`[2/5] Transcribiendo audio (${duracion}s)...`) + console.log(`[2/5] Transcribiendo audio (${duracionSeg}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, contexto_video) + const analisisRaw = await analizarTranscript(transcript, niche, plataforma, duracionSeg, contexto_video) paso = 'validacion' console.log(`[4/5] Validando schema...`) @@ -154,7 +157,7 @@ app.post('/api/analizar', async (req, res) => { const payload = { cliente_id, niche, sub_niche, mercado_objetivo, idioma, proyecto_nombre, competidor_referente, - url_origen: url, plataforma, duracion_segundos: duracion, + url_origen: url, plataforma, duracion_segundos: duracionSeg, vistas: vistas ? Number(vistas) : null, likes: likes ? Number(likes) : null, compartidos: compartidos ? Number(compartidos) : null, @@ -200,12 +203,14 @@ app.post('/api/analizar', async (req, res) => { } 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(() => {}) + try { + 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 */ } res.status(500).json({ ok: false, paso, error: err.message }) }