storyforge 0.7.4 → 0.8.0

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.
@@ -135,11 +135,11 @@ var require_p_limit = __commonJS({
135
135
  // src/bridge-poller.ts
136
136
  import { spawn as spawn2, spawnSync } from "child_process";
137
137
  import * as crypto from "crypto";
138
- import * as fs10 from "fs";
138
+ import * as fs11 from "fs";
139
139
 
140
140
  // ../pipeline/src/clip-render/render-chunk.ts
141
- import * as fs9 from "fs";
142
- import * as path10 from "path";
141
+ import * as fs10 from "fs";
142
+ import * as path11 from "path";
143
143
 
144
144
  // ../../node_modules/zod/v3/external.js
145
145
  var external_exports = {};
@@ -619,8 +619,8 @@ function getErrorMap() {
619
619
 
620
620
  // ../../node_modules/zod/v3/helpers/parseUtil.js
621
621
  var makeIssue = (params) => {
622
- const { data, path: path11, errorMaps, issueData } = params;
623
- const fullPath = [...path11, ...issueData.path || []];
622
+ const { data, path: path12, errorMaps, issueData } = params;
623
+ const fullPath = [...path12, ...issueData.path || []];
624
624
  const fullIssue = {
625
625
  ...issueData,
626
626
  path: fullPath
@@ -736,11 +736,11 @@ var errorUtil;
736
736
 
737
737
  // ../../node_modules/zod/v3/types.js
738
738
  var ParseInputLazyPath = class {
739
- constructor(parent, value, path11, key) {
739
+ constructor(parent, value, path12, key) {
740
740
  this._cachedPath = [];
741
741
  this.parent = parent;
742
742
  this.data = value;
743
- this._path = path11;
743
+ this._path = path12;
744
744
  this._key = key;
745
745
  }
746
746
  get path() {
@@ -5345,7 +5345,8 @@ var ENGINE_FALLBACK_CHAIN = {
5345
5345
  hyperframes: "remotion",
5346
5346
  "stock+remotion": "remotion",
5347
5347
  manim: "remotion",
5348
- remotion: null
5348
+ remotion: null,
5349
+ "documentary-v2": "gemini+remotion"
5349
5350
  };
5350
5351
  function nextFallback(engine) {
5351
5352
  return ENGINE_FALLBACK_CHAIN[engine] ?? null;
@@ -6082,6 +6083,81 @@ var renderStockRemotion = async (shot, ctx) => {
6082
6083
  }
6083
6084
  };
6084
6085
 
6086
+ // ../pipeline/src/clip-render/engines/documentary-v2.ts
6087
+ import * as fs8 from "fs";
6088
+ import * as path9 from "path";
6089
+ var renderDocumentaryV2 = async (shot, ctx) => {
6090
+ if (ctx.signal?.aborted) throw new Error("aborted");
6091
+ if (!shot.prompt) {
6092
+ throw new Error(
6093
+ `documentary-v2: shot.prompt must be the path to a CompositionV2Data JSON file (shot ${shot.id})`
6094
+ );
6095
+ }
6096
+ ctx.progress.emit({ phase: "preparing", shotId: shot.id });
6097
+ let data;
6098
+ if (shot.prompt.trimStart().startsWith("{")) {
6099
+ data = JSON.parse(shot.prompt);
6100
+ } else {
6101
+ if (!fs8.existsSync(shot.prompt)) {
6102
+ throw new Error(`documentary-v2: CompositionV2Data file not found: ${shot.prompt}`);
6103
+ }
6104
+ data = JSON.parse(fs8.readFileSync(shot.prompt, "utf8"));
6105
+ }
6106
+ const durationFrames = data.totalDurationFrames > 0 ? data.totalDurationFrames : Math.max(1, Math.round(shot.durationSec * ctx.fps));
6107
+ const remotion = requireRemotionRoot();
6108
+ const cacheRel = path9.posix.join("clip-render-cache", ctx.chunkId);
6109
+ const cacheAbs = path9.join(remotion.cwd, "public", cacheRel);
6110
+ fs8.mkdirSync(cacheAbs, { recursive: true });
6111
+ for (const scene of data.scenes) {
6112
+ for (const layer of scene.layers) {
6113
+ if (!layer.assetPath) continue;
6114
+ if (path9.isAbsolute(layer.assetPath) && fs8.existsSync(layer.assetPath)) {
6115
+ const filename = path9.basename(layer.assetPath);
6116
+ const destAbs = path9.join(cacheAbs, filename);
6117
+ if (!fs8.existsSync(destAbs)) {
6118
+ fs8.copyFileSync(layer.assetPath, destAbs);
6119
+ }
6120
+ layer.assetPath = path9.posix.join(cacheRel, filename);
6121
+ }
6122
+ }
6123
+ }
6124
+ const propsPath = path9.join(ctx.tmpDir, "remotion", `${shot.id}_dv2_props.json`);
6125
+ fs8.mkdirSync(path9.dirname(propsPath), { recursive: true });
6126
+ fs8.writeFileSync(propsPath, JSON.stringify({ compositionData: data }, null, 2));
6127
+ ctx.progress.emit({
6128
+ phase: "rendering",
6129
+ shotId: shot.id,
6130
+ framesRendered: 0,
6131
+ totalFrames: durationFrames
6132
+ });
6133
+ const r = await runCmd(
6134
+ "npx",
6135
+ [
6136
+ "remotion",
6137
+ "render",
6138
+ remotion.entry,
6139
+ "DocumentaryV2",
6140
+ "--props",
6141
+ propsPath,
6142
+ "--output",
6143
+ ctx.sceneOutPath,
6144
+ "--concurrency",
6145
+ "4"
6146
+ ],
6147
+ { timeoutMs: 6e5, signal: ctx.signal, cwd: remotion.cwd }
6148
+ );
6149
+ if (r.code !== 0) {
6150
+ const tail = r.stderr.split("\n").slice(-8).join(" ").slice(0, 400);
6151
+ throw new Error(`documentary-v2 render failed (code ${r.code}): ${tail}`);
6152
+ }
6153
+ ctx.progress.emit({
6154
+ phase: "rendering",
6155
+ shotId: shot.id,
6156
+ framesRendered: durationFrames,
6157
+ totalFrames: durationFrames
6158
+ });
6159
+ };
6160
+
6085
6161
  // ../pipeline/src/clip-render/engines/index.ts
6086
6162
  var ENGINE_ADAPTERS = {
6087
6163
  "gemini+remotion": renderGeminiRemotion,
@@ -6091,7 +6167,8 @@ var ENGINE_ADAPTERS = {
6091
6167
  "remotion": renderRemotion,
6092
6168
  "stock+remotion": renderStockRemotion,
6093
6169
  "hyperframes": renderHyperframes,
6094
- "remotion+htmlcanvas": renderRemotionHtmlCanvas
6170
+ "remotion+htmlcanvas": renderRemotionHtmlCanvas,
6171
+ "documentary-v2": renderDocumentaryV2
6095
6172
  };
6096
6173
  var MANIM_ENGINES = /* @__PURE__ */ new Set([
6097
6174
  "manim",
@@ -6103,11 +6180,11 @@ function isManimEngine(engine) {
6103
6180
  }
6104
6181
 
6105
6182
  // ../pipeline/src/clip-render/fs-layout.ts
6106
- import * as fs8 from "fs";
6107
- import * as path9 from "path";
6183
+ import * as fs9 from "fs";
6184
+ import * as path10 from "path";
6108
6185
  function resolveProjectRoot(projectSlug, callerOutputDir) {
6109
6186
  if (callerOutputDir && callerOutputDir.length > 0) {
6110
- if (!path9.isAbsolute(callerOutputDir)) {
6187
+ if (!path10.isAbsolute(callerOutputDir)) {
6111
6188
  throw new Error(
6112
6189
  `clip-render: spec.outputDir must be absolute, got "${callerOutputDir}"`
6113
6190
  );
@@ -6117,41 +6194,41 @@ function resolveProjectRoot(projectSlug, callerOutputDir) {
6117
6194
  if (!projectSlug || /[/\\]/.test(projectSlug)) {
6118
6195
  throw new Error(`clip-render: invalid projectSlug "${projectSlug}"`);
6119
6196
  }
6120
- return path9.resolve(process.cwd(), "forge-renders", projectSlug);
6197
+ return path10.resolve(process.cwd(), "forge-renders", projectSlug);
6121
6198
  }
6122
6199
  function chunkPaths(projectSlug, chunkId, callerOutputDir) {
6123
6200
  if (!chunkId || /[/\\]/.test(chunkId)) {
6124
6201
  throw new Error(`clip-render: invalid chunkId "${chunkId}"`);
6125
6202
  }
6126
6203
  const root = resolveProjectRoot(projectSlug, callerOutputDir);
6127
- const tmpDir = path9.join(root, ".tmp", chunkId);
6204
+ const tmpDir = path10.join(root, ".tmp", chunkId);
6128
6205
  return {
6129
6206
  root,
6130
- finalClipPath: path9.join(root, "clips", `${chunkId}.mp4`),
6207
+ finalClipPath: path10.join(root, "clips", `${chunkId}.mp4`),
6131
6208
  tmpDir,
6132
- scenesDir: path9.join(tmpDir, "scenes"),
6133
- manimDir: path9.join(tmpDir, "manim"),
6134
- remotionDir: path9.join(tmpDir, "remotion"),
6135
- hyperframesDir: path9.join(tmpDir, "hyperframes"),
6136
- progressLogPath: path9.join(root, "progress.json")
6209
+ scenesDir: path10.join(tmpDir, "scenes"),
6210
+ manimDir: path10.join(tmpDir, "manim"),
6211
+ remotionDir: path10.join(tmpDir, "remotion"),
6212
+ hyperframesDir: path10.join(tmpDir, "hyperframes"),
6213
+ progressLogPath: path10.join(root, "progress.json")
6137
6214
  };
6138
6215
  }
6139
6216
  function ensureChunkDirs(paths) {
6140
6217
  for (const dir of [
6141
- path9.dirname(paths.finalClipPath),
6218
+ path10.dirname(paths.finalClipPath),
6142
6219
  paths.tmpDir,
6143
6220
  paths.scenesDir,
6144
6221
  paths.manimDir,
6145
6222
  paths.remotionDir,
6146
6223
  paths.hyperframesDir
6147
6224
  ]) {
6148
- fs8.mkdirSync(dir, { recursive: true });
6225
+ fs9.mkdirSync(dir, { recursive: true });
6149
6226
  }
6150
6227
  }
6151
6228
  function appendProgress(progressLogPath, event) {
6152
6229
  try {
6153
- fs8.mkdirSync(path9.dirname(progressLogPath), { recursive: true });
6154
- fs8.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
6230
+ fs9.mkdirSync(path10.dirname(progressLogPath), { recursive: true });
6231
+ fs9.appendFileSync(progressLogPath, JSON.stringify(event) + "\n");
6155
6232
  } catch {
6156
6233
  }
6157
6234
  }
@@ -6254,10 +6331,10 @@ async function renderChunk(spec, opts = {}) {
6254
6331
  await renderWithEngine(spec, engine, paths, fps, tracker, opts);
6255
6332
  tracker.emit({ phase: "compositing", percent: 85 });
6256
6333
  const scenePaths = spec.shots.map((s) => sceneOutPath(paths, s));
6257
- const concatTarget = path10.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
6334
+ const concatTarget = path11.join(paths.tmpDir, `${spec.chunkId}_video.mp4`);
6258
6335
  await concatScenes(scenePaths, concatTarget, { signal: opts.signal });
6259
6336
  if (opts.signal?.aborted) throw new Error("aborted");
6260
- const audioAvailable = !!spec.audioPath && fs9.existsSync(spec.audioPath);
6337
+ const audioAvailable = !!spec.audioPath && fs10.existsSync(spec.audioPath);
6261
6338
  if (audioAvailable) {
6262
6339
  tracker.emit({ phase: "muxing", percent: 95 });
6263
6340
  await muxAudio(concatTarget, spec.audioPath, paths.finalClipPath, {
@@ -6269,7 +6346,7 @@ async function renderChunk(spec, opts = {}) {
6269
6346
  } else {
6270
6347
  warnings.push("no audioPath in spec \u2014 wrote silent clip");
6271
6348
  }
6272
- fs9.copyFileSync(concatTarget, paths.finalClipPath);
6349
+ fs10.copyFileSync(concatTarget, paths.finalClipPath);
6273
6350
  }
6274
6351
  tracker.emit({ phase: "done", percent: 100 });
6275
6352
  const renderTimeMs2 = (opts.now ?? Date.now)() - startedAt;
@@ -6308,7 +6385,7 @@ async function renderChunk(spec, opts = {}) {
6308
6385
  };
6309
6386
  }
6310
6387
  function sceneOutPath(paths, shot) {
6311
- return path10.join(paths.scenesDir, `${shot.id}.mp4`);
6388
+ return path11.join(paths.scenesDir, `${shot.id}.mp4`);
6312
6389
  }
6313
6390
  function abortedResult(spec, engine, fallbackDepth, startedAt, now) {
6314
6391
  return {
@@ -6341,7 +6418,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
6341
6418
  progress: tracker,
6342
6419
  resolvedEngine: engine
6343
6420
  };
6344
- fs9.mkdirSync(path10.dirname(ctx.sceneOutPath), { recursive: true });
6421
+ fs10.mkdirSync(path11.dirname(ctx.sceneOutPath), { recursive: true });
6345
6422
  const release = opts.acquireManimSlot ? await opts.acquireManimSlot() : void 0;
6346
6423
  try {
6347
6424
  tracker.emit({
@@ -6353,7 +6430,7 @@ async function renderWithEngine(spec, engine, paths, fps, tracker, opts) {
6353
6430
  } finally {
6354
6431
  release?.();
6355
6432
  }
6356
- if (!fs9.existsSync(ctx.sceneOutPath)) {
6433
+ if (!fs10.existsSync(ctx.sceneOutPath)) {
6357
6434
  throw new Error(
6358
6435
  `engine ${engine} produced no output at ${ctx.sceneOutPath} for shot ${shot.id}`
6359
6436
  );
@@ -6537,8 +6614,8 @@ var BridgePoller = class {
6537
6614
  if (!/^[a-z][a-z0-9-]*$/i.test(name)) return { available: false, path: null };
6538
6615
  const r = spawnSync("which", [name], { encoding: "utf-8", timeout: 2e3 });
6539
6616
  if (r.status !== 0) return { available: false, path: null };
6540
- const path11 = (r.stdout ?? "").trim();
6541
- return { available: !!path11, path: path11 || null };
6617
+ const path12 = (r.stdout ?? "").trim();
6618
+ return { available: !!path12, path: path12 || null };
6542
6619
  }
6543
6620
  async heartbeat() {
6544
6621
  if (this.stopped) return;
@@ -6759,13 +6836,36 @@ var BridgePoller = class {
6759
6836
  onProgress,
6760
6837
  signal: cancelController.signal
6761
6838
  });
6839
+ const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
6840
+ const uploadOutcomes = [];
6841
+ for (const r of result.results) {
6842
+ if (!r.ok || !r.outputPath) continue;
6843
+ const spec = specByChunk.get(r.chunkId);
6844
+ if (!spec) continue;
6845
+ const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
6846
+ uploadOutcomes.push({ chunkId: r.chunkId, ...upload });
6847
+ void this.postRenderProgress(jobId, payload.projectId, {
6848
+ chunkId: r.chunkId,
6849
+ phase: upload.ok ? "done" : "error",
6850
+ percent: upload.ok ? 100 : 95,
6851
+ elapsedMs: r.renderTimeMs,
6852
+ resolvedEngine: r.engine,
6853
+ storjKey: upload.storjKey,
6854
+ outputPath: r.outputPath,
6855
+ error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
6856
+ ts: (/* @__PURE__ */ new Date()).toISOString()
6857
+ });
6858
+ }
6762
6859
  const okCount = result.results.filter((r) => r.ok).length;
6860
+ const uploadedCount = uploadOutcomes.filter((u) => u.ok).length;
6763
6861
  const summary = {
6764
6862
  kind: "render-clips",
6765
- ok: result.ok,
6863
+ ok: result.ok && uploadedCount === okCount,
6766
6864
  total: result.results.length,
6767
6865
  succeeded: okCount,
6768
6866
  failed: result.results.length - okCount,
6867
+ uploaded: uploadedCount,
6868
+ uploadFailed: uploadOutcomes.filter((u) => !u.ok).length,
6769
6869
  totalRenderTimeMs: result.totalRenderTimeMs,
6770
6870
  observedPeakConcurrency: result.observedPeakConcurrency
6771
6871
  };
@@ -6776,6 +6876,48 @@ var BridgePoller = class {
6776
6876
  this.renderCancelControllers.delete(jobId);
6777
6877
  }
6778
6878
  }
6879
+ /**
6880
+ * POST a rendered clip to /api/cli-bridge/upload-clip as multipart
6881
+ * form data. The endpoint uploads the binary to Storj, inserts a
6882
+ * `clips` table row, and returns the storj_key. Best-effort — a
6883
+ * failed upload is recorded as a warning and the chunk's progress
6884
+ * event reports phase='error' but doesn't take down the whole batch.
6885
+ */
6886
+ async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
6887
+ try {
6888
+ const fs12 = await import("fs/promises");
6889
+ const stat = await fs12.stat(result.outputPath);
6890
+ if (!stat.isFile() || stat.size === 0) {
6891
+ return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
6892
+ }
6893
+ const buf = await fs12.readFile(result.outputPath);
6894
+ const durationFrames = Math.max(1, Math.round(result.durationSec * 30));
6895
+ const form = new FormData();
6896
+ form.append("bridgeJobId", bridgeJobId);
6897
+ form.append("chunkId", result.chunkId);
6898
+ form.append("engine", result.engine);
6899
+ form.append("aspect", spec.aspect);
6900
+ form.append("durationSec", String(result.durationSec));
6901
+ form.append("durationFrames", String(durationFrames));
6902
+ form.append("file", new Blob([new Uint8Array(buf)], { type: "video/mp4" }), `${result.chunkId}.mp4`);
6903
+ const resp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-clip`, {
6904
+ method: "POST",
6905
+ headers: { Authorization: `Bearer ${this.token}` },
6906
+ body: form,
6907
+ signal: AbortSignal.timeout(5 * 6e4)
6908
+ });
6909
+ const text = await resp.text().catch(() => "");
6910
+ if (!resp.ok) {
6911
+ return { ok: false, error: `HTTP ${resp.status}: ${text.slice(0, 300)}` };
6912
+ }
6913
+ const parsed = text ? JSON.parse(text) : {};
6914
+ log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${parsed.storjKey ?? "(no key)"}`);
6915
+ void projectId;
6916
+ return { ok: true, storjKey: parsed.storjKey };
6917
+ } catch (err) {
6918
+ return { ok: false, error: err.message };
6919
+ }
6920
+ }
6779
6921
  /** POST a single RenderProgress event to /api/render-progress. */
6780
6922
  async postRenderProgress(bridgeJobId, projectId, event) {
6781
6923
  try {
@@ -6813,9 +6955,9 @@ var BridgePoller = class {
6813
6955
  error: `stitch-final job requires @forge/pipeline/stitch (not installed): ${err.message}`
6814
6956
  };
6815
6957
  }
6816
- const path11 = await import("path");
6817
- const projectRoot = path11.resolve(process.cwd(), "forge-renders", payload.projectSlug);
6818
- const clipsDir = path11.join(projectRoot, "clips");
6958
+ const path12 = await import("path");
6959
+ const projectRoot = path12.resolve(process.cwd(), "forge-renders", payload.projectSlug);
6960
+ const clipsDir = path12.join(projectRoot, "clips");
6819
6961
  const publish = async (phase, eventPayload) => {
6820
6962
  try {
6821
6963
  await fetch(`${this.baseUrl}/api/cli-bridge/stitch-event`, {
@@ -6841,7 +6983,7 @@ var BridgePoller = class {
6841
6983
  projectSlug: payload.projectSlug,
6842
6984
  clipsDir,
6843
6985
  chunkOrder: payload.chunkOrder ?? [],
6844
- masterAudioPath: path11.isAbsolute(masterAudioRel) ? masterAudioRel : path11.join(projectRoot, masterAudioRel),
6986
+ masterAudioPath: path12.isAbsolute(masterAudioRel) ? masterAudioRel : path12.join(projectRoot, masterAudioRel),
6845
6987
  outputDir: projectRoot,
6846
6988
  aspect: payload.aspect,
6847
6989
  skipTransitions: payload.skipTransitions,
@@ -7025,10 +7167,10 @@ function collectImagesFromCodexStdout(stdout, maxCount) {
7025
7167
  for (const p of paths) {
7026
7168
  if (images.length >= maxCount) break;
7027
7169
  try {
7028
- const stat = fs10.statSync(p);
7170
+ const stat = fs11.statSync(p);
7029
7171
  if (!stat.isFile() || stat.size <= 0) continue;
7030
7172
  images.push({
7031
- base64: fs10.readFileSync(p).toString("base64"),
7173
+ base64: fs11.readFileSync(p).toString("base64"),
7032
7174
  mimeType: mimeTypeForPath(p),
7033
7175
  model: "codex-cli:imagegen"
7034
7176
  });
package/dist/index.js CHANGED
@@ -1615,7 +1615,7 @@ Return ONLY the complete updated TSX. No markdown fences, no explanation.`;
1615
1615
  return "0.0.0";
1616
1616
  })();
1617
1617
  void (async () => {
1618
- const { BridgePoller } = await import("./bridge-poller-AMTSJVFB.js");
1618
+ const { BridgePoller } = await import("./bridge-poller-HA7G7ILD.js");
1619
1619
  const poller = new BridgePoller({ baseUrl: bridgeUrl, token: bridgeToken, clientVersion: `storyforge ${pkgVersion}` });
1620
1620
  poller.start();
1621
1621
  })();
@@ -1992,7 +1992,7 @@ async function installRenderersCommand(opts = {}) {
1992
1992
  }
1993
1993
 
1994
1994
  // src/index.ts
1995
- var VERSION = "0.7.4";
1995
+ var VERSION = "0.7.5";
1996
1996
  var HELP = `
1997
1997
  storyforge \u2014 local bridge for the Forge video production web app
1998
1998
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "storyforge",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "StoryForge — local bridge for the Forge video production web app. Parallel clip-render orchestrator (Remotion 4 + Manim + HyperFrames + ffmpeg) + final video stitcher + dependency doctor.",
5
5
  "type": "module",
6
6
  "bin": {