@t3lnet/sceneforge 1.0.11 → 1.0.13
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/context/context-builder.ts +81 -2
- package/context/templates/base/cli-reference.md +37 -1
- package/context/templates/stages/stage4-rebalancing.md +22 -0
- package/dist/index.cjs +56 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +56 -4
- package/dist/index.js.map +1 -1
- package/dist/templates/templates/base/actions-reference.md +299 -0
- package/dist/templates/templates/base/cli-reference.md +272 -0
- package/dist/templates/templates/base/project-overview.md +58 -0
- package/dist/templates/templates/base/selectors-guide.md +233 -0
- package/dist/templates/templates/base/yaml-schema.md +210 -0
- package/dist/templates/templates/skills/balance-timing.md +136 -0
- package/dist/templates/templates/skills/debug-selector.md +193 -0
- package/dist/templates/templates/skills/generate-actions.md +94 -0
- package/dist/templates/templates/skills/optimize-demo.md +218 -0
- package/dist/templates/templates/skills/review-demo-yaml.md +164 -0
- package/dist/templates/templates/skills/write-step-script.md +136 -0
- package/dist/templates/templates/stages/stage1-actions.md +236 -0
- package/dist/templates/templates/stages/stage2-scripts.md +197 -0
- package/dist/templates/templates/stages/stage3-balancing.md +229 -0
- package/dist/templates/templates/stages/stage4-rebalancing.md +250 -0
- package/package.json +2 -2
|
@@ -27,6 +27,10 @@ import {
|
|
|
27
27
|
|
|
28
28
|
export type Stage = "actions" | "scripts" | "balance" | "rebalance" | "all";
|
|
29
29
|
|
|
30
|
+
// Markers for identifying SceneForge content in existing files
|
|
31
|
+
const SCENEFORGE_START_MARKER = "<!-- SCENEFORGE_CONTEXT_START -->";
|
|
32
|
+
const SCENEFORGE_END_MARKER = "<!-- SCENEFORGE_CONTEXT_END -->";
|
|
33
|
+
|
|
30
34
|
export interface ContextBuilderOptions {
|
|
31
35
|
target: TargetTool | "all";
|
|
32
36
|
stage: Stage;
|
|
@@ -50,6 +54,59 @@ export interface PreviewResult {
|
|
|
50
54
|
content: string;
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Wrap content with SceneForge markers for identification.
|
|
59
|
+
*/
|
|
60
|
+
function wrapWithMarkers(content: string): string {
|
|
61
|
+
return `${SCENEFORGE_START_MARKER}\n${content}\n${SCENEFORGE_END_MARKER}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Merge SceneForge content into an existing file.
|
|
66
|
+
* - If file doesn't exist, returns wrapped content
|
|
67
|
+
* - If file exists without markers, appends wrapped content
|
|
68
|
+
* - If file exists with markers, replaces the marked section
|
|
69
|
+
*/
|
|
70
|
+
async function mergeWithExisting(
|
|
71
|
+
filePath: string,
|
|
72
|
+
newContent: string
|
|
73
|
+
): Promise<{ content: string; merged: boolean }> {
|
|
74
|
+
const wrappedContent = wrapWithMarkers(newContent);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const existingContent = await fs.readFile(filePath, "utf-8");
|
|
78
|
+
|
|
79
|
+
// Check if file already has SceneForge markers
|
|
80
|
+
const startIndex = existingContent.indexOf(SCENEFORGE_START_MARKER);
|
|
81
|
+
const endIndex = existingContent.indexOf(SCENEFORGE_END_MARKER);
|
|
82
|
+
|
|
83
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
84
|
+
// Replace existing SceneForge section
|
|
85
|
+
const beforeSection = existingContent.slice(0, startIndex);
|
|
86
|
+
const afterSection = existingContent.slice(
|
|
87
|
+
endIndex + SCENEFORGE_END_MARKER.length
|
|
88
|
+
);
|
|
89
|
+
return {
|
|
90
|
+
content: beforeSection + wrappedContent + afterSection,
|
|
91
|
+
merged: true,
|
|
92
|
+
};
|
|
93
|
+
} else {
|
|
94
|
+
// Append SceneForge section to existing content
|
|
95
|
+
const separator = existingContent.trim().endsWith("-->") ? "\n\n" : "\n\n---\n\n";
|
|
96
|
+
return {
|
|
97
|
+
content: existingContent.trimEnd() + separator + wrappedContent + "\n",
|
|
98
|
+
merged: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// File doesn't exist, return wrapped content for new file
|
|
103
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
104
|
+
return { content: wrappedContent, merged: false };
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
53
110
|
/**
|
|
54
111
|
* Build context content for a specific tool and stage.
|
|
55
112
|
*/
|
|
@@ -124,14 +181,25 @@ export async function deployContext(
|
|
|
124
181
|
// Ensure directory exists
|
|
125
182
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
126
183
|
|
|
184
|
+
// Merge with existing content or create new
|
|
185
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
186
|
+
absolutePath,
|
|
187
|
+
content
|
|
188
|
+
);
|
|
189
|
+
|
|
127
190
|
// Write file
|
|
128
|
-
await fs.writeFile(absolutePath,
|
|
191
|
+
await fs.writeFile(absolutePath, finalContent, "utf-8");
|
|
129
192
|
|
|
130
193
|
results.push({
|
|
131
194
|
tool,
|
|
132
195
|
filePath: absolutePath,
|
|
133
196
|
created: true,
|
|
197
|
+
skipped: false,
|
|
134
198
|
});
|
|
199
|
+
|
|
200
|
+
if (merged) {
|
|
201
|
+
console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
|
|
202
|
+
}
|
|
135
203
|
} catch (error) {
|
|
136
204
|
results.push({
|
|
137
205
|
tool,
|
|
@@ -152,15 +220,26 @@ export async function deployContext(
|
|
|
152
220
|
// Ensure directory exists
|
|
153
221
|
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
|
154
222
|
|
|
223
|
+
// Merge with existing content or create new
|
|
224
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
225
|
+
absolutePath,
|
|
226
|
+
content
|
|
227
|
+
);
|
|
228
|
+
|
|
155
229
|
// Write file
|
|
156
|
-
await fs.writeFile(absolutePath,
|
|
230
|
+
await fs.writeFile(absolutePath, finalContent, "utf-8");
|
|
157
231
|
|
|
158
232
|
results.push({
|
|
159
233
|
tool,
|
|
160
234
|
filePath: absolutePath,
|
|
161
235
|
stage: stg,
|
|
162
236
|
created: true,
|
|
237
|
+
skipped: false,
|
|
163
238
|
});
|
|
239
|
+
|
|
240
|
+
if (merged) {
|
|
241
|
+
console.log(` [merged] ${path.relative(outputDir, absolutePath)}`);
|
|
242
|
+
}
|
|
164
243
|
} catch (error) {
|
|
165
244
|
results.push({
|
|
166
245
|
tool,
|
|
@@ -104,6 +104,8 @@ sceneforge split --demo <name> [options]
|
|
|
104
104
|
| `--demo` | Demo name (folder in output) |
|
|
105
105
|
| `--output`, `-o` | Output directory |
|
|
106
106
|
|
|
107
|
+
**Video Quality:** Encodes with H.264 (libx264), CRF 18, medium preset for high-quality output.
|
|
108
|
+
|
|
107
109
|
### voiceover
|
|
108
110
|
Generate voiceover audio from scripts.
|
|
109
111
|
|
|
@@ -136,6 +138,8 @@ sceneforge add-audio --demo <name> [options]
|
|
|
136
138
|
| `--demo` | Demo name |
|
|
137
139
|
| `--output`, `-o` | Output directory |
|
|
138
140
|
|
|
141
|
+
**Video Quality:** Re-encodes with H.264 (libx264), CRF 18, medium preset. Audio encoded as AAC at 192kbps.
|
|
142
|
+
|
|
139
143
|
### concat
|
|
140
144
|
Concatenate step clips into final video.
|
|
141
145
|
|
|
@@ -149,6 +153,8 @@ sceneforge concat --demo <name> [options]
|
|
|
149
153
|
| `--demo` | Demo name |
|
|
150
154
|
| `--output`, `-o` | Output directory |
|
|
151
155
|
|
|
156
|
+
**Video Quality:** Final encode with H.264 (libx264), CRF 18, medium preset. Includes `+faststart` for web streaming optimization. Audio encoded as AAC at 192kbps.
|
|
157
|
+
|
|
152
158
|
### doctor
|
|
153
159
|
Run environment diagnostics.
|
|
154
160
|
|
|
@@ -176,12 +182,18 @@ sceneforge context <subcommand> [options]
|
|
|
176
182
|
```
|
|
177
183
|
|
|
178
184
|
**Subcommands:**
|
|
179
|
-
- `deploy` - Deploy context files
|
|
185
|
+
- `deploy` - Deploy context files (additive - preserves existing content)
|
|
180
186
|
- `list` - List deployed context
|
|
181
187
|
- `remove` - Remove context files
|
|
182
188
|
- `preview` - Preview context content
|
|
183
189
|
- `skill` - Manage skills
|
|
184
190
|
|
|
191
|
+
**Additive Deployment:**
|
|
192
|
+
SceneForge uses markers to identify its content in instruction files:
|
|
193
|
+
- `<!-- SCENEFORGE_CONTEXT_START -->` and `<!-- SCENEFORGE_CONTEXT_END -->`
|
|
194
|
+
- Existing content outside these markers is preserved
|
|
195
|
+
- Re-running deploy updates only the SceneForge section
|
|
196
|
+
|
|
185
197
|
See dedicated context documentation for details.
|
|
186
198
|
|
|
187
199
|
## Output Structure
|
|
@@ -234,3 +246,27 @@ sceneforge doctor
|
|
|
234
246
|
# Record in headed mode with slow motion
|
|
235
247
|
sceneforge record -d demo.yaml -b http://localhost:3000 --headed --slowmo 500
|
|
236
248
|
```
|
|
249
|
+
|
|
250
|
+
## Video Quality
|
|
251
|
+
|
|
252
|
+
SceneForge uses high-quality FFmpeg encoding settings optimized for multi-pass video processing:
|
|
253
|
+
|
|
254
|
+
| Setting | Value | Purpose |
|
|
255
|
+
|---------|-------|---------|
|
|
256
|
+
| Codec | `libx264` | H.264 for broad compatibility |
|
|
257
|
+
| Preset | `medium` | Balanced quality/speed tradeoff |
|
|
258
|
+
| CRF | `18` | High quality (visually lossless) |
|
|
259
|
+
| Audio | `aac @ 192k` | High-quality audio |
|
|
260
|
+
| Flags | `+faststart` | Web streaming optimization |
|
|
261
|
+
|
|
262
|
+
**Why these settings:**
|
|
263
|
+
- Videos go through multiple processing stages (split → add-audio → concat)
|
|
264
|
+
- Each re-encoding can degrade quality (generation loss)
|
|
265
|
+
- CRF 18 preserves quality across all stages
|
|
266
|
+
- Medium preset provides good compression without sacrificing quality
|
|
267
|
+
|
|
268
|
+
**CRF Reference:**
|
|
269
|
+
- 0 = Lossless (huge files)
|
|
270
|
+
- 18 = Visually lossless (SceneForge default)
|
|
271
|
+
- 23 = FFmpeg default
|
|
272
|
+
- 28+ = Lower quality, smaller files
|
|
@@ -217,6 +217,26 @@ Aim for variance within ±500ms per step:
|
|
|
217
217
|
- [ ] Volume consistent across steps
|
|
218
218
|
- [ ] No unexpected pauses
|
|
219
219
|
|
|
220
|
+
### Video Quality Check
|
|
221
|
+
SceneForge uses high-quality encoding settings to preserve video fidelity:
|
|
222
|
+
- [ ] Video appears sharp (no compression artifacts)
|
|
223
|
+
- [ ] Colors are accurate (no banding)
|
|
224
|
+
- [ ] Text is readable (no blur from compression)
|
|
225
|
+
- [ ] No visible quality degradation between steps
|
|
226
|
+
|
|
227
|
+
**Encoding Settings Used:**
|
|
228
|
+
| Setting | Value | Purpose |
|
|
229
|
+
|---------|-------|---------|
|
|
230
|
+
| Codec | `libx264` | H.264 for compatibility |
|
|
231
|
+
| CRF | `18` | High quality (visually lossless) |
|
|
232
|
+
| Preset | `medium` | Balanced quality/speed |
|
|
233
|
+
| Audio | `aac @ 192k` | High-quality audio |
|
|
234
|
+
|
|
235
|
+
**If you notice quality issues:**
|
|
236
|
+
1. Ensure source recording is high quality (check `output/videos/<demo>.webm`)
|
|
237
|
+
2. Re-run the pipeline to regenerate all clips
|
|
238
|
+
3. Check available disk space (low space can affect encoding)
|
|
239
|
+
|
|
220
240
|
## Final Checklist
|
|
221
241
|
|
|
222
242
|
- [ ] All steps have acceptable timing variance
|
|
@@ -225,4 +245,6 @@ Aim for variance within ±500ms per step:
|
|
|
225
245
|
- [ ] No dead air or overlap issues
|
|
226
246
|
- [ ] Intro provides adequate context
|
|
227
247
|
- [ ] Outro concludes naturally
|
|
248
|
+
- [ ] Video quality is sharp and clear
|
|
249
|
+
- [ ] Audio is clear with consistent volume
|
|
228
250
|
- [ ] Overall demo flows professionally
|
package/dist/index.cjs
CHANGED
|
@@ -2671,6 +2671,42 @@ function isValidFormat(format) {
|
|
|
2671
2671
|
// context/context-builder.ts
|
|
2672
2672
|
var fs6 = __toESM(require("fs/promises"), 1);
|
|
2673
2673
|
var path6 = __toESM(require("path"), 1);
|
|
2674
|
+
var SCENEFORGE_START_MARKER = "<!-- SCENEFORGE_CONTEXT_START -->";
|
|
2675
|
+
var SCENEFORGE_END_MARKER = "<!-- SCENEFORGE_CONTEXT_END -->";
|
|
2676
|
+
function wrapWithMarkers(content) {
|
|
2677
|
+
return `${SCENEFORGE_START_MARKER}
|
|
2678
|
+
${content}
|
|
2679
|
+
${SCENEFORGE_END_MARKER}`;
|
|
2680
|
+
}
|
|
2681
|
+
async function mergeWithExisting(filePath, newContent) {
|
|
2682
|
+
const wrappedContent = wrapWithMarkers(newContent);
|
|
2683
|
+
try {
|
|
2684
|
+
const existingContent = await fs6.readFile(filePath, "utf-8");
|
|
2685
|
+
const startIndex = existingContent.indexOf(SCENEFORGE_START_MARKER);
|
|
2686
|
+
const endIndex = existingContent.indexOf(SCENEFORGE_END_MARKER);
|
|
2687
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
2688
|
+
const beforeSection = existingContent.slice(0, startIndex);
|
|
2689
|
+
const afterSection = existingContent.slice(
|
|
2690
|
+
endIndex + SCENEFORGE_END_MARKER.length
|
|
2691
|
+
);
|
|
2692
|
+
return {
|
|
2693
|
+
content: beforeSection + wrappedContent + afterSection,
|
|
2694
|
+
merged: true
|
|
2695
|
+
};
|
|
2696
|
+
} else {
|
|
2697
|
+
const separator = existingContent.trim().endsWith("-->") ? "\n\n" : "\n\n---\n\n";
|
|
2698
|
+
return {
|
|
2699
|
+
content: existingContent.trimEnd() + separator + wrappedContent + "\n",
|
|
2700
|
+
merged: true
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
} catch (error) {
|
|
2704
|
+
if (error.code === "ENOENT") {
|
|
2705
|
+
return { content: wrappedContent, merged: false };
|
|
2706
|
+
}
|
|
2707
|
+
throw error;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2674
2710
|
async function buildContext(tool, stage, variables) {
|
|
2675
2711
|
const templates = [];
|
|
2676
2712
|
const baseTemplates = await loadTemplatesByCategory("base");
|
|
@@ -2708,12 +2744,20 @@ async function deployContext(options) {
|
|
|
2708
2744
|
const filePath = getOutputPath(tool, format, outputDir);
|
|
2709
2745
|
const absolutePath = path6.resolve(filePath);
|
|
2710
2746
|
await fs6.mkdir(path6.dirname(absolutePath), { recursive: true });
|
|
2711
|
-
|
|
2747
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
2748
|
+
absolutePath,
|
|
2749
|
+
content
|
|
2750
|
+
);
|
|
2751
|
+
await fs6.writeFile(absolutePath, finalContent, "utf-8");
|
|
2712
2752
|
results.push({
|
|
2713
2753
|
tool,
|
|
2714
2754
|
filePath: absolutePath,
|
|
2715
|
-
created: true
|
|
2755
|
+
created: true,
|
|
2756
|
+
skipped: false
|
|
2716
2757
|
});
|
|
2758
|
+
if (merged) {
|
|
2759
|
+
console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
|
|
2760
|
+
}
|
|
2717
2761
|
} catch (error) {
|
|
2718
2762
|
results.push({
|
|
2719
2763
|
tool,
|
|
@@ -2730,13 +2774,21 @@ async function deployContext(options) {
|
|
|
2730
2774
|
const filePath = getOutputPath(tool, format, outputDir, stageName);
|
|
2731
2775
|
const absolutePath = path6.resolve(filePath);
|
|
2732
2776
|
await fs6.mkdir(path6.dirname(absolutePath), { recursive: true });
|
|
2733
|
-
|
|
2777
|
+
const { content: finalContent, merged } = await mergeWithExisting(
|
|
2778
|
+
absolutePath,
|
|
2779
|
+
content
|
|
2780
|
+
);
|
|
2781
|
+
await fs6.writeFile(absolutePath, finalContent, "utf-8");
|
|
2734
2782
|
results.push({
|
|
2735
2783
|
tool,
|
|
2736
2784
|
filePath: absolutePath,
|
|
2737
2785
|
stage: stg,
|
|
2738
|
-
created: true
|
|
2786
|
+
created: true,
|
|
2787
|
+
skipped: false
|
|
2739
2788
|
});
|
|
2789
|
+
if (merged) {
|
|
2790
|
+
console.log(` [merged] ${path6.relative(outputDir, absolutePath)}`);
|
|
2791
|
+
}
|
|
2740
2792
|
} catch (error) {
|
|
2741
2793
|
results.push({
|
|
2742
2794
|
tool,
|