✅ Mejoras Implementadas en Extracción de PDFs con IA
He mejorado significativamente el sistema de extracción de texto de PDFs para el análisis con IA. Aquí están los cambios principales:
🎯 Problemas Resueltos
Límites muy pequeños → Aumentados significativamente según capacidad del modelo
No extraía todo el documento → Ahora procesa hasta 100k caracteres
Duplicación de contenido → Detecta y elimina páginas repetidas
Sin información de estado → Reporta páginas procesadas, truncado, etc.
This commit is contained in:
@@ -209,7 +209,74 @@ def send_completed_inspection_to_n8n(inspection, db):
|
||||
# No lanzamos excepción para no interrumpir el flujo normal
|
||||
|
||||
|
||||
BACKEND_VERSION = "1.0.94"
|
||||
# ============================================================================
|
||||
# UTILIDADES PARA PROCESAMIENTO DE PDFs
|
||||
# ============================================================================
|
||||
|
||||
def extract_pdf_text_smart(pdf_content: bytes, max_chars: int = None) -> dict:
|
||||
"""
|
||||
Extrae texto de un PDF de forma inteligente, evitando duplicaciones
|
||||
y manejando PDFs grandes.
|
||||
|
||||
Args:
|
||||
pdf_content: Contenido del PDF en bytes
|
||||
max_chars: Límite máximo de caracteres (None = sin límite)
|
||||
|
||||
Returns:
|
||||
dict con 'text', 'pages', 'total_chars', 'truncated'
|
||||
"""
|
||||
from pypdf import PdfReader
|
||||
from io import BytesIO
|
||||
|
||||
try:
|
||||
pdf_file = BytesIO(pdf_content)
|
||||
pdf_reader = PdfReader(pdf_file)
|
||||
|
||||
full_text = ""
|
||||
pages_processed = 0
|
||||
total_pages = len(pdf_reader.pages)
|
||||
|
||||
for page_num, page in enumerate(pdf_reader.pages, 1):
|
||||
page_text = page.extract_text()
|
||||
|
||||
# Limpiar y validar texto de la página
|
||||
if page_text and page_text.strip():
|
||||
# Evitar duplicación: verificar si el texto ya existe
|
||||
# (algunos PDFs pueden tener páginas repetidas)
|
||||
if page_text.strip() not in full_text:
|
||||
full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n"
|
||||
pages_processed += 1
|
||||
|
||||
# Si hay límite y lo alcanzamos, detener
|
||||
if max_chars and len(full_text) >= max_chars:
|
||||
break
|
||||
|
||||
total_chars = len(full_text)
|
||||
truncated = False
|
||||
|
||||
# Aplicar límite si se especificó
|
||||
if max_chars and total_chars > max_chars:
|
||||
full_text = full_text[:max_chars]
|
||||
truncated = True
|
||||
|
||||
return {
|
||||
'text': full_text,
|
||||
'pages': total_pages,
|
||||
'pages_processed': pages_processed,
|
||||
'total_chars': total_chars,
|
||||
'truncated': truncated,
|
||||
'success': True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
'text': '',
|
||||
'error': str(e),
|
||||
'success': False
|
||||
}
|
||||
|
||||
|
||||
BACKEND_VERSION = "1.0.95"
|
||||
app = FastAPI(title="Checklist Inteligente API", version=BACKEND_VERSION)
|
||||
|
||||
# S3/MinIO configuration
|
||||
@@ -2598,7 +2665,6 @@ async def analyze_image(
|
||||
|
||||
# Guardar archivo temporalmente y procesar según tipo
|
||||
import base64
|
||||
from pypdf import PdfReader
|
||||
|
||||
contents = await file.read()
|
||||
file_type = file.content_type
|
||||
@@ -2610,34 +2676,30 @@ async def analyze_image(
|
||||
|
||||
if is_pdf:
|
||||
print("📕 Detectado PDF - extrayendo texto...")
|
||||
try:
|
||||
from io import BytesIO
|
||||
pdf_file = BytesIO(contents)
|
||||
pdf_reader = PdfReader(pdf_file)
|
||||
|
||||
# Extraer texto de todas las páginas
|
||||
pdf_text = ""
|
||||
for page_num, page in enumerate(pdf_reader.pages):
|
||||
pdf_text += f"\n--- Página {page_num + 1} ---\n"
|
||||
pdf_text += page.extract_text()
|
||||
# Usar función inteligente de extracción
|
||||
# Para análisis de imagen usamos hasta 100k caracteres (Gemini soporta mucho más)
|
||||
pdf_result = extract_pdf_text_smart(contents, max_chars=100000)
|
||||
|
||||
print(f"✅ Texto extraído: {len(pdf_text)} caracteres")
|
||||
|
||||
if not pdf_text.strip():
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No se pudo extraer texto del PDF. Puede ser un PDF escaneado sin OCR."
|
||||
}
|
||||
|
||||
# Para PDFs usamos análisis de texto, no de imagen
|
||||
image_b64 = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error al procesar PDF: {str(e)}")
|
||||
if not pdf_result['success']:
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Error al procesar PDF: {str(e)}"
|
||||
"message": f"Error al procesar PDF: {pdf_result.get('error', 'Unknown')}"
|
||||
}
|
||||
|
||||
pdf_text = pdf_result['text']
|
||||
print(f"✅ Texto extraído: {pdf_result['total_chars']} caracteres de {pdf_result['pages_processed']}/{pdf_result['pages']} páginas")
|
||||
if pdf_result['truncated']:
|
||||
print(f"⚠️ PDF truncado a 100k caracteres")
|
||||
|
||||
if not pdf_text.strip():
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No se pudo extraer texto del PDF. Puede ser un PDF escaneado sin OCR."
|
||||
}
|
||||
|
||||
# Para PDFs usamos análisis de texto, no de imagen
|
||||
image_b64 = None
|
||||
else:
|
||||
# Es una imagen
|
||||
image_b64 = base64.b64encode(contents).decode('utf-8')
|
||||
@@ -2819,7 +2881,7 @@ NOTA:
|
||||
{"role": "system", "content": system_prompt},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"{user_message}\n\n--- CONTENIDO DEL DOCUMENTO PDF ---\n{pdf_text[:4000]}" # Limitar a 4000 chars
|
||||
"content": f"{user_message}\n\n--- CONTENIDO DEL DOCUMENTO PDF ({len(pdf_text)} caracteres) ---\n{pdf_text[:30000]}" # 30k chars para GPT-4
|
||||
}
|
||||
]
|
||||
else:
|
||||
@@ -2860,8 +2922,8 @@ NOTA:
|
||||
prompt = f"{system_prompt}\n\n{user_message}"
|
||||
|
||||
if is_pdf:
|
||||
# Para PDF, solo texto
|
||||
prompt_with_content = f"{prompt}\n\n--- CONTENIDO DEL DOCUMENTO PDF ---\n{pdf_text[:4000]}"
|
||||
# Para PDF, solo texto - Gemini puede manejar contextos muy largos (2M tokens)
|
||||
prompt_with_content = f"{prompt}\n\n--- CONTENIDO DEL DOCUMENTO PDF ({len(pdf_text)} caracteres) ---\n{pdf_text[:100000]}"
|
||||
response = model.generate_content(prompt_with_content)
|
||||
else:
|
||||
# Para imagen, incluir imagen
|
||||
@@ -3036,8 +3098,6 @@ async def chat_with_ai_assistant(
|
||||
attached_files_data = []
|
||||
if files:
|
||||
import base64
|
||||
from pypdf import PdfReader
|
||||
from io import BytesIO
|
||||
|
||||
for file in files:
|
||||
file_content = await file.read()
|
||||
@@ -3051,18 +3111,22 @@ async def chat_with_ai_assistant(
|
||||
|
||||
# Si es PDF, extraer texto
|
||||
if file_type == 'application/pdf' or file.filename.lower().endswith('.pdf'):
|
||||
try:
|
||||
pdf_file = BytesIO(file_content)
|
||||
pdf_reader = PdfReader(pdf_file)
|
||||
pdf_text = ""
|
||||
for page in pdf_reader.pages:
|
||||
pdf_text += page.extract_text()
|
||||
# Usar función inteligente - límite de 50k para chat (balance entre contexto y tokens)
|
||||
pdf_result = extract_pdf_text_smart(file_content, max_chars=50000)
|
||||
|
||||
if pdf_result['success']:
|
||||
file_info['content_type'] = 'pdf'
|
||||
file_info['text'] = pdf_text[:2000] # Limitar texto
|
||||
print(f"📄 PDF procesado: {file.filename} - {len(pdf_text)} caracteres")
|
||||
except Exception as e:
|
||||
print(f"❌ Error procesando PDF {file.filename}: {str(e)}")
|
||||
file_info['error'] = str(e)
|
||||
file_info['text'] = pdf_result['text']
|
||||
file_info['total_chars'] = pdf_result['total_chars']
|
||||
file_info['pages'] = pdf_result['pages']
|
||||
file_info['pages_processed'] = pdf_result['pages_processed']
|
||||
file_info['truncated'] = pdf_result['truncated']
|
||||
|
||||
truncated_msg = " (TRUNCADO)" if pdf_result['truncated'] else ""
|
||||
print(f"📄 PDF procesado: {file.filename} - {pdf_result['total_chars']} caracteres, {pdf_result['pages_processed']}/{pdf_result['pages']} páginas{truncated_msg}")
|
||||
else:
|
||||
print(f"❌ Error procesando PDF {file.filename}: {pdf_result.get('error', 'Unknown')}")
|
||||
file_info['error'] = pdf_result.get('error', 'Error desconocido')
|
||||
|
||||
# Si es imagen, convertir a base64
|
||||
elif file_type.startswith('image/'):
|
||||
@@ -3120,9 +3184,12 @@ INFORMACIÓN DEL VEHÍCULO:
|
||||
attached_context = f"\n\nARCHIVOS ADJUNTOS EN ESTE MENSAJE ({len(attached_files_data)} archivos):\n"
|
||||
for idx, file_info in enumerate(attached_files_data, 1):
|
||||
if file_info.get('content_type') == 'pdf':
|
||||
attached_context += f"\n{idx}. PDF: {file_info['filename']}\n"
|
||||
truncated_indicator = " ⚠️TRUNCADO" if file_info.get('truncated') else ""
|
||||
pages_info = f" ({file_info.get('pages_processed', '?')}/{file_info.get('pages', '?')} páginas, {file_info.get('total_chars', '?')} caracteres{truncated_indicator})" if 'pages' in file_info else ""
|
||||
attached_context += f"\n{idx}. PDF: {file_info['filename']}{pages_info}\n"
|
||||
if 'text' in file_info:
|
||||
attached_context += f" Contenido: {file_info['text'][:500]}...\n"
|
||||
# Mostrar más contexto del PDF (primeros 2000 caracteres como preview)
|
||||
attached_context += f" Contenido: {file_info['text'][:2000]}...\n"
|
||||
elif file_info.get('content_type') == 'image':
|
||||
attached_context += f"\n{idx}. Imagen: {file_info['filename']}\n"
|
||||
|
||||
|
||||
155
docs/pdf-extraction-improvements.md
Normal file
155
docs/pdf-extraction-improvements.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Mejoras en la Extracción de PDFs con IA
|
||||
|
||||
## Versión Backend: 1.0.95
|
||||
|
||||
## Problema Original
|
||||
|
||||
El sistema tenía limitaciones al procesar PDFs con IA:
|
||||
|
||||
1. **Límites muy pequeños**: Solo extraía 2,000-4,000 caracteres
|
||||
2. **Sin manejo de duplicaciones**: Páginas repetidas se procesaban múltiples veces
|
||||
3. **No aprovechaba contextos largos**: Los modelos modernos soportan millones de tokens
|
||||
4. **Falta de información**: No reportaba páginas procesadas o si el contenido fue truncado
|
||||
|
||||
## Solución Implementada
|
||||
|
||||
### 1. Función Centralizada de Extracción
|
||||
|
||||
Nueva función `extract_pdf_text_smart()` que:
|
||||
- ✅ Extrae texto de forma inteligente
|
||||
- ✅ Detecta y evita páginas duplicadas
|
||||
- ✅ Maneja límites configurables
|
||||
- ✅ Reporta estadísticas completas (páginas, caracteres, truncado)
|
||||
- ✅ Manejo robusto de errores
|
||||
|
||||
```python
|
||||
pdf_result = extract_pdf_text_smart(pdf_content, max_chars=50000)
|
||||
# Retorna:
|
||||
# {
|
||||
# 'text': '...',
|
||||
# 'pages': 10,
|
||||
# 'pages_processed': 9, # Si una página estaba duplicada
|
||||
# 'total_chars': 45000,
|
||||
# 'truncated': False,
|
||||
# 'success': True
|
||||
# }
|
||||
```
|
||||
|
||||
### 2. Límites Optimizados por Caso de Uso
|
||||
|
||||
| Endpoint | Límite Anterior | Límite Nuevo | Modelo Objetivo |
|
||||
|----------|----------------|--------------|-----------------|
|
||||
| `/api/analyze-image` (OpenAI) | 4,000 chars | 30,000 chars | GPT-4 (128k tokens) |
|
||||
| `/api/analyze-image` (Gemini) | 4,000 chars | 100,000 chars | Gemini 1.5/2.0 (2M tokens) |
|
||||
| `/api/ai/chat-assistant` | 2,000 chars | 50,000 chars | Equilibrado para contexto |
|
||||
|
||||
### 3. Detección de Duplicaciones
|
||||
|
||||
El sistema ahora verifica si el contenido de una página ya existe antes de agregarlo:
|
||||
|
||||
```python
|
||||
if page_text.strip() not in full_text:
|
||||
full_text += f"\n--- Página {page_num}/{total_pages} ---\n{page_text.strip()}\n"
|
||||
```
|
||||
|
||||
Esto previene:
|
||||
- PDFs con páginas idénticas repetidas
|
||||
- Documentos mal generados con contenido duplicado
|
||||
- Uso innecesario de tokens en el análisis IA
|
||||
|
||||
### 4. Información Mejorada
|
||||
|
||||
El sistema ahora reporta:
|
||||
- **Páginas totales**: Total de páginas en el PDF
|
||||
- **Páginas procesadas**: Páginas únicas con contenido
|
||||
- **Caracteres totales**: Tamaño real del texto extraído
|
||||
- **Indicador de truncado**: Si el PDF fue limitado
|
||||
|
||||
Ejemplo de output:
|
||||
```
|
||||
📄 PDF procesado: manual-vehiculo.pdf - 87450 caracteres, 8/10 páginas (TRUNCADO)
|
||||
```
|
||||
|
||||
## Capacidades de Contexto por Modelo
|
||||
|
||||
### OpenAI GPT-4
|
||||
- **Contexto**: ~128,000 tokens (~500,000 caracteres)
|
||||
- **Límite aplicado**: 30,000 caracteres
|
||||
- **Razón**: Balance entre contexto útil y costo
|
||||
|
||||
### Gemini 1.5/2.0 Pro
|
||||
- **Contexto**: 2,000,000 tokens (~8,000,000 caracteres)
|
||||
- **Límite aplicado**: 100,000 caracteres
|
||||
- **Razón**: Aprovechar contexto masivo sin sobrecargar
|
||||
|
||||
### Chat Assistant
|
||||
- **Límite**: 50,000 caracteres
|
||||
- **Razón**: Incluye historial + contexto de fotos + PDF
|
||||
|
||||
## Casos de Uso Soportados
|
||||
|
||||
### ✅ PDFs Pequeños (1-5 páginas)
|
||||
Extracción completa sin truncado
|
||||
|
||||
### ✅ PDFs Medianos (5-20 páginas)
|
||||
Extracción completa o parcial según contenido
|
||||
|
||||
### ✅ PDFs Grandes (20+ páginas)
|
||||
Extracción inteligente con truncado después de límite
|
||||
|
||||
### ✅ PDFs con Páginas Duplicadas
|
||||
Detección automática y eliminación
|
||||
|
||||
### ✅ Múltiples PDFs en Chat
|
||||
Cada uno procesado independientemente con su límite
|
||||
|
||||
## Indicadores de Estado
|
||||
|
||||
### En Logs del Servidor
|
||||
```
|
||||
📄 PDF procesado: documento.pdf - 25000 caracteres, 10/10 páginas
|
||||
📄 PDF procesado: manual.pdf - 50000 caracteres, 15/20 páginas (TRUNCADO)
|
||||
```
|
||||
|
||||
### En Respuesta al Cliente
|
||||
```json
|
||||
{
|
||||
"attached_files": [
|
||||
{
|
||||
"filename": "manual.pdf",
|
||||
"type": "application/pdf",
|
||||
"pages": 20,
|
||||
"pages_processed": 15,
|
||||
"total_chars": 75000,
|
||||
"truncated": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Próximas Mejoras Potenciales
|
||||
|
||||
1. **Chunking Inteligente**: Para PDFs muy grandes, dividir en chunks semánticos
|
||||
2. **OCR Integrado**: Detectar PDFs escaneados y aplicar OCR automático
|
||||
3. **Resumen Automático**: Para PDFs grandes, generar resumen antes de análisis
|
||||
4. **Cache de Extracciones**: Guardar texto extraído en DB para reutilización
|
||||
|
||||
## Migración
|
||||
|
||||
No requiere migración de base de datos. Los cambios son retrocompatibles.
|
||||
|
||||
## Testing
|
||||
|
||||
Para probar las mejoras:
|
||||
|
||||
1. **PDF pequeño** (< 10 páginas): Debe procesarse completo
|
||||
2. **PDF grande** (> 50 páginas): Debe truncarse y reportar info
|
||||
3. **PDF con duplicados**: Debe eliminar páginas repetidas
|
||||
4. **Múltiples PDFs**: Cada uno procesado independientemente
|
||||
|
||||
## Notas Técnicas
|
||||
|
||||
- La función `extract_pdf_text_smart()` está en `main.py` línea ~210
|
||||
- Usa `pypdf.PdfReader` para extracción
|
||||
- Maneja encoding UTF-8 automáticamente
|
||||
- Thread-safe (usa BytesIO)
|
||||
BIN
frontend/public/ayutec_logo.webp
Normal file
BIN
frontend/public/ayutec_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
@@ -20,9 +20,9 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
{sidebarOpen && (
|
||||
<div className="flex items-center gap-2">
|
||||
<img
|
||||
src="/logo192.png"
|
||||
src="/ayutec_logo.png"
|
||||
alt="Ayutec"
|
||||
className="w-8 h-8 object-contain"
|
||||
className="w-8 h-8 object-contain bg-white rounded-lg p-1"
|
||||
/>
|
||||
<h2 className="text-xl font-bold bg-gradient-to-r from-indigo-400 to-purple-400 bg-clip-text text-transparent">Ayutec</h2>
|
||||
</div>
|
||||
@@ -141,16 +141,21 @@ export default function Sidebar({ user, activeTab, setActiveTab, sidebarOpen, se
|
||||
{/* Versión */}
|
||||
{sidebarOpen && (
|
||||
<div className="mb-3 px-2 py-1.5 bg-indigo-900/30 rounded-lg border border-indigo-700/30">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<a
|
||||
href="https://ayutec.es"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-2 hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<img
|
||||
src="/logo192.png"
|
||||
src="/ayutec_logo.webp"
|
||||
alt="Ayutec"
|
||||
className="w-4 h-4 object-contain"
|
||||
className="w-10 h-10 object-contain bg-white rounded p-1"
|
||||
/>
|
||||
<p className="text-xs text-indigo-300 font-medium">
|
||||
<p className="text-xs text-indigo-300 font-medium hover:text-indigo-200">
|
||||
Ayutec v1.1.0
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user