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

95 lines
3.4 KiB
Python

"""Elegir de una lista por voz, y buscar dentro de un fichero (Ctrl+F).
La capa que le faltaba a las acciones de fichero. Buscar y soltar el primer
resultado no sirve cuando hay varios: hace falta enseñar la lista y poder decir
"abre el tercero". Y para buscar DENTRO de un fichero como con Ctrl+F, hace
falta recordar cual era.
Aqui va la logica pura —interpretar "el tercero", detectar las ordenes de
seleccion y de busqueda interna—; el estado (la ultima lista, el ultimo
fichero) y la ejecucion viven en el panel.
"""
import re
# Como se dice cada posicion. Se aceptan el ordinal, el numero en palabra y la
# cifra, porque el reconocedor devuelve cualquiera de los tres.
ORDINALES = {
1: ["primero", "primera", "uno", "1"],
2: ["segundo", "segunda", "dos", "2"],
3: ["tercero", "tercera", "tres", "3"],
4: ["cuarto", "cuarta", "cuatro", "4"],
5: ["quinto", "quinta", "cinco", "5"],
6: ["sexto", "sexta", "seis", "6"],
7: ["septimo", "septima", "siete", "7"],
8: ["octavo", "octava", "ocho", "8"],
9: ["noveno", "novena", "nueve", "9"],
10: ["decimo", "decima", "diez", "10"],
}
_PALABRA_A_N = {p: n for n, ps in ORDINALES.items() for p in ps}
# "el ultimo" es comodo y no es un numero
_ULTIMO = {"ultimo", "ultima", "final"}
def posicion(texto: str):
"""Devuelve el numero (1..N) que se ha dicho, -1 para 'el ultimo', o None.
Se queda con la PRIMERA posicion mencionada, para que "abre el tercero"
de 3 y no se lie con otros numeros que puedan venir detras.
"""
for palabra in texto.split():
if palabra in _ULTIMO:
return -1
if palabra in _PALABRA_A_N:
return _PALABRA_A_N[palabra]
return None
# Ordenes que actuan sobre la lista ya mostrada. El verbo dice que hacer con lo
# elegido; la posicion, cual.
_ABRIR = re.compile(r"\b(abre|abreme|abrir)\b")
_LEER = re.compile(r"\b(lee|leeme|leer|muestrame|ensename)\b")
_SELECCION = re.compile(r"\b(el|la|numero|opcion|resultado)\b")
def orden_de_seleccion(texto: str):
"""(accion, posicion) si el texto elige de la lista, o None.
accion: "abrir" o "leer". posicion: 1..N o -1 (ultimo).
Ejemplos: "abre el tercero", "lee el numero dos", "el ultimo", "abre el 3".
"""
pos = posicion(texto)
if pos is None:
return None
# tiene que sonar a seleccion, no a otra cosa que lleve un numero
if not (_SELECCION.search(texto) or _ABRIR.search(texto) or _LEER.search(texto)):
return None
if _LEER.search(texto):
return ("leer", pos)
# por defecto, elegir es abrir
return ("abrir", pos)
# Ctrl+F: buscar un termino dentro del ultimo fichero abierto.
_DENTRO = re.compile(
r"\b(busca|buscame|encuentra)\b.*\b(dentro|en el fichero|en el archivo|en el texto)\b")
def orden_buscar_dentro(texto: str):
"""El termino a buscar dentro del fichero, o None.
"busca dentro configuracion", "busca en el texto la palabra error".
Se queda con lo que va despues de 'dentro'/'en el ...'.
"""
if not _DENTRO.search(texto):
return None
# cortar por el separador y quedarse con el resto
for sep in ("dentro de", "dentro", "en el fichero", "en el archivo", "en el texto",
"la palabra"):
if sep in texto:
resto = texto.split(sep, 1)[1].strip()
resto = re.sub(r"^(la palabra|el termino|de|la|el|los|las)\s+",
"", resto).strip()
if resto:
return resto
return None