hyper-animator-codex 0.8.1 → 0.8.3
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 +10 -8
- package/bin/hyper-animator-codex.mjs +4 -4
- package/lib/install-skill.mjs +49 -20
- package/lib/minimax-config.mjs +5 -9
- package/package.json +7 -2
- package/skills/hyper-animator-codex/SKILL.md +11 -9
- package/skills/hyper-animator-codex/contracts/shot-plan.schema.json +45 -5
- package/skills/hyper-animator-codex/contracts/workflow-policy.json +8 -2
- package/skills/hyper-animator-codex/references/examples/shot-plan-dual-catalog.json +9 -3
- package/skills/hyper-animator-codex/references/hyperframes-intent-workflow.md +4 -2
- package/skills/hyper-animator-codex/references/minimax-music-workflow.md +5 -7
- package/skills/hyper-animator-codex/references/narration-audio-workflow.md +3 -4
- package/skills/hyper-animator-codex/references/preview-controls-workflow.md +4 -2
- package/skills/hyper-animator-codex/scripts/generate_minimax_music.mjs +11 -6
- package/skills/hyper-animator-codex/scripts/generate_minimax_tts.mjs +20 -8
- package/skills/hyper-animator-codex/scripts/git_checkpoint.mjs +27 -1
- package/skills/hyper-animator-codex/scripts/inject_preview_controls.mjs +37 -1
- package/skills/hyper-animator-codex/scripts/minimax_runtime_config.mjs +5 -9
- package/skills/hyper-animator-codex/scripts/query_shotcraft.mjs +10 -1
- package/skills/hyper-animator-codex/scripts/shot_plan_schema_validator.mjs +3 -0
- package/skills/hyper-animator-codex/scripts/validate_hyperframes_html.py +122 -36
- package/skills/hyper-animator-codex/scripts/validate_shot_plan.mjs +28 -8
|
@@ -4,27 +4,72 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
|
+
import json
|
|
7
8
|
import re
|
|
8
9
|
import sys
|
|
10
|
+
from html.parser import HTMLParser
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
MOTION_HELPERS = {
|
|
15
|
+
"slam-entrance": "slamEntrance",
|
|
16
|
+
"whip-transition": "whipTransition",
|
|
17
|
+
"crash-zoom": "crashZoom",
|
|
18
|
+
"push-stack-wipe": "pushStackWipe",
|
|
19
|
+
"dataviz-landscape-open": "datavizLandscapeOpen",
|
|
20
|
+
"panel-grid-reflow": "panelGridReflow",
|
|
21
|
+
"brand-resolve": "brandResolve",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CompositionParser(HTMLParser):
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
super().__init__(convert_charrefs=True)
|
|
28
|
+
self.elements: list[tuple[str, dict[str, str | None]]] = []
|
|
29
|
+
self.scripts: list[str] = []
|
|
30
|
+
self.styles: list[str] = []
|
|
31
|
+
self.comments: list[str] = []
|
|
32
|
+
self._raw_tag: str | None = None
|
|
33
|
+
self._raw_text: list[str] = []
|
|
34
|
+
|
|
35
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
36
|
+
self.elements.append((tag.lower(), {name.lower(): value for name, value in attrs}))
|
|
37
|
+
if tag.lower() in ("script", "style"):
|
|
38
|
+
self._raw_tag = tag.lower()
|
|
39
|
+
self._raw_text = []
|
|
40
|
+
|
|
41
|
+
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None:
|
|
42
|
+
self.handle_starttag(tag, attrs)
|
|
43
|
+
self.handle_endtag(tag)
|
|
44
|
+
|
|
45
|
+
def handle_data(self, data: str) -> None:
|
|
46
|
+
if self._raw_tag:
|
|
47
|
+
self._raw_text.append(data)
|
|
48
|
+
|
|
49
|
+
def handle_endtag(self, tag: str) -> None:
|
|
50
|
+
if tag.lower() != self._raw_tag:
|
|
51
|
+
return
|
|
52
|
+
target = self.scripts if self._raw_tag == "script" else self.styles
|
|
53
|
+
target.append("".join(self._raw_text))
|
|
54
|
+
self._raw_tag = None
|
|
55
|
+
self._raw_text = []
|
|
56
|
+
|
|
57
|
+
def handle_comment(self, data: str) -> None:
|
|
58
|
+
self.comments.append(data)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_html(html: str) -> CompositionParser:
|
|
62
|
+
parser = CompositionParser()
|
|
63
|
+
parser.feed(html)
|
|
64
|
+
parser.close()
|
|
65
|
+
return parser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def positive_number(value: str | None) -> bool:
|
|
24
69
|
try:
|
|
25
|
-
return float(
|
|
70
|
+
return value is not None and float(value) > 0
|
|
26
71
|
except ValueError:
|
|
27
|
-
return
|
|
72
|
+
return False
|
|
28
73
|
|
|
29
74
|
|
|
30
75
|
def check_html(
|
|
@@ -32,70 +77,98 @@ def check_html(
|
|
|
32
77
|
*,
|
|
33
78
|
component: bool = False,
|
|
34
79
|
preview_controls: bool = False,
|
|
80
|
+
shot_plan: dict | None = None,
|
|
35
81
|
) -> tuple[list[str], list[str]]:
|
|
36
82
|
failures: list[str] = []
|
|
37
83
|
warnings: list[str] = []
|
|
84
|
+
document = parse_html(html)
|
|
85
|
+
script = "\n".join(document.scripts)
|
|
86
|
+
style = "\n".join(document.styles)
|
|
87
|
+
composition_elements = [
|
|
88
|
+
attrs for _, attrs in document.elements if attrs.get("data-composition-id")
|
|
89
|
+
]
|
|
38
90
|
|
|
39
91
|
if not component:
|
|
92
|
+
composition = composition_elements[0] if composition_elements else {}
|
|
40
93
|
for attr in ("data-width", "data-height", "data-duration", "data-composition-id"):
|
|
41
|
-
if not
|
|
94
|
+
if not composition.get(attr):
|
|
42
95
|
failures.append(f"missing {attr}")
|
|
43
96
|
for attr in ("data-width", "data-height", "data-duration"):
|
|
44
|
-
if
|
|
45
|
-
|
|
46
|
-
if value is None or value <= 0:
|
|
47
|
-
failures.append(f"{attr} must be a positive number")
|
|
97
|
+
if attr in composition and not positive_number(composition.get(attr)):
|
|
98
|
+
failures.append(f"{attr} must be a positive number")
|
|
48
99
|
|
|
49
|
-
if "
|
|
100
|
+
if any("paste from " in comment.lower() for comment in document.comments):
|
|
50
101
|
failures.append("unresolved component paste placeholder")
|
|
51
102
|
|
|
52
103
|
paused_timeline = re.search(
|
|
53
104
|
r"gsap\s*\.\s*timeline\s*\(\s*\{[^}]*paused\s*:\s*true",
|
|
54
|
-
|
|
105
|
+
script,
|
|
55
106
|
re.IGNORECASE | re.DOTALL,
|
|
56
107
|
)
|
|
57
108
|
if not paused_timeline:
|
|
58
109
|
failures.append("missing gsap.timeline({ paused: true })")
|
|
59
110
|
|
|
60
|
-
if "window.__timelines" not in
|
|
111
|
+
if "window.__timelines" not in script:
|
|
61
112
|
failures.append("missing window.__timelines registration")
|
|
62
113
|
|
|
63
|
-
if re.search(r"\bDate\s*\.\s*now\s*\(",
|
|
114
|
+
if re.search(r"\bDate\s*\.\s*now\s*\(", script):
|
|
64
115
|
failures.append("uses Date.now(); render progress must be timeline-driven")
|
|
65
116
|
|
|
66
|
-
if re.search(r"\bsetInterval\s*\(",
|
|
117
|
+
if re.search(r"\bsetInterval\s*\(", script):
|
|
67
118
|
failures.append("uses setInterval(); primary render progress must be timeline-driven")
|
|
68
119
|
|
|
69
|
-
if re.search(r"\bMath\s*\.\s*random\s*\(",
|
|
120
|
+
if re.search(r"\bMath\s*\.\s*random\s*\(", script):
|
|
70
121
|
failures.append("uses Math.random(); visual generation must use a fixed-seed deterministic PRNG")
|
|
71
122
|
|
|
72
|
-
if re.search(r"\brequestAnimationFrame\s*\(",
|
|
123
|
+
if re.search(r"\brequestAnimationFrame\s*\(", script) and "fallback" not in script.lower():
|
|
73
124
|
warnings.append("requestAnimationFrame found; ensure it is not the primary render clock and has render fallback")
|
|
74
125
|
|
|
75
|
-
if
|
|
126
|
+
if document.styles and "[data-composition-id" not in style:
|
|
76
127
|
warnings.append("style block found without [data-composition-id] scoped selector")
|
|
77
128
|
|
|
78
|
-
if
|
|
129
|
+
if sum("data-duration" in attrs for _, attrs in document.elements) > 1:
|
|
79
130
|
warnings.append("multiple data-duration values found; verify the intended render duration")
|
|
80
131
|
|
|
81
132
|
if preview_controls:
|
|
82
|
-
|
|
133
|
+
attrs = [attributes for _, attributes in document.elements]
|
|
134
|
+
if not any("data-hyper-preview-ui" in attributes for attributes in attrs):
|
|
83
135
|
failures.append("missing preview controls: data-hyper-preview-ui")
|
|
84
|
-
if "data-hyper-preview-page"
|
|
136
|
+
if not any("data-hyper-preview-page" in attributes for attributes in attrs):
|
|
85
137
|
failures.append("missing preview controls: page indicator")
|
|
86
|
-
if "data-hyper-preview-progress"
|
|
138
|
+
if not any("data-hyper-preview-progress" in attributes for attributes in attrs):
|
|
87
139
|
failures.append("missing preview controls: progress input")
|
|
88
|
-
if not
|
|
140
|
+
if not any(tag == "input" and attributes.get("type") == "range" for tag, attributes in document.elements):
|
|
89
141
|
failures.append("missing preview controls: range input")
|
|
90
|
-
if "ArrowLeft" not in
|
|
142
|
+
if "ArrowLeft" not in script or "ArrowRight" not in script:
|
|
91
143
|
failures.append("missing preview controls: left/right keyboard handlers")
|
|
92
|
-
if 'params.get("render") === "1"' not in
|
|
144
|
+
if 'params.get("render") === "1"' not in script:
|
|
93
145
|
failures.append("missing preview controls: ?render=1 hidden-mode check")
|
|
94
|
-
if 'params.get("preview") === "0"' not in
|
|
146
|
+
if 'params.get("preview") === "0"' not in script:
|
|
95
147
|
failures.append("missing preview controls: ?preview=0 hidden-mode check")
|
|
96
|
-
if 'dataset.renderMode === "video"' not in
|
|
148
|
+
if 'dataset.renderMode === "video"' not in script:
|
|
97
149
|
failures.append("missing preview controls: data-render-mode video hidden-mode check")
|
|
98
150
|
|
|
151
|
+
if shot_plan is not None:
|
|
152
|
+
shot_elements = {
|
|
153
|
+
attrs.get("data-shot-id"): attrs
|
|
154
|
+
for _, attrs in document.elements
|
|
155
|
+
if attrs.get("data-shot-id")
|
|
156
|
+
}
|
|
157
|
+
for shot in shot_plan.get("shots", []):
|
|
158
|
+
shot_id = shot.get("id")
|
|
159
|
+
preset = shot.get("motion_contract", {}).get("motion_preset")
|
|
160
|
+
attrs = shot_elements.get(shot_id)
|
|
161
|
+
if attrs is None:
|
|
162
|
+
failures.append(f"missing data-shot-id element for shot: {shot_id}")
|
|
163
|
+
continue
|
|
164
|
+
if attrs.get("data-motion-preset") != preset:
|
|
165
|
+
failures.append(f"shot {shot_id} data-motion-preset must be {preset}")
|
|
166
|
+
helper = MOTION_HELPERS.get(preset)
|
|
167
|
+
if helper:
|
|
168
|
+
calls_only = re.sub(rf"\bfunction\s+{re.escape(helper)}\s*\(", "", script)
|
|
169
|
+
if not re.search(rf"\b{re.escape(helper)}\s*\(", calls_only):
|
|
170
|
+
failures.append(f"shot {shot_id} must call motion-pack helper {helper}")
|
|
171
|
+
|
|
99
172
|
return failures, warnings
|
|
100
173
|
|
|
101
174
|
|
|
@@ -112,6 +185,10 @@ def main() -> int:
|
|
|
112
185
|
action="store_true",
|
|
113
186
|
help="Require injected Hyper Animator preview controls",
|
|
114
187
|
)
|
|
188
|
+
parser.add_argument(
|
|
189
|
+
"--shot-plan",
|
|
190
|
+
help="Cross-check data-shot-id and motion-pack usage against a shot-plan JSON file",
|
|
191
|
+
)
|
|
115
192
|
args = parser.parse_args()
|
|
116
193
|
|
|
117
194
|
path = Path(args.html_file)
|
|
@@ -120,10 +197,19 @@ def main() -> int:
|
|
|
120
197
|
return 2
|
|
121
198
|
|
|
122
199
|
html = path.read_text(encoding="utf-8")
|
|
200
|
+
shot_plan = None
|
|
201
|
+
if args.shot_plan:
|
|
202
|
+
try:
|
|
203
|
+
shot_plan = json.loads(Path(args.shot_plan).read_text(encoding="utf-8"))
|
|
204
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
205
|
+
print(f"FAIL: cannot read shot plan: {error}", file=sys.stderr)
|
|
206
|
+
return 2
|
|
207
|
+
|
|
123
208
|
failures, warnings = check_html(
|
|
124
209
|
html,
|
|
125
210
|
component=args.component,
|
|
126
211
|
preview_controls=args.preview_controls,
|
|
212
|
+
shot_plan=shot_plan,
|
|
127
213
|
)
|
|
128
214
|
|
|
129
215
|
for warning in warnings:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import validateSchema, { schemaSha256 } from "./shot_plan_schema_validator.mjs";
|
|
5
7
|
|
|
6
8
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
7
9
|
const skillRoot = dirname(scriptDir);
|
|
@@ -67,6 +69,9 @@ function validateMotionContract(contract, label, shotcraftRefs, errors) {
|
|
|
67
69
|
const expectedPresets = shotcraftRefs
|
|
68
70
|
.map((reference) => SHOTCRAFT_MOTION_PRESETS.get(reference))
|
|
69
71
|
.filter(Boolean);
|
|
72
|
+
if (new Set(expectedPresets).size > 1) {
|
|
73
|
+
errors.push(`${label}.shotcraft_refs require multiple motion presets; separate them into distinct shots`);
|
|
74
|
+
}
|
|
70
75
|
if (expectedPresets.length > 0 && !expectedPresets.includes(contract.motion_preset)) {
|
|
71
76
|
errors.push(`${label}.motion_contract.motion_preset must use ${expectedPresets.join(" or ")} for selected ShotCraft refs`);
|
|
72
77
|
}
|
|
@@ -96,7 +101,7 @@ function validateShotPlan(plan, catalogs) {
|
|
|
96
101
|
const errors = [];
|
|
97
102
|
|
|
98
103
|
if (!isObject(plan)) return ["plan must be a JSON object"];
|
|
99
|
-
if (plan.schema_version !== "1.
|
|
104
|
+
if (plan.schema_version !== "1.1.0") errors.push("schema_version must be 1.1.0");
|
|
100
105
|
if (!WORKFLOW_STAGES.has(plan.workflow_stage)) {
|
|
101
106
|
errors.push("workflow_stage must be visual_draft, audio_integrated, or render_ready");
|
|
102
107
|
}
|
|
@@ -132,13 +137,15 @@ function validateShotPlan(plan, catalogs) {
|
|
|
132
137
|
if (!isObject(plan.model_routing)) {
|
|
133
138
|
errors.push("model_routing must be an object");
|
|
134
139
|
} else {
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
const authoring = plan.model_routing.page_animation_authoring;
|
|
141
|
+
const planning = plan.model_routing.planning_and_validation;
|
|
142
|
+
if (!isObject(authoring) || authoring.tier !== "high_quality" || !authoring.actual_model) {
|
|
143
|
+
errors.push("model_routing.page_animation_authoring must declare high_quality tier and actual_model");
|
|
137
144
|
}
|
|
138
|
-
if (!
|
|
139
|
-
errors.push("model_routing.planning_and_validation
|
|
140
|
-
} else if (
|
|
141
|
-
errors.push("model_routing.planning_and_validation must
|
|
145
|
+
if (!isObject(planning) || planning.tier !== "fast" || !planning.actual_model) {
|
|
146
|
+
errors.push("model_routing.planning_and_validation must declare fast tier and actual_model");
|
|
147
|
+
} else if (authoring?.actual_model === planning.actual_model) {
|
|
148
|
+
errors.push("model_routing.planning_and_validation must use a different, fast model");
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
151
|
|
|
@@ -231,6 +238,14 @@ async function main() {
|
|
|
231
238
|
return;
|
|
232
239
|
}
|
|
233
240
|
const plan = JSON.parse(await readFile(path, "utf8"));
|
|
241
|
+
const schemaText = await readFile(
|
|
242
|
+
join(skillRoot, "contracts", "shot-plan.schema.json"),
|
|
243
|
+
"utf8",
|
|
244
|
+
);
|
|
245
|
+
const actualSchemaSha256 = createHash("sha256").update(schemaText).digest("hex");
|
|
246
|
+
if (actualSchemaSha256 !== schemaSha256) {
|
|
247
|
+
throw new Error("shot-plan schema validator is stale; run npm run build:contracts");
|
|
248
|
+
}
|
|
234
249
|
const hyperframesCatalog = JSON.parse(await readFile(
|
|
235
250
|
join(skillRoot, "references", "hyperframes-catalog-map.json"),
|
|
236
251
|
"utf8",
|
|
@@ -246,7 +261,12 @@ async function main() {
|
|
|
246
261
|
...card.styles.map((style) => style.key),
|
|
247
262
|
])),
|
|
248
263
|
};
|
|
249
|
-
const
|
|
264
|
+
const schemaErrors = validateSchema(plan)
|
|
265
|
+
? []
|
|
266
|
+
: validateSchema.errors.map((error) => (
|
|
267
|
+
`${error.instancePath || "/"} ${error.message}${error.params?.additionalProperty ? `: ${error.params.additionalProperty}` : ""}`
|
|
268
|
+
));
|
|
269
|
+
const errors = [...schemaErrors, ...validateShotPlan(plan, catalogs)];
|
|
250
270
|
if (errors.length > 0) {
|
|
251
271
|
output({ ok: false, path, errors }, 1);
|
|
252
272
|
return;
|