📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-09-03 08:57:39 +00:00
parent 0b2b056032
commit 3294e63c9f
98 changed files with 3411 additions and 467 deletions
@@ -427,7 +427,10 @@ Image Editing Mode:
action = check_logo_required(args.brand, skip_prompt=args.no_logo_prompt)
if action == 'generate':
print("\n💡 To generate a logo, use the logo-design skill:")
print(f" python ~/.claude/skills/design/scripts/logo/generate.py --brand \"{args.brand}\" --industry \"{args.industry}\"")
# Resolved from this file so the hint is correct from any cwd and in
# every install layout (plugin cache, project or --global install).
logo_script = Path(__file__).resolve().parents[1] / "logo" / "generate.py"
print(f" python \"{logo_script}\" --brand \"{args.brand}\" --industry \"{args.industry}\"")
print("\n Then re-run this command with --logo <generated_logo.png>")
sys.exit(0)
elif action == 'exit':
@@ -1,12 +1,15 @@
#!/usr/bin/env python3
"""Logo generation with Gemini or Atlas Cloud.
"""Logo generation with Gemini, Atlas Cloud, or MuAPI.
Gemini remains the default provider. Atlas Cloud is opt-in with
``--provider atlas`` and uses its asynchronous image generation API.
``--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"
@@ -14,6 +17,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 +62,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 +71,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 +167,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 +181,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 +212,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 +230,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 +246,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 +305,132 @@ 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_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")
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")
result_url = _muapi_result_url(response)
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(
result_url,
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 +494,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, Atlas Cloud, or MuAPI image generation.
Args:
aspect_ratio: Image aspect ratio. Options: "1:1", "16:9", "9:16", "4:3", "3:4"
@@ -365,6 +516,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 +539,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 +568,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 +592,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 +630,7 @@ def generate_batch(
aspect_ratio=aspect_ratio,
provider=provider,
atlas_model=atlas_model,
muapi_model=muapi_model,
)
if result:
@@ -487,7 +652,7 @@ def generate_batch(
def main():
parser = argparse.ArgumentParser(
description="Generate logos using Gemini or Atlas Cloud"
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")
@@ -514,7 +679,7 @@ def main():
)
parser.add_argument(
"--provider",
choices=["gemini", "atlas"],
choices=["gemini", "atlas", "muapi"],
default="gemini",
help="Image provider (default: gemini)",
)
@@ -523,6 +688,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 +710,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 +748,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 +761,7 @@ def main():
aspect_ratio=args.aspect_ratio,
provider=args.provider,
atlas_model=args.atlas_model,
muapi_model=args.muapi_model,
)
@@ -128,5 +128,161 @@ 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 = [
{
"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",
"status": "completed",
"output": {"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(
"https://api.muapi.ai/api/v1/results/req-123",
"muapi-key",
api_key_header="x-api-key",
),
call(
"https://api.muapi.ai/api/v1/results/req-123",
"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",
"output": {
"urls": {
"get": "https://api.muapi.ai/api/v1/results/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"
)
@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()