@remixmate/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +100 -83
  2. package/README.zh-CN.md +183 -0
  3. package/dist/handlers/gen-digital-human.d.ts +12 -0
  4. package/dist/handlers/gen-digital-human.js +157 -0
  5. package/dist/handlers/gen-image.js +5 -41
  6. package/dist/handlers/gen-video.d.ts +18 -0
  7. package/dist/handlers/gen-video.js +153 -0
  8. package/dist/handlers/gen-voice.js +2 -12
  9. package/dist/handlers/index.js +4 -2
  10. package/dist/handlers/shared.d.ts +26 -0
  11. package/dist/handlers/shared.js +66 -0
  12. package/dist/http.d.ts +4 -2
  13. package/dist/http.js +5 -3
  14. package/dist/manifest.json +38 -24
  15. package/dist/registry.d.ts +5 -16
  16. package/dist/registry.js +11 -17
  17. package/dist/skill-schema.d.ts +65 -0
  18. package/dist/skill-schema.js +53 -0
  19. package/package.json +5 -4
  20. package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
  21. package/skills/export-jianying/skill.json +1 -0
  22. package/skills/gen-digital-human/SKILL.md +23 -46
  23. package/skills/gen-digital-human/skill.json +4 -2
  24. package/skills/gen-digital-human/version.json +1 -1
  25. package/skills/gen-image/SKILL.md +2 -2
  26. package/skills/gen-image/skill.json +3 -1
  27. package/skills/gen-image/version.json +1 -1
  28. package/skills/gen-script/SKILL.md +13 -13
  29. package/skills/gen-script/scripts/gen_script.py +50 -12
  30. package/skills/gen-script/skill.json +3 -1
  31. package/skills/gen-video/SKILL.md +22 -33
  32. package/skills/gen-video/skill.json +4 -2
  33. package/skills/gen-video/version.json +1 -1
  34. package/skills/gen-voice/SKILL.md +1 -1
  35. package/skills/gen-voice/skill.json +1 -0
  36. package/skills/gen-voice/version.json +1 -1
  37. package/skills/prepare-video-assets/SKILL.md +2 -2
  38. package/skills/prepare-video-assets/skill.json +2 -1
  39. package/skills/render-video/scripts/render_video.py +8 -8
  40. package/skills/render-video/skill.json +1 -0
  41. package/skills/{template-bind → template-registry}/README.md +4 -4
  42. package/skills/{template-bind → template-registry}/SKILL.md +14 -12
  43. package/skills/template-registry/scripts/list_templates.py +214 -0
  44. package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
  45. package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
  46. package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
  47. package/skills/{template-bind → template-registry}/skill.json +8 -5
  48. package/skills/template-registry/version.json +7 -0
  49. package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
  50. package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
  51. package/skills/video-parser/SKILL.md +47 -160
  52. package/skills/video-parser/skill.json +4 -4
  53. package/skills/video-parser/version.json +1 -1
  54. package/skills/web-capture/SKILL.md +407 -116
  55. package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
  56. package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
  57. package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
  58. package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
  59. package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
  60. package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
  61. package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
  62. package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
  63. package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
  64. package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
  65. package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
  66. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
  67. package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
  68. package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
  69. package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
  70. package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
  71. package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
  72. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
  73. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
  74. package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
  75. package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
  76. package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
  77. package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
  78. package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
  79. package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
  80. package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
  81. package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
  82. package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
  83. package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
  84. package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
  85. package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
  86. package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
  87. package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
  88. package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
  89. package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
  90. package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
  91. package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
  92. package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
  93. package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
  94. package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
  95. package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
  96. package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
  97. package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
  98. package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
  99. package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
  100. package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
  101. package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
  102. package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
  103. package/skills/web-capture/scripts/record.py +186 -0
  104. package/skills/web-capture/scripts/screenshot.py +94 -0
  105. package/skills/web-capture/skill.json +15 -14
  106. package/skills/web-capture/templates/github-code-tour.json +59 -0
  107. package/skills/web-capture/templates/github-readme-tour.json +77 -0
  108. package/skills/web-capture/templates/github-repo-intro.json +74 -0
  109. package/skills/web-capture/version.json +2 -2
  110. package/dist/handlers/template-bind.d.ts +0 -15
  111. package/dist/handlers/template-bind.js +0 -149
  112. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
  113. package/skills/gen-video/scripts/gen_video.py +0 -591
  114. package/skills/template-bind/version.json +0 -7
  115. package/skills/web-capture/scripts/web_capture.py +0 -461
  116. /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
  117. /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
  118. /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
  119. /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
  120. /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
  121. /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
  122. /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
  123. /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
  124. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
  125. /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
  126. /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
@@ -1,591 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- AI 生视频脚本 — 调用 ab-api POST /model/genVideo(与前端灵创「AI 视频」、GenVideoParams 对齐)
4
- 通过 priv-token(X-Priv-Token)认证。
5
-
6
- 支持模型(与 frontend ab-web AI_VIDEO_MODELS 一致):
7
- - Seedance 1.5 Pro(火山):有声、首尾帧、固定镜头、adaptive 比例,时长 4–12 秒
8
- - Veo 3.1 / Veo 3.1 Fast(Google):首尾帧、参考图、负向提示词、4K,时长 4/6/8 秒
9
-
10
- 默认行为:
11
- 生成完成后直接输出视频 URL,供调用方直接展示,无需落盘。
12
-
13
- 用法:
14
- python gen_video.py --prompt "视频描述" --duration 5 --ratio "9:16"
15
- python gen_video.py --model veo-3.1 --prompt "..." --duration 8 --resolution 4k
16
- python gen_video.py --first-frame ./a.png --last-frame ./b.png --prompt "过渡动画"
17
-
18
- 手动配置(命令行,优先级高于环境变量):
19
- --priv-token TOKEN priv-token
20
-
21
- 视频生成为异步任务,脚本会自动轮询直到完成或超时。
22
-
23
- 环境变量:
24
- MM_API_BASE_URL - 后端 API 地址(默认: http://localhost:3001/api)
25
- PRIV_TOKEN - priv-token(优先读取;未配置时提示手动输入)
26
- MM_VIDEO_MODEL - 生视频模型(默认: doubao-seedance-1-5-pro-251215)
27
- """
28
-
29
- from __future__ import annotations
30
-
31
- import argparse
32
- import base64
33
- import json
34
- import mimetypes
35
- import os
36
- import sys
37
- import time
38
- import urllib.error
39
- import urllib.request
40
-
41
- API_BASE_URL = os.environ.get("MM_API_BASE_URL", "http://localhost:3001/api")
42
- PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
43
- VIDEO_MODEL = os.environ.get("MM_VIDEO_MODEL", "doubao-seedance-1-5-pro-251215")
44
- SKILL_NAME = "gen-video"
45
- AGENT_NAME = os.environ.get("AGENT_NAME", "")
46
-
47
- # 与 frontend AI_VIDEO_MODELS 一致的 LiteLLM / 网关 model 标识(元组: model_id, 展示名, 提供方)
48
- GEN_VIDEO_MODEL_PRESETS: tuple[tuple[str, str, str], ...] = (
49
- ("doubao-seedance-1-5-pro-251215", "Seedance 1.5 Pro", "火山引擎"),
50
- ("veo-3.1-generate-001", "Veo 3.1", "Google"),
51
- ("veo-3.1-fast-generate-001", "Veo 3.1 Fast", "Google"),
52
- )
53
-
54
- # 命令行简写 -> 完整 model id(便于与前端下拉选项对应)
55
- MODEL_ALIASES: dict[str, str] = {
56
- "seedance": "doubao-seedance-1-5-pro-251215",
57
- "seedance-1.5": "doubao-seedance-1-5-pro-251215",
58
- "seedance-1.5-pro": "doubao-seedance-1-5-pro-251215",
59
- "veo": "veo-3.1-generate-001",
60
- "veo-3.1": "veo-3.1-generate-001",
61
- "veo-3.1-generate": "veo-3.1-generate-001",
62
- "veo-fast": "veo-3.1-fast-generate-001",
63
- "veo-3.1-fast": "veo-3.1-fast-generate-001",
64
- "veo-3.1-fast-generate": "veo-3.1-fast-generate-001",
65
- }
66
-
67
- # Seedance:ratio + resolution(与前端 supported* 一致)
68
- SEEDANCE_RATIOS = ("adaptive", "16:9", "4:3", "1:1", "3:4", "9:16", "21:9")
69
- SEEDANCE_RESOLUTIONS = ("480p", "720p", "1080p")
70
- SEEDANCE_DURATIONS = tuple(range(4, 13))
71
-
72
- # Veo
73
- VEO_RATIOS = ("16:9", "9:16")
74
- VEO_RESOLUTIONS = ("720p", "1080p", "4k")
75
- VEO_DURATIONS = (4, 6, 8)
76
- VEO_MAX_REFERENCE_IMAGES = 3
77
-
78
- # 并集(用于 argparse choices,具体合法性在 main 中按模型校验)
79
- ALL_RATIOS = tuple(dict.fromkeys(SEEDANCE_RATIOS + VEO_RATIOS))
80
- ALL_RESOLUTIONS = tuple(dict.fromkeys(SEEDANCE_RESOLUTIONS + VEO_RESOLUTIONS))
81
-
82
-
83
- def resolve_token() -> str:
84
- """优先从 PRIV_TOKEN 环境变量读取 token,取不到则报错退出"""
85
- token = os.environ.get("PRIV_TOKEN", "")
86
- if not token:
87
- if sys.stdin.isatty():
88
- token = input("Enter priv-token: ").strip()
89
- else:
90
- print("❌ PRIV_TOKEN env var not configured; cannot authenticate", file=sys.stderr)
91
- sys.exit(1)
92
- return token
93
-
94
-
95
- def normalize_model_id(model: str) -> str:
96
- key = model.strip().lower()
97
- return MODEL_ALIASES.get(key, model.strip())
98
-
99
-
100
- def is_seedance_model(model: str) -> bool:
101
- return "seedance" in model.lower()
102
-
103
-
104
- def is_veo_model(model: str) -> bool:
105
- return "veo" in model.lower()
106
-
107
-
108
- def gen_video_models_help_block() -> str:
109
- lines = [
110
- "可选模型(与灵创 AI 视频下拉、POST /model/genVideo 一致):",
111
- " 简写: seedance | veo | veo-fast(见 --model 说明)",
112
- ]
113
- for mid, title, provider in GEN_VIDEO_MODEL_PRESETS:
114
- lines.append(f" {mid}")
115
- lines.append(f" └ {title} · {provider}")
116
- return "\n".join(lines)
117
-
118
-
119
- def resolve_image_input(path_or_url: str) -> str:
120
- """
121
- 首帧/尾帧/参考图:支持 https URL、data: URI,或本地文件路径(读入为 data:image/...;base64,...)
122
- """
123
- s = path_or_url.strip()
124
- if not s:
125
- return ""
126
- low = s.lower()
127
- if low.startswith(("http://", "https://", "data:")):
128
- return s
129
- if not os.path.isfile(s):
130
- print(f"❌ image path does not exist or is not a file: {s}", file=sys.stderr)
131
- sys.exit(1)
132
- mime, _ = mimetypes.guess_type(s)
133
- if not mime or not mime.startswith("image/"):
134
- mime = "image/jpeg"
135
- with open(s, "rb") as f:
136
- b64 = base64.standard_b64encode(f.read()).decode("ascii")
137
- return f"data:{mime};base64,{b64}"
138
-
139
-
140
- def _build_headers(content_type: str = "application/json") -> dict:
141
- """构建统一请求头(X-Priv-Token 认证头)"""
142
- headers = {
143
- "X-Priv-Token": PRIVATE_TOKEN,
144
- "x-invoke-skill": SKILL_NAME,
145
- }
146
- if content_type:
147
- headers["Content-Type"] = content_type
148
- if AGENT_NAME:
149
- headers["x-invoke-agent"] = AGENT_NAME
150
- return headers
151
-
152
-
153
- # 视频任务状态(与 ab-api 一致:LiteLLM/库表可能返回 completed,方舟直连可能返回 succeeded)
154
- STATUS_PENDING = "pending"
155
- STATUS_PROCESSING = "processing"
156
- STATUS_SUCCEEDED = "succeeded"
157
- STATUS_COMPLETED = "completed"
158
- STATUS_FAILED = "failed"
159
-
160
-
161
- def api_request(path: str, payload: dict, *, exit_on_error: bool = True) -> dict:
162
- """
163
- 通用 API 请求函数(统一 POST 方法)
164
- exit_on_error=False 时网络/连接错误会抛出异常而非直接退出(供轮询重试使用)
165
- """
166
- if not PRIVATE_TOKEN:
167
- print("❌ priv-token not configured: set the PRIV_TOKEN env var or pass --priv-token", file=sys.stderr)
168
- sys.exit(1)
169
-
170
- url = f"{API_BASE_URL}{path}"
171
- data = json.dumps(payload).encode("utf-8")
172
- headers = _build_headers()
173
-
174
- req = urllib.request.Request(url, data=data, headers=headers, method="POST")
175
-
176
- try:
177
- with urllib.request.urlopen(req, timeout=120) as resp:
178
- return json.loads(resp.read().decode("utf-8"))
179
- except urllib.error.HTTPError as e:
180
- body = e.read().decode("utf-8")
181
- print(f"❌ API request failed (HTTP {e.code}): {body}", file=sys.stderr)
182
- sys.exit(1)
183
- except (urllib.error.URLError, OSError, ConnectionError) as e:
184
- if exit_on_error:
185
- print(f"❌ network error: {e}", file=sys.stderr)
186
- sys.exit(1)
187
- raise
188
-
189
-
190
- def create_video_task(
191
- model: str,
192
- duration: int,
193
- ratio: str,
194
- resolution: str,
195
- prompt: str = "",
196
- first_frame_image: str | None = None,
197
- last_frame_image: str | None = None,
198
- reference_images: list[str] | None = None,
199
- generate_audio: bool = False,
200
- camera_fixed: bool = False,
201
- negative_prompt: str | None = None,
202
- seed: int | None = None,
203
- person_generation: str | None = None,
204
- ) -> str:
205
- """
206
- 创建视频生成任务,返回 task_id(请求体字段与 model.ModelGenVideoDTO / 前端 GenVideoParams 对齐)
207
- """
208
- print("🎬 Submitting video generation task...")
209
- print(f" model: {model}")
210
- if prompt.strip():
211
- print(f" prompt: {prompt.strip()}")
212
- print(f" duration: {duration} s")
213
- print(f" aspect ratio: {ratio}")
214
- print(f" resolution: {resolution}")
215
- print(f" first frame: {'provided' if first_frame_image else 'none'}")
216
- print(f" end frame: {'provided' if last_frame_image else 'none'}")
217
- if reference_images:
218
- print(f" reference images: {len(reference_images)}")
219
- print(f" generate audio: {'yes' if generate_audio else 'no'}")
220
- print(f" camera fixed: {'yes' if camera_fixed else 'no'}")
221
- if negative_prompt:
222
- print(f" negative prompt: {negative_prompt[:80]}{'…' if len(negative_prompt) > 80 else ''}")
223
- if seed is not None:
224
- print(f" seed: {seed}")
225
-
226
- payload: dict = {
227
- "model": model,
228
- "duration": duration,
229
- "ratio": ratio,
230
- "resolution": resolution,
231
- "generateAudio": generate_audio,
232
- "cameraFixed": camera_fixed,
233
- }
234
- p = prompt.strip()
235
- if p:
236
- payload["prompt"] = p
237
- if first_frame_image:
238
- payload["firstFrameImage"] = first_frame_image
239
- if last_frame_image:
240
- payload["lastFrameImage"] = last_frame_image
241
- if reference_images:
242
- payload["referenceImages"] = reference_images
243
- if negative_prompt and negative_prompt.strip():
244
- payload["negativePrompt"] = negative_prompt.strip()
245
- if seed is not None:
246
- payload["seed"] = seed
247
- if person_generation:
248
- payload["personGeneration"] = person_generation
249
-
250
- result = api_request("/model/genVideo", payload)
251
-
252
- # 检查业务状态码
253
- if result.get("code") != 0:
254
- msg = result.get("msg", "未知错误")
255
- print(f"❌ API returned an error: {msg}", file=sys.stderr)
256
- sys.exit(1)
257
-
258
- # 提取任务 ID: result["data"]["taskId"]
259
- result_data = result.get("data") or {}
260
- task_id = result_data.get("taskId")
261
- if not task_id:
262
- print(f"❌ could not parse the task id; API returned: {json.dumps(result, ensure_ascii=False)}", file=sys.stderr)
263
- sys.exit(1)
264
-
265
- print(f"✅ task submitted, task id: {task_id}")
266
- return task_id
267
-
268
-
269
- def poll_video_status(task_id: str, poll_interval: int = 10, max_wait: int = 300) -> dict:
270
- """
271
- 轮询视频生成状态,返回最终结果。
272
- 网络瞬断时自动重试(最多连续 5 次),不会因单次网络抖动而中断。
273
- """
274
- print(f"\n⏳ Waiting for video generation (up to {max_wait} s)...")
275
-
276
- start_time = time.time()
277
- consecutive_errors = 0
278
- max_consecutive_errors = 5
279
-
280
- while True:
281
- elapsed = time.time() - start_time
282
- if elapsed > max_wait:
283
- print(f"\n❌ wait timed out (waited {elapsed:.0f} s)", file=sys.stderr)
284
- print(f" task id: {task_id}", file=sys.stderr)
285
- sys.exit(1)
286
-
287
- try:
288
- result = api_request("/model/getVideoStatus", {"taskId": task_id}, exit_on_error=False)
289
- except (urllib.error.URLError, OSError, ConnectionError) as e:
290
- consecutive_errors += 1
291
- print(f"\n ⚠️ polling network error ({consecutive_errors}/{max_consecutive_errors}): {e}", flush=True)
292
- if consecutive_errors >= max_consecutive_errors:
293
- print(f"❌ {max_consecutive_errors} consecutive network errors, giving up on polling", file=sys.stderr)
294
- print(f" task id: {task_id} (the job may still be running on the backend)", file=sys.stderr)
295
- sys.exit(1)
296
- time.sleep(poll_interval)
297
- continue
298
-
299
- # 请求成功,重置连续错误计数
300
- consecutive_errors = 0
301
-
302
- # 检查业务状态码
303
- if result.get("code") != 0:
304
- msg = result.get("msg", "未知错误")
305
- print(f"\n❌ status query failed: {msg}", file=sys.stderr)
306
- sys.exit(1)
307
-
308
- result_data = result.get("data") or {}
309
- status = result_data.get("status", "unknown")
310
-
311
- # 打印状态
312
- status_icons = {
313
- STATUS_PENDING: "⏳",
314
- STATUS_PROCESSING: "🔄",
315
- STATUS_SUCCEEDED: "✅",
316
- STATUS_COMPLETED: "✅",
317
- STATUS_FAILED: "❌",
318
- }
319
- icon = status_icons.get(status, "❓")
320
- print(f"\r {icon} status: {status} | elapsed: {elapsed:.0f}s", end="", flush=True)
321
-
322
- if status in (STATUS_SUCCEEDED, STATUS_COMPLETED):
323
- print() # 换行
324
- return result_data
325
-
326
- if status == STATUS_FAILED:
327
- print()
328
- error_msg = (
329
- result_data.get("errorMsg")
330
- or result_data.get("error")
331
- or result_data.get("message")
332
- or "未知错误"
333
- )
334
- print(f"❌ video generation failed: {error_msg}", file=sys.stderr)
335
- sys.exit(1)
336
-
337
- time.sleep(poll_interval)
338
-
339
-
340
- def validate_params_for_model(
341
- model: str,
342
- duration: int,
343
- ratio: str,
344
- resolution: str,
345
- ref_count: int,
346
- ) -> None:
347
- if is_seedance_model(model):
348
- if duration not in SEEDANCE_DURATIONS:
349
- print(
350
- f"❌ Seedance duration must be {SEEDANCE_DURATIONS[0]}–{SEEDANCE_DURATIONS[-1]} s, got: {duration}",
351
- file=sys.stderr,
352
- )
353
- sys.exit(1)
354
- if ratio not in SEEDANCE_RATIOS:
355
- print(f"❌ Seedance aspect ratio must be one of: {', '.join(SEEDANCE_RATIOS)}; got: {ratio}", file=sys.stderr)
356
- sys.exit(1)
357
- if resolution not in SEEDANCE_RESOLUTIONS:
358
- print(
359
- f"❌ Seedance resolution must be one of: {', '.join(SEEDANCE_RESOLUTIONS)}; got: {resolution}",
360
- file=sys.stderr,
361
- )
362
- sys.exit(1)
363
- if ref_count > 0:
364
- print(
365
- "❌ Seedance 1.5 Pro does not support a reference-image array (first / last frame only); use --first-frame / --last-frame instead",
366
- file=sys.stderr,
367
- )
368
- sys.exit(1)
369
- elif is_veo_model(model):
370
- if duration not in VEO_DURATIONS:
371
- print(f"❌ Veo duration must be one of: {', '.join(map(str, VEO_DURATIONS))} s; got: {duration}", file=sys.stderr)
372
- sys.exit(1)
373
- if ratio not in VEO_RATIOS:
374
- print(f"❌ Veo aspect ratio must be one of: {', '.join(VEO_RATIOS)}; got: {ratio}", file=sys.stderr)
375
- sys.exit(1)
376
- if resolution not in VEO_RESOLUTIONS:
377
- print(f"❌ Veo resolution must be one of: {', '.join(VEO_RESOLUTIONS)}; got: {resolution}", file=sys.stderr)
378
- sys.exit(1)
379
- if ref_count > VEO_MAX_REFERENCE_IMAGES:
380
- print(
381
- f"❌ Veo accepts at most {VEO_MAX_REFERENCE_IMAGES} reference images; got: {ref_count}",
382
- file=sys.stderr,
383
- )
384
- sys.exit(1)
385
- # Other models (e.g. future Sora): loose validation only.
386
- elif ratio not in ALL_RATIOS:
387
- print(f"❌ unsupported aspect ratio: {ratio}", file=sys.stderr)
388
- sys.exit(1)
389
- elif resolution not in ALL_RESOLUTIONS:
390
- print(f"❌ unsupported resolution: {resolution}", file=sys.stderr)
391
- sys.exit(1)
392
-
393
-
394
- def main() -> None:
395
- parser = argparse.ArgumentParser(
396
- description="AI video generator — aligned with the Lingchuang frontend AI-video params (multi-model)",
397
- formatter_class=argparse.RawDescriptionHelpFormatter,
398
- epilog=f"""
399
- {gen_video_models_help_block()}
400
-
401
- Aspect ratio / resolution depend on the model (the script validates them):
402
- Seedance: ratios {', '.join(SEEDANCE_RATIOS)}; resolutions {', '.join(SEEDANCE_RESOLUTIONS)}; duration 4-12 s
403
- Veo: ratios {', '.join(VEO_RATIOS)}; resolutions {', '.join(VEO_RESOLUTIONS)}; duration {', '.join(map(str, VEO_DURATIONS))} s
404
-
405
- Examples:
406
- # Seedance: text-to-video (default model, 5 s)
407
- python gen_video.py --prompt "Drone shot of a futuristic city at sunrise"
408
-
409
- # Seedance: first / last frame (local files are converted to base64 data URLs)
410
- python gen_video.py --first-frame ./start.png --last-frame ./end.png --prompt "smooth transition" --duration 5
411
-
412
- # Veo 3.1: reference image + 4K (model alias 'veo')
413
- python gen_video.py -m veo --prompt "product showcase" --duration 8 --resolution 4k --reference ./ref1.png
414
- """,
415
- )
416
- parser.add_argument(
417
- "--priv-token",
418
- metavar="TOKEN",
419
- default=None,
420
- help="priv-token (priority: this flag > PRIV_TOKEN env var > interactive prompt)",
421
- )
422
- parser.add_argument(
423
- "--prompt",
424
- "-p",
425
- default="",
426
- help="Video description (can be combined with first/last frame or reference image; for pure image-to-video, an empty prompt lets the model infer)",
427
- )
428
- parser.add_argument(
429
- "--model",
430
- "-m",
431
- default=VIDEO_MODEL,
432
- help=(
433
- f"Video-gen model id or alias: seedance / veo / veo-fast (default: {VIDEO_MODEL}). "
434
- "See the preset list above for full ids."
435
- ),
436
- )
437
- parser.add_argument(
438
- "--duration",
439
- "-d",
440
- type=int,
441
- default=None,
442
- metavar="SEC",
443
- help="Video duration in seconds. Default: Seedance 5, Veo 8",
444
- )
445
- parser.add_argument(
446
- "--ratio",
447
- "-r",
448
- default="16:9",
449
- choices=ALL_RATIOS,
450
- help=f"Aspect ratio (default: 16:9). Seedance also accepts 'adaptive'",
451
- )
452
- parser.add_argument(
453
- "--resolution",
454
- default="720p",
455
- choices=ALL_RESOLUTIONS,
456
- help=f"Resolution (default: 720p). Veo also accepts 4k",
457
- )
458
- parser.add_argument(
459
- "--first-frame",
460
- "--start-frame",
461
- dest="first_frame",
462
- default=None,
463
- metavar="PATH_OR_URL",
464
- help="First-frame image: local path, https URL, or data: URI (same as the frontend 'first frame')",
465
- )
466
- parser.add_argument(
467
- "--last-frame",
468
- "--end-frame",
469
- dest="last_frame",
470
- default=None,
471
- metavar="PATH_OR_URL",
472
- help="End-frame image (same as the frontend 'end frame'; when aspect ratios mismatch, backend/model rules apply)",
473
- )
474
- parser.add_argument(
475
- "--reference",
476
- action="append",
477
- default=None,
478
- metavar="PATH_OR_URL",
479
- help="Reference image; repeatable. Veo only, max 3 (same as the frontend reference image)",
480
- )
481
- parser.add_argument(
482
- "--negative-prompt",
483
- default=None,
484
- help="Negative prompt (primarily for Veo)",
485
- )
486
- parser.add_argument(
487
- "--seed",
488
- type=int,
489
- default=None,
490
- help="Random seed (optional)",
491
- )
492
- parser.add_argument(
493
- "--person-generation",
494
- choices=("allow_all", "dont_allow"),
495
- default=None,
496
- help="Person-generation policy (Veo, maps to backend personGeneration)",
497
- )
498
- parser.add_argument(
499
- "--generate-audio",
500
- action="store_true",
501
- default=False,
502
- help="Generate audio (Seedance / Veo — actual support depends on the model)",
503
- )
504
- parser.add_argument(
505
- "--camera-fixed",
506
- action="store_true",
507
- default=False,
508
- help="Fixed camera (primarily for Seedance)",
509
- )
510
- parser.add_argument(
511
- "--poll-interval",
512
- type=int,
513
- default=10,
514
- help="Status polling interval (seconds, default: 10; Veo can be slower so you may want to raise this)",
515
- )
516
- parser.add_argument(
517
- "--max-wait",
518
- type=int,
519
- default=300,
520
- help="Maximum wait time (seconds, default: 300)",
521
- )
522
-
523
- args = parser.parse_args()
524
-
525
- resolved_model = normalize_model_id(args.model)
526
-
527
- first_val = resolve_image_input(args.first_frame) if args.first_frame else ""
528
- last_val = resolve_image_input(args.last_frame) if args.last_frame else ""
529
- ref_paths = args.reference if args.reference is not None else []
530
- ref_vals = [resolve_image_input(p) for p in ref_paths if p]
531
-
532
- has_prompt = bool(args.prompt.strip())
533
- has_visual = bool(first_val or last_val or ref_vals)
534
- if not has_prompt and not has_visual:
535
- parser.error("至少需要其一:非空 --prompt,或 --first-frame / --last-frame / --reference")
536
-
537
- if args.duration is None:
538
- duration = 8 if is_veo_model(resolved_model) else 5
539
- else:
540
- duration = args.duration
541
-
542
- validate_params_for_model(
543
- resolved_model,
544
- duration,
545
- args.ratio,
546
- args.resolution,
547
- len(ref_vals),
548
- )
549
-
550
- # 初始化 token:--priv-token 参数 > PRIV_TOKEN 环境变量 > 手动输入
551
- global PRIVATE_TOKEN
552
- if args.priv_token is not None:
553
- PRIVATE_TOKEN = args.priv_token
554
- else:
555
- PRIVATE_TOKEN = resolve_token()
556
-
557
- task_id = create_video_task(
558
- model=resolved_model,
559
- prompt=args.prompt,
560
- duration=duration,
561
- ratio=args.ratio,
562
- resolution=args.resolution,
563
- first_frame_image=first_val or None,
564
- last_frame_image=last_val or None,
565
- reference_images=ref_vals or None,
566
- generate_audio=args.generate_audio,
567
- camera_fixed=args.camera_fixed,
568
- negative_prompt=args.negative_prompt,
569
- seed=args.seed,
570
- person_generation=args.person_generation,
571
- )
572
-
573
- result_data = poll_video_status(
574
- task_id=task_id,
575
- poll_interval=args.poll_interval,
576
- max_wait=args.max_wait,
577
- )
578
-
579
- print("🎉 Video generated successfully!")
580
-
581
- video_url = result_data.get("videoUrl")
582
- if video_url:
583
- print("\n🔗 Video URL (renders directly, no download needed):")
584
- print(video_url)
585
- else:
586
- print(f"⚠️ video generated but no URL was returned, task id: {task_id}")
587
- print("\n🎉 Video generation done!")
588
-
589
-
590
- if __name__ == "__main__":
591
- main()
@@ -1,7 +0,0 @@
1
- {
2
- "skillName": "template-bind",
3
- "repoName": "agent-skill-media-maker",
4
- "skillId": "475",
5
- "version": "V10",
6
- "skillDescription": "视频模板仓库与绑定技能,存储所有视频模板定义,根据指定模板 ID 生成 TemplateBinding。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 查看可用模板、列出所有模板\n- 为 DSL 绑定模板(需指定 template-id)\n\n当 Agent 拿到 Video DSL 需要生成 TemplateBinding 时,也应使用本 skill。"
7
- }