""" ============================================================= EXTRACTOR DE PRODUCTOS PDF - GRADIO + HUGGING FACE ============================================================= """ import os import re import time import tempfile import pandas as pd import gradio as gr from openai import OpenAI from pypdf import PdfReader from io import StringIO from pdf2image import convert_from_path import pytesseract import warnings import logging warnings.filterwarnings('ignore') logging.getLogger('pypdf').setLevel(logging.ERROR) PAGINAS_POR_BLOQUE = 3 MODELO_OPENAI = "gpt-4o-mini" UMBRAL_TEXTO_MINIMO = 50 SYSTEM_PROMPT = """Eres un experto extractor de datos de catálogos de productos electrónicos colombianos y latinoamericanos. Tu ÚNICA salida permitida es CSV puro separado por punto y coma (;). REGLAS ABSOLUTAS: - NO escribas introducciones ni explicaciones - NO uses markdown, ni ```, ni comillas - NO agregues encabezados como "Referencia;Descripción;Precio" - SOLO datos línea por línea - Una línea por producto""" USER_PROMPT_TEMPLATE = """MISIÓN: Extraer TODOS los productos con precios del siguiente texto de catálogo. FORMATO DE SALIDA (CSV con ;): [Referencia];[Descripción];[Precio numérico] REGLAS CRÍTICAS PARA PRECIOS: 1. FORMATOS DE PRECIO QUE DEBES RECONOCER: - $150.000 → escribe: 150000 - $150,000 → escribe: 150000 - 150.000 → escribe: 150000 - 150,000 → escribe: 150000 - $150000 → escribe: 150000 - COP 150.000 → escribe: 150000 - 1.234.567 → escribe: 1234567 2. SIEMPRE elimina: $, COP, USD, puntos, comas del precio 3. El precio debe ser SOLO números (ej: 150000) 4. Si hay precio tachado y precio de oferta, usa el de OFERTA 5. Si NO encuentras precio, escribe: SIN_PRECIO 6. NUNCA escribas 0 como precio REGLAS PARA REFERENCIA: - Busca: REF, SKU, CÓDIGO, COD, P/N, PARTE, # - Si no hay código, inventa uno con las iniciales del producto REGLAS PARA DESCRIPCIÓN: - Une líneas separadas en una sola descripción - Incluye especificaciones técnicas - Máximo 100 caracteres TEXTO DEL CATÁLOGO: {texto} RESPONDE SOLO CON EL CSV (sin encabezados):""" def extraer_texto_normal(pdf_path, pagina_inicio, pagina_fin): reader = PdfReader(pdf_path) texto = "" for i in range(pagina_inicio, min(pagina_fin, len(reader.pages))): t = reader.pages[i].extract_text() if t: texto += f"\n--- Página {i+1} ---\n{t}" return texto def extraer_texto_ocr(pdf_path, pagina_inicio, pagina_fin): try: images = convert_from_path( pdf_path, first_page=pagina_inicio + 1, last_page=pagina_fin, dpi=200 ) texto = "" for idx, img in enumerate(images): t = pytesseract.image_to_string(img, lang='spa') texto += f"\n--- Página {pagina_inicio + idx + 1} ---\n{t}" return texto except Exception as e: return f"Error OCR: {str(e)}" def extraer_texto_inteligente(pdf_path, pagina_inicio, pagina_fin): texto_normal = extraer_texto_normal(pdf_path, pagina_inicio, pagina_fin) num_paginas = pagina_fin - pagina_inicio chars_por_pagina = len(texto_normal.replace(" ", "").replace("\n", "")) / max(num_paginas, 1) if chars_por_pagina < UMBRAL_TEXTO_MINIMO: return extraer_texto_ocr(pdf_path, pagina_inicio, pagina_fin), "OCR" else: return texto_normal, "TEXTO" def limpiar_precio(precio_raw): if pd.isna(precio_raw): return None precio_str = str(precio_raw).strip().upper() if any(x in precio_str for x in ['SIN', 'N/A', 'NA', 'CONSULTAR', '-', 'PRECIO']): return None precio_limpio = re.sub(r'[^\d.,]', '', precio_str) if not precio_limpio: return None if '.' in precio_limpio and ',' not in precio_limpio: partes = precio_limpio.split('.') if all(len(p) == 3 for p in partes[1:]): precio_limpio = precio_limpio.replace('.', '') if ',' in precio_limpio and '.' not in precio_limpio: partes = precio_limpio.split(',') if all(len(p) == 3 for p in partes[1:]): precio_limpio = precio_limpio.replace(',', '') if ',' in precio_limpio and '.' in precio_limpio: if precio_limpio.rfind('.') > precio_limpio.rfind(','): precio_limpio = precio_limpio.replace(',', '') else: precio_limpio = precio_limpio.replace('.', '').replace(',', '.') precio_limpio = re.sub(r'[,]', '', precio_limpio) try: precio_num = float(precio_limpio) return int(precio_num) if precio_num >= 0 else None except: return None def procesar_pdf(archivo_pdf, api_key, progress=gr.Progress()): if not archivo_pdf: return None, None, "❌ Por favor sube un archivo PDF" if not api_key or api_key.strip() == "": return None, None, "❌ Por favor ingresa tu API Key de OpenAI" try: client = OpenAI(api_key=api_key.strip()) except Exception as e: return None, None, f"❌ Error con API Key: {str(e)}" try: reader = PdfReader(archivo_pdf.name) total_pages = len(reader.pages) except Exception as e: return None, None, f"❌ Error al leer PDF: {str(e)}" log_mensajes = [] log_mensajes.append(f"📄 Documento: {total_pages} páginas") log_mensajes.append(f"📦 Procesando en bloques de {PAGINAS_POR_BLOQUE} páginas\n") all_csv_data = [] metodos = {"TEXTO": 0, "OCR": 0} total_bloques = (total_pages + PAGINAS_POR_BLOQUE - 1) // PAGINAS_POR_BLOQUE for idx, i in enumerate(range(0, total_pages, PAGINAS_POR_BLOQUE)): end_page = min(i + PAGINAS_POR_BLOQUE, total_pages) progress((idx + 1) / total_bloques, desc=f"Procesando páginas {i+1}-{end_page}") texto_bloque, metodo = extraer_texto_inteligente(archivo_pdf.name, i, end_page) metodos[metodo] += 1 if not texto_bloque.strip(): log_mensajes.append(f"⚠️ Páginas {i+1}-{end_page}: Sin texto extraíble") continue try: response = client.chat.completions.create( model=MODELO_OPENAI, messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": USER_PROMPT_TEMPLATE.format(texto=texto_bloque)} ], temperature=0, max_tokens=4096 ) raw_response = response.choices[0].message.content clean_csv = raw_response.replace("```csv", "").replace("```", "").strip() lines = [l for l in clean_csv.split('\n') if l.strip() and ';' in l] clean_csv = '\n'.join(lines) if not clean_csv: log_mensajes.append(f"⚠️ Páginas {i+1}-{end_page}: Respuesta vacía") continue df_chunk = pd.read_csv( StringIO(clean_csv), sep=";", on_bad_lines='skip', names=["Referencia", "Descripción", "Precio_Raw"], header=None ) df_chunk = df_chunk.dropna(subset=['Descripción']) df_chunk = df_chunk[~df_chunk['Referencia'].astype(str).str.lower().str.contains('referencia', na=False)] all_csv_data.append(df_chunk) log_mensajes.append(f"✅ Páginas {i+1}-{end_page}: {len(df_chunk)} productos [{metodo}]") time.sleep(0.5) except Exception as e: log_mensajes.append(f"⚠️ Páginas {i+1}-{end_page}: Error - {str(e)[:50]}") time.sleep(1) if not all_csv_data: return None, None, "❌ No se pudieron extraer productos del PDF\n\n" + "\n".join(log_mensajes) final_df = pd.concat(all_csv_data, ignore_index=True) final_df['Precio'] = final_df['Precio_Raw'].apply(limpiar_precio) final_df = final_df[['Referencia', 'Descripción', 'Precio']] final_df = final_df.drop_duplicates(subset=['Referencia', 'Descripción']) precios_ok = final_df['Precio'].notna().sum() precios_fail = final_df['Precio'].isna().sum() log_mensajes.append("\n" + "="*50) log_mensajes.append(f"✅ EXTRACCIÓN COMPLETADA") log_mensajes.append(f"📊 Total productos: {len(final_df)}") log_mensajes.append(f"📝 Bloques con texto normal: {metodos['TEXTO']}") log_mensajes.append(f"🔍 Bloques con OCR: {metodos['OCR']}") log_mensajes.append(f"💰 Precios encontrados: {precios_ok}") log_mensajes.append(f"❓ Precios no encontrados: {precios_fail}") with tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx') as f: excel_path = f.name final_df.to_excel(excel_path, index=False, sheet_name="Productos") with tempfile.NamedTemporaryFile(delete=False, suffix='.csv', mode='w', encoding='utf-8-sig') as f: csv_path = f.name final_df.to_csv(csv_path, index=False, sep=";") log_completo = "\n".join(log_mensajes) return excel_path, csv_path, log_completo custom_css = """ .gradio-container { max-width: 900px !important; margin: auto !important; } """ with gr.Blocks(css=custom_css, title="Extractor PDF Productos") as app: gr.Markdown(""" # 📄 Extractor de Productos PDF Extrae **referencias, descripciones y precios** de catálogos de productos en PDF. ### ✨ Características: - ✅ Soporta PDFs digitales y escaneados (OCR automático) - ✅ Reconoce múltiples formatos de precio: `$150.000` | `150,000` | `$1.234.567` - ✅ Exporta a Excel y CSV --- """) with gr.Row(): with gr.Column(scale=1): api_key_input = gr.Textbox( label="🔑 API Key de OpenAI", placeholder="sk-proj-...", type="password", info="Obtén tu key en: platform.openai.com/api-keys" ) pdf_input = gr.File( label="📁 Subir PDF", file_types=[".pdf"], type="filepath" ) btn_procesar = gr.Button("🚀 Extraer Productos", variant="primary", size="lg") with gr.Column(scale=1): gr.Markdown("### 📥 Descargar Resultados") excel_output = gr.File(label="📊 Excel", interactive=False) csv_output = gr.File(label="📄 CSV", interactive=False) log_output = gr.Textbox( label="📋 Registro de Procesamiento", lines=15, max_lines=20, interactive=False ) btn_procesar.click( fn=procesar_pdf, inputs=[pdf_input, api_key_input], outputs=[excel_output, csv_output, log_output], show_progress=True ) gr.Markdown(""" --- ### 📖 Instrucciones: 1. Ingresa tu **API Key de OpenAI** (consíguela en [platform.openai.com](https://platform.openai.com/api-keys)) 2. Sube tu archivo **PDF** (catálogo de productos) 3. Clic en **"Extraer Productos"** 4. Espera el procesamiento 5. Descarga los resultados en **Excel** o **CSV** """) if __name__ == "__main__": app.launch()