jcalbornoz's picture
Update app.py
9598b2d verified
import sys
import os
import subprocess
import pandas as pd
import datetime
import re
from fpdf import FPDF
import gradio as gr
from playwright.sync_api import sync_playwright
import time
import requests
from PIL import Image
import io
import traceback
from geopy.geocoders import Nominatim
import folium
# Silenciador de consola
def silenciador_errores_basura(unraisable):
if unraisable.exc_type == ValueError and "Invalid file descriptor: -1" in str(unraisable.exc_value): pass
else: sys.__unraisablehook__(unraisable)
sys.unraisablehook = silenciador_errores_basura
try: subprocess.run(["playwright", "install", "chromium"], check=True)
except: pass
try: from fake_useragent import UserAgent
except ImportError:
subprocess.run(["pip", "install", "fake-useragent"], check=True)
from fake_useragent import UserAgent
URL_LOGO_GOBIERNO = "https://upload.wikimedia.org/wikipedia/commons/thumb/c/c3/Escudo_de_Colombia.svg/250px-Escudo_de_Colombia.svg.png"
def descargar_recurso(url, nombre_archivo):
try:
headers = {"User-Agent": "Mozilla/5.0"}
r = requests.get(url, timeout=10, headers=headers)
if r.status_code == 200:
with open(nombre_archivo, 'wb') as f: f.write(r.content)
except: pass
def preparar_entorno_pdf():
if not os.path.exists("logo_gob.png"): descargar_recurso(URL_LOGO_GOBIERNO, "logo_gob.png")
def sanear_texto(texto):
if not isinstance(texto, str): return ""
return texto.encode('latin-1', 'ignore').decode('latin-1').strip()
def descargar_imagen(url, idx):
if not url or len(url) < 5 or url.startswith("data:"): return None
try:
headers = {"User-Agent": "Mozilla/5.0", "Accept": "image/*", "Referer": "https://www.fincaraiz.com.co/"}
r = requests.get(url, timeout=8, headers=headers)
if r.status_code == 200:
img = Image.open(io.BytesIO(r.content))
if img.mode != 'RGB': img = img.convert('RGB')
path = f"temp_img_{idx}.jpg"
img.save(path, format="JPEG")
return path
except: return None
return None
def construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina):
mapa_ant = {"Menos de 1 año": "de-0-a-1-anos", "1 a 8 años": "de-1-a-8-anos", "9 a 15 años": "de-9-a-15-anos", "16 a 30 años": "de-16-a-30-anos", "Más de 30 años": "mas-de-30-anos"}
slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anos")
slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
b_slug = barrio.lower().strip().replace(" ", "-")
c_slug = ciudad.lower().strip().replace(" ", "-")
op_slug = operacion.lower().strip()
tipo_slug = tipo.lower().strip()
residenciales = ["apartamento", "casa", "apartaestudio"]
if tipo_slug in residenciales:
tipo_fr = "casas-y-apartamentos-y-apartaestudios"
filtros_fr = f"/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{slug_park}/{slug_ant}"
filtros_mc = f"/{int(ban)}-banos-{int(hab)}-habitaciones"
else:
if tipo_slug == "local": tipo_fr = "locales"
elif tipo_slug == "edificio": tipo_fr = "edificios"
else: tipo_fr = tipo_slug + "s"
if tipo_slug in ["lote", "finca"]: filtros_fr = ""; filtros_mc = ""
else: filtros_fr = f"/{slug_park}/{slug_ant}"; filtros_mc = ""
url_fr_base = f"https://www.fincaraiz.com.co/{op_slug}/{tipo_fr}/{b_slug}/{c_slug}{filtros_fr}/m2-desde-{int(m2_min)}/m2-hasta-{int(m2_max)}"
if ascensor and tipo_slug not in ["lote", "finca"]: url_fr_base += "/con-ascensor"
if piscina and tipo_slug in residenciales + ["finca"]: url_fr_base += "/con-piscina"
url_mc = f"https://www.metrocuadrado.com/{tipo_slug}/{op_slug}/{c_slug}/{b_slug}{filtros_mc}/?search=form"
return url_fr_base, url_mc
def extraer_precio(texto, operacion):
patron = r'\$\s?(\d{1,3}(?:[.,]\d{3})*)'
coincidencias = re.findall(patron, texto)
if coincidencias:
precios = [int(p.replace('.', '').replace(',', '')) for p in coincidencias]
precios_validos = [p for p in precios if (600000 <= p <= 40000000 if operacion == "Arriendo" else p >= 40000000)]
if precios_validos: return precios_validos[0]
return 0
def extraer_ubicacion(texto):
lineas = [l.strip() for l in texto.split('\n') if len(l.strip()) > 4]
for linea in lineas:
if "$" in linea or re.search(r'(?i)(hab|baño|m2|m²)', linea): continue
if "," in linea or " en " in linea.lower():
limpio = re.sub(r'(?i)(apartamento|casa|bodega|lote|oficina|local|consultorio|finca|edificio)\s+en\s+(arriendo|venta)\s+(en\s+)?', '', linea)
return limpio[:60].strip()
for linea in lineas[1:4]:
if "$" not in linea and not re.search(r'\d', linea): return linea[:60]
return "Ubicacion en la zona"
def es_inmueble_valido(href, portal):
if not href or "javascript" in href or "blog" in href or "proyectos" in href: return False
if portal == "FR":
if re.search(r'/\d{7,10}$', href) or "arriendo-en" in href or "venta-en" in href: return True
elif portal == "MC":
if "/inmueble/" in href or "-id-" in href: return True
return False
def generar_mapa(barrio, ciudad):
try:
geolocator = Nominatim(user_agent="tramitia_geo")
location = geolocator.geocode(f"{barrio}, {ciudad}, Colombia")
if location:
lat, lon = location.latitude, location.longitude
m = folium.Map(location=[lat, lon], zoom_start=15)
folium.Marker([lat, lon], popup=f"Zona de Estudio", icon=folium.Icon(color="red", icon="info-sign")).add_to(m)
return m._repr_html_(), lat, lon
except: pass
return "<p>Mapa no disponible</p>", 10.9639, -74.7964
class PDF_SAE(FPDF):
def header(self):
if os.path.exists("logo_gob.png"):
try: self.image("logo_gob.png", x=15, y=8, h=16)
except: pass
if os.path.exists("logo_sae.png"):
try: self.image("logo_sae.png", x=155, y=8, w=40)
except: pass
elif os.path.exists("logo_sae.jpg"):
try: self.image("logo_sae.jpg", x=155, y=8, w=40)
except: pass
self.ln(25)
def footer(self):
self.set_y(-25)
try:
self.set_font('Arial', '', 7)
self.set_text_color(137, 137, 137)
texto_pie = "Direccion General: Carrera 7 # 32-42 Centro Comercial San Martin Local 107 / PBX: 7431444\nLinea Gratuita Nacional: 01 8000 111612 - atencionalciudadano@saesas.gov.co - www.saesas.gov.co"
self.multi_cell(0, 3, sanear_texto(texto_pie), align='C')
except: pass
self.set_y(-15); self.set_font('Arial', 'I', 8)
self.cell(0, 10, f'Pagina {self.page_no()}', 0, 0, 'C')
# ==========================================
# FASE 1: BÚSQUEDA (CON PROGRESO NATIVO)
# ==========================================
def fase_1_buscar_testigos(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina, progress=gr.Progress()):
progress(0.1, desc="Generando rutas y conectando al navegador...")
resultados = []; urls_vistas = set(); precios_inversos = []
url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
op_inversa = "Venta" if operacion == "Arriendo" else "Arriendo"
url_inversa, _ = construir_urls_final(op_inversa, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
log_visible = f"✅ RUTAS GENERADAS:\n- FR: {url_fr}\n- MC: {url_mc}\n\n"
ua = UserAgent()
with sync_playwright() as p:
browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
# 1. Finca Raíz
progress(0.3, desc="🏢 Escaneando Finca Raíz...")
try:
page = context.new_page()
page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
for _ in range(4): page.mouse.wheel(0, 1000); page.wait_for_timeout(1500)
elementos = page.query_selector_all("a")
cont_fr = 0
for el in elementos:
if cont_fr >= 12: break
try:
href = el.get_attribute("href")
if not es_inmueble_valido(href, "FR"): continue
full_url = f"https://www.fincaraiz.com.co{href}" if href.startswith("/") else href
if full_url in urls_vistas: continue
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
if not card: continue
txt = card.inner_text(); precio = extraer_precio(txt, operacion)
if precio > 0:
img_url = ""
img_el = card.query_selector("img")
if img_el: img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or ""
if img_url and img_url.startswith("/"): img_url = "https://www.fincaraiz.com.co" + img_url
resultados.append({"Portal": "Finca Raiz", "Precio": precio, "Precio_M2": precio / area, "Ubicacion": extraer_ubicacion(txt), "Descripcion": txt.replace('\n', ' | ')[:120] + "...", "URL": full_url, "Imagen": img_url})
urls_vistas.add(full_url); cont_fr += 1
except: continue
page.close(); log_visible += f"✅ FR: {cont_fr} inmuebles extraídos.\n"
except Exception as e: log_visible += f"⚠️ Error en FR.\n"
# 2. Metrocuadrado
progress(0.6, desc="🏢 Escaneando Metrocuadrado...")
try:
page = context.new_page()
page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
for _ in range(4): page.mouse.wheel(0, 1000); page.wait_for_timeout(1500)
elementos = page.query_selector_all("a")
cont_mc = 0
for el in elementos:
if cont_mc >= 12: break
try:
href = el.get_attribute("href")
if not es_inmueble_valido(href, "MC"): continue
full_url = f"https://www.metrocuadrado.com{href}" if href.startswith("/") else href
if full_url in urls_vistas: continue
card = el.evaluate_handle("el => el.closest('li') || el.closest('[class*=\"card\"]') || el.closest('[class*=\"property\"]') || el.parentElement.parentElement.parentElement")
if not card: continue
txt = card.inner_text(); precio = extraer_precio(txt, operacion)
if precio > 0:
img_url = ""
img_el = card.query_selector("img")
if img_el: img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or ""
if img_url and img_url.startswith("/"): img_url = "https://www.metrocuadrado.com" + img_url
resultados.append({"Portal": "Metrocuadrado", "Precio": precio, "Precio_M2": precio / area, "Ubicacion": extraer_ubicacion(txt), "Descripcion": txt.replace('\n', ' | ')[:120] + "...", "URL": full_url, "Imagen": img_url})
urls_vistas.add(full_url); cont_mc += 1
except: continue
page.close(); log_visible += f"✅ MC: {cont_mc} inmuebles extraídos.\n"
except Exception as e: log_visible += f"⚠️ Error en MC.\n"
# 3. Módulo Financiero
progress(0.8, desc="📈 Calculando Cap Rate (Búsqueda Inversa)...")
try:
page = context.new_page(); page.goto(url_inversa, wait_until="domcontentloaded", timeout=45000)
for _ in range(3): page.mouse.wheel(0, 1000); page.wait_for_timeout(1000)
elementos = page.query_selector_all("a")
for el in elementos:
if len(precios_inversos) >= 8: break
try:
href = el.get_attribute("href")
if not es_inmueble_valido(href, "FR"): continue
card = el.evaluate_handle("el => el.closest('article') || el.closest('[class*=\"card\"]') || el.parentElement.parentElement")
if not card: continue
txt = card.inner_text(); precio = extraer_precio(txt, op_inversa)
if precio > 0: precios_inversos.append(precio)
except: continue
page.close(); log_visible += f"✅ Finanzas: Capturados {len(precios_inversos)} valores de {op_inversa}.\n"
except Exception as ex: pass
browser.close()
progress(1.0, desc="¡Búsqueda Finalizada!")
if not resultados:
return log_visible, pd.DataFrame(), gr.update(choices=[], value=[], visible=False), gr.update(interactive=False), [], []
# --- TEXTOS LIMPIOS PARA LAS CASILLAS (EVITA BUGS DE GRADIO) ---
opciones_check = []
for idx, r in enumerate(resultados):
etiqueta = f"ID {idx+1} | {r['Portal'][:2]} | ${r['Precio']:,.0f} | {r['Ubicacion'][:25]}"
r['etiqueta_ui'] = etiqueta
opciones_check.append(etiqueta)
# --- PREPARACIÓN DE TABLA INTERACTIVA CON LINKS CLICKEABLES ---
df_mostrar = pd.DataFrame(resultados)[['Portal', 'Precio', 'Ubicacion', 'URL']].copy()
# Insertamos la columna ID para que el usuario sepa cuál desmarcar
df_mostrar.insert(0, 'ID_Testigo', [f"ID {i+1}" for i in range(len(resultados))])
df_mostrar['Precio'] = df_mostrar['Precio'].apply(lambda x: f"${x:,.0f}")
df_mostrar['URL'] = df_mostrar['URL'].apply(lambda x: f"[🌐 CLIC PARA VER INMUEBLE]({x})")
df_mostrar.rename(columns={'URL': 'Enlace Origial'}, inplace=True)
log_visible += "\n🛑 EXTRACCIÓN FINALIZADA.\n👉 Revisa la tabla de enlaces. Luego desmarca los IDs inválidos en la lista izquierda y haz clic en 'Generar Dictamen'."
return log_visible, df_mostrar, gr.update(choices=opciones_check, value=opciones_check, visible=True), gr.update(interactive=True), resultados, precios_inversos
# ==========================================
# FASE 2: CÁLCULO Y GENERACIÓN (CON PROGRESO NATIVO)
# ==========================================
def fase_2_generar_dictamen(seleccionados_ui, data_cruda, precios_inversos, operacion, barrio, zona_especifica, ciudad, area, tipo, hab, ban, park, progress=gr.Progress()):
if not seleccionados_ui:
return None, "❌ ERROR: No dejaste seleccionado ningún testigo. Selecciona al menos 1.", "<p>Error</p>"
progress(0.1, desc="Filtrando testigos seleccionados...")
resultados_filtrados = [r for r in data_cruda if r['etiqueta_ui'] in seleccionados_ui]
if len(resultados_filtrados) == 0:
return None, "❌ ERROR: La lista de testigos válidos está vacía.", "<p>Error</p>"
df_final = pd.DataFrame(resultados_filtrados)
df_final['V_Homogeneizado'] = df_final['Precio'] * 0.92
progress(0.4, desc="Ejecutando cálculos estadísticos SAE...")
mediana_m2 = df_final['Precio_M2'].median()
minimo_zona = df_final['Precio'].min()
maximo_zona = df_final['Precio'].max()
valor_tecnico_m2 = mediana_m2 * 0.92
valor_total_sugerido = valor_tecnico_m2 * area
valor_maximo_neg = (mediana_m2 * area) * 0.95
valor_minimo_neg = (mediana_m2 * area) * 0.90
cap_rate_txt = "Datos insuficientes en la zona para cruzar la rentabilidad."
if len(precios_inversos) > 0:
mediana_inversa = pd.Series(precios_inversos).median()
try:
if operacion == "Arriendo":
rentabilidad = ((valor_total_sugerido * 12) / mediana_inversa) * 100
cap_rate_txt = f"Valor Comercial Venta Promedio: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual (Cap Rate): {rentabilidad:.2f}%"
else:
rentabilidad = ((mediana_inversa * 12) / valor_total_sugerido) * 100
cap_rate_txt = f"Canon Arriendo Promedio: ${mediana_inversa:,.0f} COP\nRentabilidad Bruta Anual (Cap Rate): {rentabilidad:.2f}%"
except: pass
progress(0.6, desc="Dibujando mapa geoespacial...")
mapa_html, lat_mapa, lon_mapa = generar_mapa(barrio, ciudad)
progress(0.8, desc="Maquetando documento PDF...")
preparar_entorno_pdf()
pdf_path = f"Dictamen_Pericial_SAE_{int(time.time())}.pdf"
pdf = PDF_SAE()
COLOR_ROSADO = (254, 25, 120); COLOR_GRIS = (137, 137, 137); COLOR_NEGRO = (0, 0, 0)
texto_zona = f", Sector {zona_especifica.title()}" if zona_especifica.strip() else ""
pdf.add_page()
pdf.set_font("Arial", 'B', 18); pdf.set_text_color(*COLOR_ROSADO)
pdf.cell(0, 15, sanear_texto("DICTAMEN COMERCIAL Y FINANCIERO"), ln=True, align='C')
pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_GRIS)
pdf.cell(0, 8, sanear_texto(f"METODOLOGIA COMPARATIVA DE MERCADO"), ln=True, align='C')
pdf.line(20, 65, 190, 65); pdf.ln(15)
pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO)
pdf.cell(0, 10, sanear_texto("1. DATOS GEOESPACIALES Y DEL ACTIVO"), ln=True)
pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO)
pdf.cell(0, 6, sanear_texto(f"- Operacion: {operacion.capitalize()}"), ln=True)
pdf.cell(0, 6, sanear_texto(f"- Tipo de Inmueble: {tipo.capitalize()}"), ln=True)
pdf.cell(0, 6, sanear_texto(f"- Ubicacion: Barrio {barrio.title()}{texto_zona}, {ciudad.title()}"), ln=True)
pdf.cell(0, 6, sanear_texto(f"- Coordenadas de Influencia: Lat {lat_mapa:.4f}, Lon {lon_mapa:.4f}"), ln=True)
pdf.cell(0, 6, sanear_texto(f"- Area Construida: {area} m2"), ln=True)
if tipo.lower() not in ["lote", "finca", "bodega", "local", "oficina", "consultorio", "edificio"]:
pdf.cell(0, 6, sanear_texto(f"- Caracteristicas: {hab} Hab, {ban} Banos, {park} Parqueaderos"), ln=True)
pdf.ln(5)
pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_ROSADO)
pdf.cell(0, 10, sanear_texto("2. RENTABILIDAD Y DESEMPEÑO FINANCIERO (CAP RATE)"), ln=True)
pdf.set_font("Arial", '', 11); pdf.set_text_color(*COLOR_NEGRO)
pdf.multi_cell(0, 6, sanear_texto(f"Basado en el cruce de datos de la zona, el activo presenta el siguiente comportamiento:\n{cap_rate_txt}"))
pdf.ln(5)
pdf.add_page()
pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_ROSADO); pdf.set_text_color(255, 255, 255)
pdf.cell(0, 10, sanear_texto(" 3. MATRIZ DE HOMOGENEIZACION Y RANGOS"), ln=True, fill=True)
pdf.set_text_color(*COLOR_NEGRO); pdf.ln(5); pdf.set_font("Arial", '', 11)
pdf.cell(80, 8, sanear_texto("Testigos Validados (Curaduria):"), border=1)
pdf.cell(50, 8, sanear_texto(f"{len(df_final)}"), border=1, ln=True, align='C')
pdf.cell(80, 8, sanear_texto("Valor Minimo (Oferta Zona):"), border=1)
pdf.cell(50, 8, sanear_texto(f"${minimo_zona:,.0f}"), border=1, ln=True, align='C')
pdf.cell(80, 8, sanear_texto("Valor Maximo (Oferta Zona):"), border=1)
pdf.cell(50, 8, sanear_texto(f"${maximo_zona:,.0f}"), border=1, ln=True, align='C')
pdf.cell(80, 8, sanear_texto("Factor Comercializacion:"), border=1)
pdf.cell(50, 8, sanear_texto("-8.00%"), border=1, ln=True, align='C')
pdf.ln(10)
pdf.set_font("Arial", 'B', 14); pdf.set_text_color(*COLOR_ROSADO)
pdf.cell(0, 10, sanear_texto("RANGOS DE NEGOCIACION AUTORIZADOS (CIERRE):"), ln=True)
pdf.set_font("Arial", 'B', 12); pdf.set_text_color(*COLOR_GRIS)
pdf.cell(0, 8, sanear_texto(f"TECHO MAXIMO (-5%): ${valor_maximo_neg:,.0f} COP"), ln=True)
pdf.set_font("Arial", 'B', 16); pdf.set_text_color(0, 128, 0)
pdf.cell(0, 12, sanear_texto(f"VALOR OPTIMO SUGERIDO (-8%): ${valor_total_sugerido:,.0f} COP"), ln=True)
pdf.set_font("Arial", 'B', 12); pdf.set_text_color(200, 0, 0)
pdf.cell(0, 8, sanear_texto(f"PISO MINIMO ACEPTABLE (-10%): ${valor_minimo_neg:,.0f} COP"), ln=True)
pdf.add_page()
pdf.set_font("Arial", 'B', 12); pdf.set_fill_color(*COLOR_GRIS); pdf.set_text_color(255, 255, 255)
pdf.cell(0, 10, sanear_texto(" 4. ANEXO TECNICO: TESTIGOS SELECCIONADOS"), ln=True, fill=True)
pdf.set_text_color(*COLOR_NEGRO); pdf.ln(5)
for idx, r in df_final.iterrows():
if pdf.get_y() > 240: pdf.add_page()
y_start = pdf.get_y(); img_path = descargar_imagen(r['Imagen'], idx); text_x = 10
if img_path and os.path.exists(img_path):
try: pdf.image(img_path, x=10, y=y_start, w=45, h=30); text_x = 60
except: pass
pdf.set_xy(text_x, y_start); pdf.set_font("Arial", 'B', 11)
pdf.cell(0, 6, f"Oferta: ${r['Precio']:,.0f} | Homogeneizado: ${r['V_Homogeneizado']:,.0f}", ln=True)
pdf.set_x(text_x); pdf.set_font("Arial", 'B', 8); pdf.set_text_color(*COLOR_ROSADO)
pdf.cell(0, 4, f"Ubicacion: {sanear_texto(r['Ubicacion'])}", ln=True)
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(*COLOR_NEGRO)
pdf.multi_cell(0, 4, sanear_texto(r['Descripcion']))
pdf.set_x(text_x); pdf.set_font("Arial", '', 8); pdf.set_text_color(0, 102, 204)
pdf.cell(0, 4, f">> Ver testigo original online", link=r['URL'], ln=True)
pdf.set_text_color(*COLOR_NEGRO); y_end = pdf.get_y(); pdf.set_y(max(y_start + 35, y_end + 5))
pdf.set_draw_color(200, 200, 200); pdf.line(10, pdf.get_y(), 200, pdf.get_y()); pdf.ln(4)
if img_path and os.path.exists(img_path):
try: os.remove(img_path)
except: pass
progress(1.0, desc="¡Dictamen Finalizado!")
pdf.output(pdf_path)
resumen = (
f"✅ **¡DICTAMEN FINALIZADO! (Testigos: {len(df_final)})**\n\n"
f"📈 **Techo Máximo:** ${valor_maximo_neg:,.0f}\n"
f"✅ **VALOR ÓPTIMO:** ${valor_total_sugerido:,.0f}\n"
f"🛑 **Piso Mínimo:** ${valor_minimo_neg:,.0f}\n\n"
f"📊 **Análisis de Inversión:**\n{cap_rate_txt}"
)
return pdf_path, resumen, mapa_html
def adaptar_interfaz(tipo):
if tipo in ["Lote", "Finca"]: return [gr.update(visible=False)] * 6
elif tipo in ["Bodega", "Local", "Oficina", "Consultorio", "Edificio"]: return [gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)]
else: return [gr.update(visible=True)] * 6
# --- INTERFAZ GRÁFICA ---
with gr.Blocks(theme=gr.themes.Soft()) as demo:
estado_datos_crudos = gr.State([])
estado_datos_financieros = gr.State([])
gr.Markdown("## 🏢 TramitIA Pro: Dictamen Pericial (Extracción Curada)")
with gr.Row():
with gr.Column(scale=1):
op = gr.Radio(["Arriendo", "Venta"], label="Tipo de Operación", value="Arriendo")
c = gr.Textbox(label="Ciudad", value="Barranquilla")
with gr.Row():
b = gr.Textbox(label="Barrio General", value="La Concepcion")
z = gr.Textbox(label="Zona/Sector Específico", placeholder="Ej: Etapa 2...")
with gr.Row():
t = gr.Dropdown(["Apartamento", "Casa", "Apartaestudio", "Bodega", "Local", "Oficina", "Consultorio", "Lote", "Finca", "Edificio"], label="Tipo de Inmueble", value="Apartamento")
a = gr.Number(label="Área M2", value=70)
with gr.Row():
m2_min = gr.Number(label="M2 Mínimo", value=10); m2_max = gr.Number(label="M2 Máximo", value=200)
with gr.Row():
ascensor = gr.Checkbox(label="Con Ascensor"); piscina = gr.Checkbox(label="Con Piscina")
with gr.Row():
h = gr.Number(label="Hab", value=3); ban = gr.Number(label="Baños", value=2); p = gr.Number(label="Park", value=1)
e = gr.Dropdown(["Menos de 1 año", "1 a 8 años", "9 a 15 años", "16 a 30 años", "Más de 30 años"], label="Antigüedad", value="1 a 8 años")
t.change(adaptar_interfaz, inputs=t, outputs=[h, ban, p, e, ascensor, piscina])
btn_buscar = gr.Button("🔍 1. BUSCAR TESTIGOS DE MERCADO", variant="primary")
gr.Markdown("---")
gr.Markdown("### 📋 CURADURÍA (Selecciona los válidos)")
# La caja de Checkboxes usará textos seguros
selector_testigos = gr.CheckboxGroup(label="Testigos encontrados", choices=[], visible=False)
btn_generar = gr.Button("📄 2. GENERAR DICTAMEN", variant="secondary", interactive=False)
with gr.Column(scale=2):
res_fin = gr.Markdown("### 💰 Resultado, Progreso y Rentabilidad...")
with gr.Tabs():
# Dataframe optimizado para mostrar Markdown clickeables
with gr.TabItem("Exploración Bruta (Links)"): out_df = gr.Dataframe(datatype=["str", "str", "str", "str", "markdown"])
with gr.TabItem("Log del Sistema"): msg = gr.Textbox(lines=12, label="Progreso del Robot")
with gr.TabItem("Descargar Dictamen (PDF)"): out_pdf = gr.File()
with gr.TabItem("Mapa Geoespacial"): mapa_ui = gr.HTML("<p style='text-align:center; color:gray;'>El mapa aparecerá aquí...</p>")
btn_buscar.click(
fase_1_buscar_testigos,
inputs=[op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina],
outputs=[msg, out_df, selector_testigos, btn_generar, estado_datos_crudos, estado_datos_financieros]
)
btn_generar.click(
fase_2_generar_dictamen,
inputs=[selector_testigos, estado_datos_crudos, estado_datos_financieros, op, b, z, c, a, t, h, ban, p],
outputs=[out_pdf, res_fin, mapa_ui]
)
demo.launch(theme=gr.themes.Soft())