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.
80 lines
2.7 KiB
Python
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()
|