JARVIS/nucleo/manos/fonetica.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

84 lines
3.1 KiB
Python

"""Comparar por como SUENA, no por como se escribe.
## Que problema resuelve
Midiendo donde se pierde el acierto salio esto: de 150 ordenes dichas, el 79 %
llega a su accion, un 5 % se oye mal de verdad, y un **15 % se oye bien pero el
emparejador no lo relaciona**. Mirando esos casos uno a uno, no son ordenes
distintas: son la misma orden mal escrita.
dijo "receta de documentos" oyo "Leceta de documentos"
dijo "como van los pubs" oyo "¿Como van los poops?"
dijo "abre bandcamp" oyo "Abre van Kamp"
dijo "abre el drive" oyo "Abril Drive"
dijo "abreme oasis" oyo "Abre mi roasis"
Eso NO se arregla con embeddings: un modelo semantico ve "Leceta" como una
palabra inexistente, no como "receta". Y bajar el umbral de parecido tampoco,
porque el umbral esta alto por una razon —hay una cicatriz en el codigo de
cuando "que carne el tiempo" se colaba en la accion del tiempo y ejecutaba lo
que no era—.
Lo que sirve es comparar como suena. En castellano hay unas pocas confusiones
que lo explican casi todo, y son las que aplica `fonetica()`.
## Lo que NO hace
No es un Soundex ni un Metaphone: esos comprimen tanto que juntan palabras que
no se parecen, y aqui juntar de mas significa ejecutar la orden equivocada, que
es lo peor que puede hacer esto. Se queda en las equivalencias que de verdad
confunde el reconocedor de voz en castellano, y nada mas.
"""
import re
import unicodedata
# El orden importa: cada regla se aplica sobre el resultado de la anterior.
REGLAS = (
# la hache no suena
(r"h", ""),
# b y v son el mismo sonido: "van Kamp" / "bandcamp"
(r"v", "b"),
# ge/gi suenan como jota
(r"g([ei])", r"j\1"),
# gue/gui: la u es muda
(r"gu([ei])", r"g\1"),
# que/qui igual, y qu -> k
(r"qu([ei])", r"k\1"),
(r"q", "k"),
# ce/ci en seseo suenan como ese; ca/co/cu como ka
(r"c([ei])", r"s\1"),
(r"c", "k"),
# z siempre ese (seseo): asi "poops"/"pubs" y "haz"/"has" caen juntos
(r"z", "s"),
# x entre vocales suena ks, pero al principio suele ser ese
(r"^x", "s"),
(r"x", "ks"),
# ll e y son el mismo sonido para casi todo el mundo
(r"ll", "y"),
# w es de fuera: suena como u o como b segun la palabra; se unifica a b
(r"w", "b"),
# la erre doble y la simple no se distinguen al transcribir
(r"rr", "r"),
# la ñ se escribe de mil formas cuando el reconocedor duda
(r"ñ", "n"),
# las dobles no aportan: "poops" -> "pops"
(r"(.)\1+", r"\1"),
# las vocales finales se comen mucho; y la ese final tambien
(r"s$", ""),
)
def fonetica(texto: str) -> str:
"""Como suena, aproximadamente, para poder comparar dos oidos distintos."""
t = unicodedata.normalize("NFD", (texto or "").lower())
t = "".join(c for c in t if unicodedata.category(c) != "Mn" or c == "̃")
t = unicodedata.normalize("NFC", t)
t = re.sub(r"[^a-zñ\s]", " ", t)
palabras = []
for p in t.split():
for patron, cambio in REGLAS:
p = re.sub(patron, cambio, p)
if p:
palabras.append(p)
return " ".join(palabras)