- 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>
17 KiB
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_versionper project) so the rules can evolve without breaking the existing library.
0. Project ↔ AEP relationship
- One project = one
.aepfile (in V2 stored attemplates/{project_id}/template.aeporbundle.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 containsfrl/frd, keep; elsefrl_<key>.FRDMaker(key)→ stripfrl_, ensurefrd→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
Resolutionacts 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). Innerfrl_/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 byOverlapAtEnd. - 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_containeranchors0–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 afrd_layer). Expressions fan it out — it never touches each visible layer. - The scanner reads the data sources (
frsharelayers +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 canonicalScanResultthe 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/CreateMockupJSXequivalents). This is the port of the legacyJSXGenerator.cs. - Audio/duration layer —
projects/scenesfade + 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 acceptsFootageor(Footage).- Stored at
templates/{project_id}/bundle.zip; the node extracts it keeping the tree intact and runsaerenderfrom 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.aepfrom 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
flatrendertimeline offset formula (cumulativeDuration − overlap). - v2 Master-Properties spike on a real template (expressions + media replacement + scope).
frfinalvsflatrender— confirmflatrenderis always the assembled comp (built by the binder) for Flexible/Mockup.