fix: resolver FUNCTION_INVOCATION_FAILED y pipeline de Instagram
- transcriptor: restaurar await toFile() — sin él Whisper recibía una Promise en vez del archivo y devolvía 400 - transcriptor: detectar MIME type real (m4a para Instagram, mp3 TikTok) - analizar: normalizar duración (TikTok→ms, Instagram→s float) a entero antes de guardar en Supabase y pasar a GPT-4o - analizar/server: reemplazar .catch() en insert de error por try/catch — el builder de Supabase no expone .catch() directamente; el TypeError escapaba al outer catch y causaba FUNCTION_INVOCATION_FAILED en Vercel - validador: fallback de último recurso en enums cuando GPT-4o devuelve valor inválido (ej. "ninguno" para desarrollo_tipo) Probado end-to-end: Instagram Reel → OK en 27s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -53,13 +53,16 @@ export default async function handler(req, res) {
|
|||||||
paso = 'extraccion'
|
paso = 'extraccion'
|
||||||
const { audioUrl, duracion, titulo, thumbnail, plataforma } = await extraerAudio(url)
|
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 2: Transcribir con Whisper ───────────────────
|
||||||
paso = 'transcripcion'
|
paso = 'transcripcion'
|
||||||
const transcript = await transcribir(audioUrl, idioma)
|
const transcript = await transcribir(audioUrl, idioma)
|
||||||
|
|
||||||
// ── PASO 3: Analizar con GPT-4o ───────────────────────
|
// ── PASO 3: Analizar con GPT-4o ───────────────────────
|
||||||
paso = 'analisis'
|
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 4: Validar con Zod ───────────────────────────
|
||||||
paso = 'validacion'
|
paso = 'validacion'
|
||||||
@ -74,7 +77,7 @@ export default async function handler(req, res) {
|
|||||||
const payload = {
|
const payload = {
|
||||||
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
|
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
|
||||||
proyecto_nombre, competidor_referente,
|
proyecto_nombre, competidor_referente,
|
||||||
url_origen: url, plataforma, duracion_segundos: duracion,
|
url_origen: url, plataforma, duracion_segundos: duracionSeg,
|
||||||
vistas: vistas ? Number(vistas) : null,
|
vistas: vistas ? Number(vistas) : null,
|
||||||
likes: likes ? Number(likes) : null,
|
likes: likes ? Number(likes) : null,
|
||||||
compartidos: compartidos ? Number(compartidos) : null,
|
compartidos: compartidos ? Number(compartidos) : null,
|
||||||
@ -114,6 +117,7 @@ export default async function handler(req, res) {
|
|||||||
|
|
||||||
// Guardar el error en Supabase para diagnóstico
|
// Guardar el error en Supabase para diagnóstico
|
||||||
if (paso !== 'inicio') {
|
if (paso !== 'inicio') {
|
||||||
|
try {
|
||||||
await supabase.from('guiones').insert({
|
await supabase.from('guiones').insert({
|
||||||
url_origen: url,
|
url_origen: url,
|
||||||
niche,
|
niche,
|
||||||
@ -122,7 +126,8 @@ export default async function handler(req, res) {
|
|||||||
procesado_ok: false,
|
procesado_ok: false,
|
||||||
error_detalle: `[${paso}] ${err.message}`,
|
error_detalle: `[${paso}] ${err.message}`,
|
||||||
version_prompt: 'v1.0',
|
version_prompt: 'v1.0',
|
||||||
}).catch(() => {}) // silencioso si falla el insert de error
|
})
|
||||||
|
} catch (_) { /* silencioso si falla el insert de error */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(500).json({
|
return res.status(500).json({
|
||||||
|
|||||||
@ -42,7 +42,8 @@ export async function transcribir(audioUrl, idioma = 'es') {
|
|||||||
const ext = audioUrl.split('?')[0].split('.').pop()?.toLowerCase() || 'mp3'
|
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 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 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({
|
const transcripcion = await openai.audio.transcriptions.create({
|
||||||
file: audioFile,
|
file: audioFile,
|
||||||
|
|||||||
@ -40,7 +40,12 @@ function normalizarEnum(valor, validos, aliases = {}) {
|
|||||||
const matchU = validos.find(v => v.toLowerCase() === underscored)
|
const matchU = validos.find(v => v.toLowerCase() === underscored)
|
||||||
if (matchU) return matchU
|
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 */
|
/** Enum flexible: tolera acentos, mayúsculas, espacios y aliases explícitos */
|
||||||
|
|||||||
@ -132,13 +132,16 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
console.log(`[1/5] Extrayendo audio de: ${url}`)
|
console.log(`[1/5] Extrayendo audio de: ${url}`)
|
||||||
const { audioUrl, duracion, plataforma } = await extraerAudio(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'
|
paso = 'transcripcion'
|
||||||
console.log(`[2/5] Transcribiendo audio (${duracion}s)...`)
|
console.log(`[2/5] Transcribiendo audio (${duracionSeg}s)...`)
|
||||||
const transcript = await transcribir(audioUrl, idioma)
|
const transcript = await transcribir(audioUrl, idioma)
|
||||||
|
|
||||||
paso = 'analisis'
|
paso = 'analisis'
|
||||||
console.log(`[3/5] Analizando con GPT-4o...`)
|
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'
|
paso = 'validacion'
|
||||||
console.log(`[4/5] Validando schema...`)
|
console.log(`[4/5] Validando schema...`)
|
||||||
@ -154,7 +157,7 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
|
cliente_id, niche, sub_niche, mercado_objetivo, idioma,
|
||||||
proyecto_nombre, competidor_referente,
|
proyecto_nombre, competidor_referente,
|
||||||
url_origen: url, plataforma, duracion_segundos: duracion,
|
url_origen: url, plataforma, duracion_segundos: duracionSeg,
|
||||||
vistas: vistas ? Number(vistas) : null,
|
vistas: vistas ? Number(vistas) : null,
|
||||||
likes: likes ? Number(likes) : null,
|
likes: likes ? Number(likes) : null,
|
||||||
compartidos: compartidos ? Number(compartidos) : null,
|
compartidos: compartidos ? Number(compartidos) : null,
|
||||||
@ -200,12 +203,14 @@ app.post('/api/analizar', async (req, res) => {
|
|||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`✗ Error en paso "${paso}":`, err.message)
|
console.error(`✗ Error en paso "${paso}":`, err.message)
|
||||||
|
try {
|
||||||
await supabase.from('guiones').insert({
|
await supabase.from('guiones').insert({
|
||||||
url_origen: url, niche, idioma, cliente_id,
|
url_origen: url, niche, idioma, cliente_id,
|
||||||
procesado_ok: false,
|
procesado_ok: false,
|
||||||
error_detalle: `[${paso}] ${err.message}`,
|
error_detalle: `[${paso}] ${err.message}`,
|
||||||
version_prompt: 'v1.0',
|
version_prompt: 'v1.0',
|
||||||
}).catch(() => {})
|
})
|
||||||
|
} catch (_) { /* silencioso si falla el insert de error */ }
|
||||||
|
|
||||||
res.status(500).json({ ok: false, paso, error: err.message })
|
res.status(500).json({ ok: false, paso, error: err.message })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user