@remixmate/cli 0.9.26 → 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
 
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.26",
4
- "generatedAt": "2026-09-13T00:51:13.551Z",
3
+ "version": "0.9.27",
4
+ "generatedAt": "2026-09-13T09:11:30.410Z",
5
5
  "skills": [
6
6
  {
7
7
  "id": "export-jianying",
@@ -636,7 +636,7 @@
636
636
  },
637
637
  "voice_id": {
638
638
  "type": "string",
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."
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."
640
640
  },
641
641
  "speed": {
642
642
  "type": "number",
@@ -644,11 +644,7 @@
644
644
  },
645
645
  "list_voices": {
646
646
  "type": "boolean",
647
- "description": "List available voices and exit"
648
- },
649
- "local": {
650
- "type": "boolean",
651
- "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."
652
648
  },
653
649
  "json_output": {
654
650
  "type": "boolean",
@@ -667,8 +663,7 @@
667
663
  ],
668
664
  "hidden": [
669
665
  "json_output",
670
- "list_voices",
671
- "local"
666
+ "list_voices"
672
667
  ]
673
668
  }
674
669
  },
@@ -965,6 +960,162 @@
965
960
  ]
966
961
  }
967
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
+ },
968
1119
  {
969
1120
  "id": "web-record",
970
1121
  "toolName": "web_record",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remixmate/cli",
3
- "version": "0.9.26",
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,6 +21,7 @@
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",
26
27
  "test:narration-speed": "PYTHONDONTWRITEBYTECODE=1 python3 scripts/test-narration-speed.py",
@@ -1442,7 +1442,7 @@ Examples:
1442
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.")
1443
1443
  parser.add_argument("--resolution", default="1080p", help="Resolution (default: 1080p)")
1444
1444
  parser.add_argument("--scenes", type=int, default=None, help="Scene count (default: auto-planned)")
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 --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
1446
  parser.add_argument(
1447
1447
  "--speed",
1448
1448
  type=float,
@@ -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
 
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "voice_id": {
27
27
  "type": "string",
28
- "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."
28
+ "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."
29
29
  },
30
30
  "speed": {
31
31
  "type": "number",
@@ -33,11 +33,7 @@
33
33
  },
34
34
  "list_voices": {
35
35
  "type": "boolean",
36
- "description": "List available voices and exit"
37
- },
38
- "local": {
39
- "type": "boolean",
40
- "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."
36
+ "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."
41
37
  },
42
38
  "json_output": {
43
39
  "type": "boolean",
@@ -56,8 +52,7 @@
56
52
  ],
57
53
  "hidden": [
58
54
  "json_output",
59
- "list_voices",
60
- "local"
55
+ "list_voices"
61
56
  ]
62
57
  }
63
58
  }
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: web-read
3
+ description: |
4
+ Web-page reading skill — open a URL in a headless browser (Playwright Python) and get back the page's **main text**: title, headings, paragraphs, lists, code blocks and tables, as Markdown / plain text / structured JSON.
5
+ 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 first.
6
+
7
+ Text only. For a still screenshot (`.png` / `.jpg`) use web-screenshot / `web_screenshot`; for a recording (`.mp4` / `.webm`) use web-record / `web_record`.
8
+
9
+ Use this skill immediately whenever the user asks for any of:
10
+ - Read this link / what does this page say / summarize this article
11
+ - Fetch page content, extract the article text, get the text of a URL
12
+ - Use a web page as source material for a script, outline, or video
13
+ - Read a README / docs page / changelog / blog post
14
+ - Pull the code samples or the table out of a page
15
+ - Check what is behind a link before acting on it
16
+
17
+ Even when the user does not say "read", any request that needs the *content* of a URL (rather than a picture of it) should route here.
18
+ triggers:
19
+ - Read this link / what does this page say / summarize this article
20
+ - Fetch page content, extract article text, get the text of a URL
21
+ - Use a web page as source material for a script or video
22
+ - Read a README / docs page / changelog / blog post
23
+ - Pull code samples or tables out of a page
24
+ ---
25
+
26
+ # Web Read Skill (`web_read`)
27
+
28
+ Turns a URL into text. Entry script **`read_page.py`**, pure Python, prints to **stdout**.
29
+
30
+ > **Text only.** A screenshot is `web_screenshot`, a recording is `web_record`. Those two produce *files*; this one produces *content you can reason about*.
31
+
32
+ **Script location**: this skill has no `scripts/` of its own — it reuses `read_page.py` and the `_media_screenshot/` package from the web-screenshot directory (`skill.json`'s `entry.scriptPath` points relatively at `../web-screenshot/scripts/read_page.py`). Everywhere the commands below say **`<ReadScript>`**, substitute:
33
+
34
+ ```
35
+ <SkillDir>/../web-screenshot/scripts/read_page.py
36
+ ```
37
+
38
+ > Path convention: read the system-injected `Base directory for this skill: <path>` as `<SkillDir>`. Never hardcode an absolute path.
39
+
40
+ ## Prerequisites
41
+
42
+ - **Python 3.9+**
43
+ - **The `playwright` pip package + the chromium engine**: the first run **bootstraps automatically** (`pip install playwright` + `playwright install chromium`).
44
+
45
+ ## Basic usage
46
+
47
+ ```bash
48
+ python3 <ReadScript> --url "https://example.com/article/123"
49
+ ```
50
+
51
+ Prints a Markdown document: an `# title` line, a `site · author · date · url` line, then the body. A one-line extraction diagnostic (`blocks / chars / container`) goes to **stderr**, so piping stdout gives you clean text.
52
+
53
+ ## Output formats
54
+
55
+ ```bash
56
+ # Markdown (default) — headings, lists, ``` code fences, | tables |
57
+ python3 <ReadScript> --url "https://docs.python.org/3/tutorial/introduction.html"
58
+
59
+ # Plain text — no markup, for TTS or keyword work
60
+ python3 <ReadScript> --url "https://example.com/post" --format text
61
+
62
+ # JSON — typed blocks + metadata, for programmatic consumption
63
+ python3 <ReadScript> --url "https://example.com/post" --format json
64
+ ```
65
+
66
+ The JSON shape is `{url, status, metadata{title,byline,siteName,publishedTime,description,lang}, container, charCount, blocks[]}`, where each block is one of `heading` / `paragraph` / `quote` / `code` / `list` / `table` / `image` / `rule`. JSON is **never truncated** (half a JSON document is not a JSON document) — cap it with `--selector` instead.
67
+
68
+ ## Length control (read this before pointing it at a long page)
69
+
70
+ `--max-chars` defaults to **20000** and cuts on a block boundary, appending an explicit `[truncated] 已显示 N / M 字符` notice. Nothing is silently dropped.
71
+
72
+ ```bash
73
+ # Keep the whole document on disk, read a bounded slice now
74
+ python3 <ReadScript> \
75
+ --url "https://example.com/very-long-guide" \
76
+ --max-chars 8000 \
77
+ --output "./guide.md"
78
+
79
+ # No cap at all
80
+ python3 <ReadScript> --url "https://example.com/post" --max-chars 0
81
+ ```
82
+
83
+ ## When the automatic extraction misses
84
+
85
+ The container is picked by paragraph-density scoring, which is right on ordinary article/docs/blog pages and can miss on unusual layouts. In order of what to try:
86
+
87
+ ```bash
88
+ # 1. Content renders late (SPA): wait for the real element
89
+ python3 <ReadScript> --url "https://app.example.com/doc/1" --wait-for-selector "article.body"
90
+
91
+ # 2. Still short: give it a fixed settle window
92
+ python3 <ReadScript> --url "https://example.com/x" --settle-ms 3000
93
+
94
+ # 3. Wrong part of the page: name the container yourself
95
+ python3 <ReadScript> --url "https://example.com/x" --selector "#main-content"
96
+
97
+ # 4. Behind a login
98
+ python3 <ReadScript> --url "https://example.com/x" --storage-state "./auth.json"
99
+ python3 <ReadScript> --url "https://example.com/x" --cookies '[{"name":"sid","value":"…","domain":"example.com","path":"/"}]'
100
+
101
+ # 5. Site serves headless browsers a stub page
102
+ python3 <ReadScript> --url "https://example.com/x" --user-agent "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
103
+ ```
104
+
105
+ The stderr diagnostics tell you which case you are in: `⚠️ 整页有 N 字符,却只抽到 M` means the wrong container was chosen (→ `--selector`), while `⚠️ 这个页面几乎没有文本` means the page itself never rendered text (→ waiting, cookies, or user-agent).
106
+
107
+ ## Links and images
108
+
109
+ Both are dropped by default, because they are noise for a summarize/rewrite task and they inflate the character budget.
110
+
111
+ ```bash
112
+ # Keep links as [text](url) — when you need to follow them
113
+ python3 <ReadScript> --url "https://example.com/index" --include-links
114
+
115
+ # Keep images as ![alt](src) — when harvesting illustration URLs
116
+ python3 <ReadScript> --url "https://example.com/post" --include-images
117
+ ```
118
+
119
+ ## Private addresses are refused
120
+
121
+ `web_read` hands page text to a model, so by default it refuses URLs that resolve to private / loopback / link-local addresses (`localhost`, `10.*`, `169.254.169.254`, …). Reading an intranet page or a local dev server on purpose:
122
+
123
+ ```bash
124
+ WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1 python3 <ReadScript> --url "http://localhost:5173/"
125
+ ```
126
+
127
+ ## Full flag list
128
+
129
+ | Flag | Meaning |
130
+ |---|---|
131
+ | `-u, --url` | Target URL (required, http/https) |
132
+ | `--format` | `markdown` (default) / `text` / `json` |
133
+ | `--max-chars` | stdout cap, block-aligned (default 20000, `0` = unlimited) |
134
+ | `--selector` | Extract only inside this CSS selector |
135
+ | `--include-links` / `--include-images` | Keep `[text](url)` / `![alt](src)` |
136
+ | `-o, --output` | Write the **full** text to a file (stdout stays capped) |
137
+ | `--settle-ms` | Extra wait before extracting |
138
+ | `--quiet` | Suppress the stderr diagnostic line |
139
+ | `-b, --browser` | `chromium` (default) / `firefox` / `webkit` |
140
+ | `--device`, `--viewport`, `--color-scheme`, `--user-agent` | Emulation |
141
+ | `--wait-for-selector`, `--wait-for-timeout`, `--timeout` | Waiting |
142
+ | `--storage-state`, `--cookies`, `--ignore-https-errors` | Session |
143
+
144
+ ## Exit codes
145
+
146
+ `0` on success — including a page that legitimately has little text. Non-zero only for a refused URL, an unresolvable selector, or a navigation failure; each prints a single actionable line rather than a Python traceback.