Dernière activité 6 hours ago

Episodio 842 — FFmpeg: afina el oido de tu IA. Scripts de la pipeline de voz local (micro -> FFmpeg -> Whisper -> Ollama -> Piper -> FFmpeg -> altavoz).

filtros-limpieza-total.txt Brut
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
18highpass=f=80,
19lowpass=f=8000,
20afftdn,
21loudnorm
filtros-tts-profesional.txt Brut
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
14loudnorm,
15compand=attacks=0.1:decays=0.2:\
16points=-80/-80|-45/-45|-30/-30|-25/-20|-20/-15|\
17-15/-12|-10/-6|-6/-3|0/-0.5,
18highpass=f=80
pipeline-completo.py Brut
1#!/usr/bin/env python3
2"""
3Pipeline completo: micrófono → ffmpeg → Whisper → Ollama → TTS → ffmpeg → altavoz
4================================================================================
5
6Este 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
12Requisitos:
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
20atareao con Linux — Episodio 842
21"""
22
23import subprocess
24import tempfile
25import time
26from pathlib import Path
27from typing import Optional
28
29import requests
30
31# ── Configuración ────────────────────────────────────────────────────────
32OLLAMA_URL = "http://localhost:11434/api/generate"
33OLLAMA_MODEL = "llama3.2:latest"
34
35
36# ── Colores para terminal ────────────────────────────────────────────────
37class 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
45def 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
51def print_ok(msg: str) -> None:
52 print(f" {Color.GREEN}✅{Color.NC} {msg}")
53
54
55def print_warn(msg: str) -> None:
56 print(f" {Color.YELLOW}⚠️{Color.NC} {msg}")
57
58
59# ── Paso 1: Grabar y preparar con ffmpeg ────────────────────────────────
60def 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 ─────────────────────────────────────
141def 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 ──────────────────────────────────────────
190def 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 ─────────────────────────────────
231def 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 ─────────────────────────────────────────────────────
333def 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
419if __name__ == "__main__":
420 main()
whisperize.sh Brut
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# ==============================================================================
17set -euo pipefail
18
19# ── Colores (si terminal lo soporta) ──────────────────────────────────────
20if [ -t 1 ]; then
21 VERDE='\033[0;32m'
22 AMARILLO='\033[1;33m'
23 CYAN='\033[0;36m'
24 NC='\033[0m' # Sin color
25else
26 VERDE=''; AMARILLO=''; CYAN=''; NC=''
27fi
28
29# ── Ayuda ─────────────────────────────────────────────────────────────────
30usage() {
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 ────────────────────────────────────────────────────
48if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
49 usage
50fi
51
52ENTRADA="$1"
53SALIDA="${2:-${ENTRADA%.*}_whisper.wav}"
54
55# ── Validar entrada ───────────────────────────────────────────────────────
56if [ ! -f "$ENTRADA" ]; then
57 echo -e "${AMARILLO}❌ Error: No se encuentra el archivo '${ENTRADA}'${NC}" >&2
58 exit 1
59fi
60
61if ! 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
65fi
66
67# ── Mostrar info del archivo original ─────────────────────────────────────
68echo -e "${CYAN}🎤 whisperize — Preparando audio para Whisper${NC}"
69echo ""
70ORIG_SIZE=$(stat -c%s "$ENTRADA" 2>/dev/null || echo "?")
71echo -e " ${VERDE}Entrada:${NC} $ENTRADA ($([ -f "$ENTRADA" ] && echo "$(du -h "$ENTRADA" | cut -f1)" || echo "?"))"
72echo -e " ${VERDE}Salida:${NC} $SALIDA"
73echo ""
74
75# Mostrar info del audio original (si ffprobe está disponible)
76if 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 ""
83fi
84
85# ── Convertir ─────────────────────────────────────────────────────────────
86echo -e " ${VERDE}Procesando...${NC}"
87
88ffmpeg -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
98echo ""
99echo -e " ${VERDE}✅ Listo:${NC} $SALIDA ($(du -h "$SALIDA" | cut -f1))"
100echo ""
101echo -e " ${CYAN}📋 Resumen:${NC}"
102echo " Frecuencia: 16 kHz Canales: Mono Formato: PCM s16le"
103echo " Volumen: Normalizado (loudnorm, -23 LUFS)"
104echo " Listo para: whisper transcribe"