wand-kit 0.2.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 +86 -5
- package/SKILL.md +41 -4
- package/START-HERE.md +6 -0
- 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 +3 -0
- package/lib/components.js +151 -5
- package/lib/engine.js +32 -6
- 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 +52 -9
|
@@ -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
|
@@ -6,21 +6,27 @@
|
|
|
6
6
|
* node wand.js new <dir> [--surface letter] [--brand current|next] [--name "Title"]
|
|
7
7
|
* a job folder to compose in, on the default brand
|
|
8
8
|
* unless --brand names the other
|
|
9
|
-
* node wand.js
|
|
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
|
|
10
11
|
* node wand.js check <job.mjs> gates only
|
|
11
|
-
* 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
|
|
12
16
|
* node wand.js watch <job.mjs> rebuild on save, the browser kept warm
|
|
13
17
|
* node wand.js gallery [--brand current|next] every component, light and dark, on one surface each
|
|
14
18
|
* node wand.js components the vocabulary
|
|
15
19
|
* node wand.js brands the brands this kit can draw with
|
|
16
20
|
*
|
|
17
21
|
* A job is a folder with `job.mjs` (the composition) and, for a rebuild,
|
|
18
|
-
* `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>/`.
|
|
19
24
|
*/
|
|
20
25
|
import fs from "node:fs";
|
|
21
26
|
import path from "node:path";
|
|
22
27
|
import { fileURLToPath } from "node:url";
|
|
23
28
|
import { createRequire } from "node:module";
|
|
29
|
+
import { pathToFileURL } from "node:url";
|
|
24
30
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
25
31
|
import { BRANDS, DEFAULT_BRAND, ASKED_AS, brandRule } from "./lib/brand.js";
|
|
26
32
|
|
|
@@ -58,7 +64,7 @@ function newJob() {
|
|
|
58
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\"]");
|
|
59
65
|
const surface = flag("--surface", "letter"), brand = flag("--brand", DEFAULT_BRAND), name = flag("--name", path.basename(dir));
|
|
60
66
|
if (!BRANDS.includes(brand)) die(`unknown brand "${brand}" - one of ${BRANDS.join(", ")}`);
|
|
61
|
-
const tpl = path.join(KIT, "templates", `${["slide", "square", "og"].includes(surface) ? (surface === "slide" ? "slide" : "card") : "page"}.mjs`);
|
|
67
|
+
const tpl = path.join(KIT, "templates", `${surface === "storyboard" ? "storyboard" : ["slide", "square", "og"].includes(surface) ? (surface === "slide" ? "slide" : "card") : "page"}.mjs`);
|
|
62
68
|
if (!fs.existsSync(tpl)) die(`no template for surface "${surface}"`);
|
|
63
69
|
if (fs.existsSync(path.join(dir, "job.mjs"))) die(`${dir}/job.mjs already exists`);
|
|
64
70
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -68,24 +74,36 @@ function newJob() {
|
|
|
68
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")}`);
|
|
69
75
|
}
|
|
70
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
|
+
|
|
71
84
|
async function build(jobPath, opts) {
|
|
72
|
-
if (!jobPath) die("usage: node wand.js build <job.mjs>");
|
|
85
|
+
if (!jobPath) die("usage: node wand.js build <job.mjs> [--brand current|next]");
|
|
73
86
|
const { build } = await import("./lib/engine.js");
|
|
74
|
-
return build(jobPath, opts);
|
|
87
|
+
return build(jobPath, { ...opts, brand: brandFlag() });
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
async function ship(jobPath) {
|
|
78
91
|
if (!jobPath) die("usage: node wand.js ship <job.mjs> [--pptx] [--no-html]");
|
|
79
92
|
const { build, closeBrowser } = await import("./lib/engine.js");
|
|
80
|
-
const r = await build(jobPath, { quiet: true });
|
|
93
|
+
const r = await build(jobPath, { quiet: true, brand: brandFlag() });
|
|
81
94
|
const { surface } = await import("./lib/surfaces.js");
|
|
82
95
|
const s = surface(r.surface);
|
|
83
96
|
if (!on("--no-html")) {
|
|
84
97
|
const { singleFile } = await import("./lib/html_export.js");
|
|
85
98
|
const out = path.join(path.dirname(r.outputs.pdf), `${r.name}.html`);
|
|
86
|
-
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 });
|
|
87
100
|
r.outputs.single = sf.out;
|
|
88
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 }); }
|
|
89
107
|
}
|
|
90
108
|
if (on("--pptx")) {
|
|
91
109
|
try {
|
|
@@ -123,6 +141,30 @@ async function watch(jobPath) {
|
|
|
123
141
|
process.on("SIGINT", async () => { await closeBrowser(); process.exit(0); });
|
|
124
142
|
}
|
|
125
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
|
+
|
|
126
168
|
async function gallery() {
|
|
127
169
|
const brand = flag("--brand", DEFAULT_BRAND);
|
|
128
170
|
const { build, closeBrowser } = await import("./lib/engine.js");
|
|
@@ -162,10 +204,11 @@ try {
|
|
|
162
204
|
case "doctor": process.exit(await doctor());
|
|
163
205
|
case "new": newJob(); break;
|
|
164
206
|
case "build": { const r = await build(args[1], {}); const { closeBrowser } = await import("./lib/engine.js"); await closeBrowser(); process.exit(r.ok ? 0 : 1); }
|
|
165
|
-
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); }
|
|
166
208
|
case "ship": process.exit(await ship(args[1]));
|
|
167
209
|
case "watch": await watch(args[1]); break;
|
|
168
210
|
case "gallery": process.exit(await gallery());
|
|
211
|
+
case "poster": process.exit(await poster(args[1]));
|
|
169
212
|
case "components": await components(); break;
|
|
170
213
|
case "brands": await brands(); break;
|
|
171
214
|
default:
|