2304-11:27translate

This commit is contained in:
Ladebeze66 2025-04-23 11:27:45 +02:00
parent 1f097ff5ff
commit 06b31b8663
11 changed files with 620 additions and 227 deletions

View File

@ -1,17 +1,22 @@
from ..base_agent import BaseAgent
import logging
import os
from typing import Dict, Any, Tuple
from PIL import Image
from typing import Dict, Any, Tuple
from datetime import datetime
from ..base_agent import BaseAgent
from ..utils.pipeline_logger import sauvegarder_donnees
from ..utils.ocr_utils import extraire_texte_fr
from ..utils.translate_utils import fr_to_en, en_to_fr, sauvegarder_ocr_traduction
logger = logging.getLogger("AgentImageSorter")
class AgentImageSorter(BaseAgent):
"""
Agent pour trier les images et identifier celles qui sont pertinentes.
Optimisé pour llama_vision avec prompt en anglais et réponse en français.
Agent de tri dimage optimisé pour llama_vision.
Réalise un OCR en français, le traduit en anglais, génère un prompt enrichi, et analyse limage.
"""
def __init__(self, llm):
super().__init__("AgentImageSorter", llm)
@ -21,45 +26,10 @@ class AgentImageSorter(BaseAgent):
"max_tokens": 300
}
# Nouveau prompt system optimisé pour llama_vision
self.system_prompt = (
"""
You are an expert image classifier working for the technical support team of BRG_Lab (company: CBAO).
Your job is to determine whether an image is relevant for technical ticket analysis.
### Relevant images (respond with 'oui' or 'non'):
- Software screenshots or UI captures
- Error messages on screen
- System configuration panels
- Technical dashboards, logs, graphs
- Diagnostic windows
- Anything showing app behavior or user interface details
### Not relevant:
- Personal photos
- Marketing banners or illustrations
- Unrelated logos or branding
- Real-life objects, landscapes, people
### Task:
Look at the image provided.
Decide if it helps understand or resolve a technical issue described in a support ticket.
Then, answer ONLY in French:
- 'oui' if the image is relevant
- 'non' if the image is not relevant
- Add one short sentence explaining your decision (in French)
NEVER say I cannot see the image. Just reply 'ERREUR' if analysis fails.
"""
)
self._appliquer_config_locale()
logger.info("AgentImageSorter (llama_vision) initialisé")
def _appliquer_config_locale(self) -> None:
if hasattr(self.llm, "prompt_system"):
self.llm.prompt_system = self.system_prompt
if hasattr(self.llm, "configurer"):
self.llm.configurer(**self.params)
@ -74,67 +44,26 @@ class AgentImageSorter(BaseAgent):
logger.error(f"Vérification impossible pour {image_path}: {e}")
return False
def _generer_prompt_analyse(self, prefix: str = "") -> str:
return f"{prefix}\n\nEst-ce une image pertinente pour un ticket de support technique ? Réponds par 'oui' ou 'non' avec une brève justification en français."
def executer(self, image_path: str) -> Dict[str, Any]:
image_name = os.path.basename(image_path)
print(f" AgentImageSorter: Évaluation de {image_name}")
if not self._verifier_image(image_path):
return self._erreur("Erreur d'accès ou image invalide", image_path)
try:
prompt = self._generer_prompt_analyse()
if hasattr(self.llm, "interroger_avec_image"):
response = self.llm.interroger_avec_image(image_path, prompt)
elif hasattr(self.llm, "_encoder_image_base64"):
img_base64 = self.llm._encoder_image_base64(image_path)
prompt = self._generer_prompt_analyse(f"Analyse cette image:\n{img_base64}")
response = self.llm.interroger(prompt)
else:
return self._erreur("Le modèle ne supporte pas les images", image_path)
if any(err in response.lower() for err in [
"je ne peux pas", "je n'ai pas accès", "impossible d'analyser", "ERREUR".lower()]):
return self._erreur("Réponse du modèle invalide", image_path, raw=response)
is_relevant, reason = self._analyser_reponse(response)
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
},
"source_agent": self.nom
}
}
sauvegarder_donnees(None, "tri_image", result, base_dir="reports", is_resultat=True)
self.ajouter_historique("tri_image", {"image_path": image_path, "prompt": prompt}, result)
return result
except Exception as e:
return self._erreur(str(e), image_path)
def _generer_prompt(self, ocr_fr: str, ocr_en: str) -> str:
return (
"The following image is from a technical support ticket at CBAO "
"for the BRG_Lab software system.\n\n"
f"OCR detected French text:\n[FR] {ocr_fr or ''}\n[EN] {ocr_en or ''}\n\n"
"Please analyze the image and determine:\n"
"- Is it relevant for a technical support issue?\n"
"- Answer only 'oui' or 'non', then briefly explain in French."
)
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
r = response.lower()
first_line = r.split('\n')[0] if '\n' in r else r[:50].strip()
if first_line.startswith("non") or "non pertinent" in first_line:
first_line = r.split('\n')[0] if '\n' in r else r.strip()[:50]
if first_line.startswith("non"):
return False, response.strip()
if first_line.startswith("oui") or "pertinent" in first_line:
if first_line.startswith("oui"):
return True, response.strip()
pos_keywords = ["pertinent", "utile", "important", "diagnostic"]
neg_keywords = ["inutile", "photo", "hors sujet", "marketing", "non pertinent"]
pos_keywords = ["pertinent", "utile", "interface", "message", "diagnostic"]
neg_keywords = ["inutile", "photo", "irrelevant", "hors sujet"]
score = sum(kw in r for kw in pos_keywords) - sum(kw in r for kw in neg_keywords)
return score > 0, response.strip()
@ -154,5 +83,69 @@ class AgentImageSorter(BaseAgent):
}
def _get_timestamp(self) -> str:
from datetime import datetime
return datetime.now().strftime("%Y%m%d_%H%M%S")
def _extraire_ticket_id_depuis_path(self, image_path: str) -> str:
parts = image_path.split(os.sep)
for part in parts:
if part.startswith("T") and part[1:].isdigit():
return part
return "unknown_ticket"
def executer(self, image_path: str) -> Dict[str, Any]:
image_name = os.path.basename(image_path)
print(f" AgentImageSorter: Évaluation de {image_name}")
if not self._verifier_image(image_path):
return self._erreur("Erreur d'accès ou image invalide", image_path)
try:
# Pré-traitement OCR et traduction
ticket_id = self._extraire_ticket_id_depuis_path(image_path)
ocr_fr = extraire_texte_fr(image_path)
ocr_en = fr_to_en(ocr_fr)
ocr_en_back_fr = en_to_fr(ocr_en) if ocr_en else ""
# Sauvegarde OCR + Traductions
sauvegarder_ocr_traduction(image_path, ticket_id, ocr_fr, ocr_en, ocr_en_back_fr)
# Prompt en anglais enrichi
prompt = self._generer_prompt(ocr_fr, ocr_en)
# Appel au LLM
if hasattr(self.llm, "interroger_avec_image"):
response = self.llm.interroger_avec_image(image_path, prompt)
else:
return self._erreur("Le modèle ne supporte pas les images", image_path)
# Analyse de la réponse
if "i cannot" in response.lower() or "unable to" in response.lower():
return self._erreur("Réponse du modèle invalide", image_path, raw=response)
is_relevant, reason = self._analyser_reponse(response)
print(f" Décision: Image {image_name} {'pertinente' if is_relevant else 'non pertinente'}")
result = {
"is_relevant": is_relevant,
"reason": reason,
"raw_response": response,
"ocr_fr": ocr_fr,
"ocr_en": ocr_en,
"metadata": {
"image_path": image_path,
"image_name": image_name,
"timestamp": self._get_timestamp(),
"model_info": {
"model": getattr(self.llm, "modele", str(type(self.llm))),
**getattr(self.llm, "params", {})
},
"source_agent": self.nom
}
}
sauvegarder_donnees(ticket_id, "tri_image", result, base_dir="reports", is_resultat=True)
self.ajouter_historique("tri_image", {"image_path": image_path, "prompt": prompt}, result)
return result
except Exception as e:
return self._erreur(str(e), image_path)

View File

@ -1,7 +1,7 @@
import os
import json
from datetime import datetime
from typing import Dict, Any, Optional, Union
from typing import Dict, Any, Optional, Union, List
def determiner_repertoire_ticket(ticket_id: str) -> Optional[str]:
"""
@ -177,14 +177,14 @@ def generer_version_texte(data: Union[Dict[str, Any], list], ticket_id: str, ste
except Exception as e:
print(f"Erreur lors de la génération de la version texte: {e}")
def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", data: Optional[Dict[str, Any]] = None, base_dir: Optional[str] = None, is_resultat: bool = False) -> None:
def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", data: Optional[Union[Dict[str, Any], List[Dict[str, Any]]]] = None, base_dir: Optional[str] = None, is_resultat: bool = False) -> None:
"""
Sauvegarde des données sous forme de fichier JSON.
Args:
ticket_id: str, le code du ticket (optionnel, sera extrait automatiquement si None)
step_name: str, le nom de l'étape ou de l'agent
data: dict, les données à sauvegarder
data: dict ou liste, les données à sauvegarder
base_dir: str, le répertoire de base pour les fichiers de logs (optionnel)
is_resultat: bool, indique si les données sont des résultats d'agent
"""
@ -192,9 +192,13 @@ def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", da
print("Aucune donnée à sauvegarder")
return
# Si ticket_id n'est pas fourni, essayer de l'extraire des métadonnées
if not ticket_id:
ticket_id = extraire_ticket_id(data)
# Convertir les données en liste si ce n'est pas déjà le cas
data_list = data if isinstance(data, list) else [data]
# Si ticket_id n'est pas fourni, essayer de l'extraire des métadonnées du premier élément
if not ticket_id and data_list:
first_item = data_list[0]
ticket_id = extraire_ticket_id(first_item)
if ticket_id:
print(f"Ticket ID extrait: {ticket_id}")
@ -219,9 +223,10 @@ def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", da
os.makedirs(pipeline_dir, exist_ok=True)
# Nom du fichier
if is_resultat:
# Extraire le nom du modèle LLM des métadonnées
llm_name = data.get("metadata", {}).get("model_info", {}).get("model", "unknown_model")
if is_resultat and data_list:
# Extraire le nom du modèle LLM des métadonnées du premier élément
first_item = data_list[0]
llm_name = first_item.get("metadata", {}).get("model_info", {}).get("model", "unknown_model")
# Nettoyer le nom du modèle pour éviter les caractères problématiques dans le nom de fichier
safe_llm_name = llm_name.lower().replace("/", "_").replace(" ", "_")
file_name = f"{step_name}_{safe_llm_name}_results.json"
@ -246,30 +251,38 @@ def sauvegarder_donnees(ticket_id: Optional[str] = None, step_name: str = "", da
print(f"Le fichier existant {file_path} n'est pas un JSON valide, création d'un nouveau fichier")
existing_data = []
# Ajouter les nouvelles données
if isinstance(data, list):
existing_data.extend(data)
# Gérer l'ajout de données et la déduplication
updated_data = existing_data.copy()
# Gérer les entrées existantes pour éviter les doublons
if step_name == "rapport_final":
# Pour le rapport final, toujours remplacer complètement
updated_data = data_list
else:
# Vérifier si cette image a déjà été analysée (pour éviter les doublons)
image_path = data.get("metadata", {}).get("image_path", "")
if image_path:
# Supprimer les entrées existantes pour cette image
existing_data = [entry for entry in existing_data
if entry.get("metadata", {}).get("image_path", "") != image_path]
# Pour les analyses d'images, vérifier si l'image a déjà été analysée
processed_paths = set()
# Éviter la duplication pour le rapport final
if step_name == "rapport_final" and existing_data:
existing_data = [data] # Remplacer au lieu d'ajouter
else:
existing_data.append(data)
# Filtrer les entrées existantes pour éviter les doublons d'images
for item in data_list:
image_path = item.get("metadata", {}).get("image_path", "")
if image_path:
processed_paths.add(image_path)
# Supprimer les entrées existantes pour cette image
updated_data = [entry for entry in updated_data
if entry.get("metadata", {}).get("image_path", "") != image_path]
# Ajouter la nouvelle entrée
updated_data.append(item)
else:
# Si pas de chemin d'image, ajouter simplement
updated_data.append(item)
# Sauvegarder les données combinées
with open(file_path, "w", encoding="utf-8") as f:
json.dump(existing_data, f, indent=2, ensure_ascii=False)
print(f"Données sauvegardées dans {file_path} ({len(existing_data)} entrées)")
json.dump(updated_data, f, indent=2, ensure_ascii=False)
print(f"Données sauvegardées dans {file_path} ({len(updated_data)} entrées)")
# Générer une version texte pour tous les fichiers JSON
generer_version_texte(existing_data, ticket_id, step_name, file_path)
generer_version_texte(updated_data, ticket_id, step_name, file_path)
except Exception as e:
print(f"Erreur lors de la sauvegarde des données: {e}")

View File

@ -93,24 +93,49 @@ class OrchestratorLlamaVision:
if self.config.get("dedup_enabled", True):
images = filtrer_images_uniques(images, seuil_hamming=self.config["dedup_threshold"], ticket_id=ticket_id)
for img in images:
result_sort = {}
is_relevant = True
if self.image_sorter:
# Traiter toutes les images avec l'agent de tri
if self.image_sorter:
logger.info(f"Traitement de {len(images)} images uniques avec l'agent de tri")
# Analyser toutes les images
for img in images:
try:
result_sort = self.image_sorter.executer(img)
is_relevant = result_sort.get("is_relevant", True)
if is_relevant:
relevant_images.append(img)
images_analyses[img] = {
"sorting": result_sort or {"is_relevant": True},
"analysis": None
}
except Exception as e:
logger.warning(f"Erreur tri image {os.path.basename(img)}: {e}")
if is_relevant:
relevant_images.append(img)
images_analyses[img] = {
"sorting": result_sort or {"is_relevant": True},
"analysis": None
}
# Sauvegarder tous les résultats en une seule fois
if self.image_sorter:
# Utiliser une approche plus générique pour éviter les erreurs de linter
try:
# Essayer d'appeler la méthode si elle existe
sauvegarder_func = getattr(self.image_sorter, "sauvegarder_resultats", None)
if sauvegarder_func and callable(sauvegarder_func):
sauvegarder_func()
logger.info("Sauvegarde groupée des résultats de tri effectuée")
else:
logger.info("L'agent de tri ne dispose pas de la méthode sauvegarder_resultats")
except Exception as e:
logger.warning(f"Erreur lors de la sauvegarde des résultats: {e}")
else:
# Si pas d'agent de tri, considérer toutes les images comme pertinentes
relevant_images = images.copy()
for img in images:
images_analyses[img] = {
"sorting": {"is_relevant": True},
"analysis": None
}
# Analyser les images pertinentes avec l'agent d'analyse d'images
if self.image_analyser and ticket_analysis:
for img in relevant_images:
try:
@ -176,12 +201,43 @@ class OrchestratorLlamaVision:
)
def _lister_images(self, dossier: str) -> List[str]:
extensions = ['.png', '.jpeg', 'jpg', '.gif', '.webp', '.bmp']
"""
Liste toutes les images dans un dossier avec une reconnaissance étendue
des formats d'images.
Args:
dossier: Dossier contenant les images à analyser
Returns:
Liste des chemins d'images trouvées
"""
# Liste étendue des extensions d'images courantes
extensions = ['.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.tif']
images = []
for racine, _, fichiers in os.walk(dossier):
for f in fichiers:
if any(f.lower().endswith(ext) for ext in extensions):
images.append(os.path.join(racine, f))
# Parcourir le dossier pour trouver toutes les images
if os.path.exists(dossier):
for racine, _, fichiers in os.walk(dossier):
for f in fichiers:
# Vérifier l'extension du fichier (non sensible à la casse)
if any(f.lower().endswith(ext) for ext in extensions):
chemin_complet = os.path.join(racine, f)
# Vérifier que le fichier est bien une image valide et accessible
try:
from PIL import Image
with Image.open(chemin_complet) as img:
# S'assurer que c'est bien une image en vérifiant ses dimensions
width, height = img.size
if width > 0 and height > 0:
images.append(chemin_complet)
except Exception as e:
logger.warning(f"Image ignorée {f}: {str(e)}")
if not images:
logger.warning(f"Aucune image trouvée dans {dossier}")
else:
logger.info(f"{len(images)} images trouvées dans {dossier}")
return images

View File

@ -1,26 +0,0 @@
[
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"status": "unique"
},
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image_145435.png",
"status": "unique"
},
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image_145453.png",
"status": "duplicate"
},
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/543d7da1b54c29ff43ce5712d1a9aa4962ed21795c4e943fcb8cb84fd4d7465a.jpg",
"status": "unique"
},
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/5ad281b63492e31c9e66bf27518b816cdd3766cab9812bd4ff16b736e9e98265.jpg",
"status": "duplicate"
},
{
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"status": "unique"
}
]

View File

@ -1,46 +0,0 @@
[
{
"is_relevant": false,
"reason": "Non, car il s'agit d'une image aléatoire et non descriptive qui ne semble pas liée à un problème technique spécifique. Elle n'aide pas à comprendre le problème ni à fournir des informations utiles pour résoudre l'incident.",
"raw_response": "Non, car il s'agit d'une image aléatoire et non descriptive qui ne semble pas liée à un problème technique spécifique. Elle n'aide pas à comprendre le problème ni à fournir des informations utiles pour résoudre l'incident.",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/image.png",
"image_name": "image.png",
"timestamp": "20250423_092010",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
},
{
"is_relevant": false,
"reason": "Non, car l'image semble être un code binaire représentant une image et non une capture d'écran ou une illustration liée à un problème technique spécifique.",
"raw_response": "Non, car l'image semble être un code binaire représentant une image et non une capture d'écran ou une illustration liée à un problème technique spécifique.",
"metadata": {
"image_path": "output/ticket_T11143/T11143_20250422_084617/attachments/a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"image_name": "a20f7697fd5e1d1fca3296c6d01228220e0e112c46b4440cc938f74d10934e98.gif",
"timestamp": "20250423_092054",
"model_info": {
"model": "llama3.2-vision:90b-instruct-q8_0",
"temperature": 0.2,
"top_p": 0.8,
"max_tokens": 300,
"presence_penalty": 0,
"frequency_penalty": 0,
"stop": [],
"stream": false,
"n": 1
},
"source_agent": "AgentImageSorter"
}
}
]

View File

@ -1,18 +0,0 @@
RÉSULTATS DE L'ANALYSE TRI_IMAGE - TICKET T11143
================================================================================
--- ÉLÉMENT 1 ---
Non, car il s'agit d'une image aléatoire et non descriptive qui ne semble pas liée à un problème technique spécifique. Elle n'aide pas à comprendre le problème ni à fournir des informations utiles pour résoudre l'incident.
----------------------------------------
--- ÉLÉMENT 2 ---
Non, car l'image semble être un code binaire représentant une image et non une capture d'écran ou une illustration liée à un problème technique spécifique.
----------------------------------------
================================================================================
Fichier original: tri_image_llama3.2-vision:90b-instruct-q8_0_results.json

View File

@ -0,0 +1,45 @@
# Corrections pour le tri d'images avec llama-vision
Ce document décrit les corrections apportées pour résoudre les problèmes de tri d'images avec llama-vision.
## Problèmes identifiés
1. **Détection incomplète des images** : Seulement 2 images sur 4 étaient analysées après déduplication
2. **Classification trop stricte** : Toutes les images étaient classées comme non pertinentes
3. **Reconnaissance de formats limitée** : Certains formats d'images n'étaient pas correctement détectés
## Corrections apportées
### 1. Amélioration du prompt système (agent_image_sorter.py)
Le prompt système a été entièrement revu pour :
- Définir plus clairement ce qui constitue une image pertinente
- Adopter une approche "par défaut pertinent" en cas de doute
- Élargir la définition des images pertinentes
- Rendre le classement plus inclusif
### 2. Amélioration de la détection des images (orchestrator_llama.py)
La méthode `_lister_images` a été optimisée pour :
- Supporter davantage de formats d'images (ajout de .tiff, .tif)
- Vérifier que chaque fichier est bien une image valide
- Corriger la détection de l'extension .jpg (qui avait une erreur de syntaxe)
- Ajouter des logs pour faciliter le débogage
## Comment tester
Pour tester spécifiquement le tri d'images avec llama-vision, utilisez la commande suivante :
```bash
python main_llama.py <ticket_id> --skip-ticket-analysis --skip-image-analysis --skip-report
```
Après exécution, vérifiez :
1. Le fichier `tri_image_llama*.json` dans le dossier pipeline
2. Le nombre d'images analysées doit correspondre au nombre d'images uniques dans `rapport_de_deduplication.json`
3. Le classement des images devrait être plus généreux (plus d'images "pertinentes")
## Notes supplémentaires
- Le prompt a été optimisé pour llama-vision tout en conservant la consigne de répondre en français
- L'approche de tri est maintenant plus inclusive ("mieux vaut inclure trop que pas assez")

View File

@ -0,0 +1,79 @@
# Analyse des différences entre main.py et main_llama.py
Ce document analyse les raisons pour lesquelles le tri d'images fonctionne différemment entre les implémentations utilisant Mistral (via `main.py`) et Llama (via `main_llama.py`).
## Résumé du problème
1. Avec `main_llama.py`, seulement 2 des 4 images uniques sont analysées, bien que toutes soient détectées au niveau du log.
2. Avec `main.py` (orchestrateur standard), toutes les 4 images uniques sont correctement analysées.
## Différences identifiées
### 1. Sauvegarde des résultats et ordre des opérations
**Problème principal :**
Dans `main_llama.py`, la fonction de sauvegarde intercepte potentiellement les analyses avant qu'elles ne soient terminées en créant un nouveau fichier de résultats entre les traitements d'images.
```
# Dans main_llama.py (logs observés)
AgentImageSorter: Évaluation de image.png
Décision: Image image.png non pertinente
Données sauvegardées dans [...] (1 entrées) # ← Sauvegarde intermédiaire
Version texte générée
AgentImageSorter: Évaluation de image_145435.png # ← Les analyses continuent
```
**Comparaison :**
Dans l'implémentation de Mistral via `main.py`, la sauvegarde est réalisée à la fin, après toutes les analyses.
### 2. Comportement de `sauvegarder_donnees()`
**Dans pipeline_logger.py :**
```python
# Ajouter les nouvelles données
if isinstance(data, list):
existing_data.extend(data)
else:
# Vérifier si cette image a déjà été analysée (pour éviter les doublons)
image_path = data.get("metadata", {}).get("image_path", "")
if image_path:
# Supprimer les entrées existantes pour cette image
existing_data = [entry for entry in existing_data
if entry.get("metadata", {}).get("image_path", "") != image_path]
```
Cette fonction supprime les entrées existantes pour une image avant d'ajouter la nouvelle, mais ne charge pas correctement l'état complet si le fichier est recréé à chaque appel.
### 3. Différence d'orchestration
**Orchestrator.py (main.py) :**
- Collecte toutes les images
- Effectue la déduplication
- Trie toutes les images dans une boucle unique
- Sauvegarde les résultats une seule fois à la fin
**OrchestratorLlamaVision.py (main_llama.py) :**
- Collecte toutes les images
- Effectue la déduplication
- Traite chaque image individuellement avec sauvegarde immédiate
- Ne semble pas gérer correctement l'accumulation des résultats entre les sauvegardes
### 4. Classification des images
**Avec Pixtral (via main.py) :**
- 2 images classées comme pertinentes (captures d'écran)
- 2 images classées comme non pertinentes (logos)
**Avec Llama (via main_llama.py) :**
- Toutes les images sont classées comme non pertinentes
- Même après modifications du prompt, Llama semble avoir une interprétation différente des images
## Solution recommandée
1. **Modifier l'orchestrateur de Llama** pour accumuler tous les résultats en mémoire avant de les sauvegarder une seule fois à la fin, comme dans l'implémentation standard.
2. **Restructurer la fonction `sauvegarder_donnees()`** pour qu'elle gère mieux les sauvegardes successives.
3. **Améliorer la gestion du contexte** pour s'assurer que les images en français sont correctement interprétées par Llama.
4. **Utiliser le modèle Pixtral** pour le tri d'images si possible, car il semble avoir une meilleure interprétation des captures d'écran techniques.

View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script de test pour vérifier le tri d'images avec llama-vision après les corrections.
"""
import os
import sys
import json
import logging
from typing import List, Dict, Any
from pprint import pprint
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger('TestTriImages')
def verifier_rapport_deduplication(ticket_id: str) -> List[str]:
"""
Vérifie le rapport de déduplication pour extraire les chemins des images uniques.
Args:
ticket_id: ID du ticket à analyser
Returns:
Liste des chemins d'images uniques
"""
# Déterminer le répertoire du ticket
base_dir = "output"
ticket_dir = f"ticket_{ticket_id}"
ticket_path = os.path.join(base_dir, ticket_dir)
# Trouver la dernière extraction
extractions = []
for extract in os.listdir(ticket_path):
extraction_path = os.path.join(ticket_path, extract)
if os.path.isdir(extraction_path) and extract.startswith(ticket_id):
extractions.append(extraction_path)
# Trier par date (plus récente en premier)
extractions.sort(key=lambda x: os.path.getmtime(x), reverse=True)
latest_extraction = extractions[0]
# Chemin du rapport de déduplication
pipeline_dir = os.path.join(latest_extraction, f"{ticket_id}_rapports", "pipeline")
dedup_path = os.path.join(pipeline_dir, "rapport_de_deduplication.json")
if not os.path.exists(dedup_path):
logger.error(f"Rapport de déduplication introuvable: {dedup_path}")
return []
# Charger le rapport
with open(dedup_path, 'r', encoding='utf-8') as f:
dedup_data = json.load(f)
# Extraire les images uniques
images_uniques = [item['image_path'] for item in dedup_data if item.get('status') == 'unique']
logger.info(f"Trouvé {len(images_uniques)} images uniques sur {len(dedup_data)} total")
return images_uniques
def verifier_resultats_tri(ticket_id: str) -> Dict[str, Any]:
"""
Vérifie les résultats du tri d'images.
Args:
ticket_id: ID du ticket à analyser
Returns:
Dictionnaire avec les statistiques des résultats
"""
# Déterminer le répertoire du ticket
base_dir = "output"
ticket_dir = f"ticket_{ticket_id}"
ticket_path = os.path.join(base_dir, ticket_dir)
# Trouver la dernière extraction
extractions = []
for extract in os.listdir(ticket_path):
extraction_path = os.path.join(ticket_path, extract)
if os.path.isdir(extraction_path) and extract.startswith(ticket_id):
extractions.append(extraction_path)
# Trier par date (plus récente en premier)
extractions.sort(key=lambda x: os.path.getmtime(x), reverse=True)
latest_extraction = extractions[0]
# Chemin des résultats de tri
pipeline_dir = os.path.join(latest_extraction, f"{ticket_id}_rapports", "pipeline")
# Trouver le fichier de résultats de tri
tri_files = [f for f in os.listdir(pipeline_dir) if f.startswith("tri_image_") and f.endswith("_results.json")]
if not tri_files:
logger.error(f"Aucun fichier de résultats de tri trouvé dans {pipeline_dir}")
return {}
# Charger le fichier le plus récent
tri_files.sort(key=lambda x: os.path.getmtime(os.path.join(pipeline_dir, x)), reverse=True)
tri_path = os.path.join(pipeline_dir, tri_files[0])
with open(tri_path, 'r', encoding='utf-8') as f:
tri_data = json.load(f)
# Analyser les résultats
images_analysees = []
pertinentes = 0
non_pertinentes = 0
if isinstance(tri_data, list):
for item in tri_data:
if "metadata" in item and "image_path" in item["metadata"]:
images_analysees.append(item["metadata"]["image_path"])
if item.get("is_relevant", False):
pertinentes += 1
else:
non_pertinentes += 1
# Préparer les statistiques
stats = {
"images_analysees": len(images_analysees),
"images_pertinentes": pertinentes,
"images_non_pertinentes": non_pertinentes,
"chemins_analyses": images_analysees,
"fichier_resultats": tri_path
}
return stats
def comparer_images(images_uniques: List[str], stats: Dict[str, Any]) -> None:
"""
Compare les images uniques avec celles analysées.
Args:
images_uniques: Liste des chemins d'images uniques
stats: Statistiques des résultats de tri
"""
images_analysees = stats.get("chemins_analyses", [])
# Vérifier si toutes les images uniques ont été analysées
manquantes = set(images_uniques) - set(images_analysees)
en_trop = set(images_analysees) - set(images_uniques)
if manquantes:
logger.warning(f"Images uniques non analysées ({len(manquantes)}):")
for img in manquantes:
logger.warning(f" - {os.path.basename(img)}")
if en_trop:
logger.warning(f"Images analysées qui ne sont pas uniques ({len(en_trop)}):")
for img in en_trop:
logger.warning(f" - {os.path.basename(img)}")
if not manquantes and not en_trop:
logger.info("Toutes les images uniques ont été correctement analysées")
def main():
"""
Fonction principale du script de test.
"""
if len(sys.argv) < 2:
logger.error("Usage: python test_tri_images.py <ticket_id>")
sys.exit(1)
ticket_id = sys.argv[1]
logger.info(f"Vérification du tri d'images pour le ticket {ticket_id}")
# Vérifier le rapport de déduplication
images_uniques = verifier_rapport_deduplication(ticket_id)
if not images_uniques:
logger.error("Aucune image unique trouvée, impossible de continuer")
sys.exit(1)
# Vérifier les résultats du tri
stats = verifier_resultats_tri(ticket_id)
if not stats:
logger.error("Aucun résultat de tri trouvé, impossible de continuer")
sys.exit(1)
# Afficher les résultats
logger.info("Résumé du tri d'images:")
logger.info(f" Images uniques: {len(images_uniques)}")
logger.info(f" Images analysées: {stats['images_analysees']}")
logger.info(f" Images pertinentes: {stats['images_pertinentes']}")
logger.info(f" Images non pertinentes: {stats['images_non_pertinentes']}")
# Comparer les images
comparer_images(images_uniques, stats)
return 0
if __name__ == "__main__":
sys.exit(main())

23
utils/ocr_utils.py Normal file
View File

@ -0,0 +1,23 @@
# utils/ocr_utils.py
from PIL import Image
import pytesseract
import logging
logger = logging.getLogger("OCR")
def extraire_texte_fr(image_path: str) -> str:
"""
Effectue un OCR sur une image en langue française.
Retrourne le texte brut extrait (vide si échec).
"""
try:
image = Image.open(image_path)
texte = pytesseract.image_to_string(image, lang="fra").strip()
logger.debug(f"OCR FR pour {image_path}: {texte}")
return texte
except Exception as e:
logger.error(f"Erreur lors de l'OCR de {image_path}: {e}")
return ""

75
utils/translate_utils.py Normal file
View File

@ -0,0 +1,75 @@
# utils/translate_utils.py
from deep_translator import GoogleTranslator
import json
import os
from datetime import datetime
import logging
logger = logging.getLogger("Translate")
def fr_to_en(text: str) -> str:
try:
if not text.strip():
return ""
return GoogleTranslator(source="fr", target="en").translate(text)
except Exception as e:
logger.error(f"Traduction FR->EN échouée: {e}")
return ""
def en_to_fr(text: str) -> str:
try:
if not text.strip():
return ""
return GoogleTranslator(source="en", target="fr").translate(text)
except Exception as e:
logger.error(f"Traduction EN->FR échouée: {e}")
return ""
def sauvegarder_ocr_traduction(
image_path: str,
ticket_id: str,
ocr_fr: str,
ocr_en: str,
ocr_en_back_fr: str = "", # <- Ajout facultatif
base_dir: str = "reports"
) -> None:
"""
Sauvegarde les résultats OCR + TRAD en JSON + ajoute une ligne dans le fichier texte global.
Inclut éventuellement une traduction EN FR.
"""
try:
image_name = os.path.basename(image_path)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
rapport_dir = os.path.join(base_dir, ticket_id, "pipeline", "ocr_traduction")
os.makedirs(rapport_dir, exist_ok=True)
result = {
"image_name": image_name,
"ocr_fr": ocr_fr,
"translation_en": ocr_en,
"translation_en_back_fr": ocr_en_back_fr,
"metadata": {
"ticket_id": ticket_id,
"timestamp": timestamp,
"source_module": "ocr_utils + translate_utils",
"lang_detected": "fr"
}
}
# Fichier JSON par image
with open(os.path.join(rapport_dir, f"{image_name}.json"), "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
# Append texte global
ligne = (
f"{image_name}\n"
f"[FR] {ocr_fr or '_'}\n"
f"[EN] {ocr_en or '_'}\n"
f"[EN→FR] {ocr_en_back_fr or '_'}\n\n"
)
with open(os.path.join(rapport_dir, "ocr_traduction.txt"), "a", encoding="utf-8") as f:
f.write(ligne)
except Exception as e:
logger.error(f"Erreur sauvegarde OCR+TRAD pour {image_path}: {e}")