@t3lnet/sceneforge 1.0.21 → 1.0.23

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/README.md CHANGED
@@ -119,15 +119,13 @@ npx sceneforge split --demo my-demo --crf 15
119
119
 
120
120
  ## Viewport Settings (Recording)
121
121
 
122
- Control output video resolution and content zoom:
122
+ Control output video resolution:
123
123
 
124
124
  ```bash
125
125
  --viewport <WxH|preset> # Video resolution (default: 1440x900)
126
126
  # Presets: 720p, 1080p, 1440p, 4k
127
127
  --width <px> # Video width
128
128
  --height <px> # Video height
129
- --zoom <percent> # Content zoom: 100, 150, 200 (default: 100)
130
- # Makes UI elements appear larger
131
129
  ```
132
130
 
133
131
  **Examples:**
@@ -136,13 +134,10 @@ Control output video resolution and content zoom:
136
134
  # Record at 1080p
137
135
  npx sceneforge record --definition demo.yaml --base-url http://localhost:5173 --viewport 1080p
138
136
 
139
- # Record at 1080p with zoomed content (UI appears 1.5x larger)
140
- npx sceneforge record --definition demo.yaml --base-url http://localhost:5173 \
141
- --viewport 1080p --zoom 150
137
+ # Record at 4K
138
+ npx sceneforge record --definition demo.yaml --base-url http://localhost:5173 --viewport 4k
142
139
  ```
143
140
 
144
- **How zoom works:** `--viewport 1080p --zoom 150` records a 1920x1080 video where content appears 1.5x larger (browser viewport is actually 1280x720, scaled up to 1080p).
145
-
146
141
  ## Output Dimensions (Video Processing)
147
142
 
148
143
  Control final video resolution with support for different platforms and aspect ratios:
@@ -179,7 +174,7 @@ npx sceneforge concat --demo my-demo --output-size square
179
174
 
180
175
  # Full pipeline with all video options
181
176
  npx sceneforge pipeline --demo my-demo --base-url http://localhost:5173 \
182
- --viewport 1080p --zoom 150 \
177
+ --viewport 1080p \
183
178
  --output-size 1080p \
184
179
  --quality high
185
180
  ```
@@ -5,10 +5,7 @@ import { getFlagValue, hasFlag } from "../utils/args.js";
5
5
  import { getOutputPaths, resolveRoot, readJson } from "../utils/paths.js";
6
6
  import { sanitizeFileSegment } from "../utils/sanitize.js";
7
7
  import {
8
- getVideoEncodingArgs,
9
- parseQualityArgs,
10
- getQualityHelpText,
11
- logQualitySettings,
8
+ getIntermediateEncodingArgs,
12
9
  } from "../utils/quality.js";
13
10
  import {
14
11
  parseOutputDimensions,
@@ -21,6 +18,9 @@ function printHelp() {
21
18
  console.log(`
22
19
  Add audio to individual video step clips
23
20
 
21
+ Uses lossless encoding for intermediate files to preserve quality.
22
+ Final compression is applied only at the concat step.
23
+
24
24
  Usage:
25
25
  sceneforge add-audio [options]
26
26
 
@@ -31,7 +31,6 @@ Options:
31
31
  --root <path> Project root (defaults to cwd)
32
32
  --output-dir <path> Output directory (defaults to e2e/output or output)
33
33
  --help, -h Show this help message
34
- ${getQualityHelpText()}
35
34
  ${getOutputDimensionsHelpText()}
36
35
 
37
36
  Output:
@@ -39,9 +38,8 @@ Output:
39
38
 
40
39
  Examples:
41
40
  sceneforge add-audio --demo create-quote
42
- sceneforge add-audio --demo create-quote --quality high
43
41
  sceneforge add-audio --demo create-quote --output-size 1080p
44
- sceneforge add-audio --all --codec libx265
42
+ sceneforge add-audio --all
45
43
  `);
46
44
  }
47
45
 
@@ -55,11 +53,12 @@ function buildScaleFilter(outputDimensions) {
55
53
  return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black`;
56
54
  }
57
55
 
58
- async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVideoPath, qualityOptions = {}, outputDimensions = null) {
56
+ async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVideoPath, outputDimensions = null) {
59
57
  const videoDuration = await getMediaDuration(videoPath);
60
58
  const audioDuration = await getMediaDuration(audioPath);
61
59
  const targetDuration = audioDuration + padding;
62
- const encodingArgs = getVideoEncodingArgs(qualityOptions);
60
+ // Use lossless encoding for intermediate files to prevent generation loss
61
+ const encodingArgs = getIntermediateEncodingArgs({ includeAudio: true });
63
62
  const scaleFilter = buildScaleFilter(outputDimensions);
64
63
 
65
64
  if (targetDuration <= videoDuration) {
@@ -142,9 +141,9 @@ async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVid
142
141
  ]);
143
142
  }
144
143
 
145
- async function processDemo(demoName, paths, padding, qualityOptions = {}, outputDimensions = null) {
146
- console.log(`\n[audio] Processing: ${demoName}\n`);
147
- logQualitySettings(qualityOptions, "[audio]");
144
+ async function processDemo(demoName, paths, padding, outputDimensions = null) {
145
+ console.log(`\n[audio] Processing: ${demoName}`);
146
+ console.log("[audio] Using lossless encoding for intermediate files");
148
147
  logOutputDimensions(outputDimensions, "[audio]");
149
148
 
150
149
  const stepsManifestPath = path.join(paths.videosDir, demoName, "steps-manifest.json");
@@ -231,7 +230,7 @@ async function processDemo(demoName, paths, padding, qualityOptions = {}, output
231
230
  );
232
231
 
233
232
  try {
234
- await addAudioToStep(step.videoFile, audioSegment.audioFile, outputPath, padding, nextVideoPath, qualityOptions, outputDimensions);
233
+ await addAudioToStep(step.videoFile, audioSegment.audioFile, outputPath, padding, nextVideoPath, outputDimensions);
235
234
  outputFiles.push(outputPath);
236
235
  } catch (error) {
237
236
  console.error(`[audio] ${paddedIndex}. ${step.stepId}: ✗ Failed to process`);
@@ -264,7 +263,7 @@ async function processDemo(demoName, paths, padding, qualityOptions = {}, output
264
263
  console.log(`[audio] Output: ${path.join(paths.videosDir, demoName)}`);
265
264
  }
266
265
 
267
- async function processAll(paths, padding, qualityOptions = {}, outputDimensions = null) {
266
+ async function processAll(paths, padding, outputDimensions = null) {
268
267
  console.log("\n[audio] Processing all demos...\n");
269
268
 
270
269
  try {
@@ -295,7 +294,7 @@ async function processAll(paths, padding, qualityOptions = {}, outputDimensions
295
294
  console.log(`[audio] Found ${demosToProcess.length} demo(s) to process\n`);
296
295
 
297
296
  for (const demo of demosToProcess) {
298
- await processDemo(demo, paths, padding, qualityOptions, outputDimensions);
297
+ await processDemo(demo, paths, padding, outputDimensions);
299
298
  }
300
299
 
301
300
  await fs.rm(paths.tempDir, { recursive: true, force: true });
@@ -328,17 +327,16 @@ export async function runAddAudioCommand(argv) {
328
327
 
329
328
  const rootDir = resolveRoot(root);
330
329
  const paths = await getOutputPaths(rootDir, outputDir);
331
- const qualityOptions = parseQualityArgs(args, getFlagValue, hasFlag);
332
330
  const outputDimensions = parseOutputDimensions(args, getFlagValue);
333
331
 
334
332
  if (demo) {
335
- await processDemo(demo, paths, padding, qualityOptions, outputDimensions);
333
+ await processDemo(demo, paths, padding, outputDimensions);
336
334
  await fs.rm(paths.tempDir, { recursive: true, force: true });
337
335
  return;
338
336
  }
339
337
 
340
338
  if (all) {
341
- await processAll(paths, padding, qualityOptions, outputDimensions);
339
+ await processAll(paths, padding, outputDimensions);
342
340
  return;
343
341
  }
344
342
 
@@ -207,8 +207,6 @@ export async function runPipelineCommand(argv) {
207
207
  const viewport = getFlagValue(args, "--viewport");
208
208
  const viewportWidth = getFlagValue(args, "--width");
209
209
  const viewportHeight = getFlagValue(args, "--height");
210
- const zoom = getFlagValue(args, "--zoom");
211
- const deviceScaleFactor = getFlagValue(args, "--device-scale-factor");
212
210
 
213
211
  // Output dimension options (for video processing)
214
212
  const outputSize = getFlagValue(args, "--output-size");
@@ -261,13 +259,11 @@ export async function runPipelineCommand(argv) {
261
259
  if (crf) console.log(` - CRF: ${crf}`);
262
260
  if (codec) console.log(` - Codec: ${codec}`);
263
261
  }
264
- if (viewport || viewportWidth || viewportHeight || zoom || deviceScaleFactor) {
262
+ if (viewport || viewportWidth || viewportHeight) {
265
263
  console.log("\nViewport options:");
266
264
  if (viewport) console.log(` - Viewport: ${viewport}`);
267
265
  if (viewportWidth) console.log(` - Width: ${viewportWidth}`);
268
266
  if (viewportHeight) console.log(` - Height: ${viewportHeight}`);
269
- if (zoom) console.log(` - Zoom: ${zoom}%`);
270
- if (deviceScaleFactor) console.log(` - Device scale factor: ${deviceScaleFactor}`);
271
267
  }
272
268
  if (outputSize || outputWidth || outputHeight) {
273
269
  console.log("\nOutput dimension options:");
@@ -334,8 +330,9 @@ export async function runPipelineCommand(argv) {
334
330
  outputDimensionArgs.push("--output-height", outputHeight);
335
331
  }
336
332
 
333
+ // Split and add-audio use lossless encoding for intermediates (no quality args needed)
337
334
  await runStep("split", plan.split, () =>
338
- runSplitVideoCommand(["--demo", demoName, ...sharedArgs, ...qualityArgs, ...outputDimensionArgs])
335
+ runSplitVideoCommand(["--demo", demoName, ...sharedArgs, ...outputDimensionArgs])
339
336
  );
340
337
 
341
338
  const voiceArgs = ["--demo", demoName, ...sharedArgs];
@@ -347,13 +344,14 @@ export async function runPipelineCommand(argv) {
347
344
  }
348
345
  await runStep("voiceover", plan.voiceover, () => runGenerateVoiceoverCommand(voiceArgs));
349
346
 
350
- const audioArgs = ["--demo", demoName, ...sharedArgs, ...qualityArgs, ...outputDimensionArgs];
347
+ // Add-audio uses lossless encoding for intermediates (no quality args needed)
348
+ const audioArgs = ["--demo", demoName, ...sharedArgs, ...outputDimensionArgs];
351
349
  if (padding) {
352
350
  audioArgs.push("--padding", padding);
353
351
  }
354
352
  await runStep("add-audio", plan.addAudio, () => runAddAudioCommand(audioArgs));
355
353
 
356
- // Build concat args with media options and quality settings
354
+ // Concat applies final compression - quality args are used here
357
355
  const concatArgs = ["--demo", demoName, ...sharedArgs, ...qualityArgs, ...outputDimensionArgs];
358
356
  if (intro) {
359
357
  concatArgs.push("--intro", intro);
@@ -17,7 +17,6 @@ import {
17
17
  import { getMediaDuration } from "../utils/media.js";
18
18
  import {
19
19
  parseViewportArgs,
20
- parseDeviceScaleFactor,
21
20
  getViewportHelpText,
22
21
  } from "../utils/dimensions.js";
23
22
 
@@ -49,7 +48,7 @@ ${getViewportHelpText()}
49
48
  Examples:
50
49
  sceneforge record --definition demo-definitions/create-quote.yaml --base-url http://localhost:5173
51
50
  sceneforge record --demo create-quote --definitions-dir examples --base-url http://localhost:5173
52
- sceneforge record --demo create-quote --base-url http://localhost:5173 --viewport 1920x1080 --zoom 150
51
+ sceneforge record --demo create-quote --base-url http://localhost:5173 --viewport 1920x1080
53
52
  `);
54
53
  }
55
54
 
@@ -212,36 +211,9 @@ export async function runRecordDemoCommand(argv) {
212
211
  await ensureDir(outputPaths.outputDir);
213
212
  await ensureDir(outputPaths.videosDir);
214
213
 
215
- const requestedViewport = parseViewportArgs(args, getFlagValue);
216
- const zoomFactor = parseDeviceScaleFactor(args, getFlagValue);
217
-
218
- // To achieve "zoom", we use a smaller viewport but record at the requested resolution.
219
- // This makes content appear larger (zoomed in) in the final video.
220
- // Example: --viewport 1080p --zoom 150 means:
221
- // - User wants 1920x1080 output video
222
- // - Content should appear 1.5x larger (zoomed)
223
- // - So we set viewport to 1280x720 (1920/1.5 x 1080/1.5)
224
- // - Record at 1920x1080
225
- const viewport = zoomFactor !== 1
226
- ? {
227
- width: Math.round(requestedViewport.width / zoomFactor),
228
- height: Math.round(requestedViewport.height / zoomFactor),
229
- }
230
- : requestedViewport;
231
-
232
- // Recording size is the requested viewport (the final video dimensions)
233
- const recordVideoSize = {
234
- width: requestedViewport.width,
235
- height: requestedViewport.height,
236
- };
237
-
238
- // Log what's happening
239
- if (zoomFactor !== 1) {
240
- console.log(`[record] Viewport: ${viewport.width}x${viewport.height} (zoomed ${Math.round(zoomFactor * 100)}%)`);
241
- console.log(`[record] Output video: ${recordVideoSize.width}x${recordVideoSize.height}`);
242
- } else {
243
- console.log(`[record] Viewport: ${viewport.width}x${viewport.height}`);
244
- }
214
+ const viewport = parseViewportArgs(args, getFlagValue);
215
+
216
+ console.log(`[record] Viewport: ${viewport.width}x${viewport.height}`);
245
217
 
246
218
  const recordDir = path.join(outputPaths.videosDir, ".recordings", definition.name);
247
219
  if (!noVideo) {
@@ -255,9 +227,7 @@ export async function runRecordDemoCommand(argv) {
255
227
 
256
228
  const context = await browser.newContext({
257
229
  viewport,
258
- // Note: we achieve "zoom" by using a smaller viewport, not deviceScaleFactor
259
- // deviceScaleFactor only affects pixel density, not content size
260
- recordVideo: noVideo ? undefined : { dir: recordDir, size: recordVideoSize },
230
+ recordVideo: noVideo ? undefined : { dir: recordDir, size: viewport },
261
231
  storageState: storageState ? toAbsolute(rootDir, storageState) : undefined,
262
232
  locale: locale || undefined,
263
233
  extraHTTPHeaders: locale
@@ -5,10 +5,7 @@ import { getOutputPaths, readJson, resolveRoot } from "../utils/paths.js";
5
5
  import { getFlagValue, hasFlag } from "../utils/args.js";
6
6
  import { sanitizeFileSegment } from "../utils/sanitize.js";
7
7
  import {
8
- getVideoEncodingArgs,
9
- parseQualityArgs,
10
- getQualityHelpText,
11
- logQualitySettings,
8
+ getIntermediateEncodingArgs,
12
9
  } from "../utils/quality.js";
13
10
  import {
14
11
  parseOutputDimensions,
@@ -21,6 +18,9 @@ function printHelp() {
21
18
  console.log(`
22
19
  Split recorded demo video into per-step clips
23
20
 
21
+ Uses lossless encoding for intermediate files to preserve quality.
22
+ Final compression is applied only at the concat step.
23
+
24
24
  Usage:
25
25
  sceneforge split [options]
26
26
 
@@ -30,13 +30,10 @@ Options:
30
30
  --root <path> Project root (defaults to cwd)
31
31
  --output-dir <path> Output directory (defaults to e2e/output or output)
32
32
  --help, -h Show this help message
33
- ${getQualityHelpText()}
34
33
  ${getOutputDimensionsHelpText()}
35
34
 
36
35
  Examples:
37
36
  sceneforge split --demo create-quote
38
- sceneforge split --demo create-quote --quality high
39
- sceneforge split --demo create-quote --crf 15 --codec libx265
40
37
  sceneforge split --demo create-quote --output-size 1080p
41
38
  sceneforge split --all
42
39
  `);
@@ -71,9 +68,9 @@ async function findVideoFile(demoName, videosDir, testResultsDir) {
71
68
  return null;
72
69
  }
73
70
 
74
- async function splitDemo(demoName, paths, qualityOptions = {}, outputDimensions = null) {
75
- console.log(`\n[split] Processing: ${demoName}\n`);
76
- logQualitySettings(qualityOptions, "[split]");
71
+ async function splitDemo(demoName, paths, outputDimensions = null) {
72
+ console.log(`\n[split] Processing: ${demoName}`);
73
+ console.log("[split] Using lossless encoding for intermediate files");
77
74
  logOutputDimensions(outputDimensions, "[split]");
78
75
 
79
76
  const scriptPath = path.join(paths.scriptsDir, `${demoName}.json`);
@@ -135,7 +132,8 @@ async function splitDemo(demoName, paths, qualityOptions = {}, outputDimensions
135
132
  );
136
133
 
137
134
  try {
138
- const encodingArgs = getVideoEncodingArgs({ ...qualityOptions, includeAudio: false });
135
+ // Use lossless encoding for intermediate files to prevent generation loss
136
+ const encodingArgs = getIntermediateEncodingArgs({ includeAudio: false });
139
137
  const scaleArgs = getScaleFilterArgs(outputDimensions);
140
138
  await runFFmpeg([
141
139
  "-y",
@@ -192,7 +190,7 @@ async function splitDemo(demoName, paths, qualityOptions = {}, outputDimensions
192
190
  console.log(`[split] Manifest: ${manifestPath}`);
193
191
  }
194
192
 
195
- async function splitAll(paths, qualityOptions = {}, outputDimensions = null) {
193
+ async function splitAll(paths, outputDimensions = null) {
196
194
  console.log("\n[split] Processing all demos...\n");
197
195
 
198
196
  try {
@@ -210,7 +208,7 @@ async function splitAll(paths, qualityOptions = {}, outputDimensions = null) {
210
208
 
211
209
  for (const file of scriptFiles) {
212
210
  const demoName = path.basename(file, ".json");
213
- await splitDemo(demoName, paths, qualityOptions, outputDimensions);
211
+ await splitDemo(demoName, paths, outputDimensions);
214
212
  }
215
213
 
216
214
  console.log("\n[split] All demos processed!");
@@ -240,16 +238,15 @@ export async function runSplitVideoCommand(argv) {
240
238
 
241
239
  const rootDir = resolveRoot(root);
242
240
  const paths = await getOutputPaths(rootDir, outputDir);
243
- const qualityOptions = parseQualityArgs(args, getFlagValue, hasFlag);
244
241
  const outputDimensions = parseOutputDimensions(args, getFlagValue);
245
242
 
246
243
  if (demo) {
247
- await splitDemo(demo, paths, qualityOptions, outputDimensions);
244
+ await splitDemo(demo, paths, outputDimensions);
248
245
  return;
249
246
  }
250
247
 
251
248
  if (all) {
252
- await splitAll(paths, qualityOptions, outputDimensions);
249
+ await splitAll(paths, outputDimensions);
253
250
  return;
254
251
  }
255
252
 
@@ -3,7 +3,6 @@
3
3
  *
4
4
  * Browser Viewport:
5
5
  * - Controls the browser window size during recording
6
- * - Supports device scale factor (zoom) for high-DPI capture
7
6
  *
8
7
  * Output Dimensions:
9
8
  * - Controls the final video resolution after processing
@@ -35,7 +34,6 @@ export const OUTPUT_PRESETS = {
35
34
  };
36
35
 
37
36
  export const DEFAULT_VIEWPORT = { width: 1440, height: 900 };
38
- export const DEFAULT_DEVICE_SCALE_FACTOR = 1;
39
37
 
40
38
  /**
41
39
  * Parse a dimension string like "1920x1080" or a preset name like "1080p"
@@ -85,35 +83,6 @@ export function parseViewportArgs(args, getFlagValue) {
85
83
  return parseDimensions(viewportValue, VIEWPORT_PRESETS, DEFAULT_VIEWPORT);
86
84
  }
87
85
 
88
- /**
89
- * Parse device scale factor (zoom) from CLI arguments
90
- * @param {string[]} args - CLI arguments
91
- * @param {Function} getFlagValue - Function to get flag values
92
- * @returns {number} Device scale factor (1 = 100%, 1.5 = 150%, 2 = 200%)
93
- */
94
- export function parseDeviceScaleFactor(args, getFlagValue) {
95
- const zoomValue = getFlagValue(args, "--zoom");
96
- const scaleValue = getFlagValue(args, "--device-scale-factor");
97
-
98
- // --zoom takes a percentage (100, 150, 200)
99
- if (zoomValue !== null && zoomValue !== undefined) {
100
- const zoom = Number(zoomValue);
101
- if (Number.isFinite(zoom) && zoom > 0) {
102
- return zoom / 100; // Convert percentage to scale factor
103
- }
104
- }
105
-
106
- // --device-scale-factor takes the actual scale (1, 1.5, 2)
107
- if (scaleValue !== null && scaleValue !== undefined) {
108
- const scale = Number(scaleValue);
109
- if (Number.isFinite(scale) && scale > 0) {
110
- return scale;
111
- }
112
- }
113
-
114
- return DEFAULT_DEVICE_SCALE_FACTOR;
115
- }
116
-
117
86
  /**
118
87
  * Parse output dimensions from CLI arguments
119
88
  * @param {string[]} args - CLI arguments
@@ -192,11 +161,7 @@ Viewport Options (Recording):
192
161
  Presets: 720p, 1080p, 1440p, 4k
193
162
  Example: --viewport 1920x1080 or --viewport 1080p
194
163
  --width <px> Video width (overrides --viewport)
195
- --height <px> Video height (overrides --viewport)
196
- --zoom <percent> Content zoom level: 100, 150, 200 (default: 100)
197
- Makes UI elements appear larger in the video
198
- Example: --viewport 1080p --zoom 150 records at
199
- 1920x1080 with content appearing 1.5x larger`;
164
+ --height <px> Video height (overrides --viewport)`;
200
165
  }
201
166
 
202
167
  /**
@@ -217,28 +182,6 @@ Output Dimensions:
217
182
  If not specified, keeps original recording dimensions`;
218
183
  }
219
184
 
220
- /**
221
- * Log viewport settings being used
222
- * @param {Object} viewport - { width, height }
223
- * @param {number} deviceScaleFactor - Scale factor
224
- * @param {string} prefix - Log prefix
225
- */
226
- export function logViewportSettings(viewport, deviceScaleFactor, prefix = "") {
227
- const zoomPercent = Math.round(deviceScaleFactor * 100);
228
- console.log(
229
- `${prefix} Viewport: ${viewport.width}x${viewport.height} @ ${zoomPercent}% zoom`
230
- );
231
- if (deviceScaleFactor !== 1) {
232
- const effectiveRes = {
233
- width: Math.round(viewport.width * deviceScaleFactor),
234
- height: Math.round(viewport.height * deviceScaleFactor),
235
- };
236
- console.log(
237
- `${prefix} Effective capture resolution: ${effectiveRes.width}x${effectiveRes.height}`
238
- );
239
- }
240
- }
241
-
242
185
  /**
243
186
  * Log output dimension settings
244
187
  * @param {Object|null} dimensions - { width, height } or null
@@ -47,6 +47,12 @@ export const DEFAULT_QUALITY = "medium";
47
47
  export const DEFAULT_AUDIO_CODEC = "aac";
48
48
  export const DEFAULT_AUDIO_BITRATE = "192k";
49
49
 
50
+ // Lossless encoding for intermediate files to prevent generation loss
51
+ // CRF 0 = mathematically lossless for x264/x265
52
+ // Using "ultrafast" preset since these are temporary files and encoding speed matters
53
+ export const INTERMEDIATE_CRF = 0;
54
+ export const INTERMEDIATE_PRESET = "ultrafast";
55
+
50
56
  /**
51
57
  * Get video encoding arguments for FFmpeg
52
58
  * @param {Object} options
@@ -78,6 +84,36 @@ export function getVideoEncodingArgs(options = {}) {
78
84
  return args;
79
85
  }
80
86
 
87
+ /**
88
+ * Get lossless encoding arguments for intermediate files.
89
+ * Uses CRF 0 (lossless) to prevent generation loss during multi-step processing.
90
+ * Final compression should be applied only at the last step (concat).
91
+ *
92
+ * @param {Object} options
93
+ * @param {string} [options.codec] - Video codec: libx264, libx265 (default: libx264)
94
+ * @param {boolean} [options.includeAudio] - Include audio encoding args
95
+ * @returns {string[]} FFmpeg arguments for lossless intermediate encoding
96
+ */
97
+ export function getIntermediateEncodingArgs(options = {}) {
98
+ const {
99
+ codec = DEFAULT_CODEC,
100
+ includeAudio = true,
101
+ } = options;
102
+
103
+ const args = [
104
+ "-c:v", codec,
105
+ "-preset", INTERMEDIATE_PRESET,
106
+ "-crf", String(INTERMEDIATE_CRF),
107
+ ];
108
+
109
+ if (includeAudio) {
110
+ // Use high-quality audio for intermediates too
111
+ args.push("-c:a", DEFAULT_AUDIO_CODEC, "-b:a", DEFAULT_AUDIO_BITRATE);
112
+ }
113
+
114
+ return args;
115
+ }
116
+
81
117
  /**
82
118
  * Parse quality-related CLI arguments
83
119
  * @param {string[]} args - CLI arguments
@@ -28,7 +28,6 @@ sceneforge record --definition <path> [options]
28
28
  | `--viewport` | Target video resolution (preset or WxH, default: 1440x900) |
29
29
  | `--width` | Video width (overrides --viewport) |
30
30
  | `--height` | Video height (overrides --viewport) |
31
- | `--zoom` | Content zoom level: 100, 150, 200 (default: 100) - makes UI larger |
32
31
 
33
32
  **Viewport Presets:** 720p (1280x720), 1080p (1920x1080), 1440p (2560x1440), 4k (3840x2160)
34
33
 
@@ -46,8 +45,8 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --storage ./auth.json
46
45
  # Record at 1080p resolution
47
46
  sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p
48
47
 
49
- # Record at 1080p with zoomed content (UI appears 1.5x larger)
50
- sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p --zoom 150
48
+ # Record at 4K resolution
49
+ sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 4k
51
50
  ```
52
51
 
53
52
  ### setup
@@ -94,7 +93,6 @@ sceneforge pipeline --definition <path> [options]
94
93
  | `--crf` | Override CRF value |
95
94
  | `--codec` | Video codec: libx264, libx265 |
96
95
  | `--viewport` | Target video resolution (preset or WxH) |
97
- | `--zoom` | Content zoom level: 100, 150, 200 (makes UI larger) |
98
96
  | `--output-size` | Final output dimensions (preset or WxH) |
99
97
  | `--output-width` | Output width (-1 for auto) |
100
98
  | `--output-height` | Output height (-1 for auto) |
@@ -116,9 +114,9 @@ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --quality high --outpu
116
114
  # TikTok/YouTube Shorts format
117
115
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 --output-size tiktok --quality high
118
116
 
119
- # Full options: viewport + zoom + output scaling
117
+ # Full options: viewport + output scaling
120
118
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
121
- --viewport 1080p --zoom 150 \
119
+ --viewport 1080p \
122
120
  --output-size 1080p \
123
121
  --quality high
124
122
  ```
@@ -364,7 +362,7 @@ sceneforge split --demo my-demo --crf 15
364
362
 
365
363
  ## Viewport Settings
366
364
 
367
- Control output video resolution and content zoom during recording.
365
+ Control output video resolution during recording.
368
366
 
369
367
  ### CLI Flags (record, pipeline)
370
368
 
@@ -372,7 +370,6 @@ Control output video resolution and content zoom during recording.
372
370
  --viewport <WxH|preset> # Target video resolution (default: 1440x900)
373
371
  --width <px> # Video width (overrides --viewport)
374
372
  --height <px> # Video height (overrides --viewport)
375
- --zoom <percent> # Content zoom: 100, 150, 200 (default: 100)
376
373
  ```
377
374
 
378
375
  ### Viewport Presets
@@ -384,16 +381,6 @@ Control output video resolution and content zoom during recording.
384
381
  | `1440p` | 2560x1440 |
385
382
  | `4k` | 3840x2160 |
386
383
 
387
- ### How Zoom Works
388
-
389
- - `--viewport` sets the output video resolution (e.g., 1920x1080 for 1080p)
390
- - `--zoom` makes UI elements appear larger by using a smaller browser viewport
391
- - Example: `--viewport 1080p --zoom 150` records a 1920x1080 video
392
- - Actual browser viewport: 1280x720 (1920/1.5 x 1080/1.5)
393
- - Output video: 1920x1080
394
- - Result: Content appears 1.5x larger/zoomed
395
- - Useful for demos where UI elements need to be more visible and readable
396
-
397
384
  ## Output Dimensions
398
385
 
399
386
  Control final video resolution after processing. Supports landscape, portrait, and square formats.
@@ -443,7 +430,7 @@ sceneforge concat --demo my-demo --output-size square
443
430
 
444
431
  # Full pipeline with all options
445
432
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
446
- --viewport 1080p --zoom 150 \
433
+ --viewport 1080p \
447
434
  --output-size 1080p \
448
435
  --quality high
449
436
  ```
@@ -28,7 +28,6 @@ sceneforge record --definition <path> [options]
28
28
  | `--viewport` | Target video resolution (preset or WxH, default: 1440x900) |
29
29
  | `--width` | Video width (overrides --viewport) |
30
30
  | `--height` | Video height (overrides --viewport) |
31
- | `--zoom` | Content zoom level: 100, 150, 200 (default: 100) - makes UI larger |
32
31
 
33
32
  **Viewport Presets:** 720p (1280x720), 1080p (1920x1080), 1440p (2560x1440), 4k (3840x2160)
34
33
 
@@ -46,8 +45,8 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --storage ./auth.json
46
45
  # Record at 1080p resolution
47
46
  sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p
48
47
 
49
- # Record at 1080p with zoomed content (UI appears 1.5x larger)
50
- sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 1080p --zoom 150
48
+ # Record at 4K resolution
49
+ sceneforge record -d demo.yaml -b http://localhost:3000 --viewport 4k
51
50
  ```
52
51
 
53
52
  ### setup
@@ -94,7 +93,6 @@ sceneforge pipeline --definition <path> [options]
94
93
  | `--crf` | Override CRF value |
95
94
  | `--codec` | Video codec: libx264, libx265 |
96
95
  | `--viewport` | Target video resolution (preset or WxH) |
97
- | `--zoom` | Content zoom level: 100, 150, 200 (makes UI larger) |
98
96
  | `--output-size` | Final output dimensions (preset or WxH) |
99
97
  | `--output-width` | Output width (-1 for auto) |
100
98
  | `--output-height` | Output height (-1 for auto) |
@@ -116,9 +114,9 @@ sceneforge pipeline -d demo.yaml -b http://localhost:3000 --quality high --outpu
116
114
  # TikTok/YouTube Shorts format
117
115
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 --output-size tiktok --quality high
118
116
 
119
- # Full options: viewport + zoom + output scaling
117
+ # Full options: viewport + output scaling
120
118
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
121
- --viewport 1080p --zoom 150 \
119
+ --viewport 1080p \
122
120
  --output-size 1080p \
123
121
  --quality high
124
122
  ```
@@ -364,7 +362,7 @@ sceneforge split --demo my-demo --crf 15
364
362
 
365
363
  ## Viewport Settings
366
364
 
367
- Control output video resolution and content zoom during recording.
365
+ Control output video resolution during recording.
368
366
 
369
367
  ### CLI Flags (record, pipeline)
370
368
 
@@ -372,7 +370,6 @@ Control output video resolution and content zoom during recording.
372
370
  --viewport <WxH|preset> # Target video resolution (default: 1440x900)
373
371
  --width <px> # Video width (overrides --viewport)
374
372
  --height <px> # Video height (overrides --viewport)
375
- --zoom <percent> # Content zoom: 100, 150, 200 (default: 100)
376
373
  ```
377
374
 
378
375
  ### Viewport Presets
@@ -384,16 +381,6 @@ Control output video resolution and content zoom during recording.
384
381
  | `1440p` | 2560x1440 |
385
382
  | `4k` | 3840x2160 |
386
383
 
387
- ### How Zoom Works
388
-
389
- - `--viewport` sets the output video resolution (e.g., 1920x1080 for 1080p)
390
- - `--zoom` makes UI elements appear larger by using a smaller browser viewport
391
- - Example: `--viewport 1080p --zoom 150` records a 1920x1080 video
392
- - Actual browser viewport: 1280x720 (1920/1.5 x 1080/1.5)
393
- - Output video: 1920x1080
394
- - Result: Content appears 1.5x larger/zoomed
395
- - Useful for demos where UI elements need to be more visible and readable
396
-
397
384
  ## Output Dimensions
398
385
 
399
386
  Control final video resolution after processing. Supports landscape, portrait, and square formats.
@@ -443,7 +430,7 @@ sceneforge concat --demo my-demo --output-size square
443
430
 
444
431
  # Full pipeline with all options
445
432
  sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
446
- --viewport 1080p --zoom 150 \
433
+ --viewport 1080p \
447
434
  --output-size 1080p \
448
435
  --quality high
449
436
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t3lnet/sceneforge",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "SceneForge runner and generation utilities for YAML-driven demos",
5
5
  "license": "MIT",
6
6
  "author": "T3LNET",