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="""
Procesando tu factura...
El asistente está analizando...