@t3lnet/sceneforge 1.0.14 → 1.0.16
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 +144 -0
- package/context/context-builder.ts +56 -0
- package/context/templates/base/cli-reference.md +61 -17
- package/context/templates/stages/stage4-rebalancing.md +13 -11
- package/context/tests/context-builder.test.ts +15 -2
- package/dist/index.cjs +47 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +47 -0
- package/dist/index.js.map +1 -1
- package/dist/templates/base/cli-reference.md +61 -17
- package/dist/templates/stages/stage4-rebalancing.md +13 -11
- package/package.json +1 -1
|
@@ -107,6 +107,42 @@ async function mergeWithExisting(
|
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
async function writeSplitPointer(
|
|
111
|
+
tool: TargetTool,
|
|
112
|
+
stageNames: Stage[],
|
|
113
|
+
outputDir: string
|
|
114
|
+
): Promise<{ filePath: string; merged: boolean }> {
|
|
115
|
+
const config = getToolConfig(tool);
|
|
116
|
+
const stageLines = stageNames.map((stage) => {
|
|
117
|
+
const friendly = formatStageName(stage);
|
|
118
|
+
const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
|
|
119
|
+
const relativePath = path.posix.join(config.splitDir, stageFile);
|
|
120
|
+
return `- ${friendly}: ${relativePath}`;
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const pointerBody = [
|
|
124
|
+
"SceneForge context for this tool is split across the following stage files:",
|
|
125
|
+
"",
|
|
126
|
+
...stageLines,
|
|
127
|
+
"",
|
|
128
|
+
`Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`,
|
|
129
|
+
].join("\n");
|
|
130
|
+
|
|
131
|
+
const formattedPointer = formatForTool(tool, pointerBody, {
|
|
132
|
+
includeToolHeader: true,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const combinedPath = path.join(outputDir, config.combinedFile);
|
|
136
|
+
await fs.mkdir(path.dirname(combinedPath), { recursive: true });
|
|
137
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
138
|
+
combinedPath,
|
|
139
|
+
formattedPointer
|
|
140
|
+
);
|
|
141
|
+
await fs.writeFile(combinedPath, finalContent, "utf-8");
|
|
142
|
+
|
|
143
|
+
return { filePath: combinedPath, merged };
|
|
144
|
+
}
|
|
145
|
+
|
|
110
146
|
/**
|
|
111
147
|
* Build context content for a specific tool and stage.
|
|
112
148
|
*/
|
|
@@ -210,6 +246,7 @@ export async function deployContext(
|
|
|
210
246
|
}
|
|
211
247
|
} else {
|
|
212
248
|
// Generate split files for each stage
|
|
249
|
+
const deployedStages: Stage[] = [];
|
|
213
250
|
for (const stg of stages) {
|
|
214
251
|
try {
|
|
215
252
|
const content = await buildContext(tool, stg, variables);
|
|
@@ -240,6 +277,7 @@ export async function deployContext(
|
|
|
240
277
|
if (merged) {
|
|
241
278
|
console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
|
|
242
279
|
}
|
|
280
|
+
deployedStages.push(stg);
|
|
243
281
|
} catch (error) {
|
|
244
282
|
results.push({
|
|
245
283
|
tool,
|
|
@@ -250,6 +288,24 @@ export async function deployContext(
|
|
|
250
288
|
});
|
|
251
289
|
}
|
|
252
290
|
}
|
|
291
|
+
if (deployedStages.length > 0) {
|
|
292
|
+
const pointerResult = await writeSplitPointer(
|
|
293
|
+
tool,
|
|
294
|
+
deployedStages,
|
|
295
|
+
outputDir
|
|
296
|
+
);
|
|
297
|
+
results.push({
|
|
298
|
+
tool,
|
|
299
|
+
filePath: pointerResult.filePath,
|
|
300
|
+
created: true,
|
|
301
|
+
skipped: false,
|
|
302
|
+
});
|
|
303
|
+
if (pointerResult.merged) {
|
|
304
|
+
console.log(
|
|
305
|
+
` [merged] ${path.relative(outputDir, pointerResult.filePath)}`
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
253
309
|
}
|
|
254
310
|
}
|
|
255
311
|
|
|
@@ -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
|
|
|
@@ -118,7 +118,7 @@ describe("context-builder", () => {
|
|
|
118
118
|
outputDir: tempDir,
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
expect(results.length).toBe(
|
|
121
|
+
expect(results.length).toBe(2);
|
|
122
122
|
expect(results[0].created).toBe(true);
|
|
123
123
|
|
|
124
124
|
const splitDir = path.join(tempDir, ".claude/rules");
|
|
@@ -134,9 +134,22 @@ describe("context-builder", () => {
|
|
|
134
134
|
outputDir: tempDir,
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
expect(results.length).toBe(
|
|
137
|
+
expect(results.length).toBe(5);
|
|
138
138
|
expect(results.every((r) => r.created)).toBe(true);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
it("updates combined file when split files are deployed", async () => {
|
|
142
|
+
await deployContext({
|
|
143
|
+
target: "claude",
|
|
144
|
+
stage: "actions",
|
|
145
|
+
format: "split",
|
|
146
|
+
outputDir: tempDir,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const claudeFile = await fs.readFile(path.join(tempDir, "CLAUDE.md"), "utf-8");
|
|
150
|
+
expect(claudeFile).toContain("split across the following stage files");
|
|
151
|
+
expect(claudeFile).toContain(".claude/rules/stage1-actions.md");
|
|
152
|
+
});
|
|
140
153
|
});
|
|
141
154
|
|
|
142
155
|
describe("listDeployedContext", () => {
|
package/dist/index.cjs
CHANGED
|
@@ -2723,6 +2723,33 @@ async function mergeWithExisting(filePath, newContent) {
|
|
|
2723
2723
|
throw error;
|
|
2724
2724
|
}
|
|
2725
2725
|
}
|
|
2726
|
+
async function writeSplitPointer(tool, stageNames, outputDir) {
|
|
2727
|
+
const config = getToolConfig(tool);
|
|
2728
|
+
const stageLines = stageNames.map((stage) => {
|
|
2729
|
+
const friendly = formatStageName(stage);
|
|
2730
|
+
const stageFile = `${config.splitFilePrefix}${getStageFileName(stage)}${config.fileExtension}`;
|
|
2731
|
+
const relativePath = path6.posix.join(config.splitDir, stageFile);
|
|
2732
|
+
return `- ${friendly}: ${relativePath}`;
|
|
2733
|
+
});
|
|
2734
|
+
const pointerBody = [
|
|
2735
|
+
"SceneForge context for this tool is split across the following stage files:",
|
|
2736
|
+
"",
|
|
2737
|
+
...stageLines,
|
|
2738
|
+
"",
|
|
2739
|
+
`Open the stage file that matches the work you're doing, or run "npx sceneforge context preview --target ${tool} --stage <stage>" to inspect a specific stage.`
|
|
2740
|
+
].join("\n");
|
|
2741
|
+
const formattedPointer = formatForTool(tool, pointerBody, {
|
|
2742
|
+
includeToolHeader: true
|
|
2743
|
+
});
|
|
2744
|
+
const combinedPath = path6.join(outputDir, config.combinedFile);
|
|
2745
|
+
await fs6.mkdir(path6.dirname(combinedPath), { recursive: true });
|
|
2746
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
2747
|
+
combinedPath,
|
|
2748
|
+
formattedPointer
|
|
2749
|
+
);
|
|
2750
|
+
await fs6.writeFile(combinedPath, finalContent, "utf-8");
|
|
2751
|
+
return { filePath: combinedPath, merged };
|
|
2752
|
+
}
|
|
2726
2753
|
async function buildContext(tool, stage, variables) {
|
|
2727
2754
|
const templates = [];
|
|
2728
2755
|
const baseTemplates = await loadTemplatesByCategory("base");
|
|
@@ -2783,6 +2810,7 @@ async function deployContext(options) {
|
|
|
2783
2810
|
});
|
|
2784
2811
|
}
|
|
2785
2812
|
} else {
|
|
2813
|
+
const deployedStages = [];
|
|
2786
2814
|
for (const stg of stages) {
|
|
2787
2815
|
try {
|
|
2788
2816
|
const content = await buildContext(tool, stg, variables);
|
|
@@ -2805,6 +2833,7 @@ async function deployContext(options) {
|
|
|
2805
2833
|
if (merged) {
|
|
2806
2834
|
console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
|
|
2807
2835
|
}
|
|
2836
|
+
deployedStages.push(stg);
|
|
2808
2837
|
} catch (error) {
|
|
2809
2838
|
results.push({
|
|
2810
2839
|
tool,
|
|
@@ -2815,6 +2844,24 @@ async function deployContext(options) {
|
|
|
2815
2844
|
});
|
|
2816
2845
|
}
|
|
2817
2846
|
}
|
|
2847
|
+
if (deployedStages.length > 0) {
|
|
2848
|
+
const pointerResult = await writeSplitPointer(
|
|
2849
|
+
tool,
|
|
2850
|
+
deployedStages,
|
|
2851
|
+
outputDir
|
|
2852
|
+
);
|
|
2853
|
+
results.push({
|
|
2854
|
+
tool,
|
|
2855
|
+
filePath: pointerResult.filePath,
|
|
2856
|
+
created: true,
|
|
2857
|
+
skipped: false
|
|
2858
|
+
});
|
|
2859
|
+
if (pointerResult.merged) {
|
|
2860
|
+
console.log(
|
|
2861
|
+
` [merged] ${path6.relative(outputDir, pointerResult.filePath)}`
|
|
2862
|
+
);
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2818
2865
|
}
|
|
2819
2866
|
}
|
|
2820
2867
|
return results;
|