Cayo Legal
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.
Construir un script en Node.js que transcribe audio con la API de Whisper de OpenAI es uno de los ejercicios más realistas para dominar el módulo File System. Aquí aprendes a leer un archivo .mp3, enviarlo como FormData a OpenAI y guardar la transcripción en un .txt, todo de forma compatible con cualquier sistema operativo.
¿Qué necesitas antes de transcribir audio con Whisper en Node.js?
Antes de tocar código, asegúrate de tener listos los recursos básicos para que el script funcione sin fricciones.
.mp3 (puedes usar uno propio o el del recurso de la clase).fs-openai.js.¿Para qué sirve la API key de OpenAI? Es la credencial que autentica tus solicitudes a Whisper. Sin ella, la API rechaza la petición y no obtienes transcripción.
¿Por qué usar el módulo Path junto a File System?
El módulo path es nativo de Node.js y resuelve un problema concreto: las rutas de archivos cambian entre Windows, Linux y macOS. Si escribes la ruta a mano, tu script se rompe al cambiar de máquina.
Al combinarlo con fs, logras dos cosas: leer y escribir archivos con seguridad, y hacerlo de forma transversal al sistema operativo. Por eso el script empieza importando ambos módulos con require('fs') y require('path') [01:55].
¿Cómo se construye la función transcribeAudio paso a paso?
La función principal es asíncrona, recibe la ruta del audio y la API key, y encapsula toda la lógica dentro de un bloque try/catch para capturar errores con claridad.
¿Cómo validar que el archivo de audio existe?
Antes de hacer cualquier solicitud, conviene confirmar que el archivo está disponible. Para eso usas fs.existsSync(audioFilePath), que devuelve true o false de forma síncrona [04:30].
Si el archivo no existe, lanzas un error con throw new Error('El archivo de audio no existe'). Esto evita gastar llamadas a la API en peticiones que ya sabes que van a fallar.
¿Cómo preparar el FormData con Blob para enviar el audio?
Whisper espera la información en formato multipart/form-data, así que necesitas tres pasos concretos:
fs.readFileSync(audioFilePath) y guardarlo en audioFile.new Blob([audioFile]) para que la API lo entienda como datos binarios.new FormData() y agregar dos campos con append: el file con el blob y la ruta, y el model con el valor whisper-1.El Blob es importante porque convierte el contenido crudo del audio en un objeto que el fetch puede serializar dentro del body de la solicitud.
¿Cómo hacer la solicitud fetch a la API de Whisper?
La petición usa fetch apuntando a https://api.openai.com/v1/audio/transcriptions con método POST [07:40]. En los headers envías Authorization: Bearer ${apiKey} y en el body pasas el formData ya construido.
¿Qué significa Bearer en el header Authorization? Es el esquema de autenticación que indica a la API que el valor que sigue es un token. OpenAI valida ese token contra tu cuenta antes de procesar el audio.
Después del fetch validas la respuesta con if (!response.ok). Si algo falló, extraes el detalle con await response.json() y lanzas un new Error que incluye JSON.stringify(errorData). Así sabes si el problema fue la API key, el modelo o el formato del archivo.
¿Cómo guardar la transcripción en un archivo TXT dinámico?
Una vez que recibes la respuesta, extraes el texto con const transcription = data.text. Pero el siguiente reto es guardarlo con un nombre que tenga sentido y funcione en cualquier sistema.
Para eso construyes la ruta de salida combinando varios métodos del módulo path:
path.dirname(audioFilePath) obtiene la carpeta donde vive el audio.path.basename(audioFilePath, path.extname(audioFilePath)) extrae el nombre sin la extensión .mp3.path.join(...) une todo y le concatena -transcription.txt.Esto significa que si el audio se llama poema.mp3, la transcripción se guarda como poema-transcription.txt en la misma carpeta. Si mañana cambias el nombre del audio, el archivo de salida se adapta solo.
Finalmente usas fs.writeFileSync(outputFilePath, transcription) para escribir el texto y un console.log para informar al usuario dónde quedó guardado.
¿Qué aprendiste sobre File System y Path en este flujo?
Este ejercicio mezcla varias habilidades clave del desarrollo con Node.js que vale la pena identificar por nombre.
.txt con la transcripción.multipart/form-data.También quedó claro el rol de whisper-1 como modelo de transcripción de OpenAI, y cómo el header Authorization: Bearer autentica cada petición con tu API key.
¿Ya tienes tu archivo .mp3 listo? Cuéntame en los comentarios qué audio vas a transcribir primero y qué nombre le pondrás a tu archivo de salida.
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:
const DEEPGRAM_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. :)
Cayo Legal
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.
Harvey Marín
Josue David
Muchas gracias. Sos un crack. Me gusto esta version super similar a la que dio el profesor, ya que los cambios fueron minimos.
Cristian Arellano
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"
Jose Ever Muñoz Muñoz
No entiendo nada ! pero esta clase estas sumamente interesante!
Dario Mendoza
Alfonso Neil Jiménez Casallas
como sugerencia, deberían ocultar la API Key en el código del repositorio público por motivos de seguridad
Lavinia Manzanarez
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 base const 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);
William Morales
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".
Irving Juárez
Alfonso Neil Jiménez Casallas
¿es la API de Deepseek?, ¡super!
Irving Juárez
No, no es la de deepseek, es otra con un nombre similar
Luis Gutiérrez
Pablo Zoe Mata Nuñez
gracias por tu comentario, me da ánimo de seguir, a veces nos podemos desanimar cuando no comprendemos nada y existe frustración cuando tienes mucho tiempo intentando realizar un código que esta en la mismo video pero no te queda..
Juan Alberto Maizares
mas allá de probar con openai, hay que 'maquetar' bien los conceptos de este video, las capacidades propias de node, su operatoria y forma de planear su accion...es un curso de Node, no de Open
Eduardo José Álvarez
Team Platzihola!
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"; async function transcribeAudio(audioFilePath, apiKey) { try { if (!fs.existsSync(audioFilePath)) { throw new Error('El archivo de audio no existe'); } const audioFile = fs.readFileSync(audioFilePath); const response = await fetch(apiUrl, { method: "POST", headers: { Accept: "application/json", Authorization: `Token ${apiKey}`, "Content-Type": "audio/mp3", }, body: audioFile }) if(!response.ok){ const errorData = await response.json(); throw new Error(`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)
Cuando intente usar la api de openai, estaba caido el sitio, entonces utilice otra AI, y funciona bastante bien, adjunto codigo
const fs = require('fs'); const path = require('path'); const { createClient } = require('@deepgram/sdk'); const SECRET_KEY = ""; async function speechToText(fileName) { const filePath = path.join(__dirname, fileName); const deepgram = createClient(SECRET_KEY); const { error, result } = await deepgram.listen.prerecorded.transcribeFile( fs.readFileSync(filePath), { model: 'nova-3', smart_format: true, } ); if (error) throw new Error(error); if (result) { console.log('Successfully transcribed the audio file:'); fs.writeFileSync(path.join(__dirname, 'output.txt'), JSON.stringify(result)); }; } speechToText("[yout_local_file]"); ```const fs = require('fs');const path = require('path');const { createClient } = require('@deepgram/sdk');const SECRET\_KEY = "a27a612658ed5c34c1dd0499d81a19337acda96a"; async function speechToText(fileName) { const filePath = path.join(\_\_dirname, fileName); const deepgram = createClient(SECRET\_KEY); const { error, result } = await deepgram.listen.prerecorded.transcribeFile( fs.readFileSync(filePath), { model: 'nova-3', smart\_format: true, } ); if (error) throw new Error(error); if (result) { console.log('Successfully transcribed the audio file:'); fs.writeFileSync(path.join(\_\_dirname, 'output.txt'), JSON.stringify(result)); };} speechToText("sound-file.wav");
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' const DEEPGRAM_API_KEY = ''; const fileExists = async (path) => { try { await access(path, constants.F_OK); return true; } catch { return false; } }; const validateAudioType =(extensionName)=>{ const extensionsMap = { ['.mp3']: "audio/mpeg", ['.wav']: "audio/wav", ['.aac']: "audio/aac", ['.mp4']: "audio/mp4", ['.ogg']: "audio/ogg", } if(!extensionsMap[extensionName]){ throw new Error ('The extension of the file is invalid.') } return extensionsMap[extensionName] } async function transcriptAudio(audioFilePath, apiKey){ try { if(!await fileExists(audioFilePath)){ throw new Error('Audio file not found...') } console.log('File found') const audioType = extname(audioFilePath) const audioMimeType = validateAudioType(audioType) const audioFile = await readFile(audioFilePath) const response = await fetch('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() throw new Error(`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` ) await writeFile(outputFilePath,transcription) console.log(`Transcription saved at: ${outputFilePath}`) return transcription } catch (error) { console.error(error.message) throw error; } } const result = await transcriptAudio('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.