vr-app/docs/00-phase1-etat-des-lieux.md
Thomas Fransolet 0310d28b5e Canal VR Meta Quest : projet Unity, viewer web et documentation
Premier commit du cinquième front. Trois parties :

- `unity/MyInfoMateVR/` : le projet Unity (6000.0.83f1, URP, Meta XR SDK 205),
  un APK unique pour tous les clients. Menu flottant à sélection au regard,
  appairage, chargement de scène GLB, POI, cache de contenu, télémétrie.
- `unity-overlay/` : les mêmes scripts à recopier sur un projet Unity neuf, avec
  les pièges rencontrés consignés dans son README.
- `viewer/` : viewer et éditeur de scène web autonome (Vite, TypeScript,
  three.js), partagé avec les autres fronts.
- `docs/` : état des lieux, setup Unity, décisions d'architecture et plan
  d'exécution en 9 étapes.

La scène est décrite par un `scene.json` poussé par `adb push` : l'app le
préfère à celui embarqué dans l'APK.

Les binaires (GLB, textures de l'échantillon Sponza, DLL Meta XR) passent par
Git LFS dès ce premier commit — les y faire entrer après coup demanderait de
réécrire l'historique. Les artefacts régénérés par l'éditeur et par CMake
(`Library/`, `.utmp/`, Burst debug) sont ignorés.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 15:26:07 +02:00

25 KiB

Phase 1 — État des lieux du code existant

Relevé le 2026-09-03 dans le code, pas dans la doc. Chaque affirmation « existe / n'existe pas » a été vérifiée fichier par fichier sur manager-service, manager-app, visitapp-web, mymuseum-visitapp.

Ce document ne refait pas l'analyse de ../../DOCS/v2/vr-quest-unity-plan.md (canal VR : device, onglet flotte, pricing, arbitrage moteur) — il la complète sur ce que le nouveau périmètre ajoute : génération de mondes, manifeste de scène, personnages 3D, pipeline d'assets, preview web.


0. La conclusion, d'abord

Trois quarts de ce que le module VR demande ne sont pas codés — mais presque tout est déjà spécifié, dans le plan V2 (Studio, Personnages, TTS, 3D). Le module VR n'est pas un chantier isolé : c'est le premier gros consommateur de ce plan, et il en hérite gratuitement s'il se branche dessus au lieu de se construire à côté. La table de dépendances est en §9 / F1.

Brique demandée Existe en code Existe en spec
AppType.VR, Instance.IsVR, sous-menu, stats par canal §1
Un appareil non-tablette (Device.AppType) vr-quest-unity-plan.md lot V-1
Contrat de contenu pour un client non-Flutter (/export) §5
Jobs asynchrones (Hangfire) §6
Crédits, plafonds, coût estimé, journal studio-plan.md §3.5
Couche fournisseurs IA (→ World Labs Marble) studio-plan.md §3.6
Traçabilité IA, mention, droits au client studio-plan.md §3.11
Entité Persona studio-plan.md §3.8
Upload serveur dans le bucket studio-plan.md lot 0
ResourceType.Model3D / Panorama360 §2
Manifeste de scène versionné à concevoir — phase 2
Viewer 3D web (THREE.js) greenfield, §7
Zone de navigation, hotspots 3D, menu VR greenfield

➡️ Et l'inverse est vrai aussi : le module VR ne rallonge pas le backlog V2, il en réordonne le chemin critique. Trois chantiers qui pouvaient s'attendre (socle de stockage, personnages, TTS) deviennent bloquants d'un quatrième.


1. Types d'application (appType)

Ce qui existe

// manager-service/ManagerService/Data/ApplicationInstance.cs (fin de fichier)
public enum AppType { Mobile, Tablet, Web, VR, Voice }

VR vaut 3 et est persisté en base. Autour :

Brique Emplacement vérifié
Instance.IsVR Data/Instance.cs:35
ApplicationInstance unique par (InstanceId, AppType) Data/ApplicationInstance.cs — index unique composite
Sous-menu « VR » dans le back-office manager-app/lib/Screens/Main/main_screen.dart:65
L'écran derrière le sous-menu main_screen.dart:704Text("TODO vr")
Télémétrie par canal Data/VisitEvent.cs porte AppType — un event VR se stocke sans migration
Assistant IA par canal POST /api/Ai/chat prend un AppType et vérifie IsAssistant sur l'ApplicationInstance correspondante

Ce qui manque

  1. Device.AppType. Data/Device.cs n'a pas de type de canal, et Controllers/DeviceController.cs:155 résout l'ApplicationInstance en dur sur ai.AppType == AppType.Tablet. Un casque enregistré aujourd'hui atterrit dans l'onglet Kiosk.
  2. ApiKeyAppType ne connaît que { VisitApp, TabletApp, Other } (Data/ApiKeyAppType.cs, 3 lignes). Il faut VrApp en fin d'enum — persisté en int.

Ce que ça implique

Le canal existe déjà comme dimension. Ce qui n'existe pas, c'est le contenu VR : aujourd'hui une ApplicationInstance VR pointerait vers des Configuration composées des 13 types de section 2D. Le nouveau périmètre (mondes, personnages placés, hotspots 3D) est un type de contenu neuf. 👉 Question d'architecture n°1, traitée en §9 / F2.


2. Médiathèque

Modèle

// Data/Resource.cs
Resource { Id, Type(ResourceType, int), Label, InstanceId, Url, StoragePath, FileName,
           SizeBytes, Width?, Height?, DateCreation, DateUpdate,
           IncludeInAiKnowledge, AiIndexStatus, AiIndexMessage, AiIndexedAt, AiChunkCount }

enum ResourceType { Image, Video, ImageUrl, VideoUrl, Audio, PDF, JSON, JSONUrl,
                    Word, PowerPoint, Text }   // 0..10 — commentaire explicite : jamais au milieu

Les cinq faits qui décident du pipeline d'assets

  1. Aucune gestion de GLB, aucune gestion de 360. Ni Model3D, ni Panorama360. La carte kanban « Ressource 360° » (5-planifie/250-*) est encore en planifié ; Model3D est prévu au lot 11 du Studio.
  2. L'upload est fait par le navigateur, en direct vers Firebase. Le backend ne sait que supprimer : IResourceBlobService n'expose que IsConfigured et DeleteAsync (Services/ResourceBlobService.cs:32-36). ⚠️ Conséquence directe : un monde généré par Marble, qui arrive par un webhook côté serveur, n'a aujourd'hui aucun chemin pour entrer dans le bucket. C'est le lot 0 du Studio, prérequis dur.
  3. Un .glb déposé aujourd'hui produit une ressource cassée. Le switch d'upload (manager-app/lib/Screens/Resources/resources_screen.dart:400-431) n'a pas de default : l'extension inconnue laisse resourceDTO.type à null et le mimeType à chaîne vide.
  4. Pas de versionnement, pas de hash, pas de variantes, pas de CDN, pas de miniatures. Une ressource est un fichier et une URL à jeton. Le manifeste VR aura besoin d'un hash et d'un poids par assetSizeBytes existe, le hash non.
  5. Le quota stockage est un SUM(SizeBytes) par instance, contrôlé à l'upload avec un 413. Aucune notion d'enveloppe par add-on. Un monde Marble + textures pèse des centaines de Mo ; une vidéo 360 pèse des Go.

État du chantier médiathèque

La refonte V1 (facettes, index inverse « utilisée dans », panneau de détail) est codée, en attente de test : DOCS/kanban/cards/4-a-tester/100-mediatheque-codee-de-bout-en-bout*. Le service ResourceUsageService.cs existe côté serveur. Bon signe : le module VR arrive sur une médiathèque fraîchement remise à plat.


3. Personas

La bonne lecture : c'est une dépendance, pas une contradiction

« Pas de nouveau catalogue à produire. Réutilise les personas déjà définis dans le manager. »

Les personas sont définis — dans ../../DOCS/v2/studio-plan.md §3.8, arrêté le 2026-09-01. Ils ne sont simplement pas encore codés : c'est le lot 7 du Studio, en V2 (kanban 5-planifie/310-personnages-un-guide-adressable-n-narrateurs). Le module VR ne doit donc rien réinventer — il doit attendre ou tirer ce lot. Voir la table de dépendances en §9 / F1.

Ce qui existe en base aujourd'hui, c'est un guide au singulier, en colonnes plates sur l'Instance :

Data/Instance.cs
  GuideName             (l.46)
  GuidePersonaPrompt    (l.52)   le comportement pour le LLM
  GuideVoiceId          (l.55)   un nom de voix Gemini, en texte libre
  GuideFallbackMessages (l.77)   List<TranslationDTO>

Pas de table Personas, pas de PersonaView, pas de TtsVoice — vérifié : aucun résultat pour class Persona|GenerationJob|CreditLedger|VisualIdentity dans les .cs, et aucun DbSet correspondant dans Data/MyInfoMateDbContext.cs.

Deux codages en dur font foi aujourd'hui :

  • les 2 voix : manager-app/lib/Screens/GuideIa/guide_ia_screen.dart:19-20
  • l'intonation : mymuseum-visitapp/lib/constants.dart:27 (kGeminiTtsPrompt) — une constante de build, donc tous les personnages sonneraient pareil.

Ce qui est spécifié, et qui est exactement ce qu'il faut

../../DOCS/v2/studio-plan.md §3.8 (arrêté le 2026-09-01) définit l'entité Persona : un seul objet, trois facettes (VoiceId → il narre, SystemPrompt → il peut être guide, WakewordId → il est adressable), plus PersonaView (canon en jeu de vues : Portrait / Profil / PlanLarge / Dos) et une table TtsVoice. C'est le lot 7 du Studio — 0 % codé.

Ce qui manque pour porter un persona en 3D

Persona tel que spécifié est un personnage 2D : un jeu d'images de référence pour un modèle de génération d'images, plus une voix. Aucun champ d'incarnation 3D.

À ajouter Pourquoi
Persona.Model3DResourceId? le GLB du personnage
Persona.IdleAnimationClip? nom du clip dans le GLB, ou clip par défaut
Persona.ScaleMeters? un GLB de studio n'arrive jamais à l'échelle
Hauteur des yeux / ancrage du regard pour qu'il regarde le visiteur sans lip sync

⚠️ Et une question sans réponse dans le code : rien ne produit un GLB de personnage. Le lot 11 du Studio prévoit Meshy 6 / Tripo pour des objets isolés — pas des humanoïdes rigés avec un idle. Voir F8.


4. Modèle multilingue et audio

Multilingue

Un seul motif, partout : List<TranslationDTO> { language, value }, stocké en jsonb. 10 langues supportées (FR NL EN DE IT ES PL CN AR UK). LB est absent des 4 repos — bloquant pour le projet luxembourgeois, prévu au lot 0 du Studio.

Traduction assistée existante : POST /api/Ai/translate.

➡️ Le manifeste de scène doit porter ses textes dans ce format exact. Aucune raison d'en inventer un.

Audio / TTS

Le TTS n'existe pas côté serveur. Les seules occurrences de Tts/TTS dans les .cs sont Data/Instance.cs (le nom de voix) et DTOs/AiChatDTO.cs. La synthèse est faite par l'app cliente, en Dart (GeminiTtsEngine, gemini-2.5-flash-preview-tts).

Conséquences :

  • « Audio TTS pré-généré et validé » suppose ../../DOCS/v2/tts-pregenerated-plan.md, qui est planifié, pas codé (kanban 5-planifie/160-*) et attend lui aussi le lot 0.
  • Le pattern cible existe déjà : SectionArticle.ArticleAudioIds est un List<TranslationDTO> d'ids de Resource. Un hotspot VR doit copier ce pattern, pas en inventer un.
  • Chemin de repli sans dette : si le TTS n'est pas prêt, la V1 VR se contente d'audio uploadé à la main (.mp3, ResourceType.Audio, supporté de bout en bout aujourd'hui). Le champ ne change pas quand le TTS arrive — c'est le même id de ressource.

5. Publication, versionnement, récupération du contenu

Le contrat existant

GET /api/Configuration/{id}/export?language=fr
    [Authorize(Policy = AppReadAccess)]        ← ConfigurationController.cs:383
    → ExportConfigurationDTO  (configuration + sections + ressources, un seul appel)

Bonne nouvelle, et elle ferme une question ouverte du plan du 2026-08-31 : l'endpoint est décoré AppReadAccess, donc accessible par clé d'API d'app, pas seulement depuis le back-office authentifié. Unity peut l'appeler tel quel.

Les ressources sont collectées par section.GetReferencedResourceIds(language), implémenté sur les 13 sous-types. C'est aussi ce qui alimente le mode offline de mymuseum-visitapp.

Les trois trous, et ils comptent tous les trois

  1. Il n'y a pas d'état « publié ». Data/Configuration.cs n'a ni IsPublished, ni Status, ni Version — 17 colonnes, aucune de cycle de vie. Une modification dans le back-office est immédiatement visible par les clients. Le « appliquer au retour au menu, jamais à chaud » du cahier des charges n'a rien sur quoi s'appuyer. studio-plan.md §7 le dit sans détour : « Publié est dérivé, pas stocké ».
  2. Il n'y a pas de version incrémentée. Le seul signal est DateUpdate (par entité), et l'invalidation offline se fait par comparaison de dates. Un SceneManifest.version monotone est à créer, il n'existe nulle part.
  3. L'export est mono-langue (?language=). Un casque en borne change de langue à chaud et doit avoir les 10 langues en mémoire. Soit N appels, soit un mode « toutes langues » à ajouter.

Push de rafraîchissement

DeviceController.cs:303 publie déjà sur le topic MQTT player/{device.Id} quand la config d'un appareil change. Le mécanisme « le casque apprend qu'il y a du neuf » existe côté serveur ; il n'a jamais eu d'autre client que tablet-app.


6. Jobs asynchrones

Hangfire est installé et réellement utilisé — la meilleure surprise pour le pipeline Marble.

Fait
Hangfire.AspNetCore 1.8.17 + Hangfire.PostgreSql 1.20.10 ManagerService.csproj:26-27
Dashboard avec filtre d'autorisation Security/HangfireDashboardAuthorizationFilter.cs
Convention établie : un job prend un id, jamais un objet Services/IIngestionService.cs
Déclenchement automatique à l'écriture d'une entité Data/SectionIndexingInterceptor.cs (réindexation RAG)
Idempotence pensée (rejeu au redémarrage de conteneur) commentaire Data/MyInfoMateDbContext.cs:304
Jobs planifiés / annulables Controllers/NotificationController.cs:98,157,202HangfireJobId stocké, BackgroundJob.Delete
Retry maîtrisé plutôt que le défaut à 10 Services/IngestionService.cs:18

Webhooks entrants : un seul précédent, Controllers/StripeWebhookController.cs, avec vérification de signature. C'est le patron à copier pour le webhook World Labs.

Ce qui manque : aucune table de suivi de job métier (statut, progression, résultat) exposée au front. studio-plan.md §3.7 la spécifie (GenerationJob + polling front à 2 s, pas de SSE — et l'argument tient : aucun précédent SSE dans le backend).


7. Preview web et rendu 3D côté clients

Il n'y a pas une ligne de 3D dans l'écosystème. Vérifié :

  • visitapp-web/package.json : ni three, ni @react-three/*, ni model-viewer. Les seules dépendances de rendu sont leaflet et react-pdf.
  • La démo « villa d'Echternach » (visitapp-web/src/app/demo/villa-echternach/scene.ts) est de la géométrie procédurale rendue en Canvas 2D, sans aucune dépendance — son propre en-tête le dit : « pas à charger un vrai modèle glTF — celui-là viendra d'un studio 3D ».
  • manager-app est en Flutter Web : aucun viewer 3D natif utilisable. Le viewer THREE.js devra être une page web embarquée en iframe (HtmlElementView), pas un widget Flutter. À ne pas découvrir en cours de route.

➡️ Le viewer de preview est entièrement greenfield — et le fait qu'il doive être une app web autonome nourrie par une URL de manifeste le rend, par construction, réutilisable dans visitapp-web. Voir question Q2.


8. Quotas, facturation, organisation

Sujet Réalité
Organisation Instance — c'est le tenant. SubscriptionPlan n'est qu'un gabarit : l'Instance recopie localement ses valeurs (Data/Instance.cs:92-100 : StorageQuotaBytes, AiTokensPerMonth, HasStats, StatsHistoryDays, HasAdvancedStats)
Add-on Le patron existe : Instance.IsAssistant (Instance.cs:37). Un add-on = un booléen sur l'instance + un relèvement de quota. ~1 jour
Compteur d'usage IA AiTokensThisMonth + AiUsageMonthKey, reset par comparaison de clé, 429 au dépassement, incrément après l'appel
Journal ligne à ligne inexistant. Deux compteurs cumulés, aucune traçabilité par génération
Coût estimé avant lancement inexistant
Plafond dur configurable inexistant
Stripe StripeService, StripeWebhookController⚠️ 2 événements seulement (checkout.session.completed, invoice.payment_failed). Facturer un add-on automatiquement demande du travail ; ligne manuelle pour les pilotes
Audit AuditLog { EntityType, EntityId, Action, UserId, InstanceId, Timestamp, OldValues, NewValues } — utilisable tel quel pour « qui a lancé quelle génération »

⚠️ Le compteur actuel ne tient pas pour Marble. CheckQuota avant / incrément après suffit à un appel LLM bloquant de 2 s. Une génération de monde dure 5 min à 1 h : dix lancements parallèles passeraient les dix contrôles avant le premier débit. Il faut la réservation en deux temps (Reserve à l'enqueue / Charge à la complétion / Refund à l'échec) déjà spécifiée dans studio-plan.md §3.5. Ce n'est pas un raffinement : à 3 500 crédits (≈ 2,80 $) l'export mesh HQ et 1 500 le monde standard, c'est de l'argent réel et immédiat.


9. Points de friction

F1. Le module VR est un consommateur du plan V2 — la table de dépendances

Le point de séquencement, et il ne dit pas « ça n'existe pas » mais « ça existe en spec, en V2, et le module VR en dépend ». Référence de haut niveau : ../../DOCS/STATUS.md §5, qui énumère déjà la V2 (Studio, Personnages, TTS pré-généré, kiosk web, VR) et pose le même prérequis dur — « le backend ne sait pas écrire dans le bucket ».

Ce dont la VR a besoin Spec Kanban (DOCS/kanban/cards/5-planifie/) Codé
Écrire un fichier dans le bucket côté serveur studio-plan.md lot 0 300-socle-de-stockage-serveur-le-backend-ne-sait prérequis dur
Crédits, plafond, coût estimé, journal studio-plan.md §3.5, lot 1 280-module-studio-generation-d-images
IGenerationProvider + catalogue de modèles en base studio-plan.md §3.6, lot 3 280-*
Provenance IA / mention / droits au client studio-plan.md §3.11, lot 6 280-*
Persona (+ PersonaView, TtsVoice) studio-plan.md §3.8, lot 7 310-personnages-un-guide-adressable-n-narrateurs
ResourceType.Model3D + GLB en médiathèque studio-plan.md §5, lot 11 280-* (lot 11)
Audio TTS pré-généré multilingue tts-pregenerated-plan.md 160-tts-pre-genere-audioguide-multilingue
Ressource 360° (Panorama360) todo-features.md 250-ressource-360-images-panoramiques

⚠️ Le lot 0 est le goulot commun : le Studio, le TTS pré-généré et la génération de mondes Marble attendent tous la même chose — que IResourceBlobService sache écrire. ~1 jour.

Le module VR n'ajoute donc pas ces briques au backlog : il les rend urgentes. Trois chantiers V2 qui pouvaient s'attendre les uns les autres deviennent le chemin critique d'un quatrième.

⚠️ Nuancé par la décision D5 (02-decisions.md) : en V1, le monde et les personnages sont des GLB uploadés, pas générés. Aucune de ces dépendances n'est bloquante pour la V1 — la table ci-dessus décrit ce qui bloque la V1.5 (génération intégrée). C'est précisément ce que D5 achète : découpler la livraison de l'app Quest du backlog V2.

👉 World Labs Marble n'est pas un fournisseur à part : c'est une implémentation de plus de IGenerationProvider, avec un GenerationKind.World et un GenerationModel en base (ProviderKey = "worldlabs", ProviderModelId, CreditCost). Le câbler à côté du Studio recréerait crédits, jobs, webhooks et traçabilité en double — et contredirait la contrainte « ne jamais coder en dur les noms de modèles ni les fournisseurs », que le catalogue en base résout déjà.

Mais — et c'est ce qui sauve le planning — la chaîne technique Unity ne dépend de rien de tout ça. Générer un monde à la main sur le site de Marble, télécharger le GLB, le charger dans Unity, le voir dans le casque : zéro backend. Faisable cette semaine, et c'est l'étape 1 obligatoire.

F2. Deux modèles de contenu qui ne se sont pas encore rencontrés

Le plan du 2026-08-31 dit : « Unity appelle GET /api/configuration/{id}/export » — donc la VR rend les 13 types de section existants, en immersif. Le nouveau cahier des charges décrit un manifeste de scène : monde de fond, objets GLB, personas placés, hotspots, spawn, rayon de navigation.

Ce sont deux produits différents. Les faire cohabiter sans arbitrage donnerait deux pipelines de publication, deux caches offline, deux formats.

Option Ce que ça donne Coût
A — Type de section neuf SectionScene3D dans une Configuration existante Le manifeste devient une projection de l'export existant. Une seule publication, un seul offline, stats gratuites. Et la scène devient affichable en web/mobile — le « décorrélé du casque » sur lequel repose le pricing de l'add-on Le plus faible. Recommandé
B — Entité VrScene parallèle, manifeste autonome Modèle propre, taillé 3D, sans compromis avec le CMS 2D Publication, versionnement, offline, quotas, stats à refaire
C — Les deux Non

➡️ Arbitrage proposé : A, avec le manifeste généré à partir de la section, jamais saisi. Corollaire : GeoPoint devient l'objet POI unique — vr-quest-unity-plan.md §9 l'avait vu, il porte déjà Title, Description, ImageResource et surtout Contents (titre + description + Resource audio, multilingue). Il ne lui manque qu'un (x, y, z).

F3. Le versionnement du manifeste n'a aucune fondation

Pas d'état publié, pas de numéro de version, pas de hash d'asset (§5). Trois choses à créer — et elles ne sont pas propres à la VR : un Configuration.PublishedVersion profiterait aussi à l'offline de mymuseum-visitapp, qui invalide aujourd'hui à la date.

F4. Le serveur ne sait pas écrire dans son propre bucket

Répété parce que c'est le blocage le plus bête et le plus dur : le webhook Marble n'aura nulle part où poser le GLB qu'il télécharge. Lot 0 du Studio, ~1 jour, prérequis absolu de toute génération serveur — y compris du TTS pré-généré, qui attend la même chose.

F5. Le pipeline KTX2 / Meshopt n'a pas d'exécutant

Le cahier des charges demande KTX2 + Meshopt « dès le départ ». Or :

  • la compression existante (ImageCompressor) tourne dans le navigateur, en Dart, sur des JPEG ;
  • le serveur n'a aucun outil de traitement d'asset — ImageHelper en System.Drawing est du code mort, il ne tourne pas sur aspnet:8.0 Linux ;
  • transcoder en KTX2 et compresser en Meshopt, c'est appeler des binaires natifs (toktx/basisu, gltf-transform) : soit les embarquer dans l'image Docker, soit un service à part.

👉 Recommandation pour un dev solo : servir le GLB Marble tel quel en V1, mesurer, et n'optimiser qu'ensuite — mais poser dès maintenant dans le manifeste un bloc variants par asset, pour que l'ajout du KTX2 ne casse pas le contrat. La contrainte du cahier des charges (« à câbler dès le départ ») est honorée au niveau du format, pas de l'implémentation.

F6. Le viewer de preview ne peut pas être un widget Flutter

Voir §7. Conséquence de conception : le viewer doit être une app web autonome nourrie par une URL de manifeste, pas un composant. Ce qui le rend réutilisable dans visitapp-web — question Q2.

F7. Un .glb cassé à l'upload

resources_screen.dart:400-431 n'a pas de default. Petit bug, mais c'est le tout premier geste du client dans le module (« j'uploade mon personnage »).

F8. Le personnage 3D n'a pas de source

Ni catalogue existant (il n'existe pas), ni génération (Meshy/Tripo font des objets isolés, pas des humanoïdes rigés avec animation). En V1 : bibliothèque externe ou studio 3D, sous licence explicite — et c'est un sujet droits pour des institutions publiques, pas seulement un sujet technique.

F9. Traçabilité IA — l'exigence institutionnelle tombe pile

studio-plan.md §3.11 spécifie déjà exactement ce que demande le §5 du cahier des charges : Resource.AiProvenance jsonb avec modelKey, providerModelId, providerRequestId, effectivePrompt, referenceResourceIds, generatedAt, generatedByUserId, validatedByUserId, et rightsHolder par défaut au nom de l'instance — les droits au client, écrits dans la donnée, pas seulement dans les CGU. Un monde généré doit y entrer, avec en plus ses sources de capture (la vidéo / le panorama d'origine, eux-mêmes des Resource).


10. Ce qui est réutilisable tel quel — la liste courte

À garder sous les yeux en phase 2, pour ne rien réécrire :

Réutiliser Au lieu de
List<TranslationDTO> jsonb inventer un format i18n de manifeste
GeoPoint + Contents + Resource audio inventer un objet « hotspot »
ArticleAudioIds: List<TranslationDTO> d'ids de ressource inventer un stockage audio
GET /configuration/{id}/export + GetReferencedResourceIds écrire N endpoints pour Unity
Hangfire + convention « un job prend un id » une file maison
StripeWebhookController comme patron de webhook signé improviser le webhook Marble
Kiosk_devices/ (4 fichiers, 769 l.) écrire l'écran de flotte de casques
AppConfigurationLink.DeviceId inventer « une config par casque »
Le publish MQTT player/{deviceId} inventer le push de rafraîchissement
AuditLog inventer un journal d'action
Instance.IsAssistant comme patron d'add-on une table de jointure de features
Le flux d'appairage par pincode de tablet-app inventer le provisioning du casque

11. Ce qu'il reste à trancher avant la phase 2

Cinq questions, posées en restitution. Les deux bloquantes sont F2 (type de section vs entité parallèle — ça détermine tout le modèle de données) et le mécanisme de placement des personas.