JARVIS/voz/analiza_voz.py
sito 8e4bc8ad94 JARVIS: asistente de voz local para Linux
Nucleo propio: oye con whisper.cpp, piensa con un modelo de Ollama, habla
con Piper, y hace RAG sobre los apuntes del usuario. 100% local, sin
cuentas ni claves.

Escrito bajo una restriccion dura, 4 GB de VRAM: el cerebro y whisper
comparten tarjeta y solo caben porque estan dimensionados para ello. El
RAG usa embeddings estaticos con busqueda hibrida; la voz clonada se sirve
de una cache de frases.

Incluye instalador (install.sh), requisitos, y documentacion del stack,
del manejo de root y de las acciones. Los apuntes indexados y el diario NO
se incluyen: son privados y el .gitignore los bloquea.
2026-08-16 15:34:37 +02:00

80 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""Compara el original y el clon en lo que el oido nota, no solo en timbre.
La puntuacion de parecido que se venia usando es el coseno entre embeddings de
hablante: mide TIMBRE. Es ciega al tono y al ritmo, y una voz puede tener el
timbre correcto y aun asi "no parecerse nada" porque habla mas rapido, mas
agudo o mas plano que el original. Esto mide eso.
- tono medio y rango (F0): si el clon habla medio tono por encima, se nota
muchisimo mas que una diferencia de timbre
- variacion del tono: un original expresivo contra un clon monotono suena a
otra persona aunque el timbre coincida
- velocidad: silabas por segundo, aproximada por picos de energia
- brillo: centroide espectral, delata si el clon suena mas apagado
python analiza_voz.py fichero.wav [fichero2.wav ...]
"""
import sys
import numpy as np
def carga(ruta, sr=22050):
import librosa
y, _ = librosa.load(ruta, sr=sr, mono=True)
return y, sr
def analiza(ruta):
import librosa
y, sr = carga(ruta)
# F0 con pyin, que es lento pero fiable en voz
f0, sonoro, _ = librosa.pyin(
y, sr=sr, fmin=50, fmax=400, frame_length=2048)
f0v = f0[~np.isnan(f0)]
# velocidad: picos del envelope de energia ~ silabas
env = librosa.onset.onset_strength(y=y, sr=sr)
picos = librosa.util.peak_pick(env, pre_max=3, post_max=3, pre_avg=3,
post_avg=5, delta=0.2, wait=4)
dur = len(y) / sr
silabas_s = len(picos) / dur if dur else 0
# brillo
cent = float(np.mean(librosa.feature.spectral_centroid(y=y, sr=sr)))
# proporcion de voz sonora: cuanto del audio es habla con tono
prop = float(np.mean(sonoro)) if sonoro is not None else 0.0
return {
"dur": dur,
"f0_medio": float(np.median(f0v)) if len(f0v) else 0.0,
"f0_p10": float(np.percentile(f0v, 10)) if len(f0v) else 0.0,
"f0_p90": float(np.percentile(f0v, 90)) if len(f0v) else 0.0,
"f0_desv": float(np.std(f0v)) if len(f0v) else 0.0,
"silabas_s": silabas_s,
"brillo": cent,
"sonoro": prop,
}
def main():
print(f"{'fichero':34s} {'dur':>6s} {'tono':>7s} {'rango':>11s} "
f"{'varia':>7s} {'sil/s':>6s} {'brillo':>7s}")
print("-" * 84)
for ruta in sys.argv[1:]:
try:
a = analiza(ruta)
except Exception as e:
print(f"{ruta[:34]:34s} error: {e}")
continue
nombre = ruta.split("/")[-1][:34]
print(f"{nombre:34s} {a['dur']:5.1f}s {a['f0_medio']:6.1f}Hz "
f"{a['f0_p10']:4.0f}-{a['f0_p90']:<4.0f}Hz {a['f0_desv']:6.1f} "
f"{a['silabas_s']:6.2f} {a['brillo']:6.0f}Hz")
if __name__ == "__main__":
main()