filtros-limpieza-total.txt
· 856 B · Text
Originalformat
# ==============================================================================
# Pipeline completo de limpieza para audios muy ruidosos
# REGLA DE ORO: limpieza ANTES de normalización
# Episodio 842 — atareao con Linux
# ==============================================================================
#
# 1. highpass=f=80 → Quita frecuencias muy graves (rumor, viento)
# 2. lowpass=f=8000 → Quita frecuencias muy agudas (siseo, ruido electrónico)
# 3. afftdn → Reduce ruido de fondo (FFT Denoising)
# 4. loudnorm → Normaliza el volumen final (EBU R128, -23 LUFS)
#
# Uso:
# ffmpeg -i grabacion_mala.mp3 \
# -af "$(cat filtros-limpieza-total.txt)" \
# salida_limpia_normalizada.wav
# ==============================================================================
highpass=f=80,
lowpass=f=8000,
afftdn,
loudnorm
| 1 | # ============================================================================== |
| 2 | # Pipeline completo de limpieza para audios muy ruidosos |
| 3 | # REGLA DE ORO: limpieza ANTES de normalización |
| 4 | # Episodio 842 — atareao con Linux |
| 5 | # ============================================================================== |
| 6 | # |
| 7 | # 1. highpass=f=80 → Quita frecuencias muy graves (rumor, viento) |
| 8 | # 2. lowpass=f=8000 → Quita frecuencias muy agudas (siseo, ruido electrónico) |
| 9 | # 3. afftdn → Reduce ruido de fondo (FFT Denoising) |
| 10 | # 4. loudnorm → Normaliza el volumen final (EBU R128, -23 LUFS) |
| 11 | # |
| 12 | # Uso: |
| 13 | # ffmpeg -i grabacion_mala.mp3 \ |
| 14 | # -af "$(cat filtros-limpieza-total.txt)" \ |
| 15 | # salida_limpia_normalizada.wav |
| 16 | # ============================================================================== |
| 17 | |
| 18 | highpass=f=80, |
| 19 | lowpass=f=8000, |
| 20 | afftdn, |
| 21 | loudnorm |
filtros-tts-profesional.txt
· 695 B · Text
Originalformat
# ==============================================================================
# Filtros ffmpeg para postprocesado profesional de TTS
# Episodio 842 — atareao con Linux
# ==============================================================================
#
# Aplica estos filtros a la salida de Piper, Kokoro o cualquier TTS
# para que suene más natural y profesional.
#
# Uso:
# ffmpeg -i tts_output.wav -af "$(cat filtros-tts-profesional.txt)" \
# salida_profesional.wav
# ==============================================================================
loudnorm,
compand=attacks=0.1:decays=0.2:\
points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15|\
-15/-12|-10/-6|-6/-3|0/-0.5,
highpass=f=80
| 1 | # ============================================================================== |
| 2 | # Filtros ffmpeg para postprocesado profesional de TTS |
| 3 | # Episodio 842 — atareao con Linux |
| 4 | # ============================================================================== |
| 5 | # |
| 6 | # Aplica estos filtros a la salida de Piper, Kokoro o cualquier TTS |
| 7 | # para que suene más natural y profesional. |
| 8 | # |
| 9 | # Uso: |
| 10 | # ffmpeg -i tts_output.wav -af "$(cat filtros-tts-profesional.txt)" \ |
| 11 | # salida_profesional.wav |
| 12 | # ============================================================================== |
| 13 | |
| 14 | loudnorm, |
| 15 | compand=attacks=0.1:decays=0.2:\ |
| 16 | points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15|\ |
| 17 | -15/-12|-10/-6|-6/-3|0/-0.5, |
| 18 | highpass=f=80 |
pipeline-completo.py
· 15 KiB · Python
Originalformat
#!/usr/bin/env python3
"""
Pipeline completo: micrófono → ffmpeg → Whisper → Ollama → TTS → ffmpeg → altavoz
================================================================================
Este script une todo lo visto en el arco de audio/vídeo de la Temporada 9:
842 — ffmpeg (preparación y postprocesado de audio)
843 — Whisper (transcripción local)
845 — TTS con Piper/Kokoro (voz sintética)
847 — visión artificial local (LLaVA, Florence-2, Moondream)
Requisitos:
pip install faster-whisper requests piper-tts
Además necesitas tener instalado:
- ffmpeg (sudo apt install ffmpeg)
- Ollama corriendo en localhost:11434
- Piper TTS con voz en español (es_ES-sharvard-medium.onnx)
atareao con Linux — Episodio 842
"""
import subprocess
import tempfile
import time
from pathlib import Path
from typing import Optional
import requests
# ── Configuración ────────────────────────────────────────────────────────
OLLAMA_URL = "http://localhost:11434/api/generate"
OLLAMA_MODEL = "llama3.2:latest"
# ── Colores para terminal ────────────────────────────────────────────────
class Color:
CYAN = "\033[0;36m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
RED = "\033[0;31m"
NC = "\033[0m"
def print_step(emoji: str, msg: str) -> None:
"""Imprime un paso del pipeline con formato."""
print(f"\n{emoji} {Color.CYAN}{msg}{Color.NC}")
print("─" * 50)
def print_ok(msg: str) -> None:
print(f" {Color.GREEN}✅{Color.NC} {msg}")
def print_warn(msg: str) -> None:
print(f" {Color.YELLOW}⚠️{Color.NC} {msg}")
# ── Paso 1: Grabar y preparar con ffmpeg ────────────────────────────────
def grabar_y_preparar(segundos: int = 5) -> Optional[str]:
"""
Graba audio del micrófono y lo prepara para Whisper con ffmpeg.
1. arecord captura el audio en bruto
2. ffmpeg lo convierte a: 16 kHz, mono, s16le + loudnorm
Returns:
Ruta al archivo WAV listo para Whisper, o None si falla.
"""
print_step("🎤", f"Grabando {segundos} segundos...")
# Archivo temporal para la grabación en bruto
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
raw_path = f.name
# Archivo para el audio preparado
whisper_path = raw_path.replace(".wav", "_whisper.wav")
# ── arecord: grabar desde el micrófono ──────────────────────────
# PipeWire expone el micrófono como dispositivo PulseAudio.
# arecord es la herramienta CLI de ALSA/PulseAudio para grabar.
try:
subprocess.run(
["arecord", "-f", "cd", "-t", "wav", "-d", str(segundos), raw_path],
capture_output=True,
check=True,
)
print_ok(f"Grabación cruda: {raw_path}")
except subprocess.CalledProcessError as e:
print_warn(f"Error al grabar: {e.stderr.decode().strip()}")
print_warn("Asegúrate de tener el micrófono conectado y PipeWire funcionando.")
Path(raw_path).unlink(missing_ok=True)
return None
except FileNotFoundError:
print_warn("arecord no encontrado. Instala alsa-utils o pulseaudio-utils.")
return None
# ── ffmpeg: preparar para Whisper ───────────────────────────────
# Esto es el comando estrella del Episodio 842:
# -ar 16000 → 16 kHz (Whisper lo espera así)
# -ac 1 → Mono
# -c:a pcm_s16le → Formato nativo WAV
# -af loudnorm → Normalización EBU R128
print_step("⚡", "Preparando audio con ffmpeg (16 kHz, mono, loudnorm)...")
result = subprocess.run(
[
"ffmpeg",
"-y",
"-i",
raw_path,
"-ar",
"16000",
"-ac",
"1",
"-c:a",
"pcm_s16le",
"-af",
"loudnorm",
whisper_path,
],
capture_output=True,
check=False,
)
# Limpiar grabación cruda
Path(raw_path).unlink(missing_ok=True)
if result.returncode != 0 or not Path(whisper_path).exists():
print_warn(f"Error en ffmpeg: {result.stderr.decode()[:200]}")
return None
# Mostrar info del archivo resultante
size = Path(whisper_path).stat().st_size
print_ok(f"Audio preparado: {whisper_path} ({size // 1024} KB)")
print_ok("Formato: 16 kHz / Mono / PCM s16le / Normalizado")
return whisper_path
# ── Paso 2: Transcribir con Whisper ─────────────────────────────────────
def transcribir(audio_path: str) -> str:
"""
Transcribe el audio preparado con faster-whisper.
faster-whisper es una reimplementación de Whisper usando CTranslate2,
que es 4x más rápida que la original y usa menos memoria.
Args:
audio_path: Ruta al WAV preparado con ffmpeg.
Returns:
Texto transcrito, o cadena vacía si falla.
"""
print_step("📝", "Transcribiendo con Whisper...")
try:
from faster_whisper import WhisperModel
except ImportError:
print_warn("faster-whisper no instalado. Ejecuta:")
print_warn(" pip install faster-whisper")
return ""
# Cargar modelo (descarga automática la primera vez)
print_ok("Cargando modelo Whisper 'base'...")
t0 = time.time()
model = WhisperModel("base", device="cpu", compute_type="int8")
print_ok(f"Modelo cargado en {time.time() - t0:.1f}s")
# Transcribir
# language="es" fuerza español (evita que detecte mezcla)
# beam_size=1 es más rápido, beam_size>1 más preciso
segments, info = model.transcribe(
audio_path,
language="es",
beam_size=1,
)
texto = " ".join(segment.text for segment in segments)
print_ok(
f"Idioma detectado: {info.language} (prob: {info.language_probability:.2f})"
)
# Limpiar archivo temporal
Path(audio_path).unlink(missing_ok=True)
return texto.strip()
# ── Paso 3: Consultar a Ollama ──────────────────────────────────────────
def consultar_ollama(prompt: str) -> str:
"""
Envía el texto transcrito a Ollama y obtiene una respuesta.
Ollama corre el modelo localmente (llama3.2, mistral, etc.).
Nada se envía a la nube.
Args:
prompt: Texto transcrito del usuario.
Returns:
Respuesta del modelo, o mensaje de error.
"""
print_step("🧠", "Consultando Ollama...")
payload = {
"model": OLLAMA_MODEL,
"prompt": prompt,
"stream": False,
}
try:
t0 = time.time()
resp = requests.post(OLLAMA_URL, json=payload, timeout=60)
resp.raise_for_status()
respuesta = resp.json().get("response", "")
print_ok(f"Respuesta generada en {time.time() - t0:.1f}s")
return respuesta
except requests.exceptions.ConnectionError:
print_warn(f"No se pudo conectar a Ollama en {OLLAMA_URL}")
print_warn("¿Está Ollama corriendo?")
return "Lo siento, no pude conectar con el asistente."
except requests.exceptions.Timeout:
print_warn("Ollama tardó demasiado en responder.")
return "Lo siento, la respuesta tardó demasiado."
except Exception as e:
print_warn(f"Error inesperado: {e}")
return ""
# ── Paso 4: Responder con TTS + ffmpeg ─────────────────────────────────
def responder(texto: str) -> None:
"""
Genera voz sintética con Piper y la postprocesa con ffmpeg.
El pipeline de audio es:
1. Piper genera WAV crudo
2. ffmpeg postprocesa con: loudnorm + compand + highpass
3. aplay reproduce el resultado
Args:
texto: Texto a convertir a voz.
"""
if not texto:
return
print_step("🔊", "Generando voz con TTS + ffmpeg...")
try:
import wave
from piper import PiperVoice
except ImportError:
print_warn("piper-tts no instalado. Ejecuta:")
print_warn(" pip install piper-tts")
print_warn(
" (o descarga el modelo .onnx desde https://github.com/rhasspy/piper)"
)
# Fallback: mostrar el texto por pantalla
print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto}")
return
# ── Generar WAV con Piper ──────────────────────────────────────
tts_path = "/tmp/tts_raw.wav"
try:
# Cargar modelo de voz en español
# Descarga el modelo desde:
# https://huggingface.co/rhasspy/piper-voices/tree/main/es/es_ES/sharvard/medium
voice = PiperVoice.load("es_ES-sharvard-medium.onnx")
with wave.open(tts_path, "wb") as wav_file:
voice.synthesize_wav(texto, wav_file)
print_ok(f"TTS generado: {tts_path}")
except Exception as e:
print_warn(f"Error al generar TTS con Piper: {e}")
print_warn("Asegúrate de tener el modelo 'es_ES-sharvard-medium.onnx'")
# Fallback
print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto[:200]}...")
return
# ── Postprocesar con ffmpeg ────────────────────────────────────
# Este es el SEGUNDO comando estrella del Episodio 842:
# loudnorm → Normaliza el volumen
# compand → Comprime el rango dinámico (equilibra suaves y fuertes)
# highpass → Elimina frecuencias < 80 Hz (soplidos)
final_path = "/tmp/tts_final.wav"
filter_chain = (
"loudnorm,"
"compand=attacks=0.1:decays=0.2:"
"points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15"
"|-15/-12|-10/-6|-6/-3|0/-0.5,"
"highpass=f=80"
)
result = subprocess.run(
[
"ffmpeg",
"-y",
"-i",
tts_path,
"-af",
filter_chain,
final_path,
],
capture_output=True,
check=False,
)
if result.returncode != 0 or not Path(final_path).exists():
print_warn(f"Error en ffmpeg post-procesado: {result.stderr.decode()[:200]}")
# Reproducir el TTS crudo como fallback
subprocess.run(["aplay", "-q", tts_path], capture_output=True)
Path(tts_path).unlink(missing_ok=True)
return
size = Path(final_path).stat().st_size
print_ok(f"Audio postprocesado con ffmpeg: {final_path} ({size // 1024} KB)")
print_ok("Filtros aplicados: loudnorm + compand + highpass")
# ── Reproducir con aplay ───────────────────────────────────────
print_step("▶️", "Reproduciendo respuesta...")
subprocess.run(["aplay", "-q", final_path], capture_output=True)
print_ok("Reproducción completada")
# ── Limpiar archivos temporales ────────────────────────────────
Path(tts_path).unlink(missing_ok=True)
Path(final_path).unlink(missing_ok=True)
print_ok("Archivos temporales eliminados")
# ── Bucle principal ─────────────────────────────────────────────────────
def main() -> None:
"""
Bucle principal del asistente de voz.
Pipeline completo:
Micrófono → [arecord] → ffmpeg → [Whisper] → Ollama → [Piper] → ffmpeg → [aplay]
"""
print("\n" + "═" * 60)
print(f"{Color.CYAN}🎤 Asistente de voz local{Color.NC}")
print("Episodio 842 — atareao con Linux")
print(
f"{Color.GREEN}Mic → ffmpeg → Whisper → Ollama → TTS → ffmpeg → Altavoz{Color.NC}"
)
print("═" * 60)
print(f"\n{Color.YELLOW}Habla. Pulsa Ctrl+C para salir.{Color.NC}\n")
# Verificar dependencias al inicio
dependencies_ok = True
# Verificar ffmpeg
if not subprocess.run(["which", "ffmpeg"], capture_output=True).returncode == 0:
print_warn("ffmpeg no está instalado. Ejecuta: sudo apt install ffmpeg")
dependencies_ok = False
# Verificar Ollama
try:
requests.get(OLLAMA_URL.replace("/api/generate", "/api/tags"), timeout=5)
print_ok(f"Ollama conectado en {OLLAMA_URL}")
except Exception:
print_warn(f"Ollama no responde en {OLLAMA_URL}")
print_warn("¿Está corriendo? Ejecuta: ollama serve && ollama run llama3.2")
dependencies_ok = False
# Verificar arecord
if not subprocess.run(["which", "arecord"], capture_output=True).returncode == 0:
print_warn("arecord no encontrado. Instala alsa-utils.")
dependencies_ok = False
if not dependencies_ok:
print(
f"\n{Color.RED}⚠️ Hay dependencias faltantes. Revisa los mensajes anteriores.{Color.NC}"
)
print("El script intentará continuar, pero puede fallar en algunos pasos.\n")
# ── Bucle principal ────────────────────────────────────────────
try:
while True:
print("\n" + "━" * 60)
# 1. Grabar y preparar con ffmpeg
audio = grabar_y_preparar(segundos=5)
if audio is None:
print_warn("No se pudo grabar. Reintentando...")
continue
# 2. Transcribir con Whisper
texto = transcribir(audio)
if not texto:
print_warn("No se detectó voz. ¿Demasiado silencio?")
continue
print(f"\n{Color.GREEN}📝 Tú:{Color.NC} {texto}")
# 3. Consultar a Ollama
respuesta = consultar_ollama(texto)
if not respuesta:
print_warn("No se obtuvo respuesta de Ollama.")
continue
# Mostrar preview de la respuesta
preview = respuesta[:150] + ("..." if len(respuesta) > 150 else "")
print(f"{Color.YELLOW}🤖 IA: {preview}{Color.NC}")
# 4. Responder con TTS + ffmpeg
responder(respuesta)
print("─" * 60)
except KeyboardInterrupt:
print(f"\n\n{Color.GREEN}👋 ¡Hasta luego!{Color.NC}")
print("Recuerda: ffmpeg es la navaja suiza del audio para tu IA.")
except Exception as e:
print(f"\n{Color.RED}❌ Error inesperado:{Color.NC} {e}")
raise
if __name__ == "__main__":
main()
| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | Pipeline completo: micrófono → ffmpeg → Whisper → Ollama → TTS → ffmpeg → altavoz |
| 4 | ================================================================================ |
| 5 | |
| 6 | Este script une todo lo visto en el arco de audio/vídeo de la Temporada 9: |
| 7 | 842 — ffmpeg (preparación y postprocesado de audio) |
| 8 | 843 — Whisper (transcripción local) |
| 9 | 845 — TTS con Piper/Kokoro (voz sintética) |
| 10 | 847 — visión artificial local (LLaVA, Florence-2, Moondream) |
| 11 | |
| 12 | Requisitos: |
| 13 | pip install faster-whisper requests piper-tts |
| 14 | |
| 15 | Además necesitas tener instalado: |
| 16 | - ffmpeg (sudo apt install ffmpeg) |
| 17 | - Ollama corriendo en localhost:11434 |
| 18 | - Piper TTS con voz en español (es_ES-sharvard-medium.onnx) |
| 19 | |
| 20 | atareao con Linux — Episodio 842 |
| 21 | """ |
| 22 | |
| 23 | import subprocess |
| 24 | import tempfile |
| 25 | import time |
| 26 | from pathlib import Path |
| 27 | from typing import Optional |
| 28 | |
| 29 | import requests |
| 30 | |
| 31 | # ── Configuración ──────────────────────────────────────────────────────── |
| 32 | OLLAMA_URL = "http://localhost:11434/api/generate" |
| 33 | OLLAMA_MODEL = "llama3.2:latest" |
| 34 | |
| 35 | |
| 36 | # ── Colores para terminal ──────────────────────────────────────────────── |
| 37 | class Color: |
| 38 | CYAN = "\033[0;36m" |
| 39 | GREEN = "\033[0;32m" |
| 40 | YELLOW = "\033[1;33m" |
| 41 | RED = "\033[0;31m" |
| 42 | NC = "\033[0m" |
| 43 | |
| 44 | |
| 45 | def print_step(emoji: str, msg: str) -> None: |
| 46 | """Imprime un paso del pipeline con formato.""" |
| 47 | print(f"\n{emoji} {Color.CYAN}{msg}{Color.NC}") |
| 48 | print("─" * 50) |
| 49 | |
| 50 | |
| 51 | def print_ok(msg: str) -> None: |
| 52 | print(f" {Color.GREEN}✅{Color.NC} {msg}") |
| 53 | |
| 54 | |
| 55 | def print_warn(msg: str) -> None: |
| 56 | print(f" {Color.YELLOW}⚠️{Color.NC} {msg}") |
| 57 | |
| 58 | |
| 59 | # ── Paso 1: Grabar y preparar con ffmpeg ──────────────────────────────── |
| 60 | def grabar_y_preparar(segundos: int = 5) -> Optional[str]: |
| 61 | """ |
| 62 | Graba audio del micrófono y lo prepara para Whisper con ffmpeg. |
| 63 | |
| 64 | 1. arecord captura el audio en bruto |
| 65 | 2. ffmpeg lo convierte a: 16 kHz, mono, s16le + loudnorm |
| 66 | |
| 67 | Returns: |
| 68 | Ruta al archivo WAV listo para Whisper, o None si falla. |
| 69 | """ |
| 70 | print_step("🎤", f"Grabando {segundos} segundos...") |
| 71 | |
| 72 | # Archivo temporal para la grabación en bruto |
| 73 | with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f: |
| 74 | raw_path = f.name |
| 75 | |
| 76 | # Archivo para el audio preparado |
| 77 | whisper_path = raw_path.replace(".wav", "_whisper.wav") |
| 78 | |
| 79 | # ── arecord: grabar desde el micrófono ────────────────────────── |
| 80 | # PipeWire expone el micrófono como dispositivo PulseAudio. |
| 81 | # arecord es la herramienta CLI de ALSA/PulseAudio para grabar. |
| 82 | try: |
| 83 | subprocess.run( |
| 84 | ["arecord", "-f", "cd", "-t", "wav", "-d", str(segundos), raw_path], |
| 85 | capture_output=True, |
| 86 | check=True, |
| 87 | ) |
| 88 | print_ok(f"Grabación cruda: {raw_path}") |
| 89 | except subprocess.CalledProcessError as e: |
| 90 | print_warn(f"Error al grabar: {e.stderr.decode().strip()}") |
| 91 | print_warn("Asegúrate de tener el micrófono conectado y PipeWire funcionando.") |
| 92 | Path(raw_path).unlink(missing_ok=True) |
| 93 | return None |
| 94 | except FileNotFoundError: |
| 95 | print_warn("arecord no encontrado. Instala alsa-utils o pulseaudio-utils.") |
| 96 | return None |
| 97 | |
| 98 | # ── ffmpeg: preparar para Whisper ─────────────────────────────── |
| 99 | # Esto es el comando estrella del Episodio 842: |
| 100 | # -ar 16000 → 16 kHz (Whisper lo espera así) |
| 101 | # -ac 1 → Mono |
| 102 | # -c:a pcm_s16le → Formato nativo WAV |
| 103 | # -af loudnorm → Normalización EBU R128 |
| 104 | print_step("⚡", "Preparando audio con ffmpeg (16 kHz, mono, loudnorm)...") |
| 105 | |
| 106 | result = subprocess.run( |
| 107 | [ |
| 108 | "ffmpeg", |
| 109 | "-y", |
| 110 | "-i", |
| 111 | raw_path, |
| 112 | "-ar", |
| 113 | "16000", |
| 114 | "-ac", |
| 115 | "1", |
| 116 | "-c:a", |
| 117 | "pcm_s16le", |
| 118 | "-af", |
| 119 | "loudnorm", |
| 120 | whisper_path, |
| 121 | ], |
| 122 | capture_output=True, |
| 123 | check=False, |
| 124 | ) |
| 125 | |
| 126 | # Limpiar grabación cruda |
| 127 | Path(raw_path).unlink(missing_ok=True) |
| 128 | |
| 129 | if result.returncode != 0 or not Path(whisper_path).exists(): |
| 130 | print_warn(f"Error en ffmpeg: {result.stderr.decode()[:200]}") |
| 131 | return None |
| 132 | |
| 133 | # Mostrar info del archivo resultante |
| 134 | size = Path(whisper_path).stat().st_size |
| 135 | print_ok(f"Audio preparado: {whisper_path} ({size // 1024} KB)") |
| 136 | print_ok("Formato: 16 kHz / Mono / PCM s16le / Normalizado") |
| 137 | return whisper_path |
| 138 | |
| 139 | |
| 140 | # ── Paso 2: Transcribir con Whisper ───────────────────────────────────── |
| 141 | def transcribir(audio_path: str) -> str: |
| 142 | """ |
| 143 | Transcribe el audio preparado con faster-whisper. |
| 144 | |
| 145 | faster-whisper es una reimplementación de Whisper usando CTranslate2, |
| 146 | que es 4x más rápida que la original y usa menos memoria. |
| 147 | |
| 148 | Args: |
| 149 | audio_path: Ruta al WAV preparado con ffmpeg. |
| 150 | |
| 151 | Returns: |
| 152 | Texto transcrito, o cadena vacía si falla. |
| 153 | """ |
| 154 | print_step("📝", "Transcribiendo con Whisper...") |
| 155 | |
| 156 | try: |
| 157 | from faster_whisper import WhisperModel |
| 158 | except ImportError: |
| 159 | print_warn("faster-whisper no instalado. Ejecuta:") |
| 160 | print_warn(" pip install faster-whisper") |
| 161 | return "" |
| 162 | |
| 163 | # Cargar modelo (descarga automática la primera vez) |
| 164 | print_ok("Cargando modelo Whisper 'base'...") |
| 165 | t0 = time.time() |
| 166 | model = WhisperModel("base", device="cpu", compute_type="int8") |
| 167 | print_ok(f"Modelo cargado en {time.time() - t0:.1f}s") |
| 168 | |
| 169 | # Transcribir |
| 170 | # language="es" fuerza español (evita que detecte mezcla) |
| 171 | # beam_size=1 es más rápido, beam_size>1 más preciso |
| 172 | segments, info = model.transcribe( |
| 173 | audio_path, |
| 174 | language="es", |
| 175 | beam_size=1, |
| 176 | ) |
| 177 | |
| 178 | texto = " ".join(segment.text for segment in segments) |
| 179 | print_ok( |
| 180 | f"Idioma detectado: {info.language} (prob: {info.language_probability:.2f})" |
| 181 | ) |
| 182 | |
| 183 | # Limpiar archivo temporal |
| 184 | Path(audio_path).unlink(missing_ok=True) |
| 185 | |
| 186 | return texto.strip() |
| 187 | |
| 188 | |
| 189 | # ── Paso 3: Consultar a Ollama ────────────────────────────────────────── |
| 190 | def consultar_ollama(prompt: str) -> str: |
| 191 | """ |
| 192 | Envía el texto transcrito a Ollama y obtiene una respuesta. |
| 193 | |
| 194 | Ollama corre el modelo localmente (llama3.2, mistral, etc.). |
| 195 | Nada se envía a la nube. |
| 196 | |
| 197 | Args: |
| 198 | prompt: Texto transcrito del usuario. |
| 199 | |
| 200 | Returns: |
| 201 | Respuesta del modelo, o mensaje de error. |
| 202 | """ |
| 203 | print_step("🧠", "Consultando Ollama...") |
| 204 | |
| 205 | payload = { |
| 206 | "model": OLLAMA_MODEL, |
| 207 | "prompt": prompt, |
| 208 | "stream": False, |
| 209 | } |
| 210 | |
| 211 | try: |
| 212 | t0 = time.time() |
| 213 | resp = requests.post(OLLAMA_URL, json=payload, timeout=60) |
| 214 | resp.raise_for_status() |
| 215 | respuesta = resp.json().get("response", "") |
| 216 | print_ok(f"Respuesta generada en {time.time() - t0:.1f}s") |
| 217 | return respuesta |
| 218 | except requests.exceptions.ConnectionError: |
| 219 | print_warn(f"No se pudo conectar a Ollama en {OLLAMA_URL}") |
| 220 | print_warn("¿Está Ollama corriendo?") |
| 221 | return "Lo siento, no pude conectar con el asistente." |
| 222 | except requests.exceptions.Timeout: |
| 223 | print_warn("Ollama tardó demasiado en responder.") |
| 224 | return "Lo siento, la respuesta tardó demasiado." |
| 225 | except Exception as e: |
| 226 | print_warn(f"Error inesperado: {e}") |
| 227 | return "" |
| 228 | |
| 229 | |
| 230 | # ── Paso 4: Responder con TTS + ffmpeg ───────────────────────────────── |
| 231 | def responder(texto: str) -> None: |
| 232 | """ |
| 233 | Genera voz sintética con Piper y la postprocesa con ffmpeg. |
| 234 | |
| 235 | El pipeline de audio es: |
| 236 | 1. Piper genera WAV crudo |
| 237 | 2. ffmpeg postprocesa con: loudnorm + compand + highpass |
| 238 | 3. aplay reproduce el resultado |
| 239 | |
| 240 | Args: |
| 241 | texto: Texto a convertir a voz. |
| 242 | """ |
| 243 | if not texto: |
| 244 | return |
| 245 | |
| 246 | print_step("🔊", "Generando voz con TTS + ffmpeg...") |
| 247 | |
| 248 | try: |
| 249 | import wave |
| 250 | from piper import PiperVoice |
| 251 | except ImportError: |
| 252 | print_warn("piper-tts no instalado. Ejecuta:") |
| 253 | print_warn(" pip install piper-tts") |
| 254 | print_warn( |
| 255 | " (o descarga el modelo .onnx desde https://github.com/rhasspy/piper)" |
| 256 | ) |
| 257 | # Fallback: mostrar el texto por pantalla |
| 258 | print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto}") |
| 259 | return |
| 260 | |
| 261 | # ── Generar WAV con Piper ────────────────────────────────────── |
| 262 | tts_path = "/tmp/tts_raw.wav" |
| 263 | |
| 264 | try: |
| 265 | # Cargar modelo de voz en español |
| 266 | # Descarga el modelo desde: |
| 267 | # https://huggingface.co/rhasspy/piper-voices/tree/main/es/es_ES/sharvard/medium |
| 268 | voice = PiperVoice.load("es_ES-sharvard-medium.onnx") |
| 269 | |
| 270 | with wave.open(tts_path, "wb") as wav_file: |
| 271 | voice.synthesize_wav(texto, wav_file) |
| 272 | |
| 273 | print_ok(f"TTS generado: {tts_path}") |
| 274 | except Exception as e: |
| 275 | print_warn(f"Error al generar TTS con Piper: {e}") |
| 276 | print_warn("Asegúrate de tener el modelo 'es_ES-sharvard-medium.onnx'") |
| 277 | # Fallback |
| 278 | print(f"\n{Color.GREEN}🤖 IA:{Color.NC} {texto[:200]}...") |
| 279 | return |
| 280 | |
| 281 | # ── Postprocesar con ffmpeg ──────────────────────────────────── |
| 282 | # Este es el SEGUNDO comando estrella del Episodio 842: |
| 283 | # loudnorm → Normaliza el volumen |
| 284 | # compand → Comprime el rango dinámico (equilibra suaves y fuertes) |
| 285 | # highpass → Elimina frecuencias < 80 Hz (soplidos) |
| 286 | final_path = "/tmp/tts_final.wav" |
| 287 | |
| 288 | filter_chain = ( |
| 289 | "loudnorm," |
| 290 | "compand=attacks=0.1:decays=0.2:" |
| 291 | "points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15" |
| 292 | "|-15/-12|-10/-6|-6/-3|0/-0.5," |
| 293 | "highpass=f=80" |
| 294 | ) |
| 295 | |
| 296 | result = subprocess.run( |
| 297 | [ |
| 298 | "ffmpeg", |
| 299 | "-y", |
| 300 | "-i", |
| 301 | tts_path, |
| 302 | "-af", |
| 303 | filter_chain, |
| 304 | final_path, |
| 305 | ], |
| 306 | capture_output=True, |
| 307 | check=False, |
| 308 | ) |
| 309 | |
| 310 | if result.returncode != 0 or not Path(final_path).exists(): |
| 311 | print_warn(f"Error en ffmpeg post-procesado: {result.stderr.decode()[:200]}") |
| 312 | # Reproducir el TTS crudo como fallback |
| 313 | subprocess.run(["aplay", "-q", tts_path], capture_output=True) |
| 314 | Path(tts_path).unlink(missing_ok=True) |
| 315 | return |
| 316 | |
| 317 | size = Path(final_path).stat().st_size |
| 318 | print_ok(f"Audio postprocesado con ffmpeg: {final_path} ({size // 1024} KB)") |
| 319 | print_ok("Filtros aplicados: loudnorm + compand + highpass") |
| 320 | |
| 321 | # ── Reproducir con aplay ─────────────────────────────────────── |
| 322 | print_step("▶️", "Reproduciendo respuesta...") |
| 323 | subprocess.run(["aplay", "-q", final_path], capture_output=True) |
| 324 | print_ok("Reproducción completada") |
| 325 | |
| 326 | # ── Limpiar archivos temporales ──────────────────────────────── |
| 327 | Path(tts_path).unlink(missing_ok=True) |
| 328 | Path(final_path).unlink(missing_ok=True) |
| 329 | print_ok("Archivos temporales eliminados") |
| 330 | |
| 331 | |
| 332 | # ── Bucle principal ───────────────────────────────────────────────────── |
| 333 | def main() -> None: |
| 334 | """ |
| 335 | Bucle principal del asistente de voz. |
| 336 | |
| 337 | Pipeline completo: |
| 338 | Micrófono → [arecord] → ffmpeg → [Whisper] → Ollama → [Piper] → ffmpeg → [aplay] |
| 339 | """ |
| 340 | print("\n" + "═" * 60) |
| 341 | print(f"{Color.CYAN}🎤 Asistente de voz local{Color.NC}") |
| 342 | print("Episodio 842 — atareao con Linux") |
| 343 | print( |
| 344 | f"{Color.GREEN}Mic → ffmpeg → Whisper → Ollama → TTS → ffmpeg → Altavoz{Color.NC}" |
| 345 | ) |
| 346 | print("═" * 60) |
| 347 | print(f"\n{Color.YELLOW}Habla. Pulsa Ctrl+C para salir.{Color.NC}\n") |
| 348 | |
| 349 | # Verificar dependencias al inicio |
| 350 | dependencies_ok = True |
| 351 | |
| 352 | # Verificar ffmpeg |
| 353 | if not subprocess.run(["which", "ffmpeg"], capture_output=True).returncode == 0: |
| 354 | print_warn("ffmpeg no está instalado. Ejecuta: sudo apt install ffmpeg") |
| 355 | dependencies_ok = False |
| 356 | |
| 357 | # Verificar Ollama |
| 358 | try: |
| 359 | requests.get(OLLAMA_URL.replace("/api/generate", "/api/tags"), timeout=5) |
| 360 | print_ok(f"Ollama conectado en {OLLAMA_URL}") |
| 361 | except Exception: |
| 362 | print_warn(f"Ollama no responde en {OLLAMA_URL}") |
| 363 | print_warn("¿Está corriendo? Ejecuta: ollama serve && ollama run llama3.2") |
| 364 | dependencies_ok = False |
| 365 | |
| 366 | # Verificar arecord |
| 367 | if not subprocess.run(["which", "arecord"], capture_output=True).returncode == 0: |
| 368 | print_warn("arecord no encontrado. Instala alsa-utils.") |
| 369 | dependencies_ok = False |
| 370 | |
| 371 | if not dependencies_ok: |
| 372 | print( |
| 373 | f"\n{Color.RED}⚠️ Hay dependencias faltantes. Revisa los mensajes anteriores.{Color.NC}" |
| 374 | ) |
| 375 | print("El script intentará continuar, pero puede fallar en algunos pasos.\n") |
| 376 | |
| 377 | # ── Bucle principal ──────────────────────────────────────────── |
| 378 | try: |
| 379 | while True: |
| 380 | print("\n" + "━" * 60) |
| 381 | |
| 382 | # 1. Grabar y preparar con ffmpeg |
| 383 | audio = grabar_y_preparar(segundos=5) |
| 384 | if audio is None: |
| 385 | print_warn("No se pudo grabar. Reintentando...") |
| 386 | continue |
| 387 | |
| 388 | # 2. Transcribir con Whisper |
| 389 | texto = transcribir(audio) |
| 390 | if not texto: |
| 391 | print_warn("No se detectó voz. ¿Demasiado silencio?") |
| 392 | continue |
| 393 | |
| 394 | print(f"\n{Color.GREEN}📝 Tú:{Color.NC} {texto}") |
| 395 | |
| 396 | # 3. Consultar a Ollama |
| 397 | respuesta = consultar_ollama(texto) |
| 398 | if not respuesta: |
| 399 | print_warn("No se obtuvo respuesta de Ollama.") |
| 400 | continue |
| 401 | |
| 402 | # Mostrar preview de la respuesta |
| 403 | preview = respuesta[:150] + ("..." if len(respuesta) > 150 else "") |
| 404 | print(f"{Color.YELLOW}🤖 IA: {preview}{Color.NC}") |
| 405 | |
| 406 | # 4. Responder con TTS + ffmpeg |
| 407 | responder(respuesta) |
| 408 | |
| 409 | print("─" * 60) |
| 410 | |
| 411 | except KeyboardInterrupt: |
| 412 | print(f"\n\n{Color.GREEN}👋 ¡Hasta luego!{Color.NC}") |
| 413 | print("Recuerda: ffmpeg es la navaja suiza del audio para tu IA.") |
| 414 | except Exception as e: |
| 415 | print(f"\n{Color.RED}❌ Error inesperado:{Color.NC} {e}") |
| 416 | raise |
| 417 | |
| 418 | |
| 419 | if __name__ == "__main__": |
| 420 | main() |
whisperize.sh
· 4.1 KiB · Bash
Originalformat
#!/usr/bin/env bash
# ==============================================================================
# whisperize.sh — Script autónomo para preparar audio para Whisper
#
# Convierte CUALQUIER archivo de audio al formato que Whisper espera:
# - Frecuencia de muestreo: 16.000 Hz (16 kHz)
# - Canales: Mono (1)
# - Formato: PCM signed 16-bit little-endian (pcm_s16le)
# - Normalizado con loudnorm (EBU R128, -23 LUFS)
#
# Uso:
# ./whisperize.sh grabacion.mp3 → grabacion_whisper.wav
# ./whisperize.sh grabacion.mp3 salida → salida.wav
#
# atareao con Linux — Episodio 842
# ==============================================================================
set -euo pipefail
# ── Colores (si terminal lo soporta) ──────────────────────────────────────
if [ -t 1 ]; then
VERDE='\033[0;32m'
AMARILLO='\033[1;33m'
CYAN='\033[0;36m'
NC='\033[0m' # Sin color
else
VERDE=''; AMARILLO=''; CYAN=''; NC=''
fi
# ── Ayuda ─────────────────────────────────────────────────────────────────
usage() {
echo "Uso: $0 <entrada> [salida]"
echo ""
echo "Prepara un archivo de audio para su transcripción con Whisper."
echo ""
echo "Argumentos:"
echo " entrada Ruta al archivo de audio original (cualquier formato)"
echo " salida Ruta del archivo WAV resultante (opcional)"
echo ""
echo "Si no se especifica salida, se genera: <entrada_sin_ext>_whisper.wav"
echo ""
echo "Ejemplos:"
echo " $0 grabacion.mp3"
echo " $0 audio.ogg michelada_whisper.wav"
exit 0
}
# ── Parsear argumentos ────────────────────────────────────────────────────
if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
usage
fi
ENTRADA="$1"
SALIDA="${2:-${ENTRADA%.*}_whisper.wav}"
# ── Validar entrada ───────────────────────────────────────────────────────
if [ ! -f "$ENTRADA" ]; then
echo -e "${AMARILLO}❌ Error: No se encuentra el archivo '${ENTRADA}'${NC}" >&2
exit 1
fi
if ! command -v ffmpeg &>/dev/null; then
echo -e "${AMARILLO}❌ Error: ffmpeg no está instalado.${NC}" >&2
echo " Instálalo con: sudo apt install ffmpeg" >&2
exit 1
fi
# ── Mostrar info del archivo original ─────────────────────────────────────
echo -e "${CYAN}🎤 whisperize — Preparando audio para Whisper${NC}"
echo ""
ORIG_SIZE=$(stat -c%s "$ENTRADA" 2>/dev/null || echo "?")
echo -e " ${VERDE}Entrada:${NC} $ENTRADA ($([ -f "$ENTRADA" ] && echo "$(du -h "$ENTRADA" | cut -f1)" || echo "?"))"
echo -e " ${VERDE}Salida:${NC} $SALIDA"
echo ""
# Mostrar info del audio original (si ffprobe está disponible)
if command -v ffprobe &>/dev/null; then
echo " Información del original:"
ffprobe -v quiet -show_entries \
stream=codec_name,sample_rate,channels:format=format_name,duration \
-of default=noprint_wrappers=1 "$ENTRADA" 2>/dev/null \
| sed 's/^/ /'
echo ""
fi
# ── Convertir ─────────────────────────────────────────────────────────────
echo -e " ${VERDE}Procesando...${NC}"
ffmpeg -y \
-i "$ENTRADA" \
-ar 16000 \
-ac 1 \
-c:a pcm_s16le \
-af loudnorm \
"$SALIDA" 2>&1 \
| grep -E "(size=|bitrate=|Output)" \
| sed 's/^/ /'
echo ""
echo -e " ${VERDE}✅ Listo:${NC} $SALIDA ($(du -h "$SALIDA" | cut -f1))"
echo ""
echo -e " ${CYAN}📋 Resumen:${NC}"
echo " Frecuencia: 16 kHz Canales: Mono Formato: PCM s16le"
echo " Volumen: Normalizado (loudnorm, -23 LUFS)"
echo " Listo para: whisper transcribe"
| 1 | #!/usr/bin/env bash |
| 2 | # ============================================================================== |
| 3 | # whisperize.sh — Script autónomo para preparar audio para Whisper |
| 4 | # |
| 5 | # Convierte CUALQUIER archivo de audio al formato que Whisper espera: |
| 6 | # - Frecuencia de muestreo: 16.000 Hz (16 kHz) |
| 7 | # - Canales: Mono (1) |
| 8 | # - Formato: PCM signed 16-bit little-endian (pcm_s16le) |
| 9 | # - Normalizado con loudnorm (EBU R128, -23 LUFS) |
| 10 | # |
| 11 | # Uso: |
| 12 | # ./whisperize.sh grabacion.mp3 → grabacion_whisper.wav |
| 13 | # ./whisperize.sh grabacion.mp3 salida → salida.wav |
| 14 | # |
| 15 | # atareao con Linux — Episodio 842 |
| 16 | # ============================================================================== |
| 17 | set -euo pipefail |
| 18 | |
| 19 | # ── Colores (si terminal lo soporta) ────────────────────────────────────── |
| 20 | if [ -t 1 ]; then |
| 21 | VERDE='\033[0;32m' |
| 22 | AMARILLO='\033[1;33m' |
| 23 | CYAN='\033[0;36m' |
| 24 | NC='\033[0m' # Sin color |
| 25 | else |
| 26 | VERDE=''; AMARILLO=''; CYAN=''; NC='' |
| 27 | fi |
| 28 | |
| 29 | # ── Ayuda ───────────────────────────────────────────────────────────────── |
| 30 | usage() { |
| 31 | echo "Uso: $0 <entrada> [salida]" |
| 32 | echo "" |
| 33 | echo "Prepara un archivo de audio para su transcripción con Whisper." |
| 34 | echo "" |
| 35 | echo "Argumentos:" |
| 36 | echo " entrada Ruta al archivo de audio original (cualquier formato)" |
| 37 | echo " salida Ruta del archivo WAV resultante (opcional)" |
| 38 | echo "" |
| 39 | echo "Si no se especifica salida, se genera: <entrada_sin_ext>_whisper.wav" |
| 40 | echo "" |
| 41 | echo "Ejemplos:" |
| 42 | echo " $0 grabacion.mp3" |
| 43 | echo " $0 audio.ogg michelada_whisper.wav" |
| 44 | exit 0 |
| 45 | } |
| 46 | |
| 47 | # ── Parsear argumentos ──────────────────────────────────────────────────── |
| 48 | if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then |
| 49 | usage |
| 50 | fi |
| 51 | |
| 52 | ENTRADA="$1" |
| 53 | SALIDA="${2:-${ENTRADA%.*}_whisper.wav}" |
| 54 | |
| 55 | # ── Validar entrada ─────────────────────────────────────────────────────── |
| 56 | if [ ! -f "$ENTRADA" ]; then |
| 57 | echo -e "${AMARILLO}❌ Error: No se encuentra el archivo '${ENTRADA}'${NC}" >&2 |
| 58 | exit 1 |
| 59 | fi |
| 60 | |
| 61 | if ! command -v ffmpeg &>/dev/null; then |
| 62 | echo -e "${AMARILLO}❌ Error: ffmpeg no está instalado.${NC}" >&2 |
| 63 | echo " Instálalo con: sudo apt install ffmpeg" >&2 |
| 64 | exit 1 |
| 65 | fi |
| 66 | |
| 67 | # ── Mostrar info del archivo original ───────────────────────────────────── |
| 68 | echo -e "${CYAN}🎤 whisperize — Preparando audio para Whisper${NC}" |
| 69 | echo "" |
| 70 | ORIG_SIZE=$(stat -c%s "$ENTRADA" 2>/dev/null || echo "?") |
| 71 | echo -e " ${VERDE}Entrada:${NC} $ENTRADA ($([ -f "$ENTRADA" ] && echo "$(du -h "$ENTRADA" | cut -f1)" || echo "?"))" |
| 72 | echo -e " ${VERDE}Salida:${NC} $SALIDA" |
| 73 | echo "" |
| 74 | |
| 75 | # Mostrar info del audio original (si ffprobe está disponible) |
| 76 | if command -v ffprobe &>/dev/null; then |
| 77 | echo " Información del original:" |
| 78 | ffprobe -v quiet -show_entries \ |
| 79 | stream=codec_name,sample_rate,channels:format=format_name,duration \ |
| 80 | -of default=noprint_wrappers=1 "$ENTRADA" 2>/dev/null \ |
| 81 | | sed 's/^/ /' |
| 82 | echo "" |
| 83 | fi |
| 84 | |
| 85 | # ── Convertir ───────────────────────────────────────────────────────────── |
| 86 | echo -e " ${VERDE}Procesando...${NC}" |
| 87 | |
| 88 | ffmpeg -y \ |
| 89 | -i "$ENTRADA" \ |
| 90 | -ar 16000 \ |
| 91 | -ac 1 \ |
| 92 | -c:a pcm_s16le \ |
| 93 | -af loudnorm \ |
| 94 | "$SALIDA" 2>&1 \ |
| 95 | | grep -E "(size=|bitrate=|Output)" \ |
| 96 | | sed 's/^/ /' |
| 97 | |
| 98 | echo "" |
| 99 | echo -e " ${VERDE}✅ Listo:${NC} $SALIDA ($(du -h "$SALIDA" | cut -f1))" |
| 100 | echo "" |
| 101 | echo -e " ${CYAN}📋 Resumen:${NC}" |
| 102 | echo " Frecuencia: 16 kHz Canales: Mono Formato: PCM s16le" |
| 103 | echo " Volumen: Normalizado (loudnorm, -23 LUFS)" |
| 104 | echo " Listo para: whisper transcribe" |