@t3lnet/sceneforge 1.0.15 → 1.0.17
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 +45 -0
- package/cli/commands/add-audio-to-steps.js +22 -38
- package/cli/commands/concat-final-videos.js +27 -25
- package/cli/commands/pipeline.js +30 -4
- package/cli/commands/split-video.js +18 -11
- package/cli/utils/quality.js +154 -0
- package/context/templates/base/cli-reference.md +61 -17
- package/context/templates/stages/stage4-rebalancing.md +13 -11
- package/dist/templates/base/cli-reference.md +61 -17
- package/dist/templates/stages/stage4-rebalancing.md +13 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,51 @@ npx sceneforge voice-cache prune --days 7 # Remove old entries
|
|
|
72
72
|
|
|
73
73
|
Cache is stored in `.voice-cache/` in your project root.
|
|
74
74
|
|
|
75
|
+
## Video Quality Settings
|
|
76
|
+
|
|
77
|
+
SceneForge provides configurable video quality settings for the video processing pipeline.
|
|
78
|
+
|
|
79
|
+
**Quality Presets:**
|
|
80
|
+
|
|
81
|
+
| Preset | CRF | Encoding | Use Case |
|
|
82
|
+
|--------|-----|----------|----------|
|
|
83
|
+
| `low` | 28 | fast | Quick drafts, smaller files |
|
|
84
|
+
| `medium` | 18 | medium | Default - balanced quality and size |
|
|
85
|
+
| `high` | 10 | slow | Final delivery, best quality |
|
|
86
|
+
|
|
87
|
+
**Supported Codecs:**
|
|
88
|
+
|
|
89
|
+
| Codec | Description |
|
|
90
|
+
|-------|-------------|
|
|
91
|
+
| `libx264` | H.264 - excellent compatibility (default) |
|
|
92
|
+
| `libx265` | H.265/HEVC - ~50% smaller files |
|
|
93
|
+
|
|
94
|
+
**CLI Flags:**
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
--quality <preset> # low, medium, high (default: medium)
|
|
98
|
+
--crf <value> # Override CRF (0-51, lower = better)
|
|
99
|
+
--codec <codec> # libx264 or libx265
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Examples:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# High quality for final output
|
|
106
|
+
npx sceneforge concat --demo my-demo --quality high
|
|
107
|
+
|
|
108
|
+
# Smaller files with H.265
|
|
109
|
+
npx sceneforge concat --demo my-demo --codec libx265
|
|
110
|
+
|
|
111
|
+
# Custom CRF value
|
|
112
|
+
npx sceneforge split --demo my-demo --crf 15
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Why it matters:**
|
|
116
|
+
- Videos pass through multiple stages (split → add-audio → concat)
|
|
117
|
+
- Higher quality (lower CRF) prevents generation loss
|
|
118
|
+
- `high` preset (CRF 10) produces near-lossless quality for professional demos
|
|
119
|
+
|
|
75
120
|
## Notes
|
|
76
121
|
|
|
77
122
|
- Voiceover generation uses ElevenLabs and requires `ELEVENLABS_API_KEY` + `ELEVENLABS_VOICE_ID`.
|
|
@@ -4,6 +4,12 @@ import { checkFFmpeg, getMediaDuration, runFFmpeg } from "../utils/media.js";
|
|
|
4
4
|
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
|
+
import {
|
|
8
|
+
getVideoEncodingArgs,
|
|
9
|
+
parseQualityArgs,
|
|
10
|
+
getQualityHelpText,
|
|
11
|
+
logQualitySettings,
|
|
12
|
+
} from "../utils/quality.js";
|
|
7
13
|
|
|
8
14
|
function printHelp() {
|
|
9
15
|
console.log(`
|
|
@@ -19,20 +25,23 @@ Options:
|
|
|
19
25
|
--root <path> Project root (defaults to cwd)
|
|
20
26
|
--output-dir <path> Output directory (defaults to e2e/output or output)
|
|
21
27
|
--help, -h Show this help message
|
|
28
|
+
${getQualityHelpText()}
|
|
22
29
|
|
|
23
30
|
Output:
|
|
24
31
|
Creates step_XX_<stepId>_with_audio.mp4 files in the videos/<demo>/ folder
|
|
25
32
|
|
|
26
33
|
Examples:
|
|
27
34
|
sceneforge add-audio --demo create-quote
|
|
28
|
-
sceneforge add-audio --
|
|
35
|
+
sceneforge add-audio --demo create-quote --quality high
|
|
36
|
+
sceneforge add-audio --all --codec libx265
|
|
29
37
|
`);
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVideoPath) {
|
|
40
|
+
async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVideoPath, qualityOptions = {}) {
|
|
33
41
|
const videoDuration = await getMediaDuration(videoPath);
|
|
34
42
|
const audioDuration = await getMediaDuration(audioPath);
|
|
35
43
|
const targetDuration = audioDuration + padding;
|
|
44
|
+
const encodingArgs = getVideoEncodingArgs(qualityOptions);
|
|
36
45
|
|
|
37
46
|
if (targetDuration <= videoDuration) {
|
|
38
47
|
const padDuration = Math.max(0, videoDuration - audioDuration);
|
|
@@ -50,16 +59,7 @@ async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVid
|
|
|
50
59
|
"[a]",
|
|
51
60
|
"-t",
|
|
52
61
|
String(videoDuration),
|
|
53
|
-
|
|
54
|
-
"libx264",
|
|
55
|
-
"-preset",
|
|
56
|
-
"medium",
|
|
57
|
-
"-crf",
|
|
58
|
-
"18",
|
|
59
|
-
"-c:a",
|
|
60
|
-
"aac",
|
|
61
|
-
"-b:a",
|
|
62
|
-
"192k",
|
|
62
|
+
...encodingArgs,
|
|
63
63
|
outputPath,
|
|
64
64
|
]);
|
|
65
65
|
return;
|
|
@@ -89,16 +89,7 @@ async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVid
|
|
|
89
89
|
"[outv]",
|
|
90
90
|
"-map",
|
|
91
91
|
"2:a",
|
|
92
|
-
|
|
93
|
-
"libx264",
|
|
94
|
-
"-preset",
|
|
95
|
-
"medium",
|
|
96
|
-
"-crf",
|
|
97
|
-
"18",
|
|
98
|
-
"-c:a",
|
|
99
|
-
"aac",
|
|
100
|
-
"-b:a",
|
|
101
|
-
"192k",
|
|
92
|
+
...encodingArgs,
|
|
102
93
|
"-t",
|
|
103
94
|
String(targetDuration),
|
|
104
95
|
outputPath,
|
|
@@ -118,24 +109,16 @@ async function addAudioToStep(videoPath, audioPath, outputPath, padding, nextVid
|
|
|
118
109
|
"[v]",
|
|
119
110
|
"-map",
|
|
120
111
|
"1:a",
|
|
121
|
-
|
|
122
|
-
"libx264",
|
|
123
|
-
"-preset",
|
|
124
|
-
"medium",
|
|
125
|
-
"-crf",
|
|
126
|
-
"18",
|
|
127
|
-
"-c:a",
|
|
128
|
-
"aac",
|
|
129
|
-
"-b:a",
|
|
130
|
-
"192k",
|
|
112
|
+
...encodingArgs,
|
|
131
113
|
"-t",
|
|
132
114
|
String(targetDuration),
|
|
133
115
|
outputPath,
|
|
134
116
|
]);
|
|
135
117
|
}
|
|
136
118
|
|
|
137
|
-
async function processDemo(demoName, paths, padding) {
|
|
119
|
+
async function processDemo(demoName, paths, padding, qualityOptions = {}) {
|
|
138
120
|
console.log(`\n[audio] Processing: ${demoName}\n`);
|
|
121
|
+
logQualitySettings(qualityOptions, "[audio]");
|
|
139
122
|
|
|
140
123
|
const stepsManifestPath = path.join(paths.videosDir, demoName, "steps-manifest.json");
|
|
141
124
|
let stepsManifest;
|
|
@@ -221,7 +204,7 @@ async function processDemo(demoName, paths, padding) {
|
|
|
221
204
|
);
|
|
222
205
|
|
|
223
206
|
try {
|
|
224
|
-
await addAudioToStep(step.videoFile, audioSegment.audioFile, outputPath, padding, nextVideoPath);
|
|
207
|
+
await addAudioToStep(step.videoFile, audioSegment.audioFile, outputPath, padding, nextVideoPath, qualityOptions);
|
|
225
208
|
outputFiles.push(outputPath);
|
|
226
209
|
} catch (error) {
|
|
227
210
|
console.error(`[audio] ${paddedIndex}. ${step.stepId}: ✗ Failed to process`);
|
|
@@ -254,7 +237,7 @@ async function processDemo(demoName, paths, padding) {
|
|
|
254
237
|
console.log(`[audio] Output: ${path.join(paths.videosDir, demoName)}`);
|
|
255
238
|
}
|
|
256
239
|
|
|
257
|
-
async function processAll(paths, padding) {
|
|
240
|
+
async function processAll(paths, padding, qualityOptions = {}) {
|
|
258
241
|
console.log("\n[audio] Processing all demos...\n");
|
|
259
242
|
|
|
260
243
|
try {
|
|
@@ -285,7 +268,7 @@ async function processAll(paths, padding) {
|
|
|
285
268
|
console.log(`[audio] Found ${demosToProcess.length} demo(s) to process\n`);
|
|
286
269
|
|
|
287
270
|
for (const demo of demosToProcess) {
|
|
288
|
-
await processDemo(demo, paths, padding);
|
|
271
|
+
await processDemo(demo, paths, padding, qualityOptions);
|
|
289
272
|
}
|
|
290
273
|
|
|
291
274
|
await fs.rm(paths.tempDir, { recursive: true, force: true });
|
|
@@ -318,15 +301,16 @@ export async function runAddAudioCommand(argv) {
|
|
|
318
301
|
|
|
319
302
|
const rootDir = resolveRoot(root);
|
|
320
303
|
const paths = await getOutputPaths(rootDir, outputDir);
|
|
304
|
+
const qualityOptions = parseQualityArgs(args, getFlagValue, hasFlag);
|
|
321
305
|
|
|
322
306
|
if (demo) {
|
|
323
|
-
await processDemo(demo, paths, padding);
|
|
307
|
+
await processDemo(demo, paths, padding, qualityOptions);
|
|
324
308
|
await fs.rm(paths.tempDir, { recursive: true, force: true });
|
|
325
309
|
return;
|
|
326
310
|
}
|
|
327
311
|
|
|
328
312
|
if (all) {
|
|
329
|
-
await processAll(paths, padding);
|
|
313
|
+
await processAll(paths, padding, qualityOptions);
|
|
330
314
|
return;
|
|
331
315
|
}
|
|
332
316
|
|
|
@@ -3,6 +3,14 @@ import * as path from "path";
|
|
|
3
3
|
import { checkFFmpeg, getMediaDuration, runFFmpeg } from "../utils/media.js";
|
|
4
4
|
import { getFlagValue, hasFlag } from "../utils/args.js";
|
|
5
5
|
import { getOutputPaths, resolveRoot, readJson, toAbsolute } from "../utils/paths.js";
|
|
6
|
+
import {
|
|
7
|
+
getVideoEncodingArgs,
|
|
8
|
+
parseQualityArgs,
|
|
9
|
+
getQualityHelpText,
|
|
10
|
+
logQualitySettings,
|
|
11
|
+
DEFAULT_AUDIO_CODEC,
|
|
12
|
+
DEFAULT_AUDIO_BITRATE,
|
|
13
|
+
} from "../utils/quality.js";
|
|
6
14
|
|
|
7
15
|
function printHelp() {
|
|
8
16
|
console.log(`
|
|
@@ -24,12 +32,14 @@ Options:
|
|
|
24
32
|
--root <path> Project root (defaults to cwd)
|
|
25
33
|
--output-dir <path> Output directory (defaults to e2e/output or output)
|
|
26
34
|
--help, -h Show this help message
|
|
35
|
+
${getQualityHelpText()}
|
|
27
36
|
|
|
28
37
|
Examples:
|
|
29
38
|
sceneforge concat --demo create-quote
|
|
39
|
+
sceneforge concat --demo create-quote --quality high
|
|
30
40
|
sceneforge concat --demo create-quote --intro intro.mp4 --outro outro.mp4
|
|
31
41
|
sceneforge concat --demo create-quote --music background.mp3 --music-volume 0.2
|
|
32
|
-
sceneforge concat --all
|
|
42
|
+
sceneforge concat --all --codec libx265
|
|
33
43
|
`);
|
|
34
44
|
}
|
|
35
45
|
|
|
@@ -55,7 +65,7 @@ async function loadMediaConfig(demoName, paths, rootDir) {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
async function buildConcatWithIntroOutro(stepFiles, demoDir, introPath, outroPath, outputPath) {
|
|
68
|
+
async function buildConcatWithIntroOutro(stepFiles, demoDir, introPath, outroPath, outputPath, qualityOptions = {}) {
|
|
59
69
|
const allInputs = [];
|
|
60
70
|
const inputPaths = [];
|
|
61
71
|
let inputIndex = 0;
|
|
@@ -83,6 +93,7 @@ async function buildConcatWithIntroOutro(stepFiles, demoDir, introPath, outroPat
|
|
|
83
93
|
|
|
84
94
|
const concatInputs = inputPaths.map(({ index }) => `[${index}:v:0][${index}:a:0]`).join("");
|
|
85
95
|
const filterGraph = `${concatInputs}concat=n=${inputPaths.length}:v=1:a=1[outv][outa]`;
|
|
96
|
+
const encodingArgs = getVideoEncodingArgs(qualityOptions);
|
|
86
97
|
|
|
87
98
|
await runFFmpeg([
|
|
88
99
|
"-y",
|
|
@@ -93,16 +104,7 @@ async function buildConcatWithIntroOutro(stepFiles, demoDir, introPath, outroPat
|
|
|
93
104
|
"[outv]",
|
|
94
105
|
"-map",
|
|
95
106
|
"[outa]",
|
|
96
|
-
|
|
97
|
-
"libx264",
|
|
98
|
-
"-preset",
|
|
99
|
-
"medium",
|
|
100
|
-
"-crf",
|
|
101
|
-
"18",
|
|
102
|
-
"-c:a",
|
|
103
|
-
"aac",
|
|
104
|
-
"-b:a",
|
|
105
|
-
"192k",
|
|
107
|
+
...encodingArgs,
|
|
106
108
|
"-movflags",
|
|
107
109
|
"+faststart",
|
|
108
110
|
outputPath,
|
|
@@ -194,9 +196,9 @@ async function addBackgroundMusic(videoPath, musicPath, outputPath, options = {}
|
|
|
194
196
|
"-c:v",
|
|
195
197
|
"copy",
|
|
196
198
|
"-c:a",
|
|
197
|
-
|
|
199
|
+
DEFAULT_AUDIO_CODEC,
|
|
198
200
|
"-b:a",
|
|
199
|
-
|
|
201
|
+
DEFAULT_AUDIO_BITRATE,
|
|
200
202
|
"-movflags",
|
|
201
203
|
"+faststart",
|
|
202
204
|
outputPath,
|
|
@@ -205,6 +207,9 @@ async function addBackgroundMusic(videoPath, musicPath, outputPath, options = {}
|
|
|
205
207
|
|
|
206
208
|
async function concatDemo(demoName, paths, options = {}) {
|
|
207
209
|
console.log(`\n[concat] Processing: ${demoName}\n`);
|
|
210
|
+
if (options.qualityOptions) {
|
|
211
|
+
logQualitySettings(options.qualityOptions, "[concat]");
|
|
212
|
+
}
|
|
208
213
|
|
|
209
214
|
const { rootDir, introOverride, outroOverride, musicOverride, musicOptions = {} } = options;
|
|
210
215
|
const demoDir = path.join(paths.videosDir, demoName);
|
|
@@ -291,19 +296,23 @@ async function concatDemo(demoName, paths, options = {}) {
|
|
|
291
296
|
|
|
292
297
|
console.log("[concat] Concatenating clips...");
|
|
293
298
|
|
|
299
|
+
const qualityOptions = options.qualityOptions || {};
|
|
300
|
+
|
|
294
301
|
if (hasIntroOutro) {
|
|
295
302
|
await buildConcatWithIntroOutro(
|
|
296
303
|
stepFiles,
|
|
297
304
|
demoDir,
|
|
298
305
|
introPath,
|
|
299
306
|
outroPath,
|
|
300
|
-
hasMusic ? tempConcatPath : outputPath
|
|
307
|
+
hasMusic ? tempConcatPath : outputPath,
|
|
308
|
+
qualityOptions
|
|
301
309
|
);
|
|
302
310
|
} else {
|
|
303
311
|
// Original concatenation logic for steps only
|
|
304
312
|
const inputArgs = stepFiles.flatMap((file) => ["-i", path.join(demoDir, file)]);
|
|
305
313
|
const concatInputs = stepFiles.map((_, index) => `[${index}:v:0][${index}:a:0]`).join("");
|
|
306
314
|
const filterGraph = `${concatInputs}concat=n=${stepFiles.length}:v=1:a=1[outv][outa]`;
|
|
315
|
+
const encodingArgs = getVideoEncodingArgs(qualityOptions);
|
|
307
316
|
|
|
308
317
|
await runFFmpeg([
|
|
309
318
|
"-y",
|
|
@@ -314,16 +323,7 @@ async function concatDemo(demoName, paths, options = {}) {
|
|
|
314
323
|
"[outv]",
|
|
315
324
|
"-map",
|
|
316
325
|
"[outa]",
|
|
317
|
-
|
|
318
|
-
"libx264",
|
|
319
|
-
"-preset",
|
|
320
|
-
"medium",
|
|
321
|
-
"-crf",
|
|
322
|
-
"18",
|
|
323
|
-
"-c:a",
|
|
324
|
-
"aac",
|
|
325
|
-
"-b:a",
|
|
326
|
-
"192k",
|
|
326
|
+
...encodingArgs,
|
|
327
327
|
"-movflags",
|
|
328
328
|
"+faststart",
|
|
329
329
|
hasMusic ? tempConcatPath : outputPath,
|
|
@@ -456,6 +456,7 @@ export async function runConcatCommand(argv) {
|
|
|
456
456
|
|
|
457
457
|
const rootDir = resolveRoot(root);
|
|
458
458
|
const paths = await getOutputPaths(rootDir, outputDir);
|
|
459
|
+
const qualityOptions = parseQualityArgs(args, getFlagValue, hasFlag);
|
|
459
460
|
|
|
460
461
|
const options = {
|
|
461
462
|
rootDir,
|
|
@@ -468,6 +469,7 @@ export async function runConcatCommand(argv) {
|
|
|
468
469
|
fadeIn: musicFadeIn,
|
|
469
470
|
fadeOut: musicFadeOut,
|
|
470
471
|
},
|
|
472
|
+
qualityOptions,
|
|
471
473
|
};
|
|
472
474
|
|
|
473
475
|
if (demo) {
|
package/cli/commands/pipeline.js
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import { loadDemoDefinition } from "@t3lnet/sceneforge";
|
|
4
4
|
import { getFlagValue, hasFlag } from "../utils/args.js";
|
|
5
5
|
import { ensureDir, getOutputPaths, resolveRoot, toAbsolute } from "../utils/paths.js";
|
|
6
|
+
import { getQualityHelpText } from "../utils/quality.js";
|
|
6
7
|
import { runRecordDemoCommand } from "./record-demo.js";
|
|
7
8
|
import { runSplitVideoCommand } from "./split-video.js";
|
|
8
9
|
import { runGenerateVoiceoverCommand } from "./generate-voiceover.js";
|
|
@@ -40,6 +41,7 @@ Media options (for final video):
|
|
|
40
41
|
--music-loop Loop music if shorter than video
|
|
41
42
|
--music-fade-in <s> Fade in duration for music (default: 1)
|
|
42
43
|
--music-fade-out <s> Fade out duration for music (default: 2)
|
|
44
|
+
${getQualityHelpText()}
|
|
43
45
|
|
|
44
46
|
--help, -h Show this help message
|
|
45
47
|
|
|
@@ -48,6 +50,7 @@ Examples:
|
|
|
48
50
|
sceneforge pipeline --demo create-quote --definitions-dir examples --base-url http://localhost:5173 --clean
|
|
49
51
|
sceneforge pipeline --demo create-quote --output-dir output --resume --progress
|
|
50
52
|
sceneforge pipeline --demo create-quote --intro assets/intro.mp4 --music assets/bg-music.mp3
|
|
53
|
+
sceneforge pipeline --demo create-quote --base-url http://localhost:5173 --quality high
|
|
51
54
|
`);
|
|
52
55
|
}
|
|
53
56
|
|
|
@@ -192,6 +195,11 @@ export async function runPipelineCommand(argv) {
|
|
|
192
195
|
const musicFadeIn = getFlagValue(args, "--music-fade-in");
|
|
193
196
|
const musicFadeOut = getFlagValue(args, "--music-fade-out");
|
|
194
197
|
|
|
198
|
+
// Video quality options
|
|
199
|
+
const quality = getFlagValue(args, "--quality");
|
|
200
|
+
const crf = getFlagValue(args, "--crf");
|
|
201
|
+
const codec = getFlagValue(args, "--codec");
|
|
202
|
+
|
|
195
203
|
const rootDir = resolveRoot(root);
|
|
196
204
|
const outputPaths = await getOutputPaths(rootDir, outputDir);
|
|
197
205
|
|
|
@@ -232,6 +240,12 @@ export async function runPipelineCommand(argv) {
|
|
|
232
240
|
if (outro) console.log(` - Outro: ${outro}`);
|
|
233
241
|
if (music) console.log(` - Music: ${music}`);
|
|
234
242
|
}
|
|
243
|
+
if (quality || crf || codec) {
|
|
244
|
+
console.log("\nVideo quality options:");
|
|
245
|
+
if (quality) console.log(` - Quality preset: ${quality}`);
|
|
246
|
+
if (crf) console.log(` - CRF: ${crf}`);
|
|
247
|
+
if (codec) console.log(` - Codec: ${codec}`);
|
|
248
|
+
}
|
|
235
249
|
return;
|
|
236
250
|
}
|
|
237
251
|
|
|
@@ -267,8 +281,20 @@ export async function runPipelineCommand(argv) {
|
|
|
267
281
|
sharedArgs.push("--output-dir", outputDir);
|
|
268
282
|
}
|
|
269
283
|
|
|
284
|
+
// Build quality args to pass through to video processing commands
|
|
285
|
+
const qualityArgs = [];
|
|
286
|
+
if (quality) {
|
|
287
|
+
qualityArgs.push("--quality", quality);
|
|
288
|
+
}
|
|
289
|
+
if (crf) {
|
|
290
|
+
qualityArgs.push("--crf", crf);
|
|
291
|
+
}
|
|
292
|
+
if (codec) {
|
|
293
|
+
qualityArgs.push("--codec", codec);
|
|
294
|
+
}
|
|
295
|
+
|
|
270
296
|
await runStep("split", plan.split, () =>
|
|
271
|
-
runSplitVideoCommand(["--demo", demoName, ...sharedArgs])
|
|
297
|
+
runSplitVideoCommand(["--demo", demoName, ...sharedArgs, ...qualityArgs])
|
|
272
298
|
);
|
|
273
299
|
|
|
274
300
|
const voiceArgs = ["--demo", demoName, ...sharedArgs];
|
|
@@ -280,14 +306,14 @@ export async function runPipelineCommand(argv) {
|
|
|
280
306
|
}
|
|
281
307
|
await runStep("voiceover", plan.voiceover, () => runGenerateVoiceoverCommand(voiceArgs));
|
|
282
308
|
|
|
283
|
-
const audioArgs = ["--demo", demoName, ...sharedArgs];
|
|
309
|
+
const audioArgs = ["--demo", demoName, ...sharedArgs, ...qualityArgs];
|
|
284
310
|
if (padding) {
|
|
285
311
|
audioArgs.push("--padding", padding);
|
|
286
312
|
}
|
|
287
313
|
await runStep("add-audio", plan.addAudio, () => runAddAudioCommand(audioArgs));
|
|
288
314
|
|
|
289
|
-
// Build concat args with media options
|
|
290
|
-
const concatArgs = ["--demo", demoName, ...sharedArgs];
|
|
315
|
+
// Build concat args with media options and quality settings
|
|
316
|
+
const concatArgs = ["--demo", demoName, ...sharedArgs, ...qualityArgs];
|
|
291
317
|
if (intro) {
|
|
292
318
|
concatArgs.push("--intro", intro);
|
|
293
319
|
}
|
|
@@ -4,6 +4,12 @@ import { checkFFmpeg, getMediaDuration, runFFmpeg } from "../utils/media.js";
|
|
|
4
4
|
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
|
+
import {
|
|
8
|
+
getVideoEncodingArgs,
|
|
9
|
+
parseQualityArgs,
|
|
10
|
+
getQualityHelpText,
|
|
11
|
+
logQualitySettings,
|
|
12
|
+
} from "../utils/quality.js";
|
|
7
13
|
|
|
8
14
|
function printHelp() {
|
|
9
15
|
console.log(`
|
|
@@ -18,9 +24,12 @@ Options:
|
|
|
18
24
|
--root <path> Project root (defaults to cwd)
|
|
19
25
|
--output-dir <path> Output directory (defaults to e2e/output or output)
|
|
20
26
|
--help, -h Show this help message
|
|
27
|
+
${getQualityHelpText()}
|
|
21
28
|
|
|
22
29
|
Examples:
|
|
23
30
|
sceneforge split --demo create-quote
|
|
31
|
+
sceneforge split --demo create-quote --quality high
|
|
32
|
+
sceneforge split --demo create-quote --crf 15 --codec libx265
|
|
24
33
|
sceneforge split --all
|
|
25
34
|
`);
|
|
26
35
|
}
|
|
@@ -54,8 +63,9 @@ async function findVideoFile(demoName, videosDir, testResultsDir) {
|
|
|
54
63
|
return null;
|
|
55
64
|
}
|
|
56
65
|
|
|
57
|
-
async function splitDemo(demoName, paths) {
|
|
66
|
+
async function splitDemo(demoName, paths, qualityOptions = {}) {
|
|
58
67
|
console.log(`\n[split] Processing: ${demoName}\n`);
|
|
68
|
+
logQualitySettings(qualityOptions, "[split]");
|
|
59
69
|
|
|
60
70
|
const scriptPath = path.join(paths.scriptsDir, `${demoName}.json`);
|
|
61
71
|
let script;
|
|
@@ -116,6 +126,7 @@ async function splitDemo(demoName, paths) {
|
|
|
116
126
|
);
|
|
117
127
|
|
|
118
128
|
try {
|
|
129
|
+
const encodingArgs = getVideoEncodingArgs({ ...qualityOptions, includeAudio: false });
|
|
119
130
|
await runFFmpeg([
|
|
120
131
|
"-y",
|
|
121
132
|
"-i",
|
|
@@ -124,12 +135,7 @@ async function splitDemo(demoName, paths) {
|
|
|
124
135
|
String(startSec),
|
|
125
136
|
"-t",
|
|
126
137
|
String(duration),
|
|
127
|
-
|
|
128
|
-
"libx264",
|
|
129
|
-
"-preset",
|
|
130
|
-
"medium",
|
|
131
|
-
"-crf",
|
|
132
|
-
"18",
|
|
138
|
+
...encodingArgs,
|
|
133
139
|
"-an",
|
|
134
140
|
outputPath,
|
|
135
141
|
]);
|
|
@@ -175,7 +181,7 @@ async function splitDemo(demoName, paths) {
|
|
|
175
181
|
console.log(`[split] Manifest: ${manifestPath}`);
|
|
176
182
|
}
|
|
177
183
|
|
|
178
|
-
async function splitAll(paths) {
|
|
184
|
+
async function splitAll(paths, qualityOptions = {}) {
|
|
179
185
|
console.log("\n[split] Processing all demos...\n");
|
|
180
186
|
|
|
181
187
|
try {
|
|
@@ -193,7 +199,7 @@ async function splitAll(paths) {
|
|
|
193
199
|
|
|
194
200
|
for (const file of scriptFiles) {
|
|
195
201
|
const demoName = path.basename(file, ".json");
|
|
196
|
-
await splitDemo(demoName, paths);
|
|
202
|
+
await splitDemo(demoName, paths, qualityOptions);
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
console.log("\n[split] All demos processed!");
|
|
@@ -223,14 +229,15 @@ export async function runSplitVideoCommand(argv) {
|
|
|
223
229
|
|
|
224
230
|
const rootDir = resolveRoot(root);
|
|
225
231
|
const paths = await getOutputPaths(rootDir, outputDir);
|
|
232
|
+
const qualityOptions = parseQualityArgs(args, getFlagValue, hasFlag);
|
|
226
233
|
|
|
227
234
|
if (demo) {
|
|
228
|
-
await splitDemo(demo, paths);
|
|
235
|
+
await splitDemo(demo, paths, qualityOptions);
|
|
229
236
|
return;
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
if (all) {
|
|
233
|
-
await splitAll(paths);
|
|
240
|
+
await splitAll(paths, qualityOptions);
|
|
234
241
|
return;
|
|
235
242
|
}
|
|
236
243
|
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video quality configuration for FFmpeg encoding
|
|
3
|
+
*
|
|
4
|
+
* Quality Presets:
|
|
5
|
+
* - low: Fast encoding, smaller files, suitable for drafts
|
|
6
|
+
* - medium: Balanced quality and file size (default)
|
|
7
|
+
* - high: Best quality, larger files, suitable for final delivery
|
|
8
|
+
*
|
|
9
|
+
* Supported Codecs:
|
|
10
|
+
* - libx264: H.264, excellent compatibility (default)
|
|
11
|
+
* - libx265: H.265/HEVC, ~50% smaller files, slower encoding
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const QUALITY_PRESETS = {
|
|
15
|
+
low: {
|
|
16
|
+
crf: 28,
|
|
17
|
+
preset: "fast",
|
|
18
|
+
description: "Fast encoding, smaller files, suitable for drafts",
|
|
19
|
+
},
|
|
20
|
+
medium: {
|
|
21
|
+
crf: 18,
|
|
22
|
+
preset: "medium",
|
|
23
|
+
description: "Balanced quality and file size (default)",
|
|
24
|
+
},
|
|
25
|
+
high: {
|
|
26
|
+
crf: 10,
|
|
27
|
+
preset: "slow",
|
|
28
|
+
description: "Best quality, larger files, suitable for final delivery",
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const SUPPORTED_CODECS = {
|
|
33
|
+
libx264: {
|
|
34
|
+
name: "H.264",
|
|
35
|
+
description: "Excellent compatibility, plays everywhere",
|
|
36
|
+
crfRange: "0-51 (lower = better)",
|
|
37
|
+
},
|
|
38
|
+
libx265: {
|
|
39
|
+
name: "H.265/HEVC",
|
|
40
|
+
description: "~50% smaller files, slower encoding, good compatibility",
|
|
41
|
+
crfRange: "0-51 (lower = better)",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const DEFAULT_CODEC = "libx264";
|
|
46
|
+
export const DEFAULT_QUALITY = "medium";
|
|
47
|
+
export const DEFAULT_AUDIO_CODEC = "aac";
|
|
48
|
+
export const DEFAULT_AUDIO_BITRATE = "192k";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get video encoding arguments for FFmpeg
|
|
52
|
+
* @param {Object} options
|
|
53
|
+
* @param {string} [options.quality] - Quality preset: low, medium, high
|
|
54
|
+
* @param {number} [options.crf] - Override CRF value (0-51, lower = better)
|
|
55
|
+
* @param {string} [options.codec] - Video codec: libx264, libx265
|
|
56
|
+
* @param {boolean} [options.includeAudio] - Include audio encoding args
|
|
57
|
+
* @returns {string[]} FFmpeg arguments for video encoding
|
|
58
|
+
*/
|
|
59
|
+
export function getVideoEncodingArgs(options = {}) {
|
|
60
|
+
const {
|
|
61
|
+
quality = DEFAULT_QUALITY,
|
|
62
|
+
crf: crfOverride,
|
|
63
|
+
codec = DEFAULT_CODEC,
|
|
64
|
+
includeAudio = true,
|
|
65
|
+
} = options;
|
|
66
|
+
|
|
67
|
+
const preset = QUALITY_PRESETS[quality] || QUALITY_PRESETS[DEFAULT_QUALITY];
|
|
68
|
+
// Only use crfOverride if it's a valid finite number
|
|
69
|
+
const crf = (crfOverride !== undefined && Number.isFinite(crfOverride)) ? crfOverride : preset.crf;
|
|
70
|
+
const encodingPreset = preset.preset;
|
|
71
|
+
|
|
72
|
+
const args = ["-c:v", codec, "-preset", encodingPreset, "-crf", String(crf)];
|
|
73
|
+
|
|
74
|
+
if (includeAudio) {
|
|
75
|
+
args.push("-c:a", DEFAULT_AUDIO_CODEC, "-b:a", DEFAULT_AUDIO_BITRATE);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return args;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse quality-related CLI arguments
|
|
83
|
+
* @param {string[]} args - CLI arguments
|
|
84
|
+
* @param {Function} getFlagValue - Function to get flag values
|
|
85
|
+
* @param {Function} hasFlag - Function to check flag presence
|
|
86
|
+
* @returns {Object} Parsed quality options
|
|
87
|
+
*/
|
|
88
|
+
export function parseQualityArgs(args, getFlagValue, hasFlag) {
|
|
89
|
+
const quality = getFlagValue(args, "--quality") || DEFAULT_QUALITY;
|
|
90
|
+
const crfValue = getFlagValue(args, "--crf");
|
|
91
|
+
const codec = getFlagValue(args, "--codec") || DEFAULT_CODEC;
|
|
92
|
+
|
|
93
|
+
// Validate quality preset
|
|
94
|
+
if (!QUALITY_PRESETS[quality]) {
|
|
95
|
+
console.warn(
|
|
96
|
+
`[warning] Unknown quality preset "${quality}", using "${DEFAULT_QUALITY}"`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate codec
|
|
101
|
+
if (!SUPPORTED_CODECS[codec]) {
|
|
102
|
+
console.warn(
|
|
103
|
+
`[warning] Unknown codec "${codec}", using "${DEFAULT_CODEC}"`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Parse CRF value, only set if it's a valid number
|
|
108
|
+
let crf = undefined;
|
|
109
|
+
if (crfValue !== undefined && crfValue !== null && crfValue !== "") {
|
|
110
|
+
const parsed = parseInt(crfValue, 10);
|
|
111
|
+
if (!Number.isNaN(parsed)) {
|
|
112
|
+
crf = parsed;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
quality: QUALITY_PRESETS[quality] ? quality : DEFAULT_QUALITY,
|
|
118
|
+
crf,
|
|
119
|
+
codec: SUPPORTED_CODECS[codec] ? codec : DEFAULT_CODEC,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get help text for quality options
|
|
125
|
+
* @returns {string} Help text for quality CLI options
|
|
126
|
+
*/
|
|
127
|
+
export function getQualityHelpText() {
|
|
128
|
+
return `
|
|
129
|
+
Video Quality Options:
|
|
130
|
+
--quality <preset> Quality preset: low, medium, high (default: medium)
|
|
131
|
+
- low: CRF 28, fast preset (smaller files, quick encoding)
|
|
132
|
+
- medium: CRF 18, medium preset (balanced)
|
|
133
|
+
- high: CRF 10, slow preset (best quality, larger files)
|
|
134
|
+
--crf <value> Override CRF value (0-51, lower = better quality)
|
|
135
|
+
--codec <codec> Video codec: libx264, libx265 (default: libx264)
|
|
136
|
+
- libx264: H.264, excellent compatibility
|
|
137
|
+
- libx265: H.265/HEVC, ~50% smaller files`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Log the quality settings being used
|
|
142
|
+
* @param {Object} options - Quality options
|
|
143
|
+
* @param {string} prefix - Log prefix (e.g., "[split]")
|
|
144
|
+
*/
|
|
145
|
+
export function logQualitySettings(options, prefix = "") {
|
|
146
|
+
const { quality, crf, codec } = options;
|
|
147
|
+
const preset = QUALITY_PRESETS[quality] || QUALITY_PRESETS[DEFAULT_QUALITY];
|
|
148
|
+
const effectiveCrf = crf !== undefined ? crf : preset.crf;
|
|
149
|
+
const codecInfo = SUPPORTED_CODECS[codec] || SUPPORTED_CODECS[DEFAULT_CODEC];
|
|
150
|
+
|
|
151
|
+
console.log(
|
|
152
|
+
`${prefix} Quality: ${quality} (CRF ${effectiveCrf}, ${preset.preset} preset, ${codecInfo.name})`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
@@ -103,8 +103,11 @@ sceneforge split --demo <name> [options]
|
|
|
103
103
|
|------|-------------|
|
|
104
104
|
| `--demo` | Demo name (folder in output) |
|
|
105
105
|
| `--output`, `-o` | Output directory |
|
|
106
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
107
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
108
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
106
109
|
|
|
107
|
-
**Video Quality:**
|
|
110
|
+
**Video Quality:** Configurable via `--quality` preset or `--crf`/`--codec` flags. Default: medium preset (CRF 18, libx264).
|
|
108
111
|
|
|
109
112
|
### voiceover
|
|
110
113
|
Generate voiceover audio from scripts.
|
|
@@ -137,8 +140,11 @@ sceneforge add-audio --demo <name> [options]
|
|
|
137
140
|
|------|-------------|
|
|
138
141
|
| `--demo` | Demo name |
|
|
139
142
|
| `--output`, `-o` | Output directory |
|
|
143
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
144
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
145
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
140
146
|
|
|
141
|
-
**Video Quality:**
|
|
147
|
+
**Video Quality:** Configurable via `--quality` preset. Default: medium (CRF 18, libx264). Audio: AAC 192kbps.
|
|
142
148
|
|
|
143
149
|
### concat
|
|
144
150
|
Concatenate step clips into final video.
|
|
@@ -152,8 +158,15 @@ sceneforge concat --demo <name> [options]
|
|
|
152
158
|
|------|-------------|
|
|
153
159
|
| `--demo` | Demo name |
|
|
154
160
|
| `--output`, `-o` | Output directory |
|
|
161
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
162
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
163
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
164
|
+
| `--intro` | Intro video to prepend |
|
|
165
|
+
| `--outro` | Outro video to append |
|
|
166
|
+
| `--music` | Background music file |
|
|
167
|
+
| `--music-volume` | Music volume 0-1 (default: 0.15) |
|
|
155
168
|
|
|
156
|
-
**Video Quality:**
|
|
169
|
+
**Video Quality:** Configurable via `--quality` preset. Default: medium (CRF 18, libx264). Includes `+faststart` for web streaming. Audio: AAC 192kbps.
|
|
157
170
|
|
|
158
171
|
### doctor
|
|
159
172
|
Run environment diagnostics.
|
|
@@ -249,24 +262,55 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --headed --slowmo 500
|
|
|
249
262
|
|
|
250
263
|
## Video Quality
|
|
251
264
|
|
|
252
|
-
SceneForge
|
|
265
|
+
SceneForge provides configurable video quality settings via CLI flags on `split`, `add-audio`, and `concat` commands.
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
|
257
|
-
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
267
|
+
### Quality Presets
|
|
268
|
+
|
|
269
|
+
| Preset | CRF | Encoding | Use Case |
|
|
270
|
+
|--------|-----|----------|----------|
|
|
271
|
+
| `low` | 28 | fast | Quick drafts, smaller files |
|
|
272
|
+
| `medium` | 18 | medium | Default - balanced quality and size |
|
|
273
|
+
| `high` | 10 | slow | Final delivery, best quality |
|
|
274
|
+
|
|
275
|
+
### Supported Codecs
|
|
276
|
+
|
|
277
|
+
| Codec | Name | Description |
|
|
278
|
+
|-------|------|-------------|
|
|
279
|
+
| `libx264` | H.264 | Excellent compatibility (default) |
|
|
280
|
+
| `libx265` | H.265/HEVC | ~50% smaller files, slower encoding |
|
|
281
|
+
|
|
282
|
+
### CLI Flags
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
--quality <preset> # low, medium, high (default: medium)
|
|
286
|
+
--crf <value> # Override CRF (0-51, lower = better)
|
|
287
|
+
--codec <codec> # libx264 or libx265 (default: libx264)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Examples
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# High quality for final output
|
|
294
|
+
sceneforge concat --demo my-demo --quality high
|
|
295
|
+
|
|
296
|
+
# Smaller files with H.265 codec
|
|
297
|
+
sceneforge concat --demo my-demo --codec libx265
|
|
298
|
+
|
|
299
|
+
# Custom CRF for fine control
|
|
300
|
+
sceneforge split --demo my-demo --crf 15
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Why Quality Matters
|
|
261
304
|
|
|
262
|
-
**Why these settings:**
|
|
263
305
|
- Videos go through multiple processing stages (split → add-audio → concat)
|
|
264
306
|
- Each re-encoding can degrade quality (generation loss)
|
|
265
|
-
- CRF
|
|
266
|
-
-
|
|
307
|
+
- Higher quality settings (lower CRF) preserve fidelity across stages
|
|
308
|
+
- The `high` preset (CRF 10) produces near-lossless quality
|
|
309
|
+
|
|
310
|
+
### CRF Reference
|
|
267
311
|
|
|
268
|
-
**CRF Reference:**
|
|
269
312
|
- 0 = Lossless (huge files)
|
|
270
|
-
-
|
|
313
|
+
- 10 = Near-lossless (high preset)
|
|
314
|
+
- 18 = Visually lossless (medium preset, default)
|
|
271
315
|
- 23 = FFmpeg default
|
|
272
|
-
- 28
|
|
316
|
+
- 28 = Lower quality (low preset)
|
|
@@ -218,24 +218,26 @@ Aim for variance within ±500ms per step:
|
|
|
218
218
|
- [ ] No unexpected pauses
|
|
219
219
|
|
|
220
220
|
### Video Quality Check
|
|
221
|
-
SceneForge
|
|
221
|
+
SceneForge provides configurable quality settings to preserve video fidelity:
|
|
222
222
|
- [ ] Video appears sharp (no compression artifacts)
|
|
223
223
|
- [ ] Colors are accurate (no banding)
|
|
224
224
|
- [ ] Text is readable (no blur from compression)
|
|
225
225
|
- [ ] No visible quality degradation between steps
|
|
226
226
|
|
|
227
|
-
**
|
|
228
|
-
|
|
|
229
|
-
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
227
|
+
**Quality Presets:**
|
|
228
|
+
| Preset | CRF | Encoding | Use Case |
|
|
229
|
+
|--------|-----|----------|----------|
|
|
230
|
+
| `low` | 28 | fast | Quick drafts |
|
|
231
|
+
| `medium` | 18 | medium | Default - balanced |
|
|
232
|
+
| `high` | 10 | slow | Final delivery |
|
|
233
|
+
|
|
234
|
+
**CLI Flags:** `--quality`, `--crf`, `--codec` (available on split, add-audio, concat)
|
|
234
235
|
|
|
235
236
|
**If you notice quality issues:**
|
|
236
|
-
1.
|
|
237
|
-
2.
|
|
238
|
-
3.
|
|
237
|
+
1. Re-run with `--quality high` for better fidelity
|
|
238
|
+
2. Ensure source recording is high quality (check `output/videos/<demo>.webm`)
|
|
239
|
+
3. Try `--codec libx265` for better compression at same quality
|
|
240
|
+
4. Check available disk space (low space can affect encoding)
|
|
239
241
|
|
|
240
242
|
## Final Checklist
|
|
241
243
|
|
|
@@ -103,8 +103,11 @@ sceneforge split --demo <name> [options]
|
|
|
103
103
|
|------|-------------|
|
|
104
104
|
| `--demo` | Demo name (folder in output) |
|
|
105
105
|
| `--output`, `-o` | Output directory |
|
|
106
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
107
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
108
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
106
109
|
|
|
107
|
-
**Video Quality:**
|
|
110
|
+
**Video Quality:** Configurable via `--quality` preset or `--crf`/`--codec` flags. Default: medium preset (CRF 18, libx264).
|
|
108
111
|
|
|
109
112
|
### voiceover
|
|
110
113
|
Generate voiceover audio from scripts.
|
|
@@ -137,8 +140,11 @@ sceneforge add-audio --demo <name> [options]
|
|
|
137
140
|
|------|-------------|
|
|
138
141
|
| `--demo` | Demo name |
|
|
139
142
|
| `--output`, `-o` | Output directory |
|
|
143
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
144
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
145
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
140
146
|
|
|
141
|
-
**Video Quality:**
|
|
147
|
+
**Video Quality:** Configurable via `--quality` preset. Default: medium (CRF 18, libx264). Audio: AAC 192kbps.
|
|
142
148
|
|
|
143
149
|
### concat
|
|
144
150
|
Concatenate step clips into final video.
|
|
@@ -152,8 +158,15 @@ sceneforge concat --demo <name> [options]
|
|
|
152
158
|
|------|-------------|
|
|
153
159
|
| `--demo` | Demo name |
|
|
154
160
|
| `--output`, `-o` | Output directory |
|
|
161
|
+
| `--quality` | Quality preset: low, medium, high (default: medium) |
|
|
162
|
+
| `--crf` | Override CRF value (0-51, lower = better) |
|
|
163
|
+
| `--codec` | Video codec: libx264, libx265 (default: libx264) |
|
|
164
|
+
| `--intro` | Intro video to prepend |
|
|
165
|
+
| `--outro` | Outro video to append |
|
|
166
|
+
| `--music` | Background music file |
|
|
167
|
+
| `--music-volume` | Music volume 0-1 (default: 0.15) |
|
|
155
168
|
|
|
156
|
-
**Video Quality:**
|
|
169
|
+
**Video Quality:** Configurable via `--quality` preset. Default: medium (CRF 18, libx264). Includes `+faststart` for web streaming. Audio: AAC 192kbps.
|
|
157
170
|
|
|
158
171
|
### doctor
|
|
159
172
|
Run environment diagnostics.
|
|
@@ -249,24 +262,55 @@ sceneforge record -d demo.yaml -b http://localhost:3000 --headed --slowmo 500
|
|
|
249
262
|
|
|
250
263
|
## Video Quality
|
|
251
264
|
|
|
252
|
-
SceneForge
|
|
265
|
+
SceneForge provides configurable video quality settings via CLI flags on `split`, `add-audio`, and `concat` commands.
|
|
253
266
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
|
257
|
-
|
|
258
|
-
|
|
|
259
|
-
|
|
|
260
|
-
|
|
|
267
|
+
### Quality Presets
|
|
268
|
+
|
|
269
|
+
| Preset | CRF | Encoding | Use Case |
|
|
270
|
+
|--------|-----|----------|----------|
|
|
271
|
+
| `low` | 28 | fast | Quick drafts, smaller files |
|
|
272
|
+
| `medium` | 18 | medium | Default - balanced quality and size |
|
|
273
|
+
| `high` | 10 | slow | Final delivery, best quality |
|
|
274
|
+
|
|
275
|
+
### Supported Codecs
|
|
276
|
+
|
|
277
|
+
| Codec | Name | Description |
|
|
278
|
+
|-------|------|-------------|
|
|
279
|
+
| `libx264` | H.264 | Excellent compatibility (default) |
|
|
280
|
+
| `libx265` | H.265/HEVC | ~50% smaller files, slower encoding |
|
|
281
|
+
|
|
282
|
+
### CLI Flags
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
--quality <preset> # low, medium, high (default: medium)
|
|
286
|
+
--crf <value> # Override CRF (0-51, lower = better)
|
|
287
|
+
--codec <codec> # libx264 or libx265 (default: libx264)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Examples
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# High quality for final output
|
|
294
|
+
sceneforge concat --demo my-demo --quality high
|
|
295
|
+
|
|
296
|
+
# Smaller files with H.265 codec
|
|
297
|
+
sceneforge concat --demo my-demo --codec libx265
|
|
298
|
+
|
|
299
|
+
# Custom CRF for fine control
|
|
300
|
+
sceneforge split --demo my-demo --crf 15
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Why Quality Matters
|
|
261
304
|
|
|
262
|
-
**Why these settings:**
|
|
263
305
|
- Videos go through multiple processing stages (split → add-audio → concat)
|
|
264
306
|
- Each re-encoding can degrade quality (generation loss)
|
|
265
|
-
- CRF
|
|
266
|
-
-
|
|
307
|
+
- Higher quality settings (lower CRF) preserve fidelity across stages
|
|
308
|
+
- The `high` preset (CRF 10) produces near-lossless quality
|
|
309
|
+
|
|
310
|
+
### CRF Reference
|
|
267
311
|
|
|
268
|
-
**CRF Reference:**
|
|
269
312
|
- 0 = Lossless (huge files)
|
|
270
|
-
-
|
|
313
|
+
- 10 = Near-lossless (high preset)
|
|
314
|
+
- 18 = Visually lossless (medium preset, default)
|
|
271
315
|
- 23 = FFmpeg default
|
|
272
|
-
- 28
|
|
316
|
+
- 28 = Lower quality (low preset)
|
|
@@ -218,24 +218,26 @@ Aim for variance within ±500ms per step:
|
|
|
218
218
|
- [ ] No unexpected pauses
|
|
219
219
|
|
|
220
220
|
### Video Quality Check
|
|
221
|
-
SceneForge
|
|
221
|
+
SceneForge provides configurable quality settings to preserve video fidelity:
|
|
222
222
|
- [ ] Video appears sharp (no compression artifacts)
|
|
223
223
|
- [ ] Colors are accurate (no banding)
|
|
224
224
|
- [ ] Text is readable (no blur from compression)
|
|
225
225
|
- [ ] No visible quality degradation between steps
|
|
226
226
|
|
|
227
|
-
**
|
|
228
|
-
|
|
|
229
|
-
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
227
|
+
**Quality Presets:**
|
|
228
|
+
| Preset | CRF | Encoding | Use Case |
|
|
229
|
+
|--------|-----|----------|----------|
|
|
230
|
+
| `low` | 28 | fast | Quick drafts |
|
|
231
|
+
| `medium` | 18 | medium | Default - balanced |
|
|
232
|
+
| `high` | 10 | slow | Final delivery |
|
|
233
|
+
|
|
234
|
+
**CLI Flags:** `--quality`, `--crf`, `--codec` (available on split, add-audio, concat)
|
|
234
235
|
|
|
235
236
|
**If you notice quality issues:**
|
|
236
|
-
1.
|
|
237
|
-
2.
|
|
238
|
-
3.
|
|
237
|
+
1. Re-run with `--quality high` for better fidelity
|
|
238
|
+
2. Ensure source recording is high quality (check `output/videos/<demo>.webm`)
|
|
239
|
+
3. Try `--codec libx265` for better compression at same quality
|
|
240
|
+
4. Check available disk space (low space can affect encoding)
|
|
239
241
|
|
|
240
242
|
## Final Checklist
|
|
241
243
|
|