hyper-animator-codex 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/bin/hyper-animator-codex.mjs +16 -28
- package/lib/install-options.mjs +44 -0
- package/lib/install-skill.mjs +16 -0
- package/lib/minimax-config.mjs +162 -0
- package/package.json +1 -1
- package/skills/hyper-animator-codex/SKILL.md +9 -6
- package/skills/hyper-animator-codex/references/minimax-music-workflow.md +77 -0
- package/skills/hyper-animator-codex/scripts/generate_minimax_music.mjs +346 -0
- package/skills/hyper-animator-codex/scripts/minimax_runtime_config.mjs +113 -0
package/README.md
CHANGED
|
@@ -37,6 +37,37 @@ Use a custom Codex skills directory:
|
|
|
37
37
|
npx hyper-animator-codex install --target /path/to/codex/skills
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## MiniMax Music Configuration
|
|
41
|
+
|
|
42
|
+
MiniMax is the preferred generated-background-music provider. Configure it during install so the copied Codex skill can generate music without asking for credentials later:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx hyper-animator-codex install --force \
|
|
46
|
+
--minimax-api-key "$MINIMAX_API_KEY" \
|
|
47
|
+
--minimax-group-id "$MINIMAX_GROUP_ID" \
|
|
48
|
+
--minimax-model music-2.6-free
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Environment fallback is also supported:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
MINIMAX_API_KEY=... MINIMAX_GROUP_ID=... npx hyper-animator-codex install --force
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or load a JSON config file:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx hyper-animator-codex install --force --minimax-config ./minimax.json
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The installer writes local config to:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
${CODEX_HOME:-$HOME/.codex}/skills/hyper-animator-codex/config/minimax.json
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Do not commit that installed config file. The npm package does not include local MiniMax credentials.
|
|
70
|
+
|
|
40
71
|
## Contents
|
|
41
72
|
|
|
42
73
|
- `skills/hyper-animator-codex/SKILL.md`: Codex skill instructions.
|
|
@@ -61,6 +92,16 @@ Run the bundled analyzer from an installed skill directory or repository checkou
|
|
|
61
92
|
python3 skills/hyper-animator-codex/scripts/analyze_music_beats.py path/to/music.wav -o beat-map.json --fps 60 --pretty
|
|
62
93
|
```
|
|
63
94
|
|
|
95
|
+
Generate MiniMax background music from an installed skill directory:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
node scripts/generate_minimax_music.mjs \
|
|
99
|
+
--prompt "bright electronic product launch, confident, clean, rising energy" \
|
|
100
|
+
--output-dir hyper-animator-output/music
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Then analyze the generated audio with `analyze_music_beats.py` and align animation timing to the resulting beat map.
|
|
104
|
+
|
|
64
105
|
## Development
|
|
65
106
|
|
|
66
107
|
```bash
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { installSkill, resolveCodexSkillsRoot, SKILL_NAME } from "../lib/install-skill.mjs";
|
|
3
|
+
import { parseInstallArgs } from "../lib/install-options.mjs";
|
|
3
4
|
|
|
4
5
|
function printHelp() {
|
|
5
6
|
console.log(`Usage:
|
|
6
|
-
hyper-animator-codex install [--force] [--target <skills-dir>]
|
|
7
|
+
hyper-animator-codex install [--force] [--target <skills-dir>] [MiniMax options]
|
|
7
8
|
hyper-animator-codex path
|
|
8
9
|
hyper-animator-codex help
|
|
9
10
|
|
|
@@ -13,36 +14,18 @@ Commands:
|
|
|
13
14
|
help Show this help
|
|
14
15
|
|
|
15
16
|
Options:
|
|
16
|
-
--force
|
|
17
|
-
--target <dir>
|
|
17
|
+
--force Replace an existing installed skill
|
|
18
|
+
--target <dir> Install into a specific Codex skills directory
|
|
19
|
+
--minimax-api-key <key> Save MiniMax API key into installed skill config
|
|
20
|
+
--minimax-group-id <group_id> Save MiniMax group_id into installed skill config
|
|
21
|
+
--minimax-model <model> MiniMax text music model: music-2.6 or music-2.6-free
|
|
22
|
+
--minimax-config <file> Load MiniMax JSON config with api_key, group_id, and model
|
|
23
|
+
|
|
24
|
+
Environment fallback:
|
|
25
|
+
MINIMAX_API_KEY, MINIMAX_GROUP_ID, MINIMAX_MODEL
|
|
18
26
|
`);
|
|
19
27
|
}
|
|
20
28
|
|
|
21
|
-
function parseInstallArgs(args) {
|
|
22
|
-
const parsed = {
|
|
23
|
-
force: false,
|
|
24
|
-
targetRoot: undefined,
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
28
|
-
const arg = args[index];
|
|
29
|
-
if (arg === "--force") {
|
|
30
|
-
parsed.force = true;
|
|
31
|
-
} else if (arg === "--target") {
|
|
32
|
-
const value = args[index + 1];
|
|
33
|
-
if (!value) {
|
|
34
|
-
throw new Error("--target requires a directory");
|
|
35
|
-
}
|
|
36
|
-
parsed.targetRoot = value;
|
|
37
|
-
index += 1;
|
|
38
|
-
} else {
|
|
39
|
-
throw new Error(`Unknown option: ${arg}`);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return parsed;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
29
|
async function main() {
|
|
47
30
|
const [command = "install", ...args] = process.argv.slice(2);
|
|
48
31
|
|
|
@@ -66,6 +49,11 @@ async function main() {
|
|
|
66
49
|
console.log(`Installed ${result.skillName}`);
|
|
67
50
|
console.log(`Source: ${result.sourcePath}`);
|
|
68
51
|
console.log(`Target: ${result.installedPath}`);
|
|
52
|
+
|
|
53
|
+
if (result.minimaxConfigPath) {
|
|
54
|
+
console.log(`MiniMax config: ${result.minimaxConfigPath}`);
|
|
55
|
+
console.log(`MiniMax model: ${result.minimaxConfig.model}`);
|
|
56
|
+
}
|
|
69
57
|
}
|
|
70
58
|
|
|
71
59
|
main().catch((error) => {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function requireValue(args, index, flag) {
|
|
2
|
+
const value = args[index + 1];
|
|
3
|
+
|
|
4
|
+
if (!value || value.startsWith("--")) {
|
|
5
|
+
throw new Error(`${flag} requires a value`);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function parseInstallArgs(args) {
|
|
12
|
+
const parsed = {
|
|
13
|
+
force: false,
|
|
14
|
+
targetRoot: undefined,
|
|
15
|
+
minimaxConfig: {},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
19
|
+
const arg = args[index];
|
|
20
|
+
|
|
21
|
+
if (arg === "--force") {
|
|
22
|
+
parsed.force = true;
|
|
23
|
+
} else if (arg === "--target") {
|
|
24
|
+
parsed.targetRoot = requireValue(args, index, arg);
|
|
25
|
+
index += 1;
|
|
26
|
+
} else if (arg === "--minimax-api-key") {
|
|
27
|
+
parsed.minimaxConfig.api_key = requireValue(args, index, arg);
|
|
28
|
+
index += 1;
|
|
29
|
+
} else if (arg === "--minimax-group-id") {
|
|
30
|
+
parsed.minimaxConfig.group_id = requireValue(args, index, arg);
|
|
31
|
+
index += 1;
|
|
32
|
+
} else if (arg === "--minimax-model") {
|
|
33
|
+
parsed.minimaxConfig.model = requireValue(args, index, arg);
|
|
34
|
+
index += 1;
|
|
35
|
+
} else if (arg === "--minimax-config") {
|
|
36
|
+
parsed.minimaxConfig.config_path = requireValue(args, index, arg);
|
|
37
|
+
index += 1;
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
package/lib/install-skill.mjs
CHANGED
|
@@ -3,6 +3,12 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
|
+
import {
|
|
7
|
+
redactMinimaxConfig,
|
|
8
|
+
resolveInstallMinimaxConfig,
|
|
9
|
+
writeMinimaxConfig,
|
|
10
|
+
} from "./minimax-config.mjs";
|
|
11
|
+
|
|
6
12
|
export const SKILL_NAME = "hyper-animator-codex";
|
|
7
13
|
|
|
8
14
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
@@ -61,9 +67,19 @@ export async function installSkill(options = {}) {
|
|
|
61
67
|
filter: shouldCopy,
|
|
62
68
|
});
|
|
63
69
|
|
|
70
|
+
const resolvedMinimaxConfig = await resolveInstallMinimaxConfig({
|
|
71
|
+
cliConfig: options.minimaxConfig || {},
|
|
72
|
+
env: options.env || process.env,
|
|
73
|
+
});
|
|
74
|
+
const minimaxWrite = resolvedMinimaxConfig
|
|
75
|
+
? await writeMinimaxConfig(installedPath, resolvedMinimaxConfig)
|
|
76
|
+
: null;
|
|
77
|
+
|
|
64
78
|
return {
|
|
65
79
|
skillName: SKILL_NAME,
|
|
66
80
|
sourcePath,
|
|
67
81
|
installedPath,
|
|
82
|
+
minimaxConfigPath: minimaxWrite ? minimaxWrite.configPath : null,
|
|
83
|
+
minimaxConfig: minimaxWrite ? redactMinimaxConfig(minimaxWrite.config) : null,
|
|
68
84
|
};
|
|
69
85
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MINIMAX_MODEL = "music-2.6-free";
|
|
5
|
+
export const TEXT_MUSIC_MODELS = new Set(["music-2.6", "music-2.6-free"]);
|
|
6
|
+
export const MINIMAX_CONFIG_RELATIVE_PATH = join("config", "minimax.json");
|
|
7
|
+
|
|
8
|
+
async function pathExists(path) {
|
|
9
|
+
try {
|
|
10
|
+
await stat(path);
|
|
11
|
+
return true;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
if (error && error.code === "ENOENT") {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function cleanString(value) {
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeInput(input = {}) {
|
|
30
|
+
return {
|
|
31
|
+
api_key: cleanString(input.api_key),
|
|
32
|
+
group_id: cleanString(input.group_id),
|
|
33
|
+
model: cleanString(input.model),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function hasConfigValues(input = {}) {
|
|
38
|
+
return Boolean(cleanString(input.api_key) || cleanString(input.group_id) || cleanString(input.model));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readConfigFile(configPath) {
|
|
42
|
+
const raw = await readFile(configPath, "utf8");
|
|
43
|
+
let parsed;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(raw);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error(`MiniMax config file is not valid JSON: ${configPath}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
52
|
+
throw new Error(`MiniMax config file must contain a JSON object: ${configPath}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function validateMinimaxConfig(config = {}) {
|
|
59
|
+
const normalized = normalizeInput(config);
|
|
60
|
+
|
|
61
|
+
if (!normalized.api_key) {
|
|
62
|
+
throw new Error("MiniMax api_key is required when MiniMax config is provided");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!normalized.group_id) {
|
|
66
|
+
throw new Error("MiniMax group_id is required when MiniMax config is provided");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const model = normalized.model || DEFAULT_MINIMAX_MODEL;
|
|
70
|
+
|
|
71
|
+
if (!TEXT_MUSIC_MODELS.has(model)) {
|
|
72
|
+
throw new Error(`Unsupported MiniMax text music model: ${model}. Use music-2.6 or music-2.6-free.`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
api_key: normalized.api_key,
|
|
77
|
+
group_id: normalized.group_id,
|
|
78
|
+
model,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function resolveInstallMinimaxConfig({ cliConfig = {}, env = process.env } = {}) {
|
|
83
|
+
const configPath = cleanString(cliConfig.config_path);
|
|
84
|
+
const fileConfig = configPath ? await readConfigFile(configPath) : {};
|
|
85
|
+
const directConfig = normalizeInput(cliConfig);
|
|
86
|
+
const envConfig = normalizeInput({
|
|
87
|
+
api_key: env.MINIMAX_API_KEY,
|
|
88
|
+
group_id: env.MINIMAX_GROUP_ID,
|
|
89
|
+
model: env.MINIMAX_MODEL,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const merged = {
|
|
93
|
+
api_key: directConfig.api_key || cleanString(fileConfig.api_key) || envConfig.api_key,
|
|
94
|
+
group_id: directConfig.group_id || cleanString(fileConfig.group_id) || envConfig.group_id,
|
|
95
|
+
model: directConfig.model || cleanString(fileConfig.model) || envConfig.model,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
if (!configPath && !hasConfigValues(directConfig) && !hasConfigValues(envConfig)) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return validateMinimaxConfig(merged);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function writeMinimaxConfig(installedPath, config) {
|
|
106
|
+
const normalized = validateMinimaxConfig(config);
|
|
107
|
+
const configDir = join(installedPath, "config");
|
|
108
|
+
const configPath = join(installedPath, MINIMAX_CONFIG_RELATIVE_PATH);
|
|
109
|
+
const body = `${JSON.stringify(normalized, null, 2)}\n`;
|
|
110
|
+
|
|
111
|
+
await mkdir(configDir, { recursive: true });
|
|
112
|
+
await writeFile(configPath, body, { encoding: "utf8", mode: 0o600 });
|
|
113
|
+
await chmod(configPath, 0o600);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
configPath,
|
|
117
|
+
config: normalized,
|
|
118
|
+
redactedConfig: redactMinimaxConfig(normalized),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function readRuntimeMinimaxConfig({ skillRoot, env = process.env } = {}) {
|
|
123
|
+
if (!skillRoot) {
|
|
124
|
+
throw new Error("skillRoot is required to read MiniMax runtime config");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const configPath = join(skillRoot, MINIMAX_CONFIG_RELATIVE_PATH);
|
|
128
|
+
|
|
129
|
+
if (await pathExists(configPath)) {
|
|
130
|
+
return {
|
|
131
|
+
source: "file",
|
|
132
|
+
configPath,
|
|
133
|
+
config: validateMinimaxConfig(await readConfigFile(configPath)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const envConfig = normalizeInput({
|
|
138
|
+
api_key: env.MINIMAX_API_KEY,
|
|
139
|
+
group_id: env.MINIMAX_GROUP_ID,
|
|
140
|
+
model: env.MINIMAX_MODEL,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!hasConfigValues(envConfig)) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
source: "environment",
|
|
149
|
+
configPath: null,
|
|
150
|
+
config: validateMinimaxConfig(envConfig),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function redactMinimaxConfig(config = {}) {
|
|
155
|
+
const normalized = normalizeInput(config);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
api_key: normalized.api_key ? "[redacted]" : undefined,
|
|
159
|
+
group_id: normalized.group_id,
|
|
160
|
+
model: normalized.model || DEFAULT_MINIMAX_MODEL,
|
|
161
|
+
};
|
|
162
|
+
}
|
package/package.json
CHANGED
|
@@ -19,12 +19,13 @@ Turn a natural-language animation or video brief into a validated HyperFrames HT
|
|
|
19
19
|
- `generate_new_hyperframes_html` when the user asks to write HTML, create a new effect, customize style, match a brand, use complex animation, or when component snippets are only paste placeholders.
|
|
20
20
|
- `assemble_existing_catalog_items` only when the user asks to use existing catalog items or quickly compose installed blocks/components.
|
|
21
21
|
6. Clarify audio: ask whether to add animation/transition sound effects and whether to add background music.
|
|
22
|
-
7. If background music is
|
|
23
|
-
8.
|
|
24
|
-
9.
|
|
25
|
-
10.
|
|
26
|
-
11.
|
|
27
|
-
12.
|
|
22
|
+
7. If background music is requested or undecided, read `references/minimax-music-workflow.md` and prefer MiniMax generation when configured. If MiniMax is unavailable or declined, ask for a local audio path or continue without BGM.
|
|
23
|
+
8. If background music is used, read `references/beat-sync-workflow.md`, generate or obtain the audio, and run `scripts/analyze_music_beats.py` when the file is available.
|
|
24
|
+
9. Ask the second clarification round with candidate context: visual direction, motion rhythm, generation mode, audio choices, music prompt/model when MiniMax is used, and beat-sync assumptions when background music is present.
|
|
25
|
+
10. Write or assemble HTML. When beat-sync is enabled, align major reveals, cuts, transitions, camera moves, and visual accents to the beat map instead of arbitrary timestamps.
|
|
26
|
+
11. Run pre-render quality gates.
|
|
27
|
+
12. Show a concise plan summary and preview path or HTML file to the user. Ask for confirmation before video render.
|
|
28
|
+
13. Render only after user confirmation, then report output path and any caveats.
|
|
28
29
|
|
|
29
30
|
## Interactive Questions
|
|
30
31
|
|
|
@@ -42,6 +43,7 @@ Do not ask everything upfront when the brief is already specific. Ask only for m
|
|
|
42
43
|
- Read `references/hyperframes-intent-workflow.md` for the full AskUserQuestion workflow, generation-mode rules, scoring model, and render confirmation requirements.
|
|
43
44
|
- Read `references/hyperframes-catalog-map.json` whenever selecting catalog candidates or determining visual references.
|
|
44
45
|
- Read `references/hyperframes-agent-pseudocode.ts` when implementing the end-to-end loop or when the correct sequence is ambiguous.
|
|
46
|
+
- Read `references/minimax-music-workflow.md` when generated background music, MiniMax, music prompt, instrumental/vocal choice, or provider fallback is mentioned.
|
|
45
47
|
- Read `references/beat-sync-workflow.md` when sound effects, background music, soundtrack, beat sync, rhythm, BPM, audio-reactive animation, or transition timing to music is mentioned.
|
|
46
48
|
- Use `references/examples/*.json` for sanity checks against common request shapes.
|
|
47
49
|
|
|
@@ -83,6 +85,7 @@ Before rendering, summarize:
|
|
|
83
85
|
- generation mode;
|
|
84
86
|
- selected or referenced catalog items;
|
|
85
87
|
- sound effects and background music choices;
|
|
88
|
+
- MiniMax provider status, model, generated audio path, metadata path, and redacted config source when MiniMax is used;
|
|
86
89
|
- beat map path, BPM, duration, and timing assumptions when beat-sync is enabled;
|
|
87
90
|
- dimensions and duration;
|
|
88
91
|
- content assumptions;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# MiniMax Music Workflow
|
|
2
|
+
|
|
3
|
+
Use this when the user wants generated background music or asks whether Hyper Animator Codex can create music for an animation.
|
|
4
|
+
|
|
5
|
+
## MiniMax Preference
|
|
6
|
+
|
|
7
|
+
Prefer MiniMax before other background-music sources when `config/minimax.json` exists in the installed skill or `MINIMAX_API_KEY` and `MINIMAX_GROUP_ID` are present in the environment.
|
|
8
|
+
|
|
9
|
+
The installed config shape is:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"api_key": "[redacted]",
|
|
14
|
+
"group_id": "group_id",
|
|
15
|
+
"model": "music-2.6-free"
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
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.
|
|
20
|
+
|
|
21
|
+
## Clarify Music
|
|
22
|
+
|
|
23
|
+
Ask only for missing choices:
|
|
24
|
+
|
|
25
|
+
- Should background music be generated with MiniMax, supplied as a local file, or skipped?
|
|
26
|
+
- What mood, genre, energy, audience, and scene should the music prompt express?
|
|
27
|
+
- Should the music be instrumental or vocal?
|
|
28
|
+
- For vocal music, should the user provide lyrics or allow MiniMax lyrics optimization?
|
|
29
|
+
- Should animation duration follow the generated music, or should the generated music be trimmed or looped to the planned duration?
|
|
30
|
+
|
|
31
|
+
## Generate Music
|
|
32
|
+
|
|
33
|
+
From the installed skill directory:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node scripts/generate_minimax_music.mjs \
|
|
37
|
+
--prompt "bright electronic product launch, confident, clean, rising energy" \
|
|
38
|
+
--output-dir hyper-animator-output/music
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For vocal music with user-provided lyrics:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
node scripts/generate_minimax_music.mjs \
|
|
45
|
+
--prompt "optimistic pop launch hook, energetic, modern SaaS demo" \
|
|
46
|
+
--vocal \
|
|
47
|
+
--lyrics "[Verse]\n..." \
|
|
48
|
+
--output-dir hyper-animator-output/music
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For vocal music where MiniMax should create lyrics:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
node scripts/generate_minimax_music.mjs \
|
|
55
|
+
--prompt "optimistic pop launch hook, energetic, modern SaaS demo" \
|
|
56
|
+
--vocal \
|
|
57
|
+
--lyrics-optimizer \
|
|
58
|
+
--output-dir hyper-animator-output/music
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use `--dry-run` before a real call when checking config, request shape, or model choice.
|
|
62
|
+
|
|
63
|
+
## Beat Sync
|
|
64
|
+
|
|
65
|
+
After music generation succeeds, analyze the generated audio:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 scripts/analyze_music_beats.py hyper-animator-output/music/generated.mp3 -o hyper-animator-output/music/beat-map.json --fps 60 --pretty
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Use the resulting beat map exactly as described in `references/beat-sync-workflow.md`.
|
|
72
|
+
|
|
73
|
+
## Fallbacks
|
|
74
|
+
|
|
75
|
+
If MiniMax config is missing, ask whether the user wants to provide a local audio file or continue without background music.
|
|
76
|
+
|
|
77
|
+
If MiniMax generation fails, summarize the provider, model, redacted config source, and error message. Then ask whether to retry with a different prompt/model, use a local audio file, or continue without background music.
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_MINIMAX_MODEL,
|
|
8
|
+
readMinimaxRuntimeConfig,
|
|
9
|
+
redactMinimaxConfig,
|
|
10
|
+
validateMinimaxConfig,
|
|
11
|
+
} from "./minimax_runtime_config.mjs";
|
|
12
|
+
|
|
13
|
+
const ENDPOINT = "https://api.minimaxi.com/v1/music_generation";
|
|
14
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const skillRoot = dirname(scriptDir);
|
|
16
|
+
|
|
17
|
+
function requireValue(args, index, flag) {
|
|
18
|
+
const value = args[index + 1];
|
|
19
|
+
|
|
20
|
+
if (!value || value.startsWith("--")) {
|
|
21
|
+
throw new Error(`${flag} requires a value`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(args) {
|
|
28
|
+
const parsed = {
|
|
29
|
+
prompt: undefined,
|
|
30
|
+
lyrics: undefined,
|
|
31
|
+
lyrics_optimizer: false,
|
|
32
|
+
is_instrumental: true,
|
|
33
|
+
output_dir: join(process.cwd(), "hyper-animator-output", "music"),
|
|
34
|
+
output_format: "hex",
|
|
35
|
+
audio_format: "mp3",
|
|
36
|
+
sample_rate: 44100,
|
|
37
|
+
bitrate: 256000,
|
|
38
|
+
model: undefined,
|
|
39
|
+
config_path: undefined,
|
|
40
|
+
dry_run: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
44
|
+
const arg = args[index];
|
|
45
|
+
|
|
46
|
+
if (arg === "--prompt") {
|
|
47
|
+
parsed.prompt = requireValue(args, index, arg);
|
|
48
|
+
index += 1;
|
|
49
|
+
} else if (arg === "--lyrics") {
|
|
50
|
+
parsed.lyrics = requireValue(args, index, arg);
|
|
51
|
+
index += 1;
|
|
52
|
+
} else if (arg === "--lyrics-optimizer") {
|
|
53
|
+
parsed.lyrics_optimizer = true;
|
|
54
|
+
} else if (arg === "--instrumental") {
|
|
55
|
+
parsed.is_instrumental = true;
|
|
56
|
+
} else if (arg === "--vocal") {
|
|
57
|
+
parsed.is_instrumental = false;
|
|
58
|
+
} else if (arg === "--output-dir") {
|
|
59
|
+
parsed.output_dir = requireValue(args, index, arg);
|
|
60
|
+
index += 1;
|
|
61
|
+
} else if (arg === "--output-format") {
|
|
62
|
+
parsed.output_format = requireValue(args, index, arg);
|
|
63
|
+
index += 1;
|
|
64
|
+
} else if (arg === "--format") {
|
|
65
|
+
parsed.audio_format = requireValue(args, index, arg);
|
|
66
|
+
index += 1;
|
|
67
|
+
} else if (arg === "--sample-rate") {
|
|
68
|
+
parsed.sample_rate = Number.parseInt(requireValue(args, index, arg), 10);
|
|
69
|
+
index += 1;
|
|
70
|
+
} else if (arg === "--bitrate") {
|
|
71
|
+
parsed.bitrate = Number.parseInt(requireValue(args, index, arg), 10);
|
|
72
|
+
index += 1;
|
|
73
|
+
} else if (arg === "--model") {
|
|
74
|
+
parsed.model = requireValue(args, index, arg);
|
|
75
|
+
index += 1;
|
|
76
|
+
} else if (arg === "--config") {
|
|
77
|
+
parsed.config_path = requireValue(args, index, arg);
|
|
78
|
+
index += 1;
|
|
79
|
+
} else if (arg === "--dry-run") {
|
|
80
|
+
parsed.dry_run = true;
|
|
81
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
82
|
+
parsed.help = true;
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return parsed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printHelp() {
|
|
92
|
+
console.log(`Usage:
|
|
93
|
+
node scripts/generate_minimax_music.mjs --prompt <text> [options]
|
|
94
|
+
|
|
95
|
+
Options:
|
|
96
|
+
--prompt <text> Music style, mood, and scene prompt
|
|
97
|
+
--lyrics <text> Lyrics for vocal generation
|
|
98
|
+
--lyrics-optimizer Let MiniMax generate lyrics from prompt for vocal generation
|
|
99
|
+
--instrumental Generate instrumental music, default
|
|
100
|
+
--vocal Generate vocal music; requires --lyrics or --lyrics-optimizer
|
|
101
|
+
--output-dir <dir> Directory for generated audio and metadata
|
|
102
|
+
--output-format <hex|url> MiniMax response format, default hex
|
|
103
|
+
--format <mp3|wav|pcm> Audio encoding format, default mp3
|
|
104
|
+
--sample-rate <rate> 16000, 24000, 32000, or 44100; default 44100
|
|
105
|
+
--bitrate <bits> 32000, 64000, 128000, or 256000; default 256000
|
|
106
|
+
--model <model> music-2.6 or music-2.6-free
|
|
107
|
+
--config <file> Explicit MiniMax config JSON
|
|
108
|
+
--dry-run Print redacted request without contacting MiniMax
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function validateOptions(options) {
|
|
113
|
+
if (!options.prompt || options.prompt.trim().length === 0) {
|
|
114
|
+
throw new Error("--prompt is required");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.prompt.length > 2000) {
|
|
118
|
+
throw new Error("--prompt must be 2000 characters or fewer");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!["hex", "url"].includes(options.output_format)) {
|
|
122
|
+
throw new Error("--output-format must be hex or url");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!["mp3", "wav", "pcm"].includes(options.audio_format)) {
|
|
126
|
+
throw new Error("--format must be mp3, wav, or pcm");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (![16000, 24000, 32000, 44100].includes(options.sample_rate)) {
|
|
130
|
+
throw new Error("--sample-rate must be 16000, 24000, 32000, or 44100");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (![32000, 64000, 128000, 256000].includes(options.bitrate)) {
|
|
134
|
+
throw new Error("--bitrate must be 32000, 64000, 128000, or 256000");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!options.is_instrumental && !options.lyrics && !options.lyrics_optimizer) {
|
|
138
|
+
throw new Error("Vocal MiniMax generation requires --lyrics or --lyrics-optimizer");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (options.lyrics && options.lyrics.length > 3500) {
|
|
142
|
+
throw new Error("--lyrics must be 3500 characters or fewer");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function buildRequest(options, config) {
|
|
147
|
+
const model = options.model || config.model || DEFAULT_MINIMAX_MODEL;
|
|
148
|
+
const validatedConfig = validateMinimaxConfig({
|
|
149
|
+
...config,
|
|
150
|
+
model,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const request = {
|
|
154
|
+
model: validatedConfig.model,
|
|
155
|
+
prompt: options.prompt,
|
|
156
|
+
stream: false,
|
|
157
|
+
output_format: options.output_format,
|
|
158
|
+
audio_setting: {
|
|
159
|
+
sample_rate: options.sample_rate,
|
|
160
|
+
bitrate: options.bitrate,
|
|
161
|
+
format: options.audio_format,
|
|
162
|
+
},
|
|
163
|
+
is_instrumental: options.is_instrumental,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
if (options.lyrics) {
|
|
167
|
+
request.lyrics = options.lyrics;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (options.lyrics_optimizer) {
|
|
171
|
+
request.lyrics_optimizer = true;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return request;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function safeFileStem(prompt) {
|
|
178
|
+
const stem = prompt
|
|
179
|
+
.toLowerCase()
|
|
180
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
181
|
+
.replace(/^-+|-+$/g, "")
|
|
182
|
+
.slice(0, 48);
|
|
183
|
+
|
|
184
|
+
return stem || "minimax-music";
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function downloadUrl(url) {
|
|
188
|
+
const response = await fetch(url);
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`MiniMax audio URL download failed with HTTP ${response.status}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return Buffer.from(await response.arrayBuffer());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function decodeHexAudio(value) {
|
|
198
|
+
if (!/^[0-9a-fA-F]+$/.test(value) || value.length % 2 !== 0) {
|
|
199
|
+
throw new Error("MiniMax response audio is not valid hex data");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Buffer.from(value, "hex");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function callMiniMax(config, request) {
|
|
206
|
+
const response = await fetch(ENDPOINT, {
|
|
207
|
+
method: "POST",
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
Authorization: `Bearer ${config.api_key}`,
|
|
211
|
+
},
|
|
212
|
+
body: JSON.stringify(request),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const text = await response.text();
|
|
216
|
+
let json;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
json = JSON.parse(text);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
throw new Error(`MiniMax returned non-JSON response with HTTP ${response.status}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
const message = json.base_resp?.status_msg || `HTTP ${response.status}`;
|
|
226
|
+
throw new Error(`MiniMax request failed: ${message}`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const statusCode = json.base_resp?.status_code;
|
|
230
|
+
if (statusCode !== 0) {
|
|
231
|
+
const message = json.base_resp?.status_msg || "unknown MiniMax error";
|
|
232
|
+
throw new Error(`MiniMax request failed with status_code ${statusCode}: ${message}`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (json.data?.status !== 2) {
|
|
236
|
+
throw new Error(`MiniMax generation is not complete; data.status is ${json.data?.status}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!json.data?.audio) {
|
|
240
|
+
throw new Error("MiniMax response did not include data.audio");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return json;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function writeAudioOutput({ responseJson, request, outputDir, prompt }) {
|
|
247
|
+
await mkdir(outputDir, { recursive: true });
|
|
248
|
+
|
|
249
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
250
|
+
const stem = `${safeFileStem(prompt)}-${timestamp}`;
|
|
251
|
+
const extension = request.audio_setting.format === "pcm" ? "pcm" : request.audio_setting.format;
|
|
252
|
+
const audioPath = join(outputDir, `${stem}.${extension}`);
|
|
253
|
+
const metadataPath = join(outputDir, `${stem}.minimax.json`);
|
|
254
|
+
const audioValue = responseJson.data.audio;
|
|
255
|
+
const audioBuffer = request.output_format === "url"
|
|
256
|
+
? await downloadUrl(audioValue)
|
|
257
|
+
: decodeHexAudio(audioValue);
|
|
258
|
+
|
|
259
|
+
await writeFile(audioPath, audioBuffer);
|
|
260
|
+
await writeFile(
|
|
261
|
+
metadataPath,
|
|
262
|
+
`${JSON.stringify({
|
|
263
|
+
provider: "minimax",
|
|
264
|
+
endpoint: ENDPOINT,
|
|
265
|
+
request,
|
|
266
|
+
response: {
|
|
267
|
+
base_resp: responseJson.base_resp,
|
|
268
|
+
data: {
|
|
269
|
+
status: responseJson.data.status,
|
|
270
|
+
},
|
|
271
|
+
extra_info: responseJson.extra_info,
|
|
272
|
+
trace_id: responseJson.trace_id,
|
|
273
|
+
},
|
|
274
|
+
audio_path: audioPath,
|
|
275
|
+
}, null, 2)}\n`,
|
|
276
|
+
"utf8",
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
audioPath,
|
|
281
|
+
metadataPath,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function main() {
|
|
286
|
+
const options = parseArgs(process.argv.slice(2));
|
|
287
|
+
|
|
288
|
+
if (options.help) {
|
|
289
|
+
printHelp();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
validateOptions(options);
|
|
294
|
+
|
|
295
|
+
const runtime = await readMinimaxRuntimeConfig({
|
|
296
|
+
skillRoot,
|
|
297
|
+
env: process.env,
|
|
298
|
+
configPath: options.config_path,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
if (!runtime) {
|
|
302
|
+
throw 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.");
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const request = buildRequest(options, runtime.config);
|
|
306
|
+
const redactedConfig = redactMinimaxConfig(runtime.config);
|
|
307
|
+
|
|
308
|
+
if (options.dry_run) {
|
|
309
|
+
console.log(JSON.stringify({
|
|
310
|
+
ok: true,
|
|
311
|
+
dry_run: true,
|
|
312
|
+
provider: "minimax",
|
|
313
|
+
endpoint: ENDPOINT,
|
|
314
|
+
config: {
|
|
315
|
+
source: runtime.source,
|
|
316
|
+
path: runtime.configPath,
|
|
317
|
+
redacted: redactedConfig,
|
|
318
|
+
},
|
|
319
|
+
request,
|
|
320
|
+
output_dir: options.output_dir,
|
|
321
|
+
}, null, 2));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const responseJson = await callMiniMax(runtime.config, request);
|
|
326
|
+
const output = await writeAudioOutput({
|
|
327
|
+
responseJson,
|
|
328
|
+
request,
|
|
329
|
+
outputDir: options.output_dir,
|
|
330
|
+
prompt: options.prompt,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
console.log(JSON.stringify({
|
|
334
|
+
ok: true,
|
|
335
|
+
provider: "minimax",
|
|
336
|
+
model: request.model,
|
|
337
|
+
output_path: output.audioPath,
|
|
338
|
+
metadata_path: output.metadataPath,
|
|
339
|
+
beat_analysis_command: `python3 scripts/analyze_music_beats.py ${output.audioPath} -o ${join(options.output_dir, "beat-map.json")} --fps 60 --pretty`,
|
|
340
|
+
}, null, 2));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
main().catch((error) => {
|
|
344
|
+
console.error(`Error: ${error.message}`);
|
|
345
|
+
process.exitCode = 1;
|
|
346
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_MINIMAX_MODEL = "music-2.6-free";
|
|
5
|
+
export const TEXT_MUSIC_MODELS = new Set(["music-2.6", "music-2.6-free"]);
|
|
6
|
+
|
|
7
|
+
async function pathExists(path) {
|
|
8
|
+
try {
|
|
9
|
+
await stat(path);
|
|
10
|
+
return true;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error && error.code === "ENOENT") {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function cleanString(value) {
|
|
20
|
+
if (typeof value !== "string") {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalize(input = {}) {
|
|
29
|
+
return {
|
|
30
|
+
api_key: cleanString(input.api_key),
|
|
31
|
+
group_id: cleanString(input.group_id),
|
|
32
|
+
model: cleanString(input.model),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasValues(input = {}) {
|
|
37
|
+
return Boolean(cleanString(input.api_key) || cleanString(input.group_id) || cleanString(input.model));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function redactMinimaxConfig(config = {}) {
|
|
41
|
+
const normalized = normalize(config);
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
api_key: normalized.api_key ? "[redacted]" : undefined,
|
|
45
|
+
group_id: normalized.group_id,
|
|
46
|
+
model: normalized.model || DEFAULT_MINIMAX_MODEL,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateMinimaxConfig(config = {}) {
|
|
51
|
+
const normalized = normalize(config);
|
|
52
|
+
|
|
53
|
+
if (!normalized.api_key) {
|
|
54
|
+
throw new Error("MiniMax api_key is required");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!normalized.group_id) {
|
|
58
|
+
throw new Error("MiniMax group_id is required");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const model = normalized.model || DEFAULT_MINIMAX_MODEL;
|
|
62
|
+
|
|
63
|
+
if (!TEXT_MUSIC_MODELS.has(model)) {
|
|
64
|
+
throw new Error(`Unsupported MiniMax text music model: ${model}. Use music-2.6 or music-2.6-free.`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
api_key: normalized.api_key,
|
|
69
|
+
group_id: normalized.group_id,
|
|
70
|
+
model,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function readJson(path) {
|
|
75
|
+
const raw = await readFile(path, "utf8");
|
|
76
|
+
const parsed = JSON.parse(raw);
|
|
77
|
+
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
79
|
+
throw new Error(`MiniMax config file must be a JSON object: ${path}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parsed;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function readMinimaxRuntimeConfig({ skillRoot, env = process.env, configPath } = {}) {
|
|
86
|
+
const explicitConfigPath = cleanString(configPath);
|
|
87
|
+
const defaultConfigPath = skillRoot ? join(skillRoot, "config", "minimax.json") : undefined;
|
|
88
|
+
const filePath = explicitConfigPath || defaultConfigPath;
|
|
89
|
+
|
|
90
|
+
if (filePath && await pathExists(filePath)) {
|
|
91
|
+
return {
|
|
92
|
+
source: "file",
|
|
93
|
+
configPath: filePath,
|
|
94
|
+
config: validateMinimaxConfig(await readJson(filePath)),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const envConfig = normalize({
|
|
99
|
+
api_key: env.MINIMAX_API_KEY,
|
|
100
|
+
group_id: env.MINIMAX_GROUP_ID,
|
|
101
|
+
model: env.MINIMAX_MODEL,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!hasValues(envConfig)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
source: "environment",
|
|
110
|
+
configPath: null,
|
|
111
|
+
config: validateMinimaxConfig(envConfig),
|
|
112
|
+
};
|
|
113
|
+
}
|