Proyecto: Transcripción de Audio
En este proyecto práctico, construimos un módulo funcional que demuestra cómo un agente puede "escuchar" archivos de audio usando Whisper a través de Bumblebee. El código se encuentra en el directorio apps/audio_sensor.
Estructura del Proyecto
apps/audio_sensor/
├── lib/
│ └── audio_sensor.ex # Módulo principal
├── mix.exs # Configuración de dependencias
├── test/
│ └── audio_sensor_test.exs
└── README.md
Configuración (mix.exs)
defmodule AudioSensor.MixProject do
use Mix.Project
def project do
[
app: :audio_sensor,
version: "0.1.0",
elixir: "~> 1.18",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[extra_applications: [:logger]]
end
defp deps do
[
{:bumblebee, "~> 0.6.0"},
{:nx, "~> 0.9.0"},
{:exla, "~> 0.9.0"},
{:castore, ">= 0.0.0"}
]
end
end
Código Fuente (lib/audio_sensor.ex)
defmodule AudioSensor do
@moduledoc """
Sensor auditivo basado en Whisper (OpenAI) via Bumblebee.
Demuestra cómo un agente puede percibir información del
entorno a través del audio.
"""
@modelo_default "openai/whisper-tiny"
@doc """
Inicializa el sensor cargando el modelo Whisper.
Retorna un serving reutilizable.
## Opciones
- `:modelo` — ID del modelo en Hugging Face (default: whisper-tiny)
"""
def init(opts \\ []) do
modelo = Keyword.get(opts, :modelo, @modelo_default)
IO.puts("⏳ Descargando modelo #{modelo}...")
{:ok, whisper} = Bumblebee.load_model({:hf, modelo})
{:ok, featurizer} = Bumblebee.load_featurizer({:hf, modelo})
{:ok, tokenizer} = Bumblebee.load_tokenizer({:hf, modelo})
{:ok, generation_config} = Bumblebee.load_generation_config({:hf, modelo})
serving = Bumblebee.Audio.speech_to_text_whisper(
whisper,
featurizer,
tokenizer,
generation_config,
compile: [batch_size: 1],
defn_options: [compiler: EXLA]
)
IO.puts("✅ Sensor auditivo listo.")
serving
end
@doc """
Transcribe un archivo de audio a texto.
## Ejemplos
iex> serving = AudioSensor.init()
iex> AudioSensor.transcribir(serving, "grabacion.mp3")
"Hello, this is a test recording."
"""
def transcribir(serving, ruta_archivo) do
unless File.exists?(ruta_archivo) do
raise "Archivo no encontrado: #{ruta_archivo}"
end
resultado = Nx.Serving.run(serving, {:file, ruta_archivo})
resultado.results
|> Enum.map(& &1.text)
|> Enum.join(" ")
|> String.trim()
end
@doc """
Transcribe un lote de archivos en paralelo.
"""
def transcribir_lote(serving, rutas) do
rutas
|> Enum.filter(&File.exists?/1)
|> Task.async_stream(
fn ruta -> {ruta, transcribir(serving, ruta)} end,
max_concurrency: System.schedulers_online(),
timeout: 120_000
)
|> Enum.map(fn {:ok, resultado} -> resultado end)
end
end
Ejecución
cd apps/audio_sensor
mix deps.get
iex -S mix
Dentro de la consola interactiva:
# 1. Inicializar el sensor (descarga el modelo la primera vez)
serving = AudioSensor.init()
# 2. Transcribir un archivo
texto = AudioSensor.transcribir(serving, "ejemplo.mp3")
IO.puts("Texto: #{texto}")
# 3. Procesar múltiples archivos
resultados = AudioSensor.transcribir_lote(serving, [
"audio1.wav",
"audio2.mp3"
])
Enum.each(resultados, fn {archivo, texto} ->
IO.puts("📄 #{archivo}: #{texto}")
end)
|