{ "cells": [ { "cell_type": "markdown", "metadata": { "id": "title" }, "source": [ "OCR avanzado para documentos empresariales\n", "==========================================\n", "\n", "En este notebook vamos a ver cómo se utiliza un modelo moderno de OCR/document parsing dentro de un pipeline empresarial. La idea no es solamente \"leer texto\", sino convertir un PDF o una imagen en una representación útil para procesos posteriores: validación de campos, revisión humana, carga a sistemas transaccionales o RAG.\n", "\n", "Modelos recientes como Mistral OCR o DeepSeek-OCR trabajan más cerca de los modelos vision-language: reciben una página como imagen o documento, preservan parte del layout y pueden devolver Markdown, JSON o texto estructurado. Esto resulta útil cuando una factura, un contrato o un formulario contiene tablas, encabezados, sellos o información distribuida visualmente." ], "id": "title" }, { "cell_type": "markdown", "metadata": { "id": "environment-setup" }, "source": [ "## Preparación del ambiente\n", "\n", "Instalemos las dependencias necesarias desde el archivo de requerimientos asociado a este notebook. El ejemplo puede ejecutarse sin credenciales usando una respuesta simulada; si dispone de una API key de Mistral, también puede activar la llamada real al servicio." ], "id": "environment-setup" }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "install-dependencies" }, "outputs": [], "source": [ "!wget -q https://raw.githubusercontent.com/santiagxf/M72109/master/docs/document-understanding/advanced-ocr-enterprise.txt\n", "%pip install -r advanced-ocr-enterprise.txt --quiet" ], "id": "install-dependencies" }, { "cell_type": "markdown", "metadata": { "id": "colab-restart-note" }, "source": [ "Recordar reiniciar la sesión si se encuentra trabajando en Google Colab y el entorno lo solicita." ], "id": "colab-restart-note" }, { "cell_type": "markdown", "metadata": { "id": "modern-ocr-patterns" }, "source": [ "## ¿Qué cambió respecto al OCR clásico?\n", "\n", "En un pipeline clásico, primero detectamos palabras y luego intentamos reconstruir líneas, tablas y campos con reglas. En un pipeline moderno, el modelo puede producir directamente una salida más rica, por ejemplo Markdown con tablas o JSON con anotaciones. Podemos pensar el resultado como una capa intermedia entre la página visual y las aplicaciones de negocio.\n", "\n", "A grandes rasgos, las prácticas que se repiten en sistemas empresariales son:\n", "\n", "1. **Normalizar la entrada**: convertir PDF, imagen o escaneo en páginas procesables.\n", "2. **Parsear el documento**: usar OCR avanzado para obtener texto, tablas, figuras y orden de lectura.\n", "3. **Extraer estructura**: transformar el Markdown o texto en un esquema de negocio.\n", "4. **Validar y auditar**: conservar metadatos, confianza, página de origen y evidencia visual.\n", "5. **Integrar**: enviar los campos validados a un ERP, CRM, data lake o índice de búsqueda.\n", "\n", "En la investigación previa se revisaron fuentes públicas de Mistral OCR, DeepSeek-OCR, PaddleOCR y MinerU. Todas apuntan a la misma dirección: documentos convertidos a Markdown/JSON, soporte para PDFs e imágenes, manejo de tablas y layouts complejos, y salidas pensadas para LLMs, RAG o flujos de automatización empresarial." ], "id": "modern-ocr-patterns" }, { "cell_type": "markdown", "metadata": { "id": "create-example-document" }, "source": [ "## Documento de ejemplo\n", "\n", "Para mantener el notebook autocontenido, crearemos una factura simple como imagen. En un caso real, esta imagen podría venir de un PDF escaneado, de un correo electrónico, de un portal de proveedores o de un bucket de almacenamiento." ], "id": "create-example-document" }, { "cell_type": "code", "execution_count": 36, "metadata": { "id": "build-synthetic-invoice" }, "outputs": [], "source": [ "!wget -q https://raw.githubusercontent.com/santiagxf/M72109/master/docs/document-understanding/_images/factura_ejemplo.png" ], "id": "build-synthetic-invoice" }, { "cell_type": "markdown", "source": [ "![](https://raw.githubusercontent.com/santiagxf/M72109/master/docs/document-understanding/_images/factura_ejemplo.png)" ], "metadata": { "id": "undFw9eAv9dB" }, "id": "undFw9eAv9dB" }, { "cell_type": "markdown", "metadata": { "id": "mistral-ocr-section" }, "source": [ "## Opción 1: Mistral OCR como servicio\n", "\n", "Mistral OCR expone una API de OCR/document parsing. El patrón típico es subir el archivo, obtener una URL firmada y procesarlo con `mistral-ocr-latest`. La respuesta contiene páginas y Markdown, y opcionalmente imágenes embebidas en base64. Note que no escribimos la API key en el notebook: se lee desde una variable de ambiente." ], "id": "mistral-ocr-section" }, { "cell_type": "code", "source": [ "%pip install" ], "metadata": { "id": "S399Y_nYeFCM" }, "id": "S399Y_nYeFCM", "execution_count": null, "outputs": [] }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "run-mistral-or-mock" }, "outputs": [], "source": [ "import json\n", "import os\n", "\n", "MISTRAL_API_KEY = \"\" # Puede obtener una API de Mistral gratuita en https://console.mistral.ai/\n", "\n", "from mistralai.client import Mistral\n", "from mistralai.client.models import ImageURLChunk\n", "\n", "client = Mistral(api_key=MISTRAL_API_KEY)" ], "id": "run-mistral-or-mock" }, { "cell_type": "code", "source": [ "with open(\"factura_ejemplo.png\", \"rb\") as f:\n", " uploaded_file = client.files.upload(\n", " file={\n", " \"file_name\": documento_path.name,\n", " \"content\": documento_path.read(),\n", " },\n", " purpose=\"ocr\",\n", " )\n", " signed_url = client.files.get_signed_url(file_id=uploaded_file.id, expiry=1)" ], "metadata": { "id": "K_wznYMaeaUy" }, "id": "K_wznYMaeaUy", "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "respuesta_ocr = client.ocr.process(\n", " model=\"mistral-ocr-latest\",\n", " document=ImageURLChunk(image_url=signed_url.url),\n", " include_image_base64=False,\n", ")\n", "respuesta_ocr = json.loads(respuesta_ocr.model_dump_json())" ], "metadata": { "id": "daToFMo4eLZM" }, "id": "daToFMo4eLZM", "execution_count": null, "outputs": [] }, { "cell_type": "code", "source": [ "import json\n", "\n", "print(json.dumps(respuesta_ocr, indent=2))" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "-KvaFS2heC-2", "outputId": "4474d406-f25a-4098-e777-88f432f6baa3" }, "id": "-KvaFS2heC-2", "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "{\n", " \"pages\": [\n", " {\n", " \"index\": 0,\n", " \"markdown\": \"FACTURA\\n\\nProveedor: Contoso Servicios S.A.\\n\\nCliente: Fabrikam Retail\\n\\nFactura: F-2026-0017\\n\\nFecha: 2026-05-30\\n\\n| Concepto | Cantidad | Importe |\\n| --- | --- | --- |\\n| Soporte mensual | 1 | 1200.00 |\\n| Consultoria datos | 3 | 2400.00 |\\n\\nTotal: USD 3600.00\\n\\nOrden de compra: OC-8891\",\n", " \"images\": [],\n", " \"dimensions\": {\n", " \"dpi\": 200,\n", " \"height\": 650,\n", " \"width\": 900\n", " },\n", " \"tables\": [],\n", " \"hyperlinks\": [],\n", " \"header\": null,\n", " \"footer\": null,\n", " \"confidence_scores\": null\n", " }\n", " ],\n", " \"model\": \"mistral-ocr-latest\",\n", " \"usage_info\": {\n", " \"pages_processed\": 1,\n", " \"doc_size_bytes\": 15957\n", " },\n", " \"document_annotation\": null\n", "}\n" ] } ] }, { "cell_type": "markdown", "metadata": { "id": "ocr-output-note" }, "source": [ "El punto importante es que el downstream ya no recibe únicamente una lista de caracteres. Recibe una representación más cercana al documento: títulos, líneas y tablas. Esto simplifica el paso siguiente, que suele ser la extracción de campos de negocio." ], "id": "ocr-output-note" }, { "cell_type": "markdown", "metadata": { "id": "explore-markdown" }, "source": [ "### Explorando la salida en Markdown\n", "\n", "Veamos la primera página como Markdown. En documentos de muchas páginas, aquí conviene guardar también el número de página, el nombre del archivo, el hash del documento y el timestamp del procesamiento." ], "id": "explore-markdown" }, { "cell_type": "code", "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 288 }, "id": "display-markdown", "outputId": "af3777aa-b7a7-4366-f8a3-76c5524bcf03" }, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "" ], "text/markdown": "FACTURA\n\nProveedor: Contoso Servicios S.A.\n\nCliente: Fabrikam Retail\n\nFactura: F-2026-0017\n\nFecha: 2026-05-30\n\n| Concepto | Cantidad | Importe |\n| --- | --- | --- |\n| Soporte mensual | 1 | 1200.00 |\n| Consultoria datos | 3 | 2400.00 |\n\nTotal: USD 3600.00\n\nOrden de compra: OC-8891" }, "metadata": {} } ], "source": [ "from IPython.display import Markdown, display\n", "\n", "paginas = respuesta_ocr[\"pages\"]\n", "markdown_pagina = paginas[0][\"markdown\"]\n", "\n", "display(Markdown(markdown_pagina))" ], "id": "display-markdown" }, { "cell_type": "markdown", "metadata": { "id": "business-schema" }, "source": [ "### Extracción a un esquema empresarial\n", "\n", "En producción rara vez alcanza con guardar el Markdown completo. Normalmente se necesita un objeto con campos esperados: proveedor, factura, fecha, total, moneda y líneas de detalle. Para hacerlo explícito, definiremos un esquema con `pydantic` y una función de parsing simple. En un sistema real, esta función podría combinar reglas, LLMs con structured output o validaciones contra catálogos internos." ], "id": "business-schema" }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "extract-business-fields" }, "outputs": [], "source": [ "import re\n", "from typing import List\n", "\n", "from pydantic import BaseModel, Field\n", "\n", "\n", "class LineaFactura(BaseModel):\n", " concepto: str\n", " cantidad: int\n", " importe: float\n", "\n", "\n", "class FacturaExtraida(BaseModel):\n", " proveedor: str\n", " cliente: str\n", " numero_factura: str = Field(alias=\"factura\")\n", " fecha: str\n", " moneda: str\n", " total: float\n", " orden_compra: str\n", " lineas: List[LineaFactura]\n", "\n", "\n", "def buscar(pattern, texto):\n", " match = re.search(pattern, texto, flags=re.IGNORECASE)\n", " return match.group(1).strip() if match else None\n", "\n", "\n", "def extraer_factura(markdown):\n", " lineas_tabla = []\n", " for linea in markdown.splitlines():\n", " if linea.startswith(\"|\") and \"---\" not in linea and \"Concepto\" not in linea:\n", " celdas = [celda.strip() for celda in linea.strip(\"|\").split(\"|\")]\n", " if len(celdas) == 3:\n", " lineas_tabla.append(\n", " LineaFactura(\n", " concepto=celdas[0],\n", " cantidad=int(celdas[1]),\n", " importe=float(celdas[2]),\n", " )\n", " )\n", "\n", " moneda, total = re.search(r\"Total:\\s*([A-Z]{3})\\s*([0-9.]+)\", markdown).groups()\n", "\n", " return FacturaExtraida(\n", " proveedor=buscar(r\"Proveedor:\\s*(.+)\", markdown),\n", " cliente=buscar(r\"Cliente:\\s*(.+)\", markdown),\n", " factura=buscar(r\"Factura:\\s*(.+)\", markdown),\n", " fecha=buscar(r\"Fecha:\\s*([0-9-]+)\", markdown),\n", " moneda=moneda,\n", " total=float(total),\n", " orden_compra=buscar(r\"Orden de compra:\\s*(.+)\", markdown),\n", " lineas=lineas_tabla,\n", " )\n", "\n", "\n", "factura = extraer_factura(markdown_pagina)" ], "id": "extract-business-fields" }, { "cell_type": "code", "source": [ "print(json.dumps(factura.model_dump(by_alias=True), indent=2))" ], "metadata": { "id": "DrVrbCKDe0_z", "outputId": "04ef1637-b60b-4333-e888-f9612d71a869", "colab": { "base_uri": "https://localhost:8080/" } }, "id": "DrVrbCKDe0_z", "execution_count": null, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "{\n", " \"proveedor\": \"Contoso Servicios S.A.\",\n", " \"cliente\": \"Fabrikam Retail\",\n", " \"factura\": \"F-2026-0017\",\n", " \"fecha\": \"2026-05-30\",\n", " \"moneda\": \"USD\",\n", " \"total\": 3600.0,\n", " \"orden_compra\": \"OC-8891\",\n", " \"lineas\": [\n", " {\n", " \"concepto\": \"Soporte mensual\",\n", " \"cantidad\": 1,\n", " \"importe\": 1200.0\n", " },\n", " {\n", " \"concepto\": \"Consultoria datos\",\n", " \"cantidad\": 3,\n", " \"importe\": 2400.0\n", " }\n", " ]\n", "}\n" ] } ] }, { "cell_type": "markdown", "metadata": { "id": "deepseek-section" }, "source": [ "## Opción 2: DeepSeek-OCR en infraestructura propia\n", "\n", "DeepSeek-OCR es un modelo abierto orientado a OCR y conversión de documentos a Markdown. El repositorio oficial muestra dos caminos de inferencia: `vLLM` para despliegues con mayor throughput y `transformers` para pruebas más directas. En una empresa, esta opción resulta interesante cuando se requiere control de datos, despliegue privado o integración con GPU internas.\n", "\n", "El siguiente bloque queda desactivado por defecto porque requiere GPU, memoria suficiente y dependencias específicas como `torch`, `flash-attn` o `vLLM`. Se incluye para mostrar el patrón de uso, no como paso obligatorio del notebook." ], "id": "deepseek-section" }, { "cell_type": "code", "execution_count": null, "metadata": { "id": "deepseek-optional-code" }, "outputs": [], "source": [ "import torch\n", "from transformers import AutoModel, AutoTokenizer\n", "\n", "model_name = \"deepseek-ai/DeepSeek-OCR\"\n", "tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)\n", "model = AutoModel.from_pretrained(\n", " model_name,\n", " trust_remote_code=True,\n", " use_safetensors=True,\n", ").eval().cuda().to(torch.bfloat16)\n", "\n", "prompt = \"\\n<|grounding|>Convert the document to markdown.\"\n", "output_path = \"deepseek_ocr_output\"\n", "\n", "resultado = model.infer(\n", " tokenizer,\n", " prompt=prompt,\n", " image_file=str(documento_path),\n", " output_path=output_path,\n", " base_size=1024,\n", " image_size=640,\n", " crop_mode=True,\n", " save_results=True,\n", ")\n", "\n", "print(resultado)" ], "id": "deepseek-optional-code" }, { "cell_type": "markdown", "metadata": { "id": "reference-architecture" }, "source": [ "## Arquitectura de referencia\n", "\n", "Podemos resumir el flujo empresarial de esta forma:\n", "\n", "1. **Ingesta**: recibir PDFs o imágenes desde correo, SFTP, API o almacenamiento interno.\n", "2. **Clasificación ligera**: identificar tipo de documento y prioridad.\n", "3. **OCR avanzado**: procesar con un servicio como Mistral OCR o con un modelo desplegado internamente como DeepSeek-OCR, PaddleOCR o MinerU.\n", "4. **Normalización**: convertir la salida a Markdown/JSON y adjuntar metadatos.\n", "5. **Extracción estructurada**: mapear a esquemas de negocio.\n", "6. **Validación**: aplicar reglas determinísticas, catálogos y umbrales de confianza.\n", "7. **Human-in-the-loop**: derivar a revisión solo los documentos ambiguos.\n", "8. **Persistencia**: guardar documento original, salida OCR, campos extraídos y trazabilidad.\n", "\n", "La pregunta práctica sería: ¿qué modelo conviene elegir? Si se prioriza rapidez de adopción, un servicio administrado reduce la complejidad operativa. Si se priorizan datos sensibles, costos a escala o despliegue offline, un modelo abierto desplegado internamente puede ser más adecuado." ], "id": "reference-architecture" }, { "cell_type": "markdown", "metadata": { "id": "references" }, "source": [ "## Referencias consultadas\n", "\n", "- [Mistral OCR cookbook: structured OCR](https://github.com/mistralai/cookbook/blob/main/mistral/ocr/structured_ocr.ipynb).\n", "- [Mistral OCR cookbook: data extraction via annotations](https://github.com/mistralai/cookbook/blob/main/mistral/ocr/data_extraction.ipynb).\n", "- [DeepSeek-OCR repository](https://github.com/deepseek-ai/DeepSeek-OCR).\n", "- [PaddleOCR repository](https://github.com/PaddlePaddle/PaddleOCR).\n", "- [MinerU repository](https://github.com/opendatalab/MinerU)." ], "id": "references" }, { "cell_type": "markdown", "metadata": { "id": "closing" }, "source": [ "## Cierre\n", "\n", "Este ejemplo muestra el patrón principal: un modelo OCR avanzado convierte el documento en una representación rica, luego una capa de extracción y validación lo transforma en datos confiables para la empresa. En producción, recuerde evaluar con documentos reales del dominio, medir errores por tipo de campo y conservar evidencia para auditoría." ], "id": "closing" } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "name": "python", "pygments_lexer": "ipython3" }, "colab": { "provenance": [] } }, "nbformat": 4, "nbformat_minor": 5 }