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/commands/docs.py ADDED
@@ -0,0 +1,81 @@
1
+ """muapi docs — access the muapi.ai API documentation."""
2
+ import json
3
+ import webbrowser
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from .. import exitcodes
9
+ from ..config import BASE_URL
10
+ from ..utils import console, error_exit, out
11
+
12
+ app = typer.Typer(help="Access the muapi.ai API documentation.")
13
+
14
+ _HOST = BASE_URL.replace("/api/v1", "")
15
+ _OPENAPI_URL = f"{_HOST}/openapi.json"
16
+ _DOCS_URL = f"{_HOST}/docs"
17
+ _REDOC_URL = f"{_HOST}/redoc"
18
+
19
+
20
+ @app.command("openapi")
21
+ def openapi(
22
+ output_json: bool = typer.Option(True, "--output-json/--no-output-json", "-j",
23
+ help="Print the raw OpenAPI JSON (default: on)"),
24
+ jq: str = typer.Option("", "--jq", help="jq-style filter (e.g. '.paths | keys[]')"),
25
+ save: str = typer.Option("", "--save", "-s", help="Save spec to a file path"),
26
+ ):
27
+ """Fetch and print the muapi.ai OpenAPI spec.
28
+
29
+ Useful for agents to discover all available endpoints, request schemas,
30
+ and response shapes without reading source code.
31
+
32
+ Examples:
33
+
34
+ \\b
35
+ muapi docs openapi --jq '.paths | keys[]'
36
+ muapi docs openapi --save ./muapi-openapi.json
37
+ """
38
+ try:
39
+ resp = httpx.get(_OPENAPI_URL, timeout=30.0)
40
+ except httpx.RequestError as exc:
41
+ error_exit(f"Network error: {exc}", exitcodes.ERROR)
42
+
43
+ if resp.status_code >= 400:
44
+ error_exit(f"Failed to fetch OpenAPI spec: {resp.status_code}", exitcodes.ERROR)
45
+
46
+ spec = resp.json()
47
+
48
+ if save:
49
+ import pathlib
50
+ pathlib.Path(save).write_text(json.dumps(spec, indent=2))
51
+ console.print(f"[green]Saved to {save}[/green]")
52
+ return
53
+
54
+ if jq:
55
+ from ..utils import apply_jq
56
+ filtered = apply_jq(spec, jq)
57
+ if isinstance(filtered, (dict, list)):
58
+ out.print_json(json.dumps(filtered, indent=2))
59
+ else:
60
+ out.print(str(filtered))
61
+ return
62
+
63
+ if output_json:
64
+ out.print_json(json.dumps(spec))
65
+ else:
66
+ # Human summary
67
+ info = spec.get("info", {})
68
+ paths = spec.get("paths", {})
69
+ console.print(f"[bold]{info.get('title', 'muapi')}[/bold] v{info.get('version', '?')}")
70
+ console.print(f"{len(paths)} endpoints — full spec at: [blue]{_OPENAPI_URL}[/blue]")
71
+ console.print(f"Interactive docs: [blue]{_DOCS_URL}[/blue]")
72
+
73
+
74
+ @app.command("open")
75
+ def open_docs(
76
+ ui: str = typer.Option("swagger", "--ui", help="UI to open: swagger, redoc"),
77
+ ):
78
+ """Open the API documentation in your browser."""
79
+ url = _REDOC_URL if ui == "redoc" else _DOCS_URL
80
+ console.print(f"Opening [blue]{url}[/blue]")
81
+ webbrowser.open(url)
muapi/commands/edit.py ADDED
@@ -0,0 +1,134 @@
1
+ """muapi edit — video editing effects, lipsync, dance, dress-change."""
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from .. import client
7
+ from ..utils import console, download_outputs, error_exit, print_result, spinner_status
8
+
9
+ app = typer.Typer(help="Edit videos with effects, lipsync, dance, dress-change, and more.")
10
+
11
+
12
+ def _run(label: str, endpoint: str, payload: dict, wait: bool, download: Optional[str], output_json: bool):
13
+ try:
14
+ with spinner_status(f"{label}..."):
15
+ result = client.generate(endpoint, payload, wait=wait)
16
+ except client.MuapiError as e:
17
+ error_exit(str(e))
18
+ print_result(result, output_json, label=label)
19
+ if download and result.get("status") == "completed":
20
+ download_outputs(result, download)
21
+
22
+
23
+ @app.command("effects")
24
+ def effects(
25
+ video_url: str = typer.Option(None, "--video", "-v", help="Source video URL (for video effects)"),
26
+ image_url: str = typer.Option(None, "--image", "-i", help="Source image URL (for image/wan effects)"),
27
+ effect: str = typer.Option(..., "--effect", "-e", help="Effect name/prompt"),
28
+ mode: str = typer.Option("video", "--mode", "-m", help="'video', 'image', or 'wan'"),
29
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
30
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
31
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
32
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
33
+ ):
34
+ """Apply AI effects to a video or image."""
35
+ if mode == "wan":
36
+ if not image_url:
37
+ error_exit("--image required for wan effects mode")
38
+ payload = {"image_url": image_url, "effect": effect}
39
+ if webhook: payload["webhook_url"] = webhook
40
+ _run("Applying WAN effects", "generate_wan_ai_effects", payload, wait, download, output_json)
41
+ elif mode == "image":
42
+ if not image_url:
43
+ error_exit("--image required for image effects mode")
44
+ payload = {"image_url": image_url, "effect": effect}
45
+ if webhook: payload["webhook_url"] = webhook
46
+ _run("Applying image effects", "image-effects", payload, wait, download, output_json)
47
+ else:
48
+ if not video_url:
49
+ error_exit("--video required for video effects mode")
50
+ payload = {"video_url": video_url, "effect": effect}
51
+ if webhook: payload["webhook_url"] = webhook
52
+ _run("Applying video effects", "video-effects", payload, wait, download, output_json)
53
+
54
+
55
+ @app.command("lipsync")
56
+ def lipsync(
57
+ video_url: str = typer.Option(..., "--video", "-v", help="Source video URL"),
58
+ audio_url: str = typer.Option(..., "--audio", "-a", help="Audio file URL"),
59
+ model: str = typer.Option("sync", "--model", "-m", help="Model: sync, latentsync, creatify, veed"),
60
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
61
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
62
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
63
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
64
+ ):
65
+ """Sync lip movements to audio."""
66
+ endpoint_map = {
67
+ "sync": "sync-lipsync",
68
+ "latentsync": "latentsync-video",
69
+ "creatify": "creatify-lipsync",
70
+ "veed": "veed-lipsync",
71
+ "ltx-2": "ltx-2-19b-lipsync",
72
+ "ltx-2.3": "ltx-2.3-lipsync",
73
+ "kling-v1": "kling-v1-avatar-pro",
74
+ "kling-v2": "kling-v2-avatar-pro",
75
+ "wan2.2": "wan2.2-speech-to-video",
76
+ }
77
+ if model not in endpoint_map:
78
+ error_exit(f"Unknown lipsync model '{model}'. Choices: {', '.join(endpoint_map)}")
79
+ payload = {"video_url": video_url, "audio_url": audio_url}
80
+ if webhook: payload["webhook_url"] = webhook
81
+ _run(f"Lipsync ({model})", endpoint_map[model], payload, wait, download, output_json)
82
+
83
+
84
+ @app.command("dance")
85
+ def dance(
86
+ image_url: str = typer.Option(..., "--image", "-i", help="Person image URL"),
87
+ video_url: str = typer.Option(..., "--video", "-v", help="Dance reference video URL"),
88
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
89
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
90
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
91
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
92
+ ):
93
+ """Make a person dance by referencing a dance video."""
94
+ payload = {"image_url": image_url, "video_url": video_url}
95
+ if webhook: payload["webhook_url"] = webhook
96
+ _run("Generating dance", "ai-dance-effects", payload, wait, download, output_json)
97
+
98
+
99
+ @app.command("dress")
100
+ def dress(
101
+ image_url: str = typer.Option(..., "--image", "-i", help="Person/model image URL"),
102
+ dress_url: str = typer.Option(..., "--dress", "-D", help="Garment/clothing image URL"),
103
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
104
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
105
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
106
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
107
+ ):
108
+ """Change clothing/dress in an image (ai-dress-change)."""
109
+ payload: dict = {
110
+ "model_image_url": image_url,
111
+ "garment_image_url": dress_url,
112
+ }
113
+ if webhook: payload["webhook_url"] = webhook
114
+ _run("Changing dress", "ai-dress-change", payload, wait, download, output_json)
115
+
116
+
117
+ @app.command("clipping")
118
+ def clipping(
119
+ video_url: str = typer.Argument(..., help="Source video URL"),
120
+ num_highlights: int = typer.Option(3, "--highlights", "-n", help="Number of highlight clips"),
121
+ aspect_ratio: str = typer.Option("9:16", "--aspect-ratio", "-a"),
122
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
123
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
124
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
125
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
126
+ ):
127
+ """AI-powered video clipping — extract the best highlights."""
128
+ payload = {
129
+ "video_url": video_url,
130
+ "num_highlights": num_highlights,
131
+ "aspect_ratio": aspect_ratio,
132
+ }
133
+ if webhook: payload["webhook_url"] = webhook
134
+ _run("AI clipping", "ai-clipping", payload, wait, download, output_json)
@@ -0,0 +1,157 @@
1
+ """muapi enhance — image/video enhancement tools."""
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from .. import client
7
+ from ..utils import console, download_outputs, error_exit, print_result, spinner_status
8
+
9
+ app = typer.Typer(help="Enhance images and videos (upscale, bg-remove, face-swap, etc.).")
10
+
11
+
12
+ def _run(label: str, endpoint: str, payload: dict, wait: bool, download: Optional[str], output_json: bool):
13
+ try:
14
+ with spinner_status(f"{label}..."):
15
+ result = client.generate(endpoint, payload, wait=wait)
16
+ except client.MuapiError as e:
17
+ error_exit(str(e))
18
+ print_result(result, output_json, label=label)
19
+ if download and result.get("status") == "completed":
20
+ download_outputs(result, download)
21
+
22
+
23
+ def _payload_with_webhook(payload: dict, webhook: Optional[str]) -> dict:
24
+ if webhook:
25
+ payload["webhook_url"] = webhook
26
+ return payload
27
+
28
+
29
+ @app.command("upscale")
30
+ def upscale(
31
+ image_url: str = typer.Argument(..., help="Image URL to upscale"),
32
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
33
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
34
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
35
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
36
+ ):
37
+ """Upscale an image with AI."""
38
+ _run("Upscaling image", "ai-image-upscale", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
39
+
40
+
41
+ @app.command("bg-remove")
42
+ def bg_remove(
43
+ image_url: str = typer.Argument(..., help="Image URL"),
44
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
45
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
46
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
47
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
48
+ ):
49
+ """Remove the background from an image."""
50
+ _run("Removing background", "ai-background-remover", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
51
+
52
+
53
+ @app.command("face-swap")
54
+ def face_swap(
55
+ source_url: str = typer.Option(..., "--source", "-s", help="Source face image URL"),
56
+ target_url: str = typer.Option(..., "--target", "-t", help="Target image or video URL"),
57
+ mode: str = typer.Option("image", "--mode", "-m", help="'image' or 'video'"),
58
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
59
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
60
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
61
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
62
+ ):
63
+ """Swap faces in an image or video."""
64
+ payload = _payload_with_webhook({"source_url": source_url, "target_url": target_url}, webhook)
65
+ if mode == "video":
66
+ _run("Face swapping in video", "ai-video-face-swap", payload, wait, download, output_json)
67
+ else:
68
+ _run("Face swapping in image", "ai-image-face-swap", payload, wait, download, output_json)
69
+
70
+
71
+ @app.command("skin")
72
+ def skin(
73
+ image_url: str = typer.Argument(..., help="Image URL"),
74
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
75
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
76
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
77
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
78
+ ):
79
+ """Enhance skin quality in a portrait image."""
80
+ _run("Enhancing skin", "ai-skin-enhancer", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
81
+
82
+
83
+ @app.command("colorize")
84
+ def colorize(
85
+ image_url: str = typer.Argument(..., help="Black & white image URL"),
86
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
87
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
88
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
89
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
90
+ ):
91
+ """Colorize a black and white photo."""
92
+ _run("Colorizing photo", "ai-color-photo", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
93
+
94
+
95
+ @app.command("ghibli")
96
+ def ghibli(
97
+ image_url: str = typer.Argument(..., help="Image URL"),
98
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
99
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
100
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
101
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
102
+ ):
103
+ """Convert an image to Ghibli anime style."""
104
+ _run("Applying Ghibli style", "ai-ghibli-style", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
105
+
106
+
107
+ @app.command("anime")
108
+ def anime(
109
+ image_url: str = typer.Argument(..., help="Image URL"),
110
+ prompt: str = typer.Option("", "--prompt", "-p", help="Style prompt"),
111
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
112
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
113
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
114
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
115
+ ):
116
+ """Convert an image to anime style."""
117
+ _run("Generating anime style", "ai-anime-generator", _payload_with_webhook({"image_url": image_url, "prompt": prompt}, webhook), wait, download, output_json)
118
+
119
+
120
+ @app.command("extend")
121
+ def extend(
122
+ image_url: str = typer.Argument(..., help="Image URL to extend/outpaint"),
123
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
124
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
125
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
126
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
127
+ ):
128
+ """Extend/outpaint an image."""
129
+ _run("Extending image", "ai-image-extension", _payload_with_webhook({"image_url": image_url}, webhook), wait, download, output_json)
130
+
131
+
132
+ @app.command("product-shot")
133
+ def product_shot(
134
+ image_url: str = typer.Argument(..., help="Product image URL"),
135
+ background_prompt: str = typer.Option("", "--bg", help="Background description"),
136
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
137
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
138
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
139
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
140
+ ):
141
+ """Generate professional product photography."""
142
+ payload = _payload_with_webhook({"image_url": image_url, "scene_description": background_prompt}, webhook)
143
+ _run("Generating product shot", "ai-product-shot", payload, wait, download, output_json)
144
+
145
+
146
+ @app.command("erase")
147
+ def erase(
148
+ image_url: str = typer.Argument(..., help="Image URL"),
149
+ mask_url: str = typer.Option(..., "--mask", "-m", help="Mask image URL (white = erase area)"),
150
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
151
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
152
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
153
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
154
+ ):
155
+ """Erase objects from an image using a mask."""
156
+ payload = _payload_with_webhook({"image_url": image_url, "mask_image_url": mask_url}, webhook)
157
+ _run("Erasing object", "ai-object-eraser", payload, wait, download, output_json)
@@ -0,0 +1,297 @@
1
+ """muapi image — text-to-image and image-to-image generation."""
2
+ from typing import Optional
3
+
4
+ import typer
5
+
6
+ from .. import client
7
+ from .. import exitcodes
8
+ from ..utils import (
9
+ console, download_outputs, error_exit, print_result,
10
+ print_dry_run, read_stdin_if_dash, spinner_status,
11
+ )
12
+
13
+ app = typer.Typer(help="Generate and edit images.")
14
+
15
+ # ── Model registries ───────────────────────────────────────────────────────────
16
+ # Short alias → actual muapi endpoint_url (must match server schema).
17
+
18
+ T2I_MODELS = {
19
+ # Flux family
20
+ "flux-dev": "flux-dev-image",
21
+ "flux-schnell": "flux-schnell-image",
22
+ "flux-krea": "flux-krea-dev",
23
+ "flux-kontext-dev": "flux-kontext-dev-t2i",
24
+ "flux-kontext-pro": "flux-kontext-pro-t2i",
25
+ "flux-kontext-max": "flux-kontext-max-t2i",
26
+ "flux-2-dev": "flux-2-dev",
27
+ "flux-2-pro": "flux-2-pro",
28
+ "flux-2-flex": "flux-2-flex",
29
+ "flux-2-klein-4b": "flux-2-klein-4b",
30
+ "flux-2-klein-9b": "flux-2-klein-9b",
31
+ # HiDream
32
+ "hidream-fast": "hidream_i1_fast_image",
33
+ "hidream-dev": "hidream_i1_dev_image",
34
+ "hidream-full": "hidream_i1_full_image",
35
+ # Wan
36
+ "wan2.1": "wan2.1-text-to-image",
37
+ "wan2.5": "wan2.5-text-to-image",
38
+ "wan2.6": "wan2.6-text-to-image",
39
+ "wan2.7": "wan2.7-text-to-image",
40
+ "wan2.7-pro": "wan2.7-text-to-image-pro",
41
+ # OpenAI / GPT-Image
42
+ "gpt4o": "gpt4o-text-to-image",
43
+ "gpt-image": "gpt-image-1.5",
44
+ "gpt-image-2": "gpt-image-2-text-to-image",
45
+ # Google
46
+ "imagen4": "google-imagen4",
47
+ "imagen4-fast": "google-imagen4-fast",
48
+ "imagen4-ultra": "google-imagen4-ultra",
49
+ # Midjourney
50
+ "midjourney": "midjourney-v7-text-to-image",
51
+ "midjourney-v7": "midjourney-v7",
52
+ "midjourney-v8": "midjourney-v8",
53
+ "midjourney-niji": "midjourney-niji",
54
+ # Bytedance Seedream
55
+ "seedream": "bytedance-seedream-v4.5",
56
+ "seedream-v3": "bytedance-seedream-image",
57
+ "seedream-v4": "bytedance-seedream-v4",
58
+ "seedream-v4.5": "bytedance-seedream-v4.5",
59
+ "seedream-5": "seedream-5.0",
60
+ # Qwen
61
+ "qwen": "qwen-image",
62
+ "qwen-2": "qwen-image-2.0",
63
+ "qwen-2-pro": "qwen-image-2.0-pro",
64
+ # Nano-banana (Gemini-3 style)
65
+ "nano-banana": "nano-banana",
66
+ "nano-banana-pro": "nano-banana-pro",
67
+ "nano-banana-2": "nano-banana-2",
68
+ # Kling
69
+ "kling-o1": "kling-o1-text-to-image",
70
+ "kling-o3": "kling-o3-image",
71
+ # Hunyuan
72
+ "hunyuan": "hunyuan-image-2.1",
73
+ "hunyuan-3": "hunyuan-image-3.0",
74
+ # Ideogram
75
+ "ideogram": "ideogram-v3-t2i",
76
+ # Reve
77
+ "reve": "reve-text-to-image",
78
+ # Z-Image
79
+ "z-image": "z-image-base",
80
+ "z-image-turbo": "z-image-turbo",
81
+ # Leonardo
82
+ "leonardo-lucid": "leonardoai-lucid-origin",
83
+ "leonardo-phoenix": "leonardoai-phoenix-1.0",
84
+ # Grok / xAI
85
+ "grok": "grok-imagine-text-to-image",
86
+ "grok-quality": "grok-imagine-text-to-image-quality",
87
+ # Other
88
+ "chroma": "chroma-image",
89
+ "sdxl": "sdxl-image",
90
+ "perfect-pony": "perfect-pony-xl",
91
+ "neta-lumina": "neta-lumina",
92
+ }
93
+
94
+ # Models whose API takes width+height rather than aspect_ratio.
95
+ # Everything else defaults to aspect_ratio.
96
+ WIDTH_HEIGHT_MODELS = {
97
+ "flux-dev", "flux-schnell", "flux-krea",
98
+ "flux-2-dev", "flux-2-pro", "flux-2-flex",
99
+ "flux-2-klein-4b", "flux-2-klein-9b",
100
+ "hidream-fast", "hidream-dev", "hidream-full",
101
+ "wan2.1",
102
+ "sdxl", "perfect-pony", "neta-lumina",
103
+ }
104
+
105
+ I2I_MODELS = {
106
+ # Flux Kontext (edit + effects)
107
+ "flux-kontext-dev": "flux-kontext-dev-i2i",
108
+ "flux-kontext-pro": "flux-kontext-pro-i2i",
109
+ "flux-kontext-max": "flux-kontext-max-i2i",
110
+ "flux-kontext-effects": "flux-kontext-effects",
111
+ # Flux 2 edit
112
+ "flux-2-dev-edit": "flux-2-dev-edit",
113
+ "flux-2-pro-edit": "flux-2-pro-edit",
114
+ "flux-2-flex-edit": "flux-2-flex-edit",
115
+ "flux-2-klein-4b-edit": "flux-2-klein-4b-edit",
116
+ "flux-2-klein-9b-edit": "flux-2-klein-9b-edit",
117
+ # OpenAI / GPT
118
+ "gpt4o": "gpt4o-image-to-image",
119
+ "gpt4o-edit": "gpt4o-edit",
120
+ "gpt-image-edit": "gpt-image-1.5-edit",
121
+ "gpt-image-2-edit": "gpt-image-2-image-to-image",
122
+ # Bytedance Seedream / Seededit
123
+ "seededit": "bytedance-seededit-image",
124
+ "seedream-edit": "bytedance-seedream-edit-v4",
125
+ "seedream-v4.5-edit": "bytedance-seedream-v4.5-edit",
126
+ "seedream-5-edit": "seedream-5.0-edit",
127
+ "seedance-character": "seedance-2-character",
128
+ # Reve
129
+ "reve": "reve-image-edit",
130
+ # Midjourney
131
+ "midjourney": "midjourney-v7-image-to-image",
132
+ "midjourney-style": "midjourney-v7-style-reference",
133
+ "midjourney-omni": "midjourney-v7-omni-reference",
134
+ # Qwen
135
+ "qwen": "qwen-image-edit",
136
+ "qwen-plus": "qwen-image-edit-plus",
137
+ "qwen-plus-lora": "qwen-image-edit-plus-lora",
138
+ "qwen-2511": "qwen-image-edit-2511",
139
+ "qwen-2-edit": "qwen-image-2.0-edit",
140
+ "qwen-2-pro-edit": "qwen-image-2.0-pro-edit",
141
+ # Nano-banana
142
+ "nano-banana-edit": "nano-banana-edit",
143
+ "nano-banana-effects": "nano-banana-effects",
144
+ "nano-banana-2-edit": "nano-banana-2-edit",
145
+ "nano-banana-pro-edit": "nano-banana-pro-edit",
146
+ # Kling
147
+ "kling-o1-edit": "kling-o1-edit-image",
148
+ "kling-o3-edit": "kling-o3-image-edit",
149
+ # Wan
150
+ "wan2.5-edit": "wan2.5-image-edit",
151
+ "wan2.6-edit": "wan2.6-image-edit",
152
+ "wan2.7-edit": "wan2.7-image-edit",
153
+ "wan2.7-edit-pro": "wan2.7-image-edit-pro",
154
+ # Ideogram
155
+ "ideogram-character": "ideogram-character",
156
+ "ideogram-reframe": "ideogram-v3-reframe",
157
+ # Others
158
+ "flux-redux": "flux-redux",
159
+ "flux-pulid": "flux-pulid",
160
+ "grok": "grok-imagine-image-to-image",
161
+ "photo-pack": "photo-pack",
162
+ "portrait-stylist": "portrait-stylist",
163
+ "minimax-subject": "minimax-01-subject-reference",
164
+ "vidu-q2-ref": "vidu-q2-reference-to-image",
165
+ }
166
+
167
+ # I2I models that send images via "images_list" (array) instead of "image_url".
168
+ LIST_INPUT_MODELS = {
169
+ "flux-kontext-dev", "flux-kontext-pro", "flux-kontext-max", "flux-kontext-effects",
170
+ "flux-2-dev-edit", "flux-2-pro-edit", "flux-2-flex-edit",
171
+ "flux-2-klein-4b-edit", "flux-2-klein-9b-edit",
172
+ "seedream-edit", "seedream-v4.5-edit", "seedream-5-edit", "seedance-character",
173
+ "nano-banana-edit", "nano-banana-effects",
174
+ "nano-banana-2-edit", "nano-banana-pro-edit",
175
+ "qwen", "qwen-plus", "qwen-plus-lora", "qwen-2511",
176
+ "qwen-2-edit", "qwen-2-pro-edit",
177
+ "kling-o3-edit",
178
+ "wan2.5-edit", "wan2.6-edit", "wan2.7-edit", "wan2.7-edit-pro",
179
+ "vidu-q2-ref",
180
+ }
181
+
182
+
183
+ @app.command("generate")
184
+ def generate(
185
+ prompt: str = typer.Argument(..., help="Text prompt. Pass '-' to read from stdin."),
186
+ model: str = typer.Option("flux-dev", "--model", "-m",
187
+ help=f"Model. Choices: {', '.join(T2I_MODELS)}"),
188
+ width: int = typer.Option(1024, "--width", "-W"),
189
+ height: int = typer.Option(1024, "--height", "-H"),
190
+ num_images: int = typer.Option(1, "--num-images", "-n", min=1, max=4),
191
+ aspect_ratio: str = typer.Option("1:1", "--aspect-ratio", "-a"),
192
+ webhook: Optional[str] = typer.Option(None, "--webhook", help="Webhook URL for async notification"),
193
+ wait: bool = typer.Option(True, "--wait/--no-wait", help="Poll until done"),
194
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show request without executing"),
195
+ download: Optional[str] = typer.Option(None, "--download", "-d", help="Download outputs to directory"),
196
+ output_json: bool = typer.Option(False, "--output-json", "-j", help="Print raw JSON"),
197
+ jq: Optional[str] = typer.Option(None, "--jq", help="jq-style filter on JSON output (e.g. '.outputs[0]')"),
198
+ ):
199
+ """Generate an image from a text prompt.
200
+
201
+ Pipe-friendly: pass '-' as PROMPT to read from stdin.
202
+
203
+ \b
204
+ Examples:
205
+ muapi image generate "a cyberpunk city" --model flux-dev
206
+ echo "a cat" | muapi image generate -
207
+ muapi image generate "sunset" --model nano-banana-pro --download ./out
208
+ muapi image generate "logo" --output-json --jq '.outputs[0]'
209
+ """
210
+ prompt = read_stdin_if_dash(prompt)
211
+ if model not in T2I_MODELS:
212
+ error_exit(f"Unknown model '{model}'. Choices: {', '.join(T2I_MODELS)}", exitcodes.VALIDATION)
213
+ endpoint = T2I_MODELS[model]
214
+
215
+ payload: dict = {"prompt": prompt, "num_images": num_images}
216
+ if model in WIDTH_HEIGHT_MODELS:
217
+ payload["width"] = width
218
+ payload["height"] = height
219
+ else:
220
+ payload["aspect_ratio"] = aspect_ratio
221
+ if webhook:
222
+ payload["webhook_url"] = webhook
223
+
224
+ if dry_run:
225
+ print_dry_run(endpoint, payload)
226
+ return
227
+
228
+ try:
229
+ with spinner_status(f"Generating image with {model}..."):
230
+ result = client.generate(endpoint, payload, wait=wait)
231
+ except client.MuapiError as e:
232
+ error_exit(str(e), e.exit_code)
233
+
234
+ print_result(result, output_json, label=f"Image ({model})", jq=jq)
235
+ if download and result.get("status") == "completed":
236
+ download_outputs(result, download)
237
+
238
+
239
+ @app.command("edit")
240
+ def edit(
241
+ prompt: str = typer.Argument(..., help="Edit instruction. Pass '-' to read from stdin."),
242
+ image: str = typer.Option(..., "--image", "-i", help="Source image URL"),
243
+ model: str = typer.Option("flux-kontext-dev", "--model", "-m",
244
+ help=f"Model. Choices: {', '.join(I2I_MODELS)}"),
245
+ aspect_ratio: str = typer.Option("1:1", "--aspect-ratio", "-a"),
246
+ num_images: int = typer.Option(1, "--num-images", "-n", min=1, max=4),
247
+ webhook: Optional[str] = typer.Option(None, "--webhook"),
248
+ wait: bool = typer.Option(True, "--wait/--no-wait"),
249
+ dry_run: bool = typer.Option(False, "--dry-run"),
250
+ download: Optional[str] = typer.Option(None, "--download", "-d"),
251
+ output_json: bool = typer.Option(False, "--output-json", "-j"),
252
+ jq: Optional[str] = typer.Option(None, "--jq"),
253
+ ):
254
+ """Edit an image using a prompt and reference image URL."""
255
+ prompt = read_stdin_if_dash(prompt)
256
+ if model not in I2I_MODELS:
257
+ error_exit(f"Unknown model '{model}'. Choices: {', '.join(I2I_MODELS)}", exitcodes.VALIDATION)
258
+ endpoint = I2I_MODELS[model]
259
+
260
+ payload: dict = {"prompt": prompt, "aspect_ratio": aspect_ratio, "num_images": num_images}
261
+ if model in LIST_INPUT_MODELS:
262
+ payload["images_list"] = [image]
263
+ else:
264
+ payload["image_url"] = image
265
+ if webhook:
266
+ payload["webhook_url"] = webhook
267
+
268
+ if dry_run:
269
+ print_dry_run(endpoint, payload)
270
+ return
271
+
272
+ try:
273
+ with spinner_status(f"Editing image with {model}..."):
274
+ result = client.generate(endpoint, payload, wait=wait)
275
+ except client.MuapiError as e:
276
+ error_exit(str(e), e.exit_code)
277
+
278
+ print_result(result, output_json, label=f"Image Edit ({model})", jq=jq)
279
+ if download and result.get("status") == "completed":
280
+ download_outputs(result, download)
281
+
282
+
283
+ @app.command("models")
284
+ def list_models():
285
+ """List all available image generation and editing models."""
286
+ from rich.table import Table
287
+ from ..utils import out
288
+
289
+ t = Table(title="Image Models", show_header=True, header_style="bold magenta")
290
+ t.add_column("Name", style="cyan")
291
+ t.add_column("Type")
292
+ t.add_column("Endpoint")
293
+ for name, ep in T2I_MODELS.items():
294
+ t.add_row(name, "text-to-image", ep)
295
+ for name, ep in I2I_MODELS.items():
296
+ t.add_row(name, "image-to-image", ep)
297
+ out.print(t)