
Generate Video By Seedance
- 64 installs
- 32 repo stars
- Updated April 1, 2026
- chunpu/agent-skills
Generates or edits videos via ByteDance Volcengine Ark Doubao Seedance models using a bundled script, with text-to-video and image-to-video via YAML config.
About
Generates or edits videos through ByteDance's Doubao Seedance models on Volcengine Ark using a bundled uv script. A developer uses it for text-to-video and single- or multi-image reference video generation.
- Supports text-to-video and image-to-video reference workflows
- YAML config for prompt, ratio, duration, and reference images
Generate Video By Seedance by the numbers
- 64 all-time installs (skills.sh)
- Ranked #855 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/chunpu/agent-skills --skill generate-video-by-seedanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 64 |
|---|---|
| repo stars | ★ 32 |
| Last updated | April 1, 2026 |
| Repository | chunpu/agent-skills ↗ |
What it does
Generates or edits videos via ByteDance Volcengine Ark Doubao Seedance models using a bundled script, with text-to-video and image-to-video via YAML config.
Files
豆包 Seedance 视频生成
使用随技能打包的脚本,通过字节跳动 Seedance 视频模型在火山引擎 Ark 上生成/编辑视频:
- 豆包 Seedance 系列视频模型(通过 Ark contents/generations/tasks 接口)
- 支持文生视频、图生视频(单图 / 多图参考)
默认设置:
- 默认模型:
doubao-seedance-1-5-pro-251215(可用--model覆盖) - 默认时长:
5秒 - 默认比例:
16:9
文生视频
uv run {baseDir}/scripts/generate_video.py \
--prompt "一个在草地上奔跑的小狗视频,阳光明媚,电影感" \
--filename "奔跑小狗.mp4"使用 YAML 配置文件
可以将所有生视频参数写入 YAML 配置文件,然后通过 --config 参数使用:
文生视频配置示例 (text2video.yaml):
prompt: 一个在草地上奔跑的小狗视频,阳光明媚,电影感
filename: 奔跑小狗.mp4
ratio: 16:9
duration: 5图生视频配置示例(带 URL 参考图列表,image2video.yaml):
prompt: 将图1的表情换为图2的表情,做一个眨眼微笑的短视频
filename: 表情女孩.mp4
images:
- https://ark-project.tos-cn-beijing.volces.com/doc_image/seedream4_imagesToimage_1.png
- https://ark-project.tos-cn-beijing.volces.com/doc_image/seedream4_5_imagesToimage_2.png
ratio: 9:16
duration: 5本地参考图配置示例(local_images.yaml):
prompt: 多图融合成一个动画短片
filename: 多图融合动画.mp4
images:
- /path/to/参考图1.jpg
- /path/to/参考图2.png
ratio: 16:9
duration: 8使用方法:
uv run {baseDir}/scripts/generate_video.py --config image2video.yaml混合使用配置文件和命令行参数(命令行参数优先级更高):
uv run {baseDir}/scripts/generate_video.py --config prompt.yaml --filename "自定义文件名.mp4"YAML 配置文件支持的所有参数:
prompt: 视频内容描述filename: 输出文件名images: 参考图像列表(可以是 URL 或本地文件路径)ratio: 画面比例(如 "16:9"、"9:16")duration: 视频时长(秒)model: 完整的模型名称(高级选项)api_key: API 密钥
图生视频 / 参考图(URL 或本地文件均可,多图,使用 lite i2v 模型)
uv run {baseDir}/scripts/generate_video.py \
--prompt "参考图片中的女孩,做一个挥手微笑的短视频" \
--filename "挥手女孩.mp4" \
-i "https://example.com/ref_image_1.png" \
-i "/path/to/local_ref_2.jpg" \
--ratio "9:16" \
--duration 8只用参考图(无显式文案提示)
uv run {baseDir}/scripts/generate_video.py \
--filename "仅参考图生成视频.mp4" \
-i "/path/to/ref1.jpg" \
-i "/path/to/ref2.png"提示:至少需要「提示词」或「参考图」其一存在,否则脚本会报错。
注意事项
API 密钥
- 需要设置
ARK_API_KEY环境变量
参数说明
- prompt:视频内容文案(中文 / 英文均可)
- image / -i:
- URL:直接透传给 Ark(例如公网可访问的图片链接)
- 本地文件:自动读取并转成
data:image/...;base64,...形式再发送 - 可多次传入,形成多图参考(多图目前仅在
doubao-seedance-1-0-lite-i2v-250428等 lite i2v 模型下生效) - ratio:画面比例,常用:
"16:9"(默认)"9:16""1:1""21:9"- duration:视频时长(秒),由模型 / Endpoint 实际支持的区间决定
- model(可选,高级):直接指定 Ark Endpoint / 模型 ID,覆盖默认绑定
轮询脚本 get_video_task_status.py 额外参数:
- interval:轮询间隔(秒),默认
10,可根据任务耗时与频率需求自行调整 - timeout:最大等待时间(秒),默认
600,超过后脚本会报错退出
结果与文件输出
- Ark 视频生成通常是异步任务:
- 脚本会调用
POST /contents/generations/tasks - 成功时优先解析返回的
id/task_id作为任务 ID - 如果响应体中直接携带
video_url,脚本会尝试拉取并将视频保存到本地 - 保存路径规则:
- 若
--filename未指定目录,则默认保存到本仓库下的outputs/目录 - 若文件名无后缀,则默认补上
.mp4
若调用成功但后端仅返回任务 ID、尚未生成 video_url,脚本会输出任务 ID,可配合轮询脚本一起使用:
# 1) 先创建任务(可能只返回 TASK_ID)
uv run {baseDir}/scripts/generate_video.py \
--prompt "一个示例视频" \
--filename "示例视频.mp4"
# 2) 拿到上一步输出的 TASK_ID 后,轮询直到生成完成并下载视频
uv run {baseDir}/scripts/get_video_task_status.py \
cgt-20260226184301-4h8v6 \
--filename "示例视频.mp4" \
--interval 5轮询脚本会:
- 定期调用
GET /contents/generations/tasks/{task_id}查看任务状态 - 当状态为
succeeded/completed且拿到video_url时,自动下载视频到本地
文件名推荐(供调用方参考)
- 不要在文件名里包含具体实现细节(如 "seedance"、"ark" 等)
- 文件名语言建议与 Prompt 语言一致,如中文 Prompt 就用简短中文文件名(如
挥手女孩.mp4) - 文件名要有语义但尽量简短,避免过长句子
关键词提示(便于发现 / 搜索命中)
- "doubao", "豆包", "Seedance"
- "ByteDance video model", "字节跳动视频生成"
- "文生视频", "图生视频", "视频生成", "短视频生成"
豆包 Seedance 视频生成
使用随技能打包的脚本,通过字节跳动 Seedance 视频模型在火山引擎 Ark 上生成/编辑视频:
- 豆包 Seedance 系列视频模型(通过 Ark contents/generations/tasks 接口)
- 支持文生视频、图生视频(单图 / 多图参考)
使用方法
文生视频
uv run {baseDir}/scripts/generate_video.py \
--prompt "一个在草地上奔跑的小狗视频,阳光明媚,电影感" \
--filename "奔跑小狗.mp4"使用 YAML 配置文件
可以将所有生视频参数写入 YAML 配置文件,然后通过 --config 参数使用:
文生视频配置示例 (text2video.yaml):
prompt: 一个在草地上奔跑的小狗视频,阳光明媚,电影感
filename: 奔跑小狗.mp4
ratio: 16:9
duration: 5图生视频配置示例(带 URL 参考图列表,image2video.yaml):
prompt: 将图1的表情换为图2的表情,做一个眨眼微笑的短视频
filename: 表情女孩.mp4
images:
- https://ark-project.tos-cn-beijing.volces.com/doc_image/seedream4_imagesToimage_1.png
- https://ark-project.tos-cn-beijing.volces.com/doc_image/seedream4_5_imagesToimage_2.png
ratio: 9:16
duration: 5本地参考图配置示例(local_images.yaml):
prompt: 多图融合成一个动画短片
filename: 多图融合动画.mp4
images:
- /path/to/参考图1.jpg
- /path/to/参考图2.png
ratio: 16:9
duration: 8使用方法:
uv run {baseDir}/scripts/generate_video.py --config image2video.yaml混合使用配置文件和命令行参数(命令行参数优先级更高):
uv run {baseDir}/scripts/generate_video.py --config prompt.yaml --filename "自定义文件名.mp4"YAML 配置文件支持的所有参数:
prompt: 视频内容描述filename: 输出文件名images: 参考图像列表(可以是 URL 或本地文件路径)ratio: 画面比例(如 "16:9"、"9:16")duration: 视频时长(秒)model: 完整的模型名称(高级选项)api_key: API 密钥
图生视频 / 参考图
uv run {baseDir}/scripts/generate_video.py \
--prompt "参考图片中的女孩,做一个挥手微笑的短视频" \
--filename "挥手女孩.mp4" \
-i "https://example.com/ref_image_1.png" \
-i "/path/to/local_ref_2.jpg" \
--ratio "9:16" \
--duration 8只用参考图(无显式文案提示)
uv run {baseDir}/scripts/generate_video.py \
--filename "仅参考图生成视频.mp4" \
-i "/path/to/ref1.jpg" \
-i "/path/to/ref2.png"注意事项
- API 密钥:需要设置
ARK_API_KEY环境变量。 - 比例:
"16:9"(默认)、"9:16"、"1:1"、"21:9"。 - 时长:视频时长(秒)。
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "requests>=2.31.0",
# "pyyaml>=6.0",
# ]
# ///
"""
Generate videos using Volcengine Ark Doubao Seedance video generation API.
This is a thin CLI wrapper around:
POST https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks
The request / response semantics roughly mirror the TypeScript example in
`generate-video.ts` (Seedance), but with a Python + CLI interface suitable
for OpenClaw skills.
"""
from __future__ import annotations
import argparse
import base64
import os
import sys
from pathlib import Path
from typing import List, Optional, Dict, Any
try:
import yaml
HAS_YAML = True
except ImportError:
HAS_YAML = False
def get_api_key(provided_key: Optional[str]) -> Optional[str]:
"""Get API key from argument first, then environment."""
if provided_key:
return provided_key
return os.environ.get("ARK_API_KEY")
def load_config_from_yaml(config_path: str) -> Dict[str, Any]:
"""Load and parse YAML config file."""
if not HAS_YAML:
print("Error: pyyaml is required to use --config option. Please install it.", file=sys.stderr)
sys.exit(1)
path = Path(config_path)
if not path.is_file():
print(f"Error: Config file does not exist or is not a file: {config_path}", file=sys.stderr)
sys.exit(1)
try:
with path.open("r", encoding="utf-8") as f:
config = yaml.safe_load(f)
if not isinstance(config, dict):
print("Error: Config file must be a YAML mapping/dictionary.", file=sys.stderr)
sys.exit(1)
return config
except Exception as e:
print(f"Error loading config file: {e}", file=sys.stderr)
sys.exit(1)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate videos using Volcengine Ark Doubao Seedance",
)
parser.add_argument(
"--config",
"-c",
help="Path to YAML config file containing all generation parameters.",
)
parser.add_argument(
"--prompt",
"-p",
help="Video generation / editing prompt (e.g. 在草地上奔跑的小狗,电影感).",
)
parser.add_argument(
"--filename",
"-f",
help=(
"Output filename (e.g. 奔跑小狗.mp4). "
"If no directory is given, it will be saved under outputs/."
),
)
parser.add_argument(
"--image",
"-i",
action="append",
dest="images",
metavar="IMAGE",
help=(
"Reference image(s) as URL or local path. "
"URLs are sent directly; local files are encoded as data URLs. "
"Can be specified multiple times."
),
)
parser.add_argument(
"--ratio",
help='Aspect ratio for the video, e.g. "16:9", "9:16", "1:1", "21:9".',
)
parser.add_argument(
"--duration",
type=int,
help="Video duration in seconds.",
)
parser.add_argument(
"--api-key",
"-k",
help="Ark API key (overrides ARK_API_KEY env var).",
)
parser.add_argument(
"--model",
help=(
"Advanced: override Ark Seedance endpoint / model name. "
"If not provided, a default Seedance endpoint will be used."
),
)
return parser.parse_args()
def merge_config_with_args(config: Dict[str, Any], args: argparse.Namespace) -> argparse.Namespace:
"""Merge config file settings with command line args. Command line args take precedence."""
merged = argparse.Namespace(**vars(args))
if "prompt" in config and not merged.prompt:
merged.prompt = str(config["prompt"])
if "filename" in config and not merged.filename:
merged.filename = str(config["filename"])
if "images" in config and not merged.images:
images_val = config["images"]
if isinstance(images_val, list):
merged.images = [str(img) for img in images_val]
else:
merged.images = [str(images_val)]
if "ratio" in config and not merged.ratio:
merged.ratio = str(config["ratio"])
if "duration" in config and not merged.duration:
duration_val = config["duration"]
if isinstance(duration_val, (int, float)):
merged.duration = int(duration_val)
else:
merged.duration = int(str(duration_val))
if "api_key" in config and not merged.api_key:
merged.api_key = str(config["api_key"])
if "model" in config and not merged.model:
merged.model = str(config["model"])
if not merged.filename:
print("Error: --filename is required (either via command line or config file).", file=sys.stderr)
sys.exit(1)
if not merged.ratio:
merged.ratio = "16:9"
if not merged.duration:
merged.duration = 5
return merged
def build_image_list(images: Optional[List[str]]) -> List[str]:
if not images:
return []
resolved: List[str] = []
for item in images:
if not item:
continue
item = item.strip()
if not item:
continue
# If already data URL or http(s) URL, pass through.
if item.startswith("data:") or item.startswith("http://") or item.startswith("https://"):
resolved.append(item)
continue
# Otherwise, assume local file path → data URL (Base64).
path = Path(item)
if not path.is_file():
print(f"Error: image path does not exist or is not a file: {item}", file=sys.stderr)
sys.exit(1)
ext = path.suffix.lower().lstrip(".")
if ext in ("jpg", "jpeg"):
fmt = "jpeg"
elif ext in ("png", "webp"):
fmt = ext
else:
fmt = ext or "jpeg"
print(
f"Warning: unrecognised image extension for '{item}', using '{fmt}' in data URL.",
file=sys.stderr,
)
with path.open("rb") as f:
b64 = base64.b64encode(f.read()).decode("ascii")
data_url = f"data:image/{fmt};base64,{b64}"
resolved.append(data_url)
return resolved
def build_content(prompt: Optional[str], images: List[str], model_name: str) -> List[dict]:
content: List[dict] = []
text_prompt = (prompt or "").strip()
if text_prompt:
content.append({"type": "text", "text": text_prompt})
# 模型相关的图片策略:
# - lite i2v 模型(如 doubao-seedance-1-0-lite-i2v-250428)支持多张 reference_image,
# role 字段与官方示例一致,放在 image_url 同级。
# - 其他 pro 系列模型目前只接受 1 张 image content,多张会直接 400。
is_lite_i2v = "lite-i2v" in (model_name or "")
if is_lite_i2v:
# 多图:全部作为 reference_image。
for img in images or []:
if not img:
continue
content.append(
{
"type": "image_url",
"image_url": {
"url": img,
},
"role": "reference_image",
}
)
else:
if images:
first = images[0]
if len(images) > 1:
print(
f"Warning: current model '{model_name}' only supports one image; "
f"got {len(images)}, using the first one.",
file=sys.stderr,
)
if first:
content.append(
{
"type": "image_url",
"image_url": {
"url": first,
},
}
)
return content
def resolve_output_path(filename: str) -> Path:
output_path = Path(filename)
if not output_path.parent or str(output_path.parent) == ".":
output_path = Path("outputs") / output_path.name
if output_path.suffix == "":
# Default to mp4 for video.
output_path = output_path.with_suffix(".mp4")
output_path.parent.mkdir(parents=True, exist_ok=True)
return output_path
def download_video(url: str, output_path: Path) -> None:
import requests
print(f"Downloading video from: {url}")
try:
resp = requests.get(url, stream=True, timeout=300)
except Exception as e:
print(f"Error downloading video from URL: {e}", file=sys.stderr)
sys.exit(1)
if resp.status_code != 200:
print(
f"Error: Failed to download video, HTTP {resp.status_code}",
file=sys.stderr,
)
try:
print(resp.text, file=sys.stderr)
except Exception:
pass
sys.exit(1)
try:
with output_path.open("wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if not chunk:
continue
f.write(chunk)
except Exception as e:
print(f"Error saving video to file: {e}", file=sys.stderr)
sys.exit(1)
def main() -> None:
args = parse_args()
# Load config from YAML if provided
config = {}
if args.config:
config = load_config_from_yaml(args.config)
args = merge_config_with_args(config, args)
api_key = get_api_key(args.api_key)
if not api_key:
print("Error: No Ark API key provided.", file=sys.stderr)
print("Please either:", file=sys.stderr)
print(" 1. Provide --api-key argument", file=sys.stderr)
print(" 2. Set ARK_API_KEY environment variable", file=sys.stderr)
print(" 3. Specify in config file", file=sys.stderr)
sys.exit(1)
# If neither prompt nor image is provided, reject early.
if not args.prompt and not args.images:
print("Error: prompt and reference images cannot both be empty.", file=sys.stderr)
print("Provide at least one of: --prompt or -i/--image.", file=sys.stderr)
sys.exit(1)
# Import requests lazily so CLI help is fast even without dependency.
import requests
endpoint = "https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks"
# Resolve model: explicit --model wins; otherwise use Seedance 1.5 pro by default.
model_name = args.model or "doubao-seedance-1-5-pro-251215"
images = build_image_list(args.images)
content = build_content(args.prompt, images, model_name)
if not content:
print("Error: failed to build Seedance input content (empty).", file=sys.stderr)
sys.exit(1)
payload: dict = {
"model": model_name,
"content": content,
"ratio": args.ratio,
"duration": args.duration,
"watermark": False,
# generate_audio can be added later if needed / supported.
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
print(f"Calling Ark Seedance video API with model={model_name}, ratio={args.ratio}, duration={args.duration}s...")
if images:
print(f"Using {len(images)} reference image(s) (URLs and/or local files).")
try:
resp = requests.post(endpoint, headers=headers, json=payload, timeout=60)
except Exception as e:
print(f"Error calling Ark video API: {e}", file=sys.stderr)
sys.exit(1)
if resp.status_code != 200:
print(f"Ark video API returned HTTP {resp.status_code}", file=sys.stderr)
try:
print(resp.text, file=sys.stderr)
except Exception:
pass
sys.exit(1)
try:
data = resp.json()
except Exception as e:
print(f"Failed to parse Ark video API JSON response: {e}", file=sys.stderr)
print(resp.text, file=sys.stderr)
sys.exit(1)
# Seedance task API is typically async. We try to be generous in what we accept:
# - Prefer id / task_id as the task identifier.
# - Try several locations for video_url-like fields.
task_id = data.get("id") or data.get("task_id")
video_url = (
(data.get("content") or {}).get("video_url")
or data.get("video_url")
or data.get("url")
)
if task_id:
print(f"Seedance video task created, task_id={task_id}")
# Some deployments may return data: [...] wrapping task info or URLs.
if not video_url:
d = data.get("data")
if isinstance(d, list) and d:
first = d[0]
if isinstance(first, dict):
video_url = (
(first.get("content") or {}).get("video_url")
or first.get("video_url")
or first.get("url")
)
elif isinstance(d, dict):
video_url = (
(d.get("content") or {}).get("video_url")
or d.get("video_url")
or d.get("url")
)
if not video_url:
print("No immediate video_url in response; task is likely still processing.", file=sys.stderr)
print("Raw response (truncated):", file=sys.stderr)
try:
print(str(data)[:2000], file=sys.stderr)
except Exception:
pass
# Still exit 0 so callers can parse task_id from stdout.
if task_id:
print(f"TASK_ID: {task_id}")
return
output_path = resolve_output_path(args.filename)
download_video(video_url, output_path)
full_path = output_path.resolve()
print(f"\nVideo saved: {full_path}")
# OpenClaw parses MEDIA tokens and will attach the file on supported providers.
print(f"MEDIA: {full_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "requests>=2.31.0",
# ]
# ///
"""
Poll Volcengine Ark Doubao Seedance video generation task status and
download the resulting video when it is ready.
Endpoint:
GET https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{task_id}
"""
from __future__ import annotations
import argparse
import os
import sys
import time
from pathlib import Path
from typing import Optional
def get_api_key(provided_key: Optional[str]) -> Optional[str]:
if provided_key:
return provided_key
return os.environ.get("ARK_API_KEY")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Poll Seedance video task status and download the video when ready.",
)
parser.add_argument(
"task_id",
help="Seedance video task ID, e.g. cgt-20260226184301-4h8v6.",
)
parser.add_argument(
"--filename",
"-f",
help=(
"Output filename (e.g. 完成视频.mp4). "
"If not given, defaults to <task_id>.mp4 under outputs/."
),
)
parser.add_argument(
"--interval",
type=int,
default=10,
help="Polling interval in seconds (default: 10).",
)
parser.add_argument(
"--timeout",
type=int,
default=600,
help="Maximum wait time in seconds before giving up (default: 600).",
)
parser.add_argument(
"--api-key",
"-k",
help="Ark API key (overrides ARK_API_KEY env var).",
)
return parser.parse_args()
def resolve_output_path(task_id: str, filename: Optional[str]) -> Path:
if filename:
path = Path(filename)
else:
path = Path("outputs") / f"{task_id}.mp4"
if not path.parent or str(path.parent) == ".":
path = Path("outputs") / path.name
if path.suffix == "":
path = path.with_suffix(".mp4")
path.parent.mkdir(parents=True, exist_ok=True)
return path
def download_video(url: str, output_path: Path) -> None:
import requests
print(f"Downloading video from: {url}")
try:
resp = requests.get(url, stream=True, timeout=300)
except Exception as e:
print(f"Error downloading video from URL: {e}", file=sys.stderr)
sys.exit(1)
if resp.status_code != 200:
print(
f"Error: Failed to download video, HTTP {resp.status_code}",
file=sys.stderr,
)
try:
print(resp.text, file=sys.stderr)
except Exception:
pass
sys.exit(1)
try:
with output_path.open("wb") as f:
for chunk in resp.iter_content(chunk_size=8192):
if not chunk:
continue
f.write(chunk)
except Exception as e:
print(f"Error saving video to file: {e}", file=sys.stderr)
sys.exit(1)
def fetch_task(task_id: str, api_key: str) -> dict:
import requests
endpoint = f"https://ark.cn-beijing.volces.com/api/v3/contents/generations/tasks/{task_id}"
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
}
resp = requests.get(endpoint, headers=headers, timeout=30)
if resp.status_code != 200:
print(f"Ark video task API returned HTTP {resp.status_code}", file=sys.stderr)
try:
print(resp.text, file=sys.stderr)
except Exception:
pass
sys.exit(1)
try:
return resp.json()
except Exception as e:
print(f"Failed to parse Ark video task JSON response: {e}", file=sys.stderr)
print(resp.text, file=sys.stderr)
sys.exit(1)
def extract_status_and_url(data: dict) -> tuple[str, Optional[str]]:
status = str(data.get("status") or "").lower() or "pending"
video_url = (
(data.get("content") or {}).get("video_url")
or data.get("video_url")
or data.get("url")
)
if not video_url:
d = data.get("data")
if isinstance(d, list) and d:
first = d[0]
if isinstance(first, dict):
video_url = (
(first.get("content") or {}).get("video_url")
or first.get("video_url")
or first.get("url")
)
elif isinstance(d, dict):
video_url = (
(d.get("content") or {}).get("video_url")
or d.get("video_url")
or d.get("url")
)
return status, video_url
def main() -> None:
args = parse_args()
api_key = get_api_key(args.api_key)
if not api_key:
print("Error: No Ark API key provided.", file=sys.stderr)
print("Please either:", file=sys.stderr)
print(" 1. Provide --api-key argument", file=sys.stderr)
print(" 2. Set ARK_API_KEY environment variable", file=sys.stderr)
sys.exit(1)
task_id = args.task_id.strip()
if not task_id:
print("Error: task_id cannot be empty.", file=sys.stderr)
sys.exit(1)
output_path = resolve_output_path(task_id, args.filename)
start = time.time()
last_status: Optional[str] = None
while True:
elapsed = time.time() - start
if elapsed > args.timeout:
print(
f"Timeout reached ({args.timeout}s). Last known status: {last_status or 'unknown'}.",
file=sys.stderr,
)
sys.exit(1)
data = fetch_task(task_id, api_key)
status, video_url = extract_status_and_url(data)
last_status = status
print(f"Task {task_id} status: {status}")
if status in ("succeeded", "success", "completed"):
if not video_url:
print("Task succeeded but no video_url found in response.", file=sys.stderr)
sys.exit(1)
download_video(video_url, output_path)
full_path = output_path.resolve()
print(f"\nVideo saved: {full_path}")
print(f"MEDIA: {full_path}")
return
if status in ("failed", "error"):
print("Task failed.", file=sys.stderr)
# Print truncated raw data for debugging.
try:
print(str(data)[:2000], file=sys.stderr)
except Exception:
pass
sys.exit(1)
# Otherwise, keep polling.
time.sleep(args.interval)
if __name__ == "__main__":
main()