Contenido detallado
🔊 Flujo de voz: escuchar, entender, hablar
Un asistente de voz no es un solo modelo: es una línea de ensamblaje de cuatro estaciones. El micrófono capta el sonido; el reconocimiento de voz convierte el sonido en texto; el cerebro del módulo 5.1 elige y valida la acción; la síntesis de voz convierte la respuesta en sonido. Cada estación es un programa independiente que puedes probar y cambiar sin modificar las demás.
🌱 ¿Eres nuevo aquí?
STT (speech-to-text) es el reconocimiento de voz: el sonido se convierte en texto. TTS (text-to-speech) es el proceso inverso. VAD (detección de actividad de voz) separa los segmentos con voz de los segmentos en silencio. La frecuencia de muestreo indica cuántas mediciones del sonido hace el micrófono por segundo; Whisper trabaja con 16 000 (16 kHz), en un solo canal (mono).
Cómo leer: la línea de arriba (flechas color teal) es el recorrido del pedido, desde tu voz hasta el motor. La línea de abajo (flechas cian discontinuas) es el retorno: el cerebro cuenta lo que hizo y Piper lo dice. La caja brillante es la misma del módulo 5.1; la voz solo cambia el teclado por un micrófono.
✓ Hazlo
- ✓Prueba cada estación por separado antes de unirlas.
- ✓Muestra en pantalla el texto que entendió Whisper.
- ✓Haz que el robot diga qué va a hacer antes de hacerlo.
✗ No lo hagas
- ✗Saltar el validador porque «solo fue una frase».
- ✗Usar la voz como única forma de detener el robot.
- ✗Unir todo de una vez y depurar a ciegas.
Por qué aprender
La voz es la interfaz más natural para quien no programa: un niño, un visitante, un estudiante en la feria de ciencias. Armar el pipeline por partes te enseña a desconfiar de cada estación y a encontrar dónde está la demora o el error.
Conceptos clave
👂 Whisper local (faster-whisper / whisper.cpp)
O Whisper es un modelo abierto de reconocimiento de voz que entiende portugués. Dos versiones funcionan bien sin usar la nube: faster-whisper, en Python, que usa la CPU con números de 8 bits (int8) o la GPU NVIDIA; y whisper.cpp, en C++, ligero y adecuado para la Pi. El curso usa faster-whisper porque se comunica directamente con el script del módulo 5.1. El modelo se descarga de internet la primera vez; después, funciona sin conexión.
| Modelo | Precisión en portugués | Dónde usarlo |
|---|---|---|
| tiny / base | confunde palabras parecidas | Pi 5, comandos cortos y pruebas |
| small | bueno para frases sencillas | Pi 5 con paciencia, cualquier PC |
| medium | mucho mejor con acento y ruido | PC, de preferencia con GPU |
| large-v3 / turbo | el más preciso | PC con GPU NVIDIA |
Objetivo: instalar faster-whisper y averiguar el número de tu micrófono. En la primera transcripción, el modelo elegido se descarga una vez.
source ~/robo-ia/bin/activate # el mismo entorno del módulo 5.1
sudo apt install -y libportaudio2 # Linux/Pi: sounddevice lo necesita
pip install faster-whisper sounddevice numpy
python3 -m sounddevice # lista los micrófonos: anota el número del tuyo
Objetivo: grabar algunos segundos desde el micrófono e imprimir lo que dijiste. El archivo también sirve como módulo: el asistente del tema 4 importa las funciones grabar e transcribir desde aquí.
#!/usr/bin/env python3
"""Graba desde el micrófono y transcribe en portugués con faster-whisper (sin conexión después de la primera descarga).
pip install faster-whisper sounddevice numpy"""
import time
import sounddevice as sd
from faster_whisper import WhisperModel
TAXA = 16000 # Whisper trabaja con 16 kHz, mono
MODELO_WHISPER = "small" # tiny, base, small, medium... cuanto mayor, más acierta y más tarda
MICROFONE = None # None = predeterminado del sistema; o el número de: python3 -m sounddevice
_modelo = None
def carregar():
global _modelo
if _modelo is None: # CPU int8 funciona en Pi 5 y en cualquier PC
_modelo = WhisperModel(MODELO_WHISPER, device="cpu", compute_type="int8")
# con GPU NVIDIA: WhisperModel(MODELO_WHISPER, device="cuda", compute_type="float16")
return _modelo
def gravar(segundos=4.0):
print(f"Grabando {segundos:.0f} s... habla ahora.")
audio = sd.rec(int(segundos * TAXA), samplerate=TAXA, channels=1, dtype="float32", device=MICROFONE)
sd.wait()
return audio.flatten()
def transcrever(audio):
segmentos, _info = carregar().transcribe(audio, language="pt", vad_filter=True, beam_size=1)
return " ".join(s.text.strip() for s in segmentos).strip()
if __name__ == "__main__":
carregar()
while True:
input("Presiona Enter para hablar (Ctrl+C para salir) ")
audio = gravar()
t0 = time.perf_counter()
texto = transcrever(audio)
print(f"Dijiste: '{texto}' (transcripción en {time.perf_counter() - t0:.1f} s)")
Cómo verificar
- ✓"anda treinta centímetros hacia adelante" aparece escrito casi igual, junto con el tiempo de transcripción.
- ✓Si guardas silencio, el texto queda vacío (el VAD recortó el silencio).
- ✗¿Error de PortAudio o el texto siempre queda vacío? Revisa el número del micrófono en MICRÓFONO y el volumen de captura del sistema.
Por qué aprender
El reconocimiento de voz es la puerta de entrada: si entiende "treinta" como "tres", todo lo demás también se equivoca. Medir el tiempo y probar modelos de distintos tamaños en tu hardware te permitirá decidir si tu robot responde en un segundo o en diez.
Conceptos clave
🔔 Palabra de activación y micrófono en el robot
El robot no debe transcribir todo lo que oye. Necesita un disparador para empezar a prestar atención. Lo más sencillo y confiable es presionar para hablar: una tecla en la PC o un botón en el GPIO de la Pi. La alternativa es una palabra de activación ("oye, robot"), detectada por un modelo diminuto que funciona todo el tiempo; proyectos abiertos como openWakeWord hacen esto, y puedes entrenar una palabra propia (consulta la documentación del proyecto elegido).
Cómo leer: el robot pasa la mayor parte del tiempo en el círculo de la izquierda, que pulsa. Solo el disparador lo lleva a "escuchando". Cualquier resultado no válido (texto vacío, acción rechazada, modelo sin respuesta) vuelve por la flecha cian a "esperando", sin activar el motor.
✓ Un micrófono que funcione
- ✓Un micrófono USB sencillo o de solapa, apuntado hacia quien habla.
- ✓Lejos de los motores y del ventilador de la Pi (ruido mecánico).
- ✓No grabes mientras el propio robot habla (se escucharía a sí mismo).
✗ Causas de error
- ✗El micrófono de la laptop a dos metros, en una sala con eco.
- ✗Escuchar siempre, sin disparador: comandos activados por conversaciones ajenas.
- ✗Volumen de captura al máximo: el sonido se distorsiona y Whisper se equivoca.
🌱 ¿Eres nuevo aquí?
En Raspberry Pi, un botón de "presionar para hablar" es un botón común entre un GPIO y GND. Con la biblioteca gpiozero (módulo 3.1), Button(17).wait_for_press() reemplaza input() del script: el robot espera que presiones el botón en vez de esperar a que presiones Enter.
Por qué aprender
Un robot que obedece cualquier frase que escucha en la sala es impredecible. El disparador y una buena ubicación del micrófono resuelven la mayoría de los problemas de reconocimiento antes de que necesites un modelo más grande.
Conceptos clave
🔗 Del texto al comando (con el 5.1)
El texto que sale de Whisper entra exactamente donde, en el módulo 5.1, entraba lo que escribías. Nada cambia en el validador: la voz es solo otra vía de entrada. La novedad es la confirmación hablada: para movimientos grandes, el robot pregunta "¿confirmas?" y solo sigue si oye "sí".
Paso a paso
Junta los archivos en una carpeta
cerebro_llm.py (módulo 5.1), ouvir.py (tema 2) y falar.py (tema 5). El asistente importa funciones de los tres.
Prueba en modo simulado
Ejecuta con --simular: el robot habla, pero no se envía ninguna línea por el puerto serial. Revisa el texto que oyó y la acción elegida para cada solicitud.
Conecta el ESP32 con el robot suspendido
Graba en el ESP32 el ejecutor del módulo 5.1, deja las ruedas en el aire o el brazo con espacio libre y solo entonces ejecútalo sin --simular.
Objetivo: el asistente completo: Enter para hablar, transcripción, elección de la acción por el modelo local, validación, confirmación por voz cuando el movimiento es grande, envío al ESP32 y respuesta hablada, con el tiempo de cada etapa en la terminal.
#!/usr/bin/env python3
"""Asistente de voz sin conexión: escuchar (Whisper) -> elegir (modelo local) -> validar -> ESP32 -> hablar (Piper).
En la misma carpeta: cerebro_llm.py (módulo 5.1), ouvir.py y falar.py (este módulo).
Uso: python3 assistente_voz.py [--simular]"""
import sys, time
from ouvir import carregar, gravar, transcrever
from falar import falar
from cerebro_llm import perguntar_modelo, validar, abrir_serial, enviar
FRASES = {"ANDAR": "avanzando {} centímetros", "GIRAR": "girando {} grados",
"GARRA": "garra: {}", "PARAR": "parando"}
def em_palavras(linha): # "ANDAR 30" -> "avanzando 30 centímetros"
partes = linha.split()
molde = FRASES.get(partes[0], linha)
return molde.format(*[p.lower() for p in partes[1:]]) if "{}" in molde else molde
def main():
simular = "--simular" in sys.argv
carregar()
s = abrir_serial(simular)
falar("Listo. Presiona Enter y habla.")
while True:
try:
input("\n[Enter] para hablar, Ctrl+C para salir ")
except (KeyboardInterrupt, EOFError):
break
audio = gravar(4)
t0 = time.perf_counter(); texto = transcrever(audio); t1 = time.perf_counter()
print(f"Oí: '{texto}'")
if not texto:
falar("No entendí. ¿Puedes repetir?"); continue
try:
resp = perguntar_modelo(texto)
except Exception as e: # sin respuesta o JSON ilegible: no mueve nada
print("Falla en el modelo:", e); falar("Mi cerebro no respondió."); continue
t2 = time.perf_counter()
ok, linha, confirmar = validar(resp)
if not ok:
print("Recusado:", linha); falar("No puedo hacer eso."); continue
if confirmar:
falar(f"¿Confirmas {em_palavras(linha)}? Di sí o no.")
palavras = transcrever(gravar(2.5)).lower().replace(".", " ").replace(",", " ").split()
if "sim" not in palavras: # palabra completa: "así" no confirma
falar("Cancelado."); continue
print("ESP32:", enviar(s, linha))
falar(em_palavras(linha))
t3 = time.perf_counter()
print(f"Tiempos: escuchar {t1 - t0:.1f} s · pensar {t2 - t1:.1f} s · ejecutar y hablar {t3 - t2:.1f} s")
if s:
s.write(b"PARAR\n"); s.close()
if __name__ == "__main__":
main()
Cómo verificar
- ✓"gira noventa grados a la derecha" → "Oí: ...", "ESP32: (simulado) GIRAR 90" y la voz dice "girando 90 grados".
- ✓"avanza ochenta centímetros" → el robot pregunta "¿Confirmas...?"; responder "no" cancela.
- ✓"baila para mí" → "No puedo hacer eso." y no se envía nada.
Por qué aprender
Combinar piezas probadas es como se construyen los sistemas reales. Como cada archivo tiene una función clara, el proyecto final del módulo 5.6 reutiliza los tres sin reescribir nada.
Conceptos clave
🗣️ Piper: voz sin conexión en portugués
O Piper es un sintetizador de voz abierto, lo bastante rápido para Raspberry Pi. Cada voz es un archivo .onnx (el modelo) con un .onnx.json al lado (la configuración). Hay voces en portugués de Brasil, como pt_BR-faber-medium; la lista completa está en el repositorio de voces. El proyecto dejó su dirección original (rhasspy/piper) y continúa como piper1-gpl, de Open Home Foundation; el paquete piper-tts de pip acompaña este cambio, y la función de síntesis cambió de nombre entre versiones. El siguiente script funciona con ambas.
Cómo leer: de izquierda a derecha, la frase se divide en sonidos del idioma (fonemas), que el modelo de voz transforma en audio. Cambiar de voz es cambiar solo el archivo de la caja iluminada; el resto del proceso es igual.
Objetivo: instalar Piper y descargar una voz en portugués. Al final, los archivos pt_BR-faber-medium.onnx e .onnx.json deben estar en la carpeta.
pip install piper-tts
python3 -m piper.download_voices pt_BR-faber-medium # descarga .onnx y .onnx.json en la carpeta actual
# ¿versión antigua del paquete? descarga los dos archivos de la voz en huggingface.co/rhasspy/piper-voices
Objetivo: una función falar(texto) que sintetiza en la memoria y reproduce el audio en el altavoz predeterminado, sin guardar ningún archivo en el disco.
#!/usr/bin/env python3
"""Lee un texto en portugués con Piper, sin conexión. pip install piper-tts sounddevice numpy
Uso: python3 falar.py "Olá, eu sou o robô." """
import io, sys, wave
import numpy as np
import sounddevice as sd
from piper import PiperVoice
VOZ = "pt_BR-faber-medium.onnx" # el .onnx.json debe estar al lado
_voz = None
def falar(texto):
global _voz
if _voz is None:
_voz = PiperVoice.load(VOZ)
buf = io.BytesIO()
with wave.open(buf, "wb") as wav:
if hasattr(_voz, "synthesize_wav"): # versiones nuevas de piper-tts
_voz.synthesize_wav(texto, wav)
else: # versiones antiguas
_voz.synthesize(texto, wav)
buf.seek(0)
with wave.open(buf, "rb") as wav:
taxa = wav.getframerate()
dados = np.frombuffer(wav.readframes(wav.getnframes()), dtype=np.int16)
sd.play(dados, taxa)
sd.wait() # espera a que termine: el robot no se oye cuando habla
if __name__ == "__main__":
falar(" ".join(sys.argv[1:]) or "Hola, soy el robot del curso. Estoy listo.")
Cómo verificar: python3 falar.py "Girando noventa grados" reproduce la frase en portugués en menos de un segundo en una PC (un poco más en la Pi). Si aparece "No such file", el nombre en VOZ no coincide con el archivo descargado; si no se oye nada, revisa el dispositivo de salida con python3 -m sounddevice.
Por qué aprender
Un robot que dice lo que va a hacer es más seguro y más fácil de depurar: oyes "girando noventa grados" antes del movimiento y tienes tiempo de presionar el botón de parada si no era eso.
Conceptos clave
⏱️ Latencia y privacidad
Latencia es el tiempo entre que dejas de hablar y el robot reacciona. Es la suma de las etapas, y casi siempre una de ellas predomina. Por eso el asistente imprime los tiempos por separado: optimizas la etapa más lenta, no la que parece más lenta. La figura muestra la forma típica en una PC sin GPU; los números de tu hardware serán distintos, así que mídelo.
Cómo leer: cada barra es una etapa; su longitud representa el tiempo. La barra discontinua no depende de la computadora, sino de cuánto grabas. La barra que pulsa suele ser la del modelo de lenguaje: es donde un modelo más pequeño o una GPU hacen más diferencia.
✓ Reduce la espera
- ✓Carga los modelos una sola vez, al inicio (el script ya lo hace).
- ✓Graba solo mientras mantienes presionado el botón, en lugar de usar un tiempo fijo.
- ✓Un modelo de lenguaje más pequeño y un prompt breve; usa la GPU si tienes una.
- ✓Di un «ok» breve enseguida, mientras el modelo piensa.
✗ Aumenta la espera
- ✗Cargar Whisper en cada frase.
- ✗Dejar que Whisper adivine el idioma.
- ✗Pi 5 sin cooler: la CPU se calienta y reduce la velocidad.
- ✗Respuestas largas del modelo para comandos breves.
Privacidad es la otra mitad de la elección. En un servicio de voz en la nube, el audio de tu habitación viaja hasta un servidor de terceros. En un salón de clases, con voces de niños, esto requiere autorización y cuidado con la ley de protección de datos. En el pipeline local, el sonido se procesa y se descarta dentro del robot.
Cómo leer: a la izquierda, la flecha corta queda dentro del marco teal: el audio nunca sale del dispositivo. A la derecha, la flecha roja discontinua cruza internet. La nube puede ser más precisa o más rápida; la elección depende de quién habla cerca del robot y de lo que puedes enviar fuera.
Por qué aprender
Medir la latencia transforma «está lento» en «el modelo de lenguaje tarda tanto», algo que tiene solución. Y pensar en la privacidad desde el inicio evita rehacer el proyecto cuando llegue a una escuela, una feria o la casa de alguien.
Conceptos clave
✅ Resumen del módulo
Para profundizar
- faster-whisper — instalación, modelos y opciones de transcribe
- whisper.cpp — Whisper en C++ para el Pi
- Piper (repositorio original) — apunta al sucesor piper1-gpl
- Voces de Piper — busca pt_BR
- openWakeWord — palabra de activación abierta
- Ollama — el cerebro local del módulo 5.1
Siguiente módulo
5.3 · Brazo SO-101 y LeRobot: arma, calibra y teleopera el brazo que aprenderá contigo.