jcalbornoz commited on
Commit
e7941cb
·
verified ·
1 Parent(s): 5520491

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +237 -153
app.py CHANGED
@@ -7,7 +7,7 @@ import torch
7
  import numpy as np
8
  import time
9
  import datetime
10
- import traceback
11
  # Imports para PDF Profesional (Platypus)
12
  from reportlab.lib.pagesizes import letter
13
  from reportlab.lib import colors
@@ -25,6 +25,7 @@ import requests
25
  import sqlite3
26
  import pandas as pd
27
  from dotenv import load_dotenv
 
28
 
29
  load_dotenv()
30
 
@@ -65,6 +66,7 @@ def init_db():
65
  id TEXT PRIMARY KEY,
66
  date TEXT,
67
  user TEXT,
 
68
  score INTEGER,
69
  duration TEXT,
70
  zones_count INTEGER,
@@ -73,19 +75,27 @@ def init_db():
73
  json_path TEXT
74
  )
75
  ''')
 
 
 
 
 
 
 
76
  try: c.execute("ALTER TABLE reports ADD COLUMN json_path TEXT")
77
  except: pass
 
78
  conn.commit()
79
  conn.close()
80
 
81
- def save_report_to_db(user, score, duration, zones_count, summary, gemini_status, json_path):
82
  try:
83
  report_id = str(uuid.uuid4())[:8]
84
  date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
85
  conn = sqlite3.connect(DB_NAME)
86
  c = conn.cursor()
87
- c.execute("INSERT INTO reports VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
88
- (report_id, date_str, str(user), score, duration, zones_count, summary, gemini_status, json_path))
89
  conn.commit()
90
  conn.close()
91
  return report_id
@@ -96,13 +106,29 @@ def save_report_to_db(user, score, duration, zones_count, summary, gemini_status
96
  def get_history_df(request: gr.Request):
97
  try:
98
  conn = sqlite3.connect(DB_NAME)
99
- df = pd.read_sql_query("SELECT date, user, score, duration, zones_count, summary, gemini_status FROM reports ORDER BY date DESC", conn)
100
  conn.close()
101
  return df
102
  except: return pd.DataFrame()
103
 
104
  init_db()
105
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  # ==========================================
107
  # CONFIGURACIÓN Y MODELOS
108
  # ==========================================
@@ -154,8 +180,7 @@ class SmartLoader:
154
  self.defect_pipe = None
155
  self.asr_pipe = None
156
  self.sum_pipe = None
157
- # INICIALIZACIÓN EXPLÍCITA PARA EVITAR ERROR
158
- self.sent_pipe = None
159
  self.status = "Iniciando..."
160
  self.vision_active = False
161
 
@@ -165,8 +190,7 @@ class SmartLoader:
165
  self.zone_pipe = pipeline("image-classification", model=PRIMARY_ZONE)
166
  self.furn_pipe = pipeline("object-detection", model=PRIMARY_FURN)
167
  self.vision_active = True
168
- except Exception as e:
169
- print(f"⚠️ Error Visión Primaria: {e}")
170
  print("⚠️ Usando Respaldo Visión...")
171
  try:
172
  self.zone_pipe = pipeline("image-classification", model=BACKUP_ZONE)
@@ -183,7 +207,6 @@ class SmartLoader:
183
  try: self.sum_pipe = pipeline("summarization", model=ADVANCED_SUMMARY)
184
  except: pass
185
 
186
- # Carga segura del modelo de sentimiento
187
  try:
188
  self.sent_pipe = pipeline("text-classification", model=ADVANCED_SENTIMENT)
189
  except:
@@ -212,23 +235,28 @@ def extract_json(text):
212
  return json.loads(match.group(0)) if match else None
213
  except: return None
214
 
215
- def call_gemini_analysis(api_key, inventory_data, transcription):
216
  target_key = api_key or os.getenv("GOOGLE_API_KEY")
217
  if not target_key: return None
218
  target_key = target_key.strip()
219
 
220
- summary_str = f"Transcripción: {transcription[:2000]}\n"
221
  for z, d in inventory_data.items():
222
  summary_str += f"- {z}: {len(d['detailed_items'])} items, Defectos: {[x['label'] for x in d['defects']]}\n"
223
 
 
224
  prompt = f"""
225
- Actúa como Perito Inmobiliario Senior. Analiza estos datos:
226
- {summary_str}
 
227
 
228
- Genera JSON válido: {{
229
- "resumen_ejecutivo": "Texto técnico detallado.",
 
 
 
230
  "presupuesto_estimado": "Tabla markdown de costos.",
231
- "habitabilidad": "SI/NO y por qué.",
232
  "valoracion": "Alta/Media/Bajo"
233
  }}
234
  """
@@ -243,11 +271,24 @@ def call_gemini_analysis(api_key, inventory_data, transcription):
243
  def chat_response(message, history, context, api_key):
244
  target_key = api_key or os.getenv("GOOGLE_API_KEY")
245
  if not target_key: return "⚠️ Error: Falta configurar GOOGLE_API_KEY."
 
 
 
 
 
 
 
 
 
 
 
 
246
  if not context: return "⚠️ Primero analiza un video para tener contexto."
 
247
  payload = {"contents": [{"parts": [{"text": f"Contexto: {context}\nUsuario: {message}"}]}]}
248
- response = call_gemini_api(target_key.strip(), payload)
249
- if isinstance(response, dict) and "candidates" in response:
250
- return response['candidates'][0]['content']['parts'][0]['text']
251
  return "Error Gemini"
252
 
253
  # ==========================================
@@ -266,7 +307,6 @@ def transcribe_audio(video_path):
266
  except: return "Error transcripción."
267
 
268
  def analyze_sentiment(text):
269
- # ACCESO SEGURO A SENT_PIPE
270
  pipe = getattr(loader, 'sent_pipe', None)
271
  if not pipe or len(text) < 5: return "Neutro", 0.0
272
  try:
@@ -286,14 +326,16 @@ def generate_local_summary_verbose(transcription, inventory_data):
286
  total_defects = sum([len(d.get('defects', [])) for d in inventory_data.values()])
287
  zones_list = list(inventory_data.keys())
288
 
289
- summary = f"INFORME TÉCNICO:\n\nSe ha realizado una inspección detallada. "
290
- summary += f"Zonas: {', '.join(zones_list)}. "
291
- summary += f"Total items: {total_items}. "
 
292
 
293
  if total_defects > 0:
294
- summary += f"ALERTA: {total_defects} anomalías detectadas. Ver plan de mantenimiento.\n"
 
295
  else:
296
- summary += "Estado general: BUENO.\n"
297
 
298
  return summary
299
 
@@ -315,7 +357,21 @@ def clean_text_for_pdf(text):
315
  text = text.replace('⚠️', '[!]').replace('✅', '[OK]').replace('📍', '').replace('📦', '').replace('🛠️', '')
316
  return text.encode('latin-1', 'ignore').decode('latin-1')
317
 
318
- def create_pdf_report(inventory_data, transcription, local_summary, gemini_data, score_tuple, sentiment_tuple, duration, report_id):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  filename = f"ROPINS_Reporte_{report_id}.pdf"
320
  output_path = os.path.join(OUTPUT_DIR, filename)
321
 
@@ -339,14 +395,23 @@ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data,
339
  prop_score = score_tuple[0] if isinstance(score_tuple, tuple) else score_tuple
340
  sent_label, _ = sentiment_tuple
341
 
342
- # 1. PORTADA
343
- story.append(Spacer(1, 2*inch))
344
  story.append(Paragraph("INFORME DE AUDITORÍA TÉCNICA INMOBILIARIA", styles['TitleProp']))
345
  story.append(Spacer(1, 0.5*inch))
346
- story.append(Paragraph(f"ID Reporte: {report_id}", styles['SubtitleProp']))
347
- story.append(Paragraph(f"Fecha de Inspección: {datetime.datetime.now().strftime('%d/%m/%Y')}", styles['SubtitleProp']))
348
- story.append(Spacer(1, 2*inch))
349
 
 
 
 
 
 
 
 
 
 
350
  data_metrics = [
351
  ["CALIFICACIÓN TÉCNICA", "DURACIÓN VISITA", "TONO DE VOZ"],
352
  [f"{prop_score}/100", duration, clean_text_for_pdf(sent_label)]
@@ -356,20 +421,35 @@ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data,
356
  ('BACKGROUND', (0,0), (-1,0), colors.lightgrey),
357
  ('ALIGN', (0,0), (-1,-1), 'CENTER'),
358
  ('FONTNAME', (0,0), (-1,-1), 'Helvetica-Bold'),
 
359
  ('BOX', (0,0), (-1,-1), 1, colors.black),
360
  ('TEXTCOLOR', (0,1), (0,1), colors.green if prop_score > 70 else colors.orange)
361
  ]))
362
  story.append(t_metrics)
363
  story.append(PageBreak())
364
 
365
- # 2. RESUMEN
366
- story.append(Paragraph("1. RESUMEN EJECUTIVO", styles['HeadingProp']))
367
- final_sum = local_summary
 
 
 
 
368
  if gemini_data and isinstance(gemini_data, dict):
369
- if "resumen_ejecutivo" in gemini_data:
370
- final_sum = gemini_data['resumen_ejecutivo']
 
371
 
372
- story.append(Paragraph(clean_text_for_pdf(final_sum), styles['BodyProp']))
 
 
 
 
 
 
 
 
 
373
  story.append(Spacer(1, 12))
374
 
375
  # 3. ZONAS
@@ -388,7 +468,7 @@ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data,
388
  story.append(Spacer(1, 12))
389
 
390
  # 4. MANTENIMIENTO
391
- story.append(Paragraph("3. PLAN DE MANTENIMIENTO", styles['HeadingProp']))
392
  budget_txt = "Sin requerimientos críticos."
393
  if gemini_data and isinstance(gemini_data, dict) and 'presupuesto_estimado' in gemini_data:
394
  budget_txt = str(gemini_data['presupuesto_estimado'])
@@ -402,7 +482,7 @@ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data,
402
  story.append(PageBreak())
403
 
404
  # 5. DETALLE
405
- story.append(Paragraph("4. INSPECCIÓN DETALLADA", styles['TitleProp']))
406
  story.append(Spacer(1, 12))
407
 
408
  for zone, data in inventory_data.items():
@@ -423,7 +503,7 @@ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data,
423
  defs = data.get('defects', [])
424
  if defs:
425
  d_str = ', '.join([d['label'] for d in defs])
426
- story.append(Paragraph(f"<b>PATOLOGÍAS:</b> {clean_text_for_pdf(d_str)}", styles['RiskProp']))
427
 
428
  items = data.get('detailed_items', [])
429
  if items:
@@ -487,126 +567,129 @@ def save_json(inventory, transcription, score, summary, report_id):
487
  json.dump(meta, f, indent=4, ensure_ascii=False)
488
  return path
489
 
490
- def process_full(video_path, api_key, request: gr.Request, progress=gr.Progress()):
491
- # 1. Validaciones
492
- if not OPENCV_AVAILABLE: return None, " Error: OpenCV faltante.", "", ""
493
- if not video_path: return None, "⚠️ Error: Falta video.", "", ""
 
 
 
494
 
495
- # 2. Transcripción y Audio
496
  progress(0.1, desc="Audio...")
497
  transcription = transcribe_audio(video_path)
498
 
499
- # 3. Visión
500
- try:
501
- cap = cv2.VideoCapture(video_path)
502
- fps = cap.get(cv2.CAP_PROP_FPS) or 24.0
503
- frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
504
- duration = frames/fps
505
- dur_str = time.strftime('%H:%M:%S', time.gmtime(duration))
506
- step = int(fps * 4.0)
507
-
508
- inventory = {}
509
- curr = 0
510
-
511
- if loader.vision_active:
512
- progress(0.3, desc="Visión...")
513
- while True:
514
- ret, frame = cap.read()
515
- if not ret: break
516
- if curr % step == 0:
517
- pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
518
- if pil.width > 800: pil.thumbnail((800, 800))
519
-
520
- try:
521
- z_res = loader.zone_pipe(pil)[0]
522
- zone = translate_label(z_res['label'].split(':')[-1].strip())
523
- except: zone = "General"
524
-
525
- if zone not in inventory:
526
- inventory[zone] = {"detailed_items": [], "defects": [], "materials": [], "image": None, "score": 0}
527
-
528
- if not inventory[zone]["materials"] and loader.mat_pipe:
529
- try:
530
- m = loader.mat_pipe(pil)
531
- inventory[zone]["materials"] = [{"label": translate_label(m[0]['label'])}]
532
- except: pass
533
-
534
- if loader.defect_pipe:
535
- try:
536
- res = loader.defect_pipe(pil, candidate_labels=list(DEFECT_SOLUTIONS_LOCAL.keys()), threshold=0.15)
537
- for d in res:
538
- lbl = translate_label(d['label'])
539
- box = [int(v) for v in d['box'].values()]
540
- if not any(x['label']==lbl for x in inventory[zone]['defects']):
541
- inventory[zone]['defects'].append({"label": lbl, "score": d['score']})
542
- except: pass
543
-
544
- if loader.furn_pipe:
545
- try:
546
- res = loader.furn_pipe(pil)
547
- for f in res:
548
- if f['score'] > 0.6:
549
- lbl = translate_label(f['label'])
550
- box = [int(v) for v in f['box'].values()]
551
-
552
- sbox = [max(0, box[0]), max(0, box[1]), min(pil.width, box[2]), min(pil.height, box[3])]
553
- crop = pil.crop(sbox)
554
- crop.thumbnail((100, 100))
555
- inventory[zone]['detailed_items'].append({"label": lbl, "crop": crop, "score": f['score']})
556
- except: pass
557
-
558
- score_frame = len(inventory[zone]['detailed_items']) + len(inventory[zone]['defects'])
559
- if score_frame > len(inventory[zone].get('detailed_items', [])) or not inventory[zone]['image']:
560
- inventory[zone]['image'] = annotate_image(pil.copy(), [], [])
561
- curr += 1
562
- else:
563
- # Fallback
564
  ret, frame = cap.read()
565
- if ret:
 
566
  pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
567
- inventory["Video General"] = {"detailed_items": [], "defects": [], "materials": [], "image": pil}
568
- cap.release()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
569
 
570
- except Exception as e:
571
- return None, f"❌ Error Procesamiento: {str(e)}\n{traceback.format_exc()}", "", ""
572
-
573
- # 4. Reporte
574
- progress(0.8, desc="Generando...")
575
- try:
576
- score_val = calculate_score(inventory)
577
- sent_val = analyze_sentiment(transcription)
578
-
579
- local_summary = generate_local_summary_verbose(transcription, inventory)
580
- gemini_key = api_key or os.getenv("GOOGLE_API_KEY")
581
- gemini_data = call_gemini_analysis(gemini_key, inventory, transcription) if gemini_key else None
582
-
583
- pdf_path = create_pdf_report(inventory, transcription, local_summary, gemini_data, (score_val, ""), sent_val, dur_str, report_id)
584
-
585
- final_sum = gemini_data.get('resumen_ejecutivo', local_summary) if gemini_data and "resumen_ejecutivo" in gemini_data else local_summary
586
-
587
- json_path = save_json(inventory, transcription, score_val, final_sum, report_id)
588
-
589
- gemini_status = "Online" if gemini_data else "Offline"
590
-
591
- user_name = getattr(request, 'username', 'anonimo') if request else 'anonimo'
592
- save_report_to_db(user_name, score_val, dur_str, len(inventory), final_sum[:100], gemini_status, json_path)
593
-
594
- html = f"""
595
- <div class="result-card">
596
- <div class="result-header">
597
- <h3 style="margin:0; color:#60a5fa;">✅ Análisis Completado</h3>
598
- <span class="score-badge">Score: {score_val}/100</span>
599
- </div>
600
- <p><b>IA:</b> {gemini_status}</p>
601
- <p><i>"{final_sum[:300]}..."</i></p>
602
  </div>
603
- """
604
 
605
- context = f"Resumen: {final_sum}\nInventario: {str(inventory)}"
606
- return [pdf_path, json_path], html, f"Log: {loader.status}", context
607
 
608
- except Exception as e:
609
- return None, f"❌ Error Generación: {str(e)}\n{traceback.format_exc()}", "", ""
 
 
 
 
 
 
 
 
 
 
 
 
610
 
611
  def get_diagnostic_msg():
612
  key = os.getenv("GOOGLE_API_KEY")
@@ -635,20 +718,21 @@ with gr.Blocks(theme=gr.themes.Soft(primary_hue="blue", neutral_hue="slate"), cs
635
  with gr.Tab("📹 Análisis"):
636
  with gr.Row():
637
  with gr.Column(scale=4):
 
638
  vid = gr.Video(label="Video", format="mp4")
639
  btn = gr.Button("🚀 Generar Informe", variant="primary")
640
  with gr.Column(scale=6):
641
  files = gr.File(label="Descargas (PDF + JSON)", file_count="multiple", interactive=False)
642
  sts = gr.HTML()
643
  log = gr.Textbox(visible=False)
644
- btn.click(process_full, [vid, api_in], [files, sts, log, state])
645
 
646
  with gr.Tab("🤖 Chat"):
647
  gr.ChatInterface(fn=chat_response, additional_inputs=[state, api_in], title="Asistente ROPINS")
648
 
649
  with gr.Tab("📜 Historial"):
650
  ref = gr.Button("🔄 Actualizar")
651
- tbl = gr.Dataframe(headers=["Fecha", "Usuario", "Score", "Duración", "Zonas", "Resumen", "IA"], label="Registros")
652
  ref.click(get_history_df, outputs=tbl)
653
 
654
  if __name__ == "__main__":
 
7
  import numpy as np
8
  import time
9
  import datetime
10
+ import urllib.parse # Para codificar la dirección en mapas
11
  # Imports para PDF Profesional (Platypus)
12
  from reportlab.lib.pagesizes import letter
13
  from reportlab.lib import colors
 
25
  import sqlite3
26
  import pandas as pd
27
  from dotenv import load_dotenv
28
+ import glob
29
 
30
  load_dotenv()
31
 
 
66
  id TEXT PRIMARY KEY,
67
  date TEXT,
68
  user TEXT,
69
+ address TEXT,
70
  score INTEGER,
71
  duration TEXT,
72
  zones_count INTEGER,
 
75
  json_path TEXT
76
  )
77
  ''')
78
+ # Migración segura: verificar si existe la columna address
79
+ try:
80
+ c.execute("SELECT address FROM reports LIMIT 1")
81
+ except sqlite3.OperationalError:
82
+ try: c.execute("ALTER TABLE reports ADD COLUMN address TEXT")
83
+ except: pass
84
+
85
  try: c.execute("ALTER TABLE reports ADD COLUMN json_path TEXT")
86
  except: pass
87
+
88
  conn.commit()
89
  conn.close()
90
 
91
+ def save_report_to_db(user, address, score, duration, zones_count, summary, gemini_status, json_path):
92
  try:
93
  report_id = str(uuid.uuid4())[:8]
94
  date_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
95
  conn = sqlite3.connect(DB_NAME)
96
  c = conn.cursor()
97
+ c.execute("INSERT INTO reports VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
98
+ (report_id, date_str, str(user), str(address), score, duration, zones_count, summary, gemini_status, json_path))
99
  conn.commit()
100
  conn.close()
101
  return report_id
 
106
  def get_history_df(request: gr.Request):
107
  try:
108
  conn = sqlite3.connect(DB_NAME)
109
+ df = pd.read_sql_query("SELECT date, user, address, score, duration, zones_count, summary, gemini_status FROM reports ORDER BY date DESC", conn)
110
  conn.close()
111
  return df
112
  except: return pd.DataFrame()
113
 
114
  init_db()
115
 
116
+ # ==========================================
117
+ # DIAGNÓSTICO
118
+ # ==========================================
119
+ def run_diagnostics():
120
+ key = os.getenv("GOOGLE_API_KEY")
121
+ status = []
122
+ status.append(f"CV2: {'✅' if OPENCV_AVAILABLE else '❌'}")
123
+ status.append(f"MoviePy: {'✅' if MOVIEPY_AVAILABLE else '❌'}")
124
+ if not key:
125
+ status.append("Google Key: ❌ (Modo Local)")
126
+ else:
127
+ status.append(f"Google Key: ✅ ({key[:4]}...)")
128
+ return " | ".join(status)
129
+
130
+ DIAGNOSTIC_MSG = run_diagnostics()
131
+
132
  # ==========================================
133
  # CONFIGURACIÓN Y MODELOS
134
  # ==========================================
 
180
  self.defect_pipe = None
181
  self.asr_pipe = None
182
  self.sum_pipe = None
183
+ self.sent_pipe = None
 
184
  self.status = "Iniciando..."
185
  self.vision_active = False
186
 
 
190
  self.zone_pipe = pipeline("image-classification", model=PRIMARY_ZONE)
191
  self.furn_pipe = pipeline("object-detection", model=PRIMARY_FURN)
192
  self.vision_active = True
193
+ except:
 
194
  print("⚠️ Usando Respaldo Visión...")
195
  try:
196
  self.zone_pipe = pipeline("image-classification", model=BACKUP_ZONE)
 
207
  try: self.sum_pipe = pipeline("summarization", model=ADVANCED_SUMMARY)
208
  except: pass
209
 
 
210
  try:
211
  self.sent_pipe = pipeline("text-classification", model=ADVANCED_SENTIMENT)
212
  except:
 
235
  return json.loads(match.group(0)) if match else None
236
  except: return None
237
 
238
+ def call_gemini_analysis(api_key, inventory_data, transcription, address):
239
  target_key = api_key or os.getenv("GOOGLE_API_KEY")
240
  if not target_key: return None
241
  target_key = target_key.strip()
242
 
243
+ summary_str = f"Ubicación: {address}\nTranscripción: {transcription[:2000]}\n"
244
  for z, d in inventory_data.items():
245
  summary_str += f"- {z}: {len(d['detailed_items'])} items, Defectos: {[x['label'] for x in d['defects']]}\n"
246
 
247
+ # PROMPT POTENCIADO PARA RESPUESTA "SENIOR"
248
  prompt = f"""
249
+ Actúa como un **Perito Inmobiliario y Agente Comercial Senior**. Analiza estos datos técnicos del inmueble ubicado en {address}.
250
+
251
+ Datos: {summary_str}
252
 
253
+ Genera un JSON extendido y profesional con estos campos OBLIGATORIOS:
254
+ {{
255
+ "resumen_ejecutivo": "Informe técnico detallado del estado físico (mínimo 100 palabras).",
256
+ "descripcion_comercial": "Descripción atractiva para venta/alquiler destacando puntos fuertes y ubicación (mínimo 100 palabras).",
257
+ "conclusion_tecnica": "Veredicto final sobre la viabilidad y estado del inmueble.",
258
  "presupuesto_estimado": "Tabla markdown de costos.",
259
+ "habitabilidad": "SI/NO y justificación técnica.",
260
  "valoracion": "Alta/Media/Bajo"
261
  }}
262
  """
 
271
  def chat_response(message, history, context, api_key):
272
  target_key = api_key or os.getenv("GOOGLE_API_KEY")
273
  if not target_key: return "⚠️ Error: Falta configurar GOOGLE_API_KEY."
274
+
275
+ # Intento de recuperación de contexto si está vacío
276
+ if not context:
277
+ try:
278
+ list_of_files = glob.glob(os.path.join(OUTPUT_DIR, '*.json'))
279
+ if list_of_files:
280
+ latest_file = max(list_of_files, key=os.path.getctime)
281
+ with open(latest_file, 'r', encoding='utf-8') as f:
282
+ data = json.load(f)
283
+ context = f"Resumen: {data.get('summary', '')}\nDatos: {str(data.get('inventory', ''))}"
284
+ except: pass
285
+
286
  if not context: return "⚠️ Primero analiza un video para tener contexto."
287
+
288
  payload = {"contents": [{"parts": [{"text": f"Contexto: {context}\nUsuario: {message}"}]}]}
289
+ resp = call_gemini_api(target_key.strip(), payload)
290
+ if isinstance(resp, dict) and "candidates" in resp:
291
+ return resp['candidates'][0]['content']['parts'][0]['text']
292
  return "Error Gemini"
293
 
294
  # ==========================================
 
307
  except: return "Error transcripción."
308
 
309
  def analyze_sentiment(text):
 
310
  pipe = getattr(loader, 'sent_pipe', None)
311
  if not pipe or len(text) < 5: return "Neutro", 0.0
312
  try:
 
326
  total_defects = sum([len(d.get('defects', [])) for d in inventory_data.values()])
327
  zones_list = list(inventory_data.keys())
328
 
329
+ summary = f"INFORME TÉCNICO DE INSPECCIÓN:\n\n"
330
+ summary += f"Se ha realizado una inspección visual y auditiva detallada del inmueble. "
331
+ summary += f"El recorrido abarcó {len(zones_list)} zonas principales: {', '.join(zones_list)}. "
332
+ summary += f"El sistema de visión artificial inventarió un total de {total_items} elementos.\n\n"
333
 
334
  if total_defects > 0:
335
+ summary += f"HALLAZGOS CRÍTICOS: Se detectaron {total_defects} anomalías que requieren atención. "
336
+ summary += "Se recomienda consultar la sección de 'Plan de Mantenimiento' para detalles de reparación.\n"
337
  else:
338
+ summary += "ESTADO GENERAL: El inmueble presenta buenas condiciones de conservación visual, sin patologías graves aparentes en las zonas inspeccionadas.\n"
339
 
340
  return summary
341
 
 
357
  text = text.replace('⚠️', '[!]').replace('✅', '[OK]').replace('📍', '').replace('📦', '').replace('🛠️', '')
358
  return text.encode('latin-1', 'ignore').decode('latin-1')
359
 
360
+ # --- GENERACIÓN DE CÓDIGO QR PARA MAPAS ---
361
+ def generate_qr_map(address):
362
+ try:
363
+ # Generar enlace de Google Maps
364
+ maps_url = f"https://www.google.com/maps/search/?api=1&query={urllib.parse.quote(address)}"
365
+ qr = qrcode.QRCode(box_size=10, border=1)
366
+ qr.add_data(maps_url)
367
+ qr.make(fit=True)
368
+ img = qr.make_image(fill_color="black", back_color="white")
369
+ temp_qr = tempfile.mktemp(suffix=".png")
370
+ img.save(temp_qr)
371
+ return temp_qr
372
+ except: return None
373
+
374
+ def create_pdf_report(inventory_data, transcription, local_summary, gemini_data, score_tuple, sentiment_tuple, duration, report_id, address):
375
  filename = f"ROPINS_Reporte_{report_id}.pdf"
376
  output_path = os.path.join(OUTPUT_DIR, filename)
377
 
 
395
  prop_score = score_tuple[0] if isinstance(score_tuple, tuple) else score_tuple
396
  sent_label, _ = sentiment_tuple
397
 
398
+ # --- 1. PORTADA ---
399
+ story.append(Spacer(1, 1*inch))
400
  story.append(Paragraph("INFORME DE AUDITORÍA TÉCNICA INMOBILIARIA", styles['TitleProp']))
401
  story.append(Spacer(1, 0.5*inch))
402
+ story.append(Paragraph(f"Ubicación: {clean_text_for_pdf(address)}", styles['SubtitleProp']))
403
+ story.append(Paragraph(f"ID: {report_id} | Fecha: {datetime.datetime.now().strftime('%d/%m/%Y')}", styles['SubtitleProp']))
404
+ story.append(Spacer(1, 1*inch))
405
 
406
+ # QR de Ubicación
407
+ qr_path = generate_qr_map(address)
408
+ if qr_path:
409
+ im_qr = PlatypusImage(qr_path, width=2*inch, height=2*inch)
410
+ story.append(Paragraph("Escanea para ver Ubicación en Maps:", styles['SmallProp']))
411
+ story.append(im_qr)
412
+ story.append(Spacer(1, 1*inch))
413
+
414
+ # Tabla Métricas
415
  data_metrics = [
416
  ["CALIFICACIÓN TÉCNICA", "DURACIÓN VISITA", "TONO DE VOZ"],
417
  [f"{prop_score}/100", duration, clean_text_for_pdf(sent_label)]
 
421
  ('BACKGROUND', (0,0), (-1,0), colors.lightgrey),
422
  ('ALIGN', (0,0), (-1,-1), 'CENTER'),
423
  ('FONTNAME', (0,0), (-1,-1), 'Helvetica-Bold'),
424
+ ('FONTSIZE', (0,0), (-1,-1), 12),
425
  ('BOX', (0,0), (-1,-1), 1, colors.black),
426
  ('TEXTCOLOR', (0,1), (0,1), colors.green if prop_score > 70 else colors.orange)
427
  ]))
428
  story.append(t_metrics)
429
  story.append(PageBreak())
430
 
431
+ # 2. ANÁLISIS DETALLADO
432
+ story.append(Paragraph("1. ANÁLISIS TÉCNICO Y COMERCIAL", styles['HeadingProp']))
433
+
434
+ resumen_text = local_summary
435
+ desc_comercial = "Pendiente de análisis comercial."
436
+ conclusion = "Pendiente de veredicto."
437
+
438
  if gemini_data and isinstance(gemini_data, dict):
439
+ resumen_text = gemini_data.get('resumen_ejecutivo', resumen_text)
440
+ desc_comercial = gemini_data.get('descripcion_comercial', desc_comercial)
441
+ conclusion = gemini_data.get('conclusion_tecnica', conclusion)
442
 
443
+ story.append(Paragraph("<b>Resumen Ejecutivo:</b>", styles['BodyProp']))
444
+ story.append(Paragraph(clean_text_for_pdf(resumen_text), styles['BodyProp']))
445
+ story.append(Spacer(1, 12))
446
+
447
+ story.append(Paragraph("<b>Descripción Comercial (Venta/Renta):</b>", styles['BodyProp']))
448
+ story.append(Paragraph(clean_text_for_pdf(desc_comercial), styles['BodyProp']))
449
+ story.append(Spacer(1, 12))
450
+
451
+ story.append(Paragraph("<b>Conclusión Técnica:</b>", styles['BodyProp']))
452
+ story.append(Paragraph(clean_text_for_pdf(conclusion), styles['BodyProp']))
453
  story.append(Spacer(1, 12))
454
 
455
  # 3. ZONAS
 
468
  story.append(Spacer(1, 12))
469
 
470
  # 4. MANTENIMIENTO
471
+ story.append(Paragraph("3. PLAN DE MANTENIMIENTO SUGERIDO", styles['HeadingProp']))
472
  budget_txt = "Sin requerimientos críticos."
473
  if gemini_data and isinstance(gemini_data, dict) and 'presupuesto_estimado' in gemini_data:
474
  budget_txt = str(gemini_data['presupuesto_estimado'])
 
482
  story.append(PageBreak())
483
 
484
  # 5. DETALLE
485
+ story.append(Paragraph("4. INSPECCIÓN DETALLADA POR ZONA", styles['TitleProp']))
486
  story.append(Spacer(1, 12))
487
 
488
  for zone, data in inventory_data.items():
 
503
  defs = data.get('defects', [])
504
  if defs:
505
  d_str = ', '.join([d['label'] for d in defs])
506
+ story.append(Paragraph(f"<b>PATOLOGÍAS DETECTADAS:</b> {clean_text_for_pdf(d_str)}", styles['RiskProp']))
507
 
508
  items = data.get('detailed_items', [])
509
  if items:
 
567
  json.dump(meta, f, indent=4, ensure_ascii=False)
568
  return path
569
 
570
+ def process_full(video_path, address, api_key, request: gr.Request, progress=gr.Progress()):
571
+ if not OPENCV_AVAILABLE: return None, "❌ Error OpenCV", "", ""
572
+ if not video_path: return None, "⚠️ Sube un video", "", ""
573
+ if not address: address = "Sin dirección especificada"
574
+
575
+ user = request.username if request else "anonimo"
576
+ report_id = str(uuid.uuid4())[:6].upper()
577
 
578
+ # 1. Audio
579
  progress(0.1, desc="Audio...")
580
  transcription = transcribe_audio(video_path)
581
 
582
+ # 2. Video
583
+ cap = cv2.VideoCapture(video_path)
584
+ fps = cap.get(cv2.CAP_PROP_FPS) or 24.0
585
+ duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / fps
586
+ dur_str = time.strftime('%H:%M:%S', time.gmtime(duration))
587
+ step = int(fps * 4.0)
588
+
589
+ inventory = {}
590
+ curr = 0
591
+
592
+ if loader.vision_active:
593
+ progress(0.3, desc="Visión...")
594
+ while True:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
595
  ret, frame = cap.read()
596
+ if not ret: break
597
+ if curr % step == 0:
598
  pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
599
+ if pil.width > 800: pil.thumbnail((800, 800))
600
+
601
+ try:
602
+ z_res = loader.zone_pipe(pil)[0]
603
+ zone = translate_label(z_res['label'].split(':')[-1].strip())
604
+ except: zone = "General"
605
+
606
+ if zone not in inventory:
607
+ inventory[zone] = {"detailed_items": [], "defects": [], "materials": [], "image": None, "score": 0}
608
+
609
+ if not inventory[zone]["materials"] and loader.mat_pipe:
610
+ try:
611
+ m = loader.mat_pipe(pil)
612
+ inventory[zone]["materials"] = [{"label": translate_label(m[0]['label'])}]
613
+ except: pass
614
+
615
+ if loader.defect_pipe:
616
+ try:
617
+ res = loader.defect_pipe(pil, candidate_labels=list(DEFECT_SOLUTIONS_LOCAL.keys()), threshold=0.15)
618
+ for d in res:
619
+ lbl = translate_label(d['label'])
620
+ box = [int(v) for v in d['box'].values()]
621
+ if not any(x['label']==lbl for x in inventory[zone]['defects']):
622
+ inventory[zone]['defects'].append({"label": lbl, "score": d['score']})
623
+ except: pass
624
+
625
+ if loader.furn_pipe:
626
+ try:
627
+ res = loader.furn_pipe(pil)
628
+ for f in res:
629
+ if f['score'] > 0.6:
630
+ lbl = translate_label(f['label'])
631
+ box = [int(v) for v in f['box'].values()]
632
+ sbox = [max(0, box[0]), max(0, box[1]), min(pil.width, box[2]), min(pil.height, box[3])]
633
+ crop = pil.crop(sbox)
634
+ crop.thumbnail((100, 100))
635
+ inventory[zone]['detailed_items'].append({"label": lbl, "crop": crop, "score": f['score']})
636
+ except: pass
637
+
638
+ score = len(inventory[zone]['detailed_items']) + len(inventory[zone]['defects'])*2
639
+ if score > inventory[zone]['score'] or not inventory[zone]['image']:
640
+ inventory[zone]['image'] = annotate_image(pil.copy(), [], [])
641
+ inventory[zone]['score'] = score
642
+ curr += 1
643
+ else:
644
+ ret, frame = cap.read()
645
+ if ret:
646
+ pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
647
+ inventory["Video General"] = {"detailed_items": [], "defects": [], "materials": [], "image": pil, "score": 0}
648
+ cap.release()
649
 
650
+ progress(0.8, desc="Reporte...")
651
+ score_val = calculate_score(inventory)
652
+ sent_val = analyze_sentiment(transcription)
653
+
654
+ local_summary = generate_local_summary_verbose(transcription, inventory)
655
+ gemini_key = api_key or os.getenv("GOOGLE_API_KEY")
656
+ gemini_data = call_gemini_analysis(gemini_key, inventory, transcription, address) if gemini_key else None
657
+
658
+ pdf_path = create_pdf_report(inventory, transcription, local_summary, gemini_data, (score_val, ""), sent_val, dur_str, report_id, address)
659
+ final_sum = gemini_data.get('resumen_ejecutivo', local_summary) if gemini_data else local_summary
660
+ json_path = save_json(inventory, transcription, score_val, final_sum, report_id)
661
+
662
+ gemini_status = "Online" if gemini_data else "Offline"
663
+ save_report_to_db(user, address, score_val, dur_str, len(inventory), final_sum[:100], gemini_status, json_path)
664
+
665
+ # MAPA HTML
666
+ encoded_addr = urllib.parse.quote(address)
667
+ map_html = f'<iframe width="100%" height="250" src="https://maps.google.com/maps?q={encoded_addr}&t=&z=15&ie=UTF8&iwloc=&output=embed" frameborder="0" scrolling="no" marginheight="0" marginwidth="0"></iframe>'
668
+
669
+ html = f"""
670
+ <div class="result-card">
671
+ <div class="result-header">
672
+ <h3 style="margin:0; color:#60a5fa;">✅ Análisis Completado</h3>
673
+ <span class="score-badge">Score: {score_val}/100</span>
 
 
 
 
 
 
 
 
674
  </div>
 
675
 
676
+ {map_html}
677
+ <br>
678
 
679
+ <div style="background-color: #111827; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
680
+ <strong style="color:#60a5fa;">📝 Resumen:</strong>
681
+ <p style="margin-top:5px; font-style: italic; color:#d1d5db;">"{final_sum[:500]}..."</p>
682
+ </div>
683
+ <h4 style="color:#93c5fd;">Resumen de Zonas:</h4>
684
+ <ul style="color:#e5e7eb;">
685
+ """
686
+ for z in inventory.keys():
687
+ n_items = len(inventory[z]['detailed_items'])
688
+ html += f"<li><b>{z}:</b> {n_items} elementos identificados.</li>"
689
+ html += "</ul></div>"
690
+
691
+ context = f"Resumen: {final_sum}\nInventario: {str(inventory)}"
692
+ return [pdf_path, json_path], html, f"Log: {loader.status}", context
693
 
694
  def get_diagnostic_msg():
695
  key = os.getenv("GOOGLE_API_KEY")
 
718
  with gr.Tab("📹 Análisis"):
719
  with gr.Row():
720
  with gr.Column(scale=4):
721
+ address_in = gr.Textbox(label="Dirección del Inmueble (Para Mapa y Reporte)", placeholder="Ej. Av. Calle 26 # 50-20, Bogotá")
722
  vid = gr.Video(label="Video", format="mp4")
723
  btn = gr.Button("🚀 Generar Informe", variant="primary")
724
  with gr.Column(scale=6):
725
  files = gr.File(label="Descargas (PDF + JSON)", file_count="multiple", interactive=False)
726
  sts = gr.HTML()
727
  log = gr.Textbox(visible=False)
728
+ btn.click(process_full, [vid, address_in, api_in], [files, sts, log, state])
729
 
730
  with gr.Tab("🤖 Chat"):
731
  gr.ChatInterface(fn=chat_response, additional_inputs=[state, api_in], title="Asistente ROPINS")
732
 
733
  with gr.Tab("📜 Historial"):
734
  ref = gr.Button("🔄 Actualizar")
735
+ tbl = gr.Dataframe(headers=["Fecha", "Usuario", "Ubicación", "Score", "Duración", "Zonas", "Resumen", "IA"], label="Registros")
736
  ref.click(get_history_df, outputs=tbl)
737
 
738
  if __name__ == "__main__":