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/canvas.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { Background, BaseEdge, Controls, EdgeLabelRenderer, Handle, Panel, Position, ReactFlow, addEdge, applyEdgeChanges, applyNodeChanges, getBezierPath, reconnectEdge, useNodeId, useNodesData, useReactFlow, useStore, useStoreApi } from "@xyflow/react";
3
- import { AlertCircle, Atom, AudioLines, Box, Brain, Brush, Camera, CheckCircle, CheckIcon, ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, Circle, CircleIcon, Clapperboard, Clock, Combine, Download, Droplets, Ear, Edit3, Eye, File as File$1, FileArchive, FileCode, FileSpreadsheet, FileText, Film, HelpCircle, Image as Image$1, ImageIcon, Layers, Library, Lightbulb, Link, Loader2, Lock, Maximize2, MenuIcon, MessageSquare, Mic, MicOff, Move3d, Music, PackagePlus, Palette, Pause, Pencil, PersonStanding, Play, Plus, Presentation, Puzzle, RectangleHorizontal, RefreshCw, RotateCcw, Scissors, Sparkles, Split, Square, StopCircle, Tag, Trash, Trash2, Type, Unlock, Upload, Video, VolumeX, Wand2, X, XIcon } from "lucide-react";
3
+ import { AlertCircle, Atom, AudioLines, AudioWaveform, Box, Brain, Brush, Camera, CheckCircle, CheckIcon, ChevronDownIcon, ChevronRight, ChevronRightIcon, ChevronUpIcon, Circle, CircleIcon, Clapperboard, Clock, Combine, Download, Droplets, Ear, Edit3, Eye, File as File$1, FileArchive, FileCode, FileSpreadsheet, FileText, Film, HelpCircle, Image as Image$1, ImageIcon, Layers, Library, Lightbulb, Link, Loader2, Lock, Maximize2, MenuIcon, MessageSquare, Mic, MicOff, Move3d, Music, PackagePlus, Palette, Pause, Pencil, PersonStanding, Play, Plus, Presentation, Puzzle, RectangleHorizontal, RefreshCw, RotateCcw, Scissors, Sparkles, Split, Square, StopCircle, Tag, Trash, Trash2, Type, Unlock, Upload, Video, VolumeX, Wand2, X, XIcon } from "lucide-react";
4
4
  import * as React$1 from "react";
5
5
  import React, { Component, forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
6
6
  import toast from "react-hot-toast";
@@ -23,9 +23,9 @@ import * as TabsPrimitive from "@radix-ui/react-tabs";
23
23
  import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
24
24
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
25
25
  import * as LabelPrimitive from "@radix-ui/react-label";
26
+ import * as SwitchPrimitive from "@radix-ui/react-switch";
26
27
  import * as ProgressPrimitive from "@radix-ui/react-progress";
27
28
  import * as SliderPrimitive from "@radix-ui/react-slider";
28
- import * as SwitchPrimitive from "@radix-ui/react-switch";
29
29
  import { createPortal } from "react-dom";
30
30
  import { Masonry } from "masonic";
31
31
  import * as THREE from "three";
@@ -545,7 +545,11 @@ const ABI_NODES = {
545
545
  properties: {
546
546
  success: { type: "boolean" },
547
547
  error: { type: "string" },
548
- text: { type: "string" }
548
+ text: { type: "string" },
549
+ images: {
550
+ type: "array",
551
+ items: { $ref: "#/$defs/ImageRef" }
552
+ }
549
553
  },
550
554
  additionalProperties: false
551
555
  }
@@ -661,6 +665,34 @@ const ABI_NODES = {
661
665
  additionalProperties: false
662
666
  }
663
667
  },
668
+ "text-gen-audio": {
669
+ inputs: {
670
+ type: "object",
671
+ required: ["text"],
672
+ properties: {
673
+ text: {
674
+ type: "string",
675
+ minLength: 1
676
+ },
677
+ ref_audios: {
678
+ type: "array",
679
+ items: { $ref: "#/$defs/Asset" }
680
+ },
681
+ seed: { type: "integer" }
682
+ },
683
+ additionalProperties: false
684
+ },
685
+ outputs: {
686
+ type: "object",
687
+ required: ["success"],
688
+ properties: {
689
+ success: { type: "boolean" },
690
+ error: { type: "string" },
691
+ audio: { $ref: "#/$defs/AudioRef" }
692
+ },
693
+ additionalProperties: false
694
+ }
695
+ },
664
696
  "text-gen-speech-preset": {
665
697
  inputs: {
666
698
  type: "object",
@@ -1530,10 +1562,10 @@ const ABI_NODES = {
1530
1562
  convert_voice: {
1531
1563
  inputs: {
1532
1564
  type: "object",
1533
- required: ["sourceKey", "targetKey"],
1565
+ required: ["audio", "ref_audio"],
1534
1566
  properties: {
1535
- sourceKey: { $ref: "#/$defs/Asset" },
1536
- targetKey: { type: "string" }
1567
+ audio: { $ref: "#/$defs/Asset" },
1568
+ ref_audio: { $ref: "#/$defs/Asset" }
1537
1569
  },
1538
1570
  additionalProperties: false
1539
1571
  },
@@ -2140,6 +2172,7 @@ const NODE_TYPE_TO_ABI_FEATURE = {
2140
2172
  imageGenImageUpscaleNode: "image-upscale",
2141
2173
  textGenImageNode: "image-gen",
2142
2174
  textGenMusicNode: "gen-music",
2175
+ textGenAudioNode: "text-gen-audio",
2143
2176
  textGenSpeechCloneNode: "text-gen-speech-clone",
2144
2177
  textGenSpeechCloneComposeNode: "text-gen-speech-clone",
2145
2178
  imageGenVideoComposeNode: "image-gen-video",
@@ -2263,6 +2296,18 @@ const NODE_TYPE_SOURCE_SPEC = {
2263
2296
  manual: true
2264
2297
  })
2265
2298
  },
2299
+ musicCoverNode: {
2300
+ text: handle({
2301
+ nodeType: "textNode",
2302
+ path: "texts[0]",
2303
+ manual: true
2304
+ }),
2305
+ lyrics: handle({
2306
+ nodeType: "textNode",
2307
+ path: "texts[0]",
2308
+ manual: true
2309
+ })
2310
+ },
2266
2311
  textGenSpeechCloneComposeNode: { text: batchOn({
2267
2312
  nodeType: "textNode",
2268
2313
  path: "texts"
@@ -2300,7 +2345,10 @@ const NODE_TYPE_SOURCE_SPEC = {
2300
2345
  audioGenTextSpeechRecognizeNode: { audio: batchOn() },
2301
2346
  concatVideoComposeNode: { videos: collectAll() },
2302
2347
  concatVideoNode: { videos: collectAll() },
2303
- convertVoiceNode: { sourceKey: batchOn({ nodeType: "audioNode" }) },
2348
+ convertVoiceNode: {
2349
+ audio: batchOn({ nodeType: "audioNode" }),
2350
+ ref_audio: handle({ nodeType: "audioNode" })
2351
+ },
2304
2352
  denoiseAudioSubtitleNode: { fileKey: batchOn({ nodeType: "audioNode" }) },
2305
2353
  dropVideoNode: { videos: collectAll() },
2306
2354
  extractAudioNode: { video: batchOn() },
@@ -2348,6 +2396,14 @@ const NODE_TYPE_SOURCE_SPEC = {
2348
2396
  path: "texts[0]"
2349
2397
  })
2350
2398
  },
2399
+ textGenAudioNode: {
2400
+ text: handle({
2401
+ nodeType: "textNode",
2402
+ path: "texts[0]",
2403
+ manual: true
2404
+ }),
2405
+ ref_audios: collectAll({ nodeType: "audioNode" })
2406
+ },
2351
2407
  textGenSpeechCloneNode: {
2352
2408
  text: batchOn({
2353
2409
  nodeType: "textNode",
@@ -3831,7 +3887,11 @@ var tongflow_abi_default = {
3831
3887
  "properties": {
3832
3888
  "success": { "type": "boolean" },
3833
3889
  "error": { "type": "string" },
3834
- "text": { "type": "string" }
3890
+ "text": { "type": "string" },
3891
+ "images": {
3892
+ "type": "array",
3893
+ "items": { "$ref": "#/$defs/ImageRef" }
3894
+ }
3835
3895
  },
3836
3896
  "additionalProperties": false
3837
3897
  }
@@ -3951,6 +4011,35 @@ var tongflow_abi_default = {
3951
4011
  "additionalProperties": false
3952
4012
  }
3953
4013
  },
4014
+ {
4015
+ "nodeSlot": "text-gen-audio",
4016
+ "inputs": {
4017
+ "type": "object",
4018
+ "required": ["text"],
4019
+ "properties": {
4020
+ "text": {
4021
+ "type": "string",
4022
+ "minLength": 1
4023
+ },
4024
+ "ref_audios": {
4025
+ "type": "array",
4026
+ "items": { "$ref": "#/$defs/Asset" }
4027
+ },
4028
+ "seed": { "type": "integer" }
4029
+ },
4030
+ "additionalProperties": false
4031
+ },
4032
+ "outputs": {
4033
+ "type": "object",
4034
+ "required": ["success"],
4035
+ "properties": {
4036
+ "success": { "type": "boolean" },
4037
+ "error": { "type": "string" },
4038
+ "audio": { "$ref": "#/$defs/AudioRef" }
4039
+ },
4040
+ "additionalProperties": false
4041
+ }
4042
+ },
3954
4043
  {
3955
4044
  "nodeSlot": "text-gen-speech-preset",
3956
4045
  "inputs": {
@@ -4853,10 +4942,10 @@ var tongflow_abi_default = {
4853
4942
  "nodeSlot": "convert_voice",
4854
4943
  "inputs": {
4855
4944
  "type": "object",
4856
- "required": ["sourceKey", "targetKey"],
4945
+ "required": ["audio", "ref_audio"],
4857
4946
  "properties": {
4858
- "sourceKey": { "$ref": "#/$defs/Asset" },
4859
- "targetKey": { "type": "string" }
4947
+ "audio": { "$ref": "#/$defs/Asset" },
4948
+ "ref_audio": { "$ref": "#/$defs/Asset" }
4860
4949
  },
4861
4950
  "additionalProperties": false
4862
4951
  },
@@ -5139,6 +5228,103 @@ parsed.nodes;
5139
5228
  function getAbiNodeBySlot(nodeSlot) {
5140
5229
  return bySlot.get(nodeSlot);
5141
5230
  }
5231
+ //#endregion
5232
+ //#region src/core/registry/model-catalog.ts
5233
+ /** Read a dot-path (`a.b.c`) off an unknown JSON value; `undefined` when absent. */
5234
+ function getPath(value, path) {
5235
+ let cur = value;
5236
+ for (const key of path.split(".")) {
5237
+ if (cur === null || typeof cur !== "object") return void 0;
5238
+ cur = cur[key];
5239
+ }
5240
+ return cur;
5241
+ }
5242
+ /** Substring match against the field's JSON serialization (strings as-is). */
5243
+ function fieldContains(value, token) {
5244
+ if (value === void 0 || value === null) return false;
5245
+ return (typeof value === "string" ? value : JSON.stringify(value)).includes(token);
5246
+ }
5247
+ /**
5248
+ * Filter a fetched catalog payload into `slot -> model ids`, applying the
5249
+ * plugin's declared `exclude` and per-slot `slots` rules (substring tokens,
5250
+ * `!`-prefixed = must be absent). Ids keep catalog order; malformed records
5251
+ * are skipped. Pure — the fetch lives in the canvas.
5252
+ */
5253
+ function filterModelCatalog(catalog, payload) {
5254
+ const out = {};
5255
+ for (const slot of Object.keys(catalog.slots)) out[slot] = [];
5256
+ const items = getPath(payload, catalog.items);
5257
+ if (!Array.isArray(items)) return out;
5258
+ const exclude = Object.entries(catalog.exclude ?? {});
5259
+ for (const record of items) {
5260
+ const id = getPath(record, catalog.id);
5261
+ if (typeof id !== "string" || !id.trim()) continue;
5262
+ if (exclude.some(([field, literal]) => getPath(record, field) === literal)) continue;
5263
+ for (const [slot, rules] of Object.entries(catalog.slots)) if (Object.entries(rules).every(([field, tokens]) => {
5264
+ const value = getPath(record, field);
5265
+ return (Array.isArray(tokens) ? tokens : [tokens]).every((token) => token.startsWith("!") ? !fieldContains(value, token.slice(1)) : fieldContains(value, token));
5266
+ }) && !out[slot].includes(id)) out[slot].push(id);
5267
+ }
5268
+ return out;
5269
+ }
5270
+ //#endregion
5271
+ //#region src/core/registry/plugin-params.ts
5272
+ /**
5273
+ * The params a node offers for the active plugin + model: every declared entry
5274
+ * whose `models` gate is absent or names the selected model.
5275
+ */
5276
+ function visiblePluginParams(params, model) {
5277
+ if (!params) return [];
5278
+ const out = [];
5279
+ for (const [name, spec] of Object.entries(params)) {
5280
+ if (spec.models && !(model && spec.models.includes(model))) continue;
5281
+ out.push({
5282
+ name,
5283
+ spec
5284
+ });
5285
+ }
5286
+ return out;
5287
+ }
5288
+ /** True when `value` is a legal setting for `spec` (type + range/options). */
5289
+ function isValidPluginParamValue(spec, value) {
5290
+ switch (spec.type) {
5291
+ case "select": return (spec.options ?? []).some((o) => o === value);
5292
+ case "boolean": return typeof value === "boolean";
5293
+ case "text": return typeof value === "string";
5294
+ case "number":
5295
+ case "integer":
5296
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
5297
+ if (spec.type === "integer" && !Number.isInteger(value)) return false;
5298
+ if (spec.min !== void 0 && value < spec.min) return false;
5299
+ if (spec.max !== void 0 && value > spec.max) return false;
5300
+ return true;
5301
+ default: return false;
5302
+ }
5303
+ }
5304
+ /**
5305
+ * Keep only the entries of `values` that the visible params accept and that
5306
+ * differ from the declared default. Unset means "plugin default", so a value
5307
+ * equal to it is dropped; the result is what travels to the plugin.
5308
+ */
5309
+ function normalizePluginParams(entries, values) {
5310
+ const out = {};
5311
+ if (!values) return out;
5312
+ for (const { name, spec } of entries) {
5313
+ if (!(name in values)) continue;
5314
+ const v = values[name];
5315
+ if (!isValidPluginParamValue(spec, v)) continue;
5316
+ if (spec.default !== void 0 && v === spec.default) continue;
5317
+ out[name] = v;
5318
+ }
5319
+ return out;
5320
+ }
5321
+ /** Shallow key/value equality for two param records. */
5322
+ function samePluginParams(a, b) {
5323
+ const ka = Object.keys(a ?? {});
5324
+ const kb = Object.keys(b ?? {});
5325
+ if (ka.length !== kb.length) return false;
5326
+ return ka.every((k) => Object.is(a?.[k], b?.[k]));
5327
+ }
5142
5328
  /**
5143
5329
  * Deep-copy the current canvas state for the history stack, stripping
5144
5330
  * transient interaction flags so undo never restores a stale selection
@@ -5233,6 +5419,10 @@ const NODE_SIZE_TABLE = {
5233
5419
  w: 520,
5234
5420
  h: 480
5235
5421
  },
5422
+ textGenAudioNode: {
5423
+ w: 480,
5424
+ h: 480
5425
+ },
5236
5426
  musicExtractNode: {
5237
5427
  w: 420,
5238
5428
  h: 400
@@ -5737,7 +5927,7 @@ function createFlowSlice(set, get, options = {}) {
5737
5927
  if (newlyCreatedIds.length > 0) get().nodeCreatedCallbacks.forEach((cb) => cb(newlyCreatedIds));
5738
5928
  return ids;
5739
5929
  },
5740
- compose: ({ type, data }) => {
5930
+ compose: ({ type, data, sourceOrder }) => {
5741
5931
  get().commitHistory();
5742
5932
  const { comboSelectedIds, nodes, edges } = get();
5743
5933
  const nodeId = createId();
@@ -5765,7 +5955,7 @@ function createFlowSlice(set, get, options = {}) {
5765
5955
  data: data ?? {}
5766
5956
  };
5767
5957
  const usedTargetHandles = /* @__PURE__ */ new Set();
5768
- const newEdges = Array.from(comboSelectedIds).map((id) => {
5958
+ const newEdges = (sourceOrder ? [...sourceOrder.filter((id) => comboSelectedIds.has(id)), ...Array.from(comboSelectedIds).filter((id) => !sourceOrder.includes(id))] : Array.from(comboSelectedIds)).map((id) => {
5769
5959
  const node = nodes.find((n) => n.id === id);
5770
5960
  if (!node) return null;
5771
5961
  const { sourceHandle, targetHandle } = resolveEdgeHandles({
@@ -5971,6 +6161,7 @@ var en_default = {
5971
6161
  "separateAudio": "Separate Vocals",
5972
6162
  "separateSpeaker": "Speaker Separation",
5973
6163
  "separateSound": "Extract Sound",
6164
+ "speakWithStyle": "Speak with Style",
5974
6165
  "denoise": "Denoise",
5975
6166
  "convertVoice": "Voice Replacement",
5976
6167
  "filter": "Filter",
@@ -6108,6 +6299,9 @@ var en_default = {
6108
6299
  "modalDeployProgressHint": "Deployed {done}/{total}",
6109
6300
  "pluginImplementationTitle": "Implementation",
6110
6301
  "pluginModelTitle": "Model",
6302
+ "pluginParamsTitle": "Advanced",
6303
+ "pluginParamsReset": "Reset",
6304
+ "pluginParamDefault": "default {value}",
6111
6305
  "pluginImplementationLoading": "Loading plugin implementations…",
6112
6306
  "pluginImplementationEmpty": "No plugin implementations were scanned for this capability. Add Modal or API plugins under the repo's plugins/ directory (see README), then reload this page.",
6113
6307
  "pluginRegistryLoadError": "Could not load the plugin registry: {message}",
@@ -6233,11 +6427,7 @@ var en_default = {
6233
6427
  "referenceTextPlaceholder": "Optional: transcript of the reference audio — improves clone quality",
6234
6428
  "referenceTextCloneHint": "Leave empty for fast x-vector clone; paste the reference transcript for higher-quality ICL clone",
6235
6429
  "language": "Language",
6236
- "inputText": "Input Text",
6237
- "voiceOptions": {
6238
- "female": "Female Voice",
6239
- "male": "Male Voice"
6240
- }
6430
+ "inputText": "Input Text"
6241
6431
  },
6242
6432
  "titles": {
6243
6433
  "imageGenVideo": "Image to Video",
@@ -6301,7 +6491,8 @@ var en_default = {
6301
6491
  "musicExtract": "Extract Stem",
6302
6492
  "musicLego": "Add Track",
6303
6493
  "musicComplete": "Complete Arrangement",
6304
- "musicBrief": "Music Brief"
6494
+ "musicBrief": "Music Brief",
6495
+ "textGenAudio": "Text to Audio"
6305
6496
  },
6306
6497
  "options": {
6307
6498
  "portrait": "Portrait",
@@ -6608,15 +6799,9 @@ var en_default = {
6608
6799
  "audioPromptPlaceholder": "Enter audio dubbing prompt"
6609
6800
  },
6610
6801
  "convertVoice": {
6611
- "voiceLabel": "Voice:",
6612
- "selectVoice": "Select Voice",
6613
- "previewVoice": "Preview Voice",
6614
- "uploadVoice": "Upload Voice",
6615
- "recordVoice": "Record Voice",
6616
- "recordAudio": "Record Audio",
6617
- "recordHint": "You can record audio here.",
6618
- "recordNeedImpl": "Audio recording feature needs implementation",
6619
- "done": "Done"
6802
+ "sourceAudio": "Source audio",
6803
+ "referenceVoice": "Reference voice",
6804
+ "connectHint": "Connect the audio to convert and a reference voice clip"
6620
6805
  },
6621
6806
  "removeWatermark": { "hint": "Automatically detect and remove Sora video watermarks" },
6622
6807
  "videoGenText": {
@@ -6810,6 +6995,12 @@ var en_default = {
6810
6995
  "separateSound": {
6811
6996
  "textLabel": "Sound to isolate",
6812
6997
  "textPlaceholder": "e.g. dog barking, piano, sirens in the background"
6998
+ },
6999
+ "textGenAudio": {
7000
+ "voiceReference": "Reference voices",
7001
+ "promptLabel": "Audio prompt",
7002
+ "promptPlaceholder": "Describe the whole scene: who speaks and how, background ambience, sound effects. Quote the lines, e.g. @voice1 says: \"It's sunny today.\" Rain patters on the window.",
7003
+ "hint": "One pass renders speech, sound effects and ambience together. Connect up to 3 audio clips as reference voices and click one to insert @voice1…@voice3."
6813
7004
  }
6814
7005
  }
6815
7006
  },
@@ -6872,6 +7063,7 @@ var en_default = {
6872
7063
  "copy": "Copy",
6873
7064
  "copied": "Copied",
6874
7065
  "close": "Close",
7066
+ "openModalBilling": "Open Modal billing",
6875
7067
  "needHelp": "Stuck? Come find us:",
6876
7068
  "wechatGroup": "WeChat group",
6877
7069
  "wechatQrAlt": "WeChat group QR code"
@@ -6958,7 +7150,7 @@ var ja_default = {
6958
7150
  "saveSuccess": "保存しました!",
6959
7151
  "saveFailed": "保存に失敗しました。もう一度お試しください",
6960
7152
  "generateVideo": "動画生成",
6961
- "generateAudio": "音声生成",
7153
+ "generateAudio": "オーディオ生成",
6962
7154
  "generateText": "テキスト生成",
6963
7155
  "generateImage": "画像生成",
6964
7156
  "generateMusic": "音楽生成",
@@ -7003,6 +7195,7 @@ var ja_default = {
7003
7195
  "separateAudio": "ボーカル分離",
7004
7196
  "separateSpeaker": "話者分離",
7005
7197
  "separateSound": "音を抽出",
7198
+ "speakWithStyle": "感情付き音声",
7006
7199
  "denoise": "ノイズ除去",
7007
7200
  "convertVoice": "音声置換",
7008
7201
  "filter": "フィルター",
@@ -7140,6 +7333,9 @@ var ja_default = {
7140
7333
  "modalDeployProgressHint": "{done}/{total} をデプロイ済み",
7141
7334
  "pluginImplementationTitle": "実装",
7142
7335
  "pluginModelTitle": "モデル",
7336
+ "pluginParamsTitle": "詳細設定",
7337
+ "pluginParamsReset": "リセット",
7338
+ "pluginParamDefault": "既定 {value}",
7143
7339
  "pluginImplementationLoading": "プラグイン実装一覧を読み込み中…",
7144
7340
  "pluginImplementationEmpty": "この機能向けのプラグイン実装がスキャン結果にありません。リポジトリの plugins/ に Modal または API プラグインを追加してください(README 参照)。ページを再読み込みしてください。",
7145
7341
  "pluginRegistryLoadError": "プラグイン登録情報を読み込めませんでした:{message}",
@@ -7265,11 +7461,7 @@ var ja_default = {
7265
7461
  "referenceTextPlaceholder": "任意:参照音声の書き起こしテキスト。クローン品質の向上に役立ちます",
7266
7462
  "referenceTextCloneHint": "空欄の場合は x-vector クローン。転写テキストを入力すると ICL 高品質クローンになります",
7267
7463
  "language": "言語",
7268
- "inputText": "テキスト入力",
7269
- "voiceOptions": {
7270
- "female": "女性の声",
7271
- "male": "男性の声"
7272
- }
7464
+ "inputText": "テキスト入力"
7273
7465
  },
7274
7466
  "titles": {
7275
7467
  "imageGenVideo": "画像から動画",
@@ -7333,7 +7525,8 @@ var ja_default = {
7333
7525
  "musicExtract": "ステム抽出",
7334
7526
  "musicLego": "トラック追加",
7335
7527
  "musicComplete": "編曲補完",
7336
- "musicBrief": "音楽ブリーフ"
7528
+ "musicBrief": "音楽ブリーフ",
7529
+ "textGenAudio": "テキストからオーディオ"
7337
7530
  },
7338
7531
  "options": {
7339
7532
  "portrait": "縦向き",
@@ -7356,7 +7549,7 @@ var ja_default = {
7356
7549
  "describeVideo": "動画を説明",
7357
7550
  "recognizeSpeech": "音声を認識",
7358
7551
  "generateMusic": "音楽を生成",
7359
- "generateAudio": "音声を生成",
7552
+ "generateAudio": "オーディオを生成",
7360
7553
  "startReplace": "置換を開始",
7361
7554
  "startDenoise": "ノイズ除去を開始",
7362
7555
  "parseDocument": "ドキュメントを解析",
@@ -7554,15 +7747,9 @@ var ja_default = {
7554
7747
  "audioPromptPlaceholder": "音声吹き替えのプロンプトを入力"
7555
7748
  },
7556
7749
  "convertVoice": {
7557
- "voiceLabel": "音声:",
7558
- "selectVoice": "音声を選択",
7559
- "previewVoice": "音声をプレビュー",
7560
- "uploadVoice": "音声をアップロード",
7561
- "recordVoice": "音声を録音",
7562
- "recordAudio": "音声を録音",
7563
- "recordHint": "ここで音声を録音できます。",
7564
- "recordNeedImpl": "音声録音機能は実装が必要です",
7565
- "done": "完了"
7750
+ "sourceAudio": "元の音声",
7751
+ "referenceVoice": "参考ボイス",
7752
+ "connectHint": "変換したい音声と、参考にする声の音声をつないでください"
7566
7753
  },
7567
7754
  "removeWatermark": { "hint": "Sora動画の透かしを自動検出して削除" },
7568
7755
  "videoGenText": {
@@ -7756,6 +7943,12 @@ var ja_default = {
7756
7943
  "separateSound": {
7757
7944
  "textLabel": "抽出したい音",
7758
7945
  "textPlaceholder": "例:犬の鳴き声、ピアノ、背景のサイレン"
7946
+ },
7947
+ "textGenAudio": {
7948
+ "voiceReference": "参照ボイス",
7949
+ "promptLabel": "オーディオの説明",
7950
+ "promptPlaceholder": "場面をまるごと書きます:誰がどう話すか、背景の環境音、効果音。セリフは引用符で囲みます。例:@voice1 が言う:「今日はいい天気だね。」窓の外で小雨が降っている。",
7951
+ "hint": "話し声・効果音・環境音を一度に生成します。参照ボイスとして音声を最大 3 本つなぎ、クリックすると @voice1…@voice3 が挿入されます。"
7759
7952
  }
7760
7953
  }
7761
7954
  },
@@ -7818,6 +8011,7 @@ var ja_default = {
7818
8011
  "copy": "コピー",
7819
8012
  "copied": "コピーしました",
7820
8013
  "close": "閉じる",
8014
+ "openModalBilling": "Modal の請求設定を開く",
7821
8015
  "needHelp": "解決しない場合はこちらへ:",
7822
8016
  "wechatGroup": "WeChat グループ",
7823
8017
  "wechatQrAlt": "WeChat グループの QR コード"
@@ -7949,6 +8143,7 @@ var ko_default = {
7949
8143
  "separateAudio": "보컬 분리",
7950
8144
  "separateSpeaker": "화자 분리",
7951
8145
  "separateSound": "소리 추출",
8146
+ "speakWithStyle": "감정 음성 생성",
7952
8147
  "denoise": "노이즈 제거",
7953
8148
  "convertVoice": "음성 교체",
7954
8149
  "filter": "필터",
@@ -8086,6 +8281,9 @@ var ko_default = {
8086
8281
  "modalDeployProgressHint": "{done}/{total} 배포됨",
8087
8282
  "pluginImplementationTitle": "구현",
8088
8283
  "pluginModelTitle": "모델",
8284
+ "pluginParamsTitle": "고급 설정",
8285
+ "pluginParamsReset": "초기화",
8286
+ "pluginParamDefault": "기본 {value}",
8089
8287
  "pluginImplementationLoading": "플러그인 구현을 불러오는 중…",
8090
8288
  "pluginImplementationEmpty": "이 기능에 대해 스캔된 플러그인 구현이 없습니다. 저장소의 plugins/ 디렉터리 아래에 Modal 또는 API 플러그인을 추가한 후(README 참조) 이 페이지를 새로 고치세요.",
8091
8289
  "pluginRegistryLoadError": "플러그인 레지스트리를 불러올 수 없습니다: {message}",
@@ -8211,11 +8409,7 @@ var ko_default = {
8211
8409
  "referenceTextPlaceholder": "선택 사항: 참조 오디오의 전사본 — 복제 품질을 향상시킵니다",
8212
8410
  "referenceTextCloneHint": "빠른 x-vector 복제는 비워 두고, 더 높은 품질의 ICL 복제를 위해서는 참조 전사본을 붙여넣으세요",
8213
8411
  "language": "언어",
8214
- "inputText": "입력 텍스트",
8215
- "voiceOptions": {
8216
- "female": "여성 목소리",
8217
- "male": "남성 목소리"
8218
- }
8412
+ "inputText": "입력 텍스트"
8219
8413
  },
8220
8414
  "titles": {
8221
8415
  "imageGenVideo": "이미지-비디오",
@@ -8279,7 +8473,8 @@ var ko_default = {
8279
8473
  "musicExtract": "스템 추출",
8280
8474
  "musicLego": "트랙 추가",
8281
8475
  "musicComplete": "편곡 완성",
8282
- "musicBrief": "음악 브리프"
8476
+ "musicBrief": "음악 브리프",
8477
+ "textGenAudio": "텍스트로 오디오 생성"
8283
8478
  },
8284
8479
  "options": {
8285
8480
  "portrait": "세로",
@@ -8586,15 +8781,9 @@ var ko_default = {
8586
8781
  "audioPromptPlaceholder": "오디오 더빙 프롬프트 입력"
8587
8782
  },
8588
8783
  "convertVoice": {
8589
- "voiceLabel": "음성:",
8590
- "selectVoice": "음성 선택",
8591
- "previewVoice": "음성 미리듣기",
8592
- "uploadVoice": "음성 업로드",
8593
- "recordVoice": "음성 녹음",
8594
- "recordAudio": "오디오 녹음",
8595
- "recordHint": "여기에서 오디오를 녹음할 수 있습니다.",
8596
- "recordNeedImpl": "오디오 녹음 기능 구현이 필요합니다",
8597
- "done": "완료"
8784
+ "sourceAudio": "원본 오디오",
8785
+ "referenceVoice": "참고 목소리",
8786
+ "connectHint": "바꿀 오디오와 참고할 목소리 오디오를 연결하세요"
8598
8787
  },
8599
8788
  "removeWatermark": { "hint": "Sora 비디오 워터마크를 자동으로 감지하고 제거합니다" },
8600
8789
  "videoGenText": {
@@ -8788,6 +8977,12 @@ var ko_default = {
8788
8977
  "separateSound": {
8789
8978
  "textLabel": "추출할 소리",
8790
8979
  "textPlaceholder": "예: 개 짖는 소리, 피아노, 배경 사이렌"
8980
+ },
8981
+ "textGenAudio": {
8982
+ "voiceReference": "참조 음색",
8983
+ "promptLabel": "오디오 설명",
8984
+ "promptPlaceholder": "장면 전체를 적어 주세요. 누가 어떻게 말하는지, 배경 소리, 효과음까지. 대사는 따옴표로 묶어요. 예: @voice1이 말한다: \"오늘 날씨 좋다.\" 창밖에 가랑비가 내린다.",
8985
+ "hint": "말소리, 효과음, 배경음을 한 번에 만들어요. 참조 음색으로 오디오를 최대 3개까지 연결하고, 누르면 @voice1…@voice3이 설명에 들어가요."
8791
8986
  }
8792
8987
  }
8793
8988
  },
@@ -8850,6 +9045,7 @@ var ko_default = {
8850
9045
  "copy": "복사",
8851
9046
  "copied": "복사됨",
8852
9047
  "close": "닫기",
9048
+ "openModalBilling": "Modal 결제 설정 열기",
8853
9049
  "needHelp": "해결이 안 되면 커뮤니티로 오세요:",
8854
9050
  "wechatGroup": "WeChat 그룹",
8855
9051
  "wechatQrAlt": "WeChat 그룹 QR 코드"
@@ -8936,7 +9132,7 @@ var zh_default = {
8936
9132
  "saveSuccess": "保存成功!",
8937
9133
  "saveFailed": "保存失败,请重试",
8938
9134
  "generateVideo": "生成视频",
8939
- "generateAudio": "生成语音",
9135
+ "generateAudio": "生成音频",
8940
9136
  "generateText": "生成文本",
8941
9137
  "generateImage": "生成图片",
8942
9138
  "generateMusic": "生成音乐",
@@ -8981,6 +9177,7 @@ var zh_default = {
8981
9177
  "separateAudio": "人声分离",
8982
9178
  "separateSpeaker": "说话人分离",
8983
9179
  "separateSound": "提取声音",
9180
+ "speakWithStyle": "按情绪配音",
8984
9181
  "denoise": "降噪",
8985
9182
  "convertVoice": "人声替换",
8986
9183
  "filter": "筛选",
@@ -9118,6 +9315,9 @@ var zh_default = {
9118
9315
  "modalDeployProgressHint": "已部署 {done}/{total}",
9119
9316
  "pluginImplementationTitle": "实现",
9120
9317
  "pluginModelTitle": "模型",
9318
+ "pluginParamsTitle": "高级参数",
9319
+ "pluginParamsReset": "重置",
9320
+ "pluginParamDefault": "默认 {value}",
9121
9321
  "pluginImplementationLoading": "正在加载插件实现列表…",
9122
9322
  "pluginImplementationEmpty": "未扫描到适用于该能力的插件实现。请在仓库的 plugins/ 目录下添加 Modal 或 API 插件(见 README),然后刷新页面。",
9123
9323
  "pluginRegistryLoadError": "无法加载插件注册表:{message}",
@@ -9243,11 +9443,7 @@ var zh_default = {
9243
9443
  "referenceTextPlaceholder": "可选:参考音频的转写文本,有助于提升克隆质量",
9244
9444
  "referenceTextCloneHint": "不填则使用 x-vector 快速克隆;填写参考音频转写文本可启用更高质量的 ICL 克隆",
9245
9445
  "language": "语言",
9246
- "inputText": "输入文本",
9247
- "voiceOptions": {
9248
- "female": "普通女声",
9249
- "male": "普通男声"
9250
- }
9446
+ "inputText": "输入文本"
9251
9447
  },
9252
9448
  "titles": {
9253
9449
  "imageGenVideo": "图片生成视频",
@@ -9311,7 +9507,8 @@ var zh_default = {
9311
9507
  "musicExtract": "分离音轨",
9312
9508
  "musicLego": "添加乐轨",
9313
9509
  "musicComplete": "补全编曲",
9314
- "musicBrief": "音乐企划"
9510
+ "musicBrief": "音乐企划",
9511
+ "textGenAudio": "文本生成音频"
9315
9512
  },
9316
9513
  "options": {
9317
9514
  "portrait": "竖屏",
@@ -9618,15 +9815,9 @@ var zh_default = {
9618
9815
  "audioPromptPlaceholder": "输入音频配音提示词"
9619
9816
  },
9620
9817
  "convertVoice": {
9621
- "voiceLabel": "音色:",
9622
- "selectVoice": "请选择音色",
9623
- "previewVoice": "试听当前音色",
9624
- "uploadVoice": "上传音色",
9625
- "recordVoice": "录制音色",
9626
- "recordAudio": "录制音频",
9627
- "recordHint": "您可以在这里录制音频。",
9628
- "recordNeedImpl": "音频录制功能需要实现",
9629
- "done": "完成"
9818
+ "sourceAudio": "源音频",
9819
+ "referenceVoice": "参考音色",
9820
+ "connectHint": "连上要换声的音频,再连一段参考音色"
9630
9821
  },
9631
9822
  "removeWatermark": { "hint": "自动识别并去除 Sora 视频水印" },
9632
9823
  "videoGenText": {
@@ -9820,6 +10011,12 @@ var zh_default = {
9820
10011
  "separateSound": {
9821
10012
  "textLabel": "要提取的声音",
9822
10013
  "textPlaceholder": "如:狗叫、钢琴、背景里的警笛"
10014
+ },
10015
+ "textGenAudio": {
10016
+ "voiceReference": "参考音色",
10017
+ "promptLabel": "音频描述",
10018
+ "promptPlaceholder": "把整段场景写出来:谁在说话、怎么说,背景环境,音效。台词用引号,例如:@voice1 说:“今天天气真好。”窗外下着小雨。",
10019
+ "hint": "一次生成人声、音效和环境声。最多连 3 段音频作参考音色,点一下就把 @voice1…@voice3 插进描述里。"
9823
10020
  }
9824
10021
  }
9825
10022
  },
@@ -9882,6 +10079,7 @@ var zh_default = {
9882
10079
  "copy": "复制",
9883
10080
  "copied": "已复制",
9884
10081
  "close": "关闭",
10082
+ "openModalBilling": "打开 Modal 账单设置",
9885
10083
  "needHelp": "解决不了?来社群里找我们:",
9886
10084
  "wechatGroup": "微信群",
9887
10085
  "wechatQrAlt": "微信群二维码"
@@ -9972,6 +10170,7 @@ function hostFetch(input, init) {
9972
10170
  }
9973
10171
  /** URL for an asset `file_key` (private uploads endpoint by default). */
9974
10172
  function assetUrl(fileKey) {
10173
+ if (/^https?:\/\//.test(fileKey)) return fileKey;
9975
10174
  return host.resolveAssetUrl ? host.resolveAssetUrl(fileKey) : `${host.apiBaseUrl}/api/uploads/${fileKey}`;
9976
10175
  }
9977
10176
  /**
@@ -12677,12 +12876,84 @@ function dedupeIds(list) {
12677
12876
  function useNodePluginIds(nodeSlot) {
12678
12877
  return dedupeIds(usePluginsRegistryStore((s) => s.registry)?.nodePluginMap?.[nodeSlot] ?? []);
12679
12878
  }
12879
+ const useLiveModelsStore = create(() => ({ byPlugin: {} }));
12880
+ const MODEL_CATALOG_TTL_MS = 6e5;
12881
+ const catalogInflight = /* @__PURE__ */ new Map();
12680
12882
  /**
12681
- * Model ids a plugin declares for one ABI `nodeSlot` (empty for single-model
12682
- * plugins — the model dropdown is hidden in that case).
12883
+ * Fetch (or refresh, once the TTL elapsed) a plugin's live model catalog. Safe
12884
+ * to call eagerly — it is a no-op for plugins without a catalog and while a
12885
+ * fresh result is cached.
12886
+ */
12887
+ async function loadPluginModelCatalog(pluginId) {
12888
+ const catalog = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.modelCatalog;
12889
+ if (!catalog) return;
12890
+ const cached = useLiveModelsStore.getState().byPlugin[pluginId];
12891
+ if (cached && Date.now() - cached.fetchedAt < MODEL_CATALOG_TTL_MS) return;
12892
+ const inflight = catalogInflight.get(pluginId);
12893
+ if (inflight) return inflight;
12894
+ const job = (async () => {
12895
+ try {
12896
+ const res = catalog.authEnv ? await hostFetch(apiUrl(`/api/plugins/model-catalog?pluginId=${encodeURIComponent(pluginId)}`), {
12897
+ cache: "no-store",
12898
+ credentials: "same-origin"
12899
+ }) : await fetch(catalog.url, {
12900
+ method: "GET",
12901
+ cache: "no-store"
12902
+ });
12903
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
12904
+ const bySlot = filterModelCatalog(catalog, await res.json());
12905
+ useLiveModelsStore.setState((s) => ({ byPlugin: {
12906
+ ...s.byPlugin,
12907
+ [pluginId]: {
12908
+ fetchedAt: Date.now(),
12909
+ bySlot
12910
+ }
12911
+ } }));
12912
+ } catch (e) {
12913
+ console.warn(`[tongflow] model catalog for ${pluginId} unavailable:`, e);
12914
+ } finally {
12915
+ catalogInflight.delete(pluginId);
12916
+ }
12917
+ })();
12918
+ catalogInflight.set(pluginId, job);
12919
+ return job;
12920
+ }
12921
+ /**
12922
+ * Synchronous read of a plugin's model ids for one slot: the static shortlist
12923
+ * (declared order, first = default) followed by any live-catalog extras.
12924
+ */
12925
+ function getNodePluginModels(nodeSlot, pluginId) {
12926
+ const declared = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? [];
12927
+ const live = useLiveModelsStore.getState().byPlugin[pluginId]?.bySlot[nodeSlot] ?? [];
12928
+ return dedupeIds([...declared, ...live]);
12929
+ }
12930
+ /**
12931
+ * Model ids a plugin offers for one ABI `nodeSlot` (empty for single-model
12932
+ * plugins — the model dropdown is hidden in that case): the static shortlist
12933
+ * plus live-catalog extras, which this hook fetches on first use.
12683
12934
  */
12684
12935
  function useNodePluginModels(nodeSlot, pluginId) {
12685
- return dedupeIds(usePluginsRegistryStore((s) => s.registry)?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? []);
12936
+ const registry = usePluginsRegistryStore((s) => s.registry);
12937
+ const declared = registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? [];
12938
+ const hasCatalog = Boolean(registry?.plugins?.[pluginId]?.modelCatalog);
12939
+ const live = useLiveModelsStore((s) => s.byPlugin[pluginId]?.bySlot[nodeSlot]);
12940
+ useEffect(() => {
12941
+ if (hasCatalog) loadPluginModelCatalog(pluginId);
12942
+ }, [hasCatalog, pluginId]);
12943
+ return dedupeIds([...declared, ...live ?? []]);
12944
+ }
12945
+ /**
12946
+ * Synchronous read of the params a plugin offers for one slot given the
12947
+ * selected model, in declaration order.
12948
+ */
12949
+ function getNodePluginParams(nodeSlot, pluginId, model) {
12950
+ const registry = usePluginsRegistryStore.getState().registry;
12951
+ return visiblePluginParams(registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.params, model);
12952
+ }
12953
+ /** Reactive form of {@link getNodePluginParams}. */
12954
+ function useNodePluginParams(nodeSlot, pluginId, model) {
12955
+ const declared = usePluginsRegistryStore((s) => s.registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.params);
12956
+ return useMemo(() => visiblePluginParams(declared, model), [declared, model]);
12686
12957
  }
12687
12958
  /** Presentation metadata (name/description/icon) for a single plugin. */
12688
12959
  function usePluginMeta(pluginId) {
@@ -12741,7 +13012,7 @@ function PluginIcon({ icon, label }) {
12741
13012
  /**
12742
13013
  * Shared plugin implementation selector (`plugins/<id>` directory name from registry).
12743
13014
  */
12744
- function NodePluginSelect({ value, onValueChange, options, title }) {
13015
+ function NodePluginSelect({ value, onValueChange, options, title, onOpenChange }) {
12745
13016
  const t = useTranslations("Workspace.nodes.base");
12746
13017
  return /* @__PURE__ */ jsx(Card, {
12747
13018
  className: "p-3",
@@ -12753,6 +13024,7 @@ function NodePluginSelect({ value, onValueChange, options, title }) {
12753
13024
  }), /* @__PURE__ */ jsxs(Select, {
12754
13025
  value,
12755
13026
  onValueChange,
13027
+ onOpenChange,
12756
13028
  children: [/* @__PURE__ */ jsx(SelectTrigger, {
12757
13029
  className: "w-full",
12758
13030
  size: "sm",
@@ -12870,10 +13142,12 @@ function NodePluginIdSelect({ nodeSlot, data, dataKey = "pluginId" }) {
12870
13142
  //#region src/canvas/nodes/base/node-plugin-model-select.tsx
12871
13143
  /**
12872
13144
  * Model selector for router-style plugins that declare per-slot model lists
12873
- * (`TONGFLOW_SLOT_MODELS`). Renders nothing when the active plugin declares no
12874
- * models, so single-model plugins are visually unchanged. The selection is
12875
- * stored as `data.pluginModel` and travels top-level (like `pluginId`) through
12876
- * the create-task API and workflow export.
13145
+ * (`TONGFLOW_SLOT_MODELS`, optionally extended live via
13146
+ * `TONGFLOW_MODEL_CATALOG` — re-checked each time the dropdown opens). Renders
13147
+ * nothing when the active plugin declares no models, so single-model plugins
13148
+ * are visually unchanged. The selection is stored as `data.pluginModel` and
13149
+ * travels top-level (like `pluginId`) through the create-task API and workflow
13150
+ * export.
12877
13151
  */
12878
13152
  function NodePluginModelSelect({ nodeSlot, data }) {
12879
13153
  const id = useNodeId();
@@ -12908,10 +13182,218 @@ function NodePluginModelSelect({ nodeSlot, data }) {
12908
13182
  pluginModel: value
12909
13183
  }),
12910
13184
  options,
12911
- title: t("pluginModelTitle")
13185
+ title: t("pluginModelTitle"),
13186
+ onOpenChange: (open) => {
13187
+ if (open) loadPluginModelCatalog(pluginId);
13188
+ }
13189
+ });
13190
+ }
13191
+ //#endregion
13192
+ //#region src/canvas/ui/input.tsx
13193
+ function Input({ className, type, ...props }) {
13194
+ return /* @__PURE__ */ jsx("input", {
13195
+ type,
13196
+ "data-slot": "input",
13197
+ className: cn("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className),
13198
+ ...props
13199
+ });
13200
+ }
13201
+ //#endregion
13202
+ //#region src/canvas/ui/switch.tsx
13203
+ function Switch({ className, ...props }) {
13204
+ return /* @__PURE__ */ jsx(SwitchPrimitive.Root, {
13205
+ "data-slot": "switch",
13206
+ className: cn("peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50", className),
13207
+ ...props,
13208
+ children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, {
13209
+ "data-slot": "switch-thumb",
13210
+ className: cn("bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-sm transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0")
13211
+ })
12912
13212
  });
12913
13213
  }
12914
13214
  //#endregion
13215
+ //#region src/canvas/nodes/base/node-plugin-params.tsx
13216
+ function readParams(data) {
13217
+ const raw = data.pluginParams;
13218
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
13219
+ }
13220
+ /**
13221
+ * Collapsed "Advanced" section listing the run-time knobs the active plugin
13222
+ * declares for this slot (`TONGFLOW_SLOT_PARAMS`, optionally gated per router
13223
+ * model). Renders nothing for plugins that declare none. Choices live in
13224
+ * `data.pluginParams` as a diff against the declared defaults — an unset key
13225
+ * means "plugin default" — and travel top-level as `params` through the
13226
+ * create-task API and the workflow export, never inside the ABI prompt.
13227
+ */
13228
+ function NodePluginParams({ nodeSlot, data }) {
13229
+ const id = useNodeId();
13230
+ const updates = useFlow((s) => s.updates);
13231
+ const t = useTranslations("Workspace.nodes.base");
13232
+ const [open, setOpen] = useState(false);
13233
+ const { resolved: pluginId } = useResolvedPluginId(nodeSlot, data);
13234
+ const models = useNodePluginModels(nodeSlot, pluginId);
13235
+ const currentModel = String(data.pluginModel ?? "").trim();
13236
+ const entries = useNodePluginParams(nodeSlot, pluginId, models.includes(currentModel) ? currentModel : models[0] ?? void 0);
13237
+ const values = useMemo(() => readParams(data), [data]);
13238
+ useEffect(() => {
13239
+ const normalized = normalizePluginParams(entries, values);
13240
+ if (samePluginParams(normalized, data.pluginParams)) return;
13241
+ if (Object.keys(normalized).length === 0 && data.pluginParams === void 0) return;
13242
+ const next = { ...data };
13243
+ if (Object.keys(normalized).length === 0) delete next.pluginParams;
13244
+ else next.pluginParams = normalized;
13245
+ updates(id, next, { history: false });
13246
+ }, [
13247
+ id,
13248
+ data,
13249
+ entries,
13250
+ values,
13251
+ updates
13252
+ ]);
13253
+ const setParam = useCallback((name, value) => {
13254
+ const next = { ...values };
13255
+ if (value === void 0) delete next[name];
13256
+ else next[name] = value;
13257
+ const normalized = normalizePluginParams(entries, next);
13258
+ const nextData = { ...data };
13259
+ if (Object.keys(normalized).length === 0) delete nextData.pluginParams;
13260
+ else nextData.pluginParams = normalized;
13261
+ updates(id, nextData);
13262
+ }, [
13263
+ id,
13264
+ data,
13265
+ entries,
13266
+ values,
13267
+ updates
13268
+ ]);
13269
+ const reset = useCallback(() => {
13270
+ const nextData = { ...data };
13271
+ delete nextData.pluginParams;
13272
+ updates(id, nextData);
13273
+ }, [
13274
+ id,
13275
+ data,
13276
+ updates
13277
+ ]);
13278
+ if (entries.length === 0) return null;
13279
+ const changed = Object.keys(values).length;
13280
+ return /* @__PURE__ */ jsx(Card, {
13281
+ className: "p-3",
13282
+ children: /* @__PURE__ */ jsxs("div", {
13283
+ className: "space-y-2",
13284
+ children: [/* @__PURE__ */ jsxs("div", {
13285
+ className: "flex items-center gap-1",
13286
+ children: [/* @__PURE__ */ jsxs("button", {
13287
+ type: "button",
13288
+ onClick: () => setOpen((o) => !o),
13289
+ className: "nodrag flex flex-1 items-center gap-1 text-left text-sm font-medium text-muted-foreground hover:text-foreground",
13290
+ "aria-expanded": open,
13291
+ children: [
13292
+ /* @__PURE__ */ jsx(ChevronRight, { className: `size-3.5 transition-transform ${open ? "rotate-90" : ""}` }),
13293
+ /* @__PURE__ */ jsx("span", { children: t("pluginParamsTitle") }),
13294
+ changed > 0 && /* @__PURE__ */ jsx("span", {
13295
+ className: "ml-1 rounded-full bg-primary/10 px-1.5 text-[10px] font-semibold text-primary",
13296
+ children: changed
13297
+ })
13298
+ ]
13299
+ }), changed > 0 && /* @__PURE__ */ jsx("button", {
13300
+ type: "button",
13301
+ onClick: reset,
13302
+ title: t("pluginParamsReset"),
13303
+ "aria-label": t("pluginParamsReset"),
13304
+ className: "nodrag rounded p-1 text-muted-foreground hover:text-foreground",
13305
+ children: /* @__PURE__ */ jsx(RotateCcw, { className: "size-3.5" })
13306
+ })]
13307
+ }), open && /* @__PURE__ */ jsx("div", {
13308
+ className: "space-y-2",
13309
+ children: entries.map((entry) => /* @__PURE__ */ jsx(ParamRow, {
13310
+ entry,
13311
+ value: values[entry.name],
13312
+ onChange: (v) => setParam(entry.name, v)
13313
+ }, entry.name))
13314
+ })]
13315
+ })
13316
+ });
13317
+ }
13318
+ function ParamRow({ entry, value, onChange }) {
13319
+ const t = useTranslations("Workspace.nodes.base");
13320
+ const { name, spec } = entry;
13321
+ const label = spec.label ?? name;
13322
+ const inline = spec.type === "boolean";
13323
+ const changed = value !== void 0 && spec.default !== void 0;
13324
+ return /* @__PURE__ */ jsxs("div", {
13325
+ className: inline ? "flex items-center justify-between gap-2" : "space-y-1",
13326
+ children: [/* @__PURE__ */ jsxs(Label, {
13327
+ className: "flex items-baseline gap-1.5 text-xs text-muted-foreground",
13328
+ title: spec.description,
13329
+ children: [/* @__PURE__ */ jsx("span", { children: label }), changed && /* @__PURE__ */ jsx("span", {
13330
+ className: "text-[10px] text-muted-foreground/60",
13331
+ children: t("pluginParamDefault", { value: String(spec.default) })
13332
+ })]
13333
+ }), /* @__PURE__ */ jsx(ParamControl, {
13334
+ spec,
13335
+ value,
13336
+ onChange
13337
+ })]
13338
+ });
13339
+ }
13340
+ function ParamControl({ spec, value, onChange }) {
13341
+ const effective = value === void 0 ? spec.default : value;
13342
+ switch (spec.type) {
13343
+ case "select": {
13344
+ const options = spec.options ?? [];
13345
+ return /* @__PURE__ */ jsxs(Select, {
13346
+ value: effective === void 0 ? "" : String(effective),
13347
+ onValueChange: (v) => {
13348
+ onChange(options.find((o) => String(o) === v));
13349
+ },
13350
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
13351
+ className: "nodrag w-full",
13352
+ size: "sm",
13353
+ children: /* @__PURE__ */ jsx(SelectValue, {})
13354
+ }), /* @__PURE__ */ jsx(SelectContent, { children: options.map((o) => /* @__PURE__ */ jsx(SelectItem, {
13355
+ value: String(o),
13356
+ children: String(o)
13357
+ }, String(o))) })]
13358
+ });
13359
+ }
13360
+ case "boolean": return /* @__PURE__ */ jsx(Switch, {
13361
+ className: "nodrag",
13362
+ checked: effective === true,
13363
+ onCheckedChange: (checked) => onChange(checked)
13364
+ });
13365
+ case "number":
13366
+ case "integer": return /* @__PURE__ */ jsx(Input, {
13367
+ type: "number",
13368
+ className: "nodrag h-8",
13369
+ value: typeof effective === "number" ? String(effective) : "",
13370
+ min: spec.min,
13371
+ max: spec.max,
13372
+ step: spec.step ?? (spec.type === "integer" ? 1 : "any"),
13373
+ onChange: (e) => {
13374
+ const raw = e.target.value.trim();
13375
+ if (raw === "") {
13376
+ onChange(void 0);
13377
+ return;
13378
+ }
13379
+ const n = Number(raw);
13380
+ if (!Number.isFinite(n)) return;
13381
+ onChange(spec.type === "integer" ? Math.round(n) : n);
13382
+ }
13383
+ });
13384
+ case "text": return /* @__PURE__ */ jsx(Input, {
13385
+ type: "text",
13386
+ className: "nodrag h-8",
13387
+ value: typeof effective === "string" ? effective : "",
13388
+ onChange: (e) => {
13389
+ const v = e.target.value;
13390
+ onChange(v === "" ? void 0 : v);
13391
+ }
13392
+ });
13393
+ default: return null;
13394
+ }
13395
+ }
13396
+ //#endregion
12915
13397
  //#region src/canvas/nodes/base/node-thinking-bubble.tsx
12916
13398
  /**
12917
13399
  * A transient, auto-scrolling bubble anchored to the right of a node that
@@ -13015,13 +13497,20 @@ const BaseNodeShell = forwardRef(({ className, selected, count, data, children,
13015
13497
  ] }),
13016
13498
  renderPluginSelect && /* @__PURE__ */ jsxs("div", {
13017
13499
  className: "p-4 pb-0 space-y-2",
13018
- children: [/* @__PURE__ */ jsx(NodePluginIdSelect, {
13019
- nodeSlot: feature,
13020
- data: data ?? { feature }
13021
- }), /* @__PURE__ */ jsx(NodePluginModelSelect, {
13022
- nodeSlot: feature,
13023
- data: data ?? { feature }
13024
- })]
13500
+ children: [
13501
+ /* @__PURE__ */ jsx(NodePluginIdSelect, {
13502
+ nodeSlot: feature,
13503
+ data: data ?? { feature }
13504
+ }),
13505
+ /* @__PURE__ */ jsx(NodePluginModelSelect, {
13506
+ nodeSlot: feature,
13507
+ data: data ?? { feature }
13508
+ }),
13509
+ /* @__PURE__ */ jsx(NodePluginParams, {
13510
+ nodeSlot: feature,
13511
+ data: data ?? { feature }
13512
+ })
13513
+ ]
13025
13514
  }),
13026
13515
  /* @__PURE__ */ jsx("div", {
13027
13516
  className: "relative z-0",
@@ -14020,16 +14509,6 @@ function useNodeState(initialState, nodeData) {
14020
14509
  }, [id, flowUpdates])];
14021
14510
  }
14022
14511
  //#endregion
14023
- //#region src/canvas/ui/input.tsx
14024
- function Input({ className, type, ...props }) {
14025
- return /* @__PURE__ */ jsx("input", {
14026
- type,
14027
- "data-slot": "input",
14028
- className: cn("file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className),
14029
- ...props
14030
- });
14031
- }
14032
- //#endregion
14033
14512
  //#region src/canvas/nodes/add/add-link-node.tsx
14034
14513
  const AddLinkNode = ({ selected, data }) => {
14035
14514
  const t = useTranslations("Workspace.nodes.add");
@@ -16118,24 +16597,47 @@ function useNodePluginResolver(feature) {
16118
16597
  defaultPluginIdFromRegistry,
16119
16598
  pluginOptions
16120
16599
  ]);
16600
+ /**
16601
+ * Model for router-style plugins: the node's `pluginModel` when it is one
16602
+ * of the active plugin's models (declared shortlist or live catalog), else
16603
+ * that plugin's default (first declared model). `undefined` when the
16604
+ * plugin declares none — the create-task API then omits the field entirely.
16605
+ */
16606
+ const resolveActiveModel = useCallback(() => {
16607
+ if (!feature) return void 0;
16608
+ const pluginId = resolveActivePluginId();
16609
+ if (!pluginId) return void 0;
16610
+ const models = getNodePluginModels(feature, pluginId);
16611
+ if (models.length === 0) return void 0;
16612
+ const n = nodeId ? getNode(nodeId) : void 0;
16613
+ const fromData = String((n?.data)?.pluginModel ?? "").trim();
16614
+ return models.includes(fromData) ? fromData : models[0];
16615
+ }, [
16616
+ feature,
16617
+ nodeId,
16618
+ getNode,
16619
+ resolveActivePluginId
16620
+ ]);
16121
16621
  return {
16122
16622
  pluginOptions,
16123
16623
  defaultPluginIdFromRegistry,
16124
16624
  resolveActivePluginId,
16125
- resolveActiveModel: useCallback(() => {
16625
+ resolveActiveModel,
16626
+ resolveActiveParams: useCallback(() => {
16126
16627
  if (!feature) return void 0;
16127
16628
  const pluginId = resolveActivePluginId();
16128
16629
  if (!pluginId) return void 0;
16129
- const models = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[feature]?.models ?? [];
16130
- if (models.length === 0) return void 0;
16131
- const n = nodeId ? getNode(nodeId) : void 0;
16132
- const fromData = String((n?.data)?.pluginModel ?? "").trim();
16133
- return models.includes(fromData) ? fromData : models[0];
16630
+ const entries = getNodePluginParams(feature, pluginId, resolveActiveModel());
16631
+ if (entries.length === 0) return void 0;
16632
+ const raw = ((nodeId ? getNode(nodeId) : void 0)?.data)?.pluginParams;
16633
+ const params = normalizePluginParams(entries, raw && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0);
16634
+ return Object.keys(params).length > 0 ? params : void 0;
16134
16635
  }, [
16135
16636
  feature,
16136
16637
  nodeId,
16137
16638
  getNode,
16138
- resolveActivePluginId
16639
+ resolveActivePluginId,
16640
+ resolveActiveModel
16139
16641
  ])
16140
16642
  };
16141
16643
  }
@@ -16231,7 +16733,7 @@ function useAbiExecution(opts) {
16231
16733
  const expands = useFlow((s) => s.expands);
16232
16734
  const { isLoading: taskLoading, createBatchTasks, cancelTasks } = useBatchTaskManager();
16233
16735
  const loading = taskLoading || nodeExecutionStatus === "running";
16234
- const { pluginOptions, resolveActivePluginId, resolveActiveModel } = useNodePluginResolver(feature);
16736
+ const { pluginOptions, resolveActivePluginId, resolveActiveModel, resolveActiveParams } = useNodePluginResolver(feature);
16235
16737
  const { isLoaded: pluginsRegistryLoaded } = usePluginsRegistry();
16236
16738
  const handleTaskUpdate = useCallback(async (task) => {
16237
16739
  if (!nodeId) return;
@@ -16337,11 +16839,13 @@ function useAbiExecution(opts) {
16337
16839
  prompt: prompts.length === 1 ? prompts[0] : prompts
16338
16840
  });
16339
16841
  const model = resolveActiveModel();
16842
+ const params = resolveActiveParams();
16340
16843
  try {
16341
16844
  const taskConfigs = prompts.map((prompt) => ({
16342
16845
  feature,
16343
16846
  pluginId,
16344
16847
  ...model ? { model } : {},
16848
+ ...params ? { params } : {},
16345
16849
  prompt,
16346
16850
  nodeId
16347
16851
  }));
@@ -16358,6 +16862,7 @@ function useAbiExecution(opts) {
16358
16862
  createBatchTasks,
16359
16863
  resolveActivePluginId,
16360
16864
  resolveActiveModel,
16865
+ resolveActiveParams,
16361
16866
  pluginOptions,
16362
16867
  pluginsRegistryLoaded,
16363
16868
  transformPrompts
@@ -17483,30 +17988,63 @@ var merge_video_audio_default = memo(MergeVideoAudioNode);
17483
17988
  //#endregion
17484
17989
  //#region src/canvas/nodes/compose/music-cover.tsx
17485
17990
  const DEFAULT_STRENGTH$1 = .8;
17991
+ function UpstreamTextPreview({ label, text }) {
17992
+ return /* @__PURE__ */ jsx(Card, {
17993
+ className: "p-3",
17994
+ children: /* @__PURE__ */ jsxs("div", {
17995
+ className: "space-y-2",
17996
+ children: [/* @__PURE__ */ jsx(Label, {
17997
+ className: "text-sm font-medium text-muted-foreground",
17998
+ children: label
17999
+ }), /* @__PURE__ */ jsx("div", {
18000
+ className: "text-sm text-foreground p-2 bg-background rounded border border-border/50 line-clamp-4 whitespace-pre-line",
18001
+ children: text
18002
+ })]
18003
+ })
18004
+ });
18005
+ }
17486
18006
  const MusicCoverNode = ({ selected, data }) => {
17487
18007
  const t = useTranslations("Workspace.nodes");
17488
18008
  const form = useAbiForm("music-cover");
17489
18009
  const nodeId = useNodeId();
17490
18010
  const edges = useStore((state) => state.edges);
17491
- const { audioSourceId, refAudioSourceId } = useMemo(() => {
17492
- if (!nodeId) return {
18011
+ const { audioSourceId, refAudioSourceId, textSourceId, lyricsSourceId } = useMemo(() => {
18012
+ const ids = {
17493
18013
  audioSourceId: null,
17494
- refAudioSourceId: null
18014
+ refAudioSourceId: null,
18015
+ textSourceId: null,
18016
+ lyricsSourceId: null
17495
18017
  };
17496
- let audioSrc = null;
17497
- let refAudioSrc = null;
18018
+ if (!nodeId) return ids;
17498
18019
  for (const e of edges) {
17499
18020
  if (e.target !== nodeId) continue;
17500
- if (e.targetHandle === "in:audio") audioSrc = e.source;
17501
- else if (e.targetHandle === "in:ref_audio") refAudioSrc = e.source;
18021
+ if (e.targetHandle === "in:audio") ids.audioSourceId = e.source;
18022
+ else if (e.targetHandle === "in:ref_audio") ids.refAudioSourceId = e.source;
18023
+ else if (e.targetHandle === "in:text") ids.textSourceId = e.source;
18024
+ else if (e.targetHandle === "in:lyrics") ids.lyricsSourceId = e.source;
17502
18025
  }
17503
- return {
17504
- audioSourceId: audioSrc,
17505
- refAudioSourceId: refAudioSrc
17506
- };
18026
+ return ids;
17507
18027
  }, [edges, nodeId]);
17508
- const upstreamIds = useMemo(() => [audioSourceId, refAudioSourceId].filter((v) => !!v), [audioSourceId, refAudioSourceId]);
18028
+ const upstreamIds = useMemo(() => [
18029
+ audioSourceId,
18030
+ refAudioSourceId,
18031
+ textSourceId,
18032
+ lyricsSourceId
18033
+ ].filter((v) => !!v), [
18034
+ audioSourceId,
18035
+ refAudioSourceId,
18036
+ textSourceId,
18037
+ lyricsSourceId
18038
+ ]);
17509
18039
  const upstreamNodes = useNodesData(upstreamIds);
18040
+ const upstreamText = (sourceId) => {
18041
+ if (!sourceId) return void 0;
18042
+ const n = upstreamNodes.find((u) => u.id === sourceId);
18043
+ if (!n || n.type !== "textNode") return void 0;
18044
+ return coerceBaseNodeData(n.data).texts?.[0] ?? "";
18045
+ };
18046
+ const promptFromUpstream = upstreamText(textSourceId);
18047
+ const lyricsFromUpstream = upstreamText(lyricsSourceId);
17510
18048
  const audioKey = useMemo(() => {
17511
18049
  if (!audioSourceId) return void 0;
17512
18050
  const n = upstreamNodes.find((u) => u.id === audioSourceId);
@@ -17569,14 +18107,20 @@ const MusicCoverNode = ({ selected, data }) => {
17569
18107
  })]
17570
18108
  })
17571
18109
  }),
17572
- /* @__PURE__ */ jsx(NodeTextarea, {
18110
+ promptFromUpstream !== void 0 ? /* @__PURE__ */ jsx(UpstreamTextPreview, {
18111
+ label: t("musicTasks.promptLabel"),
18112
+ text: promptFromUpstream
18113
+ }) : /* @__PURE__ */ jsx(NodeTextarea, {
17573
18114
  label: t("musicTasks.promptLabel"),
17574
18115
  icon: MessageSquare,
17575
18116
  placeholder: t("musicTasks.promptPlaceholder"),
17576
18117
  ...form.bind("text"),
17577
18118
  rows: 3
17578
18119
  }),
17579
- /* @__PURE__ */ jsx(NodeTextarea, {
18120
+ lyricsFromUpstream !== void 0 ? /* @__PURE__ */ jsx(UpstreamTextPreview, {
18121
+ label: t("musicTasks.lyricsLabel"),
18122
+ text: lyricsFromUpstream
18123
+ }) : /* @__PURE__ */ jsx(NodeTextarea, {
17580
18124
  label: t("musicTasks.lyricsLabel"),
17581
18125
  placeholder: t("musicTasks.lyricsPlaceholder"),
17582
18126
  ...form.bind("lyrics"),
@@ -18880,19 +19424,6 @@ const VideoImageGenVideoMoveNode = ({ selected, data }) => {
18880
19424
  VideoImageGenVideoMoveNode.displayName = "VideoImageGenVideoMoveNode";
18881
19425
  var video_image_gen_video_move_default = memo(VideoImageGenVideoMoveNode);
18882
19426
  //#endregion
18883
- //#region src/canvas/ui/switch.tsx
18884
- function Switch({ className, ...props }) {
18885
- return /* @__PURE__ */ jsx(SwitchPrimitive.Root, {
18886
- "data-slot": "switch",
18887
- className: cn("peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none disabled:cursor-not-allowed disabled:opacity-50", className),
18888
- ...props,
18889
- children: /* @__PURE__ */ jsx(SwitchPrimitive.Thumb, {
18890
- "data-slot": "switch-thumb",
18891
- className: cn("bg-background pointer-events-none block size-4 rounded-full ring-0 shadow-sm transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0")
18892
- })
18893
- });
18894
- }
18895
- //#endregion
18896
19427
  //#region src/canvas/nodes/decompose/music-brief.tsx
18897
19428
  function buildLanguageOptions$1(tLang) {
18898
19429
  return [
@@ -21302,143 +21833,102 @@ AudioGenTextSpeechRecognizeNode.displayName = "AudioGenTextSpeechRecognizeNode";
21302
21833
  var audio_gen_text_speech_recognize_default = memo(AudioGenTextSpeechRecognizeNode);
21303
21834
  //#endregion
21304
21835
  //#region src/canvas/nodes/transfer/convert-voice.tsx
21305
- const VOICE_OPTIONS = [{
21306
- key: "female",
21307
- value: "zh_famale_1.wav"
21308
- }, {
21309
- key: "male",
21310
- value: "zh_male_1.wav"
21311
- }];
21836
+ const firstKey = (value) => {
21837
+ const v = Array.isArray(value) ? value[0] : value;
21838
+ return typeof v === "string" && v ? v : void 0;
21839
+ };
21312
21840
  const ConvertVoiceNode = ({ selected, data }) => {
21313
21841
  const t = useTranslations("Workspace.nodes");
21314
21842
  const form = useAbiForm("convert_voice");
21315
- const fileKeys = data.fileKeys;
21316
- const [extraSpeakers, setExtraSpeakers] = useState([]);
21317
- const speakers = [...VOICE_OPTIONS, ...extraSpeakers];
21318
- const targetKey = form.state.targetKey ?? "zh_famale_1.wav";
21843
+ const nodeId = useNodeId();
21844
+ const nodeLookup = useStore((state) => state.nodeLookup);
21845
+ const edges = useStore((state) => state.edges);
21846
+ const resolvedSpec = useNodeAbiSpec("convert_voice");
21847
+ const { sourceKey, refKey } = useMemo(() => {
21848
+ if (!nodeId) return {
21849
+ sourceKey: void 0,
21850
+ refKey: void 0
21851
+ };
21852
+ const values = collectHandleValues(nodeId, resolvedSpec, Array.from(nodeLookup.values()), edges);
21853
+ return {
21854
+ sourceKey: firstKey(values.audio),
21855
+ refKey: firstKey(values.ref_audio)
21856
+ };
21857
+ }, [
21858
+ nodeId,
21859
+ resolvedSpec,
21860
+ nodeLookup,
21861
+ edges
21862
+ ]);
21319
21863
  return /* @__PURE__ */ jsx(AbiNodeShell, {
21320
21864
  feature: "convert_voice",
21321
21865
  form,
21322
21866
  selected,
21867
+ className: "min-w-[360px]",
21323
21868
  data,
21324
21869
  title: t("titles.convertVoice"),
21325
21870
  icon: /* @__PURE__ */ jsx(Atom, { className: "h-5 w-5" }),
21326
21871
  executeLabel: t("actions.startReplace"),
21327
- executeDisabled: !fileKeys?.length,
21328
- children: /* @__PURE__ */ jsx(Card, {
21329
- className: "p-5 nodrag",
21330
- onPointerDown: (e) => e.stopPropagation(),
21331
- children: /* @__PURE__ */ jsxs("div", {
21332
- className: "mb-4 flex flex-wrap items-center gap-3",
21333
- children: [
21334
- /* @__PURE__ */ jsx("label", {
21335
- htmlFor: "voice-select",
21336
- className: "text-sm text-muted-foreground whitespace-nowrap",
21337
- children: t("convertVoice.voiceLabel")
21338
- }),
21339
- /* @__PURE__ */ jsxs(Select, {
21340
- value: targetKey,
21341
- onValueChange: (value) => form.set("targetKey", value),
21342
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
21343
- id: "voice-select",
21344
- className: "w-36 h-9",
21345
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: t("convertVoice.selectVoice") })
21346
- }), /* @__PURE__ */ jsx(SelectContent, { children: speakers.map((opt) => /* @__PURE__ */ jsx(SelectItem, {
21347
- value: opt.value,
21348
- children: opt.key ? t(`common.voiceOptions.${opt.key}`) : opt.value
21349
- }, opt.value)) })]
21350
- }),
21351
- /* @__PURE__ */ jsx(SpeakerVoiceUploader, {
21352
- trigger: /* @__PURE__ */ jsx(Button, {
21353
- variant: "outline",
21354
- size: "icon",
21355
- className: "ml-1",
21356
- title: t("convertVoice.uploadVoice"),
21357
- children: /* @__PURE__ */ jsx(Upload, { className: "w-4 h-4" })
21872
+ executeDisabled: !sourceKey || !refKey,
21873
+ children: /* @__PURE__ */ jsx("div", {
21874
+ className: "p-4",
21875
+ children: /* @__PURE__ */ jsx(Card, {
21876
+ className: "p-3",
21877
+ children: /* @__PURE__ */ jsxs("div", {
21878
+ className: "space-y-2",
21879
+ children: [
21880
+ /* @__PURE__ */ jsx(Label, {
21881
+ className: "text-sm font-medium text-muted-foreground",
21882
+ children: t("compose.inputData")
21358
21883
  }),
21359
- onChange: (key) => {
21360
- setExtraSpeakers((prev) => [...prev, {
21361
- key: "",
21362
- value: key
21363
- }]);
21364
- }
21365
- }),
21366
- /* @__PURE__ */ jsx(SpeakerVoiceRecorder$1, {
21367
- trigger: /* @__PURE__ */ jsx(Button, {
21368
- variant: "outline",
21369
- size: "icon",
21370
- className: "ml-1",
21371
- title: t("convertVoice.recordVoice"),
21372
- children: /* @__PURE__ */ jsx(Mic, { className: "w-4 h-4" })
21884
+ /* @__PURE__ */ jsxs("div", {
21885
+ className: "flex gap-4",
21886
+ children: [sourceKey ? /* @__PURE__ */ jsx(MediaThumbnail, {
21887
+ fileKey: sourceKey,
21888
+ label: t("convertVoice.sourceAudio"),
21889
+ type: "audio"
21890
+ }) : /* @__PURE__ */ jsxs("div", {
21891
+ className: "flex flex-col items-center gap-1.5",
21892
+ children: [/* @__PURE__ */ jsx("div", {
21893
+ className: "relative w-16 h-16 rounded-md border-2 border-gray-300 overflow-hidden bg-gray-100",
21894
+ children: /* @__PURE__ */ jsx("div", {
21895
+ className: "flex items-center justify-center h-full w-full bg-orange-50",
21896
+ children: /* @__PURE__ */ jsx(Music, { className: "w-6 h-6 text-orange-600" })
21897
+ })
21898
+ }), /* @__PURE__ */ jsx("div", {
21899
+ className: "px-1.5 py-0.5 bg-orange-100 text-orange-700 text-xs font-medium rounded",
21900
+ children: t("convertVoice.sourceAudio")
21901
+ })]
21902
+ }), refKey ? /* @__PURE__ */ jsx(MediaThumbnail, {
21903
+ fileKey: refKey,
21904
+ label: t("convertVoice.referenceVoice"),
21905
+ type: "audio"
21906
+ }) : /* @__PURE__ */ jsxs("div", {
21907
+ className: "flex flex-col items-center gap-1.5",
21908
+ children: [/* @__PURE__ */ jsx("div", {
21909
+ className: "relative w-16 h-16 rounded-md border-2 border-gray-300 overflow-hidden bg-gray-100",
21910
+ children: /* @__PURE__ */ jsx("div", {
21911
+ className: "flex items-center justify-center h-full w-full bg-blue-50",
21912
+ children: /* @__PURE__ */ jsx(Mic, { className: "w-6 h-6 text-blue-600" })
21913
+ })
21914
+ }), /* @__PURE__ */ jsx("div", {
21915
+ className: "px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs font-medium rounded",
21916
+ children: t("convertVoice.referenceVoice")
21917
+ })]
21918
+ })]
21373
21919
  }),
21374
- onChange: (key) => {
21375
- setExtraSpeakers((prev) => [...prev, {
21376
- key: "",
21377
- value: key
21378
- }]);
21379
- }
21380
- })
21381
- ]
21920
+ (!sourceKey || !refKey) && /* @__PURE__ */ jsx("p", {
21921
+ className: "text-xs text-red-500",
21922
+ children: t("convertVoice.connectHint")
21923
+ })
21924
+ ]
21925
+ })
21382
21926
  })
21383
21927
  })
21384
21928
  });
21385
21929
  };
21930
+ ConvertVoiceNode.displayName = "ConvertVoiceNode";
21386
21931
  var convert_voice_default = memo(ConvertVoiceNode);
21387
- const SpeakerVoiceUploader = ({ trigger, onChange: _onChange }) => {
21388
- const [_uploaded, setUploaded] = useState(false);
21389
- const doUpload = async (files) => {
21390
- logger.debug("Uploading files:", files);
21391
- setUploaded(true);
21392
- };
21393
- return /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs("label", {
21394
- className: "cursor-pointer",
21395
- children: [/* @__PURE__ */ jsx("input", {
21396
- type: "file",
21397
- multiple: true,
21398
- hidden: true,
21399
- onChange: (e) => {
21400
- const files = Array.from(e.target.files || []);
21401
- if (files.length > 0) doUpload(files);
21402
- }
21403
- }), trigger]
21404
- }) });
21405
- };
21406
- const SpeakerVoiceRecorder$1 = ({ trigger, onChange: _onChange }) => {
21407
- const t = useTranslations("Workspace.nodes.convertVoice");
21408
- const [file, _setFile] = useState();
21409
- const onFinish = async () => {
21410
- if (!file) return;
21411
- logger.debug("Recording file:", file);
21412
- };
21413
- return /* @__PURE__ */ jsxs(Dialog, { children: [/* @__PURE__ */ jsx(DialogTrigger, {
21414
- asChild: true,
21415
- children: trigger
21416
- }), /* @__PURE__ */ jsxs(DialogContent, {
21417
- className: "sm:max-w-md",
21418
- children: [
21419
- /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: t("recordAudio") }), /* @__PURE__ */ jsx(DialogDescription, { children: t("recordHint") })] }),
21420
- /* @__PURE__ */ jsx("div", {
21421
- className: "overflow-auto scroll-smooth w-80 max-h-80",
21422
- children: /* @__PURE__ */ jsx("p", {
21423
- className: "text-sm text-gray-500",
21424
- children: t("recordNeedImpl")
21425
- })
21426
- }),
21427
- /* @__PURE__ */ jsx(DialogFooter, {
21428
- className: "sm:justify-start",
21429
- children: /* @__PURE__ */ jsx(DialogClose, {
21430
- asChild: true,
21431
- children: /* @__PURE__ */ jsx(Button, {
21432
- type: "button",
21433
- variant: "secondary",
21434
- onClick: () => onFinish(),
21435
- children: t("done")
21436
- })
21437
- })
21438
- })
21439
- ]
21440
- })] });
21441
- };
21442
21932
  //#endregion
21443
21933
  //#region src/canvas/nodes/transfer/denoise-audio.tsx
21444
21934
  const DenoiseAudioNode = ({ selected, data }) => {
@@ -22223,7 +22713,7 @@ const RemoveWatermarkNode = ({ selected, data }) => {
22223
22713
  executeLabel: t("actions.removeWatermark"),
22224
22714
  executeDisabled: !fileKeys?.length,
22225
22715
  children: /* @__PURE__ */ jsx(Card, {
22226
- className: "p-5 space-y-4",
22716
+ className: "mx-4 mb-4 p-4 space-y-4",
22227
22717
  children: /* @__PURE__ */ jsx("div", {
22228
22718
  className: "text-sm text-muted-foreground",
22229
22719
  children: t("removeWatermark.hint")
@@ -22368,6 +22858,109 @@ const SpeechGenVideoNode = ({ selected, data }) => {
22368
22858
  SpeechGenVideoNode.displayName = "SpeechGenVideoNode";
22369
22859
  var speech_gen_video_default = memo(SpeechGenVideoNode);
22370
22860
  //#endregion
22861
+ //#region src/canvas/nodes/transfer/text-gen-audio.tsx
22862
+ const MAX_REF_AUDIOS = 3;
22863
+ const VOICE_TAG = "@voice";
22864
+ const TextGenAudioNode = ({ selected, data }) => {
22865
+ const t = useTranslations("Workspace.nodes");
22866
+ const form = useAbiForm("text-gen-audio");
22867
+ const ids = useUpstreamNodeIds(data.ids);
22868
+ const fromNodes = useNodesData(ids);
22869
+ const refAudios = fromNodes.filter((node) => node.type === "audioNode").map((node) => coerceBaseNodeData(node.data).fileKeys).filter((keys) => !!keys && keys.length > 0);
22870
+ const textNode = fromNodes.find((node) => node.type === "textNode");
22871
+ const upstreamTexts = useMemo(() => textNode ? coerceBaseNodeData(textNode.data).texts || [] : [], [textNode]);
22872
+ const hasUpstreamTexts = upstreamTexts.length > 0;
22873
+ const prompt = form.state.text ?? "";
22874
+ const textareaRef = useRef(null);
22875
+ const insertVoiceTag = useCallback((tag) => {
22876
+ const textarea = textareaRef.current;
22877
+ if (!textarea) return;
22878
+ const start = textarea.selectionStart;
22879
+ const end = textarea.selectionEnd;
22880
+ form.set("text", prompt.substring(0, start) + tag + prompt.substring(end));
22881
+ setTimeout(() => {
22882
+ textarea.focus();
22883
+ const cursor = start + tag.length;
22884
+ textarea.setSelectionRange(cursor, cursor);
22885
+ }, 0);
22886
+ }, [prompt, form]);
22887
+ return /* @__PURE__ */ jsx(AbiNodeShell, {
22888
+ feature: "text-gen-audio",
22889
+ form,
22890
+ selected,
22891
+ className: "min-w-[480px]",
22892
+ data,
22893
+ title: t("titles.textGenAudio"),
22894
+ icon: /* @__PURE__ */ jsx(AudioWaveform, { className: "h-5 w-5" }),
22895
+ executeLabel: t("actions.generateAudio"),
22896
+ executeDisabled: !hasUpstreamTexts && !prompt.trim(),
22897
+ children: /* @__PURE__ */ jsxs("div", {
22898
+ className: "p-4 space-y-4",
22899
+ children: [
22900
+ refAudios.length > 0 && /* @__PURE__ */ jsxs("div", {
22901
+ className: "space-y-2",
22902
+ children: [/* @__PURE__ */ jsxs("span", {
22903
+ className: "text-sm font-medium text-muted-foreground",
22904
+ children: [t("textGenAudio.voiceReference"), /* @__PURE__ */ jsxs("span", {
22905
+ className: "ml-2 text-xs font-normal",
22906
+ children: [
22907
+ "(",
22908
+ refAudios.length,
22909
+ "/",
22910
+ MAX_REF_AUDIOS,
22911
+ ")"
22912
+ ]
22913
+ })]
22914
+ }), /* @__PURE__ */ jsx("div", {
22915
+ className: "flex gap-3 flex-wrap",
22916
+ children: refAudios.slice(0, MAX_REF_AUDIOS).map((keys, index) => /* @__PURE__ */ jsx(MediaThumbnail, {
22917
+ fileKey: keys[0],
22918
+ label: `${VOICE_TAG}${index + 1}`,
22919
+ type: "audio",
22920
+ onClick: () => insertVoiceTag(`${VOICE_TAG}${index + 1}`)
22921
+ }, keys[0]))
22922
+ })]
22923
+ }),
22924
+ /* @__PURE__ */ jsx("p", {
22925
+ className: "text-xs text-muted-foreground",
22926
+ children: t("textGenAudio.hint")
22927
+ }),
22928
+ hasUpstreamTexts ? /* @__PURE__ */ jsxs("div", {
22929
+ className: "space-y-2",
22930
+ children: [/* @__PURE__ */ jsxs("span", {
22931
+ className: "flex items-center gap-2 text-sm font-medium text-muted-foreground",
22932
+ children: [
22933
+ /* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" }),
22934
+ t("textGenAudio.promptLabel"),
22935
+ t("imageEdit.fromUpstream")
22936
+ ]
22937
+ }), /* @__PURE__ */ jsx("div", {
22938
+ className: "space-y-1 max-h-32 overflow-y-auto",
22939
+ children: upstreamTexts.map((text, index) => /* @__PURE__ */ jsx("div", {
22940
+ className: "text-sm text-foreground p-2 bg-background rounded border border-border/50 line-clamp-3",
22941
+ children: text
22942
+ }, `${index}-${text.slice(0, 48)}`))
22943
+ })]
22944
+ }) : /* @__PURE__ */ jsxs("div", {
22945
+ className: "space-y-2",
22946
+ children: [/* @__PURE__ */ jsxs("span", {
22947
+ className: "flex items-center gap-2 text-sm font-medium text-muted-foreground",
22948
+ children: [/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" }), t("textGenAudio.promptLabel")]
22949
+ }), /* @__PURE__ */ jsx(NodeTextarea, {
22950
+ ref: textareaRef,
22951
+ showCard: false,
22952
+ placeholder: t("textGenAudio.promptPlaceholder"),
22953
+ ...form.bind("text"),
22954
+ rows: 5
22955
+ })]
22956
+ })
22957
+ ]
22958
+ })
22959
+ });
22960
+ };
22961
+ TextGenAudioNode.displayName = "TextGenAudioNode";
22962
+ var text_gen_audio_default = memo(TextGenAudioNode);
22963
+ //#endregion
22371
22964
  //#region src/canvas/nodes/base/resolution-picker.tsx
22372
22965
  function ResolutionPicker({ tiers, value, onChange }) {
22373
22966
  const t = useTranslations("Workspace.nodes");
@@ -22932,7 +23525,7 @@ const TextGenSpeechCloneNode = ({ selected, data }) => {
22932
23525
  executeLabel: t("actions.generateSpeech"),
22933
23526
  executeDisabled: !texts?.length || !refAudio,
22934
23527
  children: /* @__PURE__ */ jsxs(Card, {
22935
- className: "p-5 nodrag",
23528
+ className: "mx-4 mb-4 p-4 nodrag",
22936
23529
  onPointerDown: (e) => e.stopPropagation(),
22937
23530
  children: [
22938
23531
  /* @__PURE__ */ jsxs("div", {
@@ -23046,7 +23639,7 @@ const TextGenSpeechInstructNode = ({ selected, data }) => {
23046
23639
  executeLabel: t("actions.generateSpeech"),
23047
23640
  executeDisabled: !texts?.length,
23048
23641
  children: /* @__PURE__ */ jsxs(Card, {
23049
- className: "p-5 nodrag",
23642
+ className: "mx-4 mb-4 p-4 nodrag",
23050
23643
  onPointerDown: (e) => e.stopPropagation(),
23051
23644
  children: [
23052
23645
  /* @__PURE__ */ jsxs("div", {
@@ -23136,7 +23729,7 @@ const TextGenSpeechPresetNode = ({ selected, data }) => {
23136
23729
  executeLabel: t("actions.generateSpeech"),
23137
23730
  executeDisabled: !texts?.length,
23138
23731
  children: /* @__PURE__ */ jsxs(Card, {
23139
- className: "p-5 nodrag",
23732
+ className: "mx-4 mb-4 p-4 nodrag",
23140
23733
  onPointerDown: (e) => e.stopPropagation(),
23141
23734
  children: [/* @__PURE__ */ jsxs("div", {
23142
23735
  className: "mb-4 flex flex-wrap items-center gap-3",
@@ -23635,6 +24228,7 @@ const NODE_TYPES = {
23635
24228
  genTextNode: text_gen_text_default,
23636
24229
  textGenImageNode: text_gen_image_default,
23637
24230
  textGenMusicNode: text_gen_music_default,
24231
+ textGenAudioNode: text_gen_audio_default,
23638
24232
  textGenSpeechCloneNode: text_gen_speech_clone_default,
23639
24233
  textGenSpeechCloneComposeNode: text_gen_speech_clone_compose_default,
23640
24234
  textGenSpeechPresetNode: text_gen_speech_preset_default,
@@ -23726,6 +24320,7 @@ const NODE_CATEGORIES = {
23726
24320
  "genTextNode",
23727
24321
  "textGenImageNode",
23728
24322
  "textGenMusicNode",
24323
+ "textGenAudioNode",
23729
24324
  "textGenSpeechCloneNode",
23730
24325
  "textGenSpeechPresetNode",
23731
24326
  "textGenSpeechInstructNode",
@@ -24492,4 +25087,4 @@ function SheetDescription({ className, ...props }) {
24492
25087
  });
24493
25088
  }
24494
25089
  //#endregion
24495
- export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AudioRecorderWithVisualizer, Badge, Button, CANVAS_LOCALES, CanvasProvider, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_QWEN_SPEAKER, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EDGE_TYPES, ErrorBoundary, FORMAT_CATEGORIES, FlowCanvas, Input, Label, LibInput, NODE_CATEGORIES, NODE_TYPES, NodePluginIdSelect, Progress, QWEN_SPEAKERS, RECOMMENDATIONS, SSE_TASK_MESSAGE_EVENT, SUPPORTED_FORMATS, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Slider, SpeakerVoiceRecorder, Switch, TASK_CANCEL_REQUEST_EVENT, TROUBLESHOOTING, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadValidationError, VideoRecorder, WORKSPACE_MODE_KEY, Waterfall, WhiteBoard, apiClient, apiDelete, apiGet, apiPatch, apiPost, apiPut, apiUrl, applyResolvedOutputRoutes, assetUrl, badgeVariants, buttonVariants, canvasMessages, cn, computeOutputView, configureCanvasHost, createMaterial, createTask, deleteMaterial, emitSSEConnected, emitSSETaskMessage, emitTaskCancelRequest, getCanvasHost, getCanvasLocale, getClientTranslator, getFileUrl, getGlobalFileCache, getGlobalFileQueue, getImageDimensions, getMaterialFileUrls, getPresignedUploadUrl, getPrivateFileUrl, getSharedFileUrl, getTaskStopUrl, getTaskWaitUrl, getVideoMetadata, hostFetch, isCanvasLocale, isImageFile, isVideoFile, listMaterials, listTasks, normalizeTaskPayloadData, pluginDisplayName, refreshPluginsRegistry, saveFromTask, setCanvasLocale, showErrorToast, toggleFavorite, traceMaterialWorkflow, updateTaskStatus, useAbiExecution, useAbiForm, useBatchTaskManager, useCreateTask, useDropzone, useFileAsyncLoader, useFileAsyncLoaderBatch, useFileLoaderStats, useFileUrl, useFlow, useMultipleUpload, useNodeAbiSpec, useNodePluginIds, useNodePluginIdsUnion, useNodePluginModels, useNodePluginResolver, useNodeState, useNodeTaskUpdate, useNodeType, usePluginMeta, usePluginsRegistry, usePluginsRegistryStore, useResolvedPluginId, useTask, useTaskStore, useTaskSubscription, useUpload, useUpstreamNodeIds, validateFile };
25090
+ export { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AudioRecorderWithVisualizer, Badge, Button, CANVAS_LOCALES, CanvasProvider, Card, CardAction, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, DEFAULT_QWEN_SPEAKER, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EDGE_TYPES, ErrorBoundary, FORMAT_CATEGORIES, FlowCanvas, Input, Label, LibInput, NODE_CATEGORIES, NODE_TYPES, NodePluginIdSelect, Progress, QWEN_SPEAKERS, RECOMMENDATIONS, SSE_TASK_MESSAGE_EVENT, SUPPORTED_FORMATS, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, Skeleton, Slider, SpeakerVoiceRecorder, Switch, TASK_CANCEL_REQUEST_EVENT, TROUBLESHOOTING, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadValidationError, VideoRecorder, WORKSPACE_MODE_KEY, Waterfall, WhiteBoard, apiClient, apiDelete, apiGet, apiPatch, apiPost, apiPut, apiUrl, applyResolvedOutputRoutes, assetUrl, badgeVariants, buttonVariants, canvasMessages, cn, computeOutputView, configureCanvasHost, createMaterial, createTask, deleteMaterial, emitSSEConnected, emitSSETaskMessage, emitTaskCancelRequest, getCanvasHost, getCanvasLocale, getClientTranslator, getFileUrl, getGlobalFileCache, getGlobalFileQueue, getImageDimensions, getMaterialFileUrls, getNodePluginModels, getNodePluginParams, getPresignedUploadUrl, getPrivateFileUrl, getSharedFileUrl, getTaskStopUrl, getTaskWaitUrl, getVideoMetadata, hostFetch, isCanvasLocale, isImageFile, isVideoFile, listMaterials, listTasks, loadPluginModelCatalog, normalizeTaskPayloadData, pluginDisplayName, refreshPluginsRegistry, saveFromTask, setCanvasLocale, showErrorToast, toggleFavorite, traceMaterialWorkflow, updateTaskStatus, useAbiExecution, useAbiForm, useBatchTaskManager, useCreateTask, useDropzone, useFileAsyncLoader, useFileAsyncLoaderBatch, useFileLoaderStats, useFileUrl, useFlow, useLiveModelsStore, useMultipleUpload, useNodeAbiSpec, useNodePluginIds, useNodePluginIdsUnion, useNodePluginModels, useNodePluginParams, useNodePluginResolver, useNodeState, useNodeTaskUpdate, useNodeType, usePluginMeta, usePluginsRegistry, usePluginsRegistryStore, useResolvedPluginId, useTask, useTaskStore, useTaskSubscription, useUpload, useUpstreamNodeIds, validateFile };