@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.
Files changed (32) hide show
  1. package/README.md +6 -6
  2. package/dist/commands/skills.d.ts.map +1 -1
  3. package/dist/commands/skills.js +31 -25
  4. package/dist/utils/skillsManifest.d.ts +13 -3
  5. package/dist/utils/skillsManifest.d.ts.map +1 -1
  6. package/dist/utils/skillsManifest.js +35 -12
  7. package/examples/system/rokmilk-chocolate-promo.vid +1 -1
  8. package/examples/system/voice-visual-promo.vid +1 -1
  9. package/package.json +3 -3
  10. package/skills/scenerok/SKILL.md +150 -0
  11. package/skills/{remote → scenerok/references}/ai-models.md +1 -2
  12. package/skills/{remote → scenerok/references}/animations-catalog.md +139 -2
  13. package/skills/{remote → scenerok/references}/animations-picker.md +12 -1
  14. package/skills/{remote → scenerok/references}/brand-setup.md +3 -1
  15. package/skills/{remote → scenerok/references}/catalog-index.json +163 -19
  16. package/skills/scenerok/references/editing-actions.md +560 -0
  17. package/skills/{remote → scenerok/references}/environments.md +23 -24
  18. package/skills/{remote → scenerok/references}/intent-router.md +5 -15
  19. package/skills/{remote → scenerok/references}/media-stdlib.md +2 -3
  20. package/skills/{remote → scenerok/references}/music.md +2 -21
  21. package/skills/scenerok/references/sdk-guide.md +301 -0
  22. package/skills/{remote → scenerok/references}/shaders-filters.md +9 -6
  23. package/skills/{remote → scenerok/references}/text-styles.md +15 -35
  24. package/skills/{remote → scenerok/references}/transitions-catalog.md +2 -0
  25. package/skills/{remote → scenerok/references}/tts.md +4 -22
  26. package/skills/{remote → scenerok/references}/vidscript-guide.md +250 -67
  27. package/skills/scenerok/references/vidscript-sample.md +44 -0
  28. package/skills/{remote → scenerok/references}/vidscript-strict.md +41 -26
  29. package/skills/{remote → scenerok/references}/workflow.md +6 -7
  30. package/skills/remote/sdk-guide.md +0 -201
  31. package/skills/remote/vidscript-sample.md +0 -28
  32. package/skills/shared/SKILL.md +0 -157
@@ -2,12 +2,16 @@
2
2
 
3
3
  VidScript is a declarative DSL for composing short-form videos. Write a script, render an MP4.
4
4
 
5
+ This is the grammar reference. For task-by-task editing (layering, masks, cuts, speed, sound mixing, grading, delivery), use `editing-actions.md`; for the rules agents most often break, `vidscript-strict.md`.
6
+
7
+ Contents: Agent quick start · Program structure · Statements (input, output, let, import) · Time blocks · Including TypeScript scenes · Instructions (clips, text, grid areas, video methods, compositing, filters, motion blur, shaders, audio, plugin calls) · Reuse: functions and modules · Animations and effects · Expressions · Template placeholders · CLI commands · Best practices
8
+
5
9
  ## Agent quick start (read before composing)
6
10
 
7
11
  1. **Import every package function** — use `import xai from "@scenerok/xai"`, `import cf from "@scenerok/cloudflare"`, `import eleven from "@elevenlabs/music"`, and `import motion from "@scenerok/basic-animations"` before calling `xai.*`, `cf.*`, `eleven.*`, or `motion.*`.
8
12
  2. **Validate early** — `scenerok validate file.vid` (errors show as `Line N:C — message`).
9
13
  3. **Music package syntax** — `import eleven from "@elevenlabs/music"`, then call `eleven.music(...)`, `eleven.generateMusic(...)`, or `eleven.composeMusic(...)`.
10
- 4. **Copy working examples** — see `examples/system/` in this skill folder (e.g. `minimal-text-reel.vid`, `ecom-product-launch.vid`, `rokmilk-chocolate-promo.vid`).
14
+ 4. **Copy working examples** — `vidscript-sample.md`, and the `.vid` files in the `@scenerok/cli` package's `examples/system/` (e.g. `minimal-text-reel.vid`, `ecom-product-launch.vid`, `rokmilk-chocolate-promo.vid`).
11
15
  5. **Use URL assets selectively** — if the user gives a website or product URL and browser screenshot tools are available, you can capture above-fold screenshots, product images, app screenshots, and logos for VidScript `input` assets. Do not treat screenshots as mandatory or as the only visual source.
12
16
  6. **Probe asset type and dimensions** — before placing real assets, identify image vs video with `file`; inspect width, height, and duration with `ffprobe`, `sips`, or `identify`; then scale from the measured aspect ratio and set explicit `width`, `height`, `x`, and `y`.
13
17
  7. **Use still images directly** — `input` supports videos and images. Place still screenshots, logos, and product photos with `video image_input`; the renderer treats images as static clips for the block duration.
@@ -59,6 +63,15 @@ input logo = "./assets/logo.png"
59
63
 
60
64
  Supports HTTP(S) URLs, `/uploads/` paths, and local paths for video/image assets.
61
65
 
66
+ A numbered image sequence is one input: the pattern names the frames, and `count:` says how many (a local compile can find it by probing). Optional `rate:` (frames per second, default the output fps) and `first:` (number of the first file, default 0):
67
+
68
+ ```vidscript
69
+ input frames = "shots/shot.%04d.png", rate: 12, first: 1, count: 24
70
+ [0s .. 2s] = frames
71
+ ```
72
+
73
+ Place it by name like a video; `Trim`, `Speed`, and `Loop` work on it. `first:` and `count:` on a plain file are errors. `rate:` on a plain video file declares the footage's frame rate for a timeline at another rate (`input clip = "clip.mp4", rate: 23.976` in a 30 fps output; each output frame shows the source frame its time falls in). It is an error on audio or a still, never auto-applied from a probe, and a probed rate more than 0.1% off the declared one warns. On the render workers the pattern must be an `https` URL pattern (or a path on the worker).
74
+
62
75
  For ads based on a supplied URL, browser tools can capture real website visuals and save product images or logos when useful. Use screenshots sparingly: favor above-fold, cropped, or focused captures that remain legible at video size. Avoid full-page or long scrolling screenshots as backgrounds because their text will usually become unreadable.
63
76
 
64
77
  Website captures are optional grounding assets, not a ban on generated video. A strong ad may combine product/logo/app screenshots with generated lifestyle clips, motion backgrounds, transitions, and VidScript text primitives. Use generative clips when they improve storytelling, show context, or make the ad feel more polished.
@@ -75,6 +88,8 @@ identify asset.webp
75
88
  Use real videos as motion footage and still images as static clips. The `video` primitive accepts both; the renderer detects image vs video:
76
89
 
77
90
  ```vidscript
91
+ import motion from "@scenerok/basic-animations"
92
+
78
93
  input screenshot = "assets/screenshots/homepage.png"
79
94
  [0s .. 4.5s] = video screenshot, width: 1080, height: 1920, x: 0, y: 0, animate: motion.fadeIn(0.35s)
80
95
  ```
@@ -99,14 +114,20 @@ output to "video.mp4", resolution: "1080x1920", fps: 30
99
114
  output to "reel.mp4", resolution: "720x1280", format: "mp4", codec: "h264", bitrate: "5M", background: "#0D0D0D"
100
115
  ```
101
116
 
117
+ Several `output` statements make several canvases (`compileVidscript` returns one IR each in `irs`; `ir` is the first). Each output is the whole script lowered against its own `resolution`, `fps`, and `background`, so percent positions and `grid` areas resolve per canvas. Every output needs its own path (two with the same path is a compile error). Where it renders: the headless renderer (`renderVidscriptOutputsToMP4`, local scripts) writes one file per output. Where it errors: the render queue (the web app, the CLI, and the API) delivers one artifact per render, so a script with more than one `output` is rejected with `N outputs are rendered … only one output per render is supported by the render queue yet` before anything is billed. Use one `output` for a hosted render.
118
+
102
119
  | Option | Type | Default | Description |
103
120
  |--------|------|---------|-------------|
104
121
  | `resolution` | string | `"1080x1920"` | Width×Height or single number |
105
122
  | `fps` | number | `30` | Frames per second |
106
- | `format` | string | `"mp4"` | Container format |
107
- | `codec` | string | `"h264"` | Video codec |
123
+ | `format` | string | `"mp4"` | Container format. Every render is MP4 today |
124
+ | `codec` | string | `"h264"` | Video codec. Every render is H.264 today |
108
125
  | `bitrate` | string | `"5M"` | Encoding bitrate |
109
126
  | `background` | string | `"#000000"` | Background color hex |
127
+ | `start_timecode` | string | none | Timecode of the first frame, `"HH:MM:SS:FF"` (non-drop-frame; the frame field must be below `fps`). Written to the mp4's timecode track |
128
+ | `burn_in` | string | none | `"timecode"` draws the running timecode (from `start_timecode`, else `00:00:00:00`) onto the picture |
129
+ | `allow_offline` | boolean | `false` | `true` renders an input whose file is missing at compile time (media probing on) as a "MEDIA OFFLINE" slate: a flat `#2b2b2b` frame labelled with the input name, over the clip's block or `Trim` length. `Resize`, `x`/`y`, `opacity` and `animate:` still apply; its sound is dropped with a warning; a transition at its seam is an error. Without it a missing local file is a compile error. Media that goes missing only at render time still fails the render |
130
+ | `alpha` | boolean | `false` | Needs `format: "mov", codec: "prores"` or `format: "webm", codec: "vp9"`. No render path delivers alpha yet (renders are H.264 mp4), so `alpha: true` is a compile error for now |
110
131
 
111
132
  ### Variable Assignment
112
133
 
@@ -139,9 +160,10 @@ Time blocks are the core of VidScript. They define when instructions execute on
139
160
  ### Dynamic Playhead (recommended)
140
161
 
141
162
  ```vidscript
163
+ input hero = "hero.mp4"
142
164
  [-] = hero # auto-append: starts at visual cursor
143
- [- 3s] = text "Title", size: 72 # auto-start, last 3 seconds
144
- [- 2.5s] = filter "glow", intensity: 0.8
165
+ [- 3s] = text "Title", size: 72 # auto-start, last 3 seconds
166
+ [- 2.5s] = filter "vignette", intensity: 0.4
145
167
  ```
146
168
 
147
169
  VidScript keeps separate auto-playhead cursors for visual and audio content. Visual blocks (`video`, `text`, filters, shaders, visual plugin calls, and bare visual inputs) advance the visual cursor. Audio blocks (`audio`, `xai.tts`, `eleven.music`, and other audio plugin calls) advance the audio cursor. `[- duration]` advances the relevant cursor by the explicit duration.
@@ -163,19 +185,20 @@ The generated video above starts at the current visual cursor, usually `0s`, eve
163
185
  ### Explicit Range
164
186
 
165
187
  ```vidscript
188
+ input hero = "hero.mp4"
166
189
  [0s .. 5s] = hero # absolute range
167
190
  [0.5s .. 3s] = text "Hello" # explicit times
168
- [prev + 1s .. prev + 4s] = filter "glow" # expression-based
191
+ [prev + 1s .. prev + 4s] = filter "vignette" # expression-based
169
192
  ```
170
193
 
171
194
  Use `..` (not `-`) as the range separator to avoid ambiguity with subtraction.
172
195
 
173
196
  ### Time Formats
174
197
 
175
- ```vidscript
198
+ ```text
176
199
  5s # 5 seconds
177
200
  300ms # 300 milliseconds
178
- frame 90 # 90 frames at 30fps = 3 seconds
201
+ 90 frame # 90 frames at the output fps (default 30): 3 seconds
179
202
  0:30 # 30 seconds
180
203
  1:30:00 # 1 hour 30 minutes
181
204
  ```
@@ -186,7 +209,7 @@ frame 90 # 90 frames at 30fps = 3 seconds
186
209
 
187
210
  ```vidscript
188
211
  [- 2s] = text "Hello" # plays from cursor to cursor+2s
189
- [prev + 1s .. prev + 3s] = filter "glow" # 1s after text ends, for 2s
212
+ [prev + 1s .. prev + 3s] = text "World" # 1s after "Hello" ends, for 2s
190
213
  ```
191
214
 
192
215
  ### Symbolic Durations (`asset.duration`)
@@ -208,40 +231,74 @@ let vo2 = xai.tts("Let's dive in", voice: "eve")
208
231
  - Exported timelines expose `.duration` too: `use scene1 at 0s .. 0s with {}` then `[scene1.duration .. scene1.duration + 4] = ...`. A `use` also advances the playhead past its content, so a following `[-]` block never overlaps it.
209
232
  - This replaces guessing: `[0s .. 5s] = audio xai.tts(...)` clips or overlaps when the real audio is longer. For simple back-to-back narration, consecutive `[-] = audio ...` blocks also never overlap.
210
233
 
211
- ## Include TypeScript / JavaScript scenes
234
+ ## Import TypeScript modules
212
235
 
213
- Import a `@scenerok/sdk` scene (`.ts` / `.js`) into VidScript with the same default-import form as plugins, then `use` it like a timeline:
236
+ `import kit from "./kit.ts"` (or `.js`) runs the module with `@scenerok/sdk` and binds its exports as a namespace, like a `.vid` module. Each export is a VidScript value of the matching kind:
214
237
 
215
- ```vidscript
216
- import intro from "./intro.ts"
217
- import bump from "./bump.js"
238
+ | TypeScript export | In VidScript |
239
+ | --- | --- |
240
+ | `video(…)`, `loadVideo(…)`, `imageSequence(…)` | a video value: `[0s .. 5s] = kit.hero`, `kit.hero.Trim(1s, 3s)` |
241
+ | `image(…)` | an image value |
242
+ | `text(…)` | a text value |
243
+ | `audio(…)`, `loadAudio(…)` | an audio value: `[0s .. 10s] = audio kit.bed` |
244
+ | `solid(…)`, `gap()`, `marker(…)` | the builtin values `solid()`, `gap()`, `marker()` return |
245
+ | `timeline()` | a timeline value: flat when placed bare, one group with properties or methods |
246
+ | a number, string, array, or plain object | that data: `color: kit.accent` |
247
+ | a function returning any of the above | a function: `kit.lowerThird("Konkan", color: "#f50")` |
248
+ | `export default` scene (`main()` returning `t.toIR()`) | `use kit at 0s .. 0s with {}`; `kit.duration` is its length |
218
249
 
219
- use intro at 0s .. 0s with {}
220
- use bump at intro.duration .. intro.duration with {}
221
- [intro.duration + bump.duration .. intro.duration + bump.duration + 2s] = text "Thanks", area: "caption"
250
+ ```ts
251
+ // kit.ts
252
+ import { timeline, video, image, text, solid, fadeIn } from '@scenerok/sdk'
253
+
254
+ export const accent = '#ff5500'
255
+ export const hero = video('beach.mp4', { duration: 12 }).trim(2, 8)
256
+ export const logo = image('logo.png', { width: 200, height: 200, x: 40, y: 40 })
257
+ export const title = text('Konkan Day 1', { size: 72, area: 'title', animate: fadeIn(0.4) })
258
+
259
+ export function lowerThird(name: string, opts: { color?: string } = {}) {
260
+ const t = timeline()
261
+ t[0][3] = solid(opts.color ?? accent, { height: 160, y: 880 })
262
+ t[0][3] = text(name, { size: 48, y: '88%' })
263
+ return t
264
+ }
265
+ ```
222
266
 
223
- output to "reel.mp4", resolution: "1080x1920"
267
+ ```vidscript fragment
268
+ import kit from "./kit.ts"
269
+
270
+ [0s .. 6s] = kit.hero
271
+ [0s .. 6s] = kit.logo, opacity: 0.9
272
+ [1s .. 4s] = kit.title
273
+ [3s .. 6s] = kit.lowerThird("Nidheesh", color: kit.accent), blend: "screen"
274
+ use kit.lowerThird at 6s .. 9s with { color: "#00aa88" }
275
+
276
+ output to "reel.mp4", resolution: "1920x1080"
224
277
  ```
225
278
 
226
279
  Rules:
227
- - The SDK file must export `default function main()` (or `module.exports = main` / `exports.main`) that returns `t.toIR()` or an `IRTimeline`.
228
- - `with { ... }` params are not supported on SDK scenes (no timeline parameters).
229
- - Relative paths only (`./` / `../`). The CLI uploads companion scene files automatically when you `scenerok validate` / `scenerok render` a `.vid`.
230
- - Prefer this when a complex loop/branch scene should sit inside a mostly-VidScript reel. For a fully TypeScript project, author the entry as `.ts` instead.
280
+ - A `timeline()` with no size takes this script's output size and fps, so it lays out on the same canvas.
281
+ - Exported values keep their own properties; placement properties override them (`kit.logo, opacity: 0.9`).
282
+ - Place an audio export as `audio kit.bed`. In a `[-]` block a bare `kit.bed` starts at the picture playhead, because the block's channel is decided before the module runs.
283
+ - Function calls pass positional arguments in order and named arguments as one trailing options object: `kit.lowerThird("A", color: "#f50")` calls `lowerThird("A", { color: "#f50" })`; `use kit.f at … with { … }` calls `f({ … })`. Arguments are data (strings, numbers, booleans, lists, objects); VidScript clips and timelines cannot be passed into TypeScript.
284
+ - An exported promise is awaited (`export const clip = loadVideo("hero.mp4")`). Top-level `await` is not supported.
285
+ - Filters, shaders, and transitions are not exportable values; use them inside an exported timeline, or as VidScript statements.
286
+ - Relative paths only (`./` / `../`). The module is one file: the CLI uploads it with the `.vid`, so it cannot import other local files.
231
287
 
232
288
  ## Instructions (inside time blocks)
233
289
 
234
- Multiple instructions in the same time block go on separate lines:
290
+ Each line places one thing. Several lines can share a window:
235
291
 
236
292
  ```vidscript
237
- [0s .. 5s] = hero
238
- hero.Trim(start: 0s, end: 5s)
239
- hero.Speed(factor: 1.5)
293
+ input hero = "hero.mp4"
294
+ [0s .. 5s] = hero.Trim(0s, 7.5s).Speed(1.5)
295
+ [0s .. 5s] = text "Hello", area: "title"
240
296
  ```
241
297
 
242
298
  ### Clip Reference
243
299
 
244
300
  ```vidscript
301
+ input hero = "hero.mp4"
245
302
  [-] = hero # just play the input clip
246
303
  ```
247
304
 
@@ -268,7 +325,7 @@ hero.Speed(factor: 1.5)
268
325
  | `align` | string | `left`, `center`, `right`. Default: `center` |
269
326
  | `line_height` | number | Line height multiplier for multi-line text. Default: 1.2 |
270
327
  | `letter_spacing` | number | Extra letter spacing in pixels. Default: 0 |
271
- | `rotation` | number | Rotation in degrees. Positive = clockwise. Default: 0 |
328
+ | `rotation` | number | Rotation in degrees. Positive turns text clockwise (video turns the other way). Default: 0 |
272
329
  | `opacity` | number | Opacity 0–1. Default: 1 |
273
330
  | `stroke` | string | Outline color |
274
331
  | `stroke_width` | number | Outline thickness |
@@ -276,7 +333,8 @@ hero.Speed(factor: 1.5)
276
333
  | `shadow_blur` | number | Shadow blur radius |
277
334
  | `shadow_offset_x` | number | Shadow horizontal offset |
278
335
  | `shadow_offset_y` | number | Shadow vertical offset |
279
- | `animation` | string | Parsed but not yet rendered |
336
+ | `anchor` | string | Point the text rotates and positions around, e.g. `"top-left"` |
337
+ | `stagger` | string | `"word"` or `"char"`: reveal the text piece by piece over its `animate:` |
280
338
 
281
339
  Params are optional and comma-separated. Params can be on the same line as the content.
282
340
 
@@ -311,21 +369,58 @@ t[0][s(3)] = text('Left column', { area: 'left' })
311
369
 
312
370
  ### Video Methods
313
371
 
314
- ```vidscript
315
- hero.Trim(start: 0s, end: 5s) # trim clip (positional: hero.Trim(0s, 5s))
316
- hero.Resize(width: 720, height: 1280) # resize (positional: hero.Resize(720, 1280))
317
- hero.Speed(factor: 1.5) # playback rate
318
- hero.Loop(count: 3) # loop count
319
- hero.Opacity(value: 0.5, duration: 2s) # opacity with duration
320
- ```
372
+ Methods are operations on values. Capitalised names are methods; lowercase names are
373
+ properties (`hero.duration`). A method returns **a new value** — chain them, or bind
374
+ the result with `let`:
321
375
 
322
- Arguments can be positional or named using `:`.
376
+ ```vidscript
377
+ input beach = "beach.mp4"
378
+ let hero = beach.Trim(2s, 8s).Speed(1.5)
379
+ [0s .. 4s] = hero, opacity: 0.9
380
+ [4s .. 8s] = beach.Trim(0s, 2s).Loop(2).Resize(720, 1280)
381
+ [8s .. 10s] = beach.Opacity(0.5).Filter("sepia", intensity: 0.8)
382
+ [10s .. 12s] = beach.Crop(0, 0, 640, 360)
383
+ let framed = beach.Mask(solid("#fff", radius: 120, width: 800, height: 500), mode: "luma")
384
+ [12s .. 14s] = framed
385
+ ```
386
+
387
+ | Method | Arguments | Result |
388
+ |--------|-----------|--------|
389
+ | `Trim(start, end)` | source times | Plays that source range |
390
+ | `Speed(factor)` | number | Playback rate; negative reverses, `0` holds a frame |
391
+ | `Loop(count)` | number | Repeats the range `count` times |
392
+ | `Resize(width, height)` | pixels | Display size |
393
+ | `Opacity(value)` | 0–1 | Opacity |
394
+ | `Filter(name, …params)` | filter name | A grade on this value only |
395
+ | `Crop(x, y, width, height)` | source pixels from the top-left | The rectangle, filling the display size |
396
+ | `Mask(matte, mode?)` | any picture; `"alpha"`, `"luma"`, `"inverted"` | Drawn through the matte |
397
+ | `Overlay(other, x?, y?, opacity?)` | a picture value | `other` on top at a position |
398
+ | `Composite(other, mode?, x?, y?, opacity?)` | a picture value, a blend mode | `other` blended on top |
399
+
400
+ Arguments can be positional or named (`hero.Trim(start: 0s, end: 5s)`).
401
+
402
+ A second `Trim` is relative to the first: `a.Trim(10s, 20s).Trim(2s, 4s)` plays 12–14 s. A second `Crop` is relative to the first the same way.
403
+ `Crop` keeps the display size, so the rectangle is scaled to fill it; `Resize` to the rectangle's aspect avoids stretching.
404
+ The `Mask` matte is any picture value — a `solid` with `radius:` or `corners:` for a shape, `text(…)`, a function, or another clip (a track matte) — drawn in the same frame at the same local time.
405
+ Place a masked value (`[0s .. 4s] = a.Mask(…)`); a `Mask` line after the clip is placed, an unknown mode, a masked clip as a
406
+ `Composite` side, and a cropped clip inside a transition are compile errors.
407
+ `audio x` places a sound — an audio file, or a video's own sound (`audio a.Trim(1s, 3s)`);
408
+ `video` on an audio file is an error.
409
+
410
+ A placed value lasts its own length (range ÷ |speed| × loops), so make its window that long.
411
+ Each placement is its own value: `[0s .. 4s] = a.Trim(0s, 4s)` and `[4s .. 6s] = a.Trim(10s, 12s)` are two independent cuts.
412
+
413
+ A method on its own line right after `[…] = hero` (the legacy spelling) still edits the clip that time block placed. Outside a time block it rebinds the name:
414
+ `bg.Loop(count: 3)` makes `bg` the looped clip for later placements.
323
415
 
324
416
  ### Compositing
325
417
 
326
418
  ```vidscript
327
- hero.Overlay(logo, x: 50, y: 100, opacity: 0.8)
328
- hero.Composite(overlay, x: 0, y: 0, opacity: 0.5, mode: "screen")
419
+ input hero = "hero.mp4"
420
+ input logo = "logo.png"
421
+ input texture = "light-leak.mp4"
422
+ [0s .. 4s] = hero.Overlay(logo, x: 50, y: 100, opacity: 0.8)
423
+ [4s .. 8s] = hero.Composite(texture, x: 0, y: 0, opacity: 0.5, mode: "screen")
329
424
  ```
330
425
 
331
426
  | Param | Type | Description |
@@ -333,16 +428,36 @@ hero.Composite(overlay, x: 0, y: 0, opacity: 0.5, mode: "screen")
333
428
  | `x` | number | Horizontal offset |
334
429
  | `y` | number | Vertical offset |
335
430
  | `opacity` | number | 0–1 transparency |
336
- | `mode` | string | Blend mode for Composite |
431
+ | `mode` | string | Blend mode for Composite: `normal`, `multiply`, `screen`, `overlay`, `difference`, `add`, `subtract`, `darken`, `lighten` |
432
+
433
+ The other side can be a clip name, or a value: a function's picture, a `solid(…)`, a
434
+ `text(…)`, or a trimmed clip. A clip joins as a composite layer; anything else draws as a
435
+ group blended with `mode` (`x`, `y`, and `opacity` place that group). A composite is itself
436
+ a target, so layers stack: `let ab = a.Composite(b, mode: "screen")` then
437
+ `[0s .. 4s] = ab.Composite(card("x"), mode: "multiply")`. Both sides must overlap in time.
337
438
 
338
439
  ### Filters
339
440
 
340
441
  ```vidscript
341
- [- 2s] = filter "glow", intensity: 0.8
442
+ input hero = "hero.mp4"
443
+ [0s .. 5s] = hero
444
+ [0s .. 2s] = filter "chromatic", offset: 0.01
342
445
  [0s .. 5s] = filter "vignette", intensity: 0.3
446
+ [0s .. 5s] = filter "contrast", amount: 1.2, target: hero
343
447
  ```
344
448
 
345
- Built-in filters: `monochrome`, `sepia`, `blur`, `chromatic`, `glitch`, `vignette`, `contrast`, `saturation`, `brightness`.
449
+ Built-in filters: `monochrome`, `sepia`, `blur`, `chromatic`, `glitch`, `vignette`, `contrast`, `saturation`, `brightness`, plus `lut` and `cdl` (below and in `editing-actions.md`) and the VFX in `shaders-filters.md`. A `filter` line grades everything in its window; `target: clip` grades one clip.
450
+
451
+ **LUT.** `filter "lut", file: "look.cube"` applies a 3D `.cube` look to the frame, to one clip with `target:`, or to a function body's picture. The compiler reads the file (relative to the `.vid`, 512 KB at most, up to 33 points per axis) and embeds the table, so render workers need nothing else. The lookup runs on sRGB-encoded values with 8-bit trilinear interpolation. `Filter("lut")` as a method is an error; use the statement.
452
+
453
+ ### Motion blur
454
+
455
+ `motion_blur: true` or `motion_blur: { shutter: 180, samples: 8 }` on a video/image placement or a placed function averages `samples` sub-frames across the shutter angle (above 0, up to 360; samples 2 to 64) of that item's own `animate:` motion. Each sample is another render of the frame, so a frame with a blurred item costs `samples` times as much. It does not re-sample the video's own frames, does not blur a group's moving children (set it on them), and is an error on text, solids, captions, audio, and a clip at a transition.
456
+
457
+ ```vidscript
458
+ input hero = "hero.mp4"
459
+ [0s .. 2s] = hero, animate: { type: "slideX", from: { x: 0 }, to: { x: 900 } }, motion_blur: { shutter: 360, samples: 16 }
460
+ ```
346
461
 
347
462
  ### Shaders
348
463
 
@@ -359,7 +474,8 @@ Fragment must sample `tDiffuse`, write `gl_FragColor`, and declare uniforms matc
359
474
  ### Audio
360
475
 
361
476
  ```vidscript
362
- audio music, volume: 0.5, fade_in: 1s, fade_out: 2s
477
+ input music = "music.mp3"
478
+ [0s .. 15s] = audio music, volume: 0.5, fade_in: 1s, fade_out: 2s
363
479
  ```
364
480
 
365
481
  Audio sources can be MP3 inputs, audio plugin results, or the embedded audio stream from an input video. Video inputs are visually silent unless you explicitly add `audio clip`. Plugin-generated audio (TTS, music, and probed native audio on generated video clips) is EBU R128 loudness-normalized during render to about -16 LUFS; `volume` is a relative gain applied after that. Ordinary imported audio/video is not auto-normalized.
@@ -369,6 +485,13 @@ Audio sources can be MP3 inputs, audio plugin results, or the embedded audio str
369
485
  | `volume` | number | Relative gain (0–1+). For generated audio, applied after loudness normalization. |
370
486
  | `fade_in` | number | Fade-in duration (seconds) |
371
487
  | `fade_out` | number | Fade-out duration (seconds) |
488
+ | `trim_start` | number | Start this many seconds into the file |
489
+ | `pan` | number | -1 (left) to 1 (right) |
490
+ | `duck_under` | name | Lower this track while the named track plays |
491
+ | `loudness` | string | `"ebu-r128"` normalizes an imported file |
492
+ | `animate` | object | `{ type: "volume", from: { volume: 0 }, to: { volume: 1 } }` automates the level |
493
+
494
+ Mixing recipes (J- and L-cuts, ducking, automation): `editing-actions.md` → Sound.
372
495
 
373
496
  ### Plugin Calls
374
497
 
@@ -433,7 +556,7 @@ Use `aspect_ratio: "9:16"` for vertical output, `"16:9"` for wide, `"1:1"` for s
433
556
 
434
557
  **Invalid (will not validate):**
435
558
 
436
- ```vidscript
559
+ ```vidscript invalid
437
560
  [-] = video xai.imagine("...") # missing xAI import
438
561
  [0s .. 5s] = video cf.video("...") # missing Cloudflare import
439
562
  [0s .. 15s] = audio eleven.generateMusic("...") # missing `import eleven from "@elevenlabs/music"`
@@ -441,20 +564,92 @@ Use `aspect_ratio: "9:16"` for vertical output, `"16:9"` for wide, `"1:1"` for s
441
564
  [0s .. 15s] = audio eleven.music("..."), volume: 0.5 # trailing params after direct plugin call
442
565
  ```
443
566
 
444
- ## Animations, Effects & Rendering (v2)
567
+ ## Reuse: values, functions, and modules
568
+
569
+ Anything that can be placed is a value: an input, a plugin result, `text(…)` / `audio(…)` /
570
+ `solid(…)`, a method chain, or a function call. A time block places one.
571
+
572
+ ### Functions
573
+
574
+ ```vidscript
575
+ export fn lowerThird(title: string, color: string = "#ff5500") -> video {
576
+ [- 2s] = text title, color: color, size: 48
577
+ }
578
+
579
+ [0s .. 2s] = lowerThird("Konkan Day 1")
580
+ [2s .. 4s] = lowerThird(title: "Day 2", color: "#00ff00")
581
+
582
+ # one-liners: fn … = expression
583
+ fn label(s: string) -> text = text(s, size: 24)
584
+ ```
585
+
586
+ | Form | Meaning |
587
+ |------|---------|
588
+ | `fn name(params) -> kind { … }` | Block body: places items when called and placed |
589
+ | `fn name(params) -> kind = expr` | Expression body: returns that value |
590
+ | `export fn …` | Exported (callable through an imported namespace) |
591
+ | `export timeline name(…) { … }` | Same as `export fn name(…) -> timeline { … }` |
592
+
593
+ Return kinds: `video`, `image`, `text`, `audio`, `timeline`. `-> video` is a closed unit
594
+ (place it with properties — `[0s .. 4s] = card("A"), x: 40, opacity: 0.5, blend: "screen"` — or
595
+ chain `.Opacity()`, `.Resize()`, `.Filter()`, `.Crop()`, `.Mask()` and it draws as one item);
596
+ `-> timeline` is open, its items join the caller. A `filter` or `shader` without `target:`
597
+ inside a function or timeline body grades that unit's picture, not the whole frame.
598
+
599
+ Arguments are checked at the call: a wrong kind, an unknown or missing parameter, too
600
+ many arguments, or a value of the wrong kind is a compile error naming the parameter.
601
+ Optional parameters take `name: kind = default`.
445
602
 
446
- VidScript v2 uses a **minimal-grammar, plugin-first** architecture. The compiler lowers VidScript into `IRTimeline` — the single contract shared by browser preview and final render.
603
+ `use card at 0s .. 2s with { title: "A" }` is exactly `[0s .. 2s] = card(title: "A")` —
604
+ the same IR.
447
605
 
448
- User-facing extension uses two parameters on any text or video surface:
606
+ ### Modules
607
+
608
+ ```vidscript
609
+ import brand from "./brand.vid" # another .vid file's exports
610
+
611
+ [0s .. 2s] = brand.lowerThird("Hello")
612
+ [0s .. 2s] = text "x", color: brand.accent
613
+ use brand.card at 2s .. 4s with { title: "B" }
614
+ ```
449
615
 
450
- - `animate:` — motion over time (plugin calls or object descriptors)
451
- - `effects:` — post-processing filters with optional animated params
616
+ `brand.vid` exports with `export fn lowerThird(title: string) -> video { … }`,
617
+ `export timeline card(title: string) { … }`, and `export const accent = "#ff5500"`.
618
+
619
+ ### Builtin values
620
+
621
+ ```vidscript
622
+ [0s .. 2s] = solid("#1a2b3c", opacity: 0.8) # flat-colour rectangle
623
+ [2s .. 3s] = gap() # transparent gap (advances the cursor)
624
+ [4s .. 4s] = marker("act 2") # named instant (timeline data, not rendered)
625
+ [0s .. 8s] = captions("talk.srt", size: 32) # one text layer per SRT cue
626
+ ```
627
+
628
+ Any visual value takes the same placement properties: `x`, `y`, `width`, `height`,
629
+ `opacity`, `rotation`, `radius`, `corners`, `area`, `blend`, `animate`, `enabled`.
630
+ `blend: "screen"` on a clip lowers to a composite layer over whatever is below the placement;
631
+ on text, a solid, or a function it draws as a group with that blend. A blend needs something
632
+ below it in the same window, or it is a compile error. More than 4 nested groups, or more
633
+ than 16 alive at once, is a compile warning (each group renders into its own GPU target).
634
+ `volume:` on a generated clip with its own sound sets that sound's level.
635
+
636
+ ### Names must resolve
637
+
638
+ An unquoted word is only allowed where a value is an enumeration (`align: center`,
639
+ `position: top`, `style: title`, `mode: screen`). Everywhere else an unresolved name is
640
+ `Unknown variable 'titel'`. Quote strings; pass real variables.
641
+
642
+ ## Animations and effects
643
+
644
+ `animate:` moves any text or picture over time, with `motion.*` calls or object descriptors. Filters are separate `filter` lines (above), and can be animated too.
452
645
 
453
646
  ### animate: parameter
454
647
 
455
648
  ```vidscript
456
649
  import motion from "@scenerok/basic-animations"
457
650
 
651
+ input hero = "hero.mp4"
652
+ input app_screen = "app-screen.png"
458
653
  [0s .. 3s] = text "Hello", x: "50%", y: "50%", animate: motion.fadeIn(0.8s)
459
654
 
460
655
  [-] = video hero, animate: [motion.fadeIn(0.5s), motion.slideY(-40, 0, 1.2s)]
@@ -473,17 +668,19 @@ import motion from "@scenerok/basic-animations"
473
668
  }
474
669
  ```
475
670
 
476
- Compiler lowers `animate:` into `IRMotionTrack[]` (property-path keyframes). Legacy `IRAnimation` descriptors are adapted at runtime for backward compatibility.
671
+ Easings: `linear`, `easeInQuad`, `easeOutQuad`, `easeInOutQuad`, `easeInCubic`, `easeOutCubic`, `easeInOutCubic`, `easeOutBack`, `spring`. A list of objects with absolute `start` / `end` chains segments on one property (a path with more than two points); `keyframes:` is not supported and is silently dropped.
477
672
 
478
- ### effects: parameter
673
+ ### Animated filters
479
674
 
480
675
  ```vidscript
676
+ import motion from "@scenerok/basic-animations"
677
+
481
678
  [0s .. 5s] = filter "vignette", intensity: 0.4
482
679
 
483
680
  [0s .. 5s] = filter "glitch", intensity: 0.5, animate: motion.fadeIn(1s)
484
681
  ```
485
682
 
486
- Built-in filters: `monochrome`, `sepia`, `blur`, `chromatic`, `glitch`, `vignette`, `contrast`, `saturation`, `brightness`. Filters are optional finishing tools; use them only when they improve the composition.
683
+ Filters are optional finishing tools; use them only when they improve the composition. Parameter names per filter: `editing-actions.md` → Color and finishing.
487
684
 
488
685
  ### Animation functions (`@scenerok/basic-animations`)
489
686
 
@@ -500,24 +697,12 @@ Built-in filters: `monochrome`, `sepia`, `blur`, `chromatic`, `glitch`, `vignett
500
697
  | `motion.float(duration?, amp?)` | Gentle vertical bob |
501
698
  | `motion.typewriter(duration?)` | Character reveal |
502
699
 
503
- ### Render architecture (contributors)
504
-
505
- Implemented registry-based rendering. See `plan-v1/35-render-subsystem-architecture.md`.
506
-
507
- | Registry | Path | Add via |
508
- |----------|------|---------|
509
- | Motion | `src/lib/motion/registry.ts` | `registerEasing`, `registerMotionBehavior` |
510
- | Surface | `src/render/surfaces/registry.ts` | `registerSurfaceRenderer` |
511
- | Effect | `src/render/effects/registry.ts` | `registerEffect` or `src/shaders/library.ts` |
512
- | Engine | `src/render/engine/index.ts` | `createRenderEngine` backends |
513
-
514
- For SceneRok platform engineering, load the **`scenerok-development`** skill (`.agents/skills/scenerok-development/`).
515
-
516
700
  ## Expressions
517
701
 
518
702
  Used in time specs, `let` values, and function arguments:
519
703
 
520
704
  ```vidscript
705
+ let title_text = "Launch"
521
706
  let x = 5s # Time literals
522
707
  let y = x * 2 # Arithmetic
523
708
  let prompt = "Scene: " + title_text # String concatenation
@@ -533,7 +718,7 @@ Operators: `+`, `-`, `*`, `/` with standard precedence.
533
718
  [- 3s] = text "{{headline | Default Text}}", color: "{{color | #FFFFFF}}"
534
719
  ```
535
720
 
536
- Placeholders are filled before parsing via `fillPlaceholders()`.
721
+ Templates fill each `{{name | default}}` before the script compiles; outside a template the default is used.
537
722
 
538
723
  ## CLI Commands
539
724
 
@@ -556,8 +741,6 @@ scenerok status <render-id>
556
741
  # Install agent skills
557
742
  scenerok skills install <platform>
558
743
 
559
- # Set API secrets for plugins
560
- scenerok secrets set ELEVENLABS_API_KEY=your-key
561
744
  ```
562
745
 
563
746
  ## Best Practices
@@ -0,0 +1,44 @@
1
+ # VidScript sample: a 13-second product promo
2
+
3
+ A complete script to copy and adapt: two generated shots joined by a dissolve,
4
+ titles in named areas, a voiceover, and a music bed that ducks under it.
5
+ Validate it before rendering.
6
+
7
+ ```vidscript
8
+ import xai from "@scenerok/xai"
9
+ import eleven from "@elevenlabs/music"
10
+ import motion from "@scenerok/basic-animations"
11
+ import tr from "@scenerok/transitions"
12
+
13
+ # Copy lives in variables so it is easy to change.
14
+ let product = "RokMilk"
15
+ let hook = "Chocolate milkshake, ready in seconds"
16
+ let cta = "Find it at your nearest grocery store"
17
+
18
+ # Generated media is bound with let, so its real length is known.
19
+ # Prompts never ask for text; all copy is added with `text` below.
20
+ let kitchen = xai.imagine("Chocolate milk carton on a sunlit kitchen counter, cinematic lighting, shallow depth of field, no text, no words, no letters, no captions, no logos, no watermark, no readable UI copy", aspect_ratio: "9:16", duration: 6)
21
+ let pour = xai.imagine("Slow pour of chocolate milkshake into a tall glass, macro, warm light, no text, no words, no letters, no captions, no logos, no watermark, no readable UI copy", aspect_ratio: "9:16", duration: 6)
22
+ let vo = xai.tts("Meet RokMilk. Chocolate milkshake, ready in seconds. Find it at your nearest grocery store.", voice: "eve")
23
+ let bed = eleven.music("Warm, upbeat acoustic bed for a food ad", duration: 13, instrumental: true)
24
+
25
+ # Picture: each [-] starts where the previous visual block ended.
26
+ [-] = kitchen
27
+ [- 0.6s] = tr.dissolve()
28
+ [-] = pour
29
+
30
+ # Titles go in named areas, never at guessed coordinates.
31
+ [0s .. 3s] = text product, style: title, area: "title", animate: motion.popIn(0.6s)
32
+ [3s .. 7s] = text hook, area: "lower-third", animate: motion.riseIn(0.6s)
33
+ [9s .. 12.6s] = text cta, area: "caption", animate: motion.fadeIn(0.5s)
34
+
35
+ # Sound: the voiceover runs for its measured length; the bed ducks under it.
36
+ [0s .. vo.duration] = audio vo
37
+ [0s .. 13s] = audio bed, volume: 0.6, duck_under: vo, fade_out: 1.5s
38
+
39
+ output to "rokmilk-promo.mp4", resolution: "1080x1920", fps: 30
40
+ ```
41
+
42
+ To cut your own footage instead of generating it, replace the `let … =
43
+ xai.imagine(…)` lines with `input` declarations and see `editing-actions.md`.
44
+ The `@scenerok/cli` package ships more scripts in `examples/system/`.