Files
cesizen-individuel/generate_slides.py
T
Azmog e8b49c6b94 docs: ajoute les livrables de soutenance et corrige le nombre de sous-émotions
- generate_diagrams.py : schémas PNG (MCD Merise, cas d'usage UML, séquences
  login/saisie, architecture MVC, 3 maquettes) générés avec Pillow
- generate_cahier_charges.py : intègre les schémas et corrige les écarts
  code/dossier (28 sous-émotions, table audits, est_publie)
- generate_slides.py : support de soutenance .pptx (16 slides) via python-pptx
- Plan_Oral_CESIZen.md : déroulé minuté 20 min + préparation Q/R
- assets_dossier/ : schémas sources
- CLAUDE.md : 31 -> 28 sous-émotions (aligné sur le seed réel)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 17:19:30 +02:00

435 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Génère le support de soutenance CESIZen (PowerPoint 16:9 éditable).
Réutilise les schémas PNG produits par generate_diagrams.py.
Sortie : ./CESIZen_Soutenance.pptx
"""
import os
from PIL import Image
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# ─── Charte CESIZen ────────────────────────────────────────────────
GREEN = RGBColor(6, 198, 86)
GREEN_D = RGBColor(4, 150, 65)
BLUE = RGBColor(0, 70, 130)
BLUE_L = RGBColor(230, 240, 250)
YELLOW = RGBColor(252, 225, 23)
GREY_D = RGBColor(50, 50, 50)
GREY = RGBColor(110, 110, 110)
WHITE = RGBColor(255, 255, 255)
INK = RGBColor(35, 35, 45)
HERE = os.path.dirname(os.path.abspath(__file__))
ASSETS = os.path.join(HERE, 'assets_dossier')
OUT = os.path.join(HERE, 'CESIZen_Soutenance.pptx')
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
SW, SH = prs.slide_width, prs.slide_height
BLANK = prs.slide_layouts[6]
def _no_line(shape):
shape.line.fill.background()
def rect(slide, x, y, w, h, color, line=None):
s = slide.shapes.add_shape(MSO_SHAPE.RECTANGLE, x, y, w, h)
s.fill.solid()
s.fill.fore_color.rgb = color
if line is None:
_no_line(s)
else:
s.line.color.rgb = line
s.line.width = Pt(1)
s.shadow.inherit = False
return s
def textbox(slide, x, y, w, h, anchor=MSO_ANCHOR.TOP):
tb = slide.shapes.add_textbox(x, y, w, h)
tf = tb.text_frame
tf.word_wrap = True
tf.vertical_anchor = anchor
tf.margin_left = Inches(0.05)
tf.margin_right = Inches(0.05)
tf.margin_top = Inches(0.02)
tf.margin_bottom = Inches(0.02)
return tb, tf
def set_par(p, text, size, color, bold=False, align=PP_ALIGN.LEFT,
font='Arial', space_after=8):
p.text = text
p.alignment = align
p.space_after = Pt(space_after)
for r in p.runs:
r.font.size = Pt(size)
r.font.color.rgb = color
r.font.bold = bold
r.font.name = font
def footer(slide):
tb, tf = textbox(slide, Inches(0.4), Inches(7.05), Inches(12.5), Inches(0.35))
set_par(tf.paragraphs[0],
"CESIZen · Bloc INFCDAAL1 — Concevoir les solutions logicielles · Adam Marzuk",
9, GREY, align=PP_ALIGN.CENTER, space_after=0)
def band_title(slide, text, num=None):
rect(slide, 0, 0, SW, Inches(1.05), BLUE)
rect(slide, 0, Inches(1.05), SW, Inches(0.06), GREEN)
label = f"{num}. {text}" if num else text
tb, tf = textbox(slide, Inches(0.5), Inches(0.12), Inches(12.3), Inches(0.85),
anchor=MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], label, 30, WHITE, bold=True, space_after=0)
def bullets(slide, items, x, y, w, h, size=18, gap=10):
tb, tf = textbox(slide, x, y, w, h)
for i, it in enumerate(items):
lvl = 0
txt = it
if isinstance(it, tuple):
txt, lvl = it
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
prefix = "• " if lvl == 0 else "– "
set_par(p, prefix + txt, size if lvl == 0 else size - 2,
INK if lvl == 0 else GREY_D, bold=False, space_after=gap)
p.level = lvl
if lvl == 1:
p.runs[0].font.color.rgb = GREY
return tb
def image_fit(slide, name, x, y, max_w, max_h, caption=None):
path = os.path.join(ASSETS, name)
iw, ih = Image.open(path).size
ar = iw / ih
box_ar = max_w / max_h
if box_ar > ar:
h = max_h
w = int(h * ar)
else:
w = max_w
h = int(w / ar)
left = x + (max_w - w) // 2
top = y + (max_h - h) // 2
slide.shapes.add_picture(path, left, top, width=w, height=h)
if caption:
tb, tf = textbox(slide, x, y + max_h + Inches(0.02), max_w, Inches(0.35))
set_par(tf.paragraphs[0], caption, 11, GREY, align=PP_ALIGN.CENTER, space_after=0)
# ════════════════════════════════════════════════════════════════════
# Slide 1 — Titre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
rect(s, 0, 0, SW, SH, WHITE)
rect(s, 0, Inches(2.5), SW, Inches(0.08), GREEN)
tb, tf = textbox(s, Inches(1), Inches(0.9), Inches(11.3), Inches(1.5))
set_par(tf.paragraphs[0], "CESIZen", 60, GREEN, bold=True, align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(2.65), Inches(11.3), Inches(0.9))
set_par(tf.paragraphs[0], "L'application de votre santé mentale", 26, GREY_D,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(3.7), Inches(11.3), Inches(0.7))
set_par(tf.paragraphs[0], "Cahier des charges & conception logicielle", 18, BLUE,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(1), Inches(5.4), Inches(11.3), Inches(1.4))
for i, line in enumerate([
"Adam Marzuk — CESI École d'Ingénieurs",
"Bloc INFCDAAL1 — Concevoir les solutions logicielles",
"Commanditaire : Ministère des Solidarités et de la Santé"]):
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
set_par(p, line, 15, GREY, align=PP_ALIGN.CENTER, space_after=4)
# ════════════════════════════════════════════════════════════════════
# Slide 2 — Sommaire
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Sommaire")
bullets(s, [
"Contexte, objectifs et enjeux",
"Parties prenantes et acteurs",
"Besoins reformulés et priorisation",
"Réponse fonctionnelle (Comptes · Informations · Tracker)",
"Modélisation : cas d'usage UML, MCD, architecture MVC",
"Données personnelles et sensibles (RGPD)",
"Prototype et perspectives",
], Inches(1.2), Inches(1.5), Inches(11), Inches(5.2), size=22, gap=14)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 3 — Contexte & enjeux
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Contexte, objectifs et enjeux", 1)
bullets(s, [
"Commanditaire : Ministère des Solidarités et de la Santé.",
"Enjeu : une plateforme grand public pour prévenir le stress et accompagner la santé mentale.",
"Constat : 22 % des Français touchés par un trouble psychique au cours de leur vie (Santé Publique France, 2023).",
"Frein : stigmatisation et méconnaissance limitent l'accès au soin.",
], Inches(0.8), Inches(1.4), Inches(11.7), Inches(2.6), size=18)
# encadré objectifs
rect(s, Inches(0.8), Inches(4.2), Inches(11.7), Inches(2.4), BLUE_L)
rect(s, Inches(0.8), Inches(4.2), Inches(3.4), Inches(0.55), BLUE)
tb, tf = textbox(s, Inches(0.95), Inches(4.25), Inches(3.2), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "4 objectifs stratégiques", 15, WHITE, bold=True, space_after=0)
bullets(s, [
"Informer : contenu éducatif fiable sur la santé mentale.",
"Suivre : permettre à chacun de tracer son humeur dans le temps.",
"Orienter : diriger les personnes en détresse (numéros d'urgence).",
"Protéger : confidentialité absolue des données émotionnelles (RGPD + HDS).",
], Inches(1.0), Inches(4.9), Inches(11.3), Inches(1.6), size=15, gap=4)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 4 — Parties prenantes & acteurs
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Parties prenantes et acteurs", 2)
tb, tf = textbox(s, Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Parties prenantes du projet", 16, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Commanditaire : Ministère (pilotage, exigences santé publique et RGPD).",
"Bénéficiaires : grand public, personnes en quête de mieux-être psychique.",
"Exploitants : administrateurs et professionnels de santé (modération, contenus).",
], Inches(0.8), Inches(1.85), Inches(11.7), Inches(2.0), size=17)
tb, tf = textbox(s, Inches(0.8), Inches(4.0), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "3 acteurs du système", 16, GREEN_D, bold=True, space_after=0)
cards = [("Visiteur anonyme", "Consulte les informations, crée un compte.", GREEN),
("Utilisateur connecté", "Journal d'émotions, rapports, profil, RGPD.", BLUE),
("Administrateur", "Gère utilisateurs, contenus et émotions.", GREEN_D)]
for i, (t, d, col) in enumerate(cards):
x = Inches(0.8 + i * 4.0)
rect(s, x, Inches(4.55), Inches(3.7), Inches(1.8), BLUE_L)
rect(s, x, Inches(4.55), Inches(3.7), Inches(0.55), col)
tb, tf = textbox(s, x + Inches(0.1), Inches(4.58), Inches(3.5), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], t, 14, WHITE, bold=True, space_after=0)
tb, tf = textbox(s, x + Inches(0.15), Inches(5.2), Inches(3.4), Inches(1.0))
set_par(tf.paragraphs[0], d, 13, INK, space_after=0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 5 — Besoins & périmètre
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Besoins reformulés et périmètre", 3)
cols = [
("2 modules OBLIGATOIRES", GREEN, [
"Comptes utilisateurs : inscription JWT, profil, reset, admin, RGPD.",
"Informations : pages de contenu santé mentale, CRUD admin.",
]),
("1 module AU CHOIX (retenu)", BLUE, [
"Tracker d'émotions : journal, saisies, rapports périodiques.",
"Hiérarchie : 7 émotions de base + 28 sous-émotions.",
]),
("Modules ÉCARTÉS", GREY, [
"Diagnostic de stress.",
"Cohérence cardiaque.",
"Activités de détente.",
]),
]
for i, (t, col, items) in enumerate(cols):
x = Inches(0.7 + i * 4.05)
rect(s, x, Inches(1.5), Inches(3.85), Inches(0.6), col)
tb, tf = textbox(s, x + Inches(0.1), Inches(1.53), Inches(3.65), Inches(0.55), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], t, 13, WHITE, bold=True, space_after=0)
bullets(s, items, x + Inches(0.05), Inches(2.3), Inches(3.85), Inches(3.5), size=13, gap=8)
tb, tf = textbox(s, Inches(0.7), Inches(6.0), Inches(11.9), Inches(0.8))
set_par(tf.paragraphs[0],
"Périmètre maîtrisé : 3 modules livrés en qualité plutôt que 6 modules superficiels.",
15, GREEN_D, bold=True, align=PP_ALIGN.CENTER, space_after=0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 6 — Priorisation
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Critères de priorisation et pondération", 3)
bullets(s, [
"5 critères pondérés, total sur 15 : Complexité, Valeur Ministère, Valeur Utilisateur, Nécessité, Interdépendance.",
], Inches(0.8), Inches(1.35), Inches(11.7), Inches(0.8), size=16)
# mini-tableau top priorités
rows = [
("Fonctionnalité", "Score /15", True),
("Journal d'émotions · Rapports · Diagnostic de stress", "10", False),
("Cryptage · Inscription · Cohérence cardiaque · Ajout tracker", "9", False),
("Conformité RGPD", "8", False),
("Réinitialisation mot de passe", "7", False),
]
ty = 2.3
for i, (a, b, head) in enumerate(rows):
col = BLUE if head else (BLUE_L if i % 2 else WHITE)
rect(s, Inches(0.8), Inches(ty + i * 0.62), Inches(9.4), Inches(0.62), col,
line=RGBColor(210, 220, 230))
rect(s, Inches(10.2), Inches(ty + i * 0.62), Inches(2.3), Inches(0.62), col,
line=RGBColor(210, 220, 230))
tb, tf = textbox(s, Inches(0.95), Inches(ty + i * 0.62), Inches(9.1), Inches(0.62), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], a, 13, WHITE if head else INK, bold=head, space_after=0)
tb, tf = textbox(s, Inches(10.2), Inches(ty + i * 0.62), Inches(2.3), Inches(0.62), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], b, 13, WHITE if head else GREEN_D, bold=True,
align=PP_ALIGN.CENTER, space_after=0)
tb, tf = textbox(s, Inches(0.8), Inches(6.05), Inches(11.9), Inches(0.8))
set_par(tf.paragraphs[0],
"→ Le Tracker cumule 4 fonctionnalités notées 9-10/15 : module au choix le plus rentable.",
15, GREEN_D, bold=True, space_after=0)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 7 — Réponse fonctionnelle Comptes
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Réponse fonctionnelle — Comptes utilisateurs", 4)
bullets(s, [
"Inscription et connexion sécurisées par JWT (tymon/jwt-auth).",
"Consentement RGPD obligatoire, non pré-coché, à l'inscription.",
"Gestion du profil : édition, changement de mot de passe.",
"Espace admin : CRUD utilisateurs, activation/désactivation, rôles.",
"RGPD : export des données (JSON), anonymisation, soft delete + purge 30 j.",
"Mot de passe : bcrypt 12 rounds, min. 8 caractères.",
], Inches(0.8), Inches(1.5), Inches(8.0), Inches(5.2), size=17, gap=12)
image_fit(s, "mockup_inscription.png", Inches(9.1), Inches(1.4), Inches(3.7), Inches(5.2),
caption="Écran d'inscription (mobile)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 8 — Réponse fonctionnelle Informations
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Réponse fonctionnelle — Informations", 4)
bullets(s, [
"Pages de contenu santé mentale (feeds) accessibles au grand public.",
"Page liste (cartes : titre, extrait, image, date) et page détail (HTML rendu).",
"Back-office admin : création / édition / publication / suppression.",
"Slug unique généré automatiquement à partir du titre.",
"Champ booléen est_publie : brouillon (invisible) ou publié (visible).",
"Modération obligatoire : un administrateur valide avant publication.",
"Tri par ordre numérique pour mettre en avant un contenu.",
], Inches(0.8), Inches(1.5), Inches(11.7), Inches(5.0), size=18, gap=12)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 9 — Réponse fonctionnelle Tracker
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Réponse fonctionnelle — Tracker d'émotions", 4)
bullets(s, [
"Wizard d'ajout en 3 étapes : émotion de base → sous-émotion + intensité → date + note.",
"Journal de bord : consultation, modification, suppression des saisies.",
"Rapports analytiques sur 4 périodes : semaine, mois, trimestre, année.",
"Statistiques : intensité moyenne, émotion dominante, répartition, évolution.",
"Hiérarchie configurable par l'admin : 7 émotions + 28 sous-émotions.",
"Web (graphiques Recharts) et mobile (Expo) synchronisés via l'API.",
], Inches(0.7), Inches(1.45), Inches(7.0), Inches(5.3), size=16, gap=10)
image_fit(s, "mockup_wizard.png", Inches(7.9), Inches(1.35), Inches(2.5), Inches(5.3))
image_fit(s, "mockup_rapports.png", Inches(10.5), Inches(2.4), Inches(2.7), Inches(3.2))
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 10 — Cas d'usage UML
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Modélisation — Cas d'usage UML", 5)
image_fit(s, "usecase.png", Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4),
caption="3 acteurs · cas d'usage groupés par module · modules écartés non représentés")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 11 — MCD
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Modélisation — MCD (Merise)", 5)
image_fit(s, "mcd.png", Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4),
caption="8 entités · cardinalités Merise · hiérarchie d'émotions (auto-référence)")
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 12 — Architecture MVC
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Modélisation — Architecture MVC", 5)
image_fit(s, "mvc.png", Inches(0.6), Inches(1.25), Inches(8.6), Inches(5.5))
bullets(s, [
"API REST Laravel 11 = Contrôleur central.",
"Eloquent ORM + PostgreSQL 16 = Modèle.",
"Next.js (web) + React Native (mobile) = Vues.",
"Une seule logique métier, deux clients.",
"Sécurité centralisée : FormRequest, middleware, JWT.",
"Stack open-source, conteneurisée (Docker).",
], Inches(9.3), Inches(1.5), Inches(3.5), Inches(5.0), size=14, gap=12)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 13 — RGPD & données sensibles
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Données personnelles et sensibles (RGPD)", 6)
bullets(s, [
"Données de santé mentale = données sensibles (RGPD art. 9).",
"Base légale : consentement explicite ; finalités documentées.",
"Droits CNIL implémentés : accès, rectification, effacement, portabilité (export JSON).",
"Anonymisation et soft delete + purge à 30 jours.",
], Inches(0.8), Inches(1.45), Inches(11.7), Inches(2.4), size=17, gap=10)
rect(s, Inches(0.8), Inches(4.1), Inches(11.7), Inches(2.5), BLUE_L)
rect(s, Inches(0.8), Inches(4.1), Inches(3.6), Inches(0.55), GREEN_D)
tb, tf = textbox(s, Inches(0.95), Inches(4.13), Inches(3.4), Inches(0.5), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Mesures de sécurité", 15, WHITE, bold=True, space_after=0)
bullets(s, [
"Chiffrement en transit (HTTPS/TLS 1.3) et au repos (AES-256).",
"Authentification JWT signée, expiration 60 min, throttle login.",
"Hébergement certifié HDS (Scaleway / OVH Healthcare).",
"Journalisation : table audits trace toute action admin sensible.",
], Inches(1.0), Inches(4.8), Inches(11.3), Inches(1.7), size=14, gap=6)
footer(s)
# ════════════════════════════════════════════════════════════════════
# Slide 14 — Prototype & perspectives
# ════════════════════════════════════════════════════════════════════
s = prs.slides.add_slide(BLANK)
band_title(s, "Prototype et perspectives", 7)
tb, tf = textbox(s, Inches(0.8), Inches(1.4), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Un prototype fonctionnel accompagne le dossier", 17, GREEN_D,
bold=True, space_after=0)
bullets(s, [
"Backend Laravel 11 (API REST JWT) + PostgreSQL 16.",
"Frontend web Next.js 16 et application mobile Expo (iOS / Android).",
"Conteneurisation Docker Compose, dépôt Git.",
], Inches(0.8), Inches(1.9), Inches(11.7), Inches(1.9), size=16)
tb, tf = textbox(s, Inches(0.8), Inches(4.0), Inches(11.7), Inches(0.5))
set_par(tf.paragraphs[0], "Perspectives d'évolution", 17, GREEN_D, bold=True, space_after=0)
bullets(s, [
"Activer les modules écartés (Diagnostic, Cohérence cardiaque, Activités) sans réécrire le modèle.",
"Open Data : base anonymisée pour études épidémiologiques.",
"Interopérabilité : Mon Espace Santé (FHIR) ; internationalisation (i18n).",
], Inches(0.8), Inches(4.5), Inches(11.7), Inches(1.7), size=16)
rect(s, 0, Inches(6.6), SW, Inches(0.9), BLUE)
tb, tf = textbox(s, Inches(0.8), Inches(6.62), Inches(11.7), Inches(0.85), MSO_ANCHOR.MIDDLE)
set_par(tf.paragraphs[0], "Merci de votre attention — Questions / Réponses", 20, WHITE,
bold=True, align=PP_ALIGN.CENTER, space_after=0)
# ════════════════════════════════════════════════════════════════════
# Annexes (backup Q/R) — séquences
# ════════════════════════════════════════════════════════════════════
for name, title, cap in [
("seq_login.png", "Annexe — Séquence d'authentification JWT",
"Validation · vérification bcrypt · émission du token JWT"),
("seq_saisie.png", "Annexe — Séquence d'ajout d'une saisie",
"Wizard 3 étapes · validation · persistance"),
]:
s = prs.slides.add_slide(BLANK)
band_title(s, title)
image_fit(s, name, Inches(0.6), Inches(1.3), Inches(12.1), Inches(5.4), caption=cap)
footer(s)
prs.save(OUT)
print(f"Support de soutenance généré : {OUT}")
print(f"Nombre de slides : {len(prs.slides._sldIdLst)}")