compañero de Enterprise Document Intelligence, la serie que construye un sistema RAG empresarial a partir de cuatro ladrillos. El artículo 5 (análisis de documentos) construyó el analizador con PyMuPDF (fitz). Este complemento mantiene el mismo objetivo y las mismas tablas relacionales, y cambia el motor por Azure Layout (el modelo de diseño prediseñado), un paquete más completo que recupera lo que fitz no puede recuperar. Esa brecha es donde comenzamos.
PyMuPDF (fitz) es rápido, gratuito y exacto en prosa limpia. También se queda ciego en tres lugares, y en cada uno de ellos la empresa RAG se quiebra silenciosamente.
La tabla de la página 14 de un contrato. Fitz lee las celdas una por una y las concatena. La estructura de columnas ha desaparecido. “Tarifa de renovación 500 Tarifa de instalación 200” aterriza en el fragmento. Se le pide a su modelo que adivine qué número corresponde a qué tarifa.
La enmienda escaneada pegada al final del documento. Fitz lee las páginas nativas y devuelve cadenas vacías en las escaneadas. El usuario no obtiene respuesta sobre la enmienda porque el analizador nunca la leyó.
La figura con texto en su interior. Un gráfico con etiquetas de ejes. Un sello firmado. Una captura de pantalla de una hoja de cálculo. Fitz devuelve el bbox de la imagen. El texto del interior ha desaparecido.
Azure Document Intelligence lee los tres. Es un servicio en la nube patentado de Microsoft Azure que se rige por los Términos de servicios en línea de Microsoft. El modelo de diseño prediseñado devuelve celdas de tabla nativas (filas, columnas, encabezados), texto OCR para cada página (nativo o escaneado), figuras con el texto dentro de ellas y roles de párrafo (título, encabezado de sección, título de figura, título de tabla). Una llamada. Las mismas tablas relacionales que fitz, la mitad de ellas enriquecidas.
Al oleoducto aguas abajo no le importa qué motor produjo el dictado. Recuperación, generación, anotación de filas leídas. Nunca leen el PDF.
1. Donde fitz es ciego
Cuatro casos. En cada uno, Fitz falla y Azure trabaja.
1.1. Tablas: fitz devuelve palabras planas, Azure devuelve celdas
Una tabla de contrato tiene filas y columnas. La etiqueta "Tarifa de renovación" se encuentra en la columna 1, el valor 500 se encuentra en la columna 2. Fitz lee la página de arriba a abajo y emite una línea por segmento de texto. Las cuatro celdas de una fila se convierten en cuatro palabras sueltas. A veces, las celdas de la fila siguiente se mezclan si las coordenadas y están cerca. El trozo río abajo ve una sopa de palabras. La estructura de filas y columnas que convierte a una tabla en una tabla ha desaparecido.
El modelo de diseño prediseñado de Azure detecta cada tabla como un objeto estructurado. result.tables es una lista de tablas, cada una con celdas indexadas por (row_index, column_index). La fila del encabezado está marcada (cell.kind == "columnHeader"). El contenido de la celda es el texto de la celda, exactamente como lo escribió el autor. Aplanamos la tabla en filas de rebajas para que quede dentro de line_df como cualquier otro contenido. Una fila de cuatro celdas "Tarifa de renovación | 500 | Tarifa de instalación | 200" se convierte en una fila line_df con ese texto de reducción. La fila del encabezado obtiene un | — | — | … | separador para que un modelo posterior lea la estructura.
1.2. Imágenes: fitz devuelve el bbox, Azure devuelve el texto
Muchos archivos PDF tienen figuras con texto dentro. Diagramas de arquitectura con etiquetas de cajas. Gráficos con ejes y leyendas. Sellos de sello firmados. Capturas de pantalla integradas de hojas de cálculo. Fitz devuelve cada imagen como un bbox y los bytes sin formato. El texto interior es invisible para el analizador.
El OCR de Azure se ejecuta en todas las páginas, incluidos los píxeles dentro de las regiones de las figuras. Para cada figura, recopilamos cada palabra de Azure cuyo bbox se encuentra dentro de la región de la figura y las unimos como ocr_text. “Concat lineal h de atención de múltiples cabezales” ahora se encuentra en image_df.ocr_text para la figura de la página 4 del documento de atención. La recuperación puede coincidir con una pregunta sobre “atención de múltiples cabezas” incluso cuando la respuesta es texto dentro de una figura.
1.3. Páginas escaneadas: fitz no devuelve nada, Azure devuelve OCR
Un contrato nativo de 30 páginas tiene una enmienda escaneada de 10 páginas pegada al final. Fitz lee las páginas nativas y devuelve cadenas vacías para las escaneadas. El analizador no marca esto. El proceso descendente cubre silenciosamente el 75% del documento. El usuario no tiene idea de que falta el 25%.
Azure ejecuta OCR en cada página independientemente de la fuente. Las páginas nativas y las páginas escaneadas regresan a través de la misma ruta result.pages[i].lines con la misma forma. La columna parsing_method en line_df permite que el código posterior indique qué motor produjo qué filas. El dictado parsing_summary tiene un campo n_pages que coincide con el recuento de páginas real del documento, no solo las páginas con texto nativo.
1.4. Leyendas y encabezados: fitz usa expresiones regulares, Azure tiene roles explícitos
Fitz detecta títulos de figuras/tablas mediante expresiones regulares al inicio de cada línea (^Figura d+b, ^Tabla d+b). Funciona cuando los subtítulos se parecen a la "Figura 2" y omiten el resto ("Fig. 2", ajustes de varias líneas). También tiene falsos positivos: una frase del cuerpo del texto que comienza con "Figura 2" se recoge como título cuando se trata de una mención.
El campo de párrafos de Azure tiene etiquetas de función: cada párrafo del resultado lleva una etiqueta como "figureCaption", "tableCaption", "title" o "sectionHeading" que nos indica qué tipo de bloque es, sin ninguna expresión regular. "figureCaption" y "tableCaption" completan object_registry directamente. "título" y "secciónEncabezado" reconstruyen el TOC. La etiqueta es el modelo de diseño de Azure que nombra la función del bloque; fitz no tiene equivalente. La clave de unión (object_type, object_id) aún se extrae mediante la misma expresión regular en el texto del título, por lo que cross_ref_df se une de la misma manera.
El TOC es el caso más interesante. build_toc_df de Fitz lee marcadores nativos (doc.get_toc()). Cuando el PDF no tiene marcadores nativos, fitz devuelve un índice de contenido vacío. Este es el caso empresarial habitual: exportaciones de Word, documentos escaneados, archivos PDF de generadores de formularios. Azure reconstruye la tabla de contenido a partir de roles de párrafo. Cada párrafo de "título" se convierte en una entrada de nivel 1, cada párrafo de "encabezado de sección" pasa a ser de nivel 2. La jerarquía proviene del orden en que aparecen. Esto no es perfecto, pero produce un TOC utilizable donde fitz no produciría nada.
2. Mismo contrato, datos más ricos
Una función. Las mismas tablas que parse_pdf, con la misma forma. Una llamada de Azure compartida por cada constructor. Esa llamada es pequeña: apunte el SDK al documento con un model_id, diseño prediseñado. (El otro modelo prediseñado, lectura prediseñada, es solo OCR; el modelo de diseño es el que también devuelve tablas, roles de párrafos y orden de lectura).
de azure.ai.documentintelligence import DocumentIntelligenceClient de azure.ai.documentintelligence.models import AnalyzeDocumentRequest de azure.core.credentials import AzureKeyCredential client = DocumentIntelligenceClient(endpoint, AzureKeyCredential(key)) # "Layout" = el modelo de diseño prediseñado (NO lectura prediseñada, que es solo OCR) con open("contract.pdf", "rb") as f: poller = client.begin_analyze_document( "prebuilt-layout", AnalyzeDocumentRequest(bytes_source=f.read()), ) result = poller.result() # tablas, roles de párrafo, OCR, orden de lectura
parse_pdf_azure_layout es el gemelo de Azure de parse_pdf: la misma forma de llamada, el mismo dictado de tablas, por lo que cada bloque posterior lo lee sin saber qué motor se ejecutó. Vale la pena echarle un vistazo al cuerpo, porque es la forma que siguen todos los motores de la serie: haga una llamada, luego un pequeño constructor por tabla y reutilice los constructores independientes del motor para las tablas que solo necesitan line_df.
def parse_pdf_azure_layout(pdf_path): resultado = analizar_pdf(pdf_path) # una llamada, diseño prediseñado line_df = azure_layout_pdf_to_line_df(pdf_path, resultado=resultado) image_df = build_image_df_azure_layout(resultado) # + ocr_text toc_df = build_toc_df_azure_layout(resultado) # roles de párrafo object_registry = build_object_registry_azure_layout(resultado) # etiquetas de rol page_df = build_page_df(line_df) # constructor fitz reutilizado (solo line_df) cross_ref_df = build_cross_ref_df(line_df) # constructor fitz reutilizado (solo line_df) return {"line_df": line_df, "image_df": image_df, "toc_df": toc_df, "object_registry": object_registry, "page_df": page_df, "cross_ref_df": cross_ref_df, "span_df": pd.DataFrame(), "parsing_summary": parsing_summary}
Leyendo de arriba a abajo: un analyse_pdf realiza la llamada de Azure una vez, luego un pequeño constructor por tabla lee ese resultado compartido, y las dos tablas que solo necesitan line_df, page_df y cross_ref_df, son producidas por los mismos constructores Fitz que utiliza el analizador nativo. El dictado al final es el contrato que devuelve cada motor.
3. Qué gana cada mesa
3.1. line_df gana filas de celdas de tabla, OCR de imagen y marcas de selección
Una tabla de “Programación de cargos” de 4 columnas se convierte en 6 filas en line_df: la fila del encabezado, el separador de rebajas y cuatro filas de datos.
Mantenemos las celdas dentro de line_df en lugar de agregar un table_cells_df separado. Una tabla para que la lea cada ladrillo aguas abajo; Las líneas de párrafo y las filas de la tabla tienen el mismo aspecto al final. El costo: las consultas por celda necesitan un paso de análisis de rebajas. Para preguntas de RAG, esto está bien. El recuperador busca palabras clave en el texto de la fila. El LLM lee la rebaja directamente.
El texto OCR del interior de las imágenes también llega a line_df como filas adicionales. result.pages[i].lines de Azure ya incluye líneas que se encuentran dentro de las regiones de la figura, por lo que el creador de líneas las selecciona automáticamente. Las marcas de selección (casillas de verificación) se convierten en líneas de un solo carácter: [x] para seleccionados, [ ] para no seleccionados. Los formularios con campos con casillas de verificación se vuelven consultables.
3.2. image_df gana una columna ocr_text
Misma fila, nueva columna. Para cada figura detectada, enumeramos todas las palabras de Azure cuyo bbox se superpone a la región de la figura en al menos un 50 % y las unimos como ocr_text.
La misma columna en un image_df producido por Fitz está vacía. El analizador Fitz no realiza OCR de imágenes. Cuando parsing_method == "fitz", la columna ocr_text está ahí para la paridad de formas pero permanece en blanco. El código posterior que verifica ocr_text != "" funciona igual si la fila proviene de Fitz o Azure.
3.3. toc_df se reconstruye a partir de roles de párrafo
Cuando el PDF tiene marcadores nativos, el fitz build_toc_df es exacto y gratuito: lee lo que escribió el autor. Cuando no es así (la mayoría de los documentos empresariales), fitz devuelve un toc_df vacío y las etapas posteriores pierden la estructura de la sección.
El constructor de Azure recorre los párrafos de resultados, filtra por función en {"title", "sectionHeading"} y ensambla una tabla de contenido. Nivel 1 = título, nivel 2 = título de sección. La jerarquía proviene del orden en que aparecen los párrafos en el documento. Las mismas columnas start_page, end_page, start_y y ruta de navegación que el TOC de fitz. El pase de retrospectiva que calcula end_page (la página de inicio del siguiente par o ancestro, o total_pages para la última sección) es idéntico al de fitz; la única diferencia es de dónde vienen las filas.
La reconstrucción no es perfecta. Azure no puede distinguir los niveles de subsección más allá del encabezado de sección. La jerarquía que obtienes tiene dos profundidades como máximo. Para la mayoría de las consultas empresariales, esto es suficiente: un fragmento con el sello "Lista de cargos" permite al LLM basar su respuesta en la sección correcta incluso sin la ruta completa del Artículo 14 > Lista de cargos.
3.4. object_registry obtiene detección de rol de subtítulos
Fitz detecta subtítulos mediante expresiones regulares ancladas al inicio de una línea: ^Figura d+b, ^Tabla d+b. Dos modos de falla. Falsos negativos cuando el formato del título difiere (Fig. 2, en lugar de la Figura 2, o un ajuste de varias líneas que saca el número de la primera línea). Falsos positivos cuando una frase del cuerpo del texto comienza con "La Figura 2 muestra…".
Azure omite el problema de las expresiones regulares. Sus campos de párrafos etiquetan explícitamente "figureCaption" y "tableCaption". Leemos el papel directamente. La clave de unión (object_type, object_id) en cross_ref_df todavía se extrae del texto del título mediante la misma expresión regular que usa el constructor fitz, por lo que la unión funciona igual con cualquiera de los motores. La victoria es el recuerdo: Azure capta los subtítulos que Fitz pierde. El costo sigue siendo el mismo (una llamada de Azure, el resultado se reutiliza entre los constructores).
3.5. parsing_summary obtiene estadísticas específicas de Azure
Tres nuevos campos aparecen en el dictado de síntesis a nivel de documento:
n_tables_detected: cuántas tablas encontró Azure (cero en un documento en prosa pura, distinto de cero en un contrato con tablas). n_figures: cuántas figuras identificó el modelo de diseño. n_selection_marks: cuántas casillas de verificación (llenas o vacías) detectó Azure en todas las páginas.
Estos tres aspectos facilitan el enrutamiento de un documento. Un documento de 30 páginas con n_tables_detected = 18 parece un contrato y la estructura de la tabla es importante. Un documento con n_selection_marks = 0 probablemente no sea un formulario. Un documento con n_figures = 0 es sólo de texto; No tiene sentido ejecutar OCR de imagen.
3.6. page_df y cross_ref_df: sin cambios
Dos mesas mantienen la misma forma. page_df y cross_ref_df se crean solo a partir de line_df, por lo que el motor que produjo line_df es irrelevante. Una implementación, dos motores, sin deriva.
span_df está vacío en Azure. El modelo de diseño no expone la tipografía de sublínea (negrita o cursiva por palabra). Cuando necesite intervalos para la detección de encabezados o el énfasis de términos, permanezca en fitz para ese documento. Los dos motores se complementan.
4. La columna parsing_method: procedencia del análisis adaptativo
Cada tabla por fila de parse_pdf_azure_layout lleva parsing_method == "azure_layout". Cada tabla por fila de parse_pdf (la de fitz) lleva parsing_method == "fitz". Misma columna, mismo nombre, ambos motores. El punto está aguas abajo.
Esto es lo que consume el análisis adaptativo (Artículo 10). El pase predeterminado usa fitz. Azure vuelve a analizar las páginas que no superan una comprobación previa al análisis (región de la tabla detectada sin filas extraídas, página con muchas imágenes y texto escaso, capa de OCR con baja calidad). Las filas analizadas nuevamente reemplazan o se agregan a las filas line_df originales. La columna parsing_method mantiene el rastro.
La columna permite tres patrones posteriores:
Desduplicación: cuando la misma página obtuvo ambas pasadas, mantenga las filas azules sobre las filas fitz (df.sort_values("parsing_method").drop_duplicates(["page_num", "line_num"], keep="first") if "azure_layout" < "fitz" lexicográficamente, o use un mapa de precedencia explícito). Auditoría: una pregunta que aparece en una fila con parsing_method == "azure_layout" cuesta más de verificar (se necesitaba Azure). La ponderación de confianza de la respuesta puede utilizar esto. Contabilidad de costos: (line_df.parsing_method == "azure_layout").any() por página le indica qué páginas pasaron por Azure y cómo facturar el tiempo de análisis.
5. Costo y latencia
Azure no es gratis. Tres números importan.
Latencia: una página a través del diseño prediseñado regresa en 2 a 4 segundos. Un documento de 30 páginas tarda entre 60 y 120 segundos. Fitz analiza el mismo documento en menos de un segundo. Cuando el usuario esté esperando una consulta, analice primero con fitz. Escalar a Azure solo en páginas que Fitz manejó mal.
Dinero: Azure cobra por página. El nivel de diseño prediseñado hoy en día ronda los 10 dólares estadounidenses por cada 1.000 páginas. Un contrato de 30 páginas cuesta aproximadamente 0,30 dólares estadounidenses. Analizar 1.000 contratos de este tipo por día cuesta 300 dólares al día si cada página pasa por Azure. Restringir Azure a las páginas que lo necesitan lo reduce 10 veces o más.
Límites: el límite de tamaño de PDF por llamada es de 500 MB o 2000 páginas, lo que ocurra primero. Es necesario dividir los documentos más grandes. El nivel gratuito (F0) permite 500 páginas por mes y está bien para el desarrollo. La producción normalmente necesita S0.
El orden de magnitud es estable: Fitz es gratuito, Azure cuesta aproximadamente un centavo por página. Los precios de los niveles exactos cambian según la región y el tiempo: trate los números anteriores como una calibración, no como un contrato. El artículo 10 elige qué motor funciona.
6. Cuándo llamar a cuál
Por defecto es fitz. Escale a Azure cuando una señal específica indique que Fitz no es suficiente.
Tres señales que vale la pena cablear:
La página tiene una región de tabla, pero Fitz extrajo pocas o ninguna estructura similar a una fila. Calcule en line_df: agrupe líneas por coordenada y, busque tramos de líneas cortas espaciadas uniformemente (un signo de celdas). Si los metadatos de la página dicen "tabla detectada" (de page.find_tables() de Fitz) pero el patrón de línea no parece una tabla, escale. La página tiene muchas imágenes y texto escaso. image_df para la página cubre más del 80% del área de la página y line_df tiene menos de 10 filas en esa página. Página escaneada sin capa de OCR, o una página que es un diagrama grande con texto dentro. Cualquiera de los casos necesita Azure. El nivel de calidad de OCR es bajo: cuando page.get_text("text") de Fitz devuelve OCR codificado (proporción alta de caracteres de reemplazo Unicode, proporción baja de diccionario-palabra), vuelva a realizar el OCR con Azure. El text_quality_score se calcula en pre_parse_signals y el despachador lo lee.
Una cuarta señal es más sencilla. Si el documento no tiene una tabla de contenido nativa (fitz.toc_df.empty) y la generación necesita un contexto de sección, ejecute el documento una vez a través de Azure para obtener una tabla de contenido reconstruida. Un coste por documento, no por consulta.
El artículo 10 construye el despachador completo. La columna parsing_method es lo que permite que cada etapa posterior lea qué motor se ejecutó en qué fila.
7. Conclusión
Dos motores, un contrato: las mismas tablas relacionales, el mismo código descendente independientemente de cuál se ejecute.
Un analizador no devuelve texto; devuelve un modelo del documento. Azure enriquece ese modelo (tablas a nivel de celda, OCR dentro de figuras, subtítulos etiquetados por rol, TOC reconstruido sin marcadores) en 2 a 4 segundos y ~US$0,01 por página. Fitz no cuesta nada y funciona en milisegundos. La regla de enrutamiento es simple: fitz de forma predeterminada, Azure cuando una señal ascendente dice que fitz no es suficiente. El artículo 10 telegrafía al despachador.
8. Fuentes y lecturas adicionales
El modelo de diseño prediseñado detrás de parse_pdf_azure_layout está documentado por Microsoft y se basa en una investigación de extracción de tablas a nivel de celda (Smock et al. 2022) más una capa de roles de párrafo que convierte regiones visuales en roles estructurales. Docling (artículo 5ter) es el equivalente de código abierto de la misma cascada; Ofrece el mismo contrato de mesa en hardware local, útil cuando los documentos no pueden salir del edificio.
Misma dirección que el artículo:
Microsoft, inteligencia de documentos Azure AI. Modelo de diseño. Documentación oficial para el diseño prediseñado, el modelo detrás de parse_pdf_azure_layout. La salida de la tabla a nivel de celda, las funciones de los párrafos y la cobertura de OCR se originan aquí. Smock, Pesala, Abraham, PubTables-1M / Table Transformer (TATR), CVPR 2022 (arXiv:2110.00061). La investigación detrás de la extracción de tablas a nivel de celda de Azure Ships; útil para comprender qué está haciendo azure_layout bajo el capó.
Ángulo diferente, contexto diferente:
Auer et al., Informe técnico Docling, IBM Research 2024 (arXiv:2408.09869). Equivalente local de código abierto de la cascada de diseño de Azure. Contrato de la misma mesa; intercambia el costo de la nube por computación local. La elección correcta cuando la confidencialidad bloquea la carga en la nube que requiere Azure.
Al principio de la serie: