@scenerok/cli 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenerok/cli",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "SceneRok CLI - Create videos from your terminal and agent workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,8 +23,11 @@ validate it, render it, and check the rendered file.
23
23
  | Use for | Promos, reels, templates, and every editing action: masks, blends, crops, speed ramps, ducking, LUTs, timecode | Timelines built by code: loops over data, computed timing across many clips |
24
24
  | Start with | [vidscript-strict.md](references/vidscript-strict.md) | [sdk-guide.md](references/sdk-guide.md) |
25
25
 
26
- The SDK has no masks, blends, crops, ducking, or delivery settings. When a
27
- scene needs both, write it in VidScript and `import` the SDK scene into it.
26
+ The SDK has the same values and placement (masks, blends, crops, groups,
27
+ ducking, timecode); LUT files, caption files, several outputs, and offline
28
+ media are VidScript only. A `.vid` can `import kit from "./kit.ts"` and place
29
+ the module's exports (`[0s .. 3s] = kit.title`), so build data-driven parts in
30
+ TypeScript and finish the edit in VidScript.
28
31
 
29
32
  ## Workflow
30
33
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "https://scenerok.com/catalog-index/v1",
3
- "generatedAt": "2026-10-06T08:56:17.740Z",
3
+ "generatedAt": "2026-10-07T17:24:47.269Z",
4
4
  "cdnBase": "https://assets-public.scenerok.com/catalog",
5
5
  "categories": [
6
6
  {
@@ -260,6 +260,18 @@ input b = "shot-b.mp4"
260
260
  output to "video.mp4", resolution: "1920x1080", fps: 24
261
261
  ```
262
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
+
263
275
  ### Transitions and handles
264
276
 
265
277
  Transitions come from `@scenerok/transitions` (full list in
@@ -295,6 +307,24 @@ input frames = "renders/shot.%04d.png", rate: 12, first: 1, count: 24
295
307
  `Trim`, `Speed`, and `Loop` work on it. For cloud renders the pattern must be
296
308
  an `https` URL pattern.
297
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
+
298
328
  ## Sound
299
329
 
300
330
  Every sound is an `audio` line with its own window, so J- and L-cuts need no
@@ -488,13 +518,25 @@ output to "master.mp4", resolution: "1920x1080", fps: 25, codec: "h264", bitrate
488
518
  `start_timecode` is `HH:MM:SS:FF` and its frame field must be below `fps`.
489
519
  `burn_in: "timecode"` draws the running timecode on the picture.
490
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
+
491
533
  ## What VidScript can't do yet
492
534
 
493
535
  Don't try to work around these. Tell the user when a request needs one.
494
536
 
495
537
  | Request | Today |
496
538
  | --- | --- |
497
- | Two aspect ratios from one script | One `output` per script; a second `output` line replaces the first. Write one script per format. |
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. |
498
540
  | Transparent video (alpha) | `alpha: true` is a compile error; renders are H.264 MP4. |
499
541
  | Multicam angle switching | No syntax. Cut between the angle clips with time blocks. |
500
542
  | Mixed frame rates | A timeline has one frame rate, the output `fps`. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Use **TypeScript + `@scenerok/sdk`** when the scene needs loops, branches, arithmetic timing, or data-driven composition. Keep **VidScript** for short human-authored promos and templates.
4
4
 
5
- Both paths emit the same `IRTimeline`. Preview and render (Pass 2) are unchanged.
5
+ Both paths emit the same `IRTimeline`, and the SDK follows VidScript's rules: a value placed with the same properties lowers to the same IR. Preview and render (Pass 2) are unchanged.
6
6
 
7
7
  ## When to choose which
8
8
 
@@ -10,6 +10,7 @@ Both paths emit the same `IRTimeline`. Preview and render (Pass 2) are unchanged
10
10
  |--------|----------|
11
11
  | Short promo, human edit, template placeholders | VidScript (`.vid`) |
12
12
  | Loop over clips, computed `start`/`end`, agent-authored complex timeline | TypeScript + `@scenerok/sdk` (`.ts`) |
13
+ | Mostly an edit, with one data-driven part | VidScript that imports a `.ts` module ([below](#use-typescript-from-vidscript)) |
13
14
 
14
15
  ## Schedule syntax
15
16
 
@@ -35,53 +36,120 @@ export default async function main() {
35
36
  |------|---------|
36
37
  | `t[start][end] = value` | Preferred dual-index schedule |
37
38
  | `t.range(start, end, value)` | Explicit call (no Proxy) |
38
- | `t['-'] = value` / `t.push(value)` | Auto-append at playhead |
39
+ | `t['-'] = value` / `t.push(value, duration?)` | Auto-append at the playhead |
39
40
 
40
- `start` / `end` accept numbers (seconds), `s(3)` / `ms(300)`, or strings (`'3s'`, `'1:30'`). Full JS arithmetic works.
41
+ Times are numbers (seconds), `s(3)` / `ms(300)`, `t.frames(48)`, or strings: `'3s'`, `'300ms'`, `'1:30'`, `'48f'` (frames of the timeline's fps), `'01:00:00:00'` (timecode at the timeline's fps, or `'01:00:00:00@24'`). Full JS arithmetic works. Like VidScript, every window snaps to the timeline's frame grid; `fps: 23.976` and the other NTSC rates are exact (`ir.timeBase`).
41
42
 
42
- No `video` / `text` keywords after `=` — factories return typed values and the runtime infers IR kind.
43
+ A video or audio value whose length is known ends when it stops playing, even inside a longer window. Without a known length it fills the window.
43
44
 
44
- Use `loadVideo(path)` / `loadAudio(path)` to probe duration through the media stdlib when `duration` is omitted:
45
+ Use `loadVideo(path)` / `loadAudio(path)` to probe the length through the media stdlib, or pass `duration` when you know it:
45
46
 
46
47
  ```ts
47
48
  const clip = await loadVideo('./hero.mp4')
48
49
  t[0][clip.duration] = clip
50
+ t.push(video('./b.mp4', { duration: 4 }))
49
51
  ```
50
52
 
51
- Use the sync `video(path, { duration })` / `audio(path, { duration })` factories when you already know the timing.
53
+ ## Values
52
54
 
53
- ## Grid areas
55
+ | Factory | VidScript | Notes |
56
+ |---------|-----------|-------|
57
+ | `video(src, opts)` / `loadVideo` | `input a = "a.mp4"` | `rate:` interprets the footage at a frame rate |
58
+ | `imageSequence('shot.%04d.png', { count, rate?, first? })` | `input frames = "shot.%04d.png", count: …` | Plays `count / rate` seconds |
59
+ | `image(src, opts)` | an image input | Fills its window; 3 s when pushed |
60
+ | `text(content, opts)` | `text "…"` | `stagger: 'word' \| 'char'` for `typewriter()` |
61
+ | `audio(src, opts)` / `loadAudio` | `audio x` | `trimStart`, `pan`, `duckUnder`, `loudness: 'ebu-r128'`, `animate: volumeRamp(…)` |
62
+ | `solid(color, opts)` | `solid("#101820")` | Full frame unless sized; with `radius`/`corners` it is a shape matte |
63
+ | `gap()` | `gap()` | `t.push(gap(), 2)` moves the playhead 2 s |
64
+ | `marker(name, metadata?)` | `marker("act 2")` | `t[4][4] = marker('act 2')`; timeline data, never drawn |
65
+ | `filter(name, params)` / `shader(glsl, params)` | `filter "glow"` | Grades the frame (inside a nested timeline: only that timeline) |
54
66
 
55
- Same CSS-grid regions as VidScript. Declare a grid on the timeline, then place text with `area:`:
67
+ Values are immutable. Methods return a copy, so one clip can be cut many ways:
56
68
 
57
69
  ```ts
58
- import { timeline, text, s } from '@scenerok/sdk'
70
+ const beach = video('./beach.mp4', { duration: 60 })
71
+ t.push(beach.trim(10, 20).trim(2, 4)) // source 12–14 s: a second trim is relative to the first
72
+ t.push(beach.trim(30, 36).speed(1.5).resize(960, 540))
73
+ t.push(beach.trim(40, 44).crop(0, 0, 1280, 720).filter('sepia', { intensity: 0.6 }))
74
+ ```
59
75
 
60
- export default function main() {
61
- const t = timeline({ width: 1080, height: 1920, fps: 30 })
76
+ | Method | On | Meaning |
77
+ |--------|----|---------|
78
+ | `trim(start, end?)` | video, image, audio | Keep a source range |
79
+ | `speed(x)` / `loop(n)` | video | Retiming; the value's `duration` follows |
80
+ | `resize(w, h)` / `opacity(x)` | video, image, timeline | Drawn size, opacity |
81
+ | `filter(name, params)` | video, image, timeline | Grade only this value (pointwise colour filters on clips) |
82
+ | `crop(x, y, w, h)` | video, image, timeline | Source pixels from the top-left; a second crop is relative |
83
+ | `mask(matte, { mode })` | video, image, timeline | `alpha` (default), `luma`, `inverted`; the matte is any picture |
84
+ | `animate(…)` / `with({ … })` | all pictures | Add animations / merge options |
85
+
86
+ ## Placement properties
87
+
88
+ Every picture takes the same options as a VidScript placement:
89
+
90
+ | Option | Meaning |
91
+ |--------|---------|
92
+ | `x`, `y`, `width`, `height` | Top-left and drawn size in output pixels |
93
+ | `area` | A grid area; explicit `x`/`y`/`width`/`height` win |
94
+ | `opacity`, `rotation` | Constant opacity and rotation (degrees) over the window |
95
+ | `radius`, `corners` | Rounded corners; four `[x, y]` corner pins (TL → TR → BR → BL) |
96
+ | `blend` | `multiply`, `screen`, `overlay`, `difference`, `add`, `subtract`, `darken`, `lighten` over what is already placed in the window |
97
+ | `motionBlur` | `true` or `{ shutter, samples }`: blurs the value's own `animate` motion |
98
+ | `enabled` | `false` skips the placement |
99
+ | `animate` | One animation or a list |
62
100
 
63
- t.grid({
64
- rows: '20% 1fr 30%',
65
- cols: '1fr 1fr',
66
- areas: ['title title', 'left right', 'caption caption'],
67
- })
101
+ ```ts
102
+ t[0][6] = video('./city.mp4')
103
+ t[0][6] = video('./flare.mp4', { blend: 'screen', opacity: 0.7 })
104
+ t[1][4] = image('./logo.png', { width: 240, height: 240, x: 60, y: 60, radius: 120 })
105
+ t[0][6] = video('./portrait.mp4').mask(solid('#fff', { width: 800, height: 800, x: 560, y: 140, radius: 400 }))
106
+ ```
68
107
 
69
- t[0][s(3)] = text('BIG SALE', { size: 72, area: 'title' })
70
- t[0][s(3)] = text('Left column', { size: 40, area: 'left' })
108
+ A blend needs something below it in the window; nothing below is an error, as in VidScript.
71
109
 
72
- return t.toIR()
110
+ ## Animations
111
+
112
+ `fadeIn`, `fadeOut`, `slideY`, `slideX`, `scale`, `popIn`, `riseIn`, `swingIn`, `glitchIn`, `float`, `typewriter`, `rotate` — the same set and defaults as `@scenerok/basic-animations`. Any other type: `animation('kenBurns', { scale: 1 }, { scale: 1.1 }, 0)` (a duration of 0 spans the window). Gain automation on audio: `volumeRamp(0, 1, '2s')`.
113
+
114
+ ## Nested timelines and groups
115
+
116
+ A timeline is a value. Placed bare, its items join the parent shifted to the window start — like VidScript `use`. A nested timeline without its own size takes its parent's canvas.
117
+
118
+ ```ts
119
+ function card(title: string) {
120
+ const c = timeline({ name: 'card' })
121
+ c[0][3] = solid('#101820', { height: 200, y: 880, opacity: 0.85 })
122
+ c[0][3] = text(title, { size: 48, y: '88%' })
123
+ return c
73
124
  }
125
+
126
+ t[2][5] = card('Konkan Day 1') // flat
127
+ t[6][9] = card('Day 2').opacity(0.8).place({ x: 40, y: -120 }) // one group
128
+ t[9][12] = card('Day 3').filter('grayscale') // the grade covers only the card
74
129
  ```
75
130
 
76
- Without `t.grid(...)`, `area: 'title' | 'stage' | 'lower-third' | 'caption'` uses the built-in social-video preset (portrait vs landscape). Explicit `x` / `y` / `maxWidth` / `maxHeight` override the area.
131
+ `place(opts)`, `opacity`, `resize`, `filter`, `crop`, and `mask` draw the timeline as one `IRGroup`, as does a frame `filter` inside it. A group lasts for its window; a longer body is cut off. Transitions cannot be inside a group.
77
132
 
78
- Helpers also available: `resolveGridAreas`, `defaultGridSpec`, `placeInArea`.
133
+ ## Grid areas
79
134
 
80
- Same semantics as VidScript `grid rows: ..., areas: [...]` — see `vidscript-guide.md` → Grid Areas.
135
+ Same CSS-grid regions as VidScript. Declare a grid on the timeline, then place with `area:` (text centres in the cell; videos, images, and solids fill it):
136
+
137
+ ```ts
138
+ const t = timeline({ width: 1080, height: 1920, fps: 30 })
139
+ t.grid({
140
+ rows: '20% 1fr 30%',
141
+ cols: '1fr 1fr',
142
+ areas: ['title title', 'left right', 'caption caption'],
143
+ })
144
+ t[0][s(3)] = text('BIG SALE', { size: 72, area: 'title' })
145
+ t[0][s(3)] = image('./shoe.png', { area: 'left' })
146
+ ```
147
+
148
+ Without `t.grid(...)`, `area: 'title' | 'stage' | 'lower-third' | 'caption'` uses the built-in social-video preset (portrait vs landscape).
81
149
 
82
150
  ## Transitions
83
151
 
84
- Same `@scenerok/transitions` package as VidScript. Schedule a transition window between abutting clips; `toIR()` binds `fromId` / `toId` automatically:
152
+ Same `@scenerok/transitions` package as VidScript. Schedule a transition window between abutting clips; `toIR()` binds `fromId` / `toId`:
85
153
 
86
154
  ```ts
87
155
  import { timeline, video } from '@scenerok/sdk'
@@ -89,28 +157,67 @@ import tr from '@scenerok/transitions'
89
157
 
90
158
  export default function main() {
91
159
  const t = timeline({ width: 1080, height: 1920, fps: 30 })
92
-
93
160
  t[0][5] = video('./a.mp4', { duration: 5 })
94
- t[5][5.6] = tr.dissolve()
161
+ t[5][5.6] = tr.dissolve({ inOffset: 0.3, outOffset: 0.3 })
95
162
  t[5.6][10] = video('./b.mp4', { duration: 4.4 })
96
-
97
- // Or auto-append with catalog default (0.6s):
98
- // t.push(tr.push({ direction: 'right' }))
99
-
100
163
  return t.toIR()
101
164
  }
102
165
  ```
103
166
 
104
- Common options: `direction`, `ease`, `strength`, `mode` (`roll` | `freeze`). Full catalog: `transitions-catalog.md`.
167
+ Common options: `direction`, `ease`, `strength`, `mode` (`roll` | `freeze`), and the handles `inOffset` / `outOffset` (source seconds read past each cut). Full catalog: `transitions-catalog.md`.
168
+
169
+ ## Output
170
+
171
+ ```ts
172
+ timeline({
173
+ width: 1920, height: 1080, fps: 23.976,
174
+ output: { path: 'reel.mp4', startTimecode: '01:00:00:00', burnIn: 'timecode' },
175
+ })
176
+ ```
105
177
 
106
178
  ## Entry contract
107
179
 
108
- Export `default async function main()` (or sync `main`) that returns either:
180
+ Export `default async function main()` (or sync `main`) that returns either `t.toIR()`, the timeline, or an `IRTimeline` object. `async main` lets you `await loadVideo(...)`, `await getVideoLength(...)`, or `await grabFrame(...)`.
181
+
182
+ ## Use TypeScript from VidScript
183
+
184
+ A `.vid` can import a TypeScript module and place what it exports. Export values instead of (or beside) a default scene:
109
185
 
110
- - `t.toIR()`, or
111
- - an `IRTimeline` object
186
+ ```ts
187
+ // kit.ts
188
+ import { timeline, video, text, audio, solid } from '@scenerok/sdk'
189
+
190
+ export const accent = '#ff5500'
191
+ export const hero = video('beach.mp4', { duration: 12 }).trim(2, 8)
192
+ export const title = text('Konkan Day 1', { size: 72, area: 'title' })
193
+ export const bed = audio('bed.mp3', { duration: 30, volume: 0.4 })
194
+
195
+ export function lowerThird(name: string, opts: { color?: string } = {}) {
196
+ const t = timeline()
197
+ t[0][3] = solid(opts.color ?? accent, { height: 160, y: 880 })
198
+ t[0][3] = text(name, { size: 48, y: '88%' })
199
+ return t
200
+ }
201
+ ```
202
+
203
+ ```vidscript fragment
204
+ import kit from "./kit.ts"
112
205
 
113
- `async main` is supported so you can `await loadVideo(...)`, `await getVideoLength(...)`, or `await grabFrame(...)`.
206
+ [0s .. 6s] = kit.hero
207
+ [1s .. 4s] = kit.title
208
+ [0s .. 10s] = audio kit.bed
209
+ [3s .. 6s] = kit.lowerThird("Nidheesh", color: kit.accent)
210
+
211
+ output to "reel.mp4", resolution: "1920x1080"
212
+ ```
213
+
214
+ - `timeline()` without a size takes the importing script's output size and fps.
215
+ - Named arguments reach a function as one trailing options object; `use kit.lowerThird at 6s .. 9s with { color: "#0a8" }` calls `lowerThird({ color: "#0a8" })`.
216
+ - A default-exported scene still works: `use kit at 0s .. 0s with {}`, and `kit.duration` is its length.
217
+ - Filters, shaders, and transitions are not exportable values; use them inside an exported timeline.
218
+ - The module is one file (the CLI uploads it next to the `.vid`); top-level `await` is not supported, but an exported promise is awaited.
219
+
220
+ See `vidscript-guide.md` → Import TypeScript modules.
114
221
 
115
222
  ## Validate / render
116
223
 
@@ -119,16 +226,6 @@ scenerok validate scene.ts
119
226
  scenerok render scene.ts --watch
120
227
  ```
121
228
 
122
- You can also **include** an SDK scene from VidScript:
123
-
124
- ```vidscript fragment
125
- import intro from "./intro.ts"
126
- use intro at 0s .. 0s with {}
127
- output to "reel.mp4"
128
- ```
129
-
130
- See `vidscript-guide.md` → Include TypeScript / JavaScript scenes.
131
-
132
229
  MCP:
133
230
 
134
231
  - `scenerok_validate_vidscript` with `language: "typescript"`
@@ -137,7 +234,7 @@ MCP:
137
234
 
138
235
  ## AI plugins (xAI / Cloudflare / ElevenLabs)
139
236
 
140
- Same packages as VidScript. Import them in TypeScript; the SceneRok compile host runs the real plugins and returns SDK `video` / `audio` nodes:
237
+ Same packages as VidScript. Import them in TypeScript; the SceneRok compile host runs the real plugins and returns SDK `video` / `audio` values (a generated clip with sound brings its sound along; set its level with `volume`):
141
238
 
142
239
  ```ts
143
240
  import { timeline } from '@scenerok/sdk'
@@ -159,7 +256,7 @@ export default async function main() {
159
256
  t[0][clip.duration] = clip
160
257
  t[clip.duration][clip.duration + still.duration] = still
161
258
  t.push(vo)
162
- t.push(bed)
259
+ t.push(bed.with({ volume: 0.3, duckUnder: vo }))
163
260
  return t.toIR()
164
261
  }
165
262
  ```
@@ -172,10 +269,13 @@ export default async function main() {
172
269
 
173
270
  Requires the same env secrets as VidScript (`XAI_API_KEY`, `ELEVENLABS_API_KEY`, Cloudflare AI Gateway credentials). Generation is billed on render the same way as VidScript plugins.
174
271
 
175
- ## MVP limits
272
+ ## VidScript only
273
+
274
+ - `filter "lut", file: "look.cube"` and `captions("talk.srt")` read files at compile time.
275
+ - Several `output` statements (two canvases) and `allow_offline: true`.
276
+ - `--generate-assets` (SDK scripts generate assets during `scenerok render` when plugins are called).
176
277
 
177
- - Prefer `getVideoLength(path)` from `@scenerok/stdlib` instead of hard-coding `duration:` when assets exist on disk
178
- - `--generate-assets` remains VidScript-only (SDK scripts generate assets during `scenerok render` when plugins are called)
278
+ Import the TypeScript part into a `.vid` when a scene needs these.
179
279
 
180
280
  ## Media stdlib
181
281
 
@@ -70,7 +70,7 @@ input frames = "shots/shot.%04d.png", rate: 12, first: 1, count: 24
70
70
  [0s .. 2s] = frames
71
71
  ```
72
72
 
73
- Place it by name like a video; `Trim`, `Speed`, and `Loop` work on it. `rate:`, `first:`, `count:` on a plain file are errors. On the render workers the pattern must be an `https` URL pattern (or a path on the worker).
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
74
 
75
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.
76
76
 
@@ -114,6 +114,8 @@ output to "video.mp4", resolution: "1080x1920", fps: 30
114
114
  output to "reel.mp4", resolution: "720x1280", format: "mp4", codec: "h264", bitrate: "5M", background: "#0D0D0D"
115
115
  ```
116
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
+
117
119
  | Option | Type | Default | Description |
118
120
  |--------|------|---------|-------------|
119
121
  | `resolution` | string | `"1080x1920"` | Width×Height or single number |
@@ -124,6 +126,7 @@ output to "reel.mp4", resolution: "720x1280", format: "mp4", codec: "h264", bitr
124
126
  | `background` | string | `"#000000"` | Background color hex |
125
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 |
126
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 |
127
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 |
128
131
 
129
132
  ### Variable Assignment
@@ -228,26 +231,59 @@ let vo2 = xai.tts("Let's dive in", voice: "eve")
228
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.
229
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.
230
233
 
231
- ## Include TypeScript / JavaScript scenes
234
+ ## Import TypeScript modules
235
+
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:
237
+
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 |
232
249
 
233
- Import a `@scenerok/sdk` scene (`.ts` / `.js`) into VidScript with the same default-import form as plugins, then `use` it like a timeline:
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
+ ```
234
266
 
235
267
  ```vidscript fragment
236
- import intro from "./intro.ts"
237
- import bump from "./bump.js"
268
+ import kit from "./kit.ts"
238
269
 
239
- use intro at 0s .. 0s with {}
240
- use bump at intro.duration .. intro.duration with {}
241
- [intro.duration + bump.duration .. intro.duration + bump.duration + 2s] = text "Thanks", area: "caption"
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" }
242
275
 
243
- output to "reel.mp4", resolution: "1080x1920"
276
+ output to "reel.mp4", resolution: "1920x1080"
244
277
  ```
245
278
 
246
279
  Rules:
247
- - The SDK file must export `default function main()` (or `module.exports = main` / `exports.main`) that returns `t.toIR()` or an `IRTimeline`.
248
- - `with { ... }` params are not supported on SDK scenes (no timeline parameters).
249
- - Relative paths only (`./` / `../`). The CLI uploads companion scene files automatically when you `scenerok validate` / `scenerok render` a `.vid`.
250
- - 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.
251
287
 
252
288
  ## Instructions (inside time blocks)
253
289
 
@@ -237,7 +237,7 @@ input logo = "logo.png"
237
237
  - A placed value lasts its own length (trimmed range ÷ speed × loops). Make the window that long.
238
238
  - Put one video over another with `Overlay`, `Composite`, or `blend:`. Two plain clips in the same window have no defined order.
239
239
  - `Mask` on its own line after the clip is placed is a compile error.
240
- - Frame counts are written `48 frame` and use the output fps (30 if `output` has none).
240
+ - Frame counts are written `48 frame` and use the output fps (30 if `output` has none). Timecodes are `01:00:02:12@24` (`@rate` optional; non-drop-frame only). Block times snap to the output frame grid; NTSC fps (23.976, 29.97) encodes at the exact rational rate.
241
241
 
242
242
  All editing actions (masks, speed ramps, ducking, LUTs, timecode): `editing-actions.md`.
243
243