@remixmate/cli 0.9.44 → 0.9.46

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.44",
4
- "generatedAt": "2026-09-22T14:28:26.210Z",
3
+ "version": "0.9.46",
4
+ "generatedAt": "2026-09-23T08:47:39.701Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "doc-parse",
@@ -14,7 +14,7 @@
14
14
  "Turn this PPT / slide deck into a video",
15
15
  "Extract text or speaker notes from a .pptx",
16
16
  "Use a PowerPoint file as source material",
17
- "Get customPayload.slide for ppt-to-video"
17
+ "Read the page count and speaker notes of a slide deck"
18
18
  ],
19
19
  "entry": {
20
20
  "type": "python",
@@ -437,7 +437,7 @@
437
437
  },
438
438
  "scenes": {
439
439
  "type": "number",
440
- "description": "Scene count"
440
+ "description": "Scene count. Only applies to templates with capabilities.sceneStrategy=arc (the default); single / fixed / countdown templates own their own count. Bounded below by 3 and above by the template's capabilities.maxScenes (default 20) — a clamp is reported on stderr and is NOT an error, so always read the real count back from the returned DSL (scenes[].length) before describing the script to the user. For a template that maps one scene per source page (e.g. ppt-to-video), pass the page count you saw in doc_parse's summary."
441
441
  },
442
442
  "voice_id": {
443
443
  "type": "string",
@@ -451,6 +451,10 @@
451
451
  "type": "string",
452
452
  "description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
453
453
  },
454
+ "source_document_url": {
455
+ "type": "string",
456
+ "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration — today: ppt-to-video). **Must** pass it for such a template: it is recorded at meta.sourceDocument, and prepare_video_assets then fetches the layout itself and fills every scene's customPayload.slide. Pass the same URL you gave doc_parse. You are NOT expected to author or even read that layout data — one page is hundreds of KB, so it never travels through the conversation; without this reference the DSL is rejected downstream after you have already written all the narration."
457
+ },
454
458
  "headline": {
455
459
  "type": "string",
456
460
  "description": "On-screen headline. Stored at meta.headline and pushed into every scene's textLayers[role=headline] so the template can render it as the top big-text. **Must** be set when the user explicitly provided a headline / main title; without it, headline falls back to the long-form topic and overflows the top text layer. This is on-screen copy, so write it for a viewer who has never heard of the subject — a bare identifier (repo name, file name, product code) is not a title. Length limits, line breaks (\\n) and emphasis syntax are per-template: follow the chosen template's llmHint, which gen_script prints in full on stderr."
@@ -517,6 +521,7 @@
517
521
  "voice_id",
518
522
  "speed",
519
523
  "template_id",
524
+ "source_document_url",
520
525
  "headline",
521
526
  "subheadline",
522
527
  "carousel_items",
@@ -777,6 +782,10 @@
777
782
  "type": "string",
778
783
  "description": "Template id. When provided, the script invokes template-registry internally; no separate binding step is needed."
779
784
  },
785
+ "source_document_url": {
786
+ "type": "string",
787
+ "description": "Source document URL (.pptx) for a template that builds its per-scene picture from that document (capabilities.payloadHydration — today: ppt-to-video). Normally unnecessary: gen_script already recorded it at meta.sourceDocument when you passed source_document_url there, and the cached skeleton carries it through. Pass it here only to recover when the skeleton lacks the reference — it saves a full gen_script round trip. The layout data is fetched here and written into every scene's customPayload.slide; you neither author nor see it."
788
+ },
780
789
  "binding_json": {
781
790
  "type": "string",
782
791
  "description": "TemplateBinding JSON as an inline string (alternative to template_id; no file needed)."
@@ -806,6 +815,7 @@
806
815
  "dsl_json",
807
816
  "dsl",
808
817
  "template_id",
818
+ "source_document_url",
809
819
  "binding_json",
810
820
  "binding",
811
821
  "save_job",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.44",
3
+ "version": "0.9.46",
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",
@@ -13,14 +13,13 @@ description: |
13
13
  - Turn this PPT / slide deck into a video
14
14
  - Read / extract / summarize a .pptx, pull the text or the speaker notes out of a deck
15
15
  - Use a PowerPoint file as source material for a script
16
- - Anything that needs `customPayload.slide` for the `ppt-to-video` template
17
16
 
18
- Never hand-write `slide` nodes for `ppt-to-video`. A page has dozens of shapes with exact EMU coordinates; transcribing them by eye is guaranteed to drift from the original, and the template's own hint says so.
17
+ You call this skill for the **words** (notes, body text) and for the **page inventory** (how many pages, each page's animation tier and beat count). You do not call it to obtain `customPayload.slide` — that payload never passes through you; see "Using the result".
19
18
  triggers:
20
19
  - Turn this PPT / slide deck into a video
21
20
  - Extract text or speaker notes from a .pptx
22
21
  - Use a PowerPoint file as source material
23
- - Get customPayload.slide for ppt-to-video
22
+ - Read the page count and speaker notes of a slide deck
24
23
  ---
25
24
 
26
25
  # Document Extraction Skill (`doc_parse`)
@@ -50,7 +49,7 @@ That default matters: one page of `layout` with embedded icons can be hundreds o
50
49
 
51
50
  ## Usage
52
51
 
53
- Whole deck, both layers (what `ppt-to-video` needs):
52
+ Whole deck, both layers. The layout layer is what makes the per-page summary useful — it is where `mode` and the beat count come from, and those drive how you cut the narration:
54
53
 
55
54
  ```bash
56
55
  python3 <SkillDir>/scripts/extract_via_render.py --url "https://oss.example.com/deck.pptx"
@@ -96,7 +95,38 @@ python3 <SkillDir>/scripts/extract_via_render.py --url "https://…/deck.pptx" -
96
95
 
97
96
  ## Using the result
98
97
 
99
- **For the picture** — feed `slides[i]` straight into the template's `customPayload.slide`. Do not edit the coordinates, and do not raise `mode`: the verdict comes from the extractor, which can see reconstruction problems you cannot (`modeReason` says why). You may lower it. Raising it requires `trustMeMode: true` and means you have personally checked that page.
98
+ ### For the picture — you don't. Pass the URL instead.
99
+
100
+ The `layout` layer is written to a file, and **in the agent runtime you have no file-reading tool** (`Read` / `Bash` / `Glob` are deliberately not exposed — see `mcp-tools.ts`). So the file on disk is not a handoff to you, and `web_read` will not open it either: it only accepts http/https.
101
+
102
+ That is on purpose. One page of layout with embedded icons runs to hundreds of KB; a 23-page deck is several MB. It would bury everything else in your context and you would still only be copying bytes.
103
+
104
+ So the picture travels **by reference**:
105
+
106
+ ```bash
107
+ # 1. here: read the words and count the pages
108
+ doc_parse --url "https://…/deck.pptx"
109
+
110
+ # 2. pass the SAME url to gen_script, plus one scene per page
111
+ gen_script --template-id ppt-to-video \
112
+ --source-document-url "https://…/deck.pptx" \
113
+ --scenes 23
114
+ ```
115
+
116
+ `gen_script` records it at `meta.sourceDocument`, and `prepare_video_assets` then fetches the layout itself and fills every scene's `customPayload.slide` before validation. You never see that data, and you never hand-write it.
117
+
118
+ What you *do* own from this result:
119
+
120
+ | You read | You do |
121
+ | --- | --- |
122
+ | page count | pass it as `--scenes` (then read the real count back off the returned DSL) |
123
+ | `rawNotes` / `text` per page | write the narration (see below) |
124
+ | `mode` + beat count per page | cut the narration into that many sentences |
125
+ | `flags` | tell the user which elements the extractor could not reconstruct |
126
+
127
+ Do not raise `mode`: the verdict comes from the extractor, which can see reconstruction problems you cannot (`modeReason` says why). You may lower it by writing `customPayload.mode`. Raising it requires `trustMeMode: true` and means you have personally checked that page.
128
+
129
+ > If `prepare_video_assets` ever tells you `customPayload.slide (required)`, the missing thing is not the payload — it is the **reference**. Re-run `gen_script` with `--source-document-url`, or pass `source_document_url` to `prepare_video_assets` to recover without regenerating the script.
100
130
 
101
131
  **For the words** — `rawNotes` and `text` are two different kinds of raw material, and they need different treatment:
102
132
 
@@ -2,7 +2,7 @@
2
2
  """
3
3
  文档抽取脚本 — 通过 ab-render HTTP 接口实现,本地不需要任何 Office/解析依赖。
4
4
 
5
- 调用链:
5
+ 调用链(实现在共享模块 ``doc_extract``):
6
6
  POST $RENDER_API_URL/extract → { taskId }
7
7
  POST $RENDER_API_URL/extractStatus → { status, progress, result?, error? }
8
8
 
@@ -14,6 +14,11 @@
14
14
  直接进对话正文会撑爆上下文,还会让用户在聊天记录里滚过一屏 base64。默认写文件 +
15
15
  打一份人能读的摘要;确实要管道传全量时用 --json-output(或只要 content 层)。
16
16
 
17
+ ⚠️ 关于 layout 层的用法,看清楚这一条:**它不是给你读的**。在 ab-agent 里模型没有
18
+ 任何文件读取工具(这是有意的安全收窄),所以落盘的那个文件你打不开。需要 layout 的
19
+ 模板(ppt-to-video)按引用取数:把同一个文档 URL 传给 ``gen_script --source-document-url``,
20
+ 渲染管线会自己把每一页填进 ``customPayload.slide``。见 SKILL.md「Using the result」。
21
+
17
22
  必要环境变量(由 ab-agent 自动注入):
18
23
  RENDER_API_URL ab-render 服务地址(默认 https://api-render.remixmate.ai)
19
24
  PRIV_TOKEN ab-api 私有 token
@@ -26,99 +31,22 @@ import argparse
26
31
  import json
27
32
  import os
28
33
  import sys
29
- import time
30
- import urllib.error
31
- import urllib.request
32
-
33
- # ─── 环境变量 ─────────────────────────────────────────────────────────────────
34
-
35
- RENDER_API_URL = (
36
- os.environ.get("RENDER_API_URL")
37
- or os.environ.get("REMOTION_RENDER_API_URL")
38
- or "https://api-render.remixmate.ai"
39
- ).rstrip("/")
40
- PRIV_TOKEN = os.environ.get("PRIV_TOKEN", "")
41
- CONVERSATION_ID = os.environ.get("CONVERSATION_ID", "")
42
-
43
- # ─── HTTP 工具 ────────────────────────────────────────────────────────────────
44
-
45
-
46
- def api_post(path: str, body: dict, timeout: int = 30) -> dict:
47
- """向 ab-render 发送 POST 请求,返回解析后的 JSON。"""
48
- url = f"{RENDER_API_URL}{path}"
49
- data = json.dumps(body).encode()
50
-
51
- headers: dict[str, str] = {
52
- "Content-Type": "application/json",
53
- "X-Priv-Token": PRIV_TOKEN,
54
- }
55
- if CONVERSATION_ID:
56
- headers["x-conversation-id"] = CONVERSATION_ID
57
-
58
- req = urllib.request.Request(url, data=data, headers=headers, method="POST")
59
- try:
60
- with urllib.request.urlopen(req, timeout=timeout) as resp:
61
- return json.loads(resp.read())
62
- except urllib.error.HTTPError as e:
63
- body_text = e.read().decode(errors="replace")
64
- raise RuntimeError(f"HTTP {e.code}: {body_text}") from e
65
-
66
-
67
- def log(msg: str, silent: bool = False) -> None:
68
- if not silent:
69
- print(msg, flush=True)
70
-
71
-
72
- # ─── 摘要 ─────────────────────────────────────────────────────────────────────
73
-
74
-
75
- def summarize(result: dict, out_path: str | None) -> str:
76
- """
77
- 人能读的一页纸。**这是这个 skill 存在的一半价值** —— 端点给的是全量结构,
78
- 决定"喂多少进上下文"是入口层的责任(设计文档 §4.4)。
79
-
80
- 每页一行:页码、档位、拍数、标题、讲稿来源。档位与拍数决定后面怎么写旁白,
81
- 讲稿来源决定走严格改写还是扩写,这两件是下一步真正要用的信息。
82
- """
83
- src = result.get("source", {})
84
- lines = [
85
- f"共 {src.get('slideCount', '?')} 页,本次抽到 {len(result.get('slides', []))} 页"
86
- f"(fields={'+'.join(src.get('fields', []))})",
87
- "",
88
- ]
89
-
90
- for s in result.get("slides", []):
91
- idx = s.get("slideIndex", "?")
92
- bits = [f"第 {idx} 页"]
93
-
94
- if s.get("mode"):
95
- beats = len(s.get("beats") or [])
96
- bits.append(f"{s['mode']}/{beats} 拍")
97
- if s.get("title"):
98
- bits.append(f"「{s['title']}」")
99
-
100
- notes = (s.get("rawNotes") or "").strip()
101
- if notes:
102
- head = notes.replace("\n", " ")[:40]
103
- bits.append(f"备注 {len(notes)} 字:{head}…")
104
- elif "rawNotes" in s:
105
- bits.append("无备注(旁白需按正文扩写)")
106
-
107
- lines.append(" " + " · ".join(bits))
108
-
109
- if s.get("modeReason"):
110
- lines.append(f" 判定:{s['modeReason']}")
111
- flags = s.get("flags") or []
112
- if flags:
113
- # flag 是显式的降级记录,不是日志 —— 画面上少一块,这里不说就没人知道为什么。
114
- lines.append(f" ⚠ 降级:{'; '.join(flags[:3])}")
115
34
 
116
- if out_path:
117
- lines += ["", f"完整结果已写入:{out_path}"]
118
- return "\n".join(lines)
35
+ # 共享 Python lib 在 skills/template-registry/scripts/(见 AGENTS.md 的约定)。
36
+ # 抽取的 submit/poll 逻辑住在那里而不是这里,因为渲染管线也要用同一份 —— 见
37
+ # doc_extract 模块头的说明。
38
+ sys.path.insert(
39
+ 0,
40
+ os.path.abspath(
41
+ os.path.join(os.path.dirname(__file__), "..", "..", "template-registry", "scripts")
42
+ ),
43
+ )
119
44
 
120
-
121
- # ─── 主逻辑 ───────────────────────────────────────────────────────────────────
45
+ from doc_extract import ( # noqa: E402
46
+ DocExtractError,
47
+ extract_document,
48
+ summarize,
49
+ )
122
50
 
123
51
 
124
52
  def main() -> None:
@@ -141,77 +69,46 @@ def main() -> None:
141
69
  )
142
70
  args = parser.parse_args()
143
71
 
144
- if not PRIV_TOKEN:
145
- print(
146
- "❌ PRIV_TOKEN env var is not configured — /extract requires auth.",
147
- file=sys.stderr,
148
- )
149
- sys.exit(1)
150
-
151
- fields = [f.strip() for f in args.fields.split(",") if f.strip()]
72
+ # 管道模式下 stdout 只能有那一份 JSON,进度行一律走 stderr。
73
+ def log(msg: str) -> None:
74
+ print(msg, file=sys.stderr if args.json_output else sys.stdout, flush=True)
152
75
 
153
- # ── 1. Submit ────────────────────────────────────────────────────────────
154
- log(f"[doc-parse] Submitting extraction: {args.url[:80]}", args.json_output)
155
-
156
- body: dict = {"documentUrl": args.url, "kind": args.kind, "fields": fields}
157
- if args.slide is not None:
158
- body["slide"] = args.slide
159
-
160
- resp = api_post("/extract", body)
161
- if resp.get("code") != 0:
162
- print(f"❌ submission failed: {resp.get('msg', 'unknown error')}", file=sys.stderr)
76
+ try:
77
+ result = extract_document(
78
+ args.url,
79
+ kind=args.kind,
80
+ fields=args.fields,
81
+ slide=args.slide,
82
+ log=log,
83
+ )
84
+ except DocExtractError as exc:
85
+ # 端点的错误已经是人话(格式不对/超页数/超体积),原样透出,不要再包一层。
86
+ print(f"❌ {exc}", file=sys.stderr)
163
87
  sys.exit(1)
164
88
 
165
- task_id: str = resp["data"]["taskId"]
166
- log(f"[doc-parse] task accepted, taskId={task_id}", args.json_output)
167
-
168
- # ── 2. Poll ──────────────────────────────────────────────────────────────
169
- max_wait_sec = 600
170
- poll_interval = 2
171
- elapsed = 0
89
+ if args.json_output:
90
+ print(json.dumps(result, ensure_ascii=False))
91
+ return
172
92
 
173
- while elapsed < max_wait_sec:
174
- time.sleep(poll_interval)
175
- elapsed += poll_interval
93
+ # 默认落盘 + 摘要:layout 层直接打进 stdout 会撑爆上下文(见文件头)。
94
+ out_path = args.out or f"./{(result.get('source') or {}).get('name', 'document')}.extract.json"
95
+ with open(out_path, "w", encoding="utf-8") as f:
96
+ json.dump(result, f, ensure_ascii=False, indent=2)
176
97
 
177
- try:
178
- status_resp = api_post("/extractStatus", {"taskId": task_id})
179
- except Exception as e: # noqa: BLE001
180
- log(f"[doc-parse] polling error (will retry): {e}", args.json_output)
181
- continue
98
+ print("\n✅ 抽取完成\n")
99
+ print(summarize(result, out_path))
182
100
 
183
- if status_resp.get("code") != 0:
184
- log(f"[doc-parse] polling error code (will retry): {status_resp.get('msg')}", args.json_output)
185
- continue
186
-
187
- task = status_resp["data"]
188
- status = task.get("status", "unknown")
189
-
190
- if status == "succeeded":
191
- result = task.get("result") or {}
192
-
193
- if args.json_output:
194
- print(json.dumps(result, ensure_ascii=False))
195
- return
196
-
197
- # 默认落盘 + 摘要:layout 层直接打进 stdout 会撑爆上下文(见文件头)。
198
- out_path = args.out or f"./{(result.get('source') or {}).get('name', 'document')}.extract.json"
199
- with open(out_path, "w", encoding="utf-8") as f:
200
- json.dump(result, f, ensure_ascii=False, indent=2)
201
-
202
- print("\n✅ 抽取完成\n")
203
- print(summarize(result, out_path))
204
- return
205
-
206
- if status == "failed":
207
- # 端点的错误已经是人话(格式不对/超页数/超体积),原样透出,不要再包一层。
208
- print(f"❌ extraction failed: {task.get('error', 'unknown error')}", file=sys.stderr)
209
- sys.exit(1)
210
-
211
- log(f"[doc-parse] status={status} elapsed={elapsed}s", args.json_output)
212
-
213
- print(f"❌ extraction timed out (over {max_wait_sec}s)", file=sys.stderr)
214
- sys.exit(1)
101
+ if "layout" in (result.get("source") or {}).get("fields", []):
102
+ # 这段是为了堵住一个具体的失败模式:上一版这里只说"已写入 <文件>",读到它的
103
+ # 模型会理所当然地去读那个文件,然后发现自己没有文件读取工具,接着就没有
104
+ # 第二条路了。所以把下一步明确写出来。
105
+ print(
106
+ "\n📎 下一步:**不要去读上面那个文件,也不要手写 slide 节点**。\n"
107
+ " 画面数据按引用进管线 —— 把同一个文档 URL 传给 gen_script:\n"
108
+ f" gen_script --template-id <id> --source-document-url \"{args.url[:100]}\"\n"
109
+ " 之后 prepare_video_assets 会自己把每一页填进对应场景的 customPayload.slide。\n"
110
+ " 你要写的是旁白(用上面每页的备注改写)与场景数(= 页数)。"
111
+ )
215
112
 
216
113
 
217
114
  if __name__ == "__main__":
@@ -62,6 +62,35 @@ The intent is to avoid the failure mode where "the generic DSL looks compatible
62
62
  7. If `template_registry list_examples=true` does return files locally, read them as an extra sample — never as a substitute for step 1.
63
63
  8. After generation, run the schema check and show the script summary to the user for confirmation.
64
64
 
65
+ ### Document-driven templates: pass the source URL, don't author the payload
66
+
67
+ Some templates build each scene's picture from a source file rather than from JSON you write. They declare it as `capabilities.payloadHydration` (today: `ppt-to-video`, which fills `customPayload.slide` from a `.pptx`).
68
+
69
+ For those, pass **`--source-document-url`** — the same URL you gave `doc_parse`:
70
+
71
+ ```bash
72
+ gen_script --template-id ppt-to-video \
73
+ --source-document-url "https://…/deck.pptx" \
74
+ --scenes 23 --duration 600
75
+ ```
76
+
77
+ It is recorded at `meta.sourceDocument`, and `prepare_video_assets` fetches the layout itself and fills every scene before validation runs.
78
+
79
+ Two things follow, and both matter:
80
+
81
+ - **You cannot supply this payload yourself, so don't try.** One page of layout data is hundreds of KB. It is not in any tool result you can read, and the file `doc_parse` writes is not openable from the agent runtime. `customPayload.slide (required)` on every scene means the *reference* is missing, not the data.
82
+ - **The reference has to be set here, at `gen_script` time.** The agent layer merges your submitted `dsl_json` onto the cached skeleton and keeps `meta` / `global` from the skeleton — so writing `meta.sourceDocument` into `dsl_json` yourself is silently dropped. If you already generated a skeleton without it, either re-run `gen_script` or pass `source_document_url` to `prepare_video_assets`.
83
+
84
+ Scene count for these templates is **one scene per page**, taken from `doc_parse`'s summary. Such templates raise `capabilities.maxScenes` above the default 20 for exactly this reason.
85
+
86
+ ### Scene count: read it back, never assume it
87
+
88
+ `--scenes` is a request, not a guarantee. It is clamped to `[3, capabilities.maxScenes]` (default max 20), and a clamp is a **stderr warning, not an error** — the call still succeeds and returns a DSL with a different number of scenes than you asked for.
89
+
90
+ So after every call, take the scene count from `scenes[]` in the returned DSL. Do not describe scene 21 of a 20-scene DSL to the user. A 23-page deck once got clamped to 20 here; the agent wrote 23 scenes' worth of narration anyway, the user confirmed a 23-scene script, and the mismatch only surfaced three tool calls later as a payload error that said nothing about scene counts.
91
+
92
+ If the count you need is above the template's maximum, say so and stop — do not quietly ship fewer scenes than the source material has.
93
+
65
94
  ### Multi-layout templates: pick a layout per scene
66
95
 
67
96
  When a template's `customPayloadSchema.slideId.enum` holds more than one value (today `html-slide` and `html-slide-blackboard`, 16 layouts each), **every scene must name one**, chosen to fit that scene's information shape — a comparison, a timeline, a code walkthrough and a set of numbers are four different layouts.
@@ -94,6 +123,8 @@ Two consequences for the agent:
94
123
  - **Forbidden**: reusing a single `slideId` across the whole video on a multi-layout template because it was the first one in the enum.
95
124
  - **Forbidden**: the user specified a template, but the agent first generated a generic DSL and then passed `--template-id` to `template-registry` to force-bind it.
96
125
  - **Forbidden**: continuing into binding / rendering despite knowing that template-specific fields, scene shapes, or binding details are missing.
126
+ - **Forbidden**: showing the user a confirmation summary whose scene count differs from `scenes[]` in the returned DSL.
127
+ - **Forbidden**: omitting `--source-document-url` for a template that declares `capabilities.payloadHydration`, or trying to hand-write the payload it hydrates.
97
128
  - **Forbidden**: silently degrading to a generic DSL because the current script cannot support a template, leaving the failure to the downstream stage.
98
129
 
99
130
  ### When a template is not yet supported