Módulo 6: RAG Multimodal
5. Documentos con Imágenes
Descripción
Los documentos del mundo real no son texto puro. Un manual técnico tiene diagramas de arquitectura. Un reporte financiero tiene gráficos de tendencia. Un paper académico tiene figuras con resultados experimentales. Un catálogo de productos tiene fotos. El RAG multimodal solo funciona si puedes procesar estos documentos preservando la relación entre texto e imágenes.
En esta cápsula vas a aprender estrategias para chunking de documentos que contienen figuras, cómo asociar cada imagen con el texto que la rodea, y cómo construir un pipeline completo que tome un PDF con contenido mixto y lo prepare para indexación.
Por qué importa: Si extraes texto e imágenes por separado sin preservar su relación, pierdes contexto crítico. Un chunk de texto que dice "como muestra la Figura 3" es inútil si no sabes qué es la Figura 3. Y una descripción de imagen que dice "gráfico de barras mostrando datos" es vaga si no incluyes el título y el texto que la explica.
Conexión con el módulo: Esta cápsula alimenta el pipeline de indexación de la cápsula 03. Los chunks que generas aquí se buscan con el retrieval de la cápsula 04. Y el proyecto de la cápsula 08 procesa documentos reales con esta lógica.
El Problema: Documentos Mixtos
Qué contiene un documento típico
Manual técnico (PDF, 50 páginas):
├── Texto corrido: explicaciones, descripciones, procedimientos
├── Diagramas: arquitectura, flujo, entidad-relación
├── Tablas: configuraciones, comparaciones, especificaciones
├── Capturas de pantalla: interfaz, logs, configuración
├── Figuras referenciadas: "ver Figura 3", "como muestra el Diagrama 2"
└── Headers/footers: títulos de sección, números de página
Qué pasa si ignoras las imágenes
Pregunta: "¿Cómo se conecta el servicio de pagos con la base de datos?"
Solo texto:
→ Recupera: "El servicio de pagos se conecta a PostgreSQL vía connection pool"
→ Respuesta: parcial, sin el diagrama que muestra el flujo completo
Texto + imágenes:
→ Recupera: párrafo + "Diagrama mostrando servicio de pagos conectado
a PostgreSQL vía PgBouncer con pool de 20 conexiones"
→ Respuesta: completa, incluye detalles del diagrama
Qué pasa si no preservas la relación
Chunk de texto: "Como muestra la Figura 3, el flujo de autenticación
pasa por tres etapas."
Chunk de imagen (sin relación): "Diagrama de flujo con tres cajas
conectadas por flechas."
→ El retrieval puede encontrar el texto O la imagen, pero no sabe
que van juntos. La respuesta pierde coherencia.
Estrategias de Chunking para Documentos Mixtos
Estrategia 1: Chunk por página
Cada página del PDF es un chunk. El chunk incluye todo el texto de la página más referencias a las imágenes de esa página.
import fitz
from pathlib import Path
def chunk_by_page(pdf_path: str) -> list[dict]:
doc = fitz.open(pdf_path)
chunks = []
for page_num in range(len(doc)):
page = doc[page_num]
text = page.get_text().strip()
images = page.get_images()
if not text and not images:
continue
chunks.append({
"text": text,
"page": page_num + 1,
"chunk_index": page_num,
"has_images": len(images) > 0,
"image_count": len(images),
"strategy": "page",
})
doc.close()
return chunks
Ventajas: Simple. Las imágenes de una página están naturalmente asociadas con el texto de esa página.
Desventajas: Páginas largas producen chunks grandes que diluyen la señal. Páginas con solo una imagen producen chunks vacíos de texto.
Estrategia 2: Chunk por sección con imágenes asociadas
Divide por secciones (headers) y asocia las imágenes de esas páginas con la sección correspondiente.
import re
def chunk_by_section(pdf_path: str, max_chunk_words: int = 500) -> list[dict]:
doc = fitz.open(pdf_path)
full_text = ""
page_boundaries = {}
current_pos = 0
for page_num in range(len(doc)):
page = doc[page_num]
page_text = page.get_text()
page_boundaries[page_num + 1] = (current_pos, current_pos + len(page_text))
full_text += page_text
current_pos += len(page_text)
doc.close()
sections = re.split(r'\n(?=[A-Z][^\n]{3,50}\n)', full_text)
chunks = []
for i, section in enumerate(sections):
section = section.strip()
if not section:
continue
section_page = 1
section_start = full_text.find(section)
for page, (start, end) in page_boundaries.items():
if start <= section_start < end:
section_page = page
break
words = section.split()
if len(words) > max_chunk_words:
sub_chunks = split_text_with_overlap(section, max_chunk_words, overlap=50)
for j, sub in enumerate(sub_chunks):
chunks.append({
"text": sub,
"page": section_page,
"chunk_index": len(chunks),
"strategy": "section",
"section_part": j,
})
else:
chunks.append({
"text": section,
"page": section_page,
"chunk_index": len(chunks),
"strategy": "section",
})
return chunks
def split_text_with_overlap(
text: str,
max_words: int,
overlap: int = 50
) -> list[str]:
words = text.split()
chunks = []
start = 0
while start < len(words):
end = start + max_words
chunk = " ".join(words[start:end])
chunks.append(chunk)
start += max_words - overlap
return chunks
Estrategia 3: Chunk enriquecido con contexto de imagen
El chunk de texto incluye la descripción de las imágenes como parte del contenido. Así el embedding captura tanto el texto como el contenido visual.
from openai import OpenAI
import base64
client = OpenAI()
def describe_image_bytes(image_bytes: bytes, ext: str = "png") -> str:
b64 = base64.b64encode(image_bytes).decode("utf-8")
mime = f"image/{ext}" if ext != "jpg" else "image/jpeg"
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{
"type": "text",
"text": "Describe esta imagen en 1-2 oraciones. Incluye tipo de contenido y elementos principales."
},
{
"type": "image_url",
"image_url": {"url": f"data:{mime};base64,{b64}"}
}
]
}],
max_tokens=100
)
return response.choices[0].message.content
def enrich_chunks_with_image_context(
chunks: list[dict],
images_by_page: dict[int, list[dict]]
) -> list[dict]:
enriched = []
for chunk in chunks:
page = chunk.get("page", 0)
page_images = images_by_page.get(page, [])
if page_images:
descriptions = []
for img in page_images:
desc = describe_image_bytes(img["data"], img.get("ext", "png"))
descriptions.append(desc)
image_context = "\n\n".join(
[f"[Figura en página {page}: {d}]" for d in descriptions]
)
enriched_text = f"{chunk['text']}\n\n{image_context}"
enriched.append({
**chunk,
"text": enriched_text,
"has_images": True,
"image_descriptions": descriptions,
})
else:
enriched.append({
**chunk,
"has_images": False,
"image_descriptions": [],
})
return enriched
Comparación de estrategias
| Estrategia | Mejor para | Chunks típicos | Calidad de relación |
|---|---|---|---|
| Por página | PDFs cortos, documentos bien maquetados | ~1 por página | Media — toda la página va junta |
| Por sección | Documentos con headers claros (markdown, HTML) | Variable | Alta — secciones temáticas |
| Enriquecido | Cualquier documento | Mismo que base + contexto | Alta — texto + descripción de imagen |
Extraer Imágenes de PDF
Extracción con PyMuPDF
def extract_images_by_page(pdf_path: str) -> dict[int, list[dict]]:
doc = fitz.open(pdf_path)
images_by_page = {}
for page_num in range(len(doc)):
page = doc[page_num]
img_list = page.get_images()
if not img_list:
continue
page_images = []
for img_ref in img_list:
xref = img_ref[0]
try:
base_image = doc.extract_image(xref)
if base_image and base_image.get("image"):
page_images.append({
"data": base_image["image"],
"ext": base_image.get("ext", "png"),
"width": base_image.get("width", 0),
"height": base_image.get("height", 0),
"xref": xref,
})
except Exception:
continue
if page_images:
images_by_page[page_num + 1] = page_images
doc.close()
return images_by_page
Guardar imágenes extraídas
def save_extracted_images(
images_by_page: dict[int, list[dict]],
output_dir: str,
pdf_name: str
) -> dict[int, list[dict]]:
output = Path(output_dir)
output.mkdir(parents=True, exist_ok=True)
saved = {}
for page, images in images_by_page.items():
saved[page] = []
for i, img in enumerate(images):
filename = f"{pdf_name}_p{page}_img{i}.{img['ext']}"
filepath = output / filename
with open(filepath, "wb") as f:
f.write(img["data"])
saved[page].append({
**img,
"path": str(filepath),
"filename": filename,
})
return saved
Filtrar imágenes irrelevantes
No todas las imágenes en un PDF son útiles. Logos, iconos pequeños, separadores y fondos son ruido.
MIN_IMAGE_WIDTH = 100
MIN_IMAGE_HEIGHT = 100
MIN_IMAGE_BYTES = 5000
def filter_relevant_images(
images_by_page: dict[int, list[dict]]
) -> dict[int, list[dict]]:
filtered = {}
for page, images in images_by_page.items():
relevant = []
for img in images:
width = img.get("width", 0)
height = img.get("height", 0)
size = len(img.get("data", b""))
if width < MIN_IMAGE_WIDTH or height < MIN_IMAGE_HEIGHT:
continue
if size < MIN_IMAGE_BYTES:
continue
relevant.append(img)
if relevant:
filtered[page] = relevant
return filtered
Pipeline Completo: PDF → Chunks con Contexto de Imagen
El pipeline integrado
def process_document_with_images(
pdf_path: str,
strategy: str = "enriched",
chunk_size: int = 500,
output_dir: str = "./extracted_images",
describe_images: bool = True
) -> list[dict]:
pdf_name = Path(pdf_path).stem
if strategy == "page":
chunks = chunk_by_page(pdf_path)
elif strategy == "section":
chunks = chunk_by_section(pdf_path, max_chunk_words=chunk_size)
else:
chunks = chunk_by_section(pdf_path, max_chunk_words=chunk_size)
images_by_page = extract_images_by_page(pdf_path)
images_by_page = filter_relevant_images(images_by_page)
saved_images = save_extracted_images(images_by_page, output_dir, pdf_name)
if strategy == "enriched" and describe_images:
chunks = enrich_chunks_with_image_context(chunks, images_by_page)
image_chunks = []
for page, images in saved_images.items():
for i, img in enumerate(images):
if describe_images:
desc = describe_image_bytes(img["data"], img.get("ext", "png"))
else:
desc = f"Imagen de página {page}"
image_chunks.append({
"text": desc,
"page": page,
"chunk_index": len(chunks) + len(image_chunks),
"type": "image",
"image_path": img.get("path", ""),
"has_images": True,
})
for chunk in chunks:
if "type" not in chunk:
chunk["type"] = "text"
all_chunks = chunks + image_chunks
return all_chunks
# chunks = process_document_with_images("manual_tecnico.pdf")
# print(f"Total chunks: {len(chunks)}")
# text_chunks = [c for c in chunks if c["type"] == "text"]
# image_chunks = [c for c in chunks if c["type"] == "image"]
# print(f" Texto: {len(text_chunks)}, Imágenes: {len(image_chunks)}")
Indexar el resultado
import chromadb
from chromadb.utils import embedding_functions
import os
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=os.getenv("OPENAI_API_KEY"),
model_name="text-embedding-3-small"
)
def index_processed_document(
chunks: list[dict],
collection,
source: str
) -> dict:
stats = {"text": 0, "image": 0, "errors": 0}
documents = []
ids = []
metadatas = []
for chunk in chunks:
chunk_type = chunk.get("type", "text")
chunk_id = f"{source}_{chunk_type}_{chunk['chunk_index']}"
documents.append(chunk["text"])
ids.append(chunk_id)
metadatas.append({
"type": chunk_type,
"source": source,
"page": chunk.get("page", 0),
"has_images": chunk.get("has_images", False),
"image_path": chunk.get("image_path", ""),
})
stats[chunk_type] = stats.get(chunk_type, 0) + 1
if documents:
try:
collection.add(
documents=documents,
ids=ids,
metadatas=metadatas
)
except Exception as e:
print(f"Error indexando: {e}")
stats["errors"] += 1
return stats
Manejar Referencias a Figuras
Detectar referencias en el texto
Muchos documentos técnicos usan "ver Figura X", "como muestra el Diagrama Y", etc. Detectar estas referencias permite asociar chunks de texto con las imágenes correctas.
import re
def find_figure_references(text: str) -> list[dict]:
patterns = [
r'(?:ver|see|véase)\s+(?:la\s+)?(?:Figura|Figure|Fig\.?)\s*(\d+)',
r'(?:Figura|Figure|Fig\.?)\s*(\d+)',
r'(?:Diagrama|Diagram)\s*(\d+)',
r'(?:Tabla|Table)\s*(\d+)',
r'(?:Gráfico|Graph|Chart)\s*(\d+)',
]
references = []
for pattern in patterns:
for match in re.finditer(pattern, text, re.IGNORECASE):
references.append({
"type": "figure",
"number": int(match.group(1)),
"span": match.span(),
"context": text[max(0, match.start()-50):match.end()+50],
})
return references
sample = "Como muestra la Figura 3, el servicio de pagos se conecta vía Diagrama 1 al gateway."
refs = find_figure_references(sample)
for r in refs:
print(f" Referencia: {r['type']} #{r['number']} → ...{r['context']}...")
Resolver referencias: asociar texto con imagen
def resolve_figure_references(
text_chunks: list[dict],
image_chunks: list[dict],
images_by_page: dict
) -> list[dict]:
figure_number_to_image = {}
sorted_images = []
for page in sorted(images_by_page.keys()):
for img in images_by_page[page]:
sorted_images.append({"page": page, **img})
for i, img in enumerate(sorted_images):
figure_number_to_image[i + 1] = img
resolved_chunks = []
for chunk in text_chunks:
refs = find_figure_references(chunk["text"])
associated_images = []
for ref in refs:
fig_num = ref["number"]
if fig_num in figure_number_to_image:
associated_images.append(figure_number_to_image[fig_num])
resolved_chunks.append({
**chunk,
"figure_references": refs,
"associated_images": associated_images,
})
return resolved_chunks
Enriquecer chunks con figuras resueltas
def enrich_with_resolved_figures(
resolved_chunks: list[dict]
) -> list[dict]:
enriched = []
for chunk in resolved_chunks:
if chunk["associated_images"]:
image_descriptions = []
for img in chunk["associated_images"]:
desc = describe_image_bytes(img["data"], img.get("ext", "png"))
image_descriptions.append(desc)
fig_context = "\n".join(
[f"[Figura referenciada: {d}]" for d in image_descriptions]
)
enriched_text = f"{chunk['text']}\n\n{fig_context}"
enriched.append({
**chunk,
"text": enriched_text,
"image_descriptions": image_descriptions,
})
else:
enriched.append(chunk)
return enriched
Manejar Tablas en Documentos
Extraer tablas con PyMuPDF
def extract_tables_from_page(page) -> list[dict]:
tables = page.find_tables()
extracted = []
for table in tables:
rows = table.extract()
if not rows:
continue
headers = rows[0] if rows else []
data_rows = rows[1:] if len(rows) > 1 else []
markdown = format_table_as_markdown(headers, data_rows)
extracted.append({
"markdown": markdown,
"headers": headers,
"rows": data_rows,
"row_count": len(data_rows),
})
return extracted
def format_table_as_markdown(
headers: list,
rows: list[list]
) -> str:
if not headers:
return ""
clean_headers = [str(h).strip() if h else "" for h in headers]
header_line = "| " + " | ".join(clean_headers) + " |"
separator = "| " + " | ".join(["---"] * len(clean_headers)) + " |"
lines = [header_line, separator]
for row in rows:
clean_cells = [str(c).strip() if c else "" for c in row]
while len(clean_cells) < len(clean_headers):
clean_cells.append("")
lines.append("| " + " | ".join(clean_cells[:len(clean_headers)]) + " |")
return "\n".join(lines)
Incorporar tablas al chunking
def extract_tables_by_page(pdf_path: str) -> dict[int, list[dict]]:
doc = fitz.open(pdf_path)
tables_by_page = {}
for page_num in range(len(doc)):
page = doc[page_num]
tables = extract_tables_from_page(page)
if tables:
tables_by_page[page_num + 1] = tables
doc.close()
return tables_by_page
def enrich_chunks_with_tables(
chunks: list[dict],
tables_by_page: dict[int, list[dict]]
) -> list[dict]:
enriched = []
for chunk in chunks:
page = chunk.get("page", 0)
page_tables = tables_by_page.get(page, [])
if page_tables:
table_context = "\n\n".join(
[f"[Tabla en página {page}:\n{t['markdown']}]" for t in page_tables]
)
enriched_text = f"{chunk['text']}\n\n{table_context}"
enriched.append({**chunk, "text": enriched_text, "has_tables": True})
else:
enriched.append({**chunk, "has_tables": False})
return enriched
Pipeline Avanzado: Texto + Imágenes + Tablas
def process_document_full(
pdf_path: str,
chunk_size: int = 500,
output_dir: str = "./extracted_images",
describe_images: bool = True
) -> dict:
pdf_name = Path(pdf_path).stem
text_chunks = chunk_by_section(pdf_path, max_chunk_words=chunk_size)
images_by_page = extract_images_by_page(pdf_path)
images_by_page = filter_relevant_images(images_by_page)
saved_images = save_extracted_images(images_by_page, output_dir, pdf_name)
tables_by_page = extract_tables_by_page(pdf_path)
if describe_images:
text_chunks = enrich_chunks_with_image_context(text_chunks, images_by_page)
text_chunks = enrich_chunks_with_tables(text_chunks, tables_by_page)
image_only_chunks = []
for page, images in saved_images.items():
for i, img in enumerate(images):
desc = describe_image_bytes(img["data"], img.get("ext", "png")) if describe_images else f"Imagen p.{page}"
image_only_chunks.append({
"text": desc,
"page": page,
"chunk_index": len(text_chunks) + len(image_only_chunks),
"type": "image",
"image_path": img.get("path", ""),
})
for c in text_chunks:
c["type"] = "text"
all_chunks = text_chunks + image_only_chunks
stats = {
"total_chunks": len(all_chunks),
"text_chunks": len(text_chunks),
"image_chunks": len(image_only_chunks),
"pages_with_images": len(images_by_page),
"pages_with_tables": len(tables_by_page),
"total_images": sum(len(imgs) for imgs in images_by_page.values()),
"total_tables": sum(len(tbls) for tbls in tables_by_page.values()),
}
return {"chunks": all_chunks, "stats": stats}
Troubleshooting
PyMuPDF no extrae imágenes de ciertos PDFs
Algunos PDFs tienen imágenes embebidas como vectores (SVG) o como parte del renderizado de página, no como imágenes raster. PyMuPDF solo extrae raster.
def extract_page_as_image(
pdf_path: str,
page_num: int,
dpi: int = 150
) -> bytes:
doc = fitz.open(pdf_path)
page = doc[page_num]
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
image_bytes = pix.tobytes("png")
doc.close()
return image_bytes
Chunks demasiado largos por tablas grandes
Si una tabla tiene 100 filas, el chunk resultante es enorme.
MAX_TABLE_ROWS = 20
def truncate_table(table: dict, max_rows: int = MAX_TABLE_ROWS) -> dict:
if table["row_count"] <= max_rows:
return table
truncated_rows = table["rows"][:max_rows]
markdown = format_table_as_markdown(table["headers"], truncated_rows)
markdown += f"\n... ({table['row_count'] - max_rows} filas más)"
return {**table, "markdown": markdown, "rows": truncated_rows}
Descripciones de imagen duplicadas
Si la misma imagen aparece en múltiples páginas (logos, headers), se describe múltiples veces.
import hashlib
def deduplicate_images(
images_by_page: dict[int, list[dict]]
) -> dict[int, list[dict]]:
seen_hashes = set()
deduped = {}
for page, images in images_by_page.items():
unique = []
for img in images:
img_hash = hashlib.md5(img["data"]).hexdigest()
if img_hash not in seen_hashes:
seen_hashes.add(img_hash)
unique.append(img)
if unique:
deduped[page] = unique
return deduped
Referencias a figuras que no existen
El texto dice "ver Figura 5" pero solo hay 3 imágenes en el documento.
Solución: Log la referencia no resuelta y continúa sin enriquecer ese chunk.
No falles el pipeline completo por una referencia rota.
Ejercicios
Ejercicio 1: Asociar imagen con chunk de texto por proximidad
Dado un chunk de texto y una lista de imágenes por página, retorna las imágenes de la misma página o de páginas adyacentes (± 1).
Ver solución
def get_nearby_images(
chunk: dict,
images_by_page: dict[int, list[dict]],
range_pages: int = 1
) -> list[dict]:
page = chunk.get("page", 0)
nearby = []
for p in range(page - range_pages, page + range_pages + 1):
if p in images_by_page:
for img in images_by_page[p]:
nearby.append({**img, "from_page": p})
return nearby
chunk = {"text": "El flujo de autenticación...", "page": 5}
images_by_page = {
4: [{"data": b"...", "ext": "png"}],
5: [{"data": b"...", "ext": "png"}, {"data": b"...", "ext": "jpg"}],
7: [{"data": b"...", "ext": "png"}],
}
nearby = get_nearby_images(chunk, images_by_page, range_pages=1)
print(f"Imágenes cercanas al chunk p.{chunk['page']}: {len(nearby)}")
for img in nearby:
print(f" - Página {img['from_page']}, formato {img['ext']}")
Ejercicio 2: Pipeline de procesamiento con estadísticas
Implementa un pipeline que procese un PDF y devuelva estadísticas detalladas: chunks por tipo, páginas con imágenes, tamaño promedio de chunk, etc.
Ver solución
def process_with_stats(pdf_path: str) -> dict:
result = process_document_full(pdf_path, describe_images=False)
chunks = result["chunks"]
text_chunks = [c for c in chunks if c.get("type") == "text"]
image_chunks = [c for c in chunks if c.get("type") == "image"]
text_lengths = [len(c["text"].split()) for c in text_chunks]
avg_text_length = sum(text_lengths) / len(text_lengths) if text_lengths else 0
pages_with_images = set()
for c in chunks:
if c.get("has_images") or c.get("type") == "image":
pages_with_images.add(c.get("page", 0))
stats = {
"file": pdf_path,
"total_chunks": len(chunks),
"text_chunks": len(text_chunks),
"image_chunks": len(image_chunks),
"avg_text_words": round(avg_text_length),
"min_text_words": min(text_lengths) if text_lengths else 0,
"max_text_words": max(text_lengths) if text_lengths else 0,
"pages_with_images": len(pages_with_images),
"enriched_text_chunks": sum(1 for c in text_chunks if c.get("has_images")),
}
return stats
Ejercicio 3: Comparar estrategias de chunking
Procesa el mismo PDF con las tres estrategias (page, section, enriched) y compara el número de chunks, tamaño promedio, y cobertura de imágenes.
Ver solución
def compare_chunking_strategies(pdf_path: str) -> dict:
strategies = {}
for strategy_name in ["page", "section", "enriched"]:
if strategy_name in ("page", "section"):
if strategy_name == "page":
chunks = chunk_by_page(pdf_path)
else:
chunks = chunk_by_section(pdf_path)
for c in chunks:
c["type"] = "text"
else:
result = process_document_full(pdf_path, describe_images=False)
chunks = result["chunks"]
text_chunks = [c for c in chunks if c.get("type") == "text"]
word_counts = [len(c["text"].split()) for c in text_chunks]
strategies[strategy_name] = {
"total_chunks": len(chunks),
"text_chunks": len(text_chunks),
"avg_words": round(sum(word_counts) / len(word_counts)) if word_counts else 0,
"with_image_context": sum(1 for c in text_chunks if c.get("has_images")),
}
print(f"Comparación para: {pdf_path}\n")
print(f"{'Estrategia':<15} {'Chunks':<10} {'Texto':<10} {'Avg Words':<12} {'Con Img':<10}")
print("-" * 57)
for name, stats in strategies.items():
print(f"{name:<15} {stats['total_chunks']:<10} {stats['text_chunks']:<10} "
f"{stats['avg_words']:<12} {stats['with_image_context']:<10}")
return strategies
Resumen
- Los documentos reales mezclan texto, imágenes, tablas y referencias cruzadas.
- Tres estrategias de chunking: por página (simple), por sección (semántica), enriquecido (texto + descripción de imagen).
- Extraer imágenes con PyMuPDF y filtrar las irrelevantes (logos, iconos pequeños).
- Asociar imágenes con su texto circundante preserva el contexto que el RAG necesita.
- Detectar referencias ("ver Figura 3") y resolverlas contra las imágenes extraídas.
- Las tablas se extraen y convierten a markdown para incluir en los chunks.
- Deduplicar imágenes repetidas (logos en cada página) reduce ruido y costo.
- El pipeline completo: PDF → extraer texto + imágenes + tablas → chunking → enriquecer → indexar.
Recursos Adicionales
- PyMuPDF Documentation — Extracción de texto, imágenes y tablas
- Unstructured.io — Librería especializada en procesamiento de documentos
- Document Chunking Strategies — Guía de estrategias de chunking
- LangChain Document Loaders — Loaders para diferentes formatos