@scenerok/sdk 0.25.0 → 0.27.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/sdk",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "TypeScript-first SceneRok authoring SDK — build IRTimeline for complex agent-authored scenes",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,14 +35,15 @@ Copy this checklist into your notes and work through it:
35
35
 
36
36
  ```text
37
37
  - [ ] 1. Identify the environment (web editor, CLI, ChatGPT, MCP, chat without tools)
38
- - [ ] 2. Classify the request; read only the references it needs (table below)
39
- - [ ] 3. Branded promo with unknown colors or logo: ask once
40
- - [ ] 4. Probe real assets: type, width, height, duration
41
- - [ ] 5. Write the script
42
- - [ ] 6. Validate. Fix every error and validate again until clean
38
+ - [ ] 2. Pick the closest recipe in recipes.md and read look-and-feel.md
39
+ - [ ] 3. Gather pictures: page images, screenshots, logo, footage, or generated
40
+ stills (look-and-feel.md → Sourcing imagery). Probe each one; crop with ffmpeg
41
+ - [ ] 4. Decide the look: palette, two fonts, type sizes, beats. Write them as `let`s
42
+ - [ ] 5. Write the script from the recipe; read other references only as needed
43
+ - [ ] 6. Validate. Fix every error and aesthetic warning; validate again until clean
43
44
  - [ ] 7. Uses TTS or generated media: deep-validate and fix the report (CLI only)
44
- - [ ] 8. Render
45
- - [ ] 9. Check the rendered MP4; fix and repeat from step 6 if it reports problems (CLI only)
45
+ - [ ] 8. Render, pull one frame per beat, look at them, run the freeze check
46
+ - [ ] 9. Fix what the frames show and render again. Done = the quality gate passes
46
47
  ```
47
48
 
48
49
  With the CLI:
@@ -56,9 +57,13 @@ scenerok lint-video <render-id> --cost medium --json # billed
56
57
 
57
58
  `scenerok render script.vid --validate` renders and checks the MP4 in one step.
58
59
 
59
- With marketplace plugins (fastest when the user pasted structured source data):
60
- search, inspect, dry-run, then run a pinned ref — full workflow in
61
- [plugins.md](references/plugins.md):
60
+ With marketplace plugins: search, inspect, dry-run, then run a pinned ref
61
+ (full workflow in [plugins.md](references/plugins.md)). A plugin's output must
62
+ pass the same quality gate. When a plugin's template is text over a flat
63
+ colour, draft it (`scenerok plugin draft … --out draft.vid`), add the
64
+ pictures and motion from [recipes.md](references/recipes.md), then
65
+ `scenerok plugin render-draft <id> --vidscript draft.vid`. Today that covers
66
+ the first-party `listing-promo` and `release-video` templates.
62
67
 
63
68
  ```bash
64
69
  scenerok plugin search "product promo"
@@ -96,6 +101,43 @@ ChatGPT, and MCP have their own validate and render calls but no deep
96
101
  validation or MP4 check, and can't read local files:
97
102
  [environments.md](references/environments.md).
98
103
 
104
+ ## Quality gate
105
+
106
+ `validate` tells you the script is valid, not that the video is good. A video
107
+ is done only when all of these hold. Text over a flat colour fails, however
108
+ clean the script.
109
+
110
+ 1. **A picture under every beat.** Each beat shows a real asset (product
111
+ photo, screenshot, logo, footage, screen recording) or a generated image or
112
+ video. No beat is type on a plain colour.
113
+ 2. **Nothing stands still.** Every picture and text line has `animate:` with an
114
+ entrance; anything that leaves before the end also has an exit. Stills get a
115
+ slow push; logos float. After rendering, this prints nothing (no hold of 3 s
116
+ or longer; a `freeze_start` with no `freeze_end` runs to the end):
117
+ `ffmpeg -hide_banner -i out.mp4 -vf freezedetect=n=0.003:d=3 -map 0:v -f null - 2>&1 | grep freeze_start`
118
+ 3. **A deliberate look.** The script opens with `let` values for its palette
119
+ (bg, ink, muted, one accent) and its two fonts. Sizes follow the type scale
120
+ in [look-and-feel.md](references/look-and-feel.md): at least 44 px on a
121
+ 1920 px tall canvas.
122
+ 4. **One grade.** `filter "cdl"` and/or `vignette` (or a LUT from an HTTPS
123
+ URL) spans the whole video.
124
+ 5. **Sound.** A music bed, a voiceover, or the footage's own audio, unless the
125
+ user asked for silence.
126
+ 6. **Validate clean.** Fix every error and every aesthetic warning, or write
127
+ down why a warning doesn't apply (each recipe names the warnings it accepts
128
+ and why).
129
+ 7. **Looked at.** Render, pull one frame per beat, open the frames, and fix what
130
+ you see: wrapped or colliding text, black boxes where a picture should be,
131
+ a fallback font, empty bands. Then render again. Validate's text sizes are
132
+ estimates, so only the frames show the real result.
133
+
134
+ ```bash
135
+ for t in 1.5 5 9 13; do ffmpeg -loglevel error -y -ss $t -i out.mp4 -frames:v 1 frame-$t.png; done
136
+ ```
137
+
138
+ Without a shell (web editor, ChatGPT, MCP), check 1-6 against the script and
139
+ ask the user to watch the preview before you call it done.
140
+
99
141
  ## Rules that prevent most failures
100
142
 
101
143
  1. **Import before calling.** `import xai from "@scenerok/xai"`, then
@@ -114,6 +156,11 @@ validation or MP4 check, and can't read local files:
114
156
  6. **Use only documented syntax.** Don't invent functions or parameters. A
115
157
  validation error names the problem; fix that line rather than rewriting the
116
158
  script.
159
+ 7. **`font:` is a Google Fonts family name.** `"Inter Bold"` is not a family
160
+ and falls back. For heavy type use a heavy family such as
161
+ `"Archivo Black"`.
162
+ 8. **Crop stills with ffmpeg, not `.Crop()`.** Prepare images before you place
163
+ them; a cropped remote still rendered as nothing in testing.
117
164
 
118
165
  ## References
119
166
 
@@ -121,6 +168,7 @@ Read the ones the task needs. Paths are relative to this file.
121
168
 
122
169
  | Task | Read |
123
170
  | --- | --- |
171
+ | Any video | [recipes.md](references/recipes.md) and [look-and-feel.md](references/look-and-feel.md) (always): complete product promo, release notes and talking-head scripts; palette, type scale, safe areas, motion, sourcing pictures |
124
172
  | Any VidScript | [vidscript-strict.md](references/vidscript-strict.md) (always) |
125
173
  | Editing and compositing: trims, speed, reverse, freeze, picture-in-picture, blend modes, masks, crops, split screens, audio mixing and ducking, grading and LUTs, captions, timecode | [editing-actions.md](references/editing-actions.md) |
126
174
  | Full grammar | [vidscript-guide.md](references/vidscript-guide.md) |
@@ -148,14 +196,25 @@ chat apps are there too: `consumer-chat-apps.md`,
148
196
  `chatgpt-gpt-instructions.md`, `gemini-gem-instructions.md`, `grok-skill.md`.
149
197
  Previews of every catalog item: https://scenerok.com/catalog.
150
198
 
151
- ## Minimal VidScript
199
+ ## One beat, done properly
200
+
201
+ The smallest unit that passes the gate: a real picture with a push, copy that
202
+ enters and leaves, a grade, and sound. A video is several of these; the full
203
+ scripts are in [recipes.md](references/recipes.md).
152
204
 
153
205
  ```vidscript
154
206
  import motion from "@scenerok/basic-animations"
155
207
 
156
- input hero = "https://cdn.example.com/clip.mp4"
157
- [0s .. 4s] = hero.Trim(1s, 5s)
158
- [0s .. 3s] = text "Hello", style: title, color: "#FFFFFF", area: "title", animate: motion.fadeIn(0.8s)
208
+ let ink = "#F4F6FA"
209
+ let display = "Archivo Black"
210
+
211
+ input shot = "https://cdn.example.com/product-lifestyle.jpg" # 1080x1920, probed
212
+ input bed = "https://cdn.example.com/music.mp3"
213
+
214
+ [0s .. 4s] = shot, width: 1080, height: 1920, x: 0, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }
215
+ [0.2s .. 4s] = text "Cleans while you're out", font: display, size: 96, color: ink, x: "50%", y: "28%", max_width: "86%", align: center, shadow_color: "#000000", shadow_blur: 24, animate: [motion.riseIn(0.5s, 60), motion.fadeOut(0.3s)]
216
+ [0s .. 4s] = filter "vignette", intensity: 0.3
217
+ [0s .. 4s] = audio bed, volume: 0.5, fade_out: 0.5s
159
218
  output to "video.mp4", resolution: "1080x1920", fps: 30
160
219
  ```
161
220
 
@@ -98,6 +98,15 @@ overlays (composite over video/scenes), honor every param, and scale to any outp
98
98
  Prefer these over hand-authoring when a headline/caption/stat treatment fits; see the
99
99
  `text-anim` rows in `animations-picker.md` for intent mapping.
100
100
 
101
+ ## Code blocks in vertical video
102
+
103
+ At a 9:16 output the 12 VS Code theme `code_snippet_*` blocks and `code_diff` switch to a
104
+ native 1080x1920 layout: no desktop chrome, ~40px type that wraps instead of shrinking, a
105
+ line-by-line reveal with a moving focus bar and caret (a +/− count and a focus ring for
106
+ `code_diff`), a camera that scrolls long code, and a lift-and-fade exit timed to the
107
+ placement. Same params, same theme palettes; 16:9 is unchanged. It still reads best with
108
+ 3–12 short lines: put the one change that matters on screen, not the whole file.
109
+
101
110
  ## Custom HTML (bring your own animation)
102
111
 
103
112
  `animations.custom_html(...)` renders **your own HTML/CSS/JS** through the same HyperFrames
@@ -47,10 +47,13 @@ Param resolution: **named > positional > default**.
47
47
  | Block / family | Issue | Fix |
48
48
  | --- | --- | --- |
49
49
  | `code_diff` | Line indices may be 0-based in adapter | Match sample in `animations-catalog.md` |
50
+ | Code blocks in 9:16 | Only the VS Code theme `code_snippet_*` blocks and `code_diff` have a portrait layout; `code_typing`, `code_morph`, the terminal blocks, etc. are scaled down from 16:9 and read small | Prefer `code_snippet_<theme>` / `code_diff` for vertical video; pass 3–12 short lines, not a file |
50
51
  | Unverified blocks | `scenerok validate` warns `verified:false` | Accept demo render or switch to verified family |
51
52
  | Maps / liquid-glass / app-showcase | Ignore user params | Use for demo aesthetic only, or hand-author |
52
53
  | Adapter blocks | No inline audio/text siblings | Put `audio`, `text`, `filter` on separate time blocks |
53
54
  | `logo_outro` | Renders upstream demo's Figma-style mark when no logo param given | Pass `brand: "scenerok"` or `logoUrl: "<image url>"` |
55
+ | `code_*` and most blocks in 9:16 | Designed at 1920x1080 and scaled to fit: 56 % size with empty bands above and below (lower thirds have native 9:16 layouts) | Use them in 16:9; in 9:16 build a code card from `text` lines (`recipes.md` → Release notes) |
56
+ | `text_counter_stat` | `suffix` defaults to `"+"`, so 12 reads "12+" | Pass `suffix: ""` unless the number really is a floor |
54
57
 
55
58
  ## Two-phase compose recipe
56
59
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schema": "https://scenerok.com/catalog-index/v1",
3
- "generatedAt": "2026-10-26T01:38:31.092Z",
3
+ "generatedAt": "2026-10-27T20:44:13.966Z",
4
4
  "cdnBase": "https://assets-public.scenerok.com/catalog",
5
5
  "categories": [
6
6
  {
@@ -462,8 +462,10 @@ input a = "log-footage.mp4"
462
462
  [0s .. 4s] = filter "cdl", slope: "1.1 1 0.9", offset: "0 0 0", power: "1 1 1", saturation: 1
463
463
  ```
464
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")`.
465
+ - `filter "lut"` applies a 3D `.cube` file (512 KB at most, up to 33 points
466
+ per axis). Use the `filter` line, not `Filter("lut")`. A hosted compile (CLI
467
+ render, web, MCP) can't read a local path: give an HTTPS URL or a `data:`
468
+ URI, or use `cdl`.
467
469
  - `filter "cdl"` takes ASC CDL `slope`, `offset`, `power` (three numbers each)
468
470
  and `saturation`.
469
471
 
@@ -29,13 +29,13 @@ Defaults unless the user says otherwise:
29
29
 
30
30
  | Workflow | Load these slugs | Primary APIs (VidScript · SDK) |
31
31
  | --- | --- | --- |
32
- | **Product promo** | `ai-models.md`, `text-styles.md`, `music.md`, `workflow.md`, `brand-setup.md` (+ `sdk-guide.md` if TS) | `xai.imagine` / `await xai.genVideo(...)`, `motion.popIn` / `fadeIn`, `eleven.music` |
32
+ | **Product promo** | `recipes.md`, `look-and-feel.md`, `brand-setup.md` (+ `ai-models.md` when the page has no usable pictures, `sdk-guide.md` if TS) | page images as `input` with a scale push, `motion.riseIn` / `popIn`, `solid` CTA button, `filter "cdl"`, `eleven.music` |
33
33
  | **Motion graphic** | `animations-picker.md` (+ `animations-catalog.md` if needed) | `animations.*` (VidScript HyperFrames; prefer VidScript for catalog blocks) |
34
34
  | **Social proof card** | `animations-picker.md` | `animations.x_post`, `reddit_post`, … (VidScript) |
35
35
  | **Edit existing footage** | `editing-actions.md` (+ `media-stdlib.md` to probe the clips, `transitions-catalog.md` for cuts) | `Trim` / `Speed` / `Loop` / `Crop` / `Mask` / `Overlay` / `Composite`, `blend:`, `duck_under:`, `filter "lut"` (VidScript only) |
36
36
  | **Scene montage** | `transitions-catalog.md`, `ai-models.md` (+ `sdk-guide.md` if TS loops) | `tr.dissolve` · SDK: `import tr from '@scenerok/transitions'` + `t[start][end] = tr.dissolve()` |
37
37
  | **Narrated explainer** | `tts.md`, `text-styles.md`, `music.md`, `workflow.md` (+ `sdk-guide.md` if TS) | `xai.tts` · `await xai.tts(...)` + `t.push(vo)` |
38
- | **Code demo / PR** | `animations-picker.md` (code rows) | `code_typing`, `code_diff` (VidScript) |
38
+ | **Code demo / PR / release notes** | `recipes.md` (release notes), `animations-picker.md` (code rows) | 16:9: `code_typing`, `code_diff`; 9:16: a code card of `text` lines with `motion.typewriter` (VidScript) |
39
39
  | **Text animation / headline / stat** | `animations-picker.md` (text-anim rows) | `animations.text_*` (VidScript) · SDK kinetic: `text` + `fadeIn` / `popIn` |
40
40
  | **Kinetic title only** | `text-styles.md` (+ `sdk-guide.md` if TS) | `text` + `motion.*` · SDK: `text(..., { area })` + `.animate(fadeIn(...))` |
41
41
  | **Custom-authored animation (no block fits)** | `animations-catalog.md` → "Custom HTML" | `animations.custom_html` (VidScript) |
@@ -43,7 +43,7 @@ Defaults unless the user says otherwise:
43
43
 
44
44
  ## Staged routing
45
45
 
46
- 1. **Classify** — workflow from table above
46
+ 1. **Classify** — workflow from table above, then the matching script in `recipes.md`
47
47
  2. **Choose authoring** — VidScript by default and for every editing action (masks, blends, crops, ducking, LUTs, timecode); `@scenerok/sdk` (`sdk-guide.md`) only for loops or computed timing (it has none of the editing actions)
48
48
  3. **Load catalogs** — only what that workflow needs (conditional loading)
49
49
  4. **Name block or API** — VidScript: pick `animations.<fn>` from `animations-picker.md`. SDK: factories (`video` / `loadVideo`, `text`, `fadeIn`) + AI imports (`@scenerok/xai`, …)
@@ -0,0 +1,139 @@
1
+ # Look and feel
2
+
3
+ Decide the look before you write a block. Declare it as `let` values at the top
4
+ of the script and use only those values below. `recipes.md` shows the whole
5
+ system in use.
6
+
7
+ ## Palette
8
+
9
+ Four colours, declared once:
10
+
11
+ | Name | Use | Default (dark) |
12
+ | --- | --- | --- |
13
+ | `bg` | Canvas and plates behind cards | `#0A0C10` |
14
+ | `ink` | Headlines and body copy | `#F4F6FA` |
15
+ | `muted` | Secondary lines: ratings, URLs, captions | `#A3ADBD` |
16
+ | `accent` | One brand colour: kicker, price, bullets, CTA button, highlight bar | sampled from the brand |
17
+
18
+ Sample the accent from the brand's own pixels (a button or logo on the page)
19
+ or from `brand-setup.md`. Don't invent one. Use at most three non-neutral text
20
+ colours (`validate` warns above that).
21
+
22
+ ## Type
23
+
24
+ Use two families: a display face and a body face. `font:` is one Google
25
+ Fonts family, loaded at weight 400. A weight in the name (`"Inter Bold"`,
26
+ `"Inter Black"`) is not a family: the render falls back to a default face.
27
+ For heavy type, use a family that is heavy at 400.
28
+
29
+ Rendered and checked: `Archivo Black` (display), `Inter` (body),
30
+ `JetBrains Mono` (code; turns `=>` into a ligature). Other single-weight
31
+ display families: `Anton`, `Bebas Neue`.
32
+
33
+ Sizes in output pixels (`size:`). `validate` flags text under 2.2 % of the
34
+ canvas height:
35
+
36
+ | Role | 1080x1920 (9:16) | 1920x1080 (16:9) |
37
+ | --- | --- | --- |
38
+ | Hero / price | 120-150 | 120-140 |
39
+ | Headline | 84-104 | 72-88 |
40
+ | Bullet / caption card | 64-72 | 48-56 |
41
+ | Body / kicker / URL (the floor) | 44-48 | 32-36 |
42
+
43
+ Kickers: body face, accent colour, `letter_spacing: 6`-`10`, all caps.
44
+ Validate's text geometry is an estimate. A display face can run 15-20 % wider
45
+ than it predicts, so check the rendered frame before trusting a line to fit.
46
+
47
+ ## Layout and safe areas
48
+
49
+ - Text `x`/`y` place the text's centre (`anchor: "left"` places its
50
+ left-centre). Prefer `area:` when a named region fits. Use explicit pixels
51
+ for designed layouts like the recipes.
52
+ - 9:16: keep copy between y 220 and y 1500 and x 72 to 1008. Feed UI covers
53
+ the top ~8 %, the bottom ~20 % and a right-hand icon rail.
54
+ - 16:9: keep copy inside a 96 px margin. Put the picture on one side and the
55
+ copy on the other.
56
+ - Margins: 48 px at the edge of a card; cards use `radius: 32`-`48`.
57
+ - Show at most 3 text overlays at once, except the lines of a code card.
58
+
59
+ ## Pictures
60
+
61
+ Every beat has a picture under its type. In order of preference:
62
+
63
+ 1. **Real**: product photos, logos, app screenshots, footage, screen recordings.
64
+ 2. **Generated**: `xai.image`, `xai.imageToVideo` from a real photo, `xai.imagine`
65
+ (`ai-models.md`; prompts forbid text).
66
+ 3. **Made**: a brand-colour glow or gradient rendered with ffmpeg (recipes →
67
+ Release notes), as a background behind real pictures, never as the only picture.
68
+
69
+ Placing stills in a frame of a different shape:
70
+
71
+ - **Card**: the still at its own aspect, `radius:` and `blend: "normal"`, on
72
+ a plain or blurred plate (a product on white in a 900 px card).
73
+ - **Band + blurred fill**: a wide crop as a full-width band, over a blurred,
74
+ darkened 9:16 copy of the same shot. This works for small or landscape stills.
75
+ - **Cover**: oversize the clip to fill, e.g. 16:9 into 9:16 at
76
+ `width: 3413, height: 1920, x: -1166`. This keeps transitions legal, which
77
+ `Crop` doesn't.
78
+
79
+ Prepare stills with ffmpeg before you place them. Crop out baked-in
80
+ captions, make the blurred fill, and probe the result:
81
+
82
+ ```bash
83
+ ffmpeg -i in.jpg -vf "crop=W:H:X:Y,scale=1080:-2" band.jpg
84
+ ffmpeg -i in.jpg -vf "crop=W:H:X:Y,scale=1080:1920,boxblur=40:2,eq=brightness=-0.18" fill.jpg
85
+ sips -g pixelWidth -g pixelHeight band.jpg
86
+ ```
87
+
88
+ Use `.Crop()` for video footage. A `.Crop()` on a remote still rendered as
89
+ nothing in testing, so crop stills with ffmpeg.
90
+
91
+ ## Sourcing imagery
92
+
93
+ Collect pictures before you write copy:
94
+
95
+ | Source | How |
96
+ | --- | --- |
97
+ | Product page | Image URLs from `og:image`, the JSON-LD `Product.image`, Shopify `/products/<handle>.json` → `images`, Amazon `#landingImage` `data-old-hires` and the gallery `m.media-amazon.com/images/I/…` URLs |
98
+ | Page screenshot | Browser tool at 1440x900 (desktop) and 390x844 (mobile). Crop to the hero, the price card, or the feature you name; never use a full page as a background |
99
+ | Logo | The site's header SVG/PNG, `apple-touch-icon`, or the GitHub org avatar (`https://avatars.githubusercontent.com/u/<id>`) |
100
+ | App or site in motion | A screen recording (QuickTime, `ffmpeg -f avfoundation`, a browser recorder) as an `input` clip |
101
+ | Nothing usable | `xai.image` / `xai.imageToVideo` (`ai-models.md`) |
102
+
103
+ Local `input` files upload with `scenerok render`. Probe every file
104
+ (`sips`, `ffprobe`) and size placements from the measured aspect.
105
+
106
+ ## Files the server must read
107
+
108
+ `filter "lut", file:` and `captions("…srt")` are read by the compiler. On a
109
+ hosted compile (CLI, web, MCP) a local path fails with `local asset not
110
+ found`. Pass an HTTPS URL or a `data:` URI, or grade with `filter "cdl"`,
111
+ which needs no file.
112
+
113
+ ## Motion
114
+
115
+ | Phase | Use | Timing |
116
+ | --- | --- | --- |
117
+ | Entrance | `motion.riseIn(0.5s, 40-80)` for copy and cards, `motion.popIn(0.4-0.6s)` for prices, buttons, logos, `motion.typewriter` for code, `motion.fadeIn(0.4s)` for secondary lines | Stagger items 0.3-0.6 s apart |
118
+ | Hold | Stills: a push `{ type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }`. Logos and products: `motion.float(3s, 10-14)` | A static hold lasts 3 s at most |
119
+ | Exit | `motion.fadeOut(0.3s)` on everything that leaves before the end | The last 0.3 s of the block |
120
+
121
+ A push on a soft gradient doesn't count as motion: give it a still, footage,
122
+ or a floating element to move. Fast moves can take `motion_blur:`
123
+ (`editing-actions.md`).
124
+
125
+ ## Pacing
126
+
127
+ - First visible picture and words by 0.3 s; the hook reads in under 1 s.
128
+ - Beats last 2.5-4.5 s each. A 15 s promo is 4 beats; a 30 s video is 6-8.
129
+ - One idea per beat: the hook, the product, three features, the price, and the call to action.
130
+ - Close on the call to action for at least 2.5 s.
131
+
132
+ ## What premium means here
133
+
134
+ - A real picture fills or anchors every beat; no beat is text on a flat colour.
135
+ - One accent, used sparingly; everything else is ink and muted on dark (or the reverse).
136
+ - Big type with tight copy: 3-6 words a line, never a paragraph.
137
+ - Every element arrives, holds with a little motion, and leaves.
138
+ - One grade ties it together: `filter "cdl"` warmth or coolness plus a `vignette` of about 0.3.
139
+ - Sound under all of it: a music bed, a voiceover, or both.
@@ -7,7 +7,10 @@ cheaper than hand-composing. **Never assume which plugins exist** — discover
7
7
  them at run time; the catalog grows and changes.
8
8
 
9
9
  Plugins are the reuse-first path: check `intent-router.md` for the workflow
10
- match, and prefer a plugin whenever the user pasted structured source data.
10
+ match. A plugin's video must still pass the quality gate in `SKILL.md`. Draft
11
+ first, read the filled script, and if a beat is text over a flat colour, add
12
+ the pictures and motion from `recipes.md` before `render-draft`. Today that
13
+ applies to the first-party `listing-promo` and `release-video` templates.
11
14
 
12
15
  ## Workflow (MCP)
13
16
 
@@ -219,6 +222,18 @@ Notes:
219
222
  render afterwards. A failed draft costs no fee; a draft refused by the
220
223
  balance preflight costs nothing at all.
221
224
 
225
+ ## Permission grants
226
+
227
+ A **third-party** plugin needs a first-run permission grant per account and per
228
+ version before it can run or draft. The grant covers the capabilities the
229
+ version declares (AI providers, network domains, connected accounts, host
230
+ features) and is stored with the version's `capabilitiesHash`. The six
231
+ `@scenerok` first-party plugins are pre-installed and never ask.
232
+
233
+ | MCP result | Meaning | What to do |
234
+ | --- | --- | --- |
235
+ | `error.code: "permission_escalation"` (HTTP 409) | This account has not granted this version's permissions, or the version's `capabilitiesHash` changed since the last grant. `error.details.capabilities[]` lists exactly what is missing and `details.grantUrl` names the run page. | Tell the user which permissions are listed and ask them to open `details.grantUrl`, sign in, and allow them. Then call the same tool again. Do not loop on the retry and do not invent a grant. A `dryRun: true` still works and is free. |
236
+
222
237
  ## Reference
223
238
 
224
239
  - Environments and what they can call: `environments.md`
@@ -0,0 +1,221 @@
1
+ # Recipes
2
+
3
+ Three complete scripts that pass the quality gate in `SKILL.md`. Each uses
4
+ the look system in `look-and-feel.md`. Copy the one that fits, then swap in
5
+ your own assets, copy, and palette. Keep the structure: a picture under every
6
+ beat, motion on everything, and a grade and sound over the whole video.
7
+
8
+ ## Product promo (9:16, 15 s)
9
+
10
+ Four beats: hook, hero, features, then price and CTA. The images are the
11
+ product page's own (Amazon `landingImage`, `og:image`, or JSON-LD `image`;
12
+ see `look-and-feel.md` → Sourcing imagery). Prepare them before you write the
13
+ script. Amazon stills are 500 px squares with baked-in captions, so crop to a
14
+ clean region and make a blurred 9:16 backdrop from the same shot:
15
+
16
+ ```bash
17
+ ffmpeg -i room.jpg -vf "crop=500:185:0:95,scale=1080:-2" room-band.jpg
18
+ ffmpeg -i room.jpg -vf "crop=104:185:140:95,scale=1080:1920,boxblur=40:2,eq=brightness=-0.18" room-bg.jpg
19
+ sips -g pixelWidth -g pixelHeight room-band.jpg # 1080x398: place at 984x363
20
+ ```
21
+
22
+ ```vidscript
23
+ import motion from "@scenerok/basic-animations"
24
+ import eleven from "@elevenlabs/music"
25
+
26
+ # Look: one palette, two fonts, one accent.
27
+ let bg = "#0A0C10"
28
+ let ink = "#F4F6FA"
29
+ let muted = "#A3ADBD"
30
+ let accent = "#3D8BFF"
31
+ let display = "Archivo Black"
32
+ let body = "Inter"
33
+
34
+ # Assets: the product page's own images, cropped and blurred with ffmpeg first.
35
+ input hero = "hero.jpg" # 483x472, product on white
36
+ input roomBand = "room-band.jpg" # 1080x398, crop of a lifestyle shot
37
+ input roomBg = "room-bg.jpg" # 1080x1920, same shot, blurred + darkened
38
+ input closeBand = "closeup-band.jpg" # 1080x368
39
+ input closeBg = "closeup-bg.jpg" # 1080x1920
40
+
41
+ # Slow push-in for every still, so no frame is ever frozen.
42
+ let push = { type: "scale", from: { scale: 1 }, to: { scale: 1.08 }, easing: "linear" }
43
+
44
+ # 1. Hook (0-3 s): lifestyle shot; the headline lands in the first second.
45
+ [0s .. 3s] = roomBg, width: 1080, height: 1920, x: 0, y: 0, animate: push
46
+ [0s .. 3s] = roomBand, width: 984, height: 363, x: 48, y: 820, radius: 36, blend: "normal", animate: [motion.riseIn(0.5s, 80), push, motion.fadeOut(0.3s)]
47
+ [0.2s .. 3s] = text "Vacuums. Mops.\nEmpties itself.", font: display, size: 104, color: ink, line_height: 1.05, x: "50%", y: "27%", max_width: "86%", align: center, shadow_color: "#000000", shadow_blur: 24, animate: [motion.riseIn(0.6s, 50), motion.fadeOut(0.3s)]
48
+
49
+ # 2. Hero (3-7 s): the product on its own white card.
50
+ [3s .. 7s] = solid(bg)
51
+ [3s .. 7s] = hero, width: 900, height: 880, x: 90, y: 300, radius: 48, blend: "normal", animate: [motion.fadeIn(0.4s), push, motion.fadeOut(0.3s)]
52
+ [3.3s .. 7s] = text "ECOVACS", font: body, size: 44, color: accent, letter_spacing: 10, x: "50%", y: "64.5%", align: center, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
53
+ [3.5s .. 7s] = text "DEEBOT T30e OMNI", font: display, size: 72, color: ink, x: "50%", y: "71%", max_width: "88%", align: center, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
54
+
55
+ # 3. Features (7-11 s): a close-up band, then one bullet every 0.6 s.
56
+ [7s .. 11s] = closeBg, width: 1080, height: 1920, x: 0, y: 0, animate: push
57
+ [7s .. 11s] = closeBand, width: 984, height: 335, x: 48, y: 300, radius: 36, blend: "normal", animate: [motion.fadeIn(0.4s), push, motion.fadeOut(0.3s)]
58
+ [7.4s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 868, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
59
+ [7.4s .. 11s] = text "25,000 Pa suction", font: display, size: 64, color: ink, x: 150, y: 880, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
60
+ [8.0s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 1028, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
61
+ [8.0s .. 11s] = text "ZeroTangle 3.0 brush", font: display, size: 64, color: ink, x: 150, y: 1040, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
62
+ [8.6s .. 11s] = solid(accent, width: 24, height: 24, x: 96, y: 1188, radius: 12), animate: [motion.popIn(0.4s), motion.fadeOut(0.3s)]
63
+ [8.6s .. 11s] = text "Self-emptying dock", font: display, size: 64, color: ink, x: 150, y: 1200, max_width: "80%", anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
64
+
65
+ # 4. Price and CTA (11-15 s). The button is a solid with a radius under the label.
66
+ [11s .. 15s] = solid(bg)
67
+ [11s .. 15s] = hero, width: 560, height: 548, x: 260, y: 260, radius: 40, blend: "normal", animate: [motion.popIn(0.5s), motion.float(3s, 10)]
68
+ [11.3s .. 15s] = text "₹44,999", font: display, size: 150, color: ink, x: "50%", y: "50%", align: center, animate: motion.popIn(0.5s)
69
+ [11.6s .. 15s] = text "★ 4.4 · 166 ratings", font: body, size: 48, color: muted, x: "50%", y: "58.5%", align: center, animate: motion.fadeIn(0.4s)
70
+ [12s .. 15s] = solid(accent, width: 660, height: 132, x: 210, y: 1210, radius: 66), animate: motion.popIn(0.5s)
71
+ [12s .. 15s] = text "Shop on Amazon", font: display, size: 56, color: "#FFFFFF", x: "50%", y: 1276, align: center, animate: motion.fadeIn(0.4s)
72
+
73
+ # Grade: warm the whole frame, pull the edges down.
74
+ [0s .. 15s] = filter "cdl", slope: "1.04 1.0 0.96", offset: "0 0 0", power: "1 1 1", saturation: 1.08
75
+ [0s .. 15s] = filter "vignette", intensity: 0.3
76
+
77
+ # Sound: a bed under everything.
78
+ let bed = eleven.music("Bright, modern electronic product-ad bed, confident pulse, clean ending", duration: 15, instrumental: true)
79
+ [0s .. 15s] = audio bed, volume: 0.5, fade_out: 1s
80
+
81
+ output to "promo.mp4", resolution: "1080x1920", fps: 30, background: "#0A0C10"
82
+ ```
83
+
84
+ `validate` warns `too-many-animations` at the bullets: the three bullets
85
+ leave together on one 0.3 s fade, and the linter counts each exit as motion.
86
+ Accept that warning here and fix any other.
87
+
88
+ Variations: replace the hook band with
89
+ `xai.imageToVideo(hero, "slow orbit around the product, …", aspect_ratio: "9:16", duration: 3)`
90
+ for a moving hero shot (`ai-models.md`). For 16:9, use the 16:9 type scale
91
+ and put the product on the right and the copy on the left.
92
+
93
+ ## Release notes / changelog (9:16, 18 s)
94
+
95
+ Four beats: version hook, the one new feature as typed code with a
96
+ highlight, a stat, and an outro. Facts come from the release body
97
+ (`gh api repos/<owner>/<repo>/releases/latest --jq .body`); count the
98
+ `fix`/`feat` lines rather than estimating. The logo is the project's own
99
+ (`https://avatars.githubusercontent.com/u/<id>`, or the README image). The
100
+ background is a brand glow made with ffmpeg:
101
+
102
+ ```bash
103
+ ffmpeg -f lavfi -i "color=c=black:s=1080x1920,format=rgb24" -frames:v 1 \
104
+ -vf "geq=r='11+90*exp(-((X-760)^2+(Y-520)^2)/180000)':g='11+30*exp(-((X-760)^2+(Y-520)^2)/180000)':b='15+4*exp(-((X-760)^2+(Y-520)^2)/180000)'" glow.png
105
+ ```
106
+
107
+ ```vidscript
108
+ import motion from "@scenerok/basic-animations"
109
+ import animations from "@scenerok/animations"
110
+ import eleven from "@elevenlabs/music"
111
+
112
+ let ink = "#F5F5F7"
113
+ let muted = "#9CA3AF"
114
+ let accent = "#FF5B11"
115
+ let display = "Archivo Black"
116
+ let mono = "JetBrains Mono"
117
+
118
+ input glow = "glow.png" # 1080x1920, made with ffmpeg: brand-colour glow on near-black
119
+ input flame = "flame.png" # 460x460 RGBA, the project's logo
120
+
121
+ # Ambient layer: the background drifts for the whole video.
122
+ [0s .. 18s] = glow, width: 1080, height: 1920, x: 0, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.15 }, easing: "linear" }
123
+
124
+ # 1. Hook (0-3.5 s)
125
+ [0s .. 3.5s] = flame, width: 460, height: 460, x: 310, y: 340, blend: "normal", animate: [motion.popIn(0.6s), motion.float(2.9s, 14), motion.fadeOut(0.3s)]
126
+ [0.3s .. 3.5s] = text "RELEASE NOTES", font: mono, size: 44, color: accent, letter_spacing: 8, x: "50%", y: "50%", align: center, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
127
+ [0.5s .. 3.5s] = text "hono v4.13.14", font: display, size: 120, color: ink, x: "50%", y: "57%", max_width: "88%", align: center, animate: [motion.riseIn(0.6s, 60), motion.fadeOut(0.3s)]
128
+
129
+ # 2. The new feature as code (3.5-10 s): headline, card, then the code types itself.
130
+ [3.5s .. 10s] = text "NEW IN BEARER-AUTH", font: mono, size: 44, color: accent, letter_spacing: 6, x: 96, y: 300, anchor: "left", align: left, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
131
+ [3.6s .. 10s] = text "Error messages can\nreturn a Response", font: display, size: 84, color: ink, line_height: 1.1, x: 96, y: 440, anchor: "left", align: left, animate: [motion.riseIn(0.5s, 40), motion.fadeOut(0.3s)]
132
+ [4s .. 10s] = solid("#16161D", width: 960, height: 700, x: 60, y: 620, radius: 32), animate: [motion.riseIn(0.5s, 60), motion.fadeOut(0.3s)]
133
+ # One text layer per code line: a tall multi-line block can clip. Indent with x, not spaces.
134
+ [4.4s .. 10s] = text "bearerAuth({ token,", font: mono, size: 44, color: ink, x: 108, y: 760, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
135
+ [4.9s .. 10s] = text "invalidToken: {", font: mono, size: 44, color: ink, x: 164, y: 836, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
136
+ [5.4s .. 10s] = text "message: (c) =>", font: mono, size: 44, color: accent, x: 220, y: 912, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
137
+ [5.9s .. 10s] = text "c.json(problem, 401),", font: mono, size: 44, color: accent, x: 276, y: 988, anchor: "left", align: left, animate: [motion.typewriter(0.5s), motion.fadeOut(0.3s)]
138
+ [6.4s .. 10s] = text "}})", font: mono, size: 44, color: ink, x: 108, y: 1064, anchor: "left", align: left, animate: [motion.typewriter(0.3s), motion.fadeOut(0.3s)]
139
+ # Highlight beat: a bar sweeps under the changed lines once the code is typed.
140
+ [7.2s .. 10s] = solid(accent, width: 900, height: 158, x: 90, y: 872, radius: 16, opacity: 0.16), animate: [{ type: "slideX", from: { x: -1000 }, to: { x: 0 }, start: 0, end: 0.5, easing: "easeOutCubic" }, motion.fadeOut(0.3s)]
141
+ [7.4s .. 10s] = text "Any content type, e.g. problem+json", font: mono, size: 44, color: muted, x: 108, y: 1220, anchor: "left", align: left, animate: [motion.fadeIn(0.4s), motion.fadeOut(0.3s)]
142
+
143
+ # 3. Highlight (10-14 s): one number, then three rows. suffix: "" because the block defaults to "+".
144
+ [10s .. 14s] = animations.text_counter_stat("12", "BUG FIXES", suffix: "", accent: accent)
145
+ [11s .. 14s] = text "serve-static: GET and HEAD only", font: mono, size: 44, color: ink, x: "50%", y: "70%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
146
+ [11.4s .. 14s] = text "css: safer url() minifying", font: mono, size: 44, color: ink, x: "50%", y: "74.5%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
147
+ [11.8s .. 14s] = text "trie-router: keep first param", font: mono, size: 44, color: ink, x: "50%", y: "79%", align: center, animate: [motion.riseIn(0.4s, 30), motion.fadeOut(0.3s)]
148
+
149
+ # 4. Outro (14-18 s)
150
+ [14s .. 18s] = flame, width: 320, height: 320, x: 380, y: 520, blend: "normal", animate: [motion.popIn(0.5s), motion.float(3s, 12)]
151
+ [14.3s .. 18s] = solid("#1C1C24", width: 860, height: 120, x: 110, y: 950, radius: 60), animate: motion.popIn(0.5s)
152
+ [14.3s .. 18s] = text "npm i hono@4.13.14", font: mono, size: 48, color: ink, x: "50%", y: 1010, align: center, animate: motion.fadeIn(0.4s)
153
+ [14.8s .. 18s] = text "github.com/honojs/hono", font: mono, size: 44, color: muted, x: "50%", y: 1150, align: center, animate: motion.fadeIn(0.4s)
154
+
155
+ # Grade
156
+ [0s .. 18s] = filter "cdl", slope: "1.03 1.0 0.97", offset: "0 0 0", power: "1 1 1", saturation: 1.05
157
+ [0s .. 18s] = filter "vignette", intensity: 0.35
158
+
159
+ let bed = eleven.music("Minimal driving synth pulse for a developer product update, clean and modern", duration: 18, instrumental: true)
160
+ [0s .. 18s] = audio bed, volume: 0.5, fade_out: 1.5s
161
+
162
+ output to "release.mp4", resolution: "1080x1920", fps: 30, background: "#0B0B0F"
163
+ ```
164
+
165
+ `validate` warns `crowded-text-overlays` and `too-many-animations` on the
166
+ code card, because each code line counts as an overlay. Accept those two
167
+ there; fix any other warning. The `code_*` catalog blocks are 1920x1080
168
+ designs: use them in 16:9, where they fill the frame. In 9:16 they shrink to
169
+ 56 % with empty bands above and below.
170
+
171
+ ## Talking head with captions (9:16, 14 s)
172
+
173
+ A 16:9 interview cut for a vertical feed: fill the frame by oversizing the
174
+ clip (no `Crop`, so transitions stay legal), punch in on the key line, cut
175
+ away to b-roll with a whip pan, and burn in short, timed caption cards.
176
+
177
+ ```vidscript
178
+ import motion from "@scenerok/basic-animations"
179
+ import animations from "@scenerok/animations"
180
+ import tr from "@scenerok/transitions"
181
+
182
+ let ink = "#FFFFFF"
183
+ let accent = "#FFD43B"
184
+ let display = "Archivo Black"
185
+
186
+ input talk = "https://cdn.example.com/interview-16x9.mp4" # 1920x1080, probed
187
+ input broll = "https://cdn.example.com/broll-16x9.mp4" # 1920x1080, probed
188
+ input bed = "https://cdn.example.com/music.mp3"
189
+
190
+ # Fill 9:16 from 16:9: scale to 1920 tall (3413 wide) and centre it, x = (1080 - 3413) / 2.
191
+ # Cut 1: wide, with a slow push. Cut 2: punch in on the key line. Cut 3: b-roll. Cut 4: back to the speaker.
192
+ [0s .. 4s] = talk.Trim(12s, 16s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.05 }, easing: "linear" }
193
+ [4s .. 7s] = talk.Trim(16s, 19s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1.18 }, to: { scale: 1.22 }, easing: "linear" }
194
+ [6.8s .. 7.2s] = tr.whip_pan()
195
+ [7.2s .. 10s] = broll.Trim(0s, 2.8s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1.1 }, to: { scale: 1 }, easing: "linear" }
196
+ [9.8s .. 10.2s] = tr.dissolve()
197
+ [10.2s .. 14s] = talk.Trim(22.2s, 26s), width: 3413, height: 1920, x: -1166, y: 0, animate: { type: "scale", from: { scale: 1 }, to: { scale: 1.05 }, easing: "linear" }
198
+
199
+ # Name card from the catalog (it has a native 9:16 layout).
200
+ [0.6s .. 4s] = animations.lt_kicker_name("FOUNDER", "Asha Rao", accent: accent)
201
+
202
+ # Captions: 2-4 words per card, timed to the transcript, the key words in the accent colour.
203
+ [0.2s .. 1.4s] = text "We shipped it", font: display, size: 72, color: ink, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
204
+ [1.4s .. 2.8s] = text "in nine days", font: display, size: 72, color: accent, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
205
+ [4s .. 5.6s] = text "and it paid", font: display, size: 72, color: ink, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
206
+ [5.6s .. 7s] = text "for itself", font: display, size: 72, color: accent, stroke: "#000000", stroke_width: 6, x: "50%", y: "70%", max_width: "84%", align: center, animate: motion.popIn(0.25s)
207
+
208
+ # Sound: the speaker's own audio, with music ducked under it.
209
+ [0s .. 7s] = audio talk.Trim(12s, 19s)
210
+ [10.2s .. 14s] = audio talk.Trim(22.2s, 26s)
211
+ [0s .. 14s] = audio bed, volume: 0.5, duck_under: talk, fade_out: 1s
212
+
213
+ [0s .. 14s] = filter "cdl", slope: "1.02 1.0 0.98", offset: "0 0 0", power: "1 1 1", saturation: 1.05
214
+
215
+ output to "talk.mp4", resolution: "1080x1920", fps: 30
216
+ ```
217
+
218
+ With a transcript in an SRT file, `captions("talk.srt", font: display, size: 72, area: "caption")`
219
+ replaces the caption lines (`editing-actions.md` → Text and titles). On a
220
+ hosted render the file must be an HTTPS URL; see `look-and-feel.md` → Files the
221
+ server must read.
@@ -21,7 +21,7 @@ A `filter` line grades everything on screen during its window. Add `target: clip
21
21
  | `saturation` | Color intensity |
22
22
  | `brightness` | Exposure lift/drop |
23
23
 
24
- Use filters as optional finishing tools — do not add by default.
24
+ Give every finished video one grade across its whole length: `cdl` for warmth or coolness plus `vignette` around 0.3 (see `look-and-feel.md`). Effect filters (`glitch`, `chromatic`, `blur`) are optional accents; don't stack them by default.
25
25
 
26
26
  ## VFX filters
27
27
 
@@ -2,6 +2,11 @@
2
2
 
3
3
  `import motion from "@scenerok/basic-animations"` — use with `animate:` on `text` blocks.
4
4
 
5
+ Fonts: `font:` is one Google Fonts family at weight 400. Names with a weight such as
6
+ `"Inter Black"` or `"Inter Bold"` (in some catalog examples below) are not families and fall
7
+ back to a default face in renders; use a heavy family such as `"Archivo Black"` instead.
8
+ Type scale, palette and motion timing: `look-and-feel.md`.
9
+
5
10
  ## Motion primitives
6
11
 
7
12
  | Primitive | Suits | Phase |
@@ -11,8 +11,8 @@ Contents: Agent quick start · Program structure · Statements (input, output, l
11
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.*`.
12
12
  2. **Validate early** — `scenerok validate file.vid` (errors show as `Line N:C — message`).
13
13
  3. **Music package syntax** — `import eleven from "@elevenlabs/music"`, then call `eleven.music(...)`, `eleven.generateMusic(...)`, or `eleven.composeMusic(...)`.
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`).
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.
14
+ 4. **Start from a recipe** — `recipes.md` (product promo, release notes, talking head) and the look system in `look-and-feel.md`. A finished video passes the quality gate in `SKILL.md`: a picture under every beat, motion on everything, one grade, sound.
15
+ 5. **A URL means its pictures go in the video** — when the user gives a website or product URL, use the page's own product images, logo and focused screenshots as `input` assets (`look-and-feel.md` → Sourcing imagery). Don't use a full-page screenshot as a background.
16
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`.
17
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.
18
18
  8. **Use the full media plugin toolkit** — choose still generation, text-to-video, image-to-video, reference-to-video, video extension, TTS, and music deliberately. Do not default to only `xai.imagine`.
@@ -681,7 +681,7 @@ import motion from "@scenerok/basic-animations"
681
681
  [0s .. 5s] = filter "glitch", intensity: 0.5, animate: motion.fadeIn(1s)
682
682
  ```
683
683
 
684
- Filters are optional finishing tools; use them only when they improve the composition. Parameter names per filter: `editing-actions.md` → Color and finishing.
684
+ Grade the whole video once (`cdl`, `vignette`, or a `lut`); effect filters are optional accents. Parameter names per filter: `editing-actions.md` → Color and finishing.
685
685
 
686
686
  ### Animation functions (`@scenerok/basic-animations`)
687
687
 
@@ -15,9 +15,9 @@ Without the owning import, calls like `xai.imagine`, `cf.video`, `xai.tts`, `ele
15
15
 
16
16
  **Never** call package functions without importing the package alias first.
17
17
 
18
- ## Rule 2: Use URL assets selectively
18
+ ## Rule 2: A URL means its pictures go in the video
19
19
 
20
- A product or landing-page URL is optional source material. Capture focused, legible pieces (above-the-fold views, product shots, logos), never full-page screenshots as backgrounds, and don't invent claims. The full URL workflow is in `workflow.md`.
20
+ When the user gives a product or landing-page URL, the video uses that page's own pictures: product shots, logos, focused screenshots. Reading only the title and price is not enough. Take focused, legible pieces, never a full-page screenshot as a background, and don't invent claims. How to collect and prepare them: `look-and-feel.md` → Sourcing imagery.
21
21
 
22
22
  ## Rule 2.5: Probe real asset type and dimensions before placement
23
23
 
@@ -211,7 +211,7 @@ let bed = eleven.music("Soft premium ad music", duration: 15, instrumental: true
211
211
  [0s .. 15s] = filter "saturation", amount: 1.12
212
212
  ```
213
213
 
214
- Filters are optional finishing tools, not required ingredients. Use them only when they improve the visual result. Each filter has its own parameter name (`saturation` takes `amount`, `blur` takes `radius`); a wrong name is ignored without an error. The table is in `editing-actions.md` → Color and finishing.
214
+ Every finished video carries one grade over the whole timeline (`cdl` and/or `vignette`, or a `lut` from an HTTPS URL): see the quality gate in `SKILL.md`. Effect filters (`glitch`, `chromatic`, `blur`) stay optional. Each filter has its own parameter name (`saturation` takes `amount`, `blur` takes `radius`); a wrong name is ignored without an error. The table is in `editing-actions.md` → Color and finishing.
215
215
 
216
216
  A `filter` line grades everything in its window. To grade one clip, add `target: clip` or write `clip.Filter("sepia", intensity: 0.6)`.
217
217
 
@@ -2,35 +2,29 @@
2
2
 
3
3
  ## Pipeline
4
4
 
5
- 1. **Understand the goal** — promo, testimonial, meme, code demo, etc.
5
+ 1. **Understand the goal** — promo, testimonial, meme, code demo, etc. Pick the closest script in `recipes.md`
6
6
  2. **Choose authoring** — VidScript (`.vid`) by default and for every editing action; TypeScript `@scenerok/sdk` (`.ts`, `sdk-guide.md`) only for loops or computed timing
7
- 3. **Plan the structure** — time blocks, durations, inputs
8
- 4. **Gather assets** — URLs, local paths, generated clips, selective website screenshots
9
- 5. **Probe asset type and dimensions** — `loadVideo` / media stdlib, or `file` / `ffprobe` / `sips` / `identify`; never guess
10
- 6. **Use images directly when useful** — VidScript `video image_input`; SDK `video(path)` / `loadVideo`
7
+ 3. **Plan the structure** — beats of 2.5–4.5 s, each with a picture, copy, and motion (`look-and-feel.md`)
8
+ 4. **Gather pictures** — page images, focused screenshots, logo, footage, screen recordings; generated stills or clips when nothing real exists. A video with no picture is not done
9
+ 5. **Probe and prepare** — `file` / `ffprobe` / `sips` / `identify` (or `loadVideo` / media stdlib); crop and blur stills with ffmpeg; never guess sizes
10
+ 6. **Decide the look** — palette, two fonts, and type sizes as `let` values (`look-and-feel.md`)
11
11
  7. **Choose plugin modality** — still / t2v / i2v / ref2v / extend / TTS / music (`ai-models.md`, `tts.md`, `music.md`)
12
12
  8. **Compose** — measured dimensions, `area:` / `t.grid` for text, symbolic durations (`vo.duration` / returned node `.duration`), entry/exit beats
13
13
  9. **Validate** — `scenerok validate script.vid` or `scenerok validate scene.ts`
14
14
  10. **Deep-validate (VidScript)** — for scripts with TTS/generated media: `scenerok validate script.vid --generate-assets --report-json` (billed; assets cache for the render). SDK plugins generate during `scenerok render` instead. Fix `offCanvas`/`wasShrunk` text, `collisions`, and unintended `audio.overlaps`, then re-validate.
15
15
  11. **Render** — `scenerok render script.vid --watch` or `scenerok render scene.ts --watch`
16
- 12. **Check the render** — `scenerok lint-video <render-id> --cost medium --json` (billed); fix and re-render if it reports problems. `scenerok render script.vid --validate` does render and check in one step
17
- 13. **Deliver** — share download URL when complete
16
+ 12. **Look at the render** — pull one frame per beat with ffmpeg and open them; run the freeze check (both commands are in the quality gate in `SKILL.md`). Fix what you see and render again. `scenerok lint-video <render-id> --cost medium --json` (billed) adds an LLM review of frame grabs
17
+ 13. **Deliver** — share the download URL once the quality gate passes
18
18
 
19
19
  ## Website URL asset workflow
20
20
 
21
- When the user provides a product, company, landing page, or ecommerce URL:
22
-
23
- 1. Visit the URL with a browser tool before composing.
24
- 2. Capture screenshots sparingly — above-fold, product closeups, pricing cards. Avoid full-page backgrounds.
25
- 3. Crop to the visual evidence needed.
26
- 4. Save product images, logos, app screenshots when accessible.
27
- 5. Probe saved asset dimensions before composing.
28
- 6. Use stills as timeline inputs or feed into `xai.imageToVideo` / `xai.referenceToVideo` (VidScript or SDK).
29
- 7. Combine real assets with generated clips, text, and music.
30
- 8. Scale from measured aspect ratio; keep primary visuals in ~80–88% frame width for 9:16.
31
- 9. Animate assets: `motion.popIn` / SDK `popIn`, `fadeIn`, `slideY`.
32
- 10. Generative prompts must require **no text, logos, watermarks, or readable UI** in AI output — add copy with `text`.
33
- 11. Do not invent product claims — extract from site or ask the user.
21
+ When the user gives a product, company, landing-page or store URL, the page's
22
+ own pictures go in the video. Reading only the title, price and bullets is not
23
+ enough. Collect the image URLs and focused screenshots, probe them, crop out
24
+ baked-in captions, and place them as cards, bands over a blurred fill, or
25
+ full-frame covers: `look-and-feel.md` → Pictures and Sourcing imagery. A worked
26
+ example is the product promo in `recipes.md`. Don't invent product claims;
27
+ extract them from the page or ask.
34
28
 
35
29
  ## Best practices
36
30
 
@@ -62,7 +56,9 @@ If render fails: check `scenerok status <id>`, fix missing assets or syntax, re-
62
56
 
63
57
  ## Sample video types
64
58
 
65
- - **Product promo** — hero + headline + CTA
59
+ - **Product promo** — hero image, features, price + CTA (`recipes.md`)
60
+ - **Release notes** — logo, typed code, stat, outro (`recipes.md`)
61
+ - **Talking head** — speaker cut, punch-ins, b-roll, captions (`recipes.md`)
66
62
  - **Social hook** — fast cuts, bold text
67
63
  - **Testimonial** — speaker clip + quote overlay
68
64
  - **Meme remix** — reaction clip + punchlines