- 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.
491 lines
24 KiB
Python
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)")
|