Construye con confianza un flujo de transcripción en Node.js: leer un .mp3 con el módulo fs, enviar el audio como Blob vía FormData a la API de OpenAI Whisper y guardar un TXT con la transcripción. Con un enfoque asíncrono, manejo de errores y rutas multiplataforma con path, obtendrás un script sólido y reutilizable.
¿Cómo preparar fs y path en Node.js para Whisper?
Para empezar, crea fs-openai.js y prepara los módulos nativos. Necesitarás tu API key de OpenAI para autenticar la solicitud y un archivo de audio .mp3 (por ejemplo, audio.mp3) para transcribir.
Usa fs para verificar existencia, leer y escribir archivos.
Usa path para rutas consistentes en macOS, Windows y GNU/Linux.
Trabaja con FormData para empaquetar el archivo y el modelo.
Convierte el buffer a Blob antes de adjuntarlo.
Envía la solicitud con fetch y método POST.
Incluye headers con Authorization: Bearer y tu API key.
Procesa la respuesta con response.json() y extrae data.text.
Guarda la transcripción con fs.writeFileSync en un TXT.
¿Qué requisitos y archivos necesitas para la API key y el audio .mp3?
Cuenta en OpenAI y tu API key activa.
Un archivo .mp3 accesible por ruta, por ejemplo: audio.mp3.
Un entorno con soporte para fetch, FormData y Blob.
¿Cuál es el flujo asíncrono de lectura, solicitud y guardado?
El núcleo es una función asíncrona con try/catch que valida el archivo, construye el FormData, llama a la API y persiste el resultado.
Verifica response.ok y, si falla, lanza error con detalles.
Parsea la respuesta a JSON y toma data.text.
Genera el nombre de salida con path.join y guarda el TXT.
Muestra en consola la ruta del archivo guardado.
// fs-openai.jsconst fs =require('fs');const path =require('path');asyncfunctiontranscriptAudio(audioFilePath, apiKey){try{if(!fs.existsSync(audioFilePath)){thrownewError('El archivo de audio no existe.');}const audioFile = fs.readFileSync(audioFilePath);const formData =newFormData();const blob =newBlob([audioFile]); formData.append('file', blob, path.basename(audioFilePath)); formData.append('model','whisper-1');const response =awaitfetch('https://api.openai.com/v1/audio/transcriptions',{method:'POST',headers:{Authorization:`Bearer ${apiKey}`},body: formData,});if(!response.ok){const errorData =await response.json();thrownewError(`Error en la API: ${JSON.stringify(errorData)}`);}const data =await response.json();const transcription = data.text;const base = path.basename(audioFilePath, path.extname(audioFilePath));const outputFilePath = path.join(path.dirname(audioFilePath),`${base}.transcription.txt`); fs.writeFileSync(outputFilePath, transcription);console.log(`Transcripción guardada en: ${outputFilePath}`);return transcription;}catch(error){console.error('Error durante la transcripción:', error.message);throw error;}}
¿Cómo construir el output file path con path.join?
Toma el directorio con path.dirname(audioFilePath).
Extrae el nombre base con path.basename sin la extensión.
Obtén la extensión con path.extname para removerla del nombre.
Une directorio y nombre base con sufijo .transcription.txt usando path.join.
¿Cómo validar la respuesta y manejar errores de la API?
Controla las respuestas no exitosas y comunica el problema claramente. Esto evita falsos positivos y facilita depurar cuando el modelo o la clave no son válidos.
Verifica response.ok antes de leer el cuerpo.
Si falla, obtén detalles con response.json() en errorData.
Lanza un Error con JSON.stringify(errorData) para ver el motivo.
En catch, registra con console.error y el error.message.
¿Quieres que revisemos tu implementación o nombres de archivo y rutas? Cuéntame en un comentario qué parte te gustaría profundizar.
Desafortunadamente para usar el API de OpenAI para transcribir de audio a texto, deben tener una cuenta registrada con un plan activo, ya que no es gratuito, si lo intenta les saldrá el error:
429 - You exceeded your current quota, please check your plan and billing details
** ALTERNATIVA **
Pueden usar la api de deepgram si realmente quieren probar, tiene una capa gratuita, , solo necesitan generar una API_KEY y definirla como variable:
constDEEPGRAM_API_KEY='API KEY GENERADA AQUI';```Para correr el código y funcione deben instalar el sdk de deepgram via npm:
```js
npm install @deepgram/sdk
transcript.txt
Response
Psdt: Se debería mejorar esta clase, ya que se sigue todo el código para nada. :)
Gracias por tu aporte. Personalmente, usé todos mis creditos gratuitos y esta alternativa me sirvió bastante
te rifaste! gracias
Creo que para que todos entiendan se debería explicar por ejemplo, que es un blob, que es el binario de un archivo como imágenes, videos, audios, etc.
No entiendo nada ! pero esta clase estas sumamente interesante!
como sugerencia, deberían ocultar la API Key en el código del repositorio público por motivos de seguridad
Hola, tomando como referencia la documentación de Deepgram, adapté un código muy similar al desarrollado en esta clase. De este modo, es posible seguir el contenido de la clase realizando únicamente pequeños ajustes en la URL y en la estructura de la respuesta que devuelve Deepgram con la ventaja de ser gratuito ;)
const fs =require('fs');const path =require('path');const apiUrl ="https://api.deepgram.com/v1/listen?model=nova-3&detect_language=true";asyncfunctiontranscribeAudio(audioFilePath, apiKey){try{if(!fs.existsSync(audioFilePath)){thrownewError('El archivo de audio no existe');}const audioFile = fs.readFileSync(audioFilePath);const response =awaitfetch(apiUrl,{method:"POST",headers:{Accept:"application/json",Authorization:`Token ${apiKey}`,"Content-Type":"audio/mp3",},body: audioFile
})if(!response.ok){const errorData =await response.json();thrownewError(`Error en la API: ${JSON.stringify(errorData)}`);}const data =await response.json();const transcription = data.results.channels[0].alternatives[0].transcript;const outputFilePath = path.join(path.dirname(audioFilePath),`${path.basename(audioFilePath, path.extname(audioFilePath))}_transcription.txt`); fs.writeFileSync(outputFilePath, transcription);return transcription
}catch(error){console.error(`Error en la transcripción: ${error.message}`);throw error;}}```PD:Corregí un pequeño error en la línea 30, era audioFilePath en vez de audioFile.(Este error se aborda en la siguiente clase)
Muchas gracias. Sos un crack. Me gusto esta version super similar a la que dio el profesor, ya que los cambios fueron minimos.
funciono excelente "bajo el brillo tenue del amanecer sueñan las hojas con el canto del viento mientras un rayo tímido de sol acaricia el silencio que nace del cielo en su dorada caricia florece la esperanza y el día se viste de luz nueva"
Estoy tomando este curso para repasar bases de node.
Super interesante esta clase para quienes ya tenemos acercamientos al backend, pero a un principiante al llegar aqui se va a desmotivar y será un caso mas de "la programacion es dificil y no es para mi".
Cuando intente usar la api de openai, estaba caido el sitio, entonces utilice otra AI, y funciona bastante bien, adjunto codigo
No, no es la de deepseek, es otra con un nombre similar
Como muchos han comentado, para usar esta API se necesita PAGAR suscripcion en OpenAI, y como varios han comentado, deepgram es una version gratuita por si quieren probar este ejercicio.
No cambia mucho de la estructura del ejercicio, los puntos donde cambia son los siguientes:
- La url de la api (obviamente):
- El body es directamente el Buffer, ya no es un FormData
- la autenticacion de la API es "Token", ya no es "Bearer"
- el modelo y el lenguaje no van como parte de los headers, se mandan como parametros en la url: ?model=nova-3&language=es-419 (modelo: nova-3, lenguaje: español latino) esto se ve en la documentacion.
- la respuesta ya no esta en .text, ahora se encuentra en: results.channels[0].alternatives[0].transcript
- Se agrega en los headers, en "Content-Type", el formato MIME del archivo de audio (esto lo resolvi creando una funcion que por medio del extname regresara el tipo de archivo MIME y en caso de no ser reconocido lanza un error).
Adjunto mi código espero les sirva.
Par aaquellos que se sintieron desconcertados con la clase por no entender ciertos puntos: ANIMO! este ejercicio no es tan de FUNDAMENTOS como dicen aqui, veanlo como un ejemplo funcional.
No se desanimen y sigan aprendiendo
import{access,constants, readFile, writeFile}from'fs/promises'import{basename, dirname, extname, join}from'path'constDEEPGRAM_API_KEY='';constfileExists=async(path)=>{try{awaitaccess(path, constants.F_OK);returntrue;}catch{returnfalse;}};constvalidateAudioType=(extensionName)=>{const extensionsMap ={['.mp3']:"audio/mpeg",['.wav']:"audio/wav",['.aac']:"audio/aac",['.mp4']:"audio/mp4",['.ogg']:"audio/ogg",}if(!extensionsMap[extensionName]){thrownewError('The extension of the file is invalid.')}return extensionsMap[extensionName]}asyncfunctiontranscriptAudio(audioFilePath, apiKey){try{if(!awaitfileExists(audioFilePath)){thrownewError('Audio file not found...')}console.log('File found')const audioType =extname(audioFilePath)const audioMimeType =validateAudioType(audioType)const audioFile =awaitreadFile(audioFilePath)const response =awaitfetch('https://api.deepgram.com/v1/listen?model=nova-3&language=es-419',{method:'POST',headers:{'Content-Type': audioMimeType,Authorization:`Token ${apiKey}`},body: audioFile
})if(!response.ok){const errorData =await response.json()thrownewError(`Error in the ytanscription API: ${JSON.stringify(errorData)}`)}const data =await response.json();const transcription = data.results.channels[0].alternatives[0].transcript;const outputFilePath =join(dirname(audioFilePath),`${basename(audioFilePath,audioType)}_transcription.txt`)awaitwriteFile(outputFilePath,transcription)console.log(`Transcription saved at: ${outputFilePath}`)return transcription
}catch(error){console.error(error.message)throw error;}}const result =awaittranscriptAudio('audio.mp3',DEEPGRAM_API_KEY)console.log(result);// true o false
Opinión personal del ejercicio:
Concuerdo con varios comentarios en 2 puntos:
Esta bien que quieran usar OpenAI, pero en todo caso, recomenrdaría hacer una version con una opción gratuita, como lo es deepgram, así mismo, creo que es interesante que se use la IA desde el principio en un ejercicio, porque hace el hook con el mundo real de lo que se usa hoy, pero como un principiante en el mundo del Back, creo que como parte de fundamentos es un fallo al menos a este punto del curso hacer este ejercicio. Puede ser un ejercicio al final, pero aqui muchos detalles del código pueden no entenderse para gente que enserio va comenzando, y eso puede ser contraproducente, ya que en vez de ver una funcionalidad del mundo real, ven código podríamos catalogar basico/medio que no entienden en "FUNDAMENTOS", lo cual no tiene a mi punto de vista, mucha lógica.
Pienso que toda la parte del path fue denso, me perdí desde el outputFilePath
Dejé el código de esta manera que para mí es más clara:
// save into a file// path.dirname(audioFilePath) ➡️ carpeta donde esta el file ej. "/users/xyz/audio"// path.extname(audioFilePath) ➡️ te da la extension ej. ".mp3"// path.basename ➡️ te da el archivo sin extenxion ej. "audio-test"// resultado del template string ➡️ "audio-test_transcription.txt"// path.join ➡️ une el directorio con el nuevo nombre de archivo ➡️ "/users/xyz/audio/audio-test_transcription.txt"// resumen guarda el nuevo archivo en la misma carpeta que el archivo baseconst dir = path.dirname(audioFilePath);const fileName = path.basename(audioFilePath, path.extname(audioFilePath));const outputFileName =`${ fileName }_transcription.txt`;const outputFilePath = path.join(dir, outputFileName);fs.writeFileSync(outputFilePath, transcriptionText);