angelsg213 commited on
Commit
bf52c26
·
verified ·
1 Parent(s): cc62f8c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +516 -495
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, Image
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, decide cómo estructurarla y devuelve JSON"""
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 DESDE CSV - TEMPLATE CLÁSICO =============
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
- # Estilos personalizados
231
- titulo_style = ParagraphStyle(
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
- # Información básica
245
- info_data = [
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
- # Título moderno
376
- titulo_style = ParagraphStyle(
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
- # Encabezado elegante
501
- header_style = ParagraphStyle(
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 y Generador de Facturas") as demo:
724
 
725
  datos_json_state = gr.State()
726
  csv_file_state = gr.State()
 
 
727
 
728
  gr.Markdown("""
729
- # Extractor y Generador de Facturas
730
- ### Extrae datos de facturas PDF y genera automáticamente tu archivo CSV
731
  """)
732
 
733
  gr.Markdown("---")
734
 
735
- with gr.Row():
736
- # COLUMNA IZQUIERDA
737
- with gr.Column(scale=1):
738
- gr.Markdown("### Extraer Datos")
739
- gr.Markdown("")
740
-
741
- pdf_input = gr.File(
742
- label="Subir factura PDF para extraer datos",
743
- file_types=[".pdf"],
744
- type="filepath"
745
- )
746
-
747
- gr.Markdown("")
748
-
749
- btn_extraer = gr.Button(
750
- "Extraer Datos de la Factura",
751
- variant="primary",
752
- size="lg"
753
- )
754
-
755
- gr.Markdown("")
756
- gr.Markdown("---")
757
- gr.Markdown("")
758
-
759
- csv_output = gr.File(label="Descargar CSV con los datos extraídos")
 
 
 
 
 
 
 
 
 
 
760
 
761
- gr.Markdown("")
762
- gr.Markdown("---")
763
- gr.Markdown("")
 
 
 
 
 
 
 
 
 
764
 
765
- # Generador de PDF
766
- gr.Markdown("### Rediseñar PDF")
767
- gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
768
 
769
- template_selector = gr.Radio(
770
- choices=["Clásico", "Moderno", "Elegante"],
771
- value="Moderno",
772
- label="Seleccionar estilo de factura",
773
- info="Elige el diseño que prefieras"
774
- )
 
 
 
 
 
775
 
776
- gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
777
 
778
- btn_generar_pdf = gr.Button(
779
- "Generar Factura PDF",
780
- variant="secondary",
781
- size="lg"
782
- )
 
 
 
 
 
 
783
 
784
- gr.Markdown("")
 
 
 
 
 
 
 
 
 
 
 
 
785
 
786
- pdf_output = gr.File(label="Descargar factura PDF generada")
 
 
 
 
 
 
787
 
788
- pdf_status = gr.Textbox(
789
- label="Estado",
790
- interactive=False,
791
- lines=2
792
- )
 
793
 
794
- # COLUMNA DERECHA
795
- with gr.Column(scale=2):
796
- gr.Markdown("### Resultados del Análisis")
797
- gr.Markdown("")
798
-
799
- gr.Markdown("#### Información Útil para Administrativos")
800
- info_util = gr.Markdown(
801
- value="*Aquí aparecerá información relevante una vez procesada la factura*"
802
- )
803
 
804
- gr.Markdown("")
805
- gr.Markdown("---")
806
- gr.Markdown("")
807
-
808
- with gr.Tabs():
809
- with gr.Tab("Vista Previa CSV"):
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.Tab("Texto Original"):
818
- gr.Markdown("")
819
- texto_extraido = gr.Textbox(
820
- label="Texto extraído del PDF",
821
- lines=18,
822
- max_lines=25
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
- # Conectar botones
 
 
 
 
 
 
 
 
 
 
 
836
  btn_extraer.click(
837
  fn=procesar_factura,
838
  inputs=[pdf_input],
839
- outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util, datos_json_state, csv_file_state]
 
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()