DOCS/outputs/platform/build_platform_deck.py
Thomas Fransolet d14f151517 Livrables de presentation, seed de demo MNAHA et wireframes
- outputs/platform, outputs/prospect : scripts de construction des decks
  commerciaux + captures sources et .pptx generes.
- outputs/mnaha : seed complet de l'instance de demo luxembourgeoise
  (scripts par etape, contenus, images generees), le deck en version
  marque blanche et en version brandee, le PDF de presentation.
- outputs/Wireframes : maquettes app mobile, app web et manager.
- interview clients : notes de l'entretien Fourneau St Michel.

state.json et les __pycache__ sont ignores : le premier porte les cles
d'API de l'instance seedee, les scripts le regenerent.
2026-09-11 15:47:57 +02:00

491 lines
24 KiB
Python

# -*- coding: utf-8 -*-
"""Deck de présentation de la plateforme MyInfoMate — audience interne / acquéreur.
Réutilise les primitives de mise en page du deck MNAHA. Les chiffres viennent de
mesures réelles sur les dépôts (voir CHIFFRES.md), pas d'estimations.
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
sys.path.insert(0, str(ROOT.parent / "mnaha"))
from pptx import Presentation
from pptx.dml.color import RGBColor
from pptx.enum.shapes import MSO_SHAPE
from pptx.enum.text import MSO_ANCHOR, PP_ALIGN
from pptx.util import Inches, Pt
from build_decks import ( # noqa: E402
H, LOGO, MARGIN, W, Theme, Y_BODY_NO_SUB, bullets, footer, kicker_title,
page_base, placeholder, rect, sanitize_package, text,
)
def slide_cover(prs, th):
s = page_base(prs, th, dark=True)
rect(s, 0, H - Inches(0.9), W, Inches(0.9), fill=th.accent)
text(s, MARGIN, Inches(2.05), Inches(10.4), Inches(0.4),
[("PLATEFORME DE MÉDIATION NUMÉRIQUE", 11, True, th.accent_bright,
th.body_font, 0)])
text(s, MARGIN, Inches(2.6), Inches(10.6), Inches(1.8),
[("MyInfoMate", 52, True, RGBColor(0xFF, 0xFF, 0xFF), th.title_font, 0)],
line_spacing=1.02)
text(s, MARGIN, Inches(3.95), Inches(9.8), Inches(1.2),
[("Un back-end, un CMS, trois applications visiteur.\n"
"Six ans de développement, trois sites en production.", 16, False,
RGBColor(0xB8, 0xC2, 0xCE), th.body_font, 0)], line_spacing=1.4)
if LOGO.exists():
s.shapes.add_picture(str(LOGO), W - MARGIN - Inches(1.4), Inches(0.8),
height=Inches(1.2))
return s
def slide_pitch(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "En une phrase",
"Un musée saisit son contenu une fois. Il le diffuse sur une borne, "
"un téléphone et le web.")
text(s, MARGIN, Inches(3.35), Inches(11.0), Inches(1.6),
[("La valeur n'est pas dans une application : elle est dans le back-end "
"multi-locataire qui les alimente toutes, et dans le CMS qui rend la mise "
"à jour possible sans développeur.", 17, False, th.ink, th.body_font, 0)],
line_spacing=1.45)
rect(s, MARGIN, Inches(5.35), Inches(11.0), Pt(1), fill=th.rule)
text(s, MARGIN, Inches(5.6), Inches(11.0), Inches(0.8),
[("Chaque client est une instance isolée, avec ses contenus, ses couleurs, "
"ses langues, ses quotas et ses statistiques.", 13, False, th.muted,
th.body_font, 0)], line_spacing=1.35)
footer(s, th, n, "En une phrase")
return s
def slide_bricks(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "Les cinq briques",
"Une seule source de données, quatre surfaces d'usage.")
hub_w, hub_h = Inches(3.2), Inches(1.5)
hub_x = (W - hub_w) / 2
hub_y = Inches(3.10)
rect(s, hub_x, hub_y, hub_w, hub_h, fill=th.ink)
text(s, hub_x + Inches(0.25), hub_y, hub_w - Inches(0.5), hub_h,
[("manager-service", 16, True, RGBColor(0xFF, 0xFF, 0xFF), th.title_font, 0),
("API REST, base PostgreSQL,\nmulti-locataire", 11, False,
RGBColor(0xC5, 0xCE, 0xD8), th.body_font, 6)],
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.2)
cw = Inches(2.75)
ch = Inches(1.35)
gap = Inches(0.28)
total = 4 * cw + 3 * gap
x0 = (W - total) / 2
y = hub_y + hub_h + Inches(0.75)
apps = [
("manager-app", "Le CMS", "Flutter web — 79 écrans"),
("tablet-app", "Borne tactile", "Flutter Android — 38 écrans"),
("mymuseum-visitapp", "App visiteur", "Flutter iOS/Android — 54 écrans"),
("visitapp-web", "Visite sans install.", "Next.js — 34 composants"),
]
for i, (name, role, tech) in enumerate(apps):
x = x0 + i * (cw + gap)
rect(s, x, y, cw, ch, fill=th.surface, line=th.rule)
rect(s, x, y, cw, Pt(3), fill=th.accent)
rect(s, x + cw / 2 - Pt(1), hub_y + hub_h, Pt(2), Inches(0.75), fill=th.rule)
text(s, x + Inches(0.2), y + Inches(0.28), cw - Inches(0.4), Inches(0.3),
[(name, 12.5, True, th.ink, th.title_font, 0)], align=PP_ALIGN.CENTER)
text(s, x + Inches(0.2), y + Inches(0.68), cw - Inches(0.4), Inches(0.3),
[(role, 11.5, False, th.accent, th.body_font, 0)], align=PP_ALIGN.CENTER)
text(s, x + Inches(0.2), y + Inches(1.02), cw - Inches(0.4), Inches(0.3),
[(tech, 9.5, False, th.muted, th.body_font, 0)], align=PP_ALIGN.CENTER)
rect(s, x0, hub_y + hub_h + Inches(0.37), total, Pt(2), fill=th.rule)
footer(s, th, n, "Architecture")
return s
def slide_figures(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "Le patrimoine",
"Ce qui existe, mesuré sur les dépôts.")
figures = [
("181 000", "lignes de code écrites à la main",
"hors 34 000 lignes de client API généré"),
("867", "commits", "décembre 2020 → août 2026"),
("5", "applications", "back-end, CMS, borne, mobile, web"),
("145", "points d'API", "répartis sur 22 contrôleurs"),
("67", "migrations de base", "23 tables métier"),
("202", "tests automatisés", "sur le back-end"),
]
cols = 3
cw = (W - 2 * MARGIN - Inches(0.5)) / cols
ch = Inches(1.4)
for i, (big, label, sub) in enumerate(figures):
x = MARGIN + (i % cols) * (cw + Inches(0.25))
y = Inches(3.25) + (i // cols) * (ch + Inches(0.3))
rect(s, x, y, cw, ch, fill=th.surface, line=th.rule)
rect(s, x, y, Pt(3), ch, fill=th.accent)
text(s, x + Inches(0.3), y + Inches(0.18), cw - Inches(0.5), Inches(0.5),
[(big, 27, True, th.ink, th.title_font, 0)])
text(s, x + Inches(0.3), y + Inches(0.72), cw - Inches(0.5), Inches(0.3),
[(label, 12, True, th.ink, th.body_font, 0)])
text(s, x + Inches(0.3), y + Inches(1.02), cw - Inches(0.5), Inches(0.3),
[(sub, 9.5, False, th.muted, th.body_font, 0)])
footer(s, th, n, "Le patrimoine")
return s
def slide_split(prs, th, n, kicker, title, subtitle, items, ref=None, cap_label=None,
cap_hint=None, portrait=False):
s = page_base(prs, th)
kicker_title(s, th, kicker, title, subtitle, width=Inches(6.05))
bullets(s, th, MARGIN, Inches(3.85), Inches(5.75), items, size=12.5,
gap=Inches(0.15))
if ref:
if portrait:
placeholder(s, th, Inches(8.35), Inches(1.15), Inches(2.85), Inches(5.5),
ref, cap_label, cap_hint)
else:
placeholder(s, th, Inches(7.35), Inches(1.75), Inches(5.1), Inches(3.9),
ref, cap_label, cap_hint)
footer(s, th, n, kicker)
return s
def slide_cards(prs, th, n, kicker, title, subtitle, cards, label=None):
s = page_base(prs, th)
y = kicker_title(s, th, kicker, title, subtitle)
if subtitle:
y = Inches(3.85)
cw = (W - 2 * MARGIN - Inches(0.6)) / 3
for i, (head, body) in enumerate(cards):
x = MARGIN + i * (cw + Inches(0.3))
rect(s, x, y, cw, Inches(2.6), fill=th.surface, line=th.rule)
rect(s, x, y, cw, Pt(3), fill=th.accent)
text(s, x + Inches(0.38), y + Inches(0.38), cw - Inches(0.76), Inches(0.8),
[(head, 15, True, th.ink, th.title_font, 0)], line_spacing=1.18)
text(s, x + Inches(0.38), y + Inches(1.12), cw - Inches(0.76), Inches(1.3),
[(body, 11.5, False, th.muted, th.body_font, 0)], line_spacing=1.35)
footer(s, th, n, label or kicker)
return s
def slide_pricing(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "Modèle économique",
"Abonnement mensuel par site, options facturées à part.")
plans = [
("Essentiel", "39 € / mois", ["1 Go de médias", "Statistiques 30 jours",
"Assistant IA en option"]),
("Pro", "99 € / mois", ["15 Go de médias", "Statistiques 30 jours",
"Assistant IA en option"]),
("Premium", "179 € / mois", ["50 Go de médias", "Statistiques avancées",
"20 M de jetons IA / mois"]),
("Enterprise", "sur devis", ["Stockage illimité", "Statistiques avancées",
"Quota IA sur mesure"]),
]
cw = (W - 2 * MARGIN - Inches(0.75)) / 4
y = Inches(3.15)
for i, (name, price, feats) in enumerate(plans):
x = MARGIN + i * (cw + Inches(0.25))
rect(s, x, y, cw, Inches(2.75), fill=th.surface, line=th.rule)
rect(s, x, y, cw, Pt(3), fill=th.accent)
text(s, x + Inches(0.3), y + Inches(0.32), cw - Inches(0.6), Inches(0.3),
[(name, 14, True, th.ink, th.title_font, 0)])
text(s, x + Inches(0.3), y + Inches(0.72), cw - Inches(0.6), Inches(0.35),
[(price, 16, True, th.accent, th.body_font, 0)])
cy = y + Inches(1.3)
for f in feats:
text(s, x + Inches(0.3), cy, cw - Inches(0.6), Inches(0.45),
[(f, 10.5, False, th.muted, th.body_font, 0)], line_spacing=1.25)
cy += Inches(0.42)
text(s, MARGIN, Inches(6.15), Inches(11.0), Inches(0.5),
[("Application native, hors-ligne et balises, notifications et traduction "
"automatique se règlent par instance, en dehors du plan.", 11, False,
th.muted, th.body_font, 0)], line_spacing=1.3)
footer(s, th, n, "Modèle économique")
return s
def slide_roadmap(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "Feuille de route",
"Ce qui est déjà écrit, et ce qui reste à construire.")
rows = [
("Avatar IA sur borne", "À construire",
"Un guide incarné à l'écran, qui parle et écoute. Le socle est là : "
"l'assistant, la synthèse vocale et la reconnaissance existent déjà."),
("Assistant vocal proactif", "Code écrit, non déployé",
"Le commentaire se déclenche à l'approche d'une balise ou d'une zone. "
"Orchestrateur vocal, mot de réveil et moteurs TTS sont dans le dépôt."),
("Lunettes connectées", "R&D avancée",
"Intégration Ray-Ban Meta : le visiteur regarde une œuvre et pose sa "
"question, sans sortir son téléphone."),
("Plan d'étage interactif", "À construire",
"Les cartes sont géographiques. Le plan de salles cliquable est la "
"demande la plus fréquente des musées d'intérieur."),
("Borne VR d'accueil", "Étude",
"Le rendu du CMS porté sur casque, pour une expérience d'accueil."),
]
y = Inches(3.05)
for name, status, body in rows:
rect(s, MARGIN, y, W - 2 * MARGIN, Inches(0.68), fill=th.surface, line=th.rule)
rect(s, MARGIN, y, Pt(3), Inches(0.68), fill=th.accent)
text(s, MARGIN + Inches(0.28), y + Inches(0.21), Inches(2.85), Inches(0.3),
[(name, 12.5, True, th.ink, th.title_font, 0)])
text(s, MARGIN + Inches(3.25), y + Inches(0.23), Inches(2.1), Inches(0.3),
[(status.upper(), 8.5, True, th.accent, th.body_font, 0)])
text(s, MARGIN + Inches(5.55), y + Inches(0.11), Inches(5.85), Inches(0.5),
[(body, 10, False, th.muted, th.body_font, 0)], line_spacing=1.22)
y += Inches(0.76)
footer(s, th, n, "Feuille de route")
return s
def slide_rebuild(prs, th, n):
s = page_base(prs, th)
kicker_title(s, th, "Reconstruire à l'identique",
"L'ordre de grandeur, et ce qu'aucun budget ne rachète.")
left = [
("1 000 à 1 400", "jours-homme",
"Une équipe de trois — back-end, mobile, front — sur 18 à 24 mois."),
("30 à 70 M", "de jetons d'IA",
"Le code final pèse à lui seul ~2,2 M de jetons ; le travail réel coûte "
"quinze à trente fois cela en lectures et reprises."),
]
y = Inches(3.3)
for big, unit, body in left:
rect(s, MARGIN, y, Inches(5.75), Inches(1.45), fill=th.surface, line=th.rule)
rect(s, MARGIN, y, Pt(3), Inches(1.45), fill=th.accent)
text(s, MARGIN + Inches(0.32), y + Inches(0.16), Inches(5.1), Inches(0.75),
[(big, 22, True, th.ink, th.title_font, 0),
(unit, 12, False, th.muted, th.body_font, 2)])
text(s, MARGIN + Inches(0.32), y + Inches(0.86), Inches(5.1), Inches(0.5),
[(body, 10.5, False, th.muted, th.body_font, 0)], line_spacing=1.28)
y += Inches(1.65)
text(s, MARGIN, Inches(6.55), Inches(5.75), Inches(0.4),
[("Hypothèse assumée : reconstruction du périmètre fonctionnel existant, "
"hors reprise de données.", 9.5, False, th.muted, th.body_font, 0)],
line_spacing=1.25)
rx = Inches(7.35)
rect(s, rx, Inches(3.3), Inches(5.1), Inches(3.35), fill=th.ink)
text(s, rx + Inches(0.42), Inches(3.62), Inches(4.3), Inches(0.4),
[("Ce qui ne se reconstruit pas", 15, True, RGBColor(0xFF, 0xFF, 0xFF),
th.title_font, 0)])
items = [
"Six ans de cas limites appris en production, pas en réunion.",
"Trois sites qui tournent, dont un intégralement hors réseau.",
"Les décisions de quota, de purge et de consentement déjà tranchées.",
"Un CMS que des équipes non techniques utilisent réellement.",
]
cy = Inches(4.25)
for it in items:
rect(s, rx + Inches(0.42), cy + Inches(0.08), Inches(0.11), Inches(0.11),
fill=th.accent_bright, shape=MSO_SHAPE.OVAL)
text(s, rx + Inches(0.72), cy - Inches(0.02), Inches(4.0), Inches(0.6),
[(it, 11, False, RGBColor(0xD8, 0xDF, 0xE7), th.body_font, 0)],
line_spacing=1.3)
cy += Inches(0.58)
footer(s, th, n, "Reconstruire à l'identique")
return s
def slide_closing(prs, th):
s = page_base(prs, th, dark=True)
text(s, MARGIN, Inches(2.3), Inches(10), Inches(0.35),
[("CE QUE C'EST, AU FOND", 11, True, th.accent_bright, th.body_font, 0)])
text(s, MARGIN, Inches(2.9), Inches(10.4), Inches(1.6),
[("Une plateforme finie,\nen production, qui se vend\nà l'abonnement.", 34,
True, RGBColor(0xFF, 0xFF, 0xFF), th.title_font, 0)], line_spacing=1.15)
text(s, MARGIN, Inches(5.4), Inches(9.5), Inches(0.8),
[("Pas un prototype à finir. Pas un projet client à généraliser.", 15, False,
RGBColor(0xB8, 0xC2, 0xCE), th.body_font, 0)], line_spacing=1.35)
return s
def build(out_path):
th = Theme(branded=True)
prs = Presentation()
prs.slide_width, prs.slide_height = W, H
n = 1
slide_cover(prs, th); n += 1
slide_pitch(prs, th, n); n += 1
slide_bricks(prs, th, n); n += 1
slide_figures(prs, th, n); n += 1
slide_split(prs, th, n, "Le back-end", "manager-service — le cœur du produit.",
"ASP.NET Core, PostgreSQL, dockerisé. Tout le reste n'en est qu'un "
"client.",
["Multi-locataire : chaque client est une instance isolée, avec ses "
"quotas de stockage et d'IA.",
"145 points d'API sur 22 contrôleurs, client typé généré pour les "
"applications.",
"Diffusion temps réel vers les bornes : un contenu publié part vers "
"l'appareil sans intervention.",
"Abonnements, essais et facturation intégrés ; journal d'audit, "
"purges automatiques et quotas mesurés par client."],
"CAPTURE 01", "Documentation d'API", "Swagger du back-end"); n += 1
slide_split(prs, th, n, "Le CMS", "manager-app — là où le client travaille.",
"L'outil de production de contenu. C'est lui qui fait la différence "
"entre une application et un produit vendable.",
["79 écrans : contenus, médias, applications, appareils, "
"statistiques, utilisateurs, facturation.",
"Éditeur riche par champ, avec ses versions linguistiques.",
"Composition visuelle de l'application mobile, aperçu en direct.",
"Interface traduite en trois langues, utilisée par des équipes de "
"médiation et non par des développeurs."],
"CAPTURE 02", "Le CMS en usage", "Écran de configuration"); n += 1
slide_cards(prs, th, n, "Les applications visiteur",
"Trois surfaces, un seul contenu.", None,
[("Borne tactile", "Flutter Android, plein écran, sélection de langue, "
"veille et retour à l'accueil. Pilotée à distance, un contenu "
"différent par appareil."),
("Application mobile", "iOS et Android. Hors-ligne complet, balises "
"Bluetooth, notifications, audioguide, parcours, assistant."),
("Application web", "Next.js, sans installation. Même contenu, même "
"thème, accessible depuis un QR code.")]); n += 1
slide_split(prs, th, n, "Contenus",
"Quatorze formats, composables par le client.",
"Aucun développement par client : un nouveau musée se configure, il "
"ne se code pas.",
["Article, carte, diaporama, vidéo, audio, PDF, agenda, sommaire.",
"Quiz, jeu, parcours, événement, page web, météo.",
"Chaque format existe dans dix langues, avec traduction assistée par "
"IA champ par champ.",
"Thème, couleurs et disposition d'accueil réglés par instance."],
"CAPTURE 03", "Bibliothèque de formats"); n += 1
slide_split(prs, th, n, "Statistiques",
"Le tableau de bord que le client regarde chaque mois.",
"C'est l'argument de renouvellement d'abonnement : le client voit ce "
"que sa médiation produit.",
["Visites par jour, contenus les plus et les moins consultés, points "
"d'intérêt, quiz achevés, scans de QR.",
"Filtrable par période et par canal : borne, mobile, web.",
"Rapport PDF exportable pour un conseil d'administration ou une "
"tutelle.",
"Profondeur d'historique liée au plan d'abonnement — c'est un levier "
"commercial."],
"CAPTURE 04", "Tableau de bord"); n += 1
slide_split(prs, th, n, "Assistant IA",
"Un guide qui ne connaît que le contenu du client.",
"Recherche sémantique sur les contenus indexés de l'instance, puis "
"réponse construite à partir des seuls extraits trouvés.",
["Indexation automatique à chaque publication, par morceaux et par "
"langue.",
"Quand la recherche ne remonte rien, l'assistant le dit — il "
"n'invente pas.",
"Quota de jetons par client, mesuré et plafonné, avec garde-fou "
"pendant l'essai gratuit.",
"Disponible sur les trois applications visiteur."],
"CAPTURE 05", "Conversation avec l'assistant"); n += 1
slide_split(prs, th, n, "Pilotage de l'IA",
"Les questions sans réponse deviennent un produit.",
"Chaque échange est journalisé sans donnée nominative. Le CMS en tire "
"la liste des trous de contenu.",
["Onglet des questions auxquelles l'assistant n'a rien pu répondre.",
"Réponses de repli rédigées par le client, traduites automatiquement.",
"Activation canal par canal, consommation suivie en jetons.",
"Consentement du visiteur et purge automatique à 90 jours."],
"CAPTURE 06", "Questions des visiteurs"); n += 1
slide_split(prs, th, n, "Parcours et escape game",
"Le même moteur sert la visite guidée et le jeu de piste.",
"Un parcours est une suite d'étapes déclenchables, avec ou sans carte, "
"avec ou sans énigme.",
["Étapes ordonnées ou libres, révélées au fur et à mesure ou toutes "
"visibles.",
"Énigmes bloquantes, minuteur par étape, messages de début et de fin.",
"Déclenchement par zone géographique, par balise Bluetooth ou par "
"code QR.",
"Écran de réussite et progression sur la carte — vendu comme "
"animation scolaire ou familiale."],
"CAPTURE 07", "Parcours en cours"); n += 1
slide_split(prs, th, n, "Balises Bluetooth",
"Le contenu vient au visiteur.",
"Une balise posée dans une salle est associée à un contenu. "
"À l'approche, l'application propose ce contenu d'elle-même.",
["Suggestion automatique du contenu de la salle, sans manipulation.",
"Fonctionne sans réseau : le déclenchement est local à l'appareil.",
"Le même mécanisme sert le déclenchement d'étape de parcours.",
"Déjà déployé sur un site entièrement hors réseau ; extension vocale "
"écrite mais pas encore mise en service."],
"CAPTURE 08", "Contenu suggéré par balise", portrait=True); n += 1
slide_split(prs, th, n, "Hors-ligne",
"Une visite entière sans réseau.",
"Textes, images et audio sont téléchargés sur l'appareil. C'est ce qui "
"rend les sites souterrains et ruraux adressables.",
["Téléchargement d'un espace complet en une action, base locale sur "
"l'appareil.",
"L'application reste utilisable sans aucune connexion.",
"Les statistiques se synchronisent au retour du réseau.",
"Différenciant réel : peu de solutions du marché le font vraiment."],
"CAPTURE 09", "Téléchargement hors-ligne", portrait=True); n += 1
slide_cards(prs, th, n, "Le reste de la surface",
"Ce qui est déjà là et qu'on oublie de citer.", None,
[("Codes QR", "Génération des codes par contenu, export PDF prêt à "
"imprimer, scan côté visiteur, comptage des scans valides et "
"invalides."),
("Notifications", "Envoi immédiat ou programmé vers les applications "
"mobiles, rédaction par langue, historique et annulation."),
("Parc d'appareils", "État de connexion, batterie, version, dernière "
"activité, attribution du contenu affiché appareil par appareil.")],
label="Le reste de la surface"); n += 1
slide_split(prs, th, n, "Livrer un client",
"Marque blanche complète, sans fork de code.",
"Un nouveau client ne crée pas de branche. Il crée une instance.",
["Nom, icône, couleurs, écran de chargement et langues par instance.",
"Application native publiée sous le nom du client sur les magasins.",
"Application web sur une adresse dédiée.",
"Clés d'API par application, quatre niveaux de rôle, et aucune ligne "
"de code spécifique client dans le tronc commun."],
"CAPTURE 10", "Deux instances côte à côte"); n += 1
slide_cards(prs, th, n, "Références",
"Trois sites en production, le plus ancien depuis 2021.", None,
[("Musée de la Fraise, Wépion", "2021 — borne tactile en salle. Le "
"premier déploiement, toujours en service."),
("Fort de Saint-Héribert", "2023 — application mobile intégralement "
"hors-ligne avec balises Bluetooth."),
("Visit Namur", "2024 — borne tactile grand format en office de "
"tourisme.")]); n += 1
slide_pricing(prs, th, n); n += 1
slide_roadmap(prs, th, n); n += 1
slide_rebuild(prs, th, n); n += 1
slide_closing(prs, th)
cp = prs.core_properties
cp.author = "MyInfoMate"
cp.last_modified_by = "MyInfoMate"
cp.title = "MyInfoMate — la plateforme"
cp.subject = ""
cp.comments = ""
cp.keywords = ""
cp.category = ""
cp.content_status = ""
cp.identifier = ""
cp.language = "fr-BE"
cp.revision = 1
out_path.parent.mkdir(parents=True, exist_ok=True)
count = len(prs.slides._sldIdLst)
prs.save(str(out_path))
sanitize_package(out_path)
return out_path, count
if __name__ == "__main__":
path, count = build(ROOT / "MyInfoMate_plateforme.pptx")
print(f"{path} ({count} slides)")