Romanes commited on
Commit
87a4839
·
verified ·
1 Parent(s): d4651fb

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +133 -110
app.py CHANGED
@@ -1,20 +1,23 @@
1
  # -*- coding: utf-8 -*-
2
- # App de Evaluación de Tecnologías (Gradio)
3
- # Requisitos: pip install gradio pandas openpyxl
4
-
5
  import gradio as gr
6
  import pandas as pd
7
  import numpy as np
8
  from pathlib import Path
9
  from datetime import datetime
10
  import uuid
 
 
 
 
 
11
 
12
- EXCEL_PATH = Path("/mnt/data/Herramienta de evaluación de tecnologias.xlsx")
13
- TEMPLATE_PATH = Path("/mnt/data/reporte_template.html")
14
- OUT_DIR = EXCEL_PATH.parent / "reportes"
15
- OUT_DIR.mkdir(exist_ok=True)
16
 
17
- def leer_criterios_y_pesos(excel_path: str):
 
18
  res = pd.read_excel(excel_path, sheet_name="Resultado", header=None)
19
  res = res.dropna(how="all")
20
  criterios = []
@@ -41,7 +44,7 @@ MAPA_HOJAS = {
41
  "Potencial transferencia": "5. Potencial transferencia",
42
  }
43
 
44
- def extraer_escala(excel_path: str, hoja: str):
45
  df = pd.read_excel(excel_path, sheet_name=hoja, header=None)
46
  df = df.dropna(how="all")
47
  items = []
@@ -58,7 +61,7 @@ def extraer_escala(excel_path: str, hoja: str):
58
  escala = dict(sorted(escala.items(), key=lambda kv: (-kv[1], kv[0])))
59
  return escala
60
 
61
- def cargar_escalas(excel_path: str, criterios):
62
  out = {}
63
  for crit, _peso in criterios:
64
  clave = None
@@ -75,150 +78,170 @@ def cargar_escalas(excel_path: str, criterios):
75
  out[clave] = {"__error__": str(e)}
76
  return out
77
 
78
- CRITERIOS = leer_criterios_y_pesos(EXCEL_PATH)
79
- ESCALAS = cargar_escalas(EXCEL_PATH, CRITERIOS)
80
-
81
- def evaluar_interactivo(titulo, responsable, facultad, descripcion, fecha, elecciones, evidencias, puntajes_forzados):
82
- filas = []
83
- total_ponderado = 0.0
84
- total_pesos = 0.0
85
-
86
- for crit, peso in CRITERIOS:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  clave = None
88
- for k in ESCALAS:
89
  if k.lower() in crit.lower() or crit.lower() in k.lower():
90
  clave = k; break
91
  if clave is None: clave = crit
 
 
 
 
 
 
 
92
 
93
- opcion = elecciones.get(clave, "") or ""
94
- texto_ev = evidencias.get(clave, "") or ""
95
- pf = puntajes_forzados.get(clave, "").strip()
 
 
 
96
  usar_forzado = False
97
  try:
98
  pf_val = float(pf)
99
  usar_forzado = True
100
  except:
101
  pf_val = None
102
-
103
  if usar_forzado:
104
  puntaje = pf_val
105
  else:
106
- escala = ESCALAS.get(clave, {})
107
- puntaje = escala.get(opcion.strip(), np.nan)
108
-
109
- puntaje_pond = (puntaje if pd.notna(puntaje) else 0) * float(peso)
110
  filas.append({
111
- "criterio": clave, "peso": peso, "opcion": opcion,
112
- "puntaje": puntaje, "puntaje_ponderado": puntaje_pond,
113
  "evidencia": texto_ev
114
  })
115
- total_ponderado += puntaje_pond
116
- total_pesos += float(peso)
117
 
118
- html = TEMPLATE_PATH.read_text(encoding="utf-8")
 
119
  filas_html = ""
120
  for f in filas:
121
  filas_html += "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n".format(
122
  f['criterio'], f['peso'], f['opcion'], f['puntaje'], f['puntaje_ponderado'], f['evidencia']
123
  )
124
-
125
  fecha_str = fecha if fecha else datetime.now().strftime("%Y-%m-%d")
126
  html = (html
127
- .replace("{{titulo}}", str(titulo or ""))
128
- .replace("{{responsable}}", str(responsable or ""))
129
- .replace("{{facultad}}", str(facultad or ""))
130
- .replace("{{fecha}}", str(fecha_str))
131
- .replace("{{descripcion}}", str(descripcion or ""))
132
- .replace("{{filas}}", filas_html)
133
- .replace("{{total}}", "{:.2f}".format(total_ponderado))
134
- .replace("{{resumen}}", "La evaluación se realizó con base en los criterios y pesos de la herramienta. Se adjuntan observaciones por criterio en la tabla.")
135
- .replace("{{archivo_fuente}}", EXCEL_PATH.name)
136
- .replace("{{fecha_gen}}", datetime.now().strftime("%Y-%m-%d %H:%M"))
137
  )
138
 
139
- report_id = uuid.uuid4().hex[:8]
140
- out_html = OUT_DIR / "reporte_{}.html".format(report_id)
141
- out_csv = OUT_DIR / "detalle_{}.csv".format(report_id)
142
-
143
  pd.DataFrame(filas).to_csv(out_csv, index=False)
144
  out_html.write_text(html, encoding="utf-8")
145
 
146
- df = pd.DataFrame(filas)
147
- return df, float(total_ponderado), str(out_html), str(out_csv)
148
 
 
149
  with gr.Blocks(title="Evaluación de Tecnologías") as demo:
150
  gr.Markdown("# Evaluación de Tecnologías")
151
- gr.Markdown("Complete la información de la tecnología y seleccione una opción por criterio. Opcionalmente puede forzar un puntaje numérico.")
 
 
152
 
153
  with gr.Row():
154
- titulo = gr.Textbox(label="Título de la tecnología", placeholder="p.ej., Sensor de humedad IoT para suelos")
155
- responsable = gr.Textbox(label="Responsable / Autor", placeholder="Nombre y cargo")
156
  with gr.Row():
157
- facultad = gr.Textbox(label="Facultad / Dependencia", placeholder="p.ej., Facultad de Ciencias")
158
- fecha = gr.Textbox(label="Fecha (YYYY-MM-DD)", placeholder="2025-09-16")
159
  descripcion = gr.Textbox(label="Descripción breve", lines=3)
160
 
161
- elecciones = {}
162
- evidencias = {}
163
- puntajes_forzados = {}
164
 
165
- gr.Markdown("## Criterios")
166
- for crit, peso in CRITERIOS:
167
- clave = None
168
- for k in ESCALAS:
169
- if k.lower() in crit.lower() or crit.lower() in k.lower():
170
- clave = k; break
171
- if clave is None: clave = crit
172
-
173
- opciones = list(ESCALAS.get(clave, {}).keys())
174
- if "__error__" in opciones or not opciones:
175
- opciones = ["(No se pudo leer opciones desde el Excel)"]
176
-
177
- with gr.Group():
178
- gr.Markdown("### {} <span class='badge'>Peso: {}</span>".format(clave, peso))
179
- elecciones[clave] = gr.Dropdown(choices=opciones, label="Opción para '{}'".format(clave), multiselect=False)
180
- puntajes_forzados[clave] = gr.Textbox(label="Puntaje forzado (opcional, número)")
181
- evidencias[clave] = gr.Textbox(label="Evidencia / comentarios", lines=2)
182
-
183
- btn = gr.Button("Evaluar y generar reporte")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
  tabla = gr.Dataframe(headers=["criterio","peso","opcion","puntaje","puntaje_ponderado","evidencia"], label="Detalle")
185
  total = gr.Number(label="Total ponderado")
186
  out_html = gr.File(label="Descargar reporte HTML")
187
  out_csv = gr.File(label="Descargar detalle CSV")
188
 
189
- def _evaluate_wrapper(titulo, responsable, facultad, descripcion, fecha, *args):
190
- e = {}
191
- pf = {}
192
- ev = {}
193
-
194
- tripletas = []
195
- for crit, _ in CRITERIOS:
196
- clave = None
197
- for k in ESCALAS:
198
- if k.lower() in crit.lower() or crit.lower() in k.lower():
199
- clave = k; break
200
- if clave is None: clave = crit
201
- tripletas.append(clave)
202
-
203
- for i, c in enumerate(tripletas):
204
- base = 3*i
205
- e[c] = args[base] if base < len(args) else None
206
- pf[c] = args[base+1] if base+1 < len(args) else ""
207
- ev[c] = args[base+2] if base+2 < len(args) else ""
208
-
209
- df, tot, path_html, path_csv = evaluar_interactivo(titulo, responsable, facultad, descripcion, fecha, e, ev, pf)
210
- return df, tot, path_html, path_csv
211
-
212
- inputs = [titulo, responsable, facultad, descripcion, fecha]
213
- for crit, _ in CRITERIOS:
214
- clave = None
215
- for k in ESCALAS:
216
- if k.lower() in crit.lower() or crit.lower() in k.lower():
217
- clave = k; break
218
- if clave is None: clave = crit
219
- inputs += [elecciones[clave], puntajes_forzados[clave], evidencias[clave]]
220
-
221
- btn.click(_evaluate_wrapper, inputs=inputs, outputs=[tabla, total, out_html, out_csv])
222
 
223
  if __name__ == "__main__":
224
  demo.launch()
 
1
  # -*- coding: utf-8 -*-
 
 
 
2
  import gradio as gr
3
  import pandas as pd
4
  import numpy as np
5
  from pathlib import Path
6
  from datetime import datetime
7
  import uuid
8
+ import os
9
+
10
+ # --- Config ---
11
+ DEFAULT_EXCEL = Path("Herramienta de evaluación de tecnologias.xlsx")
12
+ DEFAULT_TEMPLATE = Path("reporte_template.html")
13
 
14
+ def ensure_outdir(base: Path) -> Path:
15
+ out_dir = base / "reportes"
16
+ out_dir.mkdir(parents=True, exist_ok=True) # <- parents=True para evitar error
17
+ return out_dir
18
 
19
+ # -------- Utilidades para leer la herramienta --------
20
+ def leer_criterios_y_pesos(excel_path: Path):
21
  res = pd.read_excel(excel_path, sheet_name="Resultado", header=None)
22
  res = res.dropna(how="all")
23
  criterios = []
 
44
  "Potencial transferencia": "5. Potencial transferencia",
45
  }
46
 
47
+ def extraer_escala(excel_path: Path, hoja: str):
48
  df = pd.read_excel(excel_path, sheet_name=hoja, header=None)
49
  df = df.dropna(how="all")
50
  items = []
 
61
  escala = dict(sorted(escala.items(), key=lambda kv: (-kv[1], kv[0])))
62
  return escala
63
 
64
+ def cargar_escalas(excel_path: Path, criterios):
65
  out = {}
66
  for crit, _peso in criterios:
67
  clave = None
 
78
  out[clave] = {"__error__": str(e)}
79
  return out
80
 
81
+ def _load_state(excel_file):
82
+ """Devuelve (excel_path, criterios, escalas, out_dir, template_path)"""
83
+ # 1) Excel: si el usuario sube archivo, usar ese. Si no, usar DEFAULT_EXCEL.
84
+ if excel_file is not None and hasattr(excel_file, 'name'):
85
+ excel_path = Path(excel_file.name)
86
+ else:
87
+ excel_path = DEFAULT_EXCEL
88
+ if not excel_path.exists():
89
+ raise FileNotFoundError(f"No se encontró el Excel: {excel_path}. Súbelo o colócalo junto al app.py")
90
+ criterios = leer_criterios_y_pesos(excel_path)
91
+ escalas = cargar_escalas(excel_path, criterios)
92
+ # 2) Template
93
+ template_path = DEFAULT_TEMPLATE if DEFAULT_TEMPLATE.exists() else Path(__file__).parent / "reporte_template.html"
94
+ if not template_path.exists():
95
+ # fallback mínimo
96
+ template_path.write_text("<html><body><h1>Reporte</h1>{filas}</body></html>", encoding="utf-8")
97
+ # 3) Carpeta de reportes relativa al excel
98
+ out_dir = ensure_outdir(excel_path.parent)
99
+ return excel_path, criterios, escalas, out_dir, template_path
100
+
101
+ # -------- Lógica de evaluación --------
102
+ def evaluar_interactivo(excel_file, titulo, responsable, facultad, descripcion, fecha, *args):
103
+ excel_path, criterios, escalas, out_dir, template_path = _load_state(excel_file)
104
+
105
+ # Reconstruir tripletas por criterio
106
+ elecciones, puntajes_forzados, evidencias = {}, {}, {}
107
+ claves = []
108
+ for crit, _ in criterios:
109
  clave = None
110
+ for k in escalas:
111
  if k.lower() in crit.lower() or crit.lower() in k.lower():
112
  clave = k; break
113
  if clave is None: clave = crit
114
+ claves.append(clave)
115
+
116
+ for i, c in enumerate(claves):
117
+ base = 3*i
118
+ elecciones[c] = args[base] if base < len(args) else None
119
+ puntajes_forzados[c] = args[base+1] if base+1 < len(args) else ""
120
+ evidencias[c] = args[base+2] if base+2 < len(args) else ""
121
 
122
+ filas = []
123
+ total_ponderado = 0.0
124
+ for (crit, peso), c in zip(criterios, claves):
125
+ opcion = (elecciones.get(c) or "").strip()
126
+ texto_ev = evidencias.get(c, "") or ""
127
+ pf = (puntajes_forzados.get(c, "") or "").strip()
128
  usar_forzado = False
129
  try:
130
  pf_val = float(pf)
131
  usar_forzado = True
132
  except:
133
  pf_val = None
 
134
  if usar_forzado:
135
  puntaje = pf_val
136
  else:
137
+ puntaje = escalas.get(c, {}).get(opcion, np.nan)
138
+ ppond = (puntaje if pd.notna(puntaje) else 0) * float(peso)
 
 
139
  filas.append({
140
+ "criterio": c, "peso": peso, "opcion": opcion,
141
+ "puntaje": puntaje, "puntaje_ponderado": ppond,
142
  "evidencia": texto_ev
143
  })
144
+ total_ponderado += ppond
 
145
 
146
+ # Render reporte
147
+ html = Path(template_path).read_text(encoding="utf-8")
148
  filas_html = ""
149
  for f in filas:
150
  filas_html += "<tr><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td><td>{}</td></tr>\n".format(
151
  f['criterio'], f['peso'], f['opcion'], f['puntaje'], f['puntaje_ponderado'], f['evidencia']
152
  )
 
153
  fecha_str = fecha if fecha else datetime.now().strftime("%Y-%m-%d")
154
  html = (html
155
+ .replace("{titulo}", str(titulo or ""))
156
+ .replace("{responsable}", str(responsable or ""))
157
+ .replace("{facultad}", str(facultad or ""))
158
+ .replace("{fecha}", str(fecha_str))
159
+ .replace("{descripcion}", str(descripcion or ""))
160
+ .replace("{filas}", filas_html)
161
+ .replace("{total}", "{:.2f}".format(total_ponderado))
162
+ .replace("{resumen}", "La evaluación se realizó con base en los criterios y pesos de la herramienta. Se adjuntan observaciones por criterio en la tabla.")
163
+ .replace("{archivo_fuente}", excel_path.name)
164
+ .replace("{fecha_gen}", datetime.now().strftime("%Y-%m-%d %H:%M"))
165
  )
166
 
167
+ rid = uuid.uuid4().hex[:8]
168
+ out_html = out_dir / f"reporte_{rid}.html"
169
+ out_csv = out_dir / f"detalle_{rid}.csv"
 
170
  pd.DataFrame(filas).to_csv(out_csv, index=False)
171
  out_html.write_text(html, encoding="utf-8")
172
 
173
+ return pd.DataFrame(filas), float(total_ponderado), str(out_html), str(out_csv)
 
174
 
175
+ # -------- UI --------
176
  with gr.Blocks(title="Evaluación de Tecnologías") as demo:
177
  gr.Markdown("# Evaluación de Tecnologías")
178
+ gr.Markdown("Suba el Excel de la herramienta (si no está en el mismo directorio), complete la información y genere el reporte.")
179
+
180
+ excel_file = gr.File(label="Excel de la herramienta (.xlsx)", file_types=[".xlsx"], optional=True)
181
 
182
  with gr.Row():
183
+ titulo = gr.Textbox(label="Título de la tecnología")
184
+ responsable = gr.Textbox(label="Responsable / Autor")
185
  with gr.Row():
186
+ facultad = gr.Textbox(label="Facultad / Dependencia")
187
+ fecha = gr.Textbox(label="Fecha (YYYY-MM-DD)")
188
  descripcion = gr.Textbox(label="Descripción breve", lines=3)
189
 
190
+ # Cargamos criterios/escala dinámicamente cuando se cambie el Excel
191
+ state_criterios = gr.State([])
192
+ state_escalas = gr.State({})
193
 
194
+ def cargar_campos(excel_file):
195
+ try:
196
+ excel_path, criterios, escalas, out_dir, template_path = _load_state(excel_file)
197
+ # Devolvemos lista de criterios y para cada criterio, la lista de opciones
198
+ claves, opciones_por = [], []
199
+ for crit, _ in criterios:
200
+ clave = None
201
+ for k in escalas:
202
+ if k.lower() in crit.lower() or crit.lower() in k.lower():
203
+ clave = k; break
204
+ if clave is None: clave = crit
205
+ claves.append(clave)
206
+ opts = list(escalas.get(clave, {}).keys())
207
+ if "__error__" in opts or not opts:
208
+ opts = ["(No se pudo leer opciones)"]
209
+ opciones_por.append(opts)
210
+ return claves, opciones_por, gr.update(visible=False), ""
211
+ except Exception as e:
212
+ return [], [], gr.update(visible=True), f"Error: {e}"
213
+
214
+ cargar_btn = gr.Button("Cargar criterios desde el Excel")
215
+ error_box = gr.Markdown(visible=False)
216
+
217
+ # Contenedores dinámicos
218
+ criterios_container = gr.Group()
219
+ inputs_dinamicos = []
220
+
221
+ def construir_form(claves, opciones_por):
222
+ # reconstruye los inputs por criterio
223
+ comps = []
224
+ for clave, opciones in zip(claves, opciones_por):
225
+ drop = gr.Dropdown(choices=opciones, label=f"Opción para '{clave}'", multiselect=False)
226
+ pf = gr.Textbox(label="Puntaje forzado (opcional)")
227
+ ev = gr.Textbox(label="Evidencia / comentarios", lines=2)
228
+ comps.extend([drop, pf, ev])
229
+ return comps
230
+
231
+ cargar_btn.click(cargar_campos, inputs=[excel_file], outputs=[state_criterios, state_escalas, error_box, error_box])
232
+
233
+ # Botón de evaluar
234
+ evaluar_btn = gr.Button("Evaluar y generar reporte")
235
  tabla = gr.Dataframe(headers=["criterio","peso","opcion","puntaje","puntaje_ponderado","evidencia"], label="Detalle")
236
  total = gr.Number(label="Total ponderado")
237
  out_html = gr.File(label="Descargar reporte HTML")
238
  out_csv = gr.File(label="Descargar detalle CSV")
239
 
240
+ # NOTA: por simplicidad en Spaces, pedimos al usuario dar clic en "Cargar criterios..." antes de evaluar.
241
+ # Para unir inputs dinámicos, recibimos un número flexible de *args.
242
+ evaluar_btn.click(evaluar_interactivo,
243
+ inputs=[excel_file, titulo, responsable, facultad, descripcion, fecha],
244
+ outputs=[tabla, total, out_html, out_csv])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
 
246
  if __name__ == "__main__":
247
  demo.launch()