Skill: 156554395/doubao-image-video (version 0.3.1)

--- name: doubao-image-video description: 豆包图片与视频生成原生技能。适用于用户提到豆包、文生图、图生图、文生视频、图生视频、查询视频生成任务、等待任务完成或下载最终视频时，直接调用火山引擎 Ark 接口，不依赖外部 MCP 服务。 author: 小蜜 version: 0.3.1 homepage: https://github.com/156554395/doubao-image-video-mcp metadata: { "openclaw": { "requires": { "bins": ["python3"] }, "config": { "env": { "DOUBAO_API_KEY": { "description": "Volcengine Ark API key for Doubao image/video generation", "required": true }, "DOUBAO_IMAGE_ENDPOINT_ID": { "description": "Default image endpoint id from Volcengine Ark", "required": false }, "DOUBAO_VIDEO_ENDPOINT_ID": { "description": "Default video endpoint id from Volcengine Ark", "required": false }, "DOUBAO_DEFAULT_IMAGE_MODEL": { "description": "Optional default image model", "default": "doubao-seedream-4-5" }, "DOUBAO_DEFAULT_VIDEO_MODEL": { "description": "Optional default video model", "default": "doubao-seedance-1.0-lite-t2v" } } } } } --- # Doubao Native Media Skill This is a native OpenClaw skill. Do not spin up the upstream MCP server unless the user explicitly asks for MCP compatibility. ## Use this skill for - Doubao / 豆包 text-to-image - image-to-image or multi-reference image generation - Doubao text-to-video or image-to-video - querying an async Doubao video task by `task_id` - troubleshooting Volcengine Ark endpoint/model issues ## Commands ### Generate an image ```bash python3 {baseDir}/scripts/doubao_media.py image \ --prompt "A cinematic cyberpunk alley in rain" \ --size 2560x1440 ``` ### Generate a video ```bash python3 {baseDir}/scripts/doubao_media.py video \ --prompt "A panda astronaut waves on the moon" \ --video-duration 5 \ --fps 24 \ --resolution 1080p ``` ### Query a video task ```bash python3 {baseDir}/scripts/doubao_media.py task --task-id your-task-id ``` ### Wait for a video task and optionally download the result ```bash python3 {baseDir}/scripts/doubao_media.py wait \ --task-id your-task-id \ --timeout 600 \ --interval 5 \ --download-to ./doubao-result.mp4 ``` ## Input rules - Always prefer `--endpoint-id` when the user has a provisioned Volcengine Ark endpoint. - Fall back to model names only when endpoint ids are unavailable. - For video generation, this skill mirrors the upstream behavior and appends `--dur`, `--fps`, `--rs`, and `--ratio` to the prompt when they are not already present. - If the user supplies image URLs, pass them through exactly; do not download or re-host unless asked. ## Troubleshooting - If neither `--endpoint-id` nor a default endpoint env var exists, the script falls back to the default model env var. - If the API returns `InvalidEndpointOrModel.NotFound`, ask the user to verify the Volcengine Ark endpoint authorization first. - Video generation is async. If generation succeeds, capture `task_id` and query it later with the `task` subcommand, or use `wait` for automatic polling. ## References - Read `references/api-notes.md` when you need request shapes, defaults, or caveats.

path: references/api-notes.md
content: # API Notes

## Endpoints

- Image generation: `POST https://ark.cn-beijing.volces.com/api/v3/images/generations`
- Video generation: `POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks`
- Video task query: `GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{task_id}`

## Environment variables

- `DOUBAO_API_KEY`: required
- `DOUBAO_IMAGE_ENDPOINT_ID`: optional default image endpoint id
- `DOUBAO_VIDEO_ENDPOINT_ID`: optional default video endpoint id
- `DOUBAO_DEFAULT_IMAGE_MODEL`: optional, default `doubao-seedream-4-5`
- `DOUBAO_DEFAULT_VIDEO_MODEL`: optional, default `doubao-seedance-1.0-lite-t2v`

## Image command

Supported flags:

- `--prompt` required
- `--endpoint-id` optional
- `--model` optional
- `--size` optional
- `--image-url` optional
- `--ref-image-url` repeatable
- `--req-key` optional
- `--watermark` optional

## Video command

Supported flags:

- `--prompt` required
- `--endpoint-id` optional
- `--model` optional
- `--video-duration` optional
- `--fps` optional
- `--resolution` optional
- `--first-frame-image-url` optional
- `--ref-image-url` repeatable
- `--req-key` optional

Behavior:

- Appends `--dur`, `--fps`, `--rs`, and `--ratio` to the prompt if missing.
- Uses ratio `16:9` for `720p`, otherwise `adaptive`.
- Returns JSON with the upstream response body.

## Wait and download

The `wait` subcommand supports:

- `--task-id`: required
- `--timeout`: max wait time in seconds, default `600`
- `--interval`: polling interval in seconds, default `5`
- `--download-to`: optional local output path; if the task succeeds and returns `content.video_url`, the file is downloaded automatically

## Important caveats

- Upstream documentation and implementation disagree on some defaults; this skill preserves the implementation-oriented behavior where practical.
- The script never prints secrets. It only emits structured JSON or concise errors.


path: scripts/doubao_media.py
content: #!/usr/bin/env python3
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

BASE_URL = "https://ark.cn-beijing.volces.com/api/v3"
DEFAULT_IMAGE_MODEL = os.getenv("DOUBAO_DEFAULT_IMAGE_MODEL", "doubao-seedream-4-5")
DEFAULT_VIDEO_MODEL = os.getenv("DOUBAO_DEFAULT_VIDEO_MODEL", "doubao-seedance-1.0-lite-t2v")
DEFAULT_IMAGE_ENDPOINT = os.getenv("DOUBAO_IMAGE_ENDPOINT_ID", "")
DEFAULT_VIDEO_ENDPOINT = os.getenv("DOUBAO_VIDEO_ENDPOINT_ID", "")


def fail(message, code=1, extra=None):
    payload = {"ok": False, "error": message}
    if extra is not None:
        payload["details"] = extra
    print(json.dumps(payload, ensure_ascii=False))
    sys.exit(code)


def request_json(method, url, api_key, body=None):
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    data = None if body is None else json.dumps(body).encode("utf-8")
    req = urllib.request.Request(url, data=data, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            raw = resp.read().decode("utf-8")
            parsed = json.loads(raw) if raw else {}
            return {"http_status": resp.status, "body": parsed}
    except urllib.error.HTTPError as e:
        raw = e.read().decode("utf-8", errors="replace")
        try:
            parsed = json.loads(raw) if raw else {}
        except Exception:
            parsed = {"raw": raw}
        fail(f"HTTP {e.code}", code=2, extra=parsed)
    except urllib.error.URLError as e:
        fail(f"request failed: {e.reason}", code=3)


def ensure_api_key():
    [REDACTED_SECRET]"DOUBAO_API_KEY", "")
    if not api_key:
        fail("DOUBAO_API_KEY is required")
    return api_key


def append_video_flags(prompt, duration, fps, resolution):
    ratio = "16:9" if resolution == "720p" else "adaptive"
    text = prompt
    if "--dur" not in text:
        text += f" --dur {duration}"
    if "--fps" not in text:
        text += f" --fps {fps}"
    if "--rs" not in text:
        text += f" --rs {resolution}"
    if "--ratio" not in text:
        text += f" --ratio {ratio}"
    return text


def cmd_image(args):
    [REDACTED_SECRET])
    model_or_endpoint = args.endpoint_id or DEFAULT_IMAGE_ENDPOINT or args.model or DEFAULT_IMAGE_MODEL
    body = {
        "model": model_or_endpoint,
        "prompt": args.prompt,
        "size": args.size,
        "watermark": args.watermark,
    }
    if args.image_url:
        body["image_url"] = args.image_url
    if args.ref_image_url:
        body["ref_image_urls"] = args.ref_image_url
    if args.req_key:
        body["req_key"] = args.req_key
    result = request_json("POST", f"{BASE_URL}/images/generations", api_key, body)
    print(json.dumps({"ok": True, "kind": "image", "request": body, "response": result}, ensure_ascii=False))


def cmd_video(args):
    [REDACTED_SECRET])
    if len(args.prompt) > 500:
        fail("prompt must be <= 500 chars")
    model_or_endpoint = args.endpoint_id or DEFAULT_VIDEO_ENDPOINT or args.model or DEFAULT_VIDEO_MODEL
    content = [
        {
            "type": "text",
            "text": append_video_flags(args.prompt, args.video_duration, args.fps, args.resolution),
        }
    ]
    if args.first_frame_image_url:
        content.append({
            "type": "image_url",
            "image_url": {"url": args.first_frame_image_url},
        })
    for url in args.ref_image_url or []:
        content.append({
            "type": "image_url",
            "image_url": {"url": url},
            "role": "reference_image",
        })
    body = {"model": model_or_endpoint, "content": content}
    if args.req_key:
        body["req_key"] = args.req_key
    result = request_json("POST", f"{BASE_URL}/contents/generations/tasks", api_key, body)
    print(json.dumps({"ok": True, "kind": "video", "request": body, "response": result}, ensure_ascii=False))


def fetch_task(api_key, task_id):
    encoded = urllib.parse.quote(task_id, safe="")
    return request_json("GET", f"{BASE_URL}/contents/generations/tasks/{encoded}", api_key)


def download_file(url, output_path):
    req = urllib.request.Request(url, method="GET")
    try:
        with urllib.request.urlopen(req, timeout=300) as resp:
            data = resp.read()
        with open(output_path, "wb") as f:
            f.write(data)
        return {"path": output_path, "bytes": len(data)}
    except urllib.error.HTTPError as e:
        fail(f"download failed: HTTP {e.code}")
    except urllib.error.URLError as e:
        fail(f"download failed: {e.reason}")


def cmd_task(args):
    [REDACTED_SECRET])
    task_id = args.task_id.strip()
    if not task_id:
        fail("task_id is required")
    result = fetch_task(api_key, task_id)
    print(json.dumps({"ok": True, "kind": "task", "task_id": task_id, "response": result}, ensure_ascii=False))


def cmd_wait(args):
    [REDACTED_SECRET])
    task_id = args.task_id.strip()
    if not task_id:
        fail("task_id is required")
    deadline = time.time() + args.timeout
    attempts = 0
    last = None
    while True:
        attempts += 1
        last = fetch_task(api_key, task_id)
        body = last.get("body", {})
        status = body.get("status")
        if status in ("succeeded", "failed", "canceled"):
            result = {
                "ok": status == "succeeded",
                "kind": "wait",
                "task_id": task_id,
                "attempts": attempts,
                "status": status,
                "response": last,
            }
            video_url = ((body.get("content") or {}).get("video_url"))
            if status == "succeeded" and args.download_to and video_url:
                result["download"] = download_file(video_url, args.download_to)
            print(json.dumps(result, ensure_ascii=False))
            return
        if time.time() >= deadline:
            print(json.dumps({
                "ok": False,
                "kind": "wait",
                "task_id": task_id,
                "attempts": attempts,
                "status": status,
                "error": "timeout",
                "response": last,
            }, ensure_ascii=False))
            sys.exit(4)
        time.sleep(args.interval)


def build_parser():
    parser = argparse.ArgumentParser(description="Doubao native media skill helper")
    sub = parser.add_subparsers(dest="command")

    p_image = sub.add_parser("image", help="Generate image")
    p_image.add_argument("--prompt", required=True)
    p_image.add_argument("--endpoint-id")
    p_image.add_argument("--model")
    p_image.add_argument("--size", default="2560x1440")
    p_image.add_argument("--image-url")
    p_image.add_argument("--ref-image-url", action="append")
    p_image.add_argument("--req-key")
    p_image.add_argument("--watermark", action="store_true")
    p_image.set_defaults(func=cmd_image)

    p_video = sub.add_parser("video", help="Generate video task")
    p_video.add_argument("--prompt", required=True)
    p_video.add_argument("--endpoint-id")
    p_video.add_argument("--model")
    p_video.add_argument("--video-duration", type=int, default=5)
    p_video.add_argument("--fps", type=int, default=24)
    p_video.add_argument("--resolution", default="1080p")
    p_video.add_argument("--first-frame-image-url")
    p_video.add_argument("--ref-image-url", action="append")
    p_video.add_argument("--req-key")
    p_video.set_defaults(func=cmd_video)

    p_task = sub.add_parser("task", help="Query video task")
    p_task.add_argument("--task-id", required=True)
    p_task.set_defaults(func=cmd_task)

    p_wait = sub.add_parser("wait", help="Wait for video task completion")
    p_wait.add_argument("--task-id", required=True)
    p_wait.add_argument("--timeout", type=int, default=600)
    p_wait.add_argument("--interval", type=int, default=5)
    p_wait.add_argument("--download-to")
    p_wait.set_defaults(func=cmd_wait)

    return parser


def main():
    parser = build_parser()
    args = parser.parse_args()
    if not hasattr(args, "func"):
        parser.print_help()
        sys.exit(2)
    args.func(args)


if __name__ == "__main__":
    main()


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