mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-15 21:06:52 +01:00
2304-11:27translate
This commit is contained in:
parent
1f097ff5ff
commit
06b31b8663
@ -1,17 +1,22 @@
|
|||||||
from ..base_agent import BaseAgent
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Any, Tuple
|
|
||||||
from PIL import Image
|
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.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")
|
logger = logging.getLogger("AgentImageSorter")
|
||||||
|
|
||||||
class AgentImageSorter(BaseAgent):
|
class AgentImageSorter(BaseAgent):
|
||||||
"""
|
"""
|
||||||
Agent pour trier les images et identifier celles qui sont pertinentes.
|
Agent de tri d’image optimisé pour llama_vision.
|
||||||
Optimisé pour llama_vision avec prompt en anglais et réponse en français.
|
Réalise un OCR en français, le traduit en anglais, génère un prompt enrichi, et analyse l’image.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, llm):
|
def __init__(self, llm):
|
||||||
super().__init__("AgentImageSorter", llm)
|
super().__init__("AgentImageSorter", llm)
|
||||||
|
|
||||||
@ -21,45 +26,10 @@ class AgentImageSorter(BaseAgent):
|
|||||||
"max_tokens": 300
|
"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()
|
self._appliquer_config_locale()
|
||||||
logger.info("AgentImageSorter (llama_vision) initialisé")
|
logger.info("AgentImageSorter (llama_vision) initialisé")
|
||||||
|
|
||||||
def _appliquer_config_locale(self) -> None:
|
def _appliquer_config_locale(self) -> None:
|
||||||
if hasattr(self.llm, "prompt_system"):
|
|
||||||
self.llm.prompt_system = self.system_prompt
|
|
||||||
if hasattr(self.llm, "configurer"):
|
if hasattr(self.llm, "configurer"):
|
||||||
self.llm.configurer(**self.params)
|
self.llm.configurer(**self.params)
|
||||||
|
|
||||||
@ -74,67 +44,26 @@ class AgentImageSorter(BaseAgent):
|
|||||||
logger.error(f"Vérification impossible pour {image_path}: {e}")
|
logger.error(f"Vérification impossible pour {image_path}: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _generer_prompt_analyse(self, prefix: str = "") -> str:
|
def _generer_prompt(self, ocr_fr: str, ocr_en: 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."
|
return (
|
||||||
|
"The following image is from a technical support ticket at CBAO "
|
||||||
def executer(self, image_path: str) -> Dict[str, Any]:
|
"for the BRG_Lab software system.\n\n"
|
||||||
image_name = os.path.basename(image_path)
|
f"OCR detected French text:\n[FR] {ocr_fr or '—'}\n[EN] {ocr_en or '—'}\n\n"
|
||||||
print(f" AgentImageSorter: Évaluation de {image_name}")
|
"Please analyze the image and determine:\n"
|
||||||
|
"- Is it relevant for a technical support issue?\n"
|
||||||
if not self._verifier_image(image_path):
|
"- Answer only 'oui' or 'non', then briefly explain in French."
|
||||||
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 _analyser_reponse(self, response: str) -> Tuple[bool, str]:
|
def _analyser_reponse(self, response: str) -> Tuple[bool, str]:
|
||||||
r = response.lower()
|
r = response.lower()
|
||||||
first_line = r.split('\n')[0] if '\n' in r else r[:50].strip()
|
first_line = r.split('\n')[0] if '\n' in r else r.strip()[:50]
|
||||||
if first_line.startswith("non") or "non pertinent" in first_line:
|
if first_line.startswith("non"):
|
||||||
return False, response.strip()
|
return False, response.strip()
|
||||||
if first_line.startswith("oui") or "pertinent" in first_line:
|
if first_line.startswith("oui"):
|
||||||
return True, response.strip()
|
return True, response.strip()
|
||||||
|
|
||||||
pos_keywords = ["pertinent", "utile", "important", "diagnostic"]
|
pos_keywords = ["pertinent", "utile", "interface", "message", "diagnostic"]
|
||||||
neg_keywords = ["inutile", "photo", "hors sujet", "marketing", "non pertinent"]
|
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)
|
score = sum(kw in r for kw in pos_keywords) - sum(kw in r for kw in neg_keywords)
|
||||||
return score > 0, response.strip()
|
return score > 0, response.strip()
|
||||||
|
|
||||||
@ -154,5 +83,69 @@ class AgentImageSorter(BaseAgent):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_timestamp(self) -> str:
|
def _get_timestamp(self) -> str:
|
||||||
from datetime import datetime
|
|
||||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
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)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
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]:
|
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:
|
except Exception as e:
|
||||||
print(f"Erreur lors de la génération de la version texte: {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.
|
Sauvegarde des données sous forme de fichier JSON.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticket_id: str, le code du ticket (optionnel, sera extrait automatiquement si None)
|
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
|
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)
|
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
|
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")
|
print("Aucune donnée à sauvegarder")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Si ticket_id n'est pas fourni, essayer de l'extraire des métadonnées
|
# Convertir les données en liste si ce n'est pas déjà le cas
|
||||||
if not ticket_id:
|
data_list = data if isinstance(data, list) else [data]
|
||||||
ticket_id = extraire_ticket_id(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:
|
if ticket_id:
|
||||||
print(f"Ticket ID extrait: {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)
|
os.makedirs(pipeline_dir, exist_ok=True)
|
||||||
|
|
||||||
# Nom du fichier
|
# Nom du fichier
|
||||||
if is_resultat:
|
if is_resultat and data_list:
|
||||||
# Extraire le nom du modèle LLM des métadonnées
|
# Extraire le nom du modèle LLM des métadonnées du premier élément
|
||||||
llm_name = data.get("metadata", {}).get("model_info", {}).get("model", "unknown_model")
|
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
|
# 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(" ", "_")
|
safe_llm_name = llm_name.lower().replace("/", "_").replace(" ", "_")
|
||||||
file_name = f"{step_name}_{safe_llm_name}_results.json"
|
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")
|
print(f"Le fichier existant {file_path} n'est pas un JSON valide, création d'un nouveau fichier")
|
||||||
existing_data = []
|
existing_data = []
|
||||||
|
|
||||||
# Ajouter les nouvelles données
|
# Gérer l'ajout de données et la déduplication
|
||||||
if isinstance(data, list):
|
updated_data = existing_data.copy()
|
||||||
existing_data.extend(data)
|
|
||||||
|
# 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:
|
else:
|
||||||
# Vérifier si cette image a déjà été analysée (pour éviter les doublons)
|
# Pour les analyses d'images, vérifier si l'image a déjà été analysée
|
||||||
image_path = data.get("metadata", {}).get("image_path", "")
|
processed_paths = set()
|
||||||
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]
|
|
||||||
|
|
||||||
# Éviter la duplication pour le rapport final
|
# Filtrer les entrées existantes pour éviter les doublons d'images
|
||||||
if step_name == "rapport_final" and existing_data:
|
for item in data_list:
|
||||||
existing_data = [data] # Remplacer au lieu d'ajouter
|
image_path = item.get("metadata", {}).get("image_path", "")
|
||||||
else:
|
if image_path:
|
||||||
existing_data.append(data)
|
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
|
# Sauvegarder les données combinées
|
||||||
with open(file_path, "w", encoding="utf-8") as f:
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
json.dump(existing_data, f, indent=2, ensure_ascii=False)
|
json.dump(updated_data, f, indent=2, ensure_ascii=False)
|
||||||
print(f"Données sauvegardées dans {file_path} ({len(existing_data)} entrées)")
|
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
|
# 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:
|
except Exception as e:
|
||||||
print(f"Erreur lors de la sauvegarde des données: {e}")
|
print(f"Erreur lors de la sauvegarde des données: {e}")
|
||||||
@ -93,24 +93,49 @@ class OrchestratorLlamaVision:
|
|||||||
if self.config.get("dedup_enabled", True):
|
if self.config.get("dedup_enabled", True):
|
||||||
images = filtrer_images_uniques(images, seuil_hamming=self.config["dedup_threshold"], ticket_id=ticket_id)
|
images = filtrer_images_uniques(images, seuil_hamming=self.config["dedup_threshold"], ticket_id=ticket_id)
|
||||||
|
|
||||||
for img in images:
|
# Traiter toutes les images avec l'agent de tri
|
||||||
result_sort = {}
|
if self.image_sorter:
|
||||||
is_relevant = True
|
logger.info(f"Traitement de {len(images)} images uniques avec l'agent de tri")
|
||||||
if self.image_sorter:
|
|
||||||
|
# Analyser toutes les images
|
||||||
|
for img in images:
|
||||||
try:
|
try:
|
||||||
result_sort = self.image_sorter.executer(img)
|
result_sort = self.image_sorter.executer(img)
|
||||||
is_relevant = result_sort.get("is_relevant", True)
|
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:
|
except Exception as e:
|
||||||
logger.warning(f"Erreur tri image {os.path.basename(img)}: {e}")
|
logger.warning(f"Erreur tri image {os.path.basename(img)}: {e}")
|
||||||
|
|
||||||
if is_relevant:
|
# Sauvegarder tous les résultats en une seule fois
|
||||||
relevant_images.append(img)
|
if self.image_sorter:
|
||||||
|
# Utiliser une approche plus générique pour éviter les erreurs de linter
|
||||||
images_analyses[img] = {
|
try:
|
||||||
"sorting": result_sort or {"is_relevant": True},
|
# Essayer d'appeler la méthode si elle existe
|
||||||
"analysis": None
|
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:
|
if self.image_analyser and ticket_analysis:
|
||||||
for img in relevant_images:
|
for img in relevant_images:
|
||||||
try:
|
try:
|
||||||
@ -176,12 +201,43 @@ class OrchestratorLlamaVision:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def _lister_images(self, dossier: str) -> List[str]:
|
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 = []
|
images = []
|
||||||
for racine, _, fichiers in os.walk(dossier):
|
|
||||||
for f in fichiers:
|
# Parcourir le dossier pour trouver toutes les images
|
||||||
if any(f.lower().endswith(ext) for ext in extensions):
|
if os.path.exists(dossier):
|
||||||
images.append(os.path.join(racine, f))
|
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
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -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
|
|
||||||
45
test_corrections/README.md
Normal file
45
test_corrections/README.md
Normal 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")
|
||||||
79
test_corrections/analyse_differences.md
Normal file
79
test_corrections/analyse_differences.md
Normal 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.
|
||||||
199
test_corrections/test_tri_images.py
Executable file
199
test_corrections/test_tri_images.py
Executable 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
23
utils/ocr_utils.py
Normal 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
75
utils/translate_utils.py
Normal 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}")
|
||||||
Loading…
x
Reference in New Issue
Block a user