Skill: zealman2025/seedream5 (version 1.0.0)

--- name: seedream5 description: 使用火山引擎豆包 Seedream 5.0 生成或编辑图片。支持文生图、单图/多图生图、组图、联网搜索。 homepage: https://www.volcengine.com/docs/82379/1541523 metadata: { "openclaw": { "emoji": "🎨", "requires": { "bins": ["uv"], "env": ["VOLC_API_KEY"] }, "primaryEnv": "VOLC_API_KEY", "install": [ { "id": "uv-brew", "kind": "brew", "formula": "uv", "bins": ["uv"], "label": "Install uv (brew)", }, ], }, } --- # Seedream5 (火山引擎豆包 Seedream 5.0) 使用火山引擎图片生成 API 生成或编辑图片。模型：`doubao-seedream-5-0-260128`。API 文档：<https://www.volcengine.com/docs/82379/1541523> ## 文生图 ```bash uv run {baseDir}/scripts/generate_image.py --prompt "你的图片描述" --filename "output.png" --resolution 2K ``` ## 单图生图 ```bash uv run {baseDir}/scripts/generate_image.py --prompt "编辑指令" --filename "output.png" -i "/path/in.png" --resolution 2K ``` ## 多图生图（2–14 张参考图） ```bash uv run {baseDir}/scripts/generate_image.py --prompt "将这几张图合成一个场景" --filename "output.png" -i img1.png -i img2.png -i img3.png ``` ## API Key 配置 本技能需要火山引擎 API Key。任选以下一种方式配置即可。 ### 方式一：环境变量（推荐用于本地开发） ```bash export VOLC_API_KEY="你的API_KEY" # 或 export ARK_API_KEY="你的API_KEY" ``` ### 方式二：openclaw.json（推荐用于持久配置） 编辑 `~/.openclaw/openclaw.json`（或你的 `OPENCLAW_CONFIG_PATH` 指向的配置文件），在 `skills.entries` 下添加： ```json { "skills": { "entries": { "seedream5": { "apiKey": "你的火山引擎API_KEY" } } } } ``` OpenClaw 会将 `apiKey` 注入为 `VOLC_API_KEY` 环境变量，脚本会自动读取。 ### 方式三：ClawPanel / Control UI 若使用 ClawPanel 或 OpenClaw Control UI，在 Skills 页面找到 seedream5，在凭证/API Key 字段中填写即可。 ### 获取 API Key 前往 [火山引擎控制台](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey) 获取长效 API Key。 ## 可选参数 指定宽高比 ```bash uv run {baseDir}/scripts/generate_image.py --prompt "竖屏人像" --filename "output.png" --aspect-ratio 9:16 ``` 不添加水印 ```bash uv run {baseDir}/scripts/generate_image.py --prompt "..." --filename "output.png" --no-watermark ``` 组图模式（多张关联图片） ```bash uv run {baseDir}/scripts/generate_image.py --prompt "生成3张不同时段场景" --filename "out.png" -i ref.png --sequential auto --max-images 3 ``` 联网搜索（天气、实时信息等） ```bash uv run {baseDir}/scripts/generate_image.py --prompt "上海未来5日天气预报图" --filename "weather.png" --web-search ``` 输入图支持 URL ```bash uv run {baseDir}/scripts/generate_image.py --prompt "换装" --filename "out.png" -i "https://example.com/ref.png" ``` ## 分辨率与宽高比 - **分辨率**：`2K`、`3K`（doubao-seedream-5-0-260128 支持） - **宽高比**：`1:1`, `2:3`, `3:2`, `3:4`, `4:3`, `4:5`, `5:4`, `9:16`, `16:9`, `21:9` - 建议在文件名中使用时间戳：`yyyy-mm-dd-hh-mm-ss-name.png` - 脚本会输出 `MEDIA:<path>` 供 OpenClaw 在支持的聊天渠道中自动附加 - 不要读取图片内容回传，仅报告保存路径即可

path: README.md
content: # Seedream5

OpenClaw 技能：使用火山引擎豆包 Seedream 5.0 生成或编辑图片。

## 安装

### 方式一：ClawHub 官方商店（推荐）

```bash
clawhub install seedream5
```

**命令行配置 API Key**（安装后执行）：

```bash
openclaw config set skills.entries.seedream5.apiKey "你的火山引擎API_KEY"
```


### 方式二：手动安装

仓库结构为 `openclaw-seedream5/seedream5/`（技能内容在 `seedream5` 子目录内）。

1. 从 [GitHub](https://github.com/zealman2025/openclaw-seedream5) 下载或克隆本仓库
2. 将 `seedream5` 文件夹复制到 OpenClaw 的 `workspace/skills/` 目录内

   ```bash
   git clone https://github.com/zealman2025/openclaw-seedream5.git
   cp -r openclaw-seedream5/seedream5 /path/to/workspace/skills/
   ```

   最终路径：`workspace/skills/seedream5/`（含 SKILL.md、scripts/、README.md）

   若放置在其他路径，可在 `openclaw.json` 的 `skills.load.extraDirs` 中指向该目录的父级。

## 配置 API Key

本技能需要火山引擎 API Key。任选以下一种方式：

### 方式一：环境变量

```bash
export VOLC_API_KEY="你的API_KEY"
# 或
export ARK_API_KEY="你的API_KEY"
```

### 方式二：openclaw.json（推荐）

编辑 `~/.openclaw/openclaw.json`，在 `skills.entries` 下添加：

```json
{
  "skills": {
    "entries": {
      "seedream5": {
        "apiKey": "你的火山引擎API_KEY"
      }
    }
  }
}
```

### 方式三：ClawPanel / Control UI

在 Skills 页面找到 seedream5，在凭证字段中填写 API Key。

### 获取 API Key

前往 [火山引擎控制台](https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey) 获取。

## 依赖

- [uv](https://docs.astral.sh/uv/)（Python 包管理）
- 脚本会自动安装 `requests`、`pillow`

## 使用示例

详见 [SKILL.md](./SKILL.md)。


path: scripts/generate_image.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
#     "requests>=2.28.0",
#     "pillow>=10.0.0",
# ]
# ///
"""
Generate images using Volcano Engine Seedream 5.0 API (豆包 doubao-seedream-5-0-260128).

API: POST https://ark.cn-beijing.volces.com/api/v3/images/generations

Usage:
    uv run generate_image.py --prompt "your image description" --filename "output.png" [--resolution 2K|3K] [--api-key KEY]

Multi-image editing (up to 14 images):
    uv run generate_image.py --prompt "combine these images" --filename "output.png" -i img1.png -i img2.png -i img3.png
"""

import argparse
import base64
import os
import sys
from io import BytesIO
from pathlib import Path

import requests

API_BASE = "https://ark.cn-beijing.volces.com/api/v3"
MODEL = "doubao-seedream-5-0-260128"

SUPPORTED_ASPECT_RATIOS = [
    "1:1",
    "2:3",
    "3:2",
    "3:4",
    "4:3",
    "4:5",
    "5:4",
    "9:16",
    "16:9",
    "21:9",
]

# doubao-seedream-5.0-lite: 2K/3K resolution, aspect ratio -> pixel size
SIZE_2K = {
    "1:1": "2048x2048",
    "4:3": "2304x1728",
    "3:4": "1728x2304",
    "16:9": "2848x1600",
    "9:16": "1600x2848",
    "3:2": "2496x1664",
    "2:3": "1664x2496",
    "21:9": "3136x1344",
}
SIZE_3K = {
    "1:1": "3072x3072",
    "4:3": "3456x2592",
    "3:4": "2592x3456",
    "16:9": "4096x2304",
    "9:16": "2304x4096",
    "2:3": "2496x3744",
    "3:2": "3744x2496",
    "21:9": "4704x2016",
}


def get_api_key(provided_key: str | None) -> str | None:
    """Get API key from argument first, then environment."""
    if provided_key:
        return provided_key
    return os.environ.get("VOLC_API_KEY") or os.environ.get("ARK_API_KEY")


def auto_detect_resolution(max_input_dim: int) -> str:
    """Infer output resolution from the largest input image dimension."""
    if max_input_dim >= 2500:
        return "3K"
    return "2K"


def choose_size(
    resolution: str,
    aspect_ratio: str | None,
) -> str:
    """Choose size parameter: 2K/3K or specific pixels."""
    res_map = {"2K": SIZE_2K, "3K": SIZE_3K}
    if aspect_ratio and resolution in res_map and aspect_ratio in res_map[resolution]:
        return res_map[resolution][aspect_ratio]
    return resolution


def image_to_base64(path: str) -> str:
    """Load image and return data URI."""
    from PIL import Image as PILImage

    with PILImage.open(path) as img:
        # Convert to RGB if necessary
        if img.mode in ("RGBA", "P"):
            img = img.convert("RGB")
        buf = BytesIO()
        fmt = "PNG" if img.mode == "RGBA" else "JPEG"
        img.save(buf, format=fmt, quality=95)
    b64 = base64.b64encode(buf.getvalue()).decode("ascii")
    mime = "image/png" if fmt == "PNG" else "image/jpeg"
    return f"data:{mime};base64,{b64}"


def main():
    parser = argparse.ArgumentParser(
        description="Generate images using Volcano Engine Seedream 5.0 lite (豆包)"
    )
    parser.add_argument(
        "--prompt", "-p",
        required=True,
        help="Image description/prompt (支持中英文)",
    )
    parser.add_argument(
        "--filename", "-f",
        required=True,
        help="Output filename (e.g., sunset-mountains.png)",
    )
    parser.add_argument(
        "--input-image", "-i",
        action="append",
        dest="input_images",
        metavar="IMAGE",
        help="Input image: local path or URL (http(s)://). Up to 14 images.",
    )
    parser.add_argument(
        "--resolution", "-r",
        choices=["2K", "3K"],
        default="2K",
        help="Output resolution: 2K or 3K (Seedream 5.0 lite). Default: 2K",
    )
    parser.add_argument(
        "--aspect-ratio", "-a",
        choices=SUPPORTED_ASPECT_RATIOS,
        default=None,
        help=f"Output aspect ratio. Options: {', '.join(SUPPORTED_ASPECT_RATIOS)}",
    )
    parser.add_argument(
        "--api-key", "-k",
        help="Volcano Engine API key (overrides VOLC_API_KEY/ARK_API_KEY env)",
    )
    parser.add_argument(
        "--no-watermark",
        action="store_true",
        help="Do not add AI watermark to output",
    )
    parser.add_argument(
        "--model", "-m",
        default=MODEL,
        help=f"Model ID or Endpoint ID (default: {MODEL})",
    )
    parser.add_argument(
        "--sequential",
        choices=["disabled", "auto"],
        default="disabled",
        help="Group image mode: disabled=single image, auto=generate multiple related images",
    )
    parser.add_argument(
        "--max-images",
        type=int,
        default=15,
        metavar="N",
        help="Max images when sequential=auto (1-15). Default: 15",
    )
    parser.add_argument(
        "--web-search",
        action="store_true",
        help="Enable web search tool (e.g. for weather, real-time info)",
    )

    args = parser.parse_args()

    [REDACTED_SECRET])
    if not [REDACTED_SECRET]"Error: No API key provided.", file=sys.stderr)
        print("Please either:", file=sys.stderr)
        print("  1. Provide --api-key argument", file=sys.stderr)
        print("  2. Set VOLC_API_KEY or ARK_API_KEY environment variable", file=sys.stderr)
        print("  3. Set skills.entries.seedream5.apiKey in openclaw.json", file=sys.stderr)
        sys.exit(1)

    output_path = Path(args.filename)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    # Load input images: URL or local file -> URL or base64 (up to 14)
    input_images_data = []
    max_input_dim = 0
    if args.input_images:
        if len(args.input_images) > 14:
            print(f"Error: Too many input images ({len(args.input_images)}). Maximum is 14.", file=sys.stderr)
            sys.exit(1)
        for img_spec in args.input_images:
            try:
                if img_spec.startswith(("http://", "https://")):
                    input_images_data.append(img_spec)
                    print(f"Using image URL: {img_spec}")
                else:
                    from PIL import Image as PILImage

                    with PILImage.open(img_spec) as img:
                        width, height = img.size
                        max_input_dim = max(max_input_dim, width, height)
                    input_images_data.append(image_to_base64(img_spec))
                    print(f"Loaded input image: {img_spec}")
            except Exception as e:
                print(f"Error loading input image '{img_spec}': {e}", file=sys.stderr)
                sys.exit(1)

    # Auto-detect resolution from input if not specified and we have inputs
    resolution = args.resolution
    if args.input_images and max_input_dim >= 2500 and args.resolution == "2K":
        resolution = "3K"
        print(f"Auto-detected resolution: 3K (from max input dimension {max_input_dim})")

    size = choose_size(resolution, args.aspect_ratio)
    img_count = len(input_images_data)
    if img_count > 0:
        print(f"Processing {img_count} image{'s' if img_count > 1 else ''} with size {size}...")
    else:
        print(f"Generating image with size {size}...")

    body = {
        "model": args.model,
        "prompt": args.prompt,
        "size": size,
        "sequential_image_generation": args.sequential,
        "response_format": "b64_json",
        "output_format": "png" if str(output_path).lower().endswith(".png") else "jpeg",
        "watermark": not args.no_watermark,
    }
    if input_images_data:
        body["image"] = input_images_data[0] if len(input_images_data) == 1 else input_images_data
    if args.sequential == "auto":
        body["sequential_image_generation_options"] = {"max_images": min(args.max_images, 15)}
    if args.web_search:
        body["tools"] = [{"type": "web_search"}]

    try:
        resp = requests.post(
            f"{API_BASE}/images/generations",
            headers={
                "Content-Type": "application/json",
                "Authorization": f"Bearer {api_key}",
            },
            json=body,
            timeout=120,
        )
        resp.raise_for_status()
        data = resp.json()
    except requests.exceptions.RequestException as e:
        err_detail = ""
        if hasattr(e, "response") and e.response is not None:
            try:
                err_detail = e.response.json()
            except Exception:
                err_detail = e.response.text[:500]
        print(f"Error calling API: {e}", file=sys.stderr)
        if err_detail:
            print(f"Response: {err_detail}", file=sys.stderr)
        sys.exit(1)

    if "error" in data:
        print(f"API error: {data['error']}", file=sys.stderr)
        sys.exit(1)

    result_data = data.get("data", [])
    if not result_data:
        print("Error: No image in response.", file=sys.stderr)
        sys.exit(1)

    saved_paths = []
    stem = output_path.stem
    suffix = output_path.suffix
    parent = output_path.parent

    for idx, item in enumerate(result_data):
        if "error" in item:
            print(f"Image {idx + 1} failed: {item['error']}", file=sys.stderr)
            continue
        b64_data = item.get("b64_json")
        if not b64_data:
            url = item.get("url")
            if url:
                print(f"Image {idx + 1} URL (not saved): {url}", file=sys.stderr)
            continue
        img_bytes = base64.b64decode(b64_data)
        if len(result_data) == 1:
            out_file = output_path
        else:
            out_file = parent / f"{stem}_{idx + 1}{suffix}"
        out_file.write_bytes(img_bytes)
        full_path = out_file.resolve()
        saved_paths.append(full_path)
        print(f"Image saved: {full_path}")
        print(f"MEDIA:{full_path}")
    if not saved_paths:
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/test_generate_image.py
content: import importlib.util
from pathlib import Path

import pytest

MODULE_PATH = Path(__file__).with_name("generate_image.py")
SPEC = importlib.util.spec_from_file_location("generate_image", MODULE_PATH)
assert SPEC and SPEC.loader
MODULE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(MODULE)


@pytest.mark.parametrize(
    ("max_input_dim", "expected"),
    [
        (0, "2K"),
        (2499, "2K"),
        (2500, "3K"),
        (4000, "3K"),
    ],
)
def test_auto_detect_resolution_thresholds(max_input_dim, expected):
    assert MODULE.auto_detect_resolution(max_input_dim) == expected


def test_choose_size_resolution_only():
    assert MODULE.choose_size("2K", None) == "2K"
    assert MODULE.choose_size("3K", None) == "3K"


def test_choose_size_with_aspect_ratio():
    assert MODULE.choose_size("2K", "9:16") == "1600x2848"
    assert MODULE.choose_size("3K", "1:1") == "3072x3072"
    assert MODULE.choose_size("2K", "16:9") == "2848x1600"


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