volito 0.1.1 → 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.
Files changed (68) hide show
  1. package/README.md +161 -5
  2. package/dist/browser.d.ts +50 -1
  3. package/dist/browser.js +352 -43
  4. package/dist/chunks/{chunk-CFDFWZAC.js → chunk-CUQY6YI3.js} +1032 -404
  5. package/dist/chunks/chunk-PM3PLI3X.js +71 -0
  6. package/dist/chunks/chunk-QA4SEJCZ.js +601 -0
  7. package/dist/chunks/chunk-ZFG3X3DU.js +67 -0
  8. package/dist/chunks/chunk-ZHMWEZ4C.js +186 -0
  9. package/dist/chunks/init-JX2BYRKT.js +189 -0
  10. package/dist/chunks/review-RBH5EUZN.js +18 -0
  11. package/dist/cli.js +232 -32
  12. package/dist/index.d.ts +314 -2
  13. package/dist/index.js +75 -21
  14. package/dist/mcp.js +129 -22
  15. package/dist/player.d.ts +643 -0
  16. package/dist/player.js +6320 -0
  17. package/dist/registry.json +1 -1
  18. package/examples/player.html +93 -0
  19. package/examples/states.html +73 -0
  20. package/examples/volito-intro/DESIGN.md +23 -0
  21. package/examples/volito-intro/frame.js +187 -0
  22. package/examples/volito-intro/item.json +43 -0
  23. package/package.json +12 -4
  24. package/packs/annotate/emphasis/frame.js +8 -0
  25. package/packs/annotate/reactions/frame.js +12 -0
  26. package/packs/backdrops/gradient/frame.js +5 -0
  27. package/packs/effects/chromatic/frame.js +17 -0
  28. package/packs/effects/chromatic/item.json +15 -0
  29. package/packs/effects/glass/frame.js +44 -0
  30. package/packs/effects/glass/item.json +16 -0
  31. package/packs/effects/grain/frame.js +20 -0
  32. package/packs/effects/grain/item.json +16 -0
  33. package/packs/effects/light-leak/frame.js +24 -0
  34. package/packs/effects/light-leak/item.json +15 -0
  35. package/packs/effects/neon/frame.js +28 -0
  36. package/packs/effects/neon/item.json +15 -0
  37. package/packs/effects/pack.json +13 -0
  38. package/packs/effects/shared.js +23 -0
  39. package/packs/effects/warp/frame.js +13 -0
  40. package/packs/effects/warp/item.json +15 -0
  41. package/packs/essentials/wipe/frame.js +13 -0
  42. package/packs/transitions/glitch/frame.js +14 -0
  43. package/packs/transitions/shared.js +12 -0
  44. package/packs/type/word-by-word/frame.js +10 -0
  45. package/packs/ui/bell/frame.js +24 -0
  46. package/packs/ui/bell/item.json +21 -0
  47. package/packs/ui/button/frame.js +55 -0
  48. package/packs/ui/button/item.json +26 -0
  49. package/packs/ui/like/frame.js +34 -0
  50. package/packs/ui/like/item.json +21 -0
  51. package/packs/ui/loader/frame.js +34 -0
  52. package/packs/ui/loader/item.json +23 -0
  53. package/packs/ui/pack.json +13 -0
  54. package/packs/ui/shared.js +54 -0
  55. package/packs/ui/toggle/frame.js +30 -0
  56. package/packs/ui/toggle/item.json +22 -0
  57. package/{skill → skills/volito}/SKILL.md +43 -3
  58. package/skills/volito/references/art-direction.md +58 -0
  59. package/skills/volito/references/brand.md +59 -0
  60. package/skills/volito/references/delivery.md +40 -0
  61. package/skills/volito/references/matching-a-reference.md +39 -0
  62. package/skills/volito/references/motion-craft.md +56 -0
  63. package/skills/volito/references/self-review.md +44 -0
  64. package/skills/volito/references/type-and-layout.md +44 -0
  65. package/skills/volito/references/voice-and-timing.md +37 -0
  66. package/skills/volito/references/writing-items.md +44 -0
  67. /package/{skill/template → templates/item}/frame.js +0 -0
  68. /package/{skill/template → templates/item}/item.json +0 -0
package/dist/cli.js CHANGED
@@ -1,7 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- ASPECT_NAMES,
3
+ bundleSpec,
4
+ isBundleFile,
5
+ loadBundleFile
6
+ } from "./chunks/chunk-PM3PLI3X.js";
7
+ import {
4
8
  AUTHORING_GUIDE,
9
+ LIBRARY_GUIDE,
10
+ MOTION_GUIDE,
11
+ SOUND_GUIDE,
12
+ findItem,
13
+ formatIssues,
14
+ isSpecFile,
15
+ itemOfSpec,
16
+ library,
17
+ loadMedia,
18
+ loadSpecFile,
19
+ readStyleArg,
20
+ readWords,
21
+ searchItems,
22
+ specFrom,
23
+ specJsonSchema,
24
+ specToOptions,
25
+ validateSpec
26
+ } from "./chunks/chunk-QA4SEJCZ.js";
27
+ import {
28
+ ASPECT_NAMES,
5
29
  BUILTIN_STYLES,
6
30
  CATEGORIES,
7
31
  DIRECTIONS,
@@ -9,10 +33,8 @@ import {
9
33
  FORMAT_NOTES,
10
34
  HITS,
11
35
  IDLES,
12
- LIBRARY_GUIDE,
13
36
  MG_API_REFERENCE,
14
37
  MOODS,
15
- MOTION_GUIDE,
16
38
  MOVES,
17
39
  MOVE_NOTES,
18
40
  MotionError,
@@ -21,27 +43,25 @@ import {
21
43
  SPLITS,
22
44
  VIDEO_FORMATS,
23
45
  checkItem,
46
+ describeCues,
24
47
  describeItem,
25
48
  describeReport,
26
- findItem,
27
- library,
28
- loadMedia,
29
- packageVersion,
30
- packsDir,
31
49
  parseAspect,
32
50
  parseMotionArgs,
33
- readStyleArg,
34
- readWords,
35
51
  renderSheet,
52
+ renderSoundFile,
36
53
  renderStill,
37
- renderVideo,
38
- searchItems
39
- } from "./chunks/chunk-CFDFWZAC.js";
54
+ renderVideo
55
+ } from "./chunks/chunk-CUQY6YI3.js";
56
+ import {
57
+ packageVersion,
58
+ packsDir
59
+ } from "./chunks/chunk-ZFG3X3DU.js";
40
60
 
41
61
  // src/cli.ts
42
62
  import { availableParallelism } from "node:os";
43
- import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
44
- import { dirname, join, resolve } from "node:path";
63
+ import { cpSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
64
+ import { dirname, join, relative, resolve } from "node:path";
45
65
  import { parseArgs } from "node:util";
46
66
  process.env.UV_THREADPOOL_SIZE ??= String(Math.max(4, Math.min(16, availableParallelism())));
47
67
  process.stdout.on("error", (e) => {
@@ -52,6 +72,16 @@ var HELP = `Volito ${packageVersion()} \u2014 brand-aware, voice-synced motion g
52
72
  Usage: volito <command> [item] [options]
53
73
 
54
74
  Commands
75
+ init [--agent a,b] [--dry-run] [--force] set this project up for Claude Code, Cursor, VS Code or Codex:
76
+ the MCP server in each agent's config and the Volito skill
77
+ doctor [--json] what is ready: Node, FFmpeg and its formats, fonts, agents
78
+ review <item> [--aspect a] [--out dir] check it, measure its timing and 9:16 safe zones, and write a
79
+ contact sheet per aspect with a report (review.md)
80
+ spec <item> [options] [--out g.volito.json] write the options as a Graphic spec, a file any command takes
81
+ bundle <spec.json> [--out g.volito] the spec and every file it uses (DESIGN.md, program, transcript,
82
+ images) in one file any command takes
83
+ validate <spec.json> [--strict] [--json] check a spec against its item before rendering
84
+ schema the Graphic spec's JSON Schema
55
85
  list [--search words] [--category c] [--json] the graphics in the packs
56
86
  info <item> [--json] [--code] what an item is and every prop it takes
57
87
  styles [--json] the built-in brand styles
@@ -59,6 +89,7 @@ Commands
59
89
  render <item> [--out file] [--format f] render to video (ProRes 4444 .mov by default)
60
90
  still <item> [--t seconds] [--out file.png|.svg] one frame
61
91
  sheet <item> [--out sheet.png] frames across its time in one picture
92
+ sound <item> [--out track.wav] its sound alone, as a WAV, with the cue list
62
93
  check <item|file.js> [--aspect a] run it at every aspect and report problems
63
94
  add <item> [--dir ./motion] copy an item's source into your project to edit
64
95
  new <id> [--dir ./motion] start a new item from a template
@@ -66,7 +97,8 @@ Commands
66
97
  guide how items and packs are written
67
98
  mcp run the MCP server on stdio
68
99
 
69
- An item is "pack/id" (essentials/lower-third), its id alone (lower-third), an item folder, or a .js program.
100
+ An item is "pack/id" (essentials/lower-third), its id alone (lower-third), an item folder, a .js program, or a
101
+ Graphic spec (.json with "volito": 1); options given on the command line win over the spec's.
70
102
 
71
103
  Options
72
104
  --prop name=value set a prop (repeat it); lists take "a; b; c" or JSON
@@ -88,6 +120,11 @@ Options
88
120
  --mood <m> a choreography: ${MOODS.join(", ")}
89
121
  --seed <n> a new choreography for every number (with or without a mood)
90
122
  --speed <x> every move faster (2) or slower (0.5)
123
+ --sound a sound track, synthesised by Volito, muxed into the video (not png/gif): the program's own
124
+ sound($) cues, else cues read from its motion \u2014 pops, whooshes, ticks, hits
125
+ --bed <key> a pad under it all, in a key: Dm, "F major", C#m
126
+ --sound-level <x> the track's level (1)
127
+ --sound-out <file.wav> also write the track alone (for an editor's timeline, next to a ProRes overlay)
91
128
  --packs <dir> more packs (also VOLITO_PACKS)
92
129
  --format <f> render: ${VIDEO_FORMATS.join(", ")} \xB7 still: png, svg
93
130
  --json machine-readable output
@@ -122,6 +159,10 @@ var OPTIONS = {
122
159
  seed: { type: "string" },
123
160
  speed: { type: "string" },
124
161
  packs: { type: "string", multiple: true },
162
+ sound: { type: "boolean" },
163
+ bed: { type: "string" },
164
+ "sound-level": { type: "string" },
165
+ "sound-out": { type: "string" },
125
166
  format: { type: "string" },
126
167
  out: { type: "string" },
127
168
  t: { type: "string" },
@@ -133,6 +174,11 @@ var OPTIONS = {
133
174
  pack: { type: "string" },
134
175
  json: { type: "boolean" },
135
176
  code: { type: "boolean" },
177
+ agent: { type: "string" },
178
+ force: { type: "boolean" },
179
+ "dry-run": { type: "boolean" },
180
+ "no-skill": { type: "boolean" },
181
+ strict: { type: "boolean" },
136
182
  quiet: { type: "boolean", short: "q" },
137
183
  help: { type: "boolean", short: "h" },
138
184
  version: { type: "boolean", short: "v" }
@@ -143,6 +189,16 @@ var num = (s, name) => {
143
189
  if (!Number.isFinite(n)) throw new MotionError(`--${name} "${s}" is not a number`);
144
190
  return n;
145
191
  };
192
+ function mergeMotion(base, over) {
193
+ const out = {};
194
+ for (const [part, s] of Object.entries(base ?? {})) out[part] = { ...s };
195
+ for (const [part, s] of Object.entries(over)) out[part] = { ...out[part] ?? {}, ...s };
196
+ return out;
197
+ }
198
+ function relativeTo(file, path) {
199
+ const rel = relative(dirname(resolve(file)), resolve(path));
200
+ return rel.startsWith(".") ? rel : `./${rel}`;
201
+ }
146
202
  function pairs(list, flag) {
147
203
  return (list ?? []).map((p) => {
148
204
  const i = p.indexOf("=");
@@ -150,27 +206,28 @@ function pairs(list, flag) {
150
206
  return [p.slice(0, i).trim(), p.slice(i + 1)];
151
207
  });
152
208
  }
153
- async function buildOptions(v, item) {
154
- let props = {};
209
+ async function buildOptions(v, item, base = {}) {
210
+ let props = { ...base.props ?? {} };
155
211
  if (v.props) {
156
212
  const raw = existsSync(resolve(v.props)) ? readFileSync(resolve(v.props), "utf8") : v.props;
157
213
  try {
158
- props = JSON.parse(raw);
214
+ props = { ...props, ...JSON.parse(raw) };
159
215
  } catch {
160
216
  throw new MotionError("--props takes JSON or a JSON file");
161
217
  }
162
218
  }
163
219
  for (const [k, val] of pairs(v.prop, "prop")) props[k] = val;
164
220
  for (const [k, val] of pairs(v.image, "image")) props[k] = val;
165
- const beats = {};
221
+ const beats = { ...base.beats ?? {} };
166
222
  for (const [k, val] of pairs(v.beat, "beat")) beats[k] = num(val, "beat");
167
- const colors = {};
223
+ const colors = { ...base.colors ?? {} };
168
224
  for (const k of ["accent", "accent2", "ink", "paper", "surface", "display", "text", "mono"]) if (v[k]) colors[k] = v[k];
169
225
  if (v.mode) {
170
226
  if (v.mode !== "dark" && v.mode !== "light") throw new MotionError('--mode is "dark" or "light"');
171
227
  colors.mode = v.mode;
172
228
  }
173
229
  const opts = {
230
+ ...base,
174
231
  props,
175
232
  colors,
176
233
  ...v.style ? { style: readStyleArg(v.style) } : {},
@@ -181,10 +238,13 @@ async function buildOptions(v, item) {
181
238
  ...v.bg ? { background: v.bg } : {},
182
239
  ...v.words ? { words: readWords(v.words, num(v.offset, "offset") ?? 0) } : {},
183
240
  ...Object.keys(beats).length ? { beats } : {},
184
- ...v.motion?.length ? { motion: parseMotionArgs(v.motion) } : {},
241
+ ...v.motion?.length ? { motion: mergeMotion(base.motion, parseMotionArgs(v.motion)) } : {},
185
242
  ...v.mood ? { mood: v.mood } : {},
186
243
  ...v.seed !== void 0 ? { seed: num(v.seed, "seed") } : {},
187
- ...v.speed ? { speed: num(v.speed, "speed") } : {}
244
+ ...v.speed ? { speed: num(v.speed, "speed") } : {},
245
+ ...v.sound || v.bed || v["sound-level"] || v["sound-out"] ? { sound: v["sound-level"] ? { level: num(v["sound-level"], "sound-level") } : true } : {},
246
+ ...v.bed ? { bed: v.bed } : {},
247
+ ...v["sound-out"] ? { soundOut: v["sound-out"] } : {}
188
248
  };
189
249
  opts.media = await loadMedia(item, props);
190
250
  return opts;
@@ -222,11 +282,123 @@ async function main(argv) {
222
282
  }
223
283
  if (rest.length) throw new MotionError(`unexpected "${rest.join(" ")}" \u2014 options start with --`);
224
284
  const lib = library(v.packs ?? []);
285
+ const loaded = target && cmd !== "validate" && cmd !== "bundle" ? isBundleFile(target) ? loadBundleFile(target) : isSpecFile(target) ? loadSpecFile(target) : null : null;
225
286
  const need = () => {
226
287
  if (!target) throw new MotionError(`${cmd} needs an item (see: volito list)`);
227
- return findItem(target, lib);
288
+ return loaded ? itemOfSpec(loaded, lib) : findItem(target, lib);
228
289
  };
290
+ const optionsFor = (item) => buildOptions(v, item, loaded ? specToOptions(loaded, item) : {});
229
291
  switch (cmd) {
292
+ case "validate": {
293
+ if (!target) throw new MotionError("validate needs a spec file");
294
+ let doc;
295
+ try {
296
+ doc = JSON.parse(readFileSync(resolve(target), "utf8"));
297
+ } catch (e) {
298
+ throw new MotionError(`${target}: ${e.message}`);
299
+ }
300
+ const rep = validateSpec(doc, { dir: dirname(resolve(target)), lib });
301
+ const ok = rep.ok && !(v.strict && rep.warnings.length);
302
+ if (v.json) process.stdout.write(`${JSON.stringify({ ...rep, ok }, null, 2)}
303
+ `);
304
+ else {
305
+ const lines = [ok ? `\u2713 ${target} is a valid spec${rep.item ? ` for ${rep.item}` : ""}` : `\u2717 ${target}`];
306
+ if (rep.errors.length) lines.push(formatIssues(rep.errors));
307
+ if (rep.warnings.length) lines.push(formatIssues(rep.warnings, "!"));
308
+ process.stdout.write(`${lines.join("\n")}
309
+ `);
310
+ }
311
+ return ok ? 0 : 1;
312
+ }
313
+ case "spec": {
314
+ const item = need();
315
+ const base = loaded?.spec;
316
+ const props = { ...base?.props ?? {} };
317
+ if (v.props) Object.assign(props, JSON.parse(existsSync(resolve(v.props)) ? readFileSync(resolve(v.props), "utf8") : v.props));
318
+ for (const [k, val] of [...pairs(v.prop, "prop"), ...pairs(v.image, "image")]) props[k] = val;
319
+ const colors = { ...base?.colors ?? {} };
320
+ for (const k of ["accent", "accent2", "ink", "paper", "surface", "mode", "display", "text", "mono"]) if (v[k]) colors[k] = v[k];
321
+ const beats = { ...base?.beats ?? {} };
322
+ for (const [k, val] of pairs(v.beat, "beat")) beats[k] = num(val, "beat");
323
+ const out = v.out ?? `${item.id}.volito.json`;
324
+ const spec = specFrom({
325
+ ...base ?? {},
326
+ ...loaded ? {} : item.pack === "custom" || item.pack === "local" ? { program: relativeTo(out, target) } : { item: item.ref },
327
+ props,
328
+ ...v.style ? { style: /\.(md|markdown|txt)$/i.test(v.style) ? relativeTo(out, v.style) : v.style } : {},
329
+ colors,
330
+ ...v.aspect ? { aspect: v.aspect } : {},
331
+ ...v.duration ? { duration: num(v.duration, "duration") } : {},
332
+ ...v.fps ? { fps: num(v.fps, "fps") } : {},
333
+ ...v.width ? { width: num(v.width, "width") } : {},
334
+ ...v.bg ? { background: v.bg } : {},
335
+ ...v.words ? { words: relativeTo(out, v.words) } : {},
336
+ ...v.offset ? { offset: num(v.offset, "offset") } : {},
337
+ beats,
338
+ ...v.motion?.length ? { motion: mergeMotion(base?.motion, parseMotionArgs(v.motion)) } : {},
339
+ ...v.mood ? { mood: v.mood } : {},
340
+ ...v.seed !== void 0 ? { seed: num(v.seed, "seed") } : {},
341
+ ...v.speed ? { speed: num(v.speed, "speed") } : {},
342
+ ...v.sound || v.bed || v["sound-level"] ? { sound: v.bed || v["sound-level"] ? { ...v.bed ? { bed: v.bed } : {}, ...v["sound-level"] ? { level: num(v["sound-level"], "sound-level") } : {} } : true } : {},
343
+ ...v.format ? { render: { ...base?.render ?? {}, format: v.format } } : {}
344
+ });
345
+ const rep = validateSpec(spec, { dir: dirname(resolve(out)), lib });
346
+ if (!rep.ok) throw new MotionError(`that spec would not render:
347
+ ${formatIssues(rep.errors)}`);
348
+ mkdirSync(dirname(resolve(out)), { recursive: true });
349
+ writeFileSync(resolve(out), `${JSON.stringify(spec, null, 2)}
350
+ `);
351
+ warn(rep.warnings.map((w) => `${w.path ? `${w.path}: ` : ""}${w.message}`), v.quiet);
352
+ process.stdout.write(`${resolve(out)}
353
+ render it with: volito render ${JSON.stringify(out)}
354
+ `);
355
+ return 0;
356
+ }
357
+ case "bundle": {
358
+ if (!target || !isSpecFile(target)) throw new MotionError('bundle needs a spec file (.json with "volito": 1) \u2014 write one with volito spec');
359
+ const b = bundleSpec(target, (spec, dir) => {
360
+ const item = itemOfSpec({ spec, dir }, lib);
361
+ return Object.entries(item.props).filter(([, p]) => p.type === "image").map(([k]) => k);
362
+ });
363
+ const out = resolve(v.out ?? target.replace(/(\.volito)?\.json$/i, "") + ".volito");
364
+ mkdirSync(dirname(out), { recursive: true });
365
+ writeFileSync(out, `${JSON.stringify(b)}
366
+ `);
367
+ const names = Object.keys(b.files);
368
+ process.stdout.write(`${out}
369
+ the spec${names.length ? ` and ${names.join(", ")}` : ""} \xB7 ${bytes(statSync(out).size)}
370
+ `);
371
+ return 0;
372
+ }
373
+ case "schema":
374
+ process.stdout.write(`${JSON.stringify(specJsonSchema(), null, 2)}
375
+ `);
376
+ return 0;
377
+ case "init": {
378
+ const { applyInit, describeInit, parseAgents, planInit } = await import("./chunks/init-JX2BYRKT.js");
379
+ let agents;
380
+ try {
381
+ agents = v.agent ? parseAgents(v.agent) : void 0;
382
+ } catch (e) {
383
+ throw new MotionError(e.message);
384
+ }
385
+ const plan = planInit(resolve(target ?? "."), { agents, force: v.force, skill: !v["no-skill"] });
386
+ if (!v["dry-run"]) applyInit(plan.actions);
387
+ if (v.json) process.stdout.write(`${JSON.stringify({ agents: plan.agents, dryRun: !!v["dry-run"], actions: plan.actions.map(({ kind, agent, detail, ...a }) => ({ kind, agent, detail, path: "path" in a ? a.path : void 0 })) }, null, 2)}
388
+ `);
389
+ else process.stdout.write(`${describeInit(plan, !!v["dry-run"])}
390
+ `);
391
+ return 0;
392
+ }
393
+ case "doctor": {
394
+ const { describeDoctor, runDoctor } = await import("./chunks/init-JX2BYRKT.js");
395
+ const checks = runDoctor(resolve(target ?? "."));
396
+ if (v.json) process.stdout.write(`${JSON.stringify(checks, null, 2)}
397
+ `);
398
+ else process.stdout.write(`${describeDoctor(checks)}
399
+ `);
400
+ return checks.some((c) => !c.ok && (c.name === "Node" || c.name === "Fonts")) ? 1 : 0;
401
+ }
230
402
  case "list": {
231
403
  if (v.category && !CATEGORIES.includes(v.category)) throw new MotionError(`--category is one of ${CATEGORIES.join(", ")}`);
232
404
  const items = searchItems({ search: v.search ?? target, category: v.category, pack: v.pack }, lib);
@@ -289,20 +461,22 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
289
461
  }
290
462
  case "render": {
291
463
  const item = need();
292
- const opts = await buildOptions(v, item);
293
- if (v.format && !VIDEO_FORMATS.includes(v.format)) throw new MotionError(`--format is one of ${VIDEO_FORMATS.join(", ")}`);
294
- const r = await renderVideo(item, { ...opts, ...v.format ? { format: v.format } : {}, ...v.out ? { out: v.out } : {}, onProgress: progress(v.quiet) });
464
+ const opts = await optionsFor(item);
465
+ const format = v.format ?? loaded?.spec.render?.format;
466
+ const out = v.out ?? loaded?.spec.render?.out;
467
+ if (format && !VIDEO_FORMATS.includes(format)) throw new MotionError(`--format is one of ${VIDEO_FORMATS.join(", ")}`);
468
+ const r = await renderVideo(item, { ...opts, ...format ? { format } : {}, ...out ? { out } : {}, onProgress: progress(v.quiet) });
295
469
  warn(r.warnings, v.quiet);
296
470
  if (v.json) process.stdout.write(`${JSON.stringify(r, null, 2)}
297
471
  `);
298
472
  else process.stdout.write(`${r.file}
299
- ${r.format} \xB7 ${r.width}\xD7${r.height} \xB7 ${r.fps} fps \xB7 ${r.duration.toFixed(2)} s \xB7 ${r.frames} frames (${r.drawn} drawn)${r.bytes ? ` \xB7 ${bytes(r.bytes)}` : ""} \xB7 ${(r.ms / 1e3).toFixed(1)} s
473
+ ${r.format} \xB7 ${r.width}\xD7${r.height} \xB7 ${r.fps} fps \xB7 ${r.duration.toFixed(2)} s \xB7 ${r.frames} frames (${r.drawn} drawn)${r.bytes ? ` \xB7 ${bytes(r.bytes)}` : ""}${r.sound ? ` \xB7 sound: ${r.sound.cues.length} cues${r.sound.bed ? ` on a ${r.sound.bed.key} ${r.sound.bed.mode} bed` : ""}${r.sound.file ? ` \u2192 ${r.sound.file}` : ""}` : ""} \xB7 ${(r.ms / 1e3).toFixed(1)} s
300
474
  `);
301
475
  return 0;
302
476
  }
303
477
  case "still": {
304
478
  const item = need();
305
- const opts = await buildOptions(v, item);
479
+ const opts = await optionsFor(item);
306
480
  const format = v.format === "svg" || v.out?.endsWith(".svg") ? "svg" : "png";
307
481
  const out = v.out ?? `${item.id}.${format}`;
308
482
  const r = await renderStill(item, { ...opts, format, out, ...v.t ? { t: num(v.t, "t") } : {} });
@@ -315,7 +489,7 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
315
489
  }
316
490
  case "sheet": {
317
491
  const item = need();
318
- const opts = await buildOptions(v, item);
492
+ const opts = await optionsFor(item);
319
493
  const times = v.times ? v.times.split(",").map((s) => num(s.trim(), "times")) : void 0;
320
494
  const r = await renderSheet(item, { ...opts, ...times ? { times } : {}, ...v.cols ? { cols: num(v.cols, "cols") } : {} });
321
495
  const out = resolve(v.out ?? `${item.id}-sheet.png`);
@@ -328,9 +502,20 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
328
502
  `);
329
503
  return 0;
330
504
  }
505
+ case "review": {
506
+ const item = need();
507
+ const opts = await optionsFor(item);
508
+ const { describeReview, reviewItem } = await import("./chunks/review-RBH5EUZN.js");
509
+ const rep = await reviewItem(item, { ...opts, ...v.aspect ? { aspects: [parseAspect(v.aspect)] } : {}, outDir: v.out ?? join("volito-review", item.id) });
510
+ process.stdout.write(v.json ? `${JSON.stringify(rep, null, 2)}
511
+ ` : `${describeReview(rep)}
512
+ report: ${join(rep.dir, "review.md")}
513
+ `);
514
+ return rep.ok ? 0 : 1;
515
+ }
331
516
  case "check": {
332
517
  const item = need();
333
- const opts = await buildOptions(v, item);
518
+ const opts = await optionsFor(item);
334
519
  const rep = await checkItem(item, { ...opts, ...v.aspect ? { aspects: [parseAspect(v.aspect)] } : {} });
335
520
  process.stdout.write(v.json ? `${JSON.stringify(rep, null, 2)}
336
521
  ` : `${describeReport(rep)}
@@ -355,7 +540,7 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
355
540
  if (!target || !/^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/.test(target)) throw new MotionError("new needs an id: lowercase words joined by hyphens");
356
541
  const dir = resolve(v.dir ?? "motion", target);
357
542
  if (existsSync(dir)) throw new MotionError(`${dir} already exists`);
358
- cpSync(join(packsDir(), "..", "skill", "template"), dir, { recursive: true });
543
+ cpSync(join(packsDir(), "..", "templates", "item"), dir, { recursive: true });
359
544
  const manifest = JSON.parse(readFileSync(join(dir, "item.json"), "utf8"));
360
545
  manifest.id = target;
361
546
  manifest.name = target.replace(/-/g, " ").replace(/^./, (c) => c.toUpperCase());
@@ -363,6 +548,19 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
363
548
  `);
364
549
  process.stdout.write(`${dir}
365
550
  edit item.json and frame.js, then: volito sheet ${JSON.stringify(dir)}
551
+ `);
552
+ return 0;
553
+ }
554
+ case "sound": {
555
+ const item = need();
556
+ const opts = await optionsFor(item);
557
+ const r = await renderSoundFile(item, { ...opts, sound: opts.sound ?? true, out: v.out ?? `${item.id}.wav` });
558
+ warn(r.warnings, v.quiet);
559
+ if (v.json) process.stdout.write(`${JSON.stringify({ file: r.file, duration: r.duration, peak: r.peak, bed: r.bed, cues: r.cues }, null, 2)}
560
+ `);
561
+ else process.stdout.write(`${r.file}
562
+ ${r.duration.toFixed(2)} s \xB7 ${r.cues.length} cues${r.bed ? ` \xB7 bed ${r.bed.key} ${r.bed.mode}` : ""} \xB7 peak ${r.peak.toFixed(2)}
563
+ ${describeCues(r.cues)}
366
564
  `);
367
565
  return 0;
368
566
  }
@@ -370,6 +568,8 @@ Also: from=${DIRECTIONS.slice(0, 4).join("|")}\u2026 \xB7 distance=px \xB7 blur=
370
568
  process.stdout.write(`${MG_API_REFERENCE}
371
569
 
372
570
  ${LIBRARY_GUIDE}
571
+
572
+ ${SOUND_GUIDE}
373
573
  `);
374
574
  return 0;
375
575
  case "guide":