@pixerate/editor 0.222.2 → 0.222.3

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": "@pixerate/editor",
3
- "version": "0.222.2",
3
+ "version": "0.222.3",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
package/skills/README.md CHANGED
@@ -50,6 +50,9 @@ request like "clean up this export" finds it.
50
50
  | [`pixerate-paint-watercolour`](pixerate-paint-watercolour/SKILL.md) | Paint (`pixerate-paint-editor`) | Watercolour paper, light to dark with the paper's white kept, wet-in-wet and wet-on-dry washes, drying before glazing, steering the water. |
51
51
  | [`pixerate-paint-oil-impasto`](pixerate-paint-oil-impasto/SKILL.md) | Paint (`pixerate-paint-editor`) | A toned ground, thin block-in to thick paint, pigment mixing, impasto relief and its light. |
52
52
  | [`pixerate-paint-sketch-to-finish`](pixerate-paint-sketch-to-finish/SKILL.md) | Paint (`pixerate-paint-editor`) | Sketch, ink, flats under the lines, clipped shading and light, one named layer per pass. |
53
+ | [`pixerate-video-social-cut`](pixerate-video-social-cut/SKILL.md) | Video (`pixerate-video-editor`) | Vertical or square reframes that fill the frame, the hook first, platform lengths, titles in the safe area. |
54
+ | [`pixerate-video-colour-grade`](pixerate-video-colour-grade/SKILL.md) | Video (`pixerate-video-editor`) | Correct and match shots, then one look on an adjustment clip; LUTs at moderate strength with skin protected. |
55
+ | [`pixerate-video-montage`](pixerate-video-montage/SKILL.md) | Video (`pixerate-video-editor`) | Selects on the beat, varied pacing, short transitions with handles, music under everything and a clean ending. |
53
56
  | [`pixerate-talking-head-edit`](pixerate-talking-head-edit/SKILL.md) | Video (`pixerate-video-editor`) | Talking-head cleanup by transcript: fillers, long pauses and stutters found in one pass and cut as one plan reviewed before it applies. Owns the dialogue tools. |
54
57
  | [`pixerate-bgm-alignment`](pixerate-bgm-alignment/SKILL.md) | Video (`pixerate-video-editor`) | Background music: beats and cuts on the beat, ducking under speech, loudness in LUFS. Owns the music tools. |
55
58
 
@@ -113,8 +116,8 @@ same change.
113
116
  `packages/core/tests/agent-skill-evals.test.ts`. An eval has a `prompt` (the task, in the
114
117
  user's words), a `fixture` (the starting document: `{ "editor": "spreadsheet", "rows" }`,
115
118
  `{ "editor": "image", "width", "height", "background", "shapes" }` or `{ "editor":
116
- "vector", "width", "height", "setup" }` with commands that draw the starting art, or
117
- `{ "editor": "paint", "width", "height", "setup" }` with tool calls), `checks` (a read-only
119
+ "vector", "width", "height", "setup" }` with commands that draw the starting art,
120
+ `{ "editor": "paint", "width", "height", "setup" }` or `{ "editor": "video", "setup" }` with tool calls), `checks` (a read-only
118
121
  tool, or `run_command` with a read-only command such as `vector.list`, its `args`, what its result must match in `expect`, and `why`) and a `solution` (the
119
122
  tool calls an expert would make). The test proves each eval: some check fails on the fixture,
120
123
  and all pass after the solution. A model harness runs the same file with the model in place
@@ -12,7 +12,7 @@
12
12
  { "tool": "set_brush", "args": { "id": "watercolor-wash" } },
13
13
  { "tool": "set_color", "args": { "color": "#6fa8dc" } },
14
14
  { "tool": "paint_stroke", "args": { "points": [{ "x": 0, "y": 6 }, { "x": 64, "y": 6 }, { "x": 64, "y": 14 }, { "x": 0, "y": 14 }] } },
15
- { "tool": "advance_wet_paint", "args": { "seconds": 30 } },
15
+ { "tool": "advance_wet_paint", "args": { "seconds": 3 } },
16
16
  { "tool": "dry_paint" },
17
17
  { "tool": "get_wet_paint" }
18
18
  ]
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: pixerate-video-colour-grade
3
+ description: The colour grading workflow in the Pixerate video editor - correcting each shot first (exposure, contrast, white balance) so a scene matches, then one look over the whole piece on an adjustment clip, LUTs at a sensible strength with skin tones protected, masks for spot changes, and checking with scopes and frames. Use when asked to colour correct, colour grade, match shots, fix exposure or white balance in a video, add a look, a LUT, a film or cinematic look, warm or cool a video, or protect skin tones, or when update_video_clip grade, add_video_adjustment_clip, set_video_lut or get_video_scopes are available. Load with pixerate-video-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-video-editor
7
+ ---
8
+
9
+ # Grading a video
10
+
11
+ A grade has two passes. **Correction** makes each shot right and makes shots in a scene match:
12
+ exposure, contrast and white balance on the clip's own `grade`. **The look** is one creative
13
+ decision over everything: an adjustment clip above the timeline with its own grade or a LUT,
14
+ so it is changed in one place. Faces are the reference: people notice skin that is wrong before
15
+ anything else.
16
+
17
+ This skill is the method. `pixerate-video-editor` has the full tool reference.
18
+
19
+ ## Goal and quality bar
20
+
21
+ - Shots in a scene match in brightness and colour; cuts between them do not jump.
22
+ - Corrections are moderate: exposure within about ±1 stop, contrast and saturation within
23
+ ±0.3, white balance within ±0.3, unless a shot is badly off.
24
+ - One look, in one place: an adjustment clip over the whole timeline (or one per scene), not
25
+ the same settings pasted on every clip.
26
+ - Skin stays natural under strong looks (skin protection 0.5-0.8 on LUTs).
27
+ - The original corrections are not overwritten by the look.
28
+
29
+ ## Inputs to confirm
30
+
31
+ - **The look:** natural, warm, cool, desaturated, high contrast, a named LUT; or a reference.
32
+ - **Which shot is the reference** for matching (the best-exposed, or the one the user likes).
33
+ - **Where the LUT file is** (a `.cube` path or URL the host can load).
34
+
35
+ ## Working method
36
+
37
+ 1. **Read.** `get_video_project` for clips and their `grade`; where the host has a preview,
38
+ `get_video_scopes` at a frame of each shot reads levels and colour in numbers.
39
+ **Check:** you know which shots are off and by how much.
40
+ 2. **Correct the reference shot** (`update_video_clip` `set.grade`): exposure first, then
41
+ contrast, then white balance (`temperature`, `tint`). The grade object replaces the clip's
42
+ whole grade, so pass every setting you want to keep.
43
+ 3. **Match the others to it.** A shot one stop under gets `exposure: 1`; a cooler shot more
44
+ `temperature`. `copy_video_grade` when shots from one camera setup need the same correction;
45
+ `apply_video_grade_preset` for a common starting point.
46
+ **Check:** the clip `grade` values; scopes or `get_video_frame` at both sides of each cut.
47
+ 4. **The look over everything.** `add_video_track` at the top (`at: 0`) and
48
+ `add_video_adjustment_clip` on it from 0 for the whole duration with the look's `grade`
49
+ (`scope: "underneath"`).
50
+ **Check:** one adjustment clip covering the timeline; media clips keep their corrections.
51
+ 5. **LUTs.** `set_video_lut` with `strength` 0.3-0.7 for a look (1 only for a technical
52
+ conversion LUT) and skin protection (`preserve_skin_tones: 0.7`, 0.5-0.8) when people are on screen;
53
+ `set_video_skin_protection` to change the protection later.
54
+ **Check:** each clip's `grade.lut` has the strength and protection you meant.
55
+ 6. **Spot changes.** `set_video_clip_mask` on a duplicate clip or an adjustment clip for a sky,
56
+ a face or a vignette.
57
+ 7. **Review.** `render_video_frame` with `times` across the piece for a contact sheet; fix any
58
+ shot that stands out.
59
+
60
+ ## Decision table
61
+
62
+ | Problem | Fix |
63
+ | --- | --- |
64
+ | Shot too dark or bright | `grade.exposure` in stops (+1 doubles the light) |
65
+ | Flat, low contrast | `grade.contrast` +0.1-0.3 |
66
+ | Blue or orange cast | `grade.temperature` against it |
67
+ | Green or magenta cast | `grade.tint` against it |
68
+ | Same correction on many shots from one setup | `copy_video_grade` |
69
+ | One look for the whole piece | Adjustment clip on a top track |
70
+ | A LUT look | `set_video_lut`, strength 0.3-0.7 |
71
+ | Faces off under the look | Skin protection 0.5-0.8 (`set_video_lut` or `set_video_skin_protection`), or lower the strength |
72
+ | Change only part of the frame | `set_video_clip_mask` |
73
+
74
+ ## Recipes
75
+
76
+ **Match an underexposed shot.** `update_video_clip` `{ clip, set: { grade: { exposure: 1 } } }`
77
+ for one stop under; check with scopes or frames on both sides of the cut.
78
+
79
+ **Warm, slightly desaturated look.** `add_video_track` `{ kind: "video", at: 0 }` →
80
+ `add_video_adjustment_clip` `{ track, start: 0, duration: <project duration>, grade:
81
+ { temperature: 0.2, saturation: -0.15 } }`.
82
+
83
+ **Tame a LUT.** `set_video_lut` `{ clips, src, strength: 0.6, preserve_skin_tones: 0.7 }`.
84
+
85
+ ## Pitfalls
86
+
87
+ - **Grading before correcting.** A look over unmatched shots makes the mismatch stronger.
88
+ - **Partial grade objects.** `set.grade` replaces the whole grade; read it first and include
89
+ the settings you keep.
90
+ - **The look on every clip.** Pasting it per clip means every change is many edits; use an
91
+ adjustment clip.
92
+ - **Full-strength creative LUTs** turn faces orange or green and crush detail.
93
+ - **ffmpeg and skin protection.** The export's `lut3d` cannot protect skin; the export notes
94
+ it, and the preview differs from the file. Lower the strength when that matters.
95
+ - **Judging on one frame.** Check several frames per shot; light changes within shots.
96
+
97
+ ## Tools used
98
+
99
+ - `get_video_project`, `get_video_scopes`, `get_video_frame`, `render_video_frame`: read grades, measure and look
100
+ - `update_video_clip`: each shot's correction (`set.grade`)
101
+ - `copy_video_grade`, `apply_video_grade_preset`: match and start from a preset
102
+ - `add_video_track`, `add_video_adjustment_clip`: one look over the timeline
103
+ - `set_video_lut`, `remove_video_lut`, `set_video_skin_protection`: LUT looks and skin
104
+ - `set_video_clip_mask`, `remove_video_clip_mask`: spot changes
105
+ - `undo_video`: take back a step
@@ -0,0 +1,16 @@
1
+ {
2
+ "prompt": "Give the whole piece a warm, slightly desaturated look that I can tweak in one place later, on top of the shot corrections.",
3
+ "fixture": { "editor": "video", "setup": [{ "tool": "add_video_asset", "args": { "id": "a", "kind": "video", "src": "shot-a.mp4", "duration": 8000 } },
4
+ { "tool": "add_video_asset", "args": { "id": "b", "kind": "video", "src": "shot-b.mp4", "duration": 8000 } },
5
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "a", "id": "c1", "start": 0, "duration": 6000 } } },
6
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "b", "id": "c2", "start": 6000, "duration": 6000 } } }] },
7
+ "checks": [
8
+ { "why": "one adjustment clip over the whole timeline", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "type": "adjustment", "start": 0, "duration": { "$min": 12000 } } } } } } },
9
+ { "why": "warm and a little desaturated", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "type": "adjustment", "grade": { "temperature": { "$min": 0.05, "$max": 0.5 }, "saturation": { "$min": -0.4, "$max": -0.02 } } } } } } } },
10
+ { "why": "the shots' own grades are left alone", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$expr": "v.filter((c) => c.type === 'media').every((c) => !c.grade)" } } } } }
11
+ ],
12
+ "solution": [
13
+ { "tool": "add_video_track", "args": { "kind": "video", "id": "v0", "at": 0 } },
14
+ { "tool": "add_video_adjustment_clip", "args": { "track": "v0", "start": 0, "duration": 12000, "grade": { "temperature": 0.2, "saturation": -0.15 } } }
15
+ ]
16
+ }
@@ -0,0 +1,132 @@
1
+ {
2
+ "prompt": "The teal-and-orange LUT on both shots is too strong and turns the faces orange. Tone the look down to a moderate strength and keep the skin tones natural.",
3
+ "fixture": {
4
+ "editor": "video",
5
+ "setup": [
6
+ {
7
+ "tool": "add_video_asset",
8
+ "args": {
9
+ "id": "a",
10
+ "kind": "video",
11
+ "src": "shot-a.mp4",
12
+ "duration": 8000
13
+ }
14
+ },
15
+ {
16
+ "tool": "add_video_asset",
17
+ "args": {
18
+ "id": "b",
19
+ "kind": "video",
20
+ "src": "shot-b.mp4",
21
+ "duration": 8000
22
+ }
23
+ },
24
+ {
25
+ "tool": "add_video_clip",
26
+ "args": {
27
+ "track": "v1",
28
+ "clip": {
29
+ "type": "media",
30
+ "asset": "a",
31
+ "id": "c1",
32
+ "start": 0,
33
+ "duration": 6000
34
+ }
35
+ }
36
+ },
37
+ {
38
+ "tool": "add_video_clip",
39
+ "args": {
40
+ "track": "v1",
41
+ "clip": {
42
+ "type": "media",
43
+ "asset": "b",
44
+ "id": "c2",
45
+ "start": 6000,
46
+ "duration": 6000
47
+ }
48
+ }
49
+ },
50
+ {
51
+ "tool": "set_video_lut",
52
+ "args": {
53
+ "clips": [
54
+ "c1",
55
+ "c2"
56
+ ],
57
+ "src": "data:text/plain,TITLE%20%22Teal%20orange%22%0ALUT_3D_SIZE%202%0A0%200.1%200.1%0A1%200.5%200.2%0A0%201%200.9%0A1%201%200.6%0A0%200.2%201%0A1%200.4%200.9%0A0%201%201%0A1%201%201%0A",
58
+ "strength": 1
59
+ }
60
+ }
61
+ ]
62
+ },
63
+ "checks": [
64
+ {
65
+ "why": "the LUT is still on both shots, at a moderate strength",
66
+ "tool": "get_video_project",
67
+ "expect": {
68
+ "tracks": {
69
+ "$some": {
70
+ "clips": {
71
+ "$length": 2,
72
+ "$every": {
73
+ "grade": {
74
+ "lut": {
75
+ "src": {
76
+ "$match": "^data:"
77
+ },
78
+ "strength": {
79
+ "$min": 0.3,
80
+ "$max": 0.85
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ },
90
+ {
91
+ "why": "skin tones protected",
92
+ "tool": "get_video_project",
93
+ "expect": {
94
+ "tracks": {
95
+ "$some": {
96
+ "clips": {
97
+ "$every": {
98
+ "grade": {
99
+ "lut": {
100
+ "preserveSkinTones": {
101
+ "$min": 0.4
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ ],
112
+ "solution": [
113
+ {
114
+ "tool": "get_video_project"
115
+ },
116
+ {
117
+ "tool": "set_video_lut",
118
+ "args": {
119
+ "clips": [
120
+ "c1",
121
+ "c2"
122
+ ],
123
+ "src": {
124
+ "$read": "get_video_project",
125
+ "path": "tracks.0.clips.0.grade.lut.src"
126
+ },
127
+ "strength": 0.6,
128
+ "preserve_skin_tones": 0.7
129
+ }
130
+ }
131
+ ]
132
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "prompt": "Shot two (c2) was filmed one stop underexposed compared with shot one. Match it to shot one without touching shot one.",
3
+ "fixture": { "editor": "video", "setup": [{ "tool": "add_video_asset", "args": { "id": "a", "kind": "video", "src": "shot-a.mp4", "duration": 8000 } },
4
+ { "tool": "add_video_asset", "args": { "id": "b", "kind": "video", "src": "shot-b.mp4", "duration": 8000 } },
5
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "a", "id": "c1", "start": 0, "duration": 6000 } } },
6
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "b", "id": "c2", "start": 6000, "duration": 6000 } } }] },
7
+ "checks": [
8
+ { "why": "c2 brought up by about one stop", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "id": "c2", "grade": { "exposure": { "$min": 0.8, "$max": 1.2 } } } } } } } },
9
+ { "why": "shot one is untouched", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "id": "c1", "$expr": "!v.grade || Object.values(v.grade).every((x) => x === 0)" } } } } } }
10
+ ],
11
+ "solution": [
12
+ { "tool": "update_video_clip", "args": { "clip": "c2", "set": { "grade": { "exposure": 1 } } } }
13
+ ]
14
+ }
@@ -33,6 +33,9 @@ the pitfalls, and uses the tools below.
33
33
 
34
34
  - `pixerate-talking-head-edit`: dialogue cleanup by transcript: fillers, pauses, stutters.
35
35
  - `pixerate-bgm-alignment`: cutting to the beat, ducking music under speech, loudness.
36
+ - `pixerate-video-social-cut`: vertical reframes, the hook first, platform lengths, titles in the safe area.
37
+ - `pixerate-video-colour-grade`: correct and match shots, one look on an adjustment clip, LUTs and skin.
38
+ - `pixerate-video-montage`: selects on the beat, pacing, transitions, music and the ending.
36
39
 
37
40
  ## Working method
38
41
 
@@ -0,0 +1,99 @@
1
+ ---
2
+ name: pixerate-video-montage
3
+ description: The montage workflow in the Pixerate video editor - choosing selects and their best moments, assembling them on beats or a rhythm, varying shot length for pacing, cutting on action, short transitions where they help, music under the whole sequence, and a clean start and end. Use when asked to make a montage, a highlight reel, a sizzle reel, a recap, a music video edit, a slideshow of clips or photos, or to cut a set of clips to music or a rhythm, or when edit_video, add_video_marker, set_video_transition or find_video_gaps are available. Load with pixerate-video-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-video-editor
7
+ ---
8
+
9
+ # Cutting a montage
10
+
11
+ A montage tells its story through rhythm: the best moment of each shot, in an order that
12
+ builds, cut to a beat. The work is choosing (selects and their best seconds), then placing
13
+ (on the beat, with shot lengths that breathe and quicken), then finishing (transitions only
14
+ where they help, music under everything, a clear ending).
15
+
16
+ This skill is the method. `pixerate-video-editor` has the full tool reference; for beats in a
17
+ music track and cutting to them, see `pixerate-bgm-alignment`.
18
+
19
+ ## Goal and quality bar
20
+
21
+ - Each shot is its best moment: taken from the middle of the action, not its first frames
22
+ (`in` past the camera's settle).
23
+ - Cuts land on beats or on action, on frame boundaries, with no gaps (`find_video_gaps` empty).
24
+ - Pacing varies: longer shots to establish, shorter to build, a held shot to land.
25
+ - Straight cuts by default; short crossfades (300-600 ms) or dips only for a reason (time
26
+ passing, a change of section), each with enough media on both sides (no handle errors).
27
+ - Music runs under the whole sequence and ends with it (a fade out or a musical ending), and
28
+ the picture ends cleanly (a fade to black or a held final frame).
29
+
30
+ ## Inputs to confirm
31
+
32
+ - **Selects and order:** which clips, in what order (story, chronology, energy).
33
+ - **Rhythm:** music with beats, markers, or a shot length (e.g. 1 s per shot).
34
+ - **Length** of the finished piece.
35
+
36
+ ## Working method
37
+
38
+ 1. **Read.** `get_video_project`: assets (with durations), markers (beats), music.
39
+ With music, `find_video_beats` (`pixerate-bgm-alignment`) marks the beats.
40
+ **Check:** you have a beat grid or a shot length, and each select's usable range.
41
+ 2. **Pick the moments.** For each select, the `in` point of its best stretch (skip the first
42
+ second or two, where the camera settles).
43
+ 3. **Assemble in one batch.** `edit_video` with one add-clip op per shot (`asset`, `id`, `in`,
44
+ `start` on a beat, `duration` to the next beat), `dry_run: true` first.
45
+ **Check:** the clips are in order, each `start` on a beat; `find_video_gaps` is empty.
46
+ 4. **Refine the rhythm.** `roll_video_edit` to move a cut to a beat or onto action without
47
+ shifting anything else; `slip_video_clip` to change what a shot shows without moving it.
48
+ Vary lengths: build up with shorter shots before the peak.
49
+ 5. **Transitions, sparingly.** `set_video_transition` `crossfade` 300-600 ms (or `dip` for a
50
+ section change); every type but dip needs half its length in media past the cut on both sides.
51
+ **Check:** `check_video_project` has no errors.
52
+ 6. **Music and ending.** The music clip on an audio track from 0 for the whole duration, with a
53
+ `fadeOut` of 1-3 s (or cut on its last beat); the last picture clip `fadeOut` 300-1000 ms to
54
+ black.
55
+ **Check:** `get_video_project` shows both, ending together.
56
+ 7. **Review.** `render_video_frame` with `times` at each cut for a contact sheet; watch the
57
+ rhythm in the preview where available.
58
+
59
+ ## Decision table
60
+
61
+ | Want | Approach |
62
+ | --- | --- |
63
+ | Energy, a build | Shorter shots toward the peak, cuts on every beat |
64
+ | Calm, reflective | Longer shots, cuts every 2-4 beats, crossfades |
65
+ | Time passing | Crossfade or dip between shots |
66
+ | A section change | Dip to black or a held shot, then a new rhythm |
67
+ | Continuity of motion | Cut on action (`roll_video_edit` onto the movement) |
68
+ | Photos in a slideshow | Image assets, a slow push-in (`set_video_keyframes` scale 1 to 1.08) |
69
+
70
+ ## Recipes
71
+
72
+ **On the beat.** Beats every `b` ms from 0: `edit_video` with `{ op: "add_clip", track: "v1", clip:
73
+ { type: "media", asset, id, in, start: k * b, duration: b } }` for each shot `k`; then `find_video_gaps`.
74
+
75
+ **Crossfades.** `edit_video` with `{ op: "set_transition", clip, transition: { type: "crossfade",
76
+ duration: 400 } }` for every clip after the first; then `check_video_project`.
77
+
78
+ **Music and fade-out.** `add_video_clip` on the audio track `{ type: "media", asset: music,
79
+ start: 0, duration: <sequence>, fadeOut: 2000 }`; `update_video_clip` on the last shot
80
+ `{ set: { fadeOut: 500 } }`.
81
+
82
+ ## Pitfalls
83
+
84
+ - **First frames of a clip** are often the camera settling; take `in` later.
85
+ - **Transitions everywhere** soften the rhythm and look dated; cut straight by default.
86
+ - **Missing handles.** A transition needs media past the cut on both sides; trimmed-to-the-edge
87
+ clips fail `check_video_project`.
88
+ - **Music that stops dead** at the end of the picture; fade it or end on a musical phrase.
89
+ - **Uniform shot lengths** for a long montage feel mechanical; vary them.
90
+
91
+ ## Tools used
92
+
93
+ - `get_video_project`, `find_video_gaps`, `check_video_project`, `render_video_frame`: read and check
94
+ - `add_video_marker`: beats and sections
95
+ - `edit_video`, `add_video_clip`: assemble in one batch
96
+ - `roll_video_edit`, `slip_video_clip`, `trim_video_clip`, `close_video_gaps`: refine the rhythm
97
+ - `set_video_transition`: crossfades and dips
98
+ - `update_video_clip`, `set_video_keyframes`: fades, volume and push-ins
99
+ - `undo_video`: take back a step
@@ -0,0 +1,231 @@
1
+ {
2
+ "prompt": "Assemble the five selects s1 to s5 in order on V1, one shot per beat: each cut lands on the next beat marker (they are 1 s apart, from 0). Use the middle of each select (from 2 s in). No gaps.",
3
+ "fixture": {
4
+ "editor": "video",
5
+ "setup": [
6
+ {
7
+ "tool": "add_video_asset",
8
+ "args": {
9
+ "id": "s1",
10
+ "kind": "video",
11
+ "src": "select-1.mp4",
12
+ "duration": 6000
13
+ }
14
+ },
15
+ {
16
+ "tool": "add_video_asset",
17
+ "args": {
18
+ "id": "s2",
19
+ "kind": "video",
20
+ "src": "select-2.mp4",
21
+ "duration": 6000
22
+ }
23
+ },
24
+ {
25
+ "tool": "add_video_asset",
26
+ "args": {
27
+ "id": "s3",
28
+ "kind": "video",
29
+ "src": "select-3.mp4",
30
+ "duration": 6000
31
+ }
32
+ },
33
+ {
34
+ "tool": "add_video_asset",
35
+ "args": {
36
+ "id": "s4",
37
+ "kind": "video",
38
+ "src": "select-4.mp4",
39
+ "duration": 6000
40
+ }
41
+ },
42
+ {
43
+ "tool": "add_video_asset",
44
+ "args": {
45
+ "id": "s5",
46
+ "kind": "video",
47
+ "src": "select-5.mp4",
48
+ "duration": 6000
49
+ }
50
+ },
51
+ {
52
+ "tool": "add_video_marker",
53
+ "args": {
54
+ "time": 0,
55
+ "label": "beat"
56
+ }
57
+ },
58
+ {
59
+ "tool": "add_video_marker",
60
+ "args": {
61
+ "time": 1000,
62
+ "label": "beat"
63
+ }
64
+ },
65
+ {
66
+ "tool": "add_video_marker",
67
+ "args": {
68
+ "time": 2000,
69
+ "label": "beat"
70
+ }
71
+ },
72
+ {
73
+ "tool": "add_video_marker",
74
+ "args": {
75
+ "time": 3000,
76
+ "label": "beat"
77
+ }
78
+ },
79
+ {
80
+ "tool": "add_video_marker",
81
+ "args": {
82
+ "time": 4000,
83
+ "label": "beat"
84
+ }
85
+ },
86
+ {
87
+ "tool": "add_video_marker",
88
+ "args": {
89
+ "time": 5000,
90
+ "label": "beat"
91
+ }
92
+ }
93
+ ]
94
+ },
95
+ "checks": [
96
+ {
97
+ "why": "five shots in order on V1",
98
+ "tool": "get_video_project",
99
+ "expect": {
100
+ "tracks": {
101
+ "$some": {
102
+ "id": "v1",
103
+ "clips": {
104
+ "$length": 5,
105
+ "$expr": "v.every((c, i) => c.asset === 's' + (i + 1))"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ },
111
+ {
112
+ "why": "every cut on a beat",
113
+ "tool": "get_video_project",
114
+ "expect": {
115
+ "tracks": {
116
+ "$some": {
117
+ "id": "v1",
118
+ "clips": {
119
+ "$every": {
120
+ "$expr": "v.start % 1000 === 0 && v.duration === 1000"
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ },
127
+ {
128
+ "why": "from the middle of each select",
129
+ "tool": "get_video_project",
130
+ "expect": {
131
+ "tracks": {
132
+ "$some": {
133
+ "id": "v1",
134
+ "clips": {
135
+ "$every": {
136
+ "in": {
137
+ "$min": 1500,
138
+ "$max": 3000
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ },
146
+ {
147
+ "why": "no gaps",
148
+ "tool": "find_video_gaps",
149
+ "expect": {
150
+ "gaps": {
151
+ "$length": 0
152
+ }
153
+ }
154
+ }
155
+ ],
156
+ "solution": [
157
+ {
158
+ "tool": "get_video_project"
159
+ },
160
+ {
161
+ "tool": "edit_video",
162
+ "args": {
163
+ "ops": [
164
+ {
165
+ "op": "add_clip",
166
+ "track": "v1",
167
+ "clip": {
168
+ "type": "media",
169
+ "asset": "s1",
170
+ "id": "c1",
171
+ "in": 2000,
172
+ "start": 0,
173
+ "duration": 1000
174
+ }
175
+ },
176
+ {
177
+ "op": "add_clip",
178
+ "track": "v1",
179
+ "clip": {
180
+ "type": "media",
181
+ "asset": "s2",
182
+ "id": "c2",
183
+ "in": 2000,
184
+ "start": 1000,
185
+ "duration": 1000
186
+ }
187
+ },
188
+ {
189
+ "op": "add_clip",
190
+ "track": "v1",
191
+ "clip": {
192
+ "type": "media",
193
+ "asset": "s3",
194
+ "id": "c3",
195
+ "in": 2000,
196
+ "start": 2000,
197
+ "duration": 1000
198
+ }
199
+ },
200
+ {
201
+ "op": "add_clip",
202
+ "track": "v1",
203
+ "clip": {
204
+ "type": "media",
205
+ "asset": "s4",
206
+ "id": "c4",
207
+ "in": 2000,
208
+ "start": 3000,
209
+ "duration": 1000
210
+ }
211
+ },
212
+ {
213
+ "op": "add_clip",
214
+ "track": "v1",
215
+ "clip": {
216
+ "type": "media",
217
+ "asset": "s5",
218
+ "id": "c5",
219
+ "in": 2000,
220
+ "start": 4000,
221
+ "duration": 1000
222
+ }
223
+ }
224
+ ]
225
+ }
226
+ },
227
+ {
228
+ "tool": "find_video_gaps"
229
+ }
230
+ ]
231
+ }
@@ -0,0 +1,210 @@
1
+ {
2
+ "prompt": "Lay the music under the whole montage, fade the music out over the last two seconds, and end the picture with a fade to black.",
3
+ "fixture": {
4
+ "editor": "video",
5
+ "setup": [
6
+ {
7
+ "tool": "add_video_asset",
8
+ "args": {
9
+ "id": "s1",
10
+ "kind": "video",
11
+ "src": "select-1.mp4",
12
+ "duration": 6000
13
+ }
14
+ },
15
+ {
16
+ "tool": "add_video_asset",
17
+ "args": {
18
+ "id": "s2",
19
+ "kind": "video",
20
+ "src": "select-2.mp4",
21
+ "duration": 6000
22
+ }
23
+ },
24
+ {
25
+ "tool": "add_video_asset",
26
+ "args": {
27
+ "id": "s3",
28
+ "kind": "video",
29
+ "src": "select-3.mp4",
30
+ "duration": 6000
31
+ }
32
+ },
33
+ {
34
+ "tool": "add_video_asset",
35
+ "args": {
36
+ "id": "s4",
37
+ "kind": "video",
38
+ "src": "select-4.mp4",
39
+ "duration": 6000
40
+ }
41
+ },
42
+ {
43
+ "tool": "add_video_asset",
44
+ "args": {
45
+ "id": "s5",
46
+ "kind": "video",
47
+ "src": "select-5.mp4",
48
+ "duration": 6000
49
+ }
50
+ },
51
+ {
52
+ "tool": "add_video_clip",
53
+ "args": {
54
+ "track": "v1",
55
+ "clip": {
56
+ "type": "media",
57
+ "asset": "s1",
58
+ "id": "c1",
59
+ "in": 2000,
60
+ "start": 0,
61
+ "duration": 1000
62
+ }
63
+ }
64
+ },
65
+ {
66
+ "tool": "add_video_clip",
67
+ "args": {
68
+ "track": "v1",
69
+ "clip": {
70
+ "type": "media",
71
+ "asset": "s2",
72
+ "id": "c2",
73
+ "in": 2000,
74
+ "start": 1000,
75
+ "duration": 1000
76
+ }
77
+ }
78
+ },
79
+ {
80
+ "tool": "add_video_clip",
81
+ "args": {
82
+ "track": "v1",
83
+ "clip": {
84
+ "type": "media",
85
+ "asset": "s3",
86
+ "id": "c3",
87
+ "in": 2000,
88
+ "start": 2000,
89
+ "duration": 1000
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "tool": "add_video_clip",
95
+ "args": {
96
+ "track": "v1",
97
+ "clip": {
98
+ "type": "media",
99
+ "asset": "s4",
100
+ "id": "c4",
101
+ "in": 2000,
102
+ "start": 3000,
103
+ "duration": 1000
104
+ }
105
+ }
106
+ },
107
+ {
108
+ "tool": "add_video_clip",
109
+ "args": {
110
+ "track": "v1",
111
+ "clip": {
112
+ "type": "media",
113
+ "asset": "s5",
114
+ "id": "c5",
115
+ "in": 2000,
116
+ "start": 4000,
117
+ "duration": 1000
118
+ }
119
+ }
120
+ },
121
+ {
122
+ "tool": "add_video_asset",
123
+ "args": {
124
+ "id": "music",
125
+ "kind": "audio",
126
+ "src": "music.mp3",
127
+ "duration": 60000
128
+ }
129
+ }
130
+ ]
131
+ },
132
+ "checks": [
133
+ {
134
+ "why": "music under the whole montage, ending with it",
135
+ "tool": "get_video_project",
136
+ "expect": {
137
+ "tracks": {
138
+ "$some": {
139
+ "kind": "audio",
140
+ "clips": {
141
+ "$some": {
142
+ "asset": "music",
143
+ "start": 0,
144
+ "duration": 5000
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ },
151
+ {
152
+ "why": "the music fades out over about two seconds",
153
+ "tool": "get_video_project",
154
+ "expect": {
155
+ "tracks": {
156
+ "$some": {
157
+ "kind": "audio",
158
+ "clips": {
159
+ "$some": {
160
+ "asset": "music",
161
+ "fadeOut": {
162
+ "$min": 1500,
163
+ "$max": 2500
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+ }
170
+ },
171
+ {
172
+ "why": "the last shot fades to black",
173
+ "tool": "get_video_project",
174
+ "expect": {
175
+ "tracks": {
176
+ "$some": {
177
+ "id": "v1",
178
+ "clips": {
179
+ "$expr": "v[v.length - 1].fadeOut >= 300"
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+ ],
186
+ "solution": [
187
+ {
188
+ "tool": "add_video_clip",
189
+ "args": {
190
+ "track": "a1",
191
+ "clip": {
192
+ "type": "media",
193
+ "asset": "music",
194
+ "start": 0,
195
+ "duration": 5000,
196
+ "fadeOut": 2000
197
+ }
198
+ }
199
+ },
200
+ {
201
+ "tool": "update_video_clip",
202
+ "args": {
203
+ "clip": "c5",
204
+ "set": {
205
+ "fadeOut": 500
206
+ }
207
+ }
208
+ }
209
+ ]
210
+ }
@@ -0,0 +1,190 @@
1
+ {
2
+ "prompt": "Join the shots of this montage with short crossfades of about 400 ms, and make sure every transition has enough media for it.",
3
+ "fixture": {
4
+ "editor": "video",
5
+ "setup": [
6
+ {
7
+ "tool": "add_video_asset",
8
+ "args": {
9
+ "id": "s1",
10
+ "kind": "video",
11
+ "src": "select-1.mp4",
12
+ "duration": 6000
13
+ }
14
+ },
15
+ {
16
+ "tool": "add_video_asset",
17
+ "args": {
18
+ "id": "s2",
19
+ "kind": "video",
20
+ "src": "select-2.mp4",
21
+ "duration": 6000
22
+ }
23
+ },
24
+ {
25
+ "tool": "add_video_asset",
26
+ "args": {
27
+ "id": "s3",
28
+ "kind": "video",
29
+ "src": "select-3.mp4",
30
+ "duration": 6000
31
+ }
32
+ },
33
+ {
34
+ "tool": "add_video_asset",
35
+ "args": {
36
+ "id": "s4",
37
+ "kind": "video",
38
+ "src": "select-4.mp4",
39
+ "duration": 6000
40
+ }
41
+ },
42
+ {
43
+ "tool": "add_video_asset",
44
+ "args": {
45
+ "id": "s5",
46
+ "kind": "video",
47
+ "src": "select-5.mp4",
48
+ "duration": 6000
49
+ }
50
+ },
51
+ {
52
+ "tool": "add_video_clip",
53
+ "args": {
54
+ "track": "v1",
55
+ "clip": {
56
+ "type": "media",
57
+ "asset": "s1",
58
+ "id": "c1",
59
+ "in": 2000,
60
+ "start": 0,
61
+ "duration": 1000
62
+ }
63
+ }
64
+ },
65
+ {
66
+ "tool": "add_video_clip",
67
+ "args": {
68
+ "track": "v1",
69
+ "clip": {
70
+ "type": "media",
71
+ "asset": "s2",
72
+ "id": "c2",
73
+ "in": 2000,
74
+ "start": 1000,
75
+ "duration": 1000
76
+ }
77
+ }
78
+ },
79
+ {
80
+ "tool": "add_video_clip",
81
+ "args": {
82
+ "track": "v1",
83
+ "clip": {
84
+ "type": "media",
85
+ "asset": "s3",
86
+ "id": "c3",
87
+ "in": 2000,
88
+ "start": 2000,
89
+ "duration": 1000
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "tool": "add_video_clip",
95
+ "args": {
96
+ "track": "v1",
97
+ "clip": {
98
+ "type": "media",
99
+ "asset": "s4",
100
+ "id": "c4",
101
+ "in": 2000,
102
+ "start": 3000,
103
+ "duration": 1000
104
+ }
105
+ }
106
+ },
107
+ {
108
+ "tool": "add_video_clip",
109
+ "args": {
110
+ "track": "v1",
111
+ "clip": {
112
+ "type": "media",
113
+ "asset": "s5",
114
+ "id": "c5",
115
+ "in": 2000,
116
+ "start": 4000,
117
+ "duration": 1000
118
+ }
119
+ }
120
+ }
121
+ ]
122
+ },
123
+ "checks": [
124
+ {
125
+ "why": "a short crossfade into every shot after the first",
126
+ "tool": "get_video_project",
127
+ "expect": {
128
+ "tracks": {
129
+ "$some": {
130
+ "id": "v1",
131
+ "clips": {
132
+ "$expr": "v.slice(1).every((c) => c.transition && c.transition.type === 'crossfade' && c.transition.duration >= 300 && c.transition.duration <= 600)"
133
+ }
134
+ }
135
+ }
136
+ }
137
+ },
138
+ {
139
+ "why": "no errors (every transition has handles)",
140
+ "tool": "check_video_project",
141
+ "expect": {
142
+ "errors": 0
143
+ }
144
+ }
145
+ ],
146
+ "solution": [
147
+ {
148
+ "tool": "edit_video",
149
+ "args": {
150
+ "ops": [
151
+ {
152
+ "op": "set_transition",
153
+ "clip": "c2",
154
+ "transition": {
155
+ "type": "crossfade",
156
+ "duration": 400
157
+ }
158
+ },
159
+ {
160
+ "op": "set_transition",
161
+ "clip": "c3",
162
+ "transition": {
163
+ "type": "crossfade",
164
+ "duration": 400
165
+ }
166
+ },
167
+ {
168
+ "op": "set_transition",
169
+ "clip": "c4",
170
+ "transition": {
171
+ "type": "crossfade",
172
+ "duration": 400
173
+ }
174
+ },
175
+ {
176
+ "op": "set_transition",
177
+ "clip": "c5",
178
+ "transition": {
179
+ "type": "crossfade",
180
+ "duration": 400
181
+ }
182
+ }
183
+ ]
184
+ }
185
+ },
186
+ {
187
+ "tool": "check_video_project"
188
+ }
189
+ ]
190
+ }
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: pixerate-video-social-cut
3
+ description: The social video workflow in the Pixerate video editor - reframing for vertical (9:16) or square, opening on the hook in the first seconds, cutting to platform length, titles and captions inside the safe area, pacing for the feed, loudness for mobile and an export for Reels, TikTok, Shorts or a feed post. Use when asked to make a social cut, a vertical, Reels, TikTok, Shorts or story version, a teaser or trailer for social, a short clip from a longer video, or to reframe, caption or title a video for social, or when set_video_settings, update_video_clip and add_video_marker are available. Load with pixerate-video-editor, which lists every tool.
4
+ metadata:
5
+ kind: workflow
6
+ reference: pixerate-video-editor
7
+ ---
8
+
9
+ # Cutting for social
10
+
11
+ A social cut is watched on a phone, often without sound, by someone who decides in the first
12
+ second or two whether to stay. So the frame is vertical or square and filled, the strongest
13
+ moment comes first, every shot earns its place, and words are on screen. Build it as its own
14
+ project or sequence from the long edit, never by damaging the original.
15
+
16
+ This skill is the method. `pixerate-video-editor` has the full tool reference; for music and
17
+ loudness see `pixerate-bgm-alignment`.
18
+
19
+ ## Goal and quality bar
20
+
21
+ - The frame suits the platform: 1080 × 1920 (9:16) for Reels, TikTok, Shorts and stories;
22
+ 1080 × 1080 or 1080 × 1350 for a feed post. The picture fills it (no black bars).
23
+ - The hook plays in the first 1-3 seconds: the most striking image or line, not a logo or a
24
+ slow build.
25
+ - The length fits the platform and the content: 7-15 s for a loop or teaser, up to 60 s for
26
+ most short-form, cut tight (no dead air, `find_video_gaps` empty).
27
+ - Text is readable without sound: titles and captions large, high-contrast, inside the safe
28
+ area (away from the top 250 px and bottom 350 px of a 1920-high frame, where the app's
29
+ interface sits).
30
+ - `check_video_project` has no errors; the mix is at the delivery loudness (-14 LUFS).
31
+
32
+ ## Inputs to confirm
33
+
34
+ - **Platform(s)** and so the frame and length.
35
+ - **The hook:** a moment, a line or a marker; ask when nothing in the footage stands out.
36
+ - **Words on screen:** captions of the speech, a title, a call to action.
37
+
38
+ ## Working method
39
+
40
+ 1. **Read.** `get_video_project`: frame size, clips, markers (a "Hook" marker or the user's
41
+ notes), duration. With speech, `get_video_transcript` to find the best lines.
42
+ **Check:** you know where the hook is in the source and how long the cut should be.
43
+ 2. **Set the frame.** `set_video_settings` `width` / `height`; then each clip `fit: "cover"`
44
+ (`update_video_clip`), and `transform` `x` to keep the subject in frame when it is off-centre.
45
+ **Check:** `get_video_project` frame size; `get_video_frame` at a few times shows the subject.
46
+ 3. **Open on the hook.** Trim and move so the first clip's `in` is at the hook (`edit_video`
47
+ with its trim and move ops, or `extract_video_range` for what goes). Then the context, then
48
+ the payoff.
49
+ **Check:** the clip at 0 starts at the hook's source time.
50
+ 4. **Cut to length.** Remove everything that does not move the story: `remove_video_ranges` for
51
+ pauses and fillers (`pixerate-talking-head-edit`), `extract_video_range` for whole passages.
52
+ Keep shots short (1-3 s for montage, as long as the line for speech).
53
+ **Check:** `get_video_project` `duration` is in range; `find_video_gaps` is empty.
54
+ 5. **Words.** A video track above the footage (`add_video_track` with `at: 0`), then text clips
55
+ (`add_video_clip` `type: "text"`) with `transform.y` in the upper or lower safe area,
56
+ `animateIn` / `animateOut` for motion. Captions follow the transcript's sentences.
57
+ **Check:** text clips are on the top track and inside the safe area.
58
+ 6. **Sound.** Music under, dialogue on top (`pixerate-bgm-alignment`); the first second should
59
+ work without sound and with it.
60
+ 7. **Check and export.** `check_video_project`; `export_video_ffmpeg` with the loudness target.
61
+
62
+ ## Decision table
63
+
64
+ | Platform | Frame | Length |
65
+ | --- | --- | --- |
66
+ | Reels, TikTok, Shorts, stories | 1080 × 1920 | 7-60 s; under 15 s loops |
67
+ | Feed post | 1080 × 1350 or 1080 × 1080 | 15-60 s |
68
+ | Landscape (YouTube, web) | 1920 × 1080 | The content's length |
69
+
70
+ | Source | Reframe |
71
+ | --- | --- |
72
+ | Subject centred | `fit: "cover"` |
73
+ | Subject off-centre | `fit: "cover"` plus `transform.x` toward the subject |
74
+ | Subject moves across the frame | Keyframe `transform.x` (`set_video_keyframes`), or split and reposition per shot |
75
+ | Wide shot that loses everything when cropped | A blurred, scaled copy behind a `contain` copy, or choose another shot |
76
+
77
+ ## Recipes
78
+
79
+ **Vertical from landscape.** `set_video_settings` `{ width: 1080, height: 1920 }` → for each
80
+ picture clip `update_video_clip` `{ set: { fit: "cover" } }` → `get_video_frame` to check.
81
+
82
+ **Hook first.** Source hook at `h` ms: `edit_video` `[{ op: "trim_clip", clip, edge: "start",
83
+ to: h }, { op: "trim_clip", clip, edge: "end", to: h + length }, { op: "move_clip", clip,
84
+ start: 0 }]`.
85
+
86
+ **Opening title.** `add_video_track` `{ kind: "video", at: 0 }` → `add_video_clip` on it with
87
+ `{ type: "text", text, start: 0, duration: 3000, transform: { y: -600 } }`.
88
+
89
+ ## Pitfalls
90
+
91
+ - **Letterboxing.** A landscape clip in a vertical frame with `contain` shows black bars.
92
+ - **Slow openings.** A logo, a fade-in or a wide establishing shot loses the viewer.
93
+ - **Text in the interface zones** is covered by captions, buttons and the username.
94
+ - **Relying on sound.** Most feeds autoplay muted; put the message on screen too.
95
+ - **Editing the master.** Make the social cut separately so the long version stays intact.
96
+
97
+ ## Tools used
98
+
99
+ - `get_video_project`, `get_video_transcript`, `get_video_frame`, `find_video_gaps`, `check_video_project`: read and check
100
+ - `set_video_settings`: the frame size
101
+ - `update_video_clip`, `set_video_keyframes`: fit, reframe and move the picture
102
+ - `edit_video`, `trim_video_clip`, `move_video_clip`, `extract_video_range`, `remove_video_ranges`: cut to the hook and to length
103
+ - `add_video_track`, `add_video_clip`: titles and captions on a track above
104
+ - `add_video_marker`: mark the hook and beats
105
+ - `export_video_ffmpeg`: deliver
@@ -0,0 +1,19 @@
1
+ {
2
+ "prompt": "Cut this interview down to a 15-second clip for social that opens on the moment marked 'Hook' and runs on from there. No gaps.",
3
+ "fixture": { "editor": "video", "setup": [{ "tool": "add_video_asset", "args": { "id": "interview", "kind": "video", "src": "interview.mp4", "duration": 40000, "width": 1920, "height": 1080 } },
4
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "interview", "id": "c1", "start": 0, "duration": 40000 } } }, { "tool": "add_video_marker", "args": { "time": 22000, "label": "Hook" } }] },
5
+ "checks": [
6
+ { "why": "about 15 seconds long", "tool": "get_video_project", "expect": { "duration": { "$min": 12000, "$max": 15500 } } },
7
+ { "why": "the first frame is the hook", "tool": "get_video_project", "expect": { "tracks": { "$some": { "id": "v1", "clips": { "$some": { "start": 0, "in": { "$min": 21500, "$max": 22500 } } } } } } },
8
+ { "why": "no gaps", "tool": "find_video_gaps", "expect": { "gaps": { "$length": 0 } } }
9
+ ],
10
+ "solution": [
11
+ { "tool": "get_video_project" },
12
+ { "tool": "edit_video", "args": { "ops": [
13
+ { "op": "trim_clip", "clip": "c1", "edge": "start", "to": 22000 },
14
+ { "op": "trim_clip", "clip": "c1", "edge": "end", "to": 37000 },
15
+ { "op": "move_clip", "clip": "c1", "start": 0 }
16
+ ] } },
17
+ { "tool": "find_video_gaps" }
18
+ ]
19
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "prompt": "Add a title saying 'New drop Friday' over the first three seconds, in the upper part of the frame, on top of the video.",
3
+ "fixture": { "editor": "video", "setup": [{ "tool": "add_video_asset", "args": { "id": "interview", "kind": "video", "src": "interview.mp4", "duration": 40000, "width": 1920, "height": 1080 } },
4
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "interview", "id": "c1", "start": 0, "duration": 40000 } } }] },
5
+ "checks": [
6
+ { "why": "a text clip with the words, from 0 for about 3 s", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "type": "text", "text": { "$match": ["new drop friday", "i"] }, "start": 0, "duration": { "$min": 2500, "$max": 4000 } } } } } } },
7
+ { "why": "in the upper part of the frame", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "type": "text", "transform": { "y": { "$max": -100 } } } } } } } },
8
+ { "why": "on a video track above the footage (tracks are listed top first)", "tool": "get_video_project", "expect": { "tracks": { "$expr": "(() => { const t = v.findIndex((tr) => tr.clips.some((c) => c.type === 'text')); const m = v.findIndex((tr) => tr.clips.some((c) => c.id === 'c1')); return t >= 0 && t < m; })()" } } }
9
+ ],
10
+ "solution": [
11
+ { "tool": "add_video_track", "args": { "kind": "video", "id": "v2", "at": 0 } },
12
+ { "tool": "add_video_clip", "args": { "track": "v2", "clip": { "type": "text", "text": "New drop Friday", "start": 0, "duration": 3000, "transform": { "y": -600 } } } }
13
+ ]
14
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "prompt": "Make this a vertical video for Reels and TikTok: 1080 by 1920, with the picture filling the whole frame (no black bars).",
3
+ "fixture": { "editor": "video", "setup": [{ "tool": "add_video_asset", "args": { "id": "interview", "kind": "video", "src": "interview.mp4", "duration": 40000, "width": 1920, "height": 1080 } },
4
+ { "tool": "add_video_clip", "args": { "track": "v1", "clip": { "type": "media", "asset": "interview", "id": "c1", "start": 0, "duration": 40000 } } }] },
5
+ "checks": [
6
+ { "why": "a 9:16 frame at 1080 × 1920", "tool": "get_video_project", "expect": { "width": 1080, "height": 1920 } },
7
+ { "why": "the picture covers the frame", "tool": "get_video_project", "expect": { "tracks": { "$some": { "clips": { "$some": { "id": "c1", "$expr": "v.fit === 'cover' || (v.transform && v.transform.scale >= 3.15)" } } } } } },
8
+ { "why": "no errors", "tool": "check_video_project", "expect": { "errors": 0 } }
9
+ ],
10
+ "solution": [
11
+ { "tool": "set_video_settings", "args": { "width": 1080, "height": 1920 } },
12
+ { "tool": "update_video_clip", "args": { "clip": "c1", "set": { "fit": "cover" } } },
13
+ { "tool": "check_video_project" }
14
+ ]
15
+ }