@scenerok/cli 2.2.0 → 2.4.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/README.md +6 -6
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +31 -25
- package/dist/utils/skillsManifest.d.ts +13 -3
- package/dist/utils/skillsManifest.d.ts.map +1 -1
- package/dist/utils/skillsManifest.js +35 -12
- package/examples/system/rokmilk-chocolate-promo.vid +1 -1
- package/examples/system/voice-visual-promo.vid +1 -1
- package/package.json +3 -3
- package/skills/scenerok/SKILL.md +150 -0
- package/skills/{remote → scenerok/references}/ai-models.md +1 -2
- package/skills/{remote → scenerok/references}/animations-catalog.md +139 -2
- package/skills/{remote → scenerok/references}/animations-picker.md +12 -1
- package/skills/{remote → scenerok/references}/brand-setup.md +3 -1
- package/skills/{remote → scenerok/references}/catalog-index.json +163 -19
- package/skills/scenerok/references/editing-actions.md +560 -0
- package/skills/{remote → scenerok/references}/environments.md +23 -24
- package/skills/{remote → scenerok/references}/intent-router.md +5 -15
- package/skills/{remote → scenerok/references}/media-stdlib.md +2 -3
- package/skills/{remote → scenerok/references}/music.md +2 -21
- package/skills/scenerok/references/sdk-guide.md +301 -0
- package/skills/{remote → scenerok/references}/shaders-filters.md +9 -6
- package/skills/{remote → scenerok/references}/text-styles.md +15 -35
- package/skills/{remote → scenerok/references}/transitions-catalog.md +2 -0
- package/skills/{remote → scenerok/references}/tts.md +4 -22
- package/skills/{remote → scenerok/references}/vidscript-guide.md +250 -67
- package/skills/scenerok/references/vidscript-sample.md +44 -0
- package/skills/{remote → scenerok/references}/vidscript-strict.md +41 -26
- package/skills/{remote → scenerok/references}/workflow.md +6 -7
- package/skills/remote/sdk-guide.md +0 -201
- package/skills/remote/vidscript-sample.md +0 -28
- package/skills/shared/SKILL.md +0 -157
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
# Editing and compositing actions
|
|
2
|
+
|
|
3
|
+
The everyday editing actions in VidScript: layering, cutting, sound, titles,
|
|
4
|
+
color, and delivery. Each section shows one working spelling. Every example
|
|
5
|
+
compiles as written; add an `output` line to render it.
|
|
6
|
+
|
|
7
|
+
These are VidScript features. The TypeScript SDK covers clips, text, audio,
|
|
8
|
+
filters, and transitions, but not masks, blends, crops, ducking, or the
|
|
9
|
+
delivery keys below. For those, write the scene in VidScript; a VidScript file
|
|
10
|
+
can still `import` an SDK scene and `use` it (see `vidscript-guide.md`).
|
|
11
|
+
|
|
12
|
+
## Contents
|
|
13
|
+
|
|
14
|
+
- Layering and compositing: picture-in-picture, blend modes, stacks, masks and
|
|
15
|
+
mattes, crop and shape, moving a group, split screens, hiding a layer
|
|
16
|
+
- Cutting and timing: trim, speed, reverse, freeze, ramps, loops, back-to-back
|
|
17
|
+
cuts, frame-accurate times, transitions with handles, image sequences
|
|
18
|
+
- Sound: a clip's own sound, volume and fades, J- and L-cuts, ducking, source
|
|
19
|
+
offset, pan, volume automation, loudness
|
|
20
|
+
- Text and titles: per-word reveals, easing, rotation, captions
|
|
21
|
+
- Color and finishing: per-clip grades, LUTs, CDL, color spaces, motion blur
|
|
22
|
+
- Reuse and delivery: functions, timelines, modules, output settings, timecode
|
|
23
|
+
- What VidScript can't do yet
|
|
24
|
+
|
|
25
|
+
## Layering and compositing
|
|
26
|
+
|
|
27
|
+
Text always draws over video. Between two video layers, say which one goes on
|
|
28
|
+
top with `Overlay`, `Composite`, or `blend:`; two plain clips placed in the
|
|
29
|
+
same window have no defined order.
|
|
30
|
+
|
|
31
|
+
### Picture-in-picture
|
|
32
|
+
|
|
33
|
+
`Overlay` puts one picture over a clip at a position (`x`, `y`, `opacity`).
|
|
34
|
+
The overlaid side can be a clip, `text(…)`, `solid(…)`, or a function call:
|
|
35
|
+
|
|
36
|
+
```vidscript
|
|
37
|
+
input main = "interview.mp4"
|
|
38
|
+
input cam = "screen.mp4"
|
|
39
|
+
[0s .. 6s] = main.Overlay(cam.Resize(360, 640), x: 680, y: 60, opacity: 0.9)
|
|
40
|
+
[6s .. 9s] = main.Overlay(text("LIVE", size: 48), x: 40, y: 40)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
For rounded corners or any other placement property, place the inset as its
|
|
44
|
+
own layer with `blend: "normal"`, which puts it over what is below. Placement
|
|
45
|
+
properties follow any value, including a method chain; `video cam` and a bare
|
|
46
|
+
`cam` are the same placement:
|
|
47
|
+
|
|
48
|
+
```vidscript
|
|
49
|
+
input main = "interview.mp4"
|
|
50
|
+
input cam = "screen.mp4"
|
|
51
|
+
[0s .. 6s] = main
|
|
52
|
+
[0s .. 6s] = cam.Trim(10s, 16s), width: 360, height: 640, x: 680, y: 60, radius: 24, blend: "normal"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Blend modes
|
|
56
|
+
|
|
57
|
+
`blend:` blends a layer with whatever is below it in the same window. Modes:
|
|
58
|
+
`normal`, `multiply`, `screen`, `overlay`, `difference`, `add`, `subtract`,
|
|
59
|
+
`darken`, `lighten`.
|
|
60
|
+
|
|
61
|
+
```vidscript
|
|
62
|
+
input footage = "city.mp4"
|
|
63
|
+
input flare = "light-leak.mp4"
|
|
64
|
+
[0s .. 5s] = footage
|
|
65
|
+
[0s .. 5s] = flare, blend: "screen", opacity: 0.7
|
|
66
|
+
[0s .. 5s] = solid("#ff6a00", opacity: 0.25), blend: "multiply"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
A blend with nothing below it is a compile error; place the base layer first.
|
|
70
|
+
|
|
71
|
+
### Stacking composites
|
|
72
|
+
|
|
73
|
+
`Composite` returns a value, so layers stack:
|
|
74
|
+
|
|
75
|
+
```vidscript
|
|
76
|
+
input base = "city.mp4"
|
|
77
|
+
input grain = "film-grain.mp4"
|
|
78
|
+
input leak = "light-leak.mp4"
|
|
79
|
+
let graded = base.Composite(grain, mode: "overlay", opacity: 0.4)
|
|
80
|
+
[0s .. 5s] = graded.Composite(leak, mode: "screen")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Both sides must overlap in time.
|
|
84
|
+
|
|
85
|
+
### Masks and mattes
|
|
86
|
+
|
|
87
|
+
`Mask(matte, mode:)` draws a value through a matte. The matte can be any
|
|
88
|
+
picture: a `solid` shape, `text(…)`, a function, or another clip (a track
|
|
89
|
+
matte). Modes: `"alpha"` (default), `"luma"`, `"inverted"`.
|
|
90
|
+
|
|
91
|
+
```vidscript
|
|
92
|
+
input portrait = "portrait.mp4"
|
|
93
|
+
input matte = "matte.mp4"
|
|
94
|
+
[0s .. 4s] = portrait.Mask(solid("#fff", x: 140, y: 560, width: 800, height: 800, radius: 400))
|
|
95
|
+
[4s .. 8s] = portrait.Mask(text("SALE", size: 400))
|
|
96
|
+
[8s .. 12s] = portrait.Mask(matte, mode: "luma")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- The matte is in output pixels, the same space as the placement: put the
|
|
100
|
+
shape where the clip appears on screen. The matte's color doesn't matter in
|
|
101
|
+
`"alpha"` mode.
|
|
102
|
+
- Place the masked value (`[…] = a.Mask(…)`). `a.Mask(…)` on its own line after
|
|
103
|
+
`a` is placed is a compile error.
|
|
104
|
+
- A masked clip can't be an `Overlay` or `Composite` side. Place it as its own
|
|
105
|
+
layer instead, with `blend: "normal"` to put it over another video:
|
|
106
|
+
`[0s .. 4s] = portrait.Mask(…), blend: "normal"`.
|
|
107
|
+
|
|
108
|
+
### Crop, resize, rounded corners, corner pin, rotation
|
|
109
|
+
|
|
110
|
+
```vidscript
|
|
111
|
+
input wide = "landscape.mp4"
|
|
112
|
+
input screen = "app-demo.mp4"
|
|
113
|
+
[0s .. 4s] = wide.Crop(420, 0, 1080, 1920)
|
|
114
|
+
[4s .. 8s] = screen, width: 900, height: 900, x: 90, y: 510, radius: 48, rotation: 4
|
|
115
|
+
[8s .. 12s] = screen, corners: [[120, 400], [960, 460], [940, 1500], [140, 1440]]
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `Crop(x, y, width, height)` takes a rectangle in source pixels from the
|
|
119
|
+
top-left; the rectangle fills the clip's display size, so `Resize` to the
|
|
120
|
+
same aspect to avoid stretching. A second `Crop` is relative to the first.
|
|
121
|
+
- `radius:` rounds the corners, in pixels.
|
|
122
|
+
- `corners:` pins the picture to four output points, clockwise from top-left.
|
|
123
|
+
- `rotation:` is in degrees. Positive values turn video counter-clockwise
|
|
124
|
+
(text turns the other way: see the note under Text and titles).
|
|
125
|
+
|
|
126
|
+
### Move several layers as one
|
|
127
|
+
|
|
128
|
+
A function that returns `video` draws as one item. Its placement takes
|
|
129
|
+
`animate:`, `blend:`, `opacity:`, and the transform properties, so this is the
|
|
130
|
+
camera move or the animated lower third. Times inside the body count from where
|
|
131
|
+
the function is placed. Inputs declared at the top of the script aren't visible
|
|
132
|
+
inside a function; pass clips in as parameters (`fn card(clip: video) -> video`).
|
|
133
|
+
On a function value, `Crop`, `Filter`, `Mask`, `Opacity`, and `Resize` work;
|
|
134
|
+
`Trim`, `Speed`, and `Loop` are errors.
|
|
135
|
+
|
|
136
|
+
```vidscript
|
|
137
|
+
input footage = "city.mp4"
|
|
138
|
+
fn lowerThird(title: string) -> video {
|
|
139
|
+
[0s .. 3s] = solid("#111111", width: 800, height: 160, x: 140, y: 1500, radius: 24)
|
|
140
|
+
[0s .. 3s] = text title, size: 56, area: "lower-third", color: "#ffffff"
|
|
141
|
+
}
|
|
142
|
+
[0s .. 3s] = footage
|
|
143
|
+
[0s .. 3s] = lowerThird("Day 1 · Goa"), animate: { type: "slideX", from: { x: -900 }, to: { x: 0 }, easing: "easeOutCubic" }
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Split screens
|
|
147
|
+
|
|
148
|
+
Grid areas work for video as well as text. `rows` and `cols` take `%` and `fr`
|
|
149
|
+
tracks; `areas` has one string per row:
|
|
150
|
+
|
|
151
|
+
```vidscript
|
|
152
|
+
input before = "before.mp4"
|
|
153
|
+
input after = "after.mp4"
|
|
154
|
+
grid rows: "1fr", cols: "1fr 1fr", areas: ["left right"]
|
|
155
|
+
[0s .. 5s] = video before, area: "left"
|
|
156
|
+
[0s .. 5s] = video after, area: "right"
|
|
157
|
+
[0s .. 5s] = text "Before", area: "left"
|
|
158
|
+
[0s .. 5s] = text "After", area: "right"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The grid applies to the whole script; `area:` is opt-in per placement, so
|
|
162
|
+
later full-frame placements are unaffected. A video in an area is sized to the
|
|
163
|
+
cell and stretched to fit, so crop it to the cell's shape first: a 1920x1080
|
|
164
|
+
clip in a 960x1080 half is `before.Crop(480, 0, 960, 1080)`. Text in an area is
|
|
165
|
+
centered in the cell; `area:` also works inside functions.
|
|
166
|
+
|
|
167
|
+
A transition can't start from a layout of several clips. To dissolve from a
|
|
168
|
+
split screen into one shot, run the layout past the cut and fade the shot in
|
|
169
|
+
over it:
|
|
170
|
+
|
|
171
|
+
```vidscript
|
|
172
|
+
import motion from "@scenerok/basic-animations"
|
|
173
|
+
input before = "before.mp4"
|
|
174
|
+
input after = "after.mp4"
|
|
175
|
+
grid rows: "1fr", cols: "1fr 1fr", areas: ["left right"]
|
|
176
|
+
[0s .. 5.5s] = before.Trim(0s, 5.5s).Crop(480, 0, 960, 1080), area: "left"
|
|
177
|
+
[0s .. 5.5s] = after.Trim(0s, 5.5s).Crop(480, 0, 960, 1080), area: "right"
|
|
178
|
+
[5s .. 10s] = after.Trim(5s, 10s), blend: "normal", animate: motion.fadeIn(0.5s)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Hide a layer without deleting it
|
|
182
|
+
|
|
183
|
+
```vidscript
|
|
184
|
+
input footage = "city.mp4"
|
|
185
|
+
input draft = "draft-overlay.mp4"
|
|
186
|
+
[0s .. 5s] = footage
|
|
187
|
+
[0s .. 5s] = video draft, enabled: false
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Cutting and timing
|
|
191
|
+
|
|
192
|
+
A placed clip lasts its own length: the trimmed range divided by the speed,
|
|
193
|
+
times the loop count. Make the window that long. A longer window does not hold
|
|
194
|
+
the last frame (see Hold a frame below). `Speed(0)` is the exception: it holds
|
|
195
|
+
for the whole window.
|
|
196
|
+
|
|
197
|
+
### Trim, speed, reverse, freeze, loop
|
|
198
|
+
|
|
199
|
+
Methods return a new value; chain them:
|
|
200
|
+
|
|
201
|
+
```vidscript
|
|
202
|
+
input clip = "surf.mp4"
|
|
203
|
+
[0s .. 4s] = clip.Trim(2s, 6s)
|
|
204
|
+
[4s .. 6s] = clip.Trim(10s, 14s).Speed(2)
|
|
205
|
+
[6s .. 8s] = clip.Trim(2s, 4s).Speed(-1)
|
|
206
|
+
[8s .. 9s] = clip.Trim(5s, 6s).Speed(0)
|
|
207
|
+
[9s .. 15s] = clip.Trim(0s, 2s).Loop(3)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
- `Trim(in, out)` plays exactly that source range. A second `Trim` is relative
|
|
211
|
+
to the first: `clip.Trim(10s, 20s).Trim(2s, 4s)` plays 12–14 s.
|
|
212
|
+
- `Speed(n)`: above 1 is faster, below 1 slower, negative plays backwards,
|
|
213
|
+
`0` holds the in-point frame.
|
|
214
|
+
- `Loop(n)` repeats the range `n` times.
|
|
215
|
+
- Each placement is its own value: trimming the same input in several
|
|
216
|
+
placements cuts several independent pieces.
|
|
217
|
+
|
|
218
|
+
### Hold a frame
|
|
219
|
+
|
|
220
|
+
`Speed(0)` holds the first frame of the range for the whole window. To end on
|
|
221
|
+
a freeze of the last frame, hold a range one frame long (1/fps seconds; 0.04 s
|
|
222
|
+
at 25 fps) at the end:
|
|
223
|
+
|
|
224
|
+
```vidscript
|
|
225
|
+
input clip = "surf.mp4"
|
|
226
|
+
[0s .. 4s] = clip.Trim(16s, 18s).Speed(0.5)
|
|
227
|
+
[4s .. 6s] = clip.Trim(17.96s, 18s).Speed(0)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Speed ramps
|
|
231
|
+
|
|
232
|
+
Animate `speed` across the placement:
|
|
233
|
+
|
|
234
|
+
```vidscript
|
|
235
|
+
input clip = "surf.mp4"
|
|
236
|
+
[0s .. 3s] = clip, animate: { type: "speed", from: { speed: 0.4 }, to: { speed: 2 }, easing: "easeInOutCubic" }
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Back-to-back cuts, gaps, and markers
|
|
240
|
+
|
|
241
|
+
`[- 2s]` starts where the last visual block ended. `gap()` leaves an empty
|
|
242
|
+
stretch; `marker("name")` records a named point (not drawn):
|
|
243
|
+
|
|
244
|
+
```vidscript
|
|
245
|
+
input a = "shot-a.mp4"
|
|
246
|
+
input b = "shot-b.mp4"
|
|
247
|
+
[- 2s] = a
|
|
248
|
+
[- 0.5s] = gap()
|
|
249
|
+
[- 2s] = b
|
|
250
|
+
[2s .. 2s] = marker("beat drop")
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Frame-accurate times
|
|
254
|
+
|
|
255
|
+
`48 frame` is 48 frames at the output frame rate (30 when `output` sets no
|
|
256
|
+
`fps:`; a computed `fps:` is an error):
|
|
257
|
+
|
|
258
|
+
```vidscript
|
|
259
|
+
[0s .. 48 frame] = text "Two seconds at 24 fps"
|
|
260
|
+
output to "video.mp4", resolution: "1920x1080", fps: 24
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Timecode literals are `HH:MM:SS:FF@rate`, non-drop-frame (`;` is an error),
|
|
264
|
+
FF below the rate, hours 0-23. Without `@rate` they use the literal output fps:
|
|
265
|
+
|
|
266
|
+
```vidscript
|
|
267
|
+
[00:00:01:12@24 .. 00:00:03:00@24] = text "1.5 s to 3 s"
|
|
268
|
+
output to "video.mp4", resolution: "1920x1080", fps: 24
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
NTSC `fps:` values (23.976, 29.97, 47.952, 59.94, 119.88) are encoded at the
|
|
272
|
+
exact `24000/1001` style rate, and block start/end times snap to whole frames
|
|
273
|
+
of the output (`[0.1s .. 0.35s]` at 30 fps is frames 3 to 11).
|
|
274
|
+
|
|
275
|
+
### Transitions and handles
|
|
276
|
+
|
|
277
|
+
Transitions come from `@scenerok/transitions` (full list in
|
|
278
|
+
`transitions-catalog.md`). By default the window sits between two clips. To
|
|
279
|
+
dissolve across a straight cut instead, add handles: `in_offset` reads that
|
|
280
|
+
much of the incoming clip before its in-point, `out_offset` that much of the
|
|
281
|
+
outgoing clip past its out-point. The clips need that much spare source.
|
|
282
|
+
|
|
283
|
+
A transition joins plain clips only. It is a compile error when either side is
|
|
284
|
+
masked, cropped, motion-blurred, or a placed function; transition between the
|
|
285
|
+
plain clips and apply those edits away from the cut.
|
|
286
|
+
|
|
287
|
+
```vidscript
|
|
288
|
+
import tr from "@scenerok/transitions"
|
|
289
|
+
input a = "shot-a.mp4"
|
|
290
|
+
input b = "shot-b.mp4"
|
|
291
|
+
[0s .. 4s] = a.Trim(2s, 6s)
|
|
292
|
+
[4s .. 8s] = b.Trim(2s, 6s)
|
|
293
|
+
[3.8s .. 4.2s] = tr.dissolve(in_offset: 0.2s, out_offset: 0.2s)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Image sequences
|
|
297
|
+
|
|
298
|
+
A numbered image sequence is one input. `count:` is the number of frames;
|
|
299
|
+
`rate:` defaults to the output fps; `first:` is the first file number
|
|
300
|
+
(default 0):
|
|
301
|
+
|
|
302
|
+
```vidscript
|
|
303
|
+
input frames = "renders/shot.%04d.png", rate: 12, first: 1, count: 24
|
|
304
|
+
[0s .. 2s] = frames
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
`Trim`, `Speed`, and `Loop` work on it. For cloud renders the pattern must be
|
|
308
|
+
an `https` URL pattern.
|
|
309
|
+
|
|
310
|
+
### Mixed frame rates
|
|
311
|
+
|
|
312
|
+
`rate:` on a plain video input says what frame rate the footage runs at, for a
|
|
313
|
+
timeline at another rate. Each output frame shows the source frame its time falls
|
|
314
|
+
in, so 23.976 footage in a 30 fps timeline repeats frames in a 2:3-like cadence
|
|
315
|
+
and never skips one:
|
|
316
|
+
|
|
317
|
+
```vidscript
|
|
318
|
+
input clip = "clip.mp4", rate: 23.976
|
|
319
|
+
[0s .. 4s] = clip.Trim(1s, 5s)
|
|
320
|
+
output to "out.mp4", resolution: "1920x1080", fps: 30
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Write it only when the footage rate differs from `fps`. It is an error on audio
|
|
324
|
+
or a still image; `first:` and `count:` stay sequence-only. A probed rate is never
|
|
325
|
+
applied for you: if the probe differs from the declared rate by more than 0.1%
|
|
326
|
+
the compile warns and the declared rate wins. Audio is not conformed.
|
|
327
|
+
|
|
328
|
+
## Sound
|
|
329
|
+
|
|
330
|
+
Every sound is an `audio` line with its own window, so J- and L-cuts need no
|
|
331
|
+
special syntax: start or end the sound before or after the picture cut.
|
|
332
|
+
|
|
333
|
+
### A clip's own sound, volume, and fades
|
|
334
|
+
|
|
335
|
+
Video inputs are silent until you place their sound:
|
|
336
|
+
|
|
337
|
+
```vidscript
|
|
338
|
+
input a = "talk-a.mp4"
|
|
339
|
+
input b = "talk-b.mp4"
|
|
340
|
+
[0s .. 4s] = a
|
|
341
|
+
[4s .. 8s] = b
|
|
342
|
+
[0s .. 4s] = audio a, fade_in: 0.3s
|
|
343
|
+
[3.5s .. 8s] = audio b.Trim(0s, 4.5s), volume: 0.9, fade_out: 1s
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The second `audio` line is a J-cut: `b`'s sound starts half a second before its
|
|
347
|
+
picture.
|
|
348
|
+
|
|
349
|
+
### Duck music under a voice
|
|
350
|
+
|
|
351
|
+
`duck_under:` lowers a bed to about a third of its volume, with short ramps,
|
|
352
|
+
for the whole window in which the named track plays (it follows the track's
|
|
353
|
+
time window, not speech). To duck only while someone speaks, place their
|
|
354
|
+
sound in separate windows, one per line of speech. Name an `input`, or a `let`
|
|
355
|
+
binding of generated speech:
|
|
356
|
+
|
|
357
|
+
```vidscript
|
|
358
|
+
import xai from "@scenerok/xai"
|
|
359
|
+
input bed = "music.mp3"
|
|
360
|
+
let vo = xai.tts("Here is what changed this week.", voice: "rex")
|
|
361
|
+
[0s .. 10s] = audio bed, volume: 0.6, duck_under: vo, fade_out: 1.5s
|
|
362
|
+
[1s .. 1s + vo.duration] = audio vo
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Start partway into a file, pan, and automate volume
|
|
366
|
+
|
|
367
|
+
```vidscript
|
|
368
|
+
input song = "song.mp3"
|
|
369
|
+
input sfx = "whoosh.wav"
|
|
370
|
+
[0s .. 8s] = audio song, trim_start: 30, animate: { type: "volume", from: { volume: 0 }, to: { volume: 0.8 }, start: 0, end: 3 }
|
|
371
|
+
[2s .. 3s] = audio sfx, pan: -0.6
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
- `trim_start:` (seconds) or `song.Trim(30s, 38s)` starts the file 30 s in.
|
|
375
|
+
- `pan:` runs from -1 (left) to 1 (right).
|
|
376
|
+
- `animate: { type: "volume", … }` changes the level over time.
|
|
377
|
+
|
|
378
|
+
### Even loudness
|
|
379
|
+
|
|
380
|
+
```vidscript
|
|
381
|
+
input voice = "raw-voiceover.wav"
|
|
382
|
+
[0s .. 10s] = audio voice, loudness: "ebu-r128"
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
Generated speech and music are already normalized to broadcast loudness;
|
|
386
|
+
use `loudness: "ebu-r128"` for your own recordings. It is the only value.
|
|
387
|
+
|
|
388
|
+
## Text and titles
|
|
389
|
+
|
|
390
|
+
```vidscript
|
|
391
|
+
[0s .. 2.5s] = text "Built for speed", area: "title", stagger: "word", animate: { type: "slideY", from: { y: 40, opacity: 0 }, to: { y: 0, opacity: 1 }, easing: "spring" }
|
|
392
|
+
[2.5s .. 5s] = text "NEW", size: 120, rotation: -8, anchor: "top-left", x: "8%", y: "12%"
|
|
393
|
+
[0s .. 8s] = captions("talk.srt", size: 40, area: "caption")
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
- `style:` is `title` (64 px), `subtitle` (54), `caption` (36), or `default` (48).
|
|
397
|
+
- Text without `x`, `y`, or `area:` is centered on the frame.
|
|
398
|
+
- `stagger: "word"` or `"char"` reveals the text in pieces over the animation.
|
|
399
|
+
- Easings: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`,
|
|
400
|
+
`easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `spring`.
|
|
401
|
+
- `rotation:` turns text around its `anchor:`; a positive value turns static
|
|
402
|
+
text clockwise, the opposite of video. Check the preview when direction
|
|
403
|
+
matters.
|
|
404
|
+
- `captions("file.srt", …)` makes one text layer per cue from a SubRip file;
|
|
405
|
+
the other arguments style every cue.
|
|
406
|
+
|
|
407
|
+
For catalog text effects (word cascades, counters, kinetic type), see
|
|
408
|
+
`animations-picker.md`.
|
|
409
|
+
|
|
410
|
+
## Color and finishing
|
|
411
|
+
|
|
412
|
+
`filter` without `target:` grades everything in its window. To grade one
|
|
413
|
+
clip, add `target:` or use the `Filter` method; inside a function it grades
|
|
414
|
+
only that function's picture.
|
|
415
|
+
|
|
416
|
+
```vidscript
|
|
417
|
+
input a = "shot-a.mp4"
|
|
418
|
+
input b = "shot-b.mp4"
|
|
419
|
+
[0s .. 4s] = a
|
|
420
|
+
[0s .. 4s] = filter "sepia", intensity: 0.6, target: a
|
|
421
|
+
[4s .. 8s] = b.Filter("saturation", amount: 1.2)
|
|
422
|
+
[4s .. 8s] = filter "vignette", intensity: 0.3
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
Filters that change each pixel on its own can target one clip: `sepia`,
|
|
426
|
+
`monochrome`, `contrast`, `saturation`, `brightness`, `vignette`, `premultiply`, `lut`, `cdl`.
|
|
427
|
+
Filters that sample neighbouring pixels (`blur`, `glitch`, `chromatic`, and the
|
|
428
|
+
VFX in `shaders-filters.md`) can't take `target:`. To blur only part of the
|
|
429
|
+
picture, put the clip and the filter in a function; the filter stays inside it:
|
|
430
|
+
|
|
431
|
+
```vidscript
|
|
432
|
+
input speaker = "speaker.mp4"
|
|
433
|
+
fn blurred(clip: video) -> video {
|
|
434
|
+
[0s .. 6s] = clip
|
|
435
|
+
[0s .. 6s] = filter "blur", radius: 8
|
|
436
|
+
}
|
|
437
|
+
[0s .. 6s] = blurred(speaker.Trim(0s, 6s))
|
|
438
|
+
[0s .. 6s] = text "Sharp on top", area: "title"
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Each filter reads its own parameter names; any other name is ignored without
|
|
442
|
+
an error, so use these:
|
|
443
|
+
|
|
444
|
+
| Filter | Parameter (default) |
|
|
445
|
+
| --- | --- |
|
|
446
|
+
| `sepia`, `monochrome` | `intensity`, 0–1 |
|
|
447
|
+
| `vignette`, `glitch` | `intensity` (0.5) |
|
|
448
|
+
| `contrast`, `saturation` | `amount`, a multiplier (contrast 1.2, saturation 1.5; 1 is unchanged) |
|
|
449
|
+
| `brightness` | `amount`, added to each channel (0.1; 0 is unchanged) |
|
|
450
|
+
| `blur` | `radius` in pixels (5) |
|
|
451
|
+
| `chromatic` | `offset` (0.02) |
|
|
452
|
+
| `lut` | `file` |
|
|
453
|
+
| `cdl` | `slope`, `offset`, `power`, `saturation` |
|
|
454
|
+
| `premultiply` | none: multiplies color by alpha, for footage with straight alpha |
|
|
455
|
+
|
|
456
|
+
### LUTs and CDL
|
|
457
|
+
|
|
458
|
+
```vidscript
|
|
459
|
+
input a = "log-footage.mp4"
|
|
460
|
+
[0s .. 4s] = a
|
|
461
|
+
[0s .. 4s] = filter "lut", file: "look.cube", target: a
|
|
462
|
+
[0s .. 4s] = filter "cdl", slope: "1.1 1 0.9", offset: "0 0 0", power: "1 1 1", saturation: 1
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
- `filter "lut"` applies a 3D `.cube` file next to the script (512 KB at
|
|
466
|
+
most, up to 33 points per axis). Use the `filter` line, not `Filter("lut")`.
|
|
467
|
+
- `filter "cdl"` takes ASC CDL `slope`, `offset`, `power` (three numbers each)
|
|
468
|
+
and `saturation`.
|
|
469
|
+
|
|
470
|
+
### Color spaces and motion blur
|
|
471
|
+
|
|
472
|
+
```vidscript
|
|
473
|
+
input a = "acescg-plate.mp4"
|
|
474
|
+
input logo = "logo.png"
|
|
475
|
+
[0s .. 3s] = video a, input_colorspace: "ACEScg", output_colorspace: "sRGB"
|
|
476
|
+
[0s .. 3s] = video logo, width: 300, height: 300, x: 390, y: 800, animate: { type: "slideX", from: { x: -400 }, to: { x: 390 }, easing: "easeOutCubic" }, motion_blur: { shutter: 180, samples: 8 }
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
- Color spaces: `sRGB`, `Rec.709` (or `rec709`), `Display P3`, `ACEScg`,
|
|
480
|
+
`Linear`, `Gamma 2.2` (or `gamma22`).
|
|
481
|
+
- `motion_blur: true` (or `{ shutter, samples }`) blurs an item's own
|
|
482
|
+
`animate:` movement. Each sample is another render of the frame, so use
|
|
483
|
+
it on fast moves only. It errors on text, solids, captions, and clips inside
|
|
484
|
+
a transition.
|
|
485
|
+
|
|
486
|
+
## Reuse and delivery
|
|
487
|
+
|
|
488
|
+
### Functions, timelines, and modules
|
|
489
|
+
|
|
490
|
+
```vidscript
|
|
491
|
+
input product = "product.png"
|
|
492
|
+
export fn card(title: string, accent: string = "#ff5500") -> video {
|
|
493
|
+
[0s .. 2s] = solid(accent, opacity: 0.85)
|
|
494
|
+
[0s .. 2s] = text title, area: "title", color: "#ffffff"
|
|
495
|
+
}
|
|
496
|
+
export timeline intro(clip: video) {
|
|
497
|
+
[- 2s] = clip
|
|
498
|
+
}
|
|
499
|
+
[0s .. 2s] = card("Spring drop")
|
|
500
|
+
[2s .. 4s] = card(title: "Free shipping", accent: "#0f766e")
|
|
501
|
+
use intro at 4s .. 4s with { clip: product }
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
- Arguments are checked at the call: a missing, unknown, or wrong-kind
|
|
505
|
+
argument is a compile error that names it.
|
|
506
|
+
- `-> video` draws as one item; a `timeline` body's items join the caller.
|
|
507
|
+
- `import brand from "./brand.vid"` loads another file's exports
|
|
508
|
+
(`brand.card("Hi")`).
|
|
509
|
+
|
|
510
|
+
### Output settings and timecode
|
|
511
|
+
|
|
512
|
+
```vidscript
|
|
513
|
+
input a = "master.mp4"
|
|
514
|
+
[0s .. 10s] = a
|
|
515
|
+
output to "master.mp4", resolution: "1920x1080", fps: 25, codec: "h264", bitrate: "8M", start_timecode: "01:00:00:00", burn_in: "timecode"
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`start_timecode` is `HH:MM:SS:FF` and its frame field must be below `fps`.
|
|
519
|
+
`burn_in: "timecode"` draws the running timecode on the picture.
|
|
520
|
+
|
|
521
|
+
### Offline media
|
|
522
|
+
|
|
523
|
+
```vidscript
|
|
524
|
+
input a = "shot-a.mp4"
|
|
525
|
+
input b = "shot-b.mp4" # not delivered yet
|
|
526
|
+
[0s .. 4s] = a
|
|
527
|
+
[4s .. 8s] = b
|
|
528
|
+
output to "draft.mp4", resolution: "1920x1080", fps: 25, allow_offline: true
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
With `allow_offline: true`, an input whose file is missing at compile time becomes an offline clip (OTIO `MissingReference`): it keeps its place and source path, lasts the block or `Trim` length, and draws as a flat `#2b2b2b` slate labelled `MEDIA OFFLINE` and the input name. `Resize`, `x`/`y`, `opacity` and `animate:` still apply. Its sound is dropped with a warning, and a transition at its seam is an error. Without the option a missing local file is a compile error. Media that disappears only at render time still fails the render.
|
|
532
|
+
|
|
533
|
+
## What VidScript can't do yet
|
|
534
|
+
|
|
535
|
+
Don't try to work around these. Tell the user when a request needs one.
|
|
536
|
+
|
|
537
|
+
| Request | Today |
|
|
538
|
+
| --- | --- |
|
|
539
|
+
| Two aspect ratios from one script | Two `output` statements compile to two canvases, but only the local headless renderer renders both (one file per output). The render queue (web app, CLI, API) rejects a script with more than one `output` before billing, so write one script per format for a hosted render. |
|
|
540
|
+
| Transparent video (alpha) | `alpha: true` is a compile error; renders are H.264 MP4. |
|
|
541
|
+
| Multicam angle switching | No syntax. Cut between the angle clips with time blocks. |
|
|
542
|
+
| Mixed frame rates | A timeline has one frame rate, the output `fps`. |
|
|
543
|
+
| `repeat n { … }` and named tracks | Not in the language. Use a function or timeline and `use` it per repeat. |
|
|
544
|
+
| Formats other than H.264 MP4 | `format:` and `codec:` are accepted, but every render is H.264 MP4 today. |
|
|
545
|
+
| Rectangle and ellipse primitives | Use `solid(…)` with `width`, `height`, and `radius:` (a circle when the solid is square and `radius` is half its width), or `corners:` for a four-point shape. |
|
|
546
|
+
|
|
547
|
+
## Compiles, but does nothing
|
|
548
|
+
|
|
549
|
+
These pass validation and are silently ignored. Avoid them:
|
|
550
|
+
|
|
551
|
+
- A filter parameter the filter doesn't have (`filter "saturation", value: 1.2`); use the table above.
|
|
552
|
+
- An easing that isn't in the list (`easing: "easeOutElastic"` runs linear).
|
|
553
|
+
- `animate: { keyframes: [...] }`: the points are dropped. For a path with
|
|
554
|
+
more than two points, list two-point segments with absolute `start` / `end`:
|
|
555
|
+
|
|
556
|
+
```vidscript
|
|
557
|
+
[0s .. 2s] = text "Path", animate: [{ type: "slideX", from: { x: 0 }, to: { x: 300 }, start: 0, end: 1 }, { type: "slideX", from: { x: 300 }, to: { x: 120 }, start: 1, end: 2 }]
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
- `Overlay(…, radius: …)`: use a placement with `blend: "normal"` (Picture-in-picture above).
|
|
@@ -11,33 +11,33 @@ SceneRok skills are shared across several runtimes. **Detect where you are runni
|
|
|
11
11
|
| ChatGPT Actions, OAuth to SceneRok, `openaiFileIdRefs` | **ChatGPT GPT** | `validateVidscript` → `renderVideo`; import uploads first |
|
|
12
12
|
| Gemini Gem or Grok Skill; may lack HTTP tools | **Consumer chat (limited)** | Compose VidScript; user renders elsewhere if no API |
|
|
13
13
|
| `scenerok-mcp` tools (`scenerok_validate_vidscript`, `scenerok_render_video`, …) or remote `https://mcp.scenerok.com/mcp` | **MCP host** | MCP tools instead of raw CLI |
|
|
14
|
-
|
|
|
14
|
+
| Only `SKILL.md` text available, no `references/` files | **Router-only** | Fetch references from the base URL below |
|
|
15
15
|
|
|
16
16
|
When unsure, ask one clarifying question: *"Are we in the SceneRok editor, your terminal, or a chat app like ChatGPT?"*
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Loading references
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
4. Conditionally load catalogs per `intent-router.md`
|
|
24
|
-
5. Offline: `scenerok skills sync` then read local skill directory
|
|
25
|
-
6. Freshness: `scenerok skills check` · update: `scenerok skills update`
|
|
26
|
-
|
|
27
|
-
Base URL: `https://scenerok.com/.well-known/agent-skills/{filename}`
|
|
20
|
+
- **Installed skill** (`scenerok skills install <platform>`, or the copy inside the `@scenerok/cli` or `@scenerok/sdk` npm package): read `references/<file>` next to `SKILL.md`.
|
|
21
|
+
- **Router-only:** fetch `https://scenerok.com/.well-known/agent-skills/<file>`; the list is at `https://scenerok.com/.well-known/agent-skills/index.json`.
|
|
22
|
+
- **Freshness:** `scenerok skills check` · refresh: `scenerok skills update`
|
|
28
23
|
|
|
29
24
|
## Capability matrix
|
|
30
25
|
|
|
31
|
-
| Capability | Web editor | CLI
|
|
32
|
-
| --- | --- | --- | --- | --- |
|
|
33
|
-
| Edit VidScript in place | `patch_vidscript` | Write `.vid` / `.ts` file | Return script to user | Return script |
|
|
34
|
-
| Validate | Editor API (VidScript) | `scenerok validate` (`.vid` or `.ts`) | `validateVidscript` (+ `language` for SDK) | Explain only |
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
39
|
-
|
|
|
40
|
-
|
|
|
26
|
+
| Capability | Web editor | CLI | MCP | ChatGPT Action | Gemini / Grok (no API) |
|
|
27
|
+
| --- | --- | --- | --- | --- | --- |
|
|
28
|
+
| Edit VidScript in place | `patch_vidscript` | Write `.vid` / `.ts` file | Pass the script as a string | Return script to user | Return script |
|
|
29
|
+
| Validate | Editor API (VidScript) | `scenerok validate` (`.vid` or `.ts`) | `scenerok_validate_vidscript` (`language: "typescript"` for SDK) | `validateVidscript` (+ `language` for SDK) | Explain only |
|
|
30
|
+
| Deep-validate with generated assets | No | `--generate-assets --report-json` | No | No | No |
|
|
31
|
+
| Render | Editor UI / API (VidScript) | `scenerok render` (`.vid` or `.ts`) | `scenerok_render_video` + `scenerok_check_render_status` | `renderVideo` + poll | Instruct user |
|
|
32
|
+
| Check the rendered MP4 | No | `scenerok lint-video` | No | No | No |
|
|
33
|
+
| Local files (`input "./x.mp4"`, `file: "look.cube"`, `import "./brand.vid"`) | No; library URLs | Yes | No; public URLs only | No; imported files | No |
|
|
34
|
+
| Shell / ffprobe / sips | No | Yes | Only if the host has a shell | No | No |
|
|
35
|
+
| Browse product URL | If browser tool | If browser tool | If browser tool | Limited | Limited |
|
|
36
|
+
| User asset library | Yes (logged in) | Via URLs / uploads | Via URLs | Via `importOpenAIConversationFiles` | Ask for public URLs |
|
|
37
|
+
| Ask brand questions | Yes (chat) | Yes (terminal) | Yes | Yes (chat) | Yes (chat) |
|
|
38
|
+
| OAuth / API tokens | Session cookie | `scenerok auth login` | Configured in the MCP server | OAuth per user | Manual token fallback |
|
|
39
|
+
|
|
40
|
+
Where deep validation or the MP4 check isn't available, validate, render, and review the result with the user instead.
|
|
41
41
|
|
|
42
42
|
## Environment-specific rules
|
|
43
43
|
|
|
@@ -49,10 +49,9 @@ Base URL: `https://scenerok.com/.well-known/agent-skills/{filename}`
|
|
|
49
49
|
|
|
50
50
|
### Desktop agent (CLI / Cursor / Claude Code / Codex)
|
|
51
51
|
- Run `scenerok validate` before every render (`.vid` or `.ts`).
|
|
52
|
-
-
|
|
52
|
+
- Write VidScript by default and for every editing action; use `@scenerok/sdk` only for loops or computed timing.
|
|
53
53
|
- Probe local assets with `loadVideo` / media stdlib, or `file` / `ffprobe` / `sips` / `identify` before placement.
|
|
54
|
-
- Install
|
|
55
|
-
- Full offline bundle: `scenerok skills sync`
|
|
54
|
+
- Install the skill: `scenerok skills install <platform>` (claude, codex, cursor, opencode, aider)
|
|
56
55
|
|
|
57
56
|
### ChatGPT GPT
|
|
58
57
|
- Load `consumer-chat-apps.md` and `chatgpt-gpt-instructions.md` for OAuth and file import.
|
|
@@ -69,5 +68,5 @@ Base URL: `https://scenerok.com/.well-known/agent-skills/{filename}`
|
|
|
69
68
|
| --- | --- | --- |
|
|
70
69
|
| `scenerok render ...` | Use editor render | Use `renderVideo` action |
|
|
71
70
|
| `scenerok auth login` | N/A (session) | OAuth instead |
|
|
72
|
-
| Fetch
|
|
71
|
+
| Fetch reference URLs | Already inlined locally | Fetch if a network tool exists |
|
|
73
72
|
| Local file paths in VidScript | Use library HTTPS URLs | Use imported asset URLs |
|