@remixmate/cli 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/argv.d.ts +15 -0
- package/dist/argv.js +51 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +83 -0
- package/dist/handlers/gen-image.d.ts +13 -0
- package/dist/handlers/gen-image.js +161 -0
- package/dist/handlers/gen-voice.d.ts +16 -0
- package/dist/handlers/gen-voice.js +110 -0
- package/dist/handlers/index.d.ts +13 -0
- package/dist/handlers/index.js +14 -0
- package/dist/handlers/template-bind.d.ts +15 -0
- package/dist/handlers/template-bind.js +149 -0
- package/dist/http.d.ts +52 -0
- package/dist/http.js +110 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +8 -0
- package/dist/manifest.json +260 -0
- package/dist/progress.d.ts +23 -0
- package/dist/progress.js +15 -0
- package/dist/registry.d.ts +33 -0
- package/dist/registry.js +72 -0
- package/dist/runner.d.ts +17 -0
- package/dist/runner.js +63 -0
- package/package.json +54 -0
- package/skills/export-jianying/SKILL.md +251 -0
- package/skills/export-jianying/scripts/gen_jianying_draft.py +1099 -0
- package/skills/export-jianying/skill.json +24 -0
- package/skills/export-jianying/version.json +7 -0
- package/skills/gen-digital-human/SKILL.md +211 -0
- package/skills/gen-digital-human/scripts/gen_digital_human_video.py +574 -0
- package/skills/gen-digital-human/skill.json +23 -0
- package/skills/gen-digital-human/version.json +7 -0
- package/skills/gen-image/SKILL.md +130 -0
- package/skills/gen-image/skill.json +22 -0
- package/skills/gen-image/version.json +7 -0
- package/skills/gen-script/SKILL.md +283 -0
- package/skills/gen-script/scripts/gen_script.py +1208 -0
- package/skills/gen-script/skill.json +41 -0
- package/skills/gen-script/version.json +7 -0
- package/skills/gen-video/SKILL.md +170 -0
- package/skills/gen-video/scripts/gen_video.py +591 -0
- package/skills/gen-video/skill.json +24 -0
- package/skills/gen-video/version.json +7 -0
- package/skills/gen-voice/SKILL.md +104 -0
- package/skills/gen-voice/skill.json +21 -0
- package/skills/gen-voice/version.json +7 -0
- package/skills/prepare-video-assets/SKILL.md +192 -0
- package/skills/prepare-video-assets/scripts/prepare_video_assets.py +57 -0
- package/skills/prepare-video-assets/skill.json +23 -0
- package/skills/prepare-video-assets/version.json +7 -0
- package/skills/render-video/SKILL.md +187 -0
- package/skills/render-video/scripts/_chrome_vendor.py +305 -0
- package/skills/render-video/scripts/_video_probe.py +174 -0
- package/skills/render-video/scripts/_vod_polling.py +86 -0
- package/skills/render-video/scripts/remote_renderer_client.py +236 -0
- package/skills/render-video/scripts/render_video.py +2288 -0
- package/skills/render-video/scripts/upload_video.py +193 -0
- package/skills/render-video/skill.json +21 -0
- package/skills/render-video/version.json +7 -0
- package/skills/template-bind/README.md +63 -0
- package/skills/template-bind/SKILL.md +234 -0
- package/skills/template-bind/scripts/check_contracts.py +196 -0
- package/skills/template-bind/scripts/match_template.py +215 -0
- package/skills/template-bind/scripts/registry_loader.py +379 -0
- package/skills/template-bind/scripts/render_job_client.py +162 -0
- package/skills/template-bind/scripts/sync_registry.py +44 -0
- package/skills/template-bind/scripts/template_paths.py +72 -0
- package/skills/template-bind/skill.json +20 -0
- package/skills/template-bind/version.json +7 -0
- package/skills/template-bind/video_dsl/README.md +97 -0
- package/skills/template-bind/video_dsl/runtime/__init__.py +11 -0
- package/skills/template-bind/video_dsl/runtime/dsl_validator.py +495 -0
- package/skills/template-bind/video_dsl/runtime/prompt_enhancer.py +274 -0
- package/skills/template-bind/video_dsl/runtime/template_binder.py +87 -0
- package/skills/template-bind/video_dsl/runtime/timeline_compiler.py +312 -0
- package/skills/template-bind/video_dsl/schema/render-plan-v1alpha1.json +193 -0
- package/skills/template-bind/video_dsl/schema/template-binding-v1alpha1.json +122 -0
- package/skills/template-bind/video_dsl/schema/template-definition-v1alpha1.json +247 -0
- package/skills/template-bind/video_dsl/schema/video-dsl-v1alpha1.json +378 -0
- package/skills/video-parser/SKILL.md +238 -0
- package/skills/video-parser/scripts/analyze_video.py +1038 -0
- package/skills/video-parser/scripts/deconstruct_video.py +602 -0
- package/skills/video-parser/scripts/parse_via_render.py +148 -0
- package/skills/video-parser/skill.json +21 -0
- package/skills/video-parser/version.json +7 -0
- package/skills/web-capture/SKILL.md +183 -0
- package/skills/web-capture/scripts/web_capture.py +461 -0
- package/skills/web-capture/skill.json +29 -0
- package/skills/web-capture/version.json +7 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
远程渲染 HTTP 客户端 — 对接独立部署的 remotion-renderer 服务。
|
|
4
|
+
|
|
5
|
+
接口(位于 /remotion-renderer/server/index.ts):
|
|
6
|
+
- POST /render → { code, data: { taskId } }
|
|
7
|
+
body 可选 coverCompositionId(如 "SpotlightCardCover");传了的话视频
|
|
8
|
+
渲染完成后服务端会再串行渲一帧静态封面、一并上传。
|
|
9
|
+
- POST /renderStatus → { code, data: { status, progress, fileUrl?,
|
|
10
|
+
coverUrl?, coverError?, error? } }
|
|
11
|
+
coverUrl 视频成功且封面成功时存在
|
|
12
|
+
coverError 视频成功但封面失败时存在(视频本身仍然 succeeded)
|
|
13
|
+
|
|
14
|
+
认证头(复用 X-Priv-Token 规范,服务端通过后端 API 校验):
|
|
15
|
+
X-Priv-Token: <PRIV_TOKEN>
|
|
16
|
+
x-invoke-skill: render-video
|
|
17
|
+
x-invoke-agent: <AGENT_NAME> # optional
|
|
18
|
+
|
|
19
|
+
Base URL 优先级:
|
|
20
|
+
1. 调用方显式传入 base_url
|
|
21
|
+
2. 环境变量 REMOTION_RENDER_API_URL
|
|
22
|
+
3. 默认 http://localhost:3000(与 server/config.ts 的 PORT 默认值一致,方便本地调试)
|
|
23
|
+
|
|
24
|
+
本模块只负责 HTTP 层;调用方(render_video.py)负责把结果写回 render_plan。
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
import os
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
import urllib.error
|
|
34
|
+
import urllib.parse
|
|
35
|
+
import urllib.request
|
|
36
|
+
from typing import Callable, Optional
|
|
37
|
+
|
|
38
|
+
DEFAULT_API_BASE_URL = "http://localhost:3000"
|
|
39
|
+
API_BASE_URL = (os.environ.get("REMOTION_RENDER_API_URL") or DEFAULT_API_BASE_URL).strip()
|
|
40
|
+
SKILL_NAME = "render-video"
|
|
41
|
+
AGENT_NAME = os.environ.get("AGENT_NAME", "")
|
|
42
|
+
|
|
43
|
+
STATUS_PENDING = "pending"
|
|
44
|
+
STATUS_PROCESSING = "processing"
|
|
45
|
+
STATUS_RUNNING = "running"
|
|
46
|
+
STATUS_SUCCEEDED = "succeeded"
|
|
47
|
+
STATUS_SUCCEED = "succeed"
|
|
48
|
+
STATUS_COMPLETED = "completed"
|
|
49
|
+
STATUS_FAILED = "failed"
|
|
50
|
+
STATUS_CANCELLED = "cancelled"
|
|
51
|
+
|
|
52
|
+
TERMINAL_SUCCESS = {STATUS_SUCCEEDED, STATUS_SUCCEED, STATUS_COMPLETED}
|
|
53
|
+
TERMINAL_FAILURE = {STATUS_FAILED, STATUS_CANCELLED}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class RemoteRenderError(RuntimeError):
|
|
57
|
+
"""抛出至调用方,由上层写入 render_plan.errors。"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_headers(private_token: str, content_type: str = "application/json", conversation_id: Optional[str] = None) -> dict:
|
|
61
|
+
if not private_token:
|
|
62
|
+
raise RemoteRenderError("PrivToken is not set (PRIV_TOKEN or --priv-token)")
|
|
63
|
+
headers = {
|
|
64
|
+
"X-Priv-Token": private_token,
|
|
65
|
+
"x-invoke-skill": SKILL_NAME,
|
|
66
|
+
}
|
|
67
|
+
if content_type:
|
|
68
|
+
headers["Content-Type"] = content_type
|
|
69
|
+
if AGENT_NAME:
|
|
70
|
+
headers["x-invoke-agent"] = AGENT_NAME
|
|
71
|
+
if conversation_id:
|
|
72
|
+
headers["x-conversation-id"] = conversation_id
|
|
73
|
+
return headers
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _request(
|
|
77
|
+
method: str,
|
|
78
|
+
path: str,
|
|
79
|
+
*,
|
|
80
|
+
private_token: str,
|
|
81
|
+
payload: Optional[dict] = None,
|
|
82
|
+
query: Optional[dict] = None,
|
|
83
|
+
base_url: Optional[str] = None,
|
|
84
|
+
timeout: float = 120.0,
|
|
85
|
+
conversation_id: Optional[str] = None,
|
|
86
|
+
) -> dict:
|
|
87
|
+
base = (base_url or API_BASE_URL).rstrip("/")
|
|
88
|
+
if not base:
|
|
89
|
+
raise RemoteRenderError("renderer service base URL is not set")
|
|
90
|
+
url = f"{base}{path}"
|
|
91
|
+
if query:
|
|
92
|
+
url = f"{url}?{urllib.parse.urlencode(query)}"
|
|
93
|
+
|
|
94
|
+
data = None
|
|
95
|
+
if payload is not None:
|
|
96
|
+
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
97
|
+
|
|
98
|
+
headers = _build_headers(private_token, conversation_id=conversation_id)
|
|
99
|
+
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
100
|
+
|
|
101
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
102
|
+
body = resp.read().decode("utf-8")
|
|
103
|
+
try:
|
|
104
|
+
return json.loads(body)
|
|
105
|
+
except json.JSONDecodeError as exc:
|
|
106
|
+
raise RemoteRenderError(f"remote response is not JSON: {body[:200]}") from exc
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _check_code(result: dict) -> dict:
|
|
110
|
+
code = result.get("code")
|
|
111
|
+
if code != 0:
|
|
112
|
+
msg = result.get("msg") or result.get("message") or "unknown error"
|
|
113
|
+
raise RemoteRenderError(f"API code={code}: {msg}")
|
|
114
|
+
return result.get("data") or {}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def start_render(
|
|
118
|
+
payload: dict,
|
|
119
|
+
*,
|
|
120
|
+
private_token: str,
|
|
121
|
+
base_url: Optional[str] = None,
|
|
122
|
+
timeout: float = 120.0,
|
|
123
|
+
conversation_id: Optional[str] = None,
|
|
124
|
+
) -> str:
|
|
125
|
+
"""POST /render;返回 taskId。"""
|
|
126
|
+
result = _request(
|
|
127
|
+
"POST",
|
|
128
|
+
"/render",
|
|
129
|
+
private_token=private_token,
|
|
130
|
+
payload=payload,
|
|
131
|
+
base_url=base_url,
|
|
132
|
+
timeout=timeout,
|
|
133
|
+
conversation_id=conversation_id,
|
|
134
|
+
)
|
|
135
|
+
data = _check_code(result)
|
|
136
|
+
task_id = data.get("taskId")
|
|
137
|
+
if not task_id:
|
|
138
|
+
raise RemoteRenderError(f"/render did not return a taskId: {result}")
|
|
139
|
+
return str(task_id)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def poll_render(
|
|
143
|
+
task_id: str,
|
|
144
|
+
*,
|
|
145
|
+
private_token: str,
|
|
146
|
+
base_url: Optional[str] = None,
|
|
147
|
+
timeout: float = 1800.0,
|
|
148
|
+
interval: float = 5.0,
|
|
149
|
+
request_timeout: float = 60.0,
|
|
150
|
+
max_consecutive_errors: int = 5,
|
|
151
|
+
on_progress: Optional[Callable[[dict], None]] = None,
|
|
152
|
+
adaptive_interval: bool = False,
|
|
153
|
+
) -> dict:
|
|
154
|
+
"""POST /renderStatus 轮询直到完成/失败/超时。
|
|
155
|
+
|
|
156
|
+
成功时返回 data dict(至少包含 fileUrl),否则抛 RemoteRenderError。
|
|
157
|
+
on_progress 回调在每次成功请求后触发,参数为完整 data dict。
|
|
158
|
+
|
|
159
|
+
adaptive_interval=True 时根据渲染进度自动调整轮询间隔:
|
|
160
|
+
0-20%: interval * 2(初始化阶段,变化慢)
|
|
161
|
+
20-80%: interval(正常渲染阶段)
|
|
162
|
+
80-100%: interval * 0.6(编码收尾阶段,更频繁检查)
|
|
163
|
+
"""
|
|
164
|
+
start = time.monotonic()
|
|
165
|
+
consecutive_errors = 0
|
|
166
|
+
current_progress = 0.0
|
|
167
|
+
|
|
168
|
+
while True:
|
|
169
|
+
elapsed = time.monotonic() - start
|
|
170
|
+
if elapsed > timeout:
|
|
171
|
+
raise RemoteRenderError(
|
|
172
|
+
f"remote render polling timed out (waited {elapsed:.0f}s, taskId={task_id}); "
|
|
173
|
+
f"the job may still be running on the backend — query /renderStatus later"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
result = _request(
|
|
178
|
+
"POST",
|
|
179
|
+
"/renderStatus",
|
|
180
|
+
private_token=private_token,
|
|
181
|
+
payload={"taskId": task_id},
|
|
182
|
+
base_url=base_url,
|
|
183
|
+
timeout=request_timeout,
|
|
184
|
+
)
|
|
185
|
+
except (urllib.error.URLError, OSError, ConnectionError) as exc:
|
|
186
|
+
consecutive_errors += 1
|
|
187
|
+
print(
|
|
188
|
+
f" ⚠️ polling network error ({consecutive_errors}/{max_consecutive_errors}): {exc}",
|
|
189
|
+
file=sys.stderr,
|
|
190
|
+
flush=True,
|
|
191
|
+
)
|
|
192
|
+
if consecutive_errors >= max_consecutive_errors:
|
|
193
|
+
raise RemoteRenderError(
|
|
194
|
+
f"{max_consecutive_errors} consecutive network errors; giving up on polling (taskId={task_id})"
|
|
195
|
+
) from exc
|
|
196
|
+
time.sleep(interval)
|
|
197
|
+
continue
|
|
198
|
+
except urllib.error.HTTPError as exc:
|
|
199
|
+
body = ""
|
|
200
|
+
try:
|
|
201
|
+
body = exc.read().decode("utf-8")
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
raise RemoteRenderError(f"HTTP {exc.code} /renderStatus: {body[:200]}") from exc
|
|
205
|
+
|
|
206
|
+
consecutive_errors = 0
|
|
207
|
+
|
|
208
|
+
data = _check_code(result)
|
|
209
|
+
if on_progress is not None:
|
|
210
|
+
try:
|
|
211
|
+
on_progress(data)
|
|
212
|
+
except Exception: # noqa: BLE001 — 进度回调异常不应中断轮询
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
status = str(data.get("status") or "").lower()
|
|
216
|
+
if status in TERMINAL_SUCCESS:
|
|
217
|
+
return data
|
|
218
|
+
if status in TERMINAL_FAILURE:
|
|
219
|
+
err = data.get("error") or data.get("errorMsg") or data.get("message") or "unknown"
|
|
220
|
+
raise RemoteRenderError(f"remote render failed ({status}): {err}")
|
|
221
|
+
|
|
222
|
+
# Adaptive interval: adjust sleep based on current progress
|
|
223
|
+
current_progress = float(data.get("progress") or 0.0)
|
|
224
|
+
if adaptive_interval:
|
|
225
|
+
if current_progress < 0.2:
|
|
226
|
+
sleep_time = interval * 2
|
|
227
|
+
elif current_progress >= 0.8:
|
|
228
|
+
sleep_time = max(interval * 0.6, 2.0)
|
|
229
|
+
else:
|
|
230
|
+
sleep_time = interval
|
|
231
|
+
else:
|
|
232
|
+
sleep_time = interval
|
|
233
|
+
time.sleep(sleep_time)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
__all__ = ["start_render", "poll_render", "RemoteRenderError"]
|