angelsg213 commited on
Commit
e141ab7
·
verified ·
1 Parent(s): 4691b65

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +236 -46
app.py CHANGED
@@ -1,6 +1,10 @@
1
  import gradio as gr
2
  import PyPDF2
3
  import os
 
 
 
 
4
  from huggingface_hub import InferenceClient
5
 
6
  # ============= EXTRAER TEXTO DEL PDF =============
@@ -14,31 +18,62 @@ def extraer_texto_pdf(pdf_file):
14
  except Exception as e:
15
  return f"Error: {str(e)}"
16
 
17
- # ============= ANALIZAR CON LLM =============
18
- def analizar_con_llm(texto):
19
- """El LLM analiza la factura y devuelve un resumen en un párrafo"""
20
 
21
  token = os.getenv("aa")
22
  if not token:
23
- return "❌ Error: Falta configurar HF_TOKEN en Settings → Secrets"
24
 
25
  # Limitar texto
26
  texto_limpio = texto[:8000]
27
 
28
- # Prompt simple
29
- prompt = f"""Analiza esta factura y dame un resumen en UN SOLO PÁRRAFO con:
30
- - Número de factura
31
- - Fecha
32
- - Emisor y cliente
33
- - Productos/servicios
34
- - Total a pagar
35
 
36
  TEXTO DE LA FACTURA:
37
  {texto_limpio}
38
 
39
- Responde en un solo párrafo claro y conciso en español:"""
 
 
 
 
 
40
 
41
- # Lista de modelos que SÍ funcionan (probados)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  modelos = [
43
  "Qwen/Qwen2.5-72B-Instruct",
44
  "meta-llama/Llama-3.2-3B-Instruct",
@@ -48,7 +83,7 @@ Responde en un solo párrafo claro y conciso en español:"""
48
 
49
  for modelo in modelos:
50
  try:
51
- print(f"🤖 Probando: {modelo}")
52
  client = InferenceClient(token=token)
53
 
54
  # Llamar al modelo
@@ -57,79 +92,234 @@ Responde en un solo párrafo claro y conciso en español:"""
57
  messages=[
58
  {"role": "user", "content": prompt}
59
  ],
60
- max_tokens=500,
61
- temperature=0.3
62
  )
63
 
64
  # Extraer respuesta
65
  resultado = response.choices[0].message.content
66
- print(f"✅ Funcionó con {modelo}")
67
- return resultado
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  except Exception as e:
70
  print(f"❌ {modelo} falló: {str(e)[:100]}")
71
  continue
72
 
73
- return "❌ Ningún modelo LLM funcionó. Verifica tu HF_TOKEN o intenta más tarde."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  # ============= FUNCIÓN PRINCIPAL =============
76
  def procesar_factura(pdf_file):
77
  if pdf_file is None:
78
- return "", "⚠️ Sube un PDF primero"
79
 
80
- # Extraer texto
 
81
  texto = extraer_texto_pdf(pdf_file)
82
 
83
  if texto.startswith("Error"):
84
- return "", f"❌ {texto}"
85
 
86
- # Mostrar texto extraído
87
- texto_preview = f"**Texto extraído ({len(texto)} caracteres):**\n\n{texto[:1000]}..."
88
 
89
- # Analizar con LLM
90
- analisis = analizar_con_llm(texto)
 
91
 
92
- # Resultado final
93
- resultado = f"""## 📄 Análisis de la Factura
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
 
95
- {analisis}
 
 
 
 
 
 
96
 
97
  ---
98
 
99
- ### 📝 Texto Original:
100
- {texto_preview}
 
 
 
 
 
 
 
101
  """
102
 
103
- return texto, resultado
 
104
 
105
  # ============= INTERFAZ GRADIO =============
106
- with gr.Blocks(title="Analizador de Facturas con IA") as demo:
107
  gr.Markdown("""
108
- # 🤖 Analizador de Facturas con IA
109
- ### Sube un PDF y el LLM lo analizará en un párrafo
 
 
 
 
 
 
110
  """)
111
 
112
  with gr.Row():
113
- with gr.Column():
114
- pdf_input = gr.File(label="📎 Subir PDF de Factura", file_types=[".pdf"])
115
- btn = gr.Button("🚀 Analizar", variant="primary", size="lg")
 
 
 
 
 
 
 
 
 
 
 
 
 
116
 
117
- with gr.Column():
118
- texto_salida = gr.Textbox(label="📝 Texto Extraído", lines=10, max_lines=15)
119
- resultado = gr.Markdown(label="🤖 Análisis del LLM")
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  btn.click(
122
  fn=procesar_factura,
123
  inputs=[pdf_input],
124
- outputs=[texto_salida, resultado]
125
  )
126
 
127
  gr.Markdown("""
128
  ---
129
- **Configuración necesaria:**
130
- 1. Instala: `pip install huggingface_hub`
131
- 2. Ve a Settings Secrets
132
- 3. Crea: `HF_TOKEN` = tu token de https://huggingface.co/settings/tokens
 
 
133
  """)
134
 
135
  if __name__ == "__main__":
 
1
  import gradio as gr
2
  import PyPDF2
3
  import os
4
+ import json
5
+ import pandas as pd
6
+ import re
7
+ from datetime import datetime
8
  from huggingface_hub import InferenceClient
9
 
10
  # ============= EXTRAER TEXTO DEL PDF =============
 
18
  except Exception as e:
19
  return f"Error: {str(e)}"
20
 
21
+ # ============= ANALIZAR CON LLM Y CONVERTIR A JSON =============
22
+ def analizar_y_convertir_json(texto):
23
+ """El LLM lee la factura, decide cómo estructurarla y devuelve JSON"""
24
 
25
  token = os.getenv("aa")
26
  if not token:
27
+ return None, "❌ Error: Falta configurar HF_TOKEN en Settings → Secrets"
28
 
29
  # Limitar texto
30
  texto_limpio = texto[:8000]
31
 
32
+ # Prompt para que el LLM decida la estructura JSON
33
+ prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON.
 
 
 
 
 
34
 
35
  TEXTO DE LA FACTURA:
36
  {texto_limpio}
37
 
38
+ INSTRUCCIONES:
39
+ 1. Analiza el texto y decide qué información es importante extraer
40
+ 2. Crea un JSON estructurado con TODOS los datos que encuentres
41
+ 3. Incluye: número de factura, fecha, emisor, cliente, productos/servicios, importes
42
+ 4. Para los números: usa formato numérico puro (ejemplo: 250 no "250€")
43
+ 5. Si hay tabla de productos, extrae CADA producto con cantidad, precio y total
44
 
45
+ FORMATO JSON (ajusta según lo que encuentres):
46
+ {{
47
+ "numero_factura": "string",
48
+ "fecha": "DD/MM/YYYY",
49
+ "emisor": {{
50
+ "nombre": "string",
51
+ "nif": "string",
52
+ "direccion": "string"
53
+ }},
54
+ "cliente": {{
55
+ "nombre": "string",
56
+ "nif": "string"
57
+ }},
58
+ "productos": [
59
+ {{
60
+ "descripcion": "string",
61
+ "cantidad": number,
62
+ "precio_unitario": number,
63
+ "total": number
64
+ }}
65
+ ],
66
+ "totales": {{
67
+ "base_imponible": number,
68
+ "iva": number,
69
+ "porcentaje_iva": number,
70
+ "total": number
71
+ }}
72
+ }}
73
+
74
+ Responde SOLO con el JSON válido (sin explicaciones, sin markdown):"""
75
+
76
+ # Lista de modelos que funcionan
77
  modelos = [
78
  "Qwen/Qwen2.5-72B-Instruct",
79
  "meta-llama/Llama-3.2-3B-Instruct",
 
83
 
84
  for modelo in modelos:
85
  try:
86
+ print(f"\n🤖 Probando: {modelo}")
87
  client = InferenceClient(token=token)
88
 
89
  # Llamar al modelo
 
92
  messages=[
93
  {"role": "user", "content": prompt}
94
  ],
95
+ max_tokens=2000,
96
+ temperature=0.1
97
  )
98
 
99
  # Extraer respuesta
100
  resultado = response.choices[0].message.content
101
+
102
+ # Limpiar respuesta (quitar markdown si existe)
103
+ resultado = resultado.strip()
104
+ resultado = re.sub(r'```json\s*', '', resultado)
105
+ resultado = re.sub(r'```\s*', '', resultado)
106
+ resultado = resultado.strip()
107
+
108
+ # Buscar JSON en la respuesta
109
+ match = re.search(r'\{.*\}', resultado, re.DOTALL)
110
+ if match:
111
+ json_str = match.group(0)
112
+ try:
113
+ datos_json = json.loads(json_str)
114
+ print(f"✅ JSON válido extraído con {modelo}")
115
+ return datos_json, f"✅ Procesado con {modelo}"
116
+ except json.JSONDecodeError as e:
117
+ print(f"⚠️ JSON inválido: {str(e)[:50]}")
118
+ continue
119
+ else:
120
+ print(f"⚠️ No se encontró JSON en la respuesta")
121
+ continue
122
 
123
  except Exception as e:
124
  print(f"❌ {modelo} falló: {str(e)[:100]}")
125
  continue
126
 
127
+ return None, "❌ Ningún modelo LLM pudo extraer el JSON. Verifica tu HF_TOKEN."
128
+
129
+ # ============= CONVERTIR JSON A CSV =============
130
+ def json_a_csv(datos_json):
131
+ """Convierte el JSON en un DataFrame para CSV"""
132
+
133
+ if not datos_json:
134
+ return None
135
+
136
+ filas = []
137
+
138
+ # === INFORMACIÓN GENERAL ===
139
+ filas.append({'Campo': '=== INFORMACIÓN GENERAL ===', 'Valor': ''})
140
+ filas.append({'Campo': 'Número de Factura', 'Valor': datos_json.get('numero_factura', 'N/A')})
141
+ filas.append({'Campo': 'Fecha', 'Valor': datos_json.get('fecha', 'N/A')})
142
+
143
+ # === EMISOR ===
144
+ if 'emisor' in datos_json:
145
+ filas.append({'Campo': '', 'Valor': ''})
146
+ filas.append({'Campo': '=== EMISOR ===', 'Valor': ''})
147
+ emisor = datos_json['emisor']
148
+ if isinstance(emisor, dict):
149
+ for key, value in emisor.items():
150
+ filas.append({'Campo': key.replace('_', ' ').title(), 'Valor': str(value)})
151
+ else:
152
+ filas.append({'Campo': 'Nombre', 'Valor': str(emisor)})
153
+
154
+ # === CLIENTE ===
155
+ if 'cliente' in datos_json:
156
+ filas.append({'Campo': '', 'Valor': ''})
157
+ filas.append({'Campo': '=== CLIENTE ===', 'Valor': ''})
158
+ cliente = datos_json['cliente']
159
+ if isinstance(cliente, dict):
160
+ for key, value in cliente.items():
161
+ filas.append({'Campo': key.replace('_', ' ').title(), 'Valor': str(value)})
162
+ else:
163
+ filas.append({'Campo': 'Nombre', 'Valor': str(cliente)})
164
+
165
+ # === PRODUCTOS/SERVICIOS ===
166
+ productos = datos_json.get('productos', datos_json.get('conceptos', datos_json.get('items', [])))
167
+ if productos and len(productos) > 0:
168
+ filas.append({'Campo': '', 'Valor': ''})
169
+ filas.append({'Campo': '=== PRODUCTOS/SERVICIOS ===', 'Valor': ''})
170
+
171
+ for i, prod in enumerate(productos, 1):
172
+ filas.append({'Campo': f'Producto {i}', 'Valor': prod.get('descripcion', 'N/A')})
173
+ filas.append({'Campo': ' Cantidad', 'Valor': str(prod.get('cantidad', ''))})
174
+ filas.append({'Campo': ' Precio Unitario', 'Valor': f"{prod.get('precio_unitario', 0)}€"})
175
+ filas.append({'Campo': ' Total', 'Valor': f"{prod.get('total', 0)}€"})
176
+ filas.append({'Campo': '', 'Valor': ''})
177
+
178
+ # === TOTALES ===
179
+ totales = datos_json.get('totales', {})
180
+ if totales or 'base_imponible' in datos_json or 'total' in datos_json:
181
+ filas.append({'Campo': '', 'Valor': ''})
182
+ filas.append({'Campo': '=== TOTALES ===', 'Valor': ''})
183
+
184
+ # Buscar totales en varios lugares del JSON
185
+ base = totales.get('base_imponible', datos_json.get('base_imponible', 0))
186
+ iva = totales.get('iva', datos_json.get('iva', 0))
187
+ porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0))
188
+ total = totales.get('total', datos_json.get('total', 0))
189
+
190
+ filas.append({'Campo': 'Base Imponible', 'Valor': f"{base}€"})
191
+ if porcentaje_iva > 0:
192
+ filas.append({'Campo': f'IVA ({porcentaje_iva}%)', 'Valor': f"{iva}€"})
193
+ else:
194
+ filas.append({'Campo': 'IVA', 'Valor': f"{iva}€"})
195
+ filas.append({'Campo': 'TOTAL', 'Valor': f"{total}€"})
196
+
197
+ return pd.DataFrame(filas)
198
 
199
  # ============= FUNCIÓN PRINCIPAL =============
200
  def procesar_factura(pdf_file):
201
  if pdf_file is None:
202
+ return "", None, None, "⚠️ Sube un PDF primero"
203
 
204
+ # PASO 1: Extraer texto del PDF
205
+ print("\n📄 Extrayendo texto del PDF...")
206
  texto = extraer_texto_pdf(pdf_file)
207
 
208
  if texto.startswith("Error"):
209
+ return "", None, None, f"❌ {texto}"
210
 
211
+ # Mostrar preview del texto
212
+ texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto
213
 
214
+ # PASO 2: LLM analiza y convierte a JSON
215
+ print("🤖 El LLM está analizando la factura y creando el JSON...")
216
+ datos_json, mensaje = analizar_y_convertir_json(texto)
217
 
218
+ if not datos_json:
219
+ return texto_preview, None, None, mensaje
220
+
221
+ # PASO 3: Convertir JSON a DataFrame
222
+ print("📊 Convirtiendo JSON a CSV...")
223
+ df = json_a_csv(datos_json)
224
+
225
+ # PASO 4: Guardar CSV
226
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
227
+ numero = datos_json.get('numero_factura', 'factura')
228
+ numero = re.sub(r'[^\w\-]', '_', str(numero)) # Limpiar caracteres especiales
229
+ csv_filename = f"{numero}_{timestamp}.csv"
230
+ df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
231
+
232
+ # PASO 5: Crear resumen
233
+ resumen = f"""## ✅ Factura Procesada Exitosamente
234
+
235
+ {mensaje}
236
 
237
+ ---
238
+
239
+ ### 📊 JSON Generado por el LLM:
240
+
241
+ ```json
242
+ {json.dumps(datos_json, indent=2, ensure_ascii=False)}
243
+ ```
244
 
245
  ---
246
 
247
+ ### 💾 Archivo CSV:
248
+ - **Nombre:** `{csv_filename}`
249
+ - **Filas:** {len(df)}
250
+
251
+ ### 📋 Datos Extraídos:
252
+ - **Número:** {datos_json.get('numero_factura', 'N/A')}
253
+ - **Fecha:** {datos_json.get('fecha', 'N/A')}
254
+ - **Productos:** {len(datos_json.get('productos', datos_json.get('conceptos', [])))}
255
+ - **Total:** {datos_json.get('totales', {}).get('total', datos_json.get('total', 'N/A'))}€
256
  """
257
 
258
+ print(f"✅ CSV guardado: {csv_filename}")
259
+ return texto_preview, df, csv_filename, resumen
260
 
261
  # ============= INTERFAZ GRADIO =============
262
+ with gr.Blocks(title="Extractor IA de Facturas", theme=gr.themes.Soft()) as demo:
263
  gr.Markdown("""
264
+ # 🤖 Extractor Inteligente de Facturas con IA
265
+
266
+ ### 📋 Proceso automático:
267
+ 1. 📄 Extrae el texto del PDF
268
+ 2. 🤖 El LLM analiza y decide cómo estructurar el JSON
269
+ 3. 📊 Convierte el JSON a CSV
270
+ 4. 👁️ Previsualiza los datos
271
+ 5. 💾 Descarga el archivo CSV
272
  """)
273
 
274
  with gr.Row():
275
+ with gr.Column(scale=1):
276
+ pdf_input = gr.File(
277
+ label="📎 Subir Factura (PDF)",
278
+ file_types=[".pdf"],
279
+ type="filepath"
280
+ )
281
+ btn = gr.Button("🚀 Procesar Factura", variant="primary", size="lg")
282
+
283
+ gr.Markdown("""
284
+ ---
285
+ ### ⚙️ Configuración:
286
+ 1. Instala: `pip install huggingface_hub gradio PyPDF2 pandas`
287
+ 2. Settings → Secrets
288
+ 3. Crea: `HF_TOKEN`
289
+ 4. Token: [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens)
290
+ """)
291
 
292
+ with gr.Column(scale=2):
293
+ texto_extraido = gr.Textbox(
294
+ label="📝 Texto Extraído del PDF",
295
+ lines=8,
296
+ max_lines=10
297
+ )
298
+
299
+ tabla_preview = gr.DataFrame(
300
+ label="👁️ Previsualización CSV",
301
+ wrap=True,
302
+ interactive=False
303
+ )
304
+
305
+ resumen = gr.Markdown(label="📊 Resumen del Análisis")
306
+
307
+ csv_output = gr.File(label="💾 Descargar CSV")
308
 
309
  btn.click(
310
  fn=procesar_factura,
311
  inputs=[pdf_input],
312
+ outputs=[texto_extraido, tabla_preview, csv_output, resumen]
313
  )
314
 
315
  gr.Markdown("""
316
  ---
317
+ ### 🎯 Características:
318
+ - El LLM **decide automáticamente** cómo estructurar el JSON
319
+ - Extrae número, fecha, emisor, cliente, productos y totales
320
+ - Genera CSV limpio y organizado
321
+ - ✅ Previsualización en tiempo real
322
+ - ✅ Modelos LLM gratuitos y potentes
323
  """)
324
 
325
  if __name__ == "__main__":