Последняя активность 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).

atareao's Avatar atareao ревизий этого фрагмента 6 hours ago. К ревизии

4 files changed, 563 insertions

filtros-limpieza-total.txt(файл создан)

@@ -0,0 +1,21 @@
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(файл создан)

@@ -0,0 +1,18 @@
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(файл создан)

@@ -0,0 +1,420 @@
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(файл создан)

@@ -0,0 +1,104 @@
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"
Новее Позже