@prestyj/cli 5.28.1 → 5.29.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 (316) hide show
  1. package/assets/motion/bin/contact-sheet.mjs +9 -3
  2. package/assets/motion/bin/cues.mjs +337 -0
  3. package/assets/motion/bin/library.mjs +53 -10
  4. package/assets/motion/bin/motion-blur.mjs +943 -0
  5. package/assets/motion/bin/motion-check.mjs +354 -3
  6. package/assets/motion/bin/music-fit.mjs +436 -0
  7. package/assets/motion/bin/pdf-extract.mjs +22 -10
  8. package/assets/motion/bin/reference-study.mjs +346 -0
  9. package/assets/motion/bin/score-synth.mjs +1052 -93
  10. package/assets/motion/library/README.md +50 -14
  11. package/assets/motion/library/kit/moves.js +1981 -0
  12. package/assets/motion/library/library.json +232 -0
  13. package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
  14. package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
  15. package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
  16. package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
  17. package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
  18. package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
  19. package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
  20. package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
  21. package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
  22. package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
  23. package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
  24. package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
  25. package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
  26. package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
  27. package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
  28. package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
  29. package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
  30. package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
  31. package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
  32. package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
  33. package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
  34. package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
  35. package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
  36. package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
  37. package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
  38. package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
  39. package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
  40. package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
  41. package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
  42. package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
  43. package/assets/motion/library/sheets/diagram.jpg +0 -0
  44. package/assets/motion/library/sheets/frame.jpg +0 -0
  45. package/assets/motion/library/sheets/transition.jpg +0 -0
  46. package/assets/motion/library/sheets/ui.jpg +0 -0
  47. package/assets/motion/references/build-sheet.md +206 -0
  48. package/assets/motion/references/runtime/determinism-rules.md +1 -1
  49. package/assets/motion/references/runtime/gsap-easing-and-stagger.md +29 -29
  50. package/assets/motion/references/runtime/inputs-and-assets.md +7 -12
  51. package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
  52. package/assets/motion/references/runtime/minimal-composition.md +1 -1
  53. package/assets/motion/references/runtime/preview-render.md +3 -3
  54. package/assets/motion/skills/app-walkthrough/SKILL.md +66 -0
  55. package/assets/motion/skills/before-after/SKILL.md +53 -0
  56. package/assets/motion/skills/brand-kit/SKILL.md +3 -3
  57. package/assets/motion/skills/dev-tool-video/SKILL.md +57 -0
  58. package/assets/motion/skills/launch-video/SKILL.md +62 -0
  59. package/assets/motion/skills/match-reference/SKILL.md +58 -0
  60. package/assets/motion/skills/motion/SKILL.md +78 -85
  61. package/assets/motion/skills/source-ingest/SKILL.md +17 -6
  62. package/assets/motion/skills/website-video/SKILL.md +59 -0
  63. package/assets/skills/bulletproof/SKILL.md +36 -11
  64. package/assets/skills/bulletproof/references/agent-surface.md +19 -9
  65. package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
  66. package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
  67. package/assets/skills/bulletproof/references/provenance.md +26 -1
  68. package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
  69. package/assets/skills/bulletproof/references/supply-chain.md +21 -17
  70. package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
  71. package/assets/skills/bulletproof/references/verification.md +2 -0
  72. package/assets/skills/clarify/SKILL.md +25 -16
  73. package/assets/skills/code-review/SKILL.md +71 -13
  74. package/assets/skills/code-review/references/agent-diffs.md +27 -0
  75. package/assets/skills/code-review/references/tests.md +19 -0
  76. package/assets/skills/compliance-guard/SKILL.md +20 -5
  77. package/assets/skills/compliance-guard/references/artifacts.md +1 -1
  78. package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
  79. package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
  80. package/assets/skills/compliance-guard/references/provenance.md +41 -2
  81. package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
  82. package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
  83. package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
  84. package/assets/skills/compliance-guard/references/us.md +27 -21
  85. package/assets/skills/durable/SKILL.md +87 -79
  86. package/assets/skills/durable/references/agent-db-safety.md +69 -0
  87. package/assets/skills/durable/references/backups-and-runtime.md +19 -12
  88. package/assets/skills/durable/references/migrations-and-schema.md +13 -6
  89. package/assets/skills/evidence-led-ui/SKILL.md +69 -127
  90. package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
  91. package/assets/skills/evidence-led-ui/references/direction.md +124 -0
  92. package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
  93. package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
  94. package/assets/skills/lean/SKILL.md +90 -71
  95. package/assets/skills/lean/references/memory-and-processes.md +3 -2
  96. package/assets/skills/lean/references/playbooks.md +37 -12
  97. package/assets/skills/refactoring/SKILL.md +24 -3
  98. package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
  99. package/assets/skills/refactoring/references/legacy.md +21 -0
  100. package/assets/skills/root-cause/SKILL.md +20 -10
  101. package/assets/skills/shared-language/SKILL.md +16 -14
  102. package/assets/skills/tdd/SKILL.md +27 -15
  103. package/dist/app-sidecar.js +203 -47
  104. package/dist/app-sidecar.js.map +1 -1
  105. package/dist/cli.js +17 -26
  106. package/dist/cli.js.map +1 -1
  107. package/dist/core/acceptance-checks.d.ts +48 -0
  108. package/dist/core/acceptance-checks.js +144 -0
  109. package/dist/core/acceptance-checks.js.map +1 -0
  110. package/dist/core/agent-session.d.ts +106 -72
  111. package/dist/core/agent-session.js +539 -399
  112. package/dist/core/agent-session.js.map +1 -1
  113. package/dist/core/agents.d.ts +6 -5
  114. package/dist/core/agents.js.map +1 -1
  115. package/dist/core/ask-user.d.ts +90 -8
  116. package/dist/core/ask-user.js +124 -13
  117. package/dist/core/ask-user.js.map +1 -1
  118. package/dist/core/bundled-agents.js +1 -3
  119. package/dist/core/bundled-agents.js.map +1 -1
  120. package/dist/core/cache-diagnostics.d.ts +68 -0
  121. package/dist/core/cache-diagnostics.js +196 -0
  122. package/dist/core/cache-diagnostics.js.map +1 -0
  123. package/dist/core/cache-expiry.d.ts +87 -0
  124. package/dist/core/cache-expiry.js +111 -0
  125. package/dist/core/cache-expiry.js.map +1 -0
  126. package/dist/core/compaction/compactor.js +78 -48
  127. package/dist/core/compaction/compactor.js.map +1 -1
  128. package/dist/core/compaction/plan-step-policy.d.ts +46 -0
  129. package/dist/core/compaction/plan-step-policy.js +57 -0
  130. package/dist/core/compaction/plan-step-policy.js.map +1 -0
  131. package/dist/core/destructive-git-guard.d.ts +90 -0
  132. package/dist/core/destructive-git-guard.js +871 -0
  133. package/dist/core/destructive-git-guard.js.map +1 -0
  134. package/dist/core/event-bus.d.ts +3 -0
  135. package/dist/core/event-bus.js +5 -0
  136. package/dist/core/event-bus.js.map +1 -1
  137. package/dist/core/injection-detect.d.ts +38 -0
  138. package/dist/core/injection-detect.js +232 -0
  139. package/dist/core/injection-detect.js.map +1 -0
  140. package/dist/core/keep-awake.d.ts +88 -0
  141. package/dist/core/keep-awake.js +251 -0
  142. package/dist/core/keep-awake.js.map +1 -0
  143. package/dist/core/mcp/client.d.ts +72 -0
  144. package/dist/core/mcp/client.js +264 -41
  145. package/dist/core/mcp/client.js.map +1 -1
  146. package/dist/core/mcp/content.js +6 -2
  147. package/dist/core/mcp/content.js.map +1 -1
  148. package/dist/core/mcp/store.d.ts +6 -1
  149. package/dist/core/mcp/store.js +12 -1
  150. package/dist/core/mcp/store.js.map +1 -1
  151. package/dist/core/mcp/types.d.ts +18 -0
  152. package/dist/core/model-unavailable.d.ts +14 -0
  153. package/dist/core/model-unavailable.js +23 -0
  154. package/dist/core/model-unavailable.js.map +1 -0
  155. package/dist/core/node-debugger.d.ts +148 -0
  156. package/dist/core/node-debugger.js +642 -0
  157. package/dist/core/node-debugger.js.map +1 -0
  158. package/dist/core/package-threats.d.ts +18 -0
  159. package/dist/core/package-threats.js +168 -0
  160. package/dist/core/package-threats.js.map +1 -0
  161. package/dist/core/persistent-shell.d.ts +58 -6
  162. package/dist/core/persistent-shell.js +331 -49
  163. package/dist/core/persistent-shell.js.map +1 -1
  164. package/dist/core/process-manager.d.ts +14 -0
  165. package/dist/core/process-manager.js +61 -0
  166. package/dist/core/process-manager.js.map +1 -1
  167. package/dist/core/progress/git-xp.js +8 -14
  168. package/dist/core/progress/git-xp.js.map +1 -1
  169. package/dist/core/session-history.d.ts +12 -0
  170. package/dist/core/session-history.js +27 -0
  171. package/dist/core/session-history.js.map +1 -1
  172. package/dist/core/session-manager.d.ts +13 -1
  173. package/dist/core/session-manager.js +38 -18
  174. package/dist/core/session-manager.js.map +1 -1
  175. package/dist/core/session-summary-index.d.ts +37 -0
  176. package/dist/core/session-summary-index.js +172 -0
  177. package/dist/core/session-summary-index.js.map +1 -0
  178. package/dist/core/settings-manager.d.ts +2 -0
  179. package/dist/core/settings-manager.js +10 -0
  180. package/dist/core/settings-manager.js.map +1 -1
  181. package/dist/core/shell-threats-popular-packages.d.ts +11 -0
  182. package/dist/core/shell-threats-popular-packages.js +675 -0
  183. package/dist/core/shell-threats-popular-packages.js.map +1 -0
  184. package/dist/core/shell-threats.d.ts +8 -0
  185. package/dist/core/shell-threats.js +186 -0
  186. package/dist/core/shell-threats.js.map +1 -0
  187. package/dist/core/skills.js +3 -1
  188. package/dist/core/skills.js.map +1 -1
  189. package/dist/core/stream-rules.d.ts +30 -0
  190. package/dist/core/stream-rules.js +151 -0
  191. package/dist/core/stream-rules.js.map +1 -0
  192. package/dist/core/subagent-manager.d.ts +20 -5
  193. package/dist/core/subagent-manager.js +22 -8
  194. package/dist/core/subagent-manager.js.map +1 -1
  195. package/dist/core/subagent-receipt.d.ts +54 -0
  196. package/dist/core/subagent-receipt.js +276 -0
  197. package/dist/core/subagent-receipt.js.map +1 -0
  198. package/dist/core/subagent-turn-record.d.ts +2 -0
  199. package/dist/core/subagent-turn-record.js.map +1 -1
  200. package/dist/core/test-impact.d.ts +73 -0
  201. package/dist/core/test-impact.js +467 -0
  202. package/dist/core/test-impact.js.map +1 -0
  203. package/dist/core/thinking-level.d.ts +1 -1
  204. package/dist/core/thinking-level.js +1 -1
  205. package/dist/core/thinking-level.js.map +1 -1
  206. package/dist/core/verification-gate.d.ts +2 -0
  207. package/dist/core/verification-gate.js +4 -0
  208. package/dist/core/verification-gate.js.map +1 -1
  209. package/dist/core/verification-snapshot.js +3 -5
  210. package/dist/core/verification-snapshot.js.map +1 -1
  211. package/dist/core/workspace-guard.d.ts +18 -7
  212. package/dist/core/workspace-guard.js +227 -60
  213. package/dist/core/workspace-guard.js.map +1 -1
  214. package/dist/interactive.js +2 -1
  215. package/dist/interactive.js.map +1 -1
  216. package/dist/modes/subagent-worker-mode.js +34 -6
  217. package/dist/modes/subagent-worker-mode.js.map +1 -1
  218. package/dist/motion-agent/motion-agent.d.ts +6 -2
  219. package/dist/motion-agent/motion-agent.js +5 -7
  220. package/dist/motion-agent/motion-agent.js.map +1 -1
  221. package/dist/motion-agent/motion-prompt.d.ts +1 -1
  222. package/dist/motion-agent/motion-prompt.js +14 -19
  223. package/dist/motion-agent/motion-prompt.js.map +1 -1
  224. package/dist/motion-agent/motion-review.d.ts +10 -3
  225. package/dist/motion-agent/motion-review.js +15 -7
  226. package/dist/motion-agent/motion-review.js.map +1 -1
  227. package/dist/motion-agent/motion-studio-context.js +1 -1
  228. package/dist/motion-agent/motion-studio-context.js.map +1 -1
  229. package/dist/system-prompt.js +3 -1
  230. package/dist/system-prompt.js.map +1 -1
  231. package/dist/test-support/keep-alive.d.ts +14 -0
  232. package/dist/test-support/keep-alive.js +17 -0
  233. package/dist/test-support/keep-alive.js.map +1 -0
  234. package/dist/tools/ask-user.js +3 -3
  235. package/dist/tools/ask-user.js.map +1 -1
  236. package/dist/tools/bash-read-evidence.d.ts +10 -0
  237. package/dist/tools/bash-read-evidence.js +133 -0
  238. package/dist/tools/bash-read-evidence.js.map +1 -0
  239. package/dist/tools/bash.d.ts +10 -1
  240. package/dist/tools/bash.js +115 -7
  241. package/dist/tools/bash.js.map +1 -1
  242. package/dist/tools/debug.d.ts +54 -0
  243. package/dist/tools/debug.js +233 -0
  244. package/dist/tools/debug.js.map +1 -0
  245. package/dist/tools/edit.js +12 -4
  246. package/dist/tools/edit.js.map +1 -1
  247. package/dist/tools/goals.d.ts +1 -1
  248. package/dist/tools/index.d.ts +19 -2
  249. package/dist/tools/index.js +47 -7
  250. package/dist/tools/index.js.map +1 -1
  251. package/dist/tools/prompt-hints.js +2 -0
  252. package/dist/tools/prompt-hints.js.map +1 -1
  253. package/dist/tools/read-tracker.d.ts +5 -0
  254. package/dist/tools/read-tracker.js +19 -9
  255. package/dist/tools/read-tracker.js.map +1 -1
  256. package/dist/tools/read.js +3 -2
  257. package/dist/tools/read.js.map +1 -1
  258. package/dist/tools/skill.js +5 -0
  259. package/dist/tools/skill.js.map +1 -1
  260. package/dist/tools/subagent-control.js +44 -8
  261. package/dist/tools/subagent-control.js.map +1 -1
  262. package/dist/tools/subagent-shared.d.ts +29 -8
  263. package/dist/tools/subagent-shared.js +45 -14
  264. package/dist/tools/subagent-shared.js.map +1 -1
  265. package/dist/tools/subagent.d.ts +8 -2
  266. package/dist/tools/subagent.js +28 -10
  267. package/dist/tools/subagent.js.map +1 -1
  268. package/dist/tools/task-output.js +3 -2
  269. package/dist/tools/task-output.js.map +1 -1
  270. package/dist/tools/task-send.d.ts +1 -1
  271. package/dist/tools/task-send.js +15 -1
  272. package/dist/tools/task-send.js.map +1 -1
  273. package/dist/tools/tool-tiers.d.ts +2 -2
  274. package/dist/tools/tool-tiers.js +3 -2
  275. package/dist/tools/tool-tiers.js.map +1 -1
  276. package/dist/tools/truncate.d.ts +21 -0
  277. package/dist/tools/truncate.js +187 -0
  278. package/dist/tools/truncate.js.map +1 -1
  279. package/dist/tools/ui-adopt.js +2 -0
  280. package/dist/tools/ui-adopt.js.map +1 -1
  281. package/dist/ui/App.d.ts +0 -4
  282. package/dist/ui/App.js +5 -28
  283. package/dist/ui/App.js.map +1 -1
  284. package/dist/ui/components/ActivityIndicator.js +1 -0
  285. package/dist/ui/components/ActivityIndicator.js.map +1 -1
  286. package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
  287. package/dist/ui/hooks/useAgentLoop.js +1 -119
  288. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  289. package/dist/ui/render.d.ts +0 -4
  290. package/dist/ui/render.js +0 -2
  291. package/dist/ui/render.js.map +1 -1
  292. package/dist/utils/git.d.ts +77 -0
  293. package/dist/utils/git.js +285 -21
  294. package/dist/utils/git.js.map +1 -1
  295. package/dist/utils/github-ci.js +2 -1
  296. package/dist/utils/github-ci.js.map +1 -1
  297. package/dist/utils/github.js +11 -9
  298. package/dist/utils/github.js.map +1 -1
  299. package/dist/utils/image.d.ts +14 -0
  300. package/dist/utils/image.js +16 -0
  301. package/dist/utils/image.js.map +1 -1
  302. package/dist/utils/process.d.ts +20 -0
  303. package/dist/utils/process.js +98 -0
  304. package/dist/utils/process.js.map +1 -1
  305. package/package.json +5 -5
  306. package/assets/motion/references/motion-language.md +0 -128
  307. package/assets/motion/skills/video-qa/SKILL.md +0 -89
  308. package/dist/core/ideal-review-subagent.d.ts +0 -56
  309. package/dist/core/ideal-review-subagent.js +0 -112
  310. package/dist/core/ideal-review-subagent.js.map +0 -1
  311. package/dist/core/ideal-review.d.ts +0 -82
  312. package/dist/core/ideal-review.js +0 -242
  313. package/dist/core/ideal-review.js.map +0 -1
  314. package/dist/motion-agent/motion-check-tool.d.ts +0 -35
  315. package/dist/motion-agent/motion-check-tool.js +0 -514
  316. package/dist/motion-agent/motion-check-tool.js.map +0 -1
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+ // EZ Motion reference study: measure a video the user likes so a new video can
3
+ // take its rules (pace, stillness, how scenes change), never its scenes.
4
+ //
5
+ // Usage: node reference-study.mjs <reference-video> [--out <folder>]
6
+ // Default out: ./reference-study. Writes report.json, report.md, two contact
7
+ // sheets (one frame per shot, and an even timeline) and the frames behind them.
8
+ // Prints a one-line JSON summary.
9
+ //
10
+ // Every frame is decoded once on motion-check's small grey flow grid, so a
11
+ // reference and a EZ render are measured the same way.
12
+ import { execFile } from "node:child_process";
13
+ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+ import { promisify } from "node:util";
17
+ import { mediaBinaries } from "./media-binaries.mjs";
18
+ import { measureFlow } from "./motion-check.mjs";
19
+
20
+ const run = promisify(execFile);
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+
23
+ /** A grid pixel changed visibly when its grey level moves by more than this... */
24
+ export const VISIBLE_LEVEL = 8;
25
+ /** ...and a frame step is still when at most this share of the grid changed visibly.
26
+ * Counting pixels, not averaging them, keeps a small moving thing (a cursor, a talking
27
+ * head in a corner) from reading as still. */
28
+ export const STILL_SHARE = 0.0002;
29
+ /** A still stretch at least this long is a rest. */
30
+ export const REST_SECONDS = 0.5;
31
+ /** Cuts this close together (seconds) belong to one burst... */
32
+ export const BURST_GAP = 0.6;
33
+ /** ...of at least this many cuts. */
34
+ export const BURST_CUTS = 3;
35
+ const MAX_SHOT_FRAMES = 24;
36
+ const TIMELINE_FRAMES = 16;
37
+
38
+ const round = (value, places = 3) => Math.round(value * 10 ** places) / 10 ** places;
39
+
40
+ function quantile(sorted, q) {
41
+ if (!sorted.length) return 0;
42
+ const at = (sorted.length - 1) * q;
43
+ const low = Math.floor(at);
44
+ const high = Math.ceil(at);
45
+ return sorted[low] + (sorted[high] - sorted[low]) * (at - low);
46
+ }
47
+
48
+ /**
49
+ * The study's figures from a flow measurement and two figures per frame step
50
+ * (index i is frame i-1 → i; index 0 is unused): `energy`, the mean grey change
51
+ * (0..1), and `changed`, the share of the grid that changed visibly.
52
+ * @param {{ duration: number; fps: number; cuts: number[]; fast: { start: number; end: number; peak: number }[] }} flow
53
+ * @param {{ energy: ArrayLike<number>; changed: ArrayLike<number> }} steps
54
+ */
55
+ export function studyFlow(flow, { energy, changed }) {
56
+ const { duration, fps } = flow;
57
+ if (!(duration > 0) || !(fps > 0)) throw new Error("Study needs a duration and frame rate");
58
+ const cuts = [...flow.cuts].filter((t) => t > 0 && t < duration).sort((a, b) => a - b);
59
+ if (energy.length !== changed.length) throw new Error("Study needs both figures for every step");
60
+ const steps = Math.max(0, energy.length - 1);
61
+
62
+ // Stillness, and the rests: still stretches long enough to read as a pause.
63
+ let still = 0;
64
+ const rests = [];
65
+ let runStart = -1;
66
+ const closeRun = (end) => {
67
+ if (runStart >= 0 && (end - runStart) / fps >= REST_SECONDS)
68
+ rests.push({ start: round((runStart - 1) / fps, 2), end: round((end - 1) / fps, 2) });
69
+ runStart = -1;
70
+ };
71
+ const moving = [];
72
+ for (let i = 1; i <= steps; i++) {
73
+ if (changed[i] <= STILL_SHARE) {
74
+ still++;
75
+ if (runStart < 0) runStart = i;
76
+ } else {
77
+ closeRun(i);
78
+ moving.push(energy[i]);
79
+ }
80
+ }
81
+ closeRun(steps + 1);
82
+
83
+ // Shots between hard cuts, and how even their lengths are.
84
+ const bounds = [0, ...cuts, duration];
85
+ const shots = [];
86
+ for (let i = 1; i < bounds.length; i++) shots.push(round(bounds[i] - bounds[i - 1], 2));
87
+ const sortedShots = [...shots].sort((a, b) => a - b);
88
+ const mean = shots.reduce((sum, s) => sum + s, 0) / shots.length;
89
+ const sd = Math.sqrt(shots.reduce((sum, s) => sum + (s - mean) ** 2, 0) / shots.length);
90
+
91
+ // Bursts: runs of cuts close together.
92
+ const bursts = [];
93
+ let group = [];
94
+ const closeGroup = () => {
95
+ if (group.length >= BURST_CUTS)
96
+ bursts.push({ start: group[0], end: group.at(-1), cuts: group.length });
97
+ group = [];
98
+ };
99
+ for (const cut of cuts) {
100
+ if (group.length && cut - group.at(-1) > BURST_GAP) closeGroup();
101
+ group.push(cut);
102
+ }
103
+ closeGroup();
104
+
105
+ const fastSeconds = flow.fast.reduce((sum, run) => sum + (run.end - run.start), 0);
106
+ moving.sort((a, b) => a - b);
107
+ return {
108
+ duration: round(duration, 2),
109
+ fps: round(fps, 2),
110
+ stillShare: steps ? round(still / steps) : 0,
111
+ rests,
112
+ longestRest: rests.reduce((best, r) => Math.max(best, round(r.end - r.start, 2)), 0),
113
+ cuts: cuts.map((t) => round(t, 2)),
114
+ cutsPerMinute: round((cuts.length / duration) * 60, 1),
115
+ bursts: bursts.map((b) => ({ start: round(b.start, 2), end: round(b.end, 2), cuts: b.cuts })),
116
+ shots: {
117
+ count: shots.length,
118
+ lengths: shots,
119
+ mean: round(mean, 2),
120
+ median: round(quantile(sortedShots, 0.5), 2),
121
+ shortest: sortedShots[0],
122
+ longest: sortedShots.at(-1),
123
+ // Standard deviation over mean: near 0 is metronomic, above ~0.5 is varied.
124
+ spread: mean > 0 ? round(sd / mean, 2) : 0,
125
+ },
126
+ motion: {
127
+ // Share of the length with something moving faster than 2% of the frame per frame.
128
+ fastShare: round(Math.min(1, fastSeconds / duration)),
129
+ fastMoves: flow.fast.length,
130
+ // Fastest move, as a share of the frame's long side per frame.
131
+ peak: flow.fast.reduce((best, run) => Math.max(best, run.peak), 0),
132
+ // Typical and busy picture change while moving (mean grey change per frame, %).
133
+ typical: round(quantile(moving, 0.5) * 100, 2),
134
+ busy: round(quantile(moving, 0.9) * 100, 2),
135
+ },
136
+ };
137
+ }
138
+
139
+ const seconds = (t) => `${t.toFixed(2)} s`;
140
+
141
+ /** A short, factual reading of the figures, for report.md. */
142
+ export function studyMarkdown(study, name) {
143
+ const lines = [
144
+ `# Reference study: ${name}`,
145
+ "",
146
+ "Rules to take, never scenes. Figures are measured on every frame.",
147
+ "",
148
+ "| Figure | Value |",
149
+ "|---|---|",
150
+ `| Length | ${seconds(study.duration)} at ${study.fps} fps |`,
151
+ `| Still | ${Math.round(study.stillShare * 100)}% of frames; ${study.rests.length} rest${study.rests.length === 1 ? "" : "s"} of ${REST_SECONDS} s or more, longest ${seconds(study.longestRest)} |`,
152
+ `| Hard cuts | ${study.cuts.length} (${study.cutsPerMinute} a minute) |`,
153
+ `| Bursts | ${study.bursts.length ? study.bursts.map((b) => `${b.cuts} cuts ${seconds(b.start)}–${seconds(b.end)}`).join("; ") : "none"} |`,
154
+ `| Shots | ${study.shots.count}; median ${seconds(study.shots.median)}, ${seconds(study.shots.shortest)}–${seconds(study.shots.longest)}, spread ${study.shots.spread} |`,
155
+ `| Fast motion | ${Math.round(study.motion.fastShare * 100)}% of the length in ${study.motion.fastMoves} move${study.motion.fastMoves === 1 ? "" : "s"}; peak ${Math.round(study.motion.peak * 100)}% of the frame a frame |`,
156
+ `| Picture change while moving | typical ${study.motion.typical}%, busy ${study.motion.busy}% a frame |`,
157
+ "",
158
+ "How to read it:",
159
+ "",
160
+ "- Spread near 0 means even shot lengths; above about 0.5 means a varied rhythm.",
161
+ "- Few cuts with motion most of the time means the video carries its scenes",
162
+ " instead of cutting; take the carry, not the cuts.",
163
+ "- Bursts are where the video speeds up on purpose; note where they fall in",
164
+ " the length, not what they show.",
165
+ "",
166
+ ];
167
+ if (study.rests.length)
168
+ lines.push(
169
+ "Rests:",
170
+ "",
171
+ ...study.rests.map((r) => `- ${seconds(r.start)}–${seconds(r.end)}`),
172
+ "",
173
+ );
174
+ if (study.cuts.length) lines.push("Cuts:", "", study.cuts.map((t) => seconds(t)).join(", "), "");
175
+ lines.push(
176
+ "Sheets: `sheet-shots.jpg` (one frame from the middle of each shot) and",
177
+ "`sheet-timeline.jpg` (evenly spaced). The frames behind them are in `frames/`.",
178
+ "",
179
+ );
180
+ return lines.join("\n");
181
+ }
182
+
183
+ /** Up to `count` evenly spread items of a list. */
184
+ function spread(list, count) {
185
+ if (list.length <= count) return list;
186
+ return Array.from(
187
+ { length: count },
188
+ (_, i) => list[Math.round((i * (list.length - 1)) / (count - 1))],
189
+ );
190
+ }
191
+
192
+ async function grabFrames(ffmpeg, file, times, folder, signal) {
193
+ await rm(folder, { recursive: true, force: true });
194
+ await mkdir(folder, { recursive: true });
195
+ for (const t of times) {
196
+ await run(
197
+ ffmpeg,
198
+ [
199
+ "-hide_banner",
200
+ "-nostdin",
201
+ "-loglevel",
202
+ "error",
203
+ "-protocol_whitelist",
204
+ "file,pipe",
205
+ "-ss",
206
+ t.toFixed(3),
207
+ "-i",
208
+ file,
209
+ "-frames:v",
210
+ "1",
211
+ "-vf",
212
+ "scale=640:-2",
213
+ "-q:v",
214
+ "3",
215
+ join(folder, `${t.toFixed(2)}s.jpg`),
216
+ ],
217
+ { timeout: 60_000, windowsHide: true, signal },
218
+ );
219
+ }
220
+ }
221
+
222
+ async function sheet(frames, out, signal) {
223
+ await run(process.execPath, [join(here, "contact-sheet.mjs"), frames, out, "--cols", "4"], {
224
+ timeout: 120_000,
225
+ windowsHide: true,
226
+ signal,
227
+ });
228
+ }
229
+
230
+ function parseArgs(argv) {
231
+ const [video, ...rest] = argv;
232
+ if (!video || video.startsWith("--"))
233
+ throw new Error("usage: reference-study.mjs <reference-video> [--out <folder>]");
234
+ const options = { video: resolve(video), out: resolve("reference-study") };
235
+ for (let index = 0; index < rest.length; index += 2) {
236
+ const flag = rest[index];
237
+ const value = rest[index + 1];
238
+ if (value === undefined || value.startsWith("--")) throw new Error(`${flag} needs a value`);
239
+ if (flag === "--out") options.out = resolve(value);
240
+ else throw new Error(`Unsupported option ${flag}`);
241
+ }
242
+ return options;
243
+ }
244
+
245
+ async function main() {
246
+ const started = performance.now();
247
+ const options = parseArgs(process.argv.slice(2));
248
+ if (!(await stat(options.video).catch(() => null))?.isFile())
249
+ throw new Error("Expected a local video file (download a linked video first)");
250
+ const controller = new AbortController();
251
+ const abort = () => controller.abort();
252
+ process.once("SIGTERM", abort);
253
+ process.once("SIGINT", abort);
254
+ try {
255
+ // Per frame step, measured in the same decode as cuts and fast moves.
256
+ const energy = [0];
257
+ const changed = [0];
258
+ let previous = null;
259
+ const flow = await measureFlow(options.video, controller.signal, (frame) => {
260
+ if (previous) {
261
+ let total = 0;
262
+ let visible = 0;
263
+ for (let i = 0; i < frame.length; i++) {
264
+ const difference = Math.abs(frame[i] - previous[i]);
265
+ total += difference;
266
+ if (difference > VISIBLE_LEVEL) visible++;
267
+ }
268
+ energy.push(total / frame.length / 255);
269
+ changed.push(visible / frame.length);
270
+ }
271
+ previous = Uint8Array.from(frame);
272
+ });
273
+ const study = studyFlow(flow, { energy, changed });
274
+ const name = basename(options.video);
275
+ await mkdir(options.out, { recursive: true });
276
+
277
+ const { ffmpeg } = await mediaBinaries();
278
+ // Frames sit a little inside each shot and the length, away from cuts and fades.
279
+ const end = Math.max(0, study.duration - 0.05);
280
+ const bounds = [0, ...study.cuts, study.duration];
281
+ const shotTimes = spread(
282
+ bounds.slice(1).map((b, i) => Math.min(end, (bounds[i] + b) / 2)),
283
+ MAX_SHOT_FRAMES,
284
+ );
285
+ const timeline = Array.from({ length: TIMELINE_FRAMES }, (_, i) =>
286
+ Math.min(end, ((i + 0.5) * study.duration) / TIMELINE_FRAMES),
287
+ );
288
+ await grabFrames(
289
+ ffmpeg,
290
+ options.video,
291
+ shotTimes,
292
+ join(options.out, "frames", "shots"),
293
+ controller.signal,
294
+ );
295
+ await grabFrames(
296
+ ffmpeg,
297
+ options.video,
298
+ timeline,
299
+ join(options.out, "frames", "timeline"),
300
+ controller.signal,
301
+ );
302
+ await sheet(
303
+ join(options.out, "frames", "shots"),
304
+ join(options.out, "sheet-shots.jpg"),
305
+ controller.signal,
306
+ );
307
+ await sheet(
308
+ join(options.out, "frames", "timeline"),
309
+ join(options.out, "sheet-timeline.jpg"),
310
+ controller.signal,
311
+ );
312
+
313
+ await writeFile(
314
+ join(options.out, "report.json"),
315
+ `${JSON.stringify({ version: 1, video: name, ...study }, null, 2)}\n`,
316
+ );
317
+ await writeFile(join(options.out, "report.md"), studyMarkdown(study, name));
318
+ process.stdout.write(
319
+ `${JSON.stringify({
320
+ ok: true,
321
+ out: options.out,
322
+ duration: study.duration,
323
+ stillShare: study.stillShare,
324
+ cuts: study.cuts.length,
325
+ bursts: study.bursts.length,
326
+ shotSpread: study.shots.spread,
327
+ fastShare: study.motion.fastShare,
328
+ elapsedMs: Math.round(performance.now() - started),
329
+ })}\n`,
330
+ );
331
+ } finally {
332
+ process.removeListener("SIGTERM", abort);
333
+ process.removeListener("SIGINT", abort);
334
+ }
335
+ }
336
+
337
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
338
+ try {
339
+ await main();
340
+ } catch (error) {
341
+ process.stderr.write(
342
+ `Reference study failed: ${error instanceof Error ? error.message : "invalid input"}\n`,
343
+ );
344
+ process.exitCode = 1;
345
+ }
346
+ }