@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.
Files changed (52) hide show
  1. package/dist/index.cjs +2047 -84
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1556 -27
  4. package/dist/index.d.ts +1556 -27
  5. package/dist/index.js +1861 -85
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/caption-styles.test.ts +207 -0
  9. package/src/__tests__/edl-multicam.test.ts +304 -0
  10. package/src/__tests__/edl.test.ts +822 -0
  11. package/src/__tests__/fan-out-rows.test.ts +208 -0
  12. package/src/__tests__/instagram-scrape.test.ts +66 -0
  13. package/src/__tests__/llm-models.test.ts +48 -11
  14. package/src/__tests__/meta-ads-scrape.test.ts +284 -0
  15. package/src/__tests__/node-runtime-keys.test.ts +15 -0
  16. package/src/__tests__/presentation-utils.test.ts +67 -0
  17. package/src/__tests__/producer-types.test.ts +19 -0
  18. package/src/__tests__/schedule-rules.test.ts +265 -0
  19. package/src/__tests__/speaker-layouts.test.ts +203 -0
  20. package/src/__tests__/transcribe-capabilities.test.ts +104 -0
  21. package/src/__tests__/transcribe-preflight.test.ts +60 -0
  22. package/src/__tests__/trigger-feeds.test.ts +39 -0
  23. package/src/__tests__/video-duration-auto.test.ts +65 -0
  24. package/src/__tests__/video-duration.test.ts +56 -0
  25. package/src/__tests__/video-link.test.ts +137 -0
  26. package/src/__tests__/workflow-export-strip.test.ts +59 -1
  27. package/src/caption-styles.ts +240 -0
  28. package/src/credit-identifiers.ts +31 -0
  29. package/src/edit-plan-contract.ts +96 -0
  30. package/src/edl-multicam.ts +185 -0
  31. package/src/edl.ts +747 -0
  32. package/src/entity-image-handle.ts +24 -1
  33. package/src/fan-out-rows.ts +213 -0
  34. package/src/index.ts +206 -3
  35. package/src/instagram-scrape.ts +204 -0
  36. package/src/llm-models.ts +80 -3
  37. package/src/meta-ads-scrape.ts +463 -0
  38. package/src/model-catalog.ts +48 -5
  39. package/src/model-constants.ts +148 -5
  40. package/src/node-mappable-fields.ts +2 -0
  41. package/src/node-runtime-keys.ts +28 -0
  42. package/src/presentation-utils.ts +49 -0
  43. package/src/producer-types.ts +20 -0
  44. package/src/schedule-rules.ts +484 -0
  45. package/src/speaker-layouts.ts +220 -0
  46. package/src/transcribe-preflight.ts +101 -0
  47. package/src/trigger-feeds.ts +59 -0
  48. package/src/trigger-node-types.ts +20 -0
  49. package/src/video-duration-auto.ts +18 -0
  50. package/src/video-duration.ts +32 -0
  51. package/src/video-link.ts +167 -0
  52. package/src/workflow-export.ts +37 -1
@@ -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
- /** Transcription providers */
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
- return parsed === undefined || Number.isNaN(parsed) ? fallback : parsed
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. */
@@ -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
  // ---------------------------------------------------------------------------
@@ -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
  ])