autokap 1.1.3 → 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 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 takeScreenshot() {
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();
@@ -92,10 +92,13 @@ export async function runCapture(options) {
92
92
  credentials: program.preconditions.credentials,
93
93
  });
94
94
  // Step 4: Execute the program
95
+ const maxParallelVariants = program.mediaMode === 'clip'
96
+ ? 1
97
+ : program.maxParallelCaptures;
95
98
  const runOptions = {
96
99
  recoveryChain,
97
100
  abortSignal: options.abortSignal,
98
- maxParallelVariants: program.maxParallelCaptures,
101
+ maxParallelVariants,
99
102
  llmConfig,
100
103
  presetName: program.presetId,
101
104
  onProgress: (event) => {
@@ -106,8 +109,12 @@ export async function runCapture(options) {
106
109
  },
107
110
  };
108
111
  const captureStart = Date.now();
109
- if (program.maxParallelCaptures) {
110
- logger.info(`[capture] Concurrency cap resolved to ${program.maxParallelCaptures} parallel variant(s)`);
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
+ }
111
118
  }
112
119
  const createAdapter = async (variant) => {
113
120
  const recordable = program.mediaMode === 'clip';
@@ -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): Promise<void>;
46
+ export declare function humanType(page: Page, text: string, options?: {
47
+ minDelayMs?: number;
48
+ maxDelayMs?: number;
49
+ }): Promise<void>;
@@ -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 = 60 + Math.random() * 80;
111
- await page.waitForTimeout(delay);
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({ page, framesDir });
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,
@@ -651,7 +656,9 @@ export class WebPlaywrightLocal {
651
656
  await page.keyboard.press('Control+A');
652
657
  }
653
658
  await page.waitForTimeout(70);
654
- await humanType(page, text);
659
+ await humanType(page, text, this.clipCursor
660
+ ? { minDelayMs: 20, maxDelayMs: 45 }
661
+ : undefined);
655
662
  }
656
663
  async seedClipCursor() {
657
664
  if (!this.clipCursor)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autokap",
3
- "version": "1.1.3",
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",