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.
This commit is contained in:
commit
8e4bc8ad94
125 changed files with 25033 additions and 0 deletions
84
nucleo/manos/fonetica.py
Normal file
84
nucleo/manos/fonetica.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue