@remixmate/cli 0.1.0 → 0.1.1
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 +27 -24
- package/dist/registry.d.ts +5 -16
- package/dist/registry.js +11 -17
- package/dist/skill-schema.d.ts +47 -0
- package/dist/skill-schema.js +49 -0
- package/package.json +5 -4
- package/skills/export-jianying/scripts/gen_jianying_draft.py +4 -4
- package/skills/gen-digital-human/SKILL.md +23 -46
- package/skills/gen-digital-human/skill.json +3 -2
- package/skills/gen-digital-human/version.json +1 -1
- package/skills/gen-image/SKILL.md +2 -2
- package/skills/gen-image/skill.json +2 -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 +11 -12
- package/skills/gen-video/SKILL.md +22 -33
- package/skills/gen-video/skill.json +3 -2
- package/skills/gen-video/version.json +1 -1
- package/skills/gen-voice/SKILL.md +1 -1
- package/skills/gen-voice/version.json +1 -1
- package/skills/prepare-video-assets/SKILL.md +2 -2
- package/skills/prepare-video-assets/skill.json +1 -1
- package/skills/render-video/scripts/render_video.py +8 -8
- 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 +129 -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 +6 -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 +3 -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 +14 -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,24 @@
|
|
|
1
|
+
"""Focus scene: dim the page with a spotlight cut-out around the target."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("focus")
|
|
10
|
+
def scene_focus(page, scene: dict) -> None:
|
|
11
|
+
locator = resolve_locator(page, scene["selector"])
|
|
12
|
+
box = locator.bounding_box()
|
|
13
|
+
if not box:
|
|
14
|
+
raise RuntimeError(f"focus: no bounding box for {scene['selector']}")
|
|
15
|
+
page.evaluate(
|
|
16
|
+
js_loader.load("focus"),
|
|
17
|
+
{
|
|
18
|
+
"box": box,
|
|
19
|
+
"pad": scene.get("padding", 12),
|
|
20
|
+
"label": scene.get("label", ""),
|
|
21
|
+
"color": scene.get("color", "#ff3b30"),
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
page.wait_for_timeout(scene.get("duration", 3000))
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Highlight scene: rectangle around a target element (or union of several)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _union_box(page, selectors: list[str]) -> dict | None:
|
|
10
|
+
"""Compute the union bounding box of every matching selector. Skips misses."""
|
|
11
|
+
# Make sure the first one is visible / on-screen before measuring.
|
|
12
|
+
resolve_locator(page, selectors[0])
|
|
13
|
+
boxes: list[dict] = []
|
|
14
|
+
for sel in selectors:
|
|
15
|
+
try:
|
|
16
|
+
loc = page.locator(sel).first
|
|
17
|
+
b = loc.bounding_box()
|
|
18
|
+
except Exception:
|
|
19
|
+
continue
|
|
20
|
+
if b:
|
|
21
|
+
boxes.append(b)
|
|
22
|
+
if not boxes:
|
|
23
|
+
return None
|
|
24
|
+
x = min(b["x"] for b in boxes)
|
|
25
|
+
y = min(b["y"] for b in boxes)
|
|
26
|
+
right = max(b["x"] + b["width"] for b in boxes)
|
|
27
|
+
bottom = max(b["y"] + b["height"] for b in boxes)
|
|
28
|
+
return {"x": x, "y": y, "width": right - x, "height": bottom - y}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register("highlight")
|
|
32
|
+
def scene_highlight(page, scene: dict) -> None:
|
|
33
|
+
selectors = scene.get("selectors")
|
|
34
|
+
if selectors:
|
|
35
|
+
box = _union_box(page, list(selectors))
|
|
36
|
+
if not box:
|
|
37
|
+
raise RuntimeError(f"highlight: no bounding boxes for {selectors}")
|
|
38
|
+
else:
|
|
39
|
+
locator = resolve_locator(page, scene["selector"])
|
|
40
|
+
box = locator.bounding_box()
|
|
41
|
+
if not box:
|
|
42
|
+
raise RuntimeError(f"highlight: no bounding box for {scene['selector']}")
|
|
43
|
+
page.evaluate(
|
|
44
|
+
js_loader.load("highlight"),
|
|
45
|
+
{
|
|
46
|
+
"box": box,
|
|
47
|
+
"pad": scene.get("padding", 8),
|
|
48
|
+
"color": scene.get("color", "#ff3b30"),
|
|
49
|
+
"lw": scene.get("lineWidth", 4),
|
|
50
|
+
"label": scene.get("label", ""),
|
|
51
|
+
"drawOnMs": int(scene.get("drawOnMs", 0)),
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
page.wait_for_timeout(scene.get("duration", 3000))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Hover scene: move cursor onto target and hold so tooltips/cards reveal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("hover")
|
|
10
|
+
def scene_hover(page, scene: dict) -> None:
|
|
11
|
+
locator = resolve_locator(page, scene["selector"])
|
|
12
|
+
box = locator.bounding_box()
|
|
13
|
+
if not box:
|
|
14
|
+
raise RuntimeError(f"hover: no bounding box for {scene['selector']}")
|
|
15
|
+
cx = box["x"] + box["width"] / 2
|
|
16
|
+
cy = box["y"] + box["height"] / 2
|
|
17
|
+
|
|
18
|
+
move_ms = int(scene.get("moveMs", 700))
|
|
19
|
+
hold_ms = int(scene.get("holdMs", 2500))
|
|
20
|
+
|
|
21
|
+
page.evaluate(js_loader.load("cursor_bootstrap"))
|
|
22
|
+
page.evaluate(js_loader.load("cursor_move"), {"x": cx, "y": cy, "ms": move_ms})
|
|
23
|
+
page.wait_for_timeout(move_ms + 80)
|
|
24
|
+
try:
|
|
25
|
+
locator.hover()
|
|
26
|
+
except Exception:
|
|
27
|
+
pass
|
|
28
|
+
page.wait_for_timeout(hold_ms)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Redact scene: blur / pixelate / solid-cover sensitive regions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("redact")
|
|
10
|
+
def scene_redact(page, scene: dict) -> None:
|
|
11
|
+
selectors: list[str] = list(scene.get("selectors") or [])
|
|
12
|
+
if not selectors and scene.get("selector"):
|
|
13
|
+
selectors.append(scene["selector"])
|
|
14
|
+
items: list[dict] = []
|
|
15
|
+
for sel in selectors:
|
|
16
|
+
try:
|
|
17
|
+
loc = resolve_locator(page, sel)
|
|
18
|
+
box = loc.bounding_box()
|
|
19
|
+
except Exception:
|
|
20
|
+
continue
|
|
21
|
+
if box:
|
|
22
|
+
items.append({"box": box, "color": scene.get("color", "#222")})
|
|
23
|
+
if not items:
|
|
24
|
+
raise RuntimeError("redact: no resolvable targets")
|
|
25
|
+
page.evaluate(
|
|
26
|
+
js_loader.load("redact"),
|
|
27
|
+
{
|
|
28
|
+
"items": items,
|
|
29
|
+
"mode": scene.get("mode", "blur"),
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
page.wait_for_timeout(scene.get("duration", 2500))
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Scroll scene: step-scroll for ``duration`` or until ``toSelector`` is hit."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from .. import js_loader
|
|
7
|
+
from ..browser import resolve_locator
|
|
8
|
+
from . import register
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register("scroll")
|
|
12
|
+
def scene_scroll(page, scene: dict) -> None:
|
|
13
|
+
step = scene.get("step", 50)
|
|
14
|
+
interval = scene.get("interval", 60)
|
|
15
|
+
if scene.get("fromSelector"):
|
|
16
|
+
from_loc = resolve_locator(page, scene["fromSelector"])
|
|
17
|
+
from_loc.scroll_into_view_if_needed()
|
|
18
|
+
page.wait_for_timeout(300)
|
|
19
|
+
elif scene.get("startTop"):
|
|
20
|
+
page.evaluate("() => window.scrollTo(0, 0)")
|
|
21
|
+
page.wait_for_timeout(200)
|
|
22
|
+
duration = scene.get("duration", 8000)
|
|
23
|
+
page.evaluate(
|
|
24
|
+
js_loader.load("scroll"),
|
|
25
|
+
{
|
|
26
|
+
"step": step,
|
|
27
|
+
"interval": interval,
|
|
28
|
+
"endTime": int(time.time() * 1000) + duration,
|
|
29
|
+
"toSelector": scene.get("toSelector"),
|
|
30
|
+
},
|
|
31
|
+
)
|
|
32
|
+
page.wait_for_timeout(scene.get("holdAtEnd", 600))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Sequence scene: multiple targets get numbered outlines, revealed in order."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("sequence")
|
|
10
|
+
def scene_sequence(page, scene: dict) -> None:
|
|
11
|
+
raw = scene.get("items") or []
|
|
12
|
+
items: list[dict] = []
|
|
13
|
+
for it in raw:
|
|
14
|
+
if isinstance(it, str):
|
|
15
|
+
sel, label = it, ""
|
|
16
|
+
else:
|
|
17
|
+
sel = it.get("selector")
|
|
18
|
+
label = it.get("label", "")
|
|
19
|
+
if not sel:
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
loc = resolve_locator(page, sel)
|
|
23
|
+
box = loc.bounding_box()
|
|
24
|
+
except Exception:
|
|
25
|
+
continue
|
|
26
|
+
if not box:
|
|
27
|
+
continue
|
|
28
|
+
items.append({"box": box, "label": label})
|
|
29
|
+
if not items:
|
|
30
|
+
raise RuntimeError("sequence: no resolvable items")
|
|
31
|
+
step_ms = int(scene.get("stepMs", 500))
|
|
32
|
+
page.evaluate(
|
|
33
|
+
js_loader.load("sequence"),
|
|
34
|
+
{
|
|
35
|
+
"items": items,
|
|
36
|
+
"color": scene.get("color", "#ff3b30"),
|
|
37
|
+
"lineWidth": scene.get("lineWidth", 3),
|
|
38
|
+
"padding": scene.get("padding", 6),
|
|
39
|
+
"stepMs": step_ms,
|
|
40
|
+
},
|
|
41
|
+
)
|
|
42
|
+
total = step_ms * len(items) + int(scene.get("holdMs", 1500))
|
|
43
|
+
page.wait_for_timeout(total)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Title card scene: full-screen opener/outro with title + subtitle + accent."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from . import register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register("title-card")
|
|
9
|
+
def scene_title_card(page, scene: dict) -> None:
|
|
10
|
+
page.evaluate(
|
|
11
|
+
js_loader.load("title_card"),
|
|
12
|
+
{
|
|
13
|
+
"title": scene.get("title", ""),
|
|
14
|
+
"subtitle": scene.get("subtitle", ""),
|
|
15
|
+
"bg": scene.get("background", "rgba(15,17,21,0.96)"),
|
|
16
|
+
"fg": scene.get("color", "#fff"),
|
|
17
|
+
"accent": scene.get("accent", "#ff3b30"),
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
page.wait_for_timeout(scene.get("duration", 2500))
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Type scene: move cursor to an input, focus, then typewriter the text."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register("type")
|
|
10
|
+
def scene_type(page, scene: dict) -> None:
|
|
11
|
+
locator = resolve_locator(page, scene["selector"])
|
|
12
|
+
text = scene.get("text", "")
|
|
13
|
+
delay = int(scene.get("delay", 80))
|
|
14
|
+
clear_first = bool(scene.get("clearFirst", True))
|
|
15
|
+
move_cursor = bool(scene.get("moveCursor", True))
|
|
16
|
+
move_ms = int(scene.get("moveMs", 600))
|
|
17
|
+
pre_hold = int(scene.get("preHoldMs", 200))
|
|
18
|
+
post_hold = int(scene.get("postHoldMs", 800))
|
|
19
|
+
|
|
20
|
+
if move_cursor:
|
|
21
|
+
box = locator.bounding_box()
|
|
22
|
+
if box:
|
|
23
|
+
cx = box["x"] + box["width"] / 2
|
|
24
|
+
cy = box["y"] + box["height"] / 2
|
|
25
|
+
page.evaluate(js_loader.load("cursor_bootstrap"))
|
|
26
|
+
page.evaluate(js_loader.load("cursor_move"), {"x": cx, "y": cy, "ms": move_ms})
|
|
27
|
+
page.wait_for_timeout(move_ms + pre_hold)
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
locator.click()
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
if clear_first:
|
|
34
|
+
try:
|
|
35
|
+
locator.fill("")
|
|
36
|
+
except Exception:
|
|
37
|
+
pass
|
|
38
|
+
try:
|
|
39
|
+
locator.type(text, delay=delay)
|
|
40
|
+
except Exception:
|
|
41
|
+
page.keyboard.type(text, delay=delay)
|
|
42
|
+
page.wait_for_timeout(post_hold)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Zoom scene: element-level OR page-level (browser-like) zoom animation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from ..browser import resolve_locator
|
|
6
|
+
from . import register
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _run_page_zoom(page, scene: dict) -> None:
|
|
10
|
+
target_scale = scene.get("scale", 2.5)
|
|
11
|
+
ramp_in_ms = scene.get("rampInMs", scene.get("rampMs", 700))
|
|
12
|
+
ramp_out_ms = scene.get("rampOutMs", 0)
|
|
13
|
+
hold_ms = scene.get("holdMs", 2500)
|
|
14
|
+
color = scene.get("color", "#ff9500")
|
|
15
|
+
label = scene.get("label", "")
|
|
16
|
+
page.evaluate(
|
|
17
|
+
js_loader.load("page_zoom_in"),
|
|
18
|
+
{
|
|
19
|
+
"selector": scene["selector"],
|
|
20
|
+
"targetScale": target_scale,
|
|
21
|
+
"rampInMs": ramp_in_ms,
|
|
22
|
+
"label": label,
|
|
23
|
+
"color": color,
|
|
24
|
+
},
|
|
25
|
+
)
|
|
26
|
+
page.wait_for_timeout(ramp_in_ms + hold_ms)
|
|
27
|
+
if ramp_out_ms > 0:
|
|
28
|
+
page.evaluate(js_loader.load("page_zoom_out"), {"rampOutMs": ramp_out_ms})
|
|
29
|
+
page.wait_for_timeout(ramp_out_ms + 100)
|
|
30
|
+
page.evaluate(js_loader.load("page_zoom_restore"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _run_element_zoom(page, scene: dict) -> None:
|
|
34
|
+
target_scale = scene.get("scale", 2.5)
|
|
35
|
+
ramp_ms = scene.get("rampMs", 700)
|
|
36
|
+
hold_ms = scene.get("holdMs", 2500)
|
|
37
|
+
color = scene.get("color", "#ff9500")
|
|
38
|
+
label = scene.get("label", "")
|
|
39
|
+
dim = scene.get("dim", True)
|
|
40
|
+
line_width = scene.get("lineWidth", 3)
|
|
41
|
+
|
|
42
|
+
page.evaluate(
|
|
43
|
+
js_loader.load("element_zoom"),
|
|
44
|
+
{
|
|
45
|
+
"selector": scene["selector"],
|
|
46
|
+
"targetScale": target_scale,
|
|
47
|
+
"rampMs": ramp_ms,
|
|
48
|
+
"color": color,
|
|
49
|
+
"label": label,
|
|
50
|
+
"dim": dim,
|
|
51
|
+
"lineWidth": line_width,
|
|
52
|
+
},
|
|
53
|
+
)
|
|
54
|
+
page.wait_for_timeout(ramp_ms + hold_ms)
|
|
55
|
+
page.evaluate(js_loader.load("element_zoom_restore"), scene["selector"])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register("zoom")
|
|
59
|
+
def scene_zoom(page, scene: dict) -> None:
|
|
60
|
+
target = scene.get("target", "element")
|
|
61
|
+
locator = resolve_locator(page, scene["selector"])
|
|
62
|
+
locator.scroll_into_view_if_needed()
|
|
63
|
+
# Center in viewport so the element has room to grow.
|
|
64
|
+
locator.evaluate(
|
|
65
|
+
"el => el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' })"
|
|
66
|
+
)
|
|
67
|
+
page.wait_for_timeout(250)
|
|
68
|
+
|
|
69
|
+
if target == "page":
|
|
70
|
+
_run_page_zoom(page, scene)
|
|
71
|
+
else:
|
|
72
|
+
_run_element_zoom(page, scene)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Screenshot operation: full-page / element / region, with optional
|
|
2
|
+
hide-before / native mask / drawn annotations.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import scenes
|
|
11
|
+
from .bootstrap import ensure_runtime
|
|
12
|
+
from .browser import (
|
|
13
|
+
apply_pre_action_waits,
|
|
14
|
+
build_context_options,
|
|
15
|
+
launch_with_browser_install,
|
|
16
|
+
)
|
|
17
|
+
from .overlay import ensure_overlay
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _apply_hide(page, selectors: list[str]) -> None:
|
|
21
|
+
"""display:none every match of every selector (idempotent, scoped to this page)."""
|
|
22
|
+
if not selectors:
|
|
23
|
+
return
|
|
24
|
+
page.evaluate(
|
|
25
|
+
"""(sels) => {
|
|
26
|
+
sels.forEach((s) => {
|
|
27
|
+
try {
|
|
28
|
+
document.querySelectorAll(s).forEach((el) => {
|
|
29
|
+
el.style.setProperty('display', 'none', 'important');
|
|
30
|
+
});
|
|
31
|
+
} catch (e) { /* invalid selector — skip */ }
|
|
32
|
+
});
|
|
33
|
+
}""",
|
|
34
|
+
selectors,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _apply_annotations(page, annotations: list[dict], settle_ms: int) -> None:
|
|
39
|
+
"""Reuse scene handlers to draw static overlays before snapshotting."""
|
|
40
|
+
if not annotations:
|
|
41
|
+
return
|
|
42
|
+
ensure_overlay(page)
|
|
43
|
+
for ann in annotations:
|
|
44
|
+
handler = scenes.get_handler(ann.get("type", ""))
|
|
45
|
+
if not handler:
|
|
46
|
+
print(
|
|
47
|
+
f"[annotate] skip unknown type: {ann.get('type')!r} "
|
|
48
|
+
f"(known: {', '.join(scenes.known_types())})",
|
|
49
|
+
file=sys.stderr,
|
|
50
|
+
)
|
|
51
|
+
continue
|
|
52
|
+
# Scene handlers wait on `duration`; for static annotations we want
|
|
53
|
+
# 0-wait so they all stack up before the snapshot fires.
|
|
54
|
+
ann_copy = dict(ann)
|
|
55
|
+
ann_copy.setdefault("duration", 0)
|
|
56
|
+
ann_copy.setdefault("holdMs", 0)
|
|
57
|
+
try:
|
|
58
|
+
handler(page, ann_copy)
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"[annotate] {ann.get('type')} failed: {e}", file=sys.stderr)
|
|
61
|
+
if settle_ms > 0:
|
|
62
|
+
page.wait_for_timeout(settle_ms)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_annotations(spec) -> tuple[list[dict], int]:
|
|
66
|
+
"""Accept either a list, a dict with 'annotations' key, or a path to such."""
|
|
67
|
+
if spec is None:
|
|
68
|
+
return [], 0
|
|
69
|
+
settle_ms = 900
|
|
70
|
+
if isinstance(spec, (str, Path)):
|
|
71
|
+
p = Path(spec)
|
|
72
|
+
if p.exists():
|
|
73
|
+
data = json.loads(p.read_text())
|
|
74
|
+
else:
|
|
75
|
+
data = json.loads(str(spec))
|
|
76
|
+
else:
|
|
77
|
+
data = spec
|
|
78
|
+
if isinstance(data, list):
|
|
79
|
+
return data, settle_ms
|
|
80
|
+
if isinstance(data, dict):
|
|
81
|
+
return list(data.get("annotations") or []), int(data.get("settleMs", settle_ms))
|
|
82
|
+
return [], settle_ms
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _build_mask_locators(page, selectors: list[str]):
|
|
86
|
+
out = []
|
|
87
|
+
for s in selectors or []:
|
|
88
|
+
try:
|
|
89
|
+
out.append(page.locator(s))
|
|
90
|
+
except Exception:
|
|
91
|
+
continue
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def do_screenshot(cfg: dict) -> Path:
|
|
96
|
+
"""Take a screenshot per cfg. Returns the absolute output path."""
|
|
97
|
+
ensure_runtime()
|
|
98
|
+
from playwright.sync_api import sync_playwright
|
|
99
|
+
|
|
100
|
+
output = Path(cfg["output"]).resolve()
|
|
101
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
|
|
103
|
+
annotations, settle_ms = _load_annotations(cfg.get("annotate"))
|
|
104
|
+
|
|
105
|
+
with sync_playwright() as p:
|
|
106
|
+
browser = launch_with_browser_install(p, cfg.get("browser"))
|
|
107
|
+
try:
|
|
108
|
+
context = browser.new_context(**build_context_options(p, cfg, None))
|
|
109
|
+
if cfg.get("timeout"):
|
|
110
|
+
context.set_default_timeout(cfg["timeout"])
|
|
111
|
+
page = context.new_page()
|
|
112
|
+
try:
|
|
113
|
+
page.goto(cfg["url"], wait_until="domcontentloaded")
|
|
114
|
+
apply_pre_action_waits(page, cfg)
|
|
115
|
+
|
|
116
|
+
_apply_hide(page, cfg.get("hideSelectors") or [])
|
|
117
|
+
_apply_annotations(page, annotations, settle_ms)
|
|
118
|
+
|
|
119
|
+
mask_locators = _build_mask_locators(page, cfg.get("maskSelectors") or [])
|
|
120
|
+
mask_kwargs: dict = {}
|
|
121
|
+
if mask_locators:
|
|
122
|
+
mask_kwargs["mask"] = mask_locators
|
|
123
|
+
if cfg.get("maskColor"):
|
|
124
|
+
mask_kwargs["mask_color"] = cfg["maskColor"]
|
|
125
|
+
|
|
126
|
+
if cfg.get("selector"):
|
|
127
|
+
locator = page.locator(cfg["selector"]).first
|
|
128
|
+
if cfg.get("scrollIntoView", True):
|
|
129
|
+
locator.scroll_into_view_if_needed()
|
|
130
|
+
if isinstance(cfg.get("clip"), (list, tuple)) and len(cfg["clip"]) == 4:
|
|
131
|
+
box = locator.bounding_box()
|
|
132
|
+
if not box:
|
|
133
|
+
raise SystemExit(
|
|
134
|
+
f'selector "{cfg["selector"]}" has no bounding box'
|
|
135
|
+
)
|
|
136
|
+
dx, dy, w, h = cfg["clip"]
|
|
137
|
+
page.screenshot(
|
|
138
|
+
path=str(output),
|
|
139
|
+
clip={
|
|
140
|
+
"x": box["x"] + dx,
|
|
141
|
+
"y": box["y"] + dy,
|
|
142
|
+
"width": w,
|
|
143
|
+
"height": h,
|
|
144
|
+
},
|
|
145
|
+
**mask_kwargs,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
locator.screenshot(path=str(output), **mask_kwargs)
|
|
149
|
+
elif isinstance(cfg.get("clip"), (list, tuple)) and len(cfg["clip"]) == 4:
|
|
150
|
+
x, y, w, h = cfg["clip"]
|
|
151
|
+
page.screenshot(
|
|
152
|
+
path=str(output),
|
|
153
|
+
clip={"x": x, "y": y, "width": w, "height": h},
|
|
154
|
+
**mask_kwargs,
|
|
155
|
+
)
|
|
156
|
+
else:
|
|
157
|
+
page.screenshot(
|
|
158
|
+
path=str(output),
|
|
159
|
+
full_page=bool(cfg.get("fullPage")),
|
|
160
|
+
**mask_kwargs,
|
|
161
|
+
)
|
|
162
|
+
finally:
|
|
163
|
+
context.close()
|
|
164
|
+
finally:
|
|
165
|
+
browser.close()
|
|
166
|
+
return output
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Storyboard runner: orchestrate a sequence of scenes with smooth transitions."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .bootstrap import ensure_runtime
|
|
9
|
+
from .browser import (
|
|
10
|
+
apply_pre_action_waits,
|
|
11
|
+
build_context_options,
|
|
12
|
+
launch_with_browser_install,
|
|
13
|
+
wait_until_settled,
|
|
14
|
+
)
|
|
15
|
+
from .overlay import clear_overlay, ensure_overlay, fade_out_overlays
|
|
16
|
+
from . import scenes
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _run_storyboard(page, cfg: dict) -> None:
|
|
20
|
+
ensure_overlay(page)
|
|
21
|
+
scene_list = cfg.get("scenes") or []
|
|
22
|
+
transition = cfg.get("transition") or {}
|
|
23
|
+
fade_out_ms = transition.get("fadeOutMs", transition.get("duration", 350))
|
|
24
|
+
gap_ms = transition.get("gapMs", 150)
|
|
25
|
+
for i, scene in enumerate(scene_list):
|
|
26
|
+
handler = scenes.get_handler(scene.get("type", ""))
|
|
27
|
+
if not handler:
|
|
28
|
+
print(
|
|
29
|
+
f"[runner] skip unknown scene type: {scene.get('type')!r} "
|
|
30
|
+
f"(known: {', '.join(scenes.known_types())})",
|
|
31
|
+
file=sys.stderr,
|
|
32
|
+
)
|
|
33
|
+
continue
|
|
34
|
+
print(
|
|
35
|
+
f"[runner] scene {i + 1}/{len(scene_list)}: {scene['type']}",
|
|
36
|
+
file=sys.stderr,
|
|
37
|
+
)
|
|
38
|
+
handler(page, scene)
|
|
39
|
+
if i < len(scene_list) - 1:
|
|
40
|
+
fade_out_overlays(page, fade_out_ms)
|
|
41
|
+
clear_overlay(page)
|
|
42
|
+
ensure_overlay(page)
|
|
43
|
+
if gap_ms > 0:
|
|
44
|
+
page.wait_for_timeout(gap_ms)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def do_storyboard(cfg: dict) -> tuple[Path, int, int]:
|
|
48
|
+
"""Run a storyboard recording. Returns (output_path, start_ms, end_ms)."""
|
|
49
|
+
ensure_runtime()
|
|
50
|
+
from playwright.sync_api import sync_playwright
|
|
51
|
+
|
|
52
|
+
output = Path(cfg["output"]).resolve()
|
|
53
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
out_dir = str(output.parent)
|
|
55
|
+
|
|
56
|
+
with sync_playwright() as p:
|
|
57
|
+
browser = launch_with_browser_install(p, cfg.get("browser"))
|
|
58
|
+
try:
|
|
59
|
+
t0 = time.time()
|
|
60
|
+
context = browser.new_context(**build_context_options(p, cfg, out_dir))
|
|
61
|
+
if cfg.get("timeout"):
|
|
62
|
+
context.set_default_timeout(cfg["timeout"])
|
|
63
|
+
page = context.new_page()
|
|
64
|
+
video_path: str | None = None
|
|
65
|
+
start_ms = 0
|
|
66
|
+
end_ms = 0
|
|
67
|
+
try:
|
|
68
|
+
page.goto(cfg["url"], wait_until="domcontentloaded")
|
|
69
|
+
apply_pre_action_waits(page, cfg)
|
|
70
|
+
wait_until_settled(page, cfg)
|
|
71
|
+
start_ms = int((time.time() - t0) * 1000)
|
|
72
|
+
_run_storyboard(page, cfg)
|
|
73
|
+
end_ms = int((time.time() - t0) * 1000)
|
|
74
|
+
video = page.video
|
|
75
|
+
context.close()
|
|
76
|
+
if video:
|
|
77
|
+
video_path = video.path()
|
|
78
|
+
finally:
|
|
79
|
+
pass
|
|
80
|
+
finally:
|
|
81
|
+
browser.close()
|
|
82
|
+
|
|
83
|
+
if not video_path or not Path(video_path).exists():
|
|
84
|
+
raise SystemExit("storyboard finished but no video file was produced")
|
|
85
|
+
src = Path(video_path)
|
|
86
|
+
if src.resolve() != output:
|
|
87
|
+
src.replace(output)
|
|
88
|
+
return output, start_ms, end_ms
|