JARVIS/config/indexar.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

156 lines
5.9 KiB
Python
Executable file

#!/usr/bin/env python3
"""Prepara el corpus de COFRE para que JARVIS lo consulte.
python3 ~/COFRE/CODERS/JARVIS/config/indexar.py # ver que entraria
python3 ~/COFRE/CODERS/JARVIS/config/indexar.py --hacerlo # copiarlo
python3 ~/COFRE/CODERS/JARVIS/config/indexar.py --hacerlo --solo-notas
Que hace: recorre las carpetas de COFRE, se queda SOLO con lo que es
conocimiento y lo copia a la carpeta de documentos de Newelle, que es de donde
lee el RAG. Despues hay que pulsar "Update Index" en los ajustes de la app.
Por que hace falta filtrar. Medido en esta maquina:
COFRE entero (las nueve carpetas) ~120 GB
de eso, ISOs, maquinas virtuales,
APKs, audio y un diccionario de
contraseñas de 9,1 GB ~100 GB
texto de menos de 100 KB 76,6 MB en 35 098 ficheros
quitando exploitdb 20,8 MB en 4 548 ficheros
exploitdb se queda fuera a proposito: son 32 000 ficheros que aplastarian el
indice y enterrarian tus notas, y ademas NO hacen falta ahi. Tienes
`searchsploit` instalado, que busca en ellos de forma exacta e instantanea. Un
indice de vectores es peor que una busqueda exacta cuando lo que buscas es el
nombre literal de un exploit.
Los nombres se aplanan conservando la ruta —PENTESTERS__OSCP__nmap.md— para que
al responder se pueda ver de donde salio cada cosa.
"""
import argparse
import hashlib
import os
import shutil
import sys
COFRE = os.path.expanduser("~/COFRE")
DESTINO = os.path.expanduser(
"~/.var/app/io.github.qwersyk.Newelle/config/models/documents")
CARPETAS = ["CODERS", "PENTESTERS", "AUTOMATAS", "DOSER", "HARD-WARERS",
"PHISHERS", "PROTECTORS", "REVERSERS", "SNEAKERS"]
EXTENSIONES = {".md", ".txt", ".rst", ".adoc"}
# Por tamaño: por encima de esto ya no es prosa, son datos. El diccionario
# kaonashi.txt son 9,1 GB de contraseñas.
TAMANO_MAXIMO = 100 * 1024
TAMANO_MINIMO = 120 # menos de esto no dice nada
# Carpetas que no aportan conocimiento y si mucho ruido
FUERA = {
"exploitdb", # 32 000 ficheros; para eso esta searchsploit
"node_modules", ".git", "venv", ".venv", "site-packages", "__pycache__",
"isos", "VirtualBox VMs", "build", "dist", ".cache", "target",
"flatpak-app", ".flatpak-builder", "rag_cache", "entrenamiento",
}
# Ficheros que son plantilla o licencia y solo ensucian
NOMBRES_FUERA = {"LICENSE.txt", "LICENSE.md", "CHANGELOG.md", "requirements.txt"}
def interesa(ruta: str, nombre: str) -> bool:
ext = os.path.splitext(nombre)[1].lower()
if ext not in EXTENSIONES or nombre in NOMBRES_FUERA:
return False
try:
tam = os.path.getsize(ruta)
except OSError:
return False
return TAMANO_MINIMO <= tam <= TAMANO_MAXIMO
def recorre(solo_notas: bool):
"""Devuelve [(origen, nombre_plano)] de todo lo que merece indexarse."""
salida = []
for carpeta in CARPETAS:
raiz = os.path.join(COFRE, carpeta)
if not os.path.isdir(raiz):
continue
for dirpath, dirnames, ficheros in os.walk(raiz):
dirnames[:] = [d for d in dirnames
if d not in FUERA and not d.startswith(".")]
for nombre in ficheros:
if solo_notas and not nombre.lower().endswith(".md"):
continue
origen = os.path.join(dirpath, nombre)
if not interesa(origen, nombre):
continue
relativa = os.path.relpath(origen, COFRE)
plano = relativa.replace(os.sep, "__")
if len(plano) > 180: # algunos nombres son kilometricos
corto = hashlib.sha256(plano.encode()).hexdigest()[:8]
plano = plano[:150] + "_" + corto + os.path.splitext(plano)[1]
salida.append((origen, plano))
return salida
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--hacerlo", action="store_true", help="copiar de verdad")
ap.add_argument("--solo-notas", action="store_true",
help="solo .md, sin los .txt sueltos")
ap.add_argument("--limpiar", action="store_true",
help="vaciar el destino antes de copiar")
args = ap.parse_args()
print("recorriendo COFRE...", flush=True)
lista = recorre(args.solo_notas)
porcarpeta, total = {}, 0
for origen, _ in lista:
carpeta = os.path.relpath(origen, COFRE).split(os.sep)[0]
tam = os.path.getsize(origen)
n, b = porcarpeta.get(carpeta, (0, 0))
porcarpeta[carpeta] = (n + 1, b + tam)
total += tam
print(f"\n{'carpeta':<14}{'ficheros':>10}{'texto':>12}")
for carpeta in CARPETAS:
if carpeta in porcarpeta:
n, b = porcarpeta[carpeta]
print(f"{carpeta:<14}{n:>10}{b/1048576:>10.1f} MB")
print(f"{'TOTAL':<14}{len(lista):>10}{total/1048576:>10.1f} MB")
print(f"\nfragmentos de 256 tokens: ~{total // 1024}")
if not args.hacerlo:
print("\nesto es solo el recuento; para copiarlo: --hacerlo")
return 0
if args.limpiar and os.path.isdir(DESTINO):
shutil.rmtree(DESTINO)
os.makedirs(DESTINO, exist_ok=True)
copiados = saltados = 0
for origen, plano in lista:
destino = os.path.join(DESTINO, plano)
try:
# solo si cambio: reindexar 4500 ficheros iguales no tiene sentido
if (os.path.exists(destino)
and os.path.getmtime(destino) >= os.path.getmtime(origen)):
saltados += 1
continue
shutil.copy2(origen, destino)
copiados += 1
except OSError as e:
print(f" no se pudo copiar {plano}: {e}")
print(f"\n{copiados} copiados, {saltados} ya estaban al dia")
print(f"en {DESTINO}")
print("\nAhora, en Newelle: Ajustes -> RAG -> Update Index")
print("La primera vez tarda; son unos 21 000 fragmentos.")
return 0
if __name__ == "__main__":
sys.exit(main())