@nodaro/shared 3.9.0 → 3.11.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 (48) hide show
  1. package/dist/index.cjs +448 -51
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +959 -30
  4. package/dist/index.d.ts +959 -30
  5. package/dist/index.js +410 -52
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/__tests__/credit-identifiers.test.ts +51 -1
  9. package/src/__tests__/gvp-supported-providers.test.ts +20 -3
  10. package/src/__tests__/model-catalog-sections.test.ts +59 -0
  11. package/src/__tests__/model-tree.test.ts +2 -2
  12. package/src/__tests__/parameter-node-value.test.ts +47 -0
  13. package/src/__tests__/pricing-default-duration.test.ts +45 -0
  14. package/src/__tests__/prompt-length-limits.test.ts +4 -0
  15. package/src/__tests__/scene3d-delivery-review.test.ts +512 -0
  16. package/src/__tests__/template-categories.test.ts +45 -0
  17. package/src/__tests__/video-analysis.test.ts +15 -0
  18. package/src/__tests__/video-frame-fit.test.ts +189 -0
  19. package/src/__tests__/video-ref-limits.test.ts +11 -2
  20. package/src/catalog-projection.ts +3 -0
  21. package/src/character-motion-metadata.ts +19 -0
  22. package/src/credit-identifiers.ts +44 -9
  23. package/src/i18n/character-motion.ar.ts +1082 -0
  24. package/src/i18n/character-motion.de.ts +1082 -0
  25. package/src/i18n/character-motion.es.ts +1082 -0
  26. package/src/i18n/character-motion.fr.ts +1082 -0
  27. package/src/i18n/character-motion.he.ts +1082 -0
  28. package/src/i18n/character-motion.hi.ts +1082 -0
  29. package/src/i18n/character-motion.ja.ts +1082 -0
  30. package/src/i18n/character-motion.ko.ts +1082 -0
  31. package/src/i18n/character-motion.pt-BR.ts +1082 -0
  32. package/src/i18n/character-motion.ru.ts +1082 -0
  33. package/src/i18n/character-motion.zh-CN.ts +1082 -0
  34. package/src/i18n/types.ts +1 -0
  35. package/src/index.ts +26 -0
  36. package/src/model-catalog.ts +49 -32
  37. package/src/model-constants.ts +50 -11
  38. package/src/node-execution-state.ts +95 -0
  39. package/src/parameter-node-value.ts +59 -0
  40. package/src/presentation-utils.ts +1 -0
  41. package/src/pro-3d-render.ts +159 -0
  42. package/src/scene3d-delivery-notes.ts +490 -0
  43. package/src/scene3d-v2-plan.ts +8 -2
  44. package/src/smart-cut-windows.ts +15 -10
  45. package/src/template-categories.ts +67 -0
  46. package/src/video-analysis.ts +15 -0
  47. package/src/video-frame-fit.ts +228 -0
  48. package/src/video-output-canvas.ts +119 -0
package/src/i18n/types.ts CHANGED
@@ -120,6 +120,7 @@ export const I18N_CATALOGS = [
120
120
  "camera-format",
121
121
  "camera-motions",
122
122
  "character-fx",
123
+ "character-motion",
123
124
  "color-look",
124
125
  "composition-effects",
125
126
  "era",
package/src/index.ts CHANGED
@@ -167,6 +167,7 @@ export {
167
167
  DEFAULT_VIDEO_DURATION_SEC,
168
168
  applyDefaultVideoSelection,
169
169
  PRICING_DEFAULT_DURATION_SEC,
170
+ pricedOutputDurationSec,
170
171
  PRICING_DEFAULT_RESOLUTION,
171
172
  } from "./model-constants.js"
172
173
 
@@ -547,6 +548,8 @@ export type { LocationCatalogRef } from "./location-preset-catalog-map.js"
547
548
  export {
548
549
  PARAMETER_NODE_TYPES,
549
550
  HINT_EXEMPT_PARAMETER_TYPES,
551
+ VIDEO_ONLY_PARAMETER_NODE_TYPES,
552
+ EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES,
550
553
  getParameterValue,
551
554
  setRegisteredPersonPackFields,
552
555
  } from "./parameter-node-value.js"
@@ -731,6 +734,8 @@ export {
731
734
  MODEL_RECOMMENDATIONS,
732
735
  listModels,
733
736
  groupByFamily,
737
+ groupByKindAndFamily,
738
+ MODEL_KINDS,
734
739
  getModel,
735
740
  validateModelInput,
736
741
  // Frontend picker derivers
@@ -977,6 +982,16 @@ export type { VoiceChangerModel } from "./voice-changer-models.js"
977
982
  // --- Node presets ---
978
983
  export { EXECUTION_DATA_KEYS, TRANSIENT_RUNTIME_KEYS, stripTransientRuntimeData } from "./node-runtime-keys.js"
979
984
 
985
+ // --- Execution node state (wire contract) ---
986
+ export {
987
+ OUTPUT_BEARING_NODE_STATUSES,
988
+ nodeStateMayCarryOutput,
989
+ } from "./node-execution-state.js"
990
+ export type {
991
+ NodeExecutionStatus,
992
+ NodeExecutionStateWire,
993
+ } from "./node-execution-state.js"
994
+
980
995
  export {
981
996
  MODEL_PARAM_NODE_TYPES,
982
997
  normalizeNodeModelParams,
@@ -1003,6 +1018,9 @@ export * from "./reference-sheet/index.js"
1003
1018
  // --- Reference Board (templates + provider constant) ---
1004
1019
  export * from "./reference-board-templates.js"
1005
1020
 
1021
+ // --- Marketplace template categories (the eight use cases) ---
1022
+ export * from "./template-categories.js"
1023
+
1006
1024
  // --- Model tree (derive node targets + product-line grouping for the Models tab) ---
1007
1025
  export * from "./model-tree.js"
1008
1026
 
@@ -1071,6 +1089,9 @@ export * from "./scene3d-v2.js"
1071
1089
  export * from "./scene3d-v2-plan.js"
1072
1090
  export * from "./scene3d-v2-resources.js"
1073
1091
  export * from "./scene3d-camera-track.js"
1092
+ // --- What an authoring run says about its own answer: assumptions, summary,
1093
+ // repair count. Additive and optional on every Scene3D authoring lane. ---
1094
+ export * from "./scene3d-delivery-notes.js"
1074
1095
  // --- 3D Render Pro: one durable operation, scene + video in one result ---
1075
1096
  export * from "./pro-3d-render.js"
1076
1097
  // --- Scene3D render pricing: which frame-size tier a render settles at.
@@ -1088,3 +1109,8 @@ export * from "./scene3d-v2-edit.js"
1088
1109
  export { STUDIO_DEPENDENT_FRAMES_CAPABILITY, SequenceExecutionRequiredError, requiresSequenceExecution, assertCanvasExecutionAllowed } from "./sequence-execution"
1089
1110
  export * from "./scene3d-authoring-engine.js"
1090
1111
  export * from "./scene3d-input-assets.js"
1112
+
1113
+ // --- Start/end frame fit: measured output canvases + the pure fit math ---
1114
+ export * from "./video-output-canvas.js"
1115
+ export * from "./video-frame-fit.js"
1116
+ export type { CharacterMotionMetadata } from "./character-motion-metadata.js"
@@ -52,7 +52,6 @@ export type ModelMode =
52
52
  | "music"
53
53
  | "sfx"
54
54
  | "stt"
55
- | "voice-clone"
56
55
  | "voice-design"
57
56
  | "voice-changer"
58
57
  | "isolation"
@@ -755,23 +754,8 @@ const IMAGE_MODELS: Record<string, ModelCatalogEntry> = {
755
754
  { identifier: "ideogram-remix:QUALITY", credits: 60, note: "best quality" },
756
755
  ],
757
756
  },
758
- "ideogram-reframe": {
759
- id: "ideogram-reframe",
760
- kind: "image",
761
- modes: ["edit"] as const,
762
- family: "Ideogram",
763
- label: "Ideogram Reframe",
764
- series: "Ideogram",
765
- description: "Outpaint / reframe to a new aspect ratio while preserving subject.",
766
- useCases: ["outpaint", "reframe"],
767
- features: ["reference-image"],
768
- aspectRatios: IDEOGRAM_RATIOS,
769
- pricing: [
770
- { identifier: "ideogram-reframe", credits: 18, note: "BALANCED default" },
771
- { identifier: "ideogram-reframe:TURBO", credits: 18, note: "fastest" },
772
- { identifier: "ideogram-reframe:QUALITY", credits: 18, note: "best quality" },
773
- ],
774
- },
757
+ // `ideogram-reframe` was retired 2026-09-15 — KIE's `ideogram/v3-reframe`
758
+ // fails every task upstream (#1331); see IDEOGRAM_PROVIDERS in model-constants.
775
759
 
776
760
  // ── Google Imagen ──
777
761
  "imagen4": {
@@ -1238,9 +1222,17 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
1238
1222
  family: "Google",
1239
1223
  label: "VEO 3.1 Quality",
1240
1224
  series: "VEO",
1241
- description: "Google VEO 3.1 Quality — premium cinematic video. 4/6/8s clips, optional end frame, native audio. Flat per-generation pricing across durations.",
1225
+ description: "Google VEO 3.1 Quality — premium cinematic video. 4/6/8s clips, optional end frame, native audio. No reference-to-video mode (Fast/Lite only). Flat per-generation pricing across durations.",
1242
1226
  useCases: ["cinematic", "premium", "narrative"],
1243
- features: ["end-frame", "audio", "reference-image"],
1227
+ // NO "reference-image": KIE serves REFERENCE_2_VIDEO on the Fast and Lite
1228
+ // SKUs only. Its own words, on a production job (2026-09-04, app-reports
1229
+ // lane G): "Reference to video only supports the Veo Fast model and Veo
1230
+ // Lite model." Until 2026-09-15 this entry claimed the feature, so the
1231
+ // editor offered the reference handles, the adapter sent
1232
+ // generationType: "REFERENCE_2_VIDEO", and the call came back 422 every
1233
+ // time — after credits were reserved. `veo3.1` (KIE `veo3_fast`) and
1234
+ // `veo3_lite` DO serve it and keep the flag.
1235
+ features: ["end-frame", "audio"],
1244
1236
  durations: [4, 6, 8],
1245
1237
  aspectRatios: VIDEO_RATIOS_HV,
1246
1238
  // 720p (default) + 1080p inline. 4K generates the base at 1080p then chains
@@ -2204,7 +2196,7 @@ const VIDEO_MODELS: Record<string, ModelCatalogEntry> = {
2204
2196
  id: "omnihuman-1-5",
2205
2197
  kind: "video",
2206
2198
  modes: ["lip-sync"] as const,
2207
- family: "ByteDance",
2199
+ family: "Bytedance",
2208
2200
  label: "OmniHuman 1.5",
2209
2201
  series: "OmniHuman",
2210
2202
  description: "Premium prompt-directed talking avatar from a still image + audio. 720p / 1080p, up to 60s. People, pets, anime.",
@@ -2468,17 +2460,8 @@ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
2468
2460
  },
2469
2461
 
2470
2462
  // ── ElevenLabs voice utilities ──
2471
- "voice-clone": {
2472
- id: "voice-clone",
2473
- kind: "audio",
2474
- modes: ["voice-clone"] as const,
2475
- family: "ElevenLabs",
2476
- label: "Voice Clone (Instant)",
2477
- series: "ElevenLabs",
2478
- description: "Clone a voice from a short reference clip. Instant clone via direct ElevenLabs API.",
2479
- useCases: ["voice-clone", "personalization"],
2480
- pricing: [{ identifier: "voice-clone", credits: 50 }],
2481
- },
2463
+ // (voice cloning was retired platform-wide on 2026-09-15 — no catalog entry,
2464
+ // no price, no MCP tool; existing clones still resolve as TTS voice ids.)
2482
2465
  "elevenlabs-voice-design": {
2483
2466
  id: "elevenlabs-voice-design",
2484
2467
  kind: "audio",
@@ -2534,6 +2517,17 @@ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
2534
2517
  useCases: ["dubbing", "multilingual"],
2535
2518
  pricing: [{ identifier: "elevenlabs-dubbing", credits: 40, note: "per minute of the dubbed span (min 1)" }],
2536
2519
  },
2520
+ "elevenlabs-dubbing-v2": {
2521
+ id: "elevenlabs-dubbing-v2",
2522
+ kind: "audio",
2523
+ modes: ["dubbing"] as const,
2524
+ family: "ElevenLabs",
2525
+ label: "ElevenLabs Dubbing v2",
2526
+ series: "ElevenLabs",
2527
+ description: "Translate + dub audio or a whole video into a new language — video in, dubbed video out. Async.",
2528
+ useCases: ["dubbing", "multilingual"],
2529
+ pricing: [{ identifier: "elevenlabs-dubbing-v2", credits: 1100, note: "per minute of the dubbed span (min 1)" }],
2530
+ },
2537
2531
  "elevenlabs-forced-alignment": {
2538
2532
  id: "elevenlabs-forced-alignment",
2539
2533
  kind: "audio",
@@ -2678,6 +2672,29 @@ export function groupByFamily(
2678
2672
  return Array.from(groups.entries()).map(([family, models]) => ({ family, models }))
2679
2673
  }
2680
2674
 
2675
+ export const MODEL_KINDS = ["image", "video", "audio"] as const satisfies readonly ModelKind[]
2676
+
2677
+ /**
2678
+ * The Image / Video / Audio envelope every model-discovery surface renders
2679
+ * (`GET /v1/models`, MCP `list_models`): each model is filed under ITS OWN
2680
+ * kind, then by vendor family within that kind — so a vendor that ships more
2681
+ * than one kind (Google: Imagen + VEO, ByteDance: Seedream + Seedance, …)
2682
+ * appears once per kind it actually ships.
2683
+ *
2684
+ * This is the ONE grouping both surfaces call. Grouping by family first and
2685
+ * letting the family's first model pick the section filed every VEO, Seedance,
2686
+ * Wan and ByteDance video model under "image" whenever the call carried no
2687
+ * `kind` filter (#1332) — an explicit `kind` pre-filtered the list and hid it.
2688
+ * Empty sections are omitted; section order is fixed image → video → audio.
2689
+ */
2690
+ export function groupByKindAndFamily(
2691
+ entries: ModelCatalogEntry[],
2692
+ ): Array<{ kind: ModelKind; families: Array<{ family: string; models: ModelCatalogEntry[] }> }> {
2693
+ return MODEL_KINDS
2694
+ .map((kind) => ({ kind, families: groupByFamily(entries.filter((m) => m.kind === kind)) }))
2695
+ .filter((section) => section.families.length > 0)
2696
+ }
2697
+
2681
2698
  export function getModel(id: string): ModelCatalogEntry | undefined {
2682
2699
  return MODEL_CATALOG[id]
2683
2700
  }
@@ -91,14 +91,20 @@ export const MAX_IMAGE_PROMPT_CHARS_BY_PROVIDER: Record<string, number> = {
91
91
  "seedream-5-lite-i2i": 3000, // docs.kie.ai/market/seedream-5-lite-image-to-image (NB: t2i sibling is 1000)
92
92
  "qwen": 3000, // docs.kie.ai/market/qwen/text-to-image
93
93
  "qwen-edit": 2000, // docs.kie.ai/market/qwen/image-edit
94
+ // Z-Image is the SHORTEST of the family — "maxLength: 1000" in its schema
95
+ // (docs.kie.ai/market/z-image/z-image, re-fetched 2026-09-15). It was listed
96
+ // below as "verified == 5000", which is how three production runs on
97
+ // 2026-09-07 sent a workflow-assembled prompt straight into KIE's
98
+ // `{"code":500,"msg":"The text length cannot exceed the maximum limit"}`.
99
+ "z-image": 1000, // docs.kie.ai/market/z-image/z-image
94
100
  // verified == 5000 default (no entry needed): imagen4(-fast/-ultra), nano-banana,
95
101
  // nano-banana-edit, flux, flux-flex, gpt-image-2, ideogram-v3/-edit/-remix,
96
- // z-image, grok, qwen-i2i, seedream-5-pro, seedream-5-pro-i2i
102
+ // grok, qwen-i2i, seedream-5-pro, seedream-5-pro-i2i
97
103
  // (docs.kie.ai/market/seedream/5-pro-text-to-image + 5-pro-image-to-image).
98
104
  // grok-i2i: doc states 390000 (78× its t2i sibling) — treated as a KIE schema
99
105
  // typo and left at the 5000 default per the sanity-cap decision.
100
106
  // UNVERIFIED (no limit stated in schema) → 5000 default: flux-kontext(-max)
101
- // gpt-image, gpt-image-i2i, flux-i2i, flux-pro-i2i, ideogram-reframe.
107
+ // gpt-image, gpt-image-i2i, flux-i2i, flux-pro-i2i.
102
108
  }
103
109
 
104
110
  /** Max assembled image-prompt length (chars) for a provider: its verified
@@ -508,7 +514,6 @@ export const MODELS_WITH_REFERENCE_IMAGE_SUPPORT = new Set([
508
514
  "flux-kontext-max",
509
515
  "ideogram-edit",
510
516
  "ideogram-remix",
511
- "ideogram-reframe",
512
517
  "qwen-i2i",
513
518
  "qwen-edit",
514
519
  "seedream-edit",
@@ -588,7 +593,6 @@ export const REF_IMAGE_MAX_LIMITS: Record<string, number> = {
588
593
  "flux-kontext-max": 1,
589
594
  "ideogram-edit": 1,
590
595
  "ideogram-remix": 1,
591
- "ideogram-reframe": 1,
592
596
  "qwen-i2i": 1,
593
597
  "qwen-edit": 1,
594
598
  "grok-i2i": 1,
@@ -662,7 +666,6 @@ export const VARIABLE_PRICING_MODELS: Record<string, "quality" | "resolution" |
662
666
  "topaz-image-upscale": "resolution",
663
667
  "ideogram-edit": "rendering-speed",
664
668
  "ideogram-remix": "rendering-speed",
665
- "ideogram-reframe": "rendering-speed",
666
669
  "ideogram-v3": "rendering-speed",
667
670
  "wan-2.7": "resolution",
668
671
  "wan-2.7-pro": "resolution",
@@ -689,7 +692,12 @@ export const RESOLUTION_2K_4K_TIERED_PROVIDERS = new Set([
689
692
  ])
690
693
 
691
694
  // Ideogram family models with TURBO/QUALITY pricing variants
692
- export const IDEOGRAM_PROVIDERS = new Set(["ideogram-edit", "ideogram-remix", "ideogram-reframe", "ideogram-v3"])
695
+ // `ideogram-reframe` (KIE `ideogram/v3-reframe`) was retired 2026-09-15: KIE no
696
+ // longer documents the model and every task — the minimal documented payload
697
+ // included — fails upstream with "[500] internal error" (#1331). Its price rows
698
+ // are removed by migration 424; re-add everywhere per the Provider Enum Sync
699
+ // table if KIE brings it back.
700
+ export const IDEOGRAM_PROVIDERS = new Set(["ideogram-edit", "ideogram-remix", "ideogram-v3"])
693
701
 
694
702
  // =====================================================================
695
703
  // Provider arrays (single source of truth for route Zod validation)
@@ -744,7 +752,6 @@ export const IMAGE_I2I_PROVIDERS = [
744
752
  "grok-2-i2i",
745
753
  "ideogram-edit",
746
754
  "ideogram-remix",
747
- "ideogram-reframe",
748
755
  "qwen-i2i",
749
756
  "qwen-edit",
750
757
  "seedream-edit",
@@ -1365,7 +1372,7 @@ export const I2I_STRENGTH_SUPPORT: Record<string, { min: number; max: number; st
1365
1372
 
1366
1373
  /** Models that accept a seed parameter for reproducible generation */
1367
1374
  export const SEED_SUPPORT = new Set([
1368
- "ideogram-remix", "ideogram-reframe", "ideogram-v3",
1375
+ "ideogram-remix", "ideogram-v3",
1369
1376
  "qwen", "qwen-i2i", "qwen-edit",
1370
1377
  "flux", "flux-flex", "flux-i2i", "flux-pro-i2i", "flux-kontext", "flux-kontext-max",
1371
1378
  "flux-2-klein", "kontext-multi",
@@ -1373,7 +1380,7 @@ export const SEED_SUPPORT = new Set([
1373
1380
 
1374
1381
  /** Ideogram models that support rendering_speed selection (TURBO/BALANCED/QUALITY) */
1375
1382
  export const RENDERING_SPEED_SUPPORT = new Set([
1376
- "ideogram-remix", "ideogram-reframe", "ideogram-v3",
1383
+ "ideogram-remix", "ideogram-v3",
1377
1384
  ])
1378
1385
 
1379
1386
  /** Models that accept guidance_scale for controlling prompt adherence */
@@ -1979,8 +1986,13 @@ export const VIDEO_REF_LIMITS_BY_PROVIDER: Record<
1979
1986
  "kling-3-omni": { images: 7 }, // catalog/docs: "end frame + up to 7 reference images"
1980
1987
  "grok-i2v": { images: 7 }, // backend kie/models.ts maxRefImages: 7
1981
1988
  "happyhorse-ref2v": { images: 9 }, // backend kie/models.ts maxRefImages: 9
1982
- // VEO 3.x — REFERENCE_2_VIDEO path caps refs at 3 (kie/video.ts slice(0, 3)).
1983
- "veo3": { images: 3 },
1989
+ // VEO 3.x — REFERENCE_2_VIDEO path caps refs at 3 (kie/video.ts slices to
1990
+ // this number). `veo3` (VEO 3.1 QUALITY) is deliberately ABSENT: KIE serves
1991
+ // reference-to-video on the Fast and Lite SKUs only and rejects it on
1992
+ // Quality with "Reference to video only supports the Veo Fast model and Veo
1993
+ // Lite model." (production, 2026-09-04). Absent ⇒ 0 ⇒ the handle dims, the
1994
+ // `connectedReferences` assembly strips image tokens, and kie/video.ts never
1995
+ // flips a Quality call to REFERENCE_2_VIDEO.
1984
1996
  "veo3.1": { images: 3 },
1985
1997
  "veo3_lite": { images: 3 },
1986
1998
  // NOTE: wan-i2v / hailuo-2.3[-pro] / bytedance-pro[-fast] / grok-imagine-video-1.5
@@ -2513,6 +2525,33 @@ export const VIDEO_VARIABLE_PRICING: Record<string, "duration" | "duration+audio
2513
2525
  */
2514
2526
  export const PRICING_DEFAULT_DURATION_SEC: Record<string, number> = {
2515
2527
  "minimax-h3": 6,
2528
+ // KIE renders 8s when `duration` is omitted (kie/models.ts extraParams), and
2529
+ // Seedance 2.5 prices one tier per second across 4–30s, so the 5s fallback
2530
+ // billed a 5s tier against an 8s render — and under-reserved every
2531
+ // reference-video run's output seconds by three (#1397).
2532
+ "seedance-2-5": 8,
2533
+ // Same shape: KIE renders 8s by default and the ladder is priced per second,
2534
+ // so the 5s fallback billed a 5s tier against an 8s render (caught by the
2535
+ // render-default ↔ priced-tier invariant in
2536
+ // backend/src/providers/__tests__/pricing-default-duration-sync.test.ts).
2537
+ "grok-imagine-video-1.5": 8,
2538
+ }
2539
+
2540
+ /**
2541
+ * The output seconds a request is PRICED at: the requested duration when the
2542
+ * caller gave one, else the provider's own default render length
2543
+ * ({@link PRICING_DEFAULT_DURATION_SEC}), else the historical 5s fallback.
2544
+ *
2545
+ * The ONE source for `buildVideoCreditModelIdentifier`'s tier AND for every
2546
+ * dynamic reservation that scales by output seconds (the Seedance 2 and
2547
+ * MiniMax Hailuo 3 reference-video overrides on both the route and the DAG
2548
+ * lane) — a literal `?? 5` in any of those places re-opens the gap this map
2549
+ * closes.
2550
+ */
2551
+ export function pricedOutputDurationSec(provider: string, requested: number | string | undefined): number {
2552
+ const fallback = PRICING_DEFAULT_DURATION_SEC[provider] ?? 5
2553
+ const parsed = typeof requested === "string" ? parseInt(requested, 10) : requested
2554
+ return parsed === undefined || Number.isNaN(parsed) ? fallback : parsed
2516
2555
  }
2517
2556
 
2518
2557
  /**
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The wire contract for one node's entry in an execution's `nodeStates` map —
3
+ * the shape the orchestrator persists on `workflow_executions.node_states`,
4
+ * emits on the SSE execution stream, and every client reads back.
5
+ *
6
+ * Why this lives here rather than in the backend
7
+ * ----------------------------------------------
8
+ * The shape had THREE independent declarations — the orchestrator's rich
9
+ * `NodeExecutionState` (workflow-engine/types.ts), the SDK's loose one
10
+ * (`packages/client/src/resources/executions.ts`) and the editor's local copy
11
+ * (`workflow-editor/run-handlers.ts`) — and nothing tied them together. The
12
+ * field that made that expensive is `output`: it used to be an unstated
13
+ * convention that only a COMPLETED node carries one, so every consumer read it
14
+ * exclusively under `status === "completed"` and a failed node's retained
15
+ * result had nowhere to travel.
16
+ *
17
+ * This module states the rule once. The backend keeps its richer `NodeOutput`
18
+ * typing (its `NodeExecutionState` must stay ASSIGNABLE to the wire shape — a
19
+ * type-level test pins that); the SDK and the editor extend/import it.
20
+ */
21
+
22
+ /**
23
+ * The status a node reports inside an execution.
24
+ *
25
+ * `cancelled` is deliberately absent: a cancelled child job is mapped onto
26
+ * `skipped` by the reconcile lane (see `lib/reconcile/node-states.ts`), and a
27
+ * HELD job keeps `status: "running"` with the `awaitingReview` sidecar rather
28
+ * than adding a member here.
29
+ */
30
+ export type NodeExecutionStatus =
31
+ | "pending"
32
+ | "running"
33
+ | "completed"
34
+ | "failed"
35
+ | "skipped"
36
+
37
+ /**
38
+ * The statuses whose node state MAY carry `output`.
39
+ *
40
+ * `completed` is the obvious one. `failed` is here because a run can refuse
41
+ * its result and still RETAIN what it produced — the 3D-scene authoring lanes
42
+ * publish a real, renderable revision and then fail the job on the visual
43
+ * reviewer's verdict (`SCENE_QUALITY_FAILED`), so the scene and the refusal are
44
+ * both true at once. That output was billed; dropping it because the status is
45
+ * not `"completed"` is what this set exists to stop.
46
+ *
47
+ * `pending` / `running` / `skipped` never carry one: nothing has settled, or
48
+ * the node was gated out.
49
+ */
50
+ export const OUTPUT_BEARING_NODE_STATUSES: ReadonlySet<NodeExecutionStatus> =
51
+ new Set<NodeExecutionStatus>(["completed", "failed"])
52
+
53
+ /** Whether a node in this status may carry `output`. */
54
+ export function nodeStateMayCarryOutput(status: string | undefined): boolean {
55
+ return (
56
+ status !== undefined &&
57
+ OUTPUT_BEARING_NODE_STATUSES.has(status as NodeExecutionStatus)
58
+ )
59
+ }
60
+
61
+ /**
62
+ * One node's execution state, as it travels on the wire.
63
+ *
64
+ * Additive by construction — an older client ignores fields it does not know,
65
+ * which is why `output` on a failed node is safe to start sending.
66
+ *
67
+ * `TOutput` lets a declaration that knows its own richer output shape (the
68
+ * orchestrator's `NodeOutput`, the editor's) stay ASSIGNABLE to this contract
69
+ * instead of restating it; the default is what an external client sees.
70
+ */
71
+ export interface NodeExecutionStateWire<TOutput = Record<string, unknown>> {
72
+ status: NodeExecutionStatus | (string & {})
73
+ nodeType?: string
74
+ jobId?: string | null
75
+ /** Every job id of a fan-out node (one per list / loop iteration). */
76
+ jobIds?: string[]
77
+ creditsUsed?: number
78
+ error?: string | null
79
+ /** Stable code for a refusal a client may branch on — never on the text. */
80
+ errorCode?: string
81
+ /**
82
+ * What the node produced.
83
+ *
84
+ * Present for a `completed` node, and for a `failed` node whose run RETAINED
85
+ * a structured result (see {@link OUTPUT_BEARING_NODE_STATUSES}). A consumer
86
+ * that reads it must therefore gate on the FIELD, not on the status, and
87
+ * must not treat its presence as success.
88
+ */
89
+ output?: TOutput
90
+ startedAt?: string | null
91
+ completedAt?: string | null
92
+ /** The node's job is parked in `pending_review`; `status` stays "running". */
93
+ awaitingReview?: boolean
94
+ progress?: number
95
+ }
@@ -45,6 +45,7 @@ export const PARAMETER_NODE_TYPES: ReadonlySet<string> = new Set([
45
45
  "post-process-effects",
46
46
  "action-fx",
47
47
  "character-fx",
48
+ "character-motion",
48
49
  "transition",
49
50
  "loop-subject",
50
51
  "scene-count",
@@ -77,6 +78,62 @@ export const HINT_EXEMPT_PARAMETER_TYPES: ReadonlySet<string> = new Set([
77
78
  "aspect-ratio",
78
79
  ])
79
80
 
81
+ /**
82
+ * Parameter pickers whose fragment only makes sense in MOTION — a camera move,
83
+ * a transition, a timeline, a character effect or a character movement. Every
84
+ * still-image consumer (generate-image, edit-image, image-to-image,
85
+ * modify-image, location) excludes these on BOTH executors and in the add-node
86
+ * popup. One set instead of three hand-synced copies: `STILL_IMAGE_EXCLUDE_TYPES`
87
+ * (frontend cinematography-hints.ts and backend payload-builder.ts) and
88
+ * `MOTION_ONLY_PICKER_TYPES` (frontend node-compatibility.ts) alias it.
89
+ */
90
+ export const VIDEO_ONLY_PARAMETER_NODE_TYPES: ReadonlySet<string> = new Set([
91
+ "camera-motion",
92
+ "temporal",
93
+ "transition",
94
+ "character-fx",
95
+ "character-motion",
96
+ ])
97
+
98
+ /**
99
+ * Pickers whose fragment depends on OTHER nodes wired into them — camera
100
+ * motion's and transition's start / end states, character motion's target and
101
+ * partner names, character FX's target name — so both executors must pass the
102
+ * graph to `getParameterPromptHint` for them.
103
+ *
104
+ * TRANSITION AND CHARACTER-FX JOINED THIS SET (signed off). They compose from
105
+ * the graph everywhere a human LOOKS at them — the config panel's injection
106
+ * preview and the canvas card both run
107
+ * `getParameterPromptHint(node, { nodes, edges })` — and on the frontend
108
+ * `{Label}` path (`execution-graph.ts :: extractNodeOutput`), but the two
109
+ * cinematography collectors that read THIS set dispatched them without the
110
+ * graph, so a wired `startState` / `target` was promised in the preview and
111
+ * dropped at execution. Admitting them here closes that gap on BOTH executors
112
+ * at once.
113
+ *
114
+ * THE BOUND, and it is proved rather than asserted: a picker with NOTHING
115
+ * wired to its own `startState` / `endState` / `target` handles emits
116
+ * byte-identical text with and without the graph — the composers are simply
117
+ * called with empty clause arrays either way. So only workflows that actually
118
+ * wire those handles change at all, and they change to the text their own
119
+ * preview already shows. The prompts package's
120
+ * `graph-composed-unwired-identity.test.ts` walks every transition and
121
+ * character-fx catalog entry in both hint modes, under two unwired graph
122
+ * shapes, and asserts that equality — with a wired positive control so it
123
+ * cannot go vacuous.
124
+ *
125
+ * ADD A NEW GRAPH-COMPOSED PICKER HERE AND TO
126
+ * `LABEL_REF_GRAPH_COMPOSED_PARAMETER_TYPES` (backend
127
+ * `services/workflow-engine/label-ref-hint-context.ts`) when it ships, so its
128
+ * `{Label}` text and its directly-wired text agree from its first release.
129
+ */
130
+ export const EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES: ReadonlySet<string> = new Set([
131
+ "camera-motion",
132
+ "character-motion",
133
+ "transition",
134
+ "character-fx",
135
+ ])
136
+
80
137
  /**
81
138
  * Extra person-dimension data-field names contributed by registered person
82
139
  * packs. Content-free (field-name strings only) — populated at runtime by
@@ -158,6 +215,8 @@ export function getParameterValue(
158
215
  return trim(data.actionFx)
159
216
  case "character-fx":
160
217
  return trim(data.characterFx)
218
+ case "character-motion":
219
+ return trim(data.characterMotion)
161
220
  case "transition":
162
221
  return trim(data.transition)
163
222
  case "style":
@@ -369,6 +369,7 @@ export const INPUT_FIELD_MAP: Record<string, InputFieldSchema> = {
369
369
  "post-process-effects": { key: "postProcess", type: "select" },
370
370
  "action-fx": { key: "actionFx", type: "select" },
371
371
  "character-fx": { key: "characterFx", type: "select" },
372
+ "character-motion": { key: "characterMotion", type: "select" },
372
373
  "transition": { key: "transition", type: "select" },
373
374
  "loop-subject": { key: "loopSubject", type: "select" },
374
375
  // --- Multi-dimension pickers: representative (first) field. Overriding only