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:
commit
8e4bc8ad94
125 changed files with 25033 additions and 0 deletions
150
config/wiki_ingesta.py
Normal file
150
config/wiki_ingesta.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue