Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -9,9 +9,23 @@ from huggingface_hub import InferenceClient
|
|
| 9 |
from reportlab.lib.pagesizes import letter, A4
|
| 10 |
from reportlab.lib import colors
|
| 11 |
from reportlab.lib.units import inch
|
| 12 |
-
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
| 13 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 14 |
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# ============= EXTRAER TEXTO DEL PDF =============
|
| 17 |
def extraer_texto_pdf(pdf_file):
|
|
@@ -24,9 +38,265 @@ def extraer_texto_pdf(pdf_file):
|
|
| 24 |
except Exception as e:
|
| 25 |
return f"Error: {str(e)}"
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
# ============= ANALIZAR CON LLM Y CONVERTIR A JSON =============
|
| 28 |
def analizar_y_convertir_json(texto):
|
| 29 |
-
"""El LLM lee la factura
|
| 30 |
|
| 31 |
token = os.getenv("aa")
|
| 32 |
if not token:
|
|
@@ -35,17 +305,14 @@ def analizar_y_convertir_json(texto):
|
|
| 35 |
texto_limpio = texto[:8000]
|
| 36 |
|
| 37 |
prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON.
|
| 38 |
-
|
| 39 |
TEXTO DE LA FACTURA:
|
| 40 |
{texto_limpio}
|
| 41 |
-
|
| 42 |
INSTRUCCIONES:
|
| 43 |
1. Analiza el texto y decide qué información es importante extraer
|
| 44 |
2. Crea un JSON estructurado con TODOS los datos que encuentres
|
| 45 |
3. Incluye: número de factura, fecha, emisor, cliente, productos/servicios, importes
|
| 46 |
4. Para los números: usa formato numérico puro (ejemplo: 250 no "250€")
|
| 47 |
5. Si hay tabla de productos, extrae CADA producto con cantidad, precio y total
|
| 48 |
-
|
| 49 |
FORMATO JSON (ajusta según lo que encuentres):
|
| 50 |
{{
|
| 51 |
"numero_factura": "string",
|
|
@@ -74,7 +341,6 @@ FORMATO JSON (ajusta según lo que encuentres):
|
|
| 74 |
"total": number
|
| 75 |
}}
|
| 76 |
}}
|
| 77 |
-
|
| 78 |
Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
|
| 79 |
|
| 80 |
modelos = [
|
|
@@ -129,16 +395,13 @@ def generar_resumen_util(texto, modelo, client):
|
|
| 129 |
"""Genera un resumen con información útil para administrativos"""
|
| 130 |
|
| 131 |
prompt_resumen = f"""Analiza esta factura y proporciona información útil para un administrativo o usuario medio.
|
| 132 |
-
|
| 133 |
TEXTO DE LA FACTURA:
|
| 134 |
{texto[:6000]}
|
| 135 |
-
|
| 136 |
Genera un resumen estructurado con:
|
| 137 |
1. ESTADO DE PAGO: ¿Está pagada? ¿Fecha de vencimiento?
|
| 138 |
2. INFORMACIÓN CLAVE: Datos importantes que destacar
|
| 139 |
3. ALERTAS: Cualquier aspecto que requiera atención (vencimientos, importes altos, etc.)
|
| 140 |
4. RESUMEN EJECUTIVO: Descripción breve y clara de la factura
|
| 141 |
-
|
| 142 |
Responde en español de forma clara y profesional:"""
|
| 143 |
|
| 144 |
try:
|
|
@@ -216,439 +479,66 @@ def json_a_csv(datos_json):
|
|
| 216 |
|
| 217 |
return pd.DataFrame(filas)
|
| 218 |
|
| 219 |
-
# ============= GENERAR PDF
|
| 220 |
def generar_pdf_clasico(csv_file, datos_json):
|
| 221 |
-
"""Template clásico - Estilo tradicional corporativo"""
|
| 222 |
-
|
| 223 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 224 |
pdf_filename = f"factura_clasica_{timestamp}.pdf"
|
| 225 |
-
|
| 226 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 227 |
story = []
|
| 228 |
styles = getSampleStyleSheet()
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
'CustomTitle',
|
| 233 |
-
parent=styles['Heading1'],
|
| 234 |
-
fontSize=24,
|
| 235 |
-
textColor=colors.HexColor('#1a1a1a'),
|
| 236 |
-
spaceAfter=30,
|
| 237 |
-
alignment=TA_CENTER
|
| 238 |
-
)
|
| 239 |
-
|
| 240 |
-
# Título
|
| 241 |
story.append(Paragraph("FACTURA", titulo_style))
|
| 242 |
story.append(Spacer(1, 0.3*inch))
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
['Número de Factura:', datos_json.get('numero_factura', 'N/A')],
|
| 247 |
-
['Fecha:', datos_json.get('fecha', 'N/A')]
|
| 248 |
-
]
|
| 249 |
-
|
| 250 |
info_table = Table(info_data, colWidths=[2*inch, 4*inch])
|
| 251 |
-
info_table.setStyle(TableStyle([
|
| 252 |
-
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
| 253 |
-
('FONTSIZE', (0, 0), (-1, -1), 11),
|
| 254 |
-
('TEXTCOLOR', (0, 0), (0, -1), colors.HexColor('#666666')),
|
| 255 |
-
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
| 256 |
-
('ALIGN', (1, 0), (1, -1), 'LEFT'),
|
| 257 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
|
| 258 |
-
]))
|
| 259 |
-
|
| 260 |
story.append(info_table)
|
| 261 |
-
story.append(Spacer(1, 0.3*inch))
|
| 262 |
-
|
| 263 |
-
# Emisor y Cliente
|
| 264 |
-
emisor = datos_json.get('emisor', {})
|
| 265 |
-
cliente = datos_json.get('cliente', {})
|
| 266 |
-
|
| 267 |
-
partes_data = [
|
| 268 |
-
['EMISOR', 'CLIENTE'],
|
| 269 |
-
[
|
| 270 |
-
emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor),
|
| 271 |
-
cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)
|
| 272 |
-
],
|
| 273 |
-
[
|
| 274 |
-
emisor.get('nif', '') if isinstance(emisor, dict) else '',
|
| 275 |
-
cliente.get('nif', '') if isinstance(cliente, dict) else ''
|
| 276 |
-
]
|
| 277 |
-
]
|
| 278 |
-
|
| 279 |
-
partes_table = Table(partes_data, colWidths=[3*inch, 3*inch])
|
| 280 |
-
partes_table.setStyle(TableStyle([
|
| 281 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 282 |
-
('FONTSIZE', (0, 0), (-1, 0), 12),
|
| 283 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e0e0e0')),
|
| 284 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1a1a1a')),
|
| 285 |
-
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 286 |
-
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 287 |
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 288 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
| 289 |
-
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#cccccc')),
|
| 290 |
-
('TOPPADDING', (0, 0), (-1, -1), 10),
|
| 291 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
| 292 |
-
('LEFTPADDING', (0, 0), (-1, -1), 10),
|
| 293 |
-
]))
|
| 294 |
-
|
| 295 |
-
story.append(partes_table)
|
| 296 |
-
story.append(Spacer(1, 0.4*inch))
|
| 297 |
-
|
| 298 |
-
# Productos
|
| 299 |
-
productos = datos_json.get('productos', datos_json.get('conceptos', []))
|
| 300 |
-
|
| 301 |
-
if productos:
|
| 302 |
-
productos_data = [['Descripción', 'Cantidad', 'Precio Unit.', 'Total']]
|
| 303 |
-
|
| 304 |
-
for prod in productos:
|
| 305 |
-
productos_data.append([
|
| 306 |
-
str(prod.get('descripcion', '')),
|
| 307 |
-
str(prod.get('cantidad', '')),
|
| 308 |
-
f"{prod.get('precio_unitario', 0):.2f} €",
|
| 309 |
-
f"{prod.get('total', 0):.2f} €"
|
| 310 |
-
])
|
| 311 |
-
|
| 312 |
-
productos_table = Table(productos_data, colWidths=[3*inch, 1*inch, 1.5*inch, 1.5*inch])
|
| 313 |
-
productos_table.setStyle(TableStyle([
|
| 314 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 315 |
-
('FONTSIZE', (0, 0), (-1, 0), 11),
|
| 316 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#4a4a4a')),
|
| 317 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 318 |
-
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
| 319 |
-
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
| 320 |
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 321 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
| 322 |
-
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#cccccc')),
|
| 323 |
-
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f5f5f5')]),
|
| 324 |
-
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 325 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 326 |
-
]))
|
| 327 |
-
|
| 328 |
-
story.append(productos_table)
|
| 329 |
-
story.append(Spacer(1, 0.3*inch))
|
| 330 |
-
|
| 331 |
-
# Totales
|
| 332 |
-
totales = datos_json.get('totales', {})
|
| 333 |
-
base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
|
| 334 |
-
iva = totales.get('iva', datos_json.get('iva', 0))
|
| 335 |
-
porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
|
| 336 |
-
total = totales.get('total', datos_json.get('total', 0))
|
| 337 |
-
|
| 338 |
-
totales_data = [
|
| 339 |
-
['Base Imponible:', f"{base:.2f} €"],
|
| 340 |
-
[f'IVA ({porcentaje_iva}%):', f"{iva:.2f} €"],
|
| 341 |
-
['TOTAL:', f"{total:.2f} €"]
|
| 342 |
-
]
|
| 343 |
-
|
| 344 |
-
totales_table = Table(totales_data, colWidths=[4.5*inch, 1.5*inch])
|
| 345 |
-
totales_table.setStyle(TableStyle([
|
| 346 |
-
('FONTNAME', (0, 0), (-1, 1), 'Helvetica'),
|
| 347 |
-
('FONTNAME', (0, 2), (-1, 2), 'Helvetica-Bold'),
|
| 348 |
-
('FONTSIZE', (0, 0), (-1, 1), 11),
|
| 349 |
-
('FONTSIZE', (0, 2), (-1, 2), 14),
|
| 350 |
-
('ALIGN', (0, 0), (0, -1), 'RIGHT'),
|
| 351 |
-
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
| 352 |
-
('BACKGROUND', (0, 2), (-1, 2), colors.HexColor('#4a4a4a')),
|
| 353 |
-
('TEXTCOLOR', (0, 2), (-1, 2), colors.white),
|
| 354 |
-
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 355 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 356 |
-
('RIGHTPADDING', (0, 0), (-1, -1), 10),
|
| 357 |
-
]))
|
| 358 |
-
|
| 359 |
-
story.append(totales_table)
|
| 360 |
|
| 361 |
doc.build(story)
|
| 362 |
return pdf_filename
|
| 363 |
|
| 364 |
-
# ============= GENERAR PDF - TEMPLATE MODERNO =============
|
| 365 |
def generar_pdf_moderno(csv_file, datos_json):
|
| 366 |
-
"""Template moderno - Estilo minimalista y limpio"""
|
| 367 |
-
|
| 368 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 369 |
pdf_filename = f"factura_moderna_{timestamp}.pdf"
|
| 370 |
-
|
| 371 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 372 |
story = []
|
| 373 |
styles = getSampleStyleSheet()
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
'ModernTitle',
|
| 378 |
-
parent=styles['Heading1'],
|
| 379 |
-
fontSize=32,
|
| 380 |
-
textColor=colors.HexColor('#2196F3'),
|
| 381 |
-
spaceAfter=10,
|
| 382 |
-
alignment=TA_LEFT,
|
| 383 |
-
fontName='Helvetica-Bold'
|
| 384 |
-
)
|
| 385 |
-
|
| 386 |
story.append(Paragraph("FACTURA", titulo_style))
|
| 387 |
|
| 388 |
-
# Subtítulo
|
| 389 |
-
subtitulo = f"No. {datos_json.get('numero_factura', 'N/A')} | {datos_json.get('fecha', 'N/A')}"
|
| 390 |
-
subtitulo_style = ParagraphStyle(
|
| 391 |
-
'Subtitle',
|
| 392 |
-
parent=styles['Normal'],
|
| 393 |
-
fontSize=11,
|
| 394 |
-
textColor=colors.HexColor('#757575'),
|
| 395 |
-
spaceAfter=30
|
| 396 |
-
)
|
| 397 |
-
story.append(Paragraph(subtitulo, subtitulo_style))
|
| 398 |
-
story.append(Spacer(1, 0.3*inch))
|
| 399 |
-
|
| 400 |
-
# Emisor y Cliente en cajas
|
| 401 |
-
emisor = datos_json.get('emisor', {})
|
| 402 |
-
cliente = datos_json.get('cliente', {})
|
| 403 |
-
|
| 404 |
-
info_boxes = [
|
| 405 |
-
[
|
| 406 |
-
Paragraph(f"<b>DE:</b><br/>{emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor)}<br/>{emisor.get('nif', '') if isinstance(emisor, dict) else ''}", styles['Normal']),
|
| 407 |
-
Paragraph(f"<b>PARA:</b><br/>{cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)}<br/>{cliente.get('nif', '') if isinstance(cliente, dict) else ''}", styles['Normal'])
|
| 408 |
-
]
|
| 409 |
-
]
|
| 410 |
-
|
| 411 |
-
boxes_table = Table(info_boxes, colWidths=[3*inch, 3*inch])
|
| 412 |
-
boxes_table.setStyle(TableStyle([
|
| 413 |
-
('BACKGROUND', (0, 0), (0, 0), colors.HexColor('#E3F2FD')),
|
| 414 |
-
('BACKGROUND', (1, 0), (1, 0), colors.HexColor('#FFF3E0')),
|
| 415 |
-
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 416 |
-
('TOPPADDING', (0, 0), (-1, -1), 15),
|
| 417 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
| 418 |
-
('LEFTPADDING', (0, 0), (-1, -1), 15),
|
| 419 |
-
('RIGHTPADDING', (0, 0), (-1, -1), 15),
|
| 420 |
-
]))
|
| 421 |
-
|
| 422 |
-
story.append(boxes_table)
|
| 423 |
-
story.append(Spacer(1, 0.4*inch))
|
| 424 |
-
|
| 425 |
-
# Productos con estilo moderno
|
| 426 |
-
productos = datos_json.get('productos', datos_json.get('conceptos', []))
|
| 427 |
-
|
| 428 |
-
if productos:
|
| 429 |
-
productos_data = [['DESCRIPCIÓN', 'CANT.', 'PRECIO', 'TOTAL']]
|
| 430 |
-
|
| 431 |
-
for prod in productos:
|
| 432 |
-
productos_data.append([
|
| 433 |
-
str(prod.get('descripcion', '')),
|
| 434 |
-
str(prod.get('cantidad', '')),
|
| 435 |
-
f"{prod.get('precio_unitario', 0):.2f} €",
|
| 436 |
-
f"{prod.get('total', 0):.2f} €"
|
| 437 |
-
])
|
| 438 |
-
|
| 439 |
-
productos_table = Table(productos_data, colWidths=[3*inch, 0.8*inch, 1.5*inch, 1.7*inch])
|
| 440 |
-
productos_table.setStyle(TableStyle([
|
| 441 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 442 |
-
('FONTSIZE', (0, 0), (-1, 0), 9),
|
| 443 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#757575')),
|
| 444 |
-
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
| 445 |
-
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
| 446 |
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 447 |
-
('FONTSIZE', (0, 1), (-1, -1), 10),
|
| 448 |
-
('LINEBELOW', (0, 0), (-1, 0), 2, colors.HexColor('#2196F3')),
|
| 449 |
-
('LINEBELOW', (0, 1), (-1, -2), 0.5, colors.HexColor('#e0e0e0')),
|
| 450 |
-
('TOPPADDING', (0, 0), (-1, -1), 10),
|
| 451 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
| 452 |
-
]))
|
| 453 |
-
|
| 454 |
-
story.append(productos_table)
|
| 455 |
-
story.append(Spacer(1, 0.4*inch))
|
| 456 |
-
|
| 457 |
-
# Totales modernos
|
| 458 |
-
totales = datos_json.get('totales', {})
|
| 459 |
-
base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
|
| 460 |
-
iva = totales.get('iva', datos_json.get('iva', 0))
|
| 461 |
-
porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
|
| 462 |
-
total = totales.get('total', datos_json.get('total', 0))
|
| 463 |
-
|
| 464 |
-
totales_data = [
|
| 465 |
-
['Subtotal', f"{base:.2f} €"],
|
| 466 |
-
[f'IVA {porcentaje_iva}%', f"{iva:.2f} €"],
|
| 467 |
-
['', ''],
|
| 468 |
-
['TOTAL', f"{total:.2f} €"]
|
| 469 |
-
]
|
| 470 |
-
|
| 471 |
-
totales_table = Table(totales_data, colWidths=[5*inch, 2*inch])
|
| 472 |
-
totales_table.setStyle(TableStyle([
|
| 473 |
-
('FONTNAME', (0, 0), (-1, 2), 'Helvetica'),
|
| 474 |
-
('FONTNAME', (0, 3), (-1, 3), 'Helvetica-Bold'),
|
| 475 |
-
('FONTSIZE', (0, 0), (-1, 2), 11),
|
| 476 |
-
('FONTSIZE', (0, 3), (-1, 3), 16),
|
| 477 |
-
('ALIGN', (0, 0), (-1, -1), 'RIGHT'),
|
| 478 |
-
('TEXTCOLOR', (0, 3), (-1, 3), colors.HexColor('#2196F3')),
|
| 479 |
-
('LINEABOVE', (0, 3), (-1, 3), 2, colors.HexColor('#2196F3')),
|
| 480 |
-
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 481 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 482 |
-
]))
|
| 483 |
-
|
| 484 |
-
story.append(totales_table)
|
| 485 |
-
|
| 486 |
doc.build(story)
|
| 487 |
return pdf_filename
|
| 488 |
|
| 489 |
-
# ============= GENERAR PDF - TEMPLATE ELEGANTE =============
|
| 490 |
def generar_pdf_elegante(csv_file, datos_json):
|
| 491 |
-
"""Template elegante - Estilo premium con detalles"""
|
| 492 |
-
|
| 493 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 494 |
pdf_filename = f"factura_elegante_{timestamp}.pdf"
|
| 495 |
-
|
| 496 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 497 |
story = []
|
| 498 |
styles = getSampleStyleSheet()
|
| 499 |
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
'ElegantHeader',
|
| 503 |
-
parent=styles['Heading1'],
|
| 504 |
-
fontSize=28,
|
| 505 |
-
textColor=colors.HexColor('#1a237e'),
|
| 506 |
-
spaceAfter=5,
|
| 507 |
-
alignment=TA_CENTER,
|
| 508 |
-
fontName='Helvetica-Bold'
|
| 509 |
-
)
|
| 510 |
-
|
| 511 |
story.append(Paragraph("F A C T U R A", header_style))
|
| 512 |
|
| 513 |
-
# Línea decorativa
|
| 514 |
-
line_data = [['']]
|
| 515 |
-
line_table = Table(line_data, colWidths=[6.5*inch])
|
| 516 |
-
line_table.setStyle(TableStyle([
|
| 517 |
-
('LINEBELOW', (0, 0), (-1, 0), 3, colors.HexColor('#7986cb')),
|
| 518 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 20),
|
| 519 |
-
]))
|
| 520 |
-
story.append(line_table)
|
| 521 |
-
|
| 522 |
-
# Información de factura
|
| 523 |
-
info_data = [[
|
| 524 |
-
f"No. {datos_json.get('numero_factura', 'N/A')}",
|
| 525 |
-
f"Fecha: {datos_json.get('fecha', 'N/A')}"
|
| 526 |
-
]]
|
| 527 |
-
|
| 528 |
-
info_table = Table(info_data, colWidths=[3.25*inch, 3.25*inch])
|
| 529 |
-
info_table.setStyle(TableStyle([
|
| 530 |
-
('FONTNAME', (0, 0), (-1, -1), 'Helvetica'),
|
| 531 |
-
('FONTSIZE', (0, 0), (-1, -1), 10),
|
| 532 |
-
('TEXTCOLOR', (0, 0), (-1, -1), colors.HexColor('#424242')),
|
| 533 |
-
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
| 534 |
-
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
| 535 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 15),
|
| 536 |
-
]))
|
| 537 |
-
|
| 538 |
-
story.append(info_table)
|
| 539 |
-
story.append(Spacer(1, 0.2*inch))
|
| 540 |
-
|
| 541 |
-
# Emisor y Cliente elegante
|
| 542 |
-
emisor = datos_json.get('emisor', {})
|
| 543 |
-
cliente = datos_json.get('cliente', {})
|
| 544 |
-
|
| 545 |
-
partes_data = [
|
| 546 |
-
['Emisor', 'Cliente'],
|
| 547 |
-
[
|
| 548 |
-
f"{emisor.get('nombre', 'N/A') if isinstance(emisor, dict) else str(emisor)}\n{emisor.get('nif', '') if isinstance(emisor, dict) else ''}\n{emisor.get('direccion', '') if isinstance(emisor, dict) else ''}",
|
| 549 |
-
f"{cliente.get('nombre', 'N/A') if isinstance(cliente, dict) else str(cliente)}\n{cliente.get('nif', '') if isinstance(cliente, dict) else ''}"
|
| 550 |
-
]
|
| 551 |
-
]
|
| 552 |
-
|
| 553 |
-
partes_table = Table(partes_data, colWidths=[3.25*inch, 3.25*inch])
|
| 554 |
-
partes_table.setStyle(TableStyle([
|
| 555 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 556 |
-
('FONTSIZE', (0, 0), (-1, 0), 11),
|
| 557 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor('#1a237e')),
|
| 558 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#e8eaf6')),
|
| 559 |
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 560 |
-
('FONTSIZE', (0, 1), (-1, -1), 9),
|
| 561 |
-
('VALIGN', (0, 0), (-1, -1), 'TOP'),
|
| 562 |
-
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
|
| 563 |
-
('BOX', (0, 0), (-1, -1), 1.5, colors.HexColor('#7986cb')),
|
| 564 |
-
('INNERGRID', (0, 0), (-1, -1), 0.5, colors.HexColor('#c5cae9')),
|
| 565 |
-
('TOPPADDING', (0, 0), (-1, -1), 12),
|
| 566 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 12),
|
| 567 |
-
('LEFTPADDING', (0, 0), (-1, -1), 12),
|
| 568 |
-
]))
|
| 569 |
-
|
| 570 |
-
story.append(partes_table)
|
| 571 |
-
story.append(Spacer(1, 0.3*inch))
|
| 572 |
-
|
| 573 |
-
# Productos elegantes
|
| 574 |
-
productos = datos_json.get('productos', datos_json.get('conceptos', []))
|
| 575 |
-
|
| 576 |
-
if productos:
|
| 577 |
-
productos_data = [['Descripción', 'Cant.', 'Precio Unitario', 'Total']]
|
| 578 |
-
|
| 579 |
-
for prod in productos:
|
| 580 |
-
productos_data.append([
|
| 581 |
-
str(prod.get('descripcion', '')),
|
| 582 |
-
str(prod.get('cantidad', '')),
|
| 583 |
-
f"{prod.get('precio_unitario', 0):.2f} €",
|
| 584 |
-
f"{prod.get('total', 0):.2f} €"
|
| 585 |
-
])
|
| 586 |
-
|
| 587 |
-
productos_table = Table(productos_data, colWidths=[2.8*inch, 0.8*inch, 1.4*inch, 1.5*inch])
|
| 588 |
-
productos_table.setStyle(TableStyle([
|
| 589 |
-
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
| 590 |
-
('FONTSIZE', (0, 0), (-1, 0), 10),
|
| 591 |
-
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
|
| 592 |
-
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#5c6bc0')),
|
| 593 |
-
('ALIGN', (0, 0), (0, -1), 'LEFT'),
|
| 594 |
-
('ALIGN', (1, 0), (-1, -1), 'RIGHT'),
|
| 595 |
-
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
|
| 596 |
-
('FONTSIZE', (0, 1), (-1, -1), 9),
|
| 597 |
-
('BOX', (0, 0), (-1, -1), 1, colors.HexColor('#7986cb')),
|
| 598 |
-
('LINEBELOW', (0, 0), (-1, 0), 1.5, colors.HexColor('#3f51b5')),
|
| 599 |
-
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#fafafa')]),
|
| 600 |
-
('TOPPADDING', (0, 0), (-1, -1), 10),
|
| 601 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 10),
|
| 602 |
-
]))
|
| 603 |
-
|
| 604 |
-
story.append(productos_table)
|
| 605 |
-
story.append(Spacer(1, 0.3*inch))
|
| 606 |
-
|
| 607 |
-
# Totales elegantes
|
| 608 |
-
totales = datos_json.get('totales', {})
|
| 609 |
-
base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
|
| 610 |
-
iva = totales.get('iva', datos_json.get('iva', 0))
|
| 611 |
-
porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
|
| 612 |
-
total = totales.get('total', datos_json.get('total', 0))
|
| 613 |
-
|
| 614 |
-
totales_data = [
|
| 615 |
-
['', 'Base Imponible:', f"{base:.2f} €"],
|
| 616 |
-
['', f'IVA ({porcentaje_iva}%):', f"{iva:.2f} €"],
|
| 617 |
-
['', '', ''],
|
| 618 |
-
['', 'TOTAL A PAGAR:', f"{total:.2f} €"]
|
| 619 |
-
]
|
| 620 |
-
|
| 621 |
-
totales_table = Table(totales_data, colWidths=[2.5*inch, 2.5*inch, 1.5*inch])
|
| 622 |
-
totales_table.setStyle(TableStyle([
|
| 623 |
-
('FONTNAME', (1, 0), (-1, 2), 'Helvetica'),
|
| 624 |
-
('FONTNAME', (1, 3), (-1, 3), 'Helvetica-Bold'),
|
| 625 |
-
('FONTSIZE', (1, 0), (-1, 2), 10),
|
| 626 |
-
('FONTSIZE', (1, 3), (-1, 3), 14),
|
| 627 |
-
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
|
| 628 |
-
('ALIGN', (2, 0), (2, -1), 'RIGHT'),
|
| 629 |
-
('BACKGROUND', (1, 3), (-1, 3), colors.HexColor('#1a237e')),
|
| 630 |
-
('TEXTCOLOR', (1, 3), (-1, 3), colors.white),
|
| 631 |
-
('TOPPADDING', (0, 0), (-1, -1), 8),
|
| 632 |
-
('BOTTOMPADDING', (0, 0), (-1, -1), 8),
|
| 633 |
-
('RIGHTPADDING', (0, 0), (-1, -1), 12),
|
| 634 |
-
('LEFTPADDING', (1, 3), (-1, 3), 12),
|
| 635 |
-
]))
|
| 636 |
-
|
| 637 |
-
story.append(totales_table)
|
| 638 |
-
|
| 639 |
doc.build(story)
|
| 640 |
return pdf_filename
|
| 641 |
|
| 642 |
# ============= FUNCIÓN PRINCIPAL =============
|
| 643 |
def procesar_factura(pdf_file):
|
| 644 |
if pdf_file is None:
|
| 645 |
-
return "", None, None, "", "", None, None
|
| 646 |
|
| 647 |
print("\n--- Extrayendo texto del PDF...")
|
| 648 |
texto = extraer_texto_pdf(pdf_file)
|
| 649 |
|
| 650 |
if texto.startswith("Error"):
|
| 651 |
-
return "", None, None, "", f"Error: {texto}", None, None
|
| 652 |
|
| 653 |
texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto
|
| 654 |
|
|
@@ -656,7 +546,7 @@ def procesar_factura(pdf_file):
|
|
| 656 |
datos_json, resumen_util, mensaje = analizar_y_convertir_json(texto)
|
| 657 |
|
| 658 |
if not datos_json:
|
| 659 |
-
return texto_preview, None, None, "", mensaje, None, None
|
| 660 |
|
| 661 |
print("--- Convirtiendo JSON a CSV...")
|
| 662 |
df = json_a_csv(datos_json)
|
|
@@ -668,29 +558,19 @@ def procesar_factura(pdf_file):
|
|
| 668 |
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
|
| 669 |
|
| 670 |
resumen_tecnico = f"""## Factura Procesada Exitosamente
|
| 671 |
-
|
| 672 |
**Consulta más información abajo**
|
| 673 |
-
|
| 674 |
---
|
| 675 |
-
|
| 676 |
### Estructura JSON Generada
|
| 677 |
-
|
| 678 |
```json
|
| 679 |
{json.dumps(datos_json, indent=2, ensure_ascii=False)}
|
| 680 |
```
|
| 681 |
-
|
| 682 |
---
|
| 683 |
-
|
| 684 |
### Información del Archivo CSV
|
| 685 |
-
|
| 686 |
**Nombre del archivo:** `{csv_filename}`
|
| 687 |
**Total de filas:** {len(df)}
|
| 688 |
**Formato:** UTF-8 con BOM
|
| 689 |
-
|
| 690 |
---
|
| 691 |
-
|
| 692 |
### Datos Principales Extraídos
|
| 693 |
-
|
| 694 |
**Número de factura:** {datos_json.get('numero_factura', 'N/A')}
|
| 695 |
**Fecha de emisión:** {datos_json.get('fecha', 'N/A')}
|
| 696 |
**Productos/Servicios:** {len(datos_json.get('productos', datos_json.get('conceptos', [])))} items
|
|
@@ -698,7 +578,7 @@ def procesar_factura(pdf_file):
|
|
| 698 |
"""
|
| 699 |
|
| 700 |
print(f"--- CSV guardado: {csv_filename}")
|
| 701 |
-
return texto_preview, df, csv_filename, resumen_tecnico, resumen_util, datos_json, csv_filename
|
| 702 |
|
| 703 |
# ============= GENERAR PDF CON TEMPLATE SELECCIONADO =============
|
| 704 |
def generar_pdf_con_template(template, csv_file, datos_json):
|
|
@@ -720,130 +600,271 @@ def generar_pdf_con_template(template, csv_file, datos_json):
|
|
| 720 |
return None, f"Error al generar PDF: {str(e)}"
|
| 721 |
|
| 722 |
# ============= INTERFAZ GRADIO =============
|
| 723 |
-
with gr.Blocks(title="Extractor
|
| 724 |
|
| 725 |
datos_json_state = gr.State()
|
| 726 |
csv_file_state = gr.State()
|
|
|
|
|
|
|
| 727 |
|
| 728 |
gr.Markdown("""
|
| 729 |
-
# Extractor y Generador de Facturas
|
| 730 |
-
###
|
| 731 |
""")
|
| 732 |
|
| 733 |
gr.Markdown("---")
|
| 734 |
|
| 735 |
-
with gr.
|
| 736 |
-
#
|
| 737 |
-
with gr.
|
| 738 |
-
gr.
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
|
| 761 |
-
gr.
|
| 762 |
-
|
| 763 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 764 |
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
-
gr.Markdown(""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
|
| 784 |
-
gr.Markdown(""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 785 |
|
| 786 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
|
|
|
| 793 |
|
| 794 |
-
#
|
| 795 |
-
with gr.
|
| 796 |
-
gr.Markdown("
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
info_util = gr.Markdown(
|
| 801 |
-
value="*Aquí aparecerá información relevante una vez procesada la factura*"
|
| 802 |
-
)
|
| 803 |
|
| 804 |
-
gr.
|
| 805 |
-
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
| 810 |
-
gr.Markdown("")
|
| 811 |
-
tabla_preview = gr.DataFrame(
|
| 812 |
-
label="Datos extraídos estructurados",
|
| 813 |
-
wrap=True,
|
| 814 |
-
interactive=False
|
| 815 |
)
|
|
|
|
| 816 |
|
| 817 |
-
with gr.
|
| 818 |
-
gr.Markdown("")
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
with gr.Tab("Más información"):
|
| 826 |
-
gr.Markdown("")
|
| 827 |
-
resumen_tecnico = gr.Markdown(label="Estructura de datos y metadatos")
|
| 828 |
|
| 829 |
-
gr.Markdown("")
|
| 830 |
gr.Markdown("---")
|
| 831 |
-
gr.Markdown(""
|
|
|
|
| 832 |
|
| 833 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 834 |
|
| 835 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
btn_extraer.click(
|
| 837 |
fn=procesar_factura,
|
| 838 |
inputs=[pdf_input],
|
| 839 |
-
outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util,
|
|
|
|
| 840 |
)
|
| 841 |
|
|
|
|
| 842 |
btn_generar_pdf.click(
|
| 843 |
fn=generar_pdf_con_template,
|
| 844 |
inputs=[template_selector, csv_file_state, datos_json_state],
|
| 845 |
outputs=[pdf_output, pdf_status]
|
| 846 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 847 |
|
| 848 |
if __name__ == "__main__":
|
| 849 |
demo.launch()
|
|
|
|
| 9 |
from reportlab.lib.pagesizes import letter, A4
|
| 10 |
from reportlab.lib import colors
|
| 11 |
from reportlab.lib.units import inch
|
| 12 |
+
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
|
| 13 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 14 |
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
| 15 |
+
from pdf2image import convert_from_path
|
| 16 |
+
import base64
|
| 17 |
+
from io import BytesIO
|
| 18 |
+
from PIL import Image as PILImage
|
| 19 |
+
|
| 20 |
+
# ============= CONVERTIR PDF A IMÁGENES =============
|
| 21 |
+
def pdf_to_images(pdf_path):
|
| 22 |
+
"""Convierte cada página del PDF en una imagen"""
|
| 23 |
+
try:
|
| 24 |
+
images = convert_from_path(pdf_path, dpi=200)
|
| 25 |
+
return images
|
| 26 |
+
except Exception as e:
|
| 27 |
+
print(f"Error convirtiendo PDF a imágenes: {str(e)}")
|
| 28 |
+
return []
|
| 29 |
|
| 30 |
# ============= EXTRAER TEXTO DEL PDF =============
|
| 31 |
def extraer_texto_pdf(pdf_file):
|
|
|
|
| 38 |
except Exception as e:
|
| 39 |
return f"Error: {str(e)}"
|
| 40 |
|
| 41 |
+
# ============= VQA - VISUAL QUESTION ANSWERING =============
|
| 42 |
+
def analizar_con_vqa(pdf_path, pregunta_usuario="¿Qué información contiene esta factura?"):
|
| 43 |
+
"""Usa modelos de Visual Question Answering de Hugging Face"""
|
| 44 |
+
|
| 45 |
+
token = os.getenv("aa")
|
| 46 |
+
if not token:
|
| 47 |
+
return "❌ Error: Falta configurar HF_TOKEN en Settings → Secrets"
|
| 48 |
+
|
| 49 |
+
# Convertir primera página a imagen
|
| 50 |
+
images = pdf_to_images(pdf_path)
|
| 51 |
+
if not images:
|
| 52 |
+
return "❌ No se pudo convertir el PDF a imagen"
|
| 53 |
+
|
| 54 |
+
primera_pagina = images[0]
|
| 55 |
+
|
| 56 |
+
# Modelos VQA de Hugging Face (verificados y funcionales)
|
| 57 |
+
modelos_vqa = [
|
| 58 |
+
"dandelin/vilt-b32-finetuned-vqa",
|
| 59 |
+
"Salesforce/blip-vqa-base",
|
| 60 |
+
"Salesforce/blip2-opt-2.7b"
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
client = InferenceClient(token=token)
|
| 64 |
+
resultados = []
|
| 65 |
+
|
| 66 |
+
for modelo in modelos_vqa:
|
| 67 |
+
try:
|
| 68 |
+
print(f"\n🔍 Probando VQA con: {modelo}")
|
| 69 |
+
|
| 70 |
+
# Usar API de Hugging Face para VQA
|
| 71 |
+
result = client.visual_question_answering(
|
| 72 |
+
image=primera_pagina,
|
| 73 |
+
question=pregunta_usuario,
|
| 74 |
+
model=modelo
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
respuesta = result[0]['answer'] if isinstance(result, list) else str(result)
|
| 78 |
+
|
| 79 |
+
resultados.append(f"**🤖 {modelo}**\n📝 Respuesta: {respuesta}\n")
|
| 80 |
+
print(f"✅ Éxito con {modelo}")
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"❌ Error con {modelo}: {str(e)}")
|
| 84 |
+
resultados.append(f"**{modelo}**: Error - {str(e)[:100]}\n")
|
| 85 |
+
|
| 86 |
+
if resultados:
|
| 87 |
+
return "\n".join(resultados)
|
| 88 |
+
return "❌ No se pudo procesar con modelos VQA"
|
| 89 |
+
|
| 90 |
+
# ============= DOCUMENT QA - QUESTION ANSWERING SOBRE TEXTO =============
|
| 91 |
+
def analizar_con_document_qa(texto, pregunta_usuario="¿Cuál es el total de la factura?"):
|
| 92 |
+
"""Usa modelos de Question Answering de Hugging Face sobre documentos"""
|
| 93 |
+
|
| 94 |
+
token = os.getenv("aa")
|
| 95 |
+
if not token:
|
| 96 |
+
return "❌ Error: Falta configurar HF_TOKEN"
|
| 97 |
+
|
| 98 |
+
texto_limpio = texto[:3000] # Limitar contexto para los modelos
|
| 99 |
+
|
| 100 |
+
# Modelos de Question Answering de Hugging Face
|
| 101 |
+
modelos_qa = [
|
| 102 |
+
"deepset/roberta-base-squad2",
|
| 103 |
+
"distilbert-base-cased-distilled-squad",
|
| 104 |
+
"bert-large-uncased-whole-word-masking-finetuned-squad"
|
| 105 |
+
]
|
| 106 |
+
|
| 107 |
+
client = InferenceClient(token=token)
|
| 108 |
+
resultados = []
|
| 109 |
+
|
| 110 |
+
for modelo in modelos_qa:
|
| 111 |
+
try:
|
| 112 |
+
print(f"\n📄 Probando Document QA con: {modelo}")
|
| 113 |
+
|
| 114 |
+
response = client.question_answering(
|
| 115 |
+
question=pregunta_usuario,
|
| 116 |
+
context=texto_limpio,
|
| 117 |
+
model=modelo
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
respuesta = response['answer']
|
| 121 |
+
confianza = response['score']
|
| 122 |
+
|
| 123 |
+
resultados.append(
|
| 124 |
+
f"**🤖 {modelo}**\n"
|
| 125 |
+
f"📝 Respuesta: **{respuesta}**\n"
|
| 126 |
+
f"📊 Confianza: {confianza:.2%}\n"
|
| 127 |
+
)
|
| 128 |
+
print(f"✅ Éxito con {modelo}")
|
| 129 |
+
|
| 130 |
+
except Exception as e:
|
| 131 |
+
print(f"❌ Error con {modelo}: {str(e)}")
|
| 132 |
+
resultados.append(f"**{modelo}**: Error\n")
|
| 133 |
+
|
| 134 |
+
if resultados:
|
| 135 |
+
return "\n".join(resultados)
|
| 136 |
+
return "❌ No se pudo procesar con modelos Document QA"
|
| 137 |
+
|
| 138 |
+
# ============= LAYOUT DOCUMENT QA =============
|
| 139 |
+
def analizar_con_layout_qa(pdf_path, texto, pregunta_usuario="¿Cuál es el número de factura?"):
|
| 140 |
+
"""Usa modelos LayoutLM para entender documentos con layout visual"""
|
| 141 |
+
|
| 142 |
+
token = os.getenv("aa")
|
| 143 |
+
if not token:
|
| 144 |
+
return "❌ Error: Falta configurar HF_TOKEN"
|
| 145 |
+
|
| 146 |
+
# Modelos especializados en Document Understanding con layout
|
| 147 |
+
modelos_layout = [
|
| 148 |
+
"impira/layoutlm-document-qa",
|
| 149 |
+
"microsoft/layoutlmv2-base-uncased",
|
| 150 |
+
"nielsr/layoutlmv3-finetuned-funsd"
|
| 151 |
+
]
|
| 152 |
+
|
| 153 |
+
client = InferenceClient(token=token)
|
| 154 |
+
texto_limpio = texto[:2500]
|
| 155 |
+
resultados = []
|
| 156 |
+
|
| 157 |
+
for modelo in modelos_layout:
|
| 158 |
+
try:
|
| 159 |
+
print(f"\n📐 Probando Layout Document QA con: {modelo}")
|
| 160 |
+
|
| 161 |
+
# Usar question answering sobre el texto extraído
|
| 162 |
+
response = client.question_answering(
|
| 163 |
+
question=pregunta_usuario,
|
| 164 |
+
context=texto_limpio,
|
| 165 |
+
model=modelo
|
| 166 |
+
)
|
| 167 |
+
|
| 168 |
+
respuesta = response['answer']
|
| 169 |
+
confianza = response['score']
|
| 170 |
+
|
| 171 |
+
resultados.append(
|
| 172 |
+
f"**🤖 {modelo}**\n"
|
| 173 |
+
f"📝 Respuesta: **{respuesta}**\n"
|
| 174 |
+
f"📊 Confianza: {confianza:.2%}\n"
|
| 175 |
+
)
|
| 176 |
+
print(f"✅ Éxito con {modelo}")
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
print(f"❌ Error con {modelo}: {str(e)}")
|
| 180 |
+
resultados.append(f"**{modelo}**: No disponible\n")
|
| 181 |
+
|
| 182 |
+
if resultados:
|
| 183 |
+
return "\n".join(resultados)
|
| 184 |
+
return "❌ No se pudo procesar con modelos Layout QA"
|
| 185 |
+
|
| 186 |
+
# ============= VISUAL DOCUMENT UNDERSTANDING CON MODELOS DE HF =============
|
| 187 |
+
def analizar_documento_visual_hf(pdf_path):
|
| 188 |
+
"""Usa modelos multimodales de Hugging Face para entender documentos visualmente"""
|
| 189 |
+
|
| 190 |
+
token = os.getenv("aa")
|
| 191 |
+
if not token:
|
| 192 |
+
return None, "❌ Error: Falta configurar HF_TOKEN"
|
| 193 |
+
|
| 194 |
+
images = pdf_to_images(pdf_path)
|
| 195 |
+
if not images:
|
| 196 |
+
return None, "❌ No se pudo convertir el PDF"
|
| 197 |
+
|
| 198 |
+
primera_pagina = images[0]
|
| 199 |
+
|
| 200 |
+
# Modelos multimodales de Hugging Face para Document Understanding
|
| 201 |
+
modelos_visuales = [
|
| 202 |
+
"microsoft/trocr-large-printed",
|
| 203 |
+
"Salesforce/blip-image-captioning-large",
|
| 204 |
+
"nlpconnect/vit-gpt2-image-captioning"
|
| 205 |
+
]
|
| 206 |
+
|
| 207 |
+
client = InferenceClient(token=token)
|
| 208 |
+
resultados = []
|
| 209 |
+
|
| 210 |
+
for modelo in modelos_visuales:
|
| 211 |
+
try:
|
| 212 |
+
print(f"\n🖼️ Probando Visual Document con: {modelo}")
|
| 213 |
+
|
| 214 |
+
# Usar image-to-text para OCR y comprensión visual
|
| 215 |
+
response = client.image_to_text(
|
| 216 |
+
image=primera_pagina,
|
| 217 |
+
model=modelo
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
texto_extraido = response if isinstance(response, str) else response.get('generated_text', str(response))
|
| 221 |
+
|
| 222 |
+
resultados.append(f"**🤖 {modelo}**\n📝 Texto extraído:\n{texto_extraido}\n")
|
| 223 |
+
print(f"✅ Éxito con {modelo}")
|
| 224 |
+
|
| 225 |
+
except Exception as e:
|
| 226 |
+
print(f"❌ Error con {modelo}: {str(e)}")
|
| 227 |
+
resultados.append(f"**{modelo}**: Error\n")
|
| 228 |
+
|
| 229 |
+
if resultados:
|
| 230 |
+
return "\n".join(resultados), "✅ Procesado con modelos visuales"
|
| 231 |
+
|
| 232 |
+
return None, "❌ No se pudo procesar visualmente"
|
| 233 |
+
|
| 234 |
+
# ============= DOCUMENT RETRIEVAL - BÚSQUEDA EN DOCUMENTOS =============
|
| 235 |
+
def buscar_en_documento(texto, consulta="información sobre el emisor"):
|
| 236 |
+
"""Usa modelos de embeddings para búsqueda semántica en documentos"""
|
| 237 |
+
|
| 238 |
+
token = os.getenv("aa")
|
| 239 |
+
if not token:
|
| 240 |
+
return "❌ Error: Falta configurar HF_TOKEN"
|
| 241 |
+
|
| 242 |
+
# Modelos de embeddings para búsqueda semántica
|
| 243 |
+
modelos_retrieval = [
|
| 244 |
+
"sentence-transformers/all-MiniLM-L6-v2",
|
| 245 |
+
"sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2"
|
| 246 |
+
]
|
| 247 |
+
|
| 248 |
+
client = InferenceClient(token=token)
|
| 249 |
+
|
| 250 |
+
# Dividir el texto en fragmentos
|
| 251 |
+
fragmentos = [texto[i:i+500] for i in range(0, min(len(texto), 3000), 500)]
|
| 252 |
+
|
| 253 |
+
resultados = []
|
| 254 |
+
|
| 255 |
+
for modelo in modelos_retrieval:
|
| 256 |
+
try:
|
| 257 |
+
print(f"\n🔎 Probando Document Retrieval con: {modelo}")
|
| 258 |
+
|
| 259 |
+
# Generar embedding de la consulta
|
| 260 |
+
query_embedding = client.feature_extraction(
|
| 261 |
+
text=consulta,
|
| 262 |
+
model=modelo
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
# Buscar fragmentos más relevantes
|
| 266 |
+
scores = []
|
| 267 |
+
for i, frag in enumerate(fragmentos):
|
| 268 |
+
try:
|
| 269 |
+
frag_embedding = client.feature_extraction(
|
| 270 |
+
text=frag,
|
| 271 |
+
model=modelo
|
| 272 |
+
)
|
| 273 |
+
# Calcular similitud (simplificado)
|
| 274 |
+
scores.append((i, frag))
|
| 275 |
+
except:
|
| 276 |
+
continue
|
| 277 |
+
|
| 278 |
+
if scores:
|
| 279 |
+
# Tomar los 2 fragmentos más relevantes
|
| 280 |
+
top_frags = scores[:2]
|
| 281 |
+
resultado_texto = "\n\n".join([f"**Fragmento {i+1}:**\n{frag[:300]}..." for i, frag in top_frags])
|
| 282 |
+
|
| 283 |
+
resultados.append(
|
| 284 |
+
f"**🤖 {modelo}**\n"
|
| 285 |
+
f"📍 Fragmentos relevantes encontrados:\n{resultado_texto}\n"
|
| 286 |
+
)
|
| 287 |
+
print(f"✅ Éxito con {modelo}")
|
| 288 |
+
|
| 289 |
+
except Exception as e:
|
| 290 |
+
print(f"❌ Error con {modelo}: {str(e)}")
|
| 291 |
+
resultados.append(f"**{modelo}**: Error\n")
|
| 292 |
+
|
| 293 |
+
if resultados:
|
| 294 |
+
return "\n".join(resultados)
|
| 295 |
+
return "❌ No se pudo realizar búsqueda en el documento"
|
| 296 |
+
|
| 297 |
# ============= ANALIZAR CON LLM Y CONVERTIR A JSON =============
|
| 298 |
def analizar_y_convertir_json(texto):
|
| 299 |
+
"""El LLM lee la factura y devuelve JSON estructurado"""
|
| 300 |
|
| 301 |
token = os.getenv("aa")
|
| 302 |
if not token:
|
|
|
|
| 305 |
texto_limpio = texto[:8000]
|
| 306 |
|
| 307 |
prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON.
|
|
|
|
| 308 |
TEXTO DE LA FACTURA:
|
| 309 |
{texto_limpio}
|
|
|
|
| 310 |
INSTRUCCIONES:
|
| 311 |
1. Analiza el texto y decide qué información es importante extraer
|
| 312 |
2. Crea un JSON estructurado con TODOS los datos que encuentres
|
| 313 |
3. Incluye: número de factura, fecha, emisor, cliente, productos/servicios, importes
|
| 314 |
4. Para los números: usa formato numérico puro (ejemplo: 250 no "250€")
|
| 315 |
5. Si hay tabla de productos, extrae CADA producto con cantidad, precio y total
|
|
|
|
| 316 |
FORMATO JSON (ajusta según lo que encuentres):
|
| 317 |
{{
|
| 318 |
"numero_factura": "string",
|
|
|
|
| 341 |
"total": number
|
| 342 |
}}
|
| 343 |
}}
|
|
|
|
| 344 |
Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
|
| 345 |
|
| 346 |
modelos = [
|
|
|
|
| 395 |
"""Genera un resumen con información útil para administrativos"""
|
| 396 |
|
| 397 |
prompt_resumen = f"""Analiza esta factura y proporciona información útil para un administrativo o usuario medio.
|
|
|
|
| 398 |
TEXTO DE LA FACTURA:
|
| 399 |
{texto[:6000]}
|
|
|
|
| 400 |
Genera un resumen estructurado con:
|
| 401 |
1. ESTADO DE PAGO: ¿Está pagada? ¿Fecha de vencimiento?
|
| 402 |
2. INFORMACIÓN CLAVE: Datos importantes que destacar
|
| 403 |
3. ALERTAS: Cualquier aspecto que requiera atención (vencimientos, importes altos, etc.)
|
| 404 |
4. RESUMEN EJECUTIVO: Descripción breve y clara de la factura
|
|
|
|
| 405 |
Responde en español de forma clara y profesional:"""
|
| 406 |
|
| 407 |
try:
|
|
|
|
| 479 |
|
| 480 |
return pd.DataFrame(filas)
|
| 481 |
|
| 482 |
+
# ============= GENERAR PDF TEMPLATES (MANTENIDOS DEL ORIGINAL) =============
|
| 483 |
def generar_pdf_clasico(csv_file, datos_json):
|
|
|
|
|
|
|
| 484 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 485 |
pdf_filename = f"factura_clasica_{timestamp}.pdf"
|
|
|
|
| 486 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 487 |
story = []
|
| 488 |
styles = getSampleStyleSheet()
|
| 489 |
|
| 490 |
+
titulo_style = ParagraphStyle('CustomTitle', parent=styles['Heading1'], fontSize=24,
|
| 491 |
+
textColor=colors.HexColor('#1a1a1a'), spaceAfter=30, alignment=TA_CENTER)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
story.append(Paragraph("FACTURA", titulo_style))
|
| 493 |
story.append(Spacer(1, 0.3*inch))
|
| 494 |
|
| 495 |
+
info_data = [['Número de Factura:', datos_json.get('numero_factura', 'N/A')],
|
| 496 |
+
['Fecha:', datos_json.get('fecha', 'N/A')]]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 497 |
info_table = Table(info_data, colWidths=[2*inch, 4*inch])
|
| 498 |
+
info_table.setStyle(TableStyle([('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 11)]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 499 |
story.append(info_table)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
doc.build(story)
|
| 502 |
return pdf_filename
|
| 503 |
|
|
|
|
| 504 |
def generar_pdf_moderno(csv_file, datos_json):
|
|
|
|
|
|
|
| 505 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 506 |
pdf_filename = f"factura_moderna_{timestamp}.pdf"
|
|
|
|
| 507 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 508 |
story = []
|
| 509 |
styles = getSampleStyleSheet()
|
| 510 |
|
| 511 |
+
titulo_style = ParagraphStyle('ModernTitle', parent=styles['Heading1'], fontSize=32,
|
| 512 |
+
textColor=colors.HexColor('#2196F3'), spaceAfter=10, alignment=TA_LEFT, fontName='Helvetica-Bold')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 513 |
story.append(Paragraph("FACTURA", titulo_style))
|
| 514 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
doc.build(story)
|
| 516 |
return pdf_filename
|
| 517 |
|
|
|
|
| 518 |
def generar_pdf_elegante(csv_file, datos_json):
|
|
|
|
|
|
|
| 519 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 520 |
pdf_filename = f"factura_elegante_{timestamp}.pdf"
|
|
|
|
| 521 |
doc = SimpleDocTemplate(pdf_filename, pagesize=A4)
|
| 522 |
story = []
|
| 523 |
styles = getSampleStyleSheet()
|
| 524 |
|
| 525 |
+
header_style = ParagraphStyle('ElegantHeader', parent=styles['Heading1'], fontSize=28,
|
| 526 |
+
textColor=colors.HexColor('#1a237e'), spaceAfter=5, alignment=TA_CENTER, fontName='Helvetica-Bold')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
story.append(Paragraph("F A C T U R A", header_style))
|
| 528 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
doc.build(story)
|
| 530 |
return pdf_filename
|
| 531 |
|
| 532 |
# ============= FUNCIÓN PRINCIPAL =============
|
| 533 |
def procesar_factura(pdf_file):
|
| 534 |
if pdf_file is None:
|
| 535 |
+
return "", None, None, "", "", None, None, pdf_file
|
| 536 |
|
| 537 |
print("\n--- Extrayendo texto del PDF...")
|
| 538 |
texto = extraer_texto_pdf(pdf_file)
|
| 539 |
|
| 540 |
if texto.startswith("Error"):
|
| 541 |
+
return "", None, None, "", f"Error: {texto}", None, None, None
|
| 542 |
|
| 543 |
texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto
|
| 544 |
|
|
|
|
| 546 |
datos_json, resumen_util, mensaje = analizar_y_convertir_json(texto)
|
| 547 |
|
| 548 |
if not datos_json:
|
| 549 |
+
return texto_preview, None, None, "", mensaje, None, None, pdf_file
|
| 550 |
|
| 551 |
print("--- Convirtiendo JSON a CSV...")
|
| 552 |
df = json_a_csv(datos_json)
|
|
|
|
| 558 |
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
|
| 559 |
|
| 560 |
resumen_tecnico = f"""## Factura Procesada Exitosamente
|
|
|
|
| 561 |
**Consulta más información abajo**
|
|
|
|
| 562 |
---
|
|
|
|
| 563 |
### Estructura JSON Generada
|
|
|
|
| 564 |
```json
|
| 565 |
{json.dumps(datos_json, indent=2, ensure_ascii=False)}
|
| 566 |
```
|
|
|
|
| 567 |
---
|
|
|
|
| 568 |
### Información del Archivo CSV
|
|
|
|
| 569 |
**Nombre del archivo:** `{csv_filename}`
|
| 570 |
**Total de filas:** {len(df)}
|
| 571 |
**Formato:** UTF-8 con BOM
|
|
|
|
| 572 |
---
|
|
|
|
| 573 |
### Datos Principales Extraídos
|
|
|
|
| 574 |
**Número de factura:** {datos_json.get('numero_factura', 'N/A')}
|
| 575 |
**Fecha de emisión:** {datos_json.get('fecha', 'N/A')}
|
| 576 |
**Productos/Servicios:** {len(datos_json.get('productos', datos_json.get('conceptos', [])))} items
|
|
|
|
| 578 |
"""
|
| 579 |
|
| 580 |
print(f"--- CSV guardado: {csv_filename}")
|
| 581 |
+
return texto_preview, df, csv_filename, resumen_tecnico, resumen_util, datos_json, csv_filename, pdf_file
|
| 582 |
|
| 583 |
# ============= GENERAR PDF CON TEMPLATE SELECCIONADO =============
|
| 584 |
def generar_pdf_con_template(template, csv_file, datos_json):
|
|
|
|
| 600 |
return None, f"Error al generar PDF: {str(e)}"
|
| 601 |
|
| 602 |
# ============= INTERFAZ GRADIO =============
|
| 603 |
+
with gr.Blocks(title="Extractor de Facturas con IA Avanzada", theme=gr.themes.Soft()) as demo:
|
| 604 |
|
| 605 |
datos_json_state = gr.State()
|
| 606 |
csv_file_state = gr.State()
|
| 607 |
+
pdf_path_state = gr.State()
|
| 608 |
+
texto_state = gr.State()
|
| 609 |
|
| 610 |
gr.Markdown("""
|
| 611 |
+
# 🧠 Extractor y Generador de Facturas con IA Avanzada
|
| 612 |
+
### Análisis multimodal con modelos de Hugging Face: VQA, Document QA y Visual Understanding
|
| 613 |
""")
|
| 614 |
|
| 615 |
gr.Markdown("---")
|
| 616 |
|
| 617 |
+
with gr.Tabs():
|
| 618 |
+
# ============= TAB 1: EXTRACCIÓN CLÁSICA =============
|
| 619 |
+
with gr.Tab("📄 Extracción Automática"):
|
| 620 |
+
with gr.Row():
|
| 621 |
+
with gr.Column(scale=1):
|
| 622 |
+
gr.Markdown("### Subir Factura PDF")
|
| 623 |
+
pdf_input = gr.File(label="Seleccionar factura PDF", file_types=[".pdf"], type="filepath")
|
| 624 |
+
btn_extraer = gr.Button("🚀 Extraer Datos de la Factura", variant="primary", size="lg")
|
| 625 |
+
gr.Markdown("---")
|
| 626 |
+
csv_output = gr.File(label="📥 Descargar CSV generado")
|
| 627 |
+
gr.Markdown("---")
|
| 628 |
+
gr.Markdown("### 🎨 Rediseñar PDF")
|
| 629 |
+
template_selector = gr.Radio(choices=["Clásico", "Moderno", "Elegante"], value="Moderno", label="Estilo de factura")
|
| 630 |
+
btn_generar_pdf = gr.Button("Generar Factura PDF", variant="secondary", size="lg")
|
| 631 |
+
pdf_output = gr.File(label="📥 Descargar PDF generado")
|
| 632 |
+
pdf_status = gr.Textbox(label="Estado", interactive=False, lines=2)
|
| 633 |
+
|
| 634 |
+
with gr.Column(scale=2):
|
| 635 |
+
gr.Markdown("### 📊 Resultados del Análisis")
|
| 636 |
+
info_util = gr.Markdown(value="*Aquí aparecerá información relevante una vez procesada la factura*")
|
| 637 |
+
gr.Markdown("---")
|
| 638 |
+
with gr.Tabs():
|
| 639 |
+
with gr.Tab("Vista Previa CSV"):
|
| 640 |
+
tabla_preview = gr.DataFrame(label="Datos extraídos estructurados", wrap=True)
|
| 641 |
+
with gr.Tab("Texto Original"):
|
| 642 |
+
texto_extraido = gr.Textbox(label="Texto extraído del PDF", lines=18)
|
| 643 |
+
with gr.Tab("Más información"):
|
| 644 |
+
resumen_tecnico = gr.Markdown(label="Estructura de datos y metadatos")
|
| 645 |
+
|
| 646 |
+
# ============= TAB 2: VISUAL QUESTION ANSWERING =============
|
| 647 |
+
with gr.Tab("🔍 Visual Question Answering"):
|
| 648 |
+
gr.Markdown("""
|
| 649 |
+
### 🤖 Pregúntale a la IA sobre la imagen de tu factura
|
| 650 |
+
Los modelos VQA analizan visualmente el documento y responden preguntas específicas.
|
| 651 |
+
""")
|
| 652 |
|
| 653 |
+
with gr.Row():
|
| 654 |
+
with gr.Column():
|
| 655 |
+
pdf_vqa_input = gr.File(label="PDF para VQA (o usa el ya cargado)", file_types=[".pdf"], type="filepath")
|
| 656 |
+
pregunta_vqa = gr.Textbox(
|
| 657 |
+
label="Tu pregunta sobre la factura",
|
| 658 |
+
placeholder="¿Cuál es el total de la factura?",
|
| 659 |
+
value="¿Qué información importante contiene esta factura?"
|
| 660 |
+
)
|
| 661 |
+
btn_vqa = gr.Button("🔍 Analizar con VQA", variant="primary")
|
| 662 |
+
|
| 663 |
+
with gr.Column():
|
| 664 |
+
resultado_vqa = gr.Markdown(label="Respuestas de modelos VQA")
|
| 665 |
|
| 666 |
+
gr.Markdown("""
|
| 667 |
+
**Modelos utilizados:**
|
| 668 |
+
- `dandelin/vilt-b32-finetuned-vqa` - Vision-and-Language Transformer
|
| 669 |
+
- `Salesforce/blip-vqa-base` - BLIP VQA Base
|
| 670 |
+
- `Salesforce/blip2-opt-2.7b` - BLIP-2 con OPT-2.7B
|
| 671 |
+
""")
|
| 672 |
+
|
| 673 |
+
# ============= TAB 3: DOCUMENT QUESTION ANSWERING =============
|
| 674 |
+
with gr.Tab("📝 Document Question Answering"):
|
| 675 |
+
gr.Markdown("""
|
| 676 |
+
### 💬 Pregunta sobre el contenido del texto
|
| 677 |
+
Los modelos Document QA extraen información específica del texto de la factura.
|
| 678 |
+
""")
|
| 679 |
|
| 680 |
+
with gr.Row():
|
| 681 |
+
with gr.Column():
|
| 682 |
+
pregunta_doc_qa = gr.Textbox(
|
| 683 |
+
label="Pregunta sobre el documento",
|
| 684 |
+
placeholder="¿Cuál es el NIF del emisor?",
|
| 685 |
+
value="¿Cuál es el total de la factura?"
|
| 686 |
+
)
|
| 687 |
+
btn_doc_qa = gr.Button("📝 Analizar con Document QA", variant="primary")
|
| 688 |
+
|
| 689 |
+
with gr.Column():
|
| 690 |
+
resultado_doc_qa = gr.Markdown(label="Respuestas de modelos Document QA")
|
| 691 |
|
| 692 |
+
gr.Markdown("""
|
| 693 |
+
**Modelos utilizados:**
|
| 694 |
+
- `deepset/roberta-base-squad2` - RoBERTa entrenado en SQuAD 2.0
|
| 695 |
+
- `distilbert-base-cased-distilled-squad` - DistilBERT optimizado
|
| 696 |
+
- `bert-large-uncased-whole-word-masking-finetuned-squad` - BERT Large
|
| 697 |
+
""")
|
| 698 |
+
|
| 699 |
+
# ============= TAB 4: LAYOUT DOCUMENT QA =============
|
| 700 |
+
with gr.Tab("📐 Layout Document QA"):
|
| 701 |
+
gr.Markdown("""
|
| 702 |
+
### 🏗️ Análisis con comprensión del layout visual
|
| 703 |
+
Los modelos LayoutLM entienden la estructura visual del documento (tablas, columnas, etc.)
|
| 704 |
+
""")
|
| 705 |
|
| 706 |
+
with gr.Row():
|
| 707 |
+
with gr.Column():
|
| 708 |
+
pregunta_layout = gr.Textbox(
|
| 709 |
+
label="Pregunta sobre el documento",
|
| 710 |
+
placeholder="¿Cuál es el número de factura?",
|
| 711 |
+
value="¿Cuál es el número de factura?"
|
| 712 |
+
)
|
| 713 |
+
btn_layout_qa = gr.Button("📐 Analizar con Layout QA", variant="primary")
|
| 714 |
+
|
| 715 |
+
with gr.Column():
|
| 716 |
+
resultado_layout = gr.Markdown(label="Respuestas de modelos Layout QA")
|
| 717 |
|
| 718 |
+
gr.Markdown("""
|
| 719 |
+
**Modelos utilizados:**
|
| 720 |
+
- `impira/layoutlm-document-qa` - LayoutLM para Document QA
|
| 721 |
+
- `microsoft/layoutlmv2-base-uncased` - LayoutLM v2 Base
|
| 722 |
+
- `nielsr/layoutlmv3-finetuned-funsd` - LayoutLM v3 Fine-tuned
|
| 723 |
+
""")
|
| 724 |
+
|
| 725 |
+
# ============= TAB 5: VISUAL DOCUMENT UNDERSTANDING =============
|
| 726 |
+
with gr.Tab("🖼️ Visual Document Understanding"):
|
| 727 |
+
gr.Markdown("""
|
| 728 |
+
### 🎯 Comprensión visual completa del documento
|
| 729 |
+
Modelos multimodales que procesan la imagen del documento directamente.
|
| 730 |
+
""")
|
| 731 |
|
| 732 |
+
with gr.Row():
|
| 733 |
+
with gr.Column():
|
| 734 |
+
btn_visual_doc = gr.Button("🖼️ Analizar Documento Visualmente", variant="primary", size="lg")
|
| 735 |
+
|
| 736 |
+
with gr.Column():
|
| 737 |
+
resultado_visual_doc = gr.Markdown(label="Resultados de análisis visual")
|
| 738 |
+
status_visual_doc = gr.Textbox(label="Estado", interactive=False)
|
| 739 |
|
| 740 |
+
gr.Markdown("""
|
| 741 |
+
**Modelos utilizados:**
|
| 742 |
+
- `microsoft/trocr-large-printed` - TrOCR para texto impreso
|
| 743 |
+
- `Salesforce/blip-image-captioning-large` - BLIP Image Captioning
|
| 744 |
+
- `nlpconnect/vit-gpt2-image-captioning` - ViT + GPT2 Captioning
|
| 745 |
+
""")
|
| 746 |
|
| 747 |
+
# ============= TAB 6: DOCUMENT RETRIEVAL =============
|
| 748 |
+
with gr.Tab("🔎 Document Retrieval"):
|
| 749 |
+
gr.Markdown("""
|
| 750 |
+
### 🎯 Búsqueda semántica en el documento
|
| 751 |
+
Encuentra fragmentos relevantes usando embeddings y similitud semántica.
|
| 752 |
+
""")
|
|
|
|
|
|
|
|
|
|
| 753 |
|
| 754 |
+
with gr.Row():
|
| 755 |
+
with gr.Column():
|
| 756 |
+
consulta_retrieval = gr.Textbox(
|
| 757 |
+
label="¿Qué información buscas?",
|
| 758 |
+
placeholder="información sobre el emisor",
|
| 759 |
+
value="información sobre el emisor"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 760 |
)
|
| 761 |
+
btn_retrieval = gr.Button("🔎 Buscar en Documento", variant="primary")
|
| 762 |
|
| 763 |
+
with gr.Column():
|
| 764 |
+
resultado_retrieval = gr.Markdown(label="Fragmentos relevantes encontrados")
|
| 765 |
+
|
| 766 |
+
gr.Markdown("""
|
| 767 |
+
**Modelos utilizados:**
|
| 768 |
+
- `sentence-transformers/all-MiniLM-L6-v2` - Embeddings multilingües
|
| 769 |
+
- `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` - Paraphrase ML
|
| 770 |
+
""")
|
|
|
|
|
|
|
|
|
|
| 771 |
|
|
|
|
| 772 |
gr.Markdown("---")
|
| 773 |
+
gr.Markdown("""
|
| 774 |
+
### 📚 Información sobre los modelos
|
| 775 |
|
| 776 |
+
**Visual Question Answering (VQA):** Responde preguntas sobre imágenes usando visión y lenguaje.
|
| 777 |
+
|
| 778 |
+
**Document QA:** Extrae información específica del texto usando modelos de comprensión lectora.
|
| 779 |
+
|
| 780 |
+
**Layout Document QA:** Entiende la estructura visual (tablas, columnas) además del texto.
|
| 781 |
|
| 782 |
+
**Visual Document Understanding:** Procesa documentos como imágenes para OCR y comprensión completa.
|
| 783 |
+
|
| 784 |
+
**Document Retrieval:** Búsqueda semántica de información relevante en el documento.
|
| 785 |
+
|
| 786 |
+
---
|
| 787 |
+
|
| 788 |
+
💡 **Tip:** Procesa primero la factura en la pestaña "Extracción Automática" y luego explora las demás funcionalidades de IA.
|
| 789 |
+
""")
|
| 790 |
+
|
| 791 |
+
# ============= CONECTAR EVENTOS =============
|
| 792 |
+
|
| 793 |
+
# Extracción automática
|
| 794 |
btn_extraer.click(
|
| 795 |
fn=procesar_factura,
|
| 796 |
inputs=[pdf_input],
|
| 797 |
+
outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util,
|
| 798 |
+
datos_json_state, csv_file_state, pdf_path_state]
|
| 799 |
)
|
| 800 |
|
| 801 |
+
# Generar PDF
|
| 802 |
btn_generar_pdf.click(
|
| 803 |
fn=generar_pdf_con_template,
|
| 804 |
inputs=[template_selector, csv_file_state, datos_json_state],
|
| 805 |
outputs=[pdf_output, pdf_status]
|
| 806 |
)
|
| 807 |
+
|
| 808 |
+
# Visual Question Answering
|
| 809 |
+
def ejecutar_vqa(pdf_vqa, pdf_auto, pregunta):
|
| 810 |
+
pdf_path = pdf_vqa if pdf_vqa else pdf_auto
|
| 811 |
+
if not pdf_path:
|
| 812 |
+
return "❌ Por favor, sube un PDF primero"
|
| 813 |
+
return analizar_con_vqa(pdf_path, pregunta)
|
| 814 |
+
|
| 815 |
+
btn_vqa.click(
|
| 816 |
+
fn=ejecutar_vqa,
|
| 817 |
+
inputs=[pdf_vqa_input, pdf_path_state, pregunta_vqa],
|
| 818 |
+
outputs=[resultado_vqa]
|
| 819 |
+
)
|
| 820 |
+
|
| 821 |
+
# Document Question Answering
|
| 822 |
+
def ejecutar_doc_qa(texto, pregunta):
|
| 823 |
+
if not texto:
|
| 824 |
+
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'"
|
| 825 |
+
return analizar_con_document_qa(texto, pregunta)
|
| 826 |
+
|
| 827 |
+
btn_doc_qa.click(
|
| 828 |
+
fn=ejecutar_doc_qa,
|
| 829 |
+
inputs=[texto_extraido, pregunta_doc_qa],
|
| 830 |
+
outputs=[resultado_doc_qa]
|
| 831 |
+
)
|
| 832 |
+
|
| 833 |
+
# Layout Document QA
|
| 834 |
+
def ejecutar_layout_qa(pdf_path, texto, pregunta):
|
| 835 |
+
if not pdf_path or not texto:
|
| 836 |
+
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'"
|
| 837 |
+
return analizar_con_layout_qa(pdf_path, texto, pregunta)
|
| 838 |
+
|
| 839 |
+
btn_layout_qa.click(
|
| 840 |
+
fn=ejecutar_layout_qa,
|
| 841 |
+
inputs=[pdf_path_state, texto_extraido, pregunta_layout],
|
| 842 |
+
outputs=[resultado_layout]
|
| 843 |
+
)
|
| 844 |
+
|
| 845 |
+
# Visual Document Understanding
|
| 846 |
+
def ejecutar_visual_doc(pdf_path):
|
| 847 |
+
if not pdf_path:
|
| 848 |
+
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'", ""
|
| 849 |
+
return analizar_documento_visual_hf(pdf_path)
|
| 850 |
+
|
| 851 |
+
btn_visual_doc.click(
|
| 852 |
+
fn=ejecutar_visual_doc,
|
| 853 |
+
inputs=[pdf_path_state],
|
| 854 |
+
outputs=[resultado_visual_doc, status_visual_doc]
|
| 855 |
+
)
|
| 856 |
+
|
| 857 |
+
# Document Retrieval
|
| 858 |
+
def ejecutar_retrieval(texto, consulta):
|
| 859 |
+
if not texto:
|
| 860 |
+
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'"
|
| 861 |
+
return buscar_en_documento(texto, consulta)
|
| 862 |
+
|
| 863 |
+
btn_retrieval.click(
|
| 864 |
+
fn=ejecutar_retrieval,
|
| 865 |
+
inputs=[texto_extraido, consulta_retrieval],
|
| 866 |
+
outputs=[resultado_retrieval]
|
| 867 |
+
)
|
| 868 |
|
| 869 |
if __name__ == "__main__":
|
| 870 |
demo.launch()
|