Skill: zuoyuting214/zyt-video-compose (version 0.4.0)

--- name: chanjing-video-compose description: Use Chanjing video synthesis APIs to create digital human videos from text or audio, with optional background upload, task polling, and explicit download when the user asks to save the result locally. metadata: openclaw: homepage: https://open-api.chanjing.cc --- # Chanjing Video Compose ## When to Use This Skill 当用户要做这些事时使用本 Skill： * 创建数字人视频合成任务 * 用文本驱动数字人出镜 * 用本地音频驱动数字人视频 * 查询公共数字人或定制数字人形象 * 轮询视频合成结果 * 在用户明确要求时下载最终视频到本地 如果需求更接近“上传一段真人视频做对口型驱动”，优先使用 `chanjing-avatar`，不要混用。 ## Preconditions 本 Skill 自己包含本地配置和鉴权流程，不依赖其他 skill 的运行时脚本。 本 Skill 使用： * 配置文件：`~/.chanjing/credentials.json` * 若设置环境变量 `CHANJING_CONFIG_DIR`：使用 `$CHANJING_CONFIG_DIR/credentials.json` * API 基础地址固定：`https://open-api.chanjing.cc` 当本地缺少 AK/SK 或 AK/SK 无效时，脚本默认返回登录引导信息，不自动打开浏览器。 如需本地自动开页，可显式设置：`CHANJING_AUTO_OPEN_LOGIN=1`。 `https://www.chanjing.cc/openapi/login` ## Standard Workflow 1. 先让用户明确选择数字人来源：`common`（公共数字人）或 `customised`（定制数字人） 2. 调用 `list_figures --source <common|customised>`（建议 `--json`，公共源可加大 `--page-size` 或翻页）获取可用形象；**在候选内对比** `name`、各 `figure` 的 `type` 与分辨率、`audio_man_id`、`audio_name`（若有）与任务人设后再选定 `person.id`。**禁止**未比较就默认列表最前几项。 3. 如果选择公共数字人，还要再确认 `figure_type`（与所选 `figures[].type` 一致），例如 `sit_body` / `whole_body` / `circle_view`。无用户特殊要求时，**默认优先年轻、有活力的形象**（名称/`audio_name` 偏青年、学生、元气等）；题材需要成熟或中老年气质时再改选。 4. 若使用文本驱动，确定 `audio_man_id` 5. 在创建任务前，必须明确询问用户字幕偏好：`show`（保留字幕）或 `hide`（隐藏字幕） 6. 如果用户选择 `show` 但没有提出自定义样式或位置需求，直接使用官方文档推荐默认值；只有在用户明确想调整字幕位置或样式时，才继续追问 `subtitle_config` 参数 7. 若用户要定制字幕位置，说明坐标以左上角为原点，再补充 `subtitle_config` 相关参数 8. 若使用本地音频或背景图，先调用 `upload_file` 获取 `file_id` 9. 调用 `create_task` 创建视频合成任务，得到 `video_id` 10. 调用 `poll_task` 轮询直到成功，得到 `video_url` 11. 只有在用户明确要求保存到本地时，才调用 `download_result` ## Covered APIs 本 Skill 当前覆盖： * `GET /open/v1/list_common_dp` * `POST /open/v1/list_customised_person` * `POST /open/v1/create_video` * `GET /open/v1/video` * `GET /open/v1/common/create_upload_url` * `GET /open/v1/common/file_detail` ## Scripts 脚本目录： * `scripts/` | 脚本 | 说明 | |------|------| | `chanjing-config` | 写入/查看本地 `app_id` 与 `secret_key`，并清理旧 token 缓存 | | `chanjing-get-token` | 从本地凭证获取有效 `access_token`（必要时自动刷新） | | `_auth.py` | 读取凭证、获取或刷新 `access_token` | | `list_figures` | 按 `--source common|customised` 列出数字人形象，输出 `person.id` / `figure_type` / `audio_man_id` / 预览信息 | | `upload_file` | 上传音频或背景素材，轮询到文件可用后输出 `file_id` | | `create_task` | 创建视频合成任务；使用公共数字人时可补充 `--figure-type ...`，字幕支持 `--subtitle show|hide` 以及完整字幕配置参数 | | `poll_task` | 轮询视频详情直到完成，默认输出 `video_url` | | `download_result` | 下载最终视频到 `outputs/video-compose/` | ## Usage Examples 示例 1：公共数字人文本驱动 ```bash # 1. 先列公共数字人 python scripts/list_figures --source common # 2. 用公共数字人创建文本驱动视频 VIDEO_ID=$(python scripts/create_task \ --person-id [REDACTED_SECRET] \ --figure-type "sit_body" \ --audio-man [REDACTED_SECRET] \ --subtitle "show" \ --subtitle-x 31 \ --subtitle-y 1521 \ --subtitle-width 1000 \ --subtitle-height 200 \ --subtitle-font-size 64 \ --subtitle-stroke-width 7 \ --text "你好，这是一个蝉镜视频合成测试。") # 3. 轮询到完成，拿到 video_url python scripts/poll_task --id "$VIDEO_ID" ``` 示例 2：定制数字人上传本地音频驱动 ```bash python scripts/list_figures --source customised AUDIO_FILE_ID=$(python scripts/upload_file \ --service make_video_audio \ --file ./input.wav) VIDEO_ID=$(python scripts/create_task \ --person-id [REDACTED_SECRET] \ --subtitle "hide" \ --audio-file-id "$AUDIO_FILE_ID") python scripts/poll_task --id "$VIDEO_ID" ``` 示例 3：显式下载最终视频 ```bash python scripts/download_result \ --url "https://example.com/output.mp4" ``` ## Download Rule 下载是显式动作，不是默认动作： * `poll_task` 成功后应先返回 `video_url` * 不要自动下载结果文件 * 只有当用户明确表达“下载到本地”“保存到 outputs”“帮我落盘”时，才执行 `download_result` ## Figure Selection Rule 选择数字人时遵循这条规则： * 如果用户要用平台已有人物库，先走公共数字人：`list_figures --source common` * 如果用户要用自己训练或上传生成的人物，先走定制数字人：`list_figures --source customised` * 使用公共数字人创建视频时，可按所选形态传 `--figure-type <type>` * 使用定制数字人时，不需要 `figure_type` ## Subtitle Rule 字幕遵循这条规则： * 不要默认假设用户要字幕或不要字幕 * 创建任务前，必须先明确询问用户选择：`show` 或 `hide` * 若由 **[REDACTED_SECRET]** 的 **`run_render.py`** 调用 `create_task`，以当次 **`workflow.json` 根级 `subtitle_required`** 为准（**默认 false** → `--subtitle hide`；**true** → `show` 及推荐样式），**无需**为该一键成片路径再单独追问字幕开关，除非用户在需求里明确要求改字幕策略 * 用户选择保留字幕时，调用 `create_task --subtitle show` * 若用户未指定字幕位置或样式，直接使用官方推荐默认值；`create_task` 在未传 `--subtitle-color` 时默认白字 `color=#FFFFFF`：1080p 为 `x=31 y=1521 width=1000 height=200 font_size=64 stroke_width=7 asr_type=0`；4K 画布为 `x=80 y=2840 width=2000 height=1000 font_size=150 stroke_width=7 asr_type=0`（两组均含 `color=#FFFFFF`） * 用户选择隐藏字幕时，调用 `create_task --subtitle hide` 或兼容旧用法 `--hide-subtitle` * 若用户要求调整字幕位置或样式，可继续传 `--subtitle-x` / `--subtitle-y` / `--subtitle-width` / `--subtitle-height` / `--subtitle-font-size` / `--subtitle-color` / `--subtitle-stroke-color` / `--subtitle-stroke-width` / `--subtitle-font-id` / `--subtitle-asr-type` * 坐标基于左上角原点；字幕区域不能超出 `screen_width` / `screen_height` * 如果用户只说“要字幕”但没指定位置，不必再追问具体数值；除非用户明确要调位置，否则直接走默认值 ## Output Convention 默认本地输出目录： * `outputs/video-compose/` ## Additional Resources 更多接口细节见： * `reference.md` * `examples.md`

path: examples.md
content: # Examples

## Natural Language Triggers

这些说法通常应该触发本 skill：

* “帮我做一个蝉镜数字人视频”
* “用这段文案生成一个数字人口播视频”
* “先列一下公共数字人”
* “先列一下我自己的定制数字人”
* “把这段 wav 上传后做成数字人视频”
* “帮我轮询视频合成任务状态”
* “把生成好的视频下载到本地”

## Minimal CLI Flows

### 1. 公共数字人文本驱动

```bash
python scripts/list_figures --source common

VIDEO_ID=$(python scripts/create_task \
  --person-id [REDACTED_SECRET] \
  --figure-type "sit_body" \
  --audio-man [REDACTED_SECRET] \
  --subtitle "show" \
  --text "你好，这是一个蝉镜视频合成测试。")

python scripts/poll_task --id "$VIDEO_ID"
```

说明：

* 仅传 `--subtitle show` 时，脚本会自动补官方推荐字幕参数
* 如需自定义位置或样式，再继续补充 `--subtitle-x` / `--subtitle-y` / `--subtitle-width` / `--subtitle-height` / `--subtitle-font-size` 等参数

### 2. 公共数字人文本驱动，自定义字幕位置

```bash
VIDEO_ID=$(python scripts/create_task \
  --person-id [REDACTED_SECRET] \
  --figure-type "sit_body" \
  --audio-man [REDACTED_SECRET] \
  --subtitle "show" \
  --subtitle-x 31 \
  --subtitle-y 1521 \
  --subtitle-width 1000 \
  --subtitle-height 200 \
  --subtitle-font-size 64 \
  --subtitle-stroke-width 7 \
  --text "你好，这是一个蝉镜视频合成测试。")

python scripts/poll_task --id "$VIDEO_ID"
```

### 3. 定制数字人本地音频驱动

```bash
python scripts/list_figures --source customised

AUDIO_FILE_ID=$(python scripts/upload_file \
  --service make_video_audio \
  --file ./demo.wav)

VIDEO_ID=$(python scripts/create_task \
  --person-id [REDACTED_SECRET] \
  --audio-file-id "$AUDIO_FILE_ID")

python scripts/poll_task --id "$VIDEO_ID"
```

### 4. 带背景图

```bash
BG_FILE_ID=$(python scripts/upload_file \
  --service make_video_background \
  --file ./background.png)

VIDEO_ID=$(python scripts/create_task \
  --person-id [REDACTED_SECRET] \
  --figure-type "whole_body" \
  --audio-man [REDACTED_SECRET] \
  --text "欢迎来到我的频道。" \
  --bg-file-id "$BG_FILE_ID")
```

### 5. 显式下载

```bash
python scripts/download_result \
  --url "https://example.com/output.mp4"
```

## Expected Outputs

* `list_figures` 默认输出表格，便于挑选 `person.id`，公共数字人还会显示 `figure_type`
* `upload_file` 输出 `file_id`
* `create_task` 输出 `video_id`
* `poll_task` 默认输出 `video_url`
* `download_result` 输出本地文件路径


path: reference.md
content: # Reference

## Covered APIs

本 skill 当前覆盖这些接口：

* `GET /open/v1/list_common_dp`
* `POST /open/v1/list_customised_person`
* `POST /open/v1/create_video`
* `GET /open/v1/video`
* `GET /open/v1/common/create_upload_url`
* `GET /open/v1/common/file_detail`

## Figure List Notes

### 定制数字人接口

```http
POST /open/v1/list_customised_person
```

典型用途：

* 获取 `person.id`
* 获取默认可复用的 `audio_man_id`
* 查看是否支持 `support_4k`
* 获取 `preview_url` 便于人工选择形象

脚本默认关注这些字段：

* `id`
* `name`
* `audio_man_id`
* `support_4k`
* `preview_url`

### 公共数字人接口

```http
GET /open/v1/list_common_dp?page=<page>&size=<size>
```

典型用途：

* 获取公共数字人 `person.id`
* 获取该人物可用的 `figures[].type`
* 获取默认可复用的 `audio_man_id`
* 获取 `figures[].preview_video_url` 便于人工选择形象

脚本默认关注这些字段：

* `id`
* `name`
* `figures[].type`
* `figures[].width`
* `figures[].height`
* `audio_man_id`
* `audio_name`
* `figures[].preview_video_url`

## Create Task Notes

接口：

```http
POST /open/v1/create_video
```

这是异步任务接口，响应 `data` 即视频任务 id，需要继续调用详情接口轮询。

### Minimum body for TTS mode

```json
{
  "person": {
    "id": "C-figure-id",
    "x": 0,
    "y": 0,
    "width": 1080,
    "height": 1920
  },
  "audio": {
    "type": "tts",
    "volume": 100,
    "language": "cn",
    "tts": {
      "text": ["你好，这是一个测试。"],
      "speed": 1,
      "audio_man": "C-audio-man-id",
      "pitch": 1
    }
  },
  "bg_color": "#EDEDED",
  "screen_width": 1080,
  "screen_height": 1920
}
```

### Minimum body for audio mode

```json
{
  "person": {
    "id": "C-figure-id",
    "x": 0,
    "y": 0,
    "width": 1080,
    "height": 1920
  },
  "audio": {
    "type": "audio",
    "file_id": "uploaded-audio-file-id",
    "volume": 100,
    "language": "cn"
  },
  "bg_color": "#EDEDED",
  "screen_width": 1080,
  "screen_height": 1920
}
```

### Common request fields

* `person.id`: 形象 id，来自 `list_common_dp` 或 `list_customised_person`
* `person.figure_type`: 公共数字人形态，如 `whole_body` / `sit_body` / `circle_view`；使用公共数字人时必传
* `audio.type`: `tts` 或 `audio`
* `audio.tts.text`: 文本数组，建议把所有文本放进一个字符串
* `audio.tts.audio_man`: 声音 id，优先使用该形象返回的 `audio_man_id`
* `audio.file_id`: 本地上传音频的 file id
* `audio.wav_url`: 远端音频链接
* `bg.file_id`: 背景素材 file id
* `bg.src_url`: 背景图片地址
* `drive_mode`: `random` 表示随机帧动作；不传表示正常顺序驱动
* `backway`: 人物素材播放顺序，`1` 正放，`2` 倒放
* `is_rgba_mode`: 是否生成四通道 webm
* `model`: `0` 基础版，`1` 高质版
* `resolution_rate`: `0` 为 1080p，`1` 为 4K
* `subtitle_config.show`: 是否显示字幕
* `subtitle_config.x`: 字幕区域起始 x 坐标，默认推荐 `31`（4K 推荐 `80`）
* `subtitle_config.y`: 字幕区域起始 y 坐标，默认推荐 `1521`（4K 推荐 `2840`）
* `subtitle_config.width`: 字幕显示范围宽度，默认推荐 `1000`（4K 推荐 `2000`）
* `subtitle_config.height`: 字幕显示范围高度，默认推荐 `200`（4K 推荐 `1000`）
* `subtitle_config.font_size`: 字幕字号，默认推荐 `64`（4K 推荐 `150`）
* `subtitle_config.color`: 字幕颜色，格式 `#RRGGBB`
* `subtitle_config.stroke_color`: 字幕描边颜色，格式 `#RRGGBB`
* `subtitle_config.stroke_width`: 字幕描边宽度，推荐 `7`
* `subtitle_config.font_id`: 字幕字体 ID
* `subtitle_config.asr_type`: 字幕时间戳来源，`0` 自动生成，`1` 用户输入
* `callback`: 任务完成回调 URL

脚本约定：

* `create_task --subtitle show` 会传 `subtitle_config.show=true`
* 若未额外传字幕位置和样式参数，`create_task --subtitle show` 会自动补齐官方推荐默认值（含白字 `color=#FFFFFF`）：1080p 为 `31/1521/1000/200/64/#FFFFFF/7/0`，4K 画布为 `80/2840/2000/1000/150/#FFFFFF/7/0`
* `create_task --subtitle hide` 会传 `subtitle_config.show=false`
* `create_task --hide-subtitle` 兼容旧用法，也会传 `subtitle_config.show=false`
* `create_task` 支持通过 `--subtitle-x` / `--subtitle-y` / `--subtitle-width` / `--subtitle-height` / `--subtitle-font-size` / `--subtitle-color` / `--subtitle-stroke-color` / `--subtitle-stroke-width` / `--subtitle-font-id` / `--subtitle-asr-type` 覆盖默认字幕配置中的任意字段
* 若用户只确认“显示字幕”而未指定位置，代理应直接使用默认值；若用户要求“字幕更高一点”“靠左一点”等，再结合左上角原点规则追问具体坐标或给出建议值

### Constraints and caveats

* 文本长度应小于 4000 字符
* 音频驱动目前适合 wav / mp3 / m4a；如需字幕，建议上传 8000 Hz 或 16000 Hz 单声道音频
* 背景图仅支持 `jpg` / `png`
* 使用公共数字人时，先从 `figures[]` 中选定具体 `type`，再把对应的宽高映射到 `person.width` / `person.height`
* 开启 `resolution_rate=1` 时，最好先确认数字人 `support_4k=true`
* 字幕坐标以左上角为原点；若传 `subtitle_config.x/y/width/height`，应确保字幕区域不超出屏幕范围
* 下载不应由创建或轮询脚本自动触发

## Poll Detail Notes

接口：

```http
GET /open/v1/video?id=<video_id>
```

轮询关注字段：

* `id`
* `status`
* `progress`
* `msg`
* `video_url`
* `subtitle_data_url`
* `preview_url`
* `duration`

状态流转：

* `10`: 生成中，继续轮询
* `30`: 成功，返回 `video_url`
* `4X`: 参数异常，视为失败
* `5X`: 服务异常，视为失败

## File Upload Notes

接口流程：

1. `GET /open/v1/common/create_upload_url?service=<service>&name=<filename>`
2. 用返回的 `sign_url` 执行 `PUT`
3. `GET /open/v1/common/file_detail?id=<file_id>` 直到文件可用

视频合成常用 `service`：

* `make_video_audio`
* `make_video_background`

文件就绪状态：

* `status = 1`: 文件可用

失败状态：

* `status = 98`: 内容安全检测失败
* `status = 99`: 文件标记删除
* `status = 100`: 文件已清理

## Script Mapping

| 脚本 | 对应接口 |
|------|----------|
| `list_figures` | `GET /open/v1/list_common_dp` 或 `POST /open/v1/list_customised_person` |
| `upload_file` | `GET /open/v1/common/create_upload_url` + `PUT sign_url` + `GET /open/v1/common/file_detail` |
| `create_task` | `POST /open/v1/create_video` |
| `poll_task` | `GET /open/v1/video` |
| `download_result` | 下载 `video_url` 到本地 |

## Download Rule

* `download_result` 是显式动作，不应在 `poll_task` 成功后自动执行
* 优先先把 `video_url` 返回给用户
* 只有在用户确认下载时，才保存到 `outputs/video-compose/`


path: scripts/_auth.py
content: # 鉴权：从 CONFIG_DIR/credentials.json 读取 AK/SK
# 实现：AK/SK 校验、Token 校验与刷新；默认不自动打开浏览器，仅返回登录引导信息
import json
import os
import time
import urllib.request
from pathlib import Path

CONFIG_DIR = Path(os.environ.get("CHANJING_CONFIG_DIR", Path.home() / ".chanjing"))
CONFIG_FILE = CONFIG_DIR / "credentials.json"
API_BASE = "https://open-api.chanjing.cc"
BUFFER_SECONDS = 300
LOGIN_URL = "https://www.chanjing.cc/openapi/login"
AUTO_OPEN_LOGIN = os.environ.get("CHANJING_AUTO_OPEN_LOGIN", "").strip().lower() in {"1", "true", "yes", "on"}

NO_CREDENTIALS_MSG = """缺少 AK/SK 本地凭证。
请先在 ~/.chanjing/credentials.json（或 $CHANJING_CONFIG_DIR/credentials.json）中配置：
  {
    "app_id": "<你的app_id>",
    "secret_key": "<你的secret_key>"
  }
登录页：https://www.chanjing.cc/openapi/login
配置完成后请重新执行您之前的操作。"""

INVALID_CREDENTIALS_MSG = """AK/SK 无效或已过期。
请重新获取 app_id 和 secret_key，并更新 ~/.chanjing/credentials.json
（或 $CHANJING_CONFIG_DIR/credentials.json）后重试。
登录页：https://www.chanjing.cc/openapi/login"""


def _run_open_login_page():
    """按配置决定是否尝试打开登录页。"""
    if not AUTO_OPEN_LOGIN:
        return
    try:
        import webbrowser

        webbrowser.open(LOGIN_URL)
    except Exception:
        pass


def read_config():
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}


def write_config(data):
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)


def clear_cached_token():
    """清除本地缓存的 access_token / expire_in。"""
    data = read_config()
    data.pop("access_token", None)
    data.pop("expire_in", None)
    write_config(data)


def _request_new_token(app_id, secret_key):
    url = API_BASE + "/open/v1/access_token"
    req = urllib.request.Request(
        url,
        data=json.dumps({"app_id": app_id, "secret_key": secret_key}).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def get_token(force_refresh=False):
    """返回 (token, None) 或 (None, error_msg)。"""
    data = read_config()
    app_id = (data.get("app_id") or "").strip()
    secret_key = (data.get("secret_key") or "").strip()
    if not app_id or not secret_key:
        _run_open_login_page()
        return None, NO_CREDENTIALS_MSG

    now = int(time.time())
    [REDACTED_SECRET]"access_token") or "").strip()
    expire_in = data.get("expire_in")
    try:
        expire_in = int(expire_in) if expire_in is not None else 0
    except (ValueError, TypeError):
        expire_in = 0

    if (not force_refresh) and token and expire_in > now + BUFFER_SECONDS:
        return token, None

    try:
        body = _request_new_token(app_id, secret_key)
    except Exception as e:
        return None, str(e)

    if body.get("code") != 0:
        _run_open_login_page()
        return None, INVALID_CREDENTIALS_MSG

    d = body.get("data", {})
    new_token = d.get("access_token")
    if not new_token:
        _run_open_login_page()
        return None, INVALID_CREDENTIALS_MSG

    data["access_token"] = new_token
    data["expire_in"] = d.get("expire_in")
    write_config(data)
    return new_token, None


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