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>
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 | Où |
|---|---|---|---|
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:704 → Text("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
Device.AppType.Data/Device.csn'a pas de type de canal, etControllers/DeviceController.cs:155résout l'ApplicationInstanceen dur surai.AppType == AppType.Tablet. Un casque enregistré aujourd'hui atterrit dans l'onglet Kiosk.ApiKeyAppTypene connaît que{ VisitApp, TabletApp, Other }(Data/ApiKeyAppType.cs, 3 lignes). Il fautVrAppen 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
- Aucune gestion de GLB, aucune gestion de 360. Ni
Model3D, niPanorama360. La carte kanban « Ressource 360° » (5-planifie/250-*) est encore en planifié ;Model3Dest prévu au lot 11 du Studio. - L'upload est fait par le navigateur, en direct vers Firebase. Le backend ne sait que
supprimer :
IResourceBlobServicen'expose queIsConfiguredetDeleteAsync(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. - Un
.glbdéposé aujourd'hui produit une ressource cassée. Leswitchd'upload (manager-app/lib/Screens/Resources/resources_screen.dart:400-431) n'a pas dedefault: l'extension inconnue laisseresourceDTO.typeànullet le mimeType à chaîne vide. - 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 asset —
SizeBytesexiste, le hash non. - Le quota stockage est un
SUM(SizeBytes)par instance, contrôlé à l'upload avec un413. 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é (kanban5-planifie/160-*) et attend lui aussi le lot 0. - Le pattern cible existe déjà :
SectionArticle.ArticleAudioIdsest unList<TranslationDTO>d'ids deResource. 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
- Il n'y a pas d'état « publié ».
Data/Configuration.csn'a niIsPublished, niStatus, niVersion— 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é ». - 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. UnSceneManifest.versionmonotone est à créer, il n'existe nulle part. - 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 | Où |
|---|---|
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,202 — HangfireJobId 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: nithree, ni@react-three/*, nimodel-viewer. Les seules dépendances de rendu sontleafletetreact-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-appest 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 —
ImageHelperenSystem.Drawingest du code mort, il ne tourne pas suraspnet:8.0Linux ; - 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.