tongflow 0.1.0 → 1.0.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
@@ -503,7 +503,11 @@ const ABI_NODES = {
503
503
  properties: {
504
504
  success: { type: "boolean" },
505
505
  error: { type: "string" },
506
- text: { type: "string" }
506
+ text: { type: "string" },
507
+ images: {
508
+ type: "array",
509
+ items: { $ref: "#/$defs/ImageRef" }
510
+ }
507
511
  },
508
512
  additionalProperties: false
509
513
  }
@@ -619,6 +623,34 @@ const ABI_NODES = {
619
623
  additionalProperties: false
620
624
  }
621
625
  },
626
+ "text-gen-audio": {
627
+ inputs: {
628
+ type: "object",
629
+ required: ["text"],
630
+ properties: {
631
+ text: {
632
+ type: "string",
633
+ minLength: 1
634
+ },
635
+ ref_audios: {
636
+ type: "array",
637
+ items: { $ref: "#/$defs/Asset" }
638
+ },
639
+ seed: { type: "integer" }
640
+ },
641
+ additionalProperties: false
642
+ },
643
+ outputs: {
644
+ type: "object",
645
+ required: ["success"],
646
+ properties: {
647
+ success: { type: "boolean" },
648
+ error: { type: "string" },
649
+ audio: { $ref: "#/$defs/AudioRef" }
650
+ },
651
+ additionalProperties: false
652
+ }
653
+ },
622
654
  "text-gen-speech-preset": {
623
655
  inputs: {
624
656
  type: "object",
@@ -1488,10 +1520,10 @@ const ABI_NODES = {
1488
1520
  convert_voice: {
1489
1521
  inputs: {
1490
1522
  type: "object",
1491
- required: ["sourceKey", "targetKey"],
1523
+ required: ["audio", "ref_audio"],
1492
1524
  properties: {
1493
- sourceKey: { $ref: "#/$defs/Asset" },
1494
- targetKey: { type: "string" }
1525
+ audio: { $ref: "#/$defs/Asset" },
1526
+ ref_audio: { $ref: "#/$defs/Asset" }
1495
1527
  },
1496
1528
  additionalProperties: false
1497
1529
  },
@@ -2128,6 +2160,7 @@ const NODE_TYPE_TO_ABI_FEATURE = {
2128
2160
  imageGenImageUpscaleNode: "image-upscale",
2129
2161
  textGenImageNode: "image-gen",
2130
2162
  textGenMusicNode: "gen-music",
2163
+ textGenAudioNode: "text-gen-audio",
2131
2164
  textGenSpeechCloneNode: "text-gen-speech-clone",
2132
2165
  textGenSpeechCloneComposeNode: "text-gen-speech-clone",
2133
2166
  imageGenVideoComposeNode: "image-gen-video",
@@ -2251,6 +2284,18 @@ const NODE_TYPE_SOURCE_SPEC = {
2251
2284
  manual: true
2252
2285
  })
2253
2286
  },
2287
+ musicCoverNode: {
2288
+ text: handle({
2289
+ nodeType: "textNode",
2290
+ path: "texts[0]",
2291
+ manual: true
2292
+ }),
2293
+ lyrics: handle({
2294
+ nodeType: "textNode",
2295
+ path: "texts[0]",
2296
+ manual: true
2297
+ })
2298
+ },
2254
2299
  textGenSpeechCloneComposeNode: { text: batchOn({
2255
2300
  nodeType: "textNode",
2256
2301
  path: "texts"
@@ -2288,7 +2333,10 @@ const NODE_TYPE_SOURCE_SPEC = {
2288
2333
  audioGenTextSpeechRecognizeNode: { audio: batchOn() },
2289
2334
  concatVideoComposeNode: { videos: collectAll() },
2290
2335
  concatVideoNode: { videos: collectAll() },
2291
- convertVoiceNode: { sourceKey: batchOn({ nodeType: "audioNode" }) },
2336
+ convertVoiceNode: {
2337
+ audio: batchOn({ nodeType: "audioNode" }),
2338
+ ref_audio: handle({ nodeType: "audioNode" })
2339
+ },
2292
2340
  denoiseAudioSubtitleNode: { fileKey: batchOn({ nodeType: "audioNode" }) },
2293
2341
  dropVideoNode: { videos: collectAll() },
2294
2342
  extractAudioNode: { video: batchOn() },
@@ -2336,6 +2384,14 @@ const NODE_TYPE_SOURCE_SPEC = {
2336
2384
  path: "texts[0]"
2337
2385
  })
2338
2386
  },
2387
+ textGenAudioNode: {
2388
+ text: handle({
2389
+ nodeType: "textNode",
2390
+ path: "texts[0]",
2391
+ manual: true
2392
+ }),
2393
+ ref_audios: collectAll({ nodeType: "audioNode" })
2394
+ },
2339
2395
  textGenSpeechCloneNode: {
2340
2396
  text: batchOn({
2341
2397
  nodeType: "textNode",
@@ -2580,6 +2636,381 @@ function getEdgeTargetOptions(edge, nodes) {
2580
2636
  return options;
2581
2637
  }
2582
2638
  //#endregion
2639
+ //#region src/core/abi/node-actions.ts
2640
+ const MEDIA_TYPES = [
2641
+ "imageNode",
2642
+ "videoNode",
2643
+ "audioNode"
2644
+ ];
2645
+ /**
2646
+ * Per-node-type presentation + caps. Every ABI node type must appear here (a
2647
+ * vitest guard fails otherwise) so a new slot can't quietly end up without an
2648
+ * entry point. Order groups by the modality a button hangs off: text 100s,
2649
+ * image 200s, video 300s, audio 400s, document/link/model 500s, combos 600s.
2650
+ */
2651
+ const NODE_ACTION_META = {
2652
+ splitTextNode: {
2653
+ label: "splitText",
2654
+ order: 100
2655
+ },
2656
+ genTextNode: {
2657
+ label: "generateText",
2658
+ order: 110
2659
+ },
2660
+ textGenImageNode: {
2661
+ label: "generateImage",
2662
+ order: 120
2663
+ },
2664
+ textGenMusicNode: {
2665
+ label: "generateMusic",
2666
+ order: 130,
2667
+ limits: { textNode: [1, 2] }
2668
+ },
2669
+ textGenAudioNode: {
2670
+ label: "generateAudio",
2671
+ order: 135
2672
+ },
2673
+ textGenSpeechCloneNode: {
2674
+ label: "textToSpeechClone",
2675
+ order: 140
2676
+ },
2677
+ textGenSpeechPresetNode: {
2678
+ label: "textToSpeechPreset",
2679
+ order: 150
2680
+ },
2681
+ textGenSpeechInstructNode: {
2682
+ label: "textToSpeechInstruct",
2683
+ order: 160
2684
+ },
2685
+ textGenVideoNode: {
2686
+ label: "generateVideo",
2687
+ order: 170
2688
+ },
2689
+ musicBriefNode: {
2690
+ label: "musicBrief",
2691
+ order: 180
2692
+ },
2693
+ textsGenTextNode: {
2694
+ label: "rewriteText",
2695
+ order: 190,
2696
+ limits: { textNode: [2, Number.POSITIVE_INFINITY] }
2697
+ },
2698
+ imageGenTextNode: {
2699
+ label: "describeReverse",
2700
+ order: 200,
2701
+ aliases: { modelNode: "imageNode" }
2702
+ },
2703
+ imageGenVideoNode: {
2704
+ label: "generateVideo",
2705
+ order: 210,
2706
+ aliases: { modelNode: "imageNode" }
2707
+ },
2708
+ imageGenVideoComposeNode: {
2709
+ label: "generateVideo",
2710
+ order: 211
2711
+ },
2712
+ imageGenImageNode: {
2713
+ label: "editImage",
2714
+ order: 220
2715
+ },
2716
+ imageGenImageUpscaleNode: {
2717
+ label: "upscale",
2718
+ order: 230
2719
+ },
2720
+ imageGenModelNode: {
2721
+ label: "generate3D",
2722
+ order: 240
2723
+ },
2724
+ imagePoseNode: {
2725
+ label: "detectPose",
2726
+ order: 250
2727
+ },
2728
+ imageBodySegNode: {
2729
+ label: "segmentBody",
2730
+ order: 260
2731
+ },
2732
+ imageNormalNode: {
2733
+ label: "estimateNormal",
2734
+ order: 270
2735
+ },
2736
+ imageMattingNode: {
2737
+ label: "extractForeground",
2738
+ order: 280
2739
+ },
2740
+ imageFusionNode: {
2741
+ label: "imageFusion",
2742
+ order: 290,
2743
+ limits: { imageNode: [2, 14] }
2744
+ },
2745
+ imagesGenVideoNode: {
2746
+ label: "imagesGenVideo",
2747
+ order: 291,
2748
+ limits: { imageNode: [2, 9] }
2749
+ },
2750
+ imageImageGenVideoNode: {
2751
+ label: "firstLastFrameVideo",
2752
+ order: 292,
2753
+ limits: { imageNode: [2, 2] }
2754
+ },
2755
+ videoGenTextNode: {
2756
+ label: "describeReverse",
2757
+ order: 300
2758
+ },
2759
+ videoGenTextSpeechRecognizeNode: {
2760
+ label: "speechRecognize",
2761
+ order: 310
2762
+ },
2763
+ videoUpscaleNode: {
2764
+ label: "upscale",
2765
+ order: 320
2766
+ },
2767
+ videoGenModelNode: {
2768
+ label: "captureMotion",
2769
+ order: 330
2770
+ },
2771
+ videoEditNode: {
2772
+ label: "editVideo",
2773
+ order: 340
2774
+ },
2775
+ extractAudioNode: {
2776
+ label: "extractAudioTrack",
2777
+ order: 350
2778
+ },
2779
+ removeVideoAudioNode: {
2780
+ label: "removeVideoAudio",
2781
+ order: 360
2782
+ },
2783
+ removeWatermarkNode: {
2784
+ label: "removeWatermark",
2785
+ order: 365
2786
+ },
2787
+ removeVideoSubtitleNode: {
2788
+ label: "removeSubtitle",
2789
+ order: 366
2790
+ },
2791
+ splitVideoNode: {
2792
+ label: "slice",
2793
+ order: 370
2794
+ },
2795
+ getFirstFrameNode: {
2796
+ label: "firstFrame",
2797
+ order: 380
2798
+ },
2799
+ getLastFrameNode: {
2800
+ label: "lastFrame",
2801
+ order: 390
2802
+ },
2803
+ dropVideoNode: {
2804
+ label: "filter",
2805
+ order: 391,
2806
+ groupOnly: true
2807
+ },
2808
+ arrangeNode: {
2809
+ label: "arrange",
2810
+ order: 392,
2811
+ groupOnly: true
2812
+ },
2813
+ concatVideoNode: {
2814
+ label: "concat",
2815
+ order: 393,
2816
+ groupOnly: true
2817
+ },
2818
+ concatVideoComposeNode: {
2819
+ label: "concat",
2820
+ order: 394,
2821
+ minNodes: 2
2822
+ },
2823
+ audioDescribeNode: {
2824
+ label: "describeReverse",
2825
+ order: 400
2826
+ },
2827
+ audioGenTextSpeechRecognizeNode: {
2828
+ label: "speechRecognize",
2829
+ order: 410
2830
+ },
2831
+ separateAudioTrackNode: {
2832
+ label: "separateAudio",
2833
+ order: 420
2834
+ },
2835
+ separateSpeakerNode: {
2836
+ label: "separateSpeaker",
2837
+ order: 430
2838
+ },
2839
+ separateSoundNode: {
2840
+ label: "separateSound",
2841
+ order: 440
2842
+ },
2843
+ denoiseAudioSubtitleNode: {
2844
+ label: "denoise",
2845
+ order: 450
2846
+ },
2847
+ convertVoiceNode: {
2848
+ label: "convertVoice",
2849
+ order: 460
2850
+ },
2851
+ musicRepaintNode: {
2852
+ label: "repaintMusic",
2853
+ order: 470
2854
+ },
2855
+ musicCoverNode: {
2856
+ label: "coverMusic",
2857
+ order: 480
2858
+ },
2859
+ musicExtractNode: {
2860
+ label: "extractStem",
2861
+ order: 490
2862
+ },
2863
+ musicLegoNode: {
2864
+ label: "addTrack",
2865
+ order: 491
2866
+ },
2867
+ musicCompleteNode: {
2868
+ label: "completeMusic",
2869
+ order: 492
2870
+ },
2871
+ fileGenTextNode: {
2872
+ label: "parseDocument",
2873
+ order: 500
2874
+ },
2875
+ linkGenTextNode: {
2876
+ label: "extractContent",
2877
+ order: 510
2878
+ },
2879
+ audioVideoLipSyncNode: {
2880
+ label: "lipSync",
2881
+ order: 600
2882
+ },
2883
+ mergeVideoAudioNode: {
2884
+ label: "merge",
2885
+ order: 610
2886
+ },
2887
+ speechVideoGenVideoNode: {
2888
+ label: "lipDub",
2889
+ order: 620
2890
+ },
2891
+ videoImageGenVideoMoveNode: {
2892
+ label: "videoTransfer",
2893
+ order: 630
2894
+ },
2895
+ videoImageGenVideoMixNode: {
2896
+ label: "characterReplace",
2897
+ order: 640
2898
+ },
2899
+ speechImageGenVideoNode: {
2900
+ label: "generateVideo",
2901
+ order: 650
2902
+ },
2903
+ speechTextGenVideoNode: {
2904
+ label: "generateVideo",
2905
+ order: 660
2906
+ },
2907
+ speechGenVideoNode: {
2908
+ label: "generateVideo",
2909
+ order: 661
2910
+ },
2911
+ textAudioGenSpeechNode: {
2912
+ label: "speakWithStyle",
2913
+ order: 670
2914
+ },
2915
+ textGenSpeechCloneComposeNode: {
2916
+ label: "cloneVoice",
2917
+ order: 680
2918
+ },
2919
+ refsGenVideoNode: {
2920
+ label: "refsGenVideo",
2921
+ order: 690,
2922
+ minNodes: 2,
2923
+ limits: {
2924
+ imageNode: [0, 9],
2925
+ videoNode: [0, 3],
2926
+ audioNode: [0, 3],
2927
+ textNode: [0, 1]
2928
+ },
2929
+ totalMedia: [1, 12]
2930
+ }
2931
+ };
2932
+ function capacities(nodeType) {
2933
+ const spec = resolvedSpecForNodeType(nodeType);
2934
+ if (!spec) return void 0;
2935
+ const caps = {};
2936
+ for (const field of spec.topology.inputOrder) {
2937
+ const f = spec.fields[field];
2938
+ if (f?.kind !== "handle") continue;
2939
+ const cap = caps[f.nodeType] ?? {
2940
+ max: 0,
2941
+ min: 0
2942
+ };
2943
+ caps[f.nodeType] = cap;
2944
+ cap.max = f.array || f.collect === true ? Number.POSITIVE_INFINITY : cap.max + 1;
2945
+ if (f.required && !f.manual) cap.min = Math.max(cap.min, 1);
2946
+ }
2947
+ return caps;
2948
+ }
2949
+ /** Fold aliased modalities into the modality they stand in for. */
2950
+ function applyAliases(counts, aliases) {
2951
+ if (!aliases) return counts;
2952
+ const out = { ...counts };
2953
+ for (const [from, to] of Object.entries(aliases)) {
2954
+ const n = out[from];
2955
+ if (!n) continue;
2956
+ out[to] = (out[to] ?? 0) + n;
2957
+ delete out[from];
2958
+ }
2959
+ return out;
2960
+ }
2961
+ function withinLimits(counts, meta) {
2962
+ for (const [type, range] of Object.entries(meta.limits ?? {})) {
2963
+ const n = counts[type] ?? 0;
2964
+ if (n < range[0] || n > range[1]) return false;
2965
+ }
2966
+ if (meta.totalMedia) {
2967
+ const total = MEDIA_TYPES.reduce((sum, t) => sum + (counts[t] ?? 0), 0);
2968
+ if (total < meta.totalMedia[0] || total > meta.totalMedia[1]) return false;
2969
+ }
2970
+ return true;
2971
+ }
2972
+ /**
2973
+ * Node types the given selection can be turned into, ordered for display.
2974
+ *
2975
+ * `counts` holds only modality nodes — the caller drops processing nodes and
2976
+ * `add*` nodes before calling, since neither carries an output to feed.
2977
+ */
2978
+ function nodeActionsForSelection(counts, options = {}) {
2979
+ const total = Object.values(counts).reduce((sum, n) => sum + (n ?? 0), 0);
2980
+ const out = [];
2981
+ for (const [nodeType, meta] of Object.entries(NODE_ACTION_META)) {
2982
+ if (meta.groupOnly && !(options.group && total === 1)) continue;
2983
+ if (meta.minNodes && total < meta.minNodes) continue;
2984
+ const caps = capacities(nodeType);
2985
+ if (!caps) continue;
2986
+ const effective = applyAliases(counts, meta.aliases);
2987
+ if (!withinLimits(effective, meta)) continue;
2988
+ const modalities = /* @__PURE__ */ new Set([...Object.keys(caps), ...Object.keys(effective)]);
2989
+ let ok = true;
2990
+ for (const modality of modalities) {
2991
+ const n = effective[modality] ?? 0;
2992
+ const cap = caps[modality] ?? {
2993
+ max: 0,
2994
+ min: 0
2995
+ };
2996
+ if (n > cap.max || n < cap.min) {
2997
+ ok = false;
2998
+ break;
2999
+ }
3000
+ }
3001
+ if (!ok) continue;
3002
+ const feature = NODE_TYPE_TO_ABI_FEATURE[nodeType];
3003
+ if (!feature) continue;
3004
+ out.push({
3005
+ nodeType,
3006
+ feature,
3007
+ label: meta.label,
3008
+ order: meta.order
3009
+ });
3010
+ }
3011
+ return out.sort((a, b) => a.order - b.order);
3012
+ }
3013
+ //#endregion
2583
3014
  //#region src/core/workflow/connection-validator.ts
2584
3015
  function resolveRefs(schema, depth = 0) {
2585
3016
  if (depth > 48 || typeof schema !== "object" || schema === null) return schema ?? {};
@@ -3491,6 +3922,7 @@ function applyGraphPatch(store, patch, options = {}) {
3491
3922
  const nodeData = { ...data };
3492
3923
  if (spec.pluginId) nodeData.pluginId = spec.pluginId;
3493
3924
  if (spec.pluginModel) nodeData.pluginModel = spec.pluginModel;
3925
+ if (spec.pluginParams && Object.keys(spec.pluginParams).length > 0) nodeData.pluginParams = { ...spec.pluginParams };
3494
3926
  const edgeIdx = pendingEdges.findIndex((e) => {
3495
3927
  if (e.to !== spec.alias) return false;
3496
3928
  return !!resolveNodeRef(e.from, store.getState().nodes, aliases).id;
@@ -3840,6 +4272,11 @@ const TONGFLOW_TOOL_DEFS = [
3840
4272
  type: "string",
3841
4273
  description: "Optional model id, only for plugins that advertise models."
3842
4274
  },
4275
+ pluginParams: {
4276
+ type: "object",
4277
+ description: "Optional advanced parameters, only keys the plugin advertises for this slot.",
4278
+ additionalProperties: true
4279
+ },
3843
4280
  fromAttachment: {
3844
4281
  type: "integer",
3845
4282
  description: "1-based index of a file the user attached to the chat. Use this for add*Node/modality nodes carrying user uploads — never write a storage key yourself."
@@ -4695,7 +5132,11 @@ var tongflow_abi_default = {
4695
5132
  "properties": {
4696
5133
  "success": { "type": "boolean" },
4697
5134
  "error": { "type": "string" },
4698
- "text": { "type": "string" }
5135
+ "text": { "type": "string" },
5136
+ "images": {
5137
+ "type": "array",
5138
+ "items": { "$ref": "#/$defs/ImageRef" }
5139
+ }
4699
5140
  },
4700
5141
  "additionalProperties": false
4701
5142
  }
@@ -4815,6 +5256,35 @@ var tongflow_abi_default = {
4815
5256
  "additionalProperties": false
4816
5257
  }
4817
5258
  },
5259
+ {
5260
+ "nodeSlot": "text-gen-audio",
5261
+ "inputs": {
5262
+ "type": "object",
5263
+ "required": ["text"],
5264
+ "properties": {
5265
+ "text": {
5266
+ "type": "string",
5267
+ "minLength": 1
5268
+ },
5269
+ "ref_audios": {
5270
+ "type": "array",
5271
+ "items": { "$ref": "#/$defs/Asset" }
5272
+ },
5273
+ "seed": { "type": "integer" }
5274
+ },
5275
+ "additionalProperties": false
5276
+ },
5277
+ "outputs": {
5278
+ "type": "object",
5279
+ "required": ["success"],
5280
+ "properties": {
5281
+ "success": { "type": "boolean" },
5282
+ "error": { "type": "string" },
5283
+ "audio": { "$ref": "#/$defs/AudioRef" }
5284
+ },
5285
+ "additionalProperties": false
5286
+ }
5287
+ },
4818
5288
  {
4819
5289
  "nodeSlot": "text-gen-speech-preset",
4820
5290
  "inputs": {
@@ -5717,10 +6187,10 @@ var tongflow_abi_default = {
5717
6187
  "nodeSlot": "convert_voice",
5718
6188
  "inputs": {
5719
6189
  "type": "object",
5720
- "required": ["sourceKey", "targetKey"],
6190
+ "required": ["audio", "ref_audio"],
5721
6191
  "properties": {
5722
- "sourceKey": { "$ref": "#/$defs/Asset" },
5723
- "targetKey": { "type": "string" }
6192
+ "audio": { "$ref": "#/$defs/Asset" },
6193
+ "ref_audio": { "$ref": "#/$defs/Asset" }
5724
6194
  },
5725
6195
  "additionalProperties": false
5726
6196
  },
@@ -6100,6 +6570,103 @@ const resolveLabelLookupFeatureName = clientRegistry.resolveLabelLookupFeatureNa
6100
6570
  const getFeatureByName = clientRegistry.getFeatureByName;
6101
6571
  const getFeatureRegistryAliases = clientRegistry.getFeatureRegistryAliases;
6102
6572
  //#endregion
6573
+ //#region src/core/registry/model-catalog.ts
6574
+ /** Read a dot-path (`a.b.c`) off an unknown JSON value; `undefined` when absent. */
6575
+ function getPath(value, path) {
6576
+ let cur = value;
6577
+ for (const key of path.split(".")) {
6578
+ if (cur === null || typeof cur !== "object") return void 0;
6579
+ cur = cur[key];
6580
+ }
6581
+ return cur;
6582
+ }
6583
+ /** Substring match against the field's JSON serialization (strings as-is). */
6584
+ function fieldContains(value, token) {
6585
+ if (value === void 0 || value === null) return false;
6586
+ return (typeof value === "string" ? value : JSON.stringify(value)).includes(token);
6587
+ }
6588
+ /**
6589
+ * Filter a fetched catalog payload into `slot -> model ids`, applying the
6590
+ * plugin's declared `exclude` and per-slot `slots` rules (substring tokens,
6591
+ * `!`-prefixed = must be absent). Ids keep catalog order; malformed records
6592
+ * are skipped. Pure — the fetch lives in the canvas.
6593
+ */
6594
+ function filterModelCatalog(catalog, payload) {
6595
+ const out = {};
6596
+ for (const slot of Object.keys(catalog.slots)) out[slot] = [];
6597
+ const items = getPath(payload, catalog.items);
6598
+ if (!Array.isArray(items)) return out;
6599
+ const exclude = Object.entries(catalog.exclude ?? {});
6600
+ for (const record of items) {
6601
+ const id = getPath(record, catalog.id);
6602
+ if (typeof id !== "string" || !id.trim()) continue;
6603
+ if (exclude.some(([field, literal]) => getPath(record, field) === literal)) continue;
6604
+ for (const [slot, rules] of Object.entries(catalog.slots)) if (Object.entries(rules).every(([field, tokens]) => {
6605
+ const value = getPath(record, field);
6606
+ return (Array.isArray(tokens) ? tokens : [tokens]).every((token) => token.startsWith("!") ? !fieldContains(value, token.slice(1)) : fieldContains(value, token));
6607
+ }) && !out[slot].includes(id)) out[slot].push(id);
6608
+ }
6609
+ return out;
6610
+ }
6611
+ //#endregion
6612
+ //#region src/core/registry/plugin-params.ts
6613
+ /**
6614
+ * The params a node offers for the active plugin + model: every declared entry
6615
+ * whose `models` gate is absent or names the selected model.
6616
+ */
6617
+ function visiblePluginParams(params, model) {
6618
+ if (!params) return [];
6619
+ const out = [];
6620
+ for (const [name, spec] of Object.entries(params)) {
6621
+ if (spec.models && !(model && spec.models.includes(model))) continue;
6622
+ out.push({
6623
+ name,
6624
+ spec
6625
+ });
6626
+ }
6627
+ return out;
6628
+ }
6629
+ /** True when `value` is a legal setting for `spec` (type + range/options). */
6630
+ function isValidPluginParamValue(spec, value) {
6631
+ switch (spec.type) {
6632
+ case "select": return (spec.options ?? []).some((o) => o === value);
6633
+ case "boolean": return typeof value === "boolean";
6634
+ case "text": return typeof value === "string";
6635
+ case "number":
6636
+ case "integer":
6637
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
6638
+ if (spec.type === "integer" && !Number.isInteger(value)) return false;
6639
+ if (spec.min !== void 0 && value < spec.min) return false;
6640
+ if (spec.max !== void 0 && value > spec.max) return false;
6641
+ return true;
6642
+ default: return false;
6643
+ }
6644
+ }
6645
+ /**
6646
+ * Keep only the entries of `values` that the visible params accept and that
6647
+ * differ from the declared default. Unset means "plugin default", so a value
6648
+ * equal to it is dropped; the result is what travels to the plugin.
6649
+ */
6650
+ function normalizePluginParams(entries, values) {
6651
+ const out = {};
6652
+ if (!values) return out;
6653
+ for (const { name, spec } of entries) {
6654
+ if (!(name in values)) continue;
6655
+ const v = values[name];
6656
+ if (!isValidPluginParamValue(spec, v)) continue;
6657
+ if (spec.default !== void 0 && v === spec.default) continue;
6658
+ out[name] = v;
6659
+ }
6660
+ return out;
6661
+ }
6662
+ /** Shallow key/value equality for two param records. */
6663
+ function samePluginParams(a, b) {
6664
+ const ka = Object.keys(a ?? {});
6665
+ const kb = Object.keys(b ?? {});
6666
+ if (ka.length !== kb.length) return false;
6667
+ return ka.every((k) => Object.is(a?.[k], b?.[k]));
6668
+ }
6669
+ //#endregion
6103
6670
  //#region src/core/registry/plugins-registry-schema.ts
6104
6671
  /**
6105
6672
  * Plugins registry produced by the Python scanner over `plugins/*`.
@@ -6117,12 +6684,75 @@ const getFeatureRegistryAliases = clientRegistry.getFeatureRegistryAliases;
6117
6684
  * the first plugin in directory order.
6118
6685
  * - plugins[pluginId]: how to launch that plugin's entry
6119
6686
  */
6687
+ /**
6688
+ * One entry of a plugin's `TONGFLOW_SLOT_PARAMS` declaration: a plugin-specific
6689
+ * run-time knob the node offers under its collapsed "Advanced" section. These
6690
+ * are never ABI fields — the canvas sends the user's picks top-level as
6691
+ * `params` and the plugin reads them via `tongflow.slots.current_params()`,
6692
+ * falling back to its own defaults for anything unset.
6693
+ */
6694
+ const paramLiteral = zod.z.union([
6695
+ zod.z.string(),
6696
+ zod.z.number(),
6697
+ zod.z.boolean()
6698
+ ]);
6699
+ const PluginParamSpecSchema = zod.z.object({
6700
+ type: zod.z.enum([
6701
+ "select",
6702
+ "number",
6703
+ "integer",
6704
+ "boolean",
6705
+ "text"
6706
+ ]),
6707
+ label: zod.z.string().min(1).optional(),
6708
+ description: zod.z.string().min(1).optional(),
6709
+ default: paramLiteral.optional(),
6710
+ /** `select` only. */
6711
+ options: zod.z.array(zod.z.union([zod.z.string(), zod.z.number()])).min(1).optional(),
6712
+ /** `number` / `integer` only. */
6713
+ min: zod.z.number().optional(),
6714
+ max: zod.z.number().optional(),
6715
+ step: zod.z.number().optional(),
6716
+ /** When set, only offered while one of these router models is selected. */
6717
+ models: zod.z.array(zod.z.string().min(1)).min(1).optional()
6718
+ });
6120
6719
  const PluginMethodSchema = zod.z.object({
6121
6720
  methodName: zod.z.string().min(1),
6122
6721
  /** Optional per-slot model ids a router-style plugin exposes
6123
6722
  * (`TONGFLOW_SLOT_MODELS` in the plugin source); first entry is the
6124
6723
  * default. Absent for single-model plugins. */
6125
- models: zod.z.array(zod.z.string().min(1)).optional()
6724
+ models: zod.z.array(zod.z.string().min(1)).optional(),
6725
+ /** Optional per-slot advanced parameters (`TONGFLOW_SLOT_PARAMS` in the
6726
+ * plugin source), in declared order. Absent when the plugin exposes none. */
6727
+ params: zod.z.record(zod.z.string().min(1), PluginParamSpecSchema).optional()
6728
+ });
6729
+ /**
6730
+ * Optional live model catalog (`TONGFLOW_MODEL_CATALOG` in the plugin source).
6731
+ * The canvas GETs `url` in the browser (public, CORS-enabled, no auth) — or,
6732
+ * when `authEnv` names the env key holding a bearer token, through the app's
6733
+ * `/api/plugins/model-catalog` route, which injects the stored key server-side
6734
+ * — reads the record array at dot-path `items`, each model id at dot-path `id`, drops
6735
+ * records where an `exclude` field equals its literal, and keeps a record for
6736
+ * a slot when every `slots[slot]` token is a substring of the named field
6737
+ * (arrays/objects are JSON-serialized before matching; a token prefixed with
6738
+ * `!` must be absent instead). Matching ids extend
6739
+ * that slot's dropdown after the static `models` shortlist.
6740
+ */
6741
+ const PluginModelCatalogSchema = zod.z.object({
6742
+ url: zod.z.string().url(),
6743
+ /** Env key whose value is sent as `Authorization: Bearer …`; the fetch is
6744
+ * proxied server-side so the key never reaches the browser. */
6745
+ authEnv: zod.z.string().min(1).optional(),
6746
+ items: zod.z.string().min(1).default("data"),
6747
+ id: zod.z.string().min(1).default("id"),
6748
+ exclude: zod.z.record(zod.z.string().min(1), zod.z.union([
6749
+ zod.z.string(),
6750
+ zod.z.boolean(),
6751
+ zod.z.number()
6752
+ ])).optional(),
6753
+ /** slot -> field -> token(s). Every token must be a substring of the
6754
+ * field's JSON; a token prefixed with `!` must be absent. */
6755
+ slots: zod.z.record(zod.z.string().min(1), zod.z.record(zod.z.string().min(1), zod.z.union([zod.z.string().min(1), zod.z.array(zod.z.string().min(1)).min(1)])))
6126
6756
  });
6127
6757
  const PluginConfigSchema = zod.z.object({
6128
6758
  /** Relative to repo root, e.g. `plugins/tongflow-<runner>-foo` */
@@ -6137,6 +6767,8 @@ const PluginConfigSchema = zod.z.object({
6137
6767
  * such as Modal): its entry.py deploys once before invoking. Informational —
6138
6768
  * the deploy step lives inside the plugin's entry.py. */
6139
6769
  needsDeploy: zod.z.boolean().optional(),
6770
+ /** Live model catalog the canvas can fetch to extend the model dropdown. */
6771
+ modelCatalog: PluginModelCatalogSchema.optional(),
6140
6772
  /** Presentation-only metadata merged in from `tongflow.plugin.json`'s
6141
6773
  * top-level `plugin` block (name/description/icon). Not produced by the
6142
6774
  * scanner; attached by the registry API route for the node picker. */
@@ -6262,6 +6894,10 @@ const NODE_SIZE_TABLE = {
6262
6894
  w: 520,
6263
6895
  h: 480
6264
6896
  },
6897
+ textGenAudioNode: {
6898
+ w: 480,
6899
+ h: 480
6900
+ },
6265
6901
  musicExtractNode: {
6266
6902
  w: 420,
6267
6903
  h: 400
@@ -6766,7 +7402,7 @@ function createFlowSlice(set, get, options = {}) {
6766
7402
  if (newlyCreatedIds.length > 0) get().nodeCreatedCallbacks.forEach((cb) => cb(newlyCreatedIds));
6767
7403
  return ids;
6768
7404
  },
6769
- compose: ({ type, data }) => {
7405
+ compose: ({ type, data, sourceOrder }) => {
6770
7406
  get().commitHistory();
6771
7407
  const { comboSelectedIds, nodes, edges } = get();
6772
7408
  const nodeId = createId();
@@ -6794,7 +7430,7 @@ function createFlowSlice(set, get, options = {}) {
6794
7430
  data: data ?? {}
6795
7431
  };
6796
7432
  const usedTargetHandles = /* @__PURE__ */ new Set();
6797
- const newEdges = Array.from(comboSelectedIds).map((id) => {
7433
+ const newEdges = (sourceOrder ? [...sourceOrder.filter((id) => comboSelectedIds.has(id)), ...Array.from(comboSelectedIds).filter((id) => !sourceOrder.includes(id))] : Array.from(comboSelectedIds)).map((id) => {
6798
7434
  const node = nodes.find((n) => n.id === id);
6799
7435
  if (!node) return null;
6800
7436
  const { sourceHandle, targetHandle } = resolveEdgeHandles({
@@ -6910,6 +7546,16 @@ function getUpstreamNodeData(nodeId, nodes, edges) {
6910
7546
  }];
6911
7547
  });
6912
7548
  }
7549
+ /**
7550
+ * Workflow input name for a level-0 data / add node: an explicit
7551
+ * `data.inputName` (a-z, 0-9, _ , -; set by authors so bindings read
7552
+ * `refs`, `prompt` …) else the legacy `input_<id8>`.
7553
+ */
7554
+ function inputNameFor(nodeId, nodeData) {
7555
+ const custom = nodeData.inputName;
7556
+ if (typeof custom === "string" && /^[A-Za-z][A-Za-z0-9_-]{0,63}$/.test(custom)) return custom;
7557
+ return `input_${nodeId.substring(0, 8)}`;
7558
+ }
6913
7559
  var WorkflowExporter = class {
6914
7560
  nodes;
6915
7561
  edges;
@@ -7020,16 +7666,14 @@ var WorkflowExporter = class {
7020
7666
  const texts = nodeData.texts;
7021
7667
  const hasStaticData = fileKeys && fileKeys.length > 0 || texts && texts.length > 0;
7022
7668
  const isInput = this.parser.getStartNodes().includes(node.id) && !hasStaticData;
7023
- if (isInput) {
7024
- const inputName = `input_${node.id.substring(0, 8)}`;
7025
- inputs.push({
7026
- name: inputName,
7027
- type: dataTypeInfo.outputField === "texts" ? "text[]" : "file[]",
7028
- description: `Input data for ${nodeType}`,
7029
- required: true,
7030
- nodeId: node.id
7031
- });
7032
- }
7669
+ const inputName = inputNameFor(node.id, nodeData);
7670
+ if (isInput) inputs.push({
7671
+ name: inputName,
7672
+ type: dataTypeInfo.outputField === "texts" ? "text[]" : "file[]",
7673
+ description: `Input data for ${nodeType}`,
7674
+ required: true,
7675
+ nodeId: node.id
7676
+ });
7033
7677
  const label = nodeData.label;
7034
7678
  const comment = nodeData.comment;
7035
7679
  return {
@@ -7039,7 +7683,7 @@ var WorkflowExporter = class {
7039
7683
  label,
7040
7684
  comment,
7041
7685
  isInput,
7042
- inputName: isInput ? `input_${node.id.substring(0, 8)}` : void 0,
7686
+ inputName: isInput ? inputName : void 0,
7043
7687
  staticData: hasStaticData ? {
7044
7688
  fileKeys,
7045
7689
  texts
@@ -7084,7 +7728,7 @@ var WorkflowExporter = class {
7084
7728
  const dataTypeInfo = DATA_NODE_TYPES[outputType];
7085
7729
  if (dataTypeInfo) {
7086
7730
  const isInput = level === 0;
7087
- const inputName = `input_${node.id.substring(0, 8)}`;
7731
+ const inputName = inputNameFor(node.id, nodeData);
7088
7732
  if (isInput) inputs.push({
7089
7733
  name: inputName,
7090
7734
  type: dataTypeInfo.outputField === "texts" ? "text[]" : "file[]",
@@ -7210,12 +7854,14 @@ var WorkflowExporter = class {
7210
7854
  }));
7211
7855
  const pluginId = typeof nodeData.pluginId === "string" ? nodeData.pluginId.trim() : "";
7212
7856
  const model = typeof nodeData.pluginModel === "string" ? nodeData.pluginModel.trim() : "";
7857
+ const params = nodeData.pluginParams && typeof nodeData.pluginParams === "object" && !Array.isArray(nodeData.pluginParams) && Object.keys(nodeData.pluginParams).length > 0 ? { ...nodeData.pluginParams } : void 0;
7213
7858
  return {
7214
7859
  id: node.id,
7215
7860
  type: nodeType,
7216
7861
  feature: ns.abi.feature,
7217
7862
  pluginId,
7218
7863
  ...model ? { model } : {},
7864
+ ...params ? { params } : {},
7219
7865
  label,
7220
7866
  comment,
7221
7867
  locked,
@@ -7477,11 +8123,14 @@ exports.MEDIA_NODE_REF_DISPLAY_WIDTH_PX = MEDIA_NODE_REF_DISPLAY_WIDTH_PX;
7477
8123
  exports.MEDIA_NODE_REF_LONG_EDGE_PX = MEDIA_NODE_REF_LONG_EDGE_PX;
7478
8124
  exports.MIN_SUPPORTED_ABI_VERSION = MIN_SUPPORTED_ABI_VERSION;
7479
8125
  exports.MODALITY_NODE_TYPES = MODALITY_NODE_TYPES;
8126
+ exports.NODE_ACTION_META = NODE_ACTION_META;
7480
8127
  exports.NODE_TYPE_SOURCE_SPEC = NODE_TYPE_SOURCE_SPEC;
7481
8128
  exports.NODE_TYPE_TO_ABI_FEATURE = NODE_TYPE_TO_ABI_FEATURE;
7482
8129
  exports.NodeStatus = NodeStatus;
7483
8130
  exports.PluginConfigSchema = PluginConfigSchema;
7484
8131
  exports.PluginMethodSchema = PluginMethodSchema;
8132
+ exports.PluginModelCatalogSchema = PluginModelCatalogSchema;
8133
+ exports.PluginParamSpecSchema = PluginParamSpecSchema;
7485
8134
  exports.PluginsRegistrySchema = PluginsRegistrySchema;
7486
8135
  exports.RUNNING_STATUSES = RUNNING_STATUSES;
7487
8136
  exports.SHORT_ID_LENGTH = SHORT_ID_LENGTH;
@@ -7527,6 +8176,7 @@ exports.estimateNodeSize = estimateNodeSize;
7527
8176
  exports.executeGraphTool = executeGraphTool;
7528
8177
  exports.exportWorkflow = exportWorkflow;
7529
8178
  exports.featureForNodeType = featureForNodeType;
8179
+ exports.filterModelCatalog = filterModelCatalog;
7530
8180
  exports.getAbiNodeBySlot = getAbiNodeBySlot;
7531
8181
  exports.getAbiOutputRoutesBySlot = getAbiOutputRoutesBySlot;
7532
8182
  exports.getAbiTopology = getAbiTopology;
@@ -7542,17 +8192,21 @@ exports.getWorkflowStartNodes = getWorkflowStartNodes;
7542
8192
  exports.handle = handle;
7543
8193
  exports.hasDuplicateAddSourceEdge = hasDuplicateAddSourceEdge;
7544
8194
  exports.hasDuplicateTargetHandle = hasDuplicateTargetHandle;
8195
+ exports.inputNameFor = inputNameFor;
7545
8196
  exports.isAbiNodeType = isAbiNodeType;
7546
8197
  exports.isKnownNodeType = isKnownNodeType;
7547
8198
  exports.isModalityNode = isModalityNode;
7548
8199
  exports.isRunningStatus = isRunningStatus;
7549
8200
  exports.isTerminalStatus = isTerminalStatus;
7550
8201
  exports.isValidFlowConnection = isValidFlowConnection;
8202
+ exports.isValidPluginParamValue = isValidPluginParamValue;
7551
8203
  exports.isWorkflowValid = isWorkflowValid;
7552
8204
  exports.logger = logger;
7553
8205
  exports.mapSSEStatusToTaskStatus = mapSSEStatusToTaskStatus;
7554
8206
  exports.neighborhood = neighborhood;
8207
+ exports.nodeActionsForSelection = nodeActionsForSelection;
7555
8208
  exports.nodeKind = nodeKind;
8209
+ exports.normalizePluginParams = normalizePluginParams;
7556
8210
  exports.parseExecutableWorkflow = parseExecutableWorkflow;
7557
8211
  exports.parseSourceHandleId = parseSourceHandleId;
7558
8212
  exports.parseTargetHandleId = parseTargetHandleId;
@@ -7570,6 +8224,7 @@ exports.resolveLabelLookupFeatureName = resolveLabelLookupFeatureName;
7570
8224
  exports.resolveNodeRef = resolveNodeRef;
7571
8225
  exports.resolveSpec = resolveSpec;
7572
8226
  exports.resolvedSpecForNodeType = resolvedSpecForNodeType;
8227
+ exports.samePluginParams = samePluginParams;
7573
8228
  exports.shortId = shortId;
7574
8229
  exports.singleEdgeTextScalarTargetHandles = singleEdgeTextScalarTargetHandles;
7575
8230
  exports.snapshotFlow = snapshotFlow;
@@ -7581,4 +8236,5 @@ exports.targetHandleId = targetHandleId;
7581
8236
  exports.tryAbiCompatibility = tryAbiCompatibility;
7582
8237
  exports.validateFeatureRegistryBundle = validateFeatureRegistryBundle;
7583
8238
  exports.validateWorkflow = validateWorkflow;
8239
+ exports.visiblePluginParams = visiblePluginParams;
7584
8240
  exports.workflowInput = workflowInput;