diff --git a/.env.example b/.env.example index a0d71b8..6264b90 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/COMO_PROBAR.md b/COMO_PROBAR.md new file mode 100644 index 0000000..ef230dc --- /dev/null +++ b/COMO_PROBAR.md @@ -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! 🚀 diff --git a/DOCKER_OCR.md b/DOCKER_OCR.md new file mode 100644 index 0000000..2eedb66 --- /dev/null +++ b/DOCKER_OCR.md @@ -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! 🎉 diff --git a/Dockerfile b/Dockerfile index fcd64a7..a04b145 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/OCR_SETUP.md b/OCR_SETUP.md new file mode 100644 index 0000000..b68a5d6 --- /dev/null +++ b/OCR_SETUP.md @@ -0,0 +1,275 @@ +# Guía de Instalación y Configuración de OCR + +## � ¿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. + +## �📋 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 diff --git a/README.md b/README.md index b03dc41..27ef67b 100644 --- a/README.md +++ b/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** diff --git a/app/api/v1/debug.py b/app/api/v1/debug.py new file mode 100644 index 0000000..5c66f0a --- /dev/null +++ b/app/api/v1/debug.py @@ -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)}" + ) diff --git a/app/api/v1/incrementables.py b/app/api/v1/incrementables.py index 16fa0be..d08f3dd 100644 --- a/app/api/v1/incrementables.py +++ b/app/api/v1/incrementables.py @@ -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: diff --git a/app/core/config.py b/app/core/config.py index 8f734e6..e3616c3 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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" diff --git a/app/main.py b/app/main.py index eff92ed..0c62b7d 100644 --- a/app/main.py +++ b/app/main.py @@ -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__": diff --git a/app/services/ocr_service.py b/app/services/ocr_service.py new file mode 100644 index 0000000..371721d --- /dev/null +++ b/app/services/ocr_service.py @@ -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)}") diff --git a/app/services/pdf_text.py b/app/services/pdf_text.py index 142e7fb..72c503b 100644 --- a/app/services/pdf_text.py +++ b/app/services/pdf_text.py @@ -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") diff --git a/app/tasks/parse_tasks.py b/app/tasks/parse_tasks.py index ed787a2..9ce0c8f 100644 --- a/app/tasks/parse_tasks.py +++ b/app/tasks/parse_tasks.py @@ -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 { diff --git a/debug_pdf.ps1 b/debug_pdf.ps1 new file mode 100644 index 0000000..6a2f78d --- /dev/null +++ b/debug_pdf.ps1 @@ -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 "" diff --git a/docker-compose.yml b/docker-compose.yml index 9bb9f7c..5fd9fb0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/install_ocr.ps1 b/install_ocr.ps1 new file mode 100644 index 0000000..3272942 --- /dev/null +++ b/install_ocr.ps1 @@ -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") diff --git a/requirements.txt b/requirements.txt index bfa7f47..43f819a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/test_ocr.py b/test_ocr.py new file mode 100644 index 0000000..6d2cd4f --- /dev/null +++ b/test_ocr.py @@ -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())