Compare commits

...
186 Commits
Author SHA1 Message Date
yolain 271685698b feat: bump version to 1.4.1 2026-09-05 00:37:55 +08:00
Aleksey Smolenchuk 859af7e7b4 fix: confine save text outputs to output directory (#1032) 2026-09-05 00:13:34 +08:00
yolain 8b522f121d fix: add missing permissions for contents in publish workflow 2026-09-03 15:58:31 +08:00
yolain 5b469409bc feat: bump version 1.4.0 with new features and bug fixes 2026-09-03 15:34:58 +08:00
yolain 80e1261b1c fix: prevent EasyUse from preempting prompt errors
Stop wrapping ComfyUI's queuePrompt so disconnected nodes, invalid connections, and validation failures remain under native frontend error handling. Move global seed metadata injection to workflow generation.
2026-09-03 01:31:00 +08:00
御坂桜 005c57839c fix: use native VAEDecodeTiled for tiled decode (Qwen Image VAE support) (#1028) 2026-09-01 11:21:44 +08:00
Tai An cf15032ab6 fix(pixart): use pe_interpolation instead of the removed lewei_scale in the ControlNet wrappers (#1030)
ControlPixArtHalf.forward_c and ControlPixArtMSHalf.forward_raw still use the
pre-rename name lewei_scale, both for the attribute and for the
get_2d_sincos_pos_embed keyword. Neither exists any more, so both PixArt
ControlNet targets fail on the first forward pass.

Signed-off-by: Anai-Guo <antai12232931@outlook.com>
2026-09-01 11:21:08 +08:00
Tai An ca93381de8 fix(preSampling): make vae/pixels optional on samplerCustomSettings.ip2p (#1029)
The latent-only call site passes neither vae nor pixels, so the IP2P
guider raises TypeError whenever a latent is supplied instead of an
image. samplerFull.ip2p in samplers.py already has the optional form.
2026-09-01 11:19:56 +08:00
max-russellandmaxru 58e077a743 fix(forLoopStart): allow total to be zero, preventing unnecessary loop execution (#1024)
Co-authored-by: maxru <m@m.com>
2026-08-27 03:07:18 +08:00
yolain 4de1ab3b66 fix: connection lines were missing from the "Refresh Nodes" feature in the new comfyui frontend 2026-08-14 21:35:08 +08:00
YseraJYandsherjy 595e0738a9 fix(promptConcat): TypeError when an input is a list (#1019)
Normalize prompt1/prompt2/separator to strings before concatenation,
joining lists with ", ", so outputs from nodes that return lists of
strings (e.g. WD14 tagger pipelines) no longer raise
"can only concatenate list (not str) to list" and the node always
outputs a plain string.

Fixes #993

Co-authored-by: sherjy <23369551+sherjy@users.noreply.github.com>
2026-07-28 18:29:14 +08:00
Joly0 7535cd0dfd Use a dedicated RNG for global seed generation (#1017)
Custom nodes calling random.seed() while they run reset the global RNG,
which makes the generated seeds deterministic and repeat after a workflow
reload or a server restart.
2026-07-28 18:28:23 +08:00
ImpactFramesandClaude Fable 5 960862223b fix(imageDetailTransfer): multi-frame masks crash on channel broadcast (#1015)
The optional mask is resized to (B, H, W) but then passed to
torch.lerp against channels-first (B, C, H, W) tensors. With a
single-frame mask (B=1) broadcasting happens to work, but any
batch/video mask (B>1) raises:

  RuntimeError: The size of tensor a (3) must match the size of
  tensor b (B) at non-singleton dimension 1

Add the missing channel dimension so per-frame video mattes
(e.g. SAM2 alpha over a clip) work.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 11:47:53 +08:00
kannkyo 54d080bf6a Specify UTF-8 encoding for file reads (#1007)
Added UTF-8 encoding to file reads for YAML and JSON files.
2026-06-19 23:07:20 +08:00
yolain 625efbfa2f Fixed an issue where the PrimeVue dialog overlay was not being cleared in a Windows environment #1000 2026-05-30 11:57:49 +08:00
yolain 5618a748c1 Fix: TypeError: object of type 'LockedMeta' has no len() #994 2026-05-28 09:58:17 +08:00
yolain 130c1b5796 fix(math): enhance evaluate_formula to handle list inputs and return results accordingly 2026-04-30 03:24:51 +08:00
yolain 3cf9ab4e63 remove unnecessary print statements 2026-04-23 12:08:38 +08:00
Ralkey ff5e3a34fc fixed detailer not working in subgraphs (#989) 2026-04-21 10:28:59 +08:00
xmarreandxmarre ec4ca6717f Fix Clean VRAM teardown ordering and clear Easy-Use cache in place (#982)
* Remove unused repo metadata and frontend submodule

* Remove repo metadata and frontend submodule

* Add .codex to .gitignore

* Delete .codex

---------

Co-authored-by: xmarre <mmquant1@gmail.com>
2026-04-09 08:12:22 +08:00
yolain b82bb48948 fix: avoid recursive group refs in nodes map 2026-04-08 12:56:30 +08:00
Mark Cockram d08eedabd3 fix(xyplot): fix crashes in Seeds++ Batch and tuple X/Y inputs (#984)
Three bugs in pipeXYPlotAdvanced.plot():

1. X/Y inputs can arrive as tuples in certain ComfyUI configurations,
   causing `AttributeError: 'tuple' object has no attribute 'get'`.
   Added isinstance check to unwrap single-element tuples.

2. Seeds++ Batch used `if new_pipe['seed']:` which:
   - Raises KeyError when 'seed' key is absent
   - Silently skips seed generation when seed is 0 (a valid seed)
   Changed to `new_pipe.get('seed') or 0` for safe fallback.

3. `!= None` → `is not None` per PEP 8.
2026-04-08 12:51:51 +08:00
Z-nonymous 8ba21d0b44 XY plot issue with XY Inputs Lora (#976)
* Fixed the LoRA handling bug in [`py/libs/xyplot.py:365-395`](py/libs/xyplot.py:365).

The original code used an if/else pattern that only processed one LoRA at a time:
```python
xy_values = x_value if self.x_type == "Lora" else y_value
```

This caused issues when both X and Y axes contained LoRAs - only the X axis LoRA was processed, and when only Y had LoRAs, the latent array wasn't populated correctly.

The fix now:
1. Creates an empty `lora_stack` list
2. Adds the X axis LoRA to the stack if `self.x_type == "Lora"`
3. Adds the Y axis LoRA to the stack if `self.y_type == "Lora"`
4. Appends any existing `plot_image_vars['lora_stack']` to the combined stack
5. Applies all LoRAs in sequence

This ensures both X and Y LoRAs are properly combined and applied when both axes contain LoRA values, fixing the `IndexError` in `rearrange_tensors()` that occurred due to mismatched latent array dimensions.

* Fixed: get_labels_and_sample loop structure (lines 588-623)

The IndexError persisted because the latent array length didn't match the expected grid dimensions. The issue is in the nested loop structure of get_labels_and_sample()
When only Y-axis has LoRA values (X is "None"), the nested loops don't generate the correct number of latents:
This results in 0 latents instead of len(y_values) latents, causing the IndexError in rearrange_tensors()

Replaced the nested loop structure with three explicit cases:

X-only variation (self.y_type == 'None'): Iterates over X values only
Y-only variation (self.x_type == 'None'): Iterates over Y values only
Both X and Y variation: Nested iteration over both axes
This ensures the correct number of latents are generated for all scenarios, fixing the IndexError in rearrange_tensors() that occurred when only Y-axis had values (like LoRAs).

* Fix for the LoRA label generation logic in [`py/libs/xyplot.py`](py/libs/xyplot.py:63-69). The changes made to the `define_variable()` method:

1. **Reduced model name truncation** from 30 to 25 characters to leave room for weight information
2. **Changed weight format** from `(0.50)` to ` w:0.50` for better visibility
3. **Only show weight when it differs from default** (1.0) - this ensures weight is displayed for non-default values
4. **Added bounds check** for `len(arr) > 3` before accessing `arr[3]` to prevent potential IndexError

Now when using the same LoRA with different weights (e.g., LoRA A at 0.5 and 1.0), each variation will have a distinct label in the axis, making it clear which weight is being applied in each column/row of the XY plot.
2026-03-31 15:11:24 +08:00
yolain 337a03bb19 Rollback the cycle node to version v1 2026-03-19 18:01:59 +08:00
yolain aef19b8772 Fix NodesMap #969 2026-03-19 14:26:21 +08:00
j2gg0s d60b61d575 fix: use getattr for flipped_img_txt to support newer ComfyUI versions (#968)
ComfyUI removed the `flipped_img_txt` attribute from `DoubleStreamBlock`
in a recent refactor (commit e1add563f, "Use torch RMSNorm for flux
models and refactor hunyuan video code"). This causes an AttributeError
when IPAdapter Flux nodes are executed.

Use `getattr` with a default of `False` (matching the original default)
to maintain compatibility with both old and new ComfyUI versions.
2026-03-13 12:18:51 +08:00
0AA01A0F1 a3f051f0c3 [*] Small Readme corrections (#963) 2026-03-05 11:39:14 +08:00
stuttlepress 8ca6ace667 Fix for loraStack/loraSwitcher/controlnetStack disable erases upstream stack (#962) 2026-03-03 23:45:35 +08:00
yolain 81c510c06e Optimize easy tableEditor display 2026-02-23 17:17:21 +08:00
yolain 7601371923 Add easy tableEditor 2026-02-23 11:53:28 +08:00
yolain b11c634872 Fix showAnything can not working on latest comfyui frontend 2026-02-13 21:56:09 +08:00
rjgoif 7c470c67d6 Update logic.py (#949)
Fixed Range(Float) node which would sometimes truncate the list of values by 1 due to rounding error.
2026-01-31 12:21:35 +08:00
yolain b5865efd16 Fix multiAnglePrompt settings failed to save 2026-01-30 19:01:11 +08:00
yolain 5ec3b5ef86 Fix easy showAnything #933 2026-01-25 17:42:53 +08:00
yolain b5e31ef12a Bump Version 2026-01-23 14:18:34 +08:00
yolain 21b3c15040 Fix #946 2026-01-23 14:04:47 +08:00
yolain 5dfcbcf51d Fix custom widgets to support subgraph and Nodes 2.0 #942 2026-01-17 19:27:14 +08:00
yolain 070001b36b latest commit supplemental fix #939 2026-01-13 18:48:50 +08:00
yolain 6b4c89adc4 prompt.py is compatible with comfyui version <= 0.7.0 #939 2026-01-13 15:46:48 +08:00
yolain 32ad26f0e1 Add Invert rotate mode (#940)
* Remove the degree restriction on the vertical viewing angle

* Modify multi-perspective prompt

* Add Invert rotate mode
2026-01-13 14:14:50 +08:00
yolain d9c2072a2d Add Hollow Mode to easy multiAngle (#936)
- Global control to enable or disable angle prompts
- Added `Hollow Mode` for more intuitive visualization
- Removed label quantity limit; now supports unlimited additions
- Label addition button will copy parameters from the selected page
- Double-clicking any face of the cube quickly switches camera angles for easier operation

- 全局控制是否添加角度提示词
- 新增了`镂空模式`,可更直观地展示
- 去除标签限制个数,可添加无数个
- 标签添加按钮将复制选中页的参数
- 双击正方体的每一面可以快速切换摄像机角度,便于操作
2026-01-11 15:51:14 +08:00
yolain e94405e610 Fix easy multiAngle styles on light theme 2026-01-10 18:22:18 +08:00
yolain 03d5a4cf12 Update easy multiAngle frontend 2026-01-10 17:44:15 +08:00
yolain ad43ed3154 Add easy multiAngle for qwen 2511 multi lora 2026-01-10 17:33:43 +08:00
yolain 5cc1f8535a Convert prompt.py to V3 Schema 2026-01-10 13:20:48 +08:00
yolain 9f42ead9db Fix humanSegmentation error 2026-01-05 15:20:41 +08:00
yolain 23d9c365bd Add stringJoinLines 2025-12-30 13:47:07 +08:00
yolain 7a17ad010d Add stringToIntList and SimpleMath 2025-12-30 13:05:38 +08:00
yolain 3b38a5ae60 Fix lazy options 2025-12-30 12:25:43 +08:00
yolain 3b224fbccd update __init__.py 2025-12-28 14:51:23 +08:00
yolain 00c69fc816 Bump version and add icon 2025-12-19 12:13:54 +08:00
yolain d39b5e13e2 Fix isNone 2025-12-19 11:52:48 +08:00
yolain e900c2ca9c Add preview_rescale to easy imageChooser 2025-12-03 13:18:20 +08:00
yolain 9947b8be70 Fix widget hidden #910 2025-11-16 10:47:45 +08:00
yolain bc4cec287e Merge remote-tracking branch 'origin/main' 2025-11-16 10:47:03 +08:00
facok 84a4348bc5 Fix: Add max parameter to wildcardsPromptMatrix offset (#909)
- Set offset max to MAX_SEED_NUM (1125899906842624) instead of default 2048
 - This allows accessing all items in large wildcards (e.g., 100k lines)
- Previously, only the first 2048 items were accessible due to ComfyUI frontend default limit
2025-11-16 10:38:58 +08:00
yolain 29cca677c5 Fix #903 2025-11-08 11:13:51 +08:00
yolain 76b5896f08 Remove changes to widget styles #902 2025-11-06 14:32:39 +08:00
yolain be70a8671a Fix SubgraphNode titlebox style 2025-11-06 12:13:30 +08:00
yolain 0f6cc6958a Fix using JoinImageWithAlpha bug 2025-11-05 22:27:20 +08:00
yolain 499ed4eafc Add remove_empty_lines on easy promptLine 2025-10-30 14:13:51 +08:00
yolain 125b3a4905 When the sidebar is transparent, do not apply styles 2025-10-20 20:11:05 +08:00
yolain a1b402b4a9 Bump version 2025-10-15 21:33:46 +08:00
yolain 755723ce64 Fix references to some official nodes 2025-10-15 21:28:01 +08:00
yolain 737feef400 Add new compare functions to the easy compare node 2025-10-15 20:50:48 +08:00
yolain 8ecc929cd4 Fix easy seedList max_num #879 2025-10-03 12:53:31 +08:00
Mr-Quin 58e9d594cd add controlnet input to xyplot (#877) 2025-10-02 15:31:15 +08:00
yolain b6deb5f515 Add Swappers for ComfyUI-Easy-IndexTTS2 2025-09-16 22:32:36 +08:00
yolain 1148fbe5fe Fix easy indexAnything negative indices not working in image batches 2025-09-02 19:24:41 +08:00
yolain 5026d36489 Fix type #862 2025-09-02 18:35:19 +08:00
yolain fe0cad5981 Support negative indexing for easy indexAnything 2025-09-02 18:19:30 +08:00
yolain 3f37ba9491 Bump Version 2025-08-29 15:26:37 +08:00
yolain a72a4dc40f Removed the definition of the CSS class name gird-cols-1 #859 2025-08-27 14:59:08 +08:00
yolain 11794f7d71 Fix lock seed not working in easy promptAwait 2025-08-19 16:30:00 +08:00
yolain 74226d8b2a Rename the nodes map 2025-08-17 13:00:50 +08:00
yolain f550ce83a8 Fix image selector output error type #845 2025-08-09 12:42:20 +08:00
yolain 34882bca10 Bump Version 2025-08-08 19:01:55 +08:00
yolain 43b94be806 Update ImageChooser frontend code 2025-08-08 18:58:59 +08:00
yolain 0349d81694 Revamp ImageChooser and removed Preview&Choose in easy samplers #838 2025-08-08 18:54:24 +08:00
yolain 93254a4c07 Can replace the default fooocus_styles with files of the same name under styles dir. 2025-08-06 17:13:22 +08:00
yolain 8485447325 Fix globalSeed to work with Partial Execution #844 2025-08-06 17:12:06 +08:00
yolain 717092a3ce Add easy loraPromptApply 2025-07-26 19:39:01 +08:00
yolain 14a1121860 Add easy loraSwitcher 2025-07-26 17:20:39 +08:00
yolain 8c1eec2858 Fix front-end v1.24.2 and later failed to display the latent preview image in easy kSamplers during initial sampling. 2025-07-24 19:04:51 +08:00
yolain b6bb4a3055 Fixed segformer_b3_clothes download link error #831 2025-07-17 14:34:16 +08:00
yolain 6873492872 Fixed getStylesList value transfer error 2025-07-15 18:24:18 +08:00
yolain 2d71b3e647 Update StylesSelector 2025-07-15 18:02:12 +08:00
yolain e7320ec0c4 Remove easy showAnything error messages #776 2025-07-12 21:13:15 +08:00
yolainandyolain 560be6aee7 Update HumanSegmentation (#826)
* Change to new mask components on humanSegmentation

* Change segformer index

* Add segformer_b3_clothes and fashion

* Add face_parsing

* Fix face_parsing output error images

---------

Co-authored-by: yolain <me@yolain.com>
2025-07-10 18:32:06 +08:00
yolain 54614079ca Fixed makeImageForIcLora issue that occurred when the heights of two images were the same during image stitching on. 2025-07-05 13:48:08 +08:00
yolain b0cd0bcb5b Add easy joyCaption3API 2025-07-05 00:03:12 +08:00
yolain e46f8a45d0 Add easy promptAwait node (#818)
* Add submodule

* Set submodule branch to main

* Add PromptAwait node

* Fix loop has started for the second time, but the prompt word has not output new content

* Add input_1

* Change select widget to toolbar

* Modify some field names and displays

* Change select max-width

* Add output random seed in promptAwait
2025-06-30 01:22:13 +08:00
yolain 1616dd6602 Upgrade v1.3.1 to ComfyRegistry 2025-06-29 11:43:22 +08:00
yolain 282eedfea6 Rewrite drawNodeWidget and fix the GroupNode preview issue 2025-06-28 18:37:56 +08:00
yolain 17b163e234 Fix typo in EN tooltip for Nodes Map sidebar icon #816 2025-06-26 16:43:19 +08:00
Laegel 501d97bb5c chore: Now able to store metadata in ImageChooser (#813) 2025-06-23 16:22:23 +08:00
WathomeBo de92038f88 Update util.py (#809)
补充了用于选择lora模型的 setLoraName
2025-06-17 12:19:42 +08:00
Thomas Ward 530333d72d Update logic.py: properly handle overwrite mode (#807)
In low-level `OPEN` logic at the system, there are two modes of opening files for writing: `WRITE` which clobbers existing file data, and `APPEND` which allows appending of data.

In the current code, using `if not overwrite: pass` does nothing to define if you're actually appending or overwriting the file in your selection, and instead you should define the file open mode based on analysis of whether you have `overwrite` set to True or not.

This code patch does this.

(discovered as a result of helping someone via the ComfyUI discord)
2025-06-17 12:19:27 +08:00
yolain 041f49540c Forced override of drawNodeWidget for nodes containing hidden widget on the official theme #801 2025-06-14 14:22:54 +08:00
MakinoHaruka 71c7865d2d locale typo (#797) 2025-06-05 10:55:11 +08:00
yolain c7fbf05970 Implement error handling for all frontend hijack attempts. If EasyUse fails (e.g., due to official frontend changes), fall back to the native callback function. 2025-06-04 23:28:45 +08:00
yolain 2986a01469 When using the easy theme to draw node components, the draw method removed from front-end v1.21.3 is supplemented #793 2025-06-03 13:27:18 +08:00
yolain fa7c5d8b4d Fix ImagePreviewWidget can not display image in v1.21.3 frontend 2025-06-01 15:35:52 +08:00
Mike KinneyandMike Kinney 1d8db7510b Update XY Plot Labels (#792)
* Add lora weight to XY title axis. Trim lora desc.

* Add weights to lora names in xyPlots. Re-add because original lost in git merge mistake.

* Only add common label, if it's not an axis type.

* Remove bad comment.

---------

Co-authored-by: Mike Kinney <mike.kinney@valorepartners.com>
2025-06-01 13:15:55 +08:00
yolain 7ef0612ce7 Fix stepping changes not working in new front-end versions #789 2025-05-27 18:04:15 +08:00
yolain 7ff4790493 Fix update node height only if the node is preSamplingcustom listening for scheduler changes #788 2025-05-27 12:26:39 +08:00
yolain 640ef31625 Fix uniform width didn't work when sizes were inconsistent 2025-05-26 13:24:09 +08:00
yolain e4ac947d96 Fix precision issues with nodes related to float numbers #779 2025-05-22 11:06:52 +08:00
yolain d287e28e5c Fix fluxLoader using widget options instead of getting ckpt_names globally #772 2025-05-19 12:42:36 +08:00
yolain f33c17f762 Fix fluxLoader duplicate fetching of node information #772 2025-05-19 11:02:56 +08:00
yolain e07b8cc7bf Fix widgets being hidden in connections 2025-05-18 13:34:42 +08:00
yolain 6abe07bb79 Merge pull request #766 from mekinney/bugfix-xyplot-optional-lora
Use previously generated model/clip for next loaded lora
2025-05-15 16:27:31 +08:00
Mike Kinney 7fbd03bda7 Merge branch 'main' into bugfix-xyplot-optional-lora 2025-05-14 07:20:08 -07:00
Mike Kinney 9cc2ac02da Use previously generated model/clip for next loaded lora 2025-05-14 06:27:36 -07:00
Mike Kinney 2f2a3035a2 Merge pull request #3 from mekinney/bug-xyplot-save-model-and-clip-when-processing-lora-stack-in-xyplot
Update clip and model when adding loras
2025-05-13 16:52:23 -07:00
Mike Kinney 5d8f0a3b0a Update clip and model when adding loras 2025-05-13 16:49:49 -07:00
Mike Kinney 8aadd72494 Merge pull request #2 from mekinney/Change-Load-LORA-formatting-to-2-digits
Updated formatting for load lora for strength displays to 3 digits
2025-05-13 07:05:24 -07:00
Mike Kinney fceec754a4 Updated formatting for load lora for strength displays to 3 digits 2025-05-13 07:03:59 -07:00
Mike Kinney 419b7c985c Merge pull request #1 from mekinney/XYPlot-Footer
Add core XYPlot Footer
2025-05-13 06:51:23 -07:00
Mike Kinney ce62fc73da Add core XYPlot Footer 2025-05-13 06:35:45 -07:00
yolain 4f31641da3 Adding text truncation to widgets of type text 2025-05-13 12:35:31 +08:00
yolain deec62ab76 Set the minimum height of the initial display when imageChooser is paused. #755 2025-05-12 11:43:44 +08:00
yolain 5c8cdb58c7 Add easy seedList node (It's useful for in loops) 2025-05-11 00:43:27 +08:00
yolain d820842e39 Upgrade v1.3.0 to ComfyRegistry 2025-05-10 00:02:13 +08:00
yolain 2f78a523b3 Fix easy imageConcat match image size bug 2025-05-10 00:01:03 +08:00
yolain b2a8666423 Fix easy humanSegmentation not being selected 2025-05-09 16:10:33 +08:00
yolain 2c02a471d0 Fix latest commit #758 2025-05-09 07:46:20 +08:00
yolain ea521e0303 Set loop nodes maximum number of inputs or outputs to 20 2025-05-09 00:50:13 +08:00
yolain 9b5daac023 Fix cannot redefine property: value 2025-05-08 15:17:56 +08:00
yolain 0de83f88dc Force default web version to v2 2025-05-06 16:15:06 +08:00
yolain 342ce8ccad Fix last commit 2025-05-06 14:04:42 +08:00
yolain b7881d84b1 Add uniform width method to easy makeImageForICLora 2025-05-06 14:01:11 +08:00
yolain 0f5ad38384 Add apikey_override to easy joycaption2API 2025-05-06 12:56:07 +08:00
yolain a3f487c822 Merge pull request #752 from yolain/wildcardsPromptMatrix
Add output_limit to `easy wildcardsMatrix`
2025-04-30 18:03:08 +08:00
yolain d0f496adc1 Add output_limit to easy wildcardsMatrix 2025-04-30 18:01:14 +08:00
yolain 665861ff35 Merge wildcardsPromptMatrix Node from Rosmeowtis/main 2025-04-29 12:09:00 +08:00
yolain 61568e021c Update wildcardsPromptMatrix Node #743 2025-04-29 12:03:54 +08:00
yolain a2edc37d89 Merge pull request #743 from Rosmeowtis/main
Add wildcardsPromptMatrix Node
2025-04-29 11:37:02 +08:00
yolain 1c4cb43f7b Fix line are removed at the end of a connection on easy related nodes #748 2025-04-28 19:11:42 +08:00
yolain 66143b0e20 Merge pull request #746 from Hapseleg/Pipe-info-fix
missing vars
2025-04-27 11:03:03 +08:00
Hapseleg f0da5e25c9 missing vars 2025-04-26 20:03:18 +02:00
rosmeowtis ebf25b585f update descriptions to conform to reality 2025-04-25 18:10:35 +08:00
rosmeowtis fb8968d438 wildcardsPromptMatrix node will treat offset in cycle 2025-04-25 18:06:07 +08:00
rosmeowtis 15cfeedf7b Add wildcardsPromptMatrix Node:
1. wildcard-replaced prompt will be returned in order rather than randomly
2. will return the amount of probilities and amount of probilities each option or wildcard
3. the prompt can be selected by offset argument, the order of probilities is fixed
4. even if the offset exceeds the total, it will not stop, but will always return to the last probility, requiring additional nodes to control.
2025-04-25 03:07:49 +08:00
yolain 50ae13a993 Fix hidden item judgment needs to be delayed when first loading the page #741 2025-04-24 14:36:56 +08:00
yolain aedf917067 Remove getModelsThumbnail API #702 2025-04-22 13:48:14 +08:00
yolain 69ac5e52a0 EasyUse still works when layerDiffuse-related diffusers error 2025-04-21 19:56:17 +08:00
yolain 368f7e508d EasyUse still works when brushnet-related diffusers error 2025-04-21 11:00:38 +08:00
yolain 44f0676323 Fix an issue where some widgets' associated input sockets fail to display in the new release #734 2025-04-16 22:21:20 +08:00
yolain 98273b37f2 Fix Easy KSamplers preview&choose bug #733 2025-04-16 18:39:28 +08:00
yolain eff718c13f Upgrade v1.2.9 to ComfyRegistry 2025-04-15 14:20:58 +08:00
yolain 615a2abcfe Fix ImageChooser causes workflow processing to cancel #732 2025-04-15 13:28:53 +08:00
yolain 2b4b38ce03 Fix brushnet tensor(640) error 2025-04-13 02:11:55 +08:00
yolain dbbd2ffef3 Fix missing output optional_clip when Apply Lora Stack is disabled #729 2025-04-13 01:50:34 +08:00
yolain b1a875b151 Fix last commit bug 2025-04-08 00:59:38 +08:00
yolain 6a39ea1188 Fix widgets not hidden in v1.6.0 frontend 2025-04-08 00:49:37 +08:00
yolain 69aac075e8 Compatible drawNodeShape with stable front-end version 2025-04-06 15:53:48 +08:00
yolain e1dc9250b9 Fix missing strokeStyle on nodes during restart resulting in misconnections. 2025-04-06 15:46:59 +08:00
yolain 8b9c577f55 Fix outer border color should be red when node doesn't exist 2025-04-06 13:31:48 +08:00
yolain 6d8c266b04 Fix missing progressBar in latest frontend version 2025-04-06 12:48:41 +08:00
yolain 9292f22862 Removed global changes to the control widget, ComfyUI frontend was fixed this issue #714 2025-03-30 12:59:50 +08:00
yolain 10e9629ca3 Fix the context menu to miss Add Reroute #713 2025-03-29 07:23:35 +08:00
yolain 4f694195a2 Fix samplers can't display output image 2025-03-27 15:22:02 +08:00
yolain ff6c0f0e39 Fix image chooser can not select images #706 2025-03-26 11:37:42 +08:00
yolain a6e8783605 Fix save image simple doesn't show preview #708 2025-03-26 10:39:26 +08:00
yolain 3e84b8cd77 Fix contextMenu monkey patching to affect custom scripts (pysssss) nodes 2025-03-17 09:36:49 +08:00
yolain 9e70cc0090 Merge pull request #697 from Naix2012/main
Update prompt.py
2025-03-16 12:02:43 +08:00
Naix2012 7dddd2d6e5 Update prompt.py 2025-03-16 01:16:43 +08:00
yolain 63a1ca5ec6 Merge pull request #693 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-03-14 15:06:39 +08:00
yolain 0104f7f6a9 Upgrade v1.2.8 to ComfyRegistry 2025-03-10 11:10:54 +08:00
yolain 6b1f5cbf69 Modify some front-end style displays 2025-03-10 11:01:58 +08:00
yolain f888e3d75d Merge pull request #685 from facok/main
fix: wildcards, improve text encoding handling to prevent Chinese character garb…
2025-03-08 15:31:49 +08:00
facok 16631d21d9 fix: improve text encoding handling to prevent Chinese character garbling
ISO-8859-1 encoding can forcibly read any byte (it maps each byte directly to its corresponding character). This means it won't throw any decoding errors, but it will incorrectly interpret UTF-8 encoded Chinese characters as other characters, resulting in garbled text (mojibake).
ISO-8859-1编码可以强制读取任何字节(它会把每个字节都映射到对应的字符)
这意味着它不会抛出解码错误,但会把UTF-8编码的中文字符错误解释为其他字符
导致中文显示为乱码
2025-03-07 17:39:53 +08:00
yolain ccb4ba08fc Fix the issue that the output images does not replace the preview images after the kSamplers has finished sampling due to ComfyUI Frontend adjustment 2025-03-06 15:42:49 +08:00
yolain 0daf114fe8 Add refine_foreground for ben2 2025-02-24 14:41:22 +08:00
yolain 4e9c9c897c Fix ben2 using the wrong model 2025-02-24 14:24:32 +08:00
yolain 31fde1ae34 Add locale files 2025-02-23 15:08:17 +08:00
yolain aadbb0b389 Fix human segmentation not working in latest ComfyUI-frontend #668 2025-02-20 12:38:44 +08:00
yolain 52a8e7faf3 Fix some chinese translation errors 2025-02-18 22:55:30 +08:00
yolain 3893873085 Fix stylesSelector unable to get selections in ComfyUI_frontend latest version #658 2025-02-14 12:43:36 +08:00
yolain 037080ac39 Add option None to ckpt_name of easy fullLoader and easy fluxLoader #652. 2025-02-13 18:09:40 +08:00
yolain 4738313b64 Fix encodeURIComponent URI malformed when special characters appear in the model name 2025-02-12 13:05:43 +08:00
yolain e842c3bd06 Merge pull request #657 from newideas99/fix-clip-vision-urls
Fix CLIP vision model URLs and improve download error handling
2025-02-11 11:53:13 +08:00
newideas99 fa73da5a00 Update version to 1.2.8 2025-02-10 22:21:55 -05:00
Jacob Ferrari ffe26e8571 Fix CLIP vision model URLs and improve download error handling
- Update CLIP vision model URLs for IPAdapter and DynamiCrafter
- Improve error handling for model downloads with clearer error messages
- Add changelog entry for v1.2.8
2025-02-11 02:14:27 +00:00
snomiao 17e022a7aa chore(publish): update GitHub Actions workflow for node publishing
- Add permissions for issue writing
- Update action version to v1 for publish-node-action
- Add condition to run job only for 'yolain' repository owner
2025-01-20 21:28:03 +00:00
79 changed files with 13824 additions and 3859 deletions
+23 -2
View File
@@ -7,15 +7,36 @@ on:
paths:
- "pyproject.toml"
permissions:
issues: write
contents: write
jobs:
publish-node:
name: Publish Custom Node to registry
runs-on: ubuntu-latest
if: ${{ github.repository_owner == 'yolain' }}
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Extract version from pyproject.toml
id: version
run: |
VERSION=$(grep -E '^\s*version\s*=' pyproject.toml | head -1 | sed -E 's/.*version\s*=\s*"([^"]+)".*/\1/')
if [ -z "$VERSION" ]; then
echo "ERROR: Could not extract version from pyproject.toml" >&2
exit 1
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION"
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.version }}
generate_release_notes: true
- name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main
uses: Comfy-Org/publish-node-action@v1
with:
## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+4 -2
View File
@@ -9,13 +9,15 @@ workflow/**
autocomplete/**
web_beta/**
web_version/dev/**
ComfyUI-Easy-Use-Frontend/
docs/**
.vscode/
.vs/
.idea/
.claude/**
mmb-preset.custom.txt
config.yaml
node.tar.gz
.codex
.cursorrules
tools/ComfyUI-Easy-Use.json
tools/ComfyUI-Easy-Use.json
+4
View File
@@ -0,0 +1,4 @@
[submodule "ComfyUI-Easy-Use-Frontend"]
path = ComfyUI-Easy-Use-Frontend
url = https://github.com/yolain/ComfyUI-Easy-Use-Frontend.git
branch = main
+96 -2
View File
@@ -19,7 +19,7 @@
- 增加了预采样参数配置的节点,可与采样节点分离,更方便预览。
- 支持通配符与Lora的提示词节点,如需使用Lora Block Weight用法,需先保证自定义节点包中安装了 [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
- 可多选的风格化提示词选择器,默认是Fooocus的样式json,可自定义json放在styles底下,samples文件夹里可放预览图(名称和name一致,图片文件名如有空格需转为下划线'_')
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像,需先安装 [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes)
- 加载器可开启A1111提示词风格模式,可重现与webui生成近乎相同的图像
- 可使用`easy latentNoisy`或`easy preSamplingNoiseIn`节点实现对潜空间的噪声注入
- 简化 SD1.x、SD2.x、SDXL、SVD、Zero123等流程
- 简化 Stable Cascade [示例参考](https://github.com/yolain/ComfyUI-Yolain-Workflows?tab=readme-ov-file#1-13-stable-cascade)
@@ -52,6 +52,101 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
## 📜 更新日志
**v1.4.1**
- 修复 `easy saveText` 将文本输出限制在输出目录 #1032
**v1.4.0**
- 添加 `easy tableEditor` 节点 - 用于编辑和显示表格数据的节点
- 修复 `easy showAnything` 在最新版 ComfyUI 前端无法工作的问题
- 修复 `easy multiAnglePrompt` 设置保存失败的问题
- 修复 `easy detailer` 在子图中无法工作的问题
- 修复新版 ComfyUI 前端中"刷新节点"功能连接线丢失的问题
- 使用原生 `VAEDecodeTiled` 进行分块解码(支持 Qwen Image VAE)
- 修复 `easy forLoopStart` - 允许 `total=0` 以防止不必要的循环执行
- 修复 `easy preSampling` - `samplerCustomSettings.ip2p` 中 `vae`/`pixels` 现在是可选的
- 修复 `easy pixart` ControlNet 包装器 - 使用 `pe_interpolation` 替代已移除的 `lewei_scale`
- 修复 `easy promptConcat` - 当输入为列表时的 TypeError 问题
- 使用专用 RNG 进行全局种子生成
- 修复 `easy imageDetailTransfer` - 多帧蒙版在通道广播时崩溃
- 修复 Windows 环境下 PrimeVue 对话框遮罩未清除的问题
- 修复 `LockedMeta` 对象 TypeError(`object of type 'LockedMeta' has no len()`)
- 增强 `easy simpleMath` `evaluate_formula` 以处理列表输入
- 修复 `loraStack`/`controlnetStack` - 禁用时不再清除上游堆栈
- 修复 XYPlot 在 `Seeds++ Batch` 和元组 X/Y 输入时崩溃的问题
- 回滚循环节点到 v1 版本
- 修复 `easy NodesMap` - 避免递归组引用
- 修复 `easy CleanVRAM` 清理顺序并正确清除 Easy-Use 缓存
- 指定文件读取的 UTF-8 编码
- 移除不必要的 print 语句
**v1.3.6**
- 恢复 `easy showAnything` 对于列表类型的支持(但一些情况下展示庞大数据时仍会导致ComfyUI崩溃)
- 修复自定义小部件以支持子图和 Nodes 2.0 #942
- 添加 `easy multiAngle` 节点
- 将 `prompt.py` 转换为 V3 Schema
- 修复 `easy humanSegmentation` 错误
- 添加 `easy stringJoinLines`、`easy stringToIntList`、`easy simpleMath`
- 修复 `easy ifElse` 和 `easy anythingIndexSwitch` 在某些环境下失败的问题
**v1.3.5**
- 修复`isNone`
- 将`preview_rescale`添加到`easy imageChooser`
- 修复小部件隐藏#910
- 将 max 参数添加到 `wildcardsPromptMatrix` 偏移量 #909
- 修复子图节点上的标题框样式
- 在 `easypromptLine` 上添加 `remove_empty_lines`
**v1.3.4**
- 修复 `easy seedList` 最大值 #879
- 为xyplot添加controlnet input #877
- 为 `easy indexAnything` 支持 `反向索引`
**v1.3.3**
- 删除CSS类名称`gird-cols-1` #859
- 修复锁定种子在 `easy promptAwait` 中不起作用
- 重命名节点图
- 修复`easy ImageChooser`输出错误类型 #845
**v1.3.2**
- 改造 `easy imageChooser` 节点以兼容 frontend>=v1.24.2, 解决方案参考自 [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
- 改造 `easy stylesSelector` 节点, 你可在 [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) 下载到 `styles` 文件夹下
- 改造 `easy humanSegmentation` 节点
- 修复 `easy makeImageForICLora` 节点.
- 添加 `easy joycaption3API` 节点
- 添加 `easy promptAwait` 节点
**v1.3.1**
- 重写 drawNodeWidget 修复组节点预览的问题.
- 更新了一些 XYPlot 的功能 by [mekinney](https://github.com/mekinney)
- 添加 `easy seedList` 节点 (它对循环节点有用)
**v1.3.0**
- 将循环节点设置为最大输入和输出数量为20
- 添加 `uniform width` 方式到 `easy makeImageForICLora`
- 增加 `wildcardsPromptMatrix` 通配符提示词矩阵,由 [Rosmeowtis](https://github.com/Rosmeowtis) 贡献
**v1.2.9**
- 修复 Imagechooser 会导致工作流处理取消
- 修复 brushnet tensor(640) 错误
- 修复v1.6.0前端之后无法隐藏小部件的bug
- 修复图像选择器无法选择图像
- 修复ContextMenu Monkey修补以影响自定义脚本(PYSSSS)节点
**v1.2.8**
- 修复了一些BUG (😹)
- 增加了多语言目录
**v1.2.7**
- 优化管理节点组显示
@@ -496,7 +591,6 @@ git clone https://github.com/yolain/ComfyUI-Easy-Use
**Comfyui-Easy-Use** 是一个 GPL 许可的开源项目。为了项目取得更好、可持续的发展,我希望能够获得更多的支持。 如果我的自定义节点为您的一天增添了价值,请考虑喝杯咖啡来进一步补充能量! 💖感谢您的支持,每一杯咖啡都是我创作的动力!
- [BiliBili充电](https://space.bilibili.com/1840885116)
- [爱发电](https://afdian.com/a/yolain)
- [Wechat/Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
感谢您的捐助,我将用这些费用来租用 GPU 或购买其他 GPT 服务,以便更好地调试和完善 ComfyUI-Easy-Use 功能
+118 -22
View File
@@ -12,29 +12,29 @@
**ComfyUI-Easy-Use** is an efficiency custom nodes integration package, which is extended on the basis of [TinyTerraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes). It has been integrated and optimized for many popular awesome custom nodes to achieve the purpose of faster and more convenient use of ComfyUI. While ensuring the degree of freedom, it restores the ultimate smooth image production experience that belongs to Stable Diffusion.
## 👨🏻‍🎨 Introduce
## 👨🏻‍🎨 Introduction
- Inspire by [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes), which greatly reduces the time cost of tossing workflows。
- Inspired by [tinyterraNodes](https://github.com/TinyTerra/ComfyUI_tinyterraNodes), which greatly reduces the time cost of tossing workflows。
- UI interface beautification, the first time you install the user, if you need to use the UI theme, please switch the theme in Settings -> Color Palette and refresh page.
- Added a node for pre-sampling parameter configuration, which can be separated from the sampling node for easier previewing
- Wildcards and lora's are supported, for Lora Block Weight usage, ensure that the custom node package has the [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack)
- Multi-selectable styled cue word selector, default is Fooocus style json, custom json can be placed under styles, samples folder can be placed in the preview image (name and name consistent, image file name such as spaces need to be converted to underscores '_')
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui, and needs to be installed [ComfyUI_smZNodes](https://github.com/shiimizu/ComfyUI_smZNodes) first.
- Noise injection into the latent space can be achieved using the `easy latentNoisy` or `easy preSamplingNoiseIn` node
- Simplified processes for SD1.x, SD2.x, SDXL, SVD, Zero123, etc. [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion)
- Simplified Stable Cascade [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade)
- Added a node for pre-sampling parameter configuration, which can be separated from the sampling node for easier previewing.
- Wildcards and lora's are supported, for Lora Block Weight usage, ensure that the custom node package has the [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack).
- Multi-selectable styled cue word selector, default is Fooocus style json, custom json can be placed under styles, samples folder can be placed in the preview image (name and name consistent, image file name such as spaces need to be converted to underscores '_').
- The loader enables the A1111 prompt mode, which reproduces nearly identical images to those generated by webui.
- Noise injection into the latent space can be achieved using the `easy latentNoisy` or `easy preSamplingNoiseIn` node.
- Simplified processes for SD1.x, SD2.x, SDXL, SVD, Zero123, etc. [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableDiffusion).
- Simplified Stable Cascade [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#StableCascade).
- Simplified Layer Diffuse [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#LayerDiffusion),The first time you use it you may need to run `pip install -r requirements.txt` to install the required dependencies.
- Simplified InstantID [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), You need to make sure that the custom node package has the [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID)
- Extending the usability of XYplot
- Fooocus Inpaint integration
- Simplified InstantID [Example](https://github.com/yolain/ComfyUI-Easy-Use?tab=readme-ov-file#InstantID), You need to make sure that the custom node package has the [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID).
- Extending the usability of XYplot.
- Fooocus Inpaint integration.
- Integration of common logical calculations, conversion of types, display of all types, etc.
- Background removal nodes for the RMBG-1.4 model supporting BriaAI, [BriaAI Guide](https://huggingface.co/briaai/RMBG-1.4)
- Forcibly cleared the memory usage of the comfy UI model are supported
- Stable Diffusion 3 multi-account API nodes are supported
- Support SD3's model
- Support Kolors‘s model
- Support Flux's model
- Support lazy if else and for loops
- Background removal nodes for the RMBG-1.4 model supporting BriaAI, [BriaAI Guide](https://huggingface.co/briaai/RMBG-1.4).
- Forcibly cleared the memory usage of the comfy UI model are supported.
- Stable Diffusion 3 multi-account API nodes are supported.
- Support SD3's model.
- Support Kolors‘s model.
- Support Flux's model.
- Support lazy if else and for loops.
## 👨🏻‍🔧 Installation
Clone the repo into the **custom_nodes** directory and install the requirements:
@@ -47,10 +47,106 @@ Double-click install.bat to install the required dependencies
## 📜 Changelog
**v1.4.1**
- Fix `easy saveText` to confine text outputs to output directory #1032
**v1.4.0**
- Add `easy tableEditor` node - A node for editing and displaying table data
- Fix `easy showAnything` not working on latest ComfyUI frontend
- Fix `easy multiAnglePrompt` settings failed to save
- Fix `easy detailer` not working in subgraphs
- Fix connection lines missing from "Refresh Nodes" feature in new ComfyUI frontend
- Use native `VAEDecodeTiled` for tiled decode (Qwen Image VAE support)
- Fix `easy forLoopStart` - allow `total=0` to prevent unnecessary loop execution
- Fix `easy preSampling` - `vae`/`pixels` now optional on `samplerCustomSettings.ip2p`
- Fix `easy pixart` ControlNet wrappers - use `pe_interpolation` instead of removed `lewei_scale`
- Fix `easy promptConcat` - TypeError when an input is a list
- Use a dedicated RNG for global seed generation
- Fix `easy imageDetailTransfer` - multi-frame masks crash on channel broadcast
- Fix PrimeVue dialog overlay not being cleared in Windows environment
- Fix `LockedMeta` object TypeError (`object of type 'LockedMeta' has no len()`)
- Enhance `easy simpleMath` `evaluate_formula` to handle list inputs
- Fix `loraStack`/`controlnetStack` - disable no longer erases upstream stack
- Fix XYPlot crashes in `Seeds++ Batch` and tuple X/Y inputs
- Rollback cycle node to version v1
- Fix `easy NodesMap` - avoid recursive group refs
- Fix `easy CleanVRAM` teardown ordering and clear Easy-Use cache properly
- Specify UTF-8 encoding for file reads
- Remove unnecessary print statements
**v1.3.6**
- Restored `easy showAnything` support for list types (but displaying large data in some cases may still cause ComfyUI to crash)
- Fix custom widgets to support subgraph and Nodes 2.0 #942
- Add `easy multiAngle` node
- Convert `prompt.py` to V3 Schema
- Fix `easy humanSegmentation` error
- Add `easy stringJoinLines`,`easy stringToIntList`, `easy simpleMath`
- Fix `easy ifElse` and `easy anythingIndexSwitch` fails in certain environments
**v1.3.5**
- Fix `isNone`
- Add `preview_rescale` to `easy imageChooser`
- Fix widget hidden #910
- Add max parameter to `wildcardsPromptMatrix` offset #909
- Fix title box style on subgraph node
- Add `remove_empty_lines` on `easy promptLine`
**v1.3.4**
- Fix `easy seedList` max_num #879
- Add controlnet input to xyplot #877
- Support `nagative indexing` for `easy indexAnything`
**v1.3.3**
- Removed the definition of the CSS class name gird-cols-1 #859
- Fix lock seed not working in `easy promptAwait`
- Rename the nodes map
- Fix `easy imageChooser` output error type #845
**v1.3.2**
- Revamp `easy imageChooser` node to adapt frontend>=v1.24.2, solution referenced from [Comfyui_LG_Tools](https://github.com/LAOGOU-666/Comfyui_LG_Tools)
- Revamp `easy stylesSelector` node, and you can download [other styles files](https://github.com/yolain/EasyUse-Styles-Templates) to the `styles` folder
- Revamp `easy humanSegmentation` node
- Fix `easy makeImageForICLora` node issue, that occurred when the heights of two images were the same during image stitching on.
- Add `easy joycaption3API` node
- Add `easy promptAwait` node
**v1.3.1**
- Rewrite drawNodeWidget and fix the GroupNode preview issue.
- Updated some features of XYPlot by [mekinney](https://github.com/mekinney)
- Add `easy seedList` node (It's useful for in loops)
**v1.3.0**
- Set loop nodes maximum number of inputs and outputs to 20
- Add `uniform width` method to `easy makeImageForICLora`
- Add `wildcardsPromptMatrix` Node by [Rosmeowtis](https://github.com/Rosmeowtis)
**v1.2.9**
- Fix ImageChooser causes workflow processing to cancel
- Fix brushnet tensor(640) error
- Fix widgets not hidden after v1.6.0 frontend
- Fix image chooser can not select images
- Fix contextMenu monkey patching to affect custom scripts (pysssss) nodes
**v1.2.8**
- Added the multi-language catalog
- Fix CLIP vision model download URLs for IPAdapter and DynamiCrafter
- Improve error handling for model downloads with clearer error messages and better handling of download failures
**v1.2.7**
- Optimize display of the node maps
- Add `ben2` on `easy imageRemBg`
- Added `ben2` on `easy imageRemBg`
- Using a new way to display the models thumbnails in the loaders (supported diffusion_models、lors、checkpoints)
**v1.2.6**
@@ -479,11 +575,11 @@ If my custom nodes has added value to your day, consider indulging in a coffee t
💖You can support me in any of the following ways:
- [BiliBili](https://space.bilibili.com/1840885116)
- [Afdian](https://afdian.com/a/yolain)
- [Wechat / Alipay](https://github.com/user-attachments/assets/803469bd-ed6a-4fab-932d-50e5088a2d03)
## 🌟Stargazers
My gratitude extends to the generous souls who bestow a star. Your support is much appreciated!
[![Stargazers repo roster for @yolain/ComfyUI-Easy-Use](https://reporoster.com/stars/yolain/ComfyUI-Easy-Use)](https://github.com/yolain/ComfyUI-Easy-Use/stargazers)
[![Stargazers repo roster for @yolain/ComfyUI-Easy-Use](https://reporoster.com/stars/yolain/ComfyUI-Easy-Use)](https://github.com/yolain/ComfyUI-Easy-Use/stargazers)
+6 -8
View File
@@ -1,10 +1,14 @@
__version__ = "1.2.7"
__version__ = "1.4.1"
import yaml
import json
import os
import folder_paths
import importlib
cwd_path = os.path.dirname(os.path.realpath(__file__))
comfy_path = folder_paths.base_path
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
@@ -16,9 +20,6 @@ for module_name in nodes_list:
NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **imported_module.NODE_CLASS_MAPPINGS}
NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **imported_module.NODE_DISPLAY_NAME_MAPPINGS}
cwd_path = os.path.dirname(os.path.realpath(__file__))
comfy_path = folder_paths.base_path
#Wildcards
from .py.libs.wildcards import read_wildcard_dict
wildcards_path = os.path.join(os.path.dirname(__file__), "wildcards")
@@ -59,11 +60,8 @@ if not os.path.exists(example_path):
with open(example_path, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4, ensure_ascii=False)
# get comfyui revision
from .py.libs.utils import compare_revision
new_frontend_revision = 2546
web_default_version = 'v2' if compare_revision(new_frontend_revision) else 'v1'
web_default_version = 'v2'
# web directory
config_path = os.path.join(cwd_path, "config.yaml")
if os.path.isfile(config_path):
+31
View File
@@ -0,0 +1,31 @@
{
"settingsCategories": {
"Hotkeys": "Hotkeys",
"Nodes": "Nodes",
"NodesMap": "NodesMap",
"StylesSelector": "StylesSelector"
},
"nodeCategories": {
"Util": "Util",
"Seed": "Seed",
"Prompt": "Prompt",
"Loaders": "Loaders",
"Adapter": "Adapter",
"Inpaint": "Inpaint",
"PreSampling": "PreSampling",
"Sampler": "Sampler",
"Fix": "Fix",
"Pipe": "Pipe",
"XY Inputs": "XY Inputs",
"Image": "Image",
"Segmentation": "Segmentation",
"\uD83D\uDEAB Deprecated": "\uD83D\uDEAB Deprecated",
"Type": "Type",
"Math": "Math",
"Switch": "Switch",
"Index Switch": "Index Switch",
"While Loop": "While Loop",
"For Loop": "For Loop",
"LoadImage": "Load Image"
}
}
File diff suppressed because it is too large Load Diff
+67
View File
@@ -0,0 +1,67 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "Enable Shift+g to add the selected nodes to a group",
"tooltip": "From v1.2.39, you can use Ctrl+g instead"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "Enable Shift+r to unload model and node cache"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "Enable Shift+m to toggle nodes map"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "Enable Shift+Up/Down/Left/Right and Shift+Ctrl+Alt+Left/Right to align selected nodes",
"tooltip": "Shift+Up/Down/Left/Right can align selected nodes, Shift+Ctrl+Alt+Left/Right can distribute nodes horizontally/vertically"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "Enable Shift+Ctrl+Left/Right to normalize selected nodes",
"tooltip": "Enable Shift+Ctrl+Left to normalize width and Shift+Ctrl+Right to normalize height"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "Enable Alt+1~9 to paste node templates into the workflow"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "Enable Up/Down/Left/Right to jump to the nearest node"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "Enable automatic nesting of subdirectories in the context menu"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "Enable model preview thumbnails"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "Enable A~Z sorting of new nodes in the context menu"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "Use three quick buttons in the context menu",
"options": {
"At the forefront": "At the forefront",
"At the end": "At the end",
"Disable": "Disable"
}
},
"EasyUse_Nodes_Runtime": {
"name": "Enable node runtime display"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "Enable chaining of get and set points with the parent node"
},
"EasyUse_NodesMap_Sorting": {
"name": "Manage nodes group sorting mode",
"tooltip": "Automatically sort by default. If set to manual, groups can be drag and dropped and the order will be saved.",
"options": {
"Auto sorting": "Auto sorting",
"Manual drag&drop sorting": "Manual drag&drop sorting"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "Enable node ID display"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "Show groups only"
},
"EasyUse_NodesMap_Enable": {
"name": "Enable Group Map",
"tooltip": "You need to refresh the page to update successfully"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"settingsCategories": {
"Hotkeys": "Raccourcis",
"Nodes": "Nœuds",
"NodesMap": "Carte des nœuds"
},
"nodeCategories": {
"Util": "Utilitaire",
"Seed": "Graine",
"Prompt": "Prompt",
"Loaders": "Chargeurs",
"Adapter": "Adaptateur",
"Inpaint": "Retouche",
"PreSampling": "Pré-échantillonnage",
"Sampler": "Échantillonneur",
"Fix": "Correction",
"Pipe": "Pipeline",
"XY Inputs": "Entrées XY",
"Image": "Image",
"Segmentation": "Segmentation",
"\uD83D\uDEAB Deprecated": "\uD83D\uDEAB Obsolète",
"Type": "Type",
"Math": "Mathématiques",
"Switch": "Interrupteur",
"Index Switch": "Interrupteur d'index",
"While Loop": "Boucle While",
"For Loop": "Boucle For",
"LoadImage": "Charger l'image"
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "Activer Shift+g pour ajouter les nœuds sélectionnés à un groupe",
"tooltip": "Depuis la v1.2.39, vous pouvez utiliser Ctrl+g à la place"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "Activer Shift+r pour décharger le cache du modèle et des nœuds"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "Activer Shift+m pour basculer la carte des nœuds"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "Activer Shift+Up/Down/Left/Right et Shift+Ctrl+Alt+Left/Right pour aligner les nœuds sélectionnés",
"tooltip": "Shift+Up/Down/Left/Right peut aligner les nœuds sélectionnés, Shift+Ctrl+Alt+Left/Right peut les répartir horizontalement/verticalement"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "Activer Shift+Ctrl+Left/Right pour normaliser les nœuds sélectionnés",
"tooltip": "Activer Shift+Ctrl+Left pour normaliser la largeur et Shift+Ctrl+Right pour normaliser la hauteur"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "Activer Alt+1~9 pour coller les modèles de nœuds dans le workflow"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "Activer Up/Down/Left/Right pour passer au nœud le plus proche"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "Activer l'imbrication automatique des sous-répertoires dans le menu contextuel"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "Activer les vignettes d'aperçu du modèle"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "Activer le tri A~Z des nouveaux nœuds dans le menu contextuel"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "Utiliser trois boutons rapides dans le menu contextuel",
"options": {
"At the forefront": "À l'avant-plan",
"At the end": "À la fin",
"Disable": "Désactiver"
}
},
"EasyUse_Nodes_Runtime": {
"name": "Activer l'affichage du temps d'exécution des nœuds"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "Activer le chaînage des points get et set avec le nœud parent"
},
"EasyUse_NodesMap_Sorting": {
"name": "Gérer le mode de tri des groupes de nœuds",
"tooltip": "Tri automatique par défaut. Si défini sur manuel, les groupes peuvent être glissés-déposés et l'ordre sera sauvegardé.",
"options": {
"Auto sorting": "Tri automatique",
"Manual drag&drop sorting": "Tri manuel par glisser-déposer"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "Activer l'affichage de l'ID du nœud"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "Afficher uniquement les groupes"
},
"EasyUse_NodesMap_Enable": {
"name": "Activer la carte des groupes",
"tooltip": "Vous devez actualiser la page pour mettre à jour"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"settingsCategories": {
"Hotkeys": "ショートカットキー",
"Nodes": "ノード",
"NodesMap": "ノードマップ"
},
"nodeCategories": {
"Util": "ユーティリティ",
"Seed": "シード",
"Prompt": "プロンプト",
"Loaders": "ローダー",
"Adapter": "アダプター",
"Inpaint": "インペイント",
"PreSampling": "プリサンプリング",
"Sampler": "サンプラー",
"Fix": "フィックス",
"Pipe": "パイプ",
"XY Inputs": "XY入力",
"Image": "画像",
"Segmentation": "セグメンテーション",
"\uD83D\uDEAB Deprecated": "🚫 非推奨",
"Type": "タイプ",
"Math": "数学",
"Switch": "スイッチ",
"Index Switch": "インデックススイッチ",
"While Loop": "Whileループ",
"For Loop": "Forループ",
"LoadImage": "画像読み込み"
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "Shift+gを使用して選択したノードをグループに追加する",
"tooltip": "v1.2.39以降、Ctrl+gが使用できます"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "Shift+rを使用してモデルおよびノードキャッシュをアンロードする"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "Shift+mを使用してノードマップを表示/非表示にします"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "Shift+上/下/左/右およびShift+Ctrl+Alt+左/右を使用して選択したノードを整列する",
"tooltip": "Shift+上/下/左/右で選択したノードを整列し、Shift+Ctrl+Alt+左/右で水平方向/垂直方向に分布させる"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "Shift+Ctrl+左/右を使用して選択したノードのサイズを正規化する",
"tooltip": "Shift+Ctrl+左で幅を、Shift+Ctrl+右で高さを正規化する"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "Alt+1~9を使用してワークフローにノードテンプレートを貼り付ける"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "上/下/左/右を使用して最も近いノードにジャンプする"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "コンテキストメニューでサブディレクトリを自動でネストする"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "モデルプレビューサムネイルを有効にする"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "コンテキストメニューで新規ノードをA~Z順に並べ替える"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "コンテキストメニューで3つのクイックボタンを使用する",
"options": {
"At the forefront": "最前面に",
"At the end": "最後に",
"Disable": "無効"
}
},
"EasyUse_Nodes_Runtime": {
"name": "ノードの実行時間表示を有効にする"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "親ノードと取得/設定ポイントを連結することを有効にする"
},
"EasyUse_NodesMap_Sorting": {
"name": "ノードグループの並べ替えモードを管理する",
"tooltip": "デフォルトで自動的に並べ替えます。マニュアルに設定した場合、グループをドラッグアンドドロップで並べ替え、順序が保存されます。",
"options": {
"Auto sorting": "自動並べ替え",
"Manual drag&drop sorting": "手動ドラッグアンドドロップによる並べ替え"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "ノードIDの表示を有効にする"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "グループのみ表示する"
},
"EasyUse_NodesMap_Enable": {
"name": "グループマップを有効にする",
"tooltip": "ページを更新する必要があります"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"settingsCategories": {
"Hotkeys": "단축키",
"Nodes": "노드",
"NodesMap": "노드 맵"
},
"nodeCategories": {
"Util": "유틸",
"Seed": "시드",
"Prompt": "프롬프트",
"Loaders": "로더",
"Adapter": "어댑터",
"Inpaint": "인페인트",
"PreSampling": "사전 샘플링",
"Sampler": "샘플러",
"Fix": "픽스",
"Pipe": "파이프",
"XY Inputs": "XY 입력",
"Image": "이미지",
"Segmentation": "분할",
"\uD83D\uDEAB Deprecated": "\uD83D\uDEAB 사용 중단",
"Type": "유형",
"Math": "수학",
"Switch": "스위치",
"Index Switch": "인덱스 스위치",
"While Loop": "while 루프",
"For Loop": "for 루프",
"LoadImage": "이미지 로드"
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "Shift+g 를 사용하여 선택된 노드를 그룹에 추가합니다",
"tooltip": "v1.2.39부터는 Ctrl+g 를 사용할 수 있습니다"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "Shift+r 를 사용하여 모델 및 노드 캐시를 언로드합니다"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "Shift+m 를 사용하여 노드 맵을 전환합니다"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "Shift+Up/Down/Left/Right 와 Shift+Ctrl+Alt+Left/Right 를 사용하여 선택된 노드를 정렬합니다",
"tooltip": "Shift+Up/Down/Left/Right 는 선택된 노드를 정렬하며, Shift+Ctrl+Alt+Left/Right 는 노드를 수평/수직으로 분배합니다"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "Shift+Ctrl+Left/Right 를 사용하여 선택된 노드를 정규화합니다",
"tooltip": "Shift+Ctrl+Left 는 너비를, Shift+Ctrl+Right 는 높이를 정규화합니다"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "Alt+1~9 를 사용하여 워크플로우에 노드 템플릿을 붙여넣습니다"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "Up/Down/Left/Right 를 사용하여 가장 가까운 노드로 이동합니다"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "컨텍스트 메뉴에서 자동으로 하위 디렉토리를 중첩합니다"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "모델 미리보기 썸네일을 활성화합니다"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "컨텍스트 메뉴에서 새로운 노드를 A~Z 순으로 정렬합니다"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "컨텍스트 메뉴에 3개의 빠른 옵션 버튼을 사용합니다",
"options": {
"At the forefront": "앞쪽에",
"At the end": "뒤쪽에",
"Disable": "비활성화"
}
},
"EasyUse_Nodes_Runtime": {
"name": "노드 실행 시간 표시를 활성화합니다"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "부모 노드와 연결된 get/ set 포인트 체이닝을 활성화합니다"
},
"EasyUse_NodesMap_Sorting": {
"name": "노드 그룹 정렬 모드를 관리합니다",
"tooltip": "기본값은 자동 정렬입니다. 수동으로 설정하면 그룹을 드래그 앤 드롭할 수 있으며 순서가 저장됩니다.",
"options": {
"Auto sorting": "자동 정렬",
"Manual drag&drop sorting": "수동 드래그 앤 드롭 정렬"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "노드 ID 표시를 활성화합니다"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "그룹만 표시합니다"
},
"EasyUse_NodesMap_Enable": {
"name": "그룹 맵을 활성화합니다",
"tooltip": "업데이트를 위해 페이지를 새로고침해야 합니다"
}
}
+30
View File
@@ -0,0 +1,30 @@
{
"settingsCategories": {
"Hotkeys": "Горячие клавиши",
"Nodes": "Узлы",
"NodesMap": "Карта узлов"
},
"nodeCategories": {
"Util": "Утилиты",
"Seed": "Сид",
"Prompt": "Подсказка",
"Loaders": "Загрузчики",
"Adapter": "Адаптер",
"Inpaint": "Ретушь",
"PreSampling": "Предвыборка",
"Sampler": "Сэмплер",
"Fix": "Исправление",
"Pipe": "Конвейер",
"XY Inputs": "Ввод XY",
"Image": "Изображение",
"Segmentation": "Сегментация",
"\uD83D\uDEAB Deprecated": "\uD83D\uDEAB Устарело",
"Type": "Тип",
"Math": "Математика",
"Switch": "Переключатель",
"Index Switch": "Переключатель индексов",
"While Loop": "Цикл while",
"For Loop": "Цикл for",
"LoadImage": "Загрузка изображения"
}
}
+67
View File
@@ -0,0 +1,67 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "Включить Shift+g для добавления выделенных узлов в группу",
"tooltip": "Начиная с версии v1.2.39, можно использовать Ctrl+g"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "Включить Shift+r для выгрузки модели и кэша узлов"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "Включить Shift+m для переключения карты узлов"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "Включить Shift+Стрелки для выравнивания выделенных узлов и Shift+Ctrl+Alt+Стрелки для распределения узлов по горизонтали/вертикали",
"tooltip": "Shift+Стрелки выравнивают выделенные узлы, Shift+Ctrl+Alt+Стрелки распределяют узлы по горизонтали/вертикали"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "Включить Shift+Ctrl+Стрелки для нормализации выделенных узлов",
"tooltip": "Включить Shift+Ctrl+Лево для нормализации ширины и Shift+Ctrl+Право для нормализации высоты"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "Включить Alt+1~9 для вставки шаблонов узлов в рабочий процесс"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "Включить Стрелки для перехода к ближайшему узлу"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "Включить автоматическое вложение подкаталогов в контекстном меню"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "Включить превью миниатюр моделей"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "Включить A~Z сортировку новых узлов в контекстном меню"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "Использовать три быстрых кнопки в контекстном меню",
"options": {
"At the forefront": "В начале",
"At the end": "В конце",
"Disable": "Отключено"
}
},
"EasyUse_Nodes_Runtime": {
"name": "Включить отображение времени выполнения узлов"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "Включить связывание точек получения и установки с родительским узлом"
},
"EasyUse_NodesMap_Sorting": {
"name": "Управление режимом сортировки групп узлов",
"tooltip": "По умолчанию автоматическая сортировка. При ручном режиме группы можно перемещать методом перетаскивания, и порядок будет сохранён.",
"options": {
"Auto sorting": "Автоматическая сортировка",
"Manual drag&drop sorting": "Ручная сортировка перетаскиванием"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "Включить отображение ID узлов"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "Показывать только группы"
},
"EasyUse_NodesMap_Enable": {
"name": "Включить карту групп",
"tooltip": "Необходимо обновить страницу для успешного обновления"
}
}
+7
View File
@@ -1,4 +1,11 @@
{
"settingsCategories": {
"Hotkeys": "快捷键",
"Nodes": "节点相关",
"NodesMap": "管理节点组",
"StylesSelector": "样式选择器",
"MultiAngle": "摄影机多角度提示词"
},
"nodeCategories": {
"Util": "工具",
"Seed": "随机种",
+291 -169
View File
@@ -75,6 +75,35 @@
}
}
},
"easy wildcardsMatrix": {
"display_name": "通配符提示词矩阵",
"inputs": {
"Select to add LoRA": {
"name": "选择添加Lora"
},
"Select to add Wildcard": {
"name": "选择添加通配符"
},
"offset": {
"name": "偏移量"
},
"output_limit": {
"name": "输出个数限制",
"tooltip": "输出n个填充后通配符, -1为输出所有可能性(偏移值失效),默认值为1"
}
},
"outputs": {
"0": {
"name": "通配填充词"
},
"1": {
"name": "可能性总数"
},
"2": {
"name": "可能性总数(每通配符)"
}
}
},
"easy prompt": {
"display_name": "提示词",
"inputs": {
@@ -125,6 +154,9 @@
},
"max_rows": {
"name": "最大行数"
},
"remove_empty_lines":{
"name": "去除空行"
}
},
"outputs": {
@@ -136,6 +168,35 @@
}
}
},
"easy promptAwait": {
"display_name": "提示词等待",
"inputs": {
"now": {
"name": "当前"
},
"prev": {
"name": "上一步"
},
"prompt": {
"name": "提示词",
"placeholder": "输入提示词或使用语音输入转文字"
}
},
"outputs": {
"0": {
"name": "输出"
},
"1": {
"name": "提示词"
},
"2": {
"name": "继续"
},
"3": {
"name": "随机种"
}
}
},
"easy promptConcat": {
"display_name": "提示词联结",
"inputs": {
@@ -332,6 +393,19 @@
}
}
},
"easy multiAngle":{
"display_name": "多视角提示词",
"inputs": {
},
"outputs": {
"0": {
"name": "提示词"
},
"1":{
"name": "参数"
}
}
},
"easy fullLoader": {
"display_name": "简易加载器 (完整版)",
"inputs": {
@@ -360,7 +434,7 @@
"name": "VAE名称"
},
"clip_skip": {
"name": "Clip停止层"
"name": "CLIP停止层"
},
"lora_name": {
"name": "LoRA名称"
@@ -412,11 +486,20 @@
"1": {
"name": "模型"
},
"2": {
"name": "VAE"
},
"3": {
"name": "CLIP"
},
"4": {
"name": "正面条件"
},
"5": {
"name": "负面条件"
},
"6": {
"name": "LATENT"
}
}
},
@@ -436,7 +519,7 @@
"name": "VAE"
},
"clip_skip": {
"name": "Clip停止层"
"name": "CLIP停止层"
},
"lora_name": {
"name": "LoRA模型"
@@ -475,6 +558,9 @@
},
"1": {
"name": "模型"
},
"2": {
"name": "VAE"
}
}
},
@@ -494,7 +580,7 @@
"name": "VAE"
},
"clip_skip": {
"name": "Clip停止层"
"name": "CLIP停止层"
},
"lora_name": {
"name": "LoRA模型"
@@ -1084,6 +1170,31 @@
}
}
},
"easy loraSwitcher": {
"display_name": "简易Lora切换器",
"inputs": {
"optional_lora_stack": {
"name": "LoRA堆(可选)"
},
"toggle": {
"name": "开关"
},
"select": {
"name": "选择项"
},
"num_loras": {
"name": "LoRA数量"
}
},
"outputs": {
"0": {
"name": "LoRA堆"
},
"1": {
"name": "LoRA名称"
}
}
},
"easy controlnetStack": {
"display_name": "简易 ControlNet 堆",
"inputs": {
@@ -1574,7 +1685,7 @@
}
},
"easy latentNoisy": {
"display_name": "噪波Latent(Sigma乘积)",
"display_name": "噪波LATENT(Sigma乘积)",
"inputs": {
"pipe": {
"name": "节点束"
@@ -1583,7 +1694,7 @@
"name": "模型(可选)"
},
"optional_latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
},
"sample_name": {
"name": "采样器"
@@ -1618,7 +1729,7 @@
"name": "节点束"
},
"1": {
"name": "Latent"
"name": "LATENT"
},
"2": {
"name": "伽马值"
@@ -1635,7 +1746,7 @@
"name": "文字拼接"
},
"source_latent": {
"name": "Latent(源)"
"name": "LATENT(源)"
},
"source_mask": {
"name": "遮罩(源)"
@@ -1655,7 +1766,7 @@
"name": "节点束"
},
"1": {
"name": "Latent"
"name": "LATENT"
},
"2": {
"name": "条件"
@@ -1672,7 +1783,7 @@
"name": "图像转Latent"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"noise": {
"name": "噪波"
@@ -1695,7 +1806,7 @@
},
"outputs": {
"0": {
"name": "Latent"
"name": "LATENT"
}
}
},
@@ -1715,6 +1826,35 @@
}
}
},
"easy seedList": {
"display_name": "随机种列表",
"description": "可用于for循环的随机数种子列表,通过与easy forLoopStart节点的索引与easy indexAny节点相连接可实现在循环中使用不同种子值进行采样",
"inputs": {
"min_num": {
"name": "最小值"
},
"max_num": {
"name": "最大值"
},
"method": {
"name": "生成方式"
},
"total": {
"name": "总量"
},
"seed": {
"name": "列表序号"
}
},
"outputs": {
"0": {
"name": "随机种"
},
"1": {
"name": "总量"
}
}
},
"easy globalSeed": {
"display_name": "全局随机种",
"inputs": {
@@ -1794,7 +1934,7 @@
"name": "图像(可选)"
},
"latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
}
},
"outputs": {
@@ -1849,7 +1989,7 @@
"name": "图像(可选)"
},
"latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
}
},
"outputs": {
@@ -1865,7 +2005,7 @@
"name": "节点束"
},
"optional_latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
},
"optional_noise_seed": {
"name": "随机种(可选)"
@@ -1914,7 +2054,7 @@
"name": "图像转Latent"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"optional_sampler": {
"name": "采样器(可选)"
@@ -1993,7 +2133,7 @@
"name": "图像(可选)"
},
"latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
}
},
"outputs": {
@@ -2232,7 +2372,7 @@
"name": "图像(可选)"
},
"latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
}
},
"outputs": {
@@ -2260,7 +2400,7 @@
"name": "图像(可选)"
},
"latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
},
"steps": {
"name": "步数"
@@ -2313,13 +2453,16 @@
"name": "负面条件"
},
"5": {
"name": "图像转Latent"
"name": "LATENT"
},
"6": {
"name": "Latent"
"name": "VAE"
},
"7": {
"name": "随机种"
"name": "CLIP"
},
"8": {
"name": "seed"
}
}
},
@@ -2594,7 +2737,7 @@
"name": "负面条件(可选)"
},
"optional_latent": {
"name": "Latent(可选)"
"name": "LATENT(可选)"
},
"steps": {
"name": "步数"
@@ -2620,7 +2763,7 @@
"name": "节点束"
},
"1": {
"name": "Latent"
"name": "LATENT"
}
}
},
@@ -2643,7 +2786,7 @@
"name": "图像转Latent"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"image": {
"name": "图像"
@@ -2679,7 +2822,7 @@
"name": "图像转Latent"
},
"5": {
"name": "Latent"
"name": "LATENT"
},
"6": {
"name": "图像"
@@ -2739,17 +2882,17 @@
"neg": {
"name": "负面条件"
},
"vae": {
"VAE": {
"name": "VAE"
},
"clip": {
"CLIP": {
"name": "CLIP"
},
"image_to_latent": {
"name": "图像转Latent"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"image": {
"name": "图像"
@@ -2811,7 +2954,7 @@
"name": "图像转Latent"
},
"9": {
"name": "Latent"
"name": "LATENT"
},
"10": {
"name": "图像"
@@ -3097,34 +3240,34 @@
"name": "模型名称10"
},
"clip_skip_1": {
"name": "Clip停止层1"
"name": "CLIP停止层1"
},
"clip_skip_2": {
"name": "Clip停止层2"
"name": "CLIP停止层2"
},
"clip_skip_3": {
"name": "Clip停止层3"
"name": "CLIP停止层3"
},
"clip_skip_4": {
"name": "Clip停止层4"
"name": "CLIP停止层4"
},
"clip_skip_5": {
"name": "Clip停止层5"
"name": "CLIP停止层5"
},
"clip_skip_6": {
"name": "Clip停止层6"
"name": "CLIP停止层6"
},
"clip_skip_7": {
"name": "Clip停止层7"
"name": "CLIP停止层7"
},
"clip_skip_8": {
"name": "Clip停止层8"
"name": "CLIP停止层8"
},
"clip_skip_9": {
"name": "Clip停止层9"
"name": "CLIP停止层9"
},
"clip_skip_10": {
"name": "Clip停止层10"
"name": "CLIP停止层10"
},
"vae_name_1": {
"name": "VAE名称1"
@@ -3847,7 +3990,7 @@
"name": "图像"
},
"2": {
"name": "Latent"
"name": "LATENT"
}
}
},
@@ -4017,10 +4160,10 @@
"model": {
"name": "模型"
},
"clip": {
"CLIP": {
"name": "CLIP"
},
"vae": {
"VAE": {
"name": "VAE"
},
"positive": {
@@ -4168,6 +4311,12 @@
},
"save_prefix": {
"name": "保存前缀"
},
"add_background": {
"name": "添加背景"
},
"refine_foreground": {
"name": "优化前景"
}
},
"outputs": {
@@ -4195,6 +4344,37 @@
}
}
},
"easy loraPromptApply":{
"display_name": "应用提示词LoRA",
"inputs": {
"model": {
"name": "模型"
},
"clip": {
"name": "CLIP"
},
"positive":{
"name": "正面提示词"
},
"negative":{
"name": "负面提示词"
}
},
"outputs":{
"0": {
"name": "模型"
},
"1": {
"name": "CLIP"
},
"2": {
"name": "正面提示词"
},
"3": {
"name": "负面提示词"
}
}
},
"easy loraStackApply": {
"display_name": "应用LoRA堆",
"inputs": {
@@ -4922,7 +5102,7 @@
"name": "模型"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"mode": {
"name": "模式"
@@ -5511,6 +5691,12 @@
},
"image_output": {
"name": "图像输出"
},
"add_background": {
"name": "添加背景"
},
"refine_foreground": {
"name": "优化前景"
}
},
"outputs": {
@@ -5664,6 +5850,9 @@
},
"pixels": {
"name": "限制像素"
},
"method": {
"name": "限制方式"
}
},
"outputs": {
@@ -5729,7 +5918,7 @@
"name": "模型"
},
"latent": {
"name": "Latent"
"name": "LATENT"
},
"head": {
"name": "head"
@@ -5899,7 +6088,7 @@
"display_name": "ckpt名称列表",
"inputs": {
"ckpt_name": {
"name": "模型名称"
"name": "模型名称"
}
},
"outputs": {
@@ -5921,6 +6110,23 @@
}
}
},
"easy tableEditor": {
"display_name": "表格编辑器",
"description": "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串及渲染图像。",
"inputs": {
"table_data": {
"name": "表格数据"
}
},
"outputs": {
"0": {
"name": "Markdown"
},
"1": {
"name": "图像"
}
}
},
"easy string": {
"display_name": "字符串",
"inputs": {
@@ -6025,73 +6231,13 @@
"easy whileLoopStart": {
"display_name": "While循环-开始",
"inputs": {
"initial_value0": {
"name": "初始值0"
},
"initial_value1": {
"name": "初始值1"
},
"initial_value2": {
"name": "初始值2"
},
"initial_value3": {
"name": "初始值3"
},
"initial_value4": {
"name": "初始值4"
},
"initial_value5": {
"name": "初始值5"
},
"initial_value6": {
"name": "初始值6"
},
"initial_value7": {
"name": "初始值7"
},
"initial_value8": {
"name": "初始值8"
},
"initial_value9": {
"name": "初始值9"
},
"condition": {
"name": "条件"
"name": "开始循环"
}
},
"outputs": {
"0": {
"name": "开始"
},
"1": {
"name": "值0"
},
"2": {
"name": "值1"
},
"3": {
"name": "值2"
},
"4": {
"name": "值3"
},
"5": {
"name": "值4"
},
"6": {
"name": "值5"
},
"7": {
"name": "值6"
},
"8": {
"name": "值7"
},
"9": {
"name": "值8"
},
"10": {
"name": "值9"
}
}
},
@@ -6101,71 +6247,11 @@
"flow": {
"name": "结束"
},
"initial_value0": {
"name": "初始值0"
},
"initial_value1": {
"name": "初始值1"
},
"initial_value2": {
"name": "初始值2"
},
"initial_value3": {
"name": "初始值3"
},
"initial_value4": {
"name": "初始值4"
},
"initial_value5": {
"name": "初始值5"
},
"initial_value6": {
"name": "初始值6"
},
"initial_value7": {
"name": "初始值7"
},
"initial_value8": {
"name": "初始值8"
},
"initial_value9": {
"name": "初始值9"
},
"condition": {
"name": "条件"
"name": "继续循环"
}
},
"outputs": {
"0": {
"name": "值0"
},
"1": {
"name": "值1"
},
"2": {
"name": "值2"
},
"3": {
"name": "值3"
},
"4": {
"name": "值4"
},
"5": {
"name": "值5"
},
"6": {
"name": "值6"
},
"7": {
"name": "值7"
},
"8": {
"name": "值8"
},
"9": {
"name": "值9"
}
}
},
"easy forLoopStart": {
@@ -6306,7 +6392,7 @@
"name": "高度"
},
"scale": {
"name": "缩放洗漱"
"name": "缩放系数"
},
"flip_w/h": {
"name": "翻转宽高"
@@ -6511,6 +6597,11 @@
"anything": {
"name": "输入任何"
}
},
"outputs": {
"0": {
"name": "输出"
}
}
},
"easy showTensorShape": {
@@ -6618,7 +6709,38 @@
"name": "温度"
},
"max_tokens": {
"name": "最大词令牌数"
"name": "最大词元数"
},
"caption_type": {
"name": "提示词类型"
},
"caption_length": {
"name": "提示词长度"
},
"name_input": {
"name": "名称输入"
}
},
"outputs": {
"0": {
"name": "提示词"
}
}
},
"easy joyCaption3API": {
"display_name": "JoyCaption3(硅基流动)",
"inputs": {
"image": {
"name": "图像"
},
"do_sample": {
"name": "执行采样"
},
"temperature": {
"name": "温度"
},
"max_tokens": {
"name": "最大词元数"
},
"caption_type": {
"name": "提示词类型"
@@ -6644,4 +6766,4 @@
}
}
}
}
}
+86
View File
@@ -0,0 +1,86 @@
{
"EasyUse_Hotkeys_AddGroup": {
"name": "启用 Shift+g 键将选中的节点添加一个组",
"tooltip": "从v1.2.39开始,可以使用Ctrl+g代替"
},
"EasyUse_Hotkeys_cleanVRAMUsed": {
"name": "启用 Shift+r 键卸载模型和节点缓存"
},
"EasyUse_Hotkeys_toggleNodesMap": {
"name": "启用 Shift+m 键显隐管理节点组"
},
"EasyUse_Hotkeys_AlignSelectedNodes": {
"name": "启用 Shift+上/下/左/右 和 Shift+Ctrl+Alt+左/右 键对齐选中的节点",
"tooltip": "Shift+上/下/左/右 可以对齐选中的节点, Shift+Ctrl+Alt+左/右 可以水平/垂直分布节点"
},
"EasyUse_Hotkeys_NormalizeSelectedNodes": {
"name": "启用 Shift+Ctrl+左/右 键规范化选中的节点",
"tooltip": "启用 Shift+Ctrl+左 键规范化宽度和 Shift+Ctrl+右 键规范化高度"
},
"EasyUse_Hotkeys_NodesTemplate": {
"name": "启用 Alt+1~9 从节点模板粘贴到工作流中"
},
"EasyUse_Hotkeys_JumpNearestNodes": {
"name": "启用 上/下/左/右 键跳转到最近的前后节点"
},
"EasyUse_ContextMenu_SubDirectories": {
"name": "启用上下文菜单自动嵌套子目录"
},
"EasyUse_ContextMenu_ModelsThumbnails": {
"name": "启动模型预览图显示"
},
"EasyUse_ContextMenu_NodesSort": {
"name": "启用右键菜单中新建节点A~Z排序"
},
"EasyUse_ContextMenu_QuickOptions": {
"name": "在右键菜单中使用三个快捷按钮",
"options": {
"At the forefront": "在最前面",
"At the end": "在最后面",
"Disable": "禁用"
}
},
"EasyUse_Nodes_Runtime": {
"name": "启动节点运行时间显示"
},
"EasyUse_Nodes_ChainGetSet": {
"name": "启用将获取点和设置点与父节点链在一起"
},
"EasyUse_NodesMap_Sorting": {
"name": "管理节点组排序模式",
"tooltip": "默认自动排序,如果设置为手动,组可以拖放并保存排序结果。",
"options": {
"Auto sorting": "自动排序",
"Manual drag&drop sorting": "手动拖拽排序"
}
},
"EasyUse_NodesMap_DisplayNodeID": {
"name": "启用节点ID显示"
},
"EasyUse_NodesMap_DisplayGroupOnly": {
"name": "仅显示组"
},
"EasyUse_NodesMap_Enable": {
"name": "启用管理节点组",
"tooltip": "您需要刷新页面以成功更新"
},
"EasyUse_StylesSelector_DisplayType": {
"name": "样式选择器显示类型",
"tooltip": "样式选择器显示类型,如果设置为“网格”,则显示为网格,如果设置为“列表”,则显示为列表",
"options": {
"Grid": "网格",
"List": "列表"
}
},
"EasyUse_MultiAngle_InvertRotate": {
"name": "启用反转旋转模式",
"tooltip": "在多角度节点中启用反转旋转模式,使旋转方向与大多数3D软件一致"
},
"EasyUse_MultiAngle_HollowMode": {
"name": "启用多角度镂空展示模式",
"tooltip": "在多角度节点中启用镂空展示模式,可以更直观地查看相机角度"
},
"EasyUse_MultiAngle_AddAnglePrompt": {
"name": "启用添加多角度提示词"
}
}
+13 -4
View File
@@ -195,7 +195,7 @@ REMBG_MODELS = {
"model_url": "briaai/RMBG-2.0"
},
"BEN2": {
"model_url": "https://huggingface.co/PramaLLC/BEN/resolve/main/BEN_Base.pth"
"model_url": "https://huggingface.co/PramaLLC/BEN2/resolve/main/BEN2_Base.pth"
}
}
@@ -336,7 +336,7 @@ IPADAPTER_CLIPVISION_MODELS = {
"model_url": "https://huggingface.co/openai/clip-vit-large-patch14-336/resolve/main/pytorch_model.bin"
},
"clip-vit-h-14-laion2B-s32B-b79K":{
"model_url": "https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_pytorch_model.safetensors"
"model_url": "https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors"
},
"sigclip_vision_patch14_384":{
"model_url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors"
@@ -350,7 +350,7 @@ DYNAMICRAFTER_MODELS = {
"model_url": "https://huggingface.co/ExponentialML/DynamiCrafterUNet/resolve/main/dynamicrafter_unet_512.safetensors",
"vae_url": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors",
"clip_url": "https://huggingface.co/stabilityai/stable-diffusion-2-1/resolve/main/text_encoder/model.safetensors",
"clip_vision_url": "https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_pytorch_model.safetensors",
"clip_vision_url": "https://huggingface.co/laion/CLIP-ViT-H-14-laion2B-s32B-b79K/resolve/main/open_clip_model.safetensors",
},
"dynamicrafter_unet_512_interp (2.98GB)": {
"model_url": "https://huggingface.co/ExponentialML/DynamiCrafterUNet/resolve/main/dynamicrafter_unet_512_interp.safetensors"
@@ -370,6 +370,15 @@ HUMANPARSING_MODELS = {
},
"human-parts":{
"model_url":"https://huggingface.co/Metal3d/deeplabv3p-resnet50-human/resolve/main/deeplabv3p-resnet50-human.onnx",
},
"segformer_b3_clothes":{
"model_name": "sayeed99/segformer_b3_clothes",
},
"segformer_b3_fashion":{
"model_name": "sayeed99/segformer-b3-fashion",
},
"face_parsing":{
"model_name": "jonathandinu/face-parsing"
}
}
@@ -395,4 +404,4 @@ PROMPT_TEMPLATE = {
"nsfw": ["nude", "breast", "small breast", "middle breast", "large breast", "nipples", "clothes lift", "pussy juice trail", "pussy juice puddle", "small testicles", "medium testicles", "large testicles", "disembodied penis", "cum on body", "cum inside", "cum outside", "fingering", "handjob", "fellatio", "licking penis", "paizuri", "doggystyle", "cowgirl", "reversed cowgirl", "piledriver", "suspended congress", "full nelson",],
}
NEW_SCHEDULERS = ['align_your_steps', 'gits']
NEW_SCHEDULERS = ['align_your_steps', 'gits']
+8 -4
View File
@@ -56,10 +56,14 @@ class BizyAIRAPI:
f"Failed to connect to the server: {e}, if you have no key, "
)
# joycaptionTwo
def joyCaption2(self, payload, image):
api_key = self.getAPIKey()
url = f"{self.base_url}/supernode/joycaption2"
# joycaption
def joyCaption(self, payload, image, apikey_override=None, API_URL='/supernode/joycaption2'):
if apikey_override is not None:
api_key = apikey_override
else:
api_key = self.getAPIKey()
url = f"{self.base_url}{API_URL}"
print('Sending request to:', url)
auth = f"Bearer {api_key}"
headers = {
"accept": "application/json",
+4 -3
View File
@@ -77,10 +77,11 @@ def update_cache(k, tag, v):
else:
cache_count[k] += 1
def remove_cache(key):
global cache
if key == '*':
cache = TaggedCache(cache_settings)
cache.clear()
cache_count.clear()
elif key in cache:
del cache[key]
cache_count.pop(key, None)
else:
print(f"invalid {key}")
print(f"invalid {key}")
+139 -38
View File
@@ -1,52 +1,153 @@
from threading import Event
import torch
from server import PromptServer
from aiohttp import web
from comfy import model_management as mm
from comfy_execution.graph import ExecutionBlocker
import time
class ChooserCancelled(Exception):
pass
class ChooserMessage:
stash = {}
messages = {}
cancelled = False
def get_chooser_cache():
"""获取选择器缓存"""
if not hasattr(PromptServer.instance, '_easyuse_chooser_node'):
PromptServer.instance._easyuse_chooser_node = {}
return PromptServer.instance._easyuse_chooser_node
@classmethod
def addMessage(cls, id, message):
if message == '__cancel__':
cls.messages = {}
cls.cancelled = True
elif message == '__start__':
cls.messages = {}
cls.stash = {}
cls.cancelled = False
else:
cls.messages[str(id)] = message
def cleanup_session_data(node_id):
"""清理会话数据"""
node_data = get_chooser_cache()
if node_id in node_data:
session_keys = ["event", "selected", "images", "total_count", "cancelled"]
for key in session_keys:
if key in node_data[node_id]:
del node_data[node_id][key]
def wait_for_chooser(id, images, mode, period=0.1):
try:
node_data = get_chooser_cache()
images = [images[i:i + 1, ...] for i in range(images.shape[0])]
if mode == "Keep Last Selection":
if id in node_data and "last_selection" in node_data[id]:
last_selection = node_data[id]["last_selection"]
if last_selection and len(last_selection) > 0:
valid_indices = [idx for idx in last_selection if 0 <= idx < len(images)]
if valid_indices:
try:
PromptServer.instance.send_sync("easyuse-image-keep-selection", {
"id": id,
"selected": valid_indices
})
except Exception as e:
pass
cleanup_session_data(id)
indices_str = ','.join(str(i) for i in valid_indices)
images = [images[idx] for idx in valid_indices]
images = torch.cat(images, dim=0)
return {"result": (images,)}
if id in node_data:
del node_data[id]
event = Event()
node_data[id] = {
"event": event,
"images": images,
"selected": None,
"total_count": len(images),
"cancelled": False,
}
while id in node_data:
node_info = node_data[id]
if node_info.get("cancelled", False):
cleanup_session_data(id)
raise ChooserCancelled("Manual selection cancelled")
if "selected" in node_info and node_info["selected"] is not None:
break
@classmethod
def waitForMessage(cls, id, period=0.1, asList=False):
sid = str(id)
while not (sid in cls.messages) and not ("-1" in cls.messages):
if cls.cancelled:
cls.cancelled = False
raise ChooserCancelled()
time.sleep(period)
if cls.cancelled:
cls.cancelled = False
raise ChooserCancelled()
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
try:
if asList:
return [int(x.strip()) for x in message.split(",")]
if id in node_data:
node_info = node_data[id]
selected_indices = node_info.get("selected")
if selected_indices is not None and len(selected_indices) > 0:
valid_indices = [idx for idx in selected_indices if 0 <= idx < len(images)]
if valid_indices:
selected_images = [images[idx] for idx in valid_indices]
if id not in node_data:
node_data[id] = {}
node_data[id]["last_selection"] = valid_indices
cleanup_session_data(id)
selected_images = torch.cat(selected_images, dim=0)
return {"result": (selected_images,)}
else:
cleanup_session_data(id)
return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
else:
return int(message.strip())
except ValueError:
print(
f"ERROR IN IMAGE_CHOOSER - failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}")
return [1] if asList else 1
cleanup_session_data(id)
return {
"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
else:
return {"result": (images[0] if len(images) > 0 else ExecutionBlocker(None),)}
except ChooserCancelled:
raise mm.InterruptProcessingException()
except Exception as e:
node_data = get_chooser_cache()
if id in node_data:
cleanup_session_data(id)
if 'image_list' in locals() and len(images) > 0:
return {"result": (images[0])}
else:
return {"result": (ExecutionBlocker(None),)}
@PromptServer.instance.routes.post('/easyuse/image_chooser_message')
async def make_image_selection(request):
post = await request.post()
ChooserMessage.addMessage(post.get("id"), post.get("message"))
return web.json_response({})
async def handle_image_selection(request):
try:
data = await request.json()
node_id = data.get("node_id")
selected = data.get("selected", [])
action = data.get("action")
node_data = get_chooser_cache()
if node_id not in node_data:
return web.json_response({"code": -1, "error": "Node data does not exist"})
try:
node_info = node_data[node_id]
if "total_count" not in node_info:
return web.json_response({"code": -1, "error": "The node has been processed"})
if action == "cancel":
node_info["cancelled"] = True
node_info["selected"] = []
elif action == "select" and isinstance(selected, list):
valid_indices = [idx for idx in selected if isinstance(idx, int) and 0 <= idx < node_info["total_count"]]
if valid_indices:
node_info["selected"] = valid_indices
node_info["cancelled"] = False
else:
return web.json_response({"code": -1, "error": "Invalid Selection Index"})
else:
return web.json_response({"code": -1, "error": "Invalid operation"})
node_info["event"].set()
return web.json_response({"code": 1})
except Exception as e:
if node_id in node_data and "event" in node_data[node_id]:
node_data[node_id]["event"].set()
return web.json_response({"code": -1, "message": "Processing Failed"})
except Exception as e:
return web.json_response({"code": -1, "message": "Request Failed"})
+29 -1
View File
@@ -125,7 +125,35 @@ class ResizeMode(Enum):
return 2
assert False, "NOTREACHED"
# credit by https://github.com/chflame163/ComfyUI_LayerStyle/blob/main/py/imagefunc.py#L591C1-L617C22
def fit_resize_image(image: Image, target_width: int, target_height: int, fit: str, resize_sampler: str,
background_color: str = '#000000') -> Image:
image = image.convert('RGB')
orig_width, orig_height = image.size
if image is not None:
if fit == 'letterbox':
if orig_width / orig_height > target_width / target_height: # 更宽,上下留黑
fit_width = target_width
fit_height = int(target_width / orig_width * orig_height)
else: # 更瘦,左右留黑
fit_height = target_height
fit_width = int(target_height / orig_height * orig_width)
fit_image = image.resize((fit_width, fit_height), resize_sampler)
ret_image = Image.new('RGB', size=(target_width, target_height), color=background_color)
ret_image.paste(fit_image, box=((target_width - fit_width) // 2, (target_height - fit_height) // 2))
elif fit == 'crop':
if orig_width / orig_height > target_width / target_height: # 更宽,裁左右
fit_width = int(orig_height * target_width / target_height)
fit_image = image.crop(
((orig_width - fit_width) // 2, 0, (orig_width - fit_width) // 2 + fit_width, orig_height))
else: # 更瘦,裁上下
fit_height = int(orig_width * target_height / target_width)
fit_image = image.crop(
(0, (orig_height - fit_height) // 2, orig_width, (orig_height - fit_height) // 2 + fit_height))
ret_image = fit_image.resize((target_width, target_height), resize_sampler)
else:
ret_image = image.resize((target_width, target_height), resize_sampler)
return ret_image
# CLIP反推
import comfy.utils
+5 -1
View File
@@ -351,7 +351,7 @@ class easyLoader:
lora_path = None
if lora_path is not None:
log_node_info("Load LORA",f"{lora_name}: {model_strength}, {clip_strength}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
log_node_info("Load LORA",f"{lora_name}: model={model_strength:.3f}, clip={clip_strength:.3f}, LBW={lbw}, A={lbw_a}, B={lbw_b}")
if lbw:
lbw = lora["lbw"]
lbw_a = lora["lbw_a"]
@@ -432,10 +432,13 @@ class easyLoader:
clip_vision = None
lora_stack = []
# Check for model override
can_load_lora = True
# 判断是否存在 模型或Lora叠加xyplot, 若存在优先缓存第一个模型
# Determine whether there is a model or Lora overlapping xyplot, and if there is, prioritize caching the first model.
xy_model_id = next((x for x in prompt if str(prompt[x]["class_type"]) in ["easy XYInputs: ModelMergeBlocks",
"easy XYInputs: Checkpoint"]), None)
# This will find nodes that aren't actively connected to anything, and skip loading lora's for them.
xy_lora_id = next((x for x in prompt if str(prompt[x]["class_type"]) == "easy XYInputs: Lora"), None)
if xy_lora_id is not None:
can_load_lora = False
@@ -461,6 +464,7 @@ class easyLoader:
if optional_lora_stack is not None and can_load_lora:
for lora in optional_lora_stack:
# This is a subtle bit of code because it uses the model created by the last call, and passes it to the next call.
lora = {"lora_name": lora[0], "model": model, "clip": clip, "model_strength": lora[1],
"clip_strength": lora[2]}
model, clip = self.load_lora(lora)
+148
View File
@@ -0,0 +1,148 @@
"""
Math utility functions for formula evaluation
"""
import math
import re
def evaluate_formula(formula: str, a=0, b=0, c=0, d=0):
"""
计算字符串数学公式
支持的运算符和函数:
- 基本运算:+, -, *, /, //, %, **
- 比较运算:>, <, >=, <=, ==, !=
- 数学函数:abs, pow, round, ceil, floor, sqrt, exp, log, log10
- 三角函数:sin, cos, tan, asin, acos, atan
- 常量:pi, e
Args:
formula: 数学公式字符串,可以使用变量a、b、c、d
a: 变量a的值
b: 变量b的值
c: 变量c的值
d: 变量d的值
Returns:
如果任意输入为list则返回list[float],否则返回float
Examples:
>>> evaluate_formula("a + b", 1, 2)
3.0
>>> evaluate_formula("pow(a, 2)", 5)
25.0
>>> evaluate_formula("ceil(a / b)", 5, 2)
3.0
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 5, 3)
3.0
>>> evaluate_formula("(a>b)*b+(a<=b)*a", 2, 3)
2.0
"""
# 安全的数学函数白名单
safe_dict = {
# 基本运算
'abs': abs,
'pow': pow,
'round': round,
# 数学函数
'ceil': math.ceil,
'floor': math.floor,
'sqrt': math.sqrt,
'exp': math.exp,
'log': math.log,
'log10': math.log10,
# 三角函数
'sin': math.sin,
'cos': math.cos,
'tan': math.tan,
'asin': math.asin,
'acos': math.acos,
'atan': math.atan,
# 常量
'pi': math.pi,
'e': math.e,
}
# 判断是否有 list 输入
list_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if isinstance(v, (list, tuple))}
scalar_inputs = {k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items() if not isinstance(v, (list, tuple))}
def _eval_single(vals: dict) -> float:
env = dict(safe_dict)
env.update({k: float(v) for k, v in vals.items()})
try:
result = eval(formula, {"__builtins__": {}}, env)
return float(result)
except Exception as e:
raise ValueError(f"公式计算错误: {str(e)}")
if not list_inputs:
# 全是标量
return _eval_single({k: v for k, v in {'a': a, 'b': b, 'c': c, 'd': d}.items()})
# 有 list 输入,逐元素计算
max_len = max(len(v) for v in list_inputs.values())
results = []
for i in range(max_len):
vals = {k: float(v) for k, v in scalar_inputs.items()}
for k, v in list_inputs.items():
vals[k] = float(v[i] if i < len(v) else v[-1])
results.append(_eval_single(vals))
return results
def ceil_value(value: float) -> int:
"""向上取整"""
return math.ceil(value)
def floor_value(value: float) -> int:
"""向下取整"""
return math.floor(value)
def round_value(value: float, decimals: int = 0) -> float:
"""
四舍五入
Args:
value: 要取整的值
decimals: 保留小数位数
Returns:
四舍五入后的值
"""
return round(value, decimals)
def power(base: float, exponent: float) -> float:
"""计算幂运算"""
return math.pow(base, exponent)
def sqrt_value(value: float) -> float:
"""计算平方根"""
if value < 0:
raise ValueError("不能对负数求平方根")
return math.sqrt(value)
def add(a: float, b: float) -> float:
"""加法"""
return a + b
def subtract(a: float, b: float) -> float:
"""减法"""
return a - b
def multiply(a: float, b: float) -> float:
"""乘法"""
return a * b
def divide(a: float, b: float) -> float:
"""除法"""
if b == 0:
raise ValueError("除数不能为零")
return a / b
+55
View File
@@ -0,0 +1,55 @@
from server import PromptServer
from aiohttp import web
import time
import json
class MessageCancelled(Exception):
pass
class Message:
stash = {}
messages = {}
cancelled = False
@classmethod
def addMessage(cls, id, message):
if message == '__cancel__':
cls.messages = {}
cls.cancelled = True
elif message == '__start__':
cls.messages = {}
cls.stash = {}
cls.cancelled = False
else:
cls.messages[str(id)] = message
@classmethod
def waitForMessage(cls, id, period=0.1, asList=False):
sid = str(id)
while not (sid in cls.messages) and not ("-1" in cls.messages):
if cls.cancelled:
cls.cancelled = False
raise MessageCancelled()
time.sleep(period)
if cls.cancelled:
cls.cancelled = False
raise MessageCancelled()
message = cls.messages.pop(str(id), None) or cls.messages.pop("-1")
try:
if asList:
return [str(x.strip()) for x in message.split(",")]
else:
try:
return json.loads(message)
except ValueError:
return message
except ValueError:
print( f"ERROR IN MESSAGE - failed to parse '${message}' as ${'comma separated list of strings' if asList else 'string'}")
return [message] if asList else message
@PromptServer.instance.routes.post('/easyuse/message_callback')
async def message_callback(request):
post = await request.post()
Message.addMessage(post.get("id"), post.get("message"))
return web.json_response({})
+29
View File
@@ -0,0 +1,29 @@
import os
def resolve_output_file_path(output_root, output_file_path, file_name, file_extension):
"""Resolve a workflow-provided output path beneath ``output_root``.
Relative output directories remain supported, but are interpreted relative
to ComfyUI's configured output directory rather than the process working
directory. Resolving both paths prevents ``..`` components and existing
symlinks from escaping the allowed root.
"""
output_root = os.path.realpath(output_root)
requested_directory = output_file_path
if not os.path.isabs(requested_directory):
requested_directory = os.path.join(output_root, requested_directory)
candidate = os.path.realpath(
os.path.join(requested_directory, f"{file_name}.{file_extension}")
)
try:
is_within_output = os.path.commonpath((output_root, candidate)) == output_root
except ValueError:
# Different Windows drives and paths containing null bytes are unsafe.
is_within_output = False
if not is_within_output:
raise ValueError("Saving outside the ComfyUI output directory is not allowed")
return candidate
+25 -5
View File
@@ -35,9 +35,13 @@ import sys
import importlib.util
import importlib.metadata
import comfy.model_management as mm
import logging
import gc
from packaging import version
from server import PromptServer
LOG = logging.getLogger(__name__)
def is_package_installed(package):
try:
module = importlib.util.find_spec(package)
@@ -82,6 +86,7 @@ def compare_revision(num):
if not comfy_ui_revision:
comfy_ui_revision = get_comfyui_revision()
return True if comfy_ui_revision == 'Unknown' or int(comfy_ui_revision) >= num else False
def find_tags(string: str, sep="/") -> list[str]:
"""
find tags from string use the sep for split
@@ -217,14 +222,15 @@ def get_local_filepath(url, dirname, local_file_name=None):
except Exception as e:
use_mirror = True
url = url.replace('huggingface.co', 'hf-mirror.com')
print(f'无法从huggingface下载,正在尝试从 {url} 下载...')
PromptServer.instance.send_sync("easyuse-toast", {'content': f'无法连接huggingface,正在尝试从 {url} 下载...', 'duration': 10000})
print(f'Unable to download from huggingface, trying mirror: {url}')
PromptServer.instance.send_sync("easyuse-toast", {'content': f'Unable to connect to huggingface, trying mirror: {url}', 'duration': 10000})
try:
download_url_to_file(url, destination)
except Exception as err:
error_msg = str(err.args[0]) if err.args else str(err)
PromptServer.instance.send_sync("easyuse-toast",
{'content': f'无法从 {url} 下载模型', 'type':'error'})
raise Exception(f'无法从 {url} 下载,错误信息:{str(err.args[0])}')
{'content': f'Unable to download model from {url}', 'type':'error'})
raise Exception(f'Download failed. Original URL and mirror both failed.\nError: {error_msg}')
return destination
def to_lora_patch_dict(state_dict: dict) -> dict:
@@ -275,6 +281,20 @@ def getMetadata(filepath):
return header
def cleanGPUUsedForce():
from .cache import remove_cache
remove_cache("*")
gc.collect()
try:
import torch
except (ImportError, OSError, RuntimeError) as exc:
LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: torch import failed: %s", exc)
else:
try:
if torch.cuda.is_available():
torch.cuda.synchronize()
except (AttributeError, OSError, RuntimeError) as exc:
LOG.debug("Skipping CUDA synchronize during cleanGPUUsedForce: %s", exc)
mm.unload_all_models()
mm.soft_empty_cache()
mm.soft_empty_cache()
+181 -9
View File
@@ -1,9 +1,13 @@
import re
import random
import os
import folder_paths
import yaml
import json
import os
import random
import re
from math import prod
import yaml
import folder_paths
from .log import log_node_info
easy_wildcard_dict = {}
@@ -34,16 +38,16 @@ def read_wildcard_dict(wildcard_path):
key = os.path.splitext(rel_path)[0].replace('\\', '/').lower()
try:
with open(file_path, 'r', encoding="ISO-8859-1") as f:
with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f:
lines = f.read().splitlines()
easy_wildcard_dict[key] = lines
except UnicodeDecodeError:
with open(file_path, 'r', encoding="UTF-8", errors="ignore") as f:
with open(file_path, 'r', encoding="ISO-8859-1") as f:
lines = f.read().splitlines()
easy_wildcard_dict[key] = lines
elif file.endswith('.yaml'):
file_path = os.path.join(root, file)
with open(file_path, 'r') as f:
with open(file_path, 'r', encoding="utf-8") as f:
yaml_data = yaml.load(f, Loader=yaml.FullLoader)
for k, v in yaml_data.items():
@@ -51,7 +55,7 @@ def read_wildcard_dict(wildcard_path):
elif file.endswith('.json'):
file_path = os.path.join(root, file)
try:
with open(file_path, 'r') as f:
with open(file_path, 'r', encoding="utf-8") as f:
json_data = json.load(f)
for key, value in json_data.items():
key = wildcard_normalize(key)
@@ -302,3 +306,171 @@ def process_with_loras(wildcard_opt, model, clip, title="Positive", seed=None, c
log_node_info("easy wildcards",f'{title}_decode: {pass1}')
return model, clip, pass2, pass1, show_wildcard_prompt, pipe_lora_stack
def expand_wildcard(keyword: str) -> tuple[str]:
"""传入文件通配符的关键词,从 easy_wildcard_dict 中获取通配符的所有选项。"""
global easy_wildcard_dict
if keyword in easy_wildcard_dict:
return tuple(easy_wildcard_dict[keyword])
elif '*' in keyword:
subpattern = keyword.replace('*', '.*').replace('+', r"\+")
total_pattern = []
for k, v in easy_wildcard_dict.items():
if re.match(subpattern, k) is not None:
total_pattern.extend(v)
if total_pattern:
return tuple(total_pattern)
elif '/' not in keyword:
return expand_wildcard(f"*/{keyword}")
def expand_options(options: str) -> tuple[str]:
"""传入去掉 {} 的选项。
展开选项通配符,返回该选项中的每一项,这里的每一项都是一个替换项。
不会对选项内容进行任何处理,即便存在空格或特殊符号,也会原样返回。"""
return tuple(options.split("|"))
def decimal_to_irregular(n, bases):
"""
将十进制数转换为不规则进制
:param n: 十进制数
:param bases: 各位置的基数列表,从低位到高位
:return: 不规则进制表示的列表,从低位到高位
"""
if n == 0:
return [0] * len(bases) if bases else [0]
digits = []
remaining = n
# 从低位到高位处理
for base in bases:
digit = remaining % base
digits.append(digit)
remaining = remaining // base
return digits
class WildcardProcessor:
"""通配符处理器
通配符格式:
+ option : {a|b}
+ wildcard: __keyword__ 通配符内容将从 Easy-Use 插件提供的 easy_wildcard_dict 中获取
"""
RE_OPTIONS = re.compile(r"{([^{}]*?)}")
RE_WILDCARD = re.compile(r"__([\w\s.\-+/*\\]+?)__")
RE_REPLACER = re.compile(r"{([^{}]*?)}|__([\w\s.\-+/*\\]+?)__")
# 将输入的提示词转化成符合 python str.format 要求格式的模板,并将 option 和 wildcard 按照顺序在模板中留下 {0}, {1} 等占位符
template: str
# option、wildcard 的替换项列表,按照在模板中出现的顺序排列,相同的替换项列表只保留第一份
replacers: dict[int, tuple[str]]
# 占位符的编号和替换项列表的索引的映射,占位符编号按照在模板中出现的顺序排列,方便减少替换项的存储占用
placeholder_mapping: dict[str, int] # placeholder_id => replacer_id
# 各替换项列表的项数,按照在模板中出现的顺序排列,提前计算,方便后续使用
placeholder_choices: dict[str, int] # placeholder_id => len(replacer)
def __init__(self, text: str):
self.__make_template(text)
self.__total = None
def random(self, seed=None) -> str:
"从所有可能性中随机获取一个"
if seed is not None:
random.seed(seed)
return self.getn(random.randint(0, self.total() - 1))
def getn(self, n: int) -> str:
"从所有可能性中获取第 n 个,以 self.total() 为周期循环"
n = n % self.total()
indice = decimal_to_irregular(n, self.placeholder_choices.values())
replacements = {
placeholder_id: self.replacers[self.placeholder_mapping[placeholder_id]][i]
for placeholder_id, i in zip(self.placeholder_mapping.keys(), indice)
}
return self.template.format(**replacements)
def getmany(self, limit: int, offset: int = 0) -> list[str]:
"""返回一组可能性组成的列表,为了避免结果太长导致内存占用超限,使用 limit 限制列表的长度,使用 offset 调整偏移。
若 limit 和 offset 的设置导致预期的结果长度超过剩下的实际长度,则会回到开头。
"""
return [self.getn(n) for n in range(offset, offset + limit)]
def total(self) -> int:
"计算可能性的数目"
if self.__total is None:
self.__total = prod(self.placeholder_choices.values())
return self.__total
def __make_template(self, text: str):
"""将输入的提示词转化成符合 python str.format 要求格式的模板,
并将 option 和 wildcard 按照顺序在模板中留下 {r0}, {r1} 等占位符,
即使遇到相同的 option 或 wildcard,留下的占位符编号也不同,从而使每项都独立变化。
"""
self.placeholder_mapping = {}
placeholder_id = 0
replacer_id = 0
replacers_rev = {} # replacers => id
blocks = []
# 记录所处理过的通配符末尾在文本中的位置,用于拼接完整的模板
tail = 0
for match in self.RE_REPLACER.finditer(text):
# 提取并展开通配符内容
m = match.group(0)
if m.startswith("{"):
choices = expand_options(m[1:-1])
elif m.startswith("__"):
keyword = m[2:-2].lower()
keyword = wildcard_normalize(keyword)
choices = expand_wildcard(keyword)
else:
raise ValueError(f"{m!r} is not a wildcard or option")
# 记录通配符的替换项列表和ID,相同的通配符只保留第一个
if choices not in replacers_rev:
replacers_rev[choices] = replacer_id
replacer_id += 1
# 拼接通配符前方文本
start, end = match.span()
blocks.append(text[tail:start])
tail = end
# 将通配符替换为占位符,并记录占位符和替换项列表的索引的映射
blocks.append(f"{{r{placeholder_id}}}")
self.placeholder_mapping[f"r{placeholder_id}"] = replacers_rev[choices]
placeholder_id += 1
if tail < len(text):
blocks.append(text[tail:])
self.template = "".join(blocks)
self.replacers = {v: k for k, v in replacers_rev.items()}
self.placeholder_choices = {
placeholder_id: len(self.replacers[replacer_id])
for placeholder_id, replacer_id in self.placeholder_mapping.items()
}
def test_option():
text = "{|a|b|c}"
answer = ["", "a", "b", "c"]
p = WildcardProcessor(text)
assert p.total() == len(answer)
assert p.getn(0) == answer[0]
assert p.getmany(4) == answer
assert p.getmany(4, 1) == answer[1:]
def test_same():
text = "{a|b},{a|b}"
answer = ["a,a", "b,a", "a,b", "b,b"]
p = WildcardProcessor(text)
assert p.total() == len(answer)
assert p.getn(0) == answer[0]
assert p.getmany(4) == answer
assert p.getmany(4, 1) == answer[1:]
+133 -35
View File
@@ -8,6 +8,7 @@ from .log import log_node_warn
from ..modules.layer_diffuse import LayerDiffuse
from ..config import RESOURCES_DIR
from nodes import CLIPTextEncode
import pprint
try:
from comfy_extras.nodes_flux import FluxGuidance
except:
@@ -52,7 +53,7 @@ class easyXYPlot():
plot_image_vars[value_type] = value
if value_type in ["seed", "Seeds++ Batch"]:
value_label = f"{value}"
value_label = f"seed: {value}"
else:
value_label = f"{value_type}: {value}"
@@ -62,8 +63,10 @@ class easyXYPlot():
if value_type in ['Lora', 'Checkpoint']:
arr = value.split(',')
model_name = os.path.basename(os.path.splitext(arr[0])[0])
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr[3]) > 2 else ''
value_label = f"{model_name}{trigger_words}"
trigger_words = ' ' + arr[3] if value_type == 'Lora' and len(arr) > 3 and len(arr[3]) > 2 else ''
lora_weight = float(arr[1]) if value_type == 'Lora' and len(arr) > 1 else 0
lora_weight_desc = f" w:{lora_weight:.2f}" if value_type == 'Lora' and lora_weight != 1.0 else ''
value_label = f"{model_name[:25]}{lora_weight_desc}{trigger_words}"
if value_type in ["ModelMergeBlocks"]:
if ":" in value:
@@ -118,24 +121,32 @@ class easyXYPlot():
def calculate_background_dimensions(self):
border_size = int((self.max_width // 8) * 1.5) if self.y_type != "None" or self.x_type != "None" else 0
bg_width = self.num_cols * (self.max_width + self.grid_spacing) - self.grid_spacing + border_size * (
self.y_type != "None")
bg_height = self.num_rows * (self.max_height + self.grid_spacing) - self.grid_spacing + border_size * (
self.x_type != "None")
# Add space at the bottom of the image for common informaiton about the image
bg_height = bg_height + (border_size*2)
# print(f"Grid Size: width = {bg_width} height = {bg_height} border_size = {border_size}")
x_offset_initial = border_size if self.y_type != "None" else 0
y_offset = border_size if self.x_type != "None" else 0
return bg_width, bg_height, x_offset_initial, y_offset
def adjust_font_size(self, text, initial_font_size, label_width):
font = self.get_font(initial_font_size, self.custom_font)
text_width = font.getbbox(text)
# pprint.pp(f"Initial font size: {initial_font_size}, text: {text}, text_width: {text_width}")
if text_width and text_width[2]:
text_width = text_width[2]
scaling_factor = 0.9
if text_width > (label_width * scaling_factor):
# print(f"Adjusting font size from {initial_font_size} to fit text width {text_width} into label width {label_width} scaling_factor {scaling_factor}")
return int(initial_font_size * (label_width / text_width) * scaling_factor)
else:
return initial_font_size
@@ -144,15 +155,22 @@ class easyXYPlot():
_, _, width, height = d.textbbox((0, 0), text=text, font=font)
return width, height
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10):
label_width = img.width if is_x_label else img.height
def create_label(self, img, text, initial_font_size, is_x_label=True, max_font_size=70, min_font_size=10, label_width=0, label_height=0):
# if the label_width is specified, leave it along. Otherwise do the old logic.
if label_width == 0:
label_width = img.width if is_x_label else img.height
text_lines = text.split('\n')
longest_line = max(text_lines, key=len)
# Adjust font size
font_size = self.adjust_font_size(text, initial_font_size, label_width)
font_size = self.adjust_font_size(longest_line, initial_font_size, label_width)
font_size = min(max_font_size, font_size) # Ensure font isn't too large
font_size = max(min_font_size, font_size) # Ensure font isn't too small
label_height = int(font_size * 1.5) if is_x_label else font_size
if label_height == 0:
label_height = int(font_size * 1.5) if is_x_label else font_size
label_bg = Image.new('RGBA', (label_width, label_height), color=(255, 255, 255, 0))
d = ImageDraw.Draw(label_bg)
@@ -166,7 +184,7 @@ class easyXYPlot():
text = text + '...'
# Compute text width and height for multi-line text
text_lines = text.split('\n')
text_widths, text_heights = zip(*[self.textsize(d, line, font=font) for line in text_lines])
max_text_width = max(text_widths)
total_text_height = sum(text_heights)
@@ -195,8 +213,7 @@ class easyXYPlot():
clip = clip if clip is not None else plot_image_vars["clip"]
steps = plot_image_vars['steps'] if "steps" in plot_image_vars else 1
sd_version = get_sd_version(plot_image_vars['model'])
sd_version = get_sd_version(plot_image_vars['model'])
# 高级用法
if plot_image_vars["x_node_type"] == "advanced" or plot_image_vars["y_node_type"] == "advanced":
if self.x_type == "Seeds++ Batch" or self.y_type == "Seeds++ Batch":
@@ -347,17 +364,34 @@ class easyXYPlot():
# Lora
if self.x_type == "Lora" or self.y_type == "Lora":
# print(f"Lora: {x_value} {y_value}")
model = model if model is not None else plot_image_vars["model"]
clip = clip if clip is not None else plot_image_vars["clip"]
# Build lora_stack from both X and Y axes if both are LoRA types
lora_stack = []
# Add X axis LoRA if present
if self.x_type == "Lora":
lora_name, lora_model_strength, lora_clip_strength, _ = x_value.split(",")
lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)})
# Add Y axis LoRA if present
if self.y_type == "Lora":
lora_name, lora_model_strength, lora_clip_strength, _ = y_value.split(",")
lora_stack.append({"lora_name": lora_name, "model": model, "clip": clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)})
# print(f"new_lora_stack: {new_lora_stack}")
xy_values = x_value if self.x_type == "Lora" else y_value
lora_name, lora_model_strength, lora_clip_strength, _ = xy_values.split(",")
lora_stack = [{"lora_name": lora_name, "model": model, "clip" :clip, "model_strength": float(lora_model_strength), "clip_strength": float(lora_clip_strength)}]
if 'lora_stack' in plot_image_vars:
lora_stack = lora_stack + plot_image_vars['lora_stack']
if lora_stack is not None and lora_stack != []:
for lora in lora_stack:
# Each generation of the model, must use the reference to previously created model / clip objects.
lora['model'] = model
lora['clip'] = clip
model, clip = self.easyCache.load_lora(lora)
# 提示词
@@ -406,7 +440,8 @@ class easyXYPlot():
strength = item[2]
start_percent = item[3]
end_percent = item[4]
positive, negative = easyControlnet().apply(control_net_name, image, positive, negative, strength, start_percent, end_percent, None, 1)
provided_control_net = item[5] if len(item) > 5 else None
positive, negative = easyControlnet().apply(control_net_name, image, positive, negative, strength, start_percent, end_percent, provided_control_net, 1)
# Flux guidance
if self.x_type == "Flux Guidance" or self.y_type == "Flux Guidance":
positive = plot_image_vars["positive_cond"] if "positive" in plot_image_vars else None
@@ -464,6 +499,7 @@ class easyXYPlot():
plot_image_vars['negative_weight_interpretation'], w_max=1.0,
apply_to_pooled="enable", a1111_prompt_style=a1111_prompt_style, steps=steps)
model = model if model is not None else plot_image_vars["model"]
vae = vae if vae is not None else plot_image_vars["vae"]
positive = positive if positive is not None else plot_image_vars["positive_cond"]
@@ -551,28 +587,40 @@ class easyXYPlot():
def get_labels_and_sample(self, plot_image_vars, latent_image, preview_latent, start_step, last_step,
force_full_denoise, disable_noise):
for x_index, x_value in enumerate(self.x_values):
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value,
x_index)
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
if self.y_type != 'None':
# Handle X-only variation (Y is "None")
if self.y_type == 'None':
for x_index, x_value in enumerate(self.x_values):
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index)
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
disable_noise, start_step, last_step, force_full_denoise, x_value)
self.num += 1
# Handle Y-only variation (X is "None")
elif self.x_type == 'None':
for y_index, y_value in enumerate(self.y_values):
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index)
self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
disable_noise, start_step, last_step, force_full_denoise, y_value=y_value)
self.num += 1
# Handle both X and Y variation
else:
for x_index, x_value in enumerate(self.x_values):
plot_image_vars, x_value_label = self.define_variable(plot_image_vars, self.x_type, x_value, x_index)
self.x_label = self.update_label(self.x_label, x_value_label, len(self.x_values))
for y_index, y_value in enumerate(self.y_values):
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value,
y_index)
plot_image_vars, y_value_label = self.define_variable(plot_image_vars, self.y_type, y_value, y_index)
self.y_label = self.update_label(self.y_label, y_value_label, len(self.y_values))
# ttNl(f'{CC.GREY}X: {x_value_label}, Y: {y_value_label}').t(
# f'Plot Values {self.num}/{self.total} ->').p()
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list,
disable_noise, start_step, last_step, force_full_denoise, x_value, y_value)
self.num += 1
else:
# ttNl(f'{CC.GREY}X: {x_value_label}').t(f'Plot Values {self.num}/{self.total} ->').p()
self.image_list, self.max_width, self.max_height, self.latents_plot = self.sample_plot_image(
plot_image_vars, latent_image, preview_latent, self.latents_plot, self.image_list, disable_noise,
start_step, last_step, force_full_denoise, x_value)
self.num += 1
# Rearrange latent array to match preview image grid
self.latents_plot = self.rearrange_tensors(self.latents_plot, self.num_cols, self.num_rows)
@@ -582,11 +630,10 @@ class easyXYPlot():
return self.latents_plot
def plot_images_and_labels(self):
# Calculate the background dimensions
def plot_images_and_labels(self, plot_image_vars):
bg_width, bg_height, x_offset_initial, y_offset = self.calculate_background_dimensions()
# Create the white background image
background = Image.new('RGBA', (int(bg_width), int(bg_height)), color=(255, 255, 255, 255))
output_image = []
@@ -618,4 +665,55 @@ class easyXYPlot():
y_offset += img.height + self.grid_spacing
return (self.sampler.pil2tensor(background), output_image)
# lookup used models in the image
common_label = ""
# Update to add a function to do the heavy lifting. Parameters are plot_image_vars name, label to use, names of the axis,
# pprint.pp(plot_image_vars)
# We don't process LORAs here because there can be multiple of them.
labels = [
{"id": "ckpt_name", "id_desc": "ckpt", "axis_type" : "Checkpoint"},
{"id": "vae_name", "id_desc": '', "axis_type" : "vae_name"},
{"id": "sampler_name", "id_desc": "sampler", "axis_type" : "Sampler"},
{"id": "scheduler", "id_desc": '', "axis_type" : "Scheduler"},
{"id": "steps", "id_desc": '', "axis_type" : "Steps"},
{"id": "Flux Guidance", "id_desc": 'guidance', "axis_type" : "Flux Guidance"},
{"id": "seed", "id_desc": '', "axis_type" : "Seeds++ Batch"}
]
for item in labels:
# Only add the label if it's not one of the axis
# print(f"Checking item: {item['id']} axis_type {item['axis_type']} x_type: {self.x_type} y_type: {self.y_type}")
if self.x_type != item['axis_type'] and self.y_type != item['axis_type']:
common_label += self.add_common_label(item['id'], plot_image_vars, item['id_desc'])
common_label += f"\n"
if plot_image_vars['lora_stack'] is not None and plot_image_vars['lora_stack'] != []:
# print(f"lora_stack: {plot_image_vars['lora_stack']}")
for lora in plot_image_vars['lora_stack']:
lora_name = lora['lora_name']
lora_weight = lora['model_strength']
if lora_name is not None and len(lora_name) > 0 and lora_weight > 0:
common_label += f"LORA: {lora_name} weight: {lora_weight:.2f} \n"
common_label = common_label.strip()
if len(common_label) > 0:
label_height = background.height - y_offset
label_bg = self.create_label(background, common_label, int(48 * background.width / 512), label_width=background.width, label_height=label_height)
label_x = (background.width - label_bg.width) // 2
label_y = y_offset
# print(f"Adding common label: {common_label} x = {label_x} y = {label_y}")
background.alpha_composite(label_bg, (label_x, label_y))
return (self.sampler.pil2tensor(background), output_image)
def add_common_label(self, tag, plot_image_vars, description = ''):
label = ''
if description == '': description = tag
if tag in plot_image_vars and plot_image_vars[tag] is not None and plot_image_vars[tag] != 'None':
label += f"{description}: {plot_image_vars[tag]} "
# print(f"add_common_label: {tag} description: {description} label: {label}" )
return label
+445 -82
View File
@@ -1,13 +1,36 @@
import math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.utils.checkpoint as checkpoint
from einops import rearrange
from PIL import Image, ImageFilter, ImageOps
from timm.layers import DropPath, to_2tuple, trunc_normal_
import torch.utils.checkpoint as checkpoint
import numpy as np
from timm.models.layers import DropPath, to_2tuple, trunc_normal_
from PIL import Image, ImageOps
from torchvision import transforms
import numpy as np
import random
import cv2
import os
import subprocess
import time
import tempfile
def set_random_seed(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed(seed)
torch.cuda.manual_seed_all(seed)
torch.backends.cudnn.deterministic = True
torch.backends.cudnn.benchmark = False
set_random_seed(9)
torch.set_float32_matmul_precision('highest')
class Mlp(nn.Module):
""" Multilayer perceptron."""
@@ -247,6 +270,7 @@ class PatchMerging(nn.Module):
dim (int): Number of input channels.
norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
"""
def __init__(self, dim, norm_layer=nn.LayerNorm):
super().__init__()
self.dim = dim
@@ -495,7 +519,8 @@ class SwinTransformer(nn.Module):
patch_size = to_2tuple(patch_size)
patches_resolution = [pretrain_img_size[0] // patch_size[0], pretrain_img_size[1] // patch_size[1]]
self.absolute_pos_embed = nn.Parameter(torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1]))
self.absolute_pos_embed = nn.Parameter(
torch.zeros(1, embed_dim, patches_resolution[0], patches_resolution[1]))
trunc_normal_(self.absolute_pos_embed, std=.02)
self.pos_drop = nn.Dropout(p=drop_rate)
@@ -550,7 +575,6 @@ class SwinTransformer(nn.Module):
for param in m.parameters():
param.requires_grad = False
def forward(self, x):
x = self.patch_embed(x)
@@ -559,18 +583,16 @@ class SwinTransformer(nn.Module):
if self.ape:
# interpolate the position embedding to the corresponding size
absolute_pos_embed = F.interpolate(self.absolute_pos_embed, size=(Wh, Ww), mode='bicubic')
x = (x + absolute_pos_embed) # B Wh*Ww C
x = (x + absolute_pos_embed) # B Wh*Ww C
outs = [x.contiguous()]
x = x.flatten(2).transpose(1, 2)
x = self.pos_drop(x)
for i in range(self.num_layers):
layer = self.layers[i]
x_out, H, W, x, Wh, Ww = layer(x, Wh, Ww)
if i in self.out_indices:
norm_layer = getattr(self, f'norm{i}')
x_out = norm_layer(x_out)
@@ -578,16 +600,9 @@ class SwinTransformer(nn.Module):
out = x_out.view(-1, H, W, self.num_features[i]).permute(0, 3, 1, 2).contiguous()
outs.append(out)
return tuple(outs)
def get_activation_fn(activation):
"""Return an activation function given a string"""
if activation == "gelu":
@@ -622,6 +637,43 @@ def patches2image(x):
"""(hg wg b) c h w -> b c (hg h) (wg w)"""
x = rearrange(x, '(hg wg b) c h w -> b c (hg h) (wg w)', hg=2, wg=2)
return x
class PositionEmbeddingSine:
def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
super().__init__()
self.num_pos_feats = num_pos_feats
self.temperature = temperature
self.normalize = normalize
if scale is not None and normalize is False:
raise ValueError("normalize should be True if scale is passed")
if scale is None:
scale = 2 * math.pi
self.scale = scale
self.dim_t = torch.arange(0, self.num_pos_feats, dtype=torch.float32)
def __call__(self, b, h, w):
device = self.dim_t.device
mask = torch.zeros([b, h, w], dtype=torch.bool, device=device)
assert mask is not None
not_mask = ~mask
y_embed = not_mask.cumsum(dim=1, dtype=torch.float32)
x_embed = not_mask.cumsum(dim=2, dtype=torch.float32)
if self.normalize:
eps = 1e-6
y_embed = (y_embed - 0.5) / (y_embed[:, -1:, :] + eps) * self.scale
x_embed = (x_embed - 0.5) / (x_embed[:, :, -1:] + eps) * self.scale
dim_t = self.temperature ** (2 * (self.dim_t.to(device) // 2) / self.num_pos_feats)
pos_x = x_embed[:, :, :, None] / dim_t
pos_y = y_embed[:, :, :, None] / dim_t
pos_x = torch.stack((pos_x[:, :, :, 0::2].sin(), pos_x[:, :, :, 1::2].cos()), dim=4).flatten(3)
pos_y = torch.stack((pos_y[:, :, :, 0::2].sin(), pos_y[:, :, :, 1::2].cos()), dim=4).flatten(3)
return torch.cat((pos_y, pos_x), dim=3).permute(0, 3, 1, 2)
class PositionEmbeddingSine:
def __init__(self, num_pos_feats=64, temperature=10000, normalize=False, scale=None):
super().__init__()
@@ -688,13 +740,15 @@ class MCLM(nn.Module):
l: 4,c,h,w
g: 1,c,h,w
"""
self.p_poses = []
self.g_pos = None
b, c, h, w = l.size()
# 4,c,h,w -> 1,c,2h,2w
concated_locs = rearrange(l, '(hg wg b) c h w -> b c (hg h) (wg w)', hg=2, wg=2)
pools = []
for pool_ratio in self.pool_ratios:
# b,c,h,w
# b,c,h,w
tgt_hw = (round(h / pool_ratio), round(w / pool_ratio))
pool = F.adaptive_avg_pool2d(concated_locs, tgt_hw)
pools.append(rearrange(pool, 'b c h w -> (h w) b c'))
@@ -712,11 +766,9 @@ class MCLM(nn.Module):
self.p_poses = self.p_poses.to(device)
self.g_pos = self.g_pos.to(device)
# attention between glb (q) & multisensory concated-locs (k,v)
g_hw_b_c = rearrange(g, 'b c h w -> (h w) b c')
g_hw_b_c = g_hw_b_c + self.dropout1(self.attention[0](g_hw_b_c + self.g_pos, pools + self.p_poses, pools)[0])
g_hw_b_c = self.norm1(g_hw_b_c)
g_hw_b_c = g_hw_b_c + self.dropout2(self.linear2(self.dropout(self.activation(self.linear1(g_hw_b_c)).clone())))
@@ -740,13 +792,6 @@ class MCLM(nn.Module):
return rearrange(l, "(h w) b c -> b c h w", h=h, w=w) ## (5,c,h*w)
class MCRM(nn.Module):
def __init__(self, d_model, num_heads, pool_ratios=[4, 8, 16], h=None):
super(MCRM, self).__init__()
@@ -852,91 +897,390 @@ class BEN_Base(nn.Module):
if isinstance(m, nn.GELU) or isinstance(m, nn.Dropout):
m.inplace = True
@torch.inference_mode()
@torch.autocast(device_type="cuda", dtype=torch.float16)
def forward(self, x):
device = x.device
shallow = self.shallow(x)
glb = rescale_to(x, scale_factor=0.5, interpolation='bilinear')
loc = image2patches(x)
input = torch.cat((loc, glb), dim=0)
feature = self.backbone(input)
e5 = self.output5(feature[4]) # (5,128,16,16)
e4 = self.output4(feature[3]) # (5,128,32,32)
e3 = self.output3(feature[2]) # (5,128,64,64)
e2 = self.output2(feature[1]) # (5,128,128,128)
e1 = self.output1(feature[0]) # (5,128,128,128)
loc_e5, glb_e5 = e5.split([4, 1], dim=0)
e5 = self.multifieldcrossatt(loc_e5, glb_e5) # (4,128,16,16)
real_batch = x.size(0)
e4, tokenattmap4 = self.dec_blk4(e4 + resize_as(e5, e4))
e4 = self.conv4(e4)
e3, tokenattmap3 = self.dec_blk3(e3 + resize_as(e4, e3))
e3 = self.conv3(e3)
e2, tokenattmap2 = self.dec_blk2(e2 + resize_as(e3, e2))
e2 = self.conv2(e2)
e1, tokenattmap1 = self.dec_blk1(e1 + resize_as(e2, e1))
e1 = self.conv1(e1)
loc_e1, glb_e1 = e1.split([4, 1], dim=0)
output1_cat = patches2image(loc_e1) # (1,128,256,256)
output1_cat = output1_cat + resize_as(glb_e1, output1_cat)
final_output = self.insmask_head(output1_cat) # (1,128,256,256)
final_output = final_output + resize_as(shallow, final_output)
final_output = self.upsample1(rescale_to(final_output))
final_output = rescale_to(final_output + resize_as(shallow, final_output))
final_output = self.upsample2(final_output)
final_output = self.output(final_output)
shallow_batch = self.shallow(x)
glb_batch = rescale_to(x, scale_factor=0.5, interpolation='bilinear')
return final_output.sigmoid()
final_input = None
for i in range(real_batch):
start = i * 4
end = (i + 1) * 4
loc_batch = image2patches(x[i, :, :, :].unsqueeze(dim=0))
input_ = torch.cat((loc_batch, glb_batch[i, :, :, :].unsqueeze(dim=0)), dim=0)
@torch.no_grad()
def inference(self,image):
image, h, w,original_image = rgb_loader_refiner(image)
if final_input == None:
final_input = input_
else:
final_input = torch.cat((final_input, input_), dim=0)
img_tensor = img_transform(image).unsqueeze(0).to(next(self.parameters()).device)
features = self.backbone(final_input)
outputs = []
res = self.forward(img_tensor)
for i in range(real_batch):
start = i * 5
end = (i + 1) * 5
pred_array = postprocess_image(res, im_size=[w, h])
f4 = features[4][start:end, :, :, :] # shape: [5, C, H, W]
f3 = features[3][start:end, :, :, :]
f2 = features[2][start:end, :, :, :]
f1 = features[1][start:end, :, :, :]
f0 = features[0][start:end, :, :, :]
e5 = self.output5(f4)
e4 = self.output4(f3)
e3 = self.output3(f2)
e2 = self.output2(f1)
e1 = self.output1(f0)
loc_e5, glb_e5 = e5.split([4, 1], dim=0)
e5 = self.multifieldcrossatt(loc_e5, glb_e5) # (4,128,16,16)
mask_image = Image.fromarray(pred_array, mode='L')
e4, tokenattmap4 = self.dec_blk4(e4 + resize_as(e5, e4))
e4 = self.conv4(e4)
e3, tokenattmap3 = self.dec_blk3(e3 + resize_as(e4, e3))
e3 = self.conv3(e3)
e2, tokenattmap2 = self.dec_blk2(e2 + resize_as(e3, e2))
e2 = self.conv2(e2)
e1, tokenattmap1 = self.dec_blk1(e1 + resize_as(e2, e1))
e1 = self.conv1(e1)
blurred_mask = mask_image.filter(ImageFilter.GaussianBlur(radius=1))
loc_e1, glb_e1 = e1.split([4, 1], dim=0)
original_image_rgba = original_image.convert("RGBA")
output1_cat = patches2image(loc_e1) # (1,128,256,256)
foreground = original_image_rgba.copy()
# add glb feat in
output1_cat = output1_cat + resize_as(glb_e1, output1_cat)
# merge
final_output = self.insmask_head(output1_cat) # (1,128,256,256)
# shallow feature merge
shallow = shallow_batch[i, :, :, :].unsqueeze(dim=0)
final_output = final_output + resize_as(shallow, final_output)
final_output = self.upsample1(rescale_to(final_output))
final_output = rescale_to(final_output + resize_as(shallow, final_output))
final_output = self.upsample2(final_output)
final_output = self.output(final_output)
mask = final_output.sigmoid()
outputs.append(mask)
foreground.putalpha(blurred_mask)
return torch.cat(outputs, dim=0)
return blurred_mask, foreground
def loadcheckpoints(self,model_path):
def loadcheckpoints(self, model_path):
model_dict = torch.load(model_path, map_location="cpu", weights_only=True)
self.load_state_dict(model_dict['model_state_dict'], strict=True)
del model_path
def inference(self, image, refine_foreground=False):
set_random_seed(9)
# image = ImageOps.exif_transpose(image)
if isinstance(image, Image.Image):
image, h, w, original_image = rgb_loader_refiner(image)
if torch.cuda.is_available():
img_tensor = img_transform(image).unsqueeze(0).to(next(self.parameters()).device)
else:
img_tensor = img_transform32(image).unsqueeze(0).to(next(self.parameters()).device)
with torch.no_grad():
res = self.forward(img_tensor)
# Show Results
if refine_foreground == True:
pred_pil = transforms.ToPILImage()(res.squeeze())
image_masked = refine_foreground_process(original_image, pred_pil)
mask = pred_pil.resize(original_image.size)
image_masked.putalpha(mask)
return mask, image_masked
else:
alpha = postprocess_image(res, im_size=[w, h])
pred_pil = transforms.ToPILImage()(alpha)
mask = pred_pil.resize(original_image.size)
original_image.putalpha(mask)
# mask = Image.fromarray(alpha)
return mask, original_image
def segment_video(self, video_path, output_path="./", fps=0, refine_foreground=False, batch=1,
print_frames_processed=True, webm=False, rgb_value=(0, 255, 0)):
"""
Segments the given video to extract the foreground (with alpha) from each frame
and saves the result as either a WebM video (with alpha channel) or MP4 (with a
color background).
Args:
video_path (str):
Path to the input video file.
output_path (str, optional):
Directory (or full path) where the output video and/or files will be saved.
Defaults to "./".
fps (int, optional):
The frames per second (FPS) to use for the output video. If 0 (default), the
original FPS of the input video is used. Otherwise, overrides it.
refine_foreground (bool, optional):
Whether to run an additional “refine foreground” process on each frame.
Defaults to False.
batch (int, optional):
Number of frames to process at once (inference batch size). Large batch sizes
may require more GPU memory. Defaults to 1.
print_frames_processed (bool, optional):
If True (default), prints progress (how many frames have been processed) to
the console.
webm (bool, optional):
If True (default), exports a WebM video with alpha channel (VP9 / yuva420p).
If False, exports an MP4 video composited over a solid color background.
rgb_value (tuple, optional):
The RGB background color (e.g., green screen) used to composite frames when
saving to MP4. Defaults to (0, 255, 0).
Returns:
None. Writes the output video(s) to disk in the specified format.
"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
raise IOError(f"Cannot open video: {video_path}")
original_fps = cap.get(cv2.CAP_PROP_FPS)
original_fps = 30 if original_fps == 0 else original_fps
fps = original_fps if fps == 0 else fps
ret, first_frame = cap.read()
if not ret:
raise ValueError("No frames found in the video.")
height, width = first_frame.shape[:2]
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
foregrounds = []
frame_idx = 0
processed_count = 0
batch_frames = []
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
while True:
ret, frame = cap.read()
if not ret:
if batch_frames:
batch_results = self.inference(batch_frames, refine_foreground)
if isinstance(batch_results, Image.Image):
foregrounds.append(batch_results)
else:
foregrounds.extend(batch_results)
if print_frames_processed:
print(f"Processed frames {frame_idx - len(batch_frames) + 1} to {frame_idx} of {total_frames}")
break
# Process every frame instead of using intervals
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
pil_frame = Image.fromarray(frame_rgb)
batch_frames.append(pil_frame)
if len(batch_frames) == batch:
batch_results = self.inference(batch_frames, refine_foreground)
if isinstance(batch_results, Image.Image):
foregrounds.append(batch_results)
else:
foregrounds.extend(batch_results)
if print_frames_processed:
print(f"Processed frames {frame_idx - batch + 1} to {frame_idx} of {total_frames}")
batch_frames = []
processed_count += batch
frame_idx += 1
if webm:
alpha_webm_path = os.path.join(output_path, "foreground.webm")
pil_images_to_webm_alpha(foregrounds, alpha_webm_path, fps=original_fps)
else:
cap.release()
fg_output = os.path.join(output_path, 'foreground.mp4')
pil_images_to_mp4(foregrounds, fg_output, fps=original_fps, rgb_value=rgb_value)
cv2.destroyAllWindows()
try:
fg_audio_output = os.path.join(output_path, 'foreground_output_with_audio.mp4')
add_audio_to_video(fg_output, video_path, fg_audio_output)
except Exception as e:
print("No audio found in the original video")
print(e)
def rgb_loader_refiner(original_image):
h, w = original_image.size
def rgb_loader_refiner( original_image):
h, w = original_image.size
# # Apply EXIF orientation
image = ImageOps.exif_transpose(original_image)
# Convert to RGB if necessary
if image.mode != 'RGB':
image = image.convert('RGB')
image = original_image
# Convert to RGB if necessary
if image.mode != 'RGB':
image = image.convert('RGB')
# Resize the image
image = image.resize((1024, 1024), resample=Image.LANCZOS)
# Resize the image
image = image.resize((1024, 1024), resample=Image.LANCZOS)
return image.convert('RGB'), h, w, original_image
return image.convert('RGB'), h, w,original_image
# Define the image transformation
img_transform = transforms.Compose([
transforms.ToTensor(),
transforms.ConvertImageDtype(torch.float16),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
img_transform32 = transforms.Compose([
transforms.ToTensor(),
transforms.ConvertImageDtype(torch.float32),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
def pil_images_to_mp4(images, output_path, fps=24, rgb_value=(0, 255, 0)):
"""
Converts an array of PIL images to an MP4 video.
Args:
images: List of PIL images
output_path: Path to save the MP4 file
fps: Frames per second (default: 24)
rgb_value: Background RGB color tuple (default: green (0, 255, 0))
"""
if not images:
raise ValueError("No images provided to convert to MP4.")
width, height = images[0].size
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
video_writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
for image in images:
# If image has alpha channel, composite onto the specified background color
if image.mode == 'RGBA':
# Create background image with specified RGB color
background = Image.new('RGB', image.size, rgb_value)
background = background.convert('RGBA')
# Composite the image onto the background
image = Image.alpha_composite(background, image)
image = image.convert('RGB')
else:
# Ensure RGB format for non-alpha images
image = image.convert('RGB')
# Convert to OpenCV format and write
open_cv_image = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR)
video_writer.write(open_cv_image)
video_writer.release()
def pil_images_to_webm_alpha(images, output_path, fps=30):
"""
Converts a list of PIL RGBA images to a VP9 .webm video with alpha channel.
NOTE: Not all players will display alpha in WebM.
Browsers like Chrome/Firefox typically do support VP9 alpha.
"""
if not images:
raise ValueError("No images provided for WebM with alpha.")
# Ensure output directory exists
os.makedirs(os.path.dirname(output_path), exist_ok=True)
with tempfile.TemporaryDirectory() as tmpdir:
# Save frames as PNG (with alpha)
for idx, img in enumerate(images):
if img.mode != "RGBA":
img = img.convert("RGBA")
out_path = os.path.join(tmpdir, f"{idx:06d}.png")
img.save(out_path, "PNG")
# Construct ffmpeg command
# -c:v libvpx-vp9 => VP9 encoder
# -pix_fmt yuva420p => alpha-enabled pixel format
# -auto-alt-ref 0 => helps preserve alpha frames (libvpx quirk)
ffmpeg_cmd = [
"ffmpeg", "-y",
"-framerate", str(fps),
"-i", os.path.join(tmpdir, "%06d.png"),
"-c:v", "libvpx-vp9",
"-pix_fmt", "yuva420p",
"-auto-alt-ref", "0",
output_path
]
subprocess.run(ffmpeg_cmd, check=True)
print(f"WebM with alpha saved to {output_path}")
def add_audio_to_video(video_without_audio_path, original_video_path, output_path):
"""
Check if the original video has an audio stream. If yes, add it. If not, skip.
"""
# 1) Probe original video for audio streams
probe_command = [
'ffprobe', '-v', 'error',
'-select_streams', 'a:0',
'-show_entries', 'stream=index',
'-of', 'csv=p=0',
original_video_path
]
result = subprocess.run(probe_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# result.stdout is empty if no audio stream found
if not result.stdout.strip():
print("No audio track found in original video, skipping audio addition.")
return
print("Audio track detected; proceeding to mux audio.")
# 2) If audio found, run ffmpeg to add it
command = [
'ffmpeg', '-y',
'-i', video_without_audio_path,
'-i', original_video_path,
'-c', 'copy',
'-map', '0:v:0',
'-map', '1:a:0', # we know there's an audio track now
output_path
]
subprocess.run(command, check=True)
print(f"Audio added successfully => {output_path}")
### Thanks to the source: https://huggingface.co/ZhengPeng7/BiRefNet/blob/main/handler.py
def refine_foreground_process(image, mask, r=90):
if mask.size != image.size:
mask = mask.resize(image.size)
image = np.array(image) / 255.0
mask = np.array(mask) / 255.0
estimated_foreground = FB_blur_fusion_foreground_estimator_2(image, mask, r=r)
image_masked = Image.fromarray((estimated_foreground * 255.0).astype(np.uint8))
return image_masked
def FB_blur_fusion_foreground_estimator_2(image, alpha, r=90):
# Thanks to the source: https://github.com/Photoroom/fast-foreground-estimation
alpha = alpha[:, :, None]
F, blur_B = FB_blur_fusion_foreground_estimator(image, image, image, alpha, r)
return FB_blur_fusion_foreground_estimator(image, F, blur_B, alpha, r=6)[0]
def FB_blur_fusion_foreground_estimator(image, F, B, alpha, r=90):
if isinstance(image, Image.Image):
image = np.array(image) / 255.0
blurred_alpha = cv2.blur(alpha, (r, r))[:, :, None]
blurred_FA = cv2.blur(F * alpha, (r, r))
blurred_F = blurred_FA / (blurred_alpha + 1e-5)
blurred_B1A = cv2.blur(B * (1 - alpha), (r, r))
blurred_B = blurred_B1A / ((1 - blurred_alpha) + 1e-5)
F = blurred_F + alpha * \
(image - alpha * blurred_F - (1 - alpha) * blurred_B)
F = np.clip(F, 0, 1)
return F, blurred_B
def postprocess_image(result: torch.Tensor, im_size: list) -> np.ndarray:
result = torch.squeeze(F.interpolate(result, size=im_size, mode='bilinear'), 0)
ma = torch.max(result)
@@ -944,4 +1288,23 @@ def postprocess_image(result: torch.Tensor, im_size: list) -> np.ndarray:
result = (result - mi) / (ma - mi)
im_array = (result * 255).permute(1, 2, 0).cpu().data.numpy().astype(np.uint8)
im_array = np.squeeze(im_array)
return im_array
return im_array
def rgb_loader_refiner(original_image):
h, w = original_image.size
# # Apply EXIF orientation
image = ImageOps.exif_transpose(original_image)
if original_image.mode != 'RGB':
original_image = original_image.convert('RGB')
image = original_image
# Convert to RGB if necessary
# Resize the image
image = image.resize((1024, 1024), resample=Image.LANCZOS)
return image, h, w, original_image
+32 -16
View File
@@ -5,13 +5,21 @@ import os
import types
import torch
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
try:
from accelerate import init_empty_weights, load_checkpoint_and_dispatch
except:
init_empty_weights, load_checkpoint_and_dispatch = None, None
import comfy
from .model import BrushNetModel, PowerPaintModel
from .model_patch import add_model_patch_option, patch_model_function_wrapper
from .powerpaint_utils import TokenizerWrapper, add_tokens
try:
from .model import BrushNetModel, PowerPaintModel
from .model_patch import add_model_patch_option, patch_model_function_wrapper
from .powerpaint_utils import TokenizerWrapper, add_tokens
except:
BrushNetModel, PowerPaintModel = None, None
add_model_patch_option, patch_model_function_wrapper = None, None
TokenizerWrapper, add_tokens = None, None
cwd_path = os.path.dirname(os.path.realpath(__file__))
brushnet_config_file = os.path.join(cwd_path, 'config', 'brushnet.json')
@@ -272,11 +280,11 @@ class BrushNet:
# unload vae
del vae
for loaded_model in comfy.model_management.current_loaded_models:
if type(loaded_model.model.model) in ModelsToUnload:
comfy.model_management.current_loaded_models.remove(loaded_model)
loaded_model.model_unload()
del loaded_model
# for loaded_model in comfy.model_management.current_loaded_models:
# if type(loaded_model.model.model) in ModelsToUnload:
# comfy.model_management.current_loaded_models.remove(loaded_model)
# loaded_model.model_unload()
# del loaded_model
# prepare embeddings
prompt_embeds = positive[0][0].to(dtype=torch_dtype).to(brushnet['brushnet'].device)
@@ -449,11 +457,11 @@ class BrushNet:
# unload vae and CLIPs
del vae
del clip
for loaded_model in comfy.model_management.current_loaded_models:
if type(loaded_model.model.model) in ModelsToUnload:
comfy.model_management.current_loaded_models.remove(loaded_model)
loaded_model.model_unload()
del loaded_model
# for loaded_model in comfy.model_management.current_loaded_models:
# if type(loaded_model.model.model) in ModelsToUnload:
# comfy.model_management.current_loaded_models.remove(loaded_model)
# loaded_model.model_unload()
# del loaded_model
# apply patch to model
@@ -663,8 +671,16 @@ def add_brushnet_patch(model, brushnet, torch_dtype, conditioning_latents,
is_SDXL = isinstance(model.model.model_config, comfy.supported_models.SDXL)
if model.model.model_config.custom_operations is None:
fp8 = model.model.model_config.optimizations.get("fp8", model.model.model_config.scaled_fp8 is not None)
operations = comfy.ops.pick_operations(model.model.model_config.unet_config.get("dtype", None), model.model.manual_cast_dtype,
fp8_optimizations=fp8, scaled_fp8=model.model.model_config.scaled_fp8)
else:
# such as gguf
operations = model.model.model_config.custom_operations
if is_SDXL:
input_blocks = [[0, comfy.ops.manual_cast.Conv2d],
input_blocks = [[0, operations.Conv2d],
[1, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[2, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample],
@@ -686,7 +702,7 @@ def add_brushnet_patch(model, brushnet, torch_dtype, conditioning_latents,
[7, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock],
[8, comfy.ldm.modules.diffusionmodules.openaimodel.ResBlock]]
else:
input_blocks = [[0, comfy.ops.manual_cast.Conv2d],
input_blocks = [[0, operations.Conv2d],
[1, comfy.ldm.modules.attention.SpatialTransformer],
[2, comfy.ldm.modules.attention.SpatialTransformer],
[3, comfy.ldm.modules.diffusionmodules.openaimodel.Downsample],
@@ -77,7 +77,7 @@ class ControlPixArtHalf(Module):
def forward_c(self, c):
self.h, self.w = c.shape[-2]//self.patch_size, c.shape[-1]//self.patch_size
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(c.device).to(self.dtype)
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), pe_interpolation=self.pe_interpolation, base_size=self.base_size)).unsqueeze(0).to(c.device).to(self.dtype)
return self.x_embedder(c) + pos_embed if c is not None else c
# def forward(self, x, t, c, **kwargs):
@@ -225,7 +225,7 @@ class ControlPixArtMSHalf(ControlPixArtHalf):
c_size, ar = data_info['img_hw'].to(self.dtype), data_info['aspect_ratio'].to(self.dtype)
self.h, self.w = x.shape[-2]//self.patch_size, x.shape[-1]//self.patch_size
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), lewei_scale=self.lewei_scale, base_size=self.base_size)).unsqueeze(0).to(x.device).to(self.dtype)
pos_embed = torch.from_numpy(get_2d_sincos_pos_embed(self.pos_embed.shape[-1], (self.h, self.w), pe_interpolation=self.pe_interpolation, base_size=self.base_size)).unsqueeze(0).to(x.device).to(self.dtype)
x = self.x_embedder(x) + pos_embed # (N, T, D), where T = H * W / patch_size ** 2
t = self.t_embedder(timestep) # (N, D)
csize = self.csize_embedder(c_size, bs) # (N, D)
+1 -1
View File
@@ -28,7 +28,7 @@ class DoubleStreamBlockIPA(nn.Module):
self.txt_norm2 = original_block.txt_norm2
self.txt_mlp = original_block.txt_mlp
self.flipped_img_txt = original_block.flipped_img_txt
self.flipped_img_txt = getattr(original_block, 'flipped_img_txt', False)
self.ip_adapter = ip_adapter
self.image_emb = image_emb
+9 -3
View File
@@ -9,7 +9,10 @@ from enum import Enum
from comfy.utils import load_torch_file
from comfy.conds import CONDRegular
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
try:
from .model import ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel
except:
ModelPatcher, TransparentVAEDecoder, calculate_weight_adjust_channel = None, None, None
from .attension_sharing import AttentionSharingPatcher
from ...config import LAYER_DIFFUSION, LAYER_DIFFUSION_DIR, LAYER_DIFFUSION_VAE
from ...libs.utils import to_lora_patch_dict, get_local_filepath, get_sd_version
@@ -51,7 +54,7 @@ class LayerDiffuse:
return (write_c_concat(cond), write_c_concat(uncond))
def apply_layer_diffusion(self, model: ModelPatcher, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
def apply_layer_diffusion(self, model, method, weight, samples, blend_samples, positive, negative, image=None, additional_cond=(None, None, None)):
control_img: Optional[torch.TensorType] = None
sd_version = get_sd_version(model)
model_url = LAYER_DIFFUSION[method.value][sd_version]["model_url"]
@@ -147,7 +150,10 @@ class LayerDiffuse:
alpha = pixel_with_alpha[..., 0]
alpha = 1.0 - alpha
new_images, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
try:
new_images, = JoinImageWithAlpha().execute(image, alpha)
except:
new_images, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
return new_images, alpha
def make_3d_mask(self, mask):
+39 -3
View File
@@ -1,5 +1,6 @@
import re
import torch
import folder_paths
import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from comfy.clip_vision import load as load_clip_vision
@@ -9,12 +10,39 @@ from ..config import *
from ..libs.log import log_node_info, log_node_warn
from ..libs.utils import get_local_filepath, get_sd_version
from ..libs.wildcards import process_with_loras
from ..libs.controlnet import easyControlnet
from ..libs.conditioning import prompt_to_cond
from ..libs import cache as backend_cache
from .. import easyCache
class applyLoraPrompt:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL",),
"clip": ("CLIP",),
"positive": ("STRING", {"default": "", "forceInput": True}),
},
"optional": {
"negative": ("STRING", {"default": "", "forceInput": True}),
}
}
RETURN_TYPES = ("MODEL", "CLIP", "STRING", "STRING")
RETURN_NAMES = ("model", "clip", "positive", "negative")
CATEGORY = "EasyUse/Adapter"
FUNCTION = "apply"
def apply(self, model, clip, positive, negative=None):
model, clip, positive, _, _, _ = process_with_loras(positive, model, clip, 'Positive', easyCache=easyCache)
if negative is not None:
model, clip, negative, _, _, _ = process_with_loras(negative, model, clip, 'Negative', easyCache=easyCache)
return (model, clip, positive, negative if negative is not None else "")
class applyLoraStack:
@classmethod
def INPUT_TYPES(s):
@@ -40,7 +68,7 @@ class applyLoraStack:
lora = {"lora_name": lora[0], "model": model, "clip": optional_clip, "model_strength": lora[1],
"clip_strength": lora[2]}
model, clip = easyCache.load_lora(lora, model, optional_clip, use_cache=False)
return (model, clip)
return (model, optional_clip if clip is None else clip)
class applyControlnetStack:
@classmethod
@@ -153,7 +181,10 @@ class icLightApply:
image = self.removebg(image)
else:
mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu")
image, = JoinImageWithAlpha().join_image_with_alpha(image, mask)
try:
image, = JoinImageWithAlpha().execute(image, mask)
except:
image, = JoinImageWithAlpha().join_image_with_alpha(image, mask)
iclight = ICLight()
if mode == 'Foreground':
@@ -163,7 +194,10 @@ class icLightApply:
if source not in ['Use Background Image', 'Use Flipped Background Image']:
_, height, width, _ = lighting_image.shape
mask = torch.full((1, height, width), 1.0, dtype=torch.float32, device="cpu")
lighting_image, = JoinImageWithAlpha().join_image_with_alpha(lighting_image, mask)
try:
lighting_image, = JoinImageWithAlpha().execute(lighting_image, mask)
except:
lighting_image, = JoinImageWithAlpha().join_image_with_alpha(lighting_image, mask)
if batch_size < 2:
image = self.batch(image, lighting_image)
else:
@@ -1284,6 +1318,7 @@ class applyPulIDADV(applyPulID):
NODE_CLASS_MAPPINGS = {
"easy loraPromptApply": applyLoraPrompt,
"easy loraStackApply": applyLoraStack,
"easy controlnetStackApply": applyControlnetStack,
"easy ipadapterApply": ipadapterApply,
@@ -1303,6 +1338,7 @@ NODE_CLASS_MAPPINGS = {
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy loraPromptApply": "Easy Apply LoraPrompt",
"easy loraStackApply": "Easy Apply LoraStack",
"easy controlnetStackApply": "Easy Apply CnetStack",
"easy ipadapterApply": "Easy Apply IPAdapter",
+13 -35
View File
@@ -3,37 +3,8 @@ from ..libs.api.fluxai import fluxaiAPI
from ..libs.api.bizyair import bizyairAPI, encode_data
from nodes import NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
class fluxPromptGenAPI:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"describe": ("STRING", {"default": "", "placeholder": "Describe your image idea (you can use any language)", "multiline": True}),
},
"optional": {
"cookie_override": ("STRING", {"default": "", "forceInput": True}),
},
"hidden": {
"prompt": "PROMPT",
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "generate"
OUTPUT_NODE = False
CATEGORY = "EasyUse/API"
def generate(self, describe, cookie_override=None, prompt=None, unique_id=None, extra_pnginfo=None):
prompt = fluxaiAPI.promptGenerate(describe, cookie_override)
return (prompt,)
class joyCaption2API:
API_URL = f"/supernode/joycaption2"
@classmethod
def INPUT_TYPES(s):
@@ -101,18 +72,21 @@ class joyCaption2API:
"multiline": True,
},
),
},
"optional":{
"apikey_override": ("STRING", {"default": "", "forceInput": True, "tooltip":"Override the API key in the local config"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("caption",)
FUNCTION = "joycaption2"
FUNCTION = "joycaption"
OUTPUT_NODE = False
CATEGORY = "EasyUse/API"
def joycaption2(
def joycaption(
self,
image,
do_sample,
@@ -123,6 +97,7 @@ class joyCaption2API:
extra_options,
name_input,
custom_prompt,
apikey_override=None
):
pbar = comfy.utils.ProgressBar(100)
pbar.update_absolute(10)
@@ -145,17 +120,20 @@ class joyCaption2API:
}
pbar.update_absolute(30)
caption = bizyairAPI.joyCaption2(payload, image)
caption = bizyairAPI.joyCaption(payload, image, apikey_override, API_URL=self.API_URL)
pbar.update_absolute(100)
return (caption,)
class joyCaption3API(joyCaption2API):
API_URL = f"/supernode/joycaption3"
NODE_CLASS_MAPPINGS = {
"easy fluxPromptGenAPI": fluxPromptGenAPI,
"easy joyCaption2API": joyCaption2API,
"easy joyCaption3API": joyCaption3API,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy fluxPromptGenAPI": "Prompt Gen (FluxAI)",
"easy joyCaption2API": "JoyCaption2 (BizyAIR)",
"easy joyCaption3API": "JoyCaption3 (BizyAIR)",
}
+3
View File
@@ -1,8 +1,11 @@
import numpy as np
import os
import json
import torch
import folder_paths
import comfy
import comfy.model_management
from PIL import Image
from PIL.PngImagePlugin import PngInfo
from nodes import ConditioningSetMask, RepeatLatentBatch
from comfy_extras.nodes_mask import LatentCompositeMasked
+6 -2
View File
@@ -438,7 +438,11 @@ class detailerFix:
# Clean loaded_objects
easyCache.update_loaded_objects(prompt)
my_unique_id = int(my_unique_id)
# my_unique_id can be a composite ID (e.g. `101:134`) when put inside a sub-graph; this fixes it so that it can support sub-graphs
try:
my_unique_id = int(my_unique_id)
except (ValueError, TypeError):
my_unique_id = int(str(my_unique_id).split(':')[-1])
model = model or (pipe["model"] if "model" in pipe else None)
if model is None:
@@ -640,4 +644,4 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy ultralyticsDetectorPipe": "UltralyticsDetector (Pipe)",
"easy samLoaderPipe": "SAMLoader (Pipe)",
"easy detailerFix": "DetailerFix",
}
}
+232 -119
View File
@@ -4,19 +4,19 @@ import torch
import numpy as np
import comfy.utils
import comfy.model_management
import shutil
from comfy_extras.nodes_compositing import JoinImageWithAlpha
from server import PromptServer
from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
from PIL import Image, ImageDraw, ImageFilter, ImageOps
import torch.nn.functional as F
from torchvision.transforms import Resize, CenterCrop, GaussianBlur
from torchvision.transforms import Resize, CenterCrop, GaussianBlur, ToPILImage
from torchvision.transforms.functional import to_pil_image
from ..libs.log import log_node_info
from ..libs.utils import AlwaysEqualProxy, ByPassTypeTuple
from ..libs.cache import cache, update_cache, remove_cache
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image
from ..libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds, RGB2RGBA, image2mask, empty_image, fit_resize_image
from ..libs.colorfix import adain_color_fix, wavelet_color_fix
from ..libs.chooser import ChooserMessage, ChooserCancelled
from ..config import REMBG_DIR, REMBG_MODELS, HUMANPARSING_MODELS, MEDIAPIPE_MODELS, MEDIAPIPE_DIR
any_type = AlwaysEqualProxy("*")
@@ -485,8 +485,7 @@ class imageSaveSimple:
def save(self, images, filename_prefix="ComfyUI", only_preview=False, prompt=None, extra_pnginfo=None):
if only_preview:
PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
return ()
return PreviewImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
else:
return SaveImage().save_images(images, filename_prefix, prompt, extra_pnginfo)
@@ -803,7 +802,20 @@ class imageConcat:
elif image2 is None:
return (image1,)
if match_image_size:
image2 = torch.nn.functional.interpolate(image2, size=(image1.shape[2], image1.shape[3]), mode="bilinear")
# Convert tensor to PIL for proper aspect ratio resizing
pil_image2 = tensor2pil(image2)
if direction in ['right', 'left']:
aspect_ratio = pil_image2.width / pil_image2.height
new_height = image1.shape[1]
new_width = int(aspect_ratio * new_height)
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
else: # 'up' or 'down'
aspect_ratio = pil_image2.height / pil_image2.width
new_width = image1.shape[2]
new_height = int(aspect_ratio * new_width)
pil_image2 = fit_resize_image(pil_image2, new_width, new_height, 'fill', Image.LANCZOS, '#000000')
image2 = pil2tensor(pil_image2)
if direction == 'right':
row = torch.cat((image1, image2), dim=2)
elif direction == 'down':
@@ -828,7 +840,8 @@ class imageRemBg:
},
"optional":{
"torchscript_jit": ("BOOLEAN", {"default": False}),
"add_background": (["none", "white", "black"], {"default": "none"})
"add_background": (["none", "white", "black"], {"default": "none"}),
"refine_foreground": ("BOOLEAN", {"default": False}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
@@ -841,7 +854,7 @@ class imageRemBg:
CATEGORY = "EasyUse/Image"
def remove(self, rem_mode, images, image_output, save_prefix, torchscript_jit=False, add_background='none',prompt=None, extra_pnginfo=None):
def remove(self, rem_mode, images, image_output, save_prefix, torchscript_jit=False, add_background='none', refine_foreground=False, prompt=None, extra_pnginfo=None):
new_images = list()
masks = list()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
@@ -941,7 +954,7 @@ class imageRemBg:
if input_image.mode != 'RGBA':
input_image = input_image.convert("RGBA")
mask, new_im = model.inference(input_image)
mask, new_im = model.inference(input_image, refine_foreground)
new_im_tensor = pil2tensor(new_im)
mask_tensor = pil2tensor(mask)
@@ -992,12 +1005,14 @@ class imageRemBg:
"result": (new_images, masks)}
# 图像选择器
from ..libs.chooser import wait_for_chooser
class imageChooser(PreviewImage):
@classmethod
def INPUT_TYPES(self):
return {
"required":{
"mode": (['Always Pause', 'Keep Last Selection'], {"default": "Always Pause"}),
"preview_rescale": ("FLOAT", {"default": 1.0, "min": 0.05, "max": 1.0, "step": 0.05}),
},
"optional": {
"images": ("IMAGE",),
@@ -1028,50 +1043,36 @@ class imageChooser(PreviewImage):
def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs):
id = my_unique_id[0]
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
if id not in ChooserMessage.stash:
ChooserMessage.stash[id] = {}
my_stash = ChooserMessage.stash[id]
# enable stashing. If images is None, we are operating in read-from-stash mode
if 'images' in kwargs:
my_stash['images'] = kwargs['images']
else:
kwargs['images'] = my_stash.get('images', None)
if (kwargs['images'] is None):
return (None, None, None, "")
if (kwargs.get('images') is None):
return (torch.zeros(1, 1, 1, 3),)
images_in = torch.cat(kwargs.pop('images'))
self.batch = images_in.shape[0]
for x in kwargs: kwargs[x] = kwargs[x][0]
result = self.save_images(images=images_in, prompt=prompt)
images = result['ui']['images']
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
try:
pnginfo = extra_pnginfo[0]
except:
pnginfo = None
preview_rescale = kwargs.pop('preview_rescale', 1.0)
if preview_rescale < 1.0:
images_preview, = imageScaleDownBy().image_scale_down_by(images_in, preview_rescale)
else:
images_preview = images_in
result = self.save_images(images=images_preview, prompt=prompt, extra_pnginfo=pnginfo)
if "ui" in result and "images" in result['ui']:
images = result["ui"]["images"]
else:
images = []
try:
PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images})
except Exception as e:
pass
# 获取上次选择
mode = kwargs.pop('mode', 'Always Pause')
last_choosen = None
if mode == 'Keep Last Selection':
if not extra_pnginfo:
print("Error: extra_pnginfo is empty")
elif (not isinstance(extra_pnginfo[0], dict) or "workflow" not in extra_pnginfo[0]):
print("Error: extra_pnginfo[0] is not a dict or missing 'workflow' key")
else:
workflow = extra_pnginfo[0]["workflow"]
node = next((x for x in workflow["nodes"] if str(x["id"]) == id), None)
if node:
last_choosen = node['properties']['values']
# wait for selection
try:
selections = ChooserMessage.waitForMessage(id, asList=True) if last_choosen is None or len(last_choosen)<1 else last_choosen
choosen = [x for x in selections if x >= 0] if len(selections)>1 else [0]
except ChooserCancelled:
raise comfy.model_management.InterruptProcessingException()
return {"ui": {"images": images},
"result": (self.tensor_bundle(images_in, choosen),)}
return wait_for_chooser(id, images_in, mode)
class imageColorMatch(PreviewImage):
@classmethod
@@ -1217,6 +1218,8 @@ class imageDetailTransfer:
new_image = torch.lerp(target_tensor, new_image, blend_factor)
if mask is not None:
mask = mask.to(device)
if mask.dim() == 3: # (B,H,W) batch/video mask -> (B,1,H,W) so it broadcasts over channels
mask = mask.unsqueeze(1)
new_image = torch.lerp(target_tensor, new_image, mask)
new_image = torch.clamp(new_image, 0, 1)
new_image = new_image.permute(0, 2, 3, 1).cpu().float()
@@ -1259,13 +1262,22 @@ class humanSegmentation:
@classmethod
def INPUT_TYPES(cls):
return {
"required":{
"image": ("IMAGE",),
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)"],),
"method": (["selfie_multiclass_256x256", "human_parsing_lip", "human_parts (deeplabv3p)", "segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"],),
"confidence": ("FLOAT", {"default": 0.4, "min": 0.05, "max": 0.95, "step": 0.01},),
"crop_multi": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 10.0, "step": 0.001},),
"mask_components":(
"EASY_COMBO",{
"options": [{'label':'Background','value':0}],
"multi_select": {
"placeholder": "select mask components",
"chip": True,
"max_selected_labels": 4,
}
}
)
},
"hidden": {
"prompt": "PROMPT",
@@ -1291,12 +1303,12 @@ class humanSegmentation:
numpy_image = cv2.cvtColor(numpy_image, cv2.COLOR_BGR2RGB)
return mp.Image(image_format=image_format, data=numpy_image)
def parsing(self, image, confidence, method, crop_multi, prompt=None, my_unique_id=None):
mask_components = []
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['mask_components']:
mask_components = prompt[my_unique_id]["inputs"]['mask_components'].split(',')
mask_components = list(map(int, mask_components))
def parsing(self, image, confidence, method, crop_multi, mask_components, prompt=None, my_unique_id=None):
if isinstance(mask_components, str):
mask_components = [int(x) for x in mask_components.split(',') if x]
else:
mask_components = mask_components if mask_components else []
if method == 'selfie_multiclass_256x256':
try:
import mediapipe as mp
@@ -1323,6 +1335,9 @@ class humanSegmentation:
ret_images = []
ret_masks = []
if len(mask_components) == 0:
return (image, torch.zeros_like(image[:, :, :, 0:1]), torch.tensor([0,0,0,0]))
with mp.tasks.vision.ImageSegmenter.create_from_options(options) as segmenter:
for img in image:
_image = torch.unsqueeze(img, 0)
@@ -1356,7 +1371,14 @@ class humanSegmentation:
mask_arrays.append(mask_background_array)
else:
for i, mask in enumerate(masks):
condition = np.stack((mask.numpy_view(),) * image_shape[-1], axis=-1) > confidence
mask_2d = mask.numpy_view()
if mask_2d.ndim == 3 and mask_2d.shape[2] == 1:
mask_2d = mask_2d.squeeze(axis=2)
elif mask_2d.ndim != 2:
raise ValueError(f"Unexpected mask shape: {mask_2d.shape}")
condition = np.stack((mask_2d,) * image_shape[-1], axis=-1) > confidence
if condition.ndim == 4 and condition.shape[2] == 1:
condition = condition.squeeze(2)
mask_array = np.where(condition, mask_foreground_array, mask_background_array)
mask_arrays.append(mask_array)
# Merge our masks taking the maximum from each
@@ -1398,7 +1420,11 @@ class humanSegmentation:
alpha = 1.0 - mask
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
try:
output_image, = JoinImageWithAlpha().execute(image, alpha)
except:
output_image, = JoinImageWithAlpha().join_image_with_alpha(image, alpha)
elif method == "human_parts (deeplabv3p)":
if method in cache:
@@ -1424,6 +1450,107 @@ class humanSegmentation:
output_image = torch.cat(ret_images, dim=0)
mask = torch.cat(ret_masks, dim=0)
elif method in ["segformer_b3_clothes", "segformer_b3_fashion", "face_parsing"]:
from transformers import SegformerImageProcessor, AutoModelForSemanticSegmentation
# 分割
def get_segmentation_from_model(tensor_image, model, processor):
cloth = tensor2pil(tensor_image)
inputs = processor(images=cloth, return_tensors="pt")
outputs = model(**inputs)
logits = outputs.logits.cpu()
upsampled_logits = F.interpolate(logits, size=cloth.size[::-1], mode="bilinear",
align_corners=False)
pred_seg = upsampled_logits.argmax(dim=1)[0].numpy()
return pred_seg, cloth
if method in cache:
_, (processor, model) = cache[method][1]
else:
model_folder_path = os.path.join(folder_paths.models_dir, method)
if os.path.exists(model_folder_path):
print(f"Start to load existing model...")
else:
from huggingface_hub import snapshot_download
PromptServer.instance.send_sync("easyuse-toast", {"content": f"Model not found locally. Downloading {method}...", "type": 'loading', "duration": 10000})
print(f"Model not found locally. Downloading {method}...")
model_path_cache = os.path.join(folder_paths.models_dir, "cache-"+method)
snapshot_download(
repo_id=HUMANPARSING_MODELS[method]['model_name'],
local_dir=model_path_cache,
local_dir_use_symlinks=False,
resume_download=True
)
shutil.move(model_path_cache, model_folder_path)
print(f"Model downloaded to {model_folder_path}...")
try:
model_folder_path = os.path.normpath(folder_paths.folder_names_and_paths[method][0][0])
except:
pass
processor = SegformerImageProcessor.from_pretrained(model_folder_path)
model = AutoModelForSemanticSegmentation.from_pretrained(model_folder_path)
update_cache(method, 'human_segmentation', (False, (processor, model)))
ret_images = []
ret_masks = []
if method == "face_parsing":
import matplotlib
import torchvision.transforms as T
transform = ToPILImage()
colormap = matplotlib.colormaps['viridis']
device = model.device
results = []
images = []
for img in image:
size = img.shape[:2]
inputs = processor(images=transform(img.permute(2, 0, 1)), return_tensors="pt")
inputs = {k: v.to(device) for k, v in inputs.items()}
outputs = model(**inputs)
logits = outputs.logits
upsampled_logits = F.interpolate(
logits,
size=size,
mode="bilinear",
align_corners=False)
pred_seg = upsampled_logits.argmax(dim=1)[0]
pred_seg_np = pred_seg.cpu().detach().numpy().astype(np.uint8)
results.append(torch.tensor(pred_seg_np))
results_out = torch.stack(results, dim=0)
for img, result_item in zip(image, results_out):
mask = torch.zeros(result_item.shape, dtype=torch.uint8)
for i in mask_components:
mask = mask | torch.where(result_item == i, 1, 0)
# 将mask转换为numpy数组,并确保数据类型正确
mask_np = (mask * 255).numpy().astype(np.uint8)
_mask = Image.fromarray(mask_np)
# 处理图像输出
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
else:
for img in image:
pred_seg, cloth = get_segmentation_from_model(img, model, processor)
i = torch.unsqueeze(img, 0)
i = pil2tensor(tensor2pil(i).convert('RGB'))
mask = np.isin(pred_seg, mask_components).astype(np.uint8)
_mask = Image.fromarray(mask * 255)
ret_image = RGB2RGBA(tensor2pil(img).convert('RGB'), _mask.convert('L'))
ret_images.append(pil2tensor(ret_image))
ret_masks.append(image2mask(_mask))
output_image = torch.cat(ret_images, dim=0)
mask = torch.cat(ret_masks, dim=0)
# use crop
bbox = [[0, 0, 0, 0]]
if crop_multi > 0.0:
@@ -1745,7 +1872,7 @@ class imageToBase64:
pil_image = tensor2pil(image)
buffered = BytesIO()
pil_image.save(buffered, format="JPEG")
pil_image.save(buffered, format="PNG")
image_bytes = buffered.getvalue()
base64_str = base64.b64encode(image_bytes).decode("utf-8")
@@ -1874,47 +2001,6 @@ class loadImagesForLoop:
"result": tuple(["stub", index, image, mask, name] + outputs),
"expand": graph.finalize(),
}
# 姿势编辑器
# class poseEditor:
# @classmethod
# def INPUT_TYPES(self):
# temp_dir = folder_paths.get_temp_directory()
#
# if not os.path.isdir(temp_dir):
# os.makedirs(temp_dir)
#
# temp_dir = folder_paths.get_temp_directory()
#
# return {"required":
# {"image": (sorted(os.listdir(temp_dir)),)},
# }
#
# RETURN_TYPES = ("IMAGE",)
# FUNCTION = "output_pose"
#
# CATEGORY = "EasyUse/🚫 Deprecated"
#
# def output_pose(self, image):
# image_path = os.path.join(folder_paths.get_temp_directory(), image)
# # print(f"Create: {image_path}")
#
# i = Image.open(image_path)
# image = i.convert("RGB")
# image = np.array(image).astype(np.float32) / 255.0
# image = torch.from_numpy(image)[None,]
#
# return (image,)
#
# @classmethod
# def IS_CHANGED(self, image):
# image_path = os.path.join(
# folder_paths.get_temp_directory(), image)
# # print(f'Change: {image_path}')
#
# m = hashlib.sha256()
# with open(image_path, 'rb') as f:
# m.update(f.read())
# return m.digest().hex()
class makeImageForICRepaint:
@classmethod
@@ -1924,6 +2010,7 @@ class makeImageForICRepaint:
"image_1": ("IMAGE",),
"direction": (["top-bottom", "left-right"], {"default": "left-right"}),
"pixels": ("INT", {"default": 0, "max": MAX_RESOLUTION, "min": 0, "step": 8, "tooltip": "The pixel of the output image is not set when it is 0"}),
"method": (["uniform height", "uniform width", "auto"],{"default": "auto"}),
},
"optional": {
"image_2": ("IMAGE",),
@@ -1950,26 +2037,52 @@ class makeImageForICRepaint:
b = torch.full([batch_size, height, width, 1], ((color) & 0xFF) / 0xFF)
return torch.cat((r, g, b), dim=-1)
def make(self, image_1, direction, pixels=0, image_2=None, mask_1=None, mask_2=None):
def resize_image_and_mask(self, image, mask, w, h ,fit='fill'):
ret_images = []
ret_masks = []
_mask = Image.new('L', size=(w, h), color='black')
_image = Image.new('RGB', size=(w, h), color='black')
if image is not None and len(image) > 0:
for i in image:
_image = tensor2pil(i).convert('RGB')
_image = fit_resize_image(_image, w, h, fit, Image.LANCZOS, '#000000')
ret_images.append(pil2tensor(_image))
if mask is not None and len(mask) > 0:
for m in mask:
_mask = tensor2pil(m).convert('L')
_mask = fit_resize_image(_mask, w, h, fit, Image.LANCZOS).convert('L')
ret_masks.append(image2mask(_mask))
if len(ret_images) > 0 and len(ret_masks) > 0:
return (torch.cat(ret_images, dim=0), torch.cat(ret_masks, dim=0),)
elif len(ret_images) > 0 and len(ret_masks) == 0:
return (torch.cat(ret_images, dim=0), None,)
elif len(ret_images) == 0 and len(ret_masks) > 0:
return (None, torch.cat(ret_masks, dim=0),)
else:
return (None, None)
def make(self, image_1, direction, pixels, method, image_2=None, mask_1=None, mask_2=None):
if image_2 is None:
image_2 = self.emptyImage(image_1.shape[2], image_1.shape[1])
mask_2 = torch.full((1, image_1.shape[1], image_1.shape[2]), 1, dtype=torch.float32, device="cpu")
elif image_2 is not None and mask_2 is None:
raise ValueError("mask_2 is required when image_2 is provided")
mask_2 = torch.full((1, image_2.shape[1], image_2.shape[2]), 1, dtype=torch.float32, device="cpu")
if pixels > 0:
_, img2_h, img2_w, _ = image_2.shape
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
if method == "uniform height":
h = pixels
w = int(img2_w * (pixels / img2_h))
elif method == "uniform width":
w = pixels
h = int(img2_h * (pixels / img2_w))
else:
h = pixels if direction == 'left-right' else int(img2_h * (pixels / img2_w))
w = pixels if direction == 'top-bottom' else int(img2_w * (pixels / img2_h))
image_2 = image_2.movedim(-1, 1)
image_2 = comfy.utils.common_upscale(image_2, w, h, 'bicubic', 'disabled')
image_2 = image_2.movedim(1, -1)
orig_image_2 = tensor2pil(image_2)
orig_mask_2 = tensor2pil(mask_2).convert('L')
orig_mask_2 = orig_mask_2.resize(orig_image_2.size)
mask_2 = pil2tensor(orig_mask_2)
image_2, mask_2 = self.resize_image_and_mask(image_2, mask_2, w, h)
_, img1_h, img1_w, _ = image_1.shape
_, img2_h, img2_w, _ = image_2.shape
@@ -1979,16 +2092,16 @@ class makeImageForICRepaint:
# resize
if img1_h != img2_h and img1_w != img2_w:
width, height = img2_w, img2_h
if direction == 'left-right' and img1_h != img2_h:
scale_factor = img2_h / img1_h
width = round(img1_w * scale_factor)
elif direction == 'top-bottom' and img1_w != img2_w:
scale_factor = img2_w / img1_w
height = round(img1_h * scale_factor)
image_1 = image_1.movedim(-1, 1)
image_1 = comfy.utils.common_upscale(image_1, width, height, 'bicubic', 'disabled')
image_1 = image_1.movedim(1, -1)
fit = 'crop'
if method != 'uniform width':
if direction == 'left-right' and img1_h != img2_h:
scale_factor = img2_h / img1_h
width = round(img1_w * scale_factor)
elif direction == 'top-bottom' and img1_w != img2_w:
scale_factor = img2_w / img1_w
height = round(img1_h * scale_factor)
fit = 'fill'
image_1, mask_1 = self.resize_image_and_mask(image_1, mask_1, width, height, fit)
if mask_1 is None:
mask_1 = torch.full((1, image_1.shape[1], image_1.shape[2]), 0, dtype=torch.float32, device="cpu")
+4 -1
View File
@@ -331,7 +331,10 @@ class applyInpaint:
new_pipe = self.inpaint_model_conditioning(new_pipe, image, vae, mask, grow_mask_by, noise_mask=noise_mask)
cls = ALL_NODE_CLASS_MAPPINGS['DifferentialDiffusion']
if cls is not None:
model, = cls().apply(new_pipe['model'])
try:
model, = cls().execute(new_pipe['model'])
except Exception:
model, = cls().apply(new_pipe['model'])
new_pipe['model'] = model
else:
raise Exception("Differential Diffusion not found,please update comfyui")
+71 -6
View File
@@ -8,7 +8,7 @@ from nodes import MAX_RESOLUTION, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS
from ..libs.log import log_node_info, log_node_error, log_node_warn
from ..libs.wildcards import process_with_loras
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version
from ..libs.utils import find_wildcards_seed, is_linked_styles_selector, get_sd_version, AlwaysEqualProxy
from ..libs.sampler import easySampler
from ..libs.controlnet import easyControlnet, union_controlnet_types
from ..libs.conditioning import prompt_to_cond
@@ -19,6 +19,7 @@ from ..config import *
from .. import easyCache, sampler
any_type = AlwaysEqualProxy("*")
# 简易加载器完整
resolution_strings = [f"{width} x {height} (custom)" if width == 'width' and height == 'height' else f"{width} x {height}" for width, height in BASE_RESOLUTIONS]
class fullLoader:
@@ -28,7 +29,7 @@ class fullLoader:
a1111_prompt_style_default = False
return {"required": {
"ckpt_name": (folder_paths.get_filename_list("checkpoints"),),
"ckpt_name": (folder_paths.get_filename_list("checkpoints") + ['None'],),
"config_name": (["Default", ] + folder_paths.get_filename_list("configs"), {"default": "Default"}),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"clip_skip": ("INT", {"default": -2, "min": -24, "max": 0, "step": 1}),
@@ -71,6 +72,9 @@ class fullLoader:
my_unique_id=None
):
if ckpt_name == 'None' and model_override is None:
raise Exception("Please select a checkpoint or provide a model override.")
# Clean models from loaded_objects
easyCache.update_loaded_objects(prompt)
@@ -923,7 +927,7 @@ class fluxLoader(fullLoader):
loras = ["None"] + folder_paths.get_filename_list("loras")
return {
"required": {
"ckpt_name": (checkpoints,),
"ckpt_name": (checkpoints + ['None'],),
"vae_name": (["Baked VAE"] + folder_paths.get_filename_list("vae"),),
"lora_name": (loras,),
"lora_model_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
@@ -1143,6 +1147,59 @@ class mochiLoader(fullLoader):
my_unique_id=my_unique_id
)
# lora
class loraSwitcher:
@classmethod
def INPUT_TYPES(s):
max_lora_num = 50
inputs = {
"required": {
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
"select": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
"lora_strength": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01})
},
"optional": {
"optional_lora_stack": ("LORA_STACK",),
},
}
for i in range(1, max_lora_num + 1):
inputs["optional"][f"lora_{i}_name"] = (
["None"] + folder_paths.get_filename_list("loras"), {"default": "None"})
return inputs
RETURN_TYPES = ("LORA_STACK", any_type)
RETURN_NAMES = ("lora_stack", "lora_name")
FUNCTION = "stack"
CATEGORY = "EasyUse/Loaders"
def stack(self, toggle, select,num_loras, lora_strength, optional_lora_stack=None, **kwargs):
if toggle in [False, None, "False"]:
return (optional_lora_stack, '')
if not kwargs and optional_lora_stack is None:
return (None, '')
loras = []
# Import Stack values
if optional_lora_stack is not None:
loras.extend([l for l in optional_lora_stack if l[0] != "None"])
# Import Lora values
lora_name = kwargs.get(f"lora_{select}_name")
if not lora_name or lora_name == "None":
return (None,'')
loras.append((lora_name, lora_strength, lora_strength))
name = os.path.splitext(os.path.basename(str(lora_name)))[0]
return (loras, name)
class loraStack:
def __init__(self):
pass
@@ -1152,7 +1209,7 @@ class loraStack:
max_lora_num = 10
inputs = {
"required": {
"toggle": ("BOOLEAN", {"label_on": "enabled", "label_off": "disabled"}),
"toggle": ("BOOLEAN", {"label_on": "on", "label_off": "off"}),
"mode": (["simple", "advanced"],),
"num_loras": ("INT", {"default": 1, "min": 1, "max": max_lora_num}),
},
@@ -1180,7 +1237,10 @@ class loraStack:
CATEGORY = "EasyUse/Loaders"
def stack(self, toggle, mode, num_loras, optional_lora_stack=None, **kwargs):
if (toggle in [False, None, "False"]) or not kwargs:
if toggle in [False, None, "False"]:
return (optional_lora_stack,)
if not kwargs and optional_lora_stack is None:
return (None,)
loras = []
@@ -1237,7 +1297,10 @@ class controlnetStack:
CATEGORY = "EasyUse/Loaders"
def stack(self, toggle, mode, num_controlnet, optional_controlnet_stack=None, **kwargs):
if (toggle in [False, None, "False"]) or not kwargs:
if toggle in [False, None, "False"]:
return (optional_controlnet_stack,)
if not kwargs and optional_controlnet_stack is None:
return (None,)
controlnets = []
@@ -1479,6 +1542,7 @@ NODE_CLASS_MAPPINGS = {
"easy hunyuanDiTLoader": hunyuanDiTLoader,
"easy pixArtLoader": pixArtLoader,
"easy mochiLoader": mochiLoader,
"easy loraSwitcher": loraSwitcher,
"easy loraStack": loraStack,
"easy controlnetStack": controlnetStack,
"easy controlnetLoader": controlnetSimple,
@@ -1500,6 +1564,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy hunyuanDiTLoader": "EasyLoader (HunyuanDiT)",
"easy pixArtLoader": "EasyLoader (PixArt)",
"easy mochiLoader": "EasyLoader (Mochi)",
"easy loraSwitcher": "EasyLoraSwitcher",
"easy loraStack": "EasyLoraStack",
"easy controlnetStack": "EasyControlnetStack",
"easy controlnetLoader": "EasyControlnet",
+1010 -1044
View File
File diff suppressed because it is too large Load Diff
+18 -14
View File
@@ -554,13 +554,17 @@ class pipeXYPlotAdvanced:
if font_path and not os.path.exists(font_path):
font_path = os.path.join(self.user_font_dir, font)
if X != None:
if X is not None:
if isinstance(X, tuple):
X = X[0]
x_axis = X.get('axis')
x_values = X.get('values')
else:
x_axis = "Nothing"
x_values = [""]
if Y != None:
if Y is not None:
if isinstance(Y, tuple):
Y = Y[0]
y_axis = Y.get('axis')
y_values = Y.get('values')
else:
@@ -626,19 +630,19 @@ class pipeXYPlotAdvanced:
}
if x_axis == 'advanced: Seeds++ Batch':
if new_pipe['seed']:
value = x_values
x_values = []
for index in range(value):
x_values.append(str(new_pipe['seed'] + index))
x_values = "; ".join(x_values)
seed = new_pipe.get('seed') or 0
value = x_values
x_values = []
for index in range(value):
x_values.append(str(seed + index))
x_values = "; ".join(x_values)
if y_axis == 'advanced: Seeds++ Batch':
if new_pipe['seed']:
value = y_values
y_values = []
for index in range(value):
y_values.append(str(new_pipe['seed'] + index))
y_values = "; ".join(y_values)
seed = new_pipe.get('seed') or 0
value = y_values
y_values = []
for index in range(value):
y_values.append(str(seed + index))
y_values = "; ".join(y_values)
if x_axis == 'advanced: Positive Prompt S/R':
if positive:
+1 -1
View File
@@ -367,7 +367,7 @@ class samplerCustomSettings:
FUNCTION = "settings"
CATEGORY = "EasyUse/PreSampling"
def ip2p(self, positive, negative, vae, pixels, latent=None):
def ip2p(self, positive, negative, vae=None, pixels=None, latent=None):
if latent is not None:
concat_latent = latent
else:
+514 -260
View File
@@ -1,143 +1,186 @@
import os
import json
import folder_paths
import os
from urllib.request import urlopen
from ..libs.log import log_node_info
from ..libs.wildcards import get_wildcard_list, process
from ..libs.utils import AlwaysEqualProxy
from ..config import RESOURCES_DIR, FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE
import folder_paths
from .. import easyCache
from ..config import FOOOCUS_STYLES_DIR, MAX_SEED_NUM, PROMPT_TEMPLATE, RESOURCES_DIR
from ..libs.log import log_node_info
from ..libs.wildcards import WildcardProcessor, get_wildcard_list, process
from comfy_api.latest import io
# 正面提示词
class positivePrompt:
def __init__(self):
pass
class positivePrompt(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
return {"required": {
"positive": ("STRING", {"default": "", "multiline": True, "placeholder": "Positive"}),}
}
def define_schema(cls):
return io.Schema(
node_id="easy positive",
category="EasyUse/Prompt",
inputs=[
io.String.Input("positive", default="", multiline=True, placeholder="Positive"),
],
outputs=[
io.String.Output(id="output_positive", display_name="positive"),
],
)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("positive",)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
@staticmethod
def main(positive):
return positive,
@classmethod
def execute(cls, positive):
return io.NodeOutput(positive)
# 通配符提示词
class wildcardsPrompt:
def __init__(self):
pass
class wildcardsPrompt(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
def define_schema(cls):
wildcard_list = get_wildcard_list()
return {"required": {
"text": ("STRING", {"default": "", "multiline": True, "dynamicPrompts": False, "placeholder": "(Support Lora Block Weight and wildcard)"}),
"Select to add LoRA": (["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras"),),
"Select to add Wildcard": (["Select the Wildcard to add to the text"] + wildcard_list,),
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
"multiline_mode": ("BOOLEAN", {"default": False}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
return io.Schema(
node_id="easy wildcards",
category="EasyUse/Prompt",
inputs=[
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support wildcard)"),
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
io.Int.Input("seed", default=0, min=0, max=MAX_SEED_NUM),
io.Boolean.Input("multiline_mode", default=False),
],
outputs=[
io.String.Output(id="output_text", display_name="text", is_output_list=True),
io.String.Output(id="populated_text", display_name="populated_text", is_output_list=True),
],
hidden=[
io.Hidden.prompt,
io.Hidden.extra_pnginfo,
io.Hidden.unique_id,
],
)
RETURN_TYPES = ("STRING", "STRING")
RETURN_NAMES = ("text", "populated_text")
OUTPUT_IS_LIST = (True, True)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
def translate(self, text):
return text
def main(self, *args, **kwargs):
prompt = kwargs["prompt"] if "prompt" in kwargs else None
seed = kwargs["seed"]
@classmethod
def execute(cls, text, seed, multiline_mode, **kwargs):
prompt = cls.hidden.prompt
# Clean loaded_objects
if prompt:
easyCache.update_loaded_objects(prompt)
text = kwargs['text']
if "multiline_mode" in kwargs and kwargs["multiline_mode"]:
if multiline_mode:
populated_text = []
_text = []
text = text.split("\n")
for t in text:
t = self.translate(t)
text_lines = text.split("\n")
for t in text_lines:
_text.append(t)
populated_text.append(process(t, seed))
text = _text
else:
text = self.translate(text)
populated_text = [process(text, seed)]
text = [text]
return {"ui": {"value": [seed]}, "result": (text, populated_text)}
return io.NodeOutput(text, populated_text, ui={"value": [seed]})
# 通配符提示词矩阵,会按顺序返回包含通配符的提示词所生成的所有可能
class wildcardsPromptMatrix(io.ComfyNode):
@classmethod
def define_schema(cls):
wildcard_list = get_wildcard_list()
return io.Schema(
node_id="easy wildcardsMatrix",
category="EasyUse/Prompt",
inputs=[
io.String.Input("text", default="", multiline=True, dynamic_prompts=False, placeholder="(Support Lora Block Weight and wildcard)"),
io.Combo.Input("Select to add LoRA", options=["Select the LoRA to add to the text"] + folder_paths.get_filename_list("loras")),
io.Combo.Input("Select to add Wildcard", options=["Select the Wildcard to add to the text"] + wildcard_list),
io.Int.Input("offset", default=0, min=0, max=MAX_SEED_NUM, step=1, control_after_generate=True),
io.Int.Input("output_limit", default=1, min=-1, step=1, tooltip="Output All Probilities", optional=True),
],
outputs=[
io.String.Output("populated_text", is_output_list=True),
io.Int.Output("total"),
io.Int.Output("factors", is_output_list=True),
],
hidden=[
io.Hidden.prompt,
io.Hidden.extra_pnginfo,
io.Hidden.unique_id,
],
)
@classmethod
def execute(cls, text, offset, output_limit=1, **kwargs):
prompt = cls.hidden.prompt
# Clean loaded_objects
if prompt:
easyCache.update_loaded_objects(prompt)
p = WildcardProcessor(text)
total = p.total()
limit = total if output_limit > total or output_limit == -1 else output_limit
offset = 0 if output_limit == -1 else offset
populated_text = p.getmany(limit, offset) if output_limit != 1 else [p.getn(offset)]
return io.NodeOutput(populated_text, p.total(), list(p.placeholder_choices.values()), ui={"value": [offset]})
# 负面提示词
class negativePrompt:
def __init__(self):
pass
class negativePrompt(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
return {"required": {
"negative": ("STRING", {"default": "", "multiline": True, "placeholder": "Negative"}),}
}
def define_schema(cls):
return io.Schema(
node_id="easy negative",
category="EasyUse/Prompt",
inputs=[
io.String.Input("negative", default="", multiline=True, placeholder="Negative"),
],
outputs=[
io.String.Output(id="output_negative", display_name="negative"),
],
)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("negative",)
FUNCTION = "main"
CATEGORY = "EasyUse/Prompt"
@staticmethod
def main(negative):
return negative,
@classmethod
def execute(cls, negative):
return io.NodeOutput(negative)
# 风格提示词选择器
class stylesPromptSelector:
class stylesPromptSelector(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
def define_schema(cls):
styles = ["fooocus_styles"]
styles_dir = FOOOCUS_STYLES_DIR
for file_name in os.listdir(styles_dir):
file = os.path.join(styles_dir, file_name)
if os.path.isfile(file) and file_name.endswith(".json"):
styles.append(file_name.split(".")[0])
return {
"required": {
"styles": (styles, {"default": "fooocus_styles"}),
},
"optional": {
"positive": ("STRING", {"forceInput": True}),
"negative": ("STRING", {"forceInput": True}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
if file_name != "fooocus_styles.json":
styles.append(file_name.split(".")[0])
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("positive", "negative",)
return io.Schema(
node_id="easy stylesSelector",
category="EasyUse/Prompt",
inputs=[
io.Combo.Input("styles", options=styles, default="fooocus_styles"),
io.String.Input("positive", default="", force_input=True, optional=True),
io.String.Input("negative", default="", force_input=True, optional=True),
io.Custom(io_type="EASY_PROMPT_STYLES").Input("select_styles", optional=True),
],
outputs=[
io.String.Output(id="output_positive", display_name="positive"),
io.String.Output(id="output_negative", display_name="negative"),
],
hidden=[
io.Hidden.prompt,
io.Hidden.extra_pnginfo,
io.Hidden.unique_id,
],
)
CATEGORY = 'EasyUse/Prompt'
FUNCTION = 'run'
def run(self, styles, positive='', negative='', prompt=None, extra_pnginfo=None, my_unique_id=None):
@classmethod
def execute(cls, styles, positive='', negative='', select_styles=None, **kwargs):
values = []
all_styles = {}
positive_prompt, negative_prompt = '', negative
if styles == "fooocus_styles":
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
if styles == "fooocus_styles" and not os.path.exists(fooocus_custom_dir):
file = os.path.join(RESOURCES_DIR, styles + '.json')
else:
file = os.path.join(FOOOCUS_STYLES_DIR, styles + '.json')
@@ -146,117 +189,135 @@ class stylesPromptSelector:
f.close()
for d in data:
all_styles[d['name']] = d
if my_unique_id in prompt:
if prompt[my_unique_id]["inputs"]['select_styles']:
values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
# if my_unique_id in prompt:
# if prompt[my_unique_id]["inputs"]['select_styles']:
# values = prompt[my_unique_id]["inputs"]['select_styles'].split(',')
if isinstance(select_styles, str):
values = select_styles.split(',')
else:
values = select_styles if select_styles else []
has_prompt = False
if len(values) == 0:
return (positive, negative)
return io.NodeOutput(positive, negative)
for index, val in enumerate(values):
if val not in all_styles:
continue
if 'prompt' in all_styles[val]:
if "{prompt}" in all_styles[val]['prompt'] and has_prompt == False:
positive_prompt = all_styles[val]['prompt'].replace('{prompt}', positive)
has_prompt = True
else:
elif "{prompt}" in all_styles[val]['prompt']:
positive_prompt += ', ' + all_styles[val]['prompt'].replace(', {prompt}', '').replace('{prompt}', '')
else:
positive_prompt = all_styles[val]['prompt'] if positive_prompt == '' else positive_prompt + ', ' + all_styles[val]['prompt']
if 'negative_prompt' in all_styles[val]:
negative_prompt += ', ' + all_styles[val]['negative_prompt'] if negative_prompt else all_styles[val]['negative_prompt']
if has_prompt == False and positive:
positive_prompt = positive + ', '
positive_prompt = positive + positive_prompt + ', '
return (positive_prompt, negative_prompt)
return io.NodeOutput(positive_prompt, negative_prompt)
#prompt
class prompt:
class prompt(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
return {"required": {
"text": ("STRING", {"default": "", "multiline": True, "placeholder": "Prompt"}),
"prefix": (["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], {"default": "Select the prefix add to the text"}),
"subject": (["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], {"default": "👤Select the subject add to the text"}),
"action": (["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], {"default": "🎬Select the action add to the text"}),
"clothes": (["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], {"default": "👚Select the clothes add to the text"}),
"environment": (["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], {"default": "☀️Select the illumination environment add to the text"}),
"background": (["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], {"default": "🎞️Select the background add to the text"}),
"nsfw": (["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], {"default": "🔞️Select the nsfw add to the text"}),
},"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},}
def define_schema(cls):
return io.Schema(
node_id="easy prompt",
category="EasyUse/Prompt",
inputs=[
io.String.Input("text", default="", multiline=True, placeholder="Prompt"),
io.Combo.Input("prefix", options=["Select the prefix add to the text"] + PROMPT_TEMPLATE["prefix"], default="Select the prefix add to the text"),
io.Combo.Input("subject", options=["👤Select the subject add to the text"] + PROMPT_TEMPLATE["subject"], default="👤Select the subject add to the text"),
io.Combo.Input("action", options=["🎬Select the action add to the text"] + PROMPT_TEMPLATE["action"], default="🎬Select the action add to the text"),
io.Combo.Input("clothes", options=["👚Select the clothes add to the text"] + PROMPT_TEMPLATE["clothes"], default="👚Select the clothes add to the text"),
io.Combo.Input("environment", options=["☀️Select the illumination environment add to the text"] + PROMPT_TEMPLATE["environment"], default="☀️Select the illumination environment add to the text"),
io.Combo.Input("background", options=["🎞️Select the background add to the text"] + PROMPT_TEMPLATE["background"], default="🎞️Select the background add to the text"),
io.Combo.Input("nsfw", options=["🔞Select the nsfw add to the text"] + PROMPT_TEMPLATE["nsfw"], default="🔞️Select the nsfw add to the text"),
],
outputs=[
io.String.Output("prompt"),
],
hidden=[
io.Hidden.prompt,
io.Hidden.extra_pnginfo,
io.Hidden.unique_id,
],
)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "doit"
CATEGORY = "EasyUse/Prompt"
def doit(self, *args, **kwargs):
text = kwargs['text']
return (text,)
@classmethod
def execute(cls, text, **kwargs):
return io.NodeOutput(text)
#promptList
class promptList:
class promptList(io.ComfyNode):
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"prompt_1": ("STRING", {"multiline": True, "default": ""}),
"prompt_2": ("STRING", {"multiline": True, "default": ""}),
"prompt_3": ("STRING", {"multiline": True, "default": ""}),
"prompt_4": ("STRING", {"multiline": True, "default": ""}),
"prompt_5": ("STRING", {"multiline": True, "default": ""}),
},
"optional": {
"optional_prompt_list": ("LIST",)
}
}
def define_schema(cls):
return io.Schema(
node_id="easy promptList",
category="EasyUse/Prompt",
inputs=[
io.String.Input("prompt_1", multiline=True, default=""),
io.String.Input("prompt_2", multiline=True, default=""),
io.String.Input("prompt_3", multiline=True, default=""),
io.String.Input("prompt_4", multiline=True, default=""),
io.String.Input("prompt_5", multiline=True, default=""),
io.Custom(io_type="LIST").Input("optional_prompt_list", optional=True),
],
outputs=[
io.Custom(io_type="LIST").Output("prompt_list"),
io.String.Output("prompt_strings", is_output_list=True),
],
)
RETURN_TYPES = ("LIST", "STRING")
RETURN_NAMES = ("prompt_list", "prompt_strings")
OUTPUT_IS_LIST = (False, True)
FUNCTION = "run"
CATEGORY = "EasyUse/Prompt"
def run(self, **kwargs):
@classmethod
def execute(cls, prompt_1="", prompt_2="", prompt_3="", prompt_4="", prompt_5="", optional_prompt_list=None, **kwargs):
prompts = []
if "optional_prompt_list" in kwargs:
for l in kwargs["optional_prompt_list"]:
if optional_prompt_list:
for l in optional_prompt_list:
prompts.append(l)
# Iterate over the received inputs in sorted order.
for k in sorted(kwargs.keys()):
v = kwargs[k]
# Add individual prompts
for p in [prompt_1, prompt_2, prompt_3, prompt_4, prompt_5]:
if isinstance(p, str) and p != '':
prompts.append(p)
# Only process string input ports.
if isinstance(v, str) and v != '':
prompts.append(v)
return (prompts, prompts)
return io.NodeOutput(prompts, prompts)
#promptLine
class promptLine:
class promptLine(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
return {"required": {
"prompt": ("STRING", {"multiline": True, "default": "text"}),
"start_index": ("INT", {"default": 0, "min": 0, "max": 9999}),
"max_rows": ("INT", {"default": 1000, "min": 1, "max": 9999}),
},
"hidden":{
"workflow_prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"
}
}
def define_schema(cls):
return io.Schema(
node_id="easy promptLine",
category="EasyUse/Prompt",
inputs=[
io.String.Input("prompt", multiline=True, default="text"),
io.Int.Input("start_index", default=0, min=0, max=9999),
io.Int.Input("max_rows", default=1000, min=1, max=9999),
io.Boolean.Input("remove_empty_lines", default=True),
],
outputs=[
io.String.Output("STRING", is_output_list=True),
io.Combo.Output("COMBO", is_output_list=True),
],
hidden=[
io.Hidden.prompt,
io.Hidden.unique_id,
],
)
RETURN_TYPES = ("STRING", AlwaysEqualProxy('*'))
RETURN_NAMES = ("STRING", "COMBO")
OUTPUT_IS_LIST = (True, True)
FUNCTION = "generate_strings"
CATEGORY = "EasyUse/Prompt"
def generate_strings(self, prompt, start_index, max_rows, workflow_prompt=None, my_unique_id=None):
@classmethod
def execute(cls, prompt, start_index, max_rows, remove_empty_lines=True, **kwargs):
lines = prompt.split('\n')
# lines = [zh_to_en([v])[0] if has_chinese(v) else v for v in lines if v]
if remove_empty_lines:
lines = [line for line in lines if line.strip()]
start_index = max(0, min(start_index, len(lines) - 1))
@@ -264,58 +325,112 @@ class promptLine:
rows = lines[start_index:end_index]
return (rows, rows)
return io.NodeOutput(rows, rows)
class promptConcat:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
},
"optional": {
"prompt1": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
"prompt2": ("STRING", {"multiline": False, "default": "", "forceInput": True}),
"separator": ("STRING", {"multiline": False, "default": ""}),
},
}
RETURN_TYPES = ("STRING", )
RETURN_NAMES = ("prompt", )
FUNCTION = "concat_text"
CATEGORY = "EasyUse/Prompt"
def concat_text(self, prompt1="", prompt2="", separator=""):
return (prompt1 + separator + prompt2,)
class promptReplace:
import comfy.utils
from server import PromptServer
from ..libs.messages import MessageCancelled, Message
class promptAwait(io.ComfyNode):
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"prompt": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
},
"optional": {
"find1": ("STRING", {"multiline": False, "default": ""}),
"replace1": ("STRING", {"multiline": False, "default": ""}),
"find2": ("STRING", {"multiline": False, "default": ""}),
"replace2": ("STRING", {"multiline": False, "default": ""}),
"find3": ("STRING", {"multiline": False, "default": ""}),
"replace3": ("STRING", {"multiline": False, "default": ""}),
},
}
def define_schema(cls):
return io.Schema(
node_id="easy promptAwait",
category="EasyUse/Prompt",
inputs=[
io.AnyType.Input("now"),
io.String.Input("prompt", multiline=True, default="", placeholder="Enter a prompt or use voice to enter to text"),
io.Custom(io_type="EASY_PROMPT_AWAIT_BAR").Input("toolbar"),
io.AnyType.Input("prev", optional=True),
],
outputs=[
io.AnyType.Output(id="output", display_name="output"),
io.String.Output(id="output_prompt", display_name="prompt"),
io.Boolean.Output("continue"),
io.Int.Output("seed"),
],
hidden=[
io.Hidden.prompt,
io.Hidden.unique_id,
io.Hidden.extra_pnginfo,
],
)
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("prompt",)
FUNCTION = "replace_text"
CATEGORY = "EasyUse/Prompt"
@classmethod
def execute(cls, now, prompt, toolbar, prev=None, **kwargs):
id = cls.hidden.unique_id
id = id.split('.')[len(id.split('.')) - 1] if "." in id else id
if ":" in id:
id = id.split(":")[0]
pbar = comfy.utils.ProgressBar(100)
pbar.update_absolute(30)
PromptServer.instance.send_sync('easyuse_prompt_await', {"id": id})
try:
res = Message.waitForMessage(id, asList=False)
if res is None or res == "-1":
result = (now, prompt, False, 0)
else:
input = now if res['select'] == 'now' or prev is None else prev
result = (input, res['prompt'], False if res['result'] == -1 else True, res['seed'] if res['unlock'] else res['last_seed'])
pbar.update_absolute(100)
return io.NodeOutput(*result)
except MessageCancelled:
pbar.update_absolute(100)
raise comfy.model_management.InterruptProcessingException()
def replace_text(self, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
class promptConcat(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="easy promptConcat",
category="EasyUse/Prompt",
inputs=[
io.String.Input("prompt1", multiline=False, default="", force_input=True, optional=True),
io.String.Input("prompt2", multiline=False, default="", force_input=True, optional=True),
io.String.Input("separator", multiline=False, default="", optional=True),
],
outputs=[
io.String.Output("prompt"),
],
)
@classmethod
def execute(cls, prompt1="", prompt2="", separator=""):
def to_string(value):
if isinstance(value, (list, tuple)):
return ", ".join(to_string(v) for v in value)
return str(value)
return io.NodeOutput(to_string(prompt1) + to_string(separator) + to_string(prompt2))
class promptReplace(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="easy promptReplace",
category="EasyUse/Prompt",
inputs=[
io.String.Input("prompt", multiline=True, default="", force_input=True),
io.String.Input("find1", multiline=False, default="", optional=True),
io.String.Input("replace1", multiline=False, default="", optional=True),
io.String.Input("find2", multiline=False, default="", optional=True),
io.String.Input("replace2", multiline=False, default="", optional=True),
io.String.Input("find3", multiline=False, default="", optional=True),
io.String.Input("replace3", multiline=False, default="", optional=True),
],
outputs=[
io.String.Output(id="output_prompt",display_name="prompt"),
],
)
@classmethod
def execute(cls, prompt, find1="", replace1="", find2="", replace2="", find3="", replace3=""):
prompt = prompt.replace(find1, replace1)
prompt = prompt.replace(find2, replace2)
prompt = prompt.replace(find3, replace3)
return (prompt,)
return io.NodeOutput(prompt)
# 肖像大师
@@ -323,10 +438,10 @@ class promptReplace:
# Version: 2.2
# https://stefanoflore.it
# https://ai-wiz.art
class portraitMaster:
class portraitMaster(io.ComfyNode):
@classmethod
def INPUT_TYPES(s):
def define_schema(cls):
max_float_value = 1.95
prompt_path = os.path.join(RESOURCES_DIR, 'portrait_prompt.json')
if not os.path.exists(prompt_path):
@@ -338,50 +453,72 @@ class portraitMaster:
del response, temp_prompt
# Load local
with open(prompt_path, 'r') as f:
list = json.load(f)
keys = [
['shot', 'COMBO', {"key": "shot_list"}], ['shot_weight', 'FLOAT'],
['gender', 'COMBO', {"default": "Woman", "key": "gender_list"}], ['age', 'INT', {"default": 30, "min": 18, "max": 90, "step": 1, "display": "slider"}],
['nationality_1', 'COMBO', {"default": "Chinese", "key": "nationality_list"}], ['nationality_2', 'COMBO', {"key": "nationality_list"}], ['nationality_mix', 'FLOAT'],
['body_type', 'COMBO', {"key": "body_type_list"}], ['body_type_weight', 'FLOAT'], ['model_pose', 'COMBO', {"key": "model_pose_list"}], ['eyes_color', 'COMBO', {"key": "eyes_color_list"}],
['facial_expression', 'COMBO', {"key": "face_expression_list"}], ['facial_expression_weight', 'FLOAT'], ['face_shape', 'COMBO', {"key": "face_shape_list"}], ['face_shape_weight', 'FLOAT'], ['facial_asymmetry', 'FLOAT'],
['hair_style', 'COMBO', {"key": "hair_style_list"}], ['hair_color', 'COMBO', {"key": "hair_color_list"}], ['disheveled', 'FLOAT'], ['beard', 'COMBO', {"key": "beard_list"}],
['skin_details', 'FLOAT'], ['skin_pores', 'FLOAT'], ['dimples', 'FLOAT'], ['freckles', 'FLOAT'],
['moles', 'FLOAT'], ['skin_imperfections', 'FLOAT'], ['skin_acne', 'FLOAT'], ['tanned_skin', 'FLOAT'],
['eyes_details', 'FLOAT'], ['iris_details', 'FLOAT'], ['circular_iris', 'FLOAT'], ['circular_pupil', 'FLOAT'],
['light_type', 'COMBO', {"key": "light_type_list"}], ['light_direction', 'COMBO', {"key": "light_direction_list"}], ['light_weight', 'FLOAT']
]
widgets = {}
for i, obj in enumerate(keys):
if obj[1] == 'COMBO':
key = obj[2]['key'] if obj[2] and 'key' in obj[2] else obj[0]
_list = list[key].copy()
_list.insert(0, '-')
widgets[obj[0]] = (_list, {**obj[2]})
elif obj[1] == 'FLOAT':
widgets[obj[0]] = ("FLOAT", {"default": 0, "step": 0.05, "min": 0, "max": max_float_value, "display": "slider",})
elif obj[1] == 'INT':
widgets[obj[0]] = (obj[1], obj[2])
del list
return {
"required": {
**widgets,
"photorealism_improvement": (["enable", "disable"],),
"prompt_start": ("STRING", {"multiline": True, "default": "raw photo, (realistic:1.5)"}),
"prompt_additional": ("STRING", {"multiline": True, "default": ""}),
"prompt_end": ("STRING", {"multiline": True, "default": ""}),
"negative_prompt": ("STRING", {"multiline": True, "default": ""}),
}
}
data = json.load(f)
inputs = []
# Shot
inputs.append(io.Combo.Input("shot", options=['-'] + data['shot_list']))
inputs.append(io.Float.Input("shot_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Gender and age
inputs.append(io.Combo.Input("gender", options=['-'] + data['gender_list'], default="Woman"))
inputs.append(io.Int.Input("age", default=30, min=18, max=90, step=1, display_mode=io.NumberDisplay.slider))
# Nationality
inputs.append(io.Combo.Input("nationality_1", options=['-'] + data['nationality_list'], default="Chinese"))
inputs.append(io.Combo.Input("nationality_2", options=['-'] + data['nationality_list']))
inputs.append(io.Float.Input("nationality_mix", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Body
inputs.append(io.Combo.Input("body_type", options=['-'] + data['body_type_list']))
inputs.append(io.Float.Input("body_type_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Combo.Input("model_pose", options=['-'] + data['model_pose_list']))
inputs.append(io.Combo.Input("eyes_color", options=['-'] + data['eyes_color_list']))
# Face
inputs.append(io.Combo.Input("facial_expression", options=['-'] + data['face_expression_list']))
inputs.append(io.Float.Input("facial_expression_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Combo.Input("face_shape", options=['-'] + data['face_shape_list']))
inputs.append(io.Float.Input("face_shape_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("facial_asymmetry", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Hair
inputs.append(io.Combo.Input("hair_style", options=['-'] + data['hair_style_list']))
inputs.append(io.Combo.Input("hair_color", options=['-'] + data['hair_color_list']))
inputs.append(io.Float.Input("disheveled", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Combo.Input("beard", options=['-'] + data['beard_list']))
# Skin details
inputs.append(io.Float.Input("skin_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("skin_pores", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("dimples", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("freckles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("moles", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("skin_imperfections", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("skin_acne", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("tanned_skin", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Eyes
inputs.append(io.Float.Input("eyes_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("iris_details", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("circular_iris", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
inputs.append(io.Float.Input("circular_pupil", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Light
inputs.append(io.Combo.Input("light_type", options=['-'] + data['light_type_list']))
inputs.append(io.Combo.Input("light_direction", options=['-'] + data['light_direction_list']))
inputs.append(io.Float.Input("light_weight", default=0, step=0.05, min=0, max=max_float_value, display_mode=io.NumberDisplay.slider))
# Additional
inputs.append(io.Combo.Input("photorealism_improvement", options=["enable", "disable"]))
inputs.append(io.String.Input("prompt_start", multiline=True, default="raw photo, (realistic:1.5)"))
inputs.append(io.String.Input("prompt_additional", multiline=True, default=""))
inputs.append(io.String.Input("prompt_end", multiline=True, default=""))
inputs.append(io.String.Input("negative_prompt", multiline=True, default=""))
return io.Schema(
node_id="easy portraitMaster",
category="EasyUse/Prompt",
inputs=inputs,
outputs=[
io.String.Output("positive"),
io.String.Output("negative"),
],
)
RETURN_TYPES = ("STRING", "STRING",)
RETURN_NAMES = ("positive", "negative",)
FUNCTION = "pm"
CATEGORY = "EasyUse/Prompt"
def pm(self, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
@classmethod
def execute(cls, shot="-", shot_weight=1, gender="-", body_type="-", body_type_weight=0, eyes_color="-",
facial_expression="-", facial_expression_weight=0, face_shape="-", face_shape_weight=0,
nationality_1="-", nationality_2="-", nationality_mix=0.5, age=30, hair_style="-", hair_color="-",
disheveled=0, dimples=0, freckles=0, skin_pores=0, skin_details=0, moles=0, skin_imperfections=0,
@@ -511,31 +648,148 @@ class portraitMaster:
log_node_info("Portrait Master as generate the prompt:", prompt)
return (prompt, negative_prompt,)
return io.NodeOutput(prompt, negative_prompt)
# 多角度
class multiAngle(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="easy multiAngle",
category="EasyUse/Prompt",
inputs=[
io.Custom(io_type="EASY_MULTI_ANGLE").Input("multi_angle", optional=True),
],
outputs=[
io.String.Output("prompt", is_output_list=True),
io.Custom(io_type="EASY_MULTI_ANGLE").Output("params"),
],
)
@classmethod
def execute(cls, multi_angle=None, **kwargs):
if multi_angle is None:
return io.NodeOutput([""])
if isinstance(multi_angle, str):
try:
multi_angle = json.loads(multi_angle)
except:
raise Exception(f"Invalid multi angle: {multi_angle}")
prompts = []
for angle_data in multi_angle:
rotate = angle_data.get("rotate", 0)
vertical = angle_data.get("vertical", 0)
zoom = angle_data.get("zoom", 5)
add_angle_prompt = angle_data.get("add_angle_prompt", True)
# Validate input ranges
rotate = max(0, min(360, int(rotate)))
vertical = max(-90, min(90, int(vertical)))
zoom = max(0.0, min(10.0, float(zoom)))
h_angle = rotate % 360
# Horizontal direction mapping
h_suffix = "" if add_angle_prompt else " quarter"
if h_angle < 22.5 or h_angle >= 337.5: h_direction = "front view"
elif h_angle < 67.5: h_direction = f"front-right{h_suffix} view"
elif h_angle < 112.5: h_direction = "right side view"
elif h_angle < 157.5: h_direction = f"back-right{h_suffix} view"
elif h_angle < 202.5: h_direction = "back view"
elif h_angle < 247.5: h_direction = f"back-left{h_suffix} view"
elif h_angle < 292.5: h_direction = "left side view"
else: h_direction = f"front-left{h_suffix} view"
# Vertical direction mapping
if add_angle_prompt:
if vertical == -90:
v_direction = "bottom-looking-up perspective, extreme worm's eye view, focus subject bottom"
elif vertical < -75:
v_direction = "bottom-looking-up perspective, extreme worm's eye view"
elif vertical < -45:
v_direction = "ultra-low angle"
elif vertical < -15:
v_direction = "low angle"
elif vertical < 15:
v_direction = "eye level"
elif vertical < 45:
v_direction = "high angle"
elif vertical < 75:
v_direction = "bird's eye view"
elif vertical < 90:
v_direction = "top-down perspective, looking straight down at the top of the subject"
else:
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
else:
if vertical < -15:
v_direction = "low-angle shot"
elif vertical < 15:
v_direction = "eye-level shot"
elif vertical < 45:
v_direction = "elevated shot"
elif vertical < 75:
v_direction = "high-angle shot"
elif vertical < 90:
v_direction = "top-down perspective, looking straight down at the top of the subject"
else:
v_direction = "top-down perspective, looking straight down at the top of the subject, face not visible, focus on subject head"
# Distance/zoom mapping
if add_angle_prompt:
if zoom < 2: distance = "extreme wide shot"
elif zoom < 4: distance = "wide shot"
elif zoom < 6: distance = "medium shot"
elif zoom < 8: distance = "close-up"
else: distance = "extreme close-up"
else:
if zoom < 2: distance = "extreme wide shot"
elif zoom < 4: distance = "wide shot"
elif zoom < 6: distance = "medium shot"
elif zoom < 8: distance = "close-up"
else: distance = "extreme close-up"
# Build prompt
if add_angle_prompt:
prompt = f"{h_direction}, {v_direction}, {distance} (horizontal: {rotate}, vertical: {vertical}, zoom: {zoom:.1f})"
else:
prompt = f"{h_direction} {v_direction} {distance}"
prompts.append(prompt)
return io.NodeOutput(prompts, multi_angle)
NODE_CLASS_MAPPINGS = {
"easy positive": positivePrompt,
"easy negative": negativePrompt,
"easy wildcards": wildcardsPrompt,
"easy wildcardsMatrix": wildcardsPromptMatrix,
"easy prompt": prompt,
"easy promptList": promptList,
"easy promptLine": promptLine,
"easy promptAwait": promptAwait,
"easy promptConcat": promptConcat,
"easy promptReplace": promptReplace,
"easy stylesSelector": stylesPromptSelector,
"easy portraitMaster": portraitMaster,
"easy multiAngle": multiAngle,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy positive": "Positive",
"easy negative": "Negative",
"easy wildcards": "Wildcards",
"easy wildcardsMatrix": "Wildcards Matrix",
"easy prompt": "Prompt",
"easy promptList": "PromptList",
"easy promptLine": "PromptLine",
"easy promptAwait": "PromptAwait",
"easy promptConcat": "PromptConcat",
"easy promptReplace": "PromptReplace",
"easy stylesSelector": "Styles Selector",
"easy portraitMaster": "Portrait Master",
}
"easy multiAngle": "Multi Angle",
}
+36 -51
View File
@@ -1,13 +1,14 @@
import sys, re, time
import torch
import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management, comfy.sampler_helpers, comfy.supported_models
import folder_paths
from comfy.model_patcher import ModelPatcher
from comfy_extras.nodes_mask import GrowMask
import comfy_extras.nodes_custom_sampler as custom_samplers
from tqdm import trange
from server import PromptServer
from nodes import RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, VAEEncodeForInpaint, InpaintModelConditioning
from nodes import RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, VAEEncodeForInpaint, InpaintModelConditioning, VAEDecodeTiled
from ..modules.layer_diffuse import LayerDiffuse
from ..config import *
@@ -15,7 +16,6 @@ from ..libs.log import log_node_warn
from ..libs.utils import easySave, get_local_filepath, get_sd_version
from ..libs.sampler import alignYourStepsScheduler, gitsScheduler
from ..libs.xyplot import easyXYPlot
from ..libs.chooser import ChooserMessage, ChooserCancelled
from .. import easyCache, sampler
@@ -30,7 +30,7 @@ class samplerFull:
"sampler_name": (comfy.samplers.KSampler.SAMPLERS,),
"scheduler": (comfy.samplers.KSampler.SCHEDULERS+NEW_SCHEDULERS,),
"denoise": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -119,7 +119,14 @@ class samplerFull:
def get_custom_cls(self, sampler_name):
try:
cls = custom_samplers.__dict__[sampler_name]
return cls()
cls = cls()
if hasattr(cls, "get_sigmas"):
cls.execute = cls.get_sigmas
elif hasattr(cls, "get_guider"):
cls.execute = cls.get_guider
elif hasattr(cls, "get_sampler"):
cls.execute = cls.get_sampler
return cls
except:
raise Exception(f"Custom sampler {sampler_name} not found, Please updated your ComfyUI")
@@ -157,15 +164,15 @@ class samplerFull:
sigmas = optional_sigmas
else:
if scheduler == 'vp':
sigmas, = self.get_custom_cls('VPScheduler').get_sigmas(steps, beta_d, beta_min, eps_s)
sigmas, = self.get_custom_cls('VPScheduler').execute(steps, beta_d, beta_min, eps_s)
elif scheduler == 'karrasADV':
sigmas, = self.get_custom_cls('KarrasScheduler').get_sigmas(steps, sigma_max, sigma_min, rho)
sigmas, = self.get_custom_cls('KarrasScheduler').execute(steps, sigma_max, sigma_min, rho)
elif scheduler == 'exponentialADV':
sigmas, = self.get_custom_cls('ExponentialScheduler').get_sigmas(steps, sigma_max, sigma_min)
sigmas, = self.get_custom_cls('ExponentialScheduler').execute(steps, sigma_max, sigma_min)
elif scheduler == 'polyExponential':
sigmas, = self.get_custom_cls('PolyexponentialScheduler').get_sigmas(steps, sigma_max, sigma_min, rho)
sigmas, = self.get_custom_cls('PolyexponentialScheduler').execute(steps, sigma_max, sigma_min, rho)
elif scheduler == 'sdturbo':
sigmas, = self.get_custom_cls('SDTurboScheduler').get_sigmas(model, steps, denoise)
sigmas, = self.get_custom_cls('SDTurboScheduler').execute(model, steps, denoise)
elif scheduler == 'alignYourSteps':
model_type = get_sd_version(model)
if model_type == 'unknown':
@@ -174,11 +181,11 @@ class samplerFull:
elif scheduler == 'gits':
sigmas, = gitsScheduler().get_sigmas(coeff, steps, denoise)
else:
sigmas, = self.get_custom_cls('BasicScheduler').get_sigmas(model, scheduler, steps, denoise)
sigmas, = self.get_custom_cls('BasicScheduler').execute(model, scheduler, steps, denoise)
# filp_sigmas
if flip_sigmas:
sigmas, = self.get_custom_cls('FlipSigmas').get_sigmas(sigmas)
sigmas, = self.get_custom_cls('FlipSigmas').execute(sigmas)
#######################################################################################
# brushnet
@@ -210,12 +217,12 @@ class samplerFull:
positive = c
if guider in ['CFG', 'IP2P+CFG']:
_guider, = self.get_custom_cls('CFGGuider').get_guider(model, positive, negative, cfg)
_guider, = self.get_custom_cls('CFGGuider').execute(model, positive, negative, cfg)
elif guider in ['DualCFG', 'IP2P+DualCFG']:
_guider, = self.get_custom_cls('DualCFGGuider').get_guider(model, positive, middle,
_guider, = self.get_custom_cls('DualCFGGuider').execute(model, positive, middle,
negative, cfg, cfg_negative)
else:
_guider, = self.get_custom_cls('BasicGuider').get_guider(model, positive)
_guider, = self.get_custom_cls('BasicGuider').execute(model, positive)
# sampler
if optional_sampler:
@@ -224,7 +231,7 @@ class samplerFull:
if sampler_name == 'inversed_euler':
_sampler, = self.get_inversed_euler_sampler()
else:
_sampler, = self.get_custom_cls('KSamplerSelect').get_sampler(sampler_name)
_sampler, = self.get_custom_cls('KSamplerSelect').execute(sampler_name)
return (_guider, _sampler, sigmas)
@@ -278,18 +285,18 @@ class samplerFull:
if width_downscale_factor > 1.75:
log_node_warn("Patch model unet add downscale...")
log_node_warn("Downscale factor:" + str(width_downscale_factor))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic",
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], width_downscale_factor, 0, 0.35, True, "bicubic",
"bicubic")
elif height_downscale_factor > 1.25:
log_node_warn("Patch model unet add downscale....")
log_node_warn("Downscale factor:" + str(height_downscale_factor))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic",
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], height_downscale_factor, 0, 0.35, True, "bicubic",
"bicubic")
else:
cls = ALL_NODE_CLASS_MAPPINGS['PatchModelAddDownscale']
log_node_warn("Patch model unet add downscale....")
log_node_warn("Downscale factor:" + str(downscale_options['downscale_factor']))
(samp_model,) = cls().patch(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method'])
(samp_model,) = cls().execute(samp_model, downscale_options['block_number'], downscale_options['downscale_factor'], downscale_options['start_percent'], downscale_options['end_percent'], downscale_options['downscale_after_skip'], downscale_options['downscale_method'], downscale_options['upscale_method'])
return samp_model
def process_sample_state(pipe, samp_model, samp_clip, samp_samples, samp_vae, samp_seed, samp_positive,
@@ -355,7 +362,7 @@ class samplerFull:
spent_time = 'Diffusion:' + str((end_time - start_time) / 1000) + '″'
else:
if tile_size is not None:
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
samp_images, = VAEDecodeTiled().decode(samp_vae, {"samples": latent}, tile_size)
else:
samp_images = samp_vae.decode(latent).cpu()
if len(samp_images.shape) == 5: # Combine batches
@@ -389,40 +396,18 @@ class samplerFull:
"loader_settings": {
**pipe["loader_settings"],
"steps": steps,
"cfg": cfg,
"sampler_name": sampler_name,
"scheduler": scheduler,
"denoise": denoise,
"add_noise": add_noise,
"spent_time": spent_time
}
}
del pipe
if image_output == 'Preview&Choose':
if my_unique_id not in ChooserMessage.stash:
ChooserMessage.stash[my_unique_id] = {}
my_stash = ChooserMessage.stash[my_unique_id]
PromptServer.instance.send_sync("easyuse-image-choose", {"id": my_unique_id, "urls": results})
# wait for selection
try:
selections = ChooserMessage.waitForMessage(my_unique_id, asList=True)
samples = samp_samples['samples']
samples = [samples[x] for x in selections if x >= 0] if len(selections) > 1 else [samples[0]]
new_images = [new_images[x] for x in selections if x >= 0] if len(selections) > 1 else [new_images[0]]
samp_images = [samp_images[x] for x in selections if x >= 0] if len(selections) > 1 else [samp_images[0]]
new_images = torch.stack(new_images, dim=0)
samp_images = torch.stack(samp_images, dim=0)
samples = torch.stack(samples, dim=0)
samp_samples = {"samples": samples}
new_pipe['samples'] = samp_samples
new_pipe['loader_settings']['batch_size'] = len(new_images)
except ChooserCancelled:
raise comfy.model_management.InterruptProcessingException()
new_pipe['images'] = new_images
new_pipe['samp_images'] = samp_images
return {"ui": {"images": results},
"result": sampler.get_output(new_pipe,)}
if image_output in ("Hide", "Hide&Save", "None"):
return {"ui":{}, "result":sampler.get_output(new_pipe,)}
@@ -507,7 +492,7 @@ class samplerFull:
samp_samples = {"samples": latents_plot}
images, image_list = sampleXYplot.plot_images_and_labels()
images, image_list = sampleXYplot.plot_images_and_labels(plot_image_vars)
# Generate output_images
output_images = torch.stack([tensor.squeeze() for tensor in image_list])
@@ -588,7 +573,7 @@ class samplerSimple(samplerFull):
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "Preview"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -620,7 +605,7 @@ class samplerSimpleCustom(samplerFull):
def INPUT_TYPES(cls):
return {"required":
{"pipe": ("PIPE_LINE",),
"image_output": (["Hide", "Preview", "Preview&Choose", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
"image_output": (["Hide", "Preview", "Save", "Hide&Save", "Sender", "Sender&Save", "None"],{"default": "None"}),
"link_id": ("INT", {"default": 0, "min": 0, "max": sys.maxsize, "step": 1}),
"save_prefix": ("STRING", {"default": "ComfyUI"}),
},
@@ -995,7 +980,7 @@ class samplerSDTurbo:
# 解码图片
if tile_size is not None:
samp_images = samp_vae.decode_tiled(latent, tile_x=tile_size // 8, tile_y=tile_size // 8, )
samp_images, = VAEDecodeTiled().decode(samp_vae, {"samples": latent}, tile_size)
else:
samp_images = samp_vae.decode(latent).cpu()
+51
View File
@@ -1,4 +1,6 @@
from ..config import MAX_SEED_NUM
import hashlib
import random
class easySeed:
@classmethod
@@ -19,6 +21,53 @@ class easySeed:
def doit(self, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
return seed,
class seedList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"min_num": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM}),
"max_num": ("INT", {"default": MAX_SEED_NUM, "max": MAX_SEED_NUM, "min": 0 }),
"method": (["random", "increment", "decrement"], {"default": "random"}),
"total": ("INT", {"default": 1, "min": 1, "max": 100000}),
"seed": ("INT", {"default": 0, "min": 0, "max": MAX_SEED_NUM,}),
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "my_unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("INT", "INT")
RETURN_NAMES = ("seed", "total")
FUNCTION = "doit"
DESCRIPTION = "Random number seed that can be used in a for loop, by connecting index and easy indexAny node to realize different seed values in the loop."
CATEGORY = "EasyUse/Seed"
def doit(self, min_num, max_num, method, total, seed=0, prompt=None, extra_pnginfo=None, my_unique_id=None):
random.seed(seed)
seed_list = []
if min_num > max_num:
min_num, max_num = max_num, min_num
for i in range(total):
if method == 'random':
s = random.randint(min_num, max_num)
elif method == 'increment':
s = min_num + i
if s > max_num:
s = max_num
elif method == 'decrement':
s = max_num - i
if s < min_num:
s = min_num
seed_list.append(s)
return seed_list, total
@classmethod
def IS_CHANGED(s, seed, **kwargs):
m = hashlib.sha256()
m.update(seed)
return m.digest().hex()
# 全局随机种
class globalSeed:
@classmethod
@@ -46,10 +95,12 @@ class globalSeed:
NODE_CLASS_MAPPINGS = {
"easy seed": easySeed,
"easy seedList": seedList,
"easy globalSeed": globalSeed,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"easy seed": "EasySeed",
"easy seedList": "EasySeedList",
"easy globalSeed": "EasyGlobalSeed",
}
+302 -1
View File
@@ -1,5 +1,7 @@
import os
import re
import folder_paths
import json
from ..libs.utils import AlwaysEqualProxy
class showLoaderSettingsNames:
@@ -106,6 +108,301 @@ class setControlName:
def set_name(self, controlnet_name):
return (controlnet_name,)
class setLoraName:
@classmethod
def INPUT_TYPES(cls):
return {"required": {
"lora_name": (folder_paths.get_filename_list("loras"),),
}
}
RETURN_TYPES = (AlwaysEqualProxy('*'),)
RETURN_NAMES = ("lora_name",)
FUNCTION = "set_name"
CATEGORY = "EasyUse/Util"
def set_name(self, lora_name):
return (lora_name,)
def _markdown_table_to_image(markdown: str, font_path: str):
"""将 Markdown 表格字符串渲染为 PIL.Image(RGB),支持单元格自动换行。"""
from PIL import Image, ImageDraw, ImageFont
# 解析行,过滤分隔行(如 |---|---|)
lines = [l for l in (markdown or '').strip().splitlines() if l.strip()]
table_rows = []
for line in lines:
if re.match(r'^\|[\s\-:|]+\|$', line.strip()):
continue
cells = [re.sub(r'\*\*(.+?)\*\*', lambda m: '\x01' + m.group(1) + '\x02',
re.sub(r'<br\s*/?>', '\n', c.strip(), flags=re.IGNORECASE))
for c in line.strip().strip('|').split('|')]
table_rows.append(cells)
if not table_rows:
return Image.new("RGB", (400, 80), (255, 255, 255))
num_cols = max(len(r) for r in table_rows)
table_rows = [r + [''] * (num_cols - len(r)) for r in table_rows]
# 加载字体
font_size = 16
try:
font = ImageFont.truetype(font_path, font_size)
except Exception:
font = ImageFont.load_default()
pad_x, pad_y = 14, 10
border = 1
max_cell_text_width = 200 # 单元格文字区域最大宽度(像素)
def get_text_width(text):
clean = re.sub('[\x01\x02]', '', text)
try:
bbox = font.getbbox(clean)
return bbox[2] - bbox[0]
except Exception:
return len(clean) * 9
def get_line_height():
try:
bbox = font.getbbox('Ag\u4e2d')
return bbox[3] - bbox[1]
except Exception:
return font_size + 2
def wrap_text(text, max_width):
"""换行:先按 \\n 切段,每段再按英文单词边界 / CJK 字符换行。"""
if not text:
return ['']
# 先按显式换行符切段,再对每段分别软换行
hard_lines = text.split('\n')
if len(hard_lines) > 1:
result = []
for hl in hard_lines:
result.extend(wrap_text(hl, max_width))
return result if result else ['']
# 将文本拆分为:CJK 单字符 / 空白序列 / 非CJK非空白序列(英文单词/标点等)
tokens = re.findall(
r'[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]'
r'|[ \t]+'
r'|[^ \t\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]+',
text
)
result, current = [], ''
for token in tokens:
is_space = token.strip() == ''
test = current + token
if get_text_width(test) <= max_width:
if is_space and not current:
continue # 跳过行首空格
current = test
else:
if is_space:
# 空白处换行,丢弃该空白
if current:
result.append(current)
current = ''
elif get_text_width(token) <= max_width:
# 整个 token 能放一行,整体移到下一行
if current:
result.append(current)
current = token
else:
# token 本身超宽(极长单词),逐字符强拆
for char in token:
if get_text_width(current + char) <= max_width:
current += char
else:
if current:
result.append(current)
current = char
if current:
result.append(current)
return result if result else ['']
line_h = get_line_height()
def parse_line_segments(line):
"""将含 \\x01..\\x02 粗体标记的行拆分为 (text, is_bold) 片段列表。"""
result, bold = [], False
for part in re.split('([\x01\x02])', line):
if part == '\x01':
bold = True
elif part == '\x02':
bold = False
elif part:
result.append((part, bold))
return result or [('', False)]
def balance_bold_markers(lines):
"""确保每行粗体标记自成一对:跨行时在行首补开、行尾补关标记。"""
result, in_bold = [], False
for line in lines:
if in_bold:
line = '\x01' + line
for ch in line:
if ch == '\x01': in_bold = True
elif ch == '\x02': in_bold = False
if in_bold:
line = line + '\x02'
result.append(line)
return result
# 第一遍:计算各列宽度(不超过 max_cell_text_width,按子行分别测量)
col_text_widths = []
for col_idx in range(num_cols):
max_w = 0
for row in table_rows:
cell = row[col_idx] if col_idx < len(row) else ''
for seg_line in cell.split('\n'):
max_w = max(max_w, min(get_text_width(seg_line), max_cell_text_width))
col_text_widths.append(max_w)
col_widths = [w + pad_x * 2 for w in col_text_widths]
# 第二遍:对每行每格换行,计算各行高度
wrapped_rows = []
row_heights = []
for row in table_rows:
wrapped_cells = []
max_lines = 1
for col_idx in range(num_cols):
cell = row[col_idx] if col_idx < len(row) else ''
wrapped = balance_bold_markers(wrap_text(cell, col_text_widths[col_idx]))
wrapped_cells.append(wrapped)
max_lines = max(max_lines, len(wrapped))
wrapped_rows.append(wrapped_cells)
row_heights.append(max_lines * line_h + pad_y * 2)
# 每列左边缘 x 坐标(每列前留 1px 边框)
col_x = [border]
for cw in col_widths:
col_x.append(col_x[-1] + cw + border)
total_width = col_x[-1]
total_height = border + sum(rh + border for rh in row_heights)
# 配色
header_bg = (52, 73, 94)
header_fg = (255, 255, 255)
even_bg = (248, 249, 252)
odd_bg = (255, 255, 255)
border_color = (180, 185, 195)
text_color = (50, 54, 62)
# 以边框色填充整张图,格线自然显现
img = Image.new("RGB", (total_width, total_height), border_color)
draw = ImageDraw.Draw(img)
def render_line(x, y, line, fg):
"""逐片段渲染一行文字;粗体通过向右偏移 1px 再描一遍来模拟加粗。"""
try:
text_offset = -font.getbbox(re.sub('[\x01\x02]', '', line) or 'A')[1]
except Exception:
text_offset = 0
sx = x
for seg, is_bold in parse_line_segments(line):
draw.text((sx, y + text_offset), seg, font=font, fill=fg)
if is_bold:
draw.text((sx + 1, y + text_offset), seg, font=font, fill=fg)
try:
w = font.getbbox(seg)[2] - font.getbbox(seg)[0]
except Exception:
w = len(seg) * 9
sx += w + (1 if is_bold else 0)
row_y = border
for row_idx, (wrapped_cells, rh) in enumerate(zip(wrapped_rows, row_heights)):
is_header = row_idx == 0
bg = header_bg if is_header else (odd_bg if row_idx % 2 == 1 else even_bg)
fg = header_fg if is_header else text_color
for col_idx in range(num_cols):
cx, cw = col_x[col_idx], col_widths[col_idx]
# 填充单元格背景
draw.rectangle([cx, row_y, cx + cw - 1, row_y + rh - 1], fill=bg)
cell_lines = wrapped_cells[col_idx] if col_idx < len(wrapped_cells) else ['']
total_text_h = len(cell_lines) * line_h
ty = row_y + (rh - total_text_h) // 2 # 垂直居中起点
for line_text in cell_lines:
render_line(cx + pad_x, ty, line_text, fg)
ty += line_h
row_y += rh + border
# 最长边不小于 1280,等比放大
min_long_side = 1280
long_side = max(img.width, img.height)
if long_side < min_long_side:
scale = min_long_side / long_side
new_w = round(img.width * scale)
new_h = round(img.height * scale)
img = img.resize((new_w, new_h), Image.LANCZOS)
return img
class tableEditor:
"""表格编辑器节点 —— 通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 字符串。"""
CATEGORY = "EasyUse/Util"
RETURN_TYPES = ("STRING", "IMAGE")
RETURN_NAMES = ("markdown", "image")
FUNCTION = "execute"
DESCRIPTION = "通过可视化表格或 Markdown 语法编辑数据,输出 Markdown 格式的表格字符串。"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"table_data": ("EASY_TABLE_EDITOR",),
},
}
def execute(self, table_data):
# 表格数据可能是纯 Markdown 字符串,也可能是序列化后的 JSON
if isinstance(table_data, str) and table_data.strip().startswith('{'):
try:
obj = json.loads(table_data)
markdown = obj.get('markdown', '')
if not markdown:
# 重新从 headers/rows 生成
headers = obj.get('headers', [])
rows = obj.get('rows', [])
col_widths = [max(len(str(h)), 3) for h in headers]
for row in rows:
for i, cell in enumerate(row):
if i < len(col_widths):
col_widths[i] = max(col_widths[i], len(str(cell)))
header_line = '| ' + ' | '.join(str(h).ljust(col_widths[i]) for i, h in enumerate(headers)) + ' |'
sep_line = '| ' + ' | '.join('-' * w for w in col_widths) + ' |'
row_lines = [
'| ' + ' | '.join(str(row[i] if i < len(row) else '').ljust(col_widths[i]) for i in range(len(headers))) + ' |'
for row in rows
]
markdown = '\n'.join([header_line, sep_line] + row_lines)
except Exception:
markdown = table_data
else:
markdown = table_data
# 将 Markdown 表格渲染为图像
font_path = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
'resources', 'wenquan.ttf'
)
from ..libs.image import pil2tensor
img_tensor = pil2tensor(_markdown_table_to_image(markdown, font_path).convert("RGB"))
return (markdown, img_tensor)
NODE_CLASS_MAPPINGS = {
@@ -113,6 +410,8 @@ NODE_CLASS_MAPPINGS = {
"easy sliderControl": sliderControl,
"easy ckptNames": setCkptName,
"easy controlnetNames": setControlName,
"easy loraNames": setLoraName,
"easy tableEditor": tableEditor,
}
NODE_DISPLAY_NAME_MAPPINGS = {
@@ -120,4 +419,6 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"easy sliderControl": "Easy Slider Control",
"easy ckptNames": "Ckpt Names",
"easy controlnetNames": "ControlNet Names",
}
"easy loraNames": "Lora Names",
"easy tableEditor": "Table Editor",
}
+13 -10
View File
@@ -413,6 +413,9 @@ class XYplot_Control_Net:
"start_percent": ("FLOAT", {"default": 0.0, "min": 0.00, "max": 1.0, "step": 0.01}),
"end_percent": ("FLOAT", {"default": 1.0, "min": 0.00, "max": 1.0, "step": 0.01}),
},
"optional": {
"control_net": ("CONTROL_NET",),
},
}
RETURN_TYPES = ("X_Y",)
@@ -421,7 +424,7 @@ class XYplot_Control_Net:
CATEGORY = "EasyUse/XY Inputs"
def xy_value(self, control_net_name, image, target_parameter, batch_count, first_strength, last_strength, first_start_percent,
last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent):
last_start_percent, first_end_percent, last_end_percent, strength, start_percent, end_percent, control_net=None):
axis, = None,
@@ -430,38 +433,38 @@ class XYplot_Control_Net:
if target_parameter == "strength":
axis = "advanced: ControlNetStrength"
values.append([(control_net_name, image, first_strength, start_percent, end_percent)])
values.append([(control_net_name, image, first_strength, start_percent, end_percent, control_net)])
strength_increment = (last_strength - first_strength) / (batch_count - 1) if batch_count > 1 else 0
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, first_strength + i * strength_increment, start_percent,
end_percent)])
end_percent, control_net)])
if batch_count > 1:
values.append([(control_net_name, image, last_strength, start_percent, end_percent)])
values.append([(control_net_name, image, last_strength, start_percent, end_percent, control_net)])
elif target_parameter == "start_percent":
axis = "advanced: ControlNetStart%"
percent_increment = (last_start_percent - first_start_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append([(control_net_name, image, strength, first_start_percent, end_percent)])
values.append([(control_net_name, image, strength, first_start_percent, end_percent, control_net)])
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, strength, first_start_percent + i * percent_increment,
end_percent)])
end_percent, control_net)])
# Always add the last start_percent if batch_count is more than 1.
if batch_count > 1:
values.append((control_net_name, image, strength, last_start_percent, end_percent))
values.append([(control_net_name, image, strength, last_start_percent, end_percent, control_net)])
elif target_parameter == "end_percent":
axis = "advanced: ControlNetEnd%"
percent_increment = (last_end_percent - first_end_percent) / (batch_count - 1) if batch_count > 1 else 0
values.append([(control_net_name, image, image, strength, start_percent, first_end_percent)])
values.append([(control_net_name, image, strength, start_percent, first_end_percent, control_net)])
for i in range(1, batch_count - 1):
values.append([(control_net_name, image, strength, start_percent,
first_end_percent + i * percent_increment)])
first_end_percent + i * percent_increment, control_net)])
if batch_count > 1:
values.append([(control_net_name, image, strength, start_percent, last_end_percent)])
values.append([(control_net_name, image, strength, start_percent, last_end_percent, control_net)])
return ({"axis": axis, "values": values},)
+38 -33
View File
@@ -12,12 +12,19 @@ from .libs.utils import getMetadata, cleanGPUUsedForce, get_local_filepath
from .libs.cache import remove_cache
from .libs.translate import has_chinese, zh_to_en
@PromptServer.instance.routes.get('/easyuse/version')
def get_version(request):
try:
from .. import __version__
return web.json_response({"version": __version__})
except Exception as e:
print(e)
return web.Response(status=500)
@PromptServer.instance.routes.post("/easyuse/cleangpu")
def cleanGPU(request):
try:
cleanGPUUsedForce()
remove_cache('*')
return web.Response(status=200)
except Exception as e:
return web.Response(status=500)
@@ -70,13 +77,14 @@ async def parse_csv(request):
@PromptServer.instance.routes.get("/easyuse/prompt/styles")
async def getStylesList(request):
if "name" in request.rel_url.query:
name = request.rel_url.query["name"]
if name == 'fooocus_styles':
file = os.path.join(RESOURCES_DIR, name+'.json')
cn_file = os.path.join(RESOURCES_DIR, name + '_cn.json')
style_name = request.rel_url.query["name"]
fooocus_custom_dir = os.path.join(FOOOCUS_STYLES_DIR, 'fooocus_styles.json')
if style_name == 'fooocus_styles' and not os.path.exists(fooocus_custom_dir):
file = os.path.join(RESOURCES_DIR, style_name+'.json')
cn_file = os.path.join(RESOURCES_DIR, style_name + '_cn.json')
else:
file = os.path.join(FOOOCUS_STYLES_DIR, name+'.json')
cn_file = os.path.join(FOOOCUS_STYLES_DIR, name + '_cn.json')
file = os.path.join(FOOOCUS_STYLES_DIR, style_name+'.json')
cn_file = os.path.join(FOOOCUS_STYLES_DIR, style_name + '_cn.json')
cn_data = None
if os.path.isfile(cn_file):
f = open(cn_file, 'r', encoding='utf-8')
@@ -95,13 +103,25 @@ async def getStylesList(request):
key = ' '.join(
word.upper() if word.lower() in ['mre', 'sai', '3d'] else word.capitalize() for word in
words)
img_name = '_'.join(words).lower()
if "name_cn" in d:
nd['name_cn'] = d['name_cn']
elif cn_data:
nd['name_cn'] = cn_data[key] if key in cn_data else key
nd["name"] = d['name']
nd['imgName'] = img_name
if "thumbnail" in d:
thumbnail = d['thumbnail']
if isinstance(d['thumbnail'], str):
nd['thumbnail'] = thumbnail if "http" in thumbnail else f'/easyuse/prompt/styles/image?path={thumbnail}'
elif isinstance(d['thumbnail'], list):
nd['thumbnail'] = [thumb if "http" in thumb else f'/easyuse/prompt/styles/image?path={thumb}' for thumb in thumbnail]
else:
nd['thumbnail'] = f'/easyuse/prompt/styles/image?name={name}&styles_name={style_name}'
if "thumbnail_variant" in d:
nd['thumbnailVariant'] = d['thumbnail_variant']
if "media_type" in d:
nd['mediaType'] = d['media_type']
if "media_subtype" in d:
nd['mediaSubtype'] = d['media_subtype']
if "prompt" in d:
nd['prompt'] = d['prompt']
if "negative_prompt" in d:
@@ -114,7 +134,15 @@ async def getStylesList(request):
@PromptServer.instance.routes.get("/easyuse/prompt/styles/image")
async def getStylesImage(request):
styles_name = request.rel_url.query["styles_name"] if "styles_name" in request.rel_url.query else None
if "name" in request.rel_url.query:
if "path" in request.rel_url.query:
path = request.rel_url.query["path"]
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', path)
parent_file = os.path.join(FOOOCUS_STYLES_DIR, path)
if os.path.isfile(file):
return web.FileResponse(file)
elif os.path.isfile(parent_file):
return web.FileResponse(parent_file)
elif "name" in request.rel_url.query:
name = request.rel_url.query["name"]
if os.path.exists(os.path.join(FOOOCUS_STYLES_DIR, 'samples')):
file = os.path.join(FOOOCUS_STYLES_DIR, 'samples', name + '.jpg')
@@ -138,29 +166,6 @@ async def getModelsList(request):
else:
return web.Response(status=400)
# get models thumbnails
@PromptServer.instance.routes.get("/easyuse/models/thumbnail")
async def getModelsThumbnail(request):
limit = 500
if "limit" in request.rel_url.query:
limit = request.rel_url.query.get("limit")
limit = int(limit)
checkpoints = folder_paths.get_filename_list("checkpoints_thumb")
loras = folder_paths.get_filename_list("loras_thumb")
checkpoints_full = []
loras_full = []
if len(checkpoints) + len(loras) >= limit:
return web.Response(status=400)
for index, i in enumerate(checkpoints):
full_path = folder_paths.get_full_path('checkpoints_thumb', str(i))
if full_path:
checkpoints_full.append(full_path)
for index, i in enumerate(loras):
full_path = folder_paths.get_full_path('loras_thumb', str(i))
if full_path:
loras_full.append(full_path)
return web.json_response(checkpoints_full + loras_full)
@PromptServer.instance.routes.post("/easyuse/metadata/notes/{name}")
async def save_notes(request):
name = request.match_info["name"]
+7 -3
View File
@@ -2,6 +2,10 @@ import random
import server
from enum import Enum
# Dedicated RNG for seed generation. Many custom nodes call random.seed() while they run,
# which resets the global RNG and makes the generated seeds repeat.
_seed_rng = random.Random()
class SGmode(Enum):
FIX = 1
INCR = 2
@@ -34,7 +38,7 @@ class SeedGenerator:
if self.base_value < 0:
self.base_value = 1125899906842624
elif self.action == SGmode.RAND:
self.base_value = random.randint(0, 1125899906842624)
self.base_value = _seed_rng.randint(0, 1125899906842624)
return seed
@@ -52,7 +56,7 @@ def control_seed(v, action, seed_is_global):
if value < 0:
value = 1125899906842624
elif action == 'randomize' or action == 'randomize for each node':
value = random.randint(0, 1125899906842624)
value = _seed_rng.randint(0, 1125899906842624)
if seed_is_global:
v['inputs']['value'] = value
@@ -163,4 +167,4 @@ def onprompt(json_data):
return json_data
server.PromptServer.instance.add_on_prompt_handler(onprompt)
server.PromptServer.instance.add_on_prompt_handler(onprompt)
+3 -3
View File
@@ -1,9 +1,9 @@
[project]
name = "comfyui-easy-use"
description = "To enhance the usability of ComfyUI, optimizations and integrations have been implemented for several commonly used nodes."
version = "1.2.7"
version = "1.4.1"
license = { file = "LICENSE" }
dependencies = ["diffusers", "accelerate", "clip_interrogator>=0.6.0", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python", "matplotlib", "peft"]
dependencies = ["diffusers", "accelerate", "clip_interrogator", "sentencepiece", "lark", "onnxruntime", "spandrel", "opencv-python-headless", "matplotlib", "peft"]
[project.urls]
Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
@@ -12,4 +12,4 @@ Repository = "https://github.com/yolain/ComfyUI-Easy-Use"
[tool.comfy]
PublisherId = "yolain"
DisplayName = "ComfyUI-Easy-Use"
Icon = ""
Icon = "https://mintlify.s3.us-west-1.amazonaws.com/yolain/images/logo.svg"
+1 -1
View File
@@ -3,7 +3,7 @@ accelerate
clip_interrogator>=0.6.0
lark
onnxruntime
opencv-python
opencv-python-headless
sentencepiece
spandrel
matplotlib
+1923 -1373
View File
File diff suppressed because it is too large Load Diff
-279
View File
@@ -1,279 +0,0 @@
{
"Fooocus V2": "Fooocus V2扩展词",
"Default (Slightly Cinematic)": "默认(轻微的电影感)",
"Fooocus Enhance": "Fooocus-优化增强",
"Fooocus Cinematic": "Fooocus-电影感",
"Fooocus Sharp": "Fooocus-锐化",
"Fooocus Masterpiece": "Fooocus-杰作",
"Fooocus Photograph": "Fooocus-照片",
"Fooocus Negative": "Fooocus-反向提示词",
"SAI 3D Model": "SAI-3D模型",
"SAI Analog Film": "SAI-模拟电影",
"SAI Anime": "SAI-动漫",
"SAI Cinematic": "SAI-电影片段",
"SAI Comic Book": "SAI-漫画",
"SAI Craft Clay": "SAI-工艺粘土",
"SAI Digital Art": "SAI-数字艺术",
"SAI Enhance": "SAI-增强",
"SAI Fantasy Art": "SAI-奇幻艺术",
"SAI Isometric": "SAI-等距风格",
"SAI Line Art": "SAI-线条艺术",
"SAI Lowpoly": "SAI-低多边形",
"SAI Neonpunk": "SAI-霓虹朋克",
"SAI Origami": "SAI-折纸",
"SAI Photographic": "SAI-摄影",
"SAI Pixel Art": "SAI-像素艺术",
"SAI Texture": "SAI-纹理",
"MRE Cinematic Dynamic": "MRE-史诗电影",
"MRE Spontaneous Picture": "MRE-自然的抓拍照片",
"MRE Artistic Vision": "MRE-艺术视觉",
"MRE Dark Dream": "MRE-黑暗梦境",
"MRE Gloomy Art": "MRE-阴郁艺术",
"MRE Bad Dream": "MRE-噩梦",
"MRE Underground": "MRE-阴森地下",
"MRE Surreal Painting": "MRE-超现实主义绘画",
"MRE Dynamic Illustration": "MRE-动态插画",
"MRE Undead Art": "MRE-遗忘艺术家作品",
"MRE Elemental Art": "MRE-元素艺术",
"MRE Space Art": "MRE-空间艺术",
"MRE Ancient Illustration": "MRE-古代插图",
"MRE Brave Art": "MRE-勇敢艺术",
"MRE Heroic Fantasy": "MRE-英雄幻想",
"MRE Dark Cyberpunk": "MRE-黑暗赛博朋克",
"MRE Lyrical Geometry": "MRE-抒情几何抽象画",
"MRE Sumi E Symbolic": "MRE-墨绘长笔画",
"MRE Sumi E Detailed": "MRE-精细墨绘画",
"MRE Manga": "MRE-日本漫画",
"MRE Anime": "MRE-日本动画片",
"MRE Comic": "MRE-成人漫画书插画",
"Ads Advertising": "广告-广告",
"Ads Automotive": "广告-汽车",
"Ads Corporate": "广告-企业品牌",
"Ads Fashion Editorial": "广告-时尚编辑",
"Ads Food Photography": "广告-食品摄影",
"Ads Gourmet Food Photography": "广告-顶级美食摄影",
"Ads Luxury": "广告-奢侈品",
"Ads Real Estate": "广告-房地产",
"Ads Retail": "广告-零售",
"Artstyle Abstract": "艺术风格-抽象",
"Artstyle Abstract Expressionism": "艺术风格-抽象表现主义",
"Artstyle Art Deco": "艺术风格-装饰艺术",
"Artstyle Art Nouveau": "艺术风格-新艺术",
"Artstyle Constructivist": "艺术风格-构造主义",
"Artstyle Cubist": "艺术风格-立体主义",
"Artstyle Expressionist": "艺术风格-表现主义",
"Artstyle Graffiti": "艺术风格-涂鸦",
"Artstyle Hyperrealism": "艺术风格-超写实主义",
"Artstyle Impressionist": "艺术风格-印象派",
"Artstyle Pointillism": "艺术风格-点彩派",
"Artstyle Pop Art": "艺术风格-波普艺术",
"Artstyle Psychedelic": "艺术风格-迷幻",
"Artstyle Renaissance": "艺术风格-文艺复兴",
"Artstyle Steampunk": "艺术风格-蒸汽朋克",
"Artstyle Surrealist": "艺术风格-超现实主义",
"Artstyle Typography": "艺术风格-字体设计",
"Artstyle Watercolor": "艺术风格-水彩",
"Futuristic Biomechanical": "未来主义-生物机械",
"Futuristic Biomechanical Cyberpunk": "未来主义-生物机械-赛博朋克",
"Futuristic Cybernetic": "未来主义-人机融合",
"Futuristic Cybernetic Robot": "未来主义-人机融合-机器人",
"Futuristic Cyberpunk Cityscape": "未来主义-赛博朋克城市",
"Futuristic Futuristic": "未来主义-未来主义",
"Futuristic Retro Cyberpunk": "未来主义-复古赛博朋克",
"Futuristic Retro Futurism": "未来主义-复古未来主义",
"Futuristic Sci Fi": "未来主义-科幻",
"Futuristic Vaporwave": "未来主义-蒸汽波",
"Game Bubble Bobble": "游戏-泡泡龙",
"Game Cyberpunk Game": "游戏-赛博朋克游戏",
"Game Fighting Game": "游戏-格斗游戏",
"Game Gta": "游戏-侠盗猎车手",
"Game Mario": "游戏-马里奥",
"Game Minecraft": "游戏-我的世界",
"Game Pokemon": "游戏-宝可梦",
"Game Retro Arcade": "游戏-复古街机",
"Game Retro Game": "游戏-复古游戏",
"Game Rpg Fantasy Game": "游戏-角色扮演幻想游戏",
"Game Strategy Game": "游戏-策略游戏",
"Game Streetfighter": "游戏-街头霸王",
"Game Zelda": "游戏-塞尔达传说",
"Misc Architectural": "其他-建筑",
"Misc Disco": "其他-迪斯科",
"Misc Dreamscape": "其他-梦境",
"Misc Dystopian": "其他-反乌托邦",
"Misc Fairy Tale": "其他-童话故事",
"Misc Gothic": "其他-哥特风",
"Misc Grunge": "其他-垮掉的",
"Misc Horror": "其他-恐怖",
"Misc Kawaii": "其他-可爱",
"Misc Lovecraftian": "其他-洛夫克拉夫特",
"Misc Macabre": "其他-恐怖",
"Misc Manga": "其他-漫画",
"Misc Metropolis": "其他-大都市",
"Misc Minimalist": "其他-极简主义",
"Misc Monochrome": "其他-单色",
"Misc Nautical": "其他-航海",
"Misc Space": "其他-太空",
"Misc Stained Glass": "其他-彩色玻璃",
"Misc Techwear Fashion": "其他-科技时尚",
"Misc Tribal": "其他-部落",
"Misc Zentangle": "其他-禅绕画",
"Papercraft Collage": "手工艺-拼贴",
"Papercraft Flat Papercut": "手工艺-平面剪纸",
"Papercraft Kirigami": "手工艺-切纸",
"Papercraft Paper Mache": "手工艺-纸浆塑造",
"Papercraft Paper Quilling": "手工艺-纸艺卷轴",
"Papercraft Papercut Collage": "手工艺-剪纸拼贴",
"Papercraft Papercut Shadow Box": "手工艺-剪纸影箱",
"Papercraft Stacked Papercut": "手工艺-层叠剪纸",
"Papercraft Thick Layered Papercut": "手工艺-厚层剪纸",
"Photo Alien": "摄影-外星人",
"Photo Film Noir": "摄影-黑色电影",
"Photo Glamour": "摄影-魅力",
"Photo Hdr": "摄影-高动态范围",
"Photo Iphone Photographic": "摄影-苹果手机摄影",
"Photo Long Exposure": "摄影-长曝光",
"Photo Neon Noir": "摄影-霓虹黑色",
"Photo Silhouette": "摄影-轮廓",
"Photo Tilt Shift": "摄影-移轴",
"Cinematic Diva": "电影女主角",
"Abstract Expressionism": "抽象表现主义",
"Academia": "学术",
"Action Figure": "动作人偶",
"Adorable 3D Character": "可爱的3D角色",
"Adorable Kawaii": "可爱的卡哇伊",
"Art Deco": "装饰艺术",
"Art Nouveau": "新艺术,美丽艺术",
"Astral Aura": "星体光环",
"Avant Garde": "前卫",
"Baroque": "巴洛克",
"Bauhaus Style Poster": "包豪斯风格海报",
"Blueprint Schematic Drawing": "蓝图示意图",
"Caricature": "漫画",
"Cel Shaded Art": "卡通渲染",
"Character Design Sheet": "角色设计表",
"Classicism Art": "古典主义艺术",
"Color Field Painting": "色彩领域绘画",
"Colored Pencil Art": "彩色铅笔艺术",
"Conceptual Art": "概念艺术",
"Constructivism": "建构主义",
"Cubism": "立体主义",
"Dadaism": "达达主义",
"Dark Fantasy": "黑暗奇幻",
"Dark Moody Atmosphere": "黑暗忧郁气氛",
"Dmt Art Style": "迷幻艺术风格",
"Doodle Art": "涂鸦艺术",
"Double Exposure": "双重曝光",
"Dripping Paint Splatter Art": "滴漆飞溅艺术",
"Expressionism": "表现主义",
"Faded Polaroid Photo": "褪色的宝丽来照片",
"Fauvism": "野兽派",
"Flat 2d Art": "平面 2D 艺术",
"Fortnite Art Style": "堡垒之夜艺术风格",
"Futurism": "未来派",
"Glitchcore": "故障核心",
"Glo Fi": "光明高保真",
"Googie Art Style": "古吉艺术风格",
"Graffiti Art": "涂鸦艺术",
"Harlem Renaissance Art": "哈莱姆文艺复兴艺术",
"High Fashion": "高级时装",
"Idyllic": "田园诗般",
"Impressionism": "印象派",
"Infographic Drawing": "信息图表绘图",
"Ink Dripping Drawing": "滴墨绘画",
"Japanese Ink Drawing": "日式水墨画",
"Knolling Photography": "规律摆放摄影",
"Light Cheery Atmosphere": "轻松愉快的气氛",
"Logo Design": "标志设计",
"Luxurious Elegance": "奢华优雅",
"Macro Photography": "微距摄影",
"Mandola Art": "曼陀罗艺术",
"Marker Drawing": "马克笔绘图",
"Medievalism": "中世纪主义",
"Minimalism": "极简主义",
"Neo Baroque": "新巴洛克",
"Neo Byzantine": "新拜占庭",
"Neo Futurism": "新未来派",
"Neo Impressionism": "新印象派",
"Neo Rococo": "新洛可可",
"Neoclassicism": "新古典主义",
"Op Art": "欧普艺术",
"Ornate And Intricate": "华丽而复杂",
"Pencil Sketch Drawing": "铅笔素描",
"Pop Art 2": "流行艺术2",
"Rococo": "洛可可",
"Silhouette Art": "剪影艺术",
"Simple Vector Art": "简单矢量艺术",
"Sketchup": "草图",
"Steampunk 2": "赛博朋克2",
"Surrealism": "超现实主义",
"Suprematism": "至上主义",
"Terragen": "地表风景",
"Tranquil Relaxing Atmosphere": "宁静轻松的氛围",
"Sticker Designs": "贴纸设计",
"Vibrant Rim Light": "生动的边缘光",
"Volumetric Lighting": "体积照明",
"Watercolor 2": "水彩2",
"Whimsical And Playful": "异想天开、俏皮",
"Mk Chromolithography": "MK 色彩版画",
"Mk Cross Processing Print": "MK 交叉过程打印",
"Mk Dufaycolor Photograph": "MK 杜法色彩照片",
"Mk Herbarium": "MK 植物标本馆",
"Mk Punk Collage": "MK 朋克拼贴画",
"Mk Mosaic": "MK 镶嵌图",
"Mk Van Gogh": "MK 梵高",
"Mk Coloring Book": "MK 色彩书",
"Mk Singer Sargent": "MK 辛格 · 萨尔生特",
"Mk Pollock": "MK 波洛克",
"Mk Basquiat": "MK 巴斯奎特",
"Mk Andy Warhol": "MK 安迪 · 沃霍尔",
"Mk Halftone Print": "MK 半色版画",
"Mk Gond Painting": "MK 贡德绘画",
"Mk Albumen Print": "MK 白蛋清印刷",
"Mk Aquatint Print": "MK 水蚀刻印刷",
"Mk Anthotype Print": "MK 花纹版画",
"Mk Inuit Carving": "MK 因纽特雕塑",
"Mk Bromoil Print": "MK 溴油印刷",
"Mk Calotype Print": "MK 卡洛雅图印刷",
"Mk Color Sketchnote": "MK色彩素描笔记",
"Mk Cibulak Porcelain": "MK 西布拉瓷器",
"Mk Alcohol Ink Art": "MK 酒精水彩艺术",
"Mk One Line Art": "MK 一线画",
"Mk Blacklight Paint": "MK 黑光油漆",
"Mk Carnival Glass": "MK 嘉年华玻璃",
"Mk Cyanotype Print": "MK 青色版画",
"Mk Cross Stitching": "MK 交叉针织",
"Mk Encaustic Paint": "MK 蜡漆",
"Mk Embroidery": "MK 刺绣",
"Mk Gyotaku": "MK 鱼拓版画",
"Mk Luminogram": "MK 光感影像",
"Mk Lite Brite Art": "MK 彩色灯泡艺术",
"Mk Mokume Gane": "MK 木金工艺",
"Pebble Art": "MK 鹅卵石艺术",
"Mk Palekh": "MK 帕列赫",
"Mk Suminagashi": "MK 澄洗画",
"Mk Scrimshaw": "MK 丝线绣",
"Mk Shibori": "MK 湿布雕版印刷",
"Mk Vitreous Enamel": "MK 玻璃珐琅",
"Mk Ukiyo E": "MK 浮世绘",
"Mk Vintage Airline Poster": "MK 古董航空公司海报",
"Mk Vintage Travel Poster": "MK 古董旅行海报",
"Mk Bauhaus Style": "Mk 包豪斯风格",
"Mk Afrofuturism": "Mk 非洲未来主义",
"Mk Atompunk": "Mk 原子朋克",
"Mk Constructivism": "Mk 构成派",
"Mk Chicano Art": "Mk 西班牙裔美国艺术",
"Mk De Stijl": "Mk 去风格派",
"Mk Dayak Art": "Mk 达雅克艺术",
"Mk Fayum Portrait": "Mk 法尤姆肖像画",
"Mk Illuminated Manuscript": "Mk 彩绘手稿",
"Mk Kalighat Painting": "Mk 卡利加特绘画",
"Mk Madhubani Painting": "Mk 马杜班尼绘画",
"Mk Pictorialism": "Mk 描绘主义",
"Mk Pichwai Painting": "Mk 皮奇瓦伊绘画",
"Mk Patachitra Painting": "Mk 帕塔基特拉绘画",
"Mk Samoan Art Inspired": "Mk 萨莫亚艺术启发的",
"Mk Tlingit Art": "Mk 特林吉特艺术",
"Mk Adnate Style": "Mk 阿达内特风格",
"Mk Ron English Style": "Mk 罗恩英国风格",
"Mk Shepard Fairey Style": "Mk 舒帕德 · 费尔利风格"
}
Binary file not shown.
+71
View File
@@ -0,0 +1,71 @@
import importlib.util
import os
from pathlib import Path
import tempfile
import unittest
MODULE_PATH = Path(__file__).parents[1] / "py" / "libs" / "path_utils.py"
SPEC = importlib.util.spec_from_file_location("easyuse_path_utils", MODULE_PATH)
path_utils = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(path_utils)
class ResolveOutputFilePathTests(unittest.TestCase):
def test_relative_subdirectory_is_resolved_under_output_root(self):
with tempfile.TemporaryDirectory() as output_root:
result = path_utils.resolve_output_file_path(
output_root, "metadata", "prompt", "txt"
)
self.assertEqual(
result,
os.path.join(os.path.realpath(output_root), "metadata", "prompt.txt"),
)
def test_absolute_directory_inside_output_root_is_allowed(self):
with tempfile.TemporaryDirectory() as output_root:
inside = os.path.join(output_root, "metadata")
result = path_utils.resolve_output_file_path(
output_root, inside, "prompt", "txt"
)
self.assertEqual(result, os.path.join(inside, "prompt.txt"))
def test_absolute_directory_outside_output_root_is_rejected(self):
with tempfile.TemporaryDirectory() as output_root:
with tempfile.TemporaryDirectory() as outside:
with self.assertRaises(ValueError):
path_utils.resolve_output_file_path(
output_root, outside, "marker", "txt"
)
def test_output_directory_traversal_is_rejected(self):
with tempfile.TemporaryDirectory() as output_root:
with self.assertRaises(ValueError):
path_utils.resolve_output_file_path(
output_root, "../outside", "marker", "txt"
)
def test_file_name_traversal_is_rejected(self):
with tempfile.TemporaryDirectory() as output_root:
with self.assertRaises(ValueError):
path_utils.resolve_output_file_path(
output_root, ".", "../../marker", "txt"
)
@unittest.skipUnless(hasattr(os, "symlink"), "symlinks are unavailable")
def test_symlink_escape_is_rejected(self):
with tempfile.TemporaryDirectory() as output_root:
with tempfile.TemporaryDirectory() as outside:
os.symlink(outside, os.path.join(output_root, "linked"))
with self.assertRaises(ValueError):
path_utils.resolve_output_file_path(
output_root, "linked", "marker", "txt"
)
if __name__ == "__main__":
unittest.main()
-50
View File
@@ -1,50 +0,0 @@
# 开发人员使用(请勿运行)
# 将 https://github.com/AIGODLIKE/AIGODLIKE-ComfyUI-Translation 的翻译文件转换格式以适配 ComfyUI核心 locales
import json
import os
import pathlib
old_json_path = 'ComfyUI-Easy-Use.json'
root_path = pathlib.Path(__file__).parent.parent
new_json_path = os.path.join(root_path,'locales/zh/nodeDefs.json')
def transform_dict(data):
new_dict = {}
for k, v in data.items():
new_dict[k] = {
"display_name": "",
"inputs": {}
}
if isinstance(v, dict):
for key, value in v.items():
if key == 'title':
new_dict[k]['display_name'] = value
elif key in ['inputs','widgets']:
for _key, _value in value.items():
new_dict[k]['inputs'] = {
**new_dict[k]['inputs'],
_key: {"name": _value}
}
elif key == 'outputs':
if not new_dict[k].get('outputs'):
new_dict[k]['outputs'] = {}
for idx, (out_key, out_value) in enumerate(value.items()):
new_dict[k]['outputs'][idx] = {"name": out_value}
return new_dict
def main():
# 读取原始JSON文件
with open(old_json_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 转换数据
transformed_data = transform_dict(data)
# 写入新的JSON文件
with open(new_json_path, 'w', encoding='utf-8') as f:
json.dump(transformed_data, f, ensure_ascii=False, indent=2)
if __name__ == '__main__':
main()
+1 -1
View File
@@ -1149,7 +1149,7 @@ app.registerExtension({
for (const list of text) {
const w = ComfyWidgets["STRING"](this, "text", ["STRING", { multiline: true }], app).widget;
w.inputEl.readOnly = true;
// w.inputEl.readOnly = true;
w.inputEl.style.opacity = 0.6;
w.value = list;
}
+147 -117
View File
@@ -12,22 +12,22 @@ const ipadapterNodes = ["easy ipadapterApply", "easy ipadapterApplyADV" ,"easy i
const pipeNodes = ['easy pipeIn','easy pipeOut', 'easy pipeEdit']
const xyNodes = ['easy XYPlot', 'easy XYPlotAdvanced']
const extraNodes = ['easy setNode']
const modelNormalNodes = [...["Reroute"],...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes]
const modelNormalNodes = [...['RescaleCFG','LoraLoaderModelOnly','LoraLoader','FreeU','FreeU_v2'],...ipadapterNodes,...extraNodes]
const suggestions = {
// prompt
"easy seed":{
"from":{
"INT": [...["Reroute"],...preSamplingNodes,...['easy fullkSampler']]
"INT": [...preSamplingNodes,...['easy fullkSampler']]
}
},
"easy positive":{
"from":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy negative":{
"from":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy wildcards":{
@@ -53,214 +53,225 @@ const suggestions = {
// sd相关
"easy fullLoader": {
"from":{
"PIPE_LINE": [...["Reroute"],...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes],
"PIPE_LINE": [...preSamplingNodes,...['easy fullkSampler'],...pipeNodes,...extraNodes],
"MODEL":modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy a1111Loader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy comfyLoader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy svdLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy zero123Loader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy sv3dLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced", "easy preSamplingDynamicCFG"], ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"STRING": [...["Reroute"],...propmts]
"STRING": [...propmts]
}
},
"easy preSampling": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
},
},
"easy preSamplingAdvanced": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingDynamicCFG": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingCustom": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingLayerDiffusion": {
"from": {
"PIPE_LINE": [...["Reroute", "easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [...["easy kSamplerLayerDiffusion"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
"easy preSamplingNoiseIn": {
"from": {
"PIPE_LINE": [...["Reroute"], ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
"PIPE_LINE": [ ...kSampler, ...pipeNodes, ...controlNetNodes, ...xyNodes, ...extraNodes]
}
},
// ksampler
"easy fullkSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes]
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix'], ...preSamplingNodes, ...extraNodes]
}
},
"easy kSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes],
"PIPE_LINE": [ ...pipeNodes.reverse(), ...['easy preDetailerFix', 'easy preMaskDetailerFix', 'easy hiresFix'], ...preSamplingNodes, ...extraNodes],
}
},
// cn
"easy controlnetLoader": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
"easy controlnetLoaderADV":{
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes]
}
},
// instant
"easy instantIDApply": {
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy instantIDApplyADV":{
"from": {
"PIPE_LINE": [...["Reroute"], ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...preSamplingNodes, ...controlNetNodes, ...instantIDNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes
},
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApply":{
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterApplyADV":{
"to":{
"STRING": [...["Reroute", "easy sliderControl"], ...propmts],
"COMBO": [...["Reroute", "easy promptLine"]]
"STRING": [...["easy sliderControl"], ...propmts],
"COMBO": [...["easy promptLine"]]
}
},
"easy ipadapterStyleComposition":{
"to":{
"COMBO": [...["Reroute", "easy promptLine"]]
"COMBO": [...["easy promptLine"]]
}
},
// fix
"easy preDetailerFix":{
"from": {
"PIPE_LINE": [...["Reroute", "easy detailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
},
"to":{
"PIPE_LINE": [...["Reroute", "easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]]
"PIPE_LINE": [...["easy ultralyticsDetectorPipe", "easy samLoaderPipe", "easy kSampler", "easy fullkSampler"]]
}
},
"easy preMaskDetailerFix":{
"from": {
"PIPE_LINE": [...["Reroute", "easy detailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy detailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy samLoaderPipe": {
"from":{
"PIPE_LINE": [...["Reroute", "easy preDetailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
"easy ultralyticsDetectorPipe": {
"from":{
"PIPE_LINE": [...["Reroute", "easy preDetailerFix"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [...["easy preDetailerFix"], ...pipeNodes, ...extraNodes]
}
},
// cascade相关
"easy cascadeLoader":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes],
"PIPE_LINE": [ ...["easy fullCascadeKSampler", 'easy preSamplingCascade'], ...controlNetNodes, ...pipeNodes, ...extraNodes],
"MODEL": modelNormalNodes.filter(cate => !ipadapterNodes.includes(cate))
}
},
"easy fullCascadeKSampler":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
"easy preSamplingCascade":{
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy cascadeKSampler",], ...pipeNodes, ...extraNodes]
}
},
"easy cascadeKSampler": {
"from": {
"PIPE_LINE": [...["Reroute"], ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
"PIPE_LINE": [ ...["easy preSampling", "easy preSamplingAdvanced"], ...pipeNodes, ...extraNodes]
}
},
}
class NullGraphError extends Error {
constructor(message="Attempted to access LGraph reference that was null or undefined.", cause) {
super(message, {cause})
this.name = "NullGraphError"
}
}
app.registerExtension({
name: "comfy.easyuse.suggestions",
async setup(app) {
async setup() {
const createDefaultNodeForSlot = LGraphCanvas.prototype.createDefaultNodeForSlot;
LGraphCanvas.prototype.createDefaultNodeForSlot = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
,optPass
);
var that = this;
const opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,position: [] // pass the event coords
,nodeType: null // choose a nodetype to add, AUTO to set at first good
,posAdd:[0,0] // adjust x,y
,posSizeFix:[0,0] // alpha, adjust the position x,y based on the new node size w,h
}
, optPass || {}
);
const { afterRerouteId } = opts
const that = this;
var isFrom = opts.nodeFrom && opts.slotFrom!==null;
var isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
const isFrom = opts.nodeFrom && opts.slotFrom!==null;
const isTo = !isFrom && opts.nodeTo && opts.slotTo!==null;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return createDefaultNodeForSlot.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to createDefaultNodeForSlot "+opts.nodeFrom+" "+opts.slotFrom+" "+opts.nodeTo+" "+opts.slotTo);
@@ -271,24 +282,24 @@ app.registerExtension({
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
var nodeType = nodeX.type
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
const nodeType = nodeX.type
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "undefined":
default:
// bad ?
@@ -324,8 +335,7 @@ app.registerExtension({
for(var typeX in slotTypesDefault[fromSlotType]){
if (opts.nodeType == slotTypesDefault[fromSlotType][typeX] || opts.nodeType == "AUTO"){
nodeNewType = slotTypesDefault[fromSlotType][typeX];
// console.log("opts.nodeType == slotTypesDefault[fromSlotType][typeX] :: "+opts.nodeType);
break; // --------
break;
}
}
}else{
@@ -379,9 +389,7 @@ app.registerExtension({
// add the node
that.graph.add(newNode);
newNode.pos = [ opts.position[0]+opts.posAdd[0]+(opts.posSizeFix[0]?opts.posSizeFix[0]*newNode.size[0]:0)
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
//that.graph.afterChange();
,opts.position[1]+opts.posAdd[1]+(opts.posSizeFix[1]?opts.posSizeFix[1]*newNode.size[1]:0)]; //that.last_click_position; //[e.canvasX+30, e.canvasX+5];*/
// connect the two!
if (isFrom){
@@ -390,11 +398,6 @@ app.registerExtension({
opts.nodeTo.connectByTypeOutput( iSlotConn, newNode, fromSlotType );
}
// if connecting in between
if (isFrom && isTo){
// TODO
}
return true;
}else{
@@ -405,43 +408,54 @@ app.registerExtension({
return false;
}
let showConnectionMenu = LGraphCanvas.prototype.showConnectionMenu
LGraphCanvas.prototype.showConnectionMenu = function(optPass) { // addNodeMenu for connection
var optPass = optPass || {};
var opts = Object.assign({ nodeFrom: null // input
,slotFrom: null // input
,nodeTo: null // output
,slotTo: null // output
,e: null
}
,optPass
);
var that = this;
const opts = Object.assign({
nodeFrom: null, // input
slotFrom: null, // input
nodeTo: null, // output
slotTo: null, // output
e: undefined,
allow_searchbox: this.allow_searchbox,
showSearchBox: this.showSearchBox,
}
,optPass || {}
);
const that = this;
const { graph } = this
const { afterRerouteId } = opts
const isFrom = opts.nodeFrom && opts.slotFrom;
const isTo = !isFrom && opts.nodeTo && opts.slotTo;
const node = isFrom ? opts.nodeFrom : opts.nodeTo
var isFrom = opts.nodeFrom && opts.slotFrom;
var isTo = !isFrom && opts.nodeTo && opts.slotTo;
// Not an Easy Use node, skip showConnectionMenu hijack
if(!node || !Object.keys(suggestions).includes(node.type)){
return showConnectionMenu.call(this, optPass)
}
if (!isFrom && !isTo){
console.warn("No data passed to showConnectionMenu");
return false;
}
var nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
var slotX = isFrom ? opts.slotFrom : opts.slotTo;
const nodeX = isFrom ? opts.nodeFrom : opts.nodeTo;
if (!nodeX) throw new TypeError("nodeX was null when creating default node for slot.")
let slotX = isFrom ? opts.slotFrom : opts.slotTo;
var iSlotConn = false;
let iSlotConn = false;
switch (typeof slotX){
case "string":
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX,false) : nodeX.findInputSlot(slotX,false);
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
case "object":
// ok slotX
iSlotConn = isFrom ? nodeX.findOutputSlot(slotX.name) : nodeX.findInputSlot(slotX.name);
break;
break;
case "number":
iSlotConn = slotX;
slotX = isFrom ? nodeX.outputs[slotX] : nodeX.inputs[slotX];
break;
break;
default:
// bad ?
//iSlotConn = 0;
@@ -449,9 +463,8 @@ app.registerExtension({
return false;
}
var options = ["Add Node",null];
if (that.allow_searchbox){
const options = ["Add Node", "Add Reroute", null]
if (opts.allow_searchbox){
options.push("Search");
options.push(null);
}
@@ -479,8 +492,12 @@ app.registerExtension({
// build menu
var menu = new LiteGraph.ContextMenu(options, {
event: opts.e,
title: (slotX && slotX.name!="" ? (slotX.name + (fromSlotType?" | ":"")) : "")+(slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked
extra: slotX,
title:
(slotX && slotX.name != ""
? slotX.name + (fromSlotType ? " | " : "")
: "") + (slotX && fromSlotType ? fromSlotType : ""),
callback: inner_clicked,
});
// callback
@@ -488,32 +505,45 @@ app.registerExtension({
//console.log("Process showConnectionMenu selection");
switch (v) {
case "Add Node":
LGraphCanvas.onMenuAdd(null, null, e, menu, function(node){
if (isFrom){
opts.nodeFrom.connectByType( iSlotConn, node, fromSlotType );
}else{
opts.nodeTo.connectByTypeOutput( iSlotConn, node, fromSlotType );
LGraphCanvas.onMenuAdd(null, null, e, menu, function (node) {
if (!node) return
if (isFrom) {
opts.nodeFrom?.connectByType(iSlotConn, node, fromSlotType, { afterRerouteId })
} else {
opts.nodeTo?.connectByTypeOutput(iSlotConn, node, fromSlotType, { afterRerouteId })
}
});
})
break;
case "Add Reroute":
const node = isFrom ? opts.nodeFrom : opts.nodeTo
const slot = options.extra
if (!graph) throw new NullGraphError()
if (!node) throw new TypeError("Cannot add reroute: node was null")
if (!slot) throw new TypeError("Cannot add reroute: slot was null")
if (!opts.e) throw new TypeError("Cannot add reroute: CanvasPointerEvent was null")
const reroute = node.connectFloatingReroute([opts.e.canvasX, opts.e.canvasY], slot, afterRerouteId)
if (!reroute) throw new Error("Failed to create reroute")
that.dirty_canvas = true
that.dirty_bgcanvas = true
break
case "Search":
if(isFrom){
that.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
opts.showSearchBox(e,{node_from: opts.nodeFrom, slot_from: slotX, type_filter_in: fromSlotType});
}else{
that.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
opts.showSearchBox(e,{node_to: opts.nodeTo, slot_from: slotX, type_filter_out: fromSlotType});
}
break;
default:
// check for defaults nodes for this slottype
var nodeCreated = that.createDefaultNodeForSlot(Object.assign(opts,{ position: [opts.e.canvasX, opts.e.canvasY]
,nodeType: v
}));
if (nodeCreated){
// new node created
//console.log("node "+v+" created")
}else{
// failed or v is not in defaults
const customProps = {
position: [opts.e?.canvasX ?? 0, opts.e?.canvasY ?? 0],
nodeType: v,
afterRerouteId,
}
// check for defaults nodes for this slottype
that.createDefaultNodeForSlot(Object.assign(opts, customProps))
break;
}
}
+18 -7
View File
@@ -1,4 +1,5 @@
import { api } from "../../../scripts/api.js";
import { app } from "../../../scripts/app.js";
// 全局Seed
function globalSeedHandler(event) {
@@ -27,21 +28,31 @@ function globalSeedHandler(event) {
api.addEventListener("easyuse-global-seed", globalSeedHandler);
const original_queuePrompt = api.queuePrompt;
async function queuePrompt_with_seed(number, { output, workflow }) {
function addSeedWidgetsToWorkflow(prompt, graph) {
const workflow = prompt?.workflow;
if (!workflow || typeof workflow !== 'object') return prompt;
workflow.seed_widgets = {};
for(let i in app.graph._nodes_by_id) {
let widgets = app.graph._nodes_by_id[i].widgets;
const nodes = graph?._nodes_by_id || {};
for(let i in nodes) {
let widgets = nodes[i].widgets;
if(widgets) {
for(let j in widgets) {
if((widgets[j].name == 'seed_num' || widgets[j].name == 'seed' || widgets[j].name == 'noise_seed') && widgets[j].type != 'converted-widget')
workflow.seed_widgets[i] = parseInt(j);
}
}
}
}
return await original_queuePrompt.call(api, number, { output, workflow });
return prompt;
}
api.queuePrompt = queuePrompt_with_seed;
// Keep ComfyUI's queuePrompt untouched so its validation errors and evolving
// execution options remain owned by the core frontend.
const original_graphToPrompt = app.graphToPrompt;
app.graphToPrompt = function graphToPrompt_with_seed(...args) {
const graph = args[0] || app.rootGraph || app.graph;
return Promise.resolve(original_graphToPrompt.apply(this, args))
.then(prompt => addSeedWidgetsToWorkflow(prompt, graph));
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long