From feb7277fe93ddf17a31dbe9239ccacb8c5075854 Mon Sep 17 00:00:00 2001 From: oussi Date: Fri, 24 Apr 2026 10:39:05 +0200 Subject: [PATCH] =?UTF-8?q?version=20=C3=A0=2086%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 10 + PLAN_EXTRACTION.md | 124 + README.md | 82 + extraction.py | 1129 ++++++ generate_report.py | 683 ++++ output/bilan_extraction_qwen_ogc (2).pdf | 137 + output/bilan_extraction_qwen_ogc.pdf | 131 + output/extraction_ogc.xlsx | Bin 0 -> 32397 bytes output/extraction_ogc_raw (1).json | 3291 +++++++++++++++++ output/extraction_ogc_raw (2).json | 3593 +++++++++++++++++++ output/extraction_ogc_raw_Correction.json | 3413 ++++++++++++++++++ output/extraction_ogc_raw_qwen.json | 3979 +++++++++++++++++++++ output/rapport_qwen.pdf | 213 ++ output/rapport_timing.pdf | 213 ++ output/timing_stats.json | 782 ++++ 16 files changed, 17780 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 PLAN_EXTRACTION.md create mode 100644 README.md create mode 100644 extraction.py create mode 100644 generate_report.py create mode 100644 output/bilan_extraction_qwen_ogc (2).pdf create mode 100644 output/bilan_extraction_qwen_ogc.pdf create mode 100644 output/extraction_ogc.xlsx create mode 100644 output/extraction_ogc_raw (1).json create mode 100644 output/extraction_ogc_raw (2).json create mode 100644 output/extraction_ogc_raw_Correction.json create mode 100644 output/extraction_ogc_raw_qwen.json create mode 100644 output/rapport_qwen.pdf create mode 100644 output/rapport_timing.pdf create mode 100644 output/timing_stats.json diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..f0f52cbe81690bf718fb599cac0339bd3fa6bd83 GIT binary patch literal 6148 zcmeHKyH3L}6umALP3b^c5Mx#rr2au#g}I=90DY<*+NLTL!4`J-8#ctuz>HY<3WS81 z;9T1kanlGUTA_O@`y|)L_Q{E2*F>ahvu=f`Ohg_EV}1+OlHhhOsmR&P4p8YaX4IwL zuwQ9(q7+*a;($1?ZVvFbTc88#(2!b`_MjK+Pm7~hy9u{sCrTkuM&lOQvT1LDA%I>76L zkHQ#Oj1B6g1C>4k0P{%ez_s4Pfj%dIfyLM$G6-e50!>$Cj~L2yN8Y!*z+!CBbSI@} z#yECnWltzd&yKvW>7)XKQi}uPfa}19xh?blf0%v#cRR_QI3N!ED+g3QY=kvDlHFTV x56640kJ3cpU|wuccR^*hV|~F}F`s*Npv~tCU|=yehzvsh2xuFm5(oa&fiE#~p925@ literal 0 HcmV?d00001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dabb963 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.pyc +.venv/ +venv/ +data/config.json +data/alerts.json +*.log +scanOgc/ +CLAUDE.md +.claude/ \ No newline at end of file diff --git a/PLAN_EXTRACTION.md b/PLAN_EXTRACTION.md new file mode 100644 index 0000000..7b73c09 --- /dev/null +++ b/PLAN_EXTRACTION.md @@ -0,0 +1,124 @@ +# Plan extraction OGC → Excel + +## Contexte +Extraction des fiches OGC (Organismes de Gestion du Contrôle) scanées. +Chaque PDF = 1 dossier patient, ~6 pages. + +## Modèles utilisés +- **Vision** : `qwen3-vl:2b` via Ollama (identifie le type de page + extrait les champs) +- **Texte** : `qwen3:4b` (optionnel, structuration secondaire si besoin) + +## Architecture +``` +PDF → pdf2image (images par page) + ↓ + Pour chaque page : + → qwen3-vl:2b : identifier le type de page + → Si "Séjour d'hospitalisation complète" : SKIP + → Sinon : extraction structurée JSON via qwen3-vl:2b + ↓ + Assemblage des champs par dossier + ↓ + Sortie Excel (openpyxl / pandas) +``` + +## Types de pages + +| Titre détecté | Action | +|---|---| +| FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL | Extraire (page principale) | +| FICHE MEDICALE DE CONCERTATION | Skip (vide ou manuscrit léger) | +| Séjour d'hospitalisation complète | **SKIP** (gros manuscrit) | +| Eléments de preuve tracés au dossier | Extraire | +| FICHE ADMINISTRATIVE DE CONCERTATION 2/2 | Extraire | +| FICHE ADMINISTRATIVE DE CONCERTATION 1/2 | Extraire | + +## Champs à extraire + +### En-tête (commun) +- N° OGC, Etablissement, FINESS, Date début contrôle +- N° Champ, Libellé champ de contrôle +- Dossier manquant, Date début séjour, Date fin séjour + +### Page 1 — Données du séjour (Etablissement + Recodage) +- Age (ans), Age (jours), Sexe +- Poids d'entrée, Durée de séjour +- Mode d'entrée, Provenance, Mode de sortie, Destination +- Nb séances, Nb RUM, Nb j EXH, Type EXB, Nb j EXB + +### Page 1 — Données du RUM (Etablissement + Recodage) +- N° RUM, Lits dédiés SP, UM, IGS II +- Durée RUM (date_debut / date_fin) +- Nature suppl., Nb suppl. + +### Page 1 — Codages (listes) +- DP_etab_code, DP_etab_libelle +- DR_etab_code, DR_etab_libelle +- DAS_etab : liste de {code, rang, libelle} +- Actes_etab : liste de {code, rang, libelle} +- DP_recodage, DR_recodage +- DAS_recodage : liste de {code, rang} +- Actes_recodage : liste de {code, rang} + +### Page 1 — GHM/GHS + décision +- GHM_etab, GHS_etab, GHM_recodage, GHS_recodage +- Recodage_impactant_facturation (0/1) +- GHS_injustifie (0/1) +- SE_coche (1/2/3/4), ATU, FFM, FSD +- Accord_Desaccord (accord/désaccord) +- Nom_praticien_conseil + +### Page Eléments de preuve (tableau 17 lignes) +Pour chaque type de document : +- present (oui/non), photocopie (nombre), absent_date_1ere_demande, date_obtention +- Date_elements_preuve +- Medecin_controleur_signataire, Medecin_DIM_signataire + +### Page Fiche Administrative 2/2 +- GHS_initial, GHS_avant_concertation, GHS_final_apres_concertation +- Decision_finale (maintien_avis_controleur / retour_groupage_DIM / autre_groupage) +- Avis_DIM_final (accord/désaccord) +- Date_concertation +- Nom_medecin_responsable_controle, Nom_medecin_DIM + +### Page Fiche Administrative 1/2 +- Date_concertation_1_2 +- Argumentaire_medecin_controleur (texte long imprimé) + +## Structure Excel de sortie + +### Onglet 1 : "Données principales" +1 ligne par OGC, colonnes = tous les champs scalaires + +### Onglet 2 : "Diagnostics" +1 ligne par code diagnostique (DP/DR/DAS), avec colonne N°OGC + +### Onglet 3 : "Actes" +1 ligne par acte, avec colonne N°OGC + +### Onglet 4 : "Eléments de preuve" +1 ligne par type de document × N°OGC + +## Fichiers +- `extraction.py` : script principal +- `scanOgc/` : dossier des PDFs source +- `output/` : dossier de sortie Excel + +## Pour relancer +```bash +cd "/Users/oussi/Documents/Documents - MacBook Pro de oussi/EttaSante/T2A/ScanOGC/testExtraction2" +python3 extraction.py +``` + +## Dépendances +```bash +pip install pdf2image pillow pandas openpyxl requests +# Ollama doit tourner : ollama serve +# Modèle requis : qwen3-vl:2b +``` + +## Notes importantes +- L'ordre des pages varie selon les fichiers → identification par titre, pas par numéro +- Page manuscrite identifiée par le titre "Séjour d'hospitalisation complète" +- qwen3-vl:2b : paramètre `think` désactivé pour JSON pur (plus rapide) +- Timeout Ollama : 120s par page (pages denses) diff --git a/README.md b/README.md new file mode 100644 index 0000000..37eb9c3 --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +# Extraction OGC — Qwen3-VL 235B + +Extraction automatique des fiches OGC (Organisme de Gestion du Contrôle) vers Excel. +Modèle : **qwen3-vl:235b-cloud** (vision multimodal via Ollama). + +--- + +## Structure du dossier + +``` +testExtraction2/ +├── extraction.py ← script principal +├── README.md ← ce fichier +├── PLAN_EXTRACTION.md ← documentation technique détaillée +├── scanOgc/ ← 12 PDFs source (OGC 358 à 429) +└── output/ + ├── extraction_ogc.xlsx ← résultat (4 onglets) + └── extraction_ogc_raw.json ← données brutes pour debug +``` + +--- + +## Lancement + +```bash +cd testExtraction2 + +# Tous les fichiers +python3 extraction.py + +# Un seul fichier (relance partielle — fusionne avec le cache) +python3 extraction.py "358" +``` + +--- + +## Ce que fait le script + +1. Convertit chaque page PDF en image (200 DPI) +2. Envoie l'image à **qwen3-vl:235b-cloud** pour identifier le type de page +3. Ignore la page "Séjour d'hospitalisation complète" (bloc manuscrit — phase 2) +4. Pour chaque autre page, extrait les données en JSON via un prompt dédié +5. Assemble tout en Excel (4 onglets) + +### Types de pages traités + +| Type | Action | +|---|---| +| FICHE_RECUEIL | Extrait (données séjour, codages, GHM/GHS) | +| ELEMENTS_PREUVE | Extrait (tableau 17 documents) | +| FICHE_ADMIN_2_2 | Extrait (GHS final, décision concertation) | +| FICHE_ADMIN_1_2 | Extrait (argumentaire imprimé complet) | +| SEJOUR_MANUSCRIT | **Ignoré** — commentaires manuscrits (phase 2) | +| FICHE_CONCERTATION_VIDE | Ignoré — page vide | + +--- + +## Sortie Excel (4 onglets) + +**Données principales** — 1 ligne par OGC, tous les champs scalaires +**Diagnostics** — 1 ligne par code (DP/DR/DAS), établissement + recodage +**Actes** — 1 ligne par acte CCAM, établissement + recodage +**Eléments de preuve** — 1 ligne par type de document × OGC + +--- + +## Particularités techniques + +- **Thinking mode** : qwen3-vl génère ~4000 tokens de réflexion interne avant la réponse. `num_predict=8192` est obligatoire pour avoir assez de budget tokens. +- **Rate limit** : pause de 2s entre chaque requête + retry automatique sur 429 (attente 60s × tentative). +- **Ordre des pages variable** : identification par titre, pas par numéro de page. +- **Relance partielle** : `python3 extraction.py "XXX"` charge le cache JSON existant et ne réécrase que le fichier relancé. + +--- + +## Prérequis + +```bash +pip install pdf2image pillow pandas openpyxl requests +# Ollama doit tourner (ollama serve) +# Modèle requis : qwen3-vl:235b-cloud +``` diff --git a/extraction.py b/extraction.py new file mode 100644 index 0000000..9f838a8 --- /dev/null +++ b/extraction.py @@ -0,0 +1,1129 @@ +""" +Extraction OGC → Excel +Modèle : qwen3-vl:235b-cloud (vision multimodal) via Ollama +""" + +import base64 +import io +import json +import re +import sys +import time +from datetime import datetime +from pathlib import Path + +import pandas as pd +import requests +from pdf2image import convert_from_path +from PIL import Image +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.units import cm +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, HRFlowable +) + +# ─── Config ─────────────────────────────────────────────────────────────────── + +SCAN_DIR = Path(__file__).parent / "scanOgc" +OUTPUT_DIR = Path(__file__).parent / "output" +OUTPUT_DIR.mkdir(exist_ok=True) + +OLLAMA_URL = "http://localhost:11434/api/generate" +MODEL = "qwen3-vl:235b-cloud" +PDF_DPI = 250 + +# Rate-limit : pause entre chaque appel et retry sur 429 +INTER_REQUEST_DELAY = 2 # secondes +RETRY_MAX = 6 +RETRY_DELAY_429 = 60 # secondes — plafond à 120s dans ask_vision + +# ─── Utilitaires image ──────────────────────────────────────────────────────── + +def image_to_b64(img: Image.Image) -> str: + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=90) + return base64.b64encode(buf.getvalue()).decode() + + +# ─── Appel Ollama ───────────────────────────────────────────────────────────── + +def ask_vision(prompt: str, img: Image.Image, + timeout: int = 240, num_predict: int = 8192, + timing_record: dict = None) -> str: + """ + Envoie une image + prompt à Ollama en mode streaming. + - qwen3-vl utilise ~4000 tokens de "thinking" avant la réponse : + num_predict=8192 est nécessaire pour avoir assez de budget. + - Retry automatique sur 429 (rate limit cloud). + - timing_record : dict optionnel pour enregistrer retries/blocages. + """ + payload = { + "model": MODEL, + "prompt": prompt, + "images": [image_to_b64(img)], + "stream": True, + "options": {"temperature": 0, "num_predict": num_predict}, + } + + for attempt in range(1, RETRY_MAX + 1): + try: + resp = requests.post(OLLAMA_URL, json=payload, + timeout=timeout, stream=True) + if resp.status_code == 429: + wait = min(RETRY_DELAY_429 * attempt, 120) + print(f" ⏳ Rate limit — attente {wait}s " + f"(tentative {attempt}/{RETRY_MAX})...") + if timing_record is not None: + timing_record.setdefault("blocages_429", []).append({ + "tentative": attempt, + "attente_s": wait, + "ts": datetime.now().isoformat(), + }) + time.sleep(wait) + continue + resp.raise_for_status() + + tokens = [] + for line in resp.iter_lines(): + if not line: + continue + try: + chunk = json.loads(line) + except json.JSONDecodeError: + continue + if chunk.get("response"): + tokens.append(chunk["response"]) + if chunk.get("done"): + break + + if timing_record is not None and attempt > 1: + timing_record["retries_total"] = \ + timing_record.get("retries_total", 0) + (attempt - 1) + + time.sleep(INTER_REQUEST_DELAY) + return "".join(tokens) + + except requests.exceptions.HTTPError as e: + if e.response is not None and e.response.status_code == 429: + wait = min(RETRY_DELAY_429 * attempt, 120) + print(f" ⏳ Rate limit — attente {wait}s " + f"(tentative {attempt}/{RETRY_MAX})...") + if timing_record is not None: + timing_record.setdefault("blocages_429", []).append({ + "tentative": attempt, + "attente_s": wait, + "ts": datetime.now().isoformat(), + }) + time.sleep(wait) + continue + raise + + raise RuntimeError(f"Echec après {RETRY_MAX} tentatives (rate limit persistant)") + + +# ─── Extraction JSON depuis la réponse ─────────────────────────────────────── + +def _try_parse(text: str): + for candidate in ( + text, + text.replace("\n", " ").replace("\r", " "), + re.sub(r",\s*([}\]])", r"\1", text), # trailing commas + re.sub(r"[\x00-\x08\x0b\x0c\x0e-\x1f]", "", text), # control chars + ): + try: + return json.loads(candidate) + except json.JSONDecodeError: + pass + return None + + +def _extract_balanced(text: str, open_c: str, close_c: str): + """Extrait la première structure équilibrée open_c…close_c du texte.""" + start = text.find(open_c) + if start == -1: + return None + depth = 0 + in_str = False + escape = False + for i, ch in enumerate(text[start:], start): + if escape: + escape = False + continue + if ch == "\\" and in_str: + escape = True + continue + if ch == '"' and not escape: + in_str = not in_str + continue + if in_str: + continue + if ch == open_c: + depth += 1 + elif ch == close_c: + depth -= 1 + if depth == 0: + return text[start:i+1] + return None + + +def extract_json(text: str): + # 1. Bloc ```json … ``` + m = re.search(r"```json\s*([\s\S]*?)```", text) + if m: + result = _try_parse(m.group(1).strip()) + if result is not None: + return result + + # 2. Extraction par accolades équilibrées (plus robuste que greedy regex) + for open_c, close_c in (('{', '}'), ('[', ']')): + candidate = _extract_balanced(text, open_c, close_c) + if candidate: + result = _try_parse(candidate) + if result is not None: + return result + + # 3. Fallback greedy regex (comportement original) + for pattern in (r"(\{[\s\S]*\})", r"(\[[\s\S]*\])"): + m = re.search(pattern, text) + if m: + result = _try_parse(m.group(1)) + if result is not None: + return result + + return None + + +# ─── Prompts ────────────────────────────────────────────────────────────────── + +PROMPT_IDENTIFY = """\ +Tu es un assistant d'analyse de documents médicaux français. +Regarde cette image et identifie son type parmi : +- FICHE_RECUEIL : "FICHE MEDICALE DE RECUEIL DU PRATICIEN CONSEIL" +- FICHE_CONCERTATION_VIDE: "FICHE MEDICALE DE CONCERTATION" (page quasi vide) +- SEJOUR_MANUSCRIT : "Séjour d'hospitalisation complète" (colonnes manuscrites) +- ELEMENTS_PREUVE : "Eléments de preuve tracés au dossier du patient" +- FICHE_ADMIN_2_2 : "FICHE ADMINISTRATIVE DE CONCERTATION 2/2" +- FICHE_ADMIN_1_2 : "FICHE ADMINISTRATIVE DE CONCERTATION 1/2" +- AUTRE : autre type + +Réponds UNIQUEMENT avec le code du type, sans aucune explication.\ +""" + +PROMPT_FICHE_RECUEIL = """\ +Tu es un assistant d'extraction de données médicales. +Extrait toutes les informations imprimées de cette fiche médicale de recueil du praticien conseil. +RÈGLES STRICTES : +- Si un champ n'a pas de valeur clairement visible et imprimée, retourner une chaîne vide "". +- Ne jamais deviner, inférer ou compléter un champ absent. +- Le champ "provenance" est souvent vide : ne pas le remplir sauf si une valeur est explicitement imprimée. +- Le champ "se_coche" correspond aux cases 1/2/3/4 : retourner "SE1", "SE2", "SE3" ou "SE4" si une case est explicitement cochée, sinon "". Ce champ est TRÈS SOUVENT vide — ne rien mettre par défaut. NE PAS confondre avec "accord_desaccord" qui est un champ séparé. +- Le champ "accord_desaccord" est distinct de "se_coche" : il indique accord/désaccord du praticien conseil, pas les cases SE. +- Le champ "dr_etab" (Diagnostic Relié) est distinct des DAS : ne mettre un code que s'il y a une ligne DR EXPLICITEMENT RENSEIGNÉE sur la fiche. Si la ligne DR est vide ou absente sur le document, retourner "" obligatoirement. NE JAMAIS copier le premier DAS dans DR — ce sont deux lignes séparées sur la grille. +- Le tableau "Données du séjour" contient ces colonnes DANS CET ORDRE EXACT, de gauche à droite : + Age(ans) | Age(jours) | Sexe | Délai dern. règles | Age gestation | Poids d'entrée | + Durée de séjour | Mode d'entrée | Provenance | Mode de sortie | Destination | + Nb séances | Nb RUM | Nb j EXH | Type EXB | Nb j EXB + RÈGLE ABSOLUE : lire chaque valeur dans sa colonne uniquement. + Si une colonne est vide, retourner "" pour ce champ. + Ne jamais décaler les valeurs vers la gauche pour compenser une cellule vide. + Exemple : si "Provenance" est vide, "Mode de sortie" reste dans "mode_sortie", pas dans "provenance". +Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ni après. +- IMPORTANT : extraire TOUTES les lignes non vides de das_etab, actes_etab, das_reco et actes_reco sans limite de nombre. Ne jamais tronquer ces listes. +- Les actes (CCAM, codes à 7+ caractères commençant par des lettres ex: JDPE002, NJFA008) vont dans "actes_etab", pas dans "das_etab". Les diagnostics (CIM-10, codes courts ex: N320, R33) vont dans "das_etab". + +{"n_ogc":"","etablissement":"","finess":"","date_debut_controle":"","n_champ":"","libelle_champ":"","dossier_manquant":"","date_debut_sejour":"","date_fin_sejour":"", +"sejour_etab":{"age_ans":"","age_jours":"","sexe":"","poids_entree":"","duree_sejour":"","mode_entree":"","provenance":"","mode_sortie":"","destination":"","nb_seances":"","nb_rum":"","nb_j_exh":"","type_exb":"","nb_j_exb":""}, +"sejour_reco":{"age_ans":"","age_jours":"","sexe":"","poids_entree":"","duree_sejour":"","mode_entree":"","provenance":"","mode_sortie":"","destination":"","nb_seances":"","nb_rum":"","nb_j_exh":"","type_exb":"","nb_j_exb":""}, +"rum_etab":{"n_rum":"","lits_dedies_sp":"","um":"","igs_ii":"","duree_rum_debut":"","duree_rum_fin":"","nature_suppl":"","nb_suppl":""}, +"rum_reco":{"n_rum":"","lits_dedies_sp":"","um":"","igs_ii":"","duree_rum_debut":"","duree_rum_fin":"","nature_suppl":"","nb_suppl":""}, +"dp_etab":{"code":"","libelle":""},"dr_etab":{"code":"","libelle":""}, +"das_etab": [] ou [{"code":"","niveau":"","libelle":""}] ou plus, +"actes_etab":[{"code":"","niveau":"","libelle":""}], +"dp_reco":{"code":""},"dr_reco":{"code":""}, +"das_reco":[{"code":"","niveau":""}],"actes_reco":[{"code":"","niveau":""}], +"ghm_etab":"","ghs_etab":"","ghm_reco":"","ghs_reco":"", +"recodage_impactant_facturation":"","ghs_injustifie":"", +"se_coche":"","atu":"","ffm":"","fsd":"","accord_desaccord":"","nom_praticien_conseil":""}\ +""" + +PROMPT_ELEMENTS_PREUVE = """\ +Tu es un assistant d'extraction de données médicales. +Extrait les informations de cette page "Eléments de preuve tracés au dossier du patient". +Pour chaque ligne : "present"=oui/non, "photocopie"=nombre écrit, dates si présentes. +Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ni après. + +{"date":"","medecin_controleur_signataire":"","medecin_dim_signataire":"", +"elements":{"compte_rendu_acte":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_operatoire":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_accouchement":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_examen_complementaire":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_imagerie":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_anatomopathologie":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"observations_medicales":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"dossier_transfusion":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"dossier_anesthesie":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"administration_therapeutique":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"compte_rendu_hospitalisation":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"lettre_sortie":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"surveillance_dossier_infirmier":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"prise_en_charge_psychologue":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"prise_en_charge_kinesitherapeute":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"prise_en_charge_dietetique":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}, +"autre":{"present":"","photocopie":"","absent_date_1ere_demande":"","date_obtention":""}}}\ +""" + +PROMPT_FICHE_ADMIN_2_2 = """\ +Tu es un assistant d'extraction de données médicales. +Extrait les informations de cette fiche administrative de concertation 2/2. +RÈGLES STRICTES : +- Pour "maintien_avis_controleur", "retour_groupage_dim", "autre_groupage" : retourner "oui" si la case est cochée (X, ✓ ou toute marque), "non" si la case est décochée, "" si absent. +- Pour les champs GHS (nombres) : retourner uniquement les chiffres sans point ni espace (ex: "6173" et non "6.173"). +- Si un champ est absent ou illisible, retourner "". +Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ni après. + +{"n_ogc":"","ghs_initial":"","ghs_avant_concertation":"","ghs_final_apres_concertation":"", +"maintien_avis_controleur":"","retour_groupage_dim":"","autre_groupage":"", +"avis_dim_final":"","date_concertation":"", +"nom_medecin_responsable_controle":"","nom_medecin_dim":""}\ +""" + +PROMPT_FICHE_ADMIN_1_2 = """\ +Tu es un assistant d'extraction de données médicales. +Extrait les informations de cette fiche administrative de concertation 1/2. +L'argumentaire est un texte long imprimé (pas manuscrit). +Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ni après. + +{"n_ogc":"","date_concertation":"","argumentaire_medecin_controleur":""}\ +""" + +PROMPTS = { + "FICHE_RECUEIL": PROMPT_FICHE_RECUEIL, + "ELEMENTS_PREUVE": PROMPT_ELEMENTS_PREUVE, + "FICHE_ADMIN_2_2": PROMPT_FICHE_ADMIN_2_2, + "FICHE_ADMIN_1_2": PROMPT_FICHE_ADMIN_1_2, +} + +SKIP_TYPES = {"SEJOUR_MANUSCRIT", "FICHE_CONCERTATION_VIDE", "AUTRE"} + +# ─── Traitement d'un PDF ────────────────────────────────────────────────────── + +# ─── Normalisation post-extraction ─────────────────────────────────────────── + +_CHECKBOX_OUI = {"x", "oui", "✓", "✗", "coché", "v", "yes"} + +def _norm_checkbox(val: str) -> str: + """Convertit toute marque de case cochée en 'oui', conserve 'non', vide sinon.""" + v = str(val).strip().lower() + if v in _CHECKBOX_OUI: + return "oui" + if v == "non": + return "non" + return "" + + +def _calc_duree_rum(debut: str, fin: str): + """Calcule la durée en jours entre deux dates (DD/MM/YYYY ou YYYY-MM-DD). Retourne None si non parsable.""" + for fmt in ("%d/%m/%Y", "%Y-%m-%d", "%d-%m-%Y"): + try: + d1 = datetime.strptime(str(debut).strip(), fmt) + d2 = datetime.strptime(str(fin).strip(), fmt) + return (d2 - d1).days + except (ValueError, AttributeError): + pass + return None + + +def _strip_dot_number(val: str) -> str: + """Supprime le point parasite dans un nombre (ex: '6.173' → '6173', '.0' → '0').""" + v = str(val).strip() + # Nombre entier avec un point au milieu (pas une vraie décimale) + if re.match(r"^\d+\.\d+$", v): + cleaned = v.replace(".", "") + return cleaned + # Point en début de chaîne + if v.startswith(".") and v[1:].isdigit(): + return v[1:] + return v + + +def _fix_year(val: str) -> str: + """ + Corrige les erreurs de lecture d'année manuscrite. + Les dates OGC sont dans la plage 2015-2019. + Si une année hors plage est détectée (ex: 2021, 2022), + la remplace par l'année valide la plus proche. + """ + if not val: + return val + m = re.search(r"(20\d\d)", val) + if m: + year = m.group(1) + valid_years = ("2015", "2016", "2017", "2018", "2019") + if year not in valid_years: + best = min(valid_years, key=lambda y: abs(int(y) - int(year))) + val = val.replace(year, best) + return val + + +def _normalize_result(result: dict) -> None: + """Normalise les données extraites en place (checkboxes, chiffres mal lus).""" + for pt in result.get("pages_traitees", []): + d = pt.get("data", {}) + if not isinstance(d, dict): + continue + + ptype = pt.get("type") + + if ptype == "FICHE_ADMIN_2_2": + for field in ("maintien_avis_controleur", "retour_groupage_dim", "autre_groupage"): + if field in d: + d[field] = _norm_checkbox(d[field]) + # GHS : supprimer points parasites + for field in ("ghs_initial", "ghs_avant_concertation", "ghs_final_apres_concertation"): + if d.get(field): + d[field] = _strip_dot_number(d[field]) + + if ptype == "FICHE_RECUEIL": + # Guard anti-confusion DR/DAS + for dr_k, das_k in (("dr_etab", "das_etab"), ("dr_reco", "das_reco")): + dr_code = (d.get(dr_k) or {}).get("code", "").strip() + if not dr_code: + continue + das = [x for x in (d.get(das_k) or []) if isinstance(x, dict) and x.get("code")] + das_codes = {x.get("code", "").strip() for x in das} + + if dr_code in das_codes: + # Cas 1 : DR duplique un DAS existant → vider DR + d[dr_k] = {"code": "", "libelle": ""} + + elif not das: + # Cas 2 : DAS vide mais DR renseigné → confusion modèle, + # déplacer le DR dans das comme premier DAS + dr_entry = d.get(dr_k) or {} + new_das_entry = {"code": dr_code, "rang": dr_entry.get("rang", "")} + if dr_k == "dr_etab": + new_das_entry["libelle"] = dr_entry.get("libelle", "") + d[das_k] = [new_das_entry] + d[dr_k] = {"code": "", "libelle": ""} + + # nature_suppl souvent lu '.0' au lieu de '0' + for section in ("rum_etab", "rum_reco"): + sec = d.get(section) or {} + if sec.get("nature_suppl"): + sec["nature_suppl"] = _strip_dot_number(sec["nature_suppl"]) + # durée calculée à partir des dates (plus fiable que la valeur extraite) + duree = _calc_duree_rum(sec.get("duree_rum_debut", ""), sec.get("duree_rum_fin", "")) + if duree is not None: + sec["duree_rum_calculee_j"] = duree + # se_coche : normaliser "1"→"SE1", rejeter toute valeur non SE1-4 + se_raw = str(d.get("se_coche", "")).strip() + if se_raw.upper() in {"SE1", "SE2", "SE3", "SE4"}: + # Format déjà correct + d["se_coche"] = se_raw.upper() + elif se_raw in {"1", "2", "3", "4"}: + # Chiffre seul = ambigu, le modèle confond avec le rang d'un DAS → vider + d["se_coche"] = "" + elif se_raw: + # Valeur inattendue (ex: "accord", "désaccord") → vider + d["se_coche"] = "" + + if ptype in ("FICHE_ADMIN_2_2", "FICHE_ADMIN_1_2"): + for date_field in ("date_concertation",): + if d.get(date_field): + d[date_field] = _fix_year(d[date_field]) + + if ptype == "ELEMENTS_PREUVE": + if d.get("date"): + d["date"] = _fix_year(d["date"]) + + +# ─── Calcul d'audit de fiabilité ───────────────────────────────────────────── + +def compute_audit(result: dict) -> dict: + """ + Calcule un bloc _audit pour l'OGC. + score_global ∈ [0,1] — seuil d'alerte : 0.80 + alertes = champs dont le score < 0.80 + """ + checks: list[tuple[str, float]] = [] # (champ, score) + + for pt in result.get("pages_traitees", []): + ptype = pt.get("type") + d = pt.get("data", {}) + page = pt.get("page", "?") + + if not isinstance(d, dict): + continue + + # JSON non parsé → données non fiables + if "raw_response" in d: + checks.append((f"page_{page}_json", 0.10)) + continue + + if ptype == "FICHE_RECUEIL": + # n_ogc vide + checks.append(("n_ogc", 1.0 if d.get("n_ogc") else 0.20)) + + # dr_etab non vide → historiquement souvent faux (confondu avec DAS) + dr_code = (d.get("dr_etab") or {}).get("code", "") + checks.append(("dr_etab", 0.31 if dr_code else 1.0)) + + # provenance non vide → souvent halluciné + prov = str((d.get("sejour_etab") or {}).get("provenance", "")).strip() + checks.append(("sejour_etab.provenance", 0.40 if prov else 1.0)) + + # se_coche non vide → souvent halluciné ; doit valoir SE1/SE2/SE3/SE4 ou "" + se_val = str(d.get("se_coche", "")).strip().lower() + if not se_val: + checks.append(("se_coche", 1.0)) + elif se_val in {"se1", "se2", "se3", "se4", "1", "2", "3", "4"}: + checks.append(("se_coche", 0.90)) # valeur plausible mais vérifier format + else: + # confusion probable avec accord_desaccord ou autre valeur inattendue + checks.append(("se_coche", 0.20)) + + # DAS vide alors que DP présent → probablement tronqué + dp_code = (d.get("dp_etab") or {}).get("code", "") + das = [x for x in (d.get("das_etab") or []) if isinstance(x, dict) and x.get("code")] + if dp_code and not das: + checks.append(("das_etab", 0.50)) + else: + checks.append(("das_etab", 1.0)) + + # Code DAS ressemble à un acte (≥7 chars, 4 premières lettres) + acte_like = any( + len(x.get("code", "")) >= 7 and x.get("code", "")[:4].isalpha() + for x in das + ) + checks.append(("das_etab.codes", 0.35 if acte_like else 1.0)) + + elif ptype == "FICHE_ADMIN_2_2": + # Au moins une case doit être cochée + maintien = str(d.get("maintien_avis_controleur", "")).strip().lower() + retour = str(d.get("retour_groupage_dim", "")).strip().lower() + autre = str(d.get("autre_groupage", "")).strip().lower() + aucun_coche = not any(v == "oui" for v in (maintien, retour, autre)) + checks.append(("maintien_retour_autre", 0.50 if aucun_coche else 1.0)) + + # GHS final encore avec point → mal lu + ghs = str(d.get("ghs_final_apres_concertation", "")).strip() + checks.append(("ghs_final", 0.40 if ("." in ghs and ghs) else 1.0)) + + elif ptype == "ELEMENTS_PREUVE": + # Ne flag que les lettres en début de chaîne seules (ex: "A", "B") + # ou des séquences de 3+ lettres consécutives — pas "A2", "1.a3" qui sont valides + suspect = any( + bool(re.search(r"(? tuple[dict, dict]: + """Retourne (result, timing) où timing contient toutes les métriques de temps.""" + print(f"\n{'='*60}\nTraitement : {pdf_path.name}\n{'='*60}") + + pdf_start = time.time() + timing = { + "fichier": pdf_path.name, + "debut": datetime.now().isoformat(), + "fin": None, + "duree_totale_s": None, + "nb_pages_total": 0, + "pages": [], + "erreurs": [], + "blocages_429": [], + "retries_total": 0, + } + + pages = convert_from_path(str(pdf_path), dpi=PDF_DPI) + timing["nb_pages_total"] = len(pages) + result = {"fichier": pdf_path.name, "pages_traitees": [], "pages_ignorees": []} + + for i, img in enumerate(pages, start=1): + print(f"\n Page {i}/{len(pages)} — identification...") + page_timing = { + "page": i, + "type": None, + "duree_identification_s": None, + "duree_extraction_s": None, + "statut": None, + "erreur": None, + } + t0 = time.time() + + try: + raw_type = ask_vision(PROMPT_IDENTIFY, img, + timeout=200, num_predict=512, + timing_record=timing).strip().upper() + except Exception as e: + print(f" ⚠ Erreur identification : {e}") + page_timing["duree_identification_s"] = round(time.time() - t0, 2) + page_timing["statut"] = "erreur_identification" + page_timing["erreur"] = str(e) + timing["erreurs"].append({"page": i, "phase": "identification", "message": str(e)}) + timing["pages"].append(page_timing) + result["pages_ignorees"].append({"page": i, "type": "ERREUR_IDENTIFICATION"}) + continue + + duree_id = round(time.time() - t0, 2) + page_timing["duree_identification_s"] = duree_id + + page_type = "AUTRE" + for known in PROMPTS.keys() | SKIP_TYPES: + if known in raw_type: + page_type = known + break + page_timing["type"] = page_type + print(f" → Type : {page_type} ({duree_id:.1f}s)") + + if page_type in SKIP_TYPES: + page_timing["statut"] = "ignoree" + timing["pages"].append(page_timing) + result["pages_ignorees"].append({"page": i, "type": page_type}) + print(" → Ignorée.") + continue + + print(" → Extraction en cours...") + t0 = time.time() + try: + num_predict = 12000 if page_type == "FICHE_RECUEIL" else 8192 + raw = ask_vision(PROMPTS[page_type], img, timeout=240, + num_predict=num_predict, timing_record=timing) + except Exception as e: + print(f" ⚠ Erreur extraction : {e}") + duree_ext = round(time.time() - t0, 2) + page_timing["duree_extraction_s"] = duree_ext + page_timing["statut"] = "erreur_extraction" + page_timing["erreur"] = str(e) + timing["erreurs"].append({"page": i, "phase": "extraction", "type": page_type, "message": str(e)}) + timing["pages"].append(page_timing) + result["pages_traitees"].append({"page": i, "type": page_type, + "data": {"erreur": str(e)}}) + continue + + duree_ext = round(time.time() - t0, 2) + page_timing["duree_extraction_s"] = duree_ext + print(f" → Réponse reçue ({duree_ext:.1f}s)") + + data = extract_json(raw) + if data is None: + print(f" ⚠ JSON non parsable — retry en cours...") + retry_prompt = ( + "Ta réponse précédente n'était pas un JSON valide. " + "Réponds UNIQUEMENT avec un objet JSON valide, sans texte avant ni après, " + "sans bloc ```json```. Voici le schéma attendu :\n\n" + + PROMPTS[page_type] + ) + try: + raw2 = ask_vision(retry_prompt, img, timeout=240, num_predict=12000, + timing_record=timing) + data = extract_json(raw2) + except Exception as e: + print(f" ⚠ Erreur retry : {e}") + data = None + + if data is None: + print(f" ⚠ Retry échoué — raw_response conservé") + page_timing["statut"] = "json_non_parsable" + timing["erreurs"].append({ + "page": i, "phase": "parsing_json", "type": page_type, + "message": f"JSON non parsable après retry : {raw[:100]}", + "retry": True, + }) + data = {"raw_response": raw} + else: + print(f" ✓ Retry réussi") + page_timing["statut"] = "ok_after_retry" + timing["erreurs"].append({ + "page": i, "phase": "parsing_json", "type": page_type, + "message": "JSON non parsable au 1er appel, corrigé par retry", + "retry": True, "retry_ok": True, + }) + else: + page_timing["statut"] = "ok" + + timing["pages"].append(page_timing) + result["pages_traitees"].append({"page": i, "type": page_type, "data": data}) + print(" ✓ OK") + + timing["fin"] = datetime.now().isoformat() + timing["duree_totale_s"] = round(time.time() - pdf_start, 2) + + _normalize_result(result) + result["_audit"] = compute_audit(result) + + return result, timing + + +# ─── Aplatissement pour Excel ───────────────────────────────────────────────── + +def flatten(result: dict) -> dict: + row = {"fichier": result["fichier"]} + general_done = False # champs généraux pris sur la 1re page FICHE_RECUEIL uniquement + for pt in result["pages_traitees"]: + d, ptype = pt["data"], pt["type"] + if ptype == "FICHE_RECUEIL": + # ── Champs généraux (par séjour, identiques sur chaque page RUM) ── + if not general_done: + for k in ["n_ogc","etablissement","finess","date_debut_controle","n_champ", + "libelle_champ","dossier_manquant","date_debut_sejour","date_fin_sejour"]: + row[k] = d.get(k, "") + for prefix in ("sejour_etab","sejour_reco"): + for k, v in (d.get(prefix) or {}).items(): + row[f"{prefix}_{k}"] = v + row["dp_etab_code"] = (d.get("dp_etab") or {}).get("code", "") + row["dp_etab_libelle"] = (d.get("dp_etab") or {}).get("libelle", "") + row["dr_etab_code"] = (d.get("dr_etab") or {}).get("code", "") + row["dr_etab_libelle"] = (d.get("dr_etab") or {}).get("libelle", "") + row["dp_reco_code"] = (d.get("dp_reco") or {}).get("code", "") + row["dr_reco_code"] = (d.get("dr_reco") or {}).get("code", "") + for k in ["ghm_etab","ghs_etab","ghm_reco","ghs_reco", + "recodage_impactant_facturation","ghs_injustifie", + "se_coche","atu","ffm","fsd","accord_desaccord","nom_praticien_conseil"]: + row[k] = d.get(k, "") + general_done = True + # ── Comptages et durées agrégés sur tous les RUM ── + row["nb_das_etab"] = row.get("nb_das_etab", 0) + len([x for x in (d.get("das_etab") or []) if isinstance(x, dict) and x.get("code")]) + row["nb_actes_etab"] = row.get("nb_actes_etab", 0) + len([x for x in (d.get("actes_etab") or []) if isinstance(x, dict) and x.get("code")]) + row["nb_das_reco"] = row.get("nb_das_reco", 0) + len([x for x in (d.get("das_reco") or []) if isinstance(x, dict) and x.get("code")]) + row["nb_actes_reco"] = row.get("nb_actes_reco", 0) + len([x for x in (d.get("actes_reco") or []) if isinstance(x, dict) and x.get("code")]) + for section, col in (("rum_etab", "duree_sejour_calc_etab_j"), + ("rum_reco", "duree_sejour_calc_reco_j")): + duree = (d.get(section) or {}).get("duree_rum_calculee_j") + if duree is not None: + row[col] = row.get(col, 0) + duree + elif ptype == "ELEMENTS_PREUVE": + row["ep_date"] = d.get("date", "") + row["ep_medecin_controleur"] = d.get("medecin_controleur_signataire", "") + row["ep_medecin_dim"] = d.get("medecin_dim_signataire", "") + for doc, vals in (d.get("elements") or {}).items(): + for col, val in (vals or {}).items(): + row[f"ep_{doc}_{col}"] = val + elif ptype == "FICHE_ADMIN_2_2": + if not row.get("n_ogc"): + row["n_ogc"] = d.get("n_ogc", "") + for k in ["ghs_initial","ghs_avant_concertation","ghs_final_apres_concertation", + "maintien_avis_controleur","retour_groupage_dim","autre_groupage", + "avis_dim_final","date_concertation", + "nom_medecin_responsable_controle","nom_medecin_dim"]: + row[f"admin22_{k}"] = d.get(k, "") + elif ptype == "FICHE_ADMIN_1_2": + row["admin12_date_concertation"] = d.get("date_concertation", "") + row["admin12_argumentaire"] = d.get("argumentaire_medecin_controleur", "") + + # ── RÈGLE MÉTIER GHS FINAL ───────────────────────────────────────────── + # ghs_final_apres_concertation est manuscrit et souvent mal lu. + # On le recalcule depuis les cases cochées (valeurs imprimées, plus fiables) : + # - maintien_avis_controleur coché → ghs_final = ghs_initial + # - retour_groupage_dim coché → ghs_final = ghs_avant_concertation + # - autre_groupage coché → ghs_final = valeur manuscrite extraite (on garde) + # Pour désactiver cette règle : supprimez le bloc entre les deux lignes de tirets. + maintien = str(row.get("admin22_maintien_avis_controleur", "")).lower() + retour = str(row.get("admin22_retour_groupage_dim", "")).lower() + autre = str(row.get("admin22_autre_groupage", "")).lower() + if maintien == "oui": + row["admin22_ghs_final_apres_concertation"] = row.get("admin22_ghs_initial", "") + elif retour == "oui": + row["admin22_ghs_final_apres_concertation"] = row.get("admin22_ghs_avant_concertation", "") + # si autre_groupage == "oui" : on garde la valeur extraite (manuscrite) + # ── FIN RÈGLE MÉTIER GHS FINAL ──────────────────────────────────────── + + return row + + +def build_rum(result: dict) -> list: + """1 ligne par RUM par OGC — données spécifiques au RUM.""" + rows = [] + for pt in result["pages_traitees"]: + if pt["type"] != "FICHE_RECUEIL": + continue + d = pt["data"] + ogc = d.get("n_ogc", result["fichier"]) + row = {"n_ogc": ogc} + for prefix in ("rum_etab", "rum_reco"): + for k, v in (d.get(prefix) or {}).items(): + row[f"{prefix}_{k}"] = v + rows.append(row) + return rows + + +def build_diagnostics(result: dict) -> list: + rows = [] + for pt in result["pages_traitees"]: + if pt["type"] != "FICHE_RECUEIL": + continue + d = pt["data"] + ogc = d.get("n_ogc", result["fichier"]) + n_rum = (d.get("rum_etab") or {}).get("n_rum", "") + for src, dp_k, dr_k, das_k in [ + ("etablissement", "dp_etab", "dr_etab", "das_etab"), + ("recodage", "dp_reco", "dr_reco", "das_reco"), + ]: + dp = d.get(dp_k) or {} + if dp.get("code"): + rows.append({"n_ogc": ogc, "n_rum": n_rum, "source": src, "type": "DP", + "code": dp["code"], "niveau": "", + "libelle": dp.get("libelle", "")}) + dr = d.get(dr_k) or {} + if dr.get("code"): + rows.append({"n_ogc": ogc, "n_rum": n_rum, "source": src, "type": "DR", + "code": dr["code"], "niveau": "", + "libelle": dr.get("libelle", "")}) + for das in (d.get(das_k) or []): + if isinstance(das, dict) and das.get("code"): + rows.append({"n_ogc": ogc, "n_rum": n_rum, "source": src, "type": "DAS", + "code": das["code"], "niveau": das.get("niveau", ""), + "libelle": das.get("libelle", "")}) + return rows + + +def build_actes(result: dict) -> list: + rows = [] + for pt in result["pages_traitees"]: + if pt["type"] != "FICHE_RECUEIL": + continue + d = pt["data"] + ogc = d.get("n_ogc", result["fichier"]) + n_rum = (d.get("rum_etab") or {}).get("n_rum", "") + for src, k in [("etablissement","actes_etab"), ("recodage","actes_reco")]: + for a in (d.get(k) or []): + if isinstance(a, dict) and a.get("code"): + rows.append({"n_ogc": ogc, "n_rum": n_rum, "source": src, + "code": a["code"], "niveau": a.get("niveau", ""), + "libelle": a.get("libelle", "")}) + return rows + + +def build_elements_preuve(result: dict) -> list: + rows = [] + for pt in result["pages_traitees"]: + if pt["type"] != "ELEMENTS_PREUVE": + continue + d = pt["data"] + ogc = result["fichier"] + for pt2 in result["pages_traitees"]: + if pt2["type"] == "FICHE_RECUEIL": + ogc = pt2["data"].get("n_ogc", ogc) + break + for doc, vals in (d.get("elements") or {}).items(): + row = {"n_ogc": ogc, "document": doc} + row.update(vals or {}) + rows.append(row) + return rows + + +# ─── Export Excel ───────────────────────────────────────────────────────────── + +def export_excel(all_results: list, all_timings: list, path: Path): + df_main = pd.DataFrame([flatten(r) for r in all_results]) + rum = sum((build_rum(r) for r in all_results), []) + diag = sum((build_diagnostics(r) for r in all_results), []) + actes = sum((build_actes(r) for r in all_results), []) + ep = sum((build_elements_preuve(r) for r in all_results), []) + + df_rum = pd.DataFrame(rum) if rum else pd.DataFrame(columns=["n_ogc","rum_etab_n_rum","rum_reco_n_rum"]) + df_diag = pd.DataFrame(diag) if diag else pd.DataFrame(columns=["n_ogc","n_rum","source","type","code","niveau","libelle"]) + df_actes = pd.DataFrame(actes) if actes else pd.DataFrame(columns=["n_ogc","n_rum","source","code","niveau","libelle"]) + df_ep = pd.DataFrame(ep) if ep else pd.DataFrame(columns=["n_ogc","document","present","photocopie"]) + + # Feuille Timing — résumé par dossier + timing_rows = [] + for t in all_timings: + nb_erreurs = len(t.get("erreurs", [])) + nb_429 = len(t.get("blocages_429", [])) + attente_429 = sum(b["attente_s"] for b in t.get("blocages_429", [])) + timing_rows.append({ + "fichier": t["fichier"], + "debut": t.get("debut", ""), + "fin": t.get("fin", ""), + "duree_totale_s": t.get("duree_totale_s", ""), + "nb_pages": t.get("nb_pages_total", ""), + "nb_erreurs": nb_erreurs, + "nb_blocages_429": nb_429, + "attente_429_s": attente_429, + "retries_total": t.get("retries_total", 0), + }) + df_timing = pd.DataFrame(timing_rows) if timing_rows else pd.DataFrame() + + with pd.ExcelWriter(path, engine="openpyxl") as w: + df_main.to_excel(w, sheet_name="Données principales", index=False) + df_rum.to_excel(w, sheet_name="RUM", index=False) + df_diag.to_excel(w, sheet_name="Diagnostics", index=False) + df_actes.to_excel(w, sheet_name="Actes", index=False) + df_ep.to_excel(w, sheet_name="Eléments de preuve", index=False) + df_timing.to_excel(w, sheet_name="Timing", index=False) + + print(f"\n✓ Excel : {path}") + print(f" Données principales : {len(df_main)} lignes") + print(f" RUM : {len(df_rum)} lignes") + print(f" Diagnostics : {len(df_diag)} lignes") + print(f" Actes : {len(df_actes)} lignes") + print(f" Eléments de preuve : {len(df_ep)} lignes") + print(f" Timing : {len(df_timing)} lignes") + + +# ─── Rapport PDF Timing ─────────────────────────────────────────────────────── + +def _fmt_s(s): + """Formate des secondes en mm:ss ou hh:mm:ss lisible.""" + if s is None: + return "—" + s = int(s) + h, r = divmod(s, 3600) + m, sec = divmod(r, 60) + if h: + return f"{h}h{m:02d}m{sec:02d}s" + if m: + return f"{m}m{sec:02d}s" + return f"{sec}s" + + +def build_timing_pdf(all_timings: list, path: Path, model: str = MODEL): + """Génère un rapport PDF d'analyse de temps d'extraction.""" + doc = SimpleDocTemplate( + str(path), pagesize=A4, + leftMargin=2*cm, rightMargin=2*cm, + topMargin=2*cm, bottomMargin=2*cm, + ) + styles = getSampleStyleSheet() + title_style = ParagraphStyle("title", parent=styles["Title"], + fontSize=18, spaceAfter=6) + h2_style = ParagraphStyle("h2", parent=styles["Heading2"], + fontSize=13, spaceBefore=14, spaceAfter=4) + h3_style = ParagraphStyle("h3", parent=styles["Heading3"], + fontSize=11, spaceBefore=10, spaceAfter=3) + body_style = ParagraphStyle("body", parent=styles["Normal"], + fontSize=9, spaceAfter=3) + warn_style = ParagraphStyle("warn", parent=styles["Normal"], + fontSize=9, textColor=colors.red, spaceAfter=3) + + story = [] + + # ── Titre ── + story.append(Paragraph(f"Rapport d'analyse de temps — {model}", title_style)) + story.append(Paragraph(f"Généré le {datetime.now().strftime('%d/%m/%Y à %H:%M:%S')}", body_style)) + story.append(HRFlowable(width="100%", thickness=1, color=colors.grey)) + story.append(Spacer(1, 0.4*cm)) + + # ── Résumé global ── + total_s = sum(t.get("duree_totale_s") or 0 for t in all_timings) + total_pages = sum(t.get("nb_pages_total") or 0 for t in all_timings) + total_err = sum(len(t.get("erreurs", [])) for t in all_timings) + total_429 = sum(len(t.get("blocages_429", [])) for t in all_timings) + total_wait = sum(b["attente_s"] for t in all_timings for b in t.get("blocages_429", [])) + nb_dossiers = len(all_timings) + + story.append(Paragraph("Résumé global", h2_style)) + summary_data = [ + ["Métrique", "Valeur"], + ["Nombre de dossiers traités", str(nb_dossiers)], + ["Nombre de pages total", str(total_pages)], + ["Durée totale d'extraction", _fmt_s(total_s)], + ["Durée moyenne / dossier", _fmt_s(total_s / nb_dossiers) if nb_dossiers else "—"], + ["Durée moyenne / page", _fmt_s(total_s / total_pages) if total_pages else "—"], + ["Erreurs totales", str(total_err)], + ["Blocages 429 (rate limit)", str(total_429)], + ["Temps perdu en attente 429", _fmt_s(total_wait)], + ] + t_sum = Table(summary_data, colWidths=[10*cm, 6*cm]) + t_sum.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#2c3e50")), + ("TEXTCOLOR", (0,0), (-1,0), colors.white), + ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"), + ("FONTSIZE", (0,0), (-1,-1), 9), + ("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#f2f2f2"), colors.white]), + ("GRID", (0,0), (-1,-1), 0.5, colors.grey), + ("LEFTPADDING", (0,0), (-1,-1), 6), + ("RIGHTPADDING",(0,0), (-1,-1), 6), + ("TOPPADDING", (0,0), (-1,-1), 4), + ("BOTTOMPADDING",(0,0),(-1,-1), 4), + ])) + story.append(t_sum) + story.append(Spacer(1, 0.5*cm)) + + # ── Détail par dossier ── + story.append(Paragraph("Détail par dossier", h2_style)) + for t in all_timings: + story.append(Paragraph(t["fichier"], h3_style)) + nb_err = len(t.get("erreurs", [])) + nb_b = len(t.get("blocages_429", [])) + att = sum(b["attente_s"] for b in t.get("blocages_429", [])) + + # Calcul durée pages traitées + pages = t.get("pages", []) + duree_id = sum(p.get("duree_identification_s") or 0 for p in pages) + duree_ext = sum(p.get("duree_extraction_s") or 0 for p in pages) + nb_ok = sum(1 for p in pages if p.get("statut") == "ok") + nb_ign = sum(1 for p in pages if p.get("statut") == "ignoree") + + rows = [ + ["Début", t.get("debut", "—")[:19].replace("T", " ")], + ["Fin", (t.get("fin") or "—")[:19].replace("T", " ")], + ["Durée totale", _fmt_s(t.get("duree_totale_s"))], + ["Pages totales", str(t.get("nb_pages_total", "—"))], + ["Pages extraites (OK)", str(nb_ok)], + ["Pages ignorées", str(nb_ign)], + ["Temps identification", _fmt_s(duree_id)], + ["Temps extraction", _fmt_s(duree_ext)], + ["Erreurs", str(nb_err)], + ["Blocages 429", str(nb_b)], + ["Attente cumulée 429", _fmt_s(att)], + ] + tbl = Table(rows, colWidths=[8*cm, 8*cm]) + tbl.setStyle(TableStyle([ + ("FONTSIZE", (0,0), (-1,-1), 8), + ("FONTNAME", (0,0), (0,-1), "Helvetica-Bold"), + ("ROWBACKGROUNDS", (0,0), (-1,-1), [colors.HexColor("#f9f9f9"), colors.white]), + ("GRID", (0,0), (-1,-1), 0.3, colors.lightgrey), + ("LEFTPADDING", (0,0), (-1,-1), 5), + ("TOPPADDING", (0,0), (-1,-1), 3), + ("BOTTOMPADDING",(0,0),(-1,-1), 3), + ])) + story.append(tbl) + + # Détail pages + if pages: + story.append(Spacer(1, 0.2*cm)) + story.append(Paragraph("Détail pages :", body_style)) + page_rows = [["Page", "Type", "Identification", "Extraction", "Statut"]] + for p in pages: + page_rows.append([ + str(p["page"]), + p.get("type") or "—", + _fmt_s(p.get("duree_identification_s")), + _fmt_s(p.get("duree_extraction_s")), + p.get("statut") or "—", + ]) + tp = Table(page_rows, colWidths=[1.5*cm, 5*cm, 3*cm, 3*cm, 3.5*cm]) + tp.setStyle(TableStyle([ + ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#34495e")), + ("TEXTCOLOR", (0,0), (-1,0), colors.white), + ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"), + ("FONTSIZE", (0,0), (-1,-1), 7.5), + ("ROWBACKGROUNDS", (0,1), (-1,-1), [colors.HexColor("#f2f2f2"), colors.white]), + ("GRID", (0,0), (-1,-1), 0.3, colors.grey), + ("LEFTPADDING", (0,0), (-1,-1), 4), + ("TOPPADDING", (0,0), (-1,-1), 3), + ("BOTTOMPADDING",(0,0),(-1,-1), 3), + ])) + story.append(tp) + + # Erreurs + if t.get("erreurs"): + story.append(Spacer(1, 0.2*cm)) + story.append(Paragraph("Erreurs :", warn_style)) + for err in t["erreurs"]: + msg = f"Page {err['page']} — {err['phase']} : {err['message'][:120]}" + story.append(Paragraph(msg, warn_style)) + + # Blocages 429 + if t.get("blocages_429"): + story.append(Paragraph("Blocages rate limit (429) :", warn_style)) + for b in t["blocages_429"]: + msg = (f"Tentative {b['tentative']} — attente {b['attente_s']}s " + f"à {b['ts'][:19].replace('T', ' ')}") + story.append(Paragraph(msg, warn_style)) + + story.append(Spacer(1, 0.4*cm)) + story.append(HRFlowable(width="100%", thickness=0.5, color=colors.lightgrey)) + + doc.build(story) + print(f"✓ Rapport timing PDF : {path}") + + +# ─── Main ───────────────────────────────────────────────────────────────────── + +def main(): + pdf_files = sorted(SCAN_DIR.glob("*.pdf")) + if not pdf_files: + print(f"Aucun PDF dans {SCAN_DIR}") + sys.exit(1) + if len(sys.argv) > 1: + pdf_files = [f for f in pdf_files if sys.argv[1] in f.name] + if not pdf_files: + print(f"Aucun fichier pour '{sys.argv[1]}'") + sys.exit(1) + + print(f"Modèle : {MODEL}") + print(f"Fichiers: {len(pdf_files)}") + for f in pdf_files: + print(f" - {f.name}") + + # Charge le cache existant pour relances partielles + json_path = OUTPUT_DIR / "extraction_ogc_raw_qwen.json" + timing_path = OUTPUT_DIR / "timing_stats.json" + cache: dict[str, dict] = {} + timing_cache: dict[str, dict] = {} + + if json_path.exists() and len(sys.argv) > 1: + with open(json_path, encoding="utf-8") as f: + for r in json.load(f): + cache[r["fichier"]] = r + print(f"({len(cache)} fichiers en cache)") + + if timing_path.exists() and len(sys.argv) > 1: + with open(timing_path, encoding="utf-8") as f: + for t in json.load(f): + timing_cache[t["fichier"]] = t + + for pdf_path in pdf_files: + try: + result, timing = process_pdf(pdf_path) + cache[pdf_path.name] = result + timing_cache[pdf_path.name] = timing + except Exception as e: + print(f"\n⚠ Erreur {pdf_path.name} : {e}") + cache[pdf_path.name] = {"fichier": pdf_path.name, "erreur": str(e), + "pages_traitees": [], "pages_ignorees": []} + timing_cache[pdf_path.name] = { + "fichier": pdf_path.name, "erreur_globale": str(e), + "debut": None, "fin": None, "duree_totale_s": None, + "nb_pages_total": 0, "pages": [], "erreurs": [], "blocages_429": [], + } + + all_results = sorted(cache.values(), key=lambda r: r["fichier"]) + all_timings = sorted(timing_cache.values(), key=lambda t: t["fichier"]) + + export_excel(all_results, all_timings, OUTPUT_DIR / "extraction_ogc.xlsx") + + with open(json_path, "w", encoding="utf-8") as f: + json.dump(all_results, f, ensure_ascii=False, indent=2) + print(f"✓ JSON : {json_path}") + + with open(timing_path, "w", encoding="utf-8") as f: + json.dump(all_timings, f, ensure_ascii=False, indent=2) + print(f"✓ Timing JSON : {timing_path}") + + rapport_path = OUTPUT_DIR / "rapport_timing.pdf" + build_timing_pdf(all_timings, rapport_path) + print(f"✓ Rapport PDF : {rapport_path}") + + +if __name__ == "__main__": + main() diff --git a/generate_report.py b/generate_report.py new file mode 100644 index 0000000..d1a046f --- /dev/null +++ b/generate_report.py @@ -0,0 +1,683 @@ +""" +Génération du bilan d'extraction OGC — QWEN +Usage : python3 generate_report.py +""" + +import json +import re +import sys +from datetime import datetime +from pathlib import Path + +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import cm +from reportlab.platypus import ( + HRFlowable, PageBreak, Paragraph, SimpleDocTemplate, Spacer, Table, + TableStyle, +) + +# ─── Config ─────────────────────────────────────────────────────────────────── + +BASE = Path(__file__).parent +OUTPUT = BASE / "output" + +MODEL = "qwen3-vl:235b-cloud" +LABEL = "QWEN" +ACC = colors.HexColor("#1a5276") + +JSON_PATH = OUTPUT / "extraction_ogc_raw.json" +CORRECTION_PATH = OUTPUT / "extraction_ogc_raw_Correction.json" +TIMING_PATH = OUTPUT / "timing_stats.json" +REPORT_PATH = OUTPUT / "bilan_extraction_qwen_ogc.pdf" + +# ─── Styles ─────────────────────────────────────────────────────────────────── + +def make_styles(acc): + base = getSampleStyleSheet() + return { + "title": ParagraphStyle("title", parent=base["Title"], + fontSize=22, textColor=colors.white, alignment=TA_LEFT), + "subtitle": ParagraphStyle("subtitle", parent=base["Normal"], + fontSize=10, textColor=colors.HexColor("#aaaaaa"), alignment=TA_LEFT), + "section": ParagraphStyle("section", parent=base["Heading2"], + fontSize=13, textColor=acc, spaceBefore=16, spaceAfter=6), + "body": ParagraphStyle("body", parent=base["Normal"], fontSize=9, leading=14), + "small": ParagraphStyle("small", parent=base["Normal"], fontSize=8, + textColor=colors.HexColor("#444444")), + "right": ParagraphStyle("right", parent=base["Normal"], fontSize=7, + textColor=colors.HexColor("#888888"), alignment=TA_RIGHT), + "kpi_num": ParagraphStyle("kpi_num", parent=base["Normal"], fontSize=36, + fontName="Helvetica-Bold", alignment=TA_CENTER), + "kpi_lbl": ParagraphStyle("kpi_lbl", parent=base["Normal"], fontSize=8, + textColor=colors.HexColor("#777777"), alignment=TA_CENTER), + "warn": ParagraphStyle("warn", parent=base["Normal"], fontSize=8, + textColor=colors.HexColor("#c0392b")), + "footnote": ParagraphStyle("footnote", parent=base["Normal"], fontSize=7, + textColor=colors.HexColor("#888888")), + "center": ParagraphStyle("center", parent=base["Normal"], fontSize=9, alignment=TA_CENTER), + "bold": ParagraphStyle("bold", parent=base["Normal"], fontSize=9, + fontName="Helvetica-Bold"), + "th": ParagraphStyle("th", parent=base["Normal"], fontSize=8, + textColor=colors.white, fontName="Helvetica-Bold"), + } + +# ─── Utilitaires ────────────────────────────────────────────────────────────── + +def _fmt_s(s): + if s is None: + return "—" + s = int(s) + h, r = divmod(s, 3600) + m, sec = divmod(r, 60) + if h: + return f"{h}h{m:02d}m{sec:02d}s" + if m: + return f"{m}m{sec:02d}s" + return f"{sec}s" + + +def _prec_color(p: float): + if p >= 90: + return colors.HexColor("#27ae60") + if p >= 75: + return colors.HexColor("#e67e22") + return colors.HexColor("#e74c3c") + + +def _gravite_color(g: str): + return { + "Critique": colors.HexColor("#e74c3c"), + "Haute": colors.HexColor("#e67e22"), + "Moyenne": colors.HexColor("#f1c40f"), + "Faible": colors.HexColor("#27ae60"), + }.get(g, colors.black) + + +_TS = TableStyle + +def _base_table_style(acc): + return [ + ("BACKGROUND", (0, 0), (-1, 0), acc), + ("TEXTCOLOR", (0, 0), (-1, 0), colors.white), + ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"), + ("FONTSIZE", (0, 0), (-1, -1), 8), + ("ROWBACKGROUNDS",(0, 1), (-1, -1), [colors.HexColor("#f8f9fa"), colors.white]), + ("GRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#cccccc")), + ("LEFTPADDING", (0, 0), (-1, -1), 6), + ("RIGHTPADDING", (0, 0), (-1, -1), 6), + ("TOPPADDING", (0, 0), (-1, -1), 4), + ("BOTTOMPADDING", (0, 0), (-1, -1), 4), + ] + +# ─── Comparaison orig vs correction ─────────────────────────────────────────── + +def _flatten(d, prefix=""): + items = {} + if isinstance(d, dict): + for k, v in d.items(): + key = f"{prefix}.{k}" if prefix else k + if isinstance(v, (dict, list)): + items.update(_flatten(v, key)) + else: + items[key] = str(v).strip() + elif isinstance(d, list): + for i, v in enumerate(d): + key = f"{prefix}[{i}]" + if isinstance(v, (dict, list)): + items.update(_flatten(v, key)) + else: + items[key] = str(v).strip() + return items + + +def _normalize_keys(flat: dict) -> dict: + """Normalise les anciens noms de clés pour compatibilité avec les fichiers de correction antérieurs. + rang → niveau (renommage effectué en avril 2026). + """ + return {k.replace(".rang", ".niveau"): v for k, v in flat.items()} + + +def _get_cat(key: str, ptype: str = "") -> str: + k = key.lower() + if ptype == "ELEMENTS_PREUVE": + if any(x in k for x in ("medecin", "signataire", "date")): + return "Signataires / Dates" + return "Éléments de preuve" + if ptype in ("FICHE_ADMIN_2_2", "FICHE_ADMIN_1_2"): + if any(x in k for x in ("nom_medecin", "date_conc", "medecin")): + return "Signataires / Dates" + return "Concertation (2/2)" + if any(x in k for x in ("das_etab", "das_reco")): + return "DAS" + if any(x in k for x in ("sejour_etab", "sejour_reco")): + return "Données séjour" + if any(x in k for x in ("dp_etab", "dr_etab", "dp_reco", "dr_reco")): + return "DP / DR" + if any(x in k for x in ("rum_etab", "rum_reco")): + return "Données RUM" + if any(x in k for x in ("actes_etab", "actes_reco")): + return "Actes" + if any(x in k for x in ("ghm_", "ghs_")): + return "GHM / GHS" + if any(x in k for x in ("accord_desaccord", "se_coche", "atu", "ffm", "fsd")): + return "Accord / SE" + if any(x in k for x in ("date_debut", "date_fin", "nom_praticien")): + return "Signataires / Dates" + return "Métadonnées" + + +def compare_extractions(orig_list, corr_list): + orig_map = {r["fichier"]: r for r in orig_list} + corr_map = {r["fichier"]: r for r in corr_list} + + total_g = correct_g = 0 + per_dossier = [] + per_cat = {} + per_type = {} + + ep_counters = {k: {"occ": 0, "dossiers": set()} for k in [ + "dr_confondu_das", "annee_mal_lue", "se_coche_halluc", "maintien_X", + "provenance_halluc", "acte_dans_das", "das_manquant", "das_code_wrong", + "json_non_parsable", + ]} + + for fichier in sorted(orig_map): + if fichier not in corr_map: + continue + o = orig_map[fichier] + c = corr_map[fichier] + + o_pages = {(p["page"], p.get("type", "")): p for p in o.get("pages_traitees", [])} + c_pages = {(p["page"], p.get("type", "")): p for p in c.get("pages_traitees", [])} + structural_error = "raw_response" in json.dumps(o) + + dos_total = dos_correct = 0 + + for page_key in sorted(set(o_pages) & set(c_pages)): + op = o_pages[page_key] + cp = c_pages[page_key] + ptype = op.get("type", "UNKNOWN") + od = op.get("data", {}) + cd = cp.get("data", {}) + + if not isinstance(od, dict) or not isinstance(cd, dict): + continue + if "raw_response" in od or "raw_response" in cd: + ep_counters["json_non_parsable"]["occ"] += 1 + ep_counters["json_non_parsable"]["dossiers"].add(fichier) + continue + + o_flat = _normalize_keys(_flatten(od)) + c_flat = _normalize_keys(_flatten(cd)) + all_keys = set(o_flat) | set(c_flat) + + for k in all_keys: + ov = o_flat.get(k, "") + cv = c_flat.get(k, "") + cat = _get_cat(k, ptype) + per_cat.setdefault(cat, {"total": 0, "correct": 0}) + per_type.setdefault(ptype, {"total": 0, "correct": 0}) + per_cat[cat]["total"] += 1 + per_type[ptype]["total"] += 1 + dos_total += 1 + if ov == cv: + per_cat[cat]["correct"] += 1 + per_type[ptype]["correct"] += 1 + dos_correct += 1 + + if ptype == "FICHE_RECUEIL": + dr = (od.get("dr_etab") or {}).get("code", "") + cdr = (cd.get("dr_etab") or {}).get("code", "") + if dr and not cdr: + ep_counters["dr_confondu_das"]["occ"] += 1 + ep_counters["dr_confondu_das"]["dossiers"].add(fichier) + + prov = str((od.get("sejour_etab") or {}).get("provenance", "")).strip() + cprov = str((cd.get("sejour_etab") or {}).get("provenance", "")).strip() + if prov and not cprov: + ep_counters["provenance_halluc"]["occ"] += 1 + ep_counters["provenance_halluc"]["dossiers"].add(fichier) + + se = str(od.get("se_coche", "")).strip() + cse = str(cd.get("se_coche", "")).strip() + if se and not cse: + ep_counters["se_coche_halluc"]["occ"] += 1 + ep_counters["se_coche_halluc"]["dossiers"].add(fichier) + + das = od.get("das_etab") or [] + cdas = cd.get("das_etab") or [] + dp = (od.get("dp_etab") or {}).get("code", "") + if dp and not [x for x in das if isinstance(x, dict) and x.get("code")]: + ep_counters["das_manquant"]["occ"] += 1 + ep_counters["das_manquant"]["dossiers"].add(fichier) + for od2, cd2 in zip(das, cdas): + if isinstance(od2, dict) and isinstance(cd2, dict): + if od2.get("code") != cd2.get("code") and cd2.get("code"): + oc = od2.get("code", "") + if len(oc) >= 7 and oc[:4].isalpha(): + ep_counters["acte_dans_das"]["occ"] += 1 + ep_counters["acte_dans_das"]["dossiers"].add(fichier) + else: + ep_counters["das_code_wrong"]["occ"] += 1 + ep_counters["das_code_wrong"]["dossiers"].add(fichier) + + if ptype == "FICHE_ADMIN_2_2": + m = str(od.get("maintien_avis_controleur", "")).strip() + cm_ = str(cd.get("maintien_avis_controleur", "")).strip().lower() + if m.upper() == "X" and cm_ == "oui": + ep_counters["maintien_X"]["occ"] += 1 + ep_counters["maintien_X"]["dossiers"].add(fichier) + + for k in od: + if "date" in k.lower(): + ov = str(od.get(k, "")).strip() + cv = str(cd.get(k, "")).strip() + if ov != cv: + oy = re.findall(r"1[6-9]", ov) + cy = re.findall(r"1[6-9]", cv) + if oy and cy and oy != cy: + ep_counters["annee_mal_lue"]["occ"] += 1 + ep_counters["annee_mal_lue"]["dossiers"].add(fichier) + + prec = round(dos_correct / dos_total * 100) if dos_total else 0 + per_dossier.append({ + "fichier": fichier.replace(".pdf", ""), + "total": dos_total, "correct": dos_correct, + "errors": dos_total - dos_correct, + "precision": prec, "structural_error": structural_error, + }) + total_g += dos_total + correct_g += dos_correct + + prec_g = round(correct_g / total_g * 100, 1) if total_g else 0 + n_total = len(orig_list) + error_patterns = [] + for desc, key, gravite in [ + ("DR confondu avec DAS", "dr_confondu_das", "Critique"), + ("Année mal lue (ex : 2017 au lieu de 2018)", "annee_mal_lue", "Haute"), + ("se_coche inventé ('1' ou '4' au lieu de vide)", "se_coche_halluc", "Haute"), + ("maintien_avis = 'X' au lieu de 'oui'", "maintien_X", "Haute"), + ("provenance inventé ('8' au lieu de vide)", "provenance_halluc", "Haute"), + ("Code acte mis dans DAS", "acte_dans_das", "Haute"), + ("DAS entier manquant", "das_manquant", "Critique"), + ("DAS code mauvais", "das_code_wrong", "Critique"), + ("JSON non parsable", "json_non_parsable", "Critique"), + ]: + e = ep_counters[key] + if e["occ"] > 0: + error_patterns.append({ + "desc": desc, "occ": e["occ"], + "dossiers": len(e["dossiers"]), "n_total": n_total, + "gravite": gravite, + }) + + return { + "total": total_g, "correct": correct_g, + "errors": total_g - correct_g, "precision": prec_g, + "per_dossier": per_dossier, "per_cat": per_cat, + "per_type": per_type, "error_patterns": error_patterns, + } + +# ─── Sections PDF ───────────────────────────────────────────────────────────── + +def _section_header(story, S, acc, text): + story.append(Paragraph(text, S["section"])) + story.append(HRFlowable(width="100%", thickness=0.5, color=acc, spaceAfter=6)) + + +def _build_header(story, S, acc, meta): + hdr = Table( + [[Paragraph(f"BILAN D'EXTRACTION —\nMODÈLE {LABEL}", S["title"]), + Paragraph(meta, S["subtitle"])]], + colWidths=[10*cm, 7*cm], + ) + hdr.setStyle(_TS([ + ("BACKGROUND", (0, 0), (-1, -1), acc), + ("LEFTPADDING", (0, 0), (-1, -1), 16), + ("RIGHTPADDING", (0, 0), (-1, -1), 12), + ("TOPPADDING", (0, 0), (-1, -1), 16), + ("BOTTOMPADDING", (0, 0), (-1, -1), 16), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ])) + story.append(hdr) + story.append(Spacer(1, 0.5*cm)) + + +def _build_kpis(story, S, acc, cmp): + GREEN = colors.HexColor("#27ae60") + RED = colors.HexColor("#e74c3c") + kpi_num_style = ParagraphStyle("kpi_num2", parent=S["kpi_num"], fontSize=28, leading=32) + + def kpi_cell(num, lbl, color=colors.HexColor("#333333")): + return [Paragraph(f'{num}', kpi_num_style), + Paragraph(lbl, S["kpi_lbl"])] + + cells = [ + kpi_cell(str(len(cmp["per_dossier"])), "Dossiers analysés", acc), + kpi_cell(str(cmp["total"]), "Champs comparés", colors.HexColor("#333333")), + kpi_cell(str(cmp["correct"]), "Champs corrects", GREEN), + kpi_cell(str(cmp["errors"]), "Champs en erreur", RED), + kpi_cell(f"{cmp['precision']}%", "Précision globale", + GREEN if cmp["precision"] >= 85 else RED), + ] + kpi_t = Table([[c[0] for c in cells], [c[1] for c in cells]], colWidths=[3.4*cm]*5) + kpi_t.setStyle(_TS([ + ("BACKGROUND", (0, 0), (-1, -1), colors.HexColor("#f8f9fa")), + ("BOX", (0, 0), (-1, -1), 0.5, colors.HexColor("#dddddd")), + ("INNERGRID", (0, 0), (-1, -1), 0.3, colors.HexColor("#eeeeee")), + ("TOPPADDING", (0, 0), (-1, 0), 14), + ("BOTTOMPADDING", (0, 0), (-1, 0), 6), + ("TOPPADDING", (0, 1), (-1, 1), 4), + ("BOTTOMPADDING", (0, 1), (-1, 1), 12), + ("ALIGN", (0, 0), (-1, -1), "CENTER"), + ("VALIGN", (0, 0), (-1, -1), "MIDDLE"), + ])) + story.append(kpi_t) + story.append(Spacer(1, 0.4*cm)) + + +def _build_per_dossier(story, S, acc, W, cmp): + header = ["N° OGC", "Champs\ntotaux", "Champs\ncorrects", "Erreurs", + "Précision", "Err. structurelle"] + rows = [header] + style_extra = [] + + for i, d in enumerate(cmp["per_dossier"], start=1): + prec = d["precision"] + rows.append([d["fichier"], str(d["total"]), str(d["correct"]), + str(d["errors"]), f"{prec}%", + "■ Oui" if d["structural_error"] else "—"]) + pc = _prec_color(prec) + style_extra += [ + ("TEXTCOLOR", (4, i), (4, i), pc), + ("FONTNAME", (4, i), (4, i), "Helvetica-Bold"), + ("TEXTCOLOR", (3, i), (3, i), + colors.HexColor("#e74c3c") if d["errors"] > 0 else colors.HexColor("#27ae60")), + ("FONTNAME", (3, i), (3, i), "Helvetica-Bold"), + ("TEXTCOLOR", (2, i), (2, i), colors.HexColor("#27ae60")), + ("FONTNAME", (2, i), (2, i), "Helvetica-Bold"), + ] + if d["structural_error"]: + style_extra += [("TEXTCOLOR", (5, i), (5, i), colors.HexColor("#e74c3c")), + ("FONTNAME", (5, i), (5, i), "Helvetica-Bold")] + + tot_prec = round(cmp["correct"] / cmp["total"] * 100, 1) if cmp["total"] else 0 + n_struct = sum(1 for d in cmp["per_dossier"] if d["structural_error"]) + rows.append(["TOTAL", str(cmp["total"]), str(cmp["correct"]), + str(cmp["errors"]), f"{tot_prec}%", f"{n_struct} dossier(s)"]) + n = len(rows) + style_extra += [("BACKGROUND", (0, n-1), (-1, n-1), colors.HexColor("#eaf0fb")), + ("FONTNAME", (0, n-1), (-1, n-1), "Helvetica-Bold")] + + col_w = [W*0.16, W*0.12, W*0.14, W*0.11, W*0.13, W*0.34] + t = Table([[Paragraph(str(c), S["th"] if i == 0 else S["small"]) for c in row] + for i, row in enumerate(rows)], colWidths=col_w) + t.setStyle(_TS(_base_table_style(acc) + style_extra)) + story.append(t) + + +def _build_per_cat(story, S, acc, W, cmp): + cat_order = [ + "DAS", "Données séjour", "DP / DR", "Données RUM", + "Signataires / Dates", "Métadonnées", "Concertation (2/2)", + "Éléments de preuve", "Accord / SE", "GHM / GHS", "Actes", + ] + rows = [["Catégorie", "Champs\ntotaux", "Champs\ncorrects", "Erreurs", "Précision"]] + style_extra = [] + for i, cat in enumerate(cat_order, start=1): + d = cmp["per_cat"].get(cat) + if not d: + continue + prec = round(d["correct"] / d["total"] * 100) if d["total"] else 0 + rows.append([cat, str(d["total"]), str(d["correct"]), + str(d["total"] - d["correct"]), f"{prec}%"]) + style_extra += [("TEXTCOLOR", (4, i), (4, i), _prec_color(prec)), + ("FONTNAME", (4, i), (4, i), "Helvetica-Bold")] + + col_w = [W*0.40, W*0.15, W*0.15, W*0.15, W*0.15] + t = Table([[Paragraph(str(c), S["th"] if i == 0 else S["small"]) for c in row] + for i, row in enumerate(rows)], colWidths=col_w) + t.setStyle(_TS(_base_table_style(acc) + style_extra)) + story.append(t) + + +def _build_per_type(story, S, acc, W, cmp): + rows = [["Type de page", "Champs\ntotaux", "Champs\ncorrects", "Erreurs", "Précision"]] + style_extra = [] + for i, (ptype, d) in enumerate(sorted(cmp["per_type"].items(), + key=lambda x: -x[1]["total"]), start=1): + prec = round(d["correct"] / d["total"] * 100) if d["total"] else 0 + label = (ptype.replace("FICHE_", "Fiche ").replace("_RECUEIL", "de recueil") + .replace("_ADMIN_", " administrative ").replace("_2_2", "2/2") + .replace("_1_2", "1/2").replace("ELEMENTS_PREUVE", "Éléments de preuve")) + rows.append([label, str(d["total"]), str(d["correct"]), + str(d["total"] - d["correct"]), f"{prec}%"]) + style_extra += [("TEXTCOLOR", (4, i), (4, i), _prec_color(prec)), + ("FONTNAME", (4, i), (4, i), "Helvetica-Bold")] + + col_w = [W*0.40, W*0.15, W*0.15, W*0.15, W*0.15] + t = Table([[Paragraph(str(c), S["th"] if i == 0 else S["small"]) for c in row] + for i, row in enumerate(rows)], colWidths=col_w) + t.setStyle(_TS(_base_table_style(acc) + style_extra)) + story.append(t) + + +def _build_error_patterns(story, S, acc, W, cmp): + rows = [["Ce que le modèle a raté — catégorie d'erreur", + "Occur-\nrences", "Sur combien\nde dossiers", "Gravité"]] + style_extra = [] + for i, p in enumerate(cmp["error_patterns"], start=1): + gc = _gravite_color(p["gravite"]) + rows.append([p["desc"], str(p["occ"]), + f"{p['dossiers']} / {p['n_total']}", p["gravite"]]) + style_extra += [("TEXTCOLOR", (3, i), (3, i), gc), + ("FONTNAME", (3, i), (3, i), "Helvetica-Bold")] + + col_w = [W*0.56, W*0.10, W*0.17, W*0.17] + t = Table([[Paragraph(str(c), S["th"] if i == 0 else S["small"]) for c in row] + for i, row in enumerate(rows)], colWidths=col_w) + t.setStyle(_TS(_base_table_style(acc) + style_extra)) + story.append(t) + + +def _build_timing(story, S, acc, W, timing_data): + if not timing_data: + story.append(Paragraph( + "Aucune donnée temporelle disponible. " + "Relancez l'extraction pour générer timing_stats.json.", S["small"])) + return + + total_s = sum(t.get("duree_totale_s") or 0 for t in timing_data) + total_pages= sum(t.get("nb_pages_total") or 0 for t in timing_data) + total_err = sum(len(t.get("erreurs", [])) for t in timing_data) + total_429 = sum(len(t.get("blocages_429", [])) for t in timing_data) + total_wait = sum(b["attente_s"] for t in timing_data for b in t.get("blocages_429", [])) + n_dos = len(timing_data) + + story.append(Paragraph("Résumé global", S["bold"])) + story.append(Spacer(1, 0.2*cm)) + + kpi_rows = [ + ["Durée totale d'extraction", _fmt_s(total_s)], + ["Durée moyenne / dossier", _fmt_s(total_s / n_dos) if n_dos else "—"], + ["Durée moyenne / page", _fmt_s(total_s / total_pages) if total_pages else "—"], + ["Pages traitées", str(total_pages)], + ["Erreurs totales", str(total_err)], + ["Blocages rate limit (429)", str(total_429)], + ["Temps perdu en attentes 429", _fmt_s(total_wait)], + ["Temps utile (hors 429)", _fmt_s(total_s - total_wait)], + ] + style_kpi = _base_table_style(acc) + [ + ("ALIGN", (1, 0), (1, -1), "CENTER"), + ("TEXTCOLOR", (1, 0), (1, -1), acc), + ("FONTNAME", (1, 0), (1, -1), "Helvetica-Bold"), + ] + t_kpi = Table([[Paragraph(k, S["small"]), Paragraph(v, S["small"])] + for k, v in kpi_rows], colWidths=[W*0.6, W*0.4]) + t_kpi.setStyle(_TS(style_kpi)) + story.append(t_kpi) + story.append(Spacer(1, 0.4*cm)) + + story.append(Paragraph("Détail par dossier", S["bold"])) + story.append(Spacer(1, 0.2*cm)) + + header = ["Dossier", "Début", "Fin", "Durée", "Pages", + "Erreurs", "Blocages\n429", "Attente\n429"] + rows = [header] + style_dos = _base_table_style(acc) + + for i, t in enumerate(timing_data, start=1): + debut = (t.get("debut") or "")[:16].replace("T", " ") + fin = (t.get("fin") or "")[:16].replace("T", " ") + n_err = len(t.get("erreurs", [])) + n_b = len(t.get("blocages_429", [])) + att = sum(b["attente_s"] for b in t.get("blocages_429", [])) + rows.append([ + t["fichier"].replace(".pdf", ""), debut, fin, + _fmt_s(t.get("duree_totale_s")), str(t.get("nb_pages_total", "—")), + str(n_err), str(n_b), _fmt_s(att) if att else "—", + ]) + if n_err > 0: + style_dos += [("TEXTCOLOR", (5, i), (5, i), colors.HexColor("#e74c3c")), + ("FONTNAME", (5, i), (5, i), "Helvetica-Bold")] + if n_b > 0: + style_dos += [("TEXTCOLOR", (6, i), (6, i), colors.HexColor("#e67e22")), + ("FONTNAME", (6, i), (6, i), "Helvetica-Bold")] + + col_w = [W*0.18, W*0.14, W*0.14, W*0.10, W*0.08, W*0.09, W*0.10, W*0.17] + t_dos = Table([[Paragraph(str(c), S["th"] if i == 0 else S["small"]) for c in row] + for i, row in enumerate(rows)], colWidths=col_w) + t_dos.setStyle(_TS(style_dos)) + story.append(t_dos) + + has_issues = any(t.get("erreurs") or t.get("blocages_429") for t in timing_data) + if has_issues: + story.append(Spacer(1, 0.4*cm)) + story.append(Paragraph("Erreurs et blocages détaillés", S["bold"])) + story.append(Spacer(1, 0.2*cm)) + for t in timing_data: + if not t.get("erreurs") and not t.get("blocages_429"): + continue + story.append(Paragraph(t["fichier"].replace(".pdf", ""), S["bold"])) + for err in t.get("erreurs", []): + story.append(Paragraph( + f" ⚠ Page {err['page']} — {err['phase']} : {err['message'][:100]}", + S["warn"])) + for b in t.get("blocages_429", []): + story.append(Paragraph( + f" ⏳ Blocage 429 — tentative {b['tentative']}, " + f"attente {b['attente_s']}s à {str(b.get('ts',''))[:16].replace('T',' ')}", + ParagraphStyle("b429", parent=S["small"], + textColor=colors.HexColor("#e67e22")))) + story.append(Spacer(1, 0.1*cm)) + + +# ─── Main builder ───────────────────────────────────────────────────────────── + +def build_pdf(): + W = A4[0] - 4*cm + + if not JSON_PATH.exists(): + print(f"⚠ JSON introuvable : {JSON_PATH}") + sys.exit(1) + + with open(JSON_PATH, encoding="utf-8") as f: + orig_data = json.load(f) + + cmp = None + if CORRECTION_PATH.exists(): + with open(CORRECTION_PATH, encoding="utf-8") as f: + corr_data = json.load(f) + cmp = compare_extractions(orig_data, corr_data) + + timing_data = None + if TIMING_PATH.exists(): + with open(TIMING_PATH, encoding="utf-8") as f: + timing_data = json.load(f) + + S = make_styles(ACC) + story = [] + + if cmp: + etabl = finess = controle = "" + for r in orig_data: + for pt in r.get("pages_traitees", []): + d = pt.get("data", {}) + if d.get("etablissement"): + etabl = d["etablissement"] + if d.get("finess"): + finess = d["finess"] + if d.get("date_debut_controle"): + controle = d["date_debut_controle"] + if etabl and finess and controle: + break + if etabl: + break + meta = (f"{etabl} · FINESS {finess}\n" + f"{len(orig_data)} dossiers OGC · Contrôle {controle} · " + f"{datetime.now().strftime('%B %Y').capitalize()}") + else: + meta = (f"{len(orig_data)} dossiers OGC\n" + f"Généré le {datetime.now().strftime('%d/%m/%Y à %H:%M')}") + + _build_header(story, S, ACC, meta) + + if cmp: + _section_header(story, S, ACC, "1. Indicateurs globaux") + _build_kpis(story, S, ACC, cmp) + + _section_header(story, S, ACC, "2. Résultats par dossier OGC") + _build_per_dossier(story, S, ACC, W, cmp) + story.append(Spacer(1, 0.4*cm)) + + _section_header(story, S, ACC, "3. Précision par catégorie de champ") + _build_per_cat(story, S, ACC, W, cmp) + story.append(Spacer(1, 0.4*cm)) + + _section_header(story, S, ACC, "4. Précision par type de page") + _build_per_type(story, S, ACC, W, cmp) + story.append(Spacer(1, 0.4*cm)) + + if cmp["error_patterns"]: + _section_header(story, S, ACC, "5. Patterns d'erreurs récurrents") + _build_error_patterns(story, S, ACC, W, cmp) + story.append(Spacer(1, 0.4*cm)) + + sec_timing = 6 + else: + sec_timing = 1 + + story.append(PageBreak()) + _section_header(story, S, ACC, f"{sec_timing}. Analyse temporelle") + _build_timing(story, S, ACC, W, timing_data) + + story.append(Spacer(1, 0.5*cm)) + note = ( + "Rapport généré par comparaison automatique de extraction_ogc_raw.json " + "vs extraction_ogc_raw_Correction.json · " + f"Périmètre : {len(orig_data)} dossiers OGC · " + "Les pourcentages de précision sont calculés champ par champ." + if cmp else + f"Rapport généré automatiquement · {len(orig_data)} dossiers OGC · " + "Aucun fichier de correction disponible — métriques de précision non calculées." + ) + story.append(HRFlowable(width="100%", thickness=0.3, color=colors.grey)) + story.append(Paragraph(note, S["footnote"])) + + doc = SimpleDocTemplate( + str(REPORT_PATH), pagesize=A4, + leftMargin=2*cm, rightMargin=2*cm, + topMargin=2*cm, bottomMargin=2*cm, + title=f"Bilan extraction OGC — {LABEL}", + author="EttaSanté / T2A", + ) + doc.build(story) + print(f"✓ {REPORT_PATH}") + + +if __name__ == "__main__": + print(f"Génération bilan {LABEL}...") + build_pdf() diff --git a/output/bilan_extraction_qwen_ogc (2).pdf b/output/bilan_extraction_qwen_ogc (2).pdf new file mode 100644 index 0000000..161ab43 --- /dev/null +++ b/output/bilan_extraction_qwen_ogc (2).pdf @@ -0,0 +1,137 @@ +%PDF-1.4 +% ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /ZapfDingbats /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 11 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/PageMode /UseNone /Pages 11 0 R /Type /Catalog +>> +endobj +10 0 obj +<< +/Author (EttaSant\351 / T2A) /CreationDate (D:20260420204759+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260420204759+02'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (Bilan extraction OGC \204 QWEN) /Trapped /False +>> +endobj +11 0 obj +<< +/Count 4 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R ] /Type /Pages +>> +endobj +12 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2572 +>> +stream +Gb!#_D,VB4&H9tYd$IEtAo_SXY)`]VL(&bIS7t(F0,%[,rtqcm.\Wbj-.U\e`CR%q+`8TaPi@<9n'c:ST:A>BB8ouZd@l7?b>bn4`aO:8go`FS#u6gRT)MPYdhe_Q)Ag#l0@8hPDf%UoBS&BjQ(&r7c#0C\DICuRkL[Mb+-9AkEfg?2adtP%Fr<,i*k""EkT1kXOcIt_auc_AGN`<9t:o2$UVfEj[[Os9%k)a3rZCq]-iOmfUtq/!B^Eg-BD^\i0HoPa'L'GuKoA:[%0J)$EPX^F>B8;Tp[!ti[fbbX>^.W0Lq,T:#S2dR7cq_52PZrm\G=B$oc'BRChftPOQ,t"h7am2gDOk?ADT0*HSsiN-Z@m!lTO9&B2b5M[1.W2110W&nD7QjQfU>3$BqtL9dDF.8)9*tHG0'$`%hR+-%L'_N9Ru^?Lo&N:DhN5/h'_s<"p-N9iMs,<2?f+=6gC\I7-:$0WS+1"rb<-`"9JMBt]N`oHXDa:^6/`rOF9]]qi4-ag:cUq1neDcTA*2oc8[!(uLG$6,Y8b9gD5G]kg^K]SI+1("fIM(+&WrS2Y+3;n8S6e:M6L0n"7ZKPkC<7M=T!)PNY4#U!D+h]3W43%u;4A1;U%7^e$\&pW7t2JQV+q4'M2e,HKYYqR#5A`QH'sg?;AOs`\Dn)f`9-9+3@pe,t%::qBS#GZn/b5?@A!+B3LJR4KT[@"4hD*j_MC4r+J#kJG[Su,WLVJMrNg@]:`kt?^VO4q-&SL+pj$k;pXDTD`)FI8Fqc]nLdUQOImOo+VWQ\2dM8o@o/?!/qU7kR9`%Qh>Y2Af6\D'/3S>AKI]RrL&G$u5#,<>E$c2I1trgq6IqXVdsUbo>20ploi=dEDmWi%;#MN8r5X@-U"].Hl\,Zkt4r9l*/tf7gDfPjmFeEM46Z<&GBXNh-7i/s,6&43maTYL+=>AY9:k82J!#Rg7i;G9YUWug\8K`6X'!eMHG&mX'&2akXnK/!geQ)SZZ:#g"$AtVQ]EB]W9<(h1e^N@2M$,4`s"SPEs.H:mq[>t#&i5duZP!M&m*cjk*ftMDJl=abkKOj_bK1VHiIj(cOs"%u#endstream +endobj +13 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2177 +>> +stream +Gb!TY?#uJr&;KZF/,&+!da"("M%Ak+jWnQo:=G9V^!"V;MJ5[1L(r[TkMMnsYm&cfYZWoU-;>LL=M(j[Q@2hD@,j1Mr#\Z7rY`W<9*U?mp^8drEa\e\0#pK>O4a"qf<[TY(&jql`K[:iIFrBcT=^*bI.\JOiN'>>ifMt:-2N&<#"LFA8gWF"[ip[fbB"Y&J$6$F)R&BDg$(4HheQc7HfX='FU[%fce>nRPNj!oH'\/5iUE#lg<.C_nr8@rDulKj@WE!oHrtkM#%<6(Ugpu"k/kRI3/*9[B/';hj6,.98,P",0I8=.FVU$!cYKr,I:hI$it,@67&qWm#YT+hM(J1>99ukh.63Eo=E/^'0#rgNpknmMBjY/$t@R>S3`#4DE;@VQC;g:!Rmuee(iXA,jGS]AfX9WPeVHp)1Cc];ZE$&l[RlLQ1O8L9Pq9_jLKt'Ml,B?mG28GlqH\.6P,k&M4M?-FWS4NPr'ipE5I8''fQ:M5Bn78\l:$@uBa$RA=f[4AE[@bC_oP'=^3aVu66k2eaP7)/IQi$u$uQA383D1_N0'"17#_:D1]qIt,a7ZG]t`fY%c*djW=N0T"^2Mrp't2et7,]%uEB8Rb9dWZ*i78[*)g+KCP`VSrJeFPUiIBFOQYAs+!Oki!`UCC"/jWSuC.CF&62eYjE=RXDaQ>!s:()_D[ilNe/s7?dAe!HF#CD"U\j,\t)r]UIgp725"&e3O`FiHZ52ijh@>2Y73'^)^A?e`,K2eWD09a.cmU_\A!VpcXT*0;`up4%uF9\*G'rTCjK^n'FMrC6r#dEp?q*V2Ns;GbrTdGM-P/h2@.(FWW(ce30`i:q(Ae1W>:,6?eng3FD3b"jX^:1s17jquT7AD]4lUYZ8NW_YgF=lMn?u[Du?Q+k!iq.tr_Vq#XWb$]m-PjAg0]H4%4U\F6pHPdps/*10i9-B#L[Je+8EY/>L]Fkf?G62ob/Zq.NU]CmM^oZ"_(Qeo6.UZSr./O\0okG]qXp$p;POMb(@r:\erGf%(jgpI^+hc<7.mA.F=PBq$l2=Ii<90+7lWT:,KfSD)d!D^X_Q?P^"mpP)OCG:1(6([5Tr+CMMhJ@agm3YkDT4#s97:G'P"PhAh4&"fMa.?r2@rKHU*QG8=HV^]J?cM'V#];9@5@)!Pqhq7-Z^pbPK0s?g.=R\n[N"qpL1V,VT)j:JEP6iJLLqLHm6M\N-.XGh-)hca(T2^$YruJ6KLCt9Gbg[Zi(P2>:^fE-OJiEQfN/X\2>)lU)N2rSHRb$H]%:TSk'nrgrE0hc5)?iDC>qf^+^Q9>,3qCshR6JY#OI)]r5tZ0?1(`8qu.(%JTE<=q;d>)7miWec@NbQU?KZ&=K!aR^fQ0LIj\MW>k:4FN6lA;eY[*V+X%/KH#O`QVE6nN0.Dg.)jd*2'ATL"u!)@ES\oqT-q%-3%3nDbegL,.&Ti5*+VcJn[835]-ENJ1Sc!XDl?Cnkh73dO^Cec5DGaR%n>9?Q^CTi=H!s*$hC@KMU9rj'cT48Icdld$6'tD3a&.FL_UtBSZWS8b'C3)B.8RO%IGS%.n/*fBc_it8G_;2U^8/k]%`)Wh_So[TSl3+t,dt0%LbL>O@gGh#g"g+=nHX"C7PtbO-mNn:PP'CFq!UTNh,2[,`hI@l3.KNB3UB@(eA8V_*R)90K)IZ`oeeRHk>H8KS`P;"fR<),o\Zgijp;+Z#_#n8=UV'!DU:R2?F9pQs!pjOaNc+8`!1W6-bWN&3sag5fr0WO_^+j1E(jA7tYM0Igk=[NF`BgWUb3G5Z?f/&-4'%%46VBM)V4(miX%hM9LRtp[.j$AIS>kH/^!R`5*&DE6I-=P?.@F[-CkZendstream +endobj +14 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2240 +>> +stream +Gau`XD/\/e&H;*)_/DsS7-2&%IV[OO?l6,Uo>^ibPGkGgb>)mX>Q=Xmfchk6Z7,,(`C1I&UjJ+l51K,$$UNEm8H&QoM9t!'HM)h36IG?S,!=n!q)`o;>l7]ce!6).;H$:d/kJO>eE/q>c3Q<;JhffP0YbiAUcB6)s0+1$afhlQ7jif-hHRY6#cFZ1I7\Kbr*6Fg@G("@U#l]&XPOW8h8BCTYC^(1E#Ds[CFs7eoXI7rLn@`52S%[%Srr"L)r7bgXRXF2iM:7k0fuX(C5;t5ihorZQ?Th,SD?)aK_)`e/2MbY:g][cDfaaC7S"hS%H)F9N--On%Hog#kMc5TsHT9EQr6Yir4D8o_*KD6uRQ.S_fea'Ic93D42;Co$]j$BteW;rSF):r&Bm2W:T<1e)!hD_i%g.g(`cMQf=WP^UgJs]5;1QSXC&G/uh?0(t1b4O`,o;6le=cKN.3kde+4Vohr?t=_tPNHA1d2LVp^Ua89O\5eZ2C//BP^s1rJ1Ok1XU&)D@E_IXkP$9m?U$Gq5)&#U^(JX8X',dX$EZ?!\8m-a07ZsQf:g8rJ?Kl4%QeMf/>*s6>'3\mKUVQH/?UiD27!$a(;Y^DbPP`+,J-eiFlicAj3^JdbMti[\1#=\I&Ca7cSXmSB/FV>2'EsT%_XN1I9&=[l,D7R]5T>BhL5s^&8ku9RD-?hB(t=IZ81TAT(@+)p^MhW5f->c5OIh_`,.kj:i^!D0X717?Y)(p(?S'RcPNQRE@*SDkF$qm:M;4MKqBYqhK<<^D]4*W**2tB(O51m:(gXk9r.D#;9p&l/B;n(Kahu7iT&mKuA'3&8pLnL)b=;H\o/OiZE@`jseL4C85MdA)Z(=3352K^[adNo21nALh&X7^;jro78+]pPE?0CksCqdJ(Ih-Ol%)GnL@t%-Xd!S:SCF7*4Q.,fZ`]6$3JGq]F8Y9kOHRK1&2XFI7:`,kn(9!"iWIZ$h@=W7>\.;)=VHXZ5jH#A:)GT%h6m)<'_?!A[cb?1Z%"$^7&&dG.jNk,iII%%:Md[\5:^m46"*sTk!0+5o,RmVW:WBVW.NHgrpZtOeJ/Eb3HhN"MS&B>-/f%0af$ELs9TLLXcK/d,#pt66k+.#:/@\0A<5VZ'_`'BoB+@%1<,8r+gUPEh/q^02]9n(-sZj&o\8RWUaj5O/XYR's=!(F+F23;;i%+#jg5[IJPZ7dDW1Dp[9npC),1k8%Xqd:Br8krmK#6G[Y4qOA(p*Bq6liR8,@ip?GiGa:8G_UWFiIE8iB4"O=,0hiL*1iKBk4B2h/;AQq5V_Uu%-6(H!]7n\%$e`h,U.k%)%L?NHhO9mJ46!a_`6HOp2l0_8o?Q5/*m+^6kG^_:QT1^5@R>1]hjY2LBs"@"M$#W0;@;nLq;RCM*~>endstream +endobj +15 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 506 +>> +stream +Gasak_+rc>&A@6WhJq;&=kedsE!-1S6A7ad/$4bM6dR.iG..&748*j^.U=N5BkH]L^GgTTN[69KXQb;B"t,`W>bLlnT!KE]%5++92<009Q!gU-2+OW<`bST'4\@A+g:%qCn97]cVC+W[3oPpO[^a"3LFhO'1bWOV7eH/VN\M6e!UF=-XE[5FhP0PG@utH#b>E[u2Dlc\`LJe#apTPp"^Vqp5f>qB`WPVmq2crJ#m?Gci,I(8mdMJ7)7]&Kbu)5ZWhFomcOiZTj^-m95eDA+Nm:F6=0gaO1d7?V(Q@\%kq4Hb\!-.Ne@f`OSh063a_N\TDHOO%XM''m8arA288Ib[(37Vju,AA80-@aLJ~>endstream +endobj +xref +0 16 +0000000000 65535 f +0000000073 00000 n +0000000124 00000 n +0000000231 00000 n +0000000343 00000 n +0000000426 00000 n +0000000631 00000 n +0000000836 00000 n +0000001041 00000 n +0000001246 00000 n +0000001315 00000 n +0000001621 00000 n +0000001699 00000 n +0000004363 00000 n +0000006632 00000 n +0000008964 00000 n +trailer +<< +/ID +[<8465917567113f196f99bd144e52b34c><8465917567113f196f99bd144e52b34c>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 10 0 R +/Root 9 0 R +/Size 16 +>> +startxref +9561 +%%EOF diff --git a/output/bilan_extraction_qwen_ogc.pdf b/output/bilan_extraction_qwen_ogc.pdf new file mode 100644 index 0000000..8deee03 --- /dev/null +++ b/output/bilan_extraction_qwen_ogc.pdf @@ -0,0 +1,131 @@ +%PDF-1.4 +% ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 11 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 12 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 13 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 14 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 10 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/PageMode /UseNone /Pages 10 0 R /Type /Catalog +>> +endobj +9 0 obj +<< +/Author (EttaSant\351 / T2A) /CreationDate (D:20260421112127+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260421112127+02'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (Bilan extraction OGC \204 QWEN) /Trapped /False +>> +endobj +10 0 obj +<< +/Count 4 /Kids [ 4 0 R 5 0 R 6 0 R 7 0 R ] /Type /Pages +>> +endobj +11 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2546 +>> +stream +Gb!#_gN)%,&:O:Si$D="1L[Mtj3b,sN&.UWa3(c[d>KJ)WACiE9(K@fqZu.F4AXN^rXA-J[l`P"GsuG-Z5A)+oR5`#*:2)qS39"U6+_=^[?PDqF/^/""X);TGToOU,MSc_P);mOJ'c^Ir9MrDk^'S3!^9cq_83PZrn'mtu((fNan:mQ';$5AG05J>9mEsBa<=U$)Fg7,CLDV1CG9510k]ACDa?Q$j0GOpp@pG"qKaPK1LPmMCN!O5TBj!Q!UC%ob5ZCX''M0>X'#VP[2Cq?,hrnU8XKM;GGmb@$&(pJsoK.(,us#GN*Yp$I(j,`H9%'&*hh17:W3?PZ/i,:lg?I*9=:$g#"[rtim=[+E:D5-/c13]C_f$W3jrS$'GBppk3B`9GdZ.[%W@C==>O2X5QX/Kril_F@UIZg(hq9#a"/^b;D\$nR;]=Z/pNGt3En!Dq/7DGck>tne=s!#upDkHLb4$lFR\lP][.`Va1AZ@[GJm#T?U/%rURUU5+bjK;gim@!Ts.ioRP.B^F3ERJ"p#O3nB5>;J*sX%RO*8B[Hka47#sH))b2c,8gCggg;C>J?D]_"W#C"h=U17=p0EIo2_`Gm^"kpm!;504,+78Yk>;HK'?t+'X`Z0@DtX6bU[l)Y@Zt_Ket@;&@&LousQ\dVaQmQKt:mDNpa+H^$$O"GPl:,sG(./T<$L.Ps4&IR5sIp![aS[%8q.ds>Z(RPsR'3Y(?%m1W6bqL"`)e&)Pjb^n=[;u:)&IDdY+('bbeH4Q9MZ<(:bZ!/S,k$'3Z%i$Nrcn_!BB577Q(AR+@24/%KWX;)FOTG&9dglJ-2U*!XangVL&"q;#nQTfIGW0."DCo1dp;X:]LZQ(`!A8JC&U%"]0_k;#i6g[dA5F$K._MjuFi?Pc-Nch]W\!5qso#_SLa6#N5[I@K9^iCjRGF39HeY)+K3)CE_D?$p[+pL>htTB]mFXIYZe^I?@bZVr,4e&_qFj@h>>#[HlGDUK*ibUG5gGZ5G'Inm4:lIYWKQ:T3_+L$S[l.H>oUl4:N[tP3nQm%3/kO[MhFhmEui#)F*!'?E])lMWU2DPLIe;Fb@rO[[^kmV.+>c9%D_"frLXUkh'-9*\6e]Q*Ejpb'ZKo//")$Psm5:dB\QD+t-O56W>*;AJbupJ2.Oh?pGS66nk8B"!SSa2>Ukm&VMokGTJ>Eh$LIF1#7aTUR65_,3*<>DkArq3V-%MGZLXEpN\!H9::ncQ04,ncG=$4S6"_6NEk=DOq1S@>c@@bHr)qI(g<0M'2j,n_\_F\VHa)G\(1=`:@@:UjpPb4bO_:t`"]H&lgqOSl:(F\N*`VMVEVab62Js&lTD10T_A(A>)r<_-@_uY(^&G)664%r9LIed96=:OWO:m*\N"*]R,N.P=ppFKU5C%fHV^J\h\Z_MaLaFDlZNK&\9cKaC;D>q/Cgc%)]Sol8F1j;aZ;C%g^0m9o]R3*S=&YP[9S"oT5R9sW91hHr#,/p)]HfF7"0CMgacBDh7Y*ZKEYGk_Ob3.0.r#''SWZEN\_gQOaPlRNIb&G*nG\\"n"Hh?[T9Fgf;(=I(n'5u=%FF_X!VkW!6#-iX3G_?PD~>endstream +endobj +12 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2076 +>> +stream +Gb!Snhc&8h&:WfGfU!OjB50!NZDDp.JC<;=],&gRAot>DfJVP^$Odep??DCP^g/KE/.V^FL;$Uu_uXLuitXLP\B]j)1r-.D+L,?QdTS$1EJ/0dIBN#U-&"H#e&J7g^FP3Y6JOH+n2nf=Apr6"Nb#12/P`+61X`b1D7aXJB_"j3,kH__^Nc]/5E']GJ[/Oh4&kjO[(^7OJe3`8\^m2,X?a:BZ#$i'$'nNRjUdnGipdSVut]uRV">m8+;a4oB3'ECF&DPh&U9'Y)sheT'.O@c(%fV*s/a13%!2DA0XY\cb_A-a-`XX?M5T/J7_:C>:,26b2/[o,cTbQLMLGOeVDn.C6@GB:i"rR-+-9oek-gtWC>e_ZNkH-->j\m8s2/C'!NcB.?f"aO)Qt(0-o3XjXR#^d-pc$rQ%=/np`T"8!X8Wj98a,-;`43+5n"8;H@_DTkm\O]4n5CXLiSe6W_h(-Eh$%RjPUVi_:)iRI$fmY"VPCC/-U6W4C:IT]SKQ^P$6W(QX8.Q]5aog_0j^[CpBUG/s+\)JXWPW3(TP$9mP?bJr!0AVJVT?_2sBhi^;eRRHN`KN!%ZQE^U#*R9>9OLH;*?*VC);6Yu]F)m_ki[M4lF/lS40V2u%pCsEbrVI;jH$8](SRkmZ\2a$$)hE+\4RI.s.9gi=;g%Lca"NL.>d!APGL&21'0\S0Z%^*?c8lBZPBJu^1,r!C5@u@VoL,3Ab1GOU(0MOXS;^.-G>5;[!HA#iQorV99cO:_D%E&n]RkLDGZ7g8HJ$nViXj]egf'1nlmW*\rt$EO3V&;p9J1`@':#]0M,OGcln6=H\WlLk1D;C(\UTqlnW9`bjdb@Yp8)1_b`oU:_F"9(8Xr';3%CqqhH@mT4;ZJ6"a.,q7\.)LWEIMB<".EO"(pWW8`hqNm5b/mFN9V^O!N),dNCNNo7r?K#\-DaV\`&j)4so6)H(7GF[\C(6+(pNMDYY)^;sD$:$c2Ztnhh6_s7a;EH+s_[X"`LZ=T[qKt6'gNa-71AO"lVO6eg$Plb71po8rD!NU=jL#\"j;S'p9&]lma3F$mBWY6SQi0B*V"6Aet=[-U@57)"Bp-\rpk6;+rMELLs()+rt:+tbU6/8#3=!<+RaY[,uf:67:0tlqS'o?P)8=7tc6r0q`2UH6QFMW*F6W:4f+>"LskDh1fB^4XnUh-<5=/oB:gqISUm@=t;DG,mZ`ek*9*;p*i]Lu]~>endstream +endobj +13 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2182 +>> +stream +Gau`XD/\/e&H;*)_/DsS<9:a5IV[OO?l6,Uo>^ibS#EFsbAM/#>Q=XmaW`0&Z7,,(,t1_oW-aQFHg'LX1dTcAPPXHJM9r:\HN'3Q6IGWc,!=n!rB#>3?N!ugF&,@`WMq3mQR!eZC@dr-B*=\.5oHFcQp51fd[U\er3A(=M1ZCR>oF!+#cf,5"h9[4HF_r"I\$mG($52-Z.SH$-YWGfXe/elpZ?hD"Mr@_S-]8qD>O/gC+V=O,^f^B4?*8M]3VepF4pX-p4HH1S=bY,ePK@b&dYC5;Ul*%]1/d!hplfAUT&-i&HS9%#OIG:dq>MgD#:P%/dX2TON&GU1(a5Z'&3\OVB]ED[i?L/r!Q1NPoE*(*/EDmTc[k5'(]"R'%h`cU8(l6/S*/3$LL]_P,iP"@PPA!foE9SHAI:nQcSeWD75kRJpnTkcr%5Sr=o;l,tOZ!u!1E2"BR>K19F@2k7Pt,XVCEIR3,kR&8m/f806Vhj5(X0O,ce$ADS^uTLKZ%W&C2fshbu>+3CJ$B'*,0VDW!C@#[69b,,OK>1Qcu+oSj9i"r[q"(jSk6>gr+()aJ21eFu/gul=cXJ*0Z*!/PA;k<280l*+]iUI$+-3g\uXZGTibm?'1R-1]+hC):tI<9F8+#H_'bhWb/\-L,E>6qOAmn.i3Jsk?:P.2q'p_^!/37of%s',S.l!aL"p1:kReQfjnOoM-,/>_p"9$.u.dCh+](Aqaeti!9SrBbqO?of,q9n,1FJ]1'CPO%.e,]6/E1NK&kD98nn?kdi=lp^gK4$9?bCCgJJRfCj]3BTJ/Y0/^OK#;Zf%[_>rD\GotANbci>ma<0l"24FAAM';T;K`_[2cb>VJYjklhliV5e.trdf*sbsWq`o[@fU!GrZO[1PML?UYLmi`IL4+.%J:c[b0<"t7JpeaO2s_KbA+\FZ!Z&[;QtfY)q[s!Vp433Ls0ou*)Q,dp8'WSI/S'OK5ijiG\:lDI5#%@m"5=r?sdXka$(?5.J?=A`]b0S^:A2J_=-'DdK!#efE^DT?Eu!QJUk_D%g4;8I>NM;q`o\/fLI)CQSnZ)Lg]+F32#4(lq;*/\>MLe5TDCK*0YZbJAn`'"=T"QM1Ied5_(367l4>u7??Igpg3od/;ARE:8n]-0/8Q$*sg4-q\W;_L#_]tLM,LL32#4(lq:skH#_d`:X;39LW?\2">#:U+@%[J9+9n7#6Ha$4q(G6i?Y/[3?&GVpc\M;)A'1BK5nD[g\1`"7Bd:*.#*/,JNWfpJ0rnrTo(Im7$KgsU&ft^j'G@U$fcH\"7`fJ8^'12&?H/sJ`&rc/hrB]*=0s?]BiA5(/?)HSTE2/U>EZ*Se=9MGT"m(QDi8Df/hc/Xj\lR3o"b0.LT48jjLD7IiS<7M0XSW3ZU>B3)^#[?&:VgH)96;Pq":cWiIDHWa*%pAadNiQ\nHmlKXW&3.,iu<+KpnKHel]7J>/'+L4]h'!rl=9df44G[eLC.spS.]F1RK)>:@/-kbAFH_Vl$;/_3&a:d@fdb]ojf?W_/Q-PXKDhd[^TtM&D@tP1Pd<.imJH<3+Ldu'@Hh%?gP2%NX;W-`\SD=g5oKgTo!T.O[;I2'QFDiS5'f@D2hl:n++6D/n";G`G50HQHMr+3idendstream +endobj +14 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 317 +>> +stream +GarVIb>,r/&A70Vk#aU1DUKd;7k3FpM2-J['NqRX>h-)p42C#h-bocAQ$07bfpT5SlP)0gBn/:*Rg0"[i().KD8:A*kJa9jDe;qRVCO_p>M"hFVFu!0c0?c#r'`ga8X"U?W!,,d7lsq>fjOJ&S?kCgfN0qi3Vf3'KSQo]J^D5n&6#r*7"(I;%LVJWH6K&l@rDV*Hh"pIRJ%^$C%Y]r]p_S%CL8tOSnBir'!.j0\-5klfFYF\!Ded2Ksjq=$N($UNKpi6QYMIg:a'c9k$=en8Jt)Z6Ai+>cFFi9hh6!m6h668aJ;U[8V,k/[EELd~>endstream +endobj +xref +0 15 +0000000000 65535 f +0000000073 00000 n +0000000114 00000 n +0000000221 00000 n +0000000333 00000 n +0000000538 00000 n +0000000743 00000 n +0000000948 00000 n +0000001153 00000 n +0000001222 00000 n +0000001527 00000 n +0000001605 00000 n +0000004243 00000 n +0000006411 00000 n +0000008685 00000 n +trailer +<< +/ID +[<5b68c87b2bd1edda18069a726c174248><5b68c87b2bd1edda18069a726c174248>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 9 0 R +/Root 8 0 R +/Size 15 +>> +startxref +9093 +%%EOF diff --git a/output/extraction_ogc.xlsx b/output/extraction_ogc.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..006aed30c3c1b34fdcf1c9193d607e37e583a8e3 GIT binary patch literal 32397 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%0AtM7xQbqsP}tXb}6lENzP^aXeC}> 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 +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +5 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/BaseFont /Symbol /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +7 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/PageMode /UseNone /Pages 15 0 R /Type /Catalog +>> +endobj +14 0 obj +<< +/Author (EttaSant\351 / T2A) /CreationDate (D:20260416195931+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260416195931+02'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (Rapport extraction OGC \204 Qwen3-VL 235B) /Trapped /False +>> +endobj +15 0 obj +<< +/Count 8 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Type /Pages +>> +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1889 +>> +stream +Gb!

uTK;'Sc)J.s/8-"%HX`-182uA\(ODKr2OmB#dUS9da9nL:7,2O6lX/Y)UC,l*.rKaKf-c3dE":7#Xt;A_HBo:<9IJ3I\jOj(*7+Qjb5XSWi#GE'%I^3u&9jiJ*IcK`U:%6+as%lO?aV2r"lE7[[,4"G3sV)%oKZ,8l[;blBe9V%;d8%N[Uge9^'.3admM`^g4)UBrrjN*!(A3[KtSOrrpL*iIKKK"O<0I6g?[aQOp1Epj85(EF9:=LT]a+4>OT>%G04#SYCu['ch6.S>WfQBNg2dTJn3aMmma+c\aY'/%.'jQbY]/)SZJT`!nb4BpZ:Y$TVjf7gfL/)u:Agj3kN2ndDO]<85V9`o5V]sW:n`nA5CXR-CH0ed`DjY]CffM\:mdRqEnUR4Z1lZXV.n$o1dd&!H4h#K%`3,8(j8i'2J<;D;fJfV*9j[+$WKg(B,+$+YKkXajTj"5]_+Z`U/[]VcL[$GXPC9HfW+EfrrUSd4h[)FbHO@;>'.qemZ-UDB0D8ON8lWe'M(CcrjsUu(/K0FNSNTg!^"bX>PJVl;Ha[no94]`l*mOb!7;'NHPkN&Q.Za+9Wa+,BB,^7$A4#$,#$j?%=4NJAPRbeD;b"`-gY$!G(a\kdb_1Pc;oRCUS*1.Jte!_r5MaM\,\fijMdbc?/q/_ZGjbg_LWpG/l,=M(CsaC[*u.OZ'42FXTFfj\(fR7Y(!MIdNu+8%*3Q;u_F@>dR>EEnmR7-&TK.LX[E+=dP!\msPs#c[A*c?^lH749e"&m\q?JEa<0HPrMZ;86lfbQIFid[J0SqnQGQ?Np(7_CG.JAZq"C5.[>'$1=2fLT7JAS"r&IR]j#d$+DE@75;rtFek&'BcU68Y\G2VUFRB,e97,)*NKU!WTa')PU-;#q=:N;MK^8>UVLigIIpqoB^$YoYS?X5?!H'`6RGdJ)!+QC@;Q8&h5-D<2o_c=eeen'NEEYR(Hdd_U\.,A=&i;Jh2Pr%LiaoIh0_oVoIfNV-pI\S9HHU*n'=bf);.\dH@Q9s8#^"M593N1md]F6Z]4h\4#V/E/@Qfi"E$U_.CCPoAgNWaUVQ*M<6R&*aI^sICR3Eqf%Bjp>N;Yq3c8[Ugs]_@Io?<;L!/;e+_f6VsM;I:>(EG?KF[taaW33mtFC[,s-';;7kP@9-&=tM:;4X&uY*6,u3-Kb?j:C\m?R0_/lUFBoe,IZ,Fb2:l,kZJ.a6r_.9b=o3I\)HC&eI0fkY+LT*e?.ugfL)RY:O47;F8\mWf`&r?l*tnXq?>_Jp\*")oEU?e.YKVujdendstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1999 +>> +stream +Gb!#^?!$&E&:N_CbY(ab4k+-jfp3_hO9Jfq=U=oC3?45"RQ\T_:8$?k?ce>CJfQ?T]]CDdaW/!()YV#gh8$%4sgqi;ZFI$SmVYGk,!YU;MPp!ndH/FAM$ZL"khHd#jif)oQ"fgcA%2nKHUZ.#I)>m8p[Ec_9onH037_sjE%rMi(.Yk"?8,:DNppHb]1eh(;()>LMF]]TNJ8iKQ0,$_QbghU!S=PtU[3>a":nFH7+Wfd"&^m?'5M$]h(a>BhD&fYSjpJ`!@q&flYnOco-o6+94Ecn50XKPTP,,[:^qm)"1Cc:IPdgkDD]pEe@,I.gB;%%6=m"(j%2_kR3eg)\,<1C^L?h*7WW!6H/AnbgB=8?7>32H^VbgHVp^M%[3;;knrK8h#;H8rB-\?6q#r]:;E-p':6nMdjfeAD#tHC#=_<-7R79'uTLrJaBr4ukSDgb)[PmX>dP_"RPs^_IgK%8I.@KCe'PEbY")9P@ofB-%US3K6#-r3dWYi(RforA\^a)/Ud?hF&UK2'L;I>I@#@\mekb,[!X!ADJETY=1@A(C""MTA.CQ1oB/NrDa(u'7d(][(r=9)KP8(j3CW(A=]uqh&or%hCWP/`YHDBrk+:.dS,:`]5])'G;U@`qP(#=[Xfp0X;:=DtOnM?1Raf&e7--+R/f">S=mF_gQ0Uss$]4)6dIc[CdZ1MtV.Jkgi=Eih+'OH,Onn=5ec'*3H_gA]Dp2tdjljKH+'K%fc3jtrBZR@i&c*Y9.3jr^A&$BePGC)I046UU\*n?0iFr#RO/bh2LrcXDD]p*[9#.p_NDjtHSDH,YVlI$RamR+XZT<'.e_O?n%JU"li.TGIm`;BA@$IZfXVpT1DN;!k-dQ%ieJq\rtm(u(V,'"bDd\puW+Z4,IJk8(-9Z"hfl"EdnCs\#!9+Uo@,*QlPkOaubHjShehCcaA-s:W,>j0eo!R(@#?hke(=/]9F(#2SoRqYB.^d9qg_U='Q6mgZZMNL6^b@D*<-YKmAd9!R_?tVpEJ?rWRgT]hE6Oo^AGl#T?`c$jh93Xr1;W3`FaW,nn6W&(B4_b+;-pZdiQP9bR[/h`HHE'MF64gjI,dbao)6Z16,Fb$%_!0C(FnG~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1645 +>> +stream +Gb"/)9lo#Z&A@sBoPNtp,3DO*LE'hJgMDGFL"P)](Br!B>Tb"3SG`;U&KhW8#Oc?Y/T509_pG1Hf2"1scRU[lrkQMQkoAV:K[(;:9H\]_bVTN4+2%+0gDSkl1l,br`5Z1'=MqRUoBWnh_h\gWBTunq^Srr?QqW:siu.TJb:ku1Euoo5VF:b`"?>!jE#340V^mRQjNAsn@7#bVd;usj:hHlg3Ej;u,0Q-oGlck;KHC>u48u12`ooTC#4(/6/8U:LK2k/["Vg]H1k3eY3h&qdB3R>54>0`.R!t%/Xl:oD%Jk&K0@;=W/c!U)P!#0Z@RA(uJRps'B%'c=LsH_Pn76Y53/_2thTWM@R%.O@N8PjNUY4]pHUD\8'sC#Cob0#.-bTDlORg,Y4YjV76t(?-!nFDSdHSXt3&B6:E8)X!j$@P2?mZ+e>DV!$"A_H[b(*VsZRjj_:OFTB@sIdaSVkI2%Q88J6[UQuMi5?HH[@mVV!!al9?lWYTi)oOX>!=Zhbor*]&iimM[_r+^1%G(bF7/-`T@o6Y[k&%]M9Ja9g)6iR-X,@V4M!S*0M?I:N*`1D3ZCeTg.$A(>VFnio['F6a$ZsoX=h:NRE(BQ>"="<8D>>upWk:q]$N8bq!;]6q_(ck'![f4(e3"3SP8`AN;(>(2QT9.He_iM`ccAESCT+V:Eb>B+82<^(:*VQ2m6/R5X][<\@WT8l';B,l*30HW?:^VaiH@FKcqRf(QK:]92(a;s8YAXUk1OO!.ri.\;E^2=%0dRe@_hP:V(G&Q>"Y[M;?)Sr9%RiYj?B]E^;)TT:QE.aKl!eqqkn`*(<",5d+p[0V#b%u,8ejR1,Y?E0:SLuj2WOidhCuN?GnV_s0+gFk9c*%N?KR75X;h_h)hgm"e]fP.T3/SCfZ?Q\6KG@!?6)G:#8S8%RUjMaD14='!AKRt1f*-'o4"!S\t8L_r,]G?+^4!N=M.PBAu'2-99MTM2d!8Q`_/`)_Yre#Nog<1)%H!EIuGHP?.N]])Iq*W%0IgmWnhZ"F]edUZ>tMIC3S8B&N&V284C&]<$b.PSfF1ncB-["Ddg:tRi@Ad*gtK:l9WkW8dO&teTGY=@CRkN-]s6j/0`Y\!Or2'n7H-PFT$Be:hCdTAY^'`i-[p%*C8R%Bb1KoGi3E"ZD%:17ZK]bgV2/.T]^Cf-g.cs4@K$=C1j6Z?eftDePP:dU$K6J]jMh)i4*[.lhR7+kF@,KW#Q%/>k>KToKIlq3j$D&!^VR!R*aKibttIGD)=.-5dhehg/MXGC,NI"OiF8C_n9oI:%:\uru.Bde^8,JXiAq,CqlL[L@#XgD!']C'P]J5'#e'3[T6BhSZ3sg`@=lQ,Vbfi+<#Y'auk6P,hE*uPEb.fo3-B"77jWo<[]B2*mTldcisrW;#Qp!`~>endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1482 +>> +stream +Gb"/ihfGM_&BE],/,JXV[q]o/(^B,2+'^)u=&BW_qlo1?kE-H-`B#`#pZLEBrWj?G2T'(o]HbFXTp_eVcAuj,,m'2^bl>tT!1tM"?6CAEkT5gg%&.P,UWsr%gg0/`M[M!G\;M+0/n@1\[Y.9o-9rV4F&BI8Ko]jbr10p(?6)#gd,ukK"PFEcgasHFSu1(me[gM5a440up2-+5@(o7rnTn\&'gjVaQQmC__\TgmgaD?&!Rl7.\PC5`,+cMe.YqC^L1^`[SKRLJ@\.,P.W)2;?3&/OP&?FBrNplCjNb;=4fprFWt]/ARrth31HB*T2R@p)Fk9pDT?1qR>SNLsa=NYSr4PNH\5Rs^B,q""p;A\lY8*8rc6+6IkR?7j]g"IBUeh%_LlYualh=iiumVVVs+WMEti,0'Wm:HFUQS(rG9,AkZ+Y*P^U*EH;N)Q''0?/fca,!@f`KdgQZ6Wbopp(F=CKHJG;;K1['m11t.uT`l*C@$:[;]B**64121DTAc][2N9!_8!j0^7'X_d-YJqfN$'sddf\%Us+B*RZ;'rcldca+cSM$).m*$W6TR1+6lr33=SY=gN#jihu4Ul._SmF$uLnP3#nR$97))VM8$-^g+VNeZRaY:M@8W-j>.:[bI)Ad$m9cl;eu^k^b_^CS6=[0=rg3"C;ergcW;W93]un=YnK=n;*]Yu5n!+:aN3)$7//EUD7H(Y%.kPqMBI^+q!l?51m1,#P)E_K?9Edc.,'"C.iAEV:0Z\iJ@W&;9^#>Zis@R(KLX;U*G@NMC'lN+]66G_CGV]1?%d2.#XG%P\M75XRpd;o3e;h0N@W`g"9WaPAJJ:hM@/:a/)H5)agN7@kAX8fiZG;IKCH5CeqQIbJkrGg"8)"F':J&Z1]%[$N#c/;&JAAlm<]BIP;[ioLF@TkstQL*0(8QkdSo4m8U7O6=^NW#_LF4?nd.iET#$0^_c=C4?JG;hN`S3*8gc7T95gN[9`U0a@2$5pA!u)TjMkg->8=`>'tMkr>VM)jh1,O@56%LT,HKAc5iN+B(c:uXe([kT=+Hq`Ffq\9GVYeK?o3TO3b8laS22H_Ir%a?hAM^U\$u.KK"W=rsJIo'a/PooR_"3?N+C&U31h&CTu"J;UNU7F.70>mjB*Q2K>/$5/YbQU~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1477 +>> +stream +Gb"/)d;GFE'Rf^Wgp_-q(,H%$bX3GM9c[8B'#7CFq8MYG&Wq^R`pN3WmfhG_>dC`S<*EAA8auL1G7KBi1Hn;?;"6uss6CJO%l'#-&g.nJ7j,C;6gT@;O8^&[j-^*M(oJA2E39bt3;17o_G+e/*:V$kPMjhd2,7TS1s1])Q3t2l'[,JO]I+K]&pV(*UrDYA%\mn(FCj%rm`^oPA6,hXbe3VS3YIpaf@e5\ZsH^%?ENlMRnU1qP!U&cQuI>h:1Z^fYf_9bhc']q]4N$55_m8GTbj[X$Z"t&XK#He2q*+UC'N@pr_VH&,61jO4W@d4;16l/>.#QahZZQn'&hjH)ZcZ[4sXZNfd93AkedS1m#hE?^6Vb]SHUa%g7;_0ngB3X:@e-FJ#]oIG#/6+F1g@6H`]O_WpG:gV\?,XcqSTp\HCp6h3Xm";fSo*n+r5J`W@AeuJSR8X=;l&gE'a.<6<"646pVA(PFdDR7&6L]Ma'V:e%,ZJ/0bXREW*G.4^PZJQt;15eS7;>lsG0Qm=]_=XlBaieBQ`:*Kq4Ar7*+W&1<2GuC,]DS\1_@%?:b6B0gV#&qQ"KKI%Xu6CFO,HqCktt!CH3g!?#^&j)KZu!"Ei'npAedYW;@d4-']KQ9r!^GY?nfnQ-lO&iSfKdiZ;.AI=MU&0")rFTu4]DU(Mm&Jd2UBi_#Jk5.CX;)Xdir]fm0D](e[e0t,sif?GDjgL`-#/DG.HGQgb,bA1:PHoB'+8Vh3#6RDDr3;MsHD9m;fh$[p<6_g)dQ]2C1[/Vh:.GPNI/]P%=IhS!FV?MY4-P,HHFHX2f4aRsmcOpN#-Y$]K>C0a>ZbiC74Mje-S1csKoK.V71Z/$1r-Rt(9HAL)K,2P/8\2Cd.*Nn(PfiMH8NVjdJ9NNl+Wts=2/57qD(gOD`2&ff,_);,)\n5Jlbpi]/]5Z$E_)3_bI8[i?d3h/G3h[eK$+amGXRaZ$_)qP^F\MIdWB3SCaS=\a*D%u6'u.5endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1575 +>> +stream +Gb"/i?$#9h&;KZL'mnQ&1Tg2iZ>%Yq+j8_3aOKpsk%F+e':+V93W;FiZ?FZ\fe#(kkio[Q6_$Pm\'((VuS-^J2j`6Tn9"*@TfQk")dR2Yd!Pc6SkdD3&EFke)*>F#.u_6F),FNS2)l^lit$82rnC'!f@eCm:@4-HF;@HaGKbOA4$4tYlDD^A=^ac&\)4m09q_)`Pf?RJ2L8KRm2I`/d-t&mEIGl(BGK^km%W`bsE')LBQRI:)I_JdBmH62&\m4=Z2KOSZM1(j/d-4l?o;-Ga=PH@=j?KWYa-IBVXP!DF03aGAJ!;eMGuqN@jqE\S?#I'f2%I6`'.]un0Ti4UeLoB']?hOO]/Z7$<4[1XljC8MV4(s32nO=2M]%k]Rq7q%.+HLYQS\ifS\Dn$([TZIL&uiP^batsQ1/DXD/co^f?'^4P`;fO2g[B^6top&7m^qH@?ee0\"GSH0T0@1JhaQs`gBjXX%Do;l.;jNck#N\jTH@S5"WLXnS/lgF78BFV3lGTNZbG@=RU?Lb*\5[b5$XB\eO[]]_I&[_qBd+E4Y7O-47ln!V,!!4,;L?g(XG68S_[46#R(Zs(Ws(nq?#Eu2!.)(+UP(QH#Jk(TdC3Z)F*B(M9:=qtW](srJ^Sd/tm-2jo,kc:iM2XU3Tg-VnLD=]!@%V2ifR=;Wc8"8$a[6V)kP%Z%?QJgJ[pO@>E#Z.LWOS>mc/Q*lmr0h3f[Z^,uWiL:0[l[JpqsBHC.3efhA_P^gFdWH9^Ap#5pLj~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1164 +>> +stream +Gb"/'9lldX&A@sBm&a![_-.ub3okgJPIM8fNK%Rsn:eG!6RKfO+DlUW([q[>1i-kRus!nUuD`tB"!7_.[ik96eU6-$Ks*rNHBB3WAl0t]pRs8*/&s!j!Y.-d=EQr]6BJq^G\G1d59oT?;hoi9>+&F\YL;d%du6Ri.-GY:r%Q(5_kJ@7/-7%oH)=Qg_e\IBFRdO!p+MM"E$]M00=`U4hc`CX9#oWSlMW^B*4l@:iA&o\_bd5l0gJf]GB&B.cd`b.5+&ab]srKNg6X=dQ0X#FbNA9&Y.S/lta47A9ro\QgLh%N_/gs$2&E4i>rLEH%OVp'_VK(NkPMW[rga0VaY6:!3?WNEqq%qq8-A7Pp1(qH4,$CE&ob=U@o?)k^YB#uIZ2TQEf'EWj"ok"3%o:VQWsq*kji@o_Uj))\?tRiF$F1kaquY^RabQXk/&X20M-n+XNOAON0U!I@PN?>Nn\G_"p*La$d*]9D3#\"gVZiF'^G/AB3St@rsX3endstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1031 +>> +stream +GatU2gN(as&:O"Km.@,''P#7O`%\hX:bKY^cobp;UWQ1Q7^cQ-9_j$98n'2_+X,m6N9sp>ffeLh$@rmE&q>0/n8\k3cNt-idlELPa^P#H?;HW"NlX8fUe+_:MSQ_I$)u&S^Vb"W;LBUKA<[f'1'c,cY-?a5N,63HA=\j19'h,p3D,hV*i,9K3ZJm.+sLFjtC&3Dj%HW]2dh;N#(O2jE1I6H;8%h('8(mR\oT>in`sCh]f?AXcmhJR?P2cpS/Bu$g9q14\LW9B,H\@hHta2[.!2%>1$TU!;u\Z$akV1A.n>Pe?5n$+lNt!8@?3WfU;uSBXJ-N`;;,$O"Wl]S+['OPGP^YF:A,Zig/o-;o-LL*2@&r8onZJlRb'DTlS#dn[[^R!DTLjUF%IJ_0*qTENr.2Z:E:rIn&[I6dho;1+RlA(OPI28f5.T!Wtj]DaqoARH/Yi(Ij>)ODm1S?NV5T[3!sl@GoDb9:3ngWGQ[X`*"@si/QhCc3ECMJ;bmWq#^4,s5oHN/8<]Z(5cNB7@%dCT/)"#4=\>j*R0K'@=576^Zlg?14!bC.*1#W#V4?X'Gr/s/<`X:0bZgK#dOXF]1([YqIM'"AK]=[r8NN9%:lYB/eNYudKfmu2q6uWt:s'E!En)8(J944_T'J_"YU9.SI+:Qa@=s#n>oWYsP7d_sF5[:&NpZj=8a1H?ud~>endstream +endobj +xref +0 24 +0000000000 65535 f +0000000073 00000 n +0000000124 00000 n +0000000231 00000 n +0000000343 00000 n +0000000548 00000 n +0000000753 00000 n +0000000830 00000 n +0000001035 00000 n +0000001240 00000 n +0000001445 00000 n +0000001651 00000 n +0000001857 00000 n +0000002063 00000 n +0000002133 00000 n +0000002450 00000 n +0000002555 00000 n +0000004536 00000 n +0000006627 00000 n +0000008364 00000 n +0000009938 00000 n +0000011507 00000 n +0000013174 00000 n +0000014430 00000 n +trailer +<< +/ID +[<8b5ac18869754bc73be1f4958d9d786f><8b5ac18869754bc73be1f4958d9d786f>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 14 0 R +/Root 13 0 R +/Size 24 +>> +startxref +15553 +%%EOF diff --git a/output/rapport_timing.pdf b/output/rapport_timing.pdf new file mode 100644 index 0000000..dc9cace --- /dev/null +++ b/output/rapport_timing.pdf @@ -0,0 +1,213 @@ +%PDF-1.4 +% ReportLab Generated PDF document http://www.reportlab.com +1 0 obj +<< +/F1 2 0 R /F2 3 0 R /F3 4 0 R +>> +endobj +2 0 obj +<< +/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font +>> +endobj +3 0 obj +<< +/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font +>> +endobj +4 0 obj +<< +/BaseFont /Helvetica-BoldOblique /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font +>> +endobj +5 0 obj +<< +/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +6 0 obj +<< +/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +7 0 obj +<< +/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +8 0 obj +<< +/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +9 0 obj +<< +/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +10 0 obj +<< +/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +11 0 obj +<< +/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +12 0 obj +<< +/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 15 0 R /Resources << +/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ] +>> /Rotate 0 /Trans << + +>> + /Type /Page +>> +endobj +13 0 obj +<< +/PageMode /UseNone /Pages 15 0 R /Type /Catalog +>> +endobj +14 0 obj +<< +/Author (\(anonymous\)) /CreationDate (D:20260421112058+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260421112058+02'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (\(anonymous\)) /Trapped /False +>> +endobj +15 0 obj +<< +/Count 8 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R ] /Type /Pages +>> +endobj +16 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2001 +>> +stream +Gb!#]gNM:1'Rf-po[^Bb`K?0u2K-:U:-),''%3\Z`?N.?d(quW9W*A)OtFiiYq4gI_?s[m[*RU?SW+cuWs:)G*t!\XNR5X7JdA$s,9e[IB_2H;7_Rt+0p)MMO[2Huj>%mHaAbCSN6CAV;bnPJ$`^cS$4mb?2'@F(,EMmudo<`9VW_hU=fn"U/XjDB/10uGRiG.D[*=NZ#2K;No_^4Hd.;bLIA$N;r*d=p9ja[C:GdBMc@%+[o=K=UEMZg&/$6;oPa0rqWb7sl-moE$[+8#dSg,?oQsB[rJaOpbI7YN"T;OQG/f*5N6-q:Xs1+J8X?InCs)n+9[jVgq`'foeBr5Ied7k/n+A$iN2101iaKQ*ZN<6RF7a#42b@)6&nXEf]Nks1lGs@]%j[te+#f7.n_F^(*$(PQt9c,0UU-q3Jf*&HSP,d'LBu1aakSt;A*'M%#,oOl[+TJ;+,$MUWeZ0a6$p,LG%Y2Mg_m\n&:ttYbJ[VWN5U1$d%1hlqXa%mQ%qk9_5]>qnpcp\L[AogLa=bej=a17-3B&.9(t(UbMW,)Oe[h\fHPT7Io^M(bTVQuoUnVcR*I5j_OJXcXOh47Qmp*d;f"MR&?)"lGOso`3O&uJFEb0bCi_G*'l"W"N[0-GJuX)qQE+$"T'fFh5a1Rn?`4Yg>6NfU]/$#%W^+qWY0)iFtJD>d;&X-;4AD526sFq*u8,*pP7)E#=+Z?'S@TOUga#W"ed7PW*+=T+@m21:G8FQL?-X@X`BPA4Ck^",VM)4%/l.(*oGe16G8-AF&oNm--)aNE%%O`@G)#]eKGe,u#=WZnsc;aAhU/e!r_#^C+$I0B:W>Bl/hs\#Q8Z9g^YVT+=84fVrcu/_#ZZAsCE1b38JUP%G0$89OS<("q8Mj+bT*C*8iQ'>Bm^^:JS4(IaAHes#j%.Pf>WDW331^"'HQWIo,`YMn\bUVbVP&H^u-A<:a,$cha@WUS.JeMA&[n%,+K"W=:iP%[UrqocE>;9q_@RU/TSp&2;?/:]dTrojL1@IM6^#STeJq%@60kg?hiUC:h+K5hitO(MLFgg9\;[+m,S^l5O#:M_eYtP,'-@#00'aQ.#RK*R?p9LFnfs=/r7'/?3F;mVcA5JjEP[`m!\_0`dSf?J"/(?QnR5?TC1(6)1j5p/"\#rU&P69D:AB7n)8#PEQ-&S3l)>**Ye>[:ZsaZ-MbCLsqUBTZG+2A-?<\cHQ"g#.m]Ag0WZUUA0pR&mmJ^'B.ucg#WdSZC>Jj05#+f%Pg`;Ra'?JH9Q\^UIDUb\WRZlALBU8Qe&l]"*:I["?B!fdb6MIF5E#cDinQmIm8l&"5L-'2dJtOI;:V+a(SAFg&:PV(h8q4+8VH^&J';+F9F~>endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1684 +>> +stream +Gb"/(>uTK;'Re<2cm.odWkN'97YBs&$_\$,:X!@T1be/,@"Xm8rqhtqY%>YZ@deL(lt/#7R!WpsYB4WP*%3#n7C1l1CP:QbkhR[VPbIjf;1Sn.cVP=:rYX#aW>JYZ$RsLr@N)8nN&+n*Kf=3#_a&d;UNgV]3MJ>X9^*$>5)^)?!IjlkqZb$s;#E5Bij3o1dqF=8V9Aq=KJa2K[1\q&k.@&V+WV@'84\c;i#Ecc!,\%cp8oK4?R7$AD&=A$@X_o4L+kFFB-sa'H'lM#6>ONN[42]jCbt&6b#;],[el2_V,>9*jEQCgRjG9c[.[tP9\X8[WGtqEOK@20&f@/[=Lsg`d8=D3Rb9gMZ,t]5B`VjgR8d,r:9DR?r$(a9c%R)K4QHqu(>7BL,/?,jOC$DA]Q)fc5Mfpf+BUU6mXh=ngj.'_-]-OO-+\A)^YJ"uKRl_UI)K@(oDno)I_Le\^pGdkN9:$(_[(>U*KKH.oup2bn'P\^,'JLdr_+l9"`Y*Ma1WK#.^J\?[mT9j6KT]*6fo"Jk.J,ijq:"V?4X$`#]!Rr<>)m/KtkT!+kjgPNET>rp.Z,VEoa#A/9@:E<*h]7WWBlNEa:Ea*CMl8\\DI::)n>10Gk[2-AIp[!O5g?-lEo1'$fc1B&Sgc_Br01cal.Nf'>jH^4Z);E1=G3^=d:GNoqtXl"cdqe"Q3k`n`":A5&kW`o%DFr"&YRI#lDf6N]/OQnS$jU!k<"!NV!GJCB2!Y!"LdX.W`\s2ZRq)lO0pF92ncC:Yl~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1684 +>> +stream +Gb"/)>uTcA'Re<2cm-.YBVd6$Z?Tl3fk/FJ2OJo%-Bk`daVFsfB%m7]b$U=&aB%X/jWTfga1b&FpKktid@!g!oS;T/Y7lRQa6:u,72/RGK.+b[`@!'k7t6P4M3J-u5sRGnS7/E!V;#]?L):'GV3K5sYPR_N#RdBeV.ffgrA)4urKY\5Js'Tsgn>,S^s%c(\[aG,dX4g6@uYHfo`V<5oNg,\PdSa6jreode-U63-ZHfnbULe3IGJ)&PN6*hN:PR&JuBdaj.4cFc92Pr$gBA*M4[j&\4hD"r-+lb5u-Z-WR[gQm,Y*QWVR3P&[Orr/EG=/0SuBPe1SFcqn[ir]Piqh4EjcAESqIR(bmGMTOfjWRuPQhQ-L.%@Do&KVM70+@eok9YC[AOTapoHBd_/I$o.\mgNS:]5lQq*M=[D19*A.O3U8YZ1t*US(ug<7JKdr11"Qb>`u'g,o?-lGK8S8rpVd*(nf!=Phn!'LOXS&u@8F'n@jt<4kaCFT=WApZ_6\Gs+ri%tj"Xl&I)9kVjRb>)OUIO6`JoPcc.flf>qs"#"7aZs#&Q=+c7#oK[_F$cU'[I.qTN:BM)A8Dg85IXN#2jtKO&^b=(-F7O][X9eu]./]2T2j\=C)2?=GgNB''WR+/i$MJ+`DYdFYP\p.(6>)j#H[=MDDiI>,j`Qd$2(!!5a6_BE82D>s_bc=@PRTr;@ZWJNd>1'W_:02B.)md8T@HY9UK2ugQ,5hc>59HTAS(V_5Dk\r9?$%`cBib=0n098r#@CP5FAT-&>h^b,9[I\$A;QDIIY[`Xts`!]4qlq2j`h#lD@SkVf!^O.`Z*&.@5n:j0IOuYh_&tLJs*8D]=(b-`STO3^)[B'k/8Eih2GT^La[J6d)cX&.EhLW2fM`")4Ch6_nRXO;kq7a6/pq;J$V`&>*OD%&SDBD><-;mKBa:Z#fD,8[*)5J2G]J6kq`[#t4H:dM8bUeaLZYW7VFZqXEB%S@`4A8-SA8TYlA`>>k!e*-A7-Tff"T:LORjVp>bD'(pdY`jTTjCNiXdF\!5l0S'3m.+UJS]'?@R:bd'DBgfegg\bAeh_&ZV:PH4bsL0L]ltjU[8!i![Eb8i$`J1gA>u'W6+=Z(r`PRl0XPi3'LQYY98;$cedO'U/#Ql^@O;A!-$>.S*0u-,&.KN(RfEA]+koaKUd43ZlpSa^B_[(5LAfZ6f+8BO*&-&!N0b=iH,=a!iD)52Mh^Qqj3:"[Q9`bV\J#ErU>\j(ll_[`lp='6gtE#\--Q?q>;cn7XXfHKfZ$'8<5\a=ZU#+i%CT?k@$&G6]eq]m)2W37Kl)@U-FZ2'>Bu4A`NNBI?cNuZH-endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1680 +>> +stream +Gb"/(>>O9='RoMSn09_%*oAW`0dY%eQHO(No7U(cR=[ImJLs0oqsW-.*'7l*\1'`eD,m6'cKCC4HsE;i*uH-l_StEu"TY/nnECbl9L*n+0S9e0-QktcIh/bP8d(CjQiWm;`*-h1cBt;;)A1>D>M]kXbohuGitQN'.G2ZJ/8*o'UN)[9/4>X4Urt3";5[CcF_X0m&ZA7V5j[i.aG]:+G&kgoC3CGU3k$`W*r,faT-4*BM@bjce)l2$UZ$.5A9`P]9ZH=*3]]KpHlj@TiSdlf83$.70n'HSM565H9Ki$*&/iJcXrsQ3HL1S'ra@gV[H%;j[Q#2IdK)"aYNM84am%d;ji)1$9"WPn_rBo@kQ^93Ca;/5/WN9JF26i^3DIfIMoiGJEqqjr4?!Vr=EY1s<=@;&kF`?8[H0i%jXlX^"."l:nE=t92SkTC2-o>*7`DFjTVN%^noE+N]VIuPfQSs0'o?L%%DjOi9ri;tZ(,>QV0nL*C4:XB+@dbb$s)9+?NuUD?c??*^nTUgn%aXfiV'DO^:S>re@b6glScE/P%t8C<>j$+>;MYIb$aqtR_`6/#.'NE%-G]e9Pdjh&?6*S!`lCis4ge+G"*Zt3Tms'>rXY%XISM0ln5C8O5f8*RNPF?1EHfNU#Lsl/Zf1!+LM3uo=-58bB3T/!M/JHM%(:2!NM.*E8s2!44V^njnR0bXLYUtFJs=9TbpCYV=Qta&c7CO'9a.l@,uj\5pKjfb?KHUjCm2(NQC)9h>]Z_a7#-s##H8jS.,jU=@OEg%,">)N`]l2BSfoqD'c,3!o=@]+Cj"[`#IeHRGSB`q^l"]\bpT"g/`IWZ+.h[W'[4hqg=-R6rRBXCrY5i+DVUF/Wd0/rKG+[#tk1A'urijok[I3McnUBdii*%*h4g)8R_7HA!29m(rJX%`7=Y=@5bH4BE4eQ,B-0<#>I.qSc/-AB62UAgLqE;Gq15B"E$9nXEc\ptuE_k=CLsfX_C"?s+43qlQ0Q5QcZDCQ[!L?.%"2dg-+S?eS.j:Xg=$]'OMe\8U#ps.%$Dgu&V0POAO3g*"ZsQI;F3^ZS47H@"!tO<1Oc,S1upeGjO]J%fD\@QP72Yu+kTA@Rn8?Bn/k^j$T.~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1741 +>> +stream +Gb"/(hc&8h&:Vr4Th!fSFOo`D`>]8(0>.E5Qp*6ga\fsO7t:e"^&.7o[UiT:/LsT[%+AWuS!`%1G#p1uT>)!QkoWh&HaQ9*?&JW+:CmPk?6d<@6g5t_UndB'@QXg%Tg028-,gk"cuGhe:'X(d_tR#-RTDEmSV#Lq'W&u.UFHpDO`'XOP&c`3(UrU:IZlphK/Y4WL=3P"+_Ro^1;^:-KHWXtab:n4$34Lj"\=Y-'ucL`qcsd=!\pjYB9]-=&u/H]Y2>8D@OfelXcGLgMRu4I9'IGXUS=H\&qZdH%0`T!c*]f.Il$15!$j:;L8($k)&u"snA;h+H_9tiS,p0,!ct)hEh(VY*^%^nN\PKlHm`:-]6nV3\=2T=qn)8Ed$ml#2E-fdC^k>/YKmV^TbGP3YCOp6XDQGGVrT8`eDB%(:K>+>p[+,G)d^QR7aVgI[Q6k#s:#.(,jkL2arOoIf:t;o[Zb:&=Pd>*oL#$(bp9QV+ASIV6Jk<&VZq_0d7:k(EMsDN%_o=E%`q=l654tT9+E`N\XC'MRe\f03uYUr(oJ`#eL$O"A-NO>%1IlppK@\8C;E:)cIdB"9MODH\!pm3]Crghp^C>)CNOgB8$`m%\c0"3C$>=2'sth4fa\*Ik('>`lq[qQfh,SR4M>jgkf3aGb4HrY?`SFR+R..L=_uUOhb0W(j\KjtO@SnnFhrjUcCo`^-'O,VW333BXmJFr!WSf.WNCkli"4r'B-[f%.F=_J2m;h[fYKJk/eNl6q>+c?OSMEeq=H6pmPqZ;oVuTufWAIgCkQ6(eff#nI^^]*/JiHb@Sl`_P.PO\@al?eeY\G7+X&\l#g1gTl+W?R\Wbq@qpAV)I":Wd<\IHcegsq->b]>r@$sc"JT5L\W_NmViGV.M2O@_[WBZQua;_8`&81(I6LJ0`o`F7YuEt)5kU:Vc"Yf>ZFG>50tNBI^FAJ/&E;9c/;T(POc7UBh$@tcf?QX4Q$V6g`I=u$3r41SZD_cOtLGEWHQU5Z,jqLjl1c#@mOE0.#1UOnYI,;(U"KuRbrrSnj"((M!FD:gnSnuIH`N6V2'`jV@G(_kS<<)?kI3Jq6A#*kYfXEJ.?]_thbHqr\o+r*JIJmYM9]cp4"GCWB@n&[N:VGnS,jTVcS8LsgejY!=Ms2[,O7IY49saC`8*s5H4G?uf('gI+;=Ri(df&)D`h!-/Y[l=r%>"0l*NMNdHH&jpb/o~>endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1695 +>> +stream +Gb"/'gJStN&:O:Sk`MYJX]fr=7a*.D/=49`H(rIC-&-FU"#3/Sp[;1)dqG9!aEC+.:m;@S)4bYf95<_T!$_+=`W(i"6>ocoa'1eo;0n(L@ueu)Pb`r8"/q<=?nE[DUo;(:Up5*O)&jm4!jqg;]GfR-42DLF$k(>tg'*J%1N`HHqTM/=KZPi;`37.&(m-"C$RsMt8/Kt+.Mquf:.?AO:qAnRa`9Cg=PgeZknfSuY#+pb"AL#<-C^]eLW.1(3>4+$2Mfd'nHU^S0(82bQN>Z:EFJ$Ija6oCJ/S2a9Hlc\Ef"%r;u=F+B`?jO\7X0<:Y]`Dkf2ABjYb\g]"p!9YJEDf%@#!jZb.OW:c\GUI%#AC'MCfQ/IX=8:aP^ZkQVU[!+/rG["d:2oo<@Vqi:`T@<4VV<,(2(NGP:V!RH%C4RT0J]hO#@-=Io7Lg`L.!Ef=SGm^K!/&ji$L2%iT5DEi0BX/#Cjp`[D+,2WG-*LF/GV5/:+sI29PDWe;4bmD2U*9u&)Q/>TmnJLWn]jM(``&4B_Z3q)(3>.A9?Hg*6q%27n19;l%2/#luI=.gOB8j,@tV>OgH+49P\ln4A,Y857%[3[K3'Eg[(aisg,0;hq'2-Uo3Jl"OK8/QLZeAV//I+C4hOg<$HV1FnKil<:A-K6>gb-R@Ki[@Nh*sGL&W[>,@cOjAB][tr6ht0WP7f?T]VhQ$%Bu"rWA6N,9L%@S(HQT*O2RoV"53ElZ$WumdR-?qO@H+SF%gOlS&8;0@!+K6#P@&`HbOKaq9:Nd97FVAIVeCs,$,"(1-B\`>[`r*n205Ul*4F&kR*!"lE^5TBe`eg$MjMkcjbR\fGqXo3$EV]o&_#O6oG8&c?kAEa.n_DB/>E6;$'i,Umo#Vilk.n1Yj/AU(:u^Dg]%+_X"Z$pPq^]/=.=nP$bI7=LLkalc>KuQok^[j.T^XVK$W6N%GZt;-k(CCFZ2lX-'?.d>\3(jdJeSM`VkCc])d9D5D=Zhd9e9ECfTYQj'XClbu]2&du"fP[:`)-a0Z6a0e^ON*_]C3adJ5HB.RV^.%6(BDIt`2p$e5k!uu>`#Ijk_h"F,FmW3RUL<\C*X>?jGRXFCc+T,p6PshdQ5a*T`Y-#eI[E)r,42,ZLq`qao*bsKX*3Ri(%lEof2eIFar:H-*+-%5lfXaRq[#q:&Y_]ZHAA5^a_cW:D`e7KoC0r0P^ssO!g!fn#4??\j15\/l)]GUAJ>#_06kQ^Zs*]Lpo=<_>b,"9o/~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1702 +>> +stream +Gb"/)>>lJ"'Roe[cm-X@l5nd#)_k"N1dGfA4>Gli`%Xq@&@5'aIpZnCZIW?alm55;b':^fopqk!FEI?P"$h]g61OhmNmO7(rGi/L#g0:]0[*Ae*i7QJb5a#f9X#-2(_RH8BVCEVl,Hm6-3:6_0kX/MElI1V[Yo^a_D#7HVN6XA"fOO/>7F5M@J&^Em]optN9EH8=)PGc]3I&eH#Cbg7@0),a.$Q6oaRYfQhZbW:Q9S\kOS[(+rN>OCup*CG(;%<>3r-ic)[&f#+_3$gubm_Y-I=hRN77^g-PiV\m_>"2Wc"L3_ZUB0W0]X@9Rf/3?c$FdrN>b8$<.F7$J?r6ZA#G\DN4rnsC-p$Ve#(+?8LO$@C"*"7/Pni(N,*WM(Y>>8pd9]e&3hJLu(p0tTaG`T#YD.b>*KSb\d0D_+QpWX1tY-p]+KQjW`,SWBT>%)n\"`#(Ag^;2Z=pA0G(,4)]2"lUTS(6t8Bp(g!q*-6S0S->9AY6grX;23DU$mB,VKBRo,=2?&(>O,T8cU+-Qbh`Bm5/h1saE'[Y,X7&9kfYfi6I!JSiFjK.#>F$Q8*P#:3r(I)cTXg]F&ijPLs+(0fd]Em9?oi@`FH:K(;k/$0ATH.7>6AX9SCIsE%P*2#p@^d?Stf%^te(;#2f]0_qtQ-uZ^A70YZjO/H(A[GNfWr3l4NfM"D-$3=-j4r2HiIl'^U`Xe`M&!@L/S$61-Z@Y64HO6dbl]+Du3l^-[QNgV1VsoNID7!$h`>Z\B2I0CO"$]TH)NYAM6o$"pY^$;36$?sfC^p0rOs.'J)s]pJI$"he3ls)Jr4Xd$_OR\?YSYFNRn8@h8ScUFD_)cDOIa9YZJ/\Fge&-#<`'US+_dmtl`+B,V!*7.#'sU@L,A1T`B\?%Z&&"!PX>$#RU2%D_E$obsXq8r%.jGJ't0bg~>endstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1249 +>> +stream +Gb!#\9iKe#&A@7.kQoC3e[MX:+IG&$!_jY0RE8u;_h?#4Jhq9DI3i*`mDQG(:7VgGb!)rae#p%c(a/dng$_?2^dSAi?X\HkL`QS/OFcU"KMh3?LFlCJ(^P[1&6M9#K[TJ!)(I)&`*i>MH0W)1H%H%9.HHFcn-K>-'8+=]mF6Lj$tE!KT4]AGSl'H[B8+!YAYU`9?f!3;k*Iu@o'Ji?8V>1'ErC"e#O;jI#To?C!`sVWnQ6dS_7H?I6AKPgGp\679t]:%'a!\%*OIB>'B`i4?=[ccLcT-blfPFZo9;72;]YWM/@NZDQ/;m+5_C]3k[K)q*.h\`uOdFTG8i-0@V+ct>;Z3"P7NP"J7_$s!uWZ.&sg-AhRQjQeMl(Kot1/g'pTekru;Dm]I@8\rmW[&a'*+VWlf,4"?!3NA!/'HOG0,>p'j?%9.\3Zh^8+nC4\faftU@\i5O:s':OOR6@:N6H-dMh2S+&sC_p>dL-J7W^Vk2O(]&u"l0AURc;..*8*4@[?Y#)0auY@Cg'*2qGsNc>@o;#E,)lQDX(G;SG7OIBViW5&Vc/.[(,!6C2i]D~>endstream +endobj +xref +0 24 +0000000000 65535 f +0000000073 00000 n +0000000124 00000 n +0000000231 00000 n +0000000343 00000 n +0000000462 00000 n +0000000667 00000 n +0000000872 00000 n +0000001077 00000 n +0000001282 00000 n +0000001487 00000 n +0000001693 00000 n +0000001899 00000 n +0000002105 00000 n +0000002175 00000 n +0000002459 00000 n +0000002564 00000 n +0000004657 00000 n +0000006433 00000 n +0000008209 00000 n +0000009981 00000 n +0000011814 00000 n +0000013601 00000 n +0000015395 00000 n +trailer +<< +/ID +[<901f406f6cfbf9b640977a323bae9dd6><901f406f6cfbf9b640977a323bae9dd6>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 14 0 R +/Root 13 0 R +/Size 24 +>> +startxref +16736 +%%EOF diff --git a/output/timing_stats.json b/output/timing_stats.json new file mode 100644 index 0000000..4d1ce00 --- /dev/null +++ b/output/timing_stats.json @@ -0,0 +1,782 @@ +[ + { + "fichier": "23_20190411093049_00001.pdf", + "debut": "2026-04-21T10:17:12.538647", + "fin": "2026-04-21T10:26:15.952573", + "duree_totale_s": 543.41, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 11.69, + "duree_extraction_s": 97.82, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_RECUEIL", + "duree_identification_s": 12.24, + "duree_extraction_s": 77.88, + "statut": "ok", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 15.92, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 13.07, + "duree_extraction_s": 81.06, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 7.02, + "duree_extraction_s": 176.77, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 8.1, + "duree_extraction_s": 41.14, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 358.pdf", + "debut": "2026-04-21T10:26:15.976638", + "fin": "2026-04-21T10:30:11.701132", + "duree_totale_s": 235.72, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 11.94, + "duree_extraction_s": 54.89, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 8.28, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 10.99, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 12.63, + "duree_extraction_s": 36.32, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 13.53, + "duree_extraction_s": 49.68, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 10.64, + "duree_extraction_s": 25.93, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 368.pdf", + "debut": "2026-04-21T10:30:11.703536", + "fin": "2026-04-21T10:34:08.640839", + "duree_totale_s": 236.94, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 16.14, + "duree_extraction_s": 64.08, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 10.97, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 14.35, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 9.12, + "duree_extraction_s": 48.81, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 8.32, + "duree_extraction_s": 22.88, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 13.82, + "duree_extraction_s": 27.6, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 371.pdf", + "debut": "2026-04-21T10:34:08.654725", + "fin": "2026-04-21T10:38:03.504814", + "duree_totale_s": 234.85, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 15.61, + "duree_extraction_s": 70.77, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 11.81, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 11.32, + "duree_extraction_s": 49.28, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 6.82, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 11.32, + "duree_extraction_s": 18.73, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 9.94, + "duree_extraction_s": 28.28, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 373.pdf", + "debut": "2026-04-21T10:38:03.508522", + "fin": "2026-04-21T10:42:37.600479", + "duree_totale_s": 274.09, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 11.39, + "duree_extraction_s": 97.01, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 11.14, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 11.22, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 13.87, + "duree_extraction_s": 58.78, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 11.0, + "duree_extraction_s": 21.31, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 10.34, + "duree_extraction_s": 27.16, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 385.pdf", + "debut": "2026-04-21T10:42:37.603168", + "fin": "2026-04-21T10:47:43.613674", + "duree_totale_s": 306.01, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 12.89, + "duree_extraction_s": 153.67, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 7.35, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 13.16, + "duree_extraction_s": 39.37, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 11.77, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 9.31, + "duree_extraction_s": 19.19, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 11.48, + "duree_extraction_s": 26.72, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 397.pdf", + "debut": "2026-04-21T10:47:43.619457", + "fin": "2026-04-21T10:51:51.030768", + "duree_totale_s": 247.41, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 9.27, + "duree_extraction_s": 76.42, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 10.26, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 10.22, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 12.02, + "duree_extraction_s": 62.89, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 7.81, + "duree_extraction_s": 31.15, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 6.48, + "duree_extraction_s": 20.03, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 398.pdf", + "debut": "2026-04-21T10:51:51.034973", + "fin": "2026-04-21T10:56:46.034162", + "duree_totale_s": 295.0, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 12.35, + "duree_extraction_s": 77.87, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 10.64, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 9.77, + "duree_extraction_s": 84.09, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 8.03, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 6.18, + "duree_extraction_s": 31.56, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 8.34, + "duree_extraction_s": 45.3, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 404.pdf", + "debut": "2026-04-21T10:56:46.039051", + "fin": "2026-04-21T11:01:41.285242", + "duree_totale_s": 295.25, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 8.05, + "duree_extraction_s": 121.21, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 6.92, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 9.45, + "duree_extraction_s": 40.67, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 12.45, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 11.47, + "duree_extraction_s": 38.9, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 14.06, + "duree_extraction_s": 31.22, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 411.pdf", + "debut": "2026-04-21T11:01:41.288964", + "fin": "2026-04-21T11:07:47.867790", + "duree_totale_s": 366.58, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 9.23, + "duree_extraction_s": 173.09, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 7.92, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 11.49, + "duree_extraction_s": 58.97, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 10.21, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 12.2, + "duree_extraction_s": 24.99, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 10.47, + "duree_extraction_s": 47.16, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 413.pdf", + "debut": "2026-04-21T11:07:47.870357", + "fin": "2026-04-21T11:13:01.604612", + "duree_totale_s": 313.74, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 11.21, + "duree_extraction_s": 140.94, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 7.76, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 10.87, + "duree_extraction_s": 44.94, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 11.68, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 8.19, + "duree_extraction_s": 31.99, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 8.74, + "duree_extraction_s": 36.54, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 425.pdf", + "debut": "2026-04-21T11:13:01.615261", + "fin": "2026-04-21T11:16:55.314927", + "duree_totale_s": 233.7, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 16.2, + "duree_extraction_s": 83.81, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 7.65, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 9.84, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 8.8, + "duree_extraction_s": 37.99, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 9.78, + "duree_extraction_s": 19.28, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 14.47, + "duree_extraction_s": 24.91, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 429.pdf", + "debut": "2026-04-21T11:16:55.318401", + "fin": "2026-04-21T11:20:58.656749", + "duree_totale_s": 243.34, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 10.55, + "duree_extraction_s": 71.53, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 10.12, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 10.72, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 10.83, + "duree_extraction_s": 37.92, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 12.02, + "duree_extraction_s": 27.99, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 7.75, + "duree_extraction_s": 43.06, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + } +] \ No newline at end of file