# FlatRender — AEP Template Convention & Rule Engine

> 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. |
| `frd_<key>` | **Data / direction** layer — hidden values, checkbox/dropdown/fill, colour data, RTL companion. |
| `frc_<key>` | **Container comp** used for duplication (scenes, repeat boxes). |
| `frs_<key>` | **Shared-ref** layer inside an `frc_` container. |
| `frd_<key>d` | Direction companion of an `frl_` layer (`0`=LTR, `1`=RTL), produced by `FRDMaker(key)+"d"`. |

### Key normalization (legacy `Helper`)
- `FRLMaker(key)` → if it already contains `frl`/`frd`, keep; else `frl_<key>`.
- `FRDMaker(key)` → strip `frl_`, ensure `frd` → `frd_<key>`.

### Naming differs by PROJECT TYPE ⚠️ (scanner must be told the type)

**Only two layer kinds:** `t` = text · `m` = media. In AE image / video / audio are the **same** footage (AVLayer), so all three are `m`.

**FIX / MusicVisualizer** — no per-scene `frc_` comps. AE project-panel folders:
| Folder | Holds |
|---|---|
| `Final/` | `frfinal` — the mother render comp |
| `Edit/` | editable comps (any name); layers `frl_c(x)t(y)` (text) / `frl_c(x)m(y)` (media). `c(x)` = scene no., `(y)` = element index |
| `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. |
| `Other/` | footage (video/image) files |

→ 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).

### 🔑 The uniqueness invariant
> **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**.

---

## 2. Project modes (5)

| Mode | Render comp | Timeline source | Duration | Binder generator | Output |
|---|---|---|---|---|---|
| **FIX** | `frfinal` | fixed (template) | `= project_duration_sec` | `CreateV2FixJSX` | video |
| **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)** |
| **MusicVisualizer** | `frfinal` | fixed (template) | `min(audioLen, project.max)` | `CreateV2FixJSX` | video |
| **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).

---

## 2.1 Output spec — aspects · quality · frame rate

**Aspects (video) — only these three:**
| Aspect | Dimensions |
|---|---|
| `16:9` | 1920 × 1080 |
| `1:1` | 1080 × 1080 |
| `9:16` | 1080 × 1920 |

**Render quality tiers (output resolution, chosen per render):** `360p` · `480p` · `720p` · `1080p` · `4K`.
- This is a **render-time** output tier (downscales the comp), distinct from the project's native design resolution.
- The project's `Resolution` acts as the **ceiling**; the user picks a tier ≤ ceiling, also bounded by their plan.

**Free vs paid:**
| | Free | Paid |
|---|---|---|
| Max quality | **360p** | up to project ceiling (4K) |
| Watermark | **yes** | no |
| Frame rate | **21 fps** (default, per-project configurable) | project fps |

**Frame rate:**
- 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?).
**Per project:** `project_duration_sec` (default/target), `min_duration_sec`, `max_duration_sec` (**hard output cap**).

**Flexible:**
```
each scene:  min ≤ SceneLength ≤ max
total     =  Σ SceneLength (+ first scene overlap)
clamp     =  min(total, project.max_duration_sec)        ; preview hard-capped at 180s
```

**MusicVisualizer / audio modes:**
```
audioLen ≤ max → duration = audioLen          (no min floor; short audio = short video)
audioLen > max → user trims (≤ max, decrease-only)   OR   untrimmed → max + fade-out
```
- Trimmer window is **capped at `project.max`** (can shrink, never exceed).
- Cap is enforced in **both** frontend (UX) and backend (authority — never trust the client).

**Fade-out (audio modes):** default **1.5 s**, admin-configurable, studio-toggleable.
```
projects.audio_fade_out_sec        default 1.5            (admin default duration)
scenes.audio_fade_out_sec          nullable override      (per-item)
projects.audio_fade_out_enabled    default on             (admin default state)
projects.is_audio_fade_changeable  default true           (may the studio user toggle?)
studio: fade_out_enabled           user choice            (only if changeable)

→ fadeEnabled  = is_audio_fade_changeable ? userChoice : project.audio_fade_out_enabled
→ fadeDuration = item.audio_fade_out_sec ?? project.audio_fade_out_sec ?? 1.5
```
*(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).

---

## 7. Input types (scanner classification targets)

| Input type | `content_element_type` | AE detection |
|---|---|---|
| image input | `Media` | footage/placeholder layer |
| image / video input | `Media` (`video_support`) | footage layer |
| text / multi-text input | `Text` / `TextArea` | text layer (single vs multi-line box) |
| text input | `Text` | text layer |
| number input | `Number` | text layer (numeric) |
| option input (list) | `DropDown` | data layer + `mapped_list` |
| yes / no input | `CheckBox` / `Toggle` | data layer (0/1) |

The detection walks layers **top → bottom**, classifies each editable layer into one of these, and emits scene content-elements in that order.

---

## 8. Expression-driven data flow ⚠️

Visible layers do **not** hold values — they run **AE expressions** that read from a **central source**:
```
binder writes ─►  frshare colour layers  +  frd_ data layers   ─►  expressions  ─►  visible frl_ layers
                  (one write)                                       (propagate automatically)
```
- **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.

| Aspect | **v1 — legacy (current)** | **v2 — Master Properties (proposed)** |
|---|---|---|
| Per-instance independence | **deep-dup + rename `_d{N}`** | **1 source comp + N layer instances**, each with **Master/Essential-Property overrides** |
| Scan target | layer-name prefixes (`frl_`/`frd_`) | `comp.masterProperties` / Essential Graphics (typed + named) |
| 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 |

**Shared rules (both versions):** §1 naming · §3 scene-type roles · §4 z-order assembly · §5 duration/overlap/fade · §6 fit-box · §7 input types · §8 expression data flow.

**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.)

---

## 10. What this drives in the V2 codebase

- **`projects.convention_version`** (+ admin panel selector) — *to add.*
- **Scanner** (`scan.jsx` / Go quick-scan) — branch on version: v1 reads names, v2 reads Essential Graphics. Emits the canonical `ScanResult` the importer consumes.
- **Render binder** — *not yet built.* Hybrid generator: v1 = deep-dup + rename + bind data sources; v2 = layer instances + master-prop overrides. Per-mode (`CreateV2FixJSX` / `CreateFlexibleJSX` / `CreateMockupJSX` equivalents). This is the port of the legacy `JSXGenerator.cs`.
- **Audio/duration layer** — `projects`/`scenes` fade + duration columns, trimmer-cap-to-max, MusicViz resolution, fade-out. (One migration, one rebuild.)
- **Dynamic scene generation** — the end goal: given business + logic, select/assemble/author scenes against these rules.

---

## 11.5 Project import bundles (admin upload)

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.

### Zip 2 — assets bundle (demos / placeholders / colour SVGs / music)
May be wrapped in a top folder (e.g. `New folder/`) → **match by basename**, strip leading dirs.
`s{i}` = the i-th scene by sort order.

| File | Meaning | Target |
|---|---|---|
| `p.jpg` | project image / thumbnail | Project image |
| `p.mp4` | full project demo (audio, 1080) | Project full demo |
| `p.svg` | project colour SVG | Project colour SVG |
| `demo.mp4` | hover preview loop (on the card) | Project hover/mini demo |
| `<name>.mp3` | **the single non-`sfx` mp3** = default **music** (arbitrary name, e.g. `Playful Ink Reveal.mp3`) | Project default music |
| `s1.mp4 … s(n).mp4` | per-scene loop demos | `Scene[i].Demo` |
| `s1.jpg … s(n).jpg` | per-scene placeholder images | `Scene[i].Image` |
| `s1.svg … s(n).svg` | per-scene colour SVG (optional) | `Scene[i].SceneColorSvg` |

**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.)

## 11. Open items to confirm / validate
- [ ] VoiceOver mode mechanics (separate mode vs flavor of Fix/Flexible).
- [ ] Exact `flatrender` timeline offset formula (cumulative `Duration − overlap`).
- [ ] v2 Master-Properties spike on a real template (expressions + media replacement + scope).
- [ ] `frfinal` vs `flatrender` — confirm `flatrender` is always the *assembled* comp (built by the binder) for Flexible/Mockup.
