feat(design): add opt-in MuAPI logo provider

This commit is contained in:
Anil-matcha 2026-08-29 11:16:47 +05:30
parent 8bd29e7754
commit dfe8765f10
8 changed files with 620 additions and 48 deletions

View File

@ -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

View File

@ -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.

View File

@ -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,
)

View File

@ -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()

View File

@ -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

View File

@ -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.

View File

@ -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,
)

View File

@ -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()