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
MAX_FILE_MB=10
# OCR Settings
OCR_ENABLED=true
OCR_LANGUAGE=spa
OCR_DPI=300
OCR_TIMEOUT=300
# Logging
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
WORKDIR /app
# Install system dependencies for PDF processing
# Install system dependencies for PDF processing and OCR
RUN apt-get update && apt-get install -y \
libmupdf-dev \
mupdf-tools \
tesseract-ocr \
tesseract-ocr-spa \
poppler-utils \
&& rm -rf /var/lib/apt/lists/*
# 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
-**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)
-**Validación de archivos** (tamaño, tipo MIME, PDF cifrado)
-**Logging estructurado** con correlation ID
@@ -19,6 +20,34 @@ Microservicio para extraer y parsear la sección de **Incrementables** de docume
- Python 3.12+
- 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
@@ -42,6 +71,12 @@ JWT_SECRET=your-secret-key-change-this-in-production
JWT_EXPIRES_MINUTES=60
MAX_FILE_MB=10
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:**
@@ -60,16 +95,22 @@ El servicio estará disponible en `http://localhost:9876`
- API FastAPI: puerto 9876
- Redis: puerto 16379
- Celery Worker: procesamiento en background
- **Tesseract OCR**: Incluido en el contenedor (sin instalación adicional)
### Opción 2: Sin Docker
1. **Instalar dependencias del sistema** (para PyMuPDF)
1. **Instalar dependencias del sistema**
```bash
# macOS
brew install mupdf-tools
brew install mupdf-tools tesseract tesseract-lang
# 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**

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
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(
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}
)
except PDFExtractionError as e:

View File

@@ -20,6 +20,12 @@ class Settings(BaseSettings):
# File Upload
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
log_level: str = "INFO"

View File

@@ -8,7 +8,7 @@ from contextlib import asynccontextmanager
from app.core.config import get_settings
from app.api import auth
from app.api.v1 import incrementables
from app.api.v1 import incrementables, debug
from app.schemas import HealthResponse
# Configure logging
@@ -83,6 +83,7 @@ async def health_check():
# Include routers
app.include_router(auth.router)
app.include_router(incrementables.router)
app.include_router(debug.router)
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 pdfplumber
import logging
from typing import Tuple, Optional
from io import BytesIO
from .ocr_service import extract_text_with_ocr, is_pdf_scanned, OCRError
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)}")
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.
Tries PyMuPDF first, falls back to pdfplumber.
Tries PyMuPDF first, falls back to pdfplumber, and uses OCR for scanned PDFs.
Args:
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:
Tuple of (extracted_text, page_count, method_used)
method_used can be: "pymupdf", "pdfplumber", "ocr"
Raises:
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:
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:
logger.warning(f"PyMuPDF failed, trying pdfplumber: {str(e)}")
# Fallback to pdfplumber
try:
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:
logger.error(f"All extraction methods failed: {str(e)}")
raise PDFExtractionError("Failed to extract text from PDF using all available methods")

View File

@@ -2,6 +2,7 @@
import logging
import hashlib
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.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()
# Extract text
settings = get_settings()
try:
text, page_count, extraction_method = extract_text_from_pdf(pdf_bytes)
logger.info(f"Task {task_id}: Extracted {len(text)} chars from {page_count} pages")
text, page_count, extraction_method = extract_text_from_pdf(
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:
logger.error(f"Task {task_id}: Extraction failed - {str(e)}")
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
- CELERY_BROKER_URL=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:
- ./app:/app/app
depends_on:
@@ -61,6 +65,10 @@ services:
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=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:
- ./app:/app/app
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
celery==5.3.6
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())