efaraneda commited on
Commit
cb5b203
·
verified ·
1 Parent(s): d2eab98

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -178
app.py CHANGED
@@ -1,37 +1,45 @@
1
- import gradio as gr
2
- from google import genai
3
- from google.genai import types
4
- import os
5
- import time
6
  from datetime import datetime, timedelta
7
- import tempfile
8
- import shutil
9
- from pathlib import Path
10
-
11
- G_API_KEY = os.environ.get("G_API_KEY")
12
-
13
- fecha_actual = datetime.now().strftime("%d/%m/%Y")
14
- dos_semanas_atras = (datetime.now() - timedelta(weeks=2)).strftime("%d/%m/%Y")
15
-
16
- # Variables globales para almacenar datos del trabajador y cédula
17
- datos_trabajador = {
18
- 'nombre': None,
19
- 'apellido': None,
20
- 'rut': None,
21
- 'fecha_vencimiento': None,
22
- 'cedula_path': None # Guardar el path temporal de la cédula
23
- }
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  prompt_cedula = """
26
  Eres un revisor de acreditación de personal para una minera chilena.
27
- Se te entrega un documento de identificación Nacional de un trabajador en formato PDF, enviado por el contratista para acreditación.
28
 
29
  Tu tarea:
30
- - Verifica la vigencia del documento.
31
- - Verifica que se vea el frente y el reverso del documento.
32
- - Verifica que exista una firma en el documento.
33
- - Verifica que exista una foto de la cara del trabajador en el documento.
34
- - Extrae la fecha de vencimiento del documento.
35
  - Identifica el RUT o RUN del trabajador, bajo la imagen de su foto.
36
 
37
  - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional):
@@ -45,11 +53,14 @@ Tu tarea:
45
  No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado.
46
  """
47
 
 
 
 
48
  prompt_documento_general = """
49
  Eres un revisor de documentos de acreditación de personal para una minera chilena.
50
  Se te entregan dos documentos PDF:
51
  1. La cédula de identidad del trabajador (como referencia para revisar firma).
52
- 2. Un documento enviado por el contratista para ser acreditado.
53
 
54
  Datos del trabajador a verificar:
55
  - Nombre: {nombre}
@@ -58,17 +69,17 @@ Datos del trabajador a verificar:
58
  - Fecha actual: {fecha_actual}
59
 
60
  Tu tarea:
61
- - Verifica que el documento contenga el nombre completo ({nombre} {apellido}) del trabajador.
62
- - Verifica que el documento contenga el RUT ({rut}) del trabajador.
63
- - Rechazar la aprobación en el caso de que las firmas que estén presentes en el documento y que sean del trabajador, no sean lo suficientemente similares a la firma en la cédula de identidad como para asumir que es él quien firma.
64
- - Verifica que el documento tenga una fecha reciente (mayor a """+f"{dos_semanas_atras}"+""").
65
- - Verifica que el documento esté completo y legible.
66
- - Extrae la fecha del documento.
67
 
68
  - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional):
69
  "Acreditado: [Nombre], [Apellido], [RUT], [Fecha_Documento], [Estado_Firma]"
70
 
71
- - Si el documento son examenes médicos, solo verifica [Nombre], [Apellido] y [Fecha_Documento]
72
 
73
  Donde [Estado_Firma] puede ser: "Firma Coincide" o "Firma Similar" o "Firma No Verificable"
74
 
@@ -79,14 +90,41 @@ Tu tarea:
79
 
80
  No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado.
81
  """
82
- client = genai.Client(api_key=G_API_KEY)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  def revisar_cedula(cedula_file):
85
  """Procesa la cédula y extrae datos del trabajador"""
86
  global datos_trabajador
87
 
88
- # Inicializar cliente Gemini
89
-
90
  try:
91
  # Leer bytes de la cédula
92
  with open(cedula_file, "rb") as f:
@@ -107,14 +145,12 @@ def revisar_cedula(cedula_file):
107
  )
108
 
109
  result = response.text.strip()
110
-
111
  if result.lower().startswith("acreditado"):
112
  try:
113
- # Extraer la parte después de "Acreditado:"
114
  txt_str = result.split(":", 1)[1].strip()
115
  partes = [p.strip() for p in txt_str.split(",")]
116
 
117
- # Verificar que tengamos 4 partes
118
  if len(partes) >= 4:
119
  datos_trabajador['nombre'] = partes[0]
120
  datos_trabajador['apellido'] = partes[1]
@@ -140,21 +176,14 @@ def revisar_cedula(cedula_file):
140
 
141
 
142
  def revisar_documento_general(documento_file):
143
- """Revisa cualquier documento comparándolo con la cédula"""
144
  global datos_trabajador
145
 
146
- # Verificar que tengamos los datos del trabajador y la cédula
147
  if not all([datos_trabajador['nombre'], datos_trabajador['apellido'],
148
  datos_trabajador['rut'], datos_trabajador['cedula_path']]):
149
  return os.path.basename(documento_file), "⚠️ Pendiente", "Debe procesarse primero la cédula"
150
 
151
- # Inicializar cliente Gemini
152
-
153
-
154
  try:
155
- # Obtener fecha actual
156
-
157
- # Crear prompt con los datos del trabajador
158
  prompt = prompt_documento_general.format(
159
  nombre=datos_trabajador['nombre'],
160
  apellido=datos_trabajador['apellido'],
@@ -162,11 +191,9 @@ def revisar_documento_general(documento_file):
162
  fecha_actual=fecha_actual
163
  )
164
 
165
- # Leer cédula
166
  with open(datos_trabajador['cedula_path'], "rb") as f:
167
  cedula_bytes = f.read()
168
 
169
- # Leer documento a validar
170
  with open(documento_file, "rb") as f:
171
  documento_bytes = f.read()
172
 
@@ -183,14 +210,12 @@ def revisar_documento_general(documento_file):
183
  )
184
 
185
  result = response.text.strip()
186
-
187
  if result.lower().startswith("acreditado"):
188
  try:
189
- # Extraer la parte después de "Acreditado:"
190
  txt_str = result.split(":", 1)[1].strip()
191
  partes = [p.strip() for p in txt_str.split(",")]
192
 
193
- # Verificar que tengamos 5 partes
194
  if len(partes) >= 5:
195
  nombre = partes[0]
196
  apellido = partes[1]
@@ -206,11 +231,9 @@ def revisar_documento_general(documento_file):
206
 
207
  return os.path.basename(documento_file), "✅ Pre-Acreditado", detalle
208
  else:
209
- # Si no hay 5 partes, mostrar lo que se recibió
210
  return os.path.basename(documento_file), "✅ Pre-Acreditado", result
211
 
212
  except Exception as e:
213
- # Si hay error al parsear, mostrar la respuesta completa
214
  return os.path.basename(documento_file), "✅ Pre-Acreditado", result
215
  else:
216
  return os.path.basename(documento_file), "❌ Posible rechazo", result.replace(f'no es mayor a {dos_semanas_atras}', 'es posible que sea muy antigua')\
@@ -220,148 +243,313 @@ def revisar_documento_general(documento_file):
220
  .replace('Rechazado: ','')
221
 
222
  except Exception as e:
223
- return os.path.basename(documento_file), "❌ Error", f"Error al procesar documento: {str(e)}"
224
 
225
 
 
 
 
226
 
227
 
228
- def revisar_carpeta_stream(archivos):
229
- global datos_trabajador
 
 
 
 
 
 
 
230
 
231
- if not archivos:
232
- yield [["⚠️ No se subieron archivos.", "", ""]]
233
- return
234
-
235
- datos_trabajador = {
236
- 'nombre': None,
237
- 'apellido': None,
238
- 'rut': None,
239
- 'fecha_vencimiento': None,
240
- 'cedula_path': None
 
 
 
 
 
 
 
 
 
241
  }
 
 
 
 
 
 
 
 
 
242
 
243
- resultados = []
244
-
245
- # Obtener lista de PDFs desde los archivos subidos
246
- lista_pdfs = []
247
- for archivo in archivos:
248
- archivo_path = archivo if isinstance(archivo, str) else archivo.name
249
- if archivo_path.lower().endswith(".pdf"):
250
- lista_pdfs.append(archivo_path)
251
-
252
- if not lista_pdfs:
253
- yield [["⚠️ No se encontraron archivos PDF.", "", ""]]
254
- return
255
-
256
- # Separar cédula del resto de documentos
257
- cedula = None
258
- otros_docs = []
259
-
260
- for pdf_path in lista_pdfs:
261
- nombre_archivo = os.path.basename(pdf_path)
262
-
263
- if 'cedula' in nombre_archivo.lower() or 'cédula' in nombre_archivo.lower():
264
- cedula = pdf_path
265
- else:
266
- otros_docs.append(pdf_path)
267
-
268
- if not cedula:
269
- yield [["❌ Error", "", "No se encontró ninguna cédula. Debe incluir un archivo con 'cedula' o 'cédula' en el nombre."]]
270
- return
271
-
272
- # 1. Procesar primero la cédula
273
  try:
274
- nombre, estado, detalle = revisar_cedula(cedula)
275
- resultados.append([nombre, estado, detalle])
276
- yield resultados
277
 
278
- if estado == "❌ Rechazado" or estado == "❌ Error":
279
- yield resultados
280
- return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- time.sleep(1)
283
  except Exception as e:
284
- resultados.append([os.path.basename(cedula), "❌ Error", str(e)])
285
- yield resultados
286
- return
287
-
288
- # 2. Procesar el resto de documentos
289
- for doc_path in otros_docs:
290
- try:
291
- nombre, estado, detalle = revisar_documento_general(doc_path)
292
- resultados.append([nombre, estado, detalle])
293
- yield resultados
294
- time.sleep(15)
295
- except Exception as e:
296
- resultados.append([os.path.basename(doc_path), "❌ Error", str(e)])
297
- yield resultados
298
-
299
- if len(resultados) == 1:
300
- resultados.append(["⚠️ Información", "", "Solo se procesó la cédula. No se encontraron otros documentos."])
301
- yield resultados
302
 
303
 
304
- # Interfaz Gradio
305
- with gr.Blocks(title="Revisión de Documentos SERCOL") as demo:
306
- gr.Markdown("""
307
- ## 🧾 Revisor de Documentos de Acreditación de Personal para SERCOL
308
-
309
- El sistema revisará primero la **cédula** para extraer los datos del trabajador y su firma,
310
- y usará esa información para validar **todos los demás documentos**, verificando:
311
-
312
- **Para la Cédula:**
313
- - ✅ Nombre, Apellido y RUT
314
- - ✅ Fecha de vencimiento
315
-
316
- **Para otros documentos:**
317
- - ✅ Nombre y apellido coincidan
318
- - ✅ RUT coincida
319
- - ✅ Firma sea similar a la de la cédula
320
- - ✅ Fecha del documento reciente (últimas 2 semanas)
321
- - ✅ Formato de fechas
322
-
323
- ⚠️ **Importante**:
324
- - El archivo de la cédula debe tener "cedula" o "cédula" en el nombre
325
- - Puedes subir archivos PDF individuales o seleccionar una carpeta completa
326
- - Todos los documentos deben estar en formato PDF
327
- """)
328
-
329
- archivos_input = gr.File(
330
- label="📁 Sube PDFs o selecciona una carpeta con documentos",
331
- file_count="directory",
332
- file_types=[".pdf"]
333
- )
334
 
335
- tabla_resultados = gr.Dataframe(
336
- headers=["Archivo", "Estado", "Detalle"],
337
- datatype=["str", "str", "str"],
338
- interactive=False,
339
- wrap=True,
340
- label="📋 Resultados de revisión"
341
- )
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
 
 
 
 
 
 
344
 
345
- # Por esto:
346
- revisar_btn = gr.Button("🔍 Iniciar revisión", variant="primary")
 
347
 
348
- def activar_carga():
349
- return gr.Button("⏳ Procesando...", interactive=False)
 
 
 
 
 
350
 
351
- def desactivar_carga():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
  return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=True)
353
 
354
- revisar_btn.click(
355
- fn=activar_carga,
356
- outputs=revisar_btn
357
- ).then(
358
- fn=revisar_carpeta_stream,
359
- inputs=archivos_input,
360
- outputs=tabla_resultados
361
- ).then(
362
- fn=desactivar_carga,
363
- outputs=revisar_btn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  )
365
 
 
366
  if __name__ == "__main__":
367
- demo.launch()
 
 
 
 
 
 
1
  from datetime import datetime, timedelta
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
+ INSTRUCCIONES_USUARIO ="""
4
+ # 📋 Sistema de Revisión de Documentos
5
+
6
+ **📦 Instrucciones para preparar el archivo ZIP:**
7
+
8
+ 1. **Crea una Carpeta para cada trabajador** (por ejemplo: "Juan Pérez", "María López", etc.)
9
+ 2. **Dentro de cada Carpeta**, coloca:
10
+ - La cédula del trabajador (el nombre debe contener "cedula" o "cédula")
11
+ - Todos los demás documentos del trabajador (contratos, afiliafonasa, etc.)
12
+ 3. **Comprime todas las carpetas en un solo archivo ZIP**
13
+ 4. **Sube el archivo ZIP** usando el botón de abajo
14
+
15
+ **Ejemplo de estructura:**
16
+ ```
17
+ Documentos.zip
18
+ ├── Juan Perez/
19
+ │ ├── cedula.pdf
20
+ │ ├── contrato.pdf
21
+ │ └── afiliafonasa.pdf
22
+ ├── Maria Lopez/
23
+ │ ├── cedula.pdf
24
+ │ ├── contrato.pdf
25
+ │ └── certificado.pdf
26
+ └── Pedro Garcia/
27
+ ├── cedula.pdf
28
+ └── contrato.pdf
29
+ ```
30
+
31
+ ⚠️ **Importante:** Cada Carpeta debe tener un archivo que contenga "cedula" o "cédula" en su nombre.
32
+ """
33
  prompt_cedula = """
34
  Eres un revisor de acreditación de personal para una minera chilena.
35
+ Se te entrega un Documento de identificación Nacional de un trabajador en formato PDF, enviado por el contratista para acreditación.
36
 
37
  Tu tarea:
38
+ - Verifica la vigencia del Documento.
39
+ - Verifica que se vea el frente y el reverso del Documento en la misma hoja.
40
+ - Verifica que exista una firma en el Documento.
41
+ - Verifica que exista una foto de la cara del trabajador en el Documento.
42
+ - Extrae la fecha de vencimiento del Documento.
43
  - Identifica el RUT o RUN del trabajador, bajo la imagen de su foto.
44
 
45
  - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional):
 
53
  No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado.
54
  """
55
 
56
+ fecha_actual = datetime.now().strftime("%d/%m/%Y")
57
+ dos_semanas_atras = (datetime.now() - timedelta(weeks=10)).strftime("%d/%m/%Y")
58
+
59
  prompt_documento_general = """
60
  Eres un revisor de documentos de acreditación de personal para una minera chilena.
61
  Se te entregan dos documentos PDF:
62
  1. La cédula de identidad del trabajador (como referencia para revisar firma).
63
+ 2. Un Documento enviado por el contratista para ser acreditado.
64
 
65
  Datos del trabajador a verificar:
66
  - Nombre: {nombre}
 
69
  - Fecha actual: {fecha_actual}
70
 
71
  Tu tarea:
72
+ - Verifica que el Documento contenga el nombre completo ({nombre} {apellido}) del trabajador.
73
+ - Verifica que el Documento contenga el RUT ({rut}) del trabajador.
74
+ - Rechazar la aprobación en el caso de que las firmas que estén presentes en el Documento y que sean del trabajador, no sean lo suficientemente similares a la firma en la cédula de identidad como para asumir que es él quien firma.
75
+ - Verifica que el Documento tenga una fecha reciente (mayor a """+f"{dos_semanas_atras}"+""").
76
+ - Verifica que el Documento esté completo y legible.
77
+ - Extrae la fecha del Documento.
78
 
79
  - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional):
80
  "Acreditado: [Nombre], [Apellido], [RUT], [Fecha_Documento], [Estado_Firma]"
81
 
82
+ - Si el Documento son examenes médicos, solo verifica [Nombre], [Apellido] y [Fecha_Documento]
83
 
84
  Donde [Estado_Firma] puede ser: "Firma Coincide" o "Firma Similar" o "Firma No Verificable"
85
 
 
90
 
91
  No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado.
92
  """
93
+
94
+ # pip install google-genai gradio
95
+
96
+ import gradio as gr
97
+ from google import genai
98
+ from google.genai import types
99
+ import os
100
+ import time
101
+ from datetime import datetime, timedelta
102
+ import tempfile
103
+ import shutil
104
+ from pathlib import Path
105
+ from collections import defaultdict
106
+ import zipfile
107
+ import analytics
108
+
109
+ fecha_actual = datetime.now().strftime("%d/%m/%Y")
110
+ dos_semanas_atras = (datetime.now() - timedelta(weeks=2)).strftime("%d/%m/%Y")
111
+
112
+ # Variables globales para almacenar datos del trabajador y cédula
113
+ datos_trabajador = {
114
+ 'nombre': None,
115
+ 'apellido': None,
116
+ 'rut': None,
117
+ 'fecha_vencimiento': None,
118
+ 'cedula_path': None
119
+ }
120
+
121
+ G_API_KEY = os.environ.get("G_API_KEY")
122
+ client = genai.Client(breakpoint_key=G_API_KEY)
123
 
124
  def revisar_cedula(cedula_file):
125
  """Procesa la cédula y extrae datos del trabajador"""
126
  global datos_trabajador
127
 
 
 
128
  try:
129
  # Leer bytes de la cédula
130
  with open(cedula_file, "rb") as f:
 
145
  )
146
 
147
  result = response.text.strip()
148
+ analytics.enviar_evento(email_us,'Revisión cédula')
149
  if result.lower().startswith("acreditado"):
150
  try:
 
151
  txt_str = result.split(":", 1)[1].strip()
152
  partes = [p.strip() for p in txt_str.split(",")]
153
 
 
154
  if len(partes) >= 4:
155
  datos_trabajador['nombre'] = partes[0]
156
  datos_trabajador['apellido'] = partes[1]
 
176
 
177
 
178
  def revisar_documento_general(documento_file):
179
+ """Revisa cualquier Documento comparándolo con la cédula"""
180
  global datos_trabajador
181
 
 
182
  if not all([datos_trabajador['nombre'], datos_trabajador['apellido'],
183
  datos_trabajador['rut'], datos_trabajador['cedula_path']]):
184
  return os.path.basename(documento_file), "⚠️ Pendiente", "Debe procesarse primero la cédula"
185
 
 
 
 
186
  try:
 
 
 
187
  prompt = prompt_documento_general.format(
188
  nombre=datos_trabajador['nombre'],
189
  apellido=datos_trabajador['apellido'],
 
191
  fecha_actual=fecha_actual
192
  )
193
 
 
194
  with open(datos_trabajador['cedula_path'], "rb") as f:
195
  cedula_bytes = f.read()
196
 
 
197
  with open(documento_file, "rb") as f:
198
  documento_bytes = f.read()
199
 
 
210
  )
211
 
212
  result = response.text.strip()
213
+ analytics.enviar_evento(email_us,'Revisión')
214
  if result.lower().startswith("acreditado"):
215
  try:
 
216
  txt_str = result.split(":", 1)[1].strip()
217
  partes = [p.strip() for p in txt_str.split(",")]
218
 
 
219
  if len(partes) >= 5:
220
  nombre = partes[0]
221
  apellido = partes[1]
 
231
 
232
  return os.path.basename(documento_file), "✅ Pre-Acreditado", detalle
233
  else:
 
234
  return os.path.basename(documento_file), "✅ Pre-Acreditado", result
235
 
236
  except Exception as e:
 
237
  return os.path.basename(documento_file), "✅ Pre-Acreditado", result
238
  else:
239
  return os.path.basename(documento_file), "❌ Posible rechazo", result.replace(f'no es mayor a {dos_semanas_atras}', 'es posible que sea muy antigua')\
 
243
  .replace('Rechazado: ','')
244
 
245
  except Exception as e:
246
+ return os.path.basename(documento_file), "❌ Error", f"Error al procesar Documento: {str(e)}"
247
 
248
 
249
+ def validar_acceso(email):
250
+ import re
251
+ global email_us
252
 
253
 
254
+ patron_email = r'^\+569[0-9]+$'
255
+ if not email or not email.strip():
256
+ return {
257
+ pantalla_acceso: gr.update(visible=True),
258
+ pantalla_principal: gr.update(visible=False),
259
+ mensaje_error: gr.update(value="⚠️ Por favor ingresa un número de WhatsApp para recibir los resultados", visible=True),
260
+ acceso_granted: False,
261
+ email_usuario: ""
262
+ }
263
 
264
+ if not re.match(patron_email, email.strip()):
265
+ return {
266
+ pantalla_acceso: gr.update(visible=True),
267
+ pantalla_principal: gr.update(visible=False),
268
+ mensaje_error: gr.update(value="⚠️ Por favor ingresa un número de WhatsApp válido", visible=True),
269
+ acceso_granted: False,
270
+ email_usuario: ""
271
+ }
272
+
273
+ email_us = email
274
+
275
+ analytics.enviar_evento(email,'Sign up')
276
+
277
+ return {
278
+ pantalla_acceso: gr.update(visible=False),
279
+ pantalla_principal: gr.update(visible=True),
280
+ mensaje_error: gr.update(visible=False),
281
+ acceso_granted: True,
282
+ email_usuario: email
283
  }
284
+
285
+
286
+ def extraer_archivos_de_zip(zip_path):
287
+ """
288
+ Extrae archivos del ZIP y los organiza por Carpeta.
289
+ Retorna un diccionario: {nombre_carpeta: [lista_de_rutas_pdf]}
290
+ """
291
+ carpetas = defaultdict(list)
292
+ temp_dir = tempfile.mkdtemp()
293
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
294
  try:
295
+ with zipfile.ZipFile(zip_path, 'r') as zip_ref:
296
+ # Extraer todos los archivos
297
+ zip_ref.extractall(temp_dir)
298
 
299
+ # Recorrer el directorio extraído
300
+ for root, dirs, files in os.walk(temp_dir):
301
+ for file in files:
302
+ if file.lower().endswith('.pdf'):
303
+ archivo_path = os.path.join(root, file)
304
+
305
+ # Obtener la Carpeta relativa
306
+ carpeta_relativa = os.path.relpath(root, temp_dir)
307
+
308
+ # Si está en la raíz, usar "Raíz"
309
+ if carpeta_relativa == '.':
310
+ carpeta_nombre = "Raíz"
311
+ else:
312
+ # Usar solo el primer nivel de Carpeta
313
+ carpeta_nombre = carpeta_relativa.split(os.sep)[0]
314
+
315
+ carpetas[carpeta_nombre].append(archivo_path)
316
+
317
+ return carpetas, temp_dir
318
 
 
319
  except Exception as e:
320
+ # Limpiar en caso de error
321
+ if os.path.exists(temp_dir):
322
+ shutil.rmtree(temp_dir)
323
+ raise e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
 
325
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
 
 
 
 
 
 
 
 
327
 
328
+ from concurrent.futures import ThreadPoolExecutor, as_completed
329
+ import threading
330
+
331
+ def revisar_zip_stream(archivo_zip, email_usuario, progress=gr.Progress()):
332
+ global datos_trabajador, resultados_acreditacion
333
+
334
+ # ✅ Reiniciar resultados de forma segura
335
+ resultados_acreditacion = {}
336
+ resultados_acreditacion['Carpeta'] = []
337
+ resultados_acreditacion['Documento'] = []
338
+ resultados_acreditacion['Estado'] = []
339
+ resultados_acreditacion['Observaciones'] = []
340
+
341
+ if not archivo_zip:
342
+ return []
343
+
344
+ zip_path = archivo_zip if isinstance(archivo_zip, str) else archivo_zip.name
345
+ if not zip_path.lower().endswith('.zip'):
346
+ return []
347
+
348
+ resumen_carpetas = []
349
+ temp_dir = None
350
+ resultados_lock = threading.Lock()
351
+
352
+ try:
353
+ progress(0, desc="📦 Extrayendo archivo ZIP...")
354
+ carpetas_dict, temp_dir = extraer_archivos_de_zip(zip_path)
355
+
356
+ if not carpetas_dict:
357
+ return []
358
+
359
+ carpetas_ordenadas = sorted(carpetas_dict.items())
360
+ total_carpetas = len(carpetas_ordenadas)
361
+
362
+ for idx_carpeta, (nombre_carpeta, archivos_carpeta) in enumerate(carpetas_ordenadas, 1):
363
+ progress(idx_carpeta / total_carpetas, desc=f"📂 Revisando carpeta: \"{nombre_carpeta}\"")
364
+
365
+ # Reiniciar datos del trabajador
366
+ datos_trabajador = {
367
+ 'nombre': None, 'apellido': None, 'rut': None,
368
+ 'fecha_vencimiento': None, 'cedula_path': None
369
+ }
370
+
371
+ # Buscar cédula
372
+ cedula = next((p for p in archivos_carpeta if 'cedula' in p.lower() or 'cédula' in p.lower() or ' ci.pdf' in p.lower()), None)
373
+ if not cedula:
374
+ resultados_acreditacion['Carpeta'].append(nombre_carpeta)
375
+ resultados_acreditacion['Documento'].append("-")
376
+ resultados_acreditacion['Estado'].append("❌ Error")
377
+ resultados_acreditacion['Observaciones'].append("No se encontró ninguna cédula en esta Carpeta.")
378
+ resumen_carpetas.append([nombre_carpeta, "❌ Error", "No se encontró cédula"])
379
+ continue
380
 
381
+ # Procesar cédula
382
+ nombre, Estado, detalle = revisar_cedula(cedula)
383
+ resultados_acreditacion['Carpeta'].append(nombre_carpeta)
384
+ resultados_acreditacion['Documento'].append(os.path.basename(cedula))
385
+ resultados_acreditacion['Estado'].append(Estado)
386
+ resultados_acreditacion['Observaciones'].append(detalle)
387
 
388
+ if Estado.startswith("❌"):
389
+ resumen_carpetas.append([nombre_carpeta, "❌ Posible rechazo", "Error en cédula"])
390
+ continue
391
 
392
+ # Procesar documentos generales
393
+ documentos_a_procesar = [
394
+ p for p in archivos_carpeta
395
+ if 'cedula' not in os.path.basename(p).lower()
396
+ and 'cédula' not in os.path.basename(p).lower()
397
+ and ' ci.pdf' not in os.path.basename(p).lower()
398
+ ]
399
 
400
+ tiene_rechazo = False
401
+ documentos_procesados = 0
402
+ total_docs = len(documentos_a_procesar)
403
+
404
+ def procesar_doc(pdf_path, idx_doc):
405
+ """Revisa un documento y actualiza el progreso."""
406
+ doc_name = os.path.basename(pdf_path)
407
+ progress(
408
+ (idx_carpeta - 1 + (idx_doc / max(1, total_docs))) / total_carpetas,
409
+ desc=f"📂 Carpeta: '{nombre_carpeta}' → 📄 Documento {idx_doc}/{total_docs}: {doc_name}"
410
+ )
411
+
412
+ nombre, Estado, detalle = revisar_documento_general(pdf_path)
413
+ with resultados_lock:
414
+ resultados_acreditacion['Carpeta'].append(nombre_carpeta)
415
+ resultados_acreditacion['Documento'].append(nombre)
416
+ resultados_acreditacion['Estado'].append(Estado)
417
+ resultados_acreditacion['Observaciones'].append(detalle)
418
+ return Estado
419
+
420
+ # ✅ Paralelización por carpeta si hay más de 1 documento
421
+ if total_docs >= 2:
422
+ print(f"🧩 Procesando {total_docs} documentos en paralelo dentro de '{nombre_carpeta}'")
423
+ with ThreadPoolExecutor(max_workers=min(4, total_docs)) as executor:
424
+ futures = {executor.submit(procesar_doc, pdf, i+1): pdf for i, pdf in enumerate(documentos_a_procesar)}
425
+ for future in as_completed(futures):
426
+ try:
427
+ estado = future.result()
428
+ if "rechazo" in estado.lower() or "error" in estado.lower():
429
+ tiene_rechazo = True
430
+ documentos_procesados += 1
431
+ except Exception as e:
432
+ print(f"⚠️ Error en revisión paralela de {futures[future]}: {e}")
433
+ else:
434
+ # Procesamiento secuencial (solo 1 documento)
435
+ for idx_doc, pdf_path in enumerate(documentos_a_procesar, 1):
436
+ estado = procesar_doc(pdf_path, idx_doc)
437
+ if "rechazo" in estado.lower() or "error" in estado.lower():
438
+ tiene_rechazo = True
439
+ documentos_procesados += 1
440
+
441
+ resumen_carpetas.append([
442
+ nombre_carpeta,
443
+ "⚠️ Posible rechazo" if tiene_rechazo else "✅ Pre-acreditada",
444
+ f"{documentos_procesados + 1} documentos revisados"
445
+ ])
446
+
447
+
448
+ progress(1.0, desc=f"🎉 Completado - {total_carpetas} Carpeta(s) revisada(s)")
449
+
450
+ # ✅ Reemplazar global al final
451
+ resultados_acreditacion = resultados_acreditacion.copy()
452
+
453
+ # Envío de correo
454
+ import brevo
455
+ email = brevo.BrevoEmail(API_BREVO)
456
+ email.enviar(
457
+ destinatario_email='emiliano@prosync.cl',
458
+ asunto=f"Resultados pre-acreditación: {email_usuario}",
459
+ data=resultados_acreditacion,
460
+ numero_documentos=len(resultados_acreditacion['Documento']),
461
+ numero_carpetas=len(set(resultados_acreditacion['Carpeta']))
462
+ )
463
+
464
+ return resumen_carpetas
465
+
466
+ finally:
467
+ if temp_dir and os.path.exists(temp_dir):
468
+ shutil.rmtree(temp_dir)
469
+
470
+
471
+ def habilitar_boton(archivo):
472
+ """Habilita el botón solo si hay un archivo cargado"""
473
+ if archivo is None:
474
+ return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=False)
475
+ else:
476
  return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=True)
477
 
478
+ with gr.Blocks(title="Revisión de Documentos de Acreditación de Personal") as demo:
479
+ acceso_granted = gr.State(False)
480
+ email_usuario = gr.State("")
481
+
482
+ # PANTALLA DE ACCESO
483
+ with gr.Column(visible=True) as pantalla_acceso:
484
+ gr.Markdown("""
485
+ # 🔐 Sistema de revisión documentos de Acreditación de Personal
486
+
487
+ Por favor, ingresa tu tu WhatsApp para recibir los resultados.
488
+ """)
489
+
490
+ email_input = gr.Textbox(
491
+ label="WhatsApp",
492
+ placeholder="Deja tu WhatsApp para recibir los resultados",
493
+ type="email",
494
+ value="+569"
495
+ )
496
+
497
+ acceder_btn = gr.Button("Acceder al Sistema", variant="primary", size="lg")
498
+ mensaje_error = gr.Markdown(visible=False)
499
+
500
+ with gr.Column(visible=False) as pantalla_principal:
501
+ gr.Markdown("""
502
+ # 📋 Sistema de Revisión de Documentos
503
+
504
+ Sube un archivo ZIP con las carpetas de documentos para revisar.
505
+ """)
506
+
507
+ archivo_zip_input = gr.File(
508
+ label="📦 Sube el archivo ZIP con las carpetas de documentos",
509
+ file_types=[".zip"],
510
+ file_count="single"
511
+ )
512
+
513
+ revisar_btn = gr.Button("🔍 Iniciar revisión", variant="primary", size="lg")
514
+
515
+ # Tabla de resumen por carpetas
516
+ tabla_resumen = gr.Dataframe(
517
+ headers=["Carpeta", "Estado", "Detalle"],
518
+ datatype=["str", "str", "str"],
519
+ interactive=False,
520
+ wrap=True,
521
+ label=f"📊 Resumen por Carpeta (Información completa en tu WhatsApp)"
522
+ )
523
+ # Habilitar botón cuando se cargue el archivo
524
+ archivo_zip_input.change(
525
+ fn=habilitar_boton,
526
+ inputs=[archivo_zip_input],
527
+ outputs=revisar_btn
528
+ )
529
+ def activar_carga():
530
+ return gr.Button("⏳ Procesando...", interactive=False)
531
+
532
+ def desactivar_carga():
533
+ return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=True)
534
+
535
+ revisar_btn.click(
536
+ fn=activar_carga,
537
+ outputs=revisar_btn
538
+ ).then(
539
+ fn=revisar_zip_stream,
540
+ inputs=[archivo_zip_input, email_usuario],
541
+ outputs=tabla_resumen
542
+ ).then(
543
+ fn=desactivar_carga,
544
+ outputs=revisar_btn
545
+ )
546
+
547
+ acceder_btn.click(
548
+ fn=validar_acceso,
549
+ inputs=[email_input],
550
+ outputs=[pantalla_acceso, pantalla_principal, mensaje_error, acceso_granted, email_usuario]
551
  )
552
 
553
+
554
  if __name__ == "__main__":
555
+ demo.launch(share=True)