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:
sito 2026-08-16 15:28:31 +02:00
commit 8e4bc8ad94
125 changed files with 25033 additions and 0 deletions

468
nucleo/datos/_generar.py Normal file
View file

@ -0,0 +1,468 @@
#!/usr/bin/env python3
"""Genera acciones.json.
Se escribe aqui y no a mano en el JSON porque son mas de cien entradas con la
misma forma: en tabla se leen de un tirón y se ve si falta un acuse o si dos
frases chocan. El JSON es el producto; este fichero es la fuente.
python3 _generar.py && python3 -m json.tool acciones.json >/dev/null
Columnas: id | frases (| separadas) | acuse | comando | respuesta
Reglas que conviene no romper:
- Las frases van sin tildes ni signos: el motor normaliza lo que oye, pero
aqui se comparan tal cual.
- Frases cortas y genericas ("abre") arrastran todo lo demas; se prefieren
de tres palabras o mas, que es como se habla de verdad.
- Todo lo que borre, mate o apague lleva confirmar=True.
- {argumento} solo en las que declaran captura.
"""
import json
import pathlib
# --------------------------------------------------------------- sistema
SISTEMA = [
("disco_libre", "cuanto espacio queda|espacio en disco|espacio libre en disco|cuanto disco queda",
"Un momento, lo miro.", "df -h / | tail -1",
"Quedan {campo4} libres de {campo2}, al {campo5} de uso."),
("disco_home", "como esta mi disco|cuanto ocupa mi disco|espacio de mi disco",
"Un momento.", "df -h ~ | tail -1",
"Su disco esta al {campo5}, con {campo4} libres de {campo2}."),
("disco_todos", "discos montados|particiones|unidades montadas",
"Enseguida.", "df -h --output=target,size,pcent -x tmpfs -x devtmpfs | tail -n +2",
"Hay {lineas} unidades montadas. {salida}"),
("memoria", "cuanta memoria|memoria libre|cuanta ram|ram libre",
"Voy.", "free -g | awk '/Mem:/{print $2, $3, $7}'",
"De {campo1} gigas hay {campo2} en uso y {campo3} disponibles."),
("swap", "como esta la swap|memoria de intercambio",
"Voy.", "free -g | awk '/Swap:/{print $2, $3}'",
"Swap: {campo2} gigas usados de {campo1}."),
("cpu_carga", "carga del sistema|como va el procesador|carga de cpu",
"Un segundo.", "uptime | sed 's/.*average: //'",
"La carga media es {ultima}."),
("cpu_modelo", "que procesador tengo|modelo de procesador|que cpu tengo",
"Enseguida.", "lscpu | grep 'Model name' | cut -d: -f2 | xargs",
"Lleva un {ultima}."),
("cpu_nucleos", "cuantos nucleos tengo|cuantos hilos tengo",
"Voy.", "nproc", "Tiene {ultima} hilos disponibles."),
("temperatura", "que temperatura hay|como esta de temperatura|se esta calentando",
"Lo compruebo.", "for z in /sys/class/thermal/thermal_zone*/temp; do cat $z; done | sort -rn | head -1 | awk '{printf \"%.0f\", $1/1000}'",
"El punto mas caliente esta a {ultima} grados."),
("gpu_estado", "como esta la grafica|estado de la gpu|uso de la grafica",
"Un momento.",
"nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total,temperature.gpu --format=csv,noheader",
"La grafica esta asi: {ultima}."),
("uptime", "cuanto lleva encendido|desde cuando esta encendido|tiempo encendido",
"Voy.", "uptime -p | sed 's/^up //; s/ days\\?/ dias/; s/ hours\\?/ horas/; s/ minutes\\?/ minutos/; s/,\\([^,]*\\)$/ y\\1/'",
"Lleva encendido {ultima}."),
("bateria", "cuanta bateria queda|como esta la bateria|nivel de bateria",
"Lo miro.", "cat /sys/class/power_supply/BAT*/capacity 2>/dev/null | head -1",
"La bateria esta al {ultima} por ciento."),
("kernel", "que kernel tengo|version del kernel|que nucleo tengo",
"Enseguida.", "uname -r", "Kernel {ultima}."),
("distro", "que distribucion tengo|que sistema tengo|que linux tengo",
"Voy.", "grep PRETTY_NAME /etc/os-release | cut -d'\"' -f2",
"Esta usando {ultima}."),
("fecha", "que hora es|que dia es|dime la fecha",
"", "date '+%H:%M del %A %d de %B'", "Son las {salida}."),
("procesos_top", "que esta consumiendo|que consume mas|procesos que mas gastan|quien se come la cpu",
"Lo compruebo.", "ps -eo pcpu,comm --sort=-pcpu --no-headers | head -5 | awk '{c=$1; $1=\"\"; sub(/^ +/,\"\"); printf \"%s al %s por ciento\\n\", $0, c}'",
"Lo que mas consume es {primera}."),
("procesos_memoria", "que consume mas memoria|quien se come la ram",
"Lo compruebo.", "ps -eo comm,pmem --sort=-pmem --no-headers | head -5",
"Por memoria: {salida}"),
("procesos_cuantos", "cuantos procesos hay|cuantos procesos corren",
"Voy.", "ps -e --no-headers | wc -l", "Hay {ultima} procesos en marcha."),
("red_ip", "cual es mi ip|mi ip local|que ip tengo",
"Un momento.", "hostname -I | awk '{print $1}'", "Su IP local es {ultima}."),
("red_ip_publica", "cual es mi ip publica|ip externa",
"Salgo a mirarlo.", "curl -s --max-time 6 ifconfig.me", "Su IP publica es {ultima}."),
("red_wifi", "a que wifi estoy conectado|red wifi|como se llama la wifi",
"Voy.", "nmcli -t -f NAME,TYPE connection show --active | grep wireless | cut -d: -f1",
"Conectado a {ultima}."),
("red_velocidad", "cuanto trafico de red|trafico de red",
"Lo miro.", "cat /proc/net/dev | awk '/wlp|enp/{rx+=$2; tx+=$10} END{printf \"%.1f %.1f\", rx/1e9, tx/1e9}'",
"Desde el arranque: {campo1} gigas bajados y {campo2} subidos."),
("red_ping", "hay internet|tengo conexion|funciona internet",
"Lo compruebo.",
"ping -c1 -W2 1.1.1.1 >/dev/null 2>&1 && echo si || echo no",
"Conexion: {ultima}."),
("puertos", "que puertos hay abiertos|puertos a la escucha|quien esta escuchando",
"Un momento.", "ss -ltnp 2>/dev/null | awk 'NR>1{print $4}' | sort -u | head -8",
"A la escucha: {salida}"),
("servicios_fallidos", "hay servicios caidos|servicios con fallos|algo ha fallado",
"Lo reviso.", "systemctl --failed --no-legend | wc -l",
"Servicios en fallo: {ultima}."),
("actualizaciones", "hay actualizaciones|paquetes por actualizar",
"Lo compruebo, tarda un poco.",
"apt list --upgradable 2>/dev/null | tail -n +2 | wc -l",
"Hay {ultima} paquetes por actualizar."),
("usuarios", "quien esta conectado|hay alguien conectado",
"Voy.", "who | wc -l", "Hay {ultima} sesiones abiertas."),
("gpu_procesos", "que usa la grafica|quien usa la gpu",
"Lo miro.", "O=$(nvidia-smi --query-compute-apps=process_name,used_gpu_memory --format=csv,noheader | awk -F, '{n=split($1,p,\"/\"); printf \"%s%s\\n\", p[n], $2}'); echo \"${O:-nada, la grafica esta libre}\"",
"En la grafica: {primera}"),
("audio_salida", "por donde suena|salida de audio|que altavoz esta puesto",
"Voy.", "pactl get-default-sink 2>/dev/null", "La salida es {ultima}."),
("audio_micro", "que microfono hay puesto|entrada de audio",
"Voy.", "pactl get-default-source 2>/dev/null", "La entrada es {ultima}."),
("pantallas", "cuantas pantallas hay|monitores conectados",
"Voy.", "n=$(xrandr --listmonitors 2>/dev/null | head -1 | awk '{print $2}'); xrandr --listmonitors 2>/dev/null | tail -n +2 | awk '{print $NF}' | paste -sd', '; echo \"Tiene $n pantalla$([ \"$n\" = 1 ] || echo s) conectada$([ \"$n\" = 1 ] || echo s)\"",
"{ultima}"),
("dispositivos_usb", "que hay conectado por usb|dispositivos usb",
"Un momento.", "lsusb | cut -d' ' -f7- | head -6", "Por USB: {salida}"),
("bluetooth", "que hay emparejado por bluetooth|dispositivos bluetooth",
"Voy.", "bluetoothctl devices 2>/dev/null | cut -d' ' -f3- | head -5",
"Emparejados: {salida}"),
("flatpaks", "que flatpaks tengo|aplicaciones flatpak",
"Voy.", "flatpak list --app --columns=name | head -10", "Instalados: {salida}"),
("docker", "hay contenedores corriendo|contenedores docker",
"Lo miro.", "O=$(docker ps --format '{{.Names}}' 2>/dev/null | head -5); echo \"${O:-ninguno en marcha}\"",
"Contenedores: {salida}"),
("git_estado", "como esta el repositorio|estado de git",
"Voy.", "cd ~/COFRE/CODERS/JARVIS && git status --short | wc -l",
"Hay {ultima} ficheros con cambios sin guardar."),
("ollama_estado", "esta ollama funcionando|como esta el modelo|que modelo hay cargado",
"Lo compruebo.",
"M=$(curl -s --max-time 3 http://localhost:11434/api/ps | grep -o '\"name\":\"[^\"]*' | cut -d'\"' -f4 | head -1); echo \"${M:-ninguno ahora mismo}\"",
"Modelo cargado: {ultima}."),
("apagar_pantalla", "apaga la pantalla|apagar pantalla",
"Enseguida.", "xset dpms force off", "Pantalla apagada."),
# --- administracion: necesitan root, disparan el dialogo de contraseña ---
# (Las de pentesting van en su propia lista, PENTEST, mas abajo.)
("actualizar_lista", "actualiza los repositorios|refresca los paquetes|actualiza la lista de paquetes",
"Voy, necesito permiso de administrador.", "sudo apt update",
"Repositorios actualizados, señor.", True),
("espacio_liberar", "limpia los paquetes viejos|libera espacio de apt|limpia el cache de paquetes",
"Necesito permiso para esto.", "sudo apt autoclean",
"Cache de paquetes limpiada, señor.", True),
("servicio_reiniciar", "reinicia el servicio de red|reinicia la red",
"Necesito permiso de administrador.", "sudo systemctl restart NetworkManager",
"Red reiniciada, señor.", True),
("fecha_sincronizar", "sincroniza la hora|pon la hora en hora",
"Un momento, con permiso.", "sudo systemctl restart systemd-timesyncd",
"Hora sincronizada, señor.", True),
("bloquear", "bloquea la sesion|bloquear pantalla|echa el cerrojo",
"Ahora mismo.", "loginctl lock-session", "Sesion bloqueada."),
("volumen_subir", "sube el volumen|mas volumen",
"", "pactl set-sink-volume @DEFAULT_SINK@ +10%", "Volumen subido."),
("volumen_bajar", "baja el volumen|menos volumen",
"", "pactl set-sink-volume @DEFAULT_SINK@ -10%", "Volumen bajado."),
("volumen_silencio", "silencia el sistema|quita el sonido|mutea el sistema",
"", "pactl set-sink-mute @DEFAULT_SINK@ toggle", "Sonido conmutado."),
("brillo_subir", "sube el brillo|mas brillo",
"", "brightnessctl set +10% 2>/dev/null || xrandr --output $(xrandr | awk '/ connected/{print $1; exit}') --brightness 1",
"Brillo subido."),
("captura_pantalla", "hazme una captura|captura de pantalla|haz una foto de la pantalla",
"Enseguida se la hago.", "D=$(xdg-user-dir PICTURES 2>/dev/null || echo ~); F=$D/captura-$(date +%H%M%S).png; import -window root \"$F\" 2>/dev/null || gnome-screenshot -f \"$F\" 2>/dev/null || ffmpeg -loglevel error -f x11grab -video_size $(xdpyinfo | awk \"/dimensions/{print \\$2}\") -i $DISPLAY -frames:v 1 -y \"$F\" 2>/dev/null; [ -s \"$F\" ] && echo \"$F\"",
"Captura guardada."),
("papelera_tamano", "cuanto ocupa la papelera|papelera llena",
"Voy.", "du -sh ~/.local/share/Trash 2>/dev/null | cut -f1",
"La papelera ocupa {ultima}."),
("descargas_listar", "que hay en descargas|ultimas descargas|que me he bajado",
"Voy.", "ls -t ~/Downloads 2>/dev/null | head -6", "Hay {lineas} cosas en descargas"),
("descargas_abrir", "abre la carpeta de descargas|abreme descargas",
"Enseguida se la abro.", "xdg-open ~/Downloads", "Carpeta de descargas abierta."),
("documentos_abrir", "abre mis documentos|abreme documentos",
"Enseguida.", "xdg-open ~/Documents", "Documentos abierto."),
("cofre_abrir", "abre el cofre|abreme el cofre",
"Enseguida.", "xdg-open ~/COFRE", "COFRE abierto."),
("ficheros_recientes", "que he tocado ultimamente|ficheros recientes|en que estaba trabajando",
"Lo miro.",
"find ~ -maxdepth 3 -type f -mmin -180 -not -path '*/.*' 2>/dev/null | head -6",
"Tocado en las ultimas horas: {salida}"),
("grandes", "que ocupa mas sitio|que tengo en descargas|que me esta llenando la carpeta",
"Lo miro.",
"du -ah ~/Downloads ~/Descargas ~/Documents ~/Desktop 2>/dev/null | sort -rh | head -6",
"Lo mas grande: {salida}"),
]
# ------------------------------------------------------- con argumento
CAPTURA = [
("buscar_fichero", "busca el fichero|buscame el fichero|encuentra el fichero",
"Lo busco.", "timeout 12 find $HOME/Documents $HOME/Desktop $HOME/COFRE/CODERS $HOME/Downloads -iname '*{argumento}*' -not -path '*/.*' -not -path '*/node_modules/*' -not -path '*venv*' 2>/dev/null | head -12",
"He encontrado {lineas}"),
("buscar_carpeta", "busca la carpeta|encuentra la carpeta",
"La busco.", "find ~ -type d -iname '*{argumento}*' -not -path '*/.*' 2>/dev/null | head -12",
"Carpetas encontradas: {salida}"),
# busca un TEXTO dentro de los ficheros, recursivo, con ruta y linea
("grep_recursivo", "busca el texto|busca la palabra en mis ficheros|donde pone|que fichero contiene",
"Lo busco en sus ficheros.", "timeout 12 grep -rIn --exclude-dir='.*' --exclude-dir='*venv*' --exclude-dir=node_modules --exclude-dir=__pycache__ --exclude-dir=modelos --exclude-dir=muestras -m1 {argumento} $HOME/Documents $HOME/Desktop $HOME/COFRE/CODERS 2>/dev/null | head -12",
"{lineas} ficheros lo contienen."),
("leer_fichero", "leeme el fichero|abre y leeme|lee el fichero",
"Voy a leerlo.", "f=$(timeout 12 find $HOME/Documents $HOME/Desktop $HOME/COFRE/CODERS -iname '*{argumento}*' -type f -not -path '*/.*' 2>/dev/null | head -1); [ -n \"$f\" ] && head -c 800 \"$f\" || echo \"no lo encuentro\"",
"{salida}, señor"),
("abrir_fichero", "abre el fichero|abreme el fichero",
"Enseguida se lo abro.",
"f=$(find ~ -iname '*{argumento}*' -type f -not -path '*/.*' 2>/dev/null | head -1); "
"[ -n \"$f\" ] && xdg-open \"$f\" && echo \"$f\" || echo 'no lo encuentro'",
"Abierto {ultima}."),
("abrir_carpeta", "abre la carpeta|abreme la carpeta",
"Enseguida.", "a={argumento}; if [ -d \"$a\" ]; then d=\"$a\"; else d=$(timeout 12 find $HOME/Documents $HOME/Desktop $HOME/COFRE $HOME/Downloads -type d -iname \"*$a*\" -not -path \"*/.*\" -not -path \"*/node_modules/*\" 2>/dev/null | head -1); fi; if [ -n \"$d\" ]; then xdg-open \"$d\" >/dev/null 2>&1 & echo \"Abierta $(basename \"$d\")\"; else echo \"No encuentro esa carpeta\"; fi",
"{ultima}"),
("tamano_carpeta", "cuanto ocupa la carpeta|tamaño de la carpeta",
"Lo calculo.",
"a={argumento}; if [ -d \"$a\" ]; then d=\"$a\"; else d=$(timeout 12 find $HOME/Documents $HOME/Desktop $HOME/COFRE $HOME/Downloads -type d -iname \"*$a*\" -not -path \"*/.*\" 2>/dev/null | head -1); fi; [ -n \"$d\" ] && du -sh \"$d\" 2>/dev/null | cut -f1 || echo \"no la encuentro\"",
"Ocupa {ultima}."),
("proceso_buscar", "esta corriendo|se esta ejecutando|hay algun proceso de",
"Lo compruebo.", "pgrep -c -f {argumento} 2>/dev/null || echo 0",
"Hay {ultima} procesos que encajan."),
("proceso_matar", "mata el proceso|cierra el proceso|termina el proceso",
"Antes de matarlo, confirmeme.", "pkill -f {argumento}",
"Proceso terminado.", True),
("instalado", "tengo instalado|esta instalado",
"Lo miro.", "command -v {argumento} >/dev/null && echo si || echo no",
"Instalado: {ultima}."),
("manual", "para que sirve el comando|que hace el comando",
"Voy.", "whatis {argumento} 2>/dev/null | head -1", "{salida}"),
]
# --------------------------------------------------------------- oasis
# --------------------------------------------------------------- pentesting
# Red de seguridad: los escaneos ACTIVOS (que mandan paquetes a otros) llevan
# confirmar=True, asi que el dialogo muestra el comando exacto antes de lanzarse
# y la voz no puede disparar un escaneo sola. Los que apuntan a un objetivo se
# limitan a la RED LOCAL por construccion (autodetectan la subred o exigen IP
# privada); un objetivo de fuera se rechaza. Lo de solo lectura —buscar un
# exploit en la base local, resolver un dominio— va directo, no toca a nadie.
#
# Que apunte solo a la red local es a proposito: un asistente por voz que
# escanea lo que crea haber entendido, con salida al host, es un incidente
# esperando. Para objetivos de fuera, mejor a mano y con intencion.
PENTEST = [
("red_dispositivos", "escanea mi red|que hay en mi red|dispositivos en la red|quien esta en la red",
"Voy a escanear la red local, necesito su permiso.",
"R=$(ip route | awk '/proto.*src/{for(i=1;i<=NF;i++)if($i~/\\//)print $i; exit}'); "
"sudo nmap -sn ${R:-192.168.1.0/24} | grep -E 'Nmap scan|MAC' | head -30",
"Estos equipos hay en la red, señor: {salida}", True),
("puertos_host", "que puertos tiene abiertos|escanea los puertos de|escanea el host",
"Un escaneo de puertos, con su permiso.",
"T={argumento}; echo \"$T\" | grep -qE '^(10\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.|192\\.168\\.|127\\.|localhost)' "
"&& nmap -F \"$T\" | grep -E '^[0-9]+/|Nmap scan' | head -20 "
"|| echo 'Solo objetivos de la red local, señor.'",
"{salida}", True),
("mis_puertos", "que puertos tengo abiertos yo|mis puertos abiertos|que estoy exponiendo",
"Lo compruebo.",
"ss -ltnp 2>/dev/null | awk 'NR>1{print $4}' | sort -u | head -15",
"Tengo {lineas} puertos a la escucha"),
("exploit_buscar", "busca un exploit para|busca exploits de|hay algun exploit de",
"Lo busco en la base de exploits.",
"searchsploit {argumento} 2>/dev/null | grep -vE '^-|Shellcode' | head -12",
"Esto he encontrado, señor: {salida}"),
("dominio_resolver", "resuelve el dominio|que ip tiene el dominio|a que ip apunta",
"Lo resuelvo.", "dig +short {argumento} 2>/dev/null | head -5",
"Resuelve a: {salida}"),
("dominio_whois", "de quien es el dominio|quien registro|whois de",
"Lo consulto.", "whois {argumento} 2>/dev/null | grep -iE 'registrar:|creation|expir|org' | head -6",
"{salida}"),
("cabeceras_web", "que servidor usa|cabeceras de|que tecnologia usa la web",
"Miro las cabeceras.",
"curl -sI --max-time 8 {argumento_url} 2>/dev/null | grep -iE 'server:|x-powered|location' | head -6",
"{salida}"),
("hash_identificar", "que tipo de hash es|identifica el hash",
"Lo miro.",
"H={argumento}; L=${#H}; case $L in 32)echo 'MD5, probablemente';; 40)echo 'SHA1';; "
"64)echo 'SHA256';; *)echo \"$L caracteres, no lo tengo claro\";; esac",
"{ultima}, señor."),
]
# las con argumento del bloque PENTEST (para marcar captura=True)
PENTEST_CAPTURA = {"puertos_host", "exploit_buscar", "dominio_resolver",
"dominio_whois", "cabeceras_web", "hash_identificar"}
OASIS = [
("oasis_corriendo", "esta oasis funcionando|esta corriendo oasis|oasis esta levantado",
"Lo compruebo.",
"pgrep -c -f 'oasis|ssb-server|sbot' 2>/dev/null || echo 0",
"Procesos de Oasis en marcha: {ultima}."),
("oasis_sincronizando", "estan sincronizando los pubs|como van los pubs|se estan sincronizando los pubs",
"Voy a mirar las conexiones.",
"python3 -c \"import json,time; d=json.load(open('$HOME/.ssb/conn.json')); "
"act=sum(1 for v in d.values() if time.time()*1000-v.get('stateChange',0)<300000); "
"fail=sum(1 for v in d.values() if v.get('failure',0)>0); "
"print(f'{len(d)} pubs, {act} activos hace poco, {fail} con fallos')\"",
"{ultima}."),
("oasis_conexiones", "cuantos pubs hay conectados|conexiones de oasis",
"Un momento.",
"cat ~/.ssb/conn.json 2>/dev/null | grep -c 'connected' || echo 0",
"Pubs conectados: {ultima}."),
("oasis_tamano", "cuanto ocupa oasis|tamaño de la base de ssb",
"Lo calculo.", "du -sh ~/.ssb 2>/dev/null | cut -f1",
"La base de SSB ocupa {ultima}."),
("oasis_abrir", "abre oasis|abreme oasis",
"Enseguida se lo abro.",
"xdg-open http://localhost:3000 2>/dev/null && echo abierto || echo 'no responde'",
"Oasis: {ultima}."),
("oasis_blobs", "cuanto ocupan los blobs|tamaño de los blobs",
"Voy.", "du -sh ~/.ssb/blobs 2>/dev/null | cut -f1", "Los blobs ocupan {ultima}."),
]
# Alias foneticos: lo que Whisper devuelve DE VERDAD, no lo que uno escribe.
# Medido con ocho ordenes habladas: "abre el correo" se transcribe "a ver el
# correo" (en castellano suenan igual), "los pubs" sale "los push" o "los
# puos", y "youtube" se parte en "yo tube". Añadirlos sube los aciertos sin
# tocar el modelo ni añadir un milisegundo.
ALIAS = {
"ff_gmail": ["a ver el correo", "abrir el correo"],
"oasis_sincronizando": ["estan sincronizando los push", "estan sincronizando los puos",
"sincronizando los pubs", "como van los push"],
"oasis_corriendo": ["esta oasis funcionando", "esta corriendo casis"],
"ff_buscar_youtube": ["busca en yo tube", "buscan youtube", "buscan yo tube",
"busca en you tube"],
"disco_libre": ["cuando espacio queda", "cuanto espacio quedan"],
"ff_youtube": ["abre yo tube", "abre you tube"],
"descargas_abrir": ["abre descargas", "abreme las descargas"],
"procesos_top": ["que esta consumiendo mas", "quien consume"],
"temperatura": ["que temperatura tiene", "como esta la temperatura"],
"bateria": ["cuanta bateria tengo", "como va la bateria"],
}
def con_señor(texto: str) -> str:
"""Mete el trato en las frases fijas.
Son las que mas suenan el acuse va en cada orden y hasta ahora eran las
unicas que no trataban de señor, asi que el personaje se caia justo ahi.
"""
if not texto or "señor" in texto.lower():
return texto
if texto.endswith("."):
return texto[:-1] + ", señor."
if texto.endswith("?"):
return texto[:-1] + ", señor?"
return texto + ", señor"
# Abrir una terminal y poner un video: de lo primero que se le pide a un
# asistente de escritorio, y no estaban. Las busquedas van acotadas y con
# tope porque el home son casi mil gigas y un find sin limite no vuelve.
ESCRITORIO = [
("terminal_abrir", "abre una terminal|abreme una terminal|abre la terminal|saca una terminal|quiero una terminal",
"Ahora mismo.", "gnome-terminal 2>/dev/null || kgx 2>/dev/null || xterm 2>/dev/null &",
"Terminal abierta."),
("terminal_aqui", "abre una terminal en el cofre|terminal en el cofre",
"Enseguida.", "gnome-terminal --working-directory=$HOME/COFRE 2>/dev/null || kgx 2>/dev/null &",
"Ahi la tiene, en el cofre."),
("videos_listar", "que peliculas tengo|que videos tengo|lista mis videos",
"Un momento, lo miro.", "timeout 12 find $HOME/Videos $HOME/Downloads $HOME/Desktop -type f -regextype posix-extended -iregex '.*\\.(mp4|mkv|avi|webm|mov|m4v)$' 2>/dev/null | head -12",
"He encontrado {lineas}."),
]
# esta captura el nombre de la pelicula
ESCRITORIO_CAPTURA = [
("pelicula_abrir", "pon la pelicula|abre la pelicula|reproduce el video|pon el video|quiero ver",
"Enseguida se la pongo.", "v=$(timeout 12 find $HOME/Videos $HOME/Downloads $HOME/Desktop -type f -regextype posix-extended -iregex '.*{argumento}.*\\.(mp4|mkv|avi|webm|mov|m4v)$' 2>/dev/null | head -1); if [ -n \"$v\" ]; then xdg-open \"$v\" >/dev/null 2>&1 & echo \"Poniendo $(basename \"$v\")\"; else echo \"No encuentro ese video\"; fi",
"{ultima}"),
]
# Ventanas y pantallas. Toda la logica vive en config/ventanas.sh (un script del
# host): aqui solo estan las frases y la llamada.
#
# Las frases llevan TRES palabras o mas y evitan a proposito ser subcadena de
# las que ya existen con "pantalla" o "ventana" dentro —apagar_pantalla,
# bloquear, captura_pantalla, ff_firefox_privado, ff_firefox_cerrar,
# proceso_matar, terminal_abrir—, porque el emparejador gana por frase mas
# larga sin mirar el grupo y una frase mal elegida se las come.
VENTANAS = [
("ventanas_listar", "que ventanas tengo abiertas|que ventanas hay abiertas|lista las ventanas",
"Un momento, lo miro.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh listar",
"Tiene {lineas} ventanas abiertas."),
("ventana_izquierda", "pon la ventana a la izquierda|manda la ventana a la izquierda|esta ventana a la izquierda",
"Ahora mismo.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh zona esta izquierda", "{ultima}"),
("ventana_derecha", "pon la ventana a la derecha|manda la ventana a la derecha|esta ventana a la derecha",
"Ahora mismo.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh zona esta derecha", "{ultima}"),
("ventana_arriba", "pon la ventana arriba del todo|manda la ventana arriba",
"Voy.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh zona esta arriba", "{ultima}"),
("ventana_abajo", "pon la ventana abajo del todo|manda la ventana abajo",
"Voy.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh zona esta abajo", "{ultima}"),
("ventana_centrar", "centra la ventana activa|pon la ventana en el centro",
"Enseguida.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh zona esta centro", "{ultima}"),
("ventana_maximizar", "maximiza la ventana activa|pon la ventana a pantalla completa",
"Ahora mismo.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh maximiza esta", "{ultima}"),
("ventana_restaurar", "restaura la ventana activa|quita la pantalla completa",
"Voy.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh restaura esta", "{ultima}"),
("ventana_minimizar", "minimiza la ventana activa|esconde la ventana activa",
"Voy.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh minimiza esta", "{ultima}"),
]
# Estas capturan lo que se dice detras: el nombre de la pantalla, el numero de
# escritorio o el mote que se le quiera poner a un monitor.
VENTANAS_CAPTURA = [
("ventana_a_pantalla", "manda la ventana a la pantalla|pasa la ventana a la pantalla",
"Ahora mismo.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh mover esta {argumento}", "{ultima}"),
("ventana_al_escritorio", "manda la ventana al escritorio|pasa la ventana al escritorio",
"Voy.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh escritorio esta {argumento}", "{ultima}"),
("pantalla_nombrar", "a esta pantalla llamala|llama a esta pantalla",
"Anotado.", "$HOME/COFRE/CODERS/JARVIS/config/ventanas.sh alias 1 {argumento}", "{ultima}"),
]
def fila(t, grupo, captura=False):
ident, frases, acuse, comando, respuesta = t[:5]
confirmar = len(t) > 5 and t[5]
return {
"id": ident,
"grupo": grupo,
"frases": frases.split("|"),
"acuse": con_señor(acuse or "Voy."),
"comando": comando,
"host": True,
"respuesta": con_señor(respuesta),
"confirmar": bool(confirmar),
"captura": captura,
}
def main():
acciones = [fila(t, "sistema") for t in SISTEMA]
acciones += [fila(t, "sistema") for t in ESCRITORIO]
acciones += [fila(t, "sistema", captura=True) for t in ESCRITORIO_CAPTURA]
acciones += [fila(t, "ventanas") for t in VENTANAS]
acciones += [fila(t, "ventanas", captura=True) for t in VENTANAS_CAPTURA]
acciones += [fila(t, "ficheros", captura=True) for t in CAPTURA]
acciones += [fila(t, "oasis") for t in OASIS]
acciones += [fila(t, "pentest", captura=t[0] in PENTEST_CAPTURA) for t in PENTEST]
# las de firefox viven en su propio fichero, que son otras cincuenta
firefox = pathlib.Path(__file__).parent / "_firefox.py"
if firefox.exists():
import importlib.util
spec = importlib.util.spec_from_file_location("firefox", firefox)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
acciones += mod.acciones()
# Los alias se aplican aqui, sobre la lista completa: si se hicieran al
# construir cada fila, las de firefox se quedarian fuera porque se generan
# en otro fichero. Se aprende por las malas.
for accion in acciones:
extra = ALIAS.get(accion["id"])
if extra:
accion["frases"] += [f for f in extra if f not in accion["frases"]]
salida = pathlib.Path(__file__).parent / "acciones.json"
salida.write_text(json.dumps({"version": 1, "acciones": acciones},
indent=2, ensure_ascii=False) + "\n")
ids = [a["id"] for a in acciones]
print(f"{len(acciones)} acciones escritas en {salida.name}")
if len(ids) != len(set(ids)):
repes = {i for i in ids if ids.count(i) > 1}
print(f"CUIDADO, ids repetidos: {repes}")
for grupo in sorted({a["grupo"] for a in acciones}):
print(f" {grupo:<10} {sum(1 for a in acciones if a['grupo'] == grupo)}")
if __name__ == "__main__":
main()