@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.
- package/assets/motion/bin/contact-sheet.mjs +9 -3
- package/assets/motion/bin/cues.mjs +337 -0
- package/assets/motion/bin/library.mjs +53 -10
- package/assets/motion/bin/motion-blur.mjs +943 -0
- package/assets/motion/bin/motion-check.mjs +354 -3
- package/assets/motion/bin/music-fit.mjs +436 -0
- package/assets/motion/bin/pdf-extract.mjs +22 -10
- package/assets/motion/bin/reference-study.mjs +346 -0
- package/assets/motion/bin/score-synth.mjs +1052 -93
- package/assets/motion/library/README.md +50 -14
- package/assets/motion/library/kit/moves.js +1981 -0
- package/assets/motion/library/library.json +232 -0
- package/assets/motion/library/pieces/camera-rig/meta.json +13 -0
- package/assets/motion/library/pieces/camera-rig/piece.html +153 -0
- package/assets/motion/library/pieces/camera-rig/preview.jpg +0 -0
- package/assets/motion/library/pieces/chain-knock/meta.json +13 -0
- package/assets/motion/library/pieces/chain-knock/piece.html +195 -0
- package/assets/motion/library/pieces/chain-knock/preview.jpg +0 -0
- package/assets/motion/library/pieces/gather-to-logo/meta.json +13 -0
- package/assets/motion/library/pieces/gather-to-logo/piece.html +159 -0
- package/assets/motion/library/pieces/gather-to-logo/preview.jpg +0 -0
- package/assets/motion/library/pieces/morph-carry/meta.json +13 -0
- package/assets/motion/library/pieces/morph-carry/piece.html +173 -0
- package/assets/motion/library/pieces/morph-carry/preview.jpg +0 -0
- package/assets/motion/library/pieces/one-shape-journey/meta.json +13 -0
- package/assets/motion/library/pieces/one-shape-journey/piece.html +195 -0
- package/assets/motion/library/pieces/one-shape-journey/preview.jpg +0 -0
- package/assets/motion/library/pieces/open-from-subject/meta.json +13 -0
- package/assets/motion/library/pieces/open-from-subject/piece.html +168 -0
- package/assets/motion/library/pieces/open-from-subject/preview.jpg +0 -0
- package/assets/motion/library/pieces/request-to-result/meta.json +13 -0
- package/assets/motion/library/pieces/request-to-result/piece.html +212 -0
- package/assets/motion/library/pieces/request-to-result/preview.jpg +0 -0
- package/assets/motion/library/pieces/scale-dive/meta.json +13 -0
- package/assets/motion/library/pieces/scale-dive/piece.html +321 -0
- package/assets/motion/library/pieces/scale-dive/preview.jpg +0 -0
- package/assets/motion/library/pieces/screen-replica-steps/meta.json +13 -0
- package/assets/motion/library/pieces/screen-replica-steps/piece.html +366 -0
- package/assets/motion/library/pieces/screen-replica-steps/preview.jpg +0 -0
- package/assets/motion/library/pieces/zoom-into-card/meta.json +13 -0
- package/assets/motion/library/pieces/zoom-into-card/piece.html +179 -0
- package/assets/motion/library/pieces/zoom-into-card/preview.jpg +0 -0
- package/assets/motion/library/sheets/diagram.jpg +0 -0
- package/assets/motion/library/sheets/frame.jpg +0 -0
- package/assets/motion/library/sheets/transition.jpg +0 -0
- package/assets/motion/library/sheets/ui.jpg +0 -0
- package/assets/motion/references/build-sheet.md +206 -0
- package/assets/motion/references/runtime/determinism-rules.md +1 -1
- package/assets/motion/references/runtime/gsap-easing-and-stagger.md +29 -29
- package/assets/motion/references/runtime/inputs-and-assets.md +7 -12
- package/assets/motion/references/runtime/lint-validate-inspect.md +3 -3
- package/assets/motion/references/runtime/minimal-composition.md +1 -1
- package/assets/motion/references/runtime/preview-render.md +3 -3
- package/assets/motion/skills/app-walkthrough/SKILL.md +66 -0
- package/assets/motion/skills/before-after/SKILL.md +53 -0
- package/assets/motion/skills/brand-kit/SKILL.md +3 -3
- package/assets/motion/skills/dev-tool-video/SKILL.md +57 -0
- package/assets/motion/skills/launch-video/SKILL.md +62 -0
- package/assets/motion/skills/match-reference/SKILL.md +58 -0
- package/assets/motion/skills/motion/SKILL.md +78 -85
- package/assets/motion/skills/source-ingest/SKILL.md +17 -6
- package/assets/motion/skills/website-video/SKILL.md +59 -0
- package/assets/skills/bulletproof/SKILL.md +36 -11
- package/assets/skills/bulletproof/references/agent-surface.md +19 -9
- package/assets/skills/bulletproof/references/audit-protocol.md +20 -5
- package/assets/skills/bulletproof/references/platform-playbooks.md +5 -4
- package/assets/skills/bulletproof/references/provenance.md +26 -1
- package/assets/skills/bulletproof/references/secure-defaults.md +6 -5
- package/assets/skills/bulletproof/references/supply-chain.md +21 -17
- package/assets/skills/bulletproof/references/threat-landscape.md +28 -26
- package/assets/skills/bulletproof/references/verification.md +2 -0
- package/assets/skills/clarify/SKILL.md +25 -16
- package/assets/skills/code-review/SKILL.md +71 -13
- package/assets/skills/code-review/references/agent-diffs.md +27 -0
- package/assets/skills/code-review/references/tests.md +19 -0
- package/assets/skills/compliance-guard/SKILL.md +20 -5
- package/assets/skills/compliance-guard/references/artifacts.md +1 -1
- package/assets/skills/compliance-guard/references/eu-uk.md +16 -16
- package/assets/skills/compliance-guard/references/lawsuit-vectors.md +5 -5
- package/assets/skills/compliance-guard/references/provenance.md +41 -2
- package/assets/skills/compliance-guard/references/sector-gates.md +3 -3
- package/assets/skills/compliance-guard/references/security-baseline.md +2 -2
- package/assets/skills/compliance-guard/references/trigger-map.md +5 -5
- package/assets/skills/compliance-guard/references/us.md +27 -21
- package/assets/skills/durable/SKILL.md +87 -79
- package/assets/skills/durable/references/agent-db-safety.md +69 -0
- package/assets/skills/durable/references/backups-and-runtime.md +19 -12
- package/assets/skills/durable/references/migrations-and-schema.md +13 -6
- package/assets/skills/evidence-led-ui/SKILL.md +69 -127
- package/assets/skills/evidence-led-ui/references/anti-defaults.md +107 -208
- package/assets/skills/evidence-led-ui/references/direction.md +124 -0
- package/assets/skills/evidence-led-ui/references/production-contract.md +8 -0
- package/assets/skills/evidence-led-ui/references/provenance.md +24 -1
- package/assets/skills/lean/SKILL.md +90 -71
- package/assets/skills/lean/references/memory-and-processes.md +3 -2
- package/assets/skills/lean/references/playbooks.md +37 -12
- package/assets/skills/refactoring/SKILL.md +24 -3
- package/assets/skills/refactoring/references/agent-pitfalls.md +4 -1
- package/assets/skills/refactoring/references/legacy.md +21 -0
- package/assets/skills/root-cause/SKILL.md +20 -10
- package/assets/skills/shared-language/SKILL.md +16 -14
- package/assets/skills/tdd/SKILL.md +27 -15
- package/dist/app-sidecar.js +203 -47
- package/dist/app-sidecar.js.map +1 -1
- package/dist/cli.js +17 -26
- package/dist/cli.js.map +1 -1
- package/dist/core/acceptance-checks.d.ts +48 -0
- package/dist/core/acceptance-checks.js +144 -0
- package/dist/core/acceptance-checks.js.map +1 -0
- package/dist/core/agent-session.d.ts +106 -72
- package/dist/core/agent-session.js +539 -399
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.d.ts +6 -5
- package/dist/core/agents.js.map +1 -1
- package/dist/core/ask-user.d.ts +90 -8
- package/dist/core/ask-user.js +124 -13
- package/dist/core/ask-user.js.map +1 -1
- package/dist/core/bundled-agents.js +1 -3
- package/dist/core/bundled-agents.js.map +1 -1
- package/dist/core/cache-diagnostics.d.ts +68 -0
- package/dist/core/cache-diagnostics.js +196 -0
- package/dist/core/cache-diagnostics.js.map +1 -0
- package/dist/core/cache-expiry.d.ts +87 -0
- package/dist/core/cache-expiry.js +111 -0
- package/dist/core/cache-expiry.js.map +1 -0
- package/dist/core/compaction/compactor.js +78 -48
- package/dist/core/compaction/compactor.js.map +1 -1
- package/dist/core/compaction/plan-step-policy.d.ts +46 -0
- package/dist/core/compaction/plan-step-policy.js +57 -0
- package/dist/core/compaction/plan-step-policy.js.map +1 -0
- package/dist/core/destructive-git-guard.d.ts +90 -0
- package/dist/core/destructive-git-guard.js +871 -0
- package/dist/core/destructive-git-guard.js.map +1 -0
- package/dist/core/event-bus.d.ts +3 -0
- package/dist/core/event-bus.js +5 -0
- package/dist/core/event-bus.js.map +1 -1
- package/dist/core/injection-detect.d.ts +38 -0
- package/dist/core/injection-detect.js +232 -0
- package/dist/core/injection-detect.js.map +1 -0
- package/dist/core/keep-awake.d.ts +88 -0
- package/dist/core/keep-awake.js +251 -0
- package/dist/core/keep-awake.js.map +1 -0
- package/dist/core/mcp/client.d.ts +72 -0
- package/dist/core/mcp/client.js +264 -41
- package/dist/core/mcp/client.js.map +1 -1
- package/dist/core/mcp/content.js +6 -2
- package/dist/core/mcp/content.js.map +1 -1
- package/dist/core/mcp/store.d.ts +6 -1
- package/dist/core/mcp/store.js +12 -1
- package/dist/core/mcp/store.js.map +1 -1
- package/dist/core/mcp/types.d.ts +18 -0
- package/dist/core/model-unavailable.d.ts +14 -0
- package/dist/core/model-unavailable.js +23 -0
- package/dist/core/model-unavailable.js.map +1 -0
- package/dist/core/node-debugger.d.ts +148 -0
- package/dist/core/node-debugger.js +642 -0
- package/dist/core/node-debugger.js.map +1 -0
- package/dist/core/package-threats.d.ts +18 -0
- package/dist/core/package-threats.js +168 -0
- package/dist/core/package-threats.js.map +1 -0
- package/dist/core/persistent-shell.d.ts +58 -6
- package/dist/core/persistent-shell.js +331 -49
- package/dist/core/persistent-shell.js.map +1 -1
- package/dist/core/process-manager.d.ts +14 -0
- package/dist/core/process-manager.js +61 -0
- package/dist/core/process-manager.js.map +1 -1
- package/dist/core/progress/git-xp.js +8 -14
- package/dist/core/progress/git-xp.js.map +1 -1
- package/dist/core/session-history.d.ts +12 -0
- package/dist/core/session-history.js +27 -0
- package/dist/core/session-history.js.map +1 -1
- package/dist/core/session-manager.d.ts +13 -1
- package/dist/core/session-manager.js +38 -18
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-summary-index.d.ts +37 -0
- package/dist/core/session-summary-index.js +172 -0
- package/dist/core/session-summary-index.js.map +1 -0
- package/dist/core/settings-manager.d.ts +2 -0
- package/dist/core/settings-manager.js +10 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/shell-threats-popular-packages.d.ts +11 -0
- package/dist/core/shell-threats-popular-packages.js +675 -0
- package/dist/core/shell-threats-popular-packages.js.map +1 -0
- package/dist/core/shell-threats.d.ts +8 -0
- package/dist/core/shell-threats.js +186 -0
- package/dist/core/shell-threats.js.map +1 -0
- package/dist/core/skills.js +3 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/stream-rules.d.ts +30 -0
- package/dist/core/stream-rules.js +151 -0
- package/dist/core/stream-rules.js.map +1 -0
- package/dist/core/subagent-manager.d.ts +20 -5
- package/dist/core/subagent-manager.js +22 -8
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-receipt.d.ts +54 -0
- package/dist/core/subagent-receipt.js +276 -0
- package/dist/core/subagent-receipt.js.map +1 -0
- package/dist/core/subagent-turn-record.d.ts +2 -0
- package/dist/core/subagent-turn-record.js.map +1 -1
- package/dist/core/test-impact.d.ts +73 -0
- package/dist/core/test-impact.js +467 -0
- package/dist/core/test-impact.js.map +1 -0
- package/dist/core/thinking-level.d.ts +1 -1
- package/dist/core/thinking-level.js +1 -1
- package/dist/core/thinking-level.js.map +1 -1
- package/dist/core/verification-gate.d.ts +2 -0
- package/dist/core/verification-gate.js +4 -0
- package/dist/core/verification-gate.js.map +1 -1
- package/dist/core/verification-snapshot.js +3 -5
- package/dist/core/verification-snapshot.js.map +1 -1
- package/dist/core/workspace-guard.d.ts +18 -7
- package/dist/core/workspace-guard.js +227 -60
- package/dist/core/workspace-guard.js.map +1 -1
- package/dist/interactive.js +2 -1
- package/dist/interactive.js.map +1 -1
- package/dist/modes/subagent-worker-mode.js +34 -6
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/motion-agent/motion-agent.d.ts +6 -2
- package/dist/motion-agent/motion-agent.js +5 -7
- package/dist/motion-agent/motion-agent.js.map +1 -1
- package/dist/motion-agent/motion-prompt.d.ts +1 -1
- package/dist/motion-agent/motion-prompt.js +14 -19
- package/dist/motion-agent/motion-prompt.js.map +1 -1
- package/dist/motion-agent/motion-review.d.ts +10 -3
- package/dist/motion-agent/motion-review.js +15 -7
- package/dist/motion-agent/motion-review.js.map +1 -1
- package/dist/motion-agent/motion-studio-context.js +1 -1
- package/dist/motion-agent/motion-studio-context.js.map +1 -1
- package/dist/system-prompt.js +3 -1
- package/dist/system-prompt.js.map +1 -1
- package/dist/test-support/keep-alive.d.ts +14 -0
- package/dist/test-support/keep-alive.js +17 -0
- package/dist/test-support/keep-alive.js.map +1 -0
- package/dist/tools/ask-user.js +3 -3
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/bash-read-evidence.d.ts +10 -0
- package/dist/tools/bash-read-evidence.js +133 -0
- package/dist/tools/bash-read-evidence.js.map +1 -0
- package/dist/tools/bash.d.ts +10 -1
- package/dist/tools/bash.js +115 -7
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/debug.d.ts +54 -0
- package/dist/tools/debug.js +233 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/edit.js +12 -4
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/goals.d.ts +1 -1
- package/dist/tools/index.d.ts +19 -2
- package/dist/tools/index.js +47 -7
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/prompt-hints.js +2 -0
- package/dist/tools/prompt-hints.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +5 -0
- package/dist/tools/read-tracker.js +19 -9
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +3 -2
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/skill.js +5 -0
- package/dist/tools/skill.js.map +1 -1
- package/dist/tools/subagent-control.js +44 -8
- package/dist/tools/subagent-control.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +29 -8
- package/dist/tools/subagent-shared.js +45 -14
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/subagent.d.ts +8 -2
- package/dist/tools/subagent.js +28 -10
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/task-output.js +3 -2
- package/dist/tools/task-output.js.map +1 -1
- package/dist/tools/task-send.d.ts +1 -1
- package/dist/tools/task-send.js +15 -1
- package/dist/tools/task-send.js.map +1 -1
- package/dist/tools/tool-tiers.d.ts +2 -2
- package/dist/tools/tool-tiers.js +3 -2
- package/dist/tools/tool-tiers.js.map +1 -1
- package/dist/tools/truncate.d.ts +21 -0
- package/dist/tools/truncate.js +187 -0
- package/dist/tools/truncate.js.map +1 -1
- package/dist/tools/ui-adopt.js +2 -0
- package/dist/tools/ui-adopt.js.map +1 -1
- package/dist/ui/App.d.ts +0 -4
- package/dist/ui/App.js +5 -28
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/components/ActivityIndicator.js +1 -0
- package/dist/ui/components/ActivityIndicator.js.map +1 -1
- package/dist/ui/hooks/useAgentLoop.d.ts +1 -8
- package/dist/ui/hooks/useAgentLoop.js +1 -119
- package/dist/ui/hooks/useAgentLoop.js.map +1 -1
- package/dist/ui/render.d.ts +0 -4
- package/dist/ui/render.js +0 -2
- package/dist/ui/render.js.map +1 -1
- package/dist/utils/git.d.ts +77 -0
- package/dist/utils/git.js +285 -21
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/github-ci.js +2 -1
- package/dist/utils/github-ci.js.map +1 -1
- package/dist/utils/github.js +11 -9
- package/dist/utils/github.js.map +1 -1
- package/dist/utils/image.d.ts +14 -0
- package/dist/utils/image.js +16 -0
- package/dist/utils/image.js.map +1 -1
- package/dist/utils/process.d.ts +20 -0
- package/dist/utils/process.js +98 -0
- package/dist/utils/process.js.map +1 -1
- package/package.json +5 -5
- package/assets/motion/references/motion-language.md +0 -128
- package/assets/motion/skills/video-qa/SKILL.md +0 -89
- package/dist/core/ideal-review-subagent.d.ts +0 -56
- package/dist/core/ideal-review-subagent.js +0 -112
- package/dist/core/ideal-review-subagent.js.map +0 -1
- package/dist/core/ideal-review.d.ts +0 -82
- package/dist/core/ideal-review.js +0 -242
- package/dist/core/ideal-review.js.map +0 -1
- package/dist/motion-agent/motion-check-tool.d.ts +0 -35
- package/dist/motion-agent/motion-check-tool.js +0 -514
- package/dist/motion-agent/motion-check-tool.js.map +0 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Check decoded video pixels, not DOM geometry: works for HTML, canvas and WebGL.
|
|
3
3
|
// A freeze detector is not a creative-quality score. Review choreography too.
|
|
4
|
-
import { execFile } from "node:child_process";
|
|
4
|
+
import { execFile, spawn } from "node:child_process";
|
|
5
5
|
import { open, stat } from "node:fs/promises";
|
|
6
6
|
import { createReadStream } from "node:fs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { resolve } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { promisify } from "node:util";
|
|
11
|
-
import { ffmpegBinary } from "./media-binaries.mjs";
|
|
11
|
+
import { ffmpegBinary, mediaBinaries } from "./media-binaries.mjs";
|
|
12
12
|
|
|
13
13
|
const run = promisify(execFile);
|
|
14
14
|
|
|
@@ -133,6 +133,224 @@ export function parseMotionOutput(text, slideshowRequested = false, holds = [])
|
|
|
133
133
|
};
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Flow measurement: hard cuts and fast motion, read from every rendered frame on a small
|
|
137
|
+
// grey grid. Used by reference-study and the blur step's draft measure, never part of the
|
|
138
|
+
// pixel pass/fail.
|
|
139
|
+
/** Long side of the analysis grid: one cell is 12 px of a 1920-wide frame. */
|
|
140
|
+
export const FLOW_SIDE = 160;
|
|
141
|
+
const TILE = 10;
|
|
142
|
+
const SEARCH = 8;
|
|
143
|
+
/** Grey levels per sampled pixel charged for each cell of shift when matching a tile. */
|
|
144
|
+
const SHIFT_COST = 0.5;
|
|
145
|
+
/** A pixel "changed" when its grey level moves by more than this between frames. */
|
|
146
|
+
const CHANGED_LEVEL = 24;
|
|
147
|
+
/** A cut changes at least this share of the frame in one step... */
|
|
148
|
+
const CUT_CHANGE = 0.12;
|
|
149
|
+
/** ...by several times the mean difference of the frame steps either side of it... */
|
|
150
|
+
const CUT_SPIKE = 2;
|
|
151
|
+
/** ...and the change lasts: the picture this long after still differs this much (as a share
|
|
152
|
+
* of the step) from the one before. Glitches and flashes return to the old picture. */
|
|
153
|
+
const CUT_PERSISTS = 0.5;
|
|
154
|
+
const CUT_LASTS_SECONDS = 0.15;
|
|
155
|
+
/** Tiles moving at least this share of the long side per frame count as fast. */
|
|
156
|
+
export const FAST_PER_FRAME = 0.02;
|
|
157
|
+
/** Fast motion needs this many tiles agreeing and this many frames in a row. */
|
|
158
|
+
const FAST_TILES = 2;
|
|
159
|
+
const FAST_FRAMES = 3;
|
|
160
|
+
/** Stillness: a grid pixel moved visibly when its grey level changes by more than this
|
|
161
|
+
* (encoder noise on a held frame stays within a few levels)... */
|
|
162
|
+
export const STILL_LEVEL = 8;
|
|
163
|
+
/** ...and a frame step is dead still when at most this share of the grid moved visibly.
|
|
164
|
+
* Counting pixels, not averaging them, keeps a slow push or a small moving thing from
|
|
165
|
+
* reading as still. The same figures as reference-study.mjs, so the two agree. */
|
|
166
|
+
export const STILL_STEP_SHARE = 0.0002;
|
|
167
|
+
|
|
168
|
+
function tileSad(a, b, width, x, y, dx, dy, step, limit) {
|
|
169
|
+
let sum = 0;
|
|
170
|
+
for (let row = 0; row < TILE; row += step) {
|
|
171
|
+
const from = (y + row) * width + x;
|
|
172
|
+
const to = (y + row + dy) * width + x + dx;
|
|
173
|
+
for (let col = 0; col < TILE; col += step) {
|
|
174
|
+
sum += Math.abs(a[from + col] - b[to + col]);
|
|
175
|
+
if (sum > limit) return sum;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return sum;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Displacement of one tile from frame `a` to frame `b` in grid cells, or null when unmatched. */
|
|
182
|
+
function tileMotion(a, b, width, height, x, y) {
|
|
183
|
+
let mean = 0;
|
|
184
|
+
for (let row = 0; row < TILE; row++)
|
|
185
|
+
for (let col = 0; col < TILE; col++) mean += a[(y + row) * width + x + col];
|
|
186
|
+
mean /= TILE * TILE;
|
|
187
|
+
let variance = 0;
|
|
188
|
+
for (let row = 0; row < TILE; row++)
|
|
189
|
+
for (let col = 0; col < TILE; col++) variance += (a[(y + row) * width + x + col] - mean) ** 2;
|
|
190
|
+
// Flat tiles carry no position, and unchanged tiles did not move.
|
|
191
|
+
if (variance / (TILE * TILE) < 36) return null;
|
|
192
|
+
const still = tileSad(a, b, width, x, y, 0, 0, 1, Infinity) / (TILE * TILE);
|
|
193
|
+
if (still < 3) return null;
|
|
194
|
+
const inside = (dx, dy) =>
|
|
195
|
+
x + dx >= 0 && y + dy >= 0 && x + dx + TILE <= width && y + dy + TILE <= height;
|
|
196
|
+
// Stripes, rules and text lines match equally well at many shifts along their length.
|
|
197
|
+
// A small cost per cell of shift makes ties resolve to the shortest move, never the
|
|
198
|
+
// first one searched.
|
|
199
|
+
const search = (candidates, step) => {
|
|
200
|
+
const samples = (TILE / step) ** 2;
|
|
201
|
+
let best = { dx: 0, dy: 0, sad: Infinity, cost: Infinity };
|
|
202
|
+
for (const [dx, dy] of candidates) {
|
|
203
|
+
if (!inside(dx, dy)) continue;
|
|
204
|
+
const penalty = SHIFT_COST * samples * (Math.abs(dx) + Math.abs(dy));
|
|
205
|
+
const sad = tileSad(a, b, width, x, y, dx, dy, step, best.cost - penalty);
|
|
206
|
+
if (sad + penalty < best.cost) best = { dx, dy, sad, cost: sad + penalty };
|
|
207
|
+
}
|
|
208
|
+
return best;
|
|
209
|
+
};
|
|
210
|
+
const around = (cx, cy, reach, stride) => {
|
|
211
|
+
const list = [];
|
|
212
|
+
for (let dy = cy - reach; dy <= cy + reach; dy += stride)
|
|
213
|
+
for (let dx = cx - reach; dx <= cx + reach; dx += stride) list.push([dx, dy]);
|
|
214
|
+
return list;
|
|
215
|
+
};
|
|
216
|
+
// Coarse search on every other cell and pixel, then refine around the best.
|
|
217
|
+
const centre = search(around(0, 0, SEARCH, 2), 2);
|
|
218
|
+
const best = search(around(centre.dx, centre.dy, 1, 1), 1);
|
|
219
|
+
const matched = best.sad / (TILE * TILE);
|
|
220
|
+
// Fades, reveals and new content change pixels without moving them: no match.
|
|
221
|
+
if (matched > 12 || matched > 0.4 * still) return null;
|
|
222
|
+
return best;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Streams grey frames (width × height bytes each) and reports hard cuts and fast motion. */
|
|
226
|
+
export function createFlowMeter(width, height, fps) {
|
|
227
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < TILE || height < TILE)
|
|
228
|
+
throw new Error("Flow grid must be at least one tile");
|
|
229
|
+
if (!Number.isFinite(fps) || fps <= 0) throw new Error("Flow meter needs a frame rate");
|
|
230
|
+
const side = Math.max(width, height);
|
|
231
|
+
// Per step i (frame i-1 → i): changed share and mean difference; `lasting[i]` compares
|
|
232
|
+
// frame i-1 with the frame `ahead` steps later, to tell a cut from a transient glitch.
|
|
233
|
+
const ahead = Math.max(2, Math.round(CUT_LASTS_SECONDS * fps));
|
|
234
|
+
const change = [0];
|
|
235
|
+
const mean = [0];
|
|
236
|
+
const lasting = [];
|
|
237
|
+
const fastTiles = [0];
|
|
238
|
+
const fastest = [0];
|
|
239
|
+
const recent = [];
|
|
240
|
+
let stillSteps = 0;
|
|
241
|
+
let stillRun = 0;
|
|
242
|
+
let longestRun = 0;
|
|
243
|
+
let previous = null;
|
|
244
|
+
const changedShare = (a, b) => {
|
|
245
|
+
let changed = 0;
|
|
246
|
+
for (let index = 0; index < a.length; index++)
|
|
247
|
+
if (Math.abs(a[index] - b[index]) > CHANGED_LEVEL) changed++;
|
|
248
|
+
return changed / a.length;
|
|
249
|
+
};
|
|
250
|
+
return {
|
|
251
|
+
push(frame) {
|
|
252
|
+
if (frame.length !== width * height) throw new Error("Flow frame has the wrong size");
|
|
253
|
+
if (previous) {
|
|
254
|
+
let total = 0;
|
|
255
|
+
for (let index = 0; index < frame.length; index++)
|
|
256
|
+
total += Math.abs(frame[index] - previous[index]);
|
|
257
|
+
mean.push(total / frame.length / 255);
|
|
258
|
+
let visible = 0;
|
|
259
|
+
for (let index = 0; index < frame.length; index++)
|
|
260
|
+
if (Math.abs(frame[index] - previous[index]) > STILL_LEVEL) visible++;
|
|
261
|
+
if (visible / frame.length <= STILL_STEP_SHARE) {
|
|
262
|
+
stillSteps++;
|
|
263
|
+
stillRun++;
|
|
264
|
+
longestRun = Math.max(longestRun, stillRun);
|
|
265
|
+
} else stillRun = 0;
|
|
266
|
+
change.push(changedShare(previous, frame));
|
|
267
|
+
const columns = Math.floor(width / TILE);
|
|
268
|
+
const rows = Math.floor(height / TILE);
|
|
269
|
+
const vectors = [];
|
|
270
|
+
for (let row = 0; row < rows; row++)
|
|
271
|
+
for (let column = 0; column < columns; column++)
|
|
272
|
+
vectors.push(tileMotion(previous, frame, width, height, column * TILE, row * TILE));
|
|
273
|
+
// Something moving spans neighbouring tiles that move together; a fade or flicker
|
|
274
|
+
// gives isolated, disagreeing matches. Only fast tiles a neighbour agrees with count.
|
|
275
|
+
const agrees = (a, b) => b !== null && Math.hypot(a.dx - b.dx, a.dy - b.dy) <= 1.5;
|
|
276
|
+
let count = 0;
|
|
277
|
+
let peak = 0;
|
|
278
|
+
for (let row = 0; row < rows; row++)
|
|
279
|
+
for (let column = 0; column < columns; column++) {
|
|
280
|
+
const moved = vectors[row * columns + column];
|
|
281
|
+
if (!moved) continue;
|
|
282
|
+
const speed = Math.hypot(moved.dx, moved.dy) / side;
|
|
283
|
+
if (speed < FAST_PER_FRAME) continue;
|
|
284
|
+
const near = [
|
|
285
|
+
column > 0 ? vectors[row * columns + column - 1] : null,
|
|
286
|
+
column + 1 < columns ? vectors[row * columns + column + 1] : null,
|
|
287
|
+
row > 0 ? vectors[(row - 1) * columns + column] : null,
|
|
288
|
+
row + 1 < rows ? vectors[(row + 1) * columns + column] : null,
|
|
289
|
+
];
|
|
290
|
+
if (!near.some((other) => agrees(moved, other))) continue;
|
|
291
|
+
count++;
|
|
292
|
+
peak = Math.max(peak, speed);
|
|
293
|
+
}
|
|
294
|
+
fastTiles.push(count);
|
|
295
|
+
fastest.push(peak);
|
|
296
|
+
}
|
|
297
|
+
previous = Uint8Array.from(frame);
|
|
298
|
+
// recent[0] is frame (count - ahead - 1): the frame before step (count - ahead).
|
|
299
|
+
recent.push(previous);
|
|
300
|
+
if (recent.length > ahead + 1) recent.shift();
|
|
301
|
+
const step = change.length - ahead;
|
|
302
|
+
if (recent.length === ahead + 1 && step >= 1)
|
|
303
|
+
lasting[step] = changedShare(recent[0], previous);
|
|
304
|
+
},
|
|
305
|
+
finish() {
|
|
306
|
+
const frames = change.length;
|
|
307
|
+
const round = (value) => Math.round(value * 1000) / 1000;
|
|
308
|
+
const cutFrames = new Set();
|
|
309
|
+
for (let index = 1; index < frames; index++) {
|
|
310
|
+
const around = Math.max(mean[index - 1] ?? 0, mean[index + 1] ?? 0, 0.004);
|
|
311
|
+
// Steps too close to the end to look ahead keep the change as lasting.
|
|
312
|
+
const lasts = (lasting[index] ?? Infinity) >= CUT_PERSISTS * change[index];
|
|
313
|
+
if (change[index] >= CUT_CHANGE && mean[index] >= CUT_SPIKE * around && lasts)
|
|
314
|
+
cutFrames.add(index);
|
|
315
|
+
}
|
|
316
|
+
const fast = [];
|
|
317
|
+
let run = null;
|
|
318
|
+
const close = () => {
|
|
319
|
+
if (run && run.end - run.start + 1 >= FAST_FRAMES)
|
|
320
|
+
fast.push({
|
|
321
|
+
start: round(run.start / fps),
|
|
322
|
+
end: round((run.end + 1) / fps),
|
|
323
|
+
peak: round(run.peak),
|
|
324
|
+
});
|
|
325
|
+
run = null;
|
|
326
|
+
};
|
|
327
|
+
for (let index = 1; index < frames; index++) {
|
|
328
|
+
const isFast = !cutFrames.has(index) && fastTiles[index] >= FAST_TILES;
|
|
329
|
+
if (isFast) {
|
|
330
|
+
// A single quiet frame inside a move does not split it.
|
|
331
|
+
if (run && index - run.end <= 2) {
|
|
332
|
+
run.end = index;
|
|
333
|
+
run.peak = Math.max(run.peak, fastest[index]);
|
|
334
|
+
} else {
|
|
335
|
+
close();
|
|
336
|
+
run = { start: index, end: index, peak: fastest[index] };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
close();
|
|
341
|
+
return {
|
|
342
|
+
frames,
|
|
343
|
+
fps,
|
|
344
|
+
cuts: [...cutFrames].map((index) => round(index / fps)),
|
|
345
|
+
fast,
|
|
346
|
+
// Share of frame steps with no visible change, and the longest run of them.
|
|
347
|
+
stillShare: frames > 1 ? round(stillSteps / (frames - 1)) : 0,
|
|
348
|
+
longestStill: round(longestRun / fps),
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
136
354
|
async function videoHash(file, signal) {
|
|
137
355
|
const hash = createHash("sha256");
|
|
138
356
|
for await (const chunk of createReadStream(file, { signal })) hash.update(chunk);
|
|
@@ -173,7 +391,139 @@ async function readHoldPlan(file, signal) {
|
|
|
173
391
|
}
|
|
174
392
|
}
|
|
175
393
|
|
|
394
|
+
/** The motion-blur render step stamps its output's comment tag with this marker. */
|
|
395
|
+
export const BLUR_STAMP = /^ez-motion-blur samples=(\d+)$/;
|
|
396
|
+
|
|
397
|
+
/** Grid of at most FLOW_SIDE on the long side that keeps the export's aspect ratio. */
|
|
398
|
+
export function flowGrid(width, height) {
|
|
399
|
+
const scale = FLOW_SIDE / Math.max(width, height);
|
|
400
|
+
return {
|
|
401
|
+
width: Math.max(TILE, Math.round(width * scale)),
|
|
402
|
+
height: Math.max(TILE, Math.round(height * scale)),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Hard cuts and fast motion of a video, from every frame. `onFrame(frame, grid)`, when given,
|
|
408
|
+
* also sees each grey frame (grid.width × grid.height bytes) so a caller can measure more in
|
|
409
|
+
* the same decode; the frame buffer is reused, so copy what you keep.
|
|
410
|
+
*/
|
|
411
|
+
export async function measureFlow(file, signal, onFrame) {
|
|
412
|
+
const { ffmpeg, ffprobe } = await mediaBinaries();
|
|
413
|
+
const { stdout } = await run(
|
|
414
|
+
ffprobe,
|
|
415
|
+
[
|
|
416
|
+
"-v",
|
|
417
|
+
"error",
|
|
418
|
+
"-protocol_whitelist",
|
|
419
|
+
"file,pipe",
|
|
420
|
+
"-select_streams",
|
|
421
|
+
"v:0",
|
|
422
|
+
"-show_entries",
|
|
423
|
+
"stream=width,height,avg_frame_rate:format=duration:format_tags=comment",
|
|
424
|
+
"-of",
|
|
425
|
+
"json",
|
|
426
|
+
file,
|
|
427
|
+
],
|
|
428
|
+
{ encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024, windowsHide: true, signal },
|
|
429
|
+
);
|
|
430
|
+
const probe = JSON.parse(stdout);
|
|
431
|
+
const stream = probe?.streams?.[0];
|
|
432
|
+
const [num, den] = String(stream?.avg_frame_rate ?? "")
|
|
433
|
+
.split("/")
|
|
434
|
+
.map(Number);
|
|
435
|
+
const fps = num / den;
|
|
436
|
+
const duration = Number(probe?.format?.duration);
|
|
437
|
+
if (
|
|
438
|
+
!Number.isInteger(stream?.width) ||
|
|
439
|
+
!Number.isInteger(stream?.height) ||
|
|
440
|
+
!(fps >= 1 && fps <= 120) ||
|
|
441
|
+
!(duration > 0 && duration <= 3600)
|
|
442
|
+
)
|
|
443
|
+
throw new Error("Unsupported video metadata for flow analysis");
|
|
444
|
+
const stamp = BLUR_STAMP.exec(String(probe?.format?.tags?.comment ?? ""));
|
|
445
|
+
const grid = flowGrid(stream.width, stream.height);
|
|
446
|
+
const meter = createFlowMeter(grid.width, grid.height, fps);
|
|
447
|
+
const size = grid.width * grid.height;
|
|
448
|
+
const child = spawn(
|
|
449
|
+
ffmpeg,
|
|
450
|
+
[
|
|
451
|
+
"-hide_banner",
|
|
452
|
+
"-nostdin",
|
|
453
|
+
"-loglevel",
|
|
454
|
+
"error",
|
|
455
|
+
"-xerror",
|
|
456
|
+
"-protocol_whitelist",
|
|
457
|
+
"file,pipe",
|
|
458
|
+
"-i",
|
|
459
|
+
file,
|
|
460
|
+
"-map",
|
|
461
|
+
"0:v:0",
|
|
462
|
+
"-an",
|
|
463
|
+
"-vf",
|
|
464
|
+
`scale=${grid.width}:${grid.height}:flags=area,format=gray`,
|
|
465
|
+
"-f",
|
|
466
|
+
"rawvideo",
|
|
467
|
+
"-",
|
|
468
|
+
],
|
|
469
|
+
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal },
|
|
470
|
+
);
|
|
471
|
+
let errors = "";
|
|
472
|
+
child.stderr.on("data", (chunk) => {
|
|
473
|
+
if (errors.length < 4000) errors += chunk;
|
|
474
|
+
});
|
|
475
|
+
const exited = new Promise((resolveExit, reject) => {
|
|
476
|
+
child.once("error", reject);
|
|
477
|
+
child.once("close", (code) => resolveExit(code));
|
|
478
|
+
});
|
|
479
|
+
let pending = Buffer.alloc(0);
|
|
480
|
+
for await (const chunk of child.stdout) {
|
|
481
|
+
pending = pending.length ? Buffer.concat([pending, chunk]) : chunk;
|
|
482
|
+
let offset = 0;
|
|
483
|
+
while (pending.length - offset >= size) {
|
|
484
|
+
const frame = pending.subarray(offset, offset + size);
|
|
485
|
+
meter.push(frame);
|
|
486
|
+
onFrame?.(frame, grid);
|
|
487
|
+
offset += size;
|
|
488
|
+
}
|
|
489
|
+
pending = pending.subarray(offset);
|
|
490
|
+
}
|
|
491
|
+
const code = await exited;
|
|
492
|
+
if (code !== 0 || pending.length) throw new Error(`Flow decode failed: ${errors.trim()}`);
|
|
493
|
+
const measured = meter.finish();
|
|
494
|
+
if (measured.frames < 2) throw new Error("Flow analysis decoded too few frames");
|
|
495
|
+
return {
|
|
496
|
+
...measured,
|
|
497
|
+
duration: Math.round(duration * 1000) / 1000,
|
|
498
|
+
blur: stamp ? { samples: Number(stamp[1]) } : null,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
async function flowMain(video, extra) {
|
|
503
|
+
if (!video || extra.length) throw new Error("usage: motion-check.mjs --flow <rendered-video>");
|
|
504
|
+
const file = resolve(video);
|
|
505
|
+
if (!(await stat(file)).isFile()) throw new Error("Expected a local rendered video file");
|
|
506
|
+
const controller = new AbortController();
|
|
507
|
+
const abort = () => controller.abort();
|
|
508
|
+
process.once("SIGTERM", abort);
|
|
509
|
+
process.once("SIGINT", abort);
|
|
510
|
+
const started = performance.now();
|
|
511
|
+
try {
|
|
512
|
+
const videoSha256 = await videoHash(file, controller.signal);
|
|
513
|
+
const flow = await measureFlow(file, controller.signal);
|
|
514
|
+
if ((await videoHash(file, controller.signal)) !== videoSha256)
|
|
515
|
+
throw new Error("Video changed during analysis; flow is unmeasured");
|
|
516
|
+
process.stdout.write(
|
|
517
|
+
`${JSON.stringify({ ok: true, ...flow, file, videoSha256, elapsedMs: Math.round(performance.now() - started) })}\n`,
|
|
518
|
+
);
|
|
519
|
+
} finally {
|
|
520
|
+
process.removeListener("SIGTERM", abort);
|
|
521
|
+
process.removeListener("SIGINT", abort);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
176
525
|
async function main() {
|
|
526
|
+
if (process.argv[2] === "--flow") return flowMain(process.argv[3], process.argv.slice(4));
|
|
177
527
|
const [video, option, holdFile, ...extra] = process.argv.slice(2);
|
|
178
528
|
if (
|
|
179
529
|
!video ||
|
|
@@ -181,7 +531,8 @@ async function main() {
|
|
|
181
531
|
(option === "--holds" ? !holdFile : holdFile || (option && option !== "--slideshow-requested"))
|
|
182
532
|
) {
|
|
183
533
|
throw new Error(
|
|
184
|
-
"usage: motion-check.mjs <rendered-video> [--holds <holds.json> | --slideshow-requested]"
|
|
534
|
+
"usage: motion-check.mjs <rendered-video> [--holds <holds.json> | --slideshow-requested]\n" +
|
|
535
|
+
" motion-check.mjs --flow <rendered-video>",
|
|
185
536
|
);
|
|
186
537
|
}
|
|
187
538
|
const file = resolve(video);
|