@remixmate/cli 0.1.0

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.
Files changed (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +166 -0
  3. package/dist/argv.d.ts +15 -0
  4. package/dist/argv.js +51 -0
  5. package/dist/cli.d.ts +13 -0
  6. package/dist/cli.js +83 -0
  7. package/dist/handlers/gen-image.d.ts +13 -0
  8. package/dist/handlers/gen-image.js +161 -0
  9. package/dist/handlers/gen-voice.d.ts +16 -0
  10. package/dist/handlers/gen-voice.js +110 -0
  11. package/dist/handlers/index.d.ts +13 -0
  12. package/dist/handlers/index.js +14 -0
  13. package/dist/handlers/template-bind.d.ts +15 -0
  14. package/dist/handlers/template-bind.js +149 -0
  15. package/dist/http.d.ts +52 -0
  16. package/dist/http.js +110 -0
  17. package/dist/index.d.ts +9 -0
  18. package/dist/index.js +8 -0
  19. package/dist/manifest.json +260 -0
  20. package/dist/progress.d.ts +23 -0
  21. package/dist/progress.js +15 -0
  22. package/dist/registry.d.ts +33 -0
  23. package/dist/registry.js +72 -0
  24. package/dist/runner.d.ts +17 -0
  25. package/dist/runner.js +63 -0
  26. package/package.json +54 -0
  27. package/skills/export-jianying/SKILL.md +251 -0
  28. package/skills/export-jianying/scripts/gen_jianying_draft.py +1099 -0
  29. package/skills/export-jianying/skill.json +24 -0
  30. package/skills/export-jianying/version.json +7 -0
  31. package/skills/gen-digital-human/SKILL.md +211 -0
  32. package/skills/gen-digital-human/scripts/gen_digital_human_video.py +574 -0
  33. package/skills/gen-digital-human/skill.json +23 -0
  34. package/skills/gen-digital-human/version.json +7 -0
  35. package/skills/gen-image/SKILL.md +130 -0
  36. package/skills/gen-image/skill.json +22 -0
  37. package/skills/gen-image/version.json +7 -0
  38. package/skills/gen-script/SKILL.md +283 -0
  39. package/skills/gen-script/scripts/gen_script.py +1208 -0
  40. package/skills/gen-script/skill.json +41 -0
  41. package/skills/gen-script/version.json +7 -0
  42. package/skills/gen-video/SKILL.md +170 -0
  43. package/skills/gen-video/scripts/gen_video.py +591 -0
  44. package/skills/gen-video/skill.json +24 -0
  45. package/skills/gen-video/version.json +7 -0
  46. package/skills/gen-voice/SKILL.md +104 -0
  47. package/skills/gen-voice/skill.json +21 -0
  48. package/skills/gen-voice/version.json +7 -0
  49. package/skills/prepare-video-assets/SKILL.md +192 -0
  50. package/skills/prepare-video-assets/scripts/prepare_video_assets.py +57 -0
  51. package/skills/prepare-video-assets/skill.json +23 -0
  52. package/skills/prepare-video-assets/version.json +7 -0
  53. package/skills/render-video/SKILL.md +187 -0
  54. package/skills/render-video/scripts/_chrome_vendor.py +305 -0
  55. package/skills/render-video/scripts/_video_probe.py +174 -0
  56. package/skills/render-video/scripts/_vod_polling.py +86 -0
  57. package/skills/render-video/scripts/remote_renderer_client.py +236 -0
  58. package/skills/render-video/scripts/render_video.py +2288 -0
  59. package/skills/render-video/scripts/upload_video.py +193 -0
  60. package/skills/render-video/skill.json +21 -0
  61. package/skills/render-video/version.json +7 -0
  62. package/skills/template-bind/README.md +63 -0
  63. package/skills/template-bind/SKILL.md +234 -0
  64. package/skills/template-bind/scripts/check_contracts.py +196 -0
  65. package/skills/template-bind/scripts/match_template.py +215 -0
  66. package/skills/template-bind/scripts/registry_loader.py +379 -0
  67. package/skills/template-bind/scripts/render_job_client.py +162 -0
  68. package/skills/template-bind/scripts/sync_registry.py +44 -0
  69. package/skills/template-bind/scripts/template_paths.py +72 -0
  70. package/skills/template-bind/skill.json +20 -0
  71. package/skills/template-bind/version.json +7 -0
  72. package/skills/template-bind/video_dsl/README.md +97 -0
  73. package/skills/template-bind/video_dsl/runtime/__init__.py +11 -0
  74. package/skills/template-bind/video_dsl/runtime/dsl_validator.py +495 -0
  75. package/skills/template-bind/video_dsl/runtime/prompt_enhancer.py +274 -0
  76. package/skills/template-bind/video_dsl/runtime/template_binder.py +87 -0
  77. package/skills/template-bind/video_dsl/runtime/timeline_compiler.py +312 -0
  78. package/skills/template-bind/video_dsl/schema/render-plan-v1alpha1.json +193 -0
  79. package/skills/template-bind/video_dsl/schema/template-binding-v1alpha1.json +122 -0
  80. package/skills/template-bind/video_dsl/schema/template-definition-v1alpha1.json +247 -0
  81. package/skills/template-bind/video_dsl/schema/video-dsl-v1alpha1.json +378 -0
  82. package/skills/video-parser/SKILL.md +238 -0
  83. package/skills/video-parser/scripts/analyze_video.py +1038 -0
  84. package/skills/video-parser/scripts/deconstruct_video.py +602 -0
  85. package/skills/video-parser/scripts/parse_via_render.py +148 -0
  86. package/skills/video-parser/skill.json +21 -0
  87. package/skills/video-parser/version.json +7 -0
  88. package/skills/web-capture/SKILL.md +183 -0
  89. package/skills/web-capture/scripts/web_capture.py +461 -0
  90. package/skills/web-capture/skill.json +29 -0
  91. package/skills/web-capture/version.json +7 -0
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 视频解构脚本 — 通过 ab-render HTTP 接口实现,无需本地 ffmpeg。
4
+
5
+ 调用链:
6
+ POST $RENDER_API_URL/parse → { taskId }
7
+ POST $RENDER_API_URL/parseStatus → { status, progress, result?, error? }
8
+
9
+ 必要环境变量(由 ab-agent 自动注入):
10
+ RENDER_API_URL ab-render 服务地址,例如 http://localhost:3000
11
+ PRIV_TOKEN ab-api 私有 token
12
+
13
+ 可选环境变量:
14
+ CONVERSATION_ID 对话 ID,用于文件关联
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import sys
21
+ import time
22
+ import urllib.error
23
+ import urllib.request
24
+
25
+ # ─── 环境变量 ─────────────────────────────────────────────────────────────────
26
+
27
+ RENDER_API_URL = os.environ.get("RENDER_API_URL", "").rstrip("/")
28
+ PRIV_TOKEN = os.environ.get("PRIV_TOKEN", "")
29
+ CONVERSATION_ID = os.environ.get("CONVERSATION_ID", "")
30
+
31
+ # ─── HTTP 工具 ────────────────────────────────────────────────────────────────
32
+
33
+ def api_post(path: str, body: dict, timeout: int = 30) -> dict:
34
+ """向 ab-render 发送 POST 请求,返回解析后的 JSON。"""
35
+ url = f"{RENDER_API_URL}{path}"
36
+ data = json.dumps(body).encode()
37
+
38
+ headers: dict[str, str] = {
39
+ "Content-Type": "application/json",
40
+ "X-Priv-Token": PRIV_TOKEN,
41
+ }
42
+ if CONVERSATION_ID:
43
+ headers["x-conversation-id"] = CONVERSATION_ID
44
+
45
+ req = urllib.request.Request(url, data=data, headers=headers, method="POST")
46
+ try:
47
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
48
+ return json.loads(resp.read())
49
+ except urllib.error.HTTPError as e:
50
+ body_text = e.read().decode(errors="replace")
51
+ raise RuntimeError(f"HTTP {e.code}: {body_text}") from e
52
+
53
+
54
+ def log(msg: str, silent: bool = False) -> None:
55
+ if not silent:
56
+ print(msg, flush=True)
57
+
58
+
59
+ # ─── 主逻辑 ───────────────────────────────────────────────────────────────────
60
+
61
+ def main() -> None:
62
+ parser = argparse.ArgumentParser(
63
+ description="Video deconstruction — calls the ab-render /parse endpoint (no local ffmpeg dependency)"
64
+ )
65
+ parser.add_argument("--url", required=True, help="Remote video URL")
66
+ parser.add_argument(
67
+ "--scene-threshold", type=float, default=0.3,
68
+ help="Scene-change sensitivity 0.0-1.0 (default 0.3)",
69
+ )
70
+ parser.add_argument("--skip-asr", action="store_true", help="Skip the ASR step")
71
+ parser.add_argument("--skip-keyframes", action="store_true", help="Skip keyframe extraction")
72
+ parser.add_argument(
73
+ "--json-output", action="store_true",
74
+ help="Pipeline mode: emit only the final JSON to stdout",
75
+ )
76
+ args = parser.parse_args()
77
+
78
+ # ── Pre-flight check ─────────────────────────────────────────────────────
79
+ if not RENDER_API_URL:
80
+ print(
81
+ "❌ RENDER_API_URL env var is not configured.\n"
82
+ " Add render_api_url: \"http://<ab-render-host>:<port>\" to Nacos ab-agent.yaml",
83
+ file=sys.stderr,
84
+ )
85
+ sys.exit(1)
86
+
87
+ # ── 1. Submit the deconstruction task ───────────────────────────────────
88
+ log(f"[video-parser] Submitting deconstruction task: {args.url[:80]}", args.json_output)
89
+
90
+ resp = api_post("/parse", {
91
+ "videoUrl": args.url,
92
+ "sceneThreshold": args.scene_threshold,
93
+ "skipAsr": args.skip_asr,
94
+ "skipKeyframes": args.skip_keyframes,
95
+ })
96
+
97
+ if resp.get("code") != 0:
98
+ print(f"❌ submission failed: {resp.get('msg', 'unknown error')}", file=sys.stderr)
99
+ sys.exit(1)
100
+
101
+ task_id: str = resp["data"]["taskId"]
102
+ log(f"[video-parser] task accepted, taskId={task_id}", args.json_output)
103
+
104
+ # ── 2. Poll status until completion ─────────────────────────────────────
105
+ max_wait_sec = 600 # Wait up to 10 minutes
106
+ poll_interval = 3 # Poll every 3 seconds
107
+ elapsed = 0
108
+
109
+ while elapsed < max_wait_sec:
110
+ time.sleep(poll_interval)
111
+ elapsed += poll_interval
112
+
113
+ try:
114
+ status_resp = api_post("/parseStatus", {"taskId": task_id})
115
+ except Exception as e: # noqa: BLE001
116
+ log(f"[video-parser] polling error (will retry): {e}", args.json_output)
117
+ continue
118
+
119
+ if status_resp.get("code") != 0:
120
+ log(f"[video-parser] polling returned an error code (will retry): {status_resp.get('msg')}", args.json_output)
121
+ continue
122
+
123
+ task = status_resp["data"]
124
+ status = task.get("status", "unknown")
125
+ progress = task.get("progress", 0)
126
+
127
+ log(f"[video-parser] status={status} progress={int(progress * 100)}% elapsed={elapsed}s", args.json_output)
128
+
129
+ if status == "succeeded":
130
+ result = task.get("result")
131
+ if args.json_output:
132
+ print(json.dumps(result, ensure_ascii=False, indent=2))
133
+ else:
134
+ print("\n✅ Deconstruction complete")
135
+ print(json.dumps(result, ensure_ascii=False, indent=2))
136
+ return
137
+
138
+ if status == "failed":
139
+ error = task.get("error", "unknown error")
140
+ print(f"❌ deconstruction failed: {error}", file=sys.stderr)
141
+ sys.exit(1)
142
+
143
+ print(f"❌ deconstruction timed out (over {max_wait_sec}s)", file=sys.stderr)
144
+ sys.exit(1)
145
+
146
+
147
+ if __name__ == "__main__":
148
+ main()
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "video-parser",
3
+ "toolName": "video_parser",
4
+ "tier": "tool",
5
+ "title": "Video Deconstruction",
6
+ "description": "Video deconstruction: split a video into reusable assets (audio extraction, ASR transcript, keyframe extraction, scene segmentation).",
7
+ "envVars": ["PRIV_TOKEN", "MM_API_BASE_URL", "AGENT_NAME", "RENDER_API_URL"],
8
+ "scriptPath": "scripts/parse_via_render.py",
9
+ "parameters": {
10
+ "type": "object",
11
+ "properties": {
12
+ "url": { "type": "string", "description": "Remote video URL (or use local)" },
13
+ "local": { "type": "string", "description": "Local video path (or use url)" },
14
+ "scene_threshold": { "type": "number", "description": "Scene-cut detection threshold 0.0-1.0 (default 0.3)" },
15
+ "skip_asr": { "type": "boolean", "description": "Skip the ASR step" },
16
+ "skip_keyframes": { "type": "boolean", "description": "Skip the keyframe-extraction step" },
17
+ "json_output": { "type": "boolean", "description": "Pipeline mode — emit JSON only on stdout" }
18
+ },
19
+ "required": []
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "skillName": "video-parser",
3
+ "repoName": "agent-skill-media-maker",
4
+ "skillId": "553",
5
+ "version": "V1",
6
+ "skillDescription": "视频解构技能,将视频拆解为可复用的内容资产(音频、ASR 文本、关键帧、场景分段)。\n\n当用户提到以下任何需求时,立即使用本 skill:\n- 视频解构、视频拆解、视频分析、解析视频\n- 提取关键帧、提取视频文案、视频转文字\n- 视频内容资产、视频素材提取\n\n即使用户没有明确说「解构」,只要他们想要从视频中提取文案、关键帧或结构化信息,也要使用本 skill。"
7
+ }
@@ -0,0 +1,183 @@
1
+ ---
2
+ name: web-capture
3
+ description: |
4
+ Web page capture skill. Drives a headless browser (Playwright) to turn a URL into reusable visual assets — viewport / full-page screenshots, or a smooth top-to-bottom scrolling video (MP4). These assets are the upstream material for the `screen-walkthrough` template.
5
+
6
+ Use this skill as soon as the user mentions any of these intents:
7
+ - Screenshot a web page / website / a specific URL, full-page screenshot
8
+ - Record a page scrolling top-to-bottom into a video, "scroll video" of a site
9
+ - Produce screen-recording / walkthrough footage of a GitHub repo, landing page, or product page
10
+
11
+ Even when the user does not say "Playwright" or "browser", use this skill whenever they want to turn a web address into a screenshot or a scrolling demo video.
12
+ triggers:
13
+ - Screenshot a web page / website / a specific URL, full-page screenshot
14
+ - Record a page scrolling top-to-bottom into a video, "scroll video" of a site
15
+ - Produce screen-recording / walkthrough footage of a GitHub repo, landing page, or product page
16
+ ---
17
+
18
+ # Web Page Capture Skill
19
+
20
+ Drives a **headless browser via Playwright (Python)** to capture a web page as reusable visual assets:
21
+
22
+ - **Screenshots** — viewport, full scrollable page, or a single element.
23
+ - **Scroll video** — opens the page and smoothly scrolls top-to-bottom, capturing frames and assembling a deterministic MP4 with ffmpeg.
24
+ - **Record video** — Playwright's native context recording (webm), transcoded to MP4.
25
+
26
+ The MP4 / PNG output is designed to feed the **`screen-walkthrough`** template's screen stage.
27
+
28
+ ## Dependencies
29
+
30
+ Aligned with method 1 of the design — the runtime provides the browser, the skill does not bundle it.
31
+
32
+ - **Python 3** with **Playwright**: `pip install playwright` then `playwright install chromium` (one-time; downloads the browser binary).
33
+ - **ffmpeg**: must be installed locally (`brew install ffmpeg`) — used to assemble the scroll-video frames and transcode native recordings to MP4. Same dependency the `video-parser` skill relies on.
34
+
35
+ The script checks for both at startup and prints an actionable install hint if either is missing (`__progress__` phase `web-capture:preflight`).
36
+
37
+ ## Multi-user isolation & security
38
+
39
+ ab-agent spawns one fresh `python3` process per tool call (`spawn('python3', ...)`), and this script launches its own Playwright browser instance per run and closes it on exit. So **concurrent users are isolated at the process + browser-instance level** — no cookie/session bleed between users. The skill hardens a few defaults on top of that:
40
+
41
+ - **Unique output dir**: when `--output-dir` is omitted, output goes to a unique system temp dir (`tempfile.mkdtemp`), never the script's cwd. Concurrent calls can't collide or pollute `skills/`.
42
+ - **Temp cleanup**: per-frame images (`_frames/`) and native-recording webm (`_rec/`) are removed in a `finally` block, so failures don't leak temp data.
43
+ - **SSRF guard**: the user-supplied `url` is validated before any browser launch — only `http`/`https`, and the resolved IP must not be private / loopback / link-local / reserved / cloud-metadata (`169.254.169.254`). Rejection exits with code 5.
44
+ - **Sandbox on by default**: Chromium keeps its sandbox; `--no-sandbox` is only added when `WEB_CAPTURE_NO_SANDBOX=1`.
45
+
46
+ Not handled in-skill (orchestration concern): **concurrency limits**. Each browser is ~150–300MB; many simultaneous `scroll-video` calls can exhaust host memory. Gate concurrency at the ab-agent layer or via host resources.
47
+
48
+ ### Environment variables
49
+
50
+ | Env var | Description | Default |
51
+ |---------|-------------|---------|
52
+ | `WEB_CAPTURE_BROWSER` | Default browser engine (`--browser` overrides). | `chromium` |
53
+ | `WEB_CAPTURE_OUTPUT_BASE` | Base dir for the auto-created temp output dir. | system tempdir |
54
+ | `WEB_CAPTURE_NO_SANDBOX` | Set `1` to add `--no-sandbox` (only for containers that can't use user namespaces). | unset (sandbox on) |
55
+ | `WEB_CAPTURE_ALLOW_PRIVATE_HOSTS` | Set `1` to disable the private/internal-IP SSRF block (single-tenant / trusted intranet only). | unset (block on) |
56
+ | `PLAYWRIGHT_BROWSERS_PATH` | Playwright browser binary location (provided by the runtime). | Playwright default |
57
+
58
+ ## Operations
59
+
60
+ 1. **Script path**: read the system-injected `Base directory for this skill: <path>` as `<SkillDir>` and always use `<SkillDir>/scripts/web_capture.py`. Never hard-code paths.
61
+ 2. **Pick the mode**: `screenshot` (default), `scroll-video`, or `record-video`.
62
+ 3. **Run the capture** and wait for it to finish.
63
+ 4. **Inspect the result**: a `capture.json` manifest plus the produced `*.png` / `*.mp4` under `--output-dir`.
64
+
65
+ ### Viewport screenshot
66
+
67
+ ```bash
68
+ python3 <SkillDir>/scripts/web_capture.py \
69
+ --url "https://github.com/microsoft/playwright" \
70
+ --mode screenshot
71
+ ```
72
+
73
+ ### Full-page screenshot (whole scrollable page)
74
+
75
+ ```bash
76
+ python3 <SkillDir>/scripts/web_capture.py \
77
+ --url "https://example.com" \
78
+ --mode screenshot \
79
+ --full-page
80
+ ```
81
+
82
+ ### Element-only screenshot
83
+
84
+ ```bash
85
+ python3 <SkillDir>/scripts/web_capture.py \
86
+ --url "https://example.com" \
87
+ --mode screenshot \
88
+ --selector "#readme"
89
+ ```
90
+
91
+ ### Scrolling video (top-to-bottom)
92
+
93
+ ```bash
94
+ python3 <SkillDir>/scripts/web_capture.py \
95
+ --url "https://github.com/microsoft/playwright" \
96
+ --mode scroll-video \
97
+ --width 1280 --height 720 \
98
+ --fps 30 \
99
+ --scroll-duration 8
100
+ ```
101
+
102
+ Produces a deterministic `scroll.mp4` — fps, resolution and scroll speed are all controllable, which keeps the output aligned with the `screen-walkthrough` timeline.
103
+
104
+ ### Native recording (webm → mp4)
105
+
106
+ ```bash
107
+ python3 <SkillDir>/scripts/web_capture.py \
108
+ --url "https://example.com" \
109
+ --mode record-video \
110
+ --scroll-duration 6
111
+ ```
112
+
113
+ Simpler than frame-capture but the frame rate is not precisely controllable; prefer `scroll-video` when timing matters.
114
+
115
+ ### JSON pipeline mode
116
+
117
+ ```bash
118
+ python3 <SkillDir>/scripts/web_capture.py \
119
+ --url "https://example.com" \
120
+ --mode screenshot \
121
+ --json-output
122
+ ```
123
+
124
+ Prints only the final `capture.json` payload to stdout — no progress lines — suitable for piping into downstream skills.
125
+
126
+ See every option: `python3 <SkillDir>/scripts/web_capture.py --help`.
127
+
128
+ ## Common CLI flags
129
+
130
+ | Flag | Description | Default |
131
+ |------|-------------|---------|
132
+ | `--url` | Target page URL (http/https). | required |
133
+ | `--mode` | `screenshot` / `scroll-video` / `record-video`. | `screenshot` |
134
+ | `-o` / `--output-dir` | Output directory. | unique system temp dir |
135
+ | `--width` | Viewport width (px). | `1280` |
136
+ | `--height` | Viewport height (px). | `720` |
137
+ | `--device-scale-factor` | Device pixel ratio (crisp / retina). | `2` |
138
+ | `--full-page` | Screenshot mode: capture the whole scrollable page. | off |
139
+ | `--selector` | Screenshot mode: capture only this CSS element. | none |
140
+ | `--fps` | Scroll / record video frame rate. | `30` |
141
+ | `--scroll-duration` | Seconds for the full top-to-bottom scroll. | `8` |
142
+ | `--browser` | `chromium` / `firefox` / `webkit`. | `chromium` |
143
+ | `--wait-until` | `load` / `domcontentloaded` / `networkidle`. | `networkidle` |
144
+ | `--wait-ms` | Extra wait after load before capture (ms). | `1500` |
145
+ | `--json-output` | Pipeline mode — JSON-only stdout. | off |
146
+
147
+ ## Output layout
148
+
149
+ ```
150
+ <output-dir>/
151
+ ├── capture.json # asset manifest (mode, url, viewport, produced files)
152
+ ├── screenshot.png # screenshot mode
153
+ └── scroll.mp4 # scroll-video / record-video mode
154
+ ```
155
+
156
+ ### `capture.json` shape
157
+
158
+ ```json
159
+ {
160
+ "source": { "url": "https://example.com", "finalUrl": "https://example.com/" },
161
+ "mode": "scroll-video",
162
+ "viewport": { "width": 1280, "height": 720, "deviceScaleFactor": 2 },
163
+ "outputs": [
164
+ { "type": "video", "localPath": "<output-dir>/scroll.mp4", "fps": 30, "durationSec": 8.0, "frames": 240 }
165
+ ]
166
+ }
167
+ ```
168
+
169
+ For `screenshot` mode each entry is `{ "type": "image", "localPath": "...", "fullPage": false }`.
170
+
171
+ ## Notes on the deprecated `microsoft/playwright-cli`
172
+
173
+ The original `microsoft/playwright-cli` repository was **archived (read-only) in January 2021** and is deprecated; its functionality was folded into the main `playwright` package. This skill therefore uses the maintained **Playwright Python** library directly. Pure screenshots are also available via `npx playwright screenshot`, but scrolling-video capture requires script-driven scrolling + frame assembly, which is what this skill provides.
174
+
175
+ ## Error handling
176
+
177
+ - **Playwright not installed**: the script detects it and prints `pip install playwright && playwright install chromium`.
178
+ - **Browser binary missing** (`Executable doesn't exist`): run `playwright install chromium` (or the chosen browser).
179
+ - **ffmpeg not installed** (video modes): the script prompts `brew install ffmpeg`.
180
+ - **URL rejected (exit 5)**: non-http(s) scheme, or the host resolves to a private/internal/metadata address. Use a public URL, or set `WEB_CAPTURE_ALLOW_PRIVATE_HOSTS=1` for a trusted intranet target.
181
+ - **Navigation timeout**: the page took too long; try `--wait-until load` or a smaller `--wait-ms`.
182
+ - **Blank / partial capture**: increase `--wait-ms`, or some sites block headless browsers.
183
+ - **Network**: verify the URL is reachable from the runtime host.