Skill: yunhe-dev/openclaw-x-article-cover-generator (version 1.0.2)

--- name: Openclaw-X-article-cover-generator description: Generate OpenClaw-themed X (Twitter) article cover images with fixed composition: lobster logo on right quarter, text zone on left three quarters. --- # Openclaw-X-article-cover-generator 用于给 X（Twitter）文章生成 OpenClaw 主题封面图。 ## 硬性规则（必须遵守） 1. **必须有标题文案才生成** - 若用户未提供标题：先追问标题，不生成。 2. **固定构图** - 龙虾 logo 主体位于画面右侧 1/4。 - 文字区占据画面左侧 3/4。 - 保持参考图中龙虾 logo 主体形象不变。 3. **比例** - 用户要求 5:2 时，API 无 5:2，使用 **21:9** 近似替代。 4. **参考图优先级** - 若用户提供多张参考图，最后明确“以这张为主/主参考图”的那张作为主参考。 - 生成时以主参考图风格与主体形象为最高优先级。 5. **输出定位** - 面向 X 文章封面，优先可读性与留白。 ## 依赖 - 环境变量：`BLT_API_KEY` - API Base URL：`https://api.bltcy.ai` - 模型：`gemini-3.1-flash-image-preview` ## 用法 ```bash uv run ~/.openclaw/workspace/skills/Openclaw-X-article-cover-generator/scripts/generate_cover.py \ --title "你的标题" \ --reference "https://aibuilder.oss-cn-hangzhou.aliyuncs.com/test/openclaw-x-cover-2026-03-02.png" \ --output "cover.png" ``` > `--reference` 现支持两种输入： > 1) 本地文件路径（jpg/png） > 2) http(s) URL（推荐用 OSS 长链接，避免打包缺失 assets） 可选参数： - `--subtitle` 副标题 - `--size` 1K/2K/4K（默认 2K） - `--async-mode` 异步生成 ## 示例提示词（内置自动生成） 脚本会自动构造如下风格提示词： > 按参考图风格生成 OpenClaw X 文章封面图。保持龙虾 logo 主体形象不变，logo 位于画面右侧 1/4。左侧 3/4 为清晰文字区，放置标题与副标题。整体科技感、深色背景、高对比、干净留白、可读性强。

path: assets/README.md
content: # Assets

Note: ClawHub currently may not package binary image files in some publish flows.

Place your primary reference image at:

- `assets/reference-main.jpg`

You can copy your local reference into this path before running generation.

Sample command:

```bash
cp /path/to/your/reference.jpg ~/.openclaw/workspace/skills/Openclaw-X-article-cover-generator/assets/reference-main.jpg
```


path: scripts/generate_cover.py
content: #!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = ["requests>=2.31.0"]
# ///

import argparse
import base64
import os
import sys
import time
from pathlib import Path
import requests

BASE_URL = "https://api.bltcy.ai/v1"
MODEL = "gemini-3.1-flash-image-preview"


def b64_data_uri(path: str) -> str:
    p = Path(path)
    if not p.exists():
        raise FileNotFoundError(path)
    mime = "image/png" if p.suffix.lower() == ".png" else "image/jpeg"
    data = base64.b64encode(p.read_bytes()).decode()
    return f"data:{mime};base64,{data}"


def normalize_reference(ref: str) -> str:
    """Support local file path or http(s) URL as reference image input."""
    if ref.startswith("http://") or ref.startswith("https://"):
        return ref
    return b64_data_uri(ref)


def build_prompt(title: str, subtitle: str | None) -> str:
    text = f"标题：{title}"
    if subtitle:
        text += f"；副标题：{subtitle}"
    return (
        "按参考图风格生成 OpenClaw 的 X（Twitter）文章封面图。"
        "保持龙虾logo主体形象不变。"
        "构图要求：龙虾logo主体位于画面右侧1/4，文字区域占据左侧3/4。"
        "文字要清晰可读、对比强、留白充足。"
        "整体风格：科技感、深色背景、简洁高级。"
        f"请在左侧文字区排版：{text}。"
    )


def post_generation(api_key: str, prompt: str, ref: str, size: str, async_mode: bool):
    url = f"{BASE_URL}/images/generations"
    if async_mode:
        url += "?async=true"
    payload = {
        "model": MODEL,
        "prompt": prompt,
        "aspect_ratio": "21:9",  # 5:2 nearest
        "image_size": size,
        "response_format": "url",
        "image": [ref],
    }
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    r = requests.post(url, headers=headers, json=payload, timeout=180)
    r.raise_for_status()
    return r.json()


def get_task(api_key: str, task_id: str):
    r = requests.get(
        f"{BASE_URL}/images/tasks/{task_id}",
        headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
        timeout=60,
    )
    r.raise_for_status()
    return r.json()


def save_from_url(url: str, out: Path):
    data = requests.get(url, timeout=120).content
    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_bytes(data)


def main():
    ap = argparse.ArgumentParser(description="Generate OpenClaw X article cover")
    ap.add_argument("--title", required=True, help="封面标题（必填）")
    ap.add_argument("--subtitle", help="副标题（可选）")
    ap.add_argument("--reference", required=True, help="主参考图（支持本地路径或 http(s) URL；若多图场景请传用户指定主参考图）")
    ap.add_argument("--output", required=True, help="输出文件名，如 cover.png")
    ap.add_argument("--size", choices=["1K", "2K", "4K"], default="2K")
    ap.add_argument("--async-mode", action="store_true")
    ap.add_argument("--api-key")
    args = ap.parse_args()

    if not args.title.strip():
        print("Error: title is required", file=sys.stderr)
        sys.exit(1)

    [REDACTED_SECRET] or os.environ.get("BLT_API_KEY")
    if not [REDACTED_SECRET]"Error: BLT_API_KEY not found", file=sys.stderr)
        sys.exit(1)

    ref = normalize_reference(args.reference)
    prompt = build_prompt(args.title.strip(), args.subtitle.strip() if args.subtitle else None)

    result = post_generation(api_key, prompt, ref, args.size, args.async_mode)
    out = Path(args.output)

    if args.async_mode:
        task_id = result.get("task_id") or result.get("data")
        if not task_id:
            print(f"Unexpected async response: {result}", file=sys.stderr)
            sys.exit(1)
        print(f"Task ID: {task_id}")
        for _ in range(80):
            s = get_task(api_key, task_id)
            d = s.get("data", {})
            status = d.get("status")
            if status == "SUCCESS":
                url = d.get("data", {}).get("data", [{}])[0].get("url")
                if not url:
                    print(f"No image url: {s}", file=sys.stderr)
                    sys.exit(1)
                save_from_url(url, out)
                print(f"Image saved: {out.resolve()}")
                return
            if status == "FAILURE":
                print(f"Task failed: {d.get('fail_reason','unknown')}", file=sys.stderr)
                sys.exit(1)
            time.sleep(5)
        print("Timeout waiting async task", file=sys.stderr)
        sys.exit(1)
    else:
        data = result.get("data", [{}])
        if isinstance(data, dict):
            data = data.get("data", [{}])
        url = data[0].get("url") if data else None
        if not url:
            print(f"No image url in sync response: {result}", file=sys.stderr)
            sys.exit(1)
        save_from_url(url, out)
        print(f"Image saved: {out.resolve()}")


if __name__ == "__main__":
    main()


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