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:
2026-04-14 10:17:14 +02:00
parent 51c75558bc
commit b23355ed23
2 changed files with 1330 additions and 0 deletions

View 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()

View 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()