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
- 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>
227 lines
13 KiB
Markdown
227 lines
13 KiB
Markdown
# 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 `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. 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.
|