Spaces:
Sleeping
Sleeping
| """ | |
| ============================================================= | |
| 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() |