Skip to main content

ocr-quirurgico-pdf-md

Pipeline completo OCR quirúrgico: PDF → página a página → Markdown/HTML con precisión total. Detecta tablas, firmas, diagramas, imágenes. Si no entiende algo, captura y graba en posición exacta. Vectoriza para búsqueda con ChromaDB + qwen3-embedding.

跳到安装

来源信息

仓库
Ntizar/NtizarBrainMasterMind
最近来源活动
2026年6月27日 12:14
检测到的 SKILL.md 语言
西班牙语
星标
2
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

文件资源管理器
3 个文件

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
name
ocr-quirurgico-pdf-md
version
1.0.0
description
Pipeline completo OCR quirúrgico: PDF → página a página → Markdown/HTML con precisión total. Detecta tablas, firmas, diagramas, imágenes. Si no entiende algo, captura y graba en posición exacta. Vectoriza para búsqueda con ChromaDB + qwen3-embedding.
tags
["ocr","pdf","markdown","html","pipeline","tablas","firmas","diagramas","chromadb","qwen3"]
# OCR Quirúrgico PDF → MD/HTML ## Resumen Pipeline que recibe un PDF, lo **corta página a página** y produce un Markdown o HTML con **precisión absoluta**: - Texto real → extraído con bounding boxes - Tablas → detectadas con pdfplumber + reconstruidas como `<table>` - Firmas → paths vectoriales capturados como imagen - Diagramas → render + OCR + imagen incrustada - Imágenes → extraídas con fitz + metadata - Si algo falla → **captura de pantalla** (render 300dpi) + imagen en posición exacta Luego **todo se vectoriza** con qwen3-embedding (4096d) para búsqueda semántica por ChromaDB. ## Cuándo usarlo - Usuario adjunta PDF técnico y pide "convertir a Markdown/HTML con todo en su sitio" - Usuario quiere un **documento navegable** donde cada tabla, firma, y diagrama esté en la posición correcta - Usuario necesita **búsqueda semántica** sobre el contenido (miles de páginas) ## No es para - OCR de documentos escaneados con fotos (usar `documentos-institucionales`) - Extracción rápida sin preservación de layout (usar `markitdown` o `pdf-to-dashboard`) - **Extracción de datos estructurados de PDFs digitales** → usar `pdf-llm-extraction` (nuevo paradigma: font analysis + LLM, 100% confianza vs ~60% con regex) - Documentos con solo texto sin tablas ni imágenes (usar `pdf-to-artifacts-david-antizar`) > ⚠️ **Paradigma shift (2026-06):** Para PDFs digitales con texto seleccionable, el pipeline `pdf-llm-extraction` (PyMuPDF + Qwen 3.6) supera cualitativamente a cualquier enfoque regex o OCR. Usar OCR solo para PDFs escaneados/imágenes. ## Stack de herramientas | Herramienta | Rol | Instalación | |------------|-----|-------------| | `fitz` (PyMuPDF) | Extracción principal + render + bounding boxes | `pip install PyMuPDF` | | `pdfplumber` | Detección de tablas + líneas de cuadrícula | `pip install pdfplumber` | | `Tesseract` | OCR de imágenes cuando el texto no es extraíble | `apt install tesseract-ocr` | | `camelot` | Tablas sin líneas (fallback) | `pip install camelot-py[cv]` | | `Pillow` | Procesado de imágenes + deskew | `pip install Pillow` | | `qwen3-embedding` | Vectorización 4096d | `api.nan.builders/v1/embeddings` | | `ChromaDB` | Almacenamiento vectorial + búsqueda | `pip install chromadb` | | `OpenCV` | Preprocesado de imágenes (threshold, denoise) | `pip install opencv-python` | ## Arquitectura del pipeline ``` PDF ↓ ┌───────────────────────────────┐ │ FASE 0 — PREPARACIÓN │ │ - Verificar herramientas │ │ - Crear directorios de salida │ │ - Detectar tipo de PDF │ └───────────────────────────────┘ ↓ ┌───────────────────────────────┐ │ FASE 1 — CORTE │ │ - fitz: abrir PDF │ │ - Por cada página: │ │ - text = get_text("dict") │ │ - images = get_images() │ │ - paths = get_drawings() │ │ - metadata = page metadata │ └───────────────────────────────┘ ↓ ┌───────────────────────────────┐ │ FASE 2 — CLASIFICACIÓN │ │ - Por cada elemento: │ │ - ¿Es texto? → extraer │ │ - ¿Es tabla? → pdfplumber │ │ - ¿Es firma? → capturar │ │ - ¿Es diagrama? → render │ │ - ¿Es imagen? → extraer │ │ - ¿No se entiende? → OCR │ │ → Captura de imagen │ └───────────────────────────────┘ ↓ ┌───────────────────────────────┐ │ FASE 3 — ENSAMBLADO │ │ - Página por página: │ │ - Markdown: texto + tablas │ │ - HTML: con imágenes incrust │ │ - Imagen fallback incrustada │ │ - Metadata por elemento: │ │ - tipo, bbox, confidence │ │ - página, documento │ └───────────────────────────────┘ ↓ ┌───────────────────────────────┐ │ FASE 4 — VECTORIZACIÓN │ │ - qwen3-embedding por página │ │ - qwen3-embedding por elemento │ │ - ChromaDB: store + metadata │ │ - Búsqueda: consulta semántica│ └───────────────────────────────┘ ``` ## Fase 0 — Preparación ```bash # 1. Verificar herramientas python3 -c " import sys try: import fitz; print('fitz ✅') import pdfplumber; print('pdfplumber ✅') import cv2; print('OpenCV ✅') except ImportError as e: print(f'Falta: {e}') sys.exit(1) " # 2. Verificar Tesseract which tesseract || echo "Instalar: apt install tesseract-ocr tesseract-ocr-spa" # 3. Crear directorio de trabajo mkdir -p /tmp/ocr-pipeline/{pages,elements,output,images} ``` ## Fase 1 — Corte página a página ```python import fitz import json from pathlib import Path def cortar_pdf(pdf_path: str, output_dir: str = "/tmp/ocr-pipeline") -> dict: """ Corta un PDF página a página y devuelve un dict con: - pages: [{index, text_blocks, images, drawings, metadata}] - total_pages: int - metadata: {title, author, producer, ...} """ doc = fitz.open(pdf_path) pages_data = [] for i, page in enumerate(doc): # Metadata de página rect = page.rect rotation = page.rotation # Extraer texto con bounding boxes (dict mode) text_dict = page.get_text("dict") # Extraer imágenes images = page.get_images(full=True) # Extraer paths/dibujos (diagramas, firmas) paths = page.get_drawings() # Extraer elementos de texto con coordenadas blocks = [] for block in text_dict.get("blocks", []): if block.get("type") == 0: # texto blocks.append({ "type": "text", "bbox": block.get("bbox"), "lines": [ { "spans": [ {"text": s.get("text"), "font": s.get("font"), "size": s.get("size"), "color": s.get("color")} for s in span.get("spans", []) ] } for line in block.get("lines", []) ] }) elif block.get("type") == 1: # imagen blocks.append({ "type": "image", "bbox": block.get("bbox"), "image": block.get("image") # bytes }) pages_data.append({ "page": i + 1, "rect": [rect.x0, rect.y0, rect.x1, rect.y1], "rotation": rotation, "blocks": blocks, "images": [{"index": j, "xref": img[0], "width": img[2], "height": img[3]} for j, img in enumerate(images)], "drawings": [{"path": d} for d in paths] }) return { "pages": pages_data, "total_pages": len(doc), "metadata": { "title": doc.metadata.get("title"), "author": doc.metadata.get("author"), "producer": doc.metadata.get("producer"), "format": doc.metadata.get("format") } } # Guardar para la fase 2 json.dump(cortar_pdf("documento.pdf"), open("/tmp/ocr-pipeline/pages_raw.json", "w"), ensure_ascii=False) ``` ### Fase 1b — Render de página para fallback ```python def render_page_as_image(pdf_path, page_num, dpi=300): """Renderiza una página completa como imagen PNG a 300 DPI""" doc = fitz.open(pdf_path) page = doc[page_num - 1] pix = page.get_pixmap(dpi=dpi) pix.save(f"/tmp/ocr-pipeline/pages/page_{page_num:04d}.png") return f"/tmp/ocr-pipeline/pages/page_{page_num:04d}.png" ``` ## Fase 2 — Clasificación de elementos ```python def clasificar_elemento(elemento: dict, page_width: float, page_height: float) -> str: """ Clasifica un bloque de texto en: - "texto" → párrafo normal - "tabla" → líneas alineadas en columnas - "firma" → paths vectoriales (curvas bezier) - "diagrama" → líneas + texto + formas - "imagen" → imagen embebida - "no_identificado" → render + OCR """ bbox = elemento.get("bbox", [0, 0, 0, 0]) w = bbox[2] - bbox[0] h = bbox[3] - bbox[1] tipo = elemento.get("type", "") # Heurísticas if tipo == "image": return "imagen" if tipo == "text": # ¿Es tabla? (múltiples líneas con mismo espaciado X) lines = elemento.get("lines", []) if len(lines) > 2: x_coords = set() for line in lines: for span in line.get("spans", []): x_coords.add(round(span["bbox"][0])) # Si hay más de 3 columnas X distintas → tabla if len(set(round(x) for x in x_coords)) > 3: return "tabla" # ¿Es imagen grande? (ocupa >80% del ancho) if w > page_width * 0.8: return "imagen_grande" # ¿Es texto normal? return "texto" # Paths vectoriales → firma o diagrama if tipo == "drawing": # Si son curvas bezier → firma # Si son rectas + texto → diagrama return "firma" if len(paths) < 5 else "diagrama" return "no_identificado" ``` ## Fase 3 — OCR de fallback (cuando no se entiende) ```python import subprocess from PIL import Image def ocr_fallback(pdf_path, page_num, bbox=None): """ Cuando el texto no es extraíble (fuentes embebidas, etc.): - Renderiza la página completa o solo el bbox - Ejecuta Tesseract - Devuelve: {"text": "...", "confidence": float, "image_path": "..."} """ doc = fitz.open(pdf_path) page = doc[page_num - 1] if bbox: # Solo renderizar la región del bbox clip = fitz.Rect(*bbox) pix = page.get_pixmap(dpi=300, clip=clip) else: # Página completa pix = page.get_pixmap(dpi=300) img_path = f"/tmp/ocr-pipeline/images/page_{page_num:04d}_bbox.png" pix.save(img_path) # Tesseract result = subprocess.run( ["tesseract", img_path, "stdout", "-l", "spa+eng", "--psm", "6"], capture_output=True, text=True, timeout=30 ) return { "text": result.stdout.strip(), "confidence": 0.8, # placeholder "image_path": img_path } def ocr_con_llm(img_path: str, model="qwen3.6"): """ Cuando Tesseract no es suficiente (diagramas, logos, firmas): Envía la imagen a un modelo multimodal para descripción. """ # Aquí iría la llamada a NaN API con el modelo multimodal # Ej: curl -X POST https://api.nan.builders/v1/chat/completions # -d '{"model": "qwen3-vl", "messages": [{"role": "user", "content": "Describe esta imagen de un documento"}]}' return {"description": "...", "type": "diagrama"} ``` ## Fase 4 — Ensamblado a Markdown/HTML ```python def ensamblar_markdown(pages_data, pdf_path, output_dir="/tmp/ocr-pipeline/output"): """ Genera un Markdown con: - Cada página como sección - Imágenes incrustadas con MEDIA: - Tablas como markdown tables - Metadata de cada elemento """ md = [] md.append(f"# {pdf_path}\n") for page_data in pages_data: md.append(f"\n## Página {page_data['page']}\n") for block in page_data.get("blocks", []): tipo = block["type"] if tipo == "text": md.append(block["text"]) elif tipo == "table": md.append(formatear_tabla(block)) elif tipo == "image": md.append(f"\n![Imagen página {page_data['page']}]({block['path']})\n") elif tipo == "diagram": md.append(f"\n```\n{block['description']}\n```\n") md.append(f"![Diagrama]({block['image_path']})\n") elif tipo == "signature": md.append(f"\n---\n### Firma\n![Firma]({block['image_path']})\n---\n") return "\n".join(md) def ensamblar_html(pages_data, pdf_path, output_dir="/tmp/ocr-pipeline/output"): """ Versión HTML con: - CSS para responsive - Imágenes en base64 (incrustadas en el HTML) - Tablas con <table> - Cada elemento con su metadata """ html = ["<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><style>"] html.append(""" body { font-family: 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; background: #fafafa; } .page { border: 1px solid #ddd; margin: 20px 0; padding: 20px; background: white; border-radius: 8px; } table { width: 100%; border-collapse: collapse; margin: 10px 0; }
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看