Spaces:
Sleeping
Sleeping
| 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) --- | |
| def index(): | |
| return render_template('index.html') | |
| 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) |