EdwinCM's picture
Create app.py
9f5a950 verified
Raw
History Blame Contribute Delete
11.4 kB
"""
=============================================================
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()