autokap 1.1.2 → 1.1.4
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/dist/browser.d.ts +1 -0
- package/dist/browser.js +175 -1
- package/dist/cli-runner.js +25 -6
- package/dist/clip-capture-loop.d.ts +9 -0
- package/dist/clip-capture-loop.js +11 -0
- package/dist/mouse-animation.d.ts +4 -1
- package/dist/mouse-animation.js +7 -3
- package/dist/web-playwright-local.js +11 -2
- package/package.json +2 -2
package/dist/browser.d.ts
CHANGED
|
@@ -147,6 +147,7 @@ export declare class Browser {
|
|
|
147
147
|
* with an overall timeout of `timeoutMs`.
|
|
148
148
|
*/
|
|
149
149
|
private waitForDomStability;
|
|
150
|
+
private waitForFontsBeforeScreenshot;
|
|
150
151
|
takeScreenshot(): Promise<Buffer>;
|
|
151
152
|
takeScreenshotForAI(options?: {
|
|
152
153
|
timeoutMs?: number;
|
package/dist/browser.js
CHANGED
|
@@ -98,7 +98,7 @@ function resolveEffectivePadding(config, bbox) {
|
|
|
98
98
|
}
|
|
99
99
|
import { dismissCookiesAndWidgets, ensureCaptureHideStyles } from './cookie-dismiss.js';
|
|
100
100
|
import { CHROMIUM_ARGS, browserPool } from './browser-pool.js';
|
|
101
|
-
import { logger } from './logger.js';
|
|
101
|
+
import { isDebugEnabled, logger } from './logger.js';
|
|
102
102
|
async function withHelperTimeout(label, timeoutMs, work) {
|
|
103
103
|
if (!timeoutMs || timeoutMs <= 0) {
|
|
104
104
|
return work();
|
|
@@ -117,6 +117,82 @@ async function withHelperTimeout(label, timeoutMs, work) {
|
|
|
117
117
|
clearTimeout(timer);
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
|
+
function isLikelyFontUrl(url) {
|
|
121
|
+
return /\.(?:woff2?|ttf|otf)(?:[?#]|$)/i.test(url);
|
|
122
|
+
}
|
|
123
|
+
async function logFontDiagnostics(page, stage) {
|
|
124
|
+
if (!isDebugEnabled())
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
const diagnostics = await page.evaluate(async () => {
|
|
128
|
+
const fontPreloads = Array.from(document.querySelectorAll('link[rel="preload"][as="font"], link[as="font"]')).map((link) => ({
|
|
129
|
+
href: link.href,
|
|
130
|
+
type: link.type || null,
|
|
131
|
+
crossOrigin: link.crossOrigin || null,
|
|
132
|
+
}));
|
|
133
|
+
const fetches = [];
|
|
134
|
+
for (const preload of fontPreloads) {
|
|
135
|
+
const startedAt = performance.now();
|
|
136
|
+
try {
|
|
137
|
+
const response = await fetch(preload.href, {
|
|
138
|
+
cache: 'no-store',
|
|
139
|
+
credentials: 'include',
|
|
140
|
+
});
|
|
141
|
+
fetches.push({
|
|
142
|
+
url: preload.href,
|
|
143
|
+
ok: response.ok,
|
|
144
|
+
status: response.status,
|
|
145
|
+
contentType: response.headers.get('content-type'),
|
|
146
|
+
contentLength: response.headers.get('content-length'),
|
|
147
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
fetches.push({
|
|
152
|
+
url: preload.href,
|
|
153
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const normalizeFamily = (family) => family.trim().replace(/^['"]|['"]$/g, '');
|
|
159
|
+
const computedFamilies = Array.from(new Set([document.body, ...Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,a,button,input,textarea,label,span,[data-ak]')).slice(0, 40)]
|
|
160
|
+
.filter(Boolean)
|
|
161
|
+
.flatMap((node) => getComputedStyle(node).fontFamily
|
|
162
|
+
.split(',')
|
|
163
|
+
.map(normalizeFamily)
|
|
164
|
+
.filter(Boolean))));
|
|
165
|
+
const fontChecks = computedFamilies.map((family) => ({
|
|
166
|
+
family,
|
|
167
|
+
weight400: document.fonts.check(`400 16px "${family}"`, 'AutoKap Aa 0123456789'),
|
|
168
|
+
weight600: document.fonts.check(`600 16px "${family}"`, 'AutoKap Aa 0123456789'),
|
|
169
|
+
}));
|
|
170
|
+
return {
|
|
171
|
+
url: location.href,
|
|
172
|
+
count: document.fonts.size,
|
|
173
|
+
status: document.fonts.status,
|
|
174
|
+
faces: Array.from(document.fonts).map((font) => ({
|
|
175
|
+
family: font.family,
|
|
176
|
+
weight: font.weight,
|
|
177
|
+
style: font.style,
|
|
178
|
+
stretch: font.stretch,
|
|
179
|
+
status: font.status,
|
|
180
|
+
})),
|
|
181
|
+
bodyComputed: getComputedStyle(document.body).fontFamily,
|
|
182
|
+
documentElementClass: document.documentElement.className,
|
|
183
|
+
bodyClass: document.body.className,
|
|
184
|
+
computedFamilies,
|
|
185
|
+
fontChecks,
|
|
186
|
+
preloads: fontPreloads,
|
|
187
|
+
fetches,
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
logger.debug(`[capture] font diagnostics ${stage}: ${JSON.stringify(diagnostics)}`);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
logger.debug(`[capture] font diagnostics ${stage} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
120
196
|
/**
|
|
121
197
|
* Map a BCP-47 language tag to a Playwright-compatible locale string.
|
|
122
198
|
* Playwright accepts both "fr" and "fr-FR". We normalize 2-char codes to their
|
|
@@ -401,6 +477,8 @@ export class Browser {
|
|
|
401
477
|
attachDebugLifecycleListeners() {
|
|
402
478
|
if (!this.page || !this.context)
|
|
403
479
|
return;
|
|
480
|
+
if (!isDebugEnabled())
|
|
481
|
+
return;
|
|
404
482
|
const page = this.page;
|
|
405
483
|
const context = this.context;
|
|
406
484
|
page.on('crash', () => {
|
|
@@ -423,6 +501,21 @@ export class Browser {
|
|
|
423
501
|
logger.debug(`[page] console.${type}: ${msg.text().slice(0, 200)}`);
|
|
424
502
|
}
|
|
425
503
|
});
|
|
504
|
+
page.on('requestfailed', (request) => {
|
|
505
|
+
const url = request.url();
|
|
506
|
+
if (request.resourceType() !== 'font' && !isLikelyFontUrl(url))
|
|
507
|
+
return;
|
|
508
|
+
logger.debug(`[page] font request failed: ${url} — ${request.failure()?.errorText ?? 'unknown error'}`);
|
|
509
|
+
});
|
|
510
|
+
page.on('response', (response) => {
|
|
511
|
+
const request = response.request();
|
|
512
|
+
const url = response.url();
|
|
513
|
+
if (request.resourceType() !== 'font' && !isLikelyFontUrl(url))
|
|
514
|
+
return;
|
|
515
|
+
const headers = response.headers();
|
|
516
|
+
logger.debug(`[page] font response: status=${response.status()} type=${headers['content-type'] ?? 'unknown'} ` +
|
|
517
|
+
`length=${headers['content-length'] ?? 'unknown'} url=${url}`);
|
|
518
|
+
});
|
|
426
519
|
context.on('close', () => {
|
|
427
520
|
logger.debug(`[context] CLOSE event`);
|
|
428
521
|
});
|
|
@@ -466,6 +559,7 @@ export class Browser {
|
|
|
466
559
|
this.context = await this.browser.newContext(this.buildContextOptions());
|
|
467
560
|
this.page = await this.context.newPage();
|
|
468
561
|
this.elementMap.clear();
|
|
562
|
+
this.attachDebugLifecycleListeners();
|
|
469
563
|
}
|
|
470
564
|
async setDeviceScaleFactor(deviceScaleFactor) {
|
|
471
565
|
const normalizedScale = normalizeDeviceScaleFactor(deviceScaleFactor);
|
|
@@ -595,11 +689,83 @@ export class Browser {
|
|
|
595
689
|
// Page may have navigated during wait
|
|
596
690
|
}
|
|
597
691
|
}
|
|
692
|
+
async waitForFontsBeforeScreenshot(page) {
|
|
693
|
+
// Wait for web fonts to be loaded AND applied to the rendered page.
|
|
694
|
+
// next/font and other `font-display: swap` setups can report the
|
|
695
|
+
// FontFaceSet as "ready" while visible text is still painted with fallback.
|
|
696
|
+
// We force-load declared and currently computed families, verify the
|
|
697
|
+
// primary rendered families with document.fonts.check(), then wait for a
|
|
698
|
+
// committed repaint.
|
|
699
|
+
await page.evaluate(() => Promise.race([
|
|
700
|
+
(async () => {
|
|
701
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
702
|
+
const nextPaint = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
703
|
+
const normalizeFamily = (family) => family.trim().replace(/^['"]|['"]$/g, '');
|
|
704
|
+
const computedFamilies = () => {
|
|
705
|
+
const families = new Set();
|
|
706
|
+
const nodes = [
|
|
707
|
+
document.body,
|
|
708
|
+
...Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,a,button,input,textarea,label,span,[data-ak]')).slice(0, 80),
|
|
709
|
+
].filter(Boolean);
|
|
710
|
+
for (const node of nodes) {
|
|
711
|
+
const stack = getComputedStyle(node).fontFamily;
|
|
712
|
+
for (const family of stack.split(',')) {
|
|
713
|
+
const normalized = normalizeFamily(family);
|
|
714
|
+
if (normalized
|
|
715
|
+
&& !/^(serif|sans-serif|monospace|system-ui|ui-sans-serif|ui-monospace)$/i.test(normalized)) {
|
|
716
|
+
families.add(normalized);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return families;
|
|
721
|
+
};
|
|
722
|
+
const allFamilies = () => new Set([
|
|
723
|
+
...Array.from(document.fonts).map((font) => font.family),
|
|
724
|
+
...computedFamilies(),
|
|
725
|
+
]);
|
|
726
|
+
const probe = document.createElement('div');
|
|
727
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
728
|
+
probe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;visibility:hidden;pointer-events:none;';
|
|
729
|
+
for (const family of allFamilies()) {
|
|
730
|
+
const span = document.createElement('span');
|
|
731
|
+
span.style.fontFamily = `"${family}"`;
|
|
732
|
+
span.textContent = 'AutoKap Aa 0123456789';
|
|
733
|
+
probe.appendChild(span);
|
|
734
|
+
}
|
|
735
|
+
document.body.appendChild(probe);
|
|
736
|
+
void probe.offsetHeight;
|
|
737
|
+
const sample = 'AutoKap Aa 0123456789';
|
|
738
|
+
await Promise.all([
|
|
739
|
+
...Array.from(document.fonts).map((font) => font.load().catch(() => null)),
|
|
740
|
+
...Array.from(allFamilies()).flatMap((family) => [
|
|
741
|
+
document.fonts.load(`400 16px "${family}"`, sample).catch(() => null),
|
|
742
|
+
document.fonts.load(`600 16px "${family}"`, sample).catch(() => null),
|
|
743
|
+
]),
|
|
744
|
+
]);
|
|
745
|
+
await document.fonts.ready;
|
|
746
|
+
const deadline = performance.now() + 7000;
|
|
747
|
+
while (performance.now() < deadline) {
|
|
748
|
+
const renderedFamilies = Array.from(computedFamilies()).filter((family) => !/\bFallback\b/i.test(family));
|
|
749
|
+
const loaded = renderedFamilies.length === 0
|
|
750
|
+
|| renderedFamilies.every((family) => document.fonts.check(`400 16px "${family}"`, sample)
|
|
751
|
+
|| document.fonts.check(`600 16px "${family}"`, sample));
|
|
752
|
+
if (document.fonts.status === 'loaded' && loaded)
|
|
753
|
+
break;
|
|
754
|
+
await sleep(100);
|
|
755
|
+
}
|
|
756
|
+
probe.remove();
|
|
757
|
+
await nextPaint();
|
|
758
|
+
})(),
|
|
759
|
+
new Promise((resolve) => setTimeout(resolve, 8000)),
|
|
760
|
+
])).catch(() => { });
|
|
761
|
+
}
|
|
598
762
|
async takeScreenshot() {
|
|
599
763
|
const page = this.ensurePage();
|
|
600
764
|
// Move cursor off-screen to avoid hover effects in screenshots
|
|
601
765
|
await page.mouse.move(0, 0);
|
|
602
766
|
await ensureCaptureHideStyles(page);
|
|
767
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
768
|
+
await logFontDiagnostics(page, 'before screenshot');
|
|
603
769
|
return Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
|
|
604
770
|
}
|
|
605
771
|
async takeScreenshotForAI(options = {}) {
|
|
@@ -3972,6 +4138,8 @@ export class Browser {
|
|
|
3972
4138
|
if (clip.width <= 0 || clip.height <= 0) {
|
|
3973
4139
|
throw new Error(`Element index ${index} is still outside the viewport after alignment`);
|
|
3974
4140
|
}
|
|
4141
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4142
|
+
await logFontDiagnostics(page, 'before element screenshot');
|
|
3975
4143
|
return Buffer.from(await page.screenshot({ type: 'png', clip }));
|
|
3976
4144
|
}
|
|
3977
4145
|
finally {
|
|
@@ -3997,6 +4165,8 @@ export class Browser {
|
|
|
3997
4165
|
// Hide fixed/sticky overlays (navbars, banners) that could cover the region
|
|
3998
4166
|
await this.hideFixedOverlays();
|
|
3999
4167
|
try {
|
|
4168
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4169
|
+
await logFontDiagnostics(page, 'before region screenshot');
|
|
4000
4170
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4001
4171
|
const image = sharp(fullPage);
|
|
4002
4172
|
const meta = await image.metadata();
|
|
@@ -4118,6 +4288,8 @@ export class Browser {
|
|
|
4118
4288
|
await this.hideFixedOverlays();
|
|
4119
4289
|
try {
|
|
4120
4290
|
const dpr = pageInfo.dpr;
|
|
4291
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4292
|
+
await logFontDiagnostics(page, 'before selector screenshot');
|
|
4121
4293
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4122
4294
|
const image = sharp(fullPage);
|
|
4123
4295
|
const meta = await image.metadata();
|
|
@@ -4180,6 +4352,8 @@ export class Browser {
|
|
|
4180
4352
|
await this.hideFixedOverlays();
|
|
4181
4353
|
try {
|
|
4182
4354
|
const dpr = pageInfo.dpr;
|
|
4355
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4356
|
+
await logFontDiagnostics(page, 'before bounding-region screenshot');
|
|
4183
4357
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4184
4358
|
const image = sharp(fullPage);
|
|
4185
4359
|
const meta = await image.metadata();
|
package/dist/cli-runner.js
CHANGED
|
@@ -26,6 +26,7 @@ import { logger } from './logger.js';
|
|
|
26
26
|
import { callLLM } from './llm-provider.js';
|
|
27
27
|
import { APP_VERSION } from './version.js';
|
|
28
28
|
import { normalizeAllowedOrigins, normalizeHttpOrigin, verifySignedExecutionProgramEnvelope, } from './program-signing.js';
|
|
29
|
+
const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
|
|
29
30
|
const HEALER_SYSTEM_PROMPT = 'You repair failed deterministic browser opcodes. Respond only with JSON.';
|
|
30
31
|
// ── Main entry point ────────────────────────────────────────────────
|
|
31
32
|
export async function runCapture(options) {
|
|
@@ -91,10 +92,13 @@ export async function runCapture(options) {
|
|
|
91
92
|
credentials: program.preconditions.credentials,
|
|
92
93
|
});
|
|
93
94
|
// Step 4: Execute the program
|
|
95
|
+
const maxParallelVariants = program.mediaMode === 'clip'
|
|
96
|
+
? 1
|
|
97
|
+
: program.maxParallelCaptures;
|
|
94
98
|
const runOptions = {
|
|
95
99
|
recoveryChain,
|
|
96
100
|
abortSignal: options.abortSignal,
|
|
97
|
-
maxParallelVariants
|
|
101
|
+
maxParallelVariants,
|
|
98
102
|
llmConfig,
|
|
99
103
|
presetName: program.presetId,
|
|
100
104
|
onProgress: (event) => {
|
|
@@ -105,22 +109,34 @@ export async function runCapture(options) {
|
|
|
105
109
|
},
|
|
106
110
|
};
|
|
107
111
|
const captureStart = Date.now();
|
|
108
|
-
if (
|
|
109
|
-
logger.info(`[capture] Concurrency cap resolved to ${
|
|
112
|
+
if (maxParallelVariants) {
|
|
113
|
+
logger.info(`[capture] Concurrency cap resolved to ${maxParallelVariants} parallel variant(s)`);
|
|
114
|
+
if (program.mediaMode === 'clip' && program.maxParallelCaptures && program.maxParallelCaptures > 1) {
|
|
115
|
+
logger.info(`[capture] Clip capture concurrency capped at 1 ` +
|
|
116
|
+
`(requested ${program.maxParallelCaptures}) to avoid CI CPU contention`);
|
|
117
|
+
}
|
|
110
118
|
}
|
|
111
119
|
const createAdapter = async (variant) => {
|
|
120
|
+
const recordable = program.mediaMode === 'clip';
|
|
121
|
+
const requestedDeviceScaleFactor = variant.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
122
|
+
const runtimeDeviceScaleFactor = recordable && Number.isFinite(requestedDeviceScaleFactor)
|
|
123
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
124
|
+
: requestedDeviceScaleFactor;
|
|
112
125
|
const browserOptions = {
|
|
113
126
|
headed: options.headed ?? false,
|
|
114
127
|
viewport: variant.viewport,
|
|
115
|
-
deviceScaleFactor:
|
|
128
|
+
deviceScaleFactor: runtimeDeviceScaleFactor,
|
|
116
129
|
lang: variant.locale,
|
|
117
130
|
colorScheme: variant.theme,
|
|
118
131
|
storageState: program.preconditions.storageState,
|
|
119
132
|
};
|
|
120
|
-
const recordable = program.mediaMode === 'clip';
|
|
121
133
|
let recordingDir;
|
|
122
134
|
let browser;
|
|
123
135
|
logger.info(`[capture] Launching browser${browserOptions.headed ? ' (headed)' : ''}…`);
|
|
136
|
+
if (recordable && runtimeDeviceScaleFactor !== requestedDeviceScaleFactor) {
|
|
137
|
+
logger.info(`[capture] Clip capture scale capped at ${runtimeDeviceScaleFactor} ` +
|
|
138
|
+
`(requested ${requestedDeviceScaleFactor}) to preserve recording FPS`);
|
|
139
|
+
}
|
|
124
140
|
if (recordable) {
|
|
125
141
|
recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
|
|
126
142
|
browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'));
|
|
@@ -228,7 +244,10 @@ async function uploadResults(config, program, result) {
|
|
|
228
244
|
if (variantSpec?.deviceFrame) {
|
|
229
245
|
formData.append('deviceFrame', variantSpec.deviceFrame);
|
|
230
246
|
}
|
|
231
|
-
const
|
|
247
|
+
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
248
|
+
const deviceScaleFactor = artifact.mediaMode === 'clip' && Number.isFinite(requestedDeviceScaleFactor)
|
|
249
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
250
|
+
: requestedDeviceScaleFactor;
|
|
232
251
|
if (Number.isFinite(deviceScaleFactor)) {
|
|
233
252
|
formData.append('deviceScaleFactor', String(deviceScaleFactor));
|
|
234
253
|
}
|
|
@@ -24,6 +24,13 @@ export interface ClipCaptureLoopOptions {
|
|
|
24
24
|
* content (high-contrast text, flat colors) but unlocks a 33% fluidity gain.
|
|
25
25
|
*/
|
|
26
26
|
jpegQuality?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Maximum capture attempts per second. The loop also yields after every frame
|
|
29
|
+
* so Playwright input and page JS can make progress while a clip is recording.
|
|
30
|
+
*/
|
|
31
|
+
targetFps?: number;
|
|
32
|
+
/** Minimum rest after each CDP screenshot, even when capture is already slow. */
|
|
33
|
+
minRestMs?: number;
|
|
27
34
|
}
|
|
28
35
|
export interface ClipCaptureLoopResult {
|
|
29
36
|
framesDir: string;
|
|
@@ -46,6 +53,8 @@ export declare class ClipCaptureLoop {
|
|
|
46
53
|
private readonly page;
|
|
47
54
|
private readonly framesDir;
|
|
48
55
|
private readonly jpegQuality;
|
|
56
|
+
private readonly targetFrameIntervalMs;
|
|
57
|
+
private readonly minRestMs;
|
|
49
58
|
private cdp;
|
|
50
59
|
private running;
|
|
51
60
|
private loopPromise;
|
|
@@ -18,6 +18,8 @@ export class ClipCaptureLoop {
|
|
|
18
18
|
page;
|
|
19
19
|
framesDir;
|
|
20
20
|
jpegQuality;
|
|
21
|
+
targetFrameIntervalMs;
|
|
22
|
+
minRestMs;
|
|
21
23
|
cdp = null;
|
|
22
24
|
running = false;
|
|
23
25
|
loopPromise = null;
|
|
@@ -30,6 +32,9 @@ export class ClipCaptureLoop {
|
|
|
30
32
|
this.page = opts.page;
|
|
31
33
|
this.framesDir = opts.framesDir;
|
|
32
34
|
this.jpegQuality = opts.jpegQuality ?? 80;
|
|
35
|
+
const targetFps = Math.max(1, Math.min(30, opts.targetFps ?? (process.platform === 'linux' ? 8 : 15)));
|
|
36
|
+
this.targetFrameIntervalMs = 1000 / targetFps;
|
|
37
|
+
this.minRestMs = Math.max(0, Math.min(250, opts.minRestMs ?? (process.platform === 'linux' ? 50 : 16)));
|
|
33
38
|
}
|
|
34
39
|
async start() {
|
|
35
40
|
this.cdp = await this.page.context().newCDPSession(this.page);
|
|
@@ -80,6 +85,7 @@ export class ClipCaptureLoop {
|
|
|
80
85
|
while (this.running) {
|
|
81
86
|
if (!this.cdp)
|
|
82
87
|
return;
|
|
88
|
+
const frameStartedAt = performance.now();
|
|
83
89
|
let data;
|
|
84
90
|
try {
|
|
85
91
|
const r = await this.cdp.send('Page.captureScreenshot', {
|
|
@@ -105,6 +111,11 @@ export class ClipCaptureLoop {
|
|
|
105
111
|
// Decode+write happens in stop().
|
|
106
112
|
this.frames.push(data);
|
|
107
113
|
this.frameTimestamps.push(ts);
|
|
114
|
+
const elapsed = performance.now() - frameStartedAt;
|
|
115
|
+
const restMs = Math.max(this.minRestMs, this.targetFrameIntervalMs - elapsed);
|
|
116
|
+
if (restMs > 0 && this.running) {
|
|
117
|
+
await new Promise(resolve => setTimeout(resolve, restMs));
|
|
118
|
+
}
|
|
108
119
|
}
|
|
109
120
|
}
|
|
110
121
|
}
|
|
@@ -43,4 +43,7 @@ export declare function animatedHover(page: Page, target: {
|
|
|
43
43
|
* Type text into the currently focused element at a human-like typing speed.
|
|
44
44
|
* Assumes the field is already focused (via a preceding click).
|
|
45
45
|
*/
|
|
46
|
-
export declare function humanType(page: Page, text: string
|
|
46
|
+
export declare function humanType(page: Page, text: string, options?: {
|
|
47
|
+
minDelayMs?: number;
|
|
48
|
+
maxDelayMs?: number;
|
|
49
|
+
}): Promise<void>;
|
package/dist/mouse-animation.js
CHANGED
|
@@ -103,12 +103,16 @@ export async function animatedHover(page, target, fromCurrent, options = {}) {
|
|
|
103
103
|
* Type text into the currently focused element at a human-like typing speed.
|
|
104
104
|
* Assumes the field is already focused (via a preceding click).
|
|
105
105
|
*/
|
|
106
|
-
export async function humanType(page, text) {
|
|
106
|
+
export async function humanType(page, text, options = {}) {
|
|
107
|
+
const minDelay = Math.max(0, options.minDelayMs ?? 60);
|
|
108
|
+
const maxDelay = Math.max(minDelay, options.maxDelayMs ?? 140);
|
|
107
109
|
for (const char of text) {
|
|
108
110
|
await page.keyboard.type(char);
|
|
109
111
|
// 60–120 WPM → ~80–130ms between characters (5 chars per word)
|
|
110
|
-
const delay =
|
|
111
|
-
|
|
112
|
+
const delay = minDelay + Math.random() * (maxDelay - minDelay);
|
|
113
|
+
if (delay > 0) {
|
|
114
|
+
await page.waitForTimeout(delay);
|
|
115
|
+
}
|
|
112
116
|
}
|
|
113
117
|
}
|
|
114
118
|
//# sourceMappingURL=mouse-animation.js.map
|
|
@@ -278,7 +278,12 @@ export class WebPlaywrightLocal {
|
|
|
278
278
|
?? await fs.mkdtemp(path.join(os.tmpdir(), 'autokap-recording-'));
|
|
279
279
|
const framesDir = path.join(baseDir, 'frames');
|
|
280
280
|
await fs.mkdir(framesDir, { recursive: true });
|
|
281
|
-
const loop = new ClipCaptureLoop({
|
|
281
|
+
const loop = new ClipCaptureLoop({
|
|
282
|
+
page,
|
|
283
|
+
framesDir,
|
|
284
|
+
targetFps: process.platform === 'linux' ? 8 : 15,
|
|
285
|
+
minRestMs: process.platform === 'linux' ? 50 : 16,
|
|
286
|
+
});
|
|
282
287
|
await loop.start();
|
|
283
288
|
this.recording = {
|
|
284
289
|
mediaMode: options.mediaMode,
|
|
@@ -305,6 +310,8 @@ export class WebPlaywrightLocal {
|
|
|
305
310
|
};
|
|
306
311
|
}
|
|
307
312
|
const result = await this.recording.loop.stop();
|
|
313
|
+
logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
|
|
314
|
+
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s`);
|
|
308
315
|
await this.browser.closeContext();
|
|
309
316
|
await assembleMp4FromFrames({
|
|
310
317
|
framesDir: this.recording.framesDir,
|
|
@@ -649,7 +656,9 @@ export class WebPlaywrightLocal {
|
|
|
649
656
|
await page.keyboard.press('Control+A');
|
|
650
657
|
}
|
|
651
658
|
await page.waitForTimeout(70);
|
|
652
|
-
await humanType(page, text
|
|
659
|
+
await humanType(page, text, this.clipCursor
|
|
660
|
+
? { minDelayMs: 20, maxDelayMs: 45 }
|
|
661
|
+
: undefined);
|
|
653
662
|
}
|
|
654
663
|
async seedClipCursor() {
|
|
655
664
|
if (!this.clipCursor)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autokap",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "AI-powered CLI tool for capturing clean screenshots of websites",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
"autokap": "dist/cli.js"
|
|
180
180
|
},
|
|
181
181
|
"scripts": {
|
|
182
|
-
"postinstall": "node -e \"try{require('child_process').execSync('npx playwright install chromium',{stdio:'inherit'})}catch(e){console.warn('AutoKap: Playwright Chromium install failed. Run
|
|
182
|
+
"postinstall": "node -e \"try{require('child_process').execSync('npx playwright install chromium',{stdio:'inherit'})}catch(e){console.warn('AutoKap: Playwright Chromium install failed. Run npx playwright install chromium manually.',e.message||'')}\""
|
|
183
183
|
},
|
|
184
184
|
"engines": {
|
|
185
185
|
"node": ">=20"
|