Skill: navygo/bailian-image-gen (version 1.0.1)

--- name: bailian-image-gen description: Alibaba Cloud Bailian Qwen Image 2.0 image generation. Supports text-to-image and image-to-image (reference image + text). For product promotion image generation, e-commerce image editing, marketing material creation. Provides Python API, CLI tool, and ComfyUI custom nodes. --- # Alibaba Cloud Bailian Image Generation Based on Alibaba Cloud Bailian platform's `qwen-image-2.0` model, providing high-quality text-to-image and image-to-image capabilities. ## Features - **Text-to-Image** - Generate high-quality images from text descriptions - **Image-to-Image** - Generate new images from reference image + text description - **ComfyUI Integration** - Complete ComfyUI custom nodes - **Auto Compression** - Automatically handle reference image size limits - **Synchronous Calls** - No async task waiting, instant results ## Use Cases - C-end App product promotion image generation - E-commerce product image style transfer and background replacement - Marketing material batch generation - Creative image editing ## Quick Start ### Environment Setup 1. Install dependencies: ```bash pip install requests Pillow ``` 2. Configure API Key (two methods): **Method 1: .env file (recommended)** ```bash cp .env.example .env # Edit .env file DASHSCOPE_API_KEY=your-api-key-here ``` **Method 2: Environment variable** ```bash export DASHSCOPE_API_KEY=your-api-key ``` Get API Key from bailian.console.aliyun.com ### Command Line Usage ```bash # Text-to-Image python scripts/bailian_image_gen.py --mode t2i --prompt "A cute orange cat, high quality" --output cat.png # Image-to-Image python scripts/bailian_image_gen.py --mode i2i --prompt "Modern minimalist living room scene, warm lighting" --reference-image product.jpg --output result.png ``` ### Python API ```python from scripts.bailian_image_gen import QwenImageGenerator # Initialize client = QwenImageGenerator() # Text-to-Image result = client.text_to_image( prompt="A cute orange cat, high quality", size="1024*1024" ) url = client.extract_image_url(result) client.download_image(url, "output.png") # Image-to-Image result = client.image_to_image( prompt="Modern minimalist living room scene, warm lighting", reference_image_path="product.jpg" ) url = client.extract_image_url(result) client.download_image(url, "output.png") ``` ## ComfyUI Integration ### Install Nodes 1. Copy files to ComfyUI: ```bash cp scripts/bailian_image_gen.py /path/to/ComfyUI/custom_nodes/ cp scripts/comfyui_bailian_node.py /path/to/ComfyUI/custom_nodes/ ``` 2. Configure API Key in ComfyUI directory: ```bash echo "DASHSCOPE_API_KEY=your-api-key" > /path/to/ComfyUI/.env ``` 3. Restart ComfyUI ### Available Nodes Search "Bailian" in ComfyUI to find these nodes: **BailianText2Image** - Inputs: prompt (STRING), size (COMBO), seed (INT) - Output: image (IMAGE) **BailianImage2Image** - Inputs: image (IMAGE), prompt (STRING), size (COMBO), seed (INT) - Output: image (IMAGE) ### Workflow Example Import `assets/comfyui_workflow.json` for product promotion image generation example. Typical workflow: ``` [Load Image] --> [BailianImage2Image] --> [Save Image] ^ prompt: "Modern minimalist living room, warm lighting" ``` ## Parameters | Parameter | Type | Description | Default | |-----------|------|-------------|---------| | `--mode` | string | t2i=text-to-image, i2i=image-to-image | Required | | `--prompt` | string | Prompt text | Required | | `--reference-image` | string | Reference image path (i2i mode) | None | | `--size` | string | Image size | 1024*1024 | | `--seed` | int | Random seed | Random | | `--output` | string | Output path | Required | ## Supported Image Sizes - `1024*1024` - Square (recommended) - `1024*768` - Landscape - `768*1024` - Portrait - `2048*2048` - High resolution ## Prompt Tips ### Product Promotion Template ``` [Product] placed in [Scene], [Style], [Lighting], [Quality requirements] ``` **Examples:** - "Smartwatch placed on minimalist white marble desktop, Nordic minimalist style, natural light, product photography quality" - "Sneakers placed on wooden floor, city skyline background, fashion magazine style, soft side lighting" - "Cosmetics placed on dressing table, surrounded by flowers and perfume bottles, luxury style, warm lighting" ## File Structure ``` bailian-image-gen/ ├── .env.example # API Key config example ├── README.md # Detailed documentation ├── requirements.txt # Dependencies ├── SKILL.md # This file ├── assets/ │ └── comfyui_workflow.json # ComfyUI workflow example └── scripts/ ├── bailian_image_gen.py # Core script └── comfyui_bailian_node.py # ComfyUI nodes ``` ## Notes 1. **API Key** - Requires Alibaba Cloud Bailian platform account and API Key 2. **Image Compression** - Reference images are automatically compressed to meet API limits 3. **Network Requirements** - Requires access to Alibaba Cloud dashscope service 4. **Synchronous Calls** - qwen-image-2.0 uses synchronous calls, no async task waiting ## Error Handling Common errors and solutions: | Error | Cause | Solution | |-------|-------|----------| | API Key error | Not configured or incorrect | Check .env file or environment variable | | Image too large | Reference image exceeds limit | Script auto-compresses, if still fails use smaller image | | Network error | Cannot access Alibaba Cloud | Check network connection | ## References - [Alibaba Cloud Bailian Console] - [OpenClaw Documentation] - [ComfyUI GitHub] ## Author @navygo

path: requirements.txt
content: requests>=2.28.0
Pillow>=9.0.0


path: scripts/bailian_image_gen.py
content: #!/usr/bin/env python3
"""
Alibaba Cloud Bailian Qwen Image 2.0 Image Generation
Supports: Text-to-Image, Image-to-Image (reference image + text)
Model: qwen-image-2.0
"""

import os
import sys
import json
import base64
import requests
import argparse
import io
from typing import Optional, Dict, Any
from PIL import Image


def load_env_file():
    """Load environment variables from .env file"""
    env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env')
    if os.path.exists(env_path):
        with open(env_path, 'r', encoding='utf-8') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#') and '=' in line:
                    key, value = line.split('=', 1)
                    os.environ.setdefault(key.strip(), value.strip())

load_env_file()


class QwenImageGenerator:
    """Alibaba Cloud Bailian Qwen Image 2.0 Client"""
    
    BASE_URL = "https://dashscope.aliyuncs.com/api/v1"
    ENDPOINT = "/services/aigc/multimodal-generation/generation"
    DEFAULT_MODEL = "qwen-image-2.0"
    MAX_REF_IMAGE_SIZE = 61440
    
    def __init__(self, [REDACTED_SECRET] = None):
        self.[REDACTED_SECRET] or os.environ.get("DASHSCOPE_API_KEY")
        if not self.api_key:
            raise ValueError("API Key required. Set DASHSCOPE_API_KEY environment variable.")
        
        self.headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json"
        }
    
    def _compress_image_to_datauri(self, image_path: str, max_size: int = MAX_REF_IMAGE_SIZE) -> str:
        img = Image.open(image_path)
        original_size = img.size
        
        if img.mode in ('RGBA', 'P'):
            background = Image.new('RGB', img.size, (255, 255, 255))
            background.paste(img, mask=img.split()[-1] if img.mode == 'RGBA' else None)
            img = background
        else:
            img = img.convert('RGB')
        
        for target_size in [1024, 768, 512, 384, 256]:
            if max(img.size) > target_size:
                ratio = target_size / max(img.size)
                new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio))
                resized = img.resize(new_size, Image.LANCZOS)
            else:
                resized = img
            
            for quality in [85, 70, 50, 40, 30]:
                buffer = io.BytesIO()
                resized.save(buffer, format='JPEG', quality=quality)
                raw_data = buffer.getvalue()
                b64_data = base64.b64encode(raw_data).decode("utf-8")
                data_uri = f"data:image/jpeg;base64,{b64_data}"
                
                if len(data_uri) <= max_size:
                    print(f"[INFO] Image compressed: {original_size} -> {resized.size}, quality={quality}")
                    return data_uri
        
        raise ValueError(f"Cannot compress image to within {max_size} characters")
    
    def text_to_image(self, prompt: str, size: str = "1024*1024", n: int = 1, 
                      seed: Optional[int] = None, model: str = DEFAULT_MODEL) -> Dict[str, Any]:
        url = f"{self.BASE_URL}{self.ENDPOINT}"
        
        payload = {
            "model": model,
            "input": {
                "messages": [{
                    "role": "user",
                    "content": [{"type": "text", "text": f"Generate an image: {prompt}"}]
                }]
            }
        }
        
        if size or n or seed is not None:
            payload["parameters"] = {}
            if size: payload["parameters"]["size"] = size
            if n: payload["parameters"]["n"] = n
            if seed is not None: payload["parameters"]["seed"] = seed
        
        response = requests.post(url, headers=self.headers, json=payload, timeout=120)
        response.raise_for_status()
        return response.json()
    
    def image_to_image(self, prompt: str, reference_image_path: str, size: str = "1024*1024",
                       n: int = 1, seed: Optional[int] = None, model: str = DEFAULT_MODEL) -> Dict[str, Any]:
        url = f"{self.BASE_URL}{self.ENDPOINT}"
        image_data = self._compress_image_to_datauri(reference_image_path)
        
        payload = {
            "model": model,
            "input": {
                "messages": [{
                    "role": "user",
                    "content": [
                        {"type": "image", "image": image_data},
                        {"type": "text", "text": f"Generate based on reference image: {prompt}"}
                    ]
                }]
            }
        }
        
        if size or n or seed is not None:
            payload["parameters"] = {}
            if size: payload["parameters"]["size"] = size
            if n: payload["parameters"]["n"] = n
            if seed is not None: payload["parameters"]["seed"] = seed
        
        response = requests.post(url, headers=self.headers, json=payload, timeout=120)
        response.raise_for_status()
        return response.json()
    
    def extract_image_url(self, result: Dict[str, Any]) -> Optional[str]:
        output = result.get('output', {})
        
        if 'choices' in output:
            for choice in output['choices']:
                message = choice.get('message', {})
                content = message.get('content', [])
                for item in content:
                    if 'image' in item:
                        return item.get('image')
        
        if 'results' in output:
            for r in output['results']:
                if 'url' in r:
                    return r['url']
        
        if 'image_url' in output:
            return output['image_url']
        
        return None
    
    def download_image(self, image_url: str, output_path: str) -> str:
        headers = {"Authorization": f"Bearer {self.api_key}"}
        response = requests.get(image_url, headers=headers, timeout=60)
        response.raise_for_status()
        
        with open(output_path, "wb") as f:
            f.write(response.content)
        
        return output_path


def main():
    parser = argparse.ArgumentParser(description="Alibaba Cloud Bailian Image Generation")
    parser.add_argument("--api-key", help="DashScope API Key")
    parser.add_argument("--mode", choices=["t2i", "i2i"], required=True,
                       help="Mode: t2i=text-to-image, i2i=image-to-image")
    parser.add_argument("--prompt", required=True, help="Prompt text")
    parser.add_argument("--reference-image", help="Reference image path (i2i mode)")
    parser.add_argument("--size", default="1024*1024", help="Image size")
    parser.add_argument("--n", type=int, default=1, help="Number of images")
    parser.add_argument("--seed", type=int, help="Random seed")
    parser.add_argument("--model", default="qwen-image-2.0", help="Model name")
    parser.add_argument("--output", "-o", required=True, help="Output path")
    
    args = parser.parse_args()
    
    try:
        client = QwenImageGenerator([REDACTED_SECRET])
    except ValueError as e:
        print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)
    
    try:
        if args.mode == "t2i":
            result = client.text_to_image(
                prompt=args.prompt, size=args.size, n=args.n, seed=args.seed, model=args.model
            )
        else:
            if not args.reference_image:
                print("[ERROR] Image-to-image mode requires --reference-image", file=sys.stderr)
                sys.exit(1)
            result = client.image_to_image(
                prompt=args.prompt, reference_image_path=args.reference_image,
                size=args.size, n=args.n, seed=args.seed, model=args.model
            )
        
        print(json.dumps(result, indent=2, ensure_ascii=False))
        
        image_url = client.extract_image_url(result)
        if image_url:
            client.download_image(image_url, args.output)
            print(f"[OK] Image saved: {args.output}")
        else:
            print("[ERROR] Cannot find image URL")
            
    except requests.exceptions.RequestException as e:
        print(f"[ERROR] API request error: {e}", file=sys.stderr)
        sys.exit(1)
    except Exception as e:
        print(f"[ERROR] Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()

path: scripts/comfyui_bailian_node.py
content: """
ComfyUI Custom Nodes - Alibaba Cloud Bailian Qwen Image 2.0
Install: Copy this file and bailian_image_gen.py to ComfyUI/custom_nodes/
"""

import os
import sys
import torch
import numpy as np
from PIL import Image
import io

current_dir = os.path.dirname(os.path.abspath(__file__))
if current_dir not in sys.path:
    sys.path.insert(0, current_dir)

def load_env_file():
    """Load environment variables from .env file"""
    possible_paths = [
        os.path.join(current_dir, '.env'),
        os.path.join(os.path.dirname(current_dir), '.env'),
        os.path.join(os.path.dirname(os.path.dirname(current_dir)), '.env'),
    ]
    
    for env_path in possible_paths:
        if os.path.exists(env_path):
            with open(env_path, 'r', encoding='utf-8') as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#') and '=' in line:
                        key, value = line.split('=', 1)
                        key = key.strip()
                        value = value.strip()
                        if key and value and key not in os.environ:
                            os.environ[key] = value
            print(f"[BailianNode] Loaded .env: {env_path}")
            return

load_env_file()

try:
    from bailian_image_gen import QwenImageGenerator
except ImportError as e:
    print(f"[BailianNode] Warning: Cannot import bailian_image_gen: {e}")
    QwenImageGenerator = None


class BailianText2Image:
    """Bailian Text-to-Image Node"""
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "prompt": ("STRING", {"multiline": True, "default": "A cute orange cat, high quality"}),
                "size": (["1024*1024", "1024*768", "768*1024", "2048*2048"], {"default": "1024*1024"}),
            },
            "optional": {
                "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}),
            }
        }
    
    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES = ("image",)
    FUNCTION = "generate"
    CATEGORY = "Bailian"
    
    def generate(self, prompt, size, seed=-1):
        if QwenImageGenerator is None:
            raise RuntimeError("bailian_image_gen module not loaded")
        
        try:
            client = QwenImageGenerator()
        except ValueError as e:
            raise RuntimeError(f"API Key error: {e}")
        
        seed_val = None if seed == -1 else seed
        
        print(f"[BailianNode] Text-to-Image: prompt='{prompt[:50]}...', size={size}")
        
        result = client.text_to_image(prompt=prompt, size=size, seed=seed_val)
        
        image_url = client.extract_image_url(result)
        if not image_url:
            raise RuntimeError(f"Cannot get image URL: {result}")
        
        print(f"[BailianNode] Image URL: {image_url[:60]}...")
        
        import requests
        headers = {"Authorization": f"Bearer {client.api_key}"}
        img_response = requests.get(image_url, headers=headers, timeout=60)
        img_response.raise_for_status()
        
        pil_image = Image.open(io.BytesIO(img_response.content))
        pil_image = pil_image.convert("RGB")
        
        image_np = np.array(pil_image).astype(np.float32) / 255.0
        image_tensor = torch.from_numpy(image_np).unsqueeze(0)
        
        print(f"[BailianNode] Generated: {pil_image.size}")
        
        return (image_tensor,)


class BailianImage2Image:
    """Bailian Image-to-Image Node"""
    
    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "image": ("IMAGE",),
                "prompt": ("STRING", {"multiline": True, "default": "Modern minimalist living room, warm lighting, high quality product photography"}),
                "size": (["1024*1024", "1024*768", "768*1024", "2048*2048"], {"default": "1024*1024"}),
            },
            "optional": {
                "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}),
            }
        }
    
    RETURN_TYPES = ("IMAGE",)
    RETURN_NAMES = ("image",)
    FUNCTION = "generate"
    CATEGORY = "Bailian"
    
    def generate(self, image, prompt, size, seed=-1):
        if QwenImageGenerator is None:
            raise RuntimeError("bailian_image_gen module not loaded")
        
        try:
            client = QwenImageGenerator()
        except ValueError as e:
            raise RuntimeError(f"API Key error: {e}")
        
        seed_val = None if seed == -1 else seed
        
        if isinstance(image, torch.Tensor):
            image_np = image[0].cpu().numpy()
            image_np = (image_np * 255).astype(np.uint8)
            pil_image = Image.fromarray(image_np)
        else:
            pil_image = image
        
        import tempfile
        with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp:
            pil_image.save(tmp.name)
            temp_path = tmp.name
        
        try:
            print(f"[BailianNode] Image-to-Image: prompt='{prompt[:50]}...', size={size}")
            
            result = client.image_to_image(
                prompt=prompt, reference_image_path=temp_path, size=size, seed=seed_val
            )
            
            image_url = client.extract_image_url(result)
            if not image_url:
                raise RuntimeError(f"Cannot get image URL: {result}")
            
            print(f"[BailianNode] Image URL: {image_url[:60]}...")
            
            import requests
            headers = {"Authorization": f"Bearer {client.api_key}"}
            img_response = requests.get(image_url, headers=headers, timeout=60)
            img_response.raise_for_status()
            
            output_image = Image.open(io.BytesIO(img_response.content))
            output_image = output_image.convert("RGB")
            
            image_np = np.array(output_image).astype(np.float32) / 255.0
            image_tensor = torch.from_numpy(image_np).unsqueeze(0)
            
            print(f"[BailianNode] Generated: {output_image.size}")
            
            return (image_tensor,)
            
        finally:
            if os.path.exists(temp_path):
                os.remove(temp_path)


NODE_CLASS_MAPPINGS = {
    "BailianText2Image": BailianText2Image,
    "BailianImage2Image": BailianImage2Image,
}

NODE_DISPLAY_NAME_MAPPINGS = {
    "BailianText2Image": "Bailian Text-to-Image",
    "BailianImage2Image": "Bailian Image-to-Image",
}


print("="*60)
print("[BailianNode] Alibaba Cloud Bailian Qwen Image 2.0 Nodes Loaded")
print("[BailianNode] Available Nodes:")
print("  - BailianText2Image: Text-to-Image")
print("  - BailianImage2Image: Image-to-Image")
print("="*60)
print("[BailianNode] Please ensure DASHSCOPE_API_KEY is set in .env or environment")
print("="*60)


path: assets/comfyui_workflow.json
content: {
  "last_node_id": 10,
  "last_link_id": 9,
  "nodes": [
    {
      "id": 1,
      "type": "LoadImage",
      "pos": [100, 100],
      "size": [320, 310],
      "flags": {},
      "order": 0,
      "mode": 0,
      "inputs": [],
      "outputs": [
        {
          "name": "IMAGE",
          "type": "IMAGE",
          "links": [1],
          "slot_index": 0
        },
        {
          "name": "MASK",
          "type": "MASK",
          "links": null
        }
      ],
      "title": "Load Product Image",
      "properties": {},
      "widgets_values": ["product.png", "image"]
    },
    {
      "id": 2,
      "type": "BailianImage2Image",
      "pos": [500, 100],
      "size": [400, 250],
      "flags": {},
      "order": 1,
      "mode": 0,
      "inputs": [
        {
          "name": "image",
          "type": "IMAGE",
          "link": 1
        }
      ],
      "outputs": [
        {
          "name": "image",
          "type": "IMAGE",
          "links": [2],
          "slot_index": 0
        }
      ],
      "title": "Bailian Image-to-Image",
      "properties": {},
      "widgets_values": [
        "Place this product in modern minimalist living room scene, warm lighting, high-end product photography quality",
        "1024*1024",
        -1
      ]
    },
    {
      "id": 3,
      "type": "SaveImage",
      "pos": [1000, 100],
      "size": [320, 270],
      "flags": {},
      "order": 2,
      "mode": 0,
      "inputs": [
        {
          "name": "images",
          "type": "IMAGE",
          "link": 2
        }
      ],
      "outputs": [],
      "title": "Save Promo Image",
      "properties": {},
      "widgets_values": ["product_promo_"]
    },
    {
      "id": 4,
      "type": "Note",
      "pos": [100, 450],
      "size": [400, 150],
      "flags": {},
      "order": 3,
      "mode": 0,
      "inputs": [],
      "outputs": [],
      "title": "Instructions",
      "properties": {},
      "widgets_values": [
        "1. Upload your product image in 'Load Product Image' node\n2. Modify prompt in 'Bailian Image-to-Image' node to describe desired scene\n3. Click 'Queue' to generate image\n\nNote: Configure DASHSCOPE_API_KEY in ComfyUI directory"
      ]
    }
  ],
  "links": [
    [1, 1, 2, 0, "IMAGE"],
    [2, 2, 3, 0, "IMAGE"]
  ],
  "groups": [],
  "config": {},
  "extra": {
    "workflow": {
      "name": "Bailian Product Promotion Image Generation"
    }
  },
  "version": 0.4
}


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