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.
150 lines
6.3 KiB
Python
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()
|