Qwen Video Generation Generate videos using Wan models. All tasks are asynchronous — submit, then poll until completion. This skill is part of qwencloud/qwencloud-ai . ⚠️ Critical Parameter Differences: - kf2v (First+Last Frame) : Duration is fixed at 5 seconds — other values will fail. Output is silent only . - Resolution parameters vary by model family, not mode alone : Fetch the current model catalog before choosing , , or . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details. | Location | Purp…

\n for candidate in candidates:\n m = re.search(pattern, candidate)\n if m:\n agent, skill = m.group(1) or \"default\", m.group(2)\n if len(agent) \u003c= 32 and len(skill) \u003c= 32:\n return json.dumps({\"channel\": \"qwencloud-skill\", \"tags\": {\"t1\": skill, \"t2\": agent}}, separators=(',', ':'))\n return None\n\n\n# ---------------------------------------------------------------------------\n# Section 6: HTTP infrastructure\n# ---------------------------------------------------------------------------\n\n_RETRYABLE_CODES = frozenset({429, 500, 502, 503, 504})\n\n\ndef http_request(\n method: str,\n url: str,\n api_key: str,\n payload: dict[str, Any] | None = None,\n *,\n extra_headers: dict[str, str] | None = None,\n timeout: int = 120,\n retries: int = 2,\n backoff: float = 1.5,\n) -> dict[str, Any]:\n \"\"\"Generic HTTP request with retry and exponential backoff.\n\n Handles JSON serialisation, provider header injection, and retryable\n HTTP codes.\n \"\"\"\n hdrs = get_provider().make_headers(api_key, payload)\n if extra_headers:\n hdrs.update(extra_headers)\n data = json.dumps(payload).encode(\"utf-8\") if payload else None\n last_err = \"\"\n for attempt in range(retries + 1):\n req = urllib.request.Request(url, data=data, headers=hdrs, method=method)\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n return json.loads(resp.read().decode(\"utf-8\"))\n except urllib.error.HTTPError as exc:\n body = \"\"\n try:\n body = exc.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n if api_key and len(api_key) > 8 and api_key in body:\n body = body.replace(api_key, mask_key(api_key))\n last_err = f\"HTTP {exc.code}: {sanitize_diagnostic(body)[:500]}\"\n if exc.code not in _RETRYABLE_CODES or attempt >= retries:\n if exc.code == 401 and is_token_plan_key(api_key):\n print(\n \"Hint: if this model is not on Token Plan, see supported \"\n \"models at https://docs.qwencloud.com/token-plan\",\n file=sys.stderr,\n )\n raise RuntimeError(last_err) from None\n time.sleep(backoff * (2 ** attempt))\n except urllib.error.URLError as exc:\n last_err = sanitize_diagnostic(str(exc.reason))\n if attempt >= retries:\n raise RuntimeError(f\"Network error: {last_err}\") from None\n time.sleep(backoff * (2 ** attempt))\n raise RuntimeError(last_err)\n\n\ndef http_post(\n url: str,\n api_key: str,\n payload: dict[str, Any],\n *,\n timeout: int = 120,\n retries: int = 2,\n backoff: float = 1.5,\n) -> dict[str, Any]:\n \"\"\"Convenience: non-streaming POST, returns parsed JSON.\"\"\"\n return http_request(\n \"POST\", url, api_key, payload,\n timeout=timeout, retries=retries, backoff=backoff,\n )\n\n\ndef _sse_error_message(\n chunk: Any,\n event_type: str,\n api_key: str,\n) -> Optional[str]:\n \"\"\"Return a safe error message for an SSE error payload, if present.\"\"\"\n error_payload = chunk.get(\"error\") if isinstance(chunk, dict) else None\n has_top_level_error = isinstance(chunk, dict) and (\n not chunk.get(\"choices\")\n and chunk.get(\"code\") is not None\n and chunk.get(\"message\") is not None\n )\n if event_type.lower() != \"error\" and error_payload is None and not has_top_level_error:\n return None\n\n payload = error_payload if error_payload is not None else chunk\n if isinstance(payload, dict):\n code = payload.get(\"code\") or payload.get(\"type\")\n message = payload.get(\"message\")\n if code and message:\n detail = f\"{code}: {message}\"\n elif code or message:\n detail = str(code or message)\n else:\n detail = json.dumps(payload, ensure_ascii=False)\n else:\n detail = str(payload)\n\n if api_key and len(api_key) > 8 and api_key in detail:\n detail = detail.replace(api_key, mask_key(api_key))\n return f\"SSE error: {sanitize_diagnostic(detail)[:500]}\"\n\n\ndef stream_sse(\n url: str,\n api_key: str,\n payload: dict[str, Any],\n *,\n timeout: int = 180,\n) -> Iterator[dict[str, Any]]:\n \"\"\"Streaming POST. Yields parsed SSE ``data:`` chunks.\n\n Automatically sets ``stream: true`` in the payload and sends the\n appropriate ``Accept`` header.\n \"\"\"\n payload[\"stream\"] = True\n hdrs = get_provider().make_headers(api_key, payload)\n hdrs[\"Accept\"] = \"text/event-stream\"\n data = json.dumps(payload).encode(\"utf-8\")\n req = urllib.request.Request(url, data=data, headers=hdrs, method=\"POST\")\n try:\n resp = urllib.request.urlopen(req, timeout=timeout)\n except urllib.error.HTTPError as exc:\n body = \"\"\n try:\n body = exc.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n if api_key and len(api_key) > 8 and api_key in body:\n body = body.replace(api_key, mask_key(api_key))\n raise RuntimeError(f\"HTTP {exc.code}: {sanitize_diagnostic(body)[:500]}\") from None\n except urllib.error.URLError as exc:\n raise RuntimeError(f\"Network error: {sanitize_diagnostic(str(exc.reason))}\") from None\n\n buf = b\"\"\n event_type = \"\"\n try:\n while True:\n raw = resp.read(4096)\n if not raw:\n break\n buf += raw\n while b\"\\n\" in buf:\n line_bytes, buf = buf.split(b\"\\n\", 1)\n line = line_bytes.decode(\"utf-8\", errors=\"replace\").strip()\n if not line:\n event_type = \"\"\n continue\n if line.startswith(\"event:\"):\n event_type = line[6:].strip()\n continue\n if not line.startswith(\"data:\"):\n continue\n json_str = line[5:].strip()\n if json_str == \"[DONE]\":\n return\n try:\n chunk = json.loads(json_str)\n except json.JSONDecodeError:\n if event_type.lower() == \"error\":\n detail = json_str\n if api_key and len(api_key) > 8 and api_key in detail:\n detail = detail.replace(api_key, mask_key(api_key))\n detail = sanitize_diagnostic(detail)[:500]\n raise RuntimeError(f\"SSE error: {detail}\") from None\n pass\n else:\n error_message = _sse_error_message(chunk, event_type, api_key)\n if error_message:\n raise RuntimeError(error_message)\n yield chunk\n event_type = \"\"\n finally:\n resp.close()\n\n\n# ---------------------------------------------------------------------------\n# Section 7: File upload / download\n# ---------------------------------------------------------------------------\n\n_BASE64_FILE_LIMIT = 7 * 1024 * 1024 # 7 MB (base64 adds ~33%; API limit is 10 MB)\n\n\ndef upload_local_file(api_key: str, model: str, fp: Path) -> str:\n \"\"\"Upload a local file to the active provider's temp storage.\n\n Returns a provider-managed URL (e.g. ``oss://`` for DashScope).\n \"\"\"\n return get_provider().upload_file(api_key, model, fp)\n\n\ndef resolve_file(\n value: str,\n *,\n api_key: str | None = None,\n model: str | None = None,\n) -> str:\n \"\"\"Resolve a file reference for API consumption.\n\n URLs (``http``, ``https``, ``data``, and provider-managed schemes)\n pass through unchanged. Local files are handled based on context:\n\n - *api_key* + *model* provided: upload to temp storage.\n - Otherwise: convert to ``data:`` base64 URI (must be \u003c 7 MB).\n \"\"\"\n provider = get_provider()\n pass_through = (\"http://\", \"https://\", \"data:\") + provider.managed_url_schemes()\n if value.startswith(pass_through):\n return value\n if value.startswith(\"file://\"):\n parsed = urllib.parse.urlparse(value)\n if parsed.netloc and parsed.netloc.lower() != \"localhost\":\n raise ValueError(\n f\"Cannot resolve non-local file URI authority {parsed.netloc!r}; \"\n \"only an empty authority or 'localhost' is supported.\"\n )\n value = urllib.request.url2pathname(parsed.path)\n p = Path(value)\n if not (p.exists() and p.is_file()):\n return value\n\n file_size = p.stat().st_size\n\n if api_key and model:\n managed_url = provider.upload_file(api_key, model, p)\n tag = \"48 h TTL\" if managed_url.startswith(\"oss://\") else \"custom OSS\"\n print(sanitize_diagnostic(f\"Uploaded {p.name} -> {managed_url} ({tag})\"), file=sys.stderr)\n return managed_url\n\n if file_size > _BASE64_FILE_LIMIT:\n print(\n f\"Warning: {p.name} is {file_size / 1024 / 1024:.1f} MB -- \"\n \"base64 may exceed the 10 MB API limit. \"\n \"Use --upload-files to auto-upload, or provide an online URL.\",\n file=sys.stderr,\n )\n\n mime = mimetypes.guess_type(p.name)[0] or \"application/octet-stream\"\n b64 = base64.b64encode(p.read_bytes()).decode(\"ascii\")\n return f\"data:{mime};base64,{b64}\"\n\n\ndef download_file(url: str, dest: Path, *, timeout: int = 120) -> Path:\n \"\"\"Download a file from *url* to *dest*, creating parent dirs as needed.\"\"\"\n dest.parent.mkdir(parents=True, exist_ok=True)\n req = urllib.request.Request(url, headers={\"User-Agent\": \"qwencloud-skills\"})\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n dest.write_bytes(resp.read())\n except urllib.error.URLError as exc:\n raise RuntimeError(f\"Download failed: {sanitize_diagnostic(str(exc))}\") from None\n return dest\n\n\n# ---------------------------------------------------------------------------\n# Section 8: Request / response utilities\n# ---------------------------------------------------------------------------\n\ndef load_request(args: Any) -> dict[str, Any]:\n \"\"\"Load request dict from ``--request`` or ``--file`` CLI argument.\"\"\"\n if getattr(args, \"request\", None):\n return json.loads(args.request)\n if getattr(args, \"file\", None):\n return json.loads(Path(args.file).read_text(encoding=\"utf-8\"))\n raise ValueError(\"Provide --request '{...}' or --file path/to/request.json\")\n\n\ndef save_result(result: dict[str, Any], output_path: str | Path) -> None:\n \"\"\"Write *result* as JSON to *output_path*, creating parent dirs.\"\"\"\n p = Path(output_path)\n p.parent.mkdir(parents=True, exist_ok=True)\n p.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n\n\ndef extract_text(content: Any) -> str:\n \"\"\"Extract plain text from a message ``content`` field.\n\n Handles ``str``, ``list[{type, text}]``, and ``None``.\n \"\"\"\n if isinstance(content, str):\n return content\n if isinstance(content, list):\n for item in content:\n if isinstance(item, dict) and item.get(\"type\") == \"text\":\n text = item.get(\"text\")\n if isinstance(text, str):\n return text\n return json.dumps(content, ensure_ascii=False)\n if content is None:\n return \"\"\n return json.dumps(content, ensure_ascii=False)\n\n\ndef try_parse_json(text: str) -> Any | None:\n \"\"\"Attempt to parse *text* as JSON; return ``None`` on failure.\"\"\"\n text = text.strip()\n if not text:\n return None\n try:\n return json.loads(text)\n except json.JSONDecodeError:\n return None\n\n\n# ---------------------------------------------------------------------------\n# Section 9: Async task polling\n# ---------------------------------------------------------------------------\n\ndef poll_task(\n task_id: str,\n api_key: str,\n *,\n timeout_s: int = 600,\n interval: int = 10,\n verbose: bool = True,\n) -> dict[str, Any]:\n \"\"\"Poll an async task until it reaches a terminal state.\n\n Returns the final result dict. Raises ``TimeoutError`` if\n *timeout_s* is exceeded.\n \"\"\"\n provider = get_provider()\n url = provider.task_poll_url(task_id)\n terminals = provider.terminal_statuses()\n start = time.time()\n poll_count = 0\n while True:\n result = http_request(\"GET\", url, api_key)\n elapsed = int(time.time() - start)\n poll_count += 1\n status = provider.extract_task_status(result)\n if verbose:\n print(f\" [{elapsed}s] task={task_id} status={status}\", file=sys.stderr)\n if status in terminals:\n return result\n if time.time() - start > timeout_s:\n raise TimeoutError(\n f\"Task {task_id} timed out after {timeout_s}s \"\n f\"({poll_count} polls, last status: {status})\"\n )\n time.sleep(interval)\n\n\n# ---------------------------------------------------------------------------\n# Section 10: Update-check signal\n# ---------------------------------------------------------------------------\n\ntry:\n from gossamer import run as _run_update_signal_impl\nexcept ImportError:\n _run_update_signal_impl = None # type: ignore[assignment]\n\n\ndef run_update_signal(caller: str | Path | None = None) -> None:\n \"\"\"Emit update-check signals to stderr (non-blocking, failure-safe).\n\n Parameters\n ----------\n caller : str or Path, optional\n ``__file__`` of the calling script.\n \"\"\"\n if _run_update_signal_impl:\n try:\n _run_update_signal_impl(caller=caller or __file__)\n except Exception:\n pass\n","content_type":"text/x-python; charset=utf-8","language":"python","size":49931,"content_sha256":"fadc836540173f9a5b1dd531f3b266931e181b2cf94c1d5bbae892c15469ed33"},{"filename":"scripts/video_lib.py","content":"\"\"\"Video generation helpers: mode constants, payload builders, result extraction.\n\nShared library for video.py. Contains all mode constants, model defaults,\nendpoint paths, payload construction, and result parsing/formatting logic.\nStdlib only -- no pip install required.\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\n\nfrom qwencloud_lib import load_cdn_model_config, resolve_file, sanitize_diagnostic # noqa: E402\n\n# ---------------------------------------------------------------------------\n# Mode constants\n# ---------------------------------------------------------------------------\n\nMODE_T2V = \"t2v\"\nMODE_I2V = \"i2v\"\nMODE_KF2V = \"kf2v\"\nMODE_R2V = \"r2v\"\nMODE_VACE = \"vace\"\nMODE_VIDEO_EDIT = \"videoedit\"\nMODE_ANIMATE = \"animate\"\n\n_MODEL_CONFIG_FILE = \"qwencloud-video-generation-config.json\"\n_MODEL_CONFIG_DIR = Path(__file__).resolve().parent.parent / \"cdn\" / \"config\"\n_MODEL_CONFIG_ARRAY_KEYS = (\n \"wan27_t2v_models\",\n \"wan27_i2v_models\",\n \"wan30_video_models\",\n \"animate_models\",\n \"happyhorse_i2v_models\",\n \"happyhorse_r2v_models\",\n \"wan27_r2v_models\",\n \"video_edit_models\",\n \"happyhorse_video_edit_models\",\n)\n\n\ndef _validate_model_config(config: dict[str, Any]) -> bool:\n defaults = config.get(\"default_models\")\n if not isinstance(defaults, dict) or not all(\n isinstance(mode, str) and isinstance(model, str) and model\n for mode, model in defaults.items()\n ):\n return False\n required_modes = {\n MODE_T2V, MODE_I2V, MODE_KF2V, MODE_R2V,\n MODE_VACE, MODE_VIDEO_EDIT, MODE_ANIMATE,\n }\n if not required_modes.issubset(defaults):\n return False\n if not all(\n isinstance(config.get(key), list)\n and bool(config[key])\n and all(isinstance(item, str) and item for item in config[key])\n for key in _MODEL_CONFIG_ARRAY_KEYS\n ):\n return False\n return (\n defaults[MODE_T2V] in config[\"wan27_t2v_models\"]\n and defaults[MODE_I2V] in (\n config[\"wan27_i2v_models\"] + config[\"happyhorse_i2v_models\"]\n )\n and defaults[MODE_R2V] in (\n config[\"wan27_r2v_models\"] + config[\"happyhorse_r2v_models\"]\n )\n and defaults[MODE_VIDEO_EDIT] in config[\"video_edit_models\"]\n and defaults[MODE_ANIMATE] in config[\"animate_models\"]\n and set(config[\"wan30_video_models\"]).issubset(config[\"wan27_t2v_models\"])\n and set(config[\"wan30_video_models\"]).issubset(config[\"wan27_i2v_models\"])\n and set(config[\"happyhorse_video_edit_models\"]).issubset(config[\"video_edit_models\"])\n )\n\n\ndef _model_config() -> dict[str, Any]:\n return load_cdn_model_config(\n _MODEL_CONFIG_FILE,\n local_dir=_MODEL_CONFIG_DIR,\n required_keys=(\"default_models\",) + _MODEL_CONFIG_ARRAY_KEYS,\n validator=_validate_model_config,\n )\n\n\ndef _model_ids(key: str) -> frozenset[str]:\n return frozenset(_model_config()[key])\n\n\ndef get_default_model(mode: str) -> str:\n model = _model_config()[\"default_models\"].get(mode)\n if not isinstance(model, str) or not model:\n raise RuntimeError(f\"Invalid video model configuration: missing default for {mode}\")\n return model\n\n\ndef is_wan27_i2v_model(model: str) -> bool:\n return model in _model_ids(\"wan27_i2v_models\")\n\n\ndef is_happyhorse_i2v_model(model: str) -> bool:\n return model in _model_ids(\"happyhorse_i2v_models\")\n\n\ndef is_video_edit_model(model: str) -> bool:\n return model in _model_ids(\"video_edit_models\")\n\n\ndef is_animate_model(model: str) -> bool:\n return model in _model_ids(\"animate_models\")\n\n# wan2.7 and wan3.0 models use the same modes but with different payload structure.\n# happyhorse-1.0/1.1-t2v share the wan2.7-t2v structure (resolution + ratio,\n# per openapi-happyhorse-text-to-video.json: resolution/ratio/duration/watermark/seed).\n# wan3.0 spec supports the `ratio` parameter (adaptive/16:9/9:16/1:1) in BOTH t2v\n# and i2v (openapi-wan3-video-generation*.json); wan2.7-i2v derives the ratio from\n# the first frame and does not accept it. Reference: qianwen-ai video_lib\n# _build_wan30_video_payload passes ratio through.\n# Image-to-animation models (openapi-wan-image-to-animation.json /\n# openapi-wan-character-swap.json): input={image_url, video_url} +\n# parameters.mode (wan-std/wan-pro, REQUIRED). No prompt. Output is a video\n# (image2video endpoint), so they live in this video skill.\n# happyhorse-i2v diverges from wan2.7-i2v: media must be EXACTLY ONE\n# {type:'first_frame', url} and parameters are limited to\n# resolution/duration/watermark/seed (openapi-happyhorse-image-to-video.json).\n# happyhorse-r2v uses media[{type:\"reference_image\", url}] + resolution+ratio\n# wan2.7-r2v uses input.media = [{type, url}] mixed image/video/audio refs (max 5)\n_WAN27_R2V_MAX_MEDIA = 5\n\n# Video-edit models share a unified media protocol (no `function` field):\n# input.media = [{type:\"video\", url}] + [{type:\"reference_image\", url}, ...].\n# wan2.7-videoedit: prompt optional, negative_prompt + ratio/duration/prompt_extend.\n# happyhorse-1.0-video-edit: prompt REQUIRED, only resolution/watermark/\n# audio_setting/seed (openapi-happyhorse-video-editing.json).\nENDPOINTS: dict[str, str] = {\n MODE_T2V: \"/services/aigc/video-generation/video-synthesis\",\n MODE_I2V: \"/services/aigc/video-generation/video-synthesis\",\n MODE_KF2V: \"/services/aigc/image2video/video-synthesis\",\n MODE_R2V: \"/services/aigc/video-generation/video-synthesis\",\n MODE_VACE: \"/services/aigc/video-generation/video-synthesis\",\n MODE_VIDEO_EDIT: \"/services/aigc/video-generation/video-synthesis\",\n MODE_ANIMATE: \"/services/aigc/image2video/video-synthesis\",\n}\n\n_PRICING_URL = \"https://docs.qwencloud.com/developer-guides/getting-started/pricing\"\n\n# ---------------------------------------------------------------------------\n# Mode detection\n# ---------------------------------------------------------------------------\n\ndef detect_mode(request: dict[str, Any], model: str = \"\") -> str:\n \"\"\"Auto-detect video generation mode from request fields.\"\"\"\n # Highest priority: model-routed modes whose requests carry no discriminating\n # fields (animate has image_url+video_url; video-edit models have no `function`).\n if is_animate_model(model):\n return MODE_ANIMATE\n if is_video_edit_model(model):\n return MODE_VIDEO_EDIT\n # happyhorse-i2v: media/img_url/first_frame_url all mean i2v mode\n # (a bare first_frame_url would otherwise be misclassified as kf2v).\n if is_happyhorse_i2v_model(model):\n return MODE_I2V\n if request.get(\"function\"):\n return MODE_VACE\n if request.get(\"reference_urls\"):\n return MODE_R2V\n # happyhorse-r2v / wan2.7-r2v may be triggered by model id alone (media provided in input)\n if (model in _model_ids(\"happyhorse_r2v_models\")\n or model in _model_ids(\"wan27_r2v_models\")):\n return MODE_R2V\n # wan2.7-i2v uses media array or first_clip_url\n if request.get(\"media\") or request.get(\"first_clip_url\"):\n return MODE_I2V\n if request.get(\"first_frame_url\"):\n return MODE_KF2V\n if request.get(\"img_url\") or request.get(\"reference_image\"):\n return MODE_I2V\n return MODE_T2V\n\n# ---------------------------------------------------------------------------\n# File resolution helpers\n# ---------------------------------------------------------------------------\n\nRESOLVE_KEYS: dict[str, list[str]] = {\n MODE_T2V: [\"audio_url\"],\n MODE_I2V: [\"img_url\", \"reference_image\", \"audio_url\",\n \"first_frame_url\", \"last_frame_url\", \"driving_audio_url\", \"first_clip_url\",\n \"media\"],\n MODE_KF2V: [\"first_frame_url\", \"last_frame_url\"],\n MODE_R2V: [\"reference_urls\", \"media\"],\n MODE_VACE: [\"video_url\", \"mask_image_url\", \"mask_video_url\",\n \"ref_images_url\", \"first_clip_url\", \"last_clip_url\",\n \"first_frame_url\", \"last_frame_url\"],\n MODE_VIDEO_EDIT: [\"video_url\", \"reference_images\", \"media\"],\n MODE_ANIMATE: [\"image_url\", \"video_url\"],\n}\n\n\ndef resolve_request_urls(request: dict[str, Any], api_key: str, model: str,\n keys: list[str]) -> None:\n \"\"\"In-place resolve local file paths to OSS URLs for the given request keys.\"\"\"\n for key in keys:\n val = request.get(key)\n if val is None:\n continue\n if isinstance(val, str):\n request[key] = resolve_file(val, api_key=api_key, model=model)\n elif isinstance(val, list):\n if val and isinstance(val[0], dict):\n # Handle list[dict] structure (e.g. media: [{type: \"...\", url: \"...\"}])\n for item in val:\n if isinstance(item, dict) and \"url\" in item:\n url_val = item[\"url\"]\n if isinstance(url_val, str):\n item[\"url\"] = resolve_file(url_val, api_key=api_key, model=model)\n else:\n # Handle list[str] (e.g. reference_urls: [\"url1\", \"url2\"])\n request[key] = [resolve_file(str(v), api_key=api_key, model=model) for v in val]\n\n# ---------------------------------------------------------------------------\n# Payload builders\n# ---------------------------------------------------------------------------\n\ndef _require_prompt(request: dict[str, Any], mode: str, model: str) -> None:\n \"\"\"Raise ValueError when a prompt-driven mode has no top-level prompt.\n\n Detects the common mistake of passing the API-native nested format\n ({\"input\": {\"prompt\": ...}}) instead of this script's flat format.\n \"\"\"\n if request.get(\"prompt\"):\n return\n msg = (f\"{model} ({mode}) requires a non-empty top-level 'prompt'. \"\n \"Request JSON uses FLAT fields, e.g. \"\n '{\"prompt\":\"A cat playing piano\"} — not the API-native nested '\n '{\"input\":{\"prompt\":...}} format.')\n if isinstance(request.get(\"input\"), dict):\n msg += (\" Detected nested 'input' object in your request — \"\n \"flatten its keys (prompt/media/...) to the top level.\")\n raise ValueError(msg)\n\n\ndef build_t2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for text-to-video generation (wan2.6/wan2.7).\"\"\"\n _require_prompt(request, MODE_T2V, model)\n is_v27 = model in _model_ids(\"wan27_t2v_models\")\n\n input_obj: dict[str, Any] = {\"prompt\": request.get(\"prompt\", \"\")}\n if request.get(\"negative_prompt\"):\n input_obj[\"negative_prompt\"] = request[\"negative_prompt\"]\n if request.get(\"audio_url\"):\n input_obj[\"audio_url\"] = request[\"audio_url\"]\n\n params: dict[str, Any] = {\"duration\": request.get(\"duration\", 5)}\n\n if is_v27:\n # wan2.7-t2v: uses resolution and ratio\n params[\"resolution\"] = request.get(\"resolution\", \"1080P\")\n if request.get(\"ratio\"):\n params[\"ratio\"] = request[\"ratio\"]\n else:\n # wan2.6-t2v: uses size\n if request.get(\"size\"):\n params[\"size\"] = request[\"size\"]\n for key in (\"seed\", \"shot_type\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n for key in (\"prompt_extend\", \"watermark\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_i2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for image-to-video generation (wan2.6/wan2.7/happyhorse).\n\n Routing (front-loaded predicates -- different model families MUST NOT\n share a builder, even when payloads look superficially similar):\n 1. happyhorse-1.0-i2v / happyhorse-1.1-i2v -> _build_happyhorse_i2v_payload (strict spec)\n 2. wan2.7-i2v / wan3.0 / explicit media[] / first_clip_url -> _build_i2v_v27_payload\n 3. wan2.6-i2v fallback (single img_url)\n \"\"\"\n # 1) happyhorse-i2v has its own strict spec; never fall through.\n if is_happyhorse_i2v_model(model):\n return _build_happyhorse_i2v_payload(request, model)\n\n is_v27 = is_wan27_i2v_model(model)\n\n # wan2.7-i2v uses media array\n if is_v27 or request.get(\"media\") or request.get(\"first_clip_url\"):\n return _build_i2v_v27_payload(request, model)\n\n # wan2.6-i2v uses img_url\n _require_prompt(request, MODE_I2V, model)\n img_url = request.get(\"img_url\") or request.get(\"reference_image\", \"\")\n input_obj: dict[str, Any] = {\"prompt\": request.get(\"prompt\", \"\"), \"img_url\": img_url}\n if request.get(\"negative_prompt\"):\n input_obj[\"negative_prompt\"] = request[\"negative_prompt\"]\n if request.get(\"audio_url\"):\n input_obj[\"audio_url\"] = request[\"audio_url\"]\n\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"720P\"),\n \"duration\": request.get(\"duration\", 5),\n }\n for key in (\"prompt_extend\", \"watermark\", \"seed\", \"shot_type\", \"template\", \"audio\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef _build_happyhorse_i2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for happyhorse-1.0-i2v / happyhorse-1.1-i2v.\n\n Per the official QwenCloud API specification:\n - input: ONLY `prompt` (optional) and `media` (required).\n - media: must be EXACTLY ONE item with `type='first_frame'`.\n - parameters: ONLY `resolution` (480P/720P/1080P) / `duration` / `watermark` / `seed`.\n - REJECTED by server (will 400, verified by API testing) if any of these\n are sent: `negative_prompt`, `prompt_extend`, `ratio`, `last_frame`,\n `driving_audio`, `first_clip`, `audio_url`.\n\n Backward-compat: accept wan2.6-style `img_url` / `reference_image` and\n auto-promote to media=[{type:'first_frame', url:...}].\n \"\"\"\n # 1) Build media (exactly one first_frame).\n media = request.get(\"media\")\n if media:\n if (not isinstance(media, list) or len(media) != 1\n or not isinstance(media[0], dict)\n or media[0].get(\"type\") != \"first_frame\"\n or not media[0].get(\"url\")):\n raise ValueError(\n f\"{model} requires media=[{{type:'first_frame', url:...}}] \"\n \"with exactly one item.\"\n )\n else:\n url = (request.get(\"first_frame_url\")\n or request.get(\"img_url\")\n or request.get(\"reference_image\"))\n if not url:\n raise ValueError(\n f\"{model} requires a first frame image. Provide via \"\n \"`first_frame_url`, `img_url`, or \"\n \"media=[{type:'first_frame', url:...}].\"\n )\n media = [{\"type\": \"first_frame\", \"url\": url}]\n\n # 2) Warn-and-drop unsupported input fields (avoid silent server 400).\n for unsupported in (\"negative_prompt\", \"last_frame_url\", \"first_clip_url\",\n \"driving_audio_url\", \"audio_url\"):\n if request.get(unsupported):\n print(\n f\"Warning: {model} does not accept `{unsupported}`; dropped. \"\n \"Use wan2.7-i2v if you need this feature.\",\n file=sys.stderr,\n )\n\n input_obj: dict[str, Any] = {\"media\": media}\n if request.get(\"prompt\"):\n input_obj[\"prompt\"] = request[\"prompt\"]\n\n # 3) Build parameters (whitelist only).\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"1080P\"),\n \"duration\": request.get(\"duration\", 5),\n }\n for key in (\"watermark\", \"seed\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n # 4) Warn-and-drop unsupported parameters.\n for unsupported in (\"prompt_extend\", \"ratio\"):\n if request.get(unsupported) is not None:\n print(\n f\"Warning: {model} does not accept parameter `{unsupported}`; \"\n \"dropped.\",\n file=sys.stderr,\n )\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef _build_i2v_v27_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for wan2.7-i2v unified image-to-video generation.\n\n Serves wan2.7-i2v / wan3.0-video / wan3.0-video-prime ONLY. happyhorse-i2v\n has its own dedicated builder (_build_happyhorse_i2v_payload) -- do NOT\n add happyhorse compat code here.\n\n Supports: first_frame, last_frame, driving_audio, first_clip (video continuation).\n \"\"\"\n media: list[dict[str, str]] = []\n\n if request.get(\"media\"):\n media = request[\"media\"]\n else:\n if request.get(\"first_frame_url\"):\n media.append({\"type\": \"first_frame\", \"url\": request[\"first_frame_url\"]})\n if request.get(\"last_frame_url\"):\n media.append({\"type\": \"last_frame\", \"url\": request[\"last_frame_url\"]})\n if request.get(\"driving_audio_url\"):\n media.append({\"type\": \"driving_audio\", \"url\": request[\"driving_audio_url\"]})\n if request.get(\"first_clip_url\"):\n media.append({\"type\": \"first_clip\", \"url\": request[\"first_clip_url\"]})\n\n if not media:\n raise ValueError(\n \"wan2.7-i2v requires at least one media asset. \"\n \"Use first_frame_url, first_clip_url, or media array.\"\n )\n\n input_obj: dict[str, Any] = {\"media\": media}\n if request.get(\"prompt\"):\n input_obj[\"prompt\"] = request[\"prompt\"]\n if request.get(\"negative_prompt\"):\n input_obj[\"negative_prompt\"] = request[\"negative_prompt\"]\n\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"1080P\"),\n \"duration\": request.get(\"duration\", 5),\n }\n # wan3.0 series: pass `ratio` through (spec supports adaptive/16:9/9:16/1:1).\n # wan2.7-i2v does NOT accept ratio (auto-derived from the first frame).\n if model in _model_ids(\"wan30_video_models\") and request.get(\"ratio\") is not None:\n params[\"ratio\"] = request[\"ratio\"]\n for key in (\"prompt_extend\", \"watermark\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_kf2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for keyframe-to-video generation.\"\"\"\n input_obj: dict[str, Any] = {\n \"first_frame_url\": request.get(\"first_frame_url\", \"\"),\n \"prompt\": request.get(\"prompt\", \"\"),\n }\n if request.get(\"last_frame_url\"):\n input_obj[\"last_frame_url\"] = request[\"last_frame_url\"]\n if request.get(\"template\"):\n input_obj[\"template\"] = request[\"template\"]\n\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"720P\"),\n \"duration\": 5, # Fixed at 5 seconds per qwdocs\n }\n for key in (\"prompt_extend\", \"watermark\", \"seed\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_r2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for reference-based role-play video generation.\"\"\"\n # wan2.7-r2v uses input.media = [{type, url}] mixed refs (image/video/audio).\n if model in _model_ids(\"wan27_r2v_models\"):\n return _build_r2v_wan27_payload(request, model)\n # happyhorse-r2v uses media array [{type:\"reference_image\", url}] + resolution+ratio.\n if model in _model_ids(\"happyhorse_r2v_models\"):\n return _build_r2v_happyhorse_payload(request, model)\n\n # wan2.6-r2v: uses reference_urls + size\n _require_prompt(request, MODE_R2V, model)\n input_obj: dict[str, Any] = {\n \"prompt\": request.get(\"prompt\", \"\"),\n \"reference_urls\": request.get(\"reference_urls\", []),\n }\n params: dict[str, Any] = {\n \"size\": request.get(\"size\", \"1280*720\"),\n \"duration\": request.get(\"duration\", 5),\n }\n for key in (\"shot_type\", \"watermark\", \"audio\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef _build_r2v_happyhorse_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for happyhorse-r2v reference-to-video.\n\n Structure: input.media = [{type:\"reference_image\", url}, ...] + resolution + ratio.\n Up to 9 reference images supported.\n \"\"\"\n _require_prompt(request, MODE_R2V, model)\n media: list[dict[str, str]] = []\n # Accept reference_urls=[url, ...] (backward-compat shorthand)\n for url in (request.get(\"reference_urls\") or []):\n media.append({\"type\": \"reference_image\", \"url\": str(url)})\n # Accept media=[{type, url}, ...] (native format)\n for item in (request.get(\"media\") or []):\n if isinstance(item, dict):\n media.append(item)\n else:\n media.append({\"type\": \"reference_image\", \"url\": str(item)})\n if not media:\n raise ValueError(\n \"happyhorse-r2v requires at least one reference image. \"\n \"Provide via reference_urls=[...] or media=[{type:reference_image, url}].\"\n )\n\n input_obj: dict[str, Any] = {\n \"prompt\": request.get(\"prompt\", \"\"),\n \"media\": media,\n }\n if request.get(\"negative_prompt\"):\n input_obj[\"negative_prompt\"] = request[\"negative_prompt\"]\n\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"720P\"),\n \"duration\": request.get(\"duration\", 5),\n }\n for key in (\"ratio\", \"watermark\", \"seed\", \"prompt_extend\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef _build_r2v_wan27_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for wan2.7-r2v multi-reference video generation.\n\n Verified contract (tmp/test-new-models/wan2.7-r2v):\n - input.media = [{type, url}, ...] where type in\n {reference_image, reference_video, reference_audio}; up to 5 mixed refs.\n Media items may carry extra fields such as reference_voice (voice cloning)\n and are passed through as-is.\n - input.prompt: required (passthrough).\n - parameters: resolution (720P/1080P) + duration (\u003c=10s) + prompt_extend\n + watermark + seed.\n - NO `ratio` (auto-derived from references), NO `fps`.\n\n Backward-compat: accept `reference_urls=[...]` (bare strings promoted to\n reference_image) in addition to explicit media=[{type, url}].\n \"\"\"\n _require_prompt(request, MODE_R2V, model)\n media: list[dict[str, str]] = []\n for url in (request.get(\"reference_urls\") or []):\n media.append({\"type\": \"reference_image\", \"url\": str(url)})\n for item in (request.get(\"media\") or []):\n if isinstance(item, dict):\n media.append(item)\n else:\n media.append({\"type\": \"reference_image\", \"url\": str(item)})\n\n if not media:\n raise ValueError(\n f\"{model} requires at least one reference. Provide via \"\n \"reference_urls=[...] or media=[{type:reference_image|reference_video|\"\n \"reference_audio, url}].\"\n )\n if len(media) > _WAN27_R2V_MAX_MEDIA:\n raise ValueError(\n f\"{model} accepts at most {_WAN27_R2V_MAX_MEDIA} reference media items \"\n f\"(got {len(media)}).\"\n )\n\n input_obj: dict[str, Any] = {\n \"prompt\": request.get(\"prompt\", \"\"),\n \"media\": media,\n }\n\n params: dict[str, Any] = {\n \"resolution\": request.get(\"resolution\", \"720P\"),\n \"duration\": request.get(\"duration\", 5),\n }\n for key in (\"prompt_extend\", \"watermark\", \"seed\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_vace_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for VACE video editing (repainting, extension, outpainting, etc.).\"\"\"\n func = request[\"function\"]\n input_obj: dict[str, Any] = {\"function\": func}\n\n if request.get(\"prompt\"):\n input_obj[\"prompt\"] = request[\"prompt\"]\n\n url_fields = [\n \"video_url\", \"mask_image_url\", \"mask_video_url\",\n \"first_clip_url\", \"last_clip_url\",\n \"first_frame_url\", \"last_frame_url\",\n ]\n for field in url_fields:\n if request.get(field):\n input_obj[field] = request[field]\n\n if request.get(\"ref_images_url\"):\n input_obj[\"ref_images_url\"] = request[\"ref_images_url\"]\n if request.get(\"mask_frame_id\") is not None:\n input_obj[\"mask_frame_id\"] = request[\"mask_frame_id\"]\n\n params: dict[str, Any] = {}\n param_keys = [\n \"prompt_extend\", \"size\", \"watermark\", \"obj_or_bg\",\n \"control_condition\", \"strength\", \"mask_type\", \"expand_ratio\",\n \"top_scale\", \"bottom_scale\", \"left_scale\", \"right_scale\",\n ]\n for key in param_keys:\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_video_edit_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for wan2.7-videoedit and happyhorse-1.0-video-edit.\n\n Unified media protocol (no `function` field):\n input.media = [{type:\"video\", url}] + [{type:\"reference_image\", url}, ...]\n\n Parameter differences (per official OpenAPI specs):\n - wan2.7-videoedit (openapi-wan27-video-editing.json): prompt optional,\n negative_prompt supported; parameters resolution/ratio/duration/\n audio_setting/prompt_extend/watermark/seed.\n - happyhorse-1.0-video-edit (openapi-happyhorse-video-editing.json):\n prompt REQUIRED; only resolution/watermark/audio_setting/seed\n (ratio/duration/prompt_extend/negative_prompt are not in the spec).\n \"\"\"\n media: list[dict[str, str]] = []\n if request.get(\"video_url\"):\n media.append({\"type\": \"video\", \"url\": str(request[\"video_url\"])})\n for item in (request.get(\"media\") or []):\n if isinstance(item, dict):\n media.append(item)\n else:\n media.append({\"type\": \"reference_image\", \"url\": str(item)})\n for url in (request.get(\"reference_images\") or []):\n media.append({\"type\": \"reference_image\", \"url\": str(url)})\n\n if not media:\n raise ValueError(\n f\"{model} requires at least one media asset. \"\n \"Provide video_url=... and/or media=[{type:'video', url}, ...].\"\n )\n\n input_obj: dict[str, Any] = {\"media\": media}\n if request.get(\"prompt\"):\n input_obj[\"prompt\"] = request[\"prompt\"]\n\n params: dict[str, Any] = {}\n if model in _model_ids(\"happyhorse_video_edit_models\"):\n # prompt is REQUIRED per the happyhorse-video-editing spec.\n _require_prompt(request, MODE_VIDEO_EDIT, model)\n for key in (\"resolution\", \"watermark\", \"audio_setting\", \"seed\"):\n if request.get(key) is not None:\n params[key] = request[key]\n # Warn-and-drop wan2.7-only fields (server would 400).\n for unsupported in (\"negative_prompt\", \"ratio\", \"duration\", \"prompt_extend\"):\n if request.get(unsupported) is not None:\n print(\n f\"Warning: {model} does not accept `{unsupported}`; dropped. \"\n \"Use wan2.7-videoedit if you need this feature.\",\n file=sys.stderr,\n )\n else:\n # wan2.7-videoedit: full parameter whitelist.\n if request.get(\"negative_prompt\"):\n input_obj[\"negative_prompt\"] = request[\"negative_prompt\"]\n for key in (\"resolution\", \"ratio\", \"duration\", \"audio_setting\",\n \"prompt_extend\", \"watermark\", \"seed\"):\n if request.get(key) is not None:\n params[key] = request[key]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\ndef build_animate_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for wan2.2-animate-move / wan2.2-animate-mix.\n\n Per the official QwenCloud API specifications:\n - input: ONLY `image_url` (character image) + `video_url` (reference\n video whose actions/expressions are transferred). Both REQUIRED. No prompt.\n - parameters: ONLY `mode` (wan-std/wan-pro, REQUIRED) + `check_image`\n (bool, default true). No resolution/duration/seed/watermark.\n - Endpoint: /services/aigc/image2video/video-synthesis (async).\n \"\"\"\n image_url = request.get(\"image_url\")\n video_url = request.get(\"video_url\")\n if not image_url:\n raise ValueError(\n f\"{model} requires 'image_url' (character image URL or local path).\"\n )\n if not video_url:\n raise ValueError(\n f\"{model} requires 'video_url' (reference video URL or local path, \"\n \"2-30s, single front-facing person).\"\n )\n mode = request.get(\"mode\")\n if mode not in (\"wan-std\", \"wan-pro\"):\n raise ValueError(\n f\"{model} requires parameters.mode to be 'wan-std' or 'wan-pro' \"\n f\"(got {mode!r}). wan-std: faster/cheaper; wan-pro: smoother/higher quality.\"\n )\n\n input_obj: dict[str, Any] = {\n \"image_url\": str(image_url),\n \"video_url\": str(video_url),\n }\n params: dict[str, Any] = {\"mode\": mode}\n if request.get(\"check_image\") is not None:\n params[\"check_image\"] = request[\"check_image\"]\n\n return {\"model\": model, \"input\": input_obj, \"parameters\": params}\n\n\nPAYLOAD_BUILDERS: dict[str, Any] = {\n MODE_T2V: build_t2v_payload,\n MODE_I2V: build_i2v_payload,\n MODE_KF2V: build_kf2v_payload,\n MODE_R2V: build_r2v_payload,\n MODE_VACE: build_vace_payload,\n MODE_VIDEO_EDIT: build_video_edit_payload,\n MODE_ANIMATE: build_animate_payload,\n}\n\n# ---------------------------------------------------------------------------\n# Result extraction and status formatting\n# ---------------------------------------------------------------------------\n\ndef extract_video_url(result: dict[str, Any]) -> str | None:\n \"\"\"Extract video URL from task result, checking all output formats.\n\n Formats seen across endpoints:\n - output.video_url (happyhorse video-edit)\n - output.results[0].url (kf2v / r2v / t2v / i2v: results is a LIST)\n - output.results.video_url (wan2.2-animate-move/mix: results is an OBJECT)\n \"\"\"\n output = result.get(\"output\", {})\n url = output.get(\"video_url\")\n if url:\n return url\n results = output.get(\"results\")\n if isinstance(results, dict):\n # animate spec: results = {\"video_url\": \"...\"}\n url = results.get(\"video_url\") or results.get(\"url\")\n if url:\n return url\n if isinstance(results, list) and results and isinstance(results[0], dict):\n return results[0].get(\"url\")\n return None\n\n\ndef estimate_cost(_model: str, _duration: int, _resolution: str,\n _cny: bool = False) -> str:\n \"\"\"Return a pricing page reference instead of a hardcoded estimate.\"\"\"\n return f\"see {_PRICING_URL} for current rates\"\n\n\ndef resolve_resolution(request: dict[str, Any], mode: str) -> str:\n \"\"\"Derive a human-readable resolution label from the request for cost estimation.\"\"\"\n # Modes that use resolution parameter\n if mode in (MODE_I2V, MODE_KF2V):\n return request.get(\"resolution\", \"720P\")\n # Modes that use size parameter (wan2.6 t2v, r2v)\n if mode in (MODE_T2V, MODE_R2V):\n # If resolution is explicitly set (wan2.7), use it\n if request.get(\"resolution\"):\n return request[\"resolution\"]\n size = request.get(\"size\", \"1280*720\")\n try:\n width, height = size.split(\"*\")\n pixels = int(width) * int(height)\n except (ValueError, AttributeError):\n return \"720P\"\n if pixels >= 1920 * 1080:\n return \"1080P\"\n if pixels >= 1280 * 720:\n return \"720P\"\n return \"480P\"\n return \"720P\"\n\n\ndef format_task_status(result: dict[str, Any], elapsed: int | None = None) -> str:\n \"\"\"Format a human-readable task status line for progress reporting.\"\"\"\n output = result.get(\"output\", {})\n status = output.get(\"task_status\", \"UNKNOWN\")\n task_id = output.get(\"task_id\", \"\")\n parts = []\n if elapsed is not None:\n parts.append(f\"[{elapsed}s]\")\n parts.append(f\"task={task_id}\" if task_id else \"\")\n parts.append(f\"status={status}\")\n metrics = output.get(\"task_metrics\", {})\n if metrics:\n total = metrics.get(\"TOTAL\", 0)\n succeeded = metrics.get(\"SUCCEEDED\", 0)\n failed = metrics.get(\"FAILED\", 0)\n if total:\n parts.append(f\"progress={succeeded}/{total}\")\n if failed:\n parts.append(f\"failed={failed}\")\n msg = output.get(\"message\", \"\")\n if msg and status in (\"FAILED\", \"CANCELED\"):\n parts.append(f\"msg={msg[:120]}\")\n if output.get(\"video_url\"):\n parts.append(\"video_url=ready\")\n return sanitize_diagnostic(\" \".join(part for part in parts if part))\n","content_type":"text/x-python; charset=utf-8","language":"python","size":33177,"content_sha256":"b99c708c688e625e93ac6ce10f095aea63cfe188de728f70e55ac8c2bab768e7"},{"filename":"scripts/video.py","content":"#!/usr/bin/env python3\n\"\"\"Video generation via Wan models on DashScope API.\n\nSupports ALL video generation modes:\n t2v -- text-to-video (prompt only)\n i2v -- image-to-video based on first frame\n kf2v -- image-to-video based on first + last frames\n r2v -- reference-based video (character role-play)\n vace -- video editing (multi-image ref, repainting, edit, extension, outpainting)\n animate -- image-to-animation (character image + reference video, wan2.2-animate-*)\n\nSubmits async task, polls until completion, downloads video.\nSelf-contained, stdlib only.\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\n\nif sys.version_info \u003c (3, 9):\n print(f\"Error: Python 3.9+ required (found {sys.version}). \"\n \"Install: https://www.python.org/downloads/\", file=sys.stderr)\n sys.exit(1)\n\nimport argparse\nimport json\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\n\nfrom qwencloud_lib import ( # noqa: E402\n check_token_plan_model_support,\n download_file,\n http_request,\n is_token_plan_key,\n is_token_plan_model_supported,\n load_request,\n native_base_url,\n poll_task,\n require_api_key,\n run_update_signal,\n sanitize_diagnostic,\n)\nfrom video_lib import ( # noqa: E402\n ENDPOINTS,\n MODE_ANIMATE,\n MODE_I2V,\n MODE_KF2V,\n MODE_VIDEO_EDIT,\n PAYLOAD_BUILDERS,\n RESOLVE_KEYS,\n detect_mode,\n get_default_model,\n is_animate_model,\n is_happyhorse_i2v_model,\n is_video_edit_model,\n is_wan27_i2v_model,\n resolve_request_urls,\n extract_video_url,\n estimate_cost,\n resolve_resolution,\n format_task_status,\n)\n\n# ---------------------------------------------------------------------------\n# Token Plan credits usage consoles — PAYG pricing is irrelevant for TP keys\n_TOKEN_PLAN_PERSONAL_CONSOLE_URL = (\n \"https://home.qwencloud.com/analytics/token-plan/individual\"\n)\n_TOKEN_PLAN_TEAM_CONSOLE_URL = \"https://home.qwencloud.com/analytics/token-plan/team\"\n# Model market — activation guidance for models not yet enabled on the account\n_MODEL_MARKET_URL = \"https://www.qwencloud.com/models/\"\n\n# Error keywords indicating the model is not activated/subscribed (case-insensitive)\n_ACTIVATION_ERROR_HINTS = (\"not activated\", \"not subscribed\", \"activation\", \"activate\")\n\n\ndef _check_not_activated(error_text: str, model: str, api_key: str) -> None:\n \"\"\"Print activation guidance when the API error indicates the model is not activated.\"\"\"\n text = (error_text or \"\").lower()\n if not any(h in text for h in _ACTIVATION_ERROR_HINTS):\n return\n if is_token_plan_key(api_key):\n print(\n f\"Hint: model '{model}' may not be included in your Token Plan subscription.\\n\"\n \"Manage your subscription:\\n\"\n f\" Personal: {_TOKEN_PLAN_PERSONAL_CONSOLE_URL}\\n\"\n f\" Team: {_TOKEN_PLAN_TEAM_CONSOLE_URL}\",\n file=sys.stderr,\n )\n else:\n print(\n f\"Hint: model '{model}' may require activation before use.\\n\"\n f\"Activate it at {_MODEL_MARKET_URL}{model} \"\n \"(see the model's page for activation steps).\",\n file=sys.stderr,\n )\n\n\ndef _handle_result(result: dict[str, Any], args: argparse.Namespace,\n model: str = \"\", api_key: str = \"\") -> None:\n output = result.get(\"output\", {})\n status = output.get(\"task_status\", \"\")\n if status != \"SUCCEEDED\":\n msg = output.get(\"message\", \"Unknown error\")\n code = output.get(\"code\", \"\")\n _check_not_activated(f\"{code}: {msg}\" if code else msg, model, api_key)\n print(sanitize_diagnostic(f\"Error: Task failed ({status}): {msg}\"), file=sys.stderr)\n sys.exit(1)\n\n video_url = extract_video_url(result)\n if not video_url:\n print(sanitize_diagnostic(f\"Error: No video URL in result: {result}\"), file=sys.stderr)\n sys.exit(1)\n\n args.output.mkdir(parents=True, exist_ok=True)\n resp_file = args.output / \"response.json\"\n resp_file.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n print(f\"Response saved to {resp_file}\", file=sys.stderr)\n\n # Use URL basename for unique filename (contains task ID from API)\n url_basename = Path(urlparse(video_url).path).name\n if url_basename and \".\" in url_basename:\n video_file = args.output / url_basename\n else:\n video_file = args.output / \"video.mp4\"\n try:\n download_file(video_url, video_file)\n except Exception as e:\n print(sanitize_diagnostic(f\"Warning: Could not download video: {e}\"), file=sys.stderr)\n else:\n print(f\"Video saved to {video_file}\", file=sys.stderr)\n\n if args.print_response:\n print(json.dumps({\n \"video_url\": video_url,\n \"local_path\": str(video_file),\n }, ensure_ascii=False))\n\n print(sanitize_diagnostic(f\"Video URL: {video_url}\"), file=sys.stderr)\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main() -> None:\n run_update_signal(caller=__file__)\n parser = argparse.ArgumentParser(\n description=\"Generate video via Wan models (t2v/i2v/kf2v/r2v/vace)\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\\\nmode auto-detection (from request JSON fields; defaults load from CDN config):\n t2v prompt only → text-to-video\n i2v img_url/media/first_frame_url → image-to-video\n kf2v first_frame_url (without media) → keyframe-to-video\n r2v reference_urls → reference role-play\n vace function → video editing/repaint/extend\n videoedit model-id (wan2.7-videoedit / happyhorse-1.0-video-edit) →\n media-based video editing\n animate model-id (wan2.2-animate-move / wan2.2-animate-mix) →\n image-to-animation\n\nmodel version differences (handled automatically):\n wan2.6-t2v: uses \"size\" param (e.g. \"1280*720\"), supports seed, shot_type\n wan2.7-t2v / happyhorse-1.0-t2v / happyhorse-1.1-t2v: use \"resolution\" + \"ratio\" params\n wan2.6-i2v: single img_url input\n wan2.7-i2v: media array (first_frame, last_frame, driving_audio, first_clip)\n wan3.0-video / wan3.0-video-prime: media array i2v + optional \"ratio\" param\n (adaptive/16:9/9:16/1:1) on top of resolution/duration\n happyhorse-1.0-i2v / happyhorse-1.1-i2v: strict spec — media=[{type:'first_frame', url}]\n (exactly one); only resolution/duration/watermark/seed params. Rejects\n negative_prompt / prompt_extend / ratio / last_frame / first_clip / driving_audio.\n Legacy img_url is auto-converted for convenience.\n wan2.7-videoedit / happyhorse-1.0-video-edit: media=[{type:'video', url}] +\n [{type:'reference_image', url}] (no `function` field). wan2.7-videoedit also\n supports negative_prompt + ratio/duration/prompt_extend; happyhorse only\n resolution/watermark/audio_setting/seed (prompt required).\n wan2.2-animate-move / wan2.2-animate-mix: image_url (character) + video_url\n (reference video, 2-30s) + mode (\"wan-std\" faster / \"wan-pro\" smoother,\n required) + optional check_image. NO prompt, NO resolution/duration/seed.\n\nrequest JSON fields (--request / --file):\n prompt Text description (required for t2v/i2v/r2v, optional for vace)\n duration Video length in seconds (default: 5, wan2.7: 2-15)\n size Resolution for wan2.6 t2v/r2v, e.g. \"1280*720\"\n resolution Resolution for wan2.7/i2v/kf2v, e.g. \"720P\", \"1080P\"\n ratio Aspect ratio for wan2.7-t2v, e.g. \"16:9\", \"9:16\"\n img_url First-frame image (i2v wan2.6 / happyhorse-i2v compat)\n first_frame_url First frame (wan2.7-i2v/kf2v)\n last_frame_url Last frame (wan2.7-i2v/kf2v)\n first_clip_url Video to continue (wan2.7-i2v)\n driving_audio_url Audio for lip-sync (wan2.7-i2v)\n media Array of {type, url} for wan2.7-i2v / happyhorse-i2v / videoedit\n reference_urls Character reference URLs (r2v)\n reference_images Reference image URLs (videoedit)\n function VACE: repainting, editing, extension, outpainting\n image_url Character image URL/path (animate)\n video_url Source video (VACE / videoedit / animate)\n mode Service mode: \"wan-std\" / \"wan-pro\" (animate, required)\n audio_url Audio track (t2v/i2v)\n negative_prompt What to avoid\n seed Reproducibility (wan2.6 only)\n prompt_extend Auto-enhance prompt (default: true)\n watermark Add watermark (default: false)\n\nlocal files:\n Local paths are auto-uploaded to DashScope temp storage (48h TTL).\n\nenvironment variables:\n QWENCLOUD_API_KEY (preferred) API key — also loaded from .env\n QWEN_API_KEY (fallback) Legacy alias\n DASHSCOPE_API_KEY (fallback) Legacy provider variable\n QWEN_REGION ap-southeast-1 (default)\n\nexamples:\n # Text-to-video (wan2.6)\n python scripts/video.py --request '{\"prompt\":\"A cat playing piano\",\"duration\":5}'\n\n # Text-to-video with wan2.7 (ratio, auto-dubbing)\n python scripts/video.py --request '{\"prompt\":\"Multi-shot cinematic...\",\n \"resolution\":\"1080P\",\"ratio\":\"16:9\",\"duration\":15}' --model wan2.7-t2v\n\n # Image-to-video (wan2.6)\n python scripts/video.py --request '{\"prompt\":\"Animate this\",\"img_url\":\"photo.jpg\"}'\n\n # Image-to-video with wan2.7 (first frame + audio sync)\n python scripts/video.py --request '{\"prompt\":\"A rapper sings\",\"first_frame_url\":\"rapper.png\",\n \"driving_audio_url\":\"rap.mp3\"}' --model wan2.7-i2v\n\n # Video continuation with wan2.7\n python scripts/video.py --request '{\"prompt\":\"Dog continues skateboarding\",\n \"first_clip_url\":\"dog_skateboard.mp4\"}' --model wan2.7-i2v\n\n # Submit only, then resume later\n python scripts/video.py --request '{\"prompt\":\"A sunset\"}' --submit-only\n python scripts/video.py --task-id \u003cTASK_ID>\n\n # VACE video editing\n python scripts/video.py --request '{\"function\":\"repainting\",\n \"video_url\":\"input.mp4\",\"prompt\":\"Change sky to sunset\"}'\n\n # Video editing with wan2.7-videoedit (media[] protocol, no `function`)\n python scripts/video.py --request '{\"prompt\":\"Replace the man with a robot\",\n \"video_url\":\"input.mp4\",\"reference_images\":[\"robot.png\"],\n \"resolution\":\"1080P\",\"ratio\":\"16:9\"}' --model wan2.7-videoedit\n\n # Image-to-video with happyhorse-i2v (img_url auto-promoted to media first_frame)\n python scripts/video.py --request '{\"prompt\":\"gentle motion\",\n \"img_url\":\"photo.png\"}' --model happyhorse-1.1-i2v\n\n # Image-to-animation: transfer actions from a reference video to a character\n python scripts/video.py --request '{\"image_url\":\"character.jpg\",\n \"video_url\":\"dance.mp4\",\"mode\":\"wan-std\"}' --model wan2.2-animate-move\n\"\"\",\n )\n parser.add_argument(\"--request\", type=str, help=\"Inline JSON: fields depend on mode\")\n parser.add_argument(\"--file\", type=Path, help=\"Path to JSON file containing request body\")\n parser.add_argument(\"--output\", type=Path, default=Path(\"output/qwencloud-video-generation\"),\n help=\"Directory to save response and video (default: %(default)s)\")\n parser.add_argument(\"--print-response\", action=\"store_true\", help=\"Print video URL to stdout\")\n parser.add_argument(\"--model\", type=str,\n help=\"Model ID (overrides auto-default; see epilog for defaults per mode)\")\n parser.add_argument(\"--mode\", type=str,\n choices=[\"t2v\", \"i2v\", \"kf2v\", \"r2v\", \"vace\", \"videoedit\", \"animate\"],\n help=\"Force mode (auto-detected from request fields if omitted). \"\n \"'videoedit' is for wan2.7-videoedit / happyhorse-1.0-video-edit; \"\n \"'animate' is for wan2.2-animate-move / wan2.2-animate-mix.\")\n parser.add_argument(\"--poll-interval\", type=int, default=15,\n help=\"Seconds between poll attempts (default: 15)\")\n parser.add_argument(\"--timeout\", type=int, default=600,\n help=\"Max seconds to wait for task (default: 600)\")\n parser.add_argument(\"--submit-only\", action=\"store_true\",\n help=\"Submit task, print task_id to stdout, exit without polling\")\n parser.add_argument(\"--task-id\", type=str,\n help=\"Resume polling an existing task (skip submission)\")\n parser.add_argument(\"--poll-once\", action=\"store_true\",\n help=\"With --task-id: single status check, exit code 2 if not done\")\n parser.add_argument(\"--quiet\", \"-q\", action=\"store_true\",\n help=\"Suppress progress output during polling\")\n args = parser.parse_args()\n\n verbose = not args.quiet\n api_key = require_api_key(script_file=__file__, domain=\"Video\")\n\n # --- Single check mode ---\n if args.task_id and args.poll_once:\n result = http_request(\"GET\", f\"{native_base_url()}/tasks/{args.task_id}\", api_key)\n if verbose:\n print(f\" {format_task_status(result)}\", file=sys.stderr)\n status = result.get(\"output\", {}).get(\"task_status\", \"\")\n if status in (\"SUCCEEDED\", \"FAILED\", \"CANCELED\"):\n _handle_result(result, args, model=args.model or \"\", api_key=api_key)\n else:\n print(json.dumps(result, ensure_ascii=False, indent=2))\n sys.exit(2)\n return\n\n # --- Resume mode ---\n if args.task_id:\n task_id = args.task_id\n if verbose:\n print(sanitize_diagnostic(f\"Resuming task: {task_id}\"), file=sys.stderr)\n print(f\"Polling every {args.poll_interval}s (timeout: {args.timeout}s)...\",\n file=sys.stderr)\n try:\n result = poll_task(task_id, api_key,\n timeout_s=args.timeout, interval=args.poll_interval,\n verbose=verbose)\n except TimeoutError as e:\n print(sanitize_diagnostic(f\"Error: {e}\"), file=sys.stderr)\n print(sanitize_diagnostic(f\"Task may still be running. Resume with: --task-id {task_id}\"), file=sys.stderr)\n sys.exit(1)\n _handle_result(result, args, model=args.model or \"\", api_key=api_key)\n return\n\n # --- Normal mode: submit new task ---\n try:\n request = load_request(args)\n except ValueError as e:\n print(sanitize_diagnostic(f\"Error: {e}\"), file=sys.stderr)\n sys.exit(1)\n\n try:\n mode = args.mode or detect_mode(request, model=args.model or request.get(\"model\", \"\"))\n model = args.model or request.get(\"model\") or get_default_model(mode)\n check_token_plan_model_support(model, domain=\"Video\")\n # Model-aware correction: wan2.7-i2v / happyhorse-i2v use first_frame_url\n # but belong to i2v mode (detect_mode would have classified them as kf2v).\n if is_wan27_i2v_model(model) or is_happyhorse_i2v_model(model):\n if mode == MODE_KF2V:\n mode = MODE_I2V\n # Correct model-routed modes when the request shape alone is ambiguous.\n if is_video_edit_model(model) and mode != MODE_VIDEO_EDIT:\n mode = MODE_VIDEO_EDIT\n if is_animate_model(model) and mode != MODE_ANIMATE:\n mode = MODE_ANIMATE\n duration = request.get(\"duration\", 5)\n resolution = resolve_resolution(request, mode)\n # Token Plan keys consume credits, not PAYG pricing.\n token_plan = is_token_plan_key(api_key)\n token_plan_model = is_token_plan_model_supported(model, \"Video\")\n cost_str = None if token_plan else estimate_cost(model, duration, resolution)\n\n if verbose:\n info = f\"Mode: {mode} | Model: {model}\"\n if token_plan and token_plan_model:\n info += (\n f\" | Credits: Personal {_TOKEN_PLAN_PERSONAL_CONSOLE_URL}\"\n f\" | Team {_TOKEN_PLAN_TEAM_CONSOLE_URL}\"\n )\n elif token_plan:\n info += \" | Billing: PAYG-only model (not available on Token Plan)\"\n elif cost_str:\n info += f\" | Cost: {cost_str}\"\n print(info, file=sys.stderr)\n\n resolve_request_urls(request, api_key, model, RESOLVE_KEYS[mode])\n builder = PAYLOAD_BUILDERS[mode]\n payload = builder(request, model)\n except (ValueError, KeyError, RuntimeError) as e:\n print(sanitize_diagnostic(f\"Error: {e}\"), file=sys.stderr)\n sys.exit(1)\n\n url = f\"{native_base_url()}{ENDPOINTS[mode]}\"\n\n try:\n resp = http_request(\"POST\", url, api_key, payload,\n extra_headers={\"X-DashScope-Async\": \"enable\"}, timeout=60)\n except Exception as e:\n _check_not_activated(str(e), model, api_key)\n print(sanitize_diagnostic(f\"API error: {e}\"), file=sys.stderr)\n sys.exit(1)\n\n task_id = resp.get(\"output\", {}).get(\"task_id\")\n if not task_id:\n print(sanitize_diagnostic(f\"Error: No task_id in response: {json.dumps(resp, ensure_ascii=False)}\")[:500],\n file=sys.stderr)\n sys.exit(1)\n\n if verbose:\n print(sanitize_diagnostic(f\"Task submitted: {task_id}\"), file=sys.stderr)\n\n if args.submit_only:\n print(task_id)\n return\n\n if verbose:\n print(f\"Polling every {args.poll_interval}s (timeout: {args.timeout}s)...\",\n file=sys.stderr)\n\n try:\n result = poll_task(task_id, api_key,\n timeout_s=args.timeout, interval=args.poll_interval,\n verbose=verbose)\n except TimeoutError as e:\n print(sanitize_diagnostic(f\"Error: {e}\"), file=sys.stderr)\n print(sanitize_diagnostic(f\"Task may still be running. Resume with: --task-id {task_id}\"), file=sys.stderr)\n sys.exit(1)\n\n _handle_result(result, args, model=model, api_key=api_key)\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":17868,"content_sha256":"c96ab8385d25fc3fc38d0b40630c2a2e5acacd67fc787034febda23e92b91c64"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Qwen Video Generation","type":"text"}]},{"type":"paragraph","content":[{"text":"Generate videos using Wan models. All tasks are ","type":"text"},{"text":"asynchronous","type":"text","marks":[{"type":"strong"}]},{"text":" — submit, then poll until completion. This skill is part of ","type":"text"},{"text":"qwencloud/qwencloud-ai","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Critical Parameter Differences:","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kf2v (First+Last Frame)","type":"text","marks":[{"type":"strong"}]},{"text":": Duration is ","type":"text"},{"text":"fixed at 5 seconds","type":"text","marks":[{"type":"strong"}]},{"text":" — other values will fail. Output is ","type":"text"},{"text":"silent only","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Resolution parameters vary by model family, not mode alone","type":"text","marks":[{"type":"strong"}]},{"text":": Fetch the current model catalog before choosing ","type":"text"},{"text":"size","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resolution","type":"text","marks":[{"type":"code_inline"}]},{"text":", or ","type":"text"},{"text":"ratio","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Skill directory","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Location","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/video.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Default execution — mode auto-detect, submit, poll, download","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/execution-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fallback: curl for all 5 modes, code generation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/request-fields.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field tables and audio handling by mode","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/workflows.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Duration extensions, multi-shot, VACE pipelines","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/polling-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Polling patterns and timing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/merge-media.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Concat, trim, audio overlay — ffmpeg/moviepy recipes","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/prompt-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Per-mode prompt formulas, sound description, multi-shot structure","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/examples.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Full script examples per mode","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/sources.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Official documentation URLs","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security","type":"text"}]},{"type":"paragraph","content":[{"text":"NEVER output any API key or credential in plaintext.","type":"text","marks":[{"type":"strong"}]},{"text":" Always use variable references (","type":"text"},{"text":"$QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" in shell, ","type":"text"},{"text":"os.environ[\"QWENCLOUD_API_KEY\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" in Python). The scripts accept ","type":"text"},{"text":"QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"QWEN_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":". Any check or detection of credentials must be ","type":"text"},{"text":"non-plaintext","type":"text","marks":[{"type":"strong"}]},{"text":": report only status (e.g. \"set\" / \"not set\", \"valid\" / \"invalid\"), never the value. Never display contents of ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" or config files that may contain secrets.","type":"text"}]},{"type":"paragraph","content":[{"text":"When the API key is not configured, NEVER ask the user to provide it directly.","type":"text","marks":[{"type":"strong"}]},{"text":" Instead, help create a ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file with a placeholder (","type":"text"},{"text":"QWENCLOUD_API_KEY=sk-your-key-here","type":"text","marks":[{"type":"code_inline"}]},{"text":") and instruct the user to replace it with their actual key from the ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":". Only write the actual key value if the user explicitly requests it.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Compatibility","type":"text"}]},{"type":"paragraph","content":[{"text":"Scripts support both ","type":"text"},{"text":"standard QwenCloud API keys","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"sk-...","type":"text","marks":[{"type":"code_inline"}]},{"text":") and ","type":"text"},{"text":"Token Plan keys","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"sk-sp-...","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Token Plan keys are automatically routed to the Token Plan endpoint for supported video models — see ","type":"text"},{"text":"Token Plan Support","type":"text","marks":[{"type":"link","attrs":{"href":"#token-plan-support","title":null}}]},{"text":" below.","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan: do not use curl; always use the bundled Python script.","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"Coding Plan keys (also ","type":"text"},{"text":"sk-sp-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix but purchased via Coding Plan subscription) cannot be used — video generation models are not available on Coding Plan. Video generation incurs per-second charges on standard keys. The script detects key type at startup and routes accordingly. If qwencloud-ops-auth is installed, see its ","type":"text"},{"text":"references/codingplan.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for full details.","type":"text"}]},{"type":"paragraph","content":[{"text":"Detect the API key type without exposing the key:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 -c \"\nimport sys; sys.path.insert(0, 'scripts')\nfrom qwencloud_lib import detect_api_key_type\nprint(detect_api_key_type('scripts/qwencloud_lib.py'))\n\"","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"token-plan","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token Plan key detected (","type":"text"},{"text":"sk-sp-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"payg","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Standard PAYG key detected","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"not-set","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No API key found in environment","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"For Token Plan, fetch and read the current ","type":"text"},{"text":"Token Plan model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-token-plan-models.md","title":null}}]},{"text":", then use an exact listed model. If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-token-plan-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Mode Selection Guide","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"User Want","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Mode","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Key Field","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generate video from text description only","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"t2v","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"prompt","type":"text","marks":[{"type":"code_inline"}]},{"text":" only","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Animate a single image","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"i2v","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"img_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"reference_image","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"wan2.7 unified i2v: first frame, first+last frame, video continuation, audio sync","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"i2v","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"media","type":"text","marks":[{"type":"code_inline"}]},{"text":" (array), ","type":"text"},{"text":"first_frame_url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"first_clip_url","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"driving_audio_url","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transition between two images (","type":"text"},{"text":"⚠️ 5s fixed, silent only","type":"text","marks":[{"type":"strong"}]},{"text":")","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"kf2v","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"first_frame_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"last_frame_url","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Role-play: make characters act a new script","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"r2v","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"reference_urls","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"media","type":"text","marks":[{"type":"code_inline"}]},{"text":"; read the CDN model catalog for model-specific limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Video editing: multi-image ref, repainting, local edit, extend, outpaint","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"vace","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"function","type":"text","marks":[{"type":"code_inline"}]},{"text":"; read the CDN model catalog for the current default","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Video editing via media protocol","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"videoedit","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--model","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"media","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":"; read the CDN model catalog for supported models","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Transfer a person's actions/expressions from a reference video to a character image","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"animate","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--model","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"image_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"mode","type":"text","marks":[{"type":"code_inline"}]},{"text":"; read the CDN model catalog for supported models","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Model Selection","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"🚫 CRITICAL — Never override user-specified parameters.","type":"text","marks":[{"type":"strong"}]},{"text":" If the user explicitly specifies a model (in prompt or request JSON), you MUST use exactly that model. Do NOT:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Replace it with a \"better suited\" or newer model (e.g., swapping the user's ","type":"text"},{"text":"wan2.6-t2v","type":"text","marks":[{"type":"code_inline"}]},{"text":" for ","type":"text"},{"text":"wan3.0-video","type":"text","marks":[{"type":"code_inline"}]},{"text":" because the task \"looks like an all-in-one job\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Replace or restructure their input fields (e.g. converting a user-specified ","type":"text"},{"text":"img_url","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"media","type":"text","marks":[{"type":"code_inline"}]},{"text":" arrangement) unless required by the chosen model's documented format — and if a conversion is required, keep the user's media order and content intact","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add parameters the user did not ask for (","type":"text"},{"text":"prompt_extend","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"shot_type","type":"text","marks":[{"type":"code_inline"}]},{"text":", style hints) — note ","type":"text"},{"text":"shot_type: \"multi\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" REQUIRES ","type":"text"},{"text":"prompt_extend: true","type":"text","marks":[{"type":"code_inline"}]},{"text":", but only when the user actually requested multi-shot","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"\"Optimize\" any explicit user choice (","type":"text"},{"text":"duration","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"size","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resolution","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"prompt","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Selection guidance below applies ","type":"text"},{"text":"only when the user has NOT specified a model","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User specified a model","type":"text","marks":[{"type":"strong"}]},{"text":" → ","type":"text"},{"text":"MANDATORY: use that exact model","type":"text","marks":[{"type":"strong"}]},{"text":" — do not substitute, do not \"optimize\", do not add unrequested parameters.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consult the qwencloud-model-selector skill","type":"text","marks":[{"type":"strong"}]},{"text":" when model choice depends on capability, scenario, or pricing.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Models","type":"text"}]},{"type":"paragraph","content":[{"text":"Before selecting, recommending, or defaulting a model, fetch and read the current ","type":"text"},{"text":"QwenCloud video-generation model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-video-generation-models.md","title":null}}]},{"text":". It contains the model list, basic model information, mode recommendations, compatibility notes, and defaults. If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-video-generation-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Important","type":"text","marks":[{"type":"strong"}]},{"text":": The model catalog is a ","type":"text"},{"text":"point-in-time snapshot","type":"text","marks":[{"type":"strong"}]},{"text":" and may be outdated. Model availability changes frequently. ","type":"text"},{"text":"Always check the ","type":"text","marks":[{"type":"strong"}]},{"text":"official model list","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.qwencloud.com/models","title":null}},{"type":"strong"}]},{"text":" for the authoritative, up-to-date catalog before making model decisions.","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Model details","type":"text","marks":[{"type":"strong"}]},{"text":": For more information about a specific model, direct the user to ","type":"text"},{"text":"https://www.qwencloud.com/models/\u003cmodel-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Replace ","type":"text"},{"text":"\u003cmodel-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the exact model ID; never modify or guess it.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Dynamic model queries","type":"text","marks":[{"type":"strong"}]},{"text":": If the ","type":"text"},{"text":"qwencloud-model-selector","type":"text","marks":[{"type":"strong"}]},{"text":" skill or ","type":"text"},{"text":"QwenCloud CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"qwencloud models info \u003cmodel>","type":"text","marks":[{"type":"code_inline"}]},{"text":") is available, use it for real-time model data. CLI requires authentication — see the ","type":"text"},{"text":"qwencloud-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill for login flow.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Execution","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Multiple artifacts","type":"text","marks":[{"type":"strong"}]},{"text":": When generating multiple files in a single session, you MUST append a numeric suffix to each filename (e.g. ","type":"text"},{"text":"out_1.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"out_2.mp4","type":"text","marks":[{"type":"code_inline"}]},{"text":") to prevent overwrites.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API Key","type":"text","marks":[{"type":"strong"}]},{"text":": Check ","type":"text"},{"text":"QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"QWEN_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" using a ","type":"text"},{"text":"non-plaintext","type":"text","marks":[{"type":"strong"}]},{"text":" check only (e.g. in shell: ","type":"text"},{"text":"[ -n \"$QWENCLOUD_API_KEY\" ]","type":"text","marks":[{"type":"code_inline"}]},{"text":"; report only \"set\" or \"not set\", never the key value). If not set: run the * ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"em"}]},{"text":"* skill if available; otherwise guide the user to obtain a key from ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":" and set it via ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file ( ","type":"text"},{"text":"echo 'QWENCLOUD_API_KEY=sk-your-key-here' >> .env","type":"text","marks":[{"type":"code_inline"}]},{"text":" in project root or current directory) or environment variable. The script searches for ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the current working directory and the project root. Skills may be installed independently — do not assume qwencloud-ops-auth is present. ","type":"text"},{"text":"Note","type":"text","marks":[{"type":"strong"}]},{"text":": The script auto-loads ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the current directory and the project root (in addition to any exported environment variable). A shell check showing ","type":"text"},{"text":"$QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" as \"not set\" does NOT mean the script will fail — it may still find the key in ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":". Treat the shell check as informational only; the authoritative test is simply running the script (it exits with a clear error if no key is found anywhere).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python 3.9+ (stdlib only, ","type":"text"},{"text":"no pip install needed","type":"text","marks":[{"type":"strong"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For media merging (concat, trim, audio overlay): see ","type":"text"},{"text":"merge-media.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/merge-media.md","title":null}}]},{"text":" for ffmpeg/moviepy recipes suited to the user's environment","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Environment Check","type":"text"}]},{"type":"paragraph","content":[{"text":"Before first execution, verify Python is available:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 --version # must be 3.9+","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"python3","type":"text","marks":[{"type":"code_inline"}]},{"text":" is unavailable or below 3.9, PAYG may use ","type":"text"},{"text":"Path 2 (curl)","type":"text","marks":[{"type":"strong"}]},{"text":"; Token Plan must install Python 3.9+ instead.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Default: Run Script","type":"text"}]},{"type":"paragraph","content":[{"text":"Script path","type":"text","marks":[{"type":"strong"}]},{"text":": Scripts are in the ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" subdirectory ","type":"text"},{"text":"of this skill's directory","type":"text","marks":[{"type":"strong"}]},{"text":" (the directory containing this SKILL.md). ","type":"text"},{"text":"You MUST first locate this skill's installation directory, then ALWAYS use the full absolute path to execute scripts.","type":"text","marks":[{"type":"strong"}]},{"text":" Do NOT assume scripts are in the current working directory. Do NOT use ","type":"text"},{"text":"cd","type":"text","marks":[{"type":"code_inline"}]},{"text":" to switch directories before execution.","type":"text"}]},{"type":"paragraph","content":[{"text":"Execution note:","type":"text","marks":[{"type":"strong"}]},{"text":" Run all scripts in the ","type":"text"},{"text":"foreground","type":"text","marks":[{"type":"strong"}]},{"text":" — wait for stdout; do not background.","type":"text"}]},{"type":"paragraph","content":[{"text":"Discovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Run ","type":"text"},{"text":"python3 \u003cthis-skill-dir>/scripts/video.py --help","type":"text","marks":[{"type":"code_inline"}]},{"text":" first to see all available arguments.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 \u003cthis-skill-dir>/scripts/video.py \\\n --model wan2.6-t2v \\\n --request '{\"prompt\":\"A detective in a rainy city at night\",\"size\":\"1280*720\",\"duration\":5}' \\\n --print-response\n\n# Image-to-animation (no prompt): transfer dance moves from reference video to a character\npython3 \u003cthis-skill-dir>/scripts/video.py \\\n --model wan2.2-animate-move \\\n --request '{\"image_url\":\"https://example.com/character.jpg\",\"video_url\":\"https://example.com/dance.mp4\",\"mode\":\"wan-std\"}' \\\n --print-response","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Argument","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--request '{...}'","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JSON request body","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--file path.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Load request from file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--mode MODE","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Override auto-detected mode (t2v/i2v/kf2v/r2v/vace/videoedit/animate)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--model ID","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Override model","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output dir/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Save video and response JSON to directory; video is auto-named from the download URL basename, preventing overwrites across runs; use distinct filenames across calls to avoid overwriting response data","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--print-response","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Print response JSON to stdout","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--submit-only","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Submit and exit (print task_id)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--task-id ID","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Operate on existing task","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--poll-interval N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Seconds between polls (default: 15)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--timeout N","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Max wait seconds (default: 600)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Verify Result","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exit code ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":" + response has ","type":"text"},{"text":"output.task_id","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"submission success","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Poll reaches ","type":"text"},{"text":"task_status: SUCCEEDED","type":"text","marks":[{"type":"code_inline"}]},{"text":" → ","type":"text"},{"text":"generation complete","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Non-zero exit, HTTP error, or ","type":"text"},{"text":"FAILED","type":"text","marks":[{"type":"code_inline"}]},{"text":" status → ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Post-execution check","type":"text","marks":[{"type":"strong"}]},{"text":": Verify the output video file exists and has non-zero size (","type":"text"},{"text":"ls -la \u003coutput_dir>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MANDATORY — stderr signal check","type":"text","marks":[{"type":"strong"}]},{"text":": After confirming the result, scan the command's stderr output for ","type":"text"},{"text":"[ACTION_REQUIRED]","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]},{"text":". If either signal is present, you ","type":"text"},{"text":"MUST","type":"text","marks":[{"type":"strong"}]},{"text":" follow the instructions in ","type":"text"},{"text":"Update Check","type":"text","marks":[{"type":"link","attrs":{"href":"#update-check-mandatory-post-execution","title":null}}]},{"text":" below before responding to the user.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"On Failure","type":"text"}]},{"type":"paragraph","content":[{"text":"If the script fails, match the error output against the diagnostic table below to determine the resolution. If no match, read ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":" for alternative paths: curl commands (Path 2 — all 5 modes), code generation (Path 3), and autonomous resolution (Path 5).","type":"text"}]},{"type":"paragraph","content":[{"text":"If Python is not available at all","type":"text","marks":[{"type":"strong"}]},{"text":" → PAYG may use Path 2 (curl); Token Plan must install Python 3.9+.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Diagnosis","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"command not found: python3","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python not on PATH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Try ","type":"text"},{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"py -3","type":"text","marks":[{"type":"code_inline"}]},{"text":"; install Python 3.9+ if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python 3.9+ required","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script version check failed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upgrade Python to 3.9+","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SyntaxError","type":"text","marks":[{"type":"code_inline"}]},{"text":" near type hints","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python \u003c 3.9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upgrade Python to 3.9+","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QWENCLOUD_API_KEY/QWEN_API_KEY/DASHSCOPE_API_KEY not found","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing API key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Obtain key from ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":"; add to ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"echo 'QWENCLOUD_API_KEY=sk-...' >> .env","type":"text","marks":[{"type":"code_inline"}]},{"text":"; or run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" if available","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 401","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid or mismatched key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" (non-plaintext check only); verify key is valid","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SSL: CERTIFICATE_VERIFY_FAILED","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SSL cert issue (proxy/corporate)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"macOS: run ","type":"text"},{"text":"Install Certificates.command","type":"text","marks":[{"type":"code_inline"}]},{"text":"; else set ","type":"text"},{"text":"SSL_CERT_FILE","type":"text","marks":[{"type":"code_inline"}]},{"text":" env var","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URLError","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"ConnectionError","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Network unreachable","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check internet; set ","type":"text"},{"text":"HTTPS_PROXY","type":"text","marks":[{"type":"code_inline"}]},{"text":" if behind proxy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 429","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limited","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait and retry with backoff","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 5xx","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retry with backoff","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ImportError: moviepy","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"moviepy not installed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pip install moviepy","type":"text","marks":[{"type":"code_inline"}]},{"text":", or use system ffmpeg instead (see ","type":"text"},{"text":"merge-media.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/merge-media.md","title":null}}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PermissionError","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Can't write output","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"--output","type":"text","marks":[{"type":"code_inline"}]},{"text":" to specify writable directory","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Request Fields Summary","type":"text"}]},{"type":"paragraph","content":[{"text":"All modes require ","type":"text"},{"text":"prompt","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"except","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"animate","type":"text","marks":[{"type":"code_inline"}]},{"text":" (no prompt) and ","type":"text"},{"text":"videoedit","type":"text","marks":[{"type":"code_inline"}]},{"text":" for wan2.7 (prompt optional). See ","type":"text"},{"text":"request-fields.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/request-fields.md","title":null}}]},{"text":" for full field tables per mode.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"⚠️ Resolution Parameters by Model Family (Critical)","type":"text"}]},{"type":"paragraph","content":[{"text":"The resolution field varies by model family. Check the model catalog above before choosing between ","type":"text"},{"text":"size","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"resolution","type":"text","marks":[{"type":"code_inline"}]},{"text":", and ","type":"text"},{"text":"ratio","type":"text","marks":[{"type":"code_inline"}]},{"text":"; using the wrong field can cause the API call to fail.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Mode-Specific Required Fields","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Required fields vary by model family. Use ","type":"text"},{"text":"request-fields.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/request-fields.md","title":null}}]},{"text":" for payload shapes and the model catalog above for current model-specific compatibility.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cost Estimation","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"🚨 ","type":"text"},{"text":"NEVER guess or fabricate any price figure.","type":"text","marks":[{"type":"strong"}]},{"text":" Always direct the user to the ","type":"text"},{"text":"official pricing page","type":"text","marks":[{"type":"link","attrs":{"href":"https://docs.qwencloud.com/developer-guides/getting-started/pricing","title":null}}]},{"text":" for exact rates.","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Cost is billed per second of generated video. Price varies by model and resolution. Fetch the ","type":"text"},{"text":"CDN model-pricing reference","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-model-pricing.md","title":null}}]},{"text":", and use the official pricing page for exact current rates. Some models may offer a limited free quota — ","type":"text"},{"text":"do not assume any call is free","type":"text","marks":[{"type":"strong"}]},{"text":"; use the ","type":"text"},{"text":"qwencloud-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill to check remaining free tier quota, or verify in the user's ","type":"text"},{"text":"QwenCloud console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/benefits","title":null}}]},{"text":". If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-model-pricing.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"To check actual usage and bills: use the ","type":"text"},{"text":"qwencloud-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill, or visit the console: ","type":"text"},{"text":"Usage Analytics","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/analytics","title":null}}]},{"text":" | ","type":"text"},{"text":"Pay-as-you-go Billing","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/billing/pay-as-you-go","title":null}}]},{"text":" | ","type":"text"},{"text":"Coding Plan Billing","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/billing/coding-plan","title":null}}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"NEVER fabricate, guess, or construct usage/billing/console URLs.","type":"text","marks":[{"type":"strong"}]},{"text":" Only provide the exact links listed in this skill. If a URL is not listed here, do not invent one.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Local File Handling","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user provides local file paths (images, videos, audio), pass them directly to the script. The script ","type":"text"},{"text":"automatically uploads","type":"text","marks":[{"type":"strong"}]},{"text":" local files to DashScope temporary storage (","type":"text"},{"text":"oss://","type":"text","marks":[{"type":"code_inline"}]},{"text":" URL, 48h TTL) and injects the ","type":"text"},{"text":"X-DashScope-OssResourceResolve: enable","type":"text","marks":[{"type":"code_inline"}]},{"text":" header. No manual upload step is needed.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Production","type":"text","marks":[{"type":"strong"}]},{"text":": Default temp storage has ","type":"text"},{"text":"48h TTL","type":"text","marks":[{"type":"strong"}]},{"text":" and ","type":"text"},{"text":"100 QPS upload limit","type":"text","marks":[{"type":"strong"}]},{"text":" — not suitable for production, high-concurrency, or load-testing. To use your own OSS bucket, set ","type":"text"},{"text":"QWEN_TMP_OSS_BUCKET","type":"text","marks":[{"type":"code_inline"}]},{"text":" and ","type":"text"},{"text":"QWEN_TMP_OSS_REGION","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":", install ","type":"text"},{"text":"pip install alibabacloud-oss-v2","type":"text","marks":[{"type":"code_inline"}]},{"text":", and provide credentials via ","type":"text"},{"text":"QWEN_TMP_OSS_AK_ID","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"QWEN_TMP_OSS_AK_SECRET","type":"text","marks":[{"type":"code_inline"}]},{"text":" or the standard ","type":"text"},{"text":"OSS_ACCESS_KEY_ID","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"OSS_ACCESS_KEY_SECRET","type":"text","marks":[{"type":"code_inline"}]},{"text":". Use a RAM user with least-privilege (","type":"text"},{"text":"oss:PutObject","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"oss:GetObject","type":"text","marks":[{"type":"code_inline"}]},{"text":" on target bucket only). If qwencloud-ops-auth is installed, see its ","type":"text"},{"text":"references/custom-oss.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for the full setup guide.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Skill Chaining","type":"text"}]},{"type":"paragraph","content":[{"text":"When using output from another skill as input (e.g., image-gen → i2v, audio-tts → audio overlay):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass the URL directly","type":"text","marks":[{"type":"strong"}]},{"text":" (e.g., ","type":"text"},{"text":"\"img_url\": \"\u003cimage_url from image-gen>\"","type":"text","marks":[{"type":"code_inline"}]},{"text":") — do NOT download and re-pass as local path","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"The script detects URL prefixes (","type":"text"},{"text":"https://","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"oss://","type":"text","marks":[{"type":"code_inline"}]},{"text":") and passes them through without re-upload","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"local_path","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the response only for user preview or non-API operations","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"When passing this skill's output to another skill (e.g., vace edit, vision analyze):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass ","type":"text","marks":[{"type":"strong"}]},{"text":"video_url","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" from the response","type":"text","marks":[{"type":"strong"}]},{"text":" — do NOT download and re-pass as local path","type":"text"}]}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Scenario","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Feed to another skill","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"image_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" (URL)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show to user / local playback","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"local_path","type":"text","marks":[{"type":"code_inline"}]},{"text":" (local file)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Important Notes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Async only","type":"text","marks":[{"type":"strong"}]},{"text":": All video APIs require ","type":"text"},{"text":"X-DashScope-Async: enable","type":"text","marks":[{"type":"code_inline"}]},{"text":" header.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"kf2v","type":"text","marks":[{"type":"strong"}]},{"text":": Uses a ","type":"text"},{"text":"different API endpoint","type":"text","marks":[{"type":"strong"}]},{"text":". Duration ","type":"text"},{"text":"fixed at 5s","type":"text","marks":[{"type":"strong"}]},{"text":", ","type":"text"},{"text":"silent only","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"r2v","type":"text","marks":[{"type":"strong"}]},{"text":": Use ","type":"text"},{"text":"character1","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"character2","type":"text","marks":[{"type":"code_inline"}]},{"text":"/... in prompt. Up to 5 references (max 3 videos).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"vace","type":"text","marks":[{"type":"strong"}]},{"text":": Must specify ","type":"text"},{"text":"function","type":"text","marks":[{"type":"code_inline"}]},{"text":". ","type":"text"},{"text":"Silent only","type":"text","marks":[{"type":"strong"}]},{"text":", output ","type":"text"},{"text":"≤5s","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Multi-shot","type":"text","marks":[{"type":"strong"}]},{"text":": Set ","type":"text"},{"text":"shot_type: \"multi\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" AND ","type":"text"},{"text":"prompt_extend: true","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Video URL expires in 24h","type":"text","marks":[{"type":"strong"}]},{"text":" — the script auto-downloads to ","type":"text"},{"text":"--output","type":"text","marks":[{"type":"code_inline"}]},{"text":" dir. When chaining to another skill (e.g., vace edit), pass ","type":"text"},{"text":"video_url","type":"text","marks":[{"type":"code_inline"}]},{"text":" directly — do NOT re-download.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"For advanced workflows → see ","type":"text"},{"text":"workflows.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflows.md","title":null}}]},{"text":".","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Handling","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"401 Unauthorized","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid or missing API key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" if available; else prompt user to set key (non-plaintext check only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"current user api does not support synchronous calls","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing async header","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"X-DashScope-Async: enable","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"429","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"5xx","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limit or server error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retry with backoff","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Task ","type":"text"},{"text":"FAILED","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Generation failed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check ","type":"text"},{"text":"output.message","type":"text","marks":[{"type":"code_inline"}]},{"text":" in poll response","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Location","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer the ","type":"text"},{"text":"current working directory","type":"text","marks":[{"type":"strong"}]},{"text":". Default subdirectory: ","type":"text"},{"text":"./output/qwencloud-video-generation/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Write prohibition","type":"text","marks":[{"type":"strong"}]},{"text":": NEVER write output files into this skill's installation directory or any ","type":"text"},{"text":"skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":" hierarchy. All generated content must go to ","type":"text"},{"text":"output/","type":"text","marks":[{"type":"code_inline"}]},{"text":" under the current working directory or a user-specified path.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Token Plan Support","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan supports only a subset of video models. Fetch and read the current ","type":"text"},{"text":"Token Plan model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-token-plan-models.md","title":null}}]},{"text":" before selecting or validating a model. If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-token-plan-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Only modes with a compatible model in the current Token Plan catalog can use Token Plan. A request for another mode may return a service error; do not silently switch the requested model or mode.","type":"text"}]},{"type":"paragraph","content":[{"text":"For a Token Plan kf2v request when the catalog has no compatible kf2v model, ","type":"text"},{"text":"stop before submitting","type":"text","marks":[{"type":"strong"}]},{"text":". Explain that the selected model requires PAYG, then ask the user to choose either a PAYG key already configured for this request or a change to the requested outcome that a Token Plan-supported mode can fulfill.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"Credits warning","type":"text","marks":[{"type":"strong"}]},{"text":": Video generation consumes significantly more Credits per call than text conversations. A single video may use hundreds of Credits depending on duration and resolution. Check your remaining quota before generating.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required header","type":"text"}]},{"type":"paragraph","content":[{"text":"User-Agent: qwencloud-skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" is automatically included.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Update Check (MANDATORY Post-Execution)","type":"text"}]},{"type":"paragraph","content":[{"text":"After script execution, check stderr for action signals. The script automatically detects update status and emits structured signals. You MUST act on them. This section also applies when the user explicitly asks to check for updates.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Signal: ","type":"text"},{"text":"[ACTION_REQUIRED] qwencloud-update-check skill is not installed","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"When stderr contains this signal, you MUST present the following options to the user and ","type":"text"},{"text":"wait for their response before proceeding","type":"text","marks":[{"type":"strong"}]},{"text":". Do NOT silently skip this.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"The update-check skill is not installed. It keeps you informed about new qwencloud/qwencloud-ai features and bug fixes.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install now","type":"text","marks":[{"type":"strong"}]},{"text":" — I'll run the install command for you","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skip for now","type":"text","marks":[{"type":"strong"}]},{"text":" — do nothing this time","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never remind me","type":"text","marks":[{"type":"strong"}]},{"text":" — permanently suppress this prompt","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"After the user responds:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"1","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"npx skills add QwenCloud/qwencloud-ai --skill qwencloud-update-check -y","type":"text","marks":[{"type":"code_inline"}]},{"text":", then run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --dismiss","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"2","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --dismiss","type":"text","marks":[{"type":"code_inline"}]},{"text":", then continue normally","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"3","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --never-install","type":"text","marks":[{"type":"code_inline"}]},{"text":", then continue normally","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Signal: ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"When stderr contains this signal, you MUST append a brief update notice to your response, including the version info and the update command shown in the stderr output.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No signal in stderr","type":"text"}]},{"type":"paragraph","content":[{"text":"If stderr contains neither ","type":"text"},{"text":"[ACTION_REQUIRED]","type":"text","marks":[{"type":"code_inline"}]},{"text":" nor ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]},{"text":", no action is needed — the skill is installed and up to date (or cached within 24h).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Explicit user request","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user explicitly asks to check for updates (e.g. \"check for updates\", \"check version\"):","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look for ","type":"text"},{"text":"qwencloud-update-check/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" in sibling skill directories.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If found — run: ","type":"text"},{"text":"python3 \u003cqwencloud-update-check-dir>/scripts/check_update.py --print-response","type":"text","marks":[{"type":"code_inline"}]},{"text":" and report the result.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If not found — present the install options above.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":" — Fallback paths (curl for all modes, code generation, autonomous)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"request-fields.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/request-fields.md","title":null}}]},{"text":" — Detailed field tables by mode + audio handling","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"workflows.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/workflows.md","title":null}}]},{"text":" — Duration extensions, audio workarounds, multi-shot, VACE pipelines","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"polling-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/polling-guide.md","title":null}}]},{"text":" — Polling patterns and timing recommendations","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"merge-media.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/merge-media.md","title":null}}]},{"text":" — Guide for generating merge/trim/audio-overlay code","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"examples.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/examples.md","title":null}}]},{"text":" — Full script execution examples for all modes","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sources.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sources.md","title":null}}]},{"text":" — Official documentation URLs","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"qwencloud-video-generation","author":"@skillopedia","source":{"stars":44,"repo_name":"qwencloud-ai","origin_url":"https://github.com/QwenCloud/qwencloud-ai/blob/HEAD/skills/video/qwencloud-video-generation/SKILL.md","repo_owner":"QwenCloud","body_sha256":"24acf2fe0cb4e53a1e06ae152829b0a9e38b010978ed5f73a6c54c59a3a5ec6e","cluster_key":"5ac7e0b6f630609a168e7b064bc675b977612a606f0c9d25c802ace9fa11e4e1","clean_bundle":{"format":"clean-skill-bundle-v1","source":"QwenCloud/qwencloud-ai/skills/video/qwencloud-video-generation/SKILL.md","text_bytes":170386,"source_commit":"60542ab371ae2adc04937a3fdbbafe7baad7738c","skills_sh_rank":9600,"attachment_count":18,"text_attachments":18,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/video/qwencloud-video-generation/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"media-content","import_tag":"skills-sh-top20k-2026-11","description":"Generate videos using Wan models. Supports text-to-video, image-to-video, first+last frame, reference-based role-play, and video editing (VACE). TRIGGER when: user wants to create, generate, or edit video content, mentions video generation/animation/video clips/Wan models, or explicitly invokes this skill by name (e.g. use qwencloud-video-generation). DO NOT TRIGGER when: user wants to generate images (use qwencloud-image-generation), understand/analyze existing videos (use qwencloud-vision), text-only tasks.","compatibility":"Requires Python 3.9+; curl is PAYG-only. Cursor: auto-loaded. Claude Code: read this skill's SKILL.md before first use."}},"renderedAt":1793661745407}

Qwen Video Generation Generate videos using Wan models. All tasks are asynchronous — submit, then poll until completion. This skill is part of qwencloud/qwencloud-ai . ⚠️ Critical Parameter Differences: - kf2v (First+Last Frame) : Duration is fixed at 5 seconds — other values will fail. Output is silent only . - Resolution parameters vary by model family, not mode alone : Fetch the current model catalog before choosing , , or . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details. | Location | Purp…