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>
449 lines
25 KiB
Markdown
449 lines
25 KiB
Markdown
# 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
|
|
|
|
```csharp
|
|
// 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
|
|
|
|
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
|
|
|
|
```csharp
|
|
// 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 asset** — `SizeBytes` 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 | 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` : 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`](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**.
|