import os import json import traceback from flask import Flask, request, jsonify, render_template import PyPDF2 from openai import OpenAI import fitz # PymuPDF from PIL import Image # Pillow import pytesseract import io # 1. Configuración de Flask app = Flask(__name__, template_folder='.') # 2. Configuración de OpenAI client = OpenAI( api_key=os.environ.get("OPENAI_API_KEY"), ) def ocr_page(img_bytes): """Realiza OCR en una imagen (byte stream) usando Tesseract.""" try: image = Image.open(io.BytesIO(img_bytes)) text = pytesseract.image_to_string(image, lang='spa') return text except Exception as e: print(f"Error en Pytesseract/OCR: {e}") return "" def extract_text_from_file(file): """Extrae texto de un PDF/TXT, usando OCR si es necesario en todas las páginas.""" file_bytes = file.read() total_text = "" # Intento de extracción nativa try: if file.filename.endswith('.pdf'): pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes)) for page in pdf_reader.pages: total_text += page.extract_text() or "" if len(total_text.strip()) > 100: return total_text.strip() elif file.filename.endswith('.txt'): return file_bytes.decode('utf-8').strip() except Exception: pass # Fallback a OCR if file.filename.endswith('.pdf'): try: document = fitz.open(stream=file_bytes, filetype="pdf") ocr_text = "" for i in range(len(document)): page = document.load_page(i) pix = page.get_pixmap(dpi=300) img_bytes = pix.tobytes("ppm") ocr_text += ocr_page(img_bytes) + "\n" if len(ocr_text.strip()) > 100: return ocr_text.strip() except Exception as e: raise Exception("Fallo la extracción de texto del PDF. Asegúrate de que el documento no sea un archivo de imagen corrupto.") return "" def generate_summary_openai(text): """ Genera un análisis experto en formato JSON. """ try: # Esquema JSON EXPANDIDO json_schema = { "type": "object", "properties": { "tipo_documento_detectado": {"type": "string", "description": "Si es Certificado de Tradición y Libertad (CTL), Certificado JAC, u Otro."}, "analisis_extenso_narrativo": {"type": "string", "description": "Resumen narrativo detallado y crítico, explicando en un párrafo los hallazgos más importantes, el tipo de documento, su validez y los riesgos detectados."}, "identificacion_principal": {"type": "string", "description": "Número de Matrícula Inmobiliaria o Nombre de la Junta/Persona Principal."}, "propietario_actual": {"type": "string", "description": "Nombre completo del propietario o titular del derecho."}, # --- CAMPOS NUEVOS DE ANÁLISIS --- "valor_o_area_registrada": {"type": "string", "description": "El valor catastral o el área (metros cuadrados o hectáreas) de la propiedad si está disponible."}, "fecha_y_entidad_expedicion": {"type": "string", "description": "Fecha de expedición del documento y entidad que lo emitió (ej. 07/10/2025, Superintendencia de Notariado y Registro)."}, "situacion_juridica_detallada": {"type": "string", "description": "Estado legal pormenorizado: Mencionar si hay hipotecas, embargos, o si la tradición es falsa. Usar la palabra 'Limpio' si no hay gravámenes serios."}, # --- FIN CAMPOS NUEVOS --- "ultima_transaccion": {"type": "string", "description": "Acto de la última adquisición (ej. Compraventa, Sucesión, o Acto comunal)."}, "limites_o_restricciones": {"type": "string", "description": "Presencia de Patrimonio de Familia, Afectación a Vivienda, o restricciones comunales/vecinales."}, "analisis_riesgo_legal": {"type": "string", "description": "Clasificación de riesgo: 'Bajo', 'Medio' o 'Alto'. Justificar brevemente esta clasificación."}, "conclusion_final": {"type": "string", "description": "Conclusión final sobre la validez del título o derecho y aptitud para una transacción. Debe ser una recomendación clara (Apto/Apto con Reservas/No Apto)."} }, "required": ["tipo_documento_detectado", "analisis_extenso_narrativo", "propietario_actual", "conclusion_final"] } schema_text = json.dumps(json_schema, indent=2) prompt_text = ( "Eres un **abogado experto en derecho inmobiliario colombiano** y en el análisis exhaustivo de Certificados de Tradición y Libertad (CTL) o Matrículas Inmobiliarias. Tienes la capacidad avanzada de identificar, analizar e interpretar referencias a **sentencias judiciales, actos de la Fiscalía General de la Nación, la Sociedad de Activos Especiales (SAE), decisiones de la autoridad ambiental (ej. CAR), o de otras entidades legales** del estado colombiano que estén contenidas o mencionadas en el historial registral del inmueble." "Tu tarea es analizar detalladamente la historia registral de TODAS las páginas del documento proporcionado y responder con un resumen de **7 puntos clave** utilizando viñetas. " "Los 7 puntos deben ser críticos y exhaustivos para un estudio de títulos detallado:\n\n" "1. **Identificación Completa del Predio y Propietario Actual**: Menciona el número de la **Matrícula Inmobiliaria**, el (los) nombre(s) del (los) propietarios actuales, el tipo de tenencia (ej. Plena Propiedad), e incluye la extracción de los **Linderos Generales** y los **Folios** de donde provienen los datos.\n" "2. **Gravámenes Vigentes y Montos**: Indicar **claramente** la existencia o inexistencia de **Hipoteca, Embargo, o Demanda Civil (Litis)**. Si existen, menciona la anotación y el valor del gravamen si está registrado.\n" "3. **Limitaciones de Dominio Vigentes**: Indicar la existencia o inexistencia de **Patrimonio de Familia, Afectación a Vivienda Familiar, o Servidumbres**. Si existe, mencionar el número de anotación.\n" "4. **Última Transacción Registrada**: Detalla el tipo de acto (ej. compraventa, sucesión, liquidación) y el número de **Escritura Pública** con que se adquirió el inmueble.\n" "5. **Cancelación de Gravámenes Anteriores**: Confirma si todas las hipotecas o embargos anteriores fueron **debidamente cancelados** y menciona el número de anotación de la cancelación.\n" "6. **Riesgos por Procesos Estatales/Judiciales (Fiscalía, SAE y Ambientales)**: Indica si existen anotaciones que sugieran Falsa Tradición, o si el inmueble ha sido objeto de procesos de **extinción de dominio (SAE)**, saneamiento, o si hay menciones de **sentencias judiciales, actos de la Fiscalía, o temas ambientales** (ej. reserva, afectación) que afecten el dominio.\n" "7. **Conclusión de Titulabilidad y Alerta de Entidades Estatales**: Breve conclusión legal sobre la **limpieza** del folio, si existen riesgos mayores para un nuevo comprador o entidad financiera. **ALERTA:** Si NO existe registrada ninguna sentencia o acto de la Fiscalía, SAE, o autoridad ambiental en el CTL/VUR/Escritura, debes mencionarlo claramente en esta conclusión como un dato relevante para el estudio de títulos.\n\n" f"\n\nEsquema JSON requerido:\n{schema_text}" f"\n\nTexto del Documento:\n\n{text}" ) response = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": prompt_text} ], response_format={"type": "json_object"}, temperature=0.3, ) json_string = response.choices[0].message.content.strip() structured_data = json.loads(json_string) return structured_data except Exception as e: raise # --- Rutas de Flask (El resto del app.py se mantiene igual) --- @app.route('/') def index(): return render_template('index.html') @app.route('/summarize', methods=['POST']) def summarize(): if 'file' not in request.files: return jsonify({'error': 'No se ha subido ningún archivo.'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'error': 'No se ha seleccionado ningún archivo.'}), 400 try: raw_text = extract_text_from_file(file) if not raw_text: return jsonify({'error': 'No se pudo extraer texto. Documento ilegible, escaneado de baja calidad o sin texto.'}), 400 structured_summary = generate_summary_openai(raw_text) # Generamos la lista de puntos clave para el panel izquierdo summary_list = [f"**{k.replace('_', ' ').title()}:** {v}" for k, v in structured_summary.items()] # También extraemos el análisis narrativo para el frontend analisis_narrativo = structured_summary.get('analisis_extenso_narrativo', 'Análisis narrativo no disponible.') return jsonify({ 'structured_data': structured_summary, 'summary': summary_list, 'narrative': analisis_narrativo # Enviamos el análisis narrativo separado }) except Exception as e: # --- BLOQUE DE DIAGNÓSTICO CRÍTICO --- print("\n" + "="*50) print("DIAGNÓSTICO: ERROR 500 DURANTE EL PROCESAMIENTO") print(f"Tipo de Error: {type(e).__name__}") traceback.print_exc() print("="*50 + "\n") # ---------------------------------------- return jsonify({'error': f"Error interno del servidor. Detalle: {type(e).__name__} - {str(e)}"}), 500 if __name__ == '__main__': app.run(debug=True)