@remixmate/cli 0.9.38 → 0.9.39
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
CHANGED
|
@@ -1,8 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.9.39",
|
|
4
|
+
"generatedAt": "2026-09-21T14:35:49.704Z",
|
|
5
5
|
"skills": [
|
|
6
|
+
{
|
|
7
|
+
"id": "doc-parse",
|
|
8
|
+
"toolName": "doc_parse",
|
|
9
|
+
"tier": "tool",
|
|
10
|
+
"category": "consuming",
|
|
11
|
+
"title": "Document Extraction",
|
|
12
|
+
"summary": "Extract a document (.pptx) into structured data: per-page title and body text, the speaker notes, and — when asked for the layout layer — the exact shapes, coordinates, colors and animation beats of every slide. Runs server-side (ab-render /extract); nothing is installed locally and no model is involved, so the result is deterministic. Two layers via `fields`: `content` is lightweight text (title / text / rawNotes) and is what you want for narration or for summarizing a deck; `layout` additionally returns the slide payload that the ppt-to-video template renders one-to-one from the original file — it embeds every icon as a data URI and is large, so it is written to a file rather than printed.",
|
|
13
|
+
"triggers": [
|
|
14
|
+
"Turn this PPT / slide deck into a video",
|
|
15
|
+
"Extract text or speaker notes from a .pptx",
|
|
16
|
+
"Use a PowerPoint file as source material",
|
|
17
|
+
"Get customPayload.slide for ppt-to-video"
|
|
18
|
+
],
|
|
19
|
+
"entry": {
|
|
20
|
+
"type": "python",
|
|
21
|
+
"scriptPath": "scripts/extract_via_render.py"
|
|
22
|
+
},
|
|
23
|
+
"runtime": "python",
|
|
24
|
+
"envVars": [
|
|
25
|
+
"RENDER_API_URL",
|
|
26
|
+
"PRIV_TOKEN",
|
|
27
|
+
"CONVERSATION_ID"
|
|
28
|
+
],
|
|
29
|
+
"parameters": {
|
|
30
|
+
"type": "object",
|
|
31
|
+
"properties": {
|
|
32
|
+
"url": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"description": "Direct document URL (http/https). A page link or a share link will not work — pass the file URL itself (e.g. what the asset library returns)."
|
|
35
|
+
},
|
|
36
|
+
"kind": {
|
|
37
|
+
"type": "string",
|
|
38
|
+
"enum": [
|
|
39
|
+
"pptx"
|
|
40
|
+
],
|
|
41
|
+
"description": "Document kind. Only pptx today; a binary .ppt must be re-saved as .pptx first."
|
|
42
|
+
},
|
|
43
|
+
"fields": {
|
|
44
|
+
"type": "string",
|
|
45
|
+
"description": "Comma-separated layers: `content` (title/text/rawNotes — small) and/or `layout` (slide shapes + beats — large, embeds icons). Default `content,layout`. Ask for content only when you just need the words."
|
|
46
|
+
},
|
|
47
|
+
"slide": {
|
|
48
|
+
"type": "number",
|
|
49
|
+
"description": "Only extract this 1-based page. Omit for the whole deck."
|
|
50
|
+
},
|
|
51
|
+
"out": {
|
|
52
|
+
"type": "string",
|
|
53
|
+
"description": "Where to write the full JSON result (default ./<name>.extract.json). stdout gets a per-page summary instead."
|
|
54
|
+
},
|
|
55
|
+
"json_output": {
|
|
56
|
+
"type": "boolean",
|
|
57
|
+
"description": "Pipeline mode — emit the full JSON on stdout. Avoid with the layout layer: it is hundreds of KB of base64 per page."
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"required": [
|
|
61
|
+
"url"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"ui": {
|
|
65
|
+
"primary": [
|
|
66
|
+
"url",
|
|
67
|
+
"fields"
|
|
68
|
+
],
|
|
69
|
+
"advanced": [
|
|
70
|
+
"slide",
|
|
71
|
+
"kind"
|
|
72
|
+
],
|
|
73
|
+
"hidden": [
|
|
74
|
+
"out",
|
|
75
|
+
"json_output"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
},
|
|
6
79
|
{
|
|
7
80
|
"id": "export-jianying",
|
|
8
81
|
"toolName": "export_jianying",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.39",
|
|
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",
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: doc-parse
|
|
3
|
+
description: |
|
|
4
|
+
Document extraction skill — turn a `.pptx` into structured data through the ab-render `/extract` endpoint.
|
|
5
|
+
|
|
6
|
+
Two layers, picked with `--fields`:
|
|
7
|
+
- **content** — per-page `title`, `text[]`, and `rawNotes` (the speaker notes). Small, no base64. This is the narration source material.
|
|
8
|
+
- **layout** — the full `slide` payload the `ppt-to-video` template renders from: every shape, coordinate, color, clip path and the animation `beats`, plus the `mode` verdict. Large (icons are embedded as data URIs).
|
|
9
|
+
|
|
10
|
+
Deterministic: the endpoint parses OOXML, it does **not** call a model. Notes come back verbatim — rewriting them into narration is your job, not the endpoint's.
|
|
11
|
+
|
|
12
|
+
Use this skill immediately whenever the user asks for any of:
|
|
13
|
+
- Turn this PPT / slide deck into a video
|
|
14
|
+
- Read / extract / summarize a .pptx, pull the text or the speaker notes out of a deck
|
|
15
|
+
- Use a PowerPoint file as source material for a script
|
|
16
|
+
- Anything that needs `customPayload.slide` for the `ppt-to-video` template
|
|
17
|
+
|
|
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.
|
|
19
|
+
triggers:
|
|
20
|
+
- Turn this PPT / slide deck into a video
|
|
21
|
+
- Extract text or speaker notes from a .pptx
|
|
22
|
+
- Use a PowerPoint file as source material
|
|
23
|
+
- Get customPayload.slide for ppt-to-video
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# Document Extraction Skill (`doc_parse`)
|
|
27
|
+
|
|
28
|
+
Entry script **`scripts/extract_via_render.py`**, pure Python (stdlib only — no Office libraries, no local install).
|
|
29
|
+
|
|
30
|
+
> Path convention: read the system-injected `Base directory for this skill: <path>` as `<SkillDir>`. Never hardcode an absolute path.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
POST $RENDER_API_URL/extract → { taskId }
|
|
36
|
+
POST $RENDER_API_URL/extractStatus → { status, result? } (polled for you)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The heavy lifting (download, unzip, OOXML parsing, geometry) happens server-side. The script submits, polls, then **writes the full JSON to a file and prints a per-page summary**.
|
|
40
|
+
|
|
41
|
+
That default matters: one page of `layout` with embedded icons can be hundreds of KB. Printing it would fill your context with base64 and bury the three facts you actually need — the page's mode, its beat count, and whether it has speaker notes.
|
|
42
|
+
|
|
43
|
+
## Environment
|
|
44
|
+
|
|
45
|
+
| Var | Meaning | Default |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `RENDER_API_URL` | ab-render base URL (falls back to `REMOTION_RENDER_API_URL`) | `https://api-render.remixmate.ai` |
|
|
48
|
+
| `PRIV_TOKEN` | ab-api private token — **required**, the endpoint refuses anonymous calls | — |
|
|
49
|
+
| `CONVERSATION_ID` | Conversation id, for provenance | — |
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
Whole deck, both layers (what `ppt-to-video` needs):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 <SkillDir>/scripts/extract_via_render.py --url "https://oss.example.com/deck.pptx"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Text and notes only — summarizing a deck, or writing narration without touching the visuals:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
python3 <SkillDir>/scripts/extract_via_render.py --url "https://…/deck.pptx" --fields content
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
One page:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
python3 <SkillDir>/scripts/extract_via_render.py --url "https://…/deck.pptx" --slide 3
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Output
|
|
72
|
+
|
|
73
|
+
```jsonc
|
|
74
|
+
{
|
|
75
|
+
"source": { "url": "…", "name": "deck.pptx", "slideCount": 12, "canvas": [1920, 1080], "fields": ["content","layout"] },
|
|
76
|
+
"slides": [
|
|
77
|
+
{
|
|
78
|
+
"slideIndex": 1,
|
|
79
|
+
// content 层
|
|
80
|
+
"title": "产品介绍",
|
|
81
|
+
"text": ["三大优势", "…"],
|
|
82
|
+
"rawNotes": "备注页原文,逐字返回,未经改写",
|
|
83
|
+
// layout 层
|
|
84
|
+
"mode": "sequenced",
|
|
85
|
+
"modeReason": "4×1 格点,4 块 + 2 组页面家具",
|
|
86
|
+
"nodes": [ /* … */ ],
|
|
87
|
+
"beats": [ { "reveal": ["n12","n13"] } ],
|
|
88
|
+
"ambientIds": ["n2"],
|
|
89
|
+
"recommendedSec": 5.3,
|
|
90
|
+
"background": { "color": "#FFFFFF" },
|
|
91
|
+
"flags": ["unsupported_geom:rightArrow×3"]
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Using the result
|
|
98
|
+
|
|
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.
|
|
100
|
+
|
|
101
|
+
**For the words** — `rawNotes` and `text` are two different kinds of raw material, and they need different treatment:
|
|
102
|
+
|
|
103
|
+
| Source | What it is | What to do | Can you check yourself? |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
| `rawNotes` | already a script, written to be spoken | re-cut into one sentence per beat, make it spoken-register, drop stage directions (“as you can see here”, “(next slide)”) and non-script items (citations, TODOs) | **yes** — the original is right there |
|
|
106
|
+
| `text` | bullet points, sometimes just keywords | expand into sentences | **no** — there is nothing to check against |
|
|
107
|
+
|
|
108
|
+
Two rules:
|
|
109
|
+
|
|
110
|
+
1. **Do not introduce facts that are not on the slide.** This is the user's own deck; inventing content puts words in their mouth.
|
|
111
|
+
2. **Talk about it, don't read it out.** The body text is already on screen. Narrating it verbatim gives the viewer the same sentence three times — on the slide, in the subtitle, and in the voice-over.
|
|
112
|
+
|
|
113
|
+
When a page had no notes and you wrote the narration from `text`, say so and ask the user to look it over. Do not ship it silently.
|
|
114
|
+
|
|
115
|
+
## Failure modes
|
|
116
|
+
|
|
117
|
+
Errors come back as plain sentences; pass them to the user as-is rather than retrying:
|
|
118
|
+
|
|
119
|
+
- a binary `.ppt` (PowerPoint 97-2003) — it must be re-saved as `.pptx`, the formats are unrelated
|
|
120
|
+
- not a zip at all, or a zip without `ppt/slides/`
|
|
121
|
+
- the deck exceeds the per-call page limit — extract page by page with `--slide`, or split the file
|
|
122
|
+
- the file is too large, or unpacks to too much
|
|
123
|
+
|
|
124
|
+
`flags` on a page is **not** an error: it records what the extractor refused to reconstruct. Surface it — a silently missing element on screen is worse than a mentioned one.
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
文档抽取脚本 — 通过 ab-render HTTP 接口实现,本地不需要任何 Office/解析依赖。
|
|
4
|
+
|
|
5
|
+
调用链:
|
|
6
|
+
POST $RENDER_API_URL/extract → { taskId }
|
|
7
|
+
POST $RENDER_API_URL/extractStatus → { status, progress, result?, error? }
|
|
8
|
+
|
|
9
|
+
两层产物(--fields):
|
|
10
|
+
content title / text[] / rawNotes —— 正文与备注页,轻量,不含任何 base64
|
|
11
|
+
layout slide{nodes,beats,mode,…} —— 坐标形状,带内嵌图标,**体积很大**
|
|
12
|
+
|
|
13
|
+
默认**不把完整结果打到 stdout**:一页 PPT 的 layout 带上内嵌图标可能几百 KB,
|
|
14
|
+
直接进对话正文会撑爆上下文,还会让用户在聊天记录里滚过一屏 base64。默认写文件 +
|
|
15
|
+
打一份人能读的摘要;确实要管道传全量时用 --json-output(或只要 content 层)。
|
|
16
|
+
|
|
17
|
+
必要环境变量(由 ab-agent 自动注入):
|
|
18
|
+
RENDER_API_URL ab-render 服务地址(默认 https://api-render.remixmate.ai)
|
|
19
|
+
PRIV_TOKEN ab-api 私有 token
|
|
20
|
+
|
|
21
|
+
可选环境变量:
|
|
22
|
+
CONVERSATION_ID 对话 ID,用于留痕与产物归属
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
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
|
+
|
|
116
|
+
if out_path:
|
|
117
|
+
lines += ["", f"完整结果已写入:{out_path}"]
|
|
118
|
+
return "\n".join(lines)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# ─── 主逻辑 ───────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> None:
|
|
125
|
+
parser = argparse.ArgumentParser(
|
|
126
|
+
description="Document extraction — calls the ab-render /extract endpoint"
|
|
127
|
+
)
|
|
128
|
+
parser.add_argument("--url", required=True, help="Direct document URL (.pptx)")
|
|
129
|
+
parser.add_argument("--kind", default="pptx", choices=["pptx"], help="Document kind (default pptx)")
|
|
130
|
+
parser.add_argument("--slide", type=int, help="Only extract this 1-based page")
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--fields", default="content,layout",
|
|
133
|
+
help="Comma-separated: content / layout (default both)",
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument(
|
|
136
|
+
"--out", help="Write the full JSON result here (default: ./<name>.extract.json)",
|
|
137
|
+
)
|
|
138
|
+
parser.add_argument(
|
|
139
|
+
"--json-output", action="store_true",
|
|
140
|
+
help="Pipeline mode: emit the full JSON on stdout (careful: layout is large)",
|
|
141
|
+
)
|
|
142
|
+
args = parser.parse_args()
|
|
143
|
+
|
|
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()]
|
|
152
|
+
|
|
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)
|
|
163
|
+
sys.exit(1)
|
|
164
|
+
|
|
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
|
|
172
|
+
|
|
173
|
+
while elapsed < max_wait_sec:
|
|
174
|
+
time.sleep(poll_interval)
|
|
175
|
+
elapsed += poll_interval
|
|
176
|
+
|
|
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
|
|
182
|
+
|
|
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)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "doc-parse",
|
|
3
|
+
"toolName": "doc_parse",
|
|
4
|
+
"tier": "tool",
|
|
5
|
+
"category": "consuming",
|
|
6
|
+
"title": "Document Extraction",
|
|
7
|
+
"description": "Extract a document (.pptx) into structured data: per-page title and body text, the speaker notes, and — when asked for the layout layer — the exact shapes, coordinates, colors and animation beats of every slide. Runs server-side (ab-render /extract); nothing is installed locally and no model is involved, so the result is deterministic. Two layers via `fields`: `content` is lightweight text (title / text / rawNotes) and is what you want for narration or for summarizing a deck; `layout` additionally returns the slide payload that the ppt-to-video template renders one-to-one from the original file — it embeds every icon as a data URI and is large, so it is written to a file rather than printed.",
|
|
8
|
+
"auth": "required",
|
|
9
|
+
"envVars": [
|
|
10
|
+
"RENDER_API_URL",
|
|
11
|
+
"PRIV_TOKEN",
|
|
12
|
+
"CONVERSATION_ID"
|
|
13
|
+
],
|
|
14
|
+
"entry": {
|
|
15
|
+
"type": "python",
|
|
16
|
+
"scriptPath": "scripts/extract_via_render.py"
|
|
17
|
+
},
|
|
18
|
+
"parameters": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"url": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"description": "Direct document URL (http/https). A page link or a share link will not work — pass the file URL itself (e.g. what the asset library returns)."
|
|
24
|
+
},
|
|
25
|
+
"kind": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["pptx"],
|
|
28
|
+
"description": "Document kind. Only pptx today; a binary .ppt must be re-saved as .pptx first."
|
|
29
|
+
},
|
|
30
|
+
"fields": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Comma-separated layers: `content` (title/text/rawNotes — small) and/or `layout` (slide shapes + beats — large, embeds icons). Default `content,layout`. Ask for content only when you just need the words."
|
|
33
|
+
},
|
|
34
|
+
"slide": {
|
|
35
|
+
"type": "number",
|
|
36
|
+
"description": "Only extract this 1-based page. Omit for the whole deck."
|
|
37
|
+
},
|
|
38
|
+
"out": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Where to write the full JSON result (default ./<name>.extract.json). stdout gets a per-page summary instead."
|
|
41
|
+
},
|
|
42
|
+
"json_output": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"description": "Pipeline mode — emit the full JSON on stdout. Avoid with the layout layer: it is hundreds of KB of base64 per page."
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"required": ["url"]
|
|
48
|
+
},
|
|
49
|
+
"ui": {
|
|
50
|
+
"primary": ["url", "fields"],
|
|
51
|
+
"advanced": ["slide", "kind"],
|
|
52
|
+
"hidden": ["out", "json_output"]
|
|
53
|
+
}
|
|
54
|
+
}
|