Files
flatrender/docs/aep-template-convention.md
T
soroush.asadi ee670552a8
Build backend images / build content-svc (push) Failing after 1s
Build backend images / build file-svc (push) Failing after 0s
Build backend images / build gateway (push) Failing after 0s
Build backend images / build identity-svc (push) Failing after 0s
Build backend images / build notification-svc (push) Failing after 1s
Build backend images / build render-svc (push) Failing after 2s
Build backend images / build studio-svc (push) Failing after 0s
feat: cross-aspect project duplication + AEP convention/rule-engine spec
- content-svc: DuplicateProjectAsync clones full scene/element/colour graph
  (identical keys, new dimensions/aspect; AEP intentionally not copied;
  starts unpublished) + POST /v1/projects/{id}/duplicate.
- admin: «تکثیر» button + modal on each project row; aspects reduced to
  supported 16:9/1:1/9:16; free fps default 21 (clamped 1-60).
- docs/aep-template-convention.md: versioned (v1/v2) convention + rule-engine
  spec — modes, scene types, flatrender assembly, duration/fade model,
  fit-box, input types, expression-driven data flow, output spec.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 16:59:23 +03:30

227 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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>`.
### 🔑 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 `08` (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. 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.