@nodaro/shared 1.7.0 → 1.10.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.
@@ -146,7 +146,7 @@ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
146
146
  { intent: "best for typography / logos / text-heavy", modelIds: ["nano-banana-pro", "gpt-image-2"], note: "Nano Banana Pro for diagrams / complex text; GPT Image 2 for logos and short copy." },
147
147
  { intent: "cheapest realistic image", modelIds: ["z-image", "qwen", "imagen4-fast"], note: "Z-Image is the cheapest at 1 credit. Qwen / Imagen4 Fast for slightly higher quality." },
148
148
  { intent: "highest fidelity image", modelIds: ["nano-banana-pro", "imagen4-ultra", "flux-flex"], note: "Pick by family preference; all three are premium tiers." },
149
- { intent: "image edit / restyle", modelIds: ["flux-kontext", "ideogram-remix", "seedream-5-lite-i2i"], note: "Flux Kontext preserves identity; Ideogram Remix is character-aware; Seedream 5 Lite for instruction-based edits." },
149
+ { intent: "image edit / restyle", modelIds: ["flux-kontext", "ideogram-remix", "seedream-5-pro-i2i"], note: "Flux Kontext preserves identity; Ideogram Remix is character-aware; Seedream 5 Pro for instruction-based edits (5 Lite is the budget option)." },
150
150
  { intent: "highest-resolution image (4K / 8K)", modelIds: ["topaz-image-upscale", "nano-banana-pro", "gpt-image-2"], note: "Generate at native then Topaz upscale for 8K." },
151
151
  { intent: "background removal / cutout", modelIds: ["recraft-remove-bg"], note: "1 credit, no prompt needed." },
152
152
  // video
@@ -165,6 +165,11 @@ export const MODEL_RECOMMENDATIONS: readonly ModelRecommendation[] = [
165
165
  // can consume them. Keep in sync when adding a new ratio set.)
166
166
  // =============================================================================
167
167
  const NANO_BANANA_RATIOS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9"] as const
168
+ // Nano Banana 2 Lite only — the model's full documented enum incl. `auto`
169
+ // (native default) and extreme banner ratios (docs.kie.ai/market/google/
170
+ // nano-banana-2-lite). Kept separate so the wider set can't leak to the
171
+ // rest of the family.
172
+ const NANO_BANANA_2_LITE_RATIOS = ["auto", "1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "4:1", "1:4", "8:1", "1:8"] as const
168
173
  // Note: these arrays are synchronized with the frontend's
169
174
  // `model-options.ts` constants of the same names. The frontend now imports
170
175
  // them via the `getAspectRatioOptions(modelId)` helper; if you update one
@@ -233,6 +238,22 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
233
238
  { identifier: "nano-banana-2:4K", credits: 5, note: "4K" },
234
239
  ],
235
240
  },
241
+ "nano-banana-2-lite": {
242
+ id: "nano-banana-2-lite",
243
+ kind: "image",
244
+ // Same single-id shape as nano-banana-2: optional input images on the one
245
+ // endpoint (via image_urls, up to 10), so reachable for both t2i and i2i.
246
+ modes: ["t2i", "i2i"] as const,
247
+ family: "Google",
248
+ label: "Nano Banana 2 Lite",
249
+ series: "Nano Banana",
250
+ description: "Lightweight Nano Banana 2 (Gemini 3.1 Flash-Lite) — fast, low-cost 1K generation and editing.",
251
+ useCases: ["realistic", "drafts", "iteration"],
252
+ features: ["reference-image"],
253
+ aspectRatios: NANO_BANANA_2_LITE_RATIOS,
254
+ // 1K only — no resolution lever, flat pricing.
255
+ pricing: [{ identifier: "nano-banana-2-lite", credits: 2, note: "1K flat" }],
256
+ },
236
257
  "nano-banana-pro": {
237
258
  id: "nano-banana-pro",
238
259
  kind: "image",
@@ -691,6 +712,40 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
691
712
  { identifier: "seedream-5-lite-i2i:high", credits: 5, note: "high quality" },
692
713
  ],
693
714
  },
715
+ "seedream-5-pro": {
716
+ id: "seedream-5-pro",
717
+ kind: "image",
718
+ modes: ["t2i"] as const,
719
+ family: "Bytedance",
720
+ label: "Seedream 5 Pro",
721
+ series: "Seedream",
722
+ description: "Flagship Seedream 5 Pro — strongest instruction following and visual reasoning. Basic = 1K, high = 2K.",
723
+ useCases: ["realistic", "instruction"],
724
+ features: ["reference-image"],
725
+ aspectRatios: SEEDREAM_RATIOS,
726
+ qualities: ["basic", "high"],
727
+ pricing: [
728
+ { identifier: "seedream-5-pro", credits: 3, note: "basic / 1K default" },
729
+ { identifier: "seedream-5-pro:high", credits: 6, note: "high / 2K" },
730
+ ],
731
+ },
732
+ "seedream-5-pro-i2i": {
733
+ id: "seedream-5-pro-i2i",
734
+ kind: "image",
735
+ modes: ["i2i"] as const,
736
+ family: "Bytedance",
737
+ label: "Seedream 5 Pro (I2I)",
738
+ series: "Seedream",
739
+ description: "Image-to-image with Seedream 5 Pro — multi-reference instruction edits. Basic = 1K, high = 2K.",
740
+ useCases: ["edit", "instruction"],
741
+ features: ["reference-image"],
742
+ aspectRatios: SEEDREAM_RATIOS,
743
+ qualities: ["basic", "high"],
744
+ pricing: [
745
+ { identifier: "seedream-5-pro-i2i", credits: 3, note: "basic / 1K default" },
746
+ { identifier: "seedream-5-pro-i2i:high", credits: 6, note: "high / 2K" },
747
+ ],
748
+ },
694
749
 
695
750
  // ── Alibaba Qwen ──
696
751
  "qwen": {
@@ -1754,10 +1809,10 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1754
1809
  id: "gemini-3-flash-video-analysis",
1755
1810
  kind: "video",
1756
1811
  modes: ["video-analysis"] as const,
1757
- family: "Google",
1758
- label: "Video Analysis (Gemini 3 Flash)",
1759
- series: "Gemini",
1760
- description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast Gemini tier. Billed per duration bucket.",
1812
+ family: "Nodaro",
1813
+ label: "Video Analysis (Fast)",
1814
+ series: "Video Analysis",
1815
+ description: "Analyze a video into a structured shot list (scenes, camera, audio) — fast, economy tier. Billed per duration bucket.",
1761
1816
  useCases: ["video-analysis", "shot-list", "fast"],
1762
1817
  pricing: [
1763
1818
  { identifier: "video-analysis:gemini-3-flash", credits: 3, note: "10-min ceiling (no duration given)" },
@@ -1771,10 +1826,10 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1771
1826
  id: "gemini-3.1-pro-video-analysis",
1772
1827
  kind: "video",
1773
1828
  modes: ["video-analysis"] as const,
1774
- family: "Google",
1775
- label: "Video Analysis (Gemini 3.1 Pro)",
1776
- series: "Gemini",
1777
- description: "Analyze a video into a structured shot list (scenes, camera, audio) — high-fidelity Gemini tier. Billed per duration bucket.",
1829
+ family: "Nodaro",
1830
+ label: "Video Analysis (Pro)",
1831
+ series: "Video Analysis",
1832
+ description: "Analyze a video into a structured shot list (scenes, camera, audio) — higher-fidelity, default tier. Billed per duration bucket.",
1778
1833
  useCases: ["video-analysis", "shot-list", "cinematic"],
1779
1834
  pricing: [
1780
1835
  { identifier: "video-analysis:gemini-3.1-pro", credits: 11, note: "10-min ceiling (no duration given)" },
@@ -37,6 +37,7 @@ export const IMAGE_PROMPT_MAX = 5000
37
37
  export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
38
38
  // ── higher than the 5000 default ──
39
39
  "nano-banana-2": 20000, // docs.kie.ai/market/google/nano-banana-2
40
+ "nano-banana-2-lite": 20000, // docs.kie.ai/market/google/nano-banana-2-lite
40
41
  "nano-banana-pro": 20000, // docs.kie.ai/market/google/pro-image-to-image
41
42
  "gpt-image-2-i2i": 20000, // docs.kie.ai/market/gpt/gpt-image-2-image-to-image
42
43
  // ── lower than the 5000 default (over-send risk if left at default) ──
@@ -48,7 +49,8 @@ export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
48
49
  "qwen-edit": 2000, // docs.kie.ai/market/qwen/image-edit
49
50
  // verified == 5000 default (no entry needed): imagen4(-fast/-ultra), nano-banana,
50
51
  // nano-banana-edit, flux, flux-flex, gpt-image-2, ideogram-v3/-edit/-remix,
51
- // z-image, grok, qwen-i2i.
52
+ // z-image, grok, qwen-i2i, seedream-5-pro, seedream-5-pro-i2i
53
+ // (docs.kie.ai/market/seedream/5-pro-text-to-image + 5-pro-image-to-image).
52
54
  // grok-i2i: doc states 390000 (78× its t2i sibling) — treated as a KIE schema
53
55
  // typo and left at the 5000 default per the sanity-cap decision.
54
56
  // UNVERIFIED (no limit stated in schema) → 5000 default: flux-kontext(-max)
@@ -93,6 +95,18 @@ export const SUNO_TEXT_MAX = 5000
93
95
  */
94
96
  export const PROMPT_HARD_CEILING = 20000
95
97
 
98
+ /**
99
+ * Ceiling for LLM TEXT-generation node inputs (the "Generate Text"/llm-chat node
100
+ * + AI Writer + Generate Script `systemPrompt`/`userInput`/`prompt` fields).
101
+ * Distinct from PROMPT_HARD_CEILING (an image/video PROMPT budget): these inputs
102
+ * go straight into an LLM whose context is huge (Claude 200K / GPT 128K+ /
103
+ * Gemini 1M tokens), so the old flat 10000 was a false blocker on pasting a
104
+ * document to summarize or rewrite. 100000 chars (~25K input tokens) covers
105
+ * long-form inputs while still bounding abuse; the OUTPUT stays capped by each
106
+ * route's maxTokens.
107
+ */
108
+ export const LLM_TEXT_INPUT_MAX = 100_000
109
+
96
110
  /**
97
111
  * Per-provider maximum VIDEO prompt length (chars), VERIFIED against each model's
98
112
  * official docs.kie.ai schema (2026-06). Absent → {@link VIDEO_PROMPT_MAX} (8000)
@@ -333,6 +347,7 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
333
347
  "nano-banana",
334
348
  "nano-banana-pro",
335
349
  "nano-banana-2",
350
+ "nano-banana-2-lite",
336
351
  // T2I providers that auto-route to their i2i sibling when refs are attached
337
352
  "gpt-image",
338
353
  "gpt-image-2",
@@ -340,6 +355,7 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
340
355
  "qwen",
341
356
  "seedream",
342
357
  "seedream-5-lite",
358
+ "seedream-5-pro",
343
359
  "flux",
344
360
  "flux-flex",
345
361
  // Image editing / image-to-image (reference = source image)
@@ -357,6 +373,7 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
357
373
  "qwen-edit",
358
374
  "seedream-edit",
359
375
  "seedream-5-lite-i2i",
376
+ "seedream-5-pro-i2i",
360
377
  "grok-i2i",
361
378
  // Upscale / background ops (source acts as the reference)
362
379
  "recraft-remove-bg",
@@ -386,6 +403,7 @@ export const T2I_TO_I2I_VARIANT: Record<string, string> = {
386
403
  "qwen": "qwen-i2i",
387
404
  "seedream": "seedream-edit",
388
405
  "seedream-5-lite": "seedream-5-lite-i2i",
406
+ "seedream-5-pro": "seedream-5-pro-i2i",
389
407
  "flux": "flux-pro-i2i",
390
408
  "flux-flex": "flux-i2i",
391
409
  }
@@ -405,6 +423,7 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
405
423
  "nano-banana": 8,
406
424
  "nano-banana-pro": 8,
407
425
  "nano-banana-2": 4,
426
+ "nano-banana-2-lite": 10,
408
427
  "wan-2.7": 9,
409
428
  // Image-to-image (multi-source array)
410
429
  "nano-banana-edit": 8,
@@ -414,6 +433,7 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
414
433
  "flux-pro-i2i": 4,
415
434
  "seedream-edit": 16,
416
435
  "seedream-5-lite-i2i": 16,
436
+ "seedream-5-pro-i2i": 16,
417
437
  // Single-source i2i (one input image)
418
438
  "flux-kontext": 1,
419
439
  "flux-kontext-max": 1,
@@ -484,6 +504,8 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
484
504
  "seedream-edit": "quality",
485
505
  "seedream-5-lite": "quality",
486
506
  "seedream-5-lite-i2i": "quality",
507
+ "seedream-5-pro": "quality",
508
+ "seedream-5-pro-i2i": "quality",
487
509
  "topaz-image-upscale": "resolution",
488
510
  "ideogram-edit": "rendering-speed",
489
511
  "ideogram-remix": "rendering-speed",
@@ -495,7 +517,7 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
495
517
 
496
518
 
497
519
  // Models where quality=high triggers composite credit identifier
498
- export const HIGH_QUALITY_PROVIDERS = new Set(["gpt-image", "gpt-image-i2i", "seedream", "seedream-edit", "seedream-5-lite", "seedream-5-lite-i2i"])
520
+ export const HIGH_QUALITY_PROVIDERS = new Set(["gpt-image", "gpt-image-i2i", "seedream", "seedream-edit", "seedream-5-lite", "seedream-5-lite-i2i", "seedream-5-pro", "seedream-5-pro-i2i"])
499
521
 
500
522
  // Models where resolution=2K triggers composite credit identifier
501
523
  export const TWO_K_RESOLUTION_PROVIDERS = new Set(["flux", "flux-pro-i2i", "flux-flex", "flux-i2i"])
@@ -522,6 +544,7 @@ export const IMAGE_GEN_PROVIDERS = [
522
544
  "flux",
523
545
  "nano-banana-pro",
524
546
  "nano-banana-2",
547
+ "nano-banana-2-lite",
525
548
  "grok",
526
549
  "gpt-image",
527
550
  "gpt-image-2",
@@ -532,6 +555,7 @@ export const IMAGE_GEN_PROVIDERS = [
532
555
  "qwen",
533
556
  "seedream",
534
557
  "seedream-5-lite",
558
+ "seedream-5-pro",
535
559
  "flux-flex",
536
560
  "flux-kontext",
537
561
  "flux-kontext-max",
@@ -548,6 +572,7 @@ export const IMAGE_GEN_PROVIDERS = [
548
572
  export const IMAGE_I2I_PROVIDERS = [
549
573
  "nano-banana",
550
574
  "nano-banana-2",
575
+ "nano-banana-2-lite",
551
576
  "nano-banana-pro",
552
577
  "grok-i2i",
553
578
  "flux-i2i",
@@ -561,6 +586,7 @@ export const IMAGE_I2I_PROVIDERS = [
561
586
  "qwen-edit",
562
587
  "seedream-edit",
563
588
  "seedream-5-lite-i2i",
589
+ "seedream-5-pro-i2i",
564
590
  "flux-kontext",
565
591
  "flux-kontext-max",
566
592
  // Replicate Open (uncensored) — multi-image Kontext via Replicate
@@ -1003,10 +1029,12 @@ export const IMAGE_MASK_MODE: Record<ImageGenProvider, ImageMaskMode> = {
1003
1029
  "nano-banana": "prompt",
1004
1030
  "nano-banana-pro": "prompt",
1005
1031
  "nano-banana-2": "prompt",
1032
+ "nano-banana-2-lite": "prompt",
1006
1033
  "gpt-image": "prompt",
1007
1034
  "gpt-image-2": "prompt",
1008
1035
  "seedream": "prompt",
1009
1036
  "seedream-5-lite": "prompt",
1037
+ "seedream-5-pro": "prompt",
1010
1038
  "qwen": "prompt",
1011
1039
  "flux-kontext": "prompt",
1012
1040
  "flux-kontext-max": "prompt",
@@ -1129,6 +1157,29 @@ export const SEEDANCE_2_REF_LIMITS = {
1129
1157
  audio: 3,
1130
1158
  } as const
1131
1159
 
1160
+ /**
1161
+ * KIE r2v REFERENCE-VIDEO MINIMUM (seconds) for the Seedance 2.0 family — the
1162
+ * provider hard-rejects shorter reference clips with a 400 BEFORE generation:
1163
+ * "the parameter video duration (seconds) specified in the request must be
1164
+ * greater than or equal to 1.8 for model dreamina-seedance-2-0-fast in r2v"
1165
+ * (2026-07-13, job dbf95612 — every 1.0s continuation tail failed
1166
+ * deterministically). Mirror of the reference-AUDIO maximum below.
1167
+ */
1168
+ export const SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC = 1.8
1169
+
1170
+ /**
1171
+ * The continuation-reference length (seconds) every Seedance-2 chaining
1172
+ * feature actually cuts — extend-node tails, generate-video-pro segment
1173
+ * tails, edit-video-pro refOut/refIn brackets — AND the length the gvp/evp
1174
+ * credit formulas bill per continuation join. Clears the provider floor
1175
+ * above with margin while staying short enough to keep the model focused on
1176
+ * continuing the boundary motion instead of re-staging the whole clip.
1177
+ * Guarded ≥ floor by model-constants tests; the private-plugin twin
1178
+ * (nodaro-cloud-plugins chain.ts TAIL_SEC / bridge-math.ts MIN_REF) is
1179
+ * guarded by that repo's r2v-ref-floor.test.ts — keep the two in sync.
1180
+ */
1181
+ export const SEEDANCE_2_CONTINUATION_REF_SEC = 2
1182
+
1132
1183
  /**
1133
1184
  * Trim-stitch parameters for the seedance-2-extend provider (spike-validated
1134
1185
  * 2026-06-11):
@@ -1139,12 +1190,19 @@ export const SEEDANCE_2_REF_LIMITS = {
1139
1190
  * boundary clicks without shifting sync (combineVideos cut+crossfade path).
1140
1191
  */
1141
1192
  export const SEEDANCE_2_EXTEND_STITCH = {
1142
- /** Frames dropped from the END of the source clip. */
1193
+ /** Frames dropped from the END of the source clip (smart-cut FALLBACK —
1194
+ * used only when the PSNR boundary matcher finds no genuine match). */
1143
1195
  trimTailFrames: 4,
1144
- /** Frames dropped from the START of the generated extension. */
1196
+ /** Frames dropped from the START of the generated extension (smart-cut
1197
+ * fallback, see above). */
1145
1198
  trimHeadFrames: 3,
1146
1199
  /** Boundary audio fade length (seconds), timeline-preserving. */
1147
1200
  audioFadeSec: 0.15,
1201
+ /** Seconds of the source's TAIL passed as the @video_1 reference — short
1202
+ * keeps the model focused on continuing the boundary motion; the value is
1203
+ * floored by SEEDANCE_2_R2V_MIN_REF_VIDEO_SEC (the original spike's 1s
1204
+ * tail is now provider-rejected). */
1205
+ referenceTailSeconds: SEEDANCE_2_CONTINUATION_REF_SEC,
1148
1206
  } as const
1149
1207
 
1150
1208
  /**
@@ -174,6 +174,8 @@ const QUALITY_MAP: Record<string, QualityMapping> = {
174
174
  "seedream-edit": { field: "quality", values: { low: "basic", mid: "basic", high: "high" } },
175
175
  "seedream-5-lite": { field: "quality", values: { low: "basic", mid: "basic", high: "high" } },
176
176
  "seedream-5-lite-i2i": { field: "quality", values: { low: "basic", mid: "basic", high: "high" } },
177
+ "seedream-5-pro": { field: "quality", values: { low: "basic", mid: "basic", high: "high" } },
178
+ "seedream-5-pro-i2i": { field: "quality", values: { low: "basic", mid: "basic", high: "high" } },
177
179
  // Video gen — resolution-style (720p/1080p)
178
180
  "veo3": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
179
181
  "veo3.1": { field: "resolution", values: { low: "720p", mid: "1080p", high: "1080p" } },
@@ -248,6 +250,7 @@ const GENERATE_IMAGE_PROVIDER_TO_MODEL: Record<string, string> = {
248
250
  "nano-banana": "gemini-2.5-flash-image",
249
251
  "nano-banana-pro": "gemini-2.5-flash-image",
250
252
  "nano-banana-2": "gemini-2.5-flash-image",
253
+ "nano-banana-2-lite": "gemini-3.1-flash-lite-image",
251
254
  }
252
255
 
253
256
  export function deriveLinkedFields(
@@ -15,6 +15,12 @@ export const NODE_MAPPABLE_FIELDS: Readonly<Record<string, readonly string[]>> =
15
15
  // for back-compat with un-migrated workflow JSON). Mirrors text-to-video so
16
16
  // fieldMappings/{} injection AND missing-ref detection work on the live node.
17
17
  "generate-video": ["prompt", "negativePrompt"],
18
+ // Trimmed multi-segment stitch variant of generate-video — prompt only, no
19
+ // negativePrompt field on the node.
20
+ "generate-video-pro": ["prompt"],
21
+ // Span-replace sibling of generate-video-pro — prompt only, no
22
+ // negativePrompt field on the node.
23
+ "edit-video-pro": ["prompt"],
18
24
  "video-analysis": ["analysisFocus", "youtubeUrl"],
19
25
  "video-to-video": ["prompt"],
20
26
  "text-to-speech": ["directText"],
@@ -30,6 +30,16 @@ export const VIDEO_PRODUCER_TYPES: ReadonlySet<string> = new Set([
30
30
  // through to the imageUrl/videoUrl/audioUrl/text default and downstream consumers
31
31
  // could silently misroute the output.
32
32
  "generate-video",
33
+ // Generate Video Pro — Seedance-2-family multi-segment stitch variant of
34
+ // generate-video (same "emits videoUrl" contract; a trimmed provider +
35
+ // handle set). Must mirror generate-video here or its output can't connect
36
+ // downstream (the recurring "cannot connect the outputs" bug class).
37
+ "generate-video-pro",
38
+ // Edit Video Pro — Seedance-2-family span-replace sibling of generate-
39
+ // video-pro (same "emits videoUrl" contract; source video + prompt in,
40
+ // ONE video out). Must mirror generate-video-pro here or its output can't
41
+ // connect downstream (the recurring "cannot connect the outputs" bug class).
42
+ "edit-video-pro",
33
43
  "upload-video",
34
44
  "youtube-video",
35
45
  "combine-videos",
package/src/selector.ts CHANGED
@@ -706,7 +706,16 @@ export function applyRangeIndices(
706
706
  return result
707
707
  }
708
708
 
709
- /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer). */
709
+ /** mulberry32 PRNG — deterministic given a seed (any 32-bit signed integer).
710
+ *
711
+ * TWIN WARNING: backend/src/providers/video/audio-fx.ts carries its own
712
+ * private mulberry32 (cosmetically different — `>>> 0` seed masking — but the
713
+ * same sequence). The two are deliberately INDEPENDENT and both frozen: the
714
+ * audio-fx copy seeds every reverb impulse response that the committed
715
+ * characterization goldens (and every customer's rendered reverb waveform)
716
+ * are pinned to. Do NOT "deduplicate" them into one shared export or "sync"
717
+ * one to match the other — a change to either sequence is a silent behavior
718
+ * change in its domain. */
710
719
  function mulberry32(seed: number): () => number {
711
720
  let a = seed | 0
712
721
  return () => {
@@ -10,13 +10,16 @@
10
10
  * these.
11
11
  *
12
12
  * The measured-rate constants and the $-derived `videoAnalysisBucketCredits`
13
- * formula live in `backend/src/lib/pricing/video-analysis-cost.ts` (core,
14
- * not ee/ — the MCP tool description builder needs them regardless of
15
- * edition). They were moved out of this package (published Apache-2.0 on
16
- * npm an irrevocable grant) per the 2026-07-06 public-flip IP audit, S5.
13
+ * formula that GENERATE these numbers live PRIVATELY in the
14
+ * `@nodaroai/cloud-plugins` package (`src/plugins/video-analysis/cost.ts`)
15
+ * never in this public repo. They were first moved out of this package
16
+ * (published Apache-2.0 on npm) per the 2026-07-06 public-flip IP audit S5,
17
+ * then out of the app repo entirely alongside the rest of the video-analysis
18
+ * node. A cross-check test in that private package guards this table so the
19
+ * public numbers can't silently drift from the formula.
17
20
  *
18
21
  * `VIDEO_ANALYSIS_BUCKET_CREDITS` below is the precomputed OUTPUT of that
19
- * backend formula for every (model × bucket) combination — a plain credit
22
+ * private formula for every (model × bucket) combination — a plain credit
20
23
  * lookup table, not a formula, mirroring the same wire-contract pattern
21
24
  * `VIDEO_CLIP_CREDITS` uses in `film-pricing.ts`. It is what the frontend's
22
25
  * client-side cost preview (`estimateNodeCredits` in
@@ -34,11 +37,12 @@ const WINDOW_LEN = 150, WINDOW_STRIDE = 145, WINDOW_OVERLAP = 5
34
37
  export const VIDEO_ANALYSIS_WINDOW = { LEN: WINDOW_LEN, STRIDE: WINDOW_STRIDE, OVERLAP: WINDOW_OVERLAP, SINGLE_MAX: 180 } as const
35
38
 
36
39
  /**
37
- * Precomputed credit cost per (model, bucket) — the OUTPUT of the backend's
38
- * `videoAnalysisBucketCredits` formula, not a formula itself. Regenerate by
39
- * running that function for every `VIDEO_ANALYSIS_LLM_MODELS` × duration
40
- * bucket combination whenever the underlying rate/token constants change
41
- * (backend test guards drift). Keep in sync with
40
+ * Precomputed credit cost per (model, bucket) — the OUTPUT of the private
41
+ * `videoAnalysisBucketCredits` formula (in `@nodaroai/cloud-plugins`), not a
42
+ * formula itself. Regenerate by running that function for every
43
+ * `VIDEO_ANALYSIS_LLM_MODELS` × duration bucket combination whenever the
44
+ * underlying rate/token constants change (the plugin's cost test guards drift).
45
+ * Keep in sync with
42
46
  * `docs/nodes/processing-video/video-analysis.md`.
43
47
  */
44
48
  export const VIDEO_ANALYSIS_BUCKET_CREDITS: Record<string, number> = {
@@ -34,11 +34,18 @@ export const entitySlotSchema = z.object({
34
34
  })
35
35
  export type EntitySlot = z.infer<typeof entitySlotSchema>
36
36
 
37
- const audioSchema = z.object({
38
- mode: z.enum(["speech", "music", "sfx", "silence"]),
39
- content: z.string(), // speech: verbatim quote; music/sfx: gen-ready description; silence: "" allowed
37
+ /**
38
+ * One concurrent sound layer in a scene. Real footage stacks sound (music bed
39
+ * under dialogue over ambient sfx), so a scene carries an ARRAY of these an
40
+ * empty array means genuine silence. `content`: speech = verbatim words;
41
+ * music/sfx = gen-ready description. `voice` is speech-only voice-casting.
42
+ */
43
+ const audioLayerSchema = z.object({
44
+ mode: z.enum(["speech", "music", "sfx"]),
45
+ content: z.string().min(1),
40
46
  voice: z.string().optional(),
41
47
  })
48
+ export type AudioLayer = z.infer<typeof audioLayerSchema>
42
49
 
43
50
  const windowSceneBase = z.object({
44
51
  startSec: z.number().min(0),
@@ -48,7 +55,8 @@ const windowSceneBase = z.object({
48
55
  camera: z.string(),
49
56
  visual: z.string().min(1),
50
57
  transitionOut: z.enum(["cut", "fade", "wipe", "whip"]).optional(),
51
- audio: audioSchema,
58
+ // Array of concurrent layers (music + speech + sfx together); [] = silence.
59
+ audio: z.array(audioLayerSchema),
52
60
  })
53
61
  // .strip() (default) drops model-emitted oversized/slotRefs — validator-computed only.
54
62
  const windowSceneSchema = windowSceneBase.refine((s) => s.endSec > s.startSec, { message: "endSec must be > startSec" })