@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 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
- 11 skills covering the full short-video production pipeline: image / video / voice / digital-human asset generation, web page capture, script planning, template binding, Remotion rendering, Jianying (CapCut) draft export, and video deconstruction.
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 (8 of 12) require `python3 >= 3.10`. `web-screenshot` / `web-record` 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.
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 11 AI media generation skills covering the full pipeline from asset generation, script orchestration, template binding, and video rendering to Jianying export.
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
- └── web-record/ # tool: headless-browser recording (entry -> web-screenshot/scripts/record.py)
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
- 包含 11 个技能,覆盖完整的短视频生产链路:图片 / 视频 / 语音 / 数字人素材生成、网页捕获、脚本编排、模板绑定、Remotion 渲染、剪映(CapCut)草稿导出,以及视频解构。
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 技能(12 个中的 8 个)需要 `python3 >= 3.10`。`web-screenshot` / `web-record` 需要 Playwright(首次运行自动安装 chromium)。`ffmpeg` 仅 `video-parser` 的可选本地工具(`deconstruct_video.py`)需要;`video-parser` 默认入口走 ab-render 服务端解构,无需本地 ffmpeg。
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
- 本项目包含 12 个 AI 媒体生成技能,覆盖从素材生成、脚本编排、模板绑定、视频渲染到剪映导出的完整视频内容生产链路。
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
- └── web-record/ # 工具: 无头浏览器录屏(入口指向 web-screenshot/scripts/record.py)
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
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.9.25",
4
- "generatedAt": "2026-09-08T15:32:17.519Z",
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 ![alt](src). 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.25",
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": 1.0}} if narration_enabled else {}),
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": 1.0}} if needs_narration else {}),
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 --local prints the language-tagged catalog.")
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": "V11",
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
- To inspect the local language-tagged fallback catalog used by the voice resolver (no remote API call), add `--local`:
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
- The local catalog prints one voice per line as `<voice-id>\t<lang>\t<display-name>`.
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` | Used with `--list-voices`: print the local fallback catalog (offline) | off |
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