reelkit-cli 0.6.0 → 0.10.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 +5 -3
- package/package.json +52 -9
- package/skill/SKILL.md +40 -19
- package/skill/THIRD_PARTY.md +104 -2
- package/skill/commands/launch-film.md +7 -0
- package/skill/reference/art-styles.md +70 -0
- package/skill/reference/asset-reuse.md +13 -2
- package/skill/reference/backgrounds.md +63 -0
- package/skill/reference/beat-sync.md +25 -19
- package/skill/reference/brand-motion.md +62 -0
- package/skill/reference/captions.md +11 -5
- package/skill/reference/clips.md +3 -3
- package/skill/reference/component-authoring.md +1 -1
- package/skill/reference/continuity.md +26 -7
- package/skill/reference/delivery-review.md +40 -0
- package/skill/reference/hebrew-rtl.md +3 -4
- package/skill/reference/{remotion-composition.md → hyperframes-composition.md} +18 -11
- package/skill/reference/kit.md +144 -12
- package/skill/reference/launch-film.md +194 -0
- package/skill/reference/motion-design.md +18 -16
- package/skill/reference/scene-treatments.md +20 -0
- package/skill/reference/scriptwriting.md +4 -1
- package/skill/reference/sound-design.md +34 -12
- package/skill/reference/studio-editing.md +55 -0
- package/skill/reference/styles.md +9 -6
- package/skill/reference/three-d.md +135 -0
- package/skill/reference/voice-sync.md +108 -0
- package/src/agents.ts +23 -12
- package/src/api/client.ts +4 -1
- package/src/cli.ts +18 -7
- package/src/commands/assets.ts +314 -30
- package/src/commands/build.ts +148 -35
- package/src/commands/init.ts +1 -1
- package/src/commands/install.ts +1 -1
- package/src/commands/plan.ts +8 -5
- package/src/commands/ref.ts +5 -2
- package/src/contract/index.ts +5 -3
- package/src/hyperframes/Root.tsx +1 -0
- package/src/hyperframes/fonts.ts +54 -0
- package/src/hyperframes/frame.tsx +46 -0
- package/src/hyperframes/host.tsx +38 -0
- package/src/hyperframes/kit/Assemble3D.tsx +92 -0
- package/src/hyperframes/kit/BrandTransform3D.tsx +12 -0
- package/src/hyperframes/kit/BrowserFrame.tsx +83 -0
- package/src/{remotion → hyperframes}/kit/Camera.tsx +7 -5
- package/src/{remotion → hyperframes}/kit/Captions.tsx +34 -18
- package/src/hyperframes/kit/Card3D.tsx +211 -0
- package/src/{remotion → hyperframes}/kit/Carry.tsx +1 -1
- package/src/hyperframes/kit/ChapterFrame.tsx +68 -0
- package/src/{remotion → hyperframes}/kit/ClipLayer.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Counter.tsx +1 -1
- package/src/hyperframes/kit/CounterRoll.tsx +75 -0
- package/src/{remotion → hyperframes}/kit/Entrance.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/FootageLayer.tsx +1 -1
- package/src/hyperframes/kit/GlassPanel.tsx +43 -0
- package/src/hyperframes/kit/Grounds.tsx +177 -0
- package/src/hyperframes/kit/Headline.tsx +97 -0
- package/src/hyperframes/kit/Hero3D.tsx +197 -0
- package/src/hyperframes/kit/HudOverlay.tsx +52 -0
- package/src/hyperframes/kit/ImageLayers.tsx +48 -0
- package/src/{remotion → hyperframes}/kit/KenBurnsImage.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/KeyedClip.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Layers.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/LowerThird.tsx +1 -1
- package/src/hyperframes/kit/Music.tsx +19 -0
- package/src/hyperframes/kit/NamedCursor.tsx +54 -0
- package/src/hyperframes/kit/Orbit3D.tsx +49 -0
- package/src/hyperframes/kit/Particles3D.tsx +74 -0
- package/src/hyperframes/kit/Place.tsx +12 -0
- package/src/hyperframes/kit/PromptBox.tsx +84 -0
- package/src/hyperframes/kit/Scene3D.tsx +70 -0
- package/src/hyperframes/kit/SceneFrame.tsx +96 -0
- package/src/{remotion → hyperframes}/kit/ScreenOverlay.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Sfx.tsx +1 -1
- package/src/hyperframes/kit/SoundCues.tsx +22 -0
- package/src/hyperframes/kit/TerminalLog.tsx +98 -0
- package/src/hyperframes/kit/Text3D.tsx +78 -0
- package/src/hyperframes/kit/TextOnImage.tsx +41 -0
- package/src/{remotion → hyperframes}/kit/TitleCard.tsx +1 -1
- package/src/{remotion → hyperframes}/kit/Voiceover.tsx +1 -1
- package/src/hyperframes/kit/Warp3D.tsx +59 -0
- package/src/hyperframes/kit/bg-math.ts +179 -0
- package/src/hyperframes/kit/brand-transform.ts +25 -0
- package/src/{remotion → hyperframes}/kit/caption-groups.ts +7 -3
- package/src/hyperframes/kit/caption-style.ts +45 -0
- package/src/hyperframes/kit/docs.ts +249 -0
- package/src/hyperframes/kit/image-layers-math.ts +115 -0
- package/src/hyperframes/kit/index.ts +74 -0
- package/src/hyperframes/kit/inter-bold-typeface.ts +3 -0
- package/src/{remotion → hyperframes}/kit/motion-math.ts +36 -2
- package/src/hyperframes/kit/music-math.ts +59 -0
- package/src/hyperframes/kit/quiet-three.ts +11 -0
- package/src/hyperframes/kit/sample-text.ts +55 -0
- package/src/hyperframes/kit/scene3d-context.ts +5 -0
- package/src/hyperframes/kit/seeded.ts +13 -0
- package/src/hyperframes/kit/sound-cues.ts +89 -0
- package/src/hyperframes/kit/sound-kinds.ts +135 -0
- package/src/{remotion → hyperframes}/kit/theme.ts +43 -39
- package/src/hyperframes/kit/three-fx-math.ts +192 -0
- package/src/hyperframes/kit/three-math.ts +145 -0
- package/src/hyperframes/kit/transition-math.ts +116 -0
- package/src/hyperframes/kit/ui-math.ts +145 -0
- package/src/hyperframes/kit/ui-theme.ts +25 -0
- package/src/hyperframes/kit/word-anchor.ts +107 -0
- package/src/hyperframes/math.ts +62 -0
- package/src/hyperframes/three.tsx +10 -0
- package/src/pipeline/beatsnap.ts +72 -0
- package/src/pipeline/review.ts +44 -10
- package/src/pipeline/schema.ts +51 -4
- package/src/pipeline/timing.ts +27 -1
- package/src/project/background.ts +33 -0
- package/src/project/chromakey.ts +1 -1
- package/src/project/layers.ts +60 -0
- package/src/project/manifest.ts +59 -14
- package/src/project/music.ts +19 -5
- package/src/project/project.ts +4 -1
- package/src/project/serve.ts +2 -2
- package/src/project/soundreport.ts +347 -0
- package/src/project/svgcheck.ts +21 -0
- package/src/render/component-preview.ts +11 -55
- package/src/render/contact-sheet.ts +39 -0
- package/src/render/continuity.ts +14 -4
- package/src/render/deps.ts +15 -3
- package/src/render/render.ts +62 -57
- package/src/render/serve.ts +31 -0
- package/src/render/sound-notes.ts +106 -0
- package/src/render/static-check.ts +15 -4
- package/src/render/validate.ts +4 -4
- package/src/render/word-check.ts +181 -0
- package/src/render/worker.ts +71 -0
- package/src/testing/conformance.ts +12 -0
- package/src/testing/fake-api.ts +4 -4
- package/src/testing/fixtures.ts +4 -1
- package/src/remotion/Root.tsx +0 -31
- package/src/remotion/kit/Music.tsx +0 -19
- package/src/remotion/kit/SceneFrame.tsx +0 -19
- package/src/remotion/kit/docs.ts +0 -124
- package/src/remotion/kit/index.ts +0 -29
- package/src/remotion/kit/music-math.ts +0 -42
- /package/src/{remotion → hyperframes}/kit/Icon.tsx +0 -0
- /package/src/{remotion → hyperframes}/kit/beat.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/brand-icons.ts +0 -0
- /package/src/{remotion → hyperframes}/kit/media.ts +0 -0
- /package/src/{remotion → hyperframes}/types.ts +0 -0
|
@@ -22,6 +22,26 @@ description: Use when choosing each scene's visual treatment and writing image p
|
|
|
22
22
|
- shareable is false when the prompt depends on the user's brand, product, name, place or uploads, and whenever imagePrompt is null.
|
|
23
23
|
- imageTags: 3 to 6 short lowercase tags (subject, style, mood). Empty when imagePrompt is null.
|
|
24
24
|
|
|
25
|
+
## An image scene is built in layers
|
|
26
|
+
|
|
27
|
+
A picture with words on it is built in layers, not as a flat picture with a caption somewhere over it: the picture at the back, the words in the middle, and the subject of the picture in front when it has one. Then the headline can pass behind the person or the object, and everything drifts a little at its own rate.
|
|
28
|
+
|
|
29
|
+
- Put the words in the picture's calm zone with `TextOnImage` inside `ImageLayers`. Never on a flat card beside the picture and never over its busiest part. The calm zone, how light it is and how busy it is are measured on this machine for free whenever a picture is registered (generated, pulled or uploaded) and are in the manifest scene as `imageLayers.textZone`.
|
|
30
|
+
- For the one or two hero pictures that carry the film, run `reelkit assets layers --scene <id>` (or add `--layers` to `assets gen image` or `assets pull --scene`). It cuts the subject out as `<name>.subject.png` and puts it in `imageLayers`. It sends the picture to the server as a one-second video and uses about one second of the monthly cutout quota, so it is not for every picture. A picture with no clear subject (the cut-out covers under 3 percent or over 85 percent) stays flat and the command still succeeds: the words still go in its calm zone.
|
|
31
|
+
- When the scene has on-screen text, the image prompt gets one fixed sentence appended for you: one clear subject, with calm empty space on one side where words can sit. Write prompts that ask for a single subject too.
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
<SceneFrame from={s.startFrame} durationInFrames={s.durationFrames}>
|
|
35
|
+
<ImageLayers layers={s.imageLayers} back={urls[s.imageKey!]}>
|
|
36
|
+
<TextOnImage layers={s.imageLayers}>
|
|
37
|
+
<Headline text="Take a breath" keyword="breath" emphasis="highlight" hero={palette.hero} />
|
|
38
|
+
</TextOnImage>
|
|
39
|
+
</ImageLayers>
|
|
40
|
+
</SceneFrame>
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`ImageLayers` works without a subject layer (one flat picture with the words over it), `behindSubject` on `TextOnImage` is chosen for you, and `reelkit check` says "the text is not a layer of the picture" when a scene has an image and words and the composition uses neither component.
|
|
44
|
+
|
|
25
45
|
## User assets
|
|
26
46
|
- Only reference assets listed in `assets/index.json`, by id, in userAssetIds.
|
|
27
47
|
- Put a logo in the first or last scene. Put a screenshot in the scene that talks about what it shows.
|
|
@@ -13,7 +13,7 @@ description: Use when writing the spoken script for a short social video - the o
|
|
|
13
13
|
## The opening decides the video
|
|
14
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
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.
|
|
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. A voiceless launch film is the exception and follows `reference/launch-film.md` instead: its opening is type and motion, with sound from the first frame.
|
|
17
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
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
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.
|
|
@@ -76,3 +76,6 @@ Each of the user's files in `assets/index.json` has a description of what it sho
|
|
|
76
76
|
## On-screen text
|
|
77
77
|
- onScreenText is what appears on screen, not the narration. Two to five words per item, at most three items per scene.
|
|
78
78
|
- It should reinforce the spoken point (a keyword, a number, a label), never repeat the sentence.
|
|
79
|
+
|
|
80
|
+
## Show, do not state
|
|
81
|
+
A claim written as a sentence is the weakest way to make it. Give each claim a small interface that performs it (a typed question and its answer, a terminal that works, a number that rolls) and keep the words to a headline of two to five with one emphasised keyword. Open with the user's own question when the product answers questions. Say which claims are highlighted and which are only mentioned, at most three highlighted in 30 seconds.
|
|
@@ -5,16 +5,16 @@ description: Use when adding sound effects to a video - which moments get a soun
|
|
|
5
5
|
|
|
6
6
|
# Sound design
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Sound gives selected actions weight and shapes the story. Density follows the material: calm speech may need almost no effects; an energetic graphic sequence may use many small cues. Spoken words take priority whenever speech is present.
|
|
9
9
|
|
|
10
10
|
## What gets a sound
|
|
11
11
|
- **Entrances that matter:** the hook's first hit, each numbered point arriving, the payoff. A whoosh, swipe or soft impact.
|
|
12
12
|
- **Scene changes:** a short transition sound across the cut.
|
|
13
13
|
- **Interface moments:** a notification arriving, a button tap, a tick appearing. A click, pop or chime.
|
|
14
14
|
- **Build-ups:** a riser into the payoff or the final call to action.
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
15
|
+
- Choose the important actions rather than sounding every decorative element. A dense interface sequence can use quiet ticks; a personal story can rely on voice and silence.
|
|
16
|
+
- Keep a family of sounds and reserve the strongest hits for the main moments. Whooshes support movement; they should not cover its landing or a spoken word.
|
|
17
|
+
- Use a boom when the reveal needs weight, not at every cut. A deliberate quiet beat can make the next arrival stronger.
|
|
18
18
|
|
|
19
19
|
## Finding sounds
|
|
20
20
|
1. `reelkit assets search "<description>" --kind sfx` with what you want to hear: "whoosh", "soft click", "notification pop", "riser", "camera shutter", "bass impact". Add "one-shot" or "short" to the query for one-shots.
|
|
@@ -23,25 +23,43 @@ A few well-placed sounds make motion feel real. Too many make a video tiring. Th
|
|
|
23
23
|
4. Reuse one whoosh and one click across the video rather than a different sound each time. Consistency sounds designed.
|
|
24
24
|
|
|
25
25
|
## Timing
|
|
26
|
-
-
|
|
26
|
+
- Align the audible peak with the visual landing. A short UI whoosh may need a 2 to 3 frame lead; a reverse swell may need much more. Inspect the actual file's attack and tail rather than using one lead for every sound.
|
|
27
27
|
- Put impacts and clicks on the exact frame the visual hits.
|
|
28
28
|
- Start a riser so that it **ends** on the reveal: `at = revealFrame - durationSec * fps`.
|
|
29
29
|
- Use the word timings in `s.words` when a sound belongs to a spoken word.
|
|
30
30
|
|
|
31
31
|
## Volume
|
|
32
|
-
- The voiceover plays at 1.
|
|
32
|
+
- The voiceover plays at 1. While narration is heard, sound effects stay at 0.3 or below. Without narration, effects can sit between 0.2 and 0.45 and a chosen hook impact can reach 0.6.
|
|
33
33
|
- Ambience and drone beds: 0.06 to 0.12.
|
|
34
|
-
-
|
|
34
|
+
- Overlap only deliberately; avoid summed peaks that clip or mask the speech. Lower competing layers before mastering.
|
|
35
35
|
- When the same kind of sound repeats, grade it: the first at full level, the rest quieter.
|
|
36
36
|
|
|
37
37
|
## Background music
|
|
38
|
-
-
|
|
39
|
-
- `reelkit assets pull <id
|
|
40
|
-
-
|
|
41
|
-
-
|
|
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.
|
|
38
|
+
- Choose music from the story's tone and pace; a personal or documentary moment can work with no music. Search by genre, mood and use, usually with English library terms. Decide a fitting track and explain the reason rather than requiring the user to choose a playlist.
|
|
39
|
+
- `reelkit assets pull <id> --music` records the selected track and its beat grid. Place `<Music src={urls[manifest.music.key]} />` once outside the scenes. It applies the measured gain, ducks under speech, loops when needed, and fades at the end. Do not manually repeat the track with `Sfx`.
|
|
40
|
+
- Leave `Music` at its defaults initially; adjust `volume`, `duckTo` or a planned `dips` interval only after listening to the rendered mix. The music should support the brand reveal without covering its lock sound.
|
|
41
|
+
- One track is usually enough for a short film. Pick its tempo before beat-led scene timing, and read `reference/beat-sync.md`. Align the peak with the reveal, allow a rest before it, and give the ending a musical resolution. Under speech prefer instrumental passages; avoid lyrics competing with the message. The current `Music` component loops from the supplied file's start, so trim a derived copy if a different loop section or entry point is needed.
|
|
43
42
|
- 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
43
|
|
|
44
|
+
## Music direction by material
|
|
45
|
+
|
|
46
|
+
| Material | Starting search direction | What to listen for |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| Tech demo or energetic explainer | `minimal tech instrumental`, `tech house instrumental`, `glitch hop` | Clear rhythm, room for words, no long empty intro |
|
|
49
|
+
| Tutorial or calm routine | `chillhop instrumental`, `deep house soft`, `light acoustic` | Steady support without a melody fighting the explanation |
|
|
50
|
+
| Premium product or brand | `cinematic minimal`, `dark downtempo`, `luxury deep house` | Restraint and a reveal that earns its weight |
|
|
51
|
+
| Montage or fashion | `fashion house`, `UK garage instrumental`, `nu disco` | Phrases that match changes of action and framing |
|
|
52
|
+
| Personal story | `felt piano`, `documentary acoustic`, or silence | Emotional fit without over-scoring a small moment |
|
|
53
|
+
| Humor or playful graphics | `quirky funk`, `chiptune`, `comedy pizzicato` | Timing that supports the joke without dictating it |
|
|
54
|
+
|
|
55
|
+
These are starting terms, not required genres or BPM ranges. A supplied track takes precedence when the user wants it. Do not infer commercial rights from a song being downloadable or present in an editor; verify a relevant license if the destination requires it.
|
|
56
|
+
|
|
57
|
+
## Phone playback and verification
|
|
58
|
+
|
|
59
|
+
Do not judge music by its sub-bass alone. A track with audible percussion, melodic content or bass harmonics can survive small speakers better than a sub-only bed. Check speech intelligibility and whether the intended music still contributes with reduced bass or actual phone playback. Adjust relative levels and arrangement, not just the final loudness.
|
|
60
|
+
|
|
61
|
+
Verify the finished export for present stems, cue alignment, clipping and truncated tails (`reference/delivery-review.md`). Measurements can locate a problem but cannot prove emotional fit. Do not impose a universal frequency-energy percentage, hit count or volume ratio. If playback is unavailable, report the checks you performed without claiming to have listened.
|
|
62
|
+
|
|
45
63
|
## The interface pack
|
|
46
64
|
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
65
|
|
|
@@ -55,3 +73,7 @@ The library holds a matched set made for interface motion; search these words wi
|
|
|
55
73
|
<Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={12} volume={0.35} />
|
|
56
74
|
```
|
|
57
75
|
Inside a SceneFrame, `at` counts from the start of that scene.
|
|
76
|
+
|
|
77
|
+
## Branded transformations
|
|
78
|
+
|
|
79
|
+
Read `reference/brand-motion.md` when the move reveals a brand, carries an object, or turns the product toward the viewer. Use `brandTransformFrames` and `cuesFor([start, settle], "transform", { offset: scene.startFrame, voice: true, sweepFrames: 10 })` for one sweep and one lock. Adjust the sweep lead to the actual sound file. Keep sounds outside `Scene3D`, and add the scene offset once. Listen to the final video; a high hit count is not a quality target by itself.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: studio-editing
|
|
3
|
+
description: Choose an editing approach from the actual material: footage, screen recordings, graphics, mixed media, pacing, inserts and sound. Read for an open brief or an existing-video edit.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Choose the edit from the material
|
|
7
|
+
|
|
8
|
+
Understand the message, audience, intended action, emotional tone and strongest source moments before adding effects. Watch supplied footage when playback is available; otherwise inspect sampled frames, metadata and an accurate transcript and state that limitation. Read the entire transcript in context, including pauses and emphasis. Do not infer spoken words from a few stills.
|
|
9
|
+
|
|
10
|
+
Choose the direction and explain it briefly. Ask about a missing fact, audience or goal only if it changes the edit. A request to improve a particular shot should produce that revision, not a new briefing or an unsolicited replacement concept.
|
|
11
|
+
|
|
12
|
+
## Choose the mode
|
|
13
|
+
|
|
14
|
+
| Material and purpose | Starting approach | What would weaken it |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| Personal or calm talking head | Preserve expression and useful pauses; clean captions; occasional motivated punch-in; quiet music or none | Constant zooms, whooshes and cuts that remove emotional timing |
|
|
17
|
+
| Energetic explanation or opinion | Strong opening claim supported by evidence; emphasis cuts; a few illustrative inserts | Effects that obscure the argument or alter its meaning |
|
|
18
|
+
| Tutorial or screen recording | Real screen, callout on the relevant control, step tracker, readable hold after the action | Camera moves or captions covering the control being taught |
|
|
19
|
+
| Product or premium brand | Supplied identity, consistent type and materials, restrained object motion, purposeful sound | Generic fabricated UI, conflicting styles or perpetual logo spin |
|
|
20
|
+
| Narration carried by graphics | A visual metaphor per idea; word-timed emphasis; continuity through a recurring object, type or accent | Repeating one flat layout or animating every word equally |
|
|
21
|
+
| Fashion, montage or reveal without speech | Cut by action and musical phrasing; minimal text; peak on the reveal | Adding explanatory captions or a narrator without a reason |
|
|
22
|
+
|
|
23
|
+
These are starting points, not templates. Mixed media can change visual worlds at a change of meaning while retaining a thread. A consistent brand system can vary framing, depth and scale without changing its identity.
|
|
24
|
+
|
|
25
|
+
## Build the edit map
|
|
26
|
+
|
|
27
|
+
For a substantial new film, keep a compact table in the project notes: source time or scene, purpose, visible action, spoken trigger or beat, text and sound. Each insert needs a reason: explain a step, demonstrate a claim, reveal the result, or make an abstract idea visible. The first few seconds should make the premise clear; a hook can be a strong image or a question, not necessarily a loud effect. End with an action only when the video's goal calls for one.
|
|
28
|
+
|
|
29
|
+
Use real footage, screens and logos first. Use code for exact type, numbers, diagrams, UI interactions and brand motion. Generate imagery only when an illustrative or filmed subject is missing. Search the shared library before generation, check the allowance with `reelkit whoami`, and use an alternative when the allowance is exhausted or a result cannot be used. Do not repeatedly regenerate an asset to chase minor taste differences.
|
|
30
|
+
|
|
31
|
+
For a new concept, show the essential direction if the user needs to choose it. Earlier approval and an explicit instruction to edit or render remain valid. A routine fix does not restart approval.
|
|
32
|
+
|
|
33
|
+
## Footage in this CLI
|
|
34
|
+
|
|
35
|
+
Reelkit overlays graphics on registered footage; it is not a CapCut draft editor or an automatic transcript-based jump-cut tool. It does not provide a CapCut JSON writer, original-speech transcription import, or an edit-decision-list command. Do not invent those commands or replace someone's recorded speech with generated narration.
|
|
36
|
+
|
|
37
|
+
Register a derived working copy with `reelkit assets upload ./working.mp4 --footage --describe "what the footage shows"`. Preserve the original. The plan then uses `mode: "footage"` and every scene uses `treatment: "footage-overlay"`. The manifest uses the footage's dimensions, capped at 1920 on the long side, and its full duration. Use one `FootageLayer` below the graphics; its `muted` default is true. Set `muted={false}` when the original audio is wanted, and do not duplicate that audio as another track. For existing speech use `voice: "none"` to avoid generating a second voice; that setting means no generated narration, not that the source audio must be silent.
|
|
38
|
+
|
|
39
|
+
If the requested edit needs cuts, normalization or reframing, make a separate derived clip with available local video tools, verify its new source timing, and register that copy. If a needed operation is unavailable, identify the missing capability. Do not install or automate another editor merely because the inspiration skill uses one. Keep media paths stable and the sources reproducible.
|
|
40
|
+
|
|
41
|
+
Original-speech captions need accurate timestamps from the supplied transcript or an available transcription tool authorized to process that recording. The standard manifest's `s.words` comes from generated voiceovers; uploading footage does not populate it. Keep original-speech timings in a separate local source and explicitly implement and validate their use in the composition. Do not hand-edit the generated manifest or use generated narration timings for the speaker. If timings cannot be obtained, complete the feasible edit and report the caption limitation. Music ducking also needs these real speech spans: pass `Music` a `scenes` array of `{ startFrame, words }` using scene-local word seconds, or keep the music conservatively low through the recording. With empty manifest words the default music cannot detect source speech.
|
|
42
|
+
|
|
43
|
+
## Inserts and pacing
|
|
44
|
+
|
|
45
|
+
Prioritize the moments where the viewer needs help. A number pop should show the real number as it is said; a callout should point at the actual control; a step tracker should advance with the step. Avoid unrelated B-roll that merely looks attractive. Align narrated visuals with the actual word onset, not the nearest clip boundary (`reference/voice-sync.md`). Keep captions and inserts clear of faces and controls.
|
|
46
|
+
|
|
47
|
+
Cut dead air, false starts and repeats only when it preserves meaning and cadence. Do not remove all pauses. Use a punch-in for emphasis or reframing, not on every sentence. Do not stack it on a screen recording that already zooms. Reduce travel or lengthen a move if it smears or ghosts; a held frame is valid when the viewer needs to read, compare or feel the moment.
|
|
48
|
+
|
|
49
|
+
When source speed changes, update the entire clock. At speed factor r, source time t maps to output time t/r. For a trimmed segment beginning at source time a and output time b, map t to b + (t-a)/r. Recompute word events, inserts and SFX from the edited timeline; regenerate narration timings if generated narration changed. Choose music phrasing separately rather than stretching the finished mix to repair sync.
|
|
50
|
+
|
|
51
|
+
## Design and sound references
|
|
52
|
+
|
|
53
|
+
Read only what the edit needs: `reference/styles.md` for visual techniques, `reference/motion-design.md` for staging, `reference/brand-motion.md` for transformations, `reference/three-d.md` for depth, `reference/hebrew-rtl.md` for Hebrew, `reference/captions.md` for generated-speech captions, and `reference/sound-design.md` for scoring. These are choices that serve the video, not requirements to use every technique.
|
|
54
|
+
|
|
55
|
+
Useful transformations include a field becoming its result, a sketch resolving into a product, a number joining its chart, or a product turning to expose its relevant face. Use particles, text rings, a comic or a 3D environment only when that metaphor fits the message and chosen style. Keep the identity recognizable after the transformation and give it a readable hold.
|
|
@@ -5,9 +5,9 @@ description: Use before planning, to choose the video's look with the user - the
|
|
|
5
5
|
|
|
6
6
|
# Choosing the look
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Choose a visual language that fits the material and record it in the first scene's `notes`. Brand films usually benefit from one system; a narration-driven mixed-media film can change worlds by meaning while retaining a thread of type, accent or object. Read `reference/studio-editing.md` when the approach is still open.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use the user's chosen direction. Otherwise recommend a fitting look with a short reason; use the catalogue as technique references, not a mandatory menu. Offer alternatives when requested. Ask only about consequential missing information.
|
|
11
11
|
|
|
12
12
|
## Two modes
|
|
13
13
|
Every look belongs to one mode. The mode decides which rules in `reference/motion-design.md` bend.
|
|
@@ -19,8 +19,8 @@ Every look belongs to one mode. The mode decides which rules in `reference/motio
|
|
|
19
19
|
|
|
20
20
|
**Showreel** (openers, announcements, anything that must stop the scroll)
|
|
21
21
|
- Hard cuts on the beat between full-colour backgrounds; type that fills the frame. Flat solid colour fields are right here.
|
|
22
|
-
-
|
|
23
|
-
-
|
|
22
|
+
- Camera movement and brief impact shakes can reinforce selected hits. Keep reading moments stable.
|
|
23
|
+
- Pace changes by meaning and musical phrasing, allowing deliberate holds rather than moving every frame.
|
|
24
24
|
- At most three full-screen flashes in any second, and never a red strobe. This is a safety rule, not a taste rule.
|
|
25
25
|
|
|
26
26
|
## The catalogue
|
|
@@ -57,7 +57,7 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
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
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,
|
|
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, an unreadable hold, and a third-party mark unrelated to the material.
|
|
61
61
|
|
|
62
62
|
### 12. Social native (showreel)
|
|
63
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.
|
|
@@ -65,8 +65,11 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
65
65
|
### 13. AI at work (restrained)
|
|
66
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
67
|
|
|
68
|
+
## Fifteen art styles (14 to 28)
|
|
69
|
+
`reference/art-styles.md` continues this list with fifteen looks whose picture is drawn in code rather than built from interface parts, adapted from the mg-styles-15 films: 14 flat vector, 15 line art, 16 isometric, 17 soft 3D render, 18 hand-drawn cel, 19 collage cut-out, 20 liquid, 21 shape morph, 22 Bauhaus grid, 23 synthwave, 24 aurora glass, 25 variety captions, 26 sticker explainer, 27 pixel art, 28 HUD. Use them as technique references, and read that file when the brief needs a style in that family (a drawn explainer, a line-art logo, an isometric city, "80s", "pixel", "glass", a kinetic poster). Each card there says which motion rules the look is allowed to bend.
|
|
70
|
+
|
|
68
71
|
## Your own
|
|
69
|
-
|
|
72
|
+
Use the supplied message, actions, colours, duration and format. Ask only for consequential missing details or a reference that the choice depends on. 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.
|
|
70
73
|
|
|
71
74
|
## What every look shares
|
|
72
75
|
- The plan checkpoint lists the exact words that will appear on screen and when. Fixing a list takes a minute; fixing a finished animation takes an hour.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: three-d
|
|
3
|
+
description: Use when a shot needs real depth - extruded type, a screen floating in space, a ring of cards, a camera that orbits or pulls back - and for deciding whether 3D earns its place at all.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 3D shots
|
|
7
|
+
|
|
8
|
+
The kit has a 3D layer on three.js: `Scene3D`, `Text3D`, `Card3D` and `Orbit3D`, and elements for the moments in a film that earn them: `Assemble3D`, `BrandTransform3D`, `Particles3D`, `Hero3D`, `Screen3D` and `Warp3D`. Everything is deterministic and driven by the frame. You import only from `reelkit/kit`; three itself is not importable from the composition.
|
|
9
|
+
|
|
10
|
+
## When 3D earns its place
|
|
11
|
+
|
|
12
|
+
One hero moment in a film: the product's name landing as thick letters, a screen floating in space and turning to face the viewer, a ring of features that the camera pushes through. That is what the extra depth is for.
|
|
13
|
+
|
|
14
|
+
It reads as a template when everything spins, when several objects each get their own 3D entrance, or when body text is extruded. Type that is read in a sentence stays 2D. One or two 3D shots usually provide enough contrast. A requested brand film or showreel can use more when each has a distinct purpose; keep sentences and detailed screens readable.
|
|
15
|
+
|
|
16
|
+
## Which element earns its place
|
|
17
|
+
|
|
18
|
+
Choose by what the moment has to say. For a brand transformation and its matching sound, read `reference/brand-motion.md`:
|
|
19
|
+
|
|
20
|
+
| The moment | Reach for | Why |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| A reveal: the name, a logo, a number of things arriving | `Assemble3D` (`shape="text"` for a word, `"grid"`, `"sphere"`, `"wall"`, `"ring"`) | Hundreds of blocks build into the thing; the piece counter (`assembledCount`) and the lock sound (`assembleEnd`) come with it |
|
|
23
|
+
| A brand or product arriving in its readable pose | `BrandTransform3D` (`mode="turn"`, `"arc"`, `"launch"`) | One deterministic arrival that holds neutral; `brandTransformFrames` gives the exact sound landing |
|
|
24
|
+
| An abstract idea: intelligence, flow, a network, "everything in one place" | `Particles3D` (`from` and `to` shapes, or `"text"`) | Light that changes shape reads as thought, and costs no artwork |
|
|
25
|
+
| The product itself | `Hero3D` (`kind="device"` with a screenshot, `"logo"`, `"coin"`, `"box"`) | A studio-lit object with clearcoat, two coloured rim lights and a soft floor |
|
|
26
|
+
| An interface carried across scenes | `Screen3D` with `poses` | One screen, re-posed at each scene, instead of a new mock each time |
|
|
27
|
+
| A fast change of place or a hyperspace beat | `Warp3D` as a ground, or `burst` as a 12 to 20 frame transition | Streaks from the vanishing point, additive |
|
|
28
|
+
|
|
29
|
+
Set the mood once on the `Scene3D` (`mood="studio"`, `"night"`, `"sunset"` or `"neon"`): it chooses the light colours, their intensities and the fog tint together, so nobody tunes lights by hand. Everything fits a 9:16 frame by default (`fit`, the share of the width at the closest the camera comes), and everything is a pure function of the frame and a `seed`.
|
|
30
|
+
|
|
31
|
+
HyperFrames captures 3D through software GL for consistent output. Render cost depends on the browser, resolution, geometry and other jobs on the machine; measure the actual shot instead of relying on timings from a previous renderer. Start with the count the picture needs and raise it only when the preview improves.
|
|
32
|
+
|
|
33
|
+
## How to layer it
|
|
34
|
+
|
|
35
|
+
Put a `Scene3D` inside a `SceneFrame`, over the 2D ground. Its background is transparent by default, so the `BgMesh` or flat colour behind shows through. Captions, small labels and logos stay 2D, in front of it. If the 3D shot is the whole scene, `Scene3D` is the only thing in the `SceneFrame` besides those labels.
|
|
36
|
+
|
|
37
|
+
## What the camera sees, in numbers
|
|
38
|
+
|
|
39
|
+
The default camera has a 50 degree field of view. At distance `d` from the object it sees `0.93 * d` units tall, and that times the frame's width over height across:
|
|
40
|
+
|
|
41
|
+
| Frame | Width seen at z 6 | At z 9 | At z 5.5 |
|
|
42
|
+
|---|---|---|---|
|
|
43
|
+
| 9:16 phone (aspect 0.5625) | 3.1 | 4.7 | 2.9 |
|
|
44
|
+
| 1:1 | 5.6 | 8.4 | 5.1 |
|
|
45
|
+
| 16:9 | 9.9 | 14.9 | 9.1 |
|
|
46
|
+
|
|
47
|
+
So on a phone frame the camera sees about 3 units across at z 6, not 5.5; a card 3 wide fills the whole frame, and a word at size 1.1 is cut off. You do not have to do this arithmetic: leave the sizes out and the kit does it. Distances count from the object, so an object at z 2 with the camera at z 8 is 6 away. The kit exports the same numbers as `visibleWidth(distance, aspect, fov?)` and `visibleHeight(distance, fov?)`.
|
|
48
|
+
|
|
49
|
+
## Sizes: leave them out
|
|
50
|
+
|
|
51
|
+
`Text3D` without `size` is sized from the measured width of its own letters so that it fills `fit` of the frame's width (default 0.8) at the closest the camera comes to it. "Closest" is read from the `Scene3D` camera keys: the key where it sees the least. A word is therefore whole at every moment, on any aspect, and exactly as big as it can be. A line break makes a second line: write `text={"Hyper\nFrames"}` in JSX, since `text="Hyper\nFrames"` passes a literal backslash and n; its height is held to `fit` of the frame's height. Giving `size` turns this off and you are back to doing the arithmetic yourself.
|
|
52
|
+
|
|
53
|
+
A push-in makes the word smaller at the start in proportion: from z 9 to z 6 it begins at two thirds of its final width. A start at about 25 percent further away than the end (z 7.5 to 6) reads as a gentle push; a larger one begins as a small word. Camera sideways moves (`x`) are not counted, so keep the word near the middle when you orbit.
|
|
54
|
+
|
|
55
|
+
`Orbit3D` without `radius` sizes the ring from its cards (the radius at which they stand side by side) and then scales the whole ring, radius and cards together, so that it lies inside `fit` of the frame (default 0.85) at the closest key, however it has turned and on any aspect. Set the cards' proportions with `width` and `height` and leave the rest. A `radius` you give wins and nothing is scaled. Pass `position` to move the ring's middle: it is part of the fit.
|
|
56
|
+
|
|
57
|
+
`Card3D` alone has no camera to fit to: size it with `width` from the table above (a card in a phone frame at z 6 is 2.2 wide at most).
|
|
58
|
+
|
|
59
|
+
## What a ring of cards carries
|
|
60
|
+
|
|
61
|
+
A ring of empty coloured cards reads as placeholders. Every card carries something: a `label` (a word, large, in the film's font), an `icon` (an image or svg from the project, drawn above the label), or a picture (`src`). Use `accent` for the rim, and keep the cards' `color` to the film's palette: one hero colour, its darker and lighter neighbours. A card face is drawn with a soft gradient, a highlight and a rim, so a flat `color` is enough.
|
|
62
|
+
|
|
63
|
+
## Camera moves
|
|
64
|
+
|
|
65
|
+
`camera.keys` move the camera with the same timing rule as the 2D `Camera`: a spring that starts 12 frames before the key's frame and lands on it. Three moves cover most needs:
|
|
66
|
+
|
|
67
|
+
- Push in: `z` from 7.5 to 6 over the shot (a word fitted at its closest, so it never leaves the frame). A bigger push from 9 to 5.5 works for a ring or a card, which are fine small at the start.
|
|
68
|
+
- Orbit a quarter turn: move `x` from -4 to 4 with `z` around 6, and keep `lookAt` on the object.
|
|
69
|
+
- Pull back to reveal: begin close on one card (`z` 3) and end wide (`z` 9) as the others arrive.
|
|
70
|
+
|
|
71
|
+
Keep a held word still for at least 1.5 seconds before the camera moves again.
|
|
72
|
+
|
|
73
|
+
## Keeping type readable
|
|
74
|
+
|
|
75
|
+
`Text3D` that must be read faces the camera: rotation near 0 and the camera in front of it. A word that turns in (`enter="turn"`) must finish turning, and then stay, for 1.5 seconds. Latin text only: Hebrew and other scripts stay 2D (see `reference/hebrew-rtl.md`, which is unchanged). Use the film's one accent colour for the extruded word.
|
|
76
|
+
|
|
77
|
+
## Cost
|
|
78
|
+
|
|
79
|
+
A 3D scene renders slower than 2D, and the first 3D render in a project bundles three. Use it for the shots that need it, not for every scene.
|
|
80
|
+
|
|
81
|
+
## Example: a portrait scene, verified by a render test
|
|
82
|
+
|
|
83
|
+
A word and a ring of labelled cards on a phone frame. Nothing is sized by hand, and a test renders this exact block on 9:16 and checks that nothing touches the edges.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import React from "react";
|
|
87
|
+
import { AbsoluteFill } from "reelkit/frame";
|
|
88
|
+
import { Card3D, Orbit3D, Scene3D, SceneFrame, Text3D, palettes } from "reelkit/kit";
|
|
89
|
+
import type { VideoProps } from "reelkit/kit";
|
|
90
|
+
|
|
91
|
+
export const Video: React.FC<VideoProps> = ({ manifest }) => {
|
|
92
|
+
const palette = palettes.darkTech;
|
|
93
|
+
const s = manifest.scenes[0];
|
|
94
|
+
return (
|
|
95
|
+
<AbsoluteFill style={{ backgroundColor: palette.bg }}>
|
|
96
|
+
<SceneFrame from={s.startFrame} durationInFrames={s.durationFrames} enter="cut" exit="cut">
|
|
97
|
+
<Scene3D camera={{ keys: [{ frame: 0, z: 7.5, y: 0 }, { frame: 40, z: 6 }] }}>
|
|
98
|
+
<Text3D text="Plan it." color={palette.ink} position={[0, 1.2, 0]} enter="rise" />
|
|
99
|
+
<Orbit3D position={[0, -0.6, 0]} speed={1.2} tiltDeg={10}>
|
|
100
|
+
<Card3D color={palette.hero} width={1.2} height={1.6} label="Plan" />
|
|
101
|
+
<Card3D color="#2563EB" width={1.2} height={1.6} label="Focus" />
|
|
102
|
+
<Card3D color="#059669" width={1.2} height={1.6} label="Ship" />
|
|
103
|
+
<Card3D color="#D97706" width={1.2} height={1.6} label="Rest" />
|
|
104
|
+
</Orbit3D>
|
|
105
|
+
</Scene3D>
|
|
106
|
+
</SceneFrame>
|
|
107
|
+
</AbsoluteFill>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Example: a reveal that builds itself, verified by a render test
|
|
113
|
+
|
|
114
|
+
The name arrives as blocks that fly out of a cloud and settle into the letters, under a neon light, with the camera pushing in. Nothing is sized by hand: the word fills 80 percent of the width at its closest, and a test renders this exact block on 9:16 and checks that it is whole and clear of the edges.
|
|
115
|
+
|
|
116
|
+
```tsx
|
|
117
|
+
import React from "react";
|
|
118
|
+
import { AbsoluteFill } from "reelkit/frame";
|
|
119
|
+
import { Assemble3D, Scene3D, SceneFrame, palettes } from "reelkit/kit";
|
|
120
|
+
import type { VideoProps } from "reelkit/kit";
|
|
121
|
+
|
|
122
|
+
export const Video: React.FC<VideoProps> = ({ manifest }) => {
|
|
123
|
+
const palette = palettes.darkTech;
|
|
124
|
+
const s = manifest.scenes[0];
|
|
125
|
+
return (
|
|
126
|
+
<AbsoluteFill style={{ backgroundColor: palette.bg }}>
|
|
127
|
+
<SceneFrame from={s.startFrame} durationInFrames={s.durationFrames} enter="cut" exit="cut">
|
|
128
|
+
<Scene3D mood="neon" camera={{ keys: [{ frame: 0, z: 7.5 }, { frame: 70, z: 6 }] }}>
|
|
129
|
+
<Assemble3D shape="text" text="Relay" count={700} frames={50} order="x" colors={[palette.hero, palette.accent]} />
|
|
130
|
+
</Scene3D>
|
|
131
|
+
</SceneFrame>
|
|
132
|
+
</AbsoluteFill>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
```
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: voice-sync
|
|
3
|
+
description: Use when writing the composition of a narrated video - how to fit the picture to the voice, so that what a word names is on screen on that word.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fitting the picture to the voice
|
|
7
|
+
|
|
8
|
+
In a narrated film the voice is the clock. The words are the only thing the viewer is sure to follow, so anything that shows or names a word has to be on screen when that word is said: not a beat earlier, not a second later. The music is the clock for scene changes and for decoration (`reference/beat-sync.md`).
|
|
9
|
+
|
|
10
|
+
## Order of work
|
|
11
|
+
|
|
12
|
+
1. Write the narration first (`reference/scriptwriting.md`). Do not design pictures for words that are not written yet.
|
|
13
|
+
2. Record it: `reelkit assets voiceover --all`. It prints the real length and the real silence between the sentences. The word times are in `manifest.json` (`scenes[i].words`, in seconds from the scene's start).
|
|
14
|
+
3. Only now place the visuals, from the real word times, with `onWord`. Never from a guess, and never by counting words by hand: the kit finds the word.
|
|
15
|
+
4. `reelkit check` fails on a word the scene never says. `reelkit preview` then shows one frame 4 frames after each word you timed something to, in a row of its own on the contact sheet: look at it.
|
|
16
|
+
|
|
17
|
+
If you change the narration, record again and place the visuals again: the times moved.
|
|
18
|
+
|
|
19
|
+
## The rules
|
|
20
|
+
|
|
21
|
+
- **One visual event per stressed word.** Pick the words that carry the meaning (a noun, a number, a verb that changes something) and give each one thing to show. Not every word. At most one event every 0.5 s: more is noise, and the viewer cannot follow it.
|
|
22
|
+
- **On the word.** What a word names is on screen within 3 frames of the word starting: `onWord(s, "tasks")` is the frame to START its entrance, 3 frames before the word, so that it is seen landing on it. Never earlier than the word, and never before the previous sentence has ended. Nothing appears for a word that has not been said.
|
|
23
|
+
- **Numbers count up to finish on their word.** Start the count `durationFrames` before the word, so it is whole when the word is said: `<Sequence from={wordFrame(s, "thousand") - 24}><Counter to={10000} durationFrames={24} /></Sequence>`.
|
|
24
|
+
- **Lists tick on each item's word.** One entrance per item, each on its own word (`onWord(s, "tasks")`, then `"deadlines"`, then `"energy"`), not one animation for the whole list on the first word.
|
|
25
|
+
- **A phrase starts on its first word.** `onWord(s, "whole week")` takes the start of "whole". Use `{ edge: "end" }` for something that must land when a word has been said, and `{ nth: 2 }` for the second time a word is said.
|
|
26
|
+
- **Scene changes fall in the gap between sentences, never inside one.** The CLI lays the scenes out that way: a scene lasts until its last word ends plus the plan's `gap`. Do not make a scene break inside a sentence by cutting the voice, and do not hold a picture for a word that belongs to the next scene.
|
|
27
|
+
- **The last word of a sentence gets a hold of the gap's length, not more.** The thing it names stays for the gap (0.2 to 0.5 s) and then the scene changes. Do not add `exitAt` or padding that keeps a picture on after its sentence for a second: that is dead air.
|
|
28
|
+
- **The beat never overrides the word.** `onWordBeat(s, "builds", manifest.music)` lets the entrance land on a beat only when one falls within 3 frames after the word; otherwise the word wins. It is never earlier than `onWord`.
|
|
29
|
+
|
|
30
|
+
## Sound effects under a voice
|
|
31
|
+
|
|
32
|
+
A sound for a word's visual goes at that visual's frame (`wordFrame(s, "tasks")`, or 2 frames before it for a soft click). Under the voice it sits lower than in a film with no voice: `volume` 0.15 to 0.3, and never above 0.3 while the voice speaks (a film with no voice uses 0.35 to 0.6; `reference/launch-film.md`). Sounds are levelled when they are pulled, so the same number is equally loud for every file. One effect per visual event, and none under a word that carries no picture. The music under the voice is `Music`'s default (`duckTo` 0.18) and stays steady through the sentences.
|
|
33
|
+
|
|
34
|
+
After a render, `reelkit sound --detail` says how the voice sits against the music (estimated from the mix) and how many times the music pumped; the voice should be at least 10 dB above the music under it, and a pump count above 2 in 30 s means something is lifting the music between sentences.
|
|
35
|
+
|
|
36
|
+
## An example
|
|
37
|
+
|
|
38
|
+
The plan (three scenes; only what the example needs is shown):
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"gap": "tight",
|
|
43
|
+
"scenes": [
|
|
44
|
+
{ "id": "hook", "narration": "Every single week, ten thousand teams plan with Tempo." },
|
|
45
|
+
{ "id": "list", "narration": "It reads your tasks, your deadlines and your energy." },
|
|
46
|
+
{ "id": "close", "narration": "Tempo. Your week, in rhythm." }
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The composition. The number finishes counting as "thousand" is said, and each row of the list lands on its own word, with a soft click under it:
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
import React from "react";
|
|
55
|
+
import { AbsoluteFill, Sequence, useVideoConfig } from "reelkit/frame";
|
|
56
|
+
import { Captions, Counter, Entrance, Music, onWord, SceneFrame, sceneById, Sfx, Voiceover, wordFrame } from "reelkit/kit";
|
|
57
|
+
import type { VideoProps } from "reelkit/kit";
|
|
58
|
+
|
|
59
|
+
const POP = "assets/lib/sfx-ui-bubble-pop/clip.mp3";
|
|
60
|
+
const ink = "#1E2A55", hero = "#E8604C";
|
|
61
|
+
|
|
62
|
+
// One row of the list (a local component): a coloured bar with its label.
|
|
63
|
+
const Card: React.FC<{ label: string; top: number }> = ({ label, top }) => {
|
|
64
|
+
const { width, height } = useVideoConfig();
|
|
65
|
+
return (
|
|
66
|
+
<div style={{ position: "absolute", left: "10%", top: height * top, width: "80%", height: height * 0.1, background: hero, borderRadius: width * 0.03, color: "#ffffff", fontSize: width * 0.06, fontWeight: 800, display: "flex", alignItems: "center", paddingLeft: width * 0.05 }}>{label}</div>
|
|
67
|
+
);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
|
|
71
|
+
const hook = sceneById(manifest, "hook");
|
|
72
|
+
const list = sceneById(manifest, "list");
|
|
73
|
+
const close = sceneById(manifest, "close");
|
|
74
|
+
// Captions and the voice, in every scene.
|
|
75
|
+
const voice = (s: typeof hook) => (
|
|
76
|
+
<>
|
|
77
|
+
<Captions words={s.words} group={manifest.captions} highlight={hero} />
|
|
78
|
+
{s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
|
|
79
|
+
</>
|
|
80
|
+
);
|
|
81
|
+
return (
|
|
82
|
+
<AbsoluteFill style={{ background: "#FBF3EA" }}>
|
|
83
|
+
<SceneFrame from={hook.startFrame} durationInFrames={hook.durationFrames}>
|
|
84
|
+
{/* 24 frames of counting, whole on "thousand" */}
|
|
85
|
+
<Sequence from={Math.max(0, wordFrame(hook, "thousand") - 24)}>
|
|
86
|
+
<Counter to={10000} durationFrames={24} color={ink} />
|
|
87
|
+
</Sequence>
|
|
88
|
+
{voice(hook)}
|
|
89
|
+
</SceneFrame>
|
|
90
|
+
<SceneFrame from={list.startFrame} durationInFrames={list.durationFrames}>
|
|
91
|
+
<Entrance delay={onWord(list, "tasks")}><Card label="Tasks" top={0.2} /></Entrance>
|
|
92
|
+
<Entrance delay={onWord(list, "deadlines")}><Card label="Deadlines" top={0.34} /></Entrance>
|
|
93
|
+
<Entrance delay={onWord(list, "energy")}><Card label="Energy" top={0.48} /></Entrance>
|
|
94
|
+
<Sfx src={urls[POP]} at={wordFrame(list, "tasks")} volume={0.25} />
|
|
95
|
+
<Sfx src={urls[POP]} at={wordFrame(list, "deadlines")} volume={0.22} />
|
|
96
|
+
<Sfx src={urls[POP]} at={wordFrame(list, "energy")} volume={0.22} />
|
|
97
|
+
{voice(list)}
|
|
98
|
+
</SceneFrame>
|
|
99
|
+
<SceneFrame from={close.startFrame} durationInFrames={close.durationFrames}>
|
|
100
|
+
{voice(close)}
|
|
101
|
+
</SceneFrame>
|
|
102
|
+
{manifest.music ? <Music src={urls[manifest.music.key]} /> : null}
|
|
103
|
+
</AbsoluteFill>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`reelkit preview` then shows, for this film, four word frames (`thousand ▸ hook`, `tasks ▸ list`, `deadlines ▸ list`, `energy ▸ list`): the number is whole in the first, and each row is already on screen in the others.
|
package/src/agents.ts
CHANGED
|
@@ -7,9 +7,9 @@ import { PKG_ROOT } from "./render/validate";
|
|
|
7
7
|
type Env = Record<string, string | undefined>;
|
|
8
8
|
|
|
9
9
|
// Every path is relative to the agents home. Skill folders: the vercel-labs/skills README. Command folders: each agent's own docs.
|
|
10
|
-
export const AGENTS: { id: string; name: string; home: string; skillDir: string;
|
|
11
|
-
{ id: "claude", name: "Claude Code", home: ".claude", skillDir: ".claude/skills/reelkit",
|
|
12
|
-
{ id: "codex", name: "Codex", home: ".codex", skillDir: ".codex/skills/reelkit",
|
|
10
|
+
export const AGENTS: { id: string; name: string; home: string; skillDir: string; commandDir?: string }[] = [
|
|
11
|
+
{ id: "claude", name: "Claude Code", home: ".claude", skillDir: ".claude/skills/reelkit", commandDir: ".claude/commands" },
|
|
12
|
+
{ id: "codex", name: "Codex", home: ".codex", skillDir: ".codex/skills/reelkit", commandDir: ".codex/prompts" },
|
|
13
13
|
{ id: "cursor", name: "Cursor", home: ".cursor", skillDir: ".cursor/skills/reelkit" },
|
|
14
14
|
{ id: "gemini", name: "Gemini CLI", home: ".gemini", skillDir: ".gemini/skills/reelkit" },
|
|
15
15
|
{ id: "agents", name: "Shared agents folder", home: ".agents", skillDir: ".agents/skills/reelkit" },
|
|
@@ -19,6 +19,13 @@ export const AGENTS: { id: string; name: string; home: string; skillDir: string;
|
|
|
19
19
|
export const agentsHome = (env: Env): string => env.REELKIT_AGENTS_HOME || homedir();
|
|
20
20
|
|
|
21
21
|
const SKILL_SRC = join(PKG_ROOT, "skill");
|
|
22
|
+
|
|
23
|
+
// The slash commands that ride with the skill. `file` is relative to the skill folder in the package; the command is installed as `<name>.md`
|
|
24
|
+
// in an agent's command folder and kept out of the skill's own folder. The first is the one that was installed before there were two.
|
|
25
|
+
export const COMMANDS: { name: string; file: string }[] = [
|
|
26
|
+
{ name: "reelkit-video", file: "command.md" },
|
|
27
|
+
{ name: "reelkit-launch-film", file: "commands/launch-film.md" },
|
|
28
|
+
];
|
|
22
29
|
const VERSION_FILE = ".reelkit-version";
|
|
23
30
|
const packageVersion = (): string => JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8")).version;
|
|
24
31
|
|
|
@@ -32,13 +39,15 @@ function recoverLeftovers(dest: string): void {
|
|
|
32
39
|
for (const p of left) rmSync(p, { recursive: true, force: true });
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
export type InstallReport = { installed: { agent: string; path: string }[]; skipped: { agent: string; reason: string }[] };
|
|
42
|
+
export type InstallReport = { installed: { agent: string; path: string; commands: string[] }[]; skipped: { agent: string; reason: string }[] };
|
|
36
43
|
|
|
37
44
|
export function installSkill(env: Env, opts: { agents?: string[]; force?: boolean; source?: string; rename?: (from: string, to: string) => void }): InstallReport {
|
|
38
45
|
const source = opts.source ?? SKILL_SRC;
|
|
39
46
|
if (!existsSync(join(source, "SKILL.md")) || !existsSync(join(source, "command.md"))) {
|
|
40
47
|
throw new Error("Reelkit's skill files are missing from this installation. Reinstall reelkit and try again.");
|
|
41
48
|
}
|
|
49
|
+
// A command the package does not have in this source is skipped, so an older source with only command.md still installs.
|
|
50
|
+
const commands = COMMANDS.filter((c) => existsSync(join(source, c.file)));
|
|
42
51
|
const rename = opts.rename ?? renameSync;
|
|
43
52
|
const home = agentsHome(env);
|
|
44
53
|
const version = packageVersion();
|
|
@@ -52,8 +61,9 @@ export function installSkill(env: Env, opts: { agents?: string[]; force?: boolea
|
|
|
52
61
|
const dest = join(home, agent.skillDir);
|
|
53
62
|
recoverLeftovers(dest);
|
|
54
63
|
const current = existsSync(join(dest, VERSION_FILE)) ? readFileSync(join(dest, VERSION_FILE), "utf8").trim() : undefined;
|
|
55
|
-
const
|
|
56
|
-
|
|
64
|
+
const commandPaths = agent.commandDir ? commands.map((c) => join(home, agent.commandDir!, `${c.name}.md`)) : [];
|
|
65
|
+
// Up to date only when the skill is the current version and every command is in place: a missing command is put back.
|
|
66
|
+
if (!opts.force && current === version && commandPaths.every((p) => existsSync(p))) {
|
|
57
67
|
report.skipped.push({ agent: agent.id, reason: "already up to date" });
|
|
58
68
|
continue;
|
|
59
69
|
}
|
|
@@ -62,7 +72,7 @@ export function installSkill(env: Env, opts: { agents?: string[]; force?: boolea
|
|
|
62
72
|
const backup = join(dirname(dest), `.reelkit.old-${process.pid}`);
|
|
63
73
|
try {
|
|
64
74
|
mkdirSync(dirname(dest), { recursive: true });
|
|
65
|
-
cpSync(source, staged, { recursive: true, filter: (src) => src !== join(source, "command.md") });
|
|
75
|
+
cpSync(source, staged, { recursive: true, filter: (src) => src !== join(source, "command.md") && src !== join(source, "commands") });
|
|
66
76
|
writeFileSync(join(staged, VERSION_FILE), `${version}\n`);
|
|
67
77
|
const hadOld = existsSync(dest);
|
|
68
78
|
if (hadOld) rename(dest, backup);
|
|
@@ -76,11 +86,12 @@ export function installSkill(env: Env, opts: { agents?: string[]; force?: boolea
|
|
|
76
86
|
rmSync(staged, { recursive: true, force: true });
|
|
77
87
|
if (existsSync(dest)) rmSync(backup, { recursive: true, force: true }); // never delete the backup while nothing is in place
|
|
78
88
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
89
|
+
commands.forEach((c, i) => {
|
|
90
|
+
if (!commandPaths[i]) return;
|
|
91
|
+
mkdirSync(dirname(commandPaths[i]!), { recursive: true });
|
|
92
|
+
cpSync(join(source, c.file), commandPaths[i]!);
|
|
93
|
+
});
|
|
94
|
+
report.installed.push({ agent: agent.id, path: dest, commands: commands.filter((_, i) => commandPaths[i]).map((c) => c.name) });
|
|
84
95
|
}
|
|
85
96
|
return report;
|
|
86
97
|
}
|
package/src/api/client.ts
CHANGED
|
@@ -12,6 +12,9 @@ const VERSION = (JSON.parse(readFileSync(new URL("../../package.json", import.me
|
|
|
12
12
|
|
|
13
13
|
export type Api = <K extends RouteName>(name: K, input: z.input<Routes[K]["req"]>) => Promise<z.infer<Routes[K]["res"]>>;
|
|
14
14
|
|
|
15
|
+
// A vector graphic takes about 45 seconds to make, and the request stays open until it is done, so the one route that can take that long is allowed 180 seconds.
|
|
16
|
+
const TIMEOUT_MS: Partial<Record<keyof typeof routes, number>> = { images: 180_000 };
|
|
17
|
+
|
|
15
18
|
export function createClient(opts: { baseUrl: string; token?: string }): Api {
|
|
16
19
|
return async (name, input) => {
|
|
17
20
|
const r = routes[name];
|
|
@@ -28,7 +31,7 @@ export function createClient(opts: { baseUrl: string; token?: string }): Api {
|
|
|
28
31
|
}
|
|
29
32
|
let res: Response;
|
|
30
33
|
try {
|
|
31
|
-
res = await fetch(url, { method: r.method, headers, body });
|
|
34
|
+
res = await fetch(url, { method: r.method, headers, body, ...(TIMEOUT_MS[name] ? { signal: AbortSignal.timeout(TIMEOUT_MS[name]!) } : {}) });
|
|
32
35
|
} catch {
|
|
33
36
|
throw new ApiFailure("server_error", `Cannot reach the Reelkit API at ${opts.baseUrl}. Check your connection or REELKIT_API_URL.`);
|
|
34
37
|
}
|