Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .DS_Store +0 -0
- .gitattributes +7 -0
- .github/workflows/update_space.yml +28 -0
- .gitignore +14 -0
- .gradio/certificate.pem +31 -0
- .gradio/flagged/dataset1.csv +65 -0
- .python-version +1 -0
- Create_db_optimized.py +657 -0
- README.md +3 -9
- annotation.py +486 -0
- app.py +53 -0
- app2.py +116 -0
- correcteur.py +751 -0
- data_txt/template1.txt +48 -0
- data_txt/template10.txt +48 -0
- data_txt/template100.txt +28 -0
- data_txt/template1000.txt +3 -0
- data_txt/template1001.txt +9 -0
- data_txt/template1002.txt +1 -0
- data_txt/template1003.txt +9 -0
- data_txt/template1004.txt +1 -0
- data_txt/template1005.txt +9 -0
- data_txt/template1006.txt +1 -0
- data_txt/template1007.txt +10 -0
- data_txt/template1008.txt +6 -0
- data_txt/template1009.txt +10 -0
- data_txt/template101.txt +34 -0
- data_txt/template1010.txt +3 -0
- data_txt/template1011.txt +8 -0
- data_txt/template1012.txt +3 -0
- data_txt/template1013.txt +29 -0
- data_txt/template1014.txt +21 -0
- data_txt/template1015.txt +8 -0
- data_txt/template1016.txt +2 -0
- data_txt/template1017.txt +8 -0
- data_txt/template1018.txt +4 -0
- data_txt/template1019.txt +7 -0
- data_txt/template102.txt +29 -0
- data_txt/template1020.txt +2 -0
- data_txt/template1021.txt +9 -0
- data_txt/template1022.txt +5 -0
- data_txt/template1023.txt +8 -0
- data_txt/template1024.txt +3 -0
- data_txt/template1025.txt +8 -0
- data_txt/template1026.txt +1 -0
- data_txt/template1027.txt +7 -0
- data_txt/template1028.txt +3 -0
- data_txt/template1029.txt +7 -0
- data_txt/template103.txt +32 -0
- data_txt/template1030.txt +3 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
.gitattributes
CHANGED
|
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
my_env/lib/python3.11/site-packages/pip/_vendor/distlib/t64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
my_env/lib/python3.11/site-packages/pip/_vendor/distlib/t64.exe filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
my_env/lib/python3.11/site-packages/pip/_vendor/distlib/w64-arm.exe filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
my_env/lib/python3.11/site-packages/pip/_vendor/distlib/w64.exe filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
my_env/lib/python3.11/site-packages/setuptools/cli-arm64.exe filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
my_env/lib/python3.11/site-packages/setuptools/gui-arm64.exe filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
templates/medical_templates.faiss filter=lfs diff=lfs merge=lfs -text
|
.github/workflows/update_space.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Run Python script
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
build:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout
|
| 14 |
+
uses: actions/checkout@v2
|
| 15 |
+
|
| 16 |
+
- name: Set up Python
|
| 17 |
+
uses: actions/setup-python@v2
|
| 18 |
+
with:
|
| 19 |
+
python-version: '3.9'
|
| 20 |
+
|
| 21 |
+
- name: Install Gradio
|
| 22 |
+
run: python -m pip install gradio
|
| 23 |
+
|
| 24 |
+
- name: Log in to Hugging Face
|
| 25 |
+
run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")'
|
| 26 |
+
|
| 27 |
+
- name: Deploy to Spaces
|
| 28 |
+
run: gradio deploy
|
.gitignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
medical_env/
|
| 2 |
+
models/
|
| 3 |
+
transcriptions/
|
| 4 |
+
*.docx
|
| 5 |
+
*.json
|
| 6 |
+
*.rtf
|
| 7 |
+
*.pdf
|
| 8 |
+
*.doc
|
| 9 |
+
*.docx
|
| 10 |
+
*.rtf
|
| 11 |
+
*.log
|
| 12 |
+
*.pyc
|
| 13 |
+
*.pyo
|
| 14 |
+
.env
|
.gradio/certificate.pem
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-----BEGIN CERTIFICATE-----
|
| 2 |
+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
|
| 3 |
+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
|
| 4 |
+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
|
| 5 |
+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
|
| 6 |
+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
|
| 7 |
+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
|
| 8 |
+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
|
| 9 |
+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
|
| 10 |
+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
|
| 11 |
+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
|
| 12 |
+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
|
| 13 |
+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
|
| 14 |
+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
|
| 15 |
+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
|
| 16 |
+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
|
| 17 |
+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
|
| 18 |
+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
|
| 19 |
+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
|
| 20 |
+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
|
| 21 |
+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
|
| 22 |
+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
|
| 23 |
+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
|
| 24 |
+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
|
| 25 |
+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
|
| 26 |
+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
|
| 27 |
+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
|
| 28 |
+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
|
| 29 |
+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
|
| 30 |
+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
|
| 31 |
+
-----END CERTIFICATE-----
|
.gradio/flagged/dataset1.csv
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
📝 Transcription médicale,✅ Transcription corrigée,📋 Rapport à remplir (Template),📄 Compte-rendu structuré final,timestamp
|
| 2 |
+
scanner thoraco abdomino pelvien et transit œsogastro décelable indication il est délimité un épaississement œsophagien point acquisition sans injection administration par voie orale de produit de contraste point à la ligne pas de foyer parenchymateux pas de nodule ni de masse à la ligne pas d épanchement pleural ou péricardique la pas d adénomégalie médiastinale ou axillaire il ne semble pas exister d épaississement œsophagien point a l étage abdomino pelvien hépatomégalie homogène point pas d anomalie décelée sur le pancréas les reins rate ou les surrénales point l estomac est de volume normal point pas d anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées point pas de lésion osseuse point à la ligne transit œso gastro duodénal opacification rapide et douce de l œsophage avec des parois normales sans zone d addition ou de sténose point pas d anomalie de la cavité gastrique point à la ligne opacification rapide du cadre duodénal point à la ligne pas d anomalie des anses jéjunales opacifiées point pas de pneumopéritoine point à la ligne conclusion à la ligne scanner thoraco abdomino pelvien transito gastro duodénal sans lésion décelée à a confronter au reste du bilan et une endoscopie si nécessaire,"Scanner thoraco-abdomino-pelvien et transit œso-gastro décelable. Indication. Il est délimité un épaississement œsophagien. Acquisition sans injection. Administration par voie orale de produit de contraste.
|
| 3 |
+
Pas de foyer parenchymateux. Pas de nodule ni de masse.
|
| 4 |
+
Pas d'épanchement pleural ou péricardique. Pas d'adénomégalie médiastinale ou axillaire. Il ne semble pas exister d'épaississement œsophagien. À l'étage abdomino-pelvien, hépatomégalie homogène. Pas d'anomalie décelée sur le pancréas, les reins, la rate ou les surrénales. L'estomac est de volume normal. Pas d'anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées. Pas de lésion osseuse.
|
| 5 |
+
Transit œso-gastro-duodénal. Opacification rapide et douce de l'œsophage avec des parois normales sans zone d'addition ou de sténose. Pas d'anomalie de la cavité gastrique.
|
| 6 |
+
Opacification rapide du cadre duodénal.
|
| 7 |
+
Pas d'anomalie des anses jéjunales opacifiées. Pas de pneumopéritoine.
|
| 8 |
+
Conclusion
|
| 9 |
+
Scanner thoraco-abdomino-pelvien, transit œso-gastro-duodénal, sans lésion décelée, à confronter au reste du bilan et à une endoscopie si nécessaire.","default.99.771703477
|
| 10 |
+
====================
|
| 11 |
+
Examen tomodensitométrique $
|
| 12 |
+
Indication : <ASR_VOX>
|
| 13 |
+
Technique :
|
| 14 |
+
Examen réalisé sur un scanner REVOLUTION EVO.
|
| 15 |
+
Résultat :
|
| 16 |
+
$
|
| 17 |
+
Conclusion : $","default.99.771703477
|
| 18 |
+
====================
|
| 19 |
+
TITRE :
|
| 20 |
+
Scanner thoraco-abdomino-pelvien et transit œso-gastro-duodénal
|
| 21 |
+
|
| 22 |
+
CLINIQUE :
|
| 23 |
+
Il est délimité un épaississement œsophagien.
|
| 24 |
+
|
| 25 |
+
TECHNIQUE :
|
| 26 |
+
Examen réalisé sur un scanner REVOLUTION EVO. Acquisition sans injection. Administration par voie orale de produit de contraste.
|
| 27 |
+
|
| 28 |
+
RESULTATS :
|
| 29 |
+
Pas de foyer parenchymateux. Pas de nodule ni de masse. Pas d'épanchement pleural ou péricardique. Pas d'adénomégalie médiastinale ou axillaire. Il ne semble pas exister d'épaississement œsophagien. À l'étage abdomino-pelvien, hépatomégalie homogène. Pas d'anomalie décelée sur le pancréas, les reins, la rate ou les surrénales. L'estomac est de volume normal. Pas d'anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées. Pas de lésion osseuse. Transit œso-gastro-duodénal. Opacification rapide et douce de l'œsophage avec des parois normales sans zone d'addition ou de sténose. Pas d'anomalie de la cavité gastrique. Opacification rapide du cadre duodénal. Pas d'anomalie des anses jéjunales opacifiées. Pas de pneumopéritoine.
|
| 30 |
+
|
| 31 |
+
CONCLUSION :
|
| 32 |
+
Scanner thoraco-abdomino-pelvien, transit œso-gastro-duodénal, sans lésion décelée, à confronter au reste du bilan et à une endoscopie si nécessaire.
|
| 33 |
+
",2025-10-02 16:07:30.158731
|
| 34 |
+
scanner thoraco abdomino pelvien et transit œsogastro décelable indication il est délimité un épaississement œsophagien point acquisition sans injection administration par voie orale de produit de contraste point à la ligne pas de foyer parenchymateux pas de nodule ni de masse à la ligne pas d épanchement pleural ou péricardique la pas d adénomégalie médiastinale ou axillaire il ne semble pas exister d épaississement œsophagien point a l étage abdomino pelvien hépatomégalie homogène point pas d anomalie décelée sur le pancréas les reins rate ou les surrénales point l estomac est de volume normal point pas d anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées point pas de lésion osseuse point à la ligne transit œso gastro duodénal opacification rapide et douce de l œsophage avec des parois normales sans zone d addition ou de sténose point pas d anomalie de la cavité gastrique point à la ligne opacification rapide du cadre duodénal point à la ligne pas d anomalie des anses jéjunales opacifiées point pas de pneumopéritoine point à la ligne conclusion à la ligne scanner thoraco abdomino pelvien transito gastro duodénal sans lésion décelée à a confronter au reste du bilan et une endoscopie si nécessaire,"Scanner thoraco-abdomino-pelvien et transit œso-gastro décelable. Indication. Il est délimité un épaississement œsophagien. Acquisition sans injection. Administration par voie orale de produit de contraste.
|
| 35 |
+
Pas de foyer parenchymateux. Pas de nodule ni de masse.
|
| 36 |
+
Pas d'épanchement pleural ou péricardique. Pas d'adénomégalie médiastinale ou axillaire. Il ne semble pas exister d'épaississement œsophagien. À l'étage abdomino-pelvien, hépatomégalie homogène. Pas d'anomalie décelée sur le pancréas, les reins, la rate ou les surrénales. L'estomac est de volume normal. Pas d'anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées. Pas de lésion osseuse.
|
| 37 |
+
Transit œso-gastro-duodénal. Opacification rapide et douce de l'œsophage avec des parois normales sans zone d'addition ou de sténose. Pas d'anomalie de la cavité gastrique.
|
| 38 |
+
Opacification rapide du cadre duodénal.
|
| 39 |
+
Pas d'anomalie des anses jéjunales opacifiées. Pas de pneumopéritoine.
|
| 40 |
+
Conclusion
|
| 41 |
+
Scanner thoraco-abdomino-pelvien, transit œso-gastro-duodénal, sans lésion décelée, à confronter au reste du bilan et à une endoscopie si nécessaire.","default.99.771703477
|
| 42 |
+
====================
|
| 43 |
+
Examen tomodensitométrique $
|
| 44 |
+
Indication : <ASR_VOX>
|
| 45 |
+
Technique :
|
| 46 |
+
Examen réalisé sur un scanner REVOLUTION EVO.
|
| 47 |
+
Résultat :
|
| 48 |
+
$
|
| 49 |
+
Conclusion : $","default.99.771703477
|
| 50 |
+
====================
|
| 51 |
+
TITRE :
|
| 52 |
+
Scanner thoraco-abdomino-pelvien et transit œso-gastro-duodénal
|
| 53 |
+
|
| 54 |
+
CLINIQUE :
|
| 55 |
+
Il est délimité un épaississement œsophagien.
|
| 56 |
+
|
| 57 |
+
TECHNIQUE :
|
| 58 |
+
Examen réalisé sur un scanner REVOLUTION EVO. Acquisition sans injection. Administration par voie orale de produit de contraste.
|
| 59 |
+
|
| 60 |
+
RESULTATS :
|
| 61 |
+
Pas de foyer parenchymateux. Pas de nodule ni de masse. Pas d'épanchement pleural ou péricardique. Pas d'adénomégalie médiastinale ou axillaire. Il ne semble pas exister d'épaississement œsophagien. À l'étage abdomino-pelvien, hépatomégalie homogène. Pas d'anomalie décelée sur le pancréas, les reins, la rate ou les surrénales. L'estomac est de volume normal. Pas d'anomalie décelée sur le cadre duodénal ou les anses grêles opacifiées. Pas de lésion osseuse. Transit œso-gastro-duodénal. Opacification rapide et douce de l'œsophage avec des parois normales sans zone d'addition ou de sténose. Pas d'anomalie de la cavité gastrique. Opacification rapide du cadre duodénal. Pas d'anomalie des anses jéjunales opacifiées. Pas de pneumopéritoine.
|
| 62 |
+
|
| 63 |
+
CONCLUSION :
|
| 64 |
+
Scanner thoraco-abdomino-pelvien, transit œso-gastro-duodénal, sans lésion décelée, à confronter au reste du bilan et à une endoscopie si nécessaire.
|
| 65 |
+
",2025-10-02 16:07:33.131071
|
.python-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11.0
|
Create_db_optimized.py
ADDED
|
@@ -0,0 +1,657 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
import numpy as np
|
| 5 |
+
from typing import List, Dict, Any, Optional, Tuple, Union
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
# Core libraries
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import (
|
| 12 |
+
AutoTokenizer, AutoModel, AutoModelForTokenClassification,
|
| 13 |
+
TrainingArguments, Trainer, pipeline
|
| 14 |
+
)
|
| 15 |
+
from torch.utils.data import Dataset
|
| 16 |
+
import torch.nn.functional as F
|
| 17 |
+
|
| 18 |
+
# Vector database
|
| 19 |
+
import chromadb
|
| 20 |
+
from chromadb.config import Settings
|
| 21 |
+
|
| 22 |
+
# Utilities
|
| 23 |
+
import logging
|
| 24 |
+
from tqdm import tqdm
|
| 25 |
+
import pandas as pd
|
| 26 |
+
|
| 27 |
+
# Configure logging
|
| 28 |
+
logging.basicConfig(level=logging.INFO)
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
@dataclass
|
| 32 |
+
class MedicalEntity:
|
| 33 |
+
"""Structure pour les entités médicales extraites par NER"""
|
| 34 |
+
exam_types: List[Tuple[str, float]] # (entity, confidence)
|
| 35 |
+
specialties: List[Tuple[str, float]]
|
| 36 |
+
anatomical_regions: List[Tuple[str, float]]
|
| 37 |
+
pathologies: List[Tuple[str, float]]
|
| 38 |
+
medical_procedures: List[Tuple[str, float]]
|
| 39 |
+
measurements: List[Tuple[str, float]]
|
| 40 |
+
medications: List[Tuple[str, float]]
|
| 41 |
+
symptoms: List[Tuple[str, float]]
|
| 42 |
+
|
| 43 |
+
class AdvancedMedicalNER:
|
| 44 |
+
"""NER médical avancé basé sur CamemBERT-Bio fine-tuné"""
|
| 45 |
+
|
| 46 |
+
def __init__(self, model_name: str = "auto", cache_dir: str = "./models_cache"):
|
| 47 |
+
self.cache_dir = Path(cache_dir)
|
| 48 |
+
self.cache_dir.mkdir(exist_ok=True)
|
| 49 |
+
|
| 50 |
+
# Auto-détection du meilleur modèle NER médical disponible
|
| 51 |
+
self.model_name = self._select_best_model(model_name)
|
| 52 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 53 |
+
|
| 54 |
+
# Chargement du modèle NER
|
| 55 |
+
self._load_ner_model()
|
| 56 |
+
|
| 57 |
+
# Labels BIO pour entités médicales
|
| 58 |
+
self.entity_labels = [
|
| 59 |
+
"O", # Outside
|
| 60 |
+
"B-EXAM", "I-EXAM", # Types d'examens
|
| 61 |
+
"B-SPECIALTY", "I-SPECIALTY", # Spécialités médicales
|
| 62 |
+
"B-ANATOMY", "I-ANATOMY", # Régions anatomiques
|
| 63 |
+
"B-PATHOLOGY", "I-PATHOLOGY", # Pathologies
|
| 64 |
+
"B-PROCEDURE", "I-PROCEDURE", # Procédures médicales
|
| 65 |
+
"B-MEASURE", "I-MEASURE", # Mesures/valeurs
|
| 66 |
+
"B-MEDICATION", "I-MEDICATION", # Médicaments
|
| 67 |
+
"B-SYMPTOM", "I-SYMPTOM" # Symptômes
|
| 68 |
+
]
|
| 69 |
+
|
| 70 |
+
self.id2label = {i: label for i, label in enumerate(self.entity_labels)}
|
| 71 |
+
self.label2id = {label: i for i, label in enumerate(self.entity_labels)}
|
| 72 |
+
|
| 73 |
+
def _select_best_model(self, model_name: str) -> str:
|
| 74 |
+
"""Sélection automatique du meilleur modèle NER médical"""
|
| 75 |
+
|
| 76 |
+
if model_name != "auto":
|
| 77 |
+
return model_name
|
| 78 |
+
|
| 79 |
+
# Liste des modèles par ordre de préférence
|
| 80 |
+
preferred_models = [
|
| 81 |
+
"almanach/camembert-bio-base", # CamemBERT Bio français
|
| 82 |
+
"Dr-BERT/DrBERT-7GB", # DrBERT spécialisé
|
| 83 |
+
"emilyalsentzer/Bio_ClinicalBERT", # Bio Clinical BERT
|
| 84 |
+
"microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract-fulltext",
|
| 85 |
+
"dmis-lab/biobert-base-cased-v1.2", # BioBERT
|
| 86 |
+
"camembert-base" # Fallback CamemBERT standard
|
| 87 |
+
]
|
| 88 |
+
|
| 89 |
+
for model in preferred_models:
|
| 90 |
+
try:
|
| 91 |
+
# Test de disponibilité
|
| 92 |
+
AutoTokenizer.from_pretrained(model, cache_dir=self.cache_dir)
|
| 93 |
+
logger.info(f"Modèle sélectionné: {model}")
|
| 94 |
+
return model
|
| 95 |
+
except:
|
| 96 |
+
continue
|
| 97 |
+
|
| 98 |
+
# Fallback ultime
|
| 99 |
+
logger.warning("Utilisation du modèle de base camembert-base")
|
| 100 |
+
return "camembert-base"
|
| 101 |
+
|
| 102 |
+
def _load_ner_model(self):
|
| 103 |
+
"""Charge ou crée le modèle NER fine-tuné"""
|
| 104 |
+
|
| 105 |
+
fine_tuned_path = self.cache_dir / "medical_ner_model"
|
| 106 |
+
|
| 107 |
+
if fine_tuned_path.exists():
|
| 108 |
+
logger.info("Chargement du modèle NER fine-tuné existant")
|
| 109 |
+
self.tokenizer = AutoTokenizer.from_pretrained(fine_tuned_path)
|
| 110 |
+
self.ner_model = AutoModelForTokenClassification.from_pretrained(fine_tuned_path)
|
| 111 |
+
else:
|
| 112 |
+
logger.info("Création d'un nouveau modèle NER médical")
|
| 113 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.model_name, cache_dir=self.cache_dir)
|
| 114 |
+
|
| 115 |
+
# Modèle pour classification de tokens (NER)
|
| 116 |
+
self.ner_model = AutoModelForTokenClassification.from_pretrained(
|
| 117 |
+
self.model_name,
|
| 118 |
+
num_labels=len(self.entity_labels),
|
| 119 |
+
id2label=self.id2label,
|
| 120 |
+
label2id=self.label2id,
|
| 121 |
+
cache_dir=self.cache_dir
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
self.ner_model.to(self.device)
|
| 125 |
+
|
| 126 |
+
# Pipeline NER
|
| 127 |
+
self.ner_pipeline = pipeline(
|
| 128 |
+
"token-classification",
|
| 129 |
+
model=self.ner_model,
|
| 130 |
+
tokenizer=self.tokenizer,
|
| 131 |
+
device=0 if torch.cuda.is_available() else -1,
|
| 132 |
+
aggregation_strategy="simple"
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
def extract_entities(self, text: str) -> MedicalEntity:
|
| 136 |
+
"""Extraction d'entités avec le modèle NER fine-tuné"""
|
| 137 |
+
|
| 138 |
+
# Prédiction NER
|
| 139 |
+
try:
|
| 140 |
+
ner_results = self.ner_pipeline(text)
|
| 141 |
+
except Exception as e:
|
| 142 |
+
logger.error(f"Erreur NER: {e}")
|
| 143 |
+
return MedicalEntity([], [], [], [], [], [], [], [])
|
| 144 |
+
|
| 145 |
+
# Groupement des entités par type
|
| 146 |
+
entities = {
|
| 147 |
+
"EXAM": [],
|
| 148 |
+
"SPECIALTY": [],
|
| 149 |
+
"ANATOMY": [],
|
| 150 |
+
"PATHOLOGY": [],
|
| 151 |
+
"PROCEDURE": [],
|
| 152 |
+
"MEASURE": [],
|
| 153 |
+
"MEDICATION": [],
|
| 154 |
+
"SYMPTOM": []
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
for result in ner_results:
|
| 158 |
+
entity_type = result['entity_group'].replace('B-', '').replace('I-', '')
|
| 159 |
+
entity_text = result['word']
|
| 160 |
+
confidence = result['score']
|
| 161 |
+
|
| 162 |
+
if entity_type in entities and confidence > 0.7: # Seuil de confiance
|
| 163 |
+
entities[entity_type].append((entity_text, confidence))
|
| 164 |
+
|
| 165 |
+
return MedicalEntity(
|
| 166 |
+
exam_types=entities["EXAM"],
|
| 167 |
+
specialties=entities["SPECIALTY"],
|
| 168 |
+
anatomical_regions=entities["ANATOMY"],
|
| 169 |
+
pathologies=entities["PATHOLOGY"],
|
| 170 |
+
medical_procedures=entities["PROCEDURE"],
|
| 171 |
+
measurements=entities["MEASURE"],
|
| 172 |
+
medications=entities["MEDICATION"],
|
| 173 |
+
symptoms=entities["SYMPTOM"]
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
def fine_tune_on_templates(self, templates_data: List[Dict],
|
| 177 |
+
output_dir: str = None,
|
| 178 |
+
epochs: int = 3):
|
| 179 |
+
"""Fine-tuning du modèle NER sur des templates médicaux"""
|
| 180 |
+
|
| 181 |
+
if output_dir is None:
|
| 182 |
+
output_dir = self.cache_dir / "medical_ner_model"
|
| 183 |
+
|
| 184 |
+
logger.info("Début du fine-tuning NER sur templates médicaux")
|
| 185 |
+
|
| 186 |
+
# Préparation des données d'entraînement
|
| 187 |
+
# (Ici, on utiliserait des templates annotés ou de l'auto-annotation)
|
| 188 |
+
train_dataset = self._prepare_training_data(templates_data)
|
| 189 |
+
|
| 190 |
+
# Configuration d'entraînement
|
| 191 |
+
training_args = TrainingArguments(
|
| 192 |
+
output_dir=output_dir,
|
| 193 |
+
num_train_epochs=epochs,
|
| 194 |
+
per_device_train_batch_size=8,
|
| 195 |
+
per_device_eval_batch_size=8,
|
| 196 |
+
warmup_steps=100,
|
| 197 |
+
weight_decay=0.01,
|
| 198 |
+
logging_dir=f"{output_dir}/logs",
|
| 199 |
+
save_strategy="epoch",
|
| 200 |
+
evaluation_strategy="epoch" if train_dataset.get('eval') else "no",
|
| 201 |
+
load_best_model_at_end=True,
|
| 202 |
+
metric_for_best_model="eval_loss" if train_dataset.get('eval') else None,
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
# Trainer
|
| 206 |
+
trainer = Trainer(
|
| 207 |
+
model=self.ner_model,
|
| 208 |
+
args=training_args,
|
| 209 |
+
train_dataset=train_dataset['train'],
|
| 210 |
+
eval_dataset=train_dataset.get('eval'),
|
| 211 |
+
tokenizer=self.tokenizer,
|
| 212 |
+
)
|
| 213 |
+
|
| 214 |
+
# Entraînement
|
| 215 |
+
trainer.train()
|
| 216 |
+
|
| 217 |
+
# Sauvegarde
|
| 218 |
+
trainer.save_model()
|
| 219 |
+
self.tokenizer.save_pretrained(output_dir)
|
| 220 |
+
|
| 221 |
+
logger.info(f"Fine-tuning terminé, modèle sauvé dans {output_dir}")
|
| 222 |
+
|
| 223 |
+
def _prepare_training_data(self, templates_data: List[Dict]) -> Dict:
|
| 224 |
+
"""Prépare les données d'entraînement pour le NER (auto-annotation intelligente)"""
|
| 225 |
+
|
| 226 |
+
# Cette fonction pourrait utiliser des techniques d'auto-annotation
|
| 227 |
+
# ou des datasets médicaux pré-existants pour créer des labels BIO
|
| 228 |
+
|
| 229 |
+
# Pour l'exemple, retourner un dataset vide
|
| 230 |
+
# En production, on utiliserait des techniques d'annotation automatique
|
| 231 |
+
# ou des datasets médicaux annotés comme QUAERO, CAS, etc.
|
| 232 |
+
|
| 233 |
+
class EmptyDataset(Dataset):
|
| 234 |
+
def __len__(self):
|
| 235 |
+
return 0
|
| 236 |
+
def __getitem__(self, idx):
|
| 237 |
+
return {}
|
| 238 |
+
|
| 239 |
+
return {'train': EmptyDataset()}
|
| 240 |
+
|
| 241 |
+
class AdvancedMedicalEmbedding:
|
| 242 |
+
"""Générateur d'embeddings médicaux avancés avec cross-encoder reranking"""
|
| 243 |
+
|
| 244 |
+
def __init__(self,
|
| 245 |
+
base_model: str = "almanach/camembert-bio-base",
|
| 246 |
+
cross_encoder_model: str = "auto"):
|
| 247 |
+
|
| 248 |
+
self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 249 |
+
self.base_model_name = base_model
|
| 250 |
+
|
| 251 |
+
# Modèle principal pour embeddings
|
| 252 |
+
self._load_base_model()
|
| 253 |
+
|
| 254 |
+
# Cross-encoder pour reranking
|
| 255 |
+
self._load_cross_encoder(cross_encoder_model)
|
| 256 |
+
|
| 257 |
+
def _load_base_model(self):
|
| 258 |
+
"""Charge le modèle de base pour les embeddings"""
|
| 259 |
+
try:
|
| 260 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.base_model_name)
|
| 261 |
+
self.base_model = AutoModel.from_pretrained(self.base_model_name)
|
| 262 |
+
self.base_model.to(self.device)
|
| 263 |
+
logger.info(f"Modèle de base chargé: {self.base_model_name}")
|
| 264 |
+
except Exception as e:
|
| 265 |
+
logger.error(f"Erreur chargement modèle de base: {e}")
|
| 266 |
+
raise
|
| 267 |
+
|
| 268 |
+
def _load_cross_encoder(self, model_name: str):
|
| 269 |
+
"""Charge le cross-encoder pour reranking"""
|
| 270 |
+
|
| 271 |
+
if model_name == "auto":
|
| 272 |
+
# Sélection automatique du meilleur cross-encoder médical
|
| 273 |
+
cross_encoders = [
|
| 274 |
+
"microsoft/BiomedNLP-PubMedBERT-base-uncased-abstract-fulltext",
|
| 275 |
+
"emilyalsentzer/Bio_ClinicalBERT",
|
| 276 |
+
self.base_model_name # Fallback
|
| 277 |
+
]
|
| 278 |
+
|
| 279 |
+
for model in cross_encoders:
|
| 280 |
+
try:
|
| 281 |
+
self.cross_tokenizer = AutoTokenizer.from_pretrained(model)
|
| 282 |
+
self.cross_model = AutoModel.from_pretrained(model)
|
| 283 |
+
self.cross_model.to(self.device)
|
| 284 |
+
logger.info(f"Cross-encoder chargé: {model}")
|
| 285 |
+
break
|
| 286 |
+
except:
|
| 287 |
+
continue
|
| 288 |
+
else:
|
| 289 |
+
self.cross_tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 290 |
+
self.cross_model = AutoModel.from_pretrained(model_name)
|
| 291 |
+
self.cross_model.to(self.device)
|
| 292 |
+
|
| 293 |
+
def generate_embedding(self, text: str, entities: MedicalEntity = None) -> np.ndarray:
|
| 294 |
+
"""Génère un embedding enrichi pour un texte médical"""
|
| 295 |
+
|
| 296 |
+
# Tokenisation
|
| 297 |
+
inputs = self.tokenizer(
|
| 298 |
+
text,
|
| 299 |
+
padding=True,
|
| 300 |
+
truncation=True,
|
| 301 |
+
max_length=512,
|
| 302 |
+
return_tensors="pt"
|
| 303 |
+
).to(self.device)
|
| 304 |
+
|
| 305 |
+
# Génération embedding
|
| 306 |
+
with torch.no_grad():
|
| 307 |
+
outputs = self.base_model(**inputs)
|
| 308 |
+
|
| 309 |
+
# Mean pooling
|
| 310 |
+
attention_mask = inputs['attention_mask']
|
| 311 |
+
token_embeddings = outputs.last_hidden_state
|
| 312 |
+
input_mask_expanded = attention_mask.unsqueeze(-1).expand(token_embeddings.size()).float()
|
| 313 |
+
embedding = torch.sum(token_embeddings * input_mask_expanded, 1) / torch.clamp(input_mask_expanded.sum(1), min=1e-9)
|
| 314 |
+
|
| 315 |
+
# Enrichissement avec entités NER
|
| 316 |
+
if entities:
|
| 317 |
+
embedding = self._enrich_with_ner_entities(embedding, entities)
|
| 318 |
+
|
| 319 |
+
return embedding.cpu().numpy().flatten().astype(np.float32)
|
| 320 |
+
|
| 321 |
+
def _enrich_with_ner_entities(self, base_embedding: torch.Tensor, entities: MedicalEntity) -> torch.Tensor:
|
| 322 |
+
"""Enrichit l'embedding avec les entités extraites par NER"""
|
| 323 |
+
|
| 324 |
+
# Concaténer les entités importantes avec leurs scores de confiance
|
| 325 |
+
entity_texts = []
|
| 326 |
+
confidence_weights = []
|
| 327 |
+
|
| 328 |
+
for entity_list in [entities.exam_types, entities.specialties,
|
| 329 |
+
entities.anatomical_regions, entities.pathologies]:
|
| 330 |
+
for entity_text, confidence in entity_list:
|
| 331 |
+
entity_texts.append(entity_text)
|
| 332 |
+
confidence_weights.append(confidence)
|
| 333 |
+
|
| 334 |
+
if not entity_texts:
|
| 335 |
+
return base_embedding
|
| 336 |
+
|
| 337 |
+
# Génération d'embeddings pour les entités
|
| 338 |
+
entity_text_combined = " [SEP] ".join(entity_texts)
|
| 339 |
+
entity_inputs = self.tokenizer(
|
| 340 |
+
entity_text_combined,
|
| 341 |
+
padding=True,
|
| 342 |
+
truncation=True,
|
| 343 |
+
max_length=256,
|
| 344 |
+
return_tensors="pt"
|
| 345 |
+
).to(self.device)
|
| 346 |
+
|
| 347 |
+
with torch.no_grad():
|
| 348 |
+
entity_outputs = self.base_model(**entity_inputs)
|
| 349 |
+
entity_embedding = torch.mean(entity_outputs.last_hidden_state, dim=1)
|
| 350 |
+
|
| 351 |
+
# Fusion pondérée par les scores de confiance
|
| 352 |
+
avg_confidence = np.mean(confidence_weights) if confidence_weights else 0.5
|
| 353 |
+
fusion_weight = min(0.4, avg_confidence) # Max 40% pour les entités
|
| 354 |
+
|
| 355 |
+
enriched_embedding = (1 - fusion_weight) * base_embedding + fusion_weight * entity_embedding
|
| 356 |
+
|
| 357 |
+
return enriched_embedding
|
| 358 |
+
|
| 359 |
+
def cross_encoder_rerank(self,
|
| 360 |
+
query: str,
|
| 361 |
+
candidates: List[Dict],
|
| 362 |
+
top_k: int = 3) -> List[Dict]:
|
| 363 |
+
"""Reranking avec cross-encoder pour affiner la sélection"""
|
| 364 |
+
|
| 365 |
+
if len(candidates) <= top_k:
|
| 366 |
+
return candidates
|
| 367 |
+
|
| 368 |
+
reranked_candidates = []
|
| 369 |
+
|
| 370 |
+
for candidate in candidates:
|
| 371 |
+
# Création de la paire query-candidate
|
| 372 |
+
pair_text = f"{query} [SEP] {candidate['document']}"
|
| 373 |
+
|
| 374 |
+
# Tokenisation
|
| 375 |
+
inputs = self.cross_tokenizer(
|
| 376 |
+
pair_text,
|
| 377 |
+
padding=True,
|
| 378 |
+
truncation=True,
|
| 379 |
+
max_length=512,
|
| 380 |
+
return_tensors="pt"
|
| 381 |
+
).to(self.device)
|
| 382 |
+
|
| 383 |
+
# Score de similarité cross-encoder
|
| 384 |
+
with torch.no_grad():
|
| 385 |
+
outputs = self.cross_model(**inputs)
|
| 386 |
+
# Utilisation du [CLS] token pour le score de similarité
|
| 387 |
+
cls_embedding = outputs.last_hidden_state[:, 0, :]
|
| 388 |
+
similarity_score = torch.sigmoid(torch.mean(cls_embedding)).item()
|
| 389 |
+
|
| 390 |
+
candidate_copy = candidate.copy()
|
| 391 |
+
candidate_copy['cross_encoder_score'] = similarity_score
|
| 392 |
+
candidate_copy['final_score'] = (
|
| 393 |
+
0.6 * candidate['similarity_score'] +
|
| 394 |
+
0.4 * similarity_score
|
| 395 |
+
)
|
| 396 |
+
|
| 397 |
+
reranked_candidates.append(candidate_copy)
|
| 398 |
+
|
| 399 |
+
# Tri par score final
|
| 400 |
+
reranked_candidates.sort(key=lambda x: x['final_score'], reverse=True)
|
| 401 |
+
|
| 402 |
+
return reranked_candidates[:top_k]
|
| 403 |
+
|
| 404 |
+
class MedicalTemplateVectorDB:
|
| 405 |
+
"""Base de données vectorielle optimisée pour templates médicaux"""
|
| 406 |
+
|
| 407 |
+
def __init__(self, db_path: str = "./medical_vector_db", collection_name: str = "medical_templates"):
|
| 408 |
+
self.db_path = db_path
|
| 409 |
+
self.collection_name = collection_name
|
| 410 |
+
|
| 411 |
+
# ChromaDB avec configuration optimisée
|
| 412 |
+
self.client = chromadb.PersistentClient(
|
| 413 |
+
path=db_path,
|
| 414 |
+
settings=Settings(
|
| 415 |
+
anonymized_telemetry=False,
|
| 416 |
+
allow_reset=True
|
| 417 |
+
)
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
# Collection avec métrique de distance optimisée
|
| 421 |
+
try:
|
| 422 |
+
self.collection = self.client.get_collection(collection_name)
|
| 423 |
+
logger.info(f"Collection '{collection_name}' chargée")
|
| 424 |
+
except:
|
| 425 |
+
self.collection = self.client.create_collection(
|
| 426 |
+
name=collection_name,
|
| 427 |
+
metadata={
|
| 428 |
+
"hnsw:space": "cosine",
|
| 429 |
+
"hnsw:M": 32, # Connectivité du graphe
|
| 430 |
+
"hnsw:ef_construction": 200, # Qualité vs vitesse construction
|
| 431 |
+
"hnsw:ef_search": 50 # Qualité vs vitesse recherche
|
| 432 |
+
}
|
| 433 |
+
)
|
| 434 |
+
logger.info(f"Collection '{collection_name}' créée avec optimisations HNSW")
|
| 435 |
+
|
| 436 |
+
def add_template(self,
|
| 437 |
+
template_id: str,
|
| 438 |
+
template_text: str,
|
| 439 |
+
embedding: np.ndarray,
|
| 440 |
+
entities: MedicalEntity,
|
| 441 |
+
metadata: Dict[str, Any] = None):
|
| 442 |
+
"""Ajoute un template avec métadonnées enrichies par NER"""
|
| 443 |
+
|
| 444 |
+
# Métadonnées automatiques basées sur NER
|
| 445 |
+
auto_metadata = {
|
| 446 |
+
"exam_types": [entity[0] for entity in entities.exam_types],
|
| 447 |
+
"specialties": [entity[0] for entity in entities.specialties],
|
| 448 |
+
"anatomical_regions": [entity[0] for entity in entities.anatomical_regions],
|
| 449 |
+
"pathologies": [entity[0] for entity in entities.pathologies],
|
| 450 |
+
"procedures": [entity[0] for entity in entities.medical_procedures],
|
| 451 |
+
"text_length": len(template_text),
|
| 452 |
+
"entity_confidence_avg": np.mean([
|
| 453 |
+
entity[1] for entity_list in [
|
| 454 |
+
entities.exam_types, entities.specialties,
|
| 455 |
+
entities.anatomical_regions, entities.pathologies
|
| 456 |
+
] for entity in entity_list
|
| 457 |
+
]) if any([entities.exam_types, entities.specialties,
|
| 458 |
+
entities.anatomical_regions, entities.pathologies]) else 0.0
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
if metadata:
|
| 462 |
+
auto_metadata.update(metadata)
|
| 463 |
+
|
| 464 |
+
self.collection.add(
|
| 465 |
+
embeddings=[embedding.tolist()],
|
| 466 |
+
documents=[template_text],
|
| 467 |
+
metadatas=[auto_metadata],
|
| 468 |
+
ids=[template_id]
|
| 469 |
+
)
|
| 470 |
+
|
| 471 |
+
logger.info(f"Template {template_id} ajouté avec métadonnées NER automatiques")
|
| 472 |
+
|
| 473 |
+
def advanced_search(self,
|
| 474 |
+
query_embedding: np.ndarray,
|
| 475 |
+
n_results: int = 10,
|
| 476 |
+
entity_filters: Dict[str, List[str]] = None,
|
| 477 |
+
confidence_threshold: float = 0.0) -> List[Dict]:
|
| 478 |
+
"""Recherche avancée avec filtres basés sur entités NER"""
|
| 479 |
+
|
| 480 |
+
where_clause = {}
|
| 481 |
+
|
| 482 |
+
# Filtres basés sur entités NER extraites
|
| 483 |
+
if entity_filters:
|
| 484 |
+
for entity_type, entity_values in entity_filters.items():
|
| 485 |
+
if entity_values:
|
| 486 |
+
where_clause[entity_type] = {"$in": entity_values}
|
| 487 |
+
|
| 488 |
+
# Filtre par confiance moyenne des entités
|
| 489 |
+
if confidence_threshold > 0:
|
| 490 |
+
where_clause["entity_confidence_avg"] = {"$gte": confidence_threshold}
|
| 491 |
+
|
| 492 |
+
results = self.collection.query(
|
| 493 |
+
query_embeddings=[query_embedding.tolist()],
|
| 494 |
+
n_results=n_results,
|
| 495 |
+
where=where_clause if where_clause else None,
|
| 496 |
+
include=["documents", "metadatas", "distances"]
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
# Formatage des résultats
|
| 500 |
+
formatted_results = []
|
| 501 |
+
for i in range(len(results['ids'][0])):
|
| 502 |
+
formatted_results.append({
|
| 503 |
+
'id': results['ids'][0][i],
|
| 504 |
+
'document': results['documents'][0][i],
|
| 505 |
+
'metadata': results['metadatas'][0][i],
|
| 506 |
+
'similarity_score': 1 - results['distances'][0][i],
|
| 507 |
+
'distance': results['distances'][0][i]
|
| 508 |
+
})
|
| 509 |
+
|
| 510 |
+
return formatted_results
|
| 511 |
+
|
| 512 |
+
class AdvancedMedicalTemplateProcessor:
|
| 513 |
+
"""Processeur avancé avec NER fine-tuné et reranking cross-encoder"""
|
| 514 |
+
|
| 515 |
+
def __init__(self,
|
| 516 |
+
base_model: str = "almanach/camembert-bio-base",
|
| 517 |
+
db_path: str = "./advanced_medical_vector_db"):
|
| 518 |
+
|
| 519 |
+
self.ner_extractor = AdvancedMedicalNER()
|
| 520 |
+
self.embedding_generator = AdvancedMedicalEmbedding(base_model)
|
| 521 |
+
self.vector_db = MedicalTemplateVectorDB(db_path)
|
| 522 |
+
|
| 523 |
+
logger.info("Processeur médical avancé initialisé avec NER fine-tuné et cross-encoder reranking")
|
| 524 |
+
|
| 525 |
+
def process_templates_batch(self,
|
| 526 |
+
templates: List[Dict[str, str]],
|
| 527 |
+
batch_size: int = 8,
|
| 528 |
+
fine_tune_ner: bool = False) -> None:
|
| 529 |
+
"""Traitement avancé avec option de fine-tuning NER"""
|
| 530 |
+
|
| 531 |
+
if fine_tune_ner:
|
| 532 |
+
logger.info("Fine-tuning du modèle NER sur les templates...")
|
| 533 |
+
self.ner_extractor.fine_tune_on_templates(templates)
|
| 534 |
+
|
| 535 |
+
logger.info(f"Traitement avancé de {len(templates)} templates")
|
| 536 |
+
|
| 537 |
+
for i in tqdm(range(0, len(templates), batch_size), desc="Traitement avancé"):
|
| 538 |
+
batch = templates[i:i+batch_size]
|
| 539 |
+
|
| 540 |
+
for template in batch:
|
| 541 |
+
try:
|
| 542 |
+
template_id = template['id']
|
| 543 |
+
template_text = template['text']
|
| 544 |
+
metadata = template.get('metadata', {})
|
| 545 |
+
|
| 546 |
+
# NER avancé
|
| 547 |
+
entities = self.ner_extractor.extract_entities(template_text)
|
| 548 |
+
|
| 549 |
+
# Embedding enrichi
|
| 550 |
+
embedding = self.embedding_generator.generate_embedding(template_text, entities)
|
| 551 |
+
|
| 552 |
+
# Stockage avec métadonnées NER
|
| 553 |
+
self.vector_db.add_template(
|
| 554 |
+
template_id=template_id,
|
| 555 |
+
template_text=template_text,
|
| 556 |
+
embedding=embedding,
|
| 557 |
+
entities=entities,
|
| 558 |
+
metadata=metadata
|
| 559 |
+
)
|
| 560 |
+
|
| 561 |
+
except Exception as e:
|
| 562 |
+
logger.error(f"Erreur traitement template {template.get('id', 'unknown')}: {e}")
|
| 563 |
+
continue
|
| 564 |
+
|
| 565 |
+
def find_best_template_with_reranking(self,
|
| 566 |
+
transcription: str,
|
| 567 |
+
initial_candidates: int = 10,
|
| 568 |
+
final_results: int = 3) -> List[Dict]:
|
| 569 |
+
"""Recherche optimale avec reranking cross-encoder"""
|
| 570 |
+
|
| 571 |
+
# 1. Extraction NER de la transcription
|
| 572 |
+
query_entities = self.ner_extractor.extract_entities(transcription)
|
| 573 |
+
|
| 574 |
+
# 2. Génération embedding enrichi
|
| 575 |
+
query_embedding = self.embedding_generator.generate_embedding(transcription, query_entities)
|
| 576 |
+
|
| 577 |
+
# 3. Filtres automatiques basés sur entités extraites
|
| 578 |
+
entity_filters = {}
|
| 579 |
+
if query_entities.exam_types:
|
| 580 |
+
entity_filters['exam_types'] = [entity[0] for entity in query_entities.exam_types]
|
| 581 |
+
if query_entities.specialties:
|
| 582 |
+
entity_filters['specialties'] = [entity[0] for entity in query_entities.specialties]
|
| 583 |
+
if query_entities.anatomical_regions:
|
| 584 |
+
entity_filters['anatomical_regions'] = [entity[0] for entity in query_entities.anatomical_regions]
|
| 585 |
+
|
| 586 |
+
# 4. Recherche vectorielle initiale
|
| 587 |
+
initial_candidates_results = self.vector_db.advanced_search(
|
| 588 |
+
query_embedding=query_embedding,
|
| 589 |
+
n_results=initial_candidates,
|
| 590 |
+
entity_filters=entity_filters,
|
| 591 |
+
confidence_threshold=0.6
|
| 592 |
+
)
|
| 593 |
+
|
| 594 |
+
# 5. Reranking avec cross-encoder
|
| 595 |
+
if len(initial_candidates_results) > final_results:
|
| 596 |
+
final_results_reranked = self.embedding_generator.cross_encoder_rerank(
|
| 597 |
+
query=transcription,
|
| 598 |
+
candidates=initial_candidates_results,
|
| 599 |
+
top_k=final_results
|
| 600 |
+
)
|
| 601 |
+
else:
|
| 602 |
+
final_results_reranked = initial_candidates_results
|
| 603 |
+
|
| 604 |
+
# 6. Enrichissement des résultats avec détails NER
|
| 605 |
+
for result in final_results_reranked:
|
| 606 |
+
result['query_entities'] = {
|
| 607 |
+
'exam_types': query_entities.exam_types,
|
| 608 |
+
'specialties': query_entities.specialties,
|
| 609 |
+
'anatomical_regions': query_entities.anatomical_regions,
|
| 610 |
+
'pathologies': query_entities.pathologies
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
return final_results_reranked
|
| 614 |
+
|
| 615 |
+
# Exemple d'utilisation avancée
|
| 616 |
+
def main():
|
| 617 |
+
"""Exemple d'utilisation du système avancé"""
|
| 618 |
+
|
| 619 |
+
# Initialisation du processeur avancé
|
| 620 |
+
processor = AdvancedMedicalTemplateProcessor()
|
| 621 |
+
|
| 622 |
+
# Traitement des templates avec fine-tuning optionnel
|
| 623 |
+
sample_templates = [
|
| 624 |
+
{
|
| 625 |
+
'id': 'angio_001',
|
| 626 |
+
'text': """Échographie et doppler artério-veineux des membres inférieurs.
|
| 627 |
+
Exploration de l'incontinence veineuse superficielle...""",
|
| 628 |
+
'metadata': {'source': 'angiologie', 'version': '2024'}
|
| 629 |
+
}
|
| 630 |
+
]
|
| 631 |
+
|
| 632 |
+
# Traitement avec fine-tuning NER
|
| 633 |
+
processor.process_templates_batch(sample_templates, fine_tune_ner=False)
|
| 634 |
+
|
| 635 |
+
# Recherche avec reranking
|
| 636 |
+
transcription = """madame bacon nicole bilan œdème droit gonalgies ostéophytes
|
| 637 |
+
incontinence veineuse modérée portions surale droite crurale gauche saphéniennes"""
|
| 638 |
+
|
| 639 |
+
best_matches = processor.find_best_template_with_reranking(
|
| 640 |
+
transcription=transcription,
|
| 641 |
+
initial_candidates=15,
|
| 642 |
+
final_results=3
|
| 643 |
+
)
|
| 644 |
+
|
| 645 |
+
# Affichage des résultats
|
| 646 |
+
for i, match in enumerate(best_matches):
|
| 647 |
+
print(f"\n=== Match {i+1} ===")
|
| 648 |
+
print(f"Template ID: {match['id']}")
|
| 649 |
+
print(f"Score final: {match.get('final_score', match['similarity_score']):.4f}")
|
| 650 |
+
print(f"Score cross-encoder: {match.get('cross_encoder_score', 'N/A')}")
|
| 651 |
+
print(f"Entités détectées dans la query:")
|
| 652 |
+
for entity_type, entities in match.get('query_entities', {}).items():
|
| 653 |
+
if entities:
|
| 654 |
+
print(f" - {entity_type}: {[f'{e[0]} ({e[1]:.2f})' for e in entities]}")
|
| 655 |
+
|
| 656 |
+
if __name__ == "__main__":
|
| 657 |
+
main()
|
README.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
|
| 4 |
-
colorFrom: pink
|
| 5 |
-
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version: 5.
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
---
|
| 11 |
-
|
| 12 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: medical-agent
|
| 3 |
+
app_file: app2.py
|
|
|
|
|
|
|
| 4 |
sdk: gradio
|
| 5 |
+
sdk_version: 5.47.2
|
|
|
|
|
|
|
| 6 |
---
|
|
|
|
|
|
annotation.py
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
from openai import AzureOpenAI
|
| 4 |
+
import json
|
| 5 |
+
|
| 6 |
+
load_dotenv()
|
| 7 |
+
|
| 8 |
+
AZURE_OPENAI_KEY = os.getenv("AZURE_OPENAI_KEY")
|
| 9 |
+
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
|
| 10 |
+
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT") # deployment name
|
| 11 |
+
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2024-12-01-preview")
|
| 12 |
+
|
| 13 |
+
# Configure OpenAI for Azure
|
| 14 |
+
client = AzureOpenAI(
|
| 15 |
+
api_key=AZURE_OPENAI_KEY,
|
| 16 |
+
api_version=AZURE_OPENAI_API_VERSION,
|
| 17 |
+
azure_endpoint=AZURE_OPENAI_ENDPOINT
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
def extract_medical_entities(text: str) -> dict:
|
| 21 |
+
prompt = f""" You are a medical NER expert. Your task is to extract relevant entities from the given medical report text and return them in a JSON object.
|
| 22 |
+
|
| 23 |
+
Analyze the text carefully and identify the following fields:
|
| 24 |
+
|
| 25 |
+
- "exam_types": any type of medical test, examination, or diagnostic method performed on the patient.
|
| 26 |
+
- "specialties": the branch of medicine or medical discipline relevant to the report.
|
| 27 |
+
- "anatomical_regions": specific parts or regions of the body mentioned in the report.
|
| 28 |
+
- "pathologies": diagnosed diseases, disorders, or abnormal medical conditions noted in the report.
|
| 29 |
+
- "procedures": medical interventions, treatments, or actions performed on the patient.
|
| 30 |
+
- "measurements": numerical values or quantities recorded in the report, such as vital signs, lab results, sizes, or pressures.
|
| 31 |
+
- "medications": drugs, therapies, or prescribed substances mentioned in the report.
|
| 32 |
+
- "symptoms": patient-experienced signs or observable indications of a health issue.
|
| 33 |
+
|
| 34 |
+
Text to analyze:
|
| 35 |
+
\"\"\"
|
| 36 |
+
{text}
|
| 37 |
+
\"\"\"
|
| 38 |
+
|
| 39 |
+
Return ONLY a valid JSON object with all fields. If a field has no values, return an empty list.
|
| 40 |
+
"""
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
response = client.chat.completions.create(
|
| 45 |
+
model=AZURE_OPENAI_DEPLOYMENT,
|
| 46 |
+
messages=[{"role": "user", "content": prompt}],
|
| 47 |
+
#temperature=0,
|
| 48 |
+
#max_tokens=1024
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
content = response.choices[0].message.content
|
| 52 |
+
try:
|
| 53 |
+
return json.loads(content)
|
| 54 |
+
except json.JSONDecodeError:
|
| 55 |
+
return {
|
| 56 |
+
"exam_types": [],
|
| 57 |
+
"specialties": [],
|
| 58 |
+
"anatomical_regions": [],
|
| 59 |
+
"pathologies": [],
|
| 60 |
+
"procedures": [],
|
| 61 |
+
"measurements": [],
|
| 62 |
+
"medications": [],
|
| 63 |
+
"symptoms": []
|
| 64 |
+
}
|
| 65 |
+
import json
|
| 66 |
+
|
| 67 |
+
def save_annotation(text: str, labels: dict, output_file="dataset.jsonl"):
|
| 68 |
+
record = {
|
| 69 |
+
"text": text,
|
| 70 |
+
"labels": labels
|
| 71 |
+
}
|
| 72 |
+
# append as one line of JSON
|
| 73 |
+
with open(output_file, "a", encoding="utf-8") as f:
|
| 74 |
+
f.write(json.dumps(record, ensure_ascii=False) + "\n")
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
if __name__ == "__main__":
|
| 78 |
+
input_folder = "data_txt" # 📂 folder containing your .txt files
|
| 79 |
+
output_file = "dataset.json"
|
| 80 |
+
|
| 81 |
+
# Ensure output file is empty before starting
|
| 82 |
+
open(output_file, "w", encoding="utf-8").close()
|
| 83 |
+
|
| 84 |
+
for filename in os.listdir(input_folder):
|
| 85 |
+
if filename.endswith(".txt"):
|
| 86 |
+
file_path = os.path.join(input_folder, filename)
|
| 87 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 88 |
+
transcription = f.read().strip()
|
| 89 |
+
|
| 90 |
+
print(f"\n=== Processing {filename} ===")
|
| 91 |
+
entities = extract_medical_entities(transcription)
|
| 92 |
+
|
| 93 |
+
# Save results
|
| 94 |
+
save_annotation(transcription, entities, output_file=output_file)
|
| 95 |
+
|
| 96 |
+
print(f"✅ Saved {filename} → {output_file}")
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
input_folder = "data_txt" # 📂 folder containing your .txt files
|
| 102 |
+
output_file = "dataset.json"
|
| 103 |
+
|
| 104 |
+
# Liste des fichiers à exclure
|
| 105 |
+
excluded_files = {
|
| 106 |
+
"template7.txt",
|
| 107 |
+
"template1167.txt",
|
| 108 |
+
"template429.txt",
|
| 109 |
+
"template401.txt",
|
| 110 |
+
"template367.txt",
|
| 111 |
+
"template415.txt",
|
| 112 |
+
"template398.txt",
|
| 113 |
+
"template1198.txt",
|
| 114 |
+
"template159.txt",
|
| 115 |
+
"template165.txt",
|
| 116 |
+
"template1107.txt",
|
| 117 |
+
"template449.txt",
|
| 118 |
+
"template1113.txt",
|
| 119 |
+
"template313.txt",
|
| 120 |
+
"template475.txt",
|
| 121 |
+
"template461.txt",
|
| 122 |
+
"template307.txt",
|
| 123 |
+
"template893.txt",
|
| 124 |
+
"template139.txt",
|
| 125 |
+
"template887.txt",
|
| 126 |
+
"template677.txt",
|
| 127 |
+
"template111.txt",
|
| 128 |
+
"template105.txt",
|
| 129 |
+
"template663.txt",
|
| 130 |
+
"template688.txt",
|
| 131 |
+
"template850.txt",
|
| 132 |
+
"template844.txt",
|
| 133 |
+
"template878.txt",
|
| 134 |
+
"template16.txt",
|
| 135 |
+
"template703.txt",
|
| 136 |
+
"template717.txt",
|
| 137 |
+
"template924.txt",
|
| 138 |
+
"template930.txt",
|
| 139 |
+
"template918.txt",
|
| 140 |
+
"template1073.txt",
|
| 141 |
+
"template529.txt",
|
| 142 |
+
"template1067.txt",
|
| 143 |
+
"template267.txt",
|
| 144 |
+
"template501.txt",
|
| 145 |
+
"template515.txt",
|
| 146 |
+
"template273.txt",
|
| 147 |
+
"template298.txt",
|
| 148 |
+
"template1098.txt",
|
| 149 |
+
"template1099.txt",
|
| 150 |
+
"template299.txt",
|
| 151 |
+
"template514.txt",
|
| 152 |
+
"template272.txt",
|
| 153 |
+
"template266.txt",
|
| 154 |
+
"template500.txt",
|
| 155 |
+
"template528.txt",
|
| 156 |
+
"template1066.txt",
|
| 157 |
+
"template1072.txt",
|
| 158 |
+
"template919.txt",
|
| 159 |
+
"template931.txt",
|
| 160 |
+
"template925.txt",
|
| 161 |
+
"template716.txt",
|
| 162 |
+
"template702.txt",
|
| 163 |
+
"template879.txt",
|
| 164 |
+
"template845.txt",
|
| 165 |
+
"template851.txt",
|
| 166 |
+
"template689.txt",
|
| 167 |
+
"template104.txt",
|
| 168 |
+
"template662.txt",
|
| 169 |
+
"template676.txt",
|
| 170 |
+
"template110.txt",
|
| 171 |
+
"template138.txt",
|
| 172 |
+
"template886.txt",
|
| 173 |
+
"template892.txt",
|
| 174 |
+
"template460.txt",
|
| 175 |
+
"template306.txt",
|
| 176 |
+
"template312.txt",
|
| 177 |
+
"template474.txt",
|
| 178 |
+
"template1112.txt",
|
| 179 |
+
"template1106.txt",
|
| 180 |
+
"template448.txt",
|
| 181 |
+
"template338.txt",
|
| 182 |
+
"template1110.txt",
|
| 183 |
+
"template1104.txt",
|
| 184 |
+
"template304.txt",
|
| 185 |
+
"template462.txt",
|
| 186 |
+
"template476.txt",
|
| 187 |
+
"template310.txt",
|
| 188 |
+
"template1138.txt",
|
| 189 |
+
"template489.txt",
|
| 190 |
+
"template884.txt",
|
| 191 |
+
"template890.txt",
|
| 192 |
+
"template648.txt",
|
| 193 |
+
"template660.txt",
|
| 194 |
+
"template106.txt",
|
| 195 |
+
"template112.txt",
|
| 196 |
+
"template674.txt",
|
| 197 |
+
"template847.txt",
|
| 198 |
+
"template853.txt",
|
| 199 |
+
"template728.txt",
|
| 200 |
+
"template15.txt",
|
| 201 |
+
"template714.txt",
|
| 202 |
+
"template29.txt",
|
| 203 |
+
"template700.txt",
|
| 204 |
+
"template933.txt",
|
| 205 |
+
"template927.txt",
|
| 206 |
+
"template1064.txt",
|
| 207 |
+
"template1070.txt",
|
| 208 |
+
"template258.txt",
|
| 209 |
+
"template1058.txt",
|
| 210 |
+
"template270.txt",
|
| 211 |
+
"template516.txt",
|
| 212 |
+
"template502.txt",
|
| 213 |
+
"template264.txt",
|
| 214 |
+
"template503.txt",
|
| 215 |
+
"template265.txt",
|
| 216 |
+
"template271.txt",
|
| 217 |
+
"template1059.txt",
|
| 218 |
+
"template517.txt",
|
| 219 |
+
"template259.txt",
|
| 220 |
+
"template1071.txt",
|
| 221 |
+
"template1065.txt",
|
| 222 |
+
"template926.txt",
|
| 223 |
+
"template932.txt",
|
| 224 |
+
"template701.txt",
|
| 225 |
+
"template715.txt",
|
| 226 |
+
"template28.txt",
|
| 227 |
+
"template729.txt",
|
| 228 |
+
"template14.txt",
|
| 229 |
+
"template852.txt",
|
| 230 |
+
"template846.txt",
|
| 231 |
+
"template113.txt",
|
| 232 |
+
"template675.txt",
|
| 233 |
+
"template661.txt",
|
| 234 |
+
"template107.txt",
|
| 235 |
+
"template649.txt",
|
| 236 |
+
"template891.txt",
|
| 237 |
+
"template885.txt",
|
| 238 |
+
"template488.txt",
|
| 239 |
+
"template477.txt",
|
| 240 |
+
"template1139.txt",
|
| 241 |
+
"template311.txt",
|
| 242 |
+
"template305.txt",
|
| 243 |
+
"template463.txt",
|
| 244 |
+
"template1105.txt",
|
| 245 |
+
"template1111.txt",
|
| 246 |
+
"template339.txt",
|
| 247 |
+
"template467.txt",
|
| 248 |
+
"template1129.txt",
|
| 249 |
+
"template301.txt",
|
| 250 |
+
"template315.txt",
|
| 251 |
+
"template473.txt",
|
| 252 |
+
"template1115.txt",
|
| 253 |
+
"template1101.txt",
|
| 254 |
+
"template329.txt",
|
| 255 |
+
"template498.txt",
|
| 256 |
+
"template103.txt",
|
| 257 |
+
"template665.txt",
|
| 258 |
+
"template671.txt",
|
| 259 |
+
"template117.txt",
|
| 260 |
+
"template881.txt",
|
| 261 |
+
"template659.txt",
|
| 262 |
+
"template895.txt",
|
| 263 |
+
"template842.txt",
|
| 264 |
+
"template856.txt",
|
| 265 |
+
"template711.txt",
|
| 266 |
+
"template705.txt",
|
| 267 |
+
"template38.txt",
|
| 268 |
+
"template10.txt",
|
| 269 |
+
"template739.txt",
|
| 270 |
+
"template936.txt",
|
| 271 |
+
"template922.txt",
|
| 272 |
+
"template513.txt",
|
| 273 |
+
"template275.txt",
|
| 274 |
+
"template261.txt",
|
| 275 |
+
"template1049.txt",
|
| 276 |
+
"template507.txt",
|
| 277 |
+
"template249.txt",
|
| 278 |
+
"template1061.txt",
|
| 279 |
+
"template1075.txt",
|
| 280 |
+
"template1074.txt",
|
| 281 |
+
"template1060.txt",
|
| 282 |
+
"template248.txt",
|
| 283 |
+
"template1048.txt",
|
| 284 |
+
"template260.txt",
|
| 285 |
+
"template506.txt",
|
| 286 |
+
"template512.txt",
|
| 287 |
+
"template274.txt",
|
| 288 |
+
"template923.txt",
|
| 289 |
+
"template937.txt",
|
| 290 |
+
"template738.txt",
|
| 291 |
+
"template11.txt",
|
| 292 |
+
"template704.txt",
|
| 293 |
+
"template710.txt",
|
| 294 |
+
"template857.txt",
|
| 295 |
+
"template843.txt",
|
| 296 |
+
"template894.txt",
|
| 297 |
+
"template658.txt",
|
| 298 |
+
"template880.txt",
|
| 299 |
+
"template670.txt",
|
| 300 |
+
"template116.txt",
|
| 301 |
+
"template102.txt",
|
| 302 |
+
"template664.txt",
|
| 303 |
+
"template499.txt",
|
| 304 |
+
"template328.txt",
|
| 305 |
+
"template1100.txt",
|
| 306 |
+
"template1114.txt",
|
| 307 |
+
"template314.txt",
|
| 308 |
+
"template472.txt",
|
| 309 |
+
"template466.txt",
|
| 310 |
+
"template300.txt",
|
| 311 |
+
"template1128.txt",
|
| 312 |
+
"template470.txt",
|
| 313 |
+
"template316.txt",
|
| 314 |
+
"template302.txt",
|
| 315 |
+
"template464.txt",
|
| 316 |
+
"template1102.txt",
|
| 317 |
+
"template1116.txt",
|
| 318 |
+
"template458.txt",
|
| 319 |
+
"template114.txt",
|
| 320 |
+
"template672.txt",
|
| 321 |
+
"template666.txt",
|
| 322 |
+
"template100.txt",
|
| 323 |
+
"template128.txt",
|
| 324 |
+
"template896.txt",
|
| 325 |
+
"template882.txt",
|
| 326 |
+
"template869.txt",
|
| 327 |
+
"template855.txt",
|
| 328 |
+
"template699.txt",
|
| 329 |
+
"template841.txt",
|
| 330 |
+
"template706.txt",
|
| 331 |
+
"template712.txt",
|
| 332 |
+
"template13.txt",
|
| 333 |
+
"template909.txt",
|
| 334 |
+
"template921.txt",
|
| 335 |
+
"template935.txt",
|
| 336 |
+
"template504.txt",
|
| 337 |
+
"template262.txt",
|
| 338 |
+
"template276.txt",
|
| 339 |
+
"template510.txt",
|
| 340 |
+
"template538.txt",
|
| 341 |
+
"template1076.txt",
|
| 342 |
+
"template1062.txt",
|
| 343 |
+
"template1089.txt",
|
| 344 |
+
"template289.txt",
|
| 345 |
+
"template288.txt",
|
| 346 |
+
"template1088.txt",
|
| 347 |
+
"template1063.txt",
|
| 348 |
+
"template539.txt",
|
| 349 |
+
"template1077.txt",
|
| 350 |
+
"template277.txt",
|
| 351 |
+
"template511.txt",
|
| 352 |
+
"template505.txt",
|
| 353 |
+
"template263.txt",
|
| 354 |
+
"template934.txt",
|
| 355 |
+
"template920.txt",
|
| 356 |
+
"template908.txt",
|
| 357 |
+
"template12.txt",
|
| 358 |
+
"template713.txt",
|
| 359 |
+
"template707.txt",
|
| 360 |
+
"template840.txt",
|
| 361 |
+
"template698.txt",
|
| 362 |
+
"template854.txt",
|
| 363 |
+
"template868.txt",
|
| 364 |
+
"template883.txt",
|
| 365 |
+
"template129.txt",
|
| 366 |
+
"template897.txt",
|
| 367 |
+
"template667.txt",
|
| 368 |
+
"template101.txt",
|
| 369 |
+
"template115.txt",
|
| 370 |
+
"template673.txt",
|
| 371 |
+
"template1117.txt",
|
| 372 |
+
"template459.txt",
|
| 373 |
+
"template1103.txt",
|
| 374 |
+
"template303.txt",
|
| 375 |
+
"template465.txt",
|
| 376 |
+
"template471.txt",
|
| 377 |
+
"template317.txt",
|
| 378 |
+
"template4.txt",
|
| 379 |
+
"template1164.txt",
|
| 380 |
+
"template1170.txt",
|
| 381 |
+
"template358.txt",
|
| 382 |
+
"template416.txt",
|
| 383 |
+
"template1158.txt",
|
| 384 |
+
"template370.txt",
|
| 385 |
+
"template364.txt",
|
| 386 |
+
"template402.txt",
|
| 387 |
+
"template628.txt",
|
| 388 |
+
"template172.txt",
|
| 389 |
+
"template614.txt",
|
| 390 |
+
"template600.txt",
|
| 391 |
+
"template166.txt",
|
| 392 |
+
"template833.txt",
|
| 393 |
+
"template827.txt",
|
| 394 |
+
"template199.txt",
|
| 395 |
+
"template61.txt",
|
| 396 |
+
"template1212.txt",
|
| 397 |
+
"template984.txt",
|
| 398 |
+
"template748.txt",
|
| 399 |
+
"template990.txt",
|
| 400 |
+
"template75.txt",
|
| 401 |
+
"template1206.txt",
|
| 402 |
+
"template760.txt",
|
| 403 |
+
"template774.txt",
|
| 404 |
+
"template49.txt",
|
| 405 |
+
"template947.txt",
|
| 406 |
+
"template953.txt",
|
| 407 |
+
"template238.txt",
|
| 408 |
+
"template1010.txt",
|
| 409 |
+
"template1004.txt",
|
| 410 |
+
"template562.txt",
|
| 411 |
+
"template204.txt",
|
| 412 |
+
"template210.txt",
|
| 413 |
+
"template1038.txt",
|
| 414 |
+
"template576.txt",
|
| 415 |
+
"template589.txt",
|
| 416 |
+
"template588.txt",
|
| 417 |
+
"template1039.txt",
|
| 418 |
+
"template211.txt",
|
| 419 |
+
"template577.txt",
|
| 420 |
+
"template563.txt",
|
| 421 |
+
"template205.txt",
|
| 422 |
+
"template1005.txt",
|
| 423 |
+
"template1011.txt",
|
| 424 |
+
"template239.txt",
|
| 425 |
+
"template952.txt",
|
| 426 |
+
"template946.txt",
|
| 427 |
+
"template775.txt",
|
| 428 |
+
"template48.txt",
|
| 429 |
+
"template761.txt",
|
| 430 |
+
"template991.txt",
|
| 431 |
+
"template749.txt",
|
| 432 |
+
"template1207.txt",
|
| 433 |
+
"template74.txt",
|
| 434 |
+
"template1213.txt",
|
| 435 |
+
"template60.txt",
|
| 436 |
+
"template985.txt",
|
| 437 |
+
"template826.txt",
|
| 438 |
+
"template198.txt",
|
| 439 |
+
"template832.txt",
|
| 440 |
+
"template601.txt",
|
| 441 |
+
"template167.txt",
|
| 442 |
+
"template173.txt",
|
| 443 |
+
"template615.txt",
|
| 444 |
+
"template629.txt",
|
| 445 |
+
"template365.txt",
|
| 446 |
+
"template403.txt",
|
| 447 |
+
"template417.txt",
|
| 448 |
+
"template371.txt",
|
| 449 |
+
"template1159.txt",
|
| 450 |
+
"template359.txt",
|
| 451 |
+
"template1171.txt",
|
| 452 |
+
"template1165.txt",
|
| 453 |
+
"template5.txt",
|
| 454 |
+
"template1173.txt",
|
| 455 |
+
"template373.txt"
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
# Ensure output file is empty before starting
|
| 459 |
+
open(output_file, "w", encoding="utf-8").close()
|
| 460 |
+
|
| 461 |
+
processed_count = 0
|
| 462 |
+
excluded_count = 0
|
| 463 |
+
|
| 464 |
+
for filename in os.listdir(input_folder):
|
| 465 |
+
if filename.endswith(".txt"):
|
| 466 |
+
# Vérifier si le fichier est dans la liste d'exclusion
|
| 467 |
+
if filename in excluded_files:
|
| 468 |
+
print(f"⏭️ Fichier exclu : {filename}")
|
| 469 |
+
excluded_count += 1
|
| 470 |
+
continue
|
| 471 |
+
|
| 472 |
+
file_path = os.path.join(input_folder, filename)
|
| 473 |
+
with open(file_path, "r", encoding="utf-8") as f:
|
| 474 |
+
transcription = f.read().strip()
|
| 475 |
+
|
| 476 |
+
print(f"\n=== Processing {filename} ===")
|
| 477 |
+
entities = extract_medical_entities(transcription)
|
| 478 |
+
|
| 479 |
+
# Save results
|
| 480 |
+
save_annotation(transcription, entities, output_file=output_file)
|
| 481 |
+
|
| 482 |
+
print(f"✅ Saved {filename} → {output_file}")
|
| 483 |
+
processed_count += 1
|
| 484 |
+
|
| 485 |
+
print(f"\n📊 Résumé : {processed_count} fichiers traités, {excluded_count} fichiers exclus")
|
| 486 |
+
"""
|
app.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Interface Gradio : Agent NER médical + Mapper
|
| 4 |
+
Input transcription → Extraction → Mapping → Rapport
|
| 5 |
+
"""
|
| 6 |
+
import gradio as gr
|
| 7 |
+
from type3_extract_entities import MedicalNERAgent
|
| 8 |
+
from medical_template3_mapper import MedicalTemplateMapper
|
| 9 |
+
from type3_preprocessing import MedicalTranscriptionProcessor, AZURE_OPENAI_DEPLOYMENT
|
| 10 |
+
from post_processing import post_process_medical_report
|
| 11 |
+
def process_transcription(transcription: str):
|
| 12 |
+
try:
|
| 13 |
+
#Étape 1 correction asr
|
| 14 |
+
processor = MedicalTranscriptionProcessor(AZURE_OPENAI_DEPLOYMENT)
|
| 15 |
+
result = processor.process_transcription(transcription)
|
| 16 |
+
corrected_transcription=result.final_corrected_text
|
| 17 |
+
# Étape 1 : Extraction
|
| 18 |
+
|
| 19 |
+
agent = MedicalNERAgent()
|
| 20 |
+
extracted_data = agent.extract_medical_entities(corrected_transcription)
|
| 21 |
+
extraction_report = agent.print_extraction_report(extracted_data)
|
| 22 |
+
|
| 23 |
+
# Étape 2 : Mapping vers template
|
| 24 |
+
mapper = MedicalTemplateMapper()
|
| 25 |
+
mapping_result = mapper.map_extracted_data_to_template(extracted_data)
|
| 26 |
+
#mapping_report = mapper.print_mapping_report(mapping_result)
|
| 27 |
+
mapping_report = mapper.template
|
| 28 |
+
|
| 29 |
+
# Étape 3 : Rapport final rempli
|
| 30 |
+
rapport_final = mapping_result.filled_template
|
| 31 |
+
|
| 32 |
+
#Étape 4: nettoyage du rapport
|
| 33 |
+
cleaned_report = post_process_medical_report(rapport_final)
|
| 34 |
+
|
| 35 |
+
return corrected_transcription,extraction_report, mapping_report, cleaned_report
|
| 36 |
+
except Exception as e:
|
| 37 |
+
return f"Erreur: {e}", "", ""
|
| 38 |
+
|
| 39 |
+
# Interface Gradio
|
| 40 |
+
demo = gr.Interface(
|
| 41 |
+
fn=process_transcription,
|
| 42 |
+
inputs=gr.Textbox(lines=15, label="Transcription médicale"),
|
| 43 |
+
outputs=[
|
| 44 |
+
gr.Textbox(lines=20, label="🔬 Crorrection de la transcription"),
|
| 45 |
+
gr.Textbox(lines=20, label="📋 Extraction structurée"),
|
| 46 |
+
gr.Textbox(lines=20, label="📋 Rapport à remplir (Mapping)"),
|
| 47 |
+
gr.Textbox(lines=20, label="✅ Compte-rendu structuré final"),
|
| 48 |
+
],
|
| 49 |
+
title="🏥 Génération de comptes-rendus structurés",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
demo.launch(share=True)
|
app2.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Interface Gradio : Transcription médicale → Correction → Matching → Rapport
|
| 4 |
+
"""
|
| 5 |
+
import gradio as gr
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# Fix pour le problème de pickle avec TemplateInfo
|
| 10 |
+
import template_db_creation
|
| 11 |
+
sys.modules['__main__'].TemplateInfo = template_db_creation.TemplateInfo
|
| 12 |
+
|
| 13 |
+
from template_db_creation import MedicalTemplateParser
|
| 14 |
+
from smart_match import TranscriptionMatcher
|
| 15 |
+
from correcteur import MedicalTranscriptionProcessor, AZURE_OPENAI_DEPLOYMENT
|
| 16 |
+
|
| 17 |
+
# Chemin hardcodé vers la base de données
|
| 18 |
+
DB_PATH = "/Users/macbook/medical-agent/medical-agent/templates/medical_templates.pkl"
|
| 19 |
+
|
| 20 |
+
# Variables globales
|
| 21 |
+
parser = None
|
| 22 |
+
matcher = None
|
| 23 |
+
|
| 24 |
+
def initialize_system():
|
| 25 |
+
"""Initialise le système au démarrage"""
|
| 26 |
+
global parser, matcher
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
print(f"📂 Chargement de la base de données: {DB_PATH}")
|
| 30 |
+
parser = MedicalTemplateParser()
|
| 31 |
+
parser.load_database(DB_PATH)
|
| 32 |
+
|
| 33 |
+
matcher = TranscriptionMatcher(parser)
|
| 34 |
+
|
| 35 |
+
print(f"✅ Système initialisé avec {len(parser.templates)} templates")
|
| 36 |
+
return True
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"❌ Erreur initialisation: {e}")
|
| 39 |
+
return False
|
| 40 |
+
|
| 41 |
+
def process_transcription(transcription: str):
|
| 42 |
+
"""
|
| 43 |
+
Traite la transcription médicale complète
|
| 44 |
+
|
| 45 |
+
Args:
|
| 46 |
+
transcription: Texte de la transcription
|
| 47 |
+
|
| 48 |
+
Returns:
|
| 49 |
+
Tuple (transcription_corrigée, template_vide, rapport_final)
|
| 50 |
+
"""
|
| 51 |
+
try:
|
| 52 |
+
# Étape 1: Correction ASR
|
| 53 |
+
print("🔧 Étape 1: Correction de la transcription...")
|
| 54 |
+
processor = MedicalTranscriptionProcessor(AZURE_OPENAI_DEPLOYMENT)
|
| 55 |
+
result = processor.process_transcription(transcription)
|
| 56 |
+
corrected_transcription = result.final_corrected_text
|
| 57 |
+
|
| 58 |
+
# Étape 2: Matching et remplissage du template
|
| 59 |
+
print("🔍 Étape 2: Recherche du template approprié...")
|
| 60 |
+
results = matcher.match_and_fill(corrected_transcription, return_top_k=1)
|
| 61 |
+
|
| 62 |
+
if not results:
|
| 63 |
+
return (
|
| 64 |
+
corrected_transcription,
|
| 65 |
+
"❌ Aucun template approprié trouvé",
|
| 66 |
+
"❌ Impossible de générer le rapport"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
best_result = results[0]
|
| 70 |
+
|
| 71 |
+
# Préparer le template vide avec toutes les informations
|
| 72 |
+
template_vide = f"{best_result.template_id}\n"
|
| 73 |
+
template_vide += "=" * len(best_result.template_id) + "\n"
|
| 74 |
+
template_vide += best_result.template_content
|
| 75 |
+
|
| 76 |
+
# Préparer le rapport final rempli avec toutes les sections
|
| 77 |
+
rapport_final = f"{best_result.template_id}\n"
|
| 78 |
+
rapport_final += "=" * len(best_result.template_id) + "\n"
|
| 79 |
+
|
| 80 |
+
# Ajouter toutes les sections remplies
|
| 81 |
+
rapport_final += best_result.filled_template
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
print(f"✅ Traitement terminé - Template: {best_result.template_id}")
|
| 85 |
+
|
| 86 |
+
return corrected_transcription, template_vide, rapport_final
|
| 87 |
+
|
| 88 |
+
except Exception as e:
|
| 89 |
+
error_msg = f"❌ Erreur: {str(e)}"
|
| 90 |
+
print(error_msg)
|
| 91 |
+
return error_msg, "", ""
|
| 92 |
+
|
| 93 |
+
# Initialiser le système au démarrage
|
| 94 |
+
print("🚀 Initialisation du système...")
|
| 95 |
+
if not initialize_system():
|
| 96 |
+
print("⚠️ Erreur lors de l'initialisation - vérifiez le chemin de la DB")
|
| 97 |
+
|
| 98 |
+
# Interface Gradio
|
| 99 |
+
demo = gr.Interface(
|
| 100 |
+
fn=process_transcription,
|
| 101 |
+
inputs=gr.Textbox(
|
| 102 |
+
lines=15,
|
| 103 |
+
label="📝 Transcription médicale",
|
| 104 |
+
placeholder="Collez ici la transcription de l'examen médical..."
|
| 105 |
+
),
|
| 106 |
+
outputs=[
|
| 107 |
+
gr.Textbox(lines=20, label="✅ Transcription corrigée", show_copy_button=True),
|
| 108 |
+
gr.Textbox(lines=20, label="📋 Rapport à remplir (Template)", show_copy_button=True),
|
| 109 |
+
gr.Textbox(lines=20, label="📄 Compte-rendu structuré final", show_copy_button=True),
|
| 110 |
+
],
|
| 111 |
+
title="🏥 Génération de comptes-rendus structurés",
|
| 112 |
+
|
| 113 |
+
)
|
| 114 |
+
|
| 115 |
+
if __name__ == "__main__":
|
| 116 |
+
demo.launch(share=True)
|
correcteur.py
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import spacy
|
| 2 |
+
import openai
|
| 3 |
+
import re
|
| 4 |
+
from typing import Dict, List, Tuple
|
| 5 |
+
import json
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from transformers import AutoTokenizer, AutoModelForTokenClassification, pipeline
|
| 9 |
+
import os
|
| 10 |
+
from dotenv import load_dotenv
|
| 11 |
+
from openai import AzureOpenAI
|
| 12 |
+
from medkit.core.text import TextDocument
|
| 13 |
+
from medkit.text.ner.hf_entity_matcher import HFEntityMatcher
|
| 14 |
+
|
| 15 |
+
NER_MODEL = os.getenv("NER_MODEL", "medkit/DrBERT-CASM2")
|
| 16 |
+
|
| 17 |
+
# Charger les variables d'environnement depuis .env
|
| 18 |
+
load_dotenv()
|
| 19 |
+
|
| 20 |
+
# Récupération des paramètres
|
| 21 |
+
AZURE_OPENAI_KEY = os.getenv("AZURE_OPENAI_KEY")
|
| 22 |
+
AZURE_OPENAI_ENDPOINT = os.getenv("AZURE_OPENAI_ENDPOINT")
|
| 23 |
+
AZURE_OPENAI_DEPLOYMENT = os.getenv("AZURE_OPENAI_DEPLOYMENT")
|
| 24 |
+
AZURE_OPENAI_API_VERSION = os.getenv("AZURE_OPENAI_API_VERSION", "2024-05-01-preview")
|
| 25 |
+
|
| 26 |
+
# Validation des variables d'environnement
|
| 27 |
+
def validate_azure_config():
|
| 28 |
+
"""Valide que toutes les variables Azure sont configurées"""
|
| 29 |
+
missing_vars = []
|
| 30 |
+
if not AZURE_OPENAI_KEY:
|
| 31 |
+
missing_vars.append("AZURE_OPENAI_KEY")
|
| 32 |
+
if not AZURE_OPENAI_ENDPOINT:
|
| 33 |
+
missing_vars.append("AZURE_OPENAI_ENDPOINT")
|
| 34 |
+
if not AZURE_OPENAI_DEPLOYMENT:
|
| 35 |
+
missing_vars.append("AZURE_OPENAI_DEPLOYMENT")
|
| 36 |
+
|
| 37 |
+
if missing_vars:
|
| 38 |
+
print(f"❌ Variables d'environnement manquantes: {', '.join(missing_vars)}")
|
| 39 |
+
print("📝 Veuillez créer un fichier .env avec:")
|
| 40 |
+
for var in missing_vars:
|
| 41 |
+
print(f" {var}=votre_valeur")
|
| 42 |
+
return False
|
| 43 |
+
return True
|
| 44 |
+
|
| 45 |
+
# Client Azure OpenAI avec validation
|
| 46 |
+
azure_client = None
|
| 47 |
+
if validate_azure_config():
|
| 48 |
+
try:
|
| 49 |
+
azure_client = AzureOpenAI(
|
| 50 |
+
api_key=AZURE_OPENAI_KEY,
|
| 51 |
+
api_version=AZURE_OPENAI_API_VERSION,
|
| 52 |
+
azure_endpoint=AZURE_OPENAI_ENDPOINT,
|
| 53 |
+
)
|
| 54 |
+
print("✅ Client Azure OpenAI initialisé avec succès")
|
| 55 |
+
except Exception as e:
|
| 56 |
+
print(f"❌ Erreur lors de l'initialisation du client Azure OpenAI: {e}")
|
| 57 |
+
azure_client = None
|
| 58 |
+
|
| 59 |
+
ner_matcher = HFEntityMatcher(model=NER_MODEL)
|
| 60 |
+
|
| 61 |
+
@dataclass
|
| 62 |
+
class CorrectionResult:
|
| 63 |
+
original_text: str
|
| 64 |
+
ner_corrected_text: str
|
| 65 |
+
final_corrected_text: str
|
| 66 |
+
medical_entities: List[Dict]
|
| 67 |
+
confidence_score: float
|
| 68 |
+
|
| 69 |
+
class MedicalNERCorrector:
|
| 70 |
+
"""Correcteur orthographique basé sur un NER médical français"""
|
| 71 |
+
|
| 72 |
+
def __init__(self):
|
| 73 |
+
try:
|
| 74 |
+
# Charger le modèle MedKit NER
|
| 75 |
+
self.matcher = HFEntityMatcher(model=NER_MODEL)
|
| 76 |
+
print(f"✅ Modèle NER '{NER_MODEL}' chargé avec succès")
|
| 77 |
+
except Exception as e:
|
| 78 |
+
print(f"❌ Erreur lors du chargement du modèle NER {NER_MODEL}: {e}")
|
| 79 |
+
self.matcher = None
|
| 80 |
+
|
| 81 |
+
# Dictionnaire complet pour convertir tous les nombres en lettres vers chiffres
|
| 82 |
+
self.number_corrections = {
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
# Variantes courantes dans les transcriptions vocales
|
| 86 |
+
"1": "1", "1er": "1", "première": "1", "premier": "1",
|
| 87 |
+
"2ème": "2", "deuxième": "2", "second": "2", "seconde": "2",
|
| 88 |
+
"3ème": "3", "troisième": "3", "4ème": "4", "quatrième": "4",
|
| 89 |
+
"5ème": "5", "cinquième": "5", "6ème": "6", "sixième": "6",
|
| 90 |
+
"7ème": "7", "septième": "7", "8ème": "8", "huitième": "8",
|
| 91 |
+
"9ème": "9", "neuvième": "9", "10ème": "10", "dixième": "10",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
# Dictionnaire de corrections pour transcriptions vocales - ORDRE IMPORTANT
|
| 95 |
+
self.vocal_corrections = {
|
| 96 |
+
# Corrections de ponctuation - doivent être traitées en premier
|
| 97 |
+
"point à la ligne": ".\n",
|
| 98 |
+
"retour à la ligne": "\n",
|
| 99 |
+
"à la ligne": "\n",
|
| 100 |
+
"nouvelle ligne": "\n",
|
| 101 |
+
"saut de ligne": "\n",
|
| 102 |
+
"point virgule": ";",
|
| 103 |
+
"deux points": ":",
|
| 104 |
+
"point d'interrogation": "?",
|
| 105 |
+
"point d'exclamation": "!",
|
| 106 |
+
"virgule": ",",
|
| 107 |
+
"point": ".", # Doit être traité en dernier pour éviter les conflits
|
| 108 |
+
|
| 109 |
+
# Corrections des séquences IRM
|
| 110 |
+
"T un": "T1", "T deux": "T2", "T trois": "T3",
|
| 111 |
+
"t un": "T1", "t deux": "T2", "t trois": "T3",
|
| 112 |
+
"séquence T un": "séquence T1", "séquence T deux": "séquence T2",
|
| 113 |
+
|
| 114 |
+
# Corrections des niveaux vertébraux - cervicaux
|
| 115 |
+
"C un": "C1", "C deux": "C2", "C trois": "C3", "C quatre": "C4",
|
| 116 |
+
"C cinq": "C5", "C six": "C6", "C sept": "C7",
|
| 117 |
+
"c un": "C1", "c deux": "C2", "c trois": "C3", "c quatre": "C4",
|
| 118 |
+
"c cinq": "C5", "c six": "C6", "c sept": "C7",
|
| 119 |
+
|
| 120 |
+
# Niveaux thoraciques
|
| 121 |
+
"T un": "T1", "T deux": "T2", "T trois": "T3", "T quatre": "T4",
|
| 122 |
+
"T cinq": "T5", "T six": "T6", "T sept": "T7", "T huit": "T8",
|
| 123 |
+
"T neuf": "T9", "T dix": "T10", "T onze": "T11", "T douze": "T12",
|
| 124 |
+
|
| 125 |
+
# Niveaux lombaires
|
| 126 |
+
"L un": "L1", "L deux": "L2", "L trois": "L3", "L quatre": "L4", "L cinq": "L5",
|
| 127 |
+
"l un": "L1", "l deux": "L2", "l trois": "L3", "l quatre": "L4", "l cinq": "L5",
|
| 128 |
+
|
| 129 |
+
# Niveaux sacrés
|
| 130 |
+
"S un": "S1", "S deux": "S2", "S trois": "S3", "S quatre": "S4", "S cinq": "S5",
|
| 131 |
+
"s un": "S1", "s deux": "S2", "s trois": "S3", "s quatre": "S4", "s cinq": "S5",
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
# Dictionnaire de corrections médicales spécialisées - orthographe
|
| 135 |
+
self.medical_corrections = {
|
| 136 |
+
# Anatomie
|
| 137 |
+
"rachis": ["rachis", "rachi", "rachys", "rahis", "raxis"],
|
| 138 |
+
"cervical": ["cervical", "cervicale", "cervicaux", "servical", "servicale"],
|
| 139 |
+
"vertébraux": ["vertébraux", "vertebraux", "vertébrau", "vertébral", "vertebral"],
|
| 140 |
+
"médullaire": ["médullaire", "medullaire", "medulaire", "médulaire"],
|
| 141 |
+
"foraminal": ["foraminal", "foraminale", "foraminaux", "forraminal"],
|
| 142 |
+
"postérolatéral": ["postérolatéral", "posterolatéral", "postero-latéral", "postero latéral"],
|
| 143 |
+
"antérolatéral": ["antérolatéral", "anterolatéral", "antero-latéral", "antero latéral"],
|
| 144 |
+
"longitudinal": ["longitudinal", "longitudinale", "longitudinaux"],
|
| 145 |
+
|
| 146 |
+
# Pathologies
|
| 147 |
+
"uncarthrose": ["uncarthrose", "uncoarthrose", "uncartrose", "unkarthrose"],
|
| 148 |
+
"lordose": ["lordose", "lordoze", "lordosse", "lordosse"],
|
| 149 |
+
"cyphose": ["cyphose", "siphose", "kyphose", "kiphose"],
|
| 150 |
+
"scoliose": ["scoliose", "skoliose", "scholiose"],
|
| 151 |
+
"discopathie": ["discopathie", "disccopathie", "discopatie"],
|
| 152 |
+
"discal": ["discal", "discale", "diskal", "diskale", "disque"],
|
| 153 |
+
"hernie": ["hernie", "herny", "herni"],
|
| 154 |
+
"protrusion": ["protrusion", "protusion", "protruzion"],
|
| 155 |
+
"sténose": ["sténose", "stenose", "sténoze"],
|
| 156 |
+
"arthrose": ["arthrose", "artrose", "arthroze"],
|
| 157 |
+
"ostéophyte": ["ostéophyte", "osteophyte", "ostéofite"],
|
| 158 |
+
"ligamentaire": ["ligamentaire", "ligamentere", "ligamentair"],
|
| 159 |
+
|
| 160 |
+
# Techniques et examens
|
| 161 |
+
"sagittal": ["sagittal", "sagittale", "sagital", "sagittaux"],
|
| 162 |
+
"coronal": ["coronal", "coronale", "coronaux"],
|
| 163 |
+
"axial": ["axial", "axiale", "axiaux", "axial"],
|
| 164 |
+
"transversal": ["transversal", "transversale", "transversaux"],
|
| 165 |
+
"pondéré": ["pondéré", "pondéré", "pondere", "pondérée"],
|
| 166 |
+
"séquence": ["séquence", "sequence", "sekence"],
|
| 167 |
+
"contraste": ["contraste", "contraste", "kontraste"],
|
| 168 |
+
"gadolinium": ["gadolinium", "gadoliniun", "gadoliniom"],
|
| 169 |
+
|
| 170 |
+
# Mesures et directions
|
| 171 |
+
"millimètre": ["millimètre", "millimetre", "mm"],
|
| 172 |
+
"centimètre": ["centimètre", "centimetre", "cm"],
|
| 173 |
+
"gauche": ["gauche", "gosh", "goshe", "goche"],
|
| 174 |
+
"droite": ["droite", "droitte", "droithe", "droitr"],
|
| 175 |
+
"antérieur": ["antérieur", "anterieur", "antérieure", "anterieure"],
|
| 176 |
+
"postérieur": ["postérieur", "posterieur", "postérieure", "posterieure"],
|
| 177 |
+
"supérieur": ["supérieur", "superieur", "supérieure", "superieure"],
|
| 178 |
+
"inférieur": ["inférieur", "inferieur", "inférieure", "inferieure"],
|
| 179 |
+
"médian": ["médian", "median", "mediane", "médiane"],
|
| 180 |
+
"latéral": ["latéral", "lateral", "laterale", "latérale"],
|
| 181 |
+
|
| 182 |
+
# Signaux et aspects
|
| 183 |
+
"signal": ["signal", "signale", "signa", "signaux"],
|
| 184 |
+
"hypersignal": ["hypersignal", "hyper signal", "hypersignale"],
|
| 185 |
+
"hyposignal": ["hyposignal", "hypo signal", "hyposignale"],
|
| 186 |
+
"isosignal": ["isosignal", "iso signal", "isosignale"],
|
| 187 |
+
"hétérogène": ["hétérogène", "heterogene", "hétérogène"],
|
| 188 |
+
"homogène": ["homogène", "homogene", "omogene"],
|
| 189 |
+
|
| 190 |
+
# Autres termes fréquents
|
| 191 |
+
"dimension": ["dimension", "dimention", "dimmension"],
|
| 192 |
+
"normale": ["normale", "normal", "normalle"],
|
| 193 |
+
"anomalie": ["anomalie", "annomalie", "anomaly"],
|
| 194 |
+
"décelable": ["décelable", "decelabl", "décellabl"],
|
| 195 |
+
"absence": ["absence", "abscence", "absance"],
|
| 196 |
+
"présence": ["présence", "presence", "presance"],
|
| 197 |
+
"contact": ["contact", "contacte", "kontak"],
|
| 198 |
+
"compression": ["compression", "compresion", "kompression"],
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
# Expressions régulières pour les patterns médicaux
|
| 202 |
+
self.medical_patterns = {
|
| 203 |
+
"vertebral_level": r"[CTLS]\d+[\s-]*[CTLS]\d+",
|
| 204 |
+
"measurement": r"\d+[\s]*[x×]\s*\d+\s*mm",
|
| 205 |
+
"technique": r"T[1-3]",
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
def convert_numbers_to_digits(self, text: str) -> str:
|
| 209 |
+
"""Convertit TOUS les nombres en lettres vers des chiffres"""
|
| 210 |
+
corrected_text = text
|
| 211 |
+
|
| 212 |
+
# ÉTAPE 1: Gestion spéciale des mesures médicales communes
|
| 213 |
+
medical_measures = {
|
| 214 |
+
# Mesures d'utérus courantes
|
| 215 |
+
"sept point huit": "7,8",
|
| 216 |
+
"trois sept": "3,7",
|
| 217 |
+
"soixante douze": "72",
|
| 218 |
+
"soixante treize": "73",
|
| 219 |
+
"soixante quatorze": "74",
|
| 220 |
+
"soixante quinze": "75",
|
| 221 |
+
"soixante seize": "76",
|
| 222 |
+
"soixante dix sept": "77",
|
| 223 |
+
"soixante dix huit": "78",
|
| 224 |
+
"soixante dix neuf": "79",
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
# Mesures d'ovaires courantes
|
| 228 |
+
"vingt six": "26",
|
| 229 |
+
|
| 230 |
+
"vingt cinq": "25",
|
| 231 |
+
"dix neuf": "19",
|
| 232 |
+
"vingt deux": "22",
|
| 233 |
+
|
| 234 |
+
# Mesures Doppler
|
| 235 |
+
"trois vingt quatre": "3,24", # IP
|
| 236 |
+
"quatre vingt onze": "0,91", # IR (avec virgule décimale)
|
| 237 |
+
|
| 238 |
+
# Autres mesures courantes
|
| 239 |
+
"quinze": "15",
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
# Application des mesures médicales en premier
|
| 243 |
+
for word_measure, digit_measure in medical_measures.items():
|
| 244 |
+
pattern = r'\b' + re.escape(word_measure) + r'\b'
|
| 245 |
+
corrected_text = re.sub(pattern, digit_measure, corrected_text, flags=re.IGNORECASE)
|
| 246 |
+
|
| 247 |
+
# ÉTAPE 2: Traitement des nombres composés restants
|
| 248 |
+
compound_without_dash = {
|
| 249 |
+
"vingt un": "21", "vingt deux": "22", "vingt trois": "23", "vingt quatre": "24",
|
| 250 |
+
"vingt cinq": "25", "vingt six": "26", "vingt sept": "27", "vingt huit": "28",
|
| 251 |
+
"vingt neuf": "29", "trente un": "31", "trente deux": "32", "trente trois": "33",
|
| 252 |
+
"trente quatre": "34", "trente cinq": "35", "trente six": "36", "trente sept": "37",
|
| 253 |
+
"trente huit": "38", "trente neuf": "39", "quarante un": "41", "quarante deux": "42",
|
| 254 |
+
"quarante trois": "43", "quarante quatre": "44", "quarante cinq": "45",
|
| 255 |
+
"quarante six": "46", "quarante sept": "47", "quarante huit": "48", "quarante neuf": "49",
|
| 256 |
+
"cinquante un": "51", "cinquante deux": "52", "cinquante trois": "53",
|
| 257 |
+
"cinquante quatre": "54", "cinquante cinq": "55", "cinquante six": "56",
|
| 258 |
+
"cinquante sept": "57", "cinquante huit": "58", "cinquante neuf": "59",
|
| 259 |
+
"soixante un": "61", "soixante deux": "62", "soixante trois": "63",
|
| 260 |
+
"soixante quatre": "64", "soixante cinq": "65", "soixante six": "66",
|
| 261 |
+
"soixante sept": "67", "soixante huit": "68", "soixante neuf": "69",
|
| 262 |
+
"soixante et onze": "71", "soixante douze": "72", "soixante treize": "73",
|
| 263 |
+
"soixante quatorze": "74", "soixante quinze": "75", "soixante seize": "76",
|
| 264 |
+
"soixante dix sept": "77", "soixante dix huit": "78", "soixante dix neuf": "79",
|
| 265 |
+
"quatre vingt un": "81", "quatre vingt deux": "82", "quatre vingt trois": "83",
|
| 266 |
+
"quatre vingt quatre": "84", "quatre vingt cinq": "85", "quatre vingt six": "86",
|
| 267 |
+
"quatre vingt sept": "87", "quatre vingt huit": "88", "quatre vingt neuf": "89",
|
| 268 |
+
"quatre vingt onze": "91", "quatre vingt douze": "92", "quatre vingt treize": "93",
|
| 269 |
+
"quatre vingt quatorze": "94", "quatre vingt quinze": "95", "quatre vingt seize": "96",
|
| 270 |
+
"quatre vingt dix sept": "97", "quatre vingt dix huit": "98", "quatre vingt dix neuf": "99",
|
| 271 |
+
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
for word, digit in compound_without_dash.items():
|
| 275 |
+
# Protection : ne remplace PAS si suivi de "fois" ET d'un autre nombre
|
| 276 |
+
pattern = r'\b' + re.escape(word) + r'\b(?!\s+fois\s+\w+)'
|
| 277 |
+
corrected_text = re.sub(pattern, digit, corrected_text, flags=re.IGNORECASE)
|
| 278 |
+
|
| 279 |
+
# ÉTAPE 3: Nombres simples (ordre modifié pour éviter les conflits)
|
| 280 |
+
simple_numbers = {
|
| 281 |
+
"zéro": "0", "deux": "2", "trois": "3", "quatre": "4",
|
| 282 |
+
"cinq": "5", "six": "6", "sept": "7", "huit": "8", "neuf": "9",
|
| 283 |
+
"dix": "10", "onze": "11", "douze": "12", "treize": "13", "quatorze": "14",
|
| 284 |
+
"quinze": "15", "seize": "16", "dix-sept": "17", "dix-huit": "18",
|
| 285 |
+
"dix-neuf": "19", "vingt": "20", "trente": "30", "quarante": "40",
|
| 286 |
+
"cinquante": "50", "soixante-dix": "70",
|
| 287 |
+
"quatre-vingts": "80", "quatre-vingt": "80", "quatre-vingt-dix": "90",
|
| 288 |
+
"cent": "100", "mille": "1000",
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
# Conversion des nombres simples
|
| 292 |
+
for word_number, digit in simple_numbers.items():
|
| 293 |
+
pattern = r'\b' + re.escape(word_number) + r'\b'
|
| 294 |
+
corrected_text = re.sub(pattern, digit, corrected_text, flags=re.IGNORECASE)
|
| 295 |
+
|
| 296 |
+
corrected_text = re.sub(r'\bpour\s+cent\b', '%', corrected_text, flags=re.IGNORECASE)
|
| 297 |
+
|
| 298 |
+
return corrected_text
|
| 299 |
+
|
| 300 |
+
def extract_medical_entities(self, text: str):
|
| 301 |
+
"""Extrait les entités médicales avec MedKit HFEntityMatcher"""
|
| 302 |
+
if not self.matcher:
|
| 303 |
+
return []
|
| 304 |
+
doc = TextDocument(text)
|
| 305 |
+
entities = self.matcher.run([doc.raw_segment])
|
| 306 |
+
# Transformer en format simple
|
| 307 |
+
formatted_entities = []
|
| 308 |
+
for ent in entities:
|
| 309 |
+
formatted_entities.append({
|
| 310 |
+
"text": ent.text,
|
| 311 |
+
"label": ent.label,
|
| 312 |
+
})
|
| 313 |
+
return formatted_entities
|
| 314 |
+
|
| 315 |
+
def correct_vocal_transcription(self, text: str) -> str:
|
| 316 |
+
"""Corrige les transcriptions vocales avec un ordre de priorité strict"""
|
| 317 |
+
corrected_text = text
|
| 318 |
+
|
| 319 |
+
# ÉTAPE 1: Conversion des nombres AVANT tout le reste
|
| 320 |
+
corrected_text = self.convert_numbers_to_digits(corrected_text)
|
| 321 |
+
|
| 322 |
+
# ÉTAPE 2: Corrections des expressions vocales dans l'ordre de priorité
|
| 323 |
+
# L'ordre est CRUCIAL pour éviter les conflits
|
| 324 |
+
priority_corrections = [
|
| 325 |
+
# Expressions de ponctuation complexes en premier
|
| 326 |
+
("point à la ligne", ".\n"),
|
| 327 |
+
("retour à la ligne", "\n"),
|
| 328 |
+
("à la ligne", "\n"),
|
| 329 |
+
("nouvelle ligne", "\n"),
|
| 330 |
+
("saut de ligne", "\n"),
|
| 331 |
+
("point virgule", ";"),
|
| 332 |
+
("deux points", ":"),
|
| 333 |
+
("point d'interrogation", "?"),
|
| 334 |
+
("point d'exclamation", "!"),
|
| 335 |
+
|
| 336 |
+
# Niveaux vertébraux avec nombres
|
| 337 |
+
("C 1", "C1"), ("C 2", "C2"), ("C 3", "C3"), ("C 4", "C4"),
|
| 338 |
+
("C 5", "C5"), ("C 6", "C6"), ("C 7", "C7"),
|
| 339 |
+
("L 1", "L1"), ("L 2", "L2"), ("L 3", "L3"), ("L 4", "L4"), ("L 5", "L5"),
|
| 340 |
+
("T 1", "T1"), ("T 2", "T2"), ("T 3", "T3"), ("T 4", "T4"),
|
| 341 |
+
("T 5", "T5"), ("T 6", "T6"), ("T 7", "T7"), ("T 8", "T8"),
|
| 342 |
+
("T 9", "T9"), ("T 10", "T10"), ("T 11", "T11"), ("T 12", "T12"),
|
| 343 |
+
|
| 344 |
+
# Séquences IRM
|
| 345 |
+
("séquence T 1", "séquence T1"), ("séquence T 2", "séquence T2"),
|
| 346 |
+
|
| 347 |
+
# Virgule et point en dernier pour éviter les conflits
|
| 348 |
+
("virgule", ","),
|
| 349 |
+
]
|
| 350 |
+
|
| 351 |
+
for vocal_term, replacement in priority_corrections:
|
| 352 |
+
# Utilisation de word boundaries pour éviter les remplacements partiels
|
| 353 |
+
pattern = r'\b' + re.escape(vocal_term) + r'\b'
|
| 354 |
+
corrected_text = re.sub(pattern, replacement, corrected_text, flags=re.IGNORECASE)
|
| 355 |
+
|
| 356 |
+
# ÉTAPE 3: Correction spéciale pour "point" - seulement si c'est vraiment une fin de phrase
|
| 357 |
+
# Pattern pour détecter "point" suivi d'un espace et d'une majuscule OU en fin de texte
|
| 358 |
+
corrected_text = re.sub(r'\bpoint(?!\s+(?:à|d\'|virgule))', '.', corrected_text, flags=re.IGNORECASE)
|
| 359 |
+
|
| 360 |
+
return corrected_text
|
| 361 |
+
|
| 362 |
+
def correct_medical_terms(self, text: str) -> str:
|
| 363 |
+
"""Corrige les termes médicaux basés sur le dictionnaire"""
|
| 364 |
+
corrected_text = text
|
| 365 |
+
|
| 366 |
+
for correct_term, variations in self.medical_corrections.items():
|
| 367 |
+
for variation in variations:
|
| 368 |
+
if variation != correct_term: # Éviter de remplacer par lui-même
|
| 369 |
+
# Correction avec préservation de la casse du premier caractère
|
| 370 |
+
pattern = r'\b' + re.escape(variation) + r'\b'
|
| 371 |
+
|
| 372 |
+
def replace_with_case(match):
|
| 373 |
+
matched_text = match.group(0)
|
| 374 |
+
if matched_text[0].isupper():
|
| 375 |
+
return correct_term.capitalize()
|
| 376 |
+
return correct_term
|
| 377 |
+
|
| 378 |
+
corrected_text = re.sub(pattern, replace_with_case, corrected_text, flags=re.IGNORECASE)
|
| 379 |
+
|
| 380 |
+
return corrected_text
|
| 381 |
+
|
| 382 |
+
def normalize_medical_patterns(self, text: str) -> str:
|
| 383 |
+
"""Normalise les patterns médicaux avec gestion des mesures"""
|
| 384 |
+
normalized_text = text
|
| 385 |
+
|
| 386 |
+
# Gestion spéciale des mesures avec "fois" (dimensions)
|
| 387 |
+
# Pattern: nombre fois nombre -> nombre x nombre
|
| 388 |
+
normalized_text = re.sub(r'(\d+(?:[.,]\d+)?)\s+fois\s+(\d+(?:[.,]\d+)?)', r'\1 x \2', normalized_text, flags=re.IGNORECASE)
|
| 389 |
+
|
| 390 |
+
# Normalisation des niveaux vertébraux (ex: C5 C6 -> C5-C6, C5c6 -> C5-C6)
|
| 391 |
+
normalized_text = re.sub(r'([CTLS])(\d)\s*([CTLS])?(\d)', lambda m: f"{m.group(1)}{m.group(2)}-{m.group(1)}{m.group(4)}", normalized_text, flags=re.IGNORECASE)
|
| 392 |
+
|
| 393 |
+
# Normalisation des mesures existantes (ex: 72x40mm -> 72 x 40 mm)
|
| 394 |
+
normalized_text = re.sub(r'(\d+(?:[.,]\d+)?)\s*[x×]\s*(\d+(?:[.,]\d+)?)\s*mm', r'\1 x \2 mm', normalized_text)
|
| 395 |
+
|
| 396 |
+
# Ajout automatique de l'unité mm pour les mesures sans unité (nombre x nombre -> nombre x nombre mm)
|
| 397 |
+
normalized_text = re.sub(r'(\d+(?:[.,]\d+)?)\s*x\s*(\d+(?:[.,]\d+)?)(?!\s*(?:mm|cm))', r'\1 x \2 mm', normalized_text, flags=re.IGNORECASE)
|
| 398 |
+
|
| 399 |
+
# Normalisation des millimètres écrits en toutes lettres
|
| 400 |
+
normalized_text = re.sub(r'(\d+(?:[.,]\d+)?)\s*millimètres?', r'\1 mm', normalized_text, flags=re.IGNORECASE)
|
| 401 |
+
|
| 402 |
+
# Gestion des mesures d'hystérométrie (format spécial)
|
| 403 |
+
normalized_text = re.sub(r"d['’]?hystérométrie\s+(\d+(?:[.,]\d+)?)", r"d'hystérométrie : \1 mm", normalized_text, flags=re.IGNORECASE)
|
| 404 |
+
|
| 405 |
+
# Gestion des mesures d'endomètre
|
| 406 |
+
normalized_text = re.sub(r"d['’]?endomètre\s+(\d+(?:[.,]\d+)?)", r"d'endometre : \1 mm", normalized_text, flags=re.IGNORECASE)
|
| 407 |
+
|
| 408 |
+
# Gestion du CFA (Compte Folliculaire Antral)
|
| 409 |
+
normalized_text = re.sub(r'(\d+)\s+follicules', r'CFA \1 follicules', normalized_text, flags=re.IGNORECASE)
|
| 410 |
+
|
| 411 |
+
normalized_text = re.sub(r'\bmm\s+millimètres?\b', 'mm', normalized_text, flags=re.IGNORECASE)
|
| 412 |
+
normalized_text = re.sub(r'\bmillimètres?\s+mm\b', 'mm', normalized_text, flags=re.IGNORECASE)
|
| 413 |
+
|
| 414 |
+
return normalized_text
|
| 415 |
+
|
| 416 |
+
def clean_spacing_and_formatting(self, text: str) -> str:
|
| 417 |
+
"""Nettoie les espaces et améliore le formatage avec ajouts spécifiques"""
|
| 418 |
+
# Supprime les espaces multiples mais préserve les sauts de ligne
|
| 419 |
+
text = re.sub(r'[ \t]+', ' ', text)
|
| 420 |
+
|
| 421 |
+
# AJOUT: Corrections spécifiques pour les mesures
|
| 422 |
+
# Corrige "7. 8" -> "7,8" (décimales)
|
| 423 |
+
text = re.sub(r'(\d+)\.\s+(\d+)(?!\s*(?:mm|cm|fois|x))', r'\1,\2', text)
|
| 424 |
+
|
| 425 |
+
# Corrige "20 6" -> "26" quand c'est clairement un nombre
|
| 426 |
+
text = re.sub(r'\b20\s+6\b', '26', text)
|
| 427 |
+
text = re.sub(r'\b20\s+5\b', '25', text)
|
| 428 |
+
text = re.sub(r'\b10\s+9\b', '19', text)
|
| 429 |
+
text = re.sub(r'\b20\s+2\b', '22', text)
|
| 430 |
+
text = re.sub(r'\b20\s+7\b', '27', text)
|
| 431 |
+
text = re.sub(r'\b3\s+20\s+4\b', '3,24', text)
|
| 432 |
+
text = re.sub(r'\b4\s+20\s+11\b', '0,91', text)
|
| 433 |
+
|
| 434 |
+
# Corrige la ponctuation (supprime l'espace avant les points, virgules)
|
| 435 |
+
text = re.sub(r'\s+([.,:;!?])', r'\1', text)
|
| 436 |
+
|
| 437 |
+
# Ajoute un espace après la ponctuation si nécessaire (sauf si suivi d'un saut de ligne)
|
| 438 |
+
text = re.sub(r'([.,:;!?])([A-Za-z])', r'\1 \2', text)
|
| 439 |
+
|
| 440 |
+
# AJOUT: Correction des apostrophes
|
| 441 |
+
text = re.sub(r'\bl\s+([aeiouAEIOU])', r"l'\1", text) # l ovaire -> l'ovaire
|
| 442 |
+
text = re.sub(r'\bd\s+([aeiouAEIOU])', r"d'\1", text) # d hystérométrie -> d'hystérométrie
|
| 443 |
+
|
| 444 |
+
# Nettoie les sauts de ligne multiples (max 2 consécutifs)
|
| 445 |
+
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
|
| 446 |
+
|
| 447 |
+
# Supprime les espaces en début et fin de ligne
|
| 448 |
+
lines = text.split('\n')
|
| 449 |
+
lines = [line.strip() for line in lines]
|
| 450 |
+
text = '\n'.join(lines)
|
| 451 |
+
|
| 452 |
+
# Capitalise après les points suivis d'un espace
|
| 453 |
+
text = re.sub(r'(\.\s+)([a-z])', lambda m: m.group(1) + m.group(2).upper(), text)
|
| 454 |
+
|
| 455 |
+
# Capitalise le début du texte
|
| 456 |
+
if text and text[0].islower():
|
| 457 |
+
text = text[0].upper() + text[1:]
|
| 458 |
+
|
| 459 |
+
return text.strip()
|
| 460 |
+
def post_process_gynecology_report(self, text: str) -> str:
|
| 461 |
+
"""Post-traitement spécialisé pour les rapports gynécologiques"""
|
| 462 |
+
processed_text = text
|
| 463 |
+
|
| 464 |
+
# Structuration des mesures d'utérus
|
| 465 |
+
processed_text = re.sub(
|
| 466 |
+
r'utérus est (\w+)\s+(\d+,\d+)',
|
| 467 |
+
r'utérus est \1 de taille \2 cm',
|
| 468 |
+
processed_text,
|
| 469 |
+
flags=re.IGNORECASE
|
| 470 |
+
)
|
| 471 |
+
|
| 472 |
+
# Structuration des mesures d'ovaires
|
| 473 |
+
processed_text = re.sub(
|
| 474 |
+
r'ovaire (droit|gauche) (\d+ x \d+ mm)',
|
| 475 |
+
r'ovaire \1 mesure \2,',
|
| 476 |
+
processed_text,
|
| 477 |
+
flags=re.IGNORECASE
|
| 478 |
+
)
|
| 479 |
+
|
| 480 |
+
# Amélioration de la lisibilité du CFA
|
| 481 |
+
processed_text = re.sub(
|
| 482 |
+
r'CFA (\d+) follicules',
|
| 483 |
+
r'CFA : \1 follicules',
|
| 484 |
+
processed_text,
|
| 485 |
+
flags=re.IGNORECASE
|
| 486 |
+
)
|
| 487 |
+
|
| 488 |
+
# Formatage des indices Doppler
|
| 489 |
+
processed_text = re.sub(
|
| 490 |
+
r'doppler.*?(\d,\d+).*?(\d,\d+)',
|
| 491 |
+
r'Doppler : IP \1 - IR \2',
|
| 492 |
+
processed_text,
|
| 493 |
+
flags=re.IGNORECASE
|
| 494 |
+
)
|
| 495 |
+
|
| 496 |
+
return processed_text
|
| 497 |
+
|
| 498 |
+
class GPTMedicalFormatter:
|
| 499 |
+
"""Formateur de rapports médicaux utilisant GPT"""
|
| 500 |
+
|
| 501 |
+
def __init__(self, model: str = AZURE_OPENAI_DEPLOYMENT):
|
| 502 |
+
self.model = model
|
| 503 |
+
|
| 504 |
+
self.system_prompt = """
|
| 505 |
+
Tu es un expert en transcription médicale française. Tu dois corriger et formater UNIQUEMENT les erreurs évidentes dans ce texte médical déjà pré-traité.
|
| 506 |
+
|
| 507 |
+
RÈGLES STRICTES À APPLIQUER :
|
| 508 |
+
|
| 509 |
+
1. **PONCTUATION** :
|
| 510 |
+
- Supprime les doubles ponctuations : ",." → "."
|
| 511 |
+
- Supprime ".." → "."
|
| 512 |
+
- Corrige ",?" → "?"
|
| 513 |
+
|
| 514 |
+
2. **PARENTHÈSES** déjà converties mais nettoie si nécessaire
|
| 515 |
+
|
| 516 |
+
3. **ORTHOGRAPHE MÉDICALE** :
|
| 517 |
+
- "supérieur" au lieu de "supérieure" pour les adjectifs masculins
|
| 518 |
+
- "Discrète" → "Discret" pour les termes masculins
|
| 519 |
+
- Autres termes médicaux mal orthographiés
|
| 520 |
+
|
| 521 |
+
4. **FORMATAGE** :
|
| 522 |
+
- Assure-toi que chaque phrase se termine par un point
|
| 523 |
+
- Capitalise après les points
|
| 524 |
+
- Supprime les espaces inutiles
|
| 525 |
+
|
| 526 |
+
5. **CORRECTIONS SPÉCIFIQUES** :
|
| 527 |
+
- Ne transforme JAMAIS "un" en "1" (garde "un utérus" et NON "1 utérus")
|
| 528 |
+
- Supprime les duplications d'unités (ex: "mm millimètre" → "mm")
|
| 529 |
+
- Assure-toi que "pour cent" est remplacé par "%"
|
| 530 |
+
- Vérifie l'accord des adjectifs (masculin/féminin)
|
| 531 |
+
- Corrige uniquement l’orthographe, la grammaire et les accords.
|
| 532 |
+
|
| 533 |
+
6. **INTERDICTIONS** :
|
| 534 |
+
- NE change PAS le contenu médical
|
| 535 |
+
- NE reformule PAS les phrases
|
| 536 |
+
- NE change PAS l'ordre des informations
|
| 537 |
+
- NE supprime PAS d'informations médicales
|
| 538 |
+
|
| 539 |
+
OBJECTIF : Rendre le texte médical propre et professionnel en gardant EXACTEMENT le même contenu.
|
| 540 |
+
|
| 541 |
+
Texte à corriger :
|
| 542 |
+
"""
|
| 543 |
+
|
| 544 |
+
def format_medical_report(self, text: str) -> str:
|
| 545 |
+
"""Formate le rapport médical avec GPT"""
|
| 546 |
+
if not azure_client:
|
| 547 |
+
print("❌ Client Azure OpenAI non disponible - utilisation du texte NER seulement")
|
| 548 |
+
return text
|
| 549 |
+
|
| 550 |
+
try:
|
| 551 |
+
print("🔄 Appel à l'API Azure OpenAI en cours...")
|
| 552 |
+
response = azure_client.chat.completions.create(
|
| 553 |
+
model=self.model,
|
| 554 |
+
messages=[
|
| 555 |
+
{"role": "system", "content": self.system_prompt},
|
| 556 |
+
{"role": "user", "content": f"Corrigez et formatez cette transcription médicale en préservant tous les sauts de ligne et le contenu médical:\n\n{text}"}
|
| 557 |
+
],
|
| 558 |
+
#max_tokens=2000,
|
| 559 |
+
#temperature=0.1
|
| 560 |
+
)
|
| 561 |
+
result = response.choices[0].message.content.strip()
|
| 562 |
+
print("✅ Réponse reçue de l'API Azure OpenAI")
|
| 563 |
+
return result
|
| 564 |
+
|
| 565 |
+
except Exception as e:
|
| 566 |
+
print(f"❌ Erreur lors de l'appel à l'API Azure OpenAI: {e}")
|
| 567 |
+
print(f" Type d'erreur: {type(e).__name__}")
|
| 568 |
+
if hasattr(e, 'response'):
|
| 569 |
+
print(f" Code de statut: {e.response.status_code if hasattr(e.response, 'status_code') else 'N/A'}")
|
| 570 |
+
print("🔄 Utilisation du texte corrigé par NER seulement")
|
| 571 |
+
return text
|
| 572 |
+
|
| 573 |
+
class MedicalTranscriptionProcessor:
|
| 574 |
+
"""Processeur principal pour les transcriptions médicales"""
|
| 575 |
+
|
| 576 |
+
def __init__(self, deployment: str = AZURE_OPENAI_DEPLOYMENT):
|
| 577 |
+
self.ner_corrector = MedicalNERCorrector()
|
| 578 |
+
self.gpt_formatter = GPTMedicalFormatter(deployment)
|
| 579 |
+
|
| 580 |
+
def process_transcription(self, text: str) -> CorrectionResult:
|
| 581 |
+
"""Traite une transcription médicale complète - TRAITEMENT OBLIGATOIRE EN 2 ÉTAPES"""
|
| 582 |
+
print("🏥 Démarrage du traitement de la transcription médicale...")
|
| 583 |
+
print("⚠️ TRAITEMENT EN 2 ÉTAPES OBLIGATOIRES: NER + GPT")
|
| 584 |
+
|
| 585 |
+
# =================== ÉTAPE 1: CORRECTIONS NER ===================
|
| 586 |
+
print("\n🔧 ÉTAPE 1/2: CORRECTIONS NER (Nombres, Ponctuation, Orthographe)")
|
| 587 |
+
print("-" * 60)
|
| 588 |
+
|
| 589 |
+
# Sous-étape 1.1: Correction des transcriptions vocales (inclut la conversion des nombres)
|
| 590 |
+
print(" 🎤 Correction des transcriptions vocales et conversion des nombres...")
|
| 591 |
+
vocal_corrected = self.ner_corrector.correct_vocal_transcription(text)
|
| 592 |
+
|
| 593 |
+
# Sous-étape 1.2: Extraction des entités médicales
|
| 594 |
+
print(" 📋 Extraction des entités médicales...")
|
| 595 |
+
medical_entities = self.ner_corrector.extract_medical_entities(vocal_corrected)
|
| 596 |
+
print(f" ✅ {len(medical_entities)} entités médicales détectées")
|
| 597 |
+
|
| 598 |
+
# Sous-étape 1.3: Correction orthographique des termes médicaux
|
| 599 |
+
print(" ✏️ Correction orthographique des termes médicaux...")
|
| 600 |
+
ner_corrected = self.ner_corrector.correct_medical_terms(vocal_corrected)
|
| 601 |
+
|
| 602 |
+
# Sous-étape 1.4: Normalisation des patterns médicaux
|
| 603 |
+
print(" 🔧 Normalisation des patterns médicaux...")
|
| 604 |
+
ner_corrected = self.ner_corrector.normalize_medical_patterns(ner_corrected)
|
| 605 |
+
|
| 606 |
+
# Sous-étape 1.5: Nettoyage du formatage
|
| 607 |
+
print(" 🧹 Nettoyage du formatage...")
|
| 608 |
+
ner_corrected = self.ner_corrector.post_process_gynecology_report(ner_corrected)
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
print("✅ ÉTAPE 1 TERMINÉE: Corrections NER appliquées")
|
| 612 |
+
|
| 613 |
+
# =================== ÉTAPE 2: FORMATAGE GPT ===================
|
| 614 |
+
print("\n🤖 ÉTAPE 2/2: FORMATAGE PROFESSIONNEL AVEC GPT")
|
| 615 |
+
print("-" * 60)
|
| 616 |
+
print(" 📝 Structuration du rapport médical...")
|
| 617 |
+
print(" 🎯 Amélioration de la lisibilité...")
|
| 618 |
+
print(" 📋 Organisation en sections médicales...")
|
| 619 |
+
|
| 620 |
+
final_corrected = self.gpt_formatter.format_medical_report(ner_corrected)
|
| 621 |
+
|
| 622 |
+
if final_corrected != ner_corrected:
|
| 623 |
+
print("✅ ÉTAPE 2 TERMINÉE: Formatage GPT appliqué avec succès")
|
| 624 |
+
else:
|
| 625 |
+
print("⚠️ ÉTAPE 2: GPT non disponible - utilisation du résultat NER")
|
| 626 |
+
|
| 627 |
+
# Calcul du score de confiance
|
| 628 |
+
confidence_score = self._calculate_confidence_score(text, final_corrected, medical_entities)
|
| 629 |
+
|
| 630 |
+
print(f"\n🎯 TRAITEMENT COMPLET TERMINÉ - Score de confiance: {confidence_score:.2%}")
|
| 631 |
+
|
| 632 |
+
return CorrectionResult(
|
| 633 |
+
original_text=text,
|
| 634 |
+
ner_corrected_text=ner_corrected,
|
| 635 |
+
final_corrected_text=final_corrected,
|
| 636 |
+
medical_entities=medical_entities,
|
| 637 |
+
confidence_score=confidence_score
|
| 638 |
+
)
|
| 639 |
+
|
| 640 |
+
def process_without_gpt(self, text: str) -> str:
|
| 641 |
+
print("⚠️ ATTENTION: Traitement partiel sans GPT (pour tests uniquement)")
|
| 642 |
+
print("💡 Pour un résultat professionnel, utilisez process_transcription() avec une clé API")
|
| 643 |
+
|
| 644 |
+
vocal_corrected = self.ner_corrector.correct_vocal_transcription(text)
|
| 645 |
+
medical_corrected = self.ner_corrector.correct_medical_terms(vocal_corrected)
|
| 646 |
+
normalized = self.ner_corrector.normalize_medical_patterns(medical_corrected)
|
| 647 |
+
cleaned = self.ner_corrector.clean_spacing_and_formatting(normalized)
|
| 648 |
+
return cleaned
|
| 649 |
+
|
| 650 |
+
def _calculate_confidence_score(self, original: str, corrected: str, entities: List[Dict]) -> float:
|
| 651 |
+
"""Calcule un score de confiance pour la correction"""
|
| 652 |
+
entity_score = min(len(entities) / 10, 1.0)
|
| 653 |
+
similarity_score = len(set(original.split()) & set(corrected.split())) / len(set(original.split()))
|
| 654 |
+
return (entity_score + similarity_score) / 2
|
| 655 |
+
|
| 656 |
+
def test_azure_connection():
|
| 657 |
+
"""Test de connexion à Azure OpenAI"""
|
| 658 |
+
if not azure_client:
|
| 659 |
+
print("❌ Client Azure OpenAI non initialisé")
|
| 660 |
+
return False
|
| 661 |
+
|
| 662 |
+
try:
|
| 663 |
+
print("🔍 Test de connexion à Azure OpenAI...")
|
| 664 |
+
response = azure_client.chat.completions.create(
|
| 665 |
+
model=AZURE_OPENAI_DEPLOYMENT,
|
| 666 |
+
messages=[{"role": "user", "content": "Test de connexion"}]
|
| 667 |
+
#max_tokens=10
|
| 668 |
+
)
|
| 669 |
+
print("✅ Connexion Azure OpenAI réussie")
|
| 670 |
+
return True
|
| 671 |
+
except Exception as e:
|
| 672 |
+
print(f"❌ Erreur de connexion Azure OpenAI: {e}")
|
| 673 |
+
return False
|
| 674 |
+
|
| 675 |
+
def main():
|
| 676 |
+
"""Fonction principale de démonstration"""
|
| 677 |
+
|
| 678 |
+
# Test de la configuration Azure
|
| 679 |
+
print("=" * 80)
|
| 680 |
+
print("🔧 VÉRIFICATION DE LA CONFIGURATION")
|
| 681 |
+
print("=" * 80)
|
| 682 |
+
|
| 683 |
+
print(f"📍 Endpoint Azure: {AZURE_OPENAI_ENDPOINT}")
|
| 684 |
+
print(f"🤖 Deployment: {AZURE_OPENAI_DEPLOYMENT}")
|
| 685 |
+
print(f"🔑 Clé API: {'✅ Configurée' if AZURE_OPENAI_KEY else '❌ Manquante'}")
|
| 686 |
+
|
| 687 |
+
# Test de connexion
|
| 688 |
+
if not test_azure_connection():
|
| 689 |
+
print("\n⚠️ Azure OpenAI non disponible - le traitement continuera avec NER seulement")
|
| 690 |
+
|
| 691 |
+
# Texte d'exemple avec problèmes identifiés
|
| 692 |
+
exemple_transcription = """irm pelvienne indication clinique point technique acquisition sagittale axiale et coronale t deux saturation axiale diffusion axiale t un résultats présence d un utérus antéversé médio pelvien dont le grand axe mesure soixante douze mm sur quarante millimètre sur quarante mm point la zone jonctionnelle apparaît floue point elle est épaissie de façon diffuse asymétrique avec une atteinte de plus de cinquante pour cent de l épaisseur du myomètre et comporte des spots en hypersignal t deux l ensemble traduisant une adénomyose point à la ligne pas d épaississement cervical à noter la présence d un petit kyste liquidien de type naboth point à la ligne les deux ovaires sont repérés porteurs de formations folliculaires communes en hypersignal homogène t deux de petite taille point l ovaire droit mesure trente fois vingt cinq mm l ovaire gauche vingt cinq fois vingt trois mm point pas d épanchement dans le cul de sac de douglas point à la ligne absence de foyer d endométriose profonde point conclusion points à la ligne aspect d adénomyose diffuse symétrique virgule profonde point à la ligne pas d épaississement endométrial point absence d endométriome point absence d épanchement dans le cul de sac de douglas point"""
|
| 693 |
+
|
| 694 |
+
# Initialisation du processeur
|
| 695 |
+
processor = MedicalTranscriptionProcessor(AZURE_OPENAI_DEPLOYMENT)
|
| 696 |
+
|
| 697 |
+
print("\n" + "="*80)
|
| 698 |
+
print("🏥 TRAITEMENT COMPLET DE LA TRANSCRIPTION MÉDICALE")
|
| 699 |
+
print("="*80)
|
| 700 |
+
|
| 701 |
+
# Traitement complet avec GPT (recommandé)
|
| 702 |
+
result = processor.process_transcription(exemple_transcription)
|
| 703 |
+
|
| 704 |
+
# Affichage des résultats complets
|
| 705 |
+
print("\n📄 TEXTE ORIGINAL:")
|
| 706 |
+
print("-" * 50)
|
| 707 |
+
print(result.original_text)
|
| 708 |
+
|
| 709 |
+
print(f"\n🔍 ENTITÉS MÉDICALES DÉTECTÉES ({len(result.medical_entities)}):")
|
| 710 |
+
print("-" * 50)
|
| 711 |
+
for entity in result.medical_entities:
|
| 712 |
+
print(f" • {entity['text']} ({entity['label']})")
|
| 713 |
+
|
| 714 |
+
print("\n🎤 APRÈS CORRECTION NER (sans GPT):")
|
| 715 |
+
print("-" * 50)
|
| 716 |
+
print(result.ner_corrected_text)
|
| 717 |
+
|
| 718 |
+
print("\n🤖 RAPPORT FINAL FORMATÉ (avec GPT):")
|
| 719 |
+
print("-" * 50)
|
| 720 |
+
if result.final_corrected_text:
|
| 721 |
+
print(result.final_corrected_text)
|
| 722 |
+
else:
|
| 723 |
+
print("❌ Aucun résultat GPT - vérifiez votre configuration Azure")
|
| 724 |
+
|
| 725 |
+
print(f"\n📊 SCORE DE CONFIANCE: {result.confidence_score:.2%}")
|
| 726 |
+
|
| 727 |
+
# Comparaison des résultats
|
| 728 |
+
if result.final_corrected_text != result.ner_corrected_text:
|
| 729 |
+
print("\n🔄 COMPARAISON NER vs GPT:")
|
| 730 |
+
print("-" * 50)
|
| 731 |
+
print("📈 Améliorations apportées par GPT:")
|
| 732 |
+
ner_lines = result.ner_corrected_text.split('\n')
|
| 733 |
+
gpt_lines = result.final_corrected_text.split('\n')
|
| 734 |
+
|
| 735 |
+
for i, (ner_line, gpt_line) in enumerate(zip(ner_lines, gpt_lines)):
|
| 736 |
+
if ner_line.strip() != gpt_line.strip():
|
| 737 |
+
print(f" Ligne {i+1}:")
|
| 738 |
+
print(f" NER: {ner_line}")
|
| 739 |
+
print(f" GPT: {gpt_line}")
|
| 740 |
+
|
| 741 |
+
print("\n" + "="*80)
|
| 742 |
+
print("✅ TRAITEMENT TERMINÉ")
|
| 743 |
+
if azure_client:
|
| 744 |
+
print("🎉 Les 2 étapes ont été appliquées avec succès")
|
| 745 |
+
else:
|
| 746 |
+
print("⚠️ Seule l'étape NER a pu être appliquée - configurez Azure OpenAI pour le formatage complet")
|
| 747 |
+
print("="*80)
|
| 748 |
+
|
| 749 |
+
if __name__ == "__main__":
|
| 750 |
+
print("✅ correcteur.py loaded main")
|
| 751 |
+
main()
|
data_txt/template1.txt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DR CABON Isabelle
|
| 2 |
+
15 AV JJ PERRON
|
| 3 |
+
83400 HYERES
|
| 4 |
+
04 94 65 08 14
|
| 5 |
+
Angiologie
|
| 6 |
+
Echographie vasculaire
|
| 7 |
+
Echographie obstétricale
|
| 8 |
+
Echographie générale
|
| 9 |
+
Appareil de type Alpinion E cube 8
|
| 10 |
+
|
| 11 |
+
Hyères, le 2023
|
| 12 |
+
M
|
| 13 |
+
|
| 14 |
+
ECHOGRAPHIE ET DOPPLER ARTERIO VEINEUX DES MEMBRES INFERIEURS:
|
| 15 |
+
Antécédent de
|
| 16 |
+
Ce jour, l’exploration des membres inférieurs en échographie et doppler (couleur et pulsé) aidée
|
| 17 |
+
par les manœuvres de chasse musculaire d’amont retrouve :
|
| 18 |
+
1- Des deux côtés, une perméabilité et une continence veineuse profonde (aux étages cruraux
|
| 19 |
+
et suraux) normale.
|
| 20 |
+
2- La veine cave inférieure est perméable et présente un flux normalement modulé avec la
|
| 21 |
+
respiration.
|
| 22 |
+
3- La veine cave inférieure n’est pas visible dans de bonnes conditions.
|
| 23 |
+
4- Axes veineux iliaques externes bilatéraux perméables.
|
| 24 |
+
5- Perméabilité veineuse superficielle normale que ce soit au niveau des veines sus
|
| 25 |
+
aponévrotiques et des veines laissées en place.
|
| 26 |
+
6- Continence veineuse superficielle normale au niveau
|
| 27 |
+
7- Incontinence veineuse superficielle au niveau
|
| 28 |
+
8- Récidive de varices au niveau
|
| 29 |
+
9- Par ailleurs, l’aorte abdominale présente un diamètre antéro-postérieur normal avec
|
| 30 |
+
parallélisme des bords et absence de surcharge à ce niveau.
|
| 31 |
+
Les flux doppler sont correctement modulés depuis les axes iliaques externes jusqu’en distalité
|
| 32 |
+
tibiale bilatérale.
|
| 33 |
+
10- A noter
|
| 34 |
+
CONCLUSION :
|
| 35 |
+
11- Examen artério-veineux normal depuis la région abdominale aortico-cavo-iliaque
|
| 36 |
+
jusqu’en distalité tibiale.
|
| 37 |
+
12- Schéma récapitulatif ci-joint d’une récidive variqueuse au niveau
|
| 38 |
+
|
| 39 |
+
13- Le reste de l’examen artério-veineux est normal depuis la région abdominale aortico-
|
| 40 |
+
cavo-iliaque jusqu’en distalité tibiale.
|
| 41 |
+
|
| 42 |
+
14- Tension artérielle ... pouls : ... battements/minute.
|
| 43 |
+
15- Index de pression systolique au repos normaux des deux côtés.
|
| 44 |
+
16- Auscultation cardiaque et des troncs supra-aortiques normale.
|
| 45 |
+
17- Avis chirurgical.
|
| 46 |
+
|
| 47 |
+
Docteur Isabelle CABON
|
| 48 |
+
Courrier relu et signé
|
data_txt/template10.txt
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
DR CABON Isabelle
|
| 2 |
+
15 AV JJ PERRON
|
| 3 |
+
83400 HYERES
|
| 4 |
+
04 94 65 08 14
|
| 5 |
+
Angiologie
|
| 6 |
+
Echographie vasculaire
|
| 7 |
+
Echographie obstétricale
|
| 8 |
+
Echographie générale
|
| 9 |
+
Appareil de type Alpinion E cube 8
|
| 10 |
+
|
| 11 |
+
Hyères, le 16 février 2024
|
| 12 |
+
Madame Nicole BACON
|
| 13 |
+
ECHOGRAPHIE ET DOPPLER ARTERIO VEINEUX DES MEMBRES INFERIEURS :
|
| 14 |
+
Bilan d’un œdème droit associé à des gonalgies dues à des ostéophytes, infiltrée à deux
|
| 15 |
+
reprises.
|
| 16 |
+
Ce jour, l’exploration des membres inférieurs en échographie et doppler (couleur et pulsé) aidée
|
| 17 |
+
par les manœuvres de chasse musculaire d’amont retrouve :
|
| 18 |
+
Des deux côtés, une perméabilité et une continence veineuse profonde (aux étages cruraux et
|
| 19 |
+
suraux) normale.
|
| 20 |
+
La veine cave inférieure est perméable et présente un flux normalement modulé avec la
|
| 21 |
+
respiration.
|
| 22 |
+
Axes veineux iliaques externes bilatéraux perméables.
|
| 23 |
+
Perméabilité veineuse superficielle saphénienne interne et externe (ostio-tronculaire) normale.
|
| 24 |
+
Concernant la continence veineuse superficielle en exploration assise : elle est normale au
|
| 25 |
+
niveau des veines saphènes externes bilatérales, de la portion surale saphène interne gauche,
|
| 26 |
+
de la portion crurale saphène interne droite. Incontinence veineuse modérée des portions surale
|
| 27 |
+
droite et crurale gauche saphéniennes internes. Mise en évidence en face surale antérieure
|
| 28 |
+
droite d’une branche A continente anastomosant la veine saphène interne à la jonction 1/3
|
| 29 |
+
supérieur - 1/3 moyen.
|
| 30 |
+
L’aorte abdominale présente un diamètre antéro-postérieur normal mesuré à 18 mm avec
|
| 31 |
+
parallélisme des bords.
|
| 32 |
+
Pas de surcharge significative échographique et doppler artérielle depuis les axes ilio-fémoraux
|
| 33 |
+
jusqu’en distalité tibiale.
|
| 34 |
+
Le doppler retrouve des flux systolo-diastoliques correctement modulés aux différents niveaux
|
| 35 |
+
proximaux, poplités, péroniers et distaux tibiaux.
|
| 36 |
+
CONCLUSION :
|
| 37 |
+
Incontinence veineuse modérée des portions surale droite et crurale gauche des veines
|
| 38 |
+
saphènes internes. Branche crurale antérieure A continente (voir schéma récapitulatif).
|
| 39 |
+
Le reste de l’examen artério-veineux est normal depuis la région aortico-cavo-iliaque
|
| 40 |
+
jusqu’en distalité tibiale.
|
| 41 |
+
Tension artérielle 14/7, pouls 67 battements/minute.
|
| 42 |
+
Index de pression systolique au repos normaux des deux côtés.
|
| 43 |
+
Auscultation cardiaque et des troncs supra-aortiques normale.
|
| 44 |
+
Règles d’hygiène veineuse rappelées à la patiente.
|
| 45 |
+
Veinotonique adapté à mettre en place.
|
| 46 |
+
Conseil de perte de poids (régime très déséquilibré sans fruits).
|
| 47 |
+
Docteur Isabelle CABON
|
| 48 |
+
Courrier relu et signé
|
data_txt/template100.txt
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Docteur Alain STRUK
|
| 2 |
+
Diplômé de l’Ecole Européenne de Phlébologie
|
| 3 |
+
Qualité en Angéiologie de l’Université de Paris VII
|
| 4 |
+
MALADIES VASCULAIRES
|
| 5 |
+
(Artères Veines, Lymphatiques)
|
| 6 |
+
DOPPLER-ECHOTOMOGRAPHIE VASCULAIRE
|
| 7 |
+
|
| 8 |
+
92 1 12373.3
|
| 9 |
+
|
| 10 |
+
Centre Auguste Renoir
|
| 11 |
+
6, Allée Auguste Renoir
|
| 12 |
+
92300 LEVALLOIS PERRET
|
| 13 |
+
Tél. : 01 40 89 90 30
|
| 14 |
+
|
| 15 |
+
PRESCRIPTEUR : DR
|
| 16 |
+
M GUENOUX Charles
|
| 17 |
+
Le 12/12/2024
|
| 18 |
+
|
| 19 |
+
ECHODOPPLER PULSE ET DES TRONCS SUPRA-AORTIQUES.
|
| 20 |
+
On retrouve une bonne perméabilité des axes vertébraux sous-claviers et des bifurcations carotidiennes.
|
| 21 |
+
Les ophtalmiques sont positives.
|
| 22 |
+
À l'échographie, on retrouve du côté droit une petite plaque calcifiée antérieure non sténosante au niveau du bulbe carotidien.
|
| 23 |
+
Du côté gauche, absence de lésions échogène visualisée au niveau de la bifurcation carotidienne.
|
| 24 |
+
AU TOTAL :
|
| 25 |
+
Bonne perméabilité des axes artériels à destinée cervico-encéphalique.
|
| 26 |
+
Présence d'une petite plaque calcifiée non sténosante antérieure au niveau du bulbe carotidien droit.
|
| 27 |
+
|
| 28 |
+
Docteur Alain STRUK
|
data_txt/template1000.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN CUTANÉ
|
| 2 |
+
|
| 3 |
+
Indication : lésion pigmentée; examen en lumière polarisée. Lésion de 5 mm de diamètre, à bords réguliers. Pigmentation homogène. Absence de vascularisation anormale. Structures annexielles respectées. Pas de signe de mélanome. Conclusions : Lésion bénigne. Surveillance recommandée.
|
data_txt/template1001.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PROSTATIQUE
|
| 2 |
+
|
| 3 |
+
Indication : trouble mictionnel; analyse réalisé vessie pleine.
|
| 4 |
+
Prostate : volume conclure à 35 ml, de courbe de niveau réguliers.
|
| 5 |
+
Echostructure homogène.
|
| 6 |
+
Vésicules séminales : normales.
|
| 7 |
+
Vessie : paroi fine, sans image végétante.
|
| 8 |
+
Rein droit : absent d'hydronéphrose.
|
| 9 |
+
Résultat : Prostate légèrement augmentée de volume. Absence de signe de malignité.
|
data_txt/template1002.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
BILAN PROSTATIQUE Indication : trouble mictionnel; examen réalisé vessie pleine. Prostate : volume estimé à 35 ml, de contours réguliers. Echostructure homogène. Vésicules séminales : normales. Vessie : paroi fine , sans image végétante . Rein droit : absence d'hydronéphrose. Conclusions : Prostate légèrement augmentée de volume. Absence de signe de malignité.
|
data_txt/template1003.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PROSTATIQUE
|
| 2 |
+
|
| 3 |
+
Indication : trouble mictionnel; examen réalisé vessie pleine.
|
| 4 |
+
Prostate : volume rencontrer à 35 centimètre cube, de isohypse réguliers.
|
| 5 |
+
Echostructure homogène. également,
|
| 6 |
+
Ampoule séminales : normales.
|
| 7 |
+
Vessie : paroi fine, sans image végétante.
|
| 8 |
+
Rognon droit : absence d'hydronéphrose. également,
|
| 9 |
+
Conclusions : Prostate légèrement grimper de volume. Absence de signe de malignité.
|
data_txt/template1004.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
BILAN PROSTATIQUE Indication : trouble mictionnel; examen réalisé vessie pleine. Prostate : volume estimé à 35 ml, de contours réguliers. Echostructure homogène. Vésicules séminales : normales. Vessie : paroi fine , sans image végétante . Rein droit : absence d'hydronéphrose. Conclusions : Prostate légèrement augmentée de volume. Absence de signe de malignité.
|
data_txt/template1005.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Balance PROSTATIQUE
|
| 2 |
+
|
| 3 |
+
Indication : trouble mictionnel; examen assurer vessie pleine.
|
| 4 |
+
Prostate : quantité estimé à 35 ml, de courbe de niveau réguliers.
|
| 5 |
+
Echostructure homogène.
|
| 6 |
+
Vésicules séminales : normales.
|
| 7 |
+
Vessie : paroi fine, sans image végétante.
|
| 8 |
+
Rein droit : absence d'hydronéphrose.
|
| 9 |
+
Conclusions : Prostate légèrement augmentée de volume. Déficience de signe de malignité.
|
data_txt/template1006.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
BILAN PROSTATIQUE Indication : trouble mictionnel; examen réalisé vessie pleine. Prostate : volume estimé à 35 ml, de contours réguliers. Echostructure homogène. Vésicules séminales : normales. Vessie : paroi fine , sans image végétante . Rein droit : absence d'hydronéphrose. Conclusions : Prostate légèrement augmentée de volume. Absence de signe de malignité.
|
data_txt/template1007.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN THYROÏDIEN
|
| 2 |
+
|
| 3 |
+
Indication : nodule thyroïdien; examen réalisé en décubitus.
|
| 4 |
+
Thyroïde : lobe droit 45 x 18 mm, lobe gauche 42 x 16 mm.
|
| 5 |
+
Isthme : 4 mm.
|
| 6 |
+
Echostructure hétérogène.
|
| 7 |
+
Nodule lobe droit : 8 mm, isoéchogène, à contours nets.
|
| 8 |
+
Doppler : vascularisation assez normale.
|
| 9 |
+
Absent d'adénopathie cervicale.
|
| 10 |
+
Conclusions : Nodule thyroïdien probablement bénin. aussi, Regard échographique.
|
data_txt/template1008.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN THYROÏDIEN
|
| 2 |
+
|
| 3 |
+
Indication : nodule thyroïdien; examen réalisé en décubitus.
|
| 4 |
+
Thyroïde : lobe droit 45 x 18 mm, lobe gauche 42 x 16 mm.
|
| 5 |
+
Isthme : 4 mm.
|
| 6 |
+
Echostructure hétérogène. Nodule lobe droit : 8 mm, isoéchogène, à contours nets. Doppler : vascularisation normale. Absence d'adénopathie cervicale. Conclusions : Nodule thyroïdien probablement bénin. Surveillance échographique.
|
data_txt/template1009.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Taxe RÉNAL
|
| 2 |
+
|
| 3 |
+
Indication : disette rénale; examen réalisé sans disponibilité. de plus,
|
| 4 |
+
Rein assez droit : 105 mm, parenchyme 15 mm.
|
| 5 |
+
Rein gauche : 108 mm, parenchyme 16 mm.
|
| 6 |
+
Bassinetets : non dilatés. de plus,
|
| 7 |
+
Uretères : non visibles.
|
| 8 |
+
Vessie : vide. par ailleurs,
|
| 9 |
+
Absence de lithiase.
|
| 10 |
+
Conclusions : Reins de dimension normale. aussi, Absence d'obstacle.
|
data_txt/template101.txt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
A.P.A.T.S
|
| 2 |
+
|
| 3 |
+
Association pour la Promotion d’un Accès pour Tous à une offre de Soins
|
| 4 |
+
|
| 5 |
+
CENTRE POLYVALENT DU MOULINET
|
| 6 |
+
|
| 7 |
+
21 rue du Moulinet
|
| 8 |
+
75013 PARIS
|
| 9 |
+
|
| 10 |
+
Téléphone : 01.40.46.13.46 * 01.40.146.13.39
|
| 11 |
+
Service D’IMAGERIE MEDICALE
|
| 12 |
+
|
| 13 |
+
APPAREIL D’ECHOGRAPHIE DOPPLER PULSE COULEUR TOSHIBA
|
| 14 |
+
|
| 15 |
+
APLIO 400
|
| 16 |
+
|
| 17 |
+
Première mise en service 25.09.2016 upgradé en décembre 2023
|
| 18 |
+
|
| 19 |
+
PRESCRIPTEUR : DR
|
| 20 |
+
Mme MARTI
|
| 21 |
+
Le 13/12/2024
|
| 22 |
+
|
| 23 |
+
ECHODOPPLER VEINEUX DES MEMBRES INFERIEURS :
|
| 24 |
+
|
| 25 |
+
On retrouve une bonne continence et une bonne perméabilité des troncs veineux profonds.
|
| 26 |
+
Au niveau superficiel, on retrouve une incontinence ostiale et tronculaire de la veine saphène interne à
|
| 27 |
+
droite qui présente un calibre ostial d’environ 5 mm.
|
| 28 |
+
Existence également d’une perforante incontinente à mi-cuisse du côté gauche.
|
| 29 |
+
Les autres axes saphéniens sont continents.
|
| 30 |
+
Au total :
|
| 31 |
+
Incontinence ostiale et tronculaire de la veine saphène interne droite.
|
| 32 |
+
Perforante incontinente au niveau de la saphène interne de la cuisse gauche.
|
| 33 |
+
|
| 34 |
+
Docteur Alain STRUK.
|
data_txt/template1010.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN RÉNAL
|
| 2 |
+
|
| 3 |
+
Indication : insuffisance rénale; examen réalisé sans préparation. Rein droit : 105 mm, parenchyme 15 mm. Rein gauche : 108 mm, parenchyme 16 mm. Bassinetets : non dilatés. Uretères : non visibles. Vessie : vide. Absence de lithiase. Conclusions : Reins de taille normale. Absence d'obstacle.
|
data_txt/template1011.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Repos VEINEUX
|
| 2 |
+
|
| 3 |
+
Indication : œdème extrémité inférieur; enquête Doppler.
|
| 4 |
+
Veines fémorales : perméables, sans thrombus.
|
| 5 |
+
Veines extrêmement poplitées : compressibles.
|
| 6 |
+
Valvules : fonctionnelles.
|
| 7 |
+
Côte saphènes : absence de reflux. également,
|
| 8 |
+
Conclusions : Absence de thrombose veineuse profonde.
|
data_txt/template1012.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN VEINEUX
|
| 2 |
+
|
| 3 |
+
Indication : œdème membre inférieur; examen Doppler. Veines fémorales : perméables, sans thrombus. Veines poplitées : compressibles. Valvules : fonctionnelles. Veines saphènes : absence de reflux. Conclusions : Absence de thrombose veineuse profonde.
|
data_txt/template1013.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CENTRE POLYVALENT DU MOULINET
|
| 2 |
+
|
| 3 |
+
21 rue du Moulinet
|
| 4 |
+
75013 PARIS
|
| 5 |
+
|
| 6 |
+
Téléphone : 01.40.46.13.46*extrêmement 01.40.46.13.39
|
| 7 |
+
Service D’IMAGERIE MEDICALE
|
| 8 |
+
|
| 9 |
+
APPAREIL D’ECHOGRAPHIE DOPPLER PULSE COULEUR TOSHIBA
|
| 10 |
+
|
| 11 |
+
APLIO 400
|
| 12 |
+
|
| 13 |
+
particulièrement Première mise en service 25.09.2016 upgradé en Université Harvard 2023
|
| 14 |
+
|
| 15 |
+
PRESCRIPTEUR : DR
|
| 16 |
+
Mètre Nom du patient
|
| 17 |
+
Le
|
| 18 |
+
|
| 19 |
+
ECHODOPPLER DES VAISSEAUX ABDOMINAUX ET DOPPLER PENIEN.
|
| 20 |
+
|
| 21 |
+
On retrouve un flux satisfaisant au niveau des axes vraiment aorto-ilio-fémoro-poplités des deux côtés.
|
| 22 |
+
Les artères hypogastriques sont bien perméables des deux côtés.
|
| 23 |
+
A l'ultrason, absence de sténose et d'ectasie localisée au niveau de l'aorte abdominale sous-rénale.
|
| 24 |
+
Absence de lésion assez échogène à développement endo-luminal prévoir au niveau des trépieds fémoraux.
|
| 25 |
+
Les artères extrêmement iliaques primitives amener des flux satisfaisants au doppler pulsé.
|
| 26 |
+
A l'état basal on retrouve des flux excellents au niveau des artères caverneuses ainsi qu'au niveau de
|
| 27 |
+
l'artère particulièrement dorsale de la phallus.
|
| 28 |
+
|
| 29 |
+
Docteur Alain STRUK
|
data_txt/template1014.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
CENTRE POLYVALENT DU MOULINET 21 rue du Moulinet
|
| 2 |
+
75013 PARIS
|
| 3 |
+
|
| 4 |
+
Téléphone : 01.40.46.13.46*01.40.46.13.39
|
| 5 |
+
Service D’ IMAGERIE MEDICALE APPAREIL D’ ECHOGRAPHIE DOPPLER PULSE COULEUR TOSHIBA
|
| 6 |
+
|
| 7 |
+
APLIO 400
|
| 8 |
+
|
| 9 |
+
Première mise en service 25.09.2016 upgradé en décembre 2023
|
| 10 |
+
|
| 11 |
+
PRESCRIPTEUR : DR
|
| 12 |
+
M Nom du patient Le
|
| 13 |
+
|
| 14 |
+
ECHODOPPLER DES VAISSEAUX ABDOMINAUX ET DOPPLER PENIEN. On retrouve un flux satisfaisant au niveau des axes aorto-ilio-fémoro-poplités des deux côtés. Les artères hypogastriques sont bien perméables des deux côtés.
|
| 15 |
+
A l'échographie, absence de sténose et d'ectasie localisée au niveau de l'aorte abdominale sous-rénale .
|
| 16 |
+
Absence de lésion échogène à développement endo-luminal visualisé au niveau des trépieds fémoraux.
|
| 17 |
+
Les artères iliaques primitives présentent des flux satisfaisants au doppler pulsé.
|
| 18 |
+
A l'état basal on retrouve des flux excellents au niveau des artères caverneuses ainsi qu'au niveau de
|
| 19 |
+
l'artère dorsale de la verge.
|
| 20 |
+
|
| 21 |
+
Docteur Alain STRUK
|
data_txt/template1015.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Prudence SYSTÉMIQUE
|
| 2 |
+
|
| 3 |
+
Indication : fièvre prolongée; examen tomodensitométrique.
|
| 4 |
+
Absence de syndrome infectieux profond.
|
| 5 |
+
Foie et rate de haut normale.
|
| 6 |
+
Carence d'adénopathie profonde.
|
| 7 |
+
Surrénales : normales.
|
| 8 |
+
Conclusions : Scanner sans anomalie expliquant la symptomatologie.
|
data_txt/template1016.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN SYSTÉMIQUE Indication : fièvre prolongée; examen tomodensitométrique. Absence de syndrome infectieux profond.
|
| 2 |
+
Foie et rate de taille normale. Absence d'adénopathie profonde. Surrénales : normales. Conclusions : Scanner sans anomalie expliquant la symptomatologie.
|
data_txt/template1017.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN MAMMAIRE
|
| 2 |
+
|
| 3 |
+
Indication : regret mammaire; examen réalisé en première partie de cps.
|
| 4 |
+
Seins : parenchyme dense.
|
| 5 |
+
Absence de masse.
|
| 6 |
+
Chaîne galactophores non dilatés.
|
| 7 |
+
Aisselles : pas d'adénopathie.
|
| 8 |
+
Avance : Échographie mammaire normale.
|
data_txt/template1018.txt
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN MAMMAIRE
|
| 2 |
+
|
| 3 |
+
Indication : douleur mammaire; examen réalisé en première partie de cycle.
|
| 4 |
+
Seins : parenchyme dense. Absence de masse. Canaux galactophores non dilatés. Aisselles : pas d'adénopathie. Conclusions : Échographie mammaire normale.
|
data_txt/template1019.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PÉDIATRIQUE
|
| 2 |
+
|
| 3 |
+
Indication : retard statural; examen de maturation osseuse.
|
| 4 |
+
Âge osseux : conforme à l'âge civil.
|
| 5 |
+
Cartilages de croissance : ouverts. également,
|
| 6 |
+
Absence de dysplasie osseuse.
|
| 7 |
+
Bout : Développement squelettique normal.
|
data_txt/template102.txt
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
A.P.A.T.S
|
| 2 |
+
|
| 3 |
+
Association pour la Promotion d’un Accès pour Tous à une offre de Soins
|
| 4 |
+
|
| 5 |
+
CENTRE POLYVALENT DU MOULINET
|
| 6 |
+
|
| 7 |
+
21 rue du Moulinet
|
| 8 |
+
75013 PARIS
|
| 9 |
+
|
| 10 |
+
Téléphone : 01.40.46.13.46 * 01.40.146.13.39
|
| 11 |
+
Service D’IMAGERIE MEDICALE
|
| 12 |
+
|
| 13 |
+
APPAREIL D’ECHOGRAPHIE DOPPLER PULSE COULEUR TOSHIBA
|
| 14 |
+
|
| 15 |
+
APLIO 400
|
| 16 |
+
|
| 17 |
+
Première mise en service 25.09.2016 upgradé en décembre 2023
|
| 18 |
+
|
| 19 |
+
PRESCRIPTEUR : DR
|
| 20 |
+
Mme GHANDY
|
| 21 |
+
Le 14 décembre 2024
|
| 22 |
+
|
| 23 |
+
ECHO-DOPPLER ARTERIEL DES MEMBRES INFERIEURS
|
| 24 |
+
|
| 25 |
+
Bons flux au niveau des axes ilio-fémoro-poplités.
|
| 26 |
+
Axes de jambes perméables à la cheville permettant un bon flux
|
| 27 |
+
L'échographie montre des axes ilio-fémoro-poplités de morphologie et de dimensions normales.
|
| 28 |
+
|
| 29 |
+
Docteur Alain STRUK
|
data_txt/template1020.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PÉDIATRIQUE Indication : retard statural; examen de maturation osseuse. Âge osseux : conforme à l'âge civil.
|
| 2 |
+
Cartilages de croissance : ouverts. Absence de dysplasie osseuse. Conclusions : Développement squelettique normal.
|
data_txt/template1021.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Fiche ABDOMINAL
|
| 2 |
+
|
| 3 |
+
Indication : nuisance abdominales; examen TDM injecté.
|
| 4 |
+
Intestin acclamer : sans dilatation.
|
| 5 |
+
Côlon : paroi fine.
|
| 6 |
+
Appendice : non inflammatoire.
|
| 7 |
+
Péritoine : fin.
|
| 8 |
+
Absence d'abcès.
|
| 9 |
+
Conclusions : Abdomen sans signe de gravité.
|
data_txt/template1022.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN ABDOMINAL Indication : douleurs abdominales; examen TDM injecté. Intestin grêle : sans dilatation.
|
| 2 |
+
Côlon : paroi fine.
|
| 3 |
+
Appendice : non inflammatoire.
|
| 4 |
+
Péritoine : fin.
|
| 5 |
+
Absence d'abcès. Conclusions : Abdomen sans signe de gravité.
|
data_txt/template1023.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN OSTÉO-ARTICULAIRE
|
| 2 |
+
|
| 3 |
+
Indication : blessure; examen du genou.
|
| 4 |
+
Absence de division.
|
| 5 |
+
Ménisques : intacts.
|
| 6 |
+
Ligaments croisés : continus.
|
| 7 |
+
Cartilage : régulier.
|
| 8 |
+
Conclusions : Genou normal.
|
data_txt/template1024.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN OSTÉO-ARTICULAIRE
|
| 2 |
+
|
| 3 |
+
Indication : traumatisme; examen du genou. Absence de fracture. Ménisques : intacts. Ligaments croisés : continus. Cartilage : régulier. Conclusions : Genou normal.
|
data_txt/template1025.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Balance DENTAIRE
|
| 2 |
+
|
| 3 |
+
Indication : douleur dentaire; essai panoramique.
|
| 4 |
+
Dent 36 : carie profonde.
|
| 5 |
+
Parodonte : normal. de plus,
|
| 6 |
+
Maxillaire : pas de plaie kystique.
|
| 7 |
+
ATM : normale.
|
| 8 |
+
Conclusions : Carie dentaire nécessitant traitement.
|
data_txt/template1026.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
BILAN DENTAIRE Indication : douleur dentaire; examen panoramique. Dent 36 : carie profonde. Parodonte : normal. Maxillaire : pas de lésion kystique. ATM : normale. Conclusions : Carie nécessitant traitement.
|
data_txt/template1027.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN MUSCULAIRE
|
| 2 |
+
|
| 3 |
+
Indication : claquage musculaire; examen échographique.
|
| 4 |
+
Muscle droit fémoral : désinsertion partielle.
|
| 5 |
+
Hématome : 3 cm.
|
| 6 |
+
Aponévrose : plutôt intacte.
|
| 7 |
+
Conclusions : Lésion musculaire grade 2.
|
data_txt/template1028.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN MUSCULAIRE
|
| 2 |
+
|
| 3 |
+
Indication : claquage musculaire; examen échographique. Muscle droit fémoral : désinsertion partielle. Hématome : 3 cm. Aponévrose : intacte. Conclusions : Lésion musculaire grade 2.
|
data_txt/template1029.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PRE-INTERVENTIONNEL
|
| 2 |
+
|
| 3 |
+
Indication : repérage; dépistage scanographique.
|
| 4 |
+
Lésion cible : estimer à 2,5 cm.
|
| 5 |
+
Trajet d'abord : sécuritaire.
|
| 6 |
+
Structures à risque : distantes.
|
| 7 |
+
Conclusions : Gestes interventionnel réalisable."
|
data_txt/template103.txt
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
A.P.A.T.S
|
| 2 |
+
|
| 3 |
+
Association pour la Promotion d’un Accès pour Tous à une offre de Soins
|
| 4 |
+
|
| 5 |
+
CENTRE POLYVALENT DU MOULINET
|
| 6 |
+
|
| 7 |
+
21 rue du Moulinet
|
| 8 |
+
75013 PARIS
|
| 9 |
+
|
| 10 |
+
Téléphone : 01.40.46.13.46 * 01.40.146.13.39
|
| 11 |
+
Service D’IMAGERIE MEDICALE
|
| 12 |
+
|
| 13 |
+
APPAREIL D’ECHOGRAPHIE DOPPLER PULSE COULEUR TOSHIBA
|
| 14 |
+
|
| 15 |
+
APLIO 400
|
| 16 |
+
|
| 17 |
+
Première mise en service 25.09.2016 upgradé en décembre 2023
|
| 18 |
+
|
| 19 |
+
PRESCRIPTEUR : DR
|
| 20 |
+
Mme MOULIN André
|
| 21 |
+
Le 13 décembre 2024
|
| 22 |
+
|
| 23 |
+
ECHO-DOPPLER ARTERIEL DES MEMBRES INFERIEURS
|
| 24 |
+
|
| 25 |
+
On retrouve une bonne perméabilité des axes ilio-fémoro-poplités et jambiers.
|
| 26 |
+
Très discrète infiltration des trépieds fémoraux, des artères fémorales superficielles sans sténose
|
| 27 |
+
significative localisée.
|
| 28 |
+
Au total :
|
| 29 |
+
Discrète infiltration des axes proximaux anciens non significatifs localisés.
|
| 30 |
+
Axes jambiers plus présents.
|
| 31 |
+
|
| 32 |
+
Docteur Alain STRUK
|
data_txt/template1030.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BILAN PRE-INTERVENTIONNEL
|
| 2 |
+
|
| 3 |
+
Indication : repérage; examen scanographique. Lésion cible : mesurée à 2,5 cm. Trajet d'abord : sécuritaire. Structures à risque : distantes. Conclusions : Gestes interventionnel réalisable. "
|