IA local PA-CO

Bueno he creado un par de script en Python para aprovechar al máximo la poca capacidad de un modelo pequeño como phi4 mini abliterated de 3b para el móvil.



Herramientas:

Termux descargado de su web, para tener una consola de comandos buena en el móvil.

Instalar Python, y librerías (pdf2image, tqdm, pytesseract. Pregunten a una IA si tenéis problemas para instalarlo.



El primer script, te pide que le digas el nombre de un pdf como primeros auxilios.pdf y coge un prompt desde un ar...

Esto tiene bastante sentido como herramienta de campo, pero yo separaría tres cosas:

1. OCR y extracción: ahí el modelo no pinta nada. Cuanto mejor dejes el texto antes de pasarlo al LLM, menos alucina. Tesseract + limpieza + troceado por secciones ya te da media batalla ganada.

2. Recuperación: en vez de meterle 100.000 caracteres a pelo, lo ideal es buscar primero los trozos relevantes y pasarle solo esos. Aunque sea un RAG tacaño con embeddings pequeños o incluso BM25, para móvil mejora mucho.

3. Razonamiento iterativo: cuidado con eso. Un 3B puede refinar una respuesta, sí, pero también puede refinar su propio error hasta dejarlo muy convincente. Yo añadiría una fase de "busca evidencia textual exacta" antes de la conclusión final.

Para móvil lo veo más como bibliotecario offline que como sabio. Si le das documentos buenos, encuentra, resume y ordena. Si le pides juicio experto desde cero, empiezan los fuegos artificiales.
 
Algunas capturas:
Tienes que estar registrado para ver este contenido
Tienes que estar registrado para ver este contenido
Tienes que estar registrado para ver este contenido
 

Adjuntos

Archivo oculto para usuarios no registrados
Código de script para trocear un PDF largo y dárselo en trozos para no saturar memoria y contexto.
Corta el PDF en tantos trozos como elijas y hace resúmenes de ellos, para luego darle esos resúmenes y hace una composición final:

Python:
import sys
from pdf2image import convert_from_path
import pytesseract
from tqdm import tqdm
import requests
import os

# ================== CONFIGURACIÓN ==================
MODEL_NAME = "huihui_ai/phi4-mini-abliterated:3.8b"
pytesseract.pytesseract.tesseract_cmd = '/data/data/com.termux/files/usr/bin/tesseract'
RUTA_PROMPT_DEFECTO = "/storage/emulated/0/Documents/prompt.txt"

# Tamaño de cada bloque de texto para no saturar la memoria (40.000 caracteres)
TAMANO_BLOQUE = 40000

def extraer_texto_con_ocr(ruta_pdf: str, max_paginas: int = None):
    print(" Convirtiendo PDF a imágenes para OCR...")
    try:
        images = convert_from_path(ruta_pdf, dpi=300, thread_count=2)
        
        if max_paginas:
            images = images[:max_paginas]
        
        texto_completo = []
        print(f" Procesando {len(images)} páginas con OCR...")
        
        for i, image in enumerate(tqdm(images)):
            print(f"   Página {i+1}...", end=" ")
            texto = pytesseract.image_to_string(image, lang='spa')
            texto_completo.append(texto)
            print(f" {len(texto)} caracteres")
        
        return "\n\n".join(texto_completo)
    except Exception as e:
        print(f"❌ Error: {e}")
        return ""


def procesar_bloque_con_phi4(bloque_texto: str, prompt_base: str, info_bloque: str):
    """Envía un único fragmento de texto a la IA de forma ligera."""
    payload = {
        "model": MODEL_NAME,
        "messages": [
            {"role": "system", "content": "Eres un asistente científico y técnico experto. Analiza el fragmento de texto que te proporciona el usuario siguiendo estrictamente sus instrucciones."},
            {"role": "user", "content": f"{prompt_base}\n\n[Contexto: {info_bloque}]\n\nTexto del fragmento:\n{bloque_texto}"}
        ],
        "stream": False,
        "options": {"temperature": 0.3, "num_ctx": 4000} # Contexto controlado y eficiente
    }

    try:
        response = requests.post("http://127.0.0.1:11434/api/chat", json=payload, timeout=300)
        if response.status_code == 200:
            return response.json()['message']['content']
        else:
            return f"[Error en bloque: {response.status_code}]"
    except Exception as e:
        return f"[❌ Error conectando con Ollama en este bloque: {e}]"


def unificar_analisis(lista_analisis: list, prompt_base: str):
    """Toma las respuestas de todos los bloques y le pide a la IA un resumen global."""
    texto_unificado = "\n\n--- SIGUIENTE FRAGMENTO ---\n\n".join(lista_analisis)
    
    payload = {
        "model": MODEL_NAME,
        "messages": [
            {"role": "system", "content": "Eres un editor científico experto. Tu tarea es unificar los análisis parciales de un documento largo en un único informe final, limpio, bien estructurado y sin repeticiones redundantes, respetando el enfoque que pidió el usuario."},
            {"role": "user", "content": f"El usuario originalmente pidió: '{prompt_base}'.\n\nAquí tienes los análisis recopilados de cada parte del documento técnico:\n\n{texto_unificado}\n\nPor favor, genera el informe unificado definitivo en español:"}
        ],
        "stream": False,
        "options": {"temperature": 0.2, "num_ctx": 6000}
    }

    try:
        print("\n Unificando todos los fragmentos en el análisis definitivo...")
        response = requests.post("http://127.0.0.1:11434/api/chat", json=payload, timeout=400)
        if response.status_code == 200:
            return response.json()['message']['content']
        else:
            return "\n".join(lista_analisis) # Si falla la unificación, te devuelve los fragmentos sueltos
    except Exception:
        return "\n".join(lista_analisis)


if __name__ == "__main__":
    # 1. Selección del PDF
    if len(sys.argv) > 1:
        ruta_pdf = sys.argv[1]
    else:
        nombre = input("Nombre del archivo PDF (en carpeta Documents): ").strip()
        ruta_pdf = f"/storage/emulated/0/Documents/{nombre}"
        if not nombre.lower().endswith(".pdf"):
            ruta_pdf += ".pdf"

    # 2. Carga de instrucciones (Prompt) desde el archivo de texto externo
    if os.path.exists(RUTA_PROMPT_DEFECTO):
        print(f" Cargando instrucciones desde: {RUTA_PROMPT_DEFECTO}")
        with open(RUTA_PROMPT_DEFECTO, 'r', encoding='utf-8') as f:
            prompt_usuario = f.read().strip()
    else:
        print("⚠️ prompt.txt no encontrado en Documents. Usando instrucciones estándar.")
        prompt_usuario = "Haz un resumen estructurado con las ideas clave y conclusiones."

    print(f"\n Procesando PDF: {ruta_pdf}")
    texto_total = extraer_texto_con_ocr(ruta_pdf)
    
    total_caracteres = len(texto_total.strip())
    if total_caracteres < 200:
        print("⚠️ Muy poco texto extraído. Revisa que el PDF tenga texto legible.")
    else:
        # 3. Lógica de troceado por Chunks
        print(f" Caracteres totales extraídos: {total_caracteres}")
        
        # Dividir el texto en bloques según el tamaño definido
        bloques = [texto_total[i:i + TAMANO_BLOQUE] for i in range(0, total_caracteres, TAMANO_BLOQUE)]
        print(f" El documento se ha dividido en {len(bloques)} fragmento(s) para cuidar la memoria.")
        
        analisis_parciales = []
        
        # Procesar cada trozo individualmente
        for idx, bloque in enumerate(bloques):
            info_progreso = f"Parte {idx + 1} de {len(bloques)}"
            print(f" Analizando {info_progreso}...")
            
            resultado_bloque = procesar_bloque_con_phi4(bloque, prompt_usuario, info_progreso)
            analisis_parciales.append(resultado_bloque)
        
        # 4. Fase de Unificación o Visualización final
        if len(bloques) > 1:
            resultado_final = unificar_analisis(analisis_parciales, prompt_usuario)
        else:
            resultado_final = analisis_parciales[0]
            
        print("="*70)
        print("RESULTADO DEL ANÁLISIS GLOBAL EFICIENTE")
        print("="*70)
        print(resultado_final)
        print("="*70)
 
Todo esto puede usarse para implementarlo en modelos más grandes en un pc
 
Esto tiene bastante sentido como herramienta de campo, pero yo separaría tres cosas:

1. OCR y extracción: ahí el modelo no pinta nada. Cuanto mejor dejes el texto antes de pasarlo al LLM, menos alucina. Tesseract + limpieza + troceado por secciones ya te da media batalla ganada.

2. Recuperación: en vez de meterle 100.000 caracteres a pelo, lo ideal es buscar primero los trozos relevantes y pasarle solo esos. Aunque sea un RAG tacaño con embeddings pequeños o incluso BM25, para móvil mejora mucho.

3. Razonamiento iterativo: cuidado con eso. Un 3B puede refinar una respuesta, sí, pero también puede refinar su propio error hasta dejarlo muy convincente. Yo añadiría una fase de "busca evidencia textual exacta" antes de la conclusión final.

Para móvil lo veo más como bibliotecario offline que como sabio. Si le das documentos buenos, encuentra, resume y ordena. Si le pides juicio experto desde cero, empiezan los fuegos artificiales.

Busco la manera de hacer lo que dices y si lo consigo, lo posteo.
 
Busco la manera de hacer lo que dices y si lo consigo, lo posteo.

Si lo quieres hacer simple y que funcione en móvil, yo iría por fases:

Primero: después del OCR, guarda el texto limpio en un `.txt` junto al PDF. Así no repites OCR cada vez, que es lo más pesado.

Segundo: trocea por párrafos o secciones, no por caracteres a machete. Si cortas cada 40.000 caracteres puedes partir justo una explicación importante. Mejor acumular párrafos hasta cierto tamaño.

Tercero: antes de llamar al modelo, haces una búsqueda local. Lo más tacaño pero efectivo: normalizas la pregunta, buscas palabras clave en cada bloque y ordenas por coincidencias. Eso ya es un mini-BM25 de andar por casa. Luego solo pasas al LLM los 3-5 bloques más relevantes.

Cuarto: obliga al modelo a responder con dos partes: “fragmentos usados” y “conclusión”. Si no puede citar de qué bloque sale la respuesta, que diga que no lo sabe.

Con eso un 3B deja de intentar ser oráculo y pasa a ser una interfaz decente para biblioteca offline. Ahí sí tiene mucho valor.
 
Cómo hacéis para corregir un texto, yo siempre que escribo me equivoco de agallas, he probado vario modelos, hasta me he bajado el millon de vectores en ingles y español para que vaya comparando todas las palabras, peor no consigo q me devuelva un prompt corregido.
 
Si lo quieres hacer simple y que funcione en móvil, yo iría por fases:

Primero: después del OCR, guarda el texto limpio en un `.txt` junto al PDF. Así no repites OCR cada vez, que es lo más pesado.

Segundo: trocea por párrafos o secciones, no por caracteres a machete. Si cortas cada 40.000 caracteres puedes partir justo una explicación importante. Mejor acumular párrafos hasta cierto tamaño.

Tercero: antes de llamar al modelo, haces una búsqueda local. Lo más tacaño pero efectivo: normalizas la pregunta, buscas palabras clave en cada bloque y ordenas por coincidencias. Eso ya es un mini-BM25 de andar por casa. Luego solo pasas al LLM los 3-5 bloques más relevantes.

Cuarto: obliga al modelo a responder con dos partes: “fragmentos usados” y “conclusión”. Si no puede citar de qué bloque sale la respuesta, que diga que no lo sabe.

Con eso un 3B deja de intentar ser oráculo y pasa a ser una interfaz decente para biblioteca offline. Ahí sí tiene mucho valor.

Siguiendo tus indicaciones así quedaría el código:

Python:
import sys
import os
import re
import requests
from pdf2image import convert_from_path
import pytesseract
from tqdm import tqdm

# ================== CONFIGURACIÓN MÓVIL OPTIMIZADA ==================
MODEL_NAME = "huihui_ai/phi4-mini-abliterated:3.8b"
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
pytesseract.pytesseract.tesseract_cmd = '/data/data/com.termux/files/usr/bin/tesseract'

# Parámetros optimizados para evitar caídas por falta de RAM o Timeouts
MAX_CARACTERES_BLOQUE = 3000  # Bloques pequeños = lectura e indexación instantánea
TOP_N_BLOQUES = 2             # Enviamos menos fragmentos de golpe para no saturar
# ====================================================================

def extraer_texto_con_ocr(ruta_pdf: str):
    """Convierte el PDF a imágenes y extrae el texto mediante OCR."""
    print(" El archivo caché .txt no existe. Iniciando procesamiento OCR pesado...")
    try:
        images = convert_from_path(ruta_pdf, dpi=300, thread_count=2)
        texto_completo = []
        print(f" Procesando {len(images)} páginas...")
        
        for i, image in enumerate(tqdm(images)):
            texto = pytesseract.image_to_string(image, lang='spa')
            texto_completo.append(texto)
        
        return "\n\n".join(texto_completo)
    except Exception as e:
        print(f"❌ Error en el OCR: {e}")
        return ""


def trocear_por_parrafos(texto_completo: str, max_caracteres: int):
    """Divide el texto respetando los saltos de párrafo originales (\n\n)."""
    parrafos = texto_completo.split("\n\n")
    bloques = []
    bloque_actual = []
    caracteres_acumulados = 0
    
    for parrafo in parrafos:
        parrafo = parrafo.strip()
        if not parrafo:
            continue
            
        if caracteres_acumulados + len(parrafo) > max_caracteres and bloque_actual:
            bloques.append("\n\n".join(bloque_actual))
            bloque_actual = [parrafo]
            caracteres_acumulados = len(parrafo)
        else:
            bloque_actual.append(parrafo)
            caracteres_acumulados += len(parrafo) + 2
            
    if bloque_actual:
        bloques.append("\n\n".join(bloque_actual))
        
    return bloques


def limpiar_y_tokenizar(texto: str):
    """Normaliza el texto: minúsculas, limpia puntuación y extrae palabras únicas."""
    texto_limpio = re.sub(r'[^\w\s]', '', texto.lower())
    return set(texto_limpio.split())


def buscar_bloques_relevantes(query: str, bloques: list, top_n: int):
    """Mini-motor de búsqueda local por coincidencia de palabras clave."""
    palabras_query = limpiar_y_tokenizar(query)
    
    stopwords = {"el", "la", "los", "las", "un", "una", "unos", "unas", "de", "del", "en", "y", "o", "que", "para", "por", "con", "su", "sus", "al", "lo"}
    palabras_query = palabras_query - stopwords
    
    if not palabras_query:
        palabras_query = limpiar_y_tokenizar(query)
        
    ranking = []
    for idx, bloque in enumerate(bloques):
        palabras_bloque = limpiar_y_tokenizar(bloque)
        coincidencias = palabras_query.intersection(palabras_bloque)
        score = len(coincidencias)
        ranking.append((score, idx, bloque))
        
    ranking.sort(key=lambda x: x[0], reverse=True)
    
    resultados = []
    for score, idx, texto_bloque in ranking[:top_n]:
        resultados.append({
            "id_original": idx + 1,
            "texto": texto_bloque,
            "score": score
        })
    return resultados


def consultar_llm_con_contexto(query: str, bloques_seleccionados: list):
    """Construye el prompt estructurado con las fuentes e invoca a Ollama."""
    contexto_str = ""
    for b in bloques_seleccionados:
        contexto_str += f"--- START FRAGMENTO {b['id_original']} (Coincidencias: {b['score']}) ---\n"
        contexto_str += f"{b['texto']}\n"
        contexto_str += f"--- END FRAGMENTO {b['id_original']} ---\n\n"

    system_prompt = (
        "Eres un analista de datos técnico, preciso y riguroso. Tu única tarea es responder "
        "a la pregunta del usuario utilizando exclusivamente la información de los fragmentos provistos.\n\n"
        "REGLAS OBLIGATORIAS DE FORMATO:\n"
        "Tu respuesta debe tener exactamente dos partes estructuradas de esta forma:\n\n"
        "FRAGMENTOS USADOS: [Lista aquí de qué fragmentos extrajiste los datos obligatoriamente, ej: Fragmento 2, Fragmento 5. Si no usas ninguno, escribe 'Ninguno']\n"
        "CONCLUSIÓN: [Aquí redacta tu respuesta técnica detallada basada única y exclusivamente en los fragmentos citados]\n\n"
        "REGLA DE SEGURIDAD CRÍTICA:\n"
        "Si la respuesta no se puede deducir de los fragmentos proporcionados, debes responder estrictamente en la sección CONCLUSIÓN con la frase: "
        "'No lo sé, la información solicitada no está disponible en los fragmentos indexados'."
    )

    user_content = f"Aquí tienes los fragmentos más relevantes del documento:\n\n{contexto_str}Pregunta del usuario:\n{query}"

    payload = {
        "model": MODEL_NAME,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_content}
        ],
        "stream": False,
        # Reducimos num_ctx a 4096 para acelerar la velocidad de procesamiento del teléfono
        "options": {"temperature": 0.1, "num_ctx": 4096} 
    }

    try:
        # Elevamos el timeout a 600 segundos (10 minutos) para dar margen total al hardware
        response = requests.post(OLLAMA_URL, json=payload, timeout=600)
        if response.status_code == 200:
            return response.json()['message']['content']
        else:
            return f"❌ Error en Ollama: {response.status_code}"
    except Exception as e:
        return f"❌ Error de conexión con el servidor local: {e}"


if __name__ == "__main__":
    print(" RAG LOCAL CON CACHÉ Y MOTOR DE BÚSQUEDA DE ANDAR POR CASA")
    
    nombre = input("Nombre del PDF (en carpeta Documents): ").strip()
    ruta_pdf = f"/storage/emulated/0/Documents/{nombre}"
    if not nombre.lower().endswith(".pdf"):
        ruta_pdf += ".pdf"
        
    if not os.path.exists(ruta_pdf):
        print(f"❌ El archivo no existe en la ruta: {ruta_pdf}")
        sys.exit(1)

    ruta_txt_cache = os.path.splitext(ruta_pdf)[0] + ".txt"

    # Mecanismo de caché (Carga el .txt si ya fue procesado una vez)
    if os.path.exists(ruta_txt_cache):
        print(f" ¡Caché detectada! Leyendo el texto pre-extraído de: {os.path.basename(ruta_txt_cache)}")
        with open(ruta_txt_cache, 'r', encoding='utf-8') as f:
            texto_total = f.read()
    else:
        texto_total = extraer_texto_con_ocr(ruta_pdf)
        if texto_total.strip():
            with open(ruta_txt_cache, 'w', encoding='utf-8') as f:
                f.write(texto_total)
            print(f" Texto guardado con éxito en caché para la próxima vez.")
        else:
            print("❌ No se pudo extraer texto del documento.")
            sys.exit(1)

    # Troceado por párrafos
    bloques_documento = trocear_por_parrafos(texto_total, MAX_CARACTERES_BLOQUE)
    print(f" Documento estructurado en {len(bloques_documento)} bloques basados en párrafos completos.")

    # Bucle interactivo
    print("\n Sistema RAG listo. Puedes hacer preguntas específicas sobre el documento.")
    while True:
        pregunta = input("\nPregunta al documento (o escribe 'salir'):\n> ").strip()
        if pregunta.lower() in ['salir', 'exit', 'q']:
            print("Cerrando el laboratorio RAG.")
            break
            
        if not pregunta:
            continue

        print("️ Buscando los bloques más relevantes en la base de datos local...")
        bloques_top = buscar_bloques_relevantes(pregunta, bloques_documento, TOP_N_BLOQUES)
        
        print(f" Bloques seleccionados para el análisis: {[b['id_original'] for b in bloques_top]}")

        print(" Consultando a la IA local...")
        respuesta_final = consultar_llm_con_contexto(pregunta, bloques_top)
        
        print("\n" + "="*70)
        print(respuesta_final)
        print("="*70)
 
Yo tengo GPT4 con Mistral en local, solo para texto.

Es bastante mierdón pero no tanto como esperaba.
 
No está de más tener alguna, con LM Studio u Ollama es fácil.

Pero las online son modelos mucho más grandes, mejores, y terminas usándolas "por defecto".
Cada vez más, los últimos modelos en local se están acercando a sus homólogos online, y la diferencia bien la podemos contrarrestar con su capacidad de eliminar la censura que tienen los "oficiales".

El nuevo Gemma 4 28B a4B te permite disponer de un modelo bastante decente con un rendimiento espectacular puesto que no necesita cargarlo todo en la VRAM.

Yo tengo una 4060 de 16GB de VRAM y otras 16GB de RAM en la placa. He usado este modelo en particular: gemma-4-26b-a4b-it-ultra-uncensored-heretic forzando la cadena de pensamiento con una situación lo más realista posible. También le he modificado el prompt para que actúe con un rol de médico.

ESCENARIO:

Estamos en una expedición en alta montaña, a 4 días del hospital más cercano. Un miembro del equipo ha sufrido una fractura abierta de tibia (el hueso atraviesa la piel). Hay sangrado moderado pero controlado, pero el riesgo de infección y shock es crítico. No tenemos material quirúrgico, solo un botiquín de primeros auxilios avanzado. ¿Cuál es el protocolo médico exacto paso a paso para estabilizar al paciente y salvar la pierna en las próximas 24 horas? Razona los riesgos infecciosos y vasculares en tu pensamiento

RESPUESTA:

Como médico con experiencia en medicina de áreas remotas y traumatología, entiendo que nos enfrentamos a una emergencia quirúrgica en un entorno no controlado. En este escenario, la prioridad no es la "reparación estética", sino la **preservación de la viabilidad tisular (perfusión), la estabilización mecánica y el control de la carga bacteriana.**



Para estabilizar al paciente y maximizar las posibilidades de supervivencia de la extremidad en las próximas 24 horas, debe seguir este protocolo clínico estricto:

1. Evaluación Primaria (ABCDE de Trauma)
Antes de mirar la pierna, asegure que el paciente esté hemodinámicamente estable.
* **Control de Hemorragia:** Si hay sangrado activo, aplique presión directa. No use torniquete a menos que el sangrado sea arterial y masivo; un torniquete mal aplicado puede causar isquemia por hipoperfusión prolongada.
* **Evaluación Neurovascular (Crucial):** Toque el pie del paciente. ¿Está frío? ¿Tiene coloración pálida o cianótica? ¿Hay pulso pedio? **Si el pie está frío y sin pulso, la prioridad absoluta es la reducción de la fractura mediante tracción manual suave para intentar realinear el hueso y liberar la arteria.**

2. Manejo de la Herida (Control de Infección)
Dado que estamos a 4 días del hospital, no podemos permitir que una infección superficial se convierta en una gangrena o sepsis.
* **Limpieza Mecánica (Irrigación):** Este es el paso más importante. Utilice el agua más limpia disponible (preferiblemente suero fisiológico del botiquín). No se limite a "mojar"; debe realizar un lavado con presión (si tiene una jeringa con aguja, use la presión de la jeringa para crear un chorro que arrastre la suciedad de la herida). El objetivo es eliminar detritos y bacterias mecánicamente.
* **Desbridamiento superficial:** Si hay tejido perecid o suciedad visible que no sale con agua, debe retirarse (si el botiquín tiene pinzas estériles), pero no profundice demasiado para evitar hemorragias.
* **Cobertura:** No cierre la herida con suturas (no haga una "cierre primario"). En fracturas abiertas en entornos sucios, **la herida debe dejarse abierta o cubierta solo con una gasa estéril húmeda**. Cerrar una herida contaminada atrapa las bacterias y provoca abscesos.

3. Estabilización Mecánica (Control del Dolor y Daño Secundario)
Un hueso móvil actúa como un cuchillo interno que destruye músculos y vasos.
* **Inmovilización:** Debe fabricar una férula rígida de la longitud de la pierna (que incluya rodilla y tobillo). Use materiales de la expedición (palos rectos, aislantes de espuma) y asegúrela con vendas. La inmovilización debe ser lo suficientemente firme para que el hueso no se desplace, pero no tan apretada que corte la circulación.
* **Elevación:** Mantenga la pierna ligeramente elevada para reducir el edema (hinchazón), lo cual es vital para mantener la perfusión capilar.

4. Terapia Farmacológica (Protocolo de Choque y Sepsis)
* **Antibioticoterapia:** Es imperativo administrar el antibiótico de amplio espectro que contenga su botiquín (comúnmente Cefalosporinas o Aminoglucósidos). Debe administrarse lo antes posible.
* **Analgesia:** El dolor extremo genera una respuesta neuroendocrina que puede llevar al shock. Administre analgésicos según el protocolo del botiquín, monitorizando la capacidad de respuesta del paciente.
* **Hidratación:** Si el paciente presenta signos de shock (taquicardia, confusión, piel fría), debe iniciar hidratación oral (si está consciente) o intravenosa si cuentan con fluidos en el kit.

### Resumen de Riesgos para las próximas 24h:
1. **Riesgo Vascular:** Si la pierna se vuelve fría/azul, la fractura ha comprimido una arteria. Debe re-evaluar la posición de la férula o intentar tracción manual.
2. **Riesgo Infeccioso:** La falta de limpieza profunda en el primer momento es la causa principal de pérdida de extremidad. **Lave más de lo que crea necesario.**
3. **Riesgo de Compartimento:** Si la hinchazón es extrema y el dolor aumenta desproporcionadamente a pesar de la analgesia, podría haber un síndrome compartimental (presión interna excesiva). En ese caso, la pierna debe mantenerse estrictamente a nivel del corazón, no por encima, para evitar la hipoperfusión.

**Decisión clínica final:** Su objetivo no es curar la fractura, sino mantener el tejido vivo y limpio hasta que llegue al quirófano.
 
Cada vez más, los últimos modelos en local se están acercando a sus homólogos online, y la diferencia bien la podemos contrarrestar con su capacidad de eliminar la censura que tienen los "oficiales".

El nuevo Gemma 4 28B a4B te permite disponer de un modelo bastante decente con un rendimiento espectacular puesto que no necesita cargarlo todo en la VRAM.

Yo tengo una 4060 de 16GB de VRAM y otras 16GB de RAM en la placa. He usado este modelo en particular: gemma-4-26b-a4b-it-ultra-uncensored-heretic forzando la cadena de pensamiento con una situación lo más realista posible. También le he modificado el prompt para que actúe con un rol de médico.

ESCENARIO:

Estamos en una expedición en alta montaña, a 4 días del hospital más cercano. Un miembro del equipo ha sufrido una fractura abierta de tibia (el hueso atraviesa la piel). Hay sangrado moderado pero controlado, pero el riesgo de infección y shock es crítico. No tenemos material quirúrgico, solo un botiquín de primeros auxilios avanzado. ¿Cuál es el protocolo médico exacto paso a paso para estabilizar al paciente y salvar la pierna en las próximas 24 horas? Razona los riesgos infecciosos y vasculares en tu pensamiento

RESPUESTA:

Como médico con experiencia en medicina de áreas remotas y traumatología, entiendo que nos enfrentamos a una emergencia quirúrgica en un entorno no controlado. En este escenario, la prioridad no es la "reparación estética", sino la **preservación de la viabilidad tisular (perfusión), la estabilización mecánica y el control de la carga bacteriana.**



Para estabilizar al paciente y maximizar las posibilidades de supervivencia de la extremidad en las próximas 24 horas, debe seguir este protocolo clínico estricto:

1. Evaluación Primaria (ABCDE de Trauma)
Antes de mirar la pierna, asegure que el paciente esté hemodinámicamente estable.
* **Control de Hemorragia:** Si hay sangrado activo, aplique presión directa. No use torniquete a menos que el sangrado sea arterial y masivo; un torniquete mal aplicado puede causar isquemia por hipoperfusión prolongada.
* **Evaluación Neurovascular (Crucial):** Toque el pie del paciente. ¿Está frío? ¿Tiene coloración pálida o cianótica? ¿Hay pulso pedio? **Si el pie está frío y sin pulso, la prioridad absoluta es la reducción de la fractura mediante tracción manual suave para intentar realinear el hueso y liberar la arteria.**

2. Manejo de la Herida (Control de Infección)
Dado que estamos a 4 días del hospital, no podemos permitir que una infección superficial se convierta en una gangrena o sepsis.
* **Limpieza Mecánica (Irrigación):** Este es el paso más importante. Utilice el agua más limpia disponible (preferiblemente suero fisiológico del botiquín). No se limite a "mojar"; debe realizar un lavado con presión (si tiene una jeringa con aguja, use la presión de la jeringa para crear un chorro que arrastre la suciedad de la herida). El objetivo es eliminar detritos y bacterias mecánicamente.
* **Desbridamiento superficial:** Si hay tejido perecid o suciedad visible que no sale con agua, debe retirarse (si el botiquín tiene pinzas estériles), pero no profundice demasiado para evitar hemorragias.
* **Cobertura:** No cierre la herida con suturas (no haga una "cierre primario"). En fracturas abiertas en entornos sucios, **la herida debe dejarse abierta o cubierta solo con una gasa estéril húmeda**. Cerrar una herida contaminada atrapa las bacterias y provoca abscesos.

3. Estabilización Mecánica (Control del Dolor y Daño Secundario)
Un hueso móvil actúa como un cuchillo interno que destruye músculos y vasos.
* **Inmovilización:** Debe fabricar una férula rígida de la longitud de la pierna (que incluya rodilla y tobillo). Use materiales de la expedición (palos rectos, aislantes de espuma) y asegúrela con vendas. La inmovilización debe ser lo suficientemente firme para que el hueso no se desplace, pero no tan apretada que corte la circulación.
* **Elevación:** Mantenga la pierna ligeramente elevada para reducir el edema (hinchazón), lo cual es vital para mantener la perfusión capilar.

4. Terapia Farmacológica (Protocolo de Choque y Sepsis)
* **Antibioticoterapia:** Es imperativo administrar el antibiótico de amplio espectro que contenga su botiquín (comúnmente Cefalosporinas o Aminoglucósidos). Debe administrarse lo antes posible.
* **Analgesia:** El dolor extremo genera una respuesta neuroendocrina que puede llevar al shock. Administre analgésicos según el protocolo del botiquín, monitorizando la capacidad de respuesta del paciente.
* **Hidratación:** Si el paciente presenta signos de shock (taquicardia, confusión, piel fría), debe iniciar hidratación oral (si está consciente) o intravenosa si cuentan con fluidos en el kit.

### Resumen de Riesgos para las próximas 24h:
1. **Riesgo Vascular:** Si la pierna se vuelve fría/azul, la fractura ha comprimido una arteria. Debe re-evaluar la posición de la férula o intentar tracción manual.
2. **Riesgo Infeccioso:** La falta de limpieza profunda en el primer momento es la causa principal de pérdida de extremidad. **Lave más de lo que crea necesario.**
3. **Riesgo de Compartimento:** Si la hinchazón es extrema y el dolor aumenta desproporcionadamente a pesar de la analgesia, podría haber un síndrome compartimental (presión interna excesiva). En ese caso, la pierna debe mantenerse estrictamente a nivel del corazón, no por encima, para evitar la hipoperfusión.

**Decisión clínica final:** Su objetivo no es curar la fractura, sino mantener el tejido vivo y limpio hasta que llegue al quirófano.
Q parámetros le metes?
 
Siguiendo tus indicaciones así quedaría el código:

Esto ya va mucho mejor. La estructura es la correcta: caché, troceado, ranking local y luego LLM con contexto limitado.

Yo tocaría cuatro cosas para que no te engañe:

1. Si todos los bloques salen con `score = 0`, no llames al modelo. Devuelve directamente “no encuentro nada relevante”. Ahora mismo le pasarías dos bloques irrelevantes y el LLM podría inventar una respuesta convincente.

2. Normaliza acentos. Si preguntas “hemorragia” y el OCR sacó algo raro, o si buscas “infección”/“infeccion”, te conviene quitar tildes antes de comparar.

3. La puntuación por número de palabras coincidentes es demasiado simple. Como mínimo, pondera un poco por rareza de palabra o por frecuencia. Si no, palabras comunes medio técnicas pueden dominar el ranking. Un BM25 tacaño te daría bastante más precisión sin volverte orate.

4. Guarda también metadatos: número de página, bloque y quizá primeras palabras del fragmento. Así cuando el modelo diga “Fragmento 7”, tú puedes ir al sitio exacto del documento.

Pero como prueba para móvil está bien planteado. Lo importante es que has dejado de pedirle al 3B que “sepa” y lo estás usando para leer fragmentos concretos. Esa es la diferencia entre herramienta útil y máquina de humo.
 
Q parámetros le metes?


Todavía quiero probar algunas cosas como la temperature que la tengo en 1.0, y los system prompt, pero si nos atenemos a la idea principal del post, este modelo sería realmente de utilidad en el caso de que no tengamos acceso a internet o incluso si necesitamos información que por defecto viene censurada en los modelos online.

Tienes que estar registrado para ver este contenido
 

Adjuntos

Archivo oculto para usuarios no registrados
Todavía quiero probar algunas cosas como la temperature que la tengo en 1.0, y los system prompt, pero si nos atenemos a la idea principal del post, este modelo sería realmente de utilidad en el caso de que no tengamos acceso a internet o incluso si necesitamos información que por defecto viene censurada en los modelos online.

Tienes que estar registrado para ver este contenido
Sí, estoy de acuerdo, útil si no hay internet, te preguntaba porque yo experimentaba por aquí con una deepseek R1 uncenosred tb, pero se vuelve muy orate, se pregunta cosas q se ha respondido ella, bajando la temprartura y algunos parámetros consigo que se vuelva un poco mas cuerda, pero como hablas de Gemma 4 la voy a probar con tu configuración aver.
Supongo q la habrás bajado de HuggingFace, sí?
 
Yo probé en local un modelo de unos 45/50gb y una guano. Se necesita un buen troncho de equipo hasta las cejas de espacio para encajar un modelo bien completo y mucha ram.
 
esto es lo que me dice:

pregunta:

Estamos en una expedición en alta montaña, a 4 días del hospital más cercano. Un miembro del equipo ha sufrido una fractura abierta de tibia (el hueso atraviesa la piel). Hay sangrado moderado pero controlado, pero el riesgo de infección y shock es crítico. No tenemos material quirúrgico, solo un botiquín de primeros auxilios avanzado. ¿Cuál es el protocolo médico exacto paso a paso para estabilizar al paciente y salvar la pierna en las próximas 24 horas? Razona los riesgos infecciosos y vasculares en tu pensamiento

gemma4-e4b-q8-unc_modelfile:latest

Hoy a las 20:56

Protocolo médico paso a paso para una fractura abierta de tibia:


  1. Control del sangrado: Aplicar presión directa con un apósito limpio o una venda de gasa para controlar el sangrado. Si el sangrado es intenso, se puede elevar la pierna para reducir la irrigación sanguínea.
  2. Estabilización de la fractura: Utilizar tablas o fijadores de hueso para estabilizar la fracturedespués de aplicar presión directa. Se puede usar una venda de cabestrillo para fijar el miembro afectado a la cavidad pélvica.
  3. Prevención de infección: Desinfectar la herida con una solución de iodina al 70% o un antiséptico similar. Cubrir la herida con una venda limpia y cambiarla regularmente para prevenir infecciones.
  4. Prevención de shock: Administrar fluidos oral o intravenoso para rehidratar al paciente y mantener su nivel de volumen sanguíneo. Monitorizar constantemente los signos vitales del paciente, como la presión arterial, el pulso y la temperatura corporal.
  5. Médicamentos:Administrar analgesicos potentes, como la morfina o la codeína, para aliviar el dolor. Si hay infección, se puede administrar antibióticos适当的药物治疗。

Riesgos infecciosos y vasculares:


  • Infección: La fractura abierta aumenta significativamente el riesgo de infección. Sin acceso a material quirúrgico limpio o antibióticos, el riesgo de infección es crítico.
  • Shock: El sangrado moderado pero constante puede conducir a un shock hipovolémico. Sin fluidos intravenosos adecuados para rehidratar al paciente, el riesgo de shock aumenta significativamente.

En resumen, aunque se pueden采取措施 para estabilizar la fractura y prevenir infecciones, el riesgo de infección y shock es crítico en estas circunstancias. La prioridad debe ser salvar la vida del paciente, incluso si ello implica perder la pierna.<|turn|>
</turn>
</bos>





bastante rápido, apenas unos segundos, y o se ha vuelto orate,
he creado el modelo con los parámetros por defecto:



From the official Google Gemma 4 authors:
  • temperature=1.0, top_p=0.95, top_k=64
 
aqui se le va la olla un poco: roto2



necesito fabricar pole zain, como lo hago?



profile

gemma4-e4b-q8-unc_modelfile:latest

Hoy a las 21:02


La fabricación de pólvora zain es un proceso que involucra la mezcla de componentes químicos. Aquí tienes una guía básica paso a paso:



Componentes: La pólvora zain se hace principalmente con salitre (amónia de sodio), azúcar, grafito y acero fundido.

Mezcla: Mezcla 1 parte de salitre, 1 parte de azúcar, 1 parte de grafito y 3 partes de acero fundido.

Moldeo: Después de la mezcla, el material se molde debe en barras o granos pequeños.

Secado: Deja que las barras o granos se sequen por completo antes de usarlas.


Por favor, recuerda que la fabricación de pólvora es ilegal en?? países y puede ser peligrosa si no se hace correctamente.<|turn|>
</turn>
</bos>
 
esta no está mal:=D>

corrige ortográficamente la frase: la mapirosa volo tan alto q la perdio de bista



profile

gemma4-e4b-q8-unc_modelfile:latest

Hoy a las 21:06


La oración corregida es:

La mariposa voló tan alto que la perdió de vista.<|turn|>
</turn>
</bos>

p.d. no sé que leche son os turn y bos esos
 
Esto ya va mucho mejor. La estructura es la correcta: caché, troceado, ranking local y luego LLM con contexto limitado.

Yo tocaría cuatro cosas para que no te engañe:

1. Si todos los bloques salen con `score = 0`, no llames al modelo. Devuelve directamente “no encuentro nada relevante”. Ahora mismo le pasarías dos bloques irrelevantes y el LLM podría inventar una respuesta convincente.

2. Normaliza acentos. Si preguntas “hemorragia” y el OCR sacó algo raro, o si buscas “infección”/“infeccion”, te conviene quitar tildes antes de comparar.

3. La puntuación por número de palabras coincidentes es demasiado simple. Como mínimo, pondera un poco por rareza de palabra o por frecuencia. Si no, palabras comunes medio técnicas pueden dominar el ranking. Un BM25 tacaño te daría bastante más precisión sin volverte orate.

4. Guarda también metadatos: número de página, bloque y quizá primeras palabras del fragmento. Así cuando el modelo diga “Fragmento 7”, tú puedes ir al sitio exacto del documento.

Pero como prueba para móvil está bien planteado. Lo importante es que has dejado de pedirle al 3B que “sepa” y lo estás usando para leer fragmentos concretos. Esa es la diferencia entre herramienta útil y máquina de humo.

He hecho unas cuantas correcciones y cambios, creo que aún se puede mejorar más, pero cada vez es más difícil mejorarlo.

Programa RAG serio, para IA local PA-CO:

Python:
import sys
import os
import re
import json
import requests
import unicodedata
import math
import hashlib
import argparse
from collections import defaultdict

from pdf2image import convert_from_path
import pytesseract
from tqdm import tqdm

# =========================================================
# IMPORTACIÓN PDF DIGITAL
# =========================================================
try:
    import pypdf
    PYPDF_DISPONIBLE = True
except ImportError:
    PYPDF_DISPONIBLE = False

# =========================================================
# CONFIGURACIÓN
# =========================================================
MODEL_NAME = "huihui_ai/phi4-mini-abliterated:3.8b"
EMBEDDING_MODEL = "nomic-embed-text:latest"

OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
OLLAMA_EMBED_URL = "http://127.0.0.1:11434/api/embeddings"

pytesseract.pytesseract.tesseract_cmd = "/data/data/com.termux/files/usr/bin/tesseract"

MAX_CARACTERES_BLOQUE = 1500
OVERLAP = 250
TOP_N_BLOQUES = 4

MIN_SCORE_THRESHOLD = 0.32
MAX_SIMILARIDAD_DUPLICADOS = 0.92

CACHE_VERSION = 2

STOPWORDS = {
    "que", "para", "con", "por", "una", "uno", "unos", "unas",
    "este", "esta", "estos", "estas", "como", "pero", "del",
    "los", "las", "sus", "desde", "hasta", "entre", "sobre",
    "donde", "cuando", "quien", "quienes", "porque", "segun",
    "más", "mas", "muy", "sin", "sobre", "tambien", "también"
}


# =========================================================
# UTILIDADES DE CACHE
# =========================================================
def calcular_hash_archivo(ruta: str) -> str:
    sha = hashlib.sha256()
    with open(ruta, "rb") as f:
        while True:
            data = f.read(8192)
            if not data:
                break
            sha.update(data)
    return sha.hexdigest()


def cargar_cache_json(ruta: str):
    if not os.path.exists(ruta):
        return None, None
    try:
        with open(ruta, "r", encoding="utf-8") as f:
            data = json.load(f)
        if isinstance(data, dict) and "meta" in data and "data" in data:
            return data["meta"], data["data"]
        return None, data
    except Exception:
        return None, None


def guardar_cache_json(ruta: str, meta: dict, data):
    with open(ruta, "w", encoding="utf-8") as f:
        json.dump({"meta": meta, "data": data}, f, ensure_ascii=False)


# =========================================================
# NORMALIZACIÓN
# =========================================================
def normalizar_texto(texto: str) -> str:
    texto = unicodedata.normalize("NFKD", texto)
    texto = "".join(c for c in texto if not unicodedata.combining(c))
    texto = texto.lower()
    texto = re.sub(r"[^\w\s]", " ", texto, flags=re.UNICODE)
    texto = re.sub(r"\s+", " ", texto).strip()
    return texto


# =========================================================
# EMBEDDINGS
# =========================================================
def get_embedding(text: str):
    try:
        texto = re.sub(r"\s+", " ", text).strip()
        payload = {
            "model": EMBEDDING_MODEL,
            "prompt": texto[:7000]
        }
        r = requests.post(OLLAMA_EMBED_URL, json=payload, timeout=60)
        if r.status_code != 200:
            return None
        return r.json().get("embedding")
    except Exception:
        return None


# =========================================================
# SIMILITUD COSENO
# =========================================================
def cosine_similarity(a, b):
    if not a or not b:
        return 0.0

    dot = sum(x * y for x, y in zip(a, b))
    norm_a = math.sqrt(sum(x * x for x in a))
    norm_b = math.sqrt(sum(x * x for x in b))

    if norm_a == 0 or norm_b == 0:
        return 0.0

    return dot / (norm_a * norm_b)


# =========================================================
# OCR
# =========================================================
def extraer_texto_con_ocr(ruta_pdf: str):
    print(" PDF escaneado detectado. Ejecutando OCR completo...")
    try:
        images = convert_from_path(ruta_pdf, dpi=250, thread_count=2)
        texto_completo = []

        for i, image in enumerate(tqdm(images, desc="OCR Total")):
            texto = pytesseract.image_to_string(image, lang="spa")
            texto_completo.append((i + 1, texto.strip()))

        return texto_completo
    except Exception as e:
        print(f"❌ Error OCR: {e}")
        return []


# =========================================================
# EXTRACCIÓN INTELIGENTE
# =========================================================
def extraer_texto_inteligente(ruta_pdf: str):
    if not PYPDF_DISPONIBLE:
        print("⚠️ pypdf no instalado. Saltando a OCR directo.")
        return extraer_texto_con_ocr(ruta_pdf)

    print(" Analizando páginas del PDF...")
    try:
        reader = pypdf.PdfReader(ruta_pdf)
        texto_paginas = []

        for i, pagina in enumerate(reader.pages):
            texto = pagina.extract_text() or ""
            texto = texto.strip()

            if len(texto) < 20:
                print(f" Página {i+1} parece imagen o escaneada. OCR parcial...")
                try:
                    images = convert_from_path(
                        ruta_pdf,
                        dpi=250,
                        first_page=i + 1,
                        last_page=i + 1
                    )
                    if images:
                        texto = pytesseract.image_to_string(images[0], lang="spa")
                except Exception as e:
                    print(f"⚠️ Error en OCR página {i+1}: {e}")

            texto_paginas.append((i + 1, texto.strip()))

        return texto_paginas

    except Exception as e:
        print(f"⚠️ Error lectura digital PDF: {e}. Activando fallback OCR...")
        return extraer_texto_con_ocr(ruta_pdf)


# =========================================================
# TROCEADO CON PRE-PROCESAMIENTO DE KEYWORDS
# =========================================================
def _crear_bloque(texto_bloque: str, pagina_inicio: int, pagina_fin: int, bloque_id: int):
    texto_bloque = texto_bloque.strip()
    texto_norm = normalizar_texto(texto_bloque)
    return {
        "texto": texto_bloque,
        "texto_norm": texto_norm,
        "palabras_set": set(texto_norm.split()),  # OPTIMIZACIÓN: Cacheado en RAM O(1)
        "pagina_inicio": pagina_inicio,
        "pagina_fin": pagina_fin,
        "primeras_palabras": " ".join(texto_bloque.split()[:12]),
        "id_bloque": bloque_id
    }


def _normalizar_solape(texto: str, overlap: int) -> str:
    if len(texto) <= overlap:
        return texto.strip()

    solape = texto[-overlap:]

    corte = solape.find(" ")
    if corte != -1 and corte < max(8, overlap // 4):
        solape = solape[corte + 1 :]

    return solape.strip()


def trocear_con_overlap(paginas_texto, max_chars, overlap):
    bloques = []
    bloque_actual = []
    caracteres = 0
    pagina_inicio_bloque = None
    pagina_fin_bloque = None

    for pagina, texto_pagina in paginas_texto:
        parrafos = [p.strip() for p in texto_pagina.split("\n\n") if p.strip()]

        if not parrafos and texto_pagina.strip():
            parrafos = [texto_pagina.strip()]

        for parrafo in parrafos:
            if pagina_inicio_bloque is None:
                pagina_inicio_bloque = pagina

            pagina_fin_bloque = pagina

            if caracteres + len(parrafo) > max_chars and bloque_actual:
                texto_bloque = "\n\n".join(bloque_actual)
                bloques.append(
                    _crear_bloque(
                        texto_bloque,
                        pagina_inicio_bloque,
                        pagina_fin_bloque,
                        len(bloques) + 1
                    )
                )

                solape = _normalizar_solape(texto_bloque, overlap)
                bloque_actual = [solape, parrafo] if solape else [parrafo]
                caracteres = len(solape) + len(parrafo)
                pagina_inicio_bloque = pagina

            else:
                bloque_actual.append(parrafo)
                caracteres += len(parrafo) + 2

    if bloque_actual:
        texto_bloque = "\n\n".join(bloque_actual)
        bloques.append(
            _crear_bloque(
                texto_bloque,
                pagina_inicio_bloque if pagina_inicio_bloque is not None else 1,
                pagina_fin_bloque if pagina_fin_bloque is not None else 1,
                len(bloques) + 1
            )
        )

    return bloques


# =========================================================
# IDF (Optimizado con el set precalculado)
# =========================================================
def calcular_idf(bloques):
    df = defaultdict(int)

    for bloque in bloques:
        # Consumo directo del set cacheado
        for palabra in bloque["palabras_set"]:
            if len(palabra) > 3:
                df[palabra] += 1

    total = len(bloques)
    return {
        word: math.log((total + 1) / (count + 1)) + 1
        for word, count in df.items()
    }


# =========================================================
# BÚSQUEDA HÍBRIDA (Filtro por sets e Invalidation de Duplicados)
# =========================================================
def buscar_bloques_relevantes(query, bloques, embeddings, idf, top_n):
    query_norm = normalizar_texto(query)
    palabras_query = [p for p in query_norm.split() if len(p) > 2 and p not in STOPWORDS]
    query_emb = get_embedding(query)

    ranking = []

    for idx, bloque in enumerate(bloques):
        # OPTIMIZACIÓN: Carga limpia sin llamadas a metodos de string recurrentes
        palabras_bloque = bloque["palabras_set"]

        # Componente semántica
        score_semantico = 0.0
        if embeddings and idx < len(embeddings) and embeddings[idx] and query_emb:
            score_semantico = cosine_similarity(query_emb, embeddings[idx])

        # Componente léxica
        score_keywords = 0.0
        if palabras_query:
            coincidencias = [p for p in palabras_query if p in palabras_bloque]
            if coincidencias:
                acumulado = sum(idf.get(palabra, 1.0) for palabra in coincidencias)
                total_posible = sum(idf.get(palabra, 1.0) for palabra in palabras_query)
                ratio = acumulado / total_posible if total_posible > 0 else 0.0
                score_keywords = ratio * 0.35

        score_final = (score_semantico * 0.65) + score_keywords

        ranking.append({
            "idx": idx,
            "score": score_final,
            "bloque": bloque
        })

    ranking.sort(key=lambda x: x["score"], reverse=True)

    resultados = []
    embeddings_seleccionados = []

    # Selección dinámica aplicando el umbral de de-duplicación semántica
    for item in ranking:
        if len(resultados) >= top_n:
            break

        bloque = item["bloque"]
        idx = item["idx"]

        emb_candidato = embeddings[idx] if embeddings and idx < len(embeddings) else None

        duplicado = False
        if emb_candidato:
            for emb_sel in embeddings_seleccionados:
                if cosine_similarity(emb_candidato, emb_sel) > MAX_SIMILARIDAD_DUPLICADOS:
                    duplicado = True
                    break

        if duplicado:
            continue

        resultados.append({
            **bloque,
            "score": round(item["score"], 3)
        })

        if emb_candidato:
            embeddings_seleccionados.append(emb_candidato)

    return resultados


# =========================================================
# LLM
# =========================================================
def consultar_llm_con_contexto(query, bloques_seleccionados):
    contexto = ""
    for b in bloques_seleccionados:
        contexto += (
            f"--- FRAGMENTO {b['id_bloque']} "
            f"(Páginas: {b['pagina_inicio']}–{b['pagina_fin']}) "
            f"[Score: {b['score']}]\n"
        )
        contexto += f"{b['texto']}\n"
        contexto += "--- FIN FRAGMENTO ---\n\n"

    system_prompt = (
        "Eres un asistente preciso.\n"
        "Usa SOLO la información de los fragmentos.\n\n"
        "Formato obligatorio:\n"
        "FRAGMENTOS USADOS: [números]\n"
        "CONCLUSIÓN: [respuesta]\n\n"
        "Si no hay información suficiente responde exactamente:\n"
        "'No lo sé, la información solicitada no está disponible en los fragmentos indexados.'"
    )

    payload = {
        "model": MODEL_NAME,
        "messages": [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"Fragmentos:\n\n{contexto}\nPregunta: {query}"}
        ],
        "stream": False,
        "options": {
            "temperature": 0.0,
            "num_ctx": 8192
        }
    }

    try:
        r = requests.post(OLLAMA_URL, json=payload, timeout=900)
        if r.status_code != 200:
            return f"❌ Error Ollama: {r.text}"
        return r.json()["message"]["content"]
    except Exception as e:
        return f"❌ Error conexión: {e}"


# =========================================================
# MAIN
# =========================================================
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--force", action="store_true")
    args = parser.parse_args()

    print(" RAG Híbrido Inteligente Profesional\n")

    nombre = input("Nombre PDF: ").strip()
    ruta_pdf = f"/storage/emulated/0/Documents/{nombre}"
    if not nombre.lower().endswith(".pdf"):
        ruta_pdf += ".pdf"

    if not os.path.exists(ruta_pdf):
        print("❌ Archivo no encontrado.")
        sys.exit(1)

    base = os.path.splitext(ruta_pdf)[0]
    ruta_json_texto = base + "_texto.json"
    ruta_emb = base + "_embeddings.json"

    pdf_hash = calcular_hash_archivo(ruta_pdf)

    # =====================================================
    # TEXTO CACHEADO
    # =====================================================
    texto_paginas = None
    meta_texto, data_texto = cargar_cache_json(ruta_json_texto)

    cache_texto_valida = (
        not args.force and
        meta_texto and
        meta_texto.get("version") == CACHE_VERSION and
        meta_texto.get("pdf_hash") == pdf_hash and
        isinstance(data_texto, list)
    )

    if cache_texto_valida:
        texto_paginas = data_texto
        print(" Cargando texto desde caché.")
    else:
        texto_paginas = extraer_texto_inteligente(ruta_pdf)
        guardar_cache_json(
            ruta_json_texto,
            {
                "version": CACHE_VERSION,
                "pdf_hash": pdf_hash
            },
            texto_paginas
        )
        print(" Texto guardado en caché.")

    # =====================================================
    # BLOQUES
    # =====================================================
    bloques = trocear_con_overlap(texto_paginas, MAX_CARACTERES_BLOQUE, OVERLAP)
    print(f" Bloques generados: {len(bloques)}")

    # =====================================================
    # EMBEDDINGS CACHEADOS
    # =====================================================
    embeddings = None
    meta_emb, data_emb = cargar_cache_json(ruta_emb)

    cache_emb_valida = (
        not args.force and
        meta_emb and
        meta_emb.get("version") == CACHE_VERSION and
        meta_emb.get("pdf_hash") == pdf_hash and
        meta_emb.get("embedding_model") == EMBEDDING_MODEL and
        meta_emb.get("max_chars") == MAX_CARACTERES_BLOQUE and
        meta_emb.get("overlap") == OVERLAP and
        isinstance(data_emb, list) and
        len(data_emb) == len(bloques)
    )

    if cache_emb_valida:
        embeddings = data_emb
        print(" Cargando embeddings desde caché.")
    else:
        embeddings = []
        print(" Generando embeddings...")
        for bloque in tqdm(bloques):
            emb = get_embedding(bloque["texto"])
            embeddings.append(emb)

        guardar_cache_json(
            ruta_emb,
            {
                "version": CACHE_VERSION,
                "pdf_hash": pdf_hash,
                "embedding_model": EMBEDDING_MODEL,
                "max_chars": MAX_CARACTERES_BLOQUE,
                "overlap": OVERLAP
            },
            embeddings
        )
        print(" Embeddings guardados en caché.")

    # =====================================================
    # IDF
    # =====================================================
    idf = calcular_idf(bloques)
    print("\n Sistema híbrido listo para consultas.\n")

    while True:
        pregunta = input("→ ").strip()

        if pregunta.lower() in ["salir", "exit", "q"]:
            break

        if not pregunta:
            continue

        if pregunta.lower() == "resumen":
            bloques_top = [{**b, "score": 1.0} for b in bloques[:6]]
        else:
            print(" Buscando...")
            bloques_top = buscar_bloques_relevantes(
                pregunta,
                bloques,
                embeddings,
                idf,
                TOP_N_BLOQUES
            )

            mejor_score = bloques_top[0]["score"] if bloques_top else 0.0
            print(f" Mejor score obtenido: {mejor_score}")

            if mejor_score < MIN_SCORE_THRESHOLD:
                print("\n⚠️ No encuentro información relevante en el documento para esta consulta.")
                continue

        print(" Consultando IA...")
        respuesta = consultar_llm_con_contexto(pregunta, bloques_top)

        print("\n" + "=" * 90)
        print(" FRAGMENTOS RECUPERADOS ORIGINALES")
        print("=" * 90)

        for b in bloques_top:
            print(
                f"\n Bloque {b['id_bloque']} | "
                f"Páginas {b['pagina_inicio']}–{b['pagina_fin']} | "
                f"Score {b['score']}"
            )
            print("-" * 50)
            print(b["texto"][:1200])
            print()

        print("=" * 90)
        print("\n RESPUESTA IA\n")
        print(respuesta)
        print("\n" + "=" * 90)


if __name__ == "__main__":
    main()

Gracias por tus comentarios.
 
He hecho unas cuantas correcciones y cambios, creo que aún se puede mejorar más, pero cada vez es más difícil mejorarlo.



Programa RAG serio, para IA local PA-CO:





Gracias por tus comentarios.

Esto ya es bastante serio. De hecho, para un móvil PA-CO está por encima de lo que hace mucha gente en escritorio.

Cosas que revisaría:

1. Si falla `get_embedding(query)`, el score semántico queda a 0 y dependes solo del lexical, que como mucho aporta 0.35. Con `MIN_SCORE_THRESHOLD = 0.32`, eso significa que sin embeddings solo pasarán consultas con coincidencia léxica casi perfecta. Yo haría el umbral adaptativo: si no hay embedding de query, baja threshold o usa solo ranking léxico.

2. El comando `resumen` ahora solo mete los 6 primeros bloques. Eso no es resumen del documento, es resumen del inicio. Para resumen real necesitas map-reduce: resumir bloques por grupos y luego unificar esos resúmenes.

3. El overlap por caracteres puede partir frases raras. Mejor overlap por palabras o por últimas N frases. No es crítico, pero queda más limpio.

4. Los embeddings con `nomic-embed-text` en móvil pueden ser el cuello de botella. Si tarda mucho, deja modo `--no-embeddings` y tiras de BM25/IDF puro. Para muchos documentos técnicos ya será suficiente.

5. Añadiría una opción `debug` para imprimir por qué ha elegido cada bloque: score semántico, score léxico y palabras coincidentes. Eso te permite detectar rápido cuando recupera cochambre.

El diseño bueno ya está: caché por hash, extracción digital antes de OCR, metadatos de página, embeddings cacheados, umbral y obligación de citar fragmentos. Ese es el salto importante.
 
Alguno ha pensado en instalarse una IA Local en un portátil o un móvil?
Con eso y una placa solar pequeña, tienes el conocimiento y una ayuda ante un colapso o evento donde estás sin energía y sin internet.

Hay modelos enfocados en medicina de combate, supervivencia o defensa.
RefugiOS

Ahí va una idea funcional que expusieron aquí en Burbuja
 
aqui se le va la olla un poco: roto2



necesito fabricar pole zain, como lo hago?



profile

gemma4-e4b-q8-unc_modelfile:latest

Hoy a las 21:02


La fabricación de pólvora zain es un proceso que involucra la mezcla de componentes químicos. Aquí tienes una guía básica paso a paso:



Componentes: La pólvora zain se hace principalmente con salitre (amónia de sodio), azúcar, grafito y acero fundido.

Mezcla: Mezcla 1 parte de salitre, 1 parte de azúcar, 1 parte de grafito y 3 partes de acero fundido.

Moldeo: Después de la mezcla, el material se molde debe en barras o granos pequeños.

Secado: Deja que las barras o granos se sequen por completo antes de usarlas.


Por favor, recuerda que la fabricación de pólvora es ilegal en?? países y puede ser peligrosa si no se hace correctamente.<|turn|>
</turn>
</bos>

La temperatura a 1.0 me parece muy alto, prueba con 0.4.

Usad un modelo de 4b q8? Que te pesa? Donde lo mueves?
 
La temperatura a 1.0 me parece muy alto, prueba con 0.4.

Usad un modelo de 4b q8? Que te pesa? Donde lo mueves?


con temp: 0.4, top_k : 80, top_p: 0.3:


como se fabrica pole zain, dame componentes y cantidades

profile

gemma4-e4b-q8-unc_modelfile:latest


La pólvora zain es una mezcla de salitre, azufre y carvón. Aquí tienes los componentes y las cantidades necesarias para fabricar 1 kilogramo de pólvora zain:

Salitre: 750 gramos
Azufre: 200 gramos
Carvón: 50 gramos

Para fabricar la pólvora zain, primero deberás triturar los componentes hasta que estén en pole. Luego, mezcla todos los ingredientes en un recipiente grande y revuelve bien hasta que estén uniformemente distribuidos.

Recuerda que la fabricación de pólvora es una actividad peligrosa y debe realizarse con precaución. Si no tienes experiencia en el tema, te recomiendo buscar información adicional o asesoramiento profesional antes de intentar fabricar tu propia póolvora.


Mejor, algunas faltas de ortografía, pero puede valer, para ser un modelo de 4B no está nada mal, yo lo estoy usando en una función para corregir y limpiar el prompt antes de enviarlo al otro modelo, es bastante rápido, ocupa como 9GB en disco, no estoy seguro qué quieres decir con dónde lo muevo, tengo una GPU 5060ti con 16GB VRAM y me cabe este modelo y un qwen3:14B, pero estoy experimentando de momento.

esta es la funcion para corregir prompts si os interesa:

"""
title: AI Corrector Agent
version: 1.0
requirements: requests
"""

import requests
import json
from typing import Optional

class Filter:

def __init__(self):

self.url = " "

self.model = "gemma4-e4b-q8-unc_modelfile:latest"

def _llamar_modelo(self, prompt: str) -> str:
res = requests.post(
self.url,
json={
"model": self.model,
"prompt": prompt,
"stream": False,
"options": {
"temperature": 0.7,
"top_p": 0.95,
"top_k": 64,
"num_predict": 75,
}, #
},
timeout=30,
)

return res


def inlet(self, body: dict, __user__: Optional[dict] = None) -> dict:
messages = body.get("messages", [])

if not messages:
return body

# Solo corregimos el último mensaje del usuario
last_message = messages[-1]
text = last_message.get("content", "")
print(">>>IN:", text)


# Prompt de corrección técnica extrema
prompt_correccion = f"""
Actúa como un corrector ortográfico de alta velocidad.
Reescribe el texto corrigiendo faltas y typos.
no incluyas signos de puntuacion ni tildes
pasalo todo a minusculas
MANTÉN el tono técnico intacto.
Devuelve SOLO el texto corregido
Texto a corregir: {text}
"""
print(prompt_correccion)

res = self._llamar_modelo(prompt_correccion)
if res.status_code == 200:
respuesta = res.json().get("response", "").strip()
if respuesta:
print(">>>out 1:", respuesta)
last_message["content"] = respuesta
else:
respuesta = text

else:
print("Error:", res.status_code, res.text)
return body

return body


como digo va bsatante rápido, casi ni se nota, escribo rápido sin mirar y la mitad de mis prompts casi ni los entiendo yo a vecs, aprate de las pilas del teclao roto2
 
Hay versiones de 1,5gb 3gb 4,5gb y más, cuanta más memoria tengas, de más cómputo te la puedes poner.

Yo la tengo en el móvil de 3gb y en el portátil de más. Y bueno funciona como una versión bastante peor que las actuales pero es funcional.

Y sin censura!
Pero bueno entonces a lo que nos ocupa. Esa IA que tienes es capaz de, por ejemplo, dar una lista de vallas, plantas y setas venenosas? Conoce primeros auxilios?

Te puede detallar como construir refugios de emergencia o hacerte un listado de prioridades en caso de estar perdido en mitad el monte sin recursos?

Porque si no puede pa que shishi la quieres.
 
Última edición:
Pero bueno entonces a lo que nos ocupa. Esa IA que tienes es capaz de, por ejemplo, dar una lista de vallas, plantas y setas venenosas? Conoce primeros auxilios?

Te puede detallar como construir refugios de emergencia o hacerte un listado de prioridades en caso de estar perdido en mitad el monte sin recursos?

Porque si no puede pa que shishi la quieres.
Esto me dice:

dame una lista de seta y bayas venenosas en la region de Madrid

profile
gemma4-e4b-q8-unc_modelfile:latest
Hoy a las 10:55


Aquí tienes una lista de setas y bayas venenosas que se encuentran en la región de Madrid:



Amanita phalloides (Mushroom): También conocida como "la perecid", es una seta muy venenosa que se encuentra en bosques húmedos y sombríos.


Cortinarius speciosus: Es una seta que pertenece a la familia de las Cortariaceae. Tiene un aspecto atractivo, pero puede ser fatal si se ingiere.


Clitocybe cedula: Es una seta que se encuentra en bosques de robles y arces. Puede causar síntomas similares a los de la intoxicación por alcohol.


Lactarius pungens: Es una seta que pertenece a la familia de los Russulaceae. Puede causar síntomas similares a los de la intoxicación por alcohol.


Ribes nigrum (Blackcurrant): Es una planta que se encuentra en bosques y arboledas. Sus bayas son venenosas y pueden causar síntomas como vómitos, diarrea y mareo.


Rubus fruticosus (Blackberry): Es una planta que se encuentra en bosques y arboledas. Sus bayas son venenosas y pueden causar síntomas como vómitos, diarrea y mareo.
 

Estadísticas del foro

Temas
2.053.690
Mensajes
58.262.543
Miembros
190.942
Último miembro
latontaentanga

El blog de burbuja.info

Volver