@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,19 @@
|
|
|
1
|
+
// Inject the fixed overlay layers used by every storyboard scene.
|
|
2
|
+
// Idempotent: a no-op if already mounted (e.g. across multiple scenes).
|
|
3
|
+
(() => {
|
|
4
|
+
if (window.__pwOverlayReady) return;
|
|
5
|
+
const root = document.createElement('div');
|
|
6
|
+
root.id = '__pw_overlay_root';
|
|
7
|
+
Object.assign(root.style, {
|
|
8
|
+
position: 'fixed', inset: '0', pointerEvents: 'none', zIndex: '2147483646',
|
|
9
|
+
});
|
|
10
|
+
const fade = document.createElement('div');
|
|
11
|
+
fade.id = '__pw_overlay_fade';
|
|
12
|
+
Object.assign(fade.style, {
|
|
13
|
+
position: 'fixed', inset: '0', background: '#000', opacity: '0',
|
|
14
|
+
transition: 'opacity 400ms ease', pointerEvents: 'none', zIndex: '2147483647',
|
|
15
|
+
});
|
|
16
|
+
document.documentElement.appendChild(root);
|
|
17
|
+
document.documentElement.appendChild(fade);
|
|
18
|
+
window.__pwOverlayReady = true;
|
|
19
|
+
})();
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Browser-like zoom: scale <body> around the target element's document
|
|
2
|
+
// center. Overlays live on <html> (sibling of body) so they don't get scaled.
|
|
3
|
+
({ selector, targetScale, rampInMs, label, color }) => {
|
|
4
|
+
const el = document.querySelector(selector);
|
|
5
|
+
if (!el) return;
|
|
6
|
+
const rect = el.getBoundingClientRect();
|
|
7
|
+
const cx = rect.left + window.scrollX + rect.width / 2;
|
|
8
|
+
const cy = rect.top + window.scrollY + rect.height / 2;
|
|
9
|
+
const body = document.body;
|
|
10
|
+
body.dataset.__pwOrigTransform = body.style.transform || '';
|
|
11
|
+
body.dataset.__pwOrigTransition = body.style.transition || '';
|
|
12
|
+
body.dataset.__pwOrigOrigin = body.style.transformOrigin || '';
|
|
13
|
+
const html = document.documentElement;
|
|
14
|
+
html.dataset.__pwOrigOverflow = html.style.overflow || '';
|
|
15
|
+
html.style.overflow = 'hidden';
|
|
16
|
+
|
|
17
|
+
body.style.transformOrigin = `${cx}px ${cy}px`;
|
|
18
|
+
body.style.transition = `transform ${rampInMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
|
19
|
+
void body.offsetWidth;
|
|
20
|
+
requestAnimationFrame(() => {
|
|
21
|
+
body.style.transform = `scale(${targetScale})`;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (label) {
|
|
25
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
26
|
+
const lbl = document.createElement('div');
|
|
27
|
+
lbl.id = '__pw_zoom_label';
|
|
28
|
+
lbl.textContent = label;
|
|
29
|
+
Object.assign(lbl.style, {
|
|
30
|
+
position: 'fixed',
|
|
31
|
+
left: '50%',
|
|
32
|
+
bottom: '48px',
|
|
33
|
+
transform: 'translateX(-50%) translateY(8px)',
|
|
34
|
+
padding: '10px 18px',
|
|
35
|
+
background: color,
|
|
36
|
+
color: '#fff',
|
|
37
|
+
fontSize: '20px',
|
|
38
|
+
fontWeight: '700',
|
|
39
|
+
borderRadius: '8px',
|
|
40
|
+
fontFamily: 'system-ui, -apple-system, "Helvetica Neue", sans-serif',
|
|
41
|
+
boxShadow: '0 6px 18px rgba(0,0,0,0.4)',
|
|
42
|
+
whiteSpace: 'nowrap',
|
|
43
|
+
opacity: '0',
|
|
44
|
+
transition: 'opacity 280ms ease, transform 280ms ease',
|
|
45
|
+
zIndex: '2147483646',
|
|
46
|
+
pointerEvents: 'none',
|
|
47
|
+
});
|
|
48
|
+
root.appendChild(lbl);
|
|
49
|
+
setTimeout(() => {
|
|
50
|
+
lbl.style.opacity = '1';
|
|
51
|
+
lbl.style.transform = 'translateX(-50%) translateY(0)';
|
|
52
|
+
}, Math.max(0, rampInMs * 0.55));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Animate <body> back to scale(1) and fade out the floating label.
|
|
2
|
+
({ rampOutMs }) => {
|
|
3
|
+
const body = document.body;
|
|
4
|
+
body.style.transition = `transform ${rampOutMs}ms cubic-bezier(0.22, 1, 0.36, 1)`;
|
|
5
|
+
body.style.transform = 'scale(1)';
|
|
6
|
+
const lbl = document.getElementById('__pw_zoom_label');
|
|
7
|
+
if (lbl) {
|
|
8
|
+
lbl.style.transition = 'opacity 200ms ease';
|
|
9
|
+
lbl.style.opacity = '0';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Restore <body> and <html> inline styles stashed by page_zoom_in.js.
|
|
2
|
+
() => {
|
|
3
|
+
const body = document.body;
|
|
4
|
+
body.style.transform = body.dataset.__pwOrigTransform || '';
|
|
5
|
+
body.style.transition = body.dataset.__pwOrigTransition || '';
|
|
6
|
+
body.style.transformOrigin = body.dataset.__pwOrigOrigin || '';
|
|
7
|
+
delete body.dataset.__pwOrigTransform;
|
|
8
|
+
delete body.dataset.__pwOrigTransition;
|
|
9
|
+
delete body.dataset.__pwOrigOrigin;
|
|
10
|
+
const html = document.documentElement;
|
|
11
|
+
html.style.overflow = html.dataset.__pwOrigOverflow || '';
|
|
12
|
+
delete html.dataset.__pwOrigOverflow;
|
|
13
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Overlay opaque / blurred / pixelated rectangles to censor sensitive regions.
|
|
2
|
+
({ items, mode }) => {
|
|
3
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
4
|
+
if (!root) return;
|
|
5
|
+
items.forEach(({ box, color }) => {
|
|
6
|
+
const r = document.createElement('div');
|
|
7
|
+
Object.assign(r.style, {
|
|
8
|
+
position: 'fixed',
|
|
9
|
+
left: `${box.x}px`,
|
|
10
|
+
top: `${box.y}px`,
|
|
11
|
+
width: `${box.width}px`,
|
|
12
|
+
height: `${box.height}px`,
|
|
13
|
+
pointerEvents: 'none',
|
|
14
|
+
borderRadius: '3px',
|
|
15
|
+
});
|
|
16
|
+
if (mode === 'blur') {
|
|
17
|
+
Object.assign(r.style, {
|
|
18
|
+
backdropFilter: 'blur(14px) saturate(1.2)',
|
|
19
|
+
WebkitBackdropFilter: 'blur(14px) saturate(1.2)',
|
|
20
|
+
background: 'rgba(255,255,255,0.04)',
|
|
21
|
+
});
|
|
22
|
+
} else if (mode === 'pixelate') {
|
|
23
|
+
Object.assign(r.style, {
|
|
24
|
+
backdropFilter: 'contrast(0.55) blur(6px)',
|
|
25
|
+
WebkitBackdropFilter: 'contrast(0.55) blur(6px)',
|
|
26
|
+
background: 'rgba(0,0,0,0.04)',
|
|
27
|
+
});
|
|
28
|
+
} else {
|
|
29
|
+
r.style.background = color || '#222';
|
|
30
|
+
}
|
|
31
|
+
root.appendChild(r);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Step-scroll until either endTime or toSelector is reached. Driven by the
|
|
2
|
+
// Python side (endTime is `Date.now() + duration` precomputed in Python).
|
|
3
|
+
async ({ step, interval, endTime, toSelector }) => {
|
|
4
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
let stopY = Infinity;
|
|
6
|
+
if (toSelector) {
|
|
7
|
+
const el = document.querySelector(toSelector);
|
|
8
|
+
if (el) {
|
|
9
|
+
const rect = el.getBoundingClientRect();
|
|
10
|
+
stopY = window.scrollY + rect.bottom;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
let last = -1;
|
|
14
|
+
while (Date.now() < endTime) {
|
|
15
|
+
const y = window.scrollY;
|
|
16
|
+
const max =
|
|
17
|
+
Math.max(
|
|
18
|
+
document.body.scrollHeight,
|
|
19
|
+
document.documentElement.scrollHeight
|
|
20
|
+
) - window.innerHeight;
|
|
21
|
+
if (y >= max - 1 || y === last || y >= stopY) break;
|
|
22
|
+
last = y;
|
|
23
|
+
window.scrollBy({ top: step, left: 0, behavior: 'instant' });
|
|
24
|
+
await sleep(interval);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Scroll from current position to the bottom of the page in fixed steps.
|
|
2
|
+
// Used by record.py --scroll-through.
|
|
3
|
+
async ({ step, interval }) => {
|
|
4
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
let last = -1;
|
|
6
|
+
while (true) {
|
|
7
|
+
const y = window.scrollY;
|
|
8
|
+
const max =
|
|
9
|
+
Math.max(
|
|
10
|
+
document.body.scrollHeight,
|
|
11
|
+
document.documentElement.scrollHeight
|
|
12
|
+
) - window.innerHeight;
|
|
13
|
+
if (y >= max - 1 || y === last) break;
|
|
14
|
+
last = y;
|
|
15
|
+
window.scrollBy({ top: step, left: 0, behavior: 'instant' });
|
|
16
|
+
await sleep(interval);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Draw numbered (1, 2, 3...) outlines around multiple boxes, staggered in time.
|
|
2
|
+
({ items, color, lineWidth, padding, stepMs }) => {
|
|
3
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
4
|
+
if (!root) return;
|
|
5
|
+
|
|
6
|
+
const drawOne = (idx, box, label) => {
|
|
7
|
+
const rect = document.createElement('div');
|
|
8
|
+
Object.assign(rect.style, {
|
|
9
|
+
position: 'fixed',
|
|
10
|
+
left: `${box.x - padding}px`,
|
|
11
|
+
top: `${box.y - padding}px`,
|
|
12
|
+
width: `${box.width + padding * 2}px`,
|
|
13
|
+
height: `${box.height + padding * 2}px`,
|
|
14
|
+
border: `${lineWidth}px solid ${color}`,
|
|
15
|
+
borderRadius: '8px',
|
|
16
|
+
opacity: '0',
|
|
17
|
+
transition: 'opacity 250ms ease',
|
|
18
|
+
});
|
|
19
|
+
root.appendChild(rect);
|
|
20
|
+
|
|
21
|
+
const badge = document.createElement('div');
|
|
22
|
+
badge.textContent = String(idx + 1);
|
|
23
|
+
Object.assign(badge.style, {
|
|
24
|
+
position: 'fixed',
|
|
25
|
+
left: `${box.x - padding - 14}px`,
|
|
26
|
+
top: `${box.y - padding - 14}px`,
|
|
27
|
+
width: '32px',
|
|
28
|
+
height: '32px',
|
|
29
|
+
lineHeight: '32px',
|
|
30
|
+
textAlign: 'center',
|
|
31
|
+
borderRadius: '999px',
|
|
32
|
+
background: color,
|
|
33
|
+
color: '#fff',
|
|
34
|
+
fontSize: '16px',
|
|
35
|
+
fontWeight: '700',
|
|
36
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
37
|
+
boxShadow: '0 3px 10px rgba(0,0,0,0.3)',
|
|
38
|
+
opacity: '0',
|
|
39
|
+
transform: 'scale(0.5)',
|
|
40
|
+
transition:
|
|
41
|
+
'opacity 300ms ease, transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
|
42
|
+
});
|
|
43
|
+
root.appendChild(badge);
|
|
44
|
+
|
|
45
|
+
if (label) {
|
|
46
|
+
const lbl = document.createElement('div');
|
|
47
|
+
lbl.textContent = label;
|
|
48
|
+
Object.assign(lbl.style, {
|
|
49
|
+
position: 'fixed',
|
|
50
|
+
left: `${box.x + box.width / 2}px`,
|
|
51
|
+
top: `${box.y + box.height + padding + 8}px`,
|
|
52
|
+
transform: 'translateX(-50%)',
|
|
53
|
+
padding: '4px 10px',
|
|
54
|
+
background: color,
|
|
55
|
+
color: '#fff',
|
|
56
|
+
fontSize: '14px',
|
|
57
|
+
fontWeight: '600',
|
|
58
|
+
borderRadius: '5px',
|
|
59
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
60
|
+
whiteSpace: 'nowrap',
|
|
61
|
+
opacity: '0',
|
|
62
|
+
transition: 'opacity 300ms ease',
|
|
63
|
+
});
|
|
64
|
+
root.appendChild(lbl);
|
|
65
|
+
setTimeout(() => (lbl.style.opacity = '1'), 100);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
requestAnimationFrame(() => {
|
|
69
|
+
rect.style.opacity = '1';
|
|
70
|
+
badge.style.opacity = '1';
|
|
71
|
+
badge.style.transform = 'scale(1)';
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
items.forEach((it, i) => setTimeout(() => drawOne(i, it.box, it.label || ''), i * stepMs));
|
|
76
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Return true once common skeleton / loading placeholders have disappeared.
|
|
2
|
+
// Used inside page.wait_for_function with a timeout fallback.
|
|
3
|
+
() => {
|
|
4
|
+
const selectors = [
|
|
5
|
+
'.skeleton-box',
|
|
6
|
+
'.Skeleton-loader',
|
|
7
|
+
'[data-loading-state="loading"]',
|
|
8
|
+
'include-fragment[aria-busy="true"]',
|
|
9
|
+
'[aria-busy="true"]',
|
|
10
|
+
];
|
|
11
|
+
for (const sel of selectors) {
|
|
12
|
+
if (document.querySelector(sel)) return false;
|
|
13
|
+
}
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Full-screen title card: title + accent bar + subtitle, with staggered reveal.
|
|
2
|
+
({ title, subtitle, bg, fg, accent }) => {
|
|
3
|
+
const root = document.getElementById('__pw_overlay_root');
|
|
4
|
+
if (!root) return;
|
|
5
|
+
|
|
6
|
+
const card = document.createElement('div');
|
|
7
|
+
Object.assign(card.style, {
|
|
8
|
+
position: 'fixed',
|
|
9
|
+
inset: '0',
|
|
10
|
+
background: bg,
|
|
11
|
+
display: 'flex',
|
|
12
|
+
alignItems: 'center',
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
flexDirection: 'column',
|
|
15
|
+
opacity: '0',
|
|
16
|
+
transition: 'opacity 400ms ease',
|
|
17
|
+
fontFamily: 'system-ui, -apple-system, "PingFang SC", "Helvetica Neue", sans-serif',
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const t = document.createElement('div');
|
|
21
|
+
t.textContent = title || '';
|
|
22
|
+
Object.assign(t.style, {
|
|
23
|
+
color: fg,
|
|
24
|
+
fontSize: '54px',
|
|
25
|
+
fontWeight: '800',
|
|
26
|
+
transform: 'translateY(20px)',
|
|
27
|
+
opacity: '0',
|
|
28
|
+
transition:
|
|
29
|
+
'transform 600ms cubic-bezier(0.22,0.61,0.36,1) 200ms, opacity 600ms ease 200ms',
|
|
30
|
+
textAlign: 'center',
|
|
31
|
+
padding: '0 40px',
|
|
32
|
+
lineHeight: '1.2',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const accentBar = document.createElement('div');
|
|
36
|
+
Object.assign(accentBar.style, {
|
|
37
|
+
width: '60px',
|
|
38
|
+
height: '4px',
|
|
39
|
+
background: accent,
|
|
40
|
+
margin: '20px 0',
|
|
41
|
+
transform: 'scaleX(0)',
|
|
42
|
+
transformOrigin: 'left center',
|
|
43
|
+
transition: 'transform 500ms cubic-bezier(0.22,0.61,0.36,1) 600ms',
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const s = document.createElement('div');
|
|
47
|
+
s.textContent = subtitle || '';
|
|
48
|
+
Object.assign(s.style, {
|
|
49
|
+
color: fg,
|
|
50
|
+
fontSize: '22px',
|
|
51
|
+
fontWeight: '400',
|
|
52
|
+
transform: 'translateY(20px)',
|
|
53
|
+
opacity: '0',
|
|
54
|
+
transition: 'transform 500ms ease 700ms, opacity 500ms ease 700ms',
|
|
55
|
+
textAlign: 'center',
|
|
56
|
+
padding: '0 40px',
|
|
57
|
+
lineHeight: '1.4',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
card.append(t);
|
|
61
|
+
if (subtitle) card.append(accentBar, s);
|
|
62
|
+
root.appendChild(card);
|
|
63
|
+
requestAnimationFrame(() => {
|
|
64
|
+
card.style.opacity = '1';
|
|
65
|
+
t.style.transform = 'translateY(0)';
|
|
66
|
+
t.style.opacity = '1';
|
|
67
|
+
accentBar.style.transform = 'scaleX(1)';
|
|
68
|
+
s.style.transform = 'translateY(0)';
|
|
69
|
+
s.style.opacity = '0.85';
|
|
70
|
+
});
|
|
71
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Load .js files from `js/` as strings, cached at import time."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
_JS_DIR = Path(__file__).resolve().parent / "js"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@lru_cache(maxsize=None)
|
|
11
|
+
def load(name: str) -> str:
|
|
12
|
+
"""Return the contents of js/<name>.js. Raises FileNotFoundError if missing."""
|
|
13
|
+
path = _JS_DIR / f"{name}.js"
|
|
14
|
+
return path.read_text(encoding="utf-8")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Small wrappers around the overlay-management JS snippets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from . import js_loader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def ensure_overlay(page) -> None:
|
|
8
|
+
page.evaluate(js_loader.load("overlay_bootstrap"))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def clear_overlay(page) -> None:
|
|
12
|
+
page.evaluate(js_loader.load("clear_overlay"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fade_out_overlays(page, duration: int) -> None:
|
|
16
|
+
page.evaluate(js_loader.load("fade_out_overlays"), duration)
|
|
17
|
+
page.wait_for_timeout(duration)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Plain recording: fixed duration / stop-when / scroll-through."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from . import js_loader
|
|
9
|
+
from .bootstrap import ensure_runtime
|
|
10
|
+
from .browser import (
|
|
11
|
+
apply_pre_action_waits,
|
|
12
|
+
build_context_options,
|
|
13
|
+
launch_with_browser_install,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _scroll_through(page, cfg: dict) -> None:
|
|
18
|
+
step = cfg.get("scrollStep", 60)
|
|
19
|
+
interval = cfg.get("scrollInterval", 50)
|
|
20
|
+
pause_top = cfg.get("scrollPauseTop", 800)
|
|
21
|
+
pause_bottom = cfg.get("scrollPauseBottom", 1200)
|
|
22
|
+
page.evaluate("() => window.scrollTo(0, 0)")
|
|
23
|
+
page.wait_for_timeout(pause_top)
|
|
24
|
+
page.evaluate(js_loader.load("scroll_through"), {"step": step, "interval": interval})
|
|
25
|
+
page.wait_for_timeout(pause_bottom)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _wait_for_stop_condition(page, cfg: dict) -> str:
|
|
29
|
+
cap = cfg.get("maxDuration", 60000)
|
|
30
|
+
duration = cfg.get("duration")
|
|
31
|
+
stop_when_selector = cfg.get("stopWhenSelector")
|
|
32
|
+
stop_when_hidden = cfg.get("stopWhenHidden")
|
|
33
|
+
if not (duration or stop_when_selector or stop_when_hidden):
|
|
34
|
+
page.wait_for_timeout(5000)
|
|
35
|
+
return "default-5s"
|
|
36
|
+
|
|
37
|
+
deadline = time.time() + cap / 1000.0
|
|
38
|
+
if duration:
|
|
39
|
+
deadline = min(deadline, time.time() + duration / 1000.0)
|
|
40
|
+
|
|
41
|
+
poll = 100
|
|
42
|
+
while time.time() < deadline:
|
|
43
|
+
if stop_when_selector:
|
|
44
|
+
try:
|
|
45
|
+
page.wait_for_selector(stop_when_selector, state="visible", timeout=poll)
|
|
46
|
+
return "stopWhenSelector"
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
if stop_when_hidden:
|
|
50
|
+
try:
|
|
51
|
+
page.wait_for_selector(stop_when_hidden, state="hidden", timeout=poll)
|
|
52
|
+
return "stopWhenHidden"
|
|
53
|
+
except Exception:
|
|
54
|
+
pass
|
|
55
|
+
if not (stop_when_selector or stop_when_hidden):
|
|
56
|
+
page.wait_for_timeout(min(poll, max(0, int((deadline - time.time()) * 1000))))
|
|
57
|
+
return "duration" if duration else "cap"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def do_record(cfg: dict) -> Path:
|
|
61
|
+
"""Plain recording — returns the absolute output path."""
|
|
62
|
+
ensure_runtime()
|
|
63
|
+
from playwright.sync_api import sync_playwright
|
|
64
|
+
|
|
65
|
+
output = Path(cfg["output"]).resolve()
|
|
66
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
out_dir = str(output.parent)
|
|
68
|
+
|
|
69
|
+
with sync_playwright() as p:
|
|
70
|
+
browser = launch_with_browser_install(p, cfg.get("browser"))
|
|
71
|
+
try:
|
|
72
|
+
context = browser.new_context(**build_context_options(p, cfg, out_dir))
|
|
73
|
+
if cfg.get("timeout"):
|
|
74
|
+
context.set_default_timeout(cfg["timeout"])
|
|
75
|
+
page = context.new_page()
|
|
76
|
+
video_path: str | None = None
|
|
77
|
+
try:
|
|
78
|
+
page.goto(cfg["url"], wait_until="domcontentloaded")
|
|
79
|
+
apply_pre_action_waits(page, cfg)
|
|
80
|
+
if cfg.get("scrollThrough"):
|
|
81
|
+
_scroll_through(page, cfg)
|
|
82
|
+
reason = "scrollThrough"
|
|
83
|
+
else:
|
|
84
|
+
reason = _wait_for_stop_condition(page, cfg)
|
|
85
|
+
print(f"[runner] stop reason: {reason}", file=sys.stderr)
|
|
86
|
+
video = page.video
|
|
87
|
+
context.close()
|
|
88
|
+
if video:
|
|
89
|
+
video_path = video.path()
|
|
90
|
+
finally:
|
|
91
|
+
pass
|
|
92
|
+
finally:
|
|
93
|
+
browser.close()
|
|
94
|
+
|
|
95
|
+
if not video_path or not Path(video_path).exists():
|
|
96
|
+
raise SystemExit("record finished but no video file was produced")
|
|
97
|
+
src = Path(video_path)
|
|
98
|
+
if src.resolve() != output:
|
|
99
|
+
src.replace(output)
|
|
100
|
+
return output
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Scene registry.
|
|
2
|
+
|
|
3
|
+
Each scene type registers a handler ``(page, scene_cfg) -> None`` via the
|
|
4
|
+
``@register("type")`` decorator. Import the per-scene modules at the bottom so
|
|
5
|
+
their decorators fire at package import time.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
SceneHandler = Callable[[object, dict], None]
|
|
12
|
+
|
|
13
|
+
_REGISTRY: dict[str, SceneHandler] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register(type_name: str):
|
|
17
|
+
def decorator(handler: SceneHandler) -> SceneHandler:
|
|
18
|
+
_REGISTRY[type_name] = handler
|
|
19
|
+
return handler
|
|
20
|
+
return decorator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_handler(type_name: str) -> SceneHandler | None:
|
|
24
|
+
return _REGISTRY.get(type_name)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def known_types() -> list[str]:
|
|
28
|
+
return sorted(_REGISTRY.keys())
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Auto-discover every non-underscore module in this package so its
|
|
32
|
+
# @register decorators fire at import time. Adding a new scene only
|
|
33
|
+
# requires dropping a `scenes/<type>.py` file — no edits here.
|
|
34
|
+
import importlib as _importlib
|
|
35
|
+
import pkgutil as _pkgutil
|
|
36
|
+
|
|
37
|
+
for _, _name, _ in _pkgutil.iter_modules(__path__):
|
|
38
|
+
if not _name.startswith("_"):
|
|
39
|
+
_importlib.import_module(f"{__name__}.{_name}")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Arrow scene: curved arrow from one anchor (selector/coords) to another."""
|
|
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 _resolve_anchor(page, anchor: dict) -> tuple[float, float]:
|
|
10
|
+
"""Anchor is either {x, y} or {selector, side}. Returns page-fixed (x, y)."""
|
|
11
|
+
if anchor is None:
|
|
12
|
+
raise RuntimeError("arrow: missing anchor (need from/to)")
|
|
13
|
+
if "x" in anchor and "y" in anchor:
|
|
14
|
+
return float(anchor["x"]), float(anchor["y"])
|
|
15
|
+
sel = anchor.get("selector")
|
|
16
|
+
if not sel:
|
|
17
|
+
raise RuntimeError("arrow: anchor needs either {x,y} or {selector}")
|
|
18
|
+
loc = resolve_locator(page, sel)
|
|
19
|
+
box = loc.bounding_box()
|
|
20
|
+
if not box:
|
|
21
|
+
raise RuntimeError(f"arrow: no bounding box for {sel}")
|
|
22
|
+
side = anchor.get("side", "center")
|
|
23
|
+
cx = box["x"] + box["width"] / 2
|
|
24
|
+
cy = box["y"] + box["height"] / 2
|
|
25
|
+
if side == "left":
|
|
26
|
+
return box["x"], cy
|
|
27
|
+
if side == "right":
|
|
28
|
+
return box["x"] + box["width"], cy
|
|
29
|
+
if side == "top":
|
|
30
|
+
return cx, box["y"]
|
|
31
|
+
if side == "bottom":
|
|
32
|
+
return cx, box["y"] + box["height"]
|
|
33
|
+
return cx, cy
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@register("arrow")
|
|
37
|
+
def scene_arrow(page, scene: dict) -> None:
|
|
38
|
+
fx, fy = _resolve_anchor(page, scene.get("from") or {})
|
|
39
|
+
tx, ty = _resolve_anchor(page, scene.get("to") or {})
|
|
40
|
+
page.evaluate(
|
|
41
|
+
js_loader.load("arrow"),
|
|
42
|
+
{
|
|
43
|
+
"from": {"x": fx, "y": fy},
|
|
44
|
+
"to": {"x": tx, "y": ty},
|
|
45
|
+
"color": scene.get("color", "#ff3b30"),
|
|
46
|
+
"lineWidth": scene.get("lineWidth", 4),
|
|
47
|
+
"label": scene.get("label", ""),
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
page.wait_for_timeout(scene.get("duration", 3000))
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Caption scene: subtitle strip sliding in from top or bottom edge."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from .. import js_loader
|
|
5
|
+
from . import register
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@register("caption")
|
|
9
|
+
def scene_caption(page, scene: dict) -> None:
|
|
10
|
+
page.evaluate(
|
|
11
|
+
js_loader.load("caption"),
|
|
12
|
+
{
|
|
13
|
+
"text": scene.get("text", ""),
|
|
14
|
+
"position": scene.get("position", "bottom"),
|
|
15
|
+
"bg": scene.get("background", "rgba(0,0,0,0.78)"),
|
|
16
|
+
"fg": scene.get("color", "#fff"),
|
|
17
|
+
"fontSize": scene.get("fontSize", 20),
|
|
18
|
+
},
|
|
19
|
+
)
|
|
20
|
+
page.wait_for_timeout(scene.get("duration", 3000))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Click scene: animated cursor flies to target, ripple, then optional real click."""
|
|
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("click")
|
|
10
|
+
def scene_click(page, scene: dict) -> None:
|
|
11
|
+
locator = resolve_locator(page, scene["selector"])
|
|
12
|
+
box = locator.bounding_box()
|
|
13
|
+
if not box:
|
|
14
|
+
raise RuntimeError(f"click: 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
|
+
pre_hold = int(scene.get("preHoldMs", 150))
|
|
20
|
+
post_hold = int(scene.get("postHoldMs", 800))
|
|
21
|
+
ripple_ms = int(scene.get("rippleMs", 550))
|
|
22
|
+
color = scene.get("color", "#ff3b30")
|
|
23
|
+
|
|
24
|
+
page.evaluate(js_loader.load("cursor_bootstrap"))
|
|
25
|
+
page.evaluate(js_loader.load("cursor_move"), {"x": cx, "y": cy, "ms": move_ms})
|
|
26
|
+
page.wait_for_timeout(move_ms + pre_hold)
|
|
27
|
+
page.evaluate(js_loader.load("cursor_ripple"), {"color": color, "ms": ripple_ms})
|
|
28
|
+
|
|
29
|
+
if scene.get("realClick", True):
|
|
30
|
+
try:
|
|
31
|
+
locator.evaluate("el => el.click()")
|
|
32
|
+
except Exception:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
page.wait_for_timeout(post_hold)
|