@prestyj/cli 5.28.0 → 5.28.1
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/README.md +2 -2
- package/assets/motion/THIRD-PARTY.md +14 -19
- package/assets/motion/bin/flash-check.mjs +314 -0
- package/assets/motion/bin/fonts.mjs +17 -13
- package/assets/motion/bin/library.mjs +8 -8
- package/assets/motion/bin/motion-check.mjs +12 -6
- package/assets/motion/fonts/finger-paint/OFL.txt +93 -0
- package/assets/motion/fonts/finger-paint/finger-paint-normal.woff2 +0 -0
- package/assets/motion/fonts/fonts.json +56 -0
- package/assets/motion/fonts/short-stack/OFL.txt +94 -0
- package/assets/motion/fonts/short-stack/short-stack-normal.woff2 +0 -0
- package/assets/motion/fonts/sora/OFL.txt +93 -0
- package/assets/motion/fonts/sora/sora-normal.woff2 +0 -0
- package/assets/motion/fonts/specimen.jpg +0 -0
- package/assets/motion/fonts/unbounded/OFL.txt +93 -0
- package/assets/motion/fonts/unbounded/unbounded-normal.woff2 +0 -0
- package/assets/motion/library/README.md +2 -0
- package/assets/motion/plugin.json +1 -1
- package/assets/motion/references/motion-language.md +128 -0
- package/assets/motion/references/runtime/determinism-rules.md +3 -3
- package/assets/motion/references/runtime/gsap-easing-and-stagger.md +19 -19
- package/assets/motion/references/runtime/gsap.md +3 -3
- package/assets/motion/references/runtime/inputs-and-assets.md +19 -9
- package/assets/motion/references/runtime/minimal-composition.md +6 -0
- package/assets/motion/skills/brand-kit/SKILL.md +16 -15
- package/assets/motion/skills/motion/SKILL.md +166 -90
- package/assets/motion/skills/source-ingest/SKILL.md +7 -10
- package/assets/motion/skills/video-qa/SKILL.md +43 -20
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/agent-session.d.ts +13 -3
- package/dist/core/agent-session.js +32 -6
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agents.js +12 -3
- package/dist/core/agents.js.map +1 -1
- package/dist/core/auth-providers.js +1 -1
- package/dist/core/auth-providers.js.map +1 -1
- package/dist/core/autopilot-verdict.d.ts +5 -1
- package/dist/core/autopilot-verdict.js +30 -13
- package/dist/core/autopilot-verdict.js.map +1 -1
- package/dist/core/fast-apply-benchmark.d.ts +1 -1
- package/dist/core/fast-apply-benchmark.js +2 -2
- package/dist/core/fast-apply-benchmark.js.map +1 -1
- package/dist/core/ideal-review-subagent.d.ts +16 -2
- package/dist/core/ideal-review-subagent.js +19 -2
- package/dist/core/ideal-review-subagent.js.map +1 -1
- package/dist/core/nolan-context.d.ts +7 -5
- package/dist/core/nolan-context.js +106 -16
- package/dist/core/nolan-context.js.map +1 -1
- package/dist/core/nolan-prompt.js +24 -21
- package/dist/core/nolan-prompt.js.map +1 -1
- package/dist/core/project-discovery.js +77 -26
- package/dist/core/project-discovery.js.map +1 -1
- package/dist/core/semantic-search-benchmark.d.ts +1 -1
- package/dist/core/semantic-search-benchmark.js +2 -2
- package/dist/core/semantic-search-benchmark.js.map +1 -1
- package/dist/core/skills.js +13 -3
- package/dist/core/skills.js.map +1 -1
- package/dist/core/subagent-manager.d.ts +3 -1
- package/dist/core/subagent-manager.js +3 -1
- package/dist/core/subagent-manager.js.map +1 -1
- package/dist/core/subagent-policy.js +1 -1
- package/dist/core/subagent-policy.js.map +1 -1
- package/dist/core/worktree-setup.d.ts +23 -0
- package/dist/core/worktree-setup.js +128 -9
- package/dist/core/worktree-setup.js.map +1 -1
- package/dist/core/worktree.d.ts +20 -2
- package/dist/core/worktree.js +74 -31
- package/dist/core/worktree.js.map +1 -1
- package/dist/modes/json-mode.js +11 -2
- package/dist/modes/json-mode.js.map +1 -1
- package/dist/modes/subagent-worker-mode.d.ts +36 -1
- package/dist/modes/subagent-worker-mode.js +68 -38
- package/dist/modes/subagent-worker-mode.js.map +1 -1
- package/dist/motion-agent/motion-agent.d.ts +1 -1
- package/dist/motion-agent/motion-agent.js +1 -1
- package/dist/motion-agent/motion-check-tool.d.ts +15 -1
- package/dist/motion-agent/motion-check-tool.js +345 -37
- package/dist/motion-agent/motion-check-tool.js.map +1 -1
- package/dist/motion-agent/motion-prompt.d.ts +1 -1
- package/dist/motion-agent/motion-prompt.js +16 -20
- package/dist/motion-agent/motion-prompt.js.map +1 -1
- package/dist/motion-agent/motion-review-session.js +1 -1
- package/dist/motion-agent/motion-review-session.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.d.ts +2 -1
- package/dist/system-prompt.js +12 -3
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/bash.js +64 -0
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/edit.js +1 -1
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/goals.d.ts +1 -1
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read-tracker.d.ts +30 -2
- package/dist/tools/read-tracker.js +92 -5
- package/dist/tools/read-tracker.js.map +1 -1
- package/dist/tools/read.js +36 -5
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/subagent-shared.d.ts +19 -0
- package/dist/tools/subagent-shared.js +30 -0
- package/dist/tools/subagent-shared.js.map +1 -1
- package/dist/tools/write.js +4 -3
- package/dist/tools/write.js.map +1 -1
- package/dist/ui/components/Footer.js +1 -1
- package/dist/ui/components/Footer.js.map +1 -1
- package/dist/ui/render.d.ts +2 -0
- package/dist/ui/render.js +3 -0
- package/dist/ui/render.js.map +1 -1
- package/dist/utils/text.d.ts +12 -0
- package/dist/utils/text.js +11 -0
- package/dist/utils/text.js.map +1 -1
- package/package.json +5 -5
- package/assets/motion/skills/mixkit-split-text-617/SKILL.md +0 -115
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-480.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-494.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-5.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-508.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6525.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6539.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6647.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6663.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6677.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6691.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6706.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6722.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6736.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6750.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6764.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6780.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6794.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6810.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6823.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6872.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-6936.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/compositions/comp-76.json +0 -1
- package/assets/motion/skills/mixkit-split-text-617/data/manifest.json +0 -495
- package/assets/motion/skills/mixkit-split-text-617/references/RECONSTRUCTION.md +0 -201
- package/assets/motion/skills/mixkit-split-text-617/references/VERIFICATION.md +0 -107
- package/assets/motion/skills/mixkit-split-text-617/tools/inspect_motion.py +0 -130
package/README.md
CHANGED
|
@@ -71,7 +71,7 @@ Switch mid-conversation with `/model`. Not locked to anyone.
|
|
|
71
71
|
| Provider | Models | Auth |
|
|
72
72
|
| ----------------- | ---------------------------------------------------------------------------- | ---------------- |
|
|
73
73
|
| **Anthropic** | Claude Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 4.5 | OAuth |
|
|
74
|
-
| **OpenAI** | GPT-6 Astra, GPT-6 Sol, GPT-6 Luna
|
|
74
|
+
| **OpenAI** | GPT-6 Astra, GPT-6.1 Sol, GPT-6 Luna | OAuth |
|
|
75
75
|
| **Moonshot** | Kimi K3, Kimi K2.8 Preview, Kimi K2.7 Code | OAuth or API key |
|
|
76
76
|
| **Z.AI (GLM)** | GLM-5.3, GLM-5.3-Flash (image) | API key |
|
|
77
77
|
| **MiniMax** | MiniMax M3 (image + video) | API key |
|
|
@@ -177,7 +177,7 @@ Plus the [Grep MCP](https://grep.dev) for searching across 1M+ public GitHub rep
|
|
|
177
177
|
|
|
178
178
|
`subagent` remains blocking. The async suite launches persistent NDJSON worker processes, so a parent can start up to four active child turns, keep working, steer them, and wait for any or all results. Up to eight idle workers remain available for follow-up; bounded snapshots retain the latest 20 agents.
|
|
179
179
|
|
|
180
|
-
Only GPT-6 Astra
|
|
180
|
+
Only GPT-6 Astra and GPT-6.1 Sol at **Ultra** delegate proactively. Lower Astra/Sol levels use async agents only when the user or project/skill instructions request delegation; other models receive no proactive policy.
|
|
181
181
|
|
|
182
182
|
Children share the parent working directory by default, so parallel writes must target disjoint files or subsystems. Pass `isolate: true` to `subagent` to give a child its own copy of the repo on a new branch instead — the right choice when several children edit at once, or when a child's changes should land on a branch of their own. The copy is reclaimed automatically when the child finishes without committing; a child that did commit keeps its branch, because that branch is the deliverable. `/integrate` then merges those branches one at a time, running the suite between each. Async fan-out is one level deep, child output is bounded, idle workers reap after 10 minutes, and workers are not resumable after a CLI/app restart.
|
|
183
183
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Third-party content in EZ Motion
|
|
2
2
|
|
|
3
|
-
Motion exposes GG-authored
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Motion exposes GG-authored skills and craft references. The old upstream
|
|
4
|
+
creative/workflow skills and guidance adapters have been removed. Retained
|
|
5
|
+
technical references are adapted for GG's pinned launcher and local-only safety
|
|
6
|
+
boundaries; do not restore entire upstream skill folders when updating the
|
|
7
|
+
runtime.
|
|
8
8
|
|
|
9
9
|
| Component | Source | Pinned at | License |
|
|
10
10
|
|---|---|---|---|
|
|
@@ -14,17 +14,11 @@ upstream skill folders when updating the runtime.
|
|
|
14
14
|
| Style-library pieces marked MIT (`library/pieces/<id>/`) | Magic UI, https://github.com/magicuidesign/magicui | converted 2026-09-28; upstream file URL in each `meta.json` | MIT (`LICENSE` beside each piece) |
|
|
15
15
|
| Fonts (`fonts/<family>/`) | Google Fonts / https://github.com/google/fonts | fetched 2026-09-28 by `scripts/fetch-motion-fonts.mjs` | SIL OFL 1.1 (`OFL.txt` beside each family) |
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Content boundaries
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
source preview video or source font binary is bundled with that recipe.
|
|
23
|
-
|
|
24
|
-
Private extraction tools, source projects, working exports and authoring-method
|
|
25
|
-
documents are Git-ignored and excluded from release packaging. They are not
|
|
26
|
-
runtime dependencies or additional callable skills. The normal video agent uses
|
|
27
|
-
installed recipes; it does not run a parser or install authoring dependencies.
|
|
19
|
+
Motion ships no templates derived from third-party After Effects or other
|
|
20
|
+
motion projects. The earlier Mixkit, Mobile Notification and Kinetic Text
|
|
21
|
+
reconstructions were removed on 2026-09-29 and are not part of any release.
|
|
28
22
|
|
|
29
23
|
Existing `references/index.json`, signal sources and compact temporal JPEGs are
|
|
30
24
|
GG studies, not private user videos or a universal style. They remain optional
|
|
@@ -34,14 +28,15 @@ licenses remain with their sources.
|
|
|
34
28
|
|
|
35
29
|
The Motion-only preference reader and review tool do not load Coder instructions.
|
|
36
30
|
Review remains bounded and tied to current source/render evidence. It checks the
|
|
37
|
-
|
|
31
|
+
video's plan and approved inputs, not a competing art direction, and does not
|
|
38
32
|
claim full playback or aesthetic certification from model image critique.
|
|
39
33
|
|
|
40
34
|
## Fonts
|
|
41
35
|
|
|
42
|
-
|
|
43
|
-
by Google Fonts, except Mona Sans
|
|
44
|
-
Names and ship as complete upstream fonts, recompressed
|
|
36
|
+
19 families, listed in `fonts/fonts.json`. Latin-subset variable woff2 as served
|
|
37
|
+
by Google Fonts, except Mona Sans, Hubot Sans, Short Stack and Finger Paint: they
|
|
38
|
+
carry OFL Reserved Font Names and ship as complete upstream fonts, recompressed
|
|
39
|
+
to woff2. Unbounded, Sora, Short Stack and Finger Paint were added 2026-09-29. `fonts.mjs add`
|
|
45
40
|
copies each family's `OFL.txt` into the video project with its font files.
|
|
46
41
|
|
|
47
42
|
## Shared audio retained from brag
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Photosensitivity screen on decoded pixels against the WCAG 2.3.1 general and red flash
|
|
3
|
+
// thresholds. As in EA's open-source IRIS tool, an area's mean change only counts on frames
|
|
4
|
+
// where at least a quarter of the area changes, so moving content mostly cancels out rather
|
|
5
|
+
// than reading as flashing. IRIS applies this to the whole screen; here it applies to every
|
|
6
|
+
// WCAG 10° field (a third of the frame each way), so a flash filling part of the frame is
|
|
7
|
+
// caught too. A transition is a move of 0.1 relative luminance (or 20 red units) away from the
|
|
8
|
+
// last peak or valley, as WCAG defines it; more than six in any second fails. Renders above
|
|
9
|
+
// 60 fps are sampled at 60. A screen, not a certification.
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { createReadStream } from "node:fs";
|
|
12
|
+
import { stat } from "node:fs/promises";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { once } from "node:events";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { ffmpegBinary } from "./media-binaries.mjs";
|
|
18
|
+
|
|
19
|
+
/** Cells per side of the analysis grid; every aspect ratio maps onto it. */
|
|
20
|
+
export const GRID = 48;
|
|
21
|
+
/** Frames per analysed second; slower renders are repeated, so one second is always 60 frames. */
|
|
22
|
+
export const ANALYSIS_FPS = 60;
|
|
23
|
+
/** WCAG approximates the 10° field as 341×256 px of a 1024×768 screen: a third each way. */
|
|
24
|
+
const FIELD = GRID / 3;
|
|
25
|
+
const FIELD_CELLS = FIELD * FIELD;
|
|
26
|
+
/** A field's change counts once a quarter of it changes. */
|
|
27
|
+
const AREA = 0.25 * FIELD_CELLS;
|
|
28
|
+
const WINDOWS_PER_SIDE = GRID - FIELD + 1;
|
|
29
|
+
const WINDOWS = WINDOWS_PER_SIDE * WINDOWS_PER_SIDE;
|
|
30
|
+
/** More than three flashes (six opposing transitions) within any one second fails. */
|
|
31
|
+
const MAX_TRANSITIONS = 6;
|
|
32
|
+
/** Transitions per field within the trailing second never exceed one per analysed frame. */
|
|
33
|
+
const RING = 64;
|
|
34
|
+
const CELLS = GRID * GRID;
|
|
35
|
+
const FRAME_BYTES = CELLS * 3;
|
|
36
|
+
const CHANNELS = {
|
|
37
|
+
// Relative luminance 0–1: a 0.1 swing, unless even the darker state is 0.8 or brighter.
|
|
38
|
+
// A cell has changed once it moves by more than encoding noise (about 1/255 linear).
|
|
39
|
+
general: { threshold: 0.1, darkLimit: 0.8, noise: 0.004 },
|
|
40
|
+
// WCAG's red measure: (R − G − B) × 320 where R / (R + G + B) ≥ 0.8, else 0.
|
|
41
|
+
red: { threshold: 20, darkLimit: Infinity, noise: 1 },
|
|
42
|
+
};
|
|
43
|
+
/** sRGB to linear light, applied before averaging so mixed cells keep their real brightness. */
|
|
44
|
+
const LINEAR = "if(lte(val,10.31475),val/12.92,255*pow((val/255+0.055)/1.055,2.4))";
|
|
45
|
+
|
|
46
|
+
function createChannel({ threshold, darkLimit, noise }) {
|
|
47
|
+
const side = GRID + 1;
|
|
48
|
+
const changed = new Float64Array(side * side);
|
|
49
|
+
const change = new Float64Array(side * side);
|
|
50
|
+
const level = new Float64Array(side * side);
|
|
51
|
+
// Per field: the accumulated counted change, its last peak and valley, and the field's
|
|
52
|
+
// mean at each, for WCAG's "darker image" rule.
|
|
53
|
+
const signal = new Float64Array(WINDOWS);
|
|
54
|
+
const peakValue = new Float64Array(WINDOWS);
|
|
55
|
+
const peakLevel = new Float64Array(WINDOWS);
|
|
56
|
+
const valleyValue = new Float64Array(WINDOWS);
|
|
57
|
+
const valleyLevel = new Float64Array(WINDOWS);
|
|
58
|
+
/** 1 after a rise, -1 after a fall, 0 before the first transition. */
|
|
59
|
+
const direction = new Int8Array(WINDOWS);
|
|
60
|
+
const ring = new Int32Array(WINDOWS * RING);
|
|
61
|
+
const head = new Int32Array(WINDOWS);
|
|
62
|
+
const tail = new Int32Array(WINDOWS);
|
|
63
|
+
const failures = [];
|
|
64
|
+
let peak = 0;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
update(frame, previous, current) {
|
|
68
|
+
// Summed-area tables, so each field's totals cost four lookups.
|
|
69
|
+
for (let y = 0; y < GRID; y++) {
|
|
70
|
+
let rowChanged = 0;
|
|
71
|
+
let rowChange = 0;
|
|
72
|
+
let rowLevel = 0;
|
|
73
|
+
for (let x = 0; x < GRID; x++) {
|
|
74
|
+
const cell = y * GRID + x;
|
|
75
|
+
const delta = current[cell] - previous[cell];
|
|
76
|
+
if (Math.abs(delta) > noise) rowChanged++;
|
|
77
|
+
rowChange += delta;
|
|
78
|
+
rowLevel += current[cell];
|
|
79
|
+
const at = (y + 1) * side + x + 1;
|
|
80
|
+
changed[at] = changed[at - side] + rowChanged;
|
|
81
|
+
change[at] = change[at - side] + rowChange;
|
|
82
|
+
level[at] = level[at - side] + rowLevel;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let most = 0;
|
|
86
|
+
for (let wy = 0; wy < WINDOWS_PER_SIDE; wy++) {
|
|
87
|
+
for (let wx = 0; wx < WINDOWS_PER_SIDE; wx++) {
|
|
88
|
+
const window = wy * WINDOWS_PER_SIDE + wx;
|
|
89
|
+
const a = wy * side + wx;
|
|
90
|
+
const b = a + FIELD;
|
|
91
|
+
const c = a + FIELD * side;
|
|
92
|
+
const d = c + FIELD;
|
|
93
|
+
const area = changed[d] - changed[b] - changed[c] + changed[a];
|
|
94
|
+
const step = (change[d] - change[b] - change[c] + change[a]) / FIELD_CELLS;
|
|
95
|
+
const mean = (level[d] - level[b] - level[c] + level[a]) / FIELD_CELLS;
|
|
96
|
+
if (frame === 1) {
|
|
97
|
+
peakLevel[window] = mean - step;
|
|
98
|
+
valleyLevel[window] = mean - step;
|
|
99
|
+
}
|
|
100
|
+
const value = signal[window] + (area >= AREA ? step : 0);
|
|
101
|
+
signal[window] = value;
|
|
102
|
+
let transition = false;
|
|
103
|
+
if (direction[window] >= 0) {
|
|
104
|
+
if (value > peakValue[window]) {
|
|
105
|
+
peakValue[window] = value;
|
|
106
|
+
peakLevel[window] = mean;
|
|
107
|
+
} else if (peakValue[window] - value >= threshold) {
|
|
108
|
+
transition = Math.min(mean, peakLevel[window]) < darkLimit;
|
|
109
|
+
direction[window] = -1;
|
|
110
|
+
valleyValue[window] = value;
|
|
111
|
+
valleyLevel[window] = mean;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (direction[window] <= 0) {
|
|
115
|
+
if (value < valleyValue[window]) {
|
|
116
|
+
valleyValue[window] = value;
|
|
117
|
+
valleyLevel[window] = mean;
|
|
118
|
+
} else if (value - valleyValue[window] >= threshold) {
|
|
119
|
+
transition = Math.min(mean, valleyLevel[window]) < darkLimit;
|
|
120
|
+
direction[window] = 1;
|
|
121
|
+
peakValue[window] = value;
|
|
122
|
+
peakLevel[window] = mean;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const base = window * RING;
|
|
126
|
+
while (
|
|
127
|
+
tail[window] < head[window] &&
|
|
128
|
+
frame - ring[base + (tail[window] % RING)] >= ANALYSIS_FPS
|
|
129
|
+
)
|
|
130
|
+
tail[window]++;
|
|
131
|
+
if (transition) {
|
|
132
|
+
ring[base + (head[window] % RING)] = frame;
|
|
133
|
+
head[window]++;
|
|
134
|
+
}
|
|
135
|
+
const count = head[window] - tail[window];
|
|
136
|
+
if (count > most) most = count;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
peak = Math.max(peak, most);
|
|
140
|
+
if (most > MAX_TRANSITIONS) {
|
|
141
|
+
const start = Math.max(0, frame + 1 - ANALYSIS_FPS) / ANALYSIS_FPS;
|
|
142
|
+
const end = (frame + 1) / ANALYSIS_FPS;
|
|
143
|
+
const last = failures.at(-1);
|
|
144
|
+
if (last && start <= last.end) last.end = end;
|
|
145
|
+
else failures.push({ start, end });
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
summary() {
|
|
149
|
+
return {
|
|
150
|
+
maxFlashesPerSecond: peak / 2,
|
|
151
|
+
failures: failures.slice(0, 20).map(({ start, end }) => ({
|
|
152
|
+
start: Math.round(start * 100) / 100,
|
|
153
|
+
end: Math.round(end * 100) / 100,
|
|
154
|
+
})),
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Pure analysis over linear-light RGB frames of GRID × GRID cells (3 bytes per cell). */
|
|
161
|
+
export function createFlashAnalyzer() {
|
|
162
|
+
const general = createChannel(CHANNELS.general);
|
|
163
|
+
const red = createChannel(CHANNELS.red);
|
|
164
|
+
let luminance = new Float64Array(CELLS);
|
|
165
|
+
let redness = new Float64Array(CELLS);
|
|
166
|
+
let previousLuminance = new Float64Array(CELLS);
|
|
167
|
+
let previousRedness = new Float64Array(CELLS);
|
|
168
|
+
let frames = 0;
|
|
169
|
+
return {
|
|
170
|
+
push(frame) {
|
|
171
|
+
if (!(frame instanceof Uint8Array) || frame.length !== FRAME_BYTES)
|
|
172
|
+
throw new Error(`Expected ${FRAME_BYTES}-byte analysis frames`);
|
|
173
|
+
for (let cell = 0; cell < CELLS; cell++) {
|
|
174
|
+
const r = frame[cell * 3] / 255;
|
|
175
|
+
const g = frame[cell * 3 + 1] / 255;
|
|
176
|
+
const b = frame[cell * 3 + 2] / 255;
|
|
177
|
+
luminance[cell] = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
178
|
+
const total = r + g + b;
|
|
179
|
+
redness[cell] = total > 0 && r / total >= 0.8 ? Math.max(0, (r - g - b) * 320) : 0;
|
|
180
|
+
}
|
|
181
|
+
if (frames > 0) {
|
|
182
|
+
general.update(frames, previousLuminance, luminance);
|
|
183
|
+
red.update(frames, previousRedness, redness);
|
|
184
|
+
}
|
|
185
|
+
[previousLuminance, luminance] = [luminance, previousLuminance];
|
|
186
|
+
[previousRedness, redness] = [redness, previousRedness];
|
|
187
|
+
frames++;
|
|
188
|
+
},
|
|
189
|
+
result() {
|
|
190
|
+
const generalSummary = general.summary();
|
|
191
|
+
const redSummary = red.summary();
|
|
192
|
+
return {
|
|
193
|
+
ok: frames >= 2 && !generalSummary.failures.length && !redSummary.failures.length,
|
|
194
|
+
method: "rendered-pixels",
|
|
195
|
+
standard: "WCAG 2.3.1 general and red flash thresholds",
|
|
196
|
+
analysisFps: ANALYSIS_FPS,
|
|
197
|
+
frames,
|
|
198
|
+
duration: frames / ANALYSIS_FPS,
|
|
199
|
+
general: generalSummary,
|
|
200
|
+
red: redSummary,
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Decode a video into linear-light analysis frames and return the flash report. */
|
|
207
|
+
export async function analyzeFlashes(file, signal) {
|
|
208
|
+
const binary = await ffmpegBinary();
|
|
209
|
+
const child = spawn(
|
|
210
|
+
binary,
|
|
211
|
+
[
|
|
212
|
+
"-hide_banner",
|
|
213
|
+
"-nostdin",
|
|
214
|
+
"-loglevel",
|
|
215
|
+
"error",
|
|
216
|
+
"-xerror",
|
|
217
|
+
"-protocol_whitelist",
|
|
218
|
+
"file,pipe",
|
|
219
|
+
"-i",
|
|
220
|
+
file,
|
|
221
|
+
"-map",
|
|
222
|
+
"0:v:0",
|
|
223
|
+
"-an",
|
|
224
|
+
"-vf",
|
|
225
|
+
[
|
|
226
|
+
// Linearise before any averaging: shrinking sRGB values first darkens fine detail
|
|
227
|
+
// by up to half the flash threshold.
|
|
228
|
+
"format=gbrp",
|
|
229
|
+
`lutrgb=r='${LINEAR}':g='${LINEAR}':b='${LINEAR}'`,
|
|
230
|
+
`scale=${GRID}:${GRID}:flags=area`,
|
|
231
|
+
`fps=${ANALYSIS_FPS}`,
|
|
232
|
+
"format=rgb24",
|
|
233
|
+
].join(","),
|
|
234
|
+
"-f",
|
|
235
|
+
"rawvideo",
|
|
236
|
+
"-pix_fmt",
|
|
237
|
+
"rgb24",
|
|
238
|
+
"pipe:1",
|
|
239
|
+
],
|
|
240
|
+
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true, signal },
|
|
241
|
+
);
|
|
242
|
+
let stderr = "";
|
|
243
|
+
child.stderr.setEncoding("utf8");
|
|
244
|
+
child.stderr.on("data", (text) => {
|
|
245
|
+
if (stderr.length < 16_000) stderr += text;
|
|
246
|
+
});
|
|
247
|
+
const closed = once(child, "close");
|
|
248
|
+
closed.catch(() => {});
|
|
249
|
+
const analyzer = createFlashAnalyzer();
|
|
250
|
+
const frame = new Uint8Array(FRAME_BYTES);
|
|
251
|
+
let filled = 0;
|
|
252
|
+
for await (const chunk of child.stdout) {
|
|
253
|
+
let offset = 0;
|
|
254
|
+
while (offset < chunk.length) {
|
|
255
|
+
const take = Math.min(FRAME_BYTES - filled, chunk.length - offset);
|
|
256
|
+
frame.set(chunk.subarray(offset, offset + take), filled);
|
|
257
|
+
filled += take;
|
|
258
|
+
offset += take;
|
|
259
|
+
if (filled === FRAME_BYTES) {
|
|
260
|
+
analyzer.push(frame);
|
|
261
|
+
filled = 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const [code] = await closed;
|
|
266
|
+
if (code !== 0)
|
|
267
|
+
throw new Error(`FFmpeg could not decode the video: ${stderr.trim().slice(-2000)}`);
|
|
268
|
+
if (filled !== 0) throw new Error("Decoded video ended mid-frame; flashing is unverified");
|
|
269
|
+
const result = analyzer.result();
|
|
270
|
+
if (result.frames < 2) throw new Error("Too few decoded frames; flashing is unverified");
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function videoHash(file, signal) {
|
|
275
|
+
const hash = createHash("sha256");
|
|
276
|
+
for await (const chunk of createReadStream(file, { signal })) hash.update(chunk);
|
|
277
|
+
return hash.digest("hex");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function main() {
|
|
281
|
+
const [video, ...extra] = process.argv.slice(2);
|
|
282
|
+
if (!video || extra.length) throw new Error("usage: flash-check.mjs <rendered-video>");
|
|
283
|
+
const file = resolve(video);
|
|
284
|
+
if (!(await stat(file)).isFile()) throw new Error("Expected a local rendered video file");
|
|
285
|
+
const controller = new AbortController();
|
|
286
|
+
const abort = () => controller.abort();
|
|
287
|
+
process.once("SIGTERM", abort);
|
|
288
|
+
process.once("SIGINT", abort);
|
|
289
|
+
const started = performance.now();
|
|
290
|
+
try {
|
|
291
|
+
const videoSha256 = await videoHash(file, controller.signal);
|
|
292
|
+
const result = await analyzeFlashes(file, controller.signal);
|
|
293
|
+
if ((await videoHash(file, controller.signal)) !== videoSha256)
|
|
294
|
+
throw new Error("Video changed during analysis; flashing is unverified");
|
|
295
|
+
process.stdout.write(
|
|
296
|
+
`${JSON.stringify({ ...result, file, videoSha256, elapsedMs: Math.round(performance.now() - started) })}\n`,
|
|
297
|
+
);
|
|
298
|
+
if (!result.ok) process.exitCode = 1;
|
|
299
|
+
} finally {
|
|
300
|
+
process.removeListener("SIGTERM", abort);
|
|
301
|
+
process.removeListener("SIGINT", abort);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
306
|
+
try {
|
|
307
|
+
await main();
|
|
308
|
+
} catch (error) {
|
|
309
|
+
process.stderr.write(
|
|
310
|
+
`Flash verification failed: ${error instanceof Error ? error.message : "invalid input"}\n`,
|
|
311
|
+
);
|
|
312
|
+
process.exitCode = 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
// node fonts.mjs add <project-dir> <Family> [<Family> ...]
|
|
8
8
|
//
|
|
9
9
|
// `add` copies each family's woff2 files and OFL.txt into
|
|
10
|
-
// <project>/assets/fonts/<family
|
|
11
|
-
// with an @font-face for every bundled
|
|
12
|
-
//
|
|
10
|
+
// <project>/assets/fonts/<family>/, rewrites <project>/assets/fonts/fonts.css,
|
|
11
|
+
// and returns `head`: a <style> block with an @font-face for every bundled
|
|
12
|
+
// family installed there, using project-relative paths. Paste `head` into the
|
|
13
|
+
// composition <head>. `hf check` only sees @font-face written in the page, so a
|
|
14
|
+
// linked fonts.css renders but is reported as missing fonts.
|
|
13
15
|
// Prints one JSON line: {"ok":true,...} or {"ok":false,"error":...}.
|
|
14
16
|
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
15
17
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -23,15 +25,16 @@ function done(result, code = 0) {
|
|
|
23
25
|
}
|
|
24
26
|
const fail = (error) => done({ ok: false, error }, 1);
|
|
25
27
|
|
|
26
|
-
/** @returns {Promise<Array<{family:string,dir:string,license:string,files:Array<{file:string,style:string,weight:string,stretch:string|null}>}>>} */
|
|
28
|
+
/** @returns {Promise<Array<{family:string,dir:string,license:string,use?:string,files:Array<{file:string,style:string,weight:string,stretch:string|null}>}>>} */
|
|
27
29
|
async function manifest() {
|
|
28
30
|
return JSON.parse(await readFile(join(LIBRARY, "fonts.json"), "utf8"));
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
/** `base` is the font folder as seen from the file that holds the rule. */
|
|
34
|
+
function fontFace(entry, file, base = "") {
|
|
32
35
|
const lines = [
|
|
33
36
|
` font-family: "${entry.family}";`,
|
|
34
|
-
` src: url("${entry.dir}/${file.file}") format("woff2");`,
|
|
37
|
+
` src: url("${base}${entry.dir}/${file.file}") format("woff2");`,
|
|
35
38
|
` font-style: ${file.style};`,
|
|
36
39
|
` font-weight: ${file.weight};`,
|
|
37
40
|
];
|
|
@@ -52,6 +55,7 @@ if (command === "list") {
|
|
|
52
55
|
styles: f.files.map((file) => file.style),
|
|
53
56
|
weight: f.files[0]?.weight,
|
|
54
57
|
width: f.files[0]?.stretch ?? null,
|
|
58
|
+
...(f.use ? { use: f.use } : {}),
|
|
55
59
|
})),
|
|
56
60
|
});
|
|
57
61
|
}
|
|
@@ -93,15 +97,15 @@ for (const entry of fonts) {
|
|
|
93
97
|
);
|
|
94
98
|
if (present) installed.push(entry);
|
|
95
99
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await writeFile(join(target, "fonts.css"), `${
|
|
100
|
+
const rules = (base) =>
|
|
101
|
+
installed.flatMap((entry) => entry.files.map((file) => fontFace(entry, file, base)));
|
|
102
|
+
const header =
|
|
103
|
+
"/* Generated by EZ Motion fonts.mjs. SIL OFL 1.1: licenses sit beside each family. */";
|
|
104
|
+
await writeFile(join(target, "fonts.css"), `${[header, ...rules("")].join("\n\n")}\n`);
|
|
101
105
|
|
|
102
106
|
done({
|
|
103
107
|
ok: true,
|
|
104
108
|
installed: installed.map((f) => f.family),
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
head: `<style>\n${[header, ...rules("assets/fonts/")].join("\n")}\n</style>`,
|
|
110
|
+
use: "Paste head into the composition <head>, replacing any earlier fonts block.",
|
|
107
111
|
});
|
|
@@ -46,14 +46,16 @@ async function projectDir(arg) {
|
|
|
46
46
|
return dir;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/** Returns the installed families and fonts.mjs's page-ready <style> block. */
|
|
49
50
|
function installFonts(project, families) {
|
|
50
|
-
if (families.length === 0) return [];
|
|
51
|
+
if (families.length === 0) return { installed: [], head: [] };
|
|
51
52
|
const out = execFileSync(process.execPath, [join(BIN, "fonts.mjs"), "add", project, ...families], {
|
|
52
53
|
encoding: "utf8",
|
|
53
54
|
});
|
|
54
55
|
const result = JSON.parse(out);
|
|
55
56
|
if (!result.ok) fail(`Font install failed: ${result.error}`);
|
|
56
|
-
|
|
57
|
+
// Inline, not a linked fonts.css: `hf check` only sees @font-face written in the page.
|
|
58
|
+
return { installed: result.installed, head: [result.head] };
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const summary = (entry, type) => ({
|
|
@@ -135,11 +137,9 @@ if (command === "look") {
|
|
|
135
137
|
ok: true,
|
|
136
138
|
look: look.id,
|
|
137
139
|
tokens: css,
|
|
138
|
-
fonts,
|
|
139
|
-
head: [
|
|
140
|
-
|
|
141
|
-
`<link rel="stylesheet" href="assets/looks/${look.id}.css" />`,
|
|
142
|
-
],
|
|
140
|
+
fonts: fonts.installed,
|
|
141
|
+
head: [...fonts.head, `<link rel="stylesheet" href="assets/looks/${look.id}.css" />`],
|
|
142
|
+
use: "Paste head into the composition <head>, replacing any earlier fonts block.",
|
|
143
143
|
root: `add class="look-${look.id}" to the root composition element`,
|
|
144
144
|
spec: join(LIBRARY, "looks", look.id, "look.md"),
|
|
145
145
|
});
|
|
@@ -196,7 +196,7 @@ if (command === "add") {
|
|
|
196
196
|
ok: true,
|
|
197
197
|
added,
|
|
198
198
|
kept,
|
|
199
|
-
fonts,
|
|
199
|
+
fonts: fonts.installed,
|
|
200
200
|
...(importmap ? { importmap, note: "put importmap in index.html <head> before any module script" } : {}),
|
|
201
201
|
mount: pieces.map(
|
|
202
202
|
(p) =>
|
|
@@ -102,12 +102,18 @@ export function parseMotionOutput(text, slideshowRequested = false, holds = [])
|
|
|
102
102
|
error:
|
|
103
103
|
"Holds cannot exempt the entire video; explicitly requested slideshows use their own mode",
|
|
104
104
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
const staleHolds = holds.filter(
|
|
106
|
+
(hold) => !freezes.some((freeze) => freeze.end > hold.start && freeze.start < hold.end),
|
|
107
|
+
);
|
|
108
|
+
// Name the stale holds and where pixels actually freeze, so the plan is fixed in one
|
|
109
|
+
// step instead of guessed again against another full check.
|
|
110
|
+
if (staleHolds.length)
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
error: "Stale hold declaration: no detected freeze overlaps its window",
|
|
114
|
+
staleHolds,
|
|
115
|
+
freezes,
|
|
116
|
+
};
|
|
111
117
|
const unexpectedFreezes = freezes.filter(
|
|
112
118
|
(freeze) =>
|
|
113
119
|
!holds.some(
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright (c) 2011 by Ralph du Carrois, with Reserved Font Name 'Finger Paint'
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|