@remixmate/cli 0.9.61 → 0.9.63
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/dist/manifest.json +11 -5
- package/dist/project/host.d.ts +1 -1
- package/dist/project/host.js +1 -1
- package/dist/project/resolve.d.ts +1 -1
- package/dist/project/resolve.js +1 -1
- package/dist/project/store.d.ts +1 -1
- package/dist/project/store.js +1 -1
- package/dist/project/take.d.ts +1 -1
- package/dist/project/take.js +1 -1
- package/dist/project/trace.d.ts +1 -1
- package/dist/project/trace.js +1 -1
- package/package.json +3 -2
- package/skills/gen-script/scripts/gen_script.py +1 -1
- package/skills/render-video/scripts/_video_probe.py +55 -0
- package/skills/render-video/scripts/render_video.py +16 -2
- package/skills/template-registry/SKILL.md +15 -2
- package/skills/template-registry/scripts/list_templates.py +107 -18
- package/skills/template-registry/scripts/match_template.py +1 -1
- package/skills/template-registry/scripts/registry_loader.py +13 -6
- package/skills/template-registry/scripts/render_job_client.py +1 -1
- package/skills/template-registry/scripts/showcase_loader.py +175 -0
- package/skills/template-registry/skill.json +2 -1
- package/skills/template-registry/version.json +1 -1
- package/skills/template-registry/video_dsl/schema/video-dsl-v1alpha1.json +1 -1
- package/skills/web-screenshot/scripts/_media_upload.py +1 -1
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-
|
|
3
|
+
"version": "0.9.63",
|
|
4
|
+
"generatedAt": "2026-10-04T06:46:47.501Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -940,7 +940,8 @@
|
|
|
940
940
|
"title": "Video Template Registry",
|
|
941
941
|
"summary": "List all available video templates (templateId / name / aspect ratio / style tags), or read one template's full definition with template_id. Listing returns summaries; pass template_id to get that template's authoring contract (llmHint / customPayloadSchema / slotMapping / variants). 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.",
|
|
942
942
|
"triggers": [
|
|
943
|
-
"View available templates / list every template"
|
|
943
|
+
"View available templates / list every template",
|
|
944
|
+
"Watch a template's sample videos / open its detail page"
|
|
944
945
|
],
|
|
945
946
|
"entry": {
|
|
946
947
|
"type": "python",
|
|
@@ -993,7 +994,11 @@
|
|
|
993
994
|
},
|
|
994
995
|
"json_output": {
|
|
995
996
|
"type": "boolean",
|
|
996
|
-
"description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }."
|
|
997
|
+
"description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }. When the backend serves samples, list summaries carry showcases { count, pageUrl, items } and the template_id view carries a top-level showcases map."
|
|
998
|
+
},
|
|
999
|
+
"open": {
|
|
1000
|
+
"type": "string",
|
|
1001
|
+
"description": "Print the web detail page URL of <templateId> (playable samples, layouts, copy-ready prompt) and open it in the browser when run from an interactive terminal. Relay the printed URL to the user."
|
|
997
1002
|
}
|
|
998
1003
|
},
|
|
999
1004
|
"required": []
|
|
@@ -1009,7 +1014,8 @@
|
|
|
1009
1014
|
"filter_aspect",
|
|
1010
1015
|
"filter_language",
|
|
1011
1016
|
"include_beta",
|
|
1012
|
-
"json_output"
|
|
1017
|
+
"json_output",
|
|
1018
|
+
"open"
|
|
1013
1019
|
],
|
|
1014
1020
|
"hidden": []
|
|
1015
1021
|
}
|
package/dist/project/host.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* which would silently group takes wrong). A host that hands us nothing simply
|
|
16
16
|
* degrades one rung; see resolveTakeKey().
|
|
17
17
|
*
|
|
18
|
-
* See docs/cli-project-binding-design.md §5.1 and appendix A.
|
|
18
|
+
* See docs/product/cli/cli-project-binding-design.md §5.1 and appendix A.
|
|
19
19
|
*/
|
|
20
20
|
export interface HostSessionProvider {
|
|
21
21
|
/** Host identifier; becomes the namespace of the grouping key. */
|
package/dist/project/host.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* which would silently group takes wrong). A host that hands us nothing simply
|
|
16
16
|
* degrades one rung; see resolveTakeKey().
|
|
17
17
|
*
|
|
18
|
-
* See docs/cli-project-binding-design.md §5.1 and appendix A.
|
|
18
|
+
* See docs/product/cli/cli-project-binding-design.md §5.1 and appendix A.
|
|
19
19
|
*/
|
|
20
20
|
/**
|
|
21
21
|
* Adding a host is a data-only change: append a row, touch nothing else.
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* remotes, monorepos and repos with no origin — each misfire silently filing
|
|
16
16
|
* output under a project the user never made.
|
|
17
17
|
*
|
|
18
|
-
* See docs/cli-project-binding-design.md §3.
|
|
18
|
+
* See docs/product/cli/cli-project-binding-design.md §3.
|
|
19
19
|
*/
|
|
20
20
|
import { type HttpContext } from '../http.js';
|
|
21
21
|
export interface ResolvedProject {
|
package/dist/project/resolve.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* remotes, monorepos and repos with no origin — each misfire silently filing
|
|
16
16
|
* output under a project the user never made.
|
|
17
17
|
*
|
|
18
|
-
* See docs/cli-project-binding-design.md §3.
|
|
18
|
+
* See docs/product/cli/cli-project-binding-design.md §3.
|
|
19
19
|
*/
|
|
20
20
|
import { mmPost } from '../http.js';
|
|
21
21
|
import { readBinding } from './store.js';
|
package/dist/project/store.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* ~/WorkBuddy/<timestamp>) would never hit a byPath entry, so a
|
|
15
15
|
* cwd-only design silently degrades to nothing for them.
|
|
16
16
|
*
|
|
17
|
-
* See docs/cli-project-binding-design.md §3.
|
|
17
|
+
* See docs/product/cli/cli-project-binding-design.md §3.
|
|
18
18
|
*/
|
|
19
19
|
export declare const CONFIG_DIR: string;
|
|
20
20
|
export declare const PROJECTS_FILE: string;
|
package/dist/project/store.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* ~/WorkBuddy/<timestamp>) would never hit a byPath entry, so a
|
|
15
15
|
* cwd-only design silently degrades to nothing for them.
|
|
16
16
|
*
|
|
17
|
-
* See docs/cli-project-binding-design.md §3.
|
|
17
|
+
* See docs/product/cli/cli-project-binding-design.md §3.
|
|
18
18
|
*/
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
import { dirname, join, resolve } from 'node:path';
|
package/dist/project/take.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* separate CLI processes, sometimes concurrent, and a local cache would have to
|
|
15
15
|
* solve staleness and write races that a unique index solves for free.
|
|
16
16
|
*
|
|
17
|
-
* See docs/cli-project-binding-design.md §5.
|
|
17
|
+
* See docs/product/cli/cli-project-binding-design.md §5.
|
|
18
18
|
*/
|
|
19
19
|
import { type HttpContext } from '../http.js';
|
|
20
20
|
export interface EnsuredTake {
|
package/dist/project/take.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* separate CLI processes, sometimes concurrent, and a local cache would have to
|
|
15
15
|
* solve staleness and write races that a unique index solves for free.
|
|
16
16
|
*
|
|
17
|
-
* See docs/cli-project-binding-design.md §5.
|
|
17
|
+
* See docs/product/cli/cli-project-binding-design.md §5.
|
|
18
18
|
*/
|
|
19
19
|
import { mmPost } from '../http.js';
|
|
20
20
|
import { resolveTakeKey } from './host.js';
|
package/dist/project/trace.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* reply — those live in the host's own transcript. So this module reports the
|
|
14
14
|
* one thing it genuinely observes: the invocation itself.
|
|
15
15
|
*
|
|
16
|
-
* See docs/cli-session-record-design.md §5.
|
|
16
|
+
* See docs/product/cli/cli-session-record-design.md §5.
|
|
17
17
|
*/
|
|
18
18
|
import { type HttpContext } from '../http.js';
|
|
19
19
|
import type { ParsedArgs } from '../argv.js';
|
package/dist/project/trace.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* reply — those live in the host's own transcript. So this module reports the
|
|
14
14
|
* one thing it genuinely observes: the invocation itself.
|
|
15
15
|
*
|
|
16
|
-
* See docs/cli-session-record-design.md §5.
|
|
16
|
+
* See docs/product/cli/cli-session-record-design.md §5.
|
|
17
17
|
*/
|
|
18
18
|
import os from 'node:os';
|
|
19
19
|
import { cleanDiagnosticText, outcomeOf } from '../diagnostics.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.63",
|
|
4
4
|
"description": "AI media generation skills for Claude Code / Codex — 12 skills covering image, video, voice, digital human, web screenshot, web recording, script, template registry, rendering, Jianying export, and video deconstruction.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"test:payload-hydration": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-payload-hydration.py",
|
|
38
38
|
"test:sentence-alignment": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-sentence-alignment.py",
|
|
39
39
|
"test:authoring-budget": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-authoring-budget.py",
|
|
40
|
-
"test:
|
|
40
|
+
"test:showcase-links": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-showcase-links.py",
|
|
41
|
+
"test:unit": "npm run -s test:narration-speed && npm run -s test:template-look && npm run -s test:narration-tts && npm run -s test:render-resume && npm run -s test:required-props && npm run -s test:countdown && npm run -s test:prepare-contracts && npm run -s test:image-sequence && npm run -s test:image-prompt-compose && npm run -s test:page-contracts && npm run -s test:declared-keys && npm run -s test:payload-hydration && npm run -s test:sentence-alignment && npm run -s test:authoring-budget && npm run -s test:showcase-links",
|
|
41
42
|
"integration": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/integration/regress.py",
|
|
42
43
|
"integration:render": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/integration/render-template-check.py"
|
|
43
44
|
},
|
|
@@ -1477,7 +1477,7 @@ def build_dsl(
|
|
|
1477
1477
|
|
|
1478
1478
|
scene_images 是用户**自带的配图**(visual-overlay 模板专用),按顺序占位:第 i 张
|
|
1479
1479
|
图给第 i 个场景,没被占到的场景照常走 gen-image 补图。这条规则不是随便定的——
|
|
1480
|
-
见 docs/asset-annotation-design.md §6:另外两种立场(模型自由挑选 / 严格一一对应)
|
|
1480
|
+
见 docs/product/creation/asset-annotation-design.md §6:另外两种立场(模型自由挑选 / 严格一一对应)
|
|
1481
1481
|
都会让"只传两张图"要么失去可预期性,要么直接跑不通。
|
|
1482
1482
|
|
|
1483
1483
|
speed 是旁白语速倍率,落在 global.narration.speed(不给 = 1.0)。它**不写进
|
|
@@ -175,3 +175,58 @@ def probe_video_duration(url: str) -> Optional[float]:
|
|
|
175
175
|
return content_length_estimate
|
|
176
176
|
|
|
177
177
|
return None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def probe_video_meta(url: str, timeout: float = 20.0) -> dict:
|
|
181
|
+
"""Probe a remote video's pixel size and duration with ffprobe (reads the URL directly).
|
|
182
|
+
|
|
183
|
+
Returns ``{"width": int, "height": int, "duration_ms": int}`` with only the fields it
|
|
184
|
+
could read; ``{}`` when ffprobe is missing or the probe fails. Callers treat every
|
|
185
|
+
field as optional.
|
|
186
|
+
|
|
187
|
+
为什么要:数字人盒子的宽高比缺省按 3:4 算,而克隆素材多是 9:16 左右的竖版
|
|
188
|
+
(实测 720×1264)。资产上没有宽高时,模板用 3:4 的盒子 object-fit: cover 裁掉
|
|
189
|
+
人物的头顶和下半身(conv_XKTQxEBLdZy3i_jIB7IG)。
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
result = subprocess.run(
|
|
193
|
+
[
|
|
194
|
+
"ffprobe", "-v", "quiet", "-print_format", "json",
|
|
195
|
+
"-select_streams", "v:0", "-show_streams", "-show_format", url,
|
|
196
|
+
],
|
|
197
|
+
capture_output=True, text=True, timeout=timeout, stdin=subprocess.DEVNULL,
|
|
198
|
+
)
|
|
199
|
+
except (FileNotFoundError, OSError, subprocess.TimeoutExpired) as e:
|
|
200
|
+
_log(f" ⚠️ video meta probe unavailable: {type(e).__name__}")
|
|
201
|
+
return {}
|
|
202
|
+
if result.returncode != 0:
|
|
203
|
+
return {}
|
|
204
|
+
try:
|
|
205
|
+
info = json.loads(result.stdout or "{}")
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
meta: dict = {}
|
|
210
|
+
streams = info.get("streams") or []
|
|
211
|
+
if streams:
|
|
212
|
+
s = streams[0]
|
|
213
|
+
w, h = s.get("width"), s.get("height")
|
|
214
|
+
if isinstance(w, int) and isinstance(h, int) and w > 0 and h > 0:
|
|
215
|
+
# 手机素材常带 90° / 270° 旋转元数据:播放器按旋转后的方向显示。
|
|
216
|
+
rotation = 0
|
|
217
|
+
for side in s.get("side_data_list") or []:
|
|
218
|
+
if isinstance(side.get("rotation"), (int, float)):
|
|
219
|
+
rotation = int(side["rotation"])
|
|
220
|
+
tags_rot = (s.get("tags") or {}).get("rotate")
|
|
221
|
+
if tags_rot and str(tags_rot).lstrip("-").isdigit():
|
|
222
|
+
rotation = int(tags_rot)
|
|
223
|
+
if abs(rotation) % 180 == 90:
|
|
224
|
+
w, h = h, w
|
|
225
|
+
meta["width"], meta["height"] = w, h
|
|
226
|
+
try:
|
|
227
|
+
dur = float((info.get("format") or {}).get("duration") or 0)
|
|
228
|
+
if dur > 0:
|
|
229
|
+
meta["duration_ms"] = int(round(dur * 1000))
|
|
230
|
+
except (TypeError, ValueError):
|
|
231
|
+
pass
|
|
232
|
+
return meta
|
|
@@ -1182,7 +1182,16 @@ def resolve_asset_digital_human(asset: dict, private_token: str, timeout: int) -
|
|
|
1182
1182
|
for line in reversed(result.stdout.strip().split("\n")):
|
|
1183
1183
|
line = line.strip()
|
|
1184
1184
|
if line.startswith("http"):
|
|
1185
|
-
|
|
1185
|
+
out = {"status": "generated", "url": line}
|
|
1186
|
+
# 宽高 / 时长写回资产:模板按它定盒子比例(缺省 3:4 会裁人),
|
|
1187
|
+
# 按时长决定 onEnd=hide/loop 何时生效。探测失败不影响生成结果。
|
|
1188
|
+
from _video_probe import probe_video_meta
|
|
1189
|
+
meta = probe_video_meta(line)
|
|
1190
|
+
if meta.get("width") and meta.get("height"):
|
|
1191
|
+
out["width"], out["height"] = meta["width"], meta["height"]
|
|
1192
|
+
if meta.get("duration_ms"):
|
|
1193
|
+
out["duration_ms"] = meta["duration_ms"]
|
|
1194
|
+
return out
|
|
1186
1195
|
return {"status": "failed", "error": result.stderr.strip()[:1000]}
|
|
1187
1196
|
except subprocess.TimeoutExpired:
|
|
1188
1197
|
return {"status": "failed", "error": f"Digital human generation timed out after {dh_timeout}s"}
|
|
@@ -1263,6 +1272,11 @@ def _resolve_single_asset(
|
|
|
1263
1272
|
asset["generatedBy"]["completedAt"] = now_iso()
|
|
1264
1273
|
if result.get("audio_length_ms") is not None:
|
|
1265
1274
|
asset["duration"] = result["audio_length_ms"]
|
|
1275
|
+
# 视频类资产(数字人):ms 时长 + 像素尺寸,模板的 ResolvedAsset 契约同名字段。
|
|
1276
|
+
if result.get("duration_ms") is not None:
|
|
1277
|
+
asset["duration"] = result["duration_ms"]
|
|
1278
|
+
if result.get("width") and result.get("height"):
|
|
1279
|
+
asset["width"], asset["height"] = result["width"], result["height"]
|
|
1266
1280
|
if result.get("subtitles"):
|
|
1267
1281
|
asset["ttsSubtitles"] = result["subtitles"]
|
|
1268
1282
|
# Propagate structured-narration metadata onto the resolved
|
|
@@ -2170,7 +2184,7 @@ def render_with_remote_api(
|
|
|
2170
2184
|
if source_oss_key:
|
|
2171
2185
|
# 只传 templateKey:由 ab-render 带本次的 X-Priv-Token 向 ab-api 换源码包 URL
|
|
2172
2186
|
# 并自行下载(所有权校验在 ab-api)。源码包 URL 不经过客户端 ——
|
|
2173
|
-
# 见 ab-platform/docs/template-source-protection-design.md §4.3。
|
|
2187
|
+
# 见 ab-platform/docs/templates/template-source-protection-design.md §4.3。
|
|
2174
2188
|
payload = {
|
|
2175
2189
|
"templateKey": template_id,
|
|
2176
2190
|
"inputProps": input_props,
|
|
@@ -9,6 +9,7 @@ description: |
|
|
|
9
9
|
Note: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally.
|
|
10
10
|
triggers:
|
|
11
11
|
- View available templates / list every template
|
|
12
|
+
- Watch a template's sample videos / open its detail page
|
|
12
13
|
---
|
|
13
14
|
|
|
14
15
|
# Video Template Registry & Binding Skill
|
|
@@ -48,7 +49,8 @@ The skill itself only reads the registry; whether a token is needed depends on t
|
|
|
48
49
|
|---------|-------------|---------|
|
|
49
50
|
| `VIDEO_TEMPLATE_REGISTRY_URL` | ab-api endpoint returning the registry (the single source of truth). | derived from `MM_API_BASE_URL` |
|
|
50
51
|
| `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.ai/api` |
|
|
51
|
-
| `PRIV_TOKEN` | Sent as `X-Priv-Token`
|
|
52
|
+
| `PRIV_TOKEN` | Sent as `X-Priv-Token` to the registry and sample endpoints. Injected by the `remixmate` runner after `remixmate login`; the Python side reads only this env var. | unset |
|
|
53
|
+
| `VIDEO_TEMPLATE_SHOWCASE_URL` | Sample endpoint override. Default `<MM_API_BASE_URL>/templateShowcase/listByTemplate`; not called when the registry is a `file://` URL. | derived |
|
|
52
54
|
| `VIDEO_TEMPLATE_REGISTRY_HTTP_METHOD` | `POST` (default) or `GET`. POST shape matches ab-api `{code,msg,data}`. | `POST` |
|
|
53
55
|
|
|
54
56
|
Resolution (single source — see `scripts/registry_loader.py` for the canonical implementation):
|
|
@@ -74,6 +76,16 @@ the registry is loaded **only** from the ab-api HTTP endpoint — explicit `VIDE
|
|
|
74
76
|
remixmate template-registry --list-templates
|
|
75
77
|
```
|
|
76
78
|
|
|
79
|
+
### Show templates to the user
|
|
80
|
+
|
|
81
|
+
When the user asks to see / browse / pick templates, they decide by *watching* samples, not by reading definitions. The list adds, per template, the number of samples and a link to its web detail page (playable samples, layouts, a copy-ready prompt) whenever the backend serves them (`↳ samples: N · <url>` in the table; `showcases.count` / `showcases.pageUrl` in JSON).
|
|
82
|
+
|
|
83
|
+
- Present each template as: name, aspect ratios, sample count, and its `pageUrl` as a clickable Markdown link. Keep the URL exactly as printed.
|
|
84
|
+
- When the user wants to see what one template looks like, give its link, or run `remixmate template-registry --open <templateId>` (prints the URL; opens the browser only from an interactive terminal — relay the printed URL either way).
|
|
85
|
+
- If the host renders images, you may add a sample's `coverImage` as a Markdown image next to the link; the link stays the primary content.
|
|
86
|
+
- No sample info (older backend, or a template with no samples yet) is not an error: show the template without it.
|
|
87
|
+
- The detail page's prompt is meant to be pasted back into this chat; treat it like any user request.
|
|
88
|
+
|
|
77
89
|
### Build a binding locally (debug only, not part of the render pipeline)
|
|
78
90
|
|
|
79
91
|
```python
|
|
@@ -99,7 +111,8 @@ print(json.dumps(binding, ensure_ascii=False, indent=2))
|
|
|
99
111
|
| `--filter-aspect` | Keep only templates declaring this aspect ratio (e.g. `9:16`). | none |
|
|
100
112
|
| `--filter-language` | Keep only templates whose `contentLanguage` includes this code (`zh`/`en`); language-agnostic templates always show. | none |
|
|
101
113
|
| `--include-beta` | Also show `status: beta` templates (same effect as `ENABLE_BETA_TEMPLATES=1`). | off |
|
|
102
|
-
| `--json-output` | List mode: emit `{ "templates": [...] }` summaries (or full definitions with `--full`) instead of the table. Detail mode (`--template-id`) is always compact JSON. | off |
|
|
114
|
+
| `--json-output` | List mode: emit `{ "templates": [...] }` summaries (or full definitions with `--full`) instead of the table. Detail mode (`--template-id`) is always compact JSON. Summaries carry `showcases: { count, pageUrl, items }` and detail mode a top-level `showcases` map when the backend serves samples (absent otherwise). | off |
|
|
115
|
+
| `--open <id>` | Print the template's web detail page URL (samples, layouts) and open the browser when run from an interactive terminal. Exits 1 when the backend has no link for it. Cannot be combined with `--template-id`. | — |
|
|
103
116
|
|
|
104
117
|
### List vs. detail
|
|
105
118
|
|
|
@@ -68,6 +68,13 @@ Flags:
|
|
|
68
68
|
--include-beta also show beta templates (else stable-only)
|
|
69
69
|
--json-output emit {"templates": [...]} or {"examples": [...]}
|
|
70
70
|
instead of a table
|
|
71
|
+
--open <id> print the template's web detail page URL
|
|
72
|
+
(samples, layouts); also opens the browser
|
|
73
|
+
when stdout is an interactive terminal
|
|
74
|
+
|
|
75
|
+
Samples (showcase_loader.py): list and detail views add each template's sample
|
|
76
|
+
count and web detail-page link when ab-api serves them; when it does not, the
|
|
77
|
+
views are unchanged apart from one stderr warning.
|
|
71
78
|
"""
|
|
72
79
|
|
|
73
80
|
from __future__ import annotations
|
|
@@ -88,6 +95,12 @@ from registry_loader import ( # noqa: E402
|
|
|
88
95
|
get_template,
|
|
89
96
|
list_templates as load_visible_templates,
|
|
90
97
|
)
|
|
98
|
+
from showcase_loader import load_showcases, summarize_group # noqa: E402
|
|
99
|
+
|
|
100
|
+
# Samples per template in list views (table count + JSON items). The detail
|
|
101
|
+
# view (--template-id) returns up to _DETAIL_SHOWCASES.
|
|
102
|
+
_LIST_SHOWCASES = 3
|
|
103
|
+
_DETAIL_SHOWCASES = 20
|
|
91
104
|
|
|
92
105
|
# Summary llmHint budget: enough to tell templates apart when choosing, far
|
|
93
106
|
# short of the multi-KB authoring contract (read that with --template-id).
|
|
@@ -168,6 +181,41 @@ def _summarize(tpl: dict) -> dict:
|
|
|
168
181
|
return summary
|
|
169
182
|
|
|
170
183
|
|
|
184
|
+
def _open_in_browser(url: str) -> None:
|
|
185
|
+
"""Best-effort browser open after the URL has already been printed.
|
|
186
|
+
|
|
187
|
+
Only when stdout is a TTY (a human at a terminal) and REMIXMATE_NO_BROWSER
|
|
188
|
+
is unset: agent hosts run commands through pipes / sandboxes, where the
|
|
189
|
+
printed URL is what they relay to the user. Failures are swallowed.
|
|
190
|
+
"""
|
|
191
|
+
if not sys.stdout.isatty() or os.environ.get("REMIXMATE_NO_BROWSER", "").strip():
|
|
192
|
+
return
|
|
193
|
+
try:
|
|
194
|
+
import webbrowser
|
|
195
|
+
|
|
196
|
+
webbrowser.open(url, new=2)
|
|
197
|
+
except Exception: # noqa: BLE001
|
|
198
|
+
pass
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _report_unknown_ids(missing: list[str], templates: list[dict]) -> None:
|
|
202
|
+
for tid in missing:
|
|
203
|
+
# Distinguish "gated out by status" from "does not exist" —
|
|
204
|
+
# otherwise a beta template reads as a typo and the caller
|
|
205
|
+
# retries the id instead of passing --include-beta.
|
|
206
|
+
gated = get_template(tid, include_all_statuses=True)
|
|
207
|
+
if gated is not None:
|
|
208
|
+
print(
|
|
209
|
+
f"❌ Template '{tid}' exists but its status is "
|
|
210
|
+
f"'{_status_of(gated)}' — pass --include-beta to read it.",
|
|
211
|
+
file=sys.stderr,
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
print(f"❌ Unknown template id: '{tid}'", file=sys.stderr)
|
|
215
|
+
known = ", ".join(sorted(str(t.get("templateId")) for t in templates))
|
|
216
|
+
print(f" Available ids: {known}", file=sys.stderr)
|
|
217
|
+
|
|
218
|
+
|
|
171
219
|
def _detail_hint(template_id: str = "<templateId>") -> str:
|
|
172
220
|
return (
|
|
173
221
|
f"Summaries only. Run --template-id {template_id} --json-output for one "
|
|
@@ -337,8 +385,14 @@ def main() -> None:
|
|
|
337
385
|
ap.add_argument("--filter-language", help="Keep templates tagged with this content language (zh|en|...).")
|
|
338
386
|
ap.add_argument("--include-beta", action="store_true", help="Also show beta templates (default: stable only).")
|
|
339
387
|
ap.add_argument("--json-output", action="store_true", help='Emit JSON ({"templates":[...]} or {"examples":[...]}) instead of a table.')
|
|
388
|
+
ap.add_argument("--open", dest="open_id", metavar="ID",
|
|
389
|
+
help="Print the web detail page URL of this template (samples, layouts) "
|
|
390
|
+
"and open it in the browser when run from an interactive terminal.")
|
|
340
391
|
args = ap.parse_args()
|
|
341
392
|
|
|
393
|
+
if args.open_id and (args.template_ids or args.list_examples or args.list_templates):
|
|
394
|
+
ap.error("--open cannot be combined with --template-id / --list-examples / --list-templates")
|
|
395
|
+
|
|
342
396
|
# --include-beta flips registry_loader's documented status gate via env, so
|
|
343
397
|
# the CLI and in-process callers share one gating implementation.
|
|
344
398
|
if args.include_beta:
|
|
@@ -404,13 +458,35 @@ def main() -> None:
|
|
|
404
458
|
print(f"❌ {exc}", file=sys.stderr)
|
|
405
459
|
sys.exit(1)
|
|
406
460
|
|
|
461
|
+
by_id = {t.get("templateId"): t for t in templates}
|
|
462
|
+
|
|
463
|
+
# ----- open mode (--open) -------------------------------------------------
|
|
464
|
+
#
|
|
465
|
+
# The URL goes to stdout first, unconditionally: in Codex / Claude Code the
|
|
466
|
+
# command runs through a pipe and the printed link is what the agent relays.
|
|
467
|
+
if args.open_id:
|
|
468
|
+
if args.open_id not in by_id:
|
|
469
|
+
_report_unknown_ids([args.open_id], templates)
|
|
470
|
+
sys.exit(1)
|
|
471
|
+
groups = load_showcases([args.open_id], 1)
|
|
472
|
+
page_url = (groups or {}).get(args.open_id, {}).get("pageUrl")
|
|
473
|
+
if not page_url:
|
|
474
|
+
print(
|
|
475
|
+
"❌ No web detail page link for this template: the backend does not "
|
|
476
|
+
"serve sample links yet, or its site address is not configured.",
|
|
477
|
+
file=sys.stderr,
|
|
478
|
+
)
|
|
479
|
+
sys.exit(1)
|
|
480
|
+
print(page_url)
|
|
481
|
+
_open_in_browser(page_url)
|
|
482
|
+
return
|
|
483
|
+
|
|
407
484
|
# ----- detail mode (--template-id) ---------------------------------------
|
|
408
485
|
#
|
|
409
486
|
# Explicit ids win over --filter-*: the caller already chose, and silently
|
|
410
487
|
# returning nothing because a stale filter excluded the pick would read as
|
|
411
488
|
# "template does not exist".
|
|
412
489
|
if args.template_ids:
|
|
413
|
-
by_id = {t.get("templateId"): t for t in templates}
|
|
414
490
|
picked: list[dict] = []
|
|
415
491
|
missing: list[str] = []
|
|
416
492
|
for tid in args.template_ids:
|
|
@@ -420,21 +496,7 @@ def main() -> None:
|
|
|
420
496
|
else:
|
|
421
497
|
picked.append(tpl)
|
|
422
498
|
if missing:
|
|
423
|
-
|
|
424
|
-
# Distinguish "gated out by status" from "does not exist" —
|
|
425
|
-
# otherwise a beta template reads as a typo and the caller
|
|
426
|
-
# retries the id instead of passing --include-beta.
|
|
427
|
-
gated = get_template(tid, include_all_statuses=True)
|
|
428
|
-
if gated is not None:
|
|
429
|
-
print(
|
|
430
|
-
f"❌ Template '{tid}' exists but its status is "
|
|
431
|
-
f"'{_status_of(gated)}' — pass --include-beta to read it.",
|
|
432
|
-
file=sys.stderr,
|
|
433
|
-
)
|
|
434
|
-
else:
|
|
435
|
-
print(f"❌ Unknown template id: '{tid}'", file=sys.stderr)
|
|
436
|
-
known = ", ".join(sorted(str(t.get("templateId")) for t in templates))
|
|
437
|
-
print(f" Available ids: {known}", file=sys.stderr)
|
|
499
|
+
_report_unknown_ids(missing, templates)
|
|
438
500
|
sys.exit(1)
|
|
439
501
|
if args.slide_ids:
|
|
440
502
|
wanted = [s for s in args.slide_ids if s != _SLIDE_EXPAND_ALL]
|
|
@@ -457,7 +519,15 @@ def main() -> None:
|
|
|
457
519
|
# tool call: indent=2 nearly doubled the size (html-slide 37 K → 65 K
|
|
458
520
|
# chars), which alone pushed the plain call past the tool-result cap and
|
|
459
521
|
# got it spilled to a file the agent cannot read. Humans can `| jq`.
|
|
460
|
-
|
|
522
|
+
detail: dict = {"templates": picked}
|
|
523
|
+
# Samples sit beside the definitions, never inside them: the definition
|
|
524
|
+
# is the template's authoring contract, samples are web content.
|
|
525
|
+
groups = load_showcases([str(t.get("templateId")) for t in picked], _DETAIL_SHOWCASES)
|
|
526
|
+
if groups is not None:
|
|
527
|
+
detail["showcases"] = {
|
|
528
|
+
tid: summarize_group(g, _DETAIL_SHOWCASES) for tid, g in groups.items()
|
|
529
|
+
}
|
|
530
|
+
print(json.dumps(detail, ensure_ascii=False))
|
|
461
531
|
return
|
|
462
532
|
|
|
463
533
|
visible = [
|
|
@@ -480,9 +550,18 @@ def main() -> None:
|
|
|
480
550
|
sys.exit(1)
|
|
481
551
|
print(payload)
|
|
482
552
|
return
|
|
553
|
+
groups = load_showcases([str(t.get("templateId")) for t in visible], _LIST_SHOWCASES) if visible else {}
|
|
554
|
+
summaries = []
|
|
555
|
+
for t in visible:
|
|
556
|
+
s = _summarize(t)
|
|
557
|
+
# Absent (not null) when the endpoint is unavailable, so a consumer
|
|
558
|
+
# never mistakes "unknown" for "zero samples".
|
|
559
|
+
if groups is not None and t.get("templateId") in groups:
|
|
560
|
+
s["showcases"] = summarize_group(groups[t["templateId"]], _LIST_SHOWCASES)
|
|
561
|
+
summaries.append(s)
|
|
483
562
|
print(json.dumps(
|
|
484
563
|
{
|
|
485
|
-
"templates":
|
|
564
|
+
"templates": summaries,
|
|
486
565
|
"detailHint": _detail_hint(),
|
|
487
566
|
},
|
|
488
567
|
ensure_ascii=False,
|
|
@@ -496,6 +575,8 @@ def main() -> None:
|
|
|
496
575
|
)
|
|
497
576
|
return
|
|
498
577
|
|
|
578
|
+
groups = load_showcases([str(t.get("templateId")) for t in visible], _LIST_SHOWCASES)
|
|
579
|
+
|
|
499
580
|
header = (
|
|
500
581
|
f"\n{'Template ID':<22} {'Name':<18} {'Aspect':<12} "
|
|
501
582
|
f"{'Lang':<8} {'Status':<8} Tags / LLM hint"
|
|
@@ -515,6 +596,12 @@ def main() -> None:
|
|
|
515
596
|
hint = _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS)
|
|
516
597
|
if hint:
|
|
517
598
|
print(f"{' ' * 22} ↳ {hint}")
|
|
599
|
+
group = (groups or {}).get(tpl.get("templateId"))
|
|
600
|
+
if group is not None:
|
|
601
|
+
line = f"samples: {int(group.get('showcaseCount') or 0)}"
|
|
602
|
+
if group.get("pageUrl"):
|
|
603
|
+
line += f" · {group['pageUrl']}"
|
|
604
|
+
print(f"{' ' * 22} ↳ {line}")
|
|
518
605
|
|
|
519
606
|
print(f"\n{len(visible)} template(s) shown")
|
|
520
607
|
first_id = str(visible[0].get("templateId", "<templateId>"))
|
|
@@ -522,6 +609,8 @@ def main() -> None:
|
|
|
522
609
|
f"Full definition of one template: --template-id {first_id} --json-output "
|
|
523
610
|
"(llmHint / customPayloadSchema / slideSchemas / slotMapping / variants)",
|
|
524
611
|
)
|
|
612
|
+
if groups:
|
|
613
|
+
print(f"Watch samples in the browser: --open {first_id}")
|
|
525
614
|
|
|
526
615
|
|
|
527
616
|
if __name__ == "__main__":
|
|
@@ -186,7 +186,7 @@ def build_binding(template: dict, dsl: dict) -> dict:
|
|
|
186
186
|
# 用户选中的字体(DSL 的 global.font)。这里只是把不透明 key 折进 typography,
|
|
187
187
|
# 真正的解析(uniqId → 排印家族名 + 字重文件)在 ab-render 侧完成。
|
|
188
188
|
#
|
|
189
|
-
# 优先级见 ab-platform/docs/font-selection-design.md §13.3:
|
|
189
|
+
# 优先级见 ab-platform/docs/product/creation/font-selection-design.md §13.3:
|
|
190
190
|
# 用户选择 > variant.defaultFontId(预留档位,当前恒为空)
|
|
191
191
|
# > template.defaultTypography.bodyFont(现状裸 family 名)> 模板 theme 字栈
|
|
192
192
|
# 下面的 variant 合并在此之后执行,所以 fontId 单独在合并后再写回,
|
|
@@ -106,6 +106,18 @@ _CACHE_DIR = os.environ.get(
|
|
|
106
106
|
_DEFAULT_API_BASE_URL = "https://api.remixmate.ai/api"
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def api_base_url() -> str:
|
|
110
|
+
"""ab-api base(不带尾斜杠)。registry 与其它挂在 ab-api 上的只读接口共用这一份解析。
|
|
111
|
+
|
|
112
|
+
顺序:MM_API_BASE_URL → MM_BACKEND_API_URL → 生产默认,见 ``_default_registry_url``。
|
|
113
|
+
"""
|
|
114
|
+
return (
|
|
115
|
+
os.environ.get("MM_API_BASE_URL", "").strip()
|
|
116
|
+
or os.environ.get("MM_BACKEND_API_URL", "").strip()
|
|
117
|
+
or _DEFAULT_API_BASE_URL
|
|
118
|
+
).rstrip("/")
|
|
119
|
+
|
|
120
|
+
|
|
109
121
|
def _default_registry_url() -> str:
|
|
110
122
|
"""未显式配置 VIDEO_TEMPLATE_REGISTRY_URL 时,推导默认 ab-api registry endpoint。
|
|
111
123
|
|
|
@@ -118,12 +130,7 @@ def _default_registry_url() -> str:
|
|
|
118
130
|
→ https://api.remixmate.ai/api(生产默认,零配置)
|
|
119
131
|
路径段固定为 ``/remotionTemplate/registry``(POST,返回 ``{code,msg,data}``)。
|
|
120
132
|
"""
|
|
121
|
-
|
|
122
|
-
os.environ.get("MM_API_BASE_URL", "").strip()
|
|
123
|
-
or os.environ.get("MM_BACKEND_API_URL", "").strip()
|
|
124
|
-
or _DEFAULT_API_BASE_URL
|
|
125
|
-
).rstrip("/")
|
|
126
|
-
return f"{base}/remotionTemplate/registry"
|
|
133
|
+
return f"{api_base_url()}/remotionTemplate/registry"
|
|
127
134
|
|
|
128
135
|
|
|
129
136
|
def _cache_path_for(url: str) -> str:
|
|
@@ -101,7 +101,7 @@ def create_job(priv_token: str, dsl: str = "", dsl_meta: dict = None) -> int:
|
|
|
101
101
|
CONVERSATION_ID 已被注入而整段跳过它;兜底补绑的 jobId 末位来源是从 stdout
|
|
102
102
|
正则抠的)。归属断了,成片在旁白面板里就查不到已生成的音频。
|
|
103
103
|
而 CONVERSATION_ID 在这一刻本来就已经在环境里了——信息在场,以前只是没传。
|
|
104
|
-
见 ab-platform/docs/render-job-binding-fix.md。
|
|
104
|
+
见 ab-platform/docs/engineering/render-job-binding-fix.md。
|
|
105
105
|
|
|
106
106
|
服务端对该字段是软失败:绑不上只记日志,不影响任务创建。
|
|
107
107
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Showcase (sample video) loader — per-template samples + web detail-page links.
|
|
2
|
+
|
|
3
|
+
Used by ``list_templates.py`` so an agent host (Codex CLI / Codex Desktop /
|
|
4
|
+
Claude Code) can show the user how many samples a template has and a link to
|
|
5
|
+
the web detail page where the samples play.
|
|
6
|
+
|
|
7
|
+
Contract (ab-api ``POST /templateShowcase/listByTemplate``, X-Priv-Token):
|
|
8
|
+
|
|
9
|
+
request {"templateKeys": [...], "limitPerTemplate": N}
|
|
10
|
+
response {code, msg, data: {"templates": [
|
|
11
|
+
{"templateKey", "pageUrl"?, "showcaseCount",
|
|
12
|
+
"showcases": [{"showcaseId", "name", "coverImage", "videoUrl",
|
|
13
|
+
"aspectRatio", "duration", "hasDemo", "pageUrl"?, ...}]}
|
|
14
|
+
]}}
|
|
15
|
+
|
|
16
|
+
Links are built server-side; the CLI never knows the web origin or routes.
|
|
17
|
+
|
|
18
|
+
Unlike the registry, samples are *enhancement* data: every failure (network,
|
|
19
|
+
older ab-api without the endpoint, business error, bad JSON) returns ``None``
|
|
20
|
+
with one stderr warning, and the caller keeps printing the template list.
|
|
21
|
+
Auth failures are also degraded here — the registry call that precedes this
|
|
22
|
+
one already surfaces them with the proper exit code.
|
|
23
|
+
|
|
24
|
+
Env:
|
|
25
|
+
VIDEO_TEMPLATE_SHOWCASE_URL explicit endpoint (tests point it at file://)
|
|
26
|
+
MM_API_BASE_URL / MM_BACKEND_API_URL base, same resolution as the registry
|
|
27
|
+
PRIV_TOKEN injected by the CLI runner
|
|
28
|
+
VIDEO_TEMPLATE_REGISTRY_CACHE_DIR shared cache dir
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import hashlib
|
|
34
|
+
import json
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
import tempfile
|
|
38
|
+
import time
|
|
39
|
+
import urllib.request
|
|
40
|
+
from typing import Optional
|
|
41
|
+
|
|
42
|
+
from registry_loader import api_base_url
|
|
43
|
+
|
|
44
|
+
# Never longer than the registry TTL: a sample added in ShowcaseManager should
|
|
45
|
+
# show up about as fast as a template change does.
|
|
46
|
+
_TTL_SECONDS = 300
|
|
47
|
+
_HTTP_TIMEOUT = 5
|
|
48
|
+
|
|
49
|
+
# Item fields kept in CLI output. The endpoint returns the full wizard item
|
|
50
|
+
# (replicateConfig, layoutPreview, tags …) for the web; an agent only needs
|
|
51
|
+
# enough to describe and link a sample.
|
|
52
|
+
SUMMARY_ITEM_FIELDS = (
|
|
53
|
+
"showcaseId", "name", "coverImage", "videoUrl",
|
|
54
|
+
"aspectRatio", "duration", "hasDemo", "pageUrl",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _endpoint() -> str:
|
|
59
|
+
"""Endpoint URL, or "" when samples should not be fetched at all.
|
|
60
|
+
|
|
61
|
+
An offline registry (``VIDEO_TEMPLATE_REGISTRY_URL=file://…``, fixtures /
|
|
62
|
+
air-gapped dev) never implies a network call for samples: the registry
|
|
63
|
+
there is not ab-api's, so neither are its templates.
|
|
64
|
+
"""
|
|
65
|
+
explicit = os.environ.get("VIDEO_TEMPLATE_SHOWCASE_URL", "").strip()
|
|
66
|
+
if explicit:
|
|
67
|
+
return explicit
|
|
68
|
+
if os.environ.get("VIDEO_TEMPLATE_REGISTRY_URL", "").strip().startswith("file://"):
|
|
69
|
+
return ""
|
|
70
|
+
return f"{api_base_url()}/templateShowcase/listByTemplate"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cache_dir() -> str:
|
|
74
|
+
return os.environ.get(
|
|
75
|
+
"VIDEO_TEMPLATE_REGISTRY_CACHE_DIR",
|
|
76
|
+
os.path.join(tempfile.gettempdir(), "ab-template-registry"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _cache_file(url: str, body: bytes, token: str) -> str:
|
|
81
|
+
# Token identity is part of the key (hashed twice, never stored): two
|
|
82
|
+
# accounts on one machine see different private templates.
|
|
83
|
+
token_id = hashlib.sha256(token.encode("utf-8")).hexdigest()[:8]
|
|
84
|
+
digest = hashlib.sha256(url.encode("utf-8") + b"\0" + body + b"\0" + token_id.encode()).hexdigest()[:16]
|
|
85
|
+
return os.path.join(_cache_dir(), f"showcases-{digest}.json")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_cache(path: str) -> Optional[dict]:
|
|
89
|
+
try:
|
|
90
|
+
with open(path, "r", encoding="utf-8") as fh:
|
|
91
|
+
cached = json.load(fh)
|
|
92
|
+
if time.time() - float(cached.get("fetchedAt", 0)) < _TTL_SECONDS:
|
|
93
|
+
return cached.get("data")
|
|
94
|
+
except (OSError, ValueError, TypeError):
|
|
95
|
+
pass
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _write_cache(path: str, data: dict) -> None:
|
|
100
|
+
try:
|
|
101
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
102
|
+
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(path))
|
|
103
|
+
with os.fdopen(fd, "w", encoding="utf-8") as fh:
|
|
104
|
+
json.dump({"fetchedAt": time.time(), "data": data}, fh, ensure_ascii=False)
|
|
105
|
+
os.replace(tmp, path)
|
|
106
|
+
except OSError:
|
|
107
|
+
pass # cache is best-effort
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _warn(reason: str) -> None:
|
|
111
|
+
print(f"⚠️ showcase info unavailable: {reason}", file=sys.stderr)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def load_showcases(template_keys: list[str], limit_per_template: int) -> Optional[dict[str, dict]]:
|
|
115
|
+
"""Return ``{templateKey: group}`` for the visible templates, or ``None`` on any failure.
|
|
116
|
+
|
|
117
|
+
Templates the caller cannot see are simply absent from the dict.
|
|
118
|
+
"""
|
|
119
|
+
keys = [k for k in dict.fromkeys(template_keys) if isinstance(k, str) and k]
|
|
120
|
+
if not keys:
|
|
121
|
+
return {}
|
|
122
|
+
|
|
123
|
+
url = _endpoint()
|
|
124
|
+
token = os.environ.get("PRIV_TOKEN", "").strip()
|
|
125
|
+
# Silent skips (not failures): offline registry, or no credential — the
|
|
126
|
+
# endpoint is authenticated and the registry step already reported auth.
|
|
127
|
+
if not url or (not token and not url.startswith("file://")):
|
|
128
|
+
return None
|
|
129
|
+
body = json.dumps(
|
|
130
|
+
{"templateKeys": keys, "limitPerTemplate": max(1, min(int(limit_per_template), 50))},
|
|
131
|
+
separators=(",", ":"),
|
|
132
|
+
).encode("utf-8")
|
|
133
|
+
cache_path = _cache_file(url, body, token)
|
|
134
|
+
|
|
135
|
+
data = _read_cache(cache_path)
|
|
136
|
+
if data is None:
|
|
137
|
+
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
|
138
|
+
if token:
|
|
139
|
+
headers["X-Priv-Token"] = token
|
|
140
|
+
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
|
|
141
|
+
try:
|
|
142
|
+
with urllib.request.urlopen(req, timeout=_HTTP_TIMEOUT) as resp:
|
|
143
|
+
parsed = json.loads(resp.read().decode("utf-8"))
|
|
144
|
+
except Exception as exc: # noqa: BLE001 — enhancement data, degrade on anything
|
|
145
|
+
# Message only: exception text never includes the token header.
|
|
146
|
+
_warn(type(exc).__name__ + (f" {exc.code}" if hasattr(exc, "code") else ""))
|
|
147
|
+
return None
|
|
148
|
+
if not isinstance(parsed, dict) or parsed.get("code") not in (0, "0"):
|
|
149
|
+
code = parsed.get("code") if isinstance(parsed, dict) else "?"
|
|
150
|
+
_warn(f"code={code}")
|
|
151
|
+
return None
|
|
152
|
+
data = parsed.get("data")
|
|
153
|
+
if not isinstance(data, dict) or not isinstance(data.get("templates"), list):
|
|
154
|
+
_warn("unexpected response shape")
|
|
155
|
+
return None
|
|
156
|
+
_write_cache(cache_path, data)
|
|
157
|
+
|
|
158
|
+
wanted = set(keys)
|
|
159
|
+
out: dict[str, dict] = {}
|
|
160
|
+
for group in data.get("templates", []):
|
|
161
|
+
if isinstance(group, dict) and group.get("templateKey") in wanted:
|
|
162
|
+
out[group["templateKey"]] = group
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def summarize_group(group: dict, max_items: int) -> dict:
|
|
167
|
+
"""The CLI's view of one template's samples: count, link, a few slim items."""
|
|
168
|
+
items = []
|
|
169
|
+
for sc in (group.get("showcases") or [])[:max_items]:
|
|
170
|
+
if isinstance(sc, dict):
|
|
171
|
+
items.append({k: sc[k] for k in SUMMARY_ITEM_FIELDS if k in sc and sc[k] not in (None, "")})
|
|
172
|
+
out: dict = {"count": int(group.get("showcaseCount") or 0), "items": items}
|
|
173
|
+
if group.get("pageUrl"):
|
|
174
|
+
out["pageUrl"] = group["pageUrl"]
|
|
175
|
+
return out
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"filter_aspect": { "type": "string", "description": "Show only templates that declare this aspect ratio. E.g. '9:16' / '16:9' / '1:1'." },
|
|
20
20
|
"filter_language": { "type": "string", "description": "Show only templates whose contentLanguage includes this code ('zh' or 'en'). Templates with no declared language are always shown (treated as language-agnostic)." },
|
|
21
21
|
"include_beta": { "type": "boolean", "description": "Also list templates with status='beta'. Default off (only 'stable' shows). The ENABLE_BETA_TEMPLATES env var has the same effect process-wide." },
|
|
22
|
-
"json_output": { "type": "boolean", "description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }." }
|
|
22
|
+
"json_output": { "type": "boolean", "description": "Emit a JSON result instead of the human-readable table: { templates: [...] } — summaries, or full definitions when template_id / full is set — or { examples: [...] }. When the backend serves samples, list summaries carry showcases { count, pageUrl, items } and the template_id view carries a top-level showcases map." },
|
|
23
|
+
"open": { "type": "string", "description": "Print the web detail page URL of <templateId> (playable samples, layouts, copy-ready prompt) and open it in the browser when run from an interactive terminal. Relay the printed URL to the user." }
|
|
23
24
|
},
|
|
24
25
|
"required": []
|
|
25
26
|
}
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "template-registry",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "475",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V27",
|
|
6
6
|
"skillDescription": "Video-template registry skill. Stores every video-template definition and lists the available templates (templateId / name / aspect ratio / style tags).\n\nUse this skill as soon as the user mentions any of these intents:\n- View available templates / list every template\n\nNote: DSL→TemplateBinding is no longer a separate exposed step — once prepare_video_assets receives a template_id it builds the binding internally."
|
|
7
7
|
}
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
"properties": {
|
|
109
109
|
"fontId": {
|
|
110
110
|
"type": "string",
|
|
111
|
-
"description": "字体库的不透明 key(font.uniq_id)。**不要在这里写 CSS family 名**:family 名是渲染细节,由 ab-render 调 /font/resolve 解析;写成 family 名一旦拼错就静默退回默认字体,没有任何报错。见 ab-platform/docs/font-selection-design.md §8.1。"
|
|
111
|
+
"description": "字体库的不透明 key(font.uniq_id)。**不要在这里写 CSS family 名**:family 名是渲染细节,由 ab-render 调 /font/resolve 解析;写成 family 名一旦拼错就静默退回默认字体,没有任何报错。见 ab-platform/docs/product/creation/font-selection-design.md §8.1。"
|
|
112
112
|
},
|
|
113
113
|
"fontName": { "type": "string", "description": "展示名,仅供人读与日志,渲染不使用" }
|
|
114
114
|
}
|
|
@@ -271,7 +271,7 @@ def annotate_asset(
|
|
|
271
271
|
"""把录屏的语义写进 ab-api 素材库(POST /asset/annotate):分镜文本 + 来源页面。
|
|
272
272
|
|
|
273
273
|
分镜里没有逐场景时间戳,整条录屏作为一个片段。失败抛 FinalizeError,调用方只记日志——
|
|
274
|
-
语义是附加信息,不能让「录屏已完成」失败。见 ab-platform/docs/asset-semantic-library-design.md §6.1。
|
|
274
|
+
语义是附加信息,不能让「录屏已完成」失败。见 ab-platform/docs/product/creation/asset-semantic-library-design.md §6.1。
|
|
275
275
|
"""
|
|
276
276
|
if not private_token or not (file_id or file_url):
|
|
277
277
|
return
|