#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Script d'analyse d'image avec contexte pour les tickets de support. Extrait des informations pertinentes d'une image en fonction du contexte du ticket. """ import os import sys import json import argparse import logging from typing import Dict, Any, Optional # Configuration du logger logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("analyze_image.log"), logging.StreamHandler() ] ) logger = logging.getLogger("analyze_image") try: from llm import Pixtral except ImportError: logger.error("Module LLM non trouvé. Veuillez vous assurer que le répertoire parent est dans PYTHONPATH.") sys.exit(1) class ImageAnalyzer: """ Analyseur d'image qui extrait des informations pertinentes en fonction du contexte. """ def __init__(self, api_key: Optional[str] = None): """ Initialise l'analyseur d'image. Args: api_key: Clé API pour le modèle de vision """ self.llm = Pixtral(api_key=api_key) # Configurer le modèle de vision try: self.llm.model = "pixtral-12b-2409" self.llm.temperature = 0.3 self.llm.max_tokens = 1024 except Exception as e: logger.warning(f"Impossible de configurer le modèle: {e}") self.historique = [] def ajouter_historique(self, action: str, entree: str, resultat: str) -> None: """ Ajoute une entrée à l'historique des actions. Args: action: Type d'action effectuée entree: Entrée de l'action resultat: Résultat de l'action """ self.historique.append({ "action": action, "entree": entree, "resultat": resultat }) def analyser_image(self, image_path: str, contexte: Optional[str] = None) -> Dict[str, Any]: """ Analyse une image en fonction du contexte donné. Args: image_path: Chemin vers l'image à analyser contexte: Contexte du ticket pour aider à l'analyse Returns: Résultat de l'analyse de l'image """ if not os.path.exists(image_path): logger.error(f"Image introuvable: {image_path}") return { "success": False, "erreur": "Image introuvable", "path": image_path } # Vérifier que le fichier est une image _, extension = os.path.splitext(image_path) if extension.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']: logger.error(f"Format de fichier non supporté: {extension}") return { "success": False, "erreur": f"Format de fichier non supporté: {extension}", "path": image_path } # Préparer le prompt pour l'analyse prompt_base = """ Tu es un expert en analyse technique d'interfaces utilisateur et de captures d'écran. Analyse cette image en détail et extrait les informations suivantes: 1. Type d'image: capture d'écran, photo, schéma, etc. 2. Interface visible: nom du logiciel, type d'interface, fonctionnalités visibles 3. Éléments importants: boutons, menus, messages d'erreur, données visibles 4. Problème potentiel: erreurs, anomalies, incohérences visibles 5. Contexte technique: environnement logiciel, version potentielle, plateforme Pour les captures d'écran, identifie précisément: - Le nom exact de la fenêtre/dialogue - Les champs/formulaires visibles - Les valeurs/données affichées - Les messages d'erreur ou d'avertissement - Les boutons/actions disponibles Réponds de manière structurée en format Markdown avec des sections claires. Sois précis et factuel, en te concentrant sur les éléments techniques visibles. """ # Ajouter le contexte si disponible if contexte: prompt_base += f""" CONTEXTE DU TICKET: {contexte} En tenant compte du contexte ci-dessus, explique également: - En quoi cette image est pertinente pour le problème décrit - Quels éléments de l'image correspondent au problème mentionné - Comment cette image peut aider à résoudre le problème """ try: # Appeler le modèle de vision try: resultat = self.llm.analyze_image(image_path, prompt_base) self.ajouter_historique("analyze_image", os.path.basename(image_path), "Analyse effectuée") except Exception as e: logger.error(f"Erreur lors de l'appel au modèle de vision: {str(e)}") return { "success": False, "erreur": f"Erreur lors de l'appel au modèle de vision: {str(e)}", "path": image_path } # Extraire le contenu de la réponse contenu = resultat.get("content", "") if not contenu: logger.error("Réponse vide du modèle de vision") return { "success": False, "erreur": "Réponse vide du modèle de vision", "path": image_path } # Créer le résultat final resultat_analyse = { "success": True, "path": image_path, "analyse": contenu, "contexte_fourni": bool(contexte) } # Essayer d'extraire des informations structurées à partir de l'analyse try: # Rechercher le type d'image import re type_match = re.search(r"Type d['']image\s*:\s*([^\n\.]+)", contenu, re.IGNORECASE) if type_match: resultat_analyse["type_image"] = type_match.group(1).strip() # Rechercher l'interface interface_match = re.search(r'Interface\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) interface_match2 = re.search(r'Interface visible\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) if interface_match: resultat_analyse["interface"] = interface_match.group(1).strip() elif interface_match2: resultat_analyse["interface"] = interface_match2.group(1).strip() # Rechercher le problème probleme_match = re.search(r'Problème\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) probleme_match2 = re.search(r'Problème potentiel\s*:\s*([^\n\.]+)', contenu, re.IGNORECASE) if probleme_match: resultat_analyse["probleme"] = probleme_match.group(1).strip() elif probleme_match2: resultat_analyse["probleme"] = probleme_match2.group(1).strip() except Exception as e: logger.warning(f"Impossible d'extraire des informations structurées: {str(e)}") return resultat_analyse except Exception as e: logger.error(f"Erreur lors de l'analyse de l'image {image_path}: {str(e)}") return { "success": False, "erreur": str(e), "path": image_path } def generer_rapport_markdown(self, analyse: Dict[str, Any]) -> str: """ Génère un rapport Markdown à partir de l'analyse d'image. Args: analyse: Résultat de l'analyse d'image Returns: Rapport au format Markdown """ if not analyse.get("success", False): return f"# Échec de l'analyse d'image\n\nErreur: {analyse.get('erreur', 'Inconnue')}\n\nImage: {analyse.get('path', 'Inconnue')}" # En-tête du rapport image_path = analyse.get("path", "Inconnue") image_name = os.path.basename(image_path) rapport = f"# Analyse de l'image: {image_name}\n\n" # Ajouter l'analyse brute rapport += analyse.get("analyse", "Aucune analyse disponible") # Ajouter des métadonnées rapport += "\n\n## Métadonnées\n\n" rapport += f"- **Chemin de l'image**: `{image_path}`\n" rapport += f"- **Contexte fourni**: {'Oui' if analyse.get('contexte_fourni', False) else 'Non'}\n" if "type_image" in analyse: rapport += f"- **Type d'image détecté**: {analyse['type_image']}\n" if "interface" in analyse: rapport += f"- **Interface identifiée**: {analyse['interface']}\n" if "probleme" in analyse: rapport += f"- **Problème détecté**: {analyse['probleme']}\n" # Ajouter les paramètres du modèle rapport += "\n## Paramètres du modèle\n\n" rapport += f"- **Modèle**: {getattr(self.llm, 'model', 'pixtral-12b-2409')}\n" rapport += f"- **Température**: {getattr(self.llm, 'temperature', 0.3)}\n" return rapport def charger_config(): """ Charge la configuration depuis config.json. Returns: Configuration chargée """ config_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.json") if not os.path.exists(config_path): logger.warning(f"Fichier de configuration non trouvé: {config_path}") return {"llm": {"api_key": None}} try: with open(config_path, 'r', encoding='utf-8') as f: config = json.load(f) return config except Exception as e: logger.error(f"Erreur lors du chargement de la configuration: {str(e)}") return {"llm": {"api_key": None}} def main(): """ Point d'entrée du script. """ parser = argparse.ArgumentParser(description="Analyse une image en fonction du contexte du ticket.") parser.add_argument("--image", "-i", required=True, help="Chemin vers l'image à analyser") parser.add_argument("--contexte", "-c", help="Chemin vers un fichier contenant le contexte du ticket") parser.add_argument("--ticket-info", "-t", help="Chemin vers un fichier ticket_info.json pour extraire le contexte") parser.add_argument("--output", "-o", help="Chemin du fichier de sortie pour le rapport Markdown (par défaut: _analyse.md)") parser.add_argument("--format", "-f", choices=["json", "md", "both"], default="both", help="Format de sortie (json, md, both)") parser.add_argument("--verbose", "-v", action="store_true", help="Afficher plus d'informations") args = parser.parse_args() # Configurer le niveau de log if args.verbose: logging.getLogger().setLevel(logging.DEBUG) # Vérifier que l'image existe if not os.path.exists(args.image): logger.error(f"Image non trouvée: {args.image}") sys.exit(1) # Charger le contexte si disponible contexte = None if args.contexte and os.path.exists(args.contexte): try: with open(args.contexte, 'r', encoding='utf-8') as f: contexte = f.read() logger.info(f"Contexte chargé depuis {args.contexte}") except Exception as e: logger.warning(f"Impossible de charger le contexte depuis {args.contexte}: {str(e)}") # Extraire le contexte depuis ticket_info.json si disponible if not contexte and args.ticket_info and os.path.exists(args.ticket_info): try: with open(args.ticket_info, 'r', encoding='utf-8') as f: ticket_info = json.load(f) if isinstance(ticket_info, dict): contexte = f""" TICKET: {ticket_info.get('code', 'Inconnu')} - {ticket_info.get('name', 'Sans titre')} DESCRIPTION: {ticket_info.get('description', 'Aucune description')} """ logger.info(f"Contexte extrait depuis {args.ticket_info}") except Exception as e: logger.warning(f"Impossible de charger le contexte depuis {args.ticket_info}: {str(e)}") # Déterminer les chemins de sortie if not args.output: output_base = os.path.splitext(args.image)[0] output_md = f"{output_base}_analyse.md" output_json = f"{output_base}_analyse.json" else: output_base = os.path.splitext(args.output)[0] output_md = f"{output_base}.md" output_json = f"{output_base}.json" # Charger la configuration config = charger_config() api_key = config.get("llm", {}).get("api_key") # Initialiser l'analyseur d'image analyzer = ImageAnalyzer(api_key=api_key) try: # Analyser l'image resultat = analyzer.analyser_image(args.image, contexte) if not resultat.get("success", False): logger.error(f"Échec de l'analyse: {resultat.get('erreur', 'Erreur inconnue')}") sys.exit(1) # Générer le rapport Markdown rapport_md = analyzer.generer_rapport_markdown(resultat) # Sauvegarder les résultats selon le format demandé if args.format in ["json", "both"]: with open(output_json, 'w', encoding='utf-8') as f: json.dump(resultat, f, indent=2, ensure_ascii=False) logger.info(f"Résultat JSON sauvegardé: {output_json}") if args.format in ["md", "both"]: with open(output_md, 'w', encoding='utf-8') as f: f.write(rapport_md) logger.info(f"Rapport Markdown sauvegardé: {output_md}") # Afficher un résumé print("\nRésumé de l'analyse:") print(f"Image: {os.path.basename(args.image)}") if "type_image" in resultat: print(f"Type d'image: {resultat['type_image']}") if "interface" in resultat: print(f"Interface: {resultat['interface']}") if "probleme" in resultat: print(f"Problème: {resultat['probleme']}") if args.format in ["json", "both"]: print(f"Résultat JSON: {output_json}") if args.format in ["md", "both"]: print(f"Rapport Markdown: {output_md}") except Exception as e: logger.error(f"Erreur lors de l'analyse: {str(e)}") import traceback logger.debug(f"Détails: {traceback.format_exc()}") sys.exit(1) if __name__ == "__main__": main()