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()