hyper-animator-codex 0.8.0 → 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 (25) hide show
  1. package/README.md +30 -13
  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 +10 -2
  6. package/skills/hyper-animator-codex/SKILL.md +33 -25
  7. package/skills/hyper-animator-codex/agents/openai.yaml +2 -2
  8. package/skills/hyper-animator-codex/contracts/shot-plan.schema.json +113 -7
  9. package/skills/hyper-animator-codex/contracts/workflow-policy.json +21 -3
  10. package/skills/hyper-animator-codex/references/examples/shot-plan-dual-catalog.json +41 -5
  11. package/skills/hyper-animator-codex/references/hyperframes-intent-workflow.md +20 -10
  12. package/skills/hyper-animator-codex/references/minimax-music-workflow.md +5 -7
  13. package/skills/hyper-animator-codex/references/narration-audio-workflow.md +20 -14
  14. package/skills/hyper-animator-codex/references/preview-controls-workflow.md +4 -2
  15. package/skills/hyper-animator-codex/references/shotcraft/motion-pack/README.md +32 -0
  16. package/skills/hyper-animator-codex/references/shotcraft/motion-pack/shotcraft-motion-pack.js +178 -0
  17. package/skills/hyper-animator-codex/scripts/generate_minimax_music.mjs +11 -6
  18. package/skills/hyper-animator-codex/scripts/generate_minimax_tts.mjs +20 -8
  19. package/skills/hyper-animator-codex/scripts/git_checkpoint.mjs +29 -3
  20. package/skills/hyper-animator-codex/scripts/inject_preview_controls.mjs +37 -1
  21. package/skills/hyper-animator-codex/scripts/minimax_runtime_config.mjs +5 -9
  22. package/skills/hyper-animator-codex/scripts/query_shotcraft.mjs +10 -1
  23. package/skills/hyper-animator-codex/scripts/shot_plan_schema_validator.mjs +3 -0
  24. package/skills/hyper-animator-codex/scripts/validate_hyperframes_html.py +122 -36
  25. package/skills/hyper-animator-codex/scripts/validate_shot_plan.mjs +110 -17
@@ -5,6 +5,7 @@
5
5
  "generated_narration": {
6
6
  "provider": "minimax_cn",
7
7
  "endpoint": "https://api.minimaxi.com/v1/t2a_v2",
8
+ "stage_after": "visual_confirmation",
8
9
  "backup_endpoints": [
9
10
  "https://api-bj.minimaxi.com/v1/t2a_v2"
10
11
  ],
@@ -26,10 +27,14 @@
26
27
  ],
27
28
  "forbidden_commands": [
28
29
  "hyperframes auth login",
29
- "hyperframes tts"
30
+ "hyperframes tts",
31
+ "python3 -m pip install librosa",
32
+ "pip3 install librosa",
33
+ "pip install librosa"
30
34
  ],
31
35
  "automatic_fallback": false,
32
36
  "automatic_runtime_install": false,
37
+ "automatic_dependency_install": false,
33
38
  "failure_choices": [
34
39
  "retry_minimax_cn",
35
40
  "use_user_local_file",
@@ -42,14 +47,20 @@
42
47
  },
43
48
  "model_routing": {
44
49
  "page_animation_authoring": {
45
- "model": "gpt-5.6-sol",
50
+ "tier": "high_quality",
51
+ "preferred_models": {
52
+ "codex": "gpt-5.6-sol"
53
+ },
46
54
  "use_for": [
47
55
  "page_generation",
48
56
  "animation_authoring"
49
57
  ]
50
58
  },
51
59
  "planning_and_validation": {
52
- "model": "gpt-5.6-terra",
60
+ "tier": "fast",
61
+ "preferred_models": {
62
+ "codex": "gpt-5.6-terra"
63
+ },
53
64
  "use_for": [
54
65
  "requirements",
55
66
  "file_detection",
@@ -69,6 +80,13 @@
69
80
  "default_statuses": [
70
81
  "implemented"
71
82
  ],
83
+ "motion_pack_path": "references/shotcraft/motion-pack/README.md",
84
+ "motion_pack_snippet": "references/shotcraft/motion-pack/shotcraft-motion-pack.js",
85
+ "prefer_motion_pack_before_freehand": true,
86
+ "motion_pack_required_for_supported_refs": true,
87
+ "motion_contract_required": true,
88
+ "minimum_commitments_per_shot": 1,
89
+ "read_selected_cards_before_html": true,
72
90
  "excluded_asset_extensions": [
73
91
  ".mp4",
74
92
  ".mov",
@@ -1,5 +1,6 @@
1
1
  {
2
- "schema_version": "1.0.0",
2
+ "schema_version": "1.1.0",
3
+ "workflow_stage": "visual_draft",
3
4
  "generation_mode": "generate_new_hyperframes_html",
4
5
  "catalog_strategy": "dual_catalog",
5
6
  "dimensions": {
@@ -9,8 +10,7 @@
9
10
  "duration_seconds": 12,
10
11
  "audio": {
11
12
  "narration": {
12
- "mode": "generated",
13
- "provider": "minimax_cn"
13
+ "mode": "deferred"
14
14
  },
15
15
  "bgm": {
16
16
  "mode": "generated",
@@ -18,8 +18,14 @@
18
18
  }
19
19
  },
20
20
  "model_routing": {
21
- "page_animation_authoring": "gpt-5.6-sol",
22
- "planning_and_validation": "gpt-5.6-terra"
21
+ "page_animation_authoring": {
22
+ "tier": "high_quality",
23
+ "actual_model": "gpt-5.6-sol"
24
+ },
25
+ "planning_and_validation": {
26
+ "tier": "fast",
27
+ "actual_model": "gpt-5.6-terra"
28
+ }
23
29
  },
24
30
  "shots": [
25
31
  {
@@ -33,6 +39,16 @@
33
39
  "shotcraft_refs": [
34
40
  "scene-locked-title"
35
41
  ],
42
+ "motion_contract": {
43
+ "motion_preset": "none",
44
+ "energy": "cinematic",
45
+ "camera": "slow crane rise from a title detail to the full product stage",
46
+ "transition": "full-frame light wipe into the proof shot",
47
+ "shotcraft_commitments": [
48
+ "keep the title locked and readable before the camera travel opens the scene"
49
+ ],
50
+ "readable_rest_seconds": 0.6
51
+ },
36
52
  "narration_scene": "1"
37
53
  },
38
54
  {
@@ -46,6 +62,16 @@
46
62
  "shotcraft_refs": [
47
63
  "ai-stream-response"
48
64
  ],
65
+ "motion_contract": {
66
+ "motion_preset": "none",
67
+ "energy": "high",
68
+ "camera": "locked readable screen with short punch-ins on key rows",
69
+ "transition": "snap to the final proof state on a clear beat",
70
+ "shotcraft_commitments": [
71
+ "stream response rows progressively instead of fading static copy"
72
+ ],
73
+ "readable_rest_seconds": 0.5
74
+ },
49
75
  "narration_scene": "2"
50
76
  },
51
77
  {
@@ -59,6 +85,16 @@
59
85
  "shotcraft_refs": [
60
86
  "ui-strip-away-outro"
61
87
  ],
88
+ "motion_contract": {
89
+ "motion_preset": "none",
90
+ "energy": "cinematic",
91
+ "camera": "wide resolve with a final centered brand hold",
92
+ "transition": "strip away interface fragments into the outro mark",
93
+ "shotcraft_commitments": [
94
+ "resolve dense motion into a clean brand hold for at least one second"
95
+ ],
96
+ "readable_rest_seconds": 1
97
+ },
62
98
  "narration_scene": "3"
63
99
  }
64
100
  ],
@@ -19,23 +19,28 @@
19
19
  Transition、可选 Outro,以及每个镜头的 ShotCraft 参考。
20
20
  6. 判断 generation mode:组装现有 catalog,还是生成新 HTML。
21
21
  7. 调用 `AskUserQuestion` 第二轮澄清样式、动效、候选取舍,以及 generation mode。
22
- 8. 若选择 `assemble_existing_catalog_items`,生成 wrapper composition,并解析 component paste 指令为真实 snippet。
23
- 9. 若选择 `generate_new_hyperframes_html`,按
22
+ 8. 写出 `workflow_stage: visual_draft` shot plan。每个镜头必须包含
23
+ `motion_contract.motion_preset` `motion_contract.shotcraft_commitments`,
24
+ 把 ShotCraft 卡片的镜头、转场、节奏和避坑要求变成可验证条目。若最终需要
25
+ 旁白,此阶段使用 `audio.narration.mode: deferred`。
26
+ 9. 运行 `scripts/validate_shot_plan.mjs shot-plan.json`。
27
+ 10. 若选择 `assemble_existing_catalog_items`,生成 wrapper composition,并解析 component paste 指令为真实 snippet。
28
+ 11. 若选择 `generate_new_hyperframes_html`,按
24
29
  `references/HyperFrames-AI-Generation-Patterns-codex.md` 生成完整
25
30
  block/component HTML;ShotCraft TSX 只作为动效参考,不引入 Remotion
26
31
  运行时。
27
- 10. `contracts/shot-plan.schema.json` 写出镜头计划,并运行
28
- `scripts/validate_shot_plan.mjs shot-plan.json`。
29
- 11. 运行 pre-render quality gates
30
- 12. 展示方案摘要和 HTML preview,让用户验证/确认。
31
- 13. 根据反馈修订。
32
- 14. 渲染视频。
32
+ 12. 运行 pre-render quality gates。
33
+ 13. 展示方案摘要和 HTML preview,让用户确认页面、转场和动画。
34
+ 14. 视觉确认后再生成旁白、字幕并按实际时长更新 shot plan
35
+ 15. 根据反馈修订。
36
+ 16. 渲染视频。
33
37
 
34
38
  ## Intent Profile
35
39
 
36
40
  ```json
37
41
  {
38
42
  "rawRequest": "",
43
+ "workflowStage": "visual_draft",
39
44
  "purpose": "product_launch",
40
45
  "format": "landscape_16_9",
41
46
  "generationMode": "generate_new_hyperframes_html",
@@ -136,6 +141,8 @@ score =
136
141
  }
137
142
  ```
138
143
 
144
+ Round 2 不选择 voice/emotion,也不生成字幕。旁白选择放到视觉确认后。
145
+
139
146
  ## HTML Generation Plan
140
147
 
141
148
  生成 HTML 前输出 plan:
@@ -149,6 +156,7 @@ score =
149
156
  {"id": "logo-outro", "role": "outro_reference"}
150
157
  ],
151
158
  "patternsRequired": true,
159
+ "audio": {"narration": {"mode": "deferred"}},
152
160
  "assumptions": ["用户未提供 logo,先使用文字 logo 占位"],
153
161
  "nextValidation": "请确认风格、文案、时长和画幅。"
154
162
  }
@@ -176,9 +184,11 @@ score =
176
184
 
177
185
  ## Model Routing
178
186
 
179
- - `gpt-5.6-sol` 仅用于页面生成和动画编写。
187
+ - 页面生成和动画编写使用当前客户端的 `high_quality` 层级模型;
188
+ Codex 优先 `gpt-5.6-sol`。
180
189
  - 需求抽取、文件检测、catalog 查询、镜头计划、音频提示词、校验和打包
181
- 使用 `gpt-5.6-terra` 或同级快速经济模型。
190
+ 使用当前客户端的 `fast` 层级模型;Codex 优先 `gpt-5.6-terra`。
191
+ - 在 shot plan 中记录层级和实际模型名;Claude Code 不要伪填 GPT 型号。
182
192
  - 不为模型路由自动新增 Agent 或其他实体;在运行环境支持模型选择时才
183
193
  应用路由。
184
194
 
@@ -26,17 +26,15 @@ The installed config shape is:
26
26
  ```json
27
27
  {
28
28
  "api_key": "[redacted]",
29
- "group_id": "group_id",
30
- "model": "music-2.6-free"
29
+ "model": "music-3.0"
31
30
  }
32
31
  ```
33
32
 
34
- Do not print or echo the raw API key. `group_id` is stored because the installer collects it, but the current `/v1/music_generation` API call uses only `Authorization: Bearer <API_key>` and the documented JSON request body.
33
+ Do not print or echo the raw API key. `group_id` is optional legacy account
34
+ metadata and is not required by the current `/v1/music_generation` call.
35
35
 
36
- The wrapper accepts `music-3.0`, `music-2.6`, and `music-2.6-free`. The stable
37
- installer default remains `music-2.6` for backward compatibility; choose
38
- `music-3.0` explicitly when the account has access and the user wants the
39
- current model.
36
+ The wrapper accepts `music-3.0`, `music-3.0-free`, `music-2.6`, and
37
+ `music-2.6-free`. The installer default is `music-3.0`.
40
38
 
41
39
  ## Clarify Music
42
40
 
@@ -15,19 +15,23 @@ Rules:
15
15
  - scan only the current directory top level;
16
16
  - trust fixed names first: `outline.*` and `narration.*`;
17
17
  - if multiple candidates are returned, ask the user to choose;
18
- - if only outline exists, ask whether to generate narration from it;
19
- - if only narration exists, ask whether to generate an outline from it;
18
+ - if only outline exists, record narration as deferred until visual approval;
19
+ - if only narration exists, use it as content context but do not generate audio or subtitles yet;
20
20
  - if narration is not used, do not generate voice audio or subtitles.
21
21
 
22
- Codex writes or edits outline/narration content. Scripts only validate, convert, call MiniMax, and write artifacts.
22
+ The coding agent writes or edits outline/narration content. Scripts only validate, convert, call MiniMax, and write artifacts.
23
+
24
+ Narration is a post-visual-confirmation stage. Do not generate MiniMax TTS,
25
+ voice lists, or subtitles until the user has approved the page, transition,
26
+ and animation direction. A visual draft shot plan should use
27
+ `audio.narration.mode: "deferred"` when narration is requested.
23
28
 
24
29
  ## Provider Boundary
25
30
 
26
31
  Generated narration and BGM use MiniMax mainland China (`minimax_cn`) only.
27
32
  Never run `hyperframes auth login` or `hyperframes tts`; never invoke HeyGen,
28
- Kokoro, or MusicGen; and never install a fallback runtime/model. Kokoro is a
29
- local open-weight TTS engine, but it is outside this product contract and is
30
- not needed.
33
+ Kokoro, or MusicGen; and never install a fallback runtime/model or beat
34
+ analysis dependency during a run. Do not add a Kokoro runtime or component.
31
35
 
32
36
  Before a generated-audio call, validate the route:
33
37
 
@@ -48,7 +52,7 @@ Use this shape:
48
52
 
49
53
  ```json
50
54
  {
51
- "voice": "XiaoR_001",
55
+ "voice": "male-qn-qingse",
52
56
  "emotion": "calm",
53
57
  "language": "zh",
54
58
  "style": "casual",
@@ -121,15 +125,17 @@ Subtitle rules:
121
125
 
122
126
  ## BGM, Beat, And Schedule
123
127
 
124
- BGM is the timing source when present:
128
+ BGM is the timing source when present, but narration still waits for visual
129
+ approval:
125
130
 
126
131
  1. decide BGM generation, local import, or skip;
127
- 2. if configured, generate BGM with `scripts/generate_minimax_music.mjs`;
128
- 3. analyze the music with `scripts/analyze_music_beats.py`;
129
- 4. schedule transitions, animation, narration, and subtitles against the beat map;
130
- 5. generate HTML;
131
- 6. preview and get user confirmation;
132
- 7. render and mix final video/audio.
132
+ 2. if BGM is needed for beat-sync, generate BGM with `scripts/generate_minimax_music.mjs`;
133
+ 3. analyze the music with `scripts/analyze_music_beats.py` only when its dependencies are already available;
134
+ 4. if beat analysis dependencies are missing, do not run `pip install`; ask for a beat map, continue with explicit manual cues, or skip beat sync;
135
+ 5. schedule transitions and animation against the beat map or manual cues;
136
+ 6. generate the visual HTML preview and get page/transition/animation confirmation;
137
+ 7. generate narration and subtitles, then reconcile timing;
138
+ 8. render and mix final video/audio after final approval.
133
139
 
134
140
  Default mix:
135
141
 
@@ -4,7 +4,7 @@ Use this whenever a generated or assembled HyperFrames HTML animation will be sh
4
4
 
5
5
  ## Required Behavior
6
6
 
7
- HTML previews should include a bottom page indicator and thin progress bar. Users can drag the progress bar to seek, or press ArrowLeft and ArrowRight to move between pages.
7
+ HTML previews should include a bottom page indicator, play/pause button, and thin progress bar. The preview copy must start the registered timeline in interactive mode; users can pause/resume, drag the progress bar to seek, or press ArrowLeft and ArrowRight to move between pages. Never show the paused base composition as the interactive preview.
8
8
 
9
9
  Video renders must hide those controls. Use one of these render-hidden signals:
10
10
 
@@ -12,6 +12,8 @@ Video renders must hide those controls. Use one of these render-hidden signals:
12
12
  - `?preview=0`
13
13
  - `<html data-render-mode="video">`
14
14
 
15
+ The main render timeline should remain `paused: true` for deterministic frame capture. Preview controls are responsible for calling `timeline.play()`/`timeline.pause()` and must skip autoplay when any render-hidden signal is active.
16
+
15
17
  ## Page Model
16
18
 
17
19
  Prefer explicit page starts:
@@ -34,7 +36,7 @@ After the base HTML passes normal quality gates, create a preview copy:
34
36
 
35
37
  ```bash
36
38
  node scripts/inject_preview_controls.mjs composition.html -o composition.preview.html --force
37
- python3 scripts/validate_hyperframes_html.py composition.preview.html --preview-controls
39
+ python3 scripts/validate_hyperframes_html.py composition.preview.html --preview-controls --shot-plan shot-plan.json
38
40
  ```
39
41
 
40
42
  Show the preview copy to the user for validation.
@@ -0,0 +1,32 @@
1
+ # ShotCraft motion pack
2
+
3
+ Use this pack before freehanding animation for the supported ShotCraft cards.
4
+ The goal is to copy the card's actual motion grammar into GSAP/HyperFrames
5
+ instead of merely describing it in prose.
6
+
7
+ | ShotCraft reference | `motion_contract.motion_preset` | Helper |
8
+ | --- | --- | --- |
9
+ | `slam-entrance-moves` | `slam-entrance` | `slamEntrance` |
10
+ | `shot-transitions` | `whip-transition` | `whipTransition` |
11
+ | `crash-zoom-punch` | `crash-zoom` | `crashZoom` |
12
+ | `bottom-push-stack-wipe` | `push-stack-wipe` | `pushStackWipe` |
13
+ | `dataviz-landscape-open` | `dataviz-landscape-open` | `datavizLandscapeOpen` |
14
+ | `panel-grid-moves` | `panel-grid-reflow` | `panelGridReflow` |
15
+ | `ui-to-brand-morph` | `brand-resolve` | `brandResolve` |
16
+
17
+ Usage:
18
+
19
+ 1. Read the selected ShotCraft card README first.
20
+ 2. Set the matching `motion_contract.motion_preset` in the shot plan.
21
+ 3. Copy only the needed helper from `shotcraft-motion-pack.js` into the
22
+ generated HTML, or include the file as a local reference when the project
23
+ allows that.
24
+ 4. Keep the helper on the main paused GSAP timeline; do not run wall-clock
25
+ timers, React, Remotion, or ShotCraft runtime code.
26
+
27
+ Quality bar:
28
+
29
+ - motion starts with a visible camera/geometry event, not a fade;
30
+ - transition covers the cut with a real mask, whip, impact, or spatial travel;
31
+ - dense motion resolves into at least `readable_rest_seconds` of rest;
32
+ - every helper call is tied to a named shot boundary or beat cue.
@@ -0,0 +1,178 @@
1
+ // Copy the needed helpers into a generated HyperFrames HTML file.
2
+ // Requires GSAP and a paused main timeline.
3
+ (function registerShotCraftMotion(global) {
4
+ function gsapRef() {
5
+ if (!global.gsap) throw new Error("GSAP is required for ShotCraft motion helpers");
6
+ return global.gsap;
7
+ }
8
+
9
+ function slamEntrance(tl, target, cue = 0, opts = {}) {
10
+ const gsap = gsapRef();
11
+ const duration = opts.duration ?? 0.72;
12
+ tl.set(target, { transformPerspective: 900, transformOrigin: "50% 55%" }, cue)
13
+ .fromTo(target, {
14
+ autoAlpha: 0,
15
+ scale: 2.4,
16
+ z: 420,
17
+ rotateX: -18,
18
+ filter: "blur(14px) brightness(1.8)",
19
+ }, {
20
+ autoAlpha: 1,
21
+ scale: 0.92,
22
+ z: 0,
23
+ rotateX: 0,
24
+ filter: "blur(0px) brightness(1)",
25
+ duration: duration * 0.58,
26
+ ease: "expo.out",
27
+ }, cue)
28
+ .to(target, { scale: 1.04, duration: duration * 0.16, ease: "power2.out" }, cue + duration * 0.42)
29
+ .to(target, { scale: 1, duration: duration * 0.26, ease: "back.out(2.4)" }, cue + duration * 0.56);
30
+ if (opts.ring) {
31
+ tl.fromTo(opts.ring, { autoAlpha: 0.9, scale: 0.12 }, {
32
+ autoAlpha: 0,
33
+ scale: 2.8,
34
+ duration: duration * 0.72,
35
+ ease: "expo.out",
36
+ }, cue + 0.04);
37
+ }
38
+ return tl;
39
+ }
40
+
41
+ function whipTransition(tl, fromTarget, toTarget, cue = 0, opts = {}) {
42
+ const distance = opts.distance ?? 260;
43
+ const blur = opts.blur ?? 18;
44
+ tl.to(fromTarget, {
45
+ x: -distance,
46
+ rotateZ: -3,
47
+ filter: `blur(${blur}px)`,
48
+ autoAlpha: 0,
49
+ duration: 0.2,
50
+ ease: "power4.in",
51
+ }, cue)
52
+ .fromTo(toTarget, {
53
+ x: distance,
54
+ rotateZ: 3,
55
+ filter: `blur(${blur}px)`,
56
+ autoAlpha: 0,
57
+ }, {
58
+ x: 0,
59
+ rotateZ: 0,
60
+ filter: "blur(0px)",
61
+ autoAlpha: 1,
62
+ duration: 0.34,
63
+ ease: "expo.out",
64
+ }, cue + 0.16);
65
+ if (opts.flash) tl.fromTo(opts.flash, { autoAlpha: 0 }, { autoAlpha: 1, yoyo: true, repeat: 1, duration: 0.05 }, cue + 0.14);
66
+ return tl;
67
+ }
68
+
69
+ function crashZoom(tl, target, cue = 0, opts = {}) {
70
+ tl.fromTo(target, {
71
+ scale: opts.fromScale ?? 0.72,
72
+ filter: "contrast(1.3) saturate(1.25)",
73
+ }, {
74
+ scale: opts.overshoot ?? 1.18,
75
+ duration: 0.16,
76
+ ease: "power4.in",
77
+ }, cue)
78
+ .to(target, { scale: 0.98, duration: 0.12, ease: "power2.out" }, cue + 0.16)
79
+ .to(target, { scale: 1, filter: "contrast(1) saturate(1)", duration: 0.28, ease: "expo.out" }, cue + 0.28);
80
+ if (opts.shock) tl.fromTo(opts.shock, { autoAlpha: 0.85, scale: 0.2 }, { autoAlpha: 0, scale: 2.2, duration: 0.34, ease: "expo.out" }, cue + 0.12);
81
+ return tl;
82
+ }
83
+
84
+ function pushStackWipe(tl, fromTarget, toTarget, cue = 0, opts = {}) {
85
+ const height = opts.height ?? "112%";
86
+ tl.fromTo(toTarget, { y: height, autoAlpha: 1 }, {
87
+ y: "0%",
88
+ duration: 0.48,
89
+ ease: "power3.inOut",
90
+ }, cue)
91
+ .to(fromTarget, {
92
+ y: "-18%",
93
+ scale: 0.94,
94
+ filter: "blur(8px)",
95
+ autoAlpha: 0,
96
+ duration: 0.42,
97
+ ease: "power3.inOut",
98
+ }, cue + 0.04)
99
+ .fromTo(opts.shadow ?? toTarget, { boxShadow: "0 -40px 80px rgba(0,0,0,0.35)" }, {
100
+ boxShadow: "0 0 0 rgba(0,0,0,0)",
101
+ duration: 0.36,
102
+ ease: "power2.out",
103
+ }, cue + 0.28);
104
+ return tl;
105
+ }
106
+
107
+ function datavizLandscapeOpen(tl, bars, labels, cue = 0, opts = {}) {
108
+ tl.fromTo(bars, {
109
+ scaleY: 0,
110
+ transformOrigin: "50% 100%",
111
+ }, {
112
+ scaleY: 1,
113
+ duration: opts.duration ?? 0.86,
114
+ stagger: opts.stagger ?? 0.035,
115
+ ease: "expo.out",
116
+ }, cue)
117
+ .fromTo(labels, { y: 18, autoAlpha: 0 }, {
118
+ y: 0,
119
+ autoAlpha: 1,
120
+ duration: 0.34,
121
+ stagger: opts.stagger ?? 0.035,
122
+ ease: "power2.out",
123
+ }, cue + 0.24);
124
+ return tl;
125
+ }
126
+
127
+ function panelGridReflow(tl, panels, cue = 0, opts = {}) {
128
+ const gsap = gsapRef();
129
+ const panelList = gsap.utils.toArray(panels);
130
+ panelList.forEach((panel, index) => {
131
+ tl.fromTo(panel, {
132
+ xPercent: index % 2 === 0 ? -18 : 18,
133
+ yPercent: index % 3 === 0 ? -12 : 12,
134
+ rotateZ: index % 2 === 0 ? -3 : 3,
135
+ autoAlpha: 0,
136
+ }, {
137
+ xPercent: 0,
138
+ yPercent: 0,
139
+ rotateZ: 0,
140
+ autoAlpha: 1,
141
+ duration: opts.duration ?? 0.58,
142
+ ease: "expo.out",
143
+ }, cue + index * (opts.stagger ?? 0.045));
144
+ });
145
+ return tl;
146
+ }
147
+
148
+ function brandResolve(tl, tiles, wordmark, cue = 0, opts = {}) {
149
+ tl.to(tiles, {
150
+ x: 0,
151
+ y: 0,
152
+ rotateZ: 0,
153
+ scale: 0.78,
154
+ duration: opts.gatherDuration ?? 0.54,
155
+ stagger: opts.stagger ?? 0.025,
156
+ ease: "power3.inOut",
157
+ }, cue)
158
+ .to(tiles, { autoAlpha: 0, scale: 0.48, duration: 0.2, ease: "power2.in" }, cue + 0.48)
159
+ .fromTo(wordmark, { autoAlpha: 0, scale: 0.86, filter: "blur(10px)" }, {
160
+ autoAlpha: 1,
161
+ scale: 1,
162
+ filter: "blur(0px)",
163
+ duration: 0.42,
164
+ ease: "expo.out",
165
+ }, cue + 0.54);
166
+ return tl;
167
+ }
168
+
169
+ global.HyperAnimatorShotCraftMotion = {
170
+ slamEntrance,
171
+ whipTransition,
172
+ crashZoom,
173
+ pushStackWipe,
174
+ datavizLandscapeOpen,
175
+ panelGridReflow,
176
+ brandResolve,
177
+ };
178
+ })(window);
@@ -108,7 +108,7 @@ Options:
108
108
  --format <mp3|wav|pcm> Audio encoding format, default mp3
109
109
  --sample-rate <rate> 16000, 24000, 32000, or 44100; default 44100
110
110
  --bitrate <bits> 32000, 64000, 128000, or 256000; default 256000
111
- --model <model> music-3.0, music-2.6, or music-2.6-free
111
+ --model <model> music-3.0, music-3.0-free, music-2.6, or music-2.6-free
112
112
  --config <file> Explicit MiniMax config JSON
113
113
  --request-json <file> Read request options from a JSON file
114
114
  --dry-run Print redacted request without contacting MiniMax
@@ -270,8 +270,12 @@ function safeFileStem(prompt) {
270
270
  return stem || "minimax-music";
271
271
  }
272
272
 
273
- async function downloadUrl(url) {
274
- const response = await fetch(url);
273
+ async function downloadUrl(url, timeoutMs) {
274
+ const parsed = URL.canParse(url) ? new URL(url) : null;
275
+ if (!parsed || !["http:", "https:"].includes(parsed.protocol)) {
276
+ throw minimaxError("MiniMax audio URL must use HTTP or HTTPS", { kind: "response" });
277
+ }
278
+ const response = await fetchWithTimeout(url, {}, timeoutMs);
275
279
 
276
280
  if (!response.ok) {
277
281
  throw new Error(`MiniMax audio URL download failed with HTTP ${response.status}`);
@@ -429,7 +433,7 @@ async function callMiniMaxWithRetries(config, request, { endpoint, timeoutMs, ma
429
433
  throw minimaxError("MiniMax request failed", { kind: "network", retryable: true, attempts });
430
434
  }
431
435
 
432
- async function writeAudioOutput({ responseJson, request, outputDir, prompt, endpoint }) {
436
+ async function writeAudioOutput({ responseJson, request, outputDir, prompt, endpoint, timeoutMs }) {
433
437
  await mkdir(outputDir, { recursive: true });
434
438
 
435
439
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
@@ -439,7 +443,7 @@ async function writeAudioOutput({ responseJson, request, outputDir, prompt, endp
439
443
  const metadataPath = join(outputDir, `${stem}.minimax.json`);
440
444
  const audioValue = responseJson.data.audio;
441
445
  const audioBuffer = request.output_format === "url"
442
- ? await downloadUrl(audioValue)
446
+ ? await downloadUrl(audioValue, timeoutMs)
443
447
  : decodeHexAudio(audioValue);
444
448
 
445
449
  await writeFile(audioPath, audioBuffer);
@@ -496,7 +500,7 @@ async function main() {
496
500
  }
497
501
 
498
502
  if (!runtime) {
499
- throw Object.assign(new Error("MiniMax config not found. Run hyper-animator-codex install with --minimax-api-key and --minimax-group-id, or set MINIMAX_API_KEY and MINIMAX_GROUP_ID."), {
503
+ throw Object.assign(new Error("MiniMax config not found. Run hyper-animator-codex install with --minimax-api-key, or set MINIMAX_API_KEY."), {
500
504
  kind: "config",
501
505
  retryable: false,
502
506
  });
@@ -539,6 +543,7 @@ async function main() {
539
543
  outputDir: options.output_dir,
540
544
  prompt: options.prompt,
541
545
  endpoint,
546
+ timeoutMs,
542
547
  });
543
548
 
544
549
  printJson({
@@ -14,7 +14,7 @@ const BACKUP_TTS_ENDPOINT = "https://api-bj.minimaxi.com/v1/t2a_v2";
14
14
  const BACKUP_VOICE_ENDPOINT = "https://api-bj.minimaxi.com/v1/get_voice";
15
15
  const PROVIDER = "minimax_cn";
16
16
  const DEFAULT_TTS_MODEL = "speech-2.8-hd";
17
- const DEFAULT_VOICE_ID = "XiaoR_001";
17
+ const DEFAULT_VOICE_ID = "male-qn-qingse";
18
18
  const EMOTIONS = new Set(["happy", "sad", "angry", "fearful", "disgusted", "surprised", "calm", "fluent", "whipser"]);
19
19
  const VOICE_TYPES = new Set(["all", "system", "voice_cloning", "voice_generation"]);
20
20
  const scriptDir = dirname(fileURLToPath(import.meta.url));
@@ -135,12 +135,12 @@ Options:
135
135
  --narration-json <file> Narration JSON with scenes[].narration
136
136
  --request-json <file> Structured TTS request options
137
137
  --text <text> Direct text input
138
- --voice-id <id> MiniMax voice_id, default from narration or XiaoR_001
138
+ --voice-id <id> MiniMax voice_id, default from narration or male-qn-qingse
139
139
  --emotion <label> happy, sad, angry, fearful, disgusted, surprised, calm, fluent, whipser
140
140
  --language <code> zh, en, or MiniMax language_boost value
141
141
  --output-dir <dir> Directory for generated audio and metadata
142
142
  --model <model> MiniMax speech model, default speech-2.8-hd
143
- --format <mp3|wav|pcm> Audio encoding format, default mp3
143
+ --format <mp3|wav|pcm|flac> Audio encoding format, default mp3
144
144
  --sample-rate <rate> Default 32000
145
145
  --bitrate <bits> Default 128000
146
146
  --channel <count> Default 1
@@ -246,14 +246,17 @@ function validateOptions(options) {
246
246
  if (!options.voice_id || !options.voice_id.trim()) {
247
247
  throw minimaxError("voice_id is required", { kind: "validation" });
248
248
  }
249
+ if (typeof options.model !== "string" || !/^speech-[a-z0-9.-]+$/i.test(options.model)) {
250
+ throw minimaxError(`Unsupported MiniMax speech model: ${options.model}`, { kind: "validation" });
251
+ }
249
252
  if (!EMOTIONS.has(options.emotion)) {
250
253
  throw minimaxError(`Unsupported emotion: ${options.emotion}`, { kind: "validation" });
251
254
  }
252
- if (!["mp3", "wav", "pcm"].includes(options.audio_format)) {
253
- throw minimaxError("--format must be mp3, wav, or pcm", { kind: "validation" });
255
+ if (!["mp3", "wav", "pcm", "flac"].includes(options.audio_format)) {
256
+ throw minimaxError("--format must be mp3, wav, pcm, or flac", { kind: "validation" });
254
257
  }
255
- if (![16000, 24000, 32000, 44100].includes(options.sample_rate)) {
256
- throw minimaxError("--sample-rate must be 16000, 24000, 32000, or 44100", { kind: "validation" });
258
+ if (![8000, 16000, 22050, 24000, 32000, 44100].includes(options.sample_rate)) {
259
+ throw minimaxError("--sample-rate must be 8000, 16000, 22050, 24000, 32000, or 44100", { kind: "validation" });
257
260
  }
258
261
  if (![32000, 64000, 128000, 256000].includes(options.bitrate)) {
259
262
  throw minimaxError("--bitrate must be 32000, 64000, 128000, or 256000", { kind: "validation" });
@@ -261,6 +264,15 @@ function validateOptions(options) {
261
264
  if (![1, 2].includes(options.channel)) {
262
265
  throw minimaxError("--channel must be 1 or 2", { kind: "validation" });
263
266
  }
267
+ if (!Number.isFinite(options.speed) || options.speed < 0.5 || options.speed > 2) {
268
+ throw minimaxError("--speed must be from 0.5 to 2", { kind: "validation" });
269
+ }
270
+ if (!Number.isFinite(options.volume) || options.volume < 0.1 || options.volume > 10) {
271
+ throw minimaxError("--volume must be from 0.1 to 10", { kind: "validation" });
272
+ }
273
+ if (!Number.isInteger(options.pitch) || options.pitch < -12 || options.pitch > 12) {
274
+ throw minimaxError("--pitch must be an integer from -12 to 12", { kind: "validation" });
275
+ }
264
276
  }
265
277
 
266
278
  function buildTtsRequest(options) {
@@ -477,7 +489,7 @@ async function readRuntime(options) {
477
489
  }
478
490
 
479
491
  if (!runtime) {
480
- throw minimaxError("MiniMax config not found. Run hyper-animator-codex install with --minimax-api-key and --minimax-group-id, or set MINIMAX_API_KEY and MINIMAX_GROUP_ID.", {
492
+ throw minimaxError("MiniMax config not found. Run hyper-animator-codex install with --minimax-api-key, or set MINIMAX_API_KEY.", {
481
493
  kind: "config",
482
494
  retryable: false,
483
495
  });