rollberry 0.1.8 → 0.2.0

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 (45) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +189 -7
  3. package/dist/capture/actions.d.ts +11 -0
  4. package/dist/capture/actions.js +81 -0
  5. package/dist/capture/browser-install.d.ts +3 -0
  6. package/dist/capture/browser-install.js +1 -1
  7. package/dist/capture/browser.d.ts +13 -0
  8. package/dist/capture/browser.js +16 -10
  9. package/dist/capture/capture.d.ts +8 -0
  10. package/dist/capture/capture.js +566 -55
  11. package/dist/capture/constants.d.ts +15 -0
  12. package/dist/capture/constants.js +7 -0
  13. package/dist/capture/ffmpeg.d.ts +41 -0
  14. package/dist/capture/ffmpeg.js +457 -36
  15. package/dist/capture/logger.d.ts +15 -0
  16. package/dist/capture/preflight.d.ts +4 -0
  17. package/dist/capture/preflight.js +8 -2
  18. package/dist/capture/progress.d.ts +7 -0
  19. package/dist/capture/progress.js +50 -0
  20. package/dist/capture/scroll-plan.d.ts +9 -0
  21. package/dist/capture/scroll-plan.js +7 -1
  22. package/dist/capture/stabilize.d.ts +7 -0
  23. package/dist/capture/stabilize.js +11 -5
  24. package/dist/capture/types.d.ts +216 -0
  25. package/dist/capture/utils.d.ts +14 -0
  26. package/dist/capture/utils.js +30 -3
  27. package/dist/cli.d.ts +2 -0
  28. package/dist/cli.js +86 -5
  29. package/dist/index.d.ts +7 -0
  30. package/dist/index.js +6 -0
  31. package/dist/options.d.ts +42 -0
  32. package/dist/options.js +229 -115
  33. package/dist/project.d.ts +27 -0
  34. package/dist/project.js +722 -0
  35. package/dist/render-plan.d.ts +103 -0
  36. package/dist/render-plan.js +144 -0
  37. package/dist/run-capture.d.ts +3 -0
  38. package/dist/run-capture.js +20 -10
  39. package/dist/run-render.d.ts +17 -0
  40. package/dist/run-render.js +434 -0
  41. package/dist/version.d.ts +1 -0
  42. package/dist/version.js +1 -0
  43. package/package.json +10 -3
  44. package/rollberry.project.sample.json +92 -0
  45. package/rollberry.project.schema.json +474 -0
package/CHANGELOG.md CHANGED
@@ -5,6 +5,67 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on Keep a Changelog and the project stays on the `v0.x.x`
6
6
  line until the CLI surface and capture behavior settle.
7
7
 
8
+ ## [0.2.0] - 2026-03-22
9
+
10
+ ### Added
11
+
12
+ - `render <project.json>` command for project-based multi-scene rendering.
13
+ - Project config support for `scenes`, `outputs`, reusable defaults, and
14
+ project-level summary manifests.
15
+ - Scene `actions` and mid-capture `timeline` segments including `pause`,
16
+ `scroll`, `click`, `hover`, `press`, `type`, and `scroll-to`.
17
+ - Multi-output rendering with per-output viewport, audio, subtitles,
18
+ transitions, and encoder settings.
19
+ - Node library exports for project loading, render planning, and render
20
+ execution.
21
+ - JSON Schema and sample project config for editor validation and onboarding.
22
+ - Scene-to-scene `crossfade` transitions.
23
+ - Subtitle support for both soft and burn-in modes with `.srt`, `.vtt`, and
24
+ `.webvtt` inputs.
25
+ - `finalVideo` encoder controls for final `mp4` and `webm` outputs.
26
+ - `intermediateArtifact` profiles for scene clip generation.
27
+
28
+ ### Changed
29
+
30
+ - Render orchestration now uses an explicit render-plan layer that separates
31
+ scene capture from final composition.
32
+ - Render manifests now use schema version `2` and separate `captureMetrics`
33
+ from `artifactMetrics`.
34
+ - Probe handling now records explicit status and warnings, and fails fast when
35
+ precise clip timing is required by composition capabilities such as
36
+ `crossfade`.
37
+
38
+ ## [0.1.9] - 2026-03-20
39
+
40
+ ### Added
41
+
42
+ - TTY-aware progress bar during frame rendering; falls back to milestone
43
+ percentages in non-TTY environments.
44
+ - `--force` flag to overwrite an existing output file.
45
+ - `--version` / `-V` flag to print the current version.
46
+ - `--help` / `-h` at both top-level and subcommand level.
47
+ - Graceful cancellation via SIGINT / SIGTERM with partial-output cleanup.
48
+ - Capture summary printed to stderr on completion (file size, duration, frame
49
+ count, pages, manifest path).
50
+ - FFmpeg availability pre-check with platform-specific install instructions.
51
+ - Frame-count safety limit (36 000 frames) and FPS cap (120) to prevent
52
+ runaway captures.
53
+ - `--hide-selector` input validation (rejects selectors containing `{` or `}`).
54
+ - URL credential sanitization — userinfo is stripped from all logs and
55
+ manifests.
56
+
57
+ ### Changed
58
+
59
+ - Font loading now times out after 10 s instead of waiting indefinitely.
60
+ - Preflight page measurement stops after 20 iterations to avoid infinite loops
61
+ on dynamically-loading pages.
62
+ - Browser session properly closes when context creation fails.
63
+ - FFmpeg encoder supports abort with SIGTERM → SIGKILL escalation.
64
+ - Manifest `status` field now includes a `cancelled` value.
65
+ - Error messages throughout the CLI include actionable hints and expected
66
+ formats.
67
+ - Help text now shows default values, max constraints, and usage examples.
68
+
8
69
  ## [0.1.8] - 2026-03-19
9
70
 
10
71
  ### Added
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Rollberry
2
2
 
3
- Rollberry is an MIT-licensed open source CLI for turning web pages into
4
- smooth top-to-bottom scroll videos. It captures one or more URLs into a single
5
- MP4. It is built for real browser capture, works with normal URLs and
6
- `localhost`, and is published for direct `npx` usage.
3
+ Rollberry is an MIT-licensed open source CLI and Node API for turning web
4
+ pages into smooth top-to-bottom scroll videos. It can capture one or more
5
+ URLs into a single MP4, or render a project JSON file into multiple output
6
+ variants such as desktop and mobile. It is built for real browser capture,
7
+ works with normal URLs and `localhost`, and is published for direct `npx`
8
+ usage.
7
9
 
8
10
  Maintained by CORe Inc.
9
11
 
@@ -12,7 +14,7 @@ Maintained by CORe Inc.
12
14
  Requirements:
13
15
 
14
16
  - Node.js `24.12.0+`
15
- - `ffmpeg` and `ffprobe` available on `PATH`
17
+ - `ffmpeg` available on `PATH`
16
18
 
17
19
  Install nothing globally. Run it directly:
18
20
 
@@ -29,6 +31,13 @@ npx rollberry capture http://localhost:3000 \
29
31
  On the first run, if Playwright Chromium is missing, Rollberry installs it
30
32
  automatically. `ffmpeg` is not auto-installed.
31
33
 
34
+ For project-based rendering, start from the bundled sample:
35
+
36
+ ```bash
37
+ cp rollberry.project.sample.json rollberry.project.json
38
+ npx rollberry render ./rollberry.project.json
39
+ ```
40
+
32
41
  ## Using With npx
33
42
 
34
43
  The normal way to run Rollberry is `npx`.
@@ -61,11 +70,17 @@ npx rollberry capture https://playwright.dev \
61
70
  --duration 8
62
71
  ```
63
72
 
73
+ Render a project file with multiple outputs:
74
+
75
+ ```bash
76
+ npx rollberry render ./rollberry.project.json
77
+ ```
78
+
64
79
  Notes:
65
80
 
66
81
  - `npx` downloads the published CLI package automatically
67
82
  - on the first run, Rollberry installs Playwright Chromium if needed
68
- - `ffmpeg` and `ffprobe` must already be available on your machine
83
+ - `ffmpeg` must already be available on your machine
69
84
  - if you want reproducible automation, pin the package version with
70
85
  `npx rollberry@<version> ...`
71
86
 
@@ -74,10 +89,12 @@ Notes:
74
89
  Each run writes:
75
90
 
76
91
  - `video.mp4`: the rendered capture
77
- - `video.manifest.json`: environment, options, result, and failure details
92
+ - `video.manifest.json`: environment, scene definitions, capture metrics, artifact metrics, and failure details
78
93
  - `video.log.jsonl`: structured operational logs
79
94
 
80
95
  You can override the sidecar paths with `--manifest` and `--log-file`.
96
+ Project renders write the same sidecars for each configured output, plus a
97
+ project-level `*.render-summary.json`.
81
98
 
82
99
  ## Common Examples
83
100
 
@@ -133,10 +150,162 @@ npx rollberry capture http://localhost:3000 \
133
150
  --debug-frames-dir ./artifacts/debug-frames
134
151
  ```
135
152
 
153
+ Render a project file into desktop and mobile outputs:
154
+
155
+ ```bash
156
+ npx rollberry render ./rollberry.project.json
157
+ ```
158
+
159
+ Render only one named output from a project:
160
+
161
+ ```bash
162
+ npx rollberry render ./rollberry.project.json --output mobile
163
+ ```
164
+
165
+ ## Project Rendering
166
+
167
+ Use `render` when you want scene-by-scene control and multiple outputs from one
168
+ config file. `actions` run before capture as setup. `timeline` runs during the
169
+ capture itself and lets you interleave scroll, pause, and interactions inside
170
+ one scene.
171
+
172
+ ```json
173
+ {
174
+ "$schema": "./rollberry.project.schema.json",
175
+ "schemaVersion": 1,
176
+ "summaryManifest": "./artifacts/demo.render-summary.json",
177
+ "defaults": {
178
+ "fps": 60,
179
+ "viewport": "1440x900",
180
+ "waitFor": "selector:body",
181
+ "hideSelectors": ["#cookie-banner"]
182
+ },
183
+ "scenes": [
184
+ {
185
+ "name": "home",
186
+ "url": "http://localhost:3000",
187
+ "actions": [
188
+ { "type": "click", "selector": "[data-open-menu]" },
189
+ { "type": "wait", "ms": 300 }
190
+ ],
191
+ "timeline": [
192
+ { "type": "pause", "duration": 0.4 },
193
+ {
194
+ "type": "click",
195
+ "selector": "[data-open-menu]",
196
+ "holdAfterSeconds": 0.4
197
+ },
198
+ { "type": "scroll", "toSelector": "#pricing", "duration": 1.4 },
199
+ { "type": "scroll", "to": "bottom", "duration": "auto" }
200
+ ],
201
+ "holdAfterSeconds": 0.75
202
+ },
203
+ {
204
+ "name": "pricing",
205
+ "url": "http://localhost:3000/pricing"
206
+ }
207
+ ],
208
+ "outputs": [
209
+ {
210
+ "name": "desktop",
211
+ "viewport": "1440x900",
212
+ "out": "./artifacts/demo-desktop.mp4",
213
+ "audio": {
214
+ "path": "./assets/demo-narration.wav",
215
+ "volume": 0.8,
216
+ "loop": true
217
+ },
218
+ "subtitles": {
219
+ "path": "./assets/demo-captions.vtt",
220
+ "mode": "burn-in"
221
+ },
222
+ "transition": {
223
+ "type": "crossfade",
224
+ "duration": 0.25
225
+ },
226
+ "intermediateArtifact": {
227
+ "format": "mp4",
228
+ "preset": "veryfast",
229
+ "crf": 20
230
+ },
231
+ "finalVideo": {
232
+ "preset": "medium",
233
+ "crf": 19
234
+ }
235
+ },
236
+ {
237
+ "name": "mobile",
238
+ "viewport": "430x932",
239
+ "out": "./artifacts/demo-mobile.webm",
240
+ "format": "webm",
241
+ "subtitles": {
242
+ "path": "./assets/demo-captions.vtt",
243
+ "mode": "soft"
244
+ },
245
+ "transition": {
246
+ "type": "crossfade",
247
+ "duration": 0.25
248
+ },
249
+ "intermediateArtifact": {
250
+ "format": "mp4",
251
+ "preset": "fast",
252
+ "crf": 22
253
+ },
254
+ "finalVideo": {
255
+ "deadline": "best",
256
+ "crf": 30
257
+ }
258
+ }
259
+ ]
260
+ }
261
+ ```
262
+
263
+ Supported setup actions:
264
+
265
+ - `wait`
266
+ - `click`
267
+ - `hover`
268
+ - `press`
269
+ - `type`
270
+ - `scroll-to`
271
+
272
+ Supported timeline segments:
273
+
274
+ - `pause`
275
+ - `wait`
276
+ - `scroll`
277
+ - `click`
278
+ - `hover`
279
+ - `press`
280
+ - `type`
281
+ - `scroll-to`
282
+
283
+ Supported output extensions:
284
+
285
+ - `mp4`
286
+ - `webm`
287
+
288
+ Supported media extensions:
289
+
290
+ - output-level background audio via `audio.path`
291
+ - output-level subtitles via `subtitles.path`
292
+ - `subtitles.mode: "soft"` for `mp4` and `webm`
293
+ - `subtitles.mode: "burn-in"` for `mp4` and `webm`
294
+ - `.srt`, `.vtt`, and `.webvtt` subtitle inputs
295
+ - output-level `transition.type: "fade-in"`
296
+ - output-level `transition.type: "crossfade"` between adjacent scenes
297
+ - output-level `intermediateArtifact.format` / `preset` / `crf` for scene clip profiles
298
+ - output-level `finalVideo.preset` / `finalVideo.crf` for `mp4` encode control
299
+ - output-level `finalVideo.deadline` / `finalVideo.crf` for `webm` encode control
300
+ - project-level summary manifest via `summaryManifest`
301
+ - render manifest separates `captureMetrics` and `artifactMetrics`
302
+ - `crossfade` uses `ffprobe`-backed clip timing when available and fails fast if scene clip probing is unavailable
303
+
136
304
  ## CLI Options
137
305
 
138
306
  ```text
139
307
  rollberry capture <url...>
308
+ rollberry render <project.json>
140
309
 
141
310
  --out <file> Output MP4 path
142
311
  --viewport <WxH> Viewport size, example: 1440x900
@@ -150,6 +319,10 @@ rollberry capture <url...>
150
319
  --debug-frames-dir <dir> Save raw PNG frames
151
320
  --manifest <file> Manifest JSON output path
152
321
  --log-file <file> Log JSONL output path
322
+
323
+ render:
324
+ --output <name> Render only the named output (repeatable)
325
+ --force Overwrite configured output files
153
326
  ```
154
327
 
155
328
  ## Localhost Behavior
@@ -168,6 +341,9 @@ If `ffmpeg` is missing:
168
341
  brew install ffmpeg
169
342
  ```
170
343
 
344
+ If you are running the test suite, `ffprobe` may also be used for extra video
345
+ verification. Most FFmpeg installs include it alongside `ffmpeg`.
346
+
171
347
  If capture fails, inspect:
172
348
 
173
349
  - `*.manifest.json` for final status and error details
@@ -187,6 +363,11 @@ If a site keeps shifting during capture:
187
363
 
188
364
  ## Local Development
189
365
 
366
+ For local CLI usage and captures, Rollberry requires `ffmpeg` on `PATH`.
367
+ When running `pnpm test`, the integration suite uses `ffprobe` when available
368
+ to inspect generated videos, but falls back to basic file validation if it is
369
+ missing.
370
+
190
371
  ```bash
191
372
  corepack pnpm install
192
373
  corepack pnpm exec playwright install chromium
@@ -199,6 +380,7 @@ Run from the repository:
199
380
 
200
381
  ```bash
201
382
  corepack pnpm dev -- capture http://localhost:3000 --out ./artifacts/demo.mp4
383
+ corepack pnpm dev -- render ./rollberry.project.sample.json
202
384
  ```
203
385
 
204
386
  Run the regression suite:
@@ -0,0 +1,11 @@
1
+ import type { Page } from 'playwright';
2
+ import type { CaptureAction } from './types.js';
3
+ export declare function executeSceneActions(options: {
4
+ page: Page;
5
+ actions: CaptureAction[];
6
+ timeoutMs: number;
7
+ onActionStart?: (action: CaptureAction, index: number) => Promise<void> | void;
8
+ onActionComplete?: (action: CaptureAction, index: number) => Promise<void> | void;
9
+ }): Promise<void>;
10
+ export declare function executeCaptureAction(page: Page, action: CaptureAction, timeoutMs: number): Promise<void>;
11
+ export declare function resetScrollPosition(page: Page): Promise<void>;
@@ -0,0 +1,81 @@
1
+ import { delay, waitForAnimationFrames } from './utils.js';
2
+ const ACTION_SETTLE_DELAY_MS = 75;
3
+ export async function executeSceneActions(options) {
4
+ const { page, actions, timeoutMs, onActionStart, onActionComplete } = options;
5
+ for (const [index, action] of actions.entries()) {
6
+ await onActionStart?.(action, index);
7
+ await executeCaptureAction(page, action, timeoutMs);
8
+ await onActionComplete?.(action, index);
9
+ }
10
+ }
11
+ export async function executeCaptureAction(page, action, timeoutMs) {
12
+ switch (action.kind) {
13
+ case 'wait':
14
+ await delay(action.ms);
15
+ break;
16
+ case 'click':
17
+ await page.waitForSelector(action.selector, {
18
+ state: 'visible',
19
+ timeout: timeoutMs,
20
+ });
21
+ await page.locator(action.selector).click();
22
+ await settlePage(page);
23
+ break;
24
+ case 'hover':
25
+ await page.waitForSelector(action.selector, {
26
+ state: 'visible',
27
+ timeout: timeoutMs,
28
+ });
29
+ await page.locator(action.selector).hover();
30
+ await settlePage(page);
31
+ break;
32
+ case 'press':
33
+ await page.keyboard.press(action.key);
34
+ await settlePage(page);
35
+ break;
36
+ case 'type':
37
+ await page.waitForSelector(action.selector, {
38
+ state: 'visible',
39
+ timeout: timeoutMs,
40
+ });
41
+ if (action.clear) {
42
+ await page.locator(action.selector).fill(action.text);
43
+ }
44
+ else {
45
+ await page.locator(action.selector).type(action.text);
46
+ }
47
+ await settlePage(page);
48
+ break;
49
+ case 'scroll-to':
50
+ await page.waitForSelector(action.selector, {
51
+ state: 'attached',
52
+ timeout: timeoutMs,
53
+ });
54
+ await page.evaluate(({ selector, block }) => {
55
+ const element = document.querySelector(selector);
56
+ if (!element) {
57
+ throw new Error(`Selector not found for scroll action: ${selector}`);
58
+ }
59
+ element.scrollIntoView({
60
+ block,
61
+ behavior: 'auto',
62
+ inline: 'nearest',
63
+ });
64
+ }, {
65
+ selector: action.selector,
66
+ block: action.block,
67
+ });
68
+ await settlePage(page);
69
+ break;
70
+ }
71
+ }
72
+ export async function resetScrollPosition(page) {
73
+ await page.evaluate(() => {
74
+ window.scrollTo({ top: 0, behavior: 'auto' });
75
+ });
76
+ await waitForAnimationFrames(page);
77
+ }
78
+ async function settlePage(page) {
79
+ await waitForAnimationFrames(page);
80
+ await delay(ACTION_SETTLE_DELAY_MS);
81
+ }
@@ -0,0 +1,3 @@
1
+ import type { CaptureLogger } from './logger.js';
2
+ export declare function ensureChromiumInstalled(logger: CaptureLogger): Promise<void>;
3
+ export declare function resolvePlaywrightCliPath(): string;
@@ -13,7 +13,7 @@ export async function ensureChromiumInstalled(logger) {
13
13
  await logger.warn('browser.install.start', 'Chromium was not found. Installing Playwright Chromium.', { executablePath });
14
14
  await installPlaywrightChromium(resolvePlaywrightCliPath());
15
15
  if (!(await hasExecutable(executablePath))) {
16
- throw new Error(`Chromium のインストール後も実行ファイルが見つかりません: ${executablePath}`);
16
+ throw new Error(`Chromium executable not found after installation: ${executablePath}\n Try running: npx playwright install chromium`);
17
17
  }
18
18
  await logger.info('browser.install.complete', 'Playwright Chromium installation finished.', { executablePath });
19
19
  }
@@ -0,0 +1,13 @@
1
+ import { chromium, type Page } from 'playwright';
2
+ import type { CaptureLogger } from './logger.js';
3
+ import type { Viewport } from './types.js';
4
+ export interface BrowserSession {
5
+ browser: Awaited<ReturnType<typeof chromium.launch>>;
6
+ page: Page;
7
+ }
8
+ export declare function openBrowserSession(options: {
9
+ viewport: Viewport;
10
+ timeoutMs: number;
11
+ urls: URL[];
12
+ }, logger: CaptureLogger): Promise<BrowserSession>;
13
+ export declare function navigateWithRetry(page: Page, url: URL, timeoutMs: number): Promise<void>;
@@ -7,16 +7,22 @@ export async function openBrowserSession(options, logger) {
7
7
  const browser = await chromium.launch({
8
8
  headless: true,
9
9
  });
10
- const context = await browser.newContext({
11
- viewport: options.viewport,
12
- ignoreHTTPSErrors: options.urls.some(isLocalUrl),
13
- });
14
- const page = await context.newPage();
15
- page.setDefaultTimeout(options.timeoutMs);
16
- await page.addInitScript(() => {
17
- history.scrollRestoration = 'manual';
18
- });
19
- return { browser, page };
10
+ try {
11
+ const context = await browser.newContext({
12
+ viewport: options.viewport,
13
+ ignoreHTTPSErrors: options.urls.some(isLocalUrl),
14
+ });
15
+ const page = await context.newPage();
16
+ page.setDefaultTimeout(options.timeoutMs);
17
+ await page.addInitScript(() => {
18
+ history.scrollRestoration = 'manual';
19
+ });
20
+ return { browser, page };
21
+ }
22
+ catch (error) {
23
+ await browser.close();
24
+ throw error;
25
+ }
20
26
  }
21
27
  export async function navigateWithRetry(page, url, timeoutMs) {
22
28
  const deadline = Date.now() + timeoutMs;
@@ -0,0 +1,8 @@
1
+ import type { CaptureLogger } from './logger.js';
2
+ import type { ProgressReporter } from './progress.js';
3
+ import type { CaptureJob, CaptureOptions, CaptureResult } from './types.js';
4
+ export declare function captureVideo(options: CaptureOptions, logger: CaptureLogger, progress?: ProgressReporter, signal?: AbortSignal): Promise<CaptureResult>;
5
+ export declare function captureSceneVideo(job: CaptureJob, logger: CaptureLogger, progress?: ProgressReporter, signal?: AbortSignal): Promise<CaptureResult>;
6
+ export declare class AbortError extends Error {
7
+ constructor();
8
+ }