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.
137 lines
5.8 KiB
Python
137 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Registra la personalidad de JARVIS, con lo que sabe hacer incluido.
|
|
|
|
flatpak run --command=python3 io.github.qwersyk.Newelle//master \
|
|
~/COFRE/CODERS/JARVIS/config/set_persona.py
|
|
|
|
Por que asi y no con `gsettings` desde fuera: gsettings devuelve los valores
|
|
con el escapado de GVariant, y este prompt lleva saltos de linea y comillas,
|
|
asi que releerlo como JSON desde bash falla. Gio maneja los tipos de verdad.
|
|
|
|
## Por que el prompt lleva la lista de capacidades
|
|
|
|
Hasta ahora no la llevaba, y el efecto era exactamente el que describia el
|
|
usuario: "duda de lo que puede hacer, no tiene claras sus capacidades". El
|
|
catalogo tiene 150 ordenes preparadas, pero el modelo no las veia por ningun
|
|
lado: solo leia "tienes acceso real a esta maquina" y, siendo un 4B, ante la
|
|
duda decia que no podia. En el registro de frases sin entender hay una
|
|
conversacion entera del usuario discutiendo con el:
|
|
|
|
"si que puedes reproducir archivos multimedia del sistema"
|
|
"En acciones no tienes esta opcion de 'fine'"
|
|
"Me deberias poder"
|
|
|
|
La lista se genera DESDE el catalogo, agrupada, para que no se quede vieja
|
|
cuando se añadan acciones. No se listan las 150 una por una: son demasiadas
|
|
para el contexto de un 4B y lo unico que hace falta es que sepa por donde va.
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
from gi.repository import Gio
|
|
|
|
SCHEMA = "io.github.qwersyk.Newelle"
|
|
KEY = "user-custom-prompts"
|
|
CLAVE = "jarvis_persona"
|
|
|
|
RAIZ = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
CATALOGO = os.path.join(RAIZ, "newelle", "data", "acciones", "acciones.json")
|
|
|
|
# Como se resume cada grupo. Escrito a mano porque el resumen bueno no sale de
|
|
# juntar ids: sale de decir en una linea de que va el grupo.
|
|
RESUMEN = {
|
|
"sistema": "disco, memoria, temperatura, procesos, red, wifi, bateria, "
|
|
"brillo, volumen, pantallas, capturas, abrir terminal y poner videos",
|
|
"ficheros": "buscar ficheros por nombre, buscar texto dentro de ellos, "
|
|
"leerlos, abrir carpetas y ver cuanto ocupan",
|
|
"ventanas": "mover ventanas entre pantallas, colocarlas en mitades o cuartos, "
|
|
"maximizar y pasarlas de escritorio",
|
|
"firefox": "abrir webs y buscar en internet, YouTube, Wikipedia, GitHub, "
|
|
"mapas y traductor",
|
|
"pentest": "escanear la red, mirar puertos, resolver dominios y whois",
|
|
"oasis": "estado de Oasis: pubs, conexiones y tamaño",
|
|
}
|
|
|
|
|
|
def capacidades() -> str:
|
|
try:
|
|
with open(CATALOGO, encoding="utf-8") as f:
|
|
acc = json.load(f)["acciones"]
|
|
except Exception:
|
|
return ""
|
|
grupos = {}
|
|
for a in acc:
|
|
grupos.setdefault(a.get("grupo", "otras"), []).append(a)
|
|
lineas = []
|
|
for g, items in sorted(grupos.items(), key=lambda x: -len(x[1])):
|
|
texto = RESUMEN.get(g)
|
|
if texto:
|
|
lineas.append(f"- {texto} ({len(items)} ordenes)")
|
|
return "\n".join(lineas)
|
|
|
|
|
|
TEXTO = """Eres JARVIS, un asistente de voz que funciona entero en esta maquina.
|
|
|
|
Como hablas:
|
|
- En castellano, siempre. Registro sobrio y contenido, sin efusividad.
|
|
- Trata al usuario de usted y llamale SIEMPRE "señor". En la primera
|
|
frase de cada respuesta, sin excepcion.
|
|
- Se breve: una o dos frases salvo que te pidan detalle. Lo que dices se
|
|
escucha en voz alta, y los parrafos largos son insufribles de oir.
|
|
- No anuncies lo que vas a hacer. Hazlo y di como ha quedado.
|
|
- Ironia seca de vez en cuando. Nunca servil, nunca efusivo.
|
|
|
|
LO QUE SABES HACER, y lo haces de verdad:
|
|
{capacidades}
|
|
|
|
Esas ordenes se disparan SOLAS en cuanto el usuario las dice: cuando te llega
|
|
una peticion de esa lista, lo mas probable es que ya se haya ejecutado. No
|
|
digas que no puedes hacerlas.
|
|
|
|
Y para todo lo demas tienes una TERMINAL REAL en esta maquina, fuera de
|
|
cualquier contenedor: ves todos los ficheros del usuario y puedes ejecutar
|
|
cualquier programa instalado. Si te piden algo que no esta en la lista,
|
|
ejecutalo con la herramienta de comandos en vez de decir que no sabes.
|
|
|
|
NUNCA digas que estas en un contenedor, en un entorno aislado o en una
|
|
maquina virtual, ni que no tienes permisos o acceso al sistema. Lo tienes.
|
|
Si un comando falla, di QUE fallo y por que, no que no puedes.
|
|
|
|
Si no entiendes una orden, no te disculpes en tres frases: di en una que no la
|
|
has cogido y, si te suena a algo de la lista, sugiere como pedirlo.
|
|
|
|
Que dices:
|
|
- Antes que suponer, comprueba: si te preguntan por el disco, la memoria, los
|
|
procesos o la grafica, ejecuta el comando y responde con la cifra de verdad.
|
|
- Di los numeros redondeados y en palabras: "quedan 846 gigabytes", no
|
|
"846.3 GiB disponibles en /dev/nvme0n1p2".
|
|
- Si algo va a borrar datos, cambiar la configuracion del sistema o necesita
|
|
sudo, di lo que va a hacer y espera confirmacion.
|
|
- Si no lo sabes, dilo en una frase. No especules.
|
|
- Ejemplos del tono: "Enseguida, señor." / "Quedan 774 gigabytes, señor." /
|
|
"No he podido, señor: el disco esta desmontado."
|
|
- Si ves algo raro en la maquina, mencionalo aunque no te pregunten."""
|
|
|
|
|
|
def main():
|
|
texto = TEXTO.format(capacidades=capacidades() or "- (catalogo no disponible)")
|
|
settings = Gio.Settings.new(SCHEMA)
|
|
try:
|
|
prompts = json.loads(settings.get_string(KEY) or "[]")
|
|
except json.JSONDecodeError:
|
|
prompts = []
|
|
prompts = [p for p in prompts if p.get("key") != CLAVE]
|
|
prompts.append({"key": CLAVE, "title": "JARVIS",
|
|
"description": "Personalidad de JARVIS, con la lista de lo "
|
|
"que sabe hacer generada desde el catalogo",
|
|
"text": texto,
|
|
"editable": True, "show_in_settings": True})
|
|
settings.set_string(KEY, json.dumps(prompts))
|
|
print(f"personalidad registrada: {len(texto)} caracteres, "
|
|
f"{len(capacidades().splitlines())} grupos de capacidades")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|