feat: découpage PDFs multi-dossiers (Trackare multi-épisodes, CRH concaténés)
Ajoute une étape de splitting entre extraction texte et parsing. Chaque chunk est traité indépendamment par le pipeline existant, avec suffixe _partN en sortie. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
124
src/extraction/document_splitter.py
Normal file
124
src/extraction/document_splitter.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""Découpage de PDFs multi-dossiers en chunks indépendants.
|
||||||
|
|
||||||
|
Certains PDFs contiennent plusieurs séjours/épisodes :
|
||||||
|
- Trackare : plusieurs Episode No dans un même export
|
||||||
|
- CRH : plusieurs lettres de sortie concaténées
|
||||||
|
|
||||||
|
Ce module insère une étape de splitting entre l'extraction texte et le parsing.
|
||||||
|
Chaque chunk est ensuite traité indépendamment par le pipeline existant.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def split_documents(text: str, doc_type: str) -> list[str]:
|
||||||
|
"""Point d'entrée : découpe un texte en chunks selon le type de document.
|
||||||
|
|
||||||
|
Retourne toujours au moins [text] (pas de split si un seul dossier).
|
||||||
|
"""
|
||||||
|
if doc_type == "trackare":
|
||||||
|
return _split_trackare(text)
|
||||||
|
elif doc_type == "crh":
|
||||||
|
return _split_crh(text)
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
|
||||||
|
def _split_trackare(text: str) -> list[str]:
|
||||||
|
"""Découpe un export Trackare multi-épisodes.
|
||||||
|
|
||||||
|
Stratégie :
|
||||||
|
1. Compter les occurrences de "Episode No:"
|
||||||
|
2. Si une seule → pas de split
|
||||||
|
3. Si plusieurs → couper sur "Détails épisode" (ou second "Episode No:")
|
||||||
|
4. Préfixer le bloc patient à chaque chunk
|
||||||
|
"""
|
||||||
|
episodes = list(re.finditer(r"Episode No:\s*\d+", text))
|
||||||
|
if len(episodes) <= 1:
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
logger.info(" Trackare multi-épisodes détecté : %d épisodes", len(episodes))
|
||||||
|
|
||||||
|
# Identifier le bloc patient (avant le premier épisode/détails épisode)
|
||||||
|
# Le bloc patient va du début jusqu'à "Détails épisode" ou le premier "Episode No:"
|
||||||
|
first_episode_start = episodes[0].start()
|
||||||
|
|
||||||
|
# Chercher "Détails épisode" qui précède chaque bloc épisode
|
||||||
|
details_markers = list(re.finditer(r"Détails épisode", text))
|
||||||
|
|
||||||
|
if len(details_markers) >= 2:
|
||||||
|
# Couper sur "Détails épisode"
|
||||||
|
split_points = [m.start() for m in details_markers]
|
||||||
|
# Le bloc patient = tout avant le premier "Détails épisode"
|
||||||
|
patient_block = text[:split_points[0]].rstrip()
|
||||||
|
else:
|
||||||
|
# Fallback : couper sur "Episode No:"
|
||||||
|
split_points = [m.start() for m in episodes]
|
||||||
|
# Le bloc patient = tout avant le premier "Episode No:"
|
||||||
|
# Remonter pour inclure "Détails épisode" s'il existe avant
|
||||||
|
if details_markers:
|
||||||
|
patient_block = text[:details_markers[0].start()].rstrip()
|
||||||
|
else:
|
||||||
|
patient_block = text[:split_points[0]].rstrip()
|
||||||
|
|
||||||
|
chunks: list[str] = []
|
||||||
|
for i, start in enumerate(split_points):
|
||||||
|
end = split_points[i + 1] if i + 1 < len(split_points) else len(text)
|
||||||
|
episode_text = text[start:end].rstrip()
|
||||||
|
# Préfixer le bloc patient pour que le parser ait les infos complètes
|
||||||
|
chunk = patient_block + "\n\n" + episode_text
|
||||||
|
chunks.append(chunk)
|
||||||
|
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
|
||||||
|
def _split_crh(text: str) -> list[str]:
|
||||||
|
"""Découpe un PDF contenant plusieurs CRH concaténés.
|
||||||
|
|
||||||
|
Stratégie :
|
||||||
|
1. Détecter les frontières par headers patient (MME|M\\.|MR) suivis de
|
||||||
|
patterns CRH (dates séjour, "Mon cher confrère")
|
||||||
|
2. Si une seule occurrence → pas de split
|
||||||
|
3. Si plusieurs → couper sur chaque header patient
|
||||||
|
"""
|
||||||
|
# Chercher les headers patient typiques d'un début de CRH
|
||||||
|
# On cherche le pattern complet : titre + nom en majuscules
|
||||||
|
headers = list(re.finditer(
|
||||||
|
r"(?:^|\n)(?=\s*(?:MME|M\.|MR)\s+[A-ZÉÈÊËÀÂ]{2,})",
|
||||||
|
text,
|
||||||
|
))
|
||||||
|
|
||||||
|
if len(headers) <= 1:
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
# Filtrer : ne garder que les headers qui sont vraiment des débuts de CRH
|
||||||
|
# (suivis dans les 2000 chars par un pattern CRH typique)
|
||||||
|
crh_starts: list[int] = []
|
||||||
|
for h in headers:
|
||||||
|
pos = h.start()
|
||||||
|
# Sauter le \n initial si présent
|
||||||
|
if text[pos:pos + 1] == "\n":
|
||||||
|
pos += 1
|
||||||
|
lookahead = text[pos:pos + 2000].lower()
|
||||||
|
if (re.search(r"du\s+\d{2}/\d{2}/\d{4}\s+au\s+\d{2}/\d{2}/\d{4}", lookahead)
|
||||||
|
or "mon cher confrère" in lookahead
|
||||||
|
or "cher confrère" in lookahead
|
||||||
|
or "chère consœur" in lookahead
|
||||||
|
or "compte rendu" in lookahead):
|
||||||
|
crh_starts.append(pos)
|
||||||
|
|
||||||
|
if len(crh_starts) <= 1:
|
||||||
|
return [text]
|
||||||
|
|
||||||
|
logger.info(" CRH multi-documents détecté : %d CRH", len(crh_starts))
|
||||||
|
|
||||||
|
chunks: list[str] = []
|
||||||
|
for i, start in enumerate(crh_starts):
|
||||||
|
end = crh_starts[i + 1] if i + 1 < len(crh_starts) else len(text)
|
||||||
|
chunks.append(text[start:end].rstrip())
|
||||||
|
|
||||||
|
return chunks
|
||||||
90
src/main.py
90
src/main.py
@@ -13,6 +13,7 @@ from .anonymization.anonymizer import Anonymizer
|
|||||||
from .config import ANONYMIZED_DIR, REPORTS_DIR, STRUCTURED_DIR, AnonymizationReport, DossierMedical
|
from .config import ANONYMIZED_DIR, REPORTS_DIR, STRUCTURED_DIR, AnonymizationReport, DossierMedical
|
||||||
from .extraction.document_classifier import classify
|
from .extraction.document_classifier import classify
|
||||||
from .extraction.crh_parser import parse_crh
|
from .extraction.crh_parser import parse_crh
|
||||||
|
from .extraction.document_splitter import split_documents
|
||||||
from .extraction.pdf_extractor import extract_text
|
from .extraction.pdf_extractor import extract_text
|
||||||
from .extraction.trackare_parser import parse_trackare
|
from .extraction.trackare_parser import parse_trackare
|
||||||
from .medical.cim10_extractor import extract_medical_info
|
from .medical.cim10_extractor import extract_medical_info
|
||||||
@@ -28,8 +29,11 @@ _use_edsnlp = True
|
|||||||
_use_rag = True
|
_use_rag = True
|
||||||
|
|
||||||
|
|
||||||
def process_pdf(pdf_path: Path) -> tuple[str, DossierMedical, AnonymizationReport]:
|
def process_pdf(pdf_path: Path) -> list[tuple[str, DossierMedical, AnonymizationReport]]:
|
||||||
"""Traite un PDF : extraction → parsing → anonymisation → extraction CIM-10."""
|
"""Traite un PDF : extraction → splitting → parsing → anonymisation → extraction CIM-10.
|
||||||
|
|
||||||
|
Retourne une liste de (texte_anonymisé, dossier, rapport) — un par dossier détecté.
|
||||||
|
"""
|
||||||
t0 = time.time()
|
t0 = time.time()
|
||||||
logger.info("Traitement de %s", pdf_path.name)
|
logger.info("Traitement de %s", pdf_path.name)
|
||||||
|
|
||||||
@@ -41,40 +45,53 @@ def process_pdf(pdf_path: Path) -> tuple[str, DossierMedical, AnonymizationRepor
|
|||||||
doc_type = classify(raw_text)
|
doc_type = classify(raw_text)
|
||||||
logger.info(" Type de document : %s", doc_type)
|
logger.info(" Type de document : %s", doc_type)
|
||||||
|
|
||||||
# 3. Parsing
|
# 3. Splitting multi-dossiers
|
||||||
if doc_type == "trackare":
|
chunks = split_documents(raw_text, doc_type)
|
||||||
parsed = parse_trackare(raw_text)
|
if len(chunks) > 1:
|
||||||
else:
|
logger.info(" Découpage : %d dossiers détectés dans %s", len(chunks), pdf_path.name)
|
||||||
parsed = parse_crh(raw_text)
|
|
||||||
|
|
||||||
# 4. Anonymisation
|
results: list[tuple[str, DossierMedical, AnonymizationReport]] = []
|
||||||
anonymizer = Anonymizer(parsed_data=parsed)
|
for i, chunk_text in enumerate(chunks):
|
||||||
anonymized_text = anonymizer.anonymize(raw_text)
|
part_label = f" [part {i+1}/{len(chunks)}]" if len(chunks) > 1 else ""
|
||||||
report = anonymizer.report
|
logger.info(" Traitement%s...", part_label)
|
||||||
report.source_file = pdf_path.name
|
|
||||||
logger.info(
|
|
||||||
" Anonymisation : %d remplacements (regex=%d, ner=%d, sweep=%d)",
|
|
||||||
report.total_replacements,
|
|
||||||
report.regex_replacements,
|
|
||||||
report.ner_replacements,
|
|
||||||
report.sweep_replacements,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 5. Analyse edsnlp (optionnelle)
|
# 4. Parsing
|
||||||
edsnlp_result = None
|
if doc_type == "trackare":
|
||||||
if _use_edsnlp:
|
parsed = parse_trackare(chunk_text)
|
||||||
edsnlp_result = _run_edsnlp(anonymized_text)
|
else:
|
||||||
|
parsed = parse_crh(chunk_text)
|
||||||
|
|
||||||
# 6. Extraction médicale CIM-10
|
# 5. Anonymisation
|
||||||
dossier = extract_medical_info(parsed, anonymized_text, edsnlp_result, use_rag=_use_rag)
|
anonymizer = Anonymizer(parsed_data=parsed)
|
||||||
dossier.source_file = pdf_path.name
|
anonymized_text = anonymizer.anonymize(chunk_text)
|
||||||
dossier.document_type = doc_type
|
report = anonymizer.report
|
||||||
dossier.processing_time_s = round(time.time() - t0, 2)
|
report.source_file = pdf_path.name
|
||||||
logger.info(" DP : %s", dossier.diagnostic_principal)
|
logger.info(
|
||||||
logger.info(" DAS : %d, Actes : %d", len(dossier.diagnostics_associes), len(dossier.actes_ccam))
|
" Anonymisation%s : %d remplacements (regex=%d, ner=%d, sweep=%d)",
|
||||||
logger.info(" Temps de traitement : %.2fs", dossier.processing_time_s)
|
part_label,
|
||||||
|
report.total_replacements,
|
||||||
|
report.regex_replacements,
|
||||||
|
report.ner_replacements,
|
||||||
|
report.sweep_replacements,
|
||||||
|
)
|
||||||
|
|
||||||
return anonymized_text, dossier, report
|
# 6. Analyse edsnlp (optionnelle)
|
||||||
|
edsnlp_result = None
|
||||||
|
if _use_edsnlp:
|
||||||
|
edsnlp_result = _run_edsnlp(anonymized_text)
|
||||||
|
|
||||||
|
# 7. Extraction médicale CIM-10
|
||||||
|
dossier = extract_medical_info(parsed, anonymized_text, edsnlp_result, use_rag=_use_rag)
|
||||||
|
dossier.source_file = pdf_path.name
|
||||||
|
dossier.document_type = doc_type
|
||||||
|
dossier.processing_time_s = round(time.time() - t0, 2)
|
||||||
|
logger.info(" DP%s : %s", part_label, dossier.diagnostic_principal)
|
||||||
|
logger.info(" DAS : %d, Actes : %d", len(dossier.diagnostics_associes), len(dossier.actes_ccam))
|
||||||
|
|
||||||
|
results.append((anonymized_text, dossier, report))
|
||||||
|
|
||||||
|
logger.info(" Temps total : %.2fs", time.time() - t0)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def _run_edsnlp(text: str):
|
def _run_edsnlp(text: str):
|
||||||
@@ -252,10 +269,13 @@ def main(input_path: str | None = None) -> None:
|
|||||||
group_dossiers: list[DossierMedical] = []
|
group_dossiers: list[DossierMedical] = []
|
||||||
for pdf_path in pdfs:
|
for pdf_path in pdfs:
|
||||||
try:
|
try:
|
||||||
anonymized_text, dossier, report = process_pdf(pdf_path)
|
pdf_results = process_pdf(pdf_path)
|
||||||
stem = pdf_path.stem.replace(" ", "_")
|
stem = pdf_path.stem.replace(" ", "_")
|
||||||
write_outputs(stem, anonymized_text, dossier, report, subdir=subdir)
|
multi = len(pdf_results) > 1
|
||||||
group_dossiers.append(dossier)
|
for part_idx, (anonymized_text, dossier, report) in enumerate(pdf_results):
|
||||||
|
part_stem = f"{stem}_part{part_idx + 1}" if multi else stem
|
||||||
|
write_outputs(part_stem, anonymized_text, dossier, report, subdir=subdir)
|
||||||
|
group_dossiers.append(dossier)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Erreur lors du traitement de %s", pdf_path.name)
|
logger.exception("Erreur lors du traitement de %s", pdf_path.name)
|
||||||
|
|
||||||
|
|||||||
218
tests/test_splitter.py
Normal file
218
tests/test_splitter.py
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
"""Tests pour le module de découpage multi-dossiers."""
|
||||||
|
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.extraction.document_splitter import split_documents, _split_trackare, _split_crh
|
||||||
|
|
||||||
|
|
||||||
|
# --- Données de test ---
|
||||||
|
|
||||||
|
TRACKARE_PATIENT_BLOCK = """CENTRE HOSPITALIER COTE BASQUE
|
||||||
|
Dossier Patient
|
||||||
|
Détails des patients
|
||||||
|
Nom de naissance: DUPONT IPP: 01234567
|
||||||
|
Nom et Prénom: DUPONT JEAN Date de naissance: 15/03/1960
|
||||||
|
Sexe: Masculin Lieu de naissance: BAYONNE
|
||||||
|
Adresse: 12 RUE DU PORT Ville de résidence: BAYONNE
|
||||||
|
Code Postal: 64100"""
|
||||||
|
|
||||||
|
TRACKARE_EPISODE_1 = """Détails épisode
|
||||||
|
Episode No: 23001111
|
||||||
|
Date d'admission: 10/01/2023 Heure d'admission: 14:30
|
||||||
|
Date de sortie: 10/01/2023 Heure de sortie: 18:00
|
||||||
|
Localisation: URG-ADULTE Médecin courant: DR MARTIN
|
||||||
|
Observations médicales
|
||||||
|
Note d'évolution DR MARTIN 10/01/2023 15:00
|
||||||
|
Douleur abdominale. Bilan normal.
|
||||||
|
Diagnostic aux urgences
|
||||||
|
Principal actif R10.4 Douleurs abdominales 10/01/2023 15:30"""
|
||||||
|
|
||||||
|
TRACKARE_EPISODE_2 = """Détails épisode
|
||||||
|
Episode No: 23002222
|
||||||
|
Date d'admission: 15/03/2023 Heure d'admission: 09:15
|
||||||
|
Date de sortie: 15/03/2023 Heure de sortie: 13:00
|
||||||
|
Localisation: URG-ADULTE Médecin courant: DR DURAND
|
||||||
|
Observations médicales
|
||||||
|
Note d'évolution DR DURAND 15/03/2023 10:00
|
||||||
|
Chute mécanique. Radio cheville gauche normale.
|
||||||
|
Diagnostic aux urgences
|
||||||
|
Principal actif S93.4 Entorse de la cheville 15/03/2023 10:30"""
|
||||||
|
|
||||||
|
TRACKARE_SINGLE = TRACKARE_PATIENT_BLOCK + "\n" + TRACKARE_EPISODE_1
|
||||||
|
TRACKARE_MULTI = TRACKARE_PATIENT_BLOCK + "\n" + TRACKARE_EPISODE_1 + "\n" + TRACKARE_EPISODE_2
|
||||||
|
|
||||||
|
|
||||||
|
CRH_SINGLE = """MME MARTIN SOPHIE
|
||||||
|
12 RUE DES FLEURS
|
||||||
|
64100 BAYONNE
|
||||||
|
|
||||||
|
Mon cher confrère,
|
||||||
|
Votre patiente MARTIN Sophie née le 05/06/1975 a été hospitalisée
|
||||||
|
du 20/01/2023 au 25/01/2023 pour le motif suivant:
|
||||||
|
Cholécystite aiguë lithiasique.
|
||||||
|
|
||||||
|
Cordialement,
|
||||||
|
Dr DURAND"""
|
||||||
|
|
||||||
|
CRH_DOC_1 = """MME MARTIN SOPHIE
|
||||||
|
12 RUE DES FLEURS
|
||||||
|
64100 BAYONNE
|
||||||
|
|
||||||
|
Mon cher confrère,
|
||||||
|
Votre patiente MARTIN Sophie née le 05/06/1975 a été hospitalisée
|
||||||
|
du 20/01/2023 au 25/01/2023 pour le motif suivant:
|
||||||
|
Cholécystite aiguë lithiasique.
|
||||||
|
|
||||||
|
Cordialement,
|
||||||
|
Dr DURAND
|
||||||
|
"""
|
||||||
|
|
||||||
|
CRH_DOC_2 = """M. BERNARD PIERRE
|
||||||
|
5 AVENUE DU MARÉCHAL
|
||||||
|
64200 BIARRITZ
|
||||||
|
|
||||||
|
Mon cher confrère,
|
||||||
|
Votre patient BERNARD Pierre né le 12/11/1950 a été hospitalisé
|
||||||
|
du 02/02/2023 au 08/02/2023 pour le motif suivant:
|
||||||
|
Décompensation cardiaque.
|
||||||
|
|
||||||
|
Cordialement,
|
||||||
|
Dr PUJOS
|
||||||
|
"""
|
||||||
|
|
||||||
|
CRH_MULTI = CRH_DOC_1 + CRH_DOC_2
|
||||||
|
|
||||||
|
|
||||||
|
# --- Tests Trackare ---
|
||||||
|
|
||||||
|
class TestSplitTrackare:
|
||||||
|
def test_single_episode_returns_unchanged(self):
|
||||||
|
result = _split_trackare(TRACKARE_SINGLE)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == TRACKARE_SINGLE
|
||||||
|
|
||||||
|
def test_multi_episode_returns_two_chunks(self):
|
||||||
|
result = _split_trackare(TRACKARE_MULTI)
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_each_chunk_has_patient_block(self):
|
||||||
|
result = _split_trackare(TRACKARE_MULTI)
|
||||||
|
for chunk in result:
|
||||||
|
assert "DUPONT JEAN" in chunk
|
||||||
|
assert "IPP: 01234567" in chunk
|
||||||
|
|
||||||
|
def test_each_chunk_has_own_episode(self):
|
||||||
|
result = _split_trackare(TRACKARE_MULTI)
|
||||||
|
assert "Episode No: 23001111" in result[0]
|
||||||
|
assert "Episode No: 23002222" in result[1]
|
||||||
|
|
||||||
|
def test_episodes_are_separated(self):
|
||||||
|
result = _split_trackare(TRACKARE_MULTI)
|
||||||
|
# Le premier chunk ne doit PAS contenir l'épisode 2
|
||||||
|
assert "Episode No: 23002222" not in result[0]
|
||||||
|
# Le second chunk ne doit PAS contenir l'épisode 1
|
||||||
|
assert "Episode No: 23001111" not in result[1]
|
||||||
|
|
||||||
|
def test_no_episode_returns_unchanged(self):
|
||||||
|
text = "Un texte sans épisode du tout."
|
||||||
|
result = _split_trackare(text)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == text
|
||||||
|
|
||||||
|
def test_fallback_without_details_episode(self):
|
||||||
|
"""Si pas de 'Détails épisode', coupe sur 'Episode No:'."""
|
||||||
|
text = (
|
||||||
|
"Nom de naissance: DUPONT IPP: 01234567\n"
|
||||||
|
"Episode No: 111\nContenu épisode 1\n"
|
||||||
|
"Episode No: 222\nContenu épisode 2"
|
||||||
|
)
|
||||||
|
result = _split_trackare(text)
|
||||||
|
assert len(result) == 2
|
||||||
|
assert "Episode No: 111" in result[0]
|
||||||
|
assert "Episode No: 222" in result[1]
|
||||||
|
|
||||||
|
|
||||||
|
# --- Tests CRH ---
|
||||||
|
|
||||||
|
class TestSplitCRH:
|
||||||
|
def test_single_crh_returns_unchanged(self):
|
||||||
|
result = _split_crh(CRH_SINGLE)
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == CRH_SINGLE
|
||||||
|
|
||||||
|
def test_multi_crh_returns_two_chunks(self):
|
||||||
|
result = _split_crh(CRH_MULTI)
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_each_chunk_is_independent(self):
|
||||||
|
result = _split_crh(CRH_MULTI)
|
||||||
|
assert "MARTIN SOPHIE" in result[0]
|
||||||
|
assert "BERNARD PIERRE" in result[1]
|
||||||
|
|
||||||
|
def test_chunks_contain_medical_content(self):
|
||||||
|
result = _split_crh(CRH_MULTI)
|
||||||
|
assert "Cholécystite" in result[0]
|
||||||
|
assert "Décompensation cardiaque" in result[1]
|
||||||
|
|
||||||
|
def test_no_header_returns_unchanged(self):
|
||||||
|
text = "Un texte médical sans header patient."
|
||||||
|
result = _split_crh(text)
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
def test_header_without_crh_markers_no_split(self):
|
||||||
|
"""Un header patient sans patterns CRH ne déclenche pas de split."""
|
||||||
|
text = "MME DUPONT MARIE\nTexte quelconque\nMME MARTIN SOPHIE\nAutre texte"
|
||||||
|
result = _split_crh(text)
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# --- Tests dispatch ---
|
||||||
|
|
||||||
|
class TestSplitDocuments:
|
||||||
|
def test_dispatch_trackare(self):
|
||||||
|
result = split_documents(TRACKARE_MULTI, "trackare")
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_dispatch_crh(self):
|
||||||
|
result = split_documents(CRH_MULTI, "crh")
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_dispatch_unknown_type(self):
|
||||||
|
result = split_documents("some text", "unknown")
|
||||||
|
assert len(result) == 1
|
||||||
|
assert result[0] == "some text"
|
||||||
|
|
||||||
|
def test_single_doc_no_split(self):
|
||||||
|
result = split_documents(TRACKARE_SINGLE, "trackare")
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# --- Test intégration process_pdf ---
|
||||||
|
|
||||||
|
class TestProcessPdfMulti:
|
||||||
|
@patch("src.main.extract_text")
|
||||||
|
@patch("src.main.extract_medical_info")
|
||||||
|
@patch("src.main._run_edsnlp", return_value=None)
|
||||||
|
@patch("src.main._use_edsnlp", False)
|
||||||
|
def test_multi_episode_returns_multiple_results(
|
||||||
|
self, mock_edsnlp, mock_medical, mock_extract
|
||||||
|
):
|
||||||
|
from pathlib import Path
|
||||||
|
from src.main import process_pdf
|
||||||
|
from src.config import DossierMedical, Diagnostic
|
||||||
|
|
||||||
|
# Mock extract_text retournant un texte multi-épisodes Trackare
|
||||||
|
mock_extract.return_value = TRACKARE_MULTI
|
||||||
|
|
||||||
|
# Mock extract_medical_info retournant un DossierMedical minimal
|
||||||
|
mock_medical.return_value = DossierMedical(
|
||||||
|
diagnostic_principal=Diagnostic(texte="test"),
|
||||||
|
)
|
||||||
|
|
||||||
|
results = process_pdf(Path("fake.pdf"))
|
||||||
|
assert len(results) == 2
|
||||||
|
# Chaque résultat est un tuple (text, dossier, report)
|
||||||
|
for anon_text, dossier, report in results:
|
||||||
|
assert isinstance(dossier, DossierMedical)
|
||||||
Reference in New Issue
Block a user