@nodaro/shared 2.27.0 → 3.1.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.
@@ -14,14 +14,14 @@ import {
14
14
  SCENE3D_PLAN_TYPE,
15
15
  SCENE3D_SCHEMA_VERSION,
16
16
  applyScene3DEditOperations,
17
- isScene3DPlan,
17
+ isScene3DPlanV1,
18
18
  newScene3DRevisionId,
19
19
  scene3DDeepEqual,
20
20
  scene3DEditOperationSchema,
21
21
  scene3DPlanSchema,
22
22
  summarizeScene3DOperations,
23
23
  type Scene3DObject,
24
- type Scene3DPlan,
24
+ type Scene3DPlanV1,
25
25
  } from "../index.js"
26
26
 
27
27
  const REV_A = "11111111-2222-4333-8444-555555555555"
@@ -41,7 +41,7 @@ function object(id: string, over: Partial<Scene3DObject> = {}): Scene3DObject {
41
41
  }
42
42
  }
43
43
 
44
- function plan(over: Partial<Scene3DPlan> = {}): Scene3DPlan {
44
+ function plan(over: Partial<Scene3DPlanV1> = {}): Scene3DPlanV1 {
45
45
  return {
46
46
  planType: SCENE3D_PLAN_TYPE,
47
47
  schemaVersion: SCENE3D_SCHEMA_VERSION,
@@ -61,7 +61,7 @@ function plan(over: Partial<Scene3DPlan> = {}): Scene3DPlan {
61
61
  describe("scene3DPlanSchema — structure", () => {
62
62
  it("accepts a minimal well-formed plan", () => {
63
63
  expect(scene3DPlanSchema.safeParse(plan()).success).toBe(true)
64
- expect(isScene3DPlan(plan())).toBe(true)
64
+ expect(isScene3DPlanV1(plan())).toBe(true)
65
65
  })
66
66
 
67
67
  it("defaults sensorWidthMm to full frame", () => {
@@ -345,7 +345,7 @@ describe("applyScene3DEditOperations", () => {
345
345
  })
346
346
 
347
347
  it("refuses to remove an object a reference points at", () => {
348
- const referenced: Scene3DPlan = {
348
+ const referenced: Scene3DPlanV1 = {
349
349
  ...plan({ objects: [object("hero"), object("prop")] }),
350
350
  references: [{ id: "r1", url: "https://x.test/a.png", kind: "image", role: "appearance", objectId: "prop" }],
351
351
  }
@@ -378,7 +378,7 @@ describe("applyScene3DEditOperations", () => {
378
378
  })
379
379
 
380
380
  it("rejects an invalid input plan without applying anything", () => {
381
- const result = applyScene3DEditOperations({ ...base, objects: [] } as Scene3DPlan, [
381
+ const result = applyScene3DEditOperations({ ...base, objects: [] } as Scene3DPlanV1, [
382
382
  { op: "set-background", color: "#000000" },
383
383
  ])
384
384
  expect(result.ok).toBe(false)
package/src/index.ts CHANGED
@@ -1057,6 +1057,12 @@ export type { EntityNodeKind } from "./entity-node-fields.js"
1057
1057
  // SDK/MCP surface. Structure only — no prompts, no pricing. ---
1058
1058
  export * from "./scene3d.js"
1059
1059
  export * from "./scene3d-edit.js"
1060
+ export * from "./scene3d-v2.js"
1061
+ export * from "./scene3d-v2-plan.js"
1062
+ export * from "./scene3d-v2-resources.js"
1063
+ export * from "./scene3d-camera-track.js"
1064
+ // --- 3D Render Pro: one durable operation, scene + video in one result ---
1065
+ export * from "./pro-3d-render.js"
1060
1066
 
1061
1067
  // --- transient studio keys — the public share read strips them ---
1062
1068
  export {
@@ -1064,3 +1070,6 @@ export {
1064
1070
  STUDIO_SHOT_TRANSIENT_KEYS,
1065
1071
  stripStudioTransientSettings,
1066
1072
  } from "./studio-transient.js"
1073
+
1074
+ export * from "./scene3d-v2-edit.js"
1075
+ export * from "./scene3d-authoring-engine.js"
@@ -1121,6 +1121,12 @@ export const ASPECT_RATIO_DIMENSIONS: Record<string, { width: number; height: nu
1121
1121
  "9:16": { width: 1080, height: 1920 },
1122
1122
  "1:1": { width: 1080, height: 1080 },
1123
1123
  "4:5": { width: 1080, height: 1350 },
1124
+ // Ultra-wide. 1680x720 rather than a 1920-wide pair because the Scene3D v2
1125
+ // admission bounds require even integers on both axes and 1920/(21/9) is
1126
+ // odd; 1680x720 is the pair the scene contract names as supported. Additive:
1127
+ // every consumer here is a keyed lookup with a fallback, and a node only
1128
+ // reaches this entry if its own aspect enum offers 21:9 (today, Pro 3D).
1129
+ "21:9": { width: 1680, height: 720 },
1124
1130
  }
1125
1131
 
1126
1132
  /** Motion transfer providers */
@@ -2637,6 +2643,10 @@ export const COMPOSER_PLAN_MAP: Readonly<Record<string, { planType: string; plan
2637
2643
  // render-video routes either one to the `3d-scene` renderer unchanged.
2638
2644
  "generate-3d-scene": { planType: "3d-scene", planField: "scenePlan" },
2639
2645
  "edit-3d-scene": { planType: "3d-scene", planField: "scenePlan" },
2646
+ // 3D Render Pro authors the SAME `scenePlan` revision (v2) alongside its
2647
+ // MP4, so the render-only re-run reads it through this map exactly as it
2648
+ // reads a Basic revision — no second plan lane, no second render path.
2649
+ "pro-3d-render": { planType: "3d-scene", planField: "scenePlan" },
2640
2650
  }
2641
2651
 
2642
2652
  /** Every composer plan-field name, derived from COMPOSER_PLAN_MAP (single source
@@ -46,6 +46,7 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
46
46
  "3d-title": ["titlePrompt"],
47
47
  "generate-3d-scene": ["scenePrompt"],
48
48
  "edit-3d-scene": ["editPrompt"],
49
+ "pro-3d-render": ["scenePrompt"],
49
50
  "motion-graphics": ["motionPrompt"],
50
51
  "generate-script": ["styleGuide"],
51
52
  "speech-to-video": ["prompt", "negativePrompt"],
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { GenericNode, GenericEdge } from "./types.js"
7
7
  import type { PresentationItem } from "./presentation-types.js"
8
+ import { AUDIO_PRODUCER_TYPES, VIDEO_PRODUCER_TYPES } from "./producer-types.js"
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Node type sets
@@ -84,6 +85,15 @@ const NON_OUTPUT_TYPES = new Set([
84
85
  "component",
85
86
  ])
86
87
 
88
+ /**
89
+ * Node types that count as an output even when something is wired to their
90
+ * output handle (the legacy `presentationVisible` rule below).
91
+ *
92
+ * A hand-kept list, and one the platform has outgrown: it is unioned at the use
93
+ * site with the producer vocabularies the canvas validators already maintain
94
+ * (see `isMediaProducingType`), so a new media node is covered by the set it
95
+ * must join anyway rather than by remembering to edit this one.
96
+ */
87
97
  const MEDIA_PRODUCING_TYPES = new Set([
88
98
  "generate-image",
89
99
  "edit-image",
@@ -121,6 +131,11 @@ const IMAGE_OUTPUT_TYPES = new Set([
121
131
  "upload-image",
122
132
  ])
123
133
 
134
+ /**
135
+ * Video-output types NOT already declared by `VIDEO_PRODUCER_TYPES` — the two
136
+ * are unioned in `getOutputType`, which is where the reading order (and the
137
+ * reason the literal sets win) is documented.
138
+ */
124
139
  const VIDEO_OUTPUT_TYPES = new Set([
125
140
  "image-to-video", "text-to-video", "video-to-video", "extend-video",
126
141
  "render-video", "video-composer", "after-effects", "lottie-overlay",
@@ -189,7 +204,7 @@ export function getOutputNodes<T extends GenericNode>(
189
204
  // Backwards compat: old flag on output-eligible nodes
190
205
  if (n.data.presentationVisible === true) {
191
206
  if (NON_OUTPUT_TYPES.has(n.type)) return false
192
- return !nodesWithOutgoing.has(n.id) || MEDIA_PRODUCING_TYPES.has(n.type)
207
+ return !nodesWithOutgoing.has(n.id) || isMediaProducingType(n.type)
193
208
  }
194
209
  return false
195
210
  }
@@ -197,16 +212,53 @@ export function getOutputNodes<T extends GenericNode>(
197
212
  })
198
213
  }
199
214
 
200
- /** Map node type to its output media type. */
215
+ /**
216
+ * Map node type to its output media type.
217
+ *
218
+ * The four literal sets above are read FIRST and win: they are the presentation
219
+ * classifier's own opinion, including for dual-mode nodes whose default medium
220
+ * is not their handle set (voice-changer and dubbing are audio here even though
221
+ * they can emit video).
222
+ *
223
+ * Anything they do not name falls through to the producer vocabularies the
224
+ * canvas validators and the orchestrator already maintain
225
+ * (`packages/shared/src/producer-types.ts`). Those sets are what a new media
226
+ * node MUST join for its outputs to connect at all, so deriving the tail from
227
+ * them is what stops this map from silently drifting behind the node catalogue
228
+ * — which it had (3D Render Pro, Generate Video, Generate Video Pro and a dozen
229
+ * ffmpeg nodes all read as `"data"`, so a published app rendered them as a JSON
230
+ * blob and `/v1` app schemas declared the wrong output type).
231
+ *
232
+ * `DYNAMIC_PRODUCER_TYPES` is deliberately NOT consulted: a node whose medium
233
+ * is decided at run time has no static answer, and `"data"` is the honest one.
234
+ */
201
235
  export function getOutputType(nodeType: string | undefined): OutputType {
202
236
  if (!nodeType) return "data"
203
237
  if (IMAGE_OUTPUT_TYPES.has(nodeType)) return "image"
204
238
  if (VIDEO_OUTPUT_TYPES.has(nodeType)) return "video"
205
239
  if (AUDIO_OUTPUT_TYPES.has(nodeType)) return "audio"
206
240
  if (TEXT_OUTPUT_TYPES.has(nodeType)) return "text"
241
+ if (VIDEO_PRODUCER_TYPES.has(nodeType)) return "video"
242
+ if (AUDIO_PRODUCER_TYPES.has(nodeType)) return "audio"
207
243
  return "data"
208
244
  }
209
245
 
246
+ /**
247
+ * Whether a node produces media of its own — the test the legacy
248
+ * `presentationVisible` rule uses to keep a media node as an OUTPUT even when
249
+ * it also feeds something downstream.
250
+ *
251
+ * Same union, same reason as `getOutputType`: the hand-kept list plus the
252
+ * producer vocabularies. Text producers stay where the literal list put them
253
+ * (`generate-script`, `ai-writer`, `llm-chat` are members; `transcribe` and
254
+ * `qa-check` are not) — this is about media, not about every node with a value.
255
+ */
256
+ function isMediaProducingType(nodeType: string): boolean {
257
+ if (MEDIA_PRODUCING_TYPES.has(nodeType)) return true
258
+ const output = getOutputType(nodeType)
259
+ return output === "image" || output === "video" || output === "audio"
260
+ }
261
+
210
262
  /** Extract the result URL or text from a node's data. */
211
263
  export function getNodeResult(
212
264
  nodeData: Record<string, unknown>,
@@ -0,0 +1,466 @@
1
+ /**
2
+ * The `pro-3d-render` ("3D Render Pro") WIRE CONTRACT.
3
+ *
4
+ * ONE durable operation, three ways in. A `source` says WHERE the scene comes
5
+ * from — a new brief, an existing revision, or a completed desktop export —
6
+ * and the settled job carries BOTH halves of the result: the exact composition
7
+ * (`scenePlan`) and the standard video field every downstream consumer already
8
+ * reads (`videoUrl`).
9
+ *
10
+ * The `source` is a strict discriminated union rather than a bag of optional
11
+ * fields, and that is the load-bearing decision here. "Prompt present" and
12
+ * "revisionId present" are not two settings on one request: they select
13
+ * different pipelines with different costs. A flat shape lets a caller send
14
+ * both, or neither, and pushes the "what did they actually mean" decision into
15
+ * whichever surface reads it last — which is how an existing scene silently
16
+ * becomes a paid re-authoring run.
17
+ *
18
+ * The same union is what makes RENDER-ONLY expressible: `{kind:'scene'}` with
19
+ * NO `editPrompt` means "export this revision", and its absence must survive
20
+ * every hop unchanged. Nothing may helpfully substitute an empty string or
21
+ * copy the node's brief into it — that converts a free export into an
22
+ * authoring run the user never asked for.
23
+ *
24
+ * What lives here is only what a client needs to CALL the operation, QUOTE it
25
+ * and READ its result. How the scene is planned, compiled, built, priced or
26
+ * authorized is not part of this contract and is not described here.
27
+ *
28
+ * Deliberately NOT here:
29
+ * - a model chooser. The planner is fixed and server-owned.
30
+ * - a credit number. The cost is resolved server-side and returned by the
31
+ * quote endpoint; a constant in a published package would be a wrong answer
32
+ * shipped to every consumer (see `PRO3D_RENDER_CREDIT_ID`).
33
+ */
34
+ import { z } from "zod"
35
+ import { SCENE3D_LIMITS, type Scene3DReference } from "./scene3d.js"
36
+ import { scene3DAnyPlanSchema, type Scene3DPlan } from "./scene3d-v2-plan.js"
37
+
38
+ /** Canvas/API/MCP node type. */
39
+ export const PRO3D_RENDER_NODE_TYPE = "pro-3d-render"
40
+
41
+ /** Display name. One string, so every surface spells it the same way. */
42
+ export const PRO3D_RENDER_LABEL = "3D Render Pro"
43
+
44
+ /**
45
+ * The credit identifier the operation settles under.
46
+ *
47
+ * An IDENTIFIER, not a price: the number is operator/deployment configuration
48
+ * (a `model_pricing` row), and the per-run ceiling comes from a quote. There is
49
+ * deliberately no fallback constant — a flat default would underprice an
50
+ * operation that plans, builds and renders, and "cheap by accident" is not a
51
+ * failure mode you notice from the outside.
52
+ */
53
+ export const PRO3D_RENDER_CREDIT_ID = "pro-3d-render"
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // Vocabularies
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /**
60
+ * Where the scene is built. `blender-local` is a paired desktop and is refused
61
+ * unless the deployment both enables it and has an engine advertising it — an
62
+ * unknown or unavailable engine is an error, never a downgrade to the cheaper
63
+ * cloud path.
64
+ */
65
+ export const PRO3D_RENDER_ENGINES = ["blender-cloud", "blender-local"] as const
66
+ export type Pro3DRenderEngine = (typeof PRO3D_RENDER_ENGINES)[number]
67
+ export const PRO3D_RENDER_DEFAULT_ENGINE: Pro3DRenderEngine = "blender-cloud"
68
+
69
+ /**
70
+ * Render quality profiles.
71
+ *
72
+ * One today. A surface must advertise only what the installed engine reports
73
+ * (`capabilities().pro.qualityProfiles`) rather than this list — offering a
74
+ * profile the engine cannot serve is a run that fails after the user chose it.
75
+ */
76
+ export const PRO3D_RENDER_QUALITY_PROFILES = ["standard"] as const
77
+ export type Pro3DRenderQuality = (typeof PRO3D_RENDER_QUALITY_PROFILES)[number]
78
+ export const PRO3D_RENDER_DEFAULT_QUALITY: Pro3DRenderQuality = "standard"
79
+
80
+ /** Material/lighting treatment. Clay is the movement-reference default. */
81
+ export const PRO3D_RENDER_STYLES = ["clay"] as const
82
+ export type Pro3DRenderStyle = (typeof PRO3D_RENDER_STYLES)[number]
83
+ export const PRO3D_RENDER_DEFAULT_STYLE: Pro3DRenderStyle = "clay"
84
+
85
+ /**
86
+ * The correction budget: how many repair passes the engine may spend after its
87
+ * first attempt. Displayed to the user because each pass is paid work.
88
+ */
89
+ export const PRO3D_RENDER_MIN_REPAIR_PASSES = 0
90
+ export const PRO3D_RENDER_MAX_REPAIR_PASSES = 2
91
+ export const PRO3D_RENDER_DEFAULT_REPAIR_PASSES = 2
92
+
93
+ /**
94
+ * Aspect ratios the node authors at.
95
+ *
96
+ * `21:9` is not decoration: the acceptance fixture is a 30-second 21:9 scene,
97
+ * so a set that omitted it could not express the case the feature is measured
98
+ * against. Its canonical pixel pair is the contract's explicitly supported
99
+ * 1680×720 (see `ASPECT_RATIO_DIMENSIONS`).
100
+ */
101
+ export const PRO3D_RENDER_ASPECT_RATIOS = ["16:9", "9:16", "1:1", "4:5", "21:9"] as const
102
+ export type Pro3DRenderAspectRatio = (typeof PRO3D_RENDER_ASPECT_RATIOS)[number]
103
+
104
+ /** Same prompt ceiling the Basic authoring routes enforce. */
105
+ export const PRO3D_RENDER_PROMPT_MAX = 8000
106
+
107
+ /**
108
+ * Request bounds shared by every ingress (HTTP route, orchestrator, MCP, SDK).
109
+ *
110
+ * Timing/reference limits reuse the Basic authoring limits verbatim rather
111
+ * than declaring a second set: the two nodes describe the same kind of scene,
112
+ * and two drifting ceilings is how one surface starts accepting what another
113
+ * refuses.
114
+ */
115
+ export const PRO3D_RENDER_LIMITS = {
116
+ promptMax: PRO3D_RENDER_PROMPT_MAX,
117
+ editPromptMax: PRO3D_RENDER_PROMPT_MAX,
118
+ minDurationSeconds: SCENE3D_LIMITS.minDurationSeconds,
119
+ maxDurationSeconds: SCENE3D_LIMITS.maxDurationSeconds,
120
+ minFps: SCENE3D_LIMITS.minFps,
121
+ maxFps: SCENE3D_LIMITS.maxFps,
122
+ maxReferences: SCENE3D_LIMITS.maxReferences,
123
+ /** Opaque ids the caller echoes back (quote, export, connection). */
124
+ maxIdLength: 200,
125
+ /** `Idempotency-Key` bounds — the platform's floor, with a ceiling so an
126
+ * unbounded header can never reach a lookup or a database column. */
127
+ minIdempotencyKeyLength: 8,
128
+ maxIdempotencyKeyLength: 255,
129
+ } as const
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // The source union
133
+ // ---------------------------------------------------------------------------
134
+
135
+ export const PRO3D_RENDER_SOURCE_KINDS = ["prompt", "scene", "local-export"] as const
136
+ export type Pro3DRenderSourceKind = (typeof PRO3D_RENDER_SOURCE_KINDS)[number]
137
+
138
+ /** A new scene, authored from a brief plus optional image/video references. */
139
+ export interface Pro3DRenderPromptSource {
140
+ kind: "prompt"
141
+ prompt: string
142
+ references?: readonly Scene3DReference[]
143
+ }
144
+
145
+ /**
146
+ * An existing immutable revision.
147
+ *
148
+ * `editPrompt` ABSENT is the render-only path — export this revision, spend no
149
+ * authoring or build credits. Its absence is meaningful and must be preserved
150
+ * verbatim; an empty string is not the same request.
151
+ *
152
+ * Retained revisions are authorized through their current scene permissions.
153
+ * `sourceJobId` locates Basic scenes stored only in job history; it is required
154
+ * for that source, but optional for retained scenes (including manual edits).
155
+ */
156
+ export interface Pro3DRenderSceneSource {
157
+ kind: "scene"
158
+ revisionId: string
159
+ sourceJobId?: string
160
+ editPrompt?: string
161
+ }
162
+
163
+ /** A completed export from a paired desktop Blender. */
164
+ export interface Pro3DRenderLocalExportSource {
165
+ kind: "local-export"
166
+ exportId: string
167
+ connectionId: string
168
+ }
169
+
170
+ export type Pro3DRenderSource =
171
+ | Pro3DRenderPromptSource
172
+ | Pro3DRenderSceneSource
173
+ | Pro3DRenderLocalExportSource
174
+
175
+ /** True when this source exports an existing revision without re-authoring it. */
176
+ export function isPro3DRenderRenderOnly(source: Pro3DRenderSource): boolean {
177
+ return source.kind === "scene" && source.editPrompt === undefined
178
+ }
179
+
180
+ /**
181
+ * Which scene-schema version a source PRODUCES, or `null` when only the server
182
+ * can know.
183
+ *
184
+ * A `prompt` or `local-export` source always mints a fresh v2 manifest, so a
185
+ * client that cannot read v2 is refusable for free, before any work. A `scene`
186
+ * source inherits whatever version the named revision already is — the host
187
+ * does not resolve revisions, so demanding v2 there would refuse a perfectly
188
+ * renderable retained v1 scene.
189
+ */
190
+ export function pro3DRenderProducedSchemaVersion(source: Pro3DRenderSource): number | null {
191
+ return source.kind === "scene" ? null : 2
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Quote
196
+ // ---------------------------------------------------------------------------
197
+
198
+ /** One priced component of a quote. Display copy, not economics. */
199
+ export interface Pro3DRenderQuoteLine {
200
+ code: string
201
+ label: string
202
+ credits: number
203
+ }
204
+
205
+ /**
206
+ * The paired quote's answer.
207
+ *
208
+ * `maxCredits` is a CEILING, not a charge: quoting reserves nothing and spends
209
+ * nothing. `normalizedInputHash` is what run admission re-checks, so a body
210
+ * edited between quote and run is refused rather than executed at a price it
211
+ * was never quoted for.
212
+ */
213
+ export interface Pro3DRenderQuote {
214
+ quoteId: string
215
+ /** ISO-8601. After this the quote is stale and run answers "quote again". */
216
+ expiresAt: string
217
+ maxCredits: number
218
+ breakdown: Pro3DRenderQuoteLine[]
219
+ pricingVersion: string
220
+ capabilitiesVersion: string
221
+ normalizedInputHash: string
222
+ }
223
+
224
+ export const pro3DRenderQuoteSchema = z
225
+ .object({
226
+ quoteId: z.string().min(1),
227
+ expiresAt: z.string().min(1),
228
+ maxCredits: z.number(),
229
+ breakdown: z.array(
230
+ z.object({ code: z.string(), label: z.string(), credits: z.number() }).passthrough(),
231
+ ),
232
+ pricingVersion: z.string(),
233
+ capabilitiesVersion: z.string(),
234
+ normalizedInputHash: z.string().min(1),
235
+ })
236
+ .passthrough()
237
+
238
+ export function isPro3DRenderQuote(value: unknown): value is Pro3DRenderQuote {
239
+ return pro3DRenderQuoteSchema.safeParse(value).success
240
+ }
241
+
242
+ // ---------------------------------------------------------------------------
243
+ // Capabilities
244
+ // ---------------------------------------------------------------------------
245
+
246
+ /**
247
+ * What this deployment can actually serve.
248
+ *
249
+ * Every surface that offers a control reads it from here rather than from the
250
+ * vocabularies above: the constants say what the CONTRACT can express, this
251
+ * says what the INSTALLED engine will accept.
252
+ */
253
+ export interface Pro3DRenderCapabilities {
254
+ available: boolean
255
+ engines: Pro3DRenderEngine[]
256
+ qualityProfiles: Pro3DRenderQuality[]
257
+ styles: Pro3DRenderStyle[]
258
+ aspectRatios: Pro3DRenderAspectRatio[]
259
+ maxRepairPasses: number
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Result
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export interface Pro3DRenderValidationWarning {
267
+ code: string
268
+ message: string
269
+ shotId?: string
270
+ }
271
+
272
+ export interface Pro3DRenderResultMetadata {
273
+ width: number
274
+ height: number
275
+ fps: number
276
+ frames: number
277
+ duration: number
278
+ }
279
+
280
+ /**
281
+ * The completed job's `output_data`.
282
+ *
283
+ * `videoUrl` is the platform's existing resolved-video field (the contract's
284
+ * `resultUrl` mapped onto the envelope this platform already has), so the node
285
+ * connects to every existing video consumer without a second video result type
286
+ * producer validators cannot parse. `scenePlan` + `sceneRevisionId` are the
287
+ * exact revision that video was rendered from, so a later render-only re-run
288
+ * costs no authoring.
289
+ *
290
+ * Everything else is what the spec requires a caller to be able to act on: the
291
+ * poster to show before playback, the validation report to read warnings from,
292
+ * the renderer/metadata to check the export against a downstream model's
293
+ * limits, and the optional source artifact to offer as a download.
294
+ */
295
+ export interface Pro3DRenderJobOutput {
296
+ videoUrl: string
297
+ scenePlan: Scene3DPlan
298
+ sceneRevisionId: string
299
+ posterAssetId: string
300
+ /** Present when an editable native source was retained for this revision. */
301
+ sourceArtifactId?: string
302
+ validation: {
303
+ status: "passed"
304
+ reportAssetId: string
305
+ warnings: Pro3DRenderValidationWarning[]
306
+ }
307
+ renderer: string
308
+ metadata: Pro3DRenderResultMetadata
309
+ /** Short, user-safe note about what this revision contains. Never diagnostics. */
310
+ changeSummary?: string
311
+ }
312
+
313
+ /**
314
+ * Reader-side schema.
315
+ *
316
+ * Passthrough on purpose: a job row may carry additive metadata a client of
317
+ * this version has never heard of, and refusing the whole result over an
318
+ * unknown key would turn an additive server change into a client outage.
319
+ *
320
+ * The required fields are required because the contract makes them so — this
321
+ * is what a COMPLETE result looks like. Nothing in the platform fabricates
322
+ * them to satisfy the schema; a runtime that has not produced them yet simply
323
+ * does not parse as complete, which is the honest answer.
324
+ */
325
+ export const pro3DRenderJobOutputSchema = z
326
+ .object({
327
+ videoUrl: z.string().min(1),
328
+ scenePlan: scene3DAnyPlanSchema,
329
+ sceneRevisionId: z.string().min(1),
330
+ posterAssetId: z.string().min(1),
331
+ sourceArtifactId: z.string().min(1).optional(),
332
+ validation: z
333
+ .object({
334
+ status: z.literal("passed"),
335
+ reportAssetId: z.string().min(1),
336
+ warnings: z.array(
337
+ z
338
+ .object({
339
+ code: z.string(),
340
+ message: z.string(),
341
+ shotId: z.string().optional(),
342
+ })
343
+ .passthrough(),
344
+ ),
345
+ })
346
+ .passthrough(),
347
+ renderer: z.string().min(1),
348
+ metadata: z
349
+ .object({
350
+ width: z.number().int().positive(),
351
+ height: z.number().int().positive(),
352
+ fps: z.number().positive(),
353
+ frames: z.number().int().positive(),
354
+ duration: z.number().positive(),
355
+ })
356
+ .passthrough(),
357
+ changeSummary: z.string().optional(),
358
+ })
359
+ .passthrough()
360
+
361
+ export function isPro3DRenderJobOutput(value: unknown): value is Pro3DRenderJobOutput {
362
+ return pro3DRenderJobOutputSchema.safeParse(value).success
363
+ }
364
+
365
+ /**
366
+ * The two fields every EXECUTION SURFACE must be able to resolve, whatever
367
+ * else a runtime does or does not attach yet.
368
+ *
369
+ * Separate from the full reader above on purpose: canvas wiring, the DAG
370
+ * extractors and the render-only re-run need "is there a video and a scene
371
+ * here", and gating those on complete metadata would blank a node over a
372
+ * missing poster id.
373
+ */
374
+ export const pro3DRenderCoreOutputSchema = z
375
+ .object({
376
+ videoUrl: z.string().min(1),
377
+ scenePlan: scene3DAnyPlanSchema,
378
+ })
379
+ .passthrough()
380
+
381
+ // ---------------------------------------------------------------------------
382
+ // Source construction, shared by every execution surface
383
+ // ---------------------------------------------------------------------------
384
+
385
+ /** What a canvas node / DAG builder holds before it can name a source. */
386
+ export interface Pro3DRenderSourceInput {
387
+ /** `"scene"` selects the existing-revision path; anything else is a brief. */
388
+ sourceMode?: string
389
+ /** The brief, already resolved and affix-applied by the caller. */
390
+ prompt?: string
391
+ references?: readonly Scene3DReference[]
392
+ /** The revision to export or edit, and the run that produced it. */
393
+ revisionId?: string
394
+ sourceJobId?: string
395
+ /** Absent/blank keeps the render-only path. */
396
+ editPrompt?: string
397
+ }
398
+
399
+ export type Pro3DRenderSourceResult =
400
+ | { ok: true; source: Pro3DRenderSource }
401
+ | { ok: false; message: string }
402
+
403
+ /**
404
+ * Turn node/DAG state into the wire `source`.
405
+ *
406
+ * Shared by BOTH execution engines because the alternative — one copy in the
407
+ * browser executor and one in the orchestrator — is the drift that lets a
408
+ * canvas run and a headless run of the same node mean different things. The
409
+ * refusals are part of that: a scene source missing its correlation must fail
410
+ * the same way on both.
411
+ *
412
+ * A blank `editPrompt` is treated as ABSENT, never as an empty instruction: a
413
+ * user who cleared the box asked for a plain export, and forwarding `""` would
414
+ * buy them an authoring pass.
415
+ */
416
+ export function buildPro3DRenderSource(input: Pro3DRenderSourceInput): Pro3DRenderSourceResult {
417
+ if (input.sourceMode === "scene") {
418
+ const revisionId = input.revisionId?.trim()
419
+ const sourceJobId = input.sourceJobId?.trim()
420
+ if (!revisionId) {
421
+ return { ok: false, message: "no scene to render — wire a 3D scene in, or run this node once." }
422
+ }
423
+ const editPrompt = input.editPrompt?.trim()
424
+ return {
425
+ ok: true,
426
+ source: { kind: "scene", revisionId, ...(sourceJobId ? { sourceJobId } : {}), ...(editPrompt ? { editPrompt } : {}) },
427
+ }
428
+ }
429
+ const prompt = input.prompt?.trim()
430
+ if (!prompt) {
431
+ return { ok: false, message: "no brief — describe the scene, or wire a prompt in." }
432
+ }
433
+ const references = input.references ?? []
434
+ return {
435
+ ok: true,
436
+ source: { kind: "prompt", prompt, ...(references.length > 0 ? { references } : {}) },
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Which timing fields a request may carry.
442
+ *
443
+ * A `scene` source already HAS timing, and the contract forbids silently
444
+ * overriding it — so the node's own duration/fps/aspect are withheld unless
445
+ * the user explicitly asked to re-time, in which case they are sent and the
446
+ * engine decides whether the change is compatible. For a new scene the node's
447
+ * settings simply are the request.
448
+ *
449
+ * Returning an object with the keys omitted (rather than set to `undefined`)
450
+ * matters: these bodies are JSON-serialized, and an explicit `undefined` and a
451
+ * missing key are the same on the wire only by luck of the serializer.
452
+ */
453
+ export function pro3DRenderTimingOverrides(input: {
454
+ source: Pro3DRenderSource
455
+ overrideSourceTiming?: boolean
456
+ durationSeconds?: number
457
+ fps?: number
458
+ aspectRatio?: string
459
+ }): { durationSeconds?: number; fps?: number; aspectRatio?: string } {
460
+ if (input.source.kind === "scene" && !input.overrideSourceTiming) return {}
461
+ const out: { durationSeconds?: number; fps?: number; aspectRatio?: string } = {}
462
+ if (typeof input.durationSeconds === "number") out.durationSeconds = input.durationSeconds
463
+ if (typeof input.fps === "number") out.fps = input.fps
464
+ if (typeof input.aspectRatio === "string") out.aspectRatio = input.aspectRatio
465
+ return out
466
+ }
@@ -89,6 +89,11 @@ export const VIDEO_PRODUCER_TYPES: ReadonlySet<string> = new Set([
89
89
  // Emits generatedVideoUrl so it connects to any downstream video consumer
90
90
  // (e.g. a Seedance video-reference input) by an ordinary edge.
91
91
  "gif-to-video",
92
+ // 3D Render Pro: authors a scene AND exports it in one operation, settling
93
+ // with the standard `videoUrl` field. It is a video producer as much as it
94
+ // is a composition producer — omitting it here is the "cannot connect the
95
+ // outputs" bug, and its `composition` handle is typed separately.
96
+ "pro-3d-render",
92
97
  ])
93
98
 
94
99
  /**