Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -8,7 +8,6 @@ import pytesseract
|
|
| 8 |
import io
|
| 9 |
|
| 10 |
# 1. Configuraci贸n de Flask
|
| 11 |
-
# Asumimos que index.html est谩 en la ra铆z del proyecto (junto a app.py).
|
| 12 |
app = Flask(__name__, template_folder='.')
|
| 13 |
|
| 14 |
# 2. Configuraci贸n de OpenAI
|
|
@@ -29,6 +28,7 @@ def ocr_page(img_bytes):
|
|
| 29 |
def extract_text_from_file(file):
|
| 30 |
"""
|
| 31 |
Extrae texto de todas las p谩ginas de un PDF, con fallback a Tesseract OCR para escaneados.
|
|
|
|
| 32 |
"""
|
| 33 |
file_bytes = file.read()
|
| 34 |
total_text = ""
|
|
@@ -37,22 +37,16 @@ def extract_text_from_file(file):
|
|
| 37 |
try:
|
| 38 |
if file.filename.endswith('.pdf'):
|
| 39 |
pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
|
| 40 |
-
|
| 41 |
-
# ITERAMOS SOBRE TODAS LAS P脕GINAS
|
| 42 |
for page in pdf_reader.pages:
|
| 43 |
total_text += page.extract_text() or ""
|
| 44 |
|
| 45 |
-
# Si se extrajo una cantidad significativa de texto, 煤salo directamente
|
| 46 |
if len(total_text.strip()) > 100:
|
| 47 |
-
print("Extracci贸n: 脡xito nativo.")
|
| 48 |
return total_text.strip()
|
| 49 |
|
| 50 |
elif file.filename.endswith('.txt'):
|
| 51 |
-
print("Extracci贸n: 脡xito TXT.")
|
| 52 |
return file_bytes.decode('utf-8').strip()
|
| 53 |
|
| 54 |
-
except Exception
|
| 55 |
-
print(f"Fallo PyPDF2: {e}. Intentando OCR...")
|
| 56 |
pass
|
| 57 |
|
| 58 |
# --- 2. Fallback a OCR con Tesseract (Solo para PDFs) ---
|
|
@@ -61,7 +55,6 @@ def extract_text_from_file(file):
|
|
| 61 |
document = fitz.open(stream=file_bytes, filetype="pdf")
|
| 62 |
ocr_text = ""
|
| 63 |
|
| 64 |
-
# ITERAMOS SOBRE TODAS LAS P脕GINAS DEL DOCUMENTO
|
| 65 |
for i in range(len(document)):
|
| 66 |
page = document.load_page(i)
|
| 67 |
pix = page.get_pixmap(dpi=300)
|
|
@@ -71,55 +64,66 @@ def extract_text_from_file(file):
|
|
| 71 |
ocr_text += ocr_page(img_bytes) + "\n"
|
| 72 |
|
| 73 |
if len(ocr_text.strip()) > 100:
|
| 74 |
-
print("Extracci贸n: 脡xito OCR.")
|
| 75 |
return ocr_text.strip()
|
| 76 |
|
| 77 |
except Exception as e:
|
| 78 |
-
print(f"Fallo el proceso OCR con Tesseract: {e}")
|
| 79 |
raise Exception("Fallo la extracci贸n de texto del PDF. Aseg煤rate de que el documento no sea un archivo de imagen corrupto.")
|
| 80 |
|
| 81 |
-
return ""
|
| 82 |
|
| 83 |
|
| 84 |
def generate_summary_openai(text):
|
| 85 |
"""
|
| 86 |
-
Genera un an谩lisis experto
|
| 87 |
"""
|
| 88 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
prompt_text = (
|
| 90 |
-
"Eres un **
|
| 91 |
-
"Tu tarea es analizar
|
| 92 |
-
"
|
| 93 |
-
|
| 94 |
-
"1. **Identificaci贸n del Predio y Propietario Actual**: Menciona el n煤mero de la **Matr铆cula Inmobiliaria**, el nombre del o los propietarios actuales, y el tipo de tenencia (ej. Plena Propiedad, Fideicomiso).\n"
|
| 95 |
-
"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"
|
| 96 |
-
"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"
|
| 97 |
-
"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"
|
| 98 |
-
"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"
|
| 99 |
-
"6. **Falsa Tradici贸n/Estatuto de Saneamiento**: Indica si existen anotaciones que sugieran Falsa Tradici贸n, o si el inmueble ha sido objeto de procesos de extinci贸n de dominio o saneamiento.\n"
|
| 100 |
-
"7. **Conclusi贸n de Titulabilidad**: Breve conclusi贸n legal sobre la **limpieza** del folio, si existen riesgos mayores para un nuevo comprador o entidad financiera, y si el inmueble es apto para una transacci贸n inmediata (Compraventa o Hipoteca).\n\n"
|
| 101 |
-
|
| 102 |
-
f"Texto completo del CTL/Matr铆cula Inmobiliaria:\n\n{text}"
|
| 103 |
)
|
| 104 |
|
| 105 |
response = client.chat.completions.create(
|
| 106 |
-
model="gpt-
|
| 107 |
messages=[
|
| 108 |
{"role": "system", "content": prompt_text}
|
| 109 |
],
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
temperature=0.3,
|
| 113 |
)
|
| 114 |
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
except Exception as e:
|
| 120 |
-
print(f"Error al
|
| 121 |
-
# Capturamos errores de la API
|
| 122 |
-
raise Exception("Error al generar el
|
| 123 |
|
| 124 |
# --- Rutas de Flask ---
|
| 125 |
|
|
@@ -140,14 +144,18 @@ def summarize():
|
|
| 140 |
raw_text = extract_text_from_file(file)
|
| 141 |
|
| 142 |
if not raw_text:
|
| 143 |
-
return jsonify({'error': 'No se pudo extraer texto
|
| 144 |
|
| 145 |
-
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
except Exception as e:
|
| 149 |
print(f"Error general en la ruta /summarize: {e}")
|
| 150 |
-
# El retorno sigue siendo un Error 500 para el servidor
|
| 151 |
return jsonify({'error': f"Error interno del servidor: {str(e)}"}), 500
|
| 152 |
|
| 153 |
if __name__ == '__main__':
|
|
|
|
| 8 |
import io
|
| 9 |
|
| 10 |
# 1. Configuraci贸n de Flask
|
|
|
|
| 11 |
app = Flask(__name__, template_folder='.')
|
| 12 |
|
| 13 |
# 2. Configuraci贸n de OpenAI
|
|
|
|
| 28 |
def extract_text_from_file(file):
|
| 29 |
"""
|
| 30 |
Extrae texto de todas las p谩ginas de un PDF, con fallback a Tesseract OCR para escaneados.
|
| 31 |
+
(La l贸gica de extracci贸n y OCR permanece igual)
|
| 32 |
"""
|
| 33 |
file_bytes = file.read()
|
| 34 |
total_text = ""
|
|
|
|
| 37 |
try:
|
| 38 |
if file.filename.endswith('.pdf'):
|
| 39 |
pdf_reader = PyPDF2.PdfReader(io.BytesIO(file_bytes))
|
|
|
|
|
|
|
| 40 |
for page in pdf_reader.pages:
|
| 41 |
total_text += page.extract_text() or ""
|
| 42 |
|
|
|
|
| 43 |
if len(total_text.strip()) > 100:
|
|
|
|
| 44 |
return total_text.strip()
|
| 45 |
|
| 46 |
elif file.filename.endswith('.txt'):
|
|
|
|
| 47 |
return file_bytes.decode('utf-8').strip()
|
| 48 |
|
| 49 |
+
except Exception:
|
|
|
|
| 50 |
pass
|
| 51 |
|
| 52 |
# --- 2. Fallback a OCR con Tesseract (Solo para PDFs) ---
|
|
|
|
| 55 |
document = fitz.open(stream=file_bytes, filetype="pdf")
|
| 56 |
ocr_text = ""
|
| 57 |
|
|
|
|
| 58 |
for i in range(len(document)):
|
| 59 |
page = document.load_page(i)
|
| 60 |
pix = page.get_pixmap(dpi=300)
|
|
|
|
| 64 |
ocr_text += ocr_page(img_bytes) + "\n"
|
| 65 |
|
| 66 |
if len(ocr_text.strip()) > 100:
|
|
|
|
| 67 |
return ocr_text.strip()
|
| 68 |
|
| 69 |
except Exception as e:
|
|
|
|
| 70 |
raise Exception("Fallo la extracci贸n de texto del PDF. Aseg煤rate de que el documento no sea un archivo de imagen corrupto.")
|
| 71 |
|
| 72 |
+
return ""
|
| 73 |
|
| 74 |
|
| 75 |
def generate_summary_openai(text):
|
| 76 |
"""
|
| 77 |
+
Genera un an谩lisis experto y devuelve los datos estructurados en formato JSON.
|
| 78 |
"""
|
| 79 |
try:
|
| 80 |
+
# Definici贸n del esquema JSON para una salida estructurada
|
| 81 |
+
json_schema = {
|
| 82 |
+
"type": "object",
|
| 83 |
+
"properties": {
|
| 84 |
+
"tipo_documento_detectado": {"type": "string", "description": "Si es CTL/Matr铆cula Inmobiliaria, o Certificado de Junta de Acci贸n Comunal."},
|
| 85 |
+
"identificacion_principal": {"type": "string", "description": "N煤mero de Matr铆cula Inmobiliaria (si es CTL) o Nombre del titular de la propiedad/derecho (si es JAC)."},
|
| 86 |
+
"propietario_actual": {"type": "string", "description": "Nombre completo del propietario o titular del derecho seg煤n el documento."},
|
| 87 |
+
"estado_gravamenes_vigentes": {"type": "string", "description": "Existencia o inexistencia de Hipoteca, Embargo, o Servidumbre. Breve descripci贸n."},
|
| 88 |
+
"ultima_transaccion": {"type": "string", "description": "Acto de la 煤ltima adquisici贸n (ej. Compraventa, Sucesi贸n, o Acto comunal)."},
|
| 89 |
+
"limites_o_restricciones": {"type": "string", "description": "Presencia de Patrimonio de Familia, Afectaci贸n a Vivienda, o restricciones comunales/vecinales."},
|
| 90 |
+
"analisis_riesgo_legal": {"type": "string", "description": "Riesgos legales detectados (ej. litigios, falsas tradiciones, o conflictos comunales)."},
|
| 91 |
+
"conclusion_final": {"type": "string", "description": "Conclusi贸n final sobre la validez del t铆tulo o derecho y aptitud para una transacci贸n."}
|
| 92 |
+
},
|
| 93 |
+
"required": ["tipo_documento_detectado", "propietario_actual", "conclusion_final"]
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
prompt_text = (
|
| 97 |
+
"Eres un **experto legal en derecho inmobiliario colombiano y reglamentos de Juntas de Acci贸n Comunal (JAC)**. "
|
| 98 |
+
"Tu tarea es analizar el documento adjunto (que puede ser un Certificado de Tradici贸n y Libertad o un documento de JAC) y extraer la informaci贸n clave para un estudio de t铆tulos o de propiedad comunal. "
|
| 99 |
+
"DEBES DEVOLVER TU RESPUESTA EXCLUSIVAMENTE EN FORMATO JSON, siguiendo el esquema proporcionado. No a帽adas texto explicativo fuera del JSON."
|
| 100 |
+
f"\n\nTexto del Documento:\n\n{text}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
)
|
| 102 |
|
| 103 |
response = client.chat.completions.create(
|
| 104 |
+
model="gpt-4o-mini", # Usamos GPT-4o-mini, que es mejor para tareas de JSON y r谩pido.
|
| 105 |
messages=[
|
| 106 |
{"role": "system", "content": prompt_text}
|
| 107 |
],
|
| 108 |
+
# Configuramos la respuesta para que sea JSON con el esquema definido
|
| 109 |
+
response_format={"type": "json_object", "schema": json_schema},
|
| 110 |
+
temperature=0.3,
|
| 111 |
)
|
| 112 |
|
| 113 |
+
# El contenido de la respuesta ser谩 una cadena JSON
|
| 114 |
+
json_string = response.choices[0].message.content.strip()
|
| 115 |
+
|
| 116 |
+
# Intentamos parsear la cadena JSON a un objeto Python
|
| 117 |
+
import json
|
| 118 |
+
structured_data = json.loads(json_string)
|
| 119 |
+
|
| 120 |
+
# Devolvemos el objeto structured_data
|
| 121 |
+
return structured_data
|
| 122 |
|
| 123 |
except Exception as e:
|
| 124 |
+
print(f"Error al generar el resumen/JSON con OpenAI: {e}")
|
| 125 |
+
# Capturamos errores de la API o de formato JSON inv谩lido
|
| 126 |
+
raise Exception("Error al generar el JSON. Verifica que la APIKey o que el documento sea legible y contenga texto relevante.")
|
| 127 |
|
| 128 |
# --- Rutas de Flask ---
|
| 129 |
|
|
|
|
| 144 |
raw_text = extract_text_from_file(file)
|
| 145 |
|
| 146 |
if not raw_text:
|
| 147 |
+
return jsonify({'error': 'No se pudo extraer texto. Documento ilegible o sin texto.'}), 400
|
| 148 |
|
| 149 |
+
# Llamamos a la funci贸n que ahora devuelve un objeto Python (diccionario)
|
| 150 |
+
structured_summary = generate_summary_openai(raw_text)
|
| 151 |
+
|
| 152 |
+
# Convertimos el diccionario a una lista de cadenas para mostrar en el frontend
|
| 153 |
+
summary_list = [f"**{k.replace('_', ' ').title()}:** {v}" for k, v in structured_summary.items()]
|
| 154 |
+
|
| 155 |
+
return jsonify({'summary': summary_list})
|
| 156 |
|
| 157 |
except Exception as e:
|
| 158 |
print(f"Error general en la ruta /summarize: {e}")
|
|
|
|
| 159 |
return jsonify({'error': f"Error interno del servidor: {str(e)}"}), 500
|
| 160 |
|
| 161 |
if __name__ == '__main__':
|