|
|
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" |
|
|
TEXT = "text" |
|
|
MEASUREMENT = "measurement" |
|
|
|
|
|
@dataclass |
|
|
class TemplateField: |
|
|
"""Définition d'un champ du template""" |
|
|
placeholder: str |
|
|
field_type: FieldType |
|
|
source_field: str |
|
|
default_value: str = "" |
|
|
validation_pattern: Optional[str] = None |
|
|
transformation_func: Optional[callable] = None |
|
|
context_identifier: Optional[str] = None |
|
|
|
|
|
@dataclass |
|
|
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 { |
|
|
|
|
|
"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 "" |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"endometrium_thickness": TemplateField( |
|
|
placeholder="L'endomètre : mesuré à &x mm", |
|
|
field_type=FieldType.MEASUREMENT, |
|
|
source_field="endometrium_thickness", |
|
|
transformation_func=self._clean_numeric_value |
|
|
), |
|
|
|
|
|
|
|
|
"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 "" |
|
|
), |
|
|
|
|
|
|
|
|
"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 "" |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"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 "" |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"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 |
|
|
), |
|
|
|
|
|
|
|
|
"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_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 |
|
|
), |
|
|
|
|
|
|
|
|
"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_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 = [] |
|
|
|
|
|
|
|
|
all_placeholders = self._find_all_placeholders(filled_template) |
|
|
logger.info(f"📍 {len(all_placeholders)} placeholders trouvés dans le template") |
|
|
|
|
|
|
|
|
for mapping_key, template_field in self.field_mappings.items(): |
|
|
try: |
|
|
|
|
|
source_value = getattr(extracted_data, template_field.source_field, None) |
|
|
|
|
|
if source_value: |
|
|
|
|
|
if template_field.transformation_func: |
|
|
mapped_value = template_field.transformation_func(source_value) |
|
|
else: |
|
|
mapped_value = str(source_value) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
remaining_placeholders = self._find_all_placeholders(filled_template) |
|
|
unmapped_placeholders = [p for p in remaining_placeholders if "&x" in p] |
|
|
|
|
|
|
|
|
filled_template = self._apply_business_logic(filled_template, extracted_data) |
|
|
|
|
|
|
|
|
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 = 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: |
|
|
|
|
|
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: |
|
|
|
|
|
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""" |
|
|
|
|
|
escaped_pattern = re.escape(context_pattern).replace(r'\&x', r'&x') |
|
|
|
|
|
|
|
|
def replace_func(match): |
|
|
return match.group(0).replace('&x', value, 1) |
|
|
|
|
|
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""" |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
if not getattr(extracted_data, 'right_ovary_accessibility', None) or getattr(extracted_data, 'right_ovary_accessibility', '').lower() == "normale": |
|
|
|
|
|
lines = template.split('\n') |
|
|
for i, line in enumerate(lines): |
|
|
if "ovaire droit" in line.lower() and i < len(lines) - 1: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
return max(0.0, base_confidence - error_penalty) |
|
|
|
|
|
|
|
|
|
|
|
def _clean_numeric_value(self, value: str) -> str: |
|
|
"""Nettoie les valeurs numériques""" |
|
|
if not value: |
|
|
return "" |
|
|
|
|
|
|
|
|
cleaned = re.sub(r'\s*(mm|cm)\s*(mm|cm)', r' \1', str(value)) |
|
|
cleaned = re.sub(r'\s*(mm|cm).*', r'', cleaned) |
|
|
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() |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
if result.mapped_fields: |
|
|
report += "✅ CHAMPS MAPPÉS:\n" |
|
|
for field, value in result.mapped_fields.items(): |
|
|
report += f" {field}: {value}\n" |
|
|
report += "\n" |
|
|
|
|
|
|
|
|
if result.unmapped_placeholders: |
|
|
report += "❌ PLACEHOLDERS NON MAPPÉS:\n" |
|
|
for placeholder in result.unmapped_placeholders[:10]: |
|
|
report += f" {placeholder[:50]}...\n" |
|
|
if len(result.unmapped_placeholders) > 10: |
|
|
report += f" ... et {len(result.unmapped_placeholders) - 10} autres\n" |
|
|
report += "\n" |
|
|
|
|
|
|
|
|
if result.errors: |
|
|
report += "⚠️ ERREURS:\n" |
|
|
for error in result.errors: |
|
|
report += f" {error}\n" |
|
|
|
|
|
return report |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
print(mapper.print_mapping_report(result)) |
|
|
|
|
|
return result.filled_template |
|
|
|
|
|
|
|
|
|
|
|
class ExtractedData: |
|
|
"""Classe exemple pour les données extraites""" |
|
|
def __init__(self): |
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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" |
|
|
self.left_ovary_cfa = "22 follicules" |
|
|
self.left_ovary_accessibility = "rétro-utérine" |
|
|
|
|
|
|
|
|
self.doppler_ip = "3,24" |
|
|
self.doppler_ir = "0,91" |
|
|
|
|
|
|
|
|
def test_corrected_mapping(): |
|
|
"""Test de la correction du mapping""" |
|
|
|
|
|
|
|
|
data = ExtractedData() |
|
|
|
|
|
|
|
|
mapper = MedicalTemplateMapper() |
|
|
result = mapper.map_extracted_data_to_template(data) |
|
|
|
|
|
print("🔧 TEST DU MAPPING CORRIGÉ") |
|
|
print("=" * 40) |
|
|
print(mapper.print_mapping_report(result)) |
|
|
|
|
|
|
|
|
print("\n🔍 VÉRIFICATIONS SPÉCIFIQUES:") |
|
|
print("-" * 30) |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
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") |
|
|
|
|
|
|
|
|
if "CFA : 22+22 follicules" in result.filled_template: |
|
|
print("✅ CFA conclusion: format correct") |
|
|
else: |
|
|
print("❌ CFA conclusion: problème format") |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
filled_report = test_corrected_mapping() |
|
|
print("\n" + "="*50) |
|
|
print("RAPPORT FINAL CORRIGÉ:") |
|
|
print("="*50) |
|
|
print(filled_report) |