informe-claro / app.py
aitziberluis's picture
Upload 3 files
46e2861 verified
Raw
History Blame Contribute Delete
6.9 kB
"""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())