""" preprocess_vision.py — Ejecutar localmente UNA VEZ antes del docker build. Renderiza cada página de los PDFs como imagen y llama a Groq Vision para extraer descripciones detalladas de tablas, gráficos e imágenes. El resultado se guarda en documents/vision_descriptions.json y debe hacerse commit para que el Docker build lo incluya en el índice. Uso: pip install pymupdf groq python-dotenv GROQ_API_KEY= python preprocess_vision.py git add documents/vision_descriptions.json git commit -m "docs: add Groq Vision descriptions for PDF pages" git push """ import argparse import base64 import json import os import sys from pathlib import Path try: import fitz except ImportError: print("ERROR: instala pymupdf: pip install pymupdf") sys.exit(1) try: from groq import Groq except ImportError: print("ERROR: instala groq sdk: pip install groq") sys.exit(1) try: from dotenv import load_dotenv load_dotenv() except ImportError: pass VISION_MODEL = os.getenv("GROQ_VISION_MODEL", "meta-llama/llama-4-scout-17b-16e-instruct") DPI_SCALE = 2.0 # ~144 dpi SKIP_TOKEN = "SIN_CONTENIDO_VISUAL" VISION_PROMPT = ( "Eres un experto en análisis de documentos. " "Analiza esta página y describe con precisión todos los elementos visuales que encuentres: " "tablas (extrae TODOS los datos fila a fila), gráficos (valores y tendencias), " "imágenes informativas o diagramas. " "Si la página solo contiene texto plano sin elementos visuales relevantes, " f"responde únicamente: {SKIP_TOKEN}" ) def render_page_b64(page: "fitz.Page") -> str: mat = fitz.Matrix(DPI_SCALE, DPI_SCALE) pix = page.get_pixmap(matrix=mat) return base64.b64encode(pix.tobytes("png")).decode("utf-8") def describe_page(client: "Groq", b64_image: str, model: str) -> str: response = client.chat.completions.create( model=model, messages=[{ "role": "user", "content": [ {"type": "text", "text": VISION_PROMPT}, {"type": "image_url", "image_url": { "url": f"data:image/png;base64,{b64_image}" }}, ], }], max_tokens=768, ) return response.choices[0].message.content.strip() def process_pdfs(docs_dir: Path, model: str) -> dict: client = Groq(api_key=os.environ["GROQ_API_KEY"]) results: dict = {} pdf_paths = sorted(docs_dir.glob("*.pdf")) if not pdf_paths: print(f"No se encontraron PDFs en {docs_dir}") return results print(f"Procesando {len(pdf_paths)} PDFs con modelo: {model}\n") for pdf_path in pdf_paths: fname = pdf_path.name print(f"[{fname}]") results[fname] = {} doc = fitz.open(str(pdf_path)) for idx in range(len(doc)): page_num = idx + 1 print(f" Página {page_num}/{len(doc)} ... ", end="", flush=True) try: b64 = render_page_b64(doc[idx]) desc = describe_page(client, b64, model) if SKIP_TOKEN in desc: print("sin contenido visual") else: results[fname][str(page_num)] = desc print(f"OK ({len(desc)} chars)") except Exception as e: print(f"ERROR: {e}") doc.close() print() return results def main() -> None: parser = argparse.ArgumentParser(description="Preprocesa PDFs con Groq Vision.") parser.add_argument("--docs-dir", default="./documents", help="Directorio de PDFs") parser.add_argument("--model", default=VISION_MODEL, help="Modelo Groq Vision a usar") args = parser.parse_args() if not os.environ.get("GROQ_API_KEY"): print("ERROR: define la variable de entorno GROQ_API_KEY") sys.exit(1) docs_dir = Path(args.docs_dir) output_path = docs_dir / "vision_descriptions.json" # Load existing descriptions to allow incremental runs existing: dict = {} if output_path.exists(): with open(output_path, encoding="utf-8") as f: existing = json.load(f) print(f"Cargadas descripciones existentes para: {list(existing.keys())}\n") new_results = process_pdfs(docs_dir, args.model) # Merge: existing is overwritten by new results merged = {**existing, **new_results} with open(output_path, "w", encoding="utf-8") as f: json.dump(merged, f, ensure_ascii=False, indent=2) total = sum(len(v) for v in merged.values()) print(f"Guardadas {total} descripciones en {output_path}") print("\nSiguiente paso:") print(" git add documents/vision_descriptions.json") print(" git commit -m 'docs: add Groq Vision descriptions'") print(" git push") if __name__ == "__main__": main()