@remixmate/cli 0.12.1 → 0.12.3
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 +9 -4
- package/package.json +1 -1
- package/skills/gen-script/SKILL.md +1 -0
- package/skills/gen-script/scripts/font_catalog.py +133 -0
- package/skills/gen-script/scripts/gen_script.py +15 -2
- package/skills/gen-script/skill.json +7 -2
- package/skills/gen-script/version.json +1 -1
- package/skills/render-video/scripts/render_video.py +31 -19
- package/skills/render-video/version.json +1 -1
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.12.
|
|
4
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"version": "0.12.3",
|
|
4
|
+
"generatedAt": "2026-10-12T14:16:45.020Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "doc-parse",
|
|
@@ -556,7 +556,11 @@
|
|
|
556
556
|
"properties": {
|
|
557
557
|
"topic": {
|
|
558
558
|
"type": "string",
|
|
559
|
-
"description": "Video topic (required)"
|
|
559
|
+
"description": "Video topic (required, except with list_fonts=true)"
|
|
560
|
+
},
|
|
561
|
+
"list_fonts": {
|
|
562
|
+
"type": "boolean",
|
|
563
|
+
"description": "List the font library and exit — no script is generated and no other parameter is needed. Returns each font's id, display name, language and category from the live library; this is the complete set of valid font_id values. Call it when the user asks which fonts are available, or wants a font but has not given its id."
|
|
560
564
|
},
|
|
561
565
|
"duration": {
|
|
562
566
|
"type": "number",
|
|
@@ -592,7 +596,7 @@
|
|
|
592
596
|
},
|
|
593
597
|
"font_id": {
|
|
594
598
|
"type": "string",
|
|
595
|
-
"description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
|
|
599
|
+
"description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. When the user names a font without its id, call gen_script with list_fonts=true first and take the id from that list; do not invent ids. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
|
|
596
600
|
},
|
|
597
601
|
"font_name": {
|
|
598
602
|
"type": "string",
|
|
@@ -691,6 +695,7 @@
|
|
|
691
695
|
"background_preset",
|
|
692
696
|
"font_id",
|
|
693
697
|
"font_name",
|
|
698
|
+
"list_fonts",
|
|
694
699
|
"template_id",
|
|
695
700
|
"template_source",
|
|
696
701
|
"template_candidates",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
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",
|
|
@@ -477,6 +477,7 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
477
477
|
| `--background-preset` | Scene background preset, one of the template's `customPayloadSchema.background.preset` values. Written to every scene's `customPayload.background.preset` (other keys of the template's default background are kept). Pass it whenever the user picked a background, e.g. `背景样式: 纯色(solid)` → `solid`. `image` / `video` also need `customPayload.background.assetRef`. Requires `--template-id`; unsupported values are rejected. | template default |
|
|
478
478
|
| `--font-id` | Font library key (`font.uniq_id`), written to `global.font.fontId`; ab-render resolves and installs the real font at render time. Pass it whenever the user picked a font — the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)`; pass the id in the parentheses. Never pass a CSS family or the display name: an unknown key falls back to the template font with no error. | template font |
|
|
479
479
|
| `--font-name` | Display name of the picked font → `global.font.fontName` (display only, never used for rendering). Pass it with `--font-id`. | — |
|
|
480
|
+
| `--list-fonts` | Print the live font library (`/font/page`: id, name, lang, category, license) and exit; no script is generated and `--topic` is not needed. The ids it prints are the only valid `--font-id` values — use it when the user asks which fonts exist or names a font without its id. | — |
|
|
480
481
|
| `--template-source` | How `--template-id` was chosen: `user-explicit` / `user-confirmed` / `auto`, written to `meta.templateSelection.source`. `auto` is refused — confirm with the user first (see "Agent behavior: template selection comes before the script"). Omitted = not recorded, with a stderr notice. Requires `--template-id`. | — |
|
|
481
482
|
| `--template-candidates` | Repeatable. Template ids offered to the user, recommended first → `meta.templateSelection.candidates`. With `user-confirmed`, `--template-id` must be one of them. | — |
|
|
482
483
|
| `--allow-digital-human` | Whether to allow digital-human assets. | off |
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import sys
|
|
23
|
+
import urllib.error
|
|
24
|
+
import urllib.request
|
|
25
|
+
|
|
26
|
+
_DEFAULT_API_BASE_URL = "https://api.remixmate.ai/api"
|
|
27
|
+
_PAGE_SIZE = 100
|
|
28
|
+
_MAX_PAGES = 10
|
|
29
|
+
_TIMEOUT_SEC = 30
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FontCatalogError(RuntimeError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _api_base_url() -> str:
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
os.environ.get("MM_API_BASE_URL", "").strip()
|
|
40
|
+
or os.environ.get("MM_BACKEND_API_URL", "").strip()
|
|
41
|
+
or _DEFAULT_API_BASE_URL
|
|
42
|
+
).rstrip("/")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _post(path: str, payload: dict) -> dict:
|
|
46
|
+
url = f"{_api_base_url()}{path}"
|
|
47
|
+
headers = {"Content-Type": "application/json", "x-invoke-skill": "gen-script"}
|
|
48
|
+
token = os.environ.get("PRIV_TOKEN", "").strip()
|
|
49
|
+
if token:
|
|
50
|
+
headers["X-Priv-Token"] = token
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
url, data=json.dumps(payload).encode("utf-8"), headers=headers, method="POST",
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
with urllib.request.urlopen(req, timeout=_TIMEOUT_SEC) as resp:
|
|
56
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
57
|
+
except urllib.error.HTTPError as exc:
|
|
58
|
+
raise FontCatalogError(f"HTTP {exc.code} from {path}") from exc
|
|
59
|
+
except urllib.error.URLError as exc:
|
|
60
|
+
raise FontCatalogError(f"network request failed ({path}): {exc.reason}") from exc
|
|
61
|
+
except ValueError as exc:
|
|
62
|
+
raise FontCatalogError(f"invalid JSON from {path}") from exc
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
if result.get("code") != 0:
|
|
66
|
+
msg = result.get("msg") or result.get("message") or "unknown error"
|
|
67
|
+
raise FontCatalogError(f"API returned an error [{path}]: {msg}")
|
|
68
|
+
return result.get("data") or {}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def fetch_fonts() -> list[dict]:
|
|
72
|
+
|
|
73
|
+
fonts: list[dict] = []
|
|
74
|
+
for current in range(1, _MAX_PAGES + 1):
|
|
75
|
+
data = _post("/font/page", {"current": current, "pageSize": _PAGE_SIZE})
|
|
76
|
+
page = data.get("list") or []
|
|
77
|
+
fonts.extend(page)
|
|
78
|
+
total = (data.get("pager") or {}).get("total")
|
|
79
|
+
if not page or len(page) < _PAGE_SIZE:
|
|
80
|
+
break
|
|
81
|
+
if isinstance(total, int) and len(fonts) >= total:
|
|
82
|
+
break
|
|
83
|
+
return fonts
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _row(font: dict) -> dict:
|
|
87
|
+
return {
|
|
88
|
+
"fontId": font.get("uniqId") or "",
|
|
89
|
+
"fontName": font.get("name") or "",
|
|
90
|
+
"lang": font.get("lang") or "",
|
|
91
|
+
"category": font.get("category") or "",
|
|
92
|
+
"license": font.get("license") or "",
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def format_fonts(fonts: list[dict]) -> str:
|
|
97
|
+
|
|
98
|
+
if not fonts:
|
|
99
|
+
return (
|
|
100
|
+
"⚠️ No fonts available in the font library. "
|
|
101
|
+
"Omit font_id to keep the template's own font.\n"
|
|
102
|
+
)
|
|
103
|
+
rows = [_row(f) for f in fonts]
|
|
104
|
+
id_w = max(len("Font ID"), *(len(r["fontId"]) for r in rows))
|
|
105
|
+
name_w = max(len("Name"), *(len(r["fontName"]) for r in rows))
|
|
106
|
+
lines = [
|
|
107
|
+
"",
|
|
108
|
+
f"{'Font ID'.ljust(id_w)} {'Name'.ljust(name_w)} Lang Category License",
|
|
109
|
+
f"{'-' * id_w} {'-' * name_w} ---- -------- -------",
|
|
110
|
+
]
|
|
111
|
+
for r in rows:
|
|
112
|
+
lines.append(
|
|
113
|
+
f"{r['fontId'].ljust(id_w)} {r['fontName'].ljust(name_w)} "
|
|
114
|
+
f"{r['lang'].ljust(4)} {r['category'].ljust(8)} {r['license']}"
|
|
115
|
+
)
|
|
116
|
+
lines += [
|
|
117
|
+
"",
|
|
118
|
+
f"{len(rows)} font(s) available. This is the live library and the complete set.",
|
|
119
|
+
"💡 Pass --font-id <Font ID> --font-name <Name> to gen_script.",
|
|
120
|
+
"",
|
|
121
|
+
]
|
|
122
|
+
return "\n".join(lines)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def print_font_catalog() -> int:
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
fonts = fetch_fonts()
|
|
129
|
+
except FontCatalogError as exc:
|
|
130
|
+
print(f"❌ failed to list fonts: {exc}", file=sys.stderr)
|
|
131
|
+
return 1
|
|
132
|
+
sys.stdout.write(format_fonts(fonts))
|
|
133
|
+
return 0
|
|
@@ -2063,14 +2063,21 @@ Examples:
|
|
|
2063
2063
|
"--font-id",
|
|
2064
2064
|
default=None,
|
|
2065
2065
|
help=(
|
|
2066
|
-
"Font family key from the font library (font.uniq_id
|
|
2067
|
-
"Omit to follow the template's own font pairing. "
|
|
2066
|
+
"Font family key from the font library (font.uniq_id); run --list-fonts to see "
|
|
2067
|
+
"the valid keys. Omit to follow the template's own font pairing. "
|
|
2068
2068
|
"Pass the opaque key, NOT a CSS family name: the family name is a rendering "
|
|
2069
2069
|
"detail resolved by ab-render; a mistyped family name silently falls back to "
|
|
2070
2070
|
"the default font with no error."
|
|
2071
2071
|
),
|
|
2072
2072
|
)
|
|
2073
2073
|
parser.add_argument("--font-name", default=None, help="Human-readable font name. Logged and stored for display only; never used for rendering.")
|
|
2074
|
+
parser.add_argument(
|
|
2075
|
+
"--list-fonts",
|
|
2076
|
+
action="store_true",
|
|
2077
|
+
help="List the font library (font id, name, lang, category) from ab-api and exit. "
|
|
2078
|
+
"No script is generated and --topic is not needed. The ids printed here are "
|
|
2079
|
+
"the only valid values for --font-id.",
|
|
2080
|
+
)
|
|
2074
2081
|
parser.add_argument(
|
|
2075
2082
|
"--variant", default=None,
|
|
2076
2083
|
help="Template visual variant id (a key of the template's `variants`, listed as variantIds by "
|
|
@@ -2167,6 +2174,12 @@ Examples:
|
|
|
2167
2174
|
|
|
2168
2175
|
args = parser.parse_args()
|
|
2169
2176
|
|
|
2177
|
+
|
|
2178
|
+
|
|
2179
|
+
if args.list_fonts:
|
|
2180
|
+
from font_catalog import print_font_catalog
|
|
2181
|
+
sys.exit(print_font_catalog())
|
|
2182
|
+
|
|
2170
2183
|
if args.validate:
|
|
2171
2184
|
if not args.input:
|
|
2172
2185
|
print("❌ validate mode requires --input", file=sys.stderr)
|
|
@@ -18,7 +18,11 @@
|
|
|
18
18
|
"properties": {
|
|
19
19
|
"topic": {
|
|
20
20
|
"type": "string",
|
|
21
|
-
"description": "Video topic (required)"
|
|
21
|
+
"description": "Video topic (required, except with list_fonts=true)"
|
|
22
|
+
},
|
|
23
|
+
"list_fonts": {
|
|
24
|
+
"type": "boolean",
|
|
25
|
+
"description": "List the font library and exit — no script is generated and no other parameter is needed. Returns each font's id, display name, language and category from the live library; this is the complete set of valid font_id values. Call it when the user asks which fonts are available, or wants a font but has not given its id."
|
|
22
26
|
},
|
|
23
27
|
"duration": {
|
|
24
28
|
"type": "number",
|
|
@@ -54,7 +58,7 @@
|
|
|
54
58
|
},
|
|
55
59
|
"font_id": {
|
|
56
60
|
"type": "string",
|
|
57
|
-
"description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
|
|
61
|
+
"description": "Font library key (font.uniq_id) for the on-screen text and subtitles. Written to global.font.fontId; match_template folds it into the binding's typography and ab-render resolves and installs the real font before rendering. **Must** pass when the user picked a font: the replicate card shows it as `名字(id)`, e.g. `字体: <名字>(<uniqId>)` → pass the id in the parentheses. Pass the opaque key, never a CSS family name or the display name — those fall back to the template font with no error. When the user names a font without its id, call gen_script with list_fonts=true first and take the id from that list; do not invent ids. Without it the choice is silently dropped and the video renders in the template's own font. Omit to follow the template's font pairing."
|
|
58
62
|
},
|
|
59
63
|
"font_name": {
|
|
60
64
|
"type": "string",
|
|
@@ -147,6 +151,7 @@
|
|
|
147
151
|
"background_preset",
|
|
148
152
|
"font_id",
|
|
149
153
|
"font_name",
|
|
154
|
+
"list_fonts",
|
|
150
155
|
"template_id",
|
|
151
156
|
"template_source",
|
|
152
157
|
"template_candidates",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V21",
|
|
6
6
|
"skillDescription": "Video-script generation skill. Turns a user-supplied topic into a structured Video DSL (JSON) that describes the full video — scene structure, asset requirements, and narrative flow.\n\nUse this skill as soon as the user mentions any of these intents:\n- Write a video script, generate a video script, plan a video, write storyboards\n- Create a short video, plan video content, generate a Video DSL\n- Turn a topic into a video structure / video content plan\n\nEven when the user does not say \"generate the DSL\", use this skill whenever they want to turn a topic into a structured video plan.\n\n⚠️ Stop-and-confirm gate: after this skill returns a DSL, show the full script and wait for the user's explicit confirmation. Never call `prepare_video_assets` in the same turn."
|
|
7
7
|
}
|
|
@@ -2671,27 +2671,39 @@ Examples:
|
|
|
2671
2671
|
|
|
2672
2672
|
|
|
2673
2673
|
if args.dsl_json:
|
|
2674
|
+
|
|
2675
|
+
|
|
2676
|
+
|
|
2677
|
+
|
|
2678
|
+
|
|
2679
|
+
|
|
2674
2680
|
try:
|
|
2681
|
+
dsl = json.loads(args.dsl_json)
|
|
2682
|
+
except json.JSONDecodeError:
|
|
2683
|
+
dsl = None
|
|
2684
|
+
if dsl is None:
|
|
2685
|
+
try:
|
|
2675
2686
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2687
|
+
_dsl_str = args.dsl_json
|
|
2688
|
+
|
|
2689
|
+
_dsl_str = _dsl_str.replace('\u201c', '"').replace('\u201d', '"')
|
|
2690
|
+
_dsl_str = _dsl_str.replace('\u2018', "'").replace('\u2019', "'")
|
|
2691
|
+
|
|
2692
|
+
_dsl_str = _dsl_str.replace('\uff1a', ':')
|
|
2693
|
+
_dsl_str = _dsl_str.replace('\uff0c', ',')
|
|
2694
|
+
_dsl_str = _dsl_str.replace('\uff08', '(')
|
|
2695
|
+
_dsl_str = _dsl_str.replace('\uff09', ')')
|
|
2696
|
+
_dsl_str = _dsl_str.replace('\u3010', '[')
|
|
2697
|
+
_dsl_str = _dsl_str.replace('\u3011', ']')
|
|
2698
|
+
_dsl_str = _dsl_str.replace('\uff3b', '[')
|
|
2699
|
+
_dsl_str = _dsl_str.replace('\uff3d', ']')
|
|
2700
|
+
_dsl_str = _dsl_str.replace('\uff5b', '{')
|
|
2701
|
+
_dsl_str = _dsl_str.replace('\uff5d', '}')
|
|
2702
|
+
dsl = json.loads(_dsl_str)
|
|
2703
|
+
LogPrint("⚠️ --dsl-json was not valid JSON; parsed after normalizing full-width punctuation", file=sys.stderr)
|
|
2704
|
+
except json.JSONDecodeError as e:
|
|
2705
|
+
LogPrint(f"❌ --dsl-json parse failed: {e}", file=sys.stderr)
|
|
2706
|
+
sys.exit(1)
|
|
2695
2707
|
else:
|
|
2696
2708
|
if not os.path.exists(args.dsl):
|
|
2697
2709
|
LogPrint(f"❌ file does not exist: {args.dsl}", file=sys.stderr)
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "render-video",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "473",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V38",
|
|
6
6
|
"skillDescription": "Final-render skill: loads a persisted RenderPlan by `job_id` and drives the Remotion engine to produce the final video.\n\nUse this skill as soon as the user mentions any of these intents (after assets are already prepared):\n- Render the video, composite the video, export the video\n- Turn the prepared assets into the final clip\n- Render with Remotion\n\nPrerequisite: assets must already be generated via `prepare_video_assets`. This skill never resolves or regenerates assets — pass it a `job_id` from a previous `prepare_video_assets` call.\n\n⚠️ Stop-and-confirm gate: never call this skill until the user has explicitly confirmed the assets prepared by `prepare_video_assets`. If those assets were prepared in the current turn and the user has not replied since, stop and ask instead of rendering."
|
|
7
7
|
}
|