| import gradio as gr |
| import pandas as pd |
| import tempfile |
|
|
| def clasificar_her2(ratio, her2_por_celula): |
| if ratio >= 2.0 and her2_por_celula >= 4.0: |
| return 1, "POSITIVO", "RATIO ≥2.0 y HER2 ≥4. Amplificación verdadera.", "pos" |
| if ratio >= 2.0 and her2_por_celula < 4.0: |
| return 2, "NEGATIVO", "RATIO ≥2.0 pero HER2 <4. Monosomía 17.", "neg" |
| if ratio < 2.0 and her2_por_celula >= 6.0: |
| return 3, "POSITIVO", "HER2 ≥6 con RATIO <2. Amplificación verdadera.", "pos" |
| if ratio < 2.0 and 4.0 <= her2_por_celula < 6.0: |
| return 4, "NEGATIVO (salvo IHQ 3+)", "HER2 4–6 con RATIO <2. Correlación necesaria.", "border" |
| if ratio < 2.0 and her2_por_celula < 4.0: |
| return 5, "NEGATIVO", "RATIO <2 y HER2 <4. No amplificado.", "neg" |
| return '-', 'PENDIENTE', '-', 'muted' |
|
|
|
|
| def pill_html(text, status): |
| palette = { |
| 'pos': {'bg': '#E6F4EA', 'fg': '#1F8F3E', 'bd': '#A5D6A7'}, |
| 'neg': {'bg': '#FDECEA', 'fg': '#C62828', 'bd': '#EF9A9A'}, |
| 'border': {'bg': '#FFF4E5', 'fg': '#EF6C00', 'bd': '#FFCC80'}, |
| 'muted': {'bg': '#F3F4F6', 'fg': '#374151', 'bd': '#E5E7EB'}, |
| } |
| c = palette.get(status, palette['muted']) |
| return f"<span style='padding:6px 10px;border-radius:8px;border:1px solid {c['bd']};background:{c['bg']};color:{c['fg']};font-weight:600;'>{text}</span>" |
|
|
|
|
| def validate_signal(value, name): |
| if value is None: |
| return False, f"{name}: valor vacío.", None |
| try: |
| iv = int(value) |
| except: |
| return False, f"{name}: debe ser entero (0–20).", None |
| if iv < 0 or iv > 20: |
| return False, f"{name}: fuera de rango (0–20).", None |
| return True, '', iv |
|
|
|
|
| def dataframe_from_rows(rows): |
| return pd.DataFrame(rows) if rows else pd.DataFrame(columns=['HER2', 'CEN17']) |
|
|
|
|
| def recomputar(rows, objetivo): |
| df = dataframe_from_rows(rows) |
| n = len(df) |
| aviso = '' |
|
|
| if n > 0: |
| her2_mean = df['HER2'].mean() |
| cen17_mean = df['CEN17'].mean() |
| ratio = her2_mean / cen17_mean if cen17_mean > 0 else 0 |
| if cen17_mean == 0: |
| aviso = 'Advertencia: CEN17 media = 0; ratio no evaluable.' |
| else: |
| her2_mean = cen17_mean = ratio = 0 |
|
|
| if n >= objetivo and cen17_mean > 0: |
| grupo, interpretacion, comentario, status = clasificar_her2(ratio, her2_mean) |
| int_html = pill_html(interpretacion, status) |
| else: |
| grupo = '-' |
| comentario = '-' |
| int_html = pill_html('PENDIENTE (requiere completar células)', 'muted') |
|
|
| progreso = f"{n}/{objetivo}" |
|
|
| return rows, df, progreso, round(her2_mean,3), round(cen17_mean,3), round(ratio,3), grupo, int_html, comentario, aviso |
|
|
|
|
| def agregar_contaje(her2, cen17, rows, objetivo): |
| rows = rows or [] |
| ok1, msg1, v1 = validate_signal(her2, 'HER2') |
| ok2, msg2, v2 = validate_signal(cen17, 'CEN17') |
| if not ok1 or not ok2: |
| aviso = ' ; '.join(m for m in [msg1, msg2] if m) |
| r = list(recomputar(rows, objetivo)) |
| r[-1] = aviso |
| return tuple(r) |
| rows.append({'HER2': v1, 'CEN17': v2}) |
| return recomputar(rows, objetivo) |
|
|
|
|
| def borrar_ultima(rows, objetivo): |
| rows = rows or [] |
| if rows: |
| rows = rows[:-1] |
| return recomputar(rows, objetivo) |
|
|
|
|
| def borrar_todas(rows, objetivo): |
| rows = [] |
| r = list(recomputar(rows, objetivo)) |
| r[-1] = 'Se han borrado todas las células.' |
| return tuple(r) |
|
|
|
|
| def generar_resumen(rows, objetivo): |
| rows, df, progreso, h, c, r, g, i, co, av = recomputar(rows, objetivo) |
| texto = [ |
| 'RESUMEN HER2 FISH (ASCO/CAP 2018)', |
| '-------------------------------------- |
| ', |
| f'Células evaluadas: {progreso}', |
| f'HER2/célula: {h}', |
| f'CEN17/célula: {c}', |
| f'RATIO: {r}', |
| f'Grupo FISH: {g}', |
| f'Interpretación: {i}', |
| f'Comentario: {co}' |
| ] |
| if av: |
| texto.append(f'Aviso: {av}') |
| texto.append(' |
| Contajes:') |
| texto.append(df.to_string(index=False)) |
|
|
| contenido = ' |
| '.join(texto) |
| tmp = tempfile.NamedTemporaryFile(delete=False, suffix='.txt', mode='w', encoding='utf-8') |
| tmp.write(contenido) |
| tmp.close() |
| return tmp.name |
|
|
|
|
| def build_ui(): |
| with gr.Blocks(title='Calculadora HER2 FISH ASCO/CAP 2018') as demo: |
| gr.Markdown('## 🧮 Calculadora FISH HER2 (ASCO/CAP 2018) — Gradio 6.x') |
|
|
| objetivo = gr.Radio([20,40], value=20, label='Células a evaluar') |
| her2 = gr.Number(label='HER2 (señales)', precision=0) |
| cen17 = gr.Number(label='CEN17 (señales)', precision=0) |
|
|
| df_state = gr.State([]) |
|
|
| btn_add = gr.Button('Agregar célula') |
| btn_del = gr.Button('Borrar última') |
| btn_clr = gr.Button('Borrar todas') |
| btn_res = gr.Button('📄 Generar resumen imprimible') |
|
|
| tabla = gr.Dataframe(headers=['HER2','CEN17'], interactive=False, height=250) |
| progreso = gr.Textbox(label='Progreso') |
| hmean = gr.Number(label='HER2/célula') |
| cmean = gr.Number(label='CEN17/célula') |
| ratio = gr.Number(label='RATIO') |
| grupo = gr.Textbox(label='Grupo') |
| interpretacion = gr.HTML(label='Interpretación') |
| comentario = gr.Textbox(label='Comentario') |
| aviso = gr.Textbox(label='Aviso') |
| resumen = gr.File(label='Descargar resumen') |
|
|
| outputs = [df_state, tabla, progreso, hmean, cmean, ratio, grupo, interpretacion, comentario, aviso] |
|
|
| btn_add.click(agregar_contaje, inputs=[her2, cen17, df_state, objetivo], outputs=outputs) |
| btn_del.click(borrar_ultima, inputs=[df_state, objetivo], outputs=outputs) |
| btn_clr.click(borrar_todas, inputs=[df_state, objetivo], outputs=outputs) |
| objetivo.change(recomputar, inputs=[df_state, objetivo], outputs=outputs) |
| btn_res.click(generar_resumen, inputs=[df_state, objetivo], outputs=resumen) |
|
|
| return demo |
|
|
|
|
| if __name__ == '__main__': |
| ui = build_ui() |
| ui.launch(server_name='0.0.0.0', server_port=7860) |
|
|