Skill: liuxiaolong98/ai-image-video-generator-wime (version 1.0.5)

--- name: wime-creator description: WIME AI 电商创作平台 OpenAPI。提供两个核心能力：1）抠图 — 上传本地图片（或 URL）自动抠图，返回透明背景图 URL；2）商拍图 — 自动抠图后生成多张 AI 商拍图。触发场景：用户提到抠图、商拍图、商品图、WIME、电商图片处理时激活。 --- # WIME OpenAPI 电商 AI 创作平台，提供两个核心 Skill。 ## 认证 该 skill 仅支持 `WIME_ACCESS_TOKEN`。 > 安全与权限说明：该 skill 需要 WIME 专用凭证，并且会把用户明确提供的本地图片上传到 WIME 服务；当输入是远程图片 URL，或 WIME 返回远程抠图结果 URL 时，可能会拉取对应图片做处理。为降低风险，远程下载应仅用于用户提供或 WIME 返回的公开图片地址，不应用于 localhost、内网或其他非公开地址。 统一使用 `scripts/wime_auth.py` 的 `get_auth()` 函数： ```python from wime_auth import get_auth auth = get_auth(uri_path="/waic/core/creationDesk/draw", body_dict=body) resp = requests.post( f"{auth['base_url']}/waic/core/creationDesk/draw", headers=auth["headers"], data=auth["body_str"].encode("utf-8") ) ``` `get_auth()` 返回： - `base_url`: API 根地址 - `headers`: 完整请求 headers dict（含 `access-token`） - `body_str`: JSON 序列化后的 body 字符串（用于 `data=` 发送），body_dict 为 None 时为 None ### 配置方式 ```bash export WIME_ACCESS_TOKEN='你的token' # 可选，默认 https://wime-ai.com export WIME_BASE_URL='https://wime-ai.com' ``` 若未配置 `WIME_ACCESS_TOKEN`，调用前应直接报错并提示用户先完成配置。 ### 获取凭证 访问 [wime-ai.com](https://wime-ai.com/)，页面右下角有 **API 对接**的联系方式，通过该入口申请获取 `access-token`。 如果当前环境未配置凭证，应直接告知用户先去官网联系获取，而不是盲目调用接口。 ## 图片来源判断（通用前置步骤） 用户输入可能是 **本地路径** 或 **URL**，统一处理为可访问的图片 URL： ``` 判断用户输入: ├─ 本地路径 (如 /Users/.../image.jpg) │ → upload → imageUrl │ └─ URL (以 http:// 或 https:// 开头) → 直接使用该 URL 作为 imageUrl → （若后续接口不接受外部 URL，则下载 → upload → imageUrl） ``` ### 新版上传接口 `POST /waic/core/file/py/upload`（multipart/form-data） 直接返回 `data.url`（可访问的图片 URL），**不再返回 mediaId**。 ```python from wime_auth import get_auth auth = get_auth() with open("image.png", "rb") as f: resp = requests.post( f"{auth['base_url']}/waic/core/file/py/upload", headers={"access-token": auth["headers"]["access-token"]}, files={"file": ("image.png", f, "image/png")} ) image_url = resp.json()["data"]["url"] ``` **注意：** 上传接口需要 `access-token` header。 ### URL 下载注意事项（仅本地路径场景需上传） - 如果用户直接给了 URL，**优先直接用**，不需要下载再上传 - 只有本地文件才需要走上传流程 - 上传完成后清理本地临时文件 - 若必须下载远程图片进行处理，只允许公开 `http/https` 地址；拒绝 `localhost`、`.local`、回环地址和内网 IP ## Skill 1：抠图 **触发词：** 抠图、去背景、透明背景、matting ### 新版流程（access-token） ``` 用户提供本地图片路径或 URL → [图片来源判断] → imageUrl → removeBg([{imageUrl}]) → taskItemIds → queryItemForSaas(taskItemId) 轮询 → 抠图结果 URL ``` **接口：** `POST /waic/core/creationDesk/removeBg` **⚠️ 新版抠图为异步接口**，返回 `taskItemIds`，需轮询获取结果。 **步骤：** 1. 获取图片 URL：本地路径 → 上传获取 url；URL → 直接使用 2. 构建请求体： ```python import json body = { "data": json.dumps([{"imageUrl": image_url}]), "skill": True } ``` 注意 `data` 字段是 **JSON 字符串**（不是 object），内部为 imageUrl 数组。 3. 调用 `removeBg`，获取 `taskItemIds` 4. 用 `taskItemIds[0]` 轮询 `queryItemForSaas`，`itemStatus == 2` 且 `url` 有值时完成 **输出给用户：** 抠图结果 URL ## Skill 2：商拍图 **触发词：** 商拍图、商品图、AI 商拍、产品摄影 ### 新版流程（access-token） ``` 用户提供本地图片路径或 URL → [图片来源判断] → imageUrl（原图 URL，记为 sourceImageUrl） → removeBg → taskItemIds → 轮询 → cutoutUrl → crop_alpha_bbox(cutoutUrl) → 本地裁剪图 (cropped.png) → upload(cropped.png) → croppedUrl → removeBg(croppedUrl) → 轮询 → croppedCutoutUrl（紧凑抠图） → draw(picList 使用 croppedCutoutUrl) → taskItemIds → queryItemForSaas 轮询 → 出一张返回一张 ``` **步骤：** 1. **获取原图 URL**： - URL 输入 → 直接用作 `sourceImageUrl` - 本地路径 → 上传获取 url，作为 `sourceImageUrl` 2. **首次抠图**：调用 `removeBg` 传入原图 URL → 轮询获取 `cutoutUrl` 3. **裁剪非透明区域**：使用 `scripts/crop_alpha.py` 的 `crop_alpha_bbox(cutoutUrl)` 裁剪最小外接矩形，去除多余透明边距 4. **上传裁剪图**：将裁剪后 PNG 通过 `/waic/core/file/py/upload` 上传 → 获取 `croppedUrl` 及裁剪后宽高 5. **二次抠图**：调用 `removeBg` 传入 `croppedUrl` → 轮询获取 `croppedCutoutUrl` 6. **构建 `draw` 请求**： **接口：** `POST /waic/core/creationDesk/draw` ```python body = { "ips": False, "needRank": True, "num": 4, "type": 1002, "data": { "aiStyle": True, "bgColor": "", "height": 1024, "width": 1024, "industry": "其他", "negPrompt": "", "prompt": "", "promptAi": "", "randomStyle": True, "referenceUrl": "", "seed": 0, "styleCode": "A0072", "skill": True, "picList": [ { "imageUrl": cropped_cutout_url, "sourceImageUrl": source_image_url, "imageCaption": "", "width": crop_w, "height": crop_h, "left": calculated_left, "top": calculated_top, "scaleX": calculated_scale, "scaleY": calculated_scale, "type": 1, "parentId": "" } ] } } ``` **注意新版 body 结构：** 外层有 `ips`/`needRank`/`type`，画布参数在 `data` 内，`data.skill` 必须为 `True`。 7. 提交后获得 `taskItemIds` 8. 轮询 `queryItemForSaas`（每个 taskItemId 单独查），**每完成一张立即输出** 9. **清理**：删除本地临时裁剪文件 ### picList 位置与缩放规则 根据**商品图裁剪后的宽高比**判断横竖版，分别计算缩放和位置： ```python canvas_w, canvas_h = 1024, 1024 # 画布尺寸 img_w, img_h = crop_w, crop_h # 裁剪后的商品尺寸 if img_w >= img_h: # 横版/方图商品：商品宽度 = 画布宽度 × 60% scale = (canvas_w * 0.6) / img_w else: # 竖版商品：商品高度 = 画布高度 × 80% scale = (canvas_h * 0.8) / img_h # 安全 clamp：缩放后商品不能超出画布任意一边 scale = min(scale, canvas_w / img_w, canvas_h / img_h) # 水平方向：居中 left = (canvas_w - img_w * scale) / 2 # 垂直方向：总空白区域中，上方占 65%，下方占 35% total_v_blank = canvas_h - img_h * scale top = total_v_blank * 0.65 ``` ### 裁剪脚本用法 ```python import sys, os sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts')) from crop_alpha import crop_alpha_bbox cropped_path, (crop_w, crop_h) = crop_alpha_bbox(cutout_url, "/tmp/wime_cropped.png") ``` ### 结果轮询 **接口：** `POST /waic/core/task/queryItemForSaas` ```python body = {"taskItemId": task_item_id} # 单个 taskItemId（Long） auth = get_auth(uri_path=[REDACTED_SECRET], body_dict=body) resp = requests.post( f"{auth['base_url']}/waic/core/task/queryItemForSaas", headers=auth["headers"], data=auth["body_str"].encode("utf-8") ) # data.itemStatus == 2 且 data.url 有值 → 完成 ``` ### 轮询状态码（实测） | itemStatus | 含义 | |------------|------| | 0 | **需区分：调度延迟 vs 真正失败**（见下方判定逻辑） | | 1 | 执行中（未出图） | | 2 | **完成**（`url` 字段有值） | | 3 | 执行中 | | 4 | 排队中 | **判断完成的条件：** `itemStatus == 2` 且 `url` 非空。 **⚠️ itemStatus=0 的判定逻辑：** 实测发现：任务在较长一段时间内**持续返回 `status=0` 仍可能最终成功**。**不要因为连续几次 `0` 就判定失败**。 **推荐轮询策略：** - 完成条件：`itemStatus == 2` 且 `url` 非空 - 轮询间隔：`5~10` 秒 - 默认最长等待：`3~10` 分钟 - 在超时前，**不要仅因 `status=0` 就判失败** - 超时后仍无结果，标记为 "超时/未完成" **输出给用户：** 每张图完成时立即返回结果 URL --- ## API 路径对照 | 功能 | 路径 | |------|------| | 图片上传 | `POST /waic/core/file/py/upload` | | 抠图 | `POST /waic/core/creationDesk/removeBg` | | 商拍图 | `POST /waic/core/creationDesk/draw` | | 结果轮询 | `POST /waic/core/task/queryItemForSaas` | ## 环境 | 环境 | Base URL | 说明 | |------|----------|------| | 生产 | `https://wime-ai.com` | 线上环境 | 默认使用生产环境。`WIME_BASE_URL` 可覆盖。 ## 接口参考 | 文件 | 说明 | |------|------| | `references/upload-image.md` | 图片上传 | | `references/cutout.md` | 抠图 | | `references/photo-shoot.md` | 商拍图 | | `references/asset-query.md` | 结果轮询 + 资产查询 | | `references/error-codes.md` | 全局错误码 | ## 注意事项 - 新版上传图片直接得到 URL，不再有 mediaId 概念 - 抠图为**异步**接口，需轮询获取结果 - 商拍图始终为**异步**接口，轮询时出一张返回一张 - body 中含 `skill: true` 的接口必须传此字段 - QPS 限制，超限返回 errcode=1000007 - 该 skill 依赖 `WIME_ACCESS_TOKEN`；发布到注册表时应显式声明该环境变量与主凭证需求

path: references/asset-query.md
content: # 用户资产 & 任务查询

## 结果轮询接口

### 新版（access-token，推荐）

`POST /waic/core/task/queryItemForSaas`

用于查询商拍图/抠图等异步任务的单个 taskItem 结果。

**入参:**

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| taskItemId | Long | 是 | 单个任务项 ID（从 draw/removeBg 返回的 taskItemIds 中取） |

**出参:**

```json
{
    "errcode": "0",
    "errmsg": "ok",
    "data": {
        "id": 902035,
        "taskId": 634428,
        "url": "https://image-c-dev.weimobwmc.com/qa-5W/xxx.png",
        "itemStatus": 2,
        "userId": 2369671,
        "sceneType": null,
        "bosId": null,
        "transData": null
    },
    "globalTicket": "..."
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| data.id | Long | taskItemId |
| data.taskId | Long | 父任务 ID |
| data.url | String | 结果图 URL（仅 itemStatus=2 时有值） |
| data.itemStatus | Integer | 状态：0=待处理/失败, 1=执行中, 2=完成, 3=执行中, 4=排队中 |

### 调用示例

```python
import requests
from wime_auth import get_auth

body = {"taskItemId": 902035}
auth = get_auth(uri_path="/waic/core/task/queryItemForSaas", body_dict=body)

resp = requests.post(
    f"{auth['base_url']}/waic/core/task/queryItemForSaas",
    headers=auth["headers"],
    data=auth["body_str"].encode("utf-8")
)
result = resp.json()
# data.itemStatus == 2 且 data.url 有值 → 完成
```

### 轮询策略

- 完成条件：`itemStatus == 2` 且 `url` 非空
- 轮询间隔：5~10 秒
- 默认最长等待：3~10 分钟
- `itemStatus=0` 不代表一定失败，可能是调度延迟，超时前不要判定失败



path: references/photo-shoot.md
content: # 商拍图接口

## 新版（access-token，推荐）

`POST /waic/core/creationDesk/draw`

### 入参

外层结构：

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| ips | Boolean | 否 | 默认 false |
| needRank | Boolean | 否 | 是否需要排序，默认 true |
| num | Integer | 是 | 生成数量，如 4 |
| type | Integer | 是 | 创作类型，固定传 `1002` |
| data | Object | 是 | 创作参数，见下方子参数 |

### data 子参数

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| aiStyle | Boolean | 否 | 是否使用 AI 风格，建议 true |
| bgColor | String | 否 | 背景颜色，空字符串表示无指定 |
| height | Integer | 是 | 画布高度（像素），如 1024 |
| width | Integer | 是 | 画布宽度（像素），如 1024 |
| industry | String | 否 | 行业分类，如 "其他" |
| negPrompt | String | 否 | 负向提示词 |
| prompt | String | 否 | 正向提示词 |
| promptAi | String | 否 | AI 提示词，空字符串由模型自动生成 |
| randomStyle | Boolean | 否 | 是否随机风格，默认 true |
| referenceUrl | String | 否 | 风格参考图 URL |
| seed | Integer | 否 | 随机种子，0 表示随机 |
| styleCode | String | 否 | 风格代码，如 "A0072" |
| skill | Boolean | **是** | **固定传 `true`** |
| picList | Array\<Object\> | 是 | 图片列表 |

### picList 子参数

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| imageUrl | String | 是 | 展示图 URL（抠图后的） |
| sourceImageUrl | String | 是 | 原始图 URL |
| imageCaption | String | 否 | 图片描述 |
| width | Integer | 是 | 图片宽度（像素） |
| height | Integer | 是 | 图片高度（像素） |
| left | Float | 是 | X 轴偏移量（px） |
| top | Float | 是 | Y 轴偏移量（px） |
| scaleX | Float | 是 | X 轴缩放比例 |
| scaleY | Float | 是 | Y 轴缩放比例 |
| type | Integer | 是 | 图片类型，1=产品图 |
| parentId | String | 否 | 父元素 ID，空字符串表示无父级 |

### 出参

```json
{
    "errcode": "0",
    "errmsg": "ok",
    "data": {
        "creationId": 238562,
        "taskId": 634422,
        "taskItemIds": [902024, 902025, 902026, 902027],
        "ranks": [0, 0, 0, 0]
    },
    "globalTicket": "..."
}
```

### 调用示例

```python
import requests, json
from wime_auth import get_auth

body = {
    "ips": False,
    "needRank": True,
    "num": 4,
    "type": 1002,
    "data": {
        "aiStyle": True,
        "bgColor": "",
        "height": 1024,
        "industry": "其他",
        "negPrompt": "",
        "picList": [
            {
                "height": 396,
                "imageCaption": "A blue and gold microfiber mop",
                "imageUrl": "https://image-c.weimobwmc.com/...",
                "left": 356.07,
                "parentId": "",
                "scaleX": 1.55,
                "scaleY": 1.55,
                "sourceImageUrl": "https://image-c.weimobwmc.com/...",
                "top": 266,
                "type": 1,
                "width": 201
            }
        ],
        "prompt": "",
        "promptAi": "",
        "randomStyle": True,
        "referenceUrl": "",
        "seed": 0,
        "styleCode": "A0072",
        "width": 1024,
        "skill": True
    }
}

auth = get_auth(uri_path="/waic/core/creationDesk/draw", body_dict=body)

resp = requests.post(
    f"{auth['base_url']}/waic/core/creationDesk/draw",
    headers=auth["headers"],
    data=auth["body_str"].encode("utf-8")
)
print(resp.json())
# 用返回的 taskItemIds 轮询 queryItemForSaas
```



path: references/upload-image.md
content: # 图片上传接口

## 新版（access-token，推荐）

`POST /waic/core/file/py/upload`

Content-Type: multipart/form-data

直接上传图片文件，返回可访问的图片 URL。**不再返回 mediaId**。

**入参:**

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| file | MultipartFile | 是 | 文件流数据 |

**出参:**

```json
{
    "errcode": "0",
    "errmsg": "ok",
    "data": {
        "url": "https://image-c-dev.weimobwmc.com/qa-5W/xxx.png"
    },
    "globalTicket": "..."
}
```

| 参数 | 类型 | 说明 |
|------|------|------|
| data.url | String | 图片可访问 URL（可直接用于抠图/商拍图的 imageUrl 参数） |

### 调用示例

```python
import requests
from wime_auth import get_auth

auth = get_auth()

with open("image.png", "rb") as f:
    resp = requests.post(
        f"{auth['base_url']}/waic/core/file/py/upload",
        headers={"access-token": auth["headers"]["access-token"]},
        files={"file": ("image.png", f, "image/png")}
    )
print(resp.json())
# {"errcode": "0", "errmsg": "ok", "data": {"url": "https://..."}}
```



path: references/error-codes.md
content: # 全局错误码

## 认证/授权类

| ErrCode | 说明 |
|---------|------|
| 1000001 | 用户不存在 |
| 1000002 | Token 过期 |
| 1000003 | 访问次数已耗尽 |
| 1000004 | 并发数不足 |
| 1000005 | 授权已过期 |
| 1000006 | 验签失败 |
| 1000007 | 接口限流 |

## 参数/上传类

| ErrCode | 说明 |
|---------|------|
| 2000001 | 参数错误 |
| 2000002 | 上传图片过大，请上传不超过 20M 的图片 |
| 2000003 | 上传图片类型不支持 |
| 2000004 | 上传图片发生异常 |
| 2000005 | 上传的图片过于敏感 |

## 服务类

| ErrCode | 说明 |
|---------|------|
| 3000001 | 服务开小差 |


path: references/cutout.md
content: # 抠图接口

## 新版（access-token，推荐）

`POST /waic/core/creationDesk/removeBg`

**同步接口**，直接返回抠图结果。传入 imageUrl 列表，无需 mediaId。

**入参:**

| 参数 | 类型 | 必传 | 说明 |
|------|------|------|------|
| data | String (JSON array) | 是 | imageUrl 的 JSON 数组字符串，如 `"[{\"imageUrl\":\"https://...\"}]"` |
| skill | Boolean | 是 | 固定传 `true` |

**出参:**

```json
{
    "errcode": "0",
    "errmsg": "ok",
    "data": {
        "creationId": 238564,
        "taskId": 634428,
        "taskItemIds": [902035],
        "ranks": null
    },
    "globalTicket": "..."
}
```

**注意：** 新版抠图为**异步接口**，返回 `taskItemIds`，需要用结果轮询接口查询结果。

### 调用示例

```python
import requests, json
from wime_auth import get_auth

body = {
    "data": json.dumps([{"imageUrl": "https://example.com/image.webp"}]),
    "skill": True
}

auth = get_auth(uri_path="/waic/core/creationDesk/removeBg", body_dict=body)

resp = requests.post(
    f"{auth['base_url']}/waic/core/creationDesk/removeBg",
    headers=auth["headers"],
    data=auth["body_str"].encode("utf-8")
)
result = resp.json()
# 用 taskItemIds 去轮询 queryItemForSaas 获取抠图结果
```



path: scripts/wime_auth.py
content: #!/usr/bin/env python3
"""
WIME OpenAPI 认证工具

仅支持 access-token 认证：
- 使用 WIME_ACCESS_TOKEN 环境变量
- 可选 WIME_BASE_URL 覆盖默认地址
"""

import json
import os
import argparse
from typing import Optional

BASE_URL = os.environ.get("WIME_BASE_URL", "https://wime-ai.com")
ACCESS_TOKEN = os.environ.get("WIME_ACCESS_TOKEN", "")


def get_auth(
    method: str = "POST",
    uri_path: str = "",
    body_dict: Optional[dict] = None,
    content_type: str = "application/json",
) -> dict:
    """生成 WIME access-token 请求配置。

    返回:
      - base_url: str
      - headers: dict（完整请求 headers，可直接传给 requests）
      - body_str: Optional[str]（序列化后的 body，用于 data= 发送）
    """
    if not ACCESS_TOKEN:
        raise ValueError(
            "WIME_ACCESS_TOKEN 未配置。请设置环境变量：\n"
            "  export WIME_ACCESS_TOKEN='你的token'"
        )

    headers = {"access-token": ACCESS_TOKEN}
    body_str = None

    if body_dict is not None:
        body_str = json.dumps(body_dict, separators=(",", ":"), sort_keys=True, ensure_ascii=False)
        headers["Content-Type"] = content_type

    return {
        "base_url": BASE_URL,
        "headers": headers,
        "body_str": body_str,
    }


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="WIME OpenAPI 认证工具")
    parser.add_argument("--method", default="POST")
    parser.add_argument("--path", default="/waic/core/creationDesk/draw")
    parser.add_argument("--body", default="{}", help="JSON body string")
    args = parser.parse_args()

    body_dict = json.loads(args.body)
    result = get_auth(method=args.method, uri_path=args.path, body_dict=body_dict)
    print(json.dumps(result, indent=2, ensure_ascii=False))


path: scripts/crop_alpha.py
content: #!/usr/bin/env python3
"""
裁剪透明背景图片的非透明区域最大矩形。

输入：带透明背景的 PNG 图片（本地路径或 URL）
输出：裁剪后的 PNG 图片保存到指定路径

用法：
    from crop_alpha import crop_alpha_bbox

    # 从本地文件裁剪
    out_path, (w, h) = crop_alpha_bbox("/path/to/cutout.png", "/path/to/cropped.png")

    # 从 URL 裁剪
    out_path, (w, h) = crop_alpha_bbox("https://example.com/cutout.png", "/tmp/cropped.png")
"""

import os
import sys
import tempfile
import socket
import ipaddress
from urllib.parse import urlparse
import requests as req
from PIL import Image
import numpy as np
from typing import Tuple, Optional


def _is_private_host(hostname: str) -> bool:
    if not hostname:
        return True
    h = hostname.strip().lower()
    if h in {"localhost", "ip6-localhost"} or h.endswith(".local"):
        return True
    try:
        ip = ipaddress.ip_address(h)
        return ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast
    except ValueError:
        pass
    try:
        infos = socket.getaddrinfo(h, None)
    except socket.gaierror:
        return True
    for info in infos:
        addr = info[4][0]
        try:
            ip = ipaddress.ip_address(addr)
            if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast:
                return True
        except ValueError:
            return True
    return False


def _validate_remote_url(input_url: str) -> None:
    parsed = urlparse(input_url)
    if parsed.scheme not in {"http", "https"}:
        raise ValueError("Only http/https URLs are allowed")
    if not parsed.hostname:
        raise ValueError("URL hostname is missing")
    if _is_private_host(parsed.hostname):
        raise ValueError(f"Refusing private or localhost URL: {parsed.hostname}")


def crop_alpha_bbox(
    input_path_or_url: str,
    output_path: Optional[str] = None,
    padding: int = 0,
) -> Tuple[str, Tuple[int, int]]:
    """
    裁剪 PNG 图片中非透明区域的最小外接矩形。

    Args:
        input_path_or_url: 本地文件路径或 HTTP(S) URL
        output_path: 输出文件路径，为 None 时自动生成 /tmp/cropped_xxx.png
        padding: 裁剪后四周保留的像素边距（默认 0）

    Returns:
        (output_path, (width, height)) — 裁剪后图片路径和尺寸
    """
    # 1. 获取图片
    tmp_download = None
    if input_path_or_url.startswith(("http://", "https://")):
        _validate_remote_url(input_path_or_url)
        resp = req.get(input_path_or_url, timeout=30)
        resp.raise_for_status()
        tmp_download = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
        tmp_download.write(resp.content)
        tmp_download.close()
        local_path = tmp_download.name
    else:
        local_path = input_path_or_url

    try:
        img = Image.open(local_path).convert("RGBA")
        arr = np.array(img)

        # 2. 提取 alpha 通道，找非透明像素
        alpha = arr[:, :, 3]
        rows = np.any(alpha > 0, axis=1)
        cols = np.any(alpha > 0, axis=0)

        if not rows.any() or not cols.any():
            # 全透明图片，原样返回
            if output_path is None:
                output_path = tempfile.mktemp(suffix=".png", dir="/tmp", prefix="cropped_")
            img.save(output_path, "PNG")
            return output_path, (img.width, img.height)

        # 3. 计算边界
        rmin, rmax = np.where(rows)[0][[0, -1]]
        cmin, cmax = np.where(cols)[0][[0, -1]]

        # 应用 padding
        if padding > 0:
            rmin = max(0, rmin - padding)
            rmax = min(arr.shape[0] - 1, rmax + padding)
            cmin = max(0, cmin - padding)
            cmax = min(arr.shape[1] - 1, cmax + padding)

        # 4. 裁剪
        cropped = img.crop((cmin, rmin, cmax + 1, rmax + 1))

        # 5. 保存
        if output_path is None:
            output_path = tempfile.mktemp(suffix=".png", dir="/tmp", prefix="cropped_")
        cropped.save(output_path, "PNG")
        return output_path, (cropped.width, cropped.height)

    finally:
        if tmp_download and os.path.exists(tmp_download.name):
            os.unlink(tmp_download.name)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Usage: python crop_alpha.py <input_path_or_url> [output_path] [padding]")
        sys.exit(1)

    input_arg = sys.argv[1]
    output_arg = sys.argv[2] if len(sys.argv) > 2 else None
    pad = int(sys.argv[3]) if len(sys.argv) > 3 else 0

    out, (w, h) = crop_alpha_bbox(input_arg, output_arg, pad)
    print(f"Cropped: {out} ({w}x{h})")


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