@remixmate/cli 0.9.25 → 0.9.27
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/README.md +11 -4
- package/README.zh-CN.md +8 -4
- package/dist/handlers/gen-voice.d.ts +11 -1
- package/dist/handlers/gen-voice.js +17 -1
- package/dist/manifest.json +166 -10
- package/package.json +3 -1
- package/skills/gen-script/SKILL.md +1 -0
- package/skills/gen-script/scripts/gen_script.py +52 -3
- package/skills/gen-script/skill.json +5 -0
- package/skills/gen-script/version.json +1 -1
- package/skills/gen-voice/SKILL.md +5 -3
- package/skills/gen-voice/skill.json +3 -8
- package/skills/render-video/scripts/render_video.py +32 -0
- package/skills/render-video/version.json +1 -1
- package/skills/web-read/SKILL.md +146 -0
- package/skills/web-read/skill.json +131 -0
- package/skills/web-screenshot/scripts/_media_screenshot/__init__.py +2 -0
- package/skills/web-screenshot/scripts/_media_screenshot/js/extract_article.js +373 -0
- package/skills/web-screenshot/scripts/_media_screenshot/reader.py +245 -0
- package/skills/web-screenshot/scripts/_media_screenshot/urlguard.py +90 -0
- package/skills/web-screenshot/scripts/read_page.py +136 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ English | [简体中文](./README.zh-CN.md)
|
|
|
6
6
|
|
|
7
7
|
AI media generation skills for Claude Code / Codex.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
13 skills covering the full short-video production pipeline: image / video / voice / digital-human asset generation, web page capture and text extraction, script planning, template binding, Remotion rendering, Jianying (CapCut) draft export, and video deconstruction.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -159,11 +159,11 @@ cp .env.example .env
|
|
|
159
159
|
source .env
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
Python skills (
|
|
162
|
+
Python skills (9 of 13) require `python3 >= 3.10`. `web-screenshot` / `web-record` / `web-read` require Playwright (auto-installs chromium on first run). `ffmpeg` is only needed for `video-parser`'s optional local tool (`deconstruct_video.py`); the default `video-parser` entry runs server-side via ab-render and needs no local ffmpeg.
|
|
163
163
|
|
|
164
164
|
## Skills
|
|
165
165
|
|
|
166
|
-
This project contains
|
|
166
|
+
This project contains 13 AI media generation skills covering the full pipeline from asset generation, script orchestration, template binding, and video rendering to Jianying export.
|
|
167
167
|
|
|
168
168
|
### Skill layers
|
|
169
169
|
|
|
@@ -186,6 +186,7 @@ This project contains 11 AI media generation skills covering the full pipeline f
|
|
|
186
186
|
│ video-parser Video deconstruction (audio / ASR / kf) │
|
|
187
187
|
│ web-screenshot Web page screenshot (png / jpg) │
|
|
188
188
|
│ web-record Web page recording / storyboard video │
|
|
189
|
+
│ web-read Web page text extraction (markdown/json) │
|
|
189
190
|
└─────────────────────────────────────────────────────────────┘
|
|
190
191
|
```
|
|
191
192
|
|
|
@@ -207,7 +208,8 @@ This project contains 11 AI media generation skills covering the full pipeline f
|
|
|
207
208
|
│ ├── export-jianying/ # orchestration: export Jianying draft ZIP
|
|
208
209
|
│ ├── video-parser/ # tool: video deconstruction & analysis
|
|
209
210
|
│ ├── web-screenshot/ # tool: headless-browser screenshot (also hosts record.py)
|
|
210
|
-
│
|
|
211
|
+
│ ├── web-record/ # tool: headless-browser recording (entry -> web-screenshot/scripts/record.py)
|
|
212
|
+
│ └── web-read/ # tool: headless-browser text extraction (entry -> web-screenshot/scripts/read_page.py)
|
|
211
213
|
└── README.md
|
|
212
214
|
```
|
|
213
215
|
|
|
@@ -227,6 +229,7 @@ This project contains 11 AI media generation skills covering the full pipeline f
|
|
|
227
229
|
| video-parser | tool | Video → audio + ASR + keyframes + scene segmentation | python `scripts/parse_via_render.py` |
|
|
228
230
|
| web-screenshot | tool | Web page screenshot (png / jpg) | python `scripts/screenshot.py` |
|
|
229
231
|
| web-record | tool | Web page recording / scroll-through / storyboard video (webm -> mp4 -> VOD) | python `../web-screenshot/scripts/record.py` |
|
|
232
|
+
| web-read | tool | Web page → main text (markdown / plain text / structured JSON) | python `../web-screenshot/scripts/read_page.py` |
|
|
230
233
|
|
|
231
234
|
### Core workflows
|
|
232
235
|
|
|
@@ -268,6 +271,10 @@ npm run regress:update
|
|
|
268
271
|
# Run only the render-plan snapshot layer:
|
|
269
272
|
npm run test:render-plan
|
|
270
273
|
|
|
274
|
+
# Narration speed wiring (gen_script → render plan → gen-voice --speed).
|
|
275
|
+
# Fully offline: no registry, no TTS call.
|
|
276
|
+
npm run test:narration-speed
|
|
277
|
+
|
|
271
278
|
# L3 — selective, real Remotion render of chosen template(s). NOT full-library
|
|
272
279
|
# by design (rendering is slow + needs asset generation). Catches crashes /
|
|
273
280
|
# black frames the static layers can't. Needs PRIV_TOKEN for narration TTS;
|
package/README.zh-CN.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
面向 Claude Code / Codex 的 AI 媒体生成技能集。
|
|
8
8
|
|
|
9
|
-
包含
|
|
9
|
+
包含 13 个技能,覆盖完整的短视频生产链路:图片 / 视频 / 语音 / 数字人素材生成、网页捕获与正文抽取、脚本编排、模板绑定、Remotion 渲染、剪映(CapCut)草稿导出,以及视频解构。
|
|
10
10
|
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
@@ -113,11 +113,11 @@ cp .env.example .env
|
|
|
113
113
|
source .env
|
|
114
114
|
```
|
|
115
115
|
|
|
116
|
-
Python 技能(
|
|
116
|
+
Python 技能(13 个中的 9 个)需要 `python3 >= 3.10`。`web-screenshot` / `web-record` / `web-read` 需要 Playwright(首次运行自动安装 chromium)。`ffmpeg` 仅 `video-parser` 的可选本地工具(`deconstruct_video.py`)需要;`video-parser` 默认入口走 ab-render 服务端解构,无需本地 ffmpeg。
|
|
117
117
|
|
|
118
118
|
## 技能
|
|
119
119
|
|
|
120
|
-
本项目包含
|
|
120
|
+
本项目包含 13 个 AI 媒体生成技能,覆盖从素材生成、脚本编排、模板绑定、视频渲染到剪映导出的完整视频内容生产链路。
|
|
121
121
|
|
|
122
122
|
### 技能分层
|
|
123
123
|
|
|
@@ -140,6 +140,7 @@ Python 技能(12 个中的 8 个)需要 `python3 >= 3.10`。`web-screenshot`
|
|
|
140
140
|
│ video-parser 视频解构(音频提取 / ASR / 关键帧) │
|
|
141
141
|
│ web-screenshot 网页截图(png / jpg) │
|
|
142
142
|
│ web-record 网页录屏 / 滚动录屏 / 分镜视频 │
|
|
143
|
+
│ web-read 网页正文抽取(markdown / 纯文本 / JSON) │
|
|
143
144
|
└─────────────────────────────────────────────────────────────┘
|
|
144
145
|
```
|
|
145
146
|
|
|
@@ -161,7 +162,8 @@ Python 技能(12 个中的 8 个)需要 `python3 >= 3.10`。`web-screenshot`
|
|
|
161
162
|
│ ├── export-jianying/ # 编排: 导出剪映草稿 ZIP
|
|
162
163
|
│ ├── video-parser/ # 工具: 视频解构与分析
|
|
163
164
|
│ ├── web-screenshot/ # 工具: 无头浏览器截图(record.py 也放在这里)
|
|
164
|
-
│
|
|
165
|
+
│ ├── web-record/ # 工具: 无头浏览器录屏(入口指向 web-screenshot/scripts/record.py)
|
|
166
|
+
│ └── web-read/ # 工具: 无头浏览器正文抽取(入口指向 web-screenshot/scripts/read_page.py)
|
|
165
167
|
└── README.md
|
|
166
168
|
```
|
|
167
169
|
|
|
@@ -181,6 +183,7 @@ Python 技能(12 个中的 8 个)需要 `python3 >= 3.10`。`web-screenshot`
|
|
|
181
183
|
| video-parser | 工具 | 视频 → 音频 + ASR + 关键帧 + 场景分段 | python `scripts/parse_via_render.py` |
|
|
182
184
|
| web-screenshot | 工具 | 网页截图(png / jpg) | python `scripts/screenshot.py` |
|
|
183
185
|
| web-record | 工具 | 网页录屏 / 滚动录屏 / 分镜视频(webm → mp4 → VOD) | python `../web-screenshot/scripts/record.py` |
|
|
186
|
+
| web-read | 工具 | 网页正文抽取(markdown / 纯文本 / 结构化 JSON) | python `../web-screenshot/scripts/read_page.py` |
|
|
184
187
|
|
|
185
188
|
### 核心链路
|
|
186
189
|
|
|
@@ -218,6 +221,7 @@ remixmate export-jianying --help
|
|
|
218
221
|
remixmate video-parser --help
|
|
219
222
|
remixmate web-screenshot --help
|
|
220
223
|
remixmate web-record --help
|
|
224
|
+
remixmate web-read --help
|
|
221
225
|
remixmate template-registry --help
|
|
222
226
|
|
|
223
227
|
# http 技能(gen-image、gen-video、gen-voice、gen-digital-human)没有
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
* Operations:
|
|
5
5
|
* --list-voices true → GET available voice IDs from /voice/page (live)
|
|
6
6
|
* --list-voices true --local true → print the built-in language-tagged
|
|
7
|
-
* fallback catalog (offline, no API call)
|
|
7
|
+
* fallback catalog (offline, no API call).
|
|
8
|
+
* **CLI-only escape hatch for offline dev.**
|
|
8
9
|
* default → synthesize text into an MP3 URL
|
|
9
10
|
*
|
|
11
|
+
* `local` is deliberately absent from `skills/gen-voice/skill.json`'s
|
|
12
|
+
* `parameters`, which is what ab-agent turns into the model-facing tool schema.
|
|
13
|
+
* A model asked to check a voice id would reach for `local: true`, get a
|
|
14
|
+
* two-entry hardcoded list back, and conclude that a perfectly valid voice does
|
|
15
|
+
* not exist — which is exactly what happened on the hosted web agent
|
|
16
|
+
* ("本地回退列表中没有该音色"). The remote catalog is the only authority on
|
|
17
|
+
* which voices exist; the local list is a developer convenience for working
|
|
18
|
+
* without a token, and it is only reachable by typing the flag yourself.
|
|
19
|
+
*
|
|
10
20
|
* The previous Python script also offered `--download` to write the MP3 to
|
|
11
21
|
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
12
22
|
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
* Operations:
|
|
5
5
|
* --list-voices true → GET available voice IDs from /voice/page (live)
|
|
6
6
|
* --list-voices true --local true → print the built-in language-tagged
|
|
7
|
-
* fallback catalog (offline, no API call)
|
|
7
|
+
* fallback catalog (offline, no API call).
|
|
8
|
+
* **CLI-only escape hatch for offline dev.**
|
|
8
9
|
* default → synthesize text into an MP3 URL
|
|
9
10
|
*
|
|
11
|
+
* `local` is deliberately absent from `skills/gen-voice/skill.json`'s
|
|
12
|
+
* `parameters`, which is what ab-agent turns into the model-facing tool schema.
|
|
13
|
+
* A model asked to check a voice id would reach for `local: true`, get a
|
|
14
|
+
* two-entry hardcoded list back, and conclude that a perfectly valid voice does
|
|
15
|
+
* not exist — which is exactly what happened on the hosted web agent
|
|
16
|
+
* ("本地回退列表中没有该音色"). The remote catalog is the only authority on
|
|
17
|
+
* which voices exist; the local list is a developer convenience for working
|
|
18
|
+
* without a token, and it is only reachable by typing the flag yourself.
|
|
19
|
+
*
|
|
10
20
|
* The previous Python script also offered `--download` to write the MP3 to
|
|
11
21
|
* disk. That's intentionally dropped here: the agent flow never used it (the
|
|
12
22
|
* audio URL is persisted in cloud) and removing local file I/O keeps the
|
|
@@ -21,6 +31,10 @@ import { isTrue, toNumber } from './shared.js';
|
|
|
21
31
|
* Mirrors the language-keyed defaults that gen_script.py uses, kept in sync
|
|
22
32
|
* by hand because the two callers live on different runtimes.
|
|
23
33
|
*
|
|
34
|
+
* This is **not** a list of available voices — it is the two ids the resolver
|
|
35
|
+
* falls back to when nothing else is specified, and one of them is still a
|
|
36
|
+
* placeholder. Never treat it as evidence about what the service offers.
|
|
37
|
+
*
|
|
24
38
|
* Promote to a JSON config once this grows beyond a handful of entries.
|
|
25
39
|
*/
|
|
26
40
|
const LOCAL_VOICE_CATALOG = [
|
|
@@ -44,6 +58,8 @@ async function listVoicesRemote(ctx) {
|
|
|
44
58
|
process.stdout.write(`💡 Use --voice-id <id> to select a voice for synthesis\n`);
|
|
45
59
|
}
|
|
46
60
|
function listVoicesLocal() {
|
|
61
|
+
process.stderr.write('⚠️ --local prints a hardcoded 2-entry fallback list, NOT the available voices. ' +
|
|
62
|
+
'Drop --local to query the live catalog.\n');
|
|
47
63
|
for (const v of LOCAL_VOICE_CATALOG) {
|
|
48
64
|
process.stdout.write(`${v.id}\t${v.language}\t${v.name}\n`);
|
|
49
65
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"version": "0.9.27",
|
|
4
|
+
"generatedAt": "2026-09-13T09:11:30.410Z",
|
|
5
5
|
"skills": [
|
|
6
6
|
{
|
|
7
7
|
"id": "export-jianying",
|
|
@@ -382,6 +382,10 @@
|
|
|
382
382
|
"type": "string",
|
|
383
383
|
"description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
|
|
384
384
|
},
|
|
385
|
+
"speed": {
|
|
386
|
+
"type": "number",
|
|
387
|
+
"description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
|
|
388
|
+
},
|
|
385
389
|
"template_id": {
|
|
386
390
|
"type": "string",
|
|
387
391
|
"description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
|
|
@@ -444,6 +448,7 @@
|
|
|
444
448
|
"ratio",
|
|
445
449
|
"scenes",
|
|
446
450
|
"voice_id",
|
|
451
|
+
"speed",
|
|
447
452
|
"template_id",
|
|
448
453
|
"headline",
|
|
449
454
|
"subheadline",
|
|
@@ -631,7 +636,7 @@
|
|
|
631
636
|
},
|
|
632
637
|
"voice_id": {
|
|
633
638
|
"type": "string",
|
|
634
|
-
"description": "Voice id. Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first to see what's available — do not invent ids."
|
|
639
|
+
"description": "Voice id. Default 'Chinese (Mandarin)_Male_Announcer'. When unsure, call with list_voices=true first to see what's available — do not invent ids, and do not declare a voice unavailable unless it is missing from that list."
|
|
635
640
|
},
|
|
636
641
|
"speed": {
|
|
637
642
|
"type": "number",
|
|
@@ -639,11 +644,7 @@
|
|
|
639
644
|
},
|
|
640
645
|
"list_voices": {
|
|
641
646
|
"type": "boolean",
|
|
642
|
-
"description": "List available voices and exit"
|
|
643
|
-
},
|
|
644
|
-
"local": {
|
|
645
|
-
"type": "boolean",
|
|
646
|
-
"description": "Used together with list_voices=true: print the voice-resolver fallback catalog with language tags (no remote /voice/page call). Output is one '<id>\\t<lang>\\t<name>' line per voice."
|
|
647
|
+
"description": "List the available voices and exit. This queries the live voice service and is the authoritative catalog — one call is enough, and its absence from this list is the only evidence that a voice id is invalid."
|
|
647
648
|
},
|
|
648
649
|
"json_output": {
|
|
649
650
|
"type": "boolean",
|
|
@@ -662,8 +663,7 @@
|
|
|
662
663
|
],
|
|
663
664
|
"hidden": [
|
|
664
665
|
"json_output",
|
|
665
|
-
"list_voices"
|
|
666
|
-
"local"
|
|
666
|
+
"list_voices"
|
|
667
667
|
]
|
|
668
668
|
}
|
|
669
669
|
},
|
|
@@ -960,6 +960,162 @@
|
|
|
960
960
|
]
|
|
961
961
|
}
|
|
962
962
|
},
|
|
963
|
+
{
|
|
964
|
+
"id": "web-read",
|
|
965
|
+
"toolName": "web_read",
|
|
966
|
+
"tier": "tool",
|
|
967
|
+
"category": "consuming",
|
|
968
|
+
"title": "Web Page Reader",
|
|
969
|
+
"summary": "Open any URL in a headless browser (Playwright Python) and return the page's MAIN TEXT — title, headings, paragraphs, lists, code blocks and tables — as Markdown, plain text, or structured JSON. Boilerplate (nav / sidebar / comments / ads / footer) is stripped by a Readability-style pass, and JS-rendered pages work because a real browser runs the page. This is the tool to use whenever you need to KNOW WHAT A PAGE SAYS: summarizing an article, pulling source material for a script, reading a README or docs page, checking what a link contains. It returns text, not pictures — for a screenshot (png/jpg) use web_screenshot, for a recording (mp4/webm) use web_record. Output is capped by max_chars (default 20000) and truncated on a block boundary; pass an `output` path to keep the full text on disk.",
|
|
970
|
+
"triggers": [
|
|
971
|
+
"Read this link / what does this page say / summarize this article",
|
|
972
|
+
"Fetch page content, extract article text, get the text of a URL",
|
|
973
|
+
"Use a web page as source material for a script or video",
|
|
974
|
+
"Read a README / docs page / changelog / blog post",
|
|
975
|
+
"Pull code samples or tables out of a page"
|
|
976
|
+
],
|
|
977
|
+
"entry": {
|
|
978
|
+
"type": "python",
|
|
979
|
+
"scriptPath": "../web-screenshot/scripts/read_page.py"
|
|
980
|
+
},
|
|
981
|
+
"runtime": "python",
|
|
982
|
+
"envVars": [
|
|
983
|
+
"WEB_CAPTURE_BROWSER",
|
|
984
|
+
"WEB_CAPTURE_ALLOW_PRIVATE_HOSTS",
|
|
985
|
+
"PLAYWRIGHT_BROWSERS_PATH"
|
|
986
|
+
],
|
|
987
|
+
"parameters": {
|
|
988
|
+
"type": "object",
|
|
989
|
+
"properties": {
|
|
990
|
+
"url": {
|
|
991
|
+
"type": "string",
|
|
992
|
+
"description": "Target page URL (http/https). Private / loopback / link-local addresses are refused unless WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1."
|
|
993
|
+
},
|
|
994
|
+
"format": {
|
|
995
|
+
"type": "string",
|
|
996
|
+
"enum": [
|
|
997
|
+
"markdown",
|
|
998
|
+
"text",
|
|
999
|
+
"json"
|
|
1000
|
+
],
|
|
1001
|
+
"description": "markdown (default: keeps headings, lists, code fences, tables) | text (plain) | json (structured blocks + metadata, not truncated)"
|
|
1002
|
+
},
|
|
1003
|
+
"max_chars": {
|
|
1004
|
+
"type": "number",
|
|
1005
|
+
"description": "Cap on the printed text, cut at a block boundary with an explicit [truncated] notice (default 20000, 0 = unlimited). Raise it when you need the whole document; a very long page will otherwise fill your context."
|
|
1006
|
+
},
|
|
1007
|
+
"selector": {
|
|
1008
|
+
"type": "string",
|
|
1009
|
+
"description": "Read only inside this CSS selector. Leave empty to auto-detect the article container — only reach for this when the auto-detected container was wrong."
|
|
1010
|
+
},
|
|
1011
|
+
"include_links": {
|
|
1012
|
+
"type": "boolean",
|
|
1013
|
+
"description": "Keep hyperlinks as [text](url) instead of plain text. Useful when you need to follow links from the page."
|
|
1014
|
+
},
|
|
1015
|
+
"include_images": {
|
|
1016
|
+
"type": "boolean",
|
|
1017
|
+
"description": "Keep images as . Useful for harvesting illustration URLs out of an article."
|
|
1018
|
+
},
|
|
1019
|
+
"output": {
|
|
1020
|
+
"type": "string",
|
|
1021
|
+
"description": "Also write the FULL (untruncated) text to this local path. stdout still respects max_chars — use this when a long page must be kept for later steps."
|
|
1022
|
+
},
|
|
1023
|
+
"settle_ms": {
|
|
1024
|
+
"type": "number",
|
|
1025
|
+
"description": "Extra wait before extracting, in ms. Raise for pages that render content late."
|
|
1026
|
+
},
|
|
1027
|
+
"wait_for_selector": {
|
|
1028
|
+
"type": "string",
|
|
1029
|
+
"description": "Wait for this CSS selector before extracting (the reliable fix for JS-rendered content)"
|
|
1030
|
+
},
|
|
1031
|
+
"wait_for_timeout": {
|
|
1032
|
+
"type": "number",
|
|
1033
|
+
"description": "Fixed wait before extracting, in ms"
|
|
1034
|
+
},
|
|
1035
|
+
"device": {
|
|
1036
|
+
"type": "string",
|
|
1037
|
+
"description": "Device emulation name, e.g. 'iPhone 15 Pro' — some sites serve a leaner page to mobile"
|
|
1038
|
+
},
|
|
1039
|
+
"viewport": {
|
|
1040
|
+
"type": "string",
|
|
1041
|
+
"description": "Viewport as 'width,height', e.g. '1280,800'"
|
|
1042
|
+
},
|
|
1043
|
+
"color_scheme": {
|
|
1044
|
+
"type": "string",
|
|
1045
|
+
"enum": [
|
|
1046
|
+
"light",
|
|
1047
|
+
"dark",
|
|
1048
|
+
"no-preference"
|
|
1049
|
+
],
|
|
1050
|
+
"description": "Emulate prefers-color-scheme"
|
|
1051
|
+
},
|
|
1052
|
+
"user_agent": {
|
|
1053
|
+
"type": "string",
|
|
1054
|
+
"description": "Override the User-Agent (try this when a site blocks headless browsers)"
|
|
1055
|
+
},
|
|
1056
|
+
"timeout": {
|
|
1057
|
+
"type": "number",
|
|
1058
|
+
"description": "Global Playwright action timeout in ms"
|
|
1059
|
+
},
|
|
1060
|
+
"ignore_https_errors": {
|
|
1061
|
+
"type": "boolean",
|
|
1062
|
+
"description": "Ignore HTTPS certificate errors"
|
|
1063
|
+
},
|
|
1064
|
+
"storage_state": {
|
|
1065
|
+
"type": "string",
|
|
1066
|
+
"description": "Path to a Playwright storageState JSON file (logged-in session)"
|
|
1067
|
+
},
|
|
1068
|
+
"cookies": {
|
|
1069
|
+
"type": "string",
|
|
1070
|
+
"description": "Playwright cookies as a JSON string or a path to a JSON file (top level is an array)"
|
|
1071
|
+
},
|
|
1072
|
+
"browser": {
|
|
1073
|
+
"type": "string",
|
|
1074
|
+
"enum": [
|
|
1075
|
+
"chromium",
|
|
1076
|
+
"firefox",
|
|
1077
|
+
"webkit"
|
|
1078
|
+
],
|
|
1079
|
+
"description": "Browser engine (default chromium)"
|
|
1080
|
+
},
|
|
1081
|
+
"quiet": {
|
|
1082
|
+
"type": "boolean",
|
|
1083
|
+
"description": "Suppress the extraction diagnostics on stderr"
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
"required": [
|
|
1087
|
+
"url"
|
|
1088
|
+
]
|
|
1089
|
+
},
|
|
1090
|
+
"ui": {
|
|
1091
|
+
"primary": [
|
|
1092
|
+
"url",
|
|
1093
|
+
"format",
|
|
1094
|
+
"max_chars"
|
|
1095
|
+
],
|
|
1096
|
+
"advanced": [
|
|
1097
|
+
"selector",
|
|
1098
|
+
"include_links",
|
|
1099
|
+
"include_images",
|
|
1100
|
+
"wait_for_selector",
|
|
1101
|
+
"settle_ms",
|
|
1102
|
+
"device",
|
|
1103
|
+
"viewport",
|
|
1104
|
+
"color_scheme",
|
|
1105
|
+
"timeout"
|
|
1106
|
+
],
|
|
1107
|
+
"hidden": [
|
|
1108
|
+
"output",
|
|
1109
|
+
"quiet",
|
|
1110
|
+
"user_agent",
|
|
1111
|
+
"ignore_https_errors",
|
|
1112
|
+
"storage_state",
|
|
1113
|
+
"cookies",
|
|
1114
|
+
"browser",
|
|
1115
|
+
"wait_for_timeout"
|
|
1116
|
+
]
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
963
1119
|
{
|
|
964
1120
|
"id": "web-record",
|
|
965
1121
|
"toolName": "web_record",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remixmate/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.27",
|
|
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",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"smoke": "node scripts/smoke.mjs",
|
|
22
22
|
"test:cli": "npm run build && node --test test/*.test.mjs",
|
|
23
23
|
"test:validators": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-validators.py",
|
|
24
|
+
"test:web-read": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-web-read.py",
|
|
24
25
|
"test:template-pipeline": "PYTHONDONTWRITEBYTECODE=1 node dist/cli.js exec -- python3 scripts/test-template-pipeline.py",
|
|
25
26
|
"test:props-contract": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-props-contract.py",
|
|
27
|
+
"test:narration-speed": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-narration-speed.py",
|
|
26
28
|
"test:render-plan": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py",
|
|
27
29
|
"test:render-plan:update": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-render-plan-snapshot.py --update",
|
|
28
30
|
"test:contracts": "PYTHONDONTWRITEBYTECODE=1 python3 skills/template-registry/scripts/check_contracts.py",
|
|
@@ -401,6 +401,7 @@ python3 <SkillDir>/scripts/gen_script.py \
|
|
|
401
401
|
| `--ratio` | Aspect ratio. | `16:9` |
|
|
402
402
|
| `--scenes` | Scene count. | auto-planned |
|
|
403
403
|
| `--voice-id` | Narration voice id. | resolved from template + language fallback |
|
|
404
|
+
| `--speed` | Narration speech rate (0.5–2.0), written to `global.narration.speed` and applied when `prepare_video_assets` calls gen-voice. Pass it whenever the user picked a speed (the replicate form's 语速 field sends one). Above ~1.3 subtitle alignment drifts and the delivery turns mechanical — shorten the script instead. | `1.0` |
|
|
404
405
|
| `--allow-digital-human` | Whether to allow digital-human assets. | off |
|
|
405
406
|
| `--allow-ai-video` | Whether to allow AI-generated video assets. | off |
|
|
406
407
|
| `--validate` | Validate-only mode: only validate the input DSL. | — |
|
|
@@ -80,6 +80,24 @@ FALLBACK_VOICE = {
|
|
|
80
80
|
"en": "english-male-friendly-01",
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
# ── Narration speed ───────────────────────────────────────────────────────────
|
|
84
|
+
# Written to global.narration.speed; render_video copies it down into the
|
|
85
|
+
# gen-voice asset payload (same route as narration text) and passes it to
|
|
86
|
+
# `gen-voice --speed`. The envelope mirrors gen-voice's own 0.5–2.0 contract;
|
|
87
|
+
# the real per-voice limits (capabilities speedMin/speedMax) are only known at
|
|
88
|
+
# synthesis time, which is several confirmation steps and a few paid image
|
|
89
|
+
# generations later — so an obviously-out-of-range value fails here instead.
|
|
90
|
+
DEFAULT_NARRATION_SPEED = 1.0
|
|
91
|
+
SPEED_MIN = 0.5
|
|
92
|
+
SPEED_MAX = 2.0
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_narration_speed(speed):
|
|
96
|
+
"""CLI speed → the value written to global.narration.speed. Total function."""
|
|
97
|
+
if speed is None:
|
|
98
|
+
return DEFAULT_NARRATION_SPEED
|
|
99
|
+
return float(speed)
|
|
100
|
+
|
|
83
101
|
|
|
84
102
|
def _resolve_template_language_meta(template_config):
|
|
85
103
|
"""Tolerant reader for outputLanguage / defaultVoiceId on a template dict.
|
|
@@ -750,6 +768,7 @@ def _build_carousel_caption_dsl(
|
|
|
750
768
|
resolution: str,
|
|
751
769
|
output_language: str,
|
|
752
770
|
resolved_voice_id: str,
|
|
771
|
+
narration_speed: float,
|
|
753
772
|
font_id: str | None,
|
|
754
773
|
font_name: str | None,
|
|
755
774
|
narration_enabled: bool,
|
|
@@ -907,7 +926,7 @@ def _build_carousel_caption_dsl(
|
|
|
907
926
|
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),
|
|
908
927
|
# 并省略 global.narration,避免下游误判存在旁白。
|
|
909
928
|
"subtitle": {"enabled": narration_enabled, "style": "bottom"},
|
|
910
|
-
**({"narration": {"voiceId": resolved_voice_id, "speed":
|
|
929
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if narration_enabled else {}),
|
|
911
930
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
912
931
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
913
932
|
},
|
|
@@ -961,6 +980,7 @@ def build_dsl(
|
|
|
961
980
|
scene_count: int | None,
|
|
962
981
|
allow_digital_human: bool,
|
|
963
982
|
allow_ai_video: bool,
|
|
983
|
+
speed: float | None = None,
|
|
964
984
|
template_id: str | None = None,
|
|
965
985
|
stub_image_url: str = "",
|
|
966
986
|
stub_video_url: str = "",
|
|
@@ -992,6 +1012,10 @@ def build_dsl(
|
|
|
992
1012
|
图给第 i 个场景,没被占到的场景照常走 gen-image 补图。这条规则不是随便定的——
|
|
993
1013
|
见 docs/asset-annotation-design.md §6:另外两种立场(模型自由挑选 / 严格一一对应)
|
|
994
1014
|
都会让"只传两张图"要么失去可预期性,要么直接跑不通。
|
|
1015
|
+
|
|
1016
|
+
speed 是旁白语速倍率,落在 global.narration.speed(不给 = 1.0)。它**不写进
|
|
1017
|
+
gen-voice 资产的 payload**——与旁白文本同一条规矩:唯一来源在 DSL 上,
|
|
1018
|
+
render_video 在调 TTS 前按 assetRef 回查注入,免得两处存一个值各自漂移。
|
|
995
1019
|
"""
|
|
996
1020
|
# Load template config(registry 单一数据源)→ 归一化 contract
|
|
997
1021
|
template_config = _load_template_config(template_id) if template_id else None
|
|
@@ -1010,6 +1034,7 @@ def build_dsl(
|
|
|
1010
1034
|
# values fall back to "zh" so old templates keep their current behavior.
|
|
1011
1035
|
output_language, template_default_voice_id = _resolve_template_language_meta(template_config)
|
|
1012
1036
|
resolved_voice_id = _resolve_voice_id(voice_id, template_default_voice_id, output_language)
|
|
1037
|
+
narration_speed = _resolve_narration_speed(speed)
|
|
1013
1038
|
if resolved_voice_id != voice_id:
|
|
1014
1039
|
print(
|
|
1015
1040
|
f"ℹ️ voice resolution: cli={voice_id!r} → resolved={resolved_voice_id!r} "
|
|
@@ -1087,6 +1112,7 @@ def build_dsl(
|
|
|
1087
1112
|
resolution=resolution,
|
|
1088
1113
|
output_language=output_language,
|
|
1089
1114
|
resolved_voice_id=resolved_voice_id,
|
|
1115
|
+
narration_speed=narration_speed,
|
|
1090
1116
|
font_id=font_id,
|
|
1091
1117
|
font_name=font_name,
|
|
1092
1118
|
narration_enabled=needs_narration,
|
|
@@ -1377,7 +1403,7 @@ def build_dsl(
|
|
|
1377
1403
|
# 无配音模板:关闭 CC 字幕(字幕段是 narration→TTS 的派生物),并省略
|
|
1378
1404
|
# global.narration,避免下游误判存在旁白。
|
|
1379
1405
|
"subtitle": {"enabled": needs_narration, "style": "bottom"},
|
|
1380
|
-
**({"narration": {"voiceId": resolved_voice_id, "speed":
|
|
1406
|
+
**({"narration": {"voiceId": resolved_voice_id, "speed": narration_speed}} if needs_narration else {}),
|
|
1381
1407
|
**({"font": {"fontId": font_id, **({"fontName": font_name} if font_name else {})}} if font_id else {}),
|
|
1382
1408
|
"bgm": {"enabled": True, "volume": 0.12},
|
|
1383
1409
|
},
|
|
@@ -1416,7 +1442,18 @@ Examples:
|
|
|
1416
1442
|
parser.add_argument("--ratio", default=None, choices=VALID_RATIOS, help="Aspect ratio. When omitted: if --template-id is set, use the template's supportedAspectRatios[0]; otherwise default 16:9.")
|
|
1417
1443
|
parser.add_argument("--resolution", default="1080p", help="Resolution (default: 1080p)")
|
|
1418
1444
|
parser.add_argument("--scenes", type=int, default=None, help="Scene count (default: auto-planned)")
|
|
1419
|
-
parser.add_argument("--voice-id", default=None, help="Narration voice id. When omitted, the resolver picks template.defaultVoiceId, then the language-keyed fallback (zh→Chinese (Mandarin)_Male_Announcer, en→TBD English voice). gen_voice --list-voices
|
|
1445
|
+
parser.add_argument("--voice-id", default=None, help="Narration voice id. When omitted, the resolver picks template.defaultVoiceId, then the language-keyed fallback (zh→Chinese (Mandarin)_Male_Announcer, en→TBD English voice). gen_voice --list-voices prints the live catalog.")
|
|
1446
|
+
parser.add_argument(
|
|
1447
|
+
"--speed",
|
|
1448
|
+
type=float,
|
|
1449
|
+
default=None,
|
|
1450
|
+
help=(
|
|
1451
|
+
f"Narration speech rate ({SPEED_MIN}-{SPEED_MAX}, default {DEFAULT_NARRATION_SPEED}). "
|
|
1452
|
+
"Stored at global.narration.speed and applied by render_video when it calls gen-voice. "
|
|
1453
|
+
"Above ~1.3 subtitle alignment starts to drift and the delivery turns mechanical — "
|
|
1454
|
+
"shorten the script instead."
|
|
1455
|
+
),
|
|
1456
|
+
)
|
|
1420
1457
|
parser.add_argument(
|
|
1421
1458
|
"--font-id",
|
|
1422
1459
|
default=None,
|
|
@@ -1521,6 +1558,15 @@ Examples:
|
|
|
1521
1558
|
parser.print_help()
|
|
1522
1559
|
sys.exit(1)
|
|
1523
1560
|
|
|
1561
|
+
if args.speed is not None and not (SPEED_MIN <= args.speed <= SPEED_MAX):
|
|
1562
|
+
print(
|
|
1563
|
+
f"❌ --speed must be in the range {SPEED_MIN}~{SPEED_MAX} (got {args.speed}).\n"
|
|
1564
|
+
" Speech rate is a multiplier, not a percentage: 1.0 is the voice's own pace, "
|
|
1565
|
+
"1.1 is slightly brisker.",
|
|
1566
|
+
file=sys.stderr,
|
|
1567
|
+
)
|
|
1568
|
+
sys.exit(1)
|
|
1569
|
+
|
|
1524
1570
|
# ── 模板感知的 ratio 自动推导 ────────────────────────────────────────────
|
|
1525
1571
|
# 用户没显式传 --ratio 时:
|
|
1526
1572
|
# 1. 若指定了 --template-id,取模板 supportedAspectRatios[0](避免横竖屏不匹配)
|
|
@@ -1552,6 +1598,8 @@ Examples:
|
|
|
1552
1598
|
print(f" platform: {args.platform}", file=sys.stderr)
|
|
1553
1599
|
print(f" duration: {args.duration}s", file=sys.stderr)
|
|
1554
1600
|
print(f" ratio: {args.ratio}", file=sys.stderr)
|
|
1601
|
+
if args.speed is not None:
|
|
1602
|
+
print(f" narration speed: {args.speed}x", file=sys.stderr)
|
|
1555
1603
|
|
|
1556
1604
|
# CLI flag takes precedence; env vars act as fallback with a visible warning
|
|
1557
1605
|
# so silent cross-session leakage is always observable.
|
|
@@ -1592,6 +1640,7 @@ Examples:
|
|
|
1592
1640
|
ratio=args.ratio,
|
|
1593
1641
|
resolution=args.resolution,
|
|
1594
1642
|
voice_id=args.voice_id,
|
|
1643
|
+
speed=args.speed,
|
|
1595
1644
|
font_id=args.font_id,
|
|
1596
1645
|
font_name=args.font_name,
|
|
1597
1646
|
scene_count=args.scenes,
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"type": "string",
|
|
53
53
|
"description": "Narration voice id. Default depends on the bound template's outputLanguage; query gen_voice with list_voices=true to see available ids."
|
|
54
54
|
},
|
|
55
|
+
"speed": {
|
|
56
|
+
"type": "number",
|
|
57
|
+
"description": "Narration speech rate, 0.5-2.0 (default 1.0). Stored at global.narration.speed and applied when prepare_video_assets synthesizes the voice-over. **Must** pass when the user picked a speech rate — a template's 语速 / speed parameter arrives as a multiplier like 1.1, and without this flag it is silently dropped and the narration comes back at 1.0. Above ~1.3 subtitle alignment drifts and the delivery sounds mechanical; suggest shortening the script instead of raising it further."
|
|
58
|
+
},
|
|
55
59
|
"template_id": {
|
|
56
60
|
"type": "string",
|
|
57
61
|
"description": "Template id (e.g. html-slide). The template owns outputLanguage and may also declare a defaultVoiceId; both flow into the produced DSL."
|
|
@@ -114,6 +118,7 @@
|
|
|
114
118
|
"ratio",
|
|
115
119
|
"scenes",
|
|
116
120
|
"voice_id",
|
|
121
|
+
"speed",
|
|
117
122
|
"template_id",
|
|
118
123
|
"headline",
|
|
119
124
|
"subheadline",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"skillName": "gen-script",
|
|
3
3
|
"repoName": "agent-skill-media-maker",
|
|
4
4
|
"skillId": "474",
|
|
5
|
-
"version": "
|
|
5
|
+
"version": "V12",
|
|
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
|
}
|
|
@@ -46,13 +46,15 @@ There is no skill-local env file — the executing process inherits the system e
|
|
|
46
46
|
remixmate gen-voice --list-voices
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
The list above is the **authoritative** catalog — a voice id is invalid only if it is missing from it.
|
|
50
|
+
|
|
51
|
+
For offline development (no token / no network) there is a `--local` escape hatch. It prints the resolver's two hardcoded fallback ids, **not** the available voices, so never use it to decide whether a voice exists:
|
|
50
52
|
|
|
51
53
|
```bash
|
|
52
54
|
remixmate gen-voice --list-voices --local
|
|
53
55
|
```
|
|
54
56
|
|
|
55
|
-
|
|
57
|
+
It prints one line per entry as `<voice-id>\t<lang>\t<display-name>`.
|
|
56
58
|
|
|
57
59
|
### Default synthesis (URL output)
|
|
58
60
|
|
|
@@ -83,7 +85,7 @@ remixmate gen-voice --text "<text-to-synthesize>" --json-output
|
|
|
83
85
|
| `--voice-id` | Voice id (use `--list-voices` to discover) | `Chinese (Mandarin)_Male_Announcer` |
|
|
84
86
|
| `--speed` | Speech rate, 0.5–2.0 | `1.0` |
|
|
85
87
|
| `--list-voices` | List available voices and exit | — |
|
|
86
|
-
| `--local` |
|
|
88
|
+
| `--local` | CLI-only. With `--list-voices`: print the 2-entry hardcoded fallback list instead of querying the service (offline dev). Not exposed to the agent — see the handler comment. | off |
|
|
87
89
|
| `--json-output` | Emit JSON (url, audio_length_ms, subtitles) | off |
|
|
88
90
|
| `--priv-token` | Override token | env var |
|
|
89
91
|
|