@nodaro/shared 3.11.0 → 3.12.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 +2047 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1556 -27
- package/dist/index.d.ts +1556 -27
- package/dist/index.js +1861 -85
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/caption-styles.test.ts +207 -0
- package/src/__tests__/edl-multicam.test.ts +304 -0
- package/src/__tests__/edl.test.ts +822 -0
- package/src/__tests__/fan-out-rows.test.ts +208 -0
- package/src/__tests__/instagram-scrape.test.ts +66 -0
- package/src/__tests__/llm-models.test.ts +48 -11
- package/src/__tests__/meta-ads-scrape.test.ts +284 -0
- package/src/__tests__/node-runtime-keys.test.ts +15 -0
- package/src/__tests__/presentation-utils.test.ts +67 -0
- package/src/__tests__/producer-types.test.ts +19 -0
- package/src/__tests__/schedule-rules.test.ts +265 -0
- package/src/__tests__/speaker-layouts.test.ts +203 -0
- package/src/__tests__/transcribe-capabilities.test.ts +104 -0
- package/src/__tests__/transcribe-preflight.test.ts +60 -0
- package/src/__tests__/trigger-feeds.test.ts +39 -0
- package/src/__tests__/video-duration-auto.test.ts +65 -0
- package/src/__tests__/video-duration.test.ts +56 -0
- package/src/__tests__/video-link.test.ts +137 -0
- package/src/__tests__/workflow-export-strip.test.ts +59 -1
- package/src/caption-styles.ts +240 -0
- package/src/credit-identifiers.ts +31 -0
- package/src/edit-plan-contract.ts +96 -0
- package/src/edl-multicam.ts +185 -0
- package/src/edl.ts +747 -0
- package/src/entity-image-handle.ts +24 -1
- package/src/fan-out-rows.ts +213 -0
- package/src/index.ts +206 -3
- package/src/instagram-scrape.ts +204 -0
- package/src/llm-models.ts +80 -3
- package/src/meta-ads-scrape.ts +463 -0
- package/src/model-catalog.ts +48 -5
- package/src/model-constants.ts +148 -5
- package/src/node-mappable-fields.ts +2 -0
- package/src/node-runtime-keys.ts +28 -0
- package/src/presentation-utils.ts +49 -0
- package/src/producer-types.ts +20 -0
- package/src/schedule-rules.ts +484 -0
- package/src/speaker-layouts.ts +220 -0
- package/src/transcribe-preflight.ts +101 -0
- package/src/trigger-feeds.ts +59 -0
- package/src/trigger-node-types.ts +20 -0
- package/src/video-duration-auto.ts +18 -0
- package/src/video-duration.ts +32 -0
- package/src/video-link.ts +167 -0
- package/src/workflow-export.ts +37 -1
package/src/model-constants.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { z } from "zod"
|
|
6
6
|
import { MODEL_CATALOG } from "./model-catalog.js"
|
|
7
|
+
import { isAutoVideoDuration, VIDEO_DURATION_AUTO } from "./video-duration-auto.js"
|
|
7
8
|
|
|
8
9
|
/** Base USD value of 1 Nodaro credit. Used for cost→credit conversion. */
|
|
9
10
|
export const CREDIT_BASE_USD = 0.002
|
|
@@ -1042,6 +1043,41 @@ export const VIDEO_TO_VIDEO_PROVIDERS = [
|
|
|
1042
1043
|
] as const
|
|
1043
1044
|
export type VideoToVideoProvider = typeof VIDEO_TO_VIDEO_PROVIDERS[number]
|
|
1044
1045
|
|
|
1046
|
+
/**
|
|
1047
|
+
* Seedance models the Video to Video NODE offers as a whole-clip, prompt-driven
|
|
1048
|
+
* EDIT ("make it black and white", "she wears @image_1").
|
|
1049
|
+
*
|
|
1050
|
+
* These are deliberately NOT members of {@link VIDEO_TO_VIDEO_PROVIDERS} (the
|
|
1051
|
+
* `/v1/video-to-video` route's own enum): Seedance has no separate edit
|
|
1052
|
+
* endpoint — it edits a REFERENCE video when the prompt reads as an edit. So
|
|
1053
|
+
* every surface (single-node run, DAG payload builder, MCP `modify_video`)
|
|
1054
|
+
* dispatches these through the ONE existing Seedance reference-video lane
|
|
1055
|
+
* (`text-to-video`), with the source clip as `@video_1` and the edit shape
|
|
1056
|
+
* below. Reference-clip bounds, the unit×(input+output) reservation, the
|
|
1057
|
+
* measured settlement, the edit-mode retry and the reconcile recovery are that
|
|
1058
|
+
* lane's — there is no second copy to keep in step.
|
|
1059
|
+
*/
|
|
1060
|
+
export const SEEDANCE_VIDEO_EDIT_PROVIDERS = ["seedance-2-5"] as const
|
|
1061
|
+
export type SeedanceVideoEditProvider = typeof SEEDANCE_VIDEO_EDIT_PROVIDERS[number]
|
|
1062
|
+
|
|
1063
|
+
export function isSeedanceVideoEditProvider(provider: string | undefined): provider is SeedanceVideoEditProvider {
|
|
1064
|
+
return !!provider && (SEEDANCE_VIDEO_EDIT_PROVIDERS as readonly string[]).includes(provider)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
/** Every model the Video to Video node can be set to: the route's own providers
|
|
1068
|
+
* plus the Seedance edit models dispatched through the text-to-video lane. */
|
|
1069
|
+
export const VIDEO_TO_VIDEO_NODE_PROVIDERS = [...VIDEO_TO_VIDEO_PROVIDERS, ...SEEDANCE_VIDEO_EDIT_PROVIDERS] as const
|
|
1070
|
+
export type VideoToVideoNodeProvider = typeof VIDEO_TO_VIDEO_NODE_PROVIDERS[number]
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* The request shape Seedance edit mode requires, sent UP FRONT by the Video to
|
|
1074
|
+
* Video node: the output takes the source clip's own ratio and length
|
|
1075
|
+
* (`adaptive`, and Auto = `VIDEO_DURATION_AUTO`). Camel-cased twin of the KIE
|
|
1076
|
+
* wire pair the provider layer resubmits with when Seedance reclassifies an
|
|
1077
|
+
* ordinary run as an edit.
|
|
1078
|
+
*/
|
|
1079
|
+
export const SEEDANCE_VIDEO_EDIT_SHAPE = { aspectRatio: "adaptive", duration: VIDEO_DURATION_AUTO } as const
|
|
1080
|
+
|
|
1045
1081
|
/** Face swap providers */
|
|
1046
1082
|
export const FACE_SWAP_PROVIDERS = [
|
|
1047
1083
|
"roop",
|
|
@@ -1208,15 +1244,97 @@ export const MUSIC_PROVIDERS = [
|
|
|
1208
1244
|
] as const
|
|
1209
1245
|
export type MusicProvider = typeof MUSIC_PROVIDERS[number]
|
|
1210
1246
|
|
|
1211
|
-
/**
|
|
1247
|
+
/**
|
|
1248
|
+
* Transcription providers a caller may name — on `/v1/transcribe`, the SDK/CLI
|
|
1249
|
+
* and the canvas Transcribe node. All three lanes are served on cloud again
|
|
1250
|
+
* (the canvas picker re-offered the two Replicate lanes in #768 while this enum
|
|
1251
|
+
* still hid them, so a single-node Run on Whisper 400'd where the same node in a
|
|
1252
|
+
* workflow Run worked). `elevenlabs-stt` stays FIRST: several call sites take
|
|
1253
|
+
* "the first enabled word-capable provider" from this order.
|
|
1254
|
+
*/
|
|
1212
1255
|
export const TRANSCRIBE_PROVIDERS = [
|
|
1213
|
-
// Replicate disabled
|
|
1214
|
-
// "whisper",
|
|
1215
|
-
// "incredibly-fast-whisper",
|
|
1216
1256
|
"elevenlabs-stt",
|
|
1257
|
+
"whisper",
|
|
1258
|
+
"incredibly-fast-whisper",
|
|
1217
1259
|
] as const
|
|
1218
1260
|
export type TranscribeProvider = typeof TRANSCRIBE_PROVIDERS[number]
|
|
1219
1261
|
|
|
1262
|
+
/**
|
|
1263
|
+
* Every transcription LANE the platform implements. Today this is the same set
|
|
1264
|
+
* as `TRANSCRIBE_PROVIDERS`; it stays a separate name because the two answer
|
|
1265
|
+
* different questions — "what may a caller name" vs "what can run" — and they
|
|
1266
|
+
* have diverged before (the Replicate lanes were hidden from callers for months
|
|
1267
|
+
* while still reached at runtime via the route default and add-captions'
|
|
1268
|
+
* auto-transcribe). Capability questions must be asked over THIS union.
|
|
1269
|
+
*/
|
|
1270
|
+
export const TRANSCRIBE_LANES = [
|
|
1271
|
+
"whisper",
|
|
1272
|
+
"incredibly-fast-whisper",
|
|
1273
|
+
"elevenlabs-stt",
|
|
1274
|
+
] as const
|
|
1275
|
+
export type TranscribeLane = typeof TRANSCRIBE_LANES[number]
|
|
1276
|
+
|
|
1277
|
+
// Compile-time pin: the user-facing enum is a subset of the lanes, so every
|
|
1278
|
+
// provider a caller can name has a capability row below.
|
|
1279
|
+
const _TRANSCRIBE_PROVIDERS_ARE_LANES: readonly TranscribeLane[] = TRANSCRIBE_PROVIDERS
|
|
1280
|
+
void _TRANSCRIBE_PROVIDERS_ARE_LANES
|
|
1281
|
+
|
|
1282
|
+
/**
|
|
1283
|
+
* What each transcription lane can actually DO. Single source of truth — never
|
|
1284
|
+
* re-derive a capability from a provider-name check.
|
|
1285
|
+
*
|
|
1286
|
+
* `wordTimestamps`: does the lane return per-word start/end times?
|
|
1287
|
+
* - `whisper` (Replicate `openai/whisper`) — NO. Its input schema has no
|
|
1288
|
+
* `word_timestamps` field in ANY published version, so Replicate silently
|
|
1289
|
+
* drops the key and the segments come back without `words`. Asking this lane
|
|
1290
|
+
* for word timings yields an empty array, never an error.
|
|
1291
|
+
* - `incredibly-fast-whisper` — yes, via `timestamp: "word"`.
|
|
1292
|
+
* - `elevenlabs-stt` (direct Scribe) — always word-level, flag or not.
|
|
1293
|
+
*/
|
|
1294
|
+
export const TRANSCRIBE_PROVIDER_CAPABILITIES: Record<TranscribeLane, { wordTimestamps: boolean }> = {
|
|
1295
|
+
"whisper": { wordTimestamps: false },
|
|
1296
|
+
"incredibly-fast-whisper": { wordTimestamps: true },
|
|
1297
|
+
"elevenlabs-stt": { wordTimestamps: true },
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/** The lanes that can honour a word-timestamps request, in declaration order. */
|
|
1301
|
+
export function transcribeProvidersWithWordTimestamps(): TranscribeLane[] {
|
|
1302
|
+
return TRANSCRIBE_LANES.filter((p) => TRANSCRIBE_PROVIDER_CAPABILITIES[p].wordTimestamps)
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Capability question for a lane id that came from UNTRUSTED data — node data,
|
|
1307
|
+
* an imported workflow, a wire body — where the string may be anything at all.
|
|
1308
|
+
* An unknown lane answers `false`: we cannot promise word timings from a lane
|
|
1309
|
+
* we know nothing about, and "false" is always the safe answer (it suppresses
|
|
1310
|
+
* an INFERRED request, and turns an EXPLICIT one into the honest refusal in
|
|
1311
|
+
* `transcribe()` instead of a `Cannot read properties of undefined` TypeError).
|
|
1312
|
+
*/
|
|
1313
|
+
export function transcribeLaneSupportsWordTimestamps(lane: string | null | undefined): boolean {
|
|
1314
|
+
if (!lane) return false
|
|
1315
|
+
return TRANSCRIBE_PROVIDER_CAPABILITIES[lane as TranscribeLane]?.wordTimestamps === true
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
/**
|
|
1319
|
+
* The lane an absent `provider` resolves to — the historical `/v1/transcribe`
|
|
1320
|
+
* default, kept as-is because the credit guard reserves on the provider id
|
|
1321
|
+
* (changing it would silently change what bills).
|
|
1322
|
+
*/
|
|
1323
|
+
export const DEFAULT_TRANSCRIBE_PROVIDER: TranscribeLane = "whisper"
|
|
1324
|
+
|
|
1325
|
+
/**
|
|
1326
|
+
* The lane a transcribe NODE with no `provider` in its data resolves to —
|
|
1327
|
+
* deliberately NOT `DEFAULT_TRANSCRIBE_PROVIDER`. The route's default is the
|
|
1328
|
+
* legacy whisper lane and exists only so a pre-existing REST caller keeps
|
|
1329
|
+
* billing the same id; a node authored/imported without a provider is a fresh
|
|
1330
|
+
* request, and the canvas picker's own default is this one. Single-sourced so
|
|
1331
|
+
* the backend DAG (`payload-builder.ts`) and the frontend run
|
|
1332
|
+
* (`execute-node.ts`) cannot drift into sending different engines for the same
|
|
1333
|
+
* node — they did, and the frontend's silent whisper fallback started 400ing
|
|
1334
|
+
* once the Replicate lanes left `TRANSCRIBE_PROVIDERS`.
|
|
1335
|
+
*/
|
|
1336
|
+
export const DEFAULT_TRANSCRIBE_NODE_PROVIDER: TranscribeLane = "elevenlabs-stt"
|
|
1337
|
+
|
|
1220
1338
|
/** Script generation providers */
|
|
1221
1339
|
export const SCRIPT_PROVIDERS = [
|
|
1222
1340
|
"gemini",
|
|
@@ -2551,7 +2669,32 @@ export const PRICING_DEFAULT_DURATION_SEC: Record<string, number> = {
|
|
|
2551
2669
|
export function pricedOutputDurationSec(provider: string, requested: number | string | undefined): number {
|
|
2552
2670
|
const fallback = PRICING_DEFAULT_DURATION_SEC[provider] ?? 5
|
|
2553
2671
|
const parsed = typeof requested === "string" ? parseInt(requested, 10) : requested
|
|
2554
|
-
|
|
2672
|
+
if (parsed === undefined || Number.isNaN(parsed)) return fallback
|
|
2673
|
+
// AUTO (`VIDEO_DURATION_AUTO`): the model picks the length, so the only
|
|
2674
|
+
// safe price is the LONGEST it can render — `commit_credits` refunds a surplus
|
|
2675
|
+
// but never collects a deficit, and the delivered clip is measured at settle
|
|
2676
|
+
// time (lib/seedance2-ref-video-settle.ts). Living HERE, the one source every
|
|
2677
|
+
// tier and every scaled reservation reads, a new pricing call site reserves
|
|
2678
|
+
// the ceiling by default instead of having to remember to. Any other
|
|
2679
|
+
// non-positive value is nonsense and prices at the render default.
|
|
2680
|
+
if (parsed <= 0) {
|
|
2681
|
+
return isAutoVideoDuration(parsed) && supportsAutoVideoDuration(provider)
|
|
2682
|
+
? maxVideoDurationSec(provider) ?? fallback
|
|
2683
|
+
: fallback
|
|
2684
|
+
}
|
|
2685
|
+
return parsed
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
/** Models that accept `VIDEO_DURATION_AUTO` — a catalog capability (docs.kie.ai:
|
|
2689
|
+
* the Seedance 2 family, "4-15 seconds or -1" / 2.5 "Special values -1"). */
|
|
2690
|
+
export function supportsAutoVideoDuration(provider: string | undefined): boolean {
|
|
2691
|
+
return !!provider && MODEL_CATALOG[provider]?.autoDuration === true
|
|
2692
|
+
}
|
|
2693
|
+
|
|
2694
|
+
/** The longest clip a duration-tiered provider can render (its top priced tier). */
|
|
2695
|
+
export function maxVideoDurationSec(provider: string): number | undefined {
|
|
2696
|
+
const tiers = VIDEO_DURATION_TIERS[provider]
|
|
2697
|
+
return tiers && tiers.length > 0 ? tiers[tiers.length - 1]!.maxSeconds : undefined
|
|
2555
2698
|
}
|
|
2556
2699
|
|
|
2557
2700
|
/**
|
|
@@ -69,6 +69,8 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
|
|
|
69
69
|
"creature": ["creatureName", "description"],
|
|
70
70
|
"location": ["locationName", "description"],
|
|
71
71
|
"web-scrape": ["query", "url", "target"],
|
|
72
|
+
"meta-ads-scrape": ["query", "pageUrls"],
|
|
73
|
+
"instagram-scrape": ["targets"],
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/** suno-generate secondary text fields exposed as `field-<key>` canvas handles. */
|
package/src/node-runtime-keys.ts
CHANGED
|
@@ -41,6 +41,9 @@ export const EXECUTION_DATA_KEYS: ReadonlySet<string> = new Set([
|
|
|
41
41
|
"__listTotal",
|
|
42
42
|
"__listCompleted",
|
|
43
43
|
"__listResults",
|
|
44
|
+
// Row-aligned twin of __listResults (Extract Field, List output) — read only
|
|
45
|
+
// by the fan-out so two lists cut from one array pair by row.
|
|
46
|
+
"__alignedListResults",
|
|
44
47
|
// List fan-out window flag (abandon-guard exemption). Set/cleared by
|
|
45
48
|
// executeNodeForList — purely execution-related, never user-edited.
|
|
46
49
|
"__listRunning",
|
|
@@ -70,6 +73,25 @@ export const EXECUTION_DATA_KEYS: ReadonlySet<string> = new Set([
|
|
|
70
73
|
"lastInputs",
|
|
71
74
|
"lastMeta",
|
|
72
75
|
"__upstreamCount",
|
|
76
|
+
// Video URL node — the download's live percent/phase, written on every
|
|
77
|
+
// progress tick (~2/s). Pure run-state; also in TRANSIENT_RUNTIME_KEYS below.
|
|
78
|
+
"downloadPercent",
|
|
79
|
+
"downloadPhase",
|
|
80
|
+
// Webhook Output's delivery receipt. A webhook target may reflect the
|
|
81
|
+
// request back (httpbin, RequestBin, an API that 400s with "headers
|
|
82
|
+
// received: …"), so `webhookResponseBody` can carry whatever the request
|
|
83
|
+
// carried — with an attached credential, the secret itself. Listing the three
|
|
84
|
+
// here is what keeps the receipt out of template exports (GENERATED_FIELDS
|
|
85
|
+
// derives from this set), out of node presets, and out of undo history.
|
|
86
|
+
"webhookSuccess",
|
|
87
|
+
"webhookStatusCode",
|
|
88
|
+
"webhookResponseBody",
|
|
89
|
+
// When the editor's "Clear results" last emptied this node (ISO time). Not a
|
|
90
|
+
// result and not config: bookkeeping that tells the load-time recovery lanes
|
|
91
|
+
// "this node is empty ON PURPOSE" — without it, every reload reads an empty
|
|
92
|
+
// node as "ran while the editor was closed" and paints the last run back.
|
|
93
|
+
// Persisted (never transient): the reload is exactly when it is read.
|
|
94
|
+
"resultsClearedAt",
|
|
73
95
|
])
|
|
74
96
|
|
|
75
97
|
/**
|
|
@@ -100,6 +122,12 @@ export const TRANSIENT_RUNTIME_KEYS: ReadonlySet<string> = new Set([
|
|
|
100
122
|
"__listRunning",
|
|
101
123
|
"_upstreamRefresh",
|
|
102
124
|
"__upstreamCount",
|
|
125
|
+
// Video URL node download ticks. They used to dirty the workflow twice a
|
|
126
|
+
// second for the length of the download — the same phantom-save chain the
|
|
127
|
+
// job-progress keys above were moved here to stop. What SURVIVES a reload is
|
|
128
|
+
// `downloadStatus` + `downloadId`; the percent is re-read from the server.
|
|
129
|
+
"downloadPercent",
|
|
130
|
+
"downloadPhase",
|
|
103
131
|
])
|
|
104
132
|
|
|
105
133
|
/**
|
|
@@ -145,6 +145,13 @@ const VIDEO_OUTPUT_TYPES = new Set([
|
|
|
145
145
|
"upload-video",
|
|
146
146
|
"lip-sync", "motion-transfer", "video-upscale", "add-captions",
|
|
147
147
|
"social-media-format",
|
|
148
|
+
// apply-edl renders an EDL into video OR audio. Its medium is decided at run
|
|
149
|
+
// time (DYNAMIC_PRODUCER_TYPES), so getOutputType would answer "data" and a
|
|
150
|
+
// published app would render the cut as a JSON blob. Declaring it here — as
|
|
151
|
+
// the voice-changer/dubbing precedent does for their default medium — makes
|
|
152
|
+
// the classifier answer "video" (the common case; an audio-only cut still
|
|
153
|
+
// plays in a video element). Asserted in producer-types.test.ts.
|
|
154
|
+
"apply-edl",
|
|
148
155
|
])
|
|
149
156
|
|
|
150
157
|
const AUDIO_OUTPUT_TYPES = new Set([
|
|
@@ -393,6 +400,48 @@ export function getInputFieldSchema(nodeType: string): InputFieldSchema | undefi
|
|
|
393
400
|
return INPUT_FIELD_MAP[nodeType]
|
|
394
401
|
}
|
|
395
402
|
|
|
403
|
+
const MEDIA_INPUT_FIELD_TYPES: ReadonlySet<InputFieldSchema["type"]> = new Set([
|
|
404
|
+
"image-url",
|
|
405
|
+
"video-url",
|
|
406
|
+
"audio-url",
|
|
407
|
+
])
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Shallow-merge run-time input overrides (a published app's inputs, an API-token
|
|
411
|
+
* run, MCP `run_app` / `run_workflow` inputs, a wired component handle) over a
|
|
412
|
+
* node's SAVED data — the one merge every such lane must use.
|
|
413
|
+
*
|
|
414
|
+
* `metadata` holds facts measured FROM the node's media (its length, its
|
|
415
|
+
* dimensions). When an override swaps that media — the node's primary input
|
|
416
|
+
* field is a media url (`INPUT_FIELD_MAP`) and the override changes it — the
|
|
417
|
+
* snapshot's facts describe a file that is no longer there, so they are dropped
|
|
418
|
+
* unless the override supplies its own. Left behind, a publisher's
|
|
419
|
+
* `metadata.durationSeconds` outranks the run's own transcript as Edit Plan's
|
|
420
|
+
* duration basis and under-buckets a caller's longer episode: an estimate that
|
|
421
|
+
* passes the balance precheck for a run the reserve then refuses.
|
|
422
|
+
*
|
|
423
|
+
* Schema-driven on purpose: a new media input node is covered by its
|
|
424
|
+
* `INPUT_FIELD_MAP` row, with no list to remember here.
|
|
425
|
+
*/
|
|
426
|
+
export function mergeNodeInputOverrides(
|
|
427
|
+
nodeType: string | undefined,
|
|
428
|
+
data: Record<string, unknown>,
|
|
429
|
+
overrides: Record<string, unknown>,
|
|
430
|
+
): Record<string, unknown> {
|
|
431
|
+
const merged: Record<string, unknown> = { ...data, ...overrides }
|
|
432
|
+
const schema = nodeType ? INPUT_FIELD_MAP[nodeType] : undefined
|
|
433
|
+
if (
|
|
434
|
+
schema &&
|
|
435
|
+
MEDIA_INPUT_FIELD_TYPES.has(schema.type) &&
|
|
436
|
+
schema.key in overrides &&
|
|
437
|
+
overrides[schema.key] !== data[schema.key] &&
|
|
438
|
+
!("metadata" in overrides)
|
|
439
|
+
) {
|
|
440
|
+
delete merged.metadata
|
|
441
|
+
}
|
|
442
|
+
return merged
|
|
443
|
+
}
|
|
444
|
+
|
|
396
445
|
// ---------------------------------------------------------------------------
|
|
397
446
|
// Migration & validation helpers (PresentationItem)
|
|
398
447
|
// ---------------------------------------------------------------------------
|
package/src/producer-types.ts
CHANGED
|
@@ -150,6 +150,19 @@ export const DYNAMIC_PRODUCER_TYPES: ReadonlySet<string> = new Set([
|
|
|
150
150
|
// backend routes the correct lane by sourceHandle in getPrimaryOutput
|
|
151
151
|
// (output-extractor.ts); the frontend does so in extractNodeOutput.
|
|
152
152
|
"split-media",
|
|
153
|
+
// apply-edl renders an EDL into ONE media output whose type is decided at
|
|
154
|
+
// run time by the node's `output` setting (video OR audio) — so its static
|
|
155
|
+
// medium is genuinely unknown and it belongs here, letting canvas validators
|
|
156
|
+
// accept its default media handle on BOTH audio and video input handles. It
|
|
157
|
+
// ALSO emits a fixed `json` handle (the remapped Transcript); that half lives
|
|
158
|
+
// in JSON_PRODUCER_TYPES (frontend/src/lib/data-handles.ts). The FIRST node
|
|
159
|
+
// with both a dynamic media handle and a fixed json handle. Because
|
|
160
|
+
// getOutputType (presentation-utils.ts) deliberately returns "data" for
|
|
161
|
+
// DYNAMIC members, apply-edl is ALSO added to the literal VIDEO_OUTPUT_TYPES
|
|
162
|
+
// there so a published app renders the cut as video, mirroring the
|
|
163
|
+
// voice-changer/dubbing precedent. Asserted in producer-types.test.ts (the
|
|
164
|
+
// suite does not fail on omission).
|
|
165
|
+
"apply-edl",
|
|
153
166
|
])
|
|
154
167
|
|
|
155
168
|
/**
|
|
@@ -216,4 +229,11 @@ export const FAN_OUT_EACH_TYPES: ReadonlySet<string> = new Set([
|
|
|
216
229
|
"merge-lists",
|
|
217
230
|
"sort-list",
|
|
218
231
|
"selector",
|
|
232
|
+
// edit-plan `clips` mode emits a bare `Edl[]` on `data.generatedJson`, so an
|
|
233
|
+
// edge leaving it defaults to "each" — one downstream execution (typically an
|
|
234
|
+
// apply-edl render) per clip. The `tighten`/`chapters` modes emit an OBJECT,
|
|
235
|
+
// for which the list extractors return undefined, so an "each" edge falls back
|
|
236
|
+
// to the scalar `edl` value (no fan-out) — the same graceful degradation
|
|
237
|
+
// web-scrape relies on. See `unwrapEditPlanOutput` in `edit-plan-contract.ts`.
|
|
238
|
+
"edit-plan",
|
|
219
239
|
])
|