wan-cli 2026.4.5.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.
- wan_cli/__init__.py +1 -0
- wan_cli/__main__.py +5 -0
- wan_cli/commands/__init__.py +1 -0
- wan_cli/commands/info.py +52 -0
- wan_cli/commands/task.py +142 -0
- wan_cli/commands/video.py +247 -0
- wan_cli/core/__init__.py +1 -0
- wan_cli/core/client.py +107 -0
- wan_cli/core/config.py +42 -0
- wan_cli/core/exceptions.py +37 -0
- wan_cli/core/output.py +147 -0
- wan_cli/main.py +70 -0
- wan_cli-2026.4.5.0.dist-info/METADATA +240 -0
- wan_cli-2026.4.5.0.dist-info/RECORD +17 -0
- wan_cli-2026.4.5.0.dist-info/WHEEL +4 -0
- wan_cli-2026.4.5.0.dist-info/entry_points.txt +3 -0
- wan_cli-2026.4.5.0.dist-info/licenses/LICENSE +21 -0
wan_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Wan CLI - AI Wan Video Generation via AceDataCloud API."""
|
wan_cli/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands for Wan CLI."""
|
wan_cli/commands/info.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Info and utility commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from wan_cli.core.config import settings
|
|
6
|
+
from wan_cli.core.output import RESOLUTIONS, console, print_models
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.command()
|
|
10
|
+
def models() -> None:
|
|
11
|
+
"""List available Wan models."""
|
|
12
|
+
print_models()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@click.command()
|
|
16
|
+
def resolutions() -> None:
|
|
17
|
+
"""List available resolutions."""
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
table = Table(title="Available Resolutions")
|
|
21
|
+
table.add_column("Resolution", style="bold cyan")
|
|
22
|
+
table.add_column("Description")
|
|
23
|
+
|
|
24
|
+
descriptions = {
|
|
25
|
+
"480P": "Standard definition",
|
|
26
|
+
"720P": "High definition",
|
|
27
|
+
"1080P": "Full high definition",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
for res in RESOLUTIONS:
|
|
31
|
+
table.add_row(res, descriptions.get(res, ""))
|
|
32
|
+
|
|
33
|
+
console.print(table)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.command()
|
|
37
|
+
def config() -> None:
|
|
38
|
+
"""Show current configuration."""
|
|
39
|
+
from rich.table import Table
|
|
40
|
+
|
|
41
|
+
table = Table(title="Wan CLI Configuration")
|
|
42
|
+
table.add_column("Setting", style="bold cyan")
|
|
43
|
+
table.add_column("Value")
|
|
44
|
+
|
|
45
|
+
table.add_row("API Base URL", settings.api_base_url)
|
|
46
|
+
table.add_row(
|
|
47
|
+
"API Token", f"{settings.api_token[:8]}..." if settings.api_token else "[red]Not set[/red]"
|
|
48
|
+
)
|
|
49
|
+
table.add_row("Default Model", settings.default_model)
|
|
50
|
+
table.add_row("Request Timeout", f"{settings.request_timeout}s")
|
|
51
|
+
|
|
52
|
+
console.print(table)
|
wan_cli/commands/task.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Task management commands."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from wan_cli.core.client import get_client
|
|
8
|
+
from wan_cli.core.exceptions import WanError
|
|
9
|
+
from wan_cli.core.output import print_error, print_json, print_success, print_task_result
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@click.command()
|
|
13
|
+
@click.argument("task_id")
|
|
14
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
15
|
+
@click.pass_context
|
|
16
|
+
def task(
|
|
17
|
+
ctx: click.Context,
|
|
18
|
+
task_id: str,
|
|
19
|
+
output_json: bool,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""Query a single task status.
|
|
22
|
+
|
|
23
|
+
TASK_ID is the task ID returned from generate commands.
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
|
|
27
|
+
wan task abc123-def456
|
|
28
|
+
"""
|
|
29
|
+
client = get_client(ctx.obj.get("token"))
|
|
30
|
+
try:
|
|
31
|
+
result = client.query_task(id=task_id, action="retrieve")
|
|
32
|
+
if output_json:
|
|
33
|
+
print_json(result)
|
|
34
|
+
else:
|
|
35
|
+
print_task_result(result)
|
|
36
|
+
except WanError as e:
|
|
37
|
+
print_error(e.message)
|
|
38
|
+
raise SystemExit(1) from e
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.command("tasks")
|
|
42
|
+
@click.argument("task_ids", nargs=-1, required=True)
|
|
43
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
44
|
+
@click.pass_context
|
|
45
|
+
def tasks_batch(
|
|
46
|
+
ctx: click.Context,
|
|
47
|
+
task_ids: tuple[str, ...],
|
|
48
|
+
output_json: bool,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""Query multiple tasks at once.
|
|
51
|
+
|
|
52
|
+
TASK_IDS are space-separated task IDs.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
|
|
56
|
+
wan tasks abc123 def456 ghi789
|
|
57
|
+
"""
|
|
58
|
+
client = get_client(ctx.obj.get("token"))
|
|
59
|
+
try:
|
|
60
|
+
result = client.query_task(ids=list(task_ids), action="retrieve_batch")
|
|
61
|
+
if output_json:
|
|
62
|
+
print_json(result)
|
|
63
|
+
else:
|
|
64
|
+
print_task_result(result)
|
|
65
|
+
except WanError as e:
|
|
66
|
+
print_error(e.message)
|
|
67
|
+
raise SystemExit(1) from e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@click.command()
|
|
71
|
+
@click.argument("task_id")
|
|
72
|
+
@click.option(
|
|
73
|
+
"--interval",
|
|
74
|
+
type=int,
|
|
75
|
+
default=5,
|
|
76
|
+
help="Polling interval in seconds (default: 5).",
|
|
77
|
+
)
|
|
78
|
+
@click.option(
|
|
79
|
+
"--timeout",
|
|
80
|
+
"max_timeout",
|
|
81
|
+
type=int,
|
|
82
|
+
default=600,
|
|
83
|
+
help="Maximum wait time in seconds (default: 600).",
|
|
84
|
+
)
|
|
85
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
86
|
+
@click.pass_context
|
|
87
|
+
def wait(
|
|
88
|
+
ctx: click.Context,
|
|
89
|
+
task_id: str,
|
|
90
|
+
interval: int,
|
|
91
|
+
max_timeout: int,
|
|
92
|
+
output_json: bool,
|
|
93
|
+
) -> None:
|
|
94
|
+
"""Wait for a task to complete, polling periodically.
|
|
95
|
+
|
|
96
|
+
TASK_ID is the task ID to monitor.
|
|
97
|
+
|
|
98
|
+
Examples:
|
|
99
|
+
|
|
100
|
+
wan wait abc123
|
|
101
|
+
|
|
102
|
+
wan wait abc123 --interval 10 --timeout 300
|
|
103
|
+
"""
|
|
104
|
+
client = get_client(ctx.obj.get("token"))
|
|
105
|
+
elapsed = 0
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
while elapsed < max_timeout:
|
|
109
|
+
result = client.query_task(id=task_id, action="retrieve")
|
|
110
|
+
data = result.get("data", {})
|
|
111
|
+
|
|
112
|
+
# Check completion - handle both list and dict responses
|
|
113
|
+
if isinstance(data, list) and data:
|
|
114
|
+
item = data[0]
|
|
115
|
+
elif isinstance(data, dict):
|
|
116
|
+
item = data
|
|
117
|
+
else:
|
|
118
|
+
item = {}
|
|
119
|
+
|
|
120
|
+
state = item.get("state", item.get("status", ""))
|
|
121
|
+
if state in ("succeeded", "completed", "complete", "failed", "error"):
|
|
122
|
+
if output_json:
|
|
123
|
+
print_json(result)
|
|
124
|
+
else:
|
|
125
|
+
if state in ("failed", "error"):
|
|
126
|
+
print_error(f"Task {task_id} failed.")
|
|
127
|
+
else:
|
|
128
|
+
print_success(f"Task {task_id} completed!")
|
|
129
|
+
print_task_result(result)
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
if not output_json:
|
|
133
|
+
click.echo(f"Status: {state or 'pending'} (waited {elapsed}s)...", err=True)
|
|
134
|
+
|
|
135
|
+
time.sleep(interval)
|
|
136
|
+
elapsed += interval
|
|
137
|
+
|
|
138
|
+
print_error(f"Timeout: task {task_id} did not complete within {max_timeout}s")
|
|
139
|
+
raise SystemExit(1)
|
|
140
|
+
except WanError as e:
|
|
141
|
+
print_error(e.message)
|
|
142
|
+
raise SystemExit(1) from e
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Video generation commands."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from wan_cli.core.client import get_client
|
|
6
|
+
from wan_cli.core.exceptions import WanError
|
|
7
|
+
from wan_cli.core.output import (
|
|
8
|
+
DEFAULT_MODEL,
|
|
9
|
+
DURATIONS,
|
|
10
|
+
RESOLUTIONS,
|
|
11
|
+
SHOT_TYPES,
|
|
12
|
+
WAN_MODELS,
|
|
13
|
+
print_error,
|
|
14
|
+
print_json,
|
|
15
|
+
print_video_result,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@click.command()
|
|
20
|
+
@click.argument("prompt")
|
|
21
|
+
@click.option(
|
|
22
|
+
"-m",
|
|
23
|
+
"--model",
|
|
24
|
+
type=click.Choice(WAN_MODELS),
|
|
25
|
+
default=DEFAULT_MODEL,
|
|
26
|
+
help="Wan model version.",
|
|
27
|
+
)
|
|
28
|
+
@click.option(
|
|
29
|
+
"-r",
|
|
30
|
+
"--resolution",
|
|
31
|
+
type=click.Choice(RESOLUTIONS),
|
|
32
|
+
default=None,
|
|
33
|
+
help="Output resolution (480P, 720P, 1080P).",
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--shot-type",
|
|
37
|
+
type=click.Choice(SHOT_TYPES),
|
|
38
|
+
default=None,
|
|
39
|
+
help="Shot type: single continuous shot or multi switching shots.",
|
|
40
|
+
)
|
|
41
|
+
@click.option(
|
|
42
|
+
"--duration",
|
|
43
|
+
type=click.Choice([str(d) for d in DURATIONS]),
|
|
44
|
+
default=None,
|
|
45
|
+
help="Duration in seconds (5, 10, 15).",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
"--negative-prompt",
|
|
49
|
+
default=None,
|
|
50
|
+
help="Reverse prompt words describing content to exclude from the video.",
|
|
51
|
+
)
|
|
52
|
+
@click.option(
|
|
53
|
+
"--size",
|
|
54
|
+
default=None,
|
|
55
|
+
help="The size of the generated video.",
|
|
56
|
+
)
|
|
57
|
+
@click.option(
|
|
58
|
+
"--audio/--no-audio",
|
|
59
|
+
default=None,
|
|
60
|
+
help="Whether the generated video has sound.",
|
|
61
|
+
)
|
|
62
|
+
@click.option(
|
|
63
|
+
"--prompt-extend/--no-prompt-extend",
|
|
64
|
+
default=None,
|
|
65
|
+
help="Enable prompt intelligent rewriting.",
|
|
66
|
+
)
|
|
67
|
+
@click.option(
|
|
68
|
+
"--audio-url",
|
|
69
|
+
default=None,
|
|
70
|
+
help="URL of an audio file to use in the generated video.",
|
|
71
|
+
)
|
|
72
|
+
@click.option("--callback-url", default=None, help="Webhook callback URL.")
|
|
73
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def generate(
|
|
76
|
+
ctx: click.Context,
|
|
77
|
+
prompt: str,
|
|
78
|
+
model: str,
|
|
79
|
+
resolution: str | None,
|
|
80
|
+
shot_type: str | None,
|
|
81
|
+
duration: str | None,
|
|
82
|
+
negative_prompt: str | None,
|
|
83
|
+
size: str | None,
|
|
84
|
+
audio: bool | None,
|
|
85
|
+
prompt_extend: bool | None,
|
|
86
|
+
audio_url: str | None,
|
|
87
|
+
callback_url: str | None,
|
|
88
|
+
output_json: bool,
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Generate a video from a text prompt.
|
|
91
|
+
|
|
92
|
+
PROMPT is a detailed description of what to generate.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
|
|
96
|
+
wan generate "Astronauts shuttle from space to volcano"
|
|
97
|
+
|
|
98
|
+
wan generate "A cat playing with yarn" -m wan2.6-t2v
|
|
99
|
+
"""
|
|
100
|
+
client = get_client(ctx.obj.get("token"))
|
|
101
|
+
try:
|
|
102
|
+
payload: dict[str, object] = {
|
|
103
|
+
"action": "text2video",
|
|
104
|
+
"prompt": prompt,
|
|
105
|
+
"model": model,
|
|
106
|
+
"resolution": resolution,
|
|
107
|
+
"shot_type": shot_type,
|
|
108
|
+
"duration": int(duration) if duration is not None else None,
|
|
109
|
+
"negative_prompt": negative_prompt,
|
|
110
|
+
"size": size,
|
|
111
|
+
"audio": audio,
|
|
112
|
+
"prompt_extend": prompt_extend,
|
|
113
|
+
"audio_url": audio_url,
|
|
114
|
+
"callback_url": callback_url,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
result = client.generate_video(**payload) # type: ignore[arg-type]
|
|
118
|
+
if output_json:
|
|
119
|
+
print_json(result)
|
|
120
|
+
else:
|
|
121
|
+
print_video_result(result)
|
|
122
|
+
except WanError as e:
|
|
123
|
+
print_error(e.message)
|
|
124
|
+
raise SystemExit(1) from e
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@click.command("image-to-video")
|
|
128
|
+
@click.argument("prompt")
|
|
129
|
+
@click.option(
|
|
130
|
+
"-i",
|
|
131
|
+
"--image-url",
|
|
132
|
+
required=True,
|
|
133
|
+
help="URL of the start image (first frame of the generated video).",
|
|
134
|
+
)
|
|
135
|
+
@click.option(
|
|
136
|
+
"-m",
|
|
137
|
+
"--model",
|
|
138
|
+
type=click.Choice(WAN_MODELS),
|
|
139
|
+
default="wan2.6-i2v",
|
|
140
|
+
help="Wan model version.",
|
|
141
|
+
)
|
|
142
|
+
@click.option(
|
|
143
|
+
"-r",
|
|
144
|
+
"--resolution",
|
|
145
|
+
type=click.Choice(RESOLUTIONS),
|
|
146
|
+
default=None,
|
|
147
|
+
help="Output resolution (480P, 720P, 1080P).",
|
|
148
|
+
)
|
|
149
|
+
@click.option(
|
|
150
|
+
"--shot-type",
|
|
151
|
+
type=click.Choice(SHOT_TYPES),
|
|
152
|
+
default=None,
|
|
153
|
+
help="Shot type: single continuous shot or multi switching shots.",
|
|
154
|
+
)
|
|
155
|
+
@click.option(
|
|
156
|
+
"--duration",
|
|
157
|
+
type=click.Choice([str(d) for d in DURATIONS]),
|
|
158
|
+
default=None,
|
|
159
|
+
help="Duration in seconds (5, 10, 15).",
|
|
160
|
+
)
|
|
161
|
+
@click.option(
|
|
162
|
+
"--negative-prompt",
|
|
163
|
+
default=None,
|
|
164
|
+
help="Reverse prompt words describing content to exclude from the video.",
|
|
165
|
+
)
|
|
166
|
+
@click.option(
|
|
167
|
+
"--size",
|
|
168
|
+
default=None,
|
|
169
|
+
help="The size of the generated video.",
|
|
170
|
+
)
|
|
171
|
+
@click.option(
|
|
172
|
+
"--audio/--no-audio",
|
|
173
|
+
default=None,
|
|
174
|
+
help="Whether the generated video has sound.",
|
|
175
|
+
)
|
|
176
|
+
@click.option(
|
|
177
|
+
"--prompt-extend/--no-prompt-extend",
|
|
178
|
+
default=None,
|
|
179
|
+
help="Enable prompt intelligent rewriting.",
|
|
180
|
+
)
|
|
181
|
+
@click.option(
|
|
182
|
+
"--audio-url",
|
|
183
|
+
default=None,
|
|
184
|
+
help="URL of an audio file to use in the generated video.",
|
|
185
|
+
)
|
|
186
|
+
@click.option(
|
|
187
|
+
"--reference-video-urls",
|
|
188
|
+
default=None,
|
|
189
|
+
help="Reference video file URL for character/timbre extraction.",
|
|
190
|
+
)
|
|
191
|
+
@click.option("--callback-url", default=None, help="Webhook callback URL.")
|
|
192
|
+
@click.option("--json", "output_json", is_flag=True, help="Output raw JSON.")
|
|
193
|
+
@click.pass_context
|
|
194
|
+
def image_to_video(
|
|
195
|
+
ctx: click.Context,
|
|
196
|
+
prompt: str,
|
|
197
|
+
image_url: str,
|
|
198
|
+
model: str,
|
|
199
|
+
resolution: str | None,
|
|
200
|
+
shot_type: str | None,
|
|
201
|
+
duration: str | None,
|
|
202
|
+
negative_prompt: str | None,
|
|
203
|
+
size: str | None,
|
|
204
|
+
audio: bool | None,
|
|
205
|
+
prompt_extend: bool | None,
|
|
206
|
+
audio_url: str | None,
|
|
207
|
+
reference_video_urls: str | None,
|
|
208
|
+
callback_url: str | None,
|
|
209
|
+
output_json: bool,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""Generate a video from a reference image.
|
|
212
|
+
|
|
213
|
+
PROMPT describes the desired video. Provide an image URL as the first frame.
|
|
214
|
+
|
|
215
|
+
Examples:
|
|
216
|
+
|
|
217
|
+
wan image-to-video "Animate this scene" -i https://example.com/photo.jpg
|
|
218
|
+
|
|
219
|
+
wan image-to-video "Bring to life" -i https://cdn.acedata.cloud/r9vsv9.png -m wan2.6-i2v
|
|
220
|
+
"""
|
|
221
|
+
client = get_client(ctx.obj.get("token"))
|
|
222
|
+
try:
|
|
223
|
+
payload: dict[str, object] = {
|
|
224
|
+
"action": "image2video",
|
|
225
|
+
"prompt": prompt,
|
|
226
|
+
"model": model,
|
|
227
|
+
"image_url": image_url,
|
|
228
|
+
"resolution": resolution,
|
|
229
|
+
"shot_type": shot_type,
|
|
230
|
+
"duration": int(duration) if duration is not None else None,
|
|
231
|
+
"negative_prompt": negative_prompt,
|
|
232
|
+
"size": size,
|
|
233
|
+
"audio": audio,
|
|
234
|
+
"prompt_extend": prompt_extend,
|
|
235
|
+
"audio_url": audio_url,
|
|
236
|
+
"reference_video_urls": reference_video_urls,
|
|
237
|
+
"callback_url": callback_url,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
result = client.generate_video(**payload) # type: ignore[arg-type]
|
|
241
|
+
if output_json:
|
|
242
|
+
print_json(result)
|
|
243
|
+
else:
|
|
244
|
+
print_video_result(result)
|
|
245
|
+
except WanError as e:
|
|
246
|
+
print_error(e.message)
|
|
247
|
+
raise SystemExit(1) from e
|
wan_cli/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core modules for Wan CLI."""
|
wan_cli/core/client.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""HTTP client for Wan API."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from wan_cli.core.config import settings
|
|
8
|
+
from wan_cli.core.exceptions import (
|
|
9
|
+
WanAPIError,
|
|
10
|
+
WanAuthError,
|
|
11
|
+
WanTimeoutError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WanClient:
|
|
16
|
+
"""HTTP client for AceDataCloud Wan 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 WanAuthError("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 Wan 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 WanAuthError("Invalid API token")
|
|
66
|
+
|
|
67
|
+
if response.status_code == 403:
|
|
68
|
+
raise WanAuthError("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 WanTimeoutError(
|
|
75
|
+
f"Request to {endpoint} timed out after {request_timeout}s"
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
except WanAuthError:
|
|
79
|
+
raise
|
|
80
|
+
|
|
81
|
+
except httpx.HTTPStatusError as e:
|
|
82
|
+
raise WanAPIError(
|
|
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, (WanAPIError, WanTimeoutError)):
|
|
90
|
+
raise
|
|
91
|
+
raise WanAPIError(message=str(e)) from e
|
|
92
|
+
|
|
93
|
+
# Convenience methods
|
|
94
|
+
def generate_video(self, **kwargs: Any) -> dict[str, Any]:
|
|
95
|
+
"""Generate video using the main endpoint."""
|
|
96
|
+
return self.request("/wan/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("/wan/tasks", kwargs)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def get_client(token: str | None = None) -> WanClient:
|
|
104
|
+
"""Get a WanClient instance, optionally overriding the token."""
|
|
105
|
+
if token:
|
|
106
|
+
return WanClient(api_token=token)
|
|
107
|
+
return WanClient()
|
wan_cli/core/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Configuration management for Wan 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("WAN_REQUEST_TIMEOUT", "1800"))
|
|
23
|
+
)
|
|
24
|
+
default_model: str = field(
|
|
25
|
+
default_factory=lambda: os.environ.get("WAN_DEFAULT_MODEL", "wan2.6-t2v")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_configured(self) -> bool:
|
|
30
|
+
"""Check if the API token is configured."""
|
|
31
|
+
return bool(self.api_token)
|
|
32
|
+
|
|
33
|
+
def validate(self) -> None:
|
|
34
|
+
"""Validate configuration. Raises ValueError if API token is missing."""
|
|
35
|
+
if not self.api_token:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
"API token not configured. "
|
|
38
|
+
"Set ACEDATACLOUD_API_TOKEN environment variable or use --token option."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
settings = Settings()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Custom exceptions for Wan CLI."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WanError(Exception):
|
|
5
|
+
"""Base exception for Wan 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 WanAuthError(WanError):
|
|
14
|
+
"""Authentication error."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
17
|
+
super().__init__(message, code="auth_error")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WanAPIError(WanError):
|
|
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 WanTimeoutError(WanError):
|
|
34
|
+
"""Request timeout error."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, message: str = "Request timed out"):
|
|
37
|
+
super().__init__(message, code="timeout_error")
|
wan_cli/core/output.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Rich terminal output formatting for Wan 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
|
+
WAN_MODELS = [
|
|
14
|
+
"wan2.6-t2v",
|
|
15
|
+
"wan2.6-i2v",
|
|
16
|
+
"wan2.6-i2v-flash",
|
|
17
|
+
"wan2.6-r2v",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
DEFAULT_MODEL = "wan2.6-t2v"
|
|
21
|
+
|
|
22
|
+
# Available resolutions
|
|
23
|
+
RESOLUTIONS = [
|
|
24
|
+
"480P",
|
|
25
|
+
"720P",
|
|
26
|
+
"1080P",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
# Available shot types
|
|
30
|
+
SHOT_TYPES = [
|
|
31
|
+
"single",
|
|
32
|
+
"multi",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Available durations (seconds)
|
|
36
|
+
DURATIONS = [5, 10, 15]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_json(data: Any) -> None:
|
|
40
|
+
"""Print data as formatted JSON."""
|
|
41
|
+
console.print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def print_error(message: str) -> None:
|
|
45
|
+
"""Print an error message."""
|
|
46
|
+
console.print(f"[bold red]Error:[/bold red] {message}")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def print_success(message: str) -> None:
|
|
50
|
+
"""Print a success message."""
|
|
51
|
+
console.print(f"[bold green]✓[/bold green] {message}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def print_video_result(data: dict[str, Any]) -> None:
|
|
55
|
+
"""Print video generation result in a rich format."""
|
|
56
|
+
task_id = data.get("task_id", "N/A")
|
|
57
|
+
trace_id = data.get("trace_id", "N/A")
|
|
58
|
+
items = data.get("data", [])
|
|
59
|
+
|
|
60
|
+
console.print(
|
|
61
|
+
Panel(
|
|
62
|
+
f"[bold]Task ID:[/bold] {task_id}\n[bold]Trace ID:[/bold] {trace_id}",
|
|
63
|
+
title="[bold green]Video Result[/bold green]",
|
|
64
|
+
border_style="green",
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if not items:
|
|
69
|
+
console.print("[yellow]No data available yet. Use 'task' to check status.[/yellow]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
if isinstance(items, list):
|
|
73
|
+
for i, item in enumerate(items, 1):
|
|
74
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
75
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
76
|
+
table.add_column("Value")
|
|
77
|
+
table.add_row("Video", f"#{i}")
|
|
78
|
+
if item.get("video_url"):
|
|
79
|
+
table.add_row("URL", item["video_url"])
|
|
80
|
+
if item.get("state"):
|
|
81
|
+
table.add_row("State", item["state"])
|
|
82
|
+
if item.get("model_name"):
|
|
83
|
+
table.add_row("Model", item["model_name"])
|
|
84
|
+
if item.get("created_at"):
|
|
85
|
+
table.add_row("Created", item["created_at"])
|
|
86
|
+
console.print(table)
|
|
87
|
+
console.print()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_task_result(data: dict[str, Any]) -> None:
|
|
91
|
+
"""Print task query result in a rich format."""
|
|
92
|
+
tasks = data.get("data", [])
|
|
93
|
+
|
|
94
|
+
if isinstance(tasks, list):
|
|
95
|
+
for task_data in tasks:
|
|
96
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
97
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
98
|
+
table.add_column("Value")
|
|
99
|
+
|
|
100
|
+
for key in ["id", "status", "state", "video_url", "model_name", "created_at"]:
|
|
101
|
+
if task_data.get(key):
|
|
102
|
+
table.add_row(key.replace("_", " ").title(), str(task_data[key]))
|
|
103
|
+
|
|
104
|
+
console.print(table)
|
|
105
|
+
console.print()
|
|
106
|
+
elif isinstance(tasks, dict):
|
|
107
|
+
table = Table(show_header=False, box=None, padding=(0, 2))
|
|
108
|
+
table.add_column("Field", style="bold cyan", width=15)
|
|
109
|
+
table.add_column("Value")
|
|
110
|
+
|
|
111
|
+
for key in ["id", "status", "state", "video_url", "model_name", "created_at"]:
|
|
112
|
+
if tasks.get(key):
|
|
113
|
+
table.add_row(key.replace("_", " ").title(), str(tasks[key]))
|
|
114
|
+
|
|
115
|
+
console.print(table)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def print_models() -> None:
|
|
119
|
+
"""Print available Wan models."""
|
|
120
|
+
table = Table(title="Available Wan Models")
|
|
121
|
+
table.add_column("Model", style="bold cyan")
|
|
122
|
+
table.add_column("Type", style="bold")
|
|
123
|
+
table.add_column("Notes")
|
|
124
|
+
|
|
125
|
+
table.add_row(
|
|
126
|
+
"wan2.6-t2v",
|
|
127
|
+
"Text-to-Video",
|
|
128
|
+
"Text-to-video generation (default)",
|
|
129
|
+
)
|
|
130
|
+
table.add_row(
|
|
131
|
+
"wan2.6-i2v",
|
|
132
|
+
"Image-to-Video",
|
|
133
|
+
"Image-to-video generation",
|
|
134
|
+
)
|
|
135
|
+
table.add_row(
|
|
136
|
+
"wan2.6-i2v-flash",
|
|
137
|
+
"Image-to-Video Flash",
|
|
138
|
+
"Fast image-to-video generation",
|
|
139
|
+
)
|
|
140
|
+
table.add_row(
|
|
141
|
+
"wan2.6-r2v",
|
|
142
|
+
"Reference-to-Video",
|
|
143
|
+
"Reference video-to-video generation",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
console.print(table)
|
|
147
|
+
console.print(f"\n[dim]Default model: {DEFAULT_MODEL}[/dim]")
|
wan_cli/main.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wan CLI - AI Wan Video Generation via AceDataCloud API.
|
|
4
|
+
|
|
5
|
+
A command-line tool for generating AI videos using Tongyi Wansiang
|
|
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 wan_cli.commands.info import config, models, resolutions
|
|
15
|
+
from wan_cli.commands.task import task, tasks_batch, wait
|
|
16
|
+
from wan_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("wan-cli")
|
|
25
|
+
except metadata.PackageNotFoundError:
|
|
26
|
+
return "dev"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.group()
|
|
30
|
+
@click.version_option(version=get_version(), prog_name="wan-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
|
+
"""Wan CLI - AI Video Generation powered by AceDataCloud.
|
|
39
|
+
|
|
40
|
+
Generate AI videos from the command line using Tongyi Wansiang.
|
|
41
|
+
|
|
42
|
+
Get your API token at https://platform.acedata.cloud
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Examples:
|
|
46
|
+
wan generate "Astronauts shuttle from space to volcano"
|
|
47
|
+
wan image-to-video "Animate this scene" -i https://example.com/photo.jpg
|
|
48
|
+
wan task abc123-def456
|
|
49
|
+
wan 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
|
+
cli.add_command(resolutions)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
cli()
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: wan-cli
|
|
3
|
+
Version: 2026.4.5.0
|
|
4
|
+
Summary: CLI tool for Tongyi Wansiang AI Video Generation via AceDataCloud API
|
|
5
|
+
Project-URL: Homepage, https://github.com/AceDataCloud/WanCli
|
|
6
|
+
Project-URL: Repository, https://github.com/AceDataCloud/WanCli
|
|
7
|
+
Project-URL: Issues, https://github.com/AceDataCloud/WanCli/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/AceDataCloud/WanCli/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,video,wan,wansiang
|
|
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: wan-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
|
+
# Wan CLI
|
|
48
|
+
|
|
49
|
+
[](https://pypi.org/project/wan-cli/)
|
|
50
|
+
[](https://pypi.org/project/wan-cli/)
|
|
51
|
+
[](https://www.python.org/downloads/)
|
|
52
|
+
[](https://opensource.org/licenses/MIT)
|
|
53
|
+
|
|
54
|
+
A command-line tool for AI video generation using [Tongyi Wansiang](https://platform.acedata.cloud/) through the [AceDataCloud API](https://platform.acedata.cloud/).
|
|
55
|
+
|
|
56
|
+
Generate AI videos directly from your terminal — no MCP client required.
|
|
57
|
+
|
|
58
|
+
## Features
|
|
59
|
+
|
|
60
|
+
- **Video Generation** — Generate videos from text prompts with multiple models
|
|
61
|
+
- **Image-to-Video** — Create videos from reference images
|
|
62
|
+
- **Multiple Models** — wan2.6-t2v, wan2.6-i2v, wan2.6-i2v-flash, wan2.6-r2v
|
|
63
|
+
- **Task Management** — Query tasks, batch query, wait with polling
|
|
64
|
+
- **Rich Output** — Beautiful terminal tables and panels via Rich
|
|
65
|
+
- **JSON Mode** — Machine-readable output with `--json` for piping
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
### 1. Get API Token
|
|
70
|
+
|
|
71
|
+
Get your API token from [AceDataCloud Platform](https://platform.acedata.cloud/):
|
|
72
|
+
|
|
73
|
+
1. Sign up or log in
|
|
74
|
+
2. Navigate to the Wan API page
|
|
75
|
+
3. Click "Acquire" to get your token
|
|
76
|
+
|
|
77
|
+
### 2. Install
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Install with pip
|
|
81
|
+
pip install wan-cli
|
|
82
|
+
|
|
83
|
+
# Or with uv (recommended)
|
|
84
|
+
uv pip install wan-cli
|
|
85
|
+
|
|
86
|
+
# Or from source
|
|
87
|
+
git clone https://github.com/AceDataCloud/WanCli.git
|
|
88
|
+
cd WanCli
|
|
89
|
+
pip install -e .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Configure
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# Set your API token
|
|
96
|
+
export ACEDATACLOUD_API_TOKEN=your_token_here
|
|
97
|
+
|
|
98
|
+
# Or use .env file
|
|
99
|
+
cp .env.example .env
|
|
100
|
+
# Edit .env with your token
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 4. Use
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Generate a video from text
|
|
107
|
+
wan generate "Astronauts shuttle from space to volcano"
|
|
108
|
+
|
|
109
|
+
# Generate from reference image
|
|
110
|
+
wan image-to-video "Animate this scene" -i https://example.com/photo.jpg
|
|
111
|
+
|
|
112
|
+
# Check task status
|
|
113
|
+
wan task <task-id>
|
|
114
|
+
|
|
115
|
+
# Wait for completion
|
|
116
|
+
wan wait <task-id> --interval 5
|
|
117
|
+
|
|
118
|
+
# List available models
|
|
119
|
+
wan models
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Commands
|
|
123
|
+
|
|
124
|
+
| Command | Description |
|
|
125
|
+
|---------|-------------|
|
|
126
|
+
| `wan generate <prompt>` | Generate a video from a text prompt |
|
|
127
|
+
| `wan image-to-video <prompt> -i <url>` | Generate a video from a reference image |
|
|
128
|
+
| `wan task <task_id>` | Query a single task status |
|
|
129
|
+
| `wan tasks <id1> <id2>...` | Query multiple tasks at once |
|
|
130
|
+
| `wan wait <task_id>` | Wait for task completion with polling |
|
|
131
|
+
| `wan models` | List available Wan models |
|
|
132
|
+
| `wan resolutions` | List available resolutions |
|
|
133
|
+
| `wan config` | Show current configuration |
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
## Global Options
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
--token TEXT API token (or set ACEDATACLOUD_API_TOKEN env var)
|
|
140
|
+
--version Show version
|
|
141
|
+
--help Show help message
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Most commands support:
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
--json Output raw JSON (for piping/scripting)
|
|
148
|
+
--model TEXT Wan model version (default: wan2.6-t2v)
|
|
149
|
+
--resolution TEXT Output resolution: 480P, 720P, 1080P
|
|
150
|
+
--duration TEXT Duration in seconds: 5, 10, 15
|
|
151
|
+
--shot-type TEXT Shot type: single, multi
|
|
152
|
+
--audio/--no-audio Whether the video has sound
|
|
153
|
+
--prompt-extend/--no-prompt-extend Enable prompt intelligent rewriting
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Available Models
|
|
157
|
+
|
|
158
|
+
| Model | Type | Notes |
|
|
159
|
+
|-------|------|-------|
|
|
160
|
+
| `wan2.6-t2v` | Text-to-Video | Text-to-video generation (default) |
|
|
161
|
+
| `wan2.6-i2v` | Image-to-Video | Image-to-video generation |
|
|
162
|
+
| `wan2.6-i2v-flash` | Image-to-Video Flash | Fast image-to-video generation |
|
|
163
|
+
| `wan2.6-r2v` | Reference-to-Video | Reference video-to-video generation |
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
## Configuration
|
|
167
|
+
|
|
168
|
+
### Environment Variables
|
|
169
|
+
|
|
170
|
+
| Variable | Description | Default |
|
|
171
|
+
|----------|-------------|---------|
|
|
172
|
+
| `ACEDATACLOUD_API_TOKEN` | API token from AceDataCloud | *Required* |
|
|
173
|
+
| `ACEDATACLOUD_API_BASE_URL` | API base URL | `https://api.acedata.cloud` |
|
|
174
|
+
| `WAN_DEFAULT_MODEL` | Default model | `wan2.6-t2v` |
|
|
175
|
+
| `WAN_REQUEST_TIMEOUT` | Timeout in seconds | `1800` |
|
|
176
|
+
|
|
177
|
+
## Development
|
|
178
|
+
|
|
179
|
+
### Setup Development Environment
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
git clone https://github.com/AceDataCloud/WanCli.git
|
|
183
|
+
cd WanCli
|
|
184
|
+
python -m venv .venv
|
|
185
|
+
source .venv/bin/activate
|
|
186
|
+
pip install -e ".[dev,test]"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Run Tests
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
pytest
|
|
193
|
+
pytest --cov=wan_cli
|
|
194
|
+
pytest tests/test_integration.py -m integration
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Code Quality
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
ruff format .
|
|
201
|
+
ruff check .
|
|
202
|
+
mypy wan_cli
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Docker
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
docker pull ghcr.io/acedatacloud/wan-cli:latest
|
|
209
|
+
docker run --rm -e ACEDATACLOUD_API_TOKEN=your_token \
|
|
210
|
+
ghcr.io/acedatacloud/wan-cli generate "Astronauts shuttle from space to volcano"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Project Structure
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
WanCli/
|
|
217
|
+
├── wan_cli/ # Main package
|
|
218
|
+
│ ├── __init__.py
|
|
219
|
+
│ ├── __main__.py # python -m wan_cli entry point
|
|
220
|
+
│ ├── main.py # CLI entry point
|
|
221
|
+
│ ├── core/ # Core modules
|
|
222
|
+
│ │ ├── client.py # HTTP client for Wan API
|
|
223
|
+
│ │ ├── config.py # Configuration management
|
|
224
|
+
│ │ ├── exceptions.py # Custom exceptions
|
|
225
|
+
│ │ └── output.py # Rich terminal formatting
|
|
226
|
+
│ └── commands/ # CLI command groups
|
|
227
|
+
│ ├── video.py # Video generation commands
|
|
228
|
+
│ ├── task.py # Task management commands
|
|
229
|
+
│ └── info.py # Info & utility commands
|
|
230
|
+
├── tests/ # Test suite
|
|
231
|
+
├── Dockerfile # Container image
|
|
232
|
+
├── deploy/ # Kubernetes deployment configs
|
|
233
|
+
├── .env.example # Environment template
|
|
234
|
+
├── pyproject.toml # Project configuration
|
|
235
|
+
└── README.md
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## License
|
|
239
|
+
|
|
240
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
wan_cli/__init__.py,sha256=nHLkZapgmJKNo-QvREDqooX3crOEkOpnMcb5mwQQzag,62
|
|
2
|
+
wan_cli/__main__.py,sha256=NyDlpzzSVkOk1qeIBJNZ3CfvbSep1EHkoDze4-VN_Qk,79
|
|
3
|
+
wan_cli/main.py,sha256=opT-V4PX5gGHrpVgvhmBGzJ4FxLHH1LaammowOkwLWg,1750
|
|
4
|
+
wan_cli/commands/__init__.py,sha256=Ek6-G6X1l1Kielt8A9AGNn-97cA38Pe_JYFOh5oHnJM,32
|
|
5
|
+
wan_cli/commands/info.py,sha256=VmfiTWNAi1jm6SxbNvTNicPKZhqfBtiz6G3Bx1OSpMg,1342
|
|
6
|
+
wan_cli/commands/task.py,sha256=15S2g_mEDbrI06jXKQQ3XnJ9I18VRcGKqcYUCOlLnj0,3822
|
|
7
|
+
wan_cli/commands/video.py,sha256=TtmjanhX9ktFms-cpeTUC79-ZAJfBN7ruI82S5V01V4,6549
|
|
8
|
+
wan_cli/core/__init__.py,sha256=4cMiAtLO3dwtXDz84rIuf_cvHumiOGzP_A4erTeOZy8,32
|
|
9
|
+
wan_cli/core/client.py,sha256=d36EURx2PxIG67sHA-FGLCus0kUT3L2GSCjKeiz1kus,3443
|
|
10
|
+
wan_cli/core/config.py,sha256=v7fuzBFsYpQm5lKlOnwK4KgAkyf2gpB3tSC7Cu_IZcQ,1234
|
|
11
|
+
wan_cli/core/exceptions.py,sha256=4kJ7Vyy4o9AYOM2_CF_OYTnAqm45Ipdcia1WQxxrbW0,934
|
|
12
|
+
wan_cli/core/output.py,sha256=BvOlB-D67ckfDpCQHh7oRfx4Ap29CEaiUliMPLCGzEo,4185
|
|
13
|
+
wan_cli-2026.4.5.0.dist-info/METADATA,sha256=t6n1LBjvNNce5bRSFx5loDUdAMunr1NyAEp4gLqMxD0,7747
|
|
14
|
+
wan_cli-2026.4.5.0.dist-info/WHEEL,sha256=TJPnKdtrSue7xZ_AVGkp9YXcvDrobsjBds1du3Nx6dc,87
|
|
15
|
+
wan_cli-2026.4.5.0.dist-info/entry_points.txt,sha256=1UwUc_zV-y_LbAbSGvVwCQUk0VbucYvk1QlhJGxYXjY,68
|
|
16
|
+
wan_cli-2026.4.5.0.dist-info/licenses/LICENSE,sha256=bxZ4efJS6INmQdLVfyxLQXtLCEaj-ORFNew2MaGiZ8E,1069
|
|
17
|
+
wan_cli-2026.4.5.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.
|