| import os |
| import gradio as gr |
| import pandas as pd |
|
|
| import database as db |
| import ai_processor as ai |
| import exportar as exp |
|
|
| db.init_db() |
|
|
| CSS = """ |
| .emergency-header { background:#b91c1c !important; color:white !important; padding:16px; border-radius:8px; text-align:center; } |
| .info-card { background:#fffbeb !important; color:#111827 !important; border-left:4px solid #f59e0b !important; padding:12px; border-radius:4px; margin:8px 0; } |
| .aviso-ia { background:#f0f9ff !important; color:#111827 !important; border-left:4px solid #0ea5e9 !important; padding:10px; border-radius:4px; margin:8px 0; font-size:0.9em; } |
| .paso { background:#f8fafc !important; color:#111827 !important; border:1px solid #e2e8f0 !important; border-left:4px solid #2563eb !important; padding:10px 14px; border-radius:6px; margin:6px 0; } |
| .dup-exacto { background:#fef2f2 !important; color:#111827 !important; border:2px solid #dc2626 !important; border-radius:8px; padding:14px; } |
| .dup-prob { background:#fff7ed !important; color:#111827 !important; border:2px solid #ea580c !important; border-radius:8px; padding:14px; } |
| footer { display:none !important; } |
| """ |
|
|
| HOSPITALES = [ |
| "", "Hospital Universitario de Caracas", "Hospital Vargas", |
| "Hospital Domingo Luciani (El Llanito)", "Hospital Pérez Carreño", |
| "Hospital Miguel Pérez Carreño", "Periférico de Catia", "Cruz Roja", |
| "Centro Médico de Caracas", "Hospital José Gregorio Hernández", |
| "Hospital Dr. Jesús Yerena (Lidice)", "Clínica El Ávila", |
| "Hospital Militar Dr. Carlos Arvelo", |
| "Hospital de Niños J.M. de los Ríos", "Otro", |
| ] |
| CONDICIONES = ["Sin información", "Estable", "Grave", "Crítico", |
| "En observación", "En cirugía", "Fallecido"] |
|
|
| COLS_CARGA = ["Nombre", "Cédula", "Edad", "Condición", "Notas"] |
|
|
|
|
| |
|
|
| def _contador_md() -> str: |
| return f"📊 **{db.total_registros()} personas registradas**" |
|
|
|
|
| def _rows_a_df(rows: list[dict]) -> pd.DataFrame: |
| if not rows: |
| return pd.DataFrame(columns=["Nombre", "Cédula", "Edad", "Hospital", |
| "Condición", "Última actualización", "Verificado"]) |
| df = pd.DataFrame(rows) |
| df["Verificado"] = df["verificado"].apply(lambda x: "✅" if x else "👤") |
| return df[["nombre", "cedula", "edad", "hospital", "condicion", |
| "fecha_update", "Verificado"]].rename(columns={ |
| "nombre": "Nombre", "cedula": "Cédula", "edad": "Edad", |
| "hospital": "Hospital", "condicion": "Condición", |
| "fecha_update": "Última actualización", |
| }) |
|
|
| def _generar_cards(rows: list[dict]) -> str: |
| if not rows: |
| return "" |
| html = '<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin-top: 16px;">' |
| for r in rows: |
| nombre = r.get("nombre") or "Desconocido" |
| ced = r.get("cedula_norm") or r.get("cedula") or "No indicada" |
| if r.get("cedula_norm"): |
| ced = db.formatear_cedula_display(r.get("cedula_norm")) |
| edad = f"{r.get('edad')} años" if r.get('edad') else "--" |
| hospital = r.get("hospital") or "No indicado" |
| cond = r.get("condicion") or "Sin información" |
| |
| notas = r.get("notas") |
| html_notas = f'<div style="color: #4b5563 !important; margin-top: 8px; padding-top: 8px; border-top: 1px dashed #e5e7eb !important; font-size: 0.85rem;">📝 <b style="color: #111827 !important;">Notas:</b> {notas}</div>' if notas and str(notas).strip() else "" |
| |
| color_cond = "#6b7280" |
| if "Crítico" in cond: color_cond = "#dc2626" |
| elif "Grave" in cond: color_cond = "#ea580c" |
| elif "Estable" in cond: color_cond = "#16a34a" |
| elif "Fallecido" in cond: color_cond = "#000000" |
| |
| verif = "✅ Verif." if r.get("verificado") else "👤 Reporte civil" |
| fecha = r.get('fecha_update', '')[:10] |
| |
| html += f''' |
| <div style="background: white !important; border: 1px solid #e5e7eb !important; border-radius: 12px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.05);"> |
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 12px;"> |
| <h3 style="margin: 0; color: #111827 !important; font-size: 1.15rem; font-weight: 700; line-height: 1.2;">{nombre}</h3> |
| <span style="background: {color_cond} !important; color: white !important; padding: 3px 8px; border-radius: 999px; font-size: 0.75rem; font-weight: 600; white-space: nowrap; margin-left: 8px;">{cond}</span> |
| </div> |
| <div style="color: #4b5563 !important; font-size: 0.95rem; line-height: 1.6;"> |
| <div style="color: #4b5563 !important;">🪪 <b style="color: #111827 !important;">C.I:</b> {ced}</div> |
| <div style="color: #4b5563 !important;">🎂 <b style="color: #111827 !important;">Edad:</b> {edad}</div> |
| <div style="color: #4b5563 !important;">🏥 <b style="color: #111827 !important;">Hospital:</b> {hospital}</div> |
| {html_notas} |
| </div> |
| <div style="margin-top: 14px; padding-top: 10px; border-top: 1px solid #e5e7eb !important; display: flex; justify-content: space-between; color: #6b7280 !important; font-size: 0.8rem;"> |
| <span style="color: #6b7280 !important;">{verif}</span> |
| <span style="color: #6b7280 !important;">Act: {fecha}</span> |
| </div> |
| </div> |
| ''' |
| html += "</div>" |
| return html |
|
|
|
|
| def _df_carga_vacio() -> pd.DataFrame: |
| return pd.DataFrame(columns=COLS_CARGA) |
|
|
|
|
| def _personas_a_df_carga(personas: list[dict]) -> pd.DataFrame: |
| if not personas: |
| return _df_carga_vacio() |
| filas = [{ |
| "Nombre": p.get("nombre") or "", |
| "Cédula": p.get("cedula") or "", |
| "Edad": p.get("edad") or "", |
| "Condición": p.get("condicion") or "Sin información", |
| "Notas": p.get("notas") or "", |
| } for p in personas] |
| return pd.DataFrame(filas, columns=COLS_CARGA) |
|
|
|
|
| def _df_carga_a_personas(tabla) -> list[dict]: |
| """Convierte la tabla editable (DataFrame o dict de Gradio) a lista de personas.""" |
| if tabla is None: |
| return [] |
| if isinstance(tabla, pd.DataFrame): |
| registros = tabla.to_dict(orient="records") |
| elif isinstance(tabla, dict) and "data" in tabla: |
| headers = tabla.get("headers", COLS_CARGA) |
| registros = [dict(zip(headers, fila)) for fila in tabla["data"]] |
| elif isinstance(tabla, list): |
| registros = [dict(zip(COLS_CARGA, fila)) for fila in tabla] |
| else: |
| return [] |
|
|
| personas = [] |
| for r in registros: |
| personas.append({ |
| "nombre": str(r.get("Nombre", "") or "").strip(), |
| "cedula": str(r.get("Cédula", "") or "").strip(), |
| "edad": r.get("Edad"), |
| "condicion": str(r.get("Condición", "") or "Sin información").strip(), |
| "notas": str(r.get("Notas", "") or "").strip() or None, |
| }) |
| return personas |
|
|
|
|
| def _formato_analisis(res: dict) -> str: |
| if not res: |
| return "Sin resultado." |
| if not res.get("procesable", True): |
| return ( |
| f"⚠️ **La IA no pudo procesar esta imagen.**\n\n" |
| f"**Razón:** {res.get('error', res.get('observaciones', 'No especificada'))}\n\n" |
| f"*Registra la persona manualmente en «Agregar Persona».*" |
| ) |
| lines = [f"### Tipo de imagen\n{res.get('tipo_imagen', 'No determinado')}\n"] |
|
|
| def campo(label, valor): |
| v = valor if valor not in (None, "", "null", "None") else "No detectado / No legible" |
| lines.append(f"**{label}:** {v}") |
|
|
| campo("Nombre", res.get("nombre_detectado")) |
| ced_raw = res.get("cedula_detectada") |
| if ced_raw: |
| ced_fmt = db.formatear_cedula_display(db.normalizar_cedula(str(ced_raw))) |
| campo("Cédula", ced_fmt or ced_raw) |
| else: |
| campo("Cédula", None) |
| campo("Edad estimada", res.get("edad_estimada")) |
| campo("Descripción física", res.get("descripcion_fisica")) |
| campo("Texto visible", res.get("texto_visible") or "Ninguno legible") |
| campo("Condición aparente", res.get("condicion_aparente")) |
| if res.get("personas_multiples"): |
| lines.append(f"\n**👥 Se detectaron {len(res['personas_multiples'])} personas en una lista.** " |
| "Usa la pestaña «📋 Cargar Lista» para guardarlas todas de una vez.") |
| if res.get("observaciones"): |
| lines.append(f"\n**Observaciones:** {res['observaciones']}") |
| lines.append(f"\n---\n*{res.get('resumen', '')}*") |
| lines.append( |
| "\n> ℹ️ Solo se reporta lo que la IA observa con certeza. " |
| "Campos «No detectado» deben completarse manualmente." |
| ) |
| return "\n".join(lines) |
|
|
|
|
| def _html_duplicados(dups: list[dict]) -> str: |
| if not dups: |
| return "" |
| filas = "" |
| for d in dups: |
| nivel = d.get("nivel_alerta", "POSIBLE") |
| cls = "dup-exacto" if nivel == "EXACTO" else "dup-prob" |
| ico = "🚨" if nivel == "EXACTO" else "⚠️" |
| filas += f""" |
| <div class="{cls}" style="margin-bottom:10px;"> |
| {ico} <b>Duplicado {nivel}</b> encontrado — ID #{d['id']}<br> |
| <b>Nombre:</b> {d.get('nombre') or '—'} | |
| <b>Cédula:</b> {d.get('cedula') or '—'} | |
| <b>Edad:</b> {d.get('edad') or '—'}<br> |
| <b>Hospital:</b> {d.get('hospital') or '—'} | |
| <b>Condición:</b> {d.get('condicion') or '—'}<br> |
| <b>Registrado:</b> {d.get('fecha_ingreso') or '—'} | |
| <b>Fuente:</b> {d.get('fuente') or '—'} |
| </div>""" |
| return f""" |
| <div style="margin:12px 0;"> |
| <h3 style="color:#b91c1c;">⛔ ¡POSIBLE DUPLICADO DETECTADO!</h3> |
| <p>Antes de registrar, revisa si esta persona <b>ya está en la base de datos</b>:</p> |
| {filas} |
| <p style="color:#b91c1c;font-weight:bold;"> |
| Si es la misma persona, <b>NO registres de nuevo</b>. |
| Solo confirma si estás seguro de que es un registro distinto. |
| </p> |
| </div>""" |
|
|
|
|
| |
|
|
| def buscar_familiar(nombre, cedula, edad_min, edad_max, hospital): |
| rows = db.buscar( |
| nombre=nombre or "", cedula=cedula or "", |
| edad_min=int(edad_min) if edad_min else None, |
| edad_max=int(edad_max) if edad_max else None, |
| hospital=hospital or "", |
| ) |
| df = _rows_a_df(rows) |
| html_cards = _generar_cards(rows) |
| n = len(rows) |
| msg = ("⚠️ No se encontraron registros. Intenta con menos filtros." if n == 0 |
| else f"✅ **{n} persona(s) encontrada(s).** Confirma siempre con el hospital.") |
| return html_cards, msg, rows |
|
|
|
|
| def exportar_excel_fn(rows): |
| return exp.exportar_excel(rows) if rows else None |
|
|
|
|
| def exportar_pdf_fn(rows): |
| return exp.exportar_pdf(rows) if rows else None |
|
|
|
|
| def normalizar_cedula_ui(cedula_raw: str) -> str: |
| n = db.normalizar_cedula(cedula_raw or "") |
| return db.formatear_cedula_display(n) if n else "" |
|
|
|
|
| |
|
|
| def verificar_antes_de_agregar(nombre, cedula, edad, hospital, hospital_manual, |
| condicion, notas, contacto, imagen, fuente): |
| hospital_final = (hospital_manual.strip() |
| if hospital == "Otro" and hospital_manual.strip() else hospital) |
|
|
| if not (nombre or "").strip() and not (cedula or "").strip(): |
| return ("❌ Debes ingresar al menos **nombre** o **cédula**.", |
| gr.update(visible=False), gr.update(visible=False), "", {}, _contador_md()) |
|
|
| cedula_norm = db.normalizar_cedula(cedula or "") |
| cedula_fmt = db.formatear_cedula_display(cedula_norm) if cedula_norm else (cedula or "") |
|
|
| descripcion_ia = "" |
| imagen_path = imagen |
| if imagen is not None: |
| res = ai.analizar_imagen_hospital(imagen) |
| if res.get("procesable", True) and "error" not in res: |
| descripcion_ia = res.get("resumen", "") |
| if not (nombre or "").strip() and res.get("nombre_detectado"): |
| nombre = res["nombre_detectado"] |
| if not cedula_norm and res.get("cedula_detectada"): |
| cedula_fmt = db.formatear_cedula_display( |
| db.normalizar_cedula(str(res["cedula_detectada"])) |
| ) or str(res["cedula_detectada"]) |
| cedula_norm = db.normalizar_cedula(str(res["cedula_detectada"])) |
| if not edad and res.get("edad_estimada"): |
| try: |
| edad = int(res["edad_estimada"]) |
| except (TypeError, ValueError): |
| pass |
| else: |
| descripcion_ia = f"[IA no procesó la imagen: {res.get('error', '')}]" |
|
|
| pendiente = dict( |
| nombre=nombre, cedula=cedula_fmt, cedula_norm=cedula_norm, |
| edad=edad, hospital=hospital_final, condicion=condicion, |
| descripcion=descripcion_ia, imagen_path=imagen_path, |
| contacto=contacto, notas=notas, fuente=fuente |
| ) |
|
|
| dups = db.buscar_duplicados(nombre or "", cedula_norm, edad) |
|
|
| if dups: |
| html_aviso = _html_duplicados(dups) |
| return ( |
| f"⚠️ Se detectaron **{len(dups)} posible(s) duplicado(s)**. " |
| "Revisa abajo y confirma solo si es un registro nuevo.", |
| gr.update(visible=True), gr.update(visible=True), html_aviso, |
| pendiente, _contador_md() |
| ) |
|
|
| new_id = _registrar(pendiente) |
| return ( |
| f"✅ Persona registrada con ID **#{new_id}** sin duplicados detectados. ¡Gracias!", |
| gr.update(visible=False), gr.update(visible=False), "", {}, _contador_md() |
| ) |
|
|
|
|
| def confirmar_registro(pendiente): |
| if not pendiente: |
| return ("❌ No hay datos pendientes.", gr.update(visible=False), |
| gr.update(visible=False), "", {}, _contador_md()) |
| new_id = _registrar(pendiente) |
| return ( |
| f"✅ Persona registrada con ID **#{new_id}** (confirmado manualmente).", |
| gr.update(visible=False), gr.update(visible=False), "", {}, _contador_md() |
| ) |
|
|
|
|
| def cancelar_registro(): |
| return ("❌ Registro cancelado. Verifica los datos e intenta con más información.", |
| gr.update(visible=False), gr.update(visible=False), "", {}, _contador_md()) |
|
|
|
|
| def _registrar(datos: dict) -> int: |
| new_id = db.agregar_persona( |
| nombre=datos.get("nombre"), cedula=datos.get("cedula"), |
| edad=datos.get("edad"), hospital=datos.get("hospital"), |
| condicion=datos.get("condicion"), descripcion=datos.get("descripcion"), |
| imagen_path=datos.get("imagen_path"), contacto=datos.get("contacto"), |
| notas=datos.get("notas"), fuente=datos.get("fuente", "ciudadano"), |
| ) |
| if datos.get("imagen_path"): |
| try: |
| db.guardar_imagen(datos["imagen_path"], new_id) |
| except Exception: |
| pass |
| return new_id |
|
|
|
|
| |
|
|
| def extraer_lista_fn(archivo): |
| if not archivo: |
| return _df_carga_vacio(), "⚠️ Sube primero una imagen, PDF o Excel con la lista." |
| res = ai.extraer_lista(archivo) |
| if not res.get("procesable"): |
| return _df_carga_vacio(), ( |
| f"⚠️ **No se pudo extraer la lista.**\n\n{res.get('error', '')}\n\n" |
| "*Puedes escribir las personas manualmente en la tabla de abajo y guardarlas.*" |
| ) |
| personas = res.get("personas", []) |
| if not personas: |
| return _df_carga_vacio(), ("⚠️ No se detectaron personas en el archivo. " |
| "Revisa que la imagen sea legible.") |
| df = _personas_a_df_carga(personas) |
| msg = (f"✅ Se extrajeron **{len(personas)} personas**. " |
| "**Revisa y corrige** la tabla antes de guardar — la IA nunca inventa, " |
| "pero conviene verificar nombres y cédulas. Luego pulsa «Guardar todos».") |
| return df, msg |
|
|
|
|
| def guardar_lote_fn(tabla, hospital, hospital_manual, fuente): |
| personas = _df_carga_a_personas(tabla) |
| if not personas: |
| return ("⚠️ La tabla está vacía. Extrae una lista o escribe al menos una persona.", |
| _contador_md()) |
|
|
| hospital_final = (hospital_manual.strip() |
| if hospital == "Otro" and (hospital_manual or "").strip() else hospital) |
|
|
| resumen = db.agregar_personas_bulk( |
| personas, hospital_default=hospital_final or None, |
| fuente=fuente or "registro hospital", saltar_duplicados=True |
| ) |
|
|
| lineas = [f"✅ **{resumen['agregados']} personas agregadas** a la base de datos."] |
| if resumen["omitidos"]: |
| ejemplos = ", ".join(resumen["nombres_omitidos"][:8]) |
| extra = "…" if len(resumen["nombres_omitidos"]) > 8 else "" |
| lineas.append(f"↩️ **{resumen['omitidos']} omitidas** por estar ya registradas " |
| f"(misma cédula): {ejemplos}{extra}") |
| if hospital_final: |
| lineas.append(f"🏥 Asignadas al hospital: **{hospital_final}**") |
| lineas.append("\n> Ve a «🔍 Buscar Familiar» para verificarlas.") |
| return "\n\n".join(lineas), _contador_md() |
|
|
|
|
| |
|
|
| def buscar_por_imagen(imagen): |
| if imagen is None: |
| return pd.DataFrame(), "⚠️ Por favor sube una imagen primero." |
| res = ai.describir_imagen_busqueda(imagen) |
| if not res.get("procesable", True): |
| return pd.DataFrame(), ( |
| f"⚠️ **La IA no pudo procesar esta imagen.**\n\n" |
| f"{res.get('error', 'Razón desconocida')}\n\n" |
| "*Prueba buscar manualmente con nombre o cédula.*" |
| ) |
| descripcion = res.get("descripcion_busqueda") or "No determinada" |
| edad_est = res.get("edad_estimada") |
| nombre_vis = res.get("nombre_visible") |
| cedula_vis = res.get("cedula_visible") |
| advertencia = res.get("advertencia") or "" |
|
|
| resultados = [] |
| if cedula_vis: |
| resultados = db.buscar(cedula=str(cedula_vis)) |
| if not resultados and nombre_vis: |
| resultados = db.buscar(nombre=nombre_vis) |
| if not resultados and edad_est: |
| try: |
| e = int(edad_est) |
| resultados = db.buscar(edad_min=max(0, e - 10), edad_max=e + 10) |
| except (TypeError, ValueError): |
| pass |
|
|
| df = _rows_a_df(resultados) |
| msg = [f"**La IA describe a la persona:**\n\n_{descripcion}_", |
| f"**Características:** {res.get('caracteristicas_clave') or 'No determinadas'}"] |
| if advertencia: |
| msg.append(f"⚠️ **Aviso:** {advertencia}") |
| msg.append(f"**{len(resultados)} coincidencia(s)** encontrada(s)." if resultados |
| else "Sin coincidencias automáticas — busca manualmente con nombre o cédula.") |
| msg.append("\n> ℹ️ La búsqueda por imagen es orientativa. Siempre verifica en el hospital.") |
| return df, "\n\n".join(msg) |
|
|
|
|
| def analizar_imagen(imagen): |
| if imagen is None: |
| return "⚠️ Sube una imagen para analizar.", {} |
| res = ai.analizar_imagen_hospital(imagen) |
| return _formato_analisis(res), res |
|
|
|
|
| def mostrar_hospital_manual(h): |
| return gr.Textbox(visible=(h == "Otro")) |
|
|
|
|
| |
|
|
| with gr.Blocks(title="Buscador Familiar – Terremoto Venezuela", css=CSS, |
| theme=gr.themes.Soft()) as demo: |
|
|
| estado_busqueda = gr.State([]) |
| estado_pendiente = gr.State({}) |
|
|
| gr.HTML(""" |
| <div class="emergency-header"> |
| <h1>🆘 Buscador de Familiares – Terremoto Venezuela</h1> |
| <p>Plataforma ciudadana para ubicar personas en hospitales y centros de atención</p> |
| </div>""") |
|
|
| gr.HTML(""" |
| <div class="info-card"> |
| <b>📞 Emergencias:</b> |
| Bomberos: <b>171</b> | |
| Cruz Roja: <b>0212-781-3557</b> | |
| Protección Civil: <b>0800-776-8352</b> | |
| CICPC: <b>0800-248-1248</b> |
| </div>""") |
|
|
| contador = gr.Markdown(_contador_md()) |
| gr.Markdown("---") |
|
|
| with gr.Tabs(): |
|
|
| |
| with gr.Tab("🔍 Buscar Familiar"): |
| gr.Markdown("### Ingresa uno o varios criterios") |
| with gr.Row(): |
| inp_nombre = gr.Textbox(label="Nombre (o parte)", placeholder="Ej: María González") |
| inp_cedula = gr.Textbox(label="Cédula", placeholder="Ej: V-12345678 ó 12345678") |
| with gr.Row(): |
| inp_emin = gr.Number(label="Edad mínima", minimum=0, maximum=120, value=None, scale=1) |
| inp_emax = gr.Number(label="Edad máxima", minimum=0, maximum=120, value=None, scale=1) |
| inp_hosp = gr.Textbox(label="Hospital", placeholder="Ej: Hospital Vargas", scale=2) |
|
|
| btn_buscar = gr.Button("🔍 Buscar", variant="primary", size="lg") |
| msg_buscar = gr.Markdown() |
| tabla = gr.HTML() |
|
|
| with gr.Row(): |
| btn_excel = gr.DownloadButton("📥 Excel", variant="secondary", size="sm") |
| btn_pdf = gr.DownloadButton("📄 PDF", variant="secondary", size="sm") |
|
|
| btn_buscar.click(buscar_familiar, |
| [inp_nombre, inp_cedula, inp_emin, inp_emax, inp_hosp], |
| [tabla, msg_buscar, estado_busqueda]) |
| btn_excel.click(exportar_excel_fn, inputs=[estado_busqueda], outputs=[btn_excel]) |
| btn_pdf.click(exportar_pdf_fn, inputs=[estado_busqueda], outputs=[btn_pdf]) |
|
|
| gr.Markdown("> ✅ Verificado por personal médico | 👤 Reportado por ciudadano") |
|
|
| |
| with gr.Tab("📋 Cargar Lista (varias personas)"): |
| gr.Markdown("### Sube una imagen, PDF o Excel con una LISTA de personas") |
| gr.HTML(""" |
| <div class="paso"><b>Paso 1.</b> Sube la imagen/lista (ej: captura de WhatsApp de un hospital con nombres, edades y cédulas).</div> |
| <div class="paso"><b>Paso 2.</b> Pulsa «Extraer con IA». La IA ordena los datos en la tabla — <b>nunca inventa</b>.</div> |
| <div class="paso"><b>Paso 3.</b> Revisa y corrige la tabla, elige el hospital, y pulsa «Guardar todos».</div> |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| carga_file = gr.File( |
| label="📎 Imagen / PDF / Excel con la lista", |
| file_types=[".jpg", ".jpeg", ".png", ".webp", ".gif", |
| ".pdf", ".xlsx", ".xls", ".csv"], |
| type="filepath") |
| carga_hosp = gr.Dropdown(choices=HOSPITALES, value="", |
| label="🏥 Hospital para TODA la lista") |
| carga_hosp_m = gr.Textbox(label="Nombre del hospital (si elegiste «Otro»)", |
| visible=False, placeholder="Nombre exacto") |
| carga_fuente = gr.Radio( |
| choices=["registro hospital", "personal médico", "voluntario", |
| "lista hospital", "ciudadano"], |
| value="registro hospital", label="¿De dónde viene la lista?") |
| btn_extraer = gr.Button("🤖 Extraer con IA", variant="primary", size="lg") |
| with gr.Column(scale=2): |
| msg_carga = gr.Markdown("Sube un archivo y pulsa «Extraer con IA».") |
| tabla_carga = gr.Dataframe( |
| headers=COLS_CARGA, |
| datatype=["str", "str", "str", "str", "str"], |
| col_count=(5, "fixed"), |
| row_count=(1, "dynamic"), |
| interactive=True, |
| wrap=True, |
| label="Revisa y corrige antes de guardar") |
| btn_guardar_lote = gr.Button("💾 Guardar todos en la base de datos", |
| variant="primary", size="lg") |
| msg_lote = gr.Markdown() |
|
|
| carga_hosp.change(mostrar_hospital_manual, carga_hosp, carga_hosp_m) |
| btn_extraer.click(extraer_lista_fn, [carga_file], [tabla_carga, msg_carga]) |
| btn_guardar_lote.click( |
| guardar_lote_fn, |
| [tabla_carga, carga_hosp, carga_hosp_m, carga_fuente], |
| [msg_lote, contador]) |
|
|
| gr.HTML("""<div class="aviso-ia"> |
| 🔒 <b>Datos sensibles:</b> esta lista puede contener menores de edad. Úsala solo para |
| reunificación familiar. Los duplicados (misma cédula) se omiten automáticamente. |
| </div>""") |
|
|
| |
| with gr.Tab("➕ Agregar Persona"): |
| gr.Markdown(""" |
| ### Registra a UNA persona ubicada en un hospital |
| Para listas de varias personas, usa la pestaña «📋 Cargar Lista». |
| """) |
| gr.HTML("""<div class="aviso-ia"> |
| 🔍 <b>Detección de duplicados activa:</b> Antes de guardar se verifica si la persona |
| ya existe. Si hay coincidencia, se mostrará un aviso para que confirmes. |
| La cédula se normaliza automáticamente al formato venezolano. |
| </div>""") |
|
|
| with gr.Row(): |
| with gr.Column(scale=1): |
| img_agregar = gr.Image(label="📷 Imagen (cualquier tipo)", type="filepath", |
| sources=["upload", "clipboard"]) |
| fuente = gr.Radio( |
| choices=["ciudadano", "personal médico", "voluntario", "familiar"], |
| value="ciudadano", label="¿Quién reporta?") |
|
|
| with gr.Column(scale=2): |
| with gr.Row(): |
| a_nombre = gr.Textbox(label="Nombre completo *", placeholder="Nombre y apellido") |
| a_cedula = gr.Textbox(label="Cédula *", placeholder="V-12345678 / E-12345678 / solo números") |
| with gr.Row(): |
| a_edad = gr.Number(label="Edad", minimum=0, maximum=120, value=None) |
| a_cond = gr.Dropdown(choices=CONDICIONES, value="Sin información", label="Condición") |
| a_hosp = gr.Dropdown(choices=HOSPITALES, value="", label="Hospital / Centro") |
| a_hosp_m = gr.Textbox(label="Nombre del hospital (si elegiste «Otro»)", |
| visible=False, placeholder="Nombre exacto") |
| a_contacto = gr.Textbox(label="Teléfono familiar (opcional)", placeholder="04XX-XXXXXXX") |
| a_notas = gr.Textbox(label="Notas adicionales", lines=2, |
| placeholder="Sala, cama, señas particulares...") |
|
|
| a_cedula.blur(normalizar_cedula_ui, inputs=[a_cedula], outputs=[a_cedula]) |
|
|
| btn_agregar = gr.Button("✅ Verificar y Registrar", variant="primary", size="lg") |
| msg_agregar = gr.Markdown() |
|
|
| html_dups = gr.HTML("") |
| with gr.Row(): |
| btn_confirmar = gr.Button("⚠️ Confirmar registro (soy consciente del duplicado)", |
| variant="stop", visible=False) |
| btn_cancelar = gr.Button("❌ Cancelar", variant="secondary", visible=False) |
|
|
| a_hosp.change(mostrar_hospital_manual, a_hosp, a_hosp_m) |
|
|
| btn_agregar.click( |
| verificar_antes_de_agregar, |
| inputs=[a_nombre, a_cedula, a_edad, a_hosp, a_hosp_m, |
| a_cond, a_notas, a_contacto, img_agregar, fuente], |
| outputs=[msg_agregar, btn_confirmar, btn_cancelar, html_dups, |
| estado_pendiente, contador] |
| ) |
| btn_confirmar.click(confirmar_registro, |
| inputs=[estado_pendiente], |
| outputs=[msg_agregar, btn_confirmar, btn_cancelar, html_dups, |
| estado_pendiente, contador]) |
| btn_cancelar.click(cancelar_registro, |
| outputs=[msg_agregar, btn_confirmar, btn_cancelar, html_dups, |
| estado_pendiente, contador]) |
|
|
| |
| with gr.Tab("📸 Buscar por Foto"): |
| gr.Markdown(""" |
| ### Sube una foto de tu familiar para buscar coincidencias |
| Acepta foto, imagen de cédula, captura de pantalla, etc. |
| **Si la IA no puede procesar la imagen, te lo dirá — nunca inventará datos.** |
| """) |
| img_busq = gr.Image(label="Imagen de tu familiar", type="filepath", |
| sources=["upload", "clipboard"]) |
| btn_bimg = gr.Button("🔍 Buscar", variant="primary") |
| msg_bimg = gr.Markdown() |
| tabla_bimg = gr.Dataframe(interactive=False, wrap=True) |
| btn_bimg.click(buscar_por_imagen, [img_busq], [tabla_bimg, msg_bimg]) |
|
|
| |
| with gr.Tab("🤖 Analizar Imagen con IA"): |
| gr.Markdown("### Extrae información de una imagen para revisarla antes de registrar") |
| gr.HTML("""<div class="aviso-ia"> |
| ⚠️ <b>Sin sesgo:</b> Si la IA no puede leer algo con certeza, lo dirá explícitamente. |
| No supondrá ni inventará nombres, cédulas ni datos. |
| </div>""") |
| img_anal = gr.Image(label="Imagen a analizar", type="filepath", |
| sources=["upload", "clipboard"]) |
| btn_anal = gr.Button("🔍 Analizar", variant="secondary") |
| txt_anal = gr.Markdown() |
| json_anal = gr.JSON(label="Datos JSON", visible=False) |
| btn_anal.click(analizar_imagen, [img_anal], [txt_anal, json_anal]) |
|
|
| |
| with gr.Tab("ℹ️ Información"): |
| gr.Markdown("## 📥 Descarga Completa de Datos\nPuedes descargar toda la base de datos (sin límites) en formato Excel para realizar respaldos o compartirla.") |
| btn_descargar_todo = gr.DownloadButton("📥 Descargar Base de Datos Completa (Excel)", variant="primary") |
| |
| def exportar_todo_fn(): |
| return exp.exportar_excel(db.obtener_todos_para_exportar()) |
| |
| btn_descargar_todo.click(exportar_todo_fn, None, btn_descargar_todo) |
| |
| gr.Markdown(""" |
| --- |
| ## ¿Cómo usar esta plataforma? |
| |
| ### 🔍 Buscar familiar |
| - Ingresa nombre, cédula (con o sin V-) o rango de edad |
| - La búsqueda acepta cédulas con puntos, guiones o sin ellos |
| - Descarga los resultados en **Excel** o **PDF** para imprimir o compartir |
| |
| ### 📋 Cargar Lista (varias personas) |
| - Sube una **imagen, PDF o Excel** con una lista de pacientes |
| - La IA **ordena** los datos (nombre, cédula, edad) en una tabla editable |
| - Revisa, corrige y **guarda todas de una vez** — los duplicados se omiten solos |
| |
| ### ➕ Agregar persona |
| - Para registrar a **una** persona con detalle (foto, condición, contacto) |
| - Antes de guardar, el sistema verifica duplicados |
| |
| ### 📸 Buscar por foto |
| - Sube la imagen de tu familiar; la IA lo describe y busca coincidencias |
| |
| --- |
| |
| ## 📞 Líneas de emergencia |
| |
| | Servicio | Número | |
| |---|---| |
| | Bomberos | 171 | |
| | Cruz Roja Venezolana | 0212-781-3557 | |
| | Protección Civil | 0800-PROTEJA (776-8352) | |
| | CICPC | 0800-248-1248 | |
| | Defensa Civil | 0500-DEFENSA | |
| |
| --- |
| |
| ## ⚠️ Avisos |
| - La IA **solo reporta lo que puede observar con certeza** — nunca inventa datos |
| - Esta es una plataforma ciudadana, **no oficial** |
| - Confirma siempre la información directamente con el hospital |
| - Los datos pueden incluir **menores de edad**: úsalos solo para reunificación familiar |
| |
| --- |
| *Hecha con ❤️ para Venezuela en tiempos de emergencia.* |
| """) |
|
|
| gr.HTML("""<div style="text-align:center;color:#9ca3af;font-size:0.8em;margin-top:16px;"> |
| Plataforma ciudadana de emergencia · Datos para facilitar la búsqueda de familiares |
| </div>""") |
|
|
| |
| btn_api = gr.Button(visible=False) |
| json_api = gr.JSON(visible=False) |
| |
| def api_get_personas(): |
| rows = db.obtener_todos_para_exportar() |
| return {"status": "success", "total": len(rows), "data": rows} |
| |
| btn_api.click(api_get_personas, inputs=[], outputs=[json_api], api_name="api_personas") |
|
|
| demo.load(_contador_md, outputs=[contador]) |
|
|
|
|
| if __name__ == "__main__": |
| demo.queue(max_size=50).launch(server_name="0.0.0.0", server_port=7860) |
|
|