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/dist/options.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { resolve } from 'node:path';
2
2
  import { parseArgs } from 'node:util';
3
- import { parseCaptureUrl } from './capture/utils.js';
4
- const DEFAULT_OUT_FILE = 'rollberry.mp4';
5
- const DEFAULT_VIEWPORT = '1440x900';
6
- const DEFAULT_FPS = 60;
7
- const DEFAULT_DURATION = 'auto';
8
- const DEFAULT_MOTION = 'ease-in-out-sine';
9
- const DEFAULT_TIMEOUT_MS = 30_000;
3
+ import { MAX_FPS } from './capture/constants.js';
4
+ import { parseCaptureUrl, validateHideSelector } from './capture/utils.js';
5
+ import { VERSION } from './version.js';
6
+ export const DEFAULT_OUT_FILE = 'rollberry.mp4';
7
+ export const DEFAULT_VIEWPORT = '1440x900';
8
+ export const DEFAULT_FPS = 60;
9
+ export const DEFAULT_DURATION = 'auto';
10
+ export const DEFAULT_MOTION = 'ease-in-out-sine';
11
+ export const DEFAULT_TIMEOUT_MS = 30_000;
10
12
  export class CliError extends Error {
11
13
  showUsage;
12
14
  constructor(message, showUsage = false) {
@@ -15,119 +17,79 @@ export class CliError extends Error {
15
17
  this.name = 'CliError';
16
18
  }
17
19
  }
18
- export function parseCliArgs(argv = process.argv.slice(2)) {
20
+ export class HelpRequest extends Error {
21
+ constructor(message) {
22
+ super(message);
23
+ this.name = 'HelpRequest';
24
+ }
25
+ }
26
+ export class VersionRequest extends Error {
27
+ version = VERSION;
28
+ constructor() {
29
+ super(VERSION);
30
+ this.name = 'VersionRequest';
31
+ }
32
+ }
33
+ export function parseCommandArgs(argv = process.argv.slice(2)) {
19
34
  const [command, ...rest] = argv;
20
- if (!command) {
21
- throw new CliError('サブコマンドが必要です。', true);
35
+ if (command === '--help' || command === '-h') {
36
+ throw new HelpRequest(formatUsage());
22
37
  }
23
- if (command !== 'capture') {
24
- throw new CliError(`未知のサブコマンドです: ${command}`, true);
38
+ if (command === '--version' || command === '-V') {
39
+ throw new VersionRequest();
25
40
  }
26
- const parsed = parseArgs({
27
- args: rest,
28
- allowPositionals: true,
29
- options: {
30
- out: {
31
- type: 'string',
32
- },
33
- viewport: {
34
- type: 'string',
35
- },
36
- fps: {
37
- type: 'string',
38
- },
39
- duration: {
40
- type: 'string',
41
- },
42
- motion: {
43
- type: 'string',
44
- },
45
- timeout: {
46
- type: 'string',
47
- },
48
- 'wait-for': {
49
- type: 'string',
50
- },
51
- 'hide-selector': {
52
- type: 'string',
53
- multiple: true,
54
- },
55
- 'debug-frames-dir': {
56
- type: 'string',
57
- },
58
- 'page-gap': {
59
- type: 'string',
60
- },
61
- manifest: {
62
- type: 'string',
63
- },
64
- 'log-file': {
65
- type: 'string',
66
- },
67
- },
68
- strict: true,
69
- });
70
- if (parsed.positionals.length === 0) {
71
- throw new CliError('capture にはURLが必要です。', true);
41
+ if (!command) {
42
+ throw createMissingSubcommandError();
72
43
  }
73
- const urls = parsed.positionals.map((raw) => parseWithCliError(raw, parseCaptureUrl));
74
- const durationOption = parsed.values.duration ?? DEFAULT_DURATION;
75
- if (durationOption !== 'auto' && Number.isNaN(Number(durationOption))) {
76
- throw new CliError(`--duration は "auto" または数値で指定してください: ${durationOption}`);
44
+ switch (command) {
45
+ case 'capture':
46
+ return { kind: 'capture', options: parseCaptureArgs(rest) };
47
+ case 'render':
48
+ return { kind: 'render', options: parseRenderArgs(rest) };
49
+ default:
50
+ throw createUnknownSubcommandError(command);
77
51
  }
78
- const outPath = resolveOutPath(parsed.values.out ?? DEFAULT_OUT_FILE);
79
- const pageGapSeconds = parseWithCliError(parsed.values['page-gap'] ?? '0', parseNonNegativeNumber);
80
- return {
81
- urls: toNonEmptyArray(urls),
82
- outPath,
83
- manifestPath: parsed.values.manifest
84
- ? resolveOutPath(parsed.values.manifest)
85
- : deriveSidecarPath(outPath, '.manifest.json'),
86
- logFilePath: parsed.values['log-file']
87
- ? resolveOutPath(parsed.values['log-file'])
88
- : deriveSidecarPath(outPath, '.log.jsonl'),
89
- viewport: parseWithCliError(parsed.values.viewport ?? DEFAULT_VIEWPORT, parseViewport),
90
- fps: parseWithCliError(parsed.values.fps ?? String(DEFAULT_FPS), parsePositiveInt),
91
- duration: durationOption === 'auto'
92
- ? 'auto'
93
- : parseWithCliError(durationOption, parsePositiveNumber),
94
- motion: parseWithCliError(parsed.values.motion ?? DEFAULT_MOTION, parseMotion),
95
- timeoutMs: parseWithCliError(parsed.values.timeout ?? String(DEFAULT_TIMEOUT_MS), parsePositiveInt),
96
- waitFor: parseWithCliError(parsed.values['wait-for'] ?? 'load', parseWaitFor),
97
- hideSelectors: parsed.values['hide-selector'] ?? [],
98
- pageGapSeconds,
99
- debugFramesDir: parsed.values['debug-frames-dir']
100
- ? resolveOutPath(parsed.values['debug-frames-dir'])
101
- : undefined,
102
- };
103
52
  }
104
- function toNonEmptyArray(items) {
105
- if (items.length === 0) {
106
- throw new CliError('capture にはURLが必要です。', true);
107
- }
108
- return items;
53
+ export function parseCliArgs(argv = process.argv.slice(2)) {
54
+ const args = argv[0] === 'capture' ? argv.slice(1) : argv;
55
+ return parseCaptureArgs(args);
109
56
  }
110
57
  export function formatUsage() {
111
58
  return [
59
+ `rollberry v${VERSION} — Capture and render web pages into video`,
60
+ '',
112
61
  'Usage:',
113
62
  ' rollberry capture <url...> [options]',
63
+ ' rollberry render <project.json> [options]',
64
+ ' rollberry --help | -h',
65
+ ' rollberry --version | -V',
114
66
  '',
115
- 'Options:',
67
+ 'Capture Options:',
116
68
  ' --out <file> Output MP4 path (default: ./rollberry.mp4)',
117
69
  ' --viewport <WxH> Viewport size (default: 1440x900)',
118
- ' --fps <n> Frames per second (default: 60)',
70
+ ` --fps <n> Frames per second (default: 60, max: ${MAX_FPS})`,
119
71
  ' --duration <seconds|auto> Capture duration (default: auto)',
120
- ' --motion <curve> ease-in-out-sine | linear',
72
+ ' --motion <curve> ease-in-out-sine | linear (default: ease-in-out-sine)',
121
73
  ' --timeout <ms> Navigation timeout (default: 30000)',
122
- ' --wait-for <mode> load | selector:<css> | ms:<n>',
123
- ' --hide-selector <css> Hide CSS selector before capture',
74
+ ' --wait-for <mode> load | selector:<css> | ms:<n> (default: load)',
75
+ ' --hide-selector <css> Hide CSS selector before capture (repeatable)',
76
+ ' --force Overwrite output file if it already exists',
124
77
  ' --debug-frames-dir <dir> Save raw PNG frames for debugging',
125
78
  ' --page-gap <seconds> Pause between pages (default: 0)',
126
79
  ' --manifest <file> Manifest JSON path (default: <out>.manifest.json)',
127
80
  ' --log-file <file> Log JSONL path (default: <out>.log.jsonl)',
81
+ '',
82
+ 'Render Options:',
83
+ ' --output <name> Render only the named output (repeatable)',
84
+ ' --force Overwrite configured output files',
85
+ '',
86
+ 'Examples:',
87
+ ' rollberry capture http://localhost:3000',
88
+ ' rollberry capture https://example.com --out demo.mp4 --viewport 1920x1080',
89
+ ' rollberry render ./rollberry.project.json --output mobile',
128
90
  ].join('\n');
129
91
  }
130
- function parseWithCliError(rawValue, parser) {
92
+ export function parseWithCliError(rawValue, parser) {
131
93
  try {
132
94
  return parser(rawValue);
133
95
  }
@@ -135,66 +97,66 @@ function parseWithCliError(rawValue, parser) {
135
97
  if (error instanceof CliError) {
136
98
  throw error;
137
99
  }
138
- throw new CliError(error instanceof Error ? error.message : '引数の解析に失敗しました。');
100
+ throw new CliError(error instanceof Error ? error.message : 'Failed to parse arguments.');
139
101
  }
140
102
  }
141
- function resolveOutPath(path) {
103
+ export function resolveOutPath(path) {
142
104
  return resolve(process.cwd(), path);
143
105
  }
144
- function deriveSidecarPath(path, suffix) {
106
+ export function deriveSidecarPath(path, suffix) {
145
107
  const extension = /\.([^.]+)$/u.exec(path);
146
108
  if (!extension) {
147
109
  return `${path}${suffix}`;
148
110
  }
149
111
  return path.slice(0, -extension[0].length) + suffix;
150
112
  }
151
- function parseViewport(rawViewport) {
113
+ export function parseViewport(rawViewport) {
152
114
  const match = /^(?<width>\d+)x(?<height>\d+)$/u.exec(rawViewport);
153
115
  if (!match?.groups) {
154
- throw new Error(`--viewport "1440x900" の形式で指定してください: ${rawViewport}`);
116
+ throw new Error(`--viewport must be in "WxH" format (e.g. "1440x900"): ${rawViewport}`);
155
117
  }
156
118
  const width = Number(match.groups.width);
157
119
  const height = Number(match.groups.height);
158
120
  if (width <= 0 || height <= 0) {
159
- throw new Error(`--viewport の値が不正です: ${rawViewport}`);
121
+ throw new Error(`--viewport dimensions must be positive (e.g. "1440x900"): ${rawViewport}`);
160
122
  }
161
123
  return { width, height };
162
124
  }
163
- function parsePositiveInt(rawValue) {
125
+ export function parsePositiveInt(rawValue) {
164
126
  const value = Number(rawValue);
165
127
  if (!Number.isInteger(value) || value <= 0) {
166
- throw new Error(`正の整数を指定してください: ${rawValue}`);
128
+ throw new Error(`Expected a positive integer (e.g. "60"): ${rawValue}`);
167
129
  }
168
130
  return value;
169
131
  }
170
- function parsePositiveNumber(rawValue) {
132
+ export function parsePositiveNumber(rawValue) {
171
133
  const value = Number(rawValue);
172
134
  if (!Number.isFinite(value) || value <= 0) {
173
- throw new Error(`正の数値を指定してください: ${rawValue}`);
135
+ throw new Error(`Expected a positive number (e.g. "5.0"): ${rawValue}`);
174
136
  }
175
137
  return value;
176
138
  }
177
- function parseNonNegativeNumber(rawValue) {
139
+ export function parseNonNegativeNumber(rawValue) {
178
140
  const value = Number(rawValue);
179
141
  if (!Number.isFinite(value) || value < 0) {
180
- throw new Error(`0以上の数値を指定してください: ${rawValue}`);
142
+ throw new Error(`Expected a non-negative number (e.g. "0" or "1.5"): ${rawValue}`);
181
143
  }
182
144
  return value;
183
145
  }
184
- function parseMotion(rawMotion) {
146
+ export function parseMotion(rawMotion) {
185
147
  if (rawMotion === 'ease-in-out-sine' || rawMotion === 'linear') {
186
148
  return rawMotion;
187
149
  }
188
- throw new Error(`--motion ease-in-out-sine または linear です: ${rawMotion}`);
150
+ throw new Error(`--motion must be "ease-in-out-sine" or "linear": ${rawMotion}`);
189
151
  }
190
- function parseWaitFor(rawWaitFor) {
152
+ export function parseWaitFor(rawWaitFor) {
191
153
  if (rawWaitFor === 'load') {
192
154
  return { kind: 'load' };
193
155
  }
194
156
  if (rawWaitFor.startsWith('selector:')) {
195
157
  const selector = rawWaitFor.slice('selector:'.length).trim();
196
158
  if (!selector) {
197
- throw new Error('--wait-for selector:<css> CSS セレクタが空です。');
159
+ throw new Error('--wait-for selector:<css> requires a non-empty CSS selector.');
198
160
  }
199
161
  return {
200
162
  kind: 'selector',
@@ -207,5 +169,157 @@ function parseWaitFor(rawWaitFor) {
207
169
  ms: parsePositiveInt(rawWaitFor.slice('ms:'.length)),
208
170
  };
209
171
  }
210
- throw new Error(`--wait-for load / selector:<css> / ms:<n> のいずれかです: ${rawWaitFor}`);
172
+ throw new Error(`--wait-for must be "load", "selector:<css>", or "ms:<n>": ${rawWaitFor}`);
173
+ }
174
+ function parseCaptureArgs(args) {
175
+ if (args.includes('--help') || args.includes('-h')) {
176
+ throw new HelpRequest(formatUsage());
177
+ }
178
+ const parsed = parseArgs({
179
+ args,
180
+ allowPositionals: true,
181
+ options: {
182
+ out: {
183
+ type: 'string',
184
+ },
185
+ viewport: {
186
+ type: 'string',
187
+ },
188
+ fps: {
189
+ type: 'string',
190
+ },
191
+ duration: {
192
+ type: 'string',
193
+ },
194
+ motion: {
195
+ type: 'string',
196
+ },
197
+ timeout: {
198
+ type: 'string',
199
+ },
200
+ 'wait-for': {
201
+ type: 'string',
202
+ },
203
+ 'hide-selector': {
204
+ type: 'string',
205
+ multiple: true,
206
+ },
207
+ 'debug-frames-dir': {
208
+ type: 'string',
209
+ },
210
+ 'page-gap': {
211
+ type: 'string',
212
+ },
213
+ manifest: {
214
+ type: 'string',
215
+ },
216
+ 'log-file': {
217
+ type: 'string',
218
+ },
219
+ force: {
220
+ type: 'boolean',
221
+ },
222
+ },
223
+ strict: true,
224
+ });
225
+ if (parsed.positionals.length === 0) {
226
+ throw new CliError('capture requires at least one URL.', true);
227
+ }
228
+ const urls = parsed.positionals.map((raw) => parseWithCliError(raw, parseCaptureUrl));
229
+ const durationOption = parsed.values.duration ?? DEFAULT_DURATION;
230
+ if (durationOption !== 'auto' && Number.isNaN(Number(durationOption))) {
231
+ throw new CliError(`--duration must be "auto" or a positive number (e.g. --duration 5): ${durationOption}`);
232
+ }
233
+ const outPath = resolveOutPath(parsed.values.out ?? DEFAULT_OUT_FILE);
234
+ const pageGapSeconds = parseWithCliError(parsed.values['page-gap'] ?? '0', parseNonNegativeNumber);
235
+ const hideSelectors = parsed.values['hide-selector'] ?? [];
236
+ for (const selector of hideSelectors) {
237
+ parseWithCliError(selector, validateHideSelector);
238
+ }
239
+ const fps = parseWithCliError(parsed.values.fps ?? String(DEFAULT_FPS), parsePositiveInt);
240
+ if (fps > MAX_FPS) {
241
+ throw new CliError(`--fps must be at most ${MAX_FPS}: ${fps}`);
242
+ }
243
+ return {
244
+ urls: toNonEmptyArray(urls),
245
+ outPath,
246
+ manifestPath: parsed.values.manifest
247
+ ? resolveOutPath(parsed.values.manifest)
248
+ : deriveSidecarPath(outPath, '.manifest.json'),
249
+ logFilePath: parsed.values['log-file']
250
+ ? resolveOutPath(parsed.values['log-file'])
251
+ : deriveSidecarPath(outPath, '.log.jsonl'),
252
+ viewport: parseWithCliError(parsed.values.viewport ?? DEFAULT_VIEWPORT, parseViewport),
253
+ fps,
254
+ duration: durationOption === 'auto'
255
+ ? 'auto'
256
+ : parseWithCliError(durationOption, parsePositiveNumber),
257
+ motion: parseWithCliError(parsed.values.motion ?? DEFAULT_MOTION, parseMotion),
258
+ timeoutMs: parseWithCliError(parsed.values.timeout ?? String(DEFAULT_TIMEOUT_MS), parsePositiveInt),
259
+ waitFor: parseWithCliError(parsed.values['wait-for'] ?? 'load', parseWaitFor),
260
+ hideSelectors,
261
+ pageGapSeconds,
262
+ debugFramesDir: parsed.values['debug-frames-dir']
263
+ ? resolveOutPath(parsed.values['debug-frames-dir'])
264
+ : undefined,
265
+ force: parsed.values.force === true,
266
+ };
267
+ }
268
+ function parseRenderArgs(args) {
269
+ if (args.includes('--help') || args.includes('-h')) {
270
+ throw new HelpRequest(formatUsage());
271
+ }
272
+ const parsed = parseArgs({
273
+ args,
274
+ allowPositionals: true,
275
+ options: {
276
+ output: {
277
+ type: 'string',
278
+ multiple: true,
279
+ },
280
+ force: {
281
+ type: 'boolean',
282
+ },
283
+ },
284
+ strict: true,
285
+ });
286
+ if (parsed.positionals.length === 0) {
287
+ throw new CliError('render requires a project JSON path.', true);
288
+ }
289
+ if (parsed.positionals.length > 1) {
290
+ throw new CliError(`render accepts exactly one project JSON path: ${parsed.positionals.slice(1).join(', ')}`, true);
291
+ }
292
+ return {
293
+ projectPath: resolveOutPath(parsed.positionals[0]),
294
+ outputNames: parsed.values.output ?? [],
295
+ force: parsed.values.force === true,
296
+ };
297
+ }
298
+ function toNonEmptyArray(items) {
299
+ if (items.length === 0) {
300
+ throw new CliError('capture requires at least one URL.', true);
301
+ }
302
+ return items;
303
+ }
304
+ function createMissingSubcommandError() {
305
+ return new CliError([
306
+ 'A subcommand is required.',
307
+ '',
308
+ 'Available commands:',
309
+ ' capture Capture a scroll video from one or more URLs',
310
+ ' render Render a project JSON file into one or more videos',
311
+ '',
312
+ 'Run rollberry --help for more information.',
313
+ ].join('\n'));
314
+ }
315
+ function createUnknownSubcommandError(command) {
316
+ return new CliError([
317
+ `Unknown subcommand: ${command}`,
318
+ '',
319
+ 'Available commands:',
320
+ ' capture Capture a scroll video from one or more URLs',
321
+ ' render Render a project JSON file into one or more videos',
322
+ '',
323
+ 'Run rollberry --help for more information.',
324
+ ].join('\n'));
211
325
  }
@@ -0,0 +1,27 @@
1
+ import type { CaptureAudioTrack, CaptureScene, CaptureSubtitleTrack, CaptureTransition, FinalVideoEncodingSettings, IntermediateArtifactProfile, NonEmptyArray, OutputFormat, Viewport } from './capture/types.js';
2
+ import { type RenderCliOptions } from './options.js';
3
+ export interface ResolvedRenderOutput {
4
+ name: string;
5
+ outPath: string;
6
+ format: OutputFormat;
7
+ manifestPath: string;
8
+ logFilePath: string;
9
+ viewport: Viewport;
10
+ fps: number;
11
+ force: boolean;
12
+ audio?: CaptureAudioTrack;
13
+ subtitles?: CaptureSubtitleTrack;
14
+ transition?: CaptureTransition;
15
+ intermediateArtifact: IntermediateArtifactProfile;
16
+ finalVideo: FinalVideoEncodingSettings;
17
+ debugFramesDir?: string;
18
+ }
19
+ export interface ResolvedRenderProject {
20
+ projectPath: string;
21
+ projectName: string;
22
+ summaryManifestPath: string;
23
+ timeoutMs: number;
24
+ scenes: NonEmptyArray<CaptureScene>;
25
+ outputs: NonEmptyArray<ResolvedRenderOutput>;
26
+ }
27
+ export declare function loadRenderProject(options: RenderCliOptions): Promise<ResolvedRenderProject>;