autokap 1.1.2 → 1.1.3
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.js +164 -1
- package/dist/cli-runner.js +15 -3
- package/dist/web-playwright-local.js +2 -0
- package/package.json +2 -2
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);
|
|
@@ -600,6 +694,75 @@ export class Browser {
|
|
|
600
694
|
// Move cursor off-screen to avoid hover effects in screenshots
|
|
601
695
|
await page.mouse.move(0, 0);
|
|
602
696
|
await ensureCaptureHideStyles(page);
|
|
697
|
+
// Wait for web fonts to be loaded AND applied to the rendered page.
|
|
698
|
+
// next/font and other `font-display: swap` setups can report the
|
|
699
|
+
// FontFaceSet as "ready" while visible text is still painted with fallback.
|
|
700
|
+
// We force-load declared and currently computed families, verify the
|
|
701
|
+
// primary rendered families with document.fonts.check(), then wait for a
|
|
702
|
+
// committed repaint.
|
|
703
|
+
await page.evaluate(() => Promise.race([
|
|
704
|
+
(async () => {
|
|
705
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
706
|
+
const nextPaint = () => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
707
|
+
const normalizeFamily = (family) => family.trim().replace(/^['"]|['"]$/g, '');
|
|
708
|
+
const computedFamilies = () => {
|
|
709
|
+
const families = new Set();
|
|
710
|
+
const nodes = [
|
|
711
|
+
document.body,
|
|
712
|
+
...Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6,p,a,button,input,textarea,label,span,[data-ak]')).slice(0, 80),
|
|
713
|
+
].filter(Boolean);
|
|
714
|
+
for (const node of nodes) {
|
|
715
|
+
const stack = getComputedStyle(node).fontFamily;
|
|
716
|
+
for (const family of stack.split(',')) {
|
|
717
|
+
const normalized = normalizeFamily(family);
|
|
718
|
+
if (normalized
|
|
719
|
+
&& !/^(serif|sans-serif|monospace|system-ui|ui-sans-serif|ui-monospace)$/i.test(normalized)) {
|
|
720
|
+
families.add(normalized);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return families;
|
|
725
|
+
};
|
|
726
|
+
const allFamilies = () => new Set([
|
|
727
|
+
...Array.from(document.fonts).map((font) => font.family),
|
|
728
|
+
...computedFamilies(),
|
|
729
|
+
]);
|
|
730
|
+
const probe = document.createElement('div');
|
|
731
|
+
probe.setAttribute('aria-hidden', 'true');
|
|
732
|
+
probe.style.cssText = 'position:fixed;left:-9999px;top:-9999px;visibility:hidden;pointer-events:none;';
|
|
733
|
+
for (const family of allFamilies()) {
|
|
734
|
+
const span = document.createElement('span');
|
|
735
|
+
span.style.fontFamily = `"${family}"`;
|
|
736
|
+
span.textContent = 'AutoKap Aa 0123456789';
|
|
737
|
+
probe.appendChild(span);
|
|
738
|
+
}
|
|
739
|
+
document.body.appendChild(probe);
|
|
740
|
+
void probe.offsetHeight;
|
|
741
|
+
const sample = 'AutoKap Aa 0123456789';
|
|
742
|
+
await Promise.all([
|
|
743
|
+
...Array.from(document.fonts).map((font) => font.load().catch(() => null)),
|
|
744
|
+
...Array.from(allFamilies()).flatMap((family) => [
|
|
745
|
+
document.fonts.load(`400 16px "${family}"`, sample).catch(() => null),
|
|
746
|
+
document.fonts.load(`600 16px "${family}"`, sample).catch(() => null),
|
|
747
|
+
]),
|
|
748
|
+
]);
|
|
749
|
+
await document.fonts.ready;
|
|
750
|
+
const deadline = performance.now() + 7000;
|
|
751
|
+
while (performance.now() < deadline) {
|
|
752
|
+
const renderedFamilies = Array.from(computedFamilies()).filter((family) => !/\bFallback\b/i.test(family));
|
|
753
|
+
const loaded = renderedFamilies.length === 0
|
|
754
|
+
|| renderedFamilies.every((family) => document.fonts.check(`400 16px "${family}"`, sample)
|
|
755
|
+
|| document.fonts.check(`600 16px "${family}"`, sample));
|
|
756
|
+
if (document.fonts.status === 'loaded' && loaded)
|
|
757
|
+
break;
|
|
758
|
+
await sleep(100);
|
|
759
|
+
}
|
|
760
|
+
probe.remove();
|
|
761
|
+
await nextPaint();
|
|
762
|
+
})(),
|
|
763
|
+
new Promise((resolve) => setTimeout(resolve, 8000)),
|
|
764
|
+
])).catch(() => { });
|
|
765
|
+
await logFontDiagnostics(page, 'before screenshot');
|
|
603
766
|
return Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
|
|
604
767
|
}
|
|
605
768
|
async takeScreenshotForAI(options = {}) {
|
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) {
|
|
@@ -109,18 +110,26 @@ export async function runCapture(options) {
|
|
|
109
110
|
logger.info(`[capture] Concurrency cap resolved to ${program.maxParallelCaptures} parallel variant(s)`);
|
|
110
111
|
}
|
|
111
112
|
const createAdapter = async (variant) => {
|
|
113
|
+
const recordable = program.mediaMode === 'clip';
|
|
114
|
+
const requestedDeviceScaleFactor = variant.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
115
|
+
const runtimeDeviceScaleFactor = recordable && Number.isFinite(requestedDeviceScaleFactor)
|
|
116
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
117
|
+
: requestedDeviceScaleFactor;
|
|
112
118
|
const browserOptions = {
|
|
113
119
|
headed: options.headed ?? false,
|
|
114
120
|
viewport: variant.viewport,
|
|
115
|
-
deviceScaleFactor:
|
|
121
|
+
deviceScaleFactor: runtimeDeviceScaleFactor,
|
|
116
122
|
lang: variant.locale,
|
|
117
123
|
colorScheme: variant.theme,
|
|
118
124
|
storageState: program.preconditions.storageState,
|
|
119
125
|
};
|
|
120
|
-
const recordable = program.mediaMode === 'clip';
|
|
121
126
|
let recordingDir;
|
|
122
127
|
let browser;
|
|
123
128
|
logger.info(`[capture] Launching browser${browserOptions.headed ? ' (headed)' : ''}…`);
|
|
129
|
+
if (recordable && runtimeDeviceScaleFactor !== requestedDeviceScaleFactor) {
|
|
130
|
+
logger.info(`[capture] Clip capture scale capped at ${runtimeDeviceScaleFactor} ` +
|
|
131
|
+
`(requested ${requestedDeviceScaleFactor}) to preserve recording FPS`);
|
|
132
|
+
}
|
|
124
133
|
if (recordable) {
|
|
125
134
|
recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
|
|
126
135
|
browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'));
|
|
@@ -228,7 +237,10 @@ async function uploadResults(config, program, result) {
|
|
|
228
237
|
if (variantSpec?.deviceFrame) {
|
|
229
238
|
formData.append('deviceFrame', variantSpec.deviceFrame);
|
|
230
239
|
}
|
|
231
|
-
const
|
|
240
|
+
const requestedDeviceScaleFactor = variantSpec?.deviceScaleFactor ?? program.outputScale ?? 2;
|
|
241
|
+
const deviceScaleFactor = artifact.mediaMode === 'clip' && Number.isFinite(requestedDeviceScaleFactor)
|
|
242
|
+
? Math.min(Number(requestedDeviceScaleFactor), MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR)
|
|
243
|
+
: requestedDeviceScaleFactor;
|
|
232
244
|
if (Number.isFinite(deviceScaleFactor)) {
|
|
233
245
|
formData.append('deviceScaleFactor', String(deviceScaleFactor));
|
|
234
246
|
}
|
|
@@ -305,6 +305,8 @@ export class WebPlaywrightLocal {
|
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
307
|
const result = await this.recording.loop.stop();
|
|
308
|
+
logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
|
|
309
|
+
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s`);
|
|
308
310
|
await this.browser.closeContext();
|
|
309
311
|
await assembleMp4FromFrames({
|
|
310
312
|
framesDir: this.recording.framesDir,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autokap",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
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"
|