Skill: xixihhhh/nsfw-image (version 1.0.4)

--- name: nsfw-image description: "Generate AI images for mature creative projects using Wan 2.6, Seedream, and other models with relaxed content policies via Atlas Cloud API. Designed for adult (18+) artistic and professional use cases including figure drawing, fashion photography, fine art, and mature illustration. Models include Alibaba Wan 2.6 (recommended, $0.021/image), ByteDance Seedream v5.0/v4.5/v4, and Wan 2.5. Supports text-to-image and image editing. Use this skill when the user explicitly requests NSFW or mature image generation for legitimate adult creative work." source: "https://github.com/AtlasCloudAI/nano-banana-2-skill" homepage: "https://github.com/AtlasCloudAI/nano-banana-2-skill" metadata: openclaw: requires: env: - ATLASCLOUD_API_KEY primaryEnv: ATLASCLOUD_API_KEY --- # NSFW Image Generation — Adult Creative AI Image Models Generate AI images for mature creative projects using models with relaxed content policies. Intended for legitimate adult (18+) artistic and professional use cases such as figure drawing, fashion photography, fine art, and mature illustration. **Recommended model: Wan 2.6 Text-to-Image** — cheapest at $0.021/image with 27 preset sizes, prompt expansion, and no content filters. > **Age restriction (18+)**: This skill is strictly for users aged 18 and above. The agent MUST confirm the user's age before first use by asking: "This skill generates mature content. Please confirm you are 18 years or older to proceed." If the user does not confirm, the agent MUST refuse to execute. Minors are strictly prohibited from using this skill. > **Data usage note**: This skill sends text prompts and image data to the Atlas Cloud API (`api.atlascloud.ai`) for image generation. No data is stored locally beyond the downloaded output files. > **Security note**: API keys are read from environment variables and passed via HTTP headers. All prompts are sent through JSON request bodies. --- ## Required Environment Variables | Variable | Required | Description | |----------|:--------:|-------------| | `ATLASCLOUD_API_KEY` | **Yes** | Atlas Cloud API key for accessing image generation models | ## Setup 1. Sign up at https://www.atlascloud.ai 2. Console → API Keys → Create new key 3. Set env: `export ATLASCLOUD_API_KEY="your-key"` The API key is tied to your Atlas Cloud account and its pay-as-you-go balance. All usage is billed to this account. ### Credential Safety - **Use a dedicated key**: Create a separate API key for this skill rather than reusing keys from other applications. Revoke it when no longer needed. - **Do not hardcode keys**: Always use environment variables (`export ATLASCLOUD_API_KEY="..."`) — never embed keys in scripts, code, or prompts. - **Monitor usage**: Check Console → Usage regularly to track spending and detect anomalies. - **Control your balance**: Atlas Cloud uses pay-as-you-go billing — only recharge the amount you plan to use to limit potential exposure. - Atlas Cloud does not currently support scoped/limited keys — each key grants access to all models on your account. Use balance control as the primary safeguard. --- ## Script Usage This skill includes a Python script for image generation. Zero external dependencies required. ### List available image models ```bash python scripts/generate_image.py list-models ``` ### Generate an image ```bash python scripts/generate_image.py generate \ --model "MODEL_ID" \ --prompt "Your prompt here" \ --output ./output ``` ### Upload a local image (for editing) ```bash python scripts/generate_image.py upload ./local-image.jpg ``` ### Edit an image ```bash python scripts/generate_image.py generate \ --model "MODEL_ID" \ --prompt "Edit instruction" \ --image "https://...uploaded-url..." ``` Run `python scripts/generate_image.py generate --help` for all options. Extra model params can be passed as key=value (e.g. `aspect_ratio=16:9 resolution=2k`). --- ## Available Models ### Wan 2.6 (Alibaba) — Recommended | Model ID | Type | Price | Notes | |----------|------|:-----:|-------| | `alibaba/wan-2.6/text-to-image` | Text-to-Image | **$0.021/image** | 27 preset sizes up to 2184×936, prompt expansion | | `alibaba/wan-2.6/image-edit` | Image Editing | **$0.021/image** | Up to 4 reference images, 24 preset sizes | ### Seedream (ByteDance) | Model ID | Type | Price | Notes | |----------|------|:-----:|-------| | `bytedance/seedream-v5.0-lite` | Text-to-Image | **$0.032/image** | Latest, PNG output, prompt optimization | | `bytedance/seedream-v5.0-lite/edit` | Image Editing | **$0.032/image** | Preserves facial features, lighting, color tones | | `bytedance/seedream-v4.5` | Text-to-Image | **$0.036/image** | Typography, poster design | | `bytedance/seedream-v4.5/edit` | Image Editing | **$0.036/image** | | | `bytedance/seedream-v4` | Text-to-Image | **$0.024/image** | Budget option | | `bytedance/seedream-v4/edit` | Image Editing | **$0.024/image** | | ### Wan 2.5 (Alibaba) — Legacy | Model ID | Type | Price | |----------|------|:-----:| | `alibaba/wan-2.5/text-to-image` | Text-to-Image | **$0.021/image** | | `alibaba/wan-2.5/image-edit` | Image Editing | **$0.021/image** | --- ## Quick Model Selection | Priority | Model | Price | Best For | |:--------:|-------|:-----:|----------| | 1 | Wan 2.6 T2I | $0.021 | General NSFW, cheapest, most flexible sizes | | 2 | Seedream v4 | $0.024 | Budget alternative | | 3 | Seedream v5.0 Lite | $0.032 | Best quality, PNG output | | 4 | Seedream v4.5 | $0.036 | Typography, poster design | --- ## Parameters ### Wan 2.6 — Text-to-Image | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `prompt` | string | Yes | - | Image description | | `negative_prompt` | string | No | - | What to exclude | | `size` | string | No | 1024*1024 | Output size (27 presets, see below) | | `enable_prompt_expansion` | boolean | No | false | Auto-expand prompt for better results | | `enable_sync_mode` | boolean | No | false | Wait for result synchronously | | `enable_base64_output` | boolean | No | false | Return Base64 instead of URL | | `seed` | integer | No | random | For reproducible results | ### Wan 2.6 — Image Edit Same as text-to-image, plus: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `images` | array | Yes | Images to edit (max 4, 384-5000px per side) | ### Seedream v5.0 Lite — Text-to-Image | Parameter | Type | Required | Default | Description | |-----------|------|----------|---------|-------------| | `prompt` | string | Yes | - | Image description | | `negative_prompt` | string | No | - | What to exclude | | `aspect_ratio` | string | No | 1:1 | 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21, etc. | | `image_size` | string | No | 1024 | 512, 1024, 2048, 4096 | | `prompt_optimization` | string | No | - | Standard, Fast | | `output_format` | string | No | png | png, jpeg, webp | | `seed` | integer | No | random | For reproducible results | ### Seedream v5.0 Lite — Edit Same as text-to-image, plus: | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `image` | string | Yes | Source image URL | ### Wan 2.6 Image Size Presets (27 options) `1024*1024`, `1280*720`, `720*1280`, `1280*960`, `960*1280`, `1536*1024`, `1024*1536`, `1280*1280`, `1536*1536`, `2048*1024`, `1024*2048`, `1536*1280`, `1280*1536`, `1680*720`, `720*1680`, `2016*864`, `864*2016`, `1536*864`, `864*1536`, `2184*936`, `936*2184`, `1400*1050`, `1050*1400`, `1680*1050`, `1050*1680`, `1176*1176`, `1560*1560` --- ## Workflow: Submit → Poll → Download ### Text-to-Image Example (Wan 2.6) ```bash # Step 1: Submit curl -s -X POST "https://api.atlascloud.ai/api/v1/model/generateImage" \ -H "Authorization: Bearer $ATLASCLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/wan-2.6/text-to-image", "prompt": "A beautiful woman in a flowing red dress standing on a cliff overlooking the ocean at sunset, dramatic lighting, photorealistic", "size": "1280*720", "enable_prompt_expansion": true }' # Returns: { "code": 200, "data": { "id": "prediction-id" } } # Step 2: Poll (every 3-5 seconds) curl -s "https://api.atlascloud.ai/api/v1/model/prediction/{prediction-id}" \ -H "Authorization: Bearer $ATLASCLOUD_API_KEY" # Returns: { "code": 200, "data": { "status": "completed", "outputs": ["https://...url..."] } } # Step 3: Download curl -o output.png "IMAGE_URL_FROM_OUTPUTS" ``` ### Image Editing Example (Wan 2.6) ```bash curl -s -X POST "https://api.atlascloud.ai/api/v1/model/generateImage" \ -H "Authorization: Bearer $ATLASCLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "alibaba/wan-2.6/image-edit", "prompt": "Change the outfit to a black evening gown, keep the face and pose unchanged", "images": ["https://example.com/photo.jpg"], "size": "1280*720" }' ``` ### Seedream Example ```bash curl -s -X POST "https://api.atlascloud.ai/api/v1/model/generateImage" \ -H "Authorization: Bearer $ATLASCLOUD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "bytedance/seedream-v5.0-lite", "prompt": "A stunning portrait of a woman with flowing silver hair in a fantasy forest, ethereal lighting, highly detailed", "aspect_ratio": "3:4", "image_size": "2048" }' ``` ### Polling Logic - `processing` / `starting` / `running` → wait 3-5s, retry (~5-15s for images) - `completed` / `succeeded` → done, get URL from `data.outputs[]` - `failed` → error, read `data.error` ### Atlas Cloud MCP Tools (if available) ``` atlas_generate_image(model="alibaba/wan-2.6/text-to-image", params={...}) atlas_quick_generate(model_keyword="wan 2.6", type="Image", prompt="...") atlas_get_prediction(prediction_id="...") ``` --- ## Implementation Guide 1. **Choose model**: Default to **Wan 2.6 T2I** ($0.021, most flexible). Use Seedream for higher quality or special features (PNG output, typography). 2. **Extract parameters**: Prompt, size/aspect ratio, negative prompt. Enable prompt expansion for better results. 3. **For editing**: Use Wan 2.6 Image Edit or Seedream Edit — provide source image URL + edit instructions. 4. **Execute**: POST to generateImage API → poll result → download. 5. **Present result**: show file path, offer to open. ## Prompt Tips - **Detailed descriptions**: These models respond well to detailed, specific prompts - **Negative prompts**: Use `negative_prompt` to exclude undesired elements — "blurry, low quality, distorted, watermark, text" - **Prompt expansion**: Enable `enable_prompt_expansion` (Wan) or `prompt_optimization` (Seedream) for auto-enhanced prompts - **Style keywords**: "photorealistic", "anime", "oil painting", "digital art", "cinematic lighting" - **Composition**: Describe poses, camera angles, lighting, and background explicitly

path: scripts/generate_image.py
content: #!/usr/bin/env python3
"""
Atlas Cloud - Image Generation & Editing Script
Supports text-to-image, image editing, and local file upload via Atlas Cloud API.
Zero external dependencies - uses only Python standard library.

Network calls (api.atlascloud.ai):
  - GET  /api/v1/models              — validate model ID (no auth required)
  - POST /api/v1/model/generateImage — submit image generation request
  - GET  /api/v1/model/prediction/*  — poll for generation result
  - POST /api/v1/model/uploadMedia   — upload local file (only via "upload" command)

Credentials (read from environment variables only, never hardcoded):
  - ATLASCLOUD_API_KEY: required for Atlas Cloud API

Usage:
  # Text-to-image
  python generate_image.py generate --model "alibaba/wan-2.6/text-to-image" --prompt "Sunset"

  # Image editing
  python generate_image.py generate --model "alibaba/wan-2.6/image-edit" --prompt "Change sky" --images "https://..."

  # Upload local file (for editing)
  python generate_image.py upload ./photo.jpg

  # List available models
  python generate_image.py list-models
"""

import argparse
import json
import os
import sys
import time
import urllib.request
import urllib.error
import urllib.parse

API_BASE = "https://api.atlascloud.ai/api/v1"
UA = "AtlasCloud-Skill/1.0"


# ---------------------------------------------------------------------------
# API key
# ---------------------------------------------------------------------------

def get_atlas_key():
    key = os.environ.get("ATLASCLOUD_API_KEY")
    if not key:
        print("Error: ATLASCLOUD_API_KEY environment variable is not set.", file=sys.stderr)
        print("Get your API key at: https://www.atlascloud.ai", file=sys.stderr)
        sys.exit(1)
    return key


# ---------------------------------------------------------------------------
# Atlas Cloud implementation
# ---------------------------------------------------------------------------

def api_request(method, endpoint, data=None):
    url = f"{API_BASE}{endpoint}"
    headers = {"Authorization": f"Bearer {get_atlas_key()}", "User-Agent": UA}
    body = None
    if data is not None and method == "POST":
        body = json.dumps(data).encode("utf-8")
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8", errors="replace")
        print(f"API Error {e.code}: {error_body}", file=sys.stderr)
        sys.exit(1)
    except urllib.error.URLError as e:
        print(f"Network Error: {e.reason}", file=sys.stderr)
        sys.exit(1)


def validate_model(model_id):
    try:
        url = f"{API_BASE}/models"
        req = urllib.request.Request(url, headers={"User-Agent": UA})
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        models = data.get("data", [])
        for m in models:
            if m.get("model") == model_id:
                return True
        print(f"Error: Model '{model_id}' not found on Atlas Cloud.", file=sys.stderr)
        keyword = model_id.split("/")[0] if "/" in model_id else model_id
        similar = [m["model"] for m in models if keyword.lower() in m.get("model", "").lower()]
        if similar:
            print("Similar models:", file=sys.stderr)
            for s in similar[:10]:
                print(f"  - {s}", file=sys.stderr)
        sys.exit(1)
    except SystemExit:
        raise
    except Exception as e:
        print(f"Warning: Could not validate model (will try anyway): {e}", file=sys.stderr)
        return True


def list_models(model_type=None):
    try:
        url = f"{API_BASE}/models"
        req = urllib.request.Request(url, headers={"User-Agent": UA})
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        models = data.get("data", [])
        if model_type:
            models = [m for m in models if m.get("type", "").lower() == model_type.lower()]
        print(f"Available {'image ' if model_type else ''}models ({len(models)}):\n")
        for m in models:
            price = m.get("price", {}).get("actual", {}).get("base_price", "N/A")
            print(f"  {m.get('model', 'unknown'):50s}  ${price}")
    except Exception as e:
        print(f"Error listing models: {e}", file=sys.stderr)
        sys.exit(1)


def submit(model_id, params):
    validate_model(model_id)
    payload = {"model": model_id, **params}
    print(f"Submitting image generation: {model_id}")
    result = api_request("POST", "/model/generateImage", data=payload)
    prediction_id = result.get("data", {}).get("id")
    if not prediction_id:
        print(f"Error: No prediction ID: {json.dumps(result, indent=2)}", file=sys.stderr)
        sys.exit(1)
    print(f"Prediction ID: {prediction_id}")
    return prediction_id


def poll(prediction_id, timeout=120, interval=3):
    endpoint = f"/model/prediction/{prediction_id}"
    start = time.time()
    while True:
        elapsed = int(time.time() - start)
        if elapsed >= timeout:
            print(f"Error: Timeout after {timeout}s.", file=sys.stderr)
            sys.exit(1)
        result = api_request("GET", endpoint)
        data = result.get("data", {})
        status = data.get("status", "unknown")
        if status in ("completed", "succeeded"):
            print(f"Completed in {elapsed}s")
            return data.get("outputs", [])
        if status == "failed":
            print(f"Generation failed: {data.get('error', 'Unknown')}", file=sys.stderr)
            sys.exit(1)
        print(f"  {status}... ({elapsed}s)")
        time.sleep(interval)


def generate(model_id, params, output_dir, timeout):
    get_atlas_key()
    prediction_id = submit(model_id, params)
    outputs = poll(prediction_id, timeout)
    if not outputs:
        print("Warning: No output files returned.", file=sys.stderr)
        return []
    saved = []
    for url in outputs:
        saved.append(download_file(url, output_dir))
    return saved


# ---------------------------------------------------------------------------
# Utilities
# ---------------------------------------------------------------------------

def download_file(url, output_dir="."):
    parsed = urllib.parse.urlparse(url)
    filename = os.path.basename(parsed.path)
    if not filename or "." not in filename:
        filename = f"output_{int(time.time())}.png"
    output_path = os.path.join(output_dir, filename)
    base, ext = os.path.splitext(output_path)
    counter = 1
    while os.path.exists(output_path):
        output_path = f"{base}_{counter}{ext}"
        counter += 1
    print(f"Downloading: {filename}")
    urllib.request.urlretrieve(url, output_path)
    size_kb = os.path.getsize(output_path) / 1024
    print(f"Saved: {output_path} ({size_kb:.1f} KB)")
    return output_path


def upload_file(file_path, skip_confirm=False):
    if not os.path.exists(file_path):
        print(f"Error: File not found: {file_path}", file=sys.stderr)
        sys.exit(1)
    if not skip_confirm:
        size_kb = os.path.getsize(file_path) / 1024
        print(f"About to upload '{file_path}' ({size_kb:.1f} KB) to Atlas Cloud (api.atlascloud.ai).")
        confirm = input("Proceed? [y/N]: ").strip().lower()
        if confirm not in ("y", "yes"):
            print("Upload cancelled.")
            sys.exit(0)
    url = f"{API_BASE}/model/uploadMedia"
    filename = os.path.basename(file_path)
    boundary = f"----AtlasCloudBoundary{int(time.time() * 1000)}"
    with open(file_path, "rb") as f:
        file_data = f.read()
    body = b""
    body += f"--{boundary}\r\n".encode()
    body += f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()
    body += b"Content-Type: application/octet-stream\r\n\r\n"
    body += file_data
    body += f"\r\n--{boundary}--\r\n".encode()
    headers = {
        "Authorization": f"Bearer {get_atlas_key()}",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
        "User-Agent": UA,
    }
    req = urllib.request.Request(url, data=body, headers=headers, method="POST")
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read().decode("utf-8"))
    except urllib.error.HTTPError as e:
        error_body = e.read().decode("utf-8", errors="replace")
        print(f"Upload Error {e.code}: {error_body}", file=sys.stderr)
        sys.exit(1)
    download_url = result.get("data", {}).get("download_url")
    if not download_url:
        print(f"Error: No download URL: {json.dumps(result, indent=2)}", file=sys.stderr)
        sys.exit(1)
    print(f"Uploaded: {filename}")
    print(f"URL: {download_url}")
    return download_url


def parse_value(value):
    if value.lower() == "true":
        return True
    if value.lower() == "false":
        return False
    try:
        return int(value)
    except ValueError:
        pass
    try:
        return float(value)
    except ValueError:
        pass
    if value.startswith(("[", "{")):
        try:
            return json.loads(value)
        except json.JSONDecodeError:
            pass
    return value


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="Image Generation & Editing (Atlas Cloud)",
    )
    subparsers = parser.add_subparsers(dest="command")

    subparsers.add_parser("list-models", help="List available image models")

    gen = subparsers.add_parser("generate", help="Generate or edit an image")
    gen.add_argument("--model", "-m", required=True, help="Model ID")
    gen.add_argument("--prompt", "-p", help="Text prompt")
    gen.add_argument("--negative-prompt", help="Negative prompt")
    gen.add_argument("--image", help="Input image URL (for editing)")
    gen.add_argument("--images", help="Comma-separated image URLs (for editing)")
    gen.add_argument("--output", "-o", default=".", help="Output directory (default: current dir)")
    gen.add_argument("--timeout", default=120, type=int, help="Polling timeout in seconds (default: 120)")
    gen.add_argument("extra", nargs="*", help="Extra params as key=value (e.g. aspect_ratio=16:9)")

    up = subparsers.add_parser("upload", help="Upload a local image to Atlas Cloud")
    up.add_argument("file", help="Path to local image file")
    up.add_argument("--yes", "-y", action="store_true", help="Skip upload confirmation prompt")

    args = parser.parse_args()
    if not args.command:
        parser.print_help()
        sys.exit(1)

    if args.command == "list-models":
        list_models("Image")
        return

    if args.command == "upload":
        get_atlas_key()
        upload_file(args.file, skip_confirm=args.yes)
        return

    # --- generate ---
    extra_params = {}
    for item in args.extra:
        if "=" not in item:
            print(f"Warning: Ignoring '{item}' (expected key=value)", file=sys.stderr)
            continue
        key, value = item.split("=", 1)
        extra_params[key] = parse_value(value)

    os.makedirs(args.output, exist_ok=True)

    params = {}
    if args.prompt:
        params["prompt"] = args.prompt
    if args.negative_prompt:
        params["negative_prompt"] = args.negative_prompt
    if args.image:
        params["images"] = [args.image]
    if args.images:
        params["images"] = [u.strip() for u in args.images.split(",")]
    params.update(extra_params)
    saved = generate(args.model, params, args.output, args.timeout)

    if saved:
        print(f"\nDone! {len(saved)} image(s) saved.")


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)