@anvpx/motion-cli 0.0.0-stage → 0.1.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 (197) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -2
  3. package/dist/analyze/command.d.ts +2 -0
  4. package/dist/analyze/command.js +190 -0
  5. package/dist/approval/approval.d.ts +32 -0
  6. package/dist/approval/approval.js +141 -0
  7. package/dist/approval/command.d.ts +2 -0
  8. package/dist/approval/command.js +255 -0
  9. package/dist/approval/gate.d.ts +7 -0
  10. package/dist/approval/gate.js +23 -0
  11. package/dist/approval/hash.d.ts +71 -0
  12. package/dist/approval/hash.js +451 -0
  13. package/dist/args.d.ts +30 -0
  14. package/dist/args.js +53 -0
  15. package/dist/assets/command.d.ts +2 -0
  16. package/dist/assets/command.js +165 -0
  17. package/dist/captions/command.d.ts +2 -0
  18. package/dist/captions/command.js +341 -0
  19. package/dist/captions/cues.d.ts +10 -0
  20. package/dist/captions/cues.js +61 -0
  21. package/dist/catalog/command.d.ts +2 -0
  22. package/dist/catalog/command.js +60 -0
  23. package/dist/catalog/format.d.ts +13 -0
  24. package/dist/catalog/format.js +54 -0
  25. package/dist/cli.d.ts +3 -0
  26. package/dist/cli.js +35 -0
  27. package/dist/commands.d.ts +3 -0
  28. package/dist/commands.js +59 -0
  29. package/dist/compare/command.d.ts +4 -0
  30. package/dist/compare/command.js +144 -0
  31. package/dist/contact/argv.d.ts +24 -0
  32. package/dist/contact/argv.js +42 -0
  33. package/dist/contact/command.d.ts +2 -0
  34. package/dist/contact/command.js +79 -0
  35. package/dist/doctor/checks.d.ts +25 -0
  36. package/dist/doctor/checks.js +571 -0
  37. package/dist/doctor/command.d.ts +4 -0
  38. package/dist/doctor/command.js +88 -0
  39. package/dist/doctor/format.d.ts +7 -0
  40. package/dist/doctor/format.js +41 -0
  41. package/dist/doctor/heal.d.ts +14 -0
  42. package/dist/doctor/heal.js +71 -0
  43. package/dist/doctor/pins.d.ts +8 -0
  44. package/dist/doctor/pins.js +28 -0
  45. package/dist/doctor/skills.d.ts +15 -0
  46. package/dist/doctor/skills.js +58 -0
  47. package/dist/doctor/types.d.ts +36 -0
  48. package/dist/doctor/types.js +1 -0
  49. package/dist/doctor/vendored.d.ts +15 -0
  50. package/dist/doctor/vendored.js +90 -0
  51. package/dist/doctor/version.d.ts +8 -0
  52. package/dist/doctor/version.js +11 -0
  53. package/dist/exit-codes.d.ts +16 -0
  54. package/dist/exit-codes.js +15 -0
  55. package/dist/fit/command.d.ts +5 -0
  56. package/dist/fit/command.js +154 -0
  57. package/dist/fit/solve.d.ts +21 -0
  58. package/dist/fit/solve.js +80 -0
  59. package/dist/fit/target.d.ts +18 -0
  60. package/dist/fit/target.js +47 -0
  61. package/dist/generate3d/command.d.ts +3 -0
  62. package/dist/generate3d/command.js +38 -0
  63. package/dist/generate3d/run.d.ts +39 -0
  64. package/dist/generate3d/run.js +77 -0
  65. package/dist/icon/command.d.ts +2 -0
  66. package/dist/icon/command.js +113 -0
  67. package/dist/index.d.ts +18 -0
  68. package/dist/index.js +16 -0
  69. package/dist/install-mk.d.ts +31 -0
  70. package/dist/install-mk.js +168 -0
  71. package/dist/integration/preview.integration.d.ts +1 -0
  72. package/dist/integration/preview.integration.js +53 -0
  73. package/dist/integration/smoke.integration.d.ts +1 -0
  74. package/dist/integration/smoke.integration.js +91 -0
  75. package/dist/main.d.ts +2 -0
  76. package/dist/main.js +4 -0
  77. package/dist/node-io.d.ts +14 -0
  78. package/dist/node-io.js +152 -0
  79. package/dist/optimize/command.d.ts +12 -0
  80. package/dist/optimize/command.js +110 -0
  81. package/dist/packs/command.d.ts +2 -0
  82. package/dist/packs/command.js +109 -0
  83. package/dist/patch/apply.d.ts +36 -0
  84. package/dist/patch/apply.js +186 -0
  85. package/dist/patch/command.d.ts +2 -0
  86. package/dist/patch/command.js +92 -0
  87. package/dist/plugin/install.d.ts +40 -0
  88. package/dist/plugin/install.js +170 -0
  89. package/dist/preview/command.d.ts +10 -0
  90. package/dist/preview/command.js +758 -0
  91. package/dist/preview/manifest.d.ts +94 -0
  92. package/dist/preview/manifest.js +19 -0
  93. package/dist/preview/pack.d.ts +24 -0
  94. package/dist/preview/pack.js +53 -0
  95. package/dist/preview/plan.d.ts +58 -0
  96. package/dist/preview/plan.js +160 -0
  97. package/dist/preview/provenance.d.ts +23 -0
  98. package/dist/preview/provenance.js +38 -0
  99. package/dist/publish/command.d.ts +2 -0
  100. package/dist/publish/command.js +70 -0
  101. package/dist/publish/player.d.ts +25 -0
  102. package/dist/publish/player.js +108 -0
  103. package/dist/publish/static.d.ts +16 -0
  104. package/dist/publish/static.js +78 -0
  105. package/dist/qc.d.ts +2 -0
  106. package/dist/qc.js +192 -0
  107. package/dist/scene/author.d.ts +16 -0
  108. package/dist/scene/author.js +260 -0
  109. package/dist/scene/command.d.ts +2 -0
  110. package/dist/scene/command.js +110 -0
  111. package/dist/scene/ops.d.ts +23 -0
  112. package/dist/scene/ops.js +151 -0
  113. package/dist/state/command.d.ts +2 -0
  114. package/dist/state/command.js +128 -0
  115. package/dist/state/evidence.d.ts +71 -0
  116. package/dist/state/evidence.js +243 -0
  117. package/dist/storyboard/check.d.ts +21 -0
  118. package/dist/storyboard/check.js +39 -0
  119. package/dist/storyboard/containment.d.ts +24 -0
  120. package/dist/storyboard/containment.js +43 -0
  121. package/dist/storyboard/custom.d.ts +15 -0
  122. package/dist/storyboard/custom.js +133 -0
  123. package/dist/storyboard/edit.d.ts +19 -0
  124. package/dist/storyboard/edit.js +39 -0
  125. package/dist/storyboard/load.d.ts +11 -0
  126. package/dist/storyboard/load.js +52 -0
  127. package/dist/storyboard/report.d.ts +5 -0
  128. package/dist/storyboard/report.js +8 -0
  129. package/dist/template/coerce.d.ts +6 -0
  130. package/dist/template/coerce.js +44 -0
  131. package/dist/template/command.d.ts +5 -0
  132. package/dist/template/command.js +163 -0
  133. package/dist/template/env.d.ts +34 -0
  134. package/dist/template/env.js +38 -0
  135. package/dist/template/fork.d.ts +8 -0
  136. package/dist/template/fork.js +156 -0
  137. package/dist/template/format.d.ts +21 -0
  138. package/dist/template/format.js +161 -0
  139. package/dist/template/list.d.ts +15 -0
  140. package/dist/template/list.js +116 -0
  141. package/dist/template/new.d.ts +6 -0
  142. package/dist/template/new.js +302 -0
  143. package/dist/testing/env.d.ts +8 -0
  144. package/dist/testing/env.js +19 -0
  145. package/dist/testing/fakes.d.ts +33 -0
  146. package/dist/testing/fakes.js +95 -0
  147. package/dist/testing/fixtures.d.ts +8 -0
  148. package/dist/testing/fixtures.js +6 -0
  149. package/dist/testing/probe.d.ts +15 -0
  150. package/dist/testing/probe.js +20 -0
  151. package/dist/testing/project.d.ts +22 -0
  152. package/dist/testing/project.js +26 -0
  153. package/dist/tooling/session.d.ts +40 -0
  154. package/dist/tooling/session.js +106 -0
  155. package/dist/types.d.ts +59 -0
  156. package/dist/types.js +1 -0
  157. package/dist/verify/audio-loudness.d.ts +46 -0
  158. package/dist/verify/audio-loudness.js +186 -0
  159. package/dist/verify/command.d.ts +2 -0
  160. package/dist/verify/command.js +161 -0
  161. package/dist/verify/determinism.d.ts +30 -0
  162. package/dist/verify/determinism.js +59 -0
  163. package/dist/verify/expectation.d.ts +15 -0
  164. package/dist/verify/expectation.js +19 -0
  165. package/dist/verify/media-events.d.ts +61 -0
  166. package/dist/verify/media-events.js +174 -0
  167. package/dist/verify/platform.d.ts +33 -0
  168. package/dist/verify/platform.js +59 -0
  169. package/dist/verify/probe.d.ts +14 -0
  170. package/dist/verify/probe.js +23 -0
  171. package/dist/verify/report.d.ts +15 -0
  172. package/dist/verify/report.js +45 -0
  173. package/dist/verify/run.d.ts +27 -0
  174. package/dist/verify/run.js +53 -0
  175. package/dist/voice/command.d.ts +3 -0
  176. package/dist/voice/command.js +49 -0
  177. package/dist/voice/manifest.d.ts +33 -0
  178. package/dist/voice/manifest.js +69 -0
  179. package/dist/voice/run.d.ts +21 -0
  180. package/dist/voice/run.js +181 -0
  181. package/dist/workflow/build.d.ts +2 -0
  182. package/dist/workflow/build.js +25 -0
  183. package/dist/workflow/draft.d.ts +9 -0
  184. package/dist/workflow/draft.js +201 -0
  185. package/dist/workflow/finish.d.ts +23 -0
  186. package/dist/workflow/finish.js +28 -0
  187. package/dist/workflow/project.d.ts +73 -0
  188. package/dist/workflow/project.js +198 -0
  189. package/dist/workflow/provenance.d.ts +18 -0
  190. package/dist/workflow/provenance.js +36 -0
  191. package/dist/workflow/render.d.ts +31 -0
  192. package/dist/workflow/render.js +705 -0
  193. package/dist/workflow/still.d.ts +5 -0
  194. package/dist/workflow/still.js +62 -0
  195. package/dist/workflow/validate.d.ts +2 -0
  196. package/dist/workflow/validate.js +49 -0
  197. package/package.json +38 -3
@@ -0,0 +1,54 @@
1
+ /** Constitution 8.4: the index stays within 3k tokens (estimated as chars / 4). */
2
+ export const INDEX_TOKEN_BUDGET = 3000;
3
+ export const CHARS_PER_TOKEN = 4;
4
+ const required = (doc) => doc.fields.filter((f) => f.required).map((f) => f.name);
5
+ const indexLine = (doc) => `${doc.type} | ${doc.layer} | ${doc.purpose} | ${required(doc).join(",") || "-"}`;
6
+ /** One line per scene: `type | layer | purpose | required fields`. */
7
+ export const formatIndex = (docs) => [
8
+ "type | layer | purpose | required",
9
+ ...docs.map(indexLine),
10
+ "Detail: `mk catalog <type>`. Props are strict; unlisted keys are rejected.",
11
+ "",
12
+ ].join("\n");
13
+ const fieldLine = (field, width) => {
14
+ const mark = field.required ? "*" : " ";
15
+ const fallback = "default" in field ? ` = ${JSON.stringify(field.default)}` : "";
16
+ return ` ${field.name.padEnd(width)}${mark} ${field.type}${fallback}`;
17
+ };
18
+ /** Fields (`*` = required), types, defaults and a minimal example of one scene. */
19
+ export const formatDetail = (doc) => {
20
+ const width = Math.max(0, ...doc.fields.map((f) => f.name.length));
21
+ return [
22
+ `${doc.type} (${doc.layer}): ${doc.purpose}`,
23
+ ...(doc.fields.length === 0 ? [" (no props)"] : doc.fields.map((f) => fieldLine(f, width))),
24
+ ...(doc.fields.some((f) => f.type.includes("icon"))
25
+ ? ["icons: `mk icon search <query>` | `mk icon list [--tag group]` (names only)"]
26
+ : []),
27
+ `props: ${JSON.stringify(doc.example)}`,
28
+ "",
29
+ ].join("\n");
30
+ };
31
+ export const indexJson = (docs) => `${JSON.stringify(docs.map((doc) => ({
32
+ type: doc.type,
33
+ layer: doc.layer,
34
+ purpose: doc.purpose,
35
+ required: required(doc),
36
+ })))}\n`;
37
+ export const detailJson = (doc) => `${JSON.stringify(doc)}\n`;
38
+ /** The committed CATALOG.md: deterministic, generated, index first. */
39
+ export const formatCatalogMarkdown = (docs) => [
40
+ "# Catalog",
41
+ "",
42
+ "Generated by `mk catalog --write` from the scene registry; do not edit. Read the index, then `mk catalog <type>` for one scene. `*` marks a required field.",
43
+ "",
44
+ "## Index",
45
+ "",
46
+ "```",
47
+ formatIndex(docs).trimEnd(),
48
+ "```",
49
+ "",
50
+ "## Scenes",
51
+ "",
52
+ ...docs.flatMap((doc) => ["```", formatDetail(doc).trimEnd(), "```", ""]),
53
+ ].join("\n");
54
+ export const estimateTokens = (text) => Math.ceil(text.length / CHARS_PER_TOKEN);
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { Command, Io } from "./types.ts";
2
+ /** Dispatches argv (without node/script) to a command; resolves to the exit code. */
3
+ export declare function runCli(argv: readonly string[], io: Io, commands?: readonly Command[]): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,35 @@
1
+ import { COMMANDS } from "./commands.js";
2
+ import { helpSkillFile } from "./doctor/skills.js";
3
+ const SKILLS_NOTE = "(brief -> storyboard -> preview -> render protocol)";
4
+ function helpText(commands) {
5
+ const width = Math.max(0, ...commands.map((c) => c.name.length));
6
+ const rows = commands.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`);
7
+ return [
8
+ "mk <command> [flags]",
9
+ "",
10
+ "Commands:",
11
+ ...rows,
12
+ "",
13
+ "One command: mk <command> --help",
14
+ "",
15
+ `skills: ${helpSkillFile()} ${SKILLS_NOTE}`,
16
+ "",
17
+ ].join("\n");
18
+ }
19
+ function isHelp(arg) {
20
+ return arg === undefined || arg === "--help" || arg === "-h";
21
+ }
22
+ /** Dispatches argv (without node/script) to a command; resolves to the exit code. */
23
+ export async function runCli(argv, io, commands = COMMANDS) {
24
+ const [name, ...rest] = argv;
25
+ if (isHelp(name)) {
26
+ io.stdout(helpText(commands));
27
+ return 0;
28
+ }
29
+ const command = commands.find((c) => c.name === name);
30
+ if (!command) {
31
+ io.stderr(`mk: unknown command "${name}". Run \`mk --help\`.\n`);
32
+ return 2;
33
+ }
34
+ return command.run(rest, io);
35
+ }
@@ -0,0 +1,3 @@
1
+ import type { Command } from "./types.ts";
2
+ /** Command registry: a new command is one more entry here. */
3
+ export declare const COMMANDS: readonly Command[];
@@ -0,0 +1,59 @@
1
+ import { analyzeCommand } from "./analyze/command.js";
2
+ import { approveCommand } from "./approval/command.js";
3
+ import { assetsCommand } from "./assets/command.js";
4
+ import { captionsCommand } from "./captions/command.js";
5
+ import { catalogCommand } from "./catalog/command.js";
6
+ import { compareCommand } from "./compare/command.js";
7
+ import { contactCommand } from "./contact/command.js";
8
+ import { doctorCommand } from "./doctor/command.js";
9
+ import { fitCommand } from "./fit/command.js";
10
+ import { generate3dCommand } from "./generate3d/command.js";
11
+ import { iconCommand } from "./icon/command.js";
12
+ import { optimizeModelCommand } from "./optimize/command.js";
13
+ import { patchCommand } from "./patch/command.js";
14
+ import { pluginCommand } from "./plugin/install.js";
15
+ import { previewCommand } from "./preview/command.js";
16
+ import { publishCommand } from "./publish/command.js";
17
+ import { qcCommand } from "./qc.js";
18
+ import { sceneCommand } from "./scene/command.js";
19
+ import { stateCommand } from "./state/command.js";
20
+ import { templateCommand } from "./template/command.js";
21
+ import { newCommand } from "./template/new.js";
22
+ import { verifyCommand } from "./verify/command.js";
23
+ import { voiceCommand } from "./voice/command.js";
24
+ import { buildCommand } from "./workflow/build.js";
25
+ import { draftCommand } from "./workflow/draft.js";
26
+ import { renderCommand } from "./workflow/render.js";
27
+ import { stillCommand } from "./workflow/still.js";
28
+ import { validateCommand } from "./workflow/validate.js";
29
+ /** Command registry: a new command is one more entry here. */
30
+ export const COMMANDS = [
31
+ doctorCommand,
32
+ validateCommand,
33
+ buildCommand,
34
+ stillCommand,
35
+ compareCommand,
36
+ draftCommand,
37
+ renderCommand,
38
+ verifyCommand,
39
+ stateCommand,
40
+ analyzeCommand,
41
+ qcCommand,
42
+ captionsCommand,
43
+ catalogCommand,
44
+ contactCommand,
45
+ templateCommand,
46
+ newCommand,
47
+ previewCommand,
48
+ publishCommand,
49
+ approveCommand,
50
+ patchCommand,
51
+ sceneCommand,
52
+ fitCommand,
53
+ iconCommand,
54
+ assetsCommand,
55
+ pluginCommand,
56
+ voiceCommand,
57
+ optimizeModelCommand,
58
+ generate3dCommand,
59
+ ];
@@ -0,0 +1,4 @@
1
+ import type { Command } from "../types.ts";
2
+ /** Wide enough to judge composition: 2-4 candidates give a 1280-2560 px sheet. */
3
+ export declare const COMPARE_TILE_WIDTH = 640;
4
+ export declare const compareCommand: Command;
@@ -0,0 +1,144 @@
1
+ import { basename, dirname, extname, join, resolve } from "node:path";
2
+ import { parseFlags, textFlag, usageError } from "../args.js";
3
+ import { EXIT } from "../exit-codes.js";
4
+ import { clearPngs, contactSheet, listPngs } from "../preview/pack.js";
5
+ import { runInProject } from "../tooling/session.js";
6
+ import { PROJECT_FLAGS, prepareProject } from "../workflow/project.js";
7
+ import { STILLS_DIR, stillSnapshotArgs } from "../workflow/still.js";
8
+ const NAME = "compare";
9
+ const USAGE = `mk compare <a.json> <b.json> [<c.json> [<d.json>]] --at <seconds> [--out <dir>] [--assets <dir>] [--ledger <assets.json>] [--json]
10
+
11
+ Build two to four storyboards and capture one still of each at the same second
12
+ (hyperframes snapshot --at <seconds> --no-end), then tile them into one PNG
13
+ in the order given, so candidates can be compared by eye instead of by text.
14
+ Default <out>: <first storyboard dir>/out/compare. Each storyboard builds into
15
+ <out>/<storyboard name>; the sheet is <out>/compare-<at>.png (one output line).
16
+ --json: one compact line {at, png, items: [{storyboard, still, sha256}]}.
17
+ `;
18
+ const FLAGS = {
19
+ ...PROJECT_FLAGS,
20
+ at: { type: "string" },
21
+ json: { type: "boolean" },
22
+ };
23
+ const MIN_BOARDS = 2;
24
+ const MAX_BOARDS = 4;
25
+ /** One timestamp (no list, unlike `mk still --at`). */
26
+ const ONE_TIME = /^\d+(\.\d+)?$/;
27
+ /** Same per-scene build cache location `parseBuildArgs` uses. */
28
+ const CACHE_DIR = ".mk-cache";
29
+ const LIST_FILE = "compare-stills.txt";
30
+ /** Wide enough to judge composition: 2-4 candidates give a 1280-2560 px sheet. */
31
+ export const COMPARE_TILE_WIDTH = 640;
32
+ /** A copy of `io` whose output is captured, so wrapped builds stay quiet on stdout. */
33
+ const captured = (io) => {
34
+ const err = [];
35
+ return { io: { ...io, stdout: () => { }, stderr: (text) => err.push(text) }, err };
36
+ };
37
+ const stemOf = (storyboard) => basename(storyboard, extname(storyboard));
38
+ /** The request `parseBuildArgs` (workflow/project.ts) builds for one storyboard, with compare's out dir. */
39
+ function requestFor(storyboard, outDir, flags) {
40
+ const assets = textFlag(flags, "assets");
41
+ const ledger = textFlag(flags, "ledger");
42
+ const shared = textFlag(flags, "shared-cache-dir");
43
+ return {
44
+ storyboard,
45
+ outDir,
46
+ ...(assets === undefined ? {} : { assetsDir: resolve(assets) }),
47
+ ...(ledger === undefined ? {} : { ledger }),
48
+ ...(shared === undefined ? {} : { sharedCacheDir: resolve(shared) }),
49
+ incremental: flags.incremental === true,
50
+ cleanCache: flags["clean-cache"] === true,
51
+ cacheDir: join(dirname(resolve(storyboard)), CACHE_DIR),
52
+ };
53
+ }
54
+ function readOptions(args, io) {
55
+ const parsed = parseFlags(NAME, USAGE, args, FLAGS, io);
56
+ if (parsed.kind === "exit")
57
+ return parsed.code;
58
+ const [first] = parsed.positionals;
59
+ if (first === undefined || parsed.positionals.length < MIN_BOARDS) {
60
+ return usageError(NAME, USAGE, "need at least two storyboards to compare", io).code;
61
+ }
62
+ if (parsed.positionals.length > MAX_BOARDS) {
63
+ return usageError(NAME, USAGE, "at most four storyboards", io).code;
64
+ }
65
+ const stems = parsed.positionals.map(stemOf);
66
+ const repeated = stems.find((stem, index) => stems.indexOf(stem) !== index);
67
+ if (repeated !== undefined) {
68
+ return usageError(NAME, USAGE, `two storyboards share the name "${repeated}"; each builds into <out>/<name>, so rename one`, io).code;
69
+ }
70
+ if (parsed.flags.incremental === true && parsed.flags["no-incremental"] === true) {
71
+ return usageError(NAME, USAGE, "--incremental and --no-incremental conflict", io).code;
72
+ }
73
+ const at = textFlag(parsed.flags, "at");
74
+ if (at === undefined)
75
+ return usageError(NAME, USAGE, "missing --at <seconds>", io).code;
76
+ if (!ONE_TIME.test(at)) {
77
+ return usageError(NAME, USAGE, "--at needs one timestamp in seconds like 3 or 1.5", io).code;
78
+ }
79
+ return {
80
+ boards: parsed.positionals,
81
+ at,
82
+ out: resolve(textFlag(parsed.flags, "out") ?? join(dirname(resolve(first)), "out", "compare")),
83
+ json: parsed.flags.json === true,
84
+ flags: parsed.flags,
85
+ };
86
+ }
87
+ /** Builds one storyboard and captures its single still; a number is the exit code on failure. */
88
+ async function stillOf(io, request, at) {
89
+ const name = basename(request.storyboard);
90
+ const build = captured(io);
91
+ const prepared = await prepareProject(request, build.io);
92
+ if (!prepared.ok) {
93
+ io.stderr(`mk ${NAME}: ${name}: build failed\n${build.err.join("")}`);
94
+ return EXIT.ERROR;
95
+ }
96
+ io.stderr(build.err.join(""));
97
+ const dir = join(prepared.outDir, STILLS_DIR);
98
+ await clearPngs(io.host, dir);
99
+ const snap = captured(io);
100
+ const code = await runInProject(snap.io, prepared.outDir, async (session) => {
101
+ await session.hf(stillSnapshotArgs(at));
102
+ });
103
+ if (code !== EXIT.OK) {
104
+ io.stderr(`mk ${NAME}: ${name}: snapshot failed\n${snap.err.join("")}`);
105
+ return EXIT.ERROR;
106
+ }
107
+ const stills = await listPngs(io.host, dir);
108
+ const still = stills[0];
109
+ if (stills.length !== 1 || still === undefined) {
110
+ io.stderr(`mk ${NAME}: ${name}: expected one still at ${at}s, hyperframes wrote ${stills.length}\n`);
111
+ return EXIT.ERROR;
112
+ }
113
+ const sha256 = await io.host.sha256File(still);
114
+ if (sha256 === undefined) {
115
+ io.stderr(`mk ${NAME}: ${name}: cannot read the still ${still}\n`);
116
+ return EXIT.ERROR;
117
+ }
118
+ return { storyboard: name, still, sha256 };
119
+ }
120
+ export const compareCommand = {
121
+ name: NAME,
122
+ summary: "build 2-4 storyboards, one still each at the same --at second, tiled into one PNG",
123
+ async run(args, io) {
124
+ const options = readOptions(args, io);
125
+ if (typeof options === "number")
126
+ return options;
127
+ const items = [];
128
+ for (const storyboard of options.boards) {
129
+ const outDir = join(options.out, stemOf(storyboard));
130
+ const item = await stillOf(io, requestFor(storyboard, outDir, options.flags), options.at);
131
+ if (typeof item === "number")
132
+ return item;
133
+ items.push(item);
134
+ }
135
+ const png = join(options.out, `compare-${options.at}.png`);
136
+ const tiled = await contactSheet(io, items.map((item) => item.still), join(options.out, LIST_FILE), png, COMPARE_TILE_WIDTH);
137
+ if (tiled !== EXIT.OK) {
138
+ io.stderr(`mk ${NAME}: tiling the stills failed\n`);
139
+ return EXIT.ERROR;
140
+ }
141
+ io.stdout(options.json ? `${JSON.stringify({ at: Number(options.at), png, items })}\n` : `${png}\n`);
142
+ return EXIT.OK;
143
+ },
144
+ };
@@ -0,0 +1,24 @@
1
+ export declare const DEFAULT_COLS = 4;
2
+ export declare const DEFAULT_COUNT = 12;
3
+ export declare const MAX_COUNT = 64;
4
+ /** Width of one tile in px; height follows the aspect ratio (even). */
5
+ export declare const TILE_WIDTH = 320;
6
+ export interface ContactPlan {
7
+ file: string;
8
+ out: string;
9
+ /** Seconds, > 0. */
10
+ duration: number;
11
+ count: number;
12
+ cols: number;
13
+ }
14
+ export declare const rowsFor: (count: number, cols: number) => number;
15
+ /**
16
+ * ffmpeg argv (no shell): `fps=count/duration` keeps frame i at time i*duration/count, so the
17
+ * frames are evenly spaced; then scale and tile them into one image.
18
+ */
19
+ export declare function contactArgs(plan: ContactPlan): string[];
20
+ /**
21
+ * ffmpeg argv (no shell) tiling the images named by an ffconcat list (`file '<path>'` + `duration 1`
22
+ * per image) into one contact sheet; same tile layout as `contactArgs`, at `width` px per tile.
23
+ */
24
+ export declare function contactImagesArgs(list: string, out: string, count: number, cols: number, width?: number): string[];
@@ -0,0 +1,42 @@
1
+ export const DEFAULT_COLS = 4;
2
+ export const DEFAULT_COUNT = 12;
3
+ export const MAX_COUNT = 64;
4
+ /** Width of one tile in px; height follows the aspect ratio (even). */
5
+ export const TILE_WIDTH = 320;
6
+ export const rowsFor = (count, cols) => Math.ceil(count / cols);
7
+ /**
8
+ * ffmpeg argv (no shell): `fps=count/duration` keeps frame i at time i*duration/count, so the
9
+ * frames are evenly spaced; then scale and tile them into one image.
10
+ */
11
+ export function contactArgs(plan) {
12
+ const { file, out, duration, count, cols } = plan;
13
+ const filter = [
14
+ `fps=${count}/${duration.toFixed(3)}`,
15
+ `scale=${TILE_WIDTH}:-2:flags=lanczos`,
16
+ `tile=${cols}x${rowsFor(count, cols)}`,
17
+ ].join(",");
18
+ return ["-y", "-v", "error", "-i", file, "-vf", filter, "-frames:v", "1", out];
19
+ }
20
+ /**
21
+ * ffmpeg argv (no shell) tiling the images named by an ffconcat list (`file '<path>'` + `duration 1`
22
+ * per image) into one contact sheet; same tile layout as `contactArgs`, at `width` px per tile.
23
+ */
24
+ export function contactImagesArgs(list, out, count, cols, width = TILE_WIDTH) {
25
+ const filter = [`scale=${width}:-2:flags=lanczos`, `tile=${cols}x${rowsFor(count, cols)}`];
26
+ return [
27
+ "-y",
28
+ "-v",
29
+ "error",
30
+ "-f",
31
+ "concat",
32
+ "-safe",
33
+ "0",
34
+ "-i",
35
+ list,
36
+ "-vf",
37
+ filter.join(","),
38
+ "-frames:v",
39
+ "1",
40
+ out,
41
+ ];
42
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "../types.ts";
2
+ export declare const contactCommand: Command;
@@ -0,0 +1,79 @@
1
+ import { basename, dirname, join } from "node:path";
2
+ import { parseFlags, requirePositional, textFlag, usageError } from "../args.js";
3
+ import { EXIT } from "../exit-codes.js";
4
+ import { probeMedia } from "../verify/probe.js";
5
+ import { contactArgs, DEFAULT_COLS, DEFAULT_COUNT, MAX_COUNT, rowsFor } from "./argv.js";
6
+ const USAGE = `mk contact <file.mp4> [--cols N] [--count N] [--out file.png]
7
+
8
+ Tile evenly spaced frames of a video into one PNG contact sheet (zero tokens: look at the image).
9
+ --cols N tiles per row, 1-${MAX_COUNT} (default ${DEFAULT_COLS})
10
+ --count N frames to sample, 1-${MAX_COUNT} (default ${DEFAULT_COUNT})
11
+ --out f output PNG (default <file>-contact.png beside the video)
12
+ `;
13
+ const FLAGS = {
14
+ cols: { type: "string" },
15
+ count: { type: "string" },
16
+ out: { type: "string" },
17
+ };
18
+ const RENDER_TIMEOUT_MS = 120_000;
19
+ const intFlag = (flags, name, fallback) => {
20
+ const raw = textFlag(flags, name);
21
+ if (raw === undefined)
22
+ return fallback;
23
+ const value = Number(raw);
24
+ return /^\d+$/.test(raw) && value >= 1 && value <= MAX_COUNT ? value : undefined;
25
+ };
26
+ const defaultOut = (file) => join(dirname(file), `${basename(file).replace(/\.[^.]+$/, "")}-contact.png`);
27
+ /** Duration in seconds from an ffprobe JSON document, or undefined. */
28
+ const durationOf = (probe) => {
29
+ const raw = probe.format?.duration;
30
+ const seconds = Number(raw);
31
+ return Number.isFinite(seconds) && seconds > 0 ? seconds : undefined;
32
+ };
33
+ async function sheet(io, file, out, count, cols) {
34
+ const probed = await probeMedia(io, file);
35
+ if (!probed.ok && probed.kind === "missing-tool") {
36
+ io.stderr("mk: ffprobe not found; install ffmpeg and put it on PATH\n");
37
+ return EXIT.MISSING_TOOL;
38
+ }
39
+ const duration = probed.ok ? durationOf(probed.probe) : undefined;
40
+ if (duration === undefined) {
41
+ io.stderr(`mk contact: cannot read a duration from ${file}\n`);
42
+ return EXIT.ERROR;
43
+ }
44
+ const args = contactArgs({ file, out, duration, count, cols });
45
+ const result = await io.run("ffmpeg", args, { timeoutMs: RENDER_TIMEOUT_MS });
46
+ if (result.code === EXIT.MISSING_TOOL) {
47
+ io.stderr("mk: ffmpeg not found; install ffmpeg and put it on PATH\n");
48
+ return EXIT.MISSING_TOOL;
49
+ }
50
+ if (result.code !== 0) {
51
+ io.stderr(`mk contact: ffmpeg failed: ${result.stderr.trim().split(/\r?\n/).pop()}\n`);
52
+ return EXIT.ERROR;
53
+ }
54
+ io.stdout(`${out} ${cols}x${rowsFor(count, cols)} tiles, ${count} frames\n`);
55
+ return EXIT.OK;
56
+ }
57
+ export const contactCommand = {
58
+ name: "contact",
59
+ summary: "tile evenly spaced frames of an mp4 into one PNG contact sheet",
60
+ async run(args, io) {
61
+ const parsed = parseFlags("contact", USAGE, args, FLAGS, io);
62
+ if (parsed.kind === "exit")
63
+ return parsed.code;
64
+ const file = requirePositional("contact", USAGE, parsed.positionals, "<file.mp4>", io);
65
+ if (typeof file !== "string")
66
+ return file.code;
67
+ const cols = intFlag(parsed.flags, "cols", DEFAULT_COLS);
68
+ const count = intFlag(parsed.flags, "count", DEFAULT_COUNT);
69
+ if (cols === undefined || count === undefined) {
70
+ return usageError("contact", USAGE, `--cols and --count must be integers 1-${MAX_COUNT}`, io)
71
+ .code;
72
+ }
73
+ if (!(await io.host.exists(file))) {
74
+ io.stderr(`mk contact: cannot read ${file}; check the path.\n`);
75
+ return EXIT.USAGE;
76
+ }
77
+ return sheet(io, file, textFlag(parsed.flags, "out") ?? defaultOut(file), count, cols);
78
+ },
79
+ };
@@ -0,0 +1,25 @@
1
+ import type { Check } from "./types.ts";
2
+ /** DL-01b voice bar: Kokoro is the local primary; failures are manual-hint-only. */
3
+ export declare const VOICE_LOCAL_PRIMARY = "Kokoro local primary";
4
+ export declare const VOICE_WAV_FALLBACK = "pre-rendered WAV fallback in pack";
5
+ export declare const VOICE_MANUAL_SETUP = "manual setup only; no auto-install";
6
+ export declare const VOICE_NO_PAID_BACKEND = "no paid backend";
7
+ export declare const VOICE_ALIGNER = "aligner";
8
+ export declare const checkNode: Check;
9
+ export declare const checkPnpm: Check;
10
+ export declare const checkFfmpeg: Check;
11
+ export declare const checkFfprobe: Check;
12
+ export declare const checkHyperframes: Check;
13
+ export declare const checkChrome: Check;
14
+ export declare const checkGpu: Check;
15
+ export declare const checkPython: Check;
16
+ export declare const checkPythonVenv: Check;
17
+ export declare const checkEspeak: Check;
18
+ export declare const checkKokoro: Check;
19
+ export declare const checkFasterWhisper: Check;
20
+ export declare const checkFonts: Check;
21
+ export declare const checkIcons: Check;
22
+ export declare const checkTemplates: Check;
23
+ export declare const checkBin: Check;
24
+ export declare const checkCatalog: Check;
25
+ export declare const CHECKS: readonly Check[];