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

101 lines
3.5 KiB
Python

#!/usr/bin/env python3
"""Construye el indice del RAG sin abrir la interfaz.
flatpak run --command=python3 io.github.qwersyk.Newelle//master \
~/COFRE/CODERS/JARVIS/config/construir_indice.py
En la app esto es el boton "Update Index" de los ajustes del RAG, que lanza un
hilo y va enseñando el progreso en una barra. Desde aqui se llama a lo mismo
pero en primer plano, para poder dejarlo corriendo en una terminal y ver si
falla, que con 20 000 fragmentos conviene.
Antes hay que haber preparado el corpus con indexar.py: este script indexa lo
que haya en la carpeta de documentos, sin filtrar nada.
"""
import os
import sys
import time
sys.path.insert(0, "/app/share/newelle")
sys.path.insert(0, os.path.expanduser(
"~/.var/app/io.github.qwersyk.Newelle/config/pip"))
from gi.repository import Gio # noqa: E402
from newelle.handlers.rag.llamaindex_handler import LlamaIndexHanlder # noqa: E402
from newelle.handlers.embeddings.model2vec import Model2VecHandler # noqa: E402
CONFIG = os.path.expanduser("~/.var/app/io.github.qwersyk.Newelle/config")
MODELOS = os.path.join(CONFIG, "models")
DOCUMENTOS = os.path.join(MODELOS, "documents")
def main():
if not os.path.isdir(DOCUMENTOS):
print(f"no existe {DOCUMENTOS}")
print("prepara antes el corpus: python3 config/indexar.py --hacerlo")
return 1
ficheros = [f for f in os.listdir(DOCUMENTOS) if not f.startswith(".")]
if not ficheros:
print("la carpeta de documentos esta vacia")
return 1
tam = sum(os.path.getsize(os.path.join(DOCUMENTOS, f))
for f in ficheros) / 1048576
print(f"{len(ficheros)} documentos · {tam:.1f} MB")
settings = Gio.Settings.new("io.github.qwersyk.Newelle")
rag = LlamaIndexHanlder(settings, MODELOS)
if not rag.is_installed():
print("faltan dependencias del RAG (llama-index, faiss, tiktoken)")
return 1
# El RAG necesita el modelo de embeddings para vectorizar, y opcionalmente
# un LLM secundario para reescribir la consulta. Aqui solo hace falta el
# primero: se indexa, no se pregunta.
rag.embedding = Model2VecHandler(settings, MODELOS)
if not rag.embedding.is_installed():
print("instalando model2vec...")
rag.embedding.install()
# create_index solo le pide load_model() al LLM —para indexar no hace falta
# generar nada— asi que un cascaron sirve y evita cargar 3,7 GB de modelo
# para vectorizar documentos.
class SinModelo:
def load_model(self, *a, **k):
return True
rag.llm = SinModelo()
rag.secondary_llm = SinModelo()
trozo = rag.get_setting("chunk_size")
print(f"fragmentos de {trozo} tokens · esto tarda, no lo cortes\n")
ini = time.time()
try:
rag.create_index()
except Exception as e:
print(f"\nfallo al indexar: {type(e).__name__}: {e}")
return 1
minutos = (time.time() - ini) / 60
_, data_path = rag.get_paths()
peso = sum(
os.path.getsize(os.path.join(dp, f))
for dp, _, fs in os.walk(data_path) for f in fs) / 1048576
# create_index se traga sus propias excepciones e imprime el error, asi que
# terminar sin fallo no significa que haya indice. Se comprueba el disco.
if peso < 0.1:
print(f"\nno se creo indice ({peso:.1f} MB en {data_path})")
print("mira el error de arriba")
return 1
print(f"\nindice construido en {minutos:.1f} min · {peso:.1f} MB")
print(f"en {data_path}")
return 0
if __name__ == "__main__":
sys.exit(main())