Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import PyPDF2 | |
| import os | |
| import json | |
| import pandas as pd | |
| import re | |
| from datetime import datetime | |
| from huggingface_hub import InferenceClient | |
| from reportlab.lib.pagesizes import letter, A4 | |
| from reportlab.lib import colors | |
| from reportlab.lib.units import inch | |
| from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer | |
| from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle | |
| from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT | |
| import time | |
| import numpy as np | |
| import wave | |
| # Para TTS emocional | |
| try: | |
| from gtts import gTTS | |
| GTTS_AVAILABLE = True | |
| except ImportError: | |
| GTTS_AVAILABLE = False | |
| print("⚠️ gTTS no disponible. Instala con: pip install gtts") | |
| # ============= EXTRAER TEXTO DEL PDF ============= | |
| def extraer_texto_pdf(pdf_file): | |
| try: | |
| pdf_reader = PyPDF2.PdfReader(pdf_file) | |
| texto = "" | |
| for pagina in pdf_reader.pages: | |
| texto += pagina.extract_text() + "\n" | |
| return texto | |
| except Exception as e: | |
| return f"Error: {str(e)}" | |
| # ============= GENERAR AUDIO CON EMOCIÓN MEJORADO ============= | |
| # ============= GENERAR AUDIO CON EMOCIÓN MEJORADO ============= | |
| # ============= GENERAR AUDIO CON EMOCIÓN MEJORADO ============= | |
| # ============= GENERAR AUDIO CON EMOCIÓN Y ANÁLISIS DE SENTIMIENTO ============= | |
| # ============= GENERAR AUDIO CON EMOCIÓN - VERSIÓN CORREGIDA ============= | |
| def generar_audio_respuesta(texto, client): | |
| """TTS emocional FUNCIONAL con gTTS (Google Text-to-Speech) - Diciembre 2024""" | |
| try: | |
| # Limpiar y preparar texto | |
| texto_limpio = texto.replace("*", "").replace("#", "").replace("`", "").replace("€", " euros").strip() | |
| oraciones = re.split(r'[.!?]+', texto_limpio) | |
| oraciones = [o.strip() for o in oraciones if o.strip() and len(o.strip()) > 10] | |
| texto_audio = ". ".join(oraciones[:5]) + "." if len(oraciones) > 5 else ". ".join(oraciones) + "." | |
| if len(texto_audio) > 500: | |
| texto_audio = texto_audio[:497] + "..." | |
| print(f"🎤 Generando audio para: '{texto_audio[:100]}...'") | |
| # PASO 1: Análisis emocional | |
| emocion_detectada = "neutral" | |
| confianza = 0.5 | |
| try: | |
| print("🧠 Analizando emoción...") | |
| emotion_response = client.text_classification( | |
| text=texto_audio[:512], | |
| model="finiteautomata/beto-sentiment-analysis" | |
| ) | |
| if emotion_response and len(emotion_response) > 0: | |
| label = emotion_response[0]['label'].lower() | |
| sentiment_to_emotion = { | |
| 'pos': 'joy', | |
| 'positive': 'joy', | |
| 'neu': 'neutral', | |
| 'neutral': 'neutral', | |
| 'neg': 'sadness', | |
| 'negative': 'sadness' | |
| } | |
| emocion_detectada = sentiment_to_emotion.get(label, 'neutral') | |
| confianza = emotion_response[0]['score'] | |
| print(f"😊 Emoción: {emocion_detectada} (confianza: {confianza:.2%})") | |
| except Exception as e: | |
| print(f"⚠️ Error en análisis emocional: {str(e)[:100]}") | |
| # PASO 2: Generar audio con gTTS | |
| print("🔊 Generando audio con Google TTS...") | |
| if GTTS_AVAILABLE: | |
| tts = gTTS(text=texto_audio, lang='es', slow=False) | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| audio_path = f"audio_emocional_{emocion_detectada}_{timestamp}.mp3" | |
| tts.save(audio_path) | |
| if os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000: | |
| print(f"✅ Audio generado: {audio_path} ({os.path.getsize(audio_path)} bytes)") | |
| return audio_path | |
| print("⚠️ Intentando método alternativo...") | |
| return generar_audio_alternativo(texto, client) | |
| except Exception as e: | |
| print(f"❌ Error general: {str(e)}") | |
| return None, "neutral", 0.5 | |
| def generar_audio_alternativo(texto, client): | |
| """Método alternativo usando HuggingFace TTS""" | |
| emocion_detectada = "neutral" | |
| confianza = 0.5 | |
| texto_limpio = texto.replace("*", "").replace("#", "").replace("`", "").replace("€", " euros").strip() | |
| oraciones = re.split(r'[.!?]+', texto_limpio) | |
| oraciones = [o.strip() for o in oraciones if o.strip() and len(o.strip()) > 10] | |
| texto_audio = ". ".join(oraciones[:3]) + "." | |
| if len(texto_audio) > 400: | |
| texto_audio = texto_audio[:397] + "..." | |
| modelos_tts = ["facebook/mms-tts-spa"] | |
| for modelo in modelos_tts: | |
| try: | |
| print(f"🔊 Probando: {modelo}") | |
| audio_data = client.text_to_speech(text=texto_audio, model=modelo) | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| audio_path = f"audio_{timestamp}.wav" | |
| with open(audio_path, "wb") as f: | |
| if isinstance(audio_data, bytes): | |
| f.write(audio_data) | |
| elif hasattr(audio_data, 'read'): | |
| f.write(audio_data.read()) | |
| else: | |
| for chunk in audio_data: | |
| if chunk: | |
| f.write(chunk if isinstance(chunk, bytes) else bytes(chunk)) | |
| if os.path.exists(audio_path) and os.path.getsize(audio_path) > 1000: | |
| print(f"✅ Audio generado con {modelo}") | |
| return audio_path | |
| else: | |
| if os.path.exists(audio_path): | |
| os.remove(audio_path) | |
| except Exception as e: | |
| print(f"❌ Error con {modelo}: {str(e)[:100]}") | |
| return None, emocion_detectada, confianza | |
| # ============= ASISTENTE IA CONVERSACIONAL ============= | |
| def asistente_ia_factura(texto, pregunta_usuario): | |
| """Asistente IA que explica conceptos, responde preguntas y da consejos sobre facturas""" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error: Falta configurar HF_TOKEN en Settings → Secrets", None | |
| texto_limpio = texto[:6000] | |
| prompt = f"""Eres un asistente experto en facturas y finanzas que ayuda a entender documentos comerciales. | |
| TEXTO DE LA FACTURA: | |
| {texto_limpio} | |
| PREGUNTA DEL USUARIO: {pregunta_usuario} | |
| INSTRUCCIONES: | |
| 1. Responde de forma clara, amigable y profesional en español | |
| 2. Si te preguntan sobre conceptos (IVA, base imponible, etc.), explícalos de manera sencilla | |
| 3. Si te preguntan datos específicos, extráelos del texto de la factura | |
| 4. Da consejos útiles cuando sea relevante (gestión, pagos, fiscalidad básica) | |
| 5. Si no encuentras información específica en la factura, indícalo claramente | |
| 6. Usa un lenguaje accesible para personas sin conocimientos técnicos | |
| 7. Sé conciso pero completo (máximo 200 palabras) | |
| 8. IMPORTANTE: Tu respuesta será convertida a audio, así que: | |
| - Usa frases cortas y claras | |
| - Evita símbolos especiales como *, #, € | |
| - Usa "euros" en lugar de "€" | |
| - Habla en tono conversacional y natural | |
| Responde ahora:""" | |
| modelos = [ | |
| "Qwen/Qwen2.5-72B-Instruct", | |
| "meta-llama/Llama-3.2-3B-Instruct", | |
| "mistralai/Mistral-Nemo-Instruct-2407" | |
| ] | |
| for modelo in modelos: | |
| try: | |
| print(f"\n🤖 Consultando con: {modelo}") | |
| client = InferenceClient(token=token) | |
| response = client.chat.completions.create( | |
| model=modelo, | |
| messages=[ | |
| {"role": "system", "content": "Eres un asistente experto en facturas, finanzas y contabilidad básica. Ayudas a las personas a entender sus documentos comerciales de forma clara y amigable. Respondes en un estilo conversacional perfecto para convertir a audio."}, | |
| {"role": "user", "content": prompt} | |
| ], | |
| max_tokens=600, | |
| temperature=0.7 | |
| ) | |
| respuesta = response.choices[0].message.content | |
| print(f"✅ Respuesta obtenida con {modelo}") | |
| # Generar audio de la respuesta | |
| # Generar audio emocional de la respuesta | |
| print("🎵 Iniciando generación de audio emocional...") | |
| audio_path = generar_audio_respuesta(respuesta, client) | |
| # Crear transcripción con información emocional | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| transcripcion_path = f"transcripcion_{timestamp}.txt" | |
| with open(transcripcion_path, "w", encoding="utf-8") as f: | |
| f.write("=" * 60 + "\n") | |
| f.write("TRANSCRIPCIÓN DE AUDIO - ASISTENTE IA\n") | |
| f.write("=" * 60 + "\n\n") | |
| f.write(f"Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}\n") | |
| f.write(f"\n" + "-" * 60 + "\n\n") | |
| f.write("TEXTO COMPLETO:\n\n") | |
| f.write(respuesta) | |
| f.write(f"\n\n" + "-" * 60 + "\n") | |
| f.write(f"\nArchivo de audio: {audio_path if audio_path else 'No generado'}\n") | |
| f.write("=" * 60 + "\n") | |
| if audio_path and os.path.exists(audio_path): | |
| print(f"✅ Audio generado correctamente: {audio_path}") | |
| return respuesta, audio_path, transcripcion_path | |
| else: | |
| print("⚠️ No se pudo generar el audio, pero la respuesta está disponible") | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| audio_vacio = f"audio_no_disponible_{timestamp}.mp3" | |
| with open(audio_vacio, "w") as f: | |
| f.write("") | |
| return respuesta, audio_vacio, transcripcion_path | |
| except Exception as e: | |
| print(f"❌ Error con {modelo}: {str(e)}") | |
| continue | |
| return "❌ No se pudo obtener respuesta del asistente IA", None, None, "neutral", 0.0 | |
| # ============= ANÁLISIS DE SENTIMIENTO DE FACTURA ============= | |
| def analizar_sentimiento_factura(texto, client): | |
| """Analiza si la factura tiene alertas, urgencias o problemas""" | |
| prompt = f"""Analiza esta factura y determina si hay algo preocupante o urgente. | |
| TEXTO: {texto[:3000]} | |
| Responde en formato JSON: | |
| {{ | |
| "sentimiento": "positivo/neutral/alerta", | |
| "urgencia": "alta/media/baja", | |
| "razon": "explicación breve", | |
| "recomendacion": "qué hacer" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=300, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"sentimiento": "neutral", "urgencia": "baja", "razon": "Análisis no disponible", "recomendacion": "Revisar manualmente"} | |
| # ============= SUGERENCIAS INTELIGENTES ============= | |
| def generar_sugerencias_ia(datos_json, client): | |
| """Genera sugerencias personalizadas basadas en la factura""" | |
| prompt = f"""Basándote en esta factura, da 3 sugerencias útiles y prácticas: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Responde en español con: | |
| 1. Sugerencia sobre organización | |
| 2. Sugerencia sobre pagos o plazos | |
| 3. Sugerencia sobre optimización o ahorro | |
| Sé breve (máximo 150 palabras total):""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.7 | |
| ) | |
| return response.choices[0].message.content | |
| except: | |
| return "💡 Sugerencias: Mantén tus facturas organizadas por fecha, verifica los plazos de pago, y considera digitalizar todos tus documentos." | |
| # ============= EXTRACTOR DE CATEGORÍAS ============= | |
| def extraer_categorias_gasto(datos_json, client): | |
| """Categoriza automáticamente el tipo de gasto""" | |
| productos = datos_json.get('productos', []) | |
| texto_productos = " ".join([p.get('descripcion', '') for p in productos[:5]]) | |
| prompt = f"""Clasifica esta factura en UNA categoría de gasto: | |
| Productos/Servicios: {texto_productos} | |
| Total: {datos_json.get('totales', {}).get('total', 0)}€ | |
| Categorías posibles: | |
| - Oficina y suministros | |
| - Tecnología e IT | |
| - Servicios profesionales | |
| - Marketing y publicidad | |
| - Viajes y transporte | |
| - Alimentación y hostelería | |
| - Mantenimiento y reparaciones | |
| - Otros gastos | |
| Responde solo con el nombre de la categoría:""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=50, | |
| temperature=0.3 | |
| ) | |
| categoria = response.choices[0].message.content.strip() | |
| return f" **Categoría:** {categoria}" | |
| except: | |
| return " **Categoría:** No clasificada" | |
| # ============= TRADUCTOR MULTIIDIOMA CON CSV TABULAR ============= | |
| def traducir_factura_con_csv(datos_json, texto, idioma_destino, client): | |
| """Traduce la factura y genera tanto texto como CSV tabular""" | |
| idiomas = { | |
| "Inglés": "English", | |
| "Francés": "Français", | |
| "Alemán": "Deutsch", | |
| "Italiano": "Italiano", | |
| "Portugués": "Português" | |
| } | |
| idioma = idiomas.get(idioma_destino, "English") | |
| # 1. Traducir el texto completo | |
| prompt_texto = f"""Traduce este resumen de factura al {idioma}. Mantén el formato y estructura: | |
| {texto[:2000]} | |
| Traducción:""" | |
| try: | |
| response_texto = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt_texto}], | |
| max_tokens=1000, | |
| temperature=0.3 | |
| ) | |
| texto_traducido = response_texto.choices[0].message.content | |
| except: | |
| texto_traducido = "❌ Error en la traducción del texto" | |
| # 2. Crear DataFrame traducido | |
| if not datos_json: | |
| return texto_traducido, None, None | |
| # Traducir etiquetas según el idioma | |
| traducciones = { | |
| "Inglés": { | |
| "seccion": "Section", | |
| "campo": "Field", | |
| "valor": "Value", | |
| "tipo": "Type", | |
| "info_general": "GENERAL INFORMATION", | |
| "numero_factura": "Invoice Number", | |
| "fecha": "Date", | |
| "identificador": "Identifier", | |
| "emisor": "ISSUER", | |
| "nombre": "Name", | |
| "nif": "Tax ID", | |
| "direccion": "Address", | |
| "cliente": "CLIENT", | |
| "productos": "PRODUCTS", | |
| "producto": "Product", | |
| "cantidad": "Quantity", | |
| "precio_unitario": "Unit Price", | |
| "total_producto": "Total", | |
| "descripcion": "Description", | |
| "numerico": "Numeric", | |
| "monetario": "Monetary", | |
| "totales": "TOTALS", | |
| "base_imponible": "Taxable Base", | |
| "iva": "VAT", | |
| "total": "TOTAL", | |
| "informacion": "Information" | |
| }, | |
| "Francés": { | |
| "seccion": "Section", | |
| "campo": "Champ", | |
| "valor": "Valeur", | |
| "tipo": "Type", | |
| "info_general": "INFORMATIONS GÉNÉRALES", | |
| "numero_factura": "Numéro de Facture", | |
| "fecha": "Date", | |
| "identificador": "Identifiant", | |
| "emisor": "ÉMETTEUR", | |
| "nombre": "Nom", | |
| "nif": "NIF", | |
| "direccion": "Adresse", | |
| "cliente": "CLIENT", | |
| "productos": "PRODUITS", | |
| "producto": "Produit", | |
| "cantidad": "Quantité", | |
| "precio_unitario": "Prix Unitaire", | |
| "total_producto": "Total", | |
| "descripcion": "Description", | |
| "numerico": "Numérique", | |
| "monetario": "Monétaire", | |
| "totales": "TOTAUX", | |
| "base_imponible": "Base Imposable", | |
| "iva": "TVA", | |
| "total": "TOTAL", | |
| "informacion": "Information" | |
| }, | |
| "Alemán": { | |
| "seccion": "Abschnitt", | |
| "campo": "Feld", | |
| "valor": "Wert", | |
| "tipo": "Typ", | |
| "info_general": "ALLGEMEINE INFORMATIONEN", | |
| "numero_factura": "Rechnungsnummer", | |
| "fecha": "Datum", | |
| "identificador": "Kennung", | |
| "emisor": "AUSSTELLER", | |
| "nombre": "Name", | |
| "nif": "Steuernummer", | |
| "direccion": "Adresse", | |
| "cliente": "KUNDE", | |
| "productos": "PRODUKTE", | |
| "producto": "Produkt", | |
| "cantidad": "Menge", | |
| "precio_unitario": "Stückpreis", | |
| "total_producto": "Gesamt", | |
| "descripcion": "Beschreibung", | |
| "numerico": "Numerisch", | |
| "monetario": "Monetär", | |
| "totales": "SUMMEN", | |
| "base_imponible": "Steuerbemessungsgrundlage", | |
| "iva": "MwSt", | |
| "total": "GESAMT", | |
| "informacion": "Information" | |
| }, | |
| "Italiano": { | |
| "seccion": "Sezione", | |
| "campo": "Campo", | |
| "valor": "Valore", | |
| "tipo": "Tipo", | |
| "info_general": "INFORMAZIONI GENERALI", | |
| "numero_factura": "Numero Fattura", | |
| "fecha": "Data", | |
| "identificador": "Identificatore", | |
| "emisor": "EMITTENTE", | |
| "nombre": "Nome", | |
| "nif": "Partita IVA", | |
| "direccion": "Indirizzo", | |
| "cliente": "CLIENTE", | |
| "productos": "PRODOTTI", | |
| "producto": "Prodotto", | |
| "cantidad": "Quantità", | |
| "precio_unitario": "Prezzo Unitario", | |
| "total_producto": "Totale", | |
| "descripcion": "Descrizione", | |
| "numerico": "Numerico", | |
| "monetario": "Monetario", | |
| "totales": "TOTALI", | |
| "base_imponible": "Imponibile", | |
| "iva": "IVA", | |
| "total": "TOTALE", | |
| "informacion": "Informazione" | |
| }, | |
| "Portugués": { | |
| "seccion": "Seção", | |
| "campo": "Campo", | |
| "valor": "Valor", | |
| "tipo": "Tipo", | |
| "info_general": "INFORMAÇÃO GERAL", | |
| "numero_factura": "Número da Fatura", | |
| "fecha": "Data", | |
| "identificador": "Identificador", | |
| "emisor": "EMISSOR", | |
| "nombre": "Nome", | |
| "nif": "NIF", | |
| "direccion": "Endereço", | |
| "cliente": "CLIENTE", | |
| "productos": "PRODUTOS", | |
| "producto": "Produto", | |
| "cantidad": "Quantidade", | |
| "precio_unitario": "Preço Unitário", | |
| "total_producto": "Total", | |
| "descripcion": "Descrição", | |
| "numerico": "Numérico", | |
| "monetario": "Monetário", | |
| "totales": "TOTAIS", | |
| "base_imponible": "Base Tributável", | |
| "iva": "IVA", | |
| "total": "TOTAL", | |
| "informacion": "Informação" | |
| } | |
| } | |
| t = traducciones.get(idioma_destino, traducciones["Inglés"]) | |
| filas = [] | |
| # Información general | |
| filas.append({ | |
| t["seccion"]: t["info_general"], | |
| t["campo"]: t["numero_factura"], | |
| t["valor"]: datos_json.get('numero_factura', 'N/A'), | |
| t["tipo"]: t["identificador"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["info_general"], | |
| t["campo"]: t["fecha"], | |
| t["valor"]: datos_json.get('fecha', 'N/A'), | |
| t["tipo"]: t["fecha"] | |
| }) | |
| # Emisor | |
| if 'emisor' in datos_json: | |
| emisor = datos_json['emisor'] | |
| if isinstance(emisor, dict): | |
| for key, value in emisor.items(): | |
| campo_traducido = t.get(key, key.replace('_', ' ').title()) | |
| filas.append({ | |
| t["seccion"]: t["emisor"], | |
| t["campo"]: campo_traducido, | |
| t["valor"]: str(value), | |
| t["tipo"]: t["informacion"] | |
| }) | |
| # Cliente | |
| if 'cliente' in datos_json: | |
| cliente = datos_json['cliente'] | |
| if isinstance(cliente, dict): | |
| for key, value in cliente.items(): | |
| campo_traducido = t.get(key, key.replace('_', ' ').title()) | |
| filas.append({ | |
| t["seccion"]: t["cliente"], | |
| t["campo"]: campo_traducido, | |
| t["valor"]: str(value), | |
| t["tipo"]: t["informacion"] | |
| }) | |
| # Productos | |
| productos = datos_json.get('productos', datos_json.get('conceptos', datos_json.get('items', []))) | |
| if productos and len(productos) > 0: | |
| for i, prod in enumerate(productos, 1): | |
| filas.append({ | |
| t["seccion"]: t["productos"], | |
| t["campo"]: f'{t["producto"]} {i}', | |
| t["valor"]: prod.get('descripcion', 'N/A'), | |
| t["tipo"]: t["descripcion"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["productos"], | |
| t["campo"]: f'{t["cantidad"]} P{i}', | |
| t["valor"]: str(prod.get('cantidad', '')), | |
| t["tipo"]: t["numerico"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["productos"], | |
| t["campo"]: f'{t["precio_unitario"]} P{i}', | |
| t["valor"]: f"{prod.get('precio_unitario', 0)}", | |
| t["tipo"]: t["monetario"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["productos"], | |
| t["campo"]: f'{t["total_producto"]} P{i}', | |
| t["valor"]: f"{prod.get('total', 0)}", | |
| t["tipo"]: t["monetario"] | |
| }) | |
| # Totales | |
| totales = datos_json.get('totales', {}) | |
| if totales or 'base_imponible' in datos_json or 'total' in datos_json: | |
| base = totales.get('base_imponible', datos_json.get('base_imponible', 0)) | |
| iva = totales.get('iva', datos_json.get('iva', 0)) | |
| porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0)) | |
| total = totales.get('total', datos_json.get('total', 0)) | |
| filas.append({ | |
| t["seccion"]: t["totales"], | |
| t["campo"]: t["base_imponible"], | |
| t["valor"]: f"{base}", | |
| t["tipo"]: t["monetario"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["totales"], | |
| t["campo"]: f'{t["iva"]} ({porcentaje_iva}%)', | |
| t["valor"]: f"{iva}", | |
| t["tipo"]: t["monetario"] | |
| }) | |
| filas.append({ | |
| t["seccion"]: t["totales"], | |
| t["campo"]: t["total"], | |
| t["valor"]: f"{total}", | |
| t["tipo"]: t["monetario"] | |
| }) | |
| df_traducido = pd.DataFrame(filas) | |
| # Guardar CSV | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| csv_filename = f"factura_traducida_{idioma_destino}_{timestamp}.csv" | |
| df_traducido.to_csv(csv_filename, index=False, encoding='utf-8-sig', sep=',') | |
| return texto_traducido, df_traducido, csv_filename | |
| # ============= DETECTOR DE FRAUDE ============= | |
| def detectar_fraude_factura(datos_json, texto, client): | |
| """Analiza la factura en busca de señales de fraude o irregularidades""" | |
| prompt = f"""Analiza esta factura y detecta posibles señales de fraude o irregularidades: | |
| DATOS JSON: {json.dumps(datos_json, indent=2)} | |
| TEXTO: {texto[:2000]} | |
| Busca: | |
| - Números de factura duplicados o sospechosos | |
| - Importes inusuales | |
| - Datos inconsistentes | |
| - Falta de información obligatoria | |
| - Patrones irregulares | |
| Responde en formato JSON: | |
| {{ | |
| "nivel_riesgo": "bajo/medio/alto", | |
| "alertas": ["alerta1", "alerta2"], | |
| "recomendacion": "texto" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.2 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"nivel_riesgo": "bajo", "alertas": [], "recomendacion": "No se detectaron irregularidades evidentes"} | |
| # ============= PREDICCIÓN DE FECHA DE PAGO ============= | |
| def predecir_fecha_pago(datos_json, client): | |
| """Predice la mejor fecha de pago basándose en condiciones de la factura""" | |
| prompt = f"""Basándote en esta factura, sugiere la fecha óptima de pago: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Considera: | |
| - Fecha de emisión | |
| - Plazos habituales (30, 60, 90 días) | |
| - Descuentos por pronto pago | |
| - Recargos por mora | |
| Responde en JSON: | |
| {{ | |
| "fecha_sugerida": "DD/MM/YYYY", | |
| "razon": "explicación breve", | |
| "ahorro_posible": "cantidad o N/A" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=300, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"fecha_sugerida": "N/A", "razon": "No se pudo calcular", "ahorro_posible": "N/A"} | |
| # ============= GENERADOR DE RESUMEN EJECUTIVO ============= | |
| def generar_resumen_ejecutivo(datos_json, client): | |
| """Genera un resumen ejecutivo tipo dashboard para gerencia""" | |
| prompt = f"""Crea un resumen ejecutivo profesional de esta factura: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Incluye: | |
| - Resumen en 2-3 líneas | |
| - Puntos clave financieros | |
| - Impacto en presupuesto | |
| - Acción requerida | |
| Formato profesional y conciso:""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.4 | |
| ) | |
| return response.choices[0].message.content | |
| except: | |
| return "No se pudo generar el resumen ejecutivo" | |
| # ============= ANÁLISIS DE DUPLICADOS ============= | |
| def detectar_facturas_duplicadas(datos_json, client): | |
| """Analiza si esta factura puede ser un duplicado""" | |
| prompt = f"""Analiza esta factura y determina indicadores de duplicación: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Busca: | |
| - Patrones de números de factura sospechosos | |
| - Fechas anómalas | |
| - Importes repetitivos | |
| Responde en JSON: | |
| {{ | |
| "posible_duplicado": true/false, | |
| "nivel_confianza": "bajo/medio/alto", | |
| "indicadores": ["indicador1", "indicador2"], | |
| "recomendacion": "texto" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=300, | |
| temperature=0.2 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"posible_duplicado": False, "nivel_confianza": "bajo", "indicadores": [], "recomendacion": "No se detectaron patrones duplicados"} | |
| # ============= CALCULADORA DE IMPACTO PRESUPUESTARIO ============= | |
| def calcular_impacto_presupuesto(datos_json, client): | |
| """Calcula el impacto de esta factura en un presupuesto mensual promedio""" | |
| total = datos_json.get('totales', {}).get('total', datos_json.get('total', 0)) | |
| prompt = f"""Analiza el impacto presupuestario de esta factura: | |
| Total: {total}€ | |
| Datos: {json.dumps(datos_json, indent=2)} | |
| Calcula: | |
| - Porcentaje sobre presupuesto promedio PYME (10.000€/mes) | |
| - Nivel de impacto | |
| - Recomendaciones de planificación | |
| Responde en JSON: | |
| {{ | |
| "impacto_porcentaje": number, | |
| "nivel_impacto": "bajo/medio/alto/crítico", | |
| "analisis": "texto", | |
| "recomendacion_financiera": "texto" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"impacto_porcentaje": 0, "nivel_impacto": "bajo", "analisis": "No disponible", "recomendacion_financiera": "Consulte con su contador"} | |
| # ============= GENERADOR DE RECORDATORIOS ============= | |
| def generar_recordatorios_pago(datos_json, client): | |
| """Genera recordatorios inteligentes de pago""" | |
| prompt = f"""Basándote en esta factura, genera un plan de recordatorios de pago: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Crea: | |
| - 3 recordatorios (inicial, intermedio, urgente) | |
| - Fechas sugeridas | |
| - Mensajes personalizados | |
| Responde en JSON: | |
| {{ | |
| "recordatorios": [ | |
| {{"tipo": "inicial", "dias_antes": number, "mensaje": "texto"}}, | |
| {{"tipo": "intermedio", "dias_antes": number, "mensaje": "texto"}}, | |
| {{"tipo": "urgente", "dias_antes": number, "mensaje": "texto"}} | |
| ] | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=500, | |
| temperature=0.4 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"recordatorios": []} | |
| # ============= ANÁLISIS DE CONDICIONES DE PAGO ============= | |
| def analizar_condiciones_pago(datos_json, texto, client): | |
| """Analiza las condiciones de pago y sugiere negociaciones""" | |
| prompt = f"""Analiza las condiciones de pago de esta factura: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| TEXTO: {texto[:2000]} | |
| Identifica: | |
| - Plazo de pago actual | |
| - Condiciones especiales | |
| - Oportunidades de negociación | |
| - Descuentos por pronto pago | |
| Responde en JSON: | |
| {{ | |
| "plazo_actual": "texto", | |
| "condiciones_especiales": ["condicion1", "condicion2"], | |
| "oportunidades_negociacion": "texto", | |
| "sugerencias_mejora": "texto" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"plazo_actual": "N/A", "condiciones_especiales": [], "oportunidades_negociacion": "No detectadas", "sugerencias_mejora": "Revisar manualmente"} | |
| # ============= COMPARADOR CON MERCADO ============= | |
| def comparar_precios_mercado(datos_json, client): | |
| """Compara los precios de la factura con precios de mercado promedio""" | |
| productos = datos_json.get('productos', []) | |
| if not productos: | |
| return {"analisis": "No hay productos para comparar"} | |
| productos_texto = "\n".join([f"- {p.get('descripcion', 'N/A')}: {p.get('precio_unitario', 0)}€" for p in productos[:5]]) | |
| prompt = f"""Analiza si estos precios son razonables comparados con el mercado: | |
| PRODUCTOS Y PRECIOS: | |
| {productos_texto} | |
| Determina: | |
| - ¿Los precios son competitivos? | |
| - ¿Hay precios excesivamente altos? | |
| - Recomendaciones | |
| Responde en JSON: | |
| {{ | |
| "evaluacion_general": "competitivo/normal/elevado", | |
| "productos_caros": ["producto1", "producto2"], | |
| "ahorro_potencial": number, | |
| "recomendacion": "texto" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"evaluacion_general": "normal", "productos_caros": [], "ahorro_potencial": 0, "recomendacion": "Precios dentro del rango esperado"} | |
| # ============= VALIDADOR DE DATOS FISCALES ============= | |
| def validar_datos_fiscales(datos_json, client): | |
| """Valida que los datos fiscales sean correctos y completos""" | |
| prompt = f"""Valida los datos fiscales de esta factura: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| Verifica: | |
| - NIF/CIF válido (formato español) | |
| - IVA correcto (21%, 10%, 4%) | |
| - Datos obligatorios presentes | |
| - Formato de factura legal | |
| Responde en JSON: | |
| {{ | |
| "es_valida": true/false, | |
| "errores": ["error1", "error2"], | |
| "advertencias": ["advertencia1"], | |
| "nivel_cumplimiento": "completo/parcial/insuficiente" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=400, | |
| temperature=0.2 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"es_valida": True, "errores": [], "advertencias": [], "nivel_cumplimiento": "completo"} | |
| def extraer_gastos_deducibles(datos_json, texto, client): | |
| """Identifica qué parte de la factura es deducible fiscalmente""" | |
| prompt = f"""Analiza esta factura e identifica los gastos deducibles fiscalmente en España: | |
| DATOS: {json.dumps(datos_json, indent=2)} | |
| TEXTO: {texto[:2000]} | |
| Responde en JSON: | |
| {{ | |
| "porcentaje_deducible": number, | |
| "importe_deducible": number, | |
| "tipo_deduccion": "texto", | |
| "explicacion": "texto breve" | |
| }}""" | |
| try: | |
| response = client.chat.completions.create( | |
| model="Qwen/Qwen2.5-72B-Instruct", | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=300, | |
| temperature=0.3 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado).strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| return json.loads(match.group(0)) | |
| except: | |
| pass | |
| return {"porcentaje_deducible": 0, "importe_deducible": 0, "tipo_deduccion": "N/A", "explicacion": "Consulta con un asesor fiscal"} | |
| # ============= ANALIZAR CON LLM Y CONVERTIR A JSON ============= | |
| def analizar_y_convertir_json(texto): | |
| """El LLM lee la factura y devuelve JSON estructurado""" | |
| token = os.getenv("aa") | |
| if not token: | |
| return None, None, "Error: Falta configurar HF_TOKEN en Settings → Secrets" | |
| texto_limpio = texto[:8000] | |
| prompt = f"""Eres un experto en análisis de facturas. Lee esta factura y conviértela a JSON. | |
| TEXTO DE LA FACTURA: | |
| {texto_limpio} | |
| INSTRUCCIONES: | |
| 1. Analiza el texto y decide qué información es importante extraer | |
| 2. Crea un JSON estructurado con TODOS los datos que encuentres | |
| 3. Incluye: número de factura, fecha, emisor, cliente, productos/servicios, importes | |
| 4. Para los números: usa formato numérico puro (ejemplo: 250 no "250€") | |
| 5. Si hay tabla de productos, extrae CADA producto con cantidad, precio y total | |
| FORMATO JSON (ajusta según lo que encuentres): | |
| {{ | |
| "numero_factura": "string", | |
| "fecha": "DD/MM/YYYY", | |
| "emisor": {{ | |
| "nombre": "string", | |
| "nif": "string", | |
| "direccion": "string" | |
| }}, | |
| "cliente": {{ | |
| "nombre": "string", | |
| "nif": "string" | |
| }}, | |
| "productos": [ | |
| {{ | |
| "descripcion": "string", | |
| "cantidad": number, | |
| "precio_unitario": number, | |
| "total": number | |
| }} | |
| ], | |
| "totales": {{ | |
| "base_imponible": number, | |
| "iva": number, | |
| "porcentaje_iva": number, | |
| "total": number | |
| }} | |
| }} | |
| Responde SOLO con el JSON válido (sin explicaciones, sin markdown):""" | |
| modelos = [ | |
| "Qwen/Qwen2.5-72B-Instruct", | |
| "meta-llama/Llama-3.2-3B-Instruct", | |
| "mistralai/Mistral-Nemo-Instruct-2407" | |
| ] | |
| for modelo in modelos: | |
| try: | |
| print(f"\nProbando: {modelo}") | |
| client = InferenceClient(token=token) | |
| response = client.chat.completions.create( | |
| model=modelo, | |
| messages=[{"role": "user", "content": prompt}], | |
| max_tokens=2000, | |
| temperature=0.1 | |
| ) | |
| resultado = response.choices[0].message.content | |
| resultado = resultado.strip() | |
| resultado = re.sub(r'```json\s*', '', resultado) | |
| resultado = re.sub(r'```\s*', '', resultado) | |
| resultado = resultado.strip() | |
| match = re.search(r'\{.*\}', resultado, re.DOTALL) | |
| if match: | |
| json_str = match.group(0) | |
| try: | |
| datos_json = json.loads(json_str) | |
| print(f"JSON válido extraído con {modelo}") | |
| resumen_util = generar_resumen_util(texto_limpio, modelo, client) | |
| return datos_json, resumen_util, f"Procesado con {modelo}" | |
| except json.JSONDecodeError as e: | |
| print(f"JSON inválido: {str(e)[:50]}") | |
| continue | |
| except Exception as e: | |
| print(f"{modelo} falló: {str(e)[:100]}") | |
| continue | |
| return None, None, "Ningún modelo LLM pudo extraer el JSON. Verifica tu HF_TOKEN." | |
| # ============= GENERAR RESUMEN ÚTIL ============= | |
| def generar_resumen_util(texto, modelo, client): | |
| """Genera un resumen con información útil para administrativos""" | |
| prompt_resumen = f"""Analiza esta factura y proporciona información útil para un administrativo o usuario medio. | |
| TEXTO DE LA FACTURA: | |
| {texto[:6000]} | |
| Genera un resumen estructurado con: | |
| 1. ESTADO DE PAGO: ¿Está pagada? ¿Fecha de vencimiento? | |
| 2. INFORMACIÓN CLAVE: Datos importantes que destacar | |
| 3. ALERTAS: Cualquier aspecto que requiera atención (vencimientos, importes altos, etc.) | |
| 4. RESUMEN EJECUTIVO: Descripción breve y clara de la factura | |
| Responde en español de forma clara y profesional:""" | |
| try: | |
| response = client.chat.completions.create( | |
| model=modelo, | |
| messages=[{"role": "user", "content": prompt_resumen}], | |
| max_tokens=800, | |
| temperature=0.4 | |
| ) | |
| return response.choices[0].message.content | |
| except: | |
| return "No se pudo generar el resumen de información útil." | |
| # ============= CONVERTIR JSON A CSV TABULAR ============= | |
| def json_a_csv(datos_json): | |
| """Convierte el JSON en un DataFrame CSV con formato tabular usando comas""" | |
| if not datos_json: | |
| return None | |
| filas = [] | |
| # Información general | |
| filas.append({ | |
| 'Sección': 'INFORMACIÓN GENERAL', | |
| 'Campo': 'Número de Factura', | |
| 'Valor': datos_json.get('numero_factura', 'N/A'), | |
| 'Tipo': 'Identificador' | |
| }) | |
| filas.append({ | |
| 'Sección': 'INFORMACIÓN GENERAL', | |
| 'Campo': 'Fecha', | |
| 'Valor': datos_json.get('fecha', 'N/A'), | |
| 'Tipo': 'Fecha' | |
| }) | |
| # Emisor | |
| if 'emisor' in datos_json: | |
| emisor = datos_json['emisor'] | |
| if isinstance(emisor, dict): | |
| for key, value in emisor.items(): | |
| filas.append({ | |
| 'Sección': 'EMISOR', | |
| 'Campo': key.replace('_', ' ').title(), | |
| 'Valor': str(value), | |
| 'Tipo': 'Información' | |
| }) | |
| # Cliente | |
| if 'cliente' in datos_json: | |
| cliente = datos_json['cliente'] | |
| if isinstance(cliente, dict): | |
| for key, value in cliente.items(): | |
| filas.append({ | |
| 'Sección': 'CLIENTE', | |
| 'Campo': key.replace('_', ' ').title(), | |
| 'Valor': str(value), | |
| 'Tipo': 'Información' | |
| }) | |
| # Productos | |
| productos = datos_json.get('productos', datos_json.get('conceptos', datos_json.get('items', []))) | |
| if productos and len(productos) > 0: | |
| for i, prod in enumerate(productos, 1): | |
| filas.append({ | |
| 'Sección': 'PRODUCTOS', | |
| 'Campo': f'Producto {i}', | |
| 'Valor': prod.get('descripcion', 'N/A'), | |
| 'Tipo': 'Descripción' | |
| }) | |
| filas.append({ | |
| 'Sección': 'PRODUCTOS', | |
| 'Campo': f'Cantidad P{i}', | |
| 'Valor': str(prod.get('cantidad', '')), | |
| 'Tipo': 'Numérico' | |
| }) | |
| filas.append({ | |
| 'Sección': 'PRODUCTOS', | |
| 'Campo': f'Precio Unitario P{i}', | |
| 'Valor': f"{prod.get('precio_unitario', 0)}", | |
| 'Tipo': 'Monetario' | |
| }) | |
| filas.append({ | |
| 'Sección': 'PRODUCTOS', | |
| 'Campo': f'Total P{i}', | |
| 'Valor': f"{prod.get('total', 0)}", | |
| 'Tipo': 'Monetario' | |
| }) | |
| # Totales | |
| totales = datos_json.get('totales', {}) | |
| if totales or 'base_imponible' in datos_json or 'total' in datos_json: | |
| base = totales.get('base_imponible', datos_json.get('base_imponible', 0)) | |
| iva = totales.get('iva', datos_json.get('iva', 0)) | |
| porcentaje_iva = totales.get('porcentaje_iva', datos_json.get('porcentaje_iva', 0)) | |
| total = totales.get('total', datos_json.get('total', 0)) | |
| filas.append({ | |
| 'Sección': 'TOTALES', | |
| 'Campo': 'Base Imponible', | |
| 'Valor': f"{base}", | |
| 'Tipo': 'Monetario' | |
| }) | |
| filas.append({ | |
| 'Sección': 'TOTALES', | |
| 'Campo': f'IVA ({porcentaje_iva}%)', | |
| 'Valor': f"{iva}", | |
| 'Tipo': 'Monetario' | |
| }) | |
| filas.append({ | |
| 'Sección': 'TOTALES', | |
| 'Campo': 'TOTAL', | |
| 'Valor': f"{total}", | |
| 'Tipo': 'Monetario' | |
| }) | |
| return pd.DataFrame(filas) | |
| # ============= GENERAR PDF TEMPLATES ============= | |
| def generar_pdf_clasico(csv_file, datos_json): | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| pdf_filename = f"factura_clasica_{timestamp}.pdf" | |
| doc = SimpleDocTemplate(pdf_filename, pagesize=A4) | |
| story = [] | |
| styles = getSampleStyleSheet() | |
| titulo_style = ParagraphStyle('CustomTitle', parent=styles['Heading1'], fontSize=24, | |
| textColor=colors.HexColor('#1a1a1a'), spaceAfter=30, alignment=TA_CENTER) | |
| story.append(Paragraph("FACTURA", titulo_style)) | |
| story.append(Spacer(1, 0.3*inch)) | |
| info_data = [['Número de Factura:', datos_json.get('numero_factura', 'N/A')], | |
| ['Fecha:', datos_json.get('fecha', 'N/A')]] | |
| info_table = Table(info_data, colWidths=[2*inch, 4*inch]) | |
| info_table.setStyle(TableStyle([('FONTNAME', (0, 0), (-1, -1), 'Helvetica'), ('FONTSIZE', (0, 0), (-1, -1), 11)])) | |
| story.append(info_table) | |
| doc.build(story) | |
| return pdf_filename | |
| def generar_pdf_moderno(csv_file, datos_json): | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| pdf_filename = f"factura_moderna_{timestamp}.pdf" | |
| doc = SimpleDocTemplate(pdf_filename, pagesize=A4) | |
| story = [] | |
| styles = getSampleStyleSheet() | |
| titulo_style = ParagraphStyle('ModernTitle', parent=styles['Heading1'], fontSize=32, | |
| textColor=colors.HexColor('#2196F3'), spaceAfter=10, alignment=TA_LEFT, fontName='Helvetica-Bold') | |
| story.append(Paragraph("FACTURA", titulo_style)) | |
| doc.build(story) | |
| return pdf_filename | |
| def generar_pdf_elegante(csv_file, datos_json): | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| pdf_filename = f"factura_elegante_{timestamp}.pdf" | |
| doc = SimpleDocTemplate(pdf_filename, pagesize=A4) | |
| story = [] | |
| styles = getSampleStyleSheet() | |
| header_style = ParagraphStyle('ElegantHeader', parent=styles['Heading1'], fontSize=28, | |
| textColor=colors.HexColor('#1a237e'), spaceAfter=5, alignment=TA_CENTER, fontName='Helvetica-Bold') | |
| story.append(Paragraph("F A C T U R A", header_style)) | |
| doc.build(story) | |
| return pdf_filename | |
| # ============= FUNCIÓN PRINCIPAL ============= | |
| def procesar_factura(pdf_file): | |
| if pdf_file is None: | |
| return "", None, None, "", "", None, None, pdf_file | |
| print("\n--- Extrayendo texto del PDF...") | |
| texto = extraer_texto_pdf(pdf_file) | |
| if texto.startswith("Error"): | |
| return "", None, None, "", f"Error: {texto}", None, None, None | |
| texto_preview = f"{texto[:1500]}..." if len(texto) > 1500 else texto | |
| print("--- El LLM está analizando la factura y creando el JSON...") | |
| datos_json, resumen_util, mensaje = analizar_y_convertir_json(texto) | |
| if not datos_json: | |
| return texto_preview, None, None, "", mensaje, None, None, pdf_file | |
| print("--- Convirtiendo JSON a CSV tabular...") | |
| df = json_a_csv(datos_json) | |
| timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') | |
| numero = datos_json.get('numero_factura', 'factura') | |
| numero = re.sub(r'[^\w\-]', '_', str(numero)) | |
| csv_filename = f"{numero}_{timestamp}.csv" | |
| # Guardar CSV con comas como separador | |
| df.to_csv(csv_filename, index=False, encoding='utf-8-sig', sep=',') | |
| resumen_tecnico = f"""## Factura Procesada Exitosamente | |
| **Consulta más información abajo** | |
| --- | |
| ### Estructura JSON Generada | |
| ```json | |
| {json.dumps(datos_json, indent=2, ensure_ascii=False)} | |
| ``` | |
| --- | |
| ### Información del Archivo CSV | |
| **Nombre del archivo:** `{csv_filename}` | |
| **Total de filas:** {len(df)} | |
| **Formato:** UTF-8 con BOM, separador: coma (,) | |
| --- | |
| ### Datos Principales Extraídos | |
| **Número de factura:** {datos_json.get('numero_factura', 'N/A')} | |
| **Fecha de emisión:** {datos_json.get('fecha', 'N/A')} | |
| **Productos/Servicios:** {len(datos_json.get('productos', datos_json.get('conceptos', [])))} items | |
| **Importe total:** {datos_json.get('totales', {}).get('total', datos_json.get('total', 'N/A'))} EUR | |
| """ | |
| print(f"--- CSV guardado: {csv_filename}") | |
| return texto_preview, df, csv_filename, resumen_tecnico, resumen_util, datos_json, csv_filename, pdf_file | |
| # ============= GENERAR PDF CON TEMPLATE SELECCIONADO ============= | |
| def generar_pdf_con_template(template, csv_file, datos_json): | |
| if not datos_json: | |
| return None, "Error: Primero debes procesar una factura" | |
| try: | |
| if template == "Clásico": | |
| pdf_file = generar_pdf_clasico(csv_file, datos_json) | |
| elif template == "Moderno": | |
| pdf_file = generar_pdf_moderno(csv_file, datos_json) | |
| elif template == "Elegante": | |
| pdf_file = generar_pdf_elegante(csv_file, datos_json) | |
| else: | |
| return None, "Template no válido" | |
| return pdf_file, f"PDF generado exitosamente: {pdf_file}" | |
| except Exception as e: | |
| return None, f"Error al generar PDF: {str(e)}" | |
| # ============= INTERFAZ GRADIO ============= | |
| with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo: | |
| datos_json_state = gr.State() | |
| csv_file_state = gr.State() | |
| pdf_path_state = gr.State() | |
| texto_state = gr.State() | |
| gr.Markdown(""" | |
| # FACTULAB | |
| ### Extrae datos de facturas PDF con IA, rápido y sin complicaciones. | |
| """) | |
| gr.Markdown("---") | |
| with gr.Tabs(): | |
| # ============= TAB 1: EXTRACCIÓN AUTOMÁTICA ============= | |
| with gr.Tab("Extracción Automática"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Subir Factura PDF") | |
| pdf_input = gr.File(label="Seleccionar factura PDF", file_types=[".pdf"], type="filepath") | |
| btn_extraer = gr.Button(" Extraer Datos de la Factura", variant="primary", size="lg") | |
| # Indicador de carga silencioso | |
| loading_extraccion = gr.HTML(visible=False, value=""" | |
| <div style="text-align: center; padding: 20px;"> | |
| <div class="spinner"></div> | |
| <p style="margin-top: 10px; color: #2196F3; font-weight: bold;"> | |
| Procesando tu factura... | |
| </p> | |
| </div> | |
| <style> | |
| .spinner { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #2196F3; | |
| border-radius: 50%; | |
| width: 35px; | |
| height: 35px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| </style> | |
| """) | |
| gr.Markdown("---") | |
| gr.Markdown("### Descarga tu factura en formato CSV") | |
| csv_output = gr.File(label="CSV Tabular (separado por comas)") | |
| gr.Markdown("---") | |
| gr.Markdown("### Rediseña tu PDF con un template") | |
| template_selector = gr.Radio( | |
| choices=["Clásico", "Moderno", "Elegante"], | |
| value="Moderno", | |
| label="Estilo de factura" | |
| ) | |
| btn_generar_pdf = gr.Button("Generar Factura PDF", variant="secondary", size="lg") | |
| pdf_output = gr.File(label="Descargar PDF generado") | |
| pdf_status = gr.Textbox(label="Estado", interactive=False, lines=2) | |
| with gr.Column(scale=2): | |
| gr.Markdown("### ") | |
| info_util = gr.Markdown(value="*Aquí aparecerá información una vez procesada la factura*") | |
| gr.Markdown("---") | |
| with gr.Tabs(): | |
| with gr.Tab("Vista previa CSV"): | |
| tabla_preview = gr.DataFrame(label="Datos estructurados en formato tabular", wrap=True) | |
| with gr.Tab("Texto procesado de tu PDF"): | |
| texto_extraido = gr.Textbox(label="Texto extraído del PDF", lines=18) | |
| with gr.Tab("Más información"): | |
| resumen_tecnico = gr.Markdown(label="Estructura de datos y metadatos") | |
| # ============= TAB 2: ASISTENTE IA CON VOZ Y AVATAR ============= | |
| # ============= TAB 2: ASISTENTE IA CON ANÁLISIS EMOCIONAL ============= | |
| with gr.Tab(" Pregunta a la IA sobre tu factura"): | |
| gr.Markdown(""" | |
| # Modelo base IA | |
| ### Pregúntale cualquier cosa sobre tu factura | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| pregunta_ia = gr.Textbox( | |
| label="Tu pregunta ", | |
| placeholder="Ejemplo: ¿Cuál es el total de esta factura?", | |
| value="¿Cuál es el total de esta factura y cuándo debería pagarla?", | |
| lines=4 | |
| ) | |
| btn_consulta_ia = gr.Button("Consultar", variant="primary", size="lg") | |
| # Indicador de carga | |
| loading_ia = gr.HTML(visible=False, value=""" | |
| <div style="text-align: center; padding: 20px;"> | |
| <div class="spinner-ia"></div> | |
| <p style="margin-top: 10px; color: #9C27B0; font-weight: bold;"> | |
| El asistente está analizando... | |
| </p> | |
| </div> | |
| <style> | |
| .spinner-ia { | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #9C27B0; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 0.8s linear infinite; | |
| margin: 0 auto; | |
| } | |
| </style> | |
| """) | |
| gr.Markdown("---") | |
| gr.Markdown("#### Ejemplos de preguntas:") | |
| gr.Markdown(""" | |
| - ¿Cuál es el total de la factura? | |
| - ¿Qué es la base imponible? | |
| - ¿Cuándo debo pagar esta factura? | |
| - ¿Hay algún descuento aplicado? | |
| - ¿Quién emitió esta factura? | |
| """) | |
| # Indicador de emoción | |
| with gr.Column(scale=2): | |
| gr.Markdown("### ") | |
| gr.Markdown("### Respuesta a tu consulta") | |
| resultado_ia = gr.Markdown( | |
| value="*Haz una pregunta y el asistente te responderá aquí...*" | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown("### Lectura automática de la respuesta") | |
| with gr.Row(): | |
| with gr.Column(): | |
| audio_respuesta = gr.Audio( | |
| label=" Reproducir respuesta en audio", | |
| type="filepath", | |
| visible=True, | |
| autoplay=True | |
| ) | |
| with gr.Column(): | |
| transcripcion_output = gr.File( | |
| label=" Descargar Transcripción (TXT)" | |
| ) | |
| # ============= TAB 3: HERRAMIENTAS IA AVANZADAS ============= | |
| with gr.Tab("Consulta el analisis inteligente de tu factura"): | |
| gr.Markdown(""" | |
| #### Verifica la información sensible | |
| """) | |
| with gr.Tabs(): | |
| # Sub-tab 1: Análisis Financiero | |
| with gr.Tab(" Análisis IA"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### ") | |
| btn_sentimiento = gr.Button(" Riesgos", variant="primary") | |
| resultado_sentimiento = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_deducibles = gr.Button(" Calcular Deducciones", variant="primary") | |
| resultado_deducibles = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_impacto = gr.Button(" Impacto presupuestario", variant="primary") | |
| resultado_impacto = gr.Markdown() | |
| with gr.Column(): | |
| gr.Markdown("### ") | |
| btn_prediccion = gr.Button(" Predicción de pago", variant="primary") | |
| resultado_prediccion = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_sugerencias = gr.Button(" Generar Recomendaciones", variant="primary") | |
| resultado_sugerencias = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_categoria = gr.Button(" Clasificar Gasto", variant="primary") | |
| resultado_categoria = gr.Markdown() | |
| # Sub-tab 2: Seguridad y Validación | |
| with gr.Tab(" Análisis de riesgos"): | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### ") | |
| btn_fraude = gr.Button("Irregularidades", variant="primary") | |
| resultado_fraude = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_validador = gr.Button(" Datos Fiscales", variant="primary") | |
| resultado_validador = gr.Markdown() | |
| with gr.Column(): | |
| gr.Markdown("### ") | |
| btn_condiciones = gr.Button(" Analizar Condiciones", variant="primary") | |
| resultado_condiciones = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_recordatorios = gr.Button(" Generar Recordatorios de pago", variant="primary") | |
| resultado_recordatorios = gr.Markdown() | |
| gr.Markdown("---") | |
| gr.Markdown("### ") | |
| btn_ejecutivo = gr.Button(" Dashboard Básico", variant="primary") | |
| resultado_ejecutivo = gr.Markdown() | |
| # Sub-tab 3: Comparación y Mercado | |
| with gr.Tab(" Análisis IA de Mercado"): | |
| gr.Markdown("### Comparador de Precios con Mercado") | |
| btn_mercado = gr.Button(" Analizar", variant="primary", size="lg") | |
| resultado_mercado = gr.Markdown() | |
| # ============= TAB 4: TRADUCCIÓN MULTIIDIOMA CON TABLA ============= | |
| with gr.Tab(" Traduce tu factura a otro idioma"): | |
| gr.Markdown(""" | |
| # | |
| ### Traduce tu factura a 5 idiomas con vista tabular y exporta a CSV | |
| """) | |
| with gr.Row(): | |
| with gr.Column(): | |
| gr.Markdown("### Seleccionar Idioma") | |
| idioma_selector = gr.Dropdown( | |
| choices=["Inglés", "Francés", "Alemán", "Italiano", "Portugués"], | |
| value="Inglés", | |
| label=" Selecciona un idioma" | |
| ) | |
| btn_traducir = gr.Button(" Traducir Factura", variant="primary", size="lg") | |
| gr.Markdown("---") | |
| gr.Markdown("### Exportar Traducción") | |
| csv_traduccion_output = gr.File(label=" Descargar CSV Tabular Traducido") | |
| with gr.Column(): | |
| gr.Markdown("### Vista Tabular Traducida") | |
| tabla_traduccion = gr.DataFrame( | |
| label="Factura traducida en formato tabular", | |
| wrap=True | |
| ) | |
| gr.Markdown("---") | |
| gr.Markdown("### Texto Traducido") | |
| resultado_traduccion = gr.Textbox( | |
| label="Resumen en texto", | |
| lines=10, | |
| placeholder="La traducción aparecerá aquí..." | |
| ) | |
| gr.Markdown("---") | |
| # ============= CONECTAR EVENTOS ============= | |
| # Extracción automática | |
| def procesar_con_loading(pdf_file): | |
| if pdf_file is None: | |
| return "", None, None, "", "", None, None, None, gr.update(visible=False) | |
| yield "", None, None, "", "", None, None, None, gr.update(visible=True) | |
| time.sleep(0.5) | |
| resultado = procesar_factura(pdf_file) | |
| yield (*resultado, gr.update(visible=False)) | |
| btn_extraer.click( | |
| fn=procesar_con_loading, | |
| inputs=[pdf_input], | |
| outputs=[texto_extraido, tabla_preview, csv_output, resumen_tecnico, info_util, | |
| datos_json_state, csv_file_state, pdf_path_state, loading_extraccion] | |
| ) | |
| # Generar PDF | |
| btn_generar_pdf.click( | |
| fn=generar_pdf_con_template, | |
| inputs=[template_selector, csv_file_state, datos_json_state], | |
| outputs=[pdf_output, pdf_status] | |
| ) | |
| def consultar_ia_con_loading(texto, pregunta): | |
| if not texto: | |
| return ("❌ Por favor, procesa una factura primero", None, None, gr.update(visible=False)) | |
| yield ("🔄 El asistente está analizando tu pregunta...", None, None, gr.update(visible=True)) | |
| time.sleep(0.3) | |
| try: | |
| respuesta, audio, transcripcion = asistente_ia_factura(texto, pregunta) | |
| audio_final = audio if (audio and os.path.exists(audio) and os.path.getsize(audio) > 100) else None | |
| if audio_final: | |
| print(f"✅ Audio disponible: {audio_final}") | |
| else: | |
| print("⚠️ Audio no disponible") | |
| emocion_info += "\n\n⚠️ *El audio no pudo generarse, pero la respuesta está en texto.*" | |
| yield (respuesta, audio_final, transcripcion, gr.update(visible=False)) | |
| except Exception as e: | |
| error_msg = f"❌ Error: {str(e)[:200]}" | |
| print(f"Error completo: {str(e)}") | |
| yield (error_msg, None, None, gr.update(visible=False)) | |
| btn_consulta_ia.click( | |
| fn=consultar_ia_con_loading, | |
| inputs=[texto_extraido, pregunta_ia], | |
| outputs=[resultado_ia, audio_respuesta, transcripcion_output, loading_ia] | |
| ) | |
| # Funciones de análisis avanzado | |
| def ejecutar_sentimiento(texto): | |
| if not texto: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = analizar_sentimiento_factura(texto, client) | |
| emoji_sentimiento = {"positivo": "✅", "neutral": "⚪", "alerta": "⚠️"} | |
| emoji_urgencia = {"alta": "🔴", "media": "🟡", "baja": "🟢"} | |
| return f"""### {emoji_sentimiento.get(resultado['sentimiento'], '⚪')} Análisis de Sentimiento | |
| **Estado:** {resultado['sentimiento'].upper()} | |
| **Urgencia:** {emoji_urgencia.get(resultado['urgencia'], '⚪')} {resultado['urgencia'].upper()} | |
| **Razón:** {resultado['razon']} | |
| **Recomendación:** {resultado['recomendacion']}""" | |
| def ejecutar_fraude(datos_json, texto): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = detectar_fraude_factura(datos_json, texto, client) | |
| nivel_color = {"bajo": "🟢", "medio": "🟡", "alto": "🔴"} | |
| alertas_texto = "\n".join([f"- {alerta}" for alerta in resultado.get('alertas', [])]) | |
| return f"""### {nivel_color.get(resultado['nivel_riesgo'], '⚪')} Detección de Fraude | |
| **Nivel de Riesgo:** {resultado['nivel_riesgo'].upper()} | |
| **Alertas Detectadas:** | |
| {alertas_texto if alertas_texto else "- No se detectaron alertas"} | |
| **Recomendación:** {resultado['recomendacion']}""" | |
| def ejecutar_deducibles(datos_json, texto): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = extraer_gastos_deducibles(datos_json, texto, client) | |
| return f"""### 💰 Análisis de Gastos Deducibles | |
| **Porcentaje Deducible:** {resultado['porcentaje_deducible']}% | |
| **Importe Deducible:** {resultado['importe_deducible']}€ | |
| **Tipo de Deducción:** {resultado['tipo_deduccion']} | |
| **Explicación:** {resultado['explicacion']} | |
| ⚠️ *Nota: Esta es una estimación. Consulta con tu asesor fiscal.*""" | |
| def ejecutar_sugerencias(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| return f"### 💡 Sugerencias Personalizadas\n\n{generar_sugerencias_ia(datos_json, client)}" | |
| def ejecutar_categoria(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| return f"### 🏷️ Categorización Automática\n\n{extraer_categorias_gasto(datos_json, client)}" | |
| def ejecutar_prediccion(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = predecir_fecha_pago(datos_json, client) | |
| return f"""### 📅 Predicción de Fecha de Pago Óptima | |
| **Fecha Sugerida:** {resultado['fecha_sugerida']} | |
| **Razón:** {resultado['razon']} | |
| **Ahorro Posible:** {resultado['ahorro_posible']} | |
| 💡 Pagar en la fecha sugerida puede optimizar tu flujo de caja.""" | |
| def ejecutar_ejecutivo(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| return f"### Resumen Ejecutivo - Dashboard\n\n{generar_resumen_ejecutivo(datos_json, client)}" | |
| # Traducción completa con tabla | |
| def ejecutar_traduccion_completa(texto, datos_json, idioma): | |
| if not texto: | |
| return "❌ Procesa una factura primero", None, None | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración", None, None | |
| client = InferenceClient(token=token) | |
| texto_traducido, df_traducido, csv_filename = traducir_factura_con_csv(datos_json, texto, idioma, client) | |
| return texto_traducido, df_traducido, csv_filename | |
| def ejecutar_duplicados(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = detectar_facturas_duplicadas(datos_json, client) | |
| return f"""### 🔄 Análisis de Duplicados | |
| **¿Es posible duplicado?** {'✅ SÍ' if resultado['posible_duplicado'] else '❌ NO'} | |
| **Nivel de confianza:** {resultado['nivel_confianza'].upper()} | |
| **Indicadores:** | |
| {chr(10).join([f"- {ind}" for ind in resultado.get('indicadores', [])]) if resultado.get('indicadores') else '- No se detectaron indicadores'} | |
| **Recomendación:** {resultado['recomendacion']}""" | |
| def ejecutar_impacto(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = calcular_impacto_presupuesto(datos_json, client) | |
| nivel_emoji = {"bajo": "🟢", "medio": "🟡", "alto": "🟠", "crítico": "🔴"} | |
| return f"""### 📊 Impacto Presupuestario | |
| **Porcentaje del presupuesto:** {resultado['impacto_porcentaje']}% | |
| **Nivel de impacto:** {nivel_emoji.get(resultado['nivel_impacto'], '⚪')} {resultado['nivel_impacto'].upper()} | |
| **Análisis:** {resultado['analisis']} | |
| **Recomendación Financiera:** {resultado['recomendacion_financiera']}""" | |
| def ejecutar_recordatorios(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = generar_recordatorios_pago(datos_json, client) | |
| recordatorios = resultado.get('recordatorios', []) | |
| texto = "### Plan de Recordatorios de Pago\n\n" | |
| for r in recordatorios: | |
| texto += f"**{r.get('tipo', '').upper()}** ({r.get('dias_antes', 0)} días antes):\n" | |
| texto += f"{r.get('mensaje', '')}\n\n" | |
| return texto if recordatorios else "No se pudieron generar recordatorios" | |
| def ejecutar_condiciones(datos_json, texto): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = analizar_condiciones_pago(datos_json, texto, client) | |
| return f"""### Análisis de Condiciones de Pago | |
| **Plazo Actual:** {resultado['plazo_actual']} | |
| **Condiciones Especiales:** | |
| {chr(10).join([f"- {c}" for c in resultado.get('condiciones_especiales', [])]) if resultado.get('condiciones_especiales') else '- No detectadas'} | |
| **Oportunidades de Negociación:** {resultado['oportunidades_negociacion']} | |
| **Sugerencias de Mejora:** {resultado['sugerencias_mejora']}""" | |
| def ejecutar_mercado(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = comparar_precios_mercado(datos_json, client) | |
| eval_emoji = {"competitivo": "✅", "normal": "⚪", "elevado": "⚠️"} | |
| return f"""### 💲 Comparación con Precios de Mercado | |
| **Evaluación General:** {eval_emoji.get(resultado['evaluacion_general'], '⚪')} {resultado['evaluacion_general'].upper()} | |
| **Productos con Precios Elevados:** | |
| {chr(10).join([f"- {p}" for p in resultado.get('productos_caros', [])]) if resultado.get('productos_caros') else '- Todos los precios son razonables'} | |
| **Ahorro Potencial:** {resultado['ahorro_potencial']}€ | |
| **Recomendación:** {resultado['recomendacion']}""" | |
| def ejecutar_validador(datos_json): | |
| if not datos_json: | |
| return "❌ Procesa una factura primero" | |
| token = os.getenv("aa") | |
| if not token: | |
| return "❌ Error de configuración" | |
| client = InferenceClient(token=token) | |
| resultado = validar_datos_fiscales(datos_json, client) | |
| return f"""### Validación de Datos Fiscales | |
| **¿Es válida?** {'✅ SÍ' if resultado['es_valida'] else '❌ NO'} | |
| **Nivel de Cumplimiento:** {resultado['nivel_cumplimiento'].upper()} | |
| **Errores Detectados:** | |
| {chr(10).join([f"- ❌ {e}" for e in resultado.get('errores', [])]) if resultado.get('errores') else '- No se detectaron errores'} | |
| **Advertencias:** | |
| {chr(10).join([f"- ⚠️ {a}" for a in resultado.get('advertencias', [])]) if resultado.get('advertencias') else '- No hay advertencias'}""" | |
| # Conectar funcionalidades | |
| btn_impacto.click(fn=ejecutar_impacto, inputs=[datos_json_state], outputs=[resultado_impacto]) | |
| btn_recordatorios.click(fn=ejecutar_recordatorios, inputs=[datos_json_state], outputs=[resultado_recordatorios]) | |
| btn_condiciones.click(fn=ejecutar_condiciones, inputs=[datos_json_state, texto_extraido], outputs=[resultado_condiciones]) | |
| btn_mercado.click(fn=ejecutar_mercado, inputs=[datos_json_state], outputs=[resultado_mercado]) | |
| btn_validador.click(fn=ejecutar_validador, inputs=[datos_json_state], outputs=[resultado_validador]) | |
| btn_sentimiento.click(fn=ejecutar_sentimiento, inputs=[texto_extraido], outputs=[resultado_sentimiento]) | |
| btn_fraude.click(fn=ejecutar_fraude, inputs=[datos_json_state, texto_extraido], outputs=[resultado_fraude]) | |
| btn_deducibles.click(fn=ejecutar_deducibles, inputs=[datos_json_state, texto_extraido], outputs=[resultado_deducibles]) | |
| btn_sugerencias.click(fn=ejecutar_sugerencias, inputs=[datos_json_state], outputs=[resultado_sugerencias]) | |
| btn_categoria.click(fn=ejecutar_categoria, inputs=[datos_json_state], outputs=[resultado_categoria]) | |
| btn_prediccion.click(fn=ejecutar_prediccion, inputs=[datos_json_state], outputs=[resultado_prediccion]) | |
| btn_ejecutivo.click(fn=ejecutar_ejecutivo, inputs=[datos_json_state], outputs=[resultado_ejecutivo]) | |
| # Traducción con tabla | |
| btn_traducir.click( | |
| fn=ejecutar_traduccion_completa, | |
| inputs=[texto_extraido, datos_json_state, idioma_selector], | |
| outputs=[resultado_traduccion, tabla_traduccion, csv_traduccion_output] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |