@remixmate/cli 0.9.6 → 0.9.8
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/README.md +23 -0
- package/dist/capabilities.d.ts +56 -0
- package/dist/capabilities.js +75 -0
- package/dist/handlers/gen-digital-human.js +7 -7
- package/dist/handlers/gen-image.d.ts +12 -6
- package/dist/handlers/gen-image.js +35 -47
- package/dist/handlers/gen-video.d.ts +7 -8
- package/dist/handlers/gen-video.js +41 -65
- package/dist/handlers/gen-voice.d.ts +1 -1
- package/dist/handlers/gen-voice.js +9 -7
- package/dist/http.d.ts +2 -2
- package/dist/http.js +3 -3
- package/dist/manifest.json +2 -2
- package/package.json +7 -1
- package/skills/export-jianying/SKILL.md +1 -1
- package/skills/export-jianying/scripts/gen_jianying_draft.py +2 -2
- package/skills/gen-digital-human/SKILL.md +1 -1
- package/skills/gen-image/SKILL.md +1 -1
- package/skills/gen-video/SKILL.md +1 -1
- package/skills/gen-voice/SKILL.md +3 -3
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/render-video/SKILL.md +4 -4
- package/skills/render-video/scripts/_video_probe.py +4 -1
- package/skills/render-video/scripts/_vod_polling.py +1 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -8
- package/skills/render-video/scripts/render_video.py +10 -4
- package/skills/render-video/scripts/upload_video.py +3 -3
- package/skills/template-registry/README.md +1 -1
- package/skills/template-registry/SKILL.md +3 -3
- package/skills/template-registry/scripts/check_contracts.py +8 -25
- package/skills/template-registry/scripts/match_template.py +100 -28
- package/skills/template-registry/scripts/registry_loader.py +3 -3
- package/skills/template-registry/scripts/render_job_client.py +2 -2
- package/skills/template-registry/scripts/verify_props_contract.py +273 -0
- package/skills/template-registry/video_dsl/README.md +0 -1
- package/skills/video-parser/SKILL.md +1 -1
- package/skills/video-parser/scripts/deconstruct_video.py +2 -2
- package/skills/video-parser/scripts/parse_via_render.py +2 -2
- package/skills/template-registry/video_dsl/schema/template-definition-v1alpha1.json +0 -247
|
@@ -37,7 +37,7 @@ There is no dedicated env file; the **process environment** variables of the cal
|
|
|
37
37
|
| Env var | Description | Default |
|
|
38
38
|
|---------|-------------|---------|
|
|
39
39
|
| `PRIV_TOKEN` | Tianyan token; overridable with `--priv-token`. | (none) |
|
|
40
|
-
| `MM_API_BASE_URL` | API root URL. | `
|
|
40
|
+
| `MM_API_BASE_URL` | API root URL. | `https://api-agent.remixmate.com/api` |
|
|
41
41
|
| `AGENT_NAME` | Optional, sent as the `x-invoke-agent` request header. | (none) |
|
|
42
42
|
|
|
43
43
|
## Steps
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
python gen_jianying_draft.py --title "测试" --scenes scenes.json --download -o draft.zip
|
|
14
14
|
|
|
15
15
|
环境变量:
|
|
16
|
-
MM_API_BASE_URL - 后端 API 地址(默认:
|
|
16
|
+
MM_API_BASE_URL - 后端 API 地址(默认: https://api-agent.remixmate.com/api)
|
|
17
17
|
PRIV_TOKEN - PrivToken(优先读取;未配置时提示手动输入)
|
|
18
18
|
"""
|
|
19
19
|
|
|
@@ -30,7 +30,7 @@ import urllib.request
|
|
|
30
30
|
import uuid
|
|
31
31
|
import zipfile
|
|
32
32
|
|
|
33
|
-
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "
|
|
33
|
+
API_BASE_URL = os.environ.get("MM_API_BASE_URL", "https://api-agent.remixmate.com/api")
|
|
34
34
|
PRIVATE_TOKEN = "" # 在 main() 中通过 resolve_token() 初始化
|
|
35
35
|
SKILL_NAME = "export-jianying"
|
|
36
36
|
AGENT_NAME = os.environ.get("AGENT_NAME", "")
|
|
@@ -44,7 +44,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
44
44
|
| Env var | Description | Default |
|
|
45
45
|
|---------|-------------|---------|
|
|
46
46
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
47
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `
|
|
47
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.com/api` |
|
|
48
48
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
49
49
|
|
|
50
50
|
## Operations
|
|
@@ -46,7 +46,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
46
46
|
|---------|-------------|---------|
|
|
47
47
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
48
48
|
| `MM_IMAGE_MODEL` | Default model id | `doubao/doubao-seedream-4-5-251128` |
|
|
49
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `
|
|
49
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.com/api` |
|
|
50
50
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
51
51
|
|
|
52
52
|
## Operations
|
|
@@ -60,7 +60,7 @@ No skill-local env file — the executing process inherits the system environmen
|
|
|
60
60
|
|---------|-------------|---------|
|
|
61
61
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
62
62
|
| `MM_VIDEO_MODEL` | Default model id | `doubao-seedance-1-5-pro-251215` |
|
|
63
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `
|
|
63
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.com/api` |
|
|
64
64
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
65
65
|
|
|
66
66
|
## Operations
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gen-voice
|
|
3
3
|
description: |
|
|
4
|
-
Text-to-speech (TTS) skill: synthesize narration audio from text via ab-api's `/tool/
|
|
4
|
+
Text-to-speech (TTS) skill: synthesize narration audio from text via ab-api's unified `/tool/tts` endpoint with `provider: "minimax"` (Minimax TTS).
|
|
5
5
|
|
|
6
6
|
Use this skill immediately whenever the user asks for any of:
|
|
7
7
|
- AI voice-over, TTS, text-to-speech, generate narration audio
|
|
@@ -17,7 +17,7 @@ triggers:
|
|
|
17
17
|
|
|
18
18
|
# Text-to-Speech (TTS) Skill
|
|
19
19
|
|
|
20
|
-
Wraps ab-api's `POST /tool/
|
|
20
|
+
Wraps ab-api's unified `POST /tool/tts` with `provider: "minimax"` (the same endpoint the web studio uses), authenticated with the **Tianyan privateToken**, backed by the **Minimax TTS** model.
|
|
21
21
|
|
|
22
22
|
## Auth & environment
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ There is no skill-local env file — the executing process inherits the system e
|
|
|
29
29
|
| Env var | Description | Default |
|
|
30
30
|
|---------|-------------|---------|
|
|
31
31
|
| `PRIV_TOKEN` | Tianyan token; `--priv-token` overrides | none |
|
|
32
|
-
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `
|
|
32
|
+
| `MM_API_BASE_URL` | API root; `--api-base-url` overrides | `https://api.remixmate.com/api` |
|
|
33
33
|
| `AGENT_NAME` | Optional `x-invoke-agent` header | none |
|
|
34
34
|
|
|
35
35
|
## Operations
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "338",
|
|
5
5
|
"version": "V7",
|
|
6
|
-
"skillDescription": "语音合成(TTS)技能,将文字转为语音音频(调用 ab-api /tool/
|
|
6
|
+
"skillDescription": "语音合成(TTS)技能,将文字转为语音音频(调用 ab-api 统一 /tool/tts,provider=minimax,基于 Minimax TTS)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- AI 配音、语音合成、文字转语音、TTS、文本转音频、生成语音\n- 使用 minimax 语音合成\n- 用户想要将一段文字朗读出来、生成音频\n\n即使用户没有明确说「使用 AI」,只要他们想要将文字转为语音,也要使用本 skill。"
|
|
7
7
|
}
|
|
@@ -54,8 +54,8 @@ The asset resolver invokes the following skills based on `AssetRef` declarations
|
|
|
54
54
|
| Env var | Description | Default |
|
|
55
55
|
|---------|-------------|---------|
|
|
56
56
|
| `PRIV_TOKEN` | Tianyan token (needed for asset generation + DB persist). | (none) |
|
|
57
|
-
| `MM_API_BASE_URL` | Asset-generation API root (ab-agent proxy). | `
|
|
58
|
-
| `MM_BACKEND_API_URL` | Backend API root (RenderPlan persistence). | `
|
|
57
|
+
| `MM_API_BASE_URL` | Asset-generation API root (ab-agent proxy). | `https://api-agent.remixmate.com/api` |
|
|
58
|
+
| `MM_BACKEND_API_URL` | Backend API root (RenderPlan persistence). | `https://api.remixmate.com/api` |
|
|
59
59
|
| `ASSET_CACHE_DIR` | Asset cache directory. | `./.asset-cache/` |
|
|
60
60
|
|
|
61
61
|
## Canonical usage (inline DSL JSON, no temp files)
|
|
@@ -49,8 +49,8 @@ Rendering supports two modes:
|
|
|
49
49
|
| Env var | Description | Default |
|
|
50
50
|
|---------|-------------|---------|
|
|
51
51
|
| `PRIV_TOKEN` | Tianyan token (needed to load the RenderPlan from the DB and to upload). | (none) |
|
|
52
|
-
| `MM_BACKEND_API_URL` | Backend API root URL (RenderPlan / VOD lookups). | `
|
|
53
|
-
| `REMOTION_RENDER_API_URL` | Remote renderer base URL (ab-render). | `
|
|
52
|
+
| `MM_BACKEND_API_URL` | Backend API root URL (RenderPlan / VOD lookups). | `https://api.remixmate.com/api` |
|
|
53
|
+
| `REMOTION_RENDER_API_URL` | Remote renderer base URL (ab-render). | `https://api-render.remixmate.com` |
|
|
54
54
|
| `REMOTION_RENDER_MODE` | Default render mode (`remote` / `local`); CLI `--renderer` overrides it. | `remote` |
|
|
55
55
|
| `REMOTION_OUTPUT_DIR` | Render output directory. | `./output/` |
|
|
56
56
|
| `REMOTION_CONCURRENCY` | Local Remotion render concurrency. | `2` |
|
|
@@ -194,8 +194,8 @@ If the remote service is unreachable or auth fails, the script exits with a deta
|
|
|
194
194
|
The Remotion render project lives at the monorepo root under `<monorepo-root>/remotion-renderer/` and is deployed as a **standalone HTTP service**. The client script in this skill only sends POST requests and does not participate in server-side code sync.
|
|
195
195
|
|
|
196
196
|
- Server code + deployment docs: `<monorepo-root>/remotion-renderer/README.md`.
|
|
197
|
-
-
|
|
198
|
-
-
|
|
197
|
+
- Production (default): the client uses `https://api-render.remixmate.com` when `REMOTION_RENDER_API_URL` is unset — no config needed.
|
|
198
|
+
- Local debugging: in the renderer directory run `npm install && npm run server:dev`, then point the client at it with `REMOTION_RENDER_API_URL=http://localhost:3000`.
|
|
199
199
|
|
|
200
200
|
## Error handling
|
|
201
201
|
|
|
@@ -59,7 +59,10 @@ def _query_duration_from_api(url: str) -> Optional[float]:
|
|
|
59
59
|
if not media_id:
|
|
60
60
|
return None
|
|
61
61
|
|
|
62
|
-
api_base = os.environ.get(
|
|
62
|
+
api_base = os.environ.get(
|
|
63
|
+
"MM_API_BASE_URL",
|
|
64
|
+
os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.com/api"),
|
|
65
|
+
).rstrip("/")
|
|
63
66
|
if not api_base:
|
|
64
67
|
return None
|
|
65
68
|
|
|
@@ -40,7 +40,7 @@ def poll_vod_playback_url(
|
|
|
40
40
|
- 看到阿里云 OSS 直链时记为 fallback,继续等 CDN
|
|
41
41
|
- 超时仍未拿到 CDN 时退回到 OSS 原始地址
|
|
42
42
|
"""
|
|
43
|
-
api_base = os.environ.get("MM_BACKEND_API_URL", "
|
|
43
|
+
api_base = os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.com/api").rstrip("/")
|
|
44
44
|
url = f"{api_base}/file/get"
|
|
45
45
|
headers = {
|
|
46
46
|
"Content-Type": "application/json",
|
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
|
|
19
19
|
Base URL 优先级:
|
|
20
20
|
1. 调用方显式传入 base_url
|
|
21
|
-
2. 环境变量 REMOTION_RENDER_API_URL
|
|
22
|
-
3. 默认
|
|
21
|
+
2. 环境变量 REMOTION_RENDER_API_URL(指向本地/staging 时在此覆盖)
|
|
22
|
+
3. 默认 https://api-render.remixmate.com(生产,零配置可用)
|
|
23
23
|
|
|
24
24
|
本模块只负责 HTTP 层;调用方(render_video.py)负责把结果写回 render_plan。
|
|
25
25
|
"""
|
|
@@ -35,7 +35,7 @@ import urllib.parse
|
|
|
35
35
|
import urllib.request
|
|
36
36
|
from typing import Callable, Optional
|
|
37
37
|
|
|
38
|
-
DEFAULT_API_BASE_URL = "
|
|
38
|
+
DEFAULT_API_BASE_URL = "https://api-render.remixmate.com"
|
|
39
39
|
API_BASE_URL = (os.environ.get("REMOTION_RENDER_API_URL") or DEFAULT_API_BASE_URL).strip()
|
|
40
40
|
SKILL_NAME = "render-video"
|
|
41
41
|
AGENT_NAME = os.environ.get("AGENT_NAME", "")
|
|
@@ -146,7 +146,7 @@ def poll_render(
|
|
|
146
146
|
private_token: str,
|
|
147
147
|
base_url: Optional[str] = None,
|
|
148
148
|
timeout: float = 1800.0,
|
|
149
|
-
interval: float =
|
|
149
|
+
interval: float = 2.0,
|
|
150
150
|
request_timeout: float = 60.0,
|
|
151
151
|
max_consecutive_errors: int = 5,
|
|
152
152
|
on_progress: Optional[Callable[[dict], None]] = None,
|
|
@@ -159,9 +159,9 @@ def poll_render(
|
|
|
159
159
|
on_progress 回调在每次成功请求后触发,参数为完整 data dict。
|
|
160
160
|
|
|
161
161
|
adaptive_interval=True 时根据渲染进度自动调整轮询间隔:
|
|
162
|
-
0-20%: interval *
|
|
162
|
+
0-20%: max(interval * 0.5, 1.0)(起步阶段采样加密,短渲染也能看到进度)
|
|
163
163
|
20-80%: interval(正常渲染阶段)
|
|
164
|
-
80-100%: interval * 0.6(编码收尾阶段,更频繁检查)
|
|
164
|
+
80-100%: max(interval * 0.6, 1.0)(编码收尾阶段,更频繁检查)
|
|
165
165
|
"""
|
|
166
166
|
start = time.monotonic()
|
|
167
167
|
consecutive_errors = 0
|
|
@@ -225,9 +225,10 @@ def poll_render(
|
|
|
225
225
|
current_progress = float(data.get("progress") or 0.0)
|
|
226
226
|
if adaptive_interval:
|
|
227
227
|
if current_progress < 0.2:
|
|
228
|
-
|
|
228
|
+
# 起步阶段(含服务端很快就渲完的短片)采样加密,避免 0%→完成的跳变
|
|
229
|
+
sleep_time = max(interval * 0.5, 1.0)
|
|
229
230
|
elif current_progress >= 0.8:
|
|
230
|
-
sleep_time = max(interval * 0.6,
|
|
231
|
+
sleep_time = max(interval * 0.6, 1.0)
|
|
231
232
|
else:
|
|
232
233
|
sleep_time = interval
|
|
233
234
|
else:
|
|
@@ -1646,6 +1646,11 @@ def render_with_remote_api(
|
|
|
1646
1646
|
"upload": True,
|
|
1647
1647
|
"uploadTitle": effective_title,
|
|
1648
1648
|
}
|
|
1649
|
+
# 私有模板封面:传 coverCompositionId 让 /renderDraft 渲模板自带 cover 组件
|
|
1650
|
+
# (ab-render 注册了 DraftCover),与内置 /render 的 MainCover 对等;
|
|
1651
|
+
# 不传则 ab-render 退回截首帧 / 不渲。
|
|
1652
|
+
if cover_composition_id:
|
|
1653
|
+
payload["coverCompositionId"] = cover_composition_id
|
|
1649
1654
|
render_path = "/renderDraft"
|
|
1650
1655
|
status_path = "/renderDraftStatus"
|
|
1651
1656
|
else:
|
|
@@ -1666,7 +1671,7 @@ def render_with_remote_api(
|
|
|
1666
1671
|
LogPrint(f" Private template (OSS dynamic bundle): {template_id}", file=sys.stderr)
|
|
1667
1672
|
else:
|
|
1668
1673
|
LogPrint(f" Composition: {composition_id}", file=sys.stderr)
|
|
1669
|
-
if cover_composition_id
|
|
1674
|
+
if cover_composition_id:
|
|
1670
1675
|
LogPrint(f" Cover: {cover_composition_id}", file=sys.stderr)
|
|
1671
1676
|
LogPrint(f" size: {config.get('width')}x{config.get('height')}", file=sys.stderr)
|
|
1672
1677
|
LogPrint(f" total frames: {total_frames}", file=sys.stderr)
|
|
@@ -1698,7 +1703,8 @@ def render_with_remote_api(
|
|
|
1698
1703
|
def _on_progress(status_data: dict):
|
|
1699
1704
|
nonlocal last_progress
|
|
1700
1705
|
progress = float(status_data.get("progress") or 0.0)
|
|
1701
|
-
|
|
1706
|
+
# 门槛设小(2%):短渲染服务端进度增量小,太大的门槛会把中间档全吞掉,只剩 0%→100%
|
|
1707
|
+
if progress - last_progress >= 0.02 or progress >= 1.0:
|
|
1702
1708
|
pct = int(progress * 100)
|
|
1703
1709
|
elapsed = time.time() - render_start_time
|
|
1704
1710
|
eta_remaining = 0.0
|
|
@@ -1933,8 +1939,8 @@ Examples:
|
|
|
1933
1939
|
parser.add_argument(
|
|
1934
1940
|
"--remote-poll-interval",
|
|
1935
1941
|
type=float,
|
|
1936
|
-
default=float(os.environ.get("REMOTION_REMOTE_POLL_INTERVAL", "
|
|
1937
|
-
help="Remote-render polling interval (seconds, default
|
|
1942
|
+
default=float(os.environ.get("REMOTION_REMOTE_POLL_INTERVAL", "2")),
|
|
1943
|
+
help="Remote-render polling interval (seconds, default 2)",
|
|
1938
1944
|
)
|
|
1939
1945
|
parser.add_argument(
|
|
1940
1946
|
"--stub-image-url",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
python3 upload_video.py --file output/video.mp4 --json-output
|
|
14
14
|
|
|
15
15
|
环境变量:
|
|
16
|
-
MM_API_BASE_URL - API 根地址(默认 MM_API_BASE_URL 环境变量或
|
|
16
|
+
MM_API_BASE_URL - API 根地址(默认 MM_API_BASE_URL 环境变量或 https://api-agent.remixmate.com/api)
|
|
17
17
|
|
|
18
18
|
注意:
|
|
19
19
|
- `uploadMediaFile` 接口视频大小限制 ≤ 50MB,音频 ≤ 15MB
|
|
@@ -31,7 +31,7 @@ import urllib.parse
|
|
|
31
31
|
import urllib.request
|
|
32
32
|
import uuid
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
DEFAULT_API_BASE_URL = "https://api-agent.remixmate.com/api"
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def _guess_content_type(file_path: str) -> str:
|
|
@@ -164,7 +164,7 @@ def main():
|
|
|
164
164
|
parser.add_argument(
|
|
165
165
|
"--api-base-url",
|
|
166
166
|
default=None,
|
|
167
|
-
help="API root URL (defaults to MM_API_BASE_URL env var or
|
|
167
|
+
help="API root URL (defaults to MM_API_BASE_URL env var or https://api-agent.remixmate.com/api)",
|
|
168
168
|
)
|
|
169
169
|
parser.add_argument("--timeout", type=int, default=600, help="Upload timeout in seconds (default 600)")
|
|
170
170
|
parser.add_argument("--json-output", action="store_true", help="Emit the full response as JSON on stdout")
|
|
@@ -9,7 +9,7 @@ CLI 暴露面是「列出可用模板」(`--list-templates`)。把 Video DSL 场
|
|
|
9
9
|
| 来源 | 触发条件 | 适用场景 |
|
|
10
10
|
|---|---|---|
|
|
11
11
|
| 显式 `VIDEO_TEMPLATE_REGISTRY_URL`(ab-api 接口) | 环境变量非空 + `PRIV_TOKEN` 有效 | 指定后端 |
|
|
12
|
-
| 派生默认 URL `<MM_API_BASE_URL>/remotionTemplate/registry` | `VIDEO_TEMPLATE_REGISTRY_URL` 未设置 | 默认(含独立安装 codex / `npm i -g`,默认 `
|
|
12
|
+
| 派生默认 URL `<MM_API_BASE_URL>/remotionTemplate/registry` | `VIDEO_TEMPLATE_REGISTRY_URL` 未设置 | 默认(含独立安装 codex / `npm i -g`,默认 `https://api.remixmate.com/api`) |
|
|
13
13
|
|
|
14
14
|
只从 ab-api 取数,是为了避免「本地文件 / monorepo 源码 / 数据库」多源并存导致的不一致——私有 / 多租户模板只存在于 ab-api,本地源永远不全。默认 URL **内置在 remixmate-cli 自身**(按 CLI 后端约定 `MM_API_BASE_URL` 推导),不再依赖 ab-agent 等宿主在 spawn 时注入,独立运行即可找到 registry。指向你自己的 ab-api 只需设 `MM_API_BASE_URL`(或直接设 `VIDEO_TEMPLATE_REGISTRY_URL`)并配置 `PRIV_TOKEN`。
|
|
15
15
|
|
|
@@ -63,12 +63,12 @@ The skill itself only reads the registry; whether a token is needed depends on t
|
|
|
63
63
|
| Env var | Description | Default |
|
|
64
64
|
|---------|-------------|---------|
|
|
65
65
|
| `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (the single source of truth). | derived from `MM_API_BASE_URL` |
|
|
66
|
-
| `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `
|
|
66
|
+
| `MM_API_BASE_URL` | ab-api base URL the CLI talks to. When `VIDEO_TEMPLATE_REGISTRY_URL` is unset, the registry endpoint is derived as `<base>/remotionTemplate/registry`. | `https://api.remixmate.com/api` |
|
|
67
67
|
| `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting the registry endpoint. Falls back to `~/.config/remixmate/credentials.json`. | unset |
|
|
68
68
|
| `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |
|
|
69
69
|
|
|
70
70
|
Resolution (single source — see `scripts/registry_loader.py` for the canonical implementation):
|
|
71
|
-
the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `
|
|
71
|
+
the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDEO_TEMPLATE_REGISTRY_URL`, otherwise `<MM_API_BASE_URL>/remotionTemplate/registry` (default `https://api.remixmate.com/api/...`). There is no local-file / monorepo / PREFER_LOCAL fallback: those multi-source paths were removed to avoid registry skew (private/multi-tenant templates only exist on ab-api). On a transient HTTP failure the loader degrades to the on-disk cache of the same URL; with no cache it fails loud. A standalone install (Codex / `npm i -g`) just needs `MM_API_BASE_URL` (or `VIDEO_TEMPLATE_REGISTRY_URL`) pointed at your ab-api plus a valid `PRIV_TOKEN`.
|
|
72
72
|
|
|
73
73
|
## Steps
|
|
74
74
|
|
|
@@ -149,7 +149,7 @@ print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
|
149
149
|
| Comes from a scene-level custom field whose name matches a prop directly | put it in `customPayload.<propName>` | `customPayload.slideId`, `customPayload.background`, `customPayload.bullets` |
|
|
150
150
|
| Structured data internal to the template (arrays, sub-objects, etc.) | use `customPayload.templateData.*` | `templateData.concepts`, `templateData.items` (shared by feature-grid / timeline / column-compare) |
|
|
151
151
|
|
|
152
|
-
**Common pitfall**: writing the same field in both `propExtractors` and `customPayload` — when they collide, `propExtractors` wins. Keep each field in exactly one place.
|
|
152
|
+
**Common pitfall**: writing the same field in both `propExtractors` and `customPayload` — when they collide, `propExtractors` wins. Keep each field in exactly one place. `build_binding` now **detects this**: any prop name that is both declared in `propExtractors` and hit by `customPayload` pass-through (top-level, or `templateData` when the extractor reads from elsewhere) is recorded on the binding row as `dualChannelProps` and printed as a stderr warning; `test-template-pipeline.py` surfaces it as a non-fatal authoring warning. A field placed in its extractor's own declared source (e.g. extractor `from:"templateData.slideId"` + value in `templateData.slideId`) is **not** flagged — that is the legitimate source, not a double-write.
|
|
153
153
|
|
|
154
154
|
> Historical issue: an early html-slide `point` slot declared `slideId` as `propExtractors.slideId={from:"templateData.slideId"}` and also marked it as `requiredProps`. But every existing DSL wrote `slideId` at `customPayload.slideId` (top-level, not inside templateData), so the propExtractor returned nothing and rendering fell back to the generic layout. The convention now is unified: **slideId flows through customPayload top-level pass-through**, propExtractors no longer declares it. New templates must follow the same convention.
|
|
155
155
|
|
|
@@ -152,31 +152,14 @@ def main() -> int:
|
|
|
152
152
|
file=sys.stderr,
|
|
153
153
|
)
|
|
154
154
|
|
|
155
|
-
#
|
|
156
|
-
#
|
|
157
|
-
#
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
tid = ref["templateId"]
|
|
164
|
-
expected_prefix = f"{tid}/"
|
|
165
|
-
# Allow either `<tid>/Cover` or `<tid>.Cover` (TS member access form).
|
|
166
|
-
if not (cid.startswith(expected_prefix) or cid.startswith(f"{tid}.")):
|
|
167
|
-
unprefixed.append((tid, cid))
|
|
168
|
-
if unprefixed:
|
|
169
|
-
print(
|
|
170
|
-
"WARN (P1.1): the following compositionIds are not prefixed with their "
|
|
171
|
-
"templateId — at 100+ templates this flat namespace will collide. "
|
|
172
|
-
"Migration plan: rename `<cid>` to `<templateId>/<cid>` in both "
|
|
173
|
-
"template.json and remotion-renderer/manifest.ts.",
|
|
174
|
-
file=sys.stderr,
|
|
175
|
-
)
|
|
176
|
-
for tid, cid in unprefixed[:10]:
|
|
177
|
-
print(f" - {tid}: '{cid}' → suggested '{tid}/{cid}'", file=sys.stderr)
|
|
178
|
-
if len(unprefixed) > 10:
|
|
179
|
-
print(f" ... ({len(unprefixed)} total)", file=sys.stderr)
|
|
155
|
+
# 命名空间唯一性说明:compositionId 是否全库唯一,由 template-library 的
|
|
156
|
+
# generateManifest 在生成 manifest.ts 时保障(跨模板撞名直接抛
|
|
157
|
+
# DuplicateCompositionIdError、CI 失败)。manifest.ts 因此按构造即唯一,
|
|
158
|
+
# 本脚本读取的已是去重结果,无需再校验唯一性。
|
|
159
|
+
#
|
|
160
|
+
# (已移除早期 P1.1 的 `<templateId>/` 前缀 warning:权威 schema 的
|
|
161
|
+
# compositionId pattern 为 PascalCase `^[A-Z][A-Za-z0-9]*$`,本就禁止 `/`,
|
|
162
|
+
# 前缀建议与 schema 矛盾且非必需——唯一性已由上游硬保障。)
|
|
180
163
|
|
|
181
164
|
if errors:
|
|
182
165
|
print("FAIL: contract check found inconsistencies:", file=sys.stderr)
|
|
@@ -14,6 +14,7 @@ sibling 的 `list_templates.py` 实现(它复用 registry_loader 的统一加
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
from registry_loader import load_registry_data
|
|
17
|
+
import sys
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def load_registry() -> list:
|
|
@@ -105,6 +106,30 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
105
106
|
# 字幕」兜底样式,所有场景看起来都是同一个布局。
|
|
106
107
|
custom = scene.get("customPayload") or {}
|
|
107
108
|
template_data = custom.get("templateData") or scene.get("templateData") or {}
|
|
109
|
+
|
|
110
|
+
# 脆弱点 3:双通道冲突检测。同一字段名既声明在 slot 的 propExtractors,
|
|
111
|
+
# 又被 customPayload 的全字段透传命中时,来源二义:propExtractors 永远赢
|
|
112
|
+
# (下面 `k not in props` 透传),但如果 extractor 返回 None、值又恰好在
|
|
113
|
+
# payload 里,作者会误以为"是 payload 生效",反之亦然。html-slide 的
|
|
114
|
+
# slideId 事故正是此类——extractor 读 `templateData.slideId`,DSL 却把值
|
|
115
|
+
# 写在 `customPayload.slideId` 顶层。
|
|
116
|
+
#
|
|
117
|
+
# 仅标记**真正的位置冲突**,避开误报:当 extractor 的 `from` 恰好指向
|
|
118
|
+
# `templateData.<同名>` 时,templateData 里出现该字段是它的**法定来源**,
|
|
119
|
+
# 不算"写错位置",跳过。其余命中(顶层透传、或 templateData 透传但 extractor
|
|
120
|
+
# 从别处取值)才算冲突。只记录 + warning,不改透传行为;CI 的
|
|
121
|
+
# test-template-pipeline.py 据此浮出去重提示。
|
|
122
|
+
extractors = slot_def.get("propExtractors", {}) or {}
|
|
123
|
+
top_keys = {k for k in custom.keys() if k != "templateData"}
|
|
124
|
+
dual_channel = []
|
|
125
|
+
for k in extractors:
|
|
126
|
+
collides_top = k in top_keys
|
|
127
|
+
extractor_from = (extractors.get(k) or {}).get("from", "")
|
|
128
|
+
collides_td = k in template_data and extractor_from != f"templateData.{k}"
|
|
129
|
+
if collides_top or collides_td:
|
|
130
|
+
dual_channel.append(k)
|
|
131
|
+
dual_channel = sorted(dual_channel)
|
|
132
|
+
|
|
108
133
|
# 先注入 templateData.* 再注入顶层 customPayload.*,让顶层覆盖同名值
|
|
109
134
|
for k, v in template_data.items():
|
|
110
135
|
if k not in props:
|
|
@@ -120,7 +145,15 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
120
145
|
entrance = anim.get("entrance", "fade")
|
|
121
146
|
transition_type = entrance if entrance != "none" else "cut"
|
|
122
147
|
|
|
123
|
-
|
|
148
|
+
# requiredProps 完整性:slotMapping 声明的 requiredProps 必须都已进 props。
|
|
149
|
+
# 这里只**记录**缺失(不抛错,保持渲染行为不变);CI 的
|
|
150
|
+
# test-template-pipeline.py 据此断言「requiredProps 齐全」并 fail loud,
|
|
151
|
+
# 避免历史上「声明了 requiredProps 却静默缺失 → React 端回退通用布局」的事故
|
|
152
|
+
# (见 template-json-design.md 脆弱点 2)。是否升级为渲染前硬失败属后续决策。
|
|
153
|
+
required_props = slot_def.get("requiredProps") or []
|
|
154
|
+
missing_required = [p for p in required_props if p not in props]
|
|
155
|
+
|
|
156
|
+
binding_row = {
|
|
124
157
|
"sceneId": scene_id,
|
|
125
158
|
"slotId": slot_id,
|
|
126
159
|
"compositionId": comp_id,
|
|
@@ -128,7 +161,18 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
128
161
|
"props": props,
|
|
129
162
|
"transitionIn": {"type": transition_type, "durationFrames": 15},
|
|
130
163
|
"transitionOut": {"type": "fade", "durationFrames": 15},
|
|
131
|
-
}
|
|
164
|
+
}
|
|
165
|
+
if missing_required:
|
|
166
|
+
binding_row["missingRequiredProps"] = missing_required
|
|
167
|
+
if dual_channel:
|
|
168
|
+
binding_row["dualChannelProps"] = dual_channel
|
|
169
|
+
print(
|
|
170
|
+
f"⚠️ 双通道字段:scene={scene_id!r} slot={slot_id!r} 的 "
|
|
171
|
+
f"{dual_channel} 同时声明在 propExtractors 与 customPayload,"
|
|
172
|
+
f"propExtractors 优先。建议二选一,避免写错位置时静默取错值。",
|
|
173
|
+
file=sys.stderr,
|
|
174
|
+
)
|
|
175
|
+
bindings.append(binding_row)
|
|
132
176
|
|
|
133
177
|
# P1.2: 不再做 screen-walkthrough 专属的截断 / 兜底。模板的 propExtractors
|
|
134
178
|
# 已声明 titleText ← textLayers[role=headline]、projectName ← textLayers[role=subheadline],
|
|
@@ -139,16 +183,16 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
139
183
|
motion = template.get("defaultMotionPreset", "smooth")
|
|
140
184
|
colors = template.get("defaultColorScheme", [])
|
|
141
185
|
|
|
142
|
-
# --- variant selection
|
|
186
|
+
# --- variant selection(确定性)---
|
|
143
187
|
# 优先级:
|
|
144
|
-
# 1) DSL 显式声明 — meta.templateVariant / renderHints.templateVariant
|
|
145
|
-
# 2)
|
|
146
|
-
#
|
|
147
|
-
# -
|
|
148
|
-
# -
|
|
149
|
-
#
|
|
150
|
-
# 的子串)。显式 templateVariant 让作者可以一锤定音。
|
|
188
|
+
# 1) DSL 显式声明 — meta.templateVariant / meta.variant / renderHints.templateVariant
|
|
189
|
+
# 2) meta.style 与 variant.styleTags 的**分级**匹配(精确 > 整段包含 > token 子串)
|
|
190
|
+
# 旧实现按 dict 遍历顺序「命中第一个」,多义时结果不稳定;现改为:
|
|
191
|
+
# - 按 variant id 排序遍历(消除顺序依赖);
|
|
192
|
+
# - 取匹配等级最高(tier 数最小)的 variant;
|
|
193
|
+
# - 同一最高等级有多个候选 → 记歧义、取排序首个,并把理由写进 variantReason。
|
|
151
194
|
variant_id = None
|
|
195
|
+
variant_reason = None
|
|
152
196
|
meta = dsl.get("meta", {}) or {}
|
|
153
197
|
render_hints = dsl.get("renderHints", {}) or {}
|
|
154
198
|
explicit_variant = (
|
|
@@ -160,31 +204,57 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
160
204
|
|
|
161
205
|
if explicit_variant and explicit_variant in variants:
|
|
162
206
|
variant_id = explicit_variant
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
# 再到 variant.styleTags 里找任一包含或被包含关系。这样 "紫色科技" 既能命中
|
|
166
|
-
# ["紫色","科技"] 这种细粒度 tag,也能命中 ["紫色科技"] 这种整段 tag。
|
|
207
|
+
variant_reason = "explicit"
|
|
208
|
+
elif variants:
|
|
167
209
|
raw_style = (meta.get("style") or "").strip().lower()
|
|
168
210
|
if raw_style:
|
|
169
|
-
#
|
|
170
|
-
|
|
171
|
-
for sep in [",", ",", ";", ";", " ", "/", "·"]:
|
|
172
|
-
|
|
173
|
-
|
|
211
|
+
# token 化:整段 + 分隔符拆分 + 单中文字符级
|
|
212
|
+
s = raw_style
|
|
213
|
+
for sep in [",", ",", ";", ";", " ", "/", "·", "、"]:
|
|
214
|
+
s = s.replace(sep, ",")
|
|
215
|
+
tokens = set()
|
|
216
|
+
for tok in s.split(","):
|
|
174
217
|
tok = tok.strip()
|
|
175
218
|
if not tok:
|
|
176
219
|
continue
|
|
177
|
-
|
|
178
|
-
# 中文按字拆,命中如 "紫"/"色"/"科"/"技" 这种单字 tag 的场景
|
|
220
|
+
tokens.add(tok)
|
|
179
221
|
for ch in tok:
|
|
180
222
|
if "\u4e00" <= ch <= "\u9fff":
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
223
|
+
tokens.add(ch)
|
|
224
|
+
|
|
225
|
+
# 为每个 variant 算最佳匹配等级(1 最强)
|
|
226
|
+
# tier 1: 某 style token 与某 tag 完全相等
|
|
227
|
+
# tier 2: 整段 raw_style 与某 tag 互为子串(整体短语级)
|
|
228
|
+
# tier 3: 某 token 与某 tag 互为子串(细粒度,最宽松)
|
|
229
|
+
by_tier: dict = {}
|
|
230
|
+
for vid in sorted(variants.keys()):
|
|
231
|
+
vtags = [str(t).strip().lower() for t in (variants[vid].get("styleTags") or []) if str(t).strip()]
|
|
232
|
+
tier = None
|
|
233
|
+
if any(tok == tag for tok in tokens for tag in vtags):
|
|
234
|
+
tier = 1
|
|
235
|
+
elif any((raw_style in tag) or (tag in raw_style) for tag in vtags):
|
|
236
|
+
tier = 2
|
|
237
|
+
elif any((tok in tag) or (tag in tok) for tok in tokens for tag in vtags):
|
|
238
|
+
tier = 3
|
|
239
|
+
if tier is not None:
|
|
240
|
+
by_tier.setdefault(tier, []).append(vid)
|
|
241
|
+
|
|
242
|
+
if by_tier:
|
|
243
|
+
best_tier = min(by_tier.keys())
|
|
244
|
+
winners = by_tier[best_tier] # 已按 vid 排序
|
|
245
|
+
variant_id = winners[0]
|
|
246
|
+
if len(winners) > 1:
|
|
247
|
+
variant_reason = (
|
|
248
|
+
f"style-match tier{best_tier} (ambiguous among {winners}; "
|
|
249
|
+
f"picked '{variant_id}' by id order — 建议作者改用显式 meta.templateVariant)"
|
|
250
|
+
)
|
|
251
|
+
print(
|
|
252
|
+
f"⚠️ variant 选择歧义:style={meta.get('style')!r} 在 tier{best_tier} "
|
|
253
|
+
f"命中多个变体 {winners},确定性取 '{variant_id}'。建议显式声明 meta.templateVariant。",
|
|
254
|
+
file=sys.stderr,
|
|
255
|
+
)
|
|
256
|
+
else:
|
|
257
|
+
variant_reason = f"style-match tier{best_tier}"
|
|
188
258
|
|
|
189
259
|
if variant_id and variant_id in variants:
|
|
190
260
|
variant = variants[variant_id]
|
|
@@ -211,6 +281,8 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
211
281
|
}
|
|
212
282
|
if variant_id:
|
|
213
283
|
result["variantId"] = variant_id
|
|
284
|
+
if variant_reason:
|
|
285
|
+
result["variantReason"] = variant_reason
|
|
214
286
|
return result
|
|
215
287
|
|
|
216
288
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
1. 显式 VIDEO_TEMPLATE_REGISTRY_URL(若设置则直接用)
|
|
16
16
|
2. 否则按 CLI 后端约定推导:
|
|
17
17
|
<MM_API_BASE_URL>/remotionTemplate/registry
|
|
18
|
-
MM_API_BASE_URL 默认
|
|
18
|
+
MM_API_BASE_URL 默认 https://api.remixmate.com/api(与 src/http.ts 一致),
|
|
19
19
|
兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
|
|
20
20
|
(codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
|
|
21
21
|
|
|
@@ -71,7 +71,7 @@ _CACHE_DIR = os.environ.get(
|
|
|
71
71
|
|
|
72
72
|
|
|
73
73
|
# CLI 默认后端 base,与 remixmate-cli/src/http.ts 的 DEFAULT_API_BASE_URL 保持一致。
|
|
74
|
-
_DEFAULT_API_BASE_URL = "
|
|
74
|
+
_DEFAULT_API_BASE_URL = "https://api.remixmate.com/api"
|
|
75
75
|
|
|
76
76
|
|
|
77
77
|
def _default_registry_url() -> str:
|
|
@@ -83,7 +83,7 @@ def _default_registry_url() -> str:
|
|
|
83
83
|
base 解析顺序与 CLI / ab-agent 对齐:
|
|
84
84
|
MM_API_BASE_URL(CLI ``src/http.ts`` 约定)
|
|
85
85
|
→ MM_BACKEND_API_URL(ab-agent 约定,向后兼容)
|
|
86
|
-
→
|
|
86
|
+
→ https://api.remixmate.com/api(生产默认,零配置)
|
|
87
87
|
路径段固定为 ``/remotionTemplate/registry``(POST,返回 ``{code,msg,data}``)。
|
|
88
88
|
"""
|
|
89
89
|
base = (
|
|
@@ -6,7 +6,7 @@ render_job_client.py — ab-api renderJob 接口 Python 客户端
|
|
|
6
6
|
gen_jianying_draft.py 使用。
|
|
7
7
|
|
|
8
8
|
认证方式: X-Priv-Token header(与其他 skill 脚本一致)
|
|
9
|
-
接口地址: 由环境变量 MM_BACKEND_API_URL 控制(默认
|
|
9
|
+
接口地址: 由环境变量 MM_BACKEND_API_URL 控制(默认 https://api.remixmate.com/api)
|
|
10
10
|
"""
|
|
11
11
|
|
|
12
12
|
import json
|
|
@@ -17,7 +17,7 @@ import urllib.request
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _api_base() -> str:
|
|
20
|
-
return os.environ.get("MM_BACKEND_API_URL", "
|
|
20
|
+
return os.environ.get("MM_BACKEND_API_URL", "https://api.remixmate.com/api").rstrip("/")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _make_headers(priv_token: str) -> dict:
|