From dfe8765f10a522e0b4763cc9f51b27d1a28aa01d Mon Sep 17 00:00:00 2001 From: Anil-matcha Date: Sat, 29 Aug 2026 11:16:47 +0530 Subject: [PATCH 1/4] feat(design): add opt-in MuAPI logo provider --- .claude/skills/design/SKILL.md | 16 +- .../skills/design/references/logo-design.md | 17 +- .../skills/design/scripts/logo/generate.py | 186 ++++++++++++++++-- .../scripts/logo/tests/test_generate.py | 115 +++++++++++ cli/assets/skills/design/SKILL.md | 16 +- .../skills/design/references/logo-design.md | 17 +- .../skills/design/scripts/logo/generate.py | 186 ++++++++++++++++-- .../scripts/logo/tests/test_generate.py | 115 +++++++++++ 8 files changed, 620 insertions(+), 48 deletions(-) diff --git a/.claude/skills/design/SKILL.md b/.claude/skills/design/SKILL.md index fadccf4..86309e3 100644 --- a/.claude/skills/design/SKILL.md +++ b/.claude/skills/design/SKILL.md @@ -1,6 +1,6 @@ --- name: design -description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or Atlas Cloud AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." +description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." argument-hint: "[design-type] [context]" license: MIT metadata: @@ -39,7 +39,8 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot ## Logo Design (Built-in) -55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana models. +55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana and +MuAPI image generation. ### Logo: Generate Design Brief @@ -63,6 +64,8 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro ``` **IMPORTANT:** When scripts fail, try to fix them directly. @@ -304,8 +307,17 @@ python3 --version || python --version ```bash export GEMINI_API_KEY="your-key" # https://aistudio.google.com/apikey pip install google-genai pillow + +# Optional MuAPI provider (no extra Python package required) +export MUAPI_API_KEY="your-key" ``` +MuAPI uses the asynchronous model endpoint and prediction result API. See the +[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication +and current model contracts. The logo generator currently supports the +`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared +`prompt` and `aspect_ratio` fields. + > **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`). ## Integration diff --git a/.claude/skills/design/references/logo-design.md b/.claude/skills/design/references/logo-design.md index 3fae93b..497197f 100644 --- a/.claude/skills/design/references/logo-design.md +++ b/.claude/skills/design/references/logo-design.md @@ -1,13 +1,13 @@ # Logo Design Reference -AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud is also available as an explicit opt-in. +AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; MuAPI is also available as an explicit opt-in provider. ## Scripts | Script | Purpose | |--------|---------| | `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs | -| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or Atlas Cloud | +| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or MuAPI | | `scripts/logo/core.py` | BM25 search engine for logo data | ## Commands @@ -39,9 +39,11 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro ``` -Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model` +Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model`, `--muapi-model` ## Available Styles @@ -93,4 +95,13 @@ pip install google-genai # Optional Atlas Cloud provider (no extra Python package required) export ATLASCLOUD_API_KEY="your-key" + +# Optional MuAPI provider (no extra Python package required) +export MUAPI_API_KEY="your-key" ``` + +MuAPI uses the asynchronous model endpoint and prediction result API. See the +[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication +and current model contracts. The logo generator currently supports the +`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared +`prompt` and `aspect_ratio` fields. diff --git a/.claude/skills/design/scripts/logo/generate.py b/.claude/skills/design/scripts/logo/generate.py index 7de5b70..d402b73 100644 --- a/.claude/skills/design/scripts/logo/generate.py +++ b/.claude/skills/design/scripts/logo/generate.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -"""Logo generation with Gemini or Atlas Cloud. +"""Logo generation with Gemini or MuAPI. -Gemini remains the default provider. Atlas Cloud is opt-in with -``--provider atlas`` and uses its asynchronous image generation API. +Gemini remains the default provider. MuAPI is opt-in with +``--provider muapi`` and uses its asynchronous image generation API with the +selected model's prompt/aspect-ratio contract. Models: - Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency @@ -14,6 +15,8 @@ Usage: python generate.py --brand "TechFlow" --industry tech --style minimalist python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model python generate.py --brand "TechFlow" --provider atlas + python generate.py --brand "TechFlow" --provider muapi + python generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro Batch mode (generates multiple variants): python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro @@ -57,6 +60,7 @@ load_env() # ============ CONFIGURATION ============ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") ATLASCLOUD_API_KEY = os.environ.get("ATLASCLOUD_API_KEY") +MUAPI_API_KEY = os.environ.get("MUAPI_API_KEY") # Gemini "Nano Banana" model configurations for image generation GEMINI_FLASH = "gemini-2.5-flash-image" # Nano Banana: fast, high-volume, low-latency @@ -65,9 +69,14 @@ GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quali # Atlas Cloud model validated against the live model catalog and schema. ATLAS_MODEL = "google/nano-banana-2-lite/text-to-image" ATLAS_API_BASE = "https://api.atlascloud.ai/api/v1" -HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (Atlas Cloud logo provider)" +MUAPI_MODEL = "nano-banana" +MUAPI_MODELS = ("nano-banana", "nano-banana-pro") +MUAPI_API_BASE = "https://api.muapi.ai/api/v1" +HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (logo generation)" ATLAS_POLL_INTERVAL = 2 ATLAS_MAX_POLLS = 90 +MUAPI_POLL_INTERVAL = 2 +MUAPI_MAX_POLLS = 90 # Supported aspect ratios ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] @@ -156,13 +165,13 @@ def _validate_public_https_url(url): or parsed.username or parsed.password ): - raise ValueError("Atlas Cloud returned an invalid media URL") + raise ValueError("Provider returned an invalid media URL") hostname = parsed.hostname.lower().rstrip(".") if hostname == "localhost" or hostname.endswith( (".localhost", ".local", ".internal") ): - raise ValueError("Atlas Cloud media URL used a local hostname") + raise ValueError("Provider media URL used a local hostname") try: ip = ipaddress.ip_address(hostname) @@ -170,17 +179,26 @@ def _validate_public_https_url(url): return else: if not ip.is_global: - raise ValueError("Atlas Cloud media URL used a non-public address") + raise ValueError("Provider media URL used a non-public address") -def _json_request(url, api_key, method="GET", payload=None): +def _json_request( + url, api_key, method="GET", payload=None, api_key_header="Authorization" +): + if api_key_header == "Authorization": + auth_value = f"Bearer {api_key}" + elif api_key_header == "x-api-key": + auth_value = api_key + else: + raise ValueError("Unsupported API key header") + body = json.dumps(payload).encode("utf-8") if payload is not None else None request = Request( url, data=body, method=method, headers={ - "Authorization": f"Bearer {api_key}", + api_key_header: auth_value, "Accept": "application/json", "User-Agent": HTTP_USER_AGENT, **({"Content-Type": "application/json"} if body is not None else {}), @@ -192,10 +210,10 @@ def _json_request(url, api_key, method="GET", payload=None): except HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise RuntimeError( - f"Atlas Cloud request failed ({exc.code}): {detail[:300]}" + f"Provider request failed ({exc.code}): {detail[:300]}" ) from exc except (URLError, TimeoutError, json.JSONDecodeError) as exc: - raise RuntimeError(f"Atlas Cloud request failed: {exc}") from exc + raise RuntimeError(f"Provider request failed: {exc}") from exc def _atlas_prediction_data(response): @@ -210,6 +228,10 @@ def _atlas_prediction_data(response): def _download_atlas_image(url, output_path): + _download_image(url, output_path, "image provider") + + +def _download_image(url, output_path, provider_name): _validate_public_https_url(url) request = Request( url, @@ -222,14 +244,14 @@ def _download_atlas_image(url, output_path): content_type = response.headers.get_content_type() if not content_type.startswith("image/"): raise RuntimeError( - f"Atlas Cloud output is not an image ({content_type})" + f"{provider_name} output is not an image ({content_type})" ) image_data = response.read() except (HTTPError, URLError, TimeoutError) as exc: - raise RuntimeError(f"Unable to download Atlas Cloud image: {exc}") from exc + raise RuntimeError(f"Unable to download {provider_name} image: {exc}") from exc if not image_data: - raise RuntimeError("Atlas Cloud returned an empty image") + raise RuntimeError(f"{provider_name} returned an empty image") with open(output_path, "wb") as output_file: output_file.write(image_data) @@ -281,6 +303,106 @@ def _generate_with_atlas(prompt, output_path, aspect_ratio, api_key, model): raise RuntimeError("Atlas Cloud prediction timed out while polling") +def _muapi_response_objects(response): + """Return the response and common MuAPI envelopes without guessing fields.""" + if not isinstance(response, dict): + raise TypeError("MuAPI returned an invalid response") + + objects = [response] + for key in ("data", "output", "result"): + value = response.get(key) + if isinstance(value, dict) and value not in objects: + objects.append(value) + return objects + + +def _muapi_response_value(response, keys): + for item in _muapi_response_objects(response): + for key in keys: + value = item.get(key) + if value not in (None, ""): + return value + return None + + +def _muapi_error(response): + value = _muapi_response_value(response, ("error", "message", "detail")) + if isinstance(value, str): + return value[:300] + return "MuAPI request failed" + + +def _muapi_output_url(response): + for item in _muapi_response_objects(response): + outputs = item.get("outputs") + if isinstance(outputs, list): + for output in outputs: + if isinstance(output, str) and output.startswith("https://"): + return output + if isinstance(output, dict): + for key in ("url", "image_url"): + value = output.get(key) + if isinstance(value, str) and value.startswith("https://"): + return value + raise RuntimeError("MuAPI completed without an HTTPS image URL") + + +def _download_muapi_image(url, output_path): + _download_image(url, output_path, "MuAPI") + + +def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): + if not api_key: + raise RuntimeError("MUAPI_API_KEY not set") + if model not in MUAPI_MODELS: + raise RuntimeError( + f"Unsupported MuAPI logo model: {model}. " + f"Choose one of: {', '.join(MUAPI_MODELS)}" + ) + + payload = { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + } + response = _json_request( + f"{MUAPI_API_BASE}/{model}", + api_key, + method="POST", + payload=payload, + api_key_header="x-api-key", + ) + request_id = _muapi_response_value(response, ("request_id", "id")) + if not isinstance(request_id, str) or not request_id: + raise RuntimeError("MuAPI did not return a request ID") + + data = response + for poll_number in range(MUAPI_MAX_POLLS + 1): + status = _muapi_response_value(data, ("status",)) + normalized_status = str(status or "").lower() + if normalized_status in {"completed", "succeeded", "success"}: + _download_muapi_image(_muapi_output_url(data), output_path) + return + if normalized_status in { + "failed", + "error", + "timeout", + "canceled", + "cancelled", + }: + raise RuntimeError(f"MuAPI generation {normalized_status}: {_muapi_error(data)}") + if poll_number == MUAPI_MAX_POLLS: + break + + time.sleep(MUAPI_POLL_INTERVAL) + data = _json_request( + f"{MUAPI_API_BASE}/predictions/{request_id}/result", + api_key, + api_key_header="x-api-key", + ) + + raise RuntimeError("MuAPI prediction timed out while polling") + + def _generate_with_gemini(prompt, output_path, aspect_ratio, use_pro): if not GEMINI_API_KEY: raise RuntimeError("GEMINI_API_KEY not set") @@ -344,8 +466,9 @@ def generate_logo( aspect_ratio=None, provider="gemini", atlas_model=ATLAS_MODEL, + muapi_model=MUAPI_MODEL, ): - """Generate a logo using Gemini or Atlas Cloud image generation. + """Generate a logo using Gemini or MuAPI image generation. Args: aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4" @@ -365,6 +488,8 @@ def generate_logo( if provider == "atlas": model_label = f"Atlas Cloud ({atlas_model})" + elif provider == "muapi": + model_label = f"MuAPI ({muapi_model})" else: model_label = ( "Nano Banana Pro (gemini-3-pro-image-preview)" @@ -386,6 +511,14 @@ def generate_logo( ATLASCLOUD_API_KEY, atlas_model, ) + elif provider == "muapi": + _generate_with_muapi( + full_prompt, + output_path, + ratio, + MUAPI_API_KEY, + muapi_model, + ) else: _generate_with_gemini(full_prompt, output_path, ratio, use_pro) @@ -407,6 +540,7 @@ def generate_batch( aspect_ratio=None, provider="gemini", atlas_model=ATLAS_MODEL, + muapi_model=MUAPI_MODEL, ): """Generate multiple logo variants with different styles""" @@ -430,6 +564,8 @@ def generate_batch( model_label = ( f"Atlas Cloud ({atlas_model})" if provider == "atlas" + else f"MuAPI ({muapi_model})" + if provider == "muapi" else f"Nano Banana {'Pro' if use_pro else 'Flash'}" ) ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO @@ -466,6 +602,7 @@ def generate_batch( aspect_ratio=aspect_ratio, provider=provider, atlas_model=atlas_model, + muapi_model=muapi_model, ) if result: @@ -487,7 +624,7 @@ def generate_batch( def main(): parser = argparse.ArgumentParser( - description="Generate logos using Gemini or Atlas Cloud" + description="Generate logos using Gemini or MuAPI" ) parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt") parser.add_argument("--brand", "-b", type=str, help="Brand name") @@ -514,7 +651,7 @@ def main(): ) parser.add_argument( "--provider", - choices=["gemini", "atlas"], + choices=["gemini", "atlas", "muapi"], default="gemini", help="Image provider (default: gemini)", ) @@ -523,6 +660,12 @@ def main(): default=ATLAS_MODEL, help=f"Atlas Cloud image model (default: {ATLAS_MODEL})", ) + parser.add_argument( + "--muapi-model", + choices=MUAPI_MODELS, + default=MUAPI_MODEL, + help=f"MuAPI image model (default: {MUAPI_MODEL})", + ) parser.add_argument( "--aspect-ratio", "-r", @@ -539,8 +682,11 @@ def main(): args = parser.parse_args() - if args.provider == "atlas" and args.pro: - parser.error("--pro is only available with --provider gemini") + if args.provider != "gemini" and args.pro: + parser.error( + "--pro is only available with --provider gemini; " + "use --muapi-model nano-banana-pro for MuAPI" + ) if args.list_styles: print("Available styles:") @@ -574,6 +720,7 @@ def main(): aspect_ratio=args.aspect_ratio, provider=args.provider, atlas_model=args.atlas_model, + muapi_model=args.muapi_model, ) else: generate_logo( @@ -586,6 +733,7 @@ def main(): aspect_ratio=args.aspect_ratio, provider=args.provider, atlas_model=args.atlas_model, + muapi_model=args.muapi_model, ) diff --git a/.claude/skills/design/scripts/logo/tests/test_generate.py b/.claude/skills/design/scripts/logo/tests/test_generate.py index 8420d90..11faa7d 100644 --- a/.claude/skills/design/scripts/logo/tests/test_generate.py +++ b/.claude/skills/design/scripts/logo/tests/test_generate.py @@ -128,5 +128,120 @@ class AtlasGenerationTests(unittest.TestCase): logo_generate._validate_public_https_url("https://assets.local/logo.png") +class MuapiGenerationTests(unittest.TestCase): + @patch.object(logo_generate, "_download_muapi_image") + @patch.object(logo_generate.time, "sleep") + @patch.object(logo_generate, "_json_request") + def test_muapi_submits_once_and_polls_until_completed( + self, json_request, sleep, download + ): + json_request.side_effect = [ + {"request_id": "req-123"}, + {"request_id": "req-123", "status": "processing"}, + { + "request_id": "req-123", + "status": "completed", + "outputs": ["https://media.example.com/logo.png"], + }, + ] + + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + self.assertEqual(json_request.call_count, 3) + self.assertEqual( + json_request.call_args_list[0], + call( + f"{logo_generate.MUAPI_API_BASE}/nano-banana", + "muapi-key", + method="POST", + payload={"prompt": "logo prompt", "aspect_ratio": "1:1"}, + api_key_header="x-api-key", + ), + ) + self.assertEqual( + json_request.call_args_list[1:], + [ + call( + f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "muapi-key", + api_key_header="x-api-key", + ), + call( + f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "muapi-key", + api_key_header="x-api-key", + ), + ], + ) + self.assertEqual(sleep.call_count, 2) + download.assert_called_once_with( + "https://media.example.com/logo.png", "logo.png" + ) + + @patch.object(logo_generate, "_json_request") + def test_muapi_does_not_retry_generation_post(self, json_request): + json_request.side_effect = RuntimeError("network error") + + with self.assertRaisesRegex(RuntimeError, "network error"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + + def test_muapi_requires_key_and_known_model(self): + with self.assertRaisesRegex(RuntimeError, "MUAPI_API_KEY not set"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", None, "nano-banana" + ) + + with self.assertRaisesRegex(RuntimeError, "Unsupported MuAPI logo model"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "unknown-model" + ) + + @patch.object(logo_generate, "build_opener") + def test_muapi_uses_x_api_key_header(self, build_opener): + class Response: + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + @staticmethod + def read(): + return b"{}" + + build_opener.return_value.open.return_value = Response() + + logo_generate._json_request( + "https://api.muapi.ai/api/v1/nano-banana", + "muapi-key", + method="POST", + payload={"prompt": "logo"}, + api_key_header="x-api-key", + ) + + request = build_opener.return_value.open.call_args.args[0] + headers = {key.lower(): value for key, value in request.header_items()} + self.assertEqual(headers["x-api-key"], "muapi-key") + self.assertNotIn("authorization", headers) + + @patch.object(logo_generate, "_json_request") + def test_muapi_reports_failed_prediction(self, json_request): + json_request.side_effect = [ + {"request_id": "req-123"}, + {"status": "failed", "error": "invalid prompt"}, + ] + + with self.assertRaisesRegex(RuntimeError, "invalid prompt"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + if __name__ == "__main__": unittest.main() diff --git a/cli/assets/skills/design/SKILL.md b/cli/assets/skills/design/SKILL.md index fadccf4..86309e3 100644 --- a/cli/assets/skills/design/SKILL.md +++ b/cli/assets/skills/design/SKILL.md @@ -1,6 +1,6 @@ --- name: design -description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or Atlas Cloud AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." +description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." argument-hint: "[design-type] [context]" license: MIT metadata: @@ -39,7 +39,8 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot ## Logo Design (Built-in) -55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana models. +55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana and +MuAPI image generation. ### Logo: Generate Design Brief @@ -63,6 +64,8 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro ``` **IMPORTANT:** When scripts fail, try to fix them directly. @@ -304,8 +307,17 @@ python3 --version || python --version ```bash export GEMINI_API_KEY="your-key" # https://aistudio.google.com/apikey pip install google-genai pillow + +# Optional MuAPI provider (no extra Python package required) +export MUAPI_API_KEY="your-key" ``` +MuAPI uses the asynchronous model endpoint and prediction result API. See the +[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication +and current model contracts. The logo generator currently supports the +`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared +`prompt` and `aspect_ratio` fields. + > **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`). ## Integration diff --git a/cli/assets/skills/design/references/logo-design.md b/cli/assets/skills/design/references/logo-design.md index 3fae93b..497197f 100644 --- a/cli/assets/skills/design/references/logo-design.md +++ b/cli/assets/skills/design/references/logo-design.md @@ -1,13 +1,13 @@ # Logo Design Reference -AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud is also available as an explicit opt-in. +AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; MuAPI is also available as an explicit opt-in provider. ## Scripts | Script | Purpose | |--------|---------| | `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs | -| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or Atlas Cloud | +| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or MuAPI | | `scripts/logo/core.py` | BM25 search engine for logo data | ## Commands @@ -39,9 +39,11 @@ python3 ~/.claude/skills/design/scripts/logo/search.py "healthcare medical" --do python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --style minimalist --industry tech python3 ~/.claude/skills/design/scripts/logo/generate.py --prompt "coffee shop vintage badge" --style vintage python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider atlas +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi +python3 ~/.claude/skills/design/scripts/logo/generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro ``` -Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model` +Options: `--style`, `--industry`, `--prompt`, `--provider`, `--atlas-model`, `--muapi-model` ## Available Styles @@ -93,4 +95,13 @@ pip install google-genai # Optional Atlas Cloud provider (no extra Python package required) export ATLASCLOUD_API_KEY="your-key" + +# Optional MuAPI provider (no extra Python package required) +export MUAPI_API_KEY="your-key" ``` + +MuAPI uses the asynchronous model endpoint and prediction result API. See the +[MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication +and current model contracts. The logo generator currently supports the +`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared +`prompt` and `aspect_ratio` fields. diff --git a/cli/assets/skills/design/scripts/logo/generate.py b/cli/assets/skills/design/scripts/logo/generate.py index 7de5b70..d402b73 100644 --- a/cli/assets/skills/design/scripts/logo/generate.py +++ b/cli/assets/skills/design/scripts/logo/generate.py @@ -1,8 +1,9 @@ #!/usr/bin/env python3 -"""Logo generation with Gemini or Atlas Cloud. +"""Logo generation with Gemini or MuAPI. -Gemini remains the default provider. Atlas Cloud is opt-in with -``--provider atlas`` and uses its asynchronous image generation API. +Gemini remains the default provider. MuAPI is opt-in with +``--provider muapi`` and uses its asynchronous image generation API with the +selected model's prompt/aspect-ratio contract. Models: - Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency @@ -14,6 +15,8 @@ Usage: python generate.py --brand "TechFlow" --industry tech --style minimalist python generate.py --brand "TechFlow" --pro # Use Nano Banana Pro model python generate.py --brand "TechFlow" --provider atlas + python generate.py --brand "TechFlow" --provider muapi + python generate.py --brand "TechFlow" --provider muapi --muapi-model nano-banana-pro Batch mode (generates multiple variants): python generate.py --brand "Unikorn" --batch 9 --output-dir ./logos --pro @@ -57,6 +60,7 @@ load_env() # ============ CONFIGURATION ============ GEMINI_API_KEY = os.environ.get("GEMINI_API_KEY") ATLASCLOUD_API_KEY = os.environ.get("ATLASCLOUD_API_KEY") +MUAPI_API_KEY = os.environ.get("MUAPI_API_KEY") # Gemini "Nano Banana" model configurations for image generation GEMINI_FLASH = "gemini-2.5-flash-image" # Nano Banana: fast, high-volume, low-latency @@ -65,9 +69,14 @@ GEMINI_PRO = "gemini-3-pro-image-preview" # Nano Banana Pro: professional quali # Atlas Cloud model validated against the live model catalog and schema. ATLAS_MODEL = "google/nano-banana-2-lite/text-to-image" ATLAS_API_BASE = "https://api.atlascloud.ai/api/v1" -HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (Atlas Cloud logo provider)" +MUAPI_MODEL = "nano-banana" +MUAPI_MODELS = ("nano-banana", "nano-banana-pro") +MUAPI_API_BASE = "https://api.muapi.ai/api/v1" +HTTP_USER_AGENT = "ui-ux-pro-max/2.5 (logo generation)" ATLAS_POLL_INTERVAL = 2 ATLAS_MAX_POLLS = 90 +MUAPI_POLL_INTERVAL = 2 +MUAPI_MAX_POLLS = 90 # Supported aspect ratios ASPECT_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4"] @@ -156,13 +165,13 @@ def _validate_public_https_url(url): or parsed.username or parsed.password ): - raise ValueError("Atlas Cloud returned an invalid media URL") + raise ValueError("Provider returned an invalid media URL") hostname = parsed.hostname.lower().rstrip(".") if hostname == "localhost" or hostname.endswith( (".localhost", ".local", ".internal") ): - raise ValueError("Atlas Cloud media URL used a local hostname") + raise ValueError("Provider media URL used a local hostname") try: ip = ipaddress.ip_address(hostname) @@ -170,17 +179,26 @@ def _validate_public_https_url(url): return else: if not ip.is_global: - raise ValueError("Atlas Cloud media URL used a non-public address") + raise ValueError("Provider media URL used a non-public address") -def _json_request(url, api_key, method="GET", payload=None): +def _json_request( + url, api_key, method="GET", payload=None, api_key_header="Authorization" +): + if api_key_header == "Authorization": + auth_value = f"Bearer {api_key}" + elif api_key_header == "x-api-key": + auth_value = api_key + else: + raise ValueError("Unsupported API key header") + body = json.dumps(payload).encode("utf-8") if payload is not None else None request = Request( url, data=body, method=method, headers={ - "Authorization": f"Bearer {api_key}", + api_key_header: auth_value, "Accept": "application/json", "User-Agent": HTTP_USER_AGENT, **({"Content-Type": "application/json"} if body is not None else {}), @@ -192,10 +210,10 @@ def _json_request(url, api_key, method="GET", payload=None): except HTTPError as exc: detail = exc.read().decode("utf-8", errors="replace") raise RuntimeError( - f"Atlas Cloud request failed ({exc.code}): {detail[:300]}" + f"Provider request failed ({exc.code}): {detail[:300]}" ) from exc except (URLError, TimeoutError, json.JSONDecodeError) as exc: - raise RuntimeError(f"Atlas Cloud request failed: {exc}") from exc + raise RuntimeError(f"Provider request failed: {exc}") from exc def _atlas_prediction_data(response): @@ -210,6 +228,10 @@ def _atlas_prediction_data(response): def _download_atlas_image(url, output_path): + _download_image(url, output_path, "image provider") + + +def _download_image(url, output_path, provider_name): _validate_public_https_url(url) request = Request( url, @@ -222,14 +244,14 @@ def _download_atlas_image(url, output_path): content_type = response.headers.get_content_type() if not content_type.startswith("image/"): raise RuntimeError( - f"Atlas Cloud output is not an image ({content_type})" + f"{provider_name} output is not an image ({content_type})" ) image_data = response.read() except (HTTPError, URLError, TimeoutError) as exc: - raise RuntimeError(f"Unable to download Atlas Cloud image: {exc}") from exc + raise RuntimeError(f"Unable to download {provider_name} image: {exc}") from exc if not image_data: - raise RuntimeError("Atlas Cloud returned an empty image") + raise RuntimeError(f"{provider_name} returned an empty image") with open(output_path, "wb") as output_file: output_file.write(image_data) @@ -281,6 +303,106 @@ def _generate_with_atlas(prompt, output_path, aspect_ratio, api_key, model): raise RuntimeError("Atlas Cloud prediction timed out while polling") +def _muapi_response_objects(response): + """Return the response and common MuAPI envelopes without guessing fields.""" + if not isinstance(response, dict): + raise TypeError("MuAPI returned an invalid response") + + objects = [response] + for key in ("data", "output", "result"): + value = response.get(key) + if isinstance(value, dict) and value not in objects: + objects.append(value) + return objects + + +def _muapi_response_value(response, keys): + for item in _muapi_response_objects(response): + for key in keys: + value = item.get(key) + if value not in (None, ""): + return value + return None + + +def _muapi_error(response): + value = _muapi_response_value(response, ("error", "message", "detail")) + if isinstance(value, str): + return value[:300] + return "MuAPI request failed" + + +def _muapi_output_url(response): + for item in _muapi_response_objects(response): + outputs = item.get("outputs") + if isinstance(outputs, list): + for output in outputs: + if isinstance(output, str) and output.startswith("https://"): + return output + if isinstance(output, dict): + for key in ("url", "image_url"): + value = output.get(key) + if isinstance(value, str) and value.startswith("https://"): + return value + raise RuntimeError("MuAPI completed without an HTTPS image URL") + + +def _download_muapi_image(url, output_path): + _download_image(url, output_path, "MuAPI") + + +def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): + if not api_key: + raise RuntimeError("MUAPI_API_KEY not set") + if model not in MUAPI_MODELS: + raise RuntimeError( + f"Unsupported MuAPI logo model: {model}. " + f"Choose one of: {', '.join(MUAPI_MODELS)}" + ) + + payload = { + "prompt": prompt, + "aspect_ratio": aspect_ratio, + } + response = _json_request( + f"{MUAPI_API_BASE}/{model}", + api_key, + method="POST", + payload=payload, + api_key_header="x-api-key", + ) + request_id = _muapi_response_value(response, ("request_id", "id")) + if not isinstance(request_id, str) or not request_id: + raise RuntimeError("MuAPI did not return a request ID") + + data = response + for poll_number in range(MUAPI_MAX_POLLS + 1): + status = _muapi_response_value(data, ("status",)) + normalized_status = str(status or "").lower() + if normalized_status in {"completed", "succeeded", "success"}: + _download_muapi_image(_muapi_output_url(data), output_path) + return + if normalized_status in { + "failed", + "error", + "timeout", + "canceled", + "cancelled", + }: + raise RuntimeError(f"MuAPI generation {normalized_status}: {_muapi_error(data)}") + if poll_number == MUAPI_MAX_POLLS: + break + + time.sleep(MUAPI_POLL_INTERVAL) + data = _json_request( + f"{MUAPI_API_BASE}/predictions/{request_id}/result", + api_key, + api_key_header="x-api-key", + ) + + raise RuntimeError("MuAPI prediction timed out while polling") + + def _generate_with_gemini(prompt, output_path, aspect_ratio, use_pro): if not GEMINI_API_KEY: raise RuntimeError("GEMINI_API_KEY not set") @@ -344,8 +466,9 @@ def generate_logo( aspect_ratio=None, provider="gemini", atlas_model=ATLAS_MODEL, + muapi_model=MUAPI_MODEL, ): - """Generate a logo using Gemini or Atlas Cloud image generation. + """Generate a logo using Gemini or MuAPI image generation. Args: aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4" @@ -365,6 +488,8 @@ def generate_logo( if provider == "atlas": model_label = f"Atlas Cloud ({atlas_model})" + elif provider == "muapi": + model_label = f"MuAPI ({muapi_model})" else: model_label = ( "Nano Banana Pro (gemini-3-pro-image-preview)" @@ -386,6 +511,14 @@ def generate_logo( ATLASCLOUD_API_KEY, atlas_model, ) + elif provider == "muapi": + _generate_with_muapi( + full_prompt, + output_path, + ratio, + MUAPI_API_KEY, + muapi_model, + ) else: _generate_with_gemini(full_prompt, output_path, ratio, use_pro) @@ -407,6 +540,7 @@ def generate_batch( aspect_ratio=None, provider="gemini", atlas_model=ATLAS_MODEL, + muapi_model=MUAPI_MODEL, ): """Generate multiple logo variants with different styles""" @@ -430,6 +564,8 @@ def generate_batch( model_label = ( f"Atlas Cloud ({atlas_model})" if provider == "atlas" + else f"MuAPI ({muapi_model})" + if provider == "muapi" else f"Nano Banana {'Pro' if use_pro else 'Flash'}" ) ratio = aspect_ratio if aspect_ratio in ASPECT_RATIOS else DEFAULT_ASPECT_RATIO @@ -466,6 +602,7 @@ def generate_batch( aspect_ratio=aspect_ratio, provider=provider, atlas_model=atlas_model, + muapi_model=muapi_model, ) if result: @@ -487,7 +624,7 @@ def generate_batch( def main(): parser = argparse.ArgumentParser( - description="Generate logos using Gemini or Atlas Cloud" + description="Generate logos using Gemini or MuAPI" ) parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt") parser.add_argument("--brand", "-b", type=str, help="Brand name") @@ -514,7 +651,7 @@ def main(): ) parser.add_argument( "--provider", - choices=["gemini", "atlas"], + choices=["gemini", "atlas", "muapi"], default="gemini", help="Image provider (default: gemini)", ) @@ -523,6 +660,12 @@ def main(): default=ATLAS_MODEL, help=f"Atlas Cloud image model (default: {ATLAS_MODEL})", ) + parser.add_argument( + "--muapi-model", + choices=MUAPI_MODELS, + default=MUAPI_MODEL, + help=f"MuAPI image model (default: {MUAPI_MODEL})", + ) parser.add_argument( "--aspect-ratio", "-r", @@ -539,8 +682,11 @@ def main(): args = parser.parse_args() - if args.provider == "atlas" and args.pro: - parser.error("--pro is only available with --provider gemini") + if args.provider != "gemini" and args.pro: + parser.error( + "--pro is only available with --provider gemini; " + "use --muapi-model nano-banana-pro for MuAPI" + ) if args.list_styles: print("Available styles:") @@ -574,6 +720,7 @@ def main(): aspect_ratio=args.aspect_ratio, provider=args.provider, atlas_model=args.atlas_model, + muapi_model=args.muapi_model, ) else: generate_logo( @@ -586,6 +733,7 @@ def main(): aspect_ratio=args.aspect_ratio, provider=args.provider, atlas_model=args.atlas_model, + muapi_model=args.muapi_model, ) diff --git a/cli/assets/skills/design/scripts/logo/tests/test_generate.py b/cli/assets/skills/design/scripts/logo/tests/test_generate.py index 8420d90..11faa7d 100644 --- a/cli/assets/skills/design/scripts/logo/tests/test_generate.py +++ b/cli/assets/skills/design/scripts/logo/tests/test_generate.py @@ -128,5 +128,120 @@ class AtlasGenerationTests(unittest.TestCase): logo_generate._validate_public_https_url("https://assets.local/logo.png") +class MuapiGenerationTests(unittest.TestCase): + @patch.object(logo_generate, "_download_muapi_image") + @patch.object(logo_generate.time, "sleep") + @patch.object(logo_generate, "_json_request") + def test_muapi_submits_once_and_polls_until_completed( + self, json_request, sleep, download + ): + json_request.side_effect = [ + {"request_id": "req-123"}, + {"request_id": "req-123", "status": "processing"}, + { + "request_id": "req-123", + "status": "completed", + "outputs": ["https://media.example.com/logo.png"], + }, + ] + + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + self.assertEqual(json_request.call_count, 3) + self.assertEqual( + json_request.call_args_list[0], + call( + f"{logo_generate.MUAPI_API_BASE}/nano-banana", + "muapi-key", + method="POST", + payload={"prompt": "logo prompt", "aspect_ratio": "1:1"}, + api_key_header="x-api-key", + ), + ) + self.assertEqual( + json_request.call_args_list[1:], + [ + call( + f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "muapi-key", + api_key_header="x-api-key", + ), + call( + f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "muapi-key", + api_key_header="x-api-key", + ), + ], + ) + self.assertEqual(sleep.call_count, 2) + download.assert_called_once_with( + "https://media.example.com/logo.png", "logo.png" + ) + + @patch.object(logo_generate, "_json_request") + def test_muapi_does_not_retry_generation_post(self, json_request): + json_request.side_effect = RuntimeError("network error") + + with self.assertRaisesRegex(RuntimeError, "network error"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + + def test_muapi_requires_key_and_known_model(self): + with self.assertRaisesRegex(RuntimeError, "MUAPI_API_KEY not set"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", None, "nano-banana" + ) + + with self.assertRaisesRegex(RuntimeError, "Unsupported MuAPI logo model"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "unknown-model" + ) + + @patch.object(logo_generate, "build_opener") + def test_muapi_uses_x_api_key_header(self, build_opener): + class Response: + def __enter__(self): + return self + + def __exit__(self, *args): + return None + + @staticmethod + def read(): + return b"{}" + + build_opener.return_value.open.return_value = Response() + + logo_generate._json_request( + "https://api.muapi.ai/api/v1/nano-banana", + "muapi-key", + method="POST", + payload={"prompt": "logo"}, + api_key_header="x-api-key", + ) + + request = build_opener.return_value.open.call_args.args[0] + headers = {key.lower(): value for key, value in request.header_items()} + self.assertEqual(headers["x-api-key"], "muapi-key") + self.assertNotIn("authorization", headers) + + @patch.object(logo_generate, "_json_request") + def test_muapi_reports_failed_prediction(self, json_request): + json_request.side_effect = [ + {"request_id": "req-123"}, + {"status": "failed", "error": "invalid prompt"}, + ] + + with self.assertRaisesRegex(RuntimeError, "invalid prompt"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + if __name__ == "__main__": unittest.main() From 0720614e6e4e2c190f68ff4ab0a71faa0fa70b4a Mon Sep 17 00:00:00 2001 From: Anil-matcha Date: Sat, 29 Aug 2026 11:46:39 +0530 Subject: [PATCH 2/4] fix(design): preserve existing logo provider docs --- .claude/skills/design/SKILL.md | 6 +++--- .claude/skills/design/references/logo-design.md | 4 ++-- .claude/skills/design/scripts/logo/generate.py | 14 ++++++++------ cli/assets/skills/design/SKILL.md | 6 +++--- cli/assets/skills/design/references/logo-design.md | 4 ++-- cli/assets/skills/design/scripts/logo/generate.py | 14 ++++++++------ 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.claude/skills/design/SKILL.md b/.claude/skills/design/SKILL.md index 86309e3..96d51a3 100644 --- a/.claude/skills/design/SKILL.md +++ b/.claude/skills/design/SKILL.md @@ -1,6 +1,6 @@ --- name: design -description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." +description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." argument-hint: "[design-type] [context]" license: MIT metadata: @@ -39,8 +39,8 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot ## Logo Design (Built-in) -55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana and -MuAPI image generation. +55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana, Atlas +Cloud, and MuAPI image generation. ### Logo: Generate Design Brief diff --git a/.claude/skills/design/references/logo-design.md b/.claude/skills/design/references/logo-design.md index 497197f..c662402 100644 --- a/.claude/skills/design/references/logo-design.md +++ b/.claude/skills/design/references/logo-design.md @@ -1,13 +1,13 @@ # Logo Design Reference -AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; MuAPI is also available as an explicit opt-in provider. +AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud and MuAPI are also available as explicit opt-in providers. ## Scripts | Script | Purpose | |--------|---------| | `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs | -| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or MuAPI | +| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana, Atlas Cloud, or MuAPI | | `scripts/logo/core.py` | BM25 search engine for logo data | ## Commands diff --git a/.claude/skills/design/scripts/logo/generate.py b/.claude/skills/design/scripts/logo/generate.py index d402b73..4987598 100644 --- a/.claude/skills/design/scripts/logo/generate.py +++ b/.claude/skills/design/scripts/logo/generate.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 -"""Logo generation with Gemini or MuAPI. +"""Logo generation with Gemini, Atlas Cloud, or MuAPI. -Gemini remains the default provider. MuAPI is opt-in with -``--provider muapi`` and uses its asynchronous image generation API with the -selected model's prompt/aspect-ratio contract. +Gemini remains the default provider. Atlas Cloud is opt-in with +``--provider atlas`` and uses its asynchronous image generation API. MuAPI is +opt-in with ``--provider muapi`` and uses its asynchronous image generation API +with the selected model's prompt/aspect-ratio contract. Models: - Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency - Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning +- MuAPI Nano Banana (--provider muapi): nano-banana - hosted asynchronous image generation Usage: python generate.py --prompt "tech startup logo minimalist blue" @@ -468,7 +470,7 @@ def generate_logo( atlas_model=ATLAS_MODEL, muapi_model=MUAPI_MODEL, ): - """Generate a logo using Gemini or MuAPI image generation. + """Generate a logo using Gemini, Atlas Cloud, or MuAPI image generation. Args: aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4" @@ -624,7 +626,7 @@ def generate_batch( def main(): parser = argparse.ArgumentParser( - description="Generate logos using Gemini or MuAPI" + description="Generate logos using Gemini, Atlas Cloud, or MuAPI" ) parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt") parser.add_argument("--brand", "-b", type=str, help="Brand name") diff --git a/cli/assets/skills/design/SKILL.md b/cli/assets/skills/design/SKILL.md index 86309e3..96d51a3 100644 --- a/cli/assets/skills/design/SKILL.md +++ b/cli/assets/skills/design/SKILL.md @@ -1,6 +1,6 @@ --- name: design -description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." +description: "Comprehensive design skill: brand identity, design tokens, UI styling, logo generation (55 styles, Gemini, Atlas Cloud, or MuAPI AI), corporate identity program (50 deliverables, CIP mockups), HTML presentations (Chart.js), banner design (22 styles, social/ads/web/print), icon design (15 styles, SVG, Gemini 3.1 Pro), social photos (HTML→screenshot, multi-platform). Actions: design logo, create CIP, generate mockups, build slides, design banner, generate icon, create social photos, social media images, brand identity, design system. Platforms: Facebook, Twitter, LinkedIn, YouTube, Instagram, Pinterest, TikTok, Threads, Google Ads." argument-hint: "[design-type] [context]" license: MIT metadata: @@ -39,8 +39,8 @@ Unified design skill: brand, tokens, UI, logo, CIP, slides, banners, social phot ## Logo Design (Built-in) -55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana and -MuAPI image generation. +55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana, Atlas +Cloud, and MuAPI image generation. ### Logo: Generate Design Brief diff --git a/cli/assets/skills/design/references/logo-design.md b/cli/assets/skills/design/references/logo-design.md index 497197f..c662402 100644 --- a/cli/assets/skills/design/references/logo-design.md +++ b/cli/assets/skills/design/references/logo-design.md @@ -1,13 +1,13 @@ # Logo Design Reference -AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; MuAPI is also available as an explicit opt-in provider. +AI-powered logo design with 55+ styles, 30 color palettes, 25 industry guides. Gemini Nano Banana is the default provider; Atlas Cloud and MuAPI are also available as explicit opt-in providers. ## Scripts | Script | Purpose | |--------|---------| | `scripts/logo/search.py` | Search styles, colors, industries; generate design briefs | -| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana or MuAPI | +| `scripts/logo/generate.py` | Generate logos with Gemini Nano Banana, Atlas Cloud, or MuAPI | | `scripts/logo/core.py` | BM25 search engine for logo data | ## Commands diff --git a/cli/assets/skills/design/scripts/logo/generate.py b/cli/assets/skills/design/scripts/logo/generate.py index d402b73..4987598 100644 --- a/cli/assets/skills/design/scripts/logo/generate.py +++ b/cli/assets/skills/design/scripts/logo/generate.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 -"""Logo generation with Gemini or MuAPI. +"""Logo generation with Gemini, Atlas Cloud, or MuAPI. -Gemini remains the default provider. MuAPI is opt-in with -``--provider muapi`` and uses its asynchronous image generation API with the -selected model's prompt/aspect-ratio contract. +Gemini remains the default provider. Atlas Cloud is opt-in with +``--provider atlas`` and uses its asynchronous image generation API. MuAPI is +opt-in with ``--provider muapi`` and uses its asynchronous image generation API +with the selected model's prompt/aspect-ratio contract. Models: - Nano Banana (default): gemini-2.5-flash-image - fast, high-volume, low-latency - Nano Banana Pro (--pro): gemini-3-pro-image-preview - professional quality, advanced reasoning +- MuAPI Nano Banana (--provider muapi): nano-banana - hosted asynchronous image generation Usage: python generate.py --prompt "tech startup logo minimalist blue" @@ -468,7 +470,7 @@ def generate_logo( atlas_model=ATLAS_MODEL, muapi_model=MUAPI_MODEL, ): - """Generate a logo using Gemini or MuAPI image generation. + """Generate a logo using Gemini, Atlas Cloud, or MuAPI image generation. Args: aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4" @@ -624,7 +626,7 @@ def generate_batch( def main(): parser = argparse.ArgumentParser( - description="Generate logos using Gemini or MuAPI" + description="Generate logos using Gemini, Atlas Cloud, or MuAPI" ) parser.add_argument("--prompt", "-p", type=str, help="Logo description prompt") parser.add_argument("--brand", "-b", type=str, help="Brand name") From e7e873dada930f362944850075d6a89b881e2a91 Mon Sep 17 00:00:00 2001 From: Anil-matcha Date: Sun, 30 Aug 2026 07:53:09 +0530 Subject: [PATCH 3/4] fix(design): document MuAPI model contracts --- .claude/skills/design/SKILL.md | 9 ++++++--- .claude/skills/design/references/logo-design.md | 9 ++++++--- .../skills/design/scripts/logo/tests/test_generate.py | 8 ++++---- cli/assets/skills/design/SKILL.md | 9 ++++++--- cli/assets/skills/design/references/logo-design.md | 9 ++++++--- .../skills/design/scripts/logo/tests/test_generate.py | 8 ++++---- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/.claude/skills/design/SKILL.md b/.claude/skills/design/SKILL.md index 96d51a3..31de559 100644 --- a/.claude/skills/design/SKILL.md +++ b/.claude/skills/design/SKILL.md @@ -314,9 +314,12 @@ export MUAPI_API_KEY="your-key" MuAPI uses the asynchronous model endpoint and prediction result API. See the [MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication -and current model contracts. The logo generator currently supports the -`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared -`prompt` and `aspect_ratio` fields. +and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana) +or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro) +for the current model-specific schemas. The logo generator supports both documented +model slugs and sends their shared required `prompt` plus optional `aspect_ratio` +fields; the Pro model also accepts an optional `resolution` field that this focused +logo workflow leaves at the provider default. > **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`). diff --git a/.claude/skills/design/references/logo-design.md b/.claude/skills/design/references/logo-design.md index c662402..a240e96 100644 --- a/.claude/skills/design/references/logo-design.md +++ b/.claude/skills/design/references/logo-design.md @@ -102,6 +102,9 @@ export MUAPI_API_KEY="your-key" MuAPI uses the asynchronous model endpoint and prediction result API. See the [MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication -and current model contracts. The logo generator currently supports the -`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared -`prompt` and `aspect_ratio` fields. +and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana) +or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro) +for the current model-specific schemas. The logo generator supports both documented +model slugs and sends their shared required `prompt` plus optional `aspect_ratio` +fields; the Pro model also accepts an optional `resolution` field that this focused +logo workflow leaves at the provider default. diff --git a/.claude/skills/design/scripts/logo/tests/test_generate.py b/.claude/skills/design/scripts/logo/tests/test_generate.py index 11faa7d..d1adb98 100644 --- a/.claude/skills/design/scripts/logo/tests/test_generate.py +++ b/.claude/skills/design/scripts/logo/tests/test_generate.py @@ -136,12 +136,12 @@ class MuapiGenerationTests(unittest.TestCase): self, json_request, sleep, download ): json_request.side_effect = [ - {"request_id": "req-123"}, - {"request_id": "req-123", "status": "processing"}, + {"id": "req-123", "status": "created"}, + {"id": "req-123", "status": "processing"}, { - "request_id": "req-123", + "id": "req-123", "status": "completed", - "outputs": ["https://media.example.com/logo.png"], + "output": {"outputs": ["https://media.example.com/logo.png"]}, }, ] diff --git a/cli/assets/skills/design/SKILL.md b/cli/assets/skills/design/SKILL.md index 96d51a3..31de559 100644 --- a/cli/assets/skills/design/SKILL.md +++ b/cli/assets/skills/design/SKILL.md @@ -314,9 +314,12 @@ export MUAPI_API_KEY="your-key" MuAPI uses the asynchronous model endpoint and prediction result API. See the [MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication -and current model contracts. The logo generator currently supports the -`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared -`prompt` and `aspect_ratio` fields. +and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana) +or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro) +for the current model-specific schemas. The logo generator supports both documented +model slugs and sends their shared required `prompt` plus optional `aspect_ratio` +fields; the Pro model also accepts an optional `resolution` field that this focused +logo workflow leaves at the provider default. > **Note for Windows:** Use `python` instead of `pip` where needed (e.g., `python -m pip install ...`). diff --git a/cli/assets/skills/design/references/logo-design.md b/cli/assets/skills/design/references/logo-design.md index c662402..a240e96 100644 --- a/cli/assets/skills/design/references/logo-design.md +++ b/cli/assets/skills/design/references/logo-design.md @@ -102,6 +102,9 @@ export MUAPI_API_KEY="your-key" MuAPI uses the asynchronous model endpoint and prediction result API. See the [MuAPI API reference](https://muapi.ai/docs/api-reference) for authentication -and current model contracts. The logo generator currently supports the -`nano-banana` and `nano-banana-pro` model slugs, and sends only the shared -`prompt` and `aspect_ratio` fields. +and the [nano-banana model contract](https://api.muapi.ai/api/v1/models/nano-banana) +or [nano-banana-pro model contract](https://api.muapi.ai/api/v1/models/nano-banana-pro) +for the current model-specific schemas. The logo generator supports both documented +model slugs and sends their shared required `prompt` plus optional `aspect_ratio` +fields; the Pro model also accepts an optional `resolution` field that this focused +logo workflow leaves at the provider default. diff --git a/cli/assets/skills/design/scripts/logo/tests/test_generate.py b/cli/assets/skills/design/scripts/logo/tests/test_generate.py index 11faa7d..d1adb98 100644 --- a/cli/assets/skills/design/scripts/logo/tests/test_generate.py +++ b/cli/assets/skills/design/scripts/logo/tests/test_generate.py @@ -136,12 +136,12 @@ class MuapiGenerationTests(unittest.TestCase): self, json_request, sleep, download ): json_request.side_effect = [ - {"request_id": "req-123"}, - {"request_id": "req-123", "status": "processing"}, + {"id": "req-123", "status": "created"}, + {"id": "req-123", "status": "processing"}, { - "request_id": "req-123", + "id": "req-123", "status": "completed", - "outputs": ["https://media.example.com/logo.png"], + "output": {"outputs": ["https://media.example.com/logo.png"]}, }, ] From 26f79bdfb62c486bd960b79c9b8cd341fa8867c4 Mon Sep 17 00:00:00 2001 From: Anil-matcha Date: Tue, 1 Sep 2026 15:13:29 +0530 Subject: [PATCH 4/4] fix(design): use MuAPI result URL from creation response --- .../skills/design/scripts/logo/generate.py | 28 ++++++++++- .../scripts/logo/tests/test_generate.py | 49 +++++++++++++++++-- .../skills/design/scripts/logo/generate.py | 28 ++++++++++- .../scripts/logo/tests/test_generate.py | 49 +++++++++++++++++-- 4 files changed, 144 insertions(+), 10 deletions(-) diff --git a/.claude/skills/design/scripts/logo/generate.py b/.claude/skills/design/scripts/logo/generate.py index 4987598..d006c13 100644 --- a/.claude/skills/design/scripts/logo/generate.py +++ b/.claude/skills/design/scripts/logo/generate.py @@ -334,6 +334,31 @@ def _muapi_error(response): return "MuAPI request failed" +def _muapi_result_url(response): + """Return the documented result URL from the creation response.""" + for item in _muapi_response_objects(response): + urls = item.get("urls") + if not isinstance(urls, dict) or "get" not in urls: + continue + + result_url = urls.get("get") + if not isinstance(result_url, str) or not result_url: + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) + try: + _validate_public_https_url(result_url) + except ValueError as exc: + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) from exc + return result_url + + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) + + def _muapi_output_url(response): for item in _muapi_response_objects(response): outputs = item.get("outputs") @@ -376,6 +401,7 @@ def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): request_id = _muapi_response_value(response, ("request_id", "id")) if not isinstance(request_id, str) or not request_id: raise RuntimeError("MuAPI did not return a request ID") + result_url = _muapi_result_url(response) data = response for poll_number in range(MUAPI_MAX_POLLS + 1): @@ -397,7 +423,7 @@ def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): time.sleep(MUAPI_POLL_INTERVAL) data = _json_request( - f"{MUAPI_API_BASE}/predictions/{request_id}/result", + result_url, api_key, api_key_header="x-api-key", ) diff --git a/.claude/skills/design/scripts/logo/tests/test_generate.py b/.claude/skills/design/scripts/logo/tests/test_generate.py index d1adb98..9c2addc 100644 --- a/.claude/skills/design/scripts/logo/tests/test_generate.py +++ b/.claude/skills/design/scripts/logo/tests/test_generate.py @@ -136,7 +136,15 @@ class MuapiGenerationTests(unittest.TestCase): self, json_request, sleep, download ): json_request.side_effect = [ - {"id": "req-123", "status": "created"}, + { + "id": "req-123", + "status": "created", + "output": { + "urls": { + "get": "https://api.muapi.ai/api/v1/results/req-123" + } + }, + }, {"id": "req-123", "status": "processing"}, { "id": "req-123", @@ -164,12 +172,12 @@ class MuapiGenerationTests(unittest.TestCase): json_request.call_args_list[1:], [ call( - f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "https://api.muapi.ai/api/v1/results/req-123", "muapi-key", api_key_header="x-api-key", ), call( - f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "https://api.muapi.ai/api/v1/results/req-123", "muapi-key", api_key_header="x-api-key", ), @@ -233,7 +241,14 @@ class MuapiGenerationTests(unittest.TestCase): @patch.object(logo_generate, "_json_request") def test_muapi_reports_failed_prediction(self, json_request): json_request.side_effect = [ - {"request_id": "req-123"}, + { + "request_id": "req-123", + "output": { + "urls": { + "get": "https://api.muapi.ai/api/v1/results/req-123" + } + }, + }, {"status": "failed", "error": "invalid prompt"}, ] @@ -242,6 +257,32 @@ class MuapiGenerationTests(unittest.TestCase): "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" ) + @patch.object(logo_generate, "_json_request") + def test_muapi_requires_creation_result_url(self, json_request): + json_request.return_value = {"request_id": "req-123", "status": "created"} + + with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + + @patch.object(logo_generate, "_json_request") + def test_muapi_rejects_invalid_creation_result_url(self, json_request): + json_request.return_value = { + "request_id": "req-123", + "status": "created", + "output": {"urls": {"get": "http://api.muapi.ai/results/req-123"}}, + } + + with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + if __name__ == "__main__": unittest.main() diff --git a/cli/assets/skills/design/scripts/logo/generate.py b/cli/assets/skills/design/scripts/logo/generate.py index 4987598..d006c13 100644 --- a/cli/assets/skills/design/scripts/logo/generate.py +++ b/cli/assets/skills/design/scripts/logo/generate.py @@ -334,6 +334,31 @@ def _muapi_error(response): return "MuAPI request failed" +def _muapi_result_url(response): + """Return the documented result URL from the creation response.""" + for item in _muapi_response_objects(response): + urls = item.get("urls") + if not isinstance(urls, dict) or "get" not in urls: + continue + + result_url = urls.get("get") + if not isinstance(result_url, str) or not result_url: + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) + try: + _validate_public_https_url(result_url) + except ValueError as exc: + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) from exc + return result_url + + raise RuntimeError( + "MuAPI creation response did not include a valid HTTPS result URL" + ) + + def _muapi_output_url(response): for item in _muapi_response_objects(response): outputs = item.get("outputs") @@ -376,6 +401,7 @@ def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): request_id = _muapi_response_value(response, ("request_id", "id")) if not isinstance(request_id, str) or not request_id: raise RuntimeError("MuAPI did not return a request ID") + result_url = _muapi_result_url(response) data = response for poll_number in range(MUAPI_MAX_POLLS + 1): @@ -397,7 +423,7 @@ def _generate_with_muapi(prompt, output_path, aspect_ratio, api_key, model): time.sleep(MUAPI_POLL_INTERVAL) data = _json_request( - f"{MUAPI_API_BASE}/predictions/{request_id}/result", + result_url, api_key, api_key_header="x-api-key", ) diff --git a/cli/assets/skills/design/scripts/logo/tests/test_generate.py b/cli/assets/skills/design/scripts/logo/tests/test_generate.py index d1adb98..9c2addc 100644 --- a/cli/assets/skills/design/scripts/logo/tests/test_generate.py +++ b/cli/assets/skills/design/scripts/logo/tests/test_generate.py @@ -136,7 +136,15 @@ class MuapiGenerationTests(unittest.TestCase): self, json_request, sleep, download ): json_request.side_effect = [ - {"id": "req-123", "status": "created"}, + { + "id": "req-123", + "status": "created", + "output": { + "urls": { + "get": "https://api.muapi.ai/api/v1/results/req-123" + } + }, + }, {"id": "req-123", "status": "processing"}, { "id": "req-123", @@ -164,12 +172,12 @@ class MuapiGenerationTests(unittest.TestCase): json_request.call_args_list[1:], [ call( - f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "https://api.muapi.ai/api/v1/results/req-123", "muapi-key", api_key_header="x-api-key", ), call( - f"{logo_generate.MUAPI_API_BASE}/predictions/req-123/result", + "https://api.muapi.ai/api/v1/results/req-123", "muapi-key", api_key_header="x-api-key", ), @@ -233,7 +241,14 @@ class MuapiGenerationTests(unittest.TestCase): @patch.object(logo_generate, "_json_request") def test_muapi_reports_failed_prediction(self, json_request): json_request.side_effect = [ - {"request_id": "req-123"}, + { + "request_id": "req-123", + "output": { + "urls": { + "get": "https://api.muapi.ai/api/v1/results/req-123" + } + }, + }, {"status": "failed", "error": "invalid prompt"}, ] @@ -242,6 +257,32 @@ class MuapiGenerationTests(unittest.TestCase): "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" ) + @patch.object(logo_generate, "_json_request") + def test_muapi_requires_creation_result_url(self, json_request): + json_request.return_value = {"request_id": "req-123", "status": "created"} + + with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + + @patch.object(logo_generate, "_json_request") + def test_muapi_rejects_invalid_creation_result_url(self, json_request): + json_request.return_value = { + "request_id": "req-123", + "status": "created", + "output": {"urls": {"get": "http://api.muapi.ai/results/req-123"}}, + } + + with self.assertRaisesRegex(RuntimeError, "valid HTTPS result URL"): + logo_generate._generate_with_muapi( + "logo prompt", "logo.png", "1:1", "muapi-key", "nano-banana" + ) + + json_request.assert_called_once() + if __name__ == "__main__": unittest.main()