@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.
- package/dist/index.cjs +1776 -159
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2921 -73
- package/dist/index.d.ts +2921 -73
- package/dist/index.js +1665 -160
- 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-camera-track.test.ts +235 -0
- package/src/__tests__/scene3d-v2-edit.test.ts +83 -0
- package/src/__tests__/scene3d-v2-fixtures.ts +232 -0
- package/src/__tests__/scene3d-v2-resources.test.ts +239 -0
- package/src/__tests__/scene3d-v2.test.ts +742 -0
- package/src/__tests__/scene3d.test.ts +6 -6
- package/src/index.ts +9 -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-camera-track.ts +369 -0
- package/src/scene3d-edit.ts +10 -10
- package/src/scene3d-v2-edit.ts +156 -0
- package/src/scene3d-v2-plan.ts +694 -0
- package/src/scene3d-v2-resources.ts +382 -0
- package/src/scene3d-v2.ts +666 -0
- package/src/scene3d.ts +39 -13
|
@@ -14,14 +14,14 @@ import {
|
|
|
14
14
|
SCENE3D_PLAN_TYPE,
|
|
15
15
|
SCENE3D_SCHEMA_VERSION,
|
|
16
16
|
applyScene3DEditOperations,
|
|
17
|
-
|
|
17
|
+
isScene3DPlanV1,
|
|
18
18
|
newScene3DRevisionId,
|
|
19
19
|
scene3DDeepEqual,
|
|
20
20
|
scene3DEditOperationSchema,
|
|
21
21
|
scene3DPlanSchema,
|
|
22
22
|
summarizeScene3DOperations,
|
|
23
23
|
type Scene3DObject,
|
|
24
|
-
type
|
|
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<
|
|
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(
|
|
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:
|
|
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
|
|
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"
|
package/src/model-constants.ts
CHANGED
|
@@ -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) ||
|
|
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
|
-
/**
|
|
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
|
+
}
|
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
|
/**
|