autokap 1.0.7 → 1.0.10
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/assets/cursors/macos.svg +4 -0
- package/assets/cursors/windows.svg +15 -0
- package/assets/skill/OPCODE-REFERENCE.md +635 -0
- package/assets/skill/README.md +39 -0
- package/assets/skill/SKILL.md +454 -468
- package/assets/skill/STUDIO-SKILL.md +476 -0
- package/assets/skill/references/examples.md +104 -0
- package/assets/skill/references/interactive-demo.md +225 -0
- package/assets/skill/references/mock-data.md +178 -0
- package/dist/action-verifier.d.ts +29 -0
- package/dist/action-verifier.js +133 -0
- package/dist/agent-action-recovery.d.ts +45 -0
- package/dist/agent-action-recovery.js +370 -0
- package/dist/agent-message-utils.d.ts +21 -0
- package/dist/agent-message-utils.js +77 -0
- package/dist/agent-url-utils.d.ts +30 -0
- package/dist/agent-url-utils.js +138 -0
- package/dist/agent.d.ts +92 -8
- package/dist/agent.js +2936 -781
- package/dist/ak-tree.d.ts +39 -0
- package/dist/ak-tree.js +368 -0
- package/dist/alt-text.d.ts +26 -0
- package/dist/alt-text.js +55 -0
- package/dist/auth-capture.d.ts +17 -0
- package/dist/auth-capture.js +197 -0
- package/dist/benchmark.d.ts +59 -0
- package/dist/benchmark.js +135 -0
- package/dist/billing-operation-logging.d.ts +3 -1
- package/dist/billing-operation-logging.js +4 -0
- package/dist/browser-bar.d.ts +14 -6
- package/dist/browser-bar.js +145 -8
- package/dist/browser-pool.d.ts +7 -0
- package/dist/browser-pool.js +15 -5
- package/dist/browser-utils.d.ts +31 -0
- package/dist/browser-utils.js +97 -0
- package/dist/browser.d.ts +61 -11
- package/dist/browser.js +1513 -59
- package/dist/capture-alt-text.js +2 -1
- package/dist/capture-encryption.d.ts +3 -1
- package/dist/capture-encryption.js +21 -6
- package/dist/capture-language-preflight.js +14 -0
- package/dist/capture-llm-page-identity.js +22 -10
- package/dist/capture-page-identity.d.ts +5 -7
- package/dist/capture-page-identity.js +211 -78
- package/dist/capture-preset-credentials.d.ts +50 -0
- package/dist/capture-preset-credentials.js +127 -0
- package/dist/capture-request-plan.d.ts +2 -2
- package/dist/capture-request-plan.js +64 -16
- package/dist/capture-run-optimizer.js +48 -33
- package/dist/capture-selector-memory.d.ts +5 -0
- package/dist/capture-selector-memory.js +18 -0
- package/dist/capture-strategy.d.ts +36 -0
- package/dist/capture-strategy.js +96 -0
- package/dist/capture-studio-sync.d.ts +1 -0
- package/dist/capture-studio-sync.js +9 -3
- package/dist/capture-surface-contract.d.ts +36 -0
- package/dist/capture-surface-contract.js +299 -0
- package/dist/capture-transition-engine.d.ts +28 -0
- package/dist/capture-transition-engine.js +292 -0
- package/dist/capture-variant-state.d.ts +2 -0
- package/dist/capture-variant-state.js +26 -0
- package/dist/capture-verification.d.ts +35 -0
- package/dist/capture-verification.js +95 -0
- package/dist/capture-viewport-lock.d.ts +48 -0
- package/dist/capture-viewport-lock.js +74 -0
- package/dist/circuit-breaker.d.ts +42 -0
- package/dist/circuit-breaker.js +119 -0
- package/dist/cli-config.d.ts +9 -1
- package/dist/cli-config.js +112 -7
- package/dist/cli-contract.d.ts +19 -0
- package/dist/cli-contract.js +173 -0
- package/dist/cli-runner-local.d.ts +12 -0
- package/dist/cli-runner-local.js +115 -0
- package/dist/cli-runner.d.ts +34 -0
- package/dist/cli-runner.js +580 -0
- package/dist/cli-utils.d.ts +0 -1
- package/dist/cli-utils.js +2 -5
- package/dist/cli.js +1011 -267
- package/dist/clip-begin-frame-recorder.d.ts +44 -0
- package/dist/clip-begin-frame-recorder.js +250 -0
- package/dist/clip-capture-backend.d.ts +25 -0
- package/dist/clip-capture-backend.js +189 -0
- package/dist/clip-capture-loop.d.ts +61 -0
- package/dist/clip-capture-loop.js +111 -0
- package/dist/clip-frame-recorder.d.ts +63 -0
- package/dist/clip-frame-recorder.js +305 -0
- package/dist/clip-orchestrator.js +9 -2
- package/dist/clip-postprocess.d.ts +31 -2
- package/dist/clip-postprocess.js +194 -68
- package/dist/clip-runtime.d.ts +18 -0
- package/dist/clip-runtime.js +67 -0
- package/dist/clip-scale.d.ts +10 -0
- package/dist/clip-scale.js +21 -0
- package/dist/clip-screencast-recorder.d.ts +42 -0
- package/dist/clip-screencast-recorder.js +242 -0
- package/dist/clip-sidecar.d.ts +54 -0
- package/dist/clip-sidecar.js +208 -0
- package/dist/cookie-dismiss.d.ts +2 -0
- package/dist/cookie-dismiss.js +48 -13
- package/dist/cost-logging.d.ts +8 -0
- package/dist/cost-logging.js +160 -46
- package/dist/cost-resolution-monitor.d.ts +16 -0
- package/dist/cost-resolution-monitor.js +34 -0
- package/dist/credential-templates.js +2 -2
- package/dist/cursor-overlay-script.d.ts +6 -0
- package/dist/cursor-overlay-script.js +169 -0
- package/dist/dom-css-purger.d.ts +65 -0
- package/dist/dom-css-purger.js +333 -0
- package/dist/dom-font-inliner.d.ts +45 -0
- package/dist/dom-font-inliner.js +148 -0
- package/dist/dom-patch-resolver.d.ts +52 -0
- package/dist/dom-patch-resolver.js +242 -0
- package/dist/dom-serializer.d.ts +82 -0
- package/dist/dom-serializer.js +378 -0
- package/dist/element-capture.d.ts +1 -41
- package/dist/element-capture.js +202 -446
- package/dist/env-validation.d.ts +5 -0
- package/dist/env-validation.js +63 -0
- package/dist/execution-schema.d.ts +4753 -0
- package/dist/execution-schema.js +563 -0
- package/dist/execution-types.d.ts +936 -0
- package/dist/execution-types.js +66 -0
- package/dist/fonts-loader.d.ts +14 -0
- package/dist/fonts-loader.js +55 -0
- package/dist/hybrid-navigator.js +12 -12
- package/dist/index.d.ts +11 -6
- package/dist/index.js +11 -4
- package/dist/legacy/agent-action-recovery.d.ts +45 -0
- package/dist/legacy/agent-action-recovery.js +370 -0
- package/dist/legacy/agent-message-utils.d.ts +21 -0
- package/dist/legacy/agent-message-utils.js +77 -0
- package/dist/legacy/agent-url-utils.d.ts +30 -0
- package/dist/legacy/agent-url-utils.js +138 -0
- package/dist/legacy/agent.d.ts +226 -0
- package/dist/legacy/agent.js +6666 -0
- package/dist/legacy/clip-orchestrator.d.ts +148 -0
- package/dist/legacy/clip-orchestrator.js +957 -0
- package/dist/legacy/credential-templates.d.ts +5 -0
- package/dist/legacy/credential-templates.js +60 -0
- package/dist/legacy/hybrid-navigator.d.ts +138 -0
- package/dist/legacy/hybrid-navigator.js +468 -0
- package/dist/legacy/llm-usage.d.ts +17 -0
- package/dist/legacy/llm-usage.js +45 -0
- package/dist/legacy/prompt-cache.d.ts +10 -0
- package/dist/legacy/prompt-cache.js +24 -0
- package/dist/legacy/prompts.d.ts +175 -0
- package/dist/legacy/prompts.js +1038 -0
- package/dist/legacy/tools.d.ts +4 -0
- package/dist/legacy/tools.js +216 -0
- package/dist/legacy/video-agent.d.ts +143 -0
- package/dist/legacy/video-agent.js +4788 -0
- package/dist/legacy/video-observation.d.ts +36 -0
- package/dist/legacy/video-observation.js +192 -0
- package/dist/legacy/video-planner.d.ts +12 -0
- package/dist/legacy/video-planner.js +501 -0
- package/dist/legacy/video-prompts.d.ts +37 -0
- package/dist/legacy/video-prompts.js +569 -0
- package/dist/legacy/video-tools.d.ts +3 -0
- package/dist/legacy/video-tools.js +59 -0
- package/dist/legacy/video-variant-state.d.ts +29 -0
- package/dist/legacy/video-variant-state.js +80 -0
- package/dist/legacy/vision-model.d.ts +17 -0
- package/dist/legacy/vision-model.js +74 -0
- package/dist/llm-healer.d.ts +55 -0
- package/dist/llm-healer.js +213 -0
- package/dist/llm-provider.d.ts +29 -0
- package/dist/llm-provider.js +83 -0
- package/dist/logger.d.ts +6 -2
- package/dist/logger.js +15 -1
- package/dist/mockup-html.js +35 -25
- package/dist/mockup.d.ts +95 -2
- package/dist/mockup.js +427 -166
- package/dist/mouse-animation.d.ts +2 -2
- package/dist/mouse-animation.js +34 -20
- package/dist/opcode-actions.d.ts +42 -0
- package/dist/opcode-actions.js +524 -0
- package/dist/opcode-runner.d.ts +51 -0
- package/dist/opcode-runner.js +779 -0
- package/dist/openrouter-client.d.ts +40 -0
- package/dist/openrouter-client.js +16 -0
- package/dist/overlay-engine.d.ts +24 -0
- package/dist/overlay-engine.js +176 -0
- package/dist/postcondition.d.ts +16 -0
- package/dist/postcondition.js +269 -0
- package/dist/program-patcher.d.ts +25 -0
- package/dist/program-patcher.js +44 -0
- package/dist/program-signing.d.ts +1094 -0
- package/dist/program-signing.js +140 -0
- package/dist/prompts.d.ts +13 -5
- package/dist/prompts.js +224 -351
- package/dist/provider-config.d.ts +17 -0
- package/dist/provider-config.js +42 -0
- package/dist/recovery-chain.d.ts +37 -0
- package/dist/recovery-chain.js +374 -0
- package/dist/remote-browser.d.ts +28 -4
- package/dist/remote-browser.js +60 -5
- package/dist/safari-browser-bar.d.ts +15 -0
- package/dist/safari-browser-bar.js +95 -0
- package/dist/safari-toolbar-asset.d.ts +15 -0
- package/dist/safari-toolbar-asset.js +12 -0
- package/dist/security.d.ts +2 -1
- package/dist/security.js +49 -10
- package/dist/selector-resolver.d.ts +34 -0
- package/dist/selector-resolver.js +181 -0
- package/dist/semantic-resolver.d.ts +35 -0
- package/dist/semantic-resolver.js +161 -0
- package/dist/server-capture-runtime.d.ts +5 -3
- package/dist/server-capture-runtime.js +42 -95
- package/dist/server-credit-usage.d.ts +2 -2
- package/dist/server-project-webhooks.d.ts +15 -1
- package/dist/server-project-webhooks.js +34 -8
- package/dist/server-screenshot-watermark.js +27 -5
- package/dist/session-profile.js +164 -1
- package/dist/sf-pro-symbols.d.ts +1 -0
- package/dist/sf-pro-symbols.js +55 -0
- package/dist/skill-packaging.d.ts +28 -0
- package/dist/skill-packaging.js +169 -0
- package/dist/smart-wait.d.ts +27 -0
- package/dist/smart-wait.js +81 -0
- package/dist/status-bar-render.d.ts +20 -0
- package/dist/status-bar-render.js +410 -0
- package/dist/status-bar.d.ts +9 -0
- package/dist/status-bar.js +298 -14
- package/dist/svg-browser-bar.d.ts +33 -0
- package/dist/svg-browser-bar.js +206 -0
- package/dist/svg-status-bar.d.ts +36 -0
- package/dist/svg-status-bar.js +597 -0
- package/dist/svg-text.d.ts +61 -0
- package/dist/svg-text.js +118 -0
- package/dist/tools.js +89 -451
- package/dist/types.d.ts +248 -7
- package/dist/types.js +23 -1
- package/dist/v2/action-verifier.d.ts +29 -0
- package/dist/v2/action-verifier.js +133 -0
- package/dist/v2/alt-text.d.ts +26 -0
- package/dist/v2/alt-text.js +55 -0
- package/dist/v2/benchmark.d.ts +59 -0
- package/dist/v2/benchmark.js +135 -0
- package/dist/v2/capture-strategy.d.ts +30 -0
- package/dist/v2/capture-strategy.js +67 -0
- package/dist/v2/capture-verification.d.ts +35 -0
- package/dist/v2/capture-verification.js +95 -0
- package/dist/v2/circuit-breaker.d.ts +42 -0
- package/dist/v2/circuit-breaker.js +119 -0
- package/dist/v2/cli-runner-local.d.ts +11 -0
- package/dist/v2/cli-runner-local.js +91 -0
- package/dist/v2/cli-runner.d.ts +34 -0
- package/dist/v2/cli-runner.js +300 -0
- package/dist/v2/compiler-prompts.d.ts +27 -0
- package/dist/v2/compiler-prompts.js +123 -0
- package/dist/v2/compiler.d.ts +37 -0
- package/dist/v2/compiler.js +147 -0
- package/dist/v2/explorer.d.ts +41 -0
- package/dist/v2/explorer.js +56 -0
- package/dist/v2/index.d.ts +37 -0
- package/dist/v2/index.js +31 -0
- package/dist/v2/llm-healer.d.ts +62 -0
- package/dist/v2/llm-healer.js +166 -0
- package/dist/v2/llm-provider.d.ts +29 -0
- package/dist/v2/llm-provider.js +80 -0
- package/dist/v2/opcode-runner.d.ts +47 -0
- package/dist/v2/opcode-runner.js +634 -0
- package/dist/v2/overlay-engine.d.ts +24 -0
- package/dist/v2/overlay-engine.js +150 -0
- package/dist/v2/postcondition.d.ts +16 -0
- package/dist/v2/postcondition.js +249 -0
- package/dist/v2/program-patcher.d.ts +25 -0
- package/dist/v2/program-patcher.js +44 -0
- package/dist/v2/recovery-chain.d.ts +30 -0
- package/dist/v2/recovery-chain.js +368 -0
- package/dist/v2/schema.d.ts +2580 -0
- package/dist/v2/schema.js +295 -0
- package/dist/v2/selector-resolver.d.ts +34 -0
- package/dist/v2/selector-resolver.js +181 -0
- package/dist/v2/semantic-resolver.d.ts +35 -0
- package/dist/v2/semantic-resolver.js +161 -0
- package/dist/v2/smart-wait.d.ts +27 -0
- package/dist/v2/smart-wait.js +81 -0
- package/dist/v2/types.d.ts +444 -0
- package/dist/v2/types.js +19 -0
- package/dist/v2/web-playwright-local.d.ts +69 -0
- package/dist/v2/web-playwright-local.js +392 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +5 -0
- package/dist/video-agent.js +18 -13
- package/dist/video-planner.js +2 -1
- package/dist/video-prompts.js +3 -3
- package/dist/web-playwright-local.d.ts +156 -0
- package/dist/web-playwright-local.js +989 -0
- package/dist/ws-auth.js +4 -1
- package/dist/ws-broadcast.d.ts +34 -0
- package/dist/ws-broadcast.js +85 -0
- package/dist/ws-connection-limits.d.ts +12 -0
- package/dist/ws-connection-limits.js +44 -0
- package/dist/ws-handler-utils.d.ts +32 -0
- package/dist/ws-handler-utils.js +139 -0
- package/dist/ws-handler.js +294 -164
- package/dist/ws-metrics-server.d.ts +9 -0
- package/dist/ws-metrics-server.js +31 -0
- package/dist/ws-server.js +41 -1
- package/package.json +62 -35
package/dist/clip-postprocess.js
CHANGED
|
@@ -1,32 +1,42 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
-
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { copyFile, stat } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
const execFileAsync = promisify(execFile);
|
|
6
6
|
// ── Default values ──────────────────────────────────────────────────
|
|
7
|
-
const DEFAULT_GIF_FPS =
|
|
8
|
-
const DEFAULT_GIF_MAX_WIDTH =
|
|
7
|
+
const DEFAULT_GIF_FPS = 24;
|
|
8
|
+
const DEFAULT_GIF_MAX_WIDTH = 1440;
|
|
9
9
|
const DEFAULT_MAX_DURATION_SEC = 8;
|
|
10
10
|
const DEFAULT_TRIM_START_SEC = 0.3;
|
|
11
|
+
const DEFAULT_MP4_FPS = 30;
|
|
11
12
|
// ── ffmpeg detection ────────────────────────────────────────────────
|
|
12
|
-
|
|
13
|
+
const cachedBinaryPaths = {};
|
|
14
|
+
async function ensureBinaryAvailable(binaryName) {
|
|
15
|
+
const cached = cachedBinaryPaths[binaryName];
|
|
16
|
+
if (cached)
|
|
17
|
+
return cached;
|
|
18
|
+
const resolver = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
+
const { stdout } = await execFileAsync(resolver, [binaryName]);
|
|
20
|
+
const resolvedPath = stdout
|
|
21
|
+
.split(/\r?\n/)
|
|
22
|
+
.map(line => line.trim())
|
|
23
|
+
.find(Boolean);
|
|
24
|
+
if (!resolvedPath) {
|
|
25
|
+
throw new Error(`${binaryName} not found`);
|
|
26
|
+
}
|
|
27
|
+
cachedBinaryPaths[binaryName] = resolvedPath;
|
|
28
|
+
return resolvedPath;
|
|
29
|
+
}
|
|
13
30
|
/**
|
|
14
31
|
* Resolve the system ffmpeg binary path. Throws if not found.
|
|
15
32
|
*/
|
|
16
33
|
export async function ensureFfmpegAvailable() {
|
|
17
|
-
if (cachedFfmpegPath)
|
|
18
|
-
return cachedFfmpegPath;
|
|
19
34
|
try {
|
|
20
|
-
|
|
21
|
-
const ffmpegPath = stdout.trim();
|
|
22
|
-
if (!ffmpegPath)
|
|
23
|
-
throw new Error('ffmpeg not found');
|
|
24
|
-
cachedFfmpegPath = ffmpegPath;
|
|
25
|
-
return ffmpegPath;
|
|
35
|
+
return await ensureBinaryAvailable('ffmpeg');
|
|
26
36
|
}
|
|
27
37
|
catch {
|
|
28
38
|
throw new Error('ffmpeg is required for clip post-processing but was not found on your system. ' +
|
|
29
|
-
'Install it via: brew install ffmpeg (macOS)
|
|
39
|
+
'Install it via: brew install ffmpeg (macOS), apt install ffmpeg (Linux), or choco install ffmpeg (Windows).');
|
|
30
40
|
}
|
|
31
41
|
}
|
|
32
42
|
// ── Conversion functions ────────────────────────────────────────────
|
|
@@ -39,43 +49,103 @@ export async function convertToGif(webmPath, outputPath, opts = {}) {
|
|
|
39
49
|
const fps = opts.fps ?? DEFAULT_GIF_FPS;
|
|
40
50
|
const maxWidth = opts.maxWidth ?? DEFAULT_GIF_MAX_WIDTH;
|
|
41
51
|
const loopFlag = (opts.loop ?? true) ? '0' : '-1';
|
|
42
|
-
const paletteDir = path.dirname(outputPath);
|
|
43
|
-
const palettePath = path.join(paletteDir, `_palette_${Date.now()}.png`);
|
|
44
52
|
const scaleFilter = `fps=${fps},scale=${maxWidth}:-1:flags=lanczos`;
|
|
45
|
-
//
|
|
53
|
+
// Single-pass GIF generation with per-frame palette (paletteuse=new=1):
|
|
54
|
+
// each frame gets its own optimal 256-color palette, re-computed from the
|
|
55
|
+
// pixels in that frame. This is ~3× better visually than a single global
|
|
56
|
+
// palette for screen recordings where UI state transitions produce very
|
|
57
|
+
// different color distributions (modal open/close, theme accents, etc.).
|
|
58
|
+
// sierra2_4a = error-diffusion dither (clean diagonal pattern vs bayer
|
|
59
|
+
// dots). We intentionally DON'T use diff_mode=rectangle because it leaves
|
|
60
|
+
// stale-palette pixels in unchanged regions when `new=1` is active.
|
|
61
|
+
const lavfi = `${scaleFilter},split[a][b];` +
|
|
62
|
+
`[a]palettegen=stats_mode=single:max_colors=256:reserve_transparent=0[p];` +
|
|
63
|
+
`[b][p]paletteuse=dither=sierra2_4a:new=1`;
|
|
46
64
|
await execFileAsync(ffmpeg, [
|
|
47
65
|
'-i', webmPath,
|
|
48
|
-
'-
|
|
49
|
-
'-
|
|
66
|
+
'-lavfi', lavfi,
|
|
67
|
+
'-loop', loopFlag,
|
|
68
|
+
'-y', outputPath,
|
|
50
69
|
]);
|
|
51
|
-
|
|
70
|
+
}
|
|
71
|
+
function resolveVideoDimension(value) {
|
|
72
|
+
return Number.isFinite(value) ? Math.max(2, Math.round(Number(value))) & ~1 : null;
|
|
73
|
+
}
|
|
74
|
+
function resolveMp4Bitrate(width, height) {
|
|
75
|
+
const effectiveWidth = width ?? 1440;
|
|
76
|
+
const effectiveHeight = height ?? 900;
|
|
77
|
+
const megapixels = Math.max(0.1, effectiveWidth * effectiveHeight / 1e6);
|
|
78
|
+
const targetMbps = Math.min(40, Math.max(6, Math.round(megapixels * 7)));
|
|
79
|
+
return {
|
|
80
|
+
target: `${targetMbps}M`,
|
|
81
|
+
maxrate: `${targetMbps}M`,
|
|
82
|
+
bufsize: `${targetMbps * 2}M`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function probeVideoDimensions(ffprobe, inputPath) {
|
|
52
86
|
try {
|
|
53
|
-
await execFileAsync(
|
|
54
|
-
'-
|
|
55
|
-
'-
|
|
56
|
-
'-
|
|
57
|
-
'-
|
|
58
|
-
|
|
87
|
+
const { stdout } = await execFileAsync(ffprobe, [
|
|
88
|
+
'-v', 'error',
|
|
89
|
+
'-select_streams', 'v:0',
|
|
90
|
+
'-show_entries', 'stream=width,height',
|
|
91
|
+
'-of', 'csv=p=0:s=,',
|
|
92
|
+
inputPath,
|
|
59
93
|
]);
|
|
94
|
+
const [w, h] = stdout.trim().split(',').map(Number);
|
|
95
|
+
if (!Number.isFinite(w) || !Number.isFinite(h))
|
|
96
|
+
return null;
|
|
97
|
+
return { width: w, height: h };
|
|
60
98
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
await stat(palettePath).then(() => execFileAsync('rm', [palettePath]));
|
|
65
|
-
}
|
|
66
|
-
catch { /* ignore */ }
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
67
101
|
}
|
|
68
102
|
}
|
|
69
103
|
/**
|
|
70
104
|
* Convert a WebM recording to an MP4 with web-optimized settings.
|
|
71
105
|
*/
|
|
72
|
-
export async function convertToMp4(webmPath, outputPath) {
|
|
106
|
+
export async function convertToMp4(webmPath, outputPath, opts = {}) {
|
|
73
107
|
const ffmpeg = await ensureFfmpegAvailable();
|
|
108
|
+
const ffprobe = await ensureBinaryAvailable('ffprobe');
|
|
109
|
+
const width = resolveVideoDimension(opts.width);
|
|
110
|
+
const height = resolveVideoDimension(opts.height);
|
|
111
|
+
const fps = Math.max(1, Math.round(opts.fps ?? DEFAULT_MP4_FPS));
|
|
112
|
+
const trimStartSec = Math.max(0, opts.trimStartSec ?? 0);
|
|
113
|
+
const maxDurationSec = Math.max(0.1, opts.maxDurationSec ?? DEFAULT_MAX_DURATION_SEC);
|
|
114
|
+
const holdLastFrameSec = Math.min(Math.max(opts.holdLastFrameSec ?? 0, 0), 10);
|
|
115
|
+
const outputDurationSec = maxDurationSec + holdLastFrameSec;
|
|
116
|
+
const filters = [
|
|
117
|
+
`fps=${fps}`,
|
|
118
|
+
'setpts=PTS-STARTPTS',
|
|
119
|
+
];
|
|
120
|
+
if (holdLastFrameSec > 0) {
|
|
121
|
+
filters.push(`tpad=stop_mode=clone:stop_duration=${holdLastFrameSec}`);
|
|
122
|
+
}
|
|
123
|
+
if (width && height) {
|
|
124
|
+
const sourceDims = await probeVideoDimensions(ffprobe, webmPath);
|
|
125
|
+
const needsScale = !sourceDims
|
|
126
|
+
|| Math.abs(sourceDims.width - width) > 1
|
|
127
|
+
|| Math.abs(sourceDims.height - height) > 1;
|
|
128
|
+
if (needsScale) {
|
|
129
|
+
filters.push(`scale=${width}:${height}:flags=lanczos`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const bitrate = resolveMp4Bitrate(width, height);
|
|
74
133
|
await execFileAsync(ffmpeg, [
|
|
134
|
+
...(trimStartSec > 0 ? ['-ss', String(trimStartSec)] : []),
|
|
75
135
|
'-i', webmPath,
|
|
136
|
+
'-t', String(outputDurationSec),
|
|
137
|
+
'-vf', filters.join(','),
|
|
76
138
|
'-c:v', 'libx264',
|
|
77
139
|
'-preset', 'slow',
|
|
78
|
-
'-crf', '
|
|
140
|
+
'-crf', '16',
|
|
141
|
+
'-b:v', bitrate.target,
|
|
142
|
+
'-maxrate', bitrate.maxrate,
|
|
143
|
+
'-bufsize', bitrate.bufsize,
|
|
144
|
+
'-g', String(fps * 2),
|
|
145
|
+
'-keyint_min', String(fps),
|
|
146
|
+
'-sc_threshold', '0',
|
|
147
|
+
'-profile:v', 'high',
|
|
148
|
+
'-tune', 'animation',
|
|
79
149
|
'-pix_fmt', 'yuv420p',
|
|
80
150
|
'-movflags', '+faststart',
|
|
81
151
|
'-an',
|
|
@@ -93,6 +163,70 @@ export async function extractThumbnail(webmPath, outputPath) {
|
|
|
93
163
|
'-y', outputPath,
|
|
94
164
|
]);
|
|
95
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Assemble a sequence of JPEG frames into an H.264 MP4.
|
|
168
|
+
* Expects frames named `frame_000000.jpg`, `frame_000001.jpg`, ... in framesDir.
|
|
169
|
+
*
|
|
170
|
+
* When `frameOffsetsMs` is provided (one offset per frame, ms from first
|
|
171
|
+
* frame), uses ffmpeg's concat demuxer to preserve the actual capture timing
|
|
172
|
+
* (VFR). This matters when the compositor is CPU-bound on heavy React UIs:
|
|
173
|
+
* frames arrive in bursts during idle periods and gaps during paint-heavy
|
|
174
|
+
* periods. Encoding those at uniform CFR would stretch bursts and compress
|
|
175
|
+
* gaps — viewers see temporally-distorted playback even though the average
|
|
176
|
+
* rate is correct. VFR lets each frame hold the screen for its true wall-
|
|
177
|
+
* clock duration.
|
|
178
|
+
*
|
|
179
|
+
* When `frameOffsetsMs` is absent, falls back to CFR at `fps` (useful for
|
|
180
|
+
* uniform-cadence captures like the synthetic smoke tests).
|
|
181
|
+
*/
|
|
182
|
+
export async function assembleMp4FromFrames(opts) {
|
|
183
|
+
const ffmpeg = await ensureFfmpegAvailable();
|
|
184
|
+
const fps = Number.isFinite(opts.fps) && opts.fps >= 1 ? opts.fps : 25;
|
|
185
|
+
if (opts.frameOffsetsMs && opts.frameOffsetsMs.length >= 2) {
|
|
186
|
+
// VFR path: generate a concat file with per-frame durations.
|
|
187
|
+
const offsets = opts.frameOffsetsMs;
|
|
188
|
+
const lines = [];
|
|
189
|
+
for (let i = 0; i < offsets.length; i++) {
|
|
190
|
+
const durationSec = i < offsets.length - 1
|
|
191
|
+
? Math.max(0.001, (offsets[i + 1] - offsets[i]) / 1000)
|
|
192
|
+
: 1 / fps; // last frame duration = 1 / avg fps
|
|
193
|
+
lines.push(`file 'frame_${String(i).padStart(6, '0')}.jpg'`);
|
|
194
|
+
lines.push(`duration ${durationSec.toFixed(6)}`);
|
|
195
|
+
}
|
|
196
|
+
// ffmpeg concat demuxer quirk: last `file` entry is needed without a
|
|
197
|
+
// `duration` directive to close the list correctly.
|
|
198
|
+
lines.push(`file 'frame_${String(offsets.length - 1).padStart(6, '0')}.jpg'`);
|
|
199
|
+
const concatPath = path.join(opts.framesDir, 'concat.txt');
|
|
200
|
+
await (await import('node:fs/promises')).writeFile(concatPath, lines.join('\n'));
|
|
201
|
+
await execFileAsync(ffmpeg, [
|
|
202
|
+
'-f', 'concat',
|
|
203
|
+
'-safe', '0',
|
|
204
|
+
'-i', concatPath,
|
|
205
|
+
'-fps_mode', 'vfr',
|
|
206
|
+
'-c:v', 'libx264',
|
|
207
|
+
'-preset', 'medium',
|
|
208
|
+
'-crf', '16',
|
|
209
|
+
'-pix_fmt', 'yuv420p',
|
|
210
|
+
'-movflags', '+faststart',
|
|
211
|
+
'-an',
|
|
212
|
+
'-y', opts.outputPath,
|
|
213
|
+
]);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// CFR fallback (uniform framerate).
|
|
217
|
+
const pattern = path.join(opts.framesDir, 'frame_%06d.jpg');
|
|
218
|
+
await execFileAsync(ffmpeg, [
|
|
219
|
+
'-framerate', String(fps),
|
|
220
|
+
'-i', pattern,
|
|
221
|
+
'-c:v', 'libx264',
|
|
222
|
+
'-preset', 'medium',
|
|
223
|
+
'-crf', '16',
|
|
224
|
+
'-pix_fmt', 'yuv420p',
|
|
225
|
+
'-movflags', '+faststart',
|
|
226
|
+
'-an',
|
|
227
|
+
'-y', opts.outputPath,
|
|
228
|
+
]);
|
|
229
|
+
}
|
|
96
230
|
/**
|
|
97
231
|
* Trim a recording: skip dead frames at the start and cap duration.
|
|
98
232
|
*/
|
|
@@ -110,8 +244,7 @@ export async function trimRecording(inputPath, outputPath, startSec = DEFAULT_TR
|
|
|
110
244
|
* Get the duration of a media file in milliseconds.
|
|
111
245
|
*/
|
|
112
246
|
export async function getMediaDurationMs(filePath) {
|
|
113
|
-
const
|
|
114
|
-
const ffprobe = ffmpeg.replace(/ffmpeg$/, 'ffprobe');
|
|
247
|
+
const ffprobe = await ensureBinaryAvailable('ffprobe');
|
|
115
248
|
const { stdout } = await execFileAsync(ffprobe, [
|
|
116
249
|
'-v', 'error',
|
|
117
250
|
'-show_entries', 'format=duration',
|
|
@@ -139,54 +272,47 @@ async function freezeLastFrame(inputPath, outputPath, durationSec) {
|
|
|
139
272
|
}
|
|
140
273
|
/**
|
|
141
274
|
* Full post-processing pipeline for a single clip recording:
|
|
142
|
-
*
|
|
275
|
+
* encode a normalized master MP4 → derive GIF + thumbnail → measure.
|
|
143
276
|
*/
|
|
144
277
|
export async function postProcessClipRecording(webmPath, outputDir, clipId, options = {}) {
|
|
145
278
|
const maxDuration = options.maxDurationSec ?? DEFAULT_MAX_DURATION_SEC;
|
|
146
|
-
// Step 1: Trim the recording — cut the setup phase (page load, overlay dismiss)
|
|
147
279
|
const trimStart = options.trimStartSec ?? DEFAULT_TRIM_START_SEC;
|
|
148
|
-
const trimmedPath = path.join(outputDir, `${clipId}_trimmed.webm`);
|
|
149
|
-
await trimRecording(webmPath, trimmedPath, trimStart, maxDuration);
|
|
150
|
-
// Step 1b: Optionally freeze the last frame for a pause before looping
|
|
151
280
|
const holdSec = Math.min(Math.max(options.holdLastFrameSec ?? 0, 0), 10);
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
281
|
+
const mp4Path = path.join(outputDir, `${clipId}.mp4`);
|
|
282
|
+
if (options.skipMp4Encode) {
|
|
283
|
+
// Source is already an H.264 MP4 produced client-side by ClipCaptureLoop.
|
|
284
|
+
// Copy it under the canonical name so the downstream GIF/thumbnail steps
|
|
285
|
+
// get a stable path. Skip re-encode to preserve quality and avoid ~1-2s
|
|
286
|
+
// of ffmpeg CPU.
|
|
287
|
+
await copyFile(webmPath, mp4Path);
|
|
157
288
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
289
|
+
else {
|
|
290
|
+
await convertToMp4(webmPath, mp4Path, {
|
|
291
|
+
width: options.mp4Width,
|
|
292
|
+
height: options.mp4Height,
|
|
293
|
+
trimStartSec: trimStart,
|
|
294
|
+
maxDurationSec: maxDuration,
|
|
295
|
+
holdLastFrameSec: holdSec,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const durationMs = await getMediaDurationMs(mp4Path);
|
|
299
|
+
const mp4Stat = await stat(mp4Path);
|
|
300
|
+
const result = {
|
|
301
|
+
durationMs,
|
|
302
|
+
mp4Path,
|
|
303
|
+
fileSizeBytes: mp4Stat.size,
|
|
304
|
+
};
|
|
163
305
|
const gifPath = path.join(outputDir, `${clipId}.gif`);
|
|
164
|
-
await convertToGif(
|
|
306
|
+
await convertToGif(mp4Path, gifPath, {
|
|
165
307
|
fps: options.gifFps,
|
|
166
308
|
maxWidth: options.gifMaxWidth,
|
|
167
309
|
loop: options.loop,
|
|
168
310
|
});
|
|
169
311
|
result.gifPath = gifPath;
|
|
170
|
-
const gifStat = await stat(gifPath);
|
|
171
|
-
result.fileSizeBytes = gifStat.size;
|
|
172
|
-
const mp4Path = path.join(outputDir, `${clipId}.mp4`);
|
|
173
|
-
await convertToMp4(sourcePath, mp4Path);
|
|
174
|
-
result.mp4Path = mp4Path;
|
|
175
312
|
// Step 3: Extract thumbnail
|
|
176
313
|
const thumbnailPath = path.join(outputDir, `${clipId}_thumb.png`);
|
|
177
|
-
await extractThumbnail(
|
|
314
|
+
await extractThumbnail(mp4Path, thumbnailPath);
|
|
178
315
|
result.thumbnailPath = thumbnailPath;
|
|
179
|
-
// Clean up intermediate files
|
|
180
|
-
try {
|
|
181
|
-
await execFileAsync('rm', [trimmedPath]);
|
|
182
|
-
}
|
|
183
|
-
catch { /* ignore */ }
|
|
184
|
-
if (sourcePath !== trimmedPath) {
|
|
185
|
-
try {
|
|
186
|
-
await execFileAsync('rm', [sourcePath]);
|
|
187
|
-
}
|
|
188
|
-
catch { /* ignore */ }
|
|
189
|
-
}
|
|
190
316
|
return result;
|
|
191
317
|
}
|
|
192
318
|
//# sourceMappingURL=clip-postprocess.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ClipCaptureMethod } from './execution-types.js';
|
|
2
|
+
export declare const CLIP_BACKEND_ENV_VAR = "AUTOKAP_CLIP_BACKEND";
|
|
3
|
+
export declare const CLIP_HEADLESS_POLICY_ENV_VAR = "AUTOKAP_CLIP_HEADLESS_POLICY";
|
|
4
|
+
export type ClipHeadlessPolicy = 'auto' | 'headless' | 'headed';
|
|
5
|
+
export interface ClipRuntimeSelection {
|
|
6
|
+
requestedMethod: ClipCaptureMethod;
|
|
7
|
+
captureMethod: ClipCaptureMethod;
|
|
8
|
+
browserHeaded: boolean;
|
|
9
|
+
headlessPolicy: ClipHeadlessPolicy;
|
|
10
|
+
fallbackReason?: string;
|
|
11
|
+
browserLaunchArgs?: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveRequestedClipCaptureMethod(): ClipCaptureMethod;
|
|
14
|
+
export declare function resolveClipHeadlessPolicy(): ClipHeadlessPolicy;
|
|
15
|
+
export declare function resolveClipRuntimeSelection(options?: {
|
|
16
|
+
headed?: boolean;
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
}): Promise<ClipRuntimeSelection>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const CLIP_BACKEND_ENV_VAR = 'AUTOKAP_CLIP_BACKEND';
|
|
2
|
+
export const CLIP_HEADLESS_POLICY_ENV_VAR = 'AUTOKAP_CLIP_HEADLESS_POLICY';
|
|
3
|
+
const BEGIN_FRAME_LAUNCH_ARGS = [
|
|
4
|
+
'--enable-begin-frame-control',
|
|
5
|
+
'--run-all-compositor-stages-before-draw',
|
|
6
|
+
];
|
|
7
|
+
function normalizeClipCaptureMethod(raw) {
|
|
8
|
+
switch (raw?.trim().toLowerCase()) {
|
|
9
|
+
case 'begin_frame':
|
|
10
|
+
case 'frame_sequence':
|
|
11
|
+
case 'playwright_video':
|
|
12
|
+
return raw.trim().toLowerCase();
|
|
13
|
+
default:
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function normalizeHeadlessPolicy(raw) {
|
|
18
|
+
switch (raw?.trim().toLowerCase()) {
|
|
19
|
+
case 'headless':
|
|
20
|
+
return 'headless';
|
|
21
|
+
case 'headed':
|
|
22
|
+
return 'headed';
|
|
23
|
+
default:
|
|
24
|
+
return 'auto';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function resolveRequestedClipCaptureMethod() {
|
|
28
|
+
return normalizeClipCaptureMethod(process.env[CLIP_BACKEND_ENV_VAR]) ?? 'begin_frame';
|
|
29
|
+
}
|
|
30
|
+
export function resolveClipHeadlessPolicy() {
|
|
31
|
+
return normalizeHeadlessPolicy(process.env[CLIP_HEADLESS_POLICY_ENV_VAR]);
|
|
32
|
+
}
|
|
33
|
+
export async function resolveClipRuntimeSelection(options = {}) {
|
|
34
|
+
const requestedMethod = resolveRequestedClipCaptureMethod();
|
|
35
|
+
const headlessPolicy = resolveClipHeadlessPolicy();
|
|
36
|
+
const platform = options.platform ?? process.platform;
|
|
37
|
+
const browserHeaded = headlessPolicy === 'headed'
|
|
38
|
+
? true
|
|
39
|
+
: headlessPolicy === 'headless'
|
|
40
|
+
? false
|
|
41
|
+
: options.headed ?? false;
|
|
42
|
+
let captureMethod = requestedMethod;
|
|
43
|
+
let fallbackReason;
|
|
44
|
+
let browserLaunchArgs;
|
|
45
|
+
if (requestedMethod === 'begin_frame') {
|
|
46
|
+
if (platform === 'darwin') {
|
|
47
|
+
captureMethod = 'frame_sequence';
|
|
48
|
+
fallbackReason = 'begin_frame is unsupported on macOS Chromium targets; falling back to frame sequence capture';
|
|
49
|
+
}
|
|
50
|
+
else if (browserHeaded) {
|
|
51
|
+
captureMethod = 'frame_sequence';
|
|
52
|
+
fallbackReason = 'begin_frame requires headless Chromium; falling back to frame sequence capture';
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
browserLaunchArgs = [...BEGIN_FRAME_LAUNCH_ARGS];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
requestedMethod,
|
|
60
|
+
captureMethod,
|
|
61
|
+
browserHeaded,
|
|
62
|
+
headlessPolicy,
|
|
63
|
+
fallbackReason,
|
|
64
|
+
browserLaunchArgs,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=clip-runtime.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_CAPTURE_OUTPUT_SCALE = 2;
|
|
2
|
+
export declare function normalizeCaptureOutputScale(value?: number | null): number | null;
|
|
3
|
+
export declare function resolveEffectiveCaptureOutputScale(requestedScale?: number | null, fallbackScale?: number): number;
|
|
4
|
+
export declare function resolvePhysicalCaptureSize(viewport: {
|
|
5
|
+
width: number;
|
|
6
|
+
height: number;
|
|
7
|
+
} | null, scale: number | null | undefined): {
|
|
8
|
+
width: number;
|
|
9
|
+
height: number;
|
|
10
|
+
} | null;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const DEFAULT_CAPTURE_OUTPUT_SCALE = 2;
|
|
2
|
+
export function normalizeCaptureOutputScale(value) {
|
|
3
|
+
if (!Number.isFinite(value))
|
|
4
|
+
return null;
|
|
5
|
+
return Math.max(0.5, Math.min(4, Number(value)));
|
|
6
|
+
}
|
|
7
|
+
export function resolveEffectiveCaptureOutputScale(requestedScale, fallbackScale = DEFAULT_CAPTURE_OUTPUT_SCALE) {
|
|
8
|
+
return normalizeCaptureOutputScale(requestedScale)
|
|
9
|
+
?? normalizeCaptureOutputScale(fallbackScale)
|
|
10
|
+
?? DEFAULT_CAPTURE_OUTPUT_SCALE;
|
|
11
|
+
}
|
|
12
|
+
export function resolvePhysicalCaptureSize(viewport, scale) {
|
|
13
|
+
if (!viewport)
|
|
14
|
+
return null;
|
|
15
|
+
const effectiveScale = resolveEffectiveCaptureOutputScale(scale);
|
|
16
|
+
return {
|
|
17
|
+
width: Math.max(2, Math.round(viewport.width * effectiveScale)) & ~1,
|
|
18
|
+
height: Math.max(2, Math.round(viewport.height * effectiveScale)) & ~1,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=clip-scale.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Page } from 'playwright';
|
|
2
|
+
import type { RecordingResult } from './execution-types.js';
|
|
3
|
+
import type { ClipOptions } from './types.js';
|
|
4
|
+
export interface CdpScreencastClipRecorderOptions {
|
|
5
|
+
baseDir?: string;
|
|
6
|
+
viewport: {
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
};
|
|
10
|
+
requestedScale?: number;
|
|
11
|
+
effectiveScale: number;
|
|
12
|
+
clipOptions?: ClipOptions;
|
|
13
|
+
sourceFps?: number;
|
|
14
|
+
outputFps?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class CdpScreencastClipRecorder {
|
|
17
|
+
private readonly page;
|
|
18
|
+
private readonly options;
|
|
19
|
+
private readonly sourceFps;
|
|
20
|
+
private readonly outputFps;
|
|
21
|
+
private readonly workingDirPromise;
|
|
22
|
+
private readonly frames;
|
|
23
|
+
private readonly startedAt;
|
|
24
|
+
private finalResult;
|
|
25
|
+
private frameDimensions;
|
|
26
|
+
private pendingWriteError;
|
|
27
|
+
private readonly pendingWrites;
|
|
28
|
+
private cdpSession;
|
|
29
|
+
private stopRequested;
|
|
30
|
+
private started;
|
|
31
|
+
constructor(page: Page, options: CdpScreencastClipRecorderOptions);
|
|
32
|
+
start(): Promise<void>;
|
|
33
|
+
stop(): Promise<RecordingResult>;
|
|
34
|
+
abort(): Promise<void>;
|
|
35
|
+
private captureInitialFrame;
|
|
36
|
+
private handleScreencastFrame;
|
|
37
|
+
private consumeFrame;
|
|
38
|
+
private queueFrameWrite;
|
|
39
|
+
private waitForPendingWriteCapacity;
|
|
40
|
+
private flushPendingWrites;
|
|
41
|
+
private cleanup;
|
|
42
|
+
}
|