docs: scripts de génération des fiches produit et technique DSI/RSSI/DPO
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
522
scripts/generate_fiche_produit.py
Normal file
522
scripts/generate_fiche_produit.py
Normal file
@@ -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()
|
||||||
808
scripts/generate_fiche_technique_dsi.py
Normal file
808
scripts/generate_fiche_technique_dsi.py
Normal file
@@ -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()
|
||||||
Reference in New Issue
Block a user