@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,574 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- 数字人口播视频脚本 - 三合一工具
4
- 支持列出形象、生成视频、查询状态。
5
-
6
- 默认行为:
7
- 生成完成后直接输出视频 URL,供调用方(如 Claude)直接展示,无需落盘。
8
-
9
- 用法:
10
- python gen_digital_human_video.py --list-avatars
11
- python gen_digital_human_video.py --avatar-id 7 --text "大家好"
12
- python gen_digital_human_video.py --avatar-id 39 --audio-url "https://..."
13
- python gen_digital_human_video.py --check-status --generation-id 593
14
-
15
- 手动配置(命令行,优先级高于环境变量):
16
- --priv-token TOKEN PrivToken
17
-
18
- 环境变量:
19
- MM_API_BASE_URL - 后端 API 地址(默认: http://localhost:3001/api)
20
- PRIV_TOKEN - PrivToken(优先读取;未配置时提示手动输入)
21
- """
22
-
23
- import argparse
24
- import json
25
- import os
26
- import sys
27
- import time
28
- import urllib.request
29
- import urllib.error
30
-
31
- API_BASE_URL = os.environ.get("MM_API_BASE_URL", "http://localhost:3001/api")
32
- PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
33
- SKILL_NAME = "gen-digital-human"
34
- AGENT_NAME = os.environ.get("AGENT_NAME", "")
35
-
36
-
37
- def resolve_token() -> str:
38
- """优先从 PRIV_TOKEN 环境变量读取 token,取不到则报错退出"""
39
- token = os.environ.get("PRIV_TOKEN", "")
40
- if not token:
41
- if sys.stdin.isatty():
42
- token = input("请输入 PrivToken: ").strip()
43
- else:
44
- print("❌ PRIV_TOKEN env var not configured; cannot authenticate", file=sys.stderr)
45
- sys.exit(1)
46
- return token
47
-
48
-
49
- VALID_ASPECT_RATIOS = ["9:16", "16:9", "3:4", "1:1"]
50
- VALID_SOURCES = ["jimeng", "hifly"]
51
-
52
- STATUS_GENERATING = "generating"
53
- STATUS_COMPLETED = "completed"
54
- STATUS_FAILED = "failed"
55
-
56
-
57
- def api_request(path: str, payload: dict) -> dict:
58
- """
59
- 通用 API 请求函数(统一 POST 方法)
60
- """
61
- if not PRIVATE_TOKEN:
62
- print("❌ PrivToken not configured: set the PRIV_TOKEN env var", file=sys.stderr)
63
- sys.exit(1)
64
-
65
- url = f"{API_BASE_URL}{path}"
66
- data = json.dumps(payload).encode("utf-8")
67
- headers = {
68
- "Content-Type": "application/json",
69
- "X-Priv-Token": PRIVATE_TOKEN,
70
- "x-invoke-skill": SKILL_NAME,
71
- }
72
- if AGENT_NAME:
73
- headers["x-invoke-agent"] = AGENT_NAME
74
-
75
- req = urllib.request.Request(url, data=data, headers=headers, method="POST")
76
-
77
- try:
78
- with urllib.request.urlopen(req, timeout=60) as resp:
79
- return json.loads(resp.read().decode("utf-8"))
80
- except urllib.error.HTTPError as e:
81
- body = e.read().decode("utf-8")
82
- print(f"❌ API request failed (HTTP {e.code}): {body}", file=sys.stderr)
83
- sys.exit(1)
84
- except urllib.error.URLError as e:
85
- print(f"❌ network error: {e.reason}", file=sys.stderr)
86
- sys.exit(1)
87
-
88
-
89
- # ---------------------------------------------------------------------------
90
- # 模式 1: 列出形象
91
- # ---------------------------------------------------------------------------
92
-
93
- def list_avatars(
94
- source: str | None = None,
95
- gender: str | None = None,
96
- mine: bool = False,
97
- name: str | None = None,
98
- ) -> None:
99
- """
100
- 查询可用数字人形象列表
101
- """
102
- payload = {"current": 1, "pageSize": 100}
103
- if source:
104
- payload["source"] = source
105
- if gender:
106
- payload["gender"] = gender
107
- if mine:
108
- payload["mine"] = True
109
- if name:
110
- payload["name"] = name
111
-
112
- print("🔍 Fetching digital-human avatar list...")
113
-
114
- result = api_request("/digital-human/avatar/page", payload)
115
-
116
- if result.get("code") != 0:
117
- msg = result.get("msg", "unknown error")
118
- print(f"❌ API returned an error: {msg}", file=sys.stderr)
119
- sys.exit(1)
120
-
121
- avatar_list = (result.get("data") or {}).get("list", [])
122
-
123
- if not avatar_list:
124
- print("⚠️ No avatars available")
125
- return
126
-
127
- # Tabular output
128
- print(f"\n{'ID':<8} {'Name':<16} {'Source':<10} {'Gender':<8} {'Ratio'}")
129
- print(f"{'-' * 8} {'-' * 16} {'-' * 10} {'-' * 8} {'-' * 10}")
130
- for avatar in avatar_list:
131
- avatar_id = avatar.get("id", "")
132
- avatar_name = avatar.get("name", "")
133
- avatar_source = avatar.get("source", "")
134
- avatar_gender = avatar.get("gender", "")
135
- aspect_ratio = avatar.get("aspectRatio", "")
136
- print(f"{avatar_id:<8} {avatar_name:<16} {avatar_source:<10} {avatar_gender:<8} {aspect_ratio}")
137
-
138
- print(f"\n{len(avatar_list)} avatar(s) available")
139
- print("💡 Use --avatar-id <ID> to pick an avatar for generation")
140
-
141
-
142
- # ---------------------------------------------------------------------------
143
- # 模式 2: 生成视频(默认模式)
144
- # ---------------------------------------------------------------------------
145
-
146
- def fetch_avatar_info(avatar_id: int) -> dict:
147
- """
148
- 获取指定 avatar 的详细信息(默认 source / aspectRatio 等)。
149
- 先查普通分页,未命中再查 mine: true(与列表接口行为一致)。
150
- """
151
- payloads = [
152
- {"current": 1, "pageSize": 100},
153
- {"current": 1, "pageSize": 100, "mine": True},
154
- ]
155
- for payload in payloads:
156
- result = api_request("/digital-human/avatar/page", payload)
157
-
158
- if result.get("code") != 0:
159
- msg = result.get("msg", "unknown error")
160
- print(f"❌ avatar lookup failed: {msg}", file=sys.stderr)
161
- sys.exit(1)
162
-
163
- avatar_list = (result.get("data") or {}).get("list", [])
164
- for avatar in avatar_list:
165
- if str(avatar.get("id")) == str(avatar_id):
166
- return avatar
167
-
168
- print(f"❌ avatar_id={avatar_id} not found; use --list-avatars [--mine] to see what is available",
169
- file=sys.stderr)
170
- sys.exit(1)
171
-
172
-
173
- def _resolve_source(explicit_source: str | None, avatar_info: dict) -> str:
174
- """
175
- 接口要求 source 为 jimeng / hifly;优先使用命令行,其次形象上的来源。
176
- """
177
- src = (explicit_source or "").strip() or (avatar_info.get("source") or "").strip()
178
- if src not in VALID_SOURCES:
179
- print(
180
- "❌ 无法确定有效的 source(jimeng/hifly)。请用 --source 指定,或确认该形象带有来源字段。",
181
- file=sys.stderr,
182
- )
183
- sys.exit(1)
184
- return src
185
-
186
-
187
- def build_generate_payload(
188
- avatar_id: int,
189
- avatar_info: dict,
190
- source: str | None,
191
- text: str | None,
192
- voice_id: str | None,
193
- voice_name: str | None,
194
- aspect_ratio: str | None,
195
- prompt: str | None,
196
- audio_url: str | None,
197
- ) -> dict:
198
- """
199
- 组装 /digital-human/video/generate 请求体(与后端 DigitalHumanVideoGenerateDTO 一致)。
200
- 有 audioUrl 时为音频驱动,可不传 text / voiceId / voiceName。
201
- """
202
- resolved_source = _resolve_source(source, avatar_info)
203
- payload = {
204
- "avatarId": int(avatar_id),
205
- "source": resolved_source,
206
- }
207
-
208
- audio_url_stripped = (audio_url or "").strip()
209
- if audio_url_stripped:
210
- payload["audioUrl"] = audio_url_stripped
211
- # 音频驱动下 text / voice 均可选;若仍传入 text 会一并提交供记录
212
- if text and str(text).strip():
213
- payload["text"] = str(text).strip()
214
- else:
215
- payload["text"] = text
216
- payload["voiceId"] = voice_id
217
- if voice_name:
218
- payload["voiceName"] = voice_name
219
-
220
- if aspect_ratio:
221
- payload["aspectRatio"] = aspect_ratio
222
- elif avatar_info.get("aspectRatio"):
223
- payload["aspectRatio"] = avatar_info["aspectRatio"]
224
-
225
- if prompt:
226
- payload["prompt"] = prompt
227
-
228
- return payload
229
-
230
-
231
- def submit_video_task(
232
- avatar_id: int,
233
- source: str | None,
234
- text: str | None,
235
- voice_id: str | None,
236
- voice_name: str | None,
237
- aspect_ratio: str | None,
238
- prompt: str | None,
239
- avatar_info: dict,
240
- audio_url: str | None,
241
- ) -> int:
242
- """
243
- 提交数字人视频生成任务,返回 generationId
244
- """
245
- payload = build_generate_payload(
246
- avatar_id=avatar_id,
247
- avatar_info=avatar_info,
248
- source=source,
249
- text=text,
250
- voice_id=voice_id,
251
- voice_name=voice_name,
252
- aspect_ratio=aspect_ratio,
253
- prompt=prompt,
254
- audio_url=audio_url,
255
- )
256
-
257
- print("🎬 Submitting digital-human video generation task...")
258
- print(f" avatar: {avatar_info.get('name', '')} (ID: {avatar_id})")
259
- print(f" source: {payload['source']}")
260
- if payload.get("audioUrl"):
261
- preview = payload["audioUrl"]
262
- if len(preview) > 60:
263
- preview = preview[:57] + "..."
264
- print(f" mode: audio-driven | audioUrl: {preview}")
265
- if payload.get("text"):
266
- tx = payload["text"]
267
- print(f" text (record-only): {tx[:50]}{'...' if len(tx) > 50 else ''}")
268
- else:
269
- t = text or ""
270
- print(f" mode: TTS | text: {t[:50]}{'...' if len(t) > 50 else ''}")
271
- print(f" voice: {voice_id}")
272
- ar = payload.get("aspectRatio")
273
- if ar:
274
- print(f" aspect ratio: {ar}")
275
- if prompt:
276
- print(f" action prompt: {prompt}")
277
-
278
- result = api_request("/digital-human/video/generate", payload)
279
-
280
- if result.get("code") != 0:
281
- msg = result.get("msg", "unknown error")
282
- print(f"❌ API returned an error: {msg}", file=sys.stderr)
283
- sys.exit(1)
284
-
285
- result_data = result.get("data") or {}
286
- generation_id = result_data.get("generationId")
287
- if not generation_id:
288
- print(f"❌ could not parse generationId; API returned: {json.dumps(result, ensure_ascii=False)}",
289
- file=sys.stderr)
290
- sys.exit(1)
291
-
292
- print(f"✅ task submitted, generationId: {generation_id}")
293
- return generation_id
294
-
295
-
296
- def poll_video_status(
297
- generation_id: int,
298
- poll_interval: int = 5,
299
- max_wait: int = 600,
300
- ) -> dict:
301
- """
302
- 轮询数字人视频生成状态,返回最终结果
303
- """
304
- print(f"\n⏳ Waiting for video generation (up to {max_wait} s)...")
305
-
306
- start_time = time.time()
307
-
308
- while True:
309
- elapsed = time.time() - start_time
310
- if elapsed > max_wait:
311
- print(f"\n❌ wait timed out (waited {elapsed:.0f} s)", file=sys.stderr)
312
- print(f" generationId: {generation_id}", file=sys.stderr)
313
- print(f" You can run --check-status --generation-id {generation_id} later",
314
- file=sys.stderr)
315
- sys.exit(1)
316
-
317
- result = api_request("/digital-human/video/status", {"generationId": generation_id})
318
-
319
- if result.get("code") != 0:
320
- msg = result.get("msg", "unknown error")
321
- print(f"\n❌ status query failed: {msg}", file=sys.stderr)
322
- sys.exit(1)
323
-
324
- result_data = result.get("data") or {}
325
- status = result_data.get("status", "unknown")
326
-
327
- status_icons = {
328
- STATUS_GENERATING: "🔄",
329
- STATUS_COMPLETED: "✅",
330
- STATUS_FAILED: "❌",
331
- }
332
- icon = status_icons.get(status, "⏳")
333
- print(f"\r {icon} status: {status} | elapsed: {elapsed:.0f}s", end="", flush=True)
334
-
335
- if status == STATUS_COMPLETED:
336
- print() # 换行
337
- return result_data
338
-
339
- if status == STATUS_FAILED:
340
- print()
341
- error_msg = (
342
- result_data.get("errorMessage")
343
- or result_data.get("error")
344
- or result_data.get("message")
345
- or "unknown error"
346
- )
347
- print(f"❌ video generation failed: {error_msg}", file=sys.stderr)
348
- sys.exit(1)
349
-
350
- time.sleep(poll_interval)
351
-
352
-
353
- def generate_video(args: argparse.Namespace) -> None:
354
- """
355
- 生成数字人视频的完整流程
356
- """
357
- # Step 1: fetch avatar info
358
- print("🔍 Fetching avatar info...")
359
- avatar_info = fetch_avatar_info(args.avatar_id)
360
- print(f" found avatar: {avatar_info.get('name', '')} (source: {avatar_info.get('source', '')})")
361
-
362
- # Step 2: 提交视频生成任务
363
- generation_id = submit_video_task(
364
- avatar_id=args.avatar_id,
365
- source=args.source,
366
- text=args.text,
367
- voice_id=args.voice_id,
368
- voice_name=args.voice_name,
369
- aspect_ratio=args.aspect_ratio,
370
- prompt=args.prompt,
371
- avatar_info=avatar_info,
372
- audio_url=args.audio_url,
373
- )
374
-
375
- # Step 3: 轮询等待完成
376
- result_data = poll_video_status(
377
- generation_id=generation_id,
378
- poll_interval=args.poll_interval,
379
- max_wait=args.max_wait,
380
- )
381
-
382
- print("🎉 Video generated successfully!")
383
-
384
- # Step 4: emit URL
385
- video_url = result_data.get("videoUrl")
386
- if video_url:
387
- print("\n🔗 Video URL (renders directly, no download needed):")
388
- print(video_url)
389
- else:
390
- print("⚠️ video generated but no URL was returned")
391
- print(f" generationId: {generation_id}")
392
- print(f" you can run --check-status --generation-id {generation_id} later")
393
- print("\n🎉 Digital-human video generation done!")
394
-
395
-
396
- # ---------------------------------------------------------------------------
397
- # 模式 3: 查询状态
398
- # ---------------------------------------------------------------------------
399
-
400
- def check_status(generation_id: int) -> None:
401
- """
402
- 查询数字人视频生成任务状态
403
- """
404
- print(f"🔍 Querying task status (generationId: {generation_id})...")
405
-
406
- result = api_request("/digital-human/video/status", {"generationId": generation_id})
407
-
408
- if result.get("code") != 0:
409
- msg = result.get("msg", "unknown error")
410
- print(f"❌ API returned an error: {msg}", file=sys.stderr)
411
- sys.exit(1)
412
-
413
- result_data = result.get("data") or {}
414
- status = result_data.get("status", "unknown")
415
- video_url = result_data.get("videoUrl", "")
416
-
417
- status_icons = {
418
- STATUS_GENERATING: "🔄",
419
- STATUS_COMPLETED: "✅",
420
- STATUS_FAILED: "❌",
421
- }
422
- icon = status_icons.get(status, "❓")
423
- print(f"\n{icon} task status: {status}")
424
-
425
- if video_url:
426
- print(f" video URL: {video_url}")
427
-
428
- if status == STATUS_FAILED:
429
- error_msg = (
430
- result_data.get("errorMessage")
431
- or result_data.get("error")
432
- or result_data.get("message")
433
- or "unknown error"
434
- )
435
- print(f" failure reason: {error_msg}")
436
-
437
-
438
- # ---------------------------------------------------------------------------
439
- # main
440
- # ---------------------------------------------------------------------------
441
-
442
- def main() -> None:
443
- parser = argparse.ArgumentParser(
444
- description="Digital-human talking-head video tool — list avatars / generate video / check status",
445
- formatter_class=argparse.RawDescriptionHelpFormatter,
446
- epilog="""
447
- Examples:
448
- # List all avatars
449
- python gen_digital_human_video.py --list-avatars
450
- python gen_digital_human_video.py --list-avatars --source hifly --gender female
451
-
452
- # Default mode: only the URL is printed so it can be displayed inline (recommended)
453
- python gen_digital_human_video.py --avatar-id 7 --text "Hello and welcome to the live stream"
454
- python gen_digital_human_video.py --avatar-id 7 -t "Hi there" --voice-id "female-shaonv" --aspect-ratio 9:16
455
-
456
- # Audio-driven (text / voice-id / voice-name optional)
457
- python gen_digital_human_video.py --avatar-id 39 --source hifly --audio-url "https://example.com/a.mp3" --aspect-ratio 16:9
458
-
459
- # Check task status
460
- python gen_digital_human_video.py --check-status --generation-id 593
461
- """,
462
- )
463
-
464
- parser.add_argument(
465
- "--priv-token",
466
- metavar="TOKEN",
467
- default=None,
468
- help="PrivToken (priority: this flag > PRIV_TOKEN env var > interactive prompt)",
469
- )
470
-
471
- # Mode switches
472
- parser.add_argument(
473
- "--list-avatars",
474
- action="store_true",
475
- help="List available digital-human avatars and exit",
476
- )
477
- parser.add_argument(
478
- "--check-status",
479
- action="store_true",
480
- help="Check the status of a digital-human video generation task",
481
- )
482
-
483
- # Listing filters / generation-time source override (defaults to the avatar's own source)
484
- parser.add_argument("--source", choices=["jimeng", "hifly"],
485
- help="jimeng / hifly; filter in list mode, override the avatar's default source in generate mode")
486
- parser.add_argument("--gender", choices=["male", "female"],
487
- help="Filter avatars by gender: male / female")
488
- parser.add_argument("--mine", action="store_true",
489
- help="Only list user-custom avatars")
490
- parser.add_argument("--name", help="Fuzzy-search avatars by name")
491
-
492
- # Generation parameters
493
- parser.add_argument("--avatar-id", type=int, help="Digital-human avatar id (required in generate mode)")
494
- parser.add_argument("--text", "-t", help="Narration text (required in TTS mode; optional with --audio-url)")
495
- parser.add_argument(
496
- "--audio-url",
497
- help="Pre-synthesized audio URL (audio-driven; with this set, --text / --voice-id / --voice-name are optional)",
498
- )
499
- parser.add_argument(
500
- "--voice-id",
501
- default=None,
502
- help="Voice id (required for TTS; default male-qn-qingse; optional with --audio-url)",
503
- )
504
- parser.add_argument("--voice-name", default="", help="Voice display name (optional for TTS)")
505
- parser.add_argument("--aspect-ratio", choices=VALID_ASPECT_RATIOS,
506
- help=f"Aspect ratio: {', '.join(VALID_ASPECT_RATIOS)}")
507
- parser.add_argument("--prompt", help="Action prompt (e.g. 'more hand gestures')")
508
- parser.add_argument("--poll-interval", type=int, default=5,
509
- help="Status polling interval (seconds, default: 5)")
510
- parser.add_argument("--max-wait", type=int, default=600,
511
- help="Maximum wait time (seconds, default: 600)")
512
-
513
- # Status-query parameters
514
- parser.add_argument("--generation-id", type=int, help="Generation task id (required in check-status mode)")
515
-
516
- args = parser.parse_args()
517
-
518
- # 初始化 token:--priv-token 参数 > PRIV_TOKEN 环境变量 > 手动输入
519
- global PRIVATE_TOKEN
520
- if args.priv_token is not None:
521
- PRIVATE_TOKEN = args.priv_token
522
- else:
523
- PRIVATE_TOKEN = resolve_token()
524
-
525
- # 模式 1: 列出形象
526
- if args.list_avatars:
527
- list_avatars(
528
- source=args.source,
529
- gender=args.gender,
530
- mine=args.mine,
531
- name=args.name,
532
- )
533
- return
534
-
535
- # 模式 3: 查询状态
536
- if args.check_status:
537
- if not args.generation_id:
538
- print("❌ check-status mode requires --generation-id", file=sys.stderr)
539
- parser.print_help()
540
- sys.exit(1)
541
- check_status(generation_id=args.generation_id)
542
- return
543
-
544
- # 模式 2: 生成视频(默认)
545
- if not args.avatar_id:
546
- print("❌ provide --avatar-id to pick the avatar", file=sys.stderr)
547
- print(" use --list-avatars to see available avatars", file=sys.stderr)
548
- parser.print_help()
549
- sys.exit(1)
550
-
551
- audio_url_stripped = (args.audio_url or "").strip()
552
- if audio_url_stripped:
553
- args.audio_url = audio_url_stripped
554
- else:
555
- args.audio_url = ""
556
-
557
- if not args.audio_url:
558
- if not args.text:
559
- print("❌ in TTS mode provide --text, or use --audio-url for audio-driven mode", file=sys.stderr)
560
- parser.print_help()
561
- sys.exit(1)
562
- if not args.text.strip():
563
- print("❌ narration text cannot be empty", file=sys.stderr)
564
- sys.exit(1)
565
- if not args.voice_id:
566
- args.voice_id = "male-qn-qingse"
567
- else:
568
- args.text = args.text or ""
569
-
570
- generate_video(args)
571
-
572
-
573
- if __name__ == "__main__":
574
- main()