diff --git a/effi-ocr-server/Dockerfile b/effi-ocr-server/Dockerfile new file mode 100644 index 0000000..d7784d8 --- /dev/null +++ b/effi-ocr-server/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + tesseract-ocr \ + tesseract-ocr-fra \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY main.py . + +EXPOSE 8301 + +CMD ["python", "main.py"] diff --git a/effi-ocr-server/docker-compose.yml b/effi-ocr-server/docker-compose.yml new file mode 100644 index 0000000..15dac06 --- /dev/null +++ b/effi-ocr-server/docker-compose.yml @@ -0,0 +1,33 @@ +# Effi-OCR sur effi-server +# Port: 8301 +# OpenWebUI tourne sur le même serveur - accède aux mêmes fichiers uploadés + +services: + effi-ocr: + build: . + container_name: effi-ocr + ports: + - "8301:8301" + restart: unless-stopped + environment: + - PORT=8301 + - PYTHONUNBUFFERED=1 + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8301/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + volumes: + - open-webui-data:/app/backend/data:ro + networks: + - effi-net + +networks: + effi-net: + name: effi-net + external: true + +volumes: + open-webui-data: + external: true diff --git a/effi-ocr-server/install.sh b/effi-ocr-server/install.sh new file mode 100644 index 0000000..c99cb7a --- /dev/null +++ b/effi-ocr-server/install.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Installation de effi-ocr sur effi-server (192.168.1.27) +# Port: 8301 + +set -e + +echo "=== Installation effi-ocr sur effi-server ===" + +# Créer le dossier +ssh root@192.168.1.27 "mkdir -p /root/effi-ocr" + +# Copier les fichiers +scp /home/folzo/effi-ocr-server/* root@192.168.1.27:/root/effi-ocr/ + +# Build et start +ssh root@192.168.1.27 "cd /root/effi-ocr && docker compose up -d --build" + +# Vérifier +sleep 5 +ssh root@192.168.1.27 "curl -s http://localhost:8301/health | jq ." + +echo "" +echo "=== effi-ocr installé sur http://192.168.1.27:8301 ===" diff --git a/effi-ocr-server/main.py b/effi-ocr-server/main.py new file mode 100644 index 0000000..0478361 --- /dev/null +++ b/effi-ocr-server/main.py @@ -0,0 +1,210 @@ +""" +Effi-OCR : Service OCR pour Effi +API FastAPI qui expose Tesseract via une interface REST +""" + +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.responses import JSONResponse +from PIL import Image +import pytesseract +import io +import logging +from typing import Optional + +# Configuration logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +app = FastAPI( + title="Effi-OCR", + description="Service OCR pour extraction de texte d'images", + version="1.0.0" +) + + +@app.get("/health") +async def health_check(): + """Endpoint de santé pour monitoring""" + try: + # Vérifier que Tesseract est accessible + import subprocess + result = subprocess.run(['tesseract', '--version'], capture_output=True, text=True) + version_line = result.stdout.split('\n')[0] if result.stdout else "unknown" + tesseract_version = str(version_line) # Forcer en string + except Exception as e: + tesseract_version = f"error: {str(e)}" + + return { + "status": "healthy", + "service": "effi-ocr", + "version": "1.0.0", + "tesseract_version": tesseract_version + } + + +@app.post("/ocr") +async def ocr_image( + file: UploadFile = File(...), + lang: str = "fra", + preprocess: bool = True +): + """ + Extraire le texte d'une image + + Args: + file: Image à analyser (jpg, png, etc.) + lang: Code langue (fra, eng, fra+eng, etc.) + preprocess: Appliquer un prétraitement pour améliorer la qualité + + Returns: + JSON avec le texte extrait et les métadonnées + """ + logger.info(f"OCR demandé - lang: {lang}, fichier: {file.filename}") + + # Vérifier le type de fichier + allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp'] + if file.content_type not in allowed_types: + raise HTTPException( + status_code=400, + detail=f"Type de fichier non supporté: {file.content_type}. Types acceptés: {', '.join(allowed_types)}" + ) + + try: + # Lire l'image + contents = await file.read() + image = Image.open(io.BytesIO(contents)) + + # Convertir en RGB si nécessaire (pour les images avec transparence) + if image.mode in ('RGBA', 'LA', 'P'): + image = image.convert('RGB') + + # Prétraitement optionnel pour améliorer la qualité OCR + if preprocess: + # Convertir en niveaux de gris pour meilleure détection texte + image = image.convert('L') + # Augmenter le contraste + from PIL import ImageEnhance + enhancer = ImageEnhance.Contrast(image) + image = enhancer.enhance(2.0) + + # OCR avec Tesseract + text = pytesseract.image_to_string(image, lang=lang) + + # Nettoyer le texte + text = text.strip() + + # Informations sur l'image + width, height = image.size + + logger.info(f"OCR terminé - {len(text)} caractères extraits") + + return JSONResponse(content={ + "success": True, + "text": text, + "language": lang, + "filename": file.filename, + "image_size": {"width": width, "height": height}, + "characters_count": len(text), + "words_count": len(text.split()) if text else 0 + }) + + except Exception as e: + logger.error(f"Erreur OCR: {str(e)}") + raise HTTPException(status_code=500, detail=f"Erreur lors de l'OCR: {str(e)}") + + +@app.post("/ocr/base64") +async def ocr_base64( + image_base64: str, + lang: str = "fra", + preprocess: bool = True +): + """ + Extraire le texte d'une image en base64 + + Args: + image_base64: Image encodée en base64 + lang: Code langue (fra, eng, fra+eng, etc.) + preprocess: Appliquer un prétraitement + + Returns: + JSON avec le texte extrait + """ + import base64 + + logger.info(f"OCR base64 demandé - lang: {lang}") + + try: + # Décoder le base64 + image_data = base64.b64decode(image_base64) + image = Image.open(io.BytesIO(image_data)) + + # Convertir en RGB si nécessaire + if image.mode in ('RGBA', 'LA', 'P'): + image = image.convert('RGB') + + # Prétraitement + if preprocess: + image = image.convert('L') + from PIL import ImageEnhance + enhancer = ImageEnhance.Contrast(image) + image = enhancer.enhance(2.0) + + # OCR + text = pytesseract.image_to_string(image, lang=lang) + text = text.strip() + + width, height = image.size + + logger.info(f"OCR base64 terminé - {len(text)} caractères") + + return { + "success": True, + "text": text, + "language": lang, + "image_size": {"width": width, "height": height}, + "characters_count": len(text), + "words_count": len(text.split()) if text else 0 + } + + except Exception as e: + logger.error(f"Erreur OCR base64: {str(e)}") + raise HTTPException(status_code=500, detail=f"Erreur lors de l'OCR: {str(e)}") + + +@app.get("/languages") +async def list_languages(): + """Lister les langues disponibles pour l'OCR""" + try: + # Commande tesseract pour lister les langues + import subprocess + result = subprocess.run( + ['tesseract', '--list-langs'], + capture_output=True, + text=True + ) + + # Parser la sortie + lines = result.stdout.strip().split('\n') + # La première ligne est "List of available languages (xxx):" + languages = lines[1:] if len(lines) > 1 else [] + + return { + "available_languages": languages, + "count": len(languages) + } + except Exception as e: + logger.error(f"Erreur listage langues: {str(e)}") + # Fallback avec les langues communes + return { + "available_languages": ["fra", "eng", "fra+eng", "deu", "spa", "ita"], + "count": 6, + "note": "Liste par défaut, erreur lors de la récupération" + } + + +if __name__ == "__main__": + import uvicorn + import os + port = int(os.getenv("PORT", "8301")) + uvicorn.run(app, host="0.0.0.0", port=port) diff --git a/effi-ocr-server/requirements.txt b/effi-ocr-server/requirements.txt new file mode 100644 index 0000000..9a579d7 --- /dev/null +++ b/effi-ocr-server/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.109.0 +uvicorn[standard]==0.27.0 +python-multipart==0.0.6 +pillow==10.2.0 +pytesseract==0.3.10