wand-kit 0.1.0 → 0.3.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/PROMPT.md +91 -7
- package/SKILL.md +68 -14
- package/START-HERE.md +7 -1
- package/examples/gallery/job.mjs +3 -1
- package/examples/storyboard/job.mjs +72 -0
- package/examples/storyboard/script.txt +6 -0
- package/examples/talk/agents-demo.html +69 -0
- package/examples/talk/agents-demo.poster-next.png +0 -0
- package/examples/talk/agents-demo.poster.png +0 -0
- package/examples/talk/job.mjs +85 -0
- package/lib/board.js +152 -0
- package/lib/brand.js +23 -0
- package/lib/components.js +151 -5
- package/lib/engine.js +33 -7
- package/lib/html_export.js +35 -4
- package/lib/runtime_check.js +108 -0
- package/lib/talk.js +620 -0
- package/package.json +2 -2
- package/templates/storyboard.mjs +33 -0
- package/wand.js +65 -18
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* __NAME__ — a storyboard on the Wand studio.
|
|
3
|
+
*
|
|
4
|
+
* `content.txt` is the script: one voiceover line a shot, verbatim, so
|
|
5
|
+
* coverage proves every word is on the board. Each shot names its act, its
|
|
6
|
+
* line (what the shot is for), the direction, and one to four frames; a frame
|
|
7
|
+
* is a `sketch`, the job's own `art` (SVG on brand roles), an `img`, or left
|
|
8
|
+
* to draw. `wpm` sets the scratch timing; `target` is the length to hit, in
|
|
9
|
+
* seconds. examples/storyboard/job.mjs is a finished one.
|
|
10
|
+
*/
|
|
11
|
+
export default {
|
|
12
|
+
name: "__NAME__",
|
|
13
|
+
brand: "__BRAND__", // current | next
|
|
14
|
+
surface: "slide",
|
|
15
|
+
tag: "Wand · Storyboard",
|
|
16
|
+
footer: "Wand AI · Draft for review",
|
|
17
|
+
content: "content.txt",
|
|
18
|
+
edits: {},
|
|
19
|
+
cut: [],
|
|
20
|
+
css: ``,
|
|
21
|
+
pages: (c) => c.storyboard({
|
|
22
|
+
title: "__NAME__",
|
|
23
|
+
lede: "What the film is for, who sees it, and where.",
|
|
24
|
+
wpm: 160,
|
|
25
|
+
target: 30,
|
|
26
|
+
shots: [
|
|
27
|
+
{ act: "Act one", name: "Opening", vo: c.L(1),
|
|
28
|
+
line: "What this shot is for, in one sentence.",
|
|
29
|
+
camera: "", motion: "", onscreen: "", sound: "", next: "Cut.",
|
|
30
|
+
frames: [{ sketch: { people: 4, apps: ["CRM"], label: "What the frame shows" }, caption: "" }] },
|
|
31
|
+
],
|
|
32
|
+
}),
|
|
33
|
+
};
|
package/wand.js
CHANGED
|
@@ -2,25 +2,33 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* wand.js — the studio's front door.
|
|
4
4
|
*
|
|
5
|
-
* node wand.js doctor what works here
|
|
6
|
-
* node wand.js new <dir> [--surface letter] [--brand current] [--name "Title"]
|
|
7
|
-
* a job folder to compose in
|
|
8
|
-
*
|
|
5
|
+
* node wand.js doctor what works here, and the brand rule
|
|
6
|
+
* node wand.js new <dir> [--surface letter] [--brand current|next] [--name "Title"]
|
|
7
|
+
* a job folder to compose in, on the default brand
|
|
8
|
+
* unless --brand names the other
|
|
9
|
+
* node wand.js new <dir> --surface storyboard a storyboard from a script (shots, frames, timing)
|
|
10
|
+
* node wand.js build <job.mjs> [--brand next] gates, then the PDF, a PNG per page, the sheet
|
|
9
11
|
* node wand.js check <job.mjs> gates only
|
|
10
|
-
* node wand.js ship <job.mjs> [--pptx] [--no-html] everything: build, one-file HTML,
|
|
12
|
+
* node wand.js ship <job.mjs> [--pptx] [--no-html] everything: build, one-file HTML (a deck animates,
|
|
13
|
+
* a storyboard plays), PowerPoint; the HTML is driven
|
|
14
|
+
* like a presenter would before it ships
|
|
15
|
+
* node wand.js poster <demo.html> [--play 4] a demo's poster: the frame every static export shows
|
|
11
16
|
* node wand.js watch <job.mjs> rebuild on save, the browser kept warm
|
|
12
17
|
* node wand.js gallery [--brand current|next] every component, light and dark, on one surface each
|
|
13
18
|
* node wand.js components the vocabulary
|
|
14
19
|
* node wand.js brands the brands this kit can draw with
|
|
15
20
|
*
|
|
16
21
|
* A job is a folder with `job.mjs` (the composition) and, for a rebuild,
|
|
17
|
-
* `content.txt` (the words, one per line). Outputs land in `<job>/out
|
|
22
|
+
* `content.txt` (the words, one per line). Outputs land in `<job>/out/`;
|
|
23
|
+
* with `--brand` naming the other brand, in `<job>/out-<brand>/`.
|
|
18
24
|
*/
|
|
19
25
|
import fs from "node:fs";
|
|
20
26
|
import path from "node:path";
|
|
21
27
|
import { fileURLToPath } from "node:url";
|
|
22
28
|
import { createRequire } from "node:module";
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
23
30
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
31
|
+
import { BRANDS, DEFAULT_BRAND, ASKED_AS, brandRule } from "./lib/brand.js";
|
|
24
32
|
|
|
25
33
|
const require = createRequire(import.meta.url);
|
|
26
34
|
const KIT = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -33,7 +41,7 @@ const rel = (p) => path.relative(process.cwd(), p) || ".";
|
|
|
33
41
|
|
|
34
42
|
async function doctor() {
|
|
35
43
|
const { findChrome, has } = await import("./lib/engine.js");
|
|
36
|
-
const {
|
|
44
|
+
const { loadBrand } = await import("./lib/brand.js");
|
|
37
45
|
const rows = [];
|
|
38
46
|
rows.push(["node", process.version, +process.version.slice(1).split(".")[0] >= 18 ? "ok" : "MISSING - Node 18 or newer"]);
|
|
39
47
|
for (const d of ["playwright-core", "pptxgenjs", "jszip", "fontkit"]) { let s = "ok"; try { require.resolve(d); } catch { s = `MISSING - npm install in ${rel(KIT)}`; } rows.push([d, s === "ok" ? "bundled" : "-", s]); }
|
|
@@ -47,14 +55,16 @@ async function doctor() {
|
|
|
47
55
|
console.log("");
|
|
48
56
|
for (const [k, v, s] of rows) console.log(`${s === "ok" ? " " : "! "}${k.padEnd(w)} ${String(v).padEnd(36)}${s === "ok" ? "" : s}`);
|
|
49
57
|
const blocked = rows.some((r) => r[2].startsWith("MISSING") || r[2].startsWith("no renderer"));
|
|
58
|
+
console.log(`\n brand rule ${brandRule()}`);
|
|
50
59
|
console.log(blocked ? "\nBLOCKED - fix the rows marked !\n" : "\nready - node wand.js new <dir>, then build <dir>/job.mjs\n");
|
|
51
60
|
return blocked ? 1 : 0;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
function newJob() {
|
|
55
64
|
const dir = args[1]; if (!dir) die("usage: node wand.js new <dir> [--surface letter|a4|slide|square|og] [--brand current|next] [--name \"Title\"]");
|
|
56
|
-
const surface = flag("--surface", "letter"), brand = flag("--brand",
|
|
57
|
-
|
|
65
|
+
const surface = flag("--surface", "letter"), brand = flag("--brand", DEFAULT_BRAND), name = flag("--name", path.basename(dir));
|
|
66
|
+
if (!BRANDS.includes(brand)) die(`unknown brand "${brand}" - one of ${BRANDS.join(", ")}`);
|
|
67
|
+
const tpl = path.join(KIT, "templates", `${surface === "storyboard" ? "storyboard" : ["slide", "square", "og"].includes(surface) ? (surface === "slide" ? "slide" : "card") : "page"}.mjs`);
|
|
58
68
|
if (!fs.existsSync(tpl)) die(`no template for surface "${surface}"`);
|
|
59
69
|
if (fs.existsSync(path.join(dir, "job.mjs"))) die(`${dir}/job.mjs already exists`);
|
|
60
70
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -64,24 +74,36 @@ function newJob() {
|
|
|
64
74
|
console.log(`${dir}/\n job.mjs the composition — pages(c) and css\n content.txt the words, one source string per line (leave empty when authoring)\n\nnext: node ${rel(path.join(KIT, "wand.js"))} build ${path.join(dir, "job.mjs")}`);
|
|
65
75
|
}
|
|
66
76
|
|
|
77
|
+
/** --brand names the brand to build on instead of the job's own. */
|
|
78
|
+
function brandFlag() {
|
|
79
|
+
const b = flag("--brand"); if (!b) return null;
|
|
80
|
+
if (!BRANDS.includes(b)) die(`unknown brand "${b}" - one of ${BRANDS.join(", ")}`);
|
|
81
|
+
return b;
|
|
82
|
+
}
|
|
83
|
+
|
|
67
84
|
async function build(jobPath, opts) {
|
|
68
|
-
if (!jobPath) die("usage: node wand.js build <job.mjs>");
|
|
85
|
+
if (!jobPath) die("usage: node wand.js build <job.mjs> [--brand current|next]");
|
|
69
86
|
const { build } = await import("./lib/engine.js");
|
|
70
|
-
return build(jobPath, opts);
|
|
87
|
+
return build(jobPath, { ...opts, brand: brandFlag() });
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
async function ship(jobPath) {
|
|
74
91
|
if (!jobPath) die("usage: node wand.js ship <job.mjs> [--pptx] [--no-html]");
|
|
75
92
|
const { build, closeBrowser } = await import("./lib/engine.js");
|
|
76
|
-
const r = await build(jobPath, { quiet: true });
|
|
93
|
+
const r = await build(jobPath, { quiet: true, brand: brandFlag() });
|
|
77
94
|
const { surface } = await import("./lib/surfaces.js");
|
|
78
95
|
const s = surface(r.surface);
|
|
79
96
|
if (!on("--no-html")) {
|
|
80
97
|
const { singleFile } = await import("./lib/html_export.js");
|
|
81
98
|
const out = path.join(path.dirname(r.outputs.pdf), `${r.name}.html`);
|
|
82
|
-
const sf = singleFile(r.outputs.html, out, { kind: s.kind, w: s.w, h: s.h });
|
|
99
|
+
const sf = singleFile(r.outputs.html, out, { kind: s.kind, w: s.w, h: s.h, talk: r.talk, board: r.board, title: r.title });
|
|
83
100
|
r.outputs.single = sf.out;
|
|
84
101
|
if (sf.missing.length) r.rows.push({ ok: false, gate: "html", msg: `missing files: ${sf.missing.join(", ")}` });
|
|
102
|
+
if (sf.bytes > 60e6) r.rows.push({ ok: false, warn: true, gate: "html", msg: `${(sf.bytes / 1e6).toFixed(0)} MB: heavy to send; shorten or compress the clips` });
|
|
103
|
+
// the runtime, used the way a presenter uses it
|
|
104
|
+
const { checkTalk, checkBoard } = await import("./lib/runtime_check.js");
|
|
105
|
+
if (r.board) { const c = await checkBoard(out, r.board); r.rows.push({ ok: c.ok, gate: "player", msg: c.msg }); }
|
|
106
|
+
else if (s.kind === "slide") { const c = await checkTalk(out, { total: r.pages }); r.rows.push({ ok: c.ok, gate: "runtime", msg: c.msg }); }
|
|
85
107
|
}
|
|
86
108
|
if (on("--pptx")) {
|
|
87
109
|
try {
|
|
@@ -119,8 +141,32 @@ async function watch(jobPath) {
|
|
|
119
141
|
process.on("SIGINT", async () => { await closeBrowser(); process.exit(0); });
|
|
120
142
|
}
|
|
121
143
|
|
|
144
|
+
/** A demo's poster: the page at the size it is shown, after it settles, optionally after it plays. */
|
|
145
|
+
async function poster(demoPath) {
|
|
146
|
+
if (!demoPath) die("usage: node wand.js poster <demo.html> [--brand current|next] [--size 1280x720] [--wait 1.5] [--play 0] [--out poster.png]");
|
|
147
|
+
const { browser, closeBrowser } = await import("./lib/engine.js");
|
|
148
|
+
const [w, h] = flag("--size", "1280x720").split("x").map(Number);
|
|
149
|
+
const out = flag("--out", demoPath.replace(/\.html?$/i, "") + ".poster.png");
|
|
150
|
+
const b = await browser();
|
|
151
|
+
const page = await b.newPage({ viewport: { width: w, height: h } });
|
|
152
|
+
const errors = []; page.on("pageerror", (e) => errors.push(String(e)));
|
|
153
|
+
// the same brand colours the deck hands a demo, so the poster matches what the room will see
|
|
154
|
+
const { loadBrand } = await import("./lib/brand.js");
|
|
155
|
+
const d = loadBrand(brandFlag() || DEFAULT_BRAND).roles.dark;
|
|
156
|
+
const brand = Object.fromEntries(Object.entries({ bg: d.track, card: d.paper, stroke: d.stroke, ink: d.ink, text: d.text, label: d.muted, accent: d.accent }).map(([k, v]) => [k, "#" + v]));
|
|
157
|
+
await page.addInitScript((b) => { window.wandEmbed = { motion: true, brand: b }; }, brand);
|
|
158
|
+
await page.goto(pathToFileURL(path.resolve(demoPath)).href, { waitUntil: "load" });
|
|
159
|
+
await page.waitForTimeout(+flag("--wait", "1.5") * 1000);
|
|
160
|
+
const play = +flag("--play", "0");
|
|
161
|
+
if (play) { await page.evaluate(() => (window.wandDemo?.play || window.bofaPlay)?.call(window.wandDemo)); await page.waitForTimeout(play * 1000); }
|
|
162
|
+
await page.screenshot({ path: out });
|
|
163
|
+
await closeBrowser();
|
|
164
|
+
console.log(`${rel(out)} (${w}x${h}${play ? `, after ${play}s of play` : ""})${errors.length ? `\n console: ${errors[0]}` : ""}`);
|
|
165
|
+
return errors.length ? 1 : 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
122
168
|
async function gallery() {
|
|
123
|
-
const brand = flag("--brand",
|
|
169
|
+
const brand = flag("--brand", DEFAULT_BRAND);
|
|
124
170
|
const { build, closeBrowser } = await import("./lib/engine.js");
|
|
125
171
|
process.env.WAND_GALLERY_BRAND = brand;
|
|
126
172
|
const r = await build(path.join(KIT, "examples", "gallery", "job.mjs"), {});
|
|
@@ -148,9 +194,9 @@ async function components() {
|
|
|
148
194
|
}
|
|
149
195
|
|
|
150
196
|
async function brands() {
|
|
151
|
-
const {
|
|
152
|
-
for (const b of BRANDS) { const br = loadBrand(b); console.log(`\n ${b.padEnd(8)} ${br.title}\n ${"".padEnd(8)} ${rel(br.dir)} · faces: ${br.families.join(", ")} · ${br.faceFiles.length} files · ${Object.keys(br.tokens.art).length} art roles`); }
|
|
153
|
-
console.log(
|
|
197
|
+
const { loadBrand } = await import("./lib/brand.js");
|
|
198
|
+
for (const b of BRANDS) { const br = loadBrand(b); console.log(`\n ${b.padEnd(8)} ${br.title}${b === DEFAULT_BRAND ? " (default)" : ""}\n ${"".padEnd(8)} ${rel(br.dir)} · faces: ${br.families.join(", ")} · ${br.faceFiles.length} files · ${Object.keys(br.tokens.art).length} art roles\n ${"".padEnd(8)} asked for as: ${ASKED_AS[b].join(", ")}`); }
|
|
199
|
+
console.log(`\n rule: ${brandRule()}\n`);
|
|
154
200
|
}
|
|
155
201
|
|
|
156
202
|
try {
|
|
@@ -158,10 +204,11 @@ try {
|
|
|
158
204
|
case "doctor": process.exit(await doctor());
|
|
159
205
|
case "new": newJob(); break;
|
|
160
206
|
case "build": { const r = await build(args[1], {}); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
|
|
161
|
-
case "check": { const r = await build(args[1], { outputs: false }); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
|
|
207
|
+
case "check": { const r = await build(args[1], { outputs: false, brand: brandFlag() }); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
|
|
162
208
|
case "ship": process.exit(await ship(args[1]));
|
|
163
209
|
case "watch": await watch(args[1]); break;
|
|
164
210
|
case "gallery": process.exit(await gallery());
|
|
211
|
+
case "poster": process.exit(await poster(args[1]));
|
|
165
212
|
case "components": await components(); break;
|
|
166
213
|
case "brands": await brands(); break;
|
|
167
214
|
default:
|