Compare commits
10 Commits
2ceb3c4916
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e55daf275e | ||
|
|
3a87751444 | ||
|
|
d326524e49 | ||
|
|
1255468676 | ||
|
|
c0b0cd9b87 | ||
|
|
6c8184cc03 | ||
|
|
b47f5c47e0 | ||
|
|
3f2e2ee9f4 | ||
|
|
7d45018139 | ||
|
|
7dc3eba1fc |
@@ -282,7 +282,7 @@
|
||||
"ghm_reco": "06M094",
|
||||
"ghs_reco": "2161",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR JP VIGNAU",
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -317,7 +317,7 @@
|
||||
"ghm_reco": "10M183",
|
||||
"ghs_reco": "3969",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "désaccord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
"ghm_reco": "06C042",
|
||||
"ghs_reco": "1940",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAÚ",
|
||||
"accord_desaccord": "désaccord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
"ghm_reco": "01C061",
|
||||
"ghs_reco": "34",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "",
|
||||
"accord_desaccord": "désaccord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -330,7 +330,7 @@
|
||||
"ghm_reco": "03M112",
|
||||
"ghs_reco": "861",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -371,7 +371,7 @@
|
||||
"ghm_reco": "04M093",
|
||||
"ghs_reco": "1163",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "désaccord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"ghm_reco": "1947",
|
||||
"ghs_reco": "06C071",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -324,7 +324,7 @@
|
||||
"ghm_reco": "23Z02Z",
|
||||
"ghs_reco": "7992",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -298,7 +298,7 @@
|
||||
"ghm_reco": "04M092",
|
||||
"ghs_reco": "1162",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0 SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "0",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "désaccord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
@@ -201,7 +201,7 @@
|
||||
"ghm_reco": "23Z02Z",
|
||||
"ghs_reco": "7992",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "SE 1 2 3 4 ATU FFM FSD",
|
||||
"ghs_injustifie": "",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {
|
||||
|
||||
262
output/v2_clean/OGC 1.json
Normal file
262
output/v2_clean/OGC 1.json
Normal file
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"fichier": "OGC 1",
|
||||
"pdf_hash": "4b2aacd453ec8903",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "1",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "16/02/2016 au 10/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "79",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "23"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "79",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "23"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "53 C",
|
||||
"igs": "0",
|
||||
"duree": "23",
|
||||
"dates": "du 16/02/2016 au 10/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "K650",
|
||||
"dp_libelle": "PERITONITE AIG.",
|
||||
"dr": "B966 * 4 BACILLUS FRAGILIS, CAUSE DE MAL. CLASSES DANS D'AUTRES CHAP.",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "06M093",
|
||||
"ghs_etab": "2160",
|
||||
"ghm_reco": "06M094",
|
||||
"ghs_reco": "2161",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR JP VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 6,
|
||||
"invalid": 0,
|
||||
"empty": 2,
|
||||
"total_codes": 6,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Péritonite aigüe"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Bacillus fragilis, cause de maladies classées dans d'autres chapitres"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "2160",
|
||||
"ghs_avant_concertation": "2160",
|
||||
"ghs_final": "2161",
|
||||
"decision": "maintien_avis_controleur",
|
||||
"date_concertation": "15.3.2018",
|
||||
"praticien_controleur": "DR JP VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "2160",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "2160",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "2161",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "15.3",
|
||||
"argumentaire": "102 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre IV, paragraphe 2.1 : « L'enregistrement dans le RUM d'une affection correspondant à la définition d'un DAS est obligatoire ». Au vu des éléments du dossier du patient, le codage d'un diagnostic correspondant à la définition d'un DAS a été omis par l'établissement."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "26/03/2018",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:10:02+00:00"
|
||||
}
|
||||
}
|
||||
286
output/v2_clean/OGC 18.json
Normal file
286
output/v2_clean/OGC 18.json
Normal file
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"fichier": "OGC 18",
|
||||
"pdf_hash": "0e7a44525007199c",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "18",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "08/03/2016 au 21/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "13 8",
|
||||
"mode_entree": "",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "13 8",
|
||||
"mode_entree": "",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "13",
|
||||
"dates": "du 08/03/2016 au 21/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "E43",
|
||||
"dp_libelle": "MALNUTRITION PROTEINO-ENERGETIQUE GRAVE, SAI",
|
||||
"dr": "J860 4 PYOTHORAX AVEC FISTULE",
|
||||
"das": [
|
||||
{
|
||||
"code": "T858",
|
||||
"position": "2",
|
||||
"libelle": "COMPLIC. DE PROTH., IMPL., GREF. INT., NCA"
|
||||
},
|
||||
{
|
||||
"code": "Z511",
|
||||
"position": "3",
|
||||
"libelle": "SEANCE DE CHIMIOTHERAPIE POUR TUM."
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "10M184",
|
||||
"ghs_etab": "3970",
|
||||
"ghm_reco": "10M183",
|
||||
"ghs_reco": "3969",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 7,
|
||||
"invalid": 1,
|
||||
"empty": 2,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Malnutrition protéinoénergétique grave, sans précision"
|
||||
},
|
||||
"dr": {
|
||||
"valid": false
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres complications de prothèses, implants et greffes internes, non classées ailleurs"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Séance de chimiothérapie pour tumeur"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "3970",
|
||||
"ghs_avant_concertation": "3969",
|
||||
"ghs_final": "3969",
|
||||
"decision": "",
|
||||
"date_concertation": "2.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "3970",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "3969",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "3969",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3.18",
|
||||
"argumentaire": "105 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre IV, paragraphe 2.1 : « Lorsqu'un patient atteint d'une maladie chronique ou de longue durée en cours de traitement est hospitalisé pour un autre motif, la maladie chronique ou de longue durée est naturellement un DAS, à moins qu'elle n'ait pas bénéficié d'une surveillance et que son traitement ait été interrompu pendant le séjour. (...) Les informations attestant de la majoration de l'effort de soins devant figurer dans le dossier médical. » Au vu des éléments du dossier du patient, la maladie chronique ou de longue durée codée en DAS par l'établissement n'a bénéficié d'aucune prise en charge diagnostique ou thérapeutique, ni majoré l'effort de prise en charge d'une autre affection."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "2018-03-13",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": true,
|
||||
"photocopie": true,
|
||||
"absent_date": "1ère demande",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": true,
|
||||
"photocopie": true,
|
||||
"absent_date": "1 à 4",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:10:41+00:00"
|
||||
}
|
||||
}
|
||||
188
output/v2_clean/OGC 20.json
Normal file
188
output/v2_clean/OGC 20.json
Normal file
@@ -0,0 +1,188 @@
|
||||
{
|
||||
"fichier": "OGC 20",
|
||||
"pdf_hash": "eb280d07819ff75d",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "20",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "09/03/2016 au 18/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "57",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9",
|
||||
"mode_entree": "8",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "57",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9",
|
||||
"mode_entree": "8",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "0",
|
||||
"igs": "53 C",
|
||||
"duree": "9",
|
||||
"dates": "du 09/03/2016 au 18/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "D374",
|
||||
"dr": "I022",
|
||||
"das": [
|
||||
{
|
||||
"code": "T814",
|
||||
"position": "3",
|
||||
"libelle": "INFECT. APRES UN ACTE, NCA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "D374",
|
||||
"dr": "I022",
|
||||
"das": [
|
||||
{
|
||||
"code": "T814",
|
||||
"position": "3",
|
||||
"libelle": "INFECT. APRES UN ACTE, NCA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "06C043",
|
||||
"ghs_etab": "1941",
|
||||
"ghm_reco": "06C042",
|
||||
"ghs_reco": "1940",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAÚ",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 2,
|
||||
"empty": 0,
|
||||
"total_codes": 10,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Côlon"
|
||||
},
|
||||
"dr": {
|
||||
"valid": false,
|
||||
"suggestion": "A022"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Infection après un acte à visée diagnostique et thérapeutique, non classée ailleurs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Côlon"
|
||||
},
|
||||
"dr": {
|
||||
"valid": false,
|
||||
"suggestion": "A022"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Infection après un acte à visée diagnostique et thérapeutique, non classée ailleurs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "",
|
||||
"ghs_avant_concertation": "",
|
||||
"ghs_final": "",
|
||||
"decision": "",
|
||||
"date_concertation": "2.3.18",
|
||||
"praticien_controleur": "",
|
||||
"medecin_dim": "",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3.18",
|
||||
"argumentaire": "Désaccord : (les éléments couverts par le secret médical sont à mentionner sur la fiche médicale de concertation)"
|
||||
},
|
||||
"preuves": {
|
||||
"date": "",
|
||||
"praticien_controleur": "",
|
||||
"medecin_dim": "",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Patient de 57 ans, puis en charge pour une colo-rectalgie et l'anapath. Confirme le Kc du colon.",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "En post-op, présence d'un escoulement au niveau du bas de la muqueuse ; retrait d'un aquafili et luchage.",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Il n'a pas d'abcès : une déficatga. Des JAS en T81.8. Pas de moton. Disphagie restaurée, pas de prétendu ad. de l'escoulement.",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:11:05+00:00"
|
||||
}
|
||||
}
|
||||
273
output/v2_clean/OGC 27.json
Normal file
273
output/v2_clean/OGC 27.json
Normal file
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"fichier": "OGC 27",
|
||||
"pdf_hash": "2b6010d745556c81",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "27",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "13/03/2016 - 16/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "55",
|
||||
"sexe": "M",
|
||||
"duree_sejour": "8"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "55",
|
||||
"sexe": "M",
|
||||
"duree_sejour": "8"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "53 C",
|
||||
"igs": "0",
|
||||
"duree": "3",
|
||||
"dates": "du 13/03/2016 au 16/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "I652",
|
||||
"dp_libelle": "STENOSE DE L'ART. CAROTIDE",
|
||||
"dr": "F412 2 TBL. ANXIUE ET DEPRES. MIXTE R471 2 DYSARTHRIE ET ANARTHRIE"
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [
|
||||
{
|
||||
"code": "EBFA012",
|
||||
"position": "1",
|
||||
"libelle": "THROMBOENDARTERIECTOMIE SIMPLE BIFURC. CAROTID. CERV.TOMIE"
|
||||
},
|
||||
{
|
||||
"code": "EBFA012",
|
||||
"position": "4",
|
||||
"libelle": "THROMBOENDARTERIECTOMIE SIMPLE BIFURC. CAROTID. CERV.TOMIE"
|
||||
}
|
||||
],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "01C062",
|
||||
"ghs_etab": "35",
|
||||
"ghm_reco": "01C061",
|
||||
"ghs_reco": "34",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 7,
|
||||
"invalid": 1,
|
||||
"empty": 2,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Occlusion et sténose de l'artère carotide"
|
||||
},
|
||||
"dr": {
|
||||
"valid": false
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "35",
|
||||
"ghs_avant_concertation": "34",
|
||||
"ghs_final": "33",
|
||||
"decision": "",
|
||||
"date_concertation": "1.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "35",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "34",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "33",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "13.2018",
|
||||
"argumentaire": "100 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « Les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de maladies. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM10 choisi pour le DAS par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "28/07/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": [
|
||||
"Dr ETTORCHI-TARDY"
|
||||
],
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'inagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "3-5-5",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:11:43+00:00"
|
||||
}
|
||||
}
|
||||
143
output/v2_clean/OGC 29.json
Normal file
143
output/v2_clean/OGC 29.json
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
"fichier": "OGC 29",
|
||||
"pdf_hash": "0347fc1e23968220",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "29",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "14/03/2016 au 17/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "82",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "3"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "82",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "3"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "3",
|
||||
"dates": "du 14/03/2016 au 17/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "K635+0",
|
||||
"dp_libelle": "POLYPOSE HYPERPLASIQUE",
|
||||
"dr": "D509"
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "E46",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "06M092",
|
||||
"ghs_etab": "2159",
|
||||
"ghm_reco": "16M112",
|
||||
"ghs_reco": "6183",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 7,
|
||||
"invalid": 0,
|
||||
"empty": 1,
|
||||
"total_codes": 7,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Polype du côlon"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Anémie par carence en fer, sans précision"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Malnutrition protéinoénergétique, sans précision"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "2159",
|
||||
"ghs_avant_concertation": "6183",
|
||||
"ghs_final": "6183",
|
||||
"decision": "",
|
||||
"date_concertation": "1.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "2159",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "6183",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "6183",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "1.3.18",
|
||||
"argumentaire": "109 : La facturation du GHs par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "",
|
||||
"praticien_controleur": true,
|
||||
"medecin_dim": true,
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Séjour d'hospitalisation complète",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:12:07+00:00"
|
||||
}
|
||||
}
|
||||
174
output/v2_clean/OGC 43.json
Normal file
174
output/v2_clean/OGC 43.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"fichier": "OGC 43",
|
||||
"pdf_hash": "9f58db479a06194b",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "43",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "16/03/2016 au 22/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "50",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "6",
|
||||
"mode_entree": "7",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "1",
|
||||
"destination": "8"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "50",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "6",
|
||||
"mode_entree": "7",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "1",
|
||||
"destination": "8"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "6",
|
||||
"dates": "du 16/03/2016 au 22/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "G409",
|
||||
"dp_libelle": "EPILEP., SAI",
|
||||
"dr": "K566",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "Z022",
|
||||
"dr": "G409",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "01M253",
|
||||
"ghs_etab": "292",
|
||||
"ghm_reco": "01M32Z",
|
||||
"ghs_reco": "324",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 0,
|
||||
"empty": 0,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Épilepsie, sans précision"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Occlusions intestinales, autres et sans précision"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Examen pour l'admission dans une institution"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Épilepsie, sans précision"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "292",
|
||||
"ghs_avant_concertation": "324",
|
||||
"ghs_final": "324",
|
||||
"decision": "",
|
||||
"date_concertation": "1.3.2018",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "292",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "324",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "324",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "1.3.2018",
|
||||
"argumentaire": "109 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "",
|
||||
"praticien_controleur": "",
|
||||
"medecin_dim": "",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Patide de 50 ans",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Admision par : - prise en charge de crises d'épilepsie - excision p canne, dit l'épileptol.",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Syndrome acclif so costalpin quonchée - ch 1 petite ay5 en cancer du sein, dit jo chimiotherapie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Bime effectuer",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:12:34+00:00"
|
||||
}
|
||||
}
|
||||
298
output/v2_clean/OGC 55.json
Normal file
298
output/v2_clean/OGC 55.json
Normal file
@@ -0,0 +1,298 @@
|
||||
{
|
||||
"fichier": "OGC 55",
|
||||
"pdf_hash": "948142f13e05f3bb",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "55",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "27/03/2016 au 01/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "78",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "",
|
||||
"mode_entree": "0",
|
||||
"provenance": "1",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "78",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "",
|
||||
"mode_entree": "0",
|
||||
"provenance": "1",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "5",
|
||||
"dates": "du 27/03/2016 au 01/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "K123",
|
||||
"dp_libelle": "MUCITE BUCCALE",
|
||||
"dr": "",
|
||||
"das": [
|
||||
{
|
||||
"code": "C795",
|
||||
"position": "2",
|
||||
"libelle": "T.M. IRE, DES OS ET DE LA MOELLE OSSEUSE"
|
||||
},
|
||||
{
|
||||
"code": "D611",
|
||||
"position": "4",
|
||||
"libelle": "APIASIE MEDULLAIRE MEDICAM."
|
||||
},
|
||||
{
|
||||
"code": "R630",
|
||||
"position": "2",
|
||||
"libelle": "ANOREXIE"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "03M114",
|
||||
"ghs_etab": "863",
|
||||
"ghm_reco": "03M112",
|
||||
"ghs_reco": "861",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 0,
|
||||
"empty": 3,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Mucite buccale (ulcéreuse)"
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Anémie médullaire [aplastique] médicamenteuse"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Anorexie"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "863",
|
||||
"ghs_avant_concertation": "861",
|
||||
"ghs_final": "861",
|
||||
"decision": "",
|
||||
"date_concertation": "9.2.1",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "863",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "861",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "861",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "9.3.18",
|
||||
"argumentaire": "100 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « Les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) ». Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM10 choisi pour le DAS par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "26/02/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire : Biologie",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "2 à 18",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:13:12+00:00"
|
||||
}
|
||||
}
|
||||
333
output/v2_clean/OGC 66.json
Normal file
333
output/v2_clean/OGC 66.json
Normal file
@@ -0,0 +1,333 @@
|
||||
{
|
||||
"fichier": "OGC 66",
|
||||
"pdf_hash": "7176836d9d496cd7",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "66",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "04/04/2016 au 20/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "80 ans",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "16 jours",
|
||||
"mode_entree": "8",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "8",
|
||||
"destination": "0"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "80 ans",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "16 jours",
|
||||
"mode_entree": "8",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "8",
|
||||
"destination": "0"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "16 jours",
|
||||
"dates": "du 04/04/2016 au 20/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "C07",
|
||||
"dp_libelle": "T.M. DE LA GLD, PAROTIDE",
|
||||
"dr": "C795 *",
|
||||
"das": [
|
||||
{
|
||||
"code": "E440",
|
||||
"position": "2",
|
||||
"libelle": "MALNUTRITION PROTEINO-ENERGETIQUE MODEREE"
|
||||
},
|
||||
{
|
||||
"code": "J91",
|
||||
"position": "2",
|
||||
"libelle": "EPANCHEMENT PLEURAL AVEC MAL. CL. AILL."
|
||||
},
|
||||
{
|
||||
"code": "Z511",
|
||||
"position": "3",
|
||||
"libelle": "SEANCE DE CHIMIOTHERAPIE POUR TUM."
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "C07",
|
||||
"dr": "C795 *",
|
||||
"das": [
|
||||
{
|
||||
"code": "E440",
|
||||
"position": "2",
|
||||
"libelle": "MALNUTRITION PROTEINO-ENERGETIQUE MODEREE"
|
||||
},
|
||||
{
|
||||
"code": "J91",
|
||||
"position": "2",
|
||||
"libelle": "EPANCHEMENT PLEURAL AVEC MAL. CL. AILL."
|
||||
},
|
||||
{
|
||||
"code": "Z511",
|
||||
"position": "3",
|
||||
"libelle": "SEANCE DE CHIMIOTHERAPIE POUR TUM."
|
||||
}
|
||||
]
|
||||
},
|
||||
"actes_etab": [
|
||||
{
|
||||
"code": "EBLA003",
|
||||
"position": "1",
|
||||
"libelle": "POSE CATHE RELIE - 1VN PROF. MB SUP/COU TRANSCUT+DIFFUSEUR SSCUT."
|
||||
}
|
||||
],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "03M073",
|
||||
"ghs_etab": "844",
|
||||
"ghm_reco": "04M093",
|
||||
"ghs_reco": "1163",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 15,
|
||||
"invalid": 0,
|
||||
"empty": 0,
|
||||
"total_codes": 15,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne de la glande parotide"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Malnutrition protéinoénergétique modérée"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Épanchement pleural au cours de maladies classées ailleurs"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Séance de chimiothérapie pour tumeur"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne de la glande parotide"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Malnutrition protéinoénergétique modérée"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Épanchement pleural au cours de maladies classées ailleurs"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Séance de chimiothérapie pour tumeur"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "844",
|
||||
"ghs_avant_concertation": "1163",
|
||||
"ghs_final": "163",
|
||||
"decision": "",
|
||||
"date_concertation": "2.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "844",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "1163",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "163",
|
||||
"valid": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3.19",
|
||||
"argumentaire": "La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "28/07/2019",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr ETTORCHI-TARDY"
|
||||
],
|
||||
"medecin_dim": "",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre : lettre M",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:13:54+00:00"
|
||||
}
|
||||
}
|
||||
298
output/v2_clean/OGC 68.json
Normal file
298
output/v2_clean/OGC 68.json
Normal file
@@ -0,0 +1,298 @@
|
||||
{
|
||||
"fichier": "OGC 68",
|
||||
"pdf_hash": "53423419517fca40",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "68",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "07/04/2016 au 11/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "63",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "4",
|
||||
"mode_entree": "7",
|
||||
"provenance": "1",
|
||||
"mode_sortie": "7",
|
||||
"destination": "1"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "63",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "4",
|
||||
"mode_entree": "7",
|
||||
"provenance": "1",
|
||||
"mode_sortie": "7",
|
||||
"destination": "1"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "40 C",
|
||||
"igs": "0",
|
||||
"duree": "4",
|
||||
"dates": "du 07/04/2016 au 11/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "R650",
|
||||
"dp_libelle": "SYND. REPONSE INFLAM. SYST. ORIGINE INFECT. SANS DEFAILLANCE ORG",
|
||||
"dr": "",
|
||||
"das": [
|
||||
{
|
||||
"code": "D508",
|
||||
"position": "2",
|
||||
"libelle": "ANEMIES PAR CARENCHE EN FER, NCA"
|
||||
},
|
||||
{
|
||||
"code": "E8758",
|
||||
"position": "2",
|
||||
"libelle": "HYPERKALIEMIES, NCA AT SAI"
|
||||
},
|
||||
{
|
||||
"code": "K868",
|
||||
"position": "2",
|
||||
"libelle": "MAL. PREC. DU PANCREAS, NCA"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "18M042",
|
||||
"ghs_etab": "6773",
|
||||
"ghm_reco": "07M112",
|
||||
"ghs_reco": "2550",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 0,
|
||||
"empty": 3,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Syndrome de réponse inflammatoire systémique d'origine infectieuse sans défaillance d'organe"
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres anémies par carence en fer"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Hyperkaliémies, autres et sans précision"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres maladies précisées du pancréas"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "6773",
|
||||
"ghs_avant_concertation": "6973-3",
|
||||
"ghs_final": "2550",
|
||||
"decision": "maintien_avis_controleur",
|
||||
"date_concertation": "16.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "6773",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "6973-3",
|
||||
"valid": false
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "2550",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "16.3.18",
|
||||
"argumentaire": "109 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "28/06/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:14:34+00:00"
|
||||
}
|
||||
}
|
||||
270
output/v2_clean/OGC 69.json
Normal file
270
output/v2_clean/OGC 69.json
Normal file
@@ -0,0 +1,270 @@
|
||||
{
|
||||
"fichier": "OGC 69",
|
||||
"pdf_hash": "75cf0c18f2090864",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "69",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "10/04/2016 au 13/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "87",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "3",
|
||||
"mode_entree": "8",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "87",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "3",
|
||||
"mode_entree": "8",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "3",
|
||||
"dates": "du 10/04/2016 au 13/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "K623",
|
||||
"dp_libelle": "PROPLAPUS RECTAL",
|
||||
"dr": "G20",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "1948",
|
||||
"ghs_etab": "06C072",
|
||||
"ghm_reco": "1947",
|
||||
"ghs_reco": "06C071",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 2,
|
||||
"invalid": 4,
|
||||
"empty": 2,
|
||||
"total_codes": 6,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Prolapsus rectal"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Maladie de Parkinson"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": false
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": false
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": false
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": false
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": null,
|
||||
"reco_ghm_ghs_coherent": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "1948",
|
||||
"ghs_avant_concertation": "1947",
|
||||
"ghs_final": "1947",
|
||||
"decision": "",
|
||||
"date_concertation": "13-02",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "1948",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "1947",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "1947",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "1 - 3 - 18",
|
||||
"argumentaire": "104 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre IV, paragraphe 2.1 : « Un diagnostic associé est significatif s'il est pris en charge à titre diagnostique ou thérapeutique ou s'il majoré l'effort de prise en charge d'une autre affection. Par prise en charge diagnostique on entend la mise en œuvre de moyens nécessaires au diagnostic d'une affection nouvelle ou au « bilan » d'une affection préexistante. Par prise en charge thérapeutique on entend la réalisation d'un traitement. Par majoration de l'effort de prise en charge d'une autre affection on entend l'augmentation imposée par une affection B de l'effort de soins relatif à une affection A enregistrée comme diagnostic principal (DP), diagnostic relié (DR) ou DAS, par rapport à ce qu'il aurait dû être en l'absence de B. Si l'affection B, quoique non prise en charge à titre diagnostique ou thérapeutique, a néanmoins alourdi la prise en charge de A, alors B est un DAS. (...) Ne doivent pas être retenues comme significatives les affections ne respectant pas la définition, par exemple, les antécédents guéris, les maladies stabilisées ou les facteurs de risque n'ayant bénéficié d'aucune prise en charge ». Au vu des éléments du dossier du patient, le DAS choisi par l'établissement ne peut pas être codé, ce diagnostic associé n'ayant nécessité aucune prise en charge documentée au dossier."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "2017",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:15:13+00:00"
|
||||
}
|
||||
}
|
||||
263
output/v2_clean/OGC 7.json
Normal file
263
output/v2_clean/OGC 7.json
Normal file
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"fichier": "OGC 7",
|
||||
"pdf_hash": "e8dc0ec994333b18",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "7",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "01/03/2016 au 04/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "75",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "3"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "75",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "3"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "40 C",
|
||||
"igs": "0",
|
||||
"duree": "3",
|
||||
"dates": "du 01/03/2016 au 04/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "TS10",
|
||||
"dp_libelle": "HEMORR. ET HEMATOME COMPLIQ. UN ACTE, NCA",
|
||||
"dr": "R33",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "R33",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "21M162",
|
||||
"ghs_etab": "7610",
|
||||
"ghm_reco": "11M122",
|
||||
"ghs_reco": "4323",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 6,
|
||||
"invalid": 1,
|
||||
"empty": 1,
|
||||
"total_codes": 7,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": false,
|
||||
"suggestion": "T010"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Rétention d'urine"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Rétention d'urine"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "7610",
|
||||
"ghs_avant_concertation": "4323",
|
||||
"ghs_final": "4323",
|
||||
"decision": "",
|
||||
"date_concertation": "1.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "7610",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "4323",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "4323",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "1.3",
|
||||
"argumentaire": "109 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de maladies. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "26/04/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:15:44+00:00"
|
||||
}
|
||||
}
|
||||
272
output/v2_clean/OGC 74.json
Normal file
272
output/v2_clean/OGC 74.json
Normal file
@@ -0,0 +1,272 @@
|
||||
{
|
||||
"fichier": "OGC 74",
|
||||
"pdf_hash": "58076293464e9771",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "74",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "08/04/2016 au 13/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "52",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "5",
|
||||
"mode_entree": "1",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "1",
|
||||
"destination": "8"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "52",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "5",
|
||||
"mode_entree": "1",
|
||||
"provenance": "8",
|
||||
"mode_sortie": "1",
|
||||
"destination": "8"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "40 C",
|
||||
"igs": "II",
|
||||
"duree": "5",
|
||||
"dates": "du 08/04/2016 au 13/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "A099",
|
||||
"dp_libelle": "GASTROENTERITE COLITE ORIGINE SAI",
|
||||
"dr": "C795 * 2",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "A099",
|
||||
"dr": "C795 * 2",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "06M032",
|
||||
"ghs_etab": "2130",
|
||||
"ghm_reco": "18M041",
|
||||
"ghs_reco": "6772",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR JP VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 0,
|
||||
"empty": 0,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Gastroentérite et colite d’origine non précisée"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Gastroentérite et colite d’origine non précisée"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "",
|
||||
"ghs_avant_concertation": "",
|
||||
"ghs_final": "",
|
||||
"decision": "",
|
||||
"date_concertation": "2-3",
|
||||
"praticien_controleur": "",
|
||||
"medecin_dim": "",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "",
|
||||
"valid": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3.18",
|
||||
"argumentaire": "Atteste avoir pris connaissance des éléments du dossier y compris ceux couverts par le secret médical et des arguments soutenus par les médecins contrôleurs et avoir eu l'opportunité d'en débattre contradictoirement"
|
||||
},
|
||||
"preuves": {
|
||||
"date": "2023/01/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte : TDP TAP TRN",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire :",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie :",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre : b-c",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:16:17+00:00"
|
||||
}
|
||||
}
|
||||
262
output/v2_clean/OGC 76.json
Normal file
262
output/v2_clean/OGC 76.json
Normal file
@@ -0,0 +1,262 @@
|
||||
{
|
||||
"fichier": "OGC 76",
|
||||
"pdf_hash": "96f8ce5a1672aad9",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "76",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "05/04/2016 au 11/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "54",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "6"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "54",
|
||||
"sexe": "2",
|
||||
"duree_sejour": "6"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "6",
|
||||
"dates": "du 05/04/2016 au 11/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "F329",
|
||||
"dp_libelle": "EPISODE DEPRES., SAI",
|
||||
"dr": "Z511",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "19M113",
|
||||
"ghs_etab": "7086",
|
||||
"ghm_reco": "17M061",
|
||||
"ghs_reco": "6487",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 6,
|
||||
"invalid": 0,
|
||||
"empty": 2,
|
||||
"total_codes": 6,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Épisode dépressif, sans précision"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Séance de chimiothérapie pour tumeur"
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "7086",
|
||||
"ghs_avant_concertation": "6487",
|
||||
"ghs_final": "6487",
|
||||
"decision": "maintien_avis_controleur",
|
||||
"date_concertation": "1.3.19",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "7086",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "6487",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "6487",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "13.2018",
|
||||
"argumentaire": "124: La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. En préalable, chapitre VI, paragraphe 1.2 : « Les circonstances du diagnostic préalable n'importent pas (...) La situation de traitement est présente lorsque le diagnostic de l'affection est fait au moment de l'entrée du patient dans l'unité médicale et que l'admission a pour but le traitement de l'affection. » Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre VI, paragraphe 1.2.1 : « La dénomination traitement répétitif rassemble les traitements qui, par nature, imposent une administration répétitive. (...) Dans les situations de traitement répétitif le codage du DP utilise des codes du chapitre XXI de la CIM-10 (« codes Z »). [Règle T1]. La règle est la même si la prise en charge, incidentellement, n'a lieu qu'une fois : c'est la nature du traitement qui est prise en considération. (...) Les séjours pour chimiothérapie, radiothérapie, transfusion sanguine, apheresis sanguine, oxygénothérapie hyperbare, injection de fer (pour carence martiale) qu'il s'agisse de séances ou d'hospitalisation complète, doivent avoir en position de DP le code adéquat de la catégorie Z51 de la CIM10. » Au vu des éléments présents dans le dossier du patient, alors que l'affection a été motivée par un traitement correspondant à la définition de traitement répétitif, l'établissement n'a pas retenu en DP le code du chapitre XXI de la CIM-10 en Z imposé par l'annexe II."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "27/04/2018",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": true,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre : CR psychologue",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:16:57+00:00"
|
||||
}
|
||||
}
|
||||
300
output/v2_clean/OGC 84.json
Normal file
300
output/v2_clean/OGC 84.json
Normal file
@@ -0,0 +1,300 @@
|
||||
{
|
||||
"fichier": "OGC 84",
|
||||
"pdf_hash": "0f61532689f119c5",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "84",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "07/04/2016 au 16/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "47",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "47",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "0",
|
||||
"igs": "0",
|
||||
"duree": "9",
|
||||
"dates": "du 07/04/2016 au 16/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "C257",
|
||||
"dp_libelle": "T.M. D'AUTRES PARTIES DU PANCREAS",
|
||||
"dr": "C787 *",
|
||||
"das": [
|
||||
{
|
||||
"code": "N179",
|
||||
"position": "2",
|
||||
"libelle": "T.M. IRE. DU FOIE ET V.B. INTRAHEP."
|
||||
},
|
||||
{
|
||||
"code": "R18",
|
||||
"position": "2",
|
||||
"libelle": "ASCITE"
|
||||
},
|
||||
{
|
||||
"code": "R410",
|
||||
"position": "2",
|
||||
"libelle": "DESORIENTATION, SAI"
|
||||
},
|
||||
{
|
||||
"code": "Z515",
|
||||
"position": "3",
|
||||
"libelle": "SOINS PALLIATIFS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "07M063",
|
||||
"ghs_etab": "2526",
|
||||
"ghm_reco": "23Z02Z",
|
||||
"ghs_reco": "7992",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 10,
|
||||
"invalid": 0,
|
||||
"empty": 2,
|
||||
"total_codes": 10,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres parties du pancréas"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire du foie et des voies biliaires intrahépatiques"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Insuffisance rénale aigüe, sans précision"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Ascite"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Désorientation, sans précision"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Soins palliatifs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "2526",
|
||||
"ghs_avant_concertation": "7992",
|
||||
"ghs_final": "7992",
|
||||
"decision": "maintien_avis_controleur",
|
||||
"date_concertation": "9.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "2526",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "7992",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "7992",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "9.3.18",
|
||||
"argumentaire": "136 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre VI, paragraphe 1.2.2.3, dans certaines situations de traitement unique médical, en exception à la règle générale, le DP est imposé [Règle T11]. A noter que « la maladie traitée est enregistrée comme diagnostic relié chaque fois qu'elle respecte sa définition. » Au vu des éléments présents dans le dossier du patient, alors que l'admission a été motivée pour une situation de traitement unique médical telle que décrite dans le chapitre VI, paragraphe 1.2.2.3 de l'annexe II dont le code CIM-10 est imposé, l'établissement n'a pas appliqué la règle T11 pour le codage du DP."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "01/03/18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:17:36+00:00"
|
||||
}
|
||||
}
|
||||
269
output/v2_clean/OGC 86.json
Normal file
269
output/v2_clean/OGC 86.json
Normal file
@@ -0,0 +1,269 @@
|
||||
{
|
||||
"fichier": "OGC 86",
|
||||
"pdf_hash": "ff2bb027e50bd9f6",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "86",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "14/04/2016 au 20/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "75",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "",
|
||||
"mode_entree": "1",
|
||||
"provenance": "",
|
||||
"mode_sortie": "9",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "75",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "",
|
||||
"mode_entree": "1",
|
||||
"provenance": "",
|
||||
"mode_sortie": "9",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "",
|
||||
"igs": "",
|
||||
"duree": "",
|
||||
"dates": ""
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "C349",
|
||||
"dp_libelle": "T.M. DE BRONCHE OU DU POUMON, SAI",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "04M093",
|
||||
"ghs_etab": "1163",
|
||||
"ghm_reco": "04M092",
|
||||
"ghs_reco": "1162",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 5,
|
||||
"invalid": 0,
|
||||
"empty": 3,
|
||||
"total_codes": 5,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Bronche ou poumon, sans précision"
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "1163",
|
||||
"ghs_avant_concertation": "1162",
|
||||
"ghs_final": "A-162",
|
||||
"decision": "",
|
||||
"date_concertation": "2.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "1163",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "1162",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "A-162",
|
||||
"valid": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3",
|
||||
"argumentaire": "105 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur un diagnostic associé significatif (DAS) codé par l'établissement dans le résumé d'unité médicale (RUM). Ce DAS n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre IV, paragraphe 2.1 : « Lorsqu'un patient atteint d'une maladie chronique ou de longue durée est naturellement un DAS, à moins qu'elle n'ait pas bénéficié d'une surveillance et que son traitement ait été interrompu pendant le séjour. (...) Les informations attestant de la majoration de l'effort de soins devant figurer dans le dossier médical. » Au vu des éléments du dossier du patient, la maladie chronique ou de longue durée codée en DAS par l'établissement n'a bénéficié d'aucune prise en charge diagnostique ou thérapeutique, ni majoré l'effort de prise en charge d'une autre affection."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "28/12/14",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU",
|
||||
"Dr PROMAX"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:18:12+00:00"
|
||||
}
|
||||
}
|
||||
176
output/v2_clean/OGC 9.json
Normal file
176
output/v2_clean/OGC 9.json
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"fichier": "OGC 9",
|
||||
"pdf_hash": "eaa3cb08305ad415",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "9",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "01/03/2016 au 08/03/2016",
|
||||
"sejour_etab": {
|
||||
"age": "84",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "7",
|
||||
"mode_entree": "8",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "84",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "7",
|
||||
"mode_entree": "8",
|
||||
"provenance": "",
|
||||
"mode_sortie": "8",
|
||||
"destination": ""
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "53 C",
|
||||
"igs": "0",
|
||||
"duree": "7",
|
||||
"dates": "du 01/03/2016 au 08/03/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "C61",
|
||||
"dp_libelle": "T.M. DE LA PROSTATE",
|
||||
"dr": "",
|
||||
"das": [
|
||||
{
|
||||
"code": "",
|
||||
"position": "",
|
||||
"libelle": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": [
|
||||
{
|
||||
"code": "",
|
||||
"position": ""
|
||||
}
|
||||
]
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "12C042",
|
||||
"ghs_etab": "4519",
|
||||
"ghm_reco": "11C132",
|
||||
"ghs_reco": "4169",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 5,
|
||||
"invalid": 0,
|
||||
"empty": 5,
|
||||
"total_codes": 5,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne de la prostate"
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "4519",
|
||||
"ghs_avant_concertation": "4169",
|
||||
"ghs_final": "4169",
|
||||
"decision": "",
|
||||
"date_concertation": "13.8",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "4519",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "4169",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "4169",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "1.3.18",
|
||||
"argumentaire": "109 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "",
|
||||
"praticien_controleur": "",
|
||||
"medecin_dim": "",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Commentaires du médecin contrôleur",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Commentaires du médecin du DIM",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:18:38+00:00"
|
||||
}
|
||||
}
|
||||
171
output/v2_clean/OGC 97.json
Normal file
171
output/v2_clean/OGC 97.json
Normal file
@@ -0,0 +1,171 @@
|
||||
{
|
||||
"fichier": "OGC 97",
|
||||
"pdf_hash": "a3d49b8a6bdef1b8",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "97",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "20/04/2016 au 29/04/2016",
|
||||
"sejour_etab": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9 8",
|
||||
"mode_entree": "",
|
||||
"provenance": "9",
|
||||
"mode_sortie": "9",
|
||||
"destination": "1"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "9 8",
|
||||
"mode_entree": "",
|
||||
"provenance": "9",
|
||||
"mode_sortie": "9",
|
||||
"destination": "1"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "0",
|
||||
"igs": "0",
|
||||
"duree": "9",
|
||||
"dates": "du 20/04/2016 au 29/04/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "C186",
|
||||
"dp_libelle": "T.M. DU COLON DESCENDANT",
|
||||
"dr": "C787 * 2 T.M. IIRE. DU FOIE ET V.B. INTRAHEP.",
|
||||
"das": [
|
||||
{
|
||||
"code": "R18",
|
||||
"position": "2",
|
||||
"libelle": "ASCITE"
|
||||
},
|
||||
{
|
||||
"code": "Z515",
|
||||
"position": "3",
|
||||
"libelle": "SOINS PALLIATIFS"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "06M053",
|
||||
"ghs_etab": "2140",
|
||||
"ghm_reco": "23Z02Z",
|
||||
"ghs_reco": "7992",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "",
|
||||
"accord_desaccord": "accord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 8,
|
||||
"invalid": 0,
|
||||
"empty": 2,
|
||||
"total_codes": 8,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Côlon descendant"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire du foie et des voies biliaires intrahépatiques"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Ascite"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Soins palliatifs"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": null
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "2140",
|
||||
"ghs_avant_concertation": "7992",
|
||||
"ghs_final": "7942",
|
||||
"decision": "",
|
||||
"date_concertation": "2.3.18",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "2140",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "7992",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "7942",
|
||||
"valid": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "",
|
||||
"argumentaire": "136 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre VI, paragraphe 1.2.2.3, dans certaines situations de traitement unique médical, en exception à la règle générale, le DP est imposé (règle T11). A noter que « la maladie traitée est enregistrée comme diagnostic relié chaque fois qu'elle respecte sa définition. » Au vu des éléments présents dans le dossier du patient, alors que l'admission a été motivée pour une situation de traitement unique médical telle que décrite dans le chapitre VI, paragraphe 1.2.2.3 de l'annexe II dont le code CIM-10 est imposé, l'établissement n'a pas appliqué la règle T11 pour le codage du DP."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "29-avril",
|
||||
"praticien_controleur": "Pachis de 66 ans",
|
||||
"medecin_dim": "Admis par AEB, dcer, acité, guère en stage palliatif. Très : cancer céleste mal: métastatique traité depuis 3ans. Perte en charge: - ponction d'acite - pas en stage de la douleur, - sth doligiques et cédation. Dès le 29-avril.",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "État DP",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:19:05+00:00"
|
||||
}
|
||||
}
|
||||
372
output/v2_clean/OGC 99.json
Normal file
372
output/v2_clean/OGC 99.json
Normal file
@@ -0,0 +1,372 @@
|
||||
{
|
||||
"fichier": "OGC 99",
|
||||
"pdf_hash": "078ba737e95e7659",
|
||||
"schema_version": "2.0",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE D'ARCACHON",
|
||||
"finess": "330780206",
|
||||
"date_debut_controle": "13/02/2018",
|
||||
"n_ogc": "99",
|
||||
"n_champ": "1",
|
||||
"dates_sejour": "16/04/2016 au 06/05/2016",
|
||||
"sejour_etab": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "20"
|
||||
},
|
||||
"sejour_reco": {
|
||||
"age": "66",
|
||||
"sexe": "1",
|
||||
"duree_sejour": "20"
|
||||
},
|
||||
"rum_etab": {
|
||||
"um": "UM",
|
||||
"igs": "IGS II",
|
||||
"duree": "20",
|
||||
"dates": "du 16/04/2016 au 06/05/2016"
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": "T827",
|
||||
"dp_libelle": "INFECT. ET REAC. INFL. DUES A PROTH.",
|
||||
"dr": "B957 *",
|
||||
"das": [
|
||||
{
|
||||
"code": "B957",
|
||||
"position": "2",
|
||||
"libelle": "STAPHYLO. NICA, CAUSE DE MAL. CLASSES DANS D'AUTRES CHAP."
|
||||
},
|
||||
{
|
||||
"code": "B957",
|
||||
"position": "2",
|
||||
"libelle": "STAPHYLO. NICA, CAUSE DE MAL. CLASSES DANS D'AUTRES CHAP."
|
||||
},
|
||||
{
|
||||
"code": "C795",
|
||||
"position": "2",
|
||||
"libelle": "PSEUDOMONAS, CAUSE DE MAL. CLASSES DANS D'AUTRES CHAP."
|
||||
},
|
||||
{
|
||||
"code": "C795",
|
||||
"position": "2",
|
||||
"libelle": "PSEUDOMONAS, CAUSE DE MAL. CLASSES DANS D'AUTRES CHAP."
|
||||
},
|
||||
{
|
||||
"code": "C797",
|
||||
"position": "2",
|
||||
"libelle": "T.M. IRE. DES OS ET DE LA MOELLE OSSEUSE"
|
||||
},
|
||||
{
|
||||
"code": "C797",
|
||||
"position": "2",
|
||||
"libelle": "T.M. IRE. DES OS ET DE LA MOELLE OSSEUSE"
|
||||
},
|
||||
{
|
||||
"code": "D619",
|
||||
"position": "2",
|
||||
"libelle": "APLAISIE MEDULLAIRE, SAI"
|
||||
},
|
||||
{
|
||||
"code": "D619",
|
||||
"position": "2",
|
||||
"libelle": "APLAISIE MEDULLAIRE, SAI"
|
||||
},
|
||||
{
|
||||
"code": "R650",
|
||||
"position": "2",
|
||||
"libelle": "ANEMIE AVEC MAL. TUMORALES"
|
||||
},
|
||||
{
|
||||
"code": "R650",
|
||||
"position": "2",
|
||||
"libelle": "ANEMIE AVEC MAL. TUMORALES"
|
||||
},
|
||||
{
|
||||
"code": "Z290",
|
||||
"position": "2",
|
||||
"libelle": "ISOLEMENT"
|
||||
},
|
||||
{
|
||||
"code": "Z290",
|
||||
"position": "2",
|
||||
"libelle": "ISOLEMENT"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": "A415",
|
||||
"dr": "",
|
||||
"das": []
|
||||
},
|
||||
"actes_etab": [],
|
||||
"actes_reco": [],
|
||||
"ghm_etab": "21M164",
|
||||
"ghs_etab": "7612",
|
||||
"ghm_reco": "18M073",
|
||||
"ghs_reco": "6783",
|
||||
"recodage_impactant": "1",
|
||||
"ghs_injustifie": "0",
|
||||
"accord_desaccord": "désaccord",
|
||||
"praticien_conseil": "DR VIGNAU",
|
||||
"_validation": {
|
||||
"summary": {
|
||||
"valid": 19,
|
||||
"invalid": 0,
|
||||
"empty": 1,
|
||||
"total_codes": 19,
|
||||
"ghm_ghs_incoherents": 0
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Infection et réaction inflammatoire dues à d'autres prothèses, implants et greffes cardiaques et vasculaires"
|
||||
},
|
||||
"dr": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres staphylocoques, cause de maladies classées dans d'autres chapitres"
|
||||
},
|
||||
"das": [
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres staphylocoques, cause de maladies classées dans d'autres chapitres"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Autres staphylocoques, cause de maladies classées dans d'autres chapitres"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire des os et de la moelle osseuse"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire de la glande surrénale"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Tumeur maligne secondaire de la glande surrénale"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Anémie médullaire [aplastique], sans précision"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Anémie médullaire [aplastique], sans précision"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Syndrome de réponse inflammatoire systémique d'origine infectieuse sans défaillance d'organe"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Syndrome de réponse inflammatoire systémique d'origine infectieuse sans défaillance d'organe"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Isolement"
|
||||
},
|
||||
{
|
||||
"valid": true,
|
||||
"libelle_ref": "Isolement"
|
||||
}
|
||||
]
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": {
|
||||
"valid": true,
|
||||
"libelle_ref": "Sepsis à d'autres microorganismes Gram négatif"
|
||||
},
|
||||
"dr": {
|
||||
"valid": null
|
||||
},
|
||||
"das": []
|
||||
},
|
||||
"ghm_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_etab": {
|
||||
"valid": true
|
||||
},
|
||||
"ghm_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"ghs_reco": {
|
||||
"valid": true
|
||||
},
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": true,
|
||||
"reco_ghm_ghs_coherent": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "7612",
|
||||
"ghs_avant_concertation": "6783",
|
||||
"ghs_final": "6783",
|
||||
"decision": "maintien_avis_controleur",
|
||||
"date_concertation": "2.3.19",
|
||||
"praticien_controleur": "DR VIGNAU",
|
||||
"medecin_dim": "DR ETTORCHI-TARDY",
|
||||
"_validation": {
|
||||
"ghs_initial": {
|
||||
"code": "7612",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_avant_concertation": {
|
||||
"code": "6783",
|
||||
"valid": true
|
||||
},
|
||||
"ghs_final": {
|
||||
"code": "6783",
|
||||
"valid": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"concertation_1": {
|
||||
"date_concertation": "2.3",
|
||||
"argumentaire": "109 : La facturation du GHS par l'établissement n'est pas conforme à l'article 1 de l'arrêté du 19 février 2015 modifié du fait d'un non-respect des règles de codage édictées dans l'annexe II de l'arrêté du 21 décembre 2015 modifiant l'arrêté du 22 février 2008. Le non-respect des règles porte sur le diagnostic principal (DP) codé par l'établissement dans le résumé d'unité médicale (RUM). Le DP n'est pas conforme aux règles de codage des diagnostics rappelées par l'annexe II, chapitre V, paragraphe 1 : « les diagnostics doivent figurer dans le RUM sous forme codée selon la plus récente mise à jour de la 10e révision de la Classification internationale des maladies (CIM-10) de l'Organisation mondiale de la santé et selon les extensions nationales données dans la plus récente version du Manuel des groupes homogènes de malades. (...) Le meilleur code est le plus précis par rapport à l'information à coder. » Au vu des éléments présents dans le dossier du patient, le code CIM-10 choisi pour le DP par l'établissement n'est pas le plus précis par rapport à l'information à coder."
|
||||
},
|
||||
"preuves": {
|
||||
"date": "27.2.18",
|
||||
"praticien_controleur": [
|
||||
"Dr RADZIKOWSKI",
|
||||
"Dr DELAYE-PHULPIN",
|
||||
"Dr TURBAN",
|
||||
"Dr DUVAL",
|
||||
"Dr VIGNAU"
|
||||
],
|
||||
"medecin_dim": "Dr ETTORCHI-TARDY",
|
||||
"pieces": [
|
||||
{
|
||||
"intitule": "Compte-rendu d'acte",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "3 à 6",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu opératoire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'accouchement",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'examen complémentaire",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'imagerie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'anatomopathologie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "1er demande",
|
||||
"date_obtention": "16"
|
||||
},
|
||||
{
|
||||
"intitule": "Observations médicales",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier de transfusion",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Dossier d'anesthésie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Administration thérapeutique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Compte-rendu d'hospitalisation",
|
||||
"present": true,
|
||||
"photocopie": false,
|
||||
"absent_date": "1-2",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Lettre de sortie",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Eléments de surveillance du dossier infirmier",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge psychologue",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge kinésithérapeute",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Prise en charge diététique",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
},
|
||||
{
|
||||
"intitule": "Autre",
|
||||
"present": false,
|
||||
"photocopie": false,
|
||||
"absent_date": "",
|
||||
"date_obtention": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"_meta": {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"generated_at": "2026-04-24T12:19:48+00:00"
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,14 @@ CONCERTATION_2_DECISION = CheckboxZones(
|
||||
desaccord= (0.280, 0.270, 0.305, 0.290), # retour groupage DIM
|
||||
)
|
||||
|
||||
# Zones des 7 cases SE 1 / 2 / 3 / 4 / ATU / FFM / FSD (page recueil, en bas).
|
||||
# TODO : recalibrer avec des vrais cas positifs — sur 18 dossiers de
|
||||
# l'échantillon 2018, aucune case n'est cochée (`ghs_injustifie = 0` partout)
|
||||
# donc impossible de valider visuellement la détection. Laissé désactivé.
|
||||
GHS_INJUSTIFIE_CHECKBOXES: dict[str, tuple[float, float, float, float]] = {
|
||||
# placeholder — à recalibrer quand un cas positif sera observé
|
||||
}
|
||||
|
||||
|
||||
def dark_ratio(image: Image.Image, zone: tuple[float, float, float, float],
|
||||
inner_frac: float = INNER_FRAC) -> float:
|
||||
@@ -58,6 +66,31 @@ def dark_ratio(image: Image.Image, zone: tuple[float, float, float, float],
|
||||
return float(np.mean(gray < DARK_THRESHOLD))
|
||||
|
||||
|
||||
def parse_ghs_injustifie(raw: str) -> str:
|
||||
"""Extrait la valeur 0/1 du champ ghs_injustifie depuis la sortie OCR brute.
|
||||
|
||||
Qwen tend à recopier le libellé complet `0 SE 1 2 3 4 ATU FFM FSD` au lieu
|
||||
du seul chiffre. On prend le premier caractère qui est 0 ou 1 et on ignore
|
||||
le reste (les chiffres 1/2/3/4 qui suivent « SE » sont des numéros de case,
|
||||
pas la valeur du flag).
|
||||
"""
|
||||
if raw is None:
|
||||
return ""
|
||||
s = str(raw).strip()
|
||||
if not s:
|
||||
return ""
|
||||
# Si déjà propre (juste "0" ou "1"), retour direct
|
||||
if s in ("0", "1"):
|
||||
return s
|
||||
# Prendre le premier chiffre trouvé qui soit 0 ou 1, en ignorant tout
|
||||
# le reste (en particulier les "SE 1 2 3 4…" qui suivent)
|
||||
import re as _re
|
||||
m = _re.match(r"\s*([01])\b", s)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return "" # illisible / format inattendu
|
||||
|
||||
|
||||
def detect_accord_desaccord(
|
||||
image_path: str | Path,
|
||||
zones: CheckboxZones = RECUEIL_ACCORD_DESACCORD,
|
||||
|
||||
125
pipeline/deskew.py
Normal file
125
pipeline/deskew.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""Détection d'angle de skew + redressement automatique des pages scannées.
|
||||
|
||||
Technique : Hough Transform sur les lignes détectées par Canny, puis moyenne
|
||||
des angles des lignes « quasi horizontales » (±15° par rapport à l'horizontale).
|
||||
Les fiches OGC ont énormément de traits de tableau → signal très fort.
|
||||
|
||||
Seuil : on ne corrige que si |angle| > `MIN_ANGLE_DEG` (0.3° par défaut) pour
|
||||
éviter de toucher les scans déjà bien cadrés et introduire du bruit inutile.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore
|
||||
_HAS_CV2 = True
|
||||
except ImportError:
|
||||
_HAS_CV2 = False
|
||||
|
||||
|
||||
MIN_ANGLE_DEG = 0.3 # en-dessous, on ne corrige pas
|
||||
MAX_ANGLE_DEG = 10.0 # au-dessus, c'est anormal → suspect, on ne corrige pas
|
||||
NEAR_HORIZONTAL_BAND = 15.0 # degrés : bande autour de l'horizontale pour filtrer
|
||||
|
||||
|
||||
def detect_skew_angle(img: Image.Image) -> float:
|
||||
"""Retourne l'angle de skew en degrés (positif = tourné dans le sens
|
||||
des aiguilles d'une montre) à appliquer pour redresser l'image.
|
||||
|
||||
Si aucune ligne horizontale n'est trouvée, retourne 0.0.
|
||||
Si l'angle détecté est hors [-MAX_ANGLE_DEG, +MAX_ANGLE_DEG], retourne 0.0
|
||||
(probablement une erreur de détection, on ne corrige pas).
|
||||
"""
|
||||
if not _HAS_CV2:
|
||||
return 0.0
|
||||
gray = np.array(img.convert("L"))
|
||||
# Réduire l'image pour accélérer (max 1500 px de large)
|
||||
h, w = gray.shape
|
||||
if w > 1500:
|
||||
scale = 1500 / w
|
||||
gray = cv2.resize(gray, (1500, int(h * scale)), interpolation=cv2.INTER_AREA)
|
||||
|
||||
# Canny edges — paramètres standards documents
|
||||
edges = cv2.Canny(gray, 50, 150, apertureSize=3)
|
||||
# Hough Lines probabiliste : rapide et robuste
|
||||
lines = cv2.HoughLinesP(
|
||||
edges, rho=1, theta=np.pi / 180, threshold=200,
|
||||
minLineLength=gray.shape[1] // 4, # au moins 25% de la largeur
|
||||
maxLineGap=20,
|
||||
)
|
||||
if lines is None or len(lines) == 0:
|
||||
return 0.0
|
||||
|
||||
# Calculer l'angle de chaque ligne en degrés
|
||||
angles = []
|
||||
for line in lines:
|
||||
x1, y1, x2, y2 = line[0]
|
||||
if x2 == x1:
|
||||
continue # ligne verticale, ignorée
|
||||
angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
|
||||
# On ne garde que les lignes proches de l'horizontale
|
||||
if abs(angle) < NEAR_HORIZONTAL_BAND:
|
||||
angles.append(angle)
|
||||
|
||||
if not angles:
|
||||
return 0.0
|
||||
|
||||
# Moyenne robuste : médiane plutôt que mean, moins sensible aux outliers
|
||||
angle = float(np.median(angles))
|
||||
if abs(angle) > MAX_ANGLE_DEG:
|
||||
return 0.0 # suspect → on ne corrige pas
|
||||
return angle
|
||||
|
||||
|
||||
def deskew_image(img: Image.Image,
|
||||
angle: float | None = None,
|
||||
min_angle: float = MIN_ANGLE_DEG) -> Tuple[Image.Image, float]:
|
||||
"""Redresse une image si le skew détecté dépasse `min_angle`.
|
||||
|
||||
Retourne (image_eventuellement_rotee, angle_applique).
|
||||
Si |angle| < min_angle, retourne l'image inchangée et angle=0.0.
|
||||
"""
|
||||
if angle is None:
|
||||
angle = detect_skew_angle(img)
|
||||
if abs(angle) < min_angle:
|
||||
return img, 0.0
|
||||
# PIL.Image.rotate : positive angle = counter-clockwise
|
||||
# detect_skew retourne positif = clockwise → on inverse pour PIL
|
||||
rotated = img.rotate(
|
||||
angle,
|
||||
resample=Image.Resampling.BICUBIC,
|
||||
expand=False,
|
||||
fillcolor="white",
|
||||
)
|
||||
return rotated, angle
|
||||
|
||||
|
||||
def deskew_file(src: Path, dst: Path | None = None,
|
||||
min_angle: float = MIN_ANGLE_DEG) -> float:
|
||||
"""Version fichier → fichier. Écrase `src` si `dst` est None.
|
||||
Retourne l'angle appliqué (0.0 si pas de rotation)."""
|
||||
img = Image.open(src)
|
||||
rotated, angle = deskew_image(img, min_angle=min_angle)
|
||||
out = dst or src
|
||||
rotated.save(out, "PNG", optimize=True)
|
||||
return angle
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import glob
|
||||
paths = [Path(p) for p in (sys.argv[1:] or sorted(glob.glob(".cache/images/*/page_01.png")))]
|
||||
print(f"Deskew sur {len(paths)} images (seuil={MIN_ANGLE_DEG}°)...")
|
||||
total_corrected = 0
|
||||
for p in paths:
|
||||
angle = detect_skew_angle(Image.open(p))
|
||||
mark = "→" if abs(angle) >= MIN_ANGLE_DEG else "·"
|
||||
if abs(angle) >= MIN_ANGLE_DEG:
|
||||
total_corrected += 1
|
||||
print(f" {mark} {p} : {angle:+.2f}°")
|
||||
print(f"\n{total_corrected}/{len(paths)} images auraient besoin d'un redressement.")
|
||||
@@ -1,117 +1,84 @@
|
||||
"""Orchestration d'extraction pour un dossier OGC."""
|
||||
import json
|
||||
import re
|
||||
"""Orchestration d'extraction pour un dossier OGC.
|
||||
|
||||
Chaîne les étages du pipeline sans connaître leur implémentation interne :
|
||||
|
||||
ingest → routing → OCR page par page → enrichissement page-spécifique → validation ATIH
|
||||
|
||||
L'orchestration elle-même ne contient aucune logique métier : elle délègue à
|
||||
`pipeline.recueil`, `pipeline.validation`, `pipeline.classify`, `pipeline.ocr_qwen`
|
||||
et `pipeline.prompts`. Cela permet de tester indépendamment chaque étage.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from .ingest import pdf_to_images
|
||||
|
||||
from .classify import detect_page_type, route_by_index
|
||||
from .ingest import pdf_to_images
|
||||
from .json_utils import parse_json_output
|
||||
from .ocr_qwen import QwenVLOCR
|
||||
from .prompts import PAGE_TYPES, PROMPT_HEADER
|
||||
from .checkboxes import detect_accord_desaccord, RECUEIL_ACCORD_DESACCORD
|
||||
from .recueil import enrich_recueil, resolve_recueil_zones
|
||||
from .validation import annotate as validate_annotate
|
||||
|
||||
|
||||
_EMPTY_OBJ_PATTERN = re.compile(
|
||||
r'\{\s*"code"\s*:\s*""\s*,\s*"position"\s*:\s*""\s*(?:,\s*"libelle"\s*:\s*""\s*)?\}',
|
||||
re.DOTALL,
|
||||
)
|
||||
def _run_page_ocr(ocr: QwenVLOCR, image_path: Path, ptype: str) -> tuple[dict | None, str, float]:
|
||||
"""Exécute le prompt principal associé à un type de page et parse le JSON.
|
||||
|
||||
|
||||
def _truncate_empty_loop(text: str, max_consecutive: int = 2) -> str:
|
||||
"""Détecte et tronque les boucles d'objets vides.
|
||||
|
||||
GLM-OCR peut boucler sur `{"code":"", "position":"", "libelle":""}` quand
|
||||
un tableau DAS ou actes est vide dans l'image. La sortie est alors
|
||||
tronquée à `max_new_tokens` sans fermer le JSON → parse error.
|
||||
On garde au plus `max_consecutive` objets vides puis on coupe.
|
||||
Retourne (parsed_dict_ou_None, ocr_raw, elapsed_s). `parsed=None` quand
|
||||
la page n'a pas de prompt structuré associé (concertation_med, hospit.).
|
||||
"""
|
||||
matches = list(_EMPTY_OBJ_PATTERN.finditer(text))
|
||||
if len(matches) <= max_consecutive:
|
||||
return text
|
||||
# On coupe après la fin du `max_consecutive`-ième match
|
||||
cut_at = matches[max_consecutive - 1].end()
|
||||
return text[:cut_at]
|
||||
conf = PAGE_TYPES.get(ptype)
|
||||
if not conf or conf["prompt"] == PROMPT_HEADER:
|
||||
return None, "", 0.0
|
||||
res = ocr.run(image_path, conf["prompt"], max_new_tokens=4096)
|
||||
parsed = parse_json_output(res["text"])
|
||||
return parsed, res["text"], round(res["elapsed_s"], 2)
|
||||
|
||||
|
||||
def _close_open_json(text: str) -> str:
|
||||
"""Ajoute les brackets/braces manquants pour tenter de fermer un JSON tronqué."""
|
||||
# Compte les brackets non balancés en ignorant ceux entre guillemets simples/doubles
|
||||
depth_brace = 0
|
||||
depth_bracket = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
for c in text:
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
if c == "\\":
|
||||
escape = True
|
||||
continue
|
||||
if c == '"':
|
||||
in_string = not in_string
|
||||
continue
|
||||
if in_string:
|
||||
continue
|
||||
if c == "{": depth_brace += 1
|
||||
elif c == "}": depth_brace -= 1
|
||||
elif c == "[": depth_bracket += 1
|
||||
elif c == "]": depth_bracket -= 1
|
||||
# Retirer les virgules traînantes
|
||||
closed = text.rstrip().rstrip(",")
|
||||
# Fermer en priorité les crochets ouverts (tableaux), puis les accolades
|
||||
closed += "]" * max(0, depth_bracket)
|
||||
closed += "}" * max(0, depth_brace)
|
||||
return closed
|
||||
def _resolve_routing(images: list[Path], ocr: QwenVLOCR,
|
||||
use_standard_routing: bool,
|
||||
verbose: bool) -> tuple[list[str | None], list[str]]:
|
||||
"""Détermine le type de chaque page, soit par ordre standard (une seule
|
||||
vérification sur la page 1), soit par classification OCR page par page.
|
||||
|
||||
|
||||
def parse_json_output(raw: str) -> dict | None:
|
||||
"""Tente d'extraire un JSON depuis la sortie GLM-OCR.
|
||||
|
||||
Stratégies successives :
|
||||
1. parse direct après retrait des fences ```json
|
||||
2. patch des virgules manquantes entre objets / tableaux
|
||||
3. détection et troncature des boucles d'objets vides (cas fréquent sur
|
||||
tableaux DAS/actes vides → boucle jusqu'à max_new_tokens)
|
||||
4. fermeture des structures JSON ouvertes après troncature
|
||||
Retourne (page_types, headers). `headers[i]` est vide si pas de classify
|
||||
effectuée sur la page i.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
text = raw.strip()
|
||||
# 1) fences markdown
|
||||
text = re.sub(r"^```(?:json)?\s*", "", text)
|
||||
text = re.sub(r"\s*```$", "", text)
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
page_types: list[str | None] = [None] * len(images)
|
||||
headers: list[str] = [""] * len(images)
|
||||
|
||||
# 2) virgules manquantes entre `} {` et `] [`
|
||||
patched = re.sub(r"\}\s*\n(\s*\{)", r"},\n\1", text)
|
||||
patched = re.sub(r"\]\s*\n(\s*\[)", r"],\n\1", patched)
|
||||
try:
|
||||
return json.loads(patched)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
if use_standard_routing and images:
|
||||
ptype1, header1 = detect_page_type(images[0], ocr)
|
||||
if ptype1 == "recueil":
|
||||
page_types = list(route_by_index(len(images)))
|
||||
headers[0] = header1
|
||||
if verbose:
|
||||
print(" routing standard (page 1 = recueil OK)")
|
||||
return page_types, headers
|
||||
if verbose:
|
||||
print(f" page 1 = {ptype1} → fallback classification")
|
||||
|
||||
# 3) troncature des boucles d'objets vides puis 4) fermeture
|
||||
trimmed = _truncate_empty_loop(patched)
|
||||
closed = _close_open_json(trimmed)
|
||||
try:
|
||||
result = json.loads(closed)
|
||||
result["_truncated_loop"] = True # trace de l'intervention
|
||||
return result
|
||||
except json.JSONDecodeError as e:
|
||||
return {"_raw": raw, "_parse_error": str(e)}
|
||||
# Fallback : classify page par page
|
||||
for i, img in enumerate(images):
|
||||
page_types[i], headers[i] = detect_page_type(img, ocr)
|
||||
return page_types, headers
|
||||
|
||||
|
||||
def extract_dossier(pdf_path: str | Path, verbose: bool = True,
|
||||
use_standard_routing: bool = True) -> dict:
|
||||
"""Pipeline complet d'un dossier : PDF → JSON structuré.
|
||||
"""Pipeline complet d'un dossier : PDF → JSON structuré + annoté ATIH.
|
||||
|
||||
use_standard_routing=True (défaut) : route les pages par index selon
|
||||
l'ordre standard OGC (6 pages), sans OCR de classification. -50% du temps.
|
||||
Vérifie uniquement la page 1 pour s'assurer qu'on commence bien par
|
||||
"recueil" — si non, bascule en classification complète (fallback).
|
||||
Étages :
|
||||
1. `ingest.pdf_to_images` : PDF → PNG 300 dpi (avec deskew auto, cache)
|
||||
2. `_resolve_routing` : type de chaque page
|
||||
3. `_run_page_ocr` : OCR du schéma structuré par type de page
|
||||
4. `recueil.enrich_recueil` : checkboxes + crop Recodage pour la page recueil
|
||||
5. `validation.annotate` : validation ATIH de tous les codes extraits
|
||||
|
||||
Paramètre `use_standard_routing=True` exploite l'ordre standard des 6 pages
|
||||
OGC et économise 5 appels OCR par dossier. Bascule automatique sur la
|
||||
classification page-à-page si la page 1 n'est pas le recueil attendu.
|
||||
"""
|
||||
pdf_path = Path(pdf_path)
|
||||
ocr = QwenVLOCR()
|
||||
@@ -122,37 +89,22 @@ def extract_dossier(pdf_path: str | Path, verbose: bool = True,
|
||||
if verbose:
|
||||
print(f"[{pdf_path.name}] {len(images)} pages converties")
|
||||
|
||||
# Choix de stratégie de routing
|
||||
page_types = [None] * len(images)
|
||||
headers = [""] * len(images)
|
||||
if use_standard_routing:
|
||||
# Vérif rapide sur la page 1 (seul OCR de classification)
|
||||
ptype1, header1 = detect_page_type(images[0], ocr)
|
||||
if ptype1 == "recueil":
|
||||
page_types = route_by_index(len(images))
|
||||
headers[0] = header1
|
||||
if verbose:
|
||||
print(f" routing standard (page 1 = recueil OK)")
|
||||
else:
|
||||
if verbose:
|
||||
print(f" page 1 = {ptype1} → fallback classification")
|
||||
use_standard_routing = False
|
||||
page_types, headers = _resolve_routing(images, ocr, use_standard_routing, verbose)
|
||||
|
||||
result = {
|
||||
_, cb_zones = resolve_recueil_zones()
|
||||
|
||||
result: dict = {
|
||||
"fichier": pdf_path.stem,
|
||||
"pdf_hash": images[0].parent.name,
|
||||
"pdf_hash": images[0].parent.name if images else "",
|
||||
"pages": [],
|
||||
"extraction": {},
|
||||
}
|
||||
|
||||
for idx, img_path in enumerate(images, 1):
|
||||
t0 = time.time()
|
||||
if use_standard_routing:
|
||||
ptype = page_types[idx - 1]
|
||||
header_text = headers[idx - 1]
|
||||
else:
|
||||
ptype, header_text = detect_page_type(img_path, ocr)
|
||||
page_info = {
|
||||
ptype = page_types[idx - 1]
|
||||
header_text = headers[idx - 1]
|
||||
page_info: dict = {
|
||||
"page": idx,
|
||||
"type": ptype,
|
||||
"header": header_text.strip(),
|
||||
@@ -161,31 +113,20 @@ def extract_dossier(pdf_path: str | Path, verbose: bool = True,
|
||||
if verbose:
|
||||
print(f" p{idx}: {ptype}")
|
||||
|
||||
prompt_conf = PAGE_TYPES.get(ptype)
|
||||
if prompt_conf and prompt_conf["prompt"] != PROMPT_HEADER:
|
||||
res = ocr.run(img_path, prompt_conf["prompt"], max_new_tokens=4096)
|
||||
parsed = parse_json_output(res["text"])
|
||||
page_info["ocr_raw"] = res["text"]
|
||||
parsed, ocr_raw, elapsed = _run_page_ocr(ocr, img_path, ptype) if ptype else (None, "", 0.0)
|
||||
if parsed is not None:
|
||||
page_info["ocr_raw"] = ocr_raw
|
||||
page_info["parsed"] = parsed
|
||||
page_info["elapsed_s"] = round(res["elapsed_s"], 2)
|
||||
page_info["elapsed_s"] = elapsed
|
||||
|
||||
# Enrichissement : checkboxes accord/désaccord sur la fiche recueil
|
||||
# (GLM-OCR ne sait pas lire les checkboxes — voir test_prompt_crop_v2.py)
|
||||
if ptype == "recueil" and isinstance(parsed, dict):
|
||||
cb = detect_accord_desaccord(img_path, RECUEIL_ACCORD_DESACCORD)
|
||||
parsed["accord_desaccord"] = cb["decision"]
|
||||
parsed["_checkbox_debug"] = cb # ratios + diff pour audit
|
||||
enrich_recueil(parsed, img_path, ocr, cb_zones)
|
||||
page_info["parsed"] = parsed
|
||||
|
||||
# Indexer par type pour accès direct dans result["extraction"]
|
||||
result["extraction"][ptype] = parsed
|
||||
else:
|
||||
# Pages non structurées : juste l'en-tête déjà OCR
|
||||
page_info["elapsed_s"] = round(time.time() - t0, 2)
|
||||
|
||||
result["pages"].append(page_info)
|
||||
|
||||
# Post-traitement : validation ATIH de tous les codes extraits
|
||||
result = validate_annotate(result)
|
||||
|
||||
return result
|
||||
return validate_annotate(result)
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""PDF → images PNG 300 dpi avec cache par hash SHA256."""
|
||||
"""PDF → images PNG 300 dpi avec cache par hash SHA256.
|
||||
|
||||
Applique optionnellement un deskew automatique (redressement) sur chaque page
|
||||
pour corriger le biais d'inclinaison des scans. Voir pipeline/deskew.py.
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
from pathlib import Path
|
||||
from pdf2image import convert_from_path
|
||||
from PIL import Image
|
||||
|
||||
from .deskew import deskew_image, MIN_ANGLE_DEG
|
||||
|
||||
DEFAULT_DPI = 300
|
||||
CACHE_ROOT = Path(".cache/images")
|
||||
|
||||
@@ -18,23 +24,37 @@ def pdf_hash(pdf_path: str) -> str:
|
||||
return h.hexdigest()[:16]
|
||||
|
||||
|
||||
def pdf_to_images(pdf_path: str, dpi: int = DEFAULT_DPI, cache_root: Path = CACHE_ROOT) -> list[Path]:
|
||||
def pdf_to_images(pdf_path: str, dpi: int = DEFAULT_DPI,
|
||||
cache_root: Path = CACHE_ROOT,
|
||||
deskew: bool = True) -> list[Path]:
|
||||
"""Convertit un PDF en PNG 300 dpi. Retourne la liste des chemins (1 par page).
|
||||
|
||||
Le cache est indexé par hash du PDF : un PDF inchangé n'est jamais reconverti.
|
||||
|
||||
Avec `deskew=True` (défaut), chaque page est redressée si son angle de skew
|
||||
dépasse le seuil défini dans `pipeline.deskew.MIN_ANGLE_DEG` (0.3°). L'angle
|
||||
appliqué est persisté dans un fichier `<page>.skew` à côté (pour audit).
|
||||
"""
|
||||
cache_root = Path(cache_root)
|
||||
h = pdf_hash(pdf_path)
|
||||
out_dir = cache_root / h
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
existing = sorted(out_dir.glob("page_*.png"))
|
||||
# Le glob est strict pour ne pas attraper les crops intermédiaires
|
||||
# (page_XX_recodage.png, etc.)
|
||||
existing = sorted(p for p in out_dir.glob("page_*.png")
|
||||
if p.stem.replace("page_", "").isdigit())
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
pages = convert_from_path(pdf_path, dpi)
|
||||
paths = []
|
||||
for i, img in enumerate(pages, 1):
|
||||
if deskew:
|
||||
img, applied = deskew_image(img)
|
||||
if applied != 0.0:
|
||||
# Trace d'audit : on note l'angle corrigé
|
||||
(out_dir / f"page_{i:02d}.skew").write_text(f"{applied:.3f}\n")
|
||||
p = out_dir / f"page_{i:02d}.png"
|
||||
img.save(p, "PNG", optimize=True)
|
||||
paths.append(p)
|
||||
|
||||
136
pipeline/json_utils.py
Normal file
136
pipeline/json_utils.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Parsing JSON tolérant pour les sorties des VLM.
|
||||
|
||||
Les VLM (Qwen, GLM-OCR, GOT-OCR…) produisent du JSON avec des anomalies
|
||||
fréquentes :
|
||||
|
||||
- Encadrement par des fences markdown ```json ... ```
|
||||
- Virgules manquantes entre objets ou éléments de tableau
|
||||
- Boucles pathologiques d'objets vides `{"code":"","position":""}` répétés
|
||||
jusqu'à `max_new_tokens`, ce qui tronque le JSON sans le fermer proprement
|
||||
|
||||
Ce module expose `parse_json_output()` qui applique plusieurs stratégies de
|
||||
récupération avant d'abandonner. En dernier recours, il renvoie un dict avec
|
||||
`_raw` + `_parse_error` pour audit, jamais `None` (ce qui casserait le pipeline).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
|
||||
|
||||
# Pattern qui matche un objet vide générique {"code":"","position":"",...}
|
||||
_EMPTY_OBJ_PATTERN = re.compile(
|
||||
r'\{\s*"code"\s*:\s*""\s*,\s*"position"\s*:\s*""\s*(?:,\s*"libelle"\s*:\s*""\s*)?\}',
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
_FENCE_OPEN_RE = re.compile(r"^```(?:json)?\s*")
|
||||
_FENCE_CLOSE_RE = re.compile(r"\s*```$")
|
||||
_MISSING_COMMA_OBJ_RE = re.compile(r"\}\s*\n(\s*\{)")
|
||||
_MISSING_COMMA_ARR_RE = re.compile(r"\]\s*\n(\s*\[)")
|
||||
|
||||
|
||||
def strip_fences(text: str) -> str:
|
||||
"""Retire un éventuel encadrement ```json ... ```."""
|
||||
text = _FENCE_OPEN_RE.sub("", text.strip())
|
||||
text = _FENCE_CLOSE_RE.sub("", text)
|
||||
return text
|
||||
|
||||
|
||||
def patch_missing_commas(text: str) -> str:
|
||||
"""Ajoute les virgules manquantes entre `}\\n{` et `]\\n[`.
|
||||
|
||||
Les VLM omettent fréquemment ces virgules dans leurs sorties JSON.
|
||||
"""
|
||||
text = _MISSING_COMMA_OBJ_RE.sub(r"},\n\1", text)
|
||||
text = _MISSING_COMMA_ARR_RE.sub(r"],\n\1", text)
|
||||
return text
|
||||
|
||||
|
||||
def truncate_empty_loop(text: str, max_consecutive: int = 2) -> str:
|
||||
"""Tronque les boucles d'objets vides (`{"code":"","position":""}` répété).
|
||||
|
||||
Cas d'usage : quand un tableau DAS ou Actes est vide dans l'image, le
|
||||
VLM a parfois tendance à générer le même objet vide en boucle jusqu'à
|
||||
saturer `max_new_tokens`. La sortie est alors tronquée sans fermer le
|
||||
JSON → parse error. Garder au maximum `max_consecutive` occurrences.
|
||||
"""
|
||||
matches = list(_EMPTY_OBJ_PATTERN.finditer(text))
|
||||
if len(matches) <= max_consecutive:
|
||||
return text
|
||||
cut_at = matches[max_consecutive - 1].end()
|
||||
return text[:cut_at]
|
||||
|
||||
|
||||
def close_open_json(text: str) -> str:
|
||||
"""Ajoute les brackets/braces manquants pour fermer un JSON tronqué.
|
||||
|
||||
Compte les accolades et crochets non-balancés en ignorant ceux à
|
||||
l'intérieur d'une chaîne, puis ferme dans le bon ordre (tableaux
|
||||
ouverts d'abord, puis objets).
|
||||
"""
|
||||
depth_brace = 0
|
||||
depth_bracket = 0
|
||||
in_string = False
|
||||
escape = False
|
||||
for c in text:
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
if c == "\\":
|
||||
escape = True
|
||||
continue
|
||||
if c == '"':
|
||||
in_string = not in_string
|
||||
continue
|
||||
if in_string:
|
||||
continue
|
||||
if c == "{":
|
||||
depth_brace += 1
|
||||
elif c == "}":
|
||||
depth_brace -= 1
|
||||
elif c == "[":
|
||||
depth_bracket += 1
|
||||
elif c == "]":
|
||||
depth_bracket -= 1
|
||||
closed = text.rstrip().rstrip(",")
|
||||
closed += "]" * max(0, depth_bracket)
|
||||
closed += "}" * max(0, depth_brace)
|
||||
return closed
|
||||
|
||||
|
||||
def parse_json_output(raw: str) -> dict | None:
|
||||
"""Parse une sortie VLM en dict. Applique plusieurs stratégies :
|
||||
|
||||
1. strip des fences markdown ```json
|
||||
2. parse direct
|
||||
3. patch des virgules manquantes
|
||||
4. troncature des boucles d'objets vides + fermeture du JSON
|
||||
|
||||
En cas d'échec de toutes les stratégies, retourne
|
||||
`{"_raw": raw, "_parse_error": str}` pour permettre l'audit manuel
|
||||
plutôt que de casser le pipeline.
|
||||
"""
|
||||
if not raw:
|
||||
return None
|
||||
text = strip_fences(raw)
|
||||
try:
|
||||
return json.loads(text)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
patched = patch_missing_commas(text)
|
||||
try:
|
||||
return json.loads(patched)
|
||||
except json.JSONDecodeError:
|
||||
pass
|
||||
|
||||
trimmed = truncate_empty_loop(patched)
|
||||
closed = close_open_json(trimmed)
|
||||
try:
|
||||
result = json.loads(closed)
|
||||
if isinstance(result, dict):
|
||||
result["_truncated_loop"] = True
|
||||
return result
|
||||
except json.JSONDecodeError as e:
|
||||
return {"_raw": raw, "_parse_error": str(e)}
|
||||
@@ -27,18 +27,64 @@ class QwenVLOCR:
|
||||
|
||||
def _init_model(self):
|
||||
t0 = time.time()
|
||||
import os as _os
|
||||
|
||||
# max_pixels limite le nombre de patches visuels pour éviter l'OOM
|
||||
# sur images 300 dpi (2481x3509). ~1.25M pixels = équilibre qualité/VRAM.
|
||||
# sur images 300 dpi (2481x3509). ~800 patches = équilibre qualité/VRAM,
|
||||
# tient confortablement dans ~5-6 Go même avec d'autres processus GPU
|
||||
# en arrière-plan. Configurable via env var QWEN_MAX_PIXELS (en patches).
|
||||
max_pixels = int(_os.environ.get("QWEN_MAX_PIXELS", 800)) * 28 * 28
|
||||
self.processor = AutoProcessor.from_pretrained(
|
||||
MODEL_PATH,
|
||||
min_pixels=256 * 28 * 28,
|
||||
max_pixels=1280 * 28 * 28,
|
||||
)
|
||||
self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
MODEL_PATH,
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map="auto",
|
||||
max_pixels=max_pixels,
|
||||
)
|
||||
|
||||
# Device : "auto" par défaut (GPU si dispo), "cpu" pour forcer le CPU
|
||||
# quand la VRAM est saturée par d'autres process. Configurable via
|
||||
# QWEN_DEVICE=cpu.
|
||||
device = _os.environ.get("QWEN_DEVICE", "auto").lower()
|
||||
if device == "cpu":
|
||||
# Sur CPU on cherche à maximiser le throughput :
|
||||
# 1. Utiliser tous les cores via torch.set_num_threads (set_num_threads
|
||||
# prime sur OMP_NUM_THREADS pour les ops PyTorch natifs).
|
||||
# 2. Choisir bfloat16 si le CPU le supporte nativement (Zen 5,
|
||||
# Zen 4, Intel Sapphire Rapids+ ont AVX-512 BF16). Sinon float32.
|
||||
n_threads = int(_os.environ.get("TORCH_NUM_THREADS", _os.cpu_count() or 8))
|
||||
torch.set_num_threads(n_threads)
|
||||
try:
|
||||
torch.set_num_interop_threads(n_threads)
|
||||
except RuntimeError:
|
||||
pass # déjà initialisé, ignorer
|
||||
|
||||
# Détection AVX-512 BF16 via /proc/cpuinfo (Linux)
|
||||
use_bf16 = False
|
||||
try:
|
||||
with open("/proc/cpuinfo") as f:
|
||||
flags = f.read()
|
||||
use_bf16 = "avx512_bf16" in flags or "amx_bf16" in flags
|
||||
except Exception:
|
||||
pass
|
||||
dtype = torch.bfloat16 if use_bf16 else torch.float32
|
||||
|
||||
self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
MODEL_PATH,
|
||||
torch_dtype=dtype,
|
||||
device_map={"": "cpu"},
|
||||
low_cpu_mem_usage=True,
|
||||
)
|
||||
self.device_used = "cpu"
|
||||
self.cpu_threads = n_threads
|
||||
self.cpu_dtype = str(dtype).replace("torch.", "")
|
||||
else:
|
||||
self.model = Qwen2_5_VLForConditionalGeneration.from_pretrained(
|
||||
MODEL_PATH,
|
||||
torch_dtype=torch.bfloat16,
|
||||
device_map="auto",
|
||||
)
|
||||
self.device_used = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
self.cpu_threads = None
|
||||
self.cpu_dtype = None
|
||||
self.model.eval()
|
||||
self.load_time = time.time() - t0
|
||||
self.vram_gb = torch.cuda.memory_allocated() / 1e9 if torch.cuda.is_available() else 0.0
|
||||
@@ -67,4 +113,9 @@ class QwenVLOCR:
|
||||
generated_ids = self.model.generate(**inputs, max_new_tokens=max_new_tokens)
|
||||
out_ids = generated_ids[:, inputs.input_ids.shape[1]:]
|
||||
output = self.processor.batch_decode(out_ids, skip_special_tokens=True)[0]
|
||||
# Libérer la VRAM allouée par l'inférence (utile quand d'autres
|
||||
# processus tournent en parallèle sur le même GPU)
|
||||
del inputs, generated_ids, out_ids
|
||||
if torch.cuda.is_available():
|
||||
torch.cuda.empty_cache()
|
||||
return {"text": output.strip(), "elapsed_s": time.time() - t0}
|
||||
|
||||
@@ -9,9 +9,10 @@ DEFAULT_OUT = Path("output/v2")
|
||||
def save_result(result: dict, out_dir: Path | str = DEFAULT_OUT) -> Path:
|
||||
out_dir = Path(out_dir)
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
from .ocr_qwen import MODEL_PATH as OCR_MODEL_ID
|
||||
result["_meta"] = {
|
||||
"pipeline_version": "v1",
|
||||
"ocr_model": "zai-org/GLM-OCR",
|
||||
"pipeline_version": "v2",
|
||||
"ocr_model": OCR_MODEL_ID,
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
||||
}
|
||||
out_path = out_dir / f"{result['fichier']}.json"
|
||||
|
||||
@@ -54,6 +54,38 @@ Si un champ est illisible, laisse une chaîne vide. Ne devine pas.
|
||||
"praticien_conseil": ""
|
||||
}"""
|
||||
|
||||
# --- Second passage dédié : colonne Recodage de la page recueil ---
|
||||
# Qwen-VL sous-extrait la colonne droite du tableau Codage quand on lui donne
|
||||
# la page entière (27% de couverture sur `codage_reco.dp` en V2.0). En lui
|
||||
# donnant directement un crop zonal de cette seule colonne, il lit beaucoup
|
||||
# mieux (la structure à une seule colonne lève l'ambiguïté).
|
||||
#
|
||||
# Zone cropée (coordonnées relatives dans l'image complète) :
|
||||
# Zone restreinte au seul bloc codage (DP/DR/DAS de la colonne Recodage).
|
||||
# On exclut la partie Actes (qui commence autour de y=0.680) pour éviter que
|
||||
# Qwen confonde les codes CCAM (actes) avec des codes CIM-10 (DAS).
|
||||
RECUEIL_RECODAGE_ZONE = (0.77, 0.330, 0.97, 0.490)
|
||||
|
||||
SCHEMA_RECUEIL_RECODAGE = """Cette image est un extrait d'une colonne d'un tableau médical.
|
||||
La colonne peut contenir ZÉRO, UN ou PLUSIEURS codes médicaux CIM-10 (format : 1 lettre majuscule + 2 à 4 chiffres, ex: K650, T810, Z954, R31, I652). Un code peut avoir un suffixe `*`. À droite d'un code, une position numérique (1-9) peut être visible.
|
||||
|
||||
IMPORTANT — LIS UNIQUEMENT CE QUI EST PHYSIQUEMENT VISIBLE :
|
||||
- La plupart des lignes de ce tableau sont VIDES. C'est NORMAL.
|
||||
- Ne liste QUE les codes effectivement écrits dans l'image. N'INVENTE rien.
|
||||
- Si l'image ne contient qu'un seul code, ta réponse doit lister exactement un code (pas plusieurs).
|
||||
- Si l'image ne contient aucun code, renvoie `"codes": []`.
|
||||
- Ne déduis pas les codes d'autres cases non montrées dans l'image.
|
||||
|
||||
Pour chaque code réellement visible, indique sa position à droite si elle est écrite, sinon "".
|
||||
|
||||
Renvoie STRICTEMENT ce JSON, sans commentaire ni markdown :
|
||||
{
|
||||
"codes": [
|
||||
{"code": "", "position": ""}
|
||||
]
|
||||
}"""
|
||||
|
||||
|
||||
# --- Page 5 : Fiche administrative de concertation 2/2 (décision finale) ---
|
||||
SCHEMA_CONCERTATION_2 = """Lis la fiche de concertation et renvoie STRICTEMENT le JSON suivant, sans commentaire ni markdown.
|
||||
Si un champ est illisible, laisse une chaîne vide.
|
||||
|
||||
203
pipeline/recueil.py
Normal file
203
pipeline/recueil.py
Normal file
@@ -0,0 +1,203 @@
|
||||
"""Logique spécifique à la page `recueil` (fiche médicale de recueil OGC).
|
||||
|
||||
Regroupe tout ce qui concerne cette page — la plus riche et la plus difficile
|
||||
à extraire du dossier OGC — séparé de l'orchestration générale :
|
||||
|
||||
- Résolution des zones configurables (crop Recodage, checkboxes)
|
||||
- Second passage VLM sur le crop de la colonne Recodage
|
||||
- Fusion du résultat crop dans le JSON principal
|
||||
- Classification des codes CIM-10 en DP/DR/DAS par règle métier
|
||||
- Enrichissement post-extraction (checkboxes Accord/Désaccord, ghs_injustifie)
|
||||
|
||||
Les fonctions sont testables indépendamment de Qwen quand on leur fournit
|
||||
déjà les sorties OCR brutes.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from .checkboxes import (
|
||||
CheckboxZones,
|
||||
RECUEIL_ACCORD_DESACCORD,
|
||||
detect_accord_desaccord,
|
||||
parse_ghs_injustifie,
|
||||
)
|
||||
from .json_utils import parse_json_output
|
||||
from .ocr_qwen import QwenVLOCR
|
||||
from .prompts import RECUEIL_RECODAGE_ZONE, SCHEMA_RECUEIL_RECODAGE
|
||||
from .zones_config import get_zone, load_config
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Résolution des zones (config JSON + fallback sur les defaults)
|
||||
# ============================================================
|
||||
|
||||
def resolve_recueil_zones() -> tuple[tuple[float, float, float, float], CheckboxZones]:
|
||||
"""Charge les zones de la page recueil depuis la config utilisateur,
|
||||
avec fallback sur les constantes compilées si la config est absente.
|
||||
|
||||
Retourne (zone_crop_recodage, zones_accord_desaccord).
|
||||
"""
|
||||
cfg = load_config()
|
||||
reco = get_zone("recueil", "codage_reco", cfg) or RECUEIL_RECODAGE_ZONE
|
||||
acc = get_zone("recueil", "accord_checkbox", cfg)
|
||||
des = get_zone("recueil", "desaccord_checkbox", cfg)
|
||||
if acc and des:
|
||||
cb = CheckboxZones(accord=acc, desaccord=des)
|
||||
else:
|
||||
cb = RECUEIL_ACCORD_DESACCORD
|
||||
return reco, cb
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Classification CIM-10 → DP / DR / DAS (pur, testable sans VLM)
|
||||
# ============================================================
|
||||
|
||||
CIM10_RE = re.compile(r"^[A-Z]\d{2,4}\s*\*?\s*\+?\d*$")
|
||||
|
||||
|
||||
def filter_cim10_codes(codes_raw: list[Any]) -> list[dict]:
|
||||
"""Filtre une liste de codes OCR bruts pour ne garder que les CIM-10.
|
||||
|
||||
Les VLM peuvent parfois lire des codes CCAM (actes) dans un crop qui
|
||||
dépasse sur le bloc Actes. On les retire ici pour ne pas polluer les DAS.
|
||||
"""
|
||||
kept = []
|
||||
for c in codes_raw or []:
|
||||
if not isinstance(c, dict):
|
||||
continue
|
||||
code = (c.get("code") or "").strip()
|
||||
if code and CIM10_RE.match(code):
|
||||
kept.append({
|
||||
"code": code,
|
||||
"position": str(c.get("position") or "").strip(),
|
||||
})
|
||||
return kept
|
||||
|
||||
|
||||
def classify_codes_dp_dr_das(codes: list[dict]) -> tuple[str, str, list[dict]]:
|
||||
"""Classifie une liste de codes {code, position} en DP, DR et liste de DAS.
|
||||
|
||||
Règle métier :
|
||||
- 1er code sans position → DP
|
||||
- 2e code sans position → DR (ignoré si identique au DP : le VLM peut
|
||||
dupliquer le DP quand la case DR est visuellement vide)
|
||||
- tous les codes avec position → DAS
|
||||
- codes sans position au-delà du 2e → DAS sans position (pour ne rien perdre)
|
||||
"""
|
||||
dp, dr = "", ""
|
||||
das: list[dict] = []
|
||||
dp_assigned = dr_assigned = False
|
||||
for c in codes:
|
||||
code, position = c["code"], c["position"]
|
||||
if not position:
|
||||
if not dp_assigned:
|
||||
dp, dp_assigned = code, True
|
||||
elif not dr_assigned:
|
||||
if code == dp:
|
||||
dr_assigned = True # doublon DP → DR vide
|
||||
else:
|
||||
dr, dr_assigned = code, True
|
||||
else:
|
||||
das.append({"code": code, "position": ""})
|
||||
else:
|
||||
das.append(c)
|
||||
return dp, dr, das
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Second passage VLM sur crop Recodage
|
||||
# ============================================================
|
||||
|
||||
def run_recodage_crop_pass(image_path: Path, ocr: QwenVLOCR,
|
||||
zone: tuple[float, float, float, float] | None = None
|
||||
) -> dict | None:
|
||||
"""Execute un second passage VLM sur le crop zonal de la colonne Recodage.
|
||||
|
||||
Sauvegarde le crop à côté de l'image source (suffixe `_recodage.png`)
|
||||
pour audit. Retourne un dict avec `dp/dr/das` + métadonnées, ou None
|
||||
en cas d'échec d'OCR ou de parsing.
|
||||
"""
|
||||
try:
|
||||
img = Image.open(image_path)
|
||||
w, h = img.size
|
||||
z = zone
|
||||
if z is None:
|
||||
z, _ = resolve_recueil_zones()
|
||||
x1, y1, x2, y2 = z
|
||||
crop = img.crop((int(x1 * w), int(y1 * h), int(x2 * w), int(y2 * h)))
|
||||
crop_path = image_path.parent / f"{image_path.stem}_recodage.png"
|
||||
crop.save(crop_path)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
t0 = time.time()
|
||||
res = ocr.run(crop_path, SCHEMA_RECUEIL_RECODAGE, max_new_tokens=1024)
|
||||
parsed = parse_json_output(res["text"])
|
||||
if not isinstance(parsed, dict) or "_parse_error" in parsed:
|
||||
return None
|
||||
|
||||
codes = filter_cim10_codes(parsed.get("codes") or [])
|
||||
dp, dr, das = classify_codes_dp_dr_das(codes)
|
||||
return {
|
||||
"dp": dp, "dr": dr, "das": das,
|
||||
"_source": "crop_recodage",
|
||||
"_elapsed_s": round(res["elapsed_s"], 2),
|
||||
"_n_codes_raw": len(parsed.get("codes") or []),
|
||||
"_n_codes_kept": len(codes),
|
||||
}
|
||||
|
||||
|
||||
def merge_codage_reco(parsed: dict, reco: dict) -> None:
|
||||
"""Fusionne le résultat du crop Recodage dans `parsed["codage_reco"]`.
|
||||
|
||||
Politique de merge : le crop est plus fiable (contexte isolé) donc il
|
||||
prime sur le passage principal. Exception : si un champ du crop est vide
|
||||
mais que le passage principal l'a rempli, on garde celui du passage
|
||||
principal (on ne dégrade jamais un résultat existant).
|
||||
"""
|
||||
existing = parsed.get("codage_reco") if isinstance(parsed.get("codage_reco"), dict) else {}
|
||||
parsed["codage_reco"] = {
|
||||
"dp": reco.get("dp", "") or existing.get("dp", ""),
|
||||
"dr": reco.get("dr", "") or existing.get("dr", ""),
|
||||
"das": reco.get("das") or existing.get("das") or [],
|
||||
}
|
||||
parsed.setdefault("_crop_recodage", {})["result"] = reco
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Enrichissement post-extraction d'une page recueil
|
||||
# ============================================================
|
||||
|
||||
def enrich_recueil(parsed: dict, image_path: Path, ocr: QwenVLOCR,
|
||||
cb_zones: CheckboxZones | None = None) -> dict:
|
||||
"""Enrichit un JSON recueil parsé avec :
|
||||
- checkbox accord/désaccord (méthode densité pixels, indépendante du VLM)
|
||||
- normalisation `ghs_injustifie` → 0 / 1 / ""
|
||||
- second passage VLM sur le crop Recodage si besoin, fusionné dans `codage_reco`
|
||||
|
||||
Modifie `parsed` en place et le renvoie (pratique pour chaînage).
|
||||
"""
|
||||
if not isinstance(parsed, dict):
|
||||
return parsed
|
||||
zones = cb_zones or resolve_recueil_zones()[1]
|
||||
|
||||
# Checkboxes accord / désaccord
|
||||
cb = detect_accord_desaccord(image_path, zones)
|
||||
parsed["accord_desaccord"] = cb["decision"]
|
||||
parsed["_checkbox_debug"] = cb
|
||||
|
||||
# Normalisation ghs_injustifie
|
||||
parsed["ghs_injustifie"] = parse_ghs_injustifie(parsed.get("ghs_injustifie", ""))
|
||||
|
||||
# Second passage Recodage
|
||||
reco = run_recodage_crop_pass(image_path, ocr)
|
||||
if reco:
|
||||
merge_codage_reco(parsed, reco)
|
||||
|
||||
return parsed
|
||||
185
pipeline/schema.py
Normal file
185
pipeline/schema.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""Schema de sortie stable du pipeline + fonction de nettoyage.
|
||||
|
||||
Le pipeline produit un JSON riche pendant l'exécution (avec des champs de debug :
|
||||
ratios checkbox, OCR raw, flags _parse_error, _truncated_loop, _crop_recodage,
|
||||
_checkbox_debug, _source, etc). Cette information est utile pour auditer un
|
||||
dossier mais pollue la structure quand on veut exposer le résultat à un
|
||||
consommateur aval (Excel, dashboard, échange inter-équipes).
|
||||
|
||||
Ce module expose :
|
||||
- `clean_dossier(raw)` : retourne une version propre, lisible et stable,
|
||||
sans champs de debug. Garde les flags de validation ATIH qui ont une valeur
|
||||
métier (codes valides, cohérence GHM↔GHS).
|
||||
- `SCHEMA_VERSION` : version du format (incrémentée à chaque breaking
|
||||
change de structure).
|
||||
- `CLEAN_FIELDS_RECUEIL` : liste des champs finaux de la page recueil
|
||||
(utile pour Excel, dashboard, docs).
|
||||
|
||||
Principe : le JSON raw reste dans `output/v2/<nom>.json` (audit complet), le
|
||||
JSON clean est produit séparément sur demande via `clean_dossier()`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from typing import Any
|
||||
|
||||
SCHEMA_VERSION = "2.0"
|
||||
|
||||
# Champs retenus sur la page recueil pour la sortie propre. L'ordre est
|
||||
# celui de l'affichage logique (en-tête → séjour → codage → GHM/GHS → décisions).
|
||||
CLEAN_FIELDS_RECUEIL = [
|
||||
"etablissement", "finess", "date_debut_controle",
|
||||
"n_ogc", "n_champ", "dates_sejour",
|
||||
"sejour_etab", "sejour_reco", "rum_etab",
|
||||
"codage_etab", "codage_reco",
|
||||
"actes_etab", "actes_reco",
|
||||
"ghm_etab", "ghs_etab", "ghm_reco", "ghs_reco",
|
||||
"recodage_impactant", "ghs_injustifie",
|
||||
"accord_desaccord", "praticien_conseil",
|
||||
]
|
||||
|
||||
CLEAN_FIELDS_CONCERTATION_2 = [
|
||||
"ghs_initial", "ghs_avant_concertation", "ghs_final",
|
||||
"decision", "date_concertation",
|
||||
"praticien_controleur", "medecin_dim",
|
||||
]
|
||||
|
||||
CLEAN_FIELDS_CONCERTATION_1 = [
|
||||
"date_concertation", "argumentaire",
|
||||
]
|
||||
|
||||
CLEAN_FIELDS_PREUVES = [
|
||||
"date", "praticien_controleur", "medecin_dim", "pieces",
|
||||
]
|
||||
|
||||
|
||||
# Champs de debug à retirer systématiquement du clean
|
||||
DEBUG_FIELDS = {
|
||||
"_checkbox_debug",
|
||||
"_crop_recodage",
|
||||
"_parse_error",
|
||||
"_raw",
|
||||
"_truncated_loop",
|
||||
"_source",
|
||||
"_elapsed_s",
|
||||
"_n_codes_raw",
|
||||
"_n_codes_kept",
|
||||
}
|
||||
|
||||
|
||||
def _pick(d: dict, keys: list[str]) -> dict:
|
||||
"""Retourne un dict ordonné avec uniquement les clés présentes."""
|
||||
out = {}
|
||||
for k in keys:
|
||||
if k in d:
|
||||
out[k] = d[k]
|
||||
return out
|
||||
|
||||
|
||||
def _clean_validation(validation: dict | None) -> dict | None:
|
||||
"""Garde la validation ATIH mais en format compact : juste les flags utiles."""
|
||||
if not isinstance(validation, dict):
|
||||
return None
|
||||
summary = validation.get("summary") or {}
|
||||
cc = validation.get("cross_checks") or {}
|
||||
# On conserve juste l'essentiel : par champ, le flag valid (True/False/None)
|
||||
# et éventuellement la suggestion de correction OCR.
|
||||
def _compact_code(entry):
|
||||
if not isinstance(entry, dict) or "valid" not in entry:
|
||||
return None
|
||||
out = {"valid": entry.get("valid")}
|
||||
if entry.get("suggestion"):
|
||||
out["suggestion"] = entry["suggestion"]
|
||||
if entry.get("libelle_ref"):
|
||||
out["libelle_ref"] = entry["libelle_ref"]
|
||||
return out
|
||||
|
||||
result = {
|
||||
"summary": summary,
|
||||
"codage_etab": {
|
||||
"dp": _compact_code(validation.get("codage_etab", {}).get("dp")),
|
||||
"dr": _compact_code(validation.get("codage_etab", {}).get("dr")),
|
||||
"das": [_compact_code(d) for d in validation.get("codage_etab", {}).get("das", []) or []],
|
||||
},
|
||||
"codage_reco": {
|
||||
"dp": _compact_code(validation.get("codage_reco", {}).get("dp")),
|
||||
"dr": _compact_code(validation.get("codage_reco", {}).get("dr")),
|
||||
"das": [_compact_code(d) for d in validation.get("codage_reco", {}).get("das", []) or []],
|
||||
},
|
||||
"ghm_etab": _compact_code(validation.get("ghm_etab")),
|
||||
"ghs_etab": _compact_code(validation.get("ghs_etab")),
|
||||
"ghm_reco": _compact_code(validation.get("ghm_reco")),
|
||||
"ghs_reco": _compact_code(validation.get("ghs_reco")),
|
||||
"cross_checks": {
|
||||
"etab_ghm_ghs_coherent": cc.get("etab", {}).get("coherent"),
|
||||
"reco_ghm_ghs_coherent": cc.get("reco", {}).get("coherent"),
|
||||
},
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _clean_recueil(page: dict) -> dict:
|
||||
cleaned = _pick(page, CLEAN_FIELDS_RECUEIL)
|
||||
# Sous-champs codage : nettoyer aussi les codes invalides
|
||||
v = _clean_validation(page.get("_validation"))
|
||||
if v:
|
||||
cleaned["_validation"] = v
|
||||
return cleaned
|
||||
|
||||
|
||||
def _clean_simple(page: dict, fields: list[str]) -> dict:
|
||||
cleaned = _pick(page, fields)
|
||||
v = page.get("_validation")
|
||||
if isinstance(v, dict):
|
||||
cleaned["_validation"] = v # déjà compact pour ces pages
|
||||
return cleaned
|
||||
|
||||
|
||||
def clean_dossier(raw: dict) -> dict:
|
||||
"""Retourne une copie nettoyée d'un résultat de pipeline.
|
||||
|
||||
Strippe les champs de debug internes, garde la validation ATIH compacte
|
||||
et une structure stable.
|
||||
"""
|
||||
extraction = raw.get("extraction") or {}
|
||||
clean_extraction: dict[str, Any] = {}
|
||||
|
||||
if "recueil" in extraction and isinstance(extraction["recueil"], dict):
|
||||
clean_extraction["recueil"] = _clean_recueil(extraction["recueil"])
|
||||
if "concertation_2" in extraction and isinstance(extraction["concertation_2"], dict):
|
||||
clean_extraction["concertation_2"] = _clean_simple(
|
||||
extraction["concertation_2"], CLEAN_FIELDS_CONCERTATION_2)
|
||||
if "concertation_1" in extraction and isinstance(extraction["concertation_1"], dict):
|
||||
clean_extraction["concertation_1"] = _clean_simple(
|
||||
extraction["concertation_1"], CLEAN_FIELDS_CONCERTATION_1)
|
||||
if "preuves" in extraction and isinstance(extraction["preuves"], dict):
|
||||
clean_extraction["preuves"] = _clean_simple(
|
||||
extraction["preuves"], CLEAN_FIELDS_PREUVES)
|
||||
|
||||
return {
|
||||
"fichier": raw.get("fichier"),
|
||||
"pdf_hash": raw.get("pdf_hash"),
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"extraction": clean_extraction,
|
||||
"_meta": raw.get("_meta", {}),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Utilitaire : nettoyer un fichier en place, ou produire une version clean
|
||||
import json, sys, glob
|
||||
from pathlib import Path
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
paths = [Path(p) for p in sys.argv[1:]]
|
||||
else:
|
||||
paths = [Path(p) for p in sorted(glob.glob("output/v2/OGC *.json"))]
|
||||
|
||||
out_dir = Path("output/v2_clean")
|
||||
out_dir.mkdir(exist_ok=True)
|
||||
for p in paths:
|
||||
raw = json.loads(p.read_text(encoding="utf-8"))
|
||||
clean = clean_dossier(raw)
|
||||
(out_dir / p.name).write_text(
|
||||
json.dumps(clean, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"{len(paths)} fichiers nettoyés → {out_dir}/")
|
||||
@@ -27,7 +27,44 @@ if str(_REPO_ROOT) not in sys.path:
|
||||
import streamlit as st
|
||||
from PIL import Image
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Compatibility shim : streamlit-drawable-canvas 0.9.3 utilise l'API privée
|
||||
# `streamlit.elements.image.image_to_url` qui a été retirée à partir de
|
||||
# Streamlit ≈ 1.49. On réinjecte une implémentation équivalente fondée sur
|
||||
# un data URI base64, ce qui permet au canvas de continuer à fonctionner
|
||||
# sans downgrader Streamlit globalement.
|
||||
#
|
||||
# Remplacer ce shim par l'upgrade de streamlit-drawable-canvas si une version
|
||||
# > 0.9.3 est publiée.
|
||||
# ----------------------------------------------------------------------------
|
||||
import base64 as _b64
|
||||
import io as _io
|
||||
from streamlit.elements import image as _st_image # type: ignore
|
||||
|
||||
if not hasattr(_st_image, "image_to_url"):
|
||||
def _image_to_url_compat(image, width, clamp, channels, output_format,
|
||||
image_id):
|
||||
"""Convertit une PIL.Image en data URI compatible avec drawable-canvas."""
|
||||
fmt = (output_format or "PNG").upper()
|
||||
if fmt == "JPG":
|
||||
fmt = "JPEG"
|
||||
buf = _io.BytesIO()
|
||||
image.save(buf, format=fmt)
|
||||
b64 = _b64.b64encode(buf.getvalue()).decode("ascii")
|
||||
mime = "image/jpeg" if fmt == "JPEG" else f"image/{fmt.lower()}"
|
||||
return f"data:{mime};base64,{b64}"
|
||||
|
||||
_st_image.image_to_url = _image_to_url_compat # type: ignore[attr-defined]
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
from pipeline.ingest import pdf_to_images
|
||||
from pipeline.zones_config import load_config, save_config, DEFAULT_CONFIG_PATH
|
||||
|
||||
try:
|
||||
from streamlit_drawable_canvas import st_canvas
|
||||
_HAS_CANVAS = True
|
||||
except ImportError:
|
||||
_HAS_CANVAS = False
|
||||
|
||||
|
||||
# ============================================================
|
||||
@@ -268,6 +305,130 @@ def render_page_editor(name: str, ptype: str, extract: dict, gold: dict | None):
|
||||
st.code(page_meta.get("ocr_raw", ""), language="json")
|
||||
|
||||
|
||||
def render_calibration_page():
|
||||
"""Mode 'Calibration zones' : dessine des rectangles à la souris sur une
|
||||
image de référence, sauvegarde dans pipeline/zones_config.json."""
|
||||
st.header("🔧 Calibration des zones")
|
||||
|
||||
if not _HAS_CANVAS:
|
||||
st.error(
|
||||
"Le package `streamlit-drawable-canvas` n'est pas installé.\n"
|
||||
"Installe-le avec : `pip install streamlit-drawable-canvas`"
|
||||
)
|
||||
return
|
||||
|
||||
pdfs = list_pdfs()
|
||||
if not pdfs:
|
||||
st.error("Aucun PDF disponible pour la calibration")
|
||||
return
|
||||
|
||||
col_ctrl, _ = st.columns([1, 3])
|
||||
with col_ctrl:
|
||||
ref_name = st.selectbox(
|
||||
"PDF de référence (bien cadré)",
|
||||
[p.stem for p in pdfs], key="calib_pdf",
|
||||
)
|
||||
page_type = st.selectbox(
|
||||
"Type de page", ["recueil"],
|
||||
help="Aujourd'hui seule la page recueil a des zones configurables",
|
||||
)
|
||||
# Page numéro selon le type (recueil = page 1)
|
||||
page_num = {"recueil": 1}.get(page_type, 1)
|
||||
|
||||
ref_pdf = next(p for p in pdfs if p.stem == ref_name)
|
||||
img_path = pdf_to_images(str(ref_pdf))[page_num - 1]
|
||||
img = Image.open(img_path)
|
||||
img_w, img_h = img.size
|
||||
|
||||
# Charger config existante et préparer les zones
|
||||
cfg = load_config()
|
||||
existing_zones = cfg.get(page_type, {})
|
||||
|
||||
# On scale l'image pour tenir dans le canvas (largeur ~900 px max)
|
||||
canvas_w = 900
|
||||
scale = canvas_w / img_w
|
||||
canvas_h = int(img_h * scale)
|
||||
|
||||
# Préparer les rectangles initiaux depuis la config
|
||||
initial_rects = []
|
||||
for zone_name, z in existing_zones.items():
|
||||
if not isinstance(z, dict): continue
|
||||
initial_rects.append({
|
||||
"type": "rect",
|
||||
"left": z["x1"] * canvas_w,
|
||||
"top": z["y1"] * canvas_h,
|
||||
"width": (z["x2"] - z["x1"]) * canvas_w,
|
||||
"height": (z["y2"] - z["y1"]) * canvas_h,
|
||||
"fill": "rgba(255, 100, 100, 0.15)",
|
||||
"stroke": "red",
|
||||
"strokeWidth": 2,
|
||||
"label_name": zone_name,
|
||||
})
|
||||
|
||||
st.caption(
|
||||
"💡 Dessine un rectangle par zone à la souris. Les zones existantes "
|
||||
"apparaissent déjà pré-dessinées. Tu peux les modifier (drag), "
|
||||
"en ajouter, ou en supprimer (touche Suppr) puis cliquer sur "
|
||||
"**Sauvegarder**."
|
||||
)
|
||||
|
||||
drawing_mode = st.radio(
|
||||
"Mode", ["rect", "transform"], horizontal=True,
|
||||
format_func=lambda x: {"rect": "✏️ Dessiner", "transform": "🖱 Sélectionner / Déplacer"}[x],
|
||||
key="calib_drawing_mode",
|
||||
)
|
||||
|
||||
canvas_result = st_canvas(
|
||||
fill_color="rgba(255, 100, 100, 0.15)",
|
||||
stroke_width=2,
|
||||
stroke_color="red",
|
||||
background_image=img,
|
||||
update_streamlit=True,
|
||||
width=canvas_w,
|
||||
height=canvas_h,
|
||||
drawing_mode=drawing_mode,
|
||||
initial_drawing={"objects": initial_rects, "version": "5.2.1"},
|
||||
key="calib_canvas",
|
||||
)
|
||||
|
||||
# Reconstituer la config à partir des rectangles dessinés
|
||||
rects = (canvas_result.json_data or {}).get("objects", []) if canvas_result.json_data else []
|
||||
|
||||
st.markdown("### Zones détectées")
|
||||
if not rects:
|
||||
st.info("Aucun rectangle dessiné.")
|
||||
return
|
||||
|
||||
new_zones = {}
|
||||
for i, r in enumerate(rects):
|
||||
if r.get("type") != "rect":
|
||||
continue
|
||||
# Récupérer le nom existant si présent, sinon demander
|
||||
default_name = r.get("label_name") or f"zone_{i+1}"
|
||||
name = st.text_input(
|
||||
f"Nom de la zone {i+1}",
|
||||
value=default_name, key=f"calib_name_{i}",
|
||||
)
|
||||
x1 = r["left"] / canvas_w
|
||||
y1 = r["top"] / canvas_h
|
||||
x2 = x1 + r["width"] / canvas_w
|
||||
y2 = y1 + r["height"] / canvas_h
|
||||
desc = existing_zones.get(name, {}).get("description", "")
|
||||
desc = st.text_input(
|
||||
f"Description (optionnel)", value=desc, key=f"calib_desc_{i}",
|
||||
)
|
||||
st.caption(f"Coords relatives : ({x1:.3f}, {y1:.3f}) → ({x2:.3f}, {y2:.3f})")
|
||||
new_zones[name] = {"x1": round(x1, 4), "y1": round(y1, 4),
|
||||
"x2": round(x2, 4), "y2": round(y2, 4),
|
||||
"description": desc}
|
||||
|
||||
if st.button("💾 Sauvegarder la configuration", type="primary"):
|
||||
cfg[page_type] = new_zones
|
||||
path = save_config(cfg)
|
||||
st.success(f"Configuration sauvegardée : {path}")
|
||||
st.json(new_zones)
|
||||
|
||||
|
||||
def main():
|
||||
st.set_page_config(page_title="OGC Overlay", layout="wide")
|
||||
|
||||
@@ -280,6 +441,13 @@ def main():
|
||||
|
||||
st.title("🩺 Extraction OGC — review & gold set")
|
||||
|
||||
# Sélecteur de mode en haut de sidebar
|
||||
with st.sidebar:
|
||||
mode = st.radio("Mode", ["📋 Review dossier", "🔧 Calibration zones"])
|
||||
if mode == "🔧 Calibration zones":
|
||||
render_calibration_page()
|
||||
return
|
||||
|
||||
pdfs = list_pdfs()
|
||||
if not pdfs:
|
||||
st.error(f"Aucun PDF trouvé dans {PDF_DIR}")
|
||||
|
||||
90
pipeline/zones_config.py
Normal file
90
pipeline/zones_config.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""Configuration des zones d'extraction éditable via l'overlay UI.
|
||||
|
||||
Les coordonnées sont relatives (0..1) dans l'image source. Elles sont chargées
|
||||
au démarrage du pipeline et utilisées à la place des constantes en dur dans
|
||||
`pipeline/prompts.py` et `pipeline/checkboxes.py` — avec fallback sur ces
|
||||
constantes si la config n'est pas présente, pour ne pas casser l'existant.
|
||||
|
||||
Structure :
|
||||
{
|
||||
"recueil": {
|
||||
"codage_reco": {"x1":0.77, "y1":0.330, "x2":0.97, "y2":0.490, "description":"..."},
|
||||
"accord_checkbox": {"x1":..., "y1":..., "x2":..., "y2":..., "description":"..."},
|
||||
"desaccord_checkbox":{...}
|
||||
},
|
||||
"concertation_2": {...}
|
||||
}
|
||||
|
||||
Un fichier unique `zones_config.json` à la racine du projet, ou au chemin pointé
|
||||
par la variable d'env `OGC_ZONES_CONFIG`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path(
|
||||
os.environ.get("OGC_ZONES_CONFIG", "zones_config.json")
|
||||
)
|
||||
|
||||
|
||||
# Zones par défaut, identiques aux constantes actuelles dans prompts.py et
|
||||
# checkboxes.py. Sert de fallback et de "mise à jour initiale" quand le
|
||||
# fichier n'existe pas encore.
|
||||
DEFAULTS: dict = {
|
||||
"recueil": {
|
||||
"codage_reco": {
|
||||
"x1": 0.77, "y1": 0.330, "x2": 0.97, "y2": 0.490,
|
||||
"description": "Colonne Recodage (DP / DR / DAS) — exclut le bloc Actes",
|
||||
},
|
||||
"accord_checkbox": {
|
||||
"x1": 0.588, "y1": 0.838, "x2": 0.622, "y2": 0.860,
|
||||
"description": "Case à cocher 'Accord'",
|
||||
},
|
||||
"desaccord_checkbox": {
|
||||
"x1": 0.588, "y1": 0.858, "x2": 0.622, "y2": 0.880,
|
||||
"description": "Case à cocher 'Désaccord'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_config(path: Path = DEFAULT_CONFIG_PATH) -> dict:
|
||||
"""Charge la config JSON, ou retourne les defaults si absente."""
|
||||
if not path.exists():
|
||||
return _deep_copy(DEFAULTS)
|
||||
try:
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return _deep_copy(DEFAULTS)
|
||||
# Merge : les defaults sont une base, la config utilisateur vient par-dessus
|
||||
merged = _deep_copy(DEFAULTS)
|
||||
for page, zones in raw.items():
|
||||
merged.setdefault(page, {}).update(zones)
|
||||
return merged
|
||||
|
||||
|
||||
def save_config(cfg: dict, path: Path = DEFAULT_CONFIG_PATH) -> Path:
|
||||
path.write_text(json.dumps(cfg, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def get_zone(page_type: str, zone_name: str,
|
||||
config: dict | None = None) -> tuple[float, float, float, float] | None:
|
||||
"""Récupère une zone depuis la config ou les defaults.
|
||||
|
||||
Retourne (x1, y1, x2, y2) ou None si inconnue.
|
||||
"""
|
||||
cfg = config or load_config()
|
||||
z = cfg.get(page_type, {}).get(zone_name)
|
||||
if not isinstance(z, dict):
|
||||
return None
|
||||
try:
|
||||
return (float(z["x1"]), float(z["y1"]), float(z["x2"]), float(z["y2"]))
|
||||
except (KeyError, ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
def _deep_copy(d: dict) -> dict:
|
||||
return json.loads(json.dumps(d))
|
||||
160
tests/test_checkboxes.py
Normal file
160
tests/test_checkboxes.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Tests unitaires pour pipeline.checkboxes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from pipeline.checkboxes import (
|
||||
AMBIGU_MARGIN,
|
||||
CheckboxZones,
|
||||
RECUEIL_ACCORD_DESACCORD,
|
||||
dark_ratio,
|
||||
detect_accord_desaccord,
|
||||
parse_ghs_injustifie,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# parse_ghs_injustifie
|
||||
# ============================================================
|
||||
|
||||
class TestParseGhsInjustifie:
|
||||
@pytest.mark.parametrize("raw,expected", [
|
||||
("0", "0"),
|
||||
("1", "1"),
|
||||
("0 SE 1 2 3 4 ATU FFM FSD", "0"),
|
||||
("1 SE 2 ATU", "1"),
|
||||
(" 0 ", "0"),
|
||||
("", ""),
|
||||
(None, ""),
|
||||
("SE 1 2 3 4 ATU FFM FSD", ""), # pas de chiffre de tête
|
||||
("abc", ""),
|
||||
("2 SE 1", ""), # 2 n'est ni 0 ni 1
|
||||
])
|
||||
def test_cas_varies(self, raw, expected):
|
||||
assert parse_ghs_injustifie(raw) == expected
|
||||
|
||||
|
||||
# ============================================================
|
||||
# dark_ratio (avec images synthétiques)
|
||||
# ============================================================
|
||||
|
||||
def _solid_image(w: int, h: int, gray_value: int = 255) -> Image.Image:
|
||||
arr = np.full((h, w), gray_value, dtype=np.uint8)
|
||||
return Image.fromarray(arr, mode="L").convert("RGB")
|
||||
|
||||
|
||||
def _image_with_dark_square(w: int, h: int,
|
||||
square_bbox: tuple[float, float, float, float]) -> Image.Image:
|
||||
"""Image blanche avec un carré noir dans la zone bbox (coords relatives)."""
|
||||
arr = np.full((h, w), 255, dtype=np.uint8)
|
||||
x1, y1, x2, y2 = square_bbox
|
||||
arr[int(y1*h):int(y2*h), int(x1*w):int(x2*w)] = 0
|
||||
return Image.fromarray(arr, mode="L").convert("RGB")
|
||||
|
||||
|
||||
class TestDarkRatio:
|
||||
def test_image_blanche(self):
|
||||
img = _solid_image(100, 100, 255)
|
||||
ratio = dark_ratio(img, (0.2, 0.2, 0.8, 0.8))
|
||||
assert ratio == 0.0
|
||||
|
||||
def test_image_noire(self):
|
||||
img = _solid_image(100, 100, 0)
|
||||
ratio = dark_ratio(img, (0.2, 0.2, 0.8, 0.8))
|
||||
assert ratio == 1.0
|
||||
|
||||
def test_inner_frac_ignore_les_bords(self):
|
||||
"""Un carré noir occupe toute la zone mais avec un grand inner_frac
|
||||
on ne voit que le centre, qui reste dans la zone noire."""
|
||||
img = _image_with_dark_square(100, 100, (0.0, 0.0, 1.0, 1.0))
|
||||
# Tout noir, peu importe inner_frac
|
||||
assert dark_ratio(img, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35) == 1.0
|
||||
|
||||
def test_cadre_seul_vs_contenu_central(self):
|
||||
"""Une case 'vide' (cadre seul) doit avoir un ratio inner_frac faible ;
|
||||
une case 'cochée' (croix au centre) doit avoir un ratio plus élevé."""
|
||||
# Simuler un cadre : carré noir sur le pourtour uniquement
|
||||
w, h = 100, 100
|
||||
arr = np.full((h, w), 255, dtype=np.uint8)
|
||||
arr[:5, :] = 0; arr[-5:, :] = 0; arr[:, :5] = 0; arr[:, -5:] = 0
|
||||
frame_only = Image.fromarray(arr, mode="L").convert("RGB")
|
||||
# Cadre + croix au centre
|
||||
arr2 = arr.copy()
|
||||
# Une croix : 2 diagonales
|
||||
for i in range(20, 80):
|
||||
arr2[i, i] = 0
|
||||
arr2[i, 100 - 1 - i] = 0
|
||||
checked = Image.fromarray(arr2, mode="L").convert("RGB")
|
||||
|
||||
ratio_empty = dark_ratio(frame_only, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35)
|
||||
ratio_full = dark_ratio(checked, (0.0, 0.0, 1.0, 1.0), inner_frac=0.35)
|
||||
|
||||
# La case cochée doit avoir un ratio clairement plus élevé
|
||||
assert ratio_full > ratio_empty + 0.05
|
||||
|
||||
|
||||
# ============================================================
|
||||
# detect_accord_desaccord (fixtures cache)
|
||||
# ============================================================
|
||||
|
||||
class TestDetectAccordDesaccord:
|
||||
"""Tests sur les images réelles du cache, avec ground truth vérifié
|
||||
visuellement (cf. historique du projet, crops audités un par un).
|
||||
|
||||
Ground truth indexé par numéro d'OGC — le mapping vers le hash du cache
|
||||
est résolu au runtime via pipeline.ingest.pdf_hash pour éviter de coder
|
||||
les hashes en dur (fragile).
|
||||
"""
|
||||
|
||||
# Ground truth vérifié visuellement sur les 18 dossiers 2018 CARC
|
||||
GROUND_TRUTH_BY_OGC = {
|
||||
1: "accord",
|
||||
7: "accord",
|
||||
9: "désaccord",
|
||||
18: "désaccord",
|
||||
20: "désaccord",
|
||||
27: "désaccord",
|
||||
29: "accord",
|
||||
55: "accord",
|
||||
66: "désaccord",
|
||||
68: "accord",
|
||||
69: "accord",
|
||||
74: "désaccord",
|
||||
76: "désaccord",
|
||||
84: "accord",
|
||||
86: "désaccord",
|
||||
97: "accord",
|
||||
99: "désaccord",
|
||||
}
|
||||
|
||||
@pytest.fixture
|
||||
def cached_pages_with_truth(self):
|
||||
"""Résout le mapping numéro OGC → page_01.png disponible au runtime."""
|
||||
from pathlib import Path
|
||||
from pipeline.ingest import pdf_hash
|
||||
pdf_dir = Path("2018 CARC")
|
||||
if not pdf_dir.is_dir():
|
||||
pytest.skip("répertoire 2018 CARC/ absent")
|
||||
found = {}
|
||||
for n, expected in self.GROUND_TRUTH_BY_OGC.items():
|
||||
pdf = pdf_dir / f"OGC {n}.pdf"
|
||||
if not pdf.exists():
|
||||
continue
|
||||
h = pdf_hash(str(pdf))
|
||||
img = Path(f".cache/images/{h}/page_01.png")
|
||||
if img.exists():
|
||||
found[f"OGC {n}"] = (str(img), expected)
|
||||
if not found:
|
||||
pytest.skip("pas de cache d'images disponible — lance le pipeline d'abord")
|
||||
return found
|
||||
|
||||
def test_ground_truth_echantillon(self, cached_pages_with_truth):
|
||||
"""Sur les cas vérifiés visuellement, le détecteur doit matcher."""
|
||||
errors = []
|
||||
for name, (path, expected) in cached_pages_with_truth.items():
|
||||
r = detect_accord_desaccord(path)
|
||||
if r["decision"] != expected:
|
||||
errors.append(f"{name}: attendu={expected}, got={r}")
|
||||
assert not errors, "\n".join(errors)
|
||||
140
tests/test_deskew.py
Normal file
140
tests/test_deskew.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Tests unitaires pour pipeline.deskew.
|
||||
|
||||
Tests sans dépendance GPU. Génère des images synthétiques en code + utilise
|
||||
les images du cache pour les cas réels.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from pipeline.deskew import (
|
||||
MAX_ANGLE_DEG,
|
||||
MIN_ANGLE_DEG,
|
||||
NEAR_HORIZONTAL_BAND,
|
||||
deskew_image,
|
||||
detect_skew_angle,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Helpers : fabriquer une image synthétique avec des lignes
|
||||
# ============================================================
|
||||
|
||||
def _make_grid_image(w: int = 800, h: int = 1000,
|
||||
n_lines: int = 30, angle_deg: float = 0.0) -> Image.Image:
|
||||
"""Crée une image blanche avec `n_lines` lignes horizontales équi-réparties,
|
||||
optionnellement tournée d'un angle donné. Parfaite pour tester le détecteur.
|
||||
"""
|
||||
arr = np.ones((h, w), dtype=np.uint8) * 255
|
||||
for i in range(1, n_lines + 1):
|
||||
y = int(i * h / (n_lines + 1))
|
||||
arr[y - 1:y + 1, 50:w - 50] = 0 # ligne horizontale noire de 2 px
|
||||
img = Image.fromarray(arr, mode="L")
|
||||
if angle_deg != 0.0:
|
||||
# PIL.rotate : angle positif = sens trigonométrique (= anti-horaire)
|
||||
# On veut tester avec notre convention (positif = horaire) donc
|
||||
# on inverse ici pour cohérence avec detect_skew_angle
|
||||
img = img.rotate(-angle_deg, resample=Image.Resampling.BICUBIC,
|
||||
expand=False, fillcolor="white")
|
||||
return img.convert("RGB")
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Tests de détection
|
||||
# ============================================================
|
||||
|
||||
class TestDetectSkewAngle:
|
||||
def test_image_parfaitement_droite(self):
|
||||
img = _make_grid_image()
|
||||
angle = detect_skew_angle(img)
|
||||
assert abs(angle) < 0.1, f"image droite doit donner ~0°, got {angle}"
|
||||
|
||||
@pytest.mark.parametrize("input_angle", [1.0, 2.0, -3.0, 4.0])
|
||||
def test_detecte_angles_modérés(self, input_angle):
|
||||
"""Sur notre image synthétique (30 lignes), la sensibilité est ~1°.
|
||||
Sur de vraies fiches OGC avec 300+ lignes de tableaux, la sensibilité
|
||||
descend à 0.3° (cf. test réel sur OGC 1 : +0.91° détecté).
|
||||
"""
|
||||
img = _make_grid_image(angle_deg=input_angle)
|
||||
detected = detect_skew_angle(img)
|
||||
assert abs(detected - input_angle) < 0.5, \
|
||||
f"attendu ~{input_angle}°, détecté {detected}°"
|
||||
|
||||
def test_image_sans_lignes_retourne_zero(self):
|
||||
# Image totalement uniforme → aucune ligne détectable
|
||||
arr = np.ones((500, 500), dtype=np.uint8) * 255
|
||||
img = Image.fromarray(arr, mode="L").convert("RGB")
|
||||
assert detect_skew_angle(img) == 0.0
|
||||
|
||||
def test_angle_extrême_rejeté(self):
|
||||
# Une rotation de 45° dépasse MAX_ANGLE_DEG → on refuse de corriger
|
||||
img = _make_grid_image(angle_deg=45.0)
|
||||
detected = detect_skew_angle(img)
|
||||
# Soit 0.0 (pas de lignes quasi-horizontales à ±15°), soit borné
|
||||
assert abs(detected) < MAX_ANGLE_DEG or detected == 0.0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Tests de correction (deskew_image)
|
||||
# ============================================================
|
||||
|
||||
class TestDeskewImage:
|
||||
def test_image_droite_inchangée(self):
|
||||
img = _make_grid_image()
|
||||
rotated, applied = deskew_image(img)
|
||||
assert applied == 0.0
|
||||
# Identité bit à bit
|
||||
assert np.array_equal(np.array(rotated), np.array(img))
|
||||
|
||||
def test_image_inclinée_corrigée(self):
|
||||
img = _make_grid_image(angle_deg=2.0)
|
||||
rotated, applied = deskew_image(img)
|
||||
# On attend qu'on applique un angle proche de 2° (convention positive)
|
||||
assert abs(applied) > MIN_ANGLE_DEG, \
|
||||
f"devrait corriger, got applied={applied}"
|
||||
# Après rotation, l'angle résiduel doit être très faible
|
||||
residual = detect_skew_angle(rotated)
|
||||
assert abs(residual) < 0.5, \
|
||||
f"angle résiduel trop grand après correction : {residual}°"
|
||||
|
||||
def test_seuil_min_angle_respecté(self):
|
||||
# Un skew juste sous le seuil ne doit pas être corrigé
|
||||
img = _make_grid_image(angle_deg=MIN_ANGLE_DEG / 2)
|
||||
_, applied = deskew_image(img)
|
||||
assert applied == 0.0
|
||||
|
||||
def test_angle_forcé(self):
|
||||
"""On peut forcer un angle arbitraire indépendamment de la détection."""
|
||||
img = _make_grid_image() # droit
|
||||
rotated, applied = deskew_image(img, angle=5.0)
|
||||
assert applied == 5.0
|
||||
# Taille conservée
|
||||
assert rotated.size == img.size
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Tests avec fixtures réelles (si cache dispo)
|
||||
# ============================================================
|
||||
|
||||
class TestOnRealCachedPages:
|
||||
"""Ces tests s'exécutent seulement si le cache d'images existe."""
|
||||
|
||||
@pytest.fixture
|
||||
def cached_pages(self):
|
||||
paths = sorted(Path(".cache/images").glob("*/page_01.png"))
|
||||
if not paths:
|
||||
pytest.skip("pas de cache d'images disponible")
|
||||
return paths
|
||||
|
||||
def test_detection_ne_crash_pas(self, cached_pages):
|
||||
"""Sur toutes les pages cachées, detect_skew_angle ne doit pas planter."""
|
||||
for p in cached_pages[:5]: # limite pour la vitesse
|
||||
img = Image.open(p)
|
||||
angle = detect_skew_angle(img)
|
||||
assert isinstance(angle, float)
|
||||
assert abs(angle) <= MAX_ANGLE_DEG
|
||||
119
tests/test_json_utils.py
Normal file
119
tests/test_json_utils.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""Tests unitaires pour pipeline.json_utils."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pipeline.json_utils import (
|
||||
close_open_json,
|
||||
parse_json_output,
|
||||
patch_missing_commas,
|
||||
strip_fences,
|
||||
truncate_empty_loop,
|
||||
)
|
||||
|
||||
|
||||
class TestStripFences:
|
||||
def test_fence_json(self):
|
||||
raw = '```json\n{"a": 1}\n```'
|
||||
assert strip_fences(raw).strip() == '{"a": 1}'
|
||||
|
||||
def test_fence_simple(self):
|
||||
raw = '```\n{"a": 1}\n```'
|
||||
assert strip_fences(raw).strip() == '{"a": 1}'
|
||||
|
||||
def test_pas_de_fence(self):
|
||||
raw = '{"a": 1}'
|
||||
assert strip_fences(raw).strip() == '{"a": 1}'
|
||||
|
||||
|
||||
class TestPatchMissingCommas:
|
||||
def test_objets_consecutifs(self):
|
||||
raw = '[\n{"a": 1}\n{"b": 2}\n]'
|
||||
patched = patch_missing_commas(raw)
|
||||
assert '},' in patched
|
||||
|
||||
def test_deja_correct(self):
|
||||
raw = '{"a": 1}'
|
||||
assert patch_missing_commas(raw) == raw
|
||||
|
||||
|
||||
class TestTruncateEmptyLoop:
|
||||
def test_moins_que_seuil(self):
|
||||
raw = '[{"code":"","position":""},{"code":"","position":""}]'
|
||||
# 2 objets vides = seuil par défaut, rien à tronquer
|
||||
out = truncate_empty_loop(raw, max_consecutive=2)
|
||||
assert out == raw
|
||||
|
||||
def test_boucle_tronquée(self):
|
||||
objs = ['{"code":"","position":""}'] * 10
|
||||
raw = '[' + ','.join(objs)
|
||||
out = truncate_empty_loop(raw, max_consecutive=2)
|
||||
# Après troncature, ne doit contenir que 2 occurrences
|
||||
assert out.count('{"code":""') == 2
|
||||
|
||||
def test_pas_de_boucle(self):
|
||||
raw = '[{"code":"K650","position":"1"}]'
|
||||
assert truncate_empty_loop(raw) == raw
|
||||
|
||||
|
||||
class TestCloseOpenJson:
|
||||
def test_deja_ferme(self):
|
||||
raw = '{"a": [1, 2]}'
|
||||
assert close_open_json(raw) == raw
|
||||
|
||||
def test_accolade_manquante(self):
|
||||
raw = '{"a": 1'
|
||||
closed = close_open_json(raw)
|
||||
assert closed == '{"a": 1}'
|
||||
|
||||
def test_crochet_manquant(self):
|
||||
raw = '{"a": [1, 2'
|
||||
closed = close_open_json(raw)
|
||||
assert closed == '{"a": [1, 2]}'
|
||||
|
||||
def test_accolades_et_crochets_imbriqués(self):
|
||||
raw = '{"a": {"b": [1, 2'
|
||||
closed = close_open_json(raw)
|
||||
assert closed == '{"a": {"b": [1, 2]}}'
|
||||
|
||||
def test_virgule_trainante_supprimée(self):
|
||||
raw = '{"a": 1, '
|
||||
closed = close_open_json(raw)
|
||||
assert closed == '{"a": 1}'
|
||||
|
||||
def test_accolade_dans_string_ignorée(self):
|
||||
raw = '{"a": "{ ceci est une { accolade dans une string"'
|
||||
closed = close_open_json(raw)
|
||||
# On ajoute juste l'accolade finale manquante
|
||||
assert closed == raw + '}'
|
||||
|
||||
|
||||
class TestParseJsonOutput:
|
||||
def test_json_valide(self):
|
||||
assert parse_json_output('{"a": 1}') == {"a": 1}
|
||||
|
||||
def test_vide(self):
|
||||
assert parse_json_output("") is None
|
||||
assert parse_json_output(None) is None
|
||||
|
||||
def test_fences_markdown(self):
|
||||
assert parse_json_output('```json\n{"a": 1}\n```') == {"a": 1}
|
||||
|
||||
def test_virgule_manquante_recuperee(self):
|
||||
raw = '[\n{"a": 1}\n{"b": 2}\n]'
|
||||
result = parse_json_output(raw)
|
||||
assert result == [{"a": 1}, {"b": 2}]
|
||||
|
||||
def test_boucle_tronquée_fermée(self):
|
||||
objs = ['{"code":"","position":"","libelle":""}'] * 10
|
||||
raw = '{"das": [\n' + ',\n'.join(objs) # non fermé
|
||||
result = parse_json_output(raw)
|
||||
assert isinstance(result, dict)
|
||||
assert "das" in result
|
||||
# Après troncature, 2 objets vides max, puis JSON refermé
|
||||
assert result.get("_truncated_loop") is True
|
||||
|
||||
def test_fallback_retourne_raw(self):
|
||||
"""Quand rien ne marche, on renvoie un dict avec _raw + _parse_error."""
|
||||
raw = "ceci n'est pas du JSON du tout !"
|
||||
result = parse_json_output(raw)
|
||||
assert result.get("_raw") == raw
|
||||
assert "_parse_error" in result
|
||||
145
tests/test_recueil.py
Normal file
145
tests/test_recueil.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""Tests unitaires pour pipeline.recueil (logique métier de la page recueil).
|
||||
|
||||
Les fonctions testées ici sont toutes pures (pas d'appel au VLM) :
|
||||
- filter_cim10_codes
|
||||
- classify_codes_dp_dr_das
|
||||
- merge_codage_reco
|
||||
- resolve_recueil_zones (juste lecture de config)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pipeline.recueil import (
|
||||
classify_codes_dp_dr_das,
|
||||
filter_cim10_codes,
|
||||
merge_codage_reco,
|
||||
resolve_recueil_zones,
|
||||
)
|
||||
|
||||
|
||||
class TestFilterCim10Codes:
|
||||
def test_codes_valides_conservés(self):
|
||||
codes = [
|
||||
{"code": "K650", "position": "1"},
|
||||
{"code": "T814", "position": "2"},
|
||||
{"code": "Z954 *", "position": "3"},
|
||||
]
|
||||
out = filter_cim10_codes(codes)
|
||||
assert len(out) == 3
|
||||
assert out[0]["code"] == "K650"
|
||||
|
||||
def test_ccam_rejeté(self):
|
||||
"""Un code CCAM (4 lettres + 3 chiffres) ne doit pas passer le filtre CIM-10."""
|
||||
codes = [
|
||||
{"code": "K650", "position": ""},
|
||||
{"code": "EBFA012", "position": "1"}, # CCAM
|
||||
]
|
||||
out = filter_cim10_codes(codes)
|
||||
assert len(out) == 1
|
||||
assert out[0]["code"] == "K650"
|
||||
|
||||
def test_code_vide_rejeté(self):
|
||||
codes = [{"code": "", "position": ""}, {"code": "K650", "position": ""}]
|
||||
out = filter_cim10_codes(codes)
|
||||
assert len(out) == 1
|
||||
|
||||
def test_non_dict_ignoré(self):
|
||||
codes = ["K650", None, {"code": "T814", "position": ""}]
|
||||
out = filter_cim10_codes(codes)
|
||||
assert len(out) == 1
|
||||
|
||||
def test_liste_vide(self):
|
||||
assert filter_cim10_codes([]) == []
|
||||
assert filter_cim10_codes(None) == []
|
||||
|
||||
|
||||
class TestClassifyCodesDpDrDas:
|
||||
def test_cas_nominal(self):
|
||||
"""1er sans position = DP, 2e sans position = DR, puis DAS avec positions."""
|
||||
codes = [
|
||||
{"code": "K650", "position": ""},
|
||||
{"code": "T814", "position": ""},
|
||||
{"code": "Z954", "position": "2"},
|
||||
{"code": "R33", "position": "3"},
|
||||
]
|
||||
dp, dr, das = classify_codes_dp_dr_das(codes)
|
||||
assert dp == "K650"
|
||||
assert dr == "T814"
|
||||
assert [d["code"] for d in das] == ["Z954", "R33"]
|
||||
|
||||
def test_dr_vide_non_duplique_dp(self):
|
||||
"""Quand Qwen duplique le DP (parce que DR est visuellement vide),
|
||||
on doit considérer que DR est vide, pas DR = DP."""
|
||||
codes = [
|
||||
{"code": "K650", "position": ""},
|
||||
{"code": "K650", "position": ""}, # doublon
|
||||
{"code": "T814", "position": "2"},
|
||||
]
|
||||
dp, dr, das = classify_codes_dp_dr_das(codes)
|
||||
assert dp == "K650"
|
||||
assert dr == "" # dédupliqué
|
||||
assert len(das) == 1
|
||||
|
||||
def test_seulement_dp(self):
|
||||
codes = [{"code": "K650", "position": ""}]
|
||||
dp, dr, das = classify_codes_dp_dr_das(codes)
|
||||
assert dp == "K650"
|
||||
assert dr == ""
|
||||
assert das == []
|
||||
|
||||
def test_tous_avec_positions(self):
|
||||
"""Si tous les codes ont une position, DP et DR sont vides, tout en DAS."""
|
||||
codes = [
|
||||
{"code": "K650", "position": "1"},
|
||||
{"code": "T814", "position": "2"},
|
||||
]
|
||||
dp, dr, das = classify_codes_dp_dr_das(codes)
|
||||
assert dp == ""
|
||||
assert dr == ""
|
||||
assert len(das) == 2
|
||||
|
||||
def test_vide(self):
|
||||
dp, dr, das = classify_codes_dp_dr_das([])
|
||||
assert (dp, dr, das) == ("", "", [])
|
||||
|
||||
|
||||
class TestMergeCodageReco:
|
||||
def test_crop_prime_sur_passage_principal(self):
|
||||
parsed = {"codage_reco": {"dp": "", "dr": "", "das": []}}
|
||||
reco = {"dp": "K650", "dr": "T814",
|
||||
"das": [{"code": "Z954", "position": "2"}]}
|
||||
merge_codage_reco(parsed, reco)
|
||||
assert parsed["codage_reco"]["dp"] == "K650"
|
||||
assert parsed["codage_reco"]["dr"] == "T814"
|
||||
assert len(parsed["codage_reco"]["das"]) == 1
|
||||
|
||||
def test_crop_vide_garde_passage_principal(self):
|
||||
"""Si le crop a un champ vide mais le passage principal l'avait rempli,
|
||||
on ne dégrade pas : on garde le passage principal."""
|
||||
parsed = {"codage_reco": {"dp": "K650", "dr": "", "das": []}}
|
||||
reco = {"dp": "", "dr": "", "das": []}
|
||||
merge_codage_reco(parsed, reco)
|
||||
assert parsed["codage_reco"]["dp"] == "K650" # préservé
|
||||
|
||||
def test_codage_reco_initialement_absent(self):
|
||||
parsed = {}
|
||||
reco = {"dp": "K650", "dr": "", "das": []}
|
||||
merge_codage_reco(parsed, reco)
|
||||
assert parsed["codage_reco"]["dp"] == "K650"
|
||||
|
||||
def test_trace_crop_ajoutee(self):
|
||||
parsed = {"codage_reco": {"dp": "", "dr": "", "das": []}}
|
||||
reco = {"dp": "K650", "_elapsed_s": 1.5}
|
||||
merge_codage_reco(parsed, reco)
|
||||
assert parsed["_crop_recodage"]["result"]["_elapsed_s"] == 1.5
|
||||
|
||||
|
||||
class TestResolveRecueilZones:
|
||||
def test_fallback_constantes(self):
|
||||
"""Sans config utilisateur, on a les zones par défaut."""
|
||||
reco, cb = resolve_recueil_zones()
|
||||
# 4 coords flottantes
|
||||
assert len(reco) == 4
|
||||
assert all(isinstance(v, float) for v in reco)
|
||||
# Checkbox zones
|
||||
assert len(cb.accord) == 4
|
||||
assert len(cb.desaccord) == 4
|
||||
118
tests/test_schema.py
Normal file
118
tests/test_schema.py
Normal file
@@ -0,0 +1,118 @@
|
||||
"""Tests unitaires pour pipeline.schema (nettoyage JSON)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pipeline.schema import (
|
||||
CLEAN_FIELDS_RECUEIL,
|
||||
DEBUG_FIELDS,
|
||||
SCHEMA_VERSION,
|
||||
clean_dossier,
|
||||
)
|
||||
|
||||
|
||||
def _sample_raw():
|
||||
"""Un JSON pipeline type, riche en champs debug."""
|
||||
return {
|
||||
"fichier": "OGC 7",
|
||||
"pdf_hash": "abc123",
|
||||
"pages": [{"page": 1, "type": "recueil"}],
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"etablissement": "CLINIQUE X",
|
||||
"finess": "330780206",
|
||||
"ghm_etab": "11M122",
|
||||
"ghs_etab": "4323",
|
||||
"codage_etab": {"dp": "K650"},
|
||||
"accord_desaccord": "accord",
|
||||
"_checkbox_debug": {"ratio_accord": 0.38, "ratio_desaccord": 0.19},
|
||||
"_parse_error": "whatever",
|
||||
"_truncated_loop": True,
|
||||
"_crop_recodage": {"dp": "K650", "_source": "crop"},
|
||||
"_validation": {
|
||||
"summary": {"valid": 3, "invalid": 0, "empty": 2, "total_codes": 3},
|
||||
"cross_checks": {
|
||||
"etab": {"checked": True, "coherent": True},
|
||||
"reco": {"checked": False, "reason": "ghm manquant"},
|
||||
},
|
||||
"codage_etab": {
|
||||
"dp": {"code": "K650", "valid": True, "libelle_ref": "Péritonite"},
|
||||
"dr": {"code": "", "valid": None},
|
||||
"das": [],
|
||||
},
|
||||
"codage_reco": {"dp": {}, "dr": {}, "das": []},
|
||||
"ghm_etab": {"code": "11M122", "valid": True,
|
||||
"ghs_possibles": ["4323"]},
|
||||
"ghs_etab": {"code": "4323", "valid": True},
|
||||
"ghm_reco": {"code": "", "valid": None},
|
||||
"ghs_reco": {"code": "", "valid": None},
|
||||
},
|
||||
},
|
||||
"concertation_2": {
|
||||
"ghs_initial": "4323",
|
||||
"ghs_final": "4323",
|
||||
"decision": "retour_groupage_dim",
|
||||
"date_concertation": "13/03/2018",
|
||||
},
|
||||
},
|
||||
"_meta": {"pipeline_version": "v2", "ocr_model": "Qwen/Qwen2.5-VL-3B-Instruct"},
|
||||
}
|
||||
|
||||
|
||||
class TestCleanDossier:
|
||||
def test_retourne_schema_version(self):
|
||||
out = clean_dossier(_sample_raw())
|
||||
assert out["schema_version"] == SCHEMA_VERSION
|
||||
|
||||
def test_retire_tous_les_champs_debug(self):
|
||||
"""Aucun champ de DEBUG_FIELDS ne doit rester dans la sortie clean."""
|
||||
out = clean_dossier(_sample_raw())
|
||||
rec = out["extraction"]["recueil"]
|
||||
for debug_field in DEBUG_FIELDS:
|
||||
assert debug_field not in rec, \
|
||||
f"{debug_field} devrait être retiré"
|
||||
|
||||
def test_garde_les_champs_metier(self):
|
||||
out = clean_dossier(_sample_raw())
|
||||
rec = out["extraction"]["recueil"]
|
||||
for f in ["etablissement", "finess", "ghm_etab", "ghs_etab",
|
||||
"codage_etab", "accord_desaccord"]:
|
||||
assert f in rec, f"{f} doit être présent dans clean"
|
||||
|
||||
def test_validation_compactee(self):
|
||||
"""La validation est conservée mais en format compact."""
|
||||
out = clean_dossier(_sample_raw())
|
||||
v = out["extraction"]["recueil"]["_validation"]
|
||||
# summary garde tel quel
|
||||
assert v["summary"]["valid"] == 3
|
||||
# cross_checks compactés : juste le coherent booléen (ou None)
|
||||
assert v["cross_checks"] == {
|
||||
"etab_ghm_ghs_coherent": True,
|
||||
"reco_ghm_ghs_coherent": None,
|
||||
}
|
||||
# Les codes validés gardent libelle_ref quand dispo
|
||||
assert v["codage_etab"]["dp"]["valid"] is True
|
||||
assert v["codage_etab"]["dp"].get("libelle_ref") == "Péritonite"
|
||||
|
||||
def test_concertation_2_conservee(self):
|
||||
out = clean_dossier(_sample_raw())
|
||||
c2 = out["extraction"]["concertation_2"]
|
||||
assert c2["ghs_initial"] == "4323"
|
||||
assert c2["decision"] == "retour_groupage_dim"
|
||||
|
||||
def test_champs_inconnus_ignorés(self):
|
||||
"""Un champ qui n'est pas dans CLEAN_FIELDS_RECUEIL est retiré."""
|
||||
raw = _sample_raw()
|
||||
raw["extraction"]["recueil"]["champ_inventé"] = "poubelle"
|
||||
out = clean_dossier(raw)
|
||||
assert "champ_inventé" not in out["extraction"]["recueil"]
|
||||
|
||||
def test_meta_preservee(self):
|
||||
out = clean_dossier(_sample_raw())
|
||||
assert out["_meta"]["pipeline_version"] == "v2"
|
||||
assert "Qwen" in out["_meta"]["ocr_model"]
|
||||
|
||||
def test_pas_de_modification_input(self):
|
||||
"""La fonction ne doit pas modifier l'input."""
|
||||
raw = _sample_raw()
|
||||
before = raw["extraction"]["recueil"].copy()
|
||||
_ = clean_dossier(raw)
|
||||
assert raw["extraction"]["recueil"] == before
|
||||
146
tests/test_validation.py
Normal file
146
tests/test_validation.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""Tests unitaires pour pipeline.validation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from pipeline.validation import (
|
||||
_check_ccam,
|
||||
_check_cim10,
|
||||
_check_ghm,
|
||||
_check_ghs,
|
||||
_cross_check_ghm_ghs,
|
||||
annotate,
|
||||
validate_recueil,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Vérifications par type de code
|
||||
# ============================================================
|
||||
|
||||
class TestCheckCim10:
|
||||
def test_code_valide(self):
|
||||
r = _check_cim10("K650")
|
||||
assert r["valid"] is True
|
||||
assert "libelle_ref" in r
|
||||
|
||||
def test_code_vide(self):
|
||||
assert _check_cim10("")["valid"] is None
|
||||
assert _check_cim10(None)["valid"] is None
|
||||
|
||||
def test_code_avec_suffixe_pmsi(self):
|
||||
# Les suffixes * et +N sont gérés par la normalisation
|
||||
r = _check_cim10("C795 *")
|
||||
assert r["valid"] is True
|
||||
|
||||
def test_code_invalide_avec_suggestion(self):
|
||||
# K65O (O au lieu de 0) n'existe pas, mais K650 oui
|
||||
r = _check_cim10("K65O")
|
||||
assert r["valid"] is False
|
||||
assert r.get("suggestion") == "K650"
|
||||
|
||||
def test_code_invalide_sans_suggestion(self):
|
||||
# Code farfelu sans voisin proche
|
||||
r = _check_cim10("ZZZZ9999")
|
||||
assert r["valid"] is False
|
||||
# suggestion peut être absente
|
||||
assert r.get("suggestion") is None or r.get("suggestion") != "ZZZZ9999"
|
||||
|
||||
|
||||
class TestCheckGhm:
|
||||
def test_ghm_valide(self):
|
||||
r = _check_ghm("11M122")
|
||||
assert r["valid"] is True
|
||||
assert isinstance(r.get("ghs_possibles"), list)
|
||||
assert len(r["ghs_possibles"]) > 0
|
||||
|
||||
def test_ghm_invalide(self):
|
||||
r = _check_ghm("99Z999")
|
||||
assert r["valid"] is False
|
||||
|
||||
|
||||
class TestCheckGhs:
|
||||
def test_ghs_valide(self):
|
||||
assert _check_ghs("4323")["valid"] is True
|
||||
|
||||
def test_ghs_invalide(self):
|
||||
assert _check_ghs("99999")["valid"] is False
|
||||
|
||||
|
||||
class TestCheckCcam:
|
||||
def test_ccam_valide(self):
|
||||
assert _check_ccam("EBFA012")["valid"] is True
|
||||
|
||||
def test_ccam_invalide(self):
|
||||
assert _check_ccam("XXXX000")["valid"] is False
|
||||
|
||||
|
||||
# ============================================================
|
||||
# Cross-checks GHM ↔ GHS
|
||||
# ============================================================
|
||||
|
||||
class TestCrossCheckGhmGhs:
|
||||
def test_couple_coherent(self):
|
||||
# 11M122 a bien 4323 dans ses GHS possibles
|
||||
r = _cross_check_ghm_ghs("11M122", "4323")
|
||||
assert r["checked"] is True
|
||||
assert r["coherent"] is True
|
||||
|
||||
def test_couple_incoherent(self):
|
||||
# 11M122 ne correspond pas à n'importe quel GHS
|
||||
r = _cross_check_ghm_ghs("11M122", "9999")
|
||||
assert r["checked"] is True
|
||||
assert r["coherent"] is False
|
||||
|
||||
def test_ghm_manquant(self):
|
||||
r = _cross_check_ghm_ghs("", "4323")
|
||||
assert r["checked"] is False
|
||||
|
||||
def test_ghm_invalide(self):
|
||||
r = _cross_check_ghm_ghs("99Z999", "4323")
|
||||
assert r["checked"] is False
|
||||
assert "invalide" in r["reason"].lower()
|
||||
|
||||
|
||||
# ============================================================
|
||||
# annotate (intégration)
|
||||
# ============================================================
|
||||
|
||||
class TestAnnotate:
|
||||
def test_annotate_json_vide(self):
|
||||
out = annotate({"fichier": "TEST", "extraction": {}})
|
||||
assert "fichier" in out
|
||||
assert out["extraction"] == {}
|
||||
|
||||
def test_annotate_recueil_complet(self):
|
||||
raw = {
|
||||
"fichier": "TEST",
|
||||
"extraction": {
|
||||
"recueil": {
|
||||
"codage_etab": {"dp": "K650", "dr": "", "das": [
|
||||
{"code": "T814", "position": "2"},
|
||||
]},
|
||||
"codage_reco": {"dp": "", "dr": "", "das": []},
|
||||
"ghm_etab": "11M122",
|
||||
"ghs_etab": "4323",
|
||||
"ghm_reco": "",
|
||||
"ghs_reco": "",
|
||||
},
|
||||
},
|
||||
}
|
||||
out = annotate(raw)
|
||||
v = out["extraction"]["recueil"]["_validation"]
|
||||
assert v["codage_etab"]["dp"]["valid"] is True
|
||||
assert v["ghm_etab"]["valid"] is True
|
||||
assert v["cross_checks"]["etab"]["coherent"] is True
|
||||
assert v["summary"]["valid"] >= 3
|
||||
|
||||
def test_annotate_preserve_source(self):
|
||||
"""L'annotation ne doit pas modifier l'input (copie défensive)."""
|
||||
raw = {
|
||||
"fichier": "T",
|
||||
"extraction": {"recueil": {"codage_etab": {"dp": "K650"}}},
|
||||
}
|
||||
out = annotate(raw)
|
||||
assert "_validation" not in raw["extraction"]["recueil"]
|
||||
assert "_validation" in out["extraction"]["recueil"]
|
||||
85
tests/test_zones_config.py
Normal file
85
tests/test_zones_config.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Tests unitaires pour pipeline.zones_config."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from pipeline.zones_config import (
|
||||
DEFAULTS,
|
||||
get_zone,
|
||||
load_config,
|
||||
save_config,
|
||||
)
|
||||
|
||||
|
||||
class TestLoadConfig:
|
||||
def test_fichier_absent_retourne_defaults(self, tmp_path):
|
||||
cfg = load_config(tmp_path / "inexistant.json")
|
||||
assert cfg == DEFAULTS
|
||||
|
||||
def test_charge_depuis_fichier(self, tmp_path):
|
||||
path = tmp_path / "zones.json"
|
||||
custom = {
|
||||
"recueil": {
|
||||
"codage_reco": {"x1": 0.5, "y1": 0.1, "x2": 0.9, "y2": 0.4,
|
||||
"description": "test"},
|
||||
},
|
||||
}
|
||||
path.write_text(json.dumps(custom))
|
||||
cfg = load_config(path)
|
||||
assert cfg["recueil"]["codage_reco"]["x1"] == 0.5
|
||||
|
||||
def test_merge_avec_defaults(self, tmp_path):
|
||||
"""Les zones non définies dans le fichier tombent en défaut."""
|
||||
path = tmp_path / "zones.json"
|
||||
partial = {
|
||||
"recueil": {"codage_reco": {"x1": 0.1, "y1": 0.2, "x2": 0.3, "y2": 0.4}},
|
||||
}
|
||||
path.write_text(json.dumps(partial))
|
||||
cfg = load_config(path)
|
||||
# User override appliqué
|
||||
assert cfg["recueil"]["codage_reco"]["x1"] == 0.1
|
||||
# Default gardé pour l'autre zone
|
||||
assert cfg["recueil"]["accord_checkbox"] == DEFAULTS["recueil"]["accord_checkbox"]
|
||||
|
||||
def test_json_corrompu_retombe_sur_defaults(self, tmp_path):
|
||||
path = tmp_path / "corrupt.json"
|
||||
path.write_text("{ not valid json [")
|
||||
cfg = load_config(path)
|
||||
assert cfg == DEFAULTS
|
||||
|
||||
|
||||
class TestSaveConfig:
|
||||
def test_save_puis_load_round_trip(self, tmp_path):
|
||||
path = tmp_path / "zones.json"
|
||||
original = {
|
||||
"recueil": {
|
||||
"codage_reco": {"x1": 0.11, "y1": 0.22, "x2": 0.33, "y2": 0.44,
|
||||
"description": "abc"},
|
||||
},
|
||||
}
|
||||
save_config(original, path)
|
||||
reloaded = load_config(path)
|
||||
assert reloaded["recueil"]["codage_reco"]["x1"] == 0.11
|
||||
assert reloaded["recueil"]["codage_reco"]["description"] == "abc"
|
||||
|
||||
|
||||
class TestGetZone:
|
||||
def test_zone_existante(self):
|
||||
z = get_zone("recueil", "codage_reco")
|
||||
assert isinstance(z, tuple)
|
||||
assert len(z) == 4
|
||||
assert all(isinstance(v, float) for v in z)
|
||||
|
||||
def test_zone_inconnue_retourne_none(self):
|
||||
assert get_zone("recueil", "zone_qui_nexiste_pas") is None
|
||||
assert get_zone("page_fantaisiste", "whatever") is None
|
||||
|
||||
def test_config_explicite(self):
|
||||
cfg = {
|
||||
"recueil": {
|
||||
"my_zone": {"x1": 0.0, "y1": 0.0, "x2": 1.0, "y2": 1.0},
|
||||
},
|
||||
}
|
||||
assert get_zone("recueil", "my_zone", config=cfg) == (0.0, 0.0, 1.0, 1.0)
|
||||
Reference in New Issue
Block a user