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
61
entrena/README.md
Normal file
61
entrena/README.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# Entrenamiento de JARVIS
|
||||
|
||||
El fine-tuning NO sustituye al RAG (nucleo/saber/), lo complementa. El RAG ya
|
||||
funciona y da el conocimiento fresco; esto hornea el estilo y los flujos en los
|
||||
pesos. Por eso fue: primero RAG, esto despues.
|
||||
|
||||
## Los tres pasos
|
||||
|
||||
1. dataset.py convierte los apuntes de COFRE en pares pregunta-respuesta
|
||||
(usa el indice de nucleo/saber/, ~10 h con el 4B local)
|
||||
2. afina.py QLoRA sobre el dataset. --revisa dice si la maquina puede
|
||||
3. exportar el adaptador a GGUF + Modelfile con ADAPTER, para ollama
|
||||
|
||||
## Por que falla hoy: qwen3.5 es MULTIMODAL (16 ago)
|
||||
|
||||
Las pruebas de `afina.py` cargan el modelo bien y se caen justo despues, al
|
||||
enganchar los adaptadores, con un mensaje que no viene a cuento:
|
||||
|
||||
Incorrect image source. Must be a valid URL starting with `http://`
|
||||
|
||||
No es un fallo de red ni de rutas. **`Qwen/Qwen3.5-2B` es `image-text-to-text`**
|
||||
—comprobado en la API de HuggingFace, y el propio GGUF lo delata: el
|
||||
`model_info` de ollama trae `qwen35.vision.block_count = 24`—. O sea que el
|
||||
modelo lleva torre de vision, y `FastLanguageModel` lo mete por el camino de
|
||||
solo texto mientras transformers intenta procesar una imagen que no existe.
|
||||
|
||||
La pista estaba en el log y se leyo como ruido:
|
||||
|
||||
Unsloth: Explicit target_modules are constrained by the
|
||||
finetune_(vision|language|attention|mlp) filters
|
||||
|
||||
Ese aviso solo lo emite el camino multimodal.
|
||||
|
||||
Por donde va el arreglo: `FastVisionModel` con `finetune_vision_layers=False`,
|
||||
que es la via de unsloth para afinar solo la parte de lenguaje de un modelo con
|
||||
vision. **Sin probar todavia.**
|
||||
|
||||
De paso, los nombres que no existen en HuggingFace y que las pruebas fueron
|
||||
descartando: `Qwen3.5-3B`, `Qwen3.5-3B-Instruct`, `Qwen3.5-1.7B`,
|
||||
`Qwen3.5-1.7B-Instruct`, `Qwen3.5-1.5B`. El unico que carga es `Qwen3.5-2B`
|
||||
(1,95 GB).
|
||||
|
||||
## Donde entrenar
|
||||
|
||||
T1200 (4 GB) al limite. Turing, sin bf16. Posible pero fragil.
|
||||
3060 (12 GB) holgado. El sitio natural. El dataset viaja como fichero.
|
||||
|
||||
## Para entrenar
|
||||
|
||||
pip install unsloth # trae peft, trl, bitsandbytes
|
||||
./afina.py --revisa # comprueba
|
||||
./afina.py # entrena
|
||||
|
||||
## Exportar a ollama (tras entrenar)
|
||||
|
||||
# con llama.cpp:
|
||||
python convert_lora_to_gguf.py jarvis-lora --outfile jarvis-lora.gguf
|
||||
# Modelfile:
|
||||
# FROM qwen3.5:4b
|
||||
# ADAPTER ./jarvis-lora.gguf
|
||||
ollama create qwen3.5:4b-jarvis-cofre -f Modelfile
|
||||
236
entrena/afina.py
Executable file
236
entrena/afina.py
Executable file
|
|
@ -0,0 +1,236 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Fine-tuning de JARVIS con QLoRA sobre el dataset de COFRE.
|
||||
|
||||
./afina.py --revisa comprueba que se puede entrenar aqui, sin entrenar
|
||||
./afina.py entrena (largo; mejor en el servidor con la 3060)
|
||||
|
||||
## Que hace y que NO hace el fine-tuning
|
||||
|
||||
Le enseña al modelo el ESTILO y las respuestas de tus apuntes con ejemplos. NO
|
||||
sustituye al RAG: el RAG le da los datos frescos en el momento —y se actualiza
|
||||
soltando un fichero—, mientras que esto los hornea en los pesos. Se complementan:
|
||||
el fine-tuning hace que "suene" a JARVIS y conozca tus flujos sin buscar; el RAG
|
||||
cubre lo que cambia y lo que es muy especifico.
|
||||
|
||||
Por eso el orden fue: primero RAG (hecho, funciona hoy), y esto despues.
|
||||
|
||||
## QLoRA en cristiano
|
||||
|
||||
Entrenar un 4B entero pide mas memoria de la que hay. QLoRA hace dos trucos:
|
||||
- carga el modelo base en 4 bits (cabe en poca VRAM, y se queda CONGELADO)
|
||||
- entrena solo unas matrices pequeñas nuevas (los "adaptadores LoRA"), que son
|
||||
una fraccion del tamaño
|
||||
|
||||
Asi se entrena un 4B en pocos GB. El resultado es un adaptador de ~50 MB que se
|
||||
le pone encima al modelo base.
|
||||
|
||||
## Donde entrenar
|
||||
|
||||
T1200 (4 GB, esta maquina) al limite. Turing (capacidad 7.5): sin bf16 ni
|
||||
flash-attention. Posible con lote 1 y secuencia
|
||||
corta, lento y fragil. --revisa lo dice.
|
||||
3060 (12 GB, el servidor) holgado. Ampere: bf16 y flash-attention. Es el
|
||||
sitio natural, por eso el dataset se genera aparte
|
||||
y viaja como fichero.
|
||||
|
||||
## Dependencias
|
||||
|
||||
pip install unsloth (trae peft, trl, bitsandbytes, accelerate)
|
||||
|
||||
unsloth porque es lo que mejor exprime una GPU pequeña: mismo resultado con la
|
||||
mitad de VRAM y el doble de velocidad que peft+trl a pelo.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
AQUI = os.path.dirname(os.path.abspath(__file__))
|
||||
DATASET = os.path.join(AQUI, "dataset-cofre.jsonl")
|
||||
SALIDA = os.path.join(AQUI, "jarvis-lora")
|
||||
|
||||
# El modelo base, en formato HuggingFace. ollama corre GGUF; para entrenar hace
|
||||
# falta el modelo de verdad. Se prueban candidatos en orden hasta que uno
|
||||
# cargue: la version pre-cuantizada de unsloth va primero porque baja menos y
|
||||
# ocupa menos VRAM, que en 4 GB es lo que decide si entra.
|
||||
#
|
||||
# El de ollama es qwen3.5:4b (arquitectura qwen35, 4,7B, embedding 2560). Si
|
||||
# ninguno carga, la fase 0 lo dice y no se pierden horas.
|
||||
CANDIDATOS = [
|
||||
os.environ.get("JARVIS_BASE_HF", ""), # lo que ponga el usuario, si pone
|
||||
"unsloth/Qwen3.5-4B-Instruct-bnb-4bit",
|
||||
"Qwen/Qwen3.5-4B-Instruct",
|
||||
"unsloth/Qwen3.5-4B-bnb-4bit",
|
||||
"Qwen/Qwen3.5-4B",
|
||||
]
|
||||
|
||||
# Apretado para 4 GB (ver el plan). Con el 4,7B en 4 bits ocupando ~2,8 GB,
|
||||
# cada megabyte de mas en secuencia o rango es el que provoca el OOM.
|
||||
EPOCAS = 2
|
||||
LOTE = 1
|
||||
ACUMULA = 16 # lote efectivo 16 sin gastar VRAM
|
||||
LR = 2e-4
|
||||
MAX_SEQ = 512 # los pares Q&A son cortos; 512 no pierde casi nada
|
||||
RANGO = 8 # adaptadores pequeños
|
||||
|
||||
|
||||
def _base_que_carga():
|
||||
"""El primer candidato que exista y cargue. None si ninguno."""
|
||||
from unsloth import FastLanguageModel
|
||||
for repo in CANDIDATOS:
|
||||
if not repo:
|
||||
continue
|
||||
try:
|
||||
print(f" probando {repo} ...", flush=True)
|
||||
modelo, tok = FastLanguageModel.from_pretrained(
|
||||
model_name=repo, max_seq_length=MAX_SEQ, dtype=None,
|
||||
load_in_4bit=True)
|
||||
print(f" ✓ carga: {repo}")
|
||||
return repo, modelo, tok
|
||||
except Exception as e:
|
||||
print(f" no: {str(e).splitlines()[0][:80]}")
|
||||
return None, None, None
|
||||
|
||||
|
||||
def revisa() -> int:
|
||||
"""El ensayo de la fase 0: carga el 4,7B y hace UN paso de entrenamiento.
|
||||
|
||||
Es el momento de la verdad. Si ese paso pasa sin quedarse sin memoria, el
|
||||
entrenamiento entero es cuestion de tiempo. Si peta, se sabe en diez minutos
|
||||
y no en la sexta hora.
|
||||
"""
|
||||
print("\n ── se puede entrenar aqui? ──\n")
|
||||
|
||||
n = 0
|
||||
if os.path.exists(DATASET):
|
||||
with open(DATASET) as f:
|
||||
n = sum(1 for _ in f)
|
||||
print(f" dataset: {n} ejemplos"
|
||||
+ (" (suficiente para el test; poco para entrenar)" if n < 500 else ""))
|
||||
|
||||
try:
|
||||
import torch
|
||||
except ImportError:
|
||||
print(" torch: NO — el venv de entrenamiento no esta montado")
|
||||
return 1
|
||||
if not torch.cuda.is_available():
|
||||
print(" cuda: NO")
|
||||
return 1
|
||||
p = torch.cuda.get_device_properties(0)
|
||||
libre = torch.cuda.mem_get_info()[0] / 1e9
|
||||
print(f" gpu: {p.name} {p.total_memory/1e9:.1f} GB, {libre:.1f} libres, "
|
||||
f"capacidad {p.major}.{p.minor}"
|
||||
+ (" (Turing: fp16, sin flash-attn)" if p.major < 8 else ""))
|
||||
if libre < 3.3:
|
||||
print(" AVISO: hay menos de 3,3 GB libres. Cierra JARVIS y ollama antes.")
|
||||
|
||||
for m in ("unsloth", "peft", "trl", "bitsandbytes"):
|
||||
try:
|
||||
__import__(m)
|
||||
except ImportError:
|
||||
print(f" {m}: NO -> el venv de entrenamiento no acabo de instalar")
|
||||
return 1
|
||||
|
||||
# El momento de la verdad: cargar + un paso real.
|
||||
print("\n ── cargando el modelo y haciendo un paso de prueba ──")
|
||||
from unsloth import FastLanguageModel
|
||||
repo, modelo, tok = _base_que_carga()
|
||||
if repo is None:
|
||||
print("\n NINGUN candidato cargo. O no existe el repo HF del qwen3.5:4b,")
|
||||
print(" o esta arquitectura (qwen35) no la soporta este transformers.")
|
||||
print(" Pon el repo correcto en JARVIS_BASE_HF y reintenta.")
|
||||
return 1
|
||||
|
||||
tras_modelo = torch.cuda.memory_allocated() / 1e9
|
||||
print(f" VRAM tras cargar el modelo: {tras_modelo:.2f} GB")
|
||||
|
||||
modelo = FastLanguageModel.get_peft_model(
|
||||
modelo, r=RANGO, lora_alpha=RANGO, lora_dropout=0,
|
||||
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
||||
"gate_proj", "up_proj", "down_proj"],
|
||||
use_gradient_checkpointing="unsloth", random_state=42)
|
||||
|
||||
# un lote de una frase, forward + backward, que es donde revienta si revienta
|
||||
try:
|
||||
import torch as T
|
||||
texto = tok.apply_chat_template(
|
||||
[{"role": "user", "content": "que es secretsdump"},
|
||||
{"role": "assistant", "content": "Saca los hashes NTLM, señor."}],
|
||||
tokenize=False)
|
||||
ids = tok(texto, return_tensors="pt", max_length=MAX_SEQ,
|
||||
truncation=True).input_ids.to("cuda")
|
||||
salida = modelo(ids, labels=ids)
|
||||
salida.loss.backward()
|
||||
pico = T.cuda.max_memory_allocated() / 1e9
|
||||
print(f" ✓ un paso de entrenamiento paso. Pico de VRAM: {pico:.2f} GB")
|
||||
print(f"\n CABE. base a usar: {repo}")
|
||||
print(" Pon ese repo en JARVIS_BASE_HF si no es el primero, y ./afina.py\n")
|
||||
return 0
|
||||
except T.cuda.OutOfMemoryError:
|
||||
print("\n OOM: el 4,7B no entra en esta grafica ni apretando.")
|
||||
print(" Plan B: bajar MAX_SEQ a 256, o el 3B, o el servidor (3060).")
|
||||
return 2
|
||||
|
||||
|
||||
def entrena() -> int:
|
||||
if not os.path.exists(DATASET):
|
||||
print(" no hay dataset: ./dataset.py primero")
|
||||
return 1
|
||||
|
||||
import torch
|
||||
from unsloth import FastLanguageModel
|
||||
from datasets import load_dataset
|
||||
from trl import SFTTrainer, SFTConfig
|
||||
|
||||
ampere = torch.cuda.get_device_properties(0).major >= 8
|
||||
|
||||
repo, modelo, tokenizer = _base_que_carga()
|
||||
if repo is None:
|
||||
print(" ningun base cargo; corre --revisa para el diagnostico")
|
||||
return 1
|
||||
|
||||
modelo = FastLanguageModel.get_peft_model(
|
||||
modelo, r=RANGO, lora_alpha=RANGO, lora_dropout=0,
|
||||
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
||||
"gate_proj", "up_proj", "down_proj"],
|
||||
use_gradient_checkpointing="unsloth", random_state=42)
|
||||
|
||||
datos = load_dataset("json", data_files=DATASET, split="train")
|
||||
|
||||
def formatea(ej):
|
||||
return {"text": tokenizer.apply_chat_template(
|
||||
ej["messages"], tokenize=False, add_generation_prompt=False)}
|
||||
datos = datos.map(formatea)
|
||||
|
||||
entrenador = SFTTrainer(
|
||||
model=modelo, tokenizer=tokenizer, train_dataset=datos,
|
||||
dataset_text_field="text", max_seq_length=MAX_SEQ,
|
||||
args=SFTConfig(
|
||||
per_device_train_batch_size=LOTE,
|
||||
gradient_accumulation_steps=ACUMULA,
|
||||
num_train_epochs=EPOCAS, learning_rate=LR,
|
||||
fp16=not ampere, bf16=ampere,
|
||||
logging_steps=10, optim="adamw_8bit",
|
||||
warmup_ratio=0.05, lr_scheduler_type="cosine",
|
||||
output_dir=os.path.join(AQUI, "checkpoints"), seed=42))
|
||||
|
||||
print(" entrenando...")
|
||||
entrenador.train()
|
||||
|
||||
modelo.save_pretrained(SALIDA)
|
||||
tokenizer.save_pretrained(SALIDA)
|
||||
print(f"\n adaptador guardado: {SALIDA}")
|
||||
print(" para usarlo con ollama hay que exportarlo a GGUF y crear un")
|
||||
print(" Modelfile con ADAPTER. Ver el README de entrena/.")
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--revisa", action="store_true")
|
||||
a = p.parse_args()
|
||||
return revisa() if a.revisa else entrena()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
171
entrena/dataset.py
Executable file
171
entrena/dataset.py
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Genera el dataset de fine-tuning a partir de los apuntes de COFRE.
|
||||
|
||||
./dataset.py --muestra 5 enseña 5 ejemplos y para (para mirar la calidad)
|
||||
./dataset.py genera el dataset entero (horas, en segundo plano)
|
||||
|
||||
## Por que esto es el paso que faltaba
|
||||
|
||||
Fine-tuning no es magia: es enseñarle al modelo con EJEMPLOS de como quieres que
|
||||
conteste. Para eso hacen falta pares pregunta-respuesta, y no los teniamos. Los
|
||||
apuntes de COFRE son conocimiento, pero en prosa; hay que convertirlos en la
|
||||
forma "alguien pregunta X, JARVIS responde Y".
|
||||
|
||||
Se generan con el modelo local a partir de cada fragmento de los apuntes: se le
|
||||
da el trozo y se le pide que invente las preguntas que ese trozo contesta, y la
|
||||
respuesta al estilo de JARVIS. El fragmento es la VERDAD —lo escribio el
|
||||
usuario— asi que las respuestas salen de ahi y no de lo que el modelo recuerde.
|
||||
|
||||
## Por que el dataset vale aunque no se entrene aqui
|
||||
|
||||
La T1200 de 4 GB va justa para entrenar un 4B. El sitio natural es el servidor
|
||||
con la 3060 de 12 GB. Pero el DATASET es lo caro de conseguir y no depende de la
|
||||
maquina: se genera una vez, es un fichero, y se entrena donde haga falta —aqui
|
||||
si cabe, o en el servidor cuando haya acceso—.
|
||||
|
||||
## El formato
|
||||
|
||||
JSONL en el formato de mensajes, que es lo que comen trl/axolotl/unsloth:
|
||||
|
||||
{"messages": [{"role":"user","content":"..."},
|
||||
{"role":"assistant","content":"..."}]}
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
AQUI = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
INDICE = os.path.join(AQUI, "nucleo", "datos", "saber.jsonl")
|
||||
SALIDA = os.path.join(AQUI, "entrena", "dataset-cofre.jsonl")
|
||||
ENDPOINT = os.environ.get("JARVIS_OLLAMA", "http://127.0.0.1:11434")
|
||||
MODELO = os.environ.get("JARVIS_MODELO", "qwen3.5:4b-jarvis")
|
||||
|
||||
# Al generador se le da MAS libertad que a JARVIS en produccion: aqui quereis
|
||||
# variedad de preguntas, no una respuesta conservadora.
|
||||
GENERADOR = """Eres un generador de datos de entrenamiento para un asistente de
|
||||
pentesting llamado JARVIS. Te doy un fragmento de los apuntes tecnicos del
|
||||
usuario. Devuelve de UNA a TRES preguntas que una persona haria y que ese
|
||||
fragmento contesta, cada una con su respuesta.
|
||||
|
||||
Reglas:
|
||||
- Las preguntas, naturales y variadas: como las diria alguien por voz.
|
||||
- Las respuestas SALEN DEL FRAGMENTO. Si el fragmento tiene un comando exacto,
|
||||
la respuesta lo lleva tal cual. No te inventes flags que no aparezcan.
|
||||
- Respuestas al estilo de JARVIS: directas, en castellano, tratando de "señor".
|
||||
- Si el fragmento no da para una pregunta util (es un titulo, una lista de
|
||||
enlaces, ruido), devuelve una lista vacia.
|
||||
|
||||
Responde SOLO con JSON, un objeto con la clave "pares":
|
||||
{"pares": [{"pregunta": "...", "respuesta": "..."}]}"""
|
||||
|
||||
|
||||
def _pide(fragmento: str) -> list:
|
||||
cuerpo = json.dumps({
|
||||
"model": MODELO,
|
||||
"messages": [{"role": "system", "content": GENERADOR},
|
||||
{"role": "user", "content": fragmento[:1500]}],
|
||||
"stream": False, "think": False,
|
||||
"options": {"temperature": 0.7, "num_predict": 500},
|
||||
"format": "json",
|
||||
}).encode()
|
||||
req = urllib.request.Request(ENDPOINT + "/api/chat", data=cuerpo,
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=120) as r:
|
||||
contenido = json.loads(r.read())["message"]["content"]
|
||||
except Exception:
|
||||
return []
|
||||
# el modelo a veces envuelve el array en un objeto {"pares": [...]}
|
||||
try:
|
||||
d = json.loads(contenido)
|
||||
except json.JSONDecodeError:
|
||||
m = re.search(r"\[.*\]", contenido, re.S)
|
||||
if not m:
|
||||
return []
|
||||
try:
|
||||
d = json.loads(m.group(0))
|
||||
except json.JSONDecodeError:
|
||||
return []
|
||||
if isinstance(d, dict):
|
||||
d = next((v for v in d.values() if isinstance(v, list)), [])
|
||||
salida = []
|
||||
for par in d if isinstance(d, list) else []:
|
||||
if isinstance(par, dict) and par.get("pregunta") and par.get("respuesta"):
|
||||
salida.append((par["pregunta"].strip(), par["respuesta"].strip()))
|
||||
return salida
|
||||
|
||||
|
||||
def apuntes():
|
||||
"""Los fragmentos de apuntes del indice, los mas sustanciosos primero."""
|
||||
fuera = []
|
||||
with open(INDICE, encoding="utf-8") as f:
|
||||
for l in f:
|
||||
try:
|
||||
d = json.loads(l)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
if d.get("tipo") == "apunte" and len(d["texto"].split()) >= 20:
|
||||
d.pop("v", None)
|
||||
fuera.append(d)
|
||||
return fuera
|
||||
|
||||
|
||||
def main() -> int:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--muestra", type=int, default=0,
|
||||
help="genera solo N y los enseña, sin guardar")
|
||||
a = p.parse_args()
|
||||
|
||||
if not os.path.exists(INDICE):
|
||||
print(" no hay indice: nucleo/saber/indexa.py primero")
|
||||
return 1
|
||||
fragmentos = apuntes()
|
||||
print(f" {len(fragmentos)} fragmentos con sustancia", flush=True)
|
||||
|
||||
if a.muestra:
|
||||
for frag in fragmentos[:a.muestra]:
|
||||
pares = _pide(frag["texto"])
|
||||
print(f"\n ── {frag.get('fichero', frag['perfil'])} ──")
|
||||
for q, r in pares:
|
||||
print(f" P: {q}")
|
||||
print(f" R: {r[:120]}")
|
||||
return 0
|
||||
|
||||
# Reanudable: un trabajo de 10 h no puede perder todo si se corta. Se apunta
|
||||
# en un .hecho por que fragmento se iba, y al arrancar se salta hasta ahi y
|
||||
# se AÑADE al dataset en vez de reescribirlo.
|
||||
marca = SALIDA + ".hecho"
|
||||
desde = 0
|
||||
if os.path.exists(marca) and os.path.exists(SALIDA):
|
||||
try:
|
||||
desde = int(open(marca).read().strip()) + 1
|
||||
except (ValueError, OSError):
|
||||
desde = 0
|
||||
modo = "a" if desde else "w"
|
||||
hechos = sum(1 for _ in open(SALIDA)) if desde and os.path.exists(SALIDA) else 0
|
||||
if desde:
|
||||
print(f" reanudando desde el fragmento {desde} ({hechos} ejemplos hechos)",
|
||||
flush=True)
|
||||
|
||||
with open(SALIDA, modo, encoding="utf-8", buffering=1) as out:
|
||||
for i in range(desde, len(fragmentos)):
|
||||
for q, r in _pide(fragmentos[i]["texto"]):
|
||||
out.write(json.dumps({"messages": [
|
||||
{"role": "user", "content": q},
|
||||
{"role": "assistant", "content": r},
|
||||
]}, ensure_ascii=False) + "\n")
|
||||
hechos += 1
|
||||
with open(marca, "w") as m:
|
||||
m.write(str(i))
|
||||
if (i + 1) % 50 == 0:
|
||||
print(f" {i+1}/{len(fragmentos)} fragmentos, {hechos} ejemplos",
|
||||
flush=True)
|
||||
print(f"\n dataset guardado: {SALIDA} ({hechos} ejemplos)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue