@nodaro/shared 3.0.0 → 3.2.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.
- package/dist/index.cjs +398 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +995 -2
- package/dist/index.d.ts +995 -2
- package/dist/index.js +368 -152
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/presentation-utils.test.ts +97 -0
- package/src/__tests__/scene3d-authoring-engine.test.ts +104 -0
- package/src/__tests__/scene3d-base-visibility.test.ts +36 -0
- package/src/index.ts +3 -0
- package/src/model-constants.ts +10 -0
- package/src/node-mappable-fields.ts +1 -0
- package/src/presentation-utils.ts +54 -2
- package/src/pro-3d-render.ts +466 -0
- package/src/producer-types.ts +5 -0
- package/src/scene3d-authoring-engine.ts +215 -0
- package/src/scene3d-v2.ts +3 -0
|
@@ -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
|
+
}
|
package/src/producer-types.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHICH authoring lane a Generate/Edit 3D Scene run goes down.
|
|
3
|
+
*
|
|
4
|
+
* There are two, and they are not interchangeable. Basic is the v1 LLM
|
|
5
|
+
* authoring path the platform has always had; Advanced is the installed
|
|
6
|
+
* private engine (`blender-cloud` / `blender-local`) that authors and edits
|
|
7
|
+
* schema-v2 scenes. The wire difference is one field — an `engine` on the body
|
|
8
|
+
* makes `POST /v1/3d-scene/{generate,edit}` hand the request to the private
|
|
9
|
+
* engine instead of the Basic guard.
|
|
10
|
+
*
|
|
11
|
+
* Deciding that is NOT a per-surface choice. The canvas executor and the
|
|
12
|
+
* headless orchestrator run the same nodes, and a node that means "Advanced"
|
|
13
|
+
* in the browser and "Basic" in a scheduled run is a scene authored by a
|
|
14
|
+
* different engine depending on who pressed Run. So the decision lives here,
|
|
15
|
+
* once, and both callers spread the SAME `fields` onto their request body.
|
|
16
|
+
*
|
|
17
|
+
* Three refusals, each of which used to be a silent wrong answer:
|
|
18
|
+
*
|
|
19
|
+
* - a **v2 plan on the Basic lane** is refused, never downgraded. Basic parses
|
|
20
|
+
* `scene3DPlanV1Schema`, so a v2 scene reaches it as a wall of Zod issues
|
|
21
|
+
* (the "Pro composition → Edit 3D Scene 400s" report) — and if it ever did
|
|
22
|
+
* parse, it would author from a scene it cannot represent.
|
|
23
|
+
* - an **explicitly requested engine this install does not have** is refused
|
|
24
|
+
* rather than quietly becoming Basic. Falling back would charge the user for
|
|
25
|
+
* a different pipeline than the one they picked.
|
|
26
|
+
* - an **unknown engine name** is refused before anything is spent.
|
|
27
|
+
*/
|
|
28
|
+
import {
|
|
29
|
+
SCENE3D_SCHEMA_VERSION,
|
|
30
|
+
} from "./scene3d.js"
|
|
31
|
+
import {
|
|
32
|
+
SCENE3D_SCHEMA_VERSION_V2,
|
|
33
|
+
SCENE3D_SUPPORTED_SCHEMA_VERSIONS,
|
|
34
|
+
SCENE3D_V2_ENGINES,
|
|
35
|
+
type Scene3DKnownEngine,
|
|
36
|
+
} from "./scene3d-v2.js"
|
|
37
|
+
import { isKnownScene3DEngine, scene3DPlanSchemaVersion } from "./scene3d-v2-plan.js"
|
|
38
|
+
|
|
39
|
+
/** The value that names the Basic lane explicitly. Absent means the same. */
|
|
40
|
+
export const SCENE3D_BASIC_ENGINE = "basic"
|
|
41
|
+
|
|
42
|
+
/** Everything a caller may put in `engine` on a Generate/Edit request. */
|
|
43
|
+
export const SCENE3D_AUTHORING_ENGINES = [SCENE3D_BASIC_ENGINE, ...SCENE3D_V2_ENGINES] as const
|
|
44
|
+
export type Scene3DAuthoringEngine = (typeof SCENE3D_AUTHORING_ENGINES)[number]
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The engine an Advanced run picks when nothing else names one.
|
|
48
|
+
*
|
|
49
|
+
* Hosted cloud, because that is the contract's default lane; `blender-local`
|
|
50
|
+
* is never inferred — it needs a paired desktop and its own deployment flag,
|
|
51
|
+
* so it is only ever used when it was explicitly asked for or when the scene
|
|
52
|
+
* under edit was authored by it and this install still offers it.
|
|
53
|
+
*/
|
|
54
|
+
export const SCENE3D_DEFAULT_ADVANCED_ENGINE: Scene3DKnownEngine = "blender-cloud"
|
|
55
|
+
|
|
56
|
+
export function isScene3DAuthoringEngine(value: unknown): value is Scene3DAuthoringEngine {
|
|
57
|
+
return typeof value === "string" && (SCENE3D_AUTHORING_ENGINES as readonly string[]).includes(value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface Scene3DEngineChoiceInput {
|
|
61
|
+
/** The node's/caller's explicit selection. `undefined` = "not chosen". */
|
|
62
|
+
requested?: string | null
|
|
63
|
+
/**
|
|
64
|
+
* The plan the run edits, for an edit. Omit for generate.
|
|
65
|
+
*
|
|
66
|
+
* The raw plan rather than a version number on purpose: the caller already
|
|
67
|
+
* holds it, and reading the version here is the ONE place the "v2 never goes
|
|
68
|
+
* to Basic" rule can be enforced for every surface at once.
|
|
69
|
+
*/
|
|
70
|
+
plan?: unknown
|
|
71
|
+
/**
|
|
72
|
+
* Advanced engines this install can actually serve, from
|
|
73
|
+
* `GET /v1/3d-scene/capabilities`.
|
|
74
|
+
*
|
|
75
|
+
* `undefined` means NOT KNOWN (the headless orchestrator never asks, and the
|
|
76
|
+
* browser has not had the answer back yet) — which is different from "none".
|
|
77
|
+
* Unknown proceeds and lets the route refuse honestly with
|
|
78
|
+
* `SCENE_CAPABILITY_UNAVAILABLE`; a known-empty list refuses here, before a
|
|
79
|
+
* request that cannot succeed is sent.
|
|
80
|
+
*/
|
|
81
|
+
availableEngines?: readonly string[] | undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The extra body fields an Advanced request carries. Empty on Basic, so the
|
|
85
|
+
* Basic request stays byte-identical to what it has always been. */
|
|
86
|
+
export interface Scene3DEngineRequestFields {
|
|
87
|
+
engine?: Scene3DKnownEngine
|
|
88
|
+
/**
|
|
89
|
+
* Which scene schema versions the CALLER can read back.
|
|
90
|
+
*
|
|
91
|
+
* Contract §5: an advanced authoring request declares this so the engine
|
|
92
|
+
* never answers with a revision the caller cannot render. Both of our
|
|
93
|
+
* surfaces read v1 and v2, so both send the same list.
|
|
94
|
+
*/
|
|
95
|
+
acceptedSceneSchemaVersions?: number[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export type Scene3DEngineChoiceRefusalCode =
|
|
99
|
+
/** The name is not an engine this contract knows. */
|
|
100
|
+
| "unknown_engine"
|
|
101
|
+
/** Explicitly asked for an engine this install does not serve. */
|
|
102
|
+
| "engine_unavailable"
|
|
103
|
+
/** A v2 scene was pointed at the Basic lane. */
|
|
104
|
+
| "schema_requires_advanced"
|
|
105
|
+
/** The scene claims a version nothing here can author against. */
|
|
106
|
+
| "unsupported_schema_version"
|
|
107
|
+
/** v2 scene, and no Advanced engine installed at all. */
|
|
108
|
+
| "advanced_unavailable"
|
|
109
|
+
|
|
110
|
+
export type Scene3DEngineChoice =
|
|
111
|
+
| { ok: true; lane: "basic"; engine: undefined; fields: Scene3DEngineRequestFields }
|
|
112
|
+
| { ok: true; lane: "advanced"; engine: Scene3DKnownEngine; fields: Scene3DEngineRequestFields }
|
|
113
|
+
| { ok: false; code: Scene3DEngineChoiceRefusalCode; message: string }
|
|
114
|
+
|
|
115
|
+
function advancedFields(engine: Scene3DKnownEngine): Scene3DEngineRequestFields {
|
|
116
|
+
return { engine, acceptedSceneSchemaVersions: [...SCENE3D_SUPPORTED_SCHEMA_VERSIONS] }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** `undefined` (unknown) is permissive; a known list is authoritative. */
|
|
120
|
+
function serves(available: readonly string[] | undefined, engine: string): boolean {
|
|
121
|
+
return available === undefined || available.includes(engine)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The engine recorded on a v2 plan's provenance, when it is one we know. */
|
|
125
|
+
function planAuthoringEngine(plan: unknown): Scene3DKnownEngine | undefined {
|
|
126
|
+
const provenance = (plan as { provenance?: { engine?: unknown } } | null | undefined)?.provenance
|
|
127
|
+
const engine = provenance?.engine
|
|
128
|
+
return typeof engine === "string" && isKnownScene3DEngine(engine) ? engine : undefined
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the lane, or refuse with a sentence the user can act on.
|
|
133
|
+
*
|
|
134
|
+
* Pure and synchronous: every caller already holds the three inputs, and the
|
|
135
|
+
* answer must be identical on the canvas and in the orchestrator.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveScene3DAuthoringEngine(input: Scene3DEngineChoiceInput): Scene3DEngineChoice {
|
|
138
|
+
const requested = typeof input.requested === "string" && input.requested.trim() !== ""
|
|
139
|
+
? input.requested.trim()
|
|
140
|
+
: undefined
|
|
141
|
+
if (requested !== undefined && !isScene3DAuthoringEngine(requested)) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
code: "unknown_engine",
|
|
145
|
+
message: `"${requested}" is not a 3D authoring engine — choose Basic, or an advanced engine this install offers.`,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// `plan` absent = generate. `null` version = not a Scene3D plan at all, which
|
|
150
|
+
// is the caller's own error to report (they need the plan for other reasons
|
|
151
|
+
// too); this resolver only speaks to versions it can read.
|
|
152
|
+
const version = input.plan === undefined ? undefined : scene3DPlanSchemaVersion(input.plan)
|
|
153
|
+
if (version !== undefined && version !== null && !(SCENE3D_SUPPORTED_SCHEMA_VERSIONS as readonly number[]).includes(version)) {
|
|
154
|
+
return {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: "unsupported_schema_version",
|
|
157
|
+
message: `This scene uses schema version ${version}, which this version of Nodaro cannot edit.`,
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const isV2 = version === SCENE3D_SCHEMA_VERSION_V2
|
|
161
|
+
|
|
162
|
+
if (isV2) {
|
|
163
|
+
if (requested === SCENE3D_BASIC_ENGINE) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
code: "schema_requires_advanced",
|
|
167
|
+
message:
|
|
168
|
+
"This scene was authored by an advanced engine (schema v2) and cannot be edited on the Basic engine — switch this node's engine to the advanced one.",
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// An EXPLICIT choice is never silently swapped for another engine — it is
|
|
172
|
+
// refused, exactly like an explicit unavailable engine on a v1 scene.
|
|
173
|
+
if (requested !== undefined) {
|
|
174
|
+
return serves(input.availableEngines, requested)
|
|
175
|
+
? { ok: true, lane: "advanced", engine: requested, fields: advancedFields(requested) }
|
|
176
|
+
: unavailable(requested)
|
|
177
|
+
}
|
|
178
|
+
// Nothing was picked. Preference order: the engine that AUTHORED the scene
|
|
179
|
+
// (an edit stays on its own engine unless told otherwise), then the hosted
|
|
180
|
+
// default.
|
|
181
|
+
const preferred: Scene3DKnownEngine[] = []
|
|
182
|
+
const authored = planAuthoringEngine(input.plan)
|
|
183
|
+
if (authored) preferred.push(authored)
|
|
184
|
+
if (!preferred.includes(SCENE3D_DEFAULT_ADVANCED_ENGINE)) preferred.push(SCENE3D_DEFAULT_ADVANCED_ENGINE)
|
|
185
|
+
const engine = preferred.find((candidate) => serves(input.availableEngines, candidate))
|
|
186
|
+
if (!engine) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
code: "advanced_unavailable",
|
|
190
|
+
message:
|
|
191
|
+
"This scene needs an advanced 3D engine to edit, and this install does not have one available.",
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { ok: true, lane: "advanced", engine, fields: advancedFields(engine) }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// v1, or a generate with no plan at all.
|
|
198
|
+
if (requested === undefined || requested === SCENE3D_BASIC_ENGINE) {
|
|
199
|
+
return { ok: true, lane: "basic", engine: undefined, fields: {} }
|
|
200
|
+
}
|
|
201
|
+
const engine = requested as Scene3DKnownEngine
|
|
202
|
+
if (!serves(input.availableEngines, engine)) return unavailable(engine)
|
|
203
|
+
return { ok: true, lane: "advanced", engine, fields: advancedFields(engine) }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function unavailable(engine: string): Scene3DEngineChoice {
|
|
207
|
+
return {
|
|
208
|
+
ok: false,
|
|
209
|
+
code: "engine_unavailable",
|
|
210
|
+
message: `The "${engine}" 3D authoring engine is not available on this install.`,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** v1's version constant, re-exported for callers narrowing a plan by hand. */
|
|
215
|
+
export const SCENE3D_BASIC_SCHEMA_VERSION = SCENE3D_SCHEMA_VERSION
|