From b23355ed236726cbd89dc07773c5e94d8df55501 Mon Sep 17 00:00:00 2001 From: Domi31tls Date: Tue, 14 Apr 2026 10:17:14 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20scripts=20de=20g=C3=A9n=C3=A9ration=20d?= =?UTF-8?q?es=20fiches=20produit=20et=20technique=20DSI/RSSI/DPO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/generate_fiche_produit.py | 522 +++++++++++++++ scripts/generate_fiche_technique_dsi.py | 808 ++++++++++++++++++++++++ 2 files changed, 1330 insertions(+) create mode 100644 scripts/generate_fiche_produit.py create mode 100644 scripts/generate_fiche_technique_dsi.py diff --git a/scripts/generate_fiche_produit.py b/scripts/generate_fiche_produit.py new file mode 100644 index 0000000..7ed0525 --- /dev/null +++ b/scripts/generate_fiche_produit.py @@ -0,0 +1,522 @@ +#!/usr/bin/env python3 +"""Génère la fiche produit Pseudonymisation en DOCX (deux versions).""" + +from docx import Document +from docx.shared import Pt, Cm, Inches, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.oxml.ns import qn +import os + +# ── Helpers ────────────────────────────────────────────────────────── + +def set_cell_shading(cell, color_hex: str): + """Applique une couleur de fond à une cellule.""" + shading = cell._element.get_or_add_tcPr() + shading_elem = shading.makeelement(qn('w:shd'), { + qn('w:val'): 'clear', + qn('w:color'): 'auto', + qn('w:fill'): color_hex, + }) + shading.append(shading_elem) + + +def add_heading_styled(doc, text, level=1, color=RGBColor(0x1A, 0x56, 0x8E)): + h = doc.add_heading(text, level=level) + for run in h.runs: + run.font.color.rgb = color + return h + + +def add_bullet(doc, text, bold_prefix=None): + p = doc.add_paragraph(style='List Bullet') + if bold_prefix: + run = p.add_run(bold_prefix) + run.bold = True + p.add_run(text) + else: + p.add_run(text) + return p + + +def add_para(doc, text, bold=False, italic=False, space_after=Pt(6)): + p = doc.add_paragraph() + run = p.add_run(text) + run.bold = bold + run.italic = italic + p.paragraph_format.space_after = space_after + return p + + +def add_table_row(table, cells_data, header=False, header_color='1A568E'): + row = table.add_row() + for i, text in enumerate(cells_data): + cell = row.cells[i] + cell.text = '' + p = cell.paragraphs[0] + run = p.add_run(text) + run.font.size = Pt(10) + if header: + run.bold = True + run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + set_cell_shading(cell, header_color) + p.paragraph_format.space_before = Pt(4) + p.paragraph_format.space_after = Pt(4) + + +def set_table_style(table): + """Style sobre pour les tableaux.""" + table.alignment = WD_TABLE_ALIGNMENT.CENTER + for row in table.rows: + for cell in row.cells: + for p in cell.paragraphs: + p.paragraph_format.space_before = Pt(3) + p.paragraph_format.space_after = Pt(3) + + +def add_section_break(doc): + doc.add_page_break() + + +# ── Couleurs ───────────────────────────────────────────────────────── + +BLUE_DARK = RGBColor(0x1A, 0x56, 0x8E) +BLUE_LIGHT = 'D6E8F7' +GRAY_LIGHT = 'F2F2F2' +GREEN = RGBColor(0x2E, 0x7D, 0x32) + + +# ══════════════════════════════════════════════════════════════════════ +# VERSION 1 — Synthétique (1 page recto, orientation portrait) +# ══════════════════════════════════════════════════════════════════════ + +def build_version_1(doc): + # ── Titre ── + title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in title.runs: + run.font.color.rgb = BLUE_DARK + run.font.size = Pt(22) + + # Sous-titre + sub = doc.add_paragraph() + sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = sub.add_run('Fiche produit — Version synthétique') + run.font.size = Pt(12) + run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) + run.italic = True + sub.paragraph_format.space_after = Pt(16) + + # ── En bref ── + add_heading_styled(doc, 'En bref', level=2) + add_para(doc, + 'Logiciel d\'anonymisation automatique de documents médicaux, ' + 'fonctionnant 100% en local sur un poste Windows. ' + 'Aucune donnée ne sort de l\'établissement. ' + 'Un fichier entre, un fichier anonymisé sort.', + space_after=Pt(10)) + + # ── Tableau comparatif ── + add_heading_styled(doc, 'Pourquoi cet outil ?', level=2) + table = doc.add_table(rows=0, cols=2) + table.style = 'Table Grid' + add_table_row(table, ['Situation actuelle', 'Avec l\'outil'], header=True) + comparisons = [ + ('Anonymisation manuelle, chronophage\net source d\'erreurs', + 'Traitement automatique\nen quelques minutes'), + ('Risque d\'oubli de données\npersonnelles (RGPD)', + 'Taux de détection > 99%\nvalidé sur corpus réel'), + ('Mobilisation de personnel qualifié\nsur une tâche répétitive', + 'L\'équipe TIM vérifie,\nelle ne produit plus à la main'), + ] + for left, right in comparisons: + add_table_row(table, [left, right]) + set_table_style(table) + doc.add_paragraph() # espace + + # ── Points clés ── + add_heading_styled(doc, 'Points clés', level=2) + points = [ + ('100% hors-ligne — ', 'aucune donnée ne transite par internet'), + ('Un seul exécutable — ', 'pas d\'installation, pas de serveur, pas de GPU'), + ('14 formats acceptés — ', 'PDF, Word, ODT, RTF, images, HTML...'), + ('4 moteurs d\'IA — ', 'spécialisés en français clinique, fonctionnent en parallèle'), + ('Paramétrable — ', 'whitelist et blacklist modifiables par l\'établissement'), + ('Irréversible — ', 'masquage par rectangles noirs, pas de calque amovible'), + ] + for bold_part, normal_part in points: + add_bullet(doc, normal_part, bold_prefix=bold_part) + + # ── Ce qui est masqué / préservé ── + add_heading_styled(doc, 'Ce qui est masqué / préservé', level=2) + table2 = doc.add_table(rows=0, cols=2) + table2.style = 'Table Grid' + add_table_row(table2, ['Masqué automatiquement', 'Préservé intégralement'], header=True) + add_table_row(table2, [ + 'Noms, prénoms, adresses,\ntéléphones, n° de sécu,\ndates de naissance,\nnoms d\'établissements,\ncodes-barres patients', + 'Diagnostics, traitements,\nposologies, actes médicaux,\nrésultats d\'examens,\ndates de séjour,\ncodage CIM / CCAM' + ]) + set_table_style(table2) + doc.add_paragraph() + + # ── Cas d'usage ── + add_heading_styled(doc, 'Cas d\'usage', level=2) + usages = [ + ('Contrôle T2A — ', 'anonymiser les pièces justificatives avant transmission'), + ('Recherche clinique — ', 'constituer des corpus anonymisés'), + ('Audits qualité / HAS — ', 'partager des dossiers sans exposition de données'), + ('Formation — ', 'utiliser des cas réels anonymisés pour l\'enseignement'), + ] + for bold_part, normal_part in usages: + add_bullet(doc, normal_part, bold_prefix=bold_part) + + +# ══════════════════════════════════════════════════════════════════════ +# VERSION 2 — Détaillée (multi-pages, par audience) +# ══════════════════════════════════════════════════════════════════════ + +def build_version_2(doc): + # ── Page de titre ── + for _ in range(4): + doc.add_paragraph() + + title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in title.runs: + run.font.color.rgb = BLUE_DARK + run.font.size = Pt(26) + + doc.add_paragraph() + sub = doc.add_paragraph() + sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = sub.add_run('Fiche produit détaillée') + run.font.size = Pt(14) + run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) + + doc.add_paragraph() + audiences = doc.add_paragraph() + audiences.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = audiences.add_run('Direction générale · DSI · Médecins · Équipe TIM') + run.font.size = Pt(11) + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + run.italic = True + + add_section_break(doc) + + # ── 1. Le problème ── + add_heading_styled(doc, 'Le problème', level=1) + add_para(doc, + 'Les établissements de santé manipulent quotidiennement des documents ' + 'contenant des données personnelles de patients : comptes-rendus opératoires, ' + 'résultats d\'examens, courriers médicaux, rapports d\'anatomo-pathologie...') + add_para(doc, + 'Lors des contrôles T2A, audits qualité, recherches cliniques ou échanges ' + 'inter-établissements, ces documents doivent être anonymisés conformément ' + 'au RGPD et au cadre réglementaire santé.') + add_para(doc, + 'L\'anonymisation manuelle est lente, coûteuse, et faillible : ' + 'un oubli suffit à exposer l\'identité d\'un patient.', + bold=True, space_after=Pt(12)) + + # ── 2. La solution ── + add_heading_styled(doc, 'La solution', level=1) + add_para(doc, + 'Un logiciel autonome, fonctionnant intégralement en local, qui anonymise ' + 'automatiquement vos documents en quelques minutes :') + bullets = [ + 'Détecte et masque les noms, prénoms, adresses, téléphones, numéros de sécurité sociale, dates de naissance, noms d\'établissements', + 'Préserve le contenu médical utile : diagnostics, traitements, actes, résultats, codage', + 'Produit un PDF anonymisé prêt à transmettre, avec masquage irréversible', + ] + for b in bullets: + add_bullet(doc, b) + doc.add_paragraph() + + # ── Tableau comparatif ── + add_heading_styled(doc, 'Avant / Après', level=2) + table = doc.add_table(rows=0, cols=2) + table.style = 'Table Grid' + add_table_row(table, ['Situation actuelle', 'Avec l\'outil'], header=True) + comparisons = [ + ('Anonymisation manuelle, chronophage et source d\'erreurs', + 'Traitement automatique en quelques minutes'), + ('Risque d\'oubli de données personnelles (RGPD, AI Act)', + 'Taux de détection > 99% validé sur corpus réel de contrôle T2A'), + ('Mobilisation de personnel qualifié sur une tâche répétitive', + 'L\'équipe TIM se concentre sur la vérification, pas la production'), + ('Dépendance à des outils cloud ou des prestataires externes', + '100% local, aucune donnée ne quitte l\'établissement'), + ] + for left, right in comparisons: + add_table_row(table, [left, right]) + set_table_style(table) + + add_section_break(doc) + + # ── 3. Conformité et sécurité (DG + DSI) ── + add_heading_styled(doc, 'Conformité et sécurité', level=1) + tag = doc.add_paragraph() + run = tag.add_run('→ Direction générale, DSI') + run.italic = True + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + tag.paragraph_format.space_after = Pt(8) + + security_points = [ + ('Aucune connexion réseau', 'Le logiciel fonctionne entièrement hors-ligne. Aucun appel à un serveur externe, aucune API cloud, aucune télémétrie.'), + ('Aucune donnée exfiltrée', 'Le traitement se fait intégralement en mémoire locale. Les documents source ne sont ni copiés ni transmis.'), + ('Conformité RGPD / AI Act', 'L\'intelligence artificielle est embarquée dans l\'exécutable. Pas de sous-traitance, pas de transfert de données à un tiers.'), + ('Masquage irréversible', 'Les zones anonymisées sont remplacées par des rectangles noirs directement dans le PDF. Il ne s\'agit pas d\'un calque amovible : l\'information est définitivement supprimée.'), + ('Traçabilité', 'Les fichiers anonymisés sont nommés avec le préfixe "ANON_" pour identification immédiate.'), + ] + for title_text, desc in security_points: + p = doc.add_paragraph() + run_title = p.add_run(title_text + ' — ') + run_title.bold = True + p.add_run(desc) + p.paragraph_format.space_after = Pt(6) + + add_section_break(doc) + + # ── 4. Déploiement (DSI) ── + add_heading_styled(doc, 'Déploiement et maintenance', level=1) + tag = doc.add_paragraph() + run = tag.add_run('→ DSI') + run.italic = True + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + tag.paragraph_format.space_after = Pt(8) + + add_heading_styled(doc, 'Prérequis', level=2) + prereqs = [ + 'Windows 10 ou 11 (64 bits)', + 'Poste bureautique standard — pas de GPU dédié, pas de serveur requis', + 'Pas de droits administrateur nécessaires pour l\'exécution', + 'Pas de connexion internet requise', + ] + for pr in prereqs: + add_bullet(doc, pr) + + add_heading_styled(doc, 'Installation', level=2) + install_steps = [ + 'Copier l\'exécutable (un seul fichier .exe) sur le poste', + 'Au premier lancement, le fichier de configuration est créé automatiquement à côté de l\'exécutable', + 'C\'est prêt — aucune autre manipulation nécessaire', + ] + for i, step in enumerate(install_steps, 1): + p = doc.add_paragraph() + run = p.add_run(f'{i}. ') + run.bold = True + p.add_run(step) + + add_heading_styled(doc, 'Mise à jour', level=2) + add_para(doc, 'Remplacer l\'exécutable par la nouvelle version. La configuration personnalisée de l\'établissement est conservée (fichier séparé).') + + add_heading_styled(doc, 'Configuration inter-établissements', level=2) + add_para(doc, + 'Les paramètres (whitelist, blacklist) sont exportables au format JSON depuis l\'interface. ' + 'Un établissement peut envoyer sa configuration par email. ' + 'Le service central fusionne les configurations et renvoie un fichier YAML consolidé.') + + add_section_break(doc) + + # ── 5. Utilisation quotidienne (TIM) ── + add_heading_styled(doc, 'Utilisation quotidienne', level=1) + tag = doc.add_paragraph() + run = tag.add_run('→ Équipe TIM, médecins') + run.italic = True + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + tag.paragraph_format.space_after = Pt(8) + + add_heading_styled(doc, 'En 3 étapes', level=2) + steps = [ + ('Sélectionner', 'Choisir un fichier ou un dossier entier depuis l\'interface'), + ('Lancer', 'Cliquer sur "Lancer l\'anonymisation"'), + ('Récupérer', 'Les documents anonymisés sont créés dans le dossier de sortie, prêts à transmettre'), + ] + for title_text, desc in steps: + p = doc.add_paragraph() + run_title = p.add_run(f'{title_text} — ') + run_title.bold = True + run_title.font.color.rgb = BLUE_DARK + p.add_run(desc) + p.paragraph_format.space_after = Pt(6) + + add_heading_styled(doc, 'Formats acceptés (14)', level=2) + table_fmt = doc.add_table(rows=0, cols=2) + table_fmt.style = 'Table Grid' + add_table_row(table_fmt, ['Type', 'Formats'], header=True) + formats = [ + ('Documents', 'PDF, Word (.docx), ODT, RTF, TXT, HTML'), + ('Images', 'JPEG, PNG, TIFF, BMP'), + ] + for type_name, fmt_list in formats: + add_table_row(table_fmt, [type_name, fmt_list]) + set_table_style(table_fmt) + doc.add_paragraph() + + add_para(doc, + 'Les images et documents scannés sont traités par reconnaissance optique de caractères (OCR) ' + 'intégrée — pas de logiciel tiers nécessaire.', + italic=True) + + add_heading_styled(doc, 'Paramétrage', level=2) + params = [ + ('Whitelist — ', 'liste de termes à ne jamais masquer (noms de services, sigles internes, noms de logiciels...). Modifiable directement dans l\'interface.'), + ('Blacklist — ', 'liste de termes à toujours masquer (noms de praticiens spécifiques...). Modifiable directement dans l\'interface.'), + ('Export/Import — ', 'les paramètres sont échangeables entre établissements par simple fichier JSON envoyé par email.'), + ] + for bold_part, normal_part in params: + add_bullet(doc, normal_part, bold_prefix=bold_part) + + add_section_break(doc) + + # ── 6. Fiabilité (Médecins + TIM) ── + add_heading_styled(doc, 'Fiabilité de la détection', level=1) + tag = doc.add_paragraph() + run = tag.add_run('→ Médecins, équipe TIM') + run.italic = True + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + tag.paragraph_format.space_after = Pt(8) + + add_para(doc, + 'Le logiciel combine 4 moteurs d\'intelligence artificielle spécialisés en français clinique ' + 'qui fonctionnent simultanément et se recoupent pour maximiser la détection :') + + engines = [ + ('Modèle CamemBERT médical', 'entraîné spécifiquement sur des documents cliniques français'), + ('Modèle de détection d\'entités', 'reconnaissance zero-shot de données personnelles'), + ('Modèle spécialisé', 'fine-tuné sur plus de 1 000 documents médicaux réels'), + ('Bases de référence nationales', '219 000 noms de famille, 36 000 prénoms, 7 300 médicaments, 108 000 établissements de santé'), + ] + for title_text, desc in engines: + add_bullet(doc, ' — ' + desc, bold_prefix=title_text) + doc.add_paragraph() + + add_para(doc, + 'Principe de précaution : en cas de doute, le logiciel masque. ' + 'Il est préférable de masquer un terme médical par excès ' + 'plutôt que de laisser visible un nom de patient.', + bold=True, space_after=Pt(8)) + + add_para(doc, + 'Taux de détection validé à plus de 99% sur un corpus réel de documents ' + 'de contrôle T2A : comptes-rendus opératoires, anatomo-pathologie, bactériologie, ' + 'anesthésie, courriers médicaux, comptes-rendus d\'hospitalisation.') + + add_section_break(doc) + + # ── 7. Cas d'usage ── + add_heading_styled(doc, 'Cas d\'usage', level=1) + + use_cases = [ + ('Contrôle T2A', + 'Anonymiser les pièces justificatives (comptes-rendus, résultats, courriers) ' + 'avant transmission à l\'organisme de contrôle.'), + ('Recherche clinique', + 'Constituer des jeux de données anonymisés à partir de dossiers patients réels, ' + 'sans risque d\'identification.'), + ('Audits qualité et certification HAS', + 'Partager des cas cliniques lors des visites de certification ' + 'sans exposer l\'identité des patients.'), + ('Échanges inter-établissements', + 'Transmettre des comptes-rendus médicaux anonymisés dans le cadre ' + 'de parcours de soins partagés.'), + ('Formation et enseignement', + 'Utiliser des cas cliniques réels anonymisés pour la formation ' + 'des internes et des équipes soignantes.'), + ] + for title_text, desc in use_cases: + p = doc.add_paragraph() + run_title = p.add_run(title_text + '\n') + run_title.bold = True + run_title.font.size = Pt(11) + run_desc = p.add_run(desc) + run_desc.font.size = Pt(10) + p.paragraph_format.space_after = Pt(10) + + # ── 8. Questions fréquentes ── + add_heading_styled(doc, 'Questions fréquentes', level=1) + + faq = [ + ('Combien de temps prend le traitement d\'un document ?', + 'En moyenne quelques dizaines de secondes par document. ' + 'Un lot de 20 à 30 documents se traite en 10 à 15 minutes sur un poste standard.'), + ('Que faire si un nom n\'a pas été détecté ?', + 'Ajouter le terme dans la blacklist via l\'interface. ' + 'Il sera systématiquement masqué lors des prochains traitements.'), + ('Que faire si un terme médical a été masqué par erreur ?', + 'Ajouter le terme dans la whitelist via l\'interface. ' + 'Il ne sera plus jamais masqué.'), + ('Faut-il une connexion internet ?', + 'Non. Le logiciel fonctionne intégralement hors-ligne. ' + 'L\'intelligence artificielle est embarquée dans l\'exécutable.'), + ('Les documents originaux sont-ils modifiés ?', + 'Non. Le logiciel crée une copie anonymisée. ' + 'Les documents originaux restent intacts.'), + ('Peut-on traiter un dossier entier d\'un coup ?', + 'Oui. Il suffit de sélectionner un dossier au lieu d\'un fichier. ' + 'Tous les documents du dossier seront traités séquentiellement.'), + ] + for question, answer in faq: + p = doc.add_paragraph() + run_q = p.add_run(question + '\n') + run_q.bold = True + run_q.font.size = Pt(10) + run_a = p.add_run(answer) + run_a.font.size = Pt(10) + p.paragraph_format.space_after = Pt(8) + + +# ══════════════════════════════════════════════════════════════════════ +# Construction du document final +# ══════════════════════════════════════════════════════════════════════ + +def main(): + doc = Document() + + # ── Style par défaut ── + style = doc.styles['Normal'] + style.font.name = 'Calibri' + style.font.size = Pt(10) + style.paragraph_format.space_after = Pt(4) + + # Marges + for section in doc.sections: + section.top_margin = Cm(1.5) + section.bottom_margin = Cm(1.5) + section.left_margin = Cm(2) + section.right_margin = Cm(2) + + # ════════════════════════════════════════════ + # VERSION 1 — Synthétique + # ════════════════════════════════════════════ + build_version_1(doc) + + # ── Séparation ── + add_section_break(doc) + sep = doc.add_paragraph() + sep.alignment = WD_ALIGN_PARAGRAPH.CENTER + for _ in range(6): + doc.add_paragraph() + sep2 = doc.add_paragraph() + sep2.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = sep2.add_run('— Version détaillée ci-après —') + run.font.size = Pt(14) + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + run.italic = True + + add_section_break(doc) + + # ════════════════════════════════════════════ + # VERSION 2 — Détaillée + # ════════════════════════════════════════════ + build_version_2(doc) + + # ── Sauvegarde ── + output_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), + 'Fiche_Produit_Pseudonymisation.docx') + doc.save(output_path) + print(f'Fiche produit générée : {output_path}') + + +if __name__ == '__main__': + main() diff --git a/scripts/generate_fiche_technique_dsi.py b/scripts/generate_fiche_technique_dsi.py new file mode 100644 index 0000000..e2d86e3 --- /dev/null +++ b/scripts/generate_fiche_technique_dsi.py @@ -0,0 +1,808 @@ +#!/usr/bin/env python3 +"""Génère la fiche technique DSI / RSSI / DPO en DOCX.""" + +from docx import Document +from docx.shared import Pt, Cm, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.enum.table import WD_TABLE_ALIGNMENT +from docx.oxml.ns import qn +import os + +# ── Helpers ────────────────────────────────────────────────────────── + +def set_cell_shading(cell, color_hex: str): + shading = cell._element.get_or_add_tcPr() + shading_elem = shading.makeelement(qn('w:shd'), { + qn('w:val'): 'clear', + qn('w:color'): 'auto', + qn('w:fill'): color_hex, + }) + shading.append(shading_elem) + + +def add_heading_styled(doc, text, level=1, color=RGBColor(0x1A, 0x56, 0x8E)): + h = doc.add_heading(text, level=level) + for run in h.runs: + run.font.color.rgb = color + return h + + +def add_bullet(doc, text, bold_prefix=None): + p = doc.add_paragraph(style='List Bullet') + if bold_prefix: + run = p.add_run(bold_prefix) + run.bold = True + p.add_run(text) + else: + p.add_run(text) + return p + + +def add_para(doc, text, bold=False, italic=False, space_after=Pt(6)): + p = doc.add_paragraph() + run = p.add_run(text) + run.bold = bold + run.italic = italic + p.paragraph_format.space_after = space_after + return p + + +def add_table_row(table, cells_data, header=False, header_color='1A568E'): + row = table.add_row() + for i, text in enumerate(cells_data): + cell = row.cells[i] + cell.text = '' + p = cell.paragraphs[0] + run = p.add_run(text) + run.font.size = Pt(9.5) + if header: + run.bold = True + run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) + set_cell_shading(cell, header_color) + p.paragraph_format.space_before = Pt(3) + p.paragraph_format.space_after = Pt(3) + + +def set_table_style(table): + table.alignment = WD_TABLE_ALIGNMENT.CENTER + for row in table.rows: + for cell in row.cells: + for p in cell.paragraphs: + p.paragraph_format.space_before = Pt(2) + p.paragraph_format.space_after = Pt(2) + + +def add_section_break(doc): + doc.add_page_break() + + +def add_audience_tag(doc, text): + tag = doc.add_paragraph() + run = tag.add_run(text) + run.italic = True + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + run.font.size = Pt(9) + tag.paragraph_format.space_after = Pt(8) + + +def add_labeled_para(doc, label, text): + """Paragraphe avec un label en gras suivi du texte normal.""" + p = doc.add_paragraph() + run_label = p.add_run(label + ' — ') + run_label.bold = True + p.add_run(text) + p.paragraph_format.space_after = Pt(5) + return p + + +# ── Couleurs ───────────────────────────────────────────────────────── + +BLUE_DARK = RGBColor(0x1A, 0x56, 0x8E) +RED_DARK = RGBColor(0x8E, 0x1A, 0x1A) +GREEN_DARK = RGBColor(0x2E, 0x7D, 0x32) + + +# ══════════════════════════════════════════════════════════════════════ +# PAGE DE TITRE +# ══════════════════════════════════════════════════════════════════════ + +def build_title_page(doc): + for _ in range(5): + doc.add_paragraph() + + title = doc.add_heading('Pseudonymisation Automatique\nde Documents Médicaux', level=0) + title.alignment = WD_ALIGN_PARAGRAPH.CENTER + for run in title.runs: + run.font.color.rgb = BLUE_DARK + run.font.size = Pt(26) + + doc.add_paragraph() + + sub = doc.add_paragraph() + sub.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = sub.add_run('Fiche technique — Sécurité et Conformité') + run.font.size = Pt(14) + run.font.color.rgb = RGBColor(0x66, 0x66, 0x66) + + doc.add_paragraph() + + audiences = doc.add_paragraph() + audiences.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = audiences.add_run('DSI · RSSI · DPO') + run.font.size = Pt(12) + run.font.color.rgb = RGBColor(0x99, 0x99, 0x99) + run.italic = True + + doc.add_paragraph() + doc.add_paragraph() + + # Encadré résumé + summary = doc.add_paragraph() + summary.alignment = WD_ALIGN_PARAGRAPH.CENTER + run = summary.add_run( + 'Logiciel d\'anonymisation fonctionnant 100% en local.\n' + 'Aucune connexion réseau. Aucun sous-traitant. Aucune donnée exfiltrée.\n' + 'Intelligence artificielle embarquée — masquage irréversible.' + ) + run.font.size = Pt(10.5) + run.font.color.rgb = RGBColor(0x44, 0x44, 0x44) + run.italic = True + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 1. ARCHITECTURE ET FLUX DE DONNÉES +# ══════════════════════════════════════════════════════════════════════ + +def build_architecture(doc): + add_heading_styled(doc, '1. Architecture et flux de données', level=1) + add_audience_tag(doc, '→ DSI, RSSI') + + add_para(doc, + 'Principe fondamental : aucune donnée ne quitte le poste de travail.', + bold=True, space_after=Pt(10)) + + points = [ + ('Exécutable autonome', + 'Un seul fichier .exe embarquant l\'intégralité des modèles d\'IA, ' + 'des bases de référence et du moteur OCR. Pas d\'appel réseau, ' + 'pas d\'API, pas de dépendance externe à l\'exécution.'), + ('Traitement en mémoire', + 'Les documents sont traités en mémoire vive. Aucune copie dans un ' + 'répertoire temporaire partagé, aucun cache disque au-delà de la session.'), + ('Aucune télémétrie', + 'Pas de phoning home, pas de vérification de licence en ligne, ' + 'pas de collecte de statistiques d\'usage.'), + ('Stateless', + 'Pas de base de données. Le logiciel ne conserve aucun état entre ' + 'deux exécutions, hormis le fichier de configuration (whitelist/blacklist).'), + ('Non destructif', + 'Le document source n\'est jamais modifié. Un nouveau fichier ' + 'anonymisé est créé dans le dossier de sortie.'), + ] + for label, text in points: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # Schéma flux + add_heading_styled(doc, 'Flux de traitement', level=2) + + flow_steps = [ + ('1', 'Entrée', 'Document source (poste local) — PDF, DOCX, image...'), + ('2', 'Extraction', 'Extraction du texte en mémoire (layout-aware + OCR intégré si nécessaire)'), + ('3', 'Détection', '4 moteurs IA analysent le texte en parallèle (CPU uniquement)'), + ('4', 'Masquage', 'Remplacement irréversible des PII dans le PDF (texte supprimé + rectangle noir)'), + ('5', 'Sortie', 'Écriture du PDF anonymisé sur disque local'), + ('6', 'Nettoyage', 'Libération mémoire — aucune trace résiduelle'), + ] + table = doc.add_table(rows=0, cols=3) + table.style = 'Table Grid' + add_table_row(table, ['Étape', 'Phase', 'Description'], header=True) + for step, phase, desc in flow_steps: + add_table_row(table, [step, phase, desc]) + set_table_style(table) + + # Largeurs colonnes + for row in table.rows: + row.cells[0].width = Cm(1.2) + row.cells[1].width = Cm(2.8) + row.cells[2].width = Cm(12) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 2. CONFORMITÉ RÉGLEMENTAIRE +# ══════════════════════════════════════════════════════════════════════ + +def build_conformite(doc): + add_heading_styled(doc, '2. Conformité réglementaire', level=1) + add_audience_tag(doc, '→ DPO, RSSI, Direction') + + # ── RGPD ── + add_heading_styled(doc, 'RGPD (Règlement 2016/679)', level=2) + + rgpd = [ + ('Art. 25 — Privacy by design', + 'Le traitement est conçu pour fonctionner sans transfert de données. ' + 'L\'intelligence artificielle est embarquée dans l\'exécutable, ' + 'pas hébergée sur un serveur distant.'), + ('Art. 28 — Sous-traitance', + 'Aucun sous-traitant au sens du RGPD. Le logiciel tourne sur ' + 'l\'infrastructure de l\'établissement, sous sa responsabilité exclusive. ' + 'Aucune donnée n\'est transmise à un tiers.'), + ('Art. 32 — Sécurité du traitement', + 'Les données ne transitent par aucun réseau. Le périmètre de sécurité ' + 'est celui du poste de travail. Les mécanismes de protection existants ' + '(chiffrement disque, contrôle d\'accès, antivirus) s\'appliquent.'), + ('Art. 35 — Analyse d\'impact (AIPD)', + 'Le logiciel réduit le risque sur les données personnelles : il anonymise, ' + 'il ne crée pas de nouveau traitement. Pas de traitement à grande échelle ' + 'externalisé. Une AIPD simplifiée peut suffire selon la politique de l\'établissement.'), + ('Art. 17 — Droit à l\'effacement', + 'Les documents anonymisés ne contiennent plus de données à caractère personnel. ' + 'Ils sortent du champ d\'application du RGPD.'), + ('Art. 5.1.e — Limitation de conservation', + 'Le logiciel ne stocke aucune donnée personnelle au-delà de la session ' + 'de traitement. Pas de base, pas de journal nominatif, pas d\'historique.'), + ] + for label, text in rgpd: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── AI Act ── + add_heading_styled(doc, 'AI Act (Règlement 2024/1689)', level=2) + + ai_act = [ + ('Classification du risque', + 'Le logiciel utilise des modèles d\'IA pour la détection d\'entités nommées ' + '(NER). Il ne prend aucune décision automatisée affectant des personnes. ' + 'L\'IA est un outil d\'aide : l\'opérateur humain reste maître de la validation.'), + ('Pas de système à haut risque', + 'Pas de scoring, pas de profilage, pas de catégorisation de personnes. ' + 'Le logiciel traite des documents, pas des individus.'), + ('Transparence', + 'Les modèles utilisés sont identifiés (CamemBERT, GLiNER). ' + 'Leur fonctionnement est documenté. Les résultats sont vérifiables ' + 'par l\'opérateur avant toute transmission.'), + ] + for label, text in ai_act: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── Cadre santé ── + add_heading_styled(doc, 'Cadre réglementaire santé', level=2) + + sante = [ + ('PGSSI-S', + 'Compatible avec la Politique Générale de Sécurité des Systèmes ' + 'd\'Information de Santé. Le logiciel fonctionne dans le périmètre SI ' + 'de l\'établissement sans ouvrir de flux réseau.'), + ('Hébergement de Données de Santé (HDS)', + 'Pas d\'agrément HDS requis. Le logiciel n\'héberge rien — ' + 'il traite en local et ne conserve aucune donnée.'), + ('Découplage DPI', + 'Le logiciel travaille sur des fichiers exportés du DPI. ' + 'Aucune connexion directe au système d\'information clinique. ' + 'Aucun risque d\'altération des données sources.'), + ] + for label, text in sante: + add_labeled_para(doc, label, text) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 3. SÉCURITÉ TECHNIQUE +# ══════════════════════════════════════════════════════════════════════ + +def build_securite(doc): + add_heading_styled(doc, '3. Sécurité technique', level=1) + add_audience_tag(doc, '→ RSSI') + + # ── Surface d'attaque ── + add_heading_styled(doc, 'Surface d\'attaque', level=2) + attack_surface = [ + 'Aucun port réseau ouvert — le logiciel n\'écoute sur aucune interface', + 'Aucune communication sortante — vérifiable par capture réseau (Wireshark, pare-feu)', + 'Pas de serveur web embarqué, pas de socket, pas d\'API REST', + 'Pas de service en arrière-plan ni de processus résident', + 'Processus mono-instance protégé par verrou fichier (pas de double exécution)', + ] + for item in attack_surface: + add_bullet(doc, item) + + doc.add_paragraph() + + # ── Exécutable ── + add_heading_styled(doc, 'Exécutable', level=2) + exe_points = [ + ('Compilation', + 'Compilé via PyInstaller — bundle Python, dépendances et modèles IA ' + 'en un seul fichier .exe autonome.'), + ('Signature de code', + 'Compatible avec les outils de signature Windows (signtool). ' + 'L\'établissement peut signer l\'exécutable avec son propre certificat ' + 'si sa politique de sécurité l\'exige.'), + ('Privilèges', + 'Fonctionne en espace utilisateur standard. Pas d\'élévation de privilèges, ' + 'pas d\'UAC, pas de droits administrateur requis.'), + ('Registre Windows', + 'Aucune écriture dans le registre Windows ni dans les répertoires système. ' + 'L\'exécutable et sa configuration restent dans leur dossier.'), + ('Empreinte', + 'Exécutable unique (~720 Mo, modèles IA inclus). ' + 'Aucune DLL externe, aucune dépendance système non standard.'), + ] + for label, text in exe_points: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── Données ── + add_heading_styled(doc, 'Données au repos et en transit', level=2) + + table = doc.add_table(rows=0, cols=3) + table.style = 'Table Grid' + add_table_row(table, ['Catégorie', 'Localisation', 'Contenu sensible'], header=True) + + data_rows = [ + ('Documents source', 'Inchangés, emplacement d\'origine', 'Oui — sous contrôle établissement'), + ('Documents anonymisés', 'Dossier de sortie (local)', 'Non — PII supprimées'), + ('Configuration (.yml)', 'À côté de l\'exécutable', 'Non — listes de termes uniquement'), + ('Fichiers temporaires', 'Aucun', 'N/A'), + ('Logs', 'Console uniquement (non persistés)', 'Non — pas de données nominatives'), + ('Données en transit', 'Aucune', 'N/A — aucune communication réseau'), + ] + for cat, loc, sensible in data_rows: + add_table_row(table, [cat, loc, sensible]) + set_table_style(table) + + doc.add_paragraph() + + # ── Vérification indépendante ── + add_heading_styled(doc, 'Vérification indépendante', level=2) + add_para(doc, + 'L\'absence de communication réseau est vérifiable de manière indépendante ' + 'par l\'équipe sécurité de l\'établissement :', + space_after=Pt(8)) + + verif = [ + ('Capture réseau', 'Wireshark ou tcpdump pendant une session de traitement : aucun paquet émis.'), + ('Pare-feu applicatif', 'Bloquer toute communication sortante pour le processus : aucun impact fonctionnel.'), + ('Analyse statique', 'Aucune URL, aucun endpoint, aucune chaîne de connexion dans l\'exécutable.'), + ('Sandbox', 'Exécution dans un environnement isolé (VM sans réseau) : fonctionnement nominal.'), + ] + for label, text in verif: + add_labeled_para(doc, label, text) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 4. DÉPLOIEMENT +# ══════════════════════════════════════════════════════════════════════ + +def build_deploiement(doc): + add_heading_styled(doc, '4. Déploiement et maintenance', level=1) + add_audience_tag(doc, '→ DSI') + + # ── Prérequis ── + add_heading_styled(doc, 'Prérequis techniques', level=2) + + table = doc.add_table(rows=0, cols=2) + table.style = 'Table Grid' + add_table_row(table, ['Élément', 'Détail'], header=True) + + prereqs = [ + ('Système d\'exploitation', 'Windows 10 / 11 (64 bits)'), + ('Processeur', 'x86-64, 4 cœurs recommandés'), + ('Mémoire vive', '8 Go minimum (16 Go recommandés pour lots volumineux)'), + ('Espace disque', '~1 Go (exécutable + configuration)'), + ('GPU', 'Non requis — inférence CPU uniquement'), + ('Droits utilisateur', 'Utilisateur standard (pas d\'administrateur)'), + ('Réseau', 'Aucun accès requis — fonctionnement 100% hors-ligne'), + ('Logiciel tiers', 'Aucun — tout est embarqué dans l\'exécutable'), + ] + for elem, detail in prereqs: + add_table_row(table, [elem, detail]) + set_table_style(table) + + for row in table.rows: + row.cells[0].width = Cm(4.5) + row.cells[1].width = Cm(11.5) + + doc.add_paragraph() + + # ── Installation ── + add_heading_styled(doc, 'Procédure d\'installation', level=2) + steps = [ + 'Copier l\'exécutable (.exe) sur le poste de travail', + 'Premier lancement : le fichier de configuration (dictionnaires.yml) ' + 'est créé automatiquement dans le même répertoire', + 'Aucune autre manipulation — le logiciel est opérationnel', + ] + for i, step in enumerate(steps, 1): + p = doc.add_paragraph() + run = p.add_run(f'{i}. ') + run.bold = True + p.add_run(step) + p.paragraph_format.space_after = Pt(4) + + add_para(doc, + 'Pas d\'installeur MSI/EXE setup. Pas de modification du registre. ' + 'Pas de redémarrage nécessaire.', + italic=True, space_after=Pt(10)) + + # ── Mise à jour ── + add_heading_styled(doc, 'Mise à jour', level=2) + updates = [ + 'Remplacer l\'exécutable par la nouvelle version', + 'La configuration personnalisée est préservée (fichier séparé)', + 'Pas de mise à jour automatique — contrôle total par la DSI', + 'Pas de dépendance à un serveur de mise à jour', + ] + for item in updates: + add_bullet(doc, item) + + doc.add_paragraph() + + # ── Déploiement multi-postes ── + add_heading_styled(doc, 'Déploiement multi-postes', level=2) + multi = [ + ('Distribution', + 'Copie simple via partage réseau, SCCM, GPO, ou clé USB.'), + ('Configuration centralisée', + 'Préparer un fichier .yml maître avec les paramètres communs, ' + 'le distribuer avec l\'exécutable.'), + ('Personnalisation par site', + 'Chaque établissement peut ajuster sa configuration localement ' + 'via l\'interface graphique (whitelist, blacklist).'), + ('Échange de configuration', + 'Export JSON depuis l\'interface → envoi par email → ' + 'fusion centralisée → renvoi du YAML consolidé.'), + ] + for label, text in multi: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── Intégration SI ── + add_heading_styled(doc, 'Intégration au SI', level=2) + integration = [ + 'Découplé du DPI — travaille sur des fichiers exportés, pas de connecteur direct', + 'Utilisable en ligne de commande pour intégration dans un workflow batch', + 'Compatible avec les espaces de travail sécurisés et les postes verrouillés', + 'Aucune dépendance externe (pas d\'accès internet, pas de service tiers)', + ] + for item in integration: + add_bullet(doc, item) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 5. GARANTIES D'ANONYMISATION +# ══════════════════════════════════════════════════════════════════════ + +def build_garanties(doc): + add_heading_styled(doc, '5. Garanties d\'anonymisation', level=1) + add_audience_tag(doc, '→ DPO, RSSI') + + # ── Nature du masquage ── + add_heading_styled(doc, 'Nature du masquage', level=2) + masquage = [ + ('Irréversibilité', + 'Le texte original est supprimé du flux PDF, puis recouvert ' + 'par un rectangle noir opaque. Il ne s\'agit pas d\'un calque amovible : ' + 'l\'information est définitivement détruite dans le fichier de sortie.'), + ('Métadonnées', + 'Les métadonnées PDF contenant des données personnelles ' + '(auteur, titre, sujet) sont nettoyées.'), + ('Codes-barres', + 'Les codes-barres contenant des identifiants patients sont ' + 'détectés et masqués automatiquement.'), + ('Intégrité du document', + 'La structure, la mise en page et le contenu médical sont ' + 'préservés. Seules les données à caractère personnel sont supprimées.'), + ] + for label, text in masquage: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── Tableau masqué / préservé ── + add_heading_styled(doc, 'Périmètre de détection', level=2) + + table = doc.add_table(rows=0, cols=2) + table.style = 'Table Grid' + add_table_row(table, ['Données masquées (PII)', 'Données préservées'], header=True) + + rows_data = [ + ('Noms et prénoms (patients)', 'Diagnostics et conclusions médicales'), + ('Noms et prénoms (médecins, soignants)', 'Traitements et posologies'), + ('Adresses postales', 'Actes médicaux (codage CCAM)'), + ('Numéros de téléphone', 'Résultats d\'examens et de biologie'), + ('Numéros de sécurité sociale', 'Dates de séjour et d\'intervention'), + ('Dates de naissance', 'Codage CIM-10'), + ('Noms d\'établissements', 'Comptes-rendus opératoires (contenu)'), + ('Codes-barres d\'identification', 'Structure et mise en page'), + ] + for masked, preserved in rows_data: + add_table_row(table, [masked, preserved]) + set_table_style(table) + + doc.add_paragraph() + + # ── Moteurs de détection ── + add_heading_styled(doc, 'Moteurs de détection', level=2) + add_para(doc, + 'Le logiciel combine 4 moteurs d\'intelligence artificielle spécialisés ' + 'en français clinique. Chaque moteur analyse le document indépendamment. ' + 'Les résultats sont croisés pour maximiser le rappel (recall) et minimiser ' + 'les oublis.', + space_after=Pt(8)) + + engines = [ + ('Modèle CamemBERT médical', + 'Modèle de langue français pré-entraîné, spécialisé dans la ' + 'reconnaissance d\'entités nommées en contexte clinique.'), + ('Détection zero-shot', + 'Modèle capable de détecter des données personnelles sans ' + 'entraînement spécifique, par compréhension sémantique du contexte.'), + ('Modèle fine-tuné', + 'Modèle entraîné spécifiquement sur plus de 1 000 documents médicaux ' + 'réels pour une précision maximale sur les formats cliniques français.'), + ('Bases de référence nationales', + '219 000 noms de famille INSEE, 36 000 prénoms, ' + '7 300 médicaments (BDPM), 108 000 établissements de santé (FINESS). ' + 'Ces bases servent à la fois à la détection et à l\'élimination ' + 'des faux positifs (termes médicaux confondus avec des noms).'), + ] + for label, text in engines: + add_labeled_para(doc, label, text) + + doc.add_paragraph() + + # ── Performances ── + add_heading_styled(doc, 'Performances mesurées', level=2) + + table2 = doc.add_table(rows=0, cols=2) + table2.style = 'Table Grid' + add_table_row(table2, ['Indicateur', 'Valeur'], header=True) + + perfs = [ + ('Taux de détection (recall)', '> 99% sur corpus réel de contrôle T2A'), + ('Types de documents validés', 'CR opératoires, anatomo-pathologie, bactériologie,\n' + 'anesthésie, courriers, CR d\'hospitalisation'), + ('Temps moyen par document', 'Quelques dizaines de secondes (poste standard)'), + ('Traitement par lot', '20-30 documents en 10-15 minutes'), + ('Principe de précaution', 'En cas de doute, le logiciel masque\n' + '(préférence faux positif sur faux négatif)'), + ] + for ind, val in perfs: + add_table_row(table2, [ind, val]) + set_table_style(table2) + + for row in table2.rows: + row.cells[0].width = Cm(5.5) + row.cells[1].width = Cm(10.5) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 6. FAQ DSI / RSSI / DPO +# ══════════════════════════════════════════════════════════════════════ + +def build_faq(doc): + add_heading_styled(doc, '6. Questions fréquentes — DSI / RSSI / DPO', level=1) + + faq = [ + ('Les données transitent-elles par un cloud ?', + 'Non. Le logiciel fonctionne intégralement hors-ligne. Aucune connexion ' + 'réseau n\'est établie, ni en entrée ni en sortie. Vérifiable par ' + 'capture réseau.'), + + ('Faut-il un agrément Hébergeur de Données de Santé (HDS) ?', + 'Non. Aucune donnée n\'est hébergée par un tiers. Le traitement est ' + 'effectué localement sur l\'infrastructure de l\'établissement.'), + + ('Y a-t-il un sous-traitant au sens du RGPD ?', + 'Non. Le logiciel est un outil exécuté en local. Aucune donnée n\'est ' + 'transmise à l\'éditeur ni à un tiers. L\'établissement est seul ' + 'responsable de traitement.'), + + ('Faut-il réaliser une AIPD ?', + 'Le logiciel réduit le risque sur les données personnelles. Il n\'introduit ' + 'pas de nouveau traitement à risque. Une AIPD simplifiée peut suffire, ' + 'selon la politique de l\'établissement et l\'avis du DPO.'), + + ('Le masquage est-il réversible ?', + 'Non. Le texte est supprimé du fichier PDF, pas simplement recouvert. ' + 'Aucun outil ne permet de récupérer les données masquées dans le ' + 'fichier anonymisé.'), + + ('Quelles données sont stockées par le logiciel ?', + 'Aucune donnée personnelle. Le logiciel ne maintient ni base de données, ' + 'ni journal nominatif, ni historique de traitement. Seul le fichier de ' + 'configuration (listes de termes) est persisté.'), + + ('Peut-on auditer le comportement réseau ?', + 'Oui. L\'absence de trafic est vérifiable par Wireshark, pare-feu ' + 'applicatif, ou exécution dans une VM sans interface réseau. ' + 'Le logiciel fonctionne de manière identique.'), + + ('L\'exécutable est-il signable ?', + 'Oui. L\'exécutable est compatible avec les outils de signature ' + 'Windows standard (signtool). L\'établissement peut appliquer ' + 'son propre certificat de signature de code.'), + + ('Comment s\'intègre-t-il avec le DPI ?', + 'Le logiciel est découplé du DPI. Il travaille sur des fichiers ' + 'exportés (PDF, DOCX, etc.). Aucun connecteur direct, aucun accès ' + 'aux bases cliniques, aucun risque d\'altération.'), + + ('Que se passe-t-il si un nom n\'est pas détecté ?', + 'L\'établissement peut ajouter le terme dans la blacklist via ' + 'l\'interface graphique. Il sera systématiquement masqué lors des ' + 'prochains traitements. La configuration est immédiatement active.'), + + ('Que se passe-t-il si un terme médical est masqué par erreur ?', + 'L\'établissement peut l\'ajouter dans la whitelist. Le terme ne sera ' + 'plus masqué. La configuration est échangeable entre sites.'), + + ('Quel est l\'impact sur la performance du poste ?', + 'Le traitement mobilise le processeur pendant quelques dizaines de ' + 'secondes par document. L\'utilisation mémoire reste sous 4 Go. ' + 'Le poste est utilisable pendant le traitement.'), + ] + + for question, answer in faq: + p = doc.add_paragraph() + run_q = p.add_run(question) + run_q.bold = True + run_q.font.size = Pt(10) + p.paragraph_format.space_after = Pt(2) + + p2 = doc.add_paragraph() + run_a = p2.add_run(answer) + run_a.font.size = Pt(9.5) + run_a.font.color.rgb = RGBColor(0x33, 0x33, 0x33) + p2.paragraph_format.space_after = Pt(10) + p2.paragraph_format.left_indent = Cm(0.5) + + add_section_break(doc) + + +# ══════════════════════════════════════════════════════════════════════ +# 7. MATRICE DE CONFORMITÉ +# ══════════════════════════════════════════════════════════════════════ + +def build_matrice(doc): + add_heading_styled(doc, '7. Matrice de conformité', level=1) + add_audience_tag(doc, '→ DPO, RSSI — synthèse décisionnelle') + + add_para(doc, + 'Tableau récapitulatif des exigences réglementaires et de sécurité, ' + 'et de la réponse apportée par le logiciel.', + space_after=Pt(10)) + + table = doc.add_table(rows=0, cols=3) + table.style = 'Table Grid' + add_table_row(table, ['Exigence', 'Statut', 'Commentaire'], header=True) + + matrix = [ + ('Données hors établissement', 'Conforme', + 'Aucune donnée ne quitte le poste'), + ('Sous-traitance RGPD', 'Conforme', + 'Aucun sous-traitant'), + ('Privacy by design (Art. 25)', 'Conforme', + 'IA embarquée, hors-ligne natif'), + ('Sécurité du traitement (Art. 32)', 'Conforme', + 'Périmètre = poste local'), + ('AIPD (Art. 35)', 'Simplifié', + 'Réduit le risque, ne le crée pas'), + ('Agrément HDS', 'Non requis', + 'Pas d\'hébergement tiers'), + ('PGSSI-S', 'Compatible', + 'Aucun flux réseau ouvert'), + ('AI Act — risque', 'Risque limité', + 'Outil d\'aide, pas de décision automatisée'), + ('Masquage irréversible', 'Conforme', + 'Suppression + rectangle noir'), + ('Signature de code', 'Compatible', + 'Signable par certificat établissement'), + ('Auditabilité réseau', 'Vérifiable', + 'Wireshark / pare-feu / sandbox'), + ('Droits administrateur', 'Non requis', + 'Espace utilisateur standard'), + ] + for exigence, statut, comment in matrix: + row = table.add_row() + # Exigence + cell0 = row.cells[0] + cell0.text = '' + p0 = cell0.paragraphs[0] + run0 = p0.add_run(exigence) + run0.font.size = Pt(9) + p0.paragraph_format.space_before = Pt(3) + p0.paragraph_format.space_after = Pt(3) + + # Statut avec couleur + cell1 = row.cells[1] + cell1.text = '' + p1 = cell1.paragraphs[0] + run1 = p1.add_run(statut) + run1.font.size = Pt(9) + run1.bold = True + if statut in ('Conforme', 'Compatible', 'Vérifiable'): + run1.font.color.rgb = GREEN_DARK + elif statut in ('Non requis', 'Simplifié', 'Risque limité'): + run1.font.color.rgb = BLUE_DARK + p1.paragraph_format.space_before = Pt(3) + p1.paragraph_format.space_after = Pt(3) + + # Commentaire + cell2 = row.cells[2] + cell2.text = '' + p2 = cell2.paragraphs[0] + run2 = p2.add_run(comment) + run2.font.size = Pt(9) + run2.font.color.rgb = RGBColor(0x55, 0x55, 0x55) + p2.paragraph_format.space_before = Pt(3) + p2.paragraph_format.space_after = Pt(3) + + set_table_style(table) + + for row in table.rows: + row.cells[0].width = Cm(4.5) + row.cells[1].width = Cm(2.5) + row.cells[2].width = Cm(9) + + +# ══════════════════════════════════════════════════════════════════════ +# ASSEMBLAGE FINAL +# ══════════════════════════════════════════════════════════════════════ + +def main(): + doc = Document() + + # ── Style par défaut ── + style = doc.styles['Normal'] + style.font.name = 'Calibri' + style.font.size = Pt(10) + style.paragraph_format.space_after = Pt(4) + + # Marges + for section in doc.sections: + section.top_margin = Cm(1.5) + section.bottom_margin = Cm(1.5) + section.left_margin = Cm(2) + section.right_margin = Cm(2) + + # Construction + build_title_page(doc) + build_architecture(doc) + build_conformite(doc) + build_securite(doc) + build_deploiement(doc) + build_garanties(doc) + build_faq(doc) + build_matrice(doc) + + # Sauvegarde + output_path = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + 'Fiche_Technique_DSI_RSSI_DPO.docx' + ) + doc.save(output_path) + print(f'Fiche technique générée : {output_path}') + + +if __name__ == '__main__': + main()