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
- 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>
280 lines
17 KiB
Markdown
280 lines
17 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>`.
|
||
|
||
### 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.
|