reelkit-cli 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/package.json +1 -1
- package/skill/SKILL.md +8 -2
- package/skill/reference/beat-sync.md +45 -0
- package/skill/reference/captions.md +18 -2
- package/skill/reference/continuity.md +99 -0
- package/skill/reference/kit.md +18 -1
- package/skill/reference/motion-design.md +9 -0
- package/skill/reference/references.md +11 -0
- package/skill/reference/remotion-composition.md +2 -1
- package/skill/reference/scene-treatments.md +1 -1
- package/skill/reference/scriptwriting.md +39 -5
- package/skill/reference/sound-design.md +11 -0
- package/skill/reference/styles.md +9 -0
- package/src/cli.ts +2 -2
- package/src/commands/assets.ts +25 -2
- package/src/commands/build.ts +73 -8
- package/src/commands/plan.ts +4 -2
- package/src/commands/ref.ts +11 -3
- package/src/pipeline/beatsnap.ts +52 -0
- package/src/pipeline/review.ts +28 -1
- package/src/pipeline/schema.ts +8 -0
- package/src/project/manifest.ts +22 -1
- package/src/project/music.ts +25 -0
- package/src/project/project.ts +1 -1
- package/src/project/refmeasure.ts +44 -0
- package/src/remotion/Root.tsx +4 -2
- package/src/remotion/kit/Camera.tsx +22 -0
- package/src/remotion/kit/Captions.tsx +25 -8
- package/src/remotion/kit/Carry.tsx +38 -0
- package/src/remotion/kit/Music.tsx +19 -0
- package/src/remotion/kit/beat.ts +23 -0
- package/src/remotion/kit/caption-groups.ts +65 -0
- package/src/remotion/kit/docs.ts +18 -1
- package/src/remotion/kit/index.ts +7 -0
- package/src/remotion/kit/media.ts +15 -0
- package/src/remotion/kit/motion-math.ts +113 -0
- package/src/remotion/kit/music-math.ts +42 -0
- package/src/render/continuity.ts +87 -0
- package/src/render/validate.ts +27 -0
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ reelkit render
|
|
|
62
62
|
| `reelkit assets gen clip` | Generate a scene's video clip, or a green-screen one keyed to a transparent video, or one with the subject cut out on the server |
|
|
63
63
|
| `reelkit plan check` | Validate `plan.json` |
|
|
64
64
|
| `reelkit check` | Check the composition without rendering |
|
|
65
|
-
| `reelkit preview` |
|
|
65
|
+
| `reelkit preview` | Preview frames, and a report of which scene changes carry something across |
|
|
66
66
|
| `reelkit render` | Render `out/video.mp4` |
|
|
67
67
|
|
|
68
68
|
## What is shared
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -30,6 +30,10 @@ If the user points at an existing video ("make one like this", a link or a file)
|
|
|
30
30
|
### 2. Look
|
|
31
31
|
Read `reference/styles.md` and agree the video's look with the user: one of its looks, or their own. If they already described what they want, match it and confirm in one sentence. Ask anything still open in one message, each question with a default. If any on-screen text will be Hebrew, read `reference/hebrew-rtl.md` now: it changes how words may enter and how lines are written.
|
|
32
32
|
|
|
33
|
+
Ask whether they want captions, and which kind: none, one word at a time (`word`), or a phrase at a time (`phrase`, the default). Write the answer into the plan as `captions`.
|
|
34
|
+
|
|
35
|
+
Then read `reference/continuity.md` and offer the user three concepts for the video, each one sentence about the picture (not the product) with a different central idea, and say what carries each scene change. They pick one; write it into the first scene's `notes` beside the look.
|
|
36
|
+
|
|
33
37
|
### 3. Plan
|
|
34
38
|
Read `reference/scriptwriting.md` and `reference/scene-treatments.md` (and `reference/clips.md` if any scene might be a video clip). Write the chosen look into the first scene's `notes`. Run `reelkit assets voices` and choose a voice that fits the idea, audience and language.
|
|
35
39
|
|
|
@@ -65,6 +69,7 @@ Write `plan.json`:
|
|
|
65
69
|
- `clipPrompt` is set only for `clip` scenes (a generated or reused video clip is the scene's picture; the rest of the scene uses `imageTags` and `shareable` as an illustration does). Clips are scarce: most videos have none or one or two.
|
|
66
70
|
- `userAssetIds` lists the ids of the user's files shown in that scene.
|
|
67
71
|
- `pace` is `slow`, `normal` or `fast`.
|
|
72
|
+
- `captions` is `none`, `word` or `phrase`, as the user chose; leave it out for `phrase`. The manifest carries it: `<Captions group={manifest.captions} />`.
|
|
68
73
|
- When the video follows a reference, add `"reference": { "id": "<id>", "take": ["fast cuts every ~1.2s"] }` (1 to 6 notes on what you took).
|
|
69
74
|
|
|
70
75
|
Run `reelkit plan check`; it prints the estimated length to tell the user. Fix everything under "Fix these". Act on "Worth improving" unless you have a good reason not to.
|
|
@@ -82,15 +87,16 @@ Each result starts with a match percentage: how likely it is good enough to reus
|
|
|
82
87
|
For each clip scene, read `reference/clips.md`, then search `reelkit assets search "<what the clip shows>" --kind clip` and pull a 60% match (`reelkit assets pull <id> --scene <sceneId>`), or generate: `reelkit assets gen clip --scene <sceneId>` (add `--green` for a green-screen subject). It waits for the clip, which takes minutes; check what is left with `reelkit whoami`.
|
|
83
88
|
|
|
84
89
|
### 6. Composition
|
|
85
|
-
Read `reference/kit.md`, `reference/remotion-composition.md`, `reference/motion-design.md` and `reference/
|
|
90
|
+
Read `reference/kit.md`, `reference/remotion-composition.md`, `reference/motion-design.md`, `reference/continuity.md`, `reference/captions.md` and `reference/beat-sync.md`.
|
|
86
91
|
|
|
92
|
+
- Choose the music before you write the composition, and read `reference/beat-sync.md`: `reelkit assets search "<mood and tempo>" --kind music`, then `reelkit assets pull <id> --music`. Scene changes then land on its beat by themselves; bring each element in on a beat inside a scene as that file shows.
|
|
87
93
|
- Search the library before writing a component: `reelkit assets search "<what it shows>" --kind component`. To use one, `reelkit assets pull <id>`: it lands in `src/` and the command prints the import line and an example.
|
|
88
94
|
- Read `reference/sound-design.md`, then find the few sounds the video needs: `reelkit assets search "whoosh" --kind sfx`, `reelkit assets pull <id>`. The pull prints how to reference the file.
|
|
89
95
|
- Before writing a new component, read `reference/component-authoring.md`.
|
|
90
96
|
- Write `src/Video.tsx` and any component files beside it. `Video.tsx` may import only `react`, `remotion`, `reelkit/kit` and sibling components (`./Name`).
|
|
91
97
|
- `manifest.json` is the exact object passed to `Video` as the `manifest` prop. Media is referenced as `urls[path]`, where `path` is the file's path in the project, such as `urls[scene.voiceoverKey]` or `urls["assets/lib/<id>/clip.mp3"]`.
|
|
92
98
|
|
|
93
|
-
Run `reelkit check` and fix every error until it passes. Then `reelkit preview` (the first preview on a machine downloads a browser once and can take a minute) and look at every frame in `out/preview/`: each scene has two, `early` (30% into the scene) and `late` (90%), so look at both frames of each scene. Look for: text cut off or overflowing, text overlapping other text or the captions, text too small or too low-contrast for a phone, an empty or broken frame, content hidden behind another layer, and any number, price or quote on screen that is not in the plan. A frame is one moment: an element mid-animation is not a problem, but anything that should be fully on screen by the late frame and is not is. Fix real problems and preview again. Go round at least twice, and finish with the studio test in `reference/motion-design.md`.
|
|
99
|
+
Run `reelkit check` and fix every error until it passes. Then `reelkit preview` (the first preview on a machine downloads a browser once and can take a minute) and look at every frame in `out/preview/`: each scene has two, `early` (30% into the scene) and `late` (90%), so look at both frames of each scene. Look for: text cut off or overflowing, text overlapping other text or the captions, text too small or too low-contrast for a phone, an empty or broken frame, content hidden behind another layer, and any number, price or quote on screen that is not in the plan. A frame is one moment: an element mid-animation is not a problem, but anything that should be fully on screen by the late frame and is not is. Fix real problems and preview again. Go round at least twice, and finish with the studio test in `reference/motion-design.md`. `reelkit preview` also reports continuity: how many scene changes carry something across. Fix every `cut` it names, or keep it as a deliberate choice (a burst of fast hits, the end card); see `reference/continuity.md`.
|
|
94
100
|
|
|
95
101
|
You only ever see frames, never the moving video, so the frames are your eyes: look at every one. And whoever built a video is the worst judge of it. If you can start a separate agent, give it only the user's original request and the preview frames (not your explanations) and ask for a score out of 100 per scene, a list of flaws, and a concrete fix for each, in numbers ("raise the title 60 px", "hold the label 0.4 s longer"). Fix, preview, and have the same reviewer look again until every scene passes 90. If two rounds leave a scene under 70, stop and show the user the gap instead of spending more rounds.
|
|
96
102
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Putting the video on the beat
|
|
2
|
+
|
|
3
|
+
A video with music feels edited when things happen on the beat. The CLI puts the scene changes on the beat for you; inside a scene it is your job.
|
|
4
|
+
|
|
5
|
+
## 1. Choose the track before the composition
|
|
6
|
+
|
|
7
|
+
`reelkit assets search "<mood and tempo>" --kind music`, for example "calm warm piano, slow" or "driving electronic, 120 BPM". Pull the best fit with `reelkit assets pull <id> --music`. That makes it the video's one track (a second `--music` replaces the first), measures its tempo here, and writes `assets/music.json`. Never use a reference video's own music; it is not yours to use.
|
|
8
|
+
|
|
9
|
+
Then `manifest.json` has `music: { key, bpm, beatFrames }`. `beatFrames` are the beats as composition frames. If a track has no clear tempo there is no `bpm`, `beatFrames` is empty, and nothing below applies: the scenes stay where the narration puts them.
|
|
10
|
+
|
|
11
|
+
## 2. Scene changes are on the beat already
|
|
12
|
+
|
|
13
|
+
When the track has a tempo, each scene is held a little at its end, by less than one beat, so the next scene starts exactly on a beat. The narration and its word times do not move. `reelkit preview` says how many scene changes land on the beat. Do not shift `startFrame` yourself.
|
|
14
|
+
|
|
15
|
+
## 3. Inside a scene: bring each element in on a beat
|
|
16
|
+
|
|
17
|
+
`beatFrames` are composition frames and a scene's clock starts at 0, so subtract the scene's `startFrame`. For an element that belongs to a word, take the beat nearest the frame where the word starts, then turn it into a `delay`:
|
|
18
|
+
|
|
19
|
+
- the word starts at `wordFrame = s.startFrame + Math.round(s.words[2].startSec * fps)`
|
|
20
|
+
- `beat = nearestBeat(manifest.music.beatFrames, wordFrame)`
|
|
21
|
+
- `delay = beat - s.startFrame`
|
|
22
|
+
|
|
23
|
+
Use `nextBeat` when the element must not come before the word. Prefer the beat nearest the word it belongs to over one that is merely free. Put the biggest move (the hero, the number, the reveal) on a strong beat: every fourth beat counts from the scene's first beat. `beatPulse(beatFrames, frame)` is 1 on a beat and falls to 0; use it sparingly, for a small scale or glow on the hero element only, never on everything.
|
|
24
|
+
|
|
25
|
+
Sound effects go on beats too: the `at` of an `Sfx` is a frame from the scene start, so use the same `delay` arithmetic.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Entrance, Music, nearestBeat, SceneFrame, Voiceover } from "reelkit/kit";
|
|
29
|
+
|
|
30
|
+
const beats = manifest.music?.beatFrames ?? [];
|
|
31
|
+
// Frames from the scene start to the beat nearest the word at index i.
|
|
32
|
+
const onBeat = (s: { startFrame: number; words: { startSec: number }[] }, i: number) => {
|
|
33
|
+
const word = s.startFrame + Math.round((s.words[i]?.startSec ?? 0) * manifest.fps);
|
|
34
|
+
return (nearestBeat(beats, word) ?? word) - s.startFrame;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
<SceneFrame from={s.startFrame} durationInFrames={s.durationFrames}>
|
|
38
|
+
<Entrance delay={onBeat(s, 2)}><Card /></Entrance>
|
|
39
|
+
{s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
|
|
40
|
+
</SceneFrame>
|
|
41
|
+
// Once, outside the scenes:
|
|
42
|
+
{manifest.music ? <Music src={urls[manifest.music.key]} /> : null}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`Music` ducks under the voice by itself and fades out over the last second; do not set its volume per scene.
|
|
@@ -7,6 +7,22 @@ description: Use when choosing and placing the spoken-word captions for a video
|
|
|
7
7
|
|
|
8
8
|
Most short video is watched with the sound off, so the captions carry the message. Captions are drawn by the kit's `<Captions>` component from the scene's word timings (`s.words`); nothing is burned in afterwards.
|
|
9
9
|
|
|
10
|
+
## First: does the video have captions?
|
|
11
|
+
|
|
12
|
+
The plan's `captions` field says: `"none"`, `"word"` (one word at a time) or `"phrase"` (a phrase at a time, the default when the field is absent). The user chose it at the Look step. It is in the manifest as `manifest.captions`, so pass it straight through and the composition follows the plan:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<Captions words={s.words} group={manifest.captions} mode="pop" highlight={palette.hero} />
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
With `"none"` the component draws nothing; better still, leave `<Captions>` out of the composition altogether. `reelkit check` notes a composition that renders `<Captions>` when the plan says `"none"`, and one that renders none when the plan asks for words.
|
|
19
|
+
|
|
20
|
+
## How words are grouped (`group`)
|
|
21
|
+
- `"word"`: exactly one word on screen at a time. Larger text, the most energy.
|
|
22
|
+
- `"phrase"`: words are grouped the way they are spoken, never by a fixed count that cuts a sentence. A group ends at the end of a sentence, at a comma or a dash once it has three words, and at a pause of 0.35 s or more between words. It never holds more than 6 words or about 32 characters; when it must break it does so after a comma or before a short joining word ("and", "of", "to") where it can, and a lone last word is joined to the line before it. Hebrew and Arabic sentence marks work the same way.
|
|
23
|
+
- Without `group`, `perLine` counts the words as it always did. Use `group` for every new video.
|
|
24
|
+
- `mode` (below) still decides how the words inside the group look.
|
|
25
|
+
|
|
10
26
|
## Pick one style for the whole video
|
|
11
27
|
|
|
12
28
|
| mode | What it does | Use for |
|
|
@@ -22,8 +38,8 @@ Most short video is watched with the sound off, so the captions carry the messag
|
|
|
22
38
|
Do not mix modes between scenes. Set `highlight` to the palette's hero colour (captions are the one exception to the one-hero-element rule) or to a high-contrast yellow on busy footage.
|
|
23
39
|
|
|
24
40
|
## Words per screen
|
|
25
|
-
- `
|
|
26
|
-
- `highlight` and `karaoke
|
|
41
|
+
- With `group="phrase"` the groups are at most 6 words, and about 32 characters, by themselves. With `group="word"` it is one.
|
|
42
|
+
- With `perLine` instead: `pop` 1 to 3 words; `highlight` and `karaoke` 3 to 5 words for vertical video, up to 6 for landscape; in Hebrew at most 4.
|
|
27
43
|
- Never show a full sentence at once. Word-level timing reads better than sentence captions.
|
|
28
44
|
|
|
29
45
|
## Timing
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: continuity
|
|
3
|
+
description: Use before the plan and again when writing the composition - how to keep a video from feeling like a slideshow, with three concepts to offer the user, what to carry across every scene change, how to vary rhythm, and how to read the continuity and rhythm numbers the CLI reports.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Continuity and rhythm
|
|
7
|
+
|
|
8
|
+
A slideshow is a row of separate pictures: each scene replaces the one before it, and every scene lasts about the same time. Two things cure it. Continuity means that at every scene change something on screen survives and visibly moves, grows or turns into the next scene. Rhythm means shot lengths differ a lot, the picture sometimes rests, and not everything moves all the time.
|
|
9
|
+
|
|
10
|
+
## Three concepts before a plan
|
|
11
|
+
|
|
12
|
+
A concept is one sentence about the picture, not about the product. Before you write `plan.json`, offer the user three concepts that differ in their central idea, and let them pick. For each one say the opening frame, what carries each scene change, and what it rules out.
|
|
13
|
+
|
|
14
|
+
Ideas to draw from (invent your own when the topic asks for it):
|
|
15
|
+
|
|
16
|
+
- **One element that transforms through the whole video.** A single card is the opening frame; it becomes the list, then the chart, then the button. Rules out unrelated pictures per scene.
|
|
17
|
+
- **One continuous camera move through scale.** The opening frame is a whole screen; the camera pushes into one detail until that detail is the next scene, and so on. Rules out cuts and flat full-frame layouts.
|
|
18
|
+
- **A chain of cause and effect.** Each event starts the next: a tap sends a message, the message lands as a notification, the notification opens the app. Rules out scenes that merely follow each other in a list.
|
|
19
|
+
- **One surface the whole video lives on.** A desk, a phone screen, a page, a map. Everything is placed on it and the camera or the objects move across it. Rules out backgrounds that change per scene.
|
|
20
|
+
- **A before-and-after split.** The frame is divided from the start; the dividing line moves, and what was on one side becomes the other. Rules out stories that have no contrast to show.
|
|
21
|
+
|
|
22
|
+
Write each concept as: "Opens on a plain card with the price. The card slides up and becomes the header of the comparison; the comparison's winning row grows into the call to action. No stock backgrounds." The user picks; write the choice into the first scene's `notes`, next to the look.
|
|
23
|
+
|
|
24
|
+
## Carry at every scene change
|
|
25
|
+
|
|
26
|
+
For each boundary between two scenes, write down in that scene's `notes` what survives and how it moves. "Scene 2 to 3: the price card shrinks to the top-left and stays as the badge." If you cannot name it, the change is a cut.
|
|
27
|
+
|
|
28
|
+
Ways to do it with the kit:
|
|
29
|
+
|
|
30
|
+
- An element held in a `Carry` layer outside the scenes. It is drawn once, above the `SceneFrame`s, so it does not fade with them, and its keys say where it must be at which frame.
|
|
31
|
+
- A `Camera` pushing into an element until it fills the frame. The next scene starts with that element as its whole picture.
|
|
32
|
+
- A container that grows into the next scene's background. The card of one scene becomes the full-frame ground of the next.
|
|
33
|
+
- A shared colour field that one scene's object expands into. The new scene is the colour the old object grew to.
|
|
34
|
+
|
|
35
|
+
A hard cut is a choice. Use it for a burst of fast hits, or for the end card, and know that you are doing it. It is not the default.
|
|
36
|
+
|
|
37
|
+
`SceneFrame` fades each scene in and out over its first and last few frames. Anything that must stay on screen through a change therefore lives outside it, in a `Carry` layer (or inside a `Camera` that wraps all the scenes). This example holds a price card through two scenes: it is large in the first, then shrinks into a badge that the second scene is built around.
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import React from "react";
|
|
41
|
+
import { AbsoluteFill } from "remotion";
|
|
42
|
+
import { BgMesh, Carry, Entrance, fonts, palettes, SceneFrame } from "reelkit/kit";
|
|
43
|
+
import type { VideoProps } from "reelkit/kit";
|
|
44
|
+
|
|
45
|
+
export const Video: React.FC<VideoProps> = ({ manifest }) => {
|
|
46
|
+
const palette = palettes.darkTech;
|
|
47
|
+
const [a, b] = manifest.scenes;
|
|
48
|
+
return (
|
|
49
|
+
<AbsoluteFill>
|
|
50
|
+
<BgMesh bg={palette.bg} hero={palette.hero} accent={palette.accent} />
|
|
51
|
+
<SceneFrame from={a.startFrame} durationInFrames={a.durationFrames}>
|
|
52
|
+
<Entrance delay={4}><div style={{ color: palette.dim, fontFamily: fonts.body, fontSize: 48 }}>Today only</div></Entrance>
|
|
53
|
+
</SceneFrame>
|
|
54
|
+
<SceneFrame from={b.startFrame} durationInFrames={b.durationFrames}>
|
|
55
|
+
<Entrance delay={10}><div style={{ color: palette.ink, fontFamily: fonts.display, fontSize: 96, fontWeight: 800, marginTop: 360 }}>Half the price</div></Entrance>
|
|
56
|
+
</SceneFrame>
|
|
57
|
+
<Carry
|
|
58
|
+
keys={[
|
|
59
|
+
{ frame: 6, x: 0.5, y: 0.45, width: 0.7, height: 0.22, radius: 0.03, opacity: 1 },
|
|
60
|
+
{ frame: b.startFrame + 6, x: 0.25, y: 0.12, width: 0.3, height: 0.08, radius: 0.02 },
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{(box) => (
|
|
64
|
+
<div style={{ width: "100%", height: "100%", background: palette.hero, display: "flex", alignItems: "center", justifyContent: "center", color: palette.ink, fontFamily: fonts.display, fontWeight: 800, fontSize: box.heightPx * 0.5 }}>
|
|
65
|
+
$49
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</Carry>
|
|
69
|
+
</AbsoluteFill>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The move toward a key starts 12 frames before the key's frame and lands on it; give the card a key early enough in the first scene that it has settled before the change, and a key just after the change for where the next scene wants it.
|
|
75
|
+
|
|
76
|
+
## Rhythm
|
|
77
|
+
|
|
78
|
+
- Vary shot length at least fourfold: a hit of a quarter of a second beside a hold of two seconds or more. A plan whose scenes all last about the same time is a slideshow.
|
|
79
|
+
- Put a rest before the biggest moment: for about a second little or nothing moves, so that the moment lands.
|
|
80
|
+
- Never the same exit twice in a row. If one scene's content slides out to the left, the next one scales or wipes.
|
|
81
|
+
- Fewer sounds than changes. Sound a change that matters; leave the others silent.
|
|
82
|
+
|
|
83
|
+
## The cause is visible
|
|
84
|
+
|
|
85
|
+
When something reacts, show what caused it: a pointer that presses before the button changes, an object that lands before the splash, a word that is spoken before the number appears. A result with no visible cause looks like a cut, even when everything is moving.
|
|
86
|
+
|
|
87
|
+
## Small elements, big ground
|
|
88
|
+
|
|
89
|
+
For product pictures, the default is a small element on a big ground: the card, phone or button takes a third of the frame and the ground (a colour field, a surface, a blurred scene) fills the rest. A full-frame screenshot is for a held shot, where the viewer needs time to read it. Small elements are what a `Carry` or a `Camera` can move; full-frame pictures can only be replaced.
|
|
90
|
+
|
|
91
|
+
## Reading the numbers
|
|
92
|
+
|
|
93
|
+
`reelkit preview` also saves the two frames around each scene change (`b01-end-<scene>.jpg`, then `b01-start-<scene>.jpg`, and so on) and prints a continuity report: a line such as "3 of 5 scene changes carry something across", and a line for each change where nothing does. The frames are the last and first ones at full strength, because `SceneFrame` fades the very last and first. The measure compares edges, the outlines of what is on screen: when at least a quarter of the earlier frame's outlines are still there, nearby, in the later frame, the change is `carried`; otherwise it is a `cut`. A change where the earlier frame is nearly empty is not counted.
|
|
94
|
+
|
|
95
|
+
Read it as a smoke alarm, not a grade. A `cut` is a prompt to fix the change or to say in the notes why it is a cut. A `carried` change can still be wrong (an element that stays but means nothing), so look at the pair of frames. The report is advice and never makes `preview` fail. Aim for most changes carried, with the cuts that remain on purpose.
|
|
96
|
+
|
|
97
|
+
`reelkit plan check` reads the rhythm from the narration: with four scenes or more, it adds a line under "Worth improving" when the scenes are about equally long, or when none is shorter than half the average. It quotes the shortest and the longest. Its data holds `sceneSeconds` for each scene and `lengthVariation`, the spread of the lengths over their mean; below 0.25 is too even. The cure is in the line itself: make one or two scenes much shorter, a hit of a few words, and let one run long.
|
|
98
|
+
|
|
99
|
+
When the video follows a reference, `reelkit ref analyze` gives targets to match. `pacing.shotLengthVariation` is the same spread for the reference's shots (left out when it has fewer than three), and `stillness` holds `share`, the part of the time in which the picture barely changes from frame to frame, and `longestSec`, the longest such hold. If the reference's shots vary by 0.8 and it is still a third of the time, plan scene lengths that vary about that much, and let the picture rest about that often. A video that moves every frame, against a reference that holds, will feel busier than the reference, whatever else you copy.
|
package/skill/reference/kit.md
CHANGED
|
@@ -15,9 +15,10 @@ LowerThird { title: string; subtitle?: string; accent?: string }
|
|
|
15
15
|
Name/label strip that slides in from the left, low on the screen.
|
|
16
16
|
<LowerThird title="Step 1" subtitle="Connect your account" />
|
|
17
17
|
|
|
18
|
-
Captions { words: WordTiming[]; mode?: "highlight" | "pop" | "karaoke"; highlight?: string; color?: string; perLine?: number; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
|
|
18
|
+
Captions { words: WordTiming[]; mode?: "highlight" | "pop" | "karaoke"; highlight?: string; color?: string; perLine?: number; group?: "none" | "word" | "phrase"; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
|
|
19
19
|
Word-timed captions near the bottom. Pass the scene's words from the manifest. Pick one mode for the whole video:
|
|
20
20
|
"highlight" (a line, spoken word coloured), "pop" (1-3 words popping in as spoken), "karaoke" (a line filling with colour).
|
|
21
|
+
group decides which words share the screen: "word" is one word at a time, "phrase" groups words as they are spoken (a group ends at a sentence end, at a comma after three words, at a pause of 0.35 s, and never holds more than 6 words or about 32 characters), "none" draws nothing. Pass the plan's choice straight through: group={manifest.captions}. Without group, perLine counts the words as before.
|
|
21
22
|
bottom is the distance from the bottom edge as a fraction of the height (default 0.16).
|
|
22
23
|
For Hebrew narration pass face={font("heebo")} and rtl.
|
|
23
24
|
<Captions words={s.words} mode="pop" highlight={palette.hero} uppercase />
|
|
@@ -60,6 +61,14 @@ WordReveal { text: string; delay?: number; per?: number; highlight?: string; hig
|
|
|
60
61
|
Headline that rises in word by word from behind a mask. highlight colours one word.
|
|
61
62
|
<WordReveal text="Stretch first, phone second" highlight="first" highlightColor={palette.hero} style={{ fontFamily: fonts.display, fontWeight: 800, fontSize: width * 0.09, color: palette.ink }} />
|
|
62
63
|
|
|
64
|
+
Carry { keys: { frame: number; x: number; y: number; width: number; height: number; radius?: number; opacity?: number; rotate?: number }[]; lead?: number; stiffness?: number; damping?: number; children | (box) => children }
|
|
65
|
+
One element held through several scenes. Place it once, beside the SceneFrames and above them, so it is still on screen when a scene changes. Each key says where the box must have arrived by an absolute composition frame: x, y are its centre and width, height are fractions of the frame (0 to 1); radius is the corner radius as a fraction of the frame width; rotate is in degrees. The move toward a key starts lead frames before the key's frame (default 12, or at the previous key's frame when the keys are closer) on a spring (default springs.smooth) and lands on the key's frame exactly; between moves the box holds. Before the first key nothing is drawn unless that key sets an opacity; after the last the box holds. Children fill the box; a function child gets { x, y, width, height, radius, opacity, rotate, widthPx, heightPx } for the current moment, so the content can change as the box does.
|
|
66
|
+
<Carry keys={[{ frame: a.startFrame + 6, x: 0.5, y: 0.45, width: 0.8, height: 0.3 }, { frame: b.startFrame, x: 0.5, y: 0.5, width: 1, height: 1, radius: 0 }]}>{(box) => <Card compact={box.width < 0.5} />}</Carry>
|
|
67
|
+
|
|
68
|
+
Camera { keys: { frame: number; x?: number; y?: number; zoom?: number; rotate?: number }[]; drift?: number; lead?: number; stiffness?: number; damping?: number; children }
|
|
69
|
+
Moves the whole picture. Wrap all the scenes for one camera that never cuts, or wrap one scene's content. x, y are the point of the content, as fractions, that sits at the middle of the frame (default 0.5, 0.5); zoom 1 shows the content as it fits; rotate is in degrees. A key that leaves a value out keeps the one before it. The timing rule is the same as Carry's: the move toward a key starts lead frames before its frame and lands on it. drift is a very slow continuous push, as a fraction of the zoom per second (0.02 is two percent a second), so a held shot is never perfectly still.
|
|
70
|
+
<Camera keys={[{ frame: 0, zoom: 1 }, { frame: b.startFrame, x: 0.7, y: 0.4, zoom: 4 }]} drift={0.01}>{scenes}</Camera>
|
|
71
|
+
|
|
63
72
|
fonts { display: string; body: string }
|
|
64
73
|
Loaded font families. Use fonts.display (weights 600-800) for headlines and numbers, fonts.body for supporting text. Never leave hero text on a default font.
|
|
65
74
|
|
|
@@ -102,6 +111,14 @@ Sfx { src: string; at?: number; volume?: number }
|
|
|
102
111
|
Plays one sound effect from the shared library, starting at the frame given by "at", counted from the start of the enclosing scene. Pull one with: reelkit assets search "<description>" --kind sfx, then reelkit assets pull <id>.
|
|
103
112
|
<Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={10} volume={0.35} />
|
|
104
113
|
|
|
114
|
+
Music { src: string; volume?: number; duckTo?: number }
|
|
115
|
+
The video's one music track (pull it with: reelkit assets pull <id> --music). Place it once, outside the scenes. It sits at duckTo (default 0.12) while the voiceover is heard and comes up to volume (default 0.5) in gaps longer than 0.6 s, loops if the video is longer than the track, and fades out over the last second. It follows the manifest, so nothing else is passed.
|
|
116
|
+
<Music src={urls[manifest.music.key]} />
|
|
117
|
+
|
|
118
|
+
nearestBeat(beatFrames, frame): number | undefined nextBeat(beatFrames, frame): number | undefined beatPulse(beatFrames, frame, decayFrames = 10): number
|
|
119
|
+
Beat helpers; beatFrames is manifest.music.beatFrames (composition frames). nearestBeat is the closest beat, nextBeat the first at or after the frame, beatPulse a number from 0 to 1 that is 1 on a beat and falls to 0 after it. Inside a scene, subtract s.startFrame from a beat to get a frame for that scene's own clock.
|
|
120
|
+
const b = nextBeat(manifest.music?.beatFrames ?? [], s.startFrame + 20) ?? s.startFrame + 20; // <Entrance delay={b - s.startFrame}>
|
|
121
|
+
|
|
105
122
|
Voiceover { src: string; volume?: number }
|
|
106
123
|
The scene's narration audio. One per narrated scene, inside its SceneFrame. Guard it with s.voiceoverKey so a scene without a recording still renders.
|
|
107
124
|
{s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
|
|
@@ -72,6 +72,15 @@ Hook (first 1.5 s: the boldest visual and claim) → context (one line, one visu
|
|
|
72
72
|
- Do not fake blur on fast motion with a blur filter on everything. Keep a short blur (6 to 10 pixels for a few frames) for content swapping inside a container and for depth.
|
|
73
73
|
- A whole word sliding hundreds of pixels in a frame leaves separate ghost copies. Give it a directional blur that follows its speed, or shorten the travel.
|
|
74
74
|
|
|
75
|
+
## Product shots
|
|
76
|
+
- **One object, then the push.** A product shot holds one thing in the middle: a field, a card, a device, a number. Space around it reads as confidence, as long as the camera group is moving in on it.
|
|
77
|
+
- **Screens live in space.** A screenshot laid flat is a slide; set at 8 to 15 degrees of turn on a card with a deep soft shadow it is an object. Bring it in from a steeper angle and let it settle; turn it to face the viewer only when its content must be read.
|
|
78
|
+
- **Change the shot through the object.** The field grows into the card, the icon opens into the screen, the tile becomes the number. When nothing can be carried across, the old object leaves in 4 to 6 frames with a directional blur and the new one lands on a heavy spring.
|
|
79
|
+
- **Interface text is typed, tapped and sent, never faded in.** A field gets a caret and keystrokes, a button gets a press (down 4 to 8 percent, back on a snappy spring) and a visible result, a switch travels. The viewer should be able to tell what was touched.
|
|
80
|
+
- **A pointer needs a reason.** Move it on an eased path, slow it into the target, press, and let something change within two frames of the press.
|
|
81
|
+
- **Two tones of text.** Set a sentence in a muted tone and bring only its key word to full ink or the hero colour as it is spoken.
|
|
82
|
+
- **End smaller than you began.** After the payoff, the logo sits small and centred on the clean ground, with at most one line under it, held at least 1.5 seconds.
|
|
83
|
+
|
|
75
84
|
## Weight on the page
|
|
76
85
|
- Keep a scene under roughly 1,500 elements; beyond that draw on one canvas and redraw it each frame.
|
|
77
86
|
- Compute heavy geometry once, outside the frame function, and only position it per frame.
|
|
@@ -25,6 +25,17 @@ Take:
|
|
|
25
25
|
|
|
26
26
|
Never take its footage, its music, its exact words, its logos or its faces. The transcript is for understanding the structure (where the hook ends, where the point lands), never for copying the script: write your own words about the user's subject.
|
|
27
27
|
|
|
28
|
+
## Several references from one maker
|
|
29
|
+
When the user points at an account or a handful of videos rather than one, bring in three to six of them and look for what repeats, because that is the style; what appears once is only that video.
|
|
30
|
+
- **The ground:** what is behind everything (white, near-black, a colour field, footage) and whether it ever changes inside a video.
|
|
31
|
+
- **How full the frame is:** one small object in a lot of space, or type edge to edge. Measure it roughly: the main object's width as a share of the frame.
|
|
32
|
+
- **The order of shots:** most makers have one. Write it as a line, such as "a line of type, the logo, a field being typed into, three interface shots, a number, the logo".
|
|
33
|
+
- **How things arrive and leave:** on a spring, with a blur, with a hard cut, by changing shape. Step through a transition a frame at a time in the saved frames.
|
|
34
|
+
- **How long a shot holds,** from `pacing.averageShotSec` across the set, and how long the last one does.
|
|
35
|
+
- **Type:** how many sizes, how many weights, whether one word is coloured, and how words enter.
|
|
36
|
+
- **Sound:** whether the music runs without a break, and whether moves have their own sounds.
|
|
37
|
+
Write those seven lines down and match them to the nearest look in `reference/styles.md`; they become the plan's `reference.take` notes. The maker's subjects, logos, screens and layouts stay theirs: build the user's video from the library's components and the user's own material.
|
|
38
|
+
|
|
28
39
|
## Turn the pace into the plan
|
|
29
40
|
- Scene count and length come from `pacing.averageShotSec`: a video of 30 seconds with shots of about 1.5 seconds is many quick scenes; since a plan has 3 to 8 scenes, group several reference shots into one scene that changes its picture inside (stacked text, quick swaps) rather than stretching it.
|
|
30
41
|
- The hook's length comes from `pacing.firstCutSec`: if the reference holds its first shot for 1.2 seconds, your first scene is one short line.
|
|
@@ -58,7 +58,8 @@ Each `scenes` entry:
|
|
|
58
58
|
2. Images: `<KenBurnsImage>` inside its scene. Every still image moves; alternate `direction` between consecutive image scenes.
|
|
59
59
|
3. Clips: `<ClipLayer>` inside its scene (a clip scene's picture); a green-screen `<KeyedClip>` above the scene's background and below its type and captions. See `reference/clips.md`.
|
|
60
60
|
4. Graphics and type, inside each scene's SceneFrame.
|
|
61
|
-
5.
|
|
61
|
+
5. Anything that must survive a scene change: a `<Carry>` layer beside the scenes and above them (outside the SceneFrame fades), and `<Camera>` around the scenes when one camera moves through all of them. See `reference/continuity.md`.
|
|
62
|
+
6. `<Grade>`, then `<Grain>`, then `<Vignette>`, once, at the very top of the video outside the scenes. In footage mode skip Grade.
|
|
62
63
|
|
|
63
64
|
## Shared overlays (optional)
|
|
64
65
|
- The library has ready-made overlay clips (`reelkit assets search "<description>" --kind overlay`): film marks, HUD frames, corner marks. They are white on black and are laid over the video with `<ScreenOverlay>`.
|
|
@@ -7,7 +7,7 @@ description: Use when choosing each scene's visual treatment and writing image p
|
|
|
7
7
|
|
|
8
8
|
## The four treatments
|
|
9
9
|
- **motion-graphic** - animated text, numbers, shapes and icons drawn in code. The default. Best for hooks, lists, statistics, steps and calls to action.
|
|
10
|
-
- **illustration** - one generated picture behind the text. Use when a concrete image carries the point better than type (an object, a place, a mood).
|
|
10
|
+
- **illustration** - one generated picture behind the text. Use when a concrete image carries the point better than type (an object, a place, a mood). Plan a picture or a clip for a third to half of the scenes, the opening first: a video of only text and shapes reads as a slideshow. Search the library before generating, since a reused picture costs nothing.
|
|
11
11
|
- **clip** - a few seconds of generated or reused video as the scene's picture (B-roll), or a green-screen subject over your own graphics. Needs a `clipPrompt`. Rare and costly: see `reference/clips.md` before using it.
|
|
12
12
|
- **footage-overlay** - text and graphics over the user's own video. Required for every scene when footage was supplied, and never used otherwise.
|
|
13
13
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: scriptwriting
|
|
3
|
-
description: Use when writing the spoken script for a short social video -
|
|
3
|
+
description: Use when writing the spoken script for a short social video - the opening, hook patterns, story shape by kind of video, pacing, and holding attention to the end.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Scriptwriting for short video
|
|
@@ -8,12 +8,46 @@ description: Use when writing the spoken script for a short social video - hooks
|
|
|
8
8
|
## Length and pace
|
|
9
9
|
- Narration is spoken at about 2.5 words per second. A 30 second video is about 75 words; 60 seconds is about 150.
|
|
10
10
|
- Without footage, aim for 30 to 60 seconds in total. With footage, never exceed the word limit that `reelkit plan check` reports.
|
|
11
|
-
- One idea per scene.
|
|
11
|
+
- One idea per scene. Scenes should not all be the same length: let one or two be a hit of three to six words and one run to two sentences. Even scenes read as slides.
|
|
12
|
+
|
|
13
|
+
## The opening decides the video
|
|
14
|
+
Most of what a short video achieves is decided in its first three seconds, and nearly all of it by ten. Write the opening first and spend the most care on it.
|
|
15
|
+
|
|
16
|
+
- **The first frame is a picture, not a title.** Open on the subject, the result or the problem itself: a generated picture, a clip, the user's own screenshot or footage, or one striking object. Text alone is never the first shot. Something must move within the first second.
|
|
17
|
+
- **It works with the sound off.** The picture and a few words on screen carry the promise without the voice; many viewers never turn the sound on.
|
|
18
|
+
- **The spoken hook is twelve words or fewer** and starts at once. No greeting, no logo sting, no "in this video", no name of the product before the reason to care.
|
|
19
|
+
- **Picture, voice and text each do a different job.** The picture shows the claim, the voice adds the tension, the on-screen text is the keyword. Never have all three say the same sentence.
|
|
20
|
+
- **The promise is made in the first sentence and kept.** By three seconds the viewer knows what they will get; for a product, it is on screen by five. The video then delivers exactly that. A hook the body does not pay off loses the viewer and their trust.
|
|
21
|
+
|
|
22
|
+
### Hook patterns
|
|
23
|
+
Pick the one that fits what is true about the idea. Each needs its picture as much as its words.
|
|
24
|
+
- **Result first.** Show the finished thing, then how. "Four hundred invoices, sent before coffee."
|
|
25
|
+
- **The wrong belief.** Name what people assume, then turn it. "Your starter is not dying. It is hungry."
|
|
26
|
+
- **The problem, shown.** Open on the mess with no explanation for a second. A calendar with forty meetings in one week.
|
|
27
|
+
- **A specific number.** "Three taps replace your morning checklist." Only a number the user gave you.
|
|
28
|
+
- **The odd image.** Something that should not be there, then the question it raises. A paper map on a car dashboard: "Why is this still in your glove box?"
|
|
29
|
+
- **The open question.** Ask what the last scene answers, and do not answer it until then.
|
|
30
|
+
- **Before and after.** Cut between the two states before saying a word about the product.
|
|
31
|
+
- **What it costs.** What the viewer loses every week by not knowing this. Only when it is true.
|
|
32
|
+
- **Start in the middle.** Open at the moment of trouble, then go back. "At two in the morning the site was down and I was the only one awake."
|
|
33
|
+
- **Already happening.** Start mid-action in the interface: the cursor is already drawing the chart.
|
|
12
34
|
|
|
13
35
|
## Structure
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
Choose the shape by what the video is.
|
|
37
|
+
- **Product or app demo:** the problem in one beat, the product doing the thing for real, the result. Show the action, not a description of it.
|
|
38
|
+
- **How something works:** a surprising claim, then cause and effect step by step, then the answer to the opening.
|
|
39
|
+
- **Tips or a list:** the strongest or most surprising item first, or build up to it. Never open with the weakest.
|
|
40
|
+
- **A story:** setup, a turn, what changed because of it.
|
|
41
|
+
- **An announcement:** the news in the first sentence, then why it matters to the viewer, then what to do.
|
|
42
|
+
|
|
43
|
+
Whatever the shape:
|
|
44
|
+
- **Every scene follows from the one before with "but" or "so", never "and then".** If two scenes could swap places, one of them is not needed.
|
|
45
|
+
- **Raise a question early and answer it last.** The answer is the payoff; put the biggest picture of the video there.
|
|
46
|
+
- **Re-hook twice.** Around forty percent and seventy percent of the way through, give the viewer a new reason to stay: a turn, a number, a reveal, a second question.
|
|
47
|
+
- **Be concrete.** At least half the scenes name a real number, a real object or a real moment. "Faster" and "better" are not content.
|
|
48
|
+
- **Something new on screen every two to three seconds:** a new shot, a new element, a camera move. The plan's `notes` should say what changes.
|
|
49
|
+
- **Mix what the scenes are made of.** A picture, a clip, the interface, type: never more than two scenes in a row of the same kind. A video of only text and shapes reads as a slideshow, so plan a picture or a clip for a third to half of the scenes, the opening first (see `reference/scene-treatments.md` and `reference/clips.md`).
|
|
50
|
+
- **End on the payoff, then one action** in five words or fewer. Where it fits, return to the opening image, changed.
|
|
17
51
|
|
|
18
52
|
## Voice
|
|
19
53
|
- Write for the ear: short sentences, contractions, plain words. Read it aloud in your head.
|
|
@@ -34,6 +34,17 @@ A few well-placed sounds make motion feel real. Too many make a video tiring. Th
|
|
|
34
34
|
- Never let two loud sounds overlap. Stagger them.
|
|
35
35
|
- When the same kind of sound repeats, grade it: the first at full level, the rest quieter.
|
|
36
36
|
|
|
37
|
+
## Background music
|
|
38
|
+
- A video with little or no voice needs a track; a voice-led video usually gains from a quiet one. Search by mood and use: `reelkit assets search "clean airy product launch" --kind music`, `"lo-fi calm"`, `"driving phonk"`, `"epic cinematic build"`, `"minimal tech under a voiceover"`. Each result says its mood and tempo.
|
|
39
|
+
- `reelkit assets pull <id>`; the pull prints the path. Place it once, outside the scenes, so it runs across the cuts: `<Sfx src={urls["<path the pull printed>"]} at={0} volume={0.12} />`.
|
|
40
|
+
- Under a voiceover the music sits at 0.08 to 0.15. With no voice it carries the video at 0.4 to 0.6, and the effects come down to 0.2 to 0.35 so they sit inside it.
|
|
41
|
+
- Library tracks are about 30 seconds. For a longer video place the track again on a scene change, where a transition sound covers the join.
|
|
42
|
+
- One track for the whole video. Pick the tempo first: scene changes on a grid of the beat (60 / bpm seconds, or two or four of them) make ordinary cuts feel designed.
|
|
43
|
+
- Never use the music of a reference or of any video the user points at. Describe its mood and tempo and search the library with that.
|
|
44
|
+
|
|
45
|
+
## The interface pack
|
|
46
|
+
The library holds a matched set made for interface motion; search these words with `--kind sfx`: "soft ui whoosh" and "fast whip" for moves, "bubble pop" and "glass tap" for things appearing and being pressed, "keyboard typing" under a typed field, "toggle switch", "message sent", "notification ping", "success chime" on a tick, "sparkle shimmer" on an AI result, "counter ticks" under a number counting up, "soft impact" and "sub bass drop" for the landing and the payoff, "short riser" into a reveal, "camera shutter" on a freeze. Sounds from one set sit together; mix sets only when one is missing what you need.
|
|
47
|
+
|
|
37
48
|
## Fit
|
|
38
49
|
- Match the sound to the look: soft pops and gentle whooshes for calm, friendly videos; glitches and bass hits for tech and high energy; camera and tape sounds for retro.
|
|
39
50
|
- Skip anything startling (gunshots, screams, alarms) unless the topic is literally about it.
|
|
@@ -56,6 +56,15 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
56
56
|
### 10. Logo sting (either mode)
|
|
57
57
|
**When:** an opener or end card, from the user's own logo file. **How:** build toward the logo from its own shapes and colours and land on it exactly as supplied, held at least 1.5 seconds. **Forbidden:** redrawing, recolouring or stretching the logo; anyone else's logo or trademark.
|
|
58
58
|
|
|
59
|
+
### 11. Launch film (restrained)
|
|
60
|
+
**When:** a product, app or feature launch for a tech, software or AI brand: the short film that opens a launch post. **How:** a clean off-white or near-black ground with one soft pool of the hero colour at an edge (search "glow backdrop"); one object in the middle of each shot and a lot of empty space around it. Open on a single line of small, calm type that comes into focus word by word (search "blur reveal"), then one idea per shot, each 1.5 to 2.5 seconds: a field being typed into (search "search bar", "prompt box"), the real screenshot on a card floating at a slight tilt (search "tilt card", "laptop mockup", "phone mockup"), tiles circling a short line (search "orbit"), a feature grid (search "bento"), a pointer that clicks through it (search "cursor click"), the one real number as the payoff (search "statistic counter"), and the logo small and centred to close (search "logo reveal", "brand splash"). Between shots the outgoing object leaves fast with a short directional blur and the next one arrives on a heavy spring; the camera pushes in 2 to 4 percent on every shot. Six to eight shots in 12 to 20 seconds, music without a voice or with very few words. This look opens small on purpose: the first object may fill only a fifth of the frame, as long as the push carries it larger. **Forbidden:** two objects competing in one shot, a busy or textured background, a second accent colour, an invented screen, a shot that holds still, and any brand's mark but the user's own.
|
|
61
|
+
|
|
62
|
+
### 12. Social native (showreel)
|
|
63
|
+
**When:** the video should feel like it was made inside the app it is posted to: a reply to a comment, a reaction, proof from the user's own account. **How:** the picture is footage or a generated image and the graphics are the platform's own furniture, used honestly: the comment being answered at the top (search "comment sticker"), a post or a story in its frame (search "post frame", "story frame"), the reel's own buttons over the picture (search "reel overlay"), a profile with a Follow button to close (search "profile card"), reactions floating up on the payoff (search "emoji reactions", "emoji pop"), and words punching in one at a time on the voice (search "punch words"). Emoji are allowed in this look, as reactions and stickers only. **Forbidden:** an invented comment, post, follower count or like count; a platform's logo when the video is not about that platform; emoji standing in for a logo or an icon.
|
|
64
|
+
|
|
65
|
+
### 13. AI at work (restrained)
|
|
66
|
+
**When:** an AI product, an assistant or an automation: the viewer should watch it think and deliver. **How:** near-black ground; the request typed into a glowing field and sent (search "prompt box", "assistant chat window"); the work shown as steps that spin and tick (search "agent steps") or as a placeholder that resolves into the result (search "shimmer skeleton"); the answer streaming in (search "streaming answer"); a calm orb when it listens or speaks (search "glow orb"). One glow, on the field or the orb, never both in one frame. Every answer on screen is the product's real output, supplied by the user. **Forbidden:** an answer the product did not give, a second glowing element, sparkles as decoration, text that streams faster than it can be read.
|
|
67
|
+
|
|
59
68
|
## Your own
|
|
60
69
|
Ask what the message is, what happens on screen, the colours, the length and shape, and for an example if they have one. Choose the nearest look above as the technical base and write a four-line brief in the same shape (when, how, forbidden, structure). Show the brief with the plan at the checkpoint.
|
|
61
70
|
|
package/src/cli.ts
CHANGED
|
@@ -77,7 +77,7 @@ assets.command("upload <file>").description("Add one of your own files to this p
|
|
|
77
77
|
.action(run((ctx, file: string, opts: Parameters<typeof assetsUpload>[2]) => assetsUpload(ctx, file, opts)));
|
|
78
78
|
assets.command("search <query>").description("Search the shared library by meaning; each result shows how well it fits").option("--kind <kind>", "image, overlay, sfx, music, component or clip").option("--limit <n>", "how many results")
|
|
79
79
|
.action(run(assetsSearch));
|
|
80
|
-
assets.command("pull <id>").description("Download a library item into this project").option("--scene <sceneId>", "use it as this scene's image or clip").option("--force", "replace a component file that already exists")
|
|
80
|
+
assets.command("pull <id>").description("Download a library item into this project").option("--scene <sceneId>", "use it as this scene's image or clip").option("--force", "replace a component file that already exists").option("--music", "make this music item the video's track (its tempo is measured and scene changes land on its beat)")
|
|
81
81
|
.action(run((ctx, id, opts) => assetsPull(ctx, id, opts)));
|
|
82
82
|
assets.command("voices").description("List the narration voices").action(run(assetsVoices));
|
|
83
83
|
assets.command("voiceover").description("Record the narration from plan.json").option("--scene <sceneId>", "one scene").option("--all", "every scene").option("--redo", "record again even if it exists")
|
|
@@ -106,7 +106,7 @@ program.command("plan").description("Work with plan.json").command("check").desc
|
|
|
106
106
|
|
|
107
107
|
program.command("check").description("Check the composition in src/ without rendering").action(run(check));
|
|
108
108
|
program.command("install").description("Install the Reelkit skill into your coding agents").option("--agent <id>", "claude, codex, cursor, gemini, agents or all").option("--force", "reinstall even when up to date").action(run((ctx, opts) => install(ctx, opts)));
|
|
109
|
-
program.command("preview").description("Render
|
|
109
|
+
program.command("preview").description("Render preview frames into out/preview/: two per scene (at 30% and 90%) and the frames either side of each scene change, with a report of what carries across").action(run((ctx) => preview(ctx)));
|
|
110
110
|
program.command("render").description("Render the video to out/video.mp4").action(run((ctx) => render(ctx)));
|
|
111
111
|
|
|
112
112
|
// Parses argv and runs the chosen command. Importing this module parses nothing; bin/reelkit.mjs calls main.
|
package/src/commands/assets.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { GreenTooDullError, keyGreen } from "../project/chromakey";
|
|
|
9
9
|
import { buildManifest, voiceoverStale, type ClipRecord, type Voiceover } from "../project/manifest";
|
|
10
10
|
import { loadPlan } from "./plan";
|
|
11
11
|
import { FILE_NAME } from "../render/validate";
|
|
12
|
+
import { measureMusic, type MusicRecord } from "../project/music";
|
|
12
13
|
import { probeFile } from "../project/probe";
|
|
13
14
|
import { FILES, openProject, type Project } from "../project/project";
|
|
14
15
|
|
|
@@ -228,7 +229,7 @@ function setSceneClip(project: Project, sceneId: string, record: ClipRecord) {
|
|
|
228
229
|
project.writeJson(FILES.clips, { ...project.readJsonOr<Record<string, ClipRecord>>(FILES.clips, {}), [sceneId]: record });
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
export async function assetsPull(ctx: Ctx, id: string, opts: { scene?: string; force?: boolean }, deps: ClipDeps = {}): Promise<Result> {
|
|
232
|
+
export async function assetsPull(ctx: Ctx, id: string, opts: { scene?: string; force?: boolean; music?: boolean }, deps: ClipDeps = {}): Promise<Result> {
|
|
232
233
|
const project = openProject(ctx.cwd);
|
|
233
234
|
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) return { ok: false, summary: `"${id}" is not a library id. Copy the id from \`reelkit assets search\`.` };
|
|
234
235
|
if (opts.scene) {
|
|
@@ -240,7 +241,9 @@ export async function assetsPull(ctx: Ctx, id: string, opts: { scene?: string; f
|
|
|
240
241
|
return { ok: false, summary: `Scene ${opts.scene} is not an illustration scene in plan.json. ${illustrations.length ? `The illustration scenes are: ${illustrations.join(", ")}.` : "The plan has no illustration scenes."}${clipScenes.length ? ` The clip scenes are: ${clipScenes.join(", ")}.` : ""}` };
|
|
241
242
|
}
|
|
242
243
|
}
|
|
244
|
+
if (opts.music && opts.scene) return { ok: false, summary: "--music makes the item the video's track, not a scene's picture. Use --music or --scene, not both." };
|
|
243
245
|
const pulled = await client(ctx)("libraryPull", { id });
|
|
246
|
+
if (opts.music && pulled.item.kind !== "music") return { ok: false, summary: `${id} is ${pulled.item.kind}, not music, so it cannot be the video's track. Search with --kind music.` };
|
|
244
247
|
if (unsafeName(pulled.filename, pulled.item.kind)) return { ok: false, summary: `The library returned an unsafe file name for ${id}, so nothing was written. Report this item.` };
|
|
245
248
|
if (opts.scene) {
|
|
246
249
|
const treatment = loadPlan(project).scenes.find((x) => x.id === opts.scene)?.treatment;
|
|
@@ -273,16 +276,36 @@ export async function assetsPull(ctx: Ctx, id: string, opts: { scene?: string; f
|
|
|
273
276
|
} else setSceneImage(project, opts.scene, path);
|
|
274
277
|
if (project.exists(FILES.plan)) note = tryManifest(project, loadPlan(project)).note;
|
|
275
278
|
}
|
|
279
|
+
if (pulled.item.kind === "music" && opts.music) return pullMusic(project, id, pulled.item.title, path);
|
|
280
|
+
const musicHint = pulled.item.kind === "music" ? ` To make it the video's track, with scene changes on its beat, run \`reelkit assets pull ${id} --music\`.` : "";
|
|
276
281
|
const refs = keyedPath ? ` The original is urls["${path}"]; the keyed copy with a transparent background is urls["${keyedPath}"].` : ` Reference it in the composition as urls["${path}"].`;
|
|
277
282
|
return {
|
|
278
283
|
ok: true, data: { path, kind: pulled.item.kind, ...(keyedPath ? { keyedPath } : {}) },
|
|
279
284
|
summary: [
|
|
280
|
-
opts.scene ? `Pulled ${id} as the ${sceneIsClip ? "clip" : "image"} for scene ${opts.scene}.${keyedPath ? refs : ""}` : `Pulled ${id} to ${path}.${refs}`,
|
|
285
|
+
opts.scene ? `Pulled ${id} as the ${sceneIsClip ? "clip" : "image"} for scene ${opts.scene}.${keyedPath ? refs : ""}` : `Pulled ${id} to ${path}.${refs}${musicHint}`,
|
|
281
286
|
...(note ? [note] : []),
|
|
282
287
|
].join("\n"),
|
|
283
288
|
};
|
|
284
289
|
}
|
|
285
290
|
|
|
291
|
+
// Makes a pulled track the video's one music track: measured here, saved in assets/music.json, and the timeline is laid out again around it.
|
|
292
|
+
async function pullMusic(project: Project, id: string, title: string, path: string): Promise<Result> {
|
|
293
|
+
let measured: Awaited<ReturnType<typeof measureMusic>>;
|
|
294
|
+
try { measured = await measureMusic(project.path(path)); }
|
|
295
|
+
catch (e) { return { ok: false, summary: `${id} was downloaded to ${path} but could not be read as audio (${e instanceof Error ? e.message : String(e)}). Pull a different track.` }; }
|
|
296
|
+
const previous = project.readJsonOr<MusicRecord | undefined>(FILES.music, undefined);
|
|
297
|
+
const record: MusicRecord = { key: path, id, title, ...measured, gainDb: 0 };
|
|
298
|
+
project.writeJson(FILES.music, record);
|
|
299
|
+
const note = project.exists(FILES.plan) ? tryManifest(project, loadPlan(project)).note : undefined;
|
|
300
|
+
const beat = record.bpm
|
|
301
|
+
? `about ${Math.round(record.bpm)} BPM; scene changes will land on its beat.`
|
|
302
|
+
: "no clear tempo was found, so scene changes stay where the narration puts them.";
|
|
303
|
+
return {
|
|
304
|
+
ok: true, data: { path, kind: "music", music: record },
|
|
305
|
+
summary: [`Pulled ${id} as the video's music track${previous && previous.id !== id ? ` (it replaces ${previous.id})` : ""}: ${record.durationSec.toFixed(1)}s, ${beat} Place it once with <Music src={urls["${path}"]} />.`, ...(note ? [note] : [])].join("\n"),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
286
309
|
export async function assetsVoices(ctx: Ctx): Promise<Result> {
|
|
287
310
|
const { voices } = await client(ctx)("voices", {});
|
|
288
311
|
return {
|