File size: 15,836 Bytes
f92da22 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 |
import os
import logging
from datetime import datetime
from typing import List
from docx import Document
from docx.shared import Inches
from docx.enum.style import WD_STYLE_TYPE
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
from docx.shared import RGBColor
from docx.oxml.shared import OxmlElement, qn
# Importer les classes du premier fichier
from template_matcher import TemplateMatcher, TemplateMatch
from dotenv import load_dotenv
# Charger les variables d'environnement
load_dotenv()
DB_PATH = os.getenv("TEMPLATE_DB_PATH", "templates/medical_templates.pkl")
OUTPUT_DIR = os.getenv("OUTPUT_DIR", "templates_remplis")
class TemplateGenerator:
"""Génère des templates médicaux remplis au format .doc"""
def __init__(self):
"""Initialise le générateur de templates"""
self.output_dir = OUTPUT_DIR
self._create_output_directory()
# Configuration du logging pour ce module
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - [GENERATOR] %(message)s'
)
def _create_output_directory(self):
"""Crée le répertoire de sortie s'il n'existe pas"""
if not os.path.exists(self.output_dir):
os.makedirs(self.output_dir)
logging.info(f"📁 Répertoire de sortie créé: {self.output_dir}")
def _add_custom_styles(self, doc: Document):
"""Ajoute des styles personnalisés au document"""
styles = doc.styles
# Style pour les titres de section
try:
section_style = styles.add_style('Section Title', WD_STYLE_TYPE.PARAGRAPH)
section_style.font.size = Inches(0.16) # 12pt
section_style.font.bold = True
section_style.font.color.rgb = RGBColor(0, 51, 102) # Bleu foncé
section_style.paragraph_format.space_after = Inches(0.1)
section_style.paragraph_format.keep_with_next = True
except:
logging.warning("Style 'Section Title' déjà existant")
# Style pour le contenu des sections
try:
content_style = styles.add_style('Section Content', WD_STYLE_TYPE.PARAGRAPH)
content_style.font.size = Inches(0.14) # 11pt
content_style.paragraph_format.left_indent = Inches(0.25)
content_style.paragraph_format.space_after = Inches(0.15)
except:
logging.warning("Style 'Section Content' déjà existant")
# Style pour l'en-tête
try:
header_style = styles.add_style('Document Header', WD_STYLE_TYPE.PARAGRAPH)
header_style.font.size = Inches(0.18) # 14pt
header_style.font.bold = True
header_style.font.color.rgb = RGBColor(0, 0, 0)
header_style.paragraph_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
header_style.paragraph_format.space_after = Inches(0.2)
except:
logging.warning("Style 'Document Header' déjà existant")
def _add_document_header(self, doc: Document, template_match: TemplateMatch, transcription_filename: str):
"""Ajoute l'en-tête du document"""
# Titre principal
header = doc.add_paragraph()
header.style = 'Document Header'
header.add_run("COMPTE-RENDU MÉDICAL GÉNÉRÉ AUTOMATIQUEMENT")
# Informations du template
info_paragraph = doc.add_paragraph()
info_paragraph.add_run("Template utilisé: ").bold = True
info_paragraph.add_run(os.path.basename(template_match.template_info.filepath))
# Informations médicales
if template_match.template_info.medecin and template_match.template_info.medecin != "Non identifié":
medecin_para = doc.add_paragraph()
medecin_para.add_run("Médecin: ").bold = True
medecin_para.add_run(template_match.template_info.medecin)
centre = getattr(template_match.template_info, 'centre_medical', 'Non spécifié')
if centre and centre != "Non spécifié":
centre_para = doc.add_paragraph()
centre_para.add_run("Centre médical: ").bold = True
centre_para.add_run(centre)
# Type de document
type_para = doc.add_paragraph()
type_para.add_run("Type de document: ").bold = True
type_para.add_run(template_match.template_info.type)
# Informations de génération
generation_para = doc.add_paragraph()
generation_para.add_run("Date de génération: ").bold = True
generation_para.add_run(datetime.now().strftime("%d/%m/%Y à %H:%M"))
score_para = doc.add_paragraph()
score_para.add_run("Score de correspondance: ").bold = True
score_para.add_run(f"{template_match.overall_score:.3f} ({template_match.confidence_level})")
filling_para = doc.add_paragraph()
filling_para.add_run("Pourcentage de remplissage: ").bold = True
filling_para.add_run(f"{template_match.filling_percentage:.1f}%")
# Ligne de séparation
doc.add_paragraph("_" * 80)
def _add_filled_sections(self, doc: Document, template_match: TemplateMatch):
"""Ajoute les sections remplies au document"""
if not template_match.extracted_data:
logging.warning("❌ Aucune section à remplir trouvée")
doc.add_paragraph("Aucune section n'a pu être remplie automatiquement.")
return
logging.info(f"📝 Génération de {len(template_match.extracted_data)} sections remplies")
# Ajouter un titre pour les sections remplies
sections_title = doc.add_paragraph()
sections_title.add_run("CONTENU EXTRAIT ET STRUCTURÉ").bold = True
sections_title.add_run().font.size = Inches(0.18)
for section_name, content in template_match.extracted_data.items():
# Titre de section
section_title = doc.add_paragraph()
section_title.style = 'Section Title'
section_title.add_run(f"{section_name.upper()}")
# Contenu de section
section_content = doc.add_paragraph()
section_content.style = 'Section Content'
section_content.add_run(content)
logging.info(f" ✅ Section ajoutée: {section_name} ({len(content)} caractères)")
def _add_missing_sections(self, doc: Document, template_match: TemplateMatch):
"""Ajoute les sections manquantes au document"""
missing_sections = [s.section_name for s in template_match.section_matches.values() if not s.can_fill]
if missing_sections:
logging.info(f"⚠️ {len(missing_sections)} sections manquantes identifiées")
# Titre pour les sections manquantes
missing_title = doc.add_paragraph()
missing_title.add_run("SECTIONS NON REMPLIES").bold = True
missing_title.add_run().font.color.rgb = RGBColor(204, 102, 0) # Orange
missing_subtitle = doc.add_paragraph()
missing_subtitle.add_run("(Informations non trouvées dans la transcription)")
missing_subtitle.add_run().font.color.rgb = RGBColor(102, 102, 102) # Gris
for section in missing_sections:
missing_para = doc.add_paragraph()
missing_para.add_run(f"• {section}")
missing_para.add_run().font.color.rgb = RGBColor(204, 102, 0)
# Ajouter un espace pour remplissage manuel
placeholder = doc.add_paragraph()
placeholder.style = 'Section Content'
placeholder.add_run("[À COMPLÉTER MANUELLEMENT]")
placeholder.add_run().font.color.rgb = RGBColor(153, 153, 153) # Gris clair
placeholder.add_run().italic = True
def _add_original_transcription(self, doc: Document, transcription: str):
"""Ajoute la transcription originale en annexe"""
# Saut de page
doc.add_page_break()
# Titre de l'annexe
annexe_title = doc.add_paragraph()
annexe_title.add_run("ANNEXE - TRANSCRIPTION ORIGINALE").bold = True
annexe_title.add_run().font.size = Inches(0.16)
annexe_title.add_run().font.color.rgb = RGBColor(102, 102, 102)
# Ligne de séparation
doc.add_paragraph("=" * 60)
# Transcription originale
transcription_para = doc.add_paragraph()
transcription_para.add_run(transcription)
transcription_para.add_run().font.size = Inches(0.12) # Texte plus petit
transcription_para.add_run().font.color.rgb = RGBColor(51, 51, 51) # Gris foncé
def generate_filled_template(self, template_match: TemplateMatch, transcription: str, transcription_filename: str) -> str:
"""
Génère un template rempli et le sauvegarde au format .doc
Args:
template_match: Le template avec le meilleur score
transcription: La transcription originale
transcription_filename: Le nom du fichier de transcription
Returns:
str: Le chemin du fichier généré
"""
logging.info("🚀 Début de la génération du template rempli")
logging.info(f"📋 Template sélectionné: {template_match.template_id}")
logging.info(f"📊 Score: {template_match.overall_score:.3f}")
logging.info(f"🔧 Remplissage: {template_match.filling_percentage:.1f}%")
try:
# Créer un nouveau document Word
doc = Document()
# Ajouter les styles personnalisés
self._add_custom_styles(doc)
# Ajouter l'en-tête du document
self._add_document_header(doc, template_match, transcription_filename)
# Ajouter les sections remplies
self._add_filled_sections(doc, template_match)
# Ajouter les sections manquantes
self._add_missing_sections(doc, template_match)
# Ajouter la transcription originale en annexe
self._add_original_transcription(doc, transcription)
# Générer le nom de fichier de sortie
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_template_id = template_match.template_id.replace('/', '_').replace('\\', '_')
output_filename = f"template_rempli_{safe_template_id}_{timestamp}.docx"
output_path = os.path.join(self.output_dir, output_filename)
# Sauvegarder le document
doc.save(output_path)
logging.info(f"✅ Template rempli généré avec succès:")
logging.info(f" 📁 Fichier: {output_path}")
logging.info(f" 📏 Taille: {os.path.getsize(output_path)} bytes")
logging.info(f" 📋 Sections remplies: {len(template_match.extracted_data)}")
logging.info(f" ⚠️ Sections manquantes: {len([s for s in template_match.section_matches.values() if not s.can_fill])}")
return output_path
except Exception as e:
logging.error(f"❌ Erreur lors de la génération du template: {e}")
raise
def display_generation_summary(self, template_match: TemplateMatch, output_path: str):
"""Affiche un résumé de la génération dans les logs"""
logging.info("=" * 80)
logging.info("📊 RÉSUMÉ DE LA GÉNÉRATION")
logging.info("=" * 80)
logging.info(f"🎯 Template utilisé: {template_match.template_id}")
logging.info(f"📁 Template source: {os.path.basename(template_match.template_info.filepath)}")
logging.info(f"👨⚕️ Médecin: {template_match.template_info.medecin}")
logging.info(f"🏥 Centre: {getattr(template_match.template_info, 'centre_medical', 'Non spécifié')}")
logging.info(f"📝 Type: {template_match.template_info.type}")
logging.info(f"📊 Score de correspondance: {template_match.overall_score:.3f} ({template_match.confidence_level})")
logging.info(f"🔧 Pourcentage de remplissage: {template_match.filling_percentage:.1f}%")
logging.info(f"📋 Sections remplies: {len(template_match.extracted_data)}")
logging.info(f"⚠️ Sections manquantes: {len([s for s in template_match.section_matches.values() if not s.can_fill])}")
logging.info(f"💾 Fichier généré: {os.path.basename(output_path)}")
logging.info(f"📏 Taille du fichier: {os.path.getsize(output_path)} bytes")
logging.info("=" * 80)
def main():
"""Fonction principale qui utilise le premier fichier pour matcher puis génère le template"""
# Configuration du logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
# Chemin de la base de données
db_path = DB_PATH
# Exemple de transcription
transcription_filename = "default.73.931915433.rtf_3650535_radiologie.doc"
transcription_content = """ la Technique :** 3 plans T2, diffusion axiale, T2 grand champ et T1 Dixon.
Résultats
L'utérus est antéversé, antéfléchi, latéralisé à droite, de taille normale pour l'âge.
L'endomètre est fin, mesurant moins de 2 mm.
Pas d'adénomyose franche.
Aspect normal du col utérin et du vagin.
L'ovaire droit, en position postérieure, mesure 18 x 11 mm avec présence de 4 follicules.
L'ovaire gauche, en position latéro-utérine, présente un volumineux endométriome de 45 mm, typique en hypersignal T1 Dixon.
Deuxième endométriome accolé à l'ovaire droit, périphérique, mesurant 13 mm.
Pas d'épaississement marqué du torus ni des ligaments utéro-sacrés.
Pas d'autre localisation pelvienne.
Pas d'épanchement pelvien.
Pas d'anomalie de la vessie.
Pas d'adénomégalie pelvienne, pas de dilatation des uretères.
en Conclusion
Endométriome ovarien droit périphérique de 13 mm.
Endométriome ovarien gauche centro-ovarien de 45 mm."""
if not os.path.exists(db_path):
logging.error(f"❌ Base de données non trouvée: {db_path}")
return
try:
logging.info("🚀 DÉMARRAGE DU PROCESSUS COMPLET")
logging.info("=" * 80)
# ÉTAPE 1: Matching avec le premier fichier
logging.info("📍 ÉTAPE 1: MATCHING DES TEMPLATES")
matcher = TemplateMatcher(db_path)
matches = matcher.match_templates(transcription_content, transcription_filename, k=3)
if not matches:
logging.error("❌ Aucun template trouvé")
return
# Sélectionner le meilleur template
best_match = matches[0]
logging.info(f"✅ Meilleur template sélectionné: {best_match.template_id}")
# ÉTAPE 2: Génération avec le deuxième fichier
logging.info("📍 ÉTAPE 2: GÉNÉRATION DU TEMPLATE REMPLI")
generator = TemplateGenerator()
output_path = generator.generate_filled_template(
best_match,
transcription_content,
transcription_filename
)
# ÉTAPE 3: Affichage du résumé
logging.info("📍 ÉTAPE 3: RÉSUMÉ FINAL")
generator.display_generation_summary(best_match, output_path)
logging.info("🎉 PROCESSUS TERMINÉ AVEC SUCCÈS")
except Exception as e:
logging.error(f"❌ Erreur dans le processus principal: {e}")
if __name__ == "__main__":
main() |