- 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>
13 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>.
🔑 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. 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.