autokap 1.1.3 → 1.1.5
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 +16 -5
- package/dist/cli-runner.js +80 -18
- package/dist/cli.js +0 -0
- package/dist/clip-capture-loop.d.ts +21 -0
- package/dist/clip-capture-loop.js +37 -0
- package/dist/mouse-animation.d.ts +4 -1
- package/dist/mouse-animation.js +7 -3
- package/dist/web-playwright-local.js +13 -3
- package/package.json +1 -1
- package/dist/agent-action-recovery.d.ts +0 -45
- package/dist/agent-action-recovery.js +0 -370
- package/dist/agent-message-utils.d.ts +0 -21
- package/dist/agent-message-utils.js +0 -77
- package/dist/agent-url-utils.d.ts +0 -30
- package/dist/agent-url-utils.js +0 -138
- package/dist/agent.d.ts +0 -226
- package/dist/agent.js +0 -6666
- package/dist/browser-utils.d.ts +0 -31
- package/dist/browser-utils.js +0 -97
- package/dist/capture-studio-sync.d.ts +0 -23
- package/dist/capture-studio-sync.js +0 -172
- package/dist/clip-begin-frame-recorder.d.ts +0 -44
- package/dist/clip-begin-frame-recorder.js +0 -250
- package/dist/clip-capture-backend.d.ts +0 -25
- package/dist/clip-capture-backend.js +0 -189
- package/dist/clip-frame-recorder.d.ts +0 -63
- package/dist/clip-frame-recorder.js +0 -305
- package/dist/clip-orchestrator.d.ts +0 -148
- package/dist/clip-orchestrator.js +0 -957
- package/dist/clip-runtime.d.ts +0 -18
- package/dist/clip-runtime.js +0 -67
- package/dist/clip-scale.d.ts +0 -10
- package/dist/clip-scale.js +0 -21
- package/dist/clip-screencast-recorder.d.ts +0 -42
- package/dist/clip-screencast-recorder.js +0 -242
- package/dist/clip-sidecar.d.ts +0 -54
- package/dist/clip-sidecar.js +0 -208
- package/dist/cost-resolution-monitor.d.ts +0 -16
- package/dist/cost-resolution-monitor.js +0 -34
- package/dist/credential-templates.d.ts +0 -5
- package/dist/credential-templates.js +0 -60
- package/dist/dom-css-purger.d.ts +0 -65
- package/dist/dom-css-purger.js +0 -333
- package/dist/dom-font-inliner.d.ts +0 -45
- package/dist/dom-font-inliner.js +0 -148
- package/dist/dom-patch-resolver.d.ts +0 -52
- package/dist/dom-patch-resolver.js +0 -242
- package/dist/dom-serializer.d.ts +0 -82
- package/dist/dom-serializer.js +0 -378
- package/dist/element-capture.d.ts +0 -13
- package/dist/element-capture.js +0 -522
- package/dist/fonts-loader.d.ts +0 -14
- package/dist/fonts-loader.js +0 -55
- package/dist/hybrid-navigator.d.ts +0 -138
- package/dist/hybrid-navigator.js +0 -468
- package/dist/legacy/agent-action-recovery.d.ts +0 -45
- package/dist/legacy/agent-action-recovery.js +0 -370
- package/dist/legacy/agent-message-utils.d.ts +0 -21
- package/dist/legacy/agent-message-utils.js +0 -77
- package/dist/legacy/agent-url-utils.d.ts +0 -30
- package/dist/legacy/agent-url-utils.js +0 -138
- package/dist/legacy/agent.d.ts +0 -226
- package/dist/legacy/agent.js +0 -6666
- package/dist/legacy/clip-orchestrator.d.ts +0 -148
- package/dist/legacy/clip-orchestrator.js +0 -957
- package/dist/legacy/credential-templates.d.ts +0 -5
- package/dist/legacy/credential-templates.js +0 -60
- package/dist/legacy/hybrid-navigator.d.ts +0 -138
- package/dist/legacy/hybrid-navigator.js +0 -468
- package/dist/legacy/llm-usage.d.ts +0 -17
- package/dist/legacy/llm-usage.js +0 -45
- package/dist/legacy/prompt-cache.d.ts +0 -10
- package/dist/legacy/prompt-cache.js +0 -24
- package/dist/legacy/prompts.d.ts +0 -175
- package/dist/legacy/prompts.js +0 -1038
- package/dist/legacy/tools.d.ts +0 -4
- package/dist/legacy/tools.js +0 -216
- package/dist/legacy/video-agent.d.ts +0 -143
- package/dist/legacy/video-agent.js +0 -4788
- package/dist/legacy/video-observation.d.ts +0 -36
- package/dist/legacy/video-observation.js +0 -192
- package/dist/legacy/video-planner.d.ts +0 -12
- package/dist/legacy/video-planner.js +0 -501
- package/dist/legacy/video-prompts.d.ts +0 -37
- package/dist/legacy/video-prompts.js +0 -569
- package/dist/legacy/video-tools.d.ts +0 -3
- package/dist/legacy/video-tools.js +0 -59
- package/dist/legacy/video-variant-state.d.ts +0 -29
- package/dist/legacy/video-variant-state.js +0 -80
- package/dist/legacy/vision-model.d.ts +0 -17
- package/dist/legacy/vision-model.js +0 -74
- package/dist/llm-usage.d.ts +0 -17
- package/dist/llm-usage.js +0 -45
- package/dist/overlay-utils.d.ts +0 -14
- package/dist/overlay-utils.js +0 -13
- package/dist/prompt-cache.d.ts +0 -10
- package/dist/prompt-cache.js +0 -24
- package/dist/prompts.d.ts +0 -175
- package/dist/prompts.js +0 -1038
- package/dist/remote-browser.d.ts +0 -215
- package/dist/remote-browser.js +0 -360
- package/dist/svg-browser-bar.d.ts +0 -33
- package/dist/svg-browser-bar.js +0 -206
- package/dist/svg-status-bar.d.ts +0 -36
- package/dist/svg-status-bar.js +0 -597
- package/dist/svg-text.d.ts +0 -61
- package/dist/svg-text.js +0 -118
- package/dist/tools.d.ts +0 -4
- package/dist/tools.js +0 -216
- package/dist/v2/action-verifier.d.ts +0 -29
- package/dist/v2/action-verifier.js +0 -133
- package/dist/v2/alt-text.d.ts +0 -26
- package/dist/v2/alt-text.js +0 -55
- package/dist/v2/benchmark.d.ts +0 -59
- package/dist/v2/benchmark.js +0 -135
- package/dist/v2/capture-strategy.d.ts +0 -30
- package/dist/v2/capture-strategy.js +0 -67
- package/dist/v2/capture-verification.d.ts +0 -35
- package/dist/v2/capture-verification.js +0 -95
- package/dist/v2/circuit-breaker.d.ts +0 -42
- package/dist/v2/circuit-breaker.js +0 -119
- package/dist/v2/cli-runner-local.d.ts +0 -11
- package/dist/v2/cli-runner-local.js +0 -91
- package/dist/v2/cli-runner.d.ts +0 -34
- package/dist/v2/cli-runner.js +0 -300
- package/dist/v2/compiler-prompts.d.ts +0 -27
- package/dist/v2/compiler-prompts.js +0 -123
- package/dist/v2/compiler.d.ts +0 -37
- package/dist/v2/compiler.js +0 -147
- package/dist/v2/explorer.d.ts +0 -41
- package/dist/v2/explorer.js +0 -56
- package/dist/v2/index.d.ts +0 -37
- package/dist/v2/index.js +0 -31
- package/dist/v2/llm-healer.d.ts +0 -62
- package/dist/v2/llm-healer.js +0 -166
- package/dist/v2/llm-provider.d.ts +0 -29
- package/dist/v2/llm-provider.js +0 -80
- package/dist/v2/opcode-runner.d.ts +0 -47
- package/dist/v2/opcode-runner.js +0 -634
- package/dist/v2/overlay-engine.d.ts +0 -24
- package/dist/v2/overlay-engine.js +0 -150
- package/dist/v2/postcondition.d.ts +0 -16
- package/dist/v2/postcondition.js +0 -249
- package/dist/v2/program-patcher.d.ts +0 -25
- package/dist/v2/program-patcher.js +0 -44
- package/dist/v2/recovery-chain.d.ts +0 -30
- package/dist/v2/recovery-chain.js +0 -368
- package/dist/v2/schema.d.ts +0 -2580
- package/dist/v2/schema.js +0 -295
- package/dist/v2/selector-resolver.d.ts +0 -34
- package/dist/v2/selector-resolver.js +0 -181
- package/dist/v2/semantic-resolver.d.ts +0 -35
- package/dist/v2/semantic-resolver.js +0 -161
- package/dist/v2/smart-wait.d.ts +0 -27
- package/dist/v2/smart-wait.js +0 -81
- package/dist/v2/types.d.ts +0 -444
- package/dist/v2/types.js +0 -19
- package/dist/v2/web-playwright-local.d.ts +0 -69
- package/dist/v2/web-playwright-local.js +0 -392
- package/dist/video-agent.d.ts +0 -143
- package/dist/video-agent.js +0 -4788
- package/dist/video-observation.d.ts +0 -36
- package/dist/video-observation.js +0 -192
- package/dist/video-planner.d.ts +0 -12
- package/dist/video-planner.js +0 -501
- package/dist/video-prompts.d.ts +0 -37
- package/dist/video-prompts.js +0 -554
- package/dist/video-tools.d.ts +0 -3
- package/dist/video-tools.js +0 -59
- package/dist/video-variant-state.d.ts +0 -29
- package/dist/video-variant-state.js +0 -80
- package/dist/vision-model.d.ts +0 -17
- package/dist/vision-model.js +0 -74
- package/dist/ws-auth.d.ts +0 -20
- package/dist/ws-auth.js +0 -70
- package/dist/ws-broadcast.d.ts +0 -34
- package/dist/ws-broadcast.js +0 -85
- package/dist/ws-connection-limits.d.ts +0 -12
- package/dist/ws-connection-limits.js +0 -44
- package/dist/ws-handler-utils.d.ts +0 -32
- package/dist/ws-handler-utils.js +0 -139
- package/dist/ws-handler.d.ts +0 -10
- package/dist/ws-handler.js +0 -1793
- package/dist/ws-metrics-server.d.ts +0 -9
- package/dist/ws-metrics-server.js +0 -31
- package/dist/ws-server.d.ts +0 -9
- package/dist/ws-server.js +0 -92
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
|
@@ -689,11 +689,7 @@ export class Browser {
|
|
|
689
689
|
// Page may have navigated during wait
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
|
-
async
|
|
693
|
-
const page = this.ensurePage();
|
|
694
|
-
// Move cursor off-screen to avoid hover effects in screenshots
|
|
695
|
-
await page.mouse.move(0, 0);
|
|
696
|
-
await ensureCaptureHideStyles(page);
|
|
692
|
+
async waitForFontsBeforeScreenshot(page) {
|
|
697
693
|
// Wait for web fonts to be loaded AND applied to the rendered page.
|
|
698
694
|
// next/font and other `font-display: swap` setups can report the
|
|
699
695
|
// FontFaceSet as "ready" while visible text is still painted with fallback.
|
|
@@ -762,6 +758,13 @@ export class Browser {
|
|
|
762
758
|
})(),
|
|
763
759
|
new Promise((resolve) => setTimeout(resolve, 8000)),
|
|
764
760
|
])).catch(() => { });
|
|
761
|
+
}
|
|
762
|
+
async takeScreenshot() {
|
|
763
|
+
const page = this.ensurePage();
|
|
764
|
+
// Move cursor off-screen to avoid hover effects in screenshots
|
|
765
|
+
await page.mouse.move(0, 0);
|
|
766
|
+
await ensureCaptureHideStyles(page);
|
|
767
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
765
768
|
await logFontDiagnostics(page, 'before screenshot');
|
|
766
769
|
return Buffer.from(await page.screenshot({ type: 'png', fullPage: false }));
|
|
767
770
|
}
|
|
@@ -4135,6 +4138,8 @@ export class Browser {
|
|
|
4135
4138
|
if (clip.width <= 0 || clip.height <= 0) {
|
|
4136
4139
|
throw new Error(`Element index ${index} is still outside the viewport after alignment`);
|
|
4137
4140
|
}
|
|
4141
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4142
|
+
await logFontDiagnostics(page, 'before element screenshot');
|
|
4138
4143
|
return Buffer.from(await page.screenshot({ type: 'png', clip }));
|
|
4139
4144
|
}
|
|
4140
4145
|
finally {
|
|
@@ -4160,6 +4165,8 @@ export class Browser {
|
|
|
4160
4165
|
// Hide fixed/sticky overlays (navbars, banners) that could cover the region
|
|
4161
4166
|
await this.hideFixedOverlays();
|
|
4162
4167
|
try {
|
|
4168
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4169
|
+
await logFontDiagnostics(page, 'before region screenshot');
|
|
4163
4170
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4164
4171
|
const image = sharp(fullPage);
|
|
4165
4172
|
const meta = await image.metadata();
|
|
@@ -4281,6 +4288,8 @@ export class Browser {
|
|
|
4281
4288
|
await this.hideFixedOverlays();
|
|
4282
4289
|
try {
|
|
4283
4290
|
const dpr = pageInfo.dpr;
|
|
4291
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4292
|
+
await logFontDiagnostics(page, 'before selector screenshot');
|
|
4284
4293
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4285
4294
|
const image = sharp(fullPage);
|
|
4286
4295
|
const meta = await image.metadata();
|
|
@@ -4343,6 +4352,8 @@ export class Browser {
|
|
|
4343
4352
|
await this.hideFixedOverlays();
|
|
4344
4353
|
try {
|
|
4345
4354
|
const dpr = pageInfo.dpr;
|
|
4355
|
+
await this.waitForFontsBeforeScreenshot(page);
|
|
4356
|
+
await logFontDiagnostics(page, 'before bounding-region screenshot');
|
|
4346
4357
|
const fullPage = Buffer.from(await page.screenshot({ type: 'png', fullPage: true }));
|
|
4347
4358
|
const image = sharp(fullPage);
|
|
4348
4359
|
const meta = await image.metadata();
|
package/dist/cli-runner.js
CHANGED
|
@@ -27,6 +27,8 @@ 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
29
|
const MAX_CLIP_CAPTURE_DEVICE_SCALE_FACTOR = 1;
|
|
30
|
+
const FETCH_PROGRAM_MAX_ATTEMPTS = 4;
|
|
31
|
+
const FETCH_PROGRAM_RETRY_DELAYS_MS = [1000, 3000, 5000];
|
|
30
32
|
const HEALER_SYSTEM_PROMPT = 'You repair failed deterministic browser opcodes. Respond only with JSON.';
|
|
31
33
|
// ── Main entry point ────────────────────────────────────────────────
|
|
32
34
|
export async function runCapture(options) {
|
|
@@ -92,10 +94,13 @@ export async function runCapture(options) {
|
|
|
92
94
|
credentials: program.preconditions.credentials,
|
|
93
95
|
});
|
|
94
96
|
// Step 4: Execute the program
|
|
97
|
+
const maxParallelVariants = program.mediaMode === 'clip'
|
|
98
|
+
? 1
|
|
99
|
+
: program.maxParallelCaptures;
|
|
95
100
|
const runOptions = {
|
|
96
101
|
recoveryChain,
|
|
97
102
|
abortSignal: options.abortSignal,
|
|
98
|
-
maxParallelVariants
|
|
103
|
+
maxParallelVariants,
|
|
99
104
|
llmConfig,
|
|
100
105
|
presetName: program.presetId,
|
|
101
106
|
onProgress: (event) => {
|
|
@@ -106,8 +111,12 @@ export async function runCapture(options) {
|
|
|
106
111
|
},
|
|
107
112
|
};
|
|
108
113
|
const captureStart = Date.now();
|
|
109
|
-
if (
|
|
110
|
-
logger.info(`[capture] Concurrency cap resolved to ${
|
|
114
|
+
if (maxParallelVariants) {
|
|
115
|
+
logger.info(`[capture] Concurrency cap resolved to ${maxParallelVariants} parallel variant(s)`);
|
|
116
|
+
if (program.mediaMode === 'clip' && program.maxParallelCaptures && program.maxParallelCaptures > 1) {
|
|
117
|
+
logger.info(`[capture] Clip capture concurrency capped at 1 ` +
|
|
118
|
+
`(requested ${program.maxParallelCaptures}) to avoid CI CPU contention`);
|
|
119
|
+
}
|
|
111
120
|
}
|
|
112
121
|
const createAdapter = async (variant) => {
|
|
113
122
|
const recordable = program.mediaMode === 'clip';
|
|
@@ -177,22 +186,55 @@ export async function runCapture(options) {
|
|
|
177
186
|
}
|
|
178
187
|
// ── Server communication ────────────────────────────────────────────
|
|
179
188
|
async function fetchProgram(config, presetId, environmentName) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
189
|
+
const url = new URL(`${config.apiBaseUrl}/api/cli/programs/${presetId}`);
|
|
190
|
+
if (environmentName) {
|
|
191
|
+
url.searchParams.set('env', environmentName);
|
|
192
|
+
}
|
|
193
|
+
const requestedUrl = url.toString();
|
|
194
|
+
for (let attempt = 1; attempt <= FETCH_PROGRAM_MAX_ATTEMPTS; attempt += 1) {
|
|
195
|
+
let response;
|
|
196
|
+
try {
|
|
197
|
+
response = await fetch(requestedUrl, {
|
|
198
|
+
headers: {
|
|
199
|
+
'Authorization': `Bearer ${config.apiKey}`,
|
|
200
|
+
'Content-Type': 'application/json',
|
|
201
|
+
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
const error = `failed to fetch program: ${err instanceof Error ? err.message : String(err)}`;
|
|
207
|
+
if (attempt < FETCH_PROGRAM_MAX_ATTEMPTS) {
|
|
208
|
+
await retryProgramFetch(attempt, error);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
return { success: false, error };
|
|
184
212
|
}
|
|
185
|
-
const response = await fetch(url.toString(), {
|
|
186
|
-
headers: {
|
|
187
|
-
'Authorization': `Bearer ${config.apiKey}`,
|
|
188
|
-
'Content-Type': 'application/json',
|
|
189
|
-
[CLI_VERSION_HEADER]: APP_VERSION,
|
|
190
|
-
},
|
|
191
|
-
});
|
|
192
213
|
if (!response.ok) {
|
|
193
|
-
|
|
214
|
+
const error = await formatServerError(response, requestedUrl);
|
|
215
|
+
if (shouldRetryProgramFetch(response.status, error) && attempt < FETCH_PROGRAM_MAX_ATTEMPTS) {
|
|
216
|
+
await retryProgramFetch(attempt, error);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
return { success: false, error };
|
|
220
|
+
}
|
|
221
|
+
const contentType = response.headers?.get?.('content-type') ?? '';
|
|
222
|
+
let data;
|
|
223
|
+
try {
|
|
224
|
+
if (contentType.includes('text/html')) {
|
|
225
|
+
const htmlPreview = (await response.text()).replace(/\s+/g, ' ').trim().slice(0, 120);
|
|
226
|
+
throw new Error(`unexpected HTML response from ${requestedUrl}${htmlPreview ? `: ${htmlPreview}` : ''}`);
|
|
227
|
+
}
|
|
228
|
+
data = await response.json();
|
|
229
|
+
}
|
|
230
|
+
catch (err) {
|
|
231
|
+
const error = `failed to fetch program: ${err instanceof Error ? err.message : String(err)}`;
|
|
232
|
+
if (attempt < FETCH_PROGRAM_MAX_ATTEMPTS) {
|
|
233
|
+
await retryProgramFetch(attempt, error);
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
return { success: false, error };
|
|
194
237
|
}
|
|
195
|
-
const data = await response.json();
|
|
196
238
|
const envelope = verifySignedExecutionProgramEnvelope({
|
|
197
239
|
apiKey: config.apiKey,
|
|
198
240
|
envelope: data,
|
|
@@ -203,9 +245,29 @@ async function fetchProgram(config, presetId, environmentName) {
|
|
|
203
245
|
}
|
|
204
246
|
return { success: true, program: envelope.program, security: envelope.security };
|
|
205
247
|
}
|
|
206
|
-
|
|
207
|
-
|
|
248
|
+
return { success: false, error: 'failed to fetch program: retry attempts exhausted' };
|
|
249
|
+
}
|
|
250
|
+
function shouldRetryProgramFetch(status, error) {
|
|
251
|
+
if ([408, 429, 500, 502, 503, 504].includes(status)) {
|
|
252
|
+
return true;
|
|
208
253
|
}
|
|
254
|
+
return status >= 500 && error.includes('unexpected HTML response');
|
|
255
|
+
}
|
|
256
|
+
async function retryProgramFetch(attempt, error) {
|
|
257
|
+
const delayMs = getProgramFetchRetryDelayMs(attempt);
|
|
258
|
+
logger.warn(`[capture] Program fetch failed (attempt ${attempt}/${FETCH_PROGRAM_MAX_ATTEMPTS}): ${error}. Retrying in ${delayMs}ms...`);
|
|
259
|
+
if (delayMs > 0) {
|
|
260
|
+
await sleep(delayMs);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function getProgramFetchRetryDelayMs(attempt) {
|
|
264
|
+
if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true') {
|
|
265
|
+
return 0;
|
|
266
|
+
}
|
|
267
|
+
return FETCH_PROGRAM_RETRY_DELAYS_MS[Math.max(0, attempt - 1)] ?? FETCH_PROGRAM_RETRY_DELAYS_MS.at(-1) ?? 0;
|
|
268
|
+
}
|
|
269
|
+
function sleep(ms) {
|
|
270
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
209
271
|
}
|
|
210
272
|
async function uploadResults(config, program, result) {
|
|
211
273
|
const runId = randomUUID();
|
package/dist/cli.js
CHANGED
|
File without changes
|
|
@@ -24,10 +24,21 @@ 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;
|
|
30
37
|
frameCount: number;
|
|
38
|
+
/** Configured capture attempt ceiling. Actual FPS may be lower when CDP is slow. */
|
|
39
|
+
targetFps: number;
|
|
40
|
+
/** Minimum idle time yielded after each CDP screenshot. */
|
|
41
|
+
minRestMs: number;
|
|
31
42
|
/** (frameCount - 1) * 1000 / (lastTs - firstTs); 0 if < 2 frames. */
|
|
32
43
|
measuredFps: number;
|
|
33
44
|
actualDurationMs: number;
|
|
@@ -41,16 +52,26 @@ export interface ClipCaptureLoopResult {
|
|
|
41
52
|
* loop keeps trying, producing bursts and gaps).
|
|
42
53
|
*/
|
|
43
54
|
frameOffsetsMs: number[];
|
|
55
|
+
/** CDP Page.captureScreenshot wall time in milliseconds. */
|
|
56
|
+
captureTimingMs: {
|
|
57
|
+
p50: number;
|
|
58
|
+
p95: number;
|
|
59
|
+
max: number;
|
|
60
|
+
};
|
|
44
61
|
}
|
|
45
62
|
export declare class ClipCaptureLoop {
|
|
46
63
|
private readonly page;
|
|
47
64
|
private readonly framesDir;
|
|
48
65
|
private readonly jpegQuality;
|
|
66
|
+
private readonly targetFps;
|
|
67
|
+
private readonly targetFrameIntervalMs;
|
|
68
|
+
private readonly minRestMs;
|
|
49
69
|
private cdp;
|
|
50
70
|
private running;
|
|
51
71
|
private loopPromise;
|
|
52
72
|
private frames;
|
|
53
73
|
private frameTimestamps;
|
|
74
|
+
private frameCaptureDurationsMs;
|
|
54
75
|
private startedAt;
|
|
55
76
|
private firstFrameAt;
|
|
56
77
|
private lastFrameAt;
|
|
@@ -18,11 +18,15 @@ export class ClipCaptureLoop {
|
|
|
18
18
|
page;
|
|
19
19
|
framesDir;
|
|
20
20
|
jpegQuality;
|
|
21
|
+
targetFps;
|
|
22
|
+
targetFrameIntervalMs;
|
|
23
|
+
minRestMs;
|
|
21
24
|
cdp = null;
|
|
22
25
|
running = false;
|
|
23
26
|
loopPromise = null;
|
|
24
27
|
frames = [];
|
|
25
28
|
frameTimestamps = [];
|
|
29
|
+
frameCaptureDurationsMs = [];
|
|
26
30
|
startedAt = 0;
|
|
27
31
|
firstFrameAt = 0;
|
|
28
32
|
lastFrameAt = 0;
|
|
@@ -30,6 +34,10 @@ export class ClipCaptureLoop {
|
|
|
30
34
|
this.page = opts.page;
|
|
31
35
|
this.framesDir = opts.framesDir;
|
|
32
36
|
this.jpegQuality = opts.jpegQuality ?? 80;
|
|
37
|
+
const targetFps = Math.max(1, Math.min(30, opts.targetFps ?? (process.platform === 'linux' ? 8 : 15)));
|
|
38
|
+
this.targetFps = targetFps;
|
|
39
|
+
this.targetFrameIntervalMs = 1000 / targetFps;
|
|
40
|
+
this.minRestMs = Math.max(0, Math.min(250, opts.minRestMs ?? (process.platform === 'linux' ? 50 : 16)));
|
|
33
41
|
}
|
|
34
42
|
async start() {
|
|
35
43
|
this.cdp = await this.page.context().newCDPSession(this.page);
|
|
@@ -64,22 +72,28 @@ export class ClipCaptureLoop {
|
|
|
64
72
|
const trimStartMs = this.firstFrameAt > 0 ? Math.max(0, this.firstFrameAt - this.startedAt) : 0;
|
|
65
73
|
// Snapshot offsets (ms from first frame) for VFR encoding downstream.
|
|
66
74
|
const frameOffsetsMs = this.frameTimestamps.map(ts => ts - this.firstFrameAt);
|
|
75
|
+
const captureTimingMs = summarizeTiming(this.frameCaptureDurationsMs);
|
|
67
76
|
// Release memory — the caller owns framesDir from here on.
|
|
68
77
|
this.frames = [];
|
|
69
78
|
this.frameTimestamps = [];
|
|
79
|
+
this.frameCaptureDurationsMs = [];
|
|
70
80
|
return {
|
|
71
81
|
framesDir: this.framesDir,
|
|
72
82
|
frameCount,
|
|
83
|
+
targetFps: this.targetFps,
|
|
84
|
+
minRestMs: this.minRestMs,
|
|
73
85
|
measuredFps,
|
|
74
86
|
actualDurationMs,
|
|
75
87
|
trimStartMs,
|
|
76
88
|
frameOffsetsMs,
|
|
89
|
+
captureTimingMs,
|
|
77
90
|
};
|
|
78
91
|
}
|
|
79
92
|
async loop() {
|
|
80
93
|
while (this.running) {
|
|
81
94
|
if (!this.cdp)
|
|
82
95
|
return;
|
|
96
|
+
const frameStartedAt = performance.now();
|
|
83
97
|
let data;
|
|
84
98
|
try {
|
|
85
99
|
const r = await this.cdp.send('Page.captureScreenshot', {
|
|
@@ -105,7 +119,30 @@ export class ClipCaptureLoop {
|
|
|
105
119
|
// Decode+write happens in stop().
|
|
106
120
|
this.frames.push(data);
|
|
107
121
|
this.frameTimestamps.push(ts);
|
|
122
|
+
const elapsed = performance.now() - frameStartedAt;
|
|
123
|
+
this.frameCaptureDurationsMs.push(elapsed);
|
|
124
|
+
const restMs = Math.max(this.minRestMs, this.targetFrameIntervalMs - elapsed);
|
|
125
|
+
if (restMs > 0 && this.running) {
|
|
126
|
+
await new Promise(resolve => setTimeout(resolve, restMs));
|
|
127
|
+
}
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
}
|
|
131
|
+
function summarizeTiming(values) {
|
|
132
|
+
if (values.length === 0)
|
|
133
|
+
return { p50: 0, p95: 0, max: 0 };
|
|
134
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
135
|
+
return {
|
|
136
|
+
p50: roundMs(percentile(sorted, 0.5)),
|
|
137
|
+
p95: roundMs(percentile(sorted, 0.95)),
|
|
138
|
+
max: roundMs(sorted[sorted.length - 1] ?? 0),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function percentile(sorted, ratio) {
|
|
142
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * ratio) - 1));
|
|
143
|
+
return sorted[index] ?? 0;
|
|
144
|
+
}
|
|
145
|
+
function roundMs(value) {
|
|
146
|
+
return Math.round(value * 10) / 10;
|
|
147
|
+
}
|
|
111
148
|
//# sourceMappingURL=clip-capture-loop.js.map
|
|
@@ -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,
|
|
@@ -306,7 +311,10 @@ export class WebPlaywrightLocal {
|
|
|
306
311
|
}
|
|
307
312
|
const result = await this.recording.loop.stop();
|
|
308
313
|
logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
|
|
309
|
-
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s`
|
|
314
|
+
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s ` +
|
|
315
|
+
`(target ${result.targetFps} fps, rest >= ${result.minRestMs}ms; ` +
|
|
316
|
+
`CDP frame time p50=${result.captureTimingMs.p50}ms ` +
|
|
317
|
+
`p95=${result.captureTimingMs.p95}ms max=${result.captureTimingMs.max}ms)`);
|
|
310
318
|
await this.browser.closeContext();
|
|
311
319
|
await assembleMp4FromFrames({
|
|
312
320
|
framesDir: this.recording.framesDir,
|
|
@@ -651,7 +659,9 @@ export class WebPlaywrightLocal {
|
|
|
651
659
|
await page.keyboard.press('Control+A');
|
|
652
660
|
}
|
|
653
661
|
await page.waitForTimeout(70);
|
|
654
|
-
await humanType(page, text
|
|
662
|
+
await humanType(page, text, this.clipCursor
|
|
663
|
+
? { minDelayMs: 20, maxDelayMs: 45 }
|
|
664
|
+
: undefined);
|
|
655
665
|
}
|
|
656
666
|
async seedClipCursor() {
|
|
657
667
|
if (!this.clipCursor)
|
package/package.json
CHANGED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Action recovery, guard inference, and login detection utilities for the capture agent.
|
|
3
|
-
* Extracted from agent.ts — these are pure functions with no closure dependencies.
|
|
4
|
-
*/
|
|
5
|
-
import type { ExecutedAction, ActionType } from './types.js';
|
|
6
|
-
export declare const META_ACTIONS: Set<ActionType>;
|
|
7
|
-
export declare const BOOTSTRAP_ACTIONS: Set<ActionType>;
|
|
8
|
-
export declare const REPLAYABLE_ACTIONS: ReadonlyArray<ActionType>;
|
|
9
|
-
export declare function isNoEffectAction(action: ExecutedAction): boolean;
|
|
10
|
-
export declare function isBootstrapStabilizationAction(action: ExecutedAction): boolean;
|
|
11
|
-
export declare function hasMeaningfulBrowserAction(actionHistory: ExecutedAction[]): boolean;
|
|
12
|
-
export declare function buildRecoveryActionSignature(action: ExecutedAction): string;
|
|
13
|
-
export declare function isExplicitLoginAction(action: ExecutedAction): boolean;
|
|
14
|
-
export declare function hasRecentExplicitLoginAction(previousActions: ExecutedAction[]): boolean;
|
|
15
|
-
export declare function isLoginAction(action: ExecutedAction, previousActions?: ExecutedAction[]): boolean;
|
|
16
|
-
export declare function compactReplayActions(recordedActions: ExecutedAction[], params?: {
|
|
17
|
-
currentUrl?: string;
|
|
18
|
-
targetUrl?: string;
|
|
19
|
-
currentViewport?: {
|
|
20
|
-
width: number;
|
|
21
|
-
height: number;
|
|
22
|
-
} | null;
|
|
23
|
-
isAuthenticated?: boolean;
|
|
24
|
-
ignoreRecordedViewport?: boolean;
|
|
25
|
-
}): ExecutedAction[];
|
|
26
|
-
export declare function countRecentNoEffectActions(actionHistory: ExecutedAction[]): number;
|
|
27
|
-
export declare function shouldTriggerRecovery(actionHistory: ExecutedAction[]): boolean;
|
|
28
|
-
export declare function inferPrematureGiveUpCorrection(params: {
|
|
29
|
-
reason: string;
|
|
30
|
-
actionHistory: ExecutedAction[];
|
|
31
|
-
lastVerificationFailure?: string;
|
|
32
|
-
iteration: number;
|
|
33
|
-
maxIterations: number;
|
|
34
|
-
}): string | null;
|
|
35
|
-
export declare function inferSearchScrollLoopGuard(params: {
|
|
36
|
-
actionHistory: ExecutedAction[];
|
|
37
|
-
action: ActionType;
|
|
38
|
-
args: Record<string, unknown>;
|
|
39
|
-
}): string | null;
|
|
40
|
-
export declare function inferRepeatedActionGuard(params: {
|
|
41
|
-
actionHistory: ExecutedAction[];
|
|
42
|
-
action: ActionType;
|
|
43
|
-
args: Record<string, unknown>;
|
|
44
|
-
currentUrl?: string;
|
|
45
|
-
}): string | null;
|