Spaces:
Sleeping
Sleeping
Download app.py from Prosync/pre-aprobador-docs-SERCOL: direct link, hf CLI and curl.
- Browser
- Download file 22.6 kB
-
https://huggingface.co/spaces/Prosync/pre-aprobador-docs-SERCOL/resolve/main/app.py
- Command line
-
hf download hf://spaces/Prosync/pre-aprobador-docs-SERCOL/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Prosync/pre-aprobador-docs-SERCOL/resolve/main/app.py
22.6 kB
| from datetime import datetime, timedelta | |
| INSTRUCCIONES_USUARIO =""" | |
| # 📋 Sistema de Revisión de Documentos | |
| **📦 Instrucciones para preparar el archivo ZIP:** | |
| 1. **Crea una Carpeta para cada trabajador** (por ejemplo: "Juan Pérez", "María López", etc.) | |
| 2. **Dentro de cada Carpeta**, coloca: | |
| - La cédula del trabajador (el nombre debe contener "cedula" o "cédula") | |
| - Todos los demás documentos del trabajador (contratos, afiliafonasa, etc.) | |
| 3. **Comprime todas las carpetas en un solo archivo ZIP** | |
| 4. **Sube el archivo ZIP** usando el botón de abajo | |
| **Ejemplo de estructura:** | |
| ``` | |
| Documentos.zip | |
| ├── Juan Perez/ | |
| │ ├── cedula.pdf | |
| │ ├── contrato.pdf | |
| │ └── afiliafonasa.pdf | |
| ├── Maria Lopez/ | |
| │ ├── cedula.pdf | |
| │ ├── contrato.pdf | |
| │ └── certificado.pdf | |
| └── Pedro Garcia/ | |
| ├── cedula.pdf | |
| └── contrato.pdf | |
| ``` | |
| ⚠️ **Importante:** Cada Carpeta debe tener un archivo que contenga "cedula" o "cédula" en su nombre. | |
| """ | |
| prompt_cedula = """ | |
| Eres un revisor de acreditación de personal para una minera chilena. | |
| Se te entrega un Documento de identificación Nacional de un trabajador en formato PDF, enviado por el contratista para acreditación. | |
| Tu tarea: | |
| - Verifica la vigencia del Documento. | |
| - Verifica que se vea el frente y el reverso del Documento en la misma hoja. | |
| - Verifica que exista una firma en el Documento. | |
| - Verifica que exista una foto de la cara del trabajador en el Documento. | |
| - Extrae la fecha de vencimiento del Documento. | |
| - Identifica el RUT o RUN del trabajador, bajo la imagen de su foto. | |
| - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional): | |
| "Acreditado: [Nombre], [Apellido], [RUT], [Fecha_Vencimiento]" | |
| Ejemplo: "Acreditado: Juan, Pérez González, 12.345.678-9, 31/12/2028" | |
| - Si hay diferencias, responde: | |
| "Rechazado: [razón detallada]" | |
| No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado. | |
| """ | |
| fecha_actual = datetime.now().strftime("%d/%m/%Y") | |
| dos_semanas_atras = (datetime.now() - timedelta(weeks=10)).strftime("%d/%m/%Y") | |
| prompt_documento_general = """ | |
| Eres un revisor de documentos de acreditación de personal para una minera chilena. | |
| Se te entregan dos documentos PDF: | |
| 1. La cédula de identidad del trabajador (como referencia para revisar firma). | |
| 2. Un Documento enviado por el contratista para ser acreditado. | |
| Datos del trabajador a verificar: | |
| - Nombre: {nombre} | |
| - Apellido: {apellido} | |
| - RUT: {rut} | |
| - Fecha actual: {fecha_actual} | |
| Tu tarea: | |
| - Verifica que el Documento contenga el nombre completo ({nombre} {apellido}) del trabajador. | |
| - Verifica que el Documento contenga el RUT ({rut}) del trabajador. | |
| - 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. | |
| - Verifica que el Documento tenga una fecha reciente (mayor a """+f"{dos_semanas_atras}"+"""). | |
| - Verifica que el Documento esté completo y legible. | |
| - Extrae la fecha del Documento. | |
| - Si todo está correcto, responde EXACTAMENTE en este formato (sin agregar texto adicional): | |
| "Acreditado: [Nombre], [Apellido], [RUT], [Fecha_Documento], [Estado_Firma]" | |
| - Si el Documento son examenes médicos, solo verifica [Nombre], [Apellido] y [Fecha_Documento] | |
| Donde [Estado_Firma] puede ser: "Firma Coincide" o "Firma Similar" o "Firma No Verificable" | |
| Ejemplo: "Acreditado: Juan, Pérez González, 12.345.678-9, 15/10/2024, Firma Coincide" | |
| - Si hay diferencias, EXACTAMENTE en este formato (sin agregar texto adicional): | |
| "Rechazado: [razón detallada explicando qué no coincide]" | |
| No agregues texto adicional, saludos, ni explicaciones fuera del formato solicitado. | |
| """ | |
| # pip install google-genai gradio | |
| import gradio as gr | |
| from google import genai | |
| from google.genai import types | |
| import os | |
| import time | |
| from datetime import datetime, timedelta | |
| import tempfile | |
| import shutil | |
| from pathlib import Path | |
| from collections import defaultdict | |
| import zipfile | |
| import analytics | |
| fecha_actual = datetime.now().strftime("%d/%m/%Y") | |
| dos_semanas_atras = (datetime.now() - timedelta(weeks=2)).strftime("%d/%m/%Y") | |
| # Variables globales para almacenar datos del trabajador y cédula | |
| datos_trabajador = { | |
| 'nombre': None, | |
| 'apellido': None, | |
| 'rut': None, | |
| 'fecha_vencimiento': None, | |
| 'cedula_path': None | |
| } | |
| API_BREVO = os.environ.get("API_BREVO") | |
| G_API_KEY = os.environ.get("G_API_KEY") | |
| client = genai.Client(api_key=G_API_KEY) | |
| def revisar_cedula(cedula_file): | |
| """Procesa la cédula y extrae datos del trabajador""" | |
| global datos_trabajador | |
| try: | |
| # Leer bytes de la cédula | |
| with open(cedula_file, "rb") as f: | |
| cedula_bytes = f.read() | |
| # Guardar path de la cédula para comparaciones futuras | |
| datos_trabajador['cedula_path'] = cedula_file | |
| parts = [ | |
| types.Part.from_text(text=prompt_cedula), | |
| types.Part.from_bytes(data=cedula_bytes, mime_type="application/pdf") | |
| ] | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=parts, | |
| config=types.GenerateContentConfig(response_modalities=["TEXT"]), | |
| ) | |
| result = response.text.strip() | |
| analytics.enviar_evento(email_us,'Revisión cédula') | |
| if result.lower().startswith("acreditado"): | |
| try: | |
| txt_str = result.split(":", 1)[1].strip() | |
| partes = [p.strip() for p in txt_str.split(",")] | |
| if len(partes) >= 4: | |
| datos_trabajador['nombre'] = partes[0] | |
| datos_trabajador['apellido'] = partes[1] | |
| datos_trabajador['rut'] = partes[2] | |
| datos_trabajador['fecha_vencimiento'] = partes[3] | |
| detalle = (f"✓ Nombre: {datos_trabajador['nombre']}\n" | |
| f"✓ Apellido: {datos_trabajador['apellido']}\n" | |
| f"✓ RUT: {datos_trabajador['rut']}\n" | |
| f"✓ Vencimiento: {datos_trabajador['fecha_vencimiento']}") | |
| return os.path.basename(cedula_file), "✅ Pre-Acreditado", detalle | |
| else: | |
| return os.path.basename(cedula_file), "❌ Error", f"Formato incorrecto en respuesta. Se esperaban 4 campos, se recibieron {len(partes)}: {result}" | |
| except Exception as e: | |
| return os.path.basename(cedula_file), "❌ Error", f"No se pudieron extraer los datos: {str(e)}\nRespuesta recibida: {result}" | |
| else: | |
| return os.path.basename(cedula_file), "❌ Rechazado", result | |
| except Exception as e: | |
| return os.path.basename(cedula_file), "❌ Error", f"Error al procesar cédula: {str(e)}" | |
| def revisar_documento_general(documento_file): | |
| """Revisa cualquier Documento comparándolo con la cédula""" | |
| global datos_trabajador | |
| if not all([datos_trabajador['nombre'], datos_trabajador['apellido'], | |
| datos_trabajador['rut'], datos_trabajador['cedula_path']]): | |
| return os.path.basename(documento_file), "⚠️ Pendiente", "Debe procesarse primero la cédula" | |
| try: | |
| prompt = prompt_documento_general.format( | |
| nombre=datos_trabajador['nombre'], | |
| apellido=datos_trabajador['apellido'], | |
| rut=datos_trabajador['rut'], | |
| fecha_actual=fecha_actual | |
| ) | |
| with open(datos_trabajador['cedula_path'], "rb") as f: | |
| cedula_bytes = f.read() | |
| with open(documento_file, "rb") as f: | |
| documento_bytes = f.read() | |
| parts = [ | |
| types.Part.from_text(text=prompt), | |
| types.Part.from_bytes(data=cedula_bytes, mime_type="application/pdf"), | |
| types.Part.from_bytes(data=documento_bytes, mime_type="application/pdf") | |
| ] | |
| response = client.models.generate_content( | |
| model="gemini-2.5-flash", | |
| contents=parts, | |
| config=types.GenerateContentConfig(response_modalities=["TEXT"]), | |
| ) | |
| result = response.text.strip() | |
| analytics.enviar_evento(email_us,'Revisión') | |
| if result.lower().startswith("acreditado"): | |
| try: | |
| txt_str = result.split(":", 1)[1].strip() | |
| partes = [p.strip() for p in txt_str.split(",")] | |
| if len(partes) >= 5: | |
| nombre = partes[0] | |
| apellido = partes[1] | |
| rut = partes[2] | |
| fecha_doc = partes[3] | |
| estado_firma = partes[4] | |
| detalle = (f"✓ Nombre: {nombre}\n" | |
| f"✓ Apellido: {apellido}\n" | |
| f"✓ RUT: {rut}\n" | |
| f"✓ Fecha Documento: {fecha_doc}\n" | |
| f"✓ Estado Firma: {estado_firma}") | |
| return os.path.basename(documento_file), "✅ Pre-Acreditado", detalle | |
| else: | |
| return os.path.basename(documento_file), "✅ Pre-Acreditado", result | |
| except Exception as e: | |
| return os.path.basename(documento_file), "✅ Pre-Acreditado", result | |
| else: | |
| return os.path.basename(documento_file), "❌ Posible rechazo", result.replace(f'no es mayor a {dos_semanas_atras}', 'es posible que sea muy antigua')\ | |
| .replace(f'no es mayor al {dos_semanas_atras}', 'es posible que sea muy antigua')\ | |
| .replace(f'no es posterior a {dos_semanas_atras}', 'es posible que sea muy antigua')\ | |
| .replace(f'no es posterior al {dos_semanas_atras}', 'es posible que sea muy antigua')\ | |
| .replace('Rechazado: ','') | |
| except Exception as e: | |
| return os.path.basename(documento_file), "❌ Error", f"Error al procesar Documento: {str(e)}" | |
| def validar_acceso(email): | |
| import re | |
| global email_us | |
| patron_email = r'^\+569[0-9]+$' | |
| if not email or not email.strip(): | |
| return { | |
| pantalla_acceso: gr.update(visible=True), | |
| pantalla_principal: gr.update(visible=False), | |
| mensaje_error: gr.update(value="⚠️ Por favor ingresa un número de WhatsApp para recibir los resultados", visible=True), | |
| acceso_granted: False, | |
| email_usuario: "" | |
| } | |
| if not re.match(patron_email, email.strip()): | |
| return { | |
| pantalla_acceso: gr.update(visible=True), | |
| pantalla_principal: gr.update(visible=False), | |
| mensaje_error: gr.update(value="⚠️ Por favor ingresa un número de WhatsApp válido", visible=True), | |
| acceso_granted: False, | |
| email_usuario: "" | |
| } | |
| email_us = email | |
| analytics.enviar_evento(email,'Sign up') | |
| return { | |
| pantalla_acceso: gr.update(visible=False), | |
| pantalla_principal: gr.update(visible=True), | |
| mensaje_error: gr.update(visible=False), | |
| acceso_granted: True, | |
| email_usuario: email | |
| } | |
| def extraer_archivos_de_zip(zip_path): | |
| """ | |
| Extrae archivos del ZIP y los organiza por Carpeta. | |
| Retorna un diccionario: {nombre_carpeta: [lista_de_rutas_pdf]} | |
| """ | |
| carpetas = defaultdict(list) | |
| temp_dir = tempfile.mkdtemp() | |
| try: | |
| with zipfile.ZipFile(zip_path, 'r') as zip_ref: | |
| # Extraer todos los archivos | |
| zip_ref.extractall(temp_dir) | |
| # Recorrer el directorio extraído | |
| for root, dirs, files in os.walk(temp_dir): | |
| for file in files: | |
| if file.lower().endswith('.pdf'): | |
| archivo_path = os.path.join(root, file) | |
| # Obtener la Carpeta relativa | |
| carpeta_relativa = os.path.relpath(root, temp_dir) | |
| # Si está en la raíz, usar "Raíz" | |
| if carpeta_relativa == '.': | |
| carpeta_nombre = "Raíz" | |
| else: | |
| # Usar solo el primer nivel de Carpeta | |
| carpeta_nombre = carpeta_relativa.split(os.sep)[0] | |
| carpetas[carpeta_nombre].append(archivo_path) | |
| return carpetas, temp_dir | |
| except Exception as e: | |
| # Limpiar en caso de error | |
| if os.path.exists(temp_dir): | |
| shutil.rmtree(temp_dir) | |
| raise e | |
| from concurrent.futures import ThreadPoolExecutor, as_completed | |
| import threading | |
| def revisar_zip_stream(archivo_zip, email_usuario, progress=gr.Progress()): | |
| global datos_trabajador, resultados_acreditacion | |
| # ✅ Reiniciar resultados de forma segura | |
| resultados_acreditacion = {} | |
| resultados_acreditacion['Carpeta'] = [] | |
| resultados_acreditacion['Documento'] = [] | |
| resultados_acreditacion['Estado'] = [] | |
| resultados_acreditacion['Observaciones'] = [] | |
| if not archivo_zip: | |
| return [] | |
| zip_path = archivo_zip if isinstance(archivo_zip, str) else archivo_zip.name | |
| if not zip_path.lower().endswith('.zip'): | |
| return [] | |
| resumen_carpetas = [] | |
| temp_dir = None | |
| resultados_lock = threading.Lock() | |
| try: | |
| progress(0, desc="📦 Extrayendo archivo ZIP...") | |
| carpetas_dict, temp_dir = extraer_archivos_de_zip(zip_path) | |
| if not carpetas_dict: | |
| return [] | |
| carpetas_ordenadas = sorted(carpetas_dict.items()) | |
| total_carpetas = len(carpetas_ordenadas) | |
| for idx_carpeta, (nombre_carpeta, archivos_carpeta) in enumerate(carpetas_ordenadas, 1): | |
| progress(idx_carpeta / total_carpetas, desc=f"📂 Revisando carpeta: \"{nombre_carpeta}\"") | |
| # Reiniciar datos del trabajador | |
| datos_trabajador = { | |
| 'nombre': None, 'apellido': None, 'rut': None, | |
| 'fecha_vencimiento': None, 'cedula_path': None | |
| } | |
| # Buscar cédula | |
| 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) | |
| if not cedula: | |
| resultados_acreditacion['Carpeta'].append(nombre_carpeta) | |
| resultados_acreditacion['Documento'].append("-") | |
| resultados_acreditacion['Estado'].append("❌ Error") | |
| resultados_acreditacion['Observaciones'].append("No se encontró ninguna cédula en esta Carpeta.") | |
| resumen_carpetas.append([nombre_carpeta, "❌ Error", "No se encontró cédula"]) | |
| continue | |
| # Procesar cédula | |
| nombre, Estado, detalle = revisar_cedula(cedula) | |
| resultados_acreditacion['Carpeta'].append(nombre_carpeta) | |
| resultados_acreditacion['Documento'].append(os.path.basename(cedula)) | |
| resultados_acreditacion['Estado'].append(Estado) | |
| resultados_acreditacion['Observaciones'].append(detalle) | |
| if Estado.startswith("❌"): | |
| resumen_carpetas.append([nombre_carpeta, "❌ Posible rechazo", "Error en cédula"]) | |
| continue | |
| # Procesar documentos generales | |
| documentos_a_procesar = [ | |
| p for p in archivos_carpeta | |
| if 'cedula' not in os.path.basename(p).lower() | |
| and 'cédula' not in os.path.basename(p).lower() | |
| and ' ci.pdf' not in os.path.basename(p).lower() | |
| ] | |
| tiene_rechazo = False | |
| documentos_procesados = 0 | |
| total_docs = len(documentos_a_procesar) | |
| def procesar_doc(pdf_path, idx_doc): | |
| """Revisa un documento y actualiza el progreso.""" | |
| doc_name = os.path.basename(pdf_path) | |
| progress( | |
| (idx_carpeta - 1 + (idx_doc / max(1, total_docs))) / total_carpetas, | |
| desc=f"📂 Carpeta: '{nombre_carpeta}' → 📄 Documento {idx_doc}/{total_docs}: {doc_name}" | |
| ) | |
| nombre, Estado, detalle = revisar_documento_general(pdf_path) | |
| with resultados_lock: | |
| resultados_acreditacion['Carpeta'].append(nombre_carpeta) | |
| resultados_acreditacion['Documento'].append(nombre) | |
| resultados_acreditacion['Estado'].append(Estado) | |
| resultados_acreditacion['Observaciones'].append(detalle) | |
| return Estado | |
| # ✅ Paralelización por carpeta si hay más de 1 documento | |
| if total_docs >= 2: | |
| print(f"🧩 Procesando {total_docs} documentos en paralelo dentro de '{nombre_carpeta}'") | |
| with ThreadPoolExecutor(max_workers=min(4, total_docs)) as executor: | |
| futures = {executor.submit(procesar_doc, pdf, i+1): pdf for i, pdf in enumerate(documentos_a_procesar)} | |
| for future in as_completed(futures): | |
| try: | |
| estado = future.result() | |
| if "rechazo" in estado.lower() or "error" in estado.lower(): | |
| tiene_rechazo = True | |
| documentos_procesados += 1 | |
| except Exception as e: | |
| print(f"⚠️ Error en revisión paralela de {futures[future]}: {e}") | |
| else: | |
| # Procesamiento secuencial (solo 1 documento) | |
| for idx_doc, pdf_path in enumerate(documentos_a_procesar, 1): | |
| estado = procesar_doc(pdf_path, idx_doc) | |
| if "rechazo" in estado.lower() or "error" in estado.lower(): | |
| tiene_rechazo = True | |
| documentos_procesados += 1 | |
| resumen_carpetas.append([ | |
| nombre_carpeta, | |
| "⚠️ Posible rechazo" if tiene_rechazo else "✅ Pre-acreditada", | |
| f"{documentos_procesados + 1} documentos revisados" | |
| ]) | |
| progress(1.0, desc=f"🎉 Completado - {total_carpetas} Carpeta(s) revisada(s)") | |
| # ✅ Reemplazar global al final | |
| resultados_acreditacion = resultados_acreditacion.copy() | |
| # Envío de correo | |
| import brevo | |
| email = brevo.BrevoEmail(API_BREVO) | |
| email.enviar( | |
| destinatario_email='emiliano@prosync.cl', | |
| asunto=f"Resultados pre-acreditación: {email_usuario}", | |
| data=resultados_acreditacion, | |
| numero_documentos=len(resultados_acreditacion['Documento']), | |
| numero_carpetas=len(set(resultados_acreditacion['Carpeta'])) | |
| ) | |
| return resumen_carpetas | |
| finally: | |
| if temp_dir and os.path.exists(temp_dir): | |
| shutil.rmtree(temp_dir) | |
| def habilitar_boton(archivo): | |
| """Habilita el botón solo si hay un archivo cargado""" | |
| if archivo is None: | |
| return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=False) | |
| else: | |
| return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=True) | |
| with gr.Blocks(title="Revisión de Documentos de Acreditación de Personal") as demo: | |
| acceso_granted = gr.State(False) | |
| email_usuario = gr.State("") | |
| # PANTALLA DE ACCESO | |
| with gr.Column(visible=True) as pantalla_acceso: | |
| gr.Markdown(""" | |
| # 🔐 Sistema de revisión documentos de Acreditación de Personal | |
| Por favor, ingresa tu tu WhatsApp para recibir los resultados. | |
| """) | |
| email_input = gr.Textbox( | |
| label="WhatsApp", | |
| placeholder="Deja tu WhatsApp para recibir los resultados", | |
| type="email", | |
| value="+569" | |
| ) | |
| acceder_btn = gr.Button("Acceder al Sistema", variant="primary", size="lg") | |
| mensaje_error = gr.Markdown(visible=False) | |
| with gr.Column(visible=False) as pantalla_principal: | |
| gr.Markdown(""" | |
| # 📋 Sistema de Revisión de Documentos | |
| Sube un archivo ZIP con las carpetas de documentos para revisar. | |
| """) | |
| archivo_zip_input = gr.File( | |
| label="📦 Sube el archivo ZIP con las carpetas de documentos", | |
| file_types=[".zip"], | |
| file_count="single" | |
| ) | |
| revisar_btn = gr.Button("🔍 Iniciar revisión", variant="primary", size="lg") | |
| # Tabla de resumen por carpetas | |
| tabla_resumen = gr.Dataframe( | |
| headers=["Carpeta", "Estado", "Detalle"], | |
| datatype=["str", "str", "str"], | |
| interactive=False, | |
| wrap=True, | |
| label=f"📊 Resumen por Carpeta (Información completa en tu WhatsApp)" | |
| ) | |
| # Habilitar botón cuando se cargue el archivo | |
| archivo_zip_input.change( | |
| fn=habilitar_boton, | |
| inputs=[archivo_zip_input], | |
| outputs=revisar_btn | |
| ) | |
| def activar_carga(): | |
| return gr.Button("⏳ Procesando...", interactive=False) | |
| def desactivar_carga(): | |
| return gr.Button("🔍 Iniciar revisión", variant="primary", interactive=True) | |
| revisar_btn.click( | |
| fn=activar_carga, | |
| outputs=revisar_btn | |
| ).then( | |
| fn=revisar_zip_stream, | |
| inputs=[archivo_zip_input, email_usuario], | |
| outputs=tabla_resumen | |
| ).then( | |
| fn=desactivar_carga, | |
| outputs=revisar_btn | |
| ) | |
| acceder_btn.click( | |
| fn=validar_acceso, | |
| inputs=[email_input], | |
| outputs=[pantalla_acceso, pantalla_principal, mensaje_error, acceso_granted, email_usuario] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(share=True) |