muapi-cli 0.2.5__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.
- muapi/__init__.py +1 -0
- muapi/client.py +121 -0
- muapi/commands/__init__.py +0 -0
- muapi/commands/account.py +89 -0
- muapi/commands/audio.py +139 -0
- muapi/commands/auth.py +193 -0
- muapi/commands/config_cmd.py +80 -0
- muapi/commands/docs.py +81 -0
- muapi/commands/edit.py +134 -0
- muapi/commands/enhance.py +157 -0
- muapi/commands/image.py +297 -0
- muapi/commands/keys.py +115 -0
- muapi/commands/mcp_server.py +905 -0
- muapi/commands/models.py +79 -0
- muapi/commands/predict.py +43 -0
- muapi/commands/run.py +173 -0
- muapi/commands/upload.py +31 -0
- muapi/commands/video.py +318 -0
- muapi/commands/workflow.py +746 -0
- muapi/config.py +110 -0
- muapi/dynamic_help.py +144 -0
- muapi/exitcodes.py +14 -0
- muapi/main.py +98 -0
- muapi/schema_introspect.py +175 -0
- muapi/utils.py +202 -0
- muapi_cli-0.2.5.dist-info/METADATA +337 -0
- muapi_cli-0.2.5.dist-info/RECORD +29 -0
- muapi_cli-0.2.5.dist-info/WHEEL +4 -0
- muapi_cli-0.2.5.dist-info/entry_points.txt +2 -0
muapi/commands/models.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""muapi models — global model discovery across all media types."""
|
|
2
|
+
import typer
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
|
|
5
|
+
from ..utils import out
|
|
6
|
+
from .image import T2I_MODELS, I2I_MODELS
|
|
7
|
+
from .video import T2V_MODELS, I2V_MODELS
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Discover available models across all categories.")
|
|
10
|
+
|
|
11
|
+
_ALL_MODELS = {
|
|
12
|
+
"image:text-to-image": T2I_MODELS,
|
|
13
|
+
"image:image-to-image": I2I_MODELS,
|
|
14
|
+
"video:text-to-video": T2V_MODELS,
|
|
15
|
+
"video:image-to-video": I2V_MODELS,
|
|
16
|
+
"audio": {
|
|
17
|
+
"suno": "suno-create-music",
|
|
18
|
+
"mmaudio": "mmaudio-v2/text-to-audio",
|
|
19
|
+
},
|
|
20
|
+
"enhance": {
|
|
21
|
+
"upscale": "ai-image-upscale",
|
|
22
|
+
"bg-remove": "ai-background-remover",
|
|
23
|
+
"face-swap": "ai-image-face-swap / ai-video-face-swap",
|
|
24
|
+
"skin": "ai-skin-enhancer",
|
|
25
|
+
"colorize": "ai-color-photo",
|
|
26
|
+
"ghibli": "ai-ghibli-style",
|
|
27
|
+
"anime": "ai-anime-generator",
|
|
28
|
+
"extend": "ai-image-extension",
|
|
29
|
+
"product-shot": "ai-product-shot",
|
|
30
|
+
"erase": "ai-object-eraser",
|
|
31
|
+
},
|
|
32
|
+
"edit": {
|
|
33
|
+
"effects": "video-effects / image-effects / wan-effects",
|
|
34
|
+
"lipsync": "sync / latentsync / creatify / veed",
|
|
35
|
+
"dance": "dance",
|
|
36
|
+
"dress": "ai-dress-change",
|
|
37
|
+
"clipping": "ai-clipping",
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@app.command("list")
|
|
43
|
+
def list_models(
|
|
44
|
+
category: str = typer.Option("all", "--category", "-c",
|
|
45
|
+
help="Filter by category: image, video, audio, enhance, edit, all"),
|
|
46
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
47
|
+
):
|
|
48
|
+
"""List all available models and endpoints.
|
|
49
|
+
|
|
50
|
+
\b
|
|
51
|
+
Examples:
|
|
52
|
+
muapi models list
|
|
53
|
+
muapi models list --category video
|
|
54
|
+
muapi models list --output-json | jq 'keys'
|
|
55
|
+
"""
|
|
56
|
+
if output_json:
|
|
57
|
+
import json
|
|
58
|
+
# Flatten to list of {name, category, endpoint}
|
|
59
|
+
rows = []
|
|
60
|
+
for cat, models in _ALL_MODELS.items():
|
|
61
|
+
if category != "all" and not cat.startswith(category):
|
|
62
|
+
continue
|
|
63
|
+
for name, ep in models.items():
|
|
64
|
+
rows.append({"name": name, "category": cat, "endpoint": ep})
|
|
65
|
+
out.print_json(json.dumps(rows))
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
t = Table(title="muapi Models", show_header=True, header_style="bold magenta")
|
|
69
|
+
t.add_column("Category", style="dim")
|
|
70
|
+
t.add_column("Name", style="cyan")
|
|
71
|
+
t.add_column("Endpoint")
|
|
72
|
+
|
|
73
|
+
for cat, models in _ALL_MODELS.items():
|
|
74
|
+
if category != "all" and not cat.startswith(category):
|
|
75
|
+
continue
|
|
76
|
+
for name, ep in models.items():
|
|
77
|
+
t.add_row(cat, name, ep)
|
|
78
|
+
|
|
79
|
+
out.print(t)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""muapi predict — check and wait for async prediction results."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .. import client, exitcodes
|
|
7
|
+
from ..utils import console, download_outputs, error_exit, print_result, spinner_status
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Check or wait for async prediction results.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("result")
|
|
13
|
+
def result(
|
|
14
|
+
request_id: str = typer.Argument(..., help="Prediction request ID"),
|
|
15
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
16
|
+
jq: Optional[str] = typer.Option(None, "--jq", help="jq-style filter on output (e.g. '.outputs[0]')"),
|
|
17
|
+
):
|
|
18
|
+
"""Fetch the current result of a prediction (no polling)."""
|
|
19
|
+
try:
|
|
20
|
+
data = client.get_result(request_id)
|
|
21
|
+
except client.MuapiError as e:
|
|
22
|
+
error_exit(str(e), e.exit_code)
|
|
23
|
+
print_result(data, output_json, label=f"Prediction {request_id}", jq=jq)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@app.command("wait")
|
|
27
|
+
def wait(
|
|
28
|
+
request_id: str = typer.Argument(..., help="Prediction request ID"),
|
|
29
|
+
timeout: int = typer.Option(600, "--timeout", "-T", help="Max wait time in seconds"),
|
|
30
|
+
download: Optional[str] = typer.Option(None, "--download", "-d"),
|
|
31
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
32
|
+
jq: Optional[str] = typer.Option(None, "--jq"),
|
|
33
|
+
):
|
|
34
|
+
"""Wait for a prediction to complete, then show the result."""
|
|
35
|
+
try:
|
|
36
|
+
with spinner_status(f"Waiting for prediction {request_id}..."):
|
|
37
|
+
data = client.wait_for_result(request_id, max_seconds=timeout)
|
|
38
|
+
except client.MuapiError as e:
|
|
39
|
+
error_exit(str(e), e.exit_code)
|
|
40
|
+
|
|
41
|
+
print_result(data, output_json, label=f"Prediction {request_id}", jq=jq)
|
|
42
|
+
if download and data.get("status") == "completed":
|
|
43
|
+
download_outputs(data, download)
|
muapi/commands/run.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""muapi run — generic, schema-driven runner for any muapi.ai model.
|
|
2
|
+
|
|
3
|
+
The curated `muapi image / video / audio / …` verbs each wrap one
|
|
4
|
+
endpoint with hand-picked flags. `run` is the escape hatch (and now the
|
|
5
|
+
default path) — pass any model/endpoint name plus `-i key=value` inputs
|
|
6
|
+
and it will POST whatever you give it.
|
|
7
|
+
|
|
8
|
+
Dynamic per-model help is handled before Typer parses (see main.py).
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
|
|
19
|
+
from .. import client, exitcodes, schema_introspect
|
|
20
|
+
from ..utils import (
|
|
21
|
+
download_outputs, error_exit, print_result, print_dry_run,
|
|
22
|
+
read_stdin_if_dash, spinner_status,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# `run` is registered as a single top-level command on the root Typer app
|
|
26
|
+
# (see muapi/main.py). It is *not* a subcommand group — the positional
|
|
27
|
+
# `model` argument needs to take the first slot after `muapi run`.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Alias resolution ─────────────────────────────────────────────────────────
|
|
31
|
+
# Short, human names → real endpoint slugs. Populated lazily from the curated
|
|
32
|
+
# tables in the existing verb modules so we keep one source of truth.
|
|
33
|
+
|
|
34
|
+
def _build_alias_map() -> dict[str, str]:
|
|
35
|
+
aliases: dict[str, str] = {}
|
|
36
|
+
try:
|
|
37
|
+
from .image import T2I_MODELS, I2I_MODELS
|
|
38
|
+
# T2I wins over I2I when names collide — text-to-image is the more
|
|
39
|
+
# common request for a bare alias.
|
|
40
|
+
for k, v in I2I_MODELS.items():
|
|
41
|
+
aliases.setdefault(k, v)
|
|
42
|
+
for k, v in T2I_MODELS.items():
|
|
43
|
+
aliases[k] = v
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
try:
|
|
47
|
+
from .video import T2V_MODELS, I2V_MODELS
|
|
48
|
+
for k, v in I2V_MODELS.items():
|
|
49
|
+
aliases.setdefault(f"video:{k}", v)
|
|
50
|
+
for k, v in T2V_MODELS.items():
|
|
51
|
+
aliases.setdefault(f"video:{k}", v)
|
|
52
|
+
aliases.setdefault(k, v)
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
return aliases
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def resolve_model(model: str) -> str:
|
|
59
|
+
"""Resolve a model arg to an endpoint slug.
|
|
60
|
+
|
|
61
|
+
If `model` looks like an endpoint slug (contains '-' or '/' or matches
|
|
62
|
+
a known path), use it verbatim. Otherwise try the curated alias map.
|
|
63
|
+
Unknown names are returned as-is so the server can give the real error.
|
|
64
|
+
"""
|
|
65
|
+
if "/" in model: # already a full path
|
|
66
|
+
return model.lstrip("/")
|
|
67
|
+
aliases = _build_alias_map()
|
|
68
|
+
if model in aliases:
|
|
69
|
+
return aliases[model]
|
|
70
|
+
return model
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ── Input parsing ────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
def _parse_kv(pair: str) -> tuple[str, object]:
|
|
76
|
+
"""Parse a `-i key=value` pair.
|
|
77
|
+
|
|
78
|
+
Value is tried as JSON first (so `count=3`, `flag=true`, `arr=[1,2]`
|
|
79
|
+
work) and falls back to a raw string.
|
|
80
|
+
"""
|
|
81
|
+
if "=" not in pair:
|
|
82
|
+
raise typer.BadParameter(f"-i expects key=value, got: {pair!r}")
|
|
83
|
+
key, raw = pair.split("=", 1)
|
|
84
|
+
key = key.strip()
|
|
85
|
+
if not key:
|
|
86
|
+
raise typer.BadParameter(f"-i has empty key: {pair!r}")
|
|
87
|
+
try:
|
|
88
|
+
value = json.loads(raw)
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
value = raw
|
|
91
|
+
return key, value
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _load_input_file(path: str) -> dict:
|
|
95
|
+
p = Path(path)
|
|
96
|
+
if not p.exists():
|
|
97
|
+
raise typer.BadParameter(f"--input-file not found: {path}")
|
|
98
|
+
try:
|
|
99
|
+
data = json.loads(p.read_text())
|
|
100
|
+
except json.JSONDecodeError as e:
|
|
101
|
+
raise typer.BadParameter(f"--input-file is not valid JSON: {e}")
|
|
102
|
+
if not isinstance(data, dict):
|
|
103
|
+
raise typer.BadParameter("--input-file must contain a JSON object")
|
|
104
|
+
return data
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ── The command ──────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
def run(
|
|
110
|
+
model: str = typer.Argument(
|
|
111
|
+
...,
|
|
112
|
+
help="Model endpoint slug (e.g. 'flux-dev-image', 'nano-banana-2', 'seedance-2-text-to-video') or a curated alias.",
|
|
113
|
+
),
|
|
114
|
+
prompt: Optional[str] = typer.Option(
|
|
115
|
+
None, "-p", "--prompt",
|
|
116
|
+
help="Prompt text. Pass '-' to read from stdin. Sets the 'prompt' field.",
|
|
117
|
+
),
|
|
118
|
+
inputs: list[str] = typer.Option(
|
|
119
|
+
[], "-i", "--input",
|
|
120
|
+
help="Inputs as key=value (repeatable). JSON values are parsed (e.g. -i num_images=2 -i tags='[\"a\",\"b\"]').",
|
|
121
|
+
),
|
|
122
|
+
input_file: Optional[str] = typer.Option(
|
|
123
|
+
None, "--input-file",
|
|
124
|
+
help="Path to a JSON file with inputs (merged before -i flags).",
|
|
125
|
+
),
|
|
126
|
+
wait: bool = typer.Option(True, "--wait/--no-wait", help="Poll until done (default: --wait)."),
|
|
127
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show the request that would be sent and exit."),
|
|
128
|
+
download: Optional[str] = typer.Option(None, "--download", "-d", help="Download outputs to directory."),
|
|
129
|
+
output_json: bool = typer.Option(False, "--output-json", "-j", help="Print raw JSON to stdout."),
|
|
130
|
+
jq: Optional[str] = typer.Option(None, "--jq", help="jq-style filter on JSON output (e.g. '.outputs[0]')."),
|
|
131
|
+
):
|
|
132
|
+
"""Run any muapi.ai model with arbitrary inputs.
|
|
133
|
+
|
|
134
|
+
\b
|
|
135
|
+
Examples:
|
|
136
|
+
muapi run flux-dev-image -p "a cyberpunk skyline"
|
|
137
|
+
muapi run nano-banana-2 -p "logo" -i num_images=2 --download ./out
|
|
138
|
+
muapi run seedance-2-text-to-video -p "drone shot" -i duration=5 --output-json
|
|
139
|
+
muapi run flux-kontext-pro-i2i -p "make it night" -i image_url=https://...
|
|
140
|
+
|
|
141
|
+
\b
|
|
142
|
+
Discover a model's inputs:
|
|
143
|
+
muapi run <model> -h # introspects the live OpenAPI schema
|
|
144
|
+
|
|
145
|
+
\b
|
|
146
|
+
Merge order for inputs (later wins):
|
|
147
|
+
--input-file < -i key=value < -p prompt
|
|
148
|
+
"""
|
|
149
|
+
endpoint = resolve_model(model)
|
|
150
|
+
|
|
151
|
+
# Build payload: file < -i flags < --prompt
|
|
152
|
+
payload: dict = {}
|
|
153
|
+
if input_file:
|
|
154
|
+
payload.update(_load_input_file(input_file))
|
|
155
|
+
for pair in inputs:
|
|
156
|
+
k, v = _parse_kv(pair)
|
|
157
|
+
payload[k] = v
|
|
158
|
+
if prompt is not None:
|
|
159
|
+
payload["prompt"] = read_stdin_if_dash(prompt)
|
|
160
|
+
|
|
161
|
+
if dry_run:
|
|
162
|
+
print_dry_run(endpoint, payload)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
with spinner_status(f"Running {endpoint}..."):
|
|
167
|
+
result = client.generate(endpoint, payload, wait=wait)
|
|
168
|
+
except client.MuapiError as e:
|
|
169
|
+
error_exit(str(e), e.exit_code)
|
|
170
|
+
|
|
171
|
+
print_result(result, output_json, label=f"Run ({endpoint})", jq=jq)
|
|
172
|
+
if download and result.get("status") == "completed":
|
|
173
|
+
download_outputs(result, download)
|
muapi/commands/upload.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""muapi upload — upload local files to get a hosted URL."""
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .. import client
|
|
7
|
+
from ..utils import error_exit, print_result, console
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Upload local files to get a hosted URL for use in generation.")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command("file")
|
|
13
|
+
def upload_file(
|
|
14
|
+
file_path: str = typer.Argument(..., help="Local file path to upload"),
|
|
15
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
16
|
+
):
|
|
17
|
+
"""Upload a local file and get back a hosted URL."""
|
|
18
|
+
path = Path(file_path)
|
|
19
|
+
if not path.exists():
|
|
20
|
+
error_exit(f"File not found: {file_path}")
|
|
21
|
+
try:
|
|
22
|
+
console.print(f"Uploading [cyan]{file_path}[/cyan]...")
|
|
23
|
+
result = client.upload_file(str(path))
|
|
24
|
+
except client.MuapiError as e:
|
|
25
|
+
error_exit(str(e))
|
|
26
|
+
|
|
27
|
+
print_result(result, output_json, label="Upload")
|
|
28
|
+
if not output_json:
|
|
29
|
+
url = result.get("url") or result.get("file_url") or result.get("output", [None])[0] if isinstance(result.get("output"), list) else result.get("output")
|
|
30
|
+
if url:
|
|
31
|
+
console.print(f"\nHosted URL: [bold green]{url}[/bold green]")
|
muapi/commands/video.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""muapi video — text-to-video and image-to-video generation."""
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from .. import client, exitcodes
|
|
7
|
+
from ..utils import (
|
|
8
|
+
console, download_outputs, error_exit, print_result,
|
|
9
|
+
print_dry_run, read_stdin_if_dash, spinner_status,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(help="Generate videos from text or images.")
|
|
13
|
+
|
|
14
|
+
# ── Model registries ──────────────────────────────────────────────────────────
|
|
15
|
+
# Short alias → actual muapi endpoint_url (must match server schema).
|
|
16
|
+
|
|
17
|
+
T2V_MODELS = {
|
|
18
|
+
# Veo
|
|
19
|
+
"veo3": "veo3-text-to-video",
|
|
20
|
+
"veo3-fast": "veo3-fast-text-to-video",
|
|
21
|
+
"veo3.1": "veo3.1-text-to-video",
|
|
22
|
+
"veo3.1-fast": "veo3.1-fast-text-to-video",
|
|
23
|
+
"veo3.1-4k": "veo3.1-4k-video",
|
|
24
|
+
"veo3.1-lite": "veo3.1-lite-text-to-video",
|
|
25
|
+
"veo4": "veo-4-text-to-video",
|
|
26
|
+
# Kling
|
|
27
|
+
"kling-master": "kling-v2.1-master-t2v",
|
|
28
|
+
"kling-v2.5-pro": "kling-v2.5-turbo-pro-t2v",
|
|
29
|
+
"kling-v2.6-pro": "kling-v2.6-pro-t2v",
|
|
30
|
+
"kling-v3-pro": "kling-v3.0-pro-text-to-video",
|
|
31
|
+
"kling-v3-std": "kling-v3.0-standard-text-to-video",
|
|
32
|
+
"kling-v3-4k": "kling-v3.0-4k-text-to-video",
|
|
33
|
+
"kling-v3-omni": "kling-v3.0-omni-pro-text-to-video",
|
|
34
|
+
"kling-v3-omni-std": "kling-v3.0-omni-standard-text-to-video",
|
|
35
|
+
"kling-v3-omni-4k": "kling-v3.0-omni-4k-text-to-video",
|
|
36
|
+
"kling-o1": "kling-o1-text-to-video",
|
|
37
|
+
# Wan
|
|
38
|
+
"wan2.1": "wan2.1-text-to-video",
|
|
39
|
+
"wan2.2": "wan2.2-text-to-video",
|
|
40
|
+
"wan2.2-5b-fast": "wan2.2-5b-fast-t2v",
|
|
41
|
+
"wan2.5": "wan2.5-text-to-video",
|
|
42
|
+
"wan2.5-fast": "wan2.5-text-to-video-fast",
|
|
43
|
+
"wan2.6": "wan2.6-text-to-video",
|
|
44
|
+
"wan2.7": "wan2.7-text-to-video",
|
|
45
|
+
# Seedance
|
|
46
|
+
"seedance-pro": "seedance-pro-t2v",
|
|
47
|
+
"seedance-pro-fast": "seedance-pro-t2v-fast",
|
|
48
|
+
"seedance-lite": "seedance-lite-t2v",
|
|
49
|
+
"seedance-v1.5": "seedance-v1.5-pro-t2v",
|
|
50
|
+
"seedance-v1.5-fast":"seedance-v1.5-pro-t2v-fast",
|
|
51
|
+
"seedance-v2": "seedance-v2.0-t2v",
|
|
52
|
+
"seedance-2": "seedance-2-text-to-video",
|
|
53
|
+
"seedance-2-fast": "seedance-2-text-to-video-fast",
|
|
54
|
+
"seedance-2-vip": "seedance-2-vip-text-to-video",
|
|
55
|
+
"seedance-2-vip-fast":"seedance-2-vip-text-to-video-fast",
|
|
56
|
+
# Hunyuan
|
|
57
|
+
"hunyuan": "hunyuan-text-to-video",
|
|
58
|
+
"hunyuan-fast": "hunyuan-fast-text-to-video",
|
|
59
|
+
# Runway
|
|
60
|
+
"runway": "runway-text-to-video",
|
|
61
|
+
# Pixverse
|
|
62
|
+
"pixverse": "pixverse-v4.5-t2v",
|
|
63
|
+
"pixverse-v4.5": "pixverse-v4.5-t2v",
|
|
64
|
+
"pixverse-v5": "pixverse-v5-t2v",
|
|
65
|
+
"pixverse-v5.5": "pixverse-v5.5-t2v",
|
|
66
|
+
"pixverse-v6": "pixverse-v6-t2v",
|
|
67
|
+
# Vidu
|
|
68
|
+
"vidu": "vidu-v2.0-t2v",
|
|
69
|
+
"vidu-q2-pro": "vidu-q2-pro-text-to-video",
|
|
70
|
+
"vidu-q2-turbo": "vidu-q2-turbo-text-to-video",
|
|
71
|
+
"vidu-q3-pro": "vidu-q3-pro-text-to-video",
|
|
72
|
+
"vidu-q3-turbo": "vidu-q3-turbo-text-to-video",
|
|
73
|
+
# MiniMax / Hailuo
|
|
74
|
+
"minimax-std": "minimax-hailuo-02-standard-t2v",
|
|
75
|
+
"minimax-pro": "minimax-hailuo-02-pro-t2v",
|
|
76
|
+
"minimax-2.3-pro": "minimax-hailuo-2.3-pro-t2v",
|
|
77
|
+
"minimax-2.3-std": "minimax-hailuo-2.3-standard-t2v",
|
|
78
|
+
# LTX
|
|
79
|
+
"ltx-2": "ltx-2-pro-text-to-video",
|
|
80
|
+
"ltx-2-fast": "ltx-2-fast-text-to-video",
|
|
81
|
+
"ltx-2-19b": "ltx-2-19b-text-to-video",
|
|
82
|
+
"ltx-2.3": "ltx-2.3-text-to-video",
|
|
83
|
+
# OpenAI Sora
|
|
84
|
+
"sora": "openai-sora",
|
|
85
|
+
"sora-2": "openai-sora-2-text-to-video",
|
|
86
|
+
"sora-2-pro": "openai-sora-2-pro-text-to-video",
|
|
87
|
+
"sora-2-standard": "openai-sora-2-standard-text-to-video",
|
|
88
|
+
"sora-2-storyboard": "openai-sora-2-pro-storyboard",
|
|
89
|
+
# Other
|
|
90
|
+
"ovi": "ovi-text-to-video",
|
|
91
|
+
"grok": "grok-imagine-text-to-video",
|
|
92
|
+
"happy-horse": "happy-horse-1-text-to-video-1080p",
|
|
93
|
+
"happy-horse-720": "happy-horse-1-text-to-video-720p",
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
I2V_MODELS = {
|
|
97
|
+
# Veo
|
|
98
|
+
"veo3": "veo3-image-to-video",
|
|
99
|
+
"veo3-fast": "veo3-fast-image-to-video",
|
|
100
|
+
"veo3.1": "veo3.1-image-to-video",
|
|
101
|
+
"veo3.1-fast": "veo3.1-fast-image-to-video",
|
|
102
|
+
"veo3.1-ref": "veo3.1-reference-to-video",
|
|
103
|
+
"veo3.1-lite": "veo3.1-lite-image-to-video",
|
|
104
|
+
"veo4": "veo-4-image-to-video",
|
|
105
|
+
# Kling
|
|
106
|
+
"kling-std": "kling-v2.1-standard-i2v",
|
|
107
|
+
"kling-pro": "kling-v2.1-pro-i2v",
|
|
108
|
+
"kling-master": "kling-v2.1-master-i2v",
|
|
109
|
+
"kling-v2.5-pro": "kling-v2.5-turbo-pro-i2v",
|
|
110
|
+
"kling-v2.5-std": "kling-v2.5-turbo-std-i2v",
|
|
111
|
+
"kling-v2.6-pro": "kling-v2.6-pro-i2v",
|
|
112
|
+
"kling-v3-pro": "kling-v3.0-pro-image-to-video",
|
|
113
|
+
"kling-v3-std": "kling-v3.0-standard-image-to-video",
|
|
114
|
+
"kling-v3-4k": "kling-v3.0-4k-image-to-video",
|
|
115
|
+
"kling-v3-omni": "kling-v3.0-omni-pro-image-to-video",
|
|
116
|
+
"kling-v3-omni-std": "kling-v3.0-omni-standard-image-to-video",
|
|
117
|
+
"kling-v3-omni-4k": "kling-v3.0-omni-4k-image-to-video",
|
|
118
|
+
"kling-o1": "kling-o1-image-to-video",
|
|
119
|
+
"kling-o1-std": "kling-o1-standard-image-to-video",
|
|
120
|
+
"kling-o1-ref": "kling-o1-reference-to-video",
|
|
121
|
+
# Wan
|
|
122
|
+
"wan2.1": "wan2.1-image-to-video",
|
|
123
|
+
"wan2.1-ref": "wan2.1-reference-video",
|
|
124
|
+
"wan2.2": "wan2.2-image-to-video",
|
|
125
|
+
"wan2.2-spicy": "wan2.2-spicy-image-to-video",
|
|
126
|
+
"wan2.5": "wan2.5-image-to-video",
|
|
127
|
+
"wan2.5-fast": "wan2.5-image-to-video-fast",
|
|
128
|
+
"wan2.6": "wan2.6-image-to-video",
|
|
129
|
+
"wan2.7": "wan2.7-image-to-video",
|
|
130
|
+
"wan2.7-ref": "wan2.7-reference-to-video",
|
|
131
|
+
# Seedance
|
|
132
|
+
"seedance-pro": "seedance-pro-i2v",
|
|
133
|
+
"seedance-pro-fast": "seedance-pro-i2v-fast",
|
|
134
|
+
"seedance-lite": "seedance-lite-i2v",
|
|
135
|
+
"seedance-lite-ref": "seedance-lite-reference-to-video",
|
|
136
|
+
"seedance-v1.5": "seedance-v1.5-pro-i2v",
|
|
137
|
+
"seedance-v1.5-fast":"seedance-v1.5-pro-i2v-fast",
|
|
138
|
+
"seedance-v2": "seedance-v2.0-i2v",
|
|
139
|
+
"seedance-v2-omni": "seedance-2.0-omni-reference",
|
|
140
|
+
"seedance-2": "seedance-2-image-to-video",
|
|
141
|
+
"seedance-2-fast": "seedance-2-image-to-video-fast",
|
|
142
|
+
"seedance-2-flf": "seedance-2-first-last-frame",
|
|
143
|
+
"seedance-2-omni": "seedance-2.0-omni-reference",
|
|
144
|
+
"seedance-2-vip": "seedance-2-vip-image-to-video",
|
|
145
|
+
# Hunyuan
|
|
146
|
+
"hunyuan": "hunyuan-image-to-video",
|
|
147
|
+
# Runway
|
|
148
|
+
"runway": "runway-image-to-video",
|
|
149
|
+
"runway-act-two": "runway-act-two-i2v",
|
|
150
|
+
# Pixverse
|
|
151
|
+
"pixverse-v4.5": "pixverse-v4.5-i2v",
|
|
152
|
+
"pixverse-v5": "pixverse-v5-i2v",
|
|
153
|
+
"pixverse-v5.5": "pixverse-v5.5-i2v",
|
|
154
|
+
"pixverse-v6": "pixverse-v6-i2v",
|
|
155
|
+
"pixverse-v6-trans": "pixverse-v6-transition",
|
|
156
|
+
# Vidu
|
|
157
|
+
"vidu": "vidu-v2.0-i2v",
|
|
158
|
+
"vidu-q1-ref": "vidu-q1-reference",
|
|
159
|
+
"vidu-q2-pro": "vidu-q2-pro-image-to-video",
|
|
160
|
+
"vidu-q2-turbo": "vidu-q2-turbo-image-to-video",
|
|
161
|
+
"vidu-q2-ref": "vidu-q2-reference",
|
|
162
|
+
"vidu-q2-start-end": "vidu-q2-pro-start-end-video",
|
|
163
|
+
"vidu-q3-pro": "vidu-q3-pro-image-to-video",
|
|
164
|
+
"vidu-q3-turbo": "vidu-q3-turbo-image-to-video",
|
|
165
|
+
"vidu-q3-flf": "vidu-q3-pro-first-last-frames",
|
|
166
|
+
# Midjourney
|
|
167
|
+
"midjourney": "midjourney-v7-image-to-video",
|
|
168
|
+
# MiniMax / Hailuo
|
|
169
|
+
"minimax-std": "minimax-hailuo-02-standard-i2v",
|
|
170
|
+
"minimax-pro": "minimax-hailuo-02-pro-i2v",
|
|
171
|
+
"minimax-2.3-pro": "minimax-hailuo-2.3-pro-i2v",
|
|
172
|
+
"minimax-2.3-std": "minimax-hailuo-2.3-standard-i2v",
|
|
173
|
+
"minimax-2.3-fast": "minimax-hailuo-2.3-fast",
|
|
174
|
+
# LTX
|
|
175
|
+
"ltx-2": "ltx-2-pro-image-to-video",
|
|
176
|
+
"ltx-2-fast": "ltx-2-fast-image-to-video",
|
|
177
|
+
"ltx-2-19b": "ltx-2-19b-image-to-video",
|
|
178
|
+
"ltx-2.3": "ltx-2.3-image-to-video",
|
|
179
|
+
# OpenAI Sora
|
|
180
|
+
"sora-2": "openai-sora-2-image-to-video",
|
|
181
|
+
"sora-2-pro": "openai-sora-2-pro-image-to-video",
|
|
182
|
+
"sora-2-standard": "openai-sora-2-standard-image-to-video",
|
|
183
|
+
# Other
|
|
184
|
+
"ovi": "ovi-image-to-video",
|
|
185
|
+
"grok": "grok-imagine-image-to-video",
|
|
186
|
+
"leonardo": "leonardoai-motion-2.0",
|
|
187
|
+
"happy-horse": "happy-horse-1-image-to-video-1080p",
|
|
188
|
+
"happy-horse-ref": "happy-horse-1-reference-to-video-1080p",
|
|
189
|
+
"infinitetalk": "infinitetalk-image-to-video",
|
|
190
|
+
"video-effects": "video-effects",
|
|
191
|
+
"wan-effects": "generate_wan_ai_effects",
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# I2V models that send images via "images_list" (array) instead of "image_url".
|
|
195
|
+
LIST_INPUT_I2V = {
|
|
196
|
+
"wan2.1", "wan2.1-ref", "wan2.2", "wan2.2-spicy",
|
|
197
|
+
"wan2.5", "wan2.5-fast", "wan2.6", "wan2.7", "wan2.7-ref",
|
|
198
|
+
"seedance-pro", "seedance-pro-fast", "seedance-lite", "seedance-lite-ref",
|
|
199
|
+
"seedance-v1.5", "seedance-v1.5-fast", "seedance-v2", "seedance-v2-omni",
|
|
200
|
+
"seedance-2", "seedance-2-fast", "seedance-2-flf", "seedance-2-omni",
|
|
201
|
+
"seedance-2-vip",
|
|
202
|
+
"vidu", "vidu-q1-ref", "vidu-q2-pro", "vidu-q2-turbo", "vidu-q2-ref",
|
|
203
|
+
"vidu-q2-start-end", "vidu-q3-pro", "vidu-q3-turbo", "vidu-q3-flf",
|
|
204
|
+
"pixverse-v4.5", "pixverse-v5", "pixverse-v5.5", "pixverse-v6", "pixverse-v6-trans",
|
|
205
|
+
"veo4",
|
|
206
|
+
"sora-2", "sora-2-pro", "sora-2-standard",
|
|
207
|
+
"kling-v3-4k", "kling-v3-omni", "kling-v3-omni-std", "kling-v3-omni-4k",
|
|
208
|
+
"happy-horse", "happy-horse-ref",
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@app.command("generate")
|
|
213
|
+
def generate(
|
|
214
|
+
prompt: str = typer.Argument(..., help="Text prompt. Pass '-' to read from stdin."),
|
|
215
|
+
model: str = typer.Option("kling-master", "--model", "-m",
|
|
216
|
+
help=f"Model. Choices: {', '.join(T2V_MODELS)}"),
|
|
217
|
+
duration: int = typer.Option(5, "--duration", "-D", help="Duration in seconds"),
|
|
218
|
+
aspect_ratio: str = typer.Option("16:9", "--aspect-ratio", "-a"),
|
|
219
|
+
webhook: Optional[str] = typer.Option(None, "--webhook"),
|
|
220
|
+
wait: bool = typer.Option(True, "--wait/--no-wait"),
|
|
221
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
222
|
+
download: Optional[str] = typer.Option(None, "--download", "-d"),
|
|
223
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
224
|
+
jq: Optional[str] = typer.Option(None, "--jq"),
|
|
225
|
+
):
|
|
226
|
+
"""Generate a video from a text prompt.
|
|
227
|
+
|
|
228
|
+
\b
|
|
229
|
+
Examples:
|
|
230
|
+
muapi video generate "a dog running on a beach" --model kling-master
|
|
231
|
+
muapi video generate "ocean waves" --no-wait --output-json --jq '.request_id'
|
|
232
|
+
"""
|
|
233
|
+
prompt = read_stdin_if_dash(prompt)
|
|
234
|
+
if model not in T2V_MODELS:
|
|
235
|
+
error_exit(f"Unknown model '{model}'. Choices: {', '.join(T2V_MODELS)}", exitcodes.VALIDATION)
|
|
236
|
+
endpoint = T2V_MODELS[model]
|
|
237
|
+
|
|
238
|
+
payload: dict = {"prompt": prompt, "duration": duration, "aspect_ratio": aspect_ratio}
|
|
239
|
+
if webhook:
|
|
240
|
+
payload["webhook_url"] = webhook
|
|
241
|
+
|
|
242
|
+
if dry_run:
|
|
243
|
+
print_dry_run(endpoint, payload)
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
with spinner_status(f"Generating video with {model}... (may take a while)"):
|
|
248
|
+
result = client.generate(endpoint, payload, wait=wait)
|
|
249
|
+
except client.MuapiError as e:
|
|
250
|
+
error_exit(str(e), e.exit_code)
|
|
251
|
+
|
|
252
|
+
print_result(result, output_json, label=f"Video ({model})", jq=jq)
|
|
253
|
+
if download and result.get("status") == "completed":
|
|
254
|
+
download_outputs(result, download)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@app.command("from-image")
|
|
258
|
+
def from_image(
|
|
259
|
+
prompt: str = typer.Argument(..., help="Motion/animation prompt. Pass '-' for stdin."),
|
|
260
|
+
image: str = typer.Option(..., "--image", "-i", help="Source image URL"),
|
|
261
|
+
model: str = typer.Option("kling-std", "--model", "-m",
|
|
262
|
+
help=f"Model. Choices: {', '.join(I2V_MODELS)}"),
|
|
263
|
+
duration: int = typer.Option(5, "--duration", "-D"),
|
|
264
|
+
aspect_ratio: str = typer.Option("16:9", "--aspect-ratio", "-a"),
|
|
265
|
+
webhook: Optional[str] = typer.Option(None, "--webhook"),
|
|
266
|
+
wait: bool = typer.Option(True, "--wait/--no-wait"),
|
|
267
|
+
dry_run: bool = typer.Option(False, "--dry-run"),
|
|
268
|
+
download: Optional[str] = typer.Option(None, "--download", "-d"),
|
|
269
|
+
output_json: bool = typer.Option(False, "--output-json", "-j"),
|
|
270
|
+
jq: Optional[str] = typer.Option(None, "--jq"),
|
|
271
|
+
):
|
|
272
|
+
"""Animate an image into a video."""
|
|
273
|
+
prompt = read_stdin_if_dash(prompt)
|
|
274
|
+
if model not in I2V_MODELS:
|
|
275
|
+
error_exit(f"Unknown model '{model}'. Choices: {', '.join(I2V_MODELS)}", exitcodes.VALIDATION)
|
|
276
|
+
endpoint = I2V_MODELS[model]
|
|
277
|
+
|
|
278
|
+
payload: dict = {
|
|
279
|
+
"prompt": prompt,
|
|
280
|
+
"duration": duration, "aspect_ratio": aspect_ratio,
|
|
281
|
+
}
|
|
282
|
+
if model in LIST_INPUT_I2V:
|
|
283
|
+
payload["images_list"] = [image]
|
|
284
|
+
else:
|
|
285
|
+
payload["image_url"] = image
|
|
286
|
+
if webhook:
|
|
287
|
+
payload["webhook_url"] = webhook
|
|
288
|
+
|
|
289
|
+
if dry_run:
|
|
290
|
+
print_dry_run(endpoint, payload)
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
with spinner_status(f"Animating image with {model}..."):
|
|
295
|
+
result = client.generate(endpoint, payload, wait=wait)
|
|
296
|
+
except client.MuapiError as e:
|
|
297
|
+
error_exit(str(e), e.exit_code)
|
|
298
|
+
|
|
299
|
+
print_result(result, output_json, label=f"Image-to-Video ({model})", jq=jq)
|
|
300
|
+
if download and result.get("status") == "completed":
|
|
301
|
+
download_outputs(result, download)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@app.command("models")
|
|
305
|
+
def list_models():
|
|
306
|
+
"""List all available video generation models."""
|
|
307
|
+
from rich.table import Table
|
|
308
|
+
from ..utils import out
|
|
309
|
+
|
|
310
|
+
t = Table(title="Video Models", show_header=True, header_style="bold magenta")
|
|
311
|
+
t.add_column("Name", style="cyan")
|
|
312
|
+
t.add_column("Type")
|
|
313
|
+
t.add_column("Endpoint")
|
|
314
|
+
for name, ep in T2V_MODELS.items():
|
|
315
|
+
t.add_row(name, "text-to-video", ep)
|
|
316
|
+
for name, ep in I2V_MODELS.items():
|
|
317
|
+
t.add_row(name, "image-to-video", ep)
|
|
318
|
+
out.print(t)
|