autokap 1.3.4 → 1.3.6
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 +5 -1
- package/dist/browser.js +21 -3
- package/dist/cli-runner.js +7 -1
- package/dist/cli.js +56 -16
- package/dist/web-playwright-local.js +53 -13
- package/package.json +1 -1
package/dist/browser.d.ts
CHANGED
|
@@ -96,6 +96,7 @@ export declare class Browser {
|
|
|
96
96
|
private browser;
|
|
97
97
|
private context;
|
|
98
98
|
private page;
|
|
99
|
+
private nativeVideoStartedAt;
|
|
99
100
|
private elementMap;
|
|
100
101
|
private akNodeIndex;
|
|
101
102
|
private poolContext;
|
|
@@ -116,7 +117,9 @@ export declare class Browser {
|
|
|
116
117
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
117
118
|
* the cursor overlay script so clicks/hover moments are visible.
|
|
118
119
|
*/
|
|
119
|
-
static forClipCapture(options: BrowserOptions, cursorScript: string
|
|
120
|
+
static forClipCapture(options: BrowserOptions, cursorScript: string, recording?: {
|
|
121
|
+
nativeVideoDir?: string;
|
|
122
|
+
}): Promise<Browser>;
|
|
120
123
|
/**
|
|
121
124
|
* Close only the browser context (not the browser process).
|
|
122
125
|
* Used by clip capture to release the context promptly after the CDP loop
|
|
@@ -332,6 +335,7 @@ export declare class Browser {
|
|
|
332
335
|
resizeViewport(width: number, height: number): Promise<void>;
|
|
333
336
|
get currentPage(): Page;
|
|
334
337
|
get browserContext(): BrowserContext;
|
|
338
|
+
get nativeVideoStartTime(): number | null;
|
|
335
339
|
/**
|
|
336
340
|
* Observation pass for mock data generation.
|
|
337
341
|
* Navigates to the given URL, waits for network idle, and records all JSON API responses.
|
package/dist/browser.js
CHANGED
|
@@ -772,6 +772,7 @@ export class Browser {
|
|
|
772
772
|
browser = null;
|
|
773
773
|
context = null;
|
|
774
774
|
page = null;
|
|
775
|
+
nativeVideoStartedAt = null;
|
|
775
776
|
elementMap = new Map();
|
|
776
777
|
akNodeIndex = new Map();
|
|
777
778
|
poolContext = false;
|
|
@@ -804,7 +805,7 @@ export class Browser {
|
|
|
804
805
|
* `--window-size`) so the captured frames match viewport × DSF pixels, and
|
|
805
806
|
* the cursor overlay script so clicks/hover moments are visible.
|
|
806
807
|
*/
|
|
807
|
-
static async forClipCapture(options, cursorScript) {
|
|
808
|
+
static async forClipCapture(options, cursorScript, recording) {
|
|
808
809
|
const instance = new Browser(options);
|
|
809
810
|
const deviceScaleFactor = normalizeDeviceScaleFactor(options.deviceScaleFactor);
|
|
810
811
|
// Enable GPU compositor on non-Linux platforms so Chrome can render
|
|
@@ -832,13 +833,23 @@ export class Browser {
|
|
|
832
833
|
headless: !options.headed,
|
|
833
834
|
args: clipArgs,
|
|
834
835
|
});
|
|
835
|
-
|
|
836
|
+
const contextOptions = {
|
|
836
837
|
viewport: options.viewport,
|
|
837
838
|
deviceScaleFactor,
|
|
838
839
|
locale: langToLocale(options.lang ?? 'en'),
|
|
839
840
|
colorScheme: options.colorScheme ?? 'light',
|
|
840
841
|
storageState: options.storageState,
|
|
841
|
-
}
|
|
842
|
+
};
|
|
843
|
+
if (recording?.nativeVideoDir) {
|
|
844
|
+
contextOptions.recordVideo = {
|
|
845
|
+
dir: recording.nativeVideoDir,
|
|
846
|
+
size: {
|
|
847
|
+
width: Math.round(options.viewport.width),
|
|
848
|
+
height: Math.round(options.viewport.height),
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
instance.context = await instance.browser.newContext(contextOptions);
|
|
842
853
|
// Inject cursor overlay at context level — survives all navigations in this session
|
|
843
854
|
await instance.context.addInitScript(cursorScript);
|
|
844
855
|
// Also hide dev/prototype chrome on every document, including navigations
|
|
@@ -863,7 +874,11 @@ export class Browser {
|
|
|
863
874
|
document.addEventListener('DOMContentLoaded', install, { once: true });
|
|
864
875
|
}
|
|
865
876
|
}, { styleId: CAPTURE_HIDE_STYLE_ID, css: getCaptureHideCSS() });
|
|
877
|
+
const nativeVideoStartedAt = Date.now();
|
|
866
878
|
instance.page = await instance.context.newPage();
|
|
879
|
+
if (recording?.nativeVideoDir) {
|
|
880
|
+
instance.nativeVideoStartedAt = nativeVideoStartedAt;
|
|
881
|
+
}
|
|
867
882
|
return instance;
|
|
868
883
|
}
|
|
869
884
|
/**
|
|
@@ -5043,6 +5058,9 @@ export class Browser {
|
|
|
5043
5058
|
get browserContext() {
|
|
5044
5059
|
return this.ensureContext();
|
|
5045
5060
|
}
|
|
5061
|
+
get nativeVideoStartTime() {
|
|
5062
|
+
return this.nativeVideoStartedAt;
|
|
5063
|
+
}
|
|
5046
5064
|
/**
|
|
5047
5065
|
* Observation pass for mock data generation.
|
|
5048
5066
|
* Navigates to the given URL, waits for network idle, and records all JSON API responses.
|
package/dist/cli-runner.js
CHANGED
|
@@ -241,7 +241,13 @@ export async function runCapture(options) {
|
|
|
241
241
|
}
|
|
242
242
|
if (recordable) {
|
|
243
243
|
recordingDir = await fs.mkdtemp(path.join(os.tmpdir(), `autokap-${program.mediaMode}-`));
|
|
244
|
-
|
|
244
|
+
const nativeCloudClipRecording = program.mediaMode === 'clip'
|
|
245
|
+
&& process.env.AUTOKAP_CLOUD_RUNNER === '1'
|
|
246
|
+
&& process.env.AUTOKAP_CLIP_RECORDER !== 'cdp';
|
|
247
|
+
if (nativeCloudClipRecording) {
|
|
248
|
+
logger.info('[capture] Cloud clip recorder: native Playwright video (CDP screenshot loop disabled)');
|
|
249
|
+
}
|
|
250
|
+
browser = await Browser.forClipCapture(browserOptions, buildCursorOverlayScript(program.artifactPlan.cursorTheme ?? 'minimal'), nativeCloudClipRecording ? { nativeVideoDir: recordingDir } : undefined);
|
|
245
251
|
}
|
|
246
252
|
else if (browserOptions.headed) {
|
|
247
253
|
// Headed mode: standalone browser (pool is always headless)
|
package/dist/cli.js
CHANGED
|
@@ -79,6 +79,40 @@ function ensureExportFormat(format) {
|
|
|
79
79
|
function printJson(value) {
|
|
80
80
|
console.log(JSON.stringify(value, null, 2));
|
|
81
81
|
}
|
|
82
|
+
function displayPresetName(preset) {
|
|
83
|
+
const name = preset.name?.trim();
|
|
84
|
+
return name && name.length > 0 ? name : preset.id;
|
|
85
|
+
}
|
|
86
|
+
function cloudCaptureProgressCheckpoint(event) {
|
|
87
|
+
switch (event.type) {
|
|
88
|
+
case 'variant_start':
|
|
89
|
+
return {
|
|
90
|
+
type: 'preset_progress',
|
|
91
|
+
message: `Capture started: ${event.variantId}`,
|
|
92
|
+
};
|
|
93
|
+
case 'variant_end':
|
|
94
|
+
return {
|
|
95
|
+
type: 'preset_progress',
|
|
96
|
+
message: event.status === 'ok'
|
|
97
|
+
? `Capture completed: ${event.variantId}`
|
|
98
|
+
: `Capture failed: ${event.variantId}`,
|
|
99
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
100
|
+
};
|
|
101
|
+
case 'upload_start':
|
|
102
|
+
return {
|
|
103
|
+
type: 'upload_start',
|
|
104
|
+
message: 'Export started',
|
|
105
|
+
};
|
|
106
|
+
case 'upload_end':
|
|
107
|
+
return {
|
|
108
|
+
type: 'upload_end',
|
|
109
|
+
message: event.status === 'failed' ? 'Export failed' : 'Export completed',
|
|
110
|
+
status: event.status === 'failed' ? 'running' : undefined,
|
|
111
|
+
};
|
|
112
|
+
default:
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
82
116
|
function buildEndpointAssetUrl(config, endpointId) {
|
|
83
117
|
return `${config.apiBaseUrl}/api/v1/assets/${endpointId}`;
|
|
84
118
|
}
|
|
@@ -249,7 +283,8 @@ program
|
|
|
249
283
|
}
|
|
250
284
|
if (opts.cloud) {
|
|
251
285
|
process.env.AUTOKAP_CLOUD_RUNNER = '1';
|
|
252
|
-
|
|
286
|
+
const runnerVersion = process.env.AUTOKAP_CLI_VERSION ?? version;
|
|
287
|
+
logger.info(`[capture] Cloud runner mode — CLI ${runnerVersion}; Linux FPS cap lifted (clips target 30 fps)`);
|
|
253
288
|
}
|
|
254
289
|
if (opts.local) {
|
|
255
290
|
process.env[API_BASE_URL_ENV_VAR] = LOCAL_API_BASE_URL;
|
|
@@ -265,6 +300,12 @@ program
|
|
|
265
300
|
const checkpointUrl = cloudRunId
|
|
266
301
|
? buildApiUrl(config, `/api/cli/cloud-recapture/${cloudRunId}/checkpoint`)
|
|
267
302
|
: null;
|
|
303
|
+
if (opts.cloud && cloudRunId) {
|
|
304
|
+
logger.info(`[auto-recapture] Cloud progress callbacks enabled for run ${cloudRunId}`);
|
|
305
|
+
}
|
|
306
|
+
else if (opts.cloud) {
|
|
307
|
+
logger.warn('[auto-recapture] Cloud progress disabled: AUTOKAP_RUN_ID is not set');
|
|
308
|
+
}
|
|
268
309
|
let lastProgressCheckpointAt = 0;
|
|
269
310
|
const postCloudCheckpoint = async (body, options = {}) => {
|
|
270
311
|
if (!checkpointUrl)
|
|
@@ -322,7 +363,7 @@ program
|
|
|
322
363
|
totalPresets: data.presets.length,
|
|
323
364
|
completedPresets: 0,
|
|
324
365
|
failedPresets: 0,
|
|
325
|
-
message: `planned ${data.presets.length} preset(s)`,
|
|
366
|
+
message: `Run planned: ${data.presets.length} preset(s)`,
|
|
326
367
|
});
|
|
327
368
|
if (data.presets.length === 0) {
|
|
328
369
|
logger.info(`[auto-recapture] No presets enabled for project ${opts.project}`);
|
|
@@ -336,9 +377,10 @@ program
|
|
|
336
377
|
totalPresets: data.presets.length,
|
|
337
378
|
completedPresets: 0,
|
|
338
379
|
failedPresets: 0,
|
|
339
|
-
message: `
|
|
380
|
+
message: `Runner started: ${data.presets.length} preset(s) to capture`,
|
|
340
381
|
});
|
|
341
382
|
for (const [index, preset] of data.presets.entries()) {
|
|
383
|
+
const presetDisplayName = displayPresetName(preset);
|
|
342
384
|
const label = preset.name ? `${preset.name} (${preset.id})` : preset.id;
|
|
343
385
|
logger.info(`[auto-recapture] Running ${label}`);
|
|
344
386
|
await postCloudCheckpoint({
|
|
@@ -349,7 +391,7 @@ program
|
|
|
349
391
|
totalPresets: data.presets.length,
|
|
350
392
|
completedPresets: index,
|
|
351
393
|
failedPresets: failures.length,
|
|
352
|
-
message: `
|
|
394
|
+
message: `Preset started: ${presetDisplayName}`,
|
|
353
395
|
});
|
|
354
396
|
const result = await runCapture({
|
|
355
397
|
presetId: preset.id,
|
|
@@ -357,23 +399,21 @@ program
|
|
|
357
399
|
headed: opts.headed,
|
|
358
400
|
allowUploadFailure: opts.allowUploadFailure,
|
|
359
401
|
onProgress: (event) => {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
? 'upload_end'
|
|
364
|
-
: 'preset_progress';
|
|
402
|
+
const checkpoint = cloudCaptureProgressCheckpoint(event);
|
|
403
|
+
if (!checkpoint)
|
|
404
|
+
return;
|
|
365
405
|
void postCloudCheckpoint({
|
|
366
|
-
type:
|
|
406
|
+
type: checkpoint.type,
|
|
367
407
|
presetId: preset.id,
|
|
368
408
|
presetName: preset.name ?? null,
|
|
369
409
|
presetIndex: index,
|
|
370
410
|
totalPresets: data.presets.length,
|
|
371
411
|
completedPresets: index,
|
|
372
412
|
failedPresets: failures.length,
|
|
373
|
-
message:
|
|
413
|
+
message: checkpoint.message,
|
|
374
414
|
progressEvent: event,
|
|
375
|
-
status:
|
|
376
|
-
}
|
|
415
|
+
status: checkpoint.status,
|
|
416
|
+
});
|
|
377
417
|
},
|
|
378
418
|
});
|
|
379
419
|
const childRunId = result.runId;
|
|
@@ -392,7 +432,7 @@ program
|
|
|
392
432
|
childRunId,
|
|
393
433
|
status: 'failed',
|
|
394
434
|
errorMessage: error,
|
|
395
|
-
message: `failed ${
|
|
435
|
+
message: `Preset failed: ${presetDisplayName}`,
|
|
396
436
|
});
|
|
397
437
|
}
|
|
398
438
|
else {
|
|
@@ -406,7 +446,7 @@ program
|
|
|
406
446
|
failedPresets: failures.length,
|
|
407
447
|
childRunId,
|
|
408
448
|
status: 'completed',
|
|
409
|
-
message: `completed ${
|
|
449
|
+
message: `Preset completed: ${presetDisplayName}`,
|
|
410
450
|
});
|
|
411
451
|
}
|
|
412
452
|
}
|
|
@@ -438,7 +478,7 @@ program
|
|
|
438
478
|
completedPresets: data.presets.length,
|
|
439
479
|
failedPresets: 0,
|
|
440
480
|
status: 'completed',
|
|
441
|
-
message: '
|
|
481
|
+
message: 'Cloud recapture completed',
|
|
442
482
|
});
|
|
443
483
|
await notifyCloudCallback('completed', {
|
|
444
484
|
totalPresets: data.presets.length,
|
|
@@ -352,21 +352,32 @@ export class WebPlaywrightLocal {
|
|
|
352
352
|
const cloudClipFps = isCloudRunner ? 30 : defaultFps;
|
|
353
353
|
const targetFps = options.captureFps
|
|
354
354
|
?? (options.mediaMode === 'video' ? 30 : cloudClipFps);
|
|
355
|
-
const
|
|
356
|
-
page
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
355
|
+
const nativeClipVideo = options.mediaMode === 'clip' && isCloudRunner
|
|
356
|
+
? page.video()
|
|
357
|
+
: null;
|
|
358
|
+
let loop = null;
|
|
359
|
+
if (nativeClipVideo) {
|
|
360
|
+
logger.info('[capture] Native cloud clip recorder enabled — using Playwright video stream instead of CDP screenshots');
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
loop = new ClipCaptureLoop({
|
|
364
|
+
page,
|
|
365
|
+
framesDir,
|
|
366
|
+
targetFps,
|
|
367
|
+
// Cloud runners have CPU headroom — drop the Linux 50 ms idle cushion
|
|
368
|
+
// (sized for tight CI runners) to let the loop stay close to its target.
|
|
369
|
+
minRestMs: process.platform === 'linux' && !isCloudRunner ? 50 : 16,
|
|
370
|
+
});
|
|
371
|
+
await loop.start();
|
|
372
|
+
}
|
|
364
373
|
this.recording = {
|
|
365
374
|
mediaMode: options.mediaMode,
|
|
366
375
|
startedAt: Date.now(),
|
|
367
376
|
framesDir,
|
|
368
377
|
mp4Path: path.join(baseDir, `${options.mediaMode}.mp4`),
|
|
369
378
|
loop,
|
|
379
|
+
nativeVideo: nativeClipVideo,
|
|
380
|
+
nativeVideoStartedAt: this.browser.nativeVideoStartTime,
|
|
370
381
|
finalized: false,
|
|
371
382
|
};
|
|
372
383
|
this.clipCursor = {
|
|
@@ -446,20 +457,47 @@ export class WebPlaywrightLocal {
|
|
|
446
457
|
throw new Error('recording was not started');
|
|
447
458
|
}
|
|
448
459
|
if (this.recording.finalized) {
|
|
449
|
-
const
|
|
450
|
-
const
|
|
460
|
+
const sourcePath = this.recording.sourcePath ?? this.recording.mp4Path;
|
|
461
|
+
const buffer = await fs.readFile(sourcePath);
|
|
462
|
+
const durationMs = this.recording.encodedDurationMs ?? await getMediaDurationMs(sourcePath);
|
|
451
463
|
this.recording.encodedDurationMs = durationMs;
|
|
452
464
|
return {
|
|
453
465
|
buffer,
|
|
454
466
|
durationMs,
|
|
455
|
-
mimeType: 'video/mp4',
|
|
456
|
-
trimStartMs: this.recording.result?.trimStartMs ?? 0,
|
|
467
|
+
mimeType: this.recording.sourceMimeType ?? 'video/mp4',
|
|
468
|
+
trimStartMs: this.recording.trimStartMs ?? this.recording.result?.trimStartMs ?? 0,
|
|
457
469
|
};
|
|
458
470
|
}
|
|
459
471
|
if (this.recordingNavWatcher) {
|
|
460
472
|
this.recordingNavWatcher.detach();
|
|
461
473
|
this.recordingNavWatcher = null;
|
|
462
474
|
}
|
|
475
|
+
if (this.recording.nativeVideo) {
|
|
476
|
+
const video = this.recording.nativeVideo;
|
|
477
|
+
const trimStartMs = Math.max(0, this.recording.startedAt
|
|
478
|
+
- (this.recording.nativeVideoStartedAt ?? this.sessionStartedAt));
|
|
479
|
+
await this.browser.closeContext();
|
|
480
|
+
const videoPath = await video.path();
|
|
481
|
+
const durationMs = await getMediaDurationMs(videoPath);
|
|
482
|
+
logger.info(`[capture] Native clip recording finalized: source ${durationMs}ms, ` +
|
|
483
|
+
`trim start ${Math.round(trimStartMs)}ms`);
|
|
484
|
+
this.recording.finalized = true;
|
|
485
|
+
this.recording.sourcePath = videoPath;
|
|
486
|
+
this.recording.sourceMimeType = 'video/webm';
|
|
487
|
+
this.recording.trimStartMs = trimStartMs;
|
|
488
|
+
this.recording.encodedDurationMs = durationMs;
|
|
489
|
+
this.clipCursor = null;
|
|
490
|
+
const buffer = await fs.readFile(videoPath);
|
|
491
|
+
return {
|
|
492
|
+
buffer,
|
|
493
|
+
durationMs,
|
|
494
|
+
mimeType: 'video/webm',
|
|
495
|
+
trimStartMs,
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (!this.recording.loop) {
|
|
499
|
+
throw new Error('recording loop was not initialized');
|
|
500
|
+
}
|
|
463
501
|
const result = await this.recording.loop.stop();
|
|
464
502
|
logger.info(`[capture] Clip frame capture: ${result.frameCount} frame(s), ` +
|
|
465
503
|
`${result.measuredFps.toFixed(1)} fps over ${(result.actualDurationMs / 1000).toFixed(2)}s ` +
|
|
@@ -475,6 +513,8 @@ export class WebPlaywrightLocal {
|
|
|
475
513
|
});
|
|
476
514
|
this.recording.finalized = true;
|
|
477
515
|
this.recording.result = result;
|
|
516
|
+
this.recording.sourcePath = this.recording.mp4Path;
|
|
517
|
+
this.recording.sourceMimeType = 'video/mp4';
|
|
478
518
|
this.recording.encodedDurationMs = await getMediaDurationMs(this.recording.mp4Path);
|
|
479
519
|
this.clipCursor = null;
|
|
480
520
|
const buffer = await fs.readFile(this.recording.mp4Path);
|