@remixmate/cli 0.9.62 → 0.9.64
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/render_video.py +1 -1
- package/skills/template-registry/SKILL.md +15 -2
- package/skills/template-registry/scripts/list_templates.py +140 -18
- package/skills/template-registry/scripts/match_template.py +1 -1
- package/skills/template-registry/scripts/registry_loader.py +30 -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.64",
|
|
4
|
+
"generatedAt": "2026-10-04T11:52:39.175Z",
|
|
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: [...] }. List summaries carry pageUrl (the template's web detail page — show it to the user as a link) and, when the backend serves samples, showcases { count, pageUrl, items }; the template_id view carries top-level pageUrls / showcases maps."
|
|
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.64",
|
|
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)。它**不写进
|
|
@@ -2184,7 +2184,7 @@ def render_with_remote_api(
|
|
|
2184
2184
|
if source_oss_key:
|
|
2185
2185
|
# 只传 templateKey:由 ab-render 带本次的 X-Priv-Token 向 ab-api 换源码包 URL
|
|
2186
2186
|
# 并自行下载(所有权校验在 ab-api)。源码包 URL 不经过客户端 ——
|
|
2187
|
-
# 见 ab-platform/docs/template-source-protection-design.md §4.3。
|
|
2187
|
+
# 见 ab-platform/docs/templates/template-source-protection-design.md §4.3。
|
|
2188
2188
|
payload = {
|
|
2189
2189
|
"templateKey": template_id,
|
|
2190
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, a link to its web detail page (playable samples, layouts, a copy-ready prompt) and the number of samples whenever the backend serves them — table: `↳ detail: <url>` and `↳ samples: N`; JSON: each summary's `pageUrl` and `showcases.count`; `--template-id`: top-level `pageUrls`.
|
|
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 `pageUrl` (web detail page) and `showcases: { count, pageUrl, items }`; detail mode carries top-level `pageUrls` and `showcases` maps. Each is absent when the backend does not serve it. | 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,14 @@ 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
|
+
Links & samples: list and detail views add each template's web detail-page
|
|
76
|
+
link (registry document-level ``pageUrls``, falling back to the samples
|
|
77
|
+
endpoint) and sample count (showcase_loader.py) when ab-api serves them; when
|
|
78
|
+
it does not, the views are unchanged apart from one stderr warning.
|
|
71
79
|
"""
|
|
72
80
|
|
|
73
81
|
from __future__ import annotations
|
|
@@ -87,7 +95,14 @@ from registry_loader import ( # noqa: E402
|
|
|
87
95
|
RegistryUnreachableError,
|
|
88
96
|
get_template,
|
|
89
97
|
list_templates as load_visible_templates,
|
|
98
|
+
registry_page_urls,
|
|
90
99
|
)
|
|
100
|
+
from showcase_loader import load_showcases, summarize_group # noqa: E402
|
|
101
|
+
|
|
102
|
+
# Samples per template in list views (table count + JSON items). The detail
|
|
103
|
+
# view (--template-id) returns up to _DETAIL_SHOWCASES.
|
|
104
|
+
_LIST_SHOWCASES = 3
|
|
105
|
+
_DETAIL_SHOWCASES = 20
|
|
91
106
|
|
|
92
107
|
# Summary llmHint budget: enough to tell templates apart when choosing, far
|
|
93
108
|
# short of the multi-KB authoring contract (read that with --template-id).
|
|
@@ -168,6 +183,58 @@ def _summarize(tpl: dict) -> dict:
|
|
|
168
183
|
return summary
|
|
169
184
|
|
|
170
185
|
|
|
186
|
+
def _open_in_browser(url: str) -> None:
|
|
187
|
+
"""Best-effort browser open after the URL has already been printed.
|
|
188
|
+
|
|
189
|
+
Only when stdout is a TTY (a human at a terminal) and REMIXMATE_NO_BROWSER
|
|
190
|
+
is unset: agent hosts run commands through pipes / sandboxes, where the
|
|
191
|
+
printed URL is what they relay to the user. Failures are swallowed.
|
|
192
|
+
"""
|
|
193
|
+
if not sys.stdout.isatty() or os.environ.get("REMIXMATE_NO_BROWSER", "").strip():
|
|
194
|
+
return
|
|
195
|
+
try:
|
|
196
|
+
import webbrowser
|
|
197
|
+
|
|
198
|
+
webbrowser.open(url, new=2)
|
|
199
|
+
except Exception: # noqa: BLE001
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _page_url(template_id: object, page_urls: dict[str, str], groups: dict | None) -> str | None:
|
|
204
|
+
"""Web detail page link for one template.
|
|
205
|
+
|
|
206
|
+
The registry's document-level ``pageUrls`` comes first: it rides on the call
|
|
207
|
+
every list makes anyway, so the link survives the samples endpoint being
|
|
208
|
+
down. The samples group's ``pageUrl`` is the fallback (same URL, built by
|
|
209
|
+
the same backend helper).
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(template_id, str):
|
|
212
|
+
return None
|
|
213
|
+
if page_urls.get(template_id):
|
|
214
|
+
return page_urls[template_id]
|
|
215
|
+
group = (groups or {}).get(template_id)
|
|
216
|
+
url = group.get("pageUrl") if isinstance(group, dict) else None
|
|
217
|
+
return url if isinstance(url, str) and url else None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _report_unknown_ids(missing: list[str], templates: list[dict]) -> None:
|
|
221
|
+
for tid in missing:
|
|
222
|
+
# Distinguish "gated out by status" from "does not exist" —
|
|
223
|
+
# otherwise a beta template reads as a typo and the caller
|
|
224
|
+
# retries the id instead of passing --include-beta.
|
|
225
|
+
gated = get_template(tid, include_all_statuses=True)
|
|
226
|
+
if gated is not None:
|
|
227
|
+
print(
|
|
228
|
+
f"❌ Template '{tid}' exists but its status is "
|
|
229
|
+
f"'{_status_of(gated)}' — pass --include-beta to read it.",
|
|
230
|
+
file=sys.stderr,
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
print(f"❌ Unknown template id: '{tid}'", file=sys.stderr)
|
|
234
|
+
known = ", ".join(sorted(str(t.get("templateId")) for t in templates))
|
|
235
|
+
print(f" Available ids: {known}", file=sys.stderr)
|
|
236
|
+
|
|
237
|
+
|
|
171
238
|
def _detail_hint(template_id: str = "<templateId>") -> str:
|
|
172
239
|
return (
|
|
173
240
|
f"Summaries only. Run --template-id {template_id} --json-output for one "
|
|
@@ -337,8 +404,14 @@ def main() -> None:
|
|
|
337
404
|
ap.add_argument("--filter-language", help="Keep templates tagged with this content language (zh|en|...).")
|
|
338
405
|
ap.add_argument("--include-beta", action="store_true", help="Also show beta templates (default: stable only).")
|
|
339
406
|
ap.add_argument("--json-output", action="store_true", help='Emit JSON ({"templates":[...]} or {"examples":[...]}) instead of a table.')
|
|
407
|
+
ap.add_argument("--open", dest="open_id", metavar="ID",
|
|
408
|
+
help="Print the web detail page URL of this template (samples, layouts) "
|
|
409
|
+
"and open it in the browser when run from an interactive terminal.")
|
|
340
410
|
args = ap.parse_args()
|
|
341
411
|
|
|
412
|
+
if args.open_id and (args.template_ids or args.list_examples or args.list_templates):
|
|
413
|
+
ap.error("--open cannot be combined with --template-id / --list-examples / --list-templates")
|
|
414
|
+
|
|
342
415
|
# --include-beta flips registry_loader's documented status gate via env, so
|
|
343
416
|
# the CLI and in-process callers share one gating implementation.
|
|
344
417
|
if args.include_beta:
|
|
@@ -404,13 +477,37 @@ def main() -> None:
|
|
|
404
477
|
print(f"❌ {exc}", file=sys.stderr)
|
|
405
478
|
sys.exit(1)
|
|
406
479
|
|
|
480
|
+
by_id = {t.get("templateId"): t for t in templates}
|
|
481
|
+
page_urls = registry_page_urls()
|
|
482
|
+
|
|
483
|
+
# ----- open mode (--open) -------------------------------------------------
|
|
484
|
+
#
|
|
485
|
+
# The URL goes to stdout first, unconditionally: in Codex / Claude Code the
|
|
486
|
+
# command runs through a pipe and the printed link is what the agent relays.
|
|
487
|
+
if args.open_id:
|
|
488
|
+
if args.open_id not in by_id:
|
|
489
|
+
_report_unknown_ids([args.open_id], templates)
|
|
490
|
+
sys.exit(1)
|
|
491
|
+
page_url = _page_url(args.open_id, page_urls, None)
|
|
492
|
+
if not page_url:
|
|
493
|
+
page_url = _page_url(args.open_id, {}, load_showcases([args.open_id], 1))
|
|
494
|
+
if not page_url:
|
|
495
|
+
print(
|
|
496
|
+
"❌ No web detail page link for this template: the backend does not "
|
|
497
|
+
"serve sample links yet, or its site address is not configured.",
|
|
498
|
+
file=sys.stderr,
|
|
499
|
+
)
|
|
500
|
+
sys.exit(1)
|
|
501
|
+
print(page_url)
|
|
502
|
+
_open_in_browser(page_url)
|
|
503
|
+
return
|
|
504
|
+
|
|
407
505
|
# ----- detail mode (--template-id) ---------------------------------------
|
|
408
506
|
#
|
|
409
507
|
# Explicit ids win over --filter-*: the caller already chose, and silently
|
|
410
508
|
# returning nothing because a stale filter excluded the pick would read as
|
|
411
509
|
# "template does not exist".
|
|
412
510
|
if args.template_ids:
|
|
413
|
-
by_id = {t.get("templateId"): t for t in templates}
|
|
414
511
|
picked: list[dict] = []
|
|
415
512
|
missing: list[str] = []
|
|
416
513
|
for tid in args.template_ids:
|
|
@@ -420,21 +517,7 @@ def main() -> None:
|
|
|
420
517
|
else:
|
|
421
518
|
picked.append(tpl)
|
|
422
519
|
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)
|
|
520
|
+
_report_unknown_ids(missing, templates)
|
|
438
521
|
sys.exit(1)
|
|
439
522
|
if args.slide_ids:
|
|
440
523
|
wanted = [s for s in args.slide_ids if s != _SLIDE_EXPAND_ALL]
|
|
@@ -457,7 +540,22 @@ def main() -> None:
|
|
|
457
540
|
# tool call: indent=2 nearly doubled the size (html-slide 37 K → 65 K
|
|
458
541
|
# chars), which alone pushed the plain call past the tool-result cap and
|
|
459
542
|
# got it spilled to a file the agent cannot read. Humans can `| jq`.
|
|
460
|
-
|
|
543
|
+
detail: dict = {"templates": picked}
|
|
544
|
+
# Samples and links sit beside the definitions, never inside them: the
|
|
545
|
+
# definition is the template's authoring contract, these are web content.
|
|
546
|
+
groups = load_showcases([str(t.get("templateId")) for t in picked], _DETAIL_SHOWCASES)
|
|
547
|
+
links = {
|
|
548
|
+
str(t.get("templateId")): url
|
|
549
|
+
for t in picked
|
|
550
|
+
if (url := _page_url(t.get("templateId"), page_urls, groups))
|
|
551
|
+
}
|
|
552
|
+
if links:
|
|
553
|
+
detail["pageUrls"] = links
|
|
554
|
+
if groups is not None:
|
|
555
|
+
detail["showcases"] = {
|
|
556
|
+
tid: summarize_group(g, _DETAIL_SHOWCASES) for tid, g in groups.items()
|
|
557
|
+
}
|
|
558
|
+
print(json.dumps(detail, ensure_ascii=False))
|
|
461
559
|
return
|
|
462
560
|
|
|
463
561
|
visible = [
|
|
@@ -480,9 +578,20 @@ def main() -> None:
|
|
|
480
578
|
sys.exit(1)
|
|
481
579
|
print(payload)
|
|
482
580
|
return
|
|
581
|
+
groups = load_showcases([str(t.get("templateId")) for t in visible], _LIST_SHOWCASES) if visible else {}
|
|
582
|
+
summaries = []
|
|
583
|
+
for t in visible:
|
|
584
|
+
s = _summarize(t)
|
|
585
|
+
if url := _page_url(t.get("templateId"), page_urls, groups):
|
|
586
|
+
s["pageUrl"] = url
|
|
587
|
+
# Absent (not null) when the endpoint is unavailable, so a consumer
|
|
588
|
+
# never mistakes "unknown" for "zero samples".
|
|
589
|
+
if groups is not None and t.get("templateId") in groups:
|
|
590
|
+
s["showcases"] = summarize_group(groups[t["templateId"]], _LIST_SHOWCASES)
|
|
591
|
+
summaries.append(s)
|
|
483
592
|
print(json.dumps(
|
|
484
593
|
{
|
|
485
|
-
"templates":
|
|
594
|
+
"templates": summaries,
|
|
486
595
|
"detailHint": _detail_hint(),
|
|
487
596
|
},
|
|
488
597
|
ensure_ascii=False,
|
|
@@ -496,6 +605,9 @@ def main() -> None:
|
|
|
496
605
|
)
|
|
497
606
|
return
|
|
498
607
|
|
|
608
|
+
groups = load_showcases([str(t.get("templateId")) for t in visible], _LIST_SHOWCASES)
|
|
609
|
+
any_link = False
|
|
610
|
+
|
|
499
611
|
header = (
|
|
500
612
|
f"\n{'Template ID':<22} {'Name':<18} {'Aspect':<12} "
|
|
501
613
|
f"{'Lang':<8} {'Status':<8} Tags / LLM hint"
|
|
@@ -515,6 +627,14 @@ def main() -> None:
|
|
|
515
627
|
hint = _truncate(tpl.get("llmHint"), _SUMMARY_HINT_CHARS)
|
|
516
628
|
if hint:
|
|
517
629
|
print(f"{' ' * 22} ↳ {hint}")
|
|
630
|
+
# Two lines, link on its own: an agent summarizing the table keeps a
|
|
631
|
+
# labelled `detail:` line, a URL trailing a count reads as an aside.
|
|
632
|
+
group = (groups or {}).get(tpl.get("templateId"))
|
|
633
|
+
if group is not None:
|
|
634
|
+
print(f"{' ' * 22} ↳ samples: {int(group.get('showcaseCount') or 0)}")
|
|
635
|
+
if url := _page_url(tpl.get("templateId"), page_urls, groups):
|
|
636
|
+
print(f"{' ' * 22} ↳ detail: {url}")
|
|
637
|
+
any_link = True
|
|
518
638
|
|
|
519
639
|
print(f"\n{len(visible)} template(s) shown")
|
|
520
640
|
first_id = str(visible[0].get("templateId", "<templateId>"))
|
|
@@ -522,6 +642,8 @@ def main() -> None:
|
|
|
522
642
|
f"Full definition of one template: --template-id {first_id} --json-output "
|
|
523
643
|
"(llmHint / customPayloadSchema / slideSchemas / slotMapping / variants)",
|
|
524
644
|
)
|
|
645
|
+
if any_link:
|
|
646
|
+
print(f"Watch samples / layouts in the browser: open a detail link, or --open {first_id}")
|
|
525
647
|
|
|
526
648
|
|
|
527
649
|
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:
|
|
@@ -299,6 +306,23 @@ def load_registry_data(
|
|
|
299
306
|
return _load_from_url(url, ttl=ttl_seconds, timeout=http_timeout)
|
|
300
307
|
|
|
301
308
|
|
|
309
|
+
def registry_page_urls() -> dict[str, str]:
|
|
310
|
+
"""Document-level ``pageUrls`` (templateId → web detail page), built by ab-api.
|
|
311
|
+
|
|
312
|
+
Kept beside ``templates`` rather than inside each definition: a definition is
|
|
313
|
+
the template's authoring contract, a link is site information. Absent on
|
|
314
|
+
older backends, offline registries, or when the backend has no site address
|
|
315
|
+
configured — returns ``{}`` then (never raises: it is display-only).
|
|
316
|
+
"""
|
|
317
|
+
try:
|
|
318
|
+
raw = load_registry_data().get("pageUrls")
|
|
319
|
+
except Exception: # noqa: BLE001 — the caller already loaded the registry once
|
|
320
|
+
return {}
|
|
321
|
+
if not isinstance(raw, dict):
|
|
322
|
+
return {}
|
|
323
|
+
return {k: v for k, v in raw.items() if isinstance(k, str) and isinstance(v, str) and v}
|
|
324
|
+
|
|
325
|
+
|
|
302
326
|
# ════════════════════════════════════════════════════════════════════════════
|
|
303
327
|
# Indexed views — O(1) template-id lookup + status / beta gating
|
|
304
328
|
#
|
|
@@ -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: [...] }. List summaries carry pageUrl (the template's web detail page — show it to the user as a link) and, when the backend serves samples, showcases { count, pageUrl, items }; the template_id view carries top-level pageUrls / showcases maps." },
|
|
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
|