From fd7f2151a624b43aac4d31e086b9ed6d1ac70ef2 Mon Sep 17 00:00:00 2001 From: oussi Date: Fri, 24 Apr 2026 11:04:31 +0200 Subject: [PATCH] version a 82% --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 10 + README.md | 94 + extraction.py | 1048 ++++++ generate_report.py | 683 ++++ output/bilan_extraction_mistral_ogc.pdf | 131 + output/extraction_ogc.xlsx | Bin 0 -> 33090 bytes output/extraction_ogc_raw_Correction.json | 3413 +++++++++++++++++ output/extraction_ogc_raw_mistral.json | 4036 +++++++++++++++++++++ output/rapport_mistral.pdf | 213 ++ output/rapport_timing.pdf | 213 ++ output/timing_stats.json | 782 ++++ 12 files changed, 10623 insertions(+) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 README.md create mode 100644 extraction.py create mode 100644 generate_report.py create mode 100644 output/bilan_extraction_mistral_ogc.pdf create mode 100644 output/extraction_ogc.xlsx create mode 100644 output/extraction_ogc_raw_Correction.json create mode 100644 output/extraction_ogc_raw_mistral.json create mode 100644 output/rapport_mistral.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..695c23ae3b8f4bbcb2f5b24c16f94d44ec070da3 GIT binary patch literal 6148 zcmeHKyG{c^3>=dPBBe=5xxc_4oT5-u#x+6Y4jLrAmM|M>iM~gG|_&BWJ`j=h59aW!CIQJSmkcn51M|_@% z8@ywKC-iv07MowY=g(AD3P=GdAO)m=6u7K__gC6%o~S4Vq<|FoQNX_sjqcbL4vF#U zV2BZbIAuDF&oN67nlkrUzq<-Nm@w(De$ipu<2^OTJn{uw~k)Udu^jX(!Y(hQO*#p km>8{?3vb1@M|I7gx!)BIi9u&R=tTVpxGpj&@D~bv0nSkyLjV8( 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/README.md b/README.md new file mode 100644 index 0000000..1e465d6 --- /dev/null +++ b/README.md @@ -0,0 +1,94 @@ +# Extraction OGC — Ministral-3 8B + +Extraction automatique des fiches OGC (Organisme de Gestion du Contrôle) vers Excel. +Modèle : **ministral-3:8b-cloud** (vision via Ollama). + +--- + +## Structure du dossier + +``` +TestExtract_mistral/ +├── extraction.py ← script principal +├── README.md ← ce fichier +├── scanOgc -> ../testExtraction2/scanOgc ← lien symbolique vers les PDFs +└── output/ + ├── extraction_ogc.xlsx ← résultat (4 onglets) + └── extraction_ogc_raw.json ← données brutes pour debug +``` + +--- + +## Lancement + +```bash +cd TestExtract_mistral + +# Tous les fichiers +python3 extraction.py + +# Un seul fichier (relance partielle — fusionne avec le cache) +python3 extraction.py "358" +``` + +--- + +## Ce que fait le script + +Même logique que le projet qwen (`testExtraction2`) : + +1. Convertit chaque page PDF en image (200 DPI) +2. Envoie l'image à **ministral-3:8b-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 + +--- + +## Différences vs projet Qwen + +| | Qwen3-VL 235B | Ministral-3 8B | +|---|---|---| +| Vitesse par page | ~10–100s | ~2–10s | +| Thinking mode | Oui (num_predict=8192 obligatoire) | Non (num_predict=2048 suffit) | +| Rate limit | Modéré | Plus serré — pause 3s entre requêtes | +| JSON malformé | Rare | Parfois des newlines dans les valeurs | + +--- + +## Particularités techniques + +- **Pas de thinking** : ministral répond directement, les prompts courts et le `num_predict=2048` suffisent. +- **Rate limit serré** : pause de 3s entre chaque requête + retry automatique sur 429. Ne pas lancer en parallèle avec le projet qwen. +- **Newlines dans JSON** : ministral peut inclure des sauts de ligne dans les valeurs string — le parser les nettoie automatiquement. +- **Relance partielle** : `python3 extraction.py "XXX"` charge le cache JSON existant. + +--- + +## Prérequis + +```bash +pip install pdf2image pillow pandas openpyxl requests +# Ollama doit tourner (ollama serve) +# Modèle requis : ministral-3:8b-cloud +``` diff --git a/extraction.py b/extraction.py new file mode 100644 index 0000000..2b4fe35 --- /dev/null +++ b/extraction.py @@ -0,0 +1,1048 @@ +""" +Extraction OGC → Excel +Modèle : ministral-3:8b-cloud 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 = "ministral-3:8b-cloud" +PDF_DPI = 200 + +# Rate-limit : pause entre chaque appel et retry sur 429 +INTER_REQUEST_DELAY = 3 # secondes (plus conservateur que qwen car quota plus serré) +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 = 120, num_predict: int = 2048, + timing_record: dict = None) -> str: + """ + Envoie une image + prompt à Ollama en mode streaming. + ministral-3 est un modèle texte/vision sans thinking mode : + num_predict=2048 suffit largement (réponses courtes et rapides). + 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 + 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 SE1/SE2/SE3/SE4 : 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. +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":[ + {"code":"","rang":"","libelle":""}, + {"code":"","rang":"","libelle":""} +], +"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"} + +# ─── 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 _strip_dot_number(val: str) -> str: + """Supprime le point parasite dans un nombre (ex: '6.173' → '6173', '.0' → '0').""" + v = str(val).strip() + if re.match(r"^\d+\.\d+$", v): + return v.replace(".", "") + if v.startswith(".") and v[1:].isdigit(): + return v[1:] + return v + + +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 _normalize_result(result: dict) -> None: + """Normalise les données extraites en place (checkboxes, chiffres mal lus, se_coche, durées).""" + 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]) + 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": ""} + + 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"]) + 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 in {"1", "2", "3", "4"}: + d["se_coche"] = f"SE{se_raw}" + elif se_raw.upper() in {"SE1", "SE2", "SE3", "SE4"}: + d["se_coche"] = se_raw.upper() + elif se_raw: + d["se_coche"] = "" + + +def compute_audit(result: dict) -> dict: + """ + Calcule un bloc _audit pour l'OGC. + score_global ∈ [0,1] — seuil d'alerte : 0.80 + """ + checks: list[tuple[str, float]] = [] + + 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 + + if "raw_response" in d: + checks.append((f"page_{page}_json", 0.10)) + continue + + if ptype == "FICHE_RECUEIL": + checks.append(("n_ogc", 1.0 if d.get("n_ogc") else 0.20)) + + dr_code = (d.get("dr_etab") or {}).get("code", "") + checks.append(("dr_etab", 0.31 if dr_code else 1.0)) + + prov = str((d.get("sejour_etab") or {}).get("provenance", "")).strip() + checks.append(("sejour_etab.provenance", 0.40 if prov else 1.0)) + + 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)) + else: + checks.append(("se_coche", 0.20)) + + 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")] + checks.append(("das_etab", 0.50 if (dp_code and not das) else 1.0)) + + 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": + 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 = 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": + suspect = any( + re.search(r"[A-Za-z]", str((v or {}).get("photocopie", ""))) + for v in (d.get("elements") or {}).values() + if isinstance(v, dict) + ) + checks.append(("elements.photocopie", 0.40 if suspect else 1.0)) + + if not checks: + score_global = 1.0 + alertes = [] + else: + scores = [s for _, s in checks] + score_global = round(sum(scores) / len(scores), 2) + alertes = [ + {"champ": champ, "score": score} + for champ, score in checks + if score < 0.80 + ] + + return { + "score_global": score_global, + "alertes": alertes, + "modele": MODEL, + "date_extraction": datetime.now().strftime("%Y-%m-%d"), + } + + +# ─── Traitement d'un PDF ────────────────────────────────────────────────────── + +def process_pdf(pdf_path: Path) -> 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=60, num_predict=32, + timing_record=timing) + 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 list(PROMPTS.keys()) + list(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: + raw = ask_vision(PROMPTS[page_type], img, timeout=120, num_predict=2048, + 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=120, num_predict=4096, + 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 ───────────────────────────────────────────── + maintien = str(row.get("admin22_maintien_avis_controleur", "")).lower() + retour = str(row.get("admin22_retour_groupage_dim", "")).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", "") + # ── 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 + + +# ─── 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 = [] + + 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)) + + 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)) + + 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", [])) + + 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") or "—")[: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) + + 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) + + 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)) + + 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}") + + +# ─── 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"]) + + 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") + + +# ─── 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_mistral.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..5b8d62d --- /dev/null +++ b/generate_report.py @@ -0,0 +1,683 @@ +""" +Génération du bilan d'extraction OGC — MISTRAL +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 = "ministral-3:8b-cloud" +LABEL = "MISTRAL" +ACC = colors.HexColor("#6c3483") + +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_mistral_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_mistral_ogc.pdf b/output/bilan_extraction_mistral_ogc.pdf new file mode 100644 index 0000000..4041646 --- /dev/null +++ b/output/bilan_extraction_mistral_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:20260421103343+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260421103343+02'00') /Producer (ReportLab PDF Library - www.reportlab.com) + /Subject (\(unspecified\)) /Title (Bilan extraction OGC \204 MISTRAL) /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 2566 +>> +stream +Gb!#_D/\/e&H88._(WdU!`aMn'DX@WdNV,K[#GH0JA6>hgSJlUc??GZhiF!QP'XU>F1.m(_]3gnOM$@]P>4BArJ'\%oT>6E,P't;&7B&@NIXP&)-rHLdE/".YjEl>U'rhCPVEI]AaFP?S-[+\1F-Cn)obDmJYu2)eb>8]]hVKM1aH6D9jZSoC.>qN3h_s:gSHSs1UF/p'GLS=)ZnXH?3;7^[5`K>3`u+1\*W983IQ6sIFeFEMs%P)OjOd495uAh55MD_K*Mkk-7tG]dZ2>SgjbrNlG4#acX7:dR/jq1+b-8kJ]^MN$j@=a>+eT5Li^428M@\bc6sP+:Ei0$2kS48ZY1GPCc^N1!]@mcrs*mZ&\];b^2a$#nedI^rcLbZ8M"qu/_HID/s&jpi#OLJE%Ac&O?@%]m[TNca^%?MGBkW[XP[,+_Q#Jm&h2I/mIVX6]HUVI2j_B[5WbN!e&a.a7^IZ*md86(@MW=;-+-;JaqC<2JAN5Mrp=53X>9u.]!H;_=eXfI[.>T_23l2c1`AH^hV^@#W"4:c6FHd9!-npA1ML]h7AC_J`oLCjai6'in%a=%0nh"QQ.gl%[IBqXiG=!=:_5]JkqVaFeUldh>62Uf;j;-5QIO2S!t\2U\+mFMEPd]R*>S5O1BqO#?:-GlD,9$[&&B\Yg3994T8TLl14B-A5rh6mb.$D(oXNs)AW&5;6'/n1#.2>lDLu7\+VDIUC1ohf5$U^6Y^//f/L?F0J%EA-h\!&B%l<@il&Ri`S?A41j7D^Lr2Z+MC;!6N2MrS`C)p.4gXd%^h8Fel`5?=l*U#gn:\\b#$Y-feYbN]C2RF81hWlQ[>FoP%o!BVpPu#2QI#R`EsV\Tsu^c->;6e(d(6P31n8odJ[*r=3&GQA`QO[[dTShqUYPKBU#tcG(>&F_gotPit`mHup!835$1k+7b%Y^O:ok*kRh)qfOo5IlObuE/h%^-O0@G.70Q-WaQM,Ta:F/aBQ654L&<_[]a4^Gu6:5]3Ka==RjoV9Nr;,D4<#6@/kRUGn4o#G6Gn]`h(ipO"c0d7eK3#g_]"L9C)75MQiddfTE#HY;.Jd<'L4r=PQE6U1`K0j'HhfKT5'KuR%E1j1]VZ73a)Mn+_BqbS7#_PAC'HfOQ!0'L"lIOEL!??/W@iGIQOJ"Bg5=Yb&kN@(MYA(HZr#@)hXAK>BcZ,VWL0/dE5=\#U#&a)n3NjK!J`k+"!AEUn464%6k)PVr,!hF#;eSB&TV/58q48;9aa"InJ`k+"!A3IL[(?$,'Ym<)dTTV05QMj<9sOCU'L4t##>tO*6u%HTi&Pga[$91fCd/#@dRmeLi8FgS5rS5Xs8O8"`CY9/a$@t5m@ko=;/KLKoA_*\q?X\t%Ph/pc1Z1,'G.q9\\fK6O+K[H_9W'#.54p0/D(GVHB>GV,<.p91Z-o=LqT<#-iInlh7AC5j(,oPYk(*&"(+1RY>0CRaTG/jItYG(85FRO,4<,O*rc&T(FSYd#iuQ:EW4hK1CUmjgJQ!!6/aB1=Dm?4W?(\U']-t*o.=e:(m0/8'H)(.Pd6gT(8E^'`!E5R;,BA=?&r>NfITY98cGnmZY`gDB\PFhWYuBgS'/f]7BUkj(9*tNec!K3&W'Um`$,dFMeEYf\Hj7N.X3AZJ-Z@D@6aqtatOri!G*)Z_.YF>04Te\PS(k*:`8S"OJZ6Z40`^WBa682IV*0a73fmAHV)2].]]U')Ne:IS.(guBEU=#SU5A"ciU9Te5.%c0Lar-IYs"/6U]%8ChH`1\5lMs(:JE]>m<6`ko[I^YVkfahlf*lf6\a&k;(Y$[MCO(2mZ)na?"5F]+Re=VY2:+9ibDDAh72S]30p4#=n3Vb@]B@rU@ZpZ/l$b;633qn^2&?7P?f^U$g6NU29F_=r36Y`#'OfD-tT\G\/A&chB(R/I0ZV6Arg\[?+gZca_H#iH:$Ps'EP,5''Z:LJSs0\0^ft6289^em4]i-GKBomU>a(]VthNLEEUZpK:jQ`lHFG!8E6Y.SGWa;9*,e~>endstream +endobj +12 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1944 +>> +stream +Gb"/);,c4/&:WeDm)oiPB5/u#ZDDp.JC<;=],&i(bhVt+6et;7mR(V>kLn,SZl[$krZWA)ZS1<@G9?.3A`fYi)47(G@(I]hj*MA%dCl&@Qp\p&;iJ?Ba&Fb$Y]b4qE$qm;>63fM;W)VJsL\UI^6"4tk?D;=tZ[>\Bq%:p1o(Egj^I%t._>&\e&r12_"B&tY\3MkpnUNWEI0:.F0,p$9iAhtBM?ldl!'/cP3t:4%ZsZRY_`[O%K?>-c.[Y=*fCt(@;sK\Yf61Pg+CWkY8Htbsd0snWhfKtY#qBS:#F:K;1!Mf,N+Q+[[giSf83BiI!1n0;&+8G4,g=+b7A=3O$aXNtJSZUYdVi__8N%20TtBAB&h#t(!ng+,4".)='HZ%sN0]/BS(fu#N@FM))@;R)m7\`*ig[KBJ7p3g"U?;0Otq%80fCqXC_A&I$q:gkVU9c'[<%<1?F\5oYZrD=nmlfIBZW_q,WBK?!j^3EJma)6Ojo9O_r3Em@?G_We]of\7,MjZ(CAGhcth9&;fC-KMoMq&a@/uG?GF*fX.WBdS4m@OX'mj(F:lE7Fk.1dC/s843/*\"qAk("SiGq6rdB-AicsrUhm%<4!la$%uoC'.M6WB_1s"ZeQCKhD6VaWX2>J`3(b[HOJomGoP=bCRD@Uu26tElhfE`^o)^PR==+EAEF<*J1KE4B%X^rEGs.+!7`dfJ.:iH@7X:[J4uAVZV)JH'?@!iYNeXH+DBWRCpO$9.hYR$@Ce/MN*PZm.3M5g/.F0S.hX.PB_h$6Bk=H\SPNpIsCmUHH;el-n-akeRg&Sg8QgelAp)O,c^4_]j)._2ISH%fUeE[^6h[Hb.]8'C5)@a%8-#cMZmkC:_*.+88O1cjLG8RrVIN#bF^p+i8-n>qaq;olu5PSB&i+Y&-(*h-N\s_=3\;^X542Q.aM.=@FgP*!C@pKp@&r[;2&!5/="1r62e4F=`>URXOhqT0oF.)lmn>FMfIaGWh)R(BND$l>E31E'!eVNY8G7/`^S+S&NcRZIbA%p6FFr@UH%l,0hk(bDP0">F^"[5>=Ug#T(l"0$g"_B5BhIZu::JBlMu=lLqa^?"@#6-ZSPuNO'I?],*[(>s9_APAgq?9EKiQoE0Q]/AqSqk:hpk\Ikmp<_kj=@ei=]5iiZ7C+Fn_#djkgls&JQ#X[P+]5XA/cOLSQ0oQ#L:/.E(-OX2VMrgUUb"cA+LJr1rA]Q&:79p`sUW_?t6A'>)Jrhs9(dU^DORkP=6q/MMbSO^C]O7'$i>BV$Fm4HD@ioF\39'5nT+GbmW!#Jb6AR34'd70'P9E4E`)_kT?0`[G\UE"A'k1DrLFL4M+U0=o!o[T\;?~>endstream +endobj +13 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 2182 +>> +stream +Gau`XD3N^$&H;*)_7)InM#mqooNTbVb]Gnp-M>EKaZ+LeeJZ-Wg%)NGpBAY?Ulu3bQg7p(%U$0bZL!40PqYL+s-!6IgDrV!=6T;^Jd=fGDhtWVU"+)FrG(l4+IA[OAjT?`:a&A!PYh3;/^>3X3E7K&3*2?=N0CoI*$+Ja=spR/*3FB&Jg-B0&CLP07>B[cf@qCF&ln&]iG^?Wg[YMOI7L?)#6'hc*AW1%r@_pf7t9hg^Q.%=q;YpkX4SRB<$nH)]!4\RNp=rEV]XOMrdaQ_5cPk9XiL&/-KjH3hO+mr^HHsMD^.`8P75o,(c8u0(RGWEE#l*-gl.C%N0km8-sohgG_s/J>\$J$@JVHR#Z_N8.F$uJ.Q2O:NX5U9`0s4@^i/SO_nHpbSgtrT#i"n/e2,#j9(.AGO!prfG)%1$IUAi$kjECND9?A$LB>rhg]h4BfjXOq]S7)Fm9tG#?ZdMs#%B2.o,T/$%/<1OkCb\d2HB.R^,Z1b!jT(nZi)-lnD>=KjnX/4/6E4k"C`[Km3Id9oBl6j>K>34?'T*8fki3\ZXd%k]RY2CLrJs+#-q^@FBWoU&*Ho@#NM6![D4SkMf7RR<6`&mZ#-k#qD>YXi*9h[WrD.ua][$6-Z+RoR@@cqhJ@=XE?**D#2s6MM(1tR6])b,`:gP2+d64fT@::j!$bt:Wm90>fD"XrR^K8+c(,;7XC`pe8(u?aq;AbLRd&a$]3@W[33LWZ:^iAk\*9D=K.9!7@b1H_T>'7?OAXfOXaeHV-'EHCo`0+KbmK3I9]_#*?dI9$G!/?a>HF3grPPMI=7pGU<3If=jn$^1ls[oA(HdmdcI6*4r1g95;]`i6bu/s]P/ijZ+tF#>6d#(D%qomfb?"T4BpRhQ)!75!1'f`sN]0ZR2NW[4B03Y1Z6j,om@_'?"O4\XI7M)3&?Korm,6D1XFEd.3)G?;.UHQWMH\(I_*8SFk#PXfMD,8_)_b1G+ET`Q/L=i@`nbA9$3Sf8!"G#I23=Xa%+(!*p[LpN%ncnQr;Q4&Xf5Zk(h2B%32#2Rlq;*/\-F#N5TEOB3/W,,?nq!M!K(;m@N`]T&>0U35`-+1qa:kiVSW?;^LLVK"3/e3a$-HEINHBj&?m#F_:5b7fU!GrYn$sc^cndmH9ggI+u7KdE'V6XfpC1g3^0U1I4u/_m!DWR*FKrt=9SM/J0NX4)T]2p"t7DRJ\g!*8$Hn]*)Q,dp*DS(I/S'O"*(h_hd?o.cTVoE)"rL\`NV!MOCK@<:l'N6@WP\Y:\"eb9n`ZVI`K0u#)o`MNdW#[-T`YbfEVHn_`eX))"o6[EpNl/$F#R>YcIJ`8R/54GJ8,?nq"@nrDE8q[rukp433tfU"s*>o4UnI`K1%E'V8.fU(*0m6,mJDV!s&b6%ms?&(l>2::5/iXI`K0u#"1RjLN0h9hih.%LY)h5(;iM,3gcTPf7=mCfg`h!`+O*J_NipB:fJRHGZ@"5>g>Ft0BQ=m/%5h/u>^"2ITL]1XihRtS5&3f*q*_^]&l@$rjWi\B^A`]?F.LE.*=,p%Do.^0;,2J2=3orC4+._s;RF,l5MRj%aEppf/3VDC(Kp'^iF4eGJEG("ddGg(_1W(2CnP=*QB?$3HBBG:Jk=Csr6ZoJ,c3Vh&6$T!b3heJsMT`SCXKW/sGf!Uin//]Q3'!h2=W-Mb@;WoGRr?;#FKX_B_oZsN)]fp.D0U(r)?SI[[c,>nC*LEY[uFEPH9A=VL+pH8HoZrI+>_ktG\eS;r[nb":5KS4s1J>:+DC9u/4rDO~>endstream +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 +0000001530 00000 n +0000001608 00000 n +0000004266 00000 n +0000006302 00000 n +0000008576 00000 n +trailer +<< +/ID +[<5db226b142aed52cb198342b3a28cf5b><5db226b142aed52cb198342b3a28cf5b>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 9 0 R +/Root 8 0 R +/Size 15 +>> +startxref +8984 +%%EOF diff --git a/output/extraction_ogc.xlsx b/output/extraction_ogc.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b9c43daa8dbad3541e11507f6c08b5ee9f44f9ba GIT binary patch literal 33090 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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 Ministral-3 8B) /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 2002 +>> +stream +Gb!$I?#uJp'Sc)T.s/77-R"Wa6L%i@ge;n?D0-@?Ce;MgkA\1uFc<=`+i"`3M5](-sG>IBg3T@6)$k"$2@^/+Oid,TH5547e;T6Z,Z]eH`;-f<`S8q(TL-;7X03]%p"r,rjPgDS.paMo*0-b]?39V*hXJIf\-R_Rutp$\;ViBALh8k1k+YT'*F&HPmSGPb]>M:3r.("1lm-4c0j%s)F!@FRPgZFc)>W(@5TjeN\,mN^5VbPsQnet0@3-:th0bT&.Cu!#^IGQ(cVHS>aK5\5TbJ[?Zs`A>]IVqs@6Bp[ECdH5/FV:1LX0INq\$%5T)^g__o9i)A\-60(#TE``ApU9PSsDPVp(sR-k"G+bV@etXW=g&_q&/ek_dr/g.?2L'?uW[.5Sk<=2WB7ZjLOib;#u'nc[0'+Y2(jmu*;6jX!L#J[gF)H0u.*?&j:L5?^sA1<+RVLO1)0%e4h3/Ea':S8P"d$F5QWiXO"I)G(icWg\:EggJ>5HtktiT:/ce:6TpG"@+ZL(d86[9$p4d(W;8qagZKrLsQQmUMtc<)<+Qrk&tTY]M,)'F,1eZ&I8bbRh!pERuMibAPkfQ;,GmGo._K$bK"'jK,h$/6MoC'qK-kD27I-OnsqE"89$9B9PplZB^I2B`T,OZ&BBp>">jgo[Hd(]u!"64ri?5Han<.3`gF4Y!-C:ic!A[&3j;kd?]"WX:R06(^,12T1P7p5Km98p%;fBqhO[X+p5l35*n.^te,r7C%m<\_M\A5S&Pk)?q9"!oN.AFlH\rhO>L>W'TMkqP['JJtUa9Y6cn_W+h31te>8@5bmdf&o6kKMlL^n]t4Cf][3B0^=?A'\I"]Vc!;Pp>##,!K^5RWuj"O_S*GG%b_$sVr)l:cpepK\bW2$%4I`eF(4!\;Qk-Q[aafjRp+qQ[eUJIThc#lk=R!m%F[W=pq9H;SfKV4SmC)?%7:&fL/X.(Re!o'Um4rR9bG[f/8K?HNS\I=sCe2Aq!qCZ%4P[1f;.D'QPX8JH-M54Ym0"Jc=krWj@ds'a`VsM8H:>(?E?KE3):>Q!dS?N)FLNZps+qBA8cJuX;i$[R3:PX^?a$KqWm"!7+B&``-W8Kf+>ugBi3%WgE<`>#Y;N=aS%TcI:#>*g-Za0=^eWZP8e[)T`HFKi`]lD6]41EQ&74W^d=k.\\"+-Lui%>toB&Bd5U/f906ENc+$HO])n'fm4fBqo=!VdcZ/1+UC<68W6c(R)dnt6a/<=Wp_0+P!Ak;%h>`63eeQUaK)endstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1849 +>> +stream +Gb!#^9lldp&A@C2oOVg/<%Xu&!J[B$g]:6lC;qrJ!8[h#edeoYL=[d1"sZ+WPtt5QSc85\Wb+IYId*pJ2=KT!i,gT(!:rb;7Ld4?APIL?AnJH9f6*FHM$;BmBcSQX35G8bsnnPk#Od+\[[nGlCC(qNH(7Q!MVd/TjXQWL0&t8':oV;^N-?DfQJ&,G_Tg2DL6+SY(KJp*U^VJDk.#GtXbL=1O&5:iPC<>%"]W"47fV@&G&J8OI_D(V$'AY_40b=p*ZFjsEe&dTUr0;1X:arfa<-C=SJ\)Qn*,PN0p0@!LB)]"tcP<7tcYGg/%@X,59T4R$UL*,_&^E0=o,^Zb-ISj??8n)1ik=R\*V670p>BJkK*3fjLjI6;:UB!ZkI5:l-sMipB-%ZpnNW0VB+o&I&r5jiq\*tVErOW>MVdYmhCr^E`7-fKe/0r[qe<&KKVs:'UG&mfh=/n_p3kDFd]#)+K'rCt*9J0!nHG7i1;hVY#3#^4fLN,Qt][U;^j1fjgS#Zn6,CMf`S<\R?#97a+-!`bfnALRf*e6kiq\R(^lel[!lbsOBpAfEnB2@nK>Z4?co+_eaVhu7GsKX]'j_U:/lo5G_?2IfYh(J;=d/iD`!3`YiH*YZTcj`1u%96`F"^.)X_p]U0nMt4nFJ<;/a_/^93"Fh"H=LOWPd*ZS`%h6#Zdr'/l7I[kil6KCKSo?5q;>u%S!skVL+;a(qo@%.IRb?:[54Y>35r]3:42YGu4N;i[e9o:-dH`=h=%8\&,:HK[E`:.4Q"-LT@k+t3aphoLaq^qq:=Wbr\(LjbDh=I?X%->[`p.P$+Ck(8bK/5oC-o'EBc3>8?jd!S#Y(]CrUFnOt@VN%.!>kC<9>>-F;pp@oe`6Z=CqQIY)LY0`>ID*50m3RPLs8##OMf7dV/bJ"IE*I"n6f'@iBd[ibXX!3dou`q7*IhXYWL^*2@j8m1I,Q?ojk1*%k2Z=:9eI@q^Q!%lj=Y!'1"f2XhQ9),D)us:$:G[S/]?[pBd4Z0\I7qP9_K+FjG>g/]B4A5\E0l)VAg%lF!Go8h3`h:q77_&adujEkm[cur29BH580+BUrAd4,'U9RQq[3VF3%"Vk8/?SB\-CH_F3fY]8s-mOI'>I\r-UsRG2Vtk1XrYW%a@Gd-fp%($)2m^$^'jY-U0CM;3f.+((og0?'NA7e:;@I&>rddZ5>!,inc9,9[T.$OM)Pp5tG@Z/!/@S?+(:ZroD\$e6dJZUd'gDEK,/`KuQkXS?^_\>"C<3!uB3GEjuWGb/*;a".l6Y9&bG53d.2Prj5^KPd"pcTqrdR.\;S7H5!57l4Ou4aEMJiU$-8X`?NW/X0aG#@6*nqs`^e&h\Ke+2%E5W'u9jfmaA""]P5^1f8WJD<%HL;1)Ydj*e>jKOl'(TRgYm=J14V)H`Z;jY[5ne%c$E15c3j1'`U7DOs+U/endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1632 +>> +stream +Gb"/)hfGM_&:XAWkdT;_mGe%o!?Urc+IdVPQlo_kK.j;L=]1,[_;,%c2hR"IOu#%UGEiLiM2j`@J,M+Np3\.j`#lMa5?e+mL=#mV!>>b-!UH0ai;gNpo:prI1E"T-9l(4b_]#tOc*-/>9deF52J<4jRh3pT9MK!3lCl+Di\HGniIZh-J8#V4&ck05)S!sIO4,u)5P%m>j=hmuK]jjk9rW$5i@.sHh(m0:G]a&G,D7-BXBY\&K;R*)1t_^IA'm5;0N\Jm;F#6:7uO8RB(IH/E41C__:*GQ"[uapG8d0;Q66k^\WK-;bUc\NgVi7TnHfb,]$n5R?%3p7@1/BbK[1LCKi)"'V/VrFtNK3@$#':pqNI*+7ROGmgA:4(Q-o/c!$e7MnWN,%rFiRtA.[,T`>c0Bshj7M`6;Ko<.8](7`$-1A]Pq86h_)AH^/A@";C4V^Ahr[Lr`JZ_#Ntsr00&6]O0*]I+7kh$f@W2R#^OJ?8T_Foh\;ko!nrJkh+pMHn:q\:?'eL(fm#fo=`mi)'+Lm>,QX@t05g]I%/+tDHf[`m4&.%#rWK9bh;4/u*LD[TVNdo&CBp8b9%#E>Jo70`Aujtr93"X$$ZAV'W^r7>;36AgU`B,m9h;PNC@>uI>hsOL7B[dJK;Q:M&gE*"n`h6;]rrh"ZQr20C%ER#%"Ld2Uj3TN?^C#2=7i"Ai*XoD$ILnA`T!0*2'Z,FSs'S/rS_AE1gVXe&FNl-kT[t6r9pnc=^+4QSqF)(7cCOKUKHlDULH3jULTh_;DAP2ro1p*^5b&<(j_d#h%(i`#EJ-DH_)=*"2Uo\W13uW;Y.FJoKIip3j#\g!^Y%!\kpt^XeSp"gF?uPLho?a[4,gT[E(*O'f33Uih,s'B:Jm;A@[/E8Or2KF-qUkPC'>nD&.Q?mAY)1`P&#X!a[pOC)*XOY,hE5N'U.SX"H4ODr))DNWUuZ[E_Wc)R7Ig<=;h-c(Tq*1Oi)at&6jISC7$Ks-;sM4Z-X^uK85gk%_uVbXrhtendstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1412 +>> +stream +Gb"/i9lldb'ScA]'k_aFc4oRMnY&AfBhVI_M=$%Z9ue4b$@#)O[T0&Y01*qM_:e;gqF@dj_K&M-V^MG"EnHo>`1&L2=8u)f_CR;C5X_Mp5`ZZ/hR;df9RI+CBHM>@PZSbD[0]rPJlijjYs+Nj42glYti%D7s!.r/Bf':+h3XYNMo\_PH0R\)^jSWk-%[f:<;6HR_S8?LfuF"B*Ukj,kB2h&SLVpl)K>=!in"M2K=to>9Z"QcW]7=VqD]lfmD<02ET/A-+&"pMFuFCgOrq"EL'%o.+r\q,t^1f?]M&EWuDR=s.&oJBBL@L;k4)XW)_Jh'4m9I3dWN;>A%["ZCob[a\WDS+jXQk_6!=UWFbNZl2uUa+_!W5UR(,De`+1HL2\13+*EiG0`d%pQt.nKe:Hs?ND>Yh9$\?L:EZR$=FH`g]j_2[BnGdWhD?(qVIJfGpuA?O;])D(]J9!M$)V[kGf17lB0"HS6(FTec[99?k/hpJ(:OX,^\!#XC*!Cg;aoQXh$H)_(3*2D5A(&Yu8r.hm!1/pbHDM;6H2#nqlAJ_Cq*n`*VBIs\H4@KNV.!lY[RI1GK0R)F$1dEb%H3i(hYTADh&F:2u0Z"0]oiqbl=V&=i+dgeIY_/4e(kKf^jn'nha#OG&jNdmG4Is1YVOi:r"o4UWbXS2molR:5@g@+79]U46MFTt%pN3L0A"O)+SC92fHTKa,Ynu4_:7I8X(?>&OQtAWZSW4FuCraM@.+U4@H5Q@B__C'+?3H#E5`@Wr[lkkBrF^H.H4gcN*MB.8Kkod!Vd)YIitI\'SdJ,>B2-cC7b%E3pEreuH%kAD0"A#*f$DNpeQR_"Co16U0874Q'r3"%~>endstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1115 +>> +stream +Gb"/'?#Q2d'Re<2\;tL'&T%&I5P/AggN:oEX\Bl._AKDR&u#Eop@EJ;JH0\#bqQ,c>aeRRRd9"O`TA-A/fphb)fGa@XC-KEJ5B?2!q@6@]BYqjr8p"G.!Y7gOm+5i?c%G!nlL:B6sg0-kf)/e\riVcb@Mj">SH31L?dmg+\2J]A-l.Z^H&5ZN5SR6=?:d%b=]n?2Ta$Lj_fXfipcARQ$bX/bG13P5+lCZ^g9qX0lkl5?4KjZhm/Z7_$5C5h8k%*6AJ88,A:@3n1-V.CQ^`j/tV?t'n!UX_uB&A:P9$1TX+PIqmKCms=MQjdT:BJ/6uc(YZKcZ4D*go!lP4s*FR)4$*cTgifu[8b(YOm]N[/=6Ij7VPo#8TopuGe&1C0-PWjoAjr7;DD?I?df(Z52FcN1:fe!.M/$E1Obp.%c7pS3'Ss+%YJ-=];liYUfN3b'\TYEi9Zq'HE`!e0dc:5jN0X3;N!;\grXpal2RFA+a8^/#m*P3>q]%bt4=0),aq>%#&"*Sl!)2b\@UBc`C0[]$+mugs<'&]UI&Z%GXUX%rD:`U]Nln/J6LN^V\8.,e_?e-YL(1*OK*o_nhaH1[Rr.4^$*(jRniI^@$dAZ>=TPgATK'r!I3e,'=Wbc^8Cg;:5*GT$XH2uX#dHRVooTGBP4So=VMK&'%&;O%3cn=_:YeFe=6PB0a-%$_:I];=9PKHbL=qd1'I3Y'R.=KXDbPkM0?&?T9NblJ)H54SZGr+dVQ;L`N@AX:=_#Sm;q@>A`fr?XX>+:G.;MWUa2Sh;rW%>4gm\~>endstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1197 +>> +stream +Gb"/g_/c#!&;KY&MEN;S.7QW0398H;M:53UBjr04Te=HKZ1/N!g;gB"0d*K\c6BaB6ud%(/:%!QpI`>P"C1I>ea%Mhn88Q]Erq$3neMLP(b@nfflHY#6Cq4ulL9U!"9KW(,iXs]SsX['HE`i!VIVNRu!H-8L":T71'C?X\qu`i+TPLMDWH6CAtqn1.%"0Gn8(3RJ^5N=ogg:&g5VF"[jOT'*=nYZ[dl+:b%S/17?a(^d@%6@2-Qt&ji2*m>2)u+8[pKldcJ4-=ZLAA.lJ@aIE`1B%lPhMG1EZW^_0>f/4+"dU&jb37)!^5[8g6kOVNbT)Ko_(OlJXhs5$CjVtYYX)8GF7Eq-KqS0cM=M4r12osBL)1U>87?MDMXR$\:>CCXdS33:2"O,hp0W^!DV*Qd--mr8O;)+!PUjuq9q;d#!OH9):V.+c=orR\/V51TGN)M"o0M]0e9Nse\87*8)eFK'(i9LQaOI"%"Eg_:0CiYe1M$\aKh3L5gmP]Zn+*[2RB2;jXp/CL^^2a.70:ta,q8rD/1o=q1Y)`"!5gGaW7+AM&3n0;$0tm*:@`GtZ&-qN;0QdHOWW'FnWO5bFLEPa(JHLEfaM;uGHL'qhP(/PK"Yc+GSmY%Wgluj.fQ95gOEeWqG)"+:I4QoJ\@5AWJ!TgPRi`)Go"Uo9)Q&V6Z"BI3G2!l#K..J^YPH8YR`3T4CP)H&mXfK4H9j$F3Bo%cZb.;p%s;/X47"]H11=YOF'PQau$9Na&\_3>+%(/.jbA\jT7;\i&00P/)MCBRS#*=tnAIi(k!:Z`2d56=d0THP&hkX!QlA3Ci/,O7YB:h2^-;BkEmcp1u%)H,S[6-ql%4W?b=*XcKoRbh%K*V`1^bk`Z57#WNBkMqE1tI(jqh`%*m=.LP!5IgeF72o\IDY%mkN;O_~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 957 +>> +stream +Gb"/'hf"u<&:Vr4EK=t5&`7"*Id"\Xrn+)J?(3t=Ke^'28r*\YgRl5:,Qq#rBEh2[e.@5WB-6#ZYs6Ee+S`h=2d$0oS&=k"!=@n4JC9W8rs\)^anH*ZH3VMjRP[?%JVREC*li.oZRn'(;ooUH)$sX`jS+js`s!)dKO,AW#i8ql\Uh@+NbP[k(LQiX3=qPSoaN+aT+L^g4K(l6+KG"-BA"a'[g>X)^\&^cLgitW'hPME8!b<0E0k2,&f+"5_@OT`dO^E>@K$8u'DmY+^-i]G$Nbec!^l3$L!gRVJgfE$o+44o>sOo7$(.MW]$?&9i!*R(VcfFVm$-[$\UMau+att`F#kL$^'h*Q:MBiB+0Xto13eP41dPsA:OGB#]P'[NhXgG]iq`)=>'0mIopT4(N#S3#B\OK$EJn_HBUZJ%It*c9Q@l:8t-3(3&&isa*mM4,EA;l17A4;7ubMKlA7Aep8]9,P^k4!D%h4&(*E7gZ1J$T4i3c-tAAc-\=(55nZX;nD=s2:HQAIPHR%ENWb0$m0i/JkrhemfT#o>E!%ld&db-$ar\4E/1o?0B.5b>,I7q@WKh$)R[.`OoH6Z#tUu9uNJ*at@i0B%O<&I+&R9lO%JF[OO&D>E[B2X&pUXeX@K]>8V%cfPFfuPE.R?(Nj+fZZ1gS%'bGY4"0_27+fendstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1034 +>> +stream +GatU2gMRZZ&:O"KN;CtebHghJZ4#Xq6(s*-Tm;Sr\R$XpuaWa+f!(LN$?aLI^SD]<#hKWI)@IaLI:BL;MHld#Fs1Nu#'G?lGj4")ZNK9LFBM-9SM"O+[0f]$2RqYp!8q;+`8W-p]]Nchd.u^-dS[?ZrofRS(>sILEqdtUQZSJNO4?!%T!i89$_38-F?7=K`mHuXaSo'Xsec/[(7oNTUpQO20kaNHT/Xm-8NbFlugPb[fl+TSs,<@2bOu"J.#OZ4XTFoP+Z?qlM\&^aoANjFLrNbnXbZ`D#WrW(cQ4(*QG,1"f*n@EpdimZ\m7=q*GhAGg3Ua<]%`H[t.NVrOa:IYI1a`ZJHg)@cG/Z&bD6JPRSN'*\Y*J.rZHcZlUdO6sKB"c0(BW;eV*RgT'SG_3$fb]]nE(?P_u(s[iseSTI_i)c$@V8Xku#=E"VqptP(,8k?7_]0dk%[&`PJY-4&!nu6*E1_R>)BRVFK=W.fM^ERjM`"=KK$*BQi$XH%dA-6N7WiKq!D&]1;Y2_=*(-b3!r`HC]h\3_>d#69@gY#MQQrIG=E!cs"hua"X`ZuNkVd8/>H?l&\lC#;Z0%2>&N>T97MEjhJ)I9p$mEHakefU:*MdH7$e2IiW<4S(@=79#IFW"HA4c+`*RNpCKlIC`^Apaa,D'W\*6GSB[^AF,r8F--cl^L%4Q+idFP$>mIYK:p*`MtD#?8>l$*k+G`@)`W32;cF:Zd\nKQ%*i8?R6?SL9fkiWnGo2@$."`Km&_/QtSnjE%-#E..F`WQP.[,,BN1eYukG]_f:W]0Icf;?qT8(al,A@%aHU,gS6OU&^4&!Om&qIf~>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 +0000002451 00000 n +0000002556 00000 n +0000004650 00000 n +0000006591 00000 n +0000008315 00000 n +0000009819 00000 n +0000011026 00000 n +0000012315 00000 n +0000013363 00000 n +trailer +<< +/ID +[<05bdcda7816aec63d7ee9027f38df985><05bdcda7816aec63d7ee9027f38df985>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 14 0 R +/Root 13 0 R +/Size 24 +>> +startxref +14489 +%%EOF diff --git a/output/rapport_timing.pdf b/output/rapport_timing.pdf new file mode 100644 index 0000000..a07c091 --- /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:20260421103114+02'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260421103114+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 1972 +>> +stream +Gb!#]?$"^Z'Rf.GkcJQ3i^d-XG2E']]@h&LQB4:AL*=CTd)A8cb\mIZ9"$9D`3Ook%26P@lJ-IQFUs3LX<+0`IOl',0LckN#pOo>:Pc6.U%)2@*"Q?[`'ar'6j=MpP08Mh,#eG>1Tm0*cNAkd/?%i?-pU$CeURWtu3N[lKo=W6C1m(>T'`<6U^#D7Q&ecKSp:E;m@Lm:L:ap_Oj`6g$Y!,5OYi]#`;N8s\%=$-Faf("]cFpQUlU+t-Nm,XN&s3#59#KLdt#)+oEV67#7r1N6&Rb9X&j/t=8Y;2WXb&^/SN]0VUb$j6'-RP(>@Md!j[dDW9L@Gq2gHo,ZK4qG&*:EO8Z3Y+'AVV:RuY'DmucAE0JR&++&B@hJ#bK]rrQTK9WKL2+R9X(tGcflY2D4G!XamJK&='8[cEO.,92V"3L>A^k@I'Z9,0US3WJrK4R(kd&;WX.8'ZCX8C.`JF[[(@D+PQb#H7]$WA0+(H\F#mgUrA(GK0:)!5gR*u1e'0&_=Do-@T0?56pF)WQ&$lf=tI[_BV>*$@7[MOq[+/.E21A*9]_:$I/DVC/PXIlLkFZNm.D7&Q[Xs#u.`:=d1UB[+8DQ_+eF961t@7COJ-t_-8X$;cKB2A&T%a)T>X@X`BPA4Ck]f%07fp0#8.(*>le16D7-AF&oNm--)aNDt#O`@;%#]eKFe,u#-WZnsc;l5=C4#OuHU/Rjp^B(1)qTTb*W>Bl/5O;#B,h0dtfGQ)/Ua4AtBWliEff9U\[Y_L>EZ_HVMmD+VkOX(H&e3<ZRZNjA^5iTg\O-]Neq,"E6,R:@9"1M%Ec:\2WkCJ/mh#6P4qEN;A:LIK:K*lbu\0mXfA7U=nq8n*F'O%KfXPqtNK[t-OlGJ.8<)jg?0.s?jI4?/Hk^PbcO13Hde^M4QV)],JG[;3k5j^Bdi"pS>."4Z@1FZoI--9)3nq$*$C##k:$;`G!1f7R9GRE$Le8P"npCaj=a!H/9@00aU-qq7J"!2?'gb'>l]bnpVT-W$=@_I_)?!_I7E;>7M(l`'WoA_[9c0.o:cirB,/$ihT8$;_fk<9Q7Q2q<9DV;#7#I\tjloq?Oj.hB#A8+hFVgT!D`gDaTs$E:ba"h$D>eLbJcB(snK-5'Y@'`q_s*df5:h8"iON9-.^s?((rtnL#_iK#ZX0;K>@Jd:U4bmVPCh2a1M%snQ=+:GlX0QS/+JQs^*hg^LM/'>YYMA1h;)U6^8e;XK\F!lsftcA$TAT'+PO%s%GIJYa=6"a8[Tc^tNRY]g7"j7GapmL,]r(-cPMF)C3ng5fdVG0!.n(UkS=ZooQ3GU8$G03rN8%RM_2!mU#-=nANRhmhu;C&%EGcC/tendstream +endobj +17 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1647 +>> +stream +Gb"/(>uTK;'Re<2cm.odWkN'77YBs&b=K=C1&'nq?Kjp?*%3$)7J$P___L>+f$ifa<2S3#J+2KnCo`[TK-C>`KQWI5uaRB;>d:jp.'-=(gJ[Uh7dj5p641s4brBg`2rp.Id$@S*0CfqL#oqN0"k(5#L7WT]5DVg_5,r#jYh1r!5O!L.9^SaY^%:-bh@.Q%A/)`J0\T*.H$fQfCrr?8.(NH`"E`$ia^XpV'DT/K:R=#`lZp'h(t><,*K0c:K;)*Vq/0FJc(Lll]O(l;kb<=8?9GCr/Utl-H;2S?VKm[cE1OIgG^6*$1NuF>Dsj?*%b&pFq>sqeYFb3?o3br="Wq_#0>"k]g4/!L3+?(DdphosqI,%;qAZU&ZgUX'mR$acJ3l:`t81=>1g#R,a/pnPp[h,IY(l$Rc3*qSj96!+-pWh$[S,MXu-0T-Xo&n.rK%fnPANA/9\C:n\IX+m599;sOE^TqU9qK5aRWF&\CaP0H\GuE90D>T3Klup;>NS-E7!+kmhQfU(aq=h,t9VVkLaW0ZCj77c-7WWBmNB>&;QJ8,PP5SFt3RQ-6MfbOKP[IEcddEqc\tl$NAH:ZYAGfXoS&XL3G]M6G*&'iG"]h9kK=!n;Ee$@OfoiYS[n?u7'Eu:HW:B]:31_IPalNA9p187YGF&Q07Ar=6?88;Zo`6re:lDdU[*kVcnN`3:]SJeD?VY73LM)qij67iRXq$0iWrqDueH%R)S`&/$5[RIq_"]0=NR*cGfr&P3q'E7f21;D_5akMZ!j]Ko=nSVVJZ8]P2!3cEl/Z[#AhEsd<<].Ld'q7u'+NL!oat(&Ff6MQq;H!K8Skh9dqu`VZsC:;lIL+YCsdDj^"sC'grqm7lClLm7O$`u/6mV+UJ.(bZZ=-dJ).+Um^Vkibk7TJ*#jEA[7#\#M?8M#LHdh*fSh,bC^DaBB=]P'!h'.fYbiNi!*$sqFqs&mG7o3.Np;QJ:jr9!IiB2Af"hWAf#\C+Y+(RrN*Z)g[ii0g7gXD#-+a%QB7;3iHcAeHJDYSe_k2*n#'Yod8Gp"o8b6(&FaTGtda('pH';Ns)YH&R9~>endstream +endobj +18 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1651 +>> +stream +Gb"/);,c4/&:Vs/cm.odWjIp?M`2PJ00KTS@/@f8-:\0%*W[m\l.s*=fp62285\`+*cmOeJ0XMt[C_T&T=tV#7"8n?s([D;`tWq]VMG:GY7]l`U\"[8l/9kg24B]d3/T+SOu#d3Ui\gQ`el8JW;TiS%.83b(*P:I(7[+d:Lgc#U[.[.@/_l1qQ3,='Otkmj-IWXn-b1VqK:m87j)j:'((O"./f1SpZi_0(59M(nTZng-(,eqSW:]gR"DphNFN@ulOGl%k1N@]DJG:e5W]LnlPoI.;U8J):K5&FD&>UuSa.)8=lf$g_hq%_4gD2uL0J#gY(f3NbW&uc=kKbn[NCKSOg84qICDP!<10XhC)^3t">@*FMf$@fIk*_Ure^\G9(ti=;&kL]jnm5r5VOu$H34MF^HBh%#0DSEmFkM%fVVY6c:7h&)XN/bFh"WXWrFRAf+-0>\^IoSlM=Xc9ed:98H4gA3#7GA^:0TL+V[4EQ/`tXIOjM$qk54!P?2dB$7?hVM?q37?n)lqeEdj#%US;N\6`m^HZ;sC6TePhThe'PE]$M)aGP"-[["X<9/S0"3/s;D>$DbLSEc\mWuEP]!pJqdb,)C[>U83Xn&:3;0JSVDWe]<18a8uON266YbD]qI@8p8+,pNO/8XF"0beaLSr1PWo=qT]Cm!t#Q[aeLU#KK/sL1^Z+.?V0;kUI-B'P?jFAf5<'"AMBEu8rn$9q*ibFc2mX)'5@h*%YeI^,_(WC2qp%?+f@^MU0]NdnIC,LKEoh*!(5WT>^jeDQqH[f$H!=l-UfZ$UoZ-26LQt:TlkNM2Fq/filK:Yi&p0)U3J`HYF_rA3(W,;Yg'o*o9M>V*W2tUl(b)cB7R&c5.kBU"%RoZ>GnYSi9XU&+70lL5-Bu`SU2cRs.T];`CLsWamA\N.O\D\kuI9AM.-/U,ZsQ.uc/(mgTr_S''3EIhVpn/=D4"5:!:SL7t&nUqTI1Q3>M\Dmnq".l_%62)ik+[+c>W1hi%#s0n04.[#^T6\:ro^VNu_K?R,isBol+klI>]m7JbBq_d9D*Do1$*suBbnNX[0iWZPo\kf>7,bnYet<,O#29f,d#1tXcEnM&%'-fgWRc]#l[0eO50]G,&:=02$iQ=O:&\T*%K6Bq:G+L~>endstream +endobj +19 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1644 +>> +stream +Gb"/'>>s99'RnB3cm.odWpU\[BJA7a\TEJg'/Q%-2,(RafO/<8s8C)!aUXIKL[CC_9El.,HoaIS8CmV/@=ZQ,QiF7U!.7JVLLU`H!U1'/$o3p9J0Z063PmXO,,g11$j`[Qej>jG`XF`RLk+pFl?78f#&+2WViW],_*1n-Q%>G!`aSD:+d2UCI82e\3i-;a;ZlTL5\93b;HH@T?C$Ar`A+ikl7a?'@9nIJnd_JQ62H$N+WrG]AqA!dnLr"#I"!tKVWhMM3Bs!]SK&sTiE.O"3,t_HP93=@Kg]G9OB7*+5F-@%.0@T"QCLl_#qcIkSW<5\\RRKF<&^$"#)T!_%=^\Wk&!'I5B/;XXXQLo\4"?\a7>b2J()bCi2SNX=RjTQrb=QULNbpgQd[*e8\UHGdha`bQ'.jFShpY@G8ASZp*ehD8g/IIUKt1XdX5^k,Y\)Cumr%G&hJC._K=P_*B#ea+bZ/.,qeXG`Xr;rG6!FP7PIrTH]P=1JMZBB"Dg?9o)DU"^D^%U:S6LAWESIURC\$a7=$ZENCH=l%5*qN(@k_aEYqfCWMfErN2*)%c8;IX?g&4FH#@f+)SKanAP?tA)!Ik52Y-o^V6rDm@V_q2bu#8@UT_n=]78Y0!P;)44tjR^!"X,q'tGfeIaYTRVi&aXq6he#Z,">;lR'LM1X%!t,!XiG((kH"D&2nuNhc@Gf<@+]X#6/Sb=<5]\*Q:cgaoVkq;Ph`2-RW09?`0n#D$*!Pk/n.D%*g&8qodMWcaQ-3J.]K>ZYMM;SmY;E%"OhBWP_JWHI%M%WZ.6,>/cCEWuH)AcgqVY*$gL-k3S!NA^K8>qD9's-BjYmEnE)l,V%FZ\2\Ho['%*_opskSQhpsn@Wp6^oa.[I;5(rQ_c&L!jr5XVg0rC9/96A'Ze3?h]h\!R8n3#Y=[0ZSotF6Cr\(rendstream +endobj +20 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1706 +>> +stream +Gb"/'hc&8h&:Vr4Th!fSFOo`D`>]8(0>.E5Qp*6gajIo"7t:e"^&.7o[UiUe=]Xn)bRMI#+E;StmB1VBIJsM0oSXF`Y>tpW8C;>t#pOoJCIB"O./ea\FS'WBN2HY,'I\R/UmQcKHJRedeNjc4n8f9Yk4QtiLEJK)7Hph.W/?+eLYFo%UmtuejXkeGh^fhr_)mn\5jA/.Og@M.)<"['_N:GK`53'_"b8;F![L:'MYog_r@VF^!\pjaB9]-=&u5+$?,%Ii`DiWL2;Y>Z;;ATJn!&8=*u4H;Pn4)[X=ku=/Zo+H\r/"l!0G.3DMm:sVf<+W_K5;Um=!MJEZ4"b$-\5=b&L2VH13eW36&bMnXY&7gh"[pjjcIthVY`E!^23_eu5o+q)_g,YP_$AO"mJnFI&G0oB[oIgtcD`VmCFpPVb'>YRT1Rj(/(717KWNMe)CTHTUR4p))G53fV5@C+HcrpO@Hd7T(*?#S^hjbG*"S-f\a+*i>.MPM*Fe;:8aA>obKBH^l`!4'Bg)EI$.Y*&#HG(ij^9,N*D]FBAEKMhas7in\/p&:'qbFeIbNtLN:cBb-JpZ\RfVWa@0KNIh$P[E:Ao>23PL8aWbj>]1Q*h`G\G"qh=19&8;mNMYU+VP1,"5SphS&WabnqrAr.8WFAU78dZnfc5Z3e7oEd87q*"jlUDXug..NcpN7^i'AFjS4-L*r[r^:.E''Hsi3q+h#&ZMp%IHQ!O\K!a./&caXoW'QLF\rbrbRZ`_[4#7JJ'L[OVS$C&Gm'o#@1>O!:55*RrI2okhAIEdpLr@9#E4;8:r!\hCf?:__/CYBq91b!bM,Zr,seC,m-S:dH4RF8qKtRn)62gp7[SlSXrjo&Rp&g^lEU",j"IYL]u8_#(i.c-=\>"9*gFF`qW^-UmrYl$V-9qpd/q(_<+=NqaGq!\6H[K;8j?Nou$/uh)*&ka,nXP(A\1.s*nFa.Vg&6S;;HBSZmAAZqF'DfY'2jpm=`HX,*Ya:G05U?gGC"Cbj])M8.5OifF5.F&-XDabE]?,^l8SruO(_kM,==+GKT=ngQi"WoIKWe)s"=gX3$1Y;]TH"X*n.J&2&E;XB<=2mI[SmM:F/oR[:$l+iP=kY\A2j&hFKuR)#=dC'4.'"@;>43MYCmT."=Pqe:)p<@2q>j:#'R:!T[tc_\`$3!qeriRoKe&rEn7PTdg@@VCAl`^:Ctendstream +endobj +21 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1656 +>> +stream +Gb"/'gN)%,&:N/3i6fZ*;;HVl<-qP9A.'G-P0-?njq/LWf^!9*!hEi0Uni?inblJ/Ml?4U+J=qD,o'.1oNG?j7n8&8Ru!SEYc3$77Lk5$'\<4!Vc`@%9R/-jBKMJQ2oW?Q5QTcU+Q95pCUa/g,g>>7IBMLB(>#6`_s'A*j[0N.Q-Xp6*rUHWiSH!pO?Z^:'tL-@/)e`DB/XCCR3m4#eeMV9k^dqlBNep';M_gPg""Cc8^QAIe;o]?3N^CT&tqR*/smFYiE37n%[nq;N/PP`+Y=S*@@9/igIAhH5j^WX/]f-p]*`OG`&:,n]B54f4>RC-?;D'MQ/?6>Ns-<_9#X_]-^ZddY!!;B"C\*5n_=M-aOA0Rtm5dEs#fq(Yu6[&iaF4,9YrEb(K^3UEISdj,*G:!eu0s^r11%R=8a>-.nD+PT#c1@c_#4ghVM*!n5O(8e_nM*38iQ5pt'%79u\!kjEPo1S.'b'bWoYBl@D"B*VB?W*g;0)(.'3?3FLsuq!<*m9[gn$EcsM@3&sTch/h"ApDiae&/k#a@2CkHpT(B6GliOKN^fO\^uRI88P[b+fnX9^nM0$nPDVEaOR^s`BRf@cXiHF2jacO9qm24Fm,[[.idSHDnCoC1sSE]PGOScrC]kG34D\]0_5bT(XGh^h+4dOjAjNqjqFL,MV8JMQ@bE!,W4&4a`D_gXXBT*5t-Yg5dpL_6592`8B)EUjhZa0slZPj?aL+l/)d%V;i][%>2MLO0Uqg7QI(D:2RHeBAT*"#^'I^f\n&aTP0lN1co<(\Y[aM-UNDU]2;`T!u&DJLP$4F9!k$tK)%krMQQ.79[?OfOe.;a.:/38^rP&a+S0oanlHc!b5=ef:Qm&^=a)gJT#'ZiAR6102pgYIohld:.lHh\9iH";.o"fKM#rT`ZJK)""=B[`*%njeff&gWBpThD%A=0XdAe-F(`CH!42akkmW:he$)\A6%S7_I$:dL5hkU>rs+0"^BM3C.2U*$f;341+Y(-lGr$P-&?5l67/b+KIAbB$1Eh^q`rec(^)$U0Z=bYsMjEMQH"'\"3b((#0=c+L<)Fi%FTn:+[jm%/=23oLIu$)@/2LK(l@i=Ag5N&p\HQO0E.mE1)H>F?$Qo;450ATLs^Lb7P3'FHR]T)QtbKlk"#TjYSN]@pBNmf+l81&3pX.=5ddZ7ome4%?.N^0,a,?JqHL/7pcu-0cQ@l]hRd\L9q!a3.ZZJENthafeggB'"R"4hE`mgkYC8d(oqsR$YC!CZOm-@.Z,6P=WR~>endstream +endobj +22 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1659 +>> +stream +Gb"/)>uTK;'Re<2cm.od>$l\\if+/k9t*AHQp*6gajRu#97R4&h>R'uq*UI#A]87CATomIl6n^)\lVtE3^-J-Oo@WFn'qc=G:ECM(_@7C?6`jFU%BR%da>N9PfPi$;@$Z$`.1,[V8qhJTQ8a$^lV#B/nb</Kksmr%Ttnh%Y5)DZMNVVg1JFudc%Sb*)sH\so/Y62u?R@7aqJoblM/:>_*d7Gm^c'VSN0-Z7>Brl#p1dpX2mq)5.s9<'<]P&Z8"*WX`;rS.=AT3codh3crP?hjhBKS.JOKP]=Mt.d)9,5BW)'":IY_rZX>a,Sm8u[KF`I-/`"&pf()WX3$>mUT2eQG@]-mc'Pp'+l@YP:?J"jfS[u]F6:=bU5BM8M%h5t#ZG0Gf]l"2.k"Fl7=%ejnLn,[I]G-3+oFe7?OmoM*mkgGhq;HEJ7EKcLuttL."QYm%G0P1mfs@?Ur7p"#fXt[b7;egnIsPAN(M3ssJY4Qu#\na13_lR:@6-D1SUI(L-/IP5dD><'nrf(o&>-r(b[obfT))27EAS6&"XY=TPN4P`T7*.T(S:fGfhoTD/.siIb-T_Oe=SflX*FeU=l=SriTt#74#oF:3$1pDBm3girD6`7\OFpt2NTanSQ[(jc2ropH$FEd3M""1Z_=GXm&4<2(+DR1ZcJ0u>URoE]7W<%6u!O7<3f2nB6kobg3b8L1j=?k!T[e]m/:%i+ib+#Y/<(no0Fa[J1dM3PRlgd@/r]_pAl3W%="7WdrIO8%9RKq[gg7BCc5\1S_l?l[M.BA9"\+(!Em&ojt@s/1t^d(0G:PB^?ZMSHe],9U,BaRn'lmIShV2dB4GVDZ)W=4W4dfXfkcR#8rVS?F)Sc*a-Arn+]9%J.eh9Q\sW7hQ"s##AQ>h(IS;F)V3Ulm2MkrFLBGRni0oF`5Y6I<*+u-[`0ofM><_+;A]*S](f-lRs(J;F(PdKg7sddmF!poC_@$6#ChD'CKtZf<,bgO]Y-S0:`1X@jh@B67ISD3qiGDKk`]K:bXhcTDq,rau?_paAJdjk;#l[2uf4X;/Ln%K`b`MojNcF5OYR$aoLh3!]4"RHG'bbp[k-+5NK^Q_u~>endstream +endobj +23 0 obj +<< +/Filter [ /ASCII85Decode /FlateDecode ] /Length 1231 +>> +stream +Gb!#\9iKe#&A@7.kQoC3[D&d"+Ll8:"Ib^LR)rl:_k^!4"D?Hfqb"<([]+,+S2q^qjaC=GVd4(^6;V2F;Z'n'&NWS4qi:jlMUHi>39DX&6qKFa*Y=*+&WW\5*(ckYd8HHZ`f.S6W)^3ph\g3Ji*/f5@o1%^LEBUb^m'B@SK*lE7B-KLT;YidDu^pB&QN`E-[J7$\RZf.7YC's>rX2q5qRt29IV'mr?93kfI9-78eL`e*OUSIG:p5THBTmW'M=4o<#J"ZGP_/4L?qOidTV2,1SA52:2CBse`%!t)-B)[n*3!TD-GS:8D8FErL&[g`,ht_H0bg"VmdIij1I:;+!*@P8.O>[kDSas5j;`UR]W1jJa_9%^:[f*k4s&hGR+Q;NlLs-K-W;i75Pd812sBUgQ)@gZRFLEZeb>Jm<9VG]p>L+d[[EtM]r[@'C_9j_)R;\.o*148COe]^O.(e/?<]t@$sS/F-8Pdg.5>t/)1ot0QFb0H]h(.Bk_b%.E)78Qd9n(H!j&XjX"E46h(MO&/9frKJ:Uf![CE5o/h**TC@a[r(0!k0d9?WKI88P/JS'\d0?/G3$B)'JQ.n?RX4l2'c2f5r.!iT7SGmg!!0]C^<6uP,e5Jipe5-RC>[t0U8]SC1V_A]m`ZeEP1`]UU4AK9FIj&j;ZSL2P?'dgtGF#UCMgrATF&pe*642P2(KoKW]>;&dLr?Kea=7h3gk5="=?X!*Ejql1rkC4K8MUEGnDrMp?(g/p/]Y1YBj$I5glcsC.$\`0YJ%lRBLc3mKrAXr.:lJm)]0P*3dGQZ`qsBg~>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 +0000004628 00000 n +0000006367 00000 n +0000008110 00000 n +0000009846 00000 n +0000011644 00000 n +0000013392 00000 n +0000015143 00000 n +trailer +<< +/ID +[<01a4daea9a4820dcaaed648811d43bad><01a4daea9a4820dcaaed648811d43bad>] +% ReportLab generated PDF document -- digest (http://www.reportlab.com) + +/Info 14 0 R +/Root 13 0 R +/Size 24 +>> +startxref +16466 +%%EOF diff --git a/output/timing_stats.json b/output/timing_stats.json new file mode 100644 index 0000000..8e7de7f --- /dev/null +++ b/output/timing_stats.json @@ -0,0 +1,782 @@ +[ + { + "fichier": "23_20190411093049_00001.pdf", + "debut": "2026-04-21T10:16:17.260420", + "fin": "2026-04-21T10:17:29.584121", + "duree_totale_s": 72.32, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.81, + "duree_extraction_s": 9.64, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.72, + "duree_extraction_s": 12.27, + "statut": "ok", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.93, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.67, + "duree_extraction_s": 9.45, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.32, + "duree_extraction_s": 6.83, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.86, + "duree_extraction_s": 5.2, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 358.pdf", + "debut": "2026-04-21T10:17:29.587640", + "fin": "2026-04-21T10:18:31.688689", + "duree_totale_s": 62.1, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 5.21, + "duree_extraction_s": 10.65, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.56, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.73, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.32, + "duree_extraction_s": 9.14, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.29, + "duree_extraction_s": 5.44, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 5.15, + "duree_extraction_s": 7.79, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 368.pdf", + "debut": "2026-04-21T10:18:31.689115", + "fin": "2026-04-21T10:19:34.838029", + "duree_totale_s": 63.15, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.78, + "duree_extraction_s": 11.69, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.44, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 5.0, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.8, + "duree_extraction_s": 8.83, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.91, + "duree_extraction_s": 6.62, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 5.24, + "duree_extraction_s": 6.03, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 371.pdf", + "debut": "2026-04-21T10:19:34.842501", + "fin": "2026-04-21T10:20:34.886910", + "duree_totale_s": 60.04, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.78, + "duree_extraction_s": 10.63, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.39, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.4, + "duree_extraction_s": 9.22, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.72, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.52, + "duree_extraction_s": 5.25, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.74, + "duree_extraction_s": 6.29, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 373.pdf", + "debut": "2026-04-21T10:20:34.888041", + "fin": "2026-04-21T10:21:42.319224", + "duree_totale_s": 67.43, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.59, + "duree_extraction_s": 14.99, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.31, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.53, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.38, + "duree_extraction_s": 9.24, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.76, + "duree_extraction_s": 6.26, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.38, + "duree_extraction_s": 9.06, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 385.pdf", + "debut": "2026-04-21T10:21:42.320662", + "fin": "2026-04-21T10:23:30.340651", + "duree_totale_s": 108.02, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 26.59, + "duree_extraction_s": 11.52, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.54, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 5.1, + "duree_extraction_s": 9.09, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 5.2, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 28.73, + "duree_extraction_s": 5.33, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.92, + "duree_extraction_s": 6.12, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 397.pdf", + "debut": "2026-04-21T10:23:30.348601", + "fin": "2026-04-21T10:24:30.467269", + "duree_totale_s": 60.12, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.81, + "duree_extraction_s": 10.82, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.59, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.5, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.56, + "duree_extraction_s": 9.41, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.51, + "duree_extraction_s": 6.05, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.36, + "duree_extraction_s": 5.15, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 398.pdf", + "debut": "2026-04-21T10:24:30.468566", + "fin": "2026-04-21T10:25:35.618130", + "duree_totale_s": 65.15, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 6.26, + "duree_extraction_s": 10.86, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.25, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.48, + "duree_extraction_s": 9.33, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.37, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.28, + "duree_extraction_s": 5.83, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.82, + "duree_extraction_s": 9.77, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 404.pdf", + "debut": "2026-04-21T10:25:35.619092", + "fin": "2026-04-21T10:26:43.866215", + "duree_totale_s": 68.25, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.84, + "duree_extraction_s": 10.5, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.18, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.32, + "duree_extraction_s": 10.8, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.72, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 9.48, + "duree_extraction_s": 5.92, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 5.17, + "duree_extraction_s": 7.43, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 411.pdf", + "debut": "2026-04-21T10:26:43.869120", + "fin": "2026-04-21T10:27:44.984002", + "duree_totale_s": 61.12, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.69, + "duree_extraction_s": 11.2, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.68, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 5.17, + "duree_extraction_s": 8.66, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.26, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.64, + "duree_extraction_s": 5.44, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 5.62, + "duree_extraction_s": 5.91, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 413.pdf", + "debut": "2026-04-21T10:27:44.986082", + "fin": "2026-04-21T10:28:53.878912", + "duree_totale_s": 68.9, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.64, + "duree_extraction_s": 10.82, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.39, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.42, + "duree_extraction_s": 17.8, + "statut": "ok", + "erreur": null + }, + { + "page": 4, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.72, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.76, + "duree_extraction_s": 6.3, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.99, + "duree_extraction_s": 5.12, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 425.pdf", + "debut": "2026-04-21T10:28:53.882864", + "fin": "2026-04-21T10:30:01.999566", + "duree_totale_s": 68.12, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 5.33, + "duree_extraction_s": 14.5, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.57, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.36, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.8, + "duree_extraction_s": 12.04, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.5, + "duree_extraction_s": 7.19, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.41, + "duree_extraction_s": 5.35, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + }, + { + "fichier": "OGC 429.pdf", + "debut": "2026-04-21T10:30:02.002040", + "fin": "2026-04-21T10:31:14.505352", + "duree_totale_s": 72.5, + "nb_pages_total": 6, + "pages": [ + { + "page": 1, + "type": "FICHE_RECUEIL", + "duree_identification_s": 4.87, + "duree_extraction_s": 21.76, + "statut": "ok", + "erreur": null + }, + { + "page": 2, + "type": "FICHE_CONCERTATION_VIDE", + "duree_identification_s": 4.34, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 3, + "type": "SEJOUR_MANUSCRIT", + "duree_identification_s": 4.7, + "duree_extraction_s": null, + "statut": "ignoree", + "erreur": null + }, + { + "page": 4, + "type": "ELEMENTS_PREUVE", + "duree_identification_s": 4.5, + "duree_extraction_s": 10.16, + "statut": "ok", + "erreur": null + }, + { + "page": 5, + "type": "FICHE_ADMIN_1_2", + "duree_identification_s": 4.88, + "duree_extraction_s": 6.01, + "statut": "ok", + "erreur": null + }, + { + "page": 6, + "type": "FICHE_ADMIN_2_2", + "duree_identification_s": 4.92, + "duree_extraction_s": 5.49, + "statut": "ok", + "erreur": null + } + ], + "erreurs": [], + "blocages_429": [], + "retries_total": 0 + } +] \ No newline at end of file