reelkit-cli 0.5.0 → 0.8.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.
Files changed (104) hide show
  1. package/README.md +3 -2
  2. package/package.json +7 -2
  3. package/skill/SKILL.md +22 -11
  4. package/skill/THIRD_PARTY.md +102 -0
  5. package/skill/commands/launch-film.md +7 -0
  6. package/skill/reference/asset-reuse.md +13 -2
  7. package/skill/reference/backgrounds.md +63 -0
  8. package/skill/reference/beat-sync.md +32 -17
  9. package/skill/reference/captions.md +11 -5
  10. package/skill/reference/component-authoring.md +12 -1
  11. package/skill/reference/continuity.md +21 -2
  12. package/skill/reference/kit.md +140 -11
  13. package/skill/reference/launch-film.md +190 -0
  14. package/skill/reference/remotion-composition.md +4 -3
  15. package/skill/reference/scene-treatments.md +20 -0
  16. package/skill/reference/scriptwriting.md +4 -1
  17. package/skill/reference/three-d.md +134 -0
  18. package/skill/reference/voice-sync.md +108 -0
  19. package/src/agents.ts +23 -12
  20. package/src/api/client.ts +4 -1
  21. package/src/cli.ts +25 -8
  22. package/src/commands/assets.ts +334 -36
  23. package/src/commands/build.ts +172 -36
  24. package/src/commands/components.ts +220 -0
  25. package/src/commands/init.ts +9 -3
  26. package/src/commands/install.ts +1 -1
  27. package/src/commands/plan.ts +8 -5
  28. package/src/commands/ref.ts +5 -2
  29. package/src/contract/index.ts +27 -5
  30. package/src/pipeline/beatsnap.ts +72 -0
  31. package/src/pipeline/review.ts +67 -7
  32. package/src/pipeline/schema.ts +51 -4
  33. package/src/pipeline/timing.ts +27 -1
  34. package/src/project/background.ts +33 -0
  35. package/src/project/layers.ts +60 -0
  36. package/src/project/loudness.ts +68 -0
  37. package/src/project/manifest.ts +68 -14
  38. package/src/project/music.ts +19 -5
  39. package/src/project/project.ts +7 -2
  40. package/src/project/refmeasure.ts +1 -1
  41. package/src/project/soundreport.ts +347 -0
  42. package/src/project/svgcheck.ts +21 -0
  43. package/src/remotion/kit/Assemble3D.tsx +92 -0
  44. package/src/remotion/kit/BrowserFrame.tsx +83 -0
  45. package/src/remotion/kit/Camera.tsx +6 -4
  46. package/src/remotion/kit/Captions.tsx +33 -17
  47. package/src/remotion/kit/Card3D.tsx +211 -0
  48. package/src/remotion/kit/ChapterFrame.tsx +68 -0
  49. package/src/remotion/kit/CounterRoll.tsx +75 -0
  50. package/src/remotion/kit/GlassPanel.tsx +43 -0
  51. package/src/remotion/kit/Grounds.tsx +177 -0
  52. package/src/remotion/kit/Headline.tsx +97 -0
  53. package/src/remotion/kit/Hero3D.tsx +197 -0
  54. package/src/remotion/kit/HudOverlay.tsx +52 -0
  55. package/src/remotion/kit/ImageLayers.tsx +48 -0
  56. package/src/remotion/kit/Music.tsx +4 -4
  57. package/src/remotion/kit/NamedCursor.tsx +54 -0
  58. package/src/remotion/kit/Orbit3D.tsx +49 -0
  59. package/src/remotion/kit/Particles3D.tsx +74 -0
  60. package/src/remotion/kit/Place.tsx +12 -0
  61. package/src/remotion/kit/PromptBox.tsx +84 -0
  62. package/src/remotion/kit/Scene3D.tsx +70 -0
  63. package/src/remotion/kit/SceneFrame.tsx +88 -11
  64. package/src/remotion/kit/Sfx.tsx +12 -6
  65. package/src/remotion/kit/SoundCues.tsx +22 -0
  66. package/src/remotion/kit/TerminalLog.tsx +98 -0
  67. package/src/remotion/kit/Text3D.tsx +78 -0
  68. package/src/remotion/kit/TextOnImage.tsx +41 -0
  69. package/src/remotion/kit/Warp3D.tsx +59 -0
  70. package/src/remotion/kit/bg-math.ts +179 -0
  71. package/src/remotion/kit/caption-groups.ts +7 -3
  72. package/src/remotion/kit/caption-style.ts +45 -0
  73. package/src/remotion/kit/docs.ts +133 -11
  74. package/src/remotion/kit/image-layers-math.ts +115 -0
  75. package/src/remotion/kit/index.ts +43 -1
  76. package/src/remotion/kit/inter-bold-typeface.ts +3 -0
  77. package/src/remotion/kit/media.ts +5 -3
  78. package/src/remotion/kit/motion-math.ts +36 -2
  79. package/src/remotion/kit/music-math.ts +27 -10
  80. package/src/remotion/kit/quiet-three.ts +11 -0
  81. package/src/remotion/kit/sample-text.ts +55 -0
  82. package/src/remotion/kit/scene3d-context.ts +5 -0
  83. package/src/remotion/kit/seeded.ts +13 -0
  84. package/src/remotion/kit/sound-cues.ts +89 -0
  85. package/src/remotion/kit/sound-kinds.ts +122 -0
  86. package/src/remotion/kit/theme.ts +2 -0
  87. package/src/remotion/kit/three-fx-math.ts +192 -0
  88. package/src/remotion/kit/three-math.ts +145 -0
  89. package/src/remotion/kit/transition-math.ts +116 -0
  90. package/src/remotion/kit/ui-math.ts +145 -0
  91. package/src/remotion/kit/ui-theme.ts +25 -0
  92. package/src/remotion/kit/word-anchor.ts +107 -0
  93. package/src/render/contact-sheet.ts +39 -0
  94. package/src/render/continuity.ts +14 -4
  95. package/src/render/deps.ts +15 -3
  96. package/src/render/master.ts +31 -0
  97. package/src/render/render.ts +15 -8
  98. package/src/render/sound-notes.ts +106 -0
  99. package/src/render/static-check.ts +156 -0
  100. package/src/render/validate.ts +3 -150
  101. package/src/render/word-check.ts +181 -0
  102. package/src/testing/conformance.ts +61 -1
  103. package/src/testing/fake-api.ts +11 -5
  104. package/src/testing/fixtures.ts +3 -0
@@ -1,11 +1,13 @@
1
1
  # The starter kit
2
2
 
3
+ Library interface components (widgets, banners, lists) come small by default: an interface piece fills at least 70% of the frame width, so scale it with a wrapper or its size prop; and one typeface for the film: pass the film font where a component takes one, and prefer components that do.
4
+
3
5
  ```
4
6
  Import from "reelkit/kit". Media props (src) take urls[key]; the kit components also accept the bare project path. For your own <Img>, <Audio> or <OffthreadVideo>, always pass urls[key]. Inside a SceneFrame, useCurrentFrame() starts at 0 for that scene.
5
7
 
6
- SceneFrame { from: number; durationInFrames: number; children }
7
- Wraps one scene. Places children on the timeline and fades them in and out.
8
- <SceneFrame from={s.startFrame} durationInFrames={s.durationFrames}>...</SceneFrame>
8
+ SceneFrame { from: number; durationInFrames: number; enter?: Transition; exit?: Transition; transitionFrames?: number; origin?: { x: number; y: number }; color?: string; shape?: "circle" | "bar" | "diagonal"; children }
9
+ Wraps one scene. Places children on the timeline and fades them in and out. enter and exit choose another transition: "fade" (the default), "cut", "zoom-in" (arrives from 1.25x and blurred, settling), "zoom-out", "zoom-through" (exit: scales up fast toward origin and blurs away; enter: the next scene arrives from small at the same point), "push-left" | "push-right" | "push-up" | "push-down", "whip-left" | "whip-right" (a fast slide with directional blur), "blur" and "turn" (a quarter turn in perspective about the vertical axis). Five more are hits meant for the beat: "color-push" (a flat field of color covers the frame as a "circle" growing from origin, a "bar" or a "diagonal" and uncovers the next scene: 10 frames), "rgb-whip" (a whip with the red and blue channels pulled apart and a smear: 6 frames), "flash" (white, or color, for 3 frames), "ring" (the scene closes into a ring of color that sweeps in to origin and the next scene opens out of one: 12 frames) and "flip" (a hard cut with ONE inverted frame). Their own lengths apply unless transitionFrames is set (a flash is never over 4 frames, a flip is always 1). The covers are fullest on the scene boundary, so changeFrame and cuesOnChanges put the hit on the boundary for color-push, flash, ring and flip and half the span before it for rgb-whip. A scene's exit and the next scene's enter are meant to be the same kind. transitionFrames is how long it takes (default 8, at most 20, never more than half the scene); origin is where a zoom aims, as fractions of the frame (default the centre). The exit plays in the last frames of its scene and the enter in the first frames of the next, so a transition is complete exactly at the scene boundary and the cuts stay on the beat.
10
+ <SceneFrame from={s.startFrame} durationInFrames={s.durationFrames} enter="zoom-through" exit="zoom-through" origin={{ x: 0.5, y: 0.4 }}>...</SceneFrame>
9
11
 
10
12
  TitleCard { text: string; subtitle?: string; color?: string; background?: string }
11
13
  Large centred title that springs in.
@@ -15,10 +17,10 @@ LowerThird { title: string; subtitle?: string; accent?: string }
15
17
  Name/label strip that slides in from the left, low on the screen.
16
18
  <LowerThird title="Step 1" subtitle="Connect your account" />
17
19
 
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
- Word-timed captions near the bottom. Pass the scene's words from the manifest. Pick one mode for the whole video:
20
+ Captions { words: WordTiming[]; mode?: "highlight" | "pop" | "karaoke"; highlight?: string; color?: string; perLine?: number; group?: "none" | "word" | "phrase"; maxWords?: number; uppercase?: boolean; bottom?: number; face?: string; rtl?: boolean }
21
+ Word-timed captions near the bottom, on a dark rounded plate (about 58% opaque) in white so they read on any ground; the spoken word sits on a chip of highlight (the film's accent) with white or dark ink, whichever reads better on it (a karaoke fill is the accent mixed toward white until it reads on the plate). Pass the scene's words from the manifest. Pick one mode for the whole video:
20
22
  "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.
23
+ 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 ". ? !" and never spans two sentences, at a comma after three words, at a pause of 0.35 s, and holds at most maxWords words, default 3, and about 32 characters), "none" draws nothing. Pass the plan's choice straight through: group={manifest.captions}. Without group and perLine the groups are 3 words, never one; only the plan's "word" shows one word at a time. perLine alone counts the words as before.
22
24
  bottom is the distance from the bottom edge as a fraction of the height (default 0.16).
23
25
  For Hebrew narration pass face={font("heebo")} and rtl.
24
26
  <Captions words={s.words} mode="pop" highlight={palette.hero} uppercase />
@@ -61,13 +63,114 @@ WordReveal { text: string; delay?: number; per?: number; highlight?: string; hig
61
63
  Headline that rises in word by word from behind a mask. highlight colours one word.
62
64
  <WordReveal text="Stretch first, phone second" highlight="first" highlightColor={palette.hero} style={{ fontFamily: fonts.display, fontWeight: 800, fontSize: width * 0.09, color: palette.ink }} />
63
65
 
66
+ Act the product out (a claim is performed in a small believable interface, not stated)
67
+ Which to reach for: a speed claim -> CounterRoll; an agent or a developer tool -> TerminalLog; an assistant -> PromptBox with a reply; a web product -> BrowserFrame with the user's own screenshot; collaboration -> NamedCursor over the screen it works on; a card, a ticket or a plan -> GlassPanel around your own content; a film's chapters -> ChapterFrame, once per scene, the same slots every time; the words that land the claim -> Headline. HudOverlay lifts any scene. Highlight at most three claims in 30 seconds and merely mention the rest.
68
+ One timing convention for all of them: every time prop (delay, per, startFrame-like props) is a frame counted from the component's own mount, that is the frame useCurrentFrame() gives where it stands (inside a SceneFrame, the scene's own frame). Every one has a companion function that takes the SAME props and returns its event frames in that clock, so a sound sits on the frame the picture settles: add the scene's startFrame (or pass it as offset to cuesFor, below). Size: every one takes width, a fraction of the frame's width (default 0.86), and fits a 9:16 phone frame without numbers; its text is at least 0.032 of the frame's width (about 35 px at 1080). Every one takes hero (the accent), font and, where it has a panel, theme: "dark" (default), "light" or "glass"; themeFor(bg) gives "light" or "dark" for a ground colour.
69
+
70
+ PromptBox { text: string; cps?: number (22); chips?: string[]; reply?: string; replyCps?: number (70); placeholder?: string; hero?; theme?; font?; width?: number (0.86); delay?: number (6); sendPause?: number (9); replyDelay?: number (12); y?: number (0.5) }
71
+ A rounded input with the question typed in at cps characters a second with a blinking caret, chips under it, a round send button that presses when the typing ends, then a reply that streams in below. Open a film with the user's own question typed into it when the product is an assistant or a tool. promptFrames(props, fps) gives { type: number[] (every character's frame), typedEnd, send, replyStart, reply: number[], end }; typedFrames(text, cps, fps, start?) gives the frame of each character.
72
+ <PromptBox text="Can you answer Anna's refund email?" chips={["Relay", "Inbox"]} reply="Done. A warm reply is ready for your review." hero={palette.hero} />
73
+
74
+ TerminalLog { lines: { kind: "cmd" | "out" | "ok" | "warn" | "edit"; text: string; add?: number; del?: number; wait?: number }[]; rate?: number (3 lines a second); title?: string; status?: string ("Working"); checklist?: string[]; checkEvery?: number (16); maxLines?: number (8); speed?: number (8); hero?; theme?; font? (monospace); width?; delay?: number (6); y? }
75
+ A window with a title bar, lines that stream in (cmd typed command, out plain, ok success, warn warning, edit a file changed with +add -del), an optional checklist whose items get struck through one by one, and a status line with an elapsed counter that becomes "Done". The newest line is at the bottom. terminalFrames(props, fps) gives { lines: number[], checks: number[], done }; lineFrames(lines, rate, fps, delay) gives the lines' frames alone.
76
+ <TerminalLog lines={[{ kind: "cmd", text: "relay triage inbox" }, { kind: "edit", text: "drafts/anna.md", add: 18, del: 2 }, { kind: "ok", text: "Replies drafted" }]} checklist={["Read", "Draft", "Queue"]} />
77
+
78
+ NamedCursor { points: { x: number; y: number; click?: boolean; hold?: number }[]; name?: string; color?: string; hero?; font?; delay?: number (4); moveFrames?: number (22); size?: number (0.055) }
79
+ An arrow with a coloured name pill (a teammate or an agent) that travels the points (fractions of the frame) on an eased arc, rests hold frames (10 on a click, 5 otherwise) and clicks with a ripple. Lay it over the screen it points at. clickFrames(points, { delay, moveFrames }) gives the click frames; cursorArrivals gives the arrival frames.
80
+ <NamedCursor name="Maya" points={[{ x: 0.2, y: 0.8 }, { x: 0.7, y: 0.45, click: true }]} />
81
+
82
+ GlassPanel { children; hero?; glow?: string; theme?; width? (0.86); height?: number (fraction of the frame, default as tall as the content); tilt?: number (degrees, 0); delay?: number (0); radius?: number (0.06); padding?: number (0.05); y? }
83
+ A slab for your own content: a near-black rounded panel (theme "light" for a pale one) with a hairline stroke lit along its top, a coloured glow pool behind it, an optional perspective tilt and an entrance that slides up and settles with a small overshoot. panelSettleFrame(delay, fps) is the frame it lands.
84
+ <GlassPanel tilt={8} glow={palette.hero}><div style={{ color: "#fff", fontSize: width * 0.06 }}>Refund request</div></GlassPanel>
85
+
86
+ ChapterFrame { index: number; total: number; color: string; ghost?: string; caption?: string; badges?: string[] (up to 3); hero?; ink?; font?; delay?: number (0); children }
87
+ The grammar of one chapter, as slots: "03 / 07" and a progress rule at the top, a large ghost word behind everything, children as the hero in the middle, up to three badges that pop in, one caption line at the bottom. color fills the frame and is the chapter's ground; ink is chosen to read on it. Use it once per scene with the same slots every time, or not at all. chapterFrames({ delay, badges }) gives { badges: number[] (the pops), progressDone }.
88
+ <ChapterFrame index={3} total={7} color="#7C3AED" ghost="Speed" badges={["Instant", "Private"]} caption="Replies in seconds" hero="#FFD166"><CounterRoll to={94} suffix="%" ink="#fff" hero="#FFD166" /></ChapterFrame>
89
+
90
+ Headline { text: string; keyword?: string; emphasis?: "color" | "highlight" | "underline" ("color"); hero?; ink?: string ("#f4f4f5"); font?; width? (0.86); maxSize?: number (0.2, of the width); delay?: number (0); per?: number (4); align?: "center" | "left"; y? }
91
+ Two to five words set heavy, entering word by word, with ONE keyword emphasised: the word in the hero colour, a block of hero that sweeps in behind it, or a bar that draws on under it. keyword must be one of the words (case and punctuation ignored) or it throws and lists the words. It breaks into lines of about 12 characters and is measured after layout, so it fills width and never leaves the frame. headlineFrames(text, { delay, per }) gives { words: number[], emphasisDone }.
92
+ <Headline text="Answer every email" keyword="every" emphasis="highlight" hero={palette.hero} />
93
+
94
+ CounterRoll { to: number; from?: number (0); frames?: number (45); delay?: number (0); prefix?; suffix?; label?; decimals?: number (0); group?: boolean (true); hero?; ink?; font?; width? (0.86); maxSize?: number (0.3); y? }
95
+ An odometer: the digits roll like wheels from from to to, easing out, and land exactly, in tabular digits. prefix and suffix and label take the hero colour. settleFrame({ delay, frames }) is the frame it lands (put the settle chime there); rollValue(frame, { from, to, frames, delay }) is its value.
96
+ <CounterRoll to={12480} prefix="$" label="saved this month" delay={8} />
97
+
98
+ HudOverlay { labels?: string[] (up to 4, top left, top right, bottom left, bottom right); timecode?: boolean (true); scanlines?: boolean (true); hero?; ink?; font?; inset?: number (0.05) }
99
+ Corner brackets, small monospace labels in the corners (your words), a running timecode from the frame and faint scan lines, over a scene. Labels are never below 0.024 of the frame's width. No events to time.
100
+ <HudOverlay labels={["relay v2", "live"]} />
101
+
102
+ BrowserFrame { src: string; device?: "browser" | "phone"; scroll?: number (0.7, of the window's height); frames?: number (90); delay?: number (0); tilt?: number (-12 browser, -16 phone); url?: string; theme?; hero?; font?; width? (0.86 browser, 0.5 phone); viewAspect?; y? }
103
+ A tilted browser window (or phone) carrying the user's own screenshot (src, a project path or urls[...]) that starts at the top and scrolls down by scroll, with a soft shadow and a sheen that sweeps once as it lands. CSS 3D. It never scrolls past the end of the picture. browserFrames({ delay }) gives { settled, sheenStart, sheenEnd }.
104
+ <BrowserFrame src={urls["assets/screens/inbox.png"]} scroll={0.8} url="relay.app" />
105
+
106
+ Images in layers (a picture's words are a layer OF the picture)
107
+ An image scene is built in layers: the picture at the back, the words in the middle, the subject cut out of the picture in front when it has one. Run reelkit assets layers --scene <id> (or --layers on assets gen image and assets pull --scene) for the one or two hero pictures: the subject is cut out as <name>.subject.png (about 1 second of the cutout quota each) and the manifest scene gets imageLayers { back, subject?, subjectBox?, textZone: { region, luminance, busy } }; without it the words still go in the calm zone, which is measured on this machine for free whenever a picture is registered.
108
+
109
+ ImageLayers { layers?: ImageLayersRecord; back?: string; subject?: string; depth?: number (0.4); focus?: "subject" | "back"; front?: ReactNode; children }
110
+ Draws back picture, then children (the middle layer: your words), then the subject, then front, so a headline can pass BEHIND the subject. layers is s.imageLayers; or give back and subject (urls[...]) yourself. Depth comes from the frame: the back drifts and grows slowly, the subject a little more and the other way (depth 0 moves them together, 1 is the most), the words between them at their own rate. focus softly blurs the other layer. Without a subject it is one flat picture with the words over it. Every layer covers the frame at every frame.
111
+ <ImageLayers layers={s.imageLayers}><TextOnImage layers={s.imageLayers}><Headline text="Meet Tom" /></TextOnImage></ImageLayers>
112
+
113
+ TextOnImage { layers?: ImageLayersRecord; zone?: "top" | "middle" | "bottom" | "left" | "right" | { x; y; w; h }; behindSubject?: boolean; ink?: string; scrim?: number; children }
114
+ The always-correct way to put words on a picture. It sets its children in the picture's calm zone (layers.textZone, or zone), picks dark or light ink from how light the zone is (a Headline inside takes it), and lays a soft scrim just behind the words, stronger the busier the zone, so they read on any picture. With a subject layer the words go BEHIND the subject when it covers 10 to 60 percent of the text block (the title behind the person) and in front of it when it would hide more than 60 percent. Never put words on a flat card beside the picture and never over its busiest part.
115
+
64
116
  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
117
  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
118
  <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
119
 
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.
120
+ Camera { keys: { frame: number; x?: number; y?: number; zoom?: number; rotate?: number }[]; drift?: number; punches?: { frame: number; x: number; y: number; amount?: number; frames?: number; hold?: number }[]; lead?: number; stiffness?: number; damping?: number; children }
121
+ 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. Frames are counted from the start of the scene when the Camera is inside a SceneFrame (frame 0 is the scene's first frame), and are the video's absolute frames when it wraps all the scenes. 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
122
  <Camera keys={[{ frame: 0, zoom: 1 }, { frame: b.startFrame, x: 0.7, y: 0.4, zoom: 4 }]} drift={0.01}>{scenes}</Camera>
123
+ punches are quick emphasis pushes on top of the keys, for a word or a click: a zoom of 8 to 15 percent (amount, default 0.1) onto the point (x, y) over frames (default 5) with a tiny overshoot, held for hold frames (default 6), then eased back. The point stays still on screen. <Camera keys={[{ frame: 0 }]} punches={[{ frame: click, x: 0.62, y: 0.4, amount: 0.12 }]}>...</Camera>
124
+ zoomTo(frame, { x, y, width, height }) makes the key that pushes into an element until it fills the frame (the box is the element's centre and size as fractions of the frame): keys={[{ frame: 0 }, zoomTo(b.startFrame, { x: 0.5, y: 0.4, width: 0.3, height: 0.2 })]}.
125
+
126
+ Scene3D { camera?: { keys: { frame: number; x?: number; y?: number; z?: number; lookAt?: [number, number, number]; fov?: number }[]; lead?: number }; background?: string; lights?: boolean; mood?: "studio" | "night" | "sunset" | "neon"; children }
127
+ A three.js scene at the size of the frame, for real depth. Transparent unless background is set, so it lays over a 2D ground; a soft three-point light is placed for you (lights={false} to place your own); mood ("studio", "night", "sunset" or "neon") chooses the light colours, their intensities and the fog tint together instead, so lights are never hand-tuned. Put it inside a SceneFrame, with captions and small labels in 2D above it. camera.keys move the camera with the same timing as Camera (a spring that starts 12 frames before the key's frame and lands on it); the default camera is at z 6, looking at the origin, fov 50, and a key that leaves a value out keeps the one before. What it sees: at fov 50 the height is 0.93 times the distance to the object and the width that times width over height, so 3.1 units across at z 6 on 9:16 (4.7 at z 9), 5.6 on 1:1 and 9.9 on 16:9 (not "5.5"). Text3D and Orbit3D read the camera keys and size themselves to fit at the key where the camera is closest: leave their sizes out. visibleWidth(distance, aspect, fov?) and visibleHeight(distance, fov?) give the same numbers. No GL context is made unless a Scene3D is on screen. A 3D scene renders slower: use it only for the shots that need it. See reference/three-d.md.
128
+ <Scene3D camera={{ keys: [{ frame: 0, z: 7.5 }, { frame: 40, z: 6 }] }}><Text3D text="Reelkit" color={palette.hero} enter="rise" /></Scene3D>
129
+
130
+ Text3D { text: string; fit?: number (0.8); size?: number; depth?: number; color?: string; position?: [x, y, z]; rotation?: [x, y, z] (degrees); enter?: "rise" | "turn" | "fly" | "none"; delay?: number }
131
+ Extruded text, centred on its position, inside a Scene3D. Latin letters, digits and common punctuation only (one bundled typeface, Inter Bold): Hebrew and other scripts stay 2D. Leave size out: the word is measured on its own letters and sized to fill fit (default 0.8) of the frame's width at the closest the Scene3D camera comes, so it is whole at every moment on any aspect; a line break in the text makes a second line, and the height is held to fit of the frame's height. A size you give wins (scene units, the height of a capital-ish line). A push-in starts the word smaller in proportion: z 7.5 to 6 is a gentle one. depth is how far it is extruded (default a quarter of the size). enter is how it arrives, starting delay frames into the scene; "none" is there from the start. Keep type that must be read facing the camera and held at least 1.5 seconds.
132
+ <Text3D text="Meet Reelkit" color="#ffffff" position={[0, 1, 0]} enter="turn" delay={6} />
133
+
134
+ Card3D { src?: string; color?: string; label?: string; icon?: string; accent?: string; font?: string; width?: number; height?: number; radius?: number; position?: [x, y, z]; rotation?: [x, y, z] (degrees); tilt?: number; float?: boolean | number }
135
+ A rounded plane in space that shows a picture (src, a project path or urls[...]; the height follows the picture) or a designed face, with a soft shadow behind it. The face is a gradient of color with a highlight and a rim in accent; label is drawn large on it in the film's font (font, default the kit's display face; ink is white on a dark colour, dark on a light one) and icon (a project path to an image or svg, drawn in its own colours) above the label. It is drawn on both sides, so a card seen from behind reads the right way round. Give every card in a ring a label or an icon, never an empty colour. tilt is the one tilt a card needs: a turn about the vertical axis in degrees with a slight lean. float makes it drift slowly and deterministically.
136
+ <Card3D src={urls[s.userAssetKeys[0]]} width={2.4} tilt={-16} float /> <Card3D color={palette.hero} width={1.2} height={1.6} label="Plan" icon="assets/icons/plan.svg" />
137
+
138
+ Orbit3D { radius?: number; fit?: number (0.85); speed?: number; tiltDeg?: number; billboard?: boolean; position?: [x, y, z]; children }
139
+ Arranges its children on a ring and turns it slowly (speed is degrees a frame, default 0.5), for a set of cards or logos. Leave radius out: the ring takes the radius at which its cards stand side by side and is scaled, radius and cards together, to lie inside fit (default 0.85) of the frame at the closest the Scene3D camera comes, however it has turned and on any aspect. A radius you give wins and nothing is scaled. Children face outward, so the one at the front faces the camera; billboard keeps all of them facing the camera. tiltDeg tips the ring toward the camera. Every card carries a label or an icon.
140
+ <Orbit3D tiltDeg={12}><Card3D color="#8B5CF6" width={1.2} height={1.6} label="Plan" /><Card3D color="#2563EB" width={1.2} height={1.6} label="Focus" /><Card3D color="#059669" width={1.2} height={1.6} label="Ship" /></Orbit3D>
141
+
142
+ Assemble3D { shape?: "grid" | "ring" | "sphere" | "wall" | "text" ("grid"); text?: string; targets?: [x, y, z][]; count?: number (400, at most 4000); order?: "y" | "x" | "radial" | "random" ("y"); from?: number (0); frames?: number (60); colors?: string[] (two or three); fit?: number (0.8); size?: number; spread?: number (1.6); seed?: number; position? }
143
+ Instanced blocks that fly from a seeded cloud into an arrangement, one after another, each on an arc with a turn that unwinds, then rest: the build-up beat. "text" puts the blocks inside the letters of text (a short Latin word); targets is any [x, y, z] list (centred and fitted). Every piece is home at from + frames. Fits the frame by default at the closest the camera comes. In a Scene3D. assembleProgress(frame, i, { ranks, from, frames }), assembledCount(frame, { ranks, from, frames }) (for a piece counter), assembleRanks(targets, order, seed) and assembleEnd({ from, frames }) (the frame of the lock sound: cuesFor([from, assembleEnd(...)], "assemble")) are exported.
144
+ <Scene3D><Assemble3D shape="text" text="Relay" count={900} order="x" colors={[palette.hero, palette.accent]} /></Scene3D>
145
+
146
+ Particles3D { from?: "sphere" | "torus" | "knot" | "galaxy" | "plane" | "text" ("sphere"); to? ("torus"); text?: string; count?: number (12000, at most 60000); morph?: number; morphFrames?: [number, number] ([20, 80]); colors?: [string, string]; drift?: number (0.012); fit?: number (0.8); size?: number (0.026); spin?: number (6, degrees a second); seed?: number; position? }
147
+ A cloud of glowing points that morphs between two shapes, an abstract idea made of light: additive, soft, a gentle seeded drift. The morph runs between morphFrames and holds either side, or give morph (0 to 1). Done on the CPU with ordinary points, so it is the same on every machine. In a Scene3D.
148
+ <Scene3D><Particles3D from="galaxy" to="text" text="Relay" morphFrames={[10, 60]} /></Scene3D>
149
+
150
+ Hero3D { kind?: "device" | "logo" | "box" | "coin" ("box"); src?: string; text?: string ("R"); hero?: string; rim1?: string; rim2?: string; floor?: "soft" | "mirror" | "none" ("soft"); spin?: number; hover?: number (0.5); fit?: number (0.7); aspect?: number; poses?: { frame: number; rotation?: [x, y, z] (degrees); position?: [x, y, z]; scale?: number }[]; lights?: boolean (true); position? }
151
+ A studio-lit hero object: physical material with a clearcoat in hero, a key light and two coloured rim lights (rim1, rim2, by default the hero colour toward blue and toward pink), a contact shadow or a faded mirror copy below. "device" is a rounded slab whose screen shows src (the user's screenshot; without it a designed screen), "logo" extrudes text (a word or one letter) in the kit's typeface, "coin" is a disc with the first letter of text on it. spin is degrees a second (12; a device sways by that many degrees instead of turning), hover is how much it bobs. poses re-pose the one object across scenes with the same timing as Camera (the move starts 12 frames before the key and lands on it); poseAt(keys, frame, fps) is exported. Fits the frame by default. In a Scene3D.
152
+ <Scene3D mood="studio"><Hero3D kind="device" src={urls["assets/screens/inbox.png"]} hero={palette.hero} poses={[{ frame: 0, rotation: [8, -28, 0] }, { frame: 70, rotation: [0, 0, 0], scale: 1.1 }]} /></Scene3D>
153
+
154
+ Screen3D { the props of Hero3D without kind and text; spin (5) }
155
+ A tilted screen in 3D carrying a screenshot (src) or a designed mock, with a bezel, a shadow and a sheen: the interface carried across scenes and re-posed with poses. It is Hero3D kind="device" with a small sway.
156
+ <Scene3D><Screen3D src={urls["assets/screens/inbox.png"]} poses={[{ frame: 0, rotation: [10, -30, 0] }, { frame: 80, rotation: [0, 0, 0] }]} /></Scene3D>
157
+
158
+ Warp3D { speed?: number | { frame: number; speed: number }[] (1); burst?: { frame: number; frames: number; peak?: number }; count?: number (500); colors?: [string, string]; width?: number (0.035); seed?: number; position? }
159
+ Speed streaks rushing out of a vanishing point, additive, in two colours: a ground for a hyperspace beat, or a 12 to 20 frame transition burst (burst: a rise and fall that is nothing outside it). The streaks are longer and quicker the higher speed is. In a Scene3D; it is drawn over everything else in it. warpSpeed(frame, speed, burst) is exported.
160
+ <Scene3D><Warp3D burst={{ frame: 20, frames: 16, peak: 4 }} /></Scene3D>
161
+
162
+ Grounds (the layer under everything; one for the whole film, see reference/backgrounds.md)
163
+ BgVideo { src; dim?: 0-0.8 (0.35); blur?: px (0); zoom?: number (0.04); tint?: string; loop?: boolean (true); playbackRate?: number (1); durationSec?: number }
164
+ A video as the ground, always muted. dim darkens it so type stays readable, tint lays a colour over at low opacity so the clip takes the film's accent, zoom is a slow push over the film. With loop and durationSec (manifest.background.durationSec), a film longer than the clip starts it again with a short cross-fade over the join. <BgVideo src={urls[manifest.background.key]} durationSec={manifest.background.durationSec} dim={0.4} tint={palette.hero} />
165
+ BgImage { src; dim?: 0.35; blur?: 0; tint?; zoom?: 0.06; drift?: "none" | "left" | "right" | "up" | "down"; fit?: "cover" }
166
+ One still picture as the ground, with a slow push and an optional slow pan. It always covers the frame: the pan and the blur never show an edge.
167
+ BgSequence { items: { src: string; from: number }[]; transition?: "fade" | "blur" | "wipe" | "zoom"; transitionFrames?: 18; align?: "center" | "start"; dim?; blur?; tint?; zoom?; drift? }
168
+ Grounds that change smoothly. items are in frame order, the first visible from frame 0 (out of order or on the same frame is an error). The change is centred on from unless align is "start". "blur" cross-fades through a blur peak, "wipe" is a soft-edged wipe, "zoom" pushes the old one in while the new one settles. Each item keeps its own slow zoom through the change. A video src is drawn too (played once, held on its last frame).
169
+ bgPerScene(manifest.scenes, srcs): { src, from }[] makes items that change exactly on scene starts (which are on the beat); a scene whose src is undefined keeps the picture before it.
170
+ <BgSequence items={bgPerScene(manifest.scenes, manifest.scenes.map((s) => s.background && urls[s.background.key]))} tint={palette.hero} dim={0.4} />
171
+ BgAurora { bg; hero; speed?; intensity? (0.6) } BgGrid { bg; hero; speed?; intensity? (0.6) } BgBeams { bg; hero; speed?; intensity? (0.6) } BgGrain { bg; hero; speed?; intensity? (0.6) }
172
+ Grounds drawn in code, with no media, cheap to render. BgAurora: two or three large soft colour fields drifting and crossing. BgGrid: a faint perspective grid receding to a horizon, with a glow there. BgBeams: wide soft diagonal light beams sweeping very slowly. BgGrain: a still ground with a slowly breathing vignette and film grain that steps by the frame number. hero is the accent; speed 1 is the default pace. intensity (0 to 1, default 0.6) is how strongly the ground lights the base; Grade and Vignette on top darken it, so raise it to 0.8 under them (reference/backgrounds.md).
173
+ <BgAurora bg={palette.bg} hero={palette.hero} />
71
174
 
72
175
  fonts { display: string; body: string }
73
176
  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.
@@ -88,7 +191,7 @@ font(name): string
88
191
  greatVibes (formal script), sacramento (monoline script), outfit (geometric sans 100, 300, 500, 900), syne (wide heavy 800),
89
192
  lilitaOne (heavy rounded), kaushanScript (bold brush script), yellowtail (flowing brush script), mrsSaintDelafield (signature script),
90
193
  permanentMarker (marker lettering), bungeeShade (outlined sign capitals), cinzel (Roman capitals 700, 900: epic),
91
- playfairDisplay (elegant serif 700, 900), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
194
+ playfairDisplay (elegant serif 700, 900), jetbrainsMono (monospace 400, 700: terminals, labels), orbitron (sci-fi 700, 900), rye (western), creepster (horror).
92
195
  Hebrew text must use a Hebrew face; every face above is Latin only and Hebrew would fall back to a default font. Hebrew faces
93
196
  (all also cover Latin): heebo, rubik, notoSansHebrew (400, 700, 900: neutral sans), assistant (400, 700, 800), alef (400, 700),
94
197
  secularOne (strong headline), varelaRound (soft rounded), fredoka (playful rounded 500, 700), karantina (tall condensed 400, 700),
@@ -109,16 +212,37 @@ ScreenOverlay { src: string; durationSec?: number; opacity?: number }
109
212
 
110
213
  Sfx { src: string; at?: number; volume?: number }
111
214
  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>.
215
+ A pulled sound is levelled when it is pulled (the manifest's soundGain), and Sfx and Music apply that by themselves: the same volume sounds equally loud for every file. Nothing to pass.
112
216
  <Sfx src={urls["assets/lib/<id>/clip.mp3"]} at={10} volume={0.35} />
113
217
 
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.
218
+ SoundCues { cues: { at: number; sound: string; volume?: number }[]; sounds: Record<string, string>; volume?: number }
219
+ Plays many sound effects from one place, for a video scored with sound (a video with no voice). Place it once, outside the scenes. "at" is an absolute frame of the video, "sound" is a key into "sounds" (name -> link), a cue's own volume defaults to 0.35, and "volume" is a master level for all of them (default 1). It applies the manifest's levelling gain by itself, like Sfx. A cue whose sound is not in "sounds" stops the render with the names that exist.
220
+ <SoundCues sounds={{ click: urls["assets/lib/<id>/clip.mp3"], whoosh: urls["assets/lib/<id>/clip.mp3"] }} cues={[{ at: 0, sound: "whoosh" }, { at: 18, sound: "click", volume: 0.3 }]} />
221
+
222
+ cuesOnBeats(beatFrames, { from, to, every, sound, volume? }): { at, sound, volume? }[] cueBefore(frame, leadFrames): number
223
+ Helpers for SoundCues cues. cuesOnBeats gives a cue on every Nth beat from the frame "from" to the frame "to" (both included), starting with the first beat in that range. cueBefore gives the frame at which a sound leadFrames long must start to END on "frame" (a riser or a whoosh into a cut), never before frame 0.
224
+ const cues = [...cuesOnBeats(manifest.music?.beatFrames ?? [], { from: 0, to: 120, every: 2, sound: "tick", volume: 0.2 }), { at: cueBefore(s.startFrame, 12), sound: "whoosh" }];
225
+
226
+ cuesOnChanges(scenes, { impact?: { sound, volume? }, whoosh?: { sound, volume?, frames }, skip?: number[] }): { at, sound, volume? }[] changeFrame(scene): number cueOnCamera(frame, { punch?, punchFrames?, lead? }): number
227
+ Sounds for the scene changes, placed where the eye sees them. A SceneFrame transition is complete on the boundary but the picture changes most half the transition's frames before it (4 for the default 8, 3 for transitionFrames={6}; none for "cut"), and a hit on the boundary lands after the change. scenes are the manifest's scenes, each with the exit and transitionFrames its SceneFrame has: cuesOnChanges gives an impact on that peak and a whoosh (frames long, from the search line's duration times 30) that ENDS on it, for every change but the one after the last scene; skip lists scene indices to leave silent. changeFrame(scene) is the peak for one scene. A camera zoom, zoomTo or punch is a picture change too: cueOnCamera(frame) is the frame where the Camera move to the key at "frame" is fastest (about 9 frames before it); with punch: true, the fastest frame of a punch (just after its frame). The Camera counts frames from the scene's start when it is inside a SceneFrame, so add the scene's startFrame for an absolute cue.
228
+ const exits = [{ exit: "zoom-through" as const }, { exit: "whip-left" as const, transitionFrames: 6 }, {}]; const cues = cuesOnChanges(manifest.scenes.map((s, i) => ({ ...s, ...exits[i] })), { impact: { sound: "thud", volume: 0.55 }, whoosh: { sound: "whoosh", frames: 24, volume: 0.3 } });
229
+
230
+ cuesFor(events, kind, { offset?, fps?, voice?, volume?, seed?, riserFrames? }): { at, sound, volume }[] clearBefore(cues, frame, { seconds?, fps?, riserFrames? }): cues
231
+ Turns the event frames a component reports into SoundCues with a sensible sound role, level and density for the kind: "type" (promptFrames(...).type: a tick on every 2nd or 3rd character, at most 12 a second, a little different in level each, and a thock one frame after the last), "click" (clickFrames), "snap" (panelSettleFrame: a card or panel landing), "pop" (chapterFrames(...).badges), "count" ([start, settleFrame]: a train of ticks that speeds up and ends on a chime ON settleFrame), "stream" (terminalFrames(...).lines: one soft tick per line, at most 6 a second), "assemble" ([start, lockFrame]: a rising run that gets louder and a lock hit on lockFrame) and "resolve" ([frame] the logo or end card lands: a riser that ends on the chime, which is on the frame). offset is the scene's startFrame when the events are in the scene's own clock. voice: true gives the quieter levels for a film with a narrator (never over 0.3). Each cue's sound is a role: type-tick, type-return, click, snap, pop, count-tick, count-settle, stream, assemble-run, assemble-lock, resolve-riser, resolve-chime. Map each role to a pulled file in sounds, searching with: type-tick "mouse click"; type-return "glass tap"; click "mouse click"; snap "soft impact"; pop "bubble pop"; count-tick "counter ticks"; count-settle "success chime"; stream "glass tap"; assemble-run "counter ticks"; assemble-lock "soft impact"; resolve-riser "whoosh buildup"; resolve-chime "sparkle shimmer" (reelkit assets search "<words>" --kind sfx; the interface pack's names begin with sfx-ui-). clearBefore(cues, chimeFrame) removes every other cue in the second before the riser so the end card has a clear second of nothing; give the music a dip there (Music dips).
232
+ const p = promptFrames({ text, reply }, fps); const cues = [...cuesFor(p.type, "type", { offset: s.startFrame }), ...cuesFor([p.send], "click", { offset: s.startFrame }), ...cuesFor([settleFrame({ delay: 20 })], "snap", { offset: s.startFrame })];
233
+
234
+ Music { src: string; volume?: number; duckTo?: number; dips?: { from: number; to: number; volume: number }[] }
235
+ 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.18) while the voiceover is heard, steady across the 0.2 to 0.5 s between sentences, and comes up to volume (default 0.5) only in a real pause of 1.2 s or more (rising over 0.4 s and back down 0.25 s before the next word), so at the start before the first word and after the last, 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. dips are rests in absolute frames: inside each the track holds at that volume (with a short ramp either side), so it can fall quiet before the big moment: dips={[{ from: reveal.startFrame - 20, to: reveal.startFrame, volume: 0.05 }]}.
116
236
  <Music src={urls[manifest.music.key]} />
117
237
 
118
238
  nearestBeat(beatFrames, frame): number | undefined nextBeat(beatFrames, frame): number | undefined beatPulse(beatFrames, frame, decayFrames = 10): number
119
239
  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
240
  const b = nextBeat(manifest.music?.beatFrames ?? [], s.startFrame + 20) ?? s.startFrame + 20; // <Entrance delay={b - s.startFrame}>
121
241
 
242
+ wordFrame(scene, word, { nth?, edge?, absolute?, fps? }): number wordFrames(scene, words[], opts?): number[] onWord(scene, word, { lead?, ...wordFrame options }): number onWordBeat(scene, word, manifest.music, { window?, lead?, ... }): number sceneById(manifest, id): scene
243
+ Put things on the spoken word. scene is a manifest scene (it has the real word times). wordFrame is the frame inside the scene (counted like useCurrentFrame() in its SceneFrame) at which the word starts; word ignores case and punctuation, nth (1-based) picks a repeated word, a phrase ("whole week") gives the first word's start, edge "end" gives where it ends, absolute adds the scene's startFrame. A word the scene does not say THROWS with the scene's words listed, so a typo fails the render instead of timing to frame 0. onWord is the frame to START an entrance (a delay, a from, an at) so it is seen landing on the word: wordFrame minus lead (default 3), never below 0. onWordBeat is the same but lets the entrance land on a beat when one lies within window frames (default 3) AFTER the word; otherwise the word wins, and it is never earlier than onWord: with a narrator the voice is the clock for anything that shows a word, the beat is for decoration. sceneById(manifest, "id") finds the scene.
244
+ const meet = sceneById(manifest, "meet"); <Entrance delay={onWord(meet, "tasks")}><Card /></Entrance> <Sequence from={onWord(meet, "builds")}><Card /></Sequence> <Sfx src={urls[SFX.pop]} at={wordFrame(meet, "energy") - 2} volume={0.25} />
245
+
122
246
  Voiceover { src: string; volume?: number }
123
247
  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.
124
248
  {s.voiceoverKey ? <Voiceover src={urls[s.voiceoverKey]} /> : null}
@@ -158,3 +282,8 @@ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
158
282
  );
159
283
  };
160
284
  ```
285
+
286
+ ## What the checks can see
287
+
288
+ - `reelkit check` counts sounds only where the code places them literally. When the cues are computed (a loop, `cuesOnBeats`, a helper, a variable that is not a plain list) its sound-count notes stay silent, so the absence of a note proves nothing. For a film with no voice, the sound report after `reelkit render` is the real count.
289
+ - `reelkit preview` prints the continuity and beat lines. It prints no rhythm line: how even the scene lengths are is `reelkit plan check`'s.
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: launch-film
3
+ description: Use when the video is a product, feature or brand reveal with no narrator - a short launch film carried by music and sound effects alone: the plan with voice none, the shape of the film, the pictures, and a full section on scoring it with sound.
4
+ ---
5
+
6
+ # A launch film with no voice
7
+
8
+ The short films that studios post when a product launches have no narrator. The words are on the screen, and the sound (a music track and a great many small effects) does the work a voice would. This file is how to make one. Read `reference/continuity.md` too: the shots must grow out of each other.
9
+
10
+ ## When to choose it
11
+
12
+ Choose it for a product, a feature or a brand reveal where nobody needs to explain anything out loud. If the user wants a person to talk, or a story told in sentences, it is the wrong tool: write a narrated video instead. Ask the user whether the video has a narrator; if not, this file applies. Skip the voice and caption steps.
13
+
14
+ ## What good ones measured
15
+
16
+ The owner measured four launch films of 15 to 55 seconds. Use these numbers as targets.
17
+
18
+ - No speech at all. Loudness between -14.1 and -14.8 LUFS in every one.
19
+ - 2.0 to 3.0 sound hits a second over the whole film (music beats and effects together), 1.5 to 2.8 of them low, and a whoosh-like swell every one to two seconds (0.5 to 1.3 a second).
20
+ - A hit lands within a tenth of a second of 55 to 75 percent of the big changes in the picture.
21
+ - Sound starts in the first 0.05 seconds in three of the four. The first big change in the picture comes between 0.7 and 3.8 seconds.
22
+ - Shots average 2.2 to 5.2 seconds; the shortest is 0.4 to 1.0 and the longest 6.5 to 12.7. The lengths vary by 0.64 to 0.77 (spread over average). The picture is still for 11 to 35 percent of the time.
23
+ - A third to a half of the cuts land on the music's beat. The tempo, where there was a clear beat, was 107 to 145 BPM.
24
+ - The look: one calm ground (near-white with a soft colour wash, near-black, or a warm photographic set), one accent colour, the product's logo tile at the start or the end, and a line of type under the object rather than over it.
25
+
26
+ ## The plan
27
+
28
+ Write `plan.json` with `"voice": "none"`. Every scene then has `seconds` (a number from 0.3 to 15) and `narration` may be an empty string; `voiceId` is not needed, and `captions` is `none` unless you set it. Up to 16 scenes are allowed in this mode.
29
+
30
+ - The film is 15 to 45 seconds and has 6 to 14 shots.
31
+ - Lengths vary at least fourfold: one or two shots under a second, one long hold, the rest in between.
32
+ - On-screen text carries every word the viewer will read, so each scene has `onScreenText` or `notes` saying what is on screen (`reelkit plan check` flags a scene with neither).
33
+ - The product's logo comes first or last.
34
+ - One calm ground and one accent colour for the whole film; write both into the first scene's `notes` with the look from `reference/styles.md` ("Launch film").
35
+
36
+ `reelkit plan check` prints `Length: 24.5s over 9 scenes.` and notes a film under 10 or over 60 seconds, and one with no scene under a second. `reelkit assets voiceover` says there is nothing to record. Build the composition from `manifest.scenes` as usual: no scene has a `voiceoverKey` and `s.words` is empty, so place no `<Voiceover>` and no `<Captions>`.
37
+
38
+ ## The shape
39
+
40
+ 1. Open on motion and sound at once, in the first frame.
41
+ 2. One line of type that sets up the product.
42
+ 3. The product's interface, as an object in space.
43
+ 4. Three to five feature beats, each with a pointer or a change that has a visible cause.
44
+ 5. A quick burst of short shots.
45
+ 6. A rest: one calm shot, and near silence before the biggest moment.
46
+ 7. The name and one action.
47
+
48
+ ## Structure and briefing
49
+
50
+ What the strongest short launch films have in common, in rules a plan can follow:
51
+
52
+ - **Act each claim out.** A claim is performed in a small believable interface, never stated: a speed claim is a `CounterRoll`, an agent or tool a `TerminalLog`, an assistant a `PromptBox` with a reply, a web product a `BrowserFrame` with the user's own screenshot, collaboration a `NamedCursor`. Choose which claims are highlighted and which are merely mentioned: at most three highlighted in 30 seconds.
53
+ - **One grammar.** Either every scene is a `ChapterFrame` (the same slots: "03 / 07", a ghost word, one hero, up to three badges, one caption, a progress rule) or none is. Write a colour script into the plan notes: one accent colour, and for each scene whether its ground is dark or light, so the grounds alternate on purpose.
54
+ - **One hero object carried across cuts**, changing pose or scale: a `Hero3D` or `Screen3D` with `poses`, a `GlassPanel`, or a `Carry`.
55
+ - **Type with a voice.** A headline is two to five words with one emphasised keyword (`Headline`); never plain centred fade-in lines.
56
+ - **Open on the question.** When the product is an assistant or a tool, the first scene is the user's own question typed into a `PromptBox`. Always end on an end card: the wordmark, one line, one call to action, with a clear second before it (`clearBefore`).
57
+ - **Ask for the real material before writing**: screenshots, the real numbers, the logo, the exact product name; and use it. No third-party brand marks.
58
+ - **Length.** 15 to 40 seconds; state it in the plan. Over 60 seconds the same effect repeats.
59
+ - **Review.** After `reelkit preview`, check the contact sheet: no text smaller than about 28 px at 1080 wide, every claim has a visible demonstration, the grounds alternate as planned, the hero object is in every scene it should be. Fix and preview again, at most twice.
60
+
61
+ ## The pictures
62
+
63
+ - Use the user's real screens (`reelkit assets upload`), never an invented interface. Put each in a device or browser frame from the library (`reelkit assets search "device frame" --kind component`), tilted in space and settling as it arrives.
64
+ - Type goes under or beside the object, with one coloured word. Keep each line to a few words.
65
+ - Use `Carry` and `Camera` from `reference/continuity.md` so that each shot grows out of the one before: the screen that settled is the card that the next shot opens on.
66
+ - A pointer that clicks, a card that lands, a tile that opens: every change on screen has a cause you can see, and the cause has a sound.
67
+
68
+ ## Sound
69
+
70
+ With no voice the sound is half of the film. Plan it as you plan the picture.
71
+
72
+ 1. **Choose the track first and let the beat set the cuts.** Search for 107 to 145 BPM (`reelkit assets search "driving clean product launch, 120 BPM" --kind music`), pull it with `reelkit assets pull <id> --music`, and read `reference/beat-sync.md`. The CLI holds each scene to the next beat, so every cut lands on the beat, a little more than the films did. Let the effects fill the places between.
73
+ 2. **Sound from the first frame.** A cue at frame 0 and the music running from frame 0. `reelkit check` notes a film with nothing placed in the first half second.
74
+ 3. **Every visible change has a sound, on the frame where the eye sees it.** A press clicks, a card arriving pops, a move whooshes, a scene change lands on a low hit, and a reveal is led in by a riser that ends on it (`cueBefore`). Two things the measurement showed:
75
+ - A `SceneFrame` transition is complete on the boundary, but the picture changes most in the middle of it: for a zoom-through, blur, push or whip exit that is half the transition's frames before the boundary (4 frames for the default 8, 3 for `transitionFrames={6}`). A hit on the boundary frame lands after the change and does not register against it. Use `cuesOnChanges(scenes, { impact, whoosh })`: it puts the impact on the visual peak and the whoosh so that it ENDS there. Give each scene the same `exit` and `transitionFrames` you give its `SceneFrame` (one array for both, so they cannot disagree).
76
+ - A camera zoom, `zoomTo` or punch is a picture change too and needs its own hit. `cueOnCamera(frame)` gives the frame where a `Camera` key's move is fastest (about 9 frames before the key's frame, because the move starts 12 frames early); `cueOnCamera(frame, { punch: true })` gives it for a punch (just after its frame). Inside a `SceneFrame` the Camera counts frames from the start of the scene: add the scene's `startFrame` for an absolute cue.
77
+ 4. **Density.** Aim for about one placed effect a second on top of the music: more in the burst, none in the rest. With the beats this gets near the measured 2 to 3 hits a second. `reelkit check` counts what the code places and says when it is under one per two seconds; that is a count of the code, not a listening test. Its sound-count notes stay silent when the cues are computed (built by a loop, a helper such as `cuesOnBeats`, or a variable that is not a plain list), so their absence proves nothing: use the render's sound report to see what the film really has.
78
+ 5. **One family of sounds.** Take them from the interface pack in `reference/sound-design.md`, and mix families only when one is missing what you need. For the music, see "Background music" there.
79
+ 6. **Levels.** For a film with no voice these are the numbers, and they replace any others you have read, including `reference/sound-design.md` and the narrated range in `reference/beat-sync.md`:
80
+
81
+ | What | `volume` |
82
+ |---|---|
83
+ | Music | 0.4 (0.5 at most) |
84
+ | Effects: pops, taps, clicks, chimes, ticks | 0.35 to 0.5 |
85
+ | Impacts and drops on a scene change | 0.5 to 0.6 |
86
+ | Swells: whooshes that rise, risers | 0.25 to 0.35 |
87
+
88
+ Every pulled sound is levelled on the way in (the interface effects got a gain of 0.7 to 0.8), and `reelkit render` then masters the whole film to -14 LUFS, so only the balance between these matters. At 0.2 to 0.3 an effect sits 6 dB or more under the music, which the tester of 0.8.0 found too quiet to register against the picture; a swell above about 0.35 hides the impact that ends it (the impact no longer rises out of what came before, and the report counts a change with no hit: on a test film, whooshes at 0.2 left 5 of 6 changes with a hit and at 0.35 or more only 2 of 6). Read the report after the render and move a level only if it says so.
89
+ - **Which sounds swell.** The library's own "UI" pack (`sfx-ui-soft-whoosh`, `sfx-ui-fast-whip`) is made of short bursts that start at full level and fall away: they are hits to a listener and to the report, not swells. A swell that carries rises for half a second or more and cuts off on the change. Search for these words: `reelkit assets search "reverse whoosh" --kind sfx` (the reverse whooshes of the Quantum Motion pack, 1.7 to 2.3 seconds, with energy between 2 and 10 kHz), `"whoosh sweep"` and `"wind sweep"` for longer air, and `"riser building up"` or `"riser tension"` for a lead into a reveal. Their length is at the end of the search line: give it to `cueBefore` or `cuesOnChanges` (`whoosh: { frames }`), minus the tail after the peak. Measured on a test film, seven reverse whooshes at 0.2 gave six swells where they stand, while the film's UI whooshes gave three at the same places.
90
+ 7. **Silence.** Before the biggest moment, drop the effects, or the music, for a beat or two. The hit that follows then lands. Give `Music` a `dips` entry for the rest, in absolute frames: `dips={[{ from: reveal.startFrame - 24, to: reveal.startFrame, volume: 0.05 }]}` brings the track down for the last moments before the reveal and back up after it.
91
+
92
+ Place all the cues once, outside the scenes, with `SoundCues`; use `cuesOnBeats` for a pulse on the beat, `cuesOnChanges` for the sounds of the scene changes, `cueOnCamera` for a camera move or punch, and `cueBefore` for a sound that must end on a frame (give it the length of the sound in frames).
93
+
94
+ ```tsx
95
+ import React from "react";
96
+ import { AbsoluteFill } from "remotion";
97
+ import { Music, SceneFrame, SoundCues, cueBefore, cuesOnBeats, cuesOnChanges } from "reelkit/kit";
98
+ import type { VideoProps } from "reelkit/kit";
99
+
100
+ export const Video: React.FC<VideoProps> = ({ manifest, urls }) => {
101
+ const beats = manifest.music?.beatFrames ?? [];
102
+ const [, product, , burst, reveal] = manifest.scenes;
103
+ const sounds = {
104
+ whoosh: urls["assets/lib/<whoosh id>/clip.mp3"],
105
+ click: urls["assets/lib/<click id>/clip.mp3"],
106
+ pop: urls["assets/lib/<pop id>/clip.mp3"],
107
+ thud: urls["assets/lib/<impact id>/clip.mp3"],
108
+ riser: urls["assets/lib/<riser id>/clip.mp3"],
109
+ };
110
+ // The same exit and transitionFrames as the SceneFrames below, so the cues and the picture agree.
111
+ const exits = [{ exit: "zoom-through" as const }, {}, { exit: "whip-left" as const, transitionFrames: 6 }, {}, {}];
112
+ const cues = [
113
+ { at: 0, sound: "whoosh", volume: 0.3 },
114
+ { at: product.startFrame + 6, sound: "pop", volume: 0.4 },
115
+ ...cuesOnBeats(beats, { from: burst.startFrame, to: reveal.startFrame, every: 1, sound: "click", volume: 0.4 }),
116
+ // An impact on the visual peak of every scene change and a whoosh that ends on it. frames is the whoosh's length: its duration from the search line times 30.
117
+ ...cuesOnChanges(manifest.scenes.map((s, i) => ({ ...s, ...exits[i] })), { impact: { sound: "thud", volume: 0.55 }, whoosh: { sound: "whoosh", frames: 24, volume: 0.3 } }),
118
+ // The riser's length in frames is its duration from the search line (`(0.8s)`), times 30. Do not assume a length.
119
+ { at: cueBefore(reveal.startFrame, 24), sound: "riser", volume: 0.3 },
120
+ ];
121
+ return (
122
+ <AbsoluteFill>
123
+ {manifest.music ? <Music src={urls[manifest.music.key]} volume={0.5} dips={[{ from: reveal.startFrame - 24, to: reveal.startFrame, volume: 0.05 }]} /> : null}
124
+ {manifest.scenes.map((s) => (
125
+ <SceneFrame key={s.id} from={s.startFrame} durationInFrames={s.durationFrames} {...exits[manifest.scenes.indexOf(s)]}>
126
+ {null}
127
+ </SceneFrame>
128
+ ))}
129
+ <SoundCues cues={cues} sounds={sounds} />
130
+ </AbsoluteFill>
131
+ );
132
+ };
133
+ ```
134
+
135
+ The riser in this example is 0.8 seconds long, which is 24 frames, so `cueBefore(reveal.startFrame, 24)` starts it 24 frames before the reveal and it ends on it. The whooshes of `cuesOnChanges` are 24 frames too. Take that length from the pulled file: `reelkit assets search` prints each sound's duration at the end of its line (for example `(0.8s)`), so a 1.5-second riser needs 45 frames. The whoosh at frame 0 opens the film with sound; the dip gives the reveal its rest.
136
+
137
+ For the one hero moment (the name landing, a screen floating in space) the kit has a 3D layer: see `reference/three-d.md`.
138
+
139
+ For the ground behind the film, an animated ground or a dimmed video is an option: see `reference/backgrounds.md`.
140
+
141
+ ## Size and typeface of library interface pieces
142
+
143
+ Library components (widgets, banners, lists) come small by default, sized for a page and not a phone. An interface piece fills at least 70 percent of the frame's width: scale it with a wrapper (`transform: scale(...)` with a `transformOrigin`) or with its `size` prop if it has one. One typeface for the film: pass the film's font (`font("outfit")`, for example) to every component that takes one, and prefer the components that do. `Camera` frames: a `Camera` inside a `SceneFrame` counts frames from the start of that scene (frame 0 is the scene's first frame, because a `SceneFrame` is a Sequence), so `zoomTo(44, ...)` is 44 frames into the scene; a `Camera` outside every `SceneFrame`, around all of them, counts the video's absolute frames.
144
+
145
+ ## No screenshots
146
+
147
+ When the user has no real screens to show, build the interface pieces from library components and plain cards, inside a device or card frame, and mark nothing as the user's real app: no real-looking logo, no invented numbers presented as true. The library's components have different defaults for colour and typeface, so pass the film's one accent colour and one typeface to EVERY library component through its props, or the film will show three accents and three typefaces. With real screens, use them instead (see "The pictures").
148
+
149
+ ## Sound for interface events
150
+
151
+ A product surface (`PromptBox`, `TerminalLog`, `NamedCursor`, `GlassPanel`, `ChapterFrame`, `CounterRoll`) is performed on screen, so its sounds are small and exact. Every such component has a companion function that returns the frames of its events (`promptFrames`, `terminalFrames`, `clickFrames`, `panelSettleFrame`, `chapterFrames`, `settleFrame`), and `cuesFor(events, kind, { offset })` turns those frames into cues. The rule: the sound sits on the frame the picture settles, the frame the companion function gives, not on the beat grid and not on the start of the move. Add the scene's `startFrame` as `offset`; the frames come out in the scene's own clock.
152
+
153
+ | Event | `cuesFor` kind and roles | Search words for the file | Length | Level without / under a voice | How many per 10 s |
154
+ |---|---|---|---|---|---|
155
+ | Typed text | `type`: `type-tick`, then `type-return` one frame after the last character | `mouse click`, `glass tap` | 0.2 s, 0.5 s | 0.2 and 0.4 / 0.1 and 0.2 | 6 to 12 ticks in 1 to 3 runs; never over 12 a second |
156
+ | Cursor click, send button | `click`: `click` | `mouse click` | 0.2 s | 0.4 / 0.22 | 1 to 3 |
157
+ | Card or panel lands | `snap`: `snap` | `soft impact` | 0.7 s | 0.45 / 0.25 | 2 to 5 |
158
+ | Badge or chip appears | `pop`: `pop` | `bubble pop` | 0.5 s | 0.4 / 0.22 | 3 to 6 |
159
+ | Number counting | `count`: `count-tick` train, `count-settle` on `settleFrame` | `counter ticks`, `success chime` | 0.1 s each, 1 s | 0.2 and 0.45 / 0.1 and 0.25 | 0 to 2 trains |
160
+ | Terminal lines | `stream`: `stream`, one per line | `glass tap` | 0.5 s | 0.18 / 0.1 | 5 to 15; never over 6 a second |
161
+ | Pieces assemble | `assemble`: `assemble-run` rising, `assemble-lock` on the last piece | `counter ticks`, `soft impact` | 0.1 s each, 0.7 s | 0.25 rising to 0.5, and 0.55 / 0.12 and 0.3 | 0 to 1 per film |
162
+ | Logo or end card | `resolve`: `resolve-riser` ending on `resolve-chime` | `whoosh buildup`, `sparkle shimmer` | 1.2 s, 1.5 s | 0.35 and 0.55 / 0.2 and 0.3 | once, at the end |
163
+
164
+ The names are the roles: map each to a pulled file in the `sounds` object of `SoundCues` (the interface pack's files are named `sfx-ui-...`: `sfx-ui-key-clicks`, `sfx-ui-glass-tap`, `sfx-ui-bubble-pop`, `sfx-ui-soft-impact`, `sfx-ui-count-ticks`, `sfx-ui-success-chime`, `sfx-ui-sparkle-shimmer`; search first with `reelkit assets search "<words>" --kind sfx` and read each file's length). A film with a narrator passes `voice: true` for the quieter column: nothing is ever over 0.3 while the voice speaks. Scene changes keep `cuesOnChanges`; a `color-push`, `flash`, `ring` or `flip` change peaks on the boundary frame itself, and `changeFrame` knows it.
165
+
166
+ Give the end card a clear second: `clearBefore(cues, chimeFrame)` removes every other cue in the second before the riser, and a `Music` dip over the same span leaves the riser alone with the picture. `cuesFor` is checked by tests: counts, the caps above, no two cues of one role on one frame, and the same cues every time.
167
+
168
+ ```tsx
169
+ const q = promptFrames({ text: question, reply }, manifest.fps);
170
+ const cues = [
171
+ ...cuesFor(q.type, "type", { offset: ask.startFrame }),
172
+ ...cuesFor([q.send], "click", { offset: ask.startFrame }),
173
+ ...cuesFor([settleFrame({ delay: 8, frames: 40 })], "count", { offset: stat.startFrame }),
174
+ ];
175
+ ```
176
+
177
+ ## Read the sound report
178
+
179
+ `reelkit render` measures the finished file and prints one line for a film with no voice; `reelkit sound [file]` prints it again for `out/video.mp4` or any file, and `reelkit sound --detail` lists every event the counts were made from: each picture change (time, frame, strength) with the nearest hit and its distance, each swell (start, peak, length) and the first sound; with `--json` the same as arrays. It is a measurement of the audio and the picture, not a judgement of how the film sounds: listen to it as well. What the numbers mean:
180
+
181
+ - **Hits a second** (`hitsPerSecond`): sudden rises in the loudness (8 dB or more over the sound 48 milliseconds before, whatever else the film holds), over the whole sound. Targets: 2 to 3. Low: add clicks, pops and a thud on the scene changes. High: some moves can stay silent.
182
+ - **Swells** (`swellsPerSecond`): a rise of the band between 0.5 and 10 kHz, the air of a whoosh or a riser, of 4 dB or more above its own level over the second and a half around it, building over at least a tenth of a second. The level is local, so one loud riser does not hide the quiet whooshes around it, and steady music gives none. Targets: one every 1 to 2 seconds, so 0.5 to 1.3 a second. Low: put a whoosh that rises on each move and a riser before each reveal (a burst that starts at full level is a hit, not a swell; see "Which sounds swell" above). The targets come from the four measured films, with the detector the owner used then; this one counts about the same sounds, but treat the target as a direction.
183
+ - **Picture changes with a hit** (`changesWithHit` of `pictureChanges`): how many of the big changes in the picture have a hit within a tenth of a second. A transition is a span of frames, not one frame: the hit is measured to the nearest point of the span that changes (the run of frames above the change threshold around the peak), so a hit on the boundary at the end of a five-frame transition counts. Camera zooms and punches are picture changes too. The films had 55 to 75 percent. Low: a hit on each change that matters, on the visual peak (`cuesOnChanges`, `cueOnCamera`); `--detail` shows which changes have none and how far the nearest hit is.
184
+ - **First sound** (`firstSoundSec`): the films began within 0.05 seconds. Later: add a cue at frame 0.
185
+
186
+ The same numbers are in the render's `data.sound`. A measurement that cannot be made never fails the render.
187
+
188
+ ## Check before you show it
189
+
190
+ `reelkit preview` shows two frames per scene and prints a continuity line and a beat line (how many scene changes carry something across and land on the beat). It prints no rhythm line: the rhythm of the scene lengths is `reelkit plan check`'s. Look for a shot that holds still for long (the films were still for 11 to 35 percent of the time and no more), a shot whose type is over the object, and a second accent colour. Then show the user the frames, as in the main steps.
@@ -22,9 +22,9 @@ description: Use when writing Video.tsx - the file rules, timing model and media
22
22
 
23
23
  Each `scenes` entry:
24
24
  - `id`: the scene id from `plan.json`.
25
- - `startFrame`, `durationFrames`: frames on the whole video's timeline. A scene lasts its narration plus 0.4 s of breathing room, rounded up to a whole frame.
25
+ - `startFrame`, `durationFrames`: frames on the whole video's timeline. A scene lasts until its last word has ended plus the plan's `gap` (0.3 s by default; the next scene's voice starts on the scene's first frame), rounded up to a whole frame; the last scene keeps 0.7 s. A change may move up to 4 frames to reach a beat.
26
26
  - `voiceoverKey`: the project path of the narration audio.
27
- - `words`: one entry per spoken word, `{ word, startSec, endSec }`, in seconds from the start of that scene (not of the video, and not frames). Convert with `Math.round(sec * fps)` for a frame inside the scene; `<Captions words={s.words} />` takes them as they are.
27
+ - `words`: one entry per spoken word, `{ word, startSec, endSec }`, in seconds from the start of that scene (not of the video, and not frames). Do not convert them yourself: `wordFrame(s, "word")` and `onWord(s, "word")` give the frame inside the scene (reference/voice-sync.md); `<Captions words={s.words} />` takes them as they are.
28
28
  - `imageKey`: only on an illustration scene that has its image; the project path of the image.
29
29
  - `userAssetKeys`: project paths of the user's own files that the plan assigned to this scene (an empty array when none).
30
30
 
@@ -49,9 +49,10 @@ Each `scenes` entry:
49
49
 
50
50
  ## Media
51
51
  - Reference media only as `urls[path]`, where `path` is the file's project path, using the paths in the manifest: `s.voiceoverKey`, `s.imageKey`, `s.clipKey`, `s.clipKeyedKey`, `s.userAssetKeys`, `manifest.footageKey`. Never hard-code a URL.
52
- - A scene with narration has `s.voiceoverKey`: give it exactly one `<Voiceover src={urls[s.voiceoverKey]} />` inside its SceneFrame, and `<Captions words={s.words} />` unless the notes say otherwise.
52
+ - A scene with narration has `s.voiceoverKey`: give it exactly one `<Voiceover src={urls[s.voiceoverKey]} />` inside its SceneFrame, and `<Captions words={s.words} />` unless the notes say otherwise. In a plan with `voice: "none"` no scene has a `voiceoverKey` and `s.words` is empty, so render no `<Voiceover>` and no `<Captions>` (`manifest.captions` is `"none"`); keep the guard `s.voiceoverKey ? ... : null` so the same code works either way.
53
53
  - Footage mode (`manifest.footageKey` is set): one `<FootageLayer>` at the bottom, outside the scenes.
54
54
  - User images: `<Img src={urls[key]} />` from remotion.
55
+ - Write a media path as one plain string, so that `reelkit check` can verify the file exists. A path built with a template string or by joining parts (`` `assets/lib/${id}/hit.mp3` ``) cannot be checked and is reported as missing. Write `"assets/lib/sfx-ab12/hit.mp3"` in full, or read the key from the manifest.
55
56
 
56
57
  ## Layer stack (every video, bottom to top)
57
58
  1. Background: `<BgMesh>` from the kit, or `<FootageLayer>` in footage mode. Never a flat solid colour.
@@ -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.