@nodaro/shared 2.20.0 → 2.21.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 CHANGED
@@ -55,7 +55,7 @@ var MODEL_RECOMMENDATIONS = [
55
55
  { intent: "cheapest realistic image", modelIds: ["z-image", "qwen", "imagen4-fast"], note: "Z-Image is the cheapest. Qwen / Imagen4 Fast for slightly higher quality." },
56
56
  { intent: "highest fidelity image", modelIds: ["nano-banana-pro", "imagen4-ultra", "flux-flex"], note: "Pick by family preference; all three are premium tiers." },
57
57
  { 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)." },
58
- { 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." },
58
+ { intent: "highest-resolution image", modelIds: ["topaz-image-upscale", "nano-banana-pro", "gpt-image-2"], note: "Generate at the model's top tier, then Topaz upscale 4x (Topaz's only lever is the 1x/2x/4x factor)." },
59
59
  { intent: "background removal / cutout", modelIds: ["recraft-remove-bg"], note: "Cheap, no prompt needed." },
60
60
  // video
61
61
  { intent: "best cinematic video", modelIds: ["veo3", "kling-3.0", "seedance-2"], note: "VEO 3.1 Quality for premium narrative; Kling 3.0 for music-synced motion; Seedance 2 for reference-driven consistency." },
@@ -398,7 +398,8 @@ var IMAGE_MODELS = {
398
398
  { identifier: "gpt-image-2", credits: 15, note: "1K default" },
399
399
  { identifier: "gpt-image-2:2K", credits: 30, note: "2K" },
400
400
  { identifier: "gpt-image-2:4K", credits: 60, note: "4K" }
401
- ]
401
+ ],
402
+ safetyFilter: { stochastic: true, fallback: "nano-banana-pro" }
402
403
  },
403
404
  "gpt-image-2-i2i": {
404
405
  id: "gpt-image-2-i2i",
@@ -818,14 +819,15 @@ var IMAGE_MODELS = {
818
819
  family: "Topaz",
819
820
  label: "Topaz Image Upscale",
820
821
  series: "Topaz",
821
- description: "High-quality image upscale up to 8K. Best for production-ready output.",
822
+ description: "High-quality image upscale at 1x (enhance only), 2x or 4x. Best for production-ready output.",
822
823
  useCases: ["upscale", "high-res", "premium"],
823
824
  features: ["reference-image"],
824
- resolutions: ["2K", "4K", "8K"],
825
+ // No `resolutions`: the provider's only quality lever is `upscale_factor`
826
+ // (1/2/4) — see resolveTopazUpscale. The 2K/4K/8K menu this used to
827
+ // advertise had no provider parameter behind it.
825
828
  pricing: [
826
- { identifier: "topaz-image-upscale", credits: 25, note: "2K default" },
827
- { identifier: "topaz-image-upscale:4K", credits: 50, note: "4K" },
828
- { identifier: "topaz-image-upscale:8K", credits: 100, note: "8K" }
829
+ { identifier: "topaz-image-upscale", credits: 25, note: "1x / 2x" },
830
+ { identifier: "topaz-image-upscale:4K", credits: 50, note: "4x (legacy identifier)" }
829
831
  ]
830
832
  }
831
833
  };
@@ -856,6 +858,7 @@ var VIDEO_MODELS = {
856
858
  // docs.kie.ai/market/minimax-h3/{text,image,reference}-to-video.
857
859
  "minimax-h3": {
858
860
  id: "minimax-h3",
861
+ unlistedResolutionRendersAs: "2K",
859
862
  kind: "video",
860
863
  modes: ["i2v", "t2v"],
861
864
  family: "MiniMax",
@@ -1341,6 +1344,7 @@ var VIDEO_MODELS = {
1341
1344
  // declared explicitly in PRICING_DEFAULT_RESOLUTION, never by array position.
1342
1345
  "wan-3": {
1343
1346
  id: "wan-3",
1347
+ unlistedResolutionRendersAs: "720p",
1344
1348
  kind: "video",
1345
1349
  modes: ["i2v", "t2v"],
1346
1350
  family: "Alibaba",
@@ -1365,6 +1369,7 @@ var VIDEO_MODELS = {
1365
1369
  },
1366
1370
  "wan-3-prime": {
1367
1371
  id: "wan-3-prime",
1372
+ unlistedResolutionRendersAs: "720p",
1368
1373
  kind: "video",
1369
1374
  modes: ["i2v", "t2v"],
1370
1375
  family: "Alibaba",
@@ -1533,6 +1538,76 @@ var VIDEO_MODELS = {
1533
1538
  { identifier: "wan-2.7-t2v", credits: 188, note: "5s 720p default" }
1534
1539
  ]
1535
1540
  },
1541
+ // Lightricks LTX 2.3 (Replicate, not KIE). Both variants run one endpoint per
1542
+ // variant and switch behaviour with a `task` discriminator, so t2v and i2v are
1543
+ // the SAME id — no VIDEO_MODE_ALIASES row. `extend` / `retake` are separate
1544
+ // priced ops on the same model (ids below) but are NOT listed in `modes`:
1545
+ // they are driven by the Extend Video / Video Retake nodes' own pickers, and
1546
+ // adding the mode here would duplicate LTX in those menus.
1547
+ // Bands are lowercase to match LTX_DURATION_TIERS' keys in credit-identifiers.ts
1548
+ // and QUALITY_MAP in node-default-mappings.ts.
1549
+ "ltx-2.3-pro": {
1550
+ id: "ltx-2.3-pro",
1551
+ kind: "video",
1552
+ modes: ["i2v", "t2v"],
1553
+ family: "Lightricks",
1554
+ label: "LTX 2.3 Pro",
1555
+ series: "LTX",
1556
+ description: "Lightricks LTX 2.3 Pro \u2014 text/image/audio\u2192video up to 4K, 6/8/10s, end-frame interpolation.",
1557
+ useCases: ["premium", "high-res", "narrative"],
1558
+ features: ["end-frame"],
1559
+ aspectRatios: ["16:9", "9:16"],
1560
+ resolutions: ["1080p", "2k", "4k"],
1561
+ durations: [6, 8, 10],
1562
+ pricing: [
1563
+ { identifier: "ltx-2.3-pro", credits: 240, note: "default 1080p 6s" },
1564
+ { identifier: "ltx-2.3-pro:1080p:6s", credits: 240 },
1565
+ { identifier: "ltx-2.3-pro:1080p:8s", credits: 320 },
1566
+ { identifier: "ltx-2.3-pro:1080p:10s", credits: 400 },
1567
+ { identifier: "ltx-2.3-pro:2k:6s", credits: 480 },
1568
+ { identifier: "ltx-2.3-pro:2k:8s", credits: 640 },
1569
+ { identifier: "ltx-2.3-pro:2k:10s", credits: 800 },
1570
+ { identifier: "ltx-2.3-pro:4k:6s", credits: 960 },
1571
+ { identifier: "ltx-2.3-pro:4k:8s", credits: 1280 },
1572
+ { identifier: "ltx-2.3-pro:4k:10s", credits: 1600 },
1573
+ { identifier: "ltx-2.3-pro-extend:per-second", credits: 40, note: "extend, per second of new footage" },
1574
+ { identifier: "ltx-2.3-pro-retake:per-second", credits: 40, note: "retake, per second re-rendered" }
1575
+ ]
1576
+ },
1577
+ "ltx-2.3-fast": {
1578
+ id: "ltx-2.3-fast",
1579
+ kind: "video",
1580
+ modes: ["i2v", "t2v"],
1581
+ family: "Lightricks",
1582
+ label: "LTX 2.3 Fast",
1583
+ series: "LTX",
1584
+ description: "Lightricks LTX 2.3 Fast \u2014 text/image\u2192video up to 20s at 1080p (6/8/10s at 2K and 4K). No audio input, no extend.",
1585
+ useCases: ["long-form", "fast", "narrative"],
1586
+ features: ["end-frame"],
1587
+ aspectRatios: ["16:9", "9:16"],
1588
+ resolutions: ["1080p", "2k", "4k"],
1589
+ // Flat union across bands — 12–20s exist only at 1080p (LTX_DURATION_TIERS
1590
+ // in credit-identifiers.ts is the per-band authority and snaps a 2k/4k
1591
+ // request back onto 6/8/10s, so the reservation is always a real tier).
1592
+ durations: [6, 8, 10, 12, 14, 16, 18, 20],
1593
+ pricing: [
1594
+ { identifier: "ltx-2.3-fast", credits: 180, note: "default 1080p 6s" },
1595
+ { identifier: "ltx-2.3-fast:1080p:6s", credits: 180 },
1596
+ { identifier: "ltx-2.3-fast:1080p:8s", credits: 240 },
1597
+ { identifier: "ltx-2.3-fast:1080p:10s", credits: 300 },
1598
+ { identifier: "ltx-2.3-fast:1080p:12s", credits: 360 },
1599
+ { identifier: "ltx-2.3-fast:1080p:14s", credits: 420 },
1600
+ { identifier: "ltx-2.3-fast:1080p:16s", credits: 480 },
1601
+ { identifier: "ltx-2.3-fast:1080p:18s", credits: 540 },
1602
+ { identifier: "ltx-2.3-fast:1080p:20s", credits: 600 },
1603
+ { identifier: "ltx-2.3-fast:2k:6s", credits: 360 },
1604
+ { identifier: "ltx-2.3-fast:2k:8s", credits: 480 },
1605
+ { identifier: "ltx-2.3-fast:2k:10s", credits: 600 },
1606
+ { identifier: "ltx-2.3-fast:4k:6s", credits: 720 },
1607
+ { identifier: "ltx-2.3-fast:4k:8s", credits: 960 },
1608
+ { identifier: "ltx-2.3-fast:4k:10s", credits: 1200 }
1609
+ ]
1610
+ },
1536
1611
  // ── HappyHorse (1.1 — ids kept version-less; repointed in place when KIE
1537
1612
  // delisted 1.0, same param surface, so saved workflows keep working) ──
1538
1613
  "happyhorse": {
@@ -2384,6 +2459,105 @@ function normalizeModelInput(modelId, input) {
2384
2459
  }
2385
2460
  return out;
2386
2461
  }
2462
+ var PASSTHROUGH_ASPECT_TOKENS = /* @__PURE__ */ new Set(["auto", "adaptive"]);
2463
+ var RESOLUTION_BAND_PIXELS = {
2464
+ "2k": 1440,
2465
+ "4k": 2160,
2466
+ "8k": 4320
2467
+ };
2468
+ function bandPixels(token) {
2469
+ const t2 = token.trim().toLowerCase();
2470
+ if (RESOLUTION_BAND_PIXELS[t2] !== void 0) return RESOLUTION_BAND_PIXELS[t2];
2471
+ const m = /^(\d+)p$/.exec(t2);
2472
+ return m ? Number(m[1]) : void 0;
2473
+ }
2474
+ function nearestResolutionBand(token, allowed) {
2475
+ let highest = allowed[allowed.length - 1];
2476
+ let highestPx = -Infinity;
2477
+ for (const a of allowed) {
2478
+ const px = bandPixels(a);
2479
+ if (px !== void 0 && px > highestPx) {
2480
+ highestPx = px;
2481
+ highest = a;
2482
+ }
2483
+ }
2484
+ const want = bandPixels(token);
2485
+ if (want === void 0) return highest;
2486
+ let best = highest;
2487
+ let bestDist = Infinity;
2488
+ for (const a of allowed) {
2489
+ const px = bandPixels(a);
2490
+ if (px === void 0) continue;
2491
+ const d = Math.abs(px - want);
2492
+ if (d < bestDist) {
2493
+ bestDist = d;
2494
+ best = a;
2495
+ }
2496
+ }
2497
+ return best;
2498
+ }
2499
+ function nearestAspectRatio(token, allowed) {
2500
+ const [w, h] = token.split(":").map(Number);
2501
+ if (!w || !h) return void 0;
2502
+ const target = Math.log(w / h);
2503
+ let best;
2504
+ let bestDist = Infinity;
2505
+ for (const c of allowed) {
2506
+ const [cw, ch] = c.split(":").map(Number);
2507
+ if (!cw || !ch) continue;
2508
+ const d = Math.abs(Math.log(cw / ch) - target);
2509
+ if (d < bestDist) {
2510
+ bestDist = d;
2511
+ best = c;
2512
+ }
2513
+ }
2514
+ return best;
2515
+ }
2516
+ function readOptionToken(value) {
2517
+ if (value === void 0 || value === null) return void 0;
2518
+ const s = (typeof value === "string" ? value : String(value)).trim();
2519
+ return s === "" ? void 0 : s;
2520
+ }
2521
+ function normalizeVideoRequestParams(modelId, input) {
2522
+ const m = MODEL_CATALOG[modelId];
2523
+ const aspect = readOptionToken(input.aspectRatio);
2524
+ const res = readOptionToken(input.resolution);
2525
+ const out = { aspectRatio: aspect, resolution: res, adjustments: [] };
2526
+ if (!m) return out;
2527
+ if (m.aspectRatios?.length && aspect && !PASSTHROUGH_ASPECT_TOKENS.has(aspect.toLowerCase())) {
2528
+ const allowed = m.aspectRatios;
2529
+ const exact = allowed.find((a) => a === aspect) ?? allowed.find((a) => a.toLowerCase() === aspect.toLowerCase());
2530
+ if (exact !== void 0) {
2531
+ out.aspectRatio = exact;
2532
+ } else {
2533
+ const next = nearestAspectRatio(aspect, allowed.filter((a) => !PASSTHROUGH_ASPECT_TOKENS.has(a.toLowerCase()))) ?? allowed[0];
2534
+ out.adjustments.push({
2535
+ field: "aspectRatio",
2536
+ from: aspect,
2537
+ to: next,
2538
+ reason: `${m.label} does not support aspect ratio "${aspect}" \u2014 using "${next}" instead. Supported: ${allowed.join(", ")}.`
2539
+ });
2540
+ out.aspectRatio = next;
2541
+ }
2542
+ }
2543
+ if (m.resolutions?.length && res) {
2544
+ const allowed = m.resolutions;
2545
+ const exact = allowed.find((a) => a === res) ?? allowed.find((a) => a.toLowerCase() === res.toLowerCase());
2546
+ if (exact !== void 0) {
2547
+ out.resolution = exact;
2548
+ } else {
2549
+ const next = m.unlistedResolutionRendersAs ?? nearestResolutionBand(res, allowed);
2550
+ out.adjustments.push({
2551
+ field: "resolution",
2552
+ from: res,
2553
+ to: next,
2554
+ reason: `${m.label} does not support resolution "${res}" \u2014 using "${next}" instead. Supported: ${allowed.join(", ")}.`
2555
+ });
2556
+ out.resolution = next;
2557
+ }
2558
+ }
2559
+ return out;
2560
+ }
2387
2561
  var MODEL_VALUE_LABELS = {
2388
2562
  // aspect ratios
2389
2563
  "1:1": "1:1 (Square)",
@@ -2398,6 +2572,7 @@ var MODEL_VALUE_LABELS = {
2398
2572
  "2K": "2K (High)",
2399
2573
  "4K": "4K (Ultra)",
2400
2574
  "4k": "4K",
2575
+ "2k": "2K",
2401
2576
  "8K": "8K (Ultra)",
2402
2577
  // qualities
2403
2578
  "medium": "Medium (Balanced)",
@@ -3583,6 +3758,39 @@ var VIDEO_REF_LIMITS_BY_PROVIDER = {
3583
3758
  // the user's references (the grok-imagine-video-1.5 bug). Add only with a
3584
3759
  // verified provider path + the catalog `reference-image` feature.
3585
3760
  };
3761
+ var VIDEO_REF_VIDEO_DURATION_LIMITS = {
3762
+ "seedance-2-5": { minSec: 2, maxSec: 30, maxTotalSec: 30 },
3763
+ // MiniMax Hailuo 3 — the provider states the per-clip bound in its own reject
3764
+ // text: "content[1].video_url: invalid param: video duration 52838 ms,
3765
+ // expected [2000, 15000] ms" (app-reports P4, 2 rows, the same clip retried).
3766
+ // The KIE reference-to-video doc adds the COMBINED cap — the same shape the
3767
+ // audio side already declares in SEEDANCE_2_R2V_MAX_AUDIO_SEC_BY_PROVIDER
3768
+ // above, whose h3 row cites the same page.
3769
+ "minimax-h3": { minSec: 2, maxSec: 15, maxTotalSec: 15 }
3770
+ };
3771
+ function checkRefVideoDurations(provider, durationsSec) {
3772
+ const limit = VIDEO_REF_VIDEO_DURATION_LIMITS[provider];
3773
+ if (!limit) return { ok: true };
3774
+ const usable = durationsSec.filter((d) => Number.isFinite(d) && d > 0);
3775
+ if (usable.length === 0) return { ok: true };
3776
+ const offender = usable.find((d) => d < limit.minSec || d > limit.maxSec);
3777
+ if (offender !== void 0) {
3778
+ return {
3779
+ ok: false,
3780
+ message: `Each reference video must be between ${limit.minSec} and ${limit.maxSec} seconds \u2014 one is ${offender.toFixed(1)}s. Trim it (a Trim Video node upstream works) and run again.`
3781
+ };
3782
+ }
3783
+ if (limit.maxTotalSec !== void 0) {
3784
+ const total = usable.reduce((a, b) => a + b, 0);
3785
+ if (total > limit.maxTotalSec) {
3786
+ return {
3787
+ ok: false,
3788
+ message: `Reference videos must not exceed ${limit.maxTotalSec} seconds in total \u2014 these add up to ${total.toFixed(1)}s. Remove one or trim them and run again.`
3789
+ };
3790
+ }
3791
+ }
3792
+ return { ok: true };
3793
+ }
3586
3794
  var RESOLUTION_VIDEO_REF_PRICING = SEEDANCE_2_PROVIDERS;
3587
3795
  var RESOLUTION_DURATION_PRICING = {
3588
3796
  // KIE supports only 480p/720p here; 480p is the default.
@@ -3676,10 +3884,39 @@ var VIDEO_AUDIO_CAPABILITY = {
3676
3884
  // and skip the lip-sync pass. `defaultOn` mirrors the KIE default so an
3677
3885
  // intent-less request is described honestly; audio is priced into the uniform
3678
3886
  // per-second rate, so NOT cost-affecting (no `:audio` composite).
3679
- // gemini-omni-flash is deliberately ABSENT — gemini-omni-video is absent too
3680
- // (mode "none"), and the siblings must not disagree.
3681
3887
  "wan-3": { mode: "ambient", field: "audio", defaultOn: true },
3682
- "wan-3-prime": { mode: "ambient", field: "audio", defaultOn: true }
3888
+ "wan-3-prime": { mode: "ambient", field: "audio", defaultOn: true },
3889
+ // Gemini Omni (both SKUs — the pro `gemini-omni-video` and the faster
3890
+ // `gemini-omni-flash`; one model at two speeds, so their rows are identical
3891
+ // and a test pins that). Settled 2026-09-03 from Google's own documentation
3892
+ // at https://ai.google.dev/gemini-api/docs/omni, having been unlisted — and
3893
+ // therefore reported as SILENT — while the catalog described both as "native
3894
+ // audio".
3895
+ //
3896
+ // "ambient", and alwaysOn with no toggle field, on three sentences from that
3897
+ // page:
3898
+ // - "By default the model will try to generate an appropriate audio track
3899
+ // for a video." Audio on every render, and the KIE input schema
3900
+ // (prompt / image_urls / first+last_frame_url / audio_ids / video_list /
3901
+ // character_ids / duration / aspect_ratio / seed / resolution — see
3902
+ // docs.kie.ai/market/gemini-omni-video and
3903
+ // docs.kie.ai/market/google/gemini-omni-flash-1-1) carries no on/off
3904
+ // lever, so there is nothing for applyVideoAudioToggle to write.
3905
+ // - NOT native_speech: "Multi-turn voice extension: Generating spoken
3906
+ // dialogue or speech is supported when extending previously generated
3907
+ // videos via multi-turn (`previous_interaction_id`)" — a field KIE's
3908
+ // createTask schema does not expose, so the dialogue path is unreachable
3909
+ // on our transport. Held to the Wan 3.0 bar: no documented dialogue
3910
+ // guarantee on the path we actually drive ⇒ ambient, and upgrade only on
3911
+ // a live probe (the kling-3.0 standard). Classifying it native_speech
3912
+ // would reroute the Story→Video dialogue pipeline past the lip-sync pass.
3913
+ // - NOT audio_driven either: "Uploading audio references is unsupported in
3914
+ // the current version of the API", and "any audio in a video reference is
3915
+ // ignored" — there is no reference-audio transport to be driven by, and
3916
+ // runGeminiOmni never sends `audio_ids`.
3917
+ // Audio is priced into the per-tier rate, so NOT cost-affecting.
3918
+ "gemini-omni-video": { mode: "ambient", alwaysOn: true },
3919
+ "gemini-omni-flash": { mode: "ambient", alwaysOn: true }
3683
3920
  };
3684
3921
  var VIDEO_AUDIO_NONE = { mode: "none" };
3685
3922
  function getVideoAudioCapability(model) {
@@ -4487,6 +4724,16 @@ var LTX_DURATION_TIERS = {
4487
4724
  "ltx-2.3-pro": { "1080p": [6, 8, 10], "2k": [6, 8, 10], "4k": [6, 8, 10] },
4488
4725
  "ltx-2.3-fast": { "1080p": [6, 8, 10, 12, 14, 16, 18, 20], "2k": [6, 8, 10], "4k": [6, 8, 10] }
4489
4726
  };
4727
+ function ltxPricedTier(provider, resolution, duration) {
4728
+ const bands = LTX_DURATION_TIERS[provider];
4729
+ if (!bands) return void 0;
4730
+ const band2 = bands[String(resolution)] ? String(resolution) : "1080p";
4731
+ const allowed = bands[band2];
4732
+ const raw = typeof duration === "string" ? parseInt(duration, 10) : duration ?? allowed[0];
4733
+ const want = Number.isNaN(raw) ? allowed[0] : raw;
4734
+ const dur = allowed.reduce((b, a) => Math.abs(a - want) < Math.abs(b - want) ? a : b);
4735
+ return { band: band2, duration: dur };
4736
+ }
4490
4737
  function buildVideoCreditModelIdentifier(provider, duration, sound, nodeType, mode, resolution, hasVideoRef) {
4491
4738
  let effectiveProvider = provider;
4492
4739
  if (nodeType === "text-to-video") {
@@ -4512,14 +4759,9 @@ function buildVideoCreditModelIdentifier(provider, duration, sound, nodeType, mo
4512
4759
  const d = Number.isNaN(raw) ? 8 : GEMINI_OMNI_DURATIONS.reduce((b, a) => Math.abs(a - raw) < Math.abs(b - raw) ? a : b);
4513
4760
  return resolution === "4k" ? `${effectiveProvider}:4k:${d}` : `${effectiveProvider}:${d}`;
4514
4761
  }
4515
- if (effectiveProvider === "ltx-2.3-pro" || effectiveProvider === "ltx-2.3-fast") {
4516
- const bands = LTX_DURATION_TIERS[effectiveProvider];
4517
- const band2 = bands[String(resolution)] ? String(resolution) : "1080p";
4518
- const allowed = bands[band2];
4519
- const raw = typeof duration === "string" ? parseInt(duration, 10) : duration ?? allowed[0];
4520
- const want = Number.isNaN(raw) ? allowed[0] : raw;
4521
- const dur = allowed.reduce((b, a) => Math.abs(a - want) < Math.abs(b - want) ? a : b);
4522
- return `${effectiveProvider}:${band2}:${dur}s`;
4762
+ const ltxTier = ltxPricedTier(effectiveProvider, resolution, duration);
4763
+ if (ltxTier) {
4764
+ return `${effectiveProvider}:${ltxTier.band}:${ltxTier.duration}s`;
4523
4765
  }
4524
4766
  if (!DURATION_PRICED_PROVIDERS.has(effectiveProvider)) {
4525
4767
  return effectiveProvider;
@@ -4559,6 +4801,22 @@ function buildVideoCreditModelIdentifier(provider, duration, sound, nodeType, mo
4559
4801
  }
4560
4802
  return identifier;
4561
4803
  }
4804
+ function pricedVideoSelection(opts) {
4805
+ const adjustments = [];
4806
+ const ltx = ltxPricedTier(opts.provider, opts.resolution, opts.duration);
4807
+ const resolution = opts.resolution ?? (ltx ? ltx.band : PRICING_DEFAULT_RESOLUTION[opts.provider]);
4808
+ if (!ltx) return { resolution, adjustments };
4809
+ const requested = typeof opts.duration === "string" ? parseInt(opts.duration, 10) : opts.duration;
4810
+ if (requested !== void 0 && !Number.isNaN(requested) && requested !== ltx.duration) {
4811
+ adjustments.push({
4812
+ field: "duration",
4813
+ from: requested,
4814
+ to: ltx.duration,
4815
+ reason: `LTX renders ${ltx.band} in ${ltx.duration}s steps \u2014 using ${ltx.duration}s instead of ${requested}s.`
4816
+ });
4817
+ }
4818
+ return { resolution, duration: ltx.duration, adjustments };
4819
+ }
4562
4820
  function buildMotionCreditModelIdentifier(provider, resolution, videoDuration) {
4563
4821
  if (provider === "wan-animate-move" || provider === "wan-animate-replace") {
4564
4822
  if (resolution === "580p" || resolution === "720p") {
@@ -4684,6 +4942,71 @@ function extractVideoDurationFromNode(data) {
4684
4942
  return void 0;
4685
4943
  }
4686
4944
 
4945
+ // src/topaz-upscale.ts
4946
+ var TOPAZ_UPSCALE_FACTORS = ["1", "2", "4"];
4947
+ var TOPAZ_DEFAULT_UPSCALE_FACTOR = "2";
4948
+ var LEGACY_TIER_TO_FACTOR = {
4949
+ "2K": "2",
4950
+ "4K": "4",
4951
+ "8K": "4"
4952
+ };
4953
+ function isFactor(v) {
4954
+ return TOPAZ_UPSCALE_FACTORS.includes(v);
4955
+ }
4956
+ function resolveTopazUpscale(input) {
4957
+ const adjustments = [];
4958
+ const rawFactor = typeof input.upscaleFactor === "string" ? input.upscaleFactor : "";
4959
+ const rawTier = typeof input.targetResolution === "string" ? input.targetResolution : "";
4960
+ const trimmedFactor = rawFactor.trim();
4961
+ const normalizedTier = rawTier.trim().toUpperCase();
4962
+ const factorGiven = trimmedFactor.length > 0;
4963
+ const factorValid = factorGiven && isFactor(trimmedFactor);
4964
+ const tierGiven = normalizedTier.length > 0;
4965
+ const tierMappedFactor = tierGiven ? LEGACY_TIER_TO_FACTOR[normalizedTier] : void 0;
4966
+ const finalFactor = factorValid ? trimmedFactor : tierMappedFactor ?? TOPAZ_DEFAULT_UPSCALE_FACTOR;
4967
+ if (factorGiven && !factorValid) {
4968
+ adjustments.push({
4969
+ field: "upscaleFactor",
4970
+ from: rawFactor,
4971
+ to: finalFactor,
4972
+ reason: `Topaz upscale accepts a factor of 1, 2 or 4 \u2014 "${rawFactor}" was replaced with ${finalFactor}.`
4973
+ });
4974
+ }
4975
+ if (factorValid && tierGiven) {
4976
+ if (tierMappedFactor !== finalFactor) {
4977
+ adjustments.push({
4978
+ field: "targetResolution",
4979
+ from: rawTier,
4980
+ to: void 0,
4981
+ reason: "upscaleFactor takes precedence over the legacy targetResolution."
4982
+ });
4983
+ }
4984
+ } else if (!factorValid && tierGiven) {
4985
+ if (tierMappedFactor) {
4986
+ if (normalizedTier === "8K") {
4987
+ adjustments.push({
4988
+ field: "targetResolution",
4989
+ from: rawTier,
4990
+ to: finalFactor,
4991
+ reason: "Topaz upscale offers factors up to 4x \u2014 an 8K target renders and bills at the 4x tier."
4992
+ });
4993
+ }
4994
+ } else {
4995
+ adjustments.push({
4996
+ field: "targetResolution",
4997
+ from: rawTier,
4998
+ to: finalFactor,
4999
+ reason: `Unknown Topaz target "${rawTier}" \u2014 rendering at ${finalFactor}x.`
5000
+ });
5001
+ }
5002
+ }
5003
+ return {
5004
+ upscaleFactor: finalFactor,
5005
+ creditTier: finalFactor === "4" ? "4K" : void 0,
5006
+ adjustments
5007
+ };
5008
+ }
5009
+
4687
5010
  // src/presentation-utils.ts
4688
5011
  var INPUT_NODE_TYPES = /* @__PURE__ */ new Set([
4689
5012
  "text-prompt",
@@ -6456,6 +6779,35 @@ function resolveNodeRefs(text, labelToOutput) {
6456
6779
  }
6457
6780
  return result;
6458
6781
  }
6782
+ var REF_TOKEN_NAMESPACE_PREFIXES = [
6783
+ "image:",
6784
+ "video:",
6785
+ "audio:",
6786
+ "slot:",
6787
+ "ref:"
6788
+ ];
6789
+ function classifyRefToken(name, resolvable) {
6790
+ const lower = name.toLowerCase();
6791
+ if (name === "" || REF_TOKEN_NAMESPACE_PREFIXES.some((p) => lower.startsWith(p))) return "skip";
6792
+ if (RESERVED_TEMPLATE_VARS.has(name)) return "reserved";
6793
+ if (resolvable === null) return "unknown";
6794
+ return resolvable.has(canonicalVarName(name)) ? "wired" : "missing";
6795
+ }
6796
+ function unresolvedRefTokens(text, opts) {
6797
+ if (typeof text !== "string" || text.length === 0) return [];
6798
+ const seen = /* @__PURE__ */ new Set();
6799
+ const out = [];
6800
+ for (const m of text.matchAll(NODE_REF_PATTERN)) {
6801
+ const { name, fallback } = parseNodeRef(m[1] ?? "");
6802
+ if (fallback !== null) continue;
6803
+ if (classifyRefToken(name, opts.resolvable) !== "missing") continue;
6804
+ const canon = canonicalVarName(name);
6805
+ if (opts.known.has(canon) || seen.has(canon)) continue;
6806
+ seen.add(canon);
6807
+ out.push(name);
6808
+ }
6809
+ return out;
6810
+ }
6459
6811
 
6460
6812
  // src/filter-condition.ts
6461
6813
  function tryParseJson(item) {
@@ -9642,6 +9994,18 @@ function togglePick(current, id, maxSelected) {
9642
9994
  return [...current.slice(1), id];
9643
9995
  }
9644
9996
 
9997
+ // src/safety-retry-policy.ts
9998
+ function safetyRetryPolicy(modelId) {
9999
+ const entry = getModel(modelId);
10000
+ const safetyFilter = entry?.safetyFilter;
10001
+ if (!safetyFilter?.stochastic) return { maxAttempts: 1 };
10002
+ const fallback = safetyFilter.fallback;
10003
+ if (fallback && getModel(fallback)) {
10004
+ return { maxAttempts: 2, fallback };
10005
+ }
10006
+ return { maxAttempts: 2 };
10007
+ }
10008
+
9645
10009
  // src/caption-styles.ts
9646
10010
  var STATIC_CAPTION_STYLES = ["subtitle"];
9647
10011
  var KINETIC_CAPTION_STYLES = [
@@ -9782,6 +10146,11 @@ var EXECUTION_DATA_KEYS = /* @__PURE__ */ new Set([
9782
10146
  "currentJobId",
9783
10147
  "currentJobProgress",
9784
10148
  "errorMessage",
10149
+ // Structured detail alongside errorMessage for a safety-filter block
10150
+ // (see `JobErrorHint` in the app's frontend/src/types/nodes.ts). Same
10151
+ // lifecycle as errorMessage: a RESULT the user expects to survive reload,
10152
+ // never user-edited config.
10153
+ "errorHint",
9785
10154
  "isStreaming",
9786
10155
  "generatedImageUrl",
9787
10156
  "generatedVideoUrl",
@@ -13519,6 +13888,26 @@ var clipLookSchema = zod.z.object({
13519
13888
  * insert) states the deviation in that scene's `visual`, as with `lighting`.
13520
13889
  */
13521
13890
  style: zod.z.string().optional(),
13891
+ /**
13892
+ * The Style picker CATALOG ID the prose in `style` corresponds to — the
13893
+ * analyzer's PICK ("pixar-3d" beside "3D stylized animation"), not a second
13894
+ * description of it.
13895
+ *
13896
+ * Worth strictly more than the prose it accompanies: an id addresses the
13897
+ * catalog, so a recreation renders the same medium the product's own Style
13898
+ * picker would render, instead of re-interpreting a sentence. Absent when the
13899
+ * analyzer read a medium it could not place in the catalog — `style` then
13900
+ * carries the whole answer, as it always did.
13901
+ *
13902
+ * A free `string` here on purpose. The PRODUCER validates it against the
13903
+ * catalog (the analyzer plugin's wire schema is an enum generated from
13904
+ * `STYLES`, so an invented id never leaves it), while this package must not
13905
+ * carry the catalog itself. A closed enum here would only add a second copy
13906
+ * of the vocabulary to drift out of date — and, worse, would REJECT a
13907
+ * catalog entry newer than the installed `@nodaro/shared`, which is exactly
13908
+ * the analysis a consumer most wants to read.
13909
+ */
13910
+ styleId: zod.z.string().optional(),
13522
13911
  /** Colour grade / palette — "muted teal-and-orange, crushed blacks". */
13523
13912
  grade: zod.z.string().optional(),
13524
13913
  /** Camera or film FORMAT and stock — "anamorphic digital", "16mm film grain". */
@@ -13589,8 +13978,9 @@ var entitySlotSchema = zod.z.object({
13589
13978
  /** NON-default looks only; present only when at least one exists. */
13590
13979
  variations: zod.z.array(slotVariationSchema).max(VIDEO_ANALYSIS_MAX_VARIATIONS).optional()
13591
13980
  });
13981
+ var VIDEO_ANALYSIS_AUDIO_MODES = ["speech", "music", "sfx", "ambience"];
13592
13982
  var audioLayerSchema = zod.z.object({
13593
- mode: zod.z.enum(["speech", "music", "sfx"]),
13983
+ mode: zod.z.enum(VIDEO_ANALYSIS_AUDIO_MODES),
13594
13984
  content: zod.z.string().min(1),
13595
13985
  voice: zod.z.string().optional(),
13596
13986
  /**
@@ -13613,7 +14003,34 @@ var audioLayerSchema = zod.z.object({
13613
14003
  * (doctrine §5), so attribution here would resurrect the phantom-entity
13614
14004
  * defect that `stripOrphanSlots` exists to kill.
13615
14005
  */
13616
- speakerSlot: zod.z.string().optional()
14006
+ speakerSlot: zod.z.string().optional(),
14007
+ /**
14008
+ * SPEECH ONLY — WHO says these words, by NAME.
14009
+ *
14010
+ * The second of two ways to name a speaker, and the one for documents that
14011
+ * have no slots: `speakerSlot` addresses an `EntitySlot` of THIS analysis by
14012
+ * id, while `speaker` is the plain cast name a production keys its own cast
14013
+ * by ("Jack Mercer"). A layer may legitimately carry both — the id for the
14014
+ * analysis it came from, the name for the document it is going into — and a
14015
+ * consumer that understands only one reads the one it understands.
14016
+ *
14017
+ * Optional, and not refined against `mode`, for the same reason
14018
+ * `speakerSlot` is not: the window schema is the enforced decode grammar and
14019
+ * must not throw away a whole roll over one mis-tagged field.
14020
+ *
14021
+ * NOT swept by `dropUnknownSpeakers`. That function is the SLOT channel's
14022
+ * sanitizer — it judges an id against the surviving slot list, and a name has
14023
+ * no id space to be unknown in. The asymmetry is deliberate and pinned by
14024
+ * test; whoever owns the name's vocabulary sanitizes the name.
14025
+ *
14026
+ * The residual that leaves: a layer can keep a `speaker` naming someone
14027
+ * `stripOrphanSlots` already pruned as a phantom, where the same claim spelled
14028
+ * `speakerSlot` would have been dropped — the invented-narrator defect, in the
14029
+ * one spelling the sweep cannot see. LATENT today, since nothing in this
14030
+ * repo emits `speaker`; the day the analyzer does, that sweep is what has to
14031
+ * grow, not this field.
14032
+ */
14033
+ speaker: zod.z.string().optional()
13617
14034
  });
13618
14035
  var windowSceneBase = zod.z.object({
13619
14036
  startSec: zod.z.number().min(0),
@@ -14453,6 +14870,7 @@ exports.REFERENCE_HANDLE_MAP = REFERENCE_HANDLE_MAP;
14453
14870
  exports.REFERENCE_ROLE_PRESETS = REFERENCE_ROLE_PRESETS;
14454
14871
  exports.REF_HANDLE_CATEGORY = REF_HANDLE_CATEGORY;
14455
14872
  exports.REF_IMAGE_MAX_LIMITS = REF_IMAGE_MAX_LIMITS;
14873
+ exports.REF_TOKEN_NAMESPACE_PREFIXES = REF_TOKEN_NAMESPACE_PREFIXES;
14456
14874
  exports.RENDERING_SPEED_SUPPORT = RENDERING_SPEED_SUPPORT;
14457
14875
  exports.REPEATABLE_NODE_TYPES = REPEATABLE_NODE_TYPES;
14458
14876
  exports.REPEAT_PLACEHOLDER = REPEAT_PLACEHOLDER;
@@ -14527,6 +14945,8 @@ exports.TEXT_TO_VIDEO_PROVIDERS = TEXT_TO_VIDEO_PROVIDERS;
14527
14945
  exports.TIER_MAX_PIPELINE_COST_CREDITS = TIER_MAX_PIPELINE_COST_CREDITS;
14528
14946
  exports.TIER_PIPELINE_PARALLELISM = TIER_PIPELINE_PARALLELISM;
14529
14947
  exports.TILT_CARRIED_FRACTION = TILT_CARRIED_FRACTION;
14948
+ exports.TOPAZ_DEFAULT_UPSCALE_FACTOR = TOPAZ_DEFAULT_UPSCALE_FACTOR;
14949
+ exports.TOPAZ_UPSCALE_FACTORS = TOPAZ_UPSCALE_FACTORS;
14530
14950
  exports.TRANSCRIBE_PROVIDERS = TRANSCRIBE_PROVIDERS;
14531
14951
  exports.TRANSIENT_RUNTIME_KEYS = TRANSIENT_RUNTIME_KEYS;
14532
14952
  exports.TTS_PROVIDERS = TTS_PROVIDERS;
@@ -14542,6 +14962,7 @@ exports.VEHICLES = VEHICLES;
14542
14962
  exports.VEHICLE_SUBCATEGORY_LABELS = VEHICLE_SUBCATEGORY_LABELS;
14543
14963
  exports.VEHICLE_SUBCATEGORY_ORDER = VEHICLE_SUBCATEGORY_ORDER;
14544
14964
  exports.VEO_PROVIDERS = VEO_PROVIDERS;
14965
+ exports.VIDEO_ANALYSIS_AUDIO_MODES = VIDEO_ANALYSIS_AUDIO_MODES;
14545
14966
  exports.VIDEO_ANALYSIS_BUCKET_CREDITS = VIDEO_ANALYSIS_BUCKET_CREDITS;
14546
14967
  exports.VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN = VIDEO_ANALYSIS_CLIP_TRANSITIONS_IN;
14547
14968
  exports.VIDEO_ANALYSIS_DEFAULT_VARIATION = VIDEO_ANALYSIS_DEFAULT_VARIATION;
@@ -14586,6 +15007,7 @@ exports.VIDEO_PROMPT_MAX = VIDEO_PROMPT_MAX;
14586
15007
  exports.VIDEO_PROVIDERS_REQUIRING_IMAGE = VIDEO_PROVIDERS_REQUIRING_IMAGE;
14587
15008
  exports.VIDEO_PROVIDERS_WITHOUT_DISPATCH = VIDEO_PROVIDERS_WITHOUT_DISPATCH;
14588
15009
  exports.VIDEO_REF_LIMITS_BY_PROVIDER = VIDEO_REF_LIMITS_BY_PROVIDER;
15010
+ exports.VIDEO_REF_VIDEO_DURATION_LIMITS = VIDEO_REF_VIDEO_DURATION_LIMITS;
14589
15011
  exports.VIDEO_TO_VIDEO_PROVIDERS = VIDEO_TO_VIDEO_PROVIDERS;
14590
15012
  exports.VIDEO_UPSCALE_PROVIDERS = VIDEO_UPSCALE_PROVIDERS;
14591
15013
  exports.VIDEO_UTIL_PRICING = VIDEO_UTIL_PRICING;
@@ -14654,9 +15076,11 @@ exports.characterMentionSlug = characterMentionSlug;
14654
15076
  exports.characterMentionableAssetArrays = characterMentionableAssetArrays;
14655
15077
  exports.characterSheetRefItems = characterSheetRefItems;
14656
15078
  exports.characterVariantAssetArrays = characterVariantAssetArrays;
15079
+ exports.checkRefVideoDurations = checkRefVideoDurations;
14657
15080
  exports.cinematicCreditId = cinematicCreditId;
14658
15081
  exports.clampCinematicDuration = clampCinematicDuration;
14659
15082
  exports.clampSmartCutWindow = clampSmartCutWindow;
15083
+ exports.classifyRefToken = classifyRefToken;
14660
15084
  exports.cleanOrphanedItems = cleanOrphanedItems;
14661
15085
  exports.clearImageCriticMetadata = clearImageCriticMetadata;
14662
15086
  exports.clearVideoCriticMetadata = clearVideoCriticMetadata;
@@ -14831,6 +15255,7 @@ exports.normalizeModelInput = normalizeModelInput;
14831
15255
  exports.normalizeNodeModelParams = normalizeNodeModelParams;
14832
15256
  exports.normalizePinterestUrl = normalizePinterestUrl;
14833
15257
  exports.normalizeRoleSlug = normalizeRoleSlug;
15258
+ exports.normalizeVideoRequestParams = normalizeVideoRequestParams;
14834
15259
  exports.normalizeWan3Resolution = normalizeWan3Resolution;
14835
15260
  exports.orderedLlmModels = orderedLlmModels;
14836
15261
  exports.parseAttributedDialogue = parseAttributedDialogue;
@@ -14855,6 +15280,7 @@ exports.presentTypes = presentTypes;
14855
15280
  exports.presetApplyClearKeys = presetApplyClearKeys;
14856
15281
  exports.presetDataMatches = presetDataMatches;
14857
15282
  exports.presetEntries = presetEntries;
15283
+ exports.pricedVideoSelection = pricedVideoSelection;
14858
15284
  exports.qualityOptionsByKind = qualityOptionsByKind;
14859
15285
  exports.readPromptAffixes = readPromptAffixes;
14860
15286
  exports.refHandleCategory = refHandleCategory;
@@ -14899,6 +15325,7 @@ exports.resolveSlideshowTransition = resolveSlideshowTransition;
14899
15325
  exports.resolveSourceThroughConnectedList = resolveSourceThroughConnectedList;
14900
15326
  exports.resolveStoredTier = resolveStoredTier;
14901
15327
  exports.resolveSwitchXCreditId = resolveSwitchXCreditId;
15328
+ exports.resolveTopazUpscale = resolveTopazUpscale;
14902
15329
  exports.resolveVideoAnalysisModel = resolveVideoAnalysisModel;
14903
15330
  exports.resolveVideoModeForInputs = resolveVideoModeForInputs;
14904
15331
  exports.resolveVideoProviderForMode = resolveVideoProviderForMode;
@@ -14909,6 +15336,7 @@ exports.rewriteSpeakerSlots = rewriteSpeakerSlots;
14909
15336
  exports.rgbaArrayToHex = rgbaArrayToHex;
14910
15337
  exports.roleToPhrase = roleToPhrase;
14911
15338
  exports.runSelector = runSelector;
15339
+ exports.safetyRetryPolicy = safetyRetryPolicy;
14912
15340
  exports.sanitizeRole = sanitizeRole;
14913
15341
  exports.searchModelVariants = searchModelVariants;
14914
15342
  exports.seedance2AudioLimitSec = seedance2AudioLimitSec;
@@ -14945,6 +15373,7 @@ exports.tryParseJson = tryParseJson;
14945
15373
  exports.uiAspectRatioFill = uiAspectRatioFill;
14946
15374
  exports.uiDurationFill = uiDurationFill;
14947
15375
  exports.uiResolutionFill = uiResolutionFill;
15376
+ exports.unresolvedRefTokens = unresolvedRefTokens;
14948
15377
  exports.unwrapUnresolvedTokens = unwrapUnresolvedTokens;
14949
15378
  exports.usageModeDirective = usageModeDirective;
14950
15379
  exports.usageModeIncludesName = usageModeIncludesName;