hyper-animator-codex 0.1.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 CHANGED
@@ -37,12 +37,71 @@ 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.
43
74
  - `skills/hyper-animator-codex/references/`: HyperFrames catalog map, workflow guide, pseudocode, and request examples.
44
75
  - `skills/hyper-animator-codex/scripts/validate_hyperframes_html.py`: static pre-render HTML quality gate.
45
76
 
77
+ ## Optional Beat Detection
78
+
79
+ The skill can analyze background music and guide Codex to align HyperFrames/GSAP transitions to beats, bars, energy peaks, and detected music segments.
80
+
81
+ Install optional Python dependencies:
82
+
83
+ ```bash
84
+ python3 -m pip install librosa pydub numpy click
85
+ ```
86
+
87
+ Analyze WAV/FLAC/OGG without ffmpeg. MP3/M4A/AAC/WMA require system `ffmpeg`.
88
+
89
+ Run the bundled analyzer from an installed skill directory or repository checkout:
90
+
91
+ ```bash
92
+ python3 skills/hyper-animator-codex/scripts/analyze_music_beats.py path/to/music.wav -o beat-map.json --fps 60 --pretty
93
+ ```
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
+
46
105
  ## Development
47
106
 
48
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 Replace an existing installed skill
17
- --target <dir> Install into a specific Codex skills directory
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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyper-animator-codex",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Install the Hyper Animator Codex skill for Codex.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: hyper-animator-codex
3
- description: Use when a user asks Codex to create, plan, author, customize, validate, preview, or render a HyperFrames, HTML, or GSAP animation/video from natural-language requirements, including product demos, code demos, data videos, podcast captions, social shorts, catalog block/component assembly, or new HyperFrames HTML.
3
+ description: Use when a user asks Codex to create, plan, author, customize, validate, preview, or render a HyperFrames, HTML, or GSAP animation/video from natural-language requirements, including product demos, code demos, data videos, podcast captions, social shorts, catalog assembly, new HyperFrames HTML, sound effects, background music, rhythm, BPM, or beat-synced transitions.
4
4
  ---
5
5
 
6
6
  # Hyper Animator Codex
@@ -18,11 +18,14 @@ Turn a natural-language animation or video brief into a validated HyperFrames HT
18
18
  5. Decide generation mode:
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
- 6. Ask the second clarification round with candidate context: visual direction, motion rhythm, generation mode, and any candidate tradeoffs.
22
- 7. Write or assemble HTML.
23
- 8. Run pre-render quality gates.
24
- 9. Show a concise plan summary and preview path or HTML file to the user. Ask for confirmation before video render.
25
- 10. Render only after user confirmation, then report output path and any caveats.
21
+ 6. Clarify audio: ask whether to add animation/transition sound effects and whether to add background music.
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.
26
29
 
27
30
  ## Interactive Questions
28
31
 
@@ -30,8 +33,8 @@ Prefer Codex interactive question tools such as `AskUserQuestion` or `request_us
30
33
 
31
34
  Use two rounds:
32
35
 
33
- - Round 1: purpose, format, duration, platform, required content, brand assets, and whether video render is expected in this turn.
34
- - Round 2: after catalog scoring, ask about style, motion, candidate selection, and generation mode.
36
+ - Round 1: purpose, format, duration, platform, required content, brand assets, sound effects, background music, and whether video render is expected in this turn.
37
+ - Round 2: after catalog scoring, ask about style, motion, candidate selection, generation mode, and beat-sync assumptions when background music is present.
35
38
 
36
39
  Do not ask everything upfront when the brief is already specific. Ask only for missing or decision-changing information.
37
40
 
@@ -40,6 +43,8 @@ Do not ask everything upfront when the brief is already specific. Ask only for m
40
43
  - Read `references/hyperframes-intent-workflow.md` for the full AskUserQuestion workflow, generation-mode rules, scoring model, and render confirmation requirements.
41
44
  - Read `references/hyperframes-catalog-map.json` whenever selecting catalog candidates or determining visual references.
42
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.
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.
43
48
  - Use `references/examples/*.json` for sanity checks against common request shapes.
44
49
 
45
50
  ## Catalog Rules
@@ -79,6 +84,9 @@ Before rendering, summarize:
79
84
 
80
85
  - generation mode;
81
86
  - selected or referenced catalog items;
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;
89
+ - beat map path, BPM, duration, and timing assumptions when beat-sync is enabled;
82
90
  - dimensions and duration;
83
91
  - content assumptions;
84
92
  - preview location;
@@ -0,0 +1,51 @@
1
+ # Beat Sync Workflow
2
+
3
+ Use this when the user mentions sound effects, background music, soundtrack, beat sync, rhythm, BPM, transitions to music, or audio-reactive animation.
4
+
5
+ ## Clarify Audio
6
+
7
+ Ask:
8
+
9
+ - Should animation or transition sound effects be added?
10
+ - Should background music be added?
11
+ - If background music is used, what is the local audio path?
12
+ - May scene timing, transition timing, and animation accents be adjusted to match the music?
13
+ - Should the music be trimmed, looped, or should video duration follow the selected music region?
14
+
15
+ ## Analyze Music
16
+
17
+ When a background music file is provided:
18
+
19
+ ```bash
20
+ python3 scripts/analyze_music_beats.py path/to/music.wav -o path/to/beat-map.json --fps 60 --pretty
21
+ ```
22
+
23
+ If dependencies are missing, tell the user:
24
+
25
+ ```bash
26
+ python3 -m pip install librosa pydub numpy click
27
+ ```
28
+
29
+ MP3/M4A/AAC/WMA require system `ffmpeg`; WAV/FLAC/OGG are safer.
30
+
31
+ ## Map Beats To Animation
32
+
33
+ - Use `meta.bpm` for global pacing.
34
+ - Use beats with `beat_in_bar: 1` for major scene changes, title reveals, camera moves, and transitions.
35
+ - Use `energy_level: strong` for visual accents, scale pulses, light sweeps, flashes, or cut emphasis.
36
+ - Use weak beats for secondary motion only.
37
+ - Use `structure.segments` to shape intro, main body, and outro.
38
+ - Use `structure.energy_peaks` for high-impact moments.
39
+ - Keep `data-duration` consistent with the selected music region or state trim/loop assumptions.
40
+
41
+ ## GSAP Timing
42
+
43
+ Convert beat times from milliseconds to seconds:
44
+
45
+ ```js
46
+ const beatSeconds = beatMap.beats.map((beat) => beat.time_ms / 1000);
47
+ tl.addLabel("downbeat_1", beatSeconds[0]);
48
+ tl.to(".hero", { opacity: 1, y: 0, duration: 0.35 }, "downbeat_1");
49
+ ```
50
+
51
+ Do not use wall-clock audio playback to drive render progress. The render timeline remains a paused GSAP timeline registered in `window.__timelines`.
@@ -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,78 @@
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ sys.dont_write_bytecode = True
9
+
10
+ SCRIPT_DIR = Path(__file__).resolve().parent
11
+ VENDOR_ROOT = SCRIPT_DIR.parent / "vendor" / "music-beat-detector"
12
+ sys.path.insert(0, str(VENDOR_ROOT))
13
+
14
+
15
+ def parse_args() -> argparse.Namespace:
16
+ parser = argparse.ArgumentParser(
17
+ description="Analyze background music and write a beat map JSON for HyperFrames timing."
18
+ )
19
+ parser.add_argument("audio_file", help="Path to WAV, FLAC, OGG, MP3, M4A, AAC, or WMA audio")
20
+ parser.add_argument("-o", "--output", help="Output JSON path. Prints JSON to stdout when omitted.")
21
+ parser.add_argument("--fps", type=int, default=60, help="Timeline frame rate for frame indexes. Default: 60")
22
+ parser.add_argument("--pretty", action="store_true", help="Pretty-print JSON")
23
+ parser.add_argument(
24
+ "--log-level",
25
+ default="error",
26
+ choices=["debug", "info", "warning", "error"],
27
+ help="Detector log level",
28
+ )
29
+ return parser.parse_args()
30
+
31
+
32
+ def main() -> int:
33
+ args = parse_args()
34
+ audio_path = Path(args.audio_file)
35
+ if not audio_path.exists():
36
+ print(f"Error: audio file does not exist: {audio_path}", file=sys.stderr)
37
+ return 1
38
+
39
+ try:
40
+ from beat_detector import analyze
41
+ from beat_detector.errors import BeatDetectorError, FFmpegRequiredError
42
+ except ModuleNotFoundError as exc:
43
+ print(
44
+ "Error: missing Python dependency for beat detection. "
45
+ "Install optional dependencies with: python3 -m pip install librosa pydub numpy click",
46
+ file=sys.stderr,
47
+ )
48
+ print(f"Missing module: {exc.name}", file=sys.stderr)
49
+ return 10
50
+
51
+ try:
52
+ result = analyze(str(audio_path), fps=args.fps, log_level=args.log_level)
53
+ if args.output:
54
+ result.save(args.output, pretty=args.pretty)
55
+ print(f"Beat map saved to: {args.output}")
56
+ else:
57
+ print(result.to_json(pretty=args.pretty))
58
+ return 0
59
+ except FFmpegRequiredError as exc:
60
+ print(f"Error: {exc}", file=sys.stderr)
61
+ print("Install ffmpeg or use WAV/FLAC/OGG audio for fewer runtime dependencies.", file=sys.stderr)
62
+ return 4
63
+ except BeatDetectorError as exc:
64
+ message = getattr(exc, "message", str(exc))
65
+ print(f"Error: {message}", file=sys.stderr)
66
+ return 3
67
+ except ModuleNotFoundError as exc:
68
+ print(
69
+ "Error: missing Python dependency for beat detection. "
70
+ "Install optional dependencies with: python3 -m pip install librosa pydub numpy click",
71
+ file=sys.stderr,
72
+ )
73
+ print(f"Missing module: {exc.name}", file=sys.stderr)
74
+ return 10
75
+
76
+
77
+ if __name__ == "__main__":
78
+ raise SystemExit(main())