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.
Files changed (22) hide show
  1. package/README.md +10 -8
  2. package/bin/hyper-animator-codex.mjs +4 -4
  3. package/lib/install-skill.mjs +49 -20
  4. package/lib/minimax-config.mjs +5 -9
  5. package/package.json +7 -2
  6. package/skills/hyper-animator-codex/SKILL.md +11 -9
  7. package/skills/hyper-animator-codex/contracts/shot-plan.schema.json +45 -5
  8. package/skills/hyper-animator-codex/contracts/workflow-policy.json +8 -2
  9. package/skills/hyper-animator-codex/references/examples/shot-plan-dual-catalog.json +9 -3
  10. package/skills/hyper-animator-codex/references/hyperframes-intent-workflow.md +4 -2
  11. package/skills/hyper-animator-codex/references/minimax-music-workflow.md +5 -7
  12. package/skills/hyper-animator-codex/references/narration-audio-workflow.md +3 -4
  13. package/skills/hyper-animator-codex/references/preview-controls-workflow.md +4 -2
  14. package/skills/hyper-animator-codex/scripts/generate_minimax_music.mjs +11 -6
  15. package/skills/hyper-animator-codex/scripts/generate_minimax_tts.mjs +20 -8
  16. package/skills/hyper-animator-codex/scripts/git_checkpoint.mjs +27 -1
  17. package/skills/hyper-animator-codex/scripts/inject_preview_controls.mjs +37 -1
  18. package/skills/hyper-animator-codex/scripts/minimax_runtime_config.mjs +5 -9
  19. package/skills/hyper-animator-codex/scripts/query_shotcraft.mjs +10 -1
  20. package/skills/hyper-animator-codex/scripts/shot_plan_schema_validator.mjs +3 -0
  21. package/skills/hyper-animator-codex/scripts/validate_hyperframes_html.py +122 -36
  22. 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
- def has_attr(html: str, name: str) -> bool:
13
- return re.search(rf"\b{name}\s*=\s*['\"]?[^'\"\s>]+", html, re.IGNORECASE) is not None
14
-
15
-
16
- def numeric_attr(html: str, name: str) -> float | None:
17
- match = re.search(
18
- rf"\b{name}\s*=\s*(?:['\"]([^'\"]+)['\"]|([^\s>]+))",
19
- html,
20
- re.IGNORECASE,
21
- )
22
- if not match:
23
- return None
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(match.group(1) or match.group(2))
70
+ return value is not None and float(value) > 0
26
71
  except ValueError:
27
- return None
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 has_attr(html, attr):
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 has_attr(html, attr):
45
- value = numeric_attr(html, attr)
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 "<!-- paste from " in html:
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
- html,
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 html:
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*\(", html):
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*\(", html):
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*\(", html):
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*\(", html) and "fallback" not in html.lower():
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 "<style" in html.lower() and "[data-composition-id" not in html:
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 len(re.findall(r"data-duration\s*=\s*['\"]?(\d+(?:\.\d+)?)", html)) > 1:
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
- if "data-hyper-preview-ui" not in html:
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" not in html:
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" not in html:
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 re.search(r"<input[^>]+type=[\"']range[\"']", html, re.IGNORECASE | re.DOTALL):
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 html or "ArrowRight" not in html:
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 html:
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 html:
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 html:
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.0.0") errors.push("schema_version must be 1.0.0");
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
- if (plan.model_routing.page_animation_authoring !== "gpt-5.6-sol") {
136
- errors.push("model_routing.page_animation_authoring must be gpt-5.6-sol");
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 (!plan.model_routing.planning_and_validation) {
139
- errors.push("model_routing.planning_and_validation is required");
140
- } else if (plan.model_routing.planning_and_validation === "gpt-5.6-sol") {
141
- errors.push("model_routing.planning_and_validation must not use gpt-5.6-sol");
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 errors = validateShotPlan(plan, catalogs);
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;