File size: 10,115 Bytes
c3b3ec1
25cd901
1cccf6d
c3b3ec1
 
 
9e8c546
 
 
 
c3b3ec1
9e8c546
7f7bdb6
c3b3ec1
9e8c546
c3b3ec1
 
 
 
9e8c546
 
 
 
 
c3b3ec1
9e8c546
 
 
 
 
1cccf6d
9e8c546
 
c3b3ec1
1cccf6d
9e8c546
 
 
 
 
 
 
 
 
 
 
6764e62
7b4024f
9e8c546
1cccf6d
9e8c546
 
 
 
7b4024f
9e8c546
205a6eb
9e8c546
 
 
 
 
 
 
 
 
6764e62
9e8c546
c3b3ec1
b4c60f7
6e503cd
b4c60f7
c3b3ec1
6e503cd
6764e62
 
 
6e503cd
 
 
 
 
 
 
 
 
 
 
6764e62
 
6e503cd
 
6764e62
6e503cd
6764e62
 
b4c60f7
 
c3b3ec1
2f2f188
 
 
 
 
 
 
 
 
 
 
 
b4c60f7
6764e62
c3b3ec1
 
 
25cd901
c3b3ec1
 
 
b4c60f7
6764e62
c3b3ec1
 
6764e62
 
 
 
c3b3ec1
 
205a6eb
9e8c546
6e503cd
c3b3ec1
 
 
7f7bdb6
c3b3ec1
 
 
 
 
 
 
 
 
 
 
 
9e8c546
c3b3ec1
25cd901
c3b3ec1
6764e62
 
6e503cd
6764e62
6e503cd
 
 
 
25cd901
ffd1b0a
6e503cd
 
25cd901
9e8c546
c3b3ec1
205a6eb
 
 
 
1cccf6d
205a6eb
 
 
 
0e5dd2d
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
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)