tongflow 0.1.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/canvas.cjs CHANGED
@@ -63,12 +63,12 @@ let _radix_ui_react_dropdown_menu = require("@radix-ui/react-dropdown-menu");
63
63
  _radix_ui_react_dropdown_menu = __toESM(_radix_ui_react_dropdown_menu, 1);
64
64
  let _radix_ui_react_label = require("@radix-ui/react-label");
65
65
  _radix_ui_react_label = __toESM(_radix_ui_react_label, 1);
66
+ let _radix_ui_react_switch = require("@radix-ui/react-switch");
67
+ _radix_ui_react_switch = __toESM(_radix_ui_react_switch, 1);
66
68
  let _radix_ui_react_progress = require("@radix-ui/react-progress");
67
69
  _radix_ui_react_progress = __toESM(_radix_ui_react_progress, 1);
68
70
  let _radix_ui_react_slider = require("@radix-ui/react-slider");
69
71
  _radix_ui_react_slider = __toESM(_radix_ui_react_slider, 1);
70
- let _radix_ui_react_switch = require("@radix-ui/react-switch");
71
- _radix_ui_react_switch = __toESM(_radix_ui_react_switch, 1);
72
72
  let react_dom = require("react-dom");
73
73
  let masonic = require("masonic");
74
74
  let three = require("three");
@@ -579,7 +579,11 @@ const ABI_NODES = {
579
579
  properties: {
580
580
  success: { type: "boolean" },
581
581
  error: { type: "string" },
582
- text: { type: "string" }
582
+ text: { type: "string" },
583
+ images: {
584
+ type: "array",
585
+ items: { $ref: "#/$defs/ImageRef" }
586
+ }
583
587
  },
584
588
  additionalProperties: false
585
589
  }
@@ -695,6 +699,34 @@ const ABI_NODES = {
695
699
  additionalProperties: false
696
700
  }
697
701
  },
702
+ "text-gen-audio": {
703
+ inputs: {
704
+ type: "object",
705
+ required: ["text"],
706
+ properties: {
707
+ text: {
708
+ type: "string",
709
+ minLength: 1
710
+ },
711
+ ref_audios: {
712
+ type: "array",
713
+ items: { $ref: "#/$defs/Asset" }
714
+ },
715
+ seed: { type: "integer" }
716
+ },
717
+ additionalProperties: false
718
+ },
719
+ outputs: {
720
+ type: "object",
721
+ required: ["success"],
722
+ properties: {
723
+ success: { type: "boolean" },
724
+ error: { type: "string" },
725
+ audio: { $ref: "#/$defs/AudioRef" }
726
+ },
727
+ additionalProperties: false
728
+ }
729
+ },
698
730
  "text-gen-speech-preset": {
699
731
  inputs: {
700
732
  type: "object",
@@ -1564,10 +1596,10 @@ const ABI_NODES = {
1564
1596
  convert_voice: {
1565
1597
  inputs: {
1566
1598
  type: "object",
1567
- required: ["sourceKey", "targetKey"],
1599
+ required: ["audio", "ref_audio"],
1568
1600
  properties: {
1569
- sourceKey: { $ref: "#/$defs/Asset" },
1570
- targetKey: { type: "string" }
1601
+ audio: { $ref: "#/$defs/Asset" },
1602
+ ref_audio: { $ref: "#/$defs/Asset" }
1571
1603
  },
1572
1604
  additionalProperties: false
1573
1605
  },
@@ -2174,6 +2206,7 @@ const NODE_TYPE_TO_ABI_FEATURE = {
2174
2206
  imageGenImageUpscaleNode: "image-upscale",
2175
2207
  textGenImageNode: "image-gen",
2176
2208
  textGenMusicNode: "gen-music",
2209
+ textGenAudioNode: "text-gen-audio",
2177
2210
  textGenSpeechCloneNode: "text-gen-speech-clone",
2178
2211
  textGenSpeechCloneComposeNode: "text-gen-speech-clone",
2179
2212
  imageGenVideoComposeNode: "image-gen-video",
@@ -2297,6 +2330,18 @@ const NODE_TYPE_SOURCE_SPEC = {
2297
2330
  manual: true
2298
2331
  })
2299
2332
  },
2333
+ musicCoverNode: {
2334
+ text: handle({
2335
+ nodeType: "textNode",
2336
+ path: "texts[0]",
2337
+ manual: true
2338
+ }),
2339
+ lyrics: handle({
2340
+ nodeType: "textNode",
2341
+ path: "texts[0]",
2342
+ manual: true
2343
+ })
2344
+ },
2300
2345
  textGenSpeechCloneComposeNode: { text: batchOn({
2301
2346
  nodeType: "textNode",
2302
2347
  path: "texts"
@@ -2334,7 +2379,10 @@ const NODE_TYPE_SOURCE_SPEC = {
2334
2379
  audioGenTextSpeechRecognizeNode: { audio: batchOn() },
2335
2380
  concatVideoComposeNode: { videos: collectAll() },
2336
2381
  concatVideoNode: { videos: collectAll() },
2337
- convertVoiceNode: { sourceKey: batchOn({ nodeType: "audioNode" }) },
2382
+ convertVoiceNode: {
2383
+ audio: batchOn({ nodeType: "audioNode" }),
2384
+ ref_audio: handle({ nodeType: "audioNode" })
2385
+ },
2338
2386
  denoiseAudioSubtitleNode: { fileKey: batchOn({ nodeType: "audioNode" }) },
2339
2387
  dropVideoNode: { videos: collectAll() },
2340
2388
  extractAudioNode: { video: batchOn() },
@@ -2382,6 +2430,14 @@ const NODE_TYPE_SOURCE_SPEC = {
2382
2430
  path: "texts[0]"
2383
2431
  })
2384
2432
  },
2433
+ textGenAudioNode: {
2434
+ text: handle({
2435
+ nodeType: "textNode",
2436
+ path: "texts[0]",
2437
+ manual: true
2438
+ }),
2439
+ ref_audios: collectAll({ nodeType: "audioNode" })
2440
+ },
2385
2441
  textGenSpeechCloneNode: {
2386
2442
  text: batchOn({
2387
2443
  nodeType: "textNode",
@@ -3865,7 +3921,11 @@ var tongflow_abi_default = {
3865
3921
  "properties": {
3866
3922
  "success": { "type": "boolean" },
3867
3923
  "error": { "type": "string" },
3868
- "text": { "type": "string" }
3924
+ "text": { "type": "string" },
3925
+ "images": {
3926
+ "type": "array",
3927
+ "items": { "$ref": "#/$defs/ImageRef" }
3928
+ }
3869
3929
  },
3870
3930
  "additionalProperties": false
3871
3931
  }
@@ -3985,6 +4045,35 @@ var tongflow_abi_default = {
3985
4045
  "additionalProperties": false
3986
4046
  }
3987
4047
  },
4048
+ {
4049
+ "nodeSlot": "text-gen-audio",
4050
+ "inputs": {
4051
+ "type": "object",
4052
+ "required": ["text"],
4053
+ "properties": {
4054
+ "text": {
4055
+ "type": "string",
4056
+ "minLength": 1
4057
+ },
4058
+ "ref_audios": {
4059
+ "type": "array",
4060
+ "items": { "$ref": "#/$defs/Asset" }
4061
+ },
4062
+ "seed": { "type": "integer" }
4063
+ },
4064
+ "additionalProperties": false
4065
+ },
4066
+ "outputs": {
4067
+ "type": "object",
4068
+ "required": ["success"],
4069
+ "properties": {
4070
+ "success": { "type": "boolean" },
4071
+ "error": { "type": "string" },
4072
+ "audio": { "$ref": "#/$defs/AudioRef" }
4073
+ },
4074
+ "additionalProperties": false
4075
+ }
4076
+ },
3988
4077
  {
3989
4078
  "nodeSlot": "text-gen-speech-preset",
3990
4079
  "inputs": {
@@ -4887,10 +4976,10 @@ var tongflow_abi_default = {
4887
4976
  "nodeSlot": "convert_voice",
4888
4977
  "inputs": {
4889
4978
  "type": "object",
4890
- "required": ["sourceKey", "targetKey"],
4979
+ "required": ["audio", "ref_audio"],
4891
4980
  "properties": {
4892
- "sourceKey": { "$ref": "#/$defs/Asset" },
4893
- "targetKey": { "type": "string" }
4981
+ "audio": { "$ref": "#/$defs/Asset" },
4982
+ "ref_audio": { "$ref": "#/$defs/Asset" }
4894
4983
  },
4895
4984
  "additionalProperties": false
4896
4985
  },
@@ -5173,6 +5262,103 @@ parsed.nodes;
5173
5262
  function getAbiNodeBySlot(nodeSlot) {
5174
5263
  return bySlot.get(nodeSlot);
5175
5264
  }
5265
+ //#endregion
5266
+ //#region src/core/registry/model-catalog.ts
5267
+ /** Read a dot-path (`a.b.c`) off an unknown JSON value; `undefined` when absent. */
5268
+ function getPath(value, path) {
5269
+ let cur = value;
5270
+ for (const key of path.split(".")) {
5271
+ if (cur === null || typeof cur !== "object") return void 0;
5272
+ cur = cur[key];
5273
+ }
5274
+ return cur;
5275
+ }
5276
+ /** Substring match against the field's JSON serialization (strings as-is). */
5277
+ function fieldContains(value, token) {
5278
+ if (value === void 0 || value === null) return false;
5279
+ return (typeof value === "string" ? value : JSON.stringify(value)).includes(token);
5280
+ }
5281
+ /**
5282
+ * Filter a fetched catalog payload into `slot -> model ids`, applying the
5283
+ * plugin's declared `exclude` and per-slot `slots` rules (substring tokens,
5284
+ * `!`-prefixed = must be absent). Ids keep catalog order; malformed records
5285
+ * are skipped. Pure — the fetch lives in the canvas.
5286
+ */
5287
+ function filterModelCatalog(catalog, payload) {
5288
+ const out = {};
5289
+ for (const slot of Object.keys(catalog.slots)) out[slot] = [];
5290
+ const items = getPath(payload, catalog.items);
5291
+ if (!Array.isArray(items)) return out;
5292
+ const exclude = Object.entries(catalog.exclude ?? {});
5293
+ for (const record of items) {
5294
+ const id = getPath(record, catalog.id);
5295
+ if (typeof id !== "string" || !id.trim()) continue;
5296
+ if (exclude.some(([field, literal]) => getPath(record, field) === literal)) continue;
5297
+ for (const [slot, rules] of Object.entries(catalog.slots)) if (Object.entries(rules).every(([field, tokens]) => {
5298
+ const value = getPath(record, field);
5299
+ return (Array.isArray(tokens) ? tokens : [tokens]).every((token) => token.startsWith("!") ? !fieldContains(value, token.slice(1)) : fieldContains(value, token));
5300
+ }) && !out[slot].includes(id)) out[slot].push(id);
5301
+ }
5302
+ return out;
5303
+ }
5304
+ //#endregion
5305
+ //#region src/core/registry/plugin-params.ts
5306
+ /**
5307
+ * The params a node offers for the active plugin + model: every declared entry
5308
+ * whose `models` gate is absent or names the selected model.
5309
+ */
5310
+ function visiblePluginParams(params, model) {
5311
+ if (!params) return [];
5312
+ const out = [];
5313
+ for (const [name, spec] of Object.entries(params)) {
5314
+ if (spec.models && !(model && spec.models.includes(model))) continue;
5315
+ out.push({
5316
+ name,
5317
+ spec
5318
+ });
5319
+ }
5320
+ return out;
5321
+ }
5322
+ /** True when `value` is a legal setting for `spec` (type + range/options). */
5323
+ function isValidPluginParamValue(spec, value) {
5324
+ switch (spec.type) {
5325
+ case "select": return (spec.options ?? []).some((o) => o === value);
5326
+ case "boolean": return typeof value === "boolean";
5327
+ case "text": return typeof value === "string";
5328
+ case "number":
5329
+ case "integer":
5330
+ if (typeof value !== "number" || !Number.isFinite(value)) return false;
5331
+ if (spec.type === "integer" && !Number.isInteger(value)) return false;
5332
+ if (spec.min !== void 0 && value < spec.min) return false;
5333
+ if (spec.max !== void 0 && value > spec.max) return false;
5334
+ return true;
5335
+ default: return false;
5336
+ }
5337
+ }
5338
+ /**
5339
+ * Keep only the entries of `values` that the visible params accept and that
5340
+ * differ from the declared default. Unset means "plugin default", so a value
5341
+ * equal to it is dropped; the result is what travels to the plugin.
5342
+ */
5343
+ function normalizePluginParams(entries, values) {
5344
+ const out = {};
5345
+ if (!values) return out;
5346
+ for (const { name, spec } of entries) {
5347
+ if (!(name in values)) continue;
5348
+ const v = values[name];
5349
+ if (!isValidPluginParamValue(spec, v)) continue;
5350
+ if (spec.default !== void 0 && v === spec.default) continue;
5351
+ out[name] = v;
5352
+ }
5353
+ return out;
5354
+ }
5355
+ /** Shallow key/value equality for two param records. */
5356
+ function samePluginParams(a, b) {
5357
+ const ka = Object.keys(a ?? {});
5358
+ const kb = Object.keys(b ?? {});
5359
+ if (ka.length !== kb.length) return false;
5360
+ return ka.every((k) => Object.is(a?.[k], b?.[k]));
5361
+ }
5176
5362
  /**
5177
5363
  * Deep-copy the current canvas state for the history stack, stripping
5178
5364
  * transient interaction flags so undo never restores a stale selection
@@ -5267,6 +5453,10 @@ const NODE_SIZE_TABLE = {
5267
5453
  w: 520,
5268
5454
  h: 480
5269
5455
  },
5456
+ textGenAudioNode: {
5457
+ w: 480,
5458
+ h: 480
5459
+ },
5270
5460
  musicExtractNode: {
5271
5461
  w: 420,
5272
5462
  h: 400
@@ -5771,7 +5961,7 @@ function createFlowSlice(set, get, options = {}) {
5771
5961
  if (newlyCreatedIds.length > 0) get().nodeCreatedCallbacks.forEach((cb) => cb(newlyCreatedIds));
5772
5962
  return ids;
5773
5963
  },
5774
- compose: ({ type, data }) => {
5964
+ compose: ({ type, data, sourceOrder }) => {
5775
5965
  get().commitHistory();
5776
5966
  const { comboSelectedIds, nodes, edges } = get();
5777
5967
  const nodeId = createId();
@@ -5799,7 +5989,7 @@ function createFlowSlice(set, get, options = {}) {
5799
5989
  data: data ?? {}
5800
5990
  };
5801
5991
  const usedTargetHandles = /* @__PURE__ */ new Set();
5802
- const newEdges = Array.from(comboSelectedIds).map((id) => {
5992
+ const newEdges = (sourceOrder ? [...sourceOrder.filter((id) => comboSelectedIds.has(id)), ...Array.from(comboSelectedIds).filter((id) => !sourceOrder.includes(id))] : Array.from(comboSelectedIds)).map((id) => {
5803
5993
  const node = nodes.find((n) => n.id === id);
5804
5994
  if (!node) return null;
5805
5995
  const { sourceHandle, targetHandle } = resolveEdgeHandles({
@@ -6005,6 +6195,7 @@ var en_default = {
6005
6195
  "separateAudio": "Separate Vocals",
6006
6196
  "separateSpeaker": "Speaker Separation",
6007
6197
  "separateSound": "Extract Sound",
6198
+ "speakWithStyle": "Speak with Style",
6008
6199
  "denoise": "Denoise",
6009
6200
  "convertVoice": "Voice Replacement",
6010
6201
  "filter": "Filter",
@@ -6142,6 +6333,9 @@ var en_default = {
6142
6333
  "modalDeployProgressHint": "Deployed {done}/{total}",
6143
6334
  "pluginImplementationTitle": "Implementation",
6144
6335
  "pluginModelTitle": "Model",
6336
+ "pluginParamsTitle": "Advanced",
6337
+ "pluginParamsReset": "Reset",
6338
+ "pluginParamDefault": "default {value}",
6145
6339
  "pluginImplementationLoading": "Loading plugin implementations…",
6146
6340
  "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.",
6147
6341
  "pluginRegistryLoadError": "Could not load the plugin registry: {message}",
@@ -6267,11 +6461,7 @@ var en_default = {
6267
6461
  "referenceTextPlaceholder": "Optional: transcript of the reference audio — improves clone quality",
6268
6462
  "referenceTextCloneHint": "Leave empty for fast x-vector clone; paste the reference transcript for higher-quality ICL clone",
6269
6463
  "language": "Language",
6270
- "inputText": "Input Text",
6271
- "voiceOptions": {
6272
- "female": "Female Voice",
6273
- "male": "Male Voice"
6274
- }
6464
+ "inputText": "Input Text"
6275
6465
  },
6276
6466
  "titles": {
6277
6467
  "imageGenVideo": "Image to Video",
@@ -6335,7 +6525,8 @@ var en_default = {
6335
6525
  "musicExtract": "Extract Stem",
6336
6526
  "musicLego": "Add Track",
6337
6527
  "musicComplete": "Complete Arrangement",
6338
- "musicBrief": "Music Brief"
6528
+ "musicBrief": "Music Brief",
6529
+ "textGenAudio": "Text to Audio"
6339
6530
  },
6340
6531
  "options": {
6341
6532
  "portrait": "Portrait",
@@ -6642,15 +6833,9 @@ var en_default = {
6642
6833
  "audioPromptPlaceholder": "Enter audio dubbing prompt"
6643
6834
  },
6644
6835
  "convertVoice": {
6645
- "voiceLabel": "Voice:",
6646
- "selectVoice": "Select Voice",
6647
- "previewVoice": "Preview Voice",
6648
- "uploadVoice": "Upload Voice",
6649
- "recordVoice": "Record Voice",
6650
- "recordAudio": "Record Audio",
6651
- "recordHint": "You can record audio here.",
6652
- "recordNeedImpl": "Audio recording feature needs implementation",
6653
- "done": "Done"
6836
+ "sourceAudio": "Source audio",
6837
+ "referenceVoice": "Reference voice",
6838
+ "connectHint": "Connect the audio to convert and a reference voice clip"
6654
6839
  },
6655
6840
  "removeWatermark": { "hint": "Automatically detect and remove Sora video watermarks" },
6656
6841
  "videoGenText": {
@@ -6844,6 +7029,12 @@ var en_default = {
6844
7029
  "separateSound": {
6845
7030
  "textLabel": "Sound to isolate",
6846
7031
  "textPlaceholder": "e.g. dog barking, piano, sirens in the background"
7032
+ },
7033
+ "textGenAudio": {
7034
+ "voiceReference": "Reference voices",
7035
+ "promptLabel": "Audio prompt",
7036
+ "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.",
7037
+ "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."
6847
7038
  }
6848
7039
  }
6849
7040
  },
@@ -6906,6 +7097,7 @@ var en_default = {
6906
7097
  "copy": "Copy",
6907
7098
  "copied": "Copied",
6908
7099
  "close": "Close",
7100
+ "openModalBilling": "Open Modal billing",
6909
7101
  "needHelp": "Stuck? Come find us:",
6910
7102
  "wechatGroup": "WeChat group",
6911
7103
  "wechatQrAlt": "WeChat group QR code"
@@ -6992,7 +7184,7 @@ var ja_default = {
6992
7184
  "saveSuccess": "保存しました!",
6993
7185
  "saveFailed": "保存に失敗しました。もう一度お試しください",
6994
7186
  "generateVideo": "動画生成",
6995
- "generateAudio": "音声生成",
7187
+ "generateAudio": "オーディオ生成",
6996
7188
  "generateText": "テキスト生成",
6997
7189
  "generateImage": "画像生成",
6998
7190
  "generateMusic": "音楽生成",
@@ -7037,6 +7229,7 @@ var ja_default = {
7037
7229
  "separateAudio": "ボーカル分離",
7038
7230
  "separateSpeaker": "話者分離",
7039
7231
  "separateSound": "音を抽出",
7232
+ "speakWithStyle": "感情付き音声",
7040
7233
  "denoise": "ノイズ除去",
7041
7234
  "convertVoice": "音声置換",
7042
7235
  "filter": "フィルター",
@@ -7174,6 +7367,9 @@ var ja_default = {
7174
7367
  "modalDeployProgressHint": "{done}/{total} をデプロイ済み",
7175
7368
  "pluginImplementationTitle": "実装",
7176
7369
  "pluginModelTitle": "モデル",
7370
+ "pluginParamsTitle": "詳細設定",
7371
+ "pluginParamsReset": "リセット",
7372
+ "pluginParamDefault": "既定 {value}",
7177
7373
  "pluginImplementationLoading": "プラグイン実装一覧を読み込み中…",
7178
7374
  "pluginImplementationEmpty": "この機能向けのプラグイン実装がスキャン結果にありません。リポジトリの plugins/ に Modal または API プラグインを追加してください(README 参照)。ページを再読み込みしてください。",
7179
7375
  "pluginRegistryLoadError": "プラグイン登録情報を読み込めませんでした:{message}",
@@ -7299,11 +7495,7 @@ var ja_default = {
7299
7495
  "referenceTextPlaceholder": "任意:参照音声の書き起こしテキスト。クローン品質の向上に役立ちます",
7300
7496
  "referenceTextCloneHint": "空欄の場合は x-vector クローン。転写テキストを入力すると ICL 高品質クローンになります",
7301
7497
  "language": "言語",
7302
- "inputText": "テキスト入力",
7303
- "voiceOptions": {
7304
- "female": "女性の声",
7305
- "male": "男性の声"
7306
- }
7498
+ "inputText": "テキスト入力"
7307
7499
  },
7308
7500
  "titles": {
7309
7501
  "imageGenVideo": "画像から動画",
@@ -7367,7 +7559,8 @@ var ja_default = {
7367
7559
  "musicExtract": "ステム抽出",
7368
7560
  "musicLego": "トラック追加",
7369
7561
  "musicComplete": "編曲補完",
7370
- "musicBrief": "音楽ブリーフ"
7562
+ "musicBrief": "音楽ブリーフ",
7563
+ "textGenAudio": "テキストからオーディオ"
7371
7564
  },
7372
7565
  "options": {
7373
7566
  "portrait": "縦向き",
@@ -7390,7 +7583,7 @@ var ja_default = {
7390
7583
  "describeVideo": "動画を説明",
7391
7584
  "recognizeSpeech": "音声を認識",
7392
7585
  "generateMusic": "音楽を生成",
7393
- "generateAudio": "音声を生成",
7586
+ "generateAudio": "オーディオを生成",
7394
7587
  "startReplace": "置換を開始",
7395
7588
  "startDenoise": "ノイズ除去を開始",
7396
7589
  "parseDocument": "ドキュメントを解析",
@@ -7588,15 +7781,9 @@ var ja_default = {
7588
7781
  "audioPromptPlaceholder": "音声吹き替えのプロンプトを入力"
7589
7782
  },
7590
7783
  "convertVoice": {
7591
- "voiceLabel": "音声:",
7592
- "selectVoice": "音声を選択",
7593
- "previewVoice": "音声をプレビュー",
7594
- "uploadVoice": "音声をアップロード",
7595
- "recordVoice": "音声を録音",
7596
- "recordAudio": "音声を録音",
7597
- "recordHint": "ここで音声を録音できます。",
7598
- "recordNeedImpl": "音声録音機能は実装が必要です",
7599
- "done": "完了"
7784
+ "sourceAudio": "元の音声",
7785
+ "referenceVoice": "参考ボイス",
7786
+ "connectHint": "変換したい音声と、参考にする声の音声をつないでください"
7600
7787
  },
7601
7788
  "removeWatermark": { "hint": "Sora動画の透かしを自動検出して削除" },
7602
7789
  "videoGenText": {
@@ -7790,6 +7977,12 @@ var ja_default = {
7790
7977
  "separateSound": {
7791
7978
  "textLabel": "抽出したい音",
7792
7979
  "textPlaceholder": "例:犬の鳴き声、ピアノ、背景のサイレン"
7980
+ },
7981
+ "textGenAudio": {
7982
+ "voiceReference": "参照ボイス",
7983
+ "promptLabel": "オーディオの説明",
7984
+ "promptPlaceholder": "場面をまるごと書きます:誰がどう話すか、背景の環境音、効果音。セリフは引用符で囲みます。例:@voice1 が言う:「今日はいい天気だね。」窓の外で小雨が降っている。",
7985
+ "hint": "話し声・効果音・環境音を一度に生成します。参照ボイスとして音声を最大 3 本つなぎ、クリックすると @voice1…@voice3 が挿入されます。"
7793
7986
  }
7794
7987
  }
7795
7988
  },
@@ -7852,6 +8045,7 @@ var ja_default = {
7852
8045
  "copy": "コピー",
7853
8046
  "copied": "コピーしました",
7854
8047
  "close": "閉じる",
8048
+ "openModalBilling": "Modal の請求設定を開く",
7855
8049
  "needHelp": "解決しない場合はこちらへ:",
7856
8050
  "wechatGroup": "WeChat グループ",
7857
8051
  "wechatQrAlt": "WeChat グループの QR コード"
@@ -7983,6 +8177,7 @@ var ko_default = {
7983
8177
  "separateAudio": "보컬 분리",
7984
8178
  "separateSpeaker": "화자 분리",
7985
8179
  "separateSound": "소리 추출",
8180
+ "speakWithStyle": "감정 음성 생성",
7986
8181
  "denoise": "노이즈 제거",
7987
8182
  "convertVoice": "음성 교체",
7988
8183
  "filter": "필터",
@@ -8120,6 +8315,9 @@ var ko_default = {
8120
8315
  "modalDeployProgressHint": "{done}/{total} 배포됨",
8121
8316
  "pluginImplementationTitle": "구현",
8122
8317
  "pluginModelTitle": "모델",
8318
+ "pluginParamsTitle": "고급 설정",
8319
+ "pluginParamsReset": "초기화",
8320
+ "pluginParamDefault": "기본 {value}",
8123
8321
  "pluginImplementationLoading": "플러그인 구현을 불러오는 중…",
8124
8322
  "pluginImplementationEmpty": "이 기능에 대해 스캔된 플러그인 구현이 없습니다. 저장소의 plugins/ 디렉터리 아래에 Modal 또는 API 플러그인을 추가한 후(README 참조) 이 페이지를 새로 고치세요.",
8125
8323
  "pluginRegistryLoadError": "플러그인 레지스트리를 불러올 수 없습니다: {message}",
@@ -8245,11 +8443,7 @@ var ko_default = {
8245
8443
  "referenceTextPlaceholder": "선택 사항: 참조 오디오의 전사본 — 복제 품질을 향상시킵니다",
8246
8444
  "referenceTextCloneHint": "빠른 x-vector 복제는 비워 두고, 더 높은 품질의 ICL 복제를 위해서는 참조 전사본을 붙여넣으세요",
8247
8445
  "language": "언어",
8248
- "inputText": "입력 텍스트",
8249
- "voiceOptions": {
8250
- "female": "여성 목소리",
8251
- "male": "남성 목소리"
8252
- }
8446
+ "inputText": "입력 텍스트"
8253
8447
  },
8254
8448
  "titles": {
8255
8449
  "imageGenVideo": "이미지-비디오",
@@ -8313,7 +8507,8 @@ var ko_default = {
8313
8507
  "musicExtract": "스템 추출",
8314
8508
  "musicLego": "트랙 추가",
8315
8509
  "musicComplete": "편곡 완성",
8316
- "musicBrief": "음악 브리프"
8510
+ "musicBrief": "음악 브리프",
8511
+ "textGenAudio": "텍스트로 오디오 생성"
8317
8512
  },
8318
8513
  "options": {
8319
8514
  "portrait": "세로",
@@ -8620,15 +8815,9 @@ var ko_default = {
8620
8815
  "audioPromptPlaceholder": "오디오 더빙 프롬프트 입력"
8621
8816
  },
8622
8817
  "convertVoice": {
8623
- "voiceLabel": "음성:",
8624
- "selectVoice": "음성 선택",
8625
- "previewVoice": "음성 미리듣기",
8626
- "uploadVoice": "음성 업로드",
8627
- "recordVoice": "음성 녹음",
8628
- "recordAudio": "오디오 녹음",
8629
- "recordHint": "여기에서 오디오를 녹음할 수 있습니다.",
8630
- "recordNeedImpl": "오디오 녹음 기능 구현이 필요합니다",
8631
- "done": "완료"
8818
+ "sourceAudio": "원본 오디오",
8819
+ "referenceVoice": "참고 목소리",
8820
+ "connectHint": "바꿀 오디오와 참고할 목소리 오디오를 연결하세요"
8632
8821
  },
8633
8822
  "removeWatermark": { "hint": "Sora 비디오 워터마크를 자동으로 감지하고 제거합니다" },
8634
8823
  "videoGenText": {
@@ -8822,6 +9011,12 @@ var ko_default = {
8822
9011
  "separateSound": {
8823
9012
  "textLabel": "추출할 소리",
8824
9013
  "textPlaceholder": "예: 개 짖는 소리, 피아노, 배경 사이렌"
9014
+ },
9015
+ "textGenAudio": {
9016
+ "voiceReference": "참조 음색",
9017
+ "promptLabel": "오디오 설명",
9018
+ "promptPlaceholder": "장면 전체를 적어 주세요. 누가 어떻게 말하는지, 배경 소리, 효과음까지. 대사는 따옴표로 묶어요. 예: @voice1이 말한다: \"오늘 날씨 좋다.\" 창밖에 가랑비가 내린다.",
9019
+ "hint": "말소리, 효과음, 배경음을 한 번에 만들어요. 참조 음색으로 오디오를 최대 3개까지 연결하고, 누르면 @voice1…@voice3이 설명에 들어가요."
8825
9020
  }
8826
9021
  }
8827
9022
  },
@@ -8884,6 +9079,7 @@ var ko_default = {
8884
9079
  "copy": "복사",
8885
9080
  "copied": "복사됨",
8886
9081
  "close": "닫기",
9082
+ "openModalBilling": "Modal 결제 설정 열기",
8887
9083
  "needHelp": "해결이 안 되면 커뮤니티로 오세요:",
8888
9084
  "wechatGroup": "WeChat 그룹",
8889
9085
  "wechatQrAlt": "WeChat 그룹 QR 코드"
@@ -8970,7 +9166,7 @@ var zh_default = {
8970
9166
  "saveSuccess": "保存成功!",
8971
9167
  "saveFailed": "保存失败,请重试",
8972
9168
  "generateVideo": "生成视频",
8973
- "generateAudio": "生成语音",
9169
+ "generateAudio": "生成音频",
8974
9170
  "generateText": "生成文本",
8975
9171
  "generateImage": "生成图片",
8976
9172
  "generateMusic": "生成音乐",
@@ -9015,6 +9211,7 @@ var zh_default = {
9015
9211
  "separateAudio": "人声分离",
9016
9212
  "separateSpeaker": "说话人分离",
9017
9213
  "separateSound": "提取声音",
9214
+ "speakWithStyle": "按情绪配音",
9018
9215
  "denoise": "降噪",
9019
9216
  "convertVoice": "人声替换",
9020
9217
  "filter": "筛选",
@@ -9152,6 +9349,9 @@ var zh_default = {
9152
9349
  "modalDeployProgressHint": "已部署 {done}/{total}",
9153
9350
  "pluginImplementationTitle": "实现",
9154
9351
  "pluginModelTitle": "模型",
9352
+ "pluginParamsTitle": "高级参数",
9353
+ "pluginParamsReset": "重置",
9354
+ "pluginParamDefault": "默认 {value}",
9155
9355
  "pluginImplementationLoading": "正在加载插件实现列表…",
9156
9356
  "pluginImplementationEmpty": "未扫描到适用于该能力的插件实现。请在仓库的 plugins/ 目录下添加 Modal 或 API 插件(见 README),然后刷新页面。",
9157
9357
  "pluginRegistryLoadError": "无法加载插件注册表:{message}",
@@ -9277,11 +9477,7 @@ var zh_default = {
9277
9477
  "referenceTextPlaceholder": "可选:参考音频的转写文本,有助于提升克隆质量",
9278
9478
  "referenceTextCloneHint": "不填则使用 x-vector 快速克隆;填写参考音频转写文本可启用更高质量的 ICL 克隆",
9279
9479
  "language": "语言",
9280
- "inputText": "输入文本",
9281
- "voiceOptions": {
9282
- "female": "普通女声",
9283
- "male": "普通男声"
9284
- }
9480
+ "inputText": "输入文本"
9285
9481
  },
9286
9482
  "titles": {
9287
9483
  "imageGenVideo": "图片生成视频",
@@ -9345,7 +9541,8 @@ var zh_default = {
9345
9541
  "musicExtract": "分离音轨",
9346
9542
  "musicLego": "添加乐轨",
9347
9543
  "musicComplete": "补全编曲",
9348
- "musicBrief": "音乐企划"
9544
+ "musicBrief": "音乐企划",
9545
+ "textGenAudio": "文本生成音频"
9349
9546
  },
9350
9547
  "options": {
9351
9548
  "portrait": "竖屏",
@@ -9652,15 +9849,9 @@ var zh_default = {
9652
9849
  "audioPromptPlaceholder": "输入音频配音提示词"
9653
9850
  },
9654
9851
  "convertVoice": {
9655
- "voiceLabel": "音色:",
9656
- "selectVoice": "请选择音色",
9657
- "previewVoice": "试听当前音色",
9658
- "uploadVoice": "上传音色",
9659
- "recordVoice": "录制音色",
9660
- "recordAudio": "录制音频",
9661
- "recordHint": "您可以在这里录制音频。",
9662
- "recordNeedImpl": "音频录制功能需要实现",
9663
- "done": "完成"
9852
+ "sourceAudio": "源音频",
9853
+ "referenceVoice": "参考音色",
9854
+ "connectHint": "连上要换声的音频,再连一段参考音色"
9664
9855
  },
9665
9856
  "removeWatermark": { "hint": "自动识别并去除 Sora 视频水印" },
9666
9857
  "videoGenText": {
@@ -9854,6 +10045,12 @@ var zh_default = {
9854
10045
  "separateSound": {
9855
10046
  "textLabel": "要提取的声音",
9856
10047
  "textPlaceholder": "如:狗叫、钢琴、背景里的警笛"
10048
+ },
10049
+ "textGenAudio": {
10050
+ "voiceReference": "参考音色",
10051
+ "promptLabel": "音频描述",
10052
+ "promptPlaceholder": "把整段场景写出来:谁在说话、怎么说,背景环境,音效。台词用引号,例如:@voice1 说:“今天天气真好。”窗外下着小雨。",
10053
+ "hint": "一次生成人声、音效和环境声。最多连 3 段音频作参考音色,点一下就把 @voice1…@voice3 插进描述里。"
9857
10054
  }
9858
10055
  }
9859
10056
  },
@@ -9916,6 +10113,7 @@ var zh_default = {
9916
10113
  "copy": "复制",
9917
10114
  "copied": "已复制",
9918
10115
  "close": "关闭",
10116
+ "openModalBilling": "打开 Modal 账单设置",
9919
10117
  "needHelp": "解决不了?来社群里找我们:",
9920
10118
  "wechatGroup": "微信群",
9921
10119
  "wechatQrAlt": "微信群二维码"
@@ -10006,6 +10204,7 @@ function hostFetch(input, init) {
10006
10204
  }
10007
10205
  /** URL for an asset `file_key` (private uploads endpoint by default). */
10008
10206
  function assetUrl(fileKey) {
10207
+ if (/^https?:\/\//.test(fileKey)) return fileKey;
10009
10208
  return host.resolveAssetUrl ? host.resolveAssetUrl(fileKey) : `${host.apiBaseUrl}/api/uploads/${fileKey}`;
10010
10209
  }
10011
10210
  /**
@@ -12711,12 +12910,84 @@ function dedupeIds(list) {
12711
12910
  function useNodePluginIds(nodeSlot) {
12712
12911
  return dedupeIds(usePluginsRegistryStore((s) => s.registry)?.nodePluginMap?.[nodeSlot] ?? []);
12713
12912
  }
12913
+ const useLiveModelsStore = (0, zustand.create)(() => ({ byPlugin: {} }));
12914
+ const MODEL_CATALOG_TTL_MS = 6e5;
12915
+ const catalogInflight = /* @__PURE__ */ new Map();
12714
12916
  /**
12715
- * Model ids a plugin declares for one ABI `nodeSlot` (empty for single-model
12716
- * plugins — the model dropdown is hidden in that case).
12917
+ * Fetch (or refresh, once the TTL elapsed) a plugin's live model catalog. Safe
12918
+ * to call eagerly — it is a no-op for plugins without a catalog and while a
12919
+ * fresh result is cached.
12920
+ */
12921
+ async function loadPluginModelCatalog(pluginId) {
12922
+ const catalog = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.modelCatalog;
12923
+ if (!catalog) return;
12924
+ const cached = useLiveModelsStore.getState().byPlugin[pluginId];
12925
+ if (cached && Date.now() - cached.fetchedAt < MODEL_CATALOG_TTL_MS) return;
12926
+ const inflight = catalogInflight.get(pluginId);
12927
+ if (inflight) return inflight;
12928
+ const job = (async () => {
12929
+ try {
12930
+ const res = catalog.authEnv ? await hostFetch(apiUrl(`/api/plugins/model-catalog?pluginId=${encodeURIComponent(pluginId)}`), {
12931
+ cache: "no-store",
12932
+ credentials: "same-origin"
12933
+ }) : await fetch(catalog.url, {
12934
+ method: "GET",
12935
+ cache: "no-store"
12936
+ });
12937
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
12938
+ const bySlot = filterModelCatalog(catalog, await res.json());
12939
+ useLiveModelsStore.setState((s) => ({ byPlugin: {
12940
+ ...s.byPlugin,
12941
+ [pluginId]: {
12942
+ fetchedAt: Date.now(),
12943
+ bySlot
12944
+ }
12945
+ } }));
12946
+ } catch (e) {
12947
+ console.warn(`[tongflow] model catalog for ${pluginId} unavailable:`, e);
12948
+ } finally {
12949
+ catalogInflight.delete(pluginId);
12950
+ }
12951
+ })();
12952
+ catalogInflight.set(pluginId, job);
12953
+ return job;
12954
+ }
12955
+ /**
12956
+ * Synchronous read of a plugin's model ids for one slot: the static shortlist
12957
+ * (declared order, first = default) followed by any live-catalog extras.
12958
+ */
12959
+ function getNodePluginModels(nodeSlot, pluginId) {
12960
+ const declared = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? [];
12961
+ const live = useLiveModelsStore.getState().byPlugin[pluginId]?.bySlot[nodeSlot] ?? [];
12962
+ return dedupeIds([...declared, ...live]);
12963
+ }
12964
+ /**
12965
+ * Model ids a plugin offers for one ABI `nodeSlot` (empty for single-model
12966
+ * plugins — the model dropdown is hidden in that case): the static shortlist
12967
+ * plus live-catalog extras, which this hook fetches on first use.
12717
12968
  */
12718
12969
  function useNodePluginModels(nodeSlot, pluginId) {
12719
- return dedupeIds(usePluginsRegistryStore((s) => s.registry)?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? []);
12970
+ const registry = usePluginsRegistryStore((s) => s.registry);
12971
+ const declared = registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.models ?? [];
12972
+ const hasCatalog = Boolean(registry?.plugins?.[pluginId]?.modelCatalog);
12973
+ const live = useLiveModelsStore((s) => s.byPlugin[pluginId]?.bySlot[nodeSlot]);
12974
+ (0, react.useEffect)(() => {
12975
+ if (hasCatalog) loadPluginModelCatalog(pluginId);
12976
+ }, [hasCatalog, pluginId]);
12977
+ return dedupeIds([...declared, ...live ?? []]);
12978
+ }
12979
+ /**
12980
+ * Synchronous read of the params a plugin offers for one slot given the
12981
+ * selected model, in declaration order.
12982
+ */
12983
+ function getNodePluginParams(nodeSlot, pluginId, model) {
12984
+ const registry = usePluginsRegistryStore.getState().registry;
12985
+ return visiblePluginParams(registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.params, model);
12986
+ }
12987
+ /** Reactive form of {@link getNodePluginParams}. */
12988
+ function useNodePluginParams(nodeSlot, pluginId, model) {
12989
+ const declared = usePluginsRegistryStore((s) => s.registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[nodeSlot]?.params);
12990
+ return (0, react.useMemo)(() => visiblePluginParams(declared, model), [declared, model]);
12720
12991
  }
12721
12992
  /** Presentation metadata (name/description/icon) for a single plugin. */
12722
12993
  function usePluginMeta(pluginId) {
@@ -12775,7 +13046,7 @@ function PluginIcon({ icon, label }) {
12775
13046
  /**
12776
13047
  * Shared plugin implementation selector (`plugins/<id>` directory name from registry).
12777
13048
  */
12778
- function NodePluginSelect({ value, onValueChange, options, title }) {
13049
+ function NodePluginSelect({ value, onValueChange, options, title, onOpenChange }) {
12779
13050
  const t = (0, use_intl.useTranslations)("Workspace.nodes.base");
12780
13051
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
12781
13052
  className: "p-3",
@@ -12787,6 +13058,7 @@ function NodePluginSelect({ value, onValueChange, options, title }) {
12787
13058
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Select, {
12788
13059
  value,
12789
13060
  onValueChange,
13061
+ onOpenChange,
12790
13062
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectTrigger, {
12791
13063
  className: "w-full",
12792
13064
  size: "sm",
@@ -12904,10 +13176,12 @@ function NodePluginIdSelect({ nodeSlot, data, dataKey = "pluginId" }) {
12904
13176
  //#region src/canvas/nodes/base/node-plugin-model-select.tsx
12905
13177
  /**
12906
13178
  * Model selector for router-style plugins that declare per-slot model lists
12907
- * (`TONGFLOW_SLOT_MODELS`). Renders nothing when the active plugin declares no
12908
- * models, so single-model plugins are visually unchanged. The selection is
12909
- * stored as `data.pluginModel` and travels top-level (like `pluginId`) through
12910
- * the create-task API and workflow export.
13179
+ * (`TONGFLOW_SLOT_MODELS`, optionally extended live via
13180
+ * `TONGFLOW_MODEL_CATALOG` — re-checked each time the dropdown opens). Renders
13181
+ * nothing when the active plugin declares no models, so single-model plugins
13182
+ * are visually unchanged. The selection is stored as `data.pluginModel` and
13183
+ * travels top-level (like `pluginId`) through the create-task API and workflow
13184
+ * export.
12911
13185
  */
12912
13186
  function NodePluginModelSelect({ nodeSlot, data }) {
12913
13187
  const id = (0, _xyflow_react.useNodeId)();
@@ -12942,10 +13216,218 @@ function NodePluginModelSelect({ nodeSlot, data }) {
12942
13216
  pluginModel: value
12943
13217
  }),
12944
13218
  options,
12945
- title: t("pluginModelTitle")
13219
+ title: t("pluginModelTitle"),
13220
+ onOpenChange: (open) => {
13221
+ if (open) loadPluginModelCatalog(pluginId);
13222
+ }
13223
+ });
13224
+ }
13225
+ //#endregion
13226
+ //#region src/canvas/ui/input.tsx
13227
+ function Input({ className, type, ...props }) {
13228
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
13229
+ type,
13230
+ "data-slot": "input",
13231
+ 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),
13232
+ ...props
13233
+ });
13234
+ }
13235
+ //#endregion
13236
+ //#region src/canvas/ui/switch.tsx
13237
+ function Switch({ className, ...props }) {
13238
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_switch.Root, {
13239
+ "data-slot": "switch",
13240
+ 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),
13241
+ ...props,
13242
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_switch.Thumb, {
13243
+ "data-slot": "switch-thumb",
13244
+ 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")
13245
+ })
12946
13246
  });
12947
13247
  }
12948
13248
  //#endregion
13249
+ //#region src/canvas/nodes/base/node-plugin-params.tsx
13250
+ function readParams(data) {
13251
+ const raw = data.pluginParams;
13252
+ return raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
13253
+ }
13254
+ /**
13255
+ * Collapsed "Advanced" section listing the run-time knobs the active plugin
13256
+ * declares for this slot (`TONGFLOW_SLOT_PARAMS`, optionally gated per router
13257
+ * model). Renders nothing for plugins that declare none. Choices live in
13258
+ * `data.pluginParams` as a diff against the declared defaults — an unset key
13259
+ * means "plugin default" — and travel top-level as `params` through the
13260
+ * create-task API and the workflow export, never inside the ABI prompt.
13261
+ */
13262
+ function NodePluginParams({ nodeSlot, data }) {
13263
+ const id = (0, _xyflow_react.useNodeId)();
13264
+ const updates = useFlow((s) => s.updates);
13265
+ const t = (0, use_intl.useTranslations)("Workspace.nodes.base");
13266
+ const [open, setOpen] = (0, react.useState)(false);
13267
+ const { resolved: pluginId } = useResolvedPluginId(nodeSlot, data);
13268
+ const models = useNodePluginModels(nodeSlot, pluginId);
13269
+ const currentModel = String(data.pluginModel ?? "").trim();
13270
+ const entries = useNodePluginParams(nodeSlot, pluginId, models.includes(currentModel) ? currentModel : models[0] ?? void 0);
13271
+ const values = (0, react.useMemo)(() => readParams(data), [data]);
13272
+ (0, react.useEffect)(() => {
13273
+ const normalized = normalizePluginParams(entries, values);
13274
+ if (samePluginParams(normalized, data.pluginParams)) return;
13275
+ if (Object.keys(normalized).length === 0 && data.pluginParams === void 0) return;
13276
+ const next = { ...data };
13277
+ if (Object.keys(normalized).length === 0) delete next.pluginParams;
13278
+ else next.pluginParams = normalized;
13279
+ updates(id, next, { history: false });
13280
+ }, [
13281
+ id,
13282
+ data,
13283
+ entries,
13284
+ values,
13285
+ updates
13286
+ ]);
13287
+ const setParam = (0, react.useCallback)((name, value) => {
13288
+ const next = { ...values };
13289
+ if (value === void 0) delete next[name];
13290
+ else next[name] = value;
13291
+ const normalized = normalizePluginParams(entries, next);
13292
+ const nextData = { ...data };
13293
+ if (Object.keys(normalized).length === 0) delete nextData.pluginParams;
13294
+ else nextData.pluginParams = normalized;
13295
+ updates(id, nextData);
13296
+ }, [
13297
+ id,
13298
+ data,
13299
+ entries,
13300
+ values,
13301
+ updates
13302
+ ]);
13303
+ const reset = (0, react.useCallback)(() => {
13304
+ const nextData = { ...data };
13305
+ delete nextData.pluginParams;
13306
+ updates(id, nextData);
13307
+ }, [
13308
+ id,
13309
+ data,
13310
+ updates
13311
+ ]);
13312
+ if (entries.length === 0) return null;
13313
+ const changed = Object.keys(values).length;
13314
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
13315
+ className: "p-3",
13316
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13317
+ className: "space-y-2",
13318
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13319
+ className: "flex items-center gap-1",
13320
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
13321
+ type: "button",
13322
+ onClick: () => setOpen((o) => !o),
13323
+ className: "nodrag flex flex-1 items-center gap-1 text-left text-sm font-medium text-muted-foreground hover:text-foreground",
13324
+ "aria-expanded": open,
13325
+ children: [
13326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ChevronRight, { className: `size-3.5 transition-transform ${open ? "rotate-90" : ""}` }),
13327
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pluginParamsTitle") }),
13328
+ changed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13329
+ className: "ml-1 rounded-full bg-primary/10 px-1.5 text-[10px] font-semibold text-primary",
13330
+ children: changed
13331
+ })
13332
+ ]
13333
+ }), changed > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
13334
+ type: "button",
13335
+ onClick: reset,
13336
+ title: t("pluginParamsReset"),
13337
+ "aria-label": t("pluginParamsReset"),
13338
+ className: "nodrag rounded p-1 text-muted-foreground hover:text-foreground",
13339
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.RotateCcw, { className: "size-3.5" })
13340
+ })]
13341
+ }), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13342
+ className: "space-y-2",
13343
+ children: entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamRow, {
13344
+ entry,
13345
+ value: values[entry.name],
13346
+ onChange: (v) => setParam(entry.name, v)
13347
+ }, entry.name))
13348
+ })]
13349
+ })
13350
+ });
13351
+ }
13352
+ function ParamRow({ entry, value, onChange }) {
13353
+ const t = (0, use_intl.useTranslations)("Workspace.nodes.base");
13354
+ const { name, spec } = entry;
13355
+ const label = spec.label ?? name;
13356
+ const inline = spec.type === "boolean";
13357
+ const changed = value !== void 0 && spec.default !== void 0;
13358
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13359
+ className: inline ? "flex items-center justify-between gap-2" : "space-y-1",
13360
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Label, {
13361
+ className: "flex items-baseline gap-1.5 text-xs text-muted-foreground",
13362
+ title: spec.description,
13363
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), changed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
13364
+ className: "text-[10px] text-muted-foreground/60",
13365
+ children: t("pluginParamDefault", { value: String(spec.default) })
13366
+ })]
13367
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ParamControl, {
13368
+ spec,
13369
+ value,
13370
+ onChange
13371
+ })]
13372
+ });
13373
+ }
13374
+ function ParamControl({ spec, value, onChange }) {
13375
+ const effective = value === void 0 ? spec.default : value;
13376
+ switch (spec.type) {
13377
+ case "select": {
13378
+ const options = spec.options ?? [];
13379
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Select, {
13380
+ value: effective === void 0 ? "" : String(effective),
13381
+ onValueChange: (v) => {
13382
+ onChange(options.find((o) => String(o) === v));
13383
+ },
13384
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectTrigger, {
13385
+ className: "nodrag w-full",
13386
+ size: "sm",
13387
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectValue, {})
13388
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectContent, { children: options.map((o) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectItem, {
13389
+ value: String(o),
13390
+ children: String(o)
13391
+ }, String(o))) })]
13392
+ });
13393
+ }
13394
+ case "boolean": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Switch, {
13395
+ className: "nodrag",
13396
+ checked: effective === true,
13397
+ onCheckedChange: (checked) => onChange(checked)
13398
+ });
13399
+ case "number":
13400
+ case "integer": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
13401
+ type: "number",
13402
+ className: "nodrag h-8",
13403
+ value: typeof effective === "number" ? String(effective) : "",
13404
+ min: spec.min,
13405
+ max: spec.max,
13406
+ step: spec.step ?? (spec.type === "integer" ? 1 : "any"),
13407
+ onChange: (e) => {
13408
+ const raw = e.target.value.trim();
13409
+ if (raw === "") {
13410
+ onChange(void 0);
13411
+ return;
13412
+ }
13413
+ const n = Number(raw);
13414
+ if (!Number.isFinite(n)) return;
13415
+ onChange(spec.type === "integer" ? Math.round(n) : n);
13416
+ }
13417
+ });
13418
+ case "text": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
13419
+ type: "text",
13420
+ className: "nodrag h-8",
13421
+ value: typeof effective === "string" ? effective : "",
13422
+ onChange: (e) => {
13423
+ const v = e.target.value;
13424
+ onChange(v === "" ? void 0 : v);
13425
+ }
13426
+ });
13427
+ default: return null;
13428
+ }
13429
+ }
13430
+ //#endregion
12949
13431
  //#region src/canvas/nodes/base/node-thinking-bubble.tsx
12950
13432
  /**
12951
13433
  * A transient, auto-scrolling bubble anchored to the right of a node that
@@ -13049,13 +13531,20 @@ const BaseNodeShell = (0, react.forwardRef)(({ className, selected, count, data,
13049
13531
  ] }),
13050
13532
  renderPluginSelect && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
13051
13533
  className: "p-4 pb-0 space-y-2",
13052
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePluginIdSelect, {
13053
- nodeSlot: feature,
13054
- data: data ?? { feature }
13055
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePluginModelSelect, {
13056
- nodeSlot: feature,
13057
- data: data ?? { feature }
13058
- })]
13534
+ children: [
13535
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePluginIdSelect, {
13536
+ nodeSlot: feature,
13537
+ data: data ?? { feature }
13538
+ }),
13539
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePluginModelSelect, {
13540
+ nodeSlot: feature,
13541
+ data: data ?? { feature }
13542
+ }),
13543
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodePluginParams, {
13544
+ nodeSlot: feature,
13545
+ data: data ?? { feature }
13546
+ })
13547
+ ]
13059
13548
  }),
13060
13549
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
13061
13550
  className: "relative z-0",
@@ -14054,16 +14543,6 @@ function useNodeState(initialState, nodeData) {
14054
14543
  }, [id, flowUpdates])];
14055
14544
  }
14056
14545
  //#endregion
14057
- //#region src/canvas/ui/input.tsx
14058
- function Input({ className, type, ...props }) {
14059
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
14060
- type,
14061
- "data-slot": "input",
14062
- 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),
14063
- ...props
14064
- });
14065
- }
14066
- //#endregion
14067
14546
  //#region src/canvas/nodes/add/add-link-node.tsx
14068
14547
  const AddLinkNode = ({ selected, data }) => {
14069
14548
  const t = (0, use_intl.useTranslations)("Workspace.nodes.add");
@@ -16152,24 +16631,47 @@ function useNodePluginResolver(feature) {
16152
16631
  defaultPluginIdFromRegistry,
16153
16632
  pluginOptions
16154
16633
  ]);
16634
+ /**
16635
+ * Model for router-style plugins: the node's `pluginModel` when it is one
16636
+ * of the active plugin's models (declared shortlist or live catalog), else
16637
+ * that plugin's default (first declared model). `undefined` when the
16638
+ * plugin declares none — the create-task API then omits the field entirely.
16639
+ */
16640
+ const resolveActiveModel = (0, react.useCallback)(() => {
16641
+ if (!feature) return void 0;
16642
+ const pluginId = resolveActivePluginId();
16643
+ if (!pluginId) return void 0;
16644
+ const models = getNodePluginModels(feature, pluginId);
16645
+ if (models.length === 0) return void 0;
16646
+ const n = nodeId ? getNode(nodeId) : void 0;
16647
+ const fromData = String((n?.data)?.pluginModel ?? "").trim();
16648
+ return models.includes(fromData) ? fromData : models[0];
16649
+ }, [
16650
+ feature,
16651
+ nodeId,
16652
+ getNode,
16653
+ resolveActivePluginId
16654
+ ]);
16155
16655
  return {
16156
16656
  pluginOptions,
16157
16657
  defaultPluginIdFromRegistry,
16158
16658
  resolveActivePluginId,
16159
- resolveActiveModel: (0, react.useCallback)(() => {
16659
+ resolveActiveModel,
16660
+ resolveActiveParams: (0, react.useCallback)(() => {
16160
16661
  if (!feature) return void 0;
16161
16662
  const pluginId = resolveActivePluginId();
16162
16663
  if (!pluginId) return void 0;
16163
- const models = usePluginsRegistryStore.getState().registry?.plugins?.[pluginId]?.methodsByNodeSlot?.[feature]?.models ?? [];
16164
- if (models.length === 0) return void 0;
16165
- const n = nodeId ? getNode(nodeId) : void 0;
16166
- const fromData = String((n?.data)?.pluginModel ?? "").trim();
16167
- return models.includes(fromData) ? fromData : models[0];
16664
+ const entries = getNodePluginParams(feature, pluginId, resolveActiveModel());
16665
+ if (entries.length === 0) return void 0;
16666
+ const raw = ((nodeId ? getNode(nodeId) : void 0)?.data)?.pluginParams;
16667
+ const params = normalizePluginParams(entries, raw && typeof raw === "object" && !Array.isArray(raw) ? raw : void 0);
16668
+ return Object.keys(params).length > 0 ? params : void 0;
16168
16669
  }, [
16169
16670
  feature,
16170
16671
  nodeId,
16171
16672
  getNode,
16172
- resolveActivePluginId
16673
+ resolveActivePluginId,
16674
+ resolveActiveModel
16173
16675
  ])
16174
16676
  };
16175
16677
  }
@@ -16265,7 +16767,7 @@ function useAbiExecution(opts) {
16265
16767
  const expands = useFlow((s) => s.expands);
16266
16768
  const { isLoading: taskLoading, createBatchTasks, cancelTasks } = useBatchTaskManager();
16267
16769
  const loading = taskLoading || nodeExecutionStatus === "running";
16268
- const { pluginOptions, resolveActivePluginId, resolveActiveModel } = useNodePluginResolver(feature);
16770
+ const { pluginOptions, resolveActivePluginId, resolveActiveModel, resolveActiveParams } = useNodePluginResolver(feature);
16269
16771
  const { isLoaded: pluginsRegistryLoaded } = usePluginsRegistry();
16270
16772
  const handleTaskUpdate = (0, react.useCallback)(async (task) => {
16271
16773
  if (!nodeId) return;
@@ -16371,11 +16873,13 @@ function useAbiExecution(opts) {
16371
16873
  prompt: prompts.length === 1 ? prompts[0] : prompts
16372
16874
  });
16373
16875
  const model = resolveActiveModel();
16876
+ const params = resolveActiveParams();
16374
16877
  try {
16375
16878
  const taskConfigs = prompts.map((prompt) => ({
16376
16879
  feature,
16377
16880
  pluginId,
16378
16881
  ...model ? { model } : {},
16882
+ ...params ? { params } : {},
16379
16883
  prompt,
16380
16884
  nodeId
16381
16885
  }));
@@ -16392,6 +16896,7 @@ function useAbiExecution(opts) {
16392
16896
  createBatchTasks,
16393
16897
  resolveActivePluginId,
16394
16898
  resolveActiveModel,
16899
+ resolveActiveParams,
16395
16900
  pluginOptions,
16396
16901
  pluginsRegistryLoaded,
16397
16902
  transformPrompts
@@ -17517,30 +18022,63 @@ var merge_video_audio_default = (0, react.memo)(MergeVideoAudioNode);
17517
18022
  //#endregion
17518
18023
  //#region src/canvas/nodes/compose/music-cover.tsx
17519
18024
  const DEFAULT_STRENGTH$1 = .8;
18025
+ function UpstreamTextPreview({ label, text }) {
18026
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
18027
+ className: "p-3",
18028
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
18029
+ className: "space-y-2",
18030
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
18031
+ className: "text-sm font-medium text-muted-foreground",
18032
+ children: label
18033
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
18034
+ className: "text-sm text-foreground p-2 bg-background rounded border border-border/50 line-clamp-4 whitespace-pre-line",
18035
+ children: text
18036
+ })]
18037
+ })
18038
+ });
18039
+ }
17520
18040
  const MusicCoverNode = ({ selected, data }) => {
17521
18041
  const t = (0, use_intl.useTranslations)("Workspace.nodes");
17522
18042
  const form = useAbiForm("music-cover");
17523
18043
  const nodeId = (0, _xyflow_react.useNodeId)();
17524
18044
  const edges = (0, _xyflow_react.useStore)((state) => state.edges);
17525
- const { audioSourceId, refAudioSourceId } = (0, react.useMemo)(() => {
17526
- if (!nodeId) return {
18045
+ const { audioSourceId, refAudioSourceId, textSourceId, lyricsSourceId } = (0, react.useMemo)(() => {
18046
+ const ids = {
17527
18047
  audioSourceId: null,
17528
- refAudioSourceId: null
18048
+ refAudioSourceId: null,
18049
+ textSourceId: null,
18050
+ lyricsSourceId: null
17529
18051
  };
17530
- let audioSrc = null;
17531
- let refAudioSrc = null;
18052
+ if (!nodeId) return ids;
17532
18053
  for (const e of edges) {
17533
18054
  if (e.target !== nodeId) continue;
17534
- if (e.targetHandle === "in:audio") audioSrc = e.source;
17535
- else if (e.targetHandle === "in:ref_audio") refAudioSrc = e.source;
18055
+ if (e.targetHandle === "in:audio") ids.audioSourceId = e.source;
18056
+ else if (e.targetHandle === "in:ref_audio") ids.refAudioSourceId = e.source;
18057
+ else if (e.targetHandle === "in:text") ids.textSourceId = e.source;
18058
+ else if (e.targetHandle === "in:lyrics") ids.lyricsSourceId = e.source;
17536
18059
  }
17537
- return {
17538
- audioSourceId: audioSrc,
17539
- refAudioSourceId: refAudioSrc
17540
- };
18060
+ return ids;
17541
18061
  }, [edges, nodeId]);
17542
- const upstreamIds = (0, react.useMemo)(() => [audioSourceId, refAudioSourceId].filter((v) => !!v), [audioSourceId, refAudioSourceId]);
18062
+ const upstreamIds = (0, react.useMemo)(() => [
18063
+ audioSourceId,
18064
+ refAudioSourceId,
18065
+ textSourceId,
18066
+ lyricsSourceId
18067
+ ].filter((v) => !!v), [
18068
+ audioSourceId,
18069
+ refAudioSourceId,
18070
+ textSourceId,
18071
+ lyricsSourceId
18072
+ ]);
17543
18073
  const upstreamNodes = (0, _xyflow_react.useNodesData)(upstreamIds);
18074
+ const upstreamText = (sourceId) => {
18075
+ if (!sourceId) return void 0;
18076
+ const n = upstreamNodes.find((u) => u.id === sourceId);
18077
+ if (!n || n.type !== "textNode") return void 0;
18078
+ return coerceBaseNodeData(n.data).texts?.[0] ?? "";
18079
+ };
18080
+ const promptFromUpstream = upstreamText(textSourceId);
18081
+ const lyricsFromUpstream = upstreamText(lyricsSourceId);
17544
18082
  const audioKey = (0, react.useMemo)(() => {
17545
18083
  if (!audioSourceId) return void 0;
17546
18084
  const n = upstreamNodes.find((u) => u.id === audioSourceId);
@@ -17603,14 +18141,20 @@ const MusicCoverNode = ({ selected, data }) => {
17603
18141
  })]
17604
18142
  })
17605
18143
  }),
17606
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeTextarea, {
18144
+ promptFromUpstream !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UpstreamTextPreview, {
18145
+ label: t("musicTasks.promptLabel"),
18146
+ text: promptFromUpstream
18147
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeTextarea, {
17607
18148
  label: t("musicTasks.promptLabel"),
17608
18149
  icon: lucide_react.MessageSquare,
17609
18150
  placeholder: t("musicTasks.promptPlaceholder"),
17610
18151
  ...form.bind("text"),
17611
18152
  rows: 3
17612
18153
  }),
17613
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeTextarea, {
18154
+ lyricsFromUpstream !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UpstreamTextPreview, {
18155
+ label: t("musicTasks.lyricsLabel"),
18156
+ text: lyricsFromUpstream
18157
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeTextarea, {
17614
18158
  label: t("musicTasks.lyricsLabel"),
17615
18159
  placeholder: t("musicTasks.lyricsPlaceholder"),
17616
18160
  ...form.bind("lyrics"),
@@ -18914,19 +19458,6 @@ const VideoImageGenVideoMoveNode = ({ selected, data }) => {
18914
19458
  VideoImageGenVideoMoveNode.displayName = "VideoImageGenVideoMoveNode";
18915
19459
  var video_image_gen_video_move_default = (0, react.memo)(VideoImageGenVideoMoveNode);
18916
19460
  //#endregion
18917
- //#region src/canvas/ui/switch.tsx
18918
- function Switch({ className, ...props }) {
18919
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_switch.Root, {
18920
- "data-slot": "switch",
18921
- 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),
18922
- ...props,
18923
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_radix_ui_react_switch.Thumb, {
18924
- "data-slot": "switch-thumb",
18925
- 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")
18926
- })
18927
- });
18928
- }
18929
- //#endregion
18930
19461
  //#region src/canvas/nodes/decompose/music-brief.tsx
18931
19462
  function buildLanguageOptions$1(tLang) {
18932
19463
  return [
@@ -21336,143 +21867,102 @@ AudioGenTextSpeechRecognizeNode.displayName = "AudioGenTextSpeechRecognizeNode";
21336
21867
  var audio_gen_text_speech_recognize_default = (0, react.memo)(AudioGenTextSpeechRecognizeNode);
21337
21868
  //#endregion
21338
21869
  //#region src/canvas/nodes/transfer/convert-voice.tsx
21339
- const VOICE_OPTIONS = [{
21340
- key: "female",
21341
- value: "zh_famale_1.wav"
21342
- }, {
21343
- key: "male",
21344
- value: "zh_male_1.wav"
21345
- }];
21870
+ const firstKey = (value) => {
21871
+ const v = Array.isArray(value) ? value[0] : value;
21872
+ return typeof v === "string" && v ? v : void 0;
21873
+ };
21346
21874
  const ConvertVoiceNode = ({ selected, data }) => {
21347
21875
  const t = (0, use_intl.useTranslations)("Workspace.nodes");
21348
21876
  const form = useAbiForm("convert_voice");
21349
- const fileKeys = data.fileKeys;
21350
- const [extraSpeakers, setExtraSpeakers] = (0, react.useState)([]);
21351
- const speakers = [...VOICE_OPTIONS, ...extraSpeakers];
21352
- const targetKey = form.state.targetKey ?? "zh_famale_1.wav";
21877
+ const nodeId = (0, _xyflow_react.useNodeId)();
21878
+ const nodeLookup = (0, _xyflow_react.useStore)((state) => state.nodeLookup);
21879
+ const edges = (0, _xyflow_react.useStore)((state) => state.edges);
21880
+ const resolvedSpec = useNodeAbiSpec("convert_voice");
21881
+ const { sourceKey, refKey } = (0, react.useMemo)(() => {
21882
+ if (!nodeId) return {
21883
+ sourceKey: void 0,
21884
+ refKey: void 0
21885
+ };
21886
+ const values = collectHandleValues(nodeId, resolvedSpec, Array.from(nodeLookup.values()), edges);
21887
+ return {
21888
+ sourceKey: firstKey(values.audio),
21889
+ refKey: firstKey(values.ref_audio)
21890
+ };
21891
+ }, [
21892
+ nodeId,
21893
+ resolvedSpec,
21894
+ nodeLookup,
21895
+ edges
21896
+ ]);
21353
21897
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AbiNodeShell, {
21354
21898
  feature: "convert_voice",
21355
21899
  form,
21356
21900
  selected,
21901
+ className: "min-w-[360px]",
21357
21902
  data,
21358
21903
  title: t("titles.convertVoice"),
21359
21904
  icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Atom, { className: "h-5 w-5" }),
21360
21905
  executeLabel: t("actions.startReplace"),
21361
- executeDisabled: !fileKeys?.length,
21362
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
21363
- className: "p-5 nodrag",
21364
- onPointerDown: (e) => e.stopPropagation(),
21365
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21366
- className: "mb-4 flex flex-wrap items-center gap-3",
21367
- children: [
21368
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
21369
- htmlFor: "voice-select",
21370
- className: "text-sm text-muted-foreground whitespace-nowrap",
21371
- children: t("convertVoice.voiceLabel")
21372
- }),
21373
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Select, {
21374
- value: targetKey,
21375
- onValueChange: (value) => form.set("targetKey", value),
21376
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectTrigger, {
21377
- id: "voice-select",
21378
- className: "w-36 h-9",
21379
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectValue, { placeholder: t("convertVoice.selectVoice") })
21380
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectContent, { children: speakers.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectItem, {
21381
- value: opt.value,
21382
- children: opt.key ? t(`common.voiceOptions.${opt.key}`) : opt.value
21383
- }, opt.value)) })]
21384
- }),
21385
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerVoiceUploader, {
21386
- trigger: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
21387
- variant: "outline",
21388
- size: "icon",
21389
- className: "ml-1",
21390
- title: t("convertVoice.uploadVoice"),
21391
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Upload, { className: "w-4 h-4" })
21906
+ executeDisabled: !sourceKey || !refKey,
21907
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21908
+ className: "p-4",
21909
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
21910
+ className: "p-3",
21911
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21912
+ className: "space-y-2",
21913
+ children: [
21914
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Label, {
21915
+ className: "text-sm font-medium text-muted-foreground",
21916
+ children: t("compose.inputData")
21392
21917
  }),
21393
- onChange: (key) => {
21394
- setExtraSpeakers((prev) => [...prev, {
21395
- key: "",
21396
- value: key
21397
- }]);
21398
- }
21399
- }),
21400
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpeakerVoiceRecorder$1, {
21401
- trigger: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
21402
- variant: "outline",
21403
- size: "icon",
21404
- className: "ml-1",
21405
- title: t("convertVoice.recordVoice"),
21406
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Mic, { className: "w-4 h-4" })
21918
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21919
+ className: "flex gap-4",
21920
+ children: [sourceKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MediaThumbnail, {
21921
+ fileKey: sourceKey,
21922
+ label: t("convertVoice.sourceAudio"),
21923
+ type: "audio"
21924
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21925
+ className: "flex flex-col items-center gap-1.5",
21926
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21927
+ className: "relative w-16 h-16 rounded-md border-2 border-gray-300 overflow-hidden bg-gray-100",
21928
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21929
+ className: "flex items-center justify-center h-full w-full bg-orange-50",
21930
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Music, { className: "w-6 h-6 text-orange-600" })
21931
+ })
21932
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21933
+ className: "px-1.5 py-0.5 bg-orange-100 text-orange-700 text-xs font-medium rounded",
21934
+ children: t("convertVoice.sourceAudio")
21935
+ })]
21936
+ }), refKey ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MediaThumbnail, {
21937
+ fileKey: refKey,
21938
+ label: t("convertVoice.referenceVoice"),
21939
+ type: "audio"
21940
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
21941
+ className: "flex flex-col items-center gap-1.5",
21942
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21943
+ className: "relative w-16 h-16 rounded-md border-2 border-gray-300 overflow-hidden bg-gray-100",
21944
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21945
+ className: "flex items-center justify-center h-full w-full bg-blue-50",
21946
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Mic, { className: "w-6 h-6 text-blue-600" })
21947
+ })
21948
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21949
+ className: "px-1.5 py-0.5 bg-blue-100 text-blue-700 text-xs font-medium rounded",
21950
+ children: t("convertVoice.referenceVoice")
21951
+ })]
21952
+ })]
21407
21953
  }),
21408
- onChange: (key) => {
21409
- setExtraSpeakers((prev) => [...prev, {
21410
- key: "",
21411
- value: key
21412
- }]);
21413
- }
21414
- })
21415
- ]
21954
+ (!sourceKey || !refKey) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
21955
+ className: "text-xs text-red-500",
21956
+ children: t("convertVoice.connectHint")
21957
+ })
21958
+ ]
21959
+ })
21416
21960
  })
21417
21961
  })
21418
21962
  });
21419
21963
  };
21964
+ ConvertVoiceNode.displayName = "ConvertVoiceNode";
21420
21965
  var convert_voice_default = (0, react.memo)(ConvertVoiceNode);
21421
- const SpeakerVoiceUploader = ({ trigger, onChange: _onChange }) => {
21422
- const [_uploaded, setUploaded] = (0, react.useState)(false);
21423
- const doUpload = async (files) => {
21424
- logger.debug("Uploading files:", files);
21425
- setUploaded(true);
21426
- };
21427
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
21428
- className: "cursor-pointer",
21429
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
21430
- type: "file",
21431
- multiple: true,
21432
- hidden: true,
21433
- onChange: (e) => {
21434
- const files = Array.from(e.target.files || []);
21435
- if (files.length > 0) doUpload(files);
21436
- }
21437
- }), trigger]
21438
- }) });
21439
- };
21440
- const SpeakerVoiceRecorder$1 = ({ trigger, onChange: _onChange }) => {
21441
- const t = (0, use_intl.useTranslations)("Workspace.nodes.convertVoice");
21442
- const [file, _setFile] = (0, react.useState)();
21443
- const onFinish = async () => {
21444
- if (!file) return;
21445
- logger.debug("Recording file:", file);
21446
- };
21447
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Dialog, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogTrigger, {
21448
- asChild: true,
21449
- children: trigger
21450
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogContent, {
21451
- className: "sm:max-w-md",
21452
- children: [
21453
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(DialogHeader, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogTitle, { children: t("recordAudio") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogDescription, { children: t("recordHint") })] }),
21454
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
21455
- className: "overflow-auto scroll-smooth w-80 max-h-80",
21456
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
21457
- className: "text-sm text-gray-500",
21458
- children: t("recordNeedImpl")
21459
- })
21460
- }),
21461
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogFooter, {
21462
- className: "sm:justify-start",
21463
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DialogClose, {
21464
- asChild: true,
21465
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
21466
- type: "button",
21467
- variant: "secondary",
21468
- onClick: () => onFinish(),
21469
- children: t("done")
21470
- })
21471
- })
21472
- })
21473
- ]
21474
- })] });
21475
- };
21476
21966
  //#endregion
21477
21967
  //#region src/canvas/nodes/transfer/denoise-audio.tsx
21478
21968
  const DenoiseAudioNode = ({ selected, data }) => {
@@ -22257,7 +22747,7 @@ const RemoveWatermarkNode = ({ selected, data }) => {
22257
22747
  executeLabel: t("actions.removeWatermark"),
22258
22748
  executeDisabled: !fileKeys?.length,
22259
22749
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Card, {
22260
- className: "p-5 space-y-4",
22750
+ className: "mx-4 mb-4 p-4 space-y-4",
22261
22751
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22262
22752
  className: "text-sm text-muted-foreground",
22263
22753
  children: t("removeWatermark.hint")
@@ -22402,6 +22892,109 @@ const SpeechGenVideoNode = ({ selected, data }) => {
22402
22892
  SpeechGenVideoNode.displayName = "SpeechGenVideoNode";
22403
22893
  var speech_gen_video_default = (0, react.memo)(SpeechGenVideoNode);
22404
22894
  //#endregion
22895
+ //#region src/canvas/nodes/transfer/text-gen-audio.tsx
22896
+ const MAX_REF_AUDIOS = 3;
22897
+ const VOICE_TAG = "@voice";
22898
+ const TextGenAudioNode = ({ selected, data }) => {
22899
+ const t = (0, use_intl.useTranslations)("Workspace.nodes");
22900
+ const form = useAbiForm("text-gen-audio");
22901
+ const ids = useUpstreamNodeIds(data.ids);
22902
+ const fromNodes = (0, _xyflow_react.useNodesData)(ids);
22903
+ const refAudios = fromNodes.filter((node) => node.type === "audioNode").map((node) => coerceBaseNodeData(node.data).fileKeys).filter((keys) => !!keys && keys.length > 0);
22904
+ const textNode = fromNodes.find((node) => node.type === "textNode");
22905
+ const upstreamTexts = (0, react.useMemo)(() => textNode ? coerceBaseNodeData(textNode.data).texts || [] : [], [textNode]);
22906
+ const hasUpstreamTexts = upstreamTexts.length > 0;
22907
+ const prompt = form.state.text ?? "";
22908
+ const textareaRef = (0, react.useRef)(null);
22909
+ const insertVoiceTag = (0, react.useCallback)((tag) => {
22910
+ const textarea = textareaRef.current;
22911
+ if (!textarea) return;
22912
+ const start = textarea.selectionStart;
22913
+ const end = textarea.selectionEnd;
22914
+ form.set("text", prompt.substring(0, start) + tag + prompt.substring(end));
22915
+ setTimeout(() => {
22916
+ textarea.focus();
22917
+ const cursor = start + tag.length;
22918
+ textarea.setSelectionRange(cursor, cursor);
22919
+ }, 0);
22920
+ }, [prompt, form]);
22921
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AbiNodeShell, {
22922
+ feature: "text-gen-audio",
22923
+ form,
22924
+ selected,
22925
+ className: "min-w-[480px]",
22926
+ data,
22927
+ title: t("titles.textGenAudio"),
22928
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.AudioWaveform, { className: "h-5 w-5" }),
22929
+ executeLabel: t("actions.generateAudio"),
22930
+ executeDisabled: !hasUpstreamTexts && !prompt.trim(),
22931
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22932
+ className: "p-4 space-y-4",
22933
+ children: [
22934
+ refAudios.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22935
+ className: "space-y-2",
22936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22937
+ className: "text-sm font-medium text-muted-foreground",
22938
+ children: [t("textGenAudio.voiceReference"), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22939
+ className: "ml-2 text-xs font-normal",
22940
+ children: [
22941
+ "(",
22942
+ refAudios.length,
22943
+ "/",
22944
+ MAX_REF_AUDIOS,
22945
+ ")"
22946
+ ]
22947
+ })]
22948
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22949
+ className: "flex gap-3 flex-wrap",
22950
+ children: refAudios.slice(0, MAX_REF_AUDIOS).map((keys, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MediaThumbnail, {
22951
+ fileKey: keys[0],
22952
+ label: `${VOICE_TAG}${index + 1}`,
22953
+ type: "audio",
22954
+ onClick: () => insertVoiceTag(`${VOICE_TAG}${index + 1}`)
22955
+ }, keys[0]))
22956
+ })]
22957
+ }),
22958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
22959
+ className: "text-xs text-muted-foreground",
22960
+ children: t("textGenAudio.hint")
22961
+ }),
22962
+ hasUpstreamTexts ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22963
+ className: "space-y-2",
22964
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22965
+ className: "flex items-center gap-2 text-sm font-medium text-muted-foreground",
22966
+ children: [
22967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Sparkles, { className: "h-4 w-4" }),
22968
+ t("textGenAudio.promptLabel"),
22969
+ t("imageEdit.fromUpstream")
22970
+ ]
22971
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22972
+ className: "space-y-1 max-h-32 overflow-y-auto",
22973
+ children: upstreamTexts.map((text, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
22974
+ className: "text-sm text-foreground p-2 bg-background rounded border border-border/50 line-clamp-3",
22975
+ children: text
22976
+ }, `${index}-${text.slice(0, 48)}`))
22977
+ })]
22978
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
22979
+ className: "space-y-2",
22980
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
22981
+ className: "flex items-center gap-2 text-sm font-medium text-muted-foreground",
22982
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Sparkles, { className: "h-4 w-4" }), t("textGenAudio.promptLabel")]
22983
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NodeTextarea, {
22984
+ ref: textareaRef,
22985
+ showCard: false,
22986
+ placeholder: t("textGenAudio.promptPlaceholder"),
22987
+ ...form.bind("text"),
22988
+ rows: 5
22989
+ })]
22990
+ })
22991
+ ]
22992
+ })
22993
+ });
22994
+ };
22995
+ TextGenAudioNode.displayName = "TextGenAudioNode";
22996
+ var text_gen_audio_default = (0, react.memo)(TextGenAudioNode);
22997
+ //#endregion
22405
22998
  //#region src/canvas/nodes/base/resolution-picker.tsx
22406
22999
  function ResolutionPicker({ tiers, value, onChange }) {
22407
23000
  const t = (0, use_intl.useTranslations)("Workspace.nodes");
@@ -22966,7 +23559,7 @@ const TextGenSpeechCloneNode = ({ selected, data }) => {
22966
23559
  executeLabel: t("actions.generateSpeech"),
22967
23560
  executeDisabled: !texts?.length || !refAudio,
22968
23561
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
22969
- className: "p-5 nodrag",
23562
+ className: "mx-4 mb-4 p-4 nodrag",
22970
23563
  onPointerDown: (e) => e.stopPropagation(),
22971
23564
  children: [
22972
23565
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -23080,7 +23673,7 @@ const TextGenSpeechInstructNode = ({ selected, data }) => {
23080
23673
  executeLabel: t("actions.generateSpeech"),
23081
23674
  executeDisabled: !texts?.length,
23082
23675
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
23083
- className: "p-5 nodrag",
23676
+ className: "mx-4 mb-4 p-4 nodrag",
23084
23677
  onPointerDown: (e) => e.stopPropagation(),
23085
23678
  children: [
23086
23679
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -23170,7 +23763,7 @@ const TextGenSpeechPresetNode = ({ selected, data }) => {
23170
23763
  executeLabel: t("actions.generateSpeech"),
23171
23764
  executeDisabled: !texts?.length,
23172
23765
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
23173
- className: "p-5 nodrag",
23766
+ className: "mx-4 mb-4 p-4 nodrag",
23174
23767
  onPointerDown: (e) => e.stopPropagation(),
23175
23768
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
23176
23769
  className: "mb-4 flex flex-wrap items-center gap-3",
@@ -23669,6 +24262,7 @@ const NODE_TYPES = {
23669
24262
  genTextNode: text_gen_text_default,
23670
24263
  textGenImageNode: text_gen_image_default,
23671
24264
  textGenMusicNode: text_gen_music_default,
24265
+ textGenAudioNode: text_gen_audio_default,
23672
24266
  textGenSpeechCloneNode: text_gen_speech_clone_default,
23673
24267
  textGenSpeechCloneComposeNode: text_gen_speech_clone_compose_default,
23674
24268
  textGenSpeechPresetNode: text_gen_speech_preset_default,
@@ -23760,6 +24354,7 @@ const NODE_CATEGORIES = {
23760
24354
  "genTextNode",
23761
24355
  "textGenImageNode",
23762
24356
  "textGenMusicNode",
24357
+ "textGenAudioNode",
23763
24358
  "textGenSpeechCloneNode",
23764
24359
  "textGenSpeechPresetNode",
23765
24360
  "textGenSpeechInstructNode",
@@ -24659,6 +25254,8 @@ exports.getGlobalFileCache = getGlobalFileCache;
24659
25254
  exports.getGlobalFileQueue = getGlobalFileQueue;
24660
25255
  exports.getImageDimensions = getImageDimensions;
24661
25256
  exports.getMaterialFileUrls = getMaterialFileUrls;
25257
+ exports.getNodePluginModels = getNodePluginModels;
25258
+ exports.getNodePluginParams = getNodePluginParams;
24662
25259
  exports.getPresignedUploadUrl = getPresignedUploadUrl;
24663
25260
  exports.getPrivateFileUrl = getPrivateFileUrl;
24664
25261
  exports.getSharedFileUrl = getSharedFileUrl;
@@ -24671,6 +25268,7 @@ exports.isImageFile = isImageFile;
24671
25268
  exports.isVideoFile = isVideoFile;
24672
25269
  exports.listMaterials = listMaterials;
24673
25270
  exports.listTasks = listTasks;
25271
+ exports.loadPluginModelCatalog = loadPluginModelCatalog;
24674
25272
  exports.normalizeTaskPayloadData = normalizeTaskPayloadData;
24675
25273
  exports.pluginDisplayName = pluginDisplayName;
24676
25274
  exports.refreshPluginsRegistry = refreshPluginsRegistry;
@@ -24690,11 +25288,13 @@ exports.useFileAsyncLoaderBatch = useFileAsyncLoaderBatch;
24690
25288
  exports.useFileLoaderStats = useFileLoaderStats;
24691
25289
  exports.useFileUrl = useFileUrl;
24692
25290
  exports.useFlow = useFlow;
25291
+ exports.useLiveModelsStore = useLiveModelsStore;
24693
25292
  exports.useMultipleUpload = useMultipleUpload;
24694
25293
  exports.useNodeAbiSpec = useNodeAbiSpec;
24695
25294
  exports.useNodePluginIds = useNodePluginIds;
24696
25295
  exports.useNodePluginIdsUnion = useNodePluginIdsUnion;
24697
25296
  exports.useNodePluginModels = useNodePluginModels;
25297
+ exports.useNodePluginParams = useNodePluginParams;
24698
25298
  exports.useNodePluginResolver = useNodePluginResolver;
24699
25299
  exports.useNodeState = useNodeState;
24700
25300
  exports.useNodeTaskUpdate = useNodeTaskUpdate;