storyforge 0.7.4 → 0.7.5

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.
@@ -6759,13 +6759,36 @@ var BridgePoller = class {
6759
6759
  onProgress,
6760
6760
  signal: cancelController.signal
6761
6761
  });
6762
+ const specByChunk = new Map(payload.specs.map((s) => [s.chunkId, s]));
6763
+ const uploadOutcomes = [];
6764
+ for (const r of result.results) {
6765
+ if (!r.ok || !r.outputPath) continue;
6766
+ const spec = specByChunk.get(r.chunkId);
6767
+ if (!spec) continue;
6768
+ const upload = await this.uploadClipToStorj(jobId, payload.projectId, spec, r);
6769
+ uploadOutcomes.push({ chunkId: r.chunkId, ...upload });
6770
+ void this.postRenderProgress(jobId, payload.projectId, {
6771
+ chunkId: r.chunkId,
6772
+ phase: upload.ok ? "done" : "error",
6773
+ percent: upload.ok ? 100 : 95,
6774
+ elapsedMs: r.renderTimeMs,
6775
+ resolvedEngine: r.engine,
6776
+ storjKey: upload.storjKey,
6777
+ outputPath: r.outputPath,
6778
+ error: upload.ok ? void 0 : `upload failed: ${upload.error ?? "unknown"}`,
6779
+ ts: (/* @__PURE__ */ new Date()).toISOString()
6780
+ });
6781
+ }
6762
6782
  const okCount = result.results.filter((r) => r.ok).length;
6783
+ const uploadedCount = uploadOutcomes.filter((u) => u.ok).length;
6763
6784
  const summary = {
6764
6785
  kind: "render-clips",
6765
- ok: result.ok,
6786
+ ok: result.ok && uploadedCount === okCount,
6766
6787
  total: result.results.length,
6767
6788
  succeeded: okCount,
6768
6789
  failed: result.results.length - okCount,
6790
+ uploaded: uploadedCount,
6791
+ uploadFailed: uploadOutcomes.filter((u) => !u.ok).length,
6769
6792
  totalRenderTimeMs: result.totalRenderTimeMs,
6770
6793
  observedPeakConcurrency: result.observedPeakConcurrency
6771
6794
  };
@@ -6776,6 +6799,48 @@ var BridgePoller = class {
6776
6799
  this.renderCancelControllers.delete(jobId);
6777
6800
  }
6778
6801
  }
6802
+ /**
6803
+ * POST a rendered clip to /api/cli-bridge/upload-clip as multipart
6804
+ * form data. The endpoint uploads the binary to Storj, inserts a
6805
+ * `clips` table row, and returns the storj_key. Best-effort — a
6806
+ * failed upload is recorded as a warning and the chunk's progress
6807
+ * event reports phase='error' but doesn't take down the whole batch.
6808
+ */
6809
+ async uploadClipToStorj(bridgeJobId, projectId, spec, result) {
6810
+ try {
6811
+ const fs11 = await import("fs/promises");
6812
+ const stat = await fs11.stat(result.outputPath);
6813
+ if (!stat.isFile() || stat.size === 0) {
6814
+ return { ok: false, error: `local clip empty or missing at ${result.outputPath}` };
6815
+ }
6816
+ const buf = await fs11.readFile(result.outputPath);
6817
+ const durationFrames = Math.max(1, Math.round(result.durationSec * 30));
6818
+ const form = new FormData();
6819
+ form.append("bridgeJobId", bridgeJobId);
6820
+ form.append("chunkId", result.chunkId);
6821
+ form.append("engine", result.engine);
6822
+ form.append("aspect", spec.aspect);
6823
+ form.append("durationSec", String(result.durationSec));
6824
+ form.append("durationFrames", String(durationFrames));
6825
+ form.append("file", new Blob([new Uint8Array(buf)], { type: "video/mp4" }), `${result.chunkId}.mp4`);
6826
+ const resp = await fetch(`${this.baseUrl}/api/cli-bridge/upload-clip`, {
6827
+ method: "POST",
6828
+ headers: { Authorization: `Bearer ${this.token}` },
6829
+ body: form,
6830
+ signal: AbortSignal.timeout(5 * 6e4)
6831
+ });
6832
+ const text = await resp.text().catch(() => "");
6833
+ if (!resp.ok) {
6834
+ return { ok: false, error: `HTTP ${resp.status}: ${text.slice(0, 300)}` };
6835
+ }
6836
+ const parsed = text ? JSON.parse(text) : {};
6837
+ log.info(`[bridge] uploaded ${result.chunkId.slice(0, 8)} -> ${parsed.storjKey ?? "(no key)"}`);
6838
+ void projectId;
6839
+ return { ok: true, storjKey: parsed.storjKey };
6840
+ } catch (err) {
6841
+ return { ok: false, error: err.message };
6842
+ }
6843
+ }
6779
6844
  /** POST a single RenderProgress event to /api/render-progress. */
6780
6845
  async postRenderProgress(bridgeJobId, projectId, event) {
6781
6846
  try {
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-C2SMBUQX.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.7.5",
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": {