bizydraft 0.2.82.dev20251209023307__py3-none-any.whl → 0.2.83__py3-none-any.whl

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.
bizydraft/oss_utils.py CHANGED
@@ -312,17 +312,9 @@ async def upload_mask(request):
312
312
  if "http" in original_subfolder:
313
313
  # subfolder 中包含 URL 基础路径
314
314
  original_subfolder = original_subfolder[original_subfolder.find("http") :]
315
- original_subfolder = unquote(original_subfolder)
316
- if "https:/" in original_subfolder and not original_subfolder.startswith(
317
- "https://"
318
- ):
319
- original_subfolder = original_subfolder.replace(
320
- "https:/", "https://", 1
321
- )
322
- if "http:/" in original_subfolder and not original_subfolder.startswith(
323
- "http://"
324
- ):
325
- original_subfolder = original_subfolder.replace("http:/", "http://", 1)
315
+ original_subfolder = unquote(original_subfolder).replace(
316
+ "https:/", "https://"
317
+ )
326
318
  original_url = f"{original_subfolder}/{original_filename}"
327
319
  elif original_filename.startswith(http_prefix_options):
328
320
  # filename 本身就是完整 URL
@@ -62,7 +62,7 @@ function replaceClipspaceUrl(urlString) {
62
62
  });
63
63
 
64
64
  const originalSetAttribute = HTMLImageElement.prototype.setAttribute;
65
- HTMLImageElement.prototype.setAttribute = function (name, value) {
65
+ HTMLImageElement.prototype.setAttribute = function(name, value) {
66
66
  if (name === 'src') {
67
67
  const modifiedValue = replaceClipspaceUrl(value);
68
68
  return originalSetAttribute.call(this, name, modifiedValue);
@@ -75,11 +75,11 @@ function replaceClipspaceUrl(urlString) {
75
75
  // 拦截上传响应,保存映射并篡改返回值
76
76
  // ═══════════════════════════════════════════════════════════════════════════
77
77
  const originalFetchApi = api.fetchApi;
78
- api.fetchApi = async function (url, options) {
78
+ api.fetchApi = async function(url, options) {
79
79
  const response = await originalFetchApi.call(this, url, options);
80
80
 
81
81
  const isUploadApi = url === '/upload/image' || url === '/upload/mask'
82
- || url === '/api/upload/image' || url === '/api/upload/mask';
82
+ || url === '/api/upload/image' || url === '/api/upload/mask';
83
83
 
84
84
  if (!isUploadApi || !response.ok) {
85
85
  return response;
@@ -89,7 +89,7 @@ api.fetchApi = async function (url, options) {
89
89
 
90
90
  // 检查是否是 OSS 上传响应
91
91
  const isOssUpload = data.subfolder?.includes('http://') || data.subfolder?.includes('https://')
92
- || data.name?.startsWith('http://') || data.name?.startsWith('https://');
92
+ || data.name?.startsWith('http://') || data.name?.startsWith('https://');
93
93
 
94
94
  if (!isOssUpload) return response;
95
95
 
@@ -118,7 +118,7 @@ api.fetchApi = async function (url, options) {
118
118
  finalUrl = ossUrl;
119
119
 
120
120
  [`clipspace-mask-${baseId}.png`, `clipspace-paint-${baseId}.png`,
121
- `clipspace-painted-${baseId}.png`, `clipspace-painted-masked-${baseId}.png`
121
+ `clipspace-painted-${baseId}.png`, `clipspace-painted-masked-${baseId}.png`
122
122
  ].forEach(v => window.CLIPSPACE_TO_OSS_MAP[v] = ossUrl);
123
123
 
124
124
  } else {
@@ -204,29 +204,16 @@ function convertClipspacePathsInPrompt(prompt) {
204
204
 
205
205
  for (const [inputKey, inputValue] of Object.entries(node.inputs)) {
206
206
  if (typeof inputValue === 'string' && inputValue.includes('clipspace')) {
207
- // 1) 特殊情况:clipspace/https://... 或 clipspace/http://...
208
- const ossUrlMatch = inputValue.match(/clipspace\/(https?:\/\/[^\s]+)/i);
209
- if (ossUrlMatch) {
210
- // 移除可能的 [input] 或 [output] 后缀
211
- let cleanUrl = ossUrlMatch[1].replace(/\s*\[(input|output)\]$/i, '');
212
- node.inputs[inputKey] = cleanUrl;
213
-
214
- if (inputKey === 'image' && node.inputs['image_name']) {
215
- node.inputs['image_name'] = cleanUrl.split('/').pop();
216
- }
217
- } else {
218
- // 2) 常规情况:clipspace/xxx.png
219
- const match = inputValue.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))/i);
220
- if (match) {
221
- const filename = match[1];
222
- const ossUrl = findOssUrl(filename);
207
+ const match = inputValue.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))/i);
208
+ if (match) {
209
+ const filename = match[1];
210
+ const ossUrl = findOssUrl(filename);
223
211
 
224
- if (ossUrl) {
225
- node.inputs[inputKey] = ossUrl;
212
+ if (ossUrl) {
213
+ node.inputs[inputKey] = ossUrl;
226
214
 
227
- if (inputKey === 'image' && node.inputs['image_name']) {
228
- node.inputs['image_name'] = ossUrl.split('/').pop();
229
- }
215
+ if (inputKey === 'image' && node.inputs['image_name']) {
216
+ node.inputs['image_name'] = ossUrl.split('/').pop();
230
217
  }
231
218
  }
232
219
  }
@@ -245,7 +232,7 @@ function interceptPasteFromClipspace() {
245
232
  if (!ComfyApp || !ComfyApp.pasteFromClipspace) return;
246
233
 
247
234
  const originalPasteFromClipspace = ComfyApp.pasteFromClipspace;
248
- ComfyApp.pasteFromClipspace = function (node) {
235
+ ComfyApp.pasteFromClipspace = function(node) {
249
236
  // 调用原始函数
250
237
  originalPasteFromClipspace.call(this, node);
251
238
 
@@ -257,23 +244,14 @@ function interceptPasteFromClipspace() {
257
244
 
258
245
  // 1) 如果是 clipspace 路径格式,替换为 OSS URL
259
246
  if (value.includes('clipspace/')) {
260
- // 1.1) 特殊情况:clipspace/https://... 或 clipspace/http://...
261
- // 这种情况是 OSS URL 被错误地加了 clipspace/ 前缀,直接移除前缀
262
- const ossUrlMatch = value.match(/clipspace\/(https?:\/\/[^\s]+)/i);
263
- if (ossUrlMatch) {
264
- // 移除可能的 [input] 或 [output] 后缀
265
- let cleanUrl = ossUrlMatch[1].replace(/\s*\[(input|output)\]$/i, '');
266
- imageWidget.value = cleanUrl;
267
- } else {
268
- // 1.2) 常规情况:clipspace/xxx.png,提取文件名并查找映射
269
- const match = value.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))(\s\[(input|output)\])?/i);
270
- if (match) {
271
- const filename = match[1];
272
- const ossUrl = findOssUrl(filename);
273
-
274
- if (ossUrl) {
275
- imageWidget.value = ossUrl;
276
- }
247
+ // 提取文件名
248
+ const match = value.match(/clipspace\/([\w-]+\.(?:png|jpg|jpeg|webp|gif))(\s\[(input|output)\])?/i);
249
+ if (match) {
250
+ const filename = match[1];
251
+ const ossUrl = findOssUrl(filename);
252
+
253
+ if (ossUrl) {
254
+ imageWidget.value = ossUrl;
277
255
  }
278
256
  }
279
257
  }
@@ -295,31 +273,22 @@ app.registerExtension({
295
273
  async setup() {
296
274
  const originalGraphToPrompt = app.graphToPrompt;
297
275
 
298
- // 在构建 Prompt 之前,先清理所有 widget 的值,去掉多余的后缀和错误的 clipspace 前缀
276
+ // 在构建 Prompt 之前,先清理所有 widget 的值,去掉多余的后缀
299
277
  function sanitizeGraphWidgets(graph) {
300
278
  const nodes = graph?._nodes || [];
301
279
  for (const node of nodes) {
302
280
  if (!node?.widgets) continue;
303
281
  for (const widget of node.widgets) {
304
282
  if (typeof widget?.value === 'string') {
305
- let value = widget.value;
306
- // 先处理 clipspace/https://... 格式
307
- if (value.includes('clipspace/')) {
308
- const ossUrlMatch = value.match(/clipspace\/(https?:\/\/[^\s]+)/i);
309
- if (ossUrlMatch) {
310
- value = ossUrlMatch[1];
311
- }
312
- }
313
- // 再移除类型后缀
314
- widget.value = stripTypeSuffix(value);
283
+ widget.value = stripTypeSuffix(widget.value);
315
284
  }
316
285
  }
317
286
  }
318
287
  }
319
288
 
320
- app.graphToPrompt = async function (...args) {
289
+ app.graphToPrompt = async function(...args) {
321
290
  // 预清理,避免 workflow.widgets_values 和 prompt 输入里包含 [input]/[output]
322
- try { sanitizeGraphWidgets(app.graph); } catch (e) { }
291
+ try { sanitizeGraphWidgets(app.graph); } catch (e) {}
323
292
 
324
293
  const result = await originalGraphToPrompt.apply(this, args);
325
294
 
@@ -1,9 +1,6 @@
1
1
  // 主入口文件,导入所有模块
2
2
  import "./disableComfyWebSocket.js";
3
- import "./hookLoadImage.js";
3
+ import "./hookLoadMedia.js";
4
4
  import "./postEvent.js";
5
5
  import "./handleStyle.js";
6
6
  import "./clipspaceToOss.js";
7
- import "./aspectRatio.js";
8
- import "./imageUpload.js";
9
- import "./limitTimeRange.js";
@@ -17,9 +17,13 @@ const extMap = {
17
17
  "VHS_LoadVideo": '.mp4,.mov,.avi,.mkv,.webm,.flv,.wmv,.m4v',
18
18
  }
19
19
  export function getCookie(name) {
20
- const value = `; ${document.cookie}`;
21
- const parts = value.split(`; ${name}=`);
22
- if (parts.length === 2) return parts.pop().split(';').shift();
20
+ const cookies = document.cookie.split(';');
21
+ for (let cookie of cookies) {
22
+ const [key, value] = cookie.trim().split('=');
23
+ if (key === name) {
24
+ return value;
25
+ }
26
+ }
23
27
  return null;
24
28
  }
25
29
  export const hideWidget = (node, widget_name) => {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bizydraft
3
- Version: 0.2.82.dev20251209023307
3
+ Version: 0.2.83
4
4
  Summary: bizydraft
5
5
  Requires-Dist: loguru
6
6
  Requires-Dist: aiohttp
@@ -3,7 +3,7 @@ bizydraft/block_nodes.py,sha256=Lqn3oSCaGDHR2OICc8a2iRoRCVVK9v1-9MM3r-qIZgA,1092
3
3
  bizydraft/env.py,sha256=VFmGopVL2TGWA6hwxyFhIglCEcQxy6iVvL_raMNd6u4,407
4
4
  bizydraft/hijack_nodes.py,sha256=riHsp4DhU60E60bfXUl8kVqjs1QvQWmx_FsgQAdWTa4,4197
5
5
  bizydraft/hijack_routes.py,sha256=j7i9xAfaWvcu-mSoDISQyEwdERd0y-GFhvY2To7MRTU,5015
6
- bizydraft/oss_utils.py,sha256=_UzbS4e_MW5UynkQd2JI6edfdWxStm-Kbt6k7CvJJGA,16663
6
+ bizydraft/oss_utils.py,sha256=IjWKFZ98KNvLw7bmFuqux5mEG36B8c9eQbQmL_gZo0M,16254
7
7
  bizydraft/patch_handlers.py,sha256=cP6gIYBYghWXI72hBbz3BbQHP1O5IvslS8iNK29MEbI,14159
8
8
  bizydraft/postload.py,sha256=XFElKcmCajT_oO7SVJJBaN04XcWro54N5HB5cSCxfvI,1308
9
9
  bizydraft/prestartup_patch.py,sha256=4FGjmRcDHELjtlQOrfTfk2Un5OS89QIqfq-gEcB9WDs,998
@@ -11,27 +11,24 @@ bizydraft/resp.py,sha256=8INvKOe5Dgai3peKfqKjrhUoYeuXWXn358w30-_cY-A,369
11
11
  bizydraft/server.py,sha256=L2zoJgOisr65IRphOyko74AdsLel59gh55peyMaUrO8,2102
12
12
  bizydraft/workflow_io.py,sha256=MYhJbpgkv8hrA5k_aolijOTrWpTtu62nzRznA4hv8JE,4298
13
13
  bizydraft/static/js/aiAppHandler.js,sha256=OQRhhoqvc8iZeCvHTtdaD2VTYBGzkeAGdCk1UMO2RZs,17525
14
- bizydraft/static/js/aspectRatio.js,sha256=p--C62sTd2QLywa0KpB4T0QI-n7FXv8dO_6Tkl8vtdk,27943
15
- bizydraft/static/js/clipspaceToOss.js,sha256=zTXYzop4Gx0k0NA1MiWUA_J4RKq75tA_wQSroBASCyg,16177
14
+ bizydraft/static/js/clipspaceToOss.js,sha256=brfEPs71Tky5Dnc47UXNEFeFlESDE3kQvUH8ducpIew,14265
16
15
  bizydraft/static/js/disableComfyWebSocket.js,sha256=ZDOVlU3v3EvWpMRkD8s_AnO43vuWojVLLjkF2pNEVrA,1957
17
16
  bizydraft/static/js/freezeModeHandler.js,sha256=SjpHD2nYymR-E13B0YcqkA6e4WycZOVI3c48Ts9qvWE,18027
18
17
  bizydraft/static/js/handleStyle.js,sha256=GPshFVoa70UxLwFQB-kXZldVREE0cEF-BxxIM44ngkc,5436
19
18
  bizydraft/static/js/hookLoadMedia.js,sha256=EYDuitzOFvlNzM_e4IdE0-wXHQfnyCx7KrwzoxVTdr0,6363
20
19
  bizydraft/static/js/hookLoadModel.js,sha256=gY4D77DqVDzpuQZlqgqcUrL0Yho3koyR_lOHLyiqGfc,8204
21
- bizydraft/static/js/imageUpload.js,sha256=S7YY4jaaZq83epWyT5_s4GOCa2vC_-4MwWMg3HVHyCU,4178
22
- bizydraft/static/js/limitTimeRange.js,sha256=Bz-qHOfqUZk0zr8HYfuR3AzCYjUJHUvp8CdYLu0o6Ic,4619
23
- bizydraft/static/js/main.js,sha256=LaIbmXPOsKrXzwsQRaXE7HXXyw_q0FcXX492ik-f0F0,272
20
+ bizydraft/static/js/main.js,sha256=-pMeAG8vkKO6yq9zXCeHI78KezGn0r9AhYm65DuZrVI,188
24
21
  bizydraft/static/js/nodeFocusHandler.js,sha256=24xXbS4Q-GjJdRqf11i-1pBo8MkOJ24F7MHFV44EG6Q,4683
25
22
  bizydraft/static/js/nodeParamsFilter.js,sha256=H7lBB0G8HNqoGhOCH1hNXqPU-rPlrFyTxg_f_JgLEMk,4168
26
23
  bizydraft/static/js/postEvent.js,sha256=_EyA71m5DwGbhHsuf1amKhpLUBxfBWZaMMxViNhuYzU,43481
27
24
  bizydraft/static/js/socket.js,sha256=VE3fTAgEfM0FZhL526Skt7OCRokOa3mzTCAjAomI_tE,2432
28
- bizydraft/static/js/tool.js,sha256=2Hhv2J18OaFZRWmHIlseahjd5Ot7ZVxUPb5z46YeXIo,2928
25
+ bizydraft/static/js/tool.js,sha256=LD7rijhv_gMRsyKiL5Cz1fgUkfAlPu8j4t_BQxkMm80,2981
29
26
  bizydraft/static/js/uploadFile.js,sha256=WvglKzHMeOzDhOH3P-fLcPHxCLbKOJpo4DntoRxeJtI,4908
30
27
  bizydraft/static/js/workflow_io.js,sha256=FWAjncvWhvy-3nN_legD2fpRwgnIncpRLHU5X016a-U,5236
31
28
  bizydraft/static/js/hookLoad/configLoader.js,sha256=R1k0GKdEmv4IIxdT2F-oOI9X9I19ECe77P_1tO3XxgY,2525
32
29
  bizydraft/static/js/hookLoad/media.js,sha256=iX_zoZ0OzLAdYZ3bn5GpzpnwU-YjcBuEozQt-MwNuwk,17936
33
30
  bizydraft/static/js/hookLoad/model.js,sha256=aHvEPt9k3CcrawHSYnQYcvbtTRwIztk-XDRA3lvgXvA,9890
34
- bizydraft-0.2.82.dev20251209023307.dist-info/METADATA,sha256=aD-Dv6wd_rBGaWBYs62q5ts7qtZad8PV0I9dh1k_tik,180
35
- bizydraft-0.2.82.dev20251209023307.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
36
- bizydraft-0.2.82.dev20251209023307.dist-info/top_level.txt,sha256=XtoBq6hjZhXIM7aas4GtPDtAiKo8FdLzMABXW8qqQ8M,10
37
- bizydraft-0.2.82.dev20251209023307.dist-info/RECORD,,
31
+ bizydraft-0.2.83.dist-info/METADATA,sha256=QC6ISi0gY7fml1L4fyoaPJzj7U8L1PCnsnrnEZbhtnI,162
32
+ bizydraft-0.2.83.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ bizydraft-0.2.83.dist-info/top_level.txt,sha256=XtoBq6hjZhXIM7aas4GtPDtAiKo8FdLzMABXW8qqQ8M,10
34
+ bizydraft-0.2.83.dist-info/RECORD,,
@@ -1,751 +0,0 @@
1
- import { app } from "../../scripts/app.js";
2
-
3
- // Seedream 4.5 节点的宽高比和总像素限制
4
- const MIN_ASPECT_RATIO = 1 / 16; // 1/16
5
- const MAX_ASPECT_RATIO = 16; // 16
6
- const MIN_TOTAL_PIXELS = 2560 * 1440; // 3686400
7
- const MAX_TOTAL_PIXELS = 4096 * 4096; // 16777216
8
-
9
- /**
10
- * 验证所有约束是否满足
11
- * @param {number} width - 宽度
12
- * @param {number} height - 高度
13
- * @returns {boolean} 是否满足所有约束
14
- */
15
- function satisfiesAllConstraints(width, height) {
16
- const totalPixels = width * height;
17
- const aspectRatio = width / height;
18
-
19
- return (
20
- totalPixels >= MIN_TOTAL_PIXELS &&
21
- totalPixels <= MAX_TOTAL_PIXELS &&
22
- aspectRatio >= MIN_ASPECT_RATIO &&
23
- aspectRatio <= MAX_ASPECT_RATIO &&
24
- width >= 480 &&
25
- width <= 4096 &&
26
- height >= 480 &&
27
- height <= 4096
28
- );
29
- }
30
-
31
- /**
32
- * 计算给定宽度时,高度的合法范围
33
- * @param {number} width - 宽度
34
- * @returns {{min: number, max: number}} 高度的最小值和最大值
35
- */
36
- function calculateHeightRange(width) {
37
- // 来自总像素的限制
38
- const minHeightByPixel = MIN_TOTAL_PIXELS / width;
39
- const maxHeightByPixel = MAX_TOTAL_PIXELS / width;
40
-
41
- // 来自宽高比的限制
42
- // width/height <= 16 => height >= width/16
43
- // width/height >= 1/16 => height <= width*16
44
- const minHeightByRatio = width / MAX_ASPECT_RATIO;
45
- const maxHeightByRatio = width * MAX_ASPECT_RATIO;
46
-
47
- // 取交集:下限取最大值(最严格),上限取最小值(最严格)
48
- const minHeight = Math.max(
49
- Math.max(minHeightByPixel, minHeightByRatio),
50
- 480 // 单边最小值
51
- );
52
- const maxHeight = Math.min(
53
- Math.min(maxHeightByPixel, maxHeightByRatio),
54
- 4096 // 单边最大值
55
- );
56
-
57
- return { min: minHeight, max: maxHeight };
58
- }
59
-
60
- /**
61
- * 计算给定高度时,宽度的合法范围
62
- * @param {number} height - 高度
63
- * @returns {{min: number, max: number}} 宽度的最小值和最大值
64
- */
65
- function calculateWidthRange(height) {
66
- // 来自总像素的限制
67
- const minWidthByPixel = MIN_TOTAL_PIXELS / height;
68
- const maxWidthByPixel = MAX_TOTAL_PIXELS / height;
69
-
70
- // 来自宽高比的限制
71
- // width/height >= 1/16 => width >= height/16
72
- // width/height <= 16 => width <= height*16
73
- const minWidthByRatio = height * MIN_ASPECT_RATIO;
74
- const maxWidthByRatio = height * MAX_ASPECT_RATIO;
75
-
76
- // 取交集
77
- const minWidth = Math.max(Math.max(minWidthByPixel, minWidthByRatio), 480);
78
- const maxWidth = Math.min(Math.min(maxWidthByPixel, maxWidthByRatio), 4096);
79
-
80
- return { min: minWidth, max: maxWidth };
81
- }
82
-
83
- /**
84
- * 根据改变的维度和当前另一个维度,计算满足所有约束的新尺寸
85
- * 使用橡皮筋算法(Clamp Algorithm):尽量保持原宽高比,超出范围则截断
86
- * @param {number} changedValue - 改变的维度值(宽或高)
87
- * @param {number} otherValue - 另一个维度的当前值
88
- * @param {boolean} isWidthChanged - 是否是宽度改变
89
- * @param {number} oldValue - 改变前的维度值(用于计算宽高比),如果未提供则使用 otherValue 作为参考
90
- * @returns {{width: number, height: number}} 计算后的宽高值
91
- */
92
- function calculateNewDimensions(
93
- changedValue,
94
- otherValue,
95
- isWidthChanged,
96
- oldValue = null
97
- ) {
98
- // 如果另一个值为0或无效,使用默认值
99
- if (!otherValue || otherValue === 0) {
100
- otherValue = changedValue;
101
- }
102
-
103
- // 第一步:先限制改变的维度本身在合法范围内
104
- let newWidth, newHeight;
105
- if (isWidthChanged) {
106
- // 宽度改变,先限制宽度在合法范围内
107
- newWidth = Math.max(480, Math.min(4096, changedValue));
108
-
109
- // 计算高度的合法范围
110
- const heightRange = calculateHeightRange(newWidth);
111
-
112
- // 如果合法范围无效(下限大于上限),说明宽度本身不合法,需要调整宽度
113
- if (heightRange.min > heightRange.max) {
114
- // 宽度太极端,无法找到匹配的高度
115
- // 尝试调整宽度到合理范围
116
- // 计算宽度的合法范围(基于一个合理的高度值,比如2048)
117
- const tempHeight = 2048;
118
- const widthRange = calculateWidthRange(tempHeight);
119
- newWidth = Math.max(widthRange.min, Math.min(widthRange.max, newWidth));
120
-
121
- // 重新计算高度范围
122
- const newHeightRange = calculateHeightRange(newWidth);
123
- if (newHeightRange.min > newHeightRange.max) {
124
- // 仍然无效,使用默认值
125
- newWidth = 2048;
126
- newHeight = 2048;
127
- return { width: Math.round(newWidth), height: Math.round(newHeight) };
128
- }
129
- }
130
-
131
- // 第二步:计算理想高度(保持原宽高比)
132
- // 如果提供了旧宽度,使用旧宽高比:idealHeight = newWidth * (oldHeight / oldWidth)
133
- // 如果没有提供旧宽度,假设用户希望保持 newWidth/otherValue 的比例(即高度不变)
134
- let idealHeight;
135
- if (oldValue !== null && oldValue > 0 && otherValue > 0) {
136
- // 使用旧宽高比计算理想高度
137
- const oldRatio = oldValue / otherValue;
138
- idealHeight = newWidth / oldRatio;
139
- } else {
140
- // 没有旧值,假设保持当前高度(即比例 = newWidth / otherValue)
141
- idealHeight = otherValue;
142
- }
143
-
144
- // 第三步:将理想高度"截断"到合法范围(橡皮筋逻辑)
145
- newHeight = Math.max(
146
- heightRange.min,
147
- Math.min(heightRange.max, idealHeight)
148
- );
149
- newHeight = Math.round(newHeight);
150
-
151
- // 第四步:如果高度被截断,可能需要微调宽度以更好地满足约束
152
- // 但为了保持"尽量不动a"的原则,这里只做轻微调整
153
- // 重新计算宽度范围(基于调整后的高度)
154
- const finalWidthRange = calculateWidthRange(newHeight);
155
- newWidth = Math.max(
156
- finalWidthRange.min,
157
- Math.min(finalWidthRange.max, newWidth)
158
- );
159
- newWidth = Math.round(newWidth);
160
- } else {
161
- // 高度改变,先限制高度在合法范围内
162
- newHeight = Math.max(480, Math.min(4096, changedValue));
163
-
164
- // 计算宽度的合法范围
165
- const widthRange = calculateWidthRange(newHeight);
166
-
167
- // 如果合法范围无效,需要调整高度
168
- if (widthRange.min > widthRange.max) {
169
- const tempWidth = 2048;
170
- const heightRange = calculateHeightRange(tempWidth);
171
- newHeight = Math.max(
172
- heightRange.min,
173
- Math.min(heightRange.max, newHeight)
174
- );
175
-
176
- const newWidthRange = calculateWidthRange(newHeight);
177
- if (newWidthRange.min > newWidthRange.max) {
178
- newWidth = 2048;
179
- newHeight = 2048;
180
- return { width: Math.round(newWidth), height: Math.round(newHeight) };
181
- }
182
- }
183
-
184
- // 计算理想宽度(保持原宽高比)
185
- // 如果提供了旧高度,使用旧宽高比:idealWidth = newHeight * (oldWidth / oldHeight)
186
- // 如果没有提供旧高度,假设用户希望保持 otherValue/newHeight 的比例(即宽度不变)
187
- let idealWidth;
188
- if (oldValue !== null && oldValue > 0 && otherValue > 0) {
189
- // 使用旧宽高比计算理想宽度
190
- const oldRatio = otherValue / oldValue;
191
- idealWidth = newHeight * oldRatio;
192
- } else {
193
- // 没有旧值,假设保持当前宽度(即比例 = otherValue / newHeight)
194
- idealWidth = otherValue;
195
- }
196
-
197
- // 将理想宽度截断到合法范围
198
- newWidth = Math.max(widthRange.min, Math.min(widthRange.max, idealWidth));
199
- newWidth = Math.round(newWidth);
200
-
201
- // 微调高度
202
- const finalHeightRange = calculateHeightRange(newWidth);
203
- newHeight = Math.max(
204
- finalHeightRange.min,
205
- Math.min(finalHeightRange.max, newHeight)
206
- );
207
- newHeight = Math.round(newHeight);
208
- }
209
-
210
- // 最终确保在合理范围内(双重保险)
211
- newWidth = Math.max(480, Math.min(4096, newWidth));
212
- newHeight = Math.max(480, Math.min(4096, newHeight));
213
-
214
- return { width: newWidth, height: newHeight };
215
- }
216
-
217
- /**
218
- * 验证并调整宽高值,确保满足所有约束(用于size切换到Custom时的验证)
219
- * 使用橡皮筋算法:尽量保持原宽高比,超出范围则截断
220
- * @param {number} width - 宽度
221
- * @param {number} height - 高度
222
- * @returns {{width: number, height: number}} 调整后的宽高
223
- */
224
- function validateAndAdjustDimensions(width, height) {
225
- // 如果已经满足所有约束,直接返回
226
- if (satisfiesAllConstraints(width, height)) {
227
- return { width: Math.round(width), height: Math.round(height) };
228
- }
229
-
230
- // 保存原始宽高比(尽量保持)
231
- const originalRatio = width / height;
232
-
233
- // 第一步:限制单边尺寸范围(480-4096)
234
- width = Math.max(480, Math.min(4096, width));
235
- height = Math.max(480, Math.min(4096, height));
236
-
237
- // 第二步:调整宽高比到合法范围(如果超出)
238
- let aspectRatio = width / height;
239
- if (aspectRatio < MIN_ASPECT_RATIO) {
240
- // 宽高比太小,调整宽度
241
- width = Math.round(height * MIN_ASPECT_RATIO);
242
- width = Math.max(480, Math.min(4096, width));
243
- } else if (aspectRatio > MAX_ASPECT_RATIO) {
244
- // 宽高比太大,调整宽度
245
- width = Math.round(height * MAX_ASPECT_RATIO);
246
- width = Math.max(480, Math.min(4096, width));
247
- }
248
-
249
- // 第三步:调整总像素到合法范围(保持当前宽高比)
250
- let totalPixels = width * height;
251
- if (totalPixels < MIN_TOTAL_PIXELS) {
252
- // 总像素太小,按比例放大
253
- const scale = Math.sqrt(MIN_TOTAL_PIXELS / totalPixels);
254
- const newWidth = Math.round(width * scale);
255
- const newHeight = Math.round(height * scale);
256
-
257
- // 检查放大后是否超出单边限制
258
- if (newWidth <= 4096 && newHeight <= 4096) {
259
- width = newWidth;
260
- height = newHeight;
261
- } else {
262
- // 超出限制,调整到最大可能的尺寸(保持宽高比)
263
- aspectRatio = width / height;
264
- if (aspectRatio >= MIN_ASPECT_RATIO && aspectRatio <= MAX_ASPECT_RATIO) {
265
- if (width > height) {
266
- width = 4096;
267
- height = Math.round(4096 / aspectRatio);
268
- height = Math.max(480, Math.min(4096, height));
269
- } else {
270
- height = 4096;
271
- width = Math.round(4096 * aspectRatio);
272
- width = Math.max(480, Math.min(4096, width));
273
- }
274
- }
275
- }
276
- } else if (totalPixels > MAX_TOTAL_PIXELS) {
277
- // 总像素太大,按比例缩小
278
- const scale = Math.sqrt(MAX_TOTAL_PIXELS / totalPixels);
279
- width = Math.round(width * scale);
280
- height = Math.round(height * scale);
281
- }
282
-
283
- // 第四步:最终验证和微调(最多一次回退检查)
284
- // 重新计算当前值
285
- width = Math.max(480, Math.min(4096, width));
286
- height = Math.max(480, Math.min(4096, height));
287
- aspectRatio = width / height;
288
- totalPixels = width * height;
289
-
290
- // 如果宽高比仍然不合法,强制调整
291
- if (aspectRatio < MIN_ASPECT_RATIO) {
292
- width = Math.round(height * MIN_ASPECT_RATIO);
293
- if (width > 4096) {
294
- width = 4096;
295
- height = Math.round(4096 / MIN_ASPECT_RATIO);
296
- if (height > 4096) {
297
- height = 4096;
298
- width = Math.round(4096 * MIN_ASPECT_RATIO);
299
- }
300
- }
301
- totalPixels = width * height;
302
- if (totalPixels > MAX_TOTAL_PIXELS) {
303
- const scale = Math.sqrt(MAX_TOTAL_PIXELS / totalPixels);
304
- width = Math.round(width * scale);
305
- height = Math.round(height * scale);
306
- }
307
- } else if (aspectRatio > MAX_ASPECT_RATIO) {
308
- width = Math.round(height * MAX_ASPECT_RATIO);
309
- if (width > 4096) {
310
- width = 4096;
311
- height = Math.round(4096 / MAX_ASPECT_RATIO);
312
- if (height > 4096) {
313
- height = 4096;
314
- width = Math.round(4096 * MAX_ASPECT_RATIO);
315
- }
316
- }
317
- totalPixels = width * height;
318
- if (totalPixels > MAX_TOTAL_PIXELS) {
319
- const scale = Math.sqrt(MAX_TOTAL_PIXELS / totalPixels);
320
- width = Math.round(width * scale);
321
- height = Math.round(height * scale);
322
- }
323
- }
324
-
325
- // 最终确保在合理范围内
326
- width = Math.max(480, Math.min(4096, width));
327
- height = Math.max(480, Math.min(4096, height));
328
-
329
- return { width: Math.round(width), height: Math.round(height) };
330
- }
331
-
332
- // 简单的链式callback实现(类似useChainCallback)
333
- function chainCallback(originalCallback, newCallback) {
334
- return function (...args) {
335
- if (originalCallback) {
336
- originalCallback.apply(this, args);
337
- }
338
- newCallback.apply(this, args);
339
- };
340
- }
341
-
342
- app.registerExtension({
343
- name: "bizyair.seedream45.aspectRatio",
344
- nodeCreated(node, app) {
345
- // 只处理 Seedream 4.5 节点
346
- if (node.title !== "☁️BizyAir Seedream 4.5") {
347
- return;
348
- }
349
-
350
- // 延迟执行,确保widget完全初始化,并且ComfyUI的setupNodeWidgetCallbacks已经执行
351
- setTimeout(() => {
352
- try {
353
- // 查找 width、height 和 size widget
354
- const widthWidget = node.widgets?.find(
355
- (w) => w.name === "custom_width"
356
- );
357
- const heightWidget = node.widgets?.find(
358
- (w) => w.name === "custom_height"
359
- );
360
- const sizeWidget = node.widgets?.find((w) => w.name === "size");
361
-
362
- if (!widthWidget || !heightWidget) {
363
- console.warn(
364
- "[Seedream4.5] 未找到 custom_width 或 custom_height widget"
365
- );
366
- return;
367
- }
368
-
369
- console.log("[Seedream4.5] 节点创建,开始设置宽高比联动", {
370
- nodeId: node.id,
371
- nodeType: node.type,
372
- sizeWidget: sizeWidget,
373
- sizeValue: sizeWidget?.value,
374
- sizeType: sizeWidget?.type,
375
- sizeOptions: sizeWidget?.options,
376
- sizeOptionsValues: sizeWidget?.options?.values,
377
- widthValue: widthWidget.value,
378
- heightValue: heightWidget.value,
379
- widthCallback: widthWidget.callback,
380
- heightCallback: heightWidget.callback,
381
- });
382
-
383
- // 标记是否正在更新,避免循环触发
384
- let isUpdating = false;
385
-
386
- // 保存当前的 callback(可能已经被ComfyUI包装过)
387
- const currentWidthCallback = widthWidget.callback;
388
- const currentHeightCallback = heightWidget.callback;
389
-
390
- // 检查 size 是否为 "Custom",只有 Custom 模式才需要联动
391
- // 实时获取size widget的值,而不是在闭包中捕获
392
- const isCustomSize = () => {
393
- if (!sizeWidget) {
394
- console.log("[Seedream4.5] sizeWidget不存在");
395
- return false;
396
- }
397
- const sizeValue = sizeWidget.value;
398
-
399
- // 获取size widget的所有选项值
400
- const sizeOptions = sizeWidget.options?.values || [];
401
- const hasCustomOption = sizeOptions.some((opt) => {
402
- const optValue = typeof opt === "string" ? opt : opt?.value || opt;
403
- return (
404
- optValue === "Custom" ||
405
- optValue === "custom" ||
406
- optValue === "CUSTOM"
407
- );
408
- });
409
-
410
- // 打印sizeOptions的详细信息
411
- const sizeOptionsDetails = sizeOptions.map((opt, idx) => {
412
- const optValue = typeof opt === "string" ? opt : opt?.value || opt;
413
- const optLabel =
414
- typeof opt === "string" ? opt : opt?.label || opt?.value || opt;
415
- return { index: idx, value: optValue, label: optLabel, raw: opt };
416
- });
417
-
418
- // 查找Custom选项的实际值
419
- let customOptionValue = null;
420
- sizeOptions.forEach((opt) => {
421
- const optValue = typeof opt === "string" ? opt : opt?.value || opt;
422
- const optLabel =
423
- typeof opt === "string" ? opt : opt?.label || opt?.value || opt;
424
- if (
425
- optValue === "Custom" ||
426
- optValue === "custom" ||
427
- optValue === "CUSTOM" ||
428
- optLabel === "Custom" ||
429
- optLabel === "custom" ||
430
- optLabel === "CUSTOM" ||
431
- String(optValue).toLowerCase().includes("custom") ||
432
- String(optLabel).toLowerCase().includes("custom")
433
- ) {
434
- customOptionValue = optValue;
435
- }
436
- });
437
-
438
- console.log("[Seedream4.5] 检查isCustomSize", {
439
- sizeValue,
440
- type: typeof sizeValue,
441
- sizeOptions: sizeOptions,
442
- sizeOptionsDetails: sizeOptionsDetails,
443
- customOptionValue: customOptionValue,
444
- hasCustomOption,
445
- equalsCustom:
446
- sizeValue === "Custom" ||
447
- sizeValue === "custom" ||
448
- sizeValue === "CUSTOM" ||
449
- sizeValue === customOptionValue,
450
- sizeWidget: sizeWidget,
451
- });
452
-
453
- // 检查多种可能的"Custom"值
454
- // 如果size widget有Custom选项,检查当前值是否等于Custom(包括找到的实际值)
455
- if (hasCustomOption) {
456
- const isCustom =
457
- sizeValue === "Custom" ||
458
- sizeValue === "custom" ||
459
- sizeValue === "CUSTOM" ||
460
- (customOptionValue && sizeValue === customOptionValue);
461
-
462
- console.log("[Seedream4.5] Custom检查结果", {
463
- sizeValue,
464
- customOptionValue,
465
- isCustom,
466
- checks: {
467
- equalsCustom: sizeValue === "Custom",
468
- equalsLowercase: sizeValue === "custom",
469
- equalsUppercase: sizeValue === "CUSTOM",
470
- equalsFoundValue:
471
- customOptionValue && sizeValue === customOptionValue,
472
- },
473
- });
474
-
475
- return isCustom;
476
- }
477
-
478
- // 如果没有Custom选项,但当前值不在预设选项中,也认为是Custom模式
479
- // 这适用于动态选项的情况
480
- const isPresetValue = sizeOptions.some((opt) => {
481
- const optValue = typeof opt === "string" ? opt : opt?.value || opt;
482
- return optValue === sizeValue;
483
- });
484
-
485
- return !isPresetValue;
486
- };
487
-
488
- // 处理宽度变化的函数
489
- const handleWidthChange = (newValue) => {
490
- const customSize = isCustomSize();
491
- console.log("[Seedream4.5] handleWidthChange 被调用", {
492
- newValue,
493
- isCustomSize: customSize,
494
- isUpdating,
495
- sizeWidgetValue: sizeWidget?.value,
496
- sizeWidgetExists: !!sizeWidget,
497
- });
498
-
499
- if (!customSize || isUpdating) {
500
- console.log("[Seedream4.5] 跳过处理", {
501
- reason: !customSize ? "size不是Custom" : "正在更新中",
502
- });
503
- return;
504
- }
505
-
506
- isUpdating = true;
507
- try {
508
- const numValue = Number(newValue);
509
- if (isNaN(numValue)) {
510
- return;
511
- }
512
-
513
- const currentHeight = Number(heightWidget.value) || 2048;
514
- // 获取旧宽度:在回调触发时,widthWidget.value 可能还是旧值(ComfyUI通常在callback中先调用callback再更新value)
515
- // 但如果已经更新了,我们使用传入的 value 作为新值,尝试从 widget.value 获取旧值
516
- // 如果 widget.value 等于新值,说明已经更新了,我们无法获取真正的旧值,使用新值作为参考
517
- let oldWidth = Number(widthWidget.value) || 2048;
518
- if (Math.abs(oldWidth - numValue) < 0.5) {
519
- // widget.value 已经等于新值,说明值已经更新
520
- // 在这种情况下,我们无法获取真正的旧值,使用新值作为参考(会假设保持当前比例)
521
- oldWidth = numValue;
522
- }
523
- // 使用新的计算函数,同时考虑总像素和宽高比约束
524
- // 传入旧宽度以正确计算宽高比
525
- const adjusted = calculateNewDimensions(
526
- numValue,
527
- currentHeight,
528
- true,
529
- oldWidth
530
- );
531
-
532
- console.log("[Seedream4.5] 计算完成", {
533
- width: numValue,
534
- currentHeight,
535
- adjustedHeight: adjusted.height,
536
- adjusted,
537
- heightDiff: Math.abs(adjusted.height - currentHeight),
538
- widthDiff: Math.abs(adjusted.width - numValue),
539
- willUpdateHeight: Math.abs(adjusted.height - currentHeight) > 0.5,
540
- willUpdateWidth: Math.abs(adjusted.width - numValue) > 0.5,
541
- });
542
-
543
- if (Math.abs(adjusted.height - currentHeight) > 0.5) {
544
- console.log("[Seedream4.5] 更新高度", {
545
- from: currentHeight,
546
- to: adjusted.height,
547
- beforeUpdate: heightWidget.value,
548
- });
549
- // 直接设置值并调用callback
550
- heightWidget.value = adjusted.height;
551
- console.log("[Seedream4.5] 高度值已更新", {
552
- afterUpdate: heightWidget.value,
553
- });
554
- if (currentHeightCallback) {
555
- currentHeightCallback.call(heightWidget, adjusted.height);
556
- console.log("[Seedream4.5] 高度callback已调用");
557
- }
558
- } else {
559
- console.log("[Seedream4.5] 跳过高度更新", {
560
- reason: "高度差值太小",
561
- currentHeight,
562
- adjustedHeight: adjusted.height,
563
- diff: Math.abs(adjusted.height - currentHeight),
564
- });
565
- }
566
-
567
- if (Math.abs(adjusted.width - numValue) > 0.5) {
568
- console.log("[Seedream4.5] 更新宽度", {
569
- from: numValue,
570
- to: adjusted.width,
571
- beforeUpdate: widthWidget.value,
572
- });
573
- widthWidget.value = adjusted.width;
574
- console.log("[Seedream4.5] 宽度值已更新", {
575
- afterUpdate: widthWidget.value,
576
- });
577
- if (currentWidthCallback) {
578
- currentWidthCallback.call(widthWidget, adjusted.width);
579
- console.log("[Seedream4.5] 宽度callback已调用");
580
- }
581
- } else {
582
- console.log("[Seedream4.5] 跳过宽度更新", {
583
- reason: "宽度差值太小",
584
- numValue,
585
- adjustedWidth: adjusted.width,
586
- diff: Math.abs(adjusted.width - numValue),
587
- });
588
- }
589
-
590
- if (node && node.setDirtyCanvas) {
591
- node.setDirtyCanvas(true, true);
592
- }
593
- } catch (error) {
594
- console.error("[Seedream4.5] 更新宽度时出错:", error);
595
- } finally {
596
- isUpdating = false;
597
- }
598
- };
599
-
600
- // 处理高度变化的函数
601
- const handleHeightChange = (newValue) => {
602
- const customSize = isCustomSize();
603
- console.log("[Seedream4.5] handleHeightChange 被调用", {
604
- newValue,
605
- isCustomSize: customSize,
606
- isUpdating,
607
- sizeWidgetValue: sizeWidget?.value,
608
- sizeWidgetExists: !!sizeWidget,
609
- });
610
-
611
- if (!customSize || isUpdating) {
612
- console.log("[Seedream4.5] 跳过处理", {
613
- reason: !customSize ? "size不是Custom" : "正在更新中",
614
- });
615
- return;
616
- }
617
-
618
- isUpdating = true;
619
- try {
620
- const numValue = Number(newValue);
621
- if (isNaN(numValue)) {
622
- return;
623
- }
624
-
625
- const currentWidth = Number(widthWidget.value) || 2048;
626
- // 获取旧高度:类似宽度的处理
627
- let oldHeight = Number(heightWidget.value) || 2048;
628
- if (Math.abs(oldHeight - numValue) < 0.5) {
629
- // widget.value 已经等于新值,说明值已经更新
630
- // 使用新值作为参考(会假设保持当前比例)
631
- oldHeight = numValue;
632
- }
633
- // 使用新的计算函数,同时考虑总像素和宽高比约束
634
- // 传入旧高度以正确计算宽高比
635
- const adjusted = calculateNewDimensions(
636
- numValue,
637
- currentWidth,
638
- false,
639
- oldHeight
640
- );
641
-
642
- console.log("[Seedream4.5] 计算完成", {
643
- height: numValue,
644
- currentWidth,
645
- adjustedWidth: adjusted.width,
646
- adjusted,
647
- });
648
-
649
- if (Math.abs(adjusted.width - currentWidth) > 0.5) {
650
- console.log("[Seedream4.5] 更新宽度", {
651
- from: currentWidth,
652
- to: adjusted.width,
653
- });
654
- widthWidget.value = adjusted.width;
655
- if (currentWidthCallback) {
656
- currentWidthCallback.call(widthWidget, adjusted.width);
657
- }
658
- }
659
-
660
- if (Math.abs(adjusted.height - numValue) > 0.5) {
661
- console.log("[Seedream4.5] 更新高度", {
662
- from: numValue,
663
- to: adjusted.height,
664
- });
665
- heightWidget.value = adjusted.height;
666
- if (currentHeightCallback) {
667
- currentHeightCallback.call(heightWidget, adjusted.height);
668
- }
669
- }
670
-
671
- if (node && node.setDirtyCanvas) {
672
- node.setDirtyCanvas(true, true);
673
- }
674
- } catch (error) {
675
- console.error("[Seedream4.5] 更新高度时出错:", error);
676
- } finally {
677
- isUpdating = false;
678
- }
679
- };
680
-
681
- // 使用链式callback包装width widget的callback
682
- widthWidget.callback = chainCallback(
683
- currentWidthCallback,
684
- function (value) {
685
- console.log("[Seedream4.5] width callback 被调用", {
686
- value,
687
- widgetValue: widthWidget.value,
688
- });
689
- // 在回调中,value 是新值,widthWidget.value 可能还是旧值(取决于ComfyUI的实现)
690
- // 为了安全,我们使用 value 作为新值,widthWidget.value 作为旧值的参考
691
- handleWidthChange(value);
692
- }
693
- );
694
-
695
- // 使用链式callback包装height widget的callback
696
- heightWidget.callback = chainCallback(
697
- currentHeightCallback,
698
- function (value) {
699
- console.log("[Seedream4.5] height callback 被调用", {
700
- value,
701
- widgetValue: heightWidget.value,
702
- });
703
- handleHeightChange(value);
704
- }
705
- );
706
-
707
- // 监听 size widget 的变化,当切换到 Custom 时,重新验证宽高
708
- if (sizeWidget) {
709
- const originalSizeCallback = sizeWidget.callback;
710
- sizeWidget.callback = chainCallback(
711
- originalSizeCallback,
712
- function (value) {
713
- if (value === "Custom") {
714
- const currentWidth = Number(widthWidget.value) || 2048;
715
- const currentHeight = Number(heightWidget.value) || 2048;
716
- const adjusted = validateAndAdjustDimensions(
717
- currentWidth,
718
- currentHeight
719
- );
720
-
721
- if (
722
- Math.abs(adjusted.width - currentWidth) > 0.5 ||
723
- Math.abs(adjusted.height - currentHeight) > 0.5
724
- ) {
725
- isUpdating = true;
726
- try {
727
- widthWidget.value = adjusted.width;
728
- heightWidget.value = adjusted.height;
729
- if (currentWidthCallback) {
730
- currentWidthCallback.call(widthWidget, adjusted.width);
731
- }
732
- if (currentHeightCallback) {
733
- currentHeightCallback.call(heightWidget, adjusted.height);
734
- }
735
- if (node && node.setDirtyCanvas) {
736
- node.setDirtyCanvas(true, true);
737
- }
738
- } finally {
739
- isUpdating = false;
740
- }
741
- }
742
- }
743
- }
744
- );
745
- }
746
- } catch (error) {
747
- console.error("[Seedream4.5] 初始化宽高比联动失败:", error);
748
- }
749
- }, 200); // 增加延迟,确保ComfyUI的setupNodeWidgetCallbacks已经执行
750
- },
751
- });
@@ -1,146 +0,0 @@
1
- import { app } from "../../../scripts/app.js";
2
-
3
- // 简单的链式callback实现
4
- function chainCallback(originalCallback, newCallback) {
5
- return function (...args) {
6
- if (originalCallback) {
7
- originalCallback.apply(this, args);
8
- }
9
- newCallback.apply(this, args);
10
- };
11
- }
12
-
13
- // 统一的函数:初始化动态输入相关的 widgets
14
- function initializeDynamicInputs(node) {
15
- // 只处理目标节点类型
16
- if (
17
- node.type !== "BizyAir_Seedream4_5" &&
18
- node.type !== "BizyAir_NanoBananaPro" &&
19
- node.type !== "BizyAir_NanoBananaProOfficial"
20
- ) {
21
- return;
22
- }
23
-
24
- node._type = "IMAGE";
25
-
26
- if (!node.inputs) {
27
- node.inputs = [];
28
- }
29
-
30
- // 先检查是否存在 inputcount widget
31
- let inputCountWidget = node.widgets?.find(
32
- (w) => w.name === "inputcount"
33
- );
34
-
35
- // 如果不存在,先创建 inputcount widget(在按钮之前创建,确保显示顺序)
36
- if (!inputCountWidget) {
37
- // 计算当前 IMAGE 类型输入数量
38
- const currentImageInputs = node.inputs.filter(
39
- (input) => input.type === node._type
40
- ).length;
41
- // 创建 inputcount widget,默认值为当前输入数量或1
42
- // 使用 precision: 0 确保是整数类型
43
- inputCountWidget = node.addWidget(
44
- "number",
45
- "inputcount",
46
- currentImageInputs || 1,
47
- null,
48
- {
49
- min: 1,
50
- max: 10,
51
- step: 10, // 旧参数(兼容性),是 step2 的 10 倍
52
- step2: 1, // 新参数,实际的步长
53
- precision: 0, // 设置为 0 表示整数
54
- }
55
- );
56
- }
57
-
58
- // 检查是否已经存在 "Update inputs" 按钮,避免重复添加
59
- const updateButton = node.widgets?.find(
60
- (w) => w.name === "Update inputs"
61
- );
62
- if (updateButton) {
63
- return; // 已经存在,不需要重复添加
64
- }
65
-
66
- // 然后添加 "Update inputs" 按钮(会在 inputcount 之后显示)
67
- node.addWidget("button", "Update inputs", null, () => {
68
- if (!node.inputs) {
69
- node.inputs = [];
70
- }
71
-
72
- // 查找 inputcount widget
73
- const inputCountWidget = node.widgets?.find(
74
- (w) => w.name === "inputcount"
75
- );
76
-
77
- // 如果仍然找不到,创建它
78
- if (!inputCountWidget) {
79
- const currentImageInputs = node.inputs.filter(
80
- (input) => input.type === node._type
81
- ).length;
82
- node.addWidget(
83
- "number",
84
- "inputcount",
85
- currentImageInputs || 1,
86
- null,
87
- {
88
- min: 1,
89
- max: 10,
90
- step: 10, // 旧参数(兼容性),是 step2 的 10 倍
91
- step2: 1, // 新参数,实际的步长
92
- precision: 0, // 设置为 0 表示整数
93
- }
94
- );
95
- return;
96
- }
97
-
98
- const target_number_of_inputs = inputCountWidget.value || 1;
99
- const num_inputs = node.inputs.filter(
100
- (input) => input.type === node._type
101
- ).length;
102
-
103
- if (target_number_of_inputs === num_inputs) {
104
- return; // already set, do nothing
105
- }
106
-
107
- if (target_number_of_inputs < num_inputs) {
108
- const inputs_to_remove = num_inputs - target_number_of_inputs;
109
- for (let i = 0; i < inputs_to_remove; i++) {
110
- node.removeInput(node.inputs.length - 1);
111
- }
112
- } else {
113
- for (let i = num_inputs + 1; i <= target_number_of_inputs; ++i) {
114
- node.addInput(`image_${i}`, node._type, { shape: 7 });
115
- }
116
- }
117
- });
118
- }
119
-
120
- app.registerExtension({
121
- name: "bizyair.dynamicImageInputs",
122
- async beforeRegisterNodeDef(nodeType, nodeData, app) {
123
- // 只处理目标节点类型
124
- if (
125
- nodeData.name !== "BizyAir_Seedream4_5" &&
126
- nodeData.name !== "BizyAir_NanoBananaPro" &&
127
- nodeData.name !== "BizyAir_NanoBananaProOfficial"
128
- ) {
129
- return;
130
- }
131
-
132
- // 使用 chainCallback 设置 onConfigure,确保在节点配置完成后也初始化 widgets
133
- const originalOnConfigure = nodeType.prototype.onConfigure;
134
- nodeType.prototype.onConfigure = chainCallback(
135
- originalOnConfigure,
136
- function (info) {
137
- // 在 configure 完成后初始化 widgets
138
- initializeDynamicInputs(this);
139
- }
140
- );
141
- },
142
- nodeCreated(node) {
143
- // 在节点创建时也初始化 widgets(用于首次创建节点时)
144
- initializeDynamicInputs(node);
145
- },
146
- });
@@ -1,129 +0,0 @@
1
- import { app } from "../../../scripts/app.js";
2
-
3
- // 简单的链式callback实现(类似useChainCallback)
4
- function chainCallback(originalCallback, newCallback) {
5
- return function (...args) {
6
- if (originalCallback) {
7
- originalCallback.apply(this, args);
8
- }
9
- newCallback.apply(this, args);
10
- };
11
- }
12
-
13
- app.registerExtension({
14
- name: "bizyair.hailuo.limitTimeRange",
15
- nodeCreated(node, app) {
16
- console.log(node.title);
17
- // 只处理 BizyAir_Hailuo2_3_T2V 节点
18
- if (
19
- node.title !== "☁️BizyAir Hailuo2.3 Image To Video" &&
20
- node.title !== "☁️BizyAir Hailuo2.3 Text To Video"
21
- ) {
22
- return;
23
- }
24
- console.log(node.title , `开始处理`);
25
- // 延迟执行,确保 widget 完全初始化
26
- setTimeout(() => {
27
- try {
28
- const resolutionWidget = node.widgets?.find(
29
- (w) => w.name === "resolution"
30
- );
31
- const durationWidget = node.widgets?.find((w) => w.name === "duration");
32
-
33
- if (!resolutionWidget || !durationWidget) {
34
- console.warn("[Hailuo2_3_T2V] 未找到 resolution 或 duration widget");
35
- return;
36
- }
37
-
38
- // 保存 duration widget 的原始选项值
39
- const originalDurationValues = durationWidget.options?.values
40
- ? Array.isArray(durationWidget.options.values)
41
- ? [...durationWidget.options.values]
42
- : typeof durationWidget.options.values === "function"
43
- ? durationWidget.options.values()
44
- : []
45
- : [];
46
-
47
- // 如果没有原始值,无法继续
48
- if (!originalDurationValues || originalDurationValues.length === 0) {
49
- console.warn("[Hailuo2_3_T2V] duration widget 没有可用的选项值");
50
- return;
51
- }
52
-
53
- // 更新 duration 选项的函数
54
- const updateDurationOptions = () => {
55
- const resolutionValue = resolutionWidget.value;
56
- // 支持 "1080p"、"1080P" 等大小写变体
57
- const is1080p =
58
- resolutionValue === "1080p" ||
59
- resolutionValue === "1080P" ||
60
- String(resolutionValue).toUpperCase() === "1080P";
61
-
62
- // 直接修改 widget.options.values 数组
63
- if (!durationWidget.options) {
64
- durationWidget.options = {};
65
- }
66
-
67
- if (is1080p) {
68
- // 如果是 1080p,从 values 数组中删除 10
69
- if (Array.isArray(durationWidget.options.values)) {
70
- // 找到 10 的索引并删除
71
- const index = durationWidget.options.values.indexOf(10);
72
- if (index !== -1) {
73
- durationWidget.options.values.splice(index, 1);
74
- }
75
- } else {
76
- // 如果不是数组,重新创建数组并过滤掉 10
77
- durationWidget.options.values = originalDurationValues.filter(
78
- (val) => val !== 10
79
- );
80
- }
81
- } else {
82
- // 如果不是 1080p,恢复原始值(包括 10)
83
- durationWidget.options.values = [...originalDurationValues];
84
- }
85
-
86
- // 检查当前值是否在新的 values 中,如果不在则调整为第一个可用值
87
- const currentValue = durationWidget.value;
88
- const valuesArray = Array.isArray(durationWidget.options.values)
89
- ? durationWidget.options.values
90
- : [];
91
-
92
- if (valuesArray.length > 0 && !valuesArray.includes(currentValue)) {
93
- const newValue = valuesArray[0];
94
- durationWidget.value = newValue;
95
- // 触发 callback
96
- if (durationWidget.callback) {
97
- durationWidget.callback.call(durationWidget, newValue);
98
- }
99
- }
100
-
101
- // 触发节点和画布更新,确保 UI 刷新
102
- if (node.setDirtyCanvas) {
103
- node.setDirtyCanvas(true, true);
104
- }
105
- if (node.graph && node.graph.setDirtyCanvas) {
106
- node.graph.setDirtyCanvas(true, true);
107
- }
108
- };
109
-
110
- // 保存原始的 resolution callback
111
- const originalResolutionCallback = resolutionWidget.callback;
112
-
113
- // 使用 chainCallback 包装 resolution callback,监听变化
114
- resolutionWidget.callback = chainCallback(
115
- originalResolutionCallback,
116
- function (value) {
117
- // 更新 duration 选项
118
- updateDurationOptions();
119
- }
120
- );
121
-
122
- // 初始化时执行一次,确保选项正确
123
- updateDurationOptions();
124
- } catch (error) {
125
- console.error("[Hailuo2_3_T2V] 初始化时间范围限制失败:", error);
126
- }
127
- }, 200);
128
- },
129
- });