feat(vwb): Remplacer EasyOCR par docTR (Mindee) pour l'OCR

docTR est plus performant et mieux maintenu. Crée un service OCR
partagé (singleton paresseux) utilisé par verify_text_content et
extraire_tableau, avec les mêmes signatures et fallbacks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-02-18 22:19:44 +01:00
parent 786e640de9
commit 773ee78949
3 changed files with 130 additions and 51 deletions

View File

@@ -7,7 +7,7 @@ de l'écran, en utilisant l'OCR pour extraire et comparer le texte.
Modes OCR disponibles:
- ollama: Utilise un modèle de vision local (GPU, meilleure qualité)
- easyocr: OCR traditionnel (CPU/GPU, plus rapide)
- doctr: OCR traditionnel (CPU/GPU, plus rapide)
"""
from typing import Dict, Any, List, Optional
@@ -34,7 +34,7 @@ class VWBVerifyTextContentAction(BaseVWBAction):
Supporte deux modes OCR:
- ollama: Modèle de vision local (meilleure qualité, utilise GPU)
- easyocr: OCR traditionnel (plus rapide, fallback)
- doctr: OCR traditionnel (plus rapide, fallback)
"""
# Configuration Ollama par défaut (centralisée via variable d'environnement)
@@ -70,7 +70,7 @@ class VWBVerifyTextContentAction(BaseVWBAction):
self.case_sensitive = parameters.get('case_sensitive', False)
# Configuration OCR
self.ocr_mode = parameters.get('ocr_mode', 'ollama') # ollama (GPU) ou easyocr
self.ocr_mode = parameters.get('ocr_mode', 'ollama') # ollama (GPU) ou doctr
self.ollama_model = parameters.get('ollama_model', self.OLLAMA_MODEL)
self.ollama_url = parameters.get('ollama_url', self.OLLAMA_URL)
@@ -100,8 +100,8 @@ class VWBVerifyTextContentAction(BaseVWBAction):
errors.append(f"Mode de matching invalide: {self.match_mode}")
# Vérifier le mode OCR
if self.ocr_mode not in ['ollama', 'easyocr']:
errors.append(f"Mode OCR invalide: {self.ocr_mode} (utilisez 'ollama' ou 'easyocr')")
if self.ocr_mode not in ['ollama', 'doctr', 'easyocr']:
errors.append(f"Mode OCR invalide: {self.ocr_mode} (utilisez 'ollama' ou 'doctr')")
return errors
@@ -214,7 +214,7 @@ class VWBVerifyTextContentAction(BaseVWBAction):
if self.ocr_mode == 'ollama':
return self._extract_with_ollama(screenshot)
else:
return self._extract_with_easyocr(screenshot)
return self._extract_with_doctr(screenshot)
except Exception as e:
print(f"❌ Erreur extraction texte: {e}")
@@ -264,20 +264,20 @@ class VWBVerifyTextContentAction(BaseVWBAction):
return extracted_text
else:
print(f" ⚠️ Erreur Ollama: {response.status_code}")
# Fallback sur easyocr
return self._extract_with_easyocr(image)
# Fallback sur docTR
return self._extract_with_doctr(image)
except requests.exceptions.ConnectionError:
print(f" ⚠️ Ollama non disponible, fallback sur easyocr")
return self._extract_with_easyocr(image)
print(f" ⚠️ Ollama non disponible, fallback sur docTR")
return self._extract_with_doctr(image)
except Exception as e:
print(f" ⚠️ Erreur Ollama: {e}, fallback sur easyocr")
return self._extract_with_easyocr(image)
print(f" ⚠️ Erreur Ollama: {e}, fallback sur docTR")
return self._extract_with_doctr(image)
def _extract_with_easyocr(self, image) -> Optional[str]:
def _extract_with_doctr(self, image) -> Optional[str]:
"""
Extrait le texte en utilisant EasyOCR.
Extrait le texte en utilisant docTR.
Args:
image: Image PIL à analyser
@@ -286,29 +286,20 @@ class VWBVerifyTextContentAction(BaseVWBAction):
Texte extrait ou None si erreur
"""
try:
import easyocr
import numpy as np
from services.ocr_service import ocr_extract_text
print("📝 Extraction OCR via EasyOCR...")
print("📝 Extraction OCR via docTR...")
# Convertir en array numpy
img_array = np.array(image)
# EasyOCR (GPU si disponible)
reader = easyocr.Reader(['fr', 'en'], gpu=True)
results = reader.readtext(img_array)
# Combiner les résultats
extracted_text = ' '.join([result[1] for result in results])
extracted_text = ocr_extract_text(image)
print(f" ✅ Texte extrait ({len(extracted_text)} caractères)")
return extracted_text.strip()
except ImportError:
print(" ⚠️ easyocr non disponible")
print(" ⚠️ docTR non disponible")
return ""
except Exception as e:
print(f" ❌ Erreur EasyOCR: {e}")
print(f" ❌ Erreur docTR: {e}")
return None
def _compare_text(self, extracted: str, expected: str) -> bool: