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.
@@ -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)
@@ -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]")
@@ -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)