reelkit-cli 0.12.4 → 0.12.5

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/CHANGELOG.md CHANGED
@@ -3,6 +3,12 @@
3
3
  What changed in each published version of `reelkit-cli`, newest first. A push to `main` publishes the version in
4
4
  `package.json` when it is not on npm yet, and it must have an entry here.
5
5
 
6
+ ## 0.12.5 - 2026-10-09
7
+
8
+ - The skill builds a film from component files instead of one long `Video.tsx`: every drawn part that could appear in another film is its own file in `src/`, so it can be reused and shows as a part of a template.
9
+ - A template lists the components a composition declares for itself, and ties a component to a scene when its name is the scene's id.
10
+ - A template carries the spoken words of each scene, for a subtitles row on its timeline, and the sound effects the project uses, so a sound on the timeline copies the command that pulls it.
11
+
6
12
  ## 0.12.4 - 2026-10-09
7
13
 
8
14
  - A template's timeline shows the music and its beats to the end of the film when the track repeats, not only for the track's own length.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reelkit-cli",
3
- "version": "0.12.4",
3
+ "version": "0.12.5",
4
4
  "description": "CLI and Claude skill for making short-form video with a shared asset library.",
5
5
  "license": "MIT",
6
6
  "author": "Daniel Livshin",
package/skill/SKILL.md CHANGED
@@ -15,7 +15,7 @@ For image or video cutouts, editable Blender product shots, custom 3D materials,
15
15
 
16
16
  To implement a chosen style, read its row in `reference/style-recipes.md` for action, timing, sound and proof. `reference/style-components.md` maps the shared components and built-in kit to shot roles; inspect the selected source before using its props.
17
17
 
18
- This skill targets `reelkit-cli` 0.12.4 and its HyperFrames kit. Check the installed version with `reelkit --version`; in a source checkout, invoke its `bin/reelkit.mjs` with Node. Use the matching CLI before authoring with `reelkit/frame`.
18
+ This skill targets `reelkit-cli` 0.12.5 and its HyperFrames kit. Check the installed version with `reelkit --version`; in a source checkout, invoke its `bin/reelkit.mjs` with Node. Use the matching CLI before authoring with `reelkit/frame`.
19
19
 
20
20
  ## What decides whether the film is accepted
21
21
 
@@ -133,6 +133,7 @@ Read `reference/hyperframes-composition.md` for the seekable HyperFrames timing
133
133
  - For a brand reveal, a transformation, or a shot with real depth, read `reference/brand-motion.md` and `reference/three-d.md`. Choose the move by what it explains, preserve the supplied identity, and share its event frames with the sound cues. Keep reading-heavy shots in 2D; a requested brand film or showreel may use several purposeful 3D moments.
134
134
  - For what sits behind the whole film (a video, a picture that changes, or an animated ground), read `reference/backgrounds.md`; pick one ground and keep it.
135
135
  - Before writing a new component, read `reference/component-authoring.md`.
136
+ - **Build the film from component files, not one long `Video.tsx`.** Every drawn part that could appear in another film (a text reveal, a card, a character, a chart, a cursor, a shape that morphs, a field of particles) is its own file in `src/`, driven by props, with the comment that describes it. `Video.tsx` keeps only what belongs to this film: the palette, the words, the frame numbers, where each part sits in each scene, and the sound. A helper declared inside `Video.tsx` that returns JSX and holds none of this film's words is a component that was not split out; move it. A film of several scenes should end with several component files, and they are what the user reuses in the next video and what a template shows as its parts.
136
137
  - Write `src/Video.tsx` and any component files beside it. `Video.tsx` may import only `react`, `reelkit/frame`, `reelkit/kit` and sibling components (`./Name`).
137
138
  - Run `reelkit lint --no-video` together with `reelkit check`. It reads the source: right-to-left text with no direction, vowel points in on-screen text, a sequence that starts after its parent ends, and a shot written on a range the film never draws.
138
139
  - `manifest.json` is the exact object passed to `Video` as the `manifest` prop. Media is referenced as `urls[path]`, where `path` is the file's path in the project, such as `urls[scene.voiceoverKey]` or `urls["assets/lib/<id>/clip.mp3"]`.
@@ -10,6 +10,25 @@ Run `reelkit assets search "<description>" --kind component`. If a library compo
10
10
 
11
11
  Use `reference/style-components.md` to choose the role and `reference/style-recipes.md` for the shot's motion and proof. A catalog name does not mean a built-in kit export or a currently published asset; search, pull and inspect the actual source first.
12
12
 
13
+ ## Split the film into parts
14
+ Searching first is about not writing a part twice. It is not a reason to write everything inside `Video.tsx`: a film whose whole picture lives in one file leaves nothing to reuse, shares nothing, and shows as one block when it is saved as a template.
15
+
16
+ Before writing `Video.tsx`, list the parts the film draws and decide where each one lives:
17
+
18
+ | It is | It goes | Example |
19
+ |---|---|---|
20
+ | A drawn thing with no words of its own | Its own file in `src/`, all values as props | `MaskRise` (a line that rises from behind a mask), `Blob`, `EyedCharacter`, `ChartCard`, `TabChip`, `DrawnCheck` |
21
+ | A way of placing or timing other things | Its own file, taking `children` | `Pin` (centre children on a point), `Shake`, `Orbit` |
22
+ | This film's scene: which parts, which words, which frames | `Video.tsx` | the `hook` shot: a `ChartCard` at frame 4, "Great month." at 53, the slam at 98 |
23
+ | The palette, the sound list, the frame numbers | `Video.tsx`, passed down as props | the blob gets `color={c.hero}` |
24
+
25
+ Rules of thumb:
26
+ - A function in `Video.tsx` that returns JSX and holds none of this film's words is a part. Move it to a file.
27
+ - A scene function may stay in `Video.tsx`, named after its scene id (`Hook` for `hook`, `OneNumber` for `one-number`), and should be mostly a list of parts with props. If it is long because it draws shapes itself, the shapes are parts.
28
+ - One part needs another (a monster has eyes): pass it in as `children` or a prop, since a component file imports no other component file.
29
+ - The same thing lives through several scenes (one ball that changes shape): one part with the values as props (`x`, `y`, `radius`, `morph`), and `Video.tsx` owns the keyframes.
30
+ - Do not split for its own sake: a two-line wrapper used once is not a part.
31
+
13
32
  ## Rules for a component file
14
33
  - One component per file. File `NumberBadge.tsx` exports `export const NumberBadge: React.FC<NumberBadgeProps>`.
15
34
  - Imports only from `react`, `reelkit/frame` and `reelkit/kit`. A component never imports another component file.
@@ -56,7 +56,7 @@ export function filmOf(project: Project): TemplateFilm | undefined {
56
56
  return {
57
57
  id: cut(m.id, 80), start: Math.max(0, Math.round(m.startFrame)), frames: Math.max(0, Math.round(m.durationFrames)),
58
58
  ...(p?.treatment ? { treatment: cut(p.treatment, 60) } : {}), ...(narration ? { narration: cut(narration, 4000) } : {}),
59
- ...(text.length ? { text } : {}), ...(m.words.length ? { words: m.words.slice(0, 600).map((w) => Math.max(0, Math.round(w.startSec * 1000) / 1000)) } : {}),
59
+ ...(text.length ? { text } : {}), ...(m.words.length ? { words: m.words.slice(0, 600).map((w) => Math.max(0, Math.round(w.startSec * 1000) / 1000)), spoken: m.words.slice(0, 600).map((w) => cut(String(w.word ?? ""), 80)) } : {}),
60
60
  ...(p?.notes?.trim() ? { notes: cut(p.notes.trim(), 2000) } : {}),
61
61
  ...(used.some((c) => c.scenes.includes(m.id)) ? { components: used.filter((c) => c.scenes.includes(m.id)).map((c) => cut(c.name, 80)).slice(0, 60) } : {}),
62
62
  };
@@ -66,9 +66,18 @@ export function filmOf(project: Project): TemplateFilm | undefined {
66
66
  voice: plan.voice !== "none",
67
67
  ...(manifest.captions ? { captions: manifest.captions } : {}),
68
68
  ...cuesOf(project, manifest, music?.key),
69
+ ...soundsOf(project),
69
70
  };
70
71
  }
71
72
 
73
+ // The sounds the project pulled from the library, by id: what a timeline can offer to copy when it cannot say which sound a hit is.
74
+ function soundsOf(project: Project): Pick<TemplateFilm, "sounds"> {
75
+ const sounds = Object.entries(project.readJsonOr<Record<string, { kind?: string; title?: string }>>(FILES.library, {}))
76
+ .filter(([, p]) => p.kind === "sfx" || p.kind === "music")
77
+ .map(([id, p]) => ({ id: id.slice(0, 128), title: (p.title ?? id).slice(0, 200), kind: p.kind! })).slice(0, 300);
78
+ return sounds.length ? { sounds } : {};
79
+ }
80
+
72
81
  // The music as it plays in the film. A track shorter than the film is repeated by the composition, and the manifest holds every beat
73
82
  // that falls inside the film (the repeats too), so those are the beats shown; the track then runs to the film's end. Without them the
74
83
  // track's own beats and length are used.
@@ -194,6 +194,8 @@ export const TemplateFilmSchema = z.object({
194
194
  scenes: z.array(z.object({
195
195
  id: text(80), start: frames, frames, treatment: text(60).optional(), narration: text(4000).optional(),
196
196
  text: z.array(text(400)).max(20).optional(), words: z.array(z.number().min(0)).max(600).optional(),
197
+ // The spoken words themselves, one for each entry of `words`, so the captions can be laid out in time.
198
+ spoken: z.array(text(80)).max(600).optional(),
197
199
  // The plan's visual direction for the scene, and the components the composition times to it (read from its source).
198
200
  notes: text(2000).optional(), components: z.array(text(80)).max(60).optional(),
199
201
  })).min(1).max(200),
@@ -209,6 +211,8 @@ export const TemplateFilmSchema = z.object({
209
211
  // The sound effects by name, when the render recorded them: each one's title, when it starts and how long it plays, in seconds.
210
212
  // `id` is the sound's id in the library, when it was pulled from there.
211
213
  cues: z.array(z.object({ at: z.number().min(0), seconds: z.number().min(0), title: text(200), kind: text(24).optional(), id: text(128).optional() })).max(1500).optional(),
214
+ // The sounds the project pulled from the library (effects and music), whether or not the render recorded when each one plays.
215
+ sounds: z.array(z.object({ id: text(128), title: text(200), kind: text(24) })).max(300).optional(),
212
216
  // Every component the composition uses: one written in the project, one pulled from the library, or one of the kit's. `uses` is how
213
217
  // many times it appears, and `about` is the comment above a project component.
214
218
  components: z.array(z.object({ name: text(80), from: z.enum(["project", "library", "kit"]), uses: z.number().int().min(1), about: text(400).optional() })).max(300).optional(),
@@ -103,7 +103,9 @@ export function componentsOf(source: string, opts: { sceneIds: string[]; pulled?
103
103
  for (const [name, body] of local) if (body === node) { inside = name; here = new Set(); }
104
104
  const opening = ts.isJsxSelfClosingElement(node) ? node : ts.isJsxElement(node) ? node.openingElement : undefined;
105
105
  if (opening) {
106
- const own = idsIn(opening.attributes);
106
+ // A spread of props ({...fx}) is a bundle, often of every scene's timings, so it names no scene: only the props written out do.
107
+ const own = new Set<string>();
108
+ for (const attr of opening.attributes.properties) if (!ts.isJsxSpreadAttribute(attr)) for (const id of idsIn(attr)) own.add(id);
107
109
  here = own.size ? own : here;
108
110
  const tag = opening.tagName.getText(sf);
109
111
  const root = tag.split(".")[0]!;
@@ -154,10 +156,40 @@ export function componentsOf(source: string, opts: { sceneIds: string[]; pulled?
154
156
  for (const use of list) for (const id of use.scenes.size ? use.scenes : (scenesOfLocal.get(use.owner) ?? [])) if (!mine.has(id)) { mine.add(id); changed = true; }
155
157
  }
156
158
  }
159
+ // A component of the file's own that is named after a scene ("Hate" for the scene "hate", "Good" for "too-good") is that scene's,
160
+ // however it is timed inside.
161
+ // Its name decides alone: whatever else its props mention, "Hate" is the scene "hate".
162
+ const plain = (text: string) => text.toLowerCase().replace(/[^a-z0-9]/g, "");
163
+ const byName = new Set<string>();
164
+ for (const name of scenesOfLocal.keys()) {
165
+ const n = plain(name);
166
+ const hits = [...known].filter((id) => { const i = plain(id); return n.length >= 3 && (i === n || (n.length >= 4 && (i.endsWith(n) || i.startsWith(n))) || (i.length >= 4 && (n.endsWith(i) || n.startsWith(i)))); });
167
+ if (hits.length) { scenesOfLocal.set(name, new Set(hits)); byName.add(name); }
168
+ }
169
+ // With scenes named this way, where a component sits says more than what its props mention (inside "Hate", everything is timed
170
+ // from the same handful of numbers): it belongs to the scenes of the component it is written in, and only a component written
171
+ // straight in the composition is read from its own props. Worked out again from the start on that rule.
172
+ const sceneFor = (use: Use): Iterable<string> => {
173
+ const around = byName.size ? scenesOfLocal.get(use.owner) : undefined;
174
+ return around?.size ? around : use.scenes.size ? use.scenes : (scenesOfLocal.get(use.owner) ?? []);
175
+ };
176
+ if (byName.size) {
177
+ for (const name of scenesOfLocal.keys()) if (!byName.has(name)) scenesOfLocal.set(name, new Set());
178
+ for (let changed = true, rounds = 0; changed && rounds < 12; rounds++) {
179
+ changed = false;
180
+ for (const [name, list] of localUses) {
181
+ if (byName.has(name)) continue;
182
+ const mine = scenesOfLocal.get(name)!;
183
+ for (const use of list) for (const id of sceneFor(use)) if (!mine.has(id)) { mine.add(id); changed = true; }
184
+ }
185
+ }
186
+ }
157
187
  const used = new Map<string, { uses: number; scenes: Set<string> }>();
188
+ // The file's own components are listed too: they are what most compositions are made of.
189
+ for (const name of local.keys()) { const list = localUses.get(name) ?? []; if (list.length) { used.set(name, { uses: list.length, scenes: scenesOfLocal.get(name)! }); origin.set(name, "project"); } }
158
190
  for (const [tag, list] of uses) {
159
191
  const scenes = new Set<string>();
160
- for (const use of list) for (const id of use.scenes.size ? use.scenes : (scenesOfLocal.get(use.owner) ?? [])) scenes.add(id);
192
+ for (const use of list) for (const id of sceneFor(use)) scenes.add(id);
161
193
  used.set(tag, { uses: list.length, scenes });
162
194
  }
163
195