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:
@@ -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
271
COMO_PROBAR.md
Normal 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
215
DOCKER_OCR.md
Normal 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! 🎉
|
||||
@@ -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
275
OCR_SETUP.md
Normal 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
|
||||
↓
|
||||
SÍ
|
||||
↓
|
||||
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
|
||||
47
README.md
47
README.md
@@ -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
87
app/api/v1/debug.py
Normal 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)}"
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
197
app/services/ocr_service.py
Normal 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)}")
|
||||
@@ -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")
|
||||
|
||||
@@ -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
174
debug_pdf.ps1
Normal 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 ""
|
||||
@@ -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
149
install_ocr.ps1
Normal 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")
|
||||
@@ -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
202
test_ocr.py
Normal 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())
|
||||
Reference in New Issue
Block a user