From 195097a14734de62b4fab215a1374ad88ce14782 Mon Sep 17 00:00:00 2001 From: Abhi Date: Wed, 3 Jun 2026 16:27:56 +0530 Subject: [PATCH 1/2] Add initial Python SDK support --- .gitignore | 18 ++++++++++ README.md | 32 ++++++++++++++++++ examples/image_edit.py | 11 +++++++ examples/image_generate.py | 10 ++++++ examples/prediction_wait.py | 16 +++++++++ examples/upload_file.py | 9 +++++ examples/video_generate.py | 11 +++++++ muapi/__init__.py | 2 ++ muapi/images.py | 66 +++++++++++++++++++++++++++++++++++++ muapi/predictions.py | 12 +++++++ muapi/sdk.py | 12 +++++++ muapi/uploads.py | 6 ++++ muapi/videos.py | 64 +++++++++++++++++++++++++++++++++++ 13 files changed, 269 insertions(+) create mode 100644 examples/image_edit.py create mode 100644 examples/image_generate.py create mode 100644 examples/prediction_wait.py create mode 100644 examples/upload_file.py create mode 100644 examples/video_generate.py create mode 100644 muapi/images.py create mode 100644 muapi/predictions.py create mode 100644 muapi/sdk.py create mode 100644 muapi/uploads.py create mode 100644 muapi/videos.py diff --git a/.gitignore b/.gitignore index a5e549c..8d48e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,21 @@ npm/bin/muapi-linux-x86_64 npm/bin/muapi-linux-arm64 npm/bin/muapi-windows-x86_64.exe .DS_Store +# Local SDK testing files +test_all.py +test_predictions.py +test_req.py +test_sdk.py +test_sdk_videos.py +test_uploads.py + +# Local test assets +image.png + +# Python cache +__pycache__/ +*.pyc + +# Virtual environments +venv/ +.venv/ \ No newline at end of file diff --git a/README.md b/README.md index f0f66c2..93fff03 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,39 @@ muapi account balance # Wait for an existing job muapi predict wait ``` +## Python SDK +Generate an image: + +```python +from muapi import MuAPI + +client = MuAPI() + +result = client.images.generate( + prompt="A futuristic city at sunset", + model="flux-dev" +) + +print(result) + +``` + +Generate a video: + +```python +from muapi import MuAPI + +client = MuAPI() + +result = client.videos.generate( + prompt="A dragon flying through clouds", + model="kling-master", + wait=False +) + +print(result) +``` ## Commands ### `muapi auth` diff --git a/examples/image_edit.py b/examples/image_edit.py new file mode 100644 index 0000000..2ed67b1 --- /dev/null +++ b/examples/image_edit.py @@ -0,0 +1,11 @@ +from muapi import MuAPI + +client = MuAPI() + +result = client.images.edit( + prompt="Convert this image into anime style", + image="https://example.com/image.jpg", + model="flux-kontext-dev" +) + +print(result) \ No newline at end of file diff --git a/examples/image_generate.py b/examples/image_generate.py new file mode 100644 index 0000000..32d4c49 --- /dev/null +++ b/examples/image_generate.py @@ -0,0 +1,10 @@ +from muapi import MuAPI + +client = MuAPI() + +result = client.images.generate( + prompt="A futuristic city at sunset", + model="flux-dev" +) + +print(result) \ No newline at end of file diff --git a/examples/prediction_wait.py b/examples/prediction_wait.py new file mode 100644 index 0000000..0958234 --- /dev/null +++ b/examples/prediction_wait.py @@ -0,0 +1,16 @@ +from muapi import MuAPI + +client = MuAPI() + +job = client.videos.generate( + prompt="A cinematic shot of a spaceship", + wait=False +) + +request_id = job["request_id"] + +result = client.predictions.wait( + request_id +) + +print(result) \ No newline at end of file diff --git a/examples/upload_file.py b/examples/upload_file.py new file mode 100644 index 0000000..e70fbc4 --- /dev/null +++ b/examples/upload_file.py @@ -0,0 +1,9 @@ +from muapi import MuAPI + +client = MuAPI() + +result = client.uploads.upload( + "image.png" +) + +print(result) \ No newline at end of file diff --git a/examples/video_generate.py b/examples/video_generate.py new file mode 100644 index 0000000..039dfe8 --- /dev/null +++ b/examples/video_generate.py @@ -0,0 +1,11 @@ +from muapi import MuAPI + +client = MuAPI() + +result = client.videos.generate( + prompt="A dragon flying through clouds", + model="kling-master", + wait=False +) + +print(result) \ No newline at end of file diff --git a/muapi/__init__.py b/muapi/__init__.py index d3ec452..a907d28 100644 --- a/muapi/__init__.py +++ b/muapi/__init__.py @@ -1 +1,3 @@ __version__ = "0.2.0" + +from .sdk import MuAPI \ No newline at end of file diff --git a/muapi/images.py b/muapi/images.py new file mode 100644 index 0000000..dcd6025 --- /dev/null +++ b/muapi/images.py @@ -0,0 +1,66 @@ +from . import client +from .commands.image import ( + T2I_MODELS, + I2I_MODELS, + LIST_INPUT_MODELS, +) + + +class ImagesAPI: + def generate( + self, + prompt: str, + model: str = "flux-dev", + wait: bool = True, + num_images: int = 1, + width: int = 1024, + height: int = 1024, + ): + if model not in T2I_MODELS: + raise ValueError(f"Unknown model: {model}") + + endpoint = T2I_MODELS[model] + + payload = { + "prompt": prompt, + "num_images": num_images, + "width": width, + "height": height, + } + + return client.generate( + endpoint, + payload, + wait=wait, + ) + + def edit( + self, + prompt: str, + image: str, + model: str = "flux-kontext-dev", + wait: bool = True, + num_images: int = 1, + aspect_ratio: str = "1:1", + ): + if model not in I2I_MODELS: + raise ValueError(f"Unknown model: {model}") + + endpoint = I2I_MODELS[model] + + payload = { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + "num_images": num_images, + } + + if model in LIST_INPUT_MODELS: + payload["images_list"] = [image] + else: + payload["image_url"] = image + + return client.generate( + endpoint, + payload, + wait=wait, + ) \ No newline at end of file diff --git a/muapi/predictions.py b/muapi/predictions.py new file mode 100644 index 0000000..58074c1 --- /dev/null +++ b/muapi/predictions.py @@ -0,0 +1,12 @@ +from .client import ( + get_result, + wait_for_result, +) + + +class PredictionsAPI: + def get(self, request_id: str): + return get_result(request_id) + + def wait(self, request_id: str): + return wait_for_result(request_id) \ No newline at end of file diff --git a/muapi/sdk.py b/muapi/sdk.py new file mode 100644 index 0000000..e8f0d9f --- /dev/null +++ b/muapi/sdk.py @@ -0,0 +1,12 @@ +from .images import ImagesAPI +from .videos import VideosAPI +from .uploads import UploadsAPI +from .predictions import PredictionsAPI + + +class MuAPI: + def __init__(self): + self.images = ImagesAPI() + self.videos = VideosAPI() + self.uploads = UploadsAPI() + self.predictions = PredictionsAPI() \ No newline at end of file diff --git a/muapi/uploads.py b/muapi/uploads.py new file mode 100644 index 0000000..d519507 --- /dev/null +++ b/muapi/uploads.py @@ -0,0 +1,6 @@ +from .client import upload_file + + +class UploadsAPI: + def upload(self, file_path: str): + return upload_file(file_path) \ No newline at end of file diff --git a/muapi/videos.py b/muapi/videos.py new file mode 100644 index 0000000..25aba6f --- /dev/null +++ b/muapi/videos.py @@ -0,0 +1,64 @@ +from . import client +from .commands.video import ( + T2V_MODELS, + I2V_MODELS, + LIST_INPUT_I2V, +) + + +class VideosAPI: + def generate( + self, + prompt: str, + model: str = "kling-master", + wait: bool = True, + duration: int = 5, + aspect_ratio: str = "16:9", + ): + if model not in T2V_MODELS: + raise ValueError(f"Unknown model: {model}") + + endpoint = T2V_MODELS[model] + + payload = { + "prompt": prompt, + "duration": duration, + "aspect_ratio": aspect_ratio, + } + + return client.generate( + endpoint, + payload, + wait=wait, + ) + + def from_image( + self, + prompt: str, + image: str, + model: str = "kling-std", + wait: bool = True, + duration: int = 5, + aspect_ratio: str = "16:9", + ): + if model not in I2V_MODELS: + raise ValueError(f"Unknown model: {model}") + + endpoint = I2V_MODELS[model] + + payload = { + "prompt": prompt, + "duration": duration, + "aspect_ratio": aspect_ratio, + } + + if model in LIST_INPUT_I2V: + payload["images_list"] = [image] + else: + payload["image_url"] = image + + return client.generate( + endpoint, + payload, + wait=wait, + ) \ No newline at end of file From c67ad84ba804938655ad17d2451ad1ee85164e1c Mon Sep 17 00:00:00 2001 From: Abhi Date: Thu, 4 Jun 2026 23:15:35 +0530 Subject: [PATCH 2/2] Add audio, account, edit, enhance and models SDK modules --- muapi/accounts.py | 48 ++++++++++++++++ muapi/audio.py | 98 +++++++++++++++++++++++++++++++++ muapi/edit.py | 116 +++++++++++++++++++++++++++++++++++++++ muapi/enhance.py | 136 ++++++++++++++++++++++++++++++++++++++++++++++ muapi/models.py | 29 ++++++++++ muapi/sdk.py | 15 ++++- 6 files changed, 439 insertions(+), 3 deletions(-) create mode 100644 muapi/accounts.py create mode 100644 muapi/audio.py create mode 100644 muapi/edit.py create mode 100644 muapi/enhance.py create mode 100644 muapi/models.py diff --git a/muapi/accounts.py b/muapi/accounts.py new file mode 100644 index 0000000..63ee16d --- /dev/null +++ b/muapi/accounts.py @@ -0,0 +1,48 @@ +import httpx + +from .config import BASE_URL, get_api_key +from .client import MuapiError + + +class AccountAPI: + def _headers(self): + key = get_api_key() + + if not key: + raise MuapiError( + "No API key configured. Run: muapi auth configure" + ) + + return {"x-api-key": key} + + def balance(self): + resp = httpx.get( + f"{BASE_URL}/account/balance", + headers=self._headers(), + timeout=30.0, + ) + + if resp.status_code >= 400: + raise MuapiError(resp.text, resp.status_code) + + return resp.json() + + def topup( + self, + amount: int, + currency: str = "usd", + ): + resp = httpx.post( + f"{BASE_URL}/account/topup", + json={ + "amount": amount, + "currency": currency.lower(), + }, + headers=self._headers(), + timeout=30.0, + ) + + if resp.status_code >= 400: + raise MuapiError(resp.text, resp.status_code) + + return resp.json() \ No newline at end of file diff --git a/muapi/audio.py b/muapi/audio.py new file mode 100644 index 0000000..5c73095 --- /dev/null +++ b/muapi/audio.py @@ -0,0 +1,98 @@ +from . import client + + +class AudioAPI: + def create( + self, + prompt: str, + title: str = "", + tags: str = "", + instrumental: bool = False, + wait: bool = True, + ): + payload = { + "prompt": prompt, + "title": title, + "tags": tags, + "make_instrumental": instrumental, + } + + return client.generate( + "suno-create-music", + payload, + wait=wait, + ) + + def remix( + self, + song_id: str, + prompt: str = "", + title: str = "", + tags: str = "", + wait: bool = True, + ): + payload = { + "song_id": song_id, + "prompt": prompt, + "title": title, + "tags": tags, + } + + return client.generate( + "suno-remix-music", + payload, + wait=wait, + ) + + def extend( + self, + song_id: str, + prompt: str = "", + continue_at: float = 0, + wait: bool = True, + ): + payload = { + "song_id": song_id, + "prompt": prompt, + "continue_at": continue_at, + } + + return client.generate( + "suno-extend-music", + payload, + wait=wait, + ) + + def from_text( + self, + prompt: str, + duration: float = 10.0, + wait: bool = True, + ): + payload = { + "prompt": prompt, + "duration": duration, + } + + return client.generate( + "mmaudio-v2/text-to-audio", + payload, + wait=wait, + ) + + def from_video( + self, + video_url: str, + prompt: str = "", + wait: bool = True, + ): + payload = { + "video_url": video_url, + "prompt": prompt, + } + + return client.generate( + "mmaudio-v2/video-to-video", + payload, + wait=wait, + ) \ No newline at end of file diff --git a/muapi/edit.py b/muapi/edit.py new file mode 100644 index 0000000..d300a9e --- /dev/null +++ b/muapi/edit.py @@ -0,0 +1,116 @@ +from . import client + + +class EditAPI: + def effects( + self, + effect: str, + mode: str = "video", + video_url: str = None, + image_url: str = None, + wait: bool = True, + ): + if mode == "wan": + endpoint = "generate_wan_ai_effects" + payload = { + "image_url": image_url, + "effect": effect, + } + + elif mode == "image": + endpoint = "image-effects" + payload = { + "image_url": image_url, + "effect": effect, + } + + else: + endpoint = "video-effects" + payload = { + "video_url": video_url, + "effect": effect, + } + + return client.generate( + endpoint, + payload, + wait=wait, + ) + + def lipsync( + self, + video_url: str, + audio_url: str, + model: str = "sync", + wait: bool = True, + ): + endpoint_map = { + "sync": "sync-lipsync", + "latentsync": "latentsync-video", + "creatify": "creatify-lipsync", + "veed": "veed-lipsync", + "ltx-2": "ltx-2-19b-lipsync", + "ltx-2.3": "ltx-2.3-lipsync", + "kling-v1": "kling-v1-avatar-pro", + "kling-v2": "kling-v2-avatar-pro", + "wan2.2": "wan2.2-speech-to-video", + } + + if model not in endpoint_map: + raise ValueError(f"Unknown model: {model}") + + return client.generate( + endpoint_map[model], + { + "video_url": video_url, + "audio_url": audio_url, + }, + wait=wait, + ) + + def dance( + self, + image_url: str, + video_url: str, + wait: bool = True, + ): + return client.generate( + "ai-dance-effects", + { + "image_url": image_url, + "video_url": video_url, + }, + wait=wait, + ) + + def dress( + self, + image_url: str, + dress_url: str, + wait: bool = True, + ): + return client.generate( + "ai-dress-change", + { + "model_image_url": image_url, + "garment_image_url": dress_url, + }, + wait=wait, + ) + + def clipping( + self, + video_url: str, + num_highlights: int = 3, + aspect_ratio: str = "9:16", + wait: bool = True, + ): + return client.generate( + "ai-clipping", + { + "video_url": video_url, + "num_highlights": num_highlights, + "aspect_ratio": aspect_ratio, + }, + wait=wait, + ) \ No newline at end of file diff --git a/muapi/enhance.py b/muapi/enhance.py new file mode 100644 index 0000000..d98769a --- /dev/null +++ b/muapi/enhance.py @@ -0,0 +1,136 @@ +from . import client + + +class EnhanceAPI: + def upscale( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-image-upscale", + {"image_url": image_url}, + wait=wait, + ) + + def remove_background( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-background-remover", + {"image_url": image_url}, + wait=wait, + ) + + def face_swap( + self, + source_url: str, + target_url: str, + mode: str = "image", + wait: bool = True, + ): + endpoint = ( + "ai-video-face-swap" + if mode == "video" + else "ai-image-face-swap" + ) + + return client.generate( + endpoint, + { + "source_url": source_url, + "target_url": target_url, + }, + wait=wait, + ) + + def skin( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-skin-enhancer", + {"image_url": image_url}, + wait=wait, + ) + + def colorize( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-color-photo", + {"image_url": image_url}, + wait=wait, + ) + + def ghibli( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-ghibli-style", + {"image_url": image_url}, + wait=wait, + ) + + def anime( + self, + image_url: str, + prompt: str = "", + wait: bool = True, + ): + return client.generate( + "ai-anime-generator", + { + "image_url": image_url, + "prompt": prompt, + }, + wait=wait, + ) + + def extend( + self, + image_url: str, + wait: bool = True, + ): + return client.generate( + "ai-image-extension", + {"image_url": image_url}, + wait=wait, + ) + + def product_shot( + self, + image_url: str, + background_prompt: str = "", + wait: bool = True, + ): + return client.generate( + "ai-product-shot", + { + "image_url": image_url, + "scene_description": background_prompt, + }, + wait=wait, + ) + + def erase( + self, + image_url: str, + mask_url: str, + wait: bool = True, + ): + return client.generate( + "ai-object-eraser", + { + "image_url": image_url, + "mask_image_url": mask_url, + }, + wait=wait, + ) \ No newline at end of file diff --git a/muapi/models.py b/muapi/models.py new file mode 100644 index 0000000..fcac59c --- /dev/null +++ b/muapi/models.py @@ -0,0 +1,29 @@ +from .commands.models import _ALL_MODELS + + +class ModelsAPI: + def list(self, category: str = "all"): + if category == "all": + return _ALL_MODELS + + result = {} + + for cat, models in _ALL_MODELS.items(): + if cat.startswith(category): + result[cat] = models + + return result + + def get(self, model_name: str): + for category, models in _ALL_MODELS.items(): + if model_name in models: + return { + "name": model_name, + "category": category, + "endpoint": models[model_name], + } + + raise ValueError(f"Model '{model_name}' not found") + + def categories(self): + return list(_ALL_MODELS.keys()) \ No newline at end of file diff --git a/muapi/sdk.py b/muapi/sdk.py index e8f0d9f..baf1b1f 100644 --- a/muapi/sdk.py +++ b/muapi/sdk.py @@ -2,11 +2,20 @@ from .videos import VideosAPI from .uploads import UploadsAPI from .predictions import PredictionsAPI - - +from .audio import AudioAPI +from .models import ModelsAPI +from .accounts import AccountAPI +from .edit import EditAPI +from .enhance import EnhanceAPI class MuAPI: def __init__(self): self.images = ImagesAPI() self.videos = VideosAPI() self.uploads = UploadsAPI() - self.predictions = PredictionsAPI() \ No newline at end of file + self.predictions = PredictionsAPI() + self.audio = AudioAPI() + self.models = ModelsAPI() + self.account=AccountAPI() + self.edit=EditAPI() + self.enhance=EnhanceAPI() +