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

150 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Descarga articulos de Wikipedia en espanol sobre los temas de JARVIS y los
deja como .txt en la carpeta de documentos del RAG. Luego, construir_indice.py
los vectoriza. Corre en el host (solo stdlib + red), sin sonido ni GPU.
python3 config/wiki_ingesta.py # descarga
# despues, dentro del flatpak:
flatpak run --command=python3 io.github.qwersyk.Newelle//master \
~/COFRE/CODERS/JARVIS/config/construir_indice.py
Es aditivo: agrega ficheros a la carpeta, no borra lo que ya haya.
"""
import os, json, time, urllib.parse, urllib.request, urllib.error
API = "https://es.wikipedia.org/w/api.php"
UA = "JARVIS-local-RAG/1.0 (asistente de voz local; contacto: local)"
DOCS = os.path.expanduser(
"~/.var/app/io.github.qwersyk.Newelle/config/models/documents")
# Por tema: semillas seguras + una busqueda que amplia con articulos afines.
TEMAS = {
"tecnologia": {
"buscar": ["tecnología", "informática", "hardware", "red informática",
"inteligencia artificial", "sistema operativo"],
"semillas": ["Tecnología", "Informática", "Internet", "Protocolo de red",
"Microprocesador", "Computación en la nube", "Base de datos"],
},
"criptografia": {
"buscar": ["criptografía", "cifrado", "criptografía asimétrica"],
"semillas": ["Criptografía", "Cifrado", "RSA", "Advanced Encryption Standard",
"Función hash criptográfica", "Firma digital",
"Diffie-Hellman", "Pretty Good Privacy", "Transport Layer Security",
"Criptografía de curva elíptica", "Cadena de bloques",
"Bitcoin", "Criptomoneda"],
},
"software_libre": {
"buscar": ["software libre", "código abierto", "historia de Linux"],
"semillas": ["Software libre", "GNU", "Richard Stallman", "GNU/Linux",
"Linus Torvalds", "Free Software Foundation", "Licencia pública general de GNU",
"Código abierto", "Debian", "Unix", "Kernel Linux",
"Movimiento del software libre"],
},
"hacking": {
"buscar": ["seguridad informática", "hacker", "ciberseguridad"],
"semillas": ["Hacker", "Seguridad informática", "Hacker (seguridad informática)",
"Malware", "Exploit", "Ingeniería social (seguridad informática)",
"Phreaking", "Kevin Mitnick", "Anonymous (colectivo)",
"Prueba de penetración", "Vulnerabilidad (informática)",
"Cortafuegos (informática)", "Ataque de denegación de servicio"],
},
"clima": {
"buscar": ["cambio climático", "calentamiento global", "clima"],
"semillas": ["Cambio climático", "Calentamiento global", "Efecto invernadero",
"Gas de efecto invernadero", "Protocolo de Kioto",
"Acuerdo de París", "IPCC", "Clima", "Dióxido de carbono",
"El Niño", "Nivel del mar", "Energía renovable"],
},
"conflictos_espana": {
"buscar": ["guerra en España", "conflicto España historia"],
"semillas": ["Guerra civil española", "Transición española",
"Guerra de la Independencia Española", "Guerras carlistas",
"Reconquista", "Dictadura de Francisco Franco",
"Segunda República Española", "Movimiento 15-M",
"Conflicto vasco", "Guerra de Sucesión Española"],
},
}
BUSCAR_POR_CONSULTA = 15 # articulos extra por cada busqueda
PAUSA = 0.8 # cortesia con la API (subida tras el 429)
def _get(params, reintentos=5):
params = {**params, "format": "json"}
url = API + "?" + urllib.parse.urlencode(params)
req = urllib.request.Request(url, headers={"User-Agent": UA})
espera = 2.0
for intento in range(reintentos):
try:
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r)
except urllib.error.HTTPError as e:
if e.code == 429 and intento < reintentos - 1:
# Wikipedia nos frena: esperar cada vez mas (backoff)
time.sleep(espera)
espera *= 2
continue
raise
raise RuntimeError("agotados los reintentos")
def titulos_de_busqueda(consulta, limite):
try:
d = _get({"action": "query", "list": "search",
"srsearch": consulta, "srlimit": limite})
return [h["title"] for h in d.get("query", {}).get("search", [])]
except Exception as e:
print(f" busqueda '{consulta}' fallo: {e}")
return []
def extracto(titulo):
try:
d = _get({"action": "query", "prop": "extracts", "explaintext": 1,
"redirects": 1, "titles": titulo})
paginas = d.get("query", {}).get("pages", {})
for _, pg in paginas.items():
txt = pg.get("extract", "")
if txt and len(txt) > 300:
return pg.get("title", titulo), txt
except Exception as e:
print(f" extracto '{titulo}' fallo: {e}")
return None, None
def slug(s):
s = "".join(c if c.isalnum() else "_" for c in s.lower())
return s.strip("_")[:80]
def main():
os.makedirs(DOCS, exist_ok=True)
total = 0
for tema, cfg in TEMAS.items():
print(f"\n=== {tema} ===")
titulos = list(cfg["semillas"])
for consulta in cfg["buscar"]:
titulos += titulos_de_busqueda(consulta, BUSCAR_POR_CONSULTA)
time.sleep(PAUSA)
# dedup conservando orden
vistos, unicos = set(), []
for t in titulos:
if t.lower() not in vistos:
vistos.add(t.lower()); unicos.append(t)
for t in unicos:
dst = os.path.join(DOCS, f"wiki_{tema}_{slug(t)}.txt")
if os.path.exists(dst):
continue
titulo_real, txt = extracto(t)
time.sleep(PAUSA)
if not txt:
continue
with open(dst, "w", encoding="utf-8") as f:
f.write(f"# {titulo_real}\n(Wikipedia, tema: {tema})\n\n{txt}\n")
total += 1
print(f" [{total}] {titulo_real} ({len(txt)//1024} KB)")
print(f"\nlisto: {total} articulos nuevos en {DOCS}")
if __name__ == "__main__":
main()