Creative Media OS — HISTORICAL (état de septembre 2026)
Archive de conception, pas contrat d'exécution actuel. Ce document décrit l'architecture du Media OS telle qu'elle était envisagée en septembre. Les anciens chemins creative/ et /ops/creative/*, les scripts Postiz…
Archive de conception, pas contrat d'exécution actuel. Ce document décrit l'architecture du Media OS telle qu'elle était envisagée en septembre. Les anciens chemins
creative/et/ops/creative/*, les scripts Postiz locaux et les étapes « suivantes » de ce texte ont évolué ou ont été retirés. Pour les opérations actuelles, lire d'abord Creative Studio — frontière de dépôts et Médias Shopify. Le code Remotion canonique réside dans Ecosystem, et les capacités médias Shopify restent dans BoostEcom ; ce document n'autorise ni leur duplication ni une intervention sur Orbit Studio.ADR lié :
0017. Items :2679(done, #1216) →2680(done, #1219) →2681–2682(+ suites2683Media desk,2684Remotion expand).Statut : PR1+PR2 sur
main(2026-09-14). Motion.so / Higgsfield retirés. Remotion local (Reel9x16 + 4 primitives) livré. Train vidéo suivant =2681(Playwright / ElevenLabs / Postiz). Ops immédiate : §14 (spine Growth texte), pas d'attendre la VO.
1. Problème qu'on résout
On ne cherche pas « un outil qui génère des vidéos ». On construit un Creative Agent / Media OS interne au SaaS :
- motion design déterministe (UI réelle, kinetic type, cards, charts) — pas du text-to-video type Runway/Kling/Veo pour l'UI produit ;
- une bibliothèque de primitives React composées par un agent ;
- des captures Playwright du vrai produit (allowlist) ;
- de la voix-off ElevenLabs alignée sur le script facts ;
- une distribution Postiz en draft ;
- une frontière claire avec le Studio marchand (plane B).
Référence visuelle : film produit SaaS (~98 s, motion UI + typo), pas un clip génératif.
2. Deux planes (non négociable)
| Plane | Nom | Pour qui | Marque | Sortie | Code / surface |
|---|---|---|---|---|---|
| A | Growth | Marketing BoostEcom / Christopher | BoostEcom (facts.md, DESIGN dérivé) | Postiz drafts | creative/ + boostecom-content / boostecom-creative + /admin/content/growth |
| B | Studio produit | Users SaaS via chat | Marque du store / org | StudioAsset + crédits | src/features/studio + tools generateImage / Veo |
Le plane C (Agency desk) est retiré depuis le 2026-09-26 (ADR 0043, qui remplace l'ADR 0004) : BoostEcom n'a ni agence ni client externe. Sont partis le Studio d'organisation
/[orgSlug]/~/studio/**,/ops/creative/{pipeline,clients}, la livraison/drop/[token], le croncreative-drop-tick, le jobopen-creative-drop, les kitscreative/brand/clients/et les modèlesStudioDrop,Prospect,ProspectEvent. Restent, au service du plane A : la QC/ops/creative/qc(revue des assets Growth avant Postiz) et les concepts/ops/creative/concepts(briefs d'angle Growth).Note opérateur. Retirer un modèle du schéma ne supprime pas sa table : le schema guard est additif. Les tables
StudioDrop,Prospect,ProspectEvent(et les colonnes retirées deStore: cadence et gate de production) restent orphelines en base. LeurDROPest une action opérateur ultérieure, délibérée, après sauvegarde (pnpm db:deploy), jamais un effet de bord d'une PR. Suivi :backlog/data-platform/3073, procédure dansdocs/ops/database-index-maintenance.md.
Règles dures
- Un render A n'écrit jamais un
StudioAssetmarchand. - (retirée avec le plane C : il n'y a plus de render client.)
- Le chat B ne lit jamais
facts.md/ plan Q4 Christopher comme vérité pub. Le mécanisme a été substitué, la règle non (ai-platform/2826, ADR 0024 §4).facts.mdest monté comme source de connaissance de la boutique du tenant Fondateur, par le chemin qu'un marchand emprunte pour les siennes (knowledgeSources). Ce qui protège n'est donc plus une consigne de prompt mais le scope :platformKnowledgeFor(storeId)rend un tableau vide pour toute autre boutique, et pour toute organisation non désignée parINTERNAL_ORG_SLUGS. Un chat B ne peut pas le lire parce qu'il ne lui est jamais remis — pas parce qu'on lui demande de ne pas le lire. Le fichier du skill reste la source unique : il est lu, jamais copié, donc un fait ajouté atteint le tenant sans intervention. Garde :src/test/the-founder-tenant-knows-its-own-product.test.ts. Corollaire (growth-web/2827) : puisque les ids de faits sont désormais lisibles, la question humaine « chaque chiffre vient-il d'un fait vérifié » devient une règle (fact.unsourceddanscopy-rules.ts) — mais seulement là où le corpus est monté. AilleursknownFactIdsest vide et la règle ne tourne pas : une règle qui se croit active et ne vérifie rien serait pire que la question qu'elle remplace. - ADR 0013 (agency vs studio permissions) reste pour sa moitié encore
vivante (
agency.qc.review,agency.economics.read) ; A est la croissance de la marque BoostEcom, pas le Studio vendu.
Voir aussi : docs/architecture/studio-agency-os.md (historique du plane C),
docs/architecture/acces-delegue-et-frontiere-studio.md.
3. Stack retenu
| Priorité | Pièce | Rôle |
|---|---|---|
| 1 | Skills + rules repo (boostecom-content, boostecom-creative) | Cerveau + garde-fous |
| 2 | Remotion + remotion-dev/skills | Moteur vidéo/image programmable |
| 3 | Playwright (allowlist) | Yeux sur le SaaS réel → captures |
| 4 | ElevenLabs TTS + forced alignment | Voix-off A (pas le chat live) |
| 5 | Postiz (déjà MCP + reconcile) | Distribution A |
| 6 | /api/og + social-cards.mjs | Stills / carousels |
| 7 | ffmpeg | Encode / GIF dérivé |
| bonus | Veo / génératif | B-roll abstrait rare seulement ; jamais l'UI |
Explicitement écarté
| Option | Pourquoi |
|---|---|
| Motion.so (Mosaic) | Crédits opaques, pas d'estimate, UI non déterministe ; owner stop 2026-09-14 |
| Higgsfield | Owner : non retenu (déjà 2678) |
| Creatomate / Banuba comme centre | Moins intelligent qu'un agent + codebase + Playwright |
| Rive comme moteur export | Utile dans le produit UI, pas pour exporter le calendrier Growth |
| Kling / Runway / Veo pour dashboards | Perte d'exactitude brand / UI |
Dupliquer un monolithe /content-engine + .ai/*.md parallèle | On a déjà content skill + Growth admin + DESIGN dérivé → un seul creative/ |
Hume vs ElevenLabs
| Hume EVI (existant) | ElevenLabs | |
|---|---|---|
| Job | Voix conversationnelle chat | TTS / VO contenus A |
| Remplacement | Chantier ai-platform séparé | Canon Media OS |
| Day-1 Media OS | Ne pas toucher | Brancher en PR3 |
Alternatives Hume plus tard : OpenAI Realtime, Gemini Live, ElevenLabs Conversational, LiveKit Agents, Cartesia.
4. Admin : combler, ne pas reconstruire
Surfaces existantes à étendre :
/admin/content/growth— units, renders, import plan, handoff/admin/content/bulletin, cms, …/ops/creative/qcet/ops/creative/concepts— revue et briefs du plane A Ajouts prévus (pas une 2ᵉ app) :
/admin/content/growth
├── Units (existe)
├── Renders (existe) ← artifacts Remotion + audio + état Postiz
├── Media desk (NEW) ← file specs, render, ledger, go humain```
---
## 5. Arborescence cible `creative/`
creative/ README.md # renvoie ici references/ ops.md # licence Remotion, 1 render, determinism hooks.md # rétention 1.3–1.7s, frame-0 text elevenlabs.md # modèles FR, alignment, caps, consent formats.md # 1:1 / 4:5 / 9:16 / 16:9 + safe zones postiz-video.md # matrices TikTok/IG/YT/X motion-language.md # langage visuel (référence → règles) hume-exit.md # note non bloquante ai-platform brand/ boostecom/ # plane A — dérivé DESIGN / assets allowlistprimitives/ # 30–50 à terme ; 8–12 en PR2 ProductWindow.tsx # cinq depuis 3036 : les cinq autres ne servaient que Reel9x16 / Landscape169 Chart.tsx BrandBackground.tsx Glow.tsx CTA.tsx scenes/ # Hook, Problem, Demo, Proof, CTA compositions/ # Templated (une par toile), UiShot (rush cinéma) ; Reel9x16 / Landscape169 supprimées (3036) captures/ # Playwright outputs (git LFS ou artifacts CI) campaigns/ # un brief → multi-renders pipeline/ plan.mjs | capture.mjs | tts.mjs | render.mjs | postiz.mjs | ledger.json out/ # gitignored package.json # Remotion isolé si possible (limite hot lockfile root)
Skills :
- `$HOME/.claude/skills/boostecom-content/` — rédaction (inchangé spine)
- `.claude/skills/boostecom-creative/` → évolue en **`boostecom-creative`**
(orchestre `creative/`, refuse Motion/Higgsfield)
- Brancher **remotion-dev/skills** pour l'agent de rendu
---
## 6. Langage motion (mesurable)
Figé dans `creative/references/motion-language.md` :
- Fond dark high-contrast ; accent brand **rare**
- Soft radial glow derrière l'UI
- UI produit en **perspective** (mock fenêtre), pas de mock générique stock
- Typo grotesk oversized ; **peu de texte par scène**
- Entrées blur/scale rapides ; caméra lente sur product shots
- Alternance scènes UI denses / typo sparse
- Chapter cards chiffrées
- **Hook text frame 0**, opacity 100 %, **zéro fade-in**
- Safe zone 9:16 : clear top **14 %** / bottom **35 %** / sides **6 %**
- Pas de logo inventé ; pas d'UI Shopify/Claude imitée ; pas de stock people
- Voix-off : script = facts verbatim FR (plane A)
---
## 7. Formats & Postiz (ops)
### Formats Remotion
| id | Ratio | px | Usage |
|----|-------|-----|--------|
| `reel-9x16` | 9:16 | 1080×1920 | TikTok / Reels / Shorts / Stories ads |
| `feed-4x5` | 4:5 | 1080×1350 | Feed Meta mobile |
| `square-1x1` | 1:1 | 1080×1080 | Carousel / Marketplace |
| `landscape-16x9` | 16:9 | 1920×1080 | YouTube / in-stream / launch |
| stills | via même composition `durationInFrames=1` ou `/api/og` | OG, LinkedIn, X |
### Postiz vidéo
| Réseau | AI disclosure | Piège |
|--------|---------------|--------|
| TikTok | `video_made_with_ai` + **`DIRECT_POST`** | `UPLOAD` = inbox 24 h, settings droppés (sauf title) ; API peut dire OK |
| Instagram | pas de champ AI Postiz | 1 vidéo = Reel |
| YouTube | pas de `made_with_ai` API | title 2–100, visibility, made-for-kids ; label AI en description / process YT |
| X | `made_with_ai` optionnel | — |
Toujours : **draft d'abord**, reconcile ledger, une série à la fois.
### ElevenLabs
- Modèles : `eleven_multilingual_v2` (stable) ou `eleven_v3` (expressif, limite chars plus basse)
- Captions : **forced alignment** (texte = vérité) → timings mots ; fallback Whisper.cpp + `t_dtw`
- Chunks VO **≤ 45–60 s**
- Voix A (BoostEcom) unique : le plane C (voix client) est retiré
- Consent écrit avant clone ; `ELEVENLABS_MONTHLY_CAP` + ledger
### Remotion ops
- Automation = licence **Automators** (ou Company) si >3 personnes
- **Un render à la fois** sur une machine
- Captions = JSON figé ; animer avec `frame/fps`, jamais timers runtime
- Determinism : mêmes `inputProps` → même artifact
### Remotion est une CAPACITÉ du registre, plus seulement une page
Depuis `ai-platform/2823` (ADR 0024 §4), `renderComposition` est un outil
du registre, famille `studio-composition`, étape `studio`. Avant, les dix
templates de `templates.json` n'étaient atteignables que par le
formulaire `/ops/creative/generate` : `render-dispatch.ts` avait sept
importeurs, tous des pages ou des actions de `/ops`, et le board du
cockpit portait un lien **sortant** pour les lancer.
Ce qui n'a pas changé, et ne doit pas :
- **un seul dispatch.** La capacité appelle `launchTemplateRender`, qui
re-valide tout ce que le formulaire valide, depuis les mêmes fonctions
de `template-catalog.ts`. Aucun appel direct à l'API GitHub ;
- **un seul catalogue.** La description servie au modèle est **dérivée**
de `CREATIVE_TEMPLATES`. Aucun identifiant de template n'est écrit dans
la capacité, et la garde `remotion-is-a-capability-not-a-page.test.ts`
le vérifie template par template ;
- **`/ops/creative/generate` garde son URL et sa porte.** Elle cesse
d'être le SEUL chemin, elle ne cesse pas d'être un chemin ;
- **l'ADR 0017 tient.** Remotion reste le moteur déterministe et la
hiérarchie « vraie UI > composants réels > captures > imitation
générative » n'est pas négociée. Les deux moteurs sont des capacités du
même registre plutôt que deux produits — c'est ce qui permet de choisir
le bon sans changer d'outil.
Trois bornes explicites :
| Borne | Pourquoi |
|---|---|
| Refus sans boutique, avant toute dépense | Un rendu se dépose dans une bibliothèque (ADR 0024 §5) |
| Moteur non configuré = une phrase, pas un échec | C'est le cas normal sur un clone : `isCreativeRenderConfigured()` est lu avant d'ouvrir la ligne |
| **Aucun scope MCP** | Un rendu dépense des minutes de runner. « Un accord OAuth durable peut-il dépenser sans qu'un humain regarde » est la question que `integrations/0714` pose au propriétaire, et aucun agent n'y répond à sa place |
**Depuis `data-platform/2828`, un rendu lancé par la capacité atterrit
sous le préfixe de la boutique qui l'a demandé** (`stores/<id>/studio/`),
pas dans `creative/library/`. La chaîne a quatre maillons — la capacité
nomme la destination, `render-dispatch` la transmet, le workflow la
déclare, `publish-artifact.mjs` l'applique — et le défaut vide laisse le
**formulaire** de `/ops/creative/generate` et un lancement à la main
inchangés.
Deux raisons qui n'en font qu'une : le balayeur de rétention ne connaît
que le préfixe d'un store (`data-platform/2791`), donc un fichier rangé
ailleurs n'a **aucun** balayeur ; et un second inventaire qui grossit est
un second inventaire permanent. `creative/library/` reste **lisible** —
rien n'est supprimé, aucun lien déjà partagé ne casse — mais il se nomme
désormais une archive dans son propre en-tête, et
`src/test/one-inventory-of-renders.test.ts` refuse qu'il redevienne « la
bibliothèque ».
**Et depuis `platform-ops/2830`, la ligne se règle.** Le run rappelle
`/api/webhooks/creative-render`, signé HMAC-SHA256 sur le corps brut :
livré avec son fichier, ou échoué avec la conclusion du run. Le rappel
part **même quand le run échoue** (`always()`) — c'est justement ce
run-là dont la ligne resterait ouverte pour toujours — et le script sort
toujours 0, parce que faire échouer un rendu publié pour un rappel raté
serait remplacer un défaut discret par un défaut bruyant et faux.
Trois refus délibérés : le rappel **n'interroge pas GitHub** pour
confirmer ce que le run vient de dire (une seconde source de vérité sur
l'état d'un run diverge de la première) ; une ligne déjà réglée est
laissée telle quelle, donc une double livraison ne peut pas la rouvrir ;
et un run qui réussit **sans publier un fichier** est réglé `FAILED`,
parce que l'opérateur n'a rien à ouvrir.
Le filet est `settle-stale-renders`, horaire : une ligne `PROCESSING`
plus vieille que la durée maximale d'un run est réglée `FAILED` avec sa
raison — personne n'a rappelé — et renvoie vers la bibliothèque, où le
fichier est peut-être bien là. Il ne devine aucune issue. C'est ce qui
rend **borné** le cas où `CREATIVE_RENDER_CALLBACK_SECRET` manque d'un
côté : à poser aux DEUX endroits, l'environnement Vercel et un secret
GitHub Actions du même nom.
La ligne `GenerationRequest` s'arrête à `PROCESSING`, et il faut le
savoir : `creative-render.yml` téléverse puis se tait, personne ne
rappelle. C'est déjà la différence entre « le run GitHub est la seule
trace » et « la boutique sait qu'elle a demandé un rendu », et c'est un
état qui ment à son tour si personne ne le termine. Le rappel signé et
son filet sont `platform-ops/2830`.
### La voix est une CAPACITÉ, plus un bouton d'une page
Depuis `ai-platform/2824`, `generateVoiceover` est un outil du registre,
famille `studio-voice`, étape `studio`. Avant, `voiceover.ts` avait trois
importeurs hors de lui-même et **un seul** appelait la synthèse : l'action
de `/ops/content`. Les deux autres — la page et le board média du cockpit
— ne lisaient que le quota. Le cockpit savait combien de caractères il
restait et ne pouvait pas poser une voix.
Deux choses réparées ensemble :
- **la porte.** Le chat et le canvas la voient par dérivation.
`/ops/content` garde son bouton et sa garde : elle cesse d'être le seul
chemin ;
- **le troisième inventaire.** Le fichier partait sous
`creative/voiceover/`, hors du préfixe d'une boutique et sans ligne en
base. Une voix demandée **pour** une boutique atterrit désormais sous
`stores/<id>/studio/voiceover/` — même préfixe qu'une image ou une
vidéo, donc même rétention et même suppression propagée
(`data-platform/2791`) — et ouvre sa ligne `GenerationRequest`. Une
voix off de la **plateforme** reste sous `creative/voiceover/` : elle
n'appartient à aucune boutique, et lui en inventer une serait pire que
le préfixe partagé.
Le coût d'une voix se compte en **caractères**, et il était entièrement
hors ledger. Il est maintenant sur la ligne, même tant qu'aucun barème ne
le convertit en dollars. Le quota, lui, reste **lu** par
`readVoiceoverQuota` et jamais recalculé : une capacité qui court après
un quota qu'une page lit ailleurs, ce sont deux vérités qui divergent le
jour où l'une se trompe.
`ELEVENLABS_*` absent est le cas **normal** sur un clone : la capacité le
dit en une phrase avant d'ouvrir quoi que ce soit, comme l'ADR 0024 §3
l'exige. La liste des moteurs reste fermée — aucun fournisseur de voix
n'est ajouté.
### Hooks (rétention)
- Décision swipe ~**1.3–1.7 s**
- Mute-first : texte on-screen frame 0
- A/B : seul le hook change ; métrique hold 3 s / 8 s
---
## 8. Playwright (captures)
Autorisé :
- pages listées dans allowlist (`assets.json` + routes marketing publiques
+ **compte démo seed** si dashboard)
- screenshots élément / full-page haute rés
- sortie versionnée sous `creative/captures/<route>/<hash>.png`
Interdit :
- stores clients réels / PII
- routes hors allowlist
- « explorer prod » sans seed
---
## 9. Workflow agent (cible)
brief ou feat mergée → lire facts + git + PRODUCT (dérivé) → (opt) Playwright capture états utiles → composer scenes via primitives (pas inventer de nouveaux composants) → TTS ElevenLabs + alignment → Remotion render multi-format → Postiz draft (A) → ledger + Growth Renders
L'IA **compose** 30–50 primitives validées ; elle n'invente pas un After Effects libre.
---
## 10. Plan d'exécution (PRs)
| PR | Backlog | Contenu | État |
|----|---------|---------|------|
| **PR1** | `2679` | ADR 0017 + ce doc + skeleton `creative/` + references/* + kill Motion + `boostecom-creative` | **DONE** [#1216](https://github.com/BoostEcom/boostecom.app/pull/1216) |
| **PR2** | `2680` | Remotion + Reel9x16 + SafeZone/HookText/KineticText/ProductWindow + render u18 **muet** | **DONE** [#1219](https://github.com/BoostEcom/boostecom.app/pull/1219) |
| **PR3** | `2681` | Playwright + ElevenLabs pipeline + Postiz draft vidéo (code) | **DONE** [#1221](https://github.com/BoostEcom/boostecom.app/pull/1221) |
| **PR4** | `2682` | `campaigns/<slug>/` multi-format + wire Renders Growth (le stub `brand/clients/` est abandonné avec le plane C) | AFTER live VO / media desk |
Suites découpées (gaps + ops post-livraison) :
| Item | Rôle | État |
|------|------|------|
| `2685` | Postiz **live** : drafts `utm_content=cmu…` + 1 `AttributionEvent` | **DONE** (T2 live + AttributionEvent u06 ; fill rest via `postiz-push-utm.mjs`) |
| `2686` | Premier MP4+VO live (ElevenLabs key + mux + draft vidéo) | **ready** (ops) |
| `2683` | Media desk — liste artifacts `creative/out/` sur `/admin/content/growth` | **DONE** [#1225] |
| `2684` | Landscape169 + primitives 8–12 + remotion-dev/skills | **ready** |
Parallèle (hors train vidéo, même pilier) :
| Item | Rôle | État |
|------|------|------|
| **Ops §14** | Spine texte mesurée | **OK** pour Done when `2685` (série cuid + AttributionEvent) ; fill Postiz restant = re-run script |
| `2660` | Cockpit objectifs lancement | **DONE** [#1217](https://github.com/BoostEcom/boostecom.app/pull/1217) |
| `2657` | Sortie Postiz depuis un `ContentRender` | AFTER renders earned |
| `0363` | DataFast visitors / events admin | decision P3 (cockpit « Not observable ») |
Hors train (ne bloque pas) :
- `ai-platform` : sortie Hume EVI
- Remotion Lambda (après local stable)
- Plane B consomme le moteur Remotion avec kit store (crédits)
### Prérequis owner
1. `sudo apt install -y ffmpeg` (WSL)
2. Débrancher Motion (+ Higgsfield) sur claude.ai
3. `ELEVENLABS_API_KEY` dans `~/.env.boostecom` avant PR3
4. Consent VO si clone de voix réelle
5. Compte / seed démo pour captures dashboard (PR3)
### Hot files
- `.claude/skills/**` → trailer `Hot-file`
- `package.json` / lockfile si Remotion au root → préférer `creative/package.json`
Pilier : **growth-web**. Cross-pillar si touche `src/features/studio` (plane B) : trailer + relecture ai-platform.
---
## 11. Inventaire repo (constat post-#1219)
Déjà là :
- `boostecom-content` (facts, voice, plans, Postiz)
- `boostecom-creative` (**Motion retired**, DESIGN dérivé, gardes ledger)
- `creative/` Remotion isolé : 4 primitives, `Reel9x16`, `render:u18`, `references/*` (les deux derniers supprimés depuis par `3036`)
- `/api/og`, `scripts/social-cards.mjs`
- Studio B : `generateImage` / Veo → `StudioAsset`
- QC Growth : `/ops/creative/qc` (le plane C, `~/studio` et ses drops, est retiré)
- Growth admin : `/admin/content/growth` (import, derive, handoff, **cockpit** `2660`)
- Playwright / Browserbase (scans — pas encore pipeline marketing)
- Runbook spine : §14
Absents (comblés par `2681`+ / suites) :
- Pipeline capture / TTS / Postiz vidéo (`2681`), Media desk artifacts (`2683`)
- Landscape169 + primitives 8–12 + skills Remotion (`2684`)
- ffmpeg local (prérequis owner), `ELEVENLABS_API_KEY`
- Write Postiz depuis admin (`2657`)
---
## 12. Ce qu'on a volontairement fusionné d'une autre session
Repris :
- Creative System = primitives + agent, pas TTV
- Playwright comme yeux
- Campagne multi-format depuis un seul code
- Mémoire = Git + fichiers + app, pas le chat
- remotion-dev/skills
Non repris :
- Monolithe `/content-engine` parallèle
- « Cursor = cerveau produit » comme architecture runtime
- Oubli Postiz / 3 planes / ElevenLabs / Growth admin
---
## 13. Critère de succès global
1. u18 (ou unité plan équivalente) → MP4 9:16 (+ VO optionnelle) → draft Postiz
2. Zéro appel `motion.so` / Higgsfield dans le skill
3. Growth admin montre l'artifact
4. Plane B et C non croisés avec facts Growth
5. Nouveau brief « launch feature X » peut s'appuyer sur primitives + captures sans réinventer le langage motion
---
## 14. Runbook — activer la spine Growth (ops)
> **NOW, pas la VO.** La spine texte → Postiz → mesure existe déjà.
> Remotion muet est livré (`2680`) ; Media desk / VO / Postiz vidéo =
> `2681`+ / `2683`. Ne pas attendre la VO pour importer, dériver, draft
> Postiz et vérifier l'attribution.
>
> Mesure détaillée : [`measurement-stack.md`](./measurement-stack.md)
> (UTM, DataFast, `AttributionEvent`). UTM Postiz :
> `$HOME/.claude/skills/boostecom-content/references/postiz.md` §4.
### Ce qu'on active
plan Q4 (skill) → Import GrowthUnit /admin/content/growth → Derive ContentRender même console, bouton Derive → growth-unit-ids.json coller les ids → rebuild + check-plan → UNE série Postiz draft utm_content = GrowthUnit.id → reconcile → DataFast goals + cron growth-attribution + AttributionEvent
### Prérequis
- Admin plateforme (console derrière `requireAdmin`)
- Manifeste déployé : `src/services/growth/plan-2026-q4-units.json`
(produit par `plan-src/build.mjs` depuis le skill)
- `POSTIZ_API_KEY` pour reconcile / MCP
- Cookie consent accepté sur le parcours de test (sinon DataFast +
`bec_acq` muets)
### Étapes ops (ordre fixe)
1. **Importer le plan** — `/admin/content/growth`, section Import.
Action `importContentPlan` → `services/growth/plan-import.ts`,
manifeste `plan-2026-q4-units.json`. Idempotent
(`dedupeKey = plan:<campaign>:<unit>`). Second clic = « already
present ». Si le manifeste a bougé **après** un import : **Resync
plan units** (réécrit thesis/facts/sources, **ne re-id pas**).
Jamais delete + reimport : les ids sont déjà dans les liens.
2. **Dériver les renders** — sur chaque unité importée (ou la série
pilote), bouton **Derive** → `deriveUnitRenders` →
`services/growth/derive.ts`. Crée les `ContentRender` mérités
(score + evidence). Idempotent sur `(unit, channel)`. Sans cette
étape : table Renders vide, `utmFor` n'a aucun appelant vivant.
3. **Coller les ids** — l'écran Import affiche `{ "u01": "clx…" }`.
Copier dans
`$HOME/.claude/skills/boostecom-content/references/growth-unit-ids.json`.
`build.mjs` refuse une clé hors plan et une valeur qui n'a pas la
forme d'un id.
4. **Rebuild + garde plan** (depuis la racine plateforme) :
```bash
node $HOME/.claude/skills/boostecom-content/scripts/plan-src/build.mjs
node $HOME/.claude/skills/boostecom-content/scripts/check-plan.mjs \
$HOME/.claude/skills/boostecom-content/references/plan-2026-q4.json
Le warning « slug in utm_content » doit baisser : chaque post
relié porte désormais l'id d'unité, pas le slug de pilier.
-
Émettre UNE série Postiz en draft — jamais le plan entier (MCP ne sait ni éditer ni supprimer en masse proprement) :
node $HOME/.claude/skills/boostecom-content/scripts/check-plan.mjs \ $HOME/.claude/skills/boostecom-content/references/plan-2026-q4.json \ --emit <SERIES> # puis integrationSchedulePostTool avec type: "draft" uniquementDraft-only.
type: "draft"obligatoire jusqu'à go humain explicite sur le texte exact (skill social STEP 0). Pas deschedule/publisheddepuis l'agent. -
Reconcile avant et après le batch :
POSTIZ_API_KEY=… node $HOME/.claude/skills/boostecom-content/scripts/postiz-reconcile.mjsStop si
ORPHANS/MISSING/DUPLICATES≠ 0. Après regen de corps :--delete-orphansretire les drafts stale (jamais un PUBLISHED).
UTM (contrat non négociable)
Constructeur unique : utmFor(channel, unitId, campaign) dans
src/features/growth/growth-kinds.ts.
| Param | Valeur |
|---|---|
utm_source | canal (x, linkedin, …) — pas boostecom |
utm_medium | social / community / … |
utm_campaign | campagne plan (ex. launch-2026-q4) |
utm_content | GrowthUnit.id (cuid), pas le slug pilier |
Seul appelant produit : services/growth/derive.ts (garde
src/test/utm-convention-reach.test.ts). Les drafts Postiz du skill
doivent coller la même formule via growth-unit-ids.json +
build.mjs. Un utm_content encore en slug (bridge-ai, spy, …)
passe le CAC (utm:x) mais ne résout aucune unité dans
services/growth/attribution.ts.
Vérifier la mesure
| Surface | Quoi lire |
|---|---|
| DataFast | Goal auth_signup (pas signup, retiré) filtré utm_source ; funnel Social → Auth → Signup → Onboarding |
| Cron | growth-attribution — src/app/api/cron/growth-attribution/route.ts, daily ~06:00 UTC (vercel.json) : reconcile subscriptions + deliveries → AttributionEvent |
| Admin | /admin/content/growth — stats attribution observée (pas d'impressions) |
Sans clic consenté + cookie datafast_visitor_id / bec_acq : DataFast
reste à zéro alors que la spine Postiz est saine. Relire
measurement-stack.md (consent + goals API).
Modes d'échec (symptôme → cause)
| Symptôme | Cause probable |
|---|---|
ContentRender vide / Derive « none » | Import jamais fait, evidence trop faible, ou score sous le plancher de distribution |
| DataFast 0 sur un clic réel | Consent refusé, ou cookie datafast_visitor_id absent (goals API) |
AttributionEvent vide après signup | utm_content encore slug pilier ; cron pas tourné ; token non résolu (unresolvedTokens dans la réponse cron) |
| CAC ok, unité muette | utm_source correct mais utm_content ≠ GrowthUnit.id |
| Doublons / orphelins Postiz | Batch multi-séries ou regenerate sans reconcile + --delete-orphans |
| Lien Instagram « perdu » | Pas de lien cliquable post ; bio seulement (utm_campaign=bio) |
Frontière train Remotion
| Maintenant (cette section) | Ensuite (2681–2684) |
|---|---|
| Import + Derive + Postiz texte draft | Attach vidéo aux drafts (2681) / Media desk (2683) |
utm_content = unit id | VO ElevenLabs, Playwright captures |
DataFast + AttributionEvent | Landscape169 + primitives (2684) |
Remotion local muet (2680) | Campagne multi-format (2682) |
Un agent qui propose Motion.so / Higgsfield ou « attendre Remotion pour poster » lit mal ce runbook.
14 bis. Le générateur : la plateforme se remplit à la demande
Livré le 2026-09-17, sur une directive : pouvoir demander une génération à tout moment, sur tout sujet, pour alimenter toute la plateforme en motion, GIF, image, vidéo et picto.
Ce qui bloquait
Le moteur savait rendre un sujet : un fichier de brief
uNN-<format>.json écrit à la main dans le skill, une composition par
mise en page, et une seule sortie, le MP4 vertical. La plateforme ne
pouvait donc se remplir qu'au rythme où quelqu'un committait un fichier —
et rien ne produisait une carte OG, un pictogramme ou un GIF.
Le modèle
Une forme × une toile × un type de fichier. Les trois sont des
listes, et les trois vivent au même endroit :
src/services/creative/templates.json.
| Ce que c'est | Aujourd'hui | |
|---|---|---|
Forme (template) | une mise en scène, pas un sujet | 10 : hook, étapes, feature, graphique, chiffre, citation, avant/après, annonce, carte OG, pictogramme |
Toile (format) | des dimensions et une zone sûre | 6 : 9:16, 16:9, 1:1, 4:5, OG 1200×630, tuile 512 |
Fichier (output) | ce qui est écrit sur le disque | 4 : MP4, GIF, WebM, PNG |
Le sujet arrive dans fields, typé (text, lines, chips, color…),
depuis le formulaire admin ou depuis un drapeau de la CLI. C'est ce qui
fait qu'une composition sert tous les sujets.
Un catalogue, deux lecteurs, aucune copie
Le JSON est sous src/ et importé par les deux côtés :
src/services/creative/template-catalog.ts (le lecteur typé + les règles)
et creative/templates/load.ts (Remotion et les scripts). Pas de jumeau
généré, donc pas de garde de fraîcheur à tenir : une seconde copie est
précisément ce que cette disposition évite.
Pourquoi sous src/ et pas sous creative/ : le formulaire est bundlé
par Next, et Vercel n'embarque pas forcément les fichiers hors src/
dans une fonction serverless — le défaut que media-desk.ts documente
déjà, où une lecture locale rend tout en dev et rien en production.
Le trajet
/admin/creative/generate formulaire dérivé du catalogue
→ workflow_dispatch creative-render.yml (Chromium sur le runner)
→ scripts/render.mjs le MÊME script qu'en local, formats × sorties
→ Vercel Blob creative/library/<template>/<slug>/<fichier>
→ /admin/creative/library planche de contact + URL publique à copier
Trois refus écrits dans le moteur, pas dans une relecture :
- une sortie animée sur une toile fixe est sautée et nommée (une carte OG en MP4 est un fichier inutile là où vont les cartes OG) ;
- un PNG est pris sur la dernière image, là où toutes les entrées se sont posées — c'est le contrat que les formes « still » respectent ;
- le script sort en erreur si un fichier demandé a échoué, donc une étape de workflow ne peut pas annoncer un succès sur un job à moitié écrit.
ffmpeg n'est plus une porte
Remotion 4 embarque son encodeur : h264, vp8 et gif se recousent sans
ffmpeg système (vérifié sur une machine qui n'en a aucun). Le rendu
appelait pourtant check-ffmpeg.mjs et refusait de démarrer : une
barrière devant une porte ouverte. ffmpeg reste requis pour le mux de
la voix off, qui l'appelle vraiment.
ffprobe non plus (3071) : le chemin UiShot lit le rush avec
FFPROBE_PATH, puis le binaire système, puis celui que Remotion livre à
côté de son ffmpeg (creative/pipeline/lib/ffprobe.mjs), et le dit par son
nom (RENDER_FFPROBE_MISSING) quand il n'y en a aucun. Ce binaire
embarqué arrive sans bit d'exécution (npm ci le dépose en 0644 ;
seuls ffmpeg et remotion l'ont dans l'archive), et Remotion le rend
exécutable lui-même avant de l'appeler : le résolveur fait le même geste
(makeExecutable), sans quoi le repli échouait en EACCES sur toute
machine sans ffprobe système. Le nombre d'images
retombe sur un comptage décodé puis sur la durée, jamais sur NaN ; une
cadence autre que 30 ou 60 est refusée (RENDER_RUSH_FPS) au lieu d'être
rendue en 60.
Le verrou de rendu dépend du codec
remotion.config.ts posait le master H.264 (CRF 18, x264 slow, AAC
192k, bt709) sur TOUT rendu, et Remotion refuse un preset x264 ou une piste
AAC hors H.264 : chaque webm et chaque gif sortait en code 1, et le job
entier était déclaré raté. Depuis 3071 le fichier lit le codec de la
commande (creative/pipeline/lib/render-lock.mjs) : H.264 garde tout le
master, vp8 garde bt709 et les défauts de Remotion (CRF, Opus), un gif n'a
ni CRF, ni audio, ni espace colorimétrique (Remotion le pose en filtre
zscale, que ffmpeg refuse à côté du graphe de palette). Preuve :
REMOTION_CODECS_E2E=1 npm run pipeline:test rend mp4, webm, gif et png
d'un même job et vérifie leurs codecs, et que la durée déclarée du gabarit
shell (119,27 s) est celle du film BoostEcom qu'il rend : déclarée à
22 s, son PNG tombait à 3,5 s au lieu de 16 % du film.
Le Studio reste la main gauche
pnpm creative:studio ouvre les mêmes compositions. La barre latérale
liste une composition par toile ; la forme et le sujet sont des props
JSON éditables à chaud. On y va pour dessiner une forme, jamais pour
en remplir une de plus. Runbook : docs/ops/creative-studio.md.
Réparé le 2026-09-26 (growth-web/3034). Cette phrase était fausse
depuis le film Home Shell : creative/src/Root.tsx n'enregistrait plus que
BoostEcom, et creative-catalogue-is-renderable.test.ts exigeait
l'absence de FORMATS.map(. Le formulaire offrait dix formes, le workflow
les acceptait, et chaque rendu mourait sur « Could not find composition
with ID <toile> ». Root enregistre de nouveau une composition par toile
(Templated, durée lue du template par calculateMetadata) à côté du
film, et le garde exige désormais l'inverse. Dans le même item : les
dépendances du film déclarées dans creative/package.json, la palette
creative/src/tokens.ts lue depuis boostecom.DESIGN.json au lieu d'hex
recopiés, et l'audio non versionné qui manque rend un film muet au lieu
d'un rendu en échec (détail : creative/README.md).
15. Capacités — maintenant vs après X
Capable maintenant (post-2680 / #1219)
| Capacité | Comment |
|---|---|
| Rédiger un post facts-only | boostecom-content + check-plan.mjs |
| Carte / still OG | /api/og + social-cards.mjs |
| Importer le plan Q4 en DB | /admin/content/growth Import |
Dériver des ContentRender | bouton Derive |
| Draft Postiz texte (une série) | MCP + §14 ; utm_content = unit id |
| Reconcile Postiz ↔ plan | postiz-reconcile.mjs |
| Mesurer signup / funnel produit | DataFast + UserMilestone |
| Attribuer une unité (si UTM correct) | cron growth-attribution → AttributionEvent |
| Refuser Motion / Higgsfield | GUARD:motion-retired, TOOLS = {} |
| Lire la doctrine 3 planes | ce fichier + ADR 0017 |
| Composer Hook / Demo / CTA | creative/scenes/* + creative/primitives/* + Remotion Studio |
| Rendre un 9:16 muet | node creative/scripts/render.mjs --template <id> --formats reel-9x16 --outputs mp4 (render:u18 supprimé par 3036) |
| Voir objectifs vague vs observé | cockpit 2660 sur /admin/content/growth |
Pas capable maintenant
| Manque | Débloqué par |
|---|---|
| VO / captions alignées | 2681 + ELEVENLABS_API_KEY |
| Captures marketing allowlist versionnées | 2681 |
| Pack multi-format d'une campagne | 2682 |
| Savoir quel hook a gagné | experiment model — LATER (audit) |
| Générer une image depuis un modèle d'IA (illustration, photo) | hors périmètre : ce moteur compose des formes, il ne synthétise pas de pixels |
| VO depuis l'admin | le workflow la mux quand ELEVENLABS_API_KEY / ELEVENLABS_VOICE_ID sont des secrets GitHub du dépôt (2686) |
Capable depuis le 2026-09-17 (2657 / 2684 / rendu en un clic)
| Capacité | Comment |
|---|---|
| Rendre depuis l'admin, sans poste ni ffmpeg | template=legacy, unités du plan Q4)3039, ce chemin n'avait plus rien à rendre depuis 3036. Rendre depuis la plateforme passe désormais par la capacité renderComposition (gabarits) ou le bouton Filmer de la section Cinéma (cf. plus bas). Les MP4 déjà publiés sous creative/out/<unit>/ restent listés par le Media desk |
| Lister les MP4 depuis l'admin Growth | Media desk = Blob (workflow) + creative/out/ local (services/growth/media-desk.ts) |
| Draft Postiz vidéo (MP4 attaché) | Media desk → « Draft in Postiz » sur un artefact Blob : Postiz héberge une copie (POST /upload), brouillon TikTok DIRECT_POST + IA / YouTube privé / X / Instagram reel (buildVideoDraft). Enregistré dans AdminAuditLog |
| Poster un rendu texte depuis l'admin (sans copier-coller) | table Rendus → « Draft in Postiz » sur un rendu Ready (linkedin / x / facebook / instagram) → scheduled ; « Relire Postiz » → published quand Postiz rapporte l'URL publique. Détail : measurement-stack.md « Le trajet render → Postiz → URL observée » |
| Landscape 16:9 | toile landscape-16x9 (Templated). La composition Landscape169 est supprimée (3036) |
| 5 primitives | ProductWindow, BrandBackground, Glow, Chart, CTA (les cinq autres ne servaient que les compositions supprimées par 3036) |
Skills Remotion sans quitter creative/ | Ecosystem/projects/creative-runtime/creative/skills/remotion/ (markdown vendorisé de remotion-dev/skills, entrée remotion-best-practices/SKILL.md) |
Capable depuis le 2026-09-26 (3033, cinéma : rush UI)
| Capacité | Comment |
|---|---|
| Filmer une scène cinéma image par image | creative/pipeline/capture-video.mjs : Chromium (playwright-core, PLAYWRIGHT_CHROMIUM_PATH), colorScheme: dark, deviceScaleFactor: 2, attente des polices et images, puis pour chaque instant de frameTimes(durationMs, fps) (dernier instant compris) un seek() sur window.__BOOSTECOM_CINEMA__ et une capture PNG de [data-cinema-scene], pipée à ffmpeg (système, ou le binaire de Remotion). Même verrou que remotion.config.ts : H.264, yuv420p, CRF 18, slow, bt709, +faststart. Sortie creative/public/rushes/<sceneId>-<fps>fps.mp4, jamais écrasée (dossier temporaire + link sans écrasement), nettoyée sur SIGINT, erreurs codées CAPTURE_VIDEO_* |
| La seule exception à l'allowlist | /ops/creative/cinema/<sceneId>?capture=1 en loopback http ou sur l'hôte de production, avec une session lue dans l'environnement (creative/references/captures.md). Depuis 3039, en production, c'est la session du compte de service de relecture, obtenue par le runner lui-même (cf. section suivante) |
| Cadrer un rush | composition UiShot : <OffthreadVideo src={staticFile("rushes/…")}> dans ProductWindow, titre et légende optionnels, 9:16 ou 16:9, fps et durée lus du rush par render.mjs --composition UiShot |
| Prouver le tuyau sans l'app | npm run pipeline:video:fixture : une page statique à faux pont, servie en 127.0.0.1, puis ffprobe (codec, nombre d'images) |
L'option UiShot du workflow creative-render.yml est fusionnée
(platform-ops/3035).
Capable depuis le 2026-09-26 (3039, « Filmer » depuis la plateforme)
Un membre de l'équipe sans terminal produit une vidéo Growth depuis la
section Cinéma du cockpit. Un clic, un run, un film dans la galerie de la
marque. Aucun second chemin : le même dispatch privé de
services/creative/render-dispatch.ts, le même workflow, la même ligne
GenerationRequest (capacité renderComposition, params.kind = "cinema"), le même rappel signé. Garde : src/test/a-film-is-a-render.test.ts.
| Étape | Où | Comment |
|---|---|---|
| Geste | filmCinemaSceneFromCockpit (components/studio/ops-actions.ts) | platform.creative.review, règles de copie sur titre et légende, AdminAuditLog studio.cinema.film_launched |
| Ligne puis ticket puis dispatch | services/creative/cinema-film.ts | la ligne s'ouvre d'abord ; issueCaptureTicket (services/creative/capture-ticket.ts) range le SHA-256 d'un nonce de 256 bits dans params.captureTicket et signe cct1.<id>.<nonce>.<exp>.<sig> (HMAC d'une clé dérivée, séparée par domaine, de CREATIVE_RENDER_CALLBACK_SECRET, 30 min) ; launchCinemaRender envoie template=cinema + ticket. Un refus annule la ligne |
| Échange | POST /api/creative/capture-session | forme, signature, échéance et liaison vérifiées sans base, puis consommation atomique (UPDATE ... WHERE usedAt absent, statut SUBMITTED ou PROCESSING). Rend une session NextAuth de 15 min du compte de service (mintReviewServiceSession, auditée au nom du demandeur). Tout refus : 401 invalid_ticket, motif au journal serveur seulement. Débit limité par IP |
| Capture + rendu | creative/pipeline/film.mjs (pas « Film the scene ») | lit ses entrées dans $GITHUB_EVENT_PATH (jamais ${{ }}, jamais env:), ::add-mask:: du ticket puis du jeton, origine d'échange fixée à la production (redirect: "error"), cookie passé à captureVideo dans un objet d'environnement jamais exporté, puis render.mjs --composition UiShot sans le cookie |
| Publication + rappel | publish-artifact.mjs --template ui-shot puis callback.mjs | sous stores/<storeId>/studio/ui-shot/<scène>-<fps>fps-<requestId>/, contentType transmis au rappel pour que la galerie range le film en vidéo. L'id de la demande est dans le label depuis 3071 (renderLabel) : deux films d'une même scène étaient UN blob, que le second écrasait sous toutes les lignes qui le pointaient. film.mjs refuse un label qui ne finit pas par l'id |
| Concurrence | creative-render.yml | un groupe PAR DEMANDE (creative-render-<journal ou run_id>), depuis 3071. GitHub ne garde qu'un run en attente par groupe : le groupe <template>-<label>, commun à toutes les boutiques, faisait annuler le deuxième lancement par le troisième, et sa ligne restait PROCESSING jusqu'au balayeur |
| Installation | creative-render.yml | pnpm install --frozen-lockfile --ignore-scripts sur creative/pnpm-lock.yaml (pnpm seulement, node-linker=hoisted dans creative/.npmrc pour garder le compositor Remotion sous node_modules/@remotion/), versionné depuis 3071, versions exactes (playwright-core = celle de la racine). Le fichier d'évènement contient déjà le ticket quand ce pas tourne : aucun script d'installation d'une dépendance ne s'exécute, aucune plage ^ ne tire une version non relue |
| Progression | readCinemaFilmFromCockpit | le panneau relit la ligne toutes les 8 s : en cours, livré (lien), échoué (motif) |
Sécurité, en une phrase par verrou : aucun secret durable ne quitte
Vercel (le runner n'a que le secret de rappel qu'il avait déjà, et il ne
suffit pas à forger un ticket accepté : le nonce n'existe qu'en base,
haché) ; le ticket est à usage unique, lié à sa ligne, perimé en 30 min ;
la session ne vaut que 15 min, que pour l'identité de service, qui ne
tient que platform.creative.review ; couper l'identité
(DELETE /api/security/service-account) éteint une session en cours à la
requête suivante. Limite connue : l'entrée ticket est visible dans la
charge de l'évènement workflow_dispatch pour qui lit le dépôt ; elle est
bornée par l'usage unique et la course est perdue d'avance pour un tiers
(le runner l'échange en premier et un échange refusé fait échouer le run
bruyamment).
Preuves locales : capture-ticket.test.ts, cinema-film.test.ts,
render-dispatch.test.ts (charge du dispatch contre les entrées déclarées
du workflow), capture-session/route.test.ts, pipeline.test.mjs
(film.mjs), et FILM_E2E=1 npm run pipeline:test : échange sur un
serveur local, capture qui n'ouvre le plateau qu'avec le cookie, rendu
UiShot réel. Non prouvé tant qu'un vrai run n'a pas tourné : le Chrome du
runner ubuntu-latest, l'échange contre la production, le rappel.
La capture en local, seul geste qui demande encore une session fabriquée
à la main. La commande « Capture en local (terminal) » de la section
Cinéma sert au développeur qui filme sur son poste. Sa session vient d'un
admin, par POST /api/security/service-account ({"action":"session"},
1 à 240 minutes) : le runbook admin
content/runbooks/fr/ops-session-capture.mdx (/admin/docs/ops-session-capture)
en est la procédure. Il a été fusionné avec l'ancienne procédure
(utilisateur e2e- créé à la main, session signée avec NEXTAUTH_SECRET)
alors que 3035 était déjà fusionné, et renvoyait au tutoriel vidéo qui
renvoyait à lui ; growth-web/3073 l'a réécrit pour le trajet actuel.
Garde : src/test/academy-tutorials-match-the-product.test.ts.
Prouvé de bout en bout le 2026-09-26 (3036)
| Preuve | Comment |
|---|---|
Rush → UiShot → MP4, 9:16 et 16:9 | UISHOT_RENDER_E2E=1 npm run pipeline:test (dans creative/) : capture de la fixture dans public/rushes/, rendu des deux toiles par render.mjs, ffprobe (H.264, yuv420p, 1080×1920 / 1920×1080, 60 fps, autant d'images que le rush, trois étiquettes bt709), puis une image à mi-plan doit contenir la barre de la fixture. Opt-in comme CINEMA_CAPTURE_E2E |
| Verrou couleur tenu | remotion.config.ts réécrit la VUI H.264 au stitch (h264_metadata) : avant, primaries et transfer sortaient unknown sur tout rendu Remotion |
| Une seule règle d'images | frameTimes vit dans src/lib/motion/cinema/frame-times.mjs, importé par l'app et par capture-video.mjs ; frame-times.test.ts et pipeline.test.mjs exigent le même objet fonction |
Après 2681
- MP4 + VO + draft Postiz vidéo (TikTok
DIRECT_POST+video_made_with_ai) - Captures produit dans les scènes
Après 2683 / 2684 (livrés)
- Media desk : go humain sur artifacts sans quitter l'admin, rendu et brouillon Postiz vidéo en un clic depuis la même table
- Landscape169, dix primitives, skills Remotion vendorisés (Landscape169 et cinq primitives supprimées depuis par
3036)
Après 2682
- Une campagne → 9:16 + 4:5 + 1:1 + stills
Après spine §14 vivante (+ cockpit déjà livré)
- Lundi : avance / retard par vague et attribution unité mesurée
- Décisions scale basées sur signup/activation, pas sur vanity seule
16. Multi-produit (growth-web/3047, PR 1 de la série)
Le système créatif (creative/ Remotion, le moteur cinéma, le Filmer) était
câblé sur BoostEcom. Le besoin : le même système, au comportement
inchangé, pour d'autres produits : l'extension Chrome « BoostEcom Spy »,
l'app « Easy Connector », un thème Shopify, une app Shopify. Chacun a son
propre dépôt et ses propres règles de design. La règle : chaque produit
possède son système, le Studio s'en nourrit.
Le registre
creative/products.json est la seule liste des produits servis. Données
pures, lues par deux lecteurs du même fichier :
creative/pipeline/lib/products.mjs(Node : pipeline, dériveur de design), qui valide la forme (validateRegistry) et exposeproductFor(id);creative/src/products.ts(TypeScript, bundle Remotion), typé.
Un identifiant inconnu est une ERREUR dans les deux lecteurs, jamais un repli silencieux sur BoostEcom. Forme d'une entrée :
| Champ | Rôle |
|---|---|
id | slug (boostecom) |
name | nom affiché |
repo | owner/name du dépôt qui possède le code et le design du produit |
design.json (+ md, assets, deriver) | le DESIGN.json dérivé du code du produit, et le script qui le dérive |
capture.productionOrigin, capture.stagePath | l'origine https de production et la forme du chemin du plateau (<scene>) |
gallery | où les rendus du produit sont listés. kind: "placeholder" tant que la PR 2 ne l'a pas décidé |
postiz.notes | optionnel : les contraintes de publication du produit |
Seul boostecom est enregistré, et c'est le défaut. Ses valeurs capture
sont celles que allowlist.mjs applique aujourd'hui (le test
creative-products.test.ts vérifie l'égalité) ; l'allowlist ne les lit pas
encore.
Ce que la PR 1 change, et ce qu'elle ne change pas
creative/src/tokens.tschoisit son DESIGN.json par produit :tokensFor(productId)(défautboostecom), ettokenssuit le produit que nomme l'environnement (productFromEnv). Le bundle Remotion ne reçoit que les variablesREMOTION_*: jusqu'à3071,CREATIVE_PRODUCT=spy npx remotion renderrendait BoostEcom sans rien dire.remotion.config.tsla transmet désormais enREMOTION_CREATIVE_PRODUCT, donc un produit inconnu fait échouer le rendu, et deux valeurs différentes sont un refus. Les valeurs BoostEcom sont identiques à l'octet près (vérifié avant/après). L'import d'un DESIGN.json reste statique (Remotion bundle le fichier) : un produit ajoute son import dansDESIGNS, et le test refuse une entrée sans import.derive-design-md.mjs --product <id>: le dériveur BoostEcom refuse un produit qu'il ne possède pas (autredesign.deriver) au lieu d'écrire un design BoostEcom sous un autre nom. Sa sortie BoostEcom est inchangée.- Aucun changement visible : ni rendu, ni capture, ni page.
La suite
- PR 2 : capture, workflow et galerie lus depuis le registre
(
allowlist.mjslitcapture, la galerie par produit remplace leplaceholder). - PR 3 : un paquet cinéma partagé et un plateau HTML générique, pour les produits qui ne sont pas une app Next (extension, thème).
Ajouter un produit
- Dans le dépôt du produit, un dériveur qui lit SON code et écrit
<id>.DESIGN.json(mêmes sections que BoostEcom :social_card,themes.dark,pillars,fonts,motion,radius). - Une entrée dans
creative/products.json(candidats :spy,easy-connector,theme,shopify-app), avec son origine https et son chemin de plateau. - L'import de son DESIGN.json dans
DESIGNS(creative/src/tokens.ts). creative-products.test.tsdoit rester vert.
Dernière mise à jour : 2026-09-26 — 3071 (un film par demande, verrou de rendu par codec, lockfile creative/). Avant : 2026-09-14, closeout 2680 / 2660.