kaizen-agent 1.19.4 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/cli/board.test.ts +6 -2
- package/cli/install.ts +6 -3
- package/cli/state.test.ts +18 -1
- package/cli/state.ts +24 -1
- package/cli/web.ts +5 -1
- package/package.json +1 -1
- package/skills/story-deck/SKILL.md +63 -0
- package/skills/story-deck/examples/build-example.js +29 -0
- package/skills/story-deck/references/rules.md +62 -0
- package/skills/story-deck/references/story-arc.md +35 -0
- package/skills/story-deck/scripts/check.sh +12 -0
- package/skills/story-deck/scripts/install_fonts.py +42 -0
- package/skills/story-deck/scripts/kit.js +174 -0
- package/skills/story-deck/scripts/prep_images.py +70 -0
- package/skills/story-deck/scripts/qa.sh +48 -0
- package/skills/story-video/SKILL.md +108 -0
- package/skills/story-video/engine/package.json +12 -0
- package/skills/story-video/engine/src/Root.tsx +24 -0
- package/skills/story-video/engine/src/engine/Film.tsx +405 -0
- package/skills/story-video/engine/src/engine/presets.json +58 -0
- package/skills/story-video/engine/src/index.ts +3 -0
- package/skills/story-video/engine/tsconfig.json +1 -0
- package/skills/story-video/examples/sample-film/README.md +12 -0
- package/skills/story-video/examples/sample-film/film.json +16 -0
- package/skills/story-video/examples/sample-film/lines.json +7 -0
- package/skills/story-video/references/licensing.md +14 -0
- package/skills/story-video/references/presets.md +21 -0
- package/skills/story-video/references/production.md +29 -0
- package/skills/story-video/references/revisions.md +25 -0
- package/skills/story-video/references/script.md +30 -0
- package/skills/story-video/scripts/check.sh +19 -0
- package/skills/story-video/scripts/intake.py +73 -0
- package/skills/story-video/scripts/master.sh +26 -0
- package/skills/story-video/scripts/music.sh +10 -0
- package/skills/story-video/scripts/setup.sh +25 -0
- package/skills/story-video/scripts/sfx.py +64 -0
- package/skills/story-video/scripts/srt.py +30 -0
- package/skills/story-video/scripts/still.sh +10 -0
- package/skills/story-video/scripts/vo.py +99 -0
- package/web/board.html +2 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Crop photos to the slide and bake in the dark cinematic grade.
|
|
2
|
+
|
|
3
|
+
Usage: python3 prep_images.py plan.json (run from the deck folder; reads img/, writes bg/)
|
|
4
|
+
|
|
5
|
+
plan.json:
|
|
6
|
+
{"backgrounds": {"s01": ["<photo id>", focusY, base, "left|right|bottom", strength]},
|
|
7
|
+
"panels": {"p1": ["<photo id>", focusY, [w_px, h_px]]}}
|
|
8
|
+
photo id = file name in img/ without .jpg. focusY 0..1 = vertical crop focus.
|
|
9
|
+
base = overall darkening 0..1. strength = gradient darkness on the text side 0..1.
|
|
10
|
+
Panel size must have the SAME ratio as its box on the slide (w_in/h_in); pptxgenjs
|
|
11
|
+
"sizing" is ignored by Google Slides and stretches the photo.
|
|
12
|
+
|
|
13
|
+
pptxgenjs has no gradient fills, so the text-side shadow lives in the image.
|
|
14
|
+
"""
|
|
15
|
+
import json
|
|
16
|
+
import sys
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from PIL import Image, ImageEnhance
|
|
20
|
+
|
|
21
|
+
W, H = 1800, 1273 # A5 landscape (210:148)
|
|
22
|
+
INK = (20, 17, 15)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def crop(im, w, h, focus_y):
|
|
26
|
+
r = w / h
|
|
27
|
+
if im.width / im.height > r: # too wide: crop sides
|
|
28
|
+
nw = int(im.height * r)
|
|
29
|
+
x = (im.width - nw) // 2
|
|
30
|
+
im = im.crop((x, 0, x + nw, im.height))
|
|
31
|
+
else: # too tall: crop around focus
|
|
32
|
+
nh = int(im.width / r)
|
|
33
|
+
y = int((im.height - nh) * focus_y)
|
|
34
|
+
im = im.crop((0, y, im.width, y + nh))
|
|
35
|
+
out = im.resize((w, h), Image.LANCZOS)
|
|
36
|
+
assert abs(out.width / out.height - r) < 0.01
|
|
37
|
+
return out
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def grade(im):
|
|
41
|
+
im = ImageEnhance.Color(im).enhance(0.75) # mute colours, film look
|
|
42
|
+
return Image.blend(im, Image.new("RGB", im.size, (60, 40, 20)), 0.08)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def overlay(im, base, side, strength):
|
|
46
|
+
ink = Image.new("RGB", im.size, INK)
|
|
47
|
+
im = Image.blend(im, ink, base)
|
|
48
|
+
g = Image.linear_gradient("L") # black top -> white bottom
|
|
49
|
+
if side == "left":
|
|
50
|
+
g = g.rotate(-90)
|
|
51
|
+
elif side == "right":
|
|
52
|
+
g = g.rotate(90)
|
|
53
|
+
g = g.resize(im.size).point(lambda v: int(v * strength))
|
|
54
|
+
return Image.composite(ink, im, g)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(plan_path):
|
|
58
|
+
plan = json.loads(Path(plan_path).read_text())
|
|
59
|
+
Path("bg").mkdir(exist_ok=True)
|
|
60
|
+
for sid, (pid, fy, base, side, s) in plan.get("backgrounds", {}).items():
|
|
61
|
+
im = Image.open(f"img/{pid}.jpg").convert("RGB")
|
|
62
|
+
overlay(grade(crop(im, W, H, fy)), base, side, s).save(f"bg/{sid}.jpg", quality=85)
|
|
63
|
+
for sid, (pid, fy, (pw, ph)) in plan.get("panels", {}).items():
|
|
64
|
+
im = grade(crop(Image.open(f"img/{pid}.jpg").convert("RGB"), pw, ph, fy))
|
|
65
|
+
Image.blend(im, Image.new("RGB", im.size, INK), 0.35).save(f"bg/{sid}.jpg", quality=85)
|
|
66
|
+
print("ok")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main(sys.argv[1] if len(sys.argv) > 1 else "plan.json")
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Build a deck, check the file, render every slide, and write 2x4 contact sheets to qa/.
|
|
3
|
+
# Usage (from the deck folder): qa.sh build-x.js -> prints the qa/sheet*.jpg paths to look at
|
|
4
|
+
# Uses the Anthropic pptx skill's validator when installed; otherwise a built-in structural check.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
BUILD=${1:?usage: qa.sh build-script.js}
|
|
7
|
+
"$(dirname "$0")/check.sh" >/dev/null 2>&1 || { "$(dirname "$0")/check.sh" 2>/dev/null || true; }
|
|
8
|
+
V=$HOME/.kaizen/decks/.venv
|
|
9
|
+
[ -x "$V/bin/python" ] || { python3 -m venv "$V" && "$V/bin/pip" -q install defusedxml lxml pillow fonttools "qrcode[pil]"; }
|
|
10
|
+
PY=$V/bin/python
|
|
11
|
+
F=$(node "$BUILD")
|
|
12
|
+
PPTX_SKILL=$(ls -d ~/.claude/skills/*/pptx/scripts ~/.claude/skills/synced/*/pptx/scripts 2>/dev/null | head -1 || true)
|
|
13
|
+
if [ -n "$PPTX_SKILL" ] && [ -f "$PPTX_SKILL/office/validate.py" ]; then
|
|
14
|
+
$PY "$PPTX_SKILL/office/validate.py" "$F"
|
|
15
|
+
$PY "$PPTX_SKILL/office/soffice.py" --headless --convert-to pdf "$F" >/dev/null
|
|
16
|
+
else
|
|
17
|
+
# structural check: valid zip, every XML part parses, every slide relationship target exists
|
|
18
|
+
$PY - "$F" <<'PYEOF'
|
|
19
|
+
import sys, zipfile, posixpath
|
|
20
|
+
from lxml import etree
|
|
21
|
+
z = zipfile.ZipFile(sys.argv[1]); bad = z.testzip()
|
|
22
|
+
assert bad is None, f"corrupt part: {bad}"
|
|
23
|
+
names = {n for n in z.namelist() if not n.endswith("/")} # skip directory entries
|
|
24
|
+
for n in names:
|
|
25
|
+
if n.endswith(".xml") or n.endswith(".rels"):
|
|
26
|
+
etree.fromstring(z.read(n))
|
|
27
|
+
for n in names:
|
|
28
|
+
if n.startswith("ppt/slides/_rels/"):
|
|
29
|
+
for r in etree.fromstring(z.read(n)):
|
|
30
|
+
t = r.get("Target")
|
|
31
|
+
if r.get("TargetMode") == "External" or not t: continue
|
|
32
|
+
p = posixpath.normpath(posixpath.join("ppt/slides", t))
|
|
33
|
+
assert p in names, f"{n}: missing {p}"
|
|
34
|
+
print("structure OK:", sum(1 for n in names if n.startswith("ppt/slides/slide")), "slides")
|
|
35
|
+
PYEOF
|
|
36
|
+
soffice --headless --convert-to pdf --outdir "$(dirname "$F")" "$F" >/dev/null 2>&1
|
|
37
|
+
fi
|
|
38
|
+
rm -rf qa && mkdir qa
|
|
39
|
+
pdftoppm -jpeg -r 70 "${F%.pptx}.pdf" qa/s
|
|
40
|
+
rm -f "${F%.pptx}.pdf"
|
|
41
|
+
$PY - <<'PYEOF'
|
|
42
|
+
from PIL import Image; import glob, os
|
|
43
|
+
ims=[Image.open(f) for f in sorted(glob.glob('qa/s-*.jpg'))]; w,h=ims[0].size
|
|
44
|
+
for p in range(0,len(ims),8):
|
|
45
|
+
g=ims[p:p+8]; sh=Image.new('RGB',(w*2,h*4),'white')
|
|
46
|
+
for i,im in enumerate(g): sh.paste(im,((i%2)*w,(i//2)*h))
|
|
47
|
+
sh.save(f'qa/sheet{p//8}.jpg'); print(f'{os.getcwd()}/qa/sheet{p//8}.jpg')
|
|
48
|
+
PYEOF
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: story-video
|
|
3
|
+
description: Turn whatever a user brings for any product — a prompt, a story, product photos, app screenshots, a deck (.pptx/.pdf), footage — into a publish-ready video (MP4 + SRT + thumbnail + credits) in a chosen style (cinematic-drama, promo-booth, fun-energetic, viral-social 9:16) with the product's own brand, narration in the user's language with emotional direction, word-synced captions, sound design, music, logo reveal and loudness mastering; then revise it on request. Use for promo / launch / opener / social / viral / explainer videos or "turn this deck into a video". Not for hand-editing an existing video timeline.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Story video
|
|
7
|
+
|
|
8
|
+
Product-agnostic. A film is data: `~/.kaizen/videos/<slug>/film.json` (storyboard + brand) + `lines.json`
|
|
9
|
+
(narration with emotion tags) + `words.json` (word timings, generated) + a preset.
|
|
10
|
+
Worked example: [`examples/sample-film/`](examples/sample-film/) (fictional product, no images needed).
|
|
11
|
+
Run `scripts/check.sh` first on a new machine.
|
|
12
|
+
|
|
13
|
+
Read before producing:
|
|
14
|
+
- [`references/production.md`](references/production.md) — what makes a video publish-grade; QA list.
|
|
15
|
+
- [`references/presets.md`](references/presets.md) — styles, when to use each, adding one.
|
|
16
|
+
- [`references/script.md`](references/script.md) — **how to write the narration** (a told story, not slide text).
|
|
17
|
+
- [`references/revisions.md`](references/revisions.md) — how user revisions are applied and versioned.
|
|
18
|
+
- [`references/licensing.md`](references/licensing.md) — **before anything is published**.
|
|
19
|
+
|
|
20
|
+
## 0. Voice — ask first
|
|
21
|
+
Ask: "Do you have an ElevenLabs account? Which plan?" Then:
|
|
22
|
+
- **Paid ElevenLabs** (Starter+): best and commercially licensed. Use a **native speaker of the
|
|
23
|
+
narration language** from the voice library (e.g. Indonesian: search `shared-voices?language=id`,
|
|
24
|
+
add it, let the user pick from 3 samples). Non-native voices sound synthetic — this is the
|
|
25
|
+
biggest single factor in how natural the video sounds.
|
|
26
|
+
- **Free ElevenLabs**: premade voices only (non-native for most languages) and **no commercial use**;
|
|
27
|
+
fine for drafts and internal review. Offer samples (v3 with `stability: 0` sounds most natural).
|
|
28
|
+
- **No ElevenLabs**: free Microsoft neural voices via edge-tts (`pip install edge-tts`), set
|
|
29
|
+
`"voiceProvider": "edge"` (automatic when no key). No emotion control; usable for drafts.
|
|
30
|
+
- Or the user records the narration: write the script with timings, cut the film to their audio.
|
|
31
|
+
Set `voiceProvider` / `voice` / `edgeVoice` in film.json accordingly.
|
|
32
|
+
|
|
33
|
+
## Where things live
|
|
34
|
+
- **Film workspace:** `~/.kaizen/videos/<slug>/` (one home for every video, whatever the product; override `STORY_VIDEO_HOME`) — `inputs/` (the user's material), generated
|
|
35
|
+
assets (photos, voice `vNN.mp3`, `music.mp3`), `lines.json`, `film.json`, `words.json`,
|
|
36
|
+
`REVISIONS.md`, `out/` (deliverables) and `out/previous/` (earlier versions). Nothing is written to the user's project folders.
|
|
37
|
+
- **Engine:** `~/.kaizen/story-video/engine/` (Remotion + node_modules + sfx), installed once per machine
|
|
38
|
+
and shared by every project. `scripts/setup.sh <slug>` (run from the project root) installs or updates
|
|
39
|
+
it and creates the film workspace (from any directory). Override with `STORY_VIDEO_ENGINE`.
|
|
40
|
+
- Scripts run from the skill: `S=<this skill>/scripts`, every script takes the film dir.
|
|
41
|
+
|
|
42
|
+
## 1. Intake — whatever the user brings
|
|
43
|
+
`$S/setup.sh <slug>`, put the user's material in `~/.kaizen/videos/<slug>/inputs/` sorted as
|
|
44
|
+
`photos/`, `screens/`, `footage/`, `logo*.png`, a deck (`*.pptx` / `*.pdf`) and/or `brief.md` (their
|
|
45
|
+
prompt or story, verbatim), then `python3 $S/intake.py ~/.kaizen/videos/<slug>`. It crops photos for
|
|
46
|
+
16:9 and 9:16, keeps screenshots untouched (records ratios), transcodes footage, renders deck slides +
|
|
47
|
+
text to `intake/`, and suggests a brand palette from the logo. Read the deck text, the brief and the
|
|
48
|
+
slide images before writing anything.
|
|
49
|
+
|
|
50
|
+
Missing material is fine: no photos → source candid ones (story-deck rules); no screenshots →
|
|
51
|
+
ask for them (product beats need the real product); no story → propose one.
|
|
52
|
+
|
|
53
|
+
## 2. Brief — ask with options (one round)
|
|
54
|
+
Purpose and where it plays · style preset · length · formats · language · voice (from step 0;
|
|
55
|
+
ask gender/tone) · music mood · brand (logo, colours, fonts — propose from the
|
|
56
|
+
logo palette) · CTA (URL / QR target / contact / event details — never invent them) · claims the
|
|
57
|
+
user can back up.
|
|
58
|
+
|
|
59
|
+
## 3. Treatment — approve before generating
|
|
60
|
+
A beat table: time · picture · narration line + emotion tag · on-screen text · SFX/ambience.
|
|
61
|
+
Write the narration as one told story (references/script.md) — the user's text is source, not script.
|
|
62
|
+
Show the full script as continuous prose too, so the user can judge how it flows. Only claims the material supports. The user approves or edits it; that is revision 0.
|
|
63
|
+
|
|
64
|
+
## 4. Produce (F = ~/.kaizen/videos/<slug>)
|
|
65
|
+
1. `F/film.json`: `preset`, `language` (`id`, `en`, …), `voiceProvider`, `voice`, `stability`,
|
|
66
|
+
`brand` (`palette`, `fonts`), `labels` (on-screen words in that language: `source`, `next`),
|
|
67
|
+
`logo` (light, for dark backgrounds), `logoOnLight`, `qr`, `cta`, `beats` — table below.
|
|
68
|
+
File names in beats are relative to `F`.
|
|
69
|
+
2. `F/lines.json` (approved script) → `python3 $S/vo.py F` (or only some lines: `vo.py F "" v04,v07`).
|
|
70
|
+
3. Music: `$S/music.sh F "Track"`; offer 3–5 previews when the mood is not settled.
|
|
71
|
+
4. QR: `python3 -c "import qrcode; qrcode.make('<url>').save('F/qr.png')"`.
|
|
72
|
+
5. Check frames: `$S/still.sh F 16x9 60 400 900`, contact-sheet, look at it (or send it to the user), fix. Delete the check images after.
|
|
73
|
+
6. Master each format: `$S/master.sh F <16x9|9x16> <name> <-14|-16> <thumb-second>` → `F/out/`.
|
|
74
|
+
|
|
75
|
+
## 5. Deliver, then revise
|
|
76
|
+
Report files, lengths, loudness, licence status, placeholders, claims to verify, and ask the user
|
|
77
|
+
to listen once (the agent cannot hear). Revisions follow `references/revisions.md`: change only
|
|
78
|
+
the touched beats/lines, re-render, keep earlier versions in `out/previous/`, log in `REVISIONS.md`.
|
|
79
|
+
|
|
80
|
+
## Beat kinds
|
|
81
|
+
|
|
82
|
+
| kind | use | key fields |
|
|
83
|
+
|---|---|---|
|
|
84
|
+
| `photo` | story beat on a photo | `img, side, kick, title, sub, ambience, sfx` |
|
|
85
|
+
| `list` | items arrive one by one with the line | `img, kick, title, items[]` |
|
|
86
|
+
| `stat` | numbers count up, source on screen | `stat[{value,suffix,label,source}], title` |
|
|
87
|
+
| `black` | silence / pause | `title, sub, hold` |
|
|
88
|
+
| `triptych` | three viewpoints | `panels[{img,title,sub}]` |
|
|
89
|
+
| `question` | the turn ("what if") | `title` |
|
|
90
|
+
| `reveal` | logo wipe-in + tagline | `sub, line` |
|
|
91
|
+
| `shot` | real product screenshot, framed | `img, shotRatio, kick, title, sub` |
|
|
92
|
+
| `footage` | video clip | `video, kick, title` |
|
|
93
|
+
| `cta` | the ask: QR, contacts, URL | `img, hold` + `film.cta` |
|
|
94
|
+
|
|
95
|
+
Any beat: `vo`, `sfx[]` (whoosh, hit, riser, tick, pop, chime, ping, pings, bell, keyboard) and
|
|
96
|
+
`ambience` (amb-morning, amb-classroom, amb-office, amb-night). Add sounds with `scripts/sfx.py` (writes to the shared engine).
|
|
97
|
+
Fonts available: Playfair Display, Inter, Fraunces, Montserrat, Poppins, DM Sans, Lora,
|
|
98
|
+
Plus Jakarta Sans, Space Grotesk (add more in `engine/Film.tsx` FONTS).
|
|
99
|
+
|
|
100
|
+
## Distribution
|
|
101
|
+
Shipped with kaizen (`skills/story-video` in the kaizen repo).
|
|
102
|
+
|
|
103
|
+
## Agent notes (Claude Code, Codex, Gemini CLI, OpenCode, Cursor…)
|
|
104
|
+
Steps name capabilities, not tools. Where your agent lacks one:
|
|
105
|
+
- **web fetch** (finding stock photos, sources): ask the user for photos/links instead.
|
|
106
|
+
- **viewing images** (checking contact sheets / frames): send the image paths to the user and ask them to look.
|
|
107
|
+
- **sub-agents** (reading several repos at once): read them one after another.
|
|
108
|
+
- **structured questions**: ask in plain text with numbered options.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "story-video-films",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": { "studio": "remotion studio src/index.ts" },
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@remotion/cli": "4.0.529",
|
|
7
|
+
"@remotion/google-fonts": "4.0.529",
|
|
8
|
+
"react": "^19.0.0",
|
|
9
|
+
"react-dom": "^19.0.0",
|
|
10
|
+
"remotion": "4.0.529"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Composition, getInputProps } from 'remotion';
|
|
3
|
+
import { FilmView, FPS, timeline, type Film, type Words } from './engine/Film';
|
|
4
|
+
|
|
5
|
+
// One film per render: scripts/master.sh passes { film, words } as props and the film folder as public dir.
|
|
6
|
+
const SIZE = { '16x9': [1920, 1080], '9x16': [1080, 1920] } as const;
|
|
7
|
+
type Props = { film: Film; words: Words };
|
|
8
|
+
const props = getInputProps() as Partial<Props>;
|
|
9
|
+
|
|
10
|
+
export const Root: React.FC = () => (
|
|
11
|
+
<>
|
|
12
|
+
{(Object.keys(SIZE) as (keyof typeof SIZE)[]).map((fmt) => (
|
|
13
|
+
<Composition
|
|
14
|
+
key={fmt} id={`Film-${fmt}`} component={FilmView as unknown as React.FC<Record<string, unknown>>}
|
|
15
|
+
fps={FPS} width={SIZE[fmt][0]} height={SIZE[fmt][1]} durationInFrames={1}
|
|
16
|
+
defaultProps={props as Record<string, unknown>}
|
|
17
|
+
calculateMetadata={({ props: pr }) => {
|
|
18
|
+
const { film, words } = pr as unknown as Props;
|
|
19
|
+
return { durationInFrames: film && words ? timeline(film, words).total : 1 };
|
|
20
|
+
}}
|
|
21
|
+
/>
|
|
22
|
+
))}
|
|
23
|
+
</>
|
|
24
|
+
);
|