fix: corregir 4 puntos de falla del pipeline de análisis
- validador: normaliza enums de GPT-4o (acentos, mayúsculas, aliases como "ninguno"→"ninguna", "shock"→"declaracion_shock") y coerce strings numéricos a enteros antes de validar con Zod - transcriptor: reintentos automáticos (3 intentos, backoff 1.2s) para URLs de CDN inestables; mejor mensaje de error en transcripción vacía - analizador: captura JSON.parse inválido con mensaje diagnóstico - vercel.json: aumenta timeout de analizar a 300s y generar a 120s Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,26 +1,44 @@
|
||||
// ============================================================
|
||||
// TRANSCRIPTOR — OpenAI Whisper
|
||||
// Descarga el audio desde la URL y lo transcribe
|
||||
// Incluye reintentos automáticos para URLs de CDN inestables
|
||||
// ============================================================
|
||||
import OpenAI, { toFile } from 'openai'
|
||||
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
|
||||
|
||||
/**
|
||||
* Descarga una URL con reintentos (para CDNs que expiran o fallan transitoriamente)
|
||||
* @param {string} url
|
||||
* @param {number} intentos Máximo de intentos (default: 3)
|
||||
* @returns {Response}
|
||||
*/
|
||||
async function fetchConReintentos(url, intentos = 3) {
|
||||
let ultimoError
|
||||
for (let i = 1; i <= intentos; i++) {
|
||||
try {
|
||||
const res = await fetch(url)
|
||||
if (res.ok) return res
|
||||
ultimoError = new Error(`HTTP ${res.status} al descargar audio (intento ${i}/${intentos})`)
|
||||
} catch (err) {
|
||||
ultimoError = err
|
||||
}
|
||||
if (i < intentos) await new Promise(r => setTimeout(r, 1200 * i)) // backoff: 1.2s, 2.4s
|
||||
}
|
||||
throw ultimoError
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} audioUrl URL directa del MP3 (de Social Download API)
|
||||
* @param {string} idioma Código de idioma: 'es', 'en', 'pt', etc.
|
||||
* @returns {string} Transcripción completa del audio
|
||||
*/
|
||||
export async function transcribir(audioUrl, idioma = 'es') {
|
||||
// Descargar el audio desde la URL del CDN
|
||||
const audioResponse = await fetch(audioUrl)
|
||||
if (!audioResponse.ok) {
|
||||
throw new Error(`Error al descargar audio: ${audioResponse.status}`)
|
||||
}
|
||||
const audioResponse = await fetchConReintentos(audioUrl)
|
||||
|
||||
// En Vercel Serverless (Node < 20), Web API `File` no está disponible por defecto,
|
||||
// y `arrayBuffer` consume mucha RAM. `toFile` soluciona ambos.
|
||||
const audioFile = await toFile(audioResponse, 'audio.mp3', { type: 'audio/mpeg' })
|
||||
const audioFile = toFile(audioResponse, 'audio.mp3', { type: 'audio/mpeg' })
|
||||
|
||||
const transcripcion = await openai.audio.transcriptions.create({
|
||||
file: audioFile,
|
||||
@ -30,7 +48,10 @@ export async function transcribir(audioUrl, idioma = 'es') {
|
||||
})
|
||||
|
||||
if (!transcripcion || transcripcion.trim().length < 10) {
|
||||
throw new Error('Whisper no pudo transcribir el audio (resultado vacío o muy corto)')
|
||||
throw new Error(
|
||||
'Whisper no detectó voz suficiente en el audio. ' +
|
||||
'Verifica que el video tenga narración clara (no solo música o texto en pantalla).'
|
||||
)
|
||||
}
|
||||
|
||||
return transcripcion.trim()
|
||||
|
||||
Reference in New Issue
Block a user