> Single source of truth for how a FlatRender After Effects template is **authored**, **scanned**, and **rendered (bound)**.
> Both the **scanner** (`scan.jsx` + Go quick-scan) and the **render binder** (the JSX generator) must obey this document.
> Conventions are **versioned** (`convention_version` per project) so the rules can evolve without breaking the existing library.
---
## 0. Project ↔ AEP relationship
- **One project = one `.aep` file** (in V2 stored at `templates/{project_id}/template.aep` or `bundle.zip`).
A real project bundle is usually **a footage folder + the `.aep`**, zipped, and extracted on the render node so relative footage resolves.
- **The project's human name never appears in AE.** Comp/layer names are **universal conventions**, not per-project. The project is identified purely by *which file* it owns.
- **AE version:** the farm always runs the **latest After Effects** (a global setting, not per-project) → Master Properties + Media Replacement are always available.
---
## 1. Naming conventions
### Composition names (universal, identical in every project)
| Comp | Role |
|---|---|
| `frfinal` | Final render comp for **Fix** and **MusicVisualizer** (pre-built). |
| `flatrender` | Final render comp for **Flexible** and **Mockup** — **assembled at render time** by the binder. |
| `frshare` | Holds the **shared colours** (one text layer per colour; layer name = element key, `sourceText` = colour value). |
| `all` | Holds the **shared-layer definitions**. |
### Layer-name prefixes
| Prefix | Meaning |
|---|---|
| `frl_<key>` | Editable **visible** layer (text / media / audio) — what the user fills in. |
| `Share/` | `frshare` comp — `frd_<name>` layers, distinguished **by value**: a text layer holding **RGBA** (4 numbers, e.g. `253,226,228,255`) = a **shared colour**; a layer holding a **single integer 0–3** = a **shared control** (an expression reads it to switch a design variant — e.g. `frd_alladdfill=1` toggles *logo = image* vs *logo = fill-colour overlay*). All user-editable; expressions on the visible layers read these. |
→ scanner derives scenes from the distinct `c<x>` in `frl_c(x)t/m(y)` layer names; element key = the full layer name; type `t`→Text, `m`→Media.
**FLEXIBLE / Mockup** — each scene **is a comp**; editable layers `frl_<key>` inside; story duplicates rename `<scene>_d{N}`.
→ The **scan takes a project-type argument** (defaults to the project's `ChooseMode`) and branches its parsing rule (`FR_SCAN_MODE` → `fix` parses layer names, `flexible` parses comps).
> **No two layers ever share a name.** Every editable field maps to exactly **one** independent value.
- This is why duplication **renames** (see §4): a shallow duplicate would collide names → values *mirror*; renaming keeps names unique → values *independent*.
- Consequence for the **scanner**: just enumerate every unique `frl_`/`frd_` layer and emit one content-element per name — **no repeat-detection needed**.
| **FLEXIBLE** | `flatrender` (assembled) | user story (Normal scenes, duplicated) | `Σ scene + first overlap`, clamped to `project.max` | `CreateFlexibleJSX` | video |
| **MockUp** | `flatrender` (assembled) | one frame per scene | `Duration = sceneCount` (`FrameRate=1`) | `CreateMockupJSX` | **images (JPG/scene)** |
| **VoiceOver** | *(like Fix/MusicViz, timed to a VO track — to confirm)* | fixed | `= VO length`, capped at `project.max` | *tbd* | video |
**Differentiators:***does the user control the timeline?* (Fix=no, Flexible=yes) · *content-swap vs design-placement* (Fix/Flexible vs Mockup) · *audio-driven?* (MusicViz/VoiceOver).
- Free renders use **21 fps** by default — stored per project (`free_fps`), **configurable in project settings**.
- **Maximum frame rate = 60** (any mode/tier).
## 3. Scene types & roles
| `scene_type` | User-pickable? | Role in assembly |
|---|---|---|
| `Normal` | ✅ yes | Story scenes — duplicated into the timeline. |
| `Config` | ❌ no | Global shared layers/config — applied across the whole comp (not on the timeline). |
| `DesignStart` | ❌ no | **BottomDesign** — full-duration **background**, bottom of the layer stack. |
| `DesignEnd` | ❌ no | **TopDesign** — full-duration **overlay** (logo/frame), top of the layer stack. |
Design scenes are **authored once in the template** and **injected** at render time (single instance, full duration — no `_d{N}`).
---
## 4. `flatrender` assembly (Flexible / Mockup)
**Layer stack, bottom → top (z-order = depth):**
```
(global) Config → shared layers/colours applied across
bottom (behind) DesignStart → background, full duration [auto · 1×]
middle (time-line) Normal story → sequenced instances, deep-dup _d{N} [user-picked]
top (in front) DesignEnd → overlay, full duration [auto · 1×]
```
**Story sequencing (Normal scenes), in `Sort` order:**
- Each instance = a **deep-duplicated** scene comp renamed **`<sceneKey>_d{N}`** (`N` = 1-based duplication index). Inner `frl_`/`frd_` names stay identical (each clone is its own namespace).
- Instance layer length `= SceneLength + OverlapAtEnd`; placed at the running time offset; consecutive scenes **cross-overlap** by `OverlapAtEnd`.
- Per-instance inputs are an **ordered list of input-sets**, one per instance (`SceneStory(Name=sceneKey) → Duplications[] → Inputs[]`).
---
## 5. Duration model (universal across modes)
**Per scene:**`default_duration_sec`, `min_duration_sec`, `max_duration_sec`, `overlap_at_end_sec`, `can_handle_duration` (may the user change it?).
*(These four `projects`/`scenes` columns + the studio flag are NOT yet in the schema — added with the audio/binder work.)*
---
## 6. Responsive fit-box (per element)
Each `frl_` element is **its own precomp (a box/region)** with a fixed area; content **auto-fits** inside it.
- **Text:** single-line or multi-line (`TextArea`); auto-scaled down if it overflows (`is_text_box`, `max_size`); wrapped for multi-line.
- **Alignment in box:** `justify` (`LEFT`/`CENTER`/`RIGHT`/`FULL_JUSTIFY`) + `position_in_container` anchors `0–8` (0 = centre, corners/edges).
- **Media:** scale-to-fit the box, keep aspect, centred.
> This is **why deep-dup is heavy**: every element is a nested comp, so a scene is comps-inside-comps; cloning duplicates the whole tree (exactly what v2 avoids).
- **The binder writes only the data sources** (a colour once in `frshare`, a value in a `frd_` layer). Expressions fan it out — it never touches each visible layer.
- **The scanner reads the data sources** (`frshare` layers + `frd_` data layers) as the source of truth for colours/values — **not** the expressions.
- Colours live in `frshare`: layer name = element key, `sourceText` = colour value (hex / `r,g,b`).
---
## 9. Convention versioning + the rule engine
`convention_version` is an **admin-set field on the project** (chosen in the add/edit panel, default = latest). The **rule engine** maps a version → the rules the scanner + binder follow. Both versions are supported (**hybrid**) so nothing breaks.
| Media per instance | duplicated footage | **Media Replacement** essential property |
| Expressions | AE auto-repoints intra-clone expressions | global colours **stay expression-from-shared**; per-instance values flow from the **instance's master property** into the internal expression/fit |
| Cost | N full comp trees (heavy) | tiny project, faster open/render |
**v2 golden rule:***global → expression-from-shared; per-instance → (v1: dup+rename · v2: master-prop feeding the expression/fit).*
### ⚠️ v2 is "validate-first"
Expressions are the #1 thing that complicate Master Properties (expression-driven prop vs master-prop override; expression scope across the master-property boundary). **v1 stays the safe default.** Before re-plumbing to v2, **spike on one real template** to confirm promotion + media replacement + expression scope behave. (Defer to the AE author here.)
Adding a project uses **two zips** with strict conventions:
### Zip 1 — render bundle (AE project)
```
final.aep ← at the zip root
(Footage)/... ← footage folder (name may be "(Footage)" with parens), SIBLING of final.aep
(Footage)/LONG VERSION/Items/SFX1..n.mp3 ← SFX are footage the AEP references (NOT separate assets)
```
-`final.aep` + the footage folder **must be siblings** so AE resolves relative paths. Footage-folder name match is case-insensitive and accepts `Footage` or `(Footage)`.
- Stored at `templates/{project_id}/bundle.zip`; the node extracts it keeping the tree intact and runs `aerender`**from the `.aep`'s folder** → footage is beside it → no "missing footage".
- **Validation on upload:** the zip must contain `final.aep` (prefer this name) with a sibling footage folder. Reject otherwise.
- The scanner reads `final.aep` from this same bundle.
- **SFX** ship *inside* this footage; they are not in the assets bundle.
**Music rule:** the project's default music is the one `.mp3` in the assets bundle whose name is **not**`sfx`. (SFX itself comes from the render footage.)
**Flow:** render bundle → scan → scenes created → assets bundle ingested → each asset uploaded to storage and its field set, mapping `s{i}` to the i-th scene by sort. (Assets-bundle ingestion is a separate admin feature — TODO.)