llm_ticket3/utils/image_extractor/html_image_extractor.py
2025-04-09 16:13:20 +02:00

449 lines
19 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Module pour extraire les images intégrées dans les messages HTML d'Odoo.
Complémentaire à l'extracteur d'images actuel, il détecte spécifiquement les images
référencées dans le HTML avec des balises <img> et les associe aux pièces jointes.
"""
import os
import re
import logging
import json
from typing import Dict, List, Any, Optional, Set, Tuple
from bs4 import BeautifulSoup
class HtmlImageExtractor:
"""
Extracteur d'images intégrées dans les messages HTML d'Odoo.
"""
def __init__(self, ticket_dir: str):
"""
Initialise l'extracteur d'images HTML.
Args:
ticket_dir: Répertoire du ticket contenant les messages et pièces jointes
"""
self.ticket_dir = ticket_dir
self.messages_file = os.path.join(ticket_dir, "messages_raw.json")
self.attachments_file = os.path.join(ticket_dir, "attachments_info.json")
self.output_file = os.path.join(ticket_dir, "embedded_images.json")
# Cache pour les données
self._messages = None
self._attachments = None
def _load_messages(self) -> List[Dict[str, Any]]:
"""
Charge les messages du ticket.
Returns:
Liste des messages
"""
if self._messages is not None:
return self._messages
# Essayer différents fichiers potentiels contenant les messages
possible_message_files = [
self.messages_file, # messages_raw.json
os.path.join(self.ticket_dir, "messages.json"),
os.path.join(self.ticket_dir, "all_messages.json")
]
for msg_file in possible_message_files:
if os.path.exists(msg_file):
try:
with open(msg_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Gérer différents formats de fichiers de messages
if isinstance(data, dict):
if "messages" in data:
self._messages = data["messages"]
return self._messages
# Si pas de clé messages mais d'autres clés, essayer de trouver les messages
for key, value in data.items():
if isinstance(value, list) and len(value) > 0:
# Vérifier si ça ressemble à des messages (ont une clé body ou content)
if isinstance(value[0], dict) and any(k in value[0] for k in ["body", "content"]):
self._messages = value
return self._messages
# Si c'est directement une liste, supposer que ce sont des messages
elif isinstance(data, list):
self._messages = data
return self._messages
except Exception as e:
logging.error(f"Erreur lors du chargement du fichier {msg_file}: {e}")
# Si on arrive ici, aucun fichier valide n'a été trouvé
logging.error(f"Aucun fichier de messages valide trouvé dans: {self.ticket_dir}")
return []
def _load_attachments(self) -> Dict[int, Dict[str, Any]]:
"""
Charge les pièces jointes du ticket.
Returns:
Dictionnaire des pièces jointes indexées par ID
"""
if self._attachments is not None:
return self._attachments
# Essayer différents fichiers potentiels de pièces jointes
possible_attachment_files = [
self.attachments_file, # attachments_info.json
os.path.join(self.ticket_dir, "attachments.json")
]
for att_file in possible_attachment_files:
if os.path.exists(att_file):
try:
with open(att_file, 'r', encoding='utf-8') as f:
attachments = json.load(f)
if not isinstance(attachments, list):
# Si ce n'est pas une liste mais un dict, essayer de trouver une liste
for key, value in attachments.items():
if isinstance(value, list) and len(value) > 0:
attachments = value
break
if not isinstance(attachments, list):
continue
# Indexer par ID pour un accès rapide
self._attachments = {}
for att in attachments:
if "id" in att:
self._attachments[att["id"]] = att
elif "attachment_id" in att:
self._attachments[att["attachment_id"]] = att
if self._attachments:
return self._attachments
except Exception as e:
logging.error(f"Erreur lors du chargement des pièces jointes depuis {att_file}: {e}")
# Si on arrive ici, aucun fichier valide n'a été trouvé
logging.error(f"Aucun fichier de pièces jointes valide trouvé dans: {self.ticket_dir}")
return {}
def _get_tag_attribute(self, tag: Any, attr_name: str, default: str = "") -> str:
"""
Récupère un attribut d'une balise HTML de manière sécurisée.
Args:
tag: Balise HTML
attr_name: Nom de l'attribut à récupérer
default: Valeur par défaut si l'attribut n'existe pas
Returns:
Valeur de l'attribut
"""
# Vérifier que c'est bien une balise et qu'elle a la méthode get
if not hasattr(tag, 'get'):
return default
# Récupérer l'attribut si présent
try:
value = tag.get(attr_name)
return str(value) if value is not None else default
except (AttributeError, KeyError, TypeError):
return default
def extract_image_references(self) -> Dict[str, Any]:
"""
Extrait les références aux images dans le HTML des messages.
Returns:
Dictionnaire contenant les références d'images trouvées
"""
messages = self._load_messages()
attachments_by_id = self._load_attachments()
if not messages:
logging.error("Aucun message trouvé pour l'extraction d'images")
return {"status": "error", "message": "Aucun message trouvé", "references": []}
# Stocker les références trouvées
image_references = []
# Ensemble pour dédupliquer
processed_ids = set()
for message in messages:
# Déterminer quel champ contient le contenu HTML
message_body = None
message_id = None
# Différentes possibilités de noms pour le contenu HTML et l'ID
for body_key in ["body", "content", "html_content", "message"]:
if body_key in message and message[body_key]:
message_body = message[body_key]
break
for id_key in ["id", "message_id", "uid"]:
if id_key in message:
message_id = message[id_key]
break
if not message_body or not message_id:
continue
# Méthode 1: Extraction depuis les balises <img> dans le HTML
try:
# Analyser le HTML du message
soup = BeautifulSoup(message_body, "html.parser")
# Trouver toutes les balises <img>
img_tags = soup.find_all("img")
for img in img_tags:
# Utiliser la méthode sécurisée pour récupérer les attributs
src = self._get_tag_attribute(img, "src")
# Ignorer les images vides ou data URLs
if not src or src.startswith("data:"):
continue
# Différents patterns de référence d'images
image_id = None
# Pattern 1: /web/image/ID?access_token=...
match = re.search(r"/web/image/(\d+)", src)
if match:
image_id = int(match.group(1))
# Pattern 2: /web/content/ID?...
if not image_id:
match = re.search(r"/web/content/(\d+)", src)
if match:
image_id = int(match.group(1))
# Pattern 3: /web/static/ID?...
if not image_id:
match = re.search(r"/web/static/(\d+)", src)
if match:
image_id = int(match.group(1))
# Pattern 4: /web/binary/image?id=ID&...
if not image_id:
match = re.search(r"[?&]id=(\d+)", src)
if match:
image_id = int(match.group(1))
if image_id:
self._ajouter_reference_image(image_id, message_id, attachments_by_id, img, processed_ids, image_references)
except Exception as e:
logging.error(f"Erreur lors de l'analyse des balises <img> dans le message {message_id}: {e}")
# Méthode 2: Extraction depuis les références textuelles à la fin du message
# Format: "- Nom_image.ext (image/type) [ID: 12345]"
try:
# Chercher des références d'images dans le texte du message
# Pattern des références: "- Nom_fichier.ext (type/mime) [ID: 12345]"
img_ref_pattern = r"- ([^\(\)]+) \(([^\(\)]*)\) \[ID: (\d+)\]"
for match in re.finditer(img_ref_pattern, message_body):
try:
nom_fichier = match.group(1).strip()
mimetype = match.group(2).strip()
image_id = int(match.group(3))
if image_id in processed_ids:
continue
processed_ids.add(image_id)
# Vérifier si cette image est dans nos pièces jointes
if image_id in attachments_by_id:
attachment = attachments_by_id[image_id]
# Récupérer le chemin local de l'image
local_path = attachment.get("local_path")
# Vérifier que le fichier existe réellement
if local_path and not os.path.exists(local_path):
# Essayer de chercher dans d'autres endroits potentiels du répertoire
for root, dirs, files in os.walk(self.ticket_dir):
for file in files:
if os.path.basename(local_path) == file:
local_path = os.path.join(root, file)
break
if os.path.exists(local_path):
break
image_ref = {
"image_id": image_id,
"message_id": message_id,
"attachment_id": attachment.get("id", attachment.get("attachment_id", 0)),
"name": nom_fichier, # Utiliser le nom trouvé dans le texte
"mimetype": mimetype, # Utiliser le type MIME trouvé dans le texte
"file_size": attachment.get("file_size", 0),
"local_path": local_path,
"source": "text_reference"
}
image_references.append(image_ref)
logging.info(f"Image référencée trouvée dans le texte: ID {image_id} dans message {message_id}")
except Exception as e:
logging.error(f"Erreur lors du traitement de la référence d'image textuelle: {e}")
except Exception as e:
logging.error(f"Erreur lors de l'analyse des références textuelles dans le message {message_id}: {e}")
# Sauvegarder les références trouvées
result = {
"status": "success" if image_references else "warning",
"message": f"Trouvé {len(image_references)} références d'images intégrées",
"references": image_references
}
try:
with open(self.output_file, 'w', encoding='utf-8') as f:
json.dump(result, f, indent=2, ensure_ascii=False)
logging.info(f"Références d'images sauvegardées dans: {self.output_file}")
except Exception as e:
logging.error(f"Erreur lors de la sauvegarde des références d'images: {e}")
return result
def _ajouter_reference_image(self, image_id: int, message_id: str, attachments_by_id: Dict[int, Dict[str, Any]],
img: Any, processed_ids: Set[int], image_references: List[Dict[str, Any]]) -> None:
"""
Ajoute une référence d'image à la liste des références
Args:
image_id: ID de l'image
message_id: ID du message
attachments_by_id: Dictionnaire des pièces jointes indexées par ID
img: Balise HTML de l'image
processed_ids: Ensemble des IDs déjà traités
image_references: Liste des références d'images à enrichir
"""
# Éviter les duplications
if image_id in processed_ids:
return
processed_ids.add(image_id)
# Vérifier si cette image est dans nos pièces jointes
if image_id in attachments_by_id:
attachment = attachments_by_id[image_id]
# Récupérer les attributs de manière sécurisée
width = self._get_tag_attribute(img, "width")
height = self._get_tag_attribute(img, "height")
alt = self._get_tag_attribute(img, "alt")
# Récupérer le chemin local de l'image
local_path = attachment.get("local_path")
# Vérifier que le fichier existe réellement
if local_path and not os.path.exists(local_path):
# Essayer de chercher dans d'autres endroits potentiels du répertoire
for root, dirs, files in os.walk(self.ticket_dir):
for file in files:
if os.path.basename(local_path) == file:
local_path = os.path.join(root, file)
break
if os.path.exists(local_path):
break
image_ref = {
"image_id": image_id,
"message_id": message_id,
"attachment_id": attachment.get("id", attachment.get("attachment_id", 0)),
"name": attachment.get("name", ""),
"mimetype": attachment.get("mimetype", ""),
"file_size": attachment.get("file_size", 0),
"local_path": local_path,
"img_width": width,
"img_height": height,
"img_alt": alt,
"source": "html_img_tag"
}
image_references.append(image_ref)
logging.info(f"Image intégrée trouvée: ID {image_id} dans message {message_id}")
def get_image_paths(self) -> List[str]:
"""
Récupère les chemins des images référencées.
Returns:
Liste des chemins locaux des images référencées
"""
try:
# Tenter d'extraire les images
data = self.extract_image_references()
# Récupérer les chemins locaux des images
paths = []
for ref in data.get("references", []):
path = ref.get("local_path")
if path and os.path.exists(path):
paths.append(path)
if not paths:
logging.warning("Aucune image intégrée trouvée ou les chemins sont invalides")
return paths
except Exception as e:
logging.error(f"Erreur lors de la récupération des chemins d'images: {e}")
return []
def extract_images_from_ticket(ticket_dir: str) -> List[str]:
"""
Fonction utilitaire pour extraire les images intégrées dans les messages HTML d'un ticket.
Args:
ticket_dir: Répertoire du ticket contenant les messages et pièces jointes
Returns:
Liste des chemins locaux des images référencées
"""
extractor = HtmlImageExtractor(ticket_dir)
return extractor.get_image_paths()
if __name__ == "__main__":
# Test avec un répertoire de ticket spécifique
import sys
if len(sys.argv) > 1:
ticket_dir = sys.argv[1]
else:
# Utiliser un répertoire de test par défaut
ticket_dir = "./output/ticket_T0241/T0241_20250409_141018"
if not os.path.exists(ticket_dir):
print(f"Répertoire introuvable: {ticket_dir}")
sys.exit(1)
print(f"Extraction des images intégrées dans le HTML pour le ticket: {os.path.basename(ticket_dir)}")
extractor = HtmlImageExtractor(ticket_dir)
result = extractor.extract_image_references()
print(f"Statut: {result['status']}")
print(f"Message: {result['message']}")
print(f"Nombre de références trouvées: {len(result['references'])}")
for i, ref in enumerate(result["references"]):
print(f"\nImage {i+1}:")
print(f" ID: {ref['image_id']}")
print(f" Nom: {ref['name']}")
print(f" Type: {ref['mimetype']}")
print(f" Taille: {ref['file_size']} octets")
print(f" Chemin local: {ref['local_path']}")
# Récupérer les chemins des images
paths = extractor.get_image_paths()
print(f"\nChemins des images ({len(paths)}):")
for path in paths:
print(f" {path}")