playrig 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +106 -0
- package/LICENSE +37 -0
- package/README.md +77 -2
- package/bin/playrig.js +4 -0
- package/editions/ae/README.md +144 -0
- package/editions/ae/client/aeb.js +9 -0
- package/editions/ae/examples/hello.jsx +3 -0
- package/editions/ae/examples/inspect.jsx +7 -0
- package/editions/ae/examples/snap.jsx +15 -0
- package/editions/ae/lib/actions.json +159 -0
- package/editions/ae/lib/bridge-api.jsxinc +474 -0
- package/editions/ae/lib/dev.jsxinc +21 -0
- package/editions/ae/lib/icons/README.md +4 -0
- package/editions/ae/lib/icons/dot-error.svg +3 -0
- package/editions/ae/lib/icons/dot-idle.svg +3 -0
- package/editions/ae/lib/icons/dot-running.svg +3 -0
- package/editions/ae/lib/icons/make-png.js +25 -0
- package/editions/ae/lib/icons/png/dot-error.png +0 -0
- package/editions/ae/lib/icons/png/dot-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle.png +0 -0
- package/editions/ae/lib/icons/png/dot-idle@2x.png +0 -0
- package/editions/ae/lib/icons/png/dot-running.png +0 -0
- package/editions/ae/lib/icons/png/dot-running@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-off.png +0 -0
- package/editions/ae/lib/icons/png/power-off@2x.png +0 -0
- package/editions/ae/lib/icons/png/power-on.png +0 -0
- package/editions/ae/lib/icons/png/power-on@2x.png +0 -0
- package/editions/ae/lib/icons/png/settings.png +0 -0
- package/editions/ae/lib/icons/png/settings@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-error.png +0 -0
- package/editions/ae/lib/icons/png/status-error@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-ok.png +0 -0
- package/editions/ae/lib/icons/png/status-ok@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected.png +0 -0
- package/editions/ae/lib/icons/png/status-rejected@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout.png +0 -0
- package/editions/ae/lib/icons/png/status-timeout@2x.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown.png +0 -0
- package/editions/ae/lib/icons/png/status-unknown@2x.png +0 -0
- package/editions/ae/lib/icons/power-off.svg +4 -0
- package/editions/ae/lib/icons/power-on.svg +4 -0
- package/editions/ae/lib/icons/settings.svg +4 -0
- package/editions/ae/lib/icons/status-error.svg +4 -0
- package/editions/ae/lib/icons/status-ok.svg +4 -0
- package/editions/ae/lib/icons/status-rejected.svg +4 -0
- package/editions/ae/lib/icons/status-timeout.svg +4 -0
- package/editions/ae/lib/icons/status-unknown.svg +5 -0
- package/editions/ae/lib/panel-core.jsxinc +249 -0
- package/editions/ae/lib/update-key.pem +11 -0
- package/editions/ae/lib/updater.jsxinc +148 -0
- package/editions/ae/library-starter/INDEX.md +5 -0
- package/editions/ae/library-starter/README.md +161 -0
- package/editions/ae/library-starter/_common.jsx +187 -0
- package/editions/ae/library-starter/categories.json +9 -0
- package/editions/ae/library-starter/recipes/.gitkeep +0 -0
- package/editions/ae/package.json +7 -0
- package/editions/ae/panel/Playrig.jsx +39 -0
- package/editions/ae/panel/core.jsx +1026 -0
- package/editions/ae/skills/create-ae-recipe/SKILL.md +85 -0
- package/editions/ae/skills/create-ae-recipe/references/ae-patterns.md +88 -0
- package/editions/ae/skills/create-ae-recipe/references/video-analysis.md +72 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media +5 -0
- package/editions/ae/skills/create-ae-recipe/scripts/media.py +253 -0
- package/editions/ae/skills/create-ae-recipe/scripts/setup.sh +17 -0
- package/editions/ae/skills/create-ae-video/SKILL.md +88 -0
- package/editions/ae/skills/create-ae-video/references/templates.md +90 -0
- package/editions/ae/skills/create-ae-video/scripts/preflight.js +99 -0
- package/editions/ae/skills/use-ae-recipes/SKILL.md +99 -0
- package/editions/ae/skills/use-ae-recipes/scripts/contact-sheet.jsx +10 -0
- package/editions/ae/skills/use-ae-recipes/scripts/edit-text.jsx +12 -0
- package/editions/ae/skills/use-ae-recipes/scripts/remove-comp.jsx +18 -0
- package/editions/pr/README.md +60 -0
- package/editions/pr/SCORE_PROCESS.md +41 -0
- package/editions/pr/client/aeb.js +9 -0
- package/editions/pr/examples/frame.js +6 -0
- package/editions/pr/examples/hello.js +4 -0
- package/editions/pr/organize.default.json +245 -0
- package/editions/pr/package.json +7 -0
- package/editions/pr/plugin/actions.js +79 -0
- package/editions/pr/plugin/dev.js +19 -0
- package/editions/pr/plugin/icons/README.md +4 -0
- package/editions/pr/plugin/icons/file-code.svg +6 -0
- package/editions/pr/plugin/icons/folder.svg +3 -0
- package/editions/pr/plugin/icons/power-off.svg +4 -0
- package/editions/pr/plugin/icons/power-on.svg +4 -0
- package/editions/pr/plugin/icons/settings.svg +4 -0
- package/editions/pr/plugin/icons/status-error.svg +4 -0
- package/editions/pr/plugin/icons/status-ok.svg +4 -0
- package/editions/pr/plugin/icons/status-rejected.svg +4 -0
- package/editions/pr/plugin/icons/status-timeout.svg +4 -0
- package/editions/pr/plugin/icons/status-unknown.svg +5 -0
- package/editions/pr/plugin/incremental.js +212 -0
- package/editions/pr/plugin/index.html +195 -0
- package/editions/pr/plugin/index.js +710 -0
- package/editions/pr/plugin/manifest.json +45 -0
- package/editions/pr/plugin/organize.js +223 -0
- package/editions/pr/skills/score-video/SKILL.md +69 -0
- package/editions/pr/skills/score-video/scripts/mix.py +112 -0
- package/editions/pr/skills/score-video/scripts/place.js +63 -0
- package/lib/ae.js +754 -0
- package/lib/common.js +132 -0
- package/lib/install.js +148 -0
- package/lib/main.js +107 -0
- package/lib/payload.js +60 -0
- package/lib/pr.js +239 -0
- package/lib/update.js +118 -0
- package/package.json +35 -4
- package/skills/create-video/SKILL.md +72 -0
- package/skills/create-video/references/lessons.md +31 -0
- package/skills/create-video/references/project-conventions.md +46 -0
- package/skills/create-video/scripts/ae/list-project.jsx +8 -0
- package/skills/create-video/scripts/ae/move-chips.jsx +14 -0
- package/skills/create-video/scripts/ae/organize-project.jsx +25 -0
- package/skills/create-video/scripts/ae/quadrant-chips.jsx +143 -0
- package/skills/create-video/scripts/ae/render-comps.jsx +16 -0
- package/skills/create-video/scripts/premiere/export-sequences.js +22 -0
- package/skills/create-video/scripts/premiere/make-sequences.js +24 -0
- package/skills/create-video/scripts/premiere/swap-media.js +43 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-ae-recipe
|
|
3
|
+
description: Add a new recipe (reusable, parameterised After Effects effect, element, camera move, transition or scene) to the motion library in <library>/. Works from a text description, iterating with visual previews until the user approves, or from a reference video that is analysed frame by frame and converted into a recipe. Use when the user wants to create, add, capture, recreate or "turn into a recipe" an animation or effect.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# create-ae-recipe
|
|
7
|
+
|
|
8
|
+
You add one recipe at a time to `<library>/` (one Markdown file per recipe, see `<library>/README.md`). Recipes run in the user's **open** After Effects project through the Playrig for After Effects, so the work is: build it, **look at it**, show the user, iterate, then save it cleanly.
|
|
9
|
+
|
|
10
|
+
Two entry points, same finish:
|
|
11
|
+
|
|
12
|
+
- **A. From a description** → draft, preview, iterate with the user.
|
|
13
|
+
- **B. From a reference video** → break it down frame by frame, measure it, rebuild it, compare against the reference.
|
|
14
|
+
- **C. Finalise** (both) → generalise params, document tweaks, lint, save previews, index, clean up.
|
|
15
|
+
|
|
16
|
+
## 0. Setup and safety (always)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
.claude/skills/create-ae-recipe/scripts/setup.sh # once: ffmpeg + Pillow venv (idempotent)
|
|
20
|
+
playrig ae scratch start # remember what's in the project before you add anything
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- After Effects must be open with the Playrig for After Effects panel started (status `Idle`). If a job times out, tell the user to start the panel.
|
|
24
|
+
- You are working inside the user's real project. **All experiments go in comps named `SCRATCH-<id>-v<n>`.** Never modify their existing layers/comps. Never save the project. Never run `scratch clean --yes` before reading its list and confirming every item is yours.
|
|
25
|
+
- Each Playrig job is one Undo step; the user can Edit > Undo.
|
|
26
|
+
- Fonts: check the requested font is installed (`textInfo.font` via inspect, or a visible substitution in the preview). Default is Inter.
|
|
27
|
+
|
|
28
|
+
Media tools (video/image): `.claude/skills/create-ae-recipe/scripts/media` (alias `M` below). `M --help` lists them.
|
|
29
|
+
|
|
30
|
+
## 1. Check the library first
|
|
31
|
+
|
|
32
|
+
Read **only** `<library>/INDEX.md`. If an existing recipe already covers (most of) the request, say so and offer: use it as is / extend it with new params / make a variant. Look at `composes_with` — many "new" looks are a chain of existing recipes. Decide the recipe boundary: **one effect per recipe**; a whole scene is a chain, not a recipe.
|
|
33
|
+
|
|
34
|
+
## A. From a description
|
|
35
|
+
|
|
36
|
+
1. **Clarify minimally.** Ask at most 3 questions, only those that change the build (light or dark background? what text/labels? duration/size?). Otherwise pick defaults and say what you assumed.
|
|
37
|
+
2. **Plan out loud in 5 lines:** recipe id (`lowercase-kebab`), category, what it builds, the 5-10 parameters you will expose, what it needs (fonts, comp order).
|
|
38
|
+
3. **Draft:** `playrig ae lib new <category>/<id>` (creates an `experimental` recipe). Write the `jsx` block using `references/ae-patterns.md` (matchNames, expression templates, ES3 rules, easing).
|
|
39
|
+
4. **Preview:** `playrig ae lib run <id> --param comp=SCRATCH-<id>-v1` renders the frames listed in `preview_times`. For motion, pick times that show the *sequence* (start, early, mid, settle) and combine them: `M sheet <png paths…> --cols 2 --out /tmp/<id>-v1.png`. **Read the image yourself first** and fix obvious defects (wrong order, clipped, invisible text, off-screen) before showing the user.
|
|
40
|
+
5. **Show the user** the sheet (give the file path; describe what they should be seeing), and ask for *concrete* feedback: timing, size, colour, amount of motion, anything missing. Offer 2-3 quick variants when taste is the question ("tighter vs floatier"): render them side by side.
|
|
41
|
+
6. **Iterate:** edit the recipe or pass different `--param`s, `playrig ae scratch clean --yes` (after checking its list), re-run as `-v2`, `-v3`. Keep a one-line changelog of what changed and why; it becomes "Gotchas"/"Tweaks".
|
|
42
|
+
7. When the user says it's right → **C. Finalise**.
|
|
43
|
+
|
|
44
|
+
## B. From a reference video
|
|
45
|
+
|
|
46
|
+
Goal: understand *what is built and how it moves*, then rebuild it parametrically, **not** to clone pixels.
|
|
47
|
+
|
|
48
|
+
1. **Probe:** `M info <video>` (fps, size, duration). `M cuts <video>` (hard cuts only: dissolves/overlaps are not detected). Note fps: express timings in frames at that fps.
|
|
49
|
+
2. **Overview:** `M frames <video> --fps 4 --out /tmp/ref` then `M sheet /tmp/ref --cols 3` (labelled contact sheets). Read **every** sheet. Write a shot list: time range, what's on screen, background, transition into the shot.
|
|
50
|
+
3. **Scope with the user:** list the distinct, reusable pieces you see (e.g. "per-word text fly-in", "chip pops", "CMY split exit") and recommend which to capture. Ask which one(s). Capture one recipe per piece; remember the rest as `planned` stubs if the user wants them tracked.
|
|
51
|
+
4. **Measure the chosen piece** (see `references/video-analysis.md` for the full checklist):
|
|
52
|
+
- Frame-accurate view: `M frames <video> --start S --end E --every-frame --out /tmp/win` → `M sheet /tmp/win --cols 4 --cell 480`.
|
|
53
|
+
- Timing and easing: `M activity <video> --start S --end E [--region x,y,w,h]`. Burst starts/ends = animation start/duration; decay shape = easing; gaps between bursts = stagger spacing.
|
|
54
|
+
- Colours: `M palette <frame>`, `M color <frame> x,y --box 5` (coordinates are in the frame's pixels; scale to 1920×1080 if the video is smaller).
|
|
55
|
+
- Detail: `M crop <frame> x y w h --scale 3` to read type weight, stroke width, corner radius, handles.
|
|
56
|
+
5. **Hypothesise the build** in After Effects terms (layers, text animators, expressions, parenting, camera) and state it. If something is ambiguous (random vs designed, 2D vs 3D) test the cheapest hypothesis first.
|
|
57
|
+
6. **Rebuild** as an `experimental` recipe (`lib new`), run it in `SCRATCH-<id>-vN`, rendering at the **same timestamps** as the reference frames you studied: `playrig ae lib run <id> --param comp=… --snap "SCRATCH-<id>-v1@t1,t2,t3"` (or set `preview_times`).
|
|
58
|
+
7. **Compare:** `M compare <ref frame> <our frame> --diff --out /tmp/cmp-t.png` for 3-4 key times. Read them. Fix the largest differences first (timing → position → size → colour → polish). Stop when it reads the same at normal speed; exact pixels are not the goal.
|
|
59
|
+
8. **Show the user** comparisons (reference | ours) and ask what to adjust. Iterate as in A.6.
|
|
60
|
+
9. → **C. Finalise.** Credit the source in `origin:` (e.g. "reference video: <name>, 3.2-4.1 s").
|
|
61
|
+
|
|
62
|
+
## C. Finalise
|
|
63
|
+
|
|
64
|
+
1. **Generalise.** Turn every magic number into a parameter named for what it does visually (`staggerFrames`, `popFrom`), with units in `desc` and sensible `min`/`max`. Defaults = the approved look. Keep to 5-15 params; fold the rest into constants. Include `comp` (always) and `width/height/duration/fps` only if the recipe can create the comp.
|
|
65
|
+
2. **Prove the tweaks.** For the 3-5 most important params, render a low and a high value and look. Write "Tweaks that matter" from what you saw, not from guesses.
|
|
66
|
+
3. **Document** the file: `summary` (≤150 chars, one sentence), `use_when`, `tags`, `aliases` (names people will actually say, incl. industry terms), `composes_with` (existing ids), `origin`, and the sections *What it looks like / How it works / Tweaks that matter / Gotchas / Combine with*. Gotchas = ordering rules, requirements (fonts, light backgrounds), limits, and anything you hit while iterating.
|
|
67
|
+
4. **Check replaceability.** Run the recipe twice on a scratch comp, the second time with `--replace` (and different params): the comp must end with one clean set of layers in the same stacking position. If the recipe changes *existing* layers (parents, 3D flags, deletes or replaces them), set `idempotent: false` and write a `replace_note` that says how to change it instead.
|
|
68
|
+
5. **Check:** `playrig ae lib check` must show 0 errors (fix warnings you can).
|
|
69
|
+
6. **Previews + status:** `playrig ae lib run <id> --param comp=SCRATCH-<id>-final --save-preview` (stores images next to the recipe and embeds them), then set `status: ready` in the frontmatter.
|
|
70
|
+
7. **Index:** `playrig ae lib index`. Confirm the new line reads well in `<library>/INDEX.md`.
|
|
71
|
+
8. **Clean up:** `playrig ae scratch clean` → read the list → `scratch clean --yes`. Confirm the item count is back to baseline.
|
|
72
|
+
9. **Report:** id, what it does, params, what you could not capture or assumed, and how to run it. Don't commit unless asked.
|
|
73
|
+
|
|
74
|
+
## Quality bar
|
|
75
|
+
|
|
76
|
+
- It was **run in After Effects and looked at**, at least at start/mid/end of the motion. "Status ok" is not evidence the picture is right.
|
|
77
|
+
- Matchnames only; ES3 only; no reliance on any specific project; self-contained expressions.
|
|
78
|
+
- Order-dependent behaviour is in Gotchas.
|
|
79
|
+
- A stranger could pick it from the index line, tune it from the Tweaks table, and run it without opening the script.
|
|
80
|
+
|
|
81
|
+
## References (load on demand)
|
|
82
|
+
|
|
83
|
+
- `references/ae-patterns.md`: matchNames, expression templates (stagger, easing, seeded random), keyframe/ease API, 3D/parenting pitfalls, ES3 rules, helper functions.
|
|
84
|
+
- `references/video-analysis.md`: the frame-by-frame breakdown checklist, effect fingerprints, how to read easing/stagger from motion data, what can't be inferred.
|
|
85
|
+
- `<library>/README.md`: the recipe file format and conventions. `<library>/_common.jsx`: helpers appended to every recipe (`libText`, `libTextAnimator`, `libKey`, …).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# After Effects patterns for recipes
|
|
2
|
+
|
|
3
|
+
Everything here was verified in After Effects 26.5 through the Playrig for After Effects while building the library. Scripts are **ExtendScript (ES3)**.
|
|
4
|
+
|
|
5
|
+
## ES3 rules (the linter `playrig ae lib check` enforces most)
|
|
6
|
+
|
|
7
|
+
No `let`/`const`, arrow functions, template literals, `JSON`, `Array.indexOf/forEach/map/filter`, `Math.sign`, `String.trim`. Use `for` loops, `var`, string concatenation. Helper for membership: loop and compare. Function declarations are hoisted, which is how `_common.jsx` helpers are available from line 1.
|
|
8
|
+
|
|
9
|
+
Parameters arrive as `P.<name>` (defaults merged by the CLI). Don't redefine `P`.
|
|
10
|
+
|
|
11
|
+
## matchNames (never display names: they break in non-English AE)
|
|
12
|
+
|
|
13
|
+
| Thing | matchName |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Transform group | `ADBE Transform Group` → `ADBE Anchor Point`, `ADBE Position` (`ADBE Position_0/1/2` if separated), `ADBE Scale`, `ADBE Rotate Z` (2D) / `ADBE Rotate X/Y/Z`, `ADBE Orientation`, `ADBE Opacity` |
|
|
16
|
+
| Camera | `ADBE Camera Options Group` → `ADBE Camera Zoom`; transform `ADBE Point of Interest` **absent on one-node cameras: null-check it** |
|
|
17
|
+
| Effects | `ADBE Effect Parade` → add with `ADBE Ramp` (Gradient Ramp), `ADBE Grid`, `ADBE Tint`, ... |
|
|
18
|
+
| Gradient Ramp props | `ADBE Ramp-0001` start, `-0002` start colour, `-0003` end, `-0004` end colour, `-0005` shape (1 linear, 2 radial), `-0006` scatter, `-0007` blend |
|
|
19
|
+
| Grid props | `ADBE Grid-0001` anchor, `-0002` size from (3 = width & height sliders), `-0004` width, `-0005` height, `-0006` border, `-0012` colour |
|
|
20
|
+
| Tint props | `ADBE Tint-0001` map black, `-0002` map white, `-0003` amount |
|
|
21
|
+
| Text | `ADBE Text Properties` → `ADBE Text Document` (source text), `ADBE Text Animators` |
|
|
22
|
+
| Text animator | `ADBE Text Animator` → `ADBE Text Selectors`, `ADBE Text Animator Properties` |
|
|
23
|
+
| Selectors | `ADBE Text Selector` (range), `ADBE Text Expressible Selector` (`ADBE Text Range Type2` based-on: 1 chars, 2 chars excl. spaces, 3 words, 4 lines; `ADBE Text Expressible Amount`), `ADBE Text Wiggly Selector` |
|
|
24
|
+
| Animator props | `ADBE Text Position 3D`, `ADBE Text Scale 3D`, `ADBE Text Rotation` (Z), `ADBE Text Rotation X/Y`, `ADBE Text Opacity`, `ADBE Text Fill Color`, `ADBE Text Skew`, `ADBE Text Blur`, `ADBE Text Tracking Amount` |
|
|
25
|
+
| Shapes | `ADBE Root Vectors Group` → `ADBE Vector Group` → `ADBE Vectors Group`; `ADBE Vector Shape - Rect` (`ADBE Vector Rect Size`, `ADBE Vector Rect Roundness`), `ADBE Vector Shape - Ellipse`, `ADBE Vector Graphic - Fill` (`ADBE Vector Fill Color`), `ADBE Vector Graphic - Stroke` (`ADBE Vector Stroke Color`, `ADBE Vector Stroke Width`) |
|
|
26
|
+
|
|
27
|
+
To discover an unknown matchName: build it by hand in a scratch comp, then walk `layer.property(...)` with `numProperties` / `.matchName` in a bridge job (see how it was done for text animators), or `PLAYRIG.inspect`.
|
|
28
|
+
|
|
29
|
+
## Text
|
|
30
|
+
|
|
31
|
+
- Create: `comp.layers.addText(str)`; style through `TextDocument` (`font` = **PostScript name** e.g. `Inter-ExtraBold`, `fontSize`, `fillColor` 0..1 RGB, `tracking`, `justification`), then `prop.setValue(doc)`. Call `doc.resetCharStyle()` first.
|
|
32
|
+
- Per-range styling: `var r = doc.characterRange(i, j); r.font = "Inter-Bold"; prop.setValue(doc);` (set `doc`, not `r`; `r` is a live view).
|
|
33
|
+
- A scripted animator starts with **no** default Range Selector (`sels.numProperties` is 0): don't call `property(1).remove()` blindly (use a while loop on numProperties).
|
|
34
|
+
- Selector amount expressions **must return an array** `[v, v, v]` (0..100), not a scalar.
|
|
35
|
+
- `layer.sourceRectAtTime(0, false).width` measures text for sizing chips/boxes.
|
|
36
|
+
|
|
37
|
+
## Expression templates (JavaScript engine)
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// frame-based stagger with ease-out (textIndex is 1-based; use unit = word/char via Based On)
|
|
41
|
+
var f = timeToFrames(time - thisLayer.inPoint);
|
|
42
|
+
var s = 1 + (textIndex - 1) * STAGGER;
|
|
43
|
+
var p = clamp((f - s) / MOVE_FRAMES, 0, 1);
|
|
44
|
+
var e = 1 - Math.pow(1 - p, 4); // easeOutQuart (cubic: 3; expo: 1 - Math.pow(2, -10*p))
|
|
45
|
+
var v = (1 - e) * 100; // 100 = fully "away", 0 = settled
|
|
46
|
+
[v, v, v]
|
|
47
|
+
```
|
|
48
|
+
```js
|
|
49
|
+
// arc: path bows mid-flight
|
|
50
|
+
var v = Math.sin(e * Math.PI) * 100; [v, v, v]
|
|
51
|
+
// stable per-letter random (scatter): seed by textIndex so renders are repeatable
|
|
52
|
+
seedRandom(textIndex * 7 + 1, true); var r = random(-1, 1);
|
|
53
|
+
// parent opacity follows another layer
|
|
54
|
+
parent.transform.opacity
|
|
55
|
+
```
|
|
56
|
+
Embed parameters into expressions by building the string in the recipe (`"... * " + P.staggerFrames`). Keep expressions self-contained (no project layer names).
|
|
57
|
+
|
|
58
|
+
## Keyframes and easing
|
|
59
|
+
|
|
60
|
+
- `prop.setValueAtTime(t, value)`; then ease with `prop.setTemporalEaseAtKey(k, [new KeyframeEase(0, inf)], [new KeyframeEase(0, outInf)])`. One ease object per dimension (`libEaseKey` handles this). Influence: 16.7 ≈ linear feel, 33 = classic Easy Ease, 60-85 = strong ease (fast start, long soft landing).
|
|
61
|
+
- Keys set by script are linear by default. Hold = `setInterpolationTypeAtKey(k, KeyframeInterpolationType.HOLD)`.
|
|
62
|
+
- Times are seconds in the comp. Convert frames with `n / comp.frameRate`. Land timings on whole frames.
|
|
63
|
+
- Don't keyframe layers whose `startTime ≠ 0` from scripts: keys come out offset. Put the animation inside the precomp or set `startTime` last and verify `keyTime()`.
|
|
64
|
+
|
|
65
|
+
## Layers, 3D, parenting
|
|
66
|
+
|
|
67
|
+
- New layers are added **on top**. If B must sit above A, add A first, then B, or `B.moveBefore(A)` (chip label above chip).
|
|
68
|
+
- Shape groups: **first group draws on top**. Inside a group: path, then stroke, then fill (stroke drawn above fill).
|
|
69
|
+
- `layer.parent = other` can jump the child's position. Make the parent's anchor = position = comp centre (identity transform) so there's no jump, and set children's local positions **after** parenting.
|
|
70
|
+
- A 3D layer's children must also be 3D to follow it in perspective (a rig recipe has to convert them).
|
|
71
|
+
- 3D layers are skipped by 2D push-in rigs by design; use the camera or the layer's own drift.
|
|
72
|
+
- Property references go stale after `addProperty()`: re-fetch by matchName.
|
|
73
|
+
- Camera: one-node camera by default; position `[cx, cy, -zoom]`, zoom param = px; Z=0 layers stay 1:1.
|
|
74
|
+
- Adjustment layer: add a solid then `layer.adjustmentLayer = true`; effects only affect layers **below**.
|
|
75
|
+
- Blend modes: `layer.blendingMode = BlendingMode.MULTIPLY`. CMY copies on Multiply make black on white backgrounds.
|
|
76
|
+
- Motion blur: `layer.motionBlur = true` per layer and `comp.motionBlur = true`; shutter angle 180 is normal.
|
|
77
|
+
|
|
78
|
+
## Bridge facts that affect recipes
|
|
79
|
+
|
|
80
|
+
- `alert()` is redirected to the log; `confirm()` returns false. No dialogs.
|
|
81
|
+
- Snapshots are taken after the job finishes; first render of a new comp can be slow (the client waits up to 30 s for late files).
|
|
82
|
+
- Fonts that aren't installed are substituted silently: verify `textInfo.font` in `PLAYRIG.inspect`.
|
|
83
|
+
- Each job = one undo step; partial work stays if a job errors halfway (remove with `scratch clean`).
|
|
84
|
+
- Job ids must be unique per run; `lib run` generates them.
|
|
85
|
+
|
|
86
|
+
## Helpers available in every recipe (`<library>/_common.jsx`)
|
|
87
|
+
|
|
88
|
+
`libHex`, `libComp`, `libTargetComp`, `libLayer`, `libRequireLayer`, `libSolid`, `libEaseKey`, `libKey`, `libFrames`, `libList`, `libText`, `libTextAnimator`, `libStyleWord`. Add a helper there (not in a recipe) when two recipes need the same thing, and keep it ES3.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Reading a reference video frame by frame
|
|
2
|
+
|
|
3
|
+
Goal: recover **what is built and how it moves**, in After Effects terms, well enough to rebuild it parametrically. Not pixel cloning.
|
|
4
|
+
|
|
5
|
+
## Normalise first
|
|
6
|
+
|
|
7
|
+
- Note the video's fps and size (`media info`). Express times in **frames at that fps**; motion design lands on small whole numbers of frames (1-4 frame staggers, 6-24 frame moves).
|
|
8
|
+
- Scale measurements to a 1920×1080 comp: `x_comp = x_video * 1920 / video_width`.
|
|
9
|
+
- Social/UI videos are often 30 or 60 fps; recipes default to 24. Convert durations in seconds, then round to frames of the comp you build in.
|
|
10
|
+
|
|
11
|
+
## Pass 1: shot list (contact sheets at ~4 fps)
|
|
12
|
+
|
|
13
|
+
For every shot write: time range, background (light/dark/gradient/grid), what's in frame, type (text, size class, weight), any camera feel (flat, perspective, push-in), and **how it ends** (hard cut / dissolve / overlap / scatter-out). Hard cuts show on `media cuts`; dissolves don't: look for frames that blend two scenes.
|
|
14
|
+
|
|
15
|
+
## Pass 2: per-effect measurement (frame-accurate)
|
|
16
|
+
|
|
17
|
+
Pick the window of one effect: `media frames --start S --end E --every-frame`, then a sheet at 4 columns.
|
|
18
|
+
|
|
19
|
+
| Question | How to answer it |
|
|
20
|
+
|---|---|
|
|
21
|
+
| When does it start/stop? | `media activity --start S --end E` (optionally `--region`): first/last frame above threshold. |
|
|
22
|
+
| Easing? | Shape of the motion score: starts high then decays = ease-out; rises then falls = ease in-out; flat = linear; abrupt jump = hold/cut. Strong ease-out (long tail) = 60-85% influence or ease-out quart/expo. |
|
|
23
|
+
| Stagger? | Repeated bursts; spacing between burst starts, in frames = stagger. Or compare when each item first appears on the frame sheet. |
|
|
24
|
+
| What moves, what doesn't? | Use `--region` on one item vs the background. |
|
|
25
|
+
| Position/size | Read pixel coordinates from frames; `media crop` to zoom. Measure at rest (end of the move) and at start. |
|
|
26
|
+
| Colours | `media palette` for the scene, `media color x,y --box 5` for specific fills/strokes (average a small box to avoid anti-aliasing). |
|
|
27
|
+
| Type | Weight (regular/medium/bold/extrabold), tracking (tight/loose), case, size (cap height in px ≈ 0.7 × font size). Ask the user for the font if you can't tell; default Inter. |
|
|
28
|
+
| Depth/3D? | Foreshortening (shapes getting narrower in one axis), perspective convergence, parallax between layers moving at different speeds → 3D layers + camera. |
|
|
29
|
+
| Motion blur? | Streaks along the motion direction on fast moves → motion blur on (180° shutter). |
|
|
30
|
+
|
|
31
|
+
## Worked example (validated against known ground truth)
|
|
32
|
+
|
|
33
|
+
A recipe with 6 words, 2-frame stagger, 9-frame ease-out was rendered to video and analysed blind:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
M activity video.mp4 # whole frame: one bell-shaped burst, frames 2-19 -> sum of overlapping moves
|
|
37
|
+
M activity video.mp4 --region 150,225,120,90 # FIRST word only: active frames 2-9, peak early, decaying tail -> ease-out, ~8-9 frame move
|
|
38
|
+
M activity video.mp4 --region 690,225,120,90 # LAST word only: active frames 12-19
|
|
39
|
+
```
|
|
40
|
+
stagger = (start_last - start_first) / (n - 1) = (12 - 2) / 5 = **2 frames**; move length ~ 8-9 frames. These matched the recipe's real values (stagger 2, move 9). Use the same trick for any repeated element: isolate the first and last with `--region`.
|
|
41
|
+
Whole-frame activity alone blurs overlapping moves together, so always go to regions once you know where the items are.
|
|
42
|
+
|
|
43
|
+
## Effect fingerprints
|
|
44
|
+
|
|
45
|
+
| You see | Likely build |
|
|
46
|
+
|---|---|
|
|
47
|
+
| Words/letters arriving one after another with rotation/offset | Text animator + Expression Selector (Words/Characters), staggered by frames |
|
|
48
|
+
| Text that is black but splits into cyan/magenta/yellow fringes | Three Multiply copies (Y/M/C) on a light background |
|
|
49
|
+
| Letters flying apart randomly, spinning, growing | Scatter animators with seeded random per letter |
|
|
50
|
+
| Soft bright centre fading to darker edges | Gradient Ramp (radial) |
|
|
51
|
+
| Faint square grid on dark | Grid effect on a solid at low opacity |
|
|
52
|
+
| Whole frame creeping larger over a hold | Controller null scale 100 → ~105% |
|
|
53
|
+
| Panels tilted in perspective, shifting against each other | 3D layers parented to a rotated null + camera |
|
|
54
|
+
| Everything draining to grey then returning | Adjustment layer + Tint amount keyed |
|
|
55
|
+
| Outline boxes with corner handles around letters | Shape layers per glyph, timed with the reveal |
|
|
56
|
+
| Chips/labels popping with a tiny overshoot | Scale keys 70 → 104 → 100, strong ease |
|
|
57
|
+
|
|
58
|
+
## Pass 3: hypothesise, build, compare
|
|
59
|
+
|
|
60
|
+
State the build in layers/animators/expressions. Build the cheapest version, render **at the reference's timestamps**, and `media compare ref ours --diff`. Fix in order: timing → position → size → colour → polish. Compare at *start, early, mid, settle* of the motion, not just the end pose.
|
|
61
|
+
|
|
62
|
+
## What you cannot recover (say so in the recipe)
|
|
63
|
+
|
|
64
|
+
- Randomness: you can see it looks random, not the seed. Match the *character* (range, speed), expose a `seed` param.
|
|
65
|
+
- Expressions/rig wiring: you see the result, not the controls. Rebuild with the simplest equivalent.
|
|
66
|
+
- Fonts, exact easing curves, and subtle colour management: approximate and expose as params.
|
|
67
|
+
- Audio and sync: ignored unless the user asks.
|
|
68
|
+
- Source resolution/compression artefacts: don't reproduce them.
|
|
69
|
+
|
|
70
|
+
## Presenting findings to the user
|
|
71
|
+
|
|
72
|
+
Short and visual: a shot list, one contact sheet of the piece you propose to capture, your build hypothesis in 3-5 lines, the measured numbers (frames, px, colours) you'll use as defaults, and what you're unsure about. Ask only about decisions that change the result.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Wrapper: runs media.py with the skill's venv (falls back to system python3 if Pillow is importable).
|
|
3
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
4
|
+
PY="$HERE/.venv/bin/python"; [ -x "$PY" ] || PY="$(command -v python3)"
|
|
5
|
+
exec "$PY" "$HERE/scripts/media.py" "$@"
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""media.py: video/image helpers for the create-ae-recipe skill.
|
|
3
|
+
|
|
4
|
+
Needs ffmpeg + ffprobe on PATH and Pillow (installed by scripts/setup.sh). Run through scripts/media (wrapper).
|
|
5
|
+
|
|
6
|
+
media check
|
|
7
|
+
media info <video>
|
|
8
|
+
media cuts <video> [--threshold 18]
|
|
9
|
+
media frames <video> [--start S] [--end S] [--fps N | --every-frame | --times t1,t2,..] [--width 960] [--out DIR]
|
|
10
|
+
media sheet <dir-or-files...> [--cols 3] [--cell 640] [--out FILE] [--labels]
|
|
11
|
+
media activity <video> [--start S] [--end S] [--region x,y,w,h] [--threshold T]
|
|
12
|
+
media compare <reference.png> <ours.png> [--out FILE] [--diff]
|
|
13
|
+
media color <image> x,y [x,y ...] [--box W]
|
|
14
|
+
media palette <image> [--n 6]
|
|
15
|
+
media crop <image> x y w h [--scale 2] [--out FILE]
|
|
16
|
+
"""
|
|
17
|
+
import argparse, glob, json, os, re, shutil, subprocess, sys, tempfile
|
|
18
|
+
|
|
19
|
+
def die(msg, code=2):
|
|
20
|
+
print(msg, file=sys.stderr); sys.exit(code)
|
|
21
|
+
|
|
22
|
+
def need(tool):
|
|
23
|
+
if not shutil.which(tool): die(f"{tool} not found. Run scripts/setup.sh")
|
|
24
|
+
|
|
25
|
+
def run(cmd, **kw):
|
|
26
|
+
return subprocess.run(cmd, capture_output=True, text=True, **kw)
|
|
27
|
+
|
|
28
|
+
def pil():
|
|
29
|
+
try:
|
|
30
|
+
from PIL import Image, ImageChops, ImageDraw, ImageFont, ImageStat
|
|
31
|
+
return Image, ImageChops, ImageDraw, ImageFont, ImageStat
|
|
32
|
+
except ImportError:
|
|
33
|
+
die("Pillow missing. Run scripts/setup.sh")
|
|
34
|
+
|
|
35
|
+
def font(size):
|
|
36
|
+
_, _, _, ImageFont, _ = pil()
|
|
37
|
+
for p in ("/System/Library/Fonts/Menlo.ttc", "/System/Library/Fonts/Monaco.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", "C:/Windows/Fonts/consola.ttf"):
|
|
38
|
+
if os.path.exists(p):
|
|
39
|
+
try: return ImageFont.truetype(p, size)
|
|
40
|
+
except OSError: pass
|
|
41
|
+
return ImageFont.load_default()
|
|
42
|
+
|
|
43
|
+
def probe(video):
|
|
44
|
+
need("ffprobe")
|
|
45
|
+
r = run(["ffprobe", "-v", "error", "-print_format", "json", "-show_streams", "-show_format", video])
|
|
46
|
+
if r.returncode: die(r.stderr.strip() or "ffprobe failed")
|
|
47
|
+
j = json.loads(r.stdout)
|
|
48
|
+
v = next((s for s in j["streams"] if s["codec_type"] == "video"), None)
|
|
49
|
+
if not v: die("no video stream")
|
|
50
|
+
num, den = (v.get("avg_frame_rate") or v["r_frame_rate"]).split("/")
|
|
51
|
+
fps = float(num) / float(den) if float(den) else float(v["r_frame_rate"].split("/")[0])
|
|
52
|
+
dur = float(v.get("duration") or j["format"].get("duration") or 0)
|
|
53
|
+
return {"width": v["width"], "height": v["height"], "fps": round(fps, 3), "duration": round(dur, 3),
|
|
54
|
+
"frames": int(v.get("nb_frames") or round(dur * fps)), "codec": v["codec_name"],
|
|
55
|
+
"audio": any(s["codec_type"] == "audio" for s in j["streams"])}
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------- commands
|
|
58
|
+
def cmd_check(a):
|
|
59
|
+
ok = True
|
|
60
|
+
for t in ("ffmpeg", "ffprobe"):
|
|
61
|
+
p = shutil.which(t); print(f"{t}: {p or 'MISSING'}"); ok &= bool(p)
|
|
62
|
+
try: import PIL; print(f"Pillow: {PIL.__version__}")
|
|
63
|
+
except ImportError: print("Pillow: MISSING"); ok = False
|
|
64
|
+
sys.exit(0 if ok else 1)
|
|
65
|
+
|
|
66
|
+
def cmd_info(a):
|
|
67
|
+
i = probe(a.video)
|
|
68
|
+
print(json.dumps(i, indent=2))
|
|
69
|
+
print(f"\n{i['width']}x{i['height']} @ {i['fps']} fps, {i['duration']} s ({i['frames']} frames), {i['codec']}, audio: {i['audio']}")
|
|
70
|
+
|
|
71
|
+
def diff_scores(video, start=None, end=None, region=None, gray=True, width=320):
|
|
72
|
+
"""Per-frame mean absolute difference vs the previous frame (0..255), at the video's own fps."""
|
|
73
|
+
Image, ImageChops, _, _, ImageStat = pil()
|
|
74
|
+
need("ffmpeg")
|
|
75
|
+
fps = probe(video)["fps"]
|
|
76
|
+
fmt = "format=gray" if gray else "format=rgb24"
|
|
77
|
+
vf = f"{'crop=' + ':'.join(region.split(',')[2:] + region.split(',')[:2]) + ',' if region else ''}scale={width}:-2,{fmt}"
|
|
78
|
+
tmp = tempfile.mkdtemp()
|
|
79
|
+
try:
|
|
80
|
+
cmd = ["ffmpeg", "-y", "-v", "error"] + (["-ss", f"{start:.4f}"] if start else []) + ["-i", video] + (["-t", f"{end - (start or 0):.4f}"] if end else []) + ["-vf", f"fps={fps},{vf}", os.path.join(tmp, "g%05d.png")]
|
|
81
|
+
r = run(cmd)
|
|
82
|
+
if r.returncode: die(r.stderr)
|
|
83
|
+
scores = []; prev = None
|
|
84
|
+
for fp in sorted(glob.glob(os.path.join(tmp, "g*.png"))):
|
|
85
|
+
im = Image.open(fp).convert("L" if gray else "RGB")
|
|
86
|
+
scores.append(0.0 if prev is None else sum(ImageStat.Stat(ImageChops.difference(im, prev)).mean) / len(ImageStat.Stat(im).mean))
|
|
87
|
+
prev = im
|
|
88
|
+
finally:
|
|
89
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
90
|
+
return scores, fps
|
|
91
|
+
|
|
92
|
+
def cmd_cuts(a):
|
|
93
|
+
# Colour-aware frame differencing (ffmpeg's luma scene score misses cuts between similar-brightness colours).
|
|
94
|
+
scores, fps = diff_scores(a.video, gray=False, width=160)
|
|
95
|
+
d = len(scores) / fps
|
|
96
|
+
cuts = []
|
|
97
|
+
for i in range(1, len(scores)):
|
|
98
|
+
nb = [scores[j] for j in (i - 2, i - 1, i + 1, i + 2) if 0 <= j < len(scores)]
|
|
99
|
+
if scores[i] >= a.threshold and scores[i] > 2.5 * (max(nb) if nb else 0):
|
|
100
|
+
cuts.append(i / fps)
|
|
101
|
+
bounds = [0.0] + cuts + [d]
|
|
102
|
+
print(f"{len(cuts)} hard cuts (frame-to-frame change >= {a.threshold}, a single-frame spike): {', '.join(f'{t:.3f}s (frame {round(t * fps)})' for t in cuts) or 'none'}")
|
|
103
|
+
print("\nsegments:")
|
|
104
|
+
for i in range(len(bounds) - 1):
|
|
105
|
+
print(f" {i + 1}: {bounds[i]:.2f}s - {bounds[i + 1]:.2f}s ({bounds[i + 1] - bounds[i]:.2f}s)")
|
|
106
|
+
print("\nOnly HARD cuts are detected. Dissolves, overlaps and fast whips are not: confirm on a contact sheet, and use `media activity` to see when things move.")
|
|
107
|
+
|
|
108
|
+
def cmd_frames(a):
|
|
109
|
+
need("ffmpeg")
|
|
110
|
+
info = probe(a.video)
|
|
111
|
+
out = a.out or os.path.join(tempfile.gettempdir(), "ae-recipe-frames", re.sub(r"\W+", "_", os.path.basename(a.video)))
|
|
112
|
+
os.makedirs(out, exist_ok=True)
|
|
113
|
+
for f in glob.glob(os.path.join(out, "t*.png")): os.remove(f)
|
|
114
|
+
vf = f"scale={a.width}:-2"
|
|
115
|
+
if a.times:
|
|
116
|
+
for t in [float(x) for x in a.times.split(",")]:
|
|
117
|
+
r = run(["ffmpeg", "-y", "-v", "error", "-ss", f"{t:.4f}", "-i", a.video, "-frames:v", "1", "-vf", vf, os.path.join(out, f"t{t:08.3f}.png")])
|
|
118
|
+
if r.returncode: die(r.stderr)
|
|
119
|
+
else:
|
|
120
|
+
start = a.start or 0.0
|
|
121
|
+
fps = info["fps"] if a.every_frame else (a.fps or 4.0)
|
|
122
|
+
cmd = ["ffmpeg", "-y", "-v", "error"]
|
|
123
|
+
if a.start: cmd += ["-ss", f"{a.start:.4f}"]
|
|
124
|
+
cmd += ["-i", a.video]
|
|
125
|
+
if a.end: cmd += ["-t", f"{a.end - start:.4f}"]
|
|
126
|
+
cmd += ["-vf", f"fps={fps},{vf}", "-start_number", "0", os.path.join(out, "f%05d.png")]
|
|
127
|
+
r = run(cmd)
|
|
128
|
+
if r.returncode: die(r.stderr)
|
|
129
|
+
for f in sorted(glob.glob(os.path.join(out, "f*.png"))):
|
|
130
|
+
n = int(re.search(r"f(\d+)", f).group(1))
|
|
131
|
+
os.rename(f, os.path.join(out, f"t{start + n / fps:08.3f}.png"))
|
|
132
|
+
files = sorted(glob.glob(os.path.join(out, "t*.png")))
|
|
133
|
+
print(f"{len(files)} frames in {out}")
|
|
134
|
+
print(f"first {os.path.basename(files[0])}, last {os.path.basename(files[-1])}" if files else "no frames")
|
|
135
|
+
print(f"video: {info['width']}x{info['height']} @ {info['fps']} fps. Next: media sheet {out}")
|
|
136
|
+
|
|
137
|
+
def frame_time(path):
|
|
138
|
+
m = re.findall(r"(\d+\.\d+|\d+)(?=\.png$)", os.path.basename(path))
|
|
139
|
+
return float(m[-1]) if m else None
|
|
140
|
+
|
|
141
|
+
def collect(paths):
|
|
142
|
+
files = []
|
|
143
|
+
for p in paths:
|
|
144
|
+
files += sorted(glob.glob(os.path.join(p, "*.png"))) if os.path.isdir(p) else [p]
|
|
145
|
+
return sorted(files, key=lambda f: (frame_time(f) if frame_time(f) is not None else 1e9, f))
|
|
146
|
+
|
|
147
|
+
def cmd_sheet(a):
|
|
148
|
+
Image, _, ImageDraw, _, _ = pil()
|
|
149
|
+
files = collect(a.paths)
|
|
150
|
+
if not files: die("no images")
|
|
151
|
+
cols = a.cols; cell = a.cell
|
|
152
|
+
first = Image.open(files[0]); ch = round(cell * first.height / first.width)
|
|
153
|
+
per = cols * cols # square-ish sheets: cols x cols per page
|
|
154
|
+
pages = [files[i:i + per] for i in range(0, len(files), per)]
|
|
155
|
+
base = a.out or os.path.join(os.path.dirname(files[0]) if os.path.isdir(a.paths[0]) is False else a.paths[0], "sheet.png")
|
|
156
|
+
stem, ext = os.path.splitext(base)
|
|
157
|
+
outs = []
|
|
158
|
+
f = font(max(14, cell // 24))
|
|
159
|
+
for pi, page in enumerate(pages):
|
|
160
|
+
rows = (len(page) + cols - 1) // cols
|
|
161
|
+
sh = Image.new("RGB", (cols * cell, rows * ch), (36, 36, 36))
|
|
162
|
+
for i, fp in enumerate(page):
|
|
163
|
+
im = Image.open(fp).convert("RGB").resize((cell - 4, ch - 4), Image.LANCZOS)
|
|
164
|
+
t = frame_time(fp)
|
|
165
|
+
label = f"{t:.3f}s" if t is not None else os.path.basename(fp)
|
|
166
|
+
d = ImageDraw.Draw(im); w = int(d.textlength(label, font=f)) + 12
|
|
167
|
+
d.rectangle([0, 0, w, f.size + 8], fill=(255, 0, 200)); d.text((6, 3), label, fill=(255, 255, 255), font=f)
|
|
168
|
+
sh.paste(im, ((i % cols) * cell + 2, (i // cols) * ch + 2))
|
|
169
|
+
out = base if len(pages) == 1 else f"{stem}-{pi + 1}{ext}"
|
|
170
|
+
sh.save(out); outs.append(out)
|
|
171
|
+
print("\n".join(outs))
|
|
172
|
+
|
|
173
|
+
def cmd_activity(a):
|
|
174
|
+
start = a.start or 0.0
|
|
175
|
+
scores, fps = diff_scores(a.video, a.start, a.end, a.region, gray=True, width=320)
|
|
176
|
+
if not scores: die("no frames")
|
|
177
|
+
peak = max(scores) or 1.0
|
|
178
|
+
thr = a.threshold if a.threshold is not None else max(0.15, peak * 0.04)
|
|
179
|
+
print(f"{len(scores)} frames @ {fps} fps from {start:.2f}s. Motion score = mean pixel change vs previous frame. Threshold {thr:.2f}.\n")
|
|
180
|
+
print("frame time score")
|
|
181
|
+
for i, sc in enumerate(scores):
|
|
182
|
+
bar = "#" * int(40 * sc / peak)
|
|
183
|
+
print(f"{i:5d} {start + i / fps:7.3f} {sc:6.2f} {bar}")
|
|
184
|
+
print("\nActive windows (motion above threshold):")
|
|
185
|
+
i = 0
|
|
186
|
+
while i < len(scores):
|
|
187
|
+
if scores[i] > thr:
|
|
188
|
+
j = i
|
|
189
|
+
while j + 1 < len(scores) and scores[j + 1] > thr: j += 1
|
|
190
|
+
print(f" {start + i / fps:.3f}s - {start + (j + 1) / fps:.3f}s ({j - i + 1} frames, peak {max(scores[i:j + 1]):.2f} at frame {i + scores[i:j + 1].index(max(scores[i:j + 1]))})")
|
|
191
|
+
i = j + 1
|
|
192
|
+
else: i += 1
|
|
193
|
+
print("\nReading it: a burst that starts high and decays = ease-out; low-high-low = ease in-out; flat plateau = linear. Gaps between bursts = stagger spacing.")
|
|
194
|
+
|
|
195
|
+
def cmd_compare(a):
|
|
196
|
+
Image, ImageChops, ImageDraw, _, _ = pil()
|
|
197
|
+
ref = Image.open(a.reference).convert("RGB"); ours = Image.open(a.ours).convert("RGB")
|
|
198
|
+
if ours.size != ref.size: ours = ours.resize(ref.size, Image.LANCZOS)
|
|
199
|
+
W = 960; H = round(W * ref.height / ref.width)
|
|
200
|
+
panels = [("REFERENCE", ref), ("OURS", ours)]
|
|
201
|
+
if a.diff: panels.append(("DIFFERENCE (x4)", ImageChops.difference(ref, ours).point(lambda v: min(255, v * 4))))
|
|
202
|
+
sh = Image.new("RGB", (W * len(panels), H), (36, 36, 36)); f = font(26)
|
|
203
|
+
for i, (name, im) in enumerate(panels):
|
|
204
|
+
im = im.resize((W - 4, H - 4), Image.LANCZOS); d = ImageDraw.Draw(im)
|
|
205
|
+
d.rectangle([0, 0, int(d.textlength(name, font=f)) + 16, 40], fill=(255, 0, 200)); d.text((8, 5), name, fill=(255, 255, 255), font=f)
|
|
206
|
+
sh.paste(im, (i * W + 2, 2))
|
|
207
|
+
out = a.out or os.path.join(tempfile.gettempdir(), "ae-recipe-compare.png"); sh.save(out); print(out)
|
|
208
|
+
|
|
209
|
+
def hexc(rgb): return "#%02X%02X%02X" % tuple(int(round(c)) for c in rgb[:3])
|
|
210
|
+
|
|
211
|
+
def cmd_color(a):
|
|
212
|
+
Image, _, _, _, ImageStat = pil()
|
|
213
|
+
im = Image.open(a.image).convert("RGB")
|
|
214
|
+
for pt in a.points:
|
|
215
|
+
x, y = [int(v) for v in pt.split(",")]
|
|
216
|
+
if a.box > 1:
|
|
217
|
+
h = a.box // 2; reg = im.crop((max(0, x - h), max(0, y - h), x + h + 1, y + h + 1))
|
|
218
|
+
c = ImageStat.Stat(reg).mean; print(f"({x},{y}) avg of {a.box}x{a.box}: {hexc(c)} rgb({c[0]:.0f},{c[1]:.0f},{c[2]:.0f})")
|
|
219
|
+
else:
|
|
220
|
+
c = im.getpixel((x, y)); print(f"({x},{y}): {hexc(c)} rgb{c}")
|
|
221
|
+
|
|
222
|
+
def cmd_palette(a):
|
|
223
|
+
Image, _, _, _, _ = pil()
|
|
224
|
+
im = Image.open(a.image).convert("RGB"); im.thumbnail((200, 200))
|
|
225
|
+
q = im.quantize(colors=a.n, method=Image.Quantize.MEDIANCUT); pal = q.getpalette()
|
|
226
|
+
counts = sorted(q.getcolors(), reverse=True); total = sum(c for c, _ in counts)
|
|
227
|
+
print("dominant colours (share of pixels):")
|
|
228
|
+
for c, idx in counts: print(f" {hexc(pal[idx * 3:idx * 3 + 3])} {100 * c / total:5.1f}%")
|
|
229
|
+
|
|
230
|
+
def cmd_crop(a):
|
|
231
|
+
Image, _, _, _, _ = pil()
|
|
232
|
+
im = Image.open(a.image).convert("RGB").crop((a.x, a.y, a.x + a.w, a.y + a.h))
|
|
233
|
+
if a.scale != 1: im = im.resize((int(a.w * a.scale), int(a.h * a.scale)), Image.LANCZOS)
|
|
234
|
+
out = a.out or os.path.join(tempfile.gettempdir(), "ae-recipe-crop.png"); im.save(out); print(out)
|
|
235
|
+
|
|
236
|
+
def main():
|
|
237
|
+
p = argparse.ArgumentParser(prog="media", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
238
|
+
s = p.add_subparsers(dest="cmd", required=True)
|
|
239
|
+
s.add_parser("check").set_defaults(f=cmd_check)
|
|
240
|
+
x = s.add_parser("info"); x.add_argument("video"); x.set_defaults(f=cmd_info)
|
|
241
|
+
x = s.add_parser("cuts"); x.add_argument("video"); x.add_argument("--threshold", type=float, default=18.0); x.set_defaults(f=cmd_cuts)
|
|
242
|
+
x = s.add_parser("frames"); x.add_argument("video"); x.add_argument("--start", type=float); x.add_argument("--end", type=float)
|
|
243
|
+
x.add_argument("--fps", type=float); x.add_argument("--every-frame", action="store_true"); x.add_argument("--times"); x.add_argument("--width", type=int, default=960); x.add_argument("--out"); x.set_defaults(f=cmd_frames)
|
|
244
|
+
x = s.add_parser("sheet"); x.add_argument("paths", nargs="+"); x.add_argument("--cols", type=int, default=3); x.add_argument("--cell", type=int, default=640); x.add_argument("--out"); x.set_defaults(f=cmd_sheet)
|
|
245
|
+
x = s.add_parser("activity"); x.add_argument("video"); x.add_argument("--start", type=float); x.add_argument("--end", type=float); x.add_argument("--region"); x.add_argument("--threshold", type=float); x.set_defaults(f=cmd_activity)
|
|
246
|
+
x = s.add_parser("compare"); x.add_argument("reference"); x.add_argument("ours"); x.add_argument("--out"); x.add_argument("--diff", action="store_true"); x.set_defaults(f=cmd_compare)
|
|
247
|
+
x = s.add_parser("color"); x.add_argument("image"); x.add_argument("points", nargs="+"); x.add_argument("--box", type=int, default=1); x.set_defaults(f=cmd_color)
|
|
248
|
+
x = s.add_parser("palette"); x.add_argument("image"); x.add_argument("--n", type=int, default=6); x.set_defaults(f=cmd_palette)
|
|
249
|
+
x = s.add_parser("crop"); x.add_argument("image"); [x.add_argument(k, type=int) for k in ("x", "y", "w", "h")]; x.add_argument("--scale", type=float, default=2); x.add_argument("--out"); x.set_defaults(f=cmd_crop)
|
|
250
|
+
a = p.parse_args(); a.f(a)
|
|
251
|
+
|
|
252
|
+
if __name__ == "__main__":
|
|
253
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Sets up the create-ae-recipe skill's tools: ffmpeg/ffprobe (Homebrew on macOS) and a local venv with Pillow.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
if ! command -v ffmpeg >/dev/null 2>&1 || ! command -v ffprobe >/dev/null 2>&1; then
|
|
7
|
+
if command -v brew >/dev/null 2>&1; then echo "Installing ffmpeg with Homebrew..."; brew install ffmpeg
|
|
8
|
+
elif command -v apt-get >/dev/null 2>&1; then echo "Installing ffmpeg with apt..."; sudo apt-get install -y ffmpeg
|
|
9
|
+
else echo "Install ffmpeg manually (https://ffmpeg.org) and re-run." >&2; exit 1; fi
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
if [ ! -x "$HERE/.venv/bin/python" ]; then
|
|
13
|
+
echo "Creating Python venv at $HERE/.venv"
|
|
14
|
+
python3 -m venv "$HERE/.venv"
|
|
15
|
+
fi
|
|
16
|
+
"$HERE/.venv/bin/pip" install -q --upgrade pillow
|
|
17
|
+
"$HERE/scripts/media" check
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: create-ae-video
|
|
3
|
+
description: Master guide for making a video in After Effects with Claude ("Create AE Video"). Use at the START of any conversation about making, animating, recreating or editing a video, promo, explainer, title sequence or motion piece in After Effects. Runs a readiness check, does a short discovery, chooses the path (library recipes, new recipes from a description or a reference video, or changing something already in the project), then guides storyboard, build, review and handover by delegating to the use-ae-recipes and create-ae-recipe skills.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Create AE Video
|
|
7
|
+
|
|
8
|
+
You are the conductor. You get the user from "I want a video" to finished comps in their After Effects project, with a clear plan, visible progress and approval gates. You stay thin: the heavy lifting is delegated.
|
|
9
|
+
|
|
10
|
+
| Skill | Use it for |
|
|
11
|
+
|---|---|
|
|
12
|
+
| **`use-ae-recipes`** | Building scenes from the library, tuning, assembling a master comp, reviewing |
|
|
13
|
+
| **`create-ae-recipe`** | Adding a missing look to the library, from a description (iterate with previews) or from a reference video (frame-by-frame breakdown) |
|
|
14
|
+
| this skill | Preflight, discovery, deciding the path, storyboard, status board, gates, handover |
|
|
15
|
+
|
|
16
|
+
Invoke the others with the Skill tool when you reach their phase; give them the brief and the relevant storyboard row so they don't re-ask. Don't re-run their preflights.
|
|
17
|
+
|
|
18
|
+
Ground rules (always): you work in the user's **open** project. Create new comps only, never touch existing ones unless asked, **never save** the project, never render the final output (the user does that). Every run is one Undo step. Show images by file path and say what to look for.
|
|
19
|
+
|
|
20
|
+
## Phase 0: Preflight (first thing, before asking anything)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
node .claude/skills/create-ae-video/scripts/preflight.js
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
It checks Node, the Playrig CLI, the library lint, ffmpeg/Pillow, sibling skills, and (live) After Effects + the Playrig panel, the open project, and the Inter fonts.
|
|
27
|
+
|
|
28
|
+
- Tell the user the result in **3 lines** (ready? what's open? any note).
|
|
29
|
+
- **Blocked?** Give the printed fix, wait, re-run. Typical: After Effects not open, panel not started (Window > Playrig, then Start), media tools missing (`.claude/skills/create-ae-recipe/scripts/setup.sh`), font missing.
|
|
30
|
+
- **Resuming?** If the project is saved and has an `EDIT_LOGS/` folder beside it (older projects: `AE-BRIDGE/`), read its `JOURNAL.md` and `assets.ae.md` to see what earlier sessions did. If comps already exist, ask whether this continues them.
|
|
31
|
+
|
|
32
|
+
## Phase 1: Discovery (short)
|
|
33
|
+
|
|
34
|
+
First, one choice question (use AskUserQuestion if available):
|
|
35
|
+
|
|
36
|
+
1. **New video from my idea/brief**
|
|
37
|
+
2. **Recreate or take inspiration from a reference video**
|
|
38
|
+
3. **Add a new effect/look to my library** → go straight to `create-ae-recipe`, then offer to continue to a video
|
|
39
|
+
4. **Continue or modify something already in the project**
|
|
40
|
+
|
|
41
|
+
Then collect the brief (template in `references/templates.md`). Ask **at most 5 questions**, only what changes the result: purpose and audience, length and format (default 1920×1080, 24 fps), the words/content, tone and look (light & clean vs dark & technical, brand colours, font), references. Assume the rest and **state the assumptions**. End with a 6-line **brief block** the user can correct.
|
|
42
|
+
|
|
43
|
+
For a reference video: ask for the file path, then run `create-ae-recipe`'s overview steps (`media info`, `cuts`, contact sheets) to understand it before planning.
|
|
44
|
+
|
|
45
|
+
## Phase 2: Library fit
|
|
46
|
+
|
|
47
|
+
Read **only** `<library>/INDEX.md`. Build the **coverage map** (template): each thing the brief needs → matching recipe(s) → `ready` / `planned` / `missing`. Don't open recipe files yet.
|
|
48
|
+
|
|
49
|
+
For each gap offer: **substitute** with something that exists, **create** it (via `create-ae-recipe`), or **drop** it. Say honestly what's weak. Recommend one option per gap; the user decides.
|
|
50
|
+
|
|
51
|
+
## Phase 3: Storyboard and gate
|
|
52
|
+
|
|
53
|
+
Produce the **storyboard** (template): scene, time range, background, content, recipe chain, transition out, plus a **status board**. Keep to one idea per scene, 1.5-4 s each. Total time should match the brief.
|
|
54
|
+
|
|
55
|
+
**Gate: do not build until the user approves the storyboard** (a quick "go" is enough; trivial single-effect requests skip the table).
|
|
56
|
+
|
|
57
|
+
## Phase 4: Fill the gaps (only if any)
|
|
58
|
+
|
|
59
|
+
For each approved gap, run `create-ae-recipe` (one at a time). Each ends with a tested, previewed, indexed recipe. Update the status board after each. If a gap turns out to be expensive, say so and re-offer substitute/drop.
|
|
60
|
+
|
|
61
|
+
## Phase 5: Build scene by scene
|
|
62
|
+
|
|
63
|
+
Run `use-ae-recipes` for one scene at a time. After each scene show its preview sheet and get a reaction before the next. Tuning = changing recipe parameters. Update the status board (`planned → built → approved`).
|
|
64
|
+
|
|
65
|
+
## Phase 6: Assemble and polish
|
|
66
|
+
|
|
67
|
+
Assemble the master comp (with a structure recipe or a job script) and review the whole timeline on a contact sheet (every 0.25 s). Run a **polish pass** with a checklist: pacing, legibility, consistent palette/type, transition feel, anything clipped or off-screen, first and last frame. Max **2 polish rounds** unless the user wants more.
|
|
68
|
+
|
|
69
|
+
## Phase 7: Handover
|
|
70
|
+
|
|
71
|
+
Use the handover template: comps created, per-scene command chains (re-runnable), the parameters most worth tweaking, assumptions, new recipes added to the library, what's still planned, and the reminders: the project is **unsaved** and **not rendered**. Offer next steps (another scene, variations, a vertical version, new recipes for the planned items).
|
|
72
|
+
|
|
73
|
+
## How to run the conversation
|
|
74
|
+
|
|
75
|
+
- **Status board.** Keep a compact board (template) and reprint it at each milestone: brief, scenes with status, recipes to create. It is how the user stays oriented.
|
|
76
|
+
- **One decision at a time.** Short messages, concrete options, a recommendation. Never ask what you can sensibly assume.
|
|
77
|
+
- **Show early.** The first visible result should arrive quickly: build the hook scene first.
|
|
78
|
+
- **Change requests are edits, not rebuilds.** New words: `edit-text.jsx`. Different look from a recipe: `playrig ae lib run <id> --replace` with the new params (replaces only that recipe's layers, in place). Rebuilding a whole scene is a last resort and discards the user's manual changes in it.
|
|
79
|
+
- **Direction change** mid-way: update the brief and board, say what it invalidates, continue.
|
|
80
|
+
- **Failures:** read the printed error, fix parameters or ordering; don't retry blindly. If Playrig stops answering, re-run preflight.
|
|
81
|
+
- **Token economy:** index first, then one recipe file at a time; never read recipe scripts to use them; don't re-read what's already in context.
|
|
82
|
+
- **Honesty:** say which recipes are planned, what you approximated, and what you could not verify.
|
|
83
|
+
- **Quality bar:** nothing is "done" until frames were looked at (start, mid, end of motion) and the user reacted to them.
|
|
84
|
+
|
|
85
|
+
## References
|
|
86
|
+
|
|
87
|
+
- `references/templates.md`: brief block, question bank with defaults, coverage map, storyboard, status board, handover.
|
|
88
|
+
- Sibling skills: `.claude/skills/use-ae-recipes/`, `.claude/skills/create-ae-recipe/`. Library: `<library>/INDEX.md`, `<library>/README.md`.
|