feat: Agregar soporte OCR con Tesseract para PDFs escaneados

- Integrar Tesseract OCR para leer PDFs escaneados automáticamente
- Detectar automáticamente si el PDF tiene texto o requiere OCR
- Agregar servicio ocr_service.py con funciones de OCR
- Actualizar Dockerfile con tesseract-ocr, tesseract-ocr-spa y poppler-utils
- Agregar variables de configuración OCR (OCR_ENABLED, OCR_LANGUAGE, OCR_DPI, OCR_TIMEOUT)
- Crear endpoint de debug para ver texto extraído (/api/v1/debug/extract-text)
- Agregar scripts de instalación y prueba (install_ocr.ps1, test_ocr.py, debug_pdf.ps1)
- Documentación completa (OCR_SETUP.md, DOCKER_OCR.md, COMO_PROBAR.md)
- Actualizar docker-compose.yml con variables de entorno OCR
- Modificar pdf_text.py para usar OCR cuando sea necesario
- Actualizar requirements.txt con pytesseract, Pillow, pdf2image
This commit is contained in:
Ernesto Herrera
2026-03-04 08:21:41 -07:00
parent 068d859f42
commit fcc516c9b3
18 changed files with 1694 additions and 14 deletions

View File

@@ -8,6 +8,12 @@ JWT_EXPIRES_MINUTES=60
# File Upload # File Upload
MAX_FILE_MB=10 MAX_FILE_MB=10
# OCR Settings
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
# Logging # Logging
LOG_LEVEL=INFO LOG_LEVEL=INFO

271
COMO_PROBAR.md Normal file
View File

@@ -0,0 +1,271 @@
# 🧪 Guía de Prueba - OCR Funcionando
## ✅ Estado del Sistema
Tu sistema está completamente configurado con OCR:
- ✅ Docker Compose ejecutándose
- ✅ Tesseract 5.5.0 instalado en contenedores
- ✅ Idioma español (spa) disponible
- ✅ Variables OCR configuradas en .env
- ✅ API corriendo en http://localhost:9876
## 🚀 Cómo Probar
### 1. Obtener un Token de Autenticación
```powershell
# Opción A: Usando curl (si lo tienes instalado)
curl -X POST http://localhost:9876/api/auth/login `
-H "Content-Type: application/x-www-form-urlencoded" `
-d "username=admin&password=12345"
# Opción B: Usando Invoke-RestMethod (PowerShell nativo)
$body = @{
username = "admin"
password = "12345"
}
$response = Invoke-RestMethod -Uri "http://localhost:9876/api/auth/login" `
-Method Post `
-ContentType "application/x-www-form-urlencoded" `
-Body $body
# Ver el token
$response.access_token
# Guardar el token en una variable
$token = $response.access_token
Write-Host "Token obtenido: $token"
```
### 2. Probar con un PDF
#### Opción A: Endpoint Síncrono (respuesta inmediata)
```powershell
# Reemplaza "C:\ruta\al\documento.pdf" con tu PDF
$pdfPath = "C:\ruta\al\documento.pdf"
# Subir y procesar
curl -X POST "http://localhost:9876/api/v1/incrementables/parse" `
-H "Authorization: Bearer $token" `
-F "file=@$pdfPath"
```
#### Opción B: Endpoint Asíncrono (recomendado para PDFs grandes o escaneados)
```powershell
# 1. Iniciar procesamiento
$pdfPath = "C:\ruta\al\documento.pdf"
$taskResponse = curl -X POST "http://localhost:9876/api/v1/incrementables/parse-async" `
-H "Authorization: Bearer $token" `
-F "file=@$pdfPath"
# 2. Extraer el task_id de la respuesta
# Busca algo como: "task_id":"abc-123-def"
# 3. Consultar el estado de la tarea
$taskId = "TU_TASK_ID_AQUI"
curl -X GET "http://localhost:9876/api/v1/incrementables/task/$taskId" `
-H "Authorization: Bearer $token"
```
### 3. Ver Logs en Tiempo Real
Para ver qué está pasando cuando procesas un PDF:
```powershell
# Ver logs de la API
docker-compose logs -f mve-incrementables-parser
# Ver logs del worker (procesamiento asíncrono)
docker-compose logs -f celery-worker
# Ver todos los logs
docker-compose logs -f
```
### 4. Identificar si se Usó OCR
En la respuesta del API, busca el campo `extraction_method`:
```json
{
"status": "success",
"data": {
"extraction_info": {
"method": "ocr", // ← "ocr" = PDF escaneado, "pymupdf"/"pdfplumber" = PDF con texto
"anchors_found": [...],
"warnings": [...]
}
}
}
```
También verás en los logs:
- `"PDF appears to be scanned, attempting OCR extraction"` → Detectó PDF escaneado
- `"Starting OCR extraction"` → Iniciando OCR
- `"OCR extraction completed: X characters"` → OCR terminado
## 📋 Script Completo de Prueba
Copia y pega esto en PowerShell:
```powershell
# 1. Obtener token
Write-Host "=== Obteniendo token ===" -ForegroundColor Cyan
$body = @{
username = "admin"
password = "12345"
}
$loginResponse = Invoke-RestMethod -Uri "http://localhost:9876/api/auth/login" `
-Method Post `
-ContentType "application/x-www-form-urlencoded" `
-Body $body
$token = $loginResponse.access_token
Write-Host "Token obtenido: $token" -ForegroundColor Green
Write-Host ""
# 2. Verificar que tienes un PDF
$pdfPath = Read-Host "Ingresa la ruta completa al PDF (ej: C:\docs\documento.pdf)"
if (-not (Test-Path $pdfPath)) {
Write-Host "Error: El archivo no existe" -ForegroundColor Red
exit
}
Write-Host "Archivo encontrado: $pdfPath" -ForegroundColor Green
Write-Host ""
# 3. Procesar PDF (asíncrono)
Write-Host "=== Procesando PDF ===" -ForegroundColor Cyan
$headers = @{
"Authorization" = "Bearer $token"
}
# Crear el form data
$fileName = Split-Path $pdfPath -Leaf
$fileContent = [System.IO.File]::ReadAllBytes($pdfPath)
$boundary = [System.Guid]::NewGuid().ToString()
$LF = "`r`n"
$bodyLines = (
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
"Content-Type: application/pdf$LF",
[System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileContent),
"--$boundary--$LF"
) -join $LF
try {
$response = Invoke-RestMethod -Uri "http://localhost:9876/api/v1/incrementables/parse-async" `
-Method Post `
-Headers $headers `
-ContentType "multipart/form-data; boundary=$boundary" `
-Body $bodyLines
Write-Host "Tarea iniciada!" -ForegroundColor Green
Write-Host "Task ID: $($response.task_id)" -ForegroundColor Cyan
Write-Host ""
# 4. Consultar estado
$taskId = $response.task_id
Write-Host "=== Consultando estado ===" -ForegroundColor Cyan
$maxAttempts = 30
$attempt = 0
do {
Start-Sleep -Seconds 2
$attempt++
$statusResponse = Invoke-RestMethod -Uri "http://localhost:9876/api/v1/incrementables/task/$taskId" `
-Method Get `
-Headers $headers
Write-Host "Intento $attempt - Estado: $($statusResponse.status)" -ForegroundColor Yellow
if ($statusResponse.status -eq "completed") {
Write-Host ""
Write-Host "=== RESULTADO ===" -ForegroundColor Green
Write-Host ($statusResponse | ConvertTo-Json -Depth 10)
break
} elseif ($statusResponse.status -eq "failed") {
Write-Host ""
Write-Host "=== ERROR ===" -ForegroundColor Red
Write-Host ($statusResponse | ConvertTo-Json -Depth 10)
break
}
} while ($attempt -lt $maxAttempts)
if ($attempt -eq $maxAttempts) {
Write-Host "Tiempo de espera agotado" -ForegroundColor Red
}
} catch {
Write-Host "Error: $_" -ForegroundColor Red
}
```
## 🔍 Qué Buscar en los Logs
Cuando procesas un PDF, verás algo como:
### PDF con Texto Normal (sin OCR):
```
INFO: Text extracted: 2500 characters, 3 pages, method: pymupdf
INFO: Extracted 2500 chars from 3 pages using pymupdf
```
### PDF Escaneado (con OCR):
```
INFO: PDF appears to be scanned (only 5 characters found)
INFO: Starting OCR extraction with language=spa, dpi=300
INFO: Converted PDF to 3 images
INFO: Page 1: Extracted 1200 characters
INFO: Page 2: Extracted 1500 characters
INFO: Page 3: Extracted 800 characters
INFO: OCR extraction completed: 3500 characters from 3 pages
INFO: Text extracted: 3500 characters, 3 pages, method: ocr
```
## 🐛 Solución de Problemas
### "401 Unauthorized"
- El token expiró o es incorrecto
- Vuelve a obtener un token con el paso 1
### "Failed to extract text"
- El PDF puede estar dañado o cifrado
- Revisa los logs: `docker-compose logs -f`
### OCR muy lento
- Normal para PDFs escaneados (5-30 segundos por página)
- Usa el endpoint asíncrono (`/parse-async`)
- Reduce `OCR_DPI=200` en `.env` para mayor velocidad
### "No incrementables found"
- El PDF no tiene la sección de incrementables
- Revisa el campo `warnings` en la respuesta
- El OCR puede no haber detectado el texto correctamente
## 📊 Ver Configuración Actual
```powershell
# Ver variables de entorno en el contenedor
docker exec mve-incrementables-parser env | Select-String OCR
# Ver configuración OCR
docker exec mve-incrementables-parser tesseract --version
docker exec mve-incrementables-parser tesseract --list-langs
```
## 🎯 Siguientes Pasos
1. Prueba con un PDF que **tenga texto seleccionable** primero
2. Luego prueba con un PDF **escaneado** (imagen)
3. Compara los tiempos de respuesta
4. Revisa el campo `extraction_method` en la respuesta
¡Listo para probar! 🚀

215
DOCKER_OCR.md Normal file
View File

@@ -0,0 +1,215 @@
# 🐳 Guía Rápida: OCR con Docker
## ✅ Lo que YA está incluido en Docker
Cuando usas Docker Compose, **TODO está incluido automáticamente**:
- ✅ Tesseract OCR
- ✅ Idioma español (spa)
- ✅ Poppler (conversión PDF a imagen)
- ✅ Todas las dependencias Python
- ✅ PyMuPDF y pdfplumber
**No necesitas instalar nada en tu máquina local.**
## 🚀 Uso Rápido
### 1. Configurar variables de entorno
Edita tu `.env`:
```env
# OCR ya viene habilitado por defecto
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
```
### 2. Construir y ejecutar
```bash
# Primera vez o después de cambios en Dockerfile
docker-compose build
# Iniciar servicios
docker-compose up -d
# Ver logs
docker-compose logs -f
```
### 3. Verificar que OCR funciona
```bash
# Verificar Tesseract en el contenedor API
docker exec mve-incrementables-parser tesseract --version
# Verificar idiomas instalados
docker exec mve-incrementables-parser tesseract --list-langs
# Verificar en el worker de Celery
docker exec mve-celery-worker tesseract --version
```
Deberías ver:
```
tesseract 5.x.x
...
spa
eng
osd
```
## 🧪 Probar con un PDF Escaneado
```bash
# 1. Primero obtén un token
curl -X POST http://localhost:9876/api/auth/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=admin&password=tu_password"
# 2. Usa el token para subir un PDF
curl -X POST http://localhost:9876/api/v1/incrementables/parse \
-H "Authorization: Bearer TU_TOKEN_AQUI" \
-F "file=@/ruta/al/documento.pdf"
```
## 📊 Ver logs del OCR
```bash
# Ver logs del contenedor principal
docker-compose logs -f mve-incrementables-parser
# Ver logs del worker (procesamiento asíncrono)
docker-compose logs -f celery-worker
# Buscar logs específicos de OCR
docker-compose logs | grep -i "ocr"
docker-compose logs | grep -i "tesseract"
```
## 🔧 Configuración Avanzada
### Cambiar idioma del OCR
Edita `.env`:
```env
OCR_LANGUAGE=eng # Para inglés
# o
OCR_LANGUAGE=spa # Para español
```
Reinicia los contenedores:
```bash
docker-compose restart
```
### Ajustar rendimiento OCR
Para **mayor velocidad** (menos calidad):
```env
OCR_DPI=200
```
Para **mayor calidad** (más lento):
```env
OCR_DPI=600
```
Reinicia:
```bash
docker-compose restart
```
### Deshabilitar OCR
Si solo quieres procesar PDFs con texto seleccionable:
```env
OCR_ENABLED=false
```
## 🐛 Solución de Problemas
### El servicio no inicia después de agregar OCR
```bash
# Reconstruir la imagen
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
### Error: "tesseract command not found"
Esto significa que la imagen no se construyó correctamente.
```bash
# Ver si Tesseract está en la imagen
docker exec mve-incrementables-parser which tesseract
# Si no está, reconstruir
docker-compose build --no-cache mve-incrementables-parser
```
### OCR muy lento
El OCR es naturalmente más lento que la extracción de texto normal. Para PDFs escaneados:
- Usa el endpoint **asíncrono** (`/parse-async`)
- El worker de Celery procesará en background
- Reduce `OCR_DPI` si no necesitas máxima calidad
### Ver qué método se usó (texto vs OCR)
El campo `extraction_method` en la respuesta te dice:
- `"pymupdf"` o `"pdfplumber"` → PDF con texto (rápido)
- `"ocr"` → PDF escaneado procesado con Tesseract (lento)
## 📝 Ejemplo de Respuesta
Cuando el sistema usa OCR automáticamente:
```json
{
"status": "success",
"message": "Incrementables extraídos exitosamente",
"data": {
"incrementables": {
"fletes": 1591.20,
"seguros": 0.0,
"almacenaje": 0.0,
"regalias": 0.0,
"currency": "USD",
"total": 1591.20
},
"document_info": {
"filename": "documento_escaneado.pdf",
"pages": 5,
"file_hash": "abc123...",
"file_size_bytes": 524288
},
"extraction_info": {
"method": "ocr", // ← Indica que se usó OCR
"anchors_found": ["AJUSTE DE INCREMENTABLES EN:"],
"warnings": []
}
}
}
```
## 🎯 Resumen
**Con Docker:**
- ✅ Todo incluido automáticamente
- ✅ Sin instalación manual
- ✅ Funciona igual en Windows, Mac, Linux
- ✅ OCR listo para usar desde el primer `docker-compose up`
**Comandos clave:**
```bash
docker-compose build # Construir imagen
docker-compose up -d # Iniciar servicios
docker-compose logs -f # Ver logs
docker-compose restart # Reiniciar después de cambios en .env
docker-compose down # Detener todo
```
¡Eso es todo! 🎉

View File

@@ -3,10 +3,13 @@ FROM python:3.12-slim
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Install system dependencies for PDF processing # Install system dependencies for PDF processing and OCR
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libmupdf-dev \ libmupdf-dev \
mupdf-tools \ mupdf-tools \
tesseract-ocr \
tesseract-ocr-spa \
poppler-utils \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Copy requirements # Copy requirements

275
OCR_SETUP.md Normal file
View File

@@ -0,0 +1,275 @@
# Guía de Instalación y Configuración de OCR
## <20> ¿Usas Docker? (Recomendado)
Si usas **Docker Compose**, **NO necesitas instalar nada localmente**. Todo está incluido en el contenedor.
### Instalación con Docker
1. **Asegúrate de tener tu archivo `.env` configurado:**
```env
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
```
2. **Construir y levantar los contenedores:**
```bash
docker-compose build
docker-compose up -d
```
3. **Verificar que Tesseract está instalado en el contenedor:**
```bash
docker exec mve-incrementables-parser tesseract --version
docker exec mve-incrementables-parser tesseract --list-langs
```
¡Eso es todo! El Dockerfile ya incluye:
- ✅ Tesseract OCR
- ✅ Paquete de idioma español (spa)
- ✅ Poppler-utils (para conversión PDF a imagen)
- ✅ Todas las dependencias Python
**Salta al final de este documento para ver cómo probar**.
---
## 💻 Instalación Local (Sin Docker)
Solo sigue estas instrucciones si **NO usas Docker** y ejecutas el servicio directamente en tu máquina.
## <20>📋 Requisitos
El sistema ahora soporta **OCR (Reconocimiento Óptico de Caracteres)** usando Tesseract para leer PDFs escaneados o no editables.
## 🔧 Instalación de Tesseract
### Windows
1. **Descargar el instalador:**
- Visita: https://github.com/UB-Mannheim/tesseract/wiki
- Descarga `tesseract-ocr-w64-setup-5.3.x.exe` (versión más reciente)
2. **Instalar Tesseract:**
- Ejecuta el instalador
- **IMPORTANTE**: Asegúrate de instalar el paquete de idioma **Español (spa)** durante la instalación
- Ruta recomendada: `C:\Program Files\Tesseract-OCR`
3. **Agregar Tesseract al PATH:**
- Botón derecho en "Este equipo" → Propiedades → Configuración avanzada del sistema
- Variables de entorno → Path → Editar → Nuevo
- Agregar: `C:\Program Files\Tesseract-OCR`
- Guardar y reiniciar la terminal
4. **Verificar instalación:**
```powershell
tesseract --version
```
Deberías ver algo como: `tesseract 5.3.x`
5. **Verificar idioma español:**
```powershell
tesseract --list-langs
```
Deberías ver `spa` en la lista
### Linux (Ubuntu/Debian)
```bash
sudo apt update
sudo apt install tesseract-ocr tesseract-ocr-spa
tesseract --version
```
### macOS
```bash
brew install tesseract tesseract-lang
```
## 📦 Instalación de Dependencias Python
```powershell
pip install -r requirements.txt
```
Esto instalará:
- `pytesseract` - Interface Python para Tesseract
- `Pillow` - Procesamiento de imágenes
- `pdf2image` - Conversión de PDF a imágenes
**Nota para Windows**: También necesitas instalar **poppler**:
1. Descarga poppler para Windows: https://github.com/oschwartz10612/poppler-windows/releases
2. Extrae el archivo ZIP
3. Agrega `poppler-xx\Library\bin` al PATH del sistema
## ⚙️ Configuración
Edita tu archivo `.env` para configurar el OCR:
```env
# OCR Settings
OCR_ENABLED=true # true para habilitar OCR, false para deshabilitarlo
OCR_LANGUAGE=spa # "spa" para español, "eng" para inglés
OCR_DPI=300 # Calidad de escaneo (300 recomendado, más alto = más lento)
OCR_TIMEOUT=300 # Tiempo máximo en segundos para procesar
```
## 🚀 Cómo Funciona
El sistema ahora funciona de manera **inteligente y automática**:
### 1. PDFs Editables (con texto seleccionable)
- Se extrae el texto normalmente usando PyMuPDF o pdfplumber
- **Rápido** y eficiente
- No usa OCR
### 2. PDFs Escaneados (imágenes, sin texto)
- El sistema **detecta automáticamente** que el PDF no tiene texto
- Activa el OCR automáticamente
- Extrae el texto de las imágenes usando Tesseract
- **Más lento** pero funcional
### 3. Proceso Automático
```
PDF recibido
Intentar extracción normal (PyMuPDF)
¿Tiene texto? → SÍ → Retornar texto
NO
¿OCR habilitado? → NO → Error
Convertir PDF a imágenes
Aplicar OCR a cada página
Retornar texto extraído
```
## 📝 Ejemplo de Uso
```python
from app.services.pdf_text import extract_text_from_pdf
# Leer PDF
with open("documento.pdf", "rb") as f:
pdf_bytes = f.read()
# Extraer texto (OCR automático si es necesario)
text, pages, method = extract_text_from_pdf(
pdf_bytes,
enable_ocr=True, # Habilitar OCR
ocr_lang="spa" # Idioma español
)
print(f"Método usado: {method}") # "pymupdf", "pdfplumber", o "ocr"
print(f"Páginas: {pages}")
print(f"Texto extraído: {text[:500]}...")
```
## 🔍 Métodos de Extracción
El sistema retorna uno de estos métodos en `method`:
- **`pymupdf`**: Extracción exitosa con PyMuPDF (texto seleccionable)
- **`pdfplumber`**: Extracción con pdfplumber (fallback)
- **`ocr`**: Texto extraído usando OCR (PDF escaneado)
## ⚡ Rendimiento
### PDFs Editables
- ⚡ **Muy rápido**: < 1 segundo para documentos de 10 páginas
- 💾 Bajo uso de CPU y memoria
### PDFs Escaneados (OCR)
- 🐌 **Más lento**: 5-30 segundos por página (depende de DPI)
- 💻 Mayor uso de CPU y memoria
- 📊 Calidad depende de:
- Resolución del escaneo original
- Claridad del texto en la imagen
- DPI configurado (más alto = mejor pero más lento)
### Optimización
Para mejorar velocidad del OCR:
- Reducir `OCR_DPI` a 200 (menos calidad, más rápido)
- Usar procesamiento asíncrono con Celery (ya implementado)
## 🧪 Pruebas
### Con Docker
```bash
# Verificar Tesseract en el contenedor
docker exec mve-incrementables-parser tesseract --version
docker exec mve-incrementables-parser tesseract --list-langs
# Probar el endpoint con un PDF
curl -X POST http://localhost:9876/api/v1/incrementables/parse \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@documento_escaneado.pdf"
```
### Local (sin Docker)
Para probar con un PDF escaneado:
```powershell
# Usando el endpoint síncrono
curl -X POST http://localhost:8000/api/v1/incrementables/parse \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@documento_escaneado.pdf"
# Usando el endpoint asíncrono (recomendado para OCR)
curl -X POST http://localhost:8000/api/v1/incrementables/parse-async \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@documento_escaneado.pdf"
```
## 🐛 Solución de Problemas
### Error: "tesseract is not installed"
- Verifica que Tesseract esté instalado: `tesseract --version`
- Verifica que esté en el PATH del sistema
- Reinicia la terminal/IDE después de agregar al PATH
### Error: "Failed to load language 'spa'"
- Instala el paquete de idioma español
- Windows: Reinstala Tesseract y marca la opción "Spanish language data"
- Linux: `sudo apt install tesseract-ocr-spa`
### Error: "Unable to load library 'libpoppler'"
- Windows: Instala poppler y agrégalo al PATH
- Linux: `sudo apt install poppler-utils`
- macOS: `brew install poppler`
### OCR muy lento
- Reduce `OCR_DPI` en el archivo `.env`
- Usa el endpoint asíncrono para procesar en background
- Considera procesar solo las páginas necesarias
### OCR no detecta texto correctamente
- Aumenta `OCR_DPI` a 400 o 600
- Verifica que el PDF escaneado tenga buena calidad
- Prueba con otro idioma si el documento está en inglés: `OCR_LANGUAGE=eng`
## 📚 Recursos Adicionales
- [Documentación Tesseract](https://github.com/tesseract-ocr/tesseract)
- [pytesseract GitHub](https://github.com/madmaze/pytesseract)
- [pdf2image Documentation](https://github.com/Belval/pdf2image)
## 🎯 Próximos Pasos
Para mejorar aún más:
1. **Pre-procesamiento de imágenes**: Aplicar filtros para mejorar calidad
2. **Detección de idioma automática**
3. **Caché de resultados OCR** para evitar reprocesar el mismo PDF
4. **Procesamiento paralelo** de páginas para mayor velocidad
5. **Migrar a servicios cloud** (AWS Textract) para mejor precisión

View File

@@ -6,6 +6,7 @@ Microservicio para extraer y parsear la sección de **Incrementables** de docume
-**Autenticación JWT** con bcrypt -**Autenticación JWT** con bcrypt
-**Extracción de texto** con PyMuPDF y pdfplumber (fallback) -**Extracción de texto** con PyMuPDF y pdfplumber (fallback)
-**OCR con Tesseract** para PDFs escaneados (detección automática)
-**Parsing robusto** de incrementables (fletes, seguros, almacenaje, regalías) -**Parsing robusto** de incrementables (fletes, seguros, almacenaje, regalías)
-**Validación de archivos** (tamaño, tipo MIME, PDF cifrado) -**Validación de archivos** (tamaño, tipo MIME, PDF cifrado)
-**Logging estructurado** con correlation ID -**Logging estructurado** con correlation ID
@@ -19,6 +20,34 @@ Microservicio para extraer y parsear la sección de **Incrementables** de docume
- Python 3.12+ - Python 3.12+
- Docker & Docker Compose (opcional) - Docker & Docker Compose (opcional)
- **Tesseract OCR** (para PDFs escaneados) - [Ver guía de instalación](OCR_SETUP.md)
- **Poppler** (para conversión PDF a imagen) - Requerido solo para OCR
### 🤖 OCR para PDFs Escaneados
El sistema puede leer **PDFs escaneados** (imágenes) usando Tesseract OCR. La detección es automática:
- Si el PDF tiene texto seleccionable → Extracción normal (rápido)
- Si el PDF está escaneado → OCR automático (más lento)
**Con Docker (Recomendado):**
- ✅ Tesseract ya incluido en la imagen
- ✅ Sin instalación adicional
- ✅ Ver [DOCKER_OCR.md](DOCKER_OCR.md) para guía completa
**Sin Docker (Instalación local):**
```powershell
# Windows - Ejecutar script de instalación
.\install_ocr.ps1
# O instalar manualmente - Ver OCR_SETUP.md para guía completa
```
**Verificar instalación local:**
```bash
python test_ocr.py
```
Ver [OCR_SETUP.md](OCR_SETUP.md) para instrucciones detalladas de instalación local.
## 🛠️ Instalación ## 🛠️ Instalación
@@ -42,6 +71,12 @@ JWT_SECRET=your-secret-key-change-this-in-production
JWT_EXPIRES_MINUTES=60 JWT_EXPIRES_MINUTES=60
MAX_FILE_MB=10 MAX_FILE_MB=10
LOG_LEVEL=INFO LOG_LEVEL=INFO
# OCR Settings (ya configurado por defecto)
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
``` ```
**Generar hash de contraseña:** **Generar hash de contraseña:**
@@ -60,16 +95,22 @@ El servicio estará disponible en `http://localhost:9876`
- API FastAPI: puerto 9876 - API FastAPI: puerto 9876
- Redis: puerto 16379 - Redis: puerto 16379
- Celery Worker: procesamiento en background - Celery Worker: procesamiento en background
- **Tesseract OCR**: Incluido en el contenedor (sin instalación adicional)
### Opción 2: Sin Docker ### Opción 2: Sin Docker
1. **Instalar dependencias del sistema** (para PyMuPDF) 1. **Instalar dependencias del sistema**
```bash ```bash
# macOS # macOS
brew install mupdf-tools brew install mupdf-tools tesseract tesseract-lang
# Ubuntu/Debian # Ubuntu/Debian
sudo apt-get install libmupdf-dev mupdf-tools sudo apt-get install libmupdf-dev mupdf-tools tesseract-ocr tesseract-ocr-spa poppler-utils
# Windows - Ver OCR_SETUP.md para instrucciones detalladas
# Descargar e instalar:
# - Tesseract: https://github.com/UB-Mannheim/tesseract/wiki
# - Poppler: https://github.com/oschwartz10612/poppler-windows/releases
``` ```
2. **Crear entorno virtual** 2. **Crear entorno virtual**

87
app/api/v1/debug.py Normal file
View File

@@ -0,0 +1,87 @@
"""Debug endpoint to see extracted text from PDF."""
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import logging
from app.core.config import get_settings
from app.core.security import decode_access_token
from app.services.pdf_text import extract_text_from_pdf, PDFExtractionError
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/v1/debug", tags=["Debug"])
security = HTTPBearer()
settings = get_settings()
def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> str:
"""Validate JWT token."""
payload = decode_access_token(credentials.credentials)
return payload.get("sub")
@router.post("/extract-text")
async def debug_extract_text(
file: UploadFile = File(...),
current_user: str = Depends(get_current_user)
):
"""
Debug endpoint to see raw extracted text from PDF.
Shows exactly what text was extracted and which method was used.
"""
# Validate file type
if file.content_type != "application/pdf":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Invalid file type: {file.content_type}. Only PDF files are allowed."
)
# Read file
pdf_bytes = await file.read()
# Validate size
file_size_mb = len(pdf_bytes) / (1024 * 1024)
if file_size_mb > settings.max_file_mb:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"File size ({file_size_mb:.2f} MB) exceeds maximum allowed size of {settings.max_file_mb} MB"
)
# Extract text
try:
text, page_count, extraction_method = extract_text_from_pdf(
pdf_bytes,
enable_ocr=settings.ocr_enabled,
ocr_lang=settings.ocr_language
)
# Get first and last 500 characters
text_preview_start = text[:500] if len(text) > 500 else text
text_preview_end = text[-500:] if len(text) > 500 else ""
return {
"status": "success",
"extraction_info": {
"method": extraction_method,
"pages": page_count,
"total_characters": len(text),
"total_words": len(text.split()),
"total_lines": len(text.split('\n'))
},
"text_preview": {
"first_500_chars": text_preview_start,
"last_500_chars": text_preview_end if text_preview_end else None
},
"full_text": text, # Complete extracted text
"ocr_settings": {
"ocr_enabled": settings.ocr_enabled,
"ocr_language": settings.ocr_language,
"ocr_dpi": settings.ocr_dpi
}
}
except PDFExtractionError as e:
logger.error(f"Extraction failed: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to extract text from PDF: {str(e)}"
)

View File

@@ -117,9 +117,13 @@ async def parse_pdf(
# Extract text from PDF # Extract text from PDF
try: try:
text, page_count, extraction_method = extract_text_from_pdf(pdf_bytes) text, page_count, extraction_method = extract_text_from_pdf(
pdf_bytes,
enable_ocr=settings.ocr_enabled,
ocr_lang=settings.ocr_language
)
logger.info( logger.info(
f"Text extracted: {len(text)} characters, {page_count} pages", f"Text extracted: {len(text)} characters, {page_count} pages, method: {extraction_method}",
extra={"correlation_id": correlation_id} extra={"correlation_id": correlation_id}
) )
except PDFExtractionError as e: except PDFExtractionError as e:

View File

@@ -20,6 +20,12 @@ class Settings(BaseSettings):
# File Upload # File Upload
max_file_mb: int = 10 max_file_mb: int = 10
# OCR Settings
ocr_enabled: bool = True
ocr_language: str = "spa" # "spa" for Spanish, "eng" for English
ocr_dpi: int = 300 # Higher = better quality but slower
ocr_timeout: int = 300 # Maximum time in seconds for OCR
# Logging # Logging
log_level: str = "INFO" log_level: str = "INFO"

View File

@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
from app.core.config import get_settings from app.core.config import get_settings
from app.api import auth from app.api import auth
from app.api.v1 import incrementables from app.api.v1 import incrementables, debug
from app.schemas import HealthResponse from app.schemas import HealthResponse
# Configure logging # Configure logging
@@ -83,6 +83,7 @@ async def health_check():
# Include routers # Include routers
app.include_router(auth.router) app.include_router(auth.router)
app.include_router(incrementables.router) app.include_router(incrementables.router)
app.include_router(debug.router)
if __name__ == "__main__": if __name__ == "__main__":

197
app/services/ocr_service.py Normal file
View File

@@ -0,0 +1,197 @@
"""OCR service using Tesseract for scanned PDFs."""
import pytesseract
from pdf2image import convert_from_bytes
from PIL import Image
import logging
from typing import Tuple, List, Optional
from io import BytesIO
logger = logging.getLogger(__name__)
class OCRError(Exception):
"""Custom exception for OCR processing errors."""
pass
def is_pdf_scanned(text: str, min_char_threshold: int = 50) -> bool:
"""
Determine if a PDF is scanned (no selectable text) or has text.
Args:
text: Extracted text from PDF
min_char_threshold: Minimum characters to consider as "has text"
Returns:
True if PDF appears to be scanned (no text), False otherwise
"""
# Remove whitespace and count actual characters
cleaned_text = text.replace(" ", "").replace("\n", "").replace("\t", "")
if len(cleaned_text) < min_char_threshold:
logger.info(f"PDF appears to be scanned (only {len(cleaned_text)} characters found)")
return True
logger.info(f"PDF has selectable text ({len(cleaned_text)} characters)")
return False
def extract_text_with_ocr(
pdf_bytes: bytes,
lang: str = "spa",
dpi: int = 300,
timeout: int = 300
) -> Tuple[str, int]:
"""
Extract text from scanned PDF using Tesseract OCR.
Args:
pdf_bytes: PDF file content as bytes
lang: Language for OCR (default: "spa" for Spanish, use "eng" for English)
dpi: DPI for image conversion (higher = better quality but slower)
timeout: Maximum time in seconds for OCR processing
Returns:
Tuple of (extracted_text, page_count)
Raises:
OCRError: If OCR processing fails
"""
try:
logger.info(f"Starting OCR extraction with language={lang}, dpi={dpi}")
# Convert PDF to images
images = convert_from_bytes(
pdf_bytes,
dpi=dpi,
fmt='png',
thread_count=2,
timeout=timeout
)
page_count = len(images)
logger.info(f"Converted PDF to {page_count} images")
# Extract text from each page
text_parts = []
for i, image in enumerate(images, 1):
try:
# Perform OCR on the image
page_text = pytesseract.image_to_string(
image,
lang=lang,
timeout=timeout // page_count # Distribute timeout across pages
)
if page_text.strip():
text_parts.append(f"--- Página {i} ---\n{page_text}")
logger.debug(f"Page {i}: Extracted {len(page_text)} characters")
else:
logger.warning(f"Page {i}: No text extracted")
text_parts.append(f"--- Página {i} ---\n[Sin texto detectado]\n")
except Exception as e:
logger.error(f"OCR failed on page {i}: {str(e)}")
text_parts.append(f"--- Página {i} ---\n[Error en OCR: {str(e)}]\n")
full_text = "\n\n".join(text_parts)
logger.info(f"OCR extraction completed: {len(full_text)} characters from {page_count} pages")
return full_text, page_count
except Exception as e:
logger.error(f"OCR extraction failed: {str(e)}")
raise OCRError(f"OCR extraction failed: {str(e)}")
def optimize_image_for_ocr(image: Image.Image) -> Image.Image:
"""
Optimize image for better OCR results.
Args:
image: PIL Image object
Returns:
Optimized PIL Image
"""
# Convert to grayscale
image = image.convert('L')
# Optional: Apply threshold to make text clearer
# This can be adjusted based on your PDFs
# from PIL import ImageEnhance
# enhancer = ImageEnhance.Contrast(image)
# image = enhancer.enhance(2)
return image
def extract_text_with_ocr_optimized(
pdf_bytes: bytes,
lang: str = "spa",
dpi: int = 300,
timeout: int = 300,
optimize: bool = True
) -> Tuple[str, int]:
"""
Extract text from scanned PDF with image optimization.
Args:
pdf_bytes: PDF file content as bytes
lang: Language for OCR
dpi: DPI for image conversion
timeout: Maximum time in seconds
optimize: Whether to optimize images before OCR
Returns:
Tuple of (extracted_text, page_count)
Raises:
OCRError: If OCR processing fails
"""
try:
logger.info(f"Starting optimized OCR extraction")
# Convert PDF to images
images = convert_from_bytes(
pdf_bytes,
dpi=dpi,
fmt='png',
thread_count=2,
timeout=timeout
)
page_count = len(images)
text_parts = []
for i, image in enumerate(images, 1):
try:
# Optimize image if requested
if optimize:
image = optimize_image_for_ocr(image)
# Perform OCR
page_text = pytesseract.image_to_string(
image,
lang=lang,
config='--psm 1', # Automatic page segmentation with OSD
timeout=timeout // page_count
)
if page_text.strip():
text_parts.append(f"--- Página {i} ---\n{page_text}")
else:
text_parts.append(f"--- Página {i} ---\n[Sin texto detectado]\n")
except Exception as e:
logger.error(f"OCR failed on page {i}: {str(e)}")
text_parts.append(f"--- Página {i} ---\n[Error en OCR]\n")
full_text = "\n\n".join(text_parts)
logger.info(f"Optimized OCR completed: {len(full_text)} characters")
return full_text, page_count
except Exception as e:
logger.error(f"Optimized OCR failed: {str(e)}")
raise OCRError(f"Optimized OCR failed: {str(e)}")

View File

@@ -1,9 +1,10 @@
"""PDF text extraction service using PyMuPDF and pdfplumber.""" """PDF text extraction service using PyMuPDF, pdfplumber, and OCR."""
import fitz # PyMuPDF import fitz # PyMuPDF
import pdfplumber import pdfplumber
import logging import logging
from typing import Tuple, Optional from typing import Tuple, Optional
from io import BytesIO from io import BytesIO
from .ocr_service import extract_text_with_ocr, is_pdf_scanned, OCRError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -82,16 +83,19 @@ def extract_text_with_pdfplumber(pdf_bytes: bytes) -> Tuple[str, int]:
raise PDFExtractionError(f"pdfplumber extraction failed: {str(e)}") raise PDFExtractionError(f"pdfplumber extraction failed: {str(e)}")
def extract_text_from_pdf(pdf_bytes: bytes) -> Tuple[str, int, str]: def extract_text_from_pdf(pdf_bytes: bytes, enable_ocr: bool = True, ocr_lang: str = "spa") -> Tuple[str, int, str]:
""" """
Extract text from PDF using available methods. Extract text from PDF using available methods.
Tries PyMuPDF first, falls back to pdfplumber. Tries PyMuPDF first, falls back to pdfplumber, and uses OCR for scanned PDFs.
Args: Args:
pdf_bytes: PDF file content as bytes pdf_bytes: PDF file content as bytes
enable_ocr: Whether to use OCR for scanned PDFs (default: True)
ocr_lang: Language for OCR - "spa" for Spanish, "eng" for English (default: "spa")
Returns: Returns:
Tuple of (extracted_text, page_count, method_used) Tuple of (extracted_text, page_count, method_used)
method_used can be: "pymupdf", "pdfplumber", "ocr"
Raises: Raises:
PDFExtractionError: If all extraction methods fail PDFExtractionError: If all extraction methods fail
@@ -99,14 +103,41 @@ def extract_text_from_pdf(pdf_bytes: bytes) -> Tuple[str, int, str]:
# Try PyMuPDF first # Try PyMuPDF first
try: try:
text, pages = extract_text_with_pymupdf(pdf_bytes) text, pages = extract_text_with_pymupdf(pdf_bytes)
return text, pages, "text"
# Check if the PDF is scanned (no text content)
if enable_ocr and is_pdf_scanned(text):
logger.info("PDF appears to be scanned, attempting OCR extraction")
try:
ocr_text, ocr_pages = extract_text_with_ocr(pdf_bytes, lang=ocr_lang)
return ocr_text, ocr_pages, "ocr"
except OCRError as ocr_e:
logger.error(f"OCR failed: {str(ocr_e)}")
# Return the minimal text we got, if any
if text.strip():
return text, pages, "pymupdf"
raise PDFExtractionError(f"PDF appears to be scanned and OCR failed: {str(ocr_e)}")
return text, pages, "pymupdf"
except PDFExtractionError as e: except PDFExtractionError as e:
logger.warning(f"PyMuPDF failed, trying pdfplumber: {str(e)}") logger.warning(f"PyMuPDF failed, trying pdfplumber: {str(e)}")
# Fallback to pdfplumber # Fallback to pdfplumber
try: try:
text, pages = extract_text_with_pdfplumber(pdf_bytes) text, pages = extract_text_with_pdfplumber(pdf_bytes)
return text, pages, "text"
# Check if the PDF is scanned
if enable_ocr and is_pdf_scanned(text):
logger.info("PDF appears to be scanned (pdfplumber), attempting OCR extraction")
try:
ocr_text, ocr_pages = extract_text_with_ocr(pdf_bytes, lang=ocr_lang)
return ocr_text, ocr_pages, "ocr"
except OCRError as ocr_e:
logger.error(f"OCR failed: {str(ocr_e)}")
if text.strip():
return text, pages, "pdfplumber"
raise PDFExtractionError(f"PDF appears to be scanned and OCR failed: {str(ocr_e)}")
return text, pages, "pdfplumber"
except PDFExtractionError as e: except PDFExtractionError as e:
logger.error(f"All extraction methods failed: {str(e)}") logger.error(f"All extraction methods failed: {str(e)}")
raise PDFExtractionError("Failed to extract text from PDF using all available methods") raise PDFExtractionError("Failed to extract text from PDF using all available methods")

View File

@@ -2,6 +2,7 @@
import logging import logging
import hashlib import hashlib
from app.core.celery_app import celery_app from app.core.celery_app import celery_app
from app.core.config import get_settings
from app.services.pdf_text import extract_text_from_pdf, PDFExtractionError from app.services.pdf_text import extract_text_from_pdf, PDFExtractionError
from app.services.parser import parse_incrementables, ParsingError from app.services.parser import parse_incrementables, ParsingError
@@ -33,9 +34,14 @@ def parse_pdf_task(self, pdf_bytes_hex: str, filename: str, document_ref: str =
file_hash = hashlib.sha256(pdf_bytes).hexdigest() file_hash = hashlib.sha256(pdf_bytes).hexdigest()
# Extract text # Extract text
settings = get_settings()
try: try:
text, page_count, extraction_method = extract_text_from_pdf(pdf_bytes) text, page_count, extraction_method = extract_text_from_pdf(
logger.info(f"Task {task_id}: Extracted {len(text)} chars from {page_count} pages") pdf_bytes,
enable_ocr=settings.ocr_enabled,
ocr_lang=settings.ocr_language
)
logger.info(f"Task {task_id}: Extracted {len(text)} chars from {page_count} pages using {extraction_method}")
except PDFExtractionError as e: except PDFExtractionError as e:
logger.error(f"Task {task_id}: Extraction failed - {str(e)}") logger.error(f"Task {task_id}: Extraction failed - {str(e)}")
return { return {

174
debug_pdf.ps1 Normal file
View File

@@ -0,0 +1,174 @@
# Script para ver qué texto extrajo el OCR de un PDF
# Uso: .\debug_pdf.ps1 -PdfPath "C:\ruta\al\archivo.pdf"
param(
[Parameter(Mandatory=$false)]
[string]$PdfPath,
[Parameter(Mandatory=$false)]
[string]$Token
)
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " Debug: Ver texto extraído del PDF" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
# Si no se proporciona el path del PDF, preguntar
if (-not $PdfPath) {
$PdfPath = Read-Host "Ingresa la ruta completa del PDF"
}
# Verificar que el archivo existe
if (-not (Test-Path $PdfPath)) {
Write-Host "❌ Error: El archivo no existe: $PdfPath" -ForegroundColor Red
exit 1
}
Write-Host "📄 PDF: $PdfPath" -ForegroundColor White
$fileInfo = Get-Item $PdfPath
Write-Host "📊 Tamaño: $([math]::Round($fileInfo.Length / 1MB, 2)) MB" -ForegroundColor Gray
Write-Host ""
# Obtener token si no se proporcionó
if (-not $Token) {
Write-Host "🔑 Obteniendo token de autenticación..." -ForegroundColor Yellow
$body = @{
username = "admin"
password = "12345"
}
try {
$loginResponse = Invoke-RestMethod -Uri "http://localhost:9876/api/auth/login" `
-Method Post `
-ContentType "application/x-www-form-urlencoded" `
-Body $body
$Token = $loginResponse.access_token
Write-Host "✅ Token obtenido" -ForegroundColor Green
} catch {
Write-Host "❌ Error al obtener token: $_" -ForegroundColor Red
exit 1
}
}
Write-Host ""
Write-Host "🔍 Extrayendo texto del PDF..." -ForegroundColor Yellow
# Preparar la petición
$headers = @{
"Authorization" = "Bearer $Token"
}
$fileName = Split-Path $PdfPath -Leaf
$fileBytes = [System.IO.File]::ReadAllBytes($PdfPath)
# Crear form data
$boundary = [System.Guid]::NewGuid().ToString()
$LF = "`r`n"
$bodyLines = @(
"--$boundary",
"Content-Disposition: form-data; name=`"file`"; filename=`"$fileName`"",
"Content-Type: application/pdf$LF",
[System.Text.Encoding]::GetEncoding("iso-8859-1").GetString($fileBytes),
"--$boundary--$LF"
) -join $LF
try {
$response = Invoke-RestMethod -Uri "http://localhost:9876/api/v1/debug/extract-text" `
-Method Post `
-Headers $headers `
-ContentType "multipart/form-data; boundary=$boundary" `
-Body $bodyLines
Write-Host ""
Write-Host "============================================" -ForegroundColor Green
Write-Host " RESULTADO DE LA EXTRACCIÓN" -ForegroundColor Green
Write-Host "============================================" -ForegroundColor Green
Write-Host ""
# Información de extracción
Write-Host "📊 Estadísticas:" -ForegroundColor Cyan
Write-Host " • Método usado: $($response.extraction_info.method)" -ForegroundColor White
if ($response.extraction_info.method -eq "ocr") {
Write-Host " └─ ✅ SE USÓ OCR" -ForegroundColor Green
} else {
Write-Host " └─ No se usó OCR (el PDF tiene texto)" -ForegroundColor Yellow
}
Write-Host " • Páginas: $($response.extraction_info.pages)" -ForegroundColor White
Write-Host " • Caracteres: $($response.extraction_info.total_characters)" -ForegroundColor White
Write-Host " • Palabras: $($response.extraction_info.total_words)" -ForegroundColor White
Write-Host " • Líneas: $($response.extraction_info.total_lines)" -ForegroundColor White
Write-Host ""
# Configuración OCR
Write-Host "⚙️ Configuración OCR:" -ForegroundColor Cyan
Write-Host " • Habilitado: $($response.ocr_settings.ocr_enabled)" -ForegroundColor White
Write-Host " • Idioma: $($response.ocr_settings.ocr_language)" -ForegroundColor White
Write-Host " • DPI: $($response.ocr_settings.ocr_dpi)" -ForegroundColor White
Write-Host ""
# Texto extraído
Write-Host "📝 TEXTO COMPLETO EXTRAÍDO:" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Gray
Write-Host $response.full_text -ForegroundColor White
Write-Host "============================================" -ForegroundColor Gray
Write-Host ""
# Guardar en archivo
$outputFile = "$($PdfPath)_extracted_text.txt"
$response.full_text | Out-File -FilePath $outputFile -Encoding UTF8
Write-Host "💾 Texto guardado en: $outputFile" -ForegroundColor Green
Write-Host ""
# Análisis
if ($response.extraction_info.total_characters -lt 100) {
Write-Host "⚠️ ADVERTENCIA: Se extrajeron pocos caracteres ($($response.extraction_info.total_characters))" -ForegroundColor Yellow
Write-Host ""
Write-Host "Posibles causas:" -ForegroundColor Yellow
Write-Host " 1. El PDF está en blanco o casi vacío" -ForegroundColor White
Write-Host " 2. La calidad de la imagen es muy mala" -ForegroundColor White
Write-Host " 3. El texto es muy pequeño (intenta aumentar OCR_DPI a 600)" -ForegroundColor White
Write-Host " 4. La imagen tiene bajo contraste" -ForegroundColor White
Write-Host " 5. El texto no está en español (cambia OCR_LANGUAGE)" -ForegroundColor White
Write-Host ""
Write-Host "💡 Soluciones sugeridas:" -ForegroundColor Cyan
Write-Host " • Aumentar DPI: Edita .env y cambia OCR_DPI=600" -ForegroundColor White
Write-Host " • Cambiar idioma: Edita .env y cambia OCR_LANGUAGE=eng" -ForegroundColor White
Write-Host " • Mejorar calidad: Escanea el documento a mayor resolución" -ForegroundColor White
Write-Host ""
}
# Buscar palabras clave de incrementables
$keywords = @("incrementables", "fletes", "seguros", "almacenaje", "regalias", "ajuste")
$foundKeywords = @()
foreach ($keyword in $keywords) {
if ($response.full_text -match $keyword) {
$foundKeywords += $keyword
}
}
if ($foundKeywords.Count -gt 0) {
Write-Host "✅ Palabras clave encontradas: $($foundKeywords -join ', ')" -ForegroundColor Green
} else {
Write-Host "❌ No se encontraron palabras clave de incrementables" -ForegroundColor Red
Write-Host " El documento puede no contener información de incrementables" -ForegroundColor Yellow
}
Write-Host ""
} catch {
Write-Host ""
Write-Host "❌ Error al procesar el PDF:" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
if ($_.Exception.Message -match "401") {
Write-Host "💡 El token expiró o es inválido. Intenta de nuevo." -ForegroundColor Yellow
}
}
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""

View File

@@ -32,6 +32,10 @@ services:
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/0
- OCR_ENABLED=${OCR_ENABLED:-true}
- OCR_LANGUAGE=${OCR_LANGUAGE:-spa}
- OCR_DPI=${OCR_DPI:-300}
- OCR_TIMEOUT=${OCR_TIMEOUT:-300}
volumes: volumes:
- ./app:/app/app - ./app:/app/app
depends_on: depends_on:
@@ -61,6 +65,10 @@ services:
- REDIS_URL=redis://redis:6379/0 - REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/0 - CELERY_BROKER_URL=redis://redis:6379/0
- CELERY_RESULT_BACKEND=redis://redis:6379/0 - CELERY_RESULT_BACKEND=redis://redis:6379/0
- OCR_ENABLED=${OCR_ENABLED:-true}
- OCR_LANGUAGE=${OCR_LANGUAGE:-spa}
- OCR_DPI=${OCR_DPI:-300}
- OCR_TIMEOUT=${OCR_TIMEOUT:-300}
volumes: volumes:
- ./app:/app/app - ./app:/app/app
depends_on: depends_on:

149
install_ocr.ps1 Normal file
View File

@@ -0,0 +1,149 @@
# Script de Instalación Rápida de OCR
# Para ejecutar: .\install_ocr.ps1
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Instalación de OCR para MVE Micro Docs" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Verificar si tesseract ya está instalado
Write-Host "Verificando si Tesseract está instalado..." -ForegroundColor Yellow
$tesseractInstalled = $false
try {
$version = tesseract --version 2>&1
if ($version -match "tesseract") {
Write-Host "✓ Tesseract ya está instalado:" -ForegroundColor Green
Write-Host $version[0] -ForegroundColor Gray
$tesseractInstalled = $true
}
} catch {
Write-Host "✗ Tesseract no está instalado" -ForegroundColor Red
}
if (-not $tesseractInstalled) {
Write-Host ""
Write-Host "Para instalar Tesseract OCR:" -ForegroundColor Yellow
Write-Host "1. Visita: https://github.com/UB-Mannheim/tesseract/wiki" -ForegroundColor White
Write-Host "2. Descarga tesseract-ocr-w64-setup-5.3.x.exe" -ForegroundColor White
Write-Host "3. Durante la instalación, MARCA la opción 'Spanish language data'" -ForegroundColor White
Write-Host "4. Agrega C:\Program Files\Tesseract-OCR al PATH del sistema" -ForegroundColor White
Write-Host "5. Reinicia PowerShell y ejecuta este script nuevamente" -ForegroundColor White
Write-Host ""
$openBrowser = Read-Host "¿Abrir el sitio de descarga en el navegador? (S/N)"
if ($openBrowser -eq "S" -or $openBrowser -eq "s") {
Start-Process "https://github.com/UB-Mannheim/tesseract/wiki"
}
Write-Host ""
Write-Host "Presiona cualquier tecla para continuar después de instalar Tesseract..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
exit
}
# Verificar idioma español
Write-Host ""
Write-Host "Verificando idioma español..." -ForegroundColor Yellow
$languages = tesseract --list-langs 2>&1
if ($languages -match "spa") {
Write-Host "✓ Paquete de idioma español (spa) instalado" -ForegroundColor Green
} else {
Write-Host "✗ Paquete de idioma español NO instalado" -ForegroundColor Red
Write-Host "Reinstala Tesseract y marca 'Spanish language data' durante la instalación" -ForegroundColor Yellow
}
# Verificar poppler
Write-Host ""
Write-Host "Verificando poppler (requerido para convertir PDF a imágenes)..." -ForegroundColor Yellow
$popplerInstalled = $false
try {
$pdfToText = Get-Command pdftoppm -ErrorAction SilentlyContinue
if ($pdfToText) {
Write-Host "✓ Poppler ya está instalado" -ForegroundColor Green
$popplerInstalled = $true
}
} catch {}
if (-not $popplerInstalled) {
Write-Host "✗ Poppler no está instalado" -ForegroundColor Red
Write-Host ""
Write-Host "Para instalar Poppler:" -ForegroundColor Yellow
Write-Host "1. Descarga: https://github.com/oschwartz10612/poppler-windows/releases/latest" -ForegroundColor White
Write-Host "2. Extrae el ZIP a C:\poppler" -ForegroundColor White
Write-Host "3. Agrega C:\poppler\Library\bin al PATH del sistema" -ForegroundColor White
Write-Host ""
$openBrowser = Read-Host "¿Abrir el sitio de descarga en el navegador? (S/N)"
if ($openBrowser -eq "S" -or $openBrowser -eq "s") {
Start-Process "https://github.com/oschwartz10612/poppler-windows/releases/latest"
}
}
# Instalar dependencias Python
Write-Host ""
Write-Host "Instalando dependencias Python..." -ForegroundColor Yellow
try {
& pip install pytesseract Pillow pdf2image
Write-Host "✓ Dependencias Python instaladas correctamente" -ForegroundColor Green
} catch {
Write-Host "✗ Error al instalar dependencias Python" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
}
# Verificar archivo .env
Write-Host ""
Write-Host "Verificando configuración..." -ForegroundColor Yellow
if (Test-Path ".env") {
Write-Host "✓ Archivo .env existe" -ForegroundColor Green
# Verificar si tiene las variables OCR
$envContent = Get-Content ".env" -Raw
if ($envContent -notmatch "OCR_ENABLED") {
Write-Host "! Agregando variables OCR al archivo .env" -ForegroundColor Yellow
$ocrConfig = @"
# OCR Settings
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
"@
Add-Content ".env" $ocrConfig
Write-Host "✓ Variables OCR agregadas" -ForegroundColor Green
} else {
Write-Host "✓ Variables OCR ya configuradas" -ForegroundColor Green
}
} else {
Write-Host "! Archivo .env no existe, copiando desde .env.example" -ForegroundColor Yellow
Copy-Item ".env.example" ".env"
Write-Host "✓ Archivo .env creado - edítalo con tus credenciales" -ForegroundColor Green
}
# Resumen final
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Resumen de Instalación" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
$allGood = $tesseractInstalled -and ($languages -match "spa")
if ($allGood) {
Write-Host "✓ ¡Todo listo para usar OCR!" -ForegroundColor Green
Write-Host ""
Write-Host "Para probar:" -ForegroundColor White
Write-Host " python -c `"import pytesseract; print('OCR Ready!')`"" -ForegroundColor Gray
Write-Host ""
Write-Host "Inicia tu servidor:" -ForegroundColor White
Write-Host " uvicorn app.main:app --reload" -ForegroundColor Gray
} else {
Write-Host "⚠ Instalación incompleta" -ForegroundColor Yellow
Write-Host ""
Write-Host "Componentes faltantes:" -ForegroundColor Yellow
if (-not $tesseractInstalled) { Write-Host " - Tesseract OCR" -ForegroundColor Red }
if ($languages -notmatch "spa") { Write-Host " - Paquete de idioma español" -ForegroundColor Red }
if (-not $popplerInstalled) { Write-Host " - Poppler (opcional pero recomendado)" -ForegroundColor Yellow }
Write-Host ""
Write-Host "Consulta OCR_SETUP.md para instrucciones detalladas" -ForegroundColor White
}
Write-Host ""
Write-Host "Presiona cualquier tecla para salir..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

View File

@@ -13,3 +13,7 @@ pytest-asyncio==0.23.3
httpx==0.26.0 httpx==0.26.0
celery==5.3.6 celery==5.3.6
redis==5.0.1 redis==5.0.1
# OCR dependencies
pytesseract==0.3.10
Pillow==10.2.0
pdf2image==1.17.0

202
test_ocr.py Normal file
View File

@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Script de prueba para verificar que OCR funciona correctamente."""
import sys
import os
def test_imports():
"""Verificar que todas las librerías necesarias están instaladas."""
print("🔍 Verificando imports...")
try:
import pytesseract
print(" ✓ pytesseract")
except ImportError as e:
print(f" ✗ pytesseract: {e}")
return False
try:
from PIL import Image
print(" ✓ Pillow (PIL)")
except ImportError as e:
print(f" ✗ Pillow: {e}")
return False
try:
from pdf2image import convert_from_bytes
print(" ✓ pdf2image")
except ImportError as e:
print(f" ✗ pdf2image: {e}")
return False
try:
import fitz
print(" ✓ PyMuPDF")
except ImportError as e:
print(f" ✗ PyMuPDF: {e}")
return False
return True
def test_tesseract():
"""Verificar que Tesseract está instalado y accesible."""
print("\n🔍 Verificando Tesseract...")
try:
import pytesseract
version = pytesseract.get_tesseract_version()
print(f" ✓ Tesseract versión: {version}")
return True
except Exception as e:
print(f" ✗ Error: {e}")
print(" → Asegúrate de que Tesseract esté instalado y en el PATH")
return False
def test_languages():
"""Verificar idiomas disponibles."""
print("\n🔍 Verificando idiomas disponibles...")
try:
import pytesseract
langs = pytesseract.get_languages()
print(f" Idiomas instalados: {', '.join(langs)}")
if 'spa' in langs:
print(" ✓ Español (spa) disponible")
else:
print(" ✗ Español (spa) NO disponible")
print(" → Reinstala Tesseract con el paquete de idioma español")
return False
return True
except Exception as e:
print(f" ✗ Error: {e}")
return False
def test_poppler():
"""Verificar que poppler está disponible."""
print("\n🔍 Verificando poppler...")
try:
from pdf2image import convert_from_bytes
# Intentar convertir un PDF simple (1x1 pixel blanco)
# Este es un PDF mínimo válido en base64
import base64
minimal_pdf = base64.b64decode(
"JVBERi0xLjEKJeLjz9MKMSAwIG9iaiAKPDwgL1R5cGUgL0NhdGFsb2cgL1BhZ2VzIDIgMCBSID4+"
"CmVuZG9iaiAKMiAwIG9iaiAKPDwgL1R5cGUgL1BhZ2VzIC9LaWRzIFszIDAgUl0gL0NvdW50IDEK"
"L01lZGlhQm94IFswIDAgMzAwIDE0NF0gPj4KZW5kb2JqIAozIDAgb2JqIAo8PCAvVHlwZSAvUGFn"
"ZSAvUGFyZW50IDIgMCBSIC9SZXNvdXJjZXMgPDwgL0ZvbnQgPDwgL0YxIDQgMCBSID4+ID4+IC9D"
"b250ZW50cyA1IDAgUiA+PgplbmRvYmogCjQgMCBvYmogCjw8IC9UeXBlIC9Gb250IC9TdWJ0eXBl"
"IC9UeXBlMSAvQmFzZUZvbnQgL1RpbWVzLVJvbWFuID4+CmVuZG9iaiAKNSAwIG9iaiAKPDwgL0xl"
"bmd0aCA0NCA+PgpzdHJlYW0KQlQKNzAgNTAgVGQKL0YxIDEyIFRmCihIZWxsbykgVGoKRVQKZW5k"
"c3RyZWFtCmVuZG9iaiAKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAw"
"MDAwIG4gCjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAx"
"IDAwMDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA2IC9Sb290IDAg"
"MCBSID4+CnN0YXJ0eHJlZgo0OTEKJSVFT0YK"
)
images = convert_from_bytes(minimal_pdf, dpi=72)
print(f" ✓ poppler funciona correctamente")
print(f" → Convertido PDF a {len(images)} imagen(es)")
return True
except Exception as e:
print(f" ✗ Error: {e}")
print(" → Instala poppler y agrégalo al PATH del sistema")
return False
def test_ocr_simple():
"""Realizar una prueba simple de OCR."""
print("\n🔍 Realizando prueba de OCR...")
try:
import pytesseract
from PIL import Image, ImageDraw, ImageFont
# Crear una imagen simple con texto
img = Image.new('RGB', (300, 100), color='white')
draw = ImageDraw.Draw(img)
# Dibujar texto simple
draw.text((10, 40), "Hola Mundo", fill='black')
# Intentar OCR
text = pytesseract.image_to_string(img, lang='spa')
print(f" Texto detectado: '{text.strip()}'")
if text.strip():
print(" ✓ OCR funciona correctamente")
return True
else:
print(" ⚠ OCR no detectó texto (esto puede ser normal con fuentes simples)")
return True
except Exception as e:
print(f" ✗ Error en OCR: {e}")
return False
def test_service_import():
"""Verificar que el servicio OCR se puede importar."""
print("\n🔍 Verificando servicio OCR del proyecto...")
try:
# Agregar el directorio del proyecto al path si no está
project_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if project_dir not in sys.path:
sys.path.insert(0, project_dir)
from app.services.ocr_service import extract_text_with_ocr, is_pdf_scanned
print(" ✓ Servicio OCR importado correctamente")
from app.services.pdf_text import extract_text_from_pdf
print(" ✓ Servicio PDF importado correctamente")
return True
except Exception as e:
print(f" ✗ Error: {e}")
return False
def main():
"""Ejecutar todas las pruebas."""
print("=" * 50)
print("🧪 Prueba de Configuración OCR")
print("=" * 50)
results = {
"Imports": test_imports(),
"Tesseract": test_tesseract(),
"Idiomas": test_languages(),
"Poppler": test_poppler(),
"OCR Simple": test_ocr_simple(),
"Servicios": test_service_import()
}
print("\n" + "=" * 50)
print("📊 Resumen")
print("=" * 50)
for test_name, result in results.items():
status = "" if result else ""
print(f"{status} {test_name}")
all_passed = all(results.values())
print("\n" + "=" * 50)
if all_passed:
print("🎉 ¡Todas las pruebas pasaron exitosamente!")
print("Tu sistema está listo para usar OCR.")
else:
print("⚠️ Algunas pruebas fallaron.")
print("Revisa los errores arriba y consulta OCR_SETUP.md")
print("=" * 50)
return 0 if all_passed else 1
if __name__ == "__main__":
sys.exit(main())