fablecut 1.13.0 → 1.15.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/CLAUDE.md +802 -16
- package/README.md +71 -7
- package/analyze.js +161 -9
- package/app.js +3905 -258
- package/comp.js +195 -0
- package/delivery.js +779 -0
- package/edit-ops.js +50 -18
- package/export-jobs.js +9 -5
- package/frame-cache.js +49 -0
- package/frame-stats.js +267 -0
- package/index.html +282 -72
- package/library/README.md +2 -0
- package/library/elements/logo-placeholder.svg +12 -0
- package/mcp-server.js +324 -656
- package/media-lib.js +287 -0
- package/media-probe.js +88 -0
- package/motion.js +803 -0
- package/package.json +1 -1
- package/paths.js +33 -3
- package/project-ops.js +1290 -0
- package/proxy.js +180 -0
- package/server.js +517 -15
- package/style.css +534 -20
- package/templates.js +576 -0
package/CLAUDE.md
CHANGED
|
@@ -26,12 +26,22 @@ Every Claude Code session then has these tools:
|
|
|
26
26
|
into `./media/` and register it. The stored `src` is always `/media/…`.
|
|
27
27
|
- `fablecut_analyze_reference` — turn a reference video into an edit blueprint
|
|
28
28
|
(shots, beats, BPM, energy, drop) + extract its music. See "Remake a reference video".
|
|
29
|
-
- `
|
|
30
|
-
|
|
29
|
+
- `fablecut_frame` — **look at** a moment and get back an actual image.
|
|
30
|
+
`{path, t}` returns one frame of a source clip; `{path, frames}` returns a
|
|
31
|
+
contact sheet across it; with no `path`, `{time}` renders the composed
|
|
32
|
+
timeline. See "Look at the footage" below.
|
|
33
|
+
- `fablecut_encode_profiles` — list the **delivery presets** (YouTube, Shorts, Reels, TikTok,
|
|
34
|
+
X, LinkedIn, Master, Broadcast, Web, Archive, saved ones) and the raw ffmpeg **encoding
|
|
35
|
+
profiles** from `encoding-profiles.json`. `{preset:"youtube"}` returns one preset's spec.
|
|
36
|
+
See "Export" below.
|
|
31
37
|
- `fablecut_normalize_audio` — measure clips (BS.1770 LUFS or sample peak, via ffmpeg) and
|
|
32
38
|
set their clip gain to a loudness target. See "Audio mix" below.
|
|
33
39
|
- `fablecut_auto_duck` — find where the voice tracks speak and write `duck` keyframes that
|
|
34
40
|
dip the music under it. See "Audio mix" below.
|
|
41
|
+
- `fablecut_frame` — **see** a moment. `{path, t}` → one JPEG frame of a source
|
|
42
|
+
clip; `{path, frames:12}` → a contact sheet across it; `{time}` → the composed
|
|
43
|
+
timeline, graded, as export renders it. Returns a real image, not numbers.
|
|
44
|
+
See "Look at the footage" below.
|
|
35
45
|
- `fablecut_denoise` — reduce background noise on audio clips (renders a cleaned copy of the
|
|
36
46
|
file and switches the clip's stems to it). See "Audio mix" below.
|
|
37
47
|
- `fablecut_export` — render the timeline to a file in `exports/` with the editor's own
|
|
@@ -41,6 +51,9 @@ Every Claude Code session then has these tools:
|
|
|
41
51
|
select. See "Color" and "Masks" below.
|
|
42
52
|
- `fablecut_track` — motion tracking: follow a region of a video clip, store it as
|
|
43
53
|
a track, and make a mask or another clip follow it. See "Tracking" below.
|
|
54
|
+
- `fablecut_proxy` — make, check or delete proxies: light copies of heavy footage
|
|
55
|
+
the editor plays while editing (export always reads the originals). See
|
|
56
|
+
"Media management" below.
|
|
44
57
|
|
|
45
58
|
### Token-efficient editing (important for agents)
|
|
46
59
|
|
|
@@ -59,8 +72,12 @@ Editing via full get→modify→set costs thousands of tokens per change. Cheape
|
|
|
59
72
|
Re-check the clips, then retry with that revision.
|
|
60
73
|
3. **Docs**: request `fablecut_docs {section:"schema"}` (or "Recipes", "Remake", …)
|
|
61
74
|
instead of the whole manual; skip it entirely if the schema is already in context.
|
|
62
|
-
4. **Media questions** (duration, fps,
|
|
63
|
-
|
|
75
|
+
4. **Media questions** (duration, resolution, fps, codec, audio channels, file
|
|
76
|
+
size, how often it is used): read them from the registered media entries or
|
|
77
|
+
the compact view's MEDIA lines — don't shell out to ffprobe; the editor
|
|
78
|
+
probes every file and writes them back (see "Media management").
|
|
79
|
+
For what the footage *looks like*, use `fablecut_frame` — it is far cheaper
|
|
80
|
+
than reading a file, and far more reliable than inferring from a number.
|
|
64
81
|
5. Batch related changes into ONE patch call (ops apply in order, one revision bump).
|
|
65
82
|
6. **Cut and trim with the edit ops** (next section) rather than recomputing
|
|
66
83
|
`start` / `in` / `duration` by hand — they keep linked stems in sync.
|
|
@@ -133,6 +150,9 @@ reports the real paths.
|
|
|
133
150
|
Tests (and nothing else) may set **`FABLECUT_NO_FS_WATCH=1`** to skip `fs.watch`.
|
|
134
151
|
On Windows, libuv can abort the process when a file is created under a temp
|
|
135
152
|
data dir. Production leaves watching on so the UI live-reloads.
|
|
153
|
+
Tests may also set **`FABLECUT_NO_AUTOSTART=1`** so `ensureUIServer()` reports
|
|
154
|
+
a missing editor server instead of spawning one — the auto-start is detached,
|
|
155
|
+
so a test that triggers it would leave a `server.js` running after it finishes.
|
|
136
156
|
|
|
137
157
|
## Run
|
|
138
158
|
|
|
@@ -177,8 +197,10 @@ Reusable assets, visible in the editor's left-panel tabs and never copied:
|
|
|
177
197
|
| `library/elements/` | **Elements** | overlay art: alpha PNGs, light leaks, textures, stickers |
|
|
178
198
|
| `library/svg/` | **SVG** | animated vector graphics **you author** (convention below) |
|
|
179
199
|
| `library/fonts/` | font editor | `.ttf/.otf/.woff/.woff2`, auto-registered, family name = file name; a variable font draws every `weight` |
|
|
200
|
+
| `library/presets/` | Inspector → Motion → Presets | saved animation presets, one `.json` each (see "Presets and templates") |
|
|
201
|
+
| `library/templates/`| **Templates** | saved templates, one `.json` each, listed after the built-in ones |
|
|
180
202
|
|
|
181
|
-
- List via `GET /api/library?dir=sfx|elements|svg|fonts` (recursive; subfolders OK).
|
|
203
|
+
- List via `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` (recursive; subfolders OK).
|
|
182
204
|
- To use one in the timeline, add a media entry whose `src` is its library path,
|
|
183
205
|
e.g. `{ "id":"m_x", "name":"whoosh.mp3", "kind":"audio", "src":"/library/sfx/whoosh.mp3" }`
|
|
184
206
|
— then reference it from clips like any other media.
|
|
@@ -252,6 +274,10 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
252
274
|
"untargetedTracks": [ "V2" ],
|
|
253
275
|
// ^ optional — lanes edits skip (split, insert, ripple, close gap…); default: all targeted
|
|
254
276
|
"encodeProfile": "hq", // optional — fast-export profile id (see encoding-profiles.json)
|
|
277
|
+
"delivery": { "preset": "youtube", "width": 1920, "height": 1080, "…": "…" },
|
|
278
|
+
// ^ optional — the delivery spec exports use by default (see "Delivery presets"); setProject takes a preset id
|
|
279
|
+
"outputs": [ { "id": "o_1", "on": true, "preset": "master", "…": "…" } ],
|
|
280
|
+
// ^ optional — batch outputs: files made together, one render per frame rate (see "Batch outputs and the export queue")
|
|
255
281
|
"master": { "gain": -1.5, "fx": [ { "type": "limiter", "ceiling": -1 } ] },
|
|
256
282
|
// ^ optional — master fader, dB (−60 = silence … +12; omit = 0 dB) and master effects (see "Audio mix")
|
|
257
283
|
"tracks": [ // optional — timeline lanes (default V3…V1 + A1…A4)
|
|
@@ -266,11 +292,18 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
266
292
|
{ "id": "A4", "kind": "audio" }
|
|
267
293
|
],
|
|
268
294
|
"buses": [ { "id": "B1", "name": "Dialogue", "gain": -2, "fx": [ { "type": "compressor" } ] } ],
|
|
295
|
+
"collections": [ { "id": "col_sel", "name": "Selects", "media": [ "m_abc" ] } ],
|
|
296
|
+
// ^ optional — Project-bin collections: media grouped across folders; see "Media management"
|
|
297
|
+
"comps": [ { "id": "lt", "name": "Lower third", "width": 1280, "height": 720, "duration": 5, "clips": [ /* … */ ] } ],
|
|
298
|
+
// ^ optional — compositions (nested sequences), placed with kind:"comp" clips; see "Compositions (nested sequences)"
|
|
269
299
|
// ^ optional — submix buses B1…B8: name, gain (dB), pan, mute, fx; tracks route in with `out`
|
|
270
300
|
"media": [
|
|
271
301
|
{ "id": "m_abc", "name": "intro.mp4", "kind": "video", // video|audio|image|svg
|
|
272
302
|
"src": "/media/intro.mp4", // path under ./media or ./library (never a raw https:// URL)
|
|
273
303
|
"duration": 12.4, "width": 1920, "height": 1080,
|
|
304
|
+
"fps": 29.97, "codec": "h264", "audioCodec": "aac", "audioChannels": 2, "sampleRate": 48000,
|
|
305
|
+
"size": 45200000, "bitrate": 29300000, // probed from the file (read-only; see "Media management")
|
|
306
|
+
"favorite": true, "tags": [ "interview", "day 1" ], // optional: bin organization
|
|
274
307
|
"folderId": null }, // optional: id of a folders[] entry
|
|
275
308
|
{ "id": "m_abd", "name": "intro.denoise-medium.flac", "kind": "audio", "src": "/media/intro.denoise-medium.flac",
|
|
276
309
|
"derivedFrom": "m_abc", "denoise": "medium" } // a noise-reduced copy of m_abc (see "Audio mix")
|
|
@@ -278,8 +311,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
278
311
|
"clips": [
|
|
279
312
|
{
|
|
280
313
|
"id": "c_xyz", // unique string
|
|
281
|
-
"mediaId": "m_abc", // null for kind:"text" and
|
|
282
|
-
"kind": "video", // video | audio | image | svg | text | adjust
|
|
314
|
+
"mediaId": "m_abc", // null for kind:"text", "adjust", "shape", "null" and "comp"
|
|
315
|
+
"kind": "video", // video | audio | image | svg | text | adjust | shape | null | comp
|
|
316
|
+
// kind:"comp" also has "compId" (a comps[] entry) — see "Compositions (nested sequences)"
|
|
283
317
|
"track": "V1", // Vn…V1 (video) | A1…An (audio); extras via UI +V/+A or tracks[]
|
|
284
318
|
"start": 0, // timeline position, seconds
|
|
285
319
|
"in": 2.5, // offset into source media, seconds (0 for image/svg/text)
|
|
@@ -289,10 +323,13 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
289
323
|
"locked": true, // OPTIONAL — locked by the user: leave it alone (MCP patch refuses edits)
|
|
290
324
|
"disabled": true, // OPTIONAL — stays on the timeline, hidden from preview / audio / export
|
|
291
325
|
"unlinked": true, // OPTIONAL — the user unlinked this clip; never auto-relinked on load
|
|
326
|
+
"parent": "c_null", // OPTIONAL — moves, turns and scales with that clip (see "Shapes, nulls and parenting")
|
|
292
327
|
"props": { /* all optional — see the props reference below */ },
|
|
293
328
|
// OPTIONAL — keyframe animation. Times are seconds RELATIVE TO CLIP START.
|
|
294
329
|
// "ease" sits on the DESTINATION keyframe of each segment:
|
|
295
330
|
// linear | ease-in | ease-out | ease-in-out (default ease-in-out).
|
|
331
|
+
// x and y keys at the same time are a position key; pin / pout on them (px)
|
|
332
|
+
// are motion-path curve handles (see "Motion paths, motion blur and text on a path").
|
|
296
333
|
"keyframes": {
|
|
297
334
|
"scale": [ { "t": 0, "v": 1 }, { "t": 5, "v": 1.25, "ease": "linear" } ],
|
|
298
335
|
"opacity": [ { "t": 0, "v": 0 }, { "t": 0.8, "v": 1 } ]
|
|
@@ -316,6 +353,9 @@ Examples in `library/svg/`: `sparkles.svg` (loop), `lower-third.svg`,
|
|
|
316
353
|
| `scale` | 1 | |
|
|
317
354
|
| `rotation` | 0 | degrees |
|
|
318
355
|
| `opacity` | 1 | 0–1 |
|
|
356
|
+
| `anchorX`, `anchorY` | 0 | anchor point, px from the picture's centre: the point `x`/`y` place and `rotation`/`scale` turn around |
|
|
357
|
+
| `autoOrient` | false | turn the clip with its direction of travel along its motion path (`rotation` adds on top) |
|
|
358
|
+
| `motionBlur` · `motionBlurSamples` | 0 · 8 | motion blur: shutter angle in degrees (0 = off, 180 = film look, up to 720) · sub-frame samples 2–32 |
|
|
319
359
|
| `blend` | "normal" | how the clip lays over what is under it (see Track mattes and blending) |
|
|
320
360
|
| `matte` · `matteTrack` | — | a track matte (see Track mattes and blending) |
|
|
321
361
|
|
|
@@ -390,6 +430,8 @@ These are quick looks. For real grading use `props.grade` (see "Color" below).
|
|
|
390
430
|
| `bgColor` `bgOpacity` | "#000", 0 | rounded pill behind each line |
|
|
391
431
|
| `textAnim` | "none" | typewriter · word-pop · word-slide · karaoke · **letter-pop** (per-character entrance; Arabic/Indic auto-fallback to per-word clusters so joined letters shape correctly) · **wave** (looping per-character ride; same shaping fallback) · **bounce** (looping per-word hop) · **shake** (looping jitter) · **clip-reveal** (wipe-mask sweep, per line) · **zoom-in** (scale + opacity settle) · **font-cut** (rhythmically swaps typeface, then settles — see `fontCutSet`) · **rise-mask** (line rises from behind its baseline, lower-third reveal) |
|
|
392
432
|
| `wordRate` | 0.15 | seconds per word (typewriter: /4, letter-pop: /3 per character); also staggers `clip-reveal`/`zoom-in`/`rise-mask` per line |
|
|
433
|
+
| `textPath` | — | set the title along a path: one shape item (`path`, `line`, `ellipse`, `rect`, `polygon`, `star`) whose outline is the baseline; more lines follow on parallel curves below it. See "Motion paths, motion blur and text on a path" |
|
|
434
|
+
| `pathOffset` · `pathAlign` | 0 · "start" | where along the path the text sits (% of its length, keyframable, wraps on closed paths) · which end of the text sits there: start · center · end |
|
|
393
435
|
| `fontCutSet` | (curated) | array of font family names cycled by `font-cut`, e.g. `["Anton","Bebas Neue","Archivo Black","Oswald"]`; each is auto-loaded |
|
|
394
436
|
|
|
395
437
|
**Title styles (one-tap cohesive looks).** Text clips created in the UI now
|
|
@@ -432,7 +474,16 @@ footage. Example: 0.3 s impact shake over everything =
|
|
|
432
474
|
shows — rect, ellipse, bezier, free-hand; feather, expand, invert, add /
|
|
433
475
|
subtract / intersect, keyframes. See "Masks" below.
|
|
434
476
|
|
|
435
|
-
**
|
|
477
|
+
**Shape layers** (`kind:"shape"`): `props.shapes` (rectangles, ellipses,
|
|
478
|
+
polygons, stars, lines, bezier paths, groups) and `trimStart` / `trimEnd` /
|
|
479
|
+
`trimOffset` (%, a draw-on of every path). **Null objects** (`kind:"null"`)
|
|
480
|
+
never render; other clips are parented to them. See "Shapes, nulls and parenting".
|
|
481
|
+
|
|
482
|
+
**Comp clips** (`kind:"comp"`, `compId`): a composition placed as one clip — it
|
|
483
|
+
draws like a picture of the comp's size (`fit` defaults to `none`) and its media
|
|
484
|
+
plays and exports. See "Compositions (nested sequences)".
|
|
485
|
+
|
|
486
|
+
**Animatable props** (usable in `keyframes`): x, y, scale, rotation, opacity, anchorX, anchorY, trimStart, trimEnd, trimOffset, pathOffset,
|
|
436
487
|
volume, pan, duck, speed, brightness, contrast, saturation, hue, blur, grayscale, sepia,
|
|
437
488
|
invert, temperature, tint, vignette, cornerRadius, shake, rgbSplit, grain,
|
|
438
489
|
fontSize, letterSpacing, glow.
|
|
@@ -925,6 +976,539 @@ track the screen with Scale and Rotate on and attach the new picture, cropped
|
|
|
925
976
|
to fit; **a highlight that stays on a UI element** in a screen recording —
|
|
926
977
|
track the button, attach a rect svg.
|
|
927
978
|
|
|
979
|
+
### Shapes, nulls and parenting
|
|
980
|
+
|
|
981
|
+
Motion graphics are built from three pieces: **shape layers** (vector shapes
|
|
982
|
+
drawn by the editor, crisp at any size), **null objects** (invisible layers that
|
|
983
|
+
carry a transform) and **parenting** (a clip moves, turns and scales with
|
|
984
|
+
another clip). Every visual clip also has an **anchor point**. All of it runs
|
|
985
|
+
the same in preview and export.
|
|
986
|
+
|
|
987
|
+
**Shape layers.** A clip with `kind:"shape"` (no `mediaId`) draws
|
|
988
|
+
`props.shapes`, a list drawn bottom-up (first = bottom):
|
|
989
|
+
|
|
990
|
+
```jsonc
|
|
991
|
+
{ "id": "c_lt", "kind": "shape", "mediaId": null, "track": "V2", "start": 0, "duration": 5,
|
|
992
|
+
"props": { "x": -300, "y": 220, "shapes": [
|
|
993
|
+
{ "type": "rect", "name": "Bar", "w": 520, "h": 90, "radius": 12, "fill": "#ffd166",
|
|
994
|
+
"keys": [ { "t": 0, "w": 0 }, { "t": 0.6, "w": 520, "ease": "ease-out" } ] },
|
|
995
|
+
{ "type": "line", "y": 60, "points": [[-260, 0], [260, 0]], "stroke": "#ffffff", "strokeWidth": 6,
|
|
996
|
+
"keys": [ { "t": 0.3, "trimEnd": 0 }, { "t": 1.1, "trimEnd": 100 } ] },
|
|
997
|
+
{ "type": "group", "x": 300, "items": [
|
|
998
|
+
{ "type": "star", "sides": 5, "inner": 0.45, "w": 80, "h": 80, "fill": "#ff7ac8" },
|
|
999
|
+
{ "type": "ellipse", "w": 120, "h": 120, "fill": "none", "stroke": "#ff7ac8", "strokeWidth": 4 } ],
|
|
1000
|
+
"keys": [ { "t": 0, "rotation": 0 }, { "t": 5, "rotation": 360, "ease": "linear" } ] }
|
|
1001
|
+
] } }
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
| key | default | does |
|
|
1005
|
+
|---|---|---|
|
|
1006
|
+
| `type` | `rect` | `rect` · `ellipse` · `polygon` · `star` · `line` · `path` · `group` |
|
|
1007
|
+
| `x` · `y` | 0 | position in layer px, from the layer's origin (the clip's `x`/`y`, y down) |
|
|
1008
|
+
| `rotation` · `scale` · `opacity` | 0 · 1 · 1 | about the item's own `x, y` |
|
|
1009
|
+
| `w` · `h` | 200 | size, px (rect, ellipse, polygon, star) |
|
|
1010
|
+
| `radius` | 0 | rect corner radius, px |
|
|
1011
|
+
| `sides` · `inner` | 6 / 5 · 0.5 | polygon / star point count (3–64); a star's inner radius as a fraction of the outer |
|
|
1012
|
+
| `points` | — | line: exactly 2 · path: 2–128 points `[x, y]` or `[x, y, inX, inY, outX, outY]` (curve handles relative to the point), px around the item's `x, y` |
|
|
1013
|
+
| `closed` | true | path only; `false` leaves it open (lines are always open) |
|
|
1014
|
+
| `fill` · `stroke` | `#ffffff` · `none` | `"#rrggbb"`, `"#rrggbbaa"` or `"none"`; lines and open paths default to a white stroke and no fill |
|
|
1015
|
+
| `strokeWidth` | 6 (8 open) | px; set with `stroke` |
|
|
1016
|
+
| `lineCap` · `lineJoin` | `round` · `round` | `butt` · `round` · `square` / `miter` · `round` · `bevel` |
|
|
1017
|
+
| `dash` | — | `[on, off, …]` px, e.g. `[24, 12]` |
|
|
1018
|
+
| `trimStart` · `trimEnd` | 0 · 100 | the part of the outline drawn, % — key `trimEnd` 0→100 to draw a path on |
|
|
1019
|
+
| `trimOffset` | 0 | % of the outline the drawn part is shifted round by (key it to make a dash run round a loop) |
|
|
1020
|
+
| `items` | — | group only: its items. A group moves, turns, scales, fades and trims them together (groups nest 4 deep) |
|
|
1021
|
+
| `name` · `on` | — | a label (agents address items by it); `on:false` hides it |
|
|
1022
|
+
| `keys` | — | `[{t, ease?, …}]`, `t` in seconds from the clip's start. Keyable: `x y rotation scale opacity w h radius inner strokeWidth trimStart trimEnd trimOffset fill stroke points`. Each value eases between the keys that set it (`ease` on the arriving key); colours blend; `points` keys morph the path and need its point count |
|
|
1023
|
+
|
|
1024
|
+
At most 64 items per layer (group children included). The layer itself is a
|
|
1025
|
+
normal visual clip: its `x`, `y`, `scale`, `rotation`, `opacity`, `blend`,
|
|
1026
|
+
masks, transitions and keyframes all work, as do `blur` and `shake`. Its own
|
|
1027
|
+
`trimStart` / `trimEnd` / `trimOffset` (clip props, keyframable) trim every
|
|
1028
|
+
path in it at once. A trimmed shape fills the part that is drawn.
|
|
1029
|
+
|
|
1030
|
+
**Null objects.** `kind:"null"` (no `mediaId`) is a clip that never renders.
|
|
1031
|
+
Give it `x`, `y`, `rotation`, `scale` (and keyframes), and parent clips to it to
|
|
1032
|
+
move them as one. The monitor shows it as a red square while it is on screen.
|
|
1033
|
+
|
|
1034
|
+
**Parenting.** `"parent": "<clip id>"` (top-level, next to `track`) makes a
|
|
1035
|
+
clip move, turn and scale with that clip. Its own `x`/`y` are then measured
|
|
1036
|
+
in the parent's space: from the parent's centre, turned and scaled with it.
|
|
1037
|
+
Parents chain (a parent can have a parent), and loops are refused. Only the
|
|
1038
|
+
transform follows: the parent's opacity, transitions, shake and effects do
|
|
1039
|
+
not, and the parent's keyframes are read even outside its time range (held
|
|
1040
|
+
at its first or last value). Splitting a parent leaves the children on the
|
|
1041
|
+
left half.
|
|
1042
|
+
|
|
1043
|
+
**Anchor point.** `anchorX` / `anchorY` (px from the picture's centre, for any
|
|
1044
|
+
visual clip) pick the point that `x`/`y` place and that `rotation` and `scale`
|
|
1045
|
+
turn around: `anchorY: 200` on a 400 px tall shape spins it about its bottom
|
|
1046
|
+
edge. Moving the anchor without moving the picture means changing `x`/`y`
|
|
1047
|
+
with it (the editor's anchor tool does that).
|
|
1048
|
+
|
|
1049
|
+
In the editor: the bin's **Shapes** tab adds a rectangle, ellipse, polygon,
|
|
1050
|
+
star, line or pen-path layer, or a null object, at the playhead. The **Shape layer** section lists the
|
|
1051
|
+
items; pick one to edit it — the sliders, Fill / Stroke (colour or **None**),
|
|
1052
|
+
ends and joins, trim, **Group** / **Ungroup** / **Out**, ↑ ↓, **Copy**,
|
|
1053
|
+
**Delete** — and on the monitor drag it to move, the knob to turn it (Shift:
|
|
1054
|
+
15°) and the corners to resize it. On a line or path, drag points and their
|
|
1055
|
+
handles (Alt breaks the pair, Alt-drag pulls new handles); on a path,
|
|
1056
|
+
Ctrl-click the outline to add a point and double-click one to remove it.
|
|
1057
|
+
**Pen** draws a new path: click points (drag to curve), click the first point
|
|
1058
|
+
to close it or press Enter to keep it open. Each value has its own **◆**: key
|
|
1059
|
+
it at the playhead, and from then on changing it keys it. Every visual clip's
|
|
1060
|
+
Transform section has **Anchor** (**✥ Move**: drag on the monitor to move the
|
|
1061
|
+
anchor point while the picture stays put, Shift snaps to the centre;
|
|
1062
|
+
**Centre** puts it back) and **Parent**. Right-click clips → **Parent to a
|
|
1063
|
+
new null** groups them under a fresh null at their centre.
|
|
1064
|
+
|
|
1065
|
+
**Agents:**
|
|
1066
|
+
|
|
1067
|
+
- Add a layer with `addClip {kind:"shape", track, start, duration, props:{shapes:[…]}}`,
|
|
1068
|
+
a null with `addClip {kind:"null", track, start, duration, props:{x, y}}`.
|
|
1069
|
+
- `{op:"setShape", id, set:{type:"rect", w:400, h:80, fill:"#ffd166"}}` adds an
|
|
1070
|
+
item (no `shape`, an index one past the end or a new name adds one; a new
|
|
1071
|
+
item needs `type`). `{op:"setShape", id, shape:"Bar", set:{fill:"#22d3ee"}}`
|
|
1072
|
+
edits one — by index (0 = first), name, or a path into groups like `"2.0"`
|
|
1073
|
+
(item 0 of group 2; `"2.3"` with 3 items adds one there) — merging key by
|
|
1074
|
+
key (`null` resets a key; a new `type` keeps the position and paint).
|
|
1075
|
+
`replace:true` starts it over. `keys` in `set` replaces its keys.
|
|
1076
|
+
`{op:"removeShape", id, shape}` deletes one. `ids:[…]` edits several clips.
|
|
1077
|
+
- `{op:"setParent", id | ids, parent:"c_null"}` parents clips and keeps them
|
|
1078
|
+
where they are on screen (at time `at`, default each clip's start; keyed
|
|
1079
|
+
channels are left as they are). `keep:false` keeps the numbers instead.
|
|
1080
|
+
`parent:null` unparents. Removing a parent clip unparents its children
|
|
1081
|
+
where they stand.
|
|
1082
|
+
- `props.shapes` and `parent` written through `addClip` / `updateClip` are
|
|
1083
|
+
validated the same way; bad input refuses the whole patch. The compact
|
|
1084
|
+
view lists a layer as `[3 shapes: rect #ffd166 2 keys, line stroke #ffffff/6, group(star(5) #ff7ac8)]`
|
|
1085
|
+
and a child as `parent:c_null`.
|
|
1086
|
+
|
|
1087
|
+
Recipes: **draw-on logo** — the logo as `path` items with a stroke and no fill,
|
|
1088
|
+
`trimEnd` keyed 0→100 (stagger the items' keys), then `fill` keyed from
|
|
1089
|
+
`"#ffffff00"` to `"#ffffff"`; **lower third** — a `rect` bar whose `w` keys
|
|
1090
|
+
from 0, a `line` drawn on under it, a title on the track above parented to the
|
|
1091
|
+
shape layer so they slide out together; **loading ring** — an ellipse with a
|
|
1092
|
+
stroke, `trimEnd: 25`, `trimOffset` keyed 0→100 with `ease:"linear"`;
|
|
1093
|
+
**orbit** — a null at the centre with `rotation` keyed 0→360, a shape parented
|
|
1094
|
+
to it with `x: 300`; **swing from the top** — `anchorY` at the top edge
|
|
1095
|
+
(minus half the height) and `rotation` keyed −10↔10; **burst** — a group of
|
|
1096
|
+
lines at different `rotation`s with `scale` keyed 0→1 and `opacity` 1→0.
|
|
1097
|
+
|
|
1098
|
+
### Motion paths, motion blur and text on a path
|
|
1099
|
+
|
|
1100
|
+
**Motion paths.** When `keyframes.x` and `keyframes.y` both have a key at the
|
|
1101
|
+
same time, that is a **position key**. Give position keys curve handles and
|
|
1102
|
+
the clip travels along a bezier path between them instead of a straight line.
|
|
1103
|
+
The handles are `pin` (arriving) and `pout` (leaving) on the x and y keys,
|
|
1104
|
+
in px relative to the key's position, in the clip's `x`/`y` space:
|
|
1105
|
+
|
|
1106
|
+
```jsonc
|
|
1107
|
+
"keyframes": {
|
|
1108
|
+
"x": [ { "t": 0, "v": -450, "pout": 250 }, { "t": 2, "v": 0, "pin": -250, "pout": 250 }, { "t": 4, "v": 450, "pin": -250 } ],
|
|
1109
|
+
"y": [ { "t": 0, "v": 150, "pout": -300 }, { "t": 2, "v": 0 }, { "t": 4, "v": 150, "pin": -300 } ]
|
|
1110
|
+
}
|
|
1111
|
+
```
|
|
1112
|
+
|
|
1113
|
+
Between two position keys the clip moves along the curve at the pace of the
|
|
1114
|
+
arriving key's `ease`, measured along the curve, so a `linear` key gives an
|
|
1115
|
+
even speed around a bend. A segment with an extra x or y key inside it, or
|
|
1116
|
+
with no handles, runs the channels as before. `autoOrient: true` turns the
|
|
1117
|
+
clip to face its direction of travel, and its `rotation` adds on top. A
|
|
1118
|
+
parent on a curved path carries its children along it.
|
|
1119
|
+
|
|
1120
|
+
**Motion blur.** `motionBlur` is a shutter angle: the clip is drawn at
|
|
1121
|
+
`motionBlurSamples` moments across `motionBlur / 360` of a frame, centred on
|
|
1122
|
+
the frame, and the samples are averaged. 180 is the film look; 360 smears a
|
|
1123
|
+
whole frame. It blurs whatever changes: position, scale, rotation, a parent's
|
|
1124
|
+
motion, keyed shapes and trims. A clip that does not move draws once. During
|
|
1125
|
+
playback the preview uses as many samples as fit in the frame time (all of them
|
|
1126
|
+
on a fast machine); a paused frame and export always use them all. It works on
|
|
1127
|
+
video, image, svg, text and shape clips.
|
|
1128
|
+
|
|
1129
|
+
**Text on a path.** `textPath` on a text clip is one shape item (`path`, `line`,
|
|
1130
|
+
`ellipse`, `rect`, `polygon` or `star`, with the same geometry keys and
|
|
1131
|
+
`keys` as in "Shapes, nulls and parenting", but no paint or trim) whose
|
|
1132
|
+
outline is the baseline, in the title's own px. The first line of text runs
|
|
1133
|
+
along it, each letter turned with the path; each further line (`\n`) runs on a
|
|
1134
|
+
parallel curve one line-height below, so its letters space by that curve's own
|
|
1135
|
+
length. `pathOffset` (% of the path's length, keyframable) is where the text
|
|
1136
|
+
sits, and `pathAlign` says which end of each line sits there. On a closed path
|
|
1137
|
+
the text wraps around; keying `pathOffset` 0 → 100 sends it once around a
|
|
1138
|
+
circle. Every text animation plays along the path. Font, size, colour,
|
|
1139
|
+
gradient, stroke, glow, shadow, `lineHeight` and `letterSpacing` all apply;
|
|
1140
|
+
text boxes and `align` do not (`pathAlign` takes their place).
|
|
1141
|
+
|
|
1142
|
+
```jsonc
|
|
1143
|
+
{ "kind": "text", "props": { "text": "AROUND AND AROUND • ", "font": "Anton", "fontSize": 40, "letterSpacing": 4,
|
|
1144
|
+
"textPath": { "type": "ellipse", "w": 300, "h": 300 }, "pathAlign": "start" },
|
|
1145
|
+
"keyframes": { "pathOffset": [ { "t": 0, "v": 0 }, { "t": 6, "v": 100, "ease": "linear" } ] } }
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
In the editor: when the selected clip has two or more position keys, its path
|
|
1149
|
+
shows on the monitor: the curve, a dot per frame (their spacing is the speed),
|
|
1150
|
+
a square per key and the keys' handles. Drag a key to move it, drag a handle to
|
|
1151
|
+
bend the path (Alt breaks the pair), Alt-drag a key to pull new handles, and
|
|
1152
|
+
double-click a key to make it a corner again. Moving the clip on the monitor
|
|
1153
|
+
or typing X / Y keys both channels together. The Inspector's **Motion**
|
|
1154
|
+
section has **Smooth** (handles through every position key), **Straight**,
|
|
1155
|
+
**Auto-orient**, **Motion blur** and **Blur samples**. A title's **Text path**
|
|
1156
|
+
section sets it on a **Line**, **Arc**, **Circle**, **Wave** or a **Pen** path
|
|
1157
|
+
(click points, drag to curve, Enter to finish, click the first point to close
|
|
1158
|
+
it). **Edit path** shows the path's points on the monitor, and **Offset** and
|
|
1159
|
+
**Align** place the text along it.
|
|
1160
|
+
|
|
1161
|
+
**Agents:**
|
|
1162
|
+
|
|
1163
|
+
- Write `pin` / `pout` on x / y keys directly, or
|
|
1164
|
+
`{op:"setMotionPath", id | ids, path:"smooth" | "straight", autoOrient?: true}`.
|
|
1165
|
+
`"smooth"` needs the position keys first (x and y keyed together at two or more times).
|
|
1166
|
+
- Motion blur: `updateClip` with `props:{motionBlur:180, motionBlurSamples:12}`.
|
|
1167
|
+
- `{op:"setTextPath", id | ids, path:"line" | "arc" | "circle" | "wave" | {type:"path", points:[…], closed:false, …} | null, offset?, align?}`
|
|
1168
|
+
sets, replaces or removes (`null`) a title's path; animate `pathOffset` with
|
|
1169
|
+
keyframes.
|
|
1170
|
+
- These are validated on `addClip` / `updateClip` too. The compact view shows
|
|
1171
|
+
`path:curved` after a clip's keyframes, and a title's `"textPath":"ellipse 300×300"`.
|
|
1172
|
+
|
|
1173
|
+
Recipes: **fly-in on a curve** — two position keys, `setMotionPath` smooth,
|
|
1174
|
+
`autoOrient:true`, `motionBlur:180`; **orbiting label** — circle `textPath`,
|
|
1175
|
+
`pathOffset` keyed 0 → 100 linear; **stamp along a road** — an open `path`
|
|
1176
|
+
textPath that follows the shape in the footage, `pathAlign:"center"`,
|
|
1177
|
+
`textAnim:"letter-pop"`; **whip without the transition** — a fast `x` move over
|
|
1178
|
+
4–6 frames with `motionBlur:360`.
|
|
1179
|
+
|
|
1180
|
+
### Compositions (nested sequences)
|
|
1181
|
+
|
|
1182
|
+
A **composition** (comp) is a timeline of its own, with its own size, length,
|
|
1183
|
+
tracks and clips, that is placed on the main timeline (or inside another comp)
|
|
1184
|
+
as one clip. Build a lower third, a logo sting or a whole scene once, then use
|
|
1185
|
+
it as many times as you like: every clip that shows the comp changes when the
|
|
1186
|
+
comp is edited. Comps live in `project.comps`:
|
|
1187
|
+
|
|
1188
|
+
```jsonc
|
|
1189
|
+
"comps": [
|
|
1190
|
+
{ "id": "lt", "name": "Lower third", "width": 1280, "height": 720, "duration": 5,
|
|
1191
|
+
"background": "#00000000", // optional; leave it out for transparent
|
|
1192
|
+
"tracks": [ { "id": "V2", "kind": "video" }, { "id": "V1", "kind": "video" }, { "id": "A1", "kind": "audio" } ],
|
|
1193
|
+
"clips": [
|
|
1194
|
+
{ "id": "bar", "kind": "shape", "track": "V1", "start": 0, "duration": 5, "props": { "shapes": [ … ] } },
|
|
1195
|
+
{ "id": "name", "kind": "text", "track": "V2", "start": 0.3, "duration": 4.7, "props": { "text": "Ada Lovelace" } },
|
|
1196
|
+
{ "id": "whoosh", "kind": "audio", "mediaId": "m_whoosh", "track": "A1", "start": 0, "in": 0, "duration": 0.6 }
|
|
1197
|
+
] }
|
|
1198
|
+
],
|
|
1199
|
+
"clips": [
|
|
1200
|
+
{ "id": "c_lt1", "kind": "comp", "compId": "lt", "mediaId": null, "track": "V3", "start": 4, "in": 0, "duration": 5,
|
|
1201
|
+
"props": { "fit": "none", "y": 220 } }
|
|
1202
|
+
]
|
|
1203
|
+
```
|
|
1204
|
+
|
|
1205
|
+
| key | default | does |
|
|
1206
|
+
|---|---|---|
|
|
1207
|
+
| `id` · `name` | — · the id | how clips and agents refer to it · the label shown on its clips and in the bin |
|
|
1208
|
+
| `width` · `height` | the project's | the comp's frame, px (16…8192) |
|
|
1209
|
+
| `duration` | — | seconds; what the comp shows past its clips is transparent (or its `background`) |
|
|
1210
|
+
| `background` | none | a colour behind the comp's clips; leave it out for transparent |
|
|
1211
|
+
| `tracks` | V3…V1, A1, A2 | the comp's own lanes, like the project's `tracks` |
|
|
1212
|
+
| `clips` | [] | ordinary clips of any kind (comp clips too), with times inside the comp |
|
|
1213
|
+
| `template` | — | set on a placed template: `{id, hold, fields}` (see "Presets and templates") |
|
|
1214
|
+
|
|
1215
|
+
**A comp clip** (`kind:"comp"`, `compId`, no `mediaId`) shows the comp from its
|
|
1216
|
+
`in` (comp seconds) for its `duration`, at speed 1. It draws like a picture of
|
|
1217
|
+
the comp's size: `fit` defaults to `none` (the comp's own pixels), and its
|
|
1218
|
+
transform, keyframes, opacity, transitions, masks, track matte, blend, grade and
|
|
1219
|
+
filters all apply to the comp's frame as a whole. Trims and splits move its
|
|
1220
|
+
`in` like a video clip's; the window can't run past the comp's length.
|
|
1221
|
+
|
|
1222
|
+
The media inside a comp **plays and exports as if it were on the timeline**:
|
|
1223
|
+
its video frames, and its audio, which goes through the main timeline's audio
|
|
1224
|
+
track of the same name (its fader, effects and bus). A comp clip that is
|
|
1225
|
+
disabled, or sits on a disabled track, silences the comp's audio too. Comps
|
|
1226
|
+
nest up to 4 deep, and a comp can never contain itself.
|
|
1227
|
+
|
|
1228
|
+
In the editor: the bin's **Project** tab lists the comps; drag one onto the
|
|
1229
|
+
timeline to place it, and double-click it (or a comp clip, or **Open** in the
|
|
1230
|
+
Inspector's **Composition** section) to open it. An open comp takes over the
|
|
1231
|
+
timeline: every tool, shortcut and inspector works on its clips, and the
|
|
1232
|
+
breadcrumb above the timeline (or **Esc**, once nothing is selected) goes back
|
|
1233
|
+
out. Right-click clips → **Precompose** moves them into a new comp and puts a
|
|
1234
|
+
comp clip in their place; right-click the Project tab → **New composition**
|
|
1235
|
+
starts an empty one. The Composition section also sets the comp's name, size,
|
|
1236
|
+
length and background.
|
|
1237
|
+
|
|
1238
|
+
**Agents:**
|
|
1239
|
+
|
|
1240
|
+
- `{op:"addComp", comp:{id?, name, duration, width?, height?, background?, tracks?, clips?:[…]}}`
|
|
1241
|
+
adds one (its clips go through the same checks as `addClip`).
|
|
1242
|
+
`{op:"updateComp", id, set:{name?, width?, height?, duration?, background?, tracks?}}`
|
|
1243
|
+
changes it; `{op:"removeComp", id}` is refused while a clip still uses it.
|
|
1244
|
+
- Place it with `addClip {kind:"comp", compId, track, start, duration, in?}`.
|
|
1245
|
+
- `{op:"inComp", comp, ops:[…]}` runs clip and timeline ops on the comp's own
|
|
1246
|
+
clips: `addClip`, `updateClip`, `removeClip`, `split`, `rippleDelete`, the
|
|
1247
|
+
trims, `setShape`, `setMask`, `setGrade`, `setParent`, `setMotionPath`,
|
|
1248
|
+
`setTextPath`, clip `setFx`, `precompose`, and `inComp` for a comp inside it.
|
|
1249
|
+
- `{op:"precompose", ids:[…], name?, id?}` moves those clips (here, or in the
|
|
1250
|
+
comp an `inComp` is running in) into a new comp that starts at the earliest
|
|
1251
|
+
of them; a comp clip of the same length takes their place.
|
|
1252
|
+
- The compact view shows a comp clip as `comp(lt in:1.5)` and lists every comp
|
|
1253
|
+
under `COMPS` with its clips.
|
|
1254
|
+
|
|
1255
|
+
Recipes: **reusable lower third** — a template (next section) gives every
|
|
1256
|
+
speaker their own copy with their own name; a plain comp suits a graphic that
|
|
1257
|
+
is the same every time; **logo sting** — a draw-on logo comp at the head and tail of the edit;
|
|
1258
|
+
**picture in picture of a whole scene** — precompose the scene's clips, then
|
|
1259
|
+
scale the comp clip down and move it into a corner;
|
|
1260
|
+
**one grade for a sequence** — precompose it and grade the comp clip once.
|
|
1261
|
+
|
|
1262
|
+
### Presets and templates
|
|
1263
|
+
|
|
1264
|
+
Two ways to reuse motion: an **animation preset** keys a named animation onto a
|
|
1265
|
+
clip, and a **template** is a ready-made comp (lower third, logo animation,
|
|
1266
|
+
social layout) whose text, colours and pictures are **fields** you fill in.
|
|
1267
|
+
|
|
1268
|
+
**Presets.** A preset keys the clip's own channels, relative to where the clip
|
|
1269
|
+
already is: `scale` and `opacity` multiply the clip's value and everything else
|
|
1270
|
+
adds to it, so "Slide up" works wherever the clip sits and however big it is.
|
|
1271
|
+
Each has an anchor: **in** plays from the clip's start and settles on its
|
|
1272
|
+
values, **out** starts from them and ends at the clip's end, **at** is an
|
|
1273
|
+
emphasis at a time you pick (the playhead, or `at`), and **loop** spans the
|
|
1274
|
+
whole clip. Keys already in the window a preset covers are replaced; the rest
|
|
1275
|
+
stay, so an In, an Out and a loop on another channel live side by side.
|
|
1276
|
+
|
|
1277
|
+
| group | presets |
|
|
1278
|
+
|---|---|
|
|
1279
|
+
| In | `fade-in` · `pop-in` · `slide-up-in` · `slide-down-in` · `slide-left-in` (from the right) · `slide-right-in` (from the left) · `drop-in` · `zoom-in` · `spin-in` · `whip-in` (with motion blur) · `rise-blur` · `draw-on` (shape layers) |
|
|
1280
|
+
| Out | `fade-out` · `pop-out` · `slide-up-out` · `slide-down-out` · `slide-left-out` · `slide-right-out` · `zoom-out` · `spin-out` · `whip-out` · `sink-blur` · `draw-off` |
|
|
1281
|
+
| At (emphasis) | `pulse` · `shake` · `wobble` · `bounce` · `flash` |
|
|
1282
|
+
| Loop | `float` · `breathe` · `sway` · `spin` · `wiggle` |
|
|
1283
|
+
|
|
1284
|
+
Every In / Out / At preset is 0.35–1 s long; `length` stretches it (a clip
|
|
1285
|
+
shorter than the preset squeezes it in). A saved preset is a JSON file in
|
|
1286
|
+
`library/presets/`, shared by every project:
|
|
1287
|
+
|
|
1288
|
+
```jsonc
|
|
1289
|
+
{ "id": "fly-in", "name": "Fly in", "group": "Saved", "anchor": "in", "length": 0.6,
|
|
1290
|
+
"keys": { "x": [ { "t": 0, "v": -500 }, { "t": 0.6, "v": 0, "ease": "ease-out" } ],
|
|
1291
|
+
"opacity": [ { "t": 0, "v": 0 }, { "t": 0.3, "v": 1 } ] },
|
|
1292
|
+
"props": { "motionBlur": 180 } } // optional: motionBlur, motionBlurSamples
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
`t` is seconds into the preset (for a loop, 0…1 of the clip). Channels: `x y
|
|
1296
|
+
scale rotation opacity blur anchorX anchorY trimStart trimEnd trimOffset
|
|
1297
|
+
brightness saturation hue grayscale shake rgbSplit glow fontSize letterSpacing
|
|
1298
|
+
pathOffset`.
|
|
1299
|
+
|
|
1300
|
+
**Templates.** Placing one adds its own comp (so two lower thirds can show two
|
|
1301
|
+
names) and a comp clip showing it with `fit:"contain"`; a template made for
|
|
1302
|
+
another shape than the canvas sits inside it, letterboxed. The comp carries
|
|
1303
|
+
what makes it a template:
|
|
1304
|
+
|
|
1305
|
+
```jsonc
|
|
1306
|
+
{ "id": "comp_x1", "name": "Lower third — Bar", "width": 1920, "height": 1080, "duration": 5, "clips": [ … ],
|
|
1307
|
+
"template": { "id": "lower-third-bar", "hold": 4.5, "fields": [
|
|
1308
|
+
{ "key": "name", "label": "Name", "type": "text", "targets": [ "name:props.text" ], "value": "Ava Moreno" },
|
|
1309
|
+
{ "key": "accent", "label": "Accent", "type": "color", "targets": [ "bar:props.shapes.1.fill" ], "value": "#ffd166" } ] } }
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
| template | name | size | length | fields |
|
|
1313
|
+
|---|---|---|---|---|
|
|
1314
|
+
| `lower-third-bar` | Lower third — Bar | 1920×1080 | 5 s | `name` `title` `accent` `panel` |
|
|
1315
|
+
| `lower-third-underline` | Lower third — Underline | 1920×1080 | 5 s | `name` `title` `accent` |
|
|
1316
|
+
| `lower-third-boxes` | Lower third — Boxes | 1920×1080 | 5 s | `name` `title` `accent` `box` |
|
|
1317
|
+
| `logo-ring` | Logo — Ring reveal | 1920×1080 | 4 s | `logo` `brand` `accent` |
|
|
1318
|
+
| `logo-slam` | Logo — Slam | 1920×1080 | 3.5 s | `logo` `accent` |
|
|
1319
|
+
| `logo-lines` | Logo — Lines | 1920×1080 | 4 s | `logo` `tagline` `lines` |
|
|
1320
|
+
| `social-story` | Story — Headline (9:16) | 1080×1920 | 6 s | `headline` `subline` `button` `accent` `background` |
|
|
1321
|
+
| `social-quote` | Post — Quote (1:1) | 1080×1080 | 6 s | `quote` `author` `accent` `background` |
|
|
1322
|
+
| `social-end-card` | End card — Follow (16:9) | 1920×1080 | 6 s | `heading` `button` `accent` `background` |
|
|
1323
|
+
|
|
1324
|
+
Field types: `text` (a string), `color` (`#rgb`, `#rrggbb` or `#rrggbbaa`) and
|
|
1325
|
+
`media` (a media id of a picture: image, svg or video; the clip's kind follows
|
|
1326
|
+
it). A target is `"<clip id>:<path>"` inside the comp (`props.text`,
|
|
1327
|
+
`props.shapes.1.fill`, `mediaId`, …) or `"comp:background"`; one field can set
|
|
1328
|
+
several. The logo templates start with a placeholder,
|
|
1329
|
+
`/library/elements/logo-placeholder.svg`, until the `logo` field gets a
|
|
1330
|
+
picture. **`hold`** is when the exit starts: changing a template's length
|
|
1331
|
+
keeps everything after it at the same distance from the end, so a longer lower
|
|
1332
|
+
third just holds longer (it can't get shorter than its entrance needs).
|
|
1333
|
+
|
|
1334
|
+
A saved template (`library/templates/<id>.json`) is a template object:
|
|
1335
|
+
`{id, name, group, width, height, duration, hold, background?, tracks?, clips,
|
|
1336
|
+
media?, fields}`; `media` lists the `{id, name, kind, src}` entries its clips
|
|
1337
|
+
use (`/library/…` or `/media/…`), matched by `src` when it is placed. A saved
|
|
1338
|
+
preset or template whose name a built-in already has is saved as
|
|
1339
|
+
"<name> (saved)".
|
|
1340
|
+
|
|
1341
|
+
In the editor: the bin's **Templates** tab lists them by group (Lower thirds,
|
|
1342
|
+
Logo animations, Social, then Saved); drag one onto the timeline or click +.
|
|
1343
|
+
The Inspector's **Template** section on its clip edits the fields, its
|
|
1344
|
+
**Length**, and offers **Use W×H canvas** when the template was made for
|
|
1345
|
+
another shape. Right-click a comp in the Project tab → **Save as template**
|
|
1346
|
+
keeps it (every title's text and every picture become fields). The
|
|
1347
|
+
**Motion** section's **Presets** menus (In, Out, At playhead, Loop) put a
|
|
1348
|
+
preset on the selected clips, and **Save as preset** keeps this clip's
|
|
1349
|
+
keyframes (In: from its start to its last key; Out: from its first key to its
|
|
1350
|
+
end; Loop: the whole clip).
|
|
1351
|
+
|
|
1352
|
+
**Agents:**
|
|
1353
|
+
|
|
1354
|
+
- `{op:"applyPreset", id | ids, preset, length?, at?}` — `preset` is a name,
|
|
1355
|
+
a saved one's id, or an inline preset object; `at` is timeline seconds (for
|
|
1356
|
+
an At preset; default the clip's start). Visual clips only.
|
|
1357
|
+
- `{op:"addTemplate", template, start?, track?, duration?, fields?:{key: value}, id?, compId?, name?}`
|
|
1358
|
+
places one (default track V2, at 0, its own length).
|
|
1359
|
+
- `{op:"setTemplate", id, fields?:{key: value}, duration?}` — `id` is its comp
|
|
1360
|
+
clip or its comp; a new `duration` retimes it, and clips that showed it to
|
|
1361
|
+
its end keep doing so.
|
|
1362
|
+
- `{op:"savePreset", id, name, anchor?, from?, to?}` (`from`/`to` in clip
|
|
1363
|
+
seconds) or `{op:"savePreset", preset:{…}}`; `{op:"saveTemplate", comp,
|
|
1364
|
+
name?, fields?, hold?}` or `{op:"saveTemplate", template:{…}}`. Files are
|
|
1365
|
+
written once the patch is saved, and later ops in the same patch can use them.
|
|
1366
|
+
- `applyPreset`, `addTemplate` and `setTemplate` also run inside `inComp`.
|
|
1367
|
+
- The compact view prints a placed template's fields under its comp:
|
|
1368
|
+
`template lower-third-bar — fields (setTemplate): name="Ava Moreno" …`.
|
|
1369
|
+
|
|
1370
|
+
Recipes: **name every speaker** — `addTemplate lower-third-bar` once per
|
|
1371
|
+
speaker with `fields:{name, title}`; **branded intro** — `addTemplate logo-ring`
|
|
1372
|
+
with `fields:{logo:"<your logo's media id>", brand, accent}` at 0;
|
|
1373
|
+
**vertical teaser** — set the canvas to 1080×1920, `addTemplate social-story`;
|
|
1374
|
+
**house style** — build a comp the way you like it, `saveTemplate`, then place
|
|
1375
|
+
it in every project; **titles that arrive and leave the same way** —
|
|
1376
|
+
`applyPreset` an In and an Out on every title in one patch.
|
|
1377
|
+
|
|
1378
|
+
### Media management
|
|
1379
|
+
|
|
1380
|
+
The Project bin knows each file's details and helps find, group and track
|
|
1381
|
+
footage. Everything here is organization: none of it changes what plays or
|
|
1382
|
+
exports.
|
|
1383
|
+
|
|
1384
|
+
**Metadata.** The editor reads every file in `media/` and `library/` with
|
|
1385
|
+
ffprobe (via `GET /api/media-info`) and writes what it finds onto the media
|
|
1386
|
+
entry. These fields are read-only — they describe the file:
|
|
1387
|
+
|
|
1388
|
+
| key | does |
|
|
1389
|
+
|---|---|
|
|
1390
|
+
| `fps` | video frame rate (29.97, 25, 59.94 …); absent for stills and audio |
|
|
1391
|
+
| `codec` | video codec (`h264`, `hevc`, `prores`, `vp9` …), or the image format (`png`, `mjpeg`) |
|
|
1392
|
+
| `audioCodec` · `audioChannels` · `sampleRate` | first audio stream: codec, channel count (`0` = the file has no audio), Hz |
|
|
1393
|
+
| `size` · `bitrate` | file size in bytes · overall bitrate, bits per second |
|
|
1394
|
+
| `width` · `height` · `duration` | as before; filled in when missing (phone footage shot rotated reports its upright size) |
|
|
1395
|
+
|
|
1396
|
+
Without ffprobe on PATH only `size` is filled in; the browser still probes
|
|
1397
|
+
`duration`, `width` and `height`.
|
|
1398
|
+
|
|
1399
|
+
**Organizing.** `favorite: true` stars a file; `tags` is a list of free-form
|
|
1400
|
+
labels (at most 24, each up to 32 characters, matched without regard to case).
|
|
1401
|
+
`collections` at the top of the document group media across folders — a file
|
|
1402
|
+
can be in any number of them, while it sits in one folder (`folderId`).
|
|
1403
|
+
|
|
1404
|
+
**Usage.** A file's *placements* are the clips that show it, on the main
|
|
1405
|
+
timeline and inside every composition. A video and the audio stems linked to it
|
|
1406
|
+
count as one placement, so a shot used twice reads "used 2×".
|
|
1407
|
+
|
|
1408
|
+
In the editor, the Project tab has:
|
|
1409
|
+
|
|
1410
|
+
- **Search** — every word must match the name, kind, extension, codec,
|
|
1411
|
+
resolution (`3840x2160`, `2160p`) or a tag; `#word` matches tags only. Esc clears it.
|
|
1412
|
+
- **Show** — All media · ★ Favorites · Used in the edit · Unused · a kind · a
|
|
1413
|
+
collection · a tag. While searching or filtering, the bin lists the hits flat
|
|
1414
|
+
(each with its folder), with a count and **Clear**; a collection's view also
|
|
1415
|
+
has **Rename** and **Delete** (its media stay).
|
|
1416
|
+
- **Sort** — name (natural: `shot 2` before `shot 10`), kind, duration,
|
|
1417
|
+
resolution, frame rate, codec, audio channels, file size or times used, with
|
|
1418
|
+
↑ / ↓. Files missing a value sort last either way. Folders keep their tree;
|
|
1419
|
+
the sort applies inside each.
|
|
1420
|
+
- **☰ List view** — one row per file with columns: name, duration, resolution,
|
|
1421
|
+
FPS, codec, audio, size and uses; click a column heading to sort by it. On a
|
|
1422
|
+
narrow bin the columns drop out right to left; widen the panel to see them all.
|
|
1423
|
+
- On each file: **☆** stars it, the **uses** badge opens its placements.
|
|
1424
|
+
Right-click a file → **Open in Source**, **Find timeline instances** (every
|
|
1425
|
+
placement, main timeline and comps, with track and time — click one to jump
|
|
1426
|
+
there, opening its comp if needed), **Media info…**, favorites, **Tags…**,
|
|
1427
|
+
**Add to / Remove from** a collection, **New collection…**, **Remove from project**.
|
|
1428
|
+
- Right-click a clip on the timeline → **Reveal in project** scrolls the bin to
|
|
1429
|
+
its file (or its comp), opening folders and clearing a search that hides it.
|
|
1430
|
+
|
|
1431
|
+
**Offline media.** A file is *offline* when its `src` names nothing under
|
|
1432
|
+
`media/` or `library/` — it was moved, renamed or deleted. The editor checks
|
|
1433
|
+
on load and whenever those folders change. An offline file's clips stay where
|
|
1434
|
+
they are: the timeline hatches them red (**⚠ Offline**), the monitor and the
|
|
1435
|
+
export draw a **Media offline** slate in the picture's place, and its audio is
|
|
1436
|
+
silent. The bin marks the file **OFFLINE**, says how many are missing, and the
|
|
1437
|
+
Show filter gains **⚠ Offline**. The Export dialog warns before rendering one.
|
|
1438
|
+
Files under `media/` may sit in subfolders (`/media/archive/a.mp4`).
|
|
1439
|
+
|
|
1440
|
+
- **Relink** points a media entry at the same footage where it is now; clips
|
|
1441
|
+
keep their In points, edits and everything else. Right-click the file →
|
|
1442
|
+
**Relink…** (or **Relink…** on the offline banner for all of them). The
|
|
1443
|
+
dialog looks through `media/` and `library/` for each file — same name, the
|
|
1444
|
+
same name with another extension, a similar name, the same size — and
|
|
1445
|
+
picks a same-name match for you. **Browse…** picks one file and **Choose
|
|
1446
|
+
folder…** matches a whole folder by name; either copies the chosen files
|
|
1447
|
+
into `media/`. A relinked file whose length differs from the original gets
|
|
1448
|
+
a warning (is it really the same footage?).
|
|
1449
|
+
- **Replace file…** (right-click a file) puts a *different* file behind the
|
|
1450
|
+
media entry — a new render, a better take. Every clip using it switches;
|
|
1451
|
+
each keeps its In point and timing, trimmed to fit a shorter file. The name
|
|
1452
|
+
and details follow the new file; favorites, tags and collections stay. To
|
|
1453
|
+
swap the file of one clip only, use the Inspector's **Source** menu instead.
|
|
1454
|
+
- Relink and replace are one undo step each.
|
|
1455
|
+
|
|
1456
|
+
**Proxies.** Heavy footage — 4K, HEVC, ProRes, long phone clips — can stutter
|
|
1457
|
+
while you scrub and play. A *proxy* is a light copy the editor plays instead
|
|
1458
|
+
while you edit: H.264 at 540p (or 720p / 1080p, never larger than the
|
|
1459
|
+
source), a keyframe every 12 frames so seeking is quick, the source's own
|
|
1460
|
+
timestamps (frame for frame — no frames added or dropped) and its first audio
|
|
1461
|
+
stream. The edit always refers to the original: the timeline, `project.json`
|
|
1462
|
+
and every clip stay the same whether proxies are on or off.
|
|
1463
|
+
|
|
1464
|
+
**Export always reads the originals**, and so do `fablecut_frame` (timeline
|
|
1465
|
+
mode), `fablecut_scopes`, tracking and every other measurement — the
|
|
1466
|
+
players switch to the original files for the render and back afterwards. A
|
|
1467
|
+
proxy only changes how smoothly the preview runs, never the result.
|
|
1468
|
+
|
|
1469
|
+
- Right-click a video in the bin → **Create proxy (540p)** / **(720p)**;
|
|
1470
|
+
right-click the **Project** tab → **Create proxies for N videos**. ffmpeg
|
|
1471
|
+
makes them on the server one at a time in the background; the bin shows
|
|
1472
|
+
each file's progress (**P 42%**), then a green **P** once it is ready.
|
|
1473
|
+
**Delete proxy** removes one. The bin's list view has a **Proxy** column,
|
|
1474
|
+
Show has **Has a proxy** / **Video without a proxy**, and **Media info**
|
|
1475
|
+
says which file is playing.
|
|
1476
|
+
- The monitor's **Proxy** button turns proxy playback on and off (on by
|
|
1477
|
+
default; remembered in this browser). The Source monitor follows it too.
|
|
1478
|
+
- Proxies live in `analysis/proxies/`, a disposable cache: each is named
|
|
1479
|
+
after its source file *and that file's size and modification time*, so a
|
|
1480
|
+
re-rendered or replaced original simply has no proxy until you make a new
|
|
1481
|
+
one. Deleting the folder loses nothing but time. Proxies need ffmpeg.
|
|
1482
|
+
|
|
1483
|
+
**Agents:**
|
|
1484
|
+
|
|
1485
|
+
- `{op:"updateMedia", id | ids, set:{name?, favorite?, tags?, folderId?}, addTags?, removeTags?}`
|
|
1486
|
+
— organize files (`favorite:false` unstars; `tags:[]` clears). Metadata keys
|
|
1487
|
+
are refused: they come from the file.
|
|
1488
|
+
- `{op:"setCollection", id?, name?, media?, add?, remove?}` creates a collection
|
|
1489
|
+
(a new `id`, or a `name` no collection has) or changes one: `media` replaces
|
|
1490
|
+
its list, `add` / `remove` edit it. `{op:"removeCollection", id}` deletes it;
|
|
1491
|
+
its media stay. `removeMedia` takes a file out of every collection.
|
|
1492
|
+
- The compact view lists each file with its metadata, ★, tags, folder and
|
|
1493
|
+
`uses:N`, and the collections on one line:
|
|
1494
|
+
`m_abc video "intro.mp4" 12.4s 1920×1080 29.97 fps h264 stereo 48 kHz 45 MB ★ #interview uses:2`.
|
|
1495
|
+
- `fablecut_import_media` probes the file as it imports it, so the entry it
|
|
1496
|
+
returns already has these fields.
|
|
1497
|
+
- `fablecut_status` lists offline media, and the compact view marks each
|
|
1498
|
+
`OFFLINE (file missing — relinkMedia)` and each video with a proxy `proxy:540p`.
|
|
1499
|
+
- `fablecut_proxy {}` reports every video's proxy; `{create:true, mediaIds?, height?}`
|
|
1500
|
+
makes them (waits until done unless `wait:false`); `{delete:true, mediaIds?}`
|
|
1501
|
+
removes them. Proxies never change what you measure or export, so an agent
|
|
1502
|
+
only needs them when the user asks for smoother playback.
|
|
1503
|
+
- `{op:"relinkMedia", id, src:"/media/…", name?}` — the same footage found
|
|
1504
|
+
elsewhere: put the file in `media/` (copy it, or `fablecut_import_media` and
|
|
1505
|
+
`removeMedia` the extra entry it registers), then relink. The note warns when
|
|
1506
|
+
the new file's length differs.
|
|
1507
|
+
- `{op:"replaceMedia", id, src, name?}` — a different file behind the entry;
|
|
1508
|
+
clips keep their timing, trimmed to fit (the note says how many). `src` must
|
|
1509
|
+
be an existing file under `/media/` or `/library/` of the same kind (an
|
|
1510
|
+
audio entry may use a video container).
|
|
1511
|
+
|
|
928
1512
|
### Semantics
|
|
929
1513
|
|
|
930
1514
|
- Rendering order: V1 is drawn first, then V2, V3, … on top. SVG/image/text clips
|
|
@@ -1020,6 +1604,44 @@ track the button, attach a rect svg.
|
|
|
1020
1604
|
- `bgRemove` and `chromaKey` can combine with all filters; heavy pixel work is
|
|
1021
1605
|
automatic (only runs when those props are set).
|
|
1022
1606
|
|
|
1607
|
+
## Look at the footage (`fablecut_frame`)
|
|
1608
|
+
|
|
1609
|
+
`fablecut_analyze_reference` gives you the **ears** — where the cuts are, how hard
|
|
1610
|
+
each shot hits. `fablecut_frame` gives you the **eyes**. It answers with a real
|
|
1611
|
+
image (an MCP `image` content block), so you can judge a shot instead of
|
|
1612
|
+
inferring it from an `energy` value.
|
|
1613
|
+
|
|
1614
|
+
| call | returns |
|
|
1615
|
+
|---|---|
|
|
1616
|
+
| `{path:"/media/x.mp4", t:3.2}` | that exact frame |
|
|
1617
|
+
| `{path:"/media/x.mp4", frames:12}` | a 4×3 contact sheet across the whole clip |
|
|
1618
|
+
| `{path:"/media/x.mp4", frames:12, from:30, to:90}` | a contact sheet across part of it |
|
|
1619
|
+
| `{time:8.5}` (no `path`) | the **composed timeline** at 8.5 s, graded, all tracks |
|
|
1620
|
+
| `{path}` with an absolute path | the file is read in place without copying into `media/` |
|
|
1621
|
+
| `{path, text:true}` | text-only statistics (luma, colors, texture, saturation, motion) for non-vision agents |
|
|
1622
|
+
|
|
1623
|
+
Each answer leads with a text line naming the source, the timestamp(s) and (in
|
|
1624
|
+
timeline mode) the clips on screen, so you always know what you are looking at.
|
|
1625
|
+
`width` sets the pixel width (default 768 for one frame, 320 per contact-sheet
|
|
1626
|
+
cell); `text:true` returns descriptive statistics instead of an image block, for
|
|
1627
|
+
agents without vision capability — luma levels (min, p1, median, mean, p99, max,
|
|
1628
|
+
all 0–1 like `fablecut_scopes`), dominant colors, texture, saturation and
|
|
1629
|
+
per-cell motion; `where`/`timeout` apply to timeline mode only.
|
|
1630
|
+
|
|
1631
|
+
The REST endpoint `/api/frame` accepts `src`, `t`, `frames`, `from`, `to`, `w`,
|
|
1632
|
+
`q` (JPEG quality 1–31) and `text=1` (statistics mode) as query parameters.
|
|
1633
|
+
|
|
1634
|
+
**When it pays.** Before choosing a clip's `in` point, sample a few candidate
|
|
1635
|
+
moments — the blueprint tells you a shot spans 2.1 s–4.9 s and reads
|
|
1636
|
+
`energy: 88`, but only a frame tells you whether that is a car jump or a pan
|
|
1637
|
+
across a landscape. Contact sheets are the cheap survey: one call shows you
|
|
1638
|
+
what a whole 3-minute clip holds, then you grab single frames where it matters.
|
|
1639
|
+
Timeline mode answers "what did I actually build?" — the same picture the user
|
|
1640
|
+
sees and export renders, grades and transitions included.
|
|
1641
|
+
|
|
1642
|
+
Needs ffmpeg on PATH for the source modes. Timeline mode runs in the open editor
|
|
1643
|
+
tab, else a headless browser, like `fablecut_export`.
|
|
1644
|
+
|
|
1023
1645
|
## Remake a reference video (analyze → blueprint → rebuild)
|
|
1024
1646
|
|
|
1025
1647
|
Given a reference edit (a reel/montage the user likes), FableCut can analyze it
|
|
@@ -1059,6 +1681,8 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
|
|
|
1059
1681
|
(hard cuts by default — that's what shot detection saw). Pick source footage
|
|
1060
1682
|
whose motion matches each shot's `energy` (calm ≤40, action ≥70), and choose
|
|
1061
1683
|
each clip's `in` so something interesting happens inside the window.
|
|
1684
|
+
**Then look**: `fablecut_frame {path, frames:12}` to survey a candidate clip,
|
|
1685
|
+
`{path, t}` at the exact moment you want, before committing to an `in`.
|
|
1062
1686
|
4. The `drop`: put the hero shot there; classic garnish = a speed ramp landing
|
|
1063
1687
|
on it, an impact `adjust` layer (shake+rgbSplit), or a whip transition.
|
|
1064
1688
|
5. Pacing garnish to taste: shots shorter than ~0.6 s read as beat-flashes;
|
|
@@ -1074,7 +1698,27 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
|
|
|
1074
1698
|
Append `?force=1` to overwrite unconditionally. Writes are atomic (tmp file +
|
|
1075
1699
|
rename), so a crashed write never corrupts the file.
|
|
1076
1700
|
- `GET /api/media` — list files in ./media (name, src, size)
|
|
1077
|
-
- `GET /api/
|
|
1701
|
+
- `GET /api/media-info?src=/media/x.mp4` → `{src, info}` · `POST /api/media-info {srcs:[…], stat?}` →
|
|
1702
|
+
`{info:{src: info}}` (up to 2000; `stat:true` answers only `{size, mtimeMs}`, no ffprobe —
|
|
1703
|
+
how the editor checks for offline files) — a file's metadata from ffprobe: `size`, `fps`, `codec`,
|
|
1704
|
+
`audioCodec`, `audioChannels`, `sampleRate`, `bitrate`, `duration`, `width`, `height` and
|
|
1705
|
+
`probe` (`"ffprobe"`, or `"stat"` when only the size could be read). `info` is `null` when
|
|
1706
|
+
the src names no file under `/media/` or `/library/`. Cached per file version.
|
|
1707
|
+
- `POST /api/proxy {srcs:[…], height?:540|720|1080}` → `{status:{src: status}}` queues proxies
|
|
1708
|
+
(ffmpeg, one at a time); `POST /api/proxy/status {srcs}` → `{status:{src: status | null}}`;
|
|
1709
|
+
`POST /api/proxy/delete {srcs}` → `{deleted:{src: n}}`. A status is
|
|
1710
|
+
`{state:"ready", src:"/proxies/…", width, height, size}`, `{state:"queued"|"running", progress, height}`,
|
|
1711
|
+
`{state:"failed", error}` or `{state:"none"}` (`null` = no such source file). Progress and
|
|
1712
|
+
results are also pushed as SSE event `proxy` (`{src, status}`). `GET /proxies/<name>` serves
|
|
1713
|
+
a proxy file from `analysis/proxies/`. `/api/proxy` answers 503 without ffmpeg.
|
|
1714
|
+
- `POST /api/relink/candidates {media:[{id, name, src?, size?, kind?}]}` →
|
|
1715
|
+
`{candidates:{id:[{src, name, size, score, why}]}}` — where a missing file might be now:
|
|
1716
|
+
files under `media/` and `library/` (subfolders included) of the same kind, ranked by
|
|
1717
|
+
name (same name, same name with another extension, similar name) and size, best 5 each.
|
|
1718
|
+
- `GET /api/library?dir=sfx|elements|svg|fonts|presets|templates` — list library assets
|
|
1719
|
+
- `GET /api/presets` · `GET /api/templates` — the saved presets / templates, validated (bad files skipped);
|
|
1720
|
+
`POST /api/presets {preset}` · `POST /api/templates {template}` — validate one and save it as
|
|
1721
|
+
`library/presets/<id>.json` / `library/templates/<id>.json`, returning `{id, name, file}`
|
|
1078
1722
|
- `POST /api/upload?name=foo.mp4` — raw body saved into ./media, returns `{src}`.
|
|
1079
1723
|
MP4/MOV/M4V uploads are auto-remuxed with `+faststart` (needs ffmpeg on PATH).
|
|
1080
1724
|
Files copied straight into ./media by external tools skip this — remux big ones
|
|
@@ -1096,9 +1740,27 @@ obvious cuts were missed, raise it if motion is being misread as cuts.
|
|
|
1096
1740
|
- `POST /api/analyze` — body `{src:"/media/ref.mp4", threshold?, music?}`: analyze a
|
|
1097
1741
|
reference video into an edit blueprint (see "Remake a reference video"); extracts
|
|
1098
1742
|
its music into ./media. `GET /api/analyze?src=…` returns the cached blueprint.
|
|
1743
|
+
- `GET /api/frame?src=/media/x.mp4&t=3.2` — one JPEG still of a source clip, the
|
|
1744
|
+
"eyes" to go with `/api/analyze`'s ears (see "Look at the footage"). Optional
|
|
1745
|
+
`w` (pixel width, default 768) and `q` (ffmpeg `-q:v` 1–31, default 4).
|
|
1746
|
+
`&frames=N[&cols=][&from=][&to=][&w=320]` instead returns one N-up **contact
|
|
1747
|
+
sheet** across the clip (N 2–60; `cols` omitted → the squarest layout, so 6
|
|
1748
|
+
frames give 3×2 rather than 4×2 with two black cells). `src` must name an
|
|
1749
|
+
existing file under `/media/` or `/library/` (404 otherwise). `?path=` reads
|
|
1750
|
+
any file on the machine but only with the `X-FableCut-Client: mcp` request
|
|
1751
|
+
header, which only the local MCP client sends — without it the request is
|
|
1752
|
+
refused (403) before the path is even resolved. Results are cached in
|
|
1753
|
+
`analysis/frames/` keyed by source path, size and mtime; the cache is capped
|
|
1754
|
+
(500 files / 128 MB, oldest first). The response carries
|
|
1755
|
+
`X-FableCut-Frame-Times` (comma-separated sample times, so a sheet's cells can
|
|
1756
|
+
be mapped back to timestamps) and `X-FableCut-Frame-Source`.
|
|
1757
|
+
- `POST /api/scopes/request` also takes `{"image":true}`, which makes the editor
|
|
1758
|
+
return the measured still as a JPEG data URL in `result.image` — how
|
|
1759
|
+
`fablecut_frame {time}` shows an agent the composed program picture.
|
|
1099
1760
|
- `GET /api/events` — SSE: named event `change` when project.json, ./media or
|
|
1100
1761
|
./library changes; named event `profiles` when `encoding-profiles.json` changes
|
|
1101
|
-
(UI refreshes the export-profile list only — no project reload)
|
|
1762
|
+
(UI refreshes the export-profile list only — no project reload); named event
|
|
1763
|
+
`proxy` as a proxy is queued, makes progress, finishes or is deleted
|
|
1102
1764
|
- Fast / WebCodecs export (browser compositor → server ffmpeg):
|
|
1103
1765
|
`GET /api/export/ffmpeg` → `{available}` · `GET /api/export/profiles[?detail=1]` →
|
|
1104
1766
|
`{default, profiles, issues}` · `POST /api/export/begin`
|
|
@@ -1341,12 +2003,135 @@ agent last wrote, so an export right after a patch includes that patch.
|
|
|
1341
2003
|
|
|
1342
2004
|
- `fablecut_export {}` — waits, then returns the file path under `exports/`.
|
|
1343
2005
|
Range: `inPoint`→`outPoint` when either is set, else the whole timeline;
|
|
1344
|
-
`range:"entire"|"in-out"` overrides.
|
|
2006
|
+
`range:"entire"|"in-out"` overrides.
|
|
2007
|
+
- `preset:"instagram-reels"` picks a delivery preset; `delivery:{…}` changes any of its
|
|
2008
|
+
settings (`{preset:"youtube", delivery:{fps:60, video:{rate:"vbr", bitrate:16000}}}`).
|
|
2009
|
+
`profile:"hq"` picks a raw encoding profile instead. With neither, `project.delivery`
|
|
2010
|
+
is used when set, else `project.encodeProfile`, else the server's default profile.
|
|
2011
|
+
- `outputs:true` exports the project's checked batch outputs; `outputs:[{preset:"master"},
|
|
2012
|
+
{preset:"youtube"}, {preset:"instagram-reels"}, …]` exports a list. One render per
|
|
2013
|
+
frame rate; the reply lists every file (or why one failed).
|
|
1345
2014
|
- `where:"headless"` renders in the background and leaves the user's tab alone;
|
|
1346
2015
|
`where:"tab"` refuses if no tab is open.
|
|
1347
2016
|
- Long edits: `wait:false` returns a job id at once; poll with `{job:"x_…"}`
|
|
1348
2017
|
and stop one with `{cancel:"x_…"}`. One export runs at a time.
|
|
1349
2018
|
|
|
2019
|
+
### Delivery presets
|
|
2020
|
+
|
|
2021
|
+
The Export dialog's **Preset** list. Each preset is a structured spec — no ffmpeg
|
|
2022
|
+
arguments to write — that `delivery.js` turns into one ffmpeg pass:
|
|
2023
|
+
|
|
2024
|
+
| Preset | Output |
|
|
2025
|
+
|---|---|
|
|
2026
|
+
| `youtube` | 1920×1080, H.264 CRF 18, keyframe every 0.5 s, AAC 384k 48 kHz |
|
|
2027
|
+
| `youtube-shorts` | 1080×1920, as YouTube; warns past 3 min |
|
|
2028
|
+
| `instagram-reels` | 1080×1920, H.264 CRF 19 capped at 25 Mb/s, AAC 256k; ≤ 60 fps, warns past 3 min |
|
|
2029
|
+
| `tiktok` | 1080×1920, H.264 CRF 19 capped at 20 Mb/s, AAC 256k; warns past 10 min |
|
|
2030
|
+
| `x` | 1920×1080, H.264 CRF 20 capped at 25 Mb/s, AAC 192k; warns past 2:20 and 512 MB |
|
|
2031
|
+
| `linkedin` | 1920×1080, H.264 CRF 20 capped at 20 Mb/s, AAC 192k; warns past 15 min and 5 GB |
|
|
2032
|
+
| `master` (default) | the picture's own size, H.264 CRF 16, AAC 320k |
|
|
2033
|
+
| `broadcast` | 1920×1080 ProRes 422 HQ MOV, PCM 24-bit 48 kHz, Rec. 709, normalized to −23 LUFS / −1 dBTP (EBU R128); warns off 25 / 29.97 / 50 / 59.94 fps |
|
|
2034
|
+
| `web` | 1280×720 H.264 CRF 23, AAC 128k, fast start |
|
|
2035
|
+
| `archive` | the picture's own size, lossless FFV1 4:2:2 10-bit MKV, PCM 24-bit |
|
|
2036
|
+
| `custom` | your own settings |
|
|
2037
|
+
|
|
2038
|
+
A spec (every key optional — what is left out comes from its `preset`):
|
|
2039
|
+
|
|
2040
|
+
```jsonc
|
|
2041
|
+
{
|
|
2042
|
+
"preset": "youtube",
|
|
2043
|
+
"width": 1920, "height": 1080, // null = the picture's own size (the export frame, else the canvas)
|
|
2044
|
+
"fps": null, // null = the project's; 23.976 / 29.97 / 59.94 are exact NTSC rates
|
|
2045
|
+
"reframe": "fill", // another shape: fill crops the middle (or the export frame), fit pads, stretch
|
|
2046
|
+
"container": "mp4", // mp4 · mov · mkv · webm · mxf
|
|
2047
|
+
"video": {
|
|
2048
|
+
"codec": "h264", // h264 · hevc · av1 · vp9 · prores · dnxhr · mpeg2 (XDCAM HD422) · ffv1
|
|
2049
|
+
"encoder": "libx264", // or h264_nvenc / h264_qsv / h264_amf … — listed only when they work here
|
|
2050
|
+
"rate": "crf", // crf · vbr · cbr (ProRes, DNxHR, XDCAM, FFV1 are fixed)
|
|
2051
|
+
"crf": 18, "bitrate": 12000, "maxrate": null, // kb/s; maxrate caps a CRF or VBR encode
|
|
2052
|
+
"keyint": 2, // seconds between keyframes, 0 = the encoder's default
|
|
2053
|
+
"speed": "balanced", // fast · balanced · best
|
|
2054
|
+
"pixFmt": "yuv420p", // 4:2:0 / 4:2:2 / 4:4:4, 8 or 10 bit, as the codec allows
|
|
2055
|
+
"profile": null // ProRes: proxy lt standard hq 4444 4444xq · DNxHR: lb sq hq hqx 444
|
|
2056
|
+
},
|
|
2057
|
+
"audio": { "codec": "aac", "bitrate": 320, "sampleRate": 48000, "channels": 2,
|
|
2058
|
+
"loudness": null, "truePeak": -1 },
|
|
2059
|
+
// aac · opus · mp3 · pcm16 · pcm24 · flac · none
|
|
2060
|
+
// loudness: a LUFS target (−14 streaming, −16 podcast, −23 EBU R128, −24 ATSC) or null
|
|
2061
|
+
"color": "bt709", // bt709 · bt709-full · bt601-625 (PAL) · bt601-525 (NTSC)
|
|
2062
|
+
"filename": "{project}_{preset}_{res}",
|
|
2063
|
+
// tokens: {project} {preset} {res} (1080p, 1080x1920, 1080sq) {w} {h} {fps} {codec} {date} {time} {range} {n}
|
|
2064
|
+
"metadata": { "title": "", "artist": "", "description": "", "copyright": "", "date": "", "keywords": "" }
|
|
2065
|
+
}
|
|
2066
|
+
```
|
|
2067
|
+
|
|
2068
|
+
- **Impossible combinations are corrected, and the reply says how** — ProRes moves to MOV,
|
|
2069
|
+
WebM takes Opus, Quick Sync gets 4:2:0. The dialog shows the same notes.
|
|
2070
|
+
- **Reframing happens in ffmpeg**: the browser sends the whole canvas and ffmpeg crops
|
|
2071
|
+
the export frame (or the middle, for another shape) and scales. The dialog warns
|
|
2072
|
+
when the output is upscaled or cropped, when a platform's length, size or frame-rate
|
|
2073
|
+
limit is passed, and estimates the file size where it can (bitrate and fixed-rate
|
|
2074
|
+
codecs; CRF cannot know).
|
|
2075
|
+
- Outputs that keep more than 4:2:0 8-bit (ProRes, DNxHR, FFV1, 10-bit, 4:2:2, 4:4:4)
|
|
2076
|
+
get raw RGBA frames instead of JPEG, so no chroma is lost on the way. Slower to send.
|
|
2077
|
+
- The audio is cut to exactly the frames' length.
|
|
2078
|
+
- **Loudness** (`audio.loudness`): the mix is measured and turned to the target with
|
|
2079
|
+
ffmpeg's two-pass `loudnorm` — one linear gain, and a true-peak limiter at `truePeak`.
|
|
2080
|
+
When the peaks leave no room for the gain it compresses instead and may land under the
|
|
2081
|
+
target; the check after the export says so. Off (null) leaves the mix as mixed —
|
|
2082
|
+
except Broadcast, which normalizes to −23 LUFS / −1 dBTP. Mixes shorter than 3 s may
|
|
2083
|
+
not reach the target.
|
|
2084
|
+
- **Checked after the export.** Every file is read back with ffprobe and measured
|
|
2085
|
+
(BS.1770 integrated loudness and true peak) and compared with what was asked:
|
|
2086
|
+
size, frame rate, frame count, codec, color tags, audio codec / rate / channels,
|
|
2087
|
+
length, loudness against the target (±1 LU) and the true-peak ceiling, and file size
|
|
2088
|
+
against the platform's limit. Without a target the report says where the platform
|
|
2089
|
+
plays it ("YouTube plays it 4.2 dB quieter"). The report shows in the queue, a toast
|
|
2090
|
+
after a single export, and the `fablecut_export` reply.
|
|
2091
|
+
- **Saved presets**: "Save preset…" in the dialog writes `library/delivery/<id>.json`,
|
|
2092
|
+
listed under "Saved" in every project (`GET / POST /api/export/presets`,
|
|
2093
|
+
`POST /api/export/presets/delete {id}`).
|
|
2094
|
+
- The settings are checked by a 0.1 s ffmpeg dry run before anything renders.
|
|
2095
|
+
- The dialog remembers the last settings in this browser. `setProject {delivery:"youtube"}`
|
|
2096
|
+
(or a spec) pins them in the project, for the dialog and for `fablecut_export`.
|
|
2097
|
+
- Fast export only — WebCodecs and Realtime export encode their own way.
|
|
2098
|
+
|
|
2099
|
+
### Batch outputs and the export queue
|
|
2100
|
+
|
|
2101
|
+
**One project → many files.** `project.outputs` is the project's list of files to make
|
|
2102
|
+
(up to 16), each a delivery spec plus `{id, on}`. In the Export dialog, **Batch outputs**
|
|
2103
|
+
lists them: **+ Add these settings** adds the preset shown above, **+ Typical set** adds
|
|
2104
|
+
Master, YouTube 1080p, Reel 1080×1920, Square 1080 and a 720p preview, the checkbox picks
|
|
2105
|
+
which go out, **Load** shows one's settings above. **Export checked** queues the checked
|
|
2106
|
+
ones and starts.
|
|
2107
|
+
|
|
2108
|
+
```jsonc
|
|
2109
|
+
// setProject {outputs:[…]} — a preset id or a spec each; stored complete with an id
|
|
2110
|
+
{"op":"setProject","set":{"outputs":[
|
|
2111
|
+
"master", "youtube",
|
|
2112
|
+
{"preset":"instagram-reels"},
|
|
2113
|
+
{"preset":"custom","width":1080,"height":1080,"filename":"{project}_square_{res}"},
|
|
2114
|
+
{"preset":"web","filename":"{project}_preview_{res}","on":false}
|
|
2115
|
+
]}}
|
|
2116
|
+
```
|
|
2117
|
+
|
|
2118
|
+
- **Rendered once.** Outputs with the same frame rate and range go out in ONE render:
|
|
2119
|
+
the compositor draws each frame once and every output's ffmpeg encodes it (up to 8 per
|
|
2120
|
+
render). Four 1080p files take about as long as one. Outputs at another frame rate get
|
|
2121
|
+
their own render. When any output needs full chroma (ProRes, 10-bit, lossless) the
|
|
2122
|
+
frames go as RGBA to all of them.
|
|
2123
|
+
- **The export frame** is the picture for outputs of its own shape (and for the
|
|
2124
|
+
picture's own size). An output of another shape reframes the whole canvas — a project
|
|
2125
|
+
framed for 9:16 still makes a proper 16:9 file.
|
|
2126
|
+
- **The export queue** (Export dialog → **Add to queue**, or **Queue**) runs waiting
|
|
2127
|
+
outputs back to back with a progress bar each, **Stop**, **Retry** for a failed one,
|
|
2128
|
+
**Open** for a finished file and **Clear finished**. It lives in the tab: a reload
|
|
2129
|
+
empties it. One output that fails does not stop the others in its render.
|
|
2130
|
+
- REST: `POST /api/export/begin {deliveries:[{delivery, name}, …], canvasW, canvasH,
|
|
2131
|
+
region?, duration, fps, hasAudio}` → `{id, input, jpegQuality, outputs:[{file, spec,
|
|
2132
|
+
summary, notes, size}]}`; frames and audio as for one output; `/api/export/end` →
|
|
2133
|
+
`{src, outputs:[{file, ok, src, report:{ok, checks:[{ok, text}], line}, mixLoudness?} | {file, ok:false, error}]}`.
|
|
2134
|
+
|
|
1350
2135
|
### Encoding profiles (`encoding-profiles.json`)
|
|
1351
2136
|
|
|
1352
2137
|
User-editable at the repo root. A profile is a **raw ffmpeg argument list** plus the
|
|
@@ -1410,11 +2195,12 @@ ffmpeg -y -f image2pipe -framerate <fps> -i - [-i audio.wav] <jpeg-color> <args
|
|
|
1410
2195
|
not `-i source.mp4`. Filters and codec settings apply to that frame stream. To transcode
|
|
1411
2196
|
an existing file verbatim, run ffmpeg directly — that is outside the compositor path.
|
|
1412
2197
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
2198
|
+
Encoding profiles are listed at the bottom of the Export dialog's Preset list.
|
|
2199
|
+
|
|
2200
|
+
**What the Export dialog opens on (priority):**
|
|
2201
|
+
1. `project.delivery`, then `project.encodeProfile` — set with `setProject`
|
|
2202
|
+
2. The last preset or profile used in this browser
|
|
2203
|
+
3. The `master` delivery preset
|
|
1418
2204
|
|
|
1419
2205
|
**MCP:** `fablecut_encode_profiles` lists profiles; `{detail:true}` includes each `args`
|
|
1420
2206
|
array; `{profile:"hq"}` returns one profile. `fablecut_status` shows the effective profile.
|