Skill: zxq790909-maker/wangyi-banana2 (version 1.0.0)

--- name: wangyi-banana description: "Generate images and videos via WangYi Banana API (nano-banana, SORA2). Supports text-to-image, image-to-image, text-to-video, image-to-video, and character creation for short video production." homepage: https://ai.t8star.cn metadata: { "openclaw": { "emoji": "🎨", "requires": { "bins": ["python3", "curl"] }, "primaryEnv": "WANGYI_API_KEY" } } --- # WangYi Banana Skill Standard API Script: `python3 {baseDir}/scripts/wangyi-banana.py` Data: `{baseDir}/data/capabilities.json` ## Persona You are **武镜画家小助手** — a creative AI assistant specializing in image and video generation for short video production. ALL responses MUST follow: - Speak Chinese. Warm & lively: "搞定啦～"、"来啦！"、"超棒的". Never robotic. - Show cost naturally if available: "花了 ¥X.XX" (not "Cost: ¥X.XX"). - Never show internal API URLs to users — use friendly descriptions. - After delivering results, suggest next steps ("要不要做成视频？"、"需要调整一下吗？"). ## CRITICAL RULES 1. **ALWAYS use the script** — never call API directly. 2. **ALWAYS use `-o /tmp/openclaw/wangyi-output/<name>.<ext>`** with timestamps in filenames. 3. **Deliver files via `message` tool** — you MUST call `message` tool to send media. Do NOT print file paths as text. 4. **NEVER show internal API URLs** — all API URLs are internal. Users cannot open them. 5. **NEVER use `![](url)` markdown images or print raw file paths** — ONLY the `message` tool can deliver files to users. 6. **ALWAYS report cost** — if script prints `COST:¥X.XX`, include it in your response as "花了 ¥X.XX". 7. **ALL video generation** → Read `{baseDir}/references/video-generation.md` and follow its complete flow. **ALL image generation** → Read `{baseDir}/references/image-generation.md` and follow its complete flow. WAIT for user choice before running any generation script. 8. **ALWAYS notify before long tasks** — Before running any video generation script, you MUST first use the `message` tool to send a progress notification to the user (e.g. "开始生成啦，视频一般需要几分钟，请稍等～ 🎬"). Send this BEFORE calling `exec`. This is critical because video tasks take 1-10+ minutes and the user needs to know the task has started. ## API Key Setup When user needs to set up or check their API key → Read `{baseDir}/references/api-key-setup.md` and follow its instructions. Quick check: `python3 {baseDir}/scripts/wangyi-banana.py --check` ## Supported Models ### Image Generation Models - `nano-banana-2-2k` - Standard image generation model (recommended) - `nano-banana-2-4k` - 4K HD version - `nano-banana-pro` - Image-to-image editing model - `gemini-2.5-flash-image-preview` - Gemini official model ### Video Generation Models - `sora-2` - Standard SORA2 video generation (10s, 15s) - `sora-2-pro` - Pro version with 25s support ## Routing Table | Intent | Endpoint | Notes | |--------|----------|-------| | **Text to image** | **⚠️ Read `{baseDir}/references/image-generation.md`** | MUST present model menu first | | **Image to image** | **⚠️ Read `{baseDir}/references/image-generation.md`** | MUST present model menu first | | **Text to video** | **⚠️ Read `{baseDir}/references/video-generation.md`** | MUST present model menu first | | **Image to video** | **⚠️ Read `{baseDir}/references/video-generation.md`** | MUST present model menu first | | Character creation | `/sora/v1/characters` | For character cameo feature | ## Script Usage **Execution flow for ALL generation tasks:** 1. **Slow tasks (video):** First send `message` notification → "开始生成啦，视频一般需要几分钟，请稍等～" → then `exec` the script 2. **Fast tasks (image):** Directly `exec` the script (notification optional) ### Image Generation ```bash python3 {baseDir}/scripts/wangyi-banana.py \ --task text-to-image \ --prompt "prompt text" \ --model nano-banana \ --aspect-ratio 4:3 \ --output /tmp/openclaw/wangyi-output/image_$(date +%s).png ``` For image-to-image: ```bash python3 {baseDir}/scripts/wangyi-banana.py \ --task image-to-image \ --prompt "prompt text" \ --image /path/to/image.png \ --model nano-banana-edit \ --output /tmp/openclaw/wangyi-output/edited_$(date +%s).png ``` ### Video Generation ```bash python3 {baseDir}/scripts/wangyi-banana.py \ --task text-to-video \ --prompt "prompt text" \ --model sora-2 \ --duration 10 \ --aspect-ratio 16:9 \ --output /tmp/openclaw/wangyi-output/video_$(date +%s).mp4 ``` For image-to-video: ```bash python3 {baseDir}/scripts/wangyi-banana.py \ --task image-to-video \ --prompt "prompt text" \ --image /path/to/image.png \ --model sora-2 \ --duration 10 \ --aspect-ratio 16:9 \ --output /tmp/openclaw/wangyi-output/video_$(date +%s).mp4 ``` ### Character Creation ```bash python3 {baseDir}/scripts/wangyi-banana.py \ --task create-character \ --url "video_url" \ --timestamps "1,3" \ --output /tmp/openclaw/wangyi-output/character_$(date +%s).json ``` Optional flags: - `--host-url URL` - API host URL (default: https://ai.t8star.cn) - `--api-key KEY` - API key (or use WANGYI_API_KEY env var) - `--model MODEL` - Model name - `--aspect-ratio RATIO` - Aspect ratio (4:3, 16:9, 9:16, etc.) - `--duration DURATION` - Video duration (10, 15, 25 for sora-2-pro) - `--hd` - Enable HD mode for video - `--watermark` - Enable watermark - `--private` - Private video mode Discovery: `--list`, `--info TASK` ## Output For media delivery and error handling details → Read `{baseDir}/references/output-delivery.md`. Key rules (always apply): - ALWAYS call `message` tool to deliver media files, then respond `NO_REPLY`. - If `message` fails, retry once. If still fails, include `OUTPUT_FILE:<path>` and explain. - Print text results directly. Include cost if `COST:` line present. ## Backup API Hosts The script automatically tries multiple backup hosts if the primary fails: - https://ai.t8star.cn (primary) - http://104.194.8.112:9088 (backup 1) - https://hk-api.gptbest.vip (backup 2) - https://api.gptbest.vip (backup 3)

path: data/capabilities.json
content: {
  "version": "2024-01-01",
  "total": 5,
  "endpoints": [
    {
      "endpoint": "text-to-image",
      "name_cn": "文生图",
      "name_en": "Text to Image",
      "task": "text-to-image",
      "output_type": "image",
      "category": "image",
      "popularity": 1,
      "tags": ["image", "generation"],
      "params": [
        {
          "key": "prompt",
          "type": "STRING",
          "required": true
        },
        {
          "key": "model",
          "type": "STRING",
          "required": false,
          "default": "nano-banana",
          "options": ["nano-banana", "nano-banana-hd", "gemini-2.5-flash-image-preview"]
        },
        {
          "key": "aspect_ratio",
          "type": "STRING",
          "required": false,
          "default": "4:3",
          "options": ["4:3", "3:4", "16:9", "9:16", "2:3", "3:2"]
        }
      ]
    },
    {
      "endpoint": "image-to-image",
      "name_cn": "图生图",
      "name_en": "Image to Image",
      "task": "image-to-image",
      "output_type": "image",
      "category": "image",
      "popularity": 2,
      "tags": ["image", "edit"],
      "params": [
        {
          "key": "prompt",
          "type": "STRING",
          "required": true
        },
        {
          "key": "image",
          "type": "IMAGE",
          "required": true
        },
        {
          "key": "model",
          "type": "STRING",
          "required": false,
          "default": "nano-banana-edit"
        }
      ]
    },
    {
      "endpoint": "text-to-video",
      "name_cn": "文生视频",
      "name_en": "Text to Video",
      "task": "text-to-video",
      "output_type": "video",
      "category": "video",
      "popularity": 1,
      "tags": ["video", "generation", "sora"],
      "params": [
        {
          "key": "prompt",
          "type": "STRING",
          "required": true
        },
        {
          "key": "model",
          "type": "STRING",
          "required": false,
          "default": "sora-2",
          "options": ["sora-2", "sora-2-pro"]
        },
        {
          "key": "duration",
          "type": "STRING",
          "required": false,
          "default": "10",
          "options": ["10", "15", "25"]
        },
        {
          "key": "aspect_ratio",
          "type": "STRING",
          "required": false,
          "default": "16:9",
          "options": ["16:9", "9:16"]
        },
        {
          "key": "hd",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        },
        {
          "key": "watermark",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        },
        {
          "key": "private",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        }
      ]
    },
    {
      "endpoint": "image-to-video",
      "name_cn": "图生视频",
      "name_en": "Image to Video",
      "task": "image-to-video",
      "output_type": "video",
      "category": "video",
      "popularity": 2,
      "tags": ["video", "generation", "sora"],
      "params": [
        {
          "key": "prompt",
          "type": "STRING",
          "required": true
        },
        {
          "key": "image",
          "type": "IMAGE",
          "required": true
        },
        {
          "key": "model",
          "type": "STRING",
          "required": false,
          "default": "sora-2",
          "options": ["sora-2", "sora-2-pro"]
        },
        {
          "key": "duration",
          "type": "STRING",
          "required": false,
          "default": "10",
          "options": ["10", "15", "25"]
        },
        {
          "key": "aspect_ratio",
          "type": "STRING",
          "required": false,
          "default": "16:9",
          "options": ["16:9", "9:16"]
        },
        {
          "key": "hd",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        },
        {
          "key": "watermark",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        },
        {
          "key": "private",
          "type": "BOOLEAN",
          "required": false,
          "default": false
        },
        {
          "key": "character_url",
          "type": "STRING",
          "required": false
        },
        {
          "key": "character_timestamps",
          "type": "STRING",
          "required": false,
          "default": "1,3"
        }
      ]
    },
    {
      "endpoint": "create-character",
      "name_cn": "创建角色",
      "name_en": "Create Character",
      "task": "create-character",
      "output_type": "string",
      "category": "character",
      "popularity": 3,
      "tags": ["character", "cameo"],
      "params": [
        {
          "key": "url",
          "type": "STRING",
          "required": true
        },
        {
          "key": "timestamps",
          "type": "STRING",
          "required": true
        }
      ]
    }
  ]
}


path: install.md
content: # WangYi Banana - OPENCLAW Skill

武镜画家（短剧制作机）的 OPENCLAW SKILLS 版本，支持图片和视频生成。

## 🚀 快速开始

### 1. 配置 API Key（贞贞工坊）

贞贞工坊注册指南：比官方价格低80%

获取APIKEY后，在右侧填入
https://ai.t8star.cn/ 获取API地址

注册送0.2，每日签到也送

国外低价SORA2基本废了，10次请求可能只有一次生成不玩也罢，大家还是去白嫖SEEDANCE2.0吧

QQ交流群：386692323

先注册贞贞的AI工坊：点这里?

注册后，创建令牌，然后复制密钥，粘贴到右边的apikey这里，当然要充值充值后才能使用哈^-^

需要注册T8的贞贞工坊才能生图和生视频哈！

注册地址：https://ai.t8star.cn/register?aff=253d8a45349

1、注册后登陆，点击左侧的令牌，创建API KEY，然后复制它

2、左侧点击钱包，充值个5美刀的算力，按这里的汇率只要6.75人民币

3、一次充5美刀算力，即6.75人民币即可！额度用完后再充

4、找到工作流中，有apikey的节点，粘贴刚才复制的api key

5、开始生成视频，可以文生视频，也可以图生视频，短剧的话，可以豆包生成分镜脚本，即梦免费出图

6、SORA2文生或图生视频，不得上传低俗擦边和真人图片，切记，不然会生成失败

7、如果生成失败是不会扣费的，可以自己查看日志和钱包



#### 方法一：使用配置脚本（最简单）

在正常运行的OPENCLAW中，开启聊天，把你的APIKEY直接发给小龙虾，让他帮你配置好，就可以用了
```

#### 方法二：手动编辑配置文件

1. 打开配置文件：`C:\Users\你的用户名\.openclaw\openclaw.json`
2. 如果文件不存在，创建它并添加：

```json
{
  "skills": {
    "entries": {
      "wangyi-banana": {
        "apiKey": "your_api_key_here"
      }
    }
  }
}
```

#### 方法三：设置环境变量

在 PowerShell 中：

```powershell
$env:WANGYI_API_KEY = "your_api_key_here"
```

### 2. 验证配置

```powershell
python3 scripts\wangyi-banana.py --check
```

如果配置正确，会显示：

```json
{
  "status": "ready",
  "key_prefix": "xxxx****",
  "host": "https://ai.t8star.cn",
  "message": "API key is valid"
}
```

## 📖 使用示例

### 生成图片

```powershell
python3 scripts\wangyi-banana.py `
  --task text-to-image `
  --prompt "一只可爱的橘猫" `
  --model nano-banana `
  --aspect-ratio 4:3 `
  -o output.png
```

### 生成视频

```powershell
python3 scripts\wangyi-banana.py `
  --task text-to-video `
  --prompt "一只猫在窗台上走动" `
  --model sora-2 `
  --duration 10 `
  --aspect-ratio 16:9 `
  -o output.mp4
```

## 📚 更多文档

- [API Key 设置指南](references/api-key-setup.md)
- [图片生成指南](references/image-generation.md)
- [视频生成指南](references/video-generation.md)
- [输出交付指南](references/output-delivery.md)

## 🔧 支持的功能

- ✅ 文生图（Text-to-Image）
- ✅ 图生图（Image-to-Image）
- ✅ 文生视频（Text-to-Video）
- ✅ 图生视频（Image-to-Video）
- ✅ 角色创建（Character Creation）

## 📝 支持的模型

### 图片模型
- `nano-banana-2-2k` - Standard image generation model (recommended)
- `nano-banana-2-4k` - 4K HD version
- `nano-banana-pro` - Image-to-image editing model
- `gemini-2.5-flash-image-preview` - Gemini official model

### 视频模型
- `sora-2` - 标准版本（10s, 15s）
- `sora-2-pro` - Pro 版本（10s, 15s, 25s）

## 🌐 API 地址

脚本会自动尝试以下备份地址：
1. https://ai.t8star.cn（主地址）
2. http://104.194.8.112:9088（备份1）
3. https://hk-api.gptbest.vip（备份2）
4. https://api.gptbest.vip（备份3）

## 📞 获取 API Key

请联系您的 WangYi Banana 服务提供商获取 API Key。


path: references/api-key-setup.md
content: # API Key 设置指南

## 获取 API Key

1. 联系您的 WangYi Banana 服务提供商获取 API Key
2. API Key 格式通常为字符串，请妥善保管

## 配置 API Key

### 方法一：通过 OpenClaw 配置文件（推荐）

**Windows 系统：**
配置文件位置：`C:\Users\你的用户名\.openclaw\openclaw.json`

**Linux/Mac 系统：**
配置文件位置：`~/.openclaw/openclaw.json`

**步骤：**
1. 如果 `.openclaw` 目录不存在，先创建它
2. 创建或编辑 `openclaw.json` 文件
3. 添加以下配置（如果文件已存在，合并到现有配置中）：

```json
{
  "skills": {
    "entries": {
      "wangyi-banana": {
        "apiKey": "your_api_key_here"
      }
    }
  }
}
```

**Windows PowerShell 快速创建：**
```powershell
# 创建目录（如果不存在）
$configDir = "$env:USERPROFILE\.openclaw"
if (-not (Test-Path $configDir)) {
    New-Item -ItemType Directory -Path $configDir -Force
}

# 创建配置文件
$configFile = "$configDir\openclaw.json"
@{
    skills = @{
        entries = @{
            "wangyi-banana" = @{
                [REDACTED_SECRET]"
            }
        }
    }
} | ConvertTo-Json -Depth 10 | Set-Content -Path $configFile -Encoding UTF8
```

### 方法二：通过环境变量

**Windows PowerShell：**
```powershell
$env:WANGYI_API_KEY = "your_api_key_here"
```

**Windows CMD：**
```cmd
set WANGYI_API_KEY=your_api_key_here
```

**Linux/Mac：**
```bash
export WANGYI_API_KEY="your_api_key_here"
```

**永久设置环境变量（Windows）：**
1. 右键"此电脑" → "属性"
2. "高级系统设置" → "环境变量"
3. 在"用户变量"中添加：
   - 变量名：`WANGYI_API_KEY`
   - 变量值：`your_api_key_here`

### 方法三：在对话中直接提供

您可以在对话中直接提供 API Key，系统会临时使用。例如：
- "我的 API Key 是：sk-xxxxx"
- "设置 API Key：your_key_here"

## 验证 API Key

运行以下命令验证 API Key 是否有效：

```bash
python3 {baseDir}/scripts/wangyi-banana.py --check
```

如果配置正确，您会看到：

```json
{
  "status": "ready",
  "key_prefix": "xxxx****",
  "host": "https://ai.t8star.cn",
  "message": "API key is valid"
}
```

## 故障排除

### API Key 无效

- 检查 API Key 是否正确复制（注意前后空格）
- 确认 API Key 是否已过期
- 联系服务提供商确认账户状态

### 无法连接

- 检查网络连接
- 确认 API 服务是否正常运行
- 脚本会自动尝试多个备份地址


path: references/image-generation.md
content: # 图片生成指南

## 支持的模型

### 文生图模型
- **nano-banana** - 标准图片生成模型（推荐）
- **nano-banana-hd** - 4K 高清版本
- **gemini-2.5-flash-image-preview** - Gemini 官方模型

### 图生图模型
- **nano-banana-edit** - 图片编辑模型

## 图片比例选项

- `4:3` - 横屏比例（推荐）
- `3:4` - 竖屏比例
- `16:9` - 宽屏比例
- `9:16` - 手机屏比例
- `2:3` - 竖屏比例
- `3:2` - 横屏比例

## 使用流程

### 文生图

1. **选择模型** - 根据需求选择合适的模型
2. **输入提示词** - 详细描述您想要的图片内容
3. **选择比例** - 根据用途选择合适的长宽比
4. **生成图片** - 等待 AI 处理完成

### 图生图

1. **上传参考图片** - 选择一张参考图片
2. **输入提示词** - 描述您想要的修改或效果
3. **生成图片** - 等待 AI 处理完成

## 提示词技巧

### 描述主体
- 人物、动物、物体等主要元素

### 添加风格
- 写实、卡通、油画、水彩、科幻等

### 指定场景
- 室内、户外、特定地点、背景环境

### 描述光线
- 明亮、昏暗、夕阳、月光、自然光等

### 添加情感
- 快乐、神秘、温馨、科幻、浪漫等

## 示例

### 文生图示例

```bash
python3 {baseDir}/scripts/wangyi-banana.py \
  --task text-to-image \
  --prompt "一只可爱的橘猫坐在窗台上，阳光透过窗户洒在它身上，写实风格，4K高清" \
  --model nano-banana \
  --aspect-ratio 4:3 \
  -o /tmp/openclaw/wangyi-output/cat.png
```

### 图生图示例

```bash
python3 {baseDir}/scripts/wangyi-banana.py \
  --task image-to-image \
  --prompt "将背景改为夜晚，添加星空" \
  --image /path/to/image.png \
  --model nano-banana-edit \
  -o /tmp/openclaw/wangyi-output/edited.png
```

## 注意事项

- 图片生成通常需要 10-30 秒
- 高清模型生成时间可能更长
- 提示词越详细，生成效果越好
- 支持多种图片格式输出（PNG, JPG 等）


path: references/output-delivery.md
content: # 输出交付指南

## 文件输出

所有生成的文件都会保存到 `/tmp/openclaw/wangyi-output/` 目录，文件名包含时间戳。

## 输出格式

### 图片输出
- 格式：PNG、JPG 等
- 路径格式：`/tmp/openclaw/wangyi-output/image_<timestamp>.png`

### 视频输出
- 格式：MP4
- 路径格式：`/tmp/openclaw/wangyi-output/video_<timestamp>.mp4`

### 文本输出
- 格式：JSON（用于角色创建等）
- 路径格式：`/tmp/openclaw/wangyi-output/character_<timestamp>.json`

## 交付方式

### 使用 message 工具

**重要**：所有媒体文件必须通过 `message` 工具交付给用户，不要直接打印文件路径。

```python
# 正确方式
message(file_path="/tmp/openclaw/wangyi-output/image_1234567890.png")
# 然后响应 NO_REPLY

# 错误方式
print("文件已保存到: /tmp/openclaw/wangyi-output/image_1234567890.png")
```

### 重试机制

如果 `message` 工具调用失败：
1. 重试一次
2. 如果仍然失败，在响应中包含 `OUTPUT_FILE:<path>` 并解释情况

## 错误处理

### 常见错误

1. **NO_API_KEY** - API Key 未配置
   - 解决方案：配置 API Key（参考 api-key-setup.md）

2. **API_ERROR** - API 请求失败
   - 解决方案：检查网络连接，脚本会自动尝试备份地址

3. **TASK_FAILED** - 任务执行失败
   - 解决方案：检查提示词和参数，重试

4. **DOWNLOAD_FAILED** - 文件下载失败
   - 解决方案：检查网络连接，重试

### 错误响应格式

脚本会输出 JSON 格式的错误信息：

```json
{
  "error": "ERROR_TYPE",
  "message": "错误描述",
  "details": {}
}
```

## 成本报告

如果 API 返回成本信息，脚本会输出：

```
COST:¥X.XX
```

在响应中应包含："花了 ¥X.XX"

## 最佳实践

1. **总是使用 message 工具** - 不要直接打印文件路径
2. **检查输出文件** - 确认文件已成功生成
3. **提供友好提示** - 告诉用户文件已生成
4. **建议下一步** - 例如"要不要做成视频？"


path: references/video-generation.md
content: # 视频生成指南

## 支持的模型

- **sora-2** - 标准 SORA2 视频生成模型（支持 10s、15s）
- **sora-2-pro** - Pro 版本（支持 10s、15s、25s）

## 视频参数

### 时长选项
- `10` - 10 秒视频（所有模型支持）
- `15` - 15 秒视频（所有模型支持）
- `25` - 25 秒视频（仅 sora-2-pro 支持）

### 比例选项
- `16:9` - 横屏比例（推荐）
- `9:16` - 竖屏比例（适合手机）

### 其他选项
- `--hd` - 启用高清模式
- `--watermark` - 添加水印
- `--private` - 私有视频模式

## 使用流程

### 文生视频

1. **选择模型** - 根据需求选择 sora-2 或 sora-2-pro
2. **输入提示词** - 详细描述您想要的视频内容
3. **设置参数** - 选择时长、比例等
4. **生成视频** - 等待 AI 处理完成（通常需要几分钟）

### 图生视频

1. **上传参考图片** - 选择一张参考图片
2. **输入提示词** - 描述视频的动作和效果
3. **设置参数** - 选择时长、比例等
4. **生成视频** - 等待 AI 处理完成

### 角色客串功能

1. **创建角色** - 先使用 create-character 任务创建角色
2. **生成视频** - 在生成视频时使用 `--character-url` 参数
3. **设置时间范围** - 使用 `--character-timestamps` 指定角色出现的时间段

## 提示词技巧

### 描述动作
- 明确描述主体要执行的动作
- 例如："一只猫从左边走到右边"

### 描述场景
- 详细描述背景环境
- 例如："在阳光明媚的公园里"

### 描述镜头
- 可以描述镜头运动
- 例如："缓慢推进"、"从远到近"

### 描述风格
- 指定视频风格
- 例如："电影质感"、"动画风格"

## 示例

### 文生视频示例

```bash
python3 {baseDir}/scripts/wangyi-banana.py \
  --task text-to-video \
  --prompt "一只可爱的橘猫在窗台上慢慢走动，阳光透过窗户洒在它身上，写实风格，电影质感" \
  --model sora-2 \
  --duration 10 \
  --aspect-ratio 16:9 \
  --hd \
  -o /tmp/openclaw/wangyi-output/cat_video.mp4
```

### 图生视频示例

```bash
python3 {baseDir}/scripts/wangyi-banana.py \
  --task image-to-video \
  --prompt "图片中的猫开始慢慢走动，镜头缓慢推进" \
  --image /path/to/image.png \
  --model sora-2-pro \
  --duration 15 \
  --aspect-ratio 16:9 \
  -o /tmp/openclaw/wangyi-output/video.mp4
```

### 带角色客串的视频

```bash
# 先创建角色
python3 {baseDir}/scripts/wangyi-banana.py \
  --task create-character \
  --url "https://example.com/character_video.mp4" \
  --timestamps "1,3"

# 然后生成带角色的视频
python3 {baseDir}/scripts/wangyi-banana.py \
  --task text-to-video \
  --prompt "一个场景，角色在1-3秒出现" \
  --model sora-2 \
  --character-url "character_url_from_previous_step" \
  --character-timestamps "1,3" \
  -o /tmp/openclaw/wangyi-output/video_with_character.mp4
```

## 注意事项

- 视频生成通常需要 3-10 分钟
- 25 秒视频仅支持 sora-2-pro 模型
- 视频生成过程中会显示进度
- 建议使用高清模式获得更好效果
- 角色客串功能需要先创建角色

## 任务状态

视频生成是异步任务，会经历以下状态：
- `SUBMITTED` - 已提交
- `QUEUED` - 排队中
- `IN_PROGRESS` - 处理中
- `SUCCESS` - 完成
- `FAILURE` - 失败

脚本会自动轮询任务状态直到完成。


path: scripts/wangyi-banana.py
content: #!/usr/bin/env python3
"""
WangYi Banana API client for OpenClaw.

Supports image and video generation via nano-banana and SORA2 APIs.
Uses only Python stdlib and curl.

Modes:
  --check                          Account health check (key + balance)
  --list                           List available tasks
  --info TASK                      Show task details
  --task TASK --prompt "..." ...   Execute a generation task
"""

from __future__ import annotations

import argparse
import base64
import json
import mimetypes
import os
import subprocess
import sys
import tempfile
import time
from pathlib import Path

# API host（只使用主地址，避免切到你没有权限的备份域名）
DEFAULT_HOST = "https://ai.t8star.cn"
MAX_POLL_SECONDS = 1800  # 30 minutes for video
POLL_INTERVAL = 5

SCRIPT_DIR = Path(__file__).resolve().parent
DATA_DIR = SCRIPT_DIR.parent / "data"
CAPABILITIES_PATH = DATA_DIR / "capabilities.json"


# ---------------------------------------------------------------------------
# API key resolution
# ---------------------------------------------------------------------------

def read_key_from_openclaw_config() -> str | None:
    cfg_path = Path.home() / ".openclaw" / "openclaw.json"
    if not cfg_path.exists():
        return None
    try:
        cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
    except Exception:
        return None
    entry = cfg.get("skills", {}).get("entries", {}).get("wangyi-banana", {})
    [REDACTED_SECRET]"apiKey")
    if isinstance(api_key, str) and api_key.strip():
        return api_key.strip()
    env_val = entry.get("env", {}).get("WANGYI_API_KEY")
    if isinstance(env_val, str) and env_val.strip():
        return env_val.strip()
    return None


def resolve_api_key(provided_key: str | None) -> str | None:
    """Resolve API key without exiting. Returns None if not found."""
    if provided_key:
        normalized = provided_key.strip()
        placeholders = {
            "your_api_key_here", "<your_api_key>",
            "YOUR_API_KEY", "WANGYI_API_KEY",
        }
        if normalized and normalized not in placeholders:
            return normalized

    env_key = os.environ.get("WANGYI_API_KEY", "").strip()
    if env_key:
        return env_key

    return read_key_from_openclaw_config()


def require_api_key(provided_key: str | None) -> str:
    key = resolve_api_key(provided_key)
    if key:
        return key
    result = {
        "error": "NO_API_KEY",
        "message": "No API key configured",
        "steps": [
            "1. Get API key from your WangYi Banana service provider",
            "2. Send the key in chat or add to ~/.openclaw/openclaw.json: skills.entries.wangyi-banana.apiKey",
        ],
    }
    print(json.dumps(result, ensure_ascii=False))
    sys.exit(1)


# ---------------------------------------------------------------------------
# HTTP helpers (curl-based, stdlib only)
# ---------------------------------------------------------------------------

def curl_post_json(url: str, payload: dict, headers: dict, timeout: int = 60) -> subprocess.CompletedProcess:
    with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
        json.dump(payload, f)
        tmp_path = f.name
    try:
        cmd = ["curl", "-s", "-S", "--fail-with-body", "-X", "POST", url,
               "--max-time", str(timeout), "-d", f"@{tmp_path}"]
        for k, v in headers.items():
            cmd += ["-H", f"{k}: {v}"]
        return subprocess.run(cmd, capture_output=True, text=True)
    finally:
        os.unlink(tmp_path)


def curl_post_form_data(url: str, form_data: dict, headers: dict, timeout: int = 120) -> subprocess.CompletedProcess:
    """Post multipart/form-data using curl.

    支持直接传入 Path / 文件路径，会自动按文件处理：
    - key=Path(...) 或 key=\"C:/path/to/file.png\" => -F key=@C:/path/to/file.png
    """
    cmd = ["curl", "-s", "-S", "--fail-with-body", "-X", "POST", url,
           "--max-time", str(timeout)]
    for k, v in headers.items():
        cmd += ["-H", f"{k}: {v}"]

    for key, value in form_data.items():
        # 多值（暂时不需要文件列表，这里保持简单）
        if isinstance(value, (list, tuple)):
            for item in value:
                if isinstance(item, Path):
                    cmd += ["-F", f"{key}=@{str(item)}"]
                elif isinstance(item, str) and os.path.exists(item):
                    cmd += ["-F", f"{key}=@{item}"]
                else:
                    cmd += ["-F", f"{key}={item}"]
        else:
            # 单值：优先按文件处理
            if isinstance(value, Path):
                cmd += ["-F", f"{key}=@{str(value)}"]
            elif isinstance(value, str) and os.path.exists(value):
                cmd += ["-F", f"{key}=@{value}"]
            else:
                cmd += ["-F", f"{key}={value}"]

    return subprocess.run(cmd, capture_output=True, text=True)


def api_request_with_backup(api_key: str, endpoint_suffix: str, payload: dict,
                            method: str = "POST", timeout: int = 60,
                            is_form_data: bool = False) -> dict:
    """单一主机请求封装（历史上支持多备份，这里只用 DEFAULT_HOST）。

    这样可以避免脚本自动切到你没有权限的其它域名（比如 api.gptbest.vip）。
    """
    url = f"{DEFAULT_HOST}{endpoint_suffix}"
    headers = {
        "Authorization": f"Bearer {api_key}",
    }

    if not is_form_data:
        headers["Content-Type"] = "application/json"

    max_retries = 3
    last_error: dict | None = None

    for retry in range(max_retries):
        try:
            if is_form_data:
                result = curl_post_form_data(url, payload, headers, timeout)
            else:
                if method == "POST":
                    result = curl_post_json(url, payload, headers, timeout)
                else:  # GET
                    cmd = [
                        "curl", "-s", "-S", "--fail-with-body", "-X", "GET", url,
                        "--max-time", str(timeout),
                    ]
                    for k, v in headers.items():
                        cmd += ["-H", f"{k}: {v}"]
                    result = subprocess.run(cmd, capture_output=True, text=True)

            if result.returncode == 0:
                try:
                    return json.loads(result.stdout)
                except json.JSONDecodeError:
                    print(f"Error: Invalid JSON response from {url}", file=sys.stderr)
                    print(f"Response: {result.stdout[:500]}", file=sys.stderr)
                    sys.exit(1)
            else:
                error_body = result.stdout or result.stderr
                try:
                    err = json.loads(error_body)
                    msg = err.get("msg", error_body)
                except (json.JSONDecodeError, TypeError):
                    msg = error_body

                last_error = {
                    "error": "API_ERROR",
                    "message": f"API request failed: {msg}",
                    "host": DEFAULT_HOST,
                    "url": url,
                }

        except Exception as e:
            last_error = {
                "error": "REQUEST_ERROR",
                "message": str(e),
                "host": DEFAULT_HOST,
            }

        if retry < max_retries - 1:
            wait_time = 1000 * (retry + 1)
            time.sleep(wait_time / 1000)
            continue

    # 所有重试失败
    if last_error:
        print(json.dumps(last_error, ensure_ascii=False), file=sys.stderr)
    else:
        print(json.dumps({
            "error": "ALL_HOSTS_FAILED",
            "message": "Primary host failed to respond"
        }, ensure_ascii=False), file=sys.stderr)
    sys.exit(1)


# ---------------------------------------------------------------------------
# --check: account health check
# ---------------------------------------------------------------------------

def cmd_check(api_key_arg: str | None, host_url: str | None):
    key = resolve_api_key(api_key_arg)
    host = host_url or DEFAULT_HOST
    
    if not key:
        print(json.dumps({
            "status": "no_key",
            "message": "No API key configured",
            "steps": [
                "1. Get API key from your WangYi Banana service provider",
                "2. Send the key in chat or add to ~/.openclaw/openclaw.json: skills.entries.wangyi-banana.apiKey",
            ],
        }, ensure_ascii=False))
        return
    
    key_prefix = key[:4] + "****"
    
    # Try to check API by making a simple request
    try:
        # Use models endpoint as health check
        resp = api_request_with_backup(key, "/v1/models", {}, method="GET", timeout=10)
        print(json.dumps({
            "status": "ready",
            "key_prefix": key_prefix,
            "host": host,
            "message": "API key is valid"
        }, ensure_ascii=False))
    except SystemExit:
        print(json.dumps({
            "status": "invalid_key",
            "key_prefix": key_prefix,
            "host": host,
            "message": "API key verification failed"
        }, ensure_ascii=False))


# ---------------------------------------------------------------------------
# Media handling
# ---------------------------------------------------------------------------

def image_to_data_uri(file_path: str) -> str:
    mime_type = mimetypes.guess_type(file_path)[0] or "image/png"
    with open(file_path, "rb") as f:
        encoded = base64.b64encode(f.read()).decode()
    return f"data:{mime_type};base64,{encoded}"


# ---------------------------------------------------------------------------
# Image generation
# ---------------------------------------------------------------------------

def cmd_generate_image(args):
    """Generate image (text-to-image or image-to-image)."""
    [REDACTED_SECRET])
    host = args.host_url or DEFAULT_HOST
    
    if not args.prompt:
        print("Error: --prompt is required", file=sys.stderr)
        sys.exit(1)
    
    model = args.model or "nano-banana"
    aspect_ratio = args.aspect_ratio or "4:3"
    
    # Check if it's image-to-image mode
    is_edit = model == "nano-banana-edit" or args.image
    
    if is_edit and not args.image:
        print("Error: --image is required for image-to-image mode", file=sys.stderr)
        sys.exit(1)
    
    # Build request payload
    if is_edit:
        # Image-to-image using /v1/images/edits
        endpoint = "/v1/images/edits"
        form_data = {
            "model": "nano-banana",
            "prompt": args.prompt,
            "response_format": "url"
        }
        
        # Add image file
        image_path = Path(args.image)
        if not image_path.exists():
            print(f"Error: image file not found: {args.image}", file=sys.stderr)
            sys.exit(1)
        
        # For form data, we need to pass file path
        form_data["image"] = image_path
        
        resp = api_request_with_backup(api_key, endpoint, form_data, is_form_data=True, timeout=120)
    elif "gemini" in model.lower():
        # Gemini model uses /v1/chat/completions
        endpoint = "/v1/chat/completions"
        content_array = [{"type": "text", "text": args.prompt}]
        
        if args.image:
            image_path = Path(args.image)
            if not image_path.exists():
                print(f"Error: image file not found: {args.image}", file=sys.stderr)
                sys.exit(1)
            image_data_uri = image_to_data_uri(str(image_path))
            content_array.append({
                "type": "image_url",
                "image_url": {"url": image_data_uri}
            })
        
        payload = {
            "model": model,
            "stream": False,
            "messages": [{"role": "user", "content": content_array}],
            "max_tokens": 4000
        }
        
        resp = api_request_with_backup(api_key, endpoint, payload, timeout=120)
        
        # Extract image URL from response
        image_url = None
        if resp.get("choices") and len(resp["choices"]) > 0:
            content = resp["choices"][0].get("message", {}).get("content", "")
            if isinstance(content, str):
                # Try to extract URL from text
                import re
                url_match = re.search(r'https?://[^\s"\'<>]+', content)
                if url_match:
                    image_url = url_match.group(0)
            elif isinstance(content, list):
                for item in content:
                    if item.get("type") == "image_url":
                        image_url = item.get("image_url", {}).get("url")
                        break
        
        if not image_url:
            print("Error: Could not extract image URL from response", file=sys.stderr)
            print(json.dumps(resp, indent=2, ensure_ascii=False), file=sys.stderr)
            sys.exit(1)
        
        # Download image
        output_path = args.output or f"/tmp/openclaw/wangyi-output/image_{int(time.time())}.png"
        Path(output_path).parent.mkdir(parents=True, exist_ok=True)
        cmd = ["curl", "-s", "-S", "-L", "-o", output_path, "--max-time", "300", image_url]
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            print(f"Download failed: {result.stderr}", file=sys.stderr)
            sys.exit(1)
        
        print(f"OUTPUT_FILE:{Path(output_path).resolve()}")
        return
    else:
        # Text-to-image using /v1/images/generations
        endpoint = "/v1/images/generations"
        payload = {
            "model": model,
            "prompt": args.prompt,
            "aspect_ratio": aspect_ratio,
            "response_format": "url",
            "n": 1
        }
        
        if args.image:
            image_path = Path(args.image)
            if not image_path.exists():
                print(f"Error: image file not found: {args.image}", file=sys.stderr)
                sys.exit(1)
            image_data_uri = image_to_data_uri(str(image_path))
            payload["image_urls"] = [image_data_uri]
        
        resp = api_request_with_backup(api_key, endpoint, payload, timeout=120)
    
    # Extract image URL from response
    image_url = None
    if resp.get("data") and len(resp["data"]) > 0:
        image_url = resp["data"][0].get("url")
    elif resp.get("url"):
        image_url = resp["url"]
    
    if not image_url:
        print("Error: No image URL in response", file=sys.stderr)
        print(json.dumps(resp, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
    
    # Download image
    output_path = args.output or f"/tmp/openclaw/wangyi-output/image_{int(time.time())}.png"
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    cmd = ["curl", "-s", "-S", "-L", "-o", output_path, "--max-time", "300", image_url]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Download failed: {result.stderr}", file=sys.stderr)
        sys.exit(1)
    
    print(f"OUTPUT_FILE:{Path(output_path).resolve()}")


# ---------------------------------------------------------------------------
# Video generation
# ---------------------------------------------------------------------------

def poll_video_task(api_key: str, task_id: str, host: str) -> dict:
    """Poll video generation task until completion."""
    endpoint = f"/v2/videos/generations/{task_id}"
    print(f"Task ID: {task_id}", file=sys.stderr)
    print("Waiting for result", end="", flush=True, file=sys.stderr)
    
    elapsed = 0
    poll_delay = 2  # Start with 2 seconds
    
    while elapsed < MAX_POLL_SECONDS:
        time.sleep(poll_delay)
        elapsed += poll_delay
        
        resp = api_request_with_backup(api_key, endpoint, {}, method="GET", timeout=30)
        
        status = resp.get("status", "UNKNOWN")
        progress = resp.get("progress", "0%")
        
        print(".", end="", flush=True, file=sys.stderr)
        
        if status == "SUCCESS":
            print(f" done ({elapsed}s)", file=sys.stderr)
            return resp
        elif status == "FAILURE":
            fail_reason = resp.get("fail_reason", "Unknown error")
            print(f"\nTask failed: {fail_reason}", file=sys.stderr)
            sys.exit(1)
        elif status in ("NOT_START", "SUBMITTED", "QUEUED", "IN_PROGRESS"):
            # Exponential backoff, max 16 seconds
            poll_delay = min(poll_delay * 2, 16)
        else:
            # Unknown status, continue polling
            poll_delay = min(poll_delay * 2, 16)
    
    print(f"\nTimeout after {MAX_POLL_SECONDS}s", file=sys.stderr)
    sys.exit(1)


def cmd_generate_video(args):
    """Generate video (text-to-video or image-to-video)."""
    [REDACTED_SECRET])
    host = args.host_url or DEFAULT_HOST
    
    if not args.prompt:
        print("Error: --prompt is required", file=sys.stderr)
        sys.exit(1)
    
    model = args.model or "sora-2"
    duration = args.duration or "10"
    aspect_ratio = args.aspect_ratio or "16:9"
    
    # Validate parameters
    valid_models = ["sora-2", "sora-2-pro"]
    if model not in valid_models:
        model = "sora-2"
        print(f"Warning: Invalid model, using sora-2", file=sys.stderr)
    
    valid_durations = ["10", "15", "25"]
    if duration not in valid_durations:
        duration = "10"
        print(f"Warning: Invalid duration, using 10", file=sys.stderr)
    
    if duration == "25" and model != "sora-2-pro":
        duration = "15"
        print(f"Warning: 25s only supported by sora-2-pro, using 15s", file=sys.stderr)
    
    valid_aspect_ratios = ["16:9", "9:16"]
    if aspect_ratio not in valid_aspect_ratios:
        aspect_ratio = "16:9"
        print(f"Warning: Invalid aspect ratio, using 16:9", file=sys.stderr)
    
    # Build request payload
    payload = {
        "prompt": args.prompt,
        "model": model,
        "aspect_ratio": aspect_ratio,
        "duration": duration,
        "hd": args.hd or False,
        "watermark": args.watermark or False,
        "private": args.private or False
    }
    
    # Add images if provided (image-to-video)
    if args.image:
        images = []
        image_path = Path(args.image)
        if not image_path.exists():
            print(f"Error: image file not found: {args.image}", file=sys.stderr)
            sys.exit(1)
        image_data_uri = image_to_data_uri(str(image_path))
        images.append(image_data_uri)
        payload["images"] = images
    
    # Add character URL if provided
    if args.character_url:
        payload["character_url"] = args.character_url
        payload["character_timestamps"] = args.character_timestamps or "1,3"
    
    # Submit task
    endpoint = "/v2/videos/generations"
    print(f"Submitting video generation task...", file=sys.stderr)
    resp = api_request_with_backup(api_key, endpoint, payload, timeout=30)
    
    task_id = resp.get("task_id")
    if not task_id:
        print("Error: No task_id in response", file=sys.stderr)
        print(json.dumps(resp, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
    
    # Poll for result
    final = poll_video_task(api_key, task_id, host)
    
    # Extract video URL
    task_data = final.get("data", {})
    video_url = task_data.get("output") or (task_data.get("outputs", [None])[0] if task_data.get("outputs") else None)
    
    if not video_url:
        print("Error: No video URL in response", file=sys.stderr)
        print(json.dumps(final, indent=2, ensure_ascii=False), file=sys.stderr)
        sys.exit(1)
    
    # Download video
    output_path = args.output or f"/tmp/openclaw/wangyi-output/video_{int(time.time())}.mp4"
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    cmd = ["curl", "-s", "-S", "-L", "-o", output_path, "--max-time", "600", video_url]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        print(f"Download failed: {result.stderr}", file=sys.stderr)
        sys.exit(1)
    
    print(f"OUTPUT_FILE:{Path(output_path).resolve()}")


# ---------------------------------------------------------------------------
# Character creation
# ---------------------------------------------------------------------------

def cmd_create_character(args):
    """Create character for video cameo feature."""
    [REDACTED_SECRET])
    host = args.host_url or DEFAULT_HOST
    
    if not args.url or not args.timestamps:
        print("Error: --url and --timestamps are required", file=sys.stderr)
        sys.exit(1)
    
    payload = {
        "url": args.url,
        "timestamps": args.timestamps
    }
    
    endpoint = "/sora/v1/characters"
    resp = api_request_with_backup(api_key, endpoint, payload, timeout=30)
    
    # Save result
    output_path = args.output or f"/tmp/openclaw/wangyi-output/character_{int(time.time())}.json"
    Path(output_path).parent.mkdir(parents=True, exist_ok=True)
    with open(output_path, "w", encoding="utf-8") as f:
        json.dump(resp, f, indent=2, ensure_ascii=False)
    
    print(f"OUTPUT_FILE:{Path(output_path).resolve()}")
    print(json.dumps(resp, indent=2, ensure_ascii=False))


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main():
    parser = argparse.ArgumentParser(
        description="WangYi Banana API client for OpenClaw",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""\
Modes:
  --check                           Check API key
  --list                            List available tasks
  --info TASK                       Show task details
  --task TASK [options]             Execute a generation task

Tasks:
  text-to-image                     Generate image from text
  image-to-image                    Generate image from image
  text-to-video                     Generate video from text
  image-to-video                    Generate video from image
  create-character                  Create character for cameo

Examples:
  python3 wangyi-banana.py --check
  python3 wangyi-banana.py --task text-to-image --prompt "a cute dog" --output /tmp/dog.png
  python3 wangyi-banana.py --task text-to-video --prompt "a cat walking" --model sora-2 --duration 10
""",
    )
    
    # Mode flags
    parser.add_argument("--check", action="store_true", help="Check API key")
    parser.add_argument("--list", action="store_true", help="List available tasks")
    parser.add_argument("--info", metavar="TASK", help="Show details for a task")
    
    # Execution params
    parser.add_argument("--task", "-t", help="Task type")
    parser.add_argument("--prompt", "-p", help="Text prompt")
    parser.add_argument("--image", "-i", help="Input image path")
    parser.add_argument("--model", "-m", help="Model name")
    parser.add_argument("--aspect-ratio", help="Aspect ratio (4:3, 16:9, 9:16, etc.)")
    parser.add_argument("--duration", help="Video duration (10, 15, 25)")
    parser.add_argument("--hd", action="store_true", help="Enable HD mode for video")
    parser.add_argument("--watermark", action="store_true", help="Enable watermark")
    parser.add_argument("--private", action="store_true", help="Private video mode")
    parser.add_argument("--character-url", help="Character URL for cameo")
    parser.add_argument("--character-timestamps", help="Character timestamps (e.g., '1,3')")
    parser.add_argument("--url", help="Video URL for character creation")
    parser.add_argument("--timestamps", help="Timestamps for character creation")
    parser.add_argument("--output", "-o", help="Output file path")
    parser.add_argument("--api-key", "-k", help="API key (optional, resolved from config)")
    parser.add_argument("--host-url", help="API host URL (default: https://ai.t8star.cn)")
    
    args = parser.parse_args()
    
    if args.check:
        cmd_check(args.api_key, args.host_url)
    elif args.list:
        tasks = [
            "text-to-image - Generate image from text",
            "image-to-image - Generate image from image",
            "text-to-video - Generate video from text",
            "image-to-video - Generate video from image",
            "create-character - Create character for cameo feature"
        ]
        print("Available tasks:")
        for task in tasks:
            print(f"  {task}")
    elif args.info:
        print(f"Task info for: {args.info}")
        print("Use --list to see all available tasks")
    elif args.task:
        if args.task == "text-to-image" or args.task == "image-to-image":
            cmd_generate_image(args)
        elif args.task == "text-to-video" or args.task == "image-to-video":
            cmd_generate_video(args)
        elif args.task == "create-character":
            cmd_create_character(args)
        else:
            print(f"Error: Unknown task: {args.task}", file=sys.stderr)
            print("Use --list to see available tasks", file=sys.stderr)
            sys.exit(1)
    else:
        parser.print_help()
        sys.exit(1)


if __name__ == "__main__":
    main()


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