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