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.
84 lines
3.1 KiB
Python
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)
|