mirror of
https://github.com/nextlevelbuilder/ui-ux-pro-max-skill.git
synced 2026-09-07 20:48:42 +00:00
feat(design): add opt-in MuAPI logo provider
This commit is contained in:
parent
8bd29e7754
commit
dfe8765f10
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user