reelkit-cli 0.10.0 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/package.json +1 -1
- package/skill/.claude-plugin/plugin.json +7 -0
- package/skill/SKILL.md +16 -1
- package/skill/THIRD_PARTY.md +34 -2
- package/skill/hooks/hooks.json +1 -0
- package/skill/hooks/player.ts +278 -0
- package/skill/hooks/project.ts +227 -0
- package/skill/hooks/register.tsx +533 -0
- package/skill/hooks/timeline.ts +218 -0
- package/skill/reference/art-styles.md +6 -6
- package/skill/reference/component-authoring.md +2 -0
- package/skill/reference/higgsfield-production.md +70 -0
- package/skill/reference/motion-design.md +8 -6
- package/skill/reference/studio-pane.md +31 -0
- package/skill/reference/style-components.md +40 -0
- package/skill/reference/style-recipes.md +86 -0
- package/skill/reference/styles.md +8 -4
- package/skill/tests/studio.test.ts +111 -0
- package/skill/types/index.d.ts +65 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: styles
|
|
3
|
-
description:
|
|
3
|
+
description: Choose a visual language from the material or the user's direction, with restrained and showreel approaches and a catalog of look-specific techniques.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Choosing the look
|
|
@@ -9,8 +9,10 @@ Choose a visual language that fits the material and record it in the first scene
|
|
|
9
9
|
|
|
10
10
|
Use the user's chosen direction. Otherwise recommend a fitting look with a short reason; use the catalogue as technique references, not a mandatory menu. Offer alternatives when requested. Ask only about consequential missing information.
|
|
11
11
|
|
|
12
|
+
After choosing the look, use `reference/style-recipes.md` to connect its visible action, implementation, sound and review evidence. `reference/style-components.md` maps all reviewed shared components to their roles. The catalog's timing, palette and exclusions are defaults for the described treatment; real speech timing, supplied brand identity, the user's requested variation and the installed runtime take precedence.
|
|
13
|
+
|
|
12
14
|
## Two modes
|
|
13
|
-
|
|
15
|
+
Restrained and showreel describe motion energy, not separate engines. Choose the energy by the shot's purpose; a mixed-media film can combine them while preserving its visual thread. The chosen treatment explains which craft defaults in `reference/motion-design.md` change.
|
|
14
16
|
|
|
15
17
|
**Restrained** (product demos, explainers, anything that must feel expensive and calm)
|
|
16
18
|
- One background, one hero colour or black and white, one face.
|
|
@@ -33,7 +35,7 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
33
35
|
**When:** results, growth, a comparison, anything with figures from the narration. **How:** one figure or one chart per scene (search "statistic counter", "line chart", "bar chart", "pie chart", "radial gauge"); the number counts up and finishes before the scene's midpoint; a before and after sits side by side (search "comparison"). The payoff number gets the biggest move of the video. **Forbidden:** any figure that is not in the plan, two charts in one frame, decorative data.
|
|
34
36
|
|
|
35
37
|
### 3. Headline hits (showreel)
|
|
36
|
-
**When:** an opener, a launch, a message of a few words that must stop the scroll. **How:** one or two words fill the frame (search "kinetic title"), hard cuts to a
|
|
38
|
+
**When:** an opener, a launch, a message of a few words that must stop the scroll. **How:** one or two words fill the frame (search "kinetic title"), selected hard cuts to a full colour on a musical or spoken event, and the whole message together at the end, held. Vary the entrance when it helps hierarchy; keep one motion language. A camera push is optional. **Avoid for this treatment:** gradients or outlines that soften the intended heavy graphic read, and motion that prevents reading the landing.
|
|
37
39
|
|
|
38
40
|
### 4. Cinematic story (restrained)
|
|
39
41
|
**When:** a mood, a place, a story told by a voice. **How:** generated or reused pictures with a slow move across them, a film grade and grain over everything, a poster-style title to open (search "cinematic title"), a film overlay if it suits (`--kind overlay`, search "film"). Few words on screen; the captions carry the text. **Forbidden:** interface parts, bright accent colours, fast cuts.
|
|
@@ -51,7 +53,7 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
51
53
|
**When:** a testimonial, an introduction, a creator talking, social proof. **How:** the person's name and role in a lower third (search "name lower third"), a quote card with its stars (search "quote testimonial"), a follow line to close (search "social handle", "cta button"). Quotes and names come from the user, word for word. **Forbidden:** an invented quote, rating or follower count.
|
|
52
54
|
|
|
53
55
|
### 9. Over footage (either mode)
|
|
54
|
-
**When:** the user supplied their own video. **How:** the footage is the picture; graphics
|
|
56
|
+
**When:** the user supplied their own video. **How:** the footage is the picture; graphics support it. Keep the active graphic clear of the speaker's face; use a punch-in only for needed emphasis or framing, with lower thirds and callouts from the library. Honor the user's caption choice and use actual source-speech timings (`reference/studio-editing.md`). **Avoid:** covering the face, a graphic left after its purpose ends, zooms stacked on footage that already has its own.
|
|
55
57
|
|
|
56
58
|
### 10. Logo sting (either mode)
|
|
57
59
|
**When:** an opener or end card, from the user's own logo file. **How:** build toward the logo from its own shapes and colours and land on it exactly as supplied, held at least 1.5 seconds. **Forbidden:** redrawing, recolouring or stretching the logo; anyone else's logo or trademark.
|
|
@@ -71,6 +73,8 @@ Each look is built from what the kit and the shared library already hold, so mos
|
|
|
71
73
|
## Your own
|
|
72
74
|
Use the supplied message, actions, colours, duration and format. Ask only for consequential missing details or a reference that the choice depends on. Choose the nearest look above as the technical base and write a four-line brief in the same shape (when, how, forbidden, structure). Show the brief with the plan at the checkpoint.
|
|
73
75
|
|
|
76
|
+
For morphing UI, particle words, cube worlds, text rings, children's-book cutaways, blueprint-to-product shots, exploded assembly, toon materials, layered posters, mixed-media narration, cutout depth and composited VFX, use the advanced rows 29 to 40 in `reference/style-recipes.md`. Select a local kit or external production route that can actually deliver the shot.
|
|
77
|
+
|
|
74
78
|
## What every look shares
|
|
75
79
|
- The plan checkpoint lists the exact words that will appear on screen and when. Fixing a list takes a minute; fixing a finished animation takes an hour.
|
|
76
80
|
- Describe the look to the user in plain words about what they will see ("the title sits on a soft dark patch"), not in studio terms.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { expect, test } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
const DIR = '/work/demo'
|
|
4
|
+
const PANE = { title: 'Reelkit', isFocused: false, bodyColumns: 100, placement: 'dock', scroll: { offset: 0, bodyRows: 40 }, view: {} } as const
|
|
5
|
+
|
|
6
|
+
const FILES: Record<string, string> = {
|
|
7
|
+
[`${DIR}/reelkit.json`]: JSON.stringify({ name: 'demo', aspect: '9:16' }),
|
|
8
|
+
[`${DIR}/plan.json`]: JSON.stringify({
|
|
9
|
+
title: 'Demo film',
|
|
10
|
+
aspect: '9:16',
|
|
11
|
+
voiceId: 'v-1',
|
|
12
|
+
scenes: [
|
|
13
|
+
{ id: 'hook', narration: 'First the hook lands.', treatment: 'motion-graphic', onScreenText: ['HOOK'] },
|
|
14
|
+
{ id: 'proof', narration: 'Then the proof follows.', treatment: 'illustration', onScreenText: ['PROOF'] },
|
|
15
|
+
],
|
|
16
|
+
}),
|
|
17
|
+
[`${DIR}/manifest.json`]: JSON.stringify({
|
|
18
|
+
fps: 30,
|
|
19
|
+
totalFrames: 240,
|
|
20
|
+
music: { key: 'assets/lib/music-a/track.mp3', bpm: 120, beatFrames: [0, 15, 30, 45] },
|
|
21
|
+
scenes: [
|
|
22
|
+
{ id: 'hook', startFrame: 0, durationFrames: 90, words: [{ word: 'First', startSec: 0.1, endSec: 0.4 }] },
|
|
23
|
+
{ id: 'proof', startFrame: 90, durationFrames: 150, words: [{ word: 'Then', startSec: 0.2, endSec: 0.5 }] },
|
|
24
|
+
],
|
|
25
|
+
}),
|
|
26
|
+
[`${DIR}/assets/music.json`]: JSON.stringify({ key: 'assets/lib/music-a/track.mp3', title: 'Track A', durationSec: 20 }),
|
|
27
|
+
[`${DIR}/src/Badge.tsx`]: '// A round badge with one word in it. Use it to mark a claim.\nexport const Badge = () => null\n',
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const entry = (name: string) => ({ name, kind: 'file' as const, size: 10, mtimeMs: 1000, isLink: false })
|
|
31
|
+
|
|
32
|
+
// The engine beneath the mod: one project on disk, a render, and commands that answer from memory.
|
|
33
|
+
type TestOn = Parameters<Extract<Parameters<typeof test>[1], (...args: never[]) => unknown>>[1]
|
|
34
|
+
|
|
35
|
+
const project = (on: TestOn, written: Record<string, string>, ran: string[][]) => {
|
|
36
|
+
on('session.cwd', () => ({ value: DIR }))
|
|
37
|
+
on('clock.now', () => ({ value: 3600000 }))
|
|
38
|
+
on('ui.open', () => ({ value: { isPlaced: true as const } }))
|
|
39
|
+
on('fs.exists', (_$, e) => ({ value: e.path in FILES }))
|
|
40
|
+
on('fs.read', (_$, e) => {
|
|
41
|
+
const text = FILES[e.path]
|
|
42
|
+
if (text === undefined) throw new Error(`ENOENT ${e.path}`)
|
|
43
|
+
return { value: text }
|
|
44
|
+
})
|
|
45
|
+
on('fs.list', (_$, e) => ({ value: e.path === `${DIR}/src` ? [entry('Badge.tsx'), entry('Video.tsx')] : [] }))
|
|
46
|
+
on('fs.stat', (_$, e) => {
|
|
47
|
+
if (e.path !== `${DIR}/out/video.mp4`) throw new Error(`ENOENT ${e.path}`)
|
|
48
|
+
return { value: { kind: 'file' as const, size: 2097152, mtimeMs: 5000, isLink: false } }
|
|
49
|
+
})
|
|
50
|
+
on('fs.write', (_$, e) => {
|
|
51
|
+
written[e.path] = e.text
|
|
52
|
+
return { value: undefined }
|
|
53
|
+
})
|
|
54
|
+
on('process.run', (_$, e) => {
|
|
55
|
+
ran.push([...e.argv])
|
|
56
|
+
const stdout =
|
|
57
|
+
e.argv[0] === 'reelkit'
|
|
58
|
+
? JSON.stringify({ ok: true, data: { items: [{ id: 'music-b', kind: 'music', title: 'Track B', description: 'Bright.', match: 0.8, meta: { bpm: 100 } }] } })
|
|
59
|
+
: ''
|
|
60
|
+
return { value: { exitCode: 0, stdout, stderr: '', isStdoutTruncated: false, isStderrTruncated: false } }
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
test('the pane shows the film as a timeline on every surface', async ($, on) => {
|
|
65
|
+
project(on, {}, [])
|
|
66
|
+
for (const surface of ['terminal', 'desktop', 'vscode', 'mobile'] as const) {
|
|
67
|
+
const ui = await $.ui.mount({ plugin: 'reelkit', surface, component: 'Pane', requestId: 'reelkit-studio', props: PANE })
|
|
68
|
+
await ui.press({ key: 'refresh' })
|
|
69
|
+
expect(await ui.find({ type: 'Text', text: /Demo film/ })).toBeDefined()
|
|
70
|
+
expect(await ui.find({ type: 'Text', text: /1\. hook/ })).toBeDefined()
|
|
71
|
+
if (surface === 'terminal') expect(await ui.find({ type: 'Text', text: /1 hook/ })).toBeDefined()
|
|
72
|
+
else expect(await ui.find({ type: 'Svg' })).toBeDefined()
|
|
73
|
+
await ui.press({ key: 'next' })
|
|
74
|
+
expect(await ui.find({ type: 'Text', text: /2\. proof/ })).toBeDefined()
|
|
75
|
+
await ui.press({ key: 'prev' })
|
|
76
|
+
await ui.unmount()
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('the tabs show the plan, the music and the components', async ($, on) => {
|
|
81
|
+
project(on, {}, [])
|
|
82
|
+
for (const surface of ['terminal', 'desktop'] as const) {
|
|
83
|
+
const ui = await $.ui.mount({ plugin: 'reelkit', surface, component: 'Pane', requestId: 'reelkit-studio', props: PANE })
|
|
84
|
+
await ui.press({ key: 'refresh' })
|
|
85
|
+
await ui.press({ key: 'tab-plan' })
|
|
86
|
+
expect((await ui.find({ type: 'Markdown' }))?.props.text).toContain('Then the proof follows.')
|
|
87
|
+
await ui.press({ key: 'tab-music' })
|
|
88
|
+
expect(await ui.find({ type: 'Text', text: /Track A/ })).toBeDefined()
|
|
89
|
+
await ui.press({ key: 'tab-components' })
|
|
90
|
+
expect(await ui.find({ type: 'Text', text: /round badge/ })).toBeDefined()
|
|
91
|
+
await ui.press({ key: 'tab-timeline' })
|
|
92
|
+
await ui.unmount()
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('the browser tab searches the library and the player page is written beside the render', async ($, on) => {
|
|
97
|
+
const written: Record<string, string> = {}
|
|
98
|
+
const ran: string[][] = []
|
|
99
|
+
project(on, written, ran)
|
|
100
|
+
const ui = await $.ui.mount({ plugin: 'reelkit', surface: 'desktop', component: 'Pane', requestId: 'reelkit-studio', props: PANE })
|
|
101
|
+
await ui.press({ key: 'refresh' })
|
|
102
|
+
await ui.press({ key: 'player' })
|
|
103
|
+
expect(written[`${DIR}/out/studio.html`]).toContain('video.mp4')
|
|
104
|
+
expect(ran.some(argv => argv[0] === 'open' && argv[1] === `${DIR}/out/studio.html`)).toBe(true)
|
|
105
|
+
await ui.press({ key: 'tab-browser' })
|
|
106
|
+
await ui.input({ key: 'query', text: 'bright' })
|
|
107
|
+
expect(ran.some(argv => argv.join(' ') === 'reelkit assets search bright --kind music --json')).toBe(true)
|
|
108
|
+
expect(await ui.find({ type: 'Text', text: /80% Track B/ })).toBeDefined()
|
|
109
|
+
await ui.press({ key: 'tab-timeline' })
|
|
110
|
+
await ui.unmount()
|
|
111
|
+
})
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type StudioTab = 'timeline' | 'plan' | 'music' | 'components' | 'browser'
|
|
2
|
+
|
|
3
|
+
export type FilmScene = {
|
|
4
|
+
id: string
|
|
5
|
+
// Frames from the film's start, and the scene's own length in frames.
|
|
6
|
+
start: number
|
|
7
|
+
frames: number
|
|
8
|
+
narration: string
|
|
9
|
+
text: string[]
|
|
10
|
+
treatment: string
|
|
11
|
+
notes: string
|
|
12
|
+
// Seconds from the scene's start at which each spoken word begins.
|
|
13
|
+
words: number[]
|
|
14
|
+
// A small picture of the scene under out/.studio, when a preview frame exists.
|
|
15
|
+
thumb: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type FilmSound = { id: string; kind: string; title: string; path: string; seconds: number }
|
|
19
|
+
|
|
20
|
+
export type FilmComponent = { name: string; path: string; about: string }
|
|
21
|
+
|
|
22
|
+
export type Film = {
|
|
23
|
+
dir: string
|
|
24
|
+
name: string
|
|
25
|
+
title: string
|
|
26
|
+
aspect: string
|
|
27
|
+
fps: number
|
|
28
|
+
totalFrames: number
|
|
29
|
+
// True once manifest.json gave real timings; false while they are estimated from the plan.
|
|
30
|
+
isTimed: boolean
|
|
31
|
+
voice: string
|
|
32
|
+
pace: string
|
|
33
|
+
captions: string
|
|
34
|
+
scenes: FilmScene[]
|
|
35
|
+
music: { title: string; path: string; bpm: number; seconds: number; beats: number[] } | null
|
|
36
|
+
sounds: FilmSound[]
|
|
37
|
+
components: FilmComponent[]
|
|
38
|
+
userAssets: { id: string; kind: string; name: string; about: string }[]
|
|
39
|
+
render: { path: string; bytes: number; at: number } | null
|
|
40
|
+
previews: number
|
|
41
|
+
loadedAt: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type LibraryItem = { id: string; kind: string; title: string; about: string; match: number; detail: string }
|
|
45
|
+
|
|
46
|
+
export type LibrarySearch = {
|
|
47
|
+
kind: string
|
|
48
|
+
query: string
|
|
49
|
+
status: 'idle' | 'busy' | 'done' | 'failed'
|
|
50
|
+
note: string
|
|
51
|
+
items: LibraryItem[]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
declare module 'claude-code' {
|
|
55
|
+
interface PluginState {
|
|
56
|
+
reelkit: {
|
|
57
|
+
tab: StudioTab
|
|
58
|
+
film: Film | null
|
|
59
|
+
projects: string[]
|
|
60
|
+
scene: number
|
|
61
|
+
component: string
|
|
62
|
+
search: LibrarySearch
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|