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
@@ -0,0 +1,103 @@
1
+ import type { VideoProbeOutcome } from './capture/ffmpeg.js';
2
+ import type { CaptureJob, CaptureResult, CaptureScene, FinalVideoEncodingSettings, IntermediateArtifactProfile, NonEmptyArray, PageCaptureResult } from './capture/types.js';
3
+ import type { ResolvedRenderOutput } from './project.js';
4
+ export interface PlannedSceneCapture {
5
+ sceneIndex: number;
6
+ scene: CaptureScene;
7
+ clipPath: string;
8
+ debugFramesDir?: string;
9
+ job: CaptureJob;
10
+ }
11
+ export interface PlannedComposition {
12
+ outPath: string;
13
+ format: ResolvedRenderOutput['format'];
14
+ fps: number;
15
+ audio: ResolvedRenderOutput['audio'];
16
+ subtitles: ResolvedRenderOutput['subtitles'];
17
+ transition: ResolvedRenderOutput['transition'];
18
+ finalVideo: FinalVideoEncodingSettings;
19
+ force: boolean;
20
+ }
21
+ export interface CompositionCapabilities {
22
+ requiresPreciseClipTiming: boolean;
23
+ timingRequirementSources: string[];
24
+ }
25
+ export interface RenderPlan {
26
+ output: ResolvedRenderOutput;
27
+ tempDir: string;
28
+ sceneCaptures: NonEmptyArray<PlannedSceneCapture>;
29
+ composition: PlannedComposition;
30
+ capabilities: CompositionCapabilities;
31
+ }
32
+ export interface SceneCaptureArtifact {
33
+ captureMetrics: CaptureResult;
34
+ clip: {
35
+ path: string;
36
+ durationSeconds: number;
37
+ frameCount?: number;
38
+ };
39
+ probe: VideoProbeOutcome;
40
+ }
41
+ export interface RenderCaptureMetrics {
42
+ frameCount: number;
43
+ durationSeconds: number;
44
+ truncated: boolean;
45
+ scenes: PageCaptureResult[];
46
+ }
47
+ export interface RenderArtifactMetrics {
48
+ videoPath: string;
49
+ frameCount: number;
50
+ durationSeconds: number;
51
+ truncated: boolean;
52
+ probe: {
53
+ status: VideoProbeOutcome['status'];
54
+ source: 'ffprobe' | 'estimate';
55
+ warning?: string;
56
+ };
57
+ }
58
+ export declare function createRenderPlan(input: {
59
+ output: ResolvedRenderOutput;
60
+ scenes: NonEmptyArray<CaptureScene>;
61
+ timeoutMs: number;
62
+ tempDir: string;
63
+ }): RenderPlan;
64
+ export declare function buildSceneCaptureArtifact(input: {
65
+ rawCapture: CaptureResult;
66
+ clipPath: string;
67
+ probe: VideoProbeOutcome;
68
+ }): SceneCaptureArtifact;
69
+ export declare function buildCaptureMetrics(sceneArtifacts: SceneCaptureArtifact[]): RenderCaptureMetrics;
70
+ export declare function buildArtifactMetrics(input: {
71
+ captureResult: CaptureResult;
72
+ probe: VideoProbeOutcome;
73
+ }): RenderArtifactMetrics;
74
+ export declare function collectProbeWarnings(sceneArtifacts: SceneCaptureArtifact[], finalProbe: VideoProbeOutcome): string[];
75
+ export declare function shouldFailOnSceneClipProbe(input: {
76
+ probe: VideoProbeOutcome;
77
+ capabilities: CompositionCapabilities;
78
+ }): boolean;
79
+ export declare function buildSceneClipProbeFailureMessage(input: {
80
+ sceneIndex: number;
81
+ probe: VideoProbeOutcome;
82
+ capabilities: CompositionCapabilities;
83
+ }): string;
84
+ export declare function buildProbeWarningEvent(input: {
85
+ target: 'scene-clip' | 'output';
86
+ index?: number;
87
+ probe: VideoProbeOutcome;
88
+ }): {
89
+ event: string;
90
+ message: string;
91
+ metadata: Record<string, unknown>;
92
+ } | null;
93
+ export declare function resolveCompositionCapabilities(input: {
94
+ transition?: ResolvedRenderOutput['transition'];
95
+ intermediateArtifact: IntermediateArtifactProfile;
96
+ }): CompositionCapabilities;
97
+ export declare function buildComposedCaptureResult(input: {
98
+ outPath: string;
99
+ fps: number;
100
+ sceneArtifacts: SceneCaptureArtifact[];
101
+ transition?: ResolvedRenderOutput['transition'];
102
+ probe: VideoProbeOutcome;
103
+ }): CaptureResult;
@@ -0,0 +1,144 @@
1
+ import { join } from 'node:path';
2
+ export function createRenderPlan(input) {
3
+ const { output, scenes, timeoutMs, tempDir } = input;
4
+ const sceneCaptures = scenes.map((scene, sceneIndex) => {
5
+ const clipPath = join(tempDir, `scene-${String(sceneIndex + 1).padStart(3, '0')}${output.intermediateArtifact.extension}`);
6
+ const debugFramesDir = output.debugFramesDir
7
+ ? join(output.debugFramesDir, `scene-${String(sceneIndex + 1).padStart(3, '0')}`)
8
+ : undefined;
9
+ return {
10
+ sceneIndex,
11
+ scene,
12
+ clipPath,
13
+ debugFramesDir,
14
+ job: {
15
+ scenes: [scene],
16
+ outPath: clipPath,
17
+ format: output.intermediateArtifact.format,
18
+ viewport: output.viewport,
19
+ fps: output.fps,
20
+ timeoutMs,
21
+ debugFramesDir,
22
+ videoEncoding: output.intermediateArtifact.videoEncoding,
23
+ includeHoldAfterFinalScene: sceneIndex < scenes.length - 1,
24
+ force: true,
25
+ },
26
+ };
27
+ });
28
+ return {
29
+ output,
30
+ tempDir,
31
+ sceneCaptures,
32
+ composition: {
33
+ outPath: output.outPath,
34
+ format: output.format,
35
+ fps: output.fps,
36
+ audio: output.audio,
37
+ subtitles: output.subtitles,
38
+ transition: output.transition,
39
+ finalVideo: output.finalVideo,
40
+ force: output.force,
41
+ },
42
+ capabilities: resolveCompositionCapabilities({
43
+ transition: output.transition,
44
+ intermediateArtifact: output.intermediateArtifact,
45
+ }),
46
+ };
47
+ }
48
+ export function buildSceneCaptureArtifact(input) {
49
+ const durationSeconds = input.probe.result?.durationSeconds ?? input.rawCapture.durationSeconds;
50
+ const frameCount = input.probe.result?.frameCount ?? input.rawCapture.frameCount;
51
+ return {
52
+ captureMetrics: input.rawCapture,
53
+ clip: {
54
+ path: input.clipPath,
55
+ durationSeconds,
56
+ frameCount,
57
+ },
58
+ probe: input.probe,
59
+ };
60
+ }
61
+ export function buildCaptureMetrics(sceneArtifacts) {
62
+ return {
63
+ frameCount: sceneArtifacts.reduce((sum, artifact) => sum + artifact.captureMetrics.frameCount, 0),
64
+ durationSeconds: sceneArtifacts.reduce((sum, artifact) => sum + artifact.captureMetrics.durationSeconds, 0),
65
+ truncated: sceneArtifacts.some((artifact) => artifact.captureMetrics.truncated),
66
+ scenes: sceneArtifacts.flatMap((artifact) => artifact.captureMetrics.pages),
67
+ };
68
+ }
69
+ export function buildArtifactMetrics(input) {
70
+ return {
71
+ videoPath: input.captureResult.outPath,
72
+ frameCount: input.captureResult.frameCount,
73
+ durationSeconds: input.captureResult.durationSeconds,
74
+ truncated: input.captureResult.truncated,
75
+ probe: {
76
+ status: input.probe.status,
77
+ source: input.probe.status === 'probed' ? 'ffprobe' : 'estimate',
78
+ warning: input.probe.warning,
79
+ },
80
+ };
81
+ }
82
+ export function collectProbeWarnings(sceneArtifacts, finalProbe) {
83
+ const warnings = new Set();
84
+ for (const artifact of sceneArtifacts) {
85
+ if (artifact.probe.status !== 'probed') {
86
+ warnings.add(`scene_clip_probe_${artifact.probe.status}`);
87
+ }
88
+ }
89
+ if (finalProbe.status !== 'probed') {
90
+ warnings.add(`output_probe_${finalProbe.status}`);
91
+ }
92
+ return [...warnings];
93
+ }
94
+ export function shouldFailOnSceneClipProbe(input) {
95
+ return (input.probe.status !== 'probed' &&
96
+ input.capabilities.requiresPreciseClipTiming);
97
+ }
98
+ export function buildSceneClipProbeFailureMessage(input) {
99
+ return `Scene clip probe failed for scene ${input.sceneIndex + 1} (${input.probe.status}). Precise clip timing is required by: ${input.capabilities.timingRequirementSources.join(', ')}. ${input.probe.warning ?? ''}`.trim();
100
+ }
101
+ export function buildProbeWarningEvent(input) {
102
+ if (input.probe.status === 'probed') {
103
+ return null;
104
+ }
105
+ return {
106
+ event: 'probe.warning',
107
+ message: input.target === 'scene-clip'
108
+ ? 'Scene clip probe warning'
109
+ : 'Output probe warning',
110
+ metadata: {
111
+ target: input.target,
112
+ index: input.index,
113
+ status: input.probe.status,
114
+ warning: input.probe.warning,
115
+ },
116
+ };
117
+ }
118
+ export function resolveCompositionCapabilities(input) {
119
+ const timingRequirementSources = [];
120
+ if (input.transition?.kind === 'crossfade') {
121
+ timingRequirementSources.push('transition:crossfade');
122
+ }
123
+ return {
124
+ requiresPreciseClipTiming: timingRequirementSources.length > 0,
125
+ timingRequirementSources,
126
+ };
127
+ }
128
+ export function buildComposedCaptureResult(input) {
129
+ const pages = input.sceneArtifacts.flatMap((artifact) => artifact.captureMetrics.pages);
130
+ const totalClipDurationSeconds = input.sceneArtifacts.reduce((sum, artifact) => sum + artifact.clip.durationSeconds, 0);
131
+ const transitionOverlapSeconds = input.transition?.kind === 'crossfade'
132
+ ? input.transition.durationSeconds * Math.max(0, pages.length - 1)
133
+ : 0;
134
+ const estimatedDurationSeconds = Math.max(0, totalClipDurationSeconds - transitionOverlapSeconds);
135
+ const durationSeconds = input.probe.result?.durationSeconds ?? estimatedDurationSeconds;
136
+ return {
137
+ outPath: input.outPath,
138
+ frameCount: input.probe.result?.frameCount ??
139
+ Math.max(1, Math.round(durationSeconds * input.fps)),
140
+ durationSeconds,
141
+ pages,
142
+ truncated: input.sceneArtifacts.some((artifact) => artifact.captureMetrics.truncated),
143
+ };
144
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProgressReporter } from './capture/progress.js';
2
+ import type { CaptureOptions, CaptureRunResult } from './capture/types.js';
3
+ export declare function runCaptureCommand(options: CaptureOptions, progress?: ProgressReporter, signal?: AbortSignal): Promise<CaptureRunResult>;
@@ -1,22 +1,26 @@
1
1
  import { writeFile } from 'node:fs/promises';
2
- import { captureVideo } from './capture/capture.js';
2
+ import { AbortError, captureVideo } from './capture/capture.js';
3
3
  import { createCaptureLogger } from './capture/logger.js';
4
- export async function runCaptureCommand(options) {
4
+ import { sanitizeUrl } from './capture/utils.js';
5
+ export async function runCaptureCommand(options, progress, signal) {
5
6
  const logger = createCaptureLogger(options.logFilePath);
6
7
  const startedAt = new Date();
8
+ let capture;
9
+ const sanitizedUrls = options.urls.map((u) => sanitizeUrl(u));
7
10
  await logger.info('capture.start', 'Capture started', {
8
- urls: options.urls.map((u) => u.toString()),
11
+ urls: sanitizedUrls,
9
12
  outPath: options.outPath,
10
13
  manifestPath: options.manifestPath,
11
14
  logFilePath: options.logFilePath,
12
15
  });
13
16
  try {
14
- const capture = await captureVideo(options, logger);
17
+ capture = await captureVideo(options, logger, progress, signal);
15
18
  const finishedAt = new Date();
16
19
  const warnings = capture.truncated ? ['scroll_height_truncated'] : [];
17
20
  const manifest = buildManifest({
18
21
  status: 'succeeded',
19
22
  options,
23
+ sanitizedUrls,
20
24
  startedAt,
21
25
  finishedAt,
22
26
  warnings,
@@ -38,16 +42,22 @@ export async function runCaptureCommand(options) {
38
42
  }
39
43
  catch (error) {
40
44
  const finishedAt = new Date();
45
+ const isCancelled = error instanceof AbortError;
46
+ const warnings = capture?.truncated ? ['scroll_height_truncated'] : [];
41
47
  const manifest = buildManifest({
42
- status: 'failed',
48
+ status: isCancelled ? 'cancelled' : 'failed',
43
49
  options,
50
+ sanitizedUrls,
44
51
  startedAt,
45
52
  finishedAt,
46
- warnings: [],
47
- videoCreated: false,
48
- error,
53
+ warnings,
54
+ videoCreated: capture !== undefined,
55
+ result: capture,
56
+ error: isCancelled ? undefined : error,
49
57
  });
50
- await logger.error('capture.failed', 'Capture failed', {
58
+ const logEvent = isCancelled ? 'capture.cancelled' : 'capture.failed';
59
+ const logMessage = isCancelled ? 'Capture cancelled' : 'Capture failed';
60
+ await logger.error(logEvent, logMessage, {
51
61
  name: manifest.error?.name,
52
62
  message: manifest.error?.message,
53
63
  manifestPath: options.manifestPath,
@@ -75,7 +85,7 @@ function buildManifest(input) {
75
85
  arch: process.arch,
76
86
  },
77
87
  options: {
78
- urls: input.options.urls.map((u) => u.toString()),
88
+ urls: input.sanitizedUrls,
79
89
  viewport: input.options.viewport,
80
90
  fps: input.options.fps,
81
91
  duration: input.options.duration,
@@ -0,0 +1,17 @@
1
+ import type { ProgressReporter } from './capture/progress.js';
2
+ import type { CaptureResult } from './capture/types.js';
3
+ import type { RenderCliOptions } from './options.js';
4
+ export interface RenderOutputResult {
5
+ name: string;
6
+ fps: number;
7
+ format: 'mp4' | 'webm';
8
+ capture: CaptureResult;
9
+ manifestPath: string;
10
+ logFilePath: string;
11
+ }
12
+ export interface RenderCommandResult {
13
+ projectPath: string;
14
+ summaryManifestPath: string;
15
+ outputs: RenderOutputResult[];
16
+ }
17
+ export declare function runRenderCommand(options: RenderCliOptions, progress?: ProgressReporter, signal?: AbortSignal): Promise<RenderCommandResult>;