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 archivo de texto, por ejemplo, busca en el documento como parar una hemorragia arterial.

Da igual que solo sea un 3b, buscará en el documento PDF aunque no esté en OCX el PDF, el script lo pasará a OCX para que la IA pueda leerlo.

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"

def extraer_texto_con_ocr(ruta_pdf: str, max_paginas: int = None):
    print(" Convirtiendo PDF a imágenes para OCR...")
    try:
        # Convertir PDF a imágenes
        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')  # 'spa' = español
            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 resumir_con_phi4(texto: str, ruta_prompt: str):
    # Intentar leer el prompt personalizado desde el archivo de texto
    if os.path.exists(ruta_prompt):
        print(f" Cargando instrucciones personalizadas desde: {ruta_prompt}")
        with open(ruta_prompt, 'r', encoding='utf-8') as f:
            prompt_base = f.read().strip()
    else:
        # Prompt de emergencia por si el archivo no existe o no se encuentra
        print("⚠️ Archivo de instrucciones no encontrado. Usando prompt de resumen por defecto.")
        prompt_base = "Haz un resumen completo y bien estructurado del siguiente documento en español."

    payload = {
        "model": MODEL_NAME,
        "messages": [
            {"role": "system", "content": "Eres un asistente científico y técnico experto en análisis de documentos. Sigue estrictamente las instrucciones del usuario."},
            {"role": "user", "content": f"{prompt_base}\n\nDocumento:\n{texto[:100000]}"}
        ],
        "stream": False,
        "options": {"temperature": 0.3, "num_ctx": 5000}
    }

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


if __name__ == "__main__":
    # 1. Gestión del archivo 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. Gestión del archivo de instrucciones (Prompt)
    print(f"\n Por defecto buscaré las instrucciones en: {RUTA_PROMPT_DEFECTO}")
    cambiar_prompt = input("¿Quieres usar otro archivo .txt para las instrucciones? (S/N): ").strip().lower()
    
    if cambiar_prompt == 's':
        nombre_prompt = input("Nombre del archivo TXT (en carpeta Documents): ").strip()
        ruta_prompt = f"/storage/emulated/0/Documents/{nombre_prompt}"
        if not ruta_prompt.lower().endswith(".txt"):
            ruta_prompt += ".txt"
    else:
        ruta_prompt = RUTA_PROMPT_DEFECTO

    print(f"\n Procesando PDF: {ruta_pdf}")
    
    texto = extraer_texto_con_ocr(ruta_pdf)
    
    if len(texto.strip()) < 200:
        print("⚠️ Muy poco texto extraído. Revisa que el PDF tenga texto legible.")
    else:
        resultado = resumir_con_phi4(texto, ruta_prompt)
        print("="*70)
        print("RESULTADO DEL PROCESAMIENTO (PHI-4 ABLITERATED)")
        print("="*70)
        print(resultado)
        print("="*70)

El segundo script, te pregunta que es lo que quieres saber, y después el número de iteraciones. Lo que hara será masticar su respuesta para afinarla más y mejor, razonar sobre su respuesta y dar más calidad en la salida y conclusión final. Dado que un 3b es un poco simple.

Python:
import requests
import sys
import time

# ================== CONFIGURACIÓN ==================
MODEL_NAME = "huihui_ai/phi4-mini-abliterated:3.8b"
OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
# ===================================================

def ejecutar_bucle_razonamiento(problema_inicial: str, max_iteraciones: int):
    # Inicializamos el historial con el rol del sistema y el problema del usuario
    historial_mensajes = [
        {
            "role": "system",
            "content": (
                "Eres un motor de razonamiento avanzado y autocrítico. Tu objetivo es resolver "
                "problemas complejos dividiendo tu pensamiento en pasos técnicos, analizando "
                "tus propios errores del pasado y refinando la solución en cada turno."
            )
        },
        {
            "role": "user",
            "content": f"Este es el problema a resolver. Da tu primer enfoque o hipótesis inicial:\n\n{problema_inicial}"
        }
    ]

    print("\n" + "="*60)
    print(" INICIANDO BUCLE DE RAZONAMIENTO ITERATIVO")
    print("="*60)
    
    tiempo_inicio = time.time()

    for i in range(1, max_iteraciones + 1):
        print(f"\n [ITERACIÓN {i} de {max_iteraciones}] El modelo está pensando...")
        
        payload = {
            "model": MODEL_NAME,
            "messages": historial_mensajes,
            "stream": False,
            "options": {
                "temperature": 0.5,  # Temperatura media para permitir enfoques alternativos
                "num_ctx": 8192      # Contexto expandido para recordar las iteraciones previas
            }
        }

        try:
            response = requests.post(OLLAMA_URL, json=payload, timeout=300)
            if response.status_code != 200:
                print(f"❌ Error en Ollama: {response.status_code}")
                break
                
            # Extraemos la respuesta del pensamiento actual
            respuesta_modelo = response.json()['message']['content']
            
            print(f"\n--- SALIDA DEL RAZONAMIENTO (PASO {i}) ---")
            print(respuesta_modelo)
            print("-" * 40)

            # Guardamos lo que pensó el modelo en el historial
            historial_mensajes.append({"role": "assistant", "content": respuesta_modelo})

            # Si es la última iteración, no le pedimos que vuelva a pensar, rompemos el ciclo
            if i == max_iteraciones:
                break

            # Inyectamos el prompt de auto-corrección para la siguiente iteración
            prompt_autocritica = (
                f"Analiza de forma crítica tu razonamiento del paso {i}. "
                "Encuentra posibles fallos lógicos, sesgos, asunciones incorrectas o datos omitidos. "
                "A raíz de tu autocrítica, genera un nuevo razonamiento superador que corrija esos errores "
                "y te acerque más a la solución matemática o técnica exacta."
            )
            historial_mensajes.append({"role": "user", "content": prompt_autocritica})

        except Exception as e:
            print(f"❌ Error de conexión: {e}")
            break

    # ================= FASE FINAL: RESPUESTA DEFINITIVA =================
    print("\n [FASE FINAL] Generando la solución depurada definitiva...")
    prompt_final = (
        "Tomando en cuenta todo tu proceso de pensamiento, tus autocríticas y las correcciones "
        "anteriores, escribe la solución final, limpia, estructurada y definitiva al problema original."
    )
    historial_mensajes.append({"role": "user", "content": prompt_final})

    try:
        payload["messages"] = historial_mensajes
        payload["options"]["temperature"] = 0.1 # Temperatura baja para máxima precisión al cerrar
        
        response = requests.post(OLLAMA_URL, json=payload, timeout=300)
        solucion_final = response.json()['message']['content']
        
        tiempo_total = time.time() - tiempo_inicio
        
        print("\n" + "="*70)
        print(f"✨ SOLUCIÓN FINAL CONVERGENTE (Tiempo total: {tiempo_total:.2f}s)")
        print("="*70)
        print(solucion_final)
        print("="*70)

    except Exception as e:
        print(f"❌ Error al generar la conclusión final: {e}")


if __name__ == "__main__":
    print("--- MOTOR DE PENSAMIENTO AUTÓNOMO LOCAL ---")
    problema = input("Introduce el problema, código o pregunta compleja:\n> ").strip()
    
    try:
        iteraciones = int(input("\n¿Cuántas iteraciones de pensamiento deseas (ej. 3 o 4)?:\n> "))
    except ValueError:
        print("Entrada no válida. Usando 3 iteraciones por defecto.")
        iteraciones = 3

    if problema:
        ejecutar_bucle_razonamiento(problema, iteraciones)
    else:
        print("No introdujiste ningún problema.")

De nada

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:
(adjunto visible para usuarios registrados)(adjunto visible para usuarios registrados)(adjunto visible para usuarios registrados)
 

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)
 
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 bastante, he probado varios modelos, hasta me he bajado el millon de vectores en inglés 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 malo 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:

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)

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.

(adjunto visible para usuarios registrados)
 

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.

(adjunto visible para usuarios registrados)
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 gran decepción. Se necesita un buen 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
 
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é qué son los turn y bos esos
 
(mensaje citado en revisión)

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
 
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 carbón. Aquí tienes los componentes y las cantidades necesarias para fabricar 1 kilogramo de pólvora zain:

Salitre: 750 gramos
Azufre: 200 gramos
Carbó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ólvora.


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 bastante rápido, casi ni se nota, escribo rápido sin mirar y la mitad de mis prompts casi ni los entiendo yo a veces, aparte de las pilas del teclado roto
 
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 qué 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 qué 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.054.093
Mensajes
58.275.080
Miembros
190.948
Último miembro
christina.damgaard

El blog de burbuja.info

Volver