Spaces:
Sleeping
Sleeping
| import re | |
| import logging | |
| from typing import Dict, List, Optional, Tuple, Any | |
| from dataclasses import dataclass | |
| from enum import Enum | |
| logger = logging.getLogger(__name__) | |
| class FieldType(Enum): | |
| """Types de champs dans le template""" | |
| CHECKBOX = "checkbox" # &x cases à cocher | |
| TEXT = "text" # &x texte libre | |
| MEASUREMENT = "measurement" # &x valeurs numériques | |
| class TemplateField: | |
| """Définition d'un champ du template""" | |
| placeholder: str # &x dans le template | |
| field_type: FieldType | |
| source_field: str # Champ correspondant dans ExtractedData | |
| default_value: str = "" | |
| validation_pattern: Optional[str] = None | |
| transformation_func: Optional[callable] = None | |
| context_identifier: Optional[str] = None # Pour différencier gauche/droite | |
| class MappingResult: | |
| """Résultat du mapping""" | |
| filled_template: str | |
| mapped_fields: Dict[str, str] | |
| unmapped_placeholders: List[str] | |
| mapping_confidence: float | |
| errors: List[str] | |
| class MedicalTemplateMapper: | |
| """Moteur de mapping des données extraites vers le template médical""" | |
| def __init__(self): | |
| self.template = self._load_template() | |
| self.field_mappings = self._define_field_mappings() | |
| self.checkbox_logic = self._define_checkbox_logic() | |
| def _load_template(self) -> str: | |
| """Template médical de base avec placeholders &x""" | |
| return """BILAN | |
| L'utérus est &x antéversé, &x rétroversé, &x intermédiaire, &x rétrofléchi, &x antéfléchi, &x fixe de taille normale (&x x &x x &x cm). | |
| Hystérométrie : distance orifice externe du col - fond de la cavité utérine : &x mm. | |
| L'endomètre : mesuré à &x mm. | |
| Myometre : pas de myome. | |
| Zone jonctionnelle : Atteinte de la zone de jonction : &x non &x oui | |
| Adénomyose associée : &x non &x oui : &x diffuse &x focale &x interne &x externe | |
| Col utérin: pas de kyste de Naboth. Absence de pathologies échographiquement décelable à son niveau. | |
| Cavité utérine en 3D: morphologie triangulaire. | |
| &xKISSING OVARIES | |
| L'ovaire droit mesure &x x &x mm, &x est de dimensions supérieures à la normale il mesure &x x &x mm, &xfolliculaire CFA &x follicules: (&x mm). &x Absence d'endométriome. &x Présence d'une formation kystique hypoéchogène, uniloculaire, non vascularisé, à contenu ground glass mesurée à &x mm d'allure endométriome. | |
| Accessibilité : &x rétro-utérin &x fixe &x aisée. | |
| L'ovaire gauche mesure &x x &x mm, &x est de dimensions supérieures à la normale il mesure &x x &x mm, &x folliculaire CFA &x follicules: (&x mm). &x Absence d'endométriome. &x Présence d'une formation kystique hypoéchogène, uniloculaire, non vascularisé, à contenu ground glass mesurée à &x mm d'allure endométriome. | |
| Accessibilité : &x rétro-utérin &x fixe &x aisée. | |
| &x Présence de micro-calcifications sous thécales &x bilatérales &x droites &x gauches pouvant témoigner d'implants endométriosiques superficiels. | |
| L'échostructure des deux ovaires apparait normale, avec une vascularisation artério-veineuse normale au Doppler, sans formation ou image kystique pathologique échographiquement décelable à leur niveau. | |
| Cavité péritonéale | |
| &x- Pas d'épanchement liquidien dans le cul du sac du Douglas. Pas de douleur à l'écho-palpation. | |
| &x- Faible épanchement corpusculé dans le cul du sac du Douglas qui silhouette des adhérences (soft marqueur d'endométriose?). Pas de douleur à l'écho-palpation. | |
| - &xVessie vide pendant l'examen. &x Vessie en semi-réplétion pendant l'examen. | |
| - &x Absence de dilatation pyélo-calicielle. | |
| - Artère utérine : IP : &x - IR : 0,&x - Spectre : type 2 avec notch protodiastolique. | |
| - Pas d'image d'hydrosalpinx visible à ce jour. | |
| RECHERCHE ENDOMETRIOSE PELVIENNE | |
| A-Compartiment antérieur (vessie en semi-réplétion) | |
| - Signe du glissement (sliding) : &xprésent &xdiminué &xabsent | |
| - Présence d'un nodule : &xnon &xoui | |
| - Uretères dans la partie pelvienne vus non dilatés. | |
| B-Compartiment postérieur | |
| - Signe du glissement (sliding) : | |
| - Espace recto-vaginal : &xprésent &xdiminué &xabsent | |
| - Plan sus-péritonéal : &xprésent &xdiminué &xabsent | |
| - Aspect du torus : &x normal &x épaissi | |
| - Aspect des ligaments utéro-sacrés : | |
| - Ligament utéro- sacré droit : &x normal &x épaissi | |
| - Ligament utéro-sacré gauche : &x normal &x épaissi | |
| - Présence d'un nodule hypoéchogène : &x non | |
| - Infiltration digestive: &x non &x oui : &x bas rectum &x moyen rectum &x haut rectum &x jonction recto-sigmoïde | |
| Conclusions | |
| Utérus de taille et de morphologie normales. | |
| Endomètre mesuré à &x mm. | |
| CFA : &x+&x follicules. | |
| Ovaires sans formation ou image kystique pathologique échographiquement décelable à leur niveau. | |
| &x Absence d'image d'endométriose visible ce jour, à confronter éventuellement à une IRM. | |
| &x Endométriose &x superficielle &x et profonde. | |
| Absence d'anomalie échographiquement décelable au niveau des trompes. | |
| --> L'ensemble de ces aspects reste à confronter au contexte clinico-thérapeutique. | |
| """ | |
| def _define_field_mappings(self) -> Dict[str, TemplateField]: | |
| """Définit les mappings entre données extraites et placeholders template""" | |
| return { | |
| # Position utérus - checkboxes | |
| "uterus_position_antéversé": TemplateField( | |
| placeholder="&x antéversé", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "antéversé" in x.lower() else "" | |
| ), | |
| "uterus_position_rétroversé": TemplateField( | |
| placeholder="&x rétroversé", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "rétroversé" in x.lower() else "" | |
| ), | |
| "uterus_position_intermédiaire": TemplateField( | |
| placeholder="&x intermédiaire", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "intermédiaire" in x.lower() else "" | |
| ), | |
| "uterus_position_rétrofléchi": TemplateField( | |
| placeholder="&x rétrofléchi", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "rétrofléchi" in x.lower() else "" | |
| ), | |
| "uterus_position_antéfléchi": TemplateField( | |
| placeholder="&x antéfléchi", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "antéfléchi" in x.lower() else "" | |
| ), | |
| "uterus_position_fixe": TemplateField( | |
| placeholder="&x fixe", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="uterus_position", | |
| transformation_func=lambda x: "X" if x and "fixe" in x.lower() else "" | |
| ), | |
| # Taille utérus - dimensions (corrected) | |
| "uterus_size_length": TemplateField( | |
| placeholder="normale (&x x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="uterus_size", | |
| transformation_func=self._extract_first_dimension | |
| ), | |
| "uterus_size_width": TemplateField( | |
| placeholder="x &x x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="uterus_size", | |
| transformation_func=self._extract_second_dimension | |
| ), | |
| "uterus_size_height": TemplateField( | |
| placeholder="x &x cm)", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="uterus_size", | |
| transformation_func=self._extract_third_dimension | |
| ), | |
| # Hystérométrie | |
| "hysterometry_value": TemplateField( | |
| placeholder="fond de la cavité utérine : &x mm", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="hysterometry", | |
| transformation_func=self._clean_numeric_value | |
| ), | |
| # Endomètre | |
| "endometrium_thickness": TemplateField( | |
| placeholder="L'endomètre : mesuré à &x mm", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="endometrium_thickness", | |
| transformation_func=self._clean_numeric_value | |
| ), | |
| # Zone jonctionnelle | |
| "junctional_zone_non": TemplateField( | |
| placeholder="Atteinte de la zone de jonction : &x non", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="junctional_zone_status", | |
| transformation_func=lambda x: "X" if not x or x.lower() in ["normale", "normal"] else "" | |
| ), | |
| "junctional_zone_oui": TemplateField( | |
| placeholder="&x oui", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="junctional_zone_status", | |
| transformation_func=lambda x: "X" if x and x.lower() in ["épaissie", "épaisse", "atteinte"] else "" | |
| ), | |
| # Adénomyose - checkboxes | |
| "adenomyosis_non": TemplateField( | |
| placeholder="Adénomyose associée : &x non", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="adenomyosis_type", | |
| transformation_func=lambda x: "X" if not x or x.lower() in ["absente", "non"] else "" | |
| ), | |
| "adenomyosis_oui": TemplateField( | |
| placeholder="&x oui :", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="adenomyosis_type", | |
| transformation_func=lambda x: "X" if x and x.lower() in ["diffuse", "focale"] else "" | |
| ), | |
| "adenomyosis_diffuse": TemplateField( | |
| placeholder="&x diffuse", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="adenomyosis_type", | |
| transformation_func=lambda x: "X" if x and "diffuse" in x.lower() else "" | |
| ), | |
| "adenomyosis_focale": TemplateField( | |
| placeholder="&x focale", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="adenomyosis_type", | |
| transformation_func=lambda x: "X" if x and "focale" in x.lower() else "" | |
| ), | |
| # Ovaire droit - dimensions (corrected with context) | |
| "right_ovary_length": TemplateField( | |
| placeholder="L'ovaire droit mesure &x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="right_ovary_dimensions", | |
| context_identifier="ovaire droit", | |
| transformation_func=self._extract_first_dimension | |
| ), | |
| "right_ovary_width_first": TemplateField( | |
| placeholder="x &x mm,", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="right_ovary_dimensions", | |
| context_identifier="ovaire droit mesure", | |
| transformation_func=self._extract_second_dimension | |
| ), | |
| # Ovaire droit - CFA | |
| "right_ovary_cfa": TemplateField( | |
| placeholder="folliculaire CFA &x follicules:", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="right_ovary_cfa", | |
| context_identifier="ovaire droit", | |
| transformation_func=self._clean_cfa_value | |
| ), | |
| # Ovaire droit - accessibilité | |
| "right_ovary_access_retro": TemplateField( | |
| placeholder="Accessibilité : &x rétro-utérin", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="right_ovary_accessibility", | |
| context_identifier="ovaire droit", | |
| transformation_func=lambda x: "X" if x and "rétro" in x.lower() else "" | |
| ), | |
| "right_ovary_access_fixe": TemplateField( | |
| placeholder="rétro-utérin &x fixe", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="right_ovary_accessibility", | |
| context_identifier="ovaire droit", | |
| transformation_func=lambda x: "X" if x and "fixe" in x.lower() else "" | |
| ), | |
| "right_ovary_access_aisee": TemplateField( | |
| placeholder="fixe &x aisée", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="right_ovary_accessibility", | |
| context_identifier="ovaire droit", | |
| transformation_func=lambda x: "X" if x and ("aisée" in x.lower() or "normale" in x.lower()) else "" | |
| ), | |
| # Ovaire gauche - dimensions (corrected with context) | |
| "left_ovary_length": TemplateField( | |
| placeholder="L'ovaire gauche mesure &x x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="left_ovary_dimensions", | |
| context_identifier="ovaire gauche", | |
| transformation_func=self._extract_first_dimension | |
| ), | |
| "left_ovary_width_first": TemplateField( | |
| placeholder="&x mm,", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="left_ovary_dimensions", | |
| context_identifier="ovaire gauche mesure", | |
| transformation_func=self._extract_second_dimension | |
| ), | |
| # Ovaire gauche - CFA | |
| "left_ovary_cfa": TemplateField( | |
| placeholder="folliculaire CFA &x follicules:", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="left_ovary_cfa", | |
| context_identifier="ovaire gauche", | |
| transformation_func=self._clean_cfa_value | |
| ), | |
| # Ovaire gauche - accessibilité | |
| "left_ovary_access_retro": TemplateField( | |
| placeholder="Accessibilité : &x rétro-utérin", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="left_ovary_accessibility", | |
| context_identifier="ovaire gauche", | |
| transformation_func=lambda x: "X" if x and "rétro" in x.lower() else "" | |
| ), | |
| "left_ovary_access_fixe": TemplateField( | |
| placeholder="rétro-utérin &x fixe", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="left_ovary_accessibility", | |
| context_identifier="ovaire gauche", | |
| transformation_func=lambda x: "X" if x and "fixe" in x.lower() else "" | |
| ), | |
| "left_ovary_access_aisee": TemplateField( | |
| placeholder="fixe &x aisée", | |
| field_type=FieldType.CHECKBOX, | |
| source_field="left_ovary_accessibility", | |
| context_identifier="ovaire gauche", | |
| transformation_func=lambda x: "X" if x and ("aisée" in x.lower() or "normale" in x.lower()) else "" | |
| ), | |
| # Doppler | |
| "doppler_ip": TemplateField( | |
| placeholder="IP : &x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="doppler_ip", | |
| transformation_func=self._clean_numeric_value | |
| ), | |
| "doppler_ir": TemplateField( | |
| placeholder="IR : 0,&x", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="doppler_ir", | |
| transformation_func=self._format_doppler_ir | |
| ), | |
| # Conclusions - CFA total | |
| "conclusion_cfa_right": TemplateField( | |
| placeholder="CFA : &x+", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="right_ovary_cfa", | |
| transformation_func=self._clean_cfa_value | |
| ), | |
| "conclusion_cfa_left": TemplateField( | |
| placeholder="+&x follicules", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="left_ovary_cfa", | |
| transformation_func=self._clean_cfa_value | |
| ), | |
| # Conclusion - endomètre | |
| "conclusion_endometrium": TemplateField( | |
| placeholder="Endomètre mesuré à &x mm", | |
| field_type=FieldType.MEASUREMENT, | |
| source_field="endometrium_thickness", | |
| transformation_func=self._clean_numeric_value | |
| ), | |
| } | |
| def _define_checkbox_logic(self) -> Dict[str, List[str]]: | |
| """Définit la logique des checkboxes mutuellement exclusives""" | |
| return { | |
| "uterus_position": ["antéversé", "rétroversé", "intermédiaire", "rétrofléchi", "antéfléchi"], | |
| "adenomyosis": ["non", "oui"], | |
| "adenomyosis_type": ["diffuse", "focale", "interne", "externe"], | |
| "ovary_accessibility": ["rétro-utérin", "fixe", "aisée"] | |
| } | |
| def map_extracted_data_to_template(self, extracted_data) -> MappingResult: | |
| """ | |
| Fonction principale de mapping des données extraites vers le template | |
| """ | |
| logger.info("🔄 Début du mapping vers le template médical") | |
| filled_template = self.template | |
| mapped_fields = {} | |
| unmapped_placeholders = [] | |
| errors = [] | |
| # Étape 1: Identifier tous les placeholders &x dans le template | |
| all_placeholders = self._find_all_placeholders(filled_template) | |
| logger.info(f"📍 {len(all_placeholders)} placeholders trouvés dans le template") | |
| # Étape 2: Appliquer les mappings définis avec gestion du contexte | |
| for mapping_key, template_field in self.field_mappings.items(): | |
| try: | |
| # Récupérer la valeur source | |
| source_value = getattr(extracted_data, template_field.source_field, None) | |
| if source_value: | |
| # Appliquer la transformation | |
| if template_field.transformation_func: | |
| mapped_value = template_field.transformation_func(source_value) | |
| else: | |
| mapped_value = str(source_value) | |
| # Remplacer dans le template avec gestion du contexte | |
| if mapped_value and mapped_value.strip(): | |
| filled_template = self._replace_placeholder_with_context( | |
| filled_template, template_field.placeholder, mapped_value, template_field.context_identifier | |
| ) | |
| mapped_fields[mapping_key] = mapped_value | |
| logger.debug(f"✅ {mapping_key}: {mapped_value}") | |
| else: | |
| logger.debug(f"⚠️ {mapping_key}: Valeur vide après transformation") | |
| except Exception as e: | |
| error_msg = f"Erreur mapping {mapping_key}: {e}" | |
| errors.append(error_msg) | |
| logger.error(error_msg) | |
| # Étape 3: Gestion des placeholders non mappés | |
| remaining_placeholders = self._find_all_placeholders(filled_template) | |
| unmapped_placeholders = [p for p in remaining_placeholders if "&x" in p] | |
| # Étape 4: Application des règles de logique métier | |
| filled_template = self._apply_business_logic(filled_template, extracted_data) | |
| # Étape 5: Calcul du score de mapping | |
| mapping_confidence = self._calculate_mapping_confidence( | |
| len(mapped_fields), len(all_placeholders), len(errors) | |
| ) | |
| logger.info(f"✅ Mapping terminé - {len(mapped_fields)} champs mappés, {len(unmapped_placeholders)} non mappés") | |
| return MappingResult( | |
| filled_template=filled_template, | |
| mapped_fields=mapped_fields, | |
| unmapped_placeholders=unmapped_placeholders, | |
| mapping_confidence=mapping_confidence, | |
| errors=errors | |
| ) | |
| def _find_all_placeholders(self, template: str) -> List[str]: | |
| """Trouve tous les placeholders &x dans le template""" | |
| # Pattern pour capturer le contexte autour de &x | |
| pattern = r'[^.]*&x[^.]*' | |
| matches = re.findall(pattern, template) | |
| return matches | |
| def _replace_placeholder_with_context(self, template: str, context_pattern: str, value: str, context_identifier: str = None) -> str: | |
| """Remplace &x dans un contexte spécifique avec gestion du contexte gauche/droit""" | |
| if context_identifier: | |
| # Trouver la section correspondante (ovaire droit/gauche) | |
| lines = template.split('\n') | |
| in_context = False | |
| context_found = False | |
| for i, line in enumerate(lines): | |
| if context_identifier.lower() in line.lower(): | |
| in_context = True | |
| context_found = True | |
| elif context_found and (("ovaire" in line.lower() and context_identifier not in line.lower()) or | |
| line.strip() == "" or | |
| "Accessibilité" in line and i > 0 and context_identifier not in lines[i-1].lower()): | |
| in_context = False | |
| if in_context and context_pattern in line: | |
| # Échapper les caractères spéciaux pour regex | |
| escaped_pattern = re.escape(context_pattern).replace(r'\&x', r'&x') | |
| lines[i] = re.sub(escaped_pattern, context_pattern.replace('&x', value), line, count=1) | |
| break | |
| return '\n'.join(lines) | |
| else: | |
| return self._replace_placeholder_in_context(template, context_pattern, value) | |
| def _replace_placeholder_in_context(self, template: str, context_pattern: str, value: str) -> str: | |
| """Remplace &x dans un contexte spécifique pour éviter les remplacements incorrects""" | |
| # Échapper les caractères spéciaux pour regex | |
| escaped_pattern = re.escape(context_pattern).replace(r'\&x', r'&x') | |
| # Remplacer &x uniquement dans ce contexte | |
| def replace_func(match): | |
| return match.group(0).replace('&x', value, 1) # Remplacer seulement le premier &x | |
| return re.sub(escaped_pattern, replace_func, template) | |
| def _apply_business_logic(self, template: str, extracted_data) -> str: | |
| """Applique la logique métier spécifique au domaine médical""" | |
| # Logique 1: Si pas d'adénomyose détectée, cocher "non" | |
| if not extracted_data.adenomyosis_type or extracted_data.adenomyosis_type.lower() == "absente": | |
| template = template.replace("Adénomyose associée : &x non", "Adénomyose associée : X non") | |
| # Logique 2: Gestion de l'accessibilité par défaut pour ovaire droit | |
| if not getattr(extracted_data, 'right_ovary_accessibility', None) or getattr(extracted_data, 'right_ovary_accessibility', '').lower() == "normale": | |
| # Chercher la section ovaire droit et marquer aisée | |
| lines = template.split('\n') | |
| for i, line in enumerate(lines): | |
| if "ovaire droit" in line.lower() and i < len(lines) - 1: | |
| # Chercher la ligne accessibilité suivante | |
| for j in range(i+1, min(i+5, len(lines))): | |
| if "Accessibilité" in lines[j] and "ovaire droit" in lines[i].lower(): | |
| lines[j] = lines[j].replace("&x aisée", "X aisée") | |
| break | |
| break | |
| template = '\n'.join(lines) | |
| # Logique 3: Gestion de l'accessibilité pour ovaire gauche | |
| if getattr(extracted_data, 'left_ovary_accessibility', None) and "rétro" in getattr(extracted_data, 'left_ovary_accessibility', '').lower(): | |
| lines = template.split('\n') | |
| for i, line in enumerate(lines): | |
| if "ovaire gauche" in line.lower() and i < len(lines) - 1: | |
| # Chercher la ligne accessibilité suivante | |
| for j in range(i+1, min(i+5, len(lines))): | |
| if "Accessibilité" in lines[j] and "gauche" in lines[i].lower(): | |
| lines[j] = lines[j].replace("Accessibilité : &x rétro-utérin", "Accessibilité : X rétro-utérin") | |
| break | |
| break | |
| template = '\n'.join(lines) | |
| # Logique 4: Valeurs par défaut pour les examens standard | |
| template = template.replace("- &xVessie vide pendant l'examen", "- XVessie vide pendant l'examen") | |
| template = template.replace("&x Absence de dilatation pyélo-calicielle", "X Absence de dilatation pyélo-calicielle") | |
| # Logique 5: Conclusions par défaut | |
| template = template.replace("&x Absence d'image d'endométriose visible ce jour", "X Absence d'image d'endométriose visible ce jour") | |
| return template | |
| def _calculate_mapping_confidence(self, mapped_count: int, total_placeholders: int, error_count: int) -> float: | |
| """Calcule le score de confiance du mapping""" | |
| if total_placeholders == 0: | |
| return 1.0 | |
| base_confidence = mapped_count / total_placeholders | |
| error_penalty = min(error_count * 0.1, 0.3) # Maximum 30% de pénalité | |
| return max(0.0, base_confidence - error_penalty) | |
| # Fonctions de transformation des données | |
| def _clean_numeric_value(self, value: str) -> str: | |
| """Nettoie les valeurs numériques""" | |
| if not value: | |
| return "" | |
| # Supprimer les unités redondantes comme "mm mm" | |
| cleaned = re.sub(r'\s*(mm|cm)\s*(mm|cm)', r' \1', str(value)) | |
| cleaned = re.sub(r'\s*(mm|cm).*', r'', cleaned) # Supprimer unités en fin | |
| cleaned = cleaned.replace(',', '.').strip() | |
| return cleaned | |
| def _clean_cfa_value(self, value: str) -> str: | |
| """Nettoie les valeurs CFA en supprimant les doublons""" | |
| if not value: | |
| return "" | |
| cleaned = str(value).replace(' follicules', '').replace(' follicules follicules', '').strip() | |
| # Extraire seulement le nombre | |
| match = re.search(r'(\d+)', cleaned) | |
| return match.group(1) if match else cleaned | |
| def _extract_first_dimension(self, dimensions: str) -> str: | |
| """Extrait la première dimension (longueur)""" | |
| if not dimensions: | |
| return "" | |
| match = re.search(r'(\d+(?:[.,]\d+)?)', dimensions) | |
| return match.group(1).replace(',', '.') if match else "" | |
| def _extract_second_dimension(self, dimensions: str) -> str: | |
| """Extrait la deuxième dimension (largeur)""" | |
| if not dimensions: | |
| return "" | |
| matches = re.findall(r'(\d+(?:[.,]\d+)?)', dimensions) | |
| return matches[1].replace(',', '.') if len(matches) > 1 else "" | |
| def _extract_third_dimension(self, dimensions: str) -> str: | |
| """Extrait la troisième dimension (hauteur)""" | |
| if not dimensions: | |
| return "" | |
| matches = re.findall(r'(\d+(?:[.,]\d+)?)', dimensions) | |
| return matches[2].replace(',', '.') if len(matches) > 2 else "" | |
| def _format_doppler_ir(self, ir_value: str) -> str: | |
| """Formate la valeur IR pour le template (0,XX)""" | |
| if not ir_value: | |
| return "" | |
| cleaned = self._clean_numeric_value(ir_value) | |
| # Si la valeur commence par 0. enlever le 0. | |
| if cleaned.startswith('0.'): | |
| return cleaned[2:] | |
| elif '.' in cleaned: | |
| return cleaned.split('.')[1] | |
| return cleaned | |
| def print_mapping_report(self, result: MappingResult) -> str: | |
| """Génère un rapport de mapping formaté""" | |
| report = "🔄 RAPPORT DE MAPPING TEMPLATE\n" | |
| report += "=" * 50 + "\n\n" | |
| # Statistiques générales | |
| report += f"📊 STATISTIQUES:\n" | |
| report += f" Champs mappés: {len(result.mapped_fields)}\n" | |
| report += f" Placeholders non mappés: {len(result.unmapped_placeholders)}\n" | |
| report += f" Score de confiance: {result.mapping_confidence:.1%}\n" | |
| report += f" Erreurs: {len(result.errors)}\n\n" | |
| # Détail des mappings | |
| if result.mapped_fields: | |
| report += "✅ CHAMPS MAPPÉS:\n" | |
| for field, value in result.mapped_fields.items(): | |
| report += f" {field}: {value}\n" | |
| report += "\n" | |
| # Placeholders non mappés | |
| if result.unmapped_placeholders: | |
| report += "❌ PLACEHOLDERS NON MAPPÉS:\n" | |
| for placeholder in result.unmapped_placeholders[:10]: # Limiter l'affichage | |
| report += f" {placeholder[:50]}...\n" | |
| if len(result.unmapped_placeholders) > 10: | |
| report += f" ... et {len(result.unmapped_placeholders) - 10} autres\n" | |
| report += "\n" | |
| # Erreurs | |
| if result.errors: | |
| report += "⚠️ ERREURS:\n" | |
| for error in result.errors: | |
| report += f" {error}\n" | |
| return report | |
| # Fonction utilitaire pour utilisation | |
| def create_filled_medical_report(extracted_data) -> str: | |
| """ | |
| Fonction principale pour créer un rapport médical complet | |
| à partir des données extraites | |
| """ | |
| mapper = MedicalTemplateMapper() | |
| result = mapper.map_extracted_data_to_template(extracted_data) | |
| # Log du rapport | |
| print(mapper.print_mapping_report(result)) | |
| return result.filled_template | |
| # Exemple d'utilisation avec correction des problèmes identifiés | |
| class ExtractedData: | |
| """Classe exemple pour les données extraites""" | |
| def __init__(self): | |
| # Données exemple basées sur votre extraction | |
| self.uterus_position = "antéversé" | |
| self.uterus_size = "7,8 cm" | |
| self.hysterometry = "60 mm" | |
| self.endometrium_thickness = "3,7 mm" | |
| self.junctional_zone_status = "épaissie" | |
| self.adenomyosis_type = "diffuse" | |
| # Données ovaires corrigées | |
| self.right_ovary_dimensions = "26 x 20 mm" | |
| self.right_ovary_cfa = "22 follicules" | |
| self.right_ovary_accessibility = "normale" | |
| self.left_ovary_dimensions = "25 x 19 mm" # Correction: 19 au lieu de 20 | |
| self.left_ovary_cfa = "22 follicules" | |
| self.left_ovary_accessibility = "rétro-utérine" | |
| # Données Doppler | |
| self.doppler_ip = "3,24" | |
| self.doppler_ir = "0,91" | |
| def test_corrected_mapping(): | |
| """Test de la correction du mapping""" | |
| # Créer des données test | |
| data = ExtractedData() | |
| # Utiliser le mapper corrigé | |
| mapper = MedicalTemplateMapper() | |
| result = mapper.map_extracted_data_to_template(data) | |
| print("🔧 TEST DU MAPPING CORRIGÉ") | |
| print("=" * 40) | |
| print(mapper.print_mapping_report(result)) | |
| # Vérifications spécifiques pour les ovaires | |
| print("\n🔍 VÉRIFICATIONS SPÉCIFIQUES:") | |
| print("-" * 30) | |
| # Vérifier ovaire droit | |
| if "L'ovaire droit mesure 26 x 20 mm" in result.filled_template: | |
| print("✅ Ovaire droit: dimensions correctes") | |
| else: | |
| print("❌ Ovaire droit: problème dimensions") | |
| # Vérifier ovaire gauche | |
| if "L'ovaire gauche mesure 25 x 19 mm" in result.filled_template: | |
| print("✅ Ovaire gauche: dimensions correctes") | |
| else: | |
| print("❌ Ovaire gauche: problème dimensions") | |
| # Vérifier CFA dans conclusions | |
| if "CFA : 22+22 follicules" in result.filled_template: | |
| print("✅ CFA conclusion: format correct") | |
| else: | |
| print("❌ CFA conclusion: problème format") | |
| # Vérifier accessibilité | |
| if "Accessibilité : X rétro-utérin" in result.filled_template and "ovaire gauche" in result.filled_template: | |
| print("✅ Accessibilité gauche: rétro-utérine correcte") | |
| else: | |
| print("❌ Accessibilité gauche: problème") | |
| return result.filled_template | |
| # Exécuter le test si le script est lancé directement | |
| if __name__ == "__main__": | |
| filled_report = test_corrected_mapping() | |
| print("\n" + "="*50) | |
| print("RAPPORT FINAL CORRIGÉ:") | |
| print("="*50) | |
| print(filled_report) |