@remixmate/cli 0.1.2 → 0.9.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.
- package/README.md +46 -0
- package/README.zh-CN.md +22 -0
- package/dist/auth/auth-lock.d.ts +26 -0
- package/dist/auth/auth-lock.js +100 -0
- package/dist/auth/auto-login.d.ts +20 -0
- package/dist/auth/auto-login.js +66 -0
- package/dist/auth/commands.d.ts +8 -0
- package/dist/auth/commands.js +130 -0
- package/dist/auth/credential-store.d.ts +44 -0
- package/dist/auth/credential-store.js +126 -0
- package/dist/auth/device-flow-runner.d.ts +43 -0
- package/dist/auth/device-flow-runner.js +62 -0
- package/dist/auth/device-flow.d.ts +52 -0
- package/dist/auth/device-flow.js +115 -0
- package/dist/auth/environment.d.ts +25 -0
- package/dist/auth/environment.js +48 -0
- package/dist/auth/resolve.d.ts +30 -0
- package/dist/auth/resolve.js +44 -0
- package/dist/cli.js +11 -0
- package/dist/handlers/gen-digital-human.js +1 -1
- package/dist/handlers/gen-image.js +1 -1
- package/dist/handlers/gen-video.js +1 -1
- package/dist/handlers/gen-voice.js +2 -2
- package/dist/http.d.ts +9 -5
- package/dist/http.js +24 -10
- package/dist/manifest.json +3 -3
- package/dist/runner.d.ts +1 -1
- package/package.json +1 -1
- package/skills/export-jianying/SKILL.md +9 -3
- package/skills/export-jianying/version.json +1 -1
- package/skills/gen-digital-human/SKILL.md +11 -11
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +6 -6
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/scripts/gen_script.py +66 -31
- package/skills/gen-script/version.json +1 -1
- package/skills/gen-video/SKILL.md +6 -6
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +6 -6
- package/skills/gen-voice/version.json +1 -1
- package/skills/render-video/SKILL.md +20 -1
- package/skills/render-video/scripts/remote_renderer_client.py +9 -7
- package/skills/render-video/scripts/render_video.py +119 -15
- package/skills/render-video/version.json +1 -1
- package/skills/template-registry/README.md +12 -13
- package/skills/template-registry/SKILL.md +11 -12
- package/skills/template-registry/scripts/registry_loader.py +117 -96
- package/skills/template-registry/scripts/render_job_client.py +12 -0
- package/skills/template-registry/skill.json +1 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/runtime/dsl_validator.py +2 -2
- package/skills/web-capture/skill.json +0 -1
- package/skills/web-capture/version.json +1 -1
|
@@ -1607,26 +1607,73 @@ def render_with_remote_api(
|
|
|
1607
1607
|
template_id = render_plan.get("templateId", "")
|
|
1608
1608
|
cover_composition_id = resolve_cover_composition_id(template_id)
|
|
1609
1609
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1610
|
+
# ─── 私有模板路由 ────────────────────────────────────────────────────
|
|
1611
|
+
# 仅【用户私有模板】(isBuiltin=false 且带 sourceOssKey) 源码在 OSS、不在
|
|
1612
|
+
# ab-render 启动 bundle,需走动态渲染:presignSource 取临时 GET URL → /renderDraft。
|
|
1613
|
+
# 内置模板即使带 sourceOssKey(builtin-template-registry 回填,仅服务
|
|
1614
|
+
# derive_template 派生),渲染仍走 ab-render 预构建的 /render,绝不走 /renderDraft,
|
|
1615
|
+
# 否则会用 OSS 上的旧快照 + DraftMainVideo 复刻渲染,与生产 MainVideo 漂移。
|
|
1616
|
+
source_oss_key = None
|
|
1617
|
+
template_id = render_plan.get("templateId", "")
|
|
1618
|
+
if template_id:
|
|
1619
|
+
try:
|
|
1620
|
+
from registry_loader import get_template # type: ignore
|
|
1621
|
+
_meta = get_template(template_id)
|
|
1622
|
+
if _meta and not _meta.get("isBuiltin"):
|
|
1623
|
+
source_oss_key = _meta.get("sourceOssKey")
|
|
1624
|
+
except Exception:
|
|
1625
|
+
source_oss_key = None
|
|
1626
|
+
|
|
1627
|
+
if source_oss_key:
|
|
1628
|
+
import render_job_client # type: ignore
|
|
1629
|
+
try:
|
|
1630
|
+
src = render_job_client.presign_template_source(template_id, private_token)
|
|
1631
|
+
tarball_url = src.get("tarballUrl")
|
|
1632
|
+
if not tarball_url:
|
|
1633
|
+
raise RuntimeError(f"presignSource 未返回 tarballUrl: {src}")
|
|
1634
|
+
except Exception as exc:
|
|
1635
|
+
LogPrint(f"❌ presignSource failed for private template {template_id}: {exc}", file=sys.stderr)
|
|
1636
|
+
render_plan["status"] = "failed"
|
|
1637
|
+
render_plan["errors"].append({
|
|
1638
|
+
"phase": "render",
|
|
1639
|
+
"message": f"presignSource failed: {exc}",
|
|
1640
|
+
"timestamp": now_iso(),
|
|
1641
|
+
})
|
|
1642
|
+
return False
|
|
1643
|
+
payload = {
|
|
1644
|
+
"tarballUrl": tarball_url,
|
|
1645
|
+
"inputProps": input_props,
|
|
1646
|
+
"upload": True,
|
|
1647
|
+
"uploadTitle": effective_title,
|
|
1648
|
+
}
|
|
1649
|
+
render_path = "/renderDraft"
|
|
1650
|
+
status_path = "/renderDraftStatus"
|
|
1651
|
+
else:
|
|
1652
|
+
payload = {
|
|
1653
|
+
"compositionId": composition_id,
|
|
1654
|
+
"renderConfig": config,
|
|
1655
|
+
"inputProps": input_props,
|
|
1656
|
+
"uploadTitle": effective_title,
|
|
1657
|
+
}
|
|
1658
|
+
if cover_composition_id:
|
|
1659
|
+
payload["coverCompositionId"] = cover_composition_id
|
|
1660
|
+
render_path = "/render"
|
|
1661
|
+
status_path = "/renderStatus"
|
|
1618
1662
|
|
|
1619
1663
|
total_frames = config.get("totalFrames", 0)
|
|
1620
1664
|
LogPrint(f"🎬 Submitting remote render task...", file=sys.stderr)
|
|
1621
|
-
|
|
1622
|
-
|
|
1665
|
+
if source_oss_key:
|
|
1666
|
+
LogPrint(f" Private template (OSS dynamic bundle): {template_id}", file=sys.stderr)
|
|
1667
|
+
else:
|
|
1668
|
+
LogPrint(f" Composition: {composition_id}", file=sys.stderr)
|
|
1669
|
+
if cover_composition_id and not source_oss_key:
|
|
1623
1670
|
LogPrint(f" Cover: {cover_composition_id}", file=sys.stderr)
|
|
1624
1671
|
LogPrint(f" size: {config.get('width')}x{config.get('height')}", file=sys.stderr)
|
|
1625
1672
|
LogPrint(f" total frames: {total_frames}", file=sys.stderr)
|
|
1626
1673
|
sys.stderr.flush()
|
|
1627
1674
|
|
|
1628
1675
|
try:
|
|
1629
|
-
task_id = remote_renderer_client.start_render(payload, private_token=private_token, conversation_id=conversation_id)
|
|
1676
|
+
task_id = remote_renderer_client.start_render(payload, private_token=private_token, conversation_id=conversation_id, path=render_path)
|
|
1630
1677
|
except Exception as exc:
|
|
1631
1678
|
LogPrint(f"❌ remote render submission failed: {exc}", file=sys.stderr)
|
|
1632
1679
|
render_plan["status"] = "failed"
|
|
@@ -1686,6 +1733,7 @@ def render_with_remote_api(
|
|
|
1686
1733
|
interval=poll_interval,
|
|
1687
1734
|
on_progress=_on_progress,
|
|
1688
1735
|
adaptive_interval=True,
|
|
1736
|
+
status_path=status_path,
|
|
1689
1737
|
)
|
|
1690
1738
|
except Exception as exc:
|
|
1691
1739
|
LogPrint(f"❌ remote render polling failed: {exc}", file=sys.stderr)
|
|
@@ -1808,6 +1856,35 @@ def auto_bind_template(dsl: dict, template_id: str) -> dict:
|
|
|
1808
1856
|
return binding
|
|
1809
1857
|
|
|
1810
1858
|
|
|
1859
|
+
def _log_render_plan_summary(render_plan: dict) -> None:
|
|
1860
|
+
"""Print a one-line identity summary of a loaded RenderPlan.
|
|
1861
|
+
|
|
1862
|
+
The two ways to feed an existing plan (``--render-plan <file>`` and
|
|
1863
|
+
``--job-id <int>``) used to print only the *source* (path / id), never the
|
|
1864
|
+
*content*. When a stale ``output/render-plan.json`` from a previous, unrelated
|
|
1865
|
+
run got picked up, the mismatch was rendered silently. Surfacing
|
|
1866
|
+
templateId / title / duration / scene count here makes a wrong plan obvious
|
|
1867
|
+
at a glance before any render time is spent.
|
|
1868
|
+
"""
|
|
1869
|
+
try:
|
|
1870
|
+
template_id = render_plan.get("templateId", "?")
|
|
1871
|
+
title = render_plan.get("title", "")
|
|
1872
|
+
cfg = render_plan.get("renderConfig", {}) or {}
|
|
1873
|
+
fps = cfg.get("fps") or 30
|
|
1874
|
+
total_frames = cfg.get("totalFrames")
|
|
1875
|
+
scenes = len(render_plan.get("timeline", []) or [])
|
|
1876
|
+
comp = (render_plan.get("remotionProps", {}) or {}).get("compositionId", "?")
|
|
1877
|
+
dur = f"{total_frames / fps:.1f}s/{total_frames}f" if total_frames else "?"
|
|
1878
|
+
LogPrint(
|
|
1879
|
+
f" ↳ plan: templateId={template_id} composition={comp} "
|
|
1880
|
+
f"duration={dur} scenes={scenes}" + (f' title=\"{title}\"' if title else ""),
|
|
1881
|
+
file=sys.stderr,
|
|
1882
|
+
)
|
|
1883
|
+
except Exception:
|
|
1884
|
+
# A summary is a convenience, never a hard dependency of rendering.
|
|
1885
|
+
pass
|
|
1886
|
+
|
|
1887
|
+
|
|
1811
1888
|
def main():
|
|
1812
1889
|
parser = argparse.ArgumentParser(
|
|
1813
1890
|
description="Video render tool — DSL + TemplateBinding → Remotion video.",
|
|
@@ -1896,21 +1973,48 @@ Examples:
|
|
|
1896
1973
|
sys.exit(1)
|
|
1897
1974
|
render_plan = json.loads(render_plan_str)
|
|
1898
1975
|
LogPrint(f"✅ RenderPlan loaded (jobId={args.job_id})", file=sys.stderr)
|
|
1976
|
+
_log_render_plan_summary(render_plan)
|
|
1899
1977
|
elif args.render_plan:
|
|
1900
1978
|
if not os.path.exists(args.render_plan):
|
|
1901
1979
|
LogPrint(f"❌ file does not exist: {args.render_plan}", file=sys.stderr)
|
|
1902
1980
|
sys.exit(1)
|
|
1903
1981
|
render_plan = load_json(args.render_plan)
|
|
1904
1982
|
LogPrint(f"📋 loaded existing RenderPlan: {args.render_plan}", file=sys.stderr)
|
|
1983
|
+
_log_render_plan_summary(render_plan)
|
|
1984
|
+
# 陈旧文件防护:output/render-plan.json 等共享文件名常被上一次别的项目
|
|
1985
|
+
# 的运行残留覆盖(output/ 是 gitignored 调试目录)。当用户同时给了 --dsl
|
|
1986
|
+
# 用以表明"我想渲这个 DSL",但磁盘上的 plan 比 DSL 还旧时,几乎可以肯定
|
|
1987
|
+
# 加载到的是过期 plan —— 明确警告而不是静默渲染错的东西。
|
|
1988
|
+
if args.dsl and os.path.exists(args.dsl):
|
|
1989
|
+
try:
|
|
1990
|
+
if os.path.getmtime(args.render_plan) < os.path.getmtime(args.dsl):
|
|
1991
|
+
LogPrint(
|
|
1992
|
+
f"⚠️ STALE RenderPlan? '{args.render_plan}' is OLDER than the DSL "
|
|
1993
|
+
f"'{args.dsl}'. You may be rendering a leftover plan from a previous "
|
|
1994
|
+
"run. Regenerate with --dsl ... --template-id ... (drop --render-plan) "
|
|
1995
|
+
"if the summary above doesn't match what you expect.",
|
|
1996
|
+
file=sys.stderr,
|
|
1997
|
+
)
|
|
1998
|
+
except OSError:
|
|
1999
|
+
pass
|
|
1905
2000
|
else:
|
|
1906
2001
|
if not args.dsl and not args.dsl_json:
|
|
1907
|
-
LogPrint(
|
|
1908
|
-
|
|
2002
|
+
LogPrint(
|
|
2003
|
+
"❌ render_video needs a DSL source. You provided neither --dsl/--dsl-json "
|
|
2004
|
+
"nor --render-plan/--job-id.\n"
|
|
2005
|
+
" Typical flow: 1) gen_script → DSL skeleton 2) prepare_video_assets "
|
|
2006
|
+
"(returns a job_id) 3) render_video with job_id=<int>.\n"
|
|
2007
|
+
" Or pass the DSL inline: render_video --dsl-json '<json>' --template-id <id>.",
|
|
2008
|
+
file=sys.stderr,
|
|
2009
|
+
)
|
|
1909
2010
|
sys.exit(1)
|
|
1910
2011
|
|
|
1911
2012
|
if not args.template_id:
|
|
1912
|
-
LogPrint(
|
|
1913
|
-
|
|
2013
|
+
LogPrint(
|
|
2014
|
+
"❌ render_video needs --template-id when rendering from --dsl/--dsl-json "
|
|
2015
|
+
"(binding is computed in-memory from the template id; --binding is no longer supported).",
|
|
2016
|
+
file=sys.stderr,
|
|
2017
|
+
)
|
|
1914
2018
|
sys.exit(1)
|
|
1915
2019
|
|
|
1916
2020
|
# 解析 DSL:优先 --dsl-json(inline),其次 --dsl(文件路径)
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V19",
|
|
6
6
|
"skillDescription": "Final-render skill (Phase 3 of the two-phase video pipeline). Loads a persisted RenderPlan by job_id and drives Remotion to produce the final video. Assets must already be generated via prepare_video_assets."
|
|
7
7
|
}
|
|
@@ -4,30 +4,29 @@ CLI 暴露面是「列出可用模板」(`--list-templates`)。把 Video DSL 场
|
|
|
4
4
|
|
|
5
5
|
## 模板数据来源
|
|
6
6
|
|
|
7
|
-
模板元数据维护在独立仓库 **template-library
|
|
7
|
+
模板元数据维护在独立仓库 **template-library**,发布到 ab-api,运行时由本 skill 通过 `scripts/registry_loader.py` 从 **单一数据源(ab-api HTTP)** 加载:
|
|
8
8
|
|
|
9
|
-
|
|
|
10
|
-
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
9
|
+
| 来源 | 触发条件 | 适用场景 |
|
|
10
|
+
|---|---|---|
|
|
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`,默认 `http://localhost:2999/api`) |
|
|
13
|
+
|
|
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`。
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
HTTP 拉取带磁盘缓存(TTL + ETag/304);瞬时故障时降级复用同一 URL 的上一份缓存(同源容错,非第二个数据源),无缓存则直接报错。
|
|
16
17
|
|
|
17
18
|
ab-api 响应包装格式 `{code, msg, data}` 由 `registry_loader._fetch_http` 透明拆封;静态 JSON endpoint 也支持(直接返回 registry 文档)。
|
|
18
19
|
|
|
19
|
-
> 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm
|
|
20
|
+
> 早期文档曾描述本 skill 通过 `@ab-templates/metadata` npm 包 / monorepo 文件消费 registry —— 这些本地来源已移除,运行时只走 ab-api HTTP。monorepo 文件仅供 `check_contracts.py` / `sync_registry.py` 等 monorepo-only 维护脚本使用。
|
|
20
21
|
|
|
21
22
|
## 新增模板工作流
|
|
22
23
|
|
|
23
|
-
新增模板**不需要改
|
|
24
|
+
新增模板**不需要改 remixmate / template-registry 代码**:
|
|
24
25
|
|
|
25
26
|
1. 在 template-library 仓库定义新模板(`template.json` + 组件代码)。
|
|
26
27
|
2. template-library CI 校验 schema + 契约。
|
|
27
28
|
3. 合并后 template-library 的发布流水线将新 registry 推到 ab-api。
|
|
28
|
-
4.
|
|
29
|
-
|
|
30
|
-
本地调试时可通过 `VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL=1` 让 template-library 的源码改动立即生效,不依赖发布周期。
|
|
29
|
+
4. remixmate 端无需更新——下一次 `--list-templates` 即可看到新模板。
|
|
31
30
|
|
|
32
31
|
## 契约一致性
|
|
33
32
|
|
|
@@ -45,7 +44,7 @@ python3 <SkillDir>/scripts/check_contracts.py
|
|
|
45
44
|
|
|
46
45
|
## 共享 Python 模块
|
|
47
46
|
|
|
48
|
-
`scripts/` 是
|
|
47
|
+
`scripts/` 是 remixmate 内**跨 skill 共享 Python 代码**的约定位置。当前住户:
|
|
49
48
|
|
|
50
49
|
| 模块 | 谁在用 | 作用 |
|
|
51
50
|
|---|---|---|
|
|
@@ -23,9 +23,9 @@ Stores every video-template definition, loads a template by **template-id**, and
|
|
|
23
23
|
|
|
24
24
|
## Template registry
|
|
25
25
|
|
|
26
|
-
Template metadata
|
|
26
|
+
Template metadata is authored in the standalone **template-library** monorepo, published to ab-api, and served to the CLI over HTTP. At runtime template-registry loads the registry from a single source — the ab-api endpoint (see the env table below) — which returns every template definition (full slotMapping, compositions, etc.) including the caller's private/tenant templates.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
> The `template-library/packages/metadata/registry.json` file below is the authoring layout in the monorepo. It is **not** read at runtime anymore; only monorepo-only maintenance tooling (`check_contracts.py`, `sync_registry.py`) touches it directly.
|
|
29
29
|
|
|
30
30
|
```
|
|
31
31
|
template-library/packages/
|
|
@@ -62,22 +62,21 @@ The skill itself only reads the registry; whether a token is needed depends on t
|
|
|
62
62
|
|
|
63
63
|
| Env var | Description | Default |
|
|
64
64
|
|---------|-------------|---------|
|
|
65
|
-
| `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (
|
|
66
|
-
| `
|
|
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`. | `http://localhost:2999/api` |
|
|
67
|
+
| `PRIV_TOKEN` | Sent as `X-Priv-Token` when hitting the registry endpoint. Falls back to `~/.config/remixmate/credentials.json`. | unset |
|
|
67
68
|
| `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |
|
|
68
|
-
| `VIDEO_TEMPLATE_REGISTRY` | Explicit local file path — bypasses HTTP/fallback entirely (CI / pinned debug). | unset |
|
|
69
|
-
| `VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL` | When `1`, monorepo `template-library/packages/metadata/registry.json` wins over HTTP. Defaults to HTTP-first. | unset |
|
|
70
69
|
|
|
71
|
-
Resolution
|
|
72
|
-
`
|
|
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 `http://localhost:2999/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`.
|
|
73
72
|
|
|
74
73
|
## Steps
|
|
75
74
|
|
|
76
|
-
> This skill is a Python skill of the
|
|
75
|
+
> This skill is a Python skill of the remixmate CLI (`entry.type: python` → `scripts/list_templates.py`). The agent tool name `template_registry` is the only entry; locally reproduce via `remixmate template-registry --list-templates`. The list command delegates to `scripts/registry_loader.py` — the same loader (with caching + stable/beta gating) that `render-video` and `gen-script` import in-process, so there is a single registry-reading implementation.
|
|
77
76
|
>
|
|
78
77
|
> The Python binding logic that maps DSL → TemplateBinding lives in `scripts/match_template.py` but is **not exposed as a CLI** — it is only consumed as a Python library by `render-video`'s `render_video.py` via `import match_template`.
|
|
79
78
|
|
|
80
|
-
1. **List available templates**: run `
|
|
79
|
+
1. **List available templates**: run `remixmate template-registry --list-templates` to view the templates in the registry along with their supported aspect ratios / style tags, and decide which `templateId` to pick.
|
|
81
80
|
2. **Write the DSL**: when generating the Video DSL, put the chosen `templateId` into `meta.templateId` (the canonical location). Use `meta.templateVariant` / `renderHints.templateVariant` to explicitly select a variant. The legacy `renderHints.templatePreference[0]` is still tolerated by `match_template.py` and `dsl_validator._pick_template_id` during transition, but new authors should write `meta.templateId`.
|
|
82
81
|
3. **Produce the TemplateBinding**: there is no standalone CLI for DSL → TemplateBinding; `prepare_video_assets` calls `match_template.build_binding(template, dsl)` inline during the asset-resolution pipeline and embeds the binding into the RenderPlan it hands to the renderer — no separate `*.binding.json` file is written.
|
|
83
82
|
|
|
@@ -86,7 +85,7 @@ Resolution order (see `scripts/registry_loader.py` for the canonical implementat
|
|
|
86
85
|
### List available templates
|
|
87
86
|
|
|
88
87
|
```bash
|
|
89
|
-
|
|
88
|
+
remixmate template-registry --list-templates
|
|
90
89
|
```
|
|
91
90
|
|
|
92
91
|
### Sync the registry cache (optional, used for offline / LLM prompt)
|
|
@@ -233,4 +232,4 @@ With `picture-book-en`, **narration reads English only, never Chinese**:
|
|
|
233
232
|
|
|
234
233
|
- **No template specified**: the DSL must set `meta.templateId` (or, for legacy DSLs only, `renderHints.templatePreference[0]`); otherwise `prepare_video_assets` refuses to build a binding and prints the list of available templates.
|
|
235
234
|
- **Malformed DSL**: validate first with `gen-script --validate` (delegates to `video_dsl.runtime.dsl_validator.validate_structural`).
|
|
236
|
-
- **Empty template registry**: confirm `VIDEO_TEMPLATE_REGISTRY_URL` is reachable
|
|
235
|
+
- **Empty template registry**: confirm the ab-api registry endpoint (`VIDEO_TEMPLATE_REGISTRY_URL`, or the one derived from `MM_API_BASE_URL`) is reachable and that `PRIV_TOKEN` is valid.
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
模板 registry 加载器 -
|
|
3
|
+
模板 registry 加载器 - 单一数据源(ab-api HTTP)。
|
|
4
4
|
|
|
5
5
|
为什么独立成一个模块:
|
|
6
6
|
template-registry、render-video 等多个 skill 都要读 registry,集中在一处便于:
|
|
7
7
|
- 缓存策略一致(HTTP 拉取 5 分钟 TTL + ETag 复用)
|
|
8
|
-
-
|
|
9
|
-
- 错误降级一致(HTTP 故障时复用上一次缓存)
|
|
8
|
+
- 错误降级一致(HTTP 故障时复用上一次磁盘缓存)
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
单一数据源(设计取舍):
|
|
11
|
+
registry 只从 ab-api HTTP 接口加载,避免「本地文件 / monorepo 源码 / 数据库」
|
|
12
|
+
多源并存导致的不一致 —— 私有 / 多租户模板只存在于 ab-api,本地源永远不全。
|
|
13
|
+
|
|
14
|
+
URL 解析:
|
|
15
|
+
1. 显式 VIDEO_TEMPLATE_REGISTRY_URL(若设置则直接用)
|
|
16
|
+
2. 否则按 CLI 后端约定推导:
|
|
17
|
+
<MM_API_BASE_URL>/remotionTemplate/registry
|
|
18
|
+
MM_API_BASE_URL 默认 http://localhost:2999/api(与 src/http.ts 一致),
|
|
19
|
+
兼容 ab-agent 注入的 MM_BACKEND_API_URL。默认值内置在 CLI 自身,独立安装
|
|
20
|
+
(codex / npm i -g)开箱即用,无需宿主在 spawn 时注入。
|
|
21
|
+
|
|
22
|
+
鉴权:PRIV_TOKEN(env)或 ~/.config/remixmate/credentials.json(同源条目)。
|
|
23
|
+
|
|
24
|
+
monorepo / 显式本地文件等其它来源已**移除**。需要离线 / pinned 数据的维护脚本
|
|
25
|
+
(check_contracts.py / sync_registry.py)直接走 template_paths 读 monorepo,
|
|
26
|
+
那是 monorepo-only 工具,不是运行时数据源。
|
|
15
27
|
|
|
16
28
|
P1.2 — list/detail split (待后端就绪):
|
|
17
29
|
100+ 模板时 ``GET /api/template/list`` 应该只返摘要(id / name / aspects /
|
|
@@ -21,14 +33,6 @@ P1.2 — list/detail split (待后端就绪):
|
|
|
21
33
|
改成「先拉摘要,触发 ``get_template`` 时再补全」,公共 API 不变。
|
|
22
34
|
调用方今天用 ``get_template(id)`` / ``list_templates()`` 已经是按需消费的形态,
|
|
23
35
|
迁移时不需要改任何 caller。
|
|
24
|
-
|
|
25
|
-
本地 dev 调试开关:
|
|
26
|
-
VIDEO_TEMPLATE_REGISTRY_PREFER_LOCAL=1
|
|
27
|
-
将 monorepo registry.json 提到 #2 之前 —— 即"软优先"语义:
|
|
28
|
-
- monorepo 文件存在 → 用本地源码(与 template-library 改动严格一致)
|
|
29
|
-
- monorepo 文件不存在 → 自动落到 HTTP API,行为与未开启此开关时一致
|
|
30
|
-
设计意图:让本地开发避开"数据库 template_json 与 template-library 源码不一致"
|
|
31
|
-
导致的渲染问题,同时不破坏在容器/CI 中错误启用此开关时的鲁棒性。
|
|
32
36
|
"""
|
|
33
37
|
|
|
34
38
|
from __future__ import annotations
|
|
@@ -43,8 +47,7 @@ import urllib.error
|
|
|
43
47
|
import urllib.request
|
|
44
48
|
from functools import lru_cache
|
|
45
49
|
from typing import Optional
|
|
46
|
-
|
|
47
|
-
from template_paths import monorepo_registry_path
|
|
50
|
+
from urllib.parse import urlparse
|
|
48
51
|
|
|
49
52
|
__all__ = [
|
|
50
53
|
"load_registry_data",
|
|
@@ -67,14 +70,28 @@ _CACHE_DIR = os.environ.get(
|
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
# CLI 默认后端 base,与 remixmate-cli/src/http.ts 的 DEFAULT_API_BASE_URL 保持一致。
|
|
74
|
+
_DEFAULT_API_BASE_URL = "http://localhost:2999/api"
|
|
75
|
+
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
def _default_registry_url() -> str:
|
|
78
|
+
"""未显式配置 VIDEO_TEMPLATE_REGISTRY_URL 时,推导默认 ab-api registry endpoint。
|
|
79
|
+
|
|
80
|
+
默认值内置在 CLI 自身(而非依赖 ab-agent 等宿主在 spawn 时注入),这样
|
|
81
|
+
``remixmate-cli`` 独立安装(codex / ``npm i -g``)也能开箱即用。
|
|
82
|
+
|
|
83
|
+
base 解析顺序与 CLI / ab-agent 对齐:
|
|
84
|
+
MM_API_BASE_URL(CLI ``src/http.ts`` 约定)
|
|
85
|
+
→ MM_BACKEND_API_URL(ab-agent 约定,向后兼容)
|
|
86
|
+
→ http://localhost:2999/api(本地 dev 兜底)
|
|
87
|
+
路径段固定为 ``/remotionTemplate/registry``(POST,返回 ``{code,msg,data}``)。
|
|
76
88
|
"""
|
|
77
|
-
|
|
89
|
+
base = (
|
|
90
|
+
os.environ.get("MM_API_BASE_URL", "").strip()
|
|
91
|
+
or os.environ.get("MM_BACKEND_API_URL", "").strip()
|
|
92
|
+
or _DEFAULT_API_BASE_URL
|
|
93
|
+
).rstrip("/")
|
|
94
|
+
return f"{base}/remotionTemplate/registry"
|
|
78
95
|
|
|
79
96
|
|
|
80
97
|
def _cache_path_for(url: str) -> str:
|
|
@@ -110,6 +127,60 @@ def _write_cached(cache_file: str, payload: dict) -> None:
|
|
|
110
127
|
raise
|
|
111
128
|
|
|
112
129
|
|
|
130
|
+
# ── PrivToken 解析(env → 凭证库回退)──────────────────────────────────────
|
|
131
|
+
#
|
|
132
|
+
# 与 CLI 的 resolve.ts 共享同一优先级与同一凭证文件路径(Req 7.5)。Python 侧只读
|
|
133
|
+
# 文件存储(不读 keychain —— keychain 由 CLI 写/读,Python 用纯 stdlib 不引依赖)。
|
|
134
|
+
|
|
135
|
+
def _credentials_file_path() -> str:
|
|
136
|
+
"""凭证文件路径,与 CLI credential-store.ts 的 CRED_FILE 保持一致。"""
|
|
137
|
+
return os.path.join(os.path.expanduser("~"), ".config", "remixmate", "credentials.json")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _origin(url: str) -> tuple:
|
|
141
|
+
p = urlparse(url)
|
|
142
|
+
return (p.scheme, p.hostname, p.port)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _priv_token_from_credential_store(url: str) -> str:
|
|
146
|
+
"""从 ~/.config/remixmate/credentials.json 取与 url 同源的 PrivToken。
|
|
147
|
+
|
|
148
|
+
匹配顺序:① 与 registry URL 同源(scheme/host/port)的条目;② 仅有单条目时直接用。
|
|
149
|
+
secret=='keychain' 的条目跳过(Python 无法读取 keychain)。任何异常静默返回空串。
|
|
150
|
+
"""
|
|
151
|
+
try:
|
|
152
|
+
with open(_credentials_file_path(), encoding="utf-8") as f:
|
|
153
|
+
doc = json.load(f)
|
|
154
|
+
except (OSError, json.JSONDecodeError):
|
|
155
|
+
return ""
|
|
156
|
+
creds = doc.get("credentials") if isinstance(doc, dict) else None
|
|
157
|
+
if not isinstance(creds, dict) or not creds:
|
|
158
|
+
return ""
|
|
159
|
+
|
|
160
|
+
target = _origin(url)
|
|
161
|
+
for base_url, entry in creds.items():
|
|
162
|
+
if not isinstance(entry, dict) or entry.get("secret") == "keychain":
|
|
163
|
+
continue
|
|
164
|
+
if _origin(base_url) == target:
|
|
165
|
+
tok = str(entry.get("privToken") or "").strip()
|
|
166
|
+
if tok:
|
|
167
|
+
return tok
|
|
168
|
+
# 单条目兜底:CLI 通常只登录一个后端。
|
|
169
|
+
if len(creds) == 1:
|
|
170
|
+
entry = next(iter(creds.values()))
|
|
171
|
+
if isinstance(entry, dict) and entry.get("secret") != "keychain":
|
|
172
|
+
return str(entry.get("privToken") or "").strip()
|
|
173
|
+
return ""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _resolve_priv_token(url: str) -> str:
|
|
177
|
+
"""PrivToken 解析:环境变量优先,空则回退凭证库(与 CLI 一致的优先级)。"""
|
|
178
|
+
env = os.environ.get("PRIV_TOKEN", "").strip()
|
|
179
|
+
if env:
|
|
180
|
+
return env
|
|
181
|
+
return _priv_token_from_credential_store(url)
|
|
182
|
+
|
|
183
|
+
|
|
113
184
|
def _fetch_http(url: str, timeout: int) -> dict:
|
|
114
185
|
"""执行 HTTP 请求,自动加 X-Priv-Token / If-None-Match 头。
|
|
115
186
|
|
|
@@ -127,7 +198,7 @@ def _fetch_http(url: str, timeout: int) -> dict:
|
|
|
127
198
|
headers = {"Accept": "application/json"}
|
|
128
199
|
if method == "POST":
|
|
129
200
|
headers["Content-Type"] = "application/json"
|
|
130
|
-
if token :=
|
|
201
|
+
if token := _resolve_priv_token(url):
|
|
131
202
|
headers["X-Priv-Token"] = token
|
|
132
203
|
if cached and (etag := cached.get("etag")):
|
|
133
204
|
headers["If-None-Match"] = etag
|
|
@@ -166,10 +237,8 @@ def _fetch_http(url: str, timeout: int) -> dict:
|
|
|
166
237
|
|
|
167
238
|
|
|
168
239
|
def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
|
|
169
|
-
"""先看缓存是否在 TTL
|
|
170
|
-
|
|
171
|
-
2. monorepo 本地 registry.json(仅本地 dev 时存在)
|
|
172
|
-
3. 都没有则抛错
|
|
240
|
+
"""先看缓存是否在 TTL 内;超期则远程拉取。失败时降级到磁盘缓存(同一 URL
|
|
241
|
+
上一份成功响应),无缓存则抛错 —— 不再回退本地文件 / monorepo,保证单一数据源。
|
|
173
242
|
"""
|
|
174
243
|
cache_file = _cache_path_for(url)
|
|
175
244
|
cached = _read_cached(cache_file)
|
|
@@ -181,38 +250,20 @@ def _load_from_url(url: str, ttl: int, timeout: int) -> dict:
|
|
|
181
250
|
try:
|
|
182
251
|
return _fetch_http(url, timeout)
|
|
183
252
|
except Exception as exc:
|
|
184
|
-
#
|
|
253
|
+
# HTTP 故障时降级:有磁盘缓存就继续用(同一数据源的容错,不是第二个源)
|
|
185
254
|
if cached:
|
|
186
255
|
print(
|
|
187
256
|
f"⚠️ registry HTTP 拉取失败,降级使用本地缓存: {exc}",
|
|
188
257
|
file=sys.stderr,
|
|
189
258
|
)
|
|
190
259
|
return cached["data"]
|
|
191
|
-
# 2. 没缓存再尝试 monorepo fallback(本地 dev / 单仓库部署时这条路一定通)
|
|
192
|
-
fallback = _monorepo_fallback_path()
|
|
193
|
-
if os.path.exists(fallback):
|
|
194
|
-
print(
|
|
195
|
-
f"⚠️ registry HTTP 拉取失败且无 HTTP 缓存,降级使用 monorepo registry: {exc}",
|
|
196
|
-
file=sys.stderr,
|
|
197
|
-
)
|
|
198
|
-
return _load_from_file(fallback)
|
|
199
|
-
# 3. 都没有,向上抛
|
|
200
260
|
raise RuntimeError(
|
|
201
|
-
f"registry HTTP
|
|
261
|
+
f"registry HTTP 拉取失败且无可用缓存: {url} ({exc})。"
|
|
262
|
+
f" 请确认 ab-api 可达、VIDEO_TEMPLATE_REGISTRY_URL / MM_API_BASE_URL"
|
|
263
|
+
f" 配置正确,并已设置有效 PRIV_TOKEN。"
|
|
202
264
|
) from exc
|
|
203
265
|
|
|
204
266
|
|
|
205
|
-
def _load_from_file(path: str) -> dict:
|
|
206
|
-
"""从本地文件加载并解析 registry.json。"""
|
|
207
|
-
with open(path, "r", encoding="utf-8") as f:
|
|
208
|
-
return json.load(f)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _truthy(value: str) -> bool:
|
|
212
|
-
"""统一解析布尔型环境变量:1 / true / yes / on(大小写不敏感)。"""
|
|
213
|
-
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
214
|
-
|
|
215
|
-
|
|
216
267
|
def load_registry_data(
|
|
217
268
|
*,
|
|
218
269
|
ttl_seconds: int = _DEFAULT_TTL_SECONDS,
|
|
@@ -220,56 +271,21 @@ def load_registry_data(
|
|
|
220
271
|
) -> dict:
|
|
221
272
|
"""加载 registry 完整文档(含 version / generatedAt / templates)。
|
|
222
273
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
3. 环境变量 VIDEO_TEMPLATE_REGISTRY_URL 指定的 HTTP API(带缓存)
|
|
227
|
-
4. monorepo 默认回退(仅本地 dev 有效)
|
|
228
|
-
|
|
229
|
-
任何来源都返回相同形状的 dict,调用方按 `data.get("templates", [])` 取列表。
|
|
274
|
+
单一数据源:ab-api HTTP。URL 取显式 ``VIDEO_TEMPLATE_REGISTRY_URL``,
|
|
275
|
+
未设置则按 ``MM_API_BASE_URL`` 推导(见 ``_default_registry_url``)。
|
|
276
|
+
带磁盘缓存(TTL + ETag/304),返回 ``data.get("templates", [])`` 形状的 dict。
|
|
230
277
|
"""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
fallback = _monorepo_fallback_path()
|
|
237
|
-
|
|
238
|
-
if prefer_local and os.path.exists(fallback):
|
|
278
|
+
url = os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip()
|
|
279
|
+
if url:
|
|
280
|
+
print(f"[registry_loader] using HTTP registry: {url}", file=sys.stderr)
|
|
281
|
+
else:
|
|
282
|
+
url = _default_registry_url()
|
|
239
283
|
print(
|
|
240
|
-
f"[registry_loader]
|
|
284
|
+
f"[registry_loader] VIDEO_TEMPLATE_REGISTRY_URL unset; using derived"
|
|
285
|
+
f" default URL: {url}",
|
|
241
286
|
file=sys.stderr,
|
|
242
287
|
)
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if url := os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip():
|
|
246
|
-
if prefer_local:
|
|
247
|
-
# 走到这里说明开启了 PREFER_LOCAL 但 monorepo 文件不存在 —— 例如容器里
|
|
248
|
-
# 误启用此开关。给出明确告警,避免使用者以为还在用本地源码。
|
|
249
|
-
print(
|
|
250
|
-
f"[registry_loader] PREFER_LOCAL=1 but monorepo registry not found"
|
|
251
|
-
f" ({fallback}), falling back to HTTP: {url}",
|
|
252
|
-
file=sys.stderr,
|
|
253
|
-
)
|
|
254
|
-
else:
|
|
255
|
-
print(f"[registry_loader] using HTTP registry: {url}", file=sys.stderr)
|
|
256
|
-
return _load_from_url(url, ttl=ttl_seconds, timeout=http_timeout)
|
|
257
|
-
|
|
258
|
-
# 第 ③ 级:monorepo 相对路径兜底。仅在该文件【实际存在】时生效
|
|
259
|
-
# (私有 monorepo 本地源码可见的场景)。独立开源仓库 clone / npm 安装
|
|
260
|
-
# 后该路径不存在,这里静默跳过,不抛错、不中断,落到下方的可理解失败。
|
|
261
|
-
if os.path.exists(fallback):
|
|
262
|
-
print(
|
|
263
|
-
f"[registry_loader] using monorepo registry (default fallback): {fallback}",
|
|
264
|
-
file=sys.stderr,
|
|
265
|
-
)
|
|
266
|
-
return _load_from_file(fallback)
|
|
267
|
-
|
|
268
|
-
raise RuntimeError(
|
|
269
|
-
"找不到模板 registry。请设置 VIDEO_TEMPLATE_REGISTRY_URL(ab-api HTTP 接口)"
|
|
270
|
-
" 并配置有效的 PRIV_TOKEN,或用 VIDEO_TEMPLATE_REGISTRY 指向一份本地"
|
|
271
|
-
" registry.json。(monorepo 内本地源码可见时会自动走 template-library 兜底。)"
|
|
272
|
-
)
|
|
288
|
+
return _load_from_url(url, ttl=ttl_seconds, timeout=http_timeout)
|
|
273
289
|
|
|
274
290
|
|
|
275
291
|
# ════════════════════════════════════════════════════════════════════════════
|
|
@@ -291,6 +307,11 @@ _DEFAULT_STATUS = "stable"
|
|
|
291
307
|
_BETA_GATE_ENV = "ENABLE_BETA_TEMPLATES"
|
|
292
308
|
|
|
293
309
|
|
|
310
|
+
def _truthy(value: str) -> bool:
|
|
311
|
+
"""统一解析布尔型环境变量:1 / true / yes / on(大小写不敏感)。"""
|
|
312
|
+
return value.strip().lower() in ("1", "true", "yes", "on")
|
|
313
|
+
|
|
314
|
+
|
|
294
315
|
def _truthy_env(name: str) -> bool:
|
|
295
316
|
return _truthy(os.environ.get(name, ""))
|
|
296
317
|
|
|
@@ -136,6 +136,18 @@ def get_job(job_id: int, priv_token: str) -> dict:
|
|
|
136
136
|
return _post("/renderJob/get", {"jobId": job_id}, priv_token)
|
|
137
137
|
|
|
138
138
|
|
|
139
|
+
def presign_template_source(template_key: str, priv_token: str) -> dict:
|
|
140
|
+
"""
|
|
141
|
+
为已注册私有模板的源码包申请临时 GET 下载 URL(渲染取包用)。
|
|
142
|
+
|
|
143
|
+
调 ab-api /remotionTemplate/presignSource,返回 { tarballUrl, sourceOssKey, version }。
|
|
144
|
+
ab-render 用 tarballUrl 走 /renderDraft 动态 bundle 渲染。
|
|
145
|
+
|
|
146
|
+
注意:presignSource 端点不在 /renderJob 下,路径以 / 开头直接拼到 _api_base。
|
|
147
|
+
"""
|
|
148
|
+
return _post("/remotionTemplate/presignSource", {"templateKey": template_key}, priv_token)
|
|
149
|
+
|
|
150
|
+
|
|
139
151
|
# ─── CLI self-test ────────────────────────────────────────────────────────────
|
|
140
152
|
|
|
141
153
|
if __name__ == "__main__":
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"category": "authoring",
|
|
6
6
|
"title": "Video Template Registry",
|
|
7
7
|
"description": "List all available video templates (templateId / name / aspect ratio / style tags). Template-to-DSL binding is no longer exposed as a separate step — once prepare_video_assets receives a template_id it runs the full DSL→RenderPlan pipeline internally.",
|
|
8
|
-
"envVars": ["PRIV_TOKEN", "
|
|
8
|
+
"envVars": ["PRIV_TOKEN", "VIDEO_TEMPLATE_REGISTRY_URL", "MM_API_BASE_URL", "VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD"],
|
|
9
9
|
"entry": { "type": "python", "scriptPath": "scripts/list_templates.py" },
|
|
10
10
|
"parameters": {
|
|
11
11
|
"type": "object",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V13",
|
|
6
6
|
"skillDescription": "视频模板仓库(列表查询)。存储所有视频模板定义,对外只暴露「列出可用模板」一个能力;DSL→TemplateBinding 的绑定逻辑已内嵌进 prepare-video-assets,不再作为独立步骤暴露。同时是跨 skill 共享 Python 库(registry_loader / match_template / template_paths / video_dsl 等)的存放位置。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 查看可用模板、列出所有模板"
|
|
7
7
|
}
|