Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,6 +13,9 @@ from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph,
|
|
| 13 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 14 |
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
| 15 |
import time
|
|
|
|
|
|
|
|
|
|
| 16 |
|
| 17 |
# ============= EXTRAER TEXTO DEL PDF =============
|
| 18 |
def extraer_texto_pdf(pdf_file):
|
|
@@ -88,18 +91,18 @@ Responde ahora:"""
|
|
| 88 |
|
| 89 |
return "❌ No se pudo obtener respuesta del asistente IA", None
|
| 90 |
|
| 91 |
-
# ============= GENERAR AUDIO DE LA RESPUESTA =============
|
| 92 |
def generar_audio_respuesta(texto, client):
|
| 93 |
-
"""Convierte la respuesta de texto a audio usando TTS
|
| 94 |
|
| 95 |
modelos_tts = [
|
| 96 |
-
"
|
| 97 |
-
"
|
| 98 |
-
"
|
| 99 |
]
|
| 100 |
|
| 101 |
-
# Limitar texto para TTS (máximo
|
| 102 |
-
texto_corto = texto[:
|
| 103 |
|
| 104 |
for modelo in modelos_tts:
|
| 105 |
try:
|
|
@@ -126,6 +129,203 @@ def generar_audio_respuesta(texto, client):
|
|
| 126 |
|
| 127 |
return None
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
# ============= ANALIZAR CON LLM Y CONVERTIR A JSON =============
|
| 130 |
def analizar_y_convertir_json(texto):
|
| 131 |
"""El LLM lee la factura y devuelve JSON estructurado"""
|
|
@@ -311,7 +511,7 @@ def json_a_csv(datos_json):
|
|
| 311 |
|
| 312 |
return pd.DataFrame(filas)
|
| 313 |
|
| 314 |
-
# ============= GENERAR PDF TEMPLATES
|
| 315 |
def generar_pdf_clasico(csv_file, datos_json):
|
| 316 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 317 |
pdf_filename = f"factura_clasica_{timestamp}.pdf"
|
|
@@ -455,24 +655,21 @@ with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo:
|
|
| 455 |
pdf_input = gr.File(label="Seleccionar factura PDF", file_types=[".pdf"], type="filepath")
|
| 456 |
btn_extraer = gr.Button("🚀 Extraer Datos de la Factura", variant="primary", size="lg")
|
| 457 |
|
| 458 |
-
# Indicador de carga para extracción
|
| 459 |
loading_extraccion = gr.HTML(visible=False, value="""
|
| 460 |
<div style="text-align: center; padding: 20px;">
|
| 461 |
<div class="spinner"></div>
|
| 462 |
<p style="margin-top: 10px; color: #2196F3; font-weight: bold;">
|
| 463 |
🔄 Procesando tu factura...
|
| 464 |
</p>
|
| 465 |
-
<audio autoplay loop>
|
| 466 |
-
<source src="https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3" type="audio/mpeg">
|
| 467 |
-
</audio>
|
| 468 |
</div>
|
| 469 |
<style>
|
| 470 |
.spinner {
|
| 471 |
-
border:
|
| 472 |
-
border-top:
|
| 473 |
border-radius: 50%;
|
| 474 |
-
width:
|
| 475 |
-
height:
|
| 476 |
animation: spin 1s linear infinite;
|
| 477 |
margin: 0 auto;
|
| 478 |
}
|
|
@@ -512,7 +709,7 @@ with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo:
|
|
| 512 |
- ✅ Responder preguntas específicas sobre tu factura
|
| 513 |
- ✅ Explicar conceptos contables (IVA, base imponible, etc.)
|
| 514 |
- ✅ Dar consejos sobre gestión y pagos
|
| 515 |
-
- ✅ **Leer la respuesta en voz alta** 🔊
|
| 516 |
""")
|
| 517 |
|
| 518 |
with gr.Row():
|
|
@@ -526,24 +723,21 @@ with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo:
|
|
| 526 |
|
| 527 |
btn_consulta_ia = gr.Button("🎤 Consultar y Escuchar Respuesta", variant="primary", size="lg")
|
| 528 |
|
| 529 |
-
# Indicador de carga para IA
|
| 530 |
loading_ia = gr.HTML(visible=False, value="""
|
| 531 |
<div style="text-align: center; padding: 20px;">
|
| 532 |
<div class="spinner-ia"></div>
|
| 533 |
<p style="margin-top: 10px; color: #9C27B0; font-weight: bold;">
|
| 534 |
🧠 El asistente IA está pensando...
|
| 535 |
</p>
|
| 536 |
-
<audio autoplay loop>
|
| 537 |
-
<source src="https://assets.mixkit.co/active_storage/sfx/2571/2571-preview.mp3" type="audio/mpeg">
|
| 538 |
-
</audio>
|
| 539 |
</div>
|
| 540 |
<style>
|
| 541 |
.spinner-ia {
|
| 542 |
-
border:
|
| 543 |
-
border-top:
|
| 544 |
border-radius: 50%;
|
| 545 |
-
width:
|
| 546 |
-
height:
|
| 547 |
animation: spin 0.8s linear infinite;
|
| 548 |
margin: 0 auto;
|
| 549 |
}
|
|
@@ -578,35 +772,104 @@ with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo:
|
|
| 578 |
|
| 579 |
gr.Markdown("---")
|
| 580 |
gr.Markdown("### 🔊 Escucha la Respuesta")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
audio_respuesta = gr.Audio(
|
| 582 |
-
label="Audio de la respuesta",
|
| 583 |
type="filepath",
|
| 584 |
-
visible=True
|
|
|
|
| 585 |
)
|
| 586 |
|
| 587 |
gr.Markdown("""
|
| 588 |
-
💡 **Tip:**
|
| 589 |
-
- ▶️
|
| 590 |
-
-
|
| 591 |
-
-
|
|
|
|
| 592 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 593 |
|
| 594 |
gr.Markdown("---")
|
| 595 |
gr.Markdown("""
|
| 596 |
### 📚 Guía rápida de uso
|
| 597 |
|
| 598 |
1. **📄 Extracción Automática:** Sube tu PDF y extrae todos los datos automáticamente
|
| 599 |
-
2. **🤖 Asistente IA con Voz:** Haz preguntas y escucha las respuestas en audio
|
|
|
|
|
|
|
| 600 |
|
| 601 |
---
|
| 602 |
|
| 603 |
-
### 🎯 Características
|
| 604 |
|
| 605 |
-
-
|
| 606 |
-
-
|
| 607 |
-
- **💡
|
| 608 |
-
-
|
| 609 |
-
-
|
|
|
|
|
|
|
|
|
|
| 610 |
|
| 611 |
💡 **Empieza por la pestaña "Extracción Automática" para procesar tu factura.**
|
| 612 |
""")
|
|
@@ -645,21 +908,126 @@ with gr.Blocks(title="Extractor de Facturas con IA Avanzada") as demo:
|
|
| 645 |
# Asistente IA con voz y loading
|
| 646 |
def consultar_ia_con_loading(texto, pregunta):
|
| 647 |
if not texto:
|
| 648 |
-
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'", None, gr.update(visible=False)
|
| 649 |
|
| 650 |
# Mostrar loading
|
| 651 |
-
yield "🔄 Consultando al asistente IA...", None, gr.update(visible=True)
|
| 652 |
|
| 653 |
# Procesar consulta
|
|
|
|
| 654 |
respuesta, audio = asistente_ia_factura(texto, pregunta)
|
| 655 |
|
|
|
|
|
|
|
|
|
|
| 656 |
# Ocultar loading y mostrar resultados
|
| 657 |
-
yield respuesta, audio, gr.update(visible=False)
|
| 658 |
|
| 659 |
btn_consulta_ia.click(
|
| 660 |
fn=consultar_ia_con_loading,
|
| 661 |
inputs=[texto_extraido, pregunta_ia],
|
| 662 |
-
outputs=[resultado_ia, audio_respuesta, loading_ia]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 663 |
)
|
| 664 |
|
| 665 |
if __name__ == "__main__":
|
|
|
|
| 13 |
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 14 |
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_LEFT
|
| 15 |
import time
|
| 16 |
+
from PIL import Image
|
| 17 |
+
import io
|
| 18 |
+
import base64
|
| 19 |
|
| 20 |
# ============= EXTRAER TEXTO DEL PDF =============
|
| 21 |
def extraer_texto_pdf(pdf_file):
|
|
|
|
| 91 |
|
| 92 |
return "❌ No se pudo obtener respuesta del asistente IA", None
|
| 93 |
|
| 94 |
+
# ============= GENERAR AUDIO DE LA RESPUESTA CON EMOCIONES =============
|
| 95 |
def generar_audio_respuesta(texto, client):
|
| 96 |
+
"""Convierte la respuesta de texto a audio usando TTS avanzado con emociones"""
|
| 97 |
|
| 98 |
modelos_tts = [
|
| 99 |
+
"facebook/mms-tts-spa", # Mejor calidad para español
|
| 100 |
+
"microsoft/speecht5_tts",
|
| 101 |
+
"suno/bark-small" # Modelo con emociones y naturalidad
|
| 102 |
]
|
| 103 |
|
| 104 |
+
# Limitar texto para TTS (máximo 400 caracteres para mejor calidad)
|
| 105 |
+
texto_corto = texto[:400] if len(texto) > 400 else texto
|
| 106 |
|
| 107 |
for modelo in modelos_tts:
|
| 108 |
try:
|
|
|
|
| 129 |
|
| 130 |
return None
|
| 131 |
|
| 132 |
+
# ============= ANÁLISIS DE SENTIMIENTO DE FACTURA =============
|
| 133 |
+
def analizar_sentimiento_factura(texto, client):
|
| 134 |
+
"""Analiza si la factura tiene alertas, urgencias o problemas"""
|
| 135 |
+
|
| 136 |
+
prompt = f"""Analiza esta factura y determina si hay algo preocupante o urgente.
|
| 137 |
+
TEXTO: {texto[:3000]}
|
| 138 |
+
|
| 139 |
+
Responde en formato JSON:
|
| 140 |
+
{{
|
| 141 |
+
"sentimiento": "positivo/neutral/alerta",
|
| 142 |
+
"urgencia": "alta/media/baja",
|
| 143 |
+
"razon": "explicación breve",
|
| 144 |
+
"recomendacion": "qué hacer"
|
| 145 |
+
}}"""
|
| 146 |
+
|
| 147 |
+
try:
|
| 148 |
+
response = client.chat.completions.create(
|
| 149 |
+
model="Qwen/Qwen2.5-72B-Instruct",
|
| 150 |
+
messages=[{"role": "user", "content": prompt}],
|
| 151 |
+
max_tokens=300,
|
| 152 |
+
temperature=0.3
|
| 153 |
+
)
|
| 154 |
+
|
| 155 |
+
resultado = response.choices[0].message.content
|
| 156 |
+
resultado = re.sub(r'```json\s*', '', resultado)
|
| 157 |
+
resultado = re.sub(r'```\s*', '', resultado).strip()
|
| 158 |
+
|
| 159 |
+
match = re.search(r'\{.*\}', resultado, re.DOTALL)
|
| 160 |
+
if match:
|
| 161 |
+
return json.loads(match.group(0))
|
| 162 |
+
except:
|
| 163 |
+
pass
|
| 164 |
+
|
| 165 |
+
return {"sentimiento": "neutral", "urgencia": "baja", "razon": "Análisis no disponible", "recomendacion": "Revisar manualmente"}
|
| 166 |
+
|
| 167 |
+
# ============= COMPARADOR DE FACTURAS =============
|
| 168 |
+
def comparar_facturas(json1, json2):
|
| 169 |
+
"""Compara dos facturas y muestra diferencias clave"""
|
| 170 |
+
|
| 171 |
+
if not json1 or not json2:
|
| 172 |
+
return "❌ Necesitas procesar dos facturas para compararlas"
|
| 173 |
+
|
| 174 |
+
try:
|
| 175 |
+
total1 = json1.get('totales', {}).get('total', json1.get('total', 0))
|
| 176 |
+
total2 = json2.get('totales', {}).get('total', json2.get('total', 0))
|
| 177 |
+
|
| 178 |
+
diferencia = total2 - total1
|
| 179 |
+
porcentaje = (diferencia / total1 * 100) if total1 > 0 else 0
|
| 180 |
+
|
| 181 |
+
comparacion = f"""## 📊 Comparación de Facturas
|
| 182 |
+
|
| 183 |
+
### Factura 1
|
| 184 |
+
- **Número:** {json1.get('numero_factura', 'N/A')}
|
| 185 |
+
- **Fecha:** {json1.get('fecha', 'N/A')}
|
| 186 |
+
- **Total:** {total1}€
|
| 187 |
+
|
| 188 |
+
### Factura 2
|
| 189 |
+
- **Número:** {json2.get('numero_factura', 'N/A')}
|
| 190 |
+
- **Fecha:** {json2.get('fecha', 'N/A')}
|
| 191 |
+
- **Total:** {total2}€
|
| 192 |
+
|
| 193 |
+
---
|
| 194 |
+
|
| 195 |
+
### 💰 Diferencia
|
| 196 |
+
- **Variación:** {diferencia:+.2f}€ ({porcentaje:+.1f}%)
|
| 197 |
+
- **Análisis:** {"⬆️ Incremento" if diferencia > 0 else "⬇️ Reducción" if diferencia < 0 else "✅ Sin cambios"}
|
| 198 |
+
|
| 199 |
+
"""
|
| 200 |
+
|
| 201 |
+
return comparacion
|
| 202 |
+
|
| 203 |
+
except Exception as e:
|
| 204 |
+
return f"❌ Error al comparar: {str(e)}"
|
| 205 |
+
|
| 206 |
+
# ============= SUGERENCIAS INTELIGENTES =============
|
| 207 |
+
def generar_sugerencias_ia(datos_json, client):
|
| 208 |
+
"""Genera sugerencias personalizadas basadas en la factura"""
|
| 209 |
+
|
| 210 |
+
prompt = f"""Basándote en esta factura, da 3 sugerencias útiles y prácticas:
|
| 211 |
+
|
| 212 |
+
DATOS: {json.dumps(datos_json, indent=2)}
|
| 213 |
+
|
| 214 |
+
Responde en español con:
|
| 215 |
+
1. Sugerencia sobre organización
|
| 216 |
+
2. Sugerencia sobre pagos o plazos
|
| 217 |
+
3. Sugerencia sobre optimización o ahorro
|
| 218 |
+
|
| 219 |
+
Sé breve (máximo 150 palabras total):"""
|
| 220 |
+
|
| 221 |
+
try:
|
| 222 |
+
response = client.chat.completions.create(
|
| 223 |
+
model="Qwen/Qwen2.5-72B-Instruct",
|
| 224 |
+
messages=[{"role": "user", "content": prompt}],
|
| 225 |
+
max_tokens=400,
|
| 226 |
+
temperature=0.7
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
return response.choices[0].message.content
|
| 230 |
+
except:
|
| 231 |
+
return "💡 Sugerencias: Mantén tus facturas organizadas por fecha, verifica los plazos de pago, y considera digitalizar todos tus documentos."
|
| 232 |
+
|
| 233 |
+
# ============= EXTRACTOR DE CATEGORÍAS =============
|
| 234 |
+
def extraer_categorias_gasto(datos_json, client):
|
| 235 |
+
"""Categoriza automáticamente el tipo de gasto"""
|
| 236 |
+
|
| 237 |
+
productos = datos_json.get('productos', [])
|
| 238 |
+
texto_productos = " ".join([p.get('descripcion', '') for p in productos[:5]])
|
| 239 |
+
|
| 240 |
+
prompt = f"""Clasifica esta factura en UNA categoría de gasto:
|
| 241 |
+
|
| 242 |
+
Productos/Servicios: {texto_productos}
|
| 243 |
+
Total: {datos_json.get('totales', {}).get('total', 0)}€
|
| 244 |
+
|
| 245 |
+
Categorías posibles:
|
| 246 |
+
- Oficina y suministros
|
| 247 |
+
- Tecnología e IT
|
| 248 |
+
- Servicios profesionales
|
| 249 |
+
- Marketing y publicidad
|
| 250 |
+
- Viajes y transporte
|
| 251 |
+
- Alimentación y hostelería
|
| 252 |
+
- Mantenimiento y reparaciones
|
| 253 |
+
- Otros gastos
|
| 254 |
+
|
| 255 |
+
Responde solo con el nombre de la categoría:"""
|
| 256 |
+
|
| 257 |
+
try:
|
| 258 |
+
response = client.chat.completions.create(
|
| 259 |
+
model="Qwen/Qwen2.5-72B-Instruct",
|
| 260 |
+
messages=[{"role": "user", "content": prompt}],
|
| 261 |
+
max_tokens=50,
|
| 262 |
+
temperature=0.3
|
| 263 |
+
)
|
| 264 |
+
|
| 265 |
+
categoria = response.choices[0].message.content.strip()
|
| 266 |
+
return f"📁 **Categoría:** {categoria}"
|
| 267 |
+
except:
|
| 268 |
+
return "📁 **Categoría:** No clasificada"
|
| 269 |
+
|
| 270 |
+
# ============= TRADUCTOR MULTIIDIOMA =============
|
| 271 |
+
def traducir_factura(texto, idioma_destino, client):
|
| 272 |
+
"""Traduce el contenido de la factura a otro idioma"""
|
| 273 |
+
|
| 274 |
+
idiomas = {
|
| 275 |
+
"Inglés": "English",
|
| 276 |
+
"Francés": "Français",
|
| 277 |
+
"Alemán": "Deutsch",
|
| 278 |
+
"Italiano": "Italiano",
|
| 279 |
+
"Portugués": "Português"
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
idioma = idiomas.get(idioma_destino, "English")
|
| 283 |
+
|
| 284 |
+
prompt = f"""Traduce este resumen de factura al {idioma}. Mantén el formato y estructura:
|
| 285 |
+
|
| 286 |
+
{texto[:2000]}
|
| 287 |
+
|
| 288 |
+
Traducción:"""
|
| 289 |
+
|
| 290 |
+
try:
|
| 291 |
+
response = client.chat.completions.create(
|
| 292 |
+
model="Qwen/Qwen2.5-72B-Instruct",
|
| 293 |
+
messages=[{"role": "user", "content": prompt}],
|
| 294 |
+
max_tokens=1000,
|
| 295 |
+
temperature=0.3
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
return response.choices[0].message.content
|
| 299 |
+
except:
|
| 300 |
+
return "❌ Error en la traducción"
|
| 301 |
+
|
| 302 |
+
# ============= VISUALIZACIÓN DE IMAGEN A TEXTO =============
|
| 303 |
+
def analizar_imagen_factura(imagen_path, client):
|
| 304 |
+
"""Analiza una imagen de factura usando Vision Language Model"""
|
| 305 |
+
|
| 306 |
+
try:
|
| 307 |
+
# Cargar y codificar la imagen
|
| 308 |
+
with Image.open(imagen_path) as img:
|
| 309 |
+
# Redimensionar si es muy grande
|
| 310 |
+
max_size = (1024, 1024)
|
| 311 |
+
img.thumbnail(max_size, Image.Resampling.LANCZOS)
|
| 312 |
+
|
| 313 |
+
# Convertir a bytes
|
| 314 |
+
buffered = io.BytesIO()
|
| 315 |
+
img.save(buffered, format="PNG")
|
| 316 |
+
img_bytes = buffered.getvalue()
|
| 317 |
+
|
| 318 |
+
# Usar modelo de visión
|
| 319 |
+
response = client.image_to_text(
|
| 320 |
+
image=img_bytes,
|
| 321 |
+
model="Salesforce/blip-image-captioning-large"
|
| 322 |
+
)
|
| 323 |
+
|
| 324 |
+
return f"🖼️ **Análisis visual:** {response}"
|
| 325 |
+
|
| 326 |
+
except Exception as e:
|
| 327 |
+
return f"❌ No se pudo analizar la imagen: {str(e)}"
|
| 328 |
+
|
| 329 |
# ============= ANALIZAR CON LLM Y CONVERTIR A JSON =============
|
| 330 |
def analizar_y_convertir_json(texto):
|
| 331 |
"""El LLM lee la factura y devuelve JSON estructurado"""
|
|
|
|
| 511 |
|
| 512 |
return pd.DataFrame(filas)
|
| 513 |
|
| 514 |
+
# ============= GENERAR PDF TEMPLATES =============
|
| 515 |
def generar_pdf_clasico(csv_file, datos_json):
|
| 516 |
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 517 |
pdf_filename = f"factura_clasica_{timestamp}.pdf"
|
|
|
|
| 655 |
pdf_input = gr.File(label="Seleccionar factura PDF", file_types=[".pdf"], type="filepath")
|
| 656 |
btn_extraer = gr.Button("🚀 Extraer Datos de la Factura", variant="primary", size="lg")
|
| 657 |
|
| 658 |
+
# Indicador de carga para extracción (SIN audio)
|
| 659 |
loading_extraccion = gr.HTML(visible=False, value="""
|
| 660 |
<div style="text-align: center; padding: 20px;">
|
| 661 |
<div class="spinner"></div>
|
| 662 |
<p style="margin-top: 10px; color: #2196F3; font-weight: bold;">
|
| 663 |
🔄 Procesando tu factura...
|
| 664 |
</p>
|
|
|
|
|
|
|
|
|
|
| 665 |
</div>
|
| 666 |
<style>
|
| 667 |
.spinner {
|
| 668 |
+
border: 3px solid #f3f3f3;
|
| 669 |
+
border-top: 3px solid #2196F3;
|
| 670 |
border-radius: 50%;
|
| 671 |
+
width: 35px;
|
| 672 |
+
height: 35px;
|
| 673 |
animation: spin 1s linear infinite;
|
| 674 |
margin: 0 auto;
|
| 675 |
}
|
|
|
|
| 709 |
- ✅ Responder preguntas específicas sobre tu factura
|
| 710 |
- ✅ Explicar conceptos contables (IVA, base imponible, etc.)
|
| 711 |
- ✅ Dar consejos sobre gestión y pagos
|
| 712 |
+
- ✅ **Leer la respuesta en voz alta con emociones** 🔊
|
| 713 |
""")
|
| 714 |
|
| 715 |
with gr.Row():
|
|
|
|
| 723 |
|
| 724 |
btn_consulta_ia = gr.Button("🎤 Consultar y Escuchar Respuesta", variant="primary", size="lg")
|
| 725 |
|
| 726 |
+
# Indicador de carga para IA (SIN audio)
|
| 727 |
loading_ia = gr.HTML(visible=False, value="""
|
| 728 |
<div style="text-align: center; padding: 20px;">
|
| 729 |
<div class="spinner-ia"></div>
|
| 730 |
<p style="margin-top: 10px; color: #9C27B0; font-weight: bold;">
|
| 731 |
🧠 El asistente IA está pensando...
|
| 732 |
</p>
|
|
|
|
|
|
|
|
|
|
| 733 |
</div>
|
| 734 |
<style>
|
| 735 |
.spinner-ia {
|
| 736 |
+
border: 3px solid #f3f3f3;
|
| 737 |
+
border-top: 3px solid #9C27B0;
|
| 738 |
border-radius: 50%;
|
| 739 |
+
width: 40px;
|
| 740 |
+
height: 40px;
|
| 741 |
animation: spin 0.8s linear infinite;
|
| 742 |
margin: 0 auto;
|
| 743 |
}
|
|
|
|
| 772 |
|
| 773 |
gr.Markdown("---")
|
| 774 |
gr.Markdown("### 🔊 Escucha la Respuesta")
|
| 775 |
+
|
| 776 |
+
# Notificación de audio completado
|
| 777 |
+
audio_status = gr.Markdown(value="", visible=True)
|
| 778 |
+
|
| 779 |
audio_respuesta = gr.Audio(
|
| 780 |
+
label="Audio de la respuesta (se genera automáticamente al completar el análisis)",
|
| 781 |
type="filepath",
|
| 782 |
+
visible=True,
|
| 783 |
+
autoplay=False
|
| 784 |
)
|
| 785 |
|
| 786 |
gr.Markdown("""
|
| 787 |
+
💡 **Tip:**
|
| 788 |
+
- ▶️ El audio se genera automáticamente cuando la IA termina de responder
|
| 789 |
+
- 🔊 Usa el reproductor para escuchar la respuesta con voz natural y emociones
|
| 790 |
+
- 📥 Descarga el audio si quieres guardarlo
|
| 791 |
+
- 🔄 Haz nuevas preguntas cuando quieras
|
| 792 |
""")
|
| 793 |
+
|
| 794 |
+
# ============= TAB 3: ANÁLISIS AVANZADO IA =============
|
| 795 |
+
with gr.Tab("🔬 Análisis Avanzado IA"):
|
| 796 |
+
gr.Markdown("""
|
| 797 |
+
# 🎯 Herramientas IA Avanzadas para Facturas
|
| 798 |
+
### Análisis inteligente, comparación, traducción y más
|
| 799 |
+
""")
|
| 800 |
+
|
| 801 |
+
with gr.Row():
|
| 802 |
+
with gr.Column():
|
| 803 |
+
gr.Markdown("### 📊 Análisis de Sentimiento")
|
| 804 |
+
btn_sentimiento = gr.Button("🔍 Analizar Riesgos y Urgencias", variant="primary")
|
| 805 |
+
resultado_sentimiento = gr.Markdown()
|
| 806 |
+
|
| 807 |
+
gr.Markdown("---")
|
| 808 |
+
gr.Markdown("### 💡 Sugerencias Personalizadas")
|
| 809 |
+
btn_sugerencias = gr.Button("✨ Generar Sugerencias IA", variant="primary")
|
| 810 |
+
resultado_sugerencias = gr.Markdown()
|
| 811 |
+
|
| 812 |
+
gr.Markdown("---")
|
| 813 |
+
gr.Markdown("### 📁 Categorización Automática")
|
| 814 |
+
btn_categoria = gr.Button("🏷️ Clasificar Tipo de Gasto", variant="primary")
|
| 815 |
+
resultado_categoria = gr.Markdown()
|
| 816 |
+
|
| 817 |
+
with gr.Column():
|
| 818 |
+
gr.Markdown("### 🌍 Traducción Multiidioma")
|
| 819 |
+
idioma_selector = gr.Dropdown(
|
| 820 |
+
choices=["Inglés", "Francés", "Alemán", "Italiano", "Portugués"],
|
| 821 |
+
value="Inglés",
|
| 822 |
+
label="Idioma de destino"
|
| 823 |
+
)
|
| 824 |
+
btn_traducir = gr.Button("🌐 Traducir Factura", variant="secondary")
|
| 825 |
+
resultado_traduccion = gr.Textbox(label="Traducción", lines=10)
|
| 826 |
+
|
| 827 |
+
gr.Markdown("---")
|
| 828 |
+
gr.Markdown("### 🖼️ Análisis de Imagen (opcional)")
|
| 829 |
+
imagen_input = gr.Image(label="Sube una imagen de la factura", type="filepath")
|
| 830 |
+
btn_analizar_imagen = gr.Button("👁️ Analizar Imagen", variant="secondary")
|
| 831 |
+
resultado_imagen = gr.Markdown()
|
| 832 |
+
|
| 833 |
+
# ============= TAB 4: COMPARADOR DE FACTURAS =============
|
| 834 |
+
with gr.Tab("⚖️ Comparador de Facturas"):
|
| 835 |
+
gr.Markdown("""
|
| 836 |
+
# 📊 Compara Dos Facturas
|
| 837 |
+
### Analiza diferencias, variaciones de precio y tendencias
|
| 838 |
+
""")
|
| 839 |
+
|
| 840 |
+
gr.Markdown("**Nota:** Primero procesa una factura en la pestaña 'Extracción Automática', luego sube una segunda aquí.")
|
| 841 |
+
|
| 842 |
+
with gr.Row():
|
| 843 |
+
with gr.Column():
|
| 844 |
+
pdf_input_comparar = gr.File(label="📄 Segunda Factura PDF", file_types=[".pdf"], type="filepath")
|
| 845 |
+
btn_comparar = gr.Button("⚖️ Comparar Facturas", variant="primary", size="lg")
|
| 846 |
+
|
| 847 |
+
with gr.Column():
|
| 848 |
+
resultado_comparacion = gr.Markdown(value="*Sube una segunda factura para comparar*")
|
| 849 |
+
|
| 850 |
+
datos_json_comparar_state = gr.State()
|
| 851 |
|
| 852 |
gr.Markdown("---")
|
| 853 |
gr.Markdown("""
|
| 854 |
### 📚 Guía rápida de uso
|
| 855 |
|
| 856 |
1. **📄 Extracción Automática:** Sube tu PDF y extrae todos los datos automáticamente
|
| 857 |
+
2. **🤖 Asistente IA con Voz:** Haz preguntas y escucha las respuestas en audio natural con emociones
|
| 858 |
+
3. **🔬 Análisis Avanzado:** Análisis de sentimiento, sugerencias IA, categorización y traducción
|
| 859 |
+
4. **⚖️ Comparador:** Compara dos facturas para ver diferencias y tendencias
|
| 860 |
|
| 861 |
---
|
| 862 |
|
| 863 |
+
### 🎯 Nuevas Características IA:
|
| 864 |
|
| 865 |
+
- **🔊 Audio con Emociones:** Voz natural y expresiva en español (Bark, MMS-TTS)
|
| 866 |
+
- **🔍 Análisis de Riesgos:** Detecta urgencias y problemas en facturas
|
| 867 |
+
- **💡 Sugerencias IA:** Consejos personalizados de optimización
|
| 868 |
+
- **📁 Categorización:** Clasifica automáticamente el tipo de gasto
|
| 869 |
+
- **🌐 Traducción:** Traduce facturas a 5 idiomas
|
| 870 |
+
- **👁️ Análisis Visual:** Extrae información de imágenes de facturas
|
| 871 |
+
- **⚖️ Comparador:** Compara dos facturas y analiza diferencias
|
| 872 |
+
- **🎨 Interfaz Silenciosa:** Sin sonidos molestos, control total del audio
|
| 873 |
|
| 874 |
💡 **Empieza por la pestaña "Extracción Automática" para procesar tu factura.**
|
| 875 |
""")
|
|
|
|
| 908 |
# Asistente IA con voz y loading
|
| 909 |
def consultar_ia_con_loading(texto, pregunta):
|
| 910 |
if not texto:
|
| 911 |
+
return "❌ Por favor, procesa una factura primero en la pestaña 'Extracción Automática'", None, "⚠️ Factura no cargada", gr.update(visible=False)
|
| 912 |
|
| 913 |
# Mostrar loading
|
| 914 |
+
yield "🔄 Consultando al asistente IA...", None, "", gr.update(visible=True)
|
| 915 |
|
| 916 |
# Procesar consulta
|
| 917 |
+
time.sleep(0.3)
|
| 918 |
respuesta, audio = asistente_ia_factura(texto, pregunta)
|
| 919 |
|
| 920 |
+
# Mensaje de estado del audio
|
| 921 |
+
audio_msg = "✅ Audio generado correctamente. ¡Escúchalo abajo!" if audio else "⚠️ No se pudo generar el audio"
|
| 922 |
+
|
| 923 |
# Ocultar loading y mostrar resultados
|
| 924 |
+
yield respuesta, audio, audio_msg, gr.update(visible=False)
|
| 925 |
|
| 926 |
btn_consulta_ia.click(
|
| 927 |
fn=consultar_ia_con_loading,
|
| 928 |
inputs=[texto_extraido, pregunta_ia],
|
| 929 |
+
outputs=[resultado_ia, audio_respuesta, audio_status, loading_ia]
|
| 930 |
+
)
|
| 931 |
+
|
| 932 |
+
# Funciones de análisis avanzado
|
| 933 |
+
def ejecutar_sentimiento(texto):
|
| 934 |
+
if not texto:
|
| 935 |
+
return "❌ Procesa una factura primero"
|
| 936 |
+
|
| 937 |
+
token = os.getenv("aa")
|
| 938 |
+
if not token:
|
| 939 |
+
return "❌ Error de configuración"
|
| 940 |
+
|
| 941 |
+
client = InferenceClient(token=token)
|
| 942 |
+
resultado = analizar_sentimiento_factura(texto, client)
|
| 943 |
+
|
| 944 |
+
emoji_sentimiento = {"positivo": "✅", "neutral": "⚪", "alerta": "⚠️"}
|
| 945 |
+
emoji_urgencia = {"alta": "🔴", "media": "🟡", "baja": "🟢"}
|
| 946 |
+
|
| 947 |
+
return f"""### {emoji_sentimiento.get(resultado['sentimiento'], '⚪')} Análisis de Sentimiento
|
| 948 |
+
|
| 949 |
+
**Estado:** {resultado['sentimiento'].upper()}
|
| 950 |
+
**Urgencia:** {emoji_urgencia.get(resultado['urgencia'], '⚪')} {resultado['urgencia'].upper()}
|
| 951 |
+
|
| 952 |
+
**Razón:** {resultado['razon']}
|
| 953 |
+
|
| 954 |
+
**Recomendación:** {resultado['recomendacion']}
|
| 955 |
+
"""
|
| 956 |
+
|
| 957 |
+
def ejecutar_sugerencias(datos_json):
|
| 958 |
+
if not datos_json:
|
| 959 |
+
return "❌ Procesa una factura primero"
|
| 960 |
+
|
| 961 |
+
token = os.getenv("aa")
|
| 962 |
+
if not token:
|
| 963 |
+
return "❌ Error de configuración"
|
| 964 |
+
|
| 965 |
+
client = InferenceClient(token=token)
|
| 966 |
+
return f"### 💡 Sugerencias Personalizadas\n\n{generar_sugerencias_ia(datos_json, client)}"
|
| 967 |
+
|
| 968 |
+
def ejecutar_categoria(datos_json):
|
| 969 |
+
if not datos_json:
|
| 970 |
+
return "❌ Procesa una factura primero"
|
| 971 |
+
|
| 972 |
+
token = os.getenv("aa")
|
| 973 |
+
if not token:
|
| 974 |
+
return "❌ Error de configuración"
|
| 975 |
+
|
| 976 |
+
client = InferenceClient(token=token)
|
| 977 |
+
return f"### 🏷️ Categorización Automática\n\n{extraer_categorias_gasto(datos_json, client)}"
|
| 978 |
+
|
| 979 |
+
def ejecutar_traduccion(texto, idioma):
|
| 980 |
+
if not texto:
|
| 981 |
+
return "❌ Procesa una factura primero"
|
| 982 |
+
|
| 983 |
+
token = os.getenv("aa")
|
| 984 |
+
if not token:
|
| 985 |
+
return "❌ Error de configuración"
|
| 986 |
+
|
| 987 |
+
client = InferenceClient(token=token)
|
| 988 |
+
return traducir_factura(texto, idioma, client)
|
| 989 |
+
|
| 990 |
+
def ejecutar_analisis_imagen(imagen_path):
|
| 991 |
+
if not imagen_path:
|
| 992 |
+
return "❌ Sube una imagen primero"
|
| 993 |
+
|
| 994 |
+
token = os.getenv("aa")
|
| 995 |
+
if not token:
|
| 996 |
+
return "❌ Error de configuración"
|
| 997 |
+
|
| 998 |
+
client = InferenceClient(token=token)
|
| 999 |
+
return analizar_imagen_factura(imagen_path, client)
|
| 1000 |
+
|
| 1001 |
+
def procesar_segunda_factura(pdf_file, datos_json_primera):
|
| 1002 |
+
if not pdf_file:
|
| 1003 |
+
return "❌ Sube una segunda factura", None
|
| 1004 |
+
|
| 1005 |
+
if not datos_json_primera:
|
| 1006 |
+
return "❌ Procesa la primera factura en la pestaña 'Extracción Automática'", None
|
| 1007 |
+
|
| 1008 |
+
# Procesar segunda factura
|
| 1009 |
+
texto = extraer_texto_pdf(pdf_file)
|
| 1010 |
+
datos_json_segunda, _, _ = analizar_y_convertir_json(texto)
|
| 1011 |
+
|
| 1012 |
+
if not datos_json_segunda:
|
| 1013 |
+
return "❌ No se pudo procesar la segunda factura", None
|
| 1014 |
+
|
| 1015 |
+
# Comparar
|
| 1016 |
+
resultado = comparar_facturas(datos_json_primera, datos_json_segunda)
|
| 1017 |
+
return resultado, datos_json_segunda
|
| 1018 |
+
|
| 1019 |
+
# Conectar eventos de análisis avanzado
|
| 1020 |
+
btn_sentimiento.click(fn=ejecutar_sentimiento, inputs=[texto_extraido], outputs=[resultado_sentimiento])
|
| 1021 |
+
btn_sugerencias.click(fn=ejecutar_sugerencias, inputs=[datos_json_state], outputs=[resultado_sugerencias])
|
| 1022 |
+
btn_categoria.click(fn=ejecutar_categoria, inputs=[datos_json_state], outputs=[resultado_categoria])
|
| 1023 |
+
btn_traducir.click(fn=ejecutar_traduccion, inputs=[texto_extraido, idioma_selector], outputs=[resultado_traduccion])
|
| 1024 |
+
btn_analizar_imagen.click(fn=ejecutar_analisis_imagen, inputs=[imagen_input], outputs=[resultado_imagen])
|
| 1025 |
+
|
| 1026 |
+
# Comparador
|
| 1027 |
+
btn_comparar.click(
|
| 1028 |
+
fn=procesar_segunda_factura,
|
| 1029 |
+
inputs=[pdf_input_comparar, datos_json_state],
|
| 1030 |
+
outputs=[resultado_comparacion, datos_json_comparar_state]
|
| 1031 |
)
|
| 1032 |
|
| 1033 |
if __name__ == "__main__":
|