tongflow 0.2.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({
@@ -7218,12 +7854,14 @@ var WorkflowExporter = class {
7218
7854
  }));
7219
7855
  const pluginId = typeof nodeData.pluginId === "string" ? nodeData.pluginId.trim() : "";
7220
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;
7221
7858
  return {
7222
7859
  id: node.id,
7223
7860
  type: nodeType,
7224
7861
  feature: ns.abi.feature,
7225
7862
  pluginId,
7226
7863
  ...model ? { model } : {},
7864
+ ...params ? { params } : {},
7227
7865
  label,
7228
7866
  comment,
7229
7867
  locked,
@@ -7485,11 +8123,14 @@ exports.MEDIA_NODE_REF_DISPLAY_WIDTH_PX = MEDIA_NODE_REF_DISPLAY_WIDTH_PX;
7485
8123
  exports.MEDIA_NODE_REF_LONG_EDGE_PX = MEDIA_NODE_REF_LONG_EDGE_PX;
7486
8124
  exports.MIN_SUPPORTED_ABI_VERSION = MIN_SUPPORTED_ABI_VERSION;
7487
8125
  exports.MODALITY_NODE_TYPES = MODALITY_NODE_TYPES;
8126
+ exports.NODE_ACTION_META = NODE_ACTION_META;
7488
8127
  exports.NODE_TYPE_SOURCE_SPEC = NODE_TYPE_SOURCE_SPEC;
7489
8128
  exports.NODE_TYPE_TO_ABI_FEATURE = NODE_TYPE_TO_ABI_FEATURE;
7490
8129
  exports.NodeStatus = NodeStatus;
7491
8130
  exports.PluginConfigSchema = PluginConfigSchema;
7492
8131
  exports.PluginMethodSchema = PluginMethodSchema;
8132
+ exports.PluginModelCatalogSchema = PluginModelCatalogSchema;
8133
+ exports.PluginParamSpecSchema = PluginParamSpecSchema;
7493
8134
  exports.PluginsRegistrySchema = PluginsRegistrySchema;
7494
8135
  exports.RUNNING_STATUSES = RUNNING_STATUSES;
7495
8136
  exports.SHORT_ID_LENGTH = SHORT_ID_LENGTH;
@@ -7535,6 +8176,7 @@ exports.estimateNodeSize = estimateNodeSize;
7535
8176
  exports.executeGraphTool = executeGraphTool;
7536
8177
  exports.exportWorkflow = exportWorkflow;
7537
8178
  exports.featureForNodeType = featureForNodeType;
8179
+ exports.filterModelCatalog = filterModelCatalog;
7538
8180
  exports.getAbiNodeBySlot = getAbiNodeBySlot;
7539
8181
  exports.getAbiOutputRoutesBySlot = getAbiOutputRoutesBySlot;
7540
8182
  exports.getAbiTopology = getAbiTopology;
@@ -7557,11 +8199,14 @@ exports.isModalityNode = isModalityNode;
7557
8199
  exports.isRunningStatus = isRunningStatus;
7558
8200
  exports.isTerminalStatus = isTerminalStatus;
7559
8201
  exports.isValidFlowConnection = isValidFlowConnection;
8202
+ exports.isValidPluginParamValue = isValidPluginParamValue;
7560
8203
  exports.isWorkflowValid = isWorkflowValid;
7561
8204
  exports.logger = logger;
7562
8205
  exports.mapSSEStatusToTaskStatus = mapSSEStatusToTaskStatus;
7563
8206
  exports.neighborhood = neighborhood;
8207
+ exports.nodeActionsForSelection = nodeActionsForSelection;
7564
8208
  exports.nodeKind = nodeKind;
8209
+ exports.normalizePluginParams = normalizePluginParams;
7565
8210
  exports.parseExecutableWorkflow = parseExecutableWorkflow;
7566
8211
  exports.parseSourceHandleId = parseSourceHandleId;
7567
8212
  exports.parseTargetHandleId = parseTargetHandleId;
@@ -7579,6 +8224,7 @@ exports.resolveLabelLookupFeatureName = resolveLabelLookupFeatureName;
7579
8224
  exports.resolveNodeRef = resolveNodeRef;
7580
8225
  exports.resolveSpec = resolveSpec;
7581
8226
  exports.resolvedSpecForNodeType = resolvedSpecForNodeType;
8227
+ exports.samePluginParams = samePluginParams;
7582
8228
  exports.shortId = shortId;
7583
8229
  exports.singleEdgeTextScalarTargetHandles = singleEdgeTextScalarTargetHandles;
7584
8230
  exports.snapshotFlow = snapshotFlow;
@@ -7590,4 +8236,5 @@ exports.targetHandleId = targetHandleId;
7590
8236
  exports.tryAbiCompatibility = tryAbiCompatibility;
7591
8237
  exports.validateFeatureRegistryBundle = validateFeatureRegistryBundle;
7592
8238
  exports.validateWorkflow = validateWorkflow;
8239
+ exports.visiblePluginParams = visiblePluginParams;
7593
8240
  exports.workflowInput = workflowInput;