@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.
@@ -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, content, "utf-8");
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, content, "utf-8");
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
- await fs6.writeFile(absolutePath, content, "utf-8");
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
- await fs6.writeFile(absolutePath, content, "utf-8");
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,