storyforge 0.7.3 → 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.
@@ -5587,13 +5587,22 @@ var renderGeminiRemotion = async (shot, ctx) => {
5587
5587
  );
5588
5588
  const durationFrames = Math.max(1, Math.round(shot.durationSec * ctx.fps));
5589
5589
  const dims = ASPECT_DIMS[ctx.aspect];
5590
+ const remotion = requireRemotionRoot();
5591
+ const publicRel = path4.posix.join(
5592
+ "clip-render-cache",
5593
+ ctx.chunkId,
5594
+ `${shot.id}${path4.extname(localImage) || ".png"}`
5595
+ );
5596
+ const publicAbs = path4.join(remotion.cwd, "public", publicRel);
5597
+ fs4.mkdirSync(path4.dirname(publicAbs), { recursive: true });
5598
+ fs4.copyFileSync(localImage, publicAbs);
5590
5599
  const propsPath = path4.join(ctx.tmpDir, "remotion", `${shot.id}_gr_props.json`);
5591
5600
  fs4.mkdirSync(path4.dirname(propsPath), { recursive: true });
5592
5601
  fs4.writeFileSync(
5593
5602
  propsPath,
5594
5603
  JSON.stringify(
5595
5604
  {
5596
- imagePath: localImage,
5605
+ imagePath: publicRel,
5597
5606
  overlayText: shot.overlayText ?? "",
5598
5607
  durationInFrames: durationFrames,
5599
5608
  width: dims.width,
@@ -5605,8 +5614,7 @@ var renderGeminiRemotion = async (shot, ctx) => {
5605
5614
  2
5606
5615
  )
5607
5616
  );
5608
- const remotion = requireRemotionRoot();
5609
- const compId = ctx.aspect === "9:16" ? "ForgeVideoVertical" : "ForgeVideo";
5617
+ const compId = ctx.aspect === "9:16" ? "ForgeShotVertical" : "ForgeShot";
5610
5618
  ctx.progress.emit({
5611
5619
  phase: "compositing",
5612
5620
  shotId: shot.id,
@@ -6751,13 +6759,36 @@ var BridgePoller = class {
6751
6759
  onProgress,
6752
6760
  signal: cancelController.signal
6753
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
+ }
6754
6782
  const okCount = result.results.filter((r) => r.ok).length;
6783
+ const uploadedCount = uploadOutcomes.filter((u) => u.ok).length;
6755
6784
  const summary = {
6756
6785
  kind: "render-clips",
6757
- ok: result.ok,
6786
+ ok: result.ok && uploadedCount === okCount,
6758
6787
  total: result.results.length,
6759
6788
  succeeded: okCount,
6760
6789
  failed: result.results.length - okCount,
6790
+ uploaded: uploadedCount,
6791
+ uploadFailed: uploadOutcomes.filter((u) => !u.ok).length,
6761
6792
  totalRenderTimeMs: result.totalRenderTimeMs,
6762
6793
  observedPeakConcurrency: result.observedPeakConcurrency
6763
6794
  };
@@ -6768,6 +6799,48 @@ var BridgePoller = class {
6768
6799
  this.renderCancelControllers.delete(jobId);
6769
6800
  }
6770
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
+ }
6771
6844
  /** POST a single RenderProgress event to /api/render-progress. */
6772
6845
  async postRenderProgress(bridgeJobId, projectId, event) {
6773
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-Z74DF5GN.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.3";
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.3",
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": {