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.
Files changed (301) hide show
  1. package/assets/cursors/macos.svg +4 -0
  2. package/assets/cursors/windows.svg +15 -0
  3. package/assets/skill/OPCODE-REFERENCE.md +635 -0
  4. package/assets/skill/README.md +39 -0
  5. package/assets/skill/SKILL.md +454 -468
  6. package/assets/skill/STUDIO-SKILL.md +476 -0
  7. package/assets/skill/references/examples.md +104 -0
  8. package/assets/skill/references/interactive-demo.md +225 -0
  9. package/assets/skill/references/mock-data.md +178 -0
  10. package/dist/action-verifier.d.ts +29 -0
  11. package/dist/action-verifier.js +133 -0
  12. package/dist/agent-action-recovery.d.ts +45 -0
  13. package/dist/agent-action-recovery.js +370 -0
  14. package/dist/agent-message-utils.d.ts +21 -0
  15. package/dist/agent-message-utils.js +77 -0
  16. package/dist/agent-url-utils.d.ts +30 -0
  17. package/dist/agent-url-utils.js +138 -0
  18. package/dist/agent.d.ts +92 -8
  19. package/dist/agent.js +2936 -781
  20. package/dist/ak-tree.d.ts +39 -0
  21. package/dist/ak-tree.js +368 -0
  22. package/dist/alt-text.d.ts +26 -0
  23. package/dist/alt-text.js +55 -0
  24. package/dist/auth-capture.d.ts +17 -0
  25. package/dist/auth-capture.js +197 -0
  26. package/dist/benchmark.d.ts +59 -0
  27. package/dist/benchmark.js +135 -0
  28. package/dist/billing-operation-logging.d.ts +3 -1
  29. package/dist/billing-operation-logging.js +4 -0
  30. package/dist/browser-bar.d.ts +14 -6
  31. package/dist/browser-bar.js +145 -8
  32. package/dist/browser-pool.d.ts +7 -0
  33. package/dist/browser-pool.js +15 -5
  34. package/dist/browser-utils.d.ts +31 -0
  35. package/dist/browser-utils.js +97 -0
  36. package/dist/browser.d.ts +61 -11
  37. package/dist/browser.js +1513 -59
  38. package/dist/capture-alt-text.js +2 -1
  39. package/dist/capture-encryption.d.ts +3 -1
  40. package/dist/capture-encryption.js +21 -6
  41. package/dist/capture-language-preflight.js +14 -0
  42. package/dist/capture-llm-page-identity.js +22 -10
  43. package/dist/capture-page-identity.d.ts +5 -7
  44. package/dist/capture-page-identity.js +211 -78
  45. package/dist/capture-preset-credentials.d.ts +50 -0
  46. package/dist/capture-preset-credentials.js +127 -0
  47. package/dist/capture-request-plan.d.ts +2 -2
  48. package/dist/capture-request-plan.js +64 -16
  49. package/dist/capture-run-optimizer.js +48 -33
  50. package/dist/capture-selector-memory.d.ts +5 -0
  51. package/dist/capture-selector-memory.js +18 -0
  52. package/dist/capture-strategy.d.ts +36 -0
  53. package/dist/capture-strategy.js +96 -0
  54. package/dist/capture-studio-sync.d.ts +1 -0
  55. package/dist/capture-studio-sync.js +9 -3
  56. package/dist/capture-surface-contract.d.ts +36 -0
  57. package/dist/capture-surface-contract.js +299 -0
  58. package/dist/capture-transition-engine.d.ts +28 -0
  59. package/dist/capture-transition-engine.js +292 -0
  60. package/dist/capture-variant-state.d.ts +2 -0
  61. package/dist/capture-variant-state.js +26 -0
  62. package/dist/capture-verification.d.ts +35 -0
  63. package/dist/capture-verification.js +95 -0
  64. package/dist/capture-viewport-lock.d.ts +48 -0
  65. package/dist/capture-viewport-lock.js +74 -0
  66. package/dist/circuit-breaker.d.ts +42 -0
  67. package/dist/circuit-breaker.js +119 -0
  68. package/dist/cli-config.d.ts +9 -1
  69. package/dist/cli-config.js +112 -7
  70. package/dist/cli-contract.d.ts +19 -0
  71. package/dist/cli-contract.js +173 -0
  72. package/dist/cli-runner-local.d.ts +12 -0
  73. package/dist/cli-runner-local.js +115 -0
  74. package/dist/cli-runner.d.ts +34 -0
  75. package/dist/cli-runner.js +580 -0
  76. package/dist/cli-utils.d.ts +0 -1
  77. package/dist/cli-utils.js +2 -5
  78. package/dist/cli.js +1011 -267
  79. package/dist/clip-begin-frame-recorder.d.ts +44 -0
  80. package/dist/clip-begin-frame-recorder.js +250 -0
  81. package/dist/clip-capture-backend.d.ts +25 -0
  82. package/dist/clip-capture-backend.js +189 -0
  83. package/dist/clip-capture-loop.d.ts +61 -0
  84. package/dist/clip-capture-loop.js +111 -0
  85. package/dist/clip-frame-recorder.d.ts +63 -0
  86. package/dist/clip-frame-recorder.js +305 -0
  87. package/dist/clip-orchestrator.js +9 -2
  88. package/dist/clip-postprocess.d.ts +31 -2
  89. package/dist/clip-postprocess.js +194 -68
  90. package/dist/clip-runtime.d.ts +18 -0
  91. package/dist/clip-runtime.js +67 -0
  92. package/dist/clip-scale.d.ts +10 -0
  93. package/dist/clip-scale.js +21 -0
  94. package/dist/clip-screencast-recorder.d.ts +42 -0
  95. package/dist/clip-screencast-recorder.js +242 -0
  96. package/dist/clip-sidecar.d.ts +54 -0
  97. package/dist/clip-sidecar.js +208 -0
  98. package/dist/cookie-dismiss.d.ts +2 -0
  99. package/dist/cookie-dismiss.js +48 -13
  100. package/dist/cost-logging.d.ts +8 -0
  101. package/dist/cost-logging.js +160 -46
  102. package/dist/cost-resolution-monitor.d.ts +16 -0
  103. package/dist/cost-resolution-monitor.js +34 -0
  104. package/dist/credential-templates.js +2 -2
  105. package/dist/cursor-overlay-script.d.ts +6 -0
  106. package/dist/cursor-overlay-script.js +169 -0
  107. package/dist/dom-css-purger.d.ts +65 -0
  108. package/dist/dom-css-purger.js +333 -0
  109. package/dist/dom-font-inliner.d.ts +45 -0
  110. package/dist/dom-font-inliner.js +148 -0
  111. package/dist/dom-patch-resolver.d.ts +52 -0
  112. package/dist/dom-patch-resolver.js +242 -0
  113. package/dist/dom-serializer.d.ts +82 -0
  114. package/dist/dom-serializer.js +378 -0
  115. package/dist/element-capture.d.ts +1 -41
  116. package/dist/element-capture.js +202 -446
  117. package/dist/env-validation.d.ts +5 -0
  118. package/dist/env-validation.js +63 -0
  119. package/dist/execution-schema.d.ts +4753 -0
  120. package/dist/execution-schema.js +563 -0
  121. package/dist/execution-types.d.ts +936 -0
  122. package/dist/execution-types.js +66 -0
  123. package/dist/fonts-loader.d.ts +14 -0
  124. package/dist/fonts-loader.js +55 -0
  125. package/dist/hybrid-navigator.js +12 -12
  126. package/dist/index.d.ts +11 -6
  127. package/dist/index.js +11 -4
  128. package/dist/legacy/agent-action-recovery.d.ts +45 -0
  129. package/dist/legacy/agent-action-recovery.js +370 -0
  130. package/dist/legacy/agent-message-utils.d.ts +21 -0
  131. package/dist/legacy/agent-message-utils.js +77 -0
  132. package/dist/legacy/agent-url-utils.d.ts +30 -0
  133. package/dist/legacy/agent-url-utils.js +138 -0
  134. package/dist/legacy/agent.d.ts +226 -0
  135. package/dist/legacy/agent.js +6666 -0
  136. package/dist/legacy/clip-orchestrator.d.ts +148 -0
  137. package/dist/legacy/clip-orchestrator.js +957 -0
  138. package/dist/legacy/credential-templates.d.ts +5 -0
  139. package/dist/legacy/credential-templates.js +60 -0
  140. package/dist/legacy/hybrid-navigator.d.ts +138 -0
  141. package/dist/legacy/hybrid-navigator.js +468 -0
  142. package/dist/legacy/llm-usage.d.ts +17 -0
  143. package/dist/legacy/llm-usage.js +45 -0
  144. package/dist/legacy/prompt-cache.d.ts +10 -0
  145. package/dist/legacy/prompt-cache.js +24 -0
  146. package/dist/legacy/prompts.d.ts +175 -0
  147. package/dist/legacy/prompts.js +1038 -0
  148. package/dist/legacy/tools.d.ts +4 -0
  149. package/dist/legacy/tools.js +216 -0
  150. package/dist/legacy/video-agent.d.ts +143 -0
  151. package/dist/legacy/video-agent.js +4788 -0
  152. package/dist/legacy/video-observation.d.ts +36 -0
  153. package/dist/legacy/video-observation.js +192 -0
  154. package/dist/legacy/video-planner.d.ts +12 -0
  155. package/dist/legacy/video-planner.js +501 -0
  156. package/dist/legacy/video-prompts.d.ts +37 -0
  157. package/dist/legacy/video-prompts.js +569 -0
  158. package/dist/legacy/video-tools.d.ts +3 -0
  159. package/dist/legacy/video-tools.js +59 -0
  160. package/dist/legacy/video-variant-state.d.ts +29 -0
  161. package/dist/legacy/video-variant-state.js +80 -0
  162. package/dist/legacy/vision-model.d.ts +17 -0
  163. package/dist/legacy/vision-model.js +74 -0
  164. package/dist/llm-healer.d.ts +55 -0
  165. package/dist/llm-healer.js +213 -0
  166. package/dist/llm-provider.d.ts +29 -0
  167. package/dist/llm-provider.js +83 -0
  168. package/dist/logger.d.ts +6 -2
  169. package/dist/logger.js +15 -1
  170. package/dist/mockup-html.js +35 -25
  171. package/dist/mockup.d.ts +95 -2
  172. package/dist/mockup.js +427 -166
  173. package/dist/mouse-animation.d.ts +2 -2
  174. package/dist/mouse-animation.js +34 -20
  175. package/dist/opcode-actions.d.ts +42 -0
  176. package/dist/opcode-actions.js +524 -0
  177. package/dist/opcode-runner.d.ts +51 -0
  178. package/dist/opcode-runner.js +779 -0
  179. package/dist/openrouter-client.d.ts +40 -0
  180. package/dist/openrouter-client.js +16 -0
  181. package/dist/overlay-engine.d.ts +24 -0
  182. package/dist/overlay-engine.js +176 -0
  183. package/dist/postcondition.d.ts +16 -0
  184. package/dist/postcondition.js +269 -0
  185. package/dist/program-patcher.d.ts +25 -0
  186. package/dist/program-patcher.js +44 -0
  187. package/dist/program-signing.d.ts +1094 -0
  188. package/dist/program-signing.js +140 -0
  189. package/dist/prompts.d.ts +13 -5
  190. package/dist/prompts.js +224 -351
  191. package/dist/provider-config.d.ts +17 -0
  192. package/dist/provider-config.js +42 -0
  193. package/dist/recovery-chain.d.ts +37 -0
  194. package/dist/recovery-chain.js +374 -0
  195. package/dist/remote-browser.d.ts +28 -4
  196. package/dist/remote-browser.js +60 -5
  197. package/dist/safari-browser-bar.d.ts +15 -0
  198. package/dist/safari-browser-bar.js +95 -0
  199. package/dist/safari-toolbar-asset.d.ts +15 -0
  200. package/dist/safari-toolbar-asset.js +12 -0
  201. package/dist/security.d.ts +2 -1
  202. package/dist/security.js +49 -10
  203. package/dist/selector-resolver.d.ts +34 -0
  204. package/dist/selector-resolver.js +181 -0
  205. package/dist/semantic-resolver.d.ts +35 -0
  206. package/dist/semantic-resolver.js +161 -0
  207. package/dist/server-capture-runtime.d.ts +5 -3
  208. package/dist/server-capture-runtime.js +42 -95
  209. package/dist/server-credit-usage.d.ts +2 -2
  210. package/dist/server-project-webhooks.d.ts +15 -1
  211. package/dist/server-project-webhooks.js +34 -8
  212. package/dist/server-screenshot-watermark.js +27 -5
  213. package/dist/session-profile.js +164 -1
  214. package/dist/sf-pro-symbols.d.ts +1 -0
  215. package/dist/sf-pro-symbols.js +55 -0
  216. package/dist/skill-packaging.d.ts +28 -0
  217. package/dist/skill-packaging.js +169 -0
  218. package/dist/smart-wait.d.ts +27 -0
  219. package/dist/smart-wait.js +81 -0
  220. package/dist/status-bar-render.d.ts +20 -0
  221. package/dist/status-bar-render.js +410 -0
  222. package/dist/status-bar.d.ts +9 -0
  223. package/dist/status-bar.js +298 -14
  224. package/dist/svg-browser-bar.d.ts +33 -0
  225. package/dist/svg-browser-bar.js +206 -0
  226. package/dist/svg-status-bar.d.ts +36 -0
  227. package/dist/svg-status-bar.js +597 -0
  228. package/dist/svg-text.d.ts +61 -0
  229. package/dist/svg-text.js +118 -0
  230. package/dist/tools.js +89 -451
  231. package/dist/types.d.ts +248 -7
  232. package/dist/types.js +23 -1
  233. package/dist/v2/action-verifier.d.ts +29 -0
  234. package/dist/v2/action-verifier.js +133 -0
  235. package/dist/v2/alt-text.d.ts +26 -0
  236. package/dist/v2/alt-text.js +55 -0
  237. package/dist/v2/benchmark.d.ts +59 -0
  238. package/dist/v2/benchmark.js +135 -0
  239. package/dist/v2/capture-strategy.d.ts +30 -0
  240. package/dist/v2/capture-strategy.js +67 -0
  241. package/dist/v2/capture-verification.d.ts +35 -0
  242. package/dist/v2/capture-verification.js +95 -0
  243. package/dist/v2/circuit-breaker.d.ts +42 -0
  244. package/dist/v2/circuit-breaker.js +119 -0
  245. package/dist/v2/cli-runner-local.d.ts +11 -0
  246. package/dist/v2/cli-runner-local.js +91 -0
  247. package/dist/v2/cli-runner.d.ts +34 -0
  248. package/dist/v2/cli-runner.js +300 -0
  249. package/dist/v2/compiler-prompts.d.ts +27 -0
  250. package/dist/v2/compiler-prompts.js +123 -0
  251. package/dist/v2/compiler.d.ts +37 -0
  252. package/dist/v2/compiler.js +147 -0
  253. package/dist/v2/explorer.d.ts +41 -0
  254. package/dist/v2/explorer.js +56 -0
  255. package/dist/v2/index.d.ts +37 -0
  256. package/dist/v2/index.js +31 -0
  257. package/dist/v2/llm-healer.d.ts +62 -0
  258. package/dist/v2/llm-healer.js +166 -0
  259. package/dist/v2/llm-provider.d.ts +29 -0
  260. package/dist/v2/llm-provider.js +80 -0
  261. package/dist/v2/opcode-runner.d.ts +47 -0
  262. package/dist/v2/opcode-runner.js +634 -0
  263. package/dist/v2/overlay-engine.d.ts +24 -0
  264. package/dist/v2/overlay-engine.js +150 -0
  265. package/dist/v2/postcondition.d.ts +16 -0
  266. package/dist/v2/postcondition.js +249 -0
  267. package/dist/v2/program-patcher.d.ts +25 -0
  268. package/dist/v2/program-patcher.js +44 -0
  269. package/dist/v2/recovery-chain.d.ts +30 -0
  270. package/dist/v2/recovery-chain.js +368 -0
  271. package/dist/v2/schema.d.ts +2580 -0
  272. package/dist/v2/schema.js +295 -0
  273. package/dist/v2/selector-resolver.d.ts +34 -0
  274. package/dist/v2/selector-resolver.js +181 -0
  275. package/dist/v2/semantic-resolver.d.ts +35 -0
  276. package/dist/v2/semantic-resolver.js +161 -0
  277. package/dist/v2/smart-wait.d.ts +27 -0
  278. package/dist/v2/smart-wait.js +81 -0
  279. package/dist/v2/types.d.ts +444 -0
  280. package/dist/v2/types.js +19 -0
  281. package/dist/v2/web-playwright-local.d.ts +69 -0
  282. package/dist/v2/web-playwright-local.js +392 -0
  283. package/dist/version.d.ts +1 -0
  284. package/dist/version.js +5 -0
  285. package/dist/video-agent.js +18 -13
  286. package/dist/video-planner.js +2 -1
  287. package/dist/video-prompts.js +3 -3
  288. package/dist/web-playwright-local.d.ts +156 -0
  289. package/dist/web-playwright-local.js +989 -0
  290. package/dist/ws-auth.js +4 -1
  291. package/dist/ws-broadcast.d.ts +34 -0
  292. package/dist/ws-broadcast.js +85 -0
  293. package/dist/ws-connection-limits.d.ts +12 -0
  294. package/dist/ws-connection-limits.js +44 -0
  295. package/dist/ws-handler-utils.d.ts +32 -0
  296. package/dist/ws-handler-utils.js +139 -0
  297. package/dist/ws-handler.js +294 -164
  298. package/dist/ws-metrics-server.d.ts +9 -0
  299. package/dist/ws-metrics-server.js +31 -0
  300. package/dist/ws-server.js +41 -1
  301. package/package.json +62 -35
@@ -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 = 15;
8
- const DEFAULT_GIF_MAX_WIDTH = 800;
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
- let cachedFfmpegPath = null;
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
- const { stdout } = await execFileAsync('which', ['ffmpeg']);
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) or apt install ffmpeg (Linux).');
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
- // Pass 1: Generate palette
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
- '-vf', `${scaleFilter},palettegen=stats_mode=diff`,
49
- '-y', palettePath,
66
+ '-lavfi', lavfi,
67
+ '-loop', loopFlag,
68
+ '-y', outputPath,
50
69
  ]);
51
- // Pass 2: Encode GIF with palette
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(ffmpeg, [
54
- '-i', webmPath,
55
- '-i', palettePath,
56
- '-lavfi', `${scaleFilter}[x];[x][1:v]paletteuse=dither=bayer:bayer_scale=3`,
57
- '-loop', loopFlag,
58
- '-y', outputPath,
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
- finally {
62
- // Clean up palette file
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', '22',
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 ffmpeg = await ensureFfmpegAvailable();
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
- * trim GIF and/or MP4 → thumbnail → measure.
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
- let sourcePath = trimmedPath;
153
- if (holdSec > 0) {
154
- const paddedPath = path.join(outputDir, `${clipId}_padded.webm`);
155
- await freezeLastFrame(trimmedPath, paddedPath, holdSec);
156
- sourcePath = paddedPath;
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
- const durationMs = await getMediaDurationMs(sourcePath);
159
- const result = { durationMs };
160
- // Step 2: Always persist both a GIF and an MP4.
161
- // The GIF remains useful for delivery and embeds, while the MP4 is the
162
- // high-fidelity source for Studio playback and downstream re-renders.
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(sourcePath, gifPath, {
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(sourcePath, thumbnailPath);
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
+ }