@t3lnet/sceneforge 1.0.22 → 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 +4 -9
- package/cli/commands/add-audio-to-steps.js +16 -18
- package/cli/commands/pipeline.js +6 -8
- package/cli/commands/record-demo.js +3 -37
- package/cli/commands/split-video.js +13 -16
- package/cli/utils/dimensions.js +1 -58
- package/cli/utils/quality.js +36 -0
- package/context/templates/base/cli-reference.md +6 -19
- package/dist/templates/base/cli-reference.md +6 -19
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
146
|
-
console.log(`\n[audio] Processing: ${demoName}
|
|
147
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
339
|
+
await processAll(paths, padding, outputDimensions);
|
|
342
340
|
return;
|
|
343
341
|
}
|
|
344
342
|
|
package/cli/commands/pipeline.js
CHANGED
|
@@ -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
|
|
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, ...
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
51
|
+
sceneforge record --demo create-quote --base-url http://localhost:5173 --viewport 1920x1080
|
|
53
52
|
`);
|
|
54
53
|
}
|
|
55
54
|
|
|
@@ -213,21 +212,8 @@ export async function runRecordDemoCommand(argv) {
|
|
|
213
212
|
await ensureDir(outputPaths.videosDir);
|
|
214
213
|
|
|
215
214
|
const viewport = parseViewportArgs(args, getFlagValue);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
// To achieve "zoom", we use CSS zoom to make content appear larger.
|
|
219
|
-
// The viewport stays at the requested size, and we inject CSS zoom after page load.
|
|
220
|
-
// Example: --viewport 4k --zoom 200 means:
|
|
221
|
-
// - Viewport is 3840x2160 (4K)
|
|
222
|
-
// - CSS zoom: 2 makes content appear 2x larger
|
|
223
|
-
// - User sees the equivalent of 1920x1080 content filling the 4K frame
|
|
224
|
-
|
|
225
|
-
// Log what's happening
|
|
226
|
-
if (zoomFactor !== 1) {
|
|
227
|
-
console.log(`[record] Viewport: ${viewport.width}x${viewport.height} with ${Math.round(zoomFactor * 100)}% CSS zoom`);
|
|
228
|
-
} else {
|
|
229
|
-
console.log(`[record] Viewport: ${viewport.width}x${viewport.height}`);
|
|
230
|
-
}
|
|
215
|
+
|
|
216
|
+
console.log(`[record] Viewport: ${viewport.width}x${viewport.height}`);
|
|
231
217
|
|
|
232
218
|
const recordDir = path.join(outputPaths.videosDir, ".recordings", definition.name);
|
|
233
219
|
if (!noVideo) {
|
|
@@ -252,32 +238,12 @@ export async function runRecordDemoCommand(argv) {
|
|
|
252
238
|
});
|
|
253
239
|
|
|
254
240
|
const page = await context.newPage();
|
|
255
|
-
|
|
256
|
-
// Apply CSS zoom if requested (must be done before navigation to affect all content)
|
|
257
|
-
if (zoomFactor !== 1) {
|
|
258
|
-
await page.addInitScript((zoom) => {
|
|
259
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
260
|
-
document.documentElement.style.zoom = String(zoom);
|
|
261
|
-
});
|
|
262
|
-
// Also set immediately in case DOMContentLoaded already fired
|
|
263
|
-
if (document.documentElement) {
|
|
264
|
-
document.documentElement.style.zoom = String(zoom);
|
|
265
|
-
}
|
|
266
|
-
}, zoomFactor);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
241
|
const video = page.video();
|
|
270
242
|
const videoRecordingStartTime = Date.now();
|
|
271
243
|
const startUrl = resolveStartUrl(startPath, baseUrl);
|
|
272
244
|
|
|
273
245
|
if (startUrl) {
|
|
274
246
|
await page.goto(startUrl, { waitUntil: "networkidle" });
|
|
275
|
-
// Ensure zoom is applied after navigation
|
|
276
|
-
if (zoomFactor !== 1) {
|
|
277
|
-
await page.evaluate((zoom) => {
|
|
278
|
-
document.documentElement.style.zoom = String(zoom);
|
|
279
|
-
}, zoomFactor);
|
|
280
|
-
}
|
|
281
247
|
}
|
|
282
248
|
const result = await runDemo(
|
|
283
249
|
definition,
|
|
@@ -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
|
-
|
|
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,
|
|
75
|
-
console.log(`\n[split] Processing: ${demoName}
|
|
76
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
244
|
+
await splitDemo(demo, paths, outputDimensions);
|
|
248
245
|
return;
|
|
249
246
|
}
|
|
250
247
|
|
|
251
248
|
if (all) {
|
|
252
|
-
await splitAll(paths,
|
|
249
|
+
await splitAll(paths, outputDimensions);
|
|
253
250
|
return;
|
|
254
251
|
}
|
|
255
252
|
|
package/cli/utils/dimensions.js
CHANGED
|
@@ -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
|
package/cli/utils/quality.js
CHANGED
|
@@ -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
|
|
50
|
-
sceneforge record -d demo.yaml -b http://localhost:3000 --viewport
|
|
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 +
|
|
117
|
+
# Full options: viewport + output scaling
|
|
120
118
|
sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
121
|
-
--viewport 1080p
|
|
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
|
|
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
|
|
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
|
|
50
|
-
sceneforge record -d demo.yaml -b http://localhost:3000 --viewport
|
|
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 +
|
|
117
|
+
# Full options: viewport + output scaling
|
|
120
118
|
sceneforge pipeline -d demo.yaml -b http://localhost:3000 \
|
|
121
|
-
--viewport 1080p
|
|
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
|
|
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
|
|
433
|
+
--viewport 1080p \
|
|
447
434
|
--output-size 1080p \
|
|
448
435
|
--quality high
|
|
449
436
|
```
|