@remixmate/cli 0.1.0 → 0.1.2
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 +100 -83
- package/README.zh-CN.md +183 -0
- package/dist/handlers/gen-digital-human.d.ts +12 -0
- package/dist/handlers/gen-digital-human.js +157 -0
- package/dist/handlers/gen-image.js +5 -41
- package/dist/handlers/gen-video.d.ts +18 -0
- package/dist/handlers/gen-video.js +153 -0
- package/dist/handlers/gen-voice.js +2 -12
- package/dist/handlers/index.js +4 -2
- package/dist/handlers/shared.d.ts +26 -0
- package/dist/handlers/shared.js +66 -0
- package/dist/http.d.ts +4 -2
- package/dist/http.js +5 -3
- package/dist/manifest.json +38 -24
- package/dist/registry.d.ts +5 -16
- package/dist/registry.js +11 -17
- package/dist/skill-schema.d.ts +65 -0
- package/dist/skill-schema.js +53 -0
- package/package.json +5 -4
- package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
- package/skills/export-jianying/skill.json +1 -0
- package/skills/gen-digital-human/SKILL.md +23 -46
- package/skills/gen-digital-human/skill.json +4 -2
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +2 -2
- package/skills/gen-image/skill.json +3 -1
- package/skills/gen-image/version.json +1 -1
- package/skills/gen-script/SKILL.md +13 -13
- package/skills/gen-script/scripts/gen_script.py +50 -12
- package/skills/gen-script/skill.json +3 -1
- package/skills/gen-video/SKILL.md +22 -33
- package/skills/gen-video/skill.json +4 -2
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/gen-voice/skill.json +1 -0
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/prepare-video-assets/skill.json +2 -1
- package/skills/render-video/scripts/render_video.py +8 -8
- package/skills/render-video/skill.json +1 -0
- package/skills/{template-bind → template-registry}/README.md +4 -4
- package/skills/{template-bind → template-registry}/SKILL.md +14 -12
- package/skills/template-registry/scripts/list_templates.py +214 -0
- package/skills/{template-bind → template-registry}/scripts/match_template.py +4 -3
- package/skills/{template-bind → template-registry}/scripts/registry_loader.py +1 -1
- package/skills/{template-bind → template-registry}/scripts/template_paths.py +7 -7
- package/skills/{template-bind → template-registry}/skill.json +8 -5
- package/skills/template-registry/version.json +7 -0
- package/skills/{template-bind → template-registry}/video_dsl/README.md +2 -2
- package/skills/{template-bind → template-registry}/video_dsl/runtime/__init__.py +1 -1
- package/skills/video-parser/SKILL.md +47 -160
- package/skills/video-parser/skill.json +4 -4
- package/skills/video-parser/version.json +1 -1
- package/skills/web-capture/SKILL.md +407 -116
- package/skills/web-capture/scripts/_media_screenshot/__init__.py +21 -0
- package/skills/web-capture/scripts/_media_screenshot/bootstrap.py +34 -0
- package/skills/web-capture/scripts/_media_screenshot/browser.py +86 -0
- package/skills/web-capture/scripts/_media_screenshot/cli_args.py +111 -0
- package/skills/web-capture/scripts/_media_screenshot/js/arrow.js +86 -0
- package/skills/web-capture/scripts/_media_screenshot/js/caption.js +27 -0
- package/skills/web-capture/scripts/_media_screenshot/js/clear_overlay.js +5 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_bootstrap.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_hide.js +6 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_move.js +12 -0
- package/skills/web-capture/scripts/_media_screenshot/js/cursor_ripple.js +25 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom.js +72 -0
- package/skills/web-capture/scripts/_media_screenshot/js/element_zoom_restore.js +21 -0
- package/skills/web-capture/scripts/_media_screenshot/js/fade_out_overlays.js +10 -0
- package/skills/web-capture/scripts/_media_screenshot/js/focus.js +38 -0
- package/skills/web-capture/scripts/_media_screenshot/js/highlight.js +97 -0
- package/skills/web-capture/scripts/_media_screenshot/js/overlay_bootstrap.js +19 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_in.js +54 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_out.js +11 -0
- package/skills/web-capture/scripts/_media_screenshot/js/page_zoom_restore.js +13 -0
- package/skills/web-capture/scripts/_media_screenshot/js/redact.js +33 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll.js +26 -0
- package/skills/web-capture/scripts/_media_screenshot/js/scroll_through.js +18 -0
- package/skills/web-capture/scripts/_media_screenshot/js/sequence.js +76 -0
- package/skills/web-capture/scripts/_media_screenshot/js/settled_check.js +15 -0
- package/skills/web-capture/scripts/_media_screenshot/js/title_card.js +71 -0
- package/skills/web-capture/scripts/_media_screenshot/js_loader.py +14 -0
- package/skills/web-capture/scripts/_media_screenshot/overlay.py +17 -0
- package/skills/web-capture/scripts/_media_screenshot/recording.py +100 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/__init__.py +39 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/arrow.py +50 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/caption.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/click.py +35 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/code_lines.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/focus.py +24 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/highlight.py +54 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/hover.py +28 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/redact.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/scroll.py +32 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/sequence.py +43 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/title_card.py +20 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/type.py +42 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/wait.py +9 -0
- package/skills/web-capture/scripts/_media_screenshot/scenes/zoom.py +72 -0
- package/skills/web-capture/scripts/_media_screenshot/screenshot.py +166 -0
- package/skills/web-capture/scripts/_media_screenshot/storyboard.py +88 -0
- package/skills/web-capture/scripts/_media_screenshot/template.py +119 -0
- package/skills/web-capture/scripts/_media_screenshot/trim.py +82 -0
- package/skills/web-capture/scripts/record.py +186 -0
- package/skills/web-capture/scripts/screenshot.py +94 -0
- package/skills/web-capture/skill.json +15 -14
- package/skills/web-capture/templates/github-code-tour.json +59 -0
- package/skills/web-capture/templates/github-readme-tour.json +77 -0
- package/skills/web-capture/templates/github-repo-intro.json +74 -0
- package/skills/web-capture/version.json +2 -2
- package/dist/handlers/template-bind.d.ts +0 -15
- package/dist/handlers/template-bind.js +0 -149
- package/skills/gen-digital-human/scripts/gen_digital_human_video.py +0 -574
- package/skills/gen-video/scripts/gen_video.py +0 -591
- package/skills/template-bind/version.json +0 -7
- package/skills/web-capture/scripts/web_capture.py +0 -461
- /package/skills/{template-bind → template-registry}/scripts/check_contracts.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/render_job_client.py +0 -0
- /package/skills/{template-bind → template-registry}/scripts/sync_registry.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/dsl_validator.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/prompt_enhancer.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/template_binder.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/runtime/timeline_compiler.py +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/render-plan-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-binding-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/template-definition-v1alpha1.json +0 -0
- /package/skills/{template-bind → template-registry}/video_dsl/schema/video-dsl-v1alpha1.json +0 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Storyboard JSON template engine.
|
|
2
|
+
|
|
3
|
+
Templates live in ``skills/media-screenshot/templates/<name>.json``.
|
|
4
|
+
Each template is a regular storyboard JSON optionally prefixed with a ``_meta``
|
|
5
|
+
block describing the variables it accepts. ``{{var_name}}`` placeholders inside
|
|
6
|
+
string values are replaced; when a string consists of a single placeholder its
|
|
7
|
+
substituted value is auto-coerced to int / float / bool / null so e.g.
|
|
8
|
+
``"scale": "{{zoom_scale}}"`` becomes a number after rendering.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
_TEMPLATES_DIR = Path(__file__).resolve().parent.parent.parent / "templates"
|
|
19
|
+
_PLACEHOLDER_RE = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")
|
|
20
|
+
_SOLE_PLACEHOLDER_RE = re.compile(
|
|
21
|
+
r"^\s*\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}\s*$"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _coerce(s: str) -> Any:
|
|
26
|
+
low = s.strip().lower()
|
|
27
|
+
if low == "true":
|
|
28
|
+
return True
|
|
29
|
+
if low == "false":
|
|
30
|
+
return False
|
|
31
|
+
if low in ("null", "none"):
|
|
32
|
+
return None
|
|
33
|
+
try:
|
|
34
|
+
return int(s)
|
|
35
|
+
except ValueError:
|
|
36
|
+
pass
|
|
37
|
+
try:
|
|
38
|
+
return float(s)
|
|
39
|
+
except ValueError:
|
|
40
|
+
pass
|
|
41
|
+
return s
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _substitute(value: Any, params: dict[str, str]) -> Any:
|
|
45
|
+
if isinstance(value, str):
|
|
46
|
+
m = _SOLE_PLACEHOLDER_RE.match(value)
|
|
47
|
+
if m:
|
|
48
|
+
key = m.group(1)
|
|
49
|
+
if key not in params:
|
|
50
|
+
raise SystemExit(f"模板缺少参数:{key}")
|
|
51
|
+
return _coerce(str(params[key]))
|
|
52
|
+
|
|
53
|
+
def repl(m: re.Match) -> str:
|
|
54
|
+
key = m.group(1)
|
|
55
|
+
if key not in params:
|
|
56
|
+
raise SystemExit(f"模板缺少参数:{key}")
|
|
57
|
+
return str(params[key])
|
|
58
|
+
|
|
59
|
+
return _PLACEHOLDER_RE.sub(repl, value)
|
|
60
|
+
if isinstance(value, list):
|
|
61
|
+
return [_substitute(v, params) for v in value]
|
|
62
|
+
if isinstance(value, dict):
|
|
63
|
+
return {k: _substitute(v, params) for k, v in value.items()}
|
|
64
|
+
return value
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def list_templates() -> list[Path]:
|
|
68
|
+
if not _TEMPLATES_DIR.exists():
|
|
69
|
+
return []
|
|
70
|
+
return sorted(_TEMPLATES_DIR.glob("*.json"))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_template(name: str) -> dict:
|
|
74
|
+
"""Resolve a template name (or path) to its parsed JSON."""
|
|
75
|
+
p = Path(name)
|
|
76
|
+
if not p.exists():
|
|
77
|
+
p = _TEMPLATES_DIR / f"{name}.json"
|
|
78
|
+
if not p.exists():
|
|
79
|
+
avail = ", ".join(t.stem for t in list_templates()) or "(none)"
|
|
80
|
+
raise SystemExit(f"模板不存在:{name}\n可用模板:{avail}")
|
|
81
|
+
try:
|
|
82
|
+
return json.loads(p.read_text())
|
|
83
|
+
except json.JSONDecodeError as e:
|
|
84
|
+
raise SystemExit(f"模板 JSON 解析失败 {p}: {e}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def render_template(name: str, params: dict[str, str]) -> dict:
|
|
88
|
+
"""Load, validate against ``_meta.params``, render, return the storyboard."""
|
|
89
|
+
tpl = load_template(name)
|
|
90
|
+
meta = tpl.pop("_meta", {})
|
|
91
|
+
declared = meta.get("params", {}) or {}
|
|
92
|
+
|
|
93
|
+
final: dict[str, str] = {}
|
|
94
|
+
for key, spec in declared.items():
|
|
95
|
+
if isinstance(spec, dict) and "default" in spec:
|
|
96
|
+
final[key] = str(spec["default"])
|
|
97
|
+
for key, val in params.items():
|
|
98
|
+
if declared and key not in declared:
|
|
99
|
+
print(
|
|
100
|
+
f"[template] 警告:参数 {key} 未在 _meta.params 中声明",
|
|
101
|
+
file=sys.stderr,
|
|
102
|
+
)
|
|
103
|
+
final[key] = val
|
|
104
|
+
for key, spec in declared.items():
|
|
105
|
+
if isinstance(spec, dict) and spec.get("required") and key not in final:
|
|
106
|
+
raise SystemExit(
|
|
107
|
+
f"模板 {name} 要求参数 {key}({spec.get('description', '')})"
|
|
108
|
+
)
|
|
109
|
+
return _substitute(tpl, final)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_param_pairs(pairs: list[str] | None) -> dict[str, str]:
|
|
113
|
+
out: dict[str, str] = {}
|
|
114
|
+
for raw in pairs or []:
|
|
115
|
+
if "=" not in raw:
|
|
116
|
+
raise SystemExit(f'--param 需要 "key=value" 格式:{raw}')
|
|
117
|
+
k, v = raw.split("=", 1)
|
|
118
|
+
out[k.strip()] = v
|
|
119
|
+
return out
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""End-anchored ffmpeg trim for storyboard videos.
|
|
2
|
+
|
|
3
|
+
Playwright's recorded video timeline does NOT align 1:1 with Python wall-clock —
|
|
4
|
+
recording starts before the first navigation finishes and the file is typically
|
|
5
|
+
a few seconds longer than the wall-clock window we measured. The storyboard
|
|
6
|
+
ends right before ``context.close()``, which IS the video's tail. So we anchor
|
|
7
|
+
on the END.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _probe_duration(path: Path) -> float | None:
|
|
19
|
+
if not shutil.which("ffprobe"):
|
|
20
|
+
return None
|
|
21
|
+
r = subprocess.run(
|
|
22
|
+
[
|
|
23
|
+
"ffprobe", "-v", "error", "-show_entries", "format=duration",
|
|
24
|
+
"-of", "default=noprint_wrappers=1:nokey=1", str(path),
|
|
25
|
+
],
|
|
26
|
+
capture_output=True, text=True, check=False,
|
|
27
|
+
)
|
|
28
|
+
try:
|
|
29
|
+
return float(r.stdout.strip())
|
|
30
|
+
except ValueError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def trim_video(out: Path, start_ms: int, end_ms: int) -> None:
|
|
35
|
+
if not shutil.which("ffmpeg"):
|
|
36
|
+
print(
|
|
37
|
+
"[record] 安装 ffmpeg 可自动裁剪开头加载段。",
|
|
38
|
+
file=sys.stderr,
|
|
39
|
+
)
|
|
40
|
+
return
|
|
41
|
+
if end_ms <= start_ms:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
storyboard_ms = end_ms - start_ms
|
|
45
|
+
lead_ms = 80
|
|
46
|
+
tail_ms = 200
|
|
47
|
+
keep_ms = storyboard_ms + lead_ms + tail_ms
|
|
48
|
+
|
|
49
|
+
video_dur = _probe_duration(out)
|
|
50
|
+
if video_dur is None:
|
|
51
|
+
# Fallback: start-anchored (legacy, may be off if recording drifts).
|
|
52
|
+
start_s = max(0.0, (start_ms - lead_ms) / 1000.0)
|
|
53
|
+
duration_s = keep_ms / 1000.0
|
|
54
|
+
else:
|
|
55
|
+
# End-anchored: video tail == storyboard end == context.close.
|
|
56
|
+
start_s = max(0.0, video_dur - keep_ms / 1000.0)
|
|
57
|
+
duration_s = video_dur - start_s
|
|
58
|
+
|
|
59
|
+
tmp = out.with_suffix(out.suffix + ".trim.webm")
|
|
60
|
+
cmd = [
|
|
61
|
+
"ffmpeg", "-y", "-loglevel", "error",
|
|
62
|
+
"-ss", f"{start_s:.3f}", "-i", str(out),
|
|
63
|
+
"-t", f"{duration_s:.3f}",
|
|
64
|
+
"-c:v", "libvpx", "-b:v", "2M",
|
|
65
|
+
"-deadline", "realtime", "-cpu-used", "4",
|
|
66
|
+
"-an", str(tmp),
|
|
67
|
+
]
|
|
68
|
+
print(
|
|
69
|
+
f"[record] ffmpeg trim (re-encode, end-anchored): "
|
|
70
|
+
f"skip {start_s:.2f}s, keep {duration_s:.2f}s "
|
|
71
|
+
f"(video={video_dur and f'{video_dur:.2f}s'}, storyboard={storyboard_ms/1000:.2f}s)",
|
|
72
|
+
file=sys.stderr,
|
|
73
|
+
)
|
|
74
|
+
r = subprocess.run(cmd, check=False)
|
|
75
|
+
if r.returncode == 0 and tmp.exists() and tmp.stat().st_size > 0:
|
|
76
|
+
os.replace(tmp, out)
|
|
77
|
+
else:
|
|
78
|
+
try:
|
|
79
|
+
tmp.unlink(missing_ok=True)
|
|
80
|
+
except OSError:
|
|
81
|
+
pass
|
|
82
|
+
print("[record] ffmpeg 裁剪失败,保留原始视频。", file=sys.stderr)
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Playwright 录屏入口(薄壳,逻辑在 _media_screenshot 包)。
|
|
3
|
+
|
|
4
|
+
模式:
|
|
5
|
+
- 固定时长 / 条件停止(--duration / --stop-when-*)
|
|
6
|
+
- 自动滚动(--scroll-through)
|
|
7
|
+
- 自定义分镜(--storyboard <json>)
|
|
8
|
+
- 模板分镜(--template <name> --param k=v ...)
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
19
|
+
from _media_screenshot import ( # noqa: E402
|
|
20
|
+
cli_args,
|
|
21
|
+
do_record,
|
|
22
|
+
do_storyboard,
|
|
23
|
+
template,
|
|
24
|
+
trim,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main() -> None:
|
|
29
|
+
ap = argparse.ArgumentParser(
|
|
30
|
+
description="通过 Playwright Python API 对网页录屏,输出 .webm。"
|
|
31
|
+
)
|
|
32
|
+
ap.add_argument(
|
|
33
|
+
"-u", "--url",
|
|
34
|
+
help="待录制的页面 URL(如未指定,模板/storyboard 必须包含 url)",
|
|
35
|
+
)
|
|
36
|
+
ap.add_argument(
|
|
37
|
+
"-o", "--output", default="recording.webm",
|
|
38
|
+
help="本地输出 .webm 路径(默认 recording.webm)",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# plain recording stop conditions
|
|
42
|
+
ap.add_argument("--duration", type=int, help="固定录制时长(毫秒)")
|
|
43
|
+
ap.add_argument("--stop-when-selector", help="选择器出现就停")
|
|
44
|
+
ap.add_argument("--stop-when-hidden", help="选择器消失就停")
|
|
45
|
+
ap.add_argument(
|
|
46
|
+
"--max-duration", type=int, default=60000,
|
|
47
|
+
help="安全上限(ms),用 stop-when-* 时防挂死",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# scroll-through
|
|
51
|
+
ap.add_argument(
|
|
52
|
+
"--scroll-through", action="store_true",
|
|
53
|
+
help="录制时自动从顶部平滑滚到底部",
|
|
54
|
+
)
|
|
55
|
+
ap.add_argument("--scroll-step", type=int, default=60)
|
|
56
|
+
ap.add_argument("--scroll-interval", type=int, default=50)
|
|
57
|
+
ap.add_argument("--scroll-pause-top", type=int, default=800)
|
|
58
|
+
ap.add_argument("--scroll-pause-bottom", type=int, default=1200)
|
|
59
|
+
|
|
60
|
+
# storyboard / template
|
|
61
|
+
ap.add_argument("--storyboard", help="分镜 JSON 文件路径")
|
|
62
|
+
ap.add_argument(
|
|
63
|
+
"--template",
|
|
64
|
+
help="使用 templates/<name>.json 模板,配合 --param 传参",
|
|
65
|
+
)
|
|
66
|
+
ap.add_argument(
|
|
67
|
+
"--param", action="append", default=[], metavar="KEY=VALUE",
|
|
68
|
+
help="给模板传参,可重复",
|
|
69
|
+
)
|
|
70
|
+
ap.add_argument(
|
|
71
|
+
"--list-templates", action="store_true",
|
|
72
|
+
help="列出可用模板并退出",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
cli_args.add_common_args(ap)
|
|
76
|
+
|
|
77
|
+
args = ap.parse_args()
|
|
78
|
+
|
|
79
|
+
if args.list_templates:
|
|
80
|
+
for tpl in template.list_templates():
|
|
81
|
+
print(tpl.stem)
|
|
82
|
+
return
|
|
83
|
+
|
|
84
|
+
if args.template and args.storyboard:
|
|
85
|
+
print("--template 与 --storyboard 互斥,请二选一。", file=sys.stderr)
|
|
86
|
+
sys.exit(2)
|
|
87
|
+
|
|
88
|
+
if not (
|
|
89
|
+
args.duration or args.stop_when_selector or args.stop_when_hidden
|
|
90
|
+
or args.scroll_through or args.storyboard or args.template
|
|
91
|
+
):
|
|
92
|
+
print(
|
|
93
|
+
"需至少提供 --duration / --stop-when-selector / --stop-when-hidden "
|
|
94
|
+
"/ --scroll-through / --storyboard / --template 之一。",
|
|
95
|
+
file=sys.stderr,
|
|
96
|
+
)
|
|
97
|
+
sys.exit(2)
|
|
98
|
+
|
|
99
|
+
if not args.output.lower().endswith(".webm"):
|
|
100
|
+
print(
|
|
101
|
+
"提示:Playwright 录屏输出为 webm,建议 --output 用 .webm 后缀。",
|
|
102
|
+
file=sys.stderr,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
storage_path, is_temp = cli_args.build_storage(args)
|
|
106
|
+
|
|
107
|
+
cfg: dict = {
|
|
108
|
+
"output": str(Path(args.output).resolve()),
|
|
109
|
+
"browser": args.browser or "chromium",
|
|
110
|
+
"maxDuration": args.max_duration,
|
|
111
|
+
}
|
|
112
|
+
if args.url:
|
|
113
|
+
cfg["url"] = args.url
|
|
114
|
+
cli_args.common_args_to_cfg(args, cfg)
|
|
115
|
+
if storage_path:
|
|
116
|
+
cfg["storageState"] = storage_path
|
|
117
|
+
|
|
118
|
+
story: dict | None = None
|
|
119
|
+
if args.template:
|
|
120
|
+
params = template.parse_param_pairs(args.param)
|
|
121
|
+
story = template.render_template(args.template, params)
|
|
122
|
+
elif args.storyboard:
|
|
123
|
+
try:
|
|
124
|
+
story = json.loads(Path(args.storyboard).read_text())
|
|
125
|
+
except (OSError, json.JSONDecodeError) as e:
|
|
126
|
+
print(f"--storyboard 读取/解析失败:{e}", file=sys.stderr)
|
|
127
|
+
sys.exit(2)
|
|
128
|
+
|
|
129
|
+
if story is not None:
|
|
130
|
+
for key in ("scenes", "transition"):
|
|
131
|
+
if key in story:
|
|
132
|
+
cfg[key] = story[key]
|
|
133
|
+
for key in (
|
|
134
|
+
"url", "viewport", "device", "colorScheme", "userAgent",
|
|
135
|
+
"ignoreHttpsErrors", "waitForSelector", "waitForTimeout",
|
|
136
|
+
"waitForReadySelectors", "settleMs",
|
|
137
|
+
):
|
|
138
|
+
if key in story:
|
|
139
|
+
cfg[key] = story[key]
|
|
140
|
+
|
|
141
|
+
if args.duration:
|
|
142
|
+
cfg["duration"] = args.duration
|
|
143
|
+
if args.stop_when_selector:
|
|
144
|
+
cfg["stopWhenSelector"] = args.stop_when_selector
|
|
145
|
+
if args.stop_when_hidden:
|
|
146
|
+
cfg["stopWhenHidden"] = args.stop_when_hidden
|
|
147
|
+
if args.scroll_through:
|
|
148
|
+
cfg["scrollThrough"] = True
|
|
149
|
+
cfg["scrollStep"] = args.scroll_step
|
|
150
|
+
cfg["scrollInterval"] = args.scroll_interval
|
|
151
|
+
cfg["scrollPauseTop"] = args.scroll_pause_top
|
|
152
|
+
cfg["scrollPauseBottom"] = args.scroll_pause_bottom
|
|
153
|
+
|
|
154
|
+
if not cfg.get("url"):
|
|
155
|
+
print(
|
|
156
|
+
"必须提供 URL:通过 --url,或在模板 / storyboard JSON 里定义 url。",
|
|
157
|
+
file=sys.stderr,
|
|
158
|
+
)
|
|
159
|
+
sys.exit(2)
|
|
160
|
+
|
|
161
|
+
is_storyboard = story is not None
|
|
162
|
+
print(
|
|
163
|
+
f"[runner] mode={'storyboard' if is_storyboard else 'record'}",
|
|
164
|
+
file=sys.stderr,
|
|
165
|
+
)
|
|
166
|
+
try:
|
|
167
|
+
if is_storyboard:
|
|
168
|
+
out, start_ms, end_ms = do_storyboard(cfg)
|
|
169
|
+
trim.trim_video(out, start_ms, end_ms)
|
|
170
|
+
else:
|
|
171
|
+
out = do_record(cfg)
|
|
172
|
+
finally:
|
|
173
|
+
if is_temp and storage_path:
|
|
174
|
+
try:
|
|
175
|
+
os.unlink(storage_path)
|
|
176
|
+
except OSError:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
if not out.exists():
|
|
180
|
+
print(f"录屏未生成:{out}", file=sys.stderr)
|
|
181
|
+
sys.exit(1)
|
|
182
|
+
print(str(out))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
if __name__ == "__main__":
|
|
186
|
+
main()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Playwright 截屏入口(薄壳,逻辑在 _media_screenshot 包)。"""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
11
|
+
from _media_screenshot import cli_args, do_screenshot # noqa: E402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
ap = argparse.ArgumentParser(
|
|
16
|
+
description="通过 Playwright 对网页进行截屏,仅落盘到本地路径。"
|
|
17
|
+
)
|
|
18
|
+
ap.add_argument("-u", "--url", required=True, help="待截屏的页面 URL")
|
|
19
|
+
ap.add_argument(
|
|
20
|
+
"-o", "--output", default="screenshot.png",
|
|
21
|
+
help="本地输出路径(默认 screenshot.png)",
|
|
22
|
+
)
|
|
23
|
+
ap.add_argument("--full-page", action="store_true", help="整页截屏")
|
|
24
|
+
ap.add_argument("--selector", help='截某个 CSS 元素本身(如 ".card")')
|
|
25
|
+
ap.add_argument(
|
|
26
|
+
"--clip",
|
|
27
|
+
help='区域截屏 "x,y,w,h"(像素)。与 --selector 同设时以元素 boundingBox 为基点偏移。',
|
|
28
|
+
)
|
|
29
|
+
ap.add_argument(
|
|
30
|
+
"--no-scroll-into-view", action="store_true",
|
|
31
|
+
help="--selector 模式下关闭自动滚动",
|
|
32
|
+
)
|
|
33
|
+
ap.add_argument(
|
|
34
|
+
"--hide-selector", action="append", default=[],
|
|
35
|
+
help='截屏前 display:none 该 CSS 选择器(隐藏 cookie banner、登录浮层等)。可重复。',
|
|
36
|
+
)
|
|
37
|
+
ap.add_argument(
|
|
38
|
+
"--mask-selector", action="append", default=[],
|
|
39
|
+
help='截屏时用 Playwright 原生 mask 涂盖该 CSS 选择器(默认粉色块)。可重复。',
|
|
40
|
+
)
|
|
41
|
+
ap.add_argument(
|
|
42
|
+
"--mask-color",
|
|
43
|
+
help='--mask-selector 的填充色(如 "#000" / "rgba(0,0,0,0.9)"),默认 Playwright 粉色。',
|
|
44
|
+
)
|
|
45
|
+
ap.add_argument(
|
|
46
|
+
"--annotate",
|
|
47
|
+
help='注释 JSON 文件或字符串:在截屏前用 storyboard 场景(highlight/arrow/caption/title-card/sequence/redact 等)画注解。'
|
|
48
|
+
' 顶层可为数组或 {"annotations": [...], "settleMs": 900}。',
|
|
49
|
+
)
|
|
50
|
+
cli_args.add_common_args(ap)
|
|
51
|
+
|
|
52
|
+
args = ap.parse_args()
|
|
53
|
+
storage_path, is_temp = cli_args.build_storage(args)
|
|
54
|
+
|
|
55
|
+
cfg: dict = {
|
|
56
|
+
"url": args.url,
|
|
57
|
+
"output": str(Path(args.output).resolve()),
|
|
58
|
+
"browser": args.browser or "chromium",
|
|
59
|
+
"fullPage": bool(args.full_page),
|
|
60
|
+
"scrollIntoView": not args.no_scroll_into_view,
|
|
61
|
+
}
|
|
62
|
+
cli_args.common_args_to_cfg(args, cfg)
|
|
63
|
+
if storage_path:
|
|
64
|
+
cfg["storageState"] = storage_path
|
|
65
|
+
if args.selector:
|
|
66
|
+
cfg["selector"] = args.selector
|
|
67
|
+
if args.clip:
|
|
68
|
+
cfg["clip"] = cli_args.parse_clip(args.clip)
|
|
69
|
+
if args.hide_selector:
|
|
70
|
+
cfg["hideSelectors"] = list(args.hide_selector)
|
|
71
|
+
if args.mask_selector:
|
|
72
|
+
cfg["maskSelectors"] = list(args.mask_selector)
|
|
73
|
+
if args.mask_color:
|
|
74
|
+
cfg["maskColor"] = args.mask_color
|
|
75
|
+
if args.annotate:
|
|
76
|
+
cfg["annotate"] = args.annotate
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
out = do_screenshot(cfg)
|
|
80
|
+
finally:
|
|
81
|
+
if is_temp and storage_path:
|
|
82
|
+
try:
|
|
83
|
+
os.unlink(storage_path)
|
|
84
|
+
except OSError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
if not out.exists():
|
|
88
|
+
print(f"截屏未生成:{out}", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
print(str(out))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
if __name__ == "__main__":
|
|
94
|
+
main()
|
|
@@ -2,27 +2,28 @@
|
|
|
2
2
|
"name": "web-capture",
|
|
3
3
|
"toolName": "web_capture",
|
|
4
4
|
"tier": "tool",
|
|
5
|
+
"category": "consuming",
|
|
5
6
|
"title": "Web Page Capture",
|
|
6
|
-
"description": "Drive a headless browser (Playwright) to capture
|
|
7
|
+
"description": "Drive a headless browser (Playwright Python) to capture any URL to local files: full-page / element / region screenshots, fixed-duration / condition-triggered / auto-scroll recordings, and storyboard videos (highlight / focus / zoom / scroll / virtual-cursor click / caption / title-card / arrow / numbered sequence / redact / code-line highlight). Two entry scripts: scripts/screenshot.py (stills) and scripts/record.py (webm video, storyboards, templates).",
|
|
7
8
|
"envVars": ["WEB_CAPTURE_BROWSER", "WEB_CAPTURE_OUTPUT_BASE", "WEB_CAPTURE_NO_SANDBOX", "WEB_CAPTURE_ALLOW_PRIVATE_HOSTS", "PLAYWRIGHT_BROWSERS_PATH"],
|
|
8
|
-
"scriptPath": "scripts/
|
|
9
|
+
"entry": { "type": "python", "scriptPath": "scripts/screenshot.py" },
|
|
10
|
+
"scriptPath": "scripts/screenshot.py",
|
|
9
11
|
"parameters": {
|
|
10
12
|
"type": "object",
|
|
11
13
|
"properties": {
|
|
12
14
|
"url": { "type": "string", "description": "Target page URL (http/https)" },
|
|
13
|
-
"
|
|
14
|
-
"output_dir": { "type": "string", "description": "Output directory (default ./web_capture_<timestamp>)" },
|
|
15
|
-
"width": { "type": "number", "description": "Viewport width in px (default 1280)" },
|
|
16
|
-
"height": { "type": "number", "description": "Viewport height in px (default 720)" },
|
|
17
|
-
"device_scale_factor": { "type": "number", "description": "Device pixel ratio for crisp/retina output (default 2)" },
|
|
18
|
-
"full_page": { "type": "boolean", "description": "screenshot mode: capture the whole scrollable page" },
|
|
19
|
-
"selector": { "type": "string", "description": "screenshot mode: capture only the element matching this CSS selector" },
|
|
20
|
-
"fps": { "type": "number", "description": "scroll/record video frames per second (default 30)" },
|
|
21
|
-
"scroll_duration": { "type": "number", "description": "Seconds for the full top-to-bottom scroll (default 8)" },
|
|
15
|
+
"output": { "type": "string", "description": "Local output path (screenshot.py: *.png, default screenshot.png)" },
|
|
22
16
|
"browser": { "type": "string", "description": "chromium | firefox | webkit (default chromium)" },
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
17
|
+
"device": { "type": "string", "description": "Device emulation name, e.g. 'iPhone 15 Pro'" },
|
|
18
|
+
"viewport": { "type": "string", "description": "Viewport as 'width,height', e.g. '1280,800'" },
|
|
19
|
+
"full_page": { "type": "boolean", "description": "screenshot.py: capture the whole scrollable page" },
|
|
20
|
+
"selector": { "type": "string", "description": "screenshot.py: capture only the element matching this CSS selector" },
|
|
21
|
+
"clip": { "type": "string", "description": "screenshot.py: region clip 'x,y,w,h' (combined with selector = offset from element box)" },
|
|
22
|
+
"wait_for_selector": { "type": "string", "description": "Wait for this CSS selector before acting" },
|
|
23
|
+
"wait_for_timeout": { "type": "number", "description": "Fixed wait (ms) before acting" },
|
|
24
|
+
"annotate": { "type": "string", "description": "screenshot.py: annotation JSON file or string (array or {annotations, settleMs})" },
|
|
25
|
+
"storage_state": { "type": "string", "description": "storageState JSON file path (logged-in capture)" },
|
|
26
|
+
"cookies": { "type": "string", "description": "Playwright cookies JSON string or file (top-level array)" }
|
|
26
27
|
},
|
|
27
28
|
"required": ["url"]
|
|
28
29
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"description": "GitHub 代码导览:跳转到指定文件 → 高亮行范围 → 字幕解说。适合讲解一段关键代码。",
|
|
4
|
+
"params": {
|
|
5
|
+
"file_url": {
|
|
6
|
+
"required": true,
|
|
7
|
+
"description": "GitHub blob 文件 URL,如 https://github.com/user/repo/blob/main/src/x.py"
|
|
8
|
+
},
|
|
9
|
+
"start_line": {
|
|
10
|
+
"required": true,
|
|
11
|
+
"description": "起始行号(含)"
|
|
12
|
+
},
|
|
13
|
+
"end_line": {
|
|
14
|
+
"required": true,
|
|
15
|
+
"description": "结束行号(含)"
|
|
16
|
+
},
|
|
17
|
+
"label": {
|
|
18
|
+
"default": "关键代码",
|
|
19
|
+
"description": "行高亮旁的标签"
|
|
20
|
+
},
|
|
21
|
+
"caption_text": {
|
|
22
|
+
"default": "重点看这里",
|
|
23
|
+
"description": "下方字幕条文字"
|
|
24
|
+
},
|
|
25
|
+
"highlight_color": {
|
|
26
|
+
"default": "#ff9500",
|
|
27
|
+
"description": "行高亮颜色"
|
|
28
|
+
},
|
|
29
|
+
"line_pattern": {
|
|
30
|
+
"default": "[data-line-number=\"{n}\"]",
|
|
31
|
+
"description": "行选择器模板({n} 替换为行号)。新版 React Blob 默认值即可;旧版 blob 表格用 \"#LC{n}\""
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"url": "{{file_url}}",
|
|
36
|
+
"viewport": [1440, 900],
|
|
37
|
+
"waitForReadySelectors": [
|
|
38
|
+
"[data-line-number=\"1\"], #LC1"
|
|
39
|
+
],
|
|
40
|
+
"settleMs": 1500,
|
|
41
|
+
"transition": { "fadeOutMs": 280, "gapMs": 150 },
|
|
42
|
+
"scenes": [
|
|
43
|
+
{
|
|
44
|
+
"type": "code-lines",
|
|
45
|
+
"startLine": "{{start_line}}",
|
|
46
|
+
"endLine": "{{end_line}}",
|
|
47
|
+
"linePattern": "{{line_pattern}}",
|
|
48
|
+
"color": "{{highlight_color}}",
|
|
49
|
+
"label": "{{label}}",
|
|
50
|
+
"duration": 4000
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"type": "caption",
|
|
54
|
+
"text": "{{caption_text}}",
|
|
55
|
+
"position": "bottom",
|
|
56
|
+
"duration": 3500
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_meta": {
|
|
3
|
+
"description": "GitHub 仓库 README 巡览:开场标题卡 → 项目名高亮 → ⭐ Star 点击演示 → README 滚动 → 结尾呼吁。",
|
|
4
|
+
"params": {
|
|
5
|
+
"repo_url": {
|
|
6
|
+
"required": true,
|
|
7
|
+
"description": "GitHub 仓库完整 URL"
|
|
8
|
+
},
|
|
9
|
+
"title": {
|
|
10
|
+
"default": "项目介绍",
|
|
11
|
+
"description": "开场大标题"
|
|
12
|
+
},
|
|
13
|
+
"subtitle": {
|
|
14
|
+
"default": "60s 速览",
|
|
15
|
+
"description": "开场副标题"
|
|
16
|
+
},
|
|
17
|
+
"outro_text": {
|
|
18
|
+
"default": "感兴趣?点 ⭐ 支持一下!",
|
|
19
|
+
"description": "结尾大字"
|
|
20
|
+
},
|
|
21
|
+
"scroll_duration": {
|
|
22
|
+
"default": "12000",
|
|
23
|
+
"description": "README 滚动时长(ms)"
|
|
24
|
+
},
|
|
25
|
+
"accent_color": {
|
|
26
|
+
"default": "#ff3b30",
|
|
27
|
+
"description": "标题卡 accent / 高亮主色"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"url": "{{repo_url}}",
|
|
32
|
+
"viewport": [1440, 900],
|
|
33
|
+
"waitForReadySelectors": [
|
|
34
|
+
"article.markdown-body",
|
|
35
|
+
"#repo-stars-counter-star"
|
|
36
|
+
],
|
|
37
|
+
"settleMs": 1200,
|
|
38
|
+
"transition": { "fadeOutMs": 320, "gapMs": 180 },
|
|
39
|
+
"scenes": [
|
|
40
|
+
{
|
|
41
|
+
"type": "title-card",
|
|
42
|
+
"title": "{{title}}",
|
|
43
|
+
"subtitle": "{{subtitle}}",
|
|
44
|
+
"accent": "{{accent_color}}",
|
|
45
|
+
"duration": 2200
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "highlight",
|
|
49
|
+
"selector": "strong[itemprop=\"name\"] a",
|
|
50
|
+
"color": "{{accent_color}}",
|
|
51
|
+
"lineWidth": 5,
|
|
52
|
+
"padding": 10,
|
|
53
|
+
"label": "项目名称",
|
|
54
|
+
"duration": 2500
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "hover",
|
|
58
|
+
"selector": "#repo-stars-counter-star",
|
|
59
|
+
"moveMs": 700,
|
|
60
|
+
"holdMs": 1800
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"type": "scroll",
|
|
64
|
+
"fromSelector": "article.markdown-body",
|
|
65
|
+
"step": 50,
|
|
66
|
+
"interval": 70,
|
|
67
|
+
"duration": "{{scroll_duration}}",
|
|
68
|
+
"holdAtEnd": 800
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"type": "title-card",
|
|
72
|
+
"title": "{{outro_text}}",
|
|
73
|
+
"accent": "{{accent_color}}",
|
|
74
|
+
"duration": 2500
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
}
|