Files
flatrender/docs/aep-template-convention.md
soroush.asadi 0a7dd9b84c
Build backend images / build content-svc (push) Failing after 45s
Build backend images / build file-svc (push) Failing after 55s
Build backend images / build gateway (push) Failing after 53s
Build backend images / build identity-svc (push) Failing after 54s
Build backend images / build notification-svc (push) Failing after 53s
Build backend images / build render-svc (push) Failing after 47s
Build backend images / build studio-svc (push) Failing after 51s
feat(nodes): live CPU/RAM/disk monitoring in the node list
- node-agent: internal/metrics — read CPU% (GetSystemTimes), RAM (GlobalMemoryStatusEx),
  disk used%/total (GetDiskFreeSpaceEx) via stdlib kernel32 (no external dep; windows
  build + non-windows stub). Heartbeat now reports cpu_pct/ram_available_mb/disk_used_pct/
  disk_total_gb + ae_running.
- render-svc: heartbeat persists last_disk_pct + disk_total_gb (migration 29); RenderNode
  model + node SELECT/scan carry them.
- admin: rewrite NodesTable to the real RenderNode shape (fixes a pre-existing items/V2Node
  mismatch that left the list empty) + a CPU/RAM/disk bars column + stale-heartbeat flag.
- assets-bundle ingestion: ProjectMediaBundle (jszip) auto-maps project.zip → project/scene
  image/demo/colour + music; PatchProject gains image/full_demo/shared_colors_svg.
- scan: RGBA (4-number) colours recognised + frshare single-int controls detected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:01:18 +03:30

280 lines
17 KiB
Markdown
Raw Permalink 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>`.
### 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 03** = 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 `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.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.