# 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 ``` 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 { 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` 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` jsonb | inventer un format i18n de manifeste | | `GeoPoint` + `Contents` + `Resource` audio | inventer un objet « hotspot » | | `ArticleAudioIds: List` 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**.