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