@nodaro/shared 3.2.0 → 3.4.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 +59 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +53 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/scene3d-input-assets.test.ts +31 -0
- package/src/__tests__/sequence-execution.test.ts +20 -0
- package/src/index.ts +2 -0
- package/src/pro-3d-render.ts +7 -1
- package/src/scene3d-input-assets.ts +27 -0
- package/src/sequence-execution.ts +24 -0
package/package.json
CHANGED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { scene3DInputAssetsForEngine, scene3DInputAssetsSchema } from "../scene3d-input-assets.js"
|
|
3
|
+
import { buildPro3DRenderSource } from "../pro-3d-render.js"
|
|
4
|
+
|
|
5
|
+
const asset = { id: "vehicle", revisionId: "00000000-0000-4000-8000-000000000010",
|
|
6
|
+
assetId: "00000000-0000-4000-8000-000000000011", label: "Vehicle" }
|
|
7
|
+
|
|
8
|
+
describe("immutable scene input selectors", () => {
|
|
9
|
+
it("requires unique bounded selectors and excludes receipts and URLs", () => {
|
|
10
|
+
expect(scene3DInputAssetsSchema.parse([asset])).toEqual([asset])
|
|
11
|
+
for (const value of [[{ ...asset, url: "https://example.com/model.glb" }], [{ ...asset, sha256: "a".repeat(64) }],
|
|
12
|
+
[{ ...asset, revisionId: "../other" }], [{ ...asset, label: "bad\nlabel" }],
|
|
13
|
+
[asset, { ...asset, id: "another" }], [asset, { ...asset, assetId: asset.revisionId }], Array(9).fill(asset)]) {
|
|
14
|
+
expect(scene3DInputAssetsSchema.safeParse(value).success).toBe(false)
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
it("refuses Basic imports instead of silently ignoring geometry", () => {
|
|
18
|
+
for (const engine of [undefined, "basic", "unknown"]) {
|
|
19
|
+
expect(() => scene3DInputAssetsForEngine([asset], engine)).toThrow("advanced scene engine")
|
|
20
|
+
}
|
|
21
|
+
expect(scene3DInputAssetsForEngine(undefined, undefined)).toEqual([])
|
|
22
|
+
expect(scene3DInputAssetsForEngine([asset], "blender-cloud")).toEqual([asset])
|
|
23
|
+
})
|
|
24
|
+
it("carries inputs on new Pro scenes while export keeps its existing revision", () => {
|
|
25
|
+
expect(buildPro3DRenderSource({ prompt: "Drive past the camera", inputAssets: [asset] }))
|
|
26
|
+
.toEqual({ ok: true, source: { kind: "prompt", prompt: "Drive past the camera", inputAssets: [asset] } })
|
|
27
|
+
expect(buildPro3DRenderSource({ sourceMode: "scene", revisionId: asset.revisionId, inputAssets: [asset] }))
|
|
28
|
+
.toEqual({ ok: true, source: { kind: "scene", revisionId: asset.revisionId } })
|
|
29
|
+
expect(buildPro3DRenderSource({ prompt: "Drive", inputAssets: [{ ...asset, url: "https://example.com" } as never] }).ok).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { assertCanvasExecutionAllowed, requiresSequenceExecution, STUDIO_DEPENDENT_FRAMES_CAPABILITY } from "../sequence-execution"
|
|
3
|
+
|
|
4
|
+
describe("dependency execution boundary", () => {
|
|
5
|
+
it.each([
|
|
6
|
+
{ requiredCapabilities: [STUDIO_DEPENDENT_FRAMES_CAPABILITY] },
|
|
7
|
+
{ keyframeId: "A" }, { sequenceBinding: { startKeyframeId: "A", endKeyframeId: "B" } },
|
|
8
|
+
{ keyframeId: null }, { sequenceBinding: null },
|
|
9
|
+
])("refuses declared or recognizable dependency nodes: %j", (data) => {
|
|
10
|
+
expect(() => assertCanvasExecutionAllowed([{ id: "linked", data }])).toThrow(expect.objectContaining({
|
|
11
|
+
code: "sequence_execution_required", nodeIds: ["linked"],
|
|
12
|
+
}))
|
|
13
|
+
})
|
|
14
|
+
it("keeps ordinary nodes runnable and reports only the blocked nodes", () => {
|
|
15
|
+
expect(requiresSequenceExecution({ data: { provider: "wan-3", imageUrl: "start", endFrameUrl: "end" } })).toBe(false)
|
|
16
|
+
expect(() => assertCanvasExecutionAllowed([{ id: "ordinary", data: {} }])).not.toThrow()
|
|
17
|
+
expect(() => assertCanvasExecutionAllowed([{ id: "ordinary", data: {} }, { id: "linked", data: { keyframeId: "A" } }]))
|
|
18
|
+
.toThrow(expect.objectContaining({ nodeIds: ["linked"] }))
|
|
19
|
+
})
|
|
20
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1072,4 +1072,6 @@ export {
|
|
|
1072
1072
|
} from "./studio-transient.js"
|
|
1073
1073
|
|
|
1074
1074
|
export * from "./scene3d-v2-edit.js"
|
|
1075
|
+
export { STUDIO_DEPENDENT_FRAMES_CAPABILITY, SequenceExecutionRequiredError, requiresSequenceExecution, assertCanvasExecutionAllowed } from "./sequence-execution"
|
|
1075
1076
|
export * from "./scene3d-authoring-engine.js"
|
|
1077
|
+
export * from "./scene3d-input-assets.js"
|
package/src/pro-3d-render.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import { z } from "zod"
|
|
35
35
|
import { SCENE3D_LIMITS, type Scene3DReference } from "./scene3d.js"
|
|
36
36
|
import { scene3DAnyPlanSchema, type Scene3DPlan } from "./scene3d-v2-plan.js"
|
|
37
|
+
import { scene3DInputAssetsSchema, type Scene3DInputAsset } from "./scene3d-input-assets.js"
|
|
37
38
|
|
|
38
39
|
/** Canvas/API/MCP node type. */
|
|
39
40
|
export const PRO3D_RENDER_NODE_TYPE = "pro-3d-render"
|
|
@@ -140,6 +141,7 @@ export interface Pro3DRenderPromptSource {
|
|
|
140
141
|
kind: "prompt"
|
|
141
142
|
prompt: string
|
|
142
143
|
references?: readonly Scene3DReference[]
|
|
144
|
+
inputAssets?: readonly Scene3DInputAsset[]
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
/**
|
|
@@ -384,6 +386,7 @@ export const pro3DRenderCoreOutputSchema = z
|
|
|
384
386
|
|
|
385
387
|
/** What a canvas node / DAG builder holds before it can name a source. */
|
|
386
388
|
export interface Pro3DRenderSourceInput {
|
|
389
|
+
inputAssets?: readonly Scene3DInputAsset[]
|
|
387
390
|
/** `"scene"` selects the existing-revision path; anything else is a brief. */
|
|
388
391
|
sourceMode?: string
|
|
389
392
|
/** The brief, already resolved and affix-applied by the caller. */
|
|
@@ -431,9 +434,12 @@ export function buildPro3DRenderSource(input: Pro3DRenderSourceInput): Pro3DRend
|
|
|
431
434
|
return { ok: false, message: "no brief — describe the scene, or wire a prompt in." }
|
|
432
435
|
}
|
|
433
436
|
const references = input.references ?? []
|
|
437
|
+
const parsedAssets = scene3DInputAssetsSchema.safeParse(input.inputAssets ?? [])
|
|
438
|
+
if (!parsedAssets.success) return { ok: false, message: "invalid scene input assets" }
|
|
434
439
|
return {
|
|
435
440
|
ok: true,
|
|
436
|
-
source: { kind: "prompt", prompt, ...(references.length > 0 ? { references } : {})
|
|
441
|
+
source: { kind: "prompt", prompt, ...(references.length > 0 ? { references } : {}),
|
|
442
|
+
...(parsedAssets.data.length ? { inputAssets: parsedAssets.data } : {}) },
|
|
437
443
|
}
|
|
438
444
|
}
|
|
439
445
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
|
|
3
|
+
/** Immutable 3D inputs are selected by revision and artifact, never by URL or caller receipt. */
|
|
4
|
+
export const scene3DInputAssetSchema = z.object({
|
|
5
|
+
id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,63}$/),
|
|
6
|
+
revisionId: z.uuid(),
|
|
7
|
+
assetId: z.uuid(),
|
|
8
|
+
label: z.string().min(1).max(64).regex(/^[^\u0000-\u001f]+$/).optional(),
|
|
9
|
+
}).strict()
|
|
10
|
+
|
|
11
|
+
export const scene3DInputAssetsSchema = z.array(scene3DInputAssetSchema).max(8).superRefine((values, ctx) => {
|
|
12
|
+
if (new Set(values.map(value => value.id)).size !== values.length ||
|
|
13
|
+
new Set(values.map(value => value.assetId)).size !== values.length) {
|
|
14
|
+
ctx.addIssue({ code: "custom", message: "Scene input assets must have unique identities" })
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
export type Scene3DInputAsset = z.infer<typeof scene3DInputAssetSchema>
|
|
19
|
+
|
|
20
|
+
/** Both workflow engines refuse unsupported imports before starting a paid Basic run. */
|
|
21
|
+
export function scene3DInputAssetsForEngine(value: unknown, engine: string | undefined): Scene3DInputAsset[] {
|
|
22
|
+
const assets = scene3DInputAssetsSchema.parse(value ?? [])
|
|
23
|
+
if (assets.length && engine !== "blender-cloud" && engine !== "blender-local") {
|
|
24
|
+
throw new Error("Imported 3D assets require an advanced scene engine")
|
|
25
|
+
}
|
|
26
|
+
return assets
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Public workflow capability marker; creative planning stays in the Studio codec. */
|
|
2
|
+
export const STUDIO_DEPENDENT_FRAMES_CAPABILITY = "studio-dependent-frames-v1"
|
|
3
|
+
|
|
4
|
+
export class SequenceExecutionRequiredError extends Error {
|
|
5
|
+
readonly code = "sequence_execution_required"
|
|
6
|
+
readonly statusCode = 400
|
|
7
|
+
constructor(readonly nodeIds: readonly string[]) {
|
|
8
|
+
super("Generate linked frames and clips in Studio or through its production API so the reviewed images are used.")
|
|
9
|
+
this.name = "SequenceExecutionRequiredError"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** A missing declaration must not make a recognizable dependency node runnable. */
|
|
14
|
+
export function requiresSequenceExecution(node: { data?: unknown }): boolean {
|
|
15
|
+
if (!node.data || typeof node.data !== "object" || Array.isArray(node.data)) return false
|
|
16
|
+
const data = node.data as Record<string, unknown>
|
|
17
|
+
return data.keyframeId !== undefined || data.sequenceBinding !== undefined
|
|
18
|
+
|| (Array.isArray(data.requiredCapabilities) && data.requiredCapabilities.includes(STUDIO_DEPENDENT_FRAMES_CAPABILITY))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function assertCanvasExecutionAllowed(nodes: readonly { id: string; data?: unknown }[]): void {
|
|
22
|
+
const blocked = nodes.filter(requiresSequenceExecution).map((node) => node.id)
|
|
23
|
+
if (blocked.length) throw new SequenceExecutionRequiredError(blocked)
|
|
24
|
+
}
|