@nodaro/shared 3.0.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 +397 -151
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +987 -2
- package/dist/index.d.ts +987 -2
- package/dist/index.js +367 -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/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/package.json
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
getItemSortId,
|
|
13
13
|
cleanOrphanedItems,
|
|
14
14
|
} from "../presentation-utils.js"
|
|
15
|
+
import { AUDIO_PRODUCER_TYPES, VIDEO_PRODUCER_TYPES } from "../producer-types.js"
|
|
15
16
|
import type { GenericNode, GenericEdge } from "../types.js"
|
|
16
17
|
import type { PresentationItem } from "../presentation-types.js"
|
|
17
18
|
|
|
@@ -1135,3 +1136,99 @@ describe("presentation-utils — action-fx", () => {
|
|
|
1135
1136
|
expect(getInputFieldSchema("action-fx")).toEqual({ key: "actionFx", type: "select" })
|
|
1136
1137
|
})
|
|
1137
1138
|
})
|
|
1139
|
+
|
|
1140
|
+
// ---------------------------------------------------------------------------
|
|
1141
|
+
// Canonical output typing — the presentation classifier vs the producer sets
|
|
1142
|
+
// ---------------------------------------------------------------------------
|
|
1143
|
+
|
|
1144
|
+
/**
|
|
1145
|
+
* `getOutputType` is what a published app renders from and what `/v1` app
|
|
1146
|
+
* schemas and run results declare (`routes/api-tokens.ts`). A node it calls
|
|
1147
|
+
* `"data"` is shown as a JSON blob instead of a player, is skipped by the
|
|
1148
|
+
* lightbox media list, and is announced to API consumers with the wrong type.
|
|
1149
|
+
*
|
|
1150
|
+
* The literal sets in the module are the classifier's own opinion and win; the
|
|
1151
|
+
* tail is derived from the producer vocabularies the canvas validators already
|
|
1152
|
+
* maintain, so a node cannot be a declared video producer and be typed `"data"`
|
|
1153
|
+
* at the same time.
|
|
1154
|
+
*/
|
|
1155
|
+
describe("getOutputType — derived from the producer vocabularies", () => {
|
|
1156
|
+
it("types 3D Render Pro as video (it settles with the standard videoUrl)", () => {
|
|
1157
|
+
expect(getOutputType("pro-3d-render")).toBe("video")
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it("types every declared video producer as video", () => {
|
|
1161
|
+
for (const t of VIDEO_PRODUCER_TYPES) expect([t, getOutputType(t)]).toEqual([t, "video"])
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
it("types every declared audio producer as audio, except the dual-mode ones the literal sets claim", () => {
|
|
1165
|
+
for (const t of AUDIO_PRODUCER_TYPES) {
|
|
1166
|
+
// voice-changer / voice-changer-pro / dubbing can emit video; the
|
|
1167
|
+
// classifier's own list still calls them audio, which is their default.
|
|
1168
|
+
expect([t, getOutputType(t)]).toEqual([t, "audio"])
|
|
1169
|
+
}
|
|
1170
|
+
})
|
|
1171
|
+
|
|
1172
|
+
it("leaves a run-time-decided producer as data — there is no honest static answer", () => {
|
|
1173
|
+
expect(getOutputType("list")).toBe("data")
|
|
1174
|
+
expect(getOutputType("sub-workflow")).toBe("data")
|
|
1175
|
+
expect(getOutputType("split-media")).toBe("data")
|
|
1176
|
+
})
|
|
1177
|
+
|
|
1178
|
+
it("keeps the literal sets authoritative where they and the producer sets disagree", () => {
|
|
1179
|
+
// adjust-volume is in AUDIO_PRODUCER_TYPES and DYNAMIC_PRODUCER_TYPES;
|
|
1180
|
+
// upload-video is a video producer AND an input node.
|
|
1181
|
+
expect(getOutputType("adjust-volume")).toBe("audio")
|
|
1182
|
+
expect(getOutputType("upload-video")).toBe("video")
|
|
1183
|
+
expect(getOutputType("text-prompt")).toBe("text")
|
|
1184
|
+
})
|
|
1185
|
+
})
|
|
1186
|
+
|
|
1187
|
+
describe("published-app / API-token output mapping for 3D Render Pro", () => {
|
|
1188
|
+
const proNode = mkNode("pro", "pro-3d-render", { presentationOutput: true, label: "Render" })
|
|
1189
|
+
|
|
1190
|
+
it("declares the app-schema output type the SDK and MCP read (mirrors routes/api-tokens.ts)", () => {
|
|
1191
|
+
// The route builds `{ nodeId, label, type: getOutputType(node.type) }` for
|
|
1192
|
+
// both the app schema and the run result.
|
|
1193
|
+
const outputs = getOutputNodes([proNode], []).map((n) => ({
|
|
1194
|
+
nodeId: n.id,
|
|
1195
|
+
type: getOutputType(n.type),
|
|
1196
|
+
}))
|
|
1197
|
+
expect(outputs).toEqual([{ nodeId: "pro", type: "video" }])
|
|
1198
|
+
})
|
|
1199
|
+
|
|
1200
|
+
it("is an OUTPUT, not an input — the picker must not offer it as an app input", () => {
|
|
1201
|
+
expect(INPUT_NODE_TYPES.has("pro-3d-render")).toBe(false)
|
|
1202
|
+
expect(getInputNodes([mkNode("pro", "pro-3d-render", { presentationVisible: true })])).toEqual([])
|
|
1203
|
+
})
|
|
1204
|
+
})
|
|
1205
|
+
|
|
1206
|
+
describe("legacy presentationVisible rules for a media producer", () => {
|
|
1207
|
+
it("stays an output when its composition ALSO feeds something downstream", () => {
|
|
1208
|
+
// The Pro node's `composition` handle commonly feeds an editor or a
|
|
1209
|
+
// re-render; the legacy rule drops a node with an outgoing edge unless it
|
|
1210
|
+
// produces media of its own.
|
|
1211
|
+
const nodes = [
|
|
1212
|
+
mkNode("pro", "pro-3d-render", { presentationVisible: true }),
|
|
1213
|
+
mkNode("edit", "edit-3d-scene", {}),
|
|
1214
|
+
]
|
|
1215
|
+
const out = getOutputNodes(nodes, [mkEdge("pro", "edit")])
|
|
1216
|
+
expect(out.map((n) => n.id)).toEqual(["pro"])
|
|
1217
|
+
})
|
|
1218
|
+
|
|
1219
|
+
it("applies the same rule to the other video producers the literal list forgot", () => {
|
|
1220
|
+
const nodes = [
|
|
1221
|
+
mkNode("gv", "generate-video", { presentationVisible: true }),
|
|
1222
|
+
mkNode("cap", "add-captions", {}),
|
|
1223
|
+
]
|
|
1224
|
+
expect(getOutputNodes(nodes, [mkEdge("gv", "cap")]).map((n) => n.id)).toEqual(["gv"])
|
|
1225
|
+
})
|
|
1226
|
+
|
|
1227
|
+
it("still drops a non-media node that feeds something downstream", () => {
|
|
1228
|
+
const nodes = [
|
|
1229
|
+
mkNode("txt", "text-prompt", { presentationVisible: true }),
|
|
1230
|
+
mkNode("gv", "generate-video", {}),
|
|
1231
|
+
]
|
|
1232
|
+
expect(getOutputNodes(nodes, [mkEdge("txt", "gv")])).toEqual([])
|
|
1233
|
+
})
|
|
1234
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
SCENE3D_DEFAULT_ADVANCED_ENGINE,
|
|
4
|
+
SCENE3D_SUPPORTED_SCHEMA_VERSIONS,
|
|
5
|
+
resolveScene3DAuthoringEngine,
|
|
6
|
+
} from "../index.js"
|
|
7
|
+
|
|
8
|
+
/** Only the fields the resolver reads — it must not need a whole valid plan. */
|
|
9
|
+
const v1Plan = { planType: "3d-scene", schemaVersion: 1 }
|
|
10
|
+
const v2Plan = (engine = "blender-cloud") => ({
|
|
11
|
+
planType: "3d-scene",
|
|
12
|
+
schemaVersion: 2,
|
|
13
|
+
provenance: { engine },
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe("resolveScene3DAuthoringEngine", () => {
|
|
17
|
+
it("keeps a plain generate on Basic, with NO extra wire fields", () => {
|
|
18
|
+
const choice = resolveScene3DAuthoringEngine({})
|
|
19
|
+
expect(choice).toEqual({ ok: true, lane: "basic", engine: undefined, fields: {} })
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it("keeps a v1 edit on Basic", () => {
|
|
23
|
+
const choice = resolveScene3DAuthoringEngine({ plan: v1Plan })
|
|
24
|
+
expect(choice.ok && choice.lane).toBe("basic")
|
|
25
|
+
expect(choice.ok && choice.fields).toEqual({})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it("sends an explicitly requested advanced engine plus the accepted versions", () => {
|
|
29
|
+
const choice = resolveScene3DAuthoringEngine({
|
|
30
|
+
requested: "blender-cloud",
|
|
31
|
+
availableEngines: ["blender-cloud"],
|
|
32
|
+
})
|
|
33
|
+
expect(choice.ok && choice.lane).toBe("advanced")
|
|
34
|
+
expect(choice.ok && choice.fields).toEqual({
|
|
35
|
+
engine: "blender-cloud",
|
|
36
|
+
acceptedSceneSchemaVersions: [...SCENE3D_SUPPORTED_SCHEMA_VERSIONS],
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("routes a v2 edit to the advanced lane with no engine chosen", () => {
|
|
41
|
+
const choice = resolveScene3DAuthoringEngine({ plan: v2Plan() })
|
|
42
|
+
expect(choice.ok && choice.lane).toBe("advanced")
|
|
43
|
+
expect(choice.ok && choice.engine).toBe(SCENE3D_DEFAULT_ADVANCED_ENGINE)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("keeps a v2 edit on the engine that authored it", () => {
|
|
47
|
+
const choice = resolveScene3DAuthoringEngine({
|
|
48
|
+
plan: v2Plan("blender-local"),
|
|
49
|
+
availableEngines: ["blender-cloud", "blender-local"],
|
|
50
|
+
})
|
|
51
|
+
expect(choice.ok && choice.engine).toBe("blender-local")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("does not send a v2 edit to an authoring engine this install dropped", () => {
|
|
55
|
+
const choice = resolveScene3DAuthoringEngine({
|
|
56
|
+
plan: v2Plan("blender-local"),
|
|
57
|
+
availableEngines: ["blender-cloud"],
|
|
58
|
+
})
|
|
59
|
+
expect(choice.ok && choice.engine).toBe("blender-cloud")
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it("REFUSES a v2 scene pointed at Basic instead of downgrading it", () => {
|
|
63
|
+
const choice = resolveScene3DAuthoringEngine({ requested: "basic", plan: v2Plan() })
|
|
64
|
+
expect(choice.ok).toBe(false)
|
|
65
|
+
expect(!choice.ok && choice.code).toBe("schema_requires_advanced")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("REFUSES a v2 edit when the install has no advanced engine at all", () => {
|
|
69
|
+
const choice = resolveScene3DAuthoringEngine({ plan: v2Plan(), availableEngines: [] })
|
|
70
|
+
expect(!choice.ok && choice.code).toBe("advanced_unavailable")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("REFUSES an explicit engine this install does not serve — never falls back to Basic", () => {
|
|
74
|
+
const choice = resolveScene3DAuthoringEngine({
|
|
75
|
+
requested: "blender-local",
|
|
76
|
+
availableEngines: ["blender-cloud"],
|
|
77
|
+
})
|
|
78
|
+
expect(!choice.ok && choice.code).toBe("engine_unavailable")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("REFUSES an explicit engine on a v2 scene the same way", () => {
|
|
82
|
+
const choice = resolveScene3DAuthoringEngine({
|
|
83
|
+
requested: "blender-local",
|
|
84
|
+
plan: v2Plan(),
|
|
85
|
+
availableEngines: ["blender-cloud"],
|
|
86
|
+
})
|
|
87
|
+
expect(!choice.ok && choice.code).toBe("engine_unavailable")
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("REFUSES an engine name the contract does not know", () => {
|
|
91
|
+
const choice = resolveScene3DAuthoringEngine({ requested: "unknown-renderer" })
|
|
92
|
+
expect(!choice.ok && choice.code).toBe("unknown_engine")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("REFUSES a scene claiming a schema version nothing here can author against", () => {
|
|
96
|
+
const choice = resolveScene3DAuthoringEngine({ plan: { planType: "3d-scene", schemaVersion: 3 } })
|
|
97
|
+
expect(!choice.ok && choice.code).toBe("unsupported_schema_version")
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it("stays permissive when availability is UNKNOWN — the route refuses honestly", () => {
|
|
101
|
+
const choice = resolveScene3DAuthoringEngine({ requested: "blender-local" })
|
|
102
|
+
expect(choice.ok && choice.engine).toBe("blender-local")
|
|
103
|
+
})
|
|
104
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -1061,6 +1061,8 @@ export * from "./scene3d-v2.js"
|
|
|
1061
1061
|
export * from "./scene3d-v2-plan.js"
|
|
1062
1062
|
export * from "./scene3d-v2-resources.js"
|
|
1063
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"
|
|
1064
1066
|
|
|
1065
1067
|
// --- transient studio keys — the public share read strips them ---
|
|
1066
1068
|
export {
|
|
@@ -1070,3 +1072,4 @@ export {
|
|
|
1070
1072
|
} from "./studio-transient.js"
|
|
1071
1073
|
|
|
1072
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>,
|