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.
Files changed (41) hide show
  1. package/README.md +11 -0
  2. package/cli/board.test.ts +6 -2
  3. package/cli/install.ts +6 -3
  4. package/cli/state.test.ts +18 -1
  5. package/cli/state.ts +24 -1
  6. package/cli/web.ts +5 -1
  7. package/package.json +1 -1
  8. package/skills/story-deck/SKILL.md +63 -0
  9. package/skills/story-deck/examples/build-example.js +29 -0
  10. package/skills/story-deck/references/rules.md +62 -0
  11. package/skills/story-deck/references/story-arc.md +35 -0
  12. package/skills/story-deck/scripts/check.sh +12 -0
  13. package/skills/story-deck/scripts/install_fonts.py +42 -0
  14. package/skills/story-deck/scripts/kit.js +174 -0
  15. package/skills/story-deck/scripts/prep_images.py +70 -0
  16. package/skills/story-deck/scripts/qa.sh +48 -0
  17. package/skills/story-video/SKILL.md +108 -0
  18. package/skills/story-video/engine/package.json +12 -0
  19. package/skills/story-video/engine/src/Root.tsx +24 -0
  20. package/skills/story-video/engine/src/engine/Film.tsx +405 -0
  21. package/skills/story-video/engine/src/engine/presets.json +58 -0
  22. package/skills/story-video/engine/src/index.ts +3 -0
  23. package/skills/story-video/engine/tsconfig.json +1 -0
  24. package/skills/story-video/examples/sample-film/README.md +12 -0
  25. package/skills/story-video/examples/sample-film/film.json +16 -0
  26. package/skills/story-video/examples/sample-film/lines.json +7 -0
  27. package/skills/story-video/references/licensing.md +14 -0
  28. package/skills/story-video/references/presets.md +21 -0
  29. package/skills/story-video/references/production.md +29 -0
  30. package/skills/story-video/references/revisions.md +25 -0
  31. package/skills/story-video/references/script.md +30 -0
  32. package/skills/story-video/scripts/check.sh +19 -0
  33. package/skills/story-video/scripts/intake.py +73 -0
  34. package/skills/story-video/scripts/master.sh +26 -0
  35. package/skills/story-video/scripts/music.sh +10 -0
  36. package/skills/story-video/scripts/setup.sh +25 -0
  37. package/skills/story-video/scripts/sfx.py +64 -0
  38. package/skills/story-video/scripts/srt.py +30 -0
  39. package/skills/story-video/scripts/still.sh +10 -0
  40. package/skills/story-video/scripts/vo.py +99 -0
  41. 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
+ );