Qwen Video Generation Generate videos using Wan and HappyHorse models. All tasks are asynchronous — submit, then poll until completion. This skill is part of QianWen-AI/qianwen-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 : Check the current model catalog before choosing , , or . For example, uses + , while uses . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default…

\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\": \"qianwenai-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 last_err = f\"HTTP {exc.code}: {body[:500]}\"\n if api_key and len(api_key) > 8 and api_key in body:\n last_err = last_err.replace(api_key, mask_key(api_key))\n if exc.code not in _RETRYABLE_CODES or attempt >= retries:\n raise RuntimeError(last_err) from exc\n time.sleep(backoff * (2 ** attempt))\n except urllib.error.URLError as exc:\n last_err = str(exc.reason)\n if attempt >= retries:\n raise RuntimeError(f\"Network error: {last_err}\") from exc\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\": \"qianwenai-skill\"})\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":43758,"content_sha256":"9343069363f8e31b7178d2462c79e2e7d56cb7a30665544d9072b1d39589d8b0"},{"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 qianwen_lib import load_cdn_model_config, resolve_file # 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\"\n\n_MODEL_CONFIG_FILE = \"qianwen-video-generation-config.json\"\n_MODEL_CONFIG_KEYS = (\n \"default_models\",\n \"wan27_t2v_models\",\n \"wan27_i2v_models\",\n \"happyhorse_i2v_models\",\n \"happyhorse_r2v_models\",\n \"wan27_r2v_models\",\n \"wan30_video_models\",\n \"video_edit_models\",\n)\n\n\ndef _model_config() -> dict[str, Any]:\n return load_cdn_model_config(_MODEL_CONFIG_FILE, required_keys=_MODEL_CONFIG_KEYS)\n\n\ndef _model_ids(key: str) -> frozenset[str]:\n values = _model_config()[key]\n if not isinstance(values, list) or not all(isinstance(item, str) for item in values):\n raise RuntimeError(f\"Invalid video model configuration: {key} must be a string array\")\n return frozenset(values)\n\n\ndef get_default_model(mode: str) -> str:\n defaults = _model_config()[\"default_models\"]\n if not isinstance(defaults, dict):\n raise RuntimeError(\"Invalid video model configuration: default_models must be an object\")\n model = defaults.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# wan2.7-style models use resolution+ratio (instead of size).\n# NOTE: happyhorse-1.0-t2v and happyhorse-1.1-t2v keep sharing the wan2.7-t2v structure (resolution+ratio).\n# happyhorse-1.0-i2v / happyhorse-1.1-i2v have been split out into their own predicate/builder\n# because their API spec diverges significantly from wan2.7-i2v\n# (no negative_prompt / no prompt_extend / no ratio / media must be exactly\n# one {type:'first_frame'}). See _build_happyhorse_i2v_payload below.\n# happyhorse-r2v uses media[{type:reference_image, url}] + resolution+ratio\n# (different from wan2.6-r2v which uses reference_urls + size).\n\n# wan2.7-r2v uses input.media = [{type, url}, ...] with mixed reference types\n# (reference_image / reference_video / reference_audio, up to 5) + resolution\n# + duration + prompt_extend + watermark. NO `ratio` (auto-derived from refs).\n# Distinct from happyhorse-r2v (reference_image only) and wan2.6-r2v (reference_urls+size).\n\n# wan3.0-video / wan3.0-video-prime are all-in-one models supporting both t2v\n# (input.prompt only) and i2v (input.media=[{type:reference_image, url}]).\n# parameters: resolution (480P~1080P) + ratio + duration (\u003c=30s).\n# Routed by model id inside build_t2v_payload / build_i2v_payload.\n\n# Video-edit models share a unified payload (media=[1 video]+[refs], no `function`).\n\n\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}\n\n_PRICING_URL = \"https://platform.qianwenai.com/docs/developer-guides/getting-started/pricing\"\n\n# ---------------------------------------------------------------------------\n# Mode detection\n# ---------------------------------------------------------------------------\n\ndef detect_mode(request: dict[str, Any]) -> str:\n \"\"\"Auto-detect video generation mode from request fields.\"\"\"\n # Highest priority: video-edit models do NOT have `function` field;\n # detect by model id so they don't fall through to MODE_VACE or MODE_I2V.\n model = request.get(\"model\", \"\")\n if is_video_edit_model(model):\n return MODE_VIDEO_EDIT\n # wan3.0-video / wan3.0-video-prime are all-in-one: pick t2v vs i2v by whether\n # a reference image is supplied (media / img_url / reference_image).\n if model in _model_ids(\"wan30_video_models\"):\n if (request.get(\"media\") or request.get(\"img_url\")\n or request.get(\"reference_image\")\n or request.get(\"reference_urls\")):\n return MODE_I2V\n return MODE_T2V\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 also be triggered by model id alone\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\", \"video_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\", \"reference_urls\"],\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\", \"media\"],\n MODE_VIDEO_EDIT: [\"video_url\", \"reference_images\", \"media\"],\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 # Original logic: handle list[str]\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 build_t2v_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for text-to-video generation (wan2.6/wan2.7/happyhorse-t2v).\"\"\"\n # wan3.0-video / wan3.0-video-prime use their own unified builder.\n if model in _model_ids(\"wan30_video_models\"):\n return _build_wan30_video_payload(request, model)\n\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 / 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 # 1b) wan3.0-video / wan3.0-video-prime use their own unified builder.\n if model in _model_ids(\"wan30_video_models\"):\n return _build_wan30_video_payload(request, model)\n\n # 2) wan2.7-i2v uses media array\n is_v27 = is_wan27_i2v_model(model)\n if is_v27 or request.get(\"media\") or request.get(\"first_clip_url\"):\n return _build_i2v_v27_payload(request, model)\n\n # 3) wan2.6-i2v uses img_url\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.\n\n Per official docs (ref/.../happyhorse-image-to-video/create-task.md):\n - input: ONLY `prompt` (optional) and `media` (required).\n - media: must be EXACTLY ONE item with `type='first_frame'`.\n - parameters: ONLY `resolution` / `duration` / `watermark` / `seed`.\n - duration range: 3-15 seconds.\n - resolution enum: '720P' | '1080P', default '1080P'.\n - REJECTED by server (will 400) if any of these are sent:\n `negative_prompt`, `prompt_extend`, `ratio`,\n `last_frame`, `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\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 ONLY. happyhorse-1.0-i2v has its own dedicated\n builder (_build_happyhorse_i2v_payload) -- do NOT add happyhorse\n compat code here.\n\n Supports: first_frame, last_frame, driving_audio, first_clip\n (video continuation), explicit media[] passthrough.\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 f\"{model} 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 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 a different payload structure (media array + resolution+ratio).\n if model in _model_ids(\"happyhorse_r2v_models\"):\n return _build_r2v_happyhorse_payload(request, model)\n\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\ndef _build_r2v_happyhorse_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for happyhorse-1.0-r2v.\n\n Structure: input.media = [{type:\"reference_image\", url}, ...] + resolution + ratio.\n Up to 9 reference images supported.\n \"\"\"\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 if not media:\n raise ValueError(\n \"happyhorse-1.0-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\", \"audio_setting\"):\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\n_WAN27_R2V_MAX_MEDIA = 5\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 - input.prompt: passthrough.\n - parameters: resolution (720P/1080P) + duration (\u003c=10s) + prompt_extend + watermark.\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 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_wan30_video_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for wan3.0-video / wan3.0-video-prime (unified t2v + i2v).\n\n Verified contract (tmp/test-new-models/wan3.0-video, wan3.0-video-prime):\n - t2v: input has only `prompt` (no media).\n - i2v: input.media = [{type:'reference_image', url}, ...] from\n media / img_url / reference_image.\n - parameters: resolution (480P~1080P) + ratio (adaptive/16:9/9:16/1:1)\n + duration (\u003c=30s).\n \"\"\"\n input_obj: dict[str, Any] = {\"prompt\": request.get(\"prompt\", \"\")}\n\n media: list[dict[str, str]] = []\n if request.get(\"media\"):\n for item in request[\"media\"]:\n if isinstance(item, dict):\n media.append(item)\n else:\n media.append({\"type\": \"reference_image\", \"url\": str(item)})\n else:\n url = request.get(\"img_url\") or request.get(\"reference_image\")\n if url:\n media.append({\"type\": \"reference_image\", \"url\": str(url)})\n # reference_urls → media compat layer (silent mapping, same as wan2.7-r2v)\n for ref_url in (request.get(\"reference_urls\") or []):\n media.append({\"type\": \"reference_image\", \"url\": str(ref_url)})\n if media:\n input_obj[\"media\"] = media\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 for key in (\"ratio\", \"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\ndef build_video_edit_payload(request: dict[str, Any], model: str) -> dict[str, Any]:\n \"\"\"Build payload for happyhorse-1.0-video-edit and wan2.7-videoedit.\n\n Structure: input.media = [{type:\"video\", url}] + [{type:\"reference_image\", url}, ...]\n No `function` field. Optional negative_prompt (wan2.7-videoedit only).\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 reference_images=[...].\"\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 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_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\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}\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 both output formats.\"\"\"\n output = result.get(\"output\", {})\n url = output.get(\"video_url\")\n if url:\n return url\n results = output.get(\"results\", [])\n if 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 \" \".join(part for part in parts if part)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":28638,"content_sha256":"1063858cda5329549c9dfeec6a8a8faf935164712fcbd54430d6018f57445fed"},{"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\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 qianwen_lib import ( # noqa: E402\n download_file,\n http_request,\n is_token_plan_key,\n load_request,\n native_base_url,\n poll_task,\n require_api_key,\n run_update_signal,\n validate_token_plan_model,\n)\nfrom video_lib import ( # noqa: E402\n ENDPOINTS,\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_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_NOT_ACTIVATED_KEYWORDS = (\"not activated\", \"not subscribed\", \"not enabled\",\n \"product is not activated\", \"model not subscribed\")\n_MODEL_MARKET_URL = \"https://www.qianwenai.com/models\"\n_TOKEN_PLAN_CONSOLE_URL = (\n \"https://platform.qianwenai.com/home/billing/subscription/token-plan\"\n)\n_VIDEO_EXTENSIONS = frozenset({\n \".mp4\", \".mov\", \".webm\", \".mkv\", \".m4v\", \".avi\", \".flv\", \".wmv\", \".ts\",\n})\n\n\ndef _check_not_activated(error_msg: str, model: str) -> bool:\n \"\"\"Check if error indicates model not activated; print guidance if so.\"\"\"\n msg_lower = error_msg.lower()\n if any(kw in msg_lower for kw in _NOT_ACTIVATED_KEYWORDS):\n print(f'\\nERROR: Model \"{model}\" is not activated on your account.',\n file=sys.stderr)\n print(f'To activate this model, visit: {_MODEL_MARKET_URL}',\n file=sys.stderr)\n print('Then find the model and click \"Enable\" / \"\\u5f00\\u901a\" to activate it.',\n file=sys.stderr)\n print(f'Original error: {error_msg}', file=sys.stderr)\n return True\n return False\n\n# ---------------------------------------------------------------------------\ndef _handle_result(result: dict[str, Any], args: argparse.Namespace,\n model: 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)\n print(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(f\"Error: No video URL in result: {result}\", file=sys.stderr)\n sys.exit(1)\n\n # Determine output: user-specified filename (video extension) vs directory\n if args.output.suffix.lower() in _VIDEO_EXTENSIONS:\n video_file = args.output\n out_dir = video_file.parent\n else:\n out_dir = args.output\n url_basename = Path(urlparse(video_url).path).name\n if url_basename and \".\" in url_basename:\n video_file = out_dir / url_basename\n else:\n video_file = out_dir / \"video.mp4\"\n\n out_dir.mkdir(parents=True, exist_ok=True)\n resp_file = out_dir / \"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 try:\n download_file(video_url, video_file)\n except Exception as e:\n print(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(f\"Video URL: {video_url}\", file=sys.stderr)\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main() -> None:\n run_update_signal(caller=__file__)\n help_default_models: dict[str, str] = {}\n if any(arg in (\"-h\", \"--help\") for arg in sys.argv[1:]):\n help_default_models = {\n mode: get_default_model(mode)\n for mode in (\"t2v\", \"i2v\", \"kf2v\", \"r2v\", \"vace\", \"videoedit\")\n }\n\n def help_default_suffix(mode: str) -> str:\n model = help_default_models.get(mode)\n return f\" (default: {model})\" if model else \"\"\n\n parser = argparse.ArgumentParser(\n description=\"Generate video via Wan and HappyHorse models (t2v/i2v/kf2v/r2v/vace)\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=(\n \"mode auto-detection (from request JSON fields):\\n\"\n f\" t2v prompt only → text-to-video{help_default_suffix('t2v')}\\n\"\n f\" i2v img_url/media/first_frame_url → image-to-video{help_default_suffix('i2v')}\\n\"\n f\" kf2v first_frame_url (without media) → keyframe-to-video{help_default_suffix('kf2v')}\\n\"\n f\" r2v reference_urls → reference role-play{help_default_suffix('r2v')}\\n\"\n f\" vace function → video editing/repaint/extend{help_default_suffix('vace')}\\n\"\n f\" videoedit video-edit model → video editing{help_default_suffix('videoedit')}\\n\"\n \"\"\"\n\nmodel version differences (handled automatically):\n wan2.6-t2v / wan2.7-t2v / happyhorse-1.0-t2v:\n - wan2.6-t2v uses \"size\" (e.g. \"1280*720\"), supports seed/shot_type\n - wan2.7-t2v / happyhorse-1.0-t2v use \"resolution\" + \"ratio\"\n wan2.6-i2v / wan2.7-i2v / happyhorse-1.0-i2v:\n - wan2.6-i2v uses single img_url\n - wan2.7-i2v uses media[] array (first_frame/last_frame/driving_audio/first_clip),\n supports negative_prompt + prompt_extend\n - happyhorse-1.0-i2v has its own strict spec: media=[{type:'first_frame',url}]\n (exactly one). Rejects negative_prompt / prompt_extend / ratio / last_frame /\n first_clip / driving_audio. Legacy img_url is auto-converted for convenience.\n wan2.6-r2v / happyhorse-1.0-r2v:\n - wan2.6-r2v uses reference_urls + size\n - happyhorse-1.0-r2v uses media=[{type:reference_image, url}] + resolution + ratio (≤9 refs)\n wan2.7-videoedit / happyhorse-1.0-video-edit:\n - both use input.media = [{type:video, url}] + [{type:reference_image, url}] (no `function` field)\n - wan2.7-videoedit additionally supports negative_prompt; happyhorse caps refs at 5\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)\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\n reference_urls Character reference URLs (r2v)\n function VACE: repainting, editing, extension, outpainting\n video_url Source video (VACE)\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 DASHSCOPE_API_KEY (required) API key — also loaded from .env\n QIANWEN_API_KEY (alternative)\n QWEN_REGION cn-beijing (default)\n\nexamples:\n # Text-to-video with the configured default model\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\"\"\"),\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/qianwen-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\"],\n help=\"Force mode (auto-detected from request fields if omitted). \"\n \"'videoedit' is for wan2.7-videoedit / happyhorse-1.0-video-edit.\")\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 \"\")\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(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(f\"Error: {e}\", file=sys.stderr)\n print(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 \"\")\n return\n\n # --- Normal mode: submit new task ---\n try:\n request = load_request(args)\n except ValueError as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # CLI --model takes precedence over any \"model\" in the request JSON. Inject it\n # into `request` BEFORE detect_mode() so that model-based routing (R2V for\n # wan2.7-r2v / happyhorse-r2v, wan3.0-video t2v/i2v, video-edit) fires even when\n # the model is supplied only via --model. Otherwise detect_mode() would see the\n # `media` field first and misclassify wan2.7-r2v/happyhorse-r2v as i2v, bypassing\n # their dedicated builders (and the wan2.7-r2v len(media)\u003c=5 guard).\n if args.model:\n request[\"model\"] = args.model\n mode = args.mode or detect_mode(request)\n model = args.model or request.get(\"model\") or get_default_model(mode)\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 # (detect_mode has no wan2.7-i2v branch, so this correction is still required.)\n if is_wan27_i2v_model(model) or is_happyhorse_i2v_model(model):\n if mode == MODE_KF2V:\n mode = MODE_I2V\n # Video-edit models: correct t2v fallback when --model specifies a video-edit model\n if is_video_edit_model(model) and mode != MODE_VIDEO_EDIT:\n mode = MODE_VIDEO_EDIT\n duration = request.get(\"duration\", 5)\n resolution = resolve_resolution(request, mode)\n token_plan = is_token_plan_key(api_key)\n validate_token_plan_model(api_key, model)\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:\n info += f\" | Credits: see {_TOKEN_PLAN_CONSOLE_URL}\"\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\n builder = PAYLOAD_BUILDERS[mode]\n try:\n payload = builder(request, model)\n except (ValueError, KeyError) as e:\n print(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)\n print(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(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(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(f\"Error: {e}\", file=sys.stderr)\n print(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)\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":16633,"content_sha256":"8e13b209fcec34d27bc2076fa466946ef25a6128b4a2a49d60ecb744932cf56c"}],"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 and HappyHorse 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":"QianWen-AI/qianwen-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":": Check 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":". For example, ","type":"text"},{"text":"happyhorse-1.1-t2v","type":"text","marks":[{"type":"code_inline"}]},{"text":" uses ","type":"text"},{"text":"resolution","type":"text","marks":[{"type":"code_inline"}]},{"text":" + ","type":"text"},{"text":"ratio","type":"text","marks":[{"type":"code_inline"}]},{"text":", while ","type":"text"},{"text":"wan2.6-t2v","type":"text","marks":[{"type":"code_inline"}]},{"text":" uses ","type":"text"},{"text":"size","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":"$DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" in shell, ","type":"text"},{"text":"os.environ[\"DASHSCOPE_API_KEY\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" in Python). 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":"DASHSCOPE_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":"QianWen Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://platform.qianwenai.com/home/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":"Both PAYG (","type":"text"},{"text":"sk-ws-...","type":"text","marks":[{"type":"code_inline"}]},{"text":"; legacy ","type":"text"},{"text":"sk-...","type":"text","marks":[{"type":"code_inline"}]},{"text":") and Token Plan (","type":"text"},{"text":"sk-sp-...","type":"text","marks":[{"type":"code_inline"}]},{"text":") keys are supported. 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 qianwen_lib import detect_api_key_type\nprint(detect_api_key_type('scripts/qianwen_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 — use only models from the Token Plan catalog below.","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":"Pay-as-you-go key — full model catalog available.","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 key configured.","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.alicdn.com/skills-info/models/references/qianwen-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/qianwen-token-plan-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan does not support local file upload; for i2v/r2v/kf2v modes, provide reference images/videos as accessible URLs (","type":"text"},{"text":"https://","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"oss://","type":"text","marks":[{"type":"code_inline"}]},{"text":") rather than local paths.","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan supports only specific models — use exactly a model from the references above; do not guess or probe model availability. For PAYG, continue below.","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":", ","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 (no ","type":"text"},{"text":"function","type":"text","marks":[{"type":"code_inline"}]},{"text":" field, uses media array)","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":"; read the CDN model catalog for supported models","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Model Selection","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":" → use directly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Consult the qianwen-model-selector skill","type":"text","marks":[{"type":"strong"}]},{"text":" when model choice depends on capability, scenario, or pricing.","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Before selecting, recommending, or defaulting a model, fetch and read the current ","type":"text"},{"text":"Qwen video-generation model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth.alicdn.com/skills-info/models/references/qianwen-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/qianwen-video-generation-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Endpoint: ","type":"text"},{"text":"/services/aigc/video-generation/video-synthesis","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Important","type":"text","marks":[{"type":"strong"}]},{"text":": The model list above 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.qianwenai.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.qianwenai.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":"qianwen-model-selector","type":"text","marks":[{"type":"strong"}]},{"text":" skill or ","type":"text"},{"text":"QianWen CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"qianwen 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":"qianwen-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":": Use the non-plaintext detector in ","type":"text"},{"text":"Key Compatibility","type":"text","marks":[{"type":"strong"}]},{"text":"; do not replace it with a variable-presence check. If no Key is found, use qianwen-ops-auth when available or guide the user to configure ","type":"text"},{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"QIANWEN_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" in ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":". Skills may be installed independently.","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 not found, try ","type":"text"},{"text":"python --version","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"py -3 --version","type":"text","marks":[{"type":"code_inline"}]},{"text":". If Python is unavailable or below 3.9, skip to ","type":"text"},{"text":"Path 2 (curl)","type":"text","marks":[{"type":"strong"}]},{"text":" in ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":".","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 --request '{\"model\":\"happyhorse-1.1-t2v\",\"prompt\":\"A detective in a rainy city at night\",\"resolution\":\"720P\",\"ratio\":\"16:9\",\"duration\":5}' \\\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)","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 path","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Directory or file path. If path ends with a video extension (.mp4/.mov/.webm/.mkv/.m4v etc.), it is used as the output filename (parent dir auto-created). Otherwise treated as directory — video is auto-named from the OSS 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":"blockquote","content":[{"type":"paragraph","content":[{"text":"Model priority","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"--model","type":"text","marks":[{"type":"code_inline"}]},{"text":" CLI flag > ","type":"text"},{"text":"\"model\"","type":"text","marks":[{"type":"code_inline"}]},{"text":" field in ","type":"text"},{"text":"--request","type":"text","marks":[{"type":"code_inline"}]},{"text":" JSON > configured default. The configured default is used when no model is specified and is not necessarily the newest or strongest model.","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":" → skip directly to Path 2 (curl) in ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":".","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":"QIANWEN_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":"QianWen Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://platform.qianwenai.com/home/api-keys","title":null}}]},{"text":"; add to ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"echo 'DASHSCOPE_API_KEY=sk-...' >> .env","type":"text","marks":[{"type":"code_inline"}]},{"text":"; or run ","type":"text"},{"text":"qianwen-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":"qianwen-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":". 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://platform.qianwenai.com/docs/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.alicdn.com/skills-info/models/references/qianwen-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":"qianwen-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill to check remaining free tier quota, or verify in the user's ","type":"text"},{"text":"QianWen console","type":"text","marks":[{"type":"link","attrs":{"href":"https://platform.qianwenai.com/home/benefits","title":null}}]},{"text":". If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qianwen-model-pricing.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"To check actual usage and bills: use the ","type":"text"},{"text":"qianwen-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://platform.qianwenai.com/home/analytics","title":null}}]},{"text":" | ","type":"text"},{"text":"Pay-as-you-go Billing","type":"text","marks":[{"type":"link","attrs":{"href":"https://platform.qianwenai.com/home/billing/pay-as-you-go","title":null}}]},{"text":" | ","type":"text"},{"text":"Token Plan Subscription","type":"text","marks":[{"type":"link","attrs":{"href":"https://platform.qianwenai.com/home/billing/subscription/token-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 qianwen-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":"qianwen-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":"The product is not activated","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"Model not subscribed","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Third-party model not enabled on account","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Visit ","type":"text"},{"text":"Model Market","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.qianwenai.com/models","title":null}}]},{"text":", find the model, click \"Enable\" / \"开通\"","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/qianwen-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":"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] qianwen-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 QianWen-AI/qianwen-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 QianWen-AI/qianwen-ai --skill qianwen-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":"qianwen-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 \u003cqianwen-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":"qianwen-video-generation","author":"@skillopedia","source":{"stars":120,"repo_name":"qianwen-ai","origin_url":"https://github.com/QianWen-AI/qianwen-ai/blob/HEAD/skills/video/qianwen-video-generation/SKILL.md","repo_owner":"QianWen-AI","body_sha256":"8210de8cac617a9823a27a418b859c3600367077fa91ba5e6ef6f687e3c888ec","cluster_key":"6971149de815f03e952a12f35636b0fcc6f8d6224500d76dca906fe569b05da5","clean_bundle":{"format":"clean-skill-bundle-v1","source":"QianWen-AI/qianwen-ai/skills/video/qianwen-video-generation/SKILL.md","text_bytes":164312,"source_commit":"11b1805167d91bc931dbaad85e17036a72e98ae0","skills_sh_rank":3976,"attachment_count":18,"text_attachments":18,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/video/qianwen-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 and HappyHorse 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/HappyHorse models, or explicitly invokes this skill by name (e.g. use qianwen-video-generation). DO NOT TRIGGER when: user wants to generate images (use qianwen-image-generation), understand/analyze existing videos (use qianwen-vision), text-only tasks.","compatibility":"Requires Python 3.9+ and curl. Cursor: auto-loaded. Claude Code: read this skill's SKILL.md before first use."}},"renderedAt":1793666724967}

Qwen Video Generation Generate videos using Wan and HappyHorse models. All tasks are asynchronous — submit, then poll until completion. This skill is part of QianWen-AI/qianwen-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 : Check the current model catalog before choosing , , or . For example, uses + , while uses . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default…