"""Informe Claro — traduce informes médicos a lenguaje llano. Pega el texto de un informe (o sube un PDF) y obtén una explicación en español corriente: qué dice el documento, un glosario de términos y preguntas útiles para llevar a la consulta. Esto es una ayuda de LECTURA. No interpreta clínicamente, no diagnostica y no sustituye a un médico. Ejecutar en local: python app.py (requiere GROQ_API_KEY) """ import gradio as gr from pypdf import PdfReader from translator import llm_available, translate_report PRIVACY_WARNING = ( "Antes de pegar un informe, borra los datos identificativos (nombre, DNI, " "número de historia). El texto se envía a la API de Groq para procesarlo." ) MEDICAL_DISCLAIMER = ( "Esta herramienta explica el lenguaje del informe, nada más. No es consejo " "médico, puede contener errores y no sustituye la interpretación de un " "profesional. Las decisiones sobre tu salud, siempre con tu médico." ) EXAMPLE_REPORT = """INFORME DE RESULTADOS - ANALITICA DE SANGRE Hemograma: Hematies 4.2 mill/mm3 (VN 4.5-5.9). Hemoglobina 12.8 g/dL (VN 13.5-17.5). Hematocrito 38% (VN 41-53). VCM 91 fL (VN 80-100). Leucocitos 6.800/mm3 (VN 4.000-11.000). Plaquetas 245.000/mm3 (VN 150.000-450.000). Bioquimica: Glucosa basal 108 mg/dL (VN 70-100). Creatinina 0.9 mg/dL (VN 0.7-1.3). Filtrado glomerular estimado (CKD-EPI) >90 mL/min. Colesterol total 215 mg/dL (VN <200). LDL 142 mg/dL (VN <130). HDL 48 mg/dL (VN >40). Trigliceridos 125 mg/dL (VN <150). GOT 22 U/L, GPT 28 U/L (VN <40). TSH 2.1 mUI/L (VN 0.4-4.5). Ferritina 18 ng/mL (VN 30-400). Conclusion: Anemia leve normocitica con ferropenia. Glucemia basal alterada. Dislipemia leve. Se recomienda valoracion por su medico de atencion primaria.""" EXAMPLE_MRI = """RESONANCIA MAGNETICA DE COLUMNA LUMBAR Tecnica: Secuencias sagitales T1, T2 y STIR; axiales T2 de L3 a S1. Hallazgos: Rectificacion de la lordosis lumbar fisiologica. Adecuada alineacion de los cuerpos vertebrales, sin listesis. Altura de los cuerpos vertebrales conservada. Cambios degenerativos discales tipo deshidratacion en L4-L5 y L5-S1. En L4-L5 se observa protrusion discal circunferencial con predominio posterocentral, que contacta con el saco tecal sin evidencia de compromiso radicular significativo. Discreta hipertrofia de facetas articulares. En L5-S1, protrusion focal paracentral izquierda que contacta con la raiz S1 izquierda en su trayecto intracanal, sin claros signos de compresion. Canal lumbar de amplitud conservada. Conos medular de morfologia y senal normales, finalizando a nivel de L1. Conclusion: Discopatia degenerativa L4-L5 y L5-S1 con protrusiones discales, la de L5-S1 en contacto con la raiz S1 izquierda. Sin estenosis de canal. Se recomienda correlacion clinica.""" EXAMPLE_DISCHARGE = """INFORME DE ALTA - SERVICIO DE URGENCIAS Motivo de consulta: Dolor toracico de 3 horas de evolucion. Enfermedad actual: Paciente que acude por dolor centrotoracico opresivo, no irradiado, de inicio en reposo, sin cortejo vegetativo acompanante. No disnea, no palpitaciones. Refiere episodio similar hace un mes autolimitado. Exploracion: Consciente y orientado. Eupneico en reposo. TA 132/84 mmHg, FC 78 lpm, SatO2 98% basal. Auscultacion cardiaca ritmica sin soplos. Auscultacion pulmonar con murmullo vesicular conservado. No edemas. Pruebas complementarias: ECG en ritmo sinusal a 76 lpm, sin alteraciones agudas de la repolarizacion. Troponina T ultrasensible seriada: 6 ng/L y 7 ng/L (VN <14). Radiografia de torax sin hallazgos patologicos agudos. Juicio clinico: Dolor toracico de perfil no coronario, probablemente osteomuscular. Sindrome coronario agudo descartado. Tratamiento al alta: Paracetamol 1 g cada 8 horas si dolor, maximo 5 dias. Control por su medico de atencion primaria. Volver a urgencias si el dolor cambia de caracteristicas, se irradia a brazo o mandibula, o asocia sudoracion o falta de aire.""" def read_pdf(file_path: str) -> str: reader = PdfReader(file_path) return "\n".join(page.extract_text() or "" for page in reader.pages) def analyze(report_text: str, pdf_file, level: str): text = (report_text or "").strip() if pdf_file is not None: try: text = read_pdf(pdf_file).strip() except Exception as e: raise gr.Error(f"No se pudo leer el PDF: {e}") if not text: raise gr.Error( "El PDF no contiene texto extraíble (probablemente es un escaneo). " "Copia el texto a mano en el cuadro de la izquierda." ) if not text: raise gr.Error("Pega el texto del informe o sube un PDF.") if len(text.split()) < 15: raise gr.Error("El texto es demasiado corto para ser un informe.") try: return translate_report(text, level) except RuntimeError as e: raise gr.Error(str(e)) with gr.Blocks(title="Informe Claro") as demo: gr.Markdown("# Informe Claro") gr.Markdown( "Traduce un informe médico a lenguaje llano: qué dice, glosario de " "términos y preguntas para llevar a tu médico." ) gr.Markdown(f"**Privacidad:** {PRIVACY_WARNING}") if not llm_available(): gr.Markdown( "*Aviso: no hay GROQ_API_KEY configurada — la app no podrá analizar " "informes hasta que la configures (gratis en console.groq.com).*" ) with gr.Row(): with gr.Column(scale=1): report_input = gr.Textbox( lines=16, label="Texto del informe", placeholder="Pega aquí el informe (sin datos personales)...", ) pdf_input = gr.File( label="... o sube un PDF (si subes PDF, se usa el PDF)", file_types=[".pdf"], type="filepath", ) level_input = gr.Radio( choices=["Sencillo", "Intermedio"], value="Sencillo", label="Nivel de la explicación", ) analyze_button = gr.Button("Explicar en llano", variant="primary") gr.Examples( examples=[[EXAMPLE_REPORT], [EXAMPLE_MRI], [EXAMPLE_DISCHARGE]], example_labels=[ "Analítica de sangre", "Resonancia de columna lumbar", "Alta de urgencias por dolor torácico", ], inputs=report_input, label="Prueba con un ejemplo (todos con datos ficticios)", ) with gr.Column(scale=2): output = gr.Markdown(label="Explicación") analyze_button.click( analyze, inputs=[report_input, pdf_input, level_input], outputs=output, ) gr.Markdown(f"---\n*{MEDICAL_DISCLAIMER}*") if __name__ == "__main__": demo.launch(theme=gr.themes.Soft())