feat(effi-ocr): add effi-server deployment configuration
- docker-compose for effi-server (192.168.1.27) - Uses same network and volumes as OpenWebUI - Port 8301
This commit is contained in:
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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 ==="
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user