clipwise 0.7.1 → 0.7.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.ko.md +2 -2
- package/README.md +34 -9
- package/dist/cli/index.js +224 -216
- package/dist/compose/frame-worker.js +58 -12
- package/dist/index.d.ts +312 -13
- package/dist/index.js +195 -70
- package/package.json +1 -1
- package/skills/clipwise.md +29 -1
package/dist/index.js
CHANGED
|
@@ -141,9 +141,16 @@ var ClipwiseRecorder = class {
|
|
|
141
141
|
this.cursorSpeed = scenario.effects.cursor.speed;
|
|
142
142
|
this.loaderDetectionEnabled = scenario.effects.smartSpeed?.enabled ?? false;
|
|
143
143
|
this.browser = await chromium.launch({ headless: true });
|
|
144
|
-
|
|
144
|
+
const contextOptions = {
|
|
145
145
|
viewport: this.viewport
|
|
146
|
-
}
|
|
146
|
+
};
|
|
147
|
+
if (scenario.auth?.storageState) {
|
|
148
|
+
contextOptions.storageState = scenario.auth.storageState;
|
|
149
|
+
}
|
|
150
|
+
this.context = await this.browser.newContext(contextOptions);
|
|
151
|
+
if (scenario.auth?.cookies?.length) {
|
|
152
|
+
await this.context.addCookies(scenario.auth.cookies);
|
|
153
|
+
}
|
|
147
154
|
this.page = await this.context.newPage();
|
|
148
155
|
this.rawFrames = [];
|
|
149
156
|
this.cursorTimeline = [];
|
|
@@ -466,6 +473,31 @@ var ClipwiseRecorder = class {
|
|
|
466
473
|
}
|
|
467
474
|
}
|
|
468
475
|
}
|
|
476
|
+
/**
|
|
477
|
+
* 조건 대기 중 프레임을 연속 캡처하는 헬퍼.
|
|
478
|
+
* smartWait과 동일한 패턴: isWaitingPhase 플래그 + forceRepaint 루프를 조건 promise와 병렬 실행.
|
|
479
|
+
*/
|
|
480
|
+
async waitForConditionWithCapture(conditionPromise, displaySpeed) {
|
|
481
|
+
this.isWaitingPhase = true;
|
|
482
|
+
this.currentDisplaySpeed = displaySpeed;
|
|
483
|
+
try {
|
|
484
|
+
let waitDone = false;
|
|
485
|
+
const repaintLoop = (async () => {
|
|
486
|
+
let toggle = false;
|
|
487
|
+
while (!waitDone && this.isCapturing && this.page) {
|
|
488
|
+
await this.forceRepaint(toggle);
|
|
489
|
+
toggle = !toggle;
|
|
490
|
+
await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
|
|
491
|
+
}
|
|
492
|
+
})();
|
|
493
|
+
await conditionPromise;
|
|
494
|
+
waitDone = true;
|
|
495
|
+
await repaintLoop;
|
|
496
|
+
} finally {
|
|
497
|
+
this.isWaitingPhase = false;
|
|
498
|
+
this.currentDisplaySpeed = void 0;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
469
501
|
/**
|
|
470
502
|
* Pre-register waitForResponse listeners at the start of each step.
|
|
471
503
|
* This ensures the listener is active before any preceding action
|
|
@@ -556,6 +588,17 @@ var ClipwiseRecorder = class {
|
|
|
556
588
|
lastClickRefresh = now;
|
|
557
589
|
}
|
|
558
590
|
}
|
|
591
|
+
await this.page.evaluate((sel) => {
|
|
592
|
+
const el = document.querySelector(sel);
|
|
593
|
+
if (!el) return;
|
|
594
|
+
const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
|
|
595
|
+
const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
|
|
596
|
+
if (setter) {
|
|
597
|
+
setter.call(el, el.value);
|
|
598
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
599
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
600
|
+
}
|
|
601
|
+
}, action.selector);
|
|
559
602
|
this.clickTimeline.push({
|
|
560
603
|
position: { ...inputTarget },
|
|
561
604
|
timestamp: Date.now()
|
|
@@ -629,83 +672,89 @@ var ClipwiseRecorder = class {
|
|
|
629
672
|
}
|
|
630
673
|
case "waitForSelector": {
|
|
631
674
|
const locator = this.page.locator(action.selector).first();
|
|
632
|
-
|
|
675
|
+
const selectorPromise = locator.waitFor({ state: action.state, timeout: action.timeout });
|
|
676
|
+
if (action.captureWhileWaiting) {
|
|
677
|
+
await this.waitForConditionWithCapture(selectorPromise, action.displaySpeed);
|
|
678
|
+
} else {
|
|
679
|
+
await selectorPromise;
|
|
680
|
+
}
|
|
633
681
|
break;
|
|
634
682
|
}
|
|
635
683
|
case "waitForNavigation": {
|
|
636
|
-
|
|
684
|
+
const navPromise = this.page.waitForLoadState(action.waitUntil, { timeout: action.timeout });
|
|
685
|
+
if (action.captureWhileWaiting) {
|
|
686
|
+
await this.waitForConditionWithCapture(navPromise, action.displaySpeed);
|
|
687
|
+
} else {
|
|
688
|
+
await navPromise;
|
|
689
|
+
}
|
|
637
690
|
break;
|
|
638
691
|
}
|
|
639
692
|
case "waitForURL": {
|
|
640
|
-
|
|
693
|
+
const urlPromise = this.page.waitForURL(action.url, { timeout: action.timeout });
|
|
694
|
+
if (action.captureWhileWaiting) {
|
|
695
|
+
await this.waitForConditionWithCapture(urlPromise, action.displaySpeed);
|
|
696
|
+
} else {
|
|
697
|
+
await urlPromise;
|
|
698
|
+
}
|
|
641
699
|
break;
|
|
642
700
|
}
|
|
643
701
|
case "waitForFunction": {
|
|
644
|
-
|
|
702
|
+
const fnPromise = this.page.waitForFunction(action.expression, void 0, {
|
|
645
703
|
polling: action.polling,
|
|
646
704
|
timeout: action.timeout
|
|
647
705
|
});
|
|
706
|
+
if (action.captureWhileWaiting) {
|
|
707
|
+
await this.waitForConditionWithCapture(fnPromise, action.displaySpeed);
|
|
708
|
+
} else {
|
|
709
|
+
await fnPromise;
|
|
710
|
+
}
|
|
648
711
|
break;
|
|
649
712
|
}
|
|
650
713
|
case "waitForResponse": {
|
|
651
714
|
const pending = this.pendingResponsePromises.get(actionIndex);
|
|
652
715
|
if (pending) {
|
|
653
|
-
|
|
716
|
+
if (action.captureWhileWaiting) {
|
|
717
|
+
await this.waitForConditionWithCapture(pending, action.displaySpeed);
|
|
718
|
+
} else {
|
|
719
|
+
await pending;
|
|
720
|
+
}
|
|
654
721
|
}
|
|
655
722
|
break;
|
|
656
723
|
}
|
|
657
724
|
case "smartWait": {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
let timer;
|
|
673
|
-
const observer = new MutationObserver(() => {
|
|
674
|
-
clearTimeout(timer);
|
|
675
|
-
timer = setTimeout(() => {
|
|
676
|
-
observer.disconnect();
|
|
677
|
-
resolve(true);
|
|
678
|
-
}, 500);
|
|
679
|
-
});
|
|
680
|
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
725
|
+
let conditionPromise;
|
|
726
|
+
switch (action.until) {
|
|
727
|
+
case "networkIdle":
|
|
728
|
+
conditionPromise = this.page.waitForLoadState("networkidle", { timeout: action.timeout });
|
|
729
|
+
break;
|
|
730
|
+
case "selector":
|
|
731
|
+
conditionPromise = action.selector ? this.page.locator(action.selector).first().waitFor({ state: "visible", timeout: action.timeout }) : Promise.resolve();
|
|
732
|
+
break;
|
|
733
|
+
case "domStable":
|
|
734
|
+
conditionPromise = this.page.waitForFunction(
|
|
735
|
+
() => new Promise((resolve) => {
|
|
736
|
+
let timer;
|
|
737
|
+
const observer = new MutationObserver(() => {
|
|
738
|
+
clearTimeout(timer);
|
|
681
739
|
timer = setTimeout(() => {
|
|
682
740
|
observer.disconnect();
|
|
683
741
|
resolve(true);
|
|
684
742
|
}, 500);
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
toggle = !toggle;
|
|
699
|
-
await new Promise((r) => setTimeout(r, REPAINT_INTERVAL_MS));
|
|
700
|
-
}
|
|
701
|
-
})();
|
|
702
|
-
await conditionPromise;
|
|
703
|
-
waitDone = true;
|
|
704
|
-
await repaintLoop;
|
|
705
|
-
} finally {
|
|
706
|
-
this.isWaitingPhase = false;
|
|
707
|
-
this.currentDisplaySpeed = void 0;
|
|
743
|
+
});
|
|
744
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true });
|
|
745
|
+
timer = setTimeout(() => {
|
|
746
|
+
observer.disconnect();
|
|
747
|
+
resolve(true);
|
|
748
|
+
}, 500);
|
|
749
|
+
}),
|
|
750
|
+
void 0,
|
|
751
|
+
{ timeout: action.timeout }
|
|
752
|
+
);
|
|
753
|
+
break;
|
|
754
|
+
default:
|
|
755
|
+
conditionPromise = Promise.resolve();
|
|
708
756
|
}
|
|
757
|
+
await this.waitForConditionWithCapture(conditionPromise, action.displaySpeed);
|
|
709
758
|
break;
|
|
710
759
|
}
|
|
711
760
|
}
|
|
@@ -1551,6 +1600,44 @@ import sharp5 from "sharp";
|
|
|
1551
1600
|
function escapeXml(s) {
|
|
1552
1601
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1553
1602
|
}
|
|
1603
|
+
function isCJK(ch) {
|
|
1604
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
1605
|
+
return code >= 4352 && code <= 4607 || // 한글 자모
|
|
1606
|
+
code >= 11904 && code <= 40959 || // CJK 부수, 한자
|
|
1607
|
+
code >= 44032 && code <= 55215 || // 한글 음절
|
|
1608
|
+
code >= 63744 && code <= 64255 || // CJK 호환 한자
|
|
1609
|
+
code >= 65072 && code <= 65103 || // CJK 호환 형태
|
|
1610
|
+
code >= 65280 && code <= 65519 || // Full-width 문자
|
|
1611
|
+
code >= 12288 && code <= 12543 || // CJK 기호, 히라가나, 가타카나
|
|
1612
|
+
code >= 12784 && code <= 12799 || // 가타카나 확장
|
|
1613
|
+
code >= 131072 && code <= 195103;
|
|
1614
|
+
}
|
|
1615
|
+
function displayWidth(text) {
|
|
1616
|
+
let w = 0;
|
|
1617
|
+
for (const ch of text) {
|
|
1618
|
+
w += isCJK(ch) ? 1.7 : 1;
|
|
1619
|
+
}
|
|
1620
|
+
return w;
|
|
1621
|
+
}
|
|
1622
|
+
function wrapText(text, maxWidth) {
|
|
1623
|
+
if (displayWidth(text) <= maxWidth) return [text];
|
|
1624
|
+
const lines = [];
|
|
1625
|
+
let current = "";
|
|
1626
|
+
let currentWidth = 0;
|
|
1627
|
+
for (const ch of text) {
|
|
1628
|
+
const chWidth = isCJK(ch) ? 1.7 : 1;
|
|
1629
|
+
if (currentWidth + chWidth > maxWidth && current.length > 0) {
|
|
1630
|
+
lines.push(current);
|
|
1631
|
+
current = ch;
|
|
1632
|
+
currentWidth = chWidth;
|
|
1633
|
+
} else {
|
|
1634
|
+
current += ch;
|
|
1635
|
+
currentWidth += chWidth;
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
if (current.length > 0) lines.push(current);
|
|
1639
|
+
return lines;
|
|
1640
|
+
}
|
|
1554
1641
|
function buildSessions(keystrokes) {
|
|
1555
1642
|
const hasSessionIds = keystrokes.some((k) => k.sessionId !== void 0);
|
|
1556
1643
|
if (!hasSessionIds) {
|
|
@@ -1584,16 +1671,22 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
1584
1671
|
const lineGap = Math.round(fontSize * 0.45);
|
|
1585
1672
|
const charWidth = fontSize * 0.615;
|
|
1586
1673
|
const maxHudWidth = frameWidth - 60 * dpr;
|
|
1587
|
-
const
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1674
|
+
const maxDisplayWidth = Math.max(10, (maxHudWidth - hudPadH * 2) / charWidth);
|
|
1675
|
+
const wrappedLines = [];
|
|
1676
|
+
sessions.forEach((text, sIdx) => {
|
|
1677
|
+
const wrapped = wrapText(text, maxDisplayWidth);
|
|
1678
|
+
for (const line of wrapped) {
|
|
1679
|
+
wrappedLines.push({ text: line, sessionIdx: sIdx });
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
const lines = wrappedLines.map((l) => l.text);
|
|
1683
|
+
const totalLineCount = lines.length;
|
|
1684
|
+
const maxLineDisplayWidth = Math.max(...lines.map((l) => displayWidth(l)));
|
|
1592
1685
|
const hudWidth = Math.min(
|
|
1593
|
-
Math.ceil(
|
|
1686
|
+
Math.ceil(maxLineDisplayWidth * charWidth) + hudPadH * 2,
|
|
1594
1687
|
maxHudWidth
|
|
1595
1688
|
);
|
|
1596
|
-
const hudHeight = Math.ceil(fontSize *
|
|
1689
|
+
const hudHeight = Math.ceil(fontSize * totalLineCount + lineGap * (totalLineCount - 1) + hudPadV * 2);
|
|
1597
1690
|
const margin = 30 * dpr;
|
|
1598
1691
|
const hudY = frameHeight - hudHeight - margin;
|
|
1599
1692
|
let hudX;
|
|
@@ -1608,18 +1701,20 @@ async function renderKeystrokeHud(frameBuffer, keystrokes, frameTimestamp, confi
|
|
|
1608
1701
|
default:
|
|
1609
1702
|
hudX = Math.round((frameWidth - hudWidth) / 2);
|
|
1610
1703
|
}
|
|
1611
|
-
const
|
|
1612
|
-
const
|
|
1704
|
+
const SESSION_OPACITY_FACTORS = [0.45, 0.7, 1];
|
|
1705
|
+
const sessionOpacities = SESSION_OPACITY_FACTORS.slice(-lineCount);
|
|
1613
1706
|
const rx = (8 * dpr).toFixed(1);
|
|
1614
1707
|
const boxOp = (globalOpacity * 0.92).toFixed(3);
|
|
1615
1708
|
const textX = hudX + hudPadH;
|
|
1616
1709
|
const baselineY = hudY + hudPadV + fontSize * 0.82;
|
|
1617
|
-
const textElements =
|
|
1618
|
-
const
|
|
1710
|
+
const textElements = wrappedLines.map(({ text, sessionIdx }, i) => {
|
|
1711
|
+
const sessionPos = sessions.length <= 3 ? sessionIdx : sessionIdx - (sessions.length - 3);
|
|
1712
|
+
const opFactor = sessionOpacities[Math.max(0, sessionPos)] ?? 1;
|
|
1713
|
+
const op = (globalOpacity * opFactor).toFixed(3);
|
|
1619
1714
|
const lineY = baselineY + i * (fontSize + lineGap);
|
|
1620
1715
|
return `<text x="${textX}" y="${lineY}"
|
|
1621
1716
|
font-family="monospace, Menlo, Consolas" font-size="${fontSize}"
|
|
1622
|
-
fill="${config.textColor}" opacity="${op}">${escapeXml(
|
|
1717
|
+
fill="${config.textColor}" opacity="${op}">${escapeXml(text)}</text>`;
|
|
1623
1718
|
}).join("\n ");
|
|
1624
1719
|
const hudSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${frameWidth}" height="${frameHeight}" shape-rendering="geometricPrecision" text-rendering="geometricPrecision">
|
|
1625
1720
|
<rect x="${hudX}" y="${hudY}" width="${hudWidth}" height="${hudHeight}"
|
|
@@ -3308,29 +3403,41 @@ var WaitForSelectorActionSchema = z.object({
|
|
|
3308
3403
|
action: z.literal("waitForSelector"),
|
|
3309
3404
|
selector: SafeSelectorSchema,
|
|
3310
3405
|
state: z.enum(["visible", "attached", "hidden"]).default("visible"),
|
|
3311
|
-
timeout: z.number().min(0).default(15e3)
|
|
3406
|
+
timeout: z.number().min(0).default(15e3),
|
|
3407
|
+
/** 대기 중 프레임 연속 캡처 (로딩 애니메이션 보존). */
|
|
3408
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
3409
|
+
/** captureWhileWaiting 사용 시 출력 영상 속도 배율 (1-32). */
|
|
3410
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
3312
3411
|
});
|
|
3313
3412
|
var WaitForNavigationActionSchema = z.object({
|
|
3314
3413
|
action: z.literal("waitForNavigation"),
|
|
3315
3414
|
waitUntil: z.enum(["load", "domcontentloaded", "networkidle"]).default("networkidle"),
|
|
3316
|
-
timeout: z.number().min(0).default(15e3)
|
|
3415
|
+
timeout: z.number().min(0).default(15e3),
|
|
3416
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
3417
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
3317
3418
|
});
|
|
3318
3419
|
var WaitForURLActionSchema = z.object({
|
|
3319
3420
|
action: z.literal("waitForURL"),
|
|
3320
3421
|
url: z.string().min(1),
|
|
3321
|
-
timeout: z.number().min(0).default(15e3)
|
|
3422
|
+
timeout: z.number().min(0).default(15e3),
|
|
3423
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
3424
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
3322
3425
|
});
|
|
3323
3426
|
var WaitForFunctionActionSchema = z.object({
|
|
3324
3427
|
action: z.literal("waitForFunction"),
|
|
3325
3428
|
expression: z.string().min(1),
|
|
3326
3429
|
polling: z.union([z.literal("raf"), z.number().min(0)]).default("raf"),
|
|
3327
|
-
timeout: z.number().min(0).default(3e4)
|
|
3430
|
+
timeout: z.number().min(0).default(3e4),
|
|
3431
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
3432
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
3328
3433
|
});
|
|
3329
3434
|
var WaitForResponseActionSchema = z.object({
|
|
3330
3435
|
action: z.literal("waitForResponse"),
|
|
3331
3436
|
url: z.string().min(1),
|
|
3332
3437
|
status: z.number().min(100).max(599).optional(),
|
|
3333
|
-
timeout: z.number().min(0).default(3e4)
|
|
3438
|
+
timeout: z.number().min(0).default(3e4),
|
|
3439
|
+
captureWhileWaiting: z.boolean().default(false),
|
|
3440
|
+
displaySpeed: z.number().min(1).max(32).default(8)
|
|
3334
3441
|
});
|
|
3335
3442
|
var SmartWaitActionSchema = z.object({
|
|
3336
3443
|
action: z.literal("smartWait"),
|
|
@@ -3525,6 +3632,22 @@ var AudioConfigSchema = z.object({
|
|
|
3525
3632
|
/** Fade-out duration in milliseconds. */
|
|
3526
3633
|
fadeOut: z.number().min(0).default(0)
|
|
3527
3634
|
});
|
|
3635
|
+
var AuthConfigSchema = z.object({
|
|
3636
|
+
/** Path to a Playwright storageState JSON file (cookies + localStorage). */
|
|
3637
|
+
storageState: z.string().optional(),
|
|
3638
|
+
/** Inline cookie definitions (applied after storageState if both specified). */
|
|
3639
|
+
cookies: z.array(
|
|
3640
|
+
z.object({
|
|
3641
|
+
name: z.string(),
|
|
3642
|
+
value: z.string(),
|
|
3643
|
+
domain: z.string(),
|
|
3644
|
+
path: z.string().default("/"),
|
|
3645
|
+
httpOnly: z.boolean().default(false),
|
|
3646
|
+
secure: z.boolean().default(false),
|
|
3647
|
+
sameSite: z.enum(["Strict", "Lax", "None"]).default("Lax")
|
|
3648
|
+
})
|
|
3649
|
+
).optional()
|
|
3650
|
+
});
|
|
3528
3651
|
var ScenarioSchema = z.object({
|
|
3529
3652
|
name: z.string(),
|
|
3530
3653
|
description: z.string().optional(),
|
|
@@ -3532,6 +3655,8 @@ var ScenarioSchema = z.object({
|
|
|
3532
3655
|
width: z.number().default(1280),
|
|
3533
3656
|
height: z.number().default(800)
|
|
3534
3657
|
}).default({}),
|
|
3658
|
+
/** Optional authentication — restores browser session for logged-in pages. */
|
|
3659
|
+
auth: AuthConfigSchema.optional(),
|
|
3535
3660
|
effects: EffectsConfigSchema.default({}),
|
|
3536
3661
|
output: OutputConfigSchema.default({}),
|
|
3537
3662
|
/** Optional audio narration — muxed into MP4 output. */
|
package/package.json
CHANGED
package/skills/clipwise.md
CHANGED
|
@@ -52,6 +52,7 @@ output:
|
|
|
52
52
|
height: 800 # Output height
|
|
53
53
|
fps: 30 # 1-60
|
|
54
54
|
preset: balanced # social | balanced | archive
|
|
55
|
+
codec: auto # auto | h264 | hevc | av1
|
|
55
56
|
outputDir: "./output"
|
|
56
57
|
filename: "my-recording"
|
|
57
58
|
|
|
@@ -71,7 +72,7 @@ steps: [] # Array of steps (min 1, first must have navigate)
|
|
|
71
72
|
actions: [] # Array of actions
|
|
72
73
|
```
|
|
73
74
|
|
|
74
|
-
### Actions (
|
|
75
|
+
### Actions (13 types)
|
|
75
76
|
|
|
76
77
|
#### Basic Actions
|
|
77
78
|
|
|
@@ -169,6 +170,16 @@ steps: [] # Array of steps (min 1, first must have navigate)
|
|
|
169
170
|
timeout: 30000
|
|
170
171
|
```
|
|
171
172
|
|
|
173
|
+
13. **smartWait** — Record real wait time, then auto-compress in output
|
|
174
|
+
```yaml
|
|
175
|
+
- action: smartWait
|
|
176
|
+
until: networkIdle # networkIdle | selector | domStable
|
|
177
|
+
selector: ".results" # Required when until=selector
|
|
178
|
+
timeout: 30000 # Max wait ms (default: 30000)
|
|
179
|
+
displaySpeed: 8 # Speed multiplier for output (1-32, default: 8)
|
|
180
|
+
```
|
|
181
|
+
> Unlike fixed `wait`, `smartWait` captures frames during the wait period (with forced repaints to bypass dedup), then compresses them at `displaySpeed` in the final video. Use this for API calls, loading states, streaming responses.
|
|
182
|
+
|
|
172
183
|
### Effects Configuration
|
|
173
184
|
|
|
174
185
|
#### Zoom — Adaptive zoom follows cursor on clicks (smart camera: auto-suppressed during scroll)
|
|
@@ -178,6 +189,7 @@ zoom:
|
|
|
178
189
|
intensity: light # subtle(1.15x) | light(1.25x) | moderate(1.35x) | strong(1.5x) | dramatic(1.8x)
|
|
179
190
|
# scale: 1.25 # Or use numeric value (overridden by intensity)
|
|
180
191
|
duration: 800 # Zoom animation ms
|
|
192
|
+
easing: ease-in-out # ease-in-out | ease-in | ease-out | linear | spring
|
|
181
193
|
autoZoom:
|
|
182
194
|
followCursor: true # Viewport pans to follow cursor position
|
|
183
195
|
transitionDuration: 300
|
|
@@ -261,6 +273,17 @@ speedRamp:
|
|
|
261
273
|
transitionFrames: 15
|
|
262
274
|
```
|
|
263
275
|
|
|
276
|
+
#### Smart Speed — Content-aware speed control (auto-compresses wait/loading periods)
|
|
277
|
+
```yaml
|
|
278
|
+
smartSpeed:
|
|
279
|
+
enabled: true
|
|
280
|
+
waitSpeed: 8 # Speed multiplier for smartWait frames (1-32, default: 8)
|
|
281
|
+
idleSpeed: 4 # Speed multiplier for idle frames (1-16, default: 4)
|
|
282
|
+
transitionDuration: 300 # Ease duration between speed changes (ms)
|
|
283
|
+
minSegmentDuration: 500 # Don't speed up segments shorter than this (ms)
|
|
284
|
+
```
|
|
285
|
+
> Unlike `speedRamp` (click-based), `smartSpeed` uses semantic metadata from `smartWait` actions and per-frame change scoring. Pairs naturally with `smartWait` for loading/API-call compression.
|
|
286
|
+
|
|
264
287
|
#### Audio Narration — Attach audio to MP4 output
|
|
265
288
|
```yaml
|
|
266
289
|
audio:
|
|
@@ -315,6 +338,11 @@ steps:
|
|
|
315
338
|
9. **Smart camera**: zoom is automatically suppressed during scroll actions; `followCursor` pans to cursor position
|
|
316
339
|
10. **Transitions**: use `fade` or `blur` for cinematic cuts between major sections; `slide-left`/`slide-up` for sequential flows
|
|
317
340
|
11. **Audio**: audio file must exist at the specified path; only works with MP4 output format
|
|
341
|
+
12. **Spring zoom**: use `easing: spring` for Screen Studio-like natural camera motion with fast initial response and smooth deceleration; nearby clicks are auto-merged into continuous zoom zones
|
|
342
|
+
13. **Zoom sustain during typing**: zoom automatically maintains throughout `type` actions — no need to add extra click events
|
|
343
|
+
14. **Auto loader detection**: CSS spinners (`@keyframes spin/rotate/pulse/bounce`) are passively detected via CDP and auto-marked for smartSpeed compression
|
|
344
|
+
15. **Codec choice**: `av1` gives 40-60% smaller files but slower encode; `hevc` provides 10-bit color (no gradient banding); `auto` picks h264 for compatibility
|
|
345
|
+
16. **smartWait over wait**: prefer `smartWait` over fixed `wait` for API calls and loading states — it captures real frames and auto-compresses them
|
|
318
346
|
|
|
319
347
|
## Timing Presets
|
|
320
348
|
|