Skill: juhemcp/juhe-video-generate (version 1.0.1)

--- name: juhe-video-generate description: AI视频创作。支持文生视频（文字描述生成视频）和图生视频（图片动态化），可指定分辨率、时长、宽高比，自动等待生成完成并下载视频到本地。使用场景：用户说"帮我生成一段夕阳海浪的视频"、"把这张图片生成动态视频"、"创作一段1080P的城市夜景延时视频"、"生成一个9:16竖版的产品展示短视频"、"做一段10秒的水墨山水动态画面"等。通过聚合数据（juhe.cn）API提交异步任务，自动轮询等待结果，生成完成后自动下载 mp4 视频文件保存到本地。 homepage: https://www.juhe.cn/docs/api/id/827 metadata: {"openclaw":{"emoji":"🎬","requires":{"bins":["python3"],"env":["JUHE_VIDEO_KEY"]},"primaryEnv":"JUHE_VIDEO_KEY"}} --- # AI视频创作 > 数据由 **[聚合数据](https://www.juhe.cn)** 提供 — 国内领先的数据服务平台，提供天气、快递、身份证、手机号、IP查询等 200+ 免费/低价 API。 支持两种视频创作模式： - **文生视频**：输入文字描述，AI 自动生成画面连贯、风格鲜明的短视频，支持指定分辨率、宽高比、时长 - **图生视频**：提供首帧图片，AI 让图片动起来，支持指定分辨率、时长，可选配音 生成完成后**自动下载 mp4 文件保存到本地**，同时返回在线访问链接（有效期24小时）。 --- ## 前置配置：获取 API Key 1. 前往 [聚合数据官网](https://www.juhe.cn) 免费注册账号 2. 进入 [AI视频创作 API](https://www.juhe.cn/docs/api/id/827) 页面，点击「申请使用」 3. 完成实名认证（本接口需要个人或企业实名认证，并提供应用场景说明） 4. 审核通过后在「我的API」中获取 AppKey 5. 配置 Key（**三选一**）： ```bash # 方式一：环境变量（推荐，一次配置永久生效） export JUHE_VIDEO_KEY=你的AppKey # 方式二：.env 文件（在脚本目录创建） echo "JUHE_VIDEO_KEY=你的AppKey" > scripts/.env # 方式三：每次命令行传入 python scripts/video_generate.py --key 你的AppKey "夕阳下的海边" ``` > 注意：视频生成为异步任务，通常需要 1~5 分钟，脚本会自动每 15 秒轮询一次，最长等待 6 分钟。 --- ## 使用方法 ### 文生视频（基础） ```bash python scripts/video_generate.py "夕阳下的海边，海浪轻柔拍打礁石，写实风格" ``` 输出示例： ``` 🎬 提交文生视频任务... 提示词: 夕阳下的海边，海浪轻柔拍打礁石，写实风格 分辨率: 480P 宽高比: 16:9 时长: 5秒 预计消耗: 5次 ✅ 任务已提交，订单号: JH819251208164211x1hC5 （如需中断，可稍后用 --query JH819251208164211x1hC5 重新查询） ⏳ 等待视频生成（视频通常需 1-5 分钟，最长等待 360s）... 状态: RUNNING... 已等待 60s ✅ 视频生成成功！ 订单号: JH819251208164211x1hC5 在线链接: https://...（24小时内有效） 📥 正在下载视频... 本地文件: output/video_20260327_153022.mp4 ``` ### 指定分辨率、宽高比、时长 ```bash # 1080P 横屏 10秒（消耗 3×10=30 次） python scripts/video_generate.py "城市夜景，霓虹灯闪烁" --resolution 1080P --proportion 16:9 --duration 10 # 720P 竖屏 5秒（适合短视频平台，消耗 2×5=10 次） python scripts/video_generate.py "唯美樱花林小径" --resolution 720P --proportion 9:16 # 480P 正方形 5秒（最省次数，消耗 1×5=5 次） python scripts/video_generate.py "云朵在天空飘动" --proportion 1:1 ``` 可用分辨率（直接影响消耗次数）： | 档位 | 系数 | 说明 | |------|------|------| | 480P（默认） | 1次/秒 | 最省次数，适合快速预览 | | 720P | 2次/秒 | 高清，适合社交平台 | | 1080P | 3次/秒 | 超清，适合正式场合 | 可用宽高比（仅文生视频支持）： | 比例 | 适用场景 | 备注 | |------|----------|------| | 16:9（默认） | 横屏视频、桌面展示 | | | 9:16 | 竖屏短视频（抖音/快手/视频号） | | | 1:1 | 方形社交媒体 | | | 4:3 | 传统横屏 | 480P 不支持 | | 3:4 | 传统竖屏 | 480P 不支持 | 可用时长： | 时长 | 说明 | |------|------| | 5秒（默认） | 最省次数 | | 10秒 | 中等时长 | | 15秒 | 最长，**480P 不支持** | > **计费公式**：次数消耗 = 分辨率系数 × 时长（秒） > 例如：1080P × 10秒 = 3 × 10 = **30次** ### 添加反向提示词 ```bash # 不希望出现的内容（限制画面元素） python scripts/video_generate.py "夏日海滩" --negative "人群、垃圾、建筑物" ``` ### 关闭提示词智能改写 ```bash # 默认开启智能改写（AI 会优化你的提示词） # --no-extend 关闭后，严格按照原始提示词生成 python scripts/video_generate.py "一只小猫追蝴蝶" --no-extend ``` ### 图生视频 ```bash # 图片 URL + 运动描述（prompt 为必填） python scripts/video_generate.py --image https://example.com/flower.jpg --prompt "让花朵慢慢绽放" # 指定分辨率和时长 python scripts/video_generate.py --image https://example.com/photo.jpg --prompt "手舞足蹈" --resolution 720P --duration 10 # 添加配音（wav/mp3，3-30秒，不超过10MB） python scripts/video_generate.py --image https://example.com/photo.jpg --prompt "人物微笑点头" --audio https://example.com/bgm.mp3 ``` 图片要求： - 格式：JPEG、JPG、PNG（不支持透明通道）、BMP、WEBP - 分辨率：宽高均在 360~2000 像素之间 - 文件大小：不超过 5MB - 支持 URL 或 Base64 编码（格式：`data:{MIME_type};base64,{base64_data}`） > 图生视频的宽高比由输入图片自动决定，不支持单独指定 `--proportion`。 ### 指定输出目录 ```bash python scripts/video_generate.py "城市夜景，霓虹灯闪烁" --output ~/Desktop/my_videos ``` ### 仅获取链接（不下载） ```bash python scripts/video_generate.py "水墨山水，云雾缭绕" --no-download ``` ### 查询已提交任务 任务提交后如需中断等待，可稍后手动查询： ```bash python scripts/video_generate.py --query JH819251208164211x1hC5 ``` --- ## AI 使用指南 当用户要求创建/生成/制作视频时，按以下步骤操作： 1. **判断模式** — 用户提供图片链接时使用图生视频模式（需同时有动作描述），否则使用文生视频模式 2. **提取参数** — 识别分辨率需求（默认480P）、宽高比（根据用途判断）、时长（默认5秒） 3. **优化提示词** — 扩充为包含主体、动态效果、风格、氛围的清晰描述，最多500字 4. **告知消耗** — 提前告知用户本次预计消耗次数（分辨率系数 × 时长） 5. **调用脚本** — 执行命令，脚本自动等待生成（1-5分钟） 6. **展示结果** — 告知本地保存路径，提示链接有效期24小时 ### 参数说明 **文生视频参数：** | 参数 | 必填 | 默认值 | 说明 | |------|------|--------|------| | 提示词（位置参数） | 是 | — | 视频内容描述，最多500字 | | `--resolution` | 否 | 480P | 分辨率：480P / 720P / 1080P | | `--proportion` | 否 | 16:9 | 宽高比：16:9 / 9:16 / 1:1 / 4:3 / 3:4 | | `--duration` | 否 | 5 | 视频时长（秒）：5 / 10 / 15 | | `--negative` | 否 | — | 反向提示词，最多300字 | | `--no-extend` | 否 | — | 关闭提示词智能改写 | | `--output` | 否 | output/ | 视频保存目录 | | `--no-download` | 否 | — | 只返回链接，不下载 | | `--key` | 否 | — | 临时传入 API Key | **图生视频参数：** | 参数 | 必填 | 默认值 | 说明 | |------|------|--------|------| | `--image` | 是 | — | 首帧图片 URL 或 Base64 编码 | | `--prompt` | 是 | — | 运动描述，最多500字 | | `--resolution` | 否 | 480P | 分辨率：480P / 720P / 1080P | | `--duration` | 否 | 5 | 视频时长（秒）：5 / 10 / 15 | | `--negative` | 否 | — | 反向提示词，最多300字 | | `--audio` | 否 | — | 配音文件 URL（wav/mp3，3-30秒） | | `--no-extend` | 否 | — | 关闭提示词智能改写 | ### 提示词优化建议 好的提示词能获得更高质量的视频： | 要素 | 示例 | |------|------| | 主体动态 | 海浪拍打礁石、城市车流光轨、花朵慢慢绽放 | | 风格 | 写实摄影风、电影感、慢动作、延时摄影、动漫风 | | 镜头运动 | 缓慢推进、由远及近、俯视旋转、平滑横移 | | 色彩/光线 | 黄金时刻暖光、蓝调夜景、自然漫射光 | | 场景氛围 | 宁静祥和、壮观震撼、浪漫唯美、科技感十足 | ### 错误处理 | 情况 | 处理方式 | |------|----------| | `error_code` 10001/10002 | API Key 无效或无权限，引导用户至 [聚合数据](https://www.juhe.cn/docs/api/id/827) 重新申请 | | `error_code` 10012 | 今日调用次数已用尽，建议升级套餐 | | `error_code` 282701 | 参数错误，检查分辨率/宽高比/时长组合是否合法，或图片链接是否可访问 | | `error_code` 282702 | 订单不存在，确认 orderid 是否正确 | | `error_code` 282703/704/705 | 服务端异常，稍后重试 | | 480P + 15秒 | 不支持此组合，建议改为 720P 或缩短时长 | | 480P + 4:3/3:4 | 不支持此宽高比，建议改用其他比例或升级分辨率 | | 状态 FAILED | 生成失败，建议修改提示词后重试 | | 等待超时（>360s） | 使用 `--query <orderid>` 稍后查询结果 | | 图片链接无法访问 | 确保 URL 为公网可访问，不支持本地路径 | | 视频下载失败 | 手动访问 videourl 链接保存视频（24小时内有效） | --- ## 脚本位置 `scripts/video_generate.py` — 封装了任务提交、状态轮询（每15秒）、视频下载和错误处理，生成的视频默认保存至脚本同目录的 `output/` 文件夹。 --- ## 关于聚合数据 [聚合数据（juhe.cn）](https://www.juhe.cn) 是国内专业的 API 数据服务平台，提供包括： - **AI 能力**：视频创作、图像生成、图像编辑、图像理解、语音合成 - **交通出行**：火车时刻查询、航班动态、快递追踪 - **生活服务**：天气预报、万年历、节假日查询 - **身份核验**：手机号归属地、身份证实名验证 - **金融数据**：汇率、股票、黄金价格 注册即可免费使用，适合个人开发者和企业接入。

path: scripts/video_generate.py
content: #!/usr/bin/env python3
"""
AI视频创作脚本 — 由聚合数据 (juhe.cn) 提供数据支持
支持文生视频（文字描述生成视频）和图生视频（图片动态化生成视频）
生成完成后自动下载视频到本地

用法（文生视频）:
    python video_generate.py "夕阳下的海边，海浪轻柔拍打礁石，写实风格"
    python video_generate.py "城市夜景" --resolution 1080P --duration 10 --proportion 16:9
    python video_generate.py "唯美樱花" --negative "落叶、枯枝" --no-extend

用法（图生视频）:
    python video_generate.py --image https://example.com/photo.jpg --prompt "让花朵慢慢开放"
    python video_generate.py --image https://example.com/photo.jpg --prompt "手舞足蹈" --resolution 720P

用法（查询已有订单）:
    python video_generate.py --query JH819251208164211x1hC5

API Key 配置（任选其一，优先级从高到低）:
    1. 环境变量: export JUHE_VIDEO_KEY=your_api_key
    2. 脚本同目录的 .env 文件: JUHE_VIDEO_KEY=your_api_key
    3. 直接传参: python video_generate.py --key your_api_key "提示词"

免费申请 API Key: https://www.juhe.cn/docs/api/id/827
"""

import sys
import os
import json
import time
import urllib.request
import urllib.parse
from pathlib import Path
from datetime import datetime

TEXT2VIDEO_URL = "https://gpt.juhe.cn/text2video/generate"
IMAGE2VIDEO_URL = "https://gpt.juhe.cn/text2video/baseimg"
QUERY_URL = "https://gpt.juhe.cn/text2video/query"
REGISTER_URL = "https://www.juhe.cn/docs/api/id/827"

MAX_PROMPT_LEN = 500
MAX_NEGATIVE_LEN = 300
POLL_INTERVAL = 15
MAX_WAIT_SECONDS = 600

VALID_RESOLUTIONS = {"480P", "720P", "1080P"}
VALID_PROPORTIONS = {"16:9", "9:16", "1:1", "4:3", "3:4"}
VALID_DURATIONS = {"5", "10", "15"}

RESOLUTION_COST = {"480P": 1, "720P": 2, "1080P": 3}


def load_api_key(cli_key: str = None) -> str:
    """按优先级加载 API Key"""
    if cli_key:
        return cli_key

    env_key = os.environ.get("JUHE_VIDEO_KEY", "").strip()
    if env_key:
        return env_key

    env_file = Path(__file__).parent / ".env"
    if env_file.exists():
        for line in env_file.read_text(encoding="utf-8").splitlines():
            line = line.strip()
            if line.startswith("JUHE_VIDEO_KEY="):
                val = line.split("=", 1)[1].strip().strip('"').strip("'")
                if val:
                    return val

    return ""


def _post_json(url: str, params: dict, timeout: int = 30) -> dict:
    """发送 POST 请求并返回 JSON"""
    data = urllib.parse.urlencode(params).encode("utf-8")
    req = urllib.request.Request(
        url,
        data=data,
        method="POST",
        headers={"Content-Type": "application/x-www-form-urlencoded"},
    )
    try:
        with urllib.request.urlopen(req, timeout=timeout) as resp:
            return json.loads(resp.read().decode("utf-8"))
    except Exception as e:
        raise RuntimeError(f"网络请求失败: {e}") from e


def create_text2video(
    prompt: str,
    api_key: str,
    resolution: str = "480P",
    proportion: str = "16:9",
    duration: str = "5",
    negative_prompt: str = "",
    prompt_extend: int = 1,
    notify_url: str = "",
) -> dict:
    """调用文生视频接口，提交生成任务"""
    if len(prompt) > MAX_PROMPT_LEN:
        prompt = prompt[:MAX_PROMPT_LEN]
        print(f"⚠️  提示词超过 {MAX_PROMPT_LEN} 字符，已自动截断")

    params = {
        "key": api_key,
        "prompt": prompt,
        "resolution": resolution,
        "proportion": proportion,
        "duration": duration,
        "promptExtend": prompt_extend,
    }
    if negative_prompt:
        params["negativePrompt"] = negative_prompt[:MAX_NEGATIVE_LEN]
    if notify_url:
        params["notifyUrl"] = notify_url

    try:
        result = _post_json(TEXT2VIDEO_URL, params)
    except RuntimeError as e:
        return {"success": False, "error": str(e)}

    return _parse_create_response(result)


def create_image2video(
    image: str,
    prompt: str,
    api_key: str,
    resolution: str = "480P",
    duration: str = "5",
    negative_prompt: str = "",
    audio_url: str = "",
    prompt_extend: int = 1,
    notify_url: str = "",
) -> dict:
    """调用图生视频接口，提交生成任务"""
    if len(prompt) > MAX_PROMPT_LEN:
        prompt = prompt[:MAX_PROMPT_LEN]
        print(f"⚠️  提示词超过 {MAX_PROMPT_LEN} 字符，已自动截断")

    params = {
        "key": api_key,
        "prompt": prompt,
        "image": image,
        "resolution": resolution,
        "duration": duration,
        "promptExtend": prompt_extend,
    }
    if negative_prompt:
        params["negativePrompt"] = negative_prompt[:MAX_NEGATIVE_LEN]
    if audio_url:
        params["audioUrl"] = audio_url
    if notify_url:
        params["notifyUrl"] = notify_url

    try:
        result = _post_json(IMAGE2VIDEO_URL, params)
    except RuntimeError as e:
        return {"success": False, "error": str(e)}

    return _parse_create_response(result)


def _parse_create_response(result: dict) -> dict:
    """解析创建任务的响应"""
    error_code = result.get("error_code", -1)
    if error_code == 0:
        res = result.get("result", {})
        return {
            "success": True,
            "orderid": res.get("orderid", ""),
            "orderstatus": res.get("orderstatus", "PENDING"),
        }

    reason = result.get("reason", "提交失败")
    hint = _error_hint(error_code)
    return {"success": False, "error": f"{reason}{hint}", "error_code": error_code}


def query_result(orderid: str, api_key: str) -> dict:
    """查询视频生成结果（POST 方式）"""
    params = {
        "key": api_key,
        "orderId": orderid,
    }

    try:
        result = _post_json(QUERY_URL, params)
    except RuntimeError as e:
        return {"success": False, "error": str(e)}

    error_code = result.get("error_code", -1)
    if error_code == 0:
        res = result.get("result", {})
        return {
            "success": True,
            "orderid": res.get("orderid", orderid),
            "orderstatus": res.get("orderstatus", ""),
            "videourl": res.get("videourl", ""),
            "code": res.get("code", 0),
            "message": res.get("message", ""),
        }

    reason = result.get("reason", "查询失败")
    hint = _error_hint(error_code)
    return {"success": False, "error": f"{reason}{hint}", "error_code": error_code}


def _error_hint(error_code: int) -> str:
    """根据错误码返回提示信息"""
    hints = {
        10001: f"\n   请检查 API Key 是否正确，免费申请：{REGISTER_URL}",
        10002: f"\n   该 Key 无请求权限，请前往 {REGISTER_URL} 申请接口",
        10012: "\n   今日调用次数已用尽，请升级套餐",
        282701: "\n   参数错误，请检查提示词、分辨率、时长或图片链接是否符合要求",
        282702: "\n   订单不存在，请确认 orderid 是否正确",
        282703: "\n   网络错误，请稍后重试",
        282704: "\n   查询失败，请稍后重试",
        282705: "\n   数据源异常，请稍后重试",
    }
    return hints.get(error_code, "")


def poll_until_done(orderid: str, api_key: str) -> dict:
    """轮询查询结果，直到生成完成或超时"""
    elapsed = 0
    attempt = 0

    while elapsed < MAX_WAIT_SECONDS:
        attempt += 1
        result = query_result(orderid, api_key)

        if not result["success"]:
            return result

        code = result.get("code", 0)
        status = result["orderstatus"]

        if status == "SUCCEEDED" or code == 1:
            return result
        elif status == "FAILED" or code == 2:
            msg = result.get("message", "视频生成失败")
            return {"success": False, "error": f"生成失败: {msg}"}
        else:
            dots = "." * ((attempt % 3) + 1)
            print(f"\r   状态: {status}{dots:<4} 已等待 {elapsed}s", end="", flush=True)
            time.sleep(POLL_INTERVAL)
            elapsed += POLL_INTERVAL

    return {
        "success": False,
        "error": (
            f"等待超时（>{MAX_WAIT_SECONDS}s），视频仍在生成中\n"
            f"   请稍后使用以下命令查询：\n"
            f"   python video_generate.py --query {orderid}"
        ),
    }


def download_video(url: str, output_dir: Path) -> str | None:
    """下载视频到本地，返回保存路径或 None"""
    output_dir.mkdir(parents=True, exist_ok=True)

    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"video_{timestamp}.mp4"
    save_path = output_dir / filename

    try:
        print(f"\n📥 正在下载视频...")
        urllib.request.urlretrieve(url, save_path)
        return str(save_path)
    except Exception as e:
        print(f"⚠️  视频下载失败: {e}")
        return None


def calc_cost(resolution: str, duration: str) -> int:
    """计算本次生成消耗的次数"""
    return RESOLUTION_COST.get(resolution, 1) * int(duration)


def parse_args(args: list) -> dict:
    """解析命令行参数"""
    result = {
        "cli_key": None,
        "prompt": None,
        "image": None,
        "resolution": "480P",
        "proportion": "16:9",
        "duration": "5",
        "negative": "",
        "audio": "",
        "no_extend": False,
        "notify_url": "",
        "output": None,
        "no_download": False,
        "query": None,
        "error": None,
    }

    i = 0
    positional = []

    while i < len(args):
        arg = args[i]

        def next_val(flag):
            nonlocal i
            if i + 1 < len(args):
                val = args[i + 1]
                i += 2
                return val
            result["error"] = f"错误: {flag} 后需要提供值"
            return None

        if arg == "--key":
            v = next_val("--key"); result["cli_key"] = v if v else result["cli_key"]
        elif arg == "--image":
            v = next_val("--image"); result["image"] = v
        elif arg == "--prompt":
            v = next_val("--prompt"); result["prompt"] = v
        elif arg == "--resolution":
            v = next_val("--resolution")
            if v:
                if v.upper() not in VALID_RESOLUTIONS:
                    result["error"] = f"错误: 无效的分辨率 '{v}'，可选: 480P、720P、1080P"
                    return result
                result["resolution"] = v.upper()
        elif arg == "--proportion":
            v = next_val("--proportion")
            if v:
                if v not in VALID_PROPORTIONS:
                    result["error"] = f"错误: 无效的宽高比 '{v}'，可选: 16:9、9:16、1:1、4:3、3:4"
                    return result
                result["proportion"] = v
        elif arg == "--duration":
            v = next_val("--duration")
            if v:
                if v not in VALID_DURATIONS:
                    result["error"] = f"错误: 无效的时长 '{v}'，可选: 5、10、15（秒）"
                    return result
                result["duration"] = v
        elif arg == "--negative":
            v = next_val("--negative"); result["negative"] = v or ""
        elif arg == "--audio":
            v = next_val("--audio"); result["audio"] = v or ""
        elif arg == "--notify-url":
            v = next_val("--notify-url"); result["notify_url"] = v or ""
        elif arg == "--no-extend":
            result["no_extend"] = True
            i += 1
        elif arg == "--output":
            v = next_val("--output"); result["output"] = v
        elif arg == "--no-download":
            result["no_download"] = True
            i += 1
        elif arg == "--query":
            v = next_val("--query"); result["query"] = v
        else:
            positional.append(arg)
            i += 1

        if result["error"]:
            return result

    if positional and result["prompt"] is None:
        result["prompt"] = " ".join(positional)

    # 参数组合校验
    if result["query"] is None:
        if result["image"] is None and result["prompt"] is None:
            result["error"] = (
                "错误: 请提供提示词（文生视频）或 --image 参数（图生视频）\n"
                "用法: python video_generate.py [选项] <提示词>\n"
                '示例: python video_generate.py "夕阳下的海边，写实风格"\n'
                '      python video_generate.py --image https://example.com/img.jpg --prompt "手舞足蹈"\n'
                f"\n免费申请 API Key: {REGISTER_URL}"
            )
            return result

        if result["image"] is not None and result["prompt"] is None:
            result["error"] = "错误: 图生视频模式下 --prompt 为必填参数，请描述期望的动态效果"
            return result

        res = result["resolution"]
        dur = result["duration"]
        prop = result["proportion"]

        if dur == "15" and res == "480P":
            result["error"] = "错误: 480P 分辨率不支持 15 秒时长，请选择 5 或 10 秒，或将分辨率升级为 720P/1080P"
            return result

        if result["image"] is None and prop in ("4:3", "3:4") and res == "480P":
            result["error"] = f"错误: 480P 分辨率不支持 {prop} 宽高比，请选择其他宽高比或升级分辨率"
            return result

    return result


def print_usage():
    print("用法: python video_generate.py [选项] <提示词>")
    print()
    print("文生视频示例:")
    print('  python video_generate.py "夕阳下的海边，海浪轻柔拍打礁石"')
    print('  python video_generate.py "城市夜景，霓虹灯闪烁" --resolution 1080P --duration 10')
    print('  python video_generate.py "唯美樱花" --proportion 9:16 --negative "落叶、枯枝"')
    print()
    print("图生视频示例:")
    print('  python video_generate.py --image https://example.com/photo.jpg --prompt "手舞足蹈"')
    print('  python video_generate.py --image https://example.com/photo.jpg --prompt "花朵开放" --resolution 720P')
    print()
    print("查询已有订单:")
    print("  python video_generate.py --query JH819251208164211x1hC5")
    print()
    print("通用选项:")
    print("  --resolution <档位>  分辨率: 480P(默认,1次/秒)、720P(2次/秒)、1080P(3次/秒)")
    print("  --duration <秒>      时长: 5(默认)、10、15（480P不支持15秒）")
    print("  --negative <文字>    反向提示词，描述不希望出现的内容（最多300字）")
    print("  --no-extend          关闭提示词智能改写（默认开启）")
    print("  --output <目录>      视频保存目录（默认：output/）")
    print("  --no-download        仅返回链接，不下载视频")
    print("  --key <KEY>          临时传入 API Key")
    print()
    print("文生视频专属选项:")
    print("  --proportion <比例>  宽高比: 16:9(默认)、9:16、1:1、4:3、3:4（480P不支持4:3/3:4）")
    print()
    print("图生视频专属选项:")
    print("  --audio <URL>        配音文件链接（wav/mp3，3-30秒，不超过10MB）")
    print()
    print("计费说明: 次数消耗 = 分辨率系数 × 时长（秒）")
    print("  示例: 720P × 10秒 = 2 × 10 = 20次")
    print(f"\n免费申请 API Key: {REGISTER_URL}")


def main():
    args = sys.argv[1:]

    if not args:
        print_usage()
        sys.exit(1)

    parsed = parse_args(args)
    if parsed["error"]:
        print(parsed["error"])
        sys.exit(1)

    [REDACTED_SECRET]"cli_key"])
    if not [REDACTED_SECRET]"❌ 未找到 API Key，请通过以下方式之一配置：")
        print("   1. 环境变量: export JUHE_VIDEO_KEY=your_api_key")
        print("   2. .env 文件: 在脚本目录创建 .env，写入 JUHE_VIDEO_KEY=your_api_key")
        print("   3. 命令行参数: python video_generate.py --key your_api_key <提示词>")
        print(f"\n免费申请 Key: {REGISTER_URL}")
        sys.exit(1)

    output_dir = (
        Path(parsed["output"]).expanduser().resolve()
        if parsed["output"]
        else Path(__file__).parent / "output"
    )

    # 仅查询已有订单
    if parsed["query"]:
        orderid = parsed["query"]
        print(f"\n🔍 查询订单 {orderid} 的生成结果...\n")
        result = query_result(orderid, api_key)
        if not result["success"]:
            print(f"❌ 查询失败: {result['error']}")
            sys.exit(1)
        _handle_result(result, output_dir, parsed["no_download"])
        return

    resolution = parsed["resolution"]
    duration = parsed["duration"]
    prompt_extend = 0 if parsed["no_extend"] else 1
    cost = calc_cost(resolution, duration)

    # 提交生成任务
    if parsed["image"]:
        imgurl = parsed["image"]
        prompt = parsed["prompt"]
        print(f"\n🎬 提交图生视频任务...")
        print(f"   图片链接: {imgurl}")
        print(f"   运动描述: {prompt[:80]}{'...' if len(prompt) > 80 else ''}")
        print(f"   分辨率: {resolution}  时长: {duration}秒  预计消耗: {cost}次")
        print()
        submit_result = create_image2video(
            image=imgurl,
            prompt=prompt,
            [REDACTED_SECRET],
            resolution=resolution,
            duration=duration,
            negative_prompt=parsed["negative"],
            audio_url=parsed["audio"],
            prompt_extend=prompt_extend,
            notify_url=parsed["notify_url"],
        )
    else:
        prompt = parsed["prompt"]
        proportion = parsed["proportion"]
        print(f"\n🎬 提交文生视频任务...")
        print(f"   提示词: {prompt[:80]}{'...' if len(prompt) > 80 else ''}")
        print(f"   分辨率: {resolution}  宽高比: {proportion}  时长: {duration}秒  预计消耗: {cost}次")
        print()
        submit_result = create_text2video(
            prompt=prompt,
            [REDACTED_SECRET],
            resolution=resolution,
            proportion=proportion,
            duration=duration,
            negative_prompt=parsed["negative"],
            prompt_extend=prompt_extend,
            notify_url=parsed["notify_url"],
        )

    if not submit_result["success"]:
        print(f"❌ 任务提交失败: {submit_result['error']}")
        sys.exit(1)

    orderid = submit_result["orderid"]
    print(f"✅ 任务已提交，订单号: {orderid}")
    print(f"   （如需中断，可稍后用 --query {orderid} 重新查询）\n")

    # 轮询等待生成完成
    print(f"⏳ 等待视频生成（视频通常需 1-5 分钟，最长等待 {MAX_WAIT_SECONDS}s）...")
    poll_result = poll_until_done(orderid, api_key)
    print()

    if not poll_result["success"]:
        print(f"❌ {poll_result['error']}")
        sys.exit(1)

    _handle_result(poll_result, output_dir, parsed["no_download"])


def _handle_result(result: dict, output_dir: Path, no_download: bool) -> None:
    """处理生成成功的结果：展示链接并可选下载"""
    video_url = result.get("videourl", "")
    orderid = result.get("orderid", "")
    status = result.get("orderstatus", "")

    if not video_url:
        if status in ("PENDING", "RUNNING"):
            print(f"⏳ 视频仍在生成中（状态: {status}），请稍后查询：")
            if orderid:
                print(f"   python video_generate.py --query {orderid}")
        else:
            print(f"⚠️  未获取到视频链接（状态: {status}）")
        return

    print("✅ 视频生成成功！")
    if orderid:
        print(f"   订单号: {orderid}")
    print(f"   在线链接: {video_url}")
    print(f"   ⚠️  链接有效期为 24 小时，请及时下载保存")

    if not no_download:
        save_path = download_video(video_url, output_dir)
        if save_path:
            print(f"   本地文件: {save_path}")
        else:
            print("   请手动访问上方链接保存视频")
    else:
        print("\n（已跳过下载，请手动访问链接保存视频）")

    print()


if __name__ == "__main__":
    main()


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