jcalbornoz's picture
Update app.py
2f2f188 verified
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)