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.
6
.gitignore
vendored
@ -2,3 +2,9 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
*.tmp
|
*.tmp
|
||||||
*~
|
*~
|
||||||
|
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Clés d'API de l'instance de démo seedée en local — régénérées par les scripts de seed
|
||||||
|
outputs/mnaha/seed/state.json
|
||||||
|
|||||||
68
interview clients/Fourneau st Michel.txt
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
Fourneau st Michel
|
||||||
|
|
||||||
|
- Comment avez-vous eu les contacts ?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Expliquer votre projet ?
|
||||||
|
|
||||||
|
|
||||||
|
musée du fer, tout est là. EN nl
|
||||||
|
|
||||||
|
Musée de plein air : panneaux multiplie, traduction.. attraction et tourisme, anglais et allemand, FR et neerl
|
||||||
|
|
||||||
|
Projet européen mercice ?
|
||||||
|
|
||||||
|
son téléphone informative
|
||||||
|
|
||||||
|
carnet de visite? non pas papier
|
||||||
|
|
||||||
|
musée archeo arlon ; app mobile ; plus loin que les panneaux et traduction
|
||||||
|
|
||||||
|
|
||||||
|
télécharger maison avant etc, point wifi charger, 4g peu de réseau
|
||||||
|
|
||||||
|
|
||||||
|
autonomie plus besoin de réseau du tout
|
||||||
|
|
||||||
|
|
||||||
|
batiment, téléphone, 1 st marc, objet mis en évidence thématique exposée - objet emblématique - photos, detail, dessin schéma
|
||||||
|
|
||||||
|
PROVINCE LUX -> budget dispo, payé dès le départ, mise à jour
|
||||||
|
|
||||||
|
Site internet.
|
||||||
|
|
||||||
|
|
||||||
|
deeple -> IA
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Les langues envisagée ? Contenu existe déjà de votre côté ou non ?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- internet public qq part ou couverture réseau correcte à l'accueil ?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Sur ce type de projet, comment ça se décide chez vous ?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Est-ce que avez besoin d'un budget ou quelque chose ? Avez-vous déjà une enveloppe arbitrée, ou est-ce encore à construire ?
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- Echeance visée ?
|
||||||
|
|
||||||
|
Soit budget cette année. engagement -> Début 2027 avant février.
|
||||||
|
|
||||||
|
Ete 2027 ->
|
||||||
|
|
||||||
|
|
||||||
|
Descriptif termes technique -> Appel ? 2500 HTVA 5000 HTVA appel d'offre
|
||||||
|
|
||||||
|
Entrée musée plein air.
|
||||||
BIN
outputs/MyMNAHA_presentation_202608.pdf
Normal file
BIN
outputs/Wireframes/App web/App_mobile_web_0.png
Normal file
|
After Width: | Height: | Size: 663 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_1.png
Normal file
|
After Width: | Height: | Size: 441 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_10.png
Normal file
|
After Width: | Height: | Size: 403 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_11.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_12.png
Normal file
|
After Width: | Height: | Size: 512 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_13.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_14.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_2.png
Normal file
|
After Width: | Height: | Size: 399 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_3.png
Normal file
|
After Width: | Height: | Size: 362 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_4.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_5.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_6.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_7.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_8.png
Normal file
|
After Width: | Height: | Size: 412 KiB |
BIN
outputs/Wireframes/App web/App_mobile_web_9.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/0_ Main page.png
Normal file
|
After Width: | Height: | Size: 539 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/1_app.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/2_app.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/3_app.png
Normal file
|
After Width: | Height: | Size: 110 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/4_app.png
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/5_app.png
Normal file
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 403 KiB |
|
After Width: | Height: | Size: 231 KiB |
|
After Width: | Height: | Size: 278 KiB |
|
After Width: | Height: | Size: 399 KiB |
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 593 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 387 KiB |
|
After Width: | Height: | Size: 729 KiB |
|
After Width: | Height: | Size: 207 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 736 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 80 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_1.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_2.png
Normal file
|
After Width: | Height: | Size: 132 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_3.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_4.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_5.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_6.png
Normal file
|
After Width: | Height: | Size: 166 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_7.png
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
outputs/Wireframes/App web/Contextualisé/Screenshot_8.png
Normal file
|
After Width: | Height: | Size: 298 KiB |
BIN
outputs/Wireframes/Manager/0_Connexion.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
outputs/Wireframes/Manager/10_Notifications.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
outputs/Wireframes/Manager/11_Users.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
outputs/Wireframes/Manager/12_Guide_IA_0.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
outputs/Wireframes/Manager/13_Guide_IA_1.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
outputs/Wireframes/Manager/14_Guide_IA_Stats.png
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
outputs/Wireframes/Manager/15_Logs_Activity.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
BIN
outputs/Wireframes/Manager/16_Clé API.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
outputs/Wireframes/Manager/1_App_Mobile_Config.png
Normal file
|
After Width: | Height: | Size: 210 KiB |
BIN
outputs/Wireframes/Manager/2_Ecran_configuration.png
Normal file
|
After Width: | Height: | Size: 535 KiB |
BIN
outputs/Wireframes/Manager/3_Configuration_1.png
Normal file
|
After Width: | Height: | Size: 230 KiB |
BIN
outputs/Wireframes/Manager/4_Configuration_0.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
outputs/Wireframes/Manager/5_Configuration_2.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
outputs/Wireframes/Manager/6_Configuration_SectionMap.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
outputs/Wireframes/Manager/7_Escape_game.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
BIN
outputs/Wireframes/Manager/8_Ecran_Stats.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
outputs/Wireframes/Manager/9_Ressource.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
outputs/Wireframes/Manager/Contextualisé/0_Connexion.png
Normal file
|
After Width: | Height: | Size: 389 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/1_Config.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/2_Confog.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/3_Config.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/4_Ressources.png
Normal file
|
After Width: | Height: | Size: 640 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/5_Stats.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/6_Guide_IA.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/7_Guide_IA_Stats.png
Normal file
|
After Width: | Height: | Size: 125 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/8_Notifications.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
outputs/Wireframes/Manager/Contextualisé/9_Logs.png
Normal file
|
After Width: | Height: | Size: 161 KiB |
46
outputs/mnaha/PLAN-CAPTURES.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Plan des captures — deck MNAHA
|
||||||
|
|
||||||
|
Chaque emplacement du deck porte une référence `CAPTURE nn`. Ce tableau dit d'où vient
|
||||||
|
l'image. Une fois les captures faites, elles s'insèrent à la place des cadres pointillés
|
||||||
|
sans toucher à la mise en page (`build_decks.py`, fonction `placeholder`).
|
||||||
|
|
||||||
|
| Réf | Slide | Écran à capturer | Source | Statut |
|
||||||
|
|-----|-------|------------------|--------|--------|
|
||||||
|
| 01 | 5 — Le back-office | Éditeur d'une fiche d'œuvre, onglets de langue visibles | manager-app (Chrome) | à capturer |
|
||||||
|
| 02 | 7 — Les bornes | Accueil borne + sélection de la langue | tablet-app (émulateur Pixel Tablet) | à capturer |
|
||||||
|
| 03 | 8 — Application mobile | Accueil bento avec les trois sites | mymuseum-visitapp (Pixel 7) | à capturer |
|
||||||
|
| 04 | 9 — Identité visuelle | Écran de composition de l'accueil + aperçu téléphone | manager-app → Applications | à capturer |
|
||||||
|
| 05 | 10 — Langues | Champ multilingue + bouton « Traduire via IA » | manager-app | à capturer |
|
||||||
|
| 06 | 11 — Assistant | Conversation, deux langues côte à côte | mymuseum-visitapp / tablet-app | à capturer |
|
||||||
|
| 07 | 12 — Pilotage assistant | Onglet « questions des visiteurs » sans réponse | manager-app → Guide IA | à capturer |
|
||||||
|
| 08 | 13 — Codes QR | Export PDF des QR codes | manager-app → section Article | à capturer |
|
||||||
|
| 09 | 14 — Hors-ligne | Indicateur de contenu téléchargé sur l'accueil | mymuseum-visitapp | à capturer |
|
||||||
|
| 10 | 15 — Parcours | Parcours en cours : carte + étape à résoudre | mymuseum-visitapp → Parcours | à capturer |
|
||||||
|
| 11 | 16 — Notifications | Écran d'envoi / programmation | manager-app → Notifications | à capturer |
|
||||||
|
| 12 | 17 — Fréquentation | Tableau de bord statistiques | manager-app → Statistiques | à capturer |
|
||||||
|
| 13 | 20 — Références | Musée de la Fraise, Wépion | `tablet-app/Screenshot_2024*.png` | asset existant |
|
||||||
|
| 14 | 20 — Références | Fort de Saint-Héribert | `myinfomate-landing/public/app-screenshot-1.png` | asset existant |
|
||||||
|
| 15 | 20 — Références | Visit Namur | à retrouver | à fournir |
|
||||||
|
| 16 | 18 — Parc d'appareils | Liste des appareils : état, batterie, contenu | manager-app → Kiosk devices | à capturer |
|
||||||
|
| 17 | 19 — Gouvernance | Gestion des utilisateurs et des rôles | manager-app → Users | à capturer |
|
||||||
|
|
||||||
|
## Pour produire les captures
|
||||||
|
|
||||||
|
Il faut la stack locale : Docker Desktop → Postgres (`Deployment/Dockerfile.postgres`) →
|
||||||
|
`dotnet ef database update` → `manager-service` en local → instance de démo MNAHA créée
|
||||||
|
via l'API → `manager-app` dans Chrome et les apps Flutter sur émulateur.
|
||||||
|
|
||||||
|
Surcharger la configuration pour ne pointer que sur le Postgres local :
|
||||||
|
`appsettings.Development.json` contient encore la chaîne du **MongoDB de production**.
|
||||||
|
|
||||||
|
## Points à trancher avant la réunion
|
||||||
|
|
||||||
|
- **Luxembourgeois** : `LB` n'est pas dans la liste des langues gérées
|
||||||
|
(`manager-app/lib/constants.dart:165` et `mymuseum-visitapp/lib/constants.dart:59` —
|
||||||
|
FR, NL, EN, DE, IT, ES, PL, CN, AR, UK), et il manque le drapeau `lu.svg`. Le deck reste
|
||||||
|
volontairement neutre sur ce point. Après ajout de `LB`, passer
|
||||||
|
`LUXEMBOURGEOIS_ACTIF = True` en tête de `build_decks.py` et régénérer : la slide 10
|
||||||
|
annoncera le luxembourgeois.
|
||||||
|
- **Plan d'étage cliquable** : n'existe pas (les cartes sont géographiques uniquement).
|
||||||
|
Aucune slide ne le promet.
|
||||||
|
- **Réalité augmentée** : rien en production. Aucune slide ne l'évoque.
|
||||||
BIN
outputs/mnaha/branded/MNAHA_presentation_branded.pptx
Normal file
746
outputs/mnaha/build_decks.py
Normal file
@ -0,0 +1,746 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Génère les deux decks MNAHA (brandé + marque blanche) depuis un contenu unique.
|
||||||
|
|
||||||
|
Chaque slide visuelle porte un emplacement de capture nommé, à remplacer plus tard
|
||||||
|
par la capture réelle sans toucher à la structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
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 Emu, Inches, Pt
|
||||||
|
|
||||||
|
# Le luxembourgeois n'est pas dans la liste des langues gérées par le code
|
||||||
|
# (FR, NL, EN, DE, IT, ES, PL, CN, AR, UK). Passer à True une fois "LB" ajouté
|
||||||
|
# aux constantes de manager-app et des applications visiteur.
|
||||||
|
LUXEMBOURGEOIS_ACTIF = False
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
LOGO = ROOT.parent.parent / "myinfomate-landing" / "public" / "myinfomate-logo.png"
|
||||||
|
|
||||||
|
W = Inches(13.333)
|
||||||
|
H = Inches(7.5)
|
||||||
|
MARGIN = Inches(0.85)
|
||||||
|
|
||||||
|
|
||||||
|
class Theme:
|
||||||
|
def __init__(self, branded):
|
||||||
|
self.branded = branded
|
||||||
|
if branded:
|
||||||
|
self.ink = RGBColor(0x0A, 0x12, 0x22)
|
||||||
|
self.bg = RGBColor(0xF5, 0xF7, 0xFA)
|
||||||
|
self.surface = RGBColor(0xFF, 0xFF, 0xFF)
|
||||||
|
self.accent = RGBColor(0x0E, 0x8F, 0x8A)
|
||||||
|
self.accent_bright = RGBColor(0x0D, 0xF2, 0xDF)
|
||||||
|
self.muted = RGBColor(0x64, 0x74, 0x8B)
|
||||||
|
self.rule = RGBColor(0xD6, 0xDD, 0xE5)
|
||||||
|
self.title_font = "Segoe UI Semibold"
|
||||||
|
self.body_font = "Segoe UI"
|
||||||
|
else:
|
||||||
|
self.ink = RGBColor(0x1C, 0x1A, 0x17)
|
||||||
|
self.bg = RGBColor(0xFA, 0xF8, 0xF5)
|
||||||
|
self.surface = RGBColor(0xFF, 0xFF, 0xFF)
|
||||||
|
self.accent = RGBColor(0x8C, 0x6A, 0x3F)
|
||||||
|
self.accent_bright = RGBColor(0xB8, 0x93, 0x62)
|
||||||
|
self.muted = RGBColor(0x6B, 0x65, 0x58)
|
||||||
|
self.rule = RGBColor(0xE0, 0xDA, 0xD0)
|
||||||
|
self.title_font = "Georgia"
|
||||||
|
self.body_font = "Segoe UI"
|
||||||
|
|
||||||
|
|
||||||
|
def blank(prs):
|
||||||
|
return prs.slides.add_slide(prs.slide_layouts[6])
|
||||||
|
|
||||||
|
|
||||||
|
def rect(slide, x, y, w, h, fill=None, line=None, line_w=Pt(1), shape=MSO_SHAPE.RECTANGLE,
|
||||||
|
dash=False):
|
||||||
|
s = slide.shapes.add_shape(shape, x, y, w, h)
|
||||||
|
s.shadow.inherit = False
|
||||||
|
if fill is None:
|
||||||
|
s.fill.background()
|
||||||
|
else:
|
||||||
|
s.fill.solid()
|
||||||
|
s.fill.fore_color.rgb = fill
|
||||||
|
if line is None:
|
||||||
|
s.line.fill.background()
|
||||||
|
else:
|
||||||
|
s.line.color.rgb = line
|
||||||
|
s.line.width = line_w
|
||||||
|
if dash:
|
||||||
|
s.line.dash_style = 4 # MSO_LINE_DASH_STYLE.DASH
|
||||||
|
s.text_frame.clear()
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def text(slide, x, y, w, h, runs, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP,
|
||||||
|
line_spacing=1.15):
|
||||||
|
"""runs = [(texte, taille_pt, gras, couleur, police, espace_avant_pt), ...]"""
|
||||||
|
box = slide.shapes.add_textbox(x, y, w, h)
|
||||||
|
tf = box.text_frame
|
||||||
|
tf.word_wrap = True
|
||||||
|
tf.vertical_anchor = anchor
|
||||||
|
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = 0
|
||||||
|
for i, (content, size, bold, color, font, space_before) in enumerate(runs):
|
||||||
|
p = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
||||||
|
p.alignment = align
|
||||||
|
p.line_spacing = line_spacing
|
||||||
|
if space_before:
|
||||||
|
p.space_before = Pt(space_before)
|
||||||
|
r = p.add_run()
|
||||||
|
r.text = content
|
||||||
|
r.font.size = Pt(size)
|
||||||
|
r.font.bold = bold
|
||||||
|
r.font.color.rgb = color
|
||||||
|
r.font.name = font
|
||||||
|
return box
|
||||||
|
|
||||||
|
|
||||||
|
def page_base(prs, th, dark=False):
|
||||||
|
s = blank(prs)
|
||||||
|
rect(s, 0, 0, W, H, fill=th.ink if dark else th.bg)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def footer(slide, th, number, label=None):
|
||||||
|
color = th.muted
|
||||||
|
if label:
|
||||||
|
text(slide, MARGIN, H - Inches(0.62), Inches(8), Inches(0.3),
|
||||||
|
[(label, 9, False, color, th.body_font, 0)])
|
||||||
|
text(slide, W - MARGIN - Inches(1.2), H - Inches(0.62), Inches(1.2), Inches(0.3),
|
||||||
|
[(str(number), 9, False, color, th.body_font, 0)], align=PP_ALIGN.RIGHT)
|
||||||
|
|
||||||
|
|
||||||
|
"""Grille verticale fixe — le titre tient sur deux lignes au maximum, donc rien
|
||||||
|
ne se déplace d'une slide à l'autre et rien ne peut se chevaucher."""
|
||||||
|
Y_KICKER = Inches(0.95)
|
||||||
|
Y_TITLE = Inches(1.38)
|
||||||
|
Y_RULE = Inches(2.78)
|
||||||
|
Y_SUB = Inches(3.05)
|
||||||
|
Y_BODY = Inches(3.62)
|
||||||
|
Y_BODY_NO_SUB = Inches(3.10)
|
||||||
|
|
||||||
|
|
||||||
|
def kicker_title(slide, th, kicker, title, subtitle=None, width=None):
|
||||||
|
width = width or (W - 2 * MARGIN)
|
||||||
|
if kicker:
|
||||||
|
text(slide, MARGIN, Y_KICKER, width, Inches(0.3),
|
||||||
|
[(kicker.upper(), 10.5, True, th.accent, th.body_font, 0)])
|
||||||
|
text(slide, MARGIN, Y_TITLE, width, Inches(1.3),
|
||||||
|
[(title, 30, True, th.ink, th.title_font, 0)], line_spacing=1.06)
|
||||||
|
rect(slide, MARGIN, Y_RULE, Inches(0.9), Pt(2.5), fill=th.accent)
|
||||||
|
if subtitle:
|
||||||
|
text(slide, MARGIN, Y_SUB, min(width, Inches(9.2)), Inches(0.85),
|
||||||
|
[(subtitle, 13.5, False, th.muted, th.body_font, 0)], line_spacing=1.3)
|
||||||
|
return Y_BODY
|
||||||
|
return Y_BODY_NO_SUB
|
||||||
|
|
||||||
|
|
||||||
|
def placeholder(slide, th, x, y, w, h, ref, label, hint=None):
|
||||||
|
rect(slide, x, y, w, h, fill=th.surface, line=th.rule, line_w=Pt(1))
|
||||||
|
rect(slide, x + Inches(0.12), y + Inches(0.12), w - Inches(0.24), h - Inches(0.24),
|
||||||
|
fill=None, line=th.accent_bright, line_w=Pt(1), dash=True)
|
||||||
|
runs = [(ref, 11, True, th.accent, th.body_font, 0),
|
||||||
|
(label, 13, False, th.ink, th.body_font, 8)]
|
||||||
|
if hint:
|
||||||
|
runs.append((hint, 10, False, th.muted, th.body_font, 6))
|
||||||
|
text(slide, x + Inches(0.5), y, w - Inches(1.0), h, runs,
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.25)
|
||||||
|
|
||||||
|
|
||||||
|
def bullets(slide, th, x, y, w, items, size=13, gap=Inches(0.17)):
|
||||||
|
"""Hauteur estimée à partir d'une largeur de glyphe volontairement pessimiste :
|
||||||
|
mieux vaut un blanc en bas qu'un chevauchement."""
|
||||||
|
chars_per_line = max(20, int((w - Inches(0.34)) / (Inches(0.0098) * size)))
|
||||||
|
cur = y
|
||||||
|
for item in items:
|
||||||
|
rect(slide, x, cur + Inches(0.075), Inches(0.12), Inches(0.12),
|
||||||
|
fill=th.accent, shape=MSO_SHAPE.OVAL)
|
||||||
|
lines = -(-len(item) // chars_per_line)
|
||||||
|
h = Inches(0.245) * lines
|
||||||
|
text(slide, x + Inches(0.34), cur - Inches(0.03), w - Inches(0.34), h + Inches(0.1),
|
||||||
|
[(item, size, False, th.ink, th.body_font, 0)], line_spacing=1.3)
|
||||||
|
cur += h + gap
|
||||||
|
return cur
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- slides
|
||||||
|
|
||||||
|
def slide_cover(prs, th, n):
|
||||||
|
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.15), Inches(10.4), Inches(0.4),
|
||||||
|
[("MUSÉE NATIONAL D'ARCHÉOLOGIE, D'HISTOIRE ET D'ART — LUXEMBOURG", 11, True,
|
||||||
|
th.accent_bright, th.body_font, 0)])
|
||||||
|
text(s, MARGIN, Inches(2.75), Inches(10.6), Inches(1.8),
|
||||||
|
[("Piloter les contenus\nde vos trois sites", 44, True,
|
||||||
|
RGBColor(0xFF, 0xFF, 0xFF), th.title_font, 0)], line_spacing=1.05)
|
||||||
|
text(s, MARGIN, Inches(4.75), Inches(9.6), Inches(1.0),
|
||||||
|
[("Un système unique de gestion pour les bornes, l'application mobile "
|
||||||
|
"et le web — sans intervention technique.", 15, False,
|
||||||
|
RGBColor(0xB8, 0xC2, 0xCE), th.body_font, 0)], line_spacing=1.35)
|
||||||
|
if th.branded and LOGO.exists():
|
||||||
|
s.shapes.add_picture(str(LOGO), W - MARGIN - Inches(1.25), Inches(0.75),
|
||||||
|
height=Inches(1.05))
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_constat(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
y = kicker_title(s, th, "Le constat",
|
||||||
|
"Les dispositifs sont en place. Les contenus, eux, ne bougent plus.")
|
||||||
|
cards = [
|
||||||
|
("Chaque modification passe par un tiers",
|
||||||
|
"Corriger une date, remplacer une photo, ajouter une langue : autant de demandes "
|
||||||
|
"à formuler, à budgéter, à attendre."),
|
||||||
|
("Le délai décourage la mise à jour",
|
||||||
|
"Quand publier prend des semaines, on renonce. Le contenu vieillit sur des écrans "
|
||||||
|
"qui, eux, fonctionnent parfaitement."),
|
||||||
|
("Trois sites, trois chaînes séparées",
|
||||||
|
"Fëschmaart, Dräi Eechelen, Echternach : rien n'est mutualisé, et rien ne se "
|
||||||
|
"mesure d'un site à l'autre."),
|
||||||
|
]
|
||||||
|
x = MARGIN
|
||||||
|
cw = (W - 2 * MARGIN - Inches(0.6)) / 3
|
||||||
|
for title, body in cards:
|
||||||
|
rect(s, x, y, cw, Inches(2.9), fill=th.surface, line=th.rule)
|
||||||
|
rect(s, x, y, cw, Pt(3), fill=th.accent)
|
||||||
|
text(s, x + Inches(0.4), y + Inches(0.42), cw - Inches(0.8), Inches(0.8),
|
||||||
|
[(title, 15, True, th.ink, th.title_font, 0)], line_spacing=1.2)
|
||||||
|
text(s, x + Inches(0.4), y + Inches(1.35), cw - Inches(0.8), Inches(1.4),
|
||||||
|
[(body, 12, False, th.muted, th.body_font, 0)], line_spacing=1.35)
|
||||||
|
x += cw + Inches(0.3)
|
||||||
|
footer(s, th, n, "Le constat")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_proposition(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
y = kicker_title(s, th, "La proposition",
|
||||||
|
"Un seul back-office alimente tous vos dispositifs.",
|
||||||
|
"Vos équipes saisissent le contenu une fois. Il apparaît là où vous "
|
||||||
|
"décidez qu'il apparaisse — sur les bornes des salles, dans "
|
||||||
|
"l'application des visiteurs, sur le web.")
|
||||||
|
x = MARGIN
|
||||||
|
cw = (W - 2 * MARGIN - Inches(0.6)) / 3
|
||||||
|
for head, sub in [
|
||||||
|
("Les bornes en salle", "Écrans et bornes déjà installés au Fëschmaart et à "
|
||||||
|
"Dräi Eechelen, pilotés à distance."),
|
||||||
|
("L'application mobile", "Les trois sites réunis dans une seule application, "
|
||||||
|
"sur le téléphone du visiteur."),
|
||||||
|
("Le web", "Le même contenu accessible depuis un navigateur, sans installation."),
|
||||||
|
]:
|
||||||
|
rect(s, x, y, cw, Inches(2.15), fill=th.surface, line=th.rule)
|
||||||
|
text(s, x + Inches(0.4), y + Inches(0.38), cw - Inches(0.8), Inches(0.4),
|
||||||
|
[(head, 16, True, th.ink, th.title_font, 0)])
|
||||||
|
text(s, x + Inches(0.4), y + Inches(0.92), cw - Inches(0.8), Inches(1.0),
|
||||||
|
[(sub, 12, False, th.muted, th.body_font, 0)], line_spacing=1.35)
|
||||||
|
x += cw + Inches(0.3)
|
||||||
|
footer(s, th, n, "La proposition")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_architecture(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
kicker_title(s, th, "Comment cela fonctionne",
|
||||||
|
"Un contenu saisi une fois, diffusé partout, mesuré en retour.")
|
||||||
|
cy = Inches(3.9)
|
||||||
|
bw, bh = Inches(2.55), Inches(1.35)
|
||||||
|
left_x = MARGIN + Inches(0.15)
|
||||||
|
rect(s, left_x, cy - bh / 2, bw, bh, fill=th.ink)
|
||||||
|
text(s, left_x + Inches(0.25), cy - bh / 2, bw - Inches(0.5), bh,
|
||||||
|
[("Le back-office", 15, True, RGBColor(0xFF, 0xFF, 0xFF), th.title_font, 0),
|
||||||
|
("Vos équipes\nde médiation", 11, False, RGBColor(0xC5, 0xCE, 0xD8),
|
||||||
|
th.body_font, 6)],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.2)
|
||||||
|
|
||||||
|
hub_x = left_x + bw + Inches(1.15)
|
||||||
|
hub_w = Inches(2.35)
|
||||||
|
rect(s, hub_x, cy - bh / 2, hub_w, bh, fill=th.surface, line=th.accent, line_w=Pt(1.5))
|
||||||
|
text(s, hub_x + Inches(0.2), cy - bh / 2, hub_w - Inches(0.4), bh,
|
||||||
|
[("Contenus, médias\net langues", 13.5, True, th.ink, th.title_font, 0),
|
||||||
|
("Une source unique", 11, False, th.muted, th.body_font, 6)],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.2)
|
||||||
|
|
||||||
|
rect(s, left_x + bw, cy - Pt(1), Inches(1.15), Pt(2), fill=th.accent)
|
||||||
|
|
||||||
|
out_x = hub_x + hub_w + Inches(1.15)
|
||||||
|
rect(s, hub_x + hub_w, cy - Pt(1), Inches(1.15), Pt(2), fill=th.accent)
|
||||||
|
ow = Inches(2.75)
|
||||||
|
oh = Inches(0.92)
|
||||||
|
tops = [cy - Inches(1.55), cy - Inches(0.46), cy + Inches(0.63)]
|
||||||
|
for top, (name, sub) in zip(tops, [
|
||||||
|
("Bornes en salle", "Fëschmaart · Dräi Eechelen"),
|
||||||
|
("Application mobile", "Les trois sites"),
|
||||||
|
("Web", "Sans installation"),
|
||||||
|
]):
|
||||||
|
rect(s, out_x, top, ow, oh, fill=th.surface, line=th.rule)
|
||||||
|
text(s, out_x + Inches(0.22), top, ow - Inches(0.44), oh,
|
||||||
|
[(name, 13, True, th.ink, th.title_font, 0),
|
||||||
|
(sub, 10, False, th.muted, th.body_font, 4)],
|
||||||
|
align=PP_ALIGN.CENTER, anchor=MSO_ANCHOR.MIDDLE, line_spacing=1.15)
|
||||||
|
|
||||||
|
ret_y = cy + Inches(2.05)
|
||||||
|
rect(s, left_x, ret_y, out_x + ow - left_x, Pt(2), fill=th.rule)
|
||||||
|
rect(s, left_x, cy + bh / 2, Pt(2), ret_y - cy - bh / 2, fill=th.rule)
|
||||||
|
rect(s, out_x + ow - Pt(2), tops[2] + oh, Pt(2), ret_y - tops[2] - oh, fill=th.rule)
|
||||||
|
arrow = s.shapes.add_shape(MSO_SHAPE.ISOSCELES_TRIANGLE, left_x - Inches(0.02),
|
||||||
|
ret_y - Inches(0.09), Inches(0.2), Inches(0.2))
|
||||||
|
arrow.rotation = 270
|
||||||
|
arrow.shadow.inherit = False
|
||||||
|
arrow.fill.solid(); arrow.fill.fore_color.rgb = th.rule
|
||||||
|
arrow.line.fill.background()
|
||||||
|
chip_w = Inches(5.0)
|
||||||
|
chip_x = left_x + ((out_x + ow - left_x) - chip_w) / 2
|
||||||
|
rect(s, chip_x, ret_y - Inches(0.16), chip_w, Inches(0.34), fill=th.bg)
|
||||||
|
text(s, chip_x, ret_y - Inches(0.11), chip_w, Inches(0.3),
|
||||||
|
[("Statistiques de fréquentation et questions des visiteurs", 11.5, False,
|
||||||
|
th.muted, th.body_font, 0)], align=PP_ALIGN.CENTER)
|
||||||
|
footer(s, th, n, "Architecture fonctionnelle")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_split(prs, th, n, kicker, title, subtitle, items, ref, cap_label, cap_hint=None,
|
||||||
|
footer_label=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.98), Inches(5.75), items)
|
||||||
|
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, footer_label or kicker)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_grid(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
kicker_title(s, th, "Types de contenus",
|
||||||
|
"Quatorze formats, combinables librement dans chaque espace.",
|
||||||
|
"Un espace se compose comme un sommaire : on y ajoute les formats voulus, "
|
||||||
|
"on les ordonne, on les traduit.")
|
||||||
|
tiles = [
|
||||||
|
("Article", "Texte enrichi, images, audio"),
|
||||||
|
("Carte", "Points d'intérêt, zones, catégories"),
|
||||||
|
("Diaporama", "Séries d'images légendées"),
|
||||||
|
("Vidéo", "Fichier ou lien en ligne"),
|
||||||
|
("Document PDF", "Consultable dans l'app"),
|
||||||
|
("Agenda", "Événements datés, filtrables"),
|
||||||
|
("Sommaire", "Menu thématique de sous-espaces"),
|
||||||
|
("Quiz", "Questions, réponses, score"),
|
||||||
|
("Jeu", "Puzzle, taquin"),
|
||||||
|
("Parcours", "Étapes guidées, escape game"),
|
||||||
|
("Événement", "Programme et plan dédiés"),
|
||||||
|
("Page web", "Site externe intégré"),
|
||||||
|
("Météo", "Conditions du jour"),
|
||||||
|
("Compte à rebours", "Mise en avant d'un événement"),
|
||||||
|
]
|
||||||
|
cols, rows = 5, 3
|
||||||
|
gx, gy = Inches(0.2), Inches(0.16)
|
||||||
|
tw = (W - 2 * MARGIN - gx * (cols - 1)) / cols
|
||||||
|
thh = Inches(0.9)
|
||||||
|
x0, y0 = MARGIN, Inches(3.68)
|
||||||
|
for i, (name, sub) in enumerate(tiles):
|
||||||
|
c, r = i % cols, i // cols
|
||||||
|
if r >= rows:
|
||||||
|
break
|
||||||
|
x = x0 + c * (tw + gx)
|
||||||
|
y = y0 + r * (thh + gy)
|
||||||
|
rect(s, x, y, tw, thh, fill=th.surface, line=th.rule)
|
||||||
|
rect(s, x, y, Pt(3), thh, fill=th.accent)
|
||||||
|
text(s, x + Inches(0.22), y + Inches(0.14), tw - Inches(0.35), Inches(0.3),
|
||||||
|
[(name, 12.5, True, th.ink, th.title_font, 0)])
|
||||||
|
text(s, x + Inches(0.22), y + Inches(0.47), tw - Inches(0.35), Inches(0.38),
|
||||||
|
[(sub, 9.5, False, th.muted, th.body_font, 0)], line_spacing=1.15)
|
||||||
|
footer(s, th, n, "Types de contenus")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_stats(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
y = kicker_title(s, th, "Fréquentation",
|
||||||
|
"Ce que les visiteurs consultent vraiment.",
|
||||||
|
width=Inches(6.05))
|
||||||
|
kpis = [("Visites", "par jour et par période"),
|
||||||
|
("Contenus", "les plus et les moins consultés"),
|
||||||
|
("Points d'intérêt", "salles et œuvres les plus visitées"),
|
||||||
|
("Quiz et parcours", "taux d'achèvement, scores")]
|
||||||
|
x = MARGIN
|
||||||
|
kw = (Inches(6.0) - Inches(0.25)) / 2
|
||||||
|
for i, (t, sub) in enumerate(kpis):
|
||||||
|
cx = x + (i % 2) * (kw + Inches(0.25))
|
||||||
|
cy = y + (i // 2) * Inches(1.15)
|
||||||
|
rect(s, cx, cy, kw, Inches(0.95), fill=th.surface, line=th.rule)
|
||||||
|
text(s, cx + Inches(0.28), cy + Inches(0.16), kw - Inches(0.5), Inches(0.3),
|
||||||
|
[(t, 13, True, th.ink, th.title_font, 0)])
|
||||||
|
text(s, cx + Inches(0.28), cy + Inches(0.5), kw - Inches(0.5), Inches(0.3),
|
||||||
|
[(sub, 10, False, th.muted, th.body_font, 0)])
|
||||||
|
text(s, MARGIN, y + Inches(2.45), Inches(6.0), Inches(0.9),
|
||||||
|
[("Filtrable par période et par dispositif — borne, mobile, web. "
|
||||||
|
"Exportable en rapport PDF pour un comité ou une tutelle.",
|
||||||
|
12, False, th.muted, th.body_font, 0)], line_spacing=1.35)
|
||||||
|
placeholder(s, th, Inches(7.35), Inches(1.75), Inches(5.1), Inches(3.9),
|
||||||
|
"CAPTURE 12", "Tableau de bord des statistiques",
|
||||||
|
"KPI, courbe des visites par jour, contenus les plus consultés")
|
||||||
|
footer(s, th, n, "Fréquentation")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_references(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
if th.branded:
|
||||||
|
kicker_title(s, th, "Références",
|
||||||
|
"Trois déploiements en production, depuis 2021.")
|
||||||
|
cases = [
|
||||||
|
("Musée de la Fraise, Wépion", "2021 — borne tactile en salle"),
|
||||||
|
("Fort de Saint-Héribert", "2023 — application mobile intégralement "
|
||||||
|
"hors-ligne, balises BLE"),
|
||||||
|
("Visit Namur", "2024 — borne tactile grand format en office de tourisme"),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
kicker_title(s, th, "Références",
|
||||||
|
"La solution est déployée sur trois sites en Belgique, depuis 2021.")
|
||||||
|
cases = [
|
||||||
|
("Musée thématique", "2021 — borne tactile en salle"),
|
||||||
|
("Site fortifié", "2023 — application mobile intégralement hors-ligne, "
|
||||||
|
"balises BLE"),
|
||||||
|
("Office de tourisme", "2024 — borne tactile grand format"),
|
||||||
|
]
|
||||||
|
x = MARGIN
|
||||||
|
cw = (W - 2 * MARGIN - Inches(0.6)) / 3
|
||||||
|
for i, (name, sub) in enumerate(cases):
|
||||||
|
rect(s, x, Inches(3.10), cw, Inches(3.2), fill=th.surface, line=th.rule)
|
||||||
|
placeholder(s, th, x + Inches(0.25), Inches(3.35), cw - Inches(0.5), Inches(1.75),
|
||||||
|
f"CAPTURE {13 + i}",
|
||||||
|
name if th.branded else f"Déploiement {i + 1}")
|
||||||
|
text(s, x + Inches(0.3), Inches(5.35), cw - Inches(0.6), Inches(0.35),
|
||||||
|
[(name, 13.5, True, th.ink, th.title_font, 0)])
|
||||||
|
text(s, x + Inches(0.3), Inches(5.74), cw - Inches(0.6), Inches(0.45),
|
||||||
|
[(sub, 10.5, False, th.muted, th.body_font, 0)], line_spacing=1.2)
|
||||||
|
x += cw + Inches(0.3)
|
||||||
|
footer(s, th, n, "Références")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_phases(prs, th, n):
|
||||||
|
s = page_base(prs, th)
|
||||||
|
kicker_title(s, th, "Mise en œuvre",
|
||||||
|
"Par étapes, en commençant par un périmètre maîtrisable.")
|
||||||
|
phases = [
|
||||||
|
("Phase 1", "Pilote au Fëschmaart",
|
||||||
|
"Reprise des contenus d'une aile, mise en service sur les bornes existantes, "
|
||||||
|
"formation de l'équipe de médiation."),
|
||||||
|
("Phase 2", "Application mobile des trois sites",
|
||||||
|
"Les trois sites réunis dans une application unique : contenus, cartes, "
|
||||||
|
"audioguide, assistant."),
|
||||||
|
("Phase 3", "Echternach et parcours hors-ligne",
|
||||||
|
"Parcours extérieur, balises, téléchargement complet pour les zones sans réseau."),
|
||||||
|
]
|
||||||
|
x = MARGIN
|
||||||
|
cw = (W - 2 * MARGIN - Inches(0.7)) / 3
|
||||||
|
for i, (num, title, body) in enumerate(phases):
|
||||||
|
rect(s, x, Inches(3.10), cw, Inches(3.0), fill=th.surface, line=th.rule)
|
||||||
|
rect(s, x, Inches(3.10), cw, Pt(3), fill=th.accent)
|
||||||
|
text(s, x + Inches(0.35), Inches(3.45), cw - Inches(0.7), Inches(0.3),
|
||||||
|
[(num.upper(), 10.5, True, th.accent, th.body_font, 0)])
|
||||||
|
text(s, x + Inches(0.35), Inches(3.85), cw - Inches(0.7), Inches(0.8),
|
||||||
|
[(title, 15, True, th.ink, th.title_font, 0)], line_spacing=1.15)
|
||||||
|
text(s, x + Inches(0.35), Inches(4.80), cw - Inches(0.7), Inches(1.1),
|
||||||
|
[(body, 11.5, False, th.muted, th.body_font, 0)], line_spacing=1.35)
|
||||||
|
x += cw + Inches(0.35)
|
||||||
|
footer(s, th, n, "Mise en œuvre")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def slide_next(prs, th, n):
|
||||||
|
s = page_base(prs, th, dark=True)
|
||||||
|
text(s, MARGIN, Inches(1.35), Inches(10), Inches(0.35),
|
||||||
|
[("PROCHAINES ÉTAPES", 11, True, th.accent_bright, th.body_font, 0)])
|
||||||
|
text(s, MARGIN, Inches(1.95), Inches(9.5), Inches(1.0),
|
||||||
|
[("Ce que nous proposons\npour avancer", 34, True, RGBColor(0xFF, 0xFF, 0xFF),
|
||||||
|
th.title_font, 0)], line_spacing=1.08)
|
||||||
|
steps = [
|
||||||
|
"Une démonstration du back-office sur vos propres contenus — une salle du "
|
||||||
|
"Fëschmaart suffit.",
|
||||||
|
"Un atelier avec l'équipe de médiation pour cadrer les formats et les langues.",
|
||||||
|
"Un périmètre de pilote chiffré et daté, sans engagement sur la suite.",
|
||||||
|
]
|
||||||
|
y = Inches(4.0)
|
||||||
|
for i, st in enumerate(steps, 1):
|
||||||
|
rect(s, MARGIN, y, Inches(0.42), Inches(0.42), fill=th.accent,
|
||||||
|
shape=MSO_SHAPE.OVAL)
|
||||||
|
text(s, MARGIN, y + Inches(0.06), Inches(0.42), Inches(0.3),
|
||||||
|
[(str(i), 12, True, RGBColor(0xFF, 0xFF, 0xFF), th.body_font, 0)],
|
||||||
|
align=PP_ALIGN.CENTER)
|
||||||
|
text(s, MARGIN + Inches(0.72), y + Inches(0.02), Inches(9.6), Inches(0.6),
|
||||||
|
[(st, 14, False, RGBColor(0xE2, 0xE8, 0xF0), th.body_font, 0)],
|
||||||
|
line_spacing=1.3)
|
||||||
|
y += Inches(0.92)
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def build(branded, out_path):
|
||||||
|
th = Theme(branded)
|
||||||
|
prs = Presentation()
|
||||||
|
prs.slide_width = W
|
||||||
|
prs.slide_height = H
|
||||||
|
|
||||||
|
n = 1
|
||||||
|
slide_cover(prs, th, n); n += 1
|
||||||
|
slide_constat(prs, th, n); n += 1
|
||||||
|
slide_proposition(prs, th, n); n += 1
|
||||||
|
slide_architecture(prs, th, n); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Le back-office",
|
||||||
|
"Modifier un contenu prend une minute.",
|
||||||
|
"L'éditeur ressemble à un traitement de texte. Aucune compétence "
|
||||||
|
"technique, aucun prestataire à solliciter.",
|
||||||
|
["Texte enrichi, images, audio, vidéo et documents dans un même écran.",
|
||||||
|
"Publication immédiate : les bornes connectées se mettent à jour sans "
|
||||||
|
"manipulation sur place.",
|
||||||
|
"Chaque champ existe dans toutes les langues déclarées.",
|
||||||
|
"Journal des modifications : qui a changé quoi, et quand."],
|
||||||
|
"CAPTURE 01", "Éditeur de contenu",
|
||||||
|
"Fiche d'une œuvre avec ses onglets de langue", "Le back-office"); n += 1
|
||||||
|
|
||||||
|
slide_grid(prs, th, n); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Les bornes",
|
||||||
|
"Vos bornes existantes, alimentées à distance.",
|
||||||
|
"Le matériel déjà installé au Fëschmaart et à Dräi Eechelen reste en "
|
||||||
|
"place. Seul le contenu change de main.",
|
||||||
|
["Choix de la langue dès le premier écran.",
|
||||||
|
"Navigation par thème : archéologie, cabinet des médailles, arts "
|
||||||
|
"décoratifs, beaux-arts.",
|
||||||
|
"Fiches d'œuvre avec image, texte et écoute audio.",
|
||||||
|
"Chaque borne peut afficher un contenu différent."],
|
||||||
|
"CAPTURE 02", "Accueil de la borne",
|
||||||
|
"Sélection de la langue, Fëschmaart", "Les bornes"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Application mobile",
|
||||||
|
"Les trois sites dans une seule application.",
|
||||||
|
"Le visiteur télécharge une application. Il y trouve le Fëschmaart, "
|
||||||
|
"Dräi Eechelen et la Réimervilla, et choisit où il se trouve.",
|
||||||
|
["Un espace par site, avec ses propres contenus et ses propres couleurs.",
|
||||||
|
"Cartes, audioguide, quiz, parcours : chaque site compose le sien.",
|
||||||
|
"Le visiteur passe d'un site à l'autre sans changer d'application.",
|
||||||
|
"Une même fréquentation mesurée sur les trois sites."],
|
||||||
|
"CAPTURE 03", "Accueil de l'application",
|
||||||
|
"Les trois sites en tuiles", "Application mobile", portrait=True); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Identité visuelle",
|
||||||
|
"L'apparence de l'application se règle ici.",
|
||||||
|
"Couleurs, images, disposition : le musée compose sa page d'accueil "
|
||||||
|
"et voit le résultat en direct, sans nouvelle version de l'application.",
|
||||||
|
["Couleurs principales et secondaires, image d'accueil, écran de "
|
||||||
|
"chargement.",
|
||||||
|
"Tuiles redimensionnables : un site ou un événement peut occuper une "
|
||||||
|
"place plus large.",
|
||||||
|
"Aperçu en temps réel dans une maquette de téléphone.",
|
||||||
|
"L'application porte le nom et l'identité du musée, pas ceux d'un "
|
||||||
|
"éditeur."],
|
||||||
|
"CAPTURE 04", "Composition de l'accueil",
|
||||||
|
"Tuiles, couleurs et aperçu téléphone", "Identité visuelle"); n += 1
|
||||||
|
|
||||||
|
langue_titre = ("Luxembourgeois, français, allemand, anglais."
|
||||||
|
if LUXEMBOURGEOIS_ACTIF
|
||||||
|
else "Chaque contenu existe dans toutes vos langues.")
|
||||||
|
langue_puce = ("Onze langues gérées, dont le luxembourgeois, activables site par site."
|
||||||
|
if LUXEMBOURGEOIS_ACTIF
|
||||||
|
else "Dix langues gérées, activables site par site et dispositif par "
|
||||||
|
"dispositif.")
|
||||||
|
slide_split(prs, th, n, "Langues", langue_titre,
|
||||||
|
"Le gestionnaire saisit son texte une fois. Le visiteur choisit sa "
|
||||||
|
"langue au premier écran.",
|
||||||
|
[langue_puce,
|
||||||
|
"Traduction assistée : un bouton propose la version des autres langues "
|
||||||
|
"à partir du texte source.",
|
||||||
|
"Le gestionnaire relit et corrige avant publication — rien n'est publié "
|
||||||
|
"automatiquement.",
|
||||||
|
"L'assistant de visite répond dans la langue de la question."],
|
||||||
|
"CAPTURE 05", "Champ multilingue",
|
||||||
|
"Onglets de langue et traduction assistée", "Langues"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Assistant de visite",
|
||||||
|
"Un guide qui ne connaît que vos collections.",
|
||||||
|
"Le visiteur pose sa question en langage courant. La réponse est "
|
||||||
|
"construite à partir de vos contenus, et uniquement des vôtres.",
|
||||||
|
["Les réponses citent les contenus du musée — pas de source extérieure.",
|
||||||
|
"Quand l'information n'existe pas dans vos contenus, l'assistant le dit "
|
||||||
|
"plutôt que d'inventer.",
|
||||||
|
"Disponible sur la borne, dans l'application et sur le web.",
|
||||||
|
"Répond dans la langue du visiteur."],
|
||||||
|
"CAPTURE 06", "Conversation avec l'assistant",
|
||||||
|
"Deux langues différentes, côte à côte", "Assistant de visite"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Pilotage de l'assistant",
|
||||||
|
"Les questions sans réponse guident la médiation.",
|
||||||
|
"Chaque échange est journalisé sans donnée nominative. Les questions "
|
||||||
|
"restées sans réponse deviennent une liste de travail pour la médiation.",
|
||||||
|
["Liste des questions auxquelles l'assistant n'a rien pu répondre.",
|
||||||
|
"Réponses de repli rédigées par le musée pour les sujets sensibles.",
|
||||||
|
"Activation dispositif par dispositif, avec consommation suivie.",
|
||||||
|
"Consentement du visiteur et purge automatique des questions."],
|
||||||
|
"CAPTURE 07", "Écran de pilotage de l'assistant",
|
||||||
|
"Onglet des questions sans réponse", "Pilotage de l'assistant"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Codes QR",
|
||||||
|
"Du cartel au contenu, sans application préalable.",
|
||||||
|
"Le back-office produit les codes QR de vos salles et de vos œuvres, "
|
||||||
|
"dans un document prêt à imprimer.",
|
||||||
|
["Un code par contenu, généré et exporté en PDF depuis le back-office.",
|
||||||
|
"Le scan ouvre directement la fiche, dans la langue du visiteur.",
|
||||||
|
"Les scans sont comptés : vous savez quels cartels fonctionnent.",
|
||||||
|
"Un code reste valable même si le contenu derrière change."],
|
||||||
|
"CAPTURE 08", "Codes QR générés",
|
||||||
|
"Export PDF prêt à imprimer", "Codes QR"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Hors-ligne et balises",
|
||||||
|
"Fonctionne dans la roche comme en plein champ.",
|
||||||
|
"Le visiteur télécharge un site entier avant sa visite. Textes, images "
|
||||||
|
"et audio restent disponibles sans aucun réseau.",
|
||||||
|
["Téléchargement complet d'un site ou d'un parcours, en une action.",
|
||||||
|
"Salles creusées dans la roche : aucune connexion nécessaire.",
|
||||||
|
"Balises Bluetooth : le contenu de la salle s'affiche à l'approche.",
|
||||||
|
"Réimervilla : déclenchement par zone géographique en extérieur."],
|
||||||
|
"CAPTURE 09", "Téléchargement hors-ligne",
|
||||||
|
"Indicateur de contenu téléchargé", "Hors-ligne et balises",
|
||||||
|
portrait=True); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Parcours et jeux",
|
||||||
|
"Du parcours thématique à la chasse au trésor.",
|
||||||
|
"Un même outil sert la visite guidée classique et le jeu de piste "
|
||||||
|
"familial ou scolaire.",
|
||||||
|
["Étapes ordonnées ou libres, visibles ou révélées au fur et à mesure.",
|
||||||
|
"Énigmes à résoudre pour débloquer l'étape suivante, minuteur optionnel.",
|
||||||
|
"Messages de début et de fin, écran de réussite, progression suivie "
|
||||||
|
"sur la carte.",
|
||||||
|
"Déclenchement d'une étape par balise, par zone géographique ou par "
|
||||||
|
"code QR."],
|
||||||
|
"CAPTURE 10", "Parcours en cours",
|
||||||
|
"Progression sur la carte et étape à résoudre", "Parcours et jeux"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Notifications",
|
||||||
|
"Prévenir vos visiteurs en une minute.",
|
||||||
|
"Les personnes ayant installé l'application reçoivent une notification, "
|
||||||
|
"envoyée depuis le back-office.",
|
||||||
|
["Envoi immédiat ou programmé à une date et une heure.",
|
||||||
|
"Rédaction dans chaque langue déclarée.",
|
||||||
|
"Historique des envois et annulation d'un envoi programmé.",
|
||||||
|
"Aucune donnée nominative n'est collectée."],
|
||||||
|
"CAPTURE 11", "Écran d'envoi de notification",
|
||||||
|
None, "Notifications"); n += 1
|
||||||
|
|
||||||
|
slide_stats(prs, th, n); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Parc d'appareils",
|
||||||
|
"Savoir ce qu'affiche chaque borne.",
|
||||||
|
"Chaque appareil déclaré remonte son état. Vous changez le contenu "
|
||||||
|
"affiché depuis le back-office.",
|
||||||
|
["État de connexion, niveau de batterie, dernière activité, version "
|
||||||
|
"installée.",
|
||||||
|
"Attribution du contenu affiché appareil par appareil.",
|
||||||
|
"Un changement de contenu part vers l'appareil sans intervention "
|
||||||
|
"sur place.",
|
||||||
|
"Utile quand les bornes sont réparties sur trois sites distants."],
|
||||||
|
"CAPTURE 16", "Liste des appareils",
|
||||||
|
"État, batterie et contenu affiché", "Parc d'appareils"); n += 1
|
||||||
|
|
||||||
|
slide_split(prs, th, n, "Gouvernance",
|
||||||
|
"Chacun accède à ce qui le concerne.",
|
||||||
|
"Les droits se règlent par personne. Les modifications sont tracées.",
|
||||||
|
["Quatre niveaux : administration, gestion du site, rédaction, "
|
||||||
|
"consultation.",
|
||||||
|
"Journal d'audit des créations et modifications.",
|
||||||
|
"Hébergement européen, questions des visiteurs purgées "
|
||||||
|
"automatiquement.",
|
||||||
|
"Aucun compte visiteur, aucune donnée nominative collectée."],
|
||||||
|
"CAPTURE 17", "Gestion des utilisateurs",
|
||||||
|
None, "Gouvernance"); n += 1
|
||||||
|
|
||||||
|
slide_references(prs, th, n); n += 1
|
||||||
|
slide_phases(prs, th, n); n += 1
|
||||||
|
slide_next(prs, th, n)
|
||||||
|
|
||||||
|
scrub(prs, branded)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
APP_XML = (
|
||||||
|
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>'
|
||||||
|
'<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/'
|
||||||
|
'extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/'
|
||||||
|
'2006/docPropsVTypes"><Application></Application><PresentationFormat>'
|
||||||
|
'Widescreen</PresentationFormat><ScaleCrop>false</ScaleCrop>'
|
||||||
|
'<LinksUpToDate>false</LinksUpToDate><SharedDoc>false</SharedDoc>'
|
||||||
|
'<HyperlinksChanged>false</HyperlinksChanged></Properties>'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_package(path):
|
||||||
|
"""Le gabarit par défaut de python-pptx laisse traîner un producteur, un format
|
||||||
|
4:3 erroné, des dates de 2013 et une vignette d'aperçu. On réécrit l'archive."""
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
src = zipfile.ZipFile(path)
|
||||||
|
items = [(i, src.read(i.filename)) for i in src.infolist()]
|
||||||
|
src.close()
|
||||||
|
|
||||||
|
stamp = "2026-08-24T09:00:00Z"
|
||||||
|
out = zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED)
|
||||||
|
for info, data in items:
|
||||||
|
name = info.filename
|
||||||
|
if name == "docProps/thumbnail.jpeg":
|
||||||
|
continue
|
||||||
|
if name == "docProps/app.xml":
|
||||||
|
data = APP_XML.encode("utf-8")
|
||||||
|
elif name == "docProps/core.xml":
|
||||||
|
txt = data.decode("utf-8")
|
||||||
|
txt = re.sub(r"(<dcterms:(?:created|modified)[^>]*>)[^<]*(</)",
|
||||||
|
lambda m: m.group(1) + stamp + m.group(2), txt)
|
||||||
|
data = txt.encode("utf-8")
|
||||||
|
elif name == "_rels/.rels":
|
||||||
|
txt = data.decode("utf-8")
|
||||||
|
txt = re.sub(r"<Relationship[^>]*thumbnail\.jpeg[^>]*/>", "", txt)
|
||||||
|
data = txt.encode("utf-8")
|
||||||
|
elif name == "[Content_Types].xml":
|
||||||
|
txt = data.decode("utf-8")
|
||||||
|
txt = txt.replace('<Default Extension="jpeg" ContentType="image/jpeg"/>', "")
|
||||||
|
data = txt.encode("utf-8")
|
||||||
|
out.writestr(info, data)
|
||||||
|
out.close()
|
||||||
|
|
||||||
|
|
||||||
|
def scrub(prs, branded):
|
||||||
|
cp = prs.core_properties
|
||||||
|
cp.author = "MyInfoMate" if branded else ""
|
||||||
|
cp.last_modified_by = "MyInfoMate" if branded else ""
|
||||||
|
cp.company = ""
|
||||||
|
cp.title = "MNAHA — Gestion de contenu multi-sites"
|
||||||
|
cp.subject = ""
|
||||||
|
cp.comments = ""
|
||||||
|
cp.category = ""
|
||||||
|
cp.keywords = ""
|
||||||
|
cp.content_status = ""
|
||||||
|
cp.identifier = ""
|
||||||
|
cp.language = "fr-BE"
|
||||||
|
cp.revision = 1
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for branded, sub, name in [
|
||||||
|
(True, "branded", "MNAHA_presentation_branded.pptx"),
|
||||||
|
(False, "whitelabel", "MNAHA_presentation_whitelabel.pptx"),
|
||||||
|
]:
|
||||||
|
path, count = build(branded, ROOT / sub / name)
|
||||||
|
print(f"{path} ({count} slides)")
|
||||||
170
outputs/mnaha/seed/README.md
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
# Instance de démonstration MNAHA
|
||||||
|
|
||||||
|
Jeu de données local pour les captures d'écran des decks MNAHA. Créé via l'API
|
||||||
|
(`https://localhost:5001`), pas en SQL direct : l'indexation de l'assistant se
|
||||||
|
déclenche alors toute seule à chaque publication.
|
||||||
|
|
||||||
|
## Ce qui a été créé
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Instance | **MNAHA**, slug `mnaha`, plan Premium, assistant activé (guide « Melusina ») |
|
||||||
|
| Langues | FR, DE, EN, NL — le luxembourgeois n'est pas géré par le code |
|
||||||
|
| Sites | Nationalmusée um Fëschmaart · Musée Dräi Eechelen · Réimervilla Echternach |
|
||||||
|
| Applications | mobile, borne, web — les trois liées aux trois sites, tuiles bento réglées |
|
||||||
|
| Contenus | 9 articles en 4 langues, 1 sommaire, 1 quiz (3 questions), 1 agenda (4 événements) |
|
||||||
|
| Carte | Echternach, 5 points d'intérêt géolocalisés, 3 catégories |
|
||||||
|
| Parcours | « Sur les pas du maître de la villa » — 5 étapes déclenchées par zone GPS |
|
||||||
|
| Escape game | « Le secret de la villa de Vichten » — 4 étapes, 4 énigmes bloquantes |
|
||||||
|
| Balises | un identifiant de balise sur chacun des 9 articles |
|
||||||
|
| Fréquentation | 23 980 événements, 2 503 sessions sur 90 jours, visite moyenne 8,1 min |
|
||||||
|
| Assistant | 14 questions de visiteurs, dont 5 restées sans réponse |
|
||||||
|
|
||||||
|
Répartition de la fréquentation : web 47 %, mobile 33 %, borne 20 % ; FR 44 %,
|
||||||
|
DE 24 %, EN 21 %, NL 10 %. Entre 29 et 254 visites par jour, avec des pics de
|
||||||
|
week-end et une légère montée sur la période.
|
||||||
|
|
||||||
|
## Accès
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Back-office | `test@email.be` — SuperAdmin, voit toutes les entrées de menu |
|
||||||
|
| Code PIN de l'instance | **1867** — sert à l'appairage des applications visiteur |
|
||||||
|
| Adresse web | slug `mnaha` → `http://localhost:3000/mnaha` avec `visitapp-web` |
|
||||||
|
| Clés d'application | `state.json`, champs `apiKeys.VisitApp` et `apiKeys.TabletApp` |
|
||||||
|
|
||||||
|
Lancer une application visiteur sur cette instance :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter run -d chrome \
|
||||||
|
--dart-define=API_BASE_URL=https://localhost:5001 \
|
||||||
|
--dart-define=INSTANCE_ID=<state.json → instanceId> \
|
||||||
|
--dart-define=API_KEY=<state.json → apiKeys.VisitApp>
|
||||||
|
```
|
||||||
|
|
||||||
|
L'instance est en plan Premium complet : mobile + borne + web, assistant IA
|
||||||
|
(20 M de jetons/mois), notifications push, statistiques avancées sur 395 jours,
|
||||||
|
50 Go de stockage, collecte des questions visiteurs activée. Seul `isVR` reste à
|
||||||
|
faux — l'écran VR du back-office n'affiche qu'un « TODO ».
|
||||||
|
|
||||||
|
## Rejouer
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd outputs/mnaha/seed
|
||||||
|
python step1_instance.py # instance, sites, applications
|
||||||
|
python step2_sections.py # articles, sommaire, quiz
|
||||||
|
python step3_map_parcours.py # carte, parcours, escape game, agenda
|
||||||
|
python step4_stats.py # 90 jours de fréquentation + ré-indexation
|
||||||
|
python step5_assistant.py # conversations visiteurs (consomme du quota IA)
|
||||||
|
python step6_order.py # ordre d'affichage des sections
|
||||||
|
python step7_images.py # ressources ImageUrl + rattachement aux sections
|
||||||
|
python step7b_finish.py # points d'intérêt et habillage des applications
|
||||||
|
python step8_devices.py # parc de trois bornes, rattachées aux liens kiosk
|
||||||
|
```
|
||||||
|
|
||||||
|
Les étapes 1 à 3 et 6 sont idempotentes. **L'étape 4 ne l'est pas** : la relancer
|
||||||
|
ajoute 24 000 événements de plus. Pour repartir à zéro :
|
||||||
|
|
||||||
|
```sql
|
||||||
|
DELETE FROM "VisitEvents" WHERE "InstanceId" = '<id de l''instance>';
|
||||||
|
```
|
||||||
|
|
||||||
|
`state.json` garde les identifiants créés ; le supprimer force une nouvelle
|
||||||
|
instance (le nom « MNAHA » étant unique, la création échouerait alors en 409).
|
||||||
|
|
||||||
|
## Deux pièges rencontrés, notés pour la prochaine fois
|
||||||
|
|
||||||
|
**Dates.** Une date sérialisée en `+00:00` est désérialisée par .NET en
|
||||||
|
`Kind=Local`, et Npgsql refuse alors de l'écrire dans une colonne `timestamptz`
|
||||||
|
— erreur 500 opaque. Il faut le suffixe `Z`.
|
||||||
|
|
||||||
|
**Durée de visite.** Elle se calcule en sommant les `DurationSeconds` des
|
||||||
|
événements `SectionLeave`, pas des `SectionView`. Sans `SectionLeave`, le
|
||||||
|
tableau de bord affiche zéro minute.
|
||||||
|
|
||||||
|
## Visuels
|
||||||
|
|
||||||
|
21 images générées par `gen_images.py` : 3 sites, 9 articles, 2 parcours,
|
||||||
|
5 points d'intérêt, l'image d'accueil et l'écran de chargement des trois
|
||||||
|
applications.
|
||||||
|
|
||||||
|
> **Lancer `serve_images.py` avant toute capture.** Les visuels sont servis sur
|
||||||
|
> `http://localhost:8099` et les ressources y pointent. Serveur éteint, images
|
||||||
|
> absentes.
|
||||||
|
|
||||||
|
Pourquoi un serveur local plutôt qu'un téléversement : **`POST /api/Resource/upload`
|
||||||
|
ne stocke aucun octet.** Il lit le fichier, l'encode en base64, jette le
|
||||||
|
résultat, et n'enregistre que le libellé, le chemin de bucket et la taille. Le
|
||||||
|
blob est déposé dans Firebase Storage par `manager-app` depuis le navigateur —
|
||||||
|
un script qui parle à l'API n'a aucun moyen de l'y écrire. Les ressources créées
|
||||||
|
par cet endpoint ressortent donc sans `url`, et `manager-app` plante dessus
|
||||||
|
(`NetworkImage(resource.url!)`, `resource_input_container.dart:170`).
|
||||||
|
|
||||||
|
Les ressources de démonstration sont donc créées en type `ImageUrl`, qui pointe
|
||||||
|
hors bucket et ne pèse pas sur le quota. Le jour où les visuels du dossier de
|
||||||
|
presse arrivent, le chemin normal est de les déposer par le CMS.
|
||||||
|
|
||||||
|
Ce sont des compositions abstraites — trame de tesselles, plan orthogonal,
|
||||||
|
courbes de niveau, éventails Art déco, monnaies — déclinées dans la palette de
|
||||||
|
chaque site. **Aucune photographie de collection** : les droits n'appartiennent
|
||||||
|
pas au projet. Elles se remplacent par les visuels du dossier de presse du MNAHA
|
||||||
|
le jour où il est disponible.
|
||||||
|
|
||||||
|
Quatre autres motifs (`bastion`, `pillars`, `arcs`, `strata`) restent dans le
|
||||||
|
fichier mais ne sont pas utilisés : réduits à une vignette, ils se lisent comme
|
||||||
|
un aplat vide ou comme une frise trop littérale.
|
||||||
|
|
||||||
|
## Liens de borne dupliqués — artefact du script, pas bug produit
|
||||||
|
|
||||||
|
`POST /api/Device` crée lui-même un `AppConfigurationLink` pour chaque nouvel
|
||||||
|
appareil (`DeviceController.cs:169-177`), sans se rapprocher d'un lien déjà
|
||||||
|
présent pour ce couple application/configuration. `step1_instance.py` liait les
|
||||||
|
trois sites aux trois applications, borne comprise : les trois appareils créés
|
||||||
|
ensuite ont donc ajouté trois liens de plus.
|
||||||
|
|
||||||
|
`step1_instance.py` ne crée plus de lien pour la borne — ils naissent de
|
||||||
|
l'appairage, ce qui est le comportement réel du produit. `step9_dedupe_links.py`
|
||||||
|
nettoie ceux qui existent déjà.
|
||||||
|
|
||||||
|
**Ce n'est pas atteignable depuis le CMS** : `showAddConfigurationLink` n'est
|
||||||
|
utilisé que par l'écran des applications mobile et web, jamais par
|
||||||
|
`kiosk_screen.dart`. Un gestionnaire ne peut pas créer un lien de borne à la
|
||||||
|
main. La fragilité reste (la création d'appareil ne vérifie rien) mais elle
|
||||||
|
dort derrière un chemin que seule l'API expose.
|
||||||
|
|
||||||
|
Vérifié au passage, contrairement à ce que j'avais d'abord écrit : un
|
||||||
|
`PUT /api/ApplicationInstance` portant ses `configurations` **ne duplique pas**,
|
||||||
|
il échoue en 500 sur la clé primaire — et le `GET` ne renvoie de toute façon pas
|
||||||
|
ce tableau. Un `PUT` de lien isolé ne duplique pas non plus.
|
||||||
|
|
||||||
|
## Quatre correctifs apportés au produit en passant
|
||||||
|
|
||||||
|
**`DELETE /api/Resource/{id}` échouait sur toute carte ayant des catégories.**
|
||||||
|
Le nettoyage des références faisait `categorie.resourceDTO.id == id` sans
|
||||||
|
vérifier que `resourceDTO` existe ; une catégorie sans icône le laisse nul.
|
||||||
|
Passé en `?.`.
|
||||||
|
|
||||||
|
**L'écran des bornes plantait sur un lien sans appareil.**
|
||||||
|
`kiosk_screen.dart` faisait `deviceLink.device!` alors qu'un contenu peut être
|
||||||
|
attribué à une borne avant qu'une tablette ne s'y appaire. La carte affiche
|
||||||
|
maintenant « aucune tablette appairée » au lieu de lever une exception.
|
||||||
|
|
||||||
|
|
||||||
|
**`GET /api/Ai/insights/{instanceId}` renvoyait 500**, sur toutes les instances,
|
||||||
|
y compris sans aucune question. `AiController` faisait un `SelectMany` sur
|
||||||
|
`CitedContentIds`, une `List<string>` stockée en jsonb via un convertisseur de
|
||||||
|
valeur : EF ne sait pas traduire cette expression en SQL. Les listes sont
|
||||||
|
maintenant rapatriées avant d'être aplaties côté client. `manager-app` avalait
|
||||||
|
l'erreur en silence, donc l'onglet « questions des visiteurs » restait
|
||||||
|
désespérément vide sans que rien ne le signale.
|
||||||
|
|
||||||
|
**Le changement d'instance plantait `manager-app`** (`Bad state: No element`).
|
||||||
|
La bascule vidait `menu.sections` puis naviguait vers la même route : Flutter
|
||||||
|
réutilisait le `State`, `initState` ne rejouait pas, et le menu perdait
|
||||||
|
définitivement Applications / Configurations / Ressources / Statistiques /
|
||||||
|
Guide IA. Construction du menu extraite dans `_buildMenu`, appelée aussi depuis
|
||||||
|
`didUpdateWidget`, et repli au lieu d'une exception quand la position courante
|
||||||
|
ne correspond à aucune entrée.
|
||||||
|
|
||||||
|
Les deux exigent un redémarrage : `dotnet run` pour le backend, **hot restart**
|
||||||
|
(pas hot reload) pour `manager-app`.
|
||||||
64
outputs/mnaha/seed/api.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Client minimal pour l'API locale de manager-service.
|
||||||
|
|
||||||
|
Le certificat de dev n'est pas vérifié : on tape sur localhost, pas sur la prod.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import urllib3
|
||||||
|
import requests
|
||||||
|
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
BASE = "https://localhost:5001"
|
||||||
|
EMAIL = "test@email.be"
|
||||||
|
PASSWORD = "kljqsdkljqsd"
|
||||||
|
|
||||||
|
LANGS = ["FR", "DE", "EN", "NL"]
|
||||||
|
|
||||||
|
|
||||||
|
class Api:
|
||||||
|
def __init__(self):
|
||||||
|
self.s = requests.Session()
|
||||||
|
self.s.verify = False
|
||||||
|
r = self.s.post(f"{BASE}/api/Authentication/Authenticate",
|
||||||
|
json={"email": EMAIL, "password": PASSWORD}, timeout=30)
|
||||||
|
r.raise_for_status()
|
||||||
|
self.token = r.json()["access_token"]
|
||||||
|
self.s.headers.update({"Authorization": f"Bearer {self.token}",
|
||||||
|
"Content-Type": "application/json"})
|
||||||
|
|
||||||
|
def _call(self, method, path, timeout=600, **kw):
|
||||||
|
# Généreux à dessein : un PUT sur une section déclenche sa ré-indexation
|
||||||
|
# pour l'assistant, donc un embedding par morceau et par langue.
|
||||||
|
r = self.s.request(method, f"{BASE}{path}", timeout=timeout, **kw)
|
||||||
|
if not r.ok:
|
||||||
|
raise RuntimeError(f"{method} {path} -> {r.status_code}\n{r.text[:600]}")
|
||||||
|
if not r.content:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return r.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def get(self, path, **kw):
|
||||||
|
return self._call("GET", path, **kw)
|
||||||
|
|
||||||
|
def post(self, path, body=None, **kw):
|
||||||
|
return self._call("POST", path, json=body, **kw)
|
||||||
|
|
||||||
|
def put(self, path, body=None, **kw):
|
||||||
|
return self._call("PUT", path, json=body, **kw)
|
||||||
|
|
||||||
|
def delete(self, path, **kw):
|
||||||
|
return self._call("DELETE", path, **kw)
|
||||||
|
|
||||||
|
|
||||||
|
def tr(**by_lang):
|
||||||
|
"""{'FR': 'Bonjour'} -> [{'language': 'FR', 'value': 'Bonjour'}], toutes langues.
|
||||||
|
|
||||||
|
Une langue absente reprend le français : mieux vaut un contenu non traduit
|
||||||
|
qu'un champ vide qui casserait l'affichage sur la borne.
|
||||||
|
"""
|
||||||
|
fallback = by_lang.get("FR") or next(iter(by_lang.values()), "")
|
||||||
|
return [{"language": lang, "value": by_lang.get(lang, fallback)} for lang in LANGS]
|
||||||
695
outputs/mnaha/seed/content.py
Normal file
@ -0,0 +1,695 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Contenus de démonstration MNAHA.
|
||||||
|
|
||||||
|
Textes rédigés pour la démonstration à partir d'informations publiques sur les
|
||||||
|
collections. Aucune photographie des collections n'est utilisée : les droits
|
||||||
|
n'appartiennent pas au projet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def p(*paragraphs):
|
||||||
|
"""Le CMS produit du HTML via Quill ; on reproduit sa sortie."""
|
||||||
|
return "".join(f"<p>{t}</p>" for t in paragraphs)
|
||||||
|
|
||||||
|
|
||||||
|
def h(title):
|
||||||
|
return f"<h3>{title}</h3>"
|
||||||
|
|
||||||
|
|
||||||
|
def ul(*items):
|
||||||
|
return "<ul>" + "".join(f"<li>{i}</li>" for i in items) + "</ul>"
|
||||||
|
|
||||||
|
|
||||||
|
def quote(text):
|
||||||
|
return f"<blockquote>{text}</blockquote>"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ARTICLES = {
|
||||||
|
"feschmaart": [
|
||||||
|
{
|
||||||
|
"label": "Mosaïque de Vichten",
|
||||||
|
"beacon": 101,
|
||||||
|
"coords": ("49.61165", "6.13205"),
|
||||||
|
"title": {
|
||||||
|
"FR": "La mosaïque de Vichten",
|
||||||
|
"DE": "Das Mosaik von Vichten",
|
||||||
|
"EN": "The Vichten Mosaic",
|
||||||
|
"NL": "Het mozaïek van Vichten",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "240 apr. J.-C. — Salle d'archéologie, niveau -2",
|
||||||
|
"DE": "240 n. Chr. — Archäologiesaal, Ebene -2",
|
||||||
|
"EN": "AD 240 — Archaeology hall, level -2",
|
||||||
|
"NL": "240 n.Chr. — Archeologiezaal, niveau -2",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Découverte en 1995 dans les vestiges d'une villa gallo-romaine à "
|
||||||
|
"Vichten, cette mosaïque de sol compte parmi les plus remarquables "
|
||||||
|
"au nord des Alpes. Elle occupait la salle de réception du domaine.",
|
||||||
|
"Au centre, Homère est entouré des neuf Muses, chacune identifiée "
|
||||||
|
"par son nom et son attribut. La composition, d'une finesse rare, "
|
||||||
|
"témoigne de la culture littéraire des propriétaires du domaine au "
|
||||||
|
"milieu du III<sup>e</sup> siècle."),
|
||||||
|
"DE": p(
|
||||||
|
"1995 in den Überresten einer gallo-römischen Villa in Vichten "
|
||||||
|
"entdeckt, zählt dieses Bodenmosaik zu den bedeutendsten nördlich "
|
||||||
|
"der Alpen. Es schmückte den Empfangsraum des Anwesens.",
|
||||||
|
"In der Mitte ist Homer von den neun Musen umgeben, jede mit Namen "
|
||||||
|
"und Attribut bezeichnet. Die außergewöhnlich feine Komposition "
|
||||||
|
"zeugt von der literarischen Bildung der Besitzer um die Mitte des "
|
||||||
|
"3. Jahrhunderts."),
|
||||||
|
"EN": p(
|
||||||
|
"Discovered in 1995 in the remains of a Gallo-Roman villa at "
|
||||||
|
"Vichten, this floor mosaic is among the most remarkable north of "
|
||||||
|
"the Alps. It adorned the reception room of the estate.",
|
||||||
|
"At its centre, Homer is surrounded by the nine Muses, each named "
|
||||||
|
"and identified by her attribute. The unusually fine composition "
|
||||||
|
"speaks to the literary culture of the owners in the middle of the "
|
||||||
|
"third century."),
|
||||||
|
"NL": p(
|
||||||
|
"In 1995 ontdekt in de resten van een Gallo-Romeinse villa in "
|
||||||
|
"Vichten, behoort dit vloermozaïek tot de merkwaardigste ten "
|
||||||
|
"noorden van de Alpen. Het sierde de ontvangstzaal van het domein.",
|
||||||
|
"In het midden is Homerus omringd door de negen Muzen, elk met naam "
|
||||||
|
"en attribuut. De verfijnde compositie getuigt van de literaire "
|
||||||
|
"cultuur van de eigenaars omstreeks het midden van de derde eeuw."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Goeblange-Nospelt",
|
||||||
|
"beacon": 102,
|
||||||
|
"coords": ("49.61170", "6.13190"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Les chambres funéraires de Goeblange-Nospelt",
|
||||||
|
"DE": "Die Grabkammern von Goeblange-Nospelt",
|
||||||
|
"EN": "The Goeblange-Nospelt burial chambers",
|
||||||
|
"NL": "De grafkamers van Goeblange-Nospelt",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "I<sup>er</sup> siècle av. J.-C. — Salle d'archéologie, niveau -2",
|
||||||
|
"DE": "1. Jahrhundert v. Chr. — Archäologiesaal, Ebene -2",
|
||||||
|
"EN": "1st century BC — Archaeology hall, level -2",
|
||||||
|
"NL": "1e eeuw v.Chr. — Archeologiezaal, niveau -2",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Les tombes de l'aristocratie trévire mises au jour à "
|
||||||
|
"Goeblange-Nospelt livrent un mobilier funéraire d'une richesse "
|
||||||
|
"exceptionnelle : services à vin en bronze, amphores, armes.",
|
||||||
|
"Ces dépôts éclairent le moment où les élites gauloises adoptent "
|
||||||
|
"les usages romains sans renoncer à leurs propres rites."),
|
||||||
|
"DE": p(
|
||||||
|
"Die in Goeblange-Nospelt freigelegten Gräber der treverischen "
|
||||||
|
"Aristokratie enthielten außergewöhnlich reiche Beigaben: "
|
||||||
|
"Weinservice aus Bronze, Amphoren, Waffen.",
|
||||||
|
"Diese Funde beleuchten den Moment, in dem die gallischen Eliten "
|
||||||
|
"römische Gebräuche übernahmen, ohne ihre eigenen Riten aufzugeben."),
|
||||||
|
"EN": p(
|
||||||
|
"The tombs of the Treveran aristocracy uncovered at "
|
||||||
|
"Goeblange-Nospelt held exceptionally rich grave goods: bronze wine "
|
||||||
|
"services, amphorae, weapons.",
|
||||||
|
"These deposits illuminate the moment when Gaulish elites adopted "
|
||||||
|
"Roman customs without abandoning their own rites."),
|
||||||
|
"NL": p(
|
||||||
|
"De in Goeblange-Nospelt blootgelegde graven van de Trevirische "
|
||||||
|
"aristocratie bevatten uitzonderlijk rijke grafgiften: bronzen "
|
||||||
|
"wijnserviezen, amforen, wapens.",
|
||||||
|
"Deze vondsten belichten het moment waarop de Gallische elites "
|
||||||
|
"Romeinse gebruiken overnamen zonder hun eigen riten op te geven."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Villeroy & Boch Art déco",
|
||||||
|
"beacon": 103,
|
||||||
|
"coords": ("49.61180", "6.13215"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Villeroy & Boch, l'Art déco luxembourgeois",
|
||||||
|
"DE": "Villeroy & Boch, Luxemburger Art déco",
|
||||||
|
"EN": "Villeroy & Boch, Luxembourg Art Deco",
|
||||||
|
"NL": "Villeroy & Boch, Luxemburgse art deco",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "1920-1940 — Arts décoratifs, niveau 2",
|
||||||
|
"DE": "1920-1940 — Kunstgewerbe, Ebene 2",
|
||||||
|
"EN": "1920-1940 — Decorative arts, level 2",
|
||||||
|
"NL": "1920-1940 — Sierkunst, niveau 2",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"La manufacture de Septfontaines a produit, entre les deux guerres, "
|
||||||
|
"des séries de céramiques aux lignes géométriques caractéristiques "
|
||||||
|
"de l'Art déco.",
|
||||||
|
"La collection du musée retrace cette production industrielle "
|
||||||
|
"devenue patrimoine, des pièces de série aux modèles de créateurs."),
|
||||||
|
"DE": p(
|
||||||
|
"Die Manufaktur Septfontaines produzierte zwischen den Weltkriegen "
|
||||||
|
"Keramikserien mit den geometrischen Linien des Art déco.",
|
||||||
|
"Die Sammlung des Museums zeichnet diese Industrieproduktion nach, "
|
||||||
|
"die zum Kulturerbe wurde — von der Serienware bis zum "
|
||||||
|
"Künstlerentwurf."),
|
||||||
|
"EN": p(
|
||||||
|
"Between the wars, the Septfontaines works produced ceramic ranges "
|
||||||
|
"in the geometric lines characteristic of Art Deco.",
|
||||||
|
"The museum's collection traces this industrial output as it became "
|
||||||
|
"heritage, from production runs to designer models."),
|
||||||
|
"NL": p(
|
||||||
|
"Tussen beide wereldoorlogen produceerde de fabriek van "
|
||||||
|
"Septfontaines keramiekseries met de geometrische lijnen van de art "
|
||||||
|
"deco.",
|
||||||
|
"De collectie van het museum volgt deze industriële productie die "
|
||||||
|
"erfgoed werd, van serieproductie tot ontwerpersmodellen."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Cabinet des médailles",
|
||||||
|
"beacon": 104,
|
||||||
|
"coords": ("49.61158", "6.13198"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Le cabinet des médailles",
|
||||||
|
"DE": "Das Münzkabinett",
|
||||||
|
"EN": "The coin cabinet",
|
||||||
|
"NL": "Het penningkabinet",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "Numismatique — niveau 1",
|
||||||
|
"DE": "Numismatik — Ebene 1",
|
||||||
|
"EN": "Numismatics — level 1",
|
||||||
|
"NL": "Numismatiek — niveau 1",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Monnaies celtiques, romaines, médiévales et modernes : le cabinet "
|
||||||
|
"conserve la mémoire monétaire du territoire luxembourgeois sur "
|
||||||
|
"plus de deux millénaires.",
|
||||||
|
"Chaque pièce documente un pouvoir, un échange, une frontière."),
|
||||||
|
"DE": p(
|
||||||
|
"Keltische, römische, mittelalterliche und moderne Münzen: das "
|
||||||
|
"Kabinett bewahrt das monetäre Gedächtnis des luxemburgischen "
|
||||||
|
"Raums über mehr als zwei Jahrtausende.",
|
||||||
|
"Jedes Stück dokumentiert eine Macht, einen Austausch, eine Grenze."),
|
||||||
|
"EN": p(
|
||||||
|
"Celtic, Roman, medieval and modern coinage: the cabinet holds the "
|
||||||
|
"monetary memory of Luxembourg's territory across more than two "
|
||||||
|
"millennia.",
|
||||||
|
"Every piece documents a power, an exchange, a border."),
|
||||||
|
"NL": p(
|
||||||
|
"Keltische, Romeinse, middeleeuwse en moderne munten: het kabinet "
|
||||||
|
"bewaart het monetaire geheugen van het Luxemburgse gebied over "
|
||||||
|
"meer dan twee millennia.",
|
||||||
|
"Elk stuk documenteert een macht, een uitwisseling, een grens."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"draieechelen": [
|
||||||
|
{
|
||||||
|
"label": "Fort Thüngen",
|
||||||
|
"beacon": 201,
|
||||||
|
"coords": ("49.62115", "6.14185"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Le Fort Thüngen",
|
||||||
|
"DE": "Das Fort Thüngen",
|
||||||
|
"EN": "Fort Thüngen",
|
||||||
|
"NL": "Fort Thüngen",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "1732 — Le fort et ses trois tours",
|
||||||
|
"DE": "1732 — Das Fort und seine drei Türme",
|
||||||
|
"EN": "1732 — The fort and its three towers",
|
||||||
|
"NL": "1732 — Het fort en zijn drie torens",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Édifié en 1732 par les Autrichiens, le Fort Thüngen doit son "
|
||||||
|
"surnom de « Dräi Eechelen » aux trois glands dorés qui couronnent "
|
||||||
|
"ses tours.",
|
||||||
|
"Démantelé après le traité de Londres de 1867, il a été restauré "
|
||||||
|
"puis transformé en musée consacré à l'histoire de la forteresse."),
|
||||||
|
"DE": p(
|
||||||
|
"1732 von den Österreichern errichtet, verdankt das Fort Thüngen "
|
||||||
|
"seinen Beinamen „Dräi Eechelen“ den drei vergoldeten Eicheln auf "
|
||||||
|
"seinen Türmen.",
|
||||||
|
"Nach dem Londoner Vertrag von 1867 geschleift, wurde es restauriert "
|
||||||
|
"und in ein Museum zur Geschichte der Festung umgewandelt."),
|
||||||
|
"EN": p(
|
||||||
|
"Built in 1732 by the Austrians, Fort Thüngen owes its nickname "
|
||||||
|
"“Dräi Eechelen” to the three gilded acorns crowning its towers.",
|
||||||
|
"Dismantled after the 1867 Treaty of London, it was restored and "
|
||||||
|
"turned into a museum devoted to the history of the fortress."),
|
||||||
|
"NL": p(
|
||||||
|
"Het in 1732 door de Oostenrijkers gebouwde Fort Thüngen dankt zijn "
|
||||||
|
"bijnaam „Dräi Eechelen“ aan de drie vergulde eikels op zijn torens.",
|
||||||
|
"Na het Verdrag van Londen van 1867 ontmanteld, werd het "
|
||||||
|
"gerestaureerd en omgevormd tot een museum over de geschiedenis van "
|
||||||
|
"de vesting."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Démantèlement 1867",
|
||||||
|
"beacon": 202,
|
||||||
|
"coords": ("49.62130", "6.14170"),
|
||||||
|
"title": {
|
||||||
|
"FR": "1867 : le démantèlement de la forteresse",
|
||||||
|
"DE": "1867: die Schleifung der Festung",
|
||||||
|
"EN": "1867: dismantling the fortress",
|
||||||
|
"NL": "1867: de ontmanteling van de vesting",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "Traité de Londres — salle 4",
|
||||||
|
"DE": "Londoner Vertrag — Saal 4",
|
||||||
|
"EN": "Treaty of London — room 4",
|
||||||
|
"NL": "Verdrag van Londen — zaal 4",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Le traité de Londres impose la neutralité du Luxembourg et la "
|
||||||
|
"démolition de ses fortifications. Seize années de travaux effacent "
|
||||||
|
"l'une des places fortes les plus puissantes d'Europe.",
|
||||||
|
"Ce qu'il en reste — casemates, redoutes, murs — dessine encore la "
|
||||||
|
"ville d'aujourd'hui."),
|
||||||
|
"DE": p(
|
||||||
|
"Der Londoner Vertrag verordnet Luxemburgs Neutralität und den "
|
||||||
|
"Abriss seiner Befestigungen. Sechzehn Jahre Arbeit tilgen eine der "
|
||||||
|
"stärksten Festungen Europas.",
|
||||||
|
"Was blieb — Kasematten, Redouten, Mauern — prägt bis heute das "
|
||||||
|
"Stadtbild."),
|
||||||
|
"EN": p(
|
||||||
|
"The Treaty of London imposed Luxembourg's neutrality and the "
|
||||||
|
"demolition of its fortifications. Sixteen years of work erased one "
|
||||||
|
"of Europe's most powerful strongholds.",
|
||||||
|
"What remains — casemates, redoubts, walls — still shapes the city "
|
||||||
|
"today."),
|
||||||
|
"NL": p(
|
||||||
|
"Het Verdrag van Londen legde de neutraliteit van Luxemburg op en "
|
||||||
|
"de sloop van zijn vestingwerken. Zestien jaar werk wiste een van "
|
||||||
|
"de sterkste vestingen van Europa uit.",
|
||||||
|
"Wat overbleef — kazematten, redoutes, muren — tekent vandaag nog "
|
||||||
|
"de stad."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Maquettes et plans-reliefs",
|
||||||
|
"beacon": 203,
|
||||||
|
"coords": ("49.62100", "6.14200"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Maquettes et plans-reliefs",
|
||||||
|
"DE": "Modelle und Reliefpläne",
|
||||||
|
"EN": "Models and relief maps",
|
||||||
|
"NL": "Maquettes en reliëfkaarten",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "La forteresse en trois dimensions — salle 6",
|
||||||
|
"DE": "Die Festung in drei Dimensionen — Saal 6",
|
||||||
|
"EN": "The fortress in three dimensions — room 6",
|
||||||
|
"NL": "De vesting in drie dimensies — zaal 6",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"Les plans-reliefs servaient aux ingénieurs militaires à penser le "
|
||||||
|
"terrain avant de le fortifier. Ils offrent aujourd'hui la lecture "
|
||||||
|
"la plus claire d'un système défensif disparu.",
|
||||||
|
"Les maquettes du musée restituent l'ampleur des ouvrages successifs, "
|
||||||
|
"du Moyen Âge au XIX<sup>e</sup> siècle."),
|
||||||
|
"DE": p(
|
||||||
|
"Reliefpläne dienten Militäringenieuren dazu, das Gelände vor der "
|
||||||
|
"Befestigung zu erfassen. Heute bieten sie den klarsten Zugang zu "
|
||||||
|
"einem verschwundenen Verteidigungssystem.",
|
||||||
|
"Die Modelle des Museums vergegenwärtigen den Umfang der "
|
||||||
|
"aufeinanderfolgenden Anlagen vom Mittelalter bis ins 19. "
|
||||||
|
"Jahrhundert."),
|
||||||
|
"EN": p(
|
||||||
|
"Relief maps allowed military engineers to read the terrain before "
|
||||||
|
"fortifying it. Today they offer the clearest view of a vanished "
|
||||||
|
"defensive system.",
|
||||||
|
"The museum's models convey the scale of successive works, from the "
|
||||||
|
"Middle Ages to the nineteenth century."),
|
||||||
|
"NL": p(
|
||||||
|
"Reliëfkaarten hielpen militaire ingenieurs het terrein te lezen "
|
||||||
|
"voor ze het versterkten. Vandaag bieden ze het helderste beeld van "
|
||||||
|
"een verdwenen verdedigingsstelsel.",
|
||||||
|
"De maquettes van het museum tonen de omvang van de opeenvolgende "
|
||||||
|
"werken, van de middeleeuwen tot de negentiende eeuw."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"echternach": [
|
||||||
|
{
|
||||||
|
"label": "La villa romaine",
|
||||||
|
"beacon": 301,
|
||||||
|
"coords": ("49.80655", "6.40210"),
|
||||||
|
"title": {
|
||||||
|
"FR": "La villa romaine d'Echternach",
|
||||||
|
"DE": "Die römische Villa von Echternach",
|
||||||
|
"EN": "The Roman villa of Echternach",
|
||||||
|
"NL": "De Romeinse villa van Echternach",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "I<sup>er</sup>-V<sup>e</sup> siècle — entrée du site",
|
||||||
|
"DE": "1.-5. Jahrhundert — Eingang der Anlage",
|
||||||
|
"EN": "1st-5th century — site entrance",
|
||||||
|
"NL": "1e-5e eeuw — ingang van de site",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": (
|
||||||
|
p("Fouillée en 1975 et 1976, la villa d'Echternach est l'un des "
|
||||||
|
"plus vastes domaines gallo-romains au nord des Alpes : plus de "
|
||||||
|
"<strong>cent mètres de façade</strong>, des portiques, des cours "
|
||||||
|
"intérieures et des bassins.")
|
||||||
|
+ h("Un domaine hors norme")
|
||||||
|
+ p("Le bâtiment résidentiel s'organisait autour d'une cour "
|
||||||
|
"centrale, prolongée par des jardins descendant vers la Sûre. "
|
||||||
|
"L'ensemble tenait autant de la résidence de prestige que de "
|
||||||
|
"l'exploitation agricole.")
|
||||||
|
+ ul("Une <strong>façade monumentale</strong> tournée vers la "
|
||||||
|
"vallée, rythmée par une longue galerie couverte.",
|
||||||
|
"Des <strong>dizaines de pièces</strong> autour de la cour : "
|
||||||
|
"réception, appartements privés, quartiers de service.",
|
||||||
|
"Des <strong>jardins et bassins d'agrément</strong>, mis en "
|
||||||
|
"scène dans l'axe de la villa.",
|
||||||
|
"Une <strong>partie agricole</strong> qui faisait vivre le "
|
||||||
|
"domaine et approvisionnait la région.")
|
||||||
|
+ h("Vivre au bord de la Sûre")
|
||||||
|
+ p("Sols et murs recevaient <strong>enduits peints</strong> et "
|
||||||
|
"revêtements soignés ; le domaine disposait de ses propres "
|
||||||
|
"thermes. Entretenir un tel ensemble supposait une fortune "
|
||||||
|
"considérable — et le goût de la montrer.")
|
||||||
|
+ quote("Le saviez-vous ? La villa ne s'est pas construite d'un "
|
||||||
|
"seul geste : agrandie au fil des générations, elle atteint "
|
||||||
|
"son apogée au <strong>II<sup>e</sup> siècle</strong> avant "
|
||||||
|
"d'être délaissée dans les troubles de l'Antiquité tardive.")
|
||||||
|
+ h("En bref")
|
||||||
|
+ ul("<strong>Fouilles :</strong> 1975 et 1976",
|
||||||
|
"<strong>Façade :</strong> plus de 100 mètres",
|
||||||
|
"<strong>Apogée :</strong> II<sup>e</sup> siècle apr. J.-C.",
|
||||||
|
"<strong>Cadre :</strong> vallée de la Sûre, Echternach")
|
||||||
|
),
|
||||||
|
"DE": (
|
||||||
|
p("1975 und 1976 ausgegraben, ist die Villa von Echternach eine "
|
||||||
|
"der größten gallo-römischen Anlagen nördlich der Alpen: über "
|
||||||
|
"<strong>hundert Meter Fassade</strong>, Portiken, Innenhöfe und "
|
||||||
|
"Becken.")
|
||||||
|
+ h("Eine Anlage von seltenem Ausmaß")
|
||||||
|
+ p("Das Wohngebäude gruppierte sich um einen zentralen Hof, "
|
||||||
|
"verlängert durch Gärten, die zur Sauer hin abfielen. Das "
|
||||||
|
"Anwesen war ebenso Prunkresidenz wie Landwirtschaftsbetrieb.")
|
||||||
|
+ ul("Eine <strong>monumentale Fassade</strong> zum Tal hin, "
|
||||||
|
"gegliedert durch eine lange überdachte Galerie.",
|
||||||
|
"<strong>Dutzende Räume</strong> um den Hof: Empfang, "
|
||||||
|
"Privatgemächer, Wirtschaftsräume.",
|
||||||
|
"<strong>Gärten und Zierbecken</strong>, in der Achse der "
|
||||||
|
"Villa in Szene gesetzt.",
|
||||||
|
"Ein <strong>landwirtschaftlicher Teil</strong>, der das "
|
||||||
|
"Anwesen trug und die Region versorgte.")
|
||||||
|
+ h("Leben an der Sauer")
|
||||||
|
+ p("Böden und Wände trugen <strong>bemalten Putz</strong> und "
|
||||||
|
"sorgfältige Beläge; das Anwesen besaß eigene Thermen. Ein "
|
||||||
|
"solches Ensemble zu unterhalten setzte großen Reichtum "
|
||||||
|
"voraus — und die Lust, ihn zu zeigen.")
|
||||||
|
+ quote("Wussten Sie schon? Die Villa entstand nicht in einem Zug: "
|
||||||
|
"über Generationen erweitert, erreichte sie im "
|
||||||
|
"<strong>2. Jahrhundert</strong> ihren Höhepunkt, ehe sie "
|
||||||
|
"in den Wirren der Spätantike aufgegeben wurde.")
|
||||||
|
+ h("Kurz gefasst")
|
||||||
|
+ ul("<strong>Ausgrabungen:</strong> 1975 und 1976",
|
||||||
|
"<strong>Fassade:</strong> über 100 Meter",
|
||||||
|
"<strong>Höhepunkt:</strong> 2. Jahrhundert n. Chr.",
|
||||||
|
"<strong>Lage:</strong> Sauertal, Echternach")
|
||||||
|
),
|
||||||
|
"EN": (
|
||||||
|
p("Excavated in 1975 and 1976, the Echternach villa is one of the "
|
||||||
|
"largest Gallo-Roman estates north of the Alps: over <strong>a "
|
||||||
|
"hundred metres of frontage</strong>, porticoes, inner courtyards "
|
||||||
|
"and pools.")
|
||||||
|
+ h("An estate on a rare scale")
|
||||||
|
+ p("The residential building was arranged around a central court, "
|
||||||
|
"extended by gardens sloping down towards the Sûre. It was as "
|
||||||
|
"much a prestige residence as a working farm.")
|
||||||
|
+ ul("A <strong>monumental frontage</strong> facing the valley, "
|
||||||
|
"set out along a long covered gallery.",
|
||||||
|
"<strong>Dozens of rooms</strong> around the court: reception, "
|
||||||
|
"private apartments, service quarters.",
|
||||||
|
"<strong>Gardens and ornamental pools</strong>, staged along "
|
||||||
|
"the axis of the villa.",
|
||||||
|
"A <strong>farming quarter</strong> that sustained the estate "
|
||||||
|
"and supplied the region.")
|
||||||
|
+ h("Living beside the Sûre")
|
||||||
|
+ p("Floors and walls carried <strong>painted plaster</strong> and "
|
||||||
|
"careful finishes, and the estate had baths of its own. Keeping "
|
||||||
|
"such a place going called for considerable wealth — and a "
|
||||||
|
"taste for showing it.")
|
||||||
|
+ quote("Did you know? The villa was not built in one go: enlarged "
|
||||||
|
"over generations, it reached its height in the "
|
||||||
|
"<strong>second century</strong> before being left behind "
|
||||||
|
"in the upheavals of late antiquity.")
|
||||||
|
+ h("In brief")
|
||||||
|
+ ul("<strong>Excavations:</strong> 1975 and 1976",
|
||||||
|
"<strong>Frontage:</strong> over 100 metres",
|
||||||
|
"<strong>Height:</strong> second century AD",
|
||||||
|
"<strong>Setting:</strong> Sûre valley, Echternach")
|
||||||
|
),
|
||||||
|
"NL": (
|
||||||
|
p("De in 1975 en 1976 opgegraven villa van Echternach is een van "
|
||||||
|
"de grootste Gallo-Romeinse domeinen ten noorden van de Alpen: "
|
||||||
|
"meer dan <strong>honderd meter gevel</strong>, zuilengangen, "
|
||||||
|
"binnenplaatsen en bekkens.")
|
||||||
|
+ h("Een domein van zeldzame omvang")
|
||||||
|
+ p("Het woongebouw lag rond een centrale binnenplaats, verlengd "
|
||||||
|
"door tuinen die afliepen naar de Sûre. Het geheel was even "
|
||||||
|
"goed pronkresidentie als landbouwbedrijf.")
|
||||||
|
+ ul("Een <strong>monumentale gevel</strong> naar het dal, geleed "
|
||||||
|
"door een lange overdekte galerij.",
|
||||||
|
"<strong>Tientallen vertrekken</strong> rond de binnenplaats: "
|
||||||
|
"ontvangst, privévertrekken, dienstruimten.",
|
||||||
|
"<strong>Tuinen en siervijvers</strong>, in de as van de villa "
|
||||||
|
"geënsceneerd.",
|
||||||
|
"Een <strong>landbouwgedeelte</strong> dat het domein droeg en "
|
||||||
|
"de streek bevoorraadde.")
|
||||||
|
+ h("Leven aan de Sûre")
|
||||||
|
+ p("Vloeren en muren kregen <strong>beschilderd pleisterwerk</strong> "
|
||||||
|
"en verzorgde afwerkingen; het domein had eigen thermen. Zo'n "
|
||||||
|
"geheel onderhouden vergde een aanzienlijk fortuin — en de lust "
|
||||||
|
"om het te tonen.")
|
||||||
|
+ quote("Wist u dat? De villa ontstond niet in één keer: over "
|
||||||
|
"generaties uitgebreid bereikte zij haar hoogtepunt in de "
|
||||||
|
"<strong>2e eeuw</strong>, voordat zij in de onrust van de "
|
||||||
|
"late oudheid werd verlaten.")
|
||||||
|
+ h("In het kort")
|
||||||
|
+ ul("<strong>Opgravingen:</strong> 1975 en 1976",
|
||||||
|
"<strong>Gevel:</strong> meer dan 100 meter",
|
||||||
|
"<strong>Hoogtepunt:</strong> 2e eeuw n.Chr.",
|
||||||
|
"<strong>Ligging:</strong> Sûre-vallei, Echternach")
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Thermes et hypocauste",
|
||||||
|
"beacon": 302,
|
||||||
|
"coords": ("49.80630", "6.40165"),
|
||||||
|
"title": {
|
||||||
|
"FR": "Les thermes et le chauffage par le sol",
|
||||||
|
"DE": "Thermen und Fußbodenheizung",
|
||||||
|
"EN": "The baths and underfloor heating",
|
||||||
|
"NL": "De thermen en vloerverwarming",
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"FR": "Aile ouest — vestiges en extérieur",
|
||||||
|
"DE": "Westflügel — Freigelände",
|
||||||
|
"EN": "West wing — outdoor remains",
|
||||||
|
"NL": "Westvleugel — buitenresten",
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"FR": p(
|
||||||
|
"L'hypocauste chauffait les sols en faisant circuler l'air chaud "
|
||||||
|
"d'un foyer sous des pilettes de terre cuite. Les vestiges visibles "
|
||||||
|
"en montrent encore l'agencement.",
|
||||||
|
"Le domaine disposait de ses propres thermes : salle froide, salle "
|
||||||
|
"tiède, salle chaude — le confort d'une villa de premier rang."),
|
||||||
|
"DE": p(
|
||||||
|
"Das Hypokaustum beheizte die Böden, indem warme Luft aus einem "
|
||||||
|
"Ofen unter Pfeilern aus Ziegel zirkulierte. Die sichtbaren Reste "
|
||||||
|
"zeigen die Anordnung noch heute.",
|
||||||
|
"Das Anwesen besaß eigene Thermen: Kaltbad, Laubad, Warmbad — der "
|
||||||
|
"Komfort einer erstrangigen Villa."),
|
||||||
|
"EN": p(
|
||||||
|
"The hypocaust heated the floors by circulating hot air from a "
|
||||||
|
"furnace beneath brick pillars. The visible remains still show the "
|
||||||
|
"layout.",
|
||||||
|
"The estate had its own baths: cold room, warm room, hot room — the "
|
||||||
|
"comfort of a first-rank villa."),
|
||||||
|
"NL": p(
|
||||||
|
"Het hypocaustum verwarmde de vloeren door hete lucht uit een "
|
||||||
|
"stookplaats onder bakstenen pijlers te laten circuleren. De "
|
||||||
|
"zichtbare resten tonen de opzet nog.",
|
||||||
|
"Het domein had eigen thermen: koude, lauwe en warme zaal — het "
|
||||||
|
"comfort van een villa van de eerste rang."),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
MENUS = {
|
||||||
|
"feschmaart": {
|
||||||
|
"label": "Collections",
|
||||||
|
"title": {"FR": "Les collections", "DE": "Die Sammlungen",
|
||||||
|
"EN": "The collections", "NL": "De collecties"},
|
||||||
|
"description": {
|
||||||
|
"FR": "Archéologie, cabinet des médailles, arts décoratifs, beaux-arts",
|
||||||
|
"DE": "Archäologie, Münzkabinett, Kunstgewerbe, bildende Kunst",
|
||||||
|
"EN": "Archaeology, coin cabinet, decorative arts, fine arts",
|
||||||
|
"NL": "Archeologie, penningkabinet, sierkunst, schone kunsten"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
QUIZZES = {
|
||||||
|
"feschmaart": {
|
||||||
|
"label": "Quiz archéologie",
|
||||||
|
"title": {"FR": "Connaissez-vous l'archéologie luxembourgeoise ?",
|
||||||
|
"DE": "Kennen Sie die Luxemburger Archäologie?",
|
||||||
|
"EN": "How well do you know Luxembourg archaeology?",
|
||||||
|
"NL": "Kent u de Luxemburgse archeologie?"},
|
||||||
|
"questions": [
|
||||||
|
{
|
||||||
|
"q": {"FR": "En quelle année la mosaïque de Vichten a-t-elle été découverte ?",
|
||||||
|
"DE": "In welchem Jahr wurde das Mosaik von Vichten entdeckt?",
|
||||||
|
"EN": "In which year was the Vichten mosaic discovered?",
|
||||||
|
"NL": "In welk jaar werd het mozaïek van Vichten ontdekt?"},
|
||||||
|
"answers": [
|
||||||
|
({"FR": "1995", "DE": "1995", "EN": "1995", "NL": "1995"}, True),
|
||||||
|
({"FR": "1963", "DE": "1963", "EN": "1963", "NL": "1963"}, False),
|
||||||
|
({"FR": "1978", "DE": "1978", "EN": "1978", "NL": "1978"}, False),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": {"FR": "Qui figure au centre de la mosaïque de Vichten ?",
|
||||||
|
"DE": "Wer steht im Zentrum des Mosaiks von Vichten?",
|
||||||
|
"EN": "Who is at the centre of the Vichten mosaic?",
|
||||||
|
"NL": "Wie staat in het midden van het mozaïek van Vichten?"},
|
||||||
|
"answers": [
|
||||||
|
({"FR": "Homère", "DE": "Homer", "EN": "Homer", "NL": "Homerus"}, True),
|
||||||
|
({"FR": "Jupiter", "DE": "Jupiter", "EN": "Jupiter", "NL": "Jupiter"}, False),
|
||||||
|
({"FR": "Orphée", "DE": "Orpheus", "EN": "Orpheus", "NL": "Orpheus"}, False),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"q": {"FR": "Que signifie « Dräi Eechelen » ?",
|
||||||
|
"DE": "Was bedeutet „Dräi Eechelen“?",
|
||||||
|
"EN": "What does “Dräi Eechelen” mean?",
|
||||||
|
"NL": "Wat betekent „Dräi Eechelen“?"},
|
||||||
|
"answers": [
|
||||||
|
({"FR": "Trois glands", "DE": "Drei Eicheln",
|
||||||
|
"EN": "Three acorns", "NL": "Drie eikels"}, True),
|
||||||
|
({"FR": "Trois tours", "DE": "Drei Türme",
|
||||||
|
"EN": "Three towers", "NL": "Drie torens"}, False),
|
||||||
|
({"FR": "Trois portes", "DE": "Drei Tore",
|
||||||
|
"EN": "Three gates", "NL": "Drie poorten"}, False),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Points d'intérêt de la carte extérieure d'Echternach, sur l'emprise réelle du site.
|
||||||
|
MAP_POINTS = [
|
||||||
|
{
|
||||||
|
"title": {"FR": "Entrée du site", "DE": "Eingang", "EN": "Site entrance",
|
||||||
|
"NL": "Ingang"},
|
||||||
|
"description": {"FR": "Accueil, billetterie et départ du parcours.",
|
||||||
|
"DE": "Empfang, Kasse und Start des Rundgangs.",
|
||||||
|
"EN": "Reception, ticketing and start of the trail.",
|
||||||
|
"NL": "Onthaal, ticketing en start van de route."},
|
||||||
|
"lat": 49.80680, "lon": 6.40255, "cat": 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": {"FR": "Cour intérieure", "DE": "Innenhof", "EN": "Inner courtyard",
|
||||||
|
"NL": "Binnenplaats"},
|
||||||
|
"description": {"FR": "Le cœur du bâtiment résidentiel et ses portiques.",
|
||||||
|
"DE": "Das Herz des Wohngebäudes und seine Portiken.",
|
||||||
|
"EN": "The heart of the residential building and its porticoes.",
|
||||||
|
"NL": "Het hart van het woongebouw en zijn zuilengangen."},
|
||||||
|
"lat": 49.80655, "lon": 6.40210, "cat": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": {"FR": "Les thermes", "DE": "Die Thermen", "EN": "The baths",
|
||||||
|
"NL": "De thermen"},
|
||||||
|
"description": {"FR": "Salles froide, tiède et chaude ; hypocauste visible.",
|
||||||
|
"DE": "Kalt-, Lau- und Warmbad; sichtbares Hypokaustum.",
|
||||||
|
"EN": "Cold, warm and hot rooms; visible hypocaust.",
|
||||||
|
"NL": "Koude, lauwe en warme zalen; zichtbaar hypocaustum."},
|
||||||
|
"lat": 49.80630, "lon": 6.40165, "cat": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": {"FR": "Les bassins", "DE": "Die Becken", "EN": "The pools",
|
||||||
|
"NL": "De bekkens"},
|
||||||
|
"description": {"FR": "Bassins d'agrément des jardins en terrasse.",
|
||||||
|
"DE": "Zierbecken der Terrassengärten.",
|
||||||
|
"EN": "Ornamental pools of the terraced gardens.",
|
||||||
|
"NL": "Sierbekkens van de terrastuinen."},
|
||||||
|
"lat": 49.80610, "lon": 6.40230, "cat": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": {"FR": "Point de vue sur la Sûre", "DE": "Aussichtspunkt auf die Sauer",
|
||||||
|
"EN": "Viewpoint over the Sûre", "NL": "Uitzicht over de Sûre"},
|
||||||
|
"description": {"FR": "Le domaine dominait la vallée et sa voie de passage.",
|
||||||
|
"DE": "Das Anwesen beherrschte das Tal und seinen Verkehrsweg.",
|
||||||
|
"EN": "The estate overlooked the valley and its through route.",
|
||||||
|
"NL": "Het domein beheerste de vallei en haar doorgangsweg."},
|
||||||
|
"lat": 49.80590, "lon": 6.40170, "cat": 3,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
MAP_CATEGORIES = [
|
||||||
|
{"id": 1, "label": {"FR": "Accueil", "DE": "Empfang", "EN": "Reception",
|
||||||
|
"NL": "Onthaal"}, "color": "0xFF1F3A5F"},
|
||||||
|
{"id": 2, "label": {"FR": "Vestiges", "DE": "Überreste", "EN": "Remains",
|
||||||
|
"NL": "Overblijfselen"}, "color": "0xFF7A4A2F"},
|
||||||
|
{"id": 3, "label": {"FR": "Panorama", "DE": "Panorama", "EN": "Viewpoint",
|
||||||
|
"NL": "Panorama"}, "color": "0xFF4A5C3D"},
|
||||||
|
]
|
||||||
|
|
||||||
|
AGENDA_EVENTS = [
|
||||||
|
{"title": {"FR": "Visite guidée : la mosaïque de Vichten",
|
||||||
|
"DE": "Führung: das Mosaik von Vichten",
|
||||||
|
"EN": "Guided tour: the Vichten mosaic",
|
||||||
|
"NL": "Rondleiding: het mozaïek van Vichten"},
|
||||||
|
"description": {"FR": "Une heure avec un médiateur, autour de la pièce maîtresse "
|
||||||
|
"des salles d'archéologie.",
|
||||||
|
"DE": "Eine Stunde mit einem Vermittler rund um das Hauptwerk der "
|
||||||
|
"Archäologiesäle.",
|
||||||
|
"EN": "An hour with a guide around the centrepiece of the "
|
||||||
|
"archaeology halls.",
|
||||||
|
"NL": "Een uur met een gids rond het pronkstuk van de "
|
||||||
|
"archeologiezalen."},
|
||||||
|
"day_offset": 6, "hour": 15},
|
||||||
|
{"title": {"FR": "Atelier famille : fouiller comme un archéologue",
|
||||||
|
"DE": "Familienworkshop: graben wie ein Archäologe",
|
||||||
|
"EN": "Family workshop: dig like an archaeologist",
|
||||||
|
"NL": "Gezinsworkshop: graven als een archeoloog"},
|
||||||
|
"description": {"FR": "Dès 8 ans. Sur inscription.",
|
||||||
|
"DE": "Ab 8 Jahren. Anmeldung erforderlich.",
|
||||||
|
"EN": "Ages 8 and up. Registration required.",
|
||||||
|
"NL": "Vanaf 8 jaar. Inschrijving vereist."},
|
||||||
|
"day_offset": 13, "hour": 14},
|
||||||
|
{"title": {"FR": "Nocturne aux chandelles au Fort Thüngen",
|
||||||
|
"DE": "Abendöffnung bei Kerzenschein im Fort Thüngen",
|
||||||
|
"EN": "Candlelit evening at Fort Thüngen",
|
||||||
|
"NL": "Avondopening bij kaarslicht in Fort Thüngen"},
|
||||||
|
"description": {"FR": "Le fort ouvert jusqu'à 22 h, parcours éclairé.",
|
||||||
|
"DE": "Das Fort bis 22 Uhr geöffnet, beleuchteter Rundgang.",
|
||||||
|
"EN": "The fort open until 10 pm, illuminated route.",
|
||||||
|
"NL": "Het fort open tot 22 uur, verlichte route."},
|
||||||
|
"day_offset": 20, "hour": 19},
|
||||||
|
{"title": {"FR": "Conférence : fouiller Echternach, cinquante ans après",
|
||||||
|
"DE": "Vortrag: Echternach ausgraben, fünfzig Jahre danach",
|
||||||
|
"EN": "Lecture: excavating Echternach, fifty years on",
|
||||||
|
"NL": "Lezing: Echternach opgraven, vijftig jaar later"},
|
||||||
|
"description": {"FR": "Retour sur les campagnes de 1975-1976 et sur ce que les "
|
||||||
|
"méthodes actuelles y liraient.",
|
||||||
|
"DE": "Rückblick auf die Kampagnen 1975-1976 und darauf, was "
|
||||||
|
"heutige Methoden dort lesen würden.",
|
||||||
|
"EN": "Revisiting the 1975-1976 campaigns and what today's methods "
|
||||||
|
"would read there.",
|
||||||
|
"NL": "Terugblik op de campagnes van 1975-1976 en wat de huidige "
|
||||||
|
"methodes daar zouden lezen."},
|
||||||
|
"day_offset": 27, "hour": 18},
|
||||||
|
]
|
||||||
363
outputs/mnaha/seed/gen_images.py
Normal file
@ -0,0 +1,363 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Visuels d'illustration pour l'instance de démonstration.
|
||||||
|
|
||||||
|
Compositions abstraites, générées : aucune photographie de collection n'est
|
||||||
|
utilisée, les droits n'appartiennent pas au projet. Chaque motif évoque son sujet
|
||||||
|
sans le représenter — trame de tesselles, géométrie de bastion, pilettes
|
||||||
|
d'hypocauste — dans la palette du site auquel il appartient.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from PIL import Image, ImageDraw, ImageFilter
|
||||||
|
|
||||||
|
OUT = Path(__file__).resolve().parent / "images"
|
||||||
|
|
||||||
|
PALETTES = {
|
||||||
|
"feschmaart": {"deep": (18, 32, 54), "mid": (31, 58, 95), "warm": (176, 141, 87),
|
||||||
|
"pale": (214, 199, 176)},
|
||||||
|
"draieechelen": {"deep": (28, 36, 24), "mid": (74, 92, 61), "warm": (156, 122, 74),
|
||||||
|
"pale": (206, 199, 176)},
|
||||||
|
"echternach": {"deep": (48, 28, 18), "mid": (122, 74, 47), "warm": (192, 161, 107),
|
||||||
|
"pale": (222, 206, 180)},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def blend(a, b, t):
|
||||||
|
return tuple(int(round(a[i] + (b[i] - a[i]) * t)) for i in range(3))
|
||||||
|
|
||||||
|
|
||||||
|
def background(size, palette, tilt=0.0):
|
||||||
|
"""Dégradé diagonal doux, du plus sombre en haut à gauche."""
|
||||||
|
w, h = size
|
||||||
|
base = Image.new("RGB", size)
|
||||||
|
px = base.load()
|
||||||
|
for y in range(h):
|
||||||
|
for x in range(0, w, 4):
|
||||||
|
t = (x / w * (0.35 + tilt) + y / h * 0.65)
|
||||||
|
colour = blend(palette["deep"], palette["mid"], min(1.0, t))
|
||||||
|
for dx in range(4):
|
||||||
|
if x + dx < w:
|
||||||
|
px[x + dx, y] = colour
|
||||||
|
return base
|
||||||
|
|
||||||
|
|
||||||
|
def grain(img, amount=7, seed=0):
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
noise = Image.new("L", img.size)
|
||||||
|
noise.putdata([128 + rnd.randint(-amount, amount)
|
||||||
|
for _ in range(img.size[0] * img.size[1])])
|
||||||
|
noise = noise.filter(ImageFilter.GaussianBlur(0.4))
|
||||||
|
return Image.blend(img, Image.merge("RGB", (noise, noise, noise)), 0.06)
|
||||||
|
|
||||||
|
|
||||||
|
def vignette(img, strength=0.45):
|
||||||
|
w, h = img.size
|
||||||
|
mask = Image.new("L", (w, h), 0)
|
||||||
|
d = ImageDraw.Draw(mask)
|
||||||
|
d.ellipse([-w * 0.25, -h * 0.35, w * 1.25, h * 1.35], fill=255)
|
||||||
|
mask = mask.filter(ImageFilter.GaussianBlur(min(w, h) * 0.18))
|
||||||
|
dark = Image.new("RGB", (w, h), (0, 0, 0))
|
||||||
|
return Image.composite(img, Image.blend(img, dark, strength), mask)
|
||||||
|
|
||||||
|
|
||||||
|
def layer(size):
|
||||||
|
return Image.new("RGBA", size, (0, 0, 0, 0))
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------- motifs
|
||||||
|
|
||||||
|
def tesserae(size, palette, seed):
|
||||||
|
"""Trame de tesselles, pour la mosaïque."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
cell = max(14, w // 46)
|
||||||
|
for y in range(0, h + cell, cell):
|
||||||
|
for x in range(0, w + cell, cell):
|
||||||
|
if rnd.random() < 0.14:
|
||||||
|
continue
|
||||||
|
jx, jy = rnd.randint(-2, 2), rnd.randint(-2, 2)
|
||||||
|
t = rnd.random()
|
||||||
|
colour = blend(palette["warm"], palette["pale"], t)
|
||||||
|
radial = math.hypot(x - w * 0.5, y - h * 0.5) / (w * 0.6)
|
||||||
|
alpha = int(max(0, 150 * (1.05 - radial)) * (0.45 + rnd.random() * 0.55))
|
||||||
|
d.rectangle([x + jx, y + jy, x + jx + cell - 3, y + jy + cell - 3],
|
||||||
|
fill=colour + (alpha,))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def bastion(size, palette, seed):
|
||||||
|
"""Tracé bastionné : enceintes concentriques, glacis hachuré, lignes de tir."""
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
cx, cy = w * 0.5, h * 0.52
|
||||||
|
|
||||||
|
for i in range(0, int(w * 1.6), max(9, w // 90)):
|
||||||
|
d.line([(i - w * 0.3, h), (i, 0)], fill=palette["warm"] + (16,), width=1)
|
||||||
|
|
||||||
|
for ring in range(15):
|
||||||
|
radius = min(w, h) * (0.05 + ring * 0.031)
|
||||||
|
points = []
|
||||||
|
spikes = 6
|
||||||
|
for i in range(spikes * 2):
|
||||||
|
angle = math.pi * i / spikes + ring * 0.05
|
||||||
|
r = radius * (1.0 if i % 2 == 0 else 0.74)
|
||||||
|
points.append((cx + r * math.cos(angle), cy + r * math.sin(angle) * 0.86))
|
||||||
|
colour = blend(palette["warm"], palette["pale"], ring / 15)
|
||||||
|
d.line(points + [points[0]], fill=colour + (max(34, int(130 - ring * 6)),),
|
||||||
|
width=max(2, int(w * 0.0028)))
|
||||||
|
if ring % 4 == 0:
|
||||||
|
d.polygon(points, fill=colour + (10,))
|
||||||
|
|
||||||
|
for i in range(12):
|
||||||
|
angle = math.tau * i / 12
|
||||||
|
d.line([cx + min(w, h) * 0.07 * math.cos(angle),
|
||||||
|
cy + min(w, h) * 0.07 * math.sin(angle) * 0.86,
|
||||||
|
cx + w * 0.75 * math.cos(angle),
|
||||||
|
cy + w * 0.75 * math.sin(angle) * 0.86],
|
||||||
|
fill=palette["pale"] + (30,), width=1)
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def pillars(size, palette, seed):
|
||||||
|
"""Champ de pilettes en fuite : l'hypocauste sous le plancher."""
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
rows = 9
|
||||||
|
for r in range(rows):
|
||||||
|
depth = (r + 1) / rows
|
||||||
|
y = h * (0.16 + 0.86 * depth ** 1.7)
|
||||||
|
pw = w * 0.012 + w * 0.030 * depth
|
||||||
|
ph = h * 0.035 + h * 0.10 * depth
|
||||||
|
gap = pw * 2.35
|
||||||
|
colour = blend(palette["warm"], palette["pale"], 0.25 + depth * 0.5)
|
||||||
|
alpha = int(12 + depth * 62)
|
||||||
|
x = (r % 2) * gap * 0.5 - gap * 0.5
|
||||||
|
while x < w + gap:
|
||||||
|
d.rectangle([x, y - ph, x + pw, y], fill=colour + (alpha,))
|
||||||
|
d.rectangle([x - pw * 0.22, y - ph - ph * 0.10,
|
||||||
|
x + pw * 1.22, y - ph + ph * 0.02],
|
||||||
|
fill=colour + (min(255, alpha + 26),))
|
||||||
|
x += gap
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def strata(size, palette, seed):
|
||||||
|
"""Couches sédimentaires : la fouille, la stratigraphie."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
y = h * 0.06
|
||||||
|
while y < h:
|
||||||
|
thickness = rnd.uniform(h * 0.018, h * 0.055)
|
||||||
|
t = y / h
|
||||||
|
colour = blend(palette["warm"], palette["pale"], rnd.uniform(0.1, 0.9))
|
||||||
|
alpha = int(18 + 46 * t)
|
||||||
|
points = [(0, y)]
|
||||||
|
x = 0
|
||||||
|
while x < w:
|
||||||
|
x += w / 18
|
||||||
|
points.append((x, y + rnd.uniform(-h * 0.014, h * 0.014)))
|
||||||
|
points += [(w, y + thickness), (0, y + thickness)]
|
||||||
|
d.polygon(points, fill=colour + (alpha,))
|
||||||
|
# Inclusions : éclats et tessons pris dans la couche.
|
||||||
|
for _ in range(int(thickness / h * 90)):
|
||||||
|
ix = rnd.uniform(0, w)
|
||||||
|
iy = rnd.uniform(y + thickness * 0.15, y + thickness * 0.85)
|
||||||
|
ir = rnd.uniform(w * 0.002, w * 0.006)
|
||||||
|
d.ellipse([ix - ir, iy - ir * 0.6, ix + ir, iy + ir * 0.6],
|
||||||
|
fill=palette["pale"] + (int(alpha * 0.9),))
|
||||||
|
y += thickness + rnd.uniform(h * 0.002, h * 0.012)
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def arcs(size, palette, seed):
|
||||||
|
"""Portique : arcades en enfilade sur stylobate."""
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
bays = 5
|
||||||
|
span = w / bays
|
||||||
|
for register, (ground, bays_here) in enumerate([(h * 0.52, 7), (h * 0.94, 5)]):
|
||||||
|
span = w / bays_here
|
||||||
|
for b in range(bays_here + 1):
|
||||||
|
cx = span * b
|
||||||
|
for k in range(4):
|
||||||
|
r = span * (0.50 - k * 0.045)
|
||||||
|
top = ground - r * 1.15
|
||||||
|
colour = blend(palette["pale"], palette["warm"], k / 4)
|
||||||
|
d.arc([cx - r, top - r * 0.35, cx + r, top + r * 1.5], 180, 360,
|
||||||
|
fill=colour + (int(120 - k * 20),), width=max(2, int(w * 0.0028)))
|
||||||
|
cw = span * 0.075
|
||||||
|
d.rectangle([cx - cw, ground - span * 0.62, cx + cw, ground],
|
||||||
|
fill=palette["warm"] + (44,))
|
||||||
|
d.line([(0, ground), (w, ground)], fill=palette["warm"] + (70,),
|
||||||
|
width=max(2, int(w * 0.0025)))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def medallions(size, palette, seed):
|
||||||
|
"""Monnaies : disques serrés, frappés, qui se recouvrent."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
for _ in range(78):
|
||||||
|
r = rnd.uniform(w * 0.025, w * 0.085)
|
||||||
|
cx = rnd.uniform(-w * 0.02, w * 1.02)
|
||||||
|
cy = rnd.uniform(-h * 0.02, h * 1.02)
|
||||||
|
colour = blend(palette["warm"], palette["pale"], rnd.random())
|
||||||
|
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=colour + (16,),
|
||||||
|
outline=colour + (110,), width=max(2, int(w * 0.0022)))
|
||||||
|
d.ellipse([cx - r * 0.66, cy - r * 0.66, cx + r * 0.66, cy + r * 0.66],
|
||||||
|
outline=colour + (62,), width=max(1, int(w * 0.0014)))
|
||||||
|
beads = 18
|
||||||
|
for i in range(beads):
|
||||||
|
a = math.tau * i / beads
|
||||||
|
bx, by = cx + r * 0.84 * math.cos(a), cy + r * 0.84 * math.sin(a)
|
||||||
|
d.ellipse([bx - 1.4, by - 1.4, bx + 1.4, by + 1.4], fill=colour + (70,))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def deco(size, palette, seed):
|
||||||
|
"""Éventails et arcs : le vocabulaire Art déco, en trame pleine."""
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
for cx, cy, scale in [(w * 0.16, h * 1.02, 1.0), (w * 0.5, h * 1.02, 1.25),
|
||||||
|
(w * 0.84, h * 1.02, 1.0), (w * 0.33, h * -0.02, 0.7),
|
||||||
|
(w * 0.67, h * -0.02, 0.7)]:
|
||||||
|
upward = cy < h * 0.5
|
||||||
|
for i in range(19):
|
||||||
|
angle = (0 if upward else math.pi) + i * math.pi / 18
|
||||||
|
length = min(w, h) * scale * (0.62 if i % 2 == 0 else 0.48)
|
||||||
|
d.line([cx, cy, cx + length * math.cos(angle),
|
||||||
|
cy + length * math.sin(angle)],
|
||||||
|
fill=blend(palette["pale"], palette["warm"], i / 19)
|
||||||
|
+ (88 if i % 2 else 120,),
|
||||||
|
width=max(2, int(w * 0.0026)))
|
||||||
|
for ring in range(6):
|
||||||
|
r = min(w, h) * scale * (0.11 + ring * 0.105)
|
||||||
|
d.arc([cx - r, cy - r, cx + r, cy + r],
|
||||||
|
0 if upward else 180, 180 if upward else 360,
|
||||||
|
fill=palette["warm"] + (48,), width=max(1, int(w * 0.0018)))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def contours(size, palette, seed):
|
||||||
|
"""Courbes de niveau : le site, le terrain, la vallée."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
for i in range(32):
|
||||||
|
points = []
|
||||||
|
base = h * (0.02 + i * 0.032)
|
||||||
|
phase = rnd.uniform(0, math.tau)
|
||||||
|
for x in range(0, w + 12, 12):
|
||||||
|
y = base + math.sin(x / w * math.tau * 1.4 + phase) * h * 0.055 \
|
||||||
|
+ math.sin(x / w * math.tau * 3.1 + phase) * h * 0.018
|
||||||
|
points.append((x, y))
|
||||||
|
colour = blend(palette["warm"], palette["pale"], i / 32)
|
||||||
|
d.line(points, fill=colour + (int(100 - i * 1.6),),
|
||||||
|
width=max(1, int(w * 0.0018)))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
def plan(size, palette, seed):
|
||||||
|
"""Trame orthogonale : le plan, la maquette, le relevé."""
|
||||||
|
rnd = random.Random(seed)
|
||||||
|
lay = layer(size)
|
||||||
|
d = ImageDraw.Draw(lay)
|
||||||
|
w, h = size
|
||||||
|
step = w // 30
|
||||||
|
for x in range(0, w, step):
|
||||||
|
d.line([(x, 0), (x, h)], fill=palette["warm"] + (34,), width=1)
|
||||||
|
for y in range(0, h, step):
|
||||||
|
d.line([(0, y), (w, y)], fill=palette["warm"] + (34,), width=1)
|
||||||
|
for _ in range(22):
|
||||||
|
x0 = rnd.randrange(0, w - step * 5, step)
|
||||||
|
y0 = rnd.randrange(0, h - step * 4, step)
|
||||||
|
x1 = x0 + step * rnd.randint(2, 7)
|
||||||
|
y1 = y0 + step * rnd.randint(2, 6)
|
||||||
|
colour = blend(palette["pale"], palette["warm"], rnd.random())
|
||||||
|
d.rectangle([x0, y0, x1, y1], fill=colour + (14,), outline=colour + (120,),
|
||||||
|
width=max(2, int(w * 0.0024)))
|
||||||
|
return lay
|
||||||
|
|
||||||
|
|
||||||
|
MOTIFS = {
|
||||||
|
"tesserae": tesserae, "bastion": bastion, "pillars": pillars, "strata": strata,
|
||||||
|
"arcs": arcs, "medallions": medallions, "deco": deco, "contours": contours,
|
||||||
|
"plan": plan,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def compose(name, site, motif, size=(1600, 1000), seed=0, tilt=0.0):
|
||||||
|
palette = PALETTES[site]
|
||||||
|
img = background(size, palette, tilt).convert("RGBA")
|
||||||
|
img = Image.alpha_composite(img, MOTIFS[motif](size, palette, seed))
|
||||||
|
img = vignette(img.convert("RGB"))
|
||||||
|
img = grain(img, seed=seed)
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = OUT / f"{name}.png"
|
||||||
|
img.save(path, "PNG", optimize=True)
|
||||||
|
print(f" {path.name:34} {size[0]}x{size[1]} {path.stat().st_size // 1024} Ko")
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
# Cinq motifs retenus : ceux qui tiennent à taille de vignette. `bastion`,
|
||||||
|
# `pillars`, `arcs` et `strata` restent définis mais ne sont pas utilisés — ils
|
||||||
|
# se lisent comme un aplat vide ou comme une frise trop littérale une fois
|
||||||
|
# réduits. Un plan orthogonal évoque d'ailleurs mieux une fouille, un fort ou
|
||||||
|
# une villa que le motif inventé qu'ils remplaçaient.
|
||||||
|
#
|
||||||
|
# nom de fichier -> (site, motif, taille, graine)
|
||||||
|
IMAGES = [
|
||||||
|
# sites (tuiles d'accueil)
|
||||||
|
("site-feschmaart", "feschmaart", "tesserae", (1600, 1000), 11),
|
||||||
|
("site-draieechelen", "draieechelen", "plan", (1600, 1000), 12),
|
||||||
|
("site-echternach", "echternach", "contours", (1600, 1000), 13),
|
||||||
|
# habillage de l'application
|
||||||
|
("app-accueil", "feschmaart", "tesserae", (1200, 1600), 21),
|
||||||
|
("app-chargement", "feschmaart", "medallions", (1080, 1920), 22),
|
||||||
|
# articles du Fëschmaart
|
||||||
|
("art-vichten", "feschmaart", "tesserae", (1600, 1000), 31),
|
||||||
|
("art-goeblange", "feschmaart", "plan", (1600, 1000), 32),
|
||||||
|
("art-villeroy", "feschmaart", "deco", (1600, 1000), 33),
|
||||||
|
("art-medailles", "feschmaart", "medallions", (1600, 1000), 34),
|
||||||
|
# articles de Dräi Eechelen
|
||||||
|
("art-thungen", "draieechelen", "plan", (1600, 1000), 41),
|
||||||
|
("art-demantelement", "draieechelen", "contours", (1600, 1000), 42),
|
||||||
|
("art-maquettes", "draieechelen", "plan", (1600, 1000), 43),
|
||||||
|
# articles d'Echternach
|
||||||
|
("art-villa", "echternach", "plan", (1600, 1000), 51),
|
||||||
|
("art-thermes", "echternach", "tesserae", (1600, 1000), 52),
|
||||||
|
# parcours
|
||||||
|
("parcours-echternach", "echternach", "contours", (1600, 1000), 61),
|
||||||
|
("parcours-escape", "feschmaart", "tesserae", (1600, 1000), 62),
|
||||||
|
# points d'intérêt de la carte
|
||||||
|
("poi-entree", "echternach", "plan", (1200, 800), 71),
|
||||||
|
("poi-cour", "echternach", "plan", (1200, 800), 72),
|
||||||
|
("poi-thermes", "echternach", "tesserae", (1200, 800), 73),
|
||||||
|
("poi-bassins", "echternach", "contours", (1200, 800), 74),
|
||||||
|
("poi-panorama", "echternach", "contours", (1200, 800), 75),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"génération dans {OUT}")
|
||||||
|
for name, site, motif, size, seed in IMAGES:
|
||||||
|
compose(name, site, motif, size, seed, tilt=(seed % 5) * 0.04)
|
||||||
|
print(f"\n{len(IMAGES)} visuels générés")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
outputs/mnaha/seed/images/_apercu.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
outputs/mnaha/seed/images/_apercu2.png
Normal file
|
After Width: | Height: | Size: 719 KiB |
BIN
outputs/mnaha/seed/images/app-accueil.png
Normal file
|
After Width: | Height: | Size: 950 KiB |
BIN
outputs/mnaha/seed/images/app-chargement.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
outputs/mnaha/seed/images/art-demantelement.png
Normal file
|
After Width: | Height: | Size: 661 KiB |
BIN
outputs/mnaha/seed/images/art-goeblange.png
Normal file
|
After Width: | Height: | Size: 746 KiB |
BIN
outputs/mnaha/seed/images/art-maquettes.png
Normal file
|
After Width: | Height: | Size: 805 KiB |
BIN
outputs/mnaha/seed/images/art-medailles.png
Normal file
|
After Width: | Height: | Size: 833 KiB |
BIN
outputs/mnaha/seed/images/art-thermes.png
Normal file
|
After Width: | Height: | Size: 809 KiB |
BIN
outputs/mnaha/seed/images/art-thungen.png
Normal file
|
After Width: | Height: | Size: 789 KiB |
BIN
outputs/mnaha/seed/images/art-vichten.png
Normal file
|
After Width: | Height: | Size: 803 KiB |
BIN
outputs/mnaha/seed/images/art-villa.png
Normal file
|
After Width: | Height: | Size: 772 KiB |
BIN
outputs/mnaha/seed/images/art-villeroy.png
Normal file
|
After Width: | Height: | Size: 979 KiB |
BIN
outputs/mnaha/seed/images/parcours-echternach.png
Normal file
|
After Width: | Height: | Size: 646 KiB |
BIN
outputs/mnaha/seed/images/parcours-escape.png
Normal file
|
After Width: | Height: | Size: 800 KiB |
BIN
outputs/mnaha/seed/images/poi-bassins.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
outputs/mnaha/seed/images/poi-cour.png
Normal file
|
After Width: | Height: | Size: 481 KiB |