hailuo-cli 2026.7.31.0__py3-none-any.whl
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.
- hailuo_cli/__init__.py +1 -0
- hailuo_cli/__main__.py +5 -0
- hailuo_cli/commands/__init__.py +1 -0
- hailuo_cli/commands/info.py +30 -0
- hailuo_cli/commands/task.py +160 -0
- hailuo_cli/commands/video.py +128 -0
- hailuo_cli/core/__init__.py +1 -0
- hailuo_cli/core/client.py +107 -0
- hailuo_cli/core/config.py +39 -0
- hailuo_cli/core/exceptions.py +37 -0
- hailuo_cli/core/output.py +116 -0
- hailuo_cli/main.py +69 -0
- hailuo_cli-2026.7.31.0.dist-info/METADATA +162 -0
- hailuo_cli-2026.7.31.0.dist-info/RECORD +17 -0
- hailuo_cli-2026.7.31.0.dist-info/WHEEL +4 -0
- hailuo_cli-2026.7.31.0.dist-info/entry_points.txt +3 -0
- hailuo_cli-2026.7.31.0.dist-info/licenses/LICENSE +21 -0
hailuo_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Hailuo CLI - AI Video Generation via AceDataCloud API."""
|
hailuo_cli/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Commands package for Hailuo CLI."""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Info and utility commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from hailuo_cli.core.config import settings
|
|
6
|
+
from hailuo_cli.core.output import console, print_models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
def models() -> None:
|
|
11
|
+
"""List available Hailuo models."""
|
|
12
|
+
print_models()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
def config() -> None:
|
|
17
|
+
"""Show current configuration."""
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
table = Table(title="Hailuo CLI Configuration")
|
|
21
|
+
table.add_column("Setting", style="bold cyan")
|
|
22
|
+
table.add_column("Value")
|
|
23
|
+
|
|
24
|
+
table.add_row("API Base URL", settings.api_base_url)
|
|
25
|
+
table.add_row(
|
|
26
|
+
"API Token", f"{settings.api_token[:8]}..." if settings.api_token else "[red]Not set[/red]"
|
|
27
|
+
)
|
|
28
|
+
table.add_row("Request Timeout", f"{settings.request_timeout}s")
|
|
29
|
+
|
|
30
|
+
console.print(table)
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Task management commands."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from hailuo_cli.core.client import get_client
|
|
8
|
+
from hailuo_cli.core.exceptions import HailuoError
|
|
9
|
+
from hailuo_cli.core.output import (
|
|
10
|
+
print_error,
|
|
11
|
+
print_json,
|
|
12
|
+
print_success,
|
|
13
|
+
print_task_result,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.argument("task_id")
|
|
19
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def task(
|
|
22
|
+
ctx: click.Context,
|
|
23
|
+
task_id: str,
|
|
24
|
+
output_json: bool,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Query a single task status.
|
|
27
|
+
|
|
28
|
+
TASK_ID is the task ID returned from generate commands.
|
|
29
|
+
|
|
30
|
+
\b
|
|
31
|
+
Examples:
|
|
32
|
+
hailuo task abc123-def456
|
|
33
|
+
"""
|
|
34
|
+
client = get_client(ctx.obj.get("token"))
|
|
35
|
+
try:
|
|
36
|
+
result = client.query_task(id=task_id, action="retrieve")
|
|
37
|
+
if output_json:
|
|
38
|
+
print_json(result)
|
|
39
|
+
else:
|
|
40
|
+
print_task_result(result)
|
|
41
|
+
except HailuoError as e:
|
|
42
|
+
print_error(e.message)
|
|
43
|
+
raise SystemExit(1) from e
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@click.command("tasks")
|
|
47
|
+
@click.argument("task_ids", nargs=-1, required=True)
|
|
48
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
49
|
+
@click.pass_context
|
|
50
|
+
def tasks_batch(
|
|
51
|
+
ctx: click.Context,
|
|
52
|
+
task_ids: tuple[str, ...],
|
|
53
|
+
output_json: bool,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Query multiple tasks at once.
|
|
56
|
+
|
|
57
|
+
TASK_IDS are space-separated task IDs.
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
Examples:
|
|
61
|
+
hailuo tasks abc123 def456 ghi789
|
|
62
|
+
"""
|
|
63
|
+
client = get_client(ctx.obj.get("token"))
|
|
64
|
+
try:
|
|
65
|
+
result = client.query_task(ids=list(task_ids), action="retrieve_batch")
|
|
66
|
+
if output_json:
|
|
67
|
+
print_json(result)
|
|
68
|
+
else:
|
|
69
|
+
print_task_result(result)
|
|
70
|
+
except HailuoError as e:
|
|
71
|
+
print_error(e.message)
|
|
72
|
+
raise SystemExit(1) from e
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@click.command()
|
|
76
|
+
@click.argument("task_id")
|
|
77
|
+
@click.option(
|
|
78
|
+
"--interval",
|
|
79
|
+
type=int,
|
|
80
|
+
default=5,
|
|
81
|
+
help="Polling interval in seconds (default: 5).",
|
|
82
|
+
)
|
|
83
|
+
@click.option(
|
|
84
|
+
"--timeout",
|
|
85
|
+
"max_timeout",
|
|
86
|
+
type=int,
|
|
87
|
+
default=600,
|
|
88
|
+
help="Maximum wait time in seconds (default: 600).",
|
|
89
|
+
)
|
|
90
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def wait(
|
|
93
|
+
ctx: click.Context,
|
|
94
|
+
task_id: str,
|
|
95
|
+
interval: int,
|
|
96
|
+
max_timeout: int,
|
|
97
|
+
output_json: bool,
|
|
98
|
+
) -> None:
|
|
99
|
+
"""Wait for a task to complete, polling periodically.
|
|
100
|
+
|
|
101
|
+
TASK_ID is the task ID to monitor.
|
|
102
|
+
|
|
103
|
+
\b
|
|
104
|
+
Examples:
|
|
105
|
+
hailuo wait abc123
|
|
106
|
+
hailuo wait abc123 --interval 10 --timeout 300
|
|
107
|
+
"""
|
|
108
|
+
client = get_client(ctx.obj.get("token"))
|
|
109
|
+
elapsed = 0
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
while elapsed < max_timeout:
|
|
113
|
+
result = client.query_task(id=task_id, action="retrieve")
|
|
114
|
+
|
|
115
|
+
# Check for completion signals
|
|
116
|
+
data = result.get("data", result)
|
|
117
|
+
if isinstance(data, dict):
|
|
118
|
+
item = data
|
|
119
|
+
elif isinstance(data, list) and data:
|
|
120
|
+
item = data[0]
|
|
121
|
+
else:
|
|
122
|
+
item = result
|
|
123
|
+
|
|
124
|
+
state = item.get("state", item.get("status", ""))
|
|
125
|
+
video_url = item.get("video_url") or result.get("video_url")
|
|
126
|
+
error = item.get("error") or result.get("error")
|
|
127
|
+
|
|
128
|
+
if error:
|
|
129
|
+
if output_json:
|
|
130
|
+
print_json(result)
|
|
131
|
+
else:
|
|
132
|
+
print_error(f"Task {task_id} failed: {error}")
|
|
133
|
+
raise SystemExit(1)
|
|
134
|
+
|
|
135
|
+
if state in ("succeeded", "completed", "complete") or video_url:
|
|
136
|
+
if output_json:
|
|
137
|
+
print_json(result)
|
|
138
|
+
else:
|
|
139
|
+
print_success(f"Task {task_id} completed!")
|
|
140
|
+
print_task_result(result)
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
if state in ("failed", "error"):
|
|
144
|
+
if output_json:
|
|
145
|
+
print_json(result)
|
|
146
|
+
else:
|
|
147
|
+
print_error(f"Task {task_id} failed.")
|
|
148
|
+
raise SystemExit(1)
|
|
149
|
+
|
|
150
|
+
if not output_json:
|
|
151
|
+
click.echo(f"Status: {state or 'pending'} (waited {elapsed}s)...", err=True)
|
|
152
|
+
|
|
153
|
+
time.sleep(interval)
|
|
154
|
+
elapsed += interval
|
|
155
|
+
|
|
156
|
+
print_error(f"Timeout: task {task_id} did not complete within {max_timeout}s")
|
|
157
|
+
raise SystemExit(1)
|
|
158
|
+
except HailuoError as e:
|
|
159
|
+
print_error(e.message)
|
|
160
|
+
raise SystemExit(1) from e
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Video generation commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from hailuo_cli.core.client import get_client
|
|
6
|
+
from hailuo_cli.core.exceptions import HailuoError
|
|
7
|
+
from hailuo_cli.core.output import (
|
|
8
|
+
DEFAULT_MODEL,
|
|
9
|
+
HAILUO_MODELS,
|
|
10
|
+
print_error,
|
|
11
|
+
print_json,
|
|
12
|
+
print_video_result,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command()
|
|
17
|
+
@click.argument("prompt")
|
|
18
|
+
@click.option(
|
|
19
|
+
"-m",
|
|
20
|
+
"--model",
|
|
21
|
+
type=click.Choice(HAILUO_MODELS),
|
|
22
|
+
default=DEFAULT_MODEL,
|
|
23
|
+
help="Hailuo model to use (default: minimax-t2v).",
|
|
24
|
+
)
|
|
25
|
+
@click.option("--callback-url", default=None, help="Webhook callback URL.")
|
|
26
|
+
@click.option(
|
|
27
|
+
"--async",
|
|
28
|
+
"async_mode",
|
|
29
|
+
is_flag=True,
|
|
30
|
+
default=False,
|
|
31
|
+
help="Submit asynchronously; returns a task_id to poll instead of waiting.",
|
|
32
|
+
)
|
|
33
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
34
|
+
@click.pass_context
|
|
35
|
+
def generate(
|
|
36
|
+
ctx: click.Context,
|
|
37
|
+
prompt: str,
|
|
38
|
+
model: str,
|
|
39
|
+
callback_url: str | None,
|
|
40
|
+
async_mode: bool,
|
|
41
|
+
output_json: bool,
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Generate a video from a text prompt.
|
|
44
|
+
|
|
45
|
+
PROMPT is a detailed description of what to generate.
|
|
46
|
+
|
|
47
|
+
\b
|
|
48
|
+
Examples:
|
|
49
|
+
hailuo generate "A cat playing in the snow"
|
|
50
|
+
hailuo generate "Ocean waves at sunset" --model minimax-t2v
|
|
51
|
+
"""
|
|
52
|
+
client = get_client(ctx.obj.get("token"))
|
|
53
|
+
try:
|
|
54
|
+
payload: dict[str, object] = {
|
|
55
|
+
"action": "generate",
|
|
56
|
+
"prompt": prompt,
|
|
57
|
+
"model": model,
|
|
58
|
+
"callback_url": callback_url,
|
|
59
|
+
"async": async_mode,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result = client.generate_video(**payload) # type: ignore[arg-type]
|
|
63
|
+
if output_json:
|
|
64
|
+
print_json(result)
|
|
65
|
+
else:
|
|
66
|
+
print_video_result(result)
|
|
67
|
+
except HailuoError as e:
|
|
68
|
+
print_error(e.message)
|
|
69
|
+
raise SystemExit(1) from e
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@click.command("image-to-video")
|
|
73
|
+
@click.argument("prompt")
|
|
74
|
+
@click.option("--image-url", required=True, help="URL of the first frame reference image.")
|
|
75
|
+
@click.option(
|
|
76
|
+
"-m",
|
|
77
|
+
"--model",
|
|
78
|
+
type=click.Choice(["minimax-i2v", "minimax-t2v", "minimax-i2v-director"]),
|
|
79
|
+
default="minimax-i2v",
|
|
80
|
+
help="Hailuo image-to-video model (default: minimax-i2v).",
|
|
81
|
+
)
|
|
82
|
+
@click.option("--callback-url", default=None, help="Webhook callback URL.")
|
|
83
|
+
@click.option(
|
|
84
|
+
"--async",
|
|
85
|
+
"async_mode",
|
|
86
|
+
is_flag=True,
|
|
87
|
+
default=False,
|
|
88
|
+
help="Submit asynchronously; returns a task_id to poll instead of waiting.",
|
|
89
|
+
)
|
|
90
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
91
|
+
@click.pass_context
|
|
92
|
+
def image_to_video(
|
|
93
|
+
ctx: click.Context,
|
|
94
|
+
prompt: str,
|
|
95
|
+
image_url: str,
|
|
96
|
+
model: str,
|
|
97
|
+
callback_url: str | None,
|
|
98
|
+
async_mode: bool,
|
|
99
|
+
output_json: bool,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Generate a video from an image and text prompt.
|
|
102
|
+
|
|
103
|
+
PROMPT describes the desired video content.
|
|
104
|
+
|
|
105
|
+
\b
|
|
106
|
+
Examples:
|
|
107
|
+
hailuo image-to-video "Animate this scene" --image-url https://example.com/photo.jpg
|
|
108
|
+
hailuo image-to-video "Cinematic pan" --image-url img.jpg --model minimax-i2v-director
|
|
109
|
+
"""
|
|
110
|
+
client = get_client(ctx.obj.get("token"))
|
|
111
|
+
try:
|
|
112
|
+
payload: dict[str, object] = {
|
|
113
|
+
"action": "generate",
|
|
114
|
+
"prompt": prompt,
|
|
115
|
+
"model": model,
|
|
116
|
+
"first_image_url": image_url,
|
|
117
|
+
"callback_url": callback_url,
|
|
118
|
+
"async": async_mode,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result = client.generate_video(**payload) # type: ignore[arg-type]
|
|
122
|
+
if output_json:
|
|
123
|
+
print_json(result)
|
|
124
|
+
else:
|
|
125
|
+
print_video_result(result)
|
|
126
|
+
except HailuoError as e:
|
|
127
|
+
print_error(e.message)
|
|
128
|
+
raise SystemExit(1) from e
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core package for Hailuo CLI."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""HTTP client for Hailuo API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from hailuo_cli.core.config import settings
|
|
8
|
+
from hailuo_cli.core.exceptions import (
|
|
9
|
+
HailuoAPIError,
|
|
10
|
+
HailuoAuthError,
|
|
11
|
+
HailuoTimeoutError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class HailuoClient:
|
|
16
|
+
"""HTTP client for AceDataCloud Hailuo API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_token: str | None = None, base_url: str | None = None):
|
|
19
|
+
self.api_token = api_token if api_token is not None else settings.api_token
|
|
20
|
+
self.base_url = base_url or settings.api_base_url
|
|
21
|
+
self.timeout = settings.request_timeout
|
|
22
|
+
|
|
23
|
+
def _get_headers(self) -> dict[str, str]:
|
|
24
|
+
"""Get request headers with authentication."""
|
|
25
|
+
if not self.api_token:
|
|
26
|
+
raise HailuoAuthError("API token not configured")
|
|
27
|
+
return {
|
|
28
|
+
"accept": "application/json",
|
|
29
|
+
"authorization": f"Bearer {self.api_token}",
|
|
30
|
+
"content-type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
def request(
|
|
34
|
+
self,
|
|
35
|
+
endpoint: str,
|
|
36
|
+
payload: dict[str, Any],
|
|
37
|
+
timeout: float | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Make a POST request to the Hailuo API.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
endpoint: API endpoint path
|
|
43
|
+
payload: Request body as dictionary
|
|
44
|
+
timeout: Optional timeout override
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
API response as dictionary
|
|
48
|
+
"""
|
|
49
|
+
url = f"{self.base_url}{endpoint}"
|
|
50
|
+
request_timeout = timeout or self.timeout
|
|
51
|
+
|
|
52
|
+
# Remove None values from payload
|
|
53
|
+
payload = {k: v for k, v in payload.items() if v is not None}
|
|
54
|
+
|
|
55
|
+
with httpx.Client() as http_client:
|
|
56
|
+
try:
|
|
57
|
+
response = http_client.post(
|
|
58
|
+
url,
|
|
59
|
+
json=payload,
|
|
60
|
+
headers=self._get_headers(),
|
|
61
|
+
timeout=request_timeout,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if response.status_code == 401:
|
|
65
|
+
raise HailuoAuthError("Invalid API token")
|
|
66
|
+
|
|
67
|
+
if response.status_code == 403:
|
|
68
|
+
raise HailuoAuthError("Access denied. Check your API permissions.")
|
|
69
|
+
|
|
70
|
+
response.raise_for_status()
|
|
71
|
+
return response.json() # type: ignore[no-any-return]
|
|
72
|
+
|
|
73
|
+
except httpx.TimeoutException as e:
|
|
74
|
+
raise HailuoTimeoutError(
|
|
75
|
+
f"Request to {endpoint} timed out after {request_timeout}s"
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
except HailuoAuthError:
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
except httpx.HTTPStatusError as e:
|
|
82
|
+
raise HailuoAPIError(
|
|
83
|
+
message=e.response.text,
|
|
84
|
+
code=f"http_{e.response.status_code}",
|
|
85
|
+
status_code=e.response.status_code,
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
if isinstance(e, HailuoAPIError | HailuoTimeoutError):
|
|
90
|
+
raise
|
|
91
|
+
raise HailuoAPIError(message=str(e)) from e
|
|
92
|
+
|
|
93
|
+
# Convenience methods
|
|
94
|
+
def generate_video(self, **kwargs: Any) -> dict[str, Any]:
|
|
95
|
+
"""Generate video using the videos endpoint."""
|
|
96
|
+
return self.request("/hailuo/videos", kwargs)
|
|
97
|
+
|
|
98
|
+
def query_task(self, **kwargs: Any) -> dict[str, Any]:
|
|
99
|
+
"""Query task status using the tasks endpoint."""
|
|
100
|
+
return self.request("/hailuo/tasks", kwargs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_client(token: str | None = None) -> HailuoClient:
|
|
104
|
+
"""Get a HailuoClient instance, optionally overriding the token."""
|
|
105
|
+
if token:
|
|
106
|
+
return HailuoClient(api_token=token)
|
|
107
|
+
return HailuoClient()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Configuration management for Hailuo CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
load_dotenv()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Settings:
|
|
13
|
+
"""Application settings loaded from environment variables."""
|
|
14
|
+
|
|
15
|
+
api_base_url: str = field(
|
|
16
|
+
default_factory=lambda: os.environ.get(
|
|
17
|
+
"ACEDATACLOUD_API_BASE_URL", "https://api.acedata.cloud"
|
|
18
|
+
)
|
|
19
|
+
)
|
|
20
|
+
api_token: str = field(default_factory=lambda: os.environ.get("ACEDATACLOUD_API_TOKEN", ""))
|
|
21
|
+
request_timeout: float = field(
|
|
22
|
+
default_factory=lambda: float(os.environ.get("HAILUO_REQUEST_TIMEOUT", "1800"))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def is_configured(self) -> bool:
|
|
27
|
+
"""Check if the API token is configured."""
|
|
28
|
+
return bool(self.api_token)
|
|
29
|
+
|
|
30
|
+
def validate(self) -> None:
|
|
31
|
+
"""Validate configuration. Raises ValueError if API token is missing."""
|
|
32
|
+
if not self.api_token:
|
|
33
|
+
raise ValueError(
|
|
34
|
+
"API token not configured. "
|
|
35
|
+
"Set ACEDATACLOUD_API_TOKEN environment variable or use --token option."
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
settings = Settings()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Custom exceptions for Hailuo CLI."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HailuoError(Exception):
|
|
5
|
+
"""Base exception for Hailuo CLI."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: str = "unknown"):
|
|
8
|
+
self.message = message
|
|
9
|
+
self.code = code
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HailuoAuthError(HailuoError):
|
|
14
|
+
"""Authentication error."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
17
|
+
super().__init__(message, code="auth_error")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HailuoAPIError(HailuoError):
|
|
21
|
+
"""API error with HTTP status code."""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
message: str = "API request failed",
|
|
26
|
+
code: str = "api_error",
|
|
27
|
+
status_code: int | None = None,
|
|
28
|
+
):
|
|
29
|
+
self.status_code = status_code
|
|
30
|
+
super().__init__(message, code)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class HailuoTimeoutError(HailuoError):
|
|
34
|
+
"""Request timeout error."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str = "Request timed out"):
|
|
37
|
+
super().__init__(message, code="timeout_error")
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Rich terminal output formatting for Hailuo CLI."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.panel import Panel
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
# Available models
|
|
13
|
+
HAILUO_MODELS = [
|
|
14
|
+
"minimax-t2v",
|
|
15
|
+
"minimax-i2v",
|
|
16
|
+
"minimax-i2v-director",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
DEFAULT_MODEL = "minimax-t2v"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def print_json(data: Any) -> None:
|
|
23
|
+
"""Print data as formatted JSON."""
|
|
24
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def print_error(message: str) -> None:
|
|
28
|
+
"""Print an error message."""
|
|
29
|
+
console.print(f"[bold red]Error:[/bold red] {message}")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def print_success(message: str) -> None:
|
|
33
|
+
"""Print a success message."""
|
|
34
|
+
console.print(f"[bold green]\u2713[/bold green] {message}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def print_video_result(data: dict[str, Any]) -> None:
|
|
38
|
+
"""Print video generation result in a rich format."""
|
|
39
|
+
task_id = data.get("task_id", "N/A")
|
|
40
|
+
trace_id = data.get("trace_id", "N/A")
|
|
41
|
+
|
|
42
|
+
console.print(
|
|
43
|
+
Panel(
|
|
44
|
+
f"[bold]Task ID:[/bold] {task_id}\n[bold]Trace ID:[/bold] {trace_id}",
|
|
45
|
+
title="[bold green]Video Result[/bold green]",
|
|
46
|
+
border_style="green",
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
video_url = data.get("video_url")
|
|
51
|
+
if video_url:
|
|
52
|
+
console.print(f"[bold]Video URL:[/bold] {video_url}")
|
|
53
|
+
elif not data.get("task_id"):
|
|
54
|
+
console.print("[yellow]No video available yet. Use 'task' to check status.[/yellow]")
|
|
55
|
+
else:
|
|
56
|
+
console.print("[yellow]Video is being generated. Use 'task' to check status.[/yellow]")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def print_task_result(data: dict[str, Any]) -> None:
|
|
60
|
+
"""Print task query result in a rich format."""
|
|
61
|
+
# Handle single task response
|
|
62
|
+
if isinstance(data.get("data"), dict):
|
|
63
|
+
task_data = data["data"]
|
|
64
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
65
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
66
|
+
table.add_column("Value")
|
|
67
|
+
for key in ["id", "status", "state", "video_url", "model", "created_at"]:
|
|
68
|
+
if task_data.get(key):
|
|
69
|
+
table.add_row(key.replace("_", " ").title(), str(task_data[key]))
|
|
70
|
+
console.print(table)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
# Handle batch response
|
|
74
|
+
items = data.get("data", [])
|
|
75
|
+
if isinstance(items, list) and items:
|
|
76
|
+
for item in items:
|
|
77
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
78
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
79
|
+
table.add_column("Value")
|
|
80
|
+
for key in ["id", "status", "state", "video_url", "model", "created_at"]:
|
|
81
|
+
if item.get(key):
|
|
82
|
+
table.add_row(key.replace("_", " ").title(), str(item[key]))
|
|
83
|
+
console.print(table)
|
|
84
|
+
console.print()
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
# Handle direct top-level task fields
|
|
88
|
+
if data.get("task_id") or data.get("video_url"):
|
|
89
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
90
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
91
|
+
table.add_column("Value")
|
|
92
|
+
for key in ["task_id", "video_url", "model", "status", "state"]:
|
|
93
|
+
val = data.get(key)
|
|
94
|
+
if val:
|
|
95
|
+
table.add_row(key.replace("_", " ").title(), str(val))
|
|
96
|
+
console.print(table)
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
console.print("[yellow]No data available.[/yellow]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def print_models() -> None:
|
|
103
|
+
"""Print available Hailuo models."""
|
|
104
|
+
table = Table(title="Available Hailuo Models")
|
|
105
|
+
table.add_column("Model", style="bold cyan")
|
|
106
|
+
table.add_column("Type")
|
|
107
|
+
table.add_column("Notes")
|
|
108
|
+
|
|
109
|
+
table.add_row("minimax-t2v", "Text-to-Video", "Generate video from text prompt (default)")
|
|
110
|
+
table.add_row("minimax-i2v", "Image-to-Video", "Generate video from image + text")
|
|
111
|
+
table.add_row(
|
|
112
|
+
"minimax-i2v-director", "Image-to-Video", "Cinematic director mode with image reference"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
console.print(f"\n[dim]Default model: {DEFAULT_MODEL}[/dim]")
|
hailuo_cli/main.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Hailuo CLI - AI Video Generation via AceDataCloud API.
|
|
4
|
+
|
|
5
|
+
A command-line tool for generating AI videos using Hailuo (MiniMax)
|
|
6
|
+
through the AceDataCloud platform.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
from hailuo_cli.commands.info import config, models
|
|
15
|
+
from hailuo_cli.commands.task import task, tasks_batch, wait
|
|
16
|
+
from hailuo_cli.commands.video import generate, image_to_video
|
|
17
|
+
|
|
18
|
+
load_dotenv()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_version() -> str:
|
|
22
|
+
"""Get the package version."""
|
|
23
|
+
try:
|
|
24
|
+
return metadata.version("hailuo-cli")
|
|
25
|
+
except metadata.PackageNotFoundError:
|
|
26
|
+
return "dev"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
@click.version_option(version=get_version(), prog_name="hailuo-cli")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--token",
|
|
33
|
+
envvar="ACEDATACLOUD_API_TOKEN",
|
|
34
|
+
help="API token (or set ACEDATACLOUD_API_TOKEN env var).",
|
|
35
|
+
)
|
|
36
|
+
@click.pass_context
|
|
37
|
+
def cli(ctx: click.Context, token: str | None) -> None:
|
|
38
|
+
"""Hailuo CLI - AI Video Generation powered by AceDataCloud.
|
|
39
|
+
|
|
40
|
+
Generate AI videos from the command line using Hailuo (MiniMax) models.
|
|
41
|
+
|
|
42
|
+
Get your API token at https://platform.acedata.cloud
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Examples:
|
|
46
|
+
hailuo generate "A cat playing in the snow"
|
|
47
|
+
hailuo image-to-video "Animate this" --image-url https://example.com/img.jpg
|
|
48
|
+
hailuo task abc123-def456
|
|
49
|
+
hailuo wait abc123 --interval 5
|
|
50
|
+
|
|
51
|
+
Set your token:
|
|
52
|
+
export ACEDATACLOUD_API_TOKEN=your_token
|
|
53
|
+
"""
|
|
54
|
+
ctx.ensure_object(dict)
|
|
55
|
+
ctx.obj["token"] = token
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Register commands
|
|
59
|
+
cli.add_command(generate)
|
|
60
|
+
cli.add_command(image_to_video)
|
|
61
|
+
cli.add_command(task)
|
|
62
|
+
cli.add_command(tasks_batch)
|
|
63
|
+
cli.add_command(wait)
|
|
64
|
+
cli.add_command(models)
|
|
65
|
+
cli.add_command(config)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
cli()
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: hailuo-cli
|
|
3
|
+
Version: 2026.7.31.0
|
|
4
|
+
Summary: CLI tool for Hailuo AI Video Generation via AceDataCloud API
|
|
5
|
+
Project-URL: Homepage, https://github.com/AceDataCloud/HailuoCli
|
|
6
|
+
Project-URL: Repository, https://github.com/AceDataCloud/HailuoCli
|
|
7
|
+
Project-URL: Issues, https://github.com/AceDataCloud/HailuoCli/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/AceDataCloud/HailuoCli/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: AceDataCloud <support@acedata.cloud>
|
|
10
|
+
Maintainer-email: AceDataCloud <support@acedata.cloud>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: acedata,ai,cli,command-line,generation,hailuo,minimax,video
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Multimedia :: Video
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: click>=8.1.0
|
|
27
|
+
Requires-Dist: httpx>=0.27.0
|
|
28
|
+
Requires-Dist: pydantic>=2.0.0
|
|
29
|
+
Requires-Dist: python-dotenv>=1.0.0
|
|
30
|
+
Requires-Dist: rich>=13.0.0
|
|
31
|
+
Provides-Extra: all
|
|
32
|
+
Requires-Dist: hailuo-cli[dev,release,test]; extra == 'all'
|
|
33
|
+
Provides-Extra: dev
|
|
34
|
+
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pre-commit>=3.7.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: release
|
|
38
|
+
Requires-Dist: build>=1.2.0; extra == 'release'
|
|
39
|
+
Requires-Dist: twine>=6.1.0; extra == 'release'
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'test'
|
|
42
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'test'
|
|
43
|
+
Requires-Dist: pytest>=8.0.0; extra == 'test'
|
|
44
|
+
Requires-Dist: respx>=0.21.0; extra == 'test'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# Hailuo CLI
|
|
48
|
+
|
|
49
|
+
A command-line tool for generating AI videos using [Hailuo (MiniMax)](https://platform.acedata.cloud) through the AceDataCloud platform.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install hailuo-cli
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
export ACEDATACLOUD_API_TOKEN=your_token
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or copy `.env.example` to `.env` and fill in your token.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
### Video Generation
|
|
68
|
+
|
|
69
|
+
| Command | Description |
|
|
70
|
+
|---------|-------------|
|
|
71
|
+
| `hailuo generate <prompt>` | Generate a video from text (minimax-t2v) |
|
|
72
|
+
| `hailuo image-to-video <prompt> --image-url <url>` | Generate video from image + text |
|
|
73
|
+
|
|
74
|
+
### Task Management
|
|
75
|
+
|
|
76
|
+
| Command | Description |
|
|
77
|
+
|---------|-------------|
|
|
78
|
+
| `hailuo task <task_id>` | Query a single task status |
|
|
79
|
+
| `hailuo tasks <id1> <id2> [...]` | Query multiple tasks at once |
|
|
80
|
+
| `hailuo wait <task_id>` | Wait for task completion with polling |
|
|
81
|
+
|
|
82
|
+
### Utilities
|
|
83
|
+
|
|
84
|
+
| Command | Description |
|
|
85
|
+
|---------|-------------|
|
|
86
|
+
| `hailuo models` | List available Hailuo models |
|
|
87
|
+
| `hailuo config` | Show current configuration |
|
|
88
|
+
|
|
89
|
+
## Global Options
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
--token TEXT API token (or set ACEDATACLOUD_API_TOKEN env var)
|
|
93
|
+
--version Show version
|
|
94
|
+
--help Show help message
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Most commands support:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
--json Output raw JSON (for piping/scripting)
|
|
101
|
+
--callback-url Webhook URL for async notifications
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Examples
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# Generate a video from a text prompt
|
|
108
|
+
hailuo generate "A cat playing in the snow"
|
|
109
|
+
|
|
110
|
+
# Generate from image reference
|
|
111
|
+
hailuo image-to-video "Animate the scene" --image-url https://example.com/photo.jpg
|
|
112
|
+
|
|
113
|
+
# Use a specific model
|
|
114
|
+
hailuo generate "Ocean waves" --model minimax-t2v
|
|
115
|
+
|
|
116
|
+
# Director model with image reference
|
|
117
|
+
hailuo image-to-video "Cinematic pan" --image-url img.jpg --model minimax-i2v-director
|
|
118
|
+
|
|
119
|
+
# Check task status
|
|
120
|
+
hailuo task abc123-def456
|
|
121
|
+
|
|
122
|
+
# Wait for completion
|
|
123
|
+
hailuo wait abc123 --timeout 300
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Docker
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
# Pull the image
|
|
130
|
+
docker pull ghcr.io/acedatacloud/hailuo-cli:latest
|
|
131
|
+
|
|
132
|
+
# Run a command
|
|
133
|
+
docker run --rm -e ACEDATACLOUD_API_TOKEN=your_token \
|
|
134
|
+
ghcr.io/acedatacloud/hailuo-cli generate "A happy scene"
|
|
135
|
+
|
|
136
|
+
# Or use docker-compose
|
|
137
|
+
docker compose run --rm hailuo-cli generate "A happy scene"
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Project Structure
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
HailuoCli/
|
|
144
|
+
├── hailuo_cli/ # Main package
|
|
145
|
+
│ ├── __init__.py
|
|
146
|
+
│ ├── __main__.py # python -m hailuo_cli entry point
|
|
147
|
+
│ ├── main.py # CLI entry point
|
|
148
|
+
│ ├── core/ # Core modules
|
|
149
|
+
│ │ ├── client.py # HTTP client for Hailuo API
|
|
150
|
+
│ │ ├── config.py # Configuration management
|
|
151
|
+
│ │ ├── exceptions.py # Custom exceptions
|
|
152
|
+
│ │ └── output.py # Rich terminal formatting
|
|
153
|
+
│ └── commands/ # CLI command groups
|
|
154
|
+
│ ├── video.py # Video generation commands
|
|
155
|
+
│ ├── task.py # Task management commands
|
|
156
|
+
│ └── info.py # Info and utility commands
|
|
157
|
+
└── tests/ # Test suite
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
hailuo_cli/__init__.py,sha256=tfyP7ia1C4E4QR6UU8MJz438_v1sJ6h37RDpreKf2Sc,61
|
|
2
|
+
hailuo_cli/__main__.py,sha256=iFzCKPwnD2OJCzyemVSV7CCrCHKZJTzExfJYRkhficU,85
|
|
3
|
+
hailuo_cli/main.py,sha256=PMdxllo8wSYwMEqSiA5rpePkOp40tFMWhheCTlDP11g,1732
|
|
4
|
+
hailuo_cli/commands/__init__.py,sha256=_1AM6pXttEBO3ZlftokfV_SBJ4ZqiCak2YCxaayKGNc,39
|
|
5
|
+
hailuo_cli/commands/info.py,sha256=PI6HW8MZJhl4Oh11bAHKwjpIjad4FyCK_slPyUgf3n4,773
|
|
6
|
+
hailuo_cli/commands/task.py,sha256=y8xqUZcKMcO64RMPBnX-D1RmBL0PdSq_zGnAKN_YI70,4311
|
|
7
|
+
hailuo_cli/commands/video.py,sha256=v15XiXT0i_hpwJdaPCFSLSQLt1pMcBimTfJ3_oUJSL4,3587
|
|
8
|
+
hailuo_cli/core/__init__.py,sha256=Ivx0Brt3XsJvJ-3Jj8Ix-CgwsxyMfui11PuBzJ2l8u8,35
|
|
9
|
+
hailuo_cli/core/client.py,sha256=6UPNsmk_kq_lrIkiJWMZf1B84ee2FEJo8qGbGRekk2c,3516
|
|
10
|
+
hailuo_cli/core/config.py,sha256=_9d-b8YC8cz6hkL0R3sDKESrxTvHqVm-VIOonCm-74E,1120
|
|
11
|
+
hailuo_cli/core/exceptions.py,sha256=uh_M8fP1h9Evntk3J9ec3__Xz9UmSsWdgf4mmyHDONE,961
|
|
12
|
+
hailuo_cli/core/output.py,sha256=Qs6-8hNdJ14aI85DayoUHkGhdfV1E-oHtewEdKtwRM8,3931
|
|
13
|
+
hailuo_cli-2026.7.31.0.dist-info/METADATA,sha256=LSUlSfH5obcJurDEuNIneNnGfhw3s2sJ04Q6nKziY2M,5020
|
|
14
|
+
hailuo_cli-2026.7.31.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
15
|
+
hailuo_cli-2026.7.31.0.dist-info/entry_points.txt,sha256=qLRpjcfXhQ_t-YmcQ--IVej3XYwEOoRnG0CWjf9NWJo,80
|
|
16
|
+
hailuo_cli-2026.7.31.0.dist-info/licenses/LICENSE,sha256=bxZ4efJS6INmQdLVfyxLQXtLCEaj-ORFNew2MaGiZ8E,1069
|
|
17
|
+
hailuo_cli-2026.7.31.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AceDataCloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|