Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1840bac168 | ||
|
|
52fdc76c19 | ||
|
|
8f21bc9227 | ||
|
|
167c22388f | ||
|
|
038cb1bcf3 | ||
|
|
d70697842c | ||
|
|
fd673a0d5b | ||
|
|
fcb63aefa6 | ||
|
|
94b066a5c2 | ||
|
|
fdc1bc4f2f | ||
|
|
579162e440 | ||
|
|
e78bf45995 | ||
|
|
94413c6d9f | ||
|
|
ffdac507ae | ||
|
|
67faf38fbe | ||
|
|
69a534eaae | ||
|
|
c3d90e874b | ||
|
|
204d990afa | ||
|
|
da502219ad | ||
|
|
df72d2c478 | ||
|
|
798c769e13 | ||
|
|
3bd170b9ca | ||
|
|
75ef59b8e0 | ||
|
|
13696a11b7 | ||
|
|
08a0a2afc5 | ||
|
|
58fe45eb87 | ||
|
|
dab719f369 | ||
|
|
5e23d3f8cc | ||
|
|
336ed5a15f | ||
|
|
d3d21f8795 | ||
|
|
bb2358e43a | ||
|
|
b222d39f5f | ||
|
|
1bafa1a6b4 | ||
|
|
7b6ff9a879 | ||
|
|
ac8de2995e | ||
|
|
a0c5c9e2fb | ||
|
|
71a6bba451 | ||
|
|
c85cb5a309 | ||
|
|
34da83b4ab | ||
|
|
4ea73cf4ec | ||
|
|
9f5a726c8a | ||
|
|
d4856a595e | ||
|
|
498a8c58f7 | ||
|
|
feb0a5c791 | ||
|
|
04819cb5c2 | ||
|
|
806b78b902 | ||
|
|
807261cb00 | ||
|
|
5523190db9 | ||
|
|
5e3764728c | ||
|
|
b81f0e653d | ||
|
|
2e60c904c8 | ||
|
|
c7427d324f | ||
|
|
96641c3e4a |
@@ -1,21 +0,0 @@
|
||||
name: Publish to Comfy registry
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
@@ -1,8 +1,8 @@
|
||||
all: format check
|
||||
@echo "Done"
|
||||
check:
|
||||
pyflakes *.py */*.py
|
||||
pyflakes *.py */*.py */*/*.py
|
||||
format:
|
||||
black -l 120 *.py */*.py
|
||||
black -l 120 *.py */*.py */*/*.py
|
||||
|
||||
.PHONY: check format all
|
||||
|
||||
@@ -1,383 +1,9 @@
|
||||
# ComfyUI prompt control
|
||||
# ComfyUI prompt control (LEGACY VERSION)
|
||||
|
||||
Nodes for LoRA and prompt scheduling that make basic operations in ComfyUI completely prompt-controllable.
|
||||
Go to https://github.com/asagi4/comfyui-prompt-control for the revised version of prompt control.
|
||||
|
||||
LoRA and prompt scheduling should produce identical output to the equivalent ComfyUI workflow using multiple samplers or the various conditioning manipulation nodes. If you find situations where this is not the case, please report a bug.
|
||||
|
||||
## What can it do?
|
||||
|
||||
Things you can control via the prompt:
|
||||
- Prompt editing and filtering without multiple samplers
|
||||
- LoRA loading and scheduling (including LoRA block weights)
|
||||
- Prompt masking and area control, combining prompts and interpolation
|
||||
- SDXL parameters
|
||||
- Other miscellaneous things
|
||||
|
||||
[This example workflow](workflows/example.json?raw=1) implements a two-pass workflow illustrating most scheduling features.
|
||||
|
||||
The tools in this repository combine well with the macro and wildcard functionality in [comfyui-utility-nodes](https://github.com/asagi4/comfyui-utility-nodes)
|
||||
|
||||
## Requirements
|
||||
|
||||
For `PCEncodeSchedule` and `PCLoraHooksFromSchedule`, you'll need at least version 0.3.7 of ComfyUI (0.3.36 of ComfyUI desktop).
|
||||
|
||||
You need to have `lark` installed in your Python environment for parsing to work (If you reuse A1111's venv, it'll already be there).
|
||||
|
||||
If you use the portable version of ComfyUI on Windows with its embedded Python, you must open a terminal in the ComfyUI installation directory and run the command:
|
||||
```
|
||||
.\python_embeded\python.exe -m pip install lark
|
||||
```
|
||||
|
||||
Then restart ComfyUI afterwards.
|
||||
|
||||
## Notable changes
|
||||
|
||||
I try to avoid behavioural changes that break old prompts, but they may happen occasionally.
|
||||
|
||||
- 2024-12-03 ComfyUI merged support for model/conditioning hooks. There are two new nodes, `PCEncodeSchedule` and `PCLoraHooksFromSchedule` that can be used in combination with the hook nodes. Some functionality is still missing from them, but going forward, these nodes will be the only nodes supported; **I will not spend significant time fixing bugs in the old monkeypatched nodes anymore.**
|
||||
- 2024-02-02 The node will now automatically enable offloading LoRA backup weights to the CPU if you run out of memory during LoRA operations, even when `--highvram` is specified. This change persists until ComfyUI is restarted.
|
||||
- 2024-01-14 Multiple `CLIP_L` instances are now joined with a space separator instead of concatenated.
|
||||
- 2024-01-09 AITemplate support dropped. I don't recommend or test AITemplate anymore. Use Stable-Fast instead (see below for info)
|
||||
- 2024-01-08 Prompt control now enables in-place weight updates on the model. This shouldn't affect anything, but increases performance slightly. You can disable this by setting the environment variable `PC_NO_INPLACE_UPDATE` to any non-empty value.
|
||||
- 2023-12-28 MASK now uses ComfyUI's `mask_strength` attribute instead of calculating it on its own. This changes its behaviour slightly.
|
||||
- 2023-12-06: Removed `JinjaRender`, `SimpleWildcard`, `ConditioningCutoff`, `CondLinearInterpolate` and `StringConcat`. For the first two, see [this repository](https://github.com/asagi4/comfyui-utility-nodes) for mostly-compatible implementations.
|
||||
- 2023-10-04: `STYLE:...` syntax changed to `STYLE(...)`
|
||||
|
||||
## Note on how schedules work
|
||||
|
||||
ComfyUI does not use the step number to determine whether to apply conds; instead, it uses the sampler's timestep value which is affected by the scheduler you're using. This means that when the sampler scheduler isn't linear, the schedules generated by prompt control will not be either.
|
||||
|
||||
# Scheduling syntax
|
||||
|
||||
Syntax is like A1111 for now, but only fractions are supported for steps. LoRAs are scheduled by including them in a scheduling expression.
|
||||
|
||||
```
|
||||
a [large::0.1] [cat|dog:0.05] [<lora:somelora:0.5:0.6>::0.5]
|
||||
[in a park:in space:0.4]
|
||||
```
|
||||
|
||||
## Scheduled prompts
|
||||
|
||||
There are two forms of scheduled prompts.
|
||||
|
||||
### Basic scheduling expressions
|
||||
Basic expressions take the form `[before:after:X]` where `X` is the switch point, a decimal number between 0.0 and 1.0 inclusive, representing 0 to 100% of timesteps.
|
||||
For example:
|
||||
```
|
||||
a [red:blue:0.5] cat
|
||||
```
|
||||
switches from `a red cat` to `a blue cat` at 0.5. `before` and `after` can be arbitrary prompts (`after` can also be empty), including other scheduling expressions, allowing nesting:
|
||||
```
|
||||
a [red:[blue::0.7]:0.5] cat
|
||||
```
|
||||
|
||||
switches from `a red cat` to `a blue cat` at 0.5 and to `a cat` at 0.7
|
||||
|
||||
|
||||
**Note:** As a special case, `[cat:0.5]` is like `[:cat:0.5]` meaning it switches from empty to `cat` at 0.5. Currently, `[:cat:0.5]` doesn't actually parse correctly, so you **must** use the shortcut form
|
||||
|
||||
### Range expressions
|
||||
|
||||
You can also use `a [during:after:0.3,0.7]` as a shortcut. The prompt be `a` until 0.3, `a during` until 0.7, and then `a after`. This form is equivalent to `[[during:after:0.7]:0.3]`
|
||||
For convenience, `[during:0.1,0.4]` is equivalent to `[during::0.1,0.4]`
|
||||
|
||||
## Tag selection
|
||||
Using the `FilterSchedule` node, in addition to step percentages, you can use a *tag* to select part of an input:
|
||||
```
|
||||
a large [dog:cat<lora:catlora:0.5>:SECOND_PASS]
|
||||
```
|
||||
Set the `tags` parameter in the `FilterSchedule` node to filter the prompt. If the tag matches any tag `tags` (comma-separated), the second option is returned (`cat`, in this case, with the LoRA). Otherwise, the first option is chosen (`dog`, without LoRA).
|
||||
|
||||
the values in `tags` are case-insensitive, but the tags in the input **must** be uppercase A-Z and underscores only, or they won't be recognized. That is, `[dog:cat:hr]` will not work.
|
||||
|
||||
For example, a prompt
|
||||
```
|
||||
a [black:blue:X] [cat:dog:Y] [walking:running:Z] in space
|
||||
```
|
||||
with `tags` `x,z` would result in the prompt `a blue cat running in space`
|
||||
|
||||
## LoRA Scheduling
|
||||
LoRAs can be scheduled by referring to them in a scheduling expression, like so:
|
||||
|
||||
`<lora:fulllora:1> [<lora:partialora:1>::0.5]`
|
||||
|
||||
This will schedule `fulllora` for the entire duration of the prompt and `partiallora` until half of sampling is complete.
|
||||
|
||||
`PCLoraHooksFromSchedule` creates a properly scheduled `HOOKS` object from LoRA expressions included in the prompt. The older (deprecated) `ScheduleToModel` nodes will monkeypatch ComfyUI sampling and attempt to perform LoRA loading directly.
|
||||
|
||||
You can refer to LoRAs by using the filename without extension and subdirectories will also be searched. For example, `<lora:cats:1>`. will match both `cats.safetensors` and `sd15/animals/cats.safetensors`. If there are multiple LoRAs with the same name, the first match will be loaded.
|
||||
|
||||
Alternatively, the name can include the full directory path relative to ComfyUI's search paths, without extension: `<lora:XL/sdxllora:0.5>`. In this case, the *full* path must match.
|
||||
|
||||
If no match is found, the node will try to replace spaces with underscores and search again. That is, `<lora:cats and dogs:1>` will find `cats_and_dogs.safetensors`. This helps with some autocompletion scripts that replace underscores with spaces.
|
||||
|
||||
Finally, you can give the exact path (including the extension) as shown in `LoRALoader`.
|
||||
|
||||
|
||||
## Alternating
|
||||
|
||||
Alternating syntax is `[a|b:pct_steps]`, causing the prompt to alternate every `pct_steps`. `pct_steps` defaults to 0.1 if not specified. You can also have more than two options.
|
||||
|
||||
|
||||
## Sequences
|
||||
|
||||
The syntax `[SEQ:a:N1:b:N2:c:N3]` is shorthand for `[a:[b:[c::N3]:N2]:N1]` ie. it switches from `a` to `b` to `c` to nothing at the specified points in sequence.
|
||||
|
||||
Might be useful with Jinja templating (see https://github.com/asagi4/comfyui-utility-nodes). For example:
|
||||
```
|
||||
[SEQ<% for x in steps(0.1, 0.9, 0.1) %>:<lora:test:<= sin(x*pi) + 0.1 =>>:<= x =><% endfor %>]
|
||||
```
|
||||
generates a LoRA schedule based on a sinewave
|
||||
|
||||
|
||||
## Prompt interpolation
|
||||
|
||||
Note: Not currently supported by `PCEncodeSchedule`
|
||||
|
||||
`a red [INT:dog:cat:0.2,0.8:0.05]` will attempt to interpolate the tensors for `a red dog` and `a red cat` between the specified range in as many steps of 0.05 as will fit.
|
||||
|
||||
|
||||
## SDXL
|
||||
|
||||
The nodes do not treat SDXL models specially, but there are some utilities that enable SDXL specific functionality.
|
||||
|
||||
You can use the function `SDXL(width height, target_width target_height, crop_w crop_h)` to set SDXL prompt parameters. `SDXL()` is equivalent to `SDXL(1024 1024, 1024 1024, 0 0)` unless the default values have been overridden by `PCScheduleSettings`.
|
||||
|
||||
To set the `clip_l` prompt, as with `CLIPTextEncodeSDXL`, use the function `CLIP_L(prompt text goes here)`.
|
||||
|
||||
Things to note:
|
||||
- Multiple instances of `CLIP_L` are joined with a space. That is, `CLIP_L(foo)CLIP_L(bar)` is the same as `CLIP_L(foo bar)`
|
||||
- Using `BREAK` isn't supported in it; it'll just parse as the plain word BREAK.
|
||||
- similarly, `AND` inside `CLIP_L` does not do anything sensible; `CLIP_L(foo AND bar)` will parse as two prompts `CLIP_L(foo` and `bar)`
|
||||
- `CLIP_L` and `SDXL` have no effect on SD 1.5.
|
||||
- The rest of the prompt becomes the `clip_g` prompt.
|
||||
- If there is no `CLIP_L` or `SDXL`, the prompts will work as with `CLIPTextEncode`.
|
||||
|
||||
# Other syntax:
|
||||
|
||||
- `<emb:xyz>` is alternative syntax for `embedding:xyz` to work around a syntax conflict with `[embedding:xyz:0.5]` which is parsed as a schedule that switches from `embedding` to `xyz`.
|
||||
|
||||
- The keyword `BREAK` causes the prompt to be tokenized in separate chunks, which results in each chunk being individually padded to the text encoder's maximum token length. This is mostly equivalent to the `ConditioningConcat` node.
|
||||
|
||||
|
||||
## Combining prompts
|
||||
`AND` can be used to combine prompts. You can also use a weight at the end. It does a weighted sum of each prompt,
|
||||
```
|
||||
cat :1 AND dog :2
|
||||
```
|
||||
The weight defaults to 1 and are normalized so that `a:2 AND b:2` is equal to `a AND b`. `AND` is processed after schedule parsing, so you can change the weight mid-prompt: `cat:[1:2:0.5] AND dog`
|
||||
|
||||
if there is `COMFYAND()` in the prompt, the behaviour of `AND` will change to work like `ConditioningCombine`, but in practice this seems to be just slower while producing the same output.
|
||||
|
||||
Note: `PCEncodeSchedule` only has ComfYUI behaviour and does not have ´COMFYAND()´
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
There are some "functions" that can be included in a prompt to do various things.
|
||||
|
||||
Functions have the form `FUNCNAME(param1, param2, ...)`. How parameters are interpreted is up to the function.
|
||||
Note: Whitespace is *not* stripped from string parameters by default. Commas can be escaped with `\,`
|
||||
|
||||
Like `AND`, these functions are parsed after regular scheduling syntax has been expanded, allowing things like `[AREA:MASK:0.3](...)`, in case that's somehow useful.
|
||||
|
||||
### SHUFFLE and SHIFT
|
||||
|
||||
Default parameters: `SHUFFLE(seed=0, separator=,, joiner=,)`, `SHIFT(steps=0, separator=,, joiner=,)`
|
||||
|
||||
`SHIFT` moves elements to the left by `steps`. The default is 0 so `SHIFT()` does nothing
|
||||
`SHUFFLE` generates a random permutation with `seed` as its seed.
|
||||
|
||||
These functions are applied to each prompt chunk **after** `BREAK`, `AND` etc. have been parsed. The prompt is split by `separator`, the operation is applied, and it's then joined back by `joiner`.
|
||||
|
||||
Multiple instances of these functions are applied in the order they appear in the prompt.
|
||||
|
||||
**NOTE:** These functions are *not* smart about syntax and will break emphasis if the separator occurs inside parentheses. I might fix this at some point, but for now, keep this in mind.
|
||||
|
||||
For example:
|
||||
- `SHIFT(1) cat, dog, tiger, mouse` does a shift and results in `dog, tiger, mouse, cat`. (whitespace may vary)
|
||||
- `SHIFT(1,;) cat, dog ; tiger, mouse` results in `tiger, mouse, cat, dog`
|
||||
- `SHUFFLE() cat, dog, tiger, mouse` results in `cat, dog, mouse, tiger`
|
||||
- `SHUFFLE() SHIFT(1) cat, dog, tiger, mouse` results in `dog, mouse, tiger, cat`
|
||||
|
||||
- `SHIFT(1) cat,dog BREAK tiger,mouse` results in `dog,cat BREAK tiger,mouse`
|
||||
- `SHIFT(1) cat, dog AND SHIFT(1) tiger, mouse` results in `dog, cat BREAK mouse, tiger`
|
||||
|
||||
Whitespace is *not* stripped and may also be used as a joiner or separator
|
||||
- `SHIFT(1,, ) cat,dog` results in `dog cat`
|
||||
|
||||
### NOISE
|
||||
|
||||
The function `NOISE(weight, seed)` adds some random noise into the prompt. The seed is optional, and if not specified, the global RNG is used. `weight` should be between 0 and 1.
|
||||
|
||||
### MASK, IMASK and AREA
|
||||
|
||||
You can use `MASK(x1 x2, y1 y2, weight, op)` to specify a region mask for a prompt. The values are specified as a percentage with a float between `0` and `1`, or as absolute pixel values (these can't be mixed). `1` will be interpreted as a percentage instead of a pixel value.
|
||||
|
||||
Similarly, you can use `AREA(x1 x2, y1 y2, weight)` to specify an area for the prompt (see ComfyUI's area composition examples). The area is calculated by ComfyUI relative to your latent size.
|
||||
|
||||
#### Custom masks: IMASK and `PCScheduleAddMasks`
|
||||
|
||||
You can attach custom masks to a `PROMPT_SCHEDULE` with the `PCScheduleAddMasks` node and then refer to those masks in the prompt using `IMASK(index, weight, op)`. Indexing starts from zero, so 0 is the first attached mask etc. `PCSCheduleAddMasks` ignores empty inputs, so if you only add a mask to the `mask4` input, it will still have index 0.
|
||||
|
||||
Applying `PCScheduleAddMasks` multiple times *appends* masks to a schedule rather than overriding existing ones, so if you need more than 4, you can just use it more than once.
|
||||
|
||||
#### Behaviour of masks
|
||||
If multiple `MASK`s are specified, they are combined together with ComfyUI's `MaskComposite` node, with `op` specifying the operation to use (default `multiply`). In this case, the combined mask weight can be set with `MASKW(weight)` (defaults to 1.0).
|
||||
|
||||
Masks assume a size of `(512, 512)`, unless overridden with `PCScheduleSettings` and pixel values will be relative to that. ComfyUI will scale the mask to match the image resolution. You can change it manually by using `MASK_SIZE(width, height)` anywhere in the prompt,
|
||||
|
||||
These are handled per `AND`-ed prompt, so in `prompt1 AND MASK(...) prompt2`, the mask will only affect prompt2.
|
||||
|
||||
The default values are `MASK(0 1, 0 1, 1)` and you can omit unnecessary ones, that is, `MASK(0 0.5, 0.3)` is `MASK(0 0.5, 0.3 1, 1)`
|
||||
|
||||
Note that because the default values are percentages, `MASK(0 256, 64 512)` is valid, but `MASK(0 200)` will raise an error.
|
||||
|
||||
Masking does not affect LoRA scheduling unless you set unet weights to 0 for a LoRA.
|
||||
|
||||
### FEATHER
|
||||
|
||||
When you use `MASK` or `IMASK`, you can also call `FEATHER(left top right bottom)` to apply feathering using ComfyUI's `FeatherMask` node. The values are in pixels and default to `0`.
|
||||
|
||||
If multiple masks are used, `FEATHER` is applied *before compositing* in the order they appear in the prompt, and any leftovers are applied to the combined mask. If you want to skip feathering a mask while compositing, just use `FEATHER()` with no arguments.
|
||||
|
||||
For example:
|
||||
```
|
||||
MASK(1) MASK(2) MASK(3) FEATHER(1) FEATHER() FEATHER(3) weirdmask FEATHER(4)
|
||||
```
|
||||
|
||||
gives you a mask that is a combination of 1, 2 and 3, where 1 and 3 are feathered before compositing and then `FEATHER(4)` is applied to the composite.
|
||||
|
||||
The order of the `FEATHER` and `MASK` calls doesn't matter; you can have `FEATHER` before `MASK` or even interleave them.
|
||||
|
||||
# Schedulable LoRAs
|
||||
|
||||
Note: Use `PCLoraHooksFromSchedule`. It will work better.
|
||||
|
||||
## Old nodes
|
||||
The `ScheduleToModel` node patches a model so that when sampling, it'll switch LoRAs between steps. You can apply the LoRA's effect separately to CLIP conditioning and the unet (model).
|
||||
|
||||
Swapping LoRAs often can be quite slow without the `--highvram` switch because ComfyUI will shuffle things between the CPU and GPU. When things stay on the GPU, it's quite fast.
|
||||
|
||||
If you run out of VRAM during a LoRA swap, the node will attempt to save VRAM by enabling CPU offloading for future generations even in highvram mode. This persists until ComfyUI is restarted.
|
||||
|
||||
You can also set the `PC_RETRY_ON_OOM` environment variable to any non-empty value to automatically retry sampling once if VRAM runs out.
|
||||
|
||||
## LoRA Block Weight
|
||||
|
||||
Note: Not supported by `PCEncodeSchedule` yet
|
||||
|
||||
If you have [ComfyUI Inspire Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) installed, you can use its Lora Block Weight syntax, for example:
|
||||
|
||||
```
|
||||
a prompt <lora:cars:1:LBW=SD-OUTALL;A=1.0;B=0.0;>
|
||||
```
|
||||
The `;` is optional if there is only 1 parameter.
|
||||
The syntax is the same as in the `ImpactWildcard` node, documented [here](https://github.com/ltdrdata/ComfyUI-extension-tutorials/blob/Main/ComfyUI-Impact-Pack/tutorial/ImpactWildcard.md)
|
||||
|
||||
# Other integrations
|
||||
## Advanced CLIP encoding
|
||||
Note: `perp` is not supported by `PCEncodeSchedule`
|
||||
|
||||
You can use the syntax `STYLE(weight_interpretation, normalization)` in a prompt to affect how prompts are interpreted.
|
||||
|
||||
Without any extra nodes, only `perp` is available, which does the same as [ComfyUI_PerpWeight](https://github.com/bvhari/ComfyUI_PerpWeight) extension.
|
||||
|
||||
If you have [Advanced CLIP Encoding nodes](https://github.com/BlenderNeko/ComfyUI_ADV_CLIP_emb/tree/master) cloned into your `custom_nodes`, more options will be available.
|
||||
|
||||
The style can be specified separately for each AND:ed prompt, but the first prompt is special; later prompts will "inherit" it as default. For example:
|
||||
|
||||
```
|
||||
STYLE(A1111) a (red:1.1) cat with (brown:0.9) spots and a long tail AND an (old:0.5) dog AND a (green:1.4) (balloon:1.1)
|
||||
```
|
||||
will interpret everything as A1111, but
|
||||
```
|
||||
a (red:1.1) cat with (brown:0.9) spots and a long tail AND STYLE(A1111) an (old:0.5) dog AND a (green:1.4) (balloon:1.1)
|
||||
```
|
||||
Will interpret the first one using the default ComfyUI behaviour, the second prompt with A1111 and the last prompt with the default again
|
||||
|
||||
For things (ie. the code imports) to work, the nodes must be cloned in a directory named exactly `ComfyUI_ADV_CLIP_emb`.
|
||||
|
||||
## Cutoff node integration
|
||||
|
||||
Note: Not supported by `PCEncodeSchedule` yet.
|
||||
|
||||
If you have [ComfyUI Cutoff](https://github.com/BlenderNeko/ComfyUI_Cutoff) cloned into your `custom_nodes`, you can use the `CUT` keyword to use cutoff functionality
|
||||
|
||||
The syntax is
|
||||
```
|
||||
a group of animals, [CUT:white cat:white], [CUT:brown dog:brown:0.5:1.0:1.0:_]
|
||||
```
|
||||
the parameters in the `CUT` section are `region_text:target_text:weight;strict_mask:start_from_masked:padding_token` of which only the first two are required.
|
||||
If `strict_mask`, `start_from_masked` or `padding_token` are specified in more than one section, the last one takes effect for the whole prompt
|
||||
|
||||
# Nodes
|
||||
|
||||
## PCLoraHooksFromSchedule
|
||||
|
||||
Creates a ComfyUI `HOOKS` object from a prompt schedule. Can be attached to a CLIP model to perform encoding and LoRA switching
|
||||
|
||||
## PCEncodeSchedule
|
||||
|
||||
Encodes all prompts in a schedule. Pass in a `CLIP` object with hooks attached for LoRA scheduling, then use the resulting `CONDITIONING` normally
|
||||
|
||||
## PromptToSchedule
|
||||
Parses a schedule from a text prompt. A schedule is essentially an array of `(valid_until, prompt)` pairs that the other nodes can use.
|
||||
|
||||
## FilterSchedule
|
||||
Filters a schedule according to its parameters, removing any *changes* that do not occur within `[start, end)`.
|
||||
|
||||
The node also does tag filtering if any tags are specified.
|
||||
|
||||
Always returns at least the last prompt in the schedule if everything would otherwise be filtered.
|
||||
|
||||
`start=0, end=0` returns the prompt at the start and `start=1.0, end=1.0` returns the prompt at the end.
|
||||
|
||||
## PCScheduleSettings
|
||||
Returns an object representing **default values** for the `SDXL` function and allows configuring `MASK_SIZE` outside the prompt. You need to apply them to a schedule with `PCApplySettings`. Note that for the SDXL settings to apply, you still need to have `SDXL()` in the prompt.
|
||||
|
||||
The "steps" parameter currently does nothing; it's for future features.
|
||||
|
||||
## PCApplySettings
|
||||
Applies the give default values from `PCScheduleSettings` to a schedule
|
||||
|
||||
## PCPromptFromSchedule
|
||||
|
||||
Extracts a text prompt from a schedule; also logs it to the console.
|
||||
LoRAs are *not* included in the text prompt, though they are logged.
|
||||
|
||||
## PCScheduleAddMasks
|
||||
|
||||
Attaches custom masks to a `PROMPT_SCHEDULE` that can then be used in a prompt.
|
||||
|
||||
## ScheduleToCond (deprecated)
|
||||
Produces a combined conditioning for the appropriate timesteps. From a schedule. Also applies LoRAs to the CLIP model according to the schedule.
|
||||
|
||||
## ScheduleToModel (deprecated)
|
||||
Produces a model that'll cause the sampler to reapply LoRAs at specific steps according to the schedule.
|
||||
|
||||
This depends on a callback handled by a monkeypatch of the ComfyUI sampler function, so it might not work with custom samplers, but it shouldn't interfere with them either.
|
||||
|
||||
## PCSplitSampling (deprecated)
|
||||
Causes sampling to be split into multiple sampler calls instead of relying on timesteps for scheduling. This makes the schedules more accurate, but seems to cause weird behaviour with SDE samplers. (Upstream bug?)
|
||||
|
||||
|
||||
## PromptControlSimple (deprecated)
|
||||
This node exists purely for convenience. It's a combination of `PromptToSchedule`, `ScheduleToCond`, `ScheduleToModel` and `FilterSchedule` such that it provides as output a model, positive conds and negative conds, both with and without any specified filters applied.
|
||||
|
||||
This makes it handy for quick one- or two-pass workflows.
|
||||
|
||||
## Older nodes
|
||||
|
||||
- `EditableCLIPEncode`: A combination of `PromptToSchedule` and `ScheduleToCond`
|
||||
- `LoRAScheduler`: A combination of `PromptToSchedule`, `FilterSchedule` and `ScheduleToModel`
|
||||
These nodes exist only to reproduce old workflows. They are unmaintained
|
||||
|
||||
# Known issues
|
||||
|
||||
- If you use LoRA scheduling in a workflow with `LoRALoader` nodes, you might get inconsistent results. For now, just avoid mixing `ScheduleToModel` or `LoRAScheduler` with `LoRALoader`. See https://github.com/asagi4/comfyui-prompt-control/issues/36
|
||||
- Workflows using `SamplerCustom` will calculate LoRA schedules based on the number of sigmas given to the sampler instead of the number of steps, since that information isn't available.
|
||||
- `CUT` does not work with `STYLE:perp`
|
||||
- `PCSplitSampling` overrides ComfyUI's `BrownianTreeNoiseSampler` noise sampling behaviour so that each split segment doesn't add crazy amounts of noise to the result with some samplers.
|
||||
- Split sampling may have weird behaviour if your step percentages go below 1 step.
|
||||
- Interpolation is probably buggy and will likely change behaviour whenever code gets refactored.
|
||||
- If execution is interrupted and LoRA scheduling is used, your models might be left in an undefined state until you restart ComfyUI
|
||||
- ComfyUI's LoRA hooks are a bit slower than LoRALoader currently when the LoRA doesn't actually require scheduling. Hopefully this will improve upstream.
|
||||
|
||||
+25
-43
@@ -2,9 +2,9 @@ import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from .prompt_control.node_clip import EditableCLIPEncode, ScheduleToCond
|
||||
from .prompt_control.node_lora import LoRAScheduler, ScheduleToModel, PCSplitSampling, PCWrapGuider
|
||||
from .prompt_control.node_other import (
|
||||
from .prompt_control.legacy.node_clip import EditableCLIPEncode, ScheduleToCond
|
||||
from .prompt_control.legacy.node_lora import LoRAScheduler, ScheduleToModel, PCSplitSampling, PCWrapGuider
|
||||
from .prompt_control.legacy.node_other import (
|
||||
PromptToSchedule,
|
||||
FilterSchedule,
|
||||
PCScheduleSettings,
|
||||
@@ -12,53 +12,35 @@ from .prompt_control.node_other import (
|
||||
PCApplySettings,
|
||||
PCPromptFromSchedule,
|
||||
)
|
||||
from .prompt_control.node_aio import PromptControlSimple
|
||||
from .prompt_control.legacy.node_aio import PromptControlSimple
|
||||
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
log.propagate = False
|
||||
if not log.handlers:
|
||||
h = logging.StreamHandler(sys.stdout)
|
||||
h.setFormatter(logging.Formatter("[%(levelname)s] PromptControl: %(message)s"))
|
||||
h.setFormatter(logging.Formatter("[%(levelname)s] PromptControl (LEGACY VERSION): %(message)s"))
|
||||
log.addHandler(h)
|
||||
|
||||
if os.environ.get("COMFYUI_PC_DEBUG"):
|
||||
log.setLevel(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
import importlib
|
||||
|
||||
if importlib.util.find_spec("comfy.hooks"):
|
||||
from .prompt_control.node_hooks import PCLoraHooksFromSchedule, PCEncodeSchedule
|
||||
|
||||
maps = {
|
||||
"PCLoraHooksFromSchedule": PCLoraHooksFromSchedule,
|
||||
"PCEncodeSchedule": PCEncodeSchedule,
|
||||
}
|
||||
else:
|
||||
log.warning(
|
||||
"Your ComfyUI version is too old, can't import comfy.hooks for PCEncodeSchedule and PCLoraHooksFromSchedule. Update your installation."
|
||||
)
|
||||
maps = {}
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy"))
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"PromptControlSimple": PromptControlSimple,
|
||||
"PromptToSchedule": PromptToSchedule,
|
||||
"PCSplitSampling": PCSplitSampling,
|
||||
"PCScheduleSettings": PCScheduleSettings,
|
||||
"PCScheduleAddMasks": PCScheduleAddMasks,
|
||||
"PCApplySettings": PCApplySettings,
|
||||
"PCPromptFromSchedule": PCPromptFromSchedule,
|
||||
"PCWrapGuider": PCWrapGuider,
|
||||
"FilterSchedule": FilterSchedule,
|
||||
"ScheduleToCond": ScheduleToCond,
|
||||
"ScheduleToModel": ScheduleToModel,
|
||||
"EditableCLIPEncode": EditableCLIPEncode,
|
||||
"LoRAScheduler": LoRAScheduler,
|
||||
}
|
||||
|
||||
NODE_CLASS_MAPPINGS.update(maps)
|
||||
NODE_CLASS_MAPPINGS.update(
|
||||
{
|
||||
"PromptControlSimple": PromptControlSimple,
|
||||
"PromptToSchedule": PromptToSchedule,
|
||||
"PCSplitSampling": PCSplitSampling,
|
||||
"PCPromptFromSchedule": PCPromptFromSchedule,
|
||||
"PCScheduleSettings": PCScheduleSettings,
|
||||
"PCScheduleAddMasks": PCScheduleAddMasks,
|
||||
"PCApplySettings": PCApplySettings,
|
||||
"PCWrapGuider": PCWrapGuider,
|
||||
"FilterSchedule": FilterSchedule,
|
||||
"ScheduleToCond": ScheduleToCond,
|
||||
"ScheduleToModel": ScheduleToModel,
|
||||
"EditableCLIPEncode": EditableCLIPEncode,
|
||||
"LoRAScheduler": LoRAScheduler,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
# Legacy node documentation
|
||||
|
||||
You really shouldn't be using these anymore
|
||||
|
||||
## Old nodes
|
||||
The `ScheduleToModel` node patches a model so that when sampling, it'll switch LoRAs between steps. You can apply the LoRA's effect separately to CLIP conditioning and the unet (model).
|
||||
|
||||
Swapping LoRAs often can be quite slow without the `--highvram` switch because ComfyUI will shuffle things between the CPU and GPU. When things stay on the GPU, it's quite fast.
|
||||
|
||||
If you run out of VRAM during a LoRA swap, the node will attempt to save VRAM by enabling CPU offloading for future generations even in highvram mode. This persists until ComfyUI is restarted.
|
||||
|
||||
You can also set the `PC_RETRY_ON_OOM` environment variable to any non-empty value to automatically retry sampling once if VRAM runs out.
|
||||
|
||||
## ScheduleToCond (deprecated)
|
||||
Produces a combined conditioning for the appropriate timesteps. From a schedule. Also applies LoRAs to the CLIP model according to the schedule.
|
||||
|
||||
## ScheduleToModel (deprecated)
|
||||
Produces a model that'll cause the sampler to reapply LoRAs at specific steps according to the schedule.
|
||||
|
||||
This depends on a callback handled by a monkeypatch of the ComfyUI sampler function, so it might not work with custom samplers, but it shouldn't interfere with them either.
|
||||
|
||||
## PCSplitSampling (deprecated)
|
||||
Causes sampling to be split into multiple sampler calls instead of relying on timesteps for scheduling. This makes the schedules more accurate, but seems to cause weird behaviour with SDE samplers. (Upstream bug?)
|
||||
|
||||
|
||||
## PromptControlSimple (deprecated)
|
||||
This node exists purely for convenience. It's a combination of `PromptToSchedule`, `ScheduleToCond`, `ScheduleToModel` and `FilterSchedule` such that it provides as output a model, positive conds and negative conds, both with and without any specified filters applied.
|
||||
|
||||
This makes it handy for quick one- or two-pass workflows.
|
||||
|
||||
## Older nodes
|
||||
|
||||
- `EditableCLIPEncode`: A combination of `PromptToSchedule` and `ScheduleToCond`
|
||||
- `LoRAScheduler`: A combination of `PromptToSchedule`, `FilterSchedule` and `ScheduleToModel`
|
||||
|
||||
# Known issues
|
||||
|
||||
- If you use LoRA scheduling in a workflow with `LoRALoader` nodes, you might get inconsistent results. For now, just avoid mixing `ScheduleToModel` or `LoRAScheduler` with `LoRALoader`. See https://github.com/asagi4/comfyui-prompt-control/issues/36
|
||||
- Workflows using `SamplerCustom` will calculate LoRA schedules based on the number of sigmas given to the sampler instead of the number of steps, since that information isn't available.
|
||||
- `CUT` does not work with `STYLE:perp`
|
||||
- `PCSplitSampling` overrides ComfyUI's `BrownianTreeNoiseSampler` noise sampling behaviour so that each split segment doesn't add crazy amounts of noise to the result with some samplers.
|
||||
- Split sampling may have weird behaviour if your step percentages go below 1 step.
|
||||
- Interpolation is probably buggy and will likely change behaviour whenever code gets refactored.
|
||||
- If execution is interrupted and LoRA scheduling is used, your models might be left in an undefined state until you restart ComfyUI
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
# Scheduling syntax
|
||||
|
||||
Syntax is like A1111 for now, but only fractions are supported for steps. LoRAs are scheduled by including them in a scheduling expression.
|
||||
|
||||
```
|
||||
a [large::0.1] [cat|dog:0.05] [<lora:somelora:0.5:0.6>::0.5]
|
||||
[in a park:in space:0.4]
|
||||
```
|
||||
|
||||
## Scheduled prompts
|
||||
|
||||
There are two forms of scheduled prompts.
|
||||
|
||||
### Basic scheduling expressions
|
||||
Basic expressions take the form `[before:after:X]` where `X` is the switch point, a decimal number between 0.0 and 1.0 inclusive, representing 0 to 100% of timesteps.
|
||||
For example:
|
||||
```
|
||||
a [red:blue:0.5] cat
|
||||
```
|
||||
switches from `a red cat` to `a blue cat` at 0.5. `before` and `after` can be arbitrary prompts (`after` can also be empty), including other scheduling expressions, allowing nesting:
|
||||
```
|
||||
a [red:[blue::0.7]:0.5] cat
|
||||
```
|
||||
|
||||
switches from `a red cat` to `a blue cat` at 0.5 and to `a cat` at 0.7
|
||||
|
||||
|
||||
**Note:** As a special case, `[cat:0.5]` is like `[:cat:0.5]` meaning it switches from empty to `cat` at 0.5. Currently, `[:cat:0.5]` doesn't actually parse correctly, so you **must** use the shortcut form
|
||||
|
||||
### Range expressions
|
||||
|
||||
You can also use `a [during:after:0.3,0.7]` as a shortcut. The prompt be `a` until 0.3, `a during` until 0.7, and then `a after`. This form is equivalent to `[[during:after:0.7]:0.3]`
|
||||
For convenience, `[during:0.1,0.4]` is equivalent to `[during::0.1,0.4]`
|
||||
|
||||
## Tag selection
|
||||
Using the `FilterSchedule` node, in addition to step percentages, you can use a *tag* to select part of an input:
|
||||
```
|
||||
a large [dog:cat<lora:catlora:0.5>:SECOND_PASS]
|
||||
```
|
||||
Set the `tags` parameter in the `FilterSchedule` node to filter the prompt. If the tag matches any tag `tags` (comma-separated), the second option is returned (`cat`, in this case, with the LoRA). Otherwise, the first option is chosen (`dog`, without LoRA).
|
||||
|
||||
the values in `tags` are case-insensitive, but the tags in the input **must** be uppercase A-Z and underscores only, or they won't be recognized. That is, `[dog:cat:hr]` will not work.
|
||||
|
||||
For example, a prompt
|
||||
```
|
||||
a [black:blue:X] [cat:dog:Y] [walking:running:Z] in space
|
||||
```
|
||||
with `tags` `x,z` would result in the prompt `a blue cat running in space`
|
||||
|
||||
|
||||
## LoRA Scheduling
|
||||
LoRAs can be scheduled by referring to them in a scheduling expression, like so:
|
||||
|
||||
`<lora:fulllora:1> [<lora:partialora:1>::0.5]`
|
||||
|
||||
This will schedule `fulllora` for the entire duration of the prompt and `partiallora` until half of sampling is complete.
|
||||
|
||||
`PCLoraHooksFromSchedule` creates a properly scheduled `HOOKS` object from LoRA expressions included in the prompt. The older (deprecated) `ScheduleToModel` nodes will monkeypatch ComfyUI sampling and attempt to perform LoRA loading directly.
|
||||
|
||||
You can refer to LoRAs by using the filename without extension and subdirectories will also be searched. For example, `<lora:cats:1>`. will match both `cats.safetensors` and `sd15/animals/cats.safetensors`. If there are multiple LoRAs with the same name, the first match will be loaded.
|
||||
|
||||
Alternatively, the name can include the full directory path relative to ComfyUI's search paths, without extension: `<lora:XL/sdxllora:0.5>`. In this case, the *full* path must match.
|
||||
|
||||
If no match is found, the node will try to replace spaces with underscores and search again. That is, `<lora:cats and dogs:1>` will find `cats_and_dogs.safetensors`. This helps with some autocompletion scripts that replace underscores with spaces.
|
||||
|
||||
Finally, you can give the exact path (including the extension) as shown in `LoRALoader`.
|
||||
|
||||
|
||||
## Alternating
|
||||
|
||||
Alternating syntax is `[a|b:pct_steps]`, causing the prompt to alternate every `pct_steps`. `pct_steps` defaults to 0.1 if not specified. You can also have more than two options.
|
||||
|
||||
|
||||
## Sequences
|
||||
|
||||
The syntax `[SEQ:a:N1:b:N2:c:N3]` is shorthand for `[a:[b:[c::N3]:N2]:N1]` ie. it switches from `a` to `b` to `c` to nothing at the specified points in sequence.
|
||||
|
||||
Might be useful with Jinja templating (see https://github.com/asagi4/comfyui-utility-nodes). For example:
|
||||
```
|
||||
[SEQ<% for x in steps(0.1, 0.9, 0.1) %>:<lora:test:<= sin(x*pi) + 0.1 =>>:<= x =><% endfor %>]
|
||||
```
|
||||
generates a LoRA schedule based on a sinewave
|
||||
|
||||
## Prompt interpolation
|
||||
|
||||
Note: Not currently supported by `PCEncodeSchedule`
|
||||
|
||||
`a red [INT:dog:cat:0.2,0.8:0.05]` will attempt to interpolate the tensors for `a red dog` and `a red cat` between the specified range in as many steps of 0.05 as will fit.
|
||||
|
||||
|
||||
# Basic prompt syntax
|
||||
|
||||
This syntax is also available in outside scheduled prompts, where applicable.
|
||||
|
||||
## LoRA loading
|
||||
|
||||
The A111-style syntax `<lora:loraname:weight>` can be used to load LoRAs via the prompt. See LoRA scheduling above.
|
||||
|
||||
## Combining prompts, A1111-style
|
||||
|
||||
- The keyword `BREAK` causes the prompt to be tokenized in separate chunks, which results in each chunk being individually padded to the text encoder's maximum token length. This is mostly equivalent to the `ConditioningConcat` node.
|
||||
|
||||
`AND` can be used to combine prompts. You can also use a weight at the end. It does a weighted sum of each prompt,
|
||||
```
|
||||
cat :1 AND dog :2
|
||||
```
|
||||
The weight defaults to 1 and are normalized so that `a:2 AND b:2` is equal to `a AND b`. `AND` is processed after schedule parsing, so you can change the weight mid-prompt: `cat:[1:2:0.5] AND dog`
|
||||
|
||||
|
||||
## Functions
|
||||
|
||||
There are some "functions" that can be included in a prompt to do various things.
|
||||
|
||||
Functions have the form `FUNCNAME(param1, param2, ...)`. How parameters are interpreted is up to the function.
|
||||
Note: Whitespace is *not* stripped from string parameters by default. Commas can be escaped with `\,`
|
||||
|
||||
Like `AND`, these functions are parsed after regular scheduling syntax has been expanded, allowing things like `[AREA:MASK:0.3](...)`, in case that's somehow useful.
|
||||
|
||||
### SDXL
|
||||
|
||||
The nodes do not treat SDXL models specially, but there are some utilities that enable SDXL specific functionality.
|
||||
|
||||
You can use the function `SDXL(width height, target_width target_height, crop_w crop_h)` to set SDXL prompt parameters. `SDXL()` is equivalent to `SDXL(1024 1024, 1024 1024, 0 0)` unless the default values have been overridden by `PCScheduleSettings`.
|
||||
|
||||
To set the `clip_l` prompt, as with `CLIPTextEncodeSDXL`, use the function `CLIP_L(prompt text goes here)`.
|
||||
|
||||
Things to note:
|
||||
- Multiple instances of `CLIP_L` are joined with a space. That is, `CLIP_L(foo)CLIP_L(bar)` is the same as `CLIP_L(foo bar)`
|
||||
- Using `BREAK` isn't supported in it; it'll just parse as the plain word BREAK.
|
||||
- similarly, `AND` inside `CLIP_L` does not do anything sensible; `CLIP_L(foo AND bar)` will parse as two prompts `CLIP_L(foo` and `bar)`
|
||||
- `CLIP_L` and `SDXL` have no effect on SD 1.5.
|
||||
- The rest of the prompt becomes the `clip_g` prompt.
|
||||
- If there is no `CLIP_L` or `SDXL`, the prompts will work as with `CLIPTextEncode`.
|
||||
|
||||
### SHUFFLE and SHIFT
|
||||
|
||||
Default parameters: `SHUFFLE(seed=0, separator=,, joiner=,)`, `SHIFT(steps=0, separator=,, joiner=,)`
|
||||
|
||||
`SHIFT` moves elements to the left by `steps`. The default is 0 so `SHIFT()` does nothing
|
||||
`SHUFFLE` generates a random permutation with `seed` as its seed.
|
||||
|
||||
These functions are applied to each prompt chunk **after** `BREAK`, `AND` etc. have been parsed. The prompt is split by `separator`, the operation is applied, and it's then joined back by `joiner`.
|
||||
|
||||
Multiple instances of these functions are applied in the order they appear in the prompt.
|
||||
|
||||
**NOTE:** These functions are *not* smart about syntax and will break emphasis if the separator occurs inside parentheses. I might fix this at some point, but for now, keep this in mind.
|
||||
|
||||
For example:
|
||||
- `SHIFT(1) cat, dog, tiger, mouse` does a shift and results in `dog, tiger, mouse, cat`. (whitespace may vary)
|
||||
- `SHIFT(1,;) cat, dog ; tiger, mouse` results in `tiger, mouse, cat, dog`
|
||||
- `SHUFFLE() cat, dog, tiger, mouse` results in `cat, dog, mouse, tiger`
|
||||
- `SHUFFLE() SHIFT(1) cat, dog, tiger, mouse` results in `dog, mouse, tiger, cat`
|
||||
|
||||
- `SHIFT(1) cat,dog BREAK tiger,mouse` results in `dog,cat BREAK tiger,mouse`
|
||||
- `SHIFT(1) cat, dog AND SHIFT(1) tiger, mouse` results in `dog, cat BREAK mouse, tiger`
|
||||
|
||||
Whitespace is *not* stripped and may also be used as a joiner or separator
|
||||
- `SHIFT(1,, ) cat,dog` results in `dog cat`
|
||||
|
||||
### NOISE
|
||||
|
||||
The function `NOISE(weight, seed)` adds some random noise into the prompt. The seed is optional, and if not specified, the global RNG is used. `weight` should be between 0 and 1.
|
||||
|
||||
### MASK, IMASK and AREA
|
||||
|
||||
You can use `MASK(x1 x2, y1 y2, weight, op)` to specify a region mask for a prompt. The values are specified as a percentage with a float between `0` and `1`, or as absolute pixel values (these can't be mixed). `1` will be interpreted as a percentage instead of a pixel value.
|
||||
|
||||
Similarly, you can use `AREA(x1 x2, y1 y2, weight)` to specify an area for the prompt (see ComfyUI's area composition examples). The area is calculated by ComfyUI relative to your latent size.
|
||||
|
||||
#### Custom masks: IMASK and `PCScheduleAddMasks`
|
||||
|
||||
You can attach custom masks to a `PROMPT_SCHEDULE` with the `PCScheduleAddMasks` node and then refer to those masks in the prompt using `IMASK(index, weight, op)`. Indexing starts from zero, so 0 is the first attached mask etc. `PCSCheduleAddMasks` ignores empty inputs, so if you only add a mask to the `mask4` input, it will still have index 0.
|
||||
|
||||
Applying `PCScheduleAddMasks` multiple times *appends* masks to a schedule rather than overriding existing ones, so if you need more than 4, you can just use it more than once.
|
||||
|
||||
#### Behaviour of masks
|
||||
If multiple `MASK`s are specified, they are combined together with ComfyUI's `MaskComposite` node, with `op` specifying the operation to use (default `multiply`). In this case, the combined mask weight can be set with `MASKW(weight)` (defaults to 1.0).
|
||||
|
||||
Masks assume a size of `(512, 512)`, unless overridden with `PCScheduleSettings` and pixel values will be relative to that. ComfyUI will scale the mask to match the image resolution. You can change it manually by using `MASK_SIZE(width, height)` anywhere in the prompt,
|
||||
|
||||
These are handled per `AND`-ed prompt, so in `prompt1 AND MASK(...) prompt2`, the mask will only affect prompt2.
|
||||
|
||||
The default values are `MASK(0 1, 0 1, 1)` and you can omit unnecessary ones, that is, `MASK(0 0.5, 0.3)` is `MASK(0 0.5, 0.3 1, 1)`
|
||||
|
||||
Note that because the default values are percentages, `MASK(0 256, 64 512)` is valid, but `MASK(0 200)` will raise an error.
|
||||
|
||||
Masking does not affect LoRA scheduling unless you set unet weights to 0 for a LoRA.
|
||||
|
||||
### FEATHER
|
||||
|
||||
When you use `MASK` or `IMASK`, you can also call `FEATHER(left top right bottom)` to apply feathering using ComfyUI's `FeatherMask` node. The values are in pixels and default to `0`.
|
||||
|
||||
If multiple masks are used, `FEATHER` is applied *before compositing* in the order they appear in the prompt, and any leftovers are applied to the combined mask. If you want to skip feathering a mask while compositing, just use `FEATHER()` with no arguments.
|
||||
|
||||
For example:
|
||||
```
|
||||
MASK(1) MASK(2) MASK(3) FEATHER(1) FEATHER() FEATHER(3) weirdmask FEATHER(4)
|
||||
```
|
||||
|
||||
gives you a mask that is a combination of 1, 2 and 3, where 1 and 3 are feathered before compositing and then `FEATHER(4)` is applied to the composite.
|
||||
|
||||
The order of the `FEATHER` and `MASK` calls doesn't matter; you can have `FEATHER` before `MASK` or even interleave them.
|
||||
|
||||
## Miscellaneous
|
||||
- `<emb:xyz>` is alternative syntax for `embedding:xyz` to work around a syntax conflict with `[embedding:xyz:0.5]` which is parsed as a schedule that switches from `embedding` to `xyz`.
|
||||
@@ -6,7 +6,7 @@ import gc
|
||||
import comfy.model_management
|
||||
import os
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
|
||||
def has_hijack(obj):
|
||||
@@ -1,6 +1,6 @@
|
||||
from .node_clip import control_to_clip_common
|
||||
from .node_lora import schedule_lora_common
|
||||
from .parser import parse_prompt_schedules
|
||||
from ..parser import parse_prompt_schedules
|
||||
|
||||
|
||||
class PromptControlSimple:
|
||||
@@ -20,9 +20,10 @@ class PromptControlSimple:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("MODEL", "CONDITIONING", "CONDITIONING", "MODEL", "CONDITIONING", "CONDITIONING")
|
||||
RETURN_NAMES = ("model", "positive", "negative", "model_filtered", "pos_filtered", "neg_filtered")
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, clip, positive, negative, tags="", start=0.0, end=1.0):
|
||||
@@ -1,15 +1,15 @@
|
||||
import logging
|
||||
import re
|
||||
import torch
|
||||
from . import utils as utils
|
||||
from .parser import parse_prompt_schedules, parse_cuts
|
||||
from .utils import Timer, equalize, safe_float, get_function, parse_floats
|
||||
from ..parser import parse_prompt_schedules, parse_cuts
|
||||
from .utils import Timer, equalize, apply_loras_from_spec
|
||||
from ..utils import safe_float, get_function, parse_floats # non-legacy
|
||||
from .perp_weight import perp_encode
|
||||
from comfy_extras.nodes_mask import FeatherMask, MaskComposite
|
||||
from node_helpers import conditioning_set_values
|
||||
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
try:
|
||||
from custom_nodes.ComfyUI_ADV_CLIP_emb.adv_encode import (
|
||||
@@ -142,8 +142,9 @@ class ScheduleToCond:
|
||||
"required": {"clip": ("CLIP",), "prompt_schedule": ("PROMPT_SCHEDULE",)},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, prompt_schedule):
|
||||
@@ -163,8 +164,9 @@ class EditableCLIPEncode:
|
||||
"optional": {"filter_tags": ("STRING", {"default": ""})},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
CATEGORY = "promptcontrol/old"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "parse"
|
||||
|
||||
def parse(self, clip, text, filter_tags=""):
|
||||
@@ -661,9 +663,7 @@ def control_to_clip_common(clip, schedules, lora_cache=None, cond_cache=None):
|
||||
cond = cond_cache.get(cachekey)
|
||||
if cond is None:
|
||||
if loras != current_loras:
|
||||
_, clip = utils.apply_loras_from_spec(
|
||||
loras, clip=orig_clip, cache=lora_cache, applied_loras=current_loras
|
||||
)
|
||||
_, clip = apply_loras_from_spec(loras, clip=orig_clip, cache=lora_cache, applied_loras=current_loras)
|
||||
current_loras = loras
|
||||
cond_cache[cachekey] = do_encode(clip, prompt, schedules.defaults, schedules.masks)
|
||||
return cond_cache[cachekey]
|
||||
@@ -2,11 +2,11 @@ import logging
|
||||
import torch
|
||||
|
||||
from .utils import unpatch_model, clone_model, set_callback, apply_loras_from_spec
|
||||
from .parser import parse_prompt_schedules
|
||||
from ..parser import parse_prompt_schedules
|
||||
from .hijack import do_hijack
|
||||
from comfy.samplers import CFGGuider
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
|
||||
def apply_lora_for_step(schedules, step, total_steps, state, original_model, lora_cache, patch=True):
|
||||
@@ -141,7 +141,8 @@ class PCWrapGuider:
|
||||
},
|
||||
}
|
||||
|
||||
CATEGORY = "promptcontrol"
|
||||
DEPRECATED = True
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
RETURN_TYPES = ("GUIDER",)
|
||||
|
||||
@@ -200,8 +201,9 @@ class ScheduleToModel:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, prompt_schedule):
|
||||
@@ -218,8 +220,9 @@ class PCSplitSampling:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, split_sampling):
|
||||
@@ -238,8 +241,9 @@ class LoRAScheduler:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
CATEGORY = "promptcontrol/old"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, model, text):
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from .parser import parse_prompt_schedules
|
||||
from ..parser import parse_prompt_schedules
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
|
||||
class FilterSchedule:
|
||||
@@ -16,8 +16,9 @@ class FilterSchedule:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("PROMPT_SCHEDULE",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, prompt_schedule, tags="", start=0.0, end=1.0):
|
||||
@@ -34,8 +35,9 @@ class PCApplySettings:
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": {"prompt_schedule": ("PROMPT_SCHEDULE",), "settings": ("SCHEDULE_SETTINGS",)}}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("PROMPT_SCHEDULE",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, prompt_schedule, settings):
|
||||
@@ -55,8 +57,9 @@ class PCScheduleAddMasks:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("PROMPT_SCHEDULE",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, prompt_schedule, mask1=None, mask2=None, mask3=None, mask4=None):
|
||||
@@ -83,8 +86,9 @@ class PCScheduleSettings:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("SCHEDULE_SETTINGS",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(
|
||||
@@ -124,8 +128,9 @@ class PCPromptFromSchedule:
|
||||
"optional": {"tags": ("STRING", {"default": ""})},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("STRING",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, prompt_schedule, at, tags=""):
|
||||
@@ -144,8 +149,9 @@ class PromptToSchedule:
|
||||
},
|
||||
}
|
||||
|
||||
DEPRECATED = True
|
||||
RETURN_TYPES = ("PROMPT_SCHEDULE",)
|
||||
CATEGORY = "promptcontrol"
|
||||
CATEGORY = "promptcontrol/_legacy"
|
||||
FUNCTION = "parse"
|
||||
|
||||
def parse(self, text, settings=None):
|
||||
@@ -0,0 +1,265 @@
|
||||
from collections import namedtuple
|
||||
from os import environ
|
||||
from math import lcm
|
||||
import time
|
||||
import logging
|
||||
import torch
|
||||
|
||||
from ..utils import lora_name_to_file, safe_float
|
||||
|
||||
import nodes
|
||||
|
||||
import comfy.model_management
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
FORCE_CPU_OFFLOAD = bool(environ.get("COMFYUI_PC_CPU_OFFLOAD"))
|
||||
|
||||
|
||||
# Minimal Modelpatcher that doesn't do anything, for LoRA loading when not
|
||||
# interested in either CLIP or unet
|
||||
class DummyModelPatcher:
|
||||
class DummyTorchModel:
|
||||
def __init__(self):
|
||||
dummyconf = {
|
||||
"num_res_blocks": [],
|
||||
"channel_mult": [],
|
||||
"transformer_depth": [],
|
||||
"transformer_depth_output": [],
|
||||
"transformer_depth_middle": 0,
|
||||
}
|
||||
self.model_config = namedtuple("DummyConfig", ["unet_config"])(dummyconf)
|
||||
|
||||
def state_dict(self):
|
||||
return {}
|
||||
|
||||
def __init__(self):
|
||||
self.model = self.DummyTorchModel()
|
||||
self.cond_stage_model = self.DummyTorchModel()
|
||||
self.weight_inplace_update = True
|
||||
self.model_options = {}
|
||||
|
||||
def add_patches(self, patches, *args, **kwargs):
|
||||
return []
|
||||
|
||||
def patch_model(self):
|
||||
pass
|
||||
|
||||
def unpatch_model(self):
|
||||
pass
|
||||
|
||||
def clone(self):
|
||||
return self
|
||||
|
||||
|
||||
DUMMY_MODEL = DummyModelPatcher()
|
||||
|
||||
|
||||
def equalize(*tensors):
|
||||
if all(t.shape[1] == tensors[0].shape[1] for t in tensors):
|
||||
return tensors
|
||||
|
||||
x = lcm(*(t.shape[1] for t in tensors))
|
||||
|
||||
return (t.repeat(1, x // t.shape[1], 1) for t in tensors)
|
||||
|
||||
|
||||
def unpatch_model(model):
|
||||
if model:
|
||||
log.info("Unpatching model")
|
||||
model.unpatch_model()
|
||||
|
||||
|
||||
def clone_model(model):
|
||||
if not model:
|
||||
return None
|
||||
model = model.clone()
|
||||
if not environ.get("PC_NO_INPLACE_UPDATE"):
|
||||
model.weight_inplace_update = True
|
||||
return model
|
||||
|
||||
|
||||
def add_patches(model, patches, weight):
|
||||
model.add_patches(patches, weight)
|
||||
|
||||
|
||||
def patch_model(model, forget=False, orig=None):
|
||||
global FORCE_CPU_OFFLOAD
|
||||
try:
|
||||
return _patch_model(model, forget, orig, FORCE_CPU_OFFLOAD)
|
||||
except comfy.model_management.OOM_EXCEPTION:
|
||||
FORCE_CPU_OFFLOAD = True
|
||||
log.error("Ran out of memory while applying LoRAs, Forcing CPU offload from now on")
|
||||
# Unpatch to restore partially applied weights
|
||||
unpatch_model(model)
|
||||
raise
|
||||
|
||||
|
||||
def _patch_model(model, forget=False, orig=None, offload_to_cpu=False):
|
||||
if not model:
|
||||
return None
|
||||
if offload_to_cpu:
|
||||
saved_offload = model.offload_device
|
||||
model.offload_device = torch.device("cpu")
|
||||
log.info(
|
||||
"Patching model, model.load_device=%s model.model.device=%s cpu_offload=%s",
|
||||
model.load_device,
|
||||
model.model.device,
|
||||
model.offload_device == torch.device("cpu"),
|
||||
)
|
||||
if orig:
|
||||
model.backup = orig.backup
|
||||
model.patch_model(device_to=model.load_device)
|
||||
if offload_to_cpu:
|
||||
model.offload_device = saved_offload
|
||||
if forget:
|
||||
model.patches = {}
|
||||
model.object_patches = {}
|
||||
return model
|
||||
|
||||
|
||||
def get_callback(model):
|
||||
return model.model_options.get("prompt_control_callback")
|
||||
|
||||
|
||||
def set_callback(model, cb):
|
||||
model.model_options["prompt_control_callback"] = cb
|
||||
|
||||
|
||||
# Hack to temporarily override printing to stdout to stop log spam
|
||||
def suppress_print(f):
|
||||
def noop(*args):
|
||||
pass
|
||||
|
||||
p = print
|
||||
__builtins__["print"] = noop
|
||||
rootlogger = logging.getLogger()
|
||||
oldlevel = rootlogger.level
|
||||
try:
|
||||
rootlogger.setLevel(logging.ERROR)
|
||||
x = f()
|
||||
except BaseException:
|
||||
__builtins__["print"] = p
|
||||
rootlogger.setLevel(oldlevel)
|
||||
raise
|
||||
__builtins__["print"] = p
|
||||
rootlogger.setLevel(oldlevel)
|
||||
return x
|
||||
|
||||
|
||||
def load_lbw():
|
||||
return nodes.NODE_CLASS_MAPPINGS.get("LoraLoaderBlockWeight //Inspire")
|
||||
|
||||
|
||||
def make_loader(filename, lbw):
|
||||
if not lbw:
|
||||
l = nodes.LoraLoader()
|
||||
|
||||
def loader(model, clip, model_weight, clip_weight, lbw):
|
||||
return suppress_print(lambda: l.load_lora(model, clip, filename, model_weight, clip_weight))
|
||||
|
||||
else:
|
||||
# This is already checked before calling make_loader
|
||||
l = load_lbw()()
|
||||
|
||||
def loader(model, clip, model_weight, clip_weight, lbw):
|
||||
spec = lbw["LBW"]
|
||||
lbw_a = safe_float(lbw.get("A"), 4.0)
|
||||
lbw_b = safe_float(lbw.get("B"), 1.0)
|
||||
m = model or DUMMY_MODEL
|
||||
c = clip or DUMMY_MODEL
|
||||
m, c, _ = suppress_print(
|
||||
lambda: l.doit(m, c, filename, model_weight, clip_weight, False, 0, lbw_a, lbw_b, "", spec)
|
||||
)
|
||||
if m is DUMMY_MODEL:
|
||||
m = None
|
||||
if c is DUMMY_MODEL:
|
||||
c = None
|
||||
return m, c
|
||||
|
||||
return loader
|
||||
|
||||
|
||||
def apply_loras_from_spec(
|
||||
loraspec, model=None, clip=None, orig_model=None, orig_clip=None, patch=False, cache=None, applied_loras=None
|
||||
):
|
||||
if applied_loras is None:
|
||||
applied_loras = {}
|
||||
actual_loraspec = {}
|
||||
additive = True
|
||||
for key in loraspec:
|
||||
if key in applied_loras and applied_loras[key] == loraspec[key]:
|
||||
continue
|
||||
if key in applied_loras and applied_loras[key] != loraspec[key]:
|
||||
additive = False
|
||||
actual_loraspec[key] = loraspec[key]
|
||||
|
||||
for key in applied_loras:
|
||||
if key not in loraspec:
|
||||
actual_loraspec = loraspec
|
||||
additive = False
|
||||
|
||||
backup_model = model
|
||||
if not additive:
|
||||
unpatch_model(model)
|
||||
# Reset clip to unpatched
|
||||
if clip:
|
||||
clip = orig_clip or clip
|
||||
|
||||
if cache is None:
|
||||
cache = {}
|
||||
if not loraspec:
|
||||
return model, clip
|
||||
|
||||
for name, params in actual_loraspec.items():
|
||||
m, c = model, clip
|
||||
w, w_clip = params["weight"], params["weight_clip"]
|
||||
if w == 0:
|
||||
m = None
|
||||
if w_clip == 0:
|
||||
c = None
|
||||
if not w and not c:
|
||||
continue
|
||||
|
||||
lbw = params.get("lbw")
|
||||
if lbw and not load_lbw():
|
||||
log.warning("LoraBlockWeight not available, ignoring LBW parameters")
|
||||
lbw = None
|
||||
|
||||
# Cache the loader instance so that it doesn't reload the LoRA from disk all the time
|
||||
cache_key = name, bool(lbw)
|
||||
loader = cache.get(cache_key)
|
||||
if not loader:
|
||||
f = lora_name_to_file(name)
|
||||
if not f:
|
||||
log.warning("Lora %s not found", name)
|
||||
continue
|
||||
log.info("Loading LoRA: %s", f)
|
||||
loader = make_loader(f, bool(lbw))
|
||||
cache[cache_key] = loader
|
||||
|
||||
m, c = loader(m, c, w, w_clip, lbw)
|
||||
model = m or model
|
||||
clip = c or clip
|
||||
if model:
|
||||
log.info("Applying LoRA: %s:%s, LBW=%s, additive=%s", name, params["weight"], bool(lbw), additive)
|
||||
if clip:
|
||||
log.info("Applying CLIP LoRA: %s:%s, LBW=%s, additive=%s", name, params["weight_clip"], bool(lbw), additive)
|
||||
|
||||
# forget patches so we don't double-patch
|
||||
model = patch_model(model, forget=True, orig=backup_model)
|
||||
return model, clip
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.start = None
|
||||
|
||||
def __enter__(self):
|
||||
self.start = time.time()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
elapsed = time.time() - self.start
|
||||
if environ.get("PC_SHOW_TIMINGS"):
|
||||
log.info("Executed %s in %s seconds", self.name, elapsed)
|
||||
@@ -1,508 +0,0 @@
|
||||
import logging
|
||||
import re
|
||||
import torch
|
||||
from .utils import safe_float, get_function, parse_floats, lora_name_to_file
|
||||
from comfy_extras.nodes_mask import FeatherMask, MaskComposite
|
||||
import comfy.utils
|
||||
import comfy.hooks
|
||||
import folder_paths
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
AVAILABLE_STYLES = ["comfy"]
|
||||
AVAILABLE_NORMALIZATIONS = ["none"]
|
||||
|
||||
have_advanced_encode = False
|
||||
try:
|
||||
import custom_nodes.ComfyUI_ADV_CLIP_emb.adv_encode as adv_encode
|
||||
|
||||
have_advanced_encode = True
|
||||
AVAILABLE_STYLES.extend(["A1111", "compel", "comfy++", "down_weight"])
|
||||
AVAILABLE_NORMALIZATIONS.extend(["mean", "length", "length+mean"])
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class PCLoraHooksFromSchedule:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"prompt_schedule": ("PROMPT_SCHEDULE",)},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("HOOKS",)
|
||||
OUTPUT_TOOLTIPS = ("set of hooks created from the prompt schedule",)
|
||||
CATEGORY = "promptcontrol/_unstable"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, prompt_schedule):
|
||||
return (lora_hooks_from_schedule(prompt_schedule),)
|
||||
|
||||
|
||||
class PCEncodeSchedule:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {"clip": ("CLIP",), "prompt_schedule": ("PROMPT_SCHEDULE",)},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("CONDITIONING",)
|
||||
CATEGORY = "promptcontrol/_unstable"
|
||||
FUNCTION = "apply"
|
||||
|
||||
def apply(self, clip, prompt_schedule):
|
||||
return (encode_schedule(clip, prompt_schedule),)
|
||||
|
||||
|
||||
SHUFFLE_GEN = torch.Generator(device="cpu")
|
||||
|
||||
|
||||
def get_sdxl(text, defaults):
|
||||
# Defaults fail to parse and get looked up from the defaults dict
|
||||
text, sdxl = get_function(text, "SDXL", ["none", "none", "none"])
|
||||
if not sdxl:
|
||||
return text, {}
|
||||
args = sdxl[0]
|
||||
d = defaults
|
||||
w, h = parse_floats(args[0], [d.get("sdxl_width", 1024), d.get("sdxl_height", 1024)], split_re="\\s+")
|
||||
tw, th = parse_floats(args[1], [d.get("sdxl_twidth", 1024), d.get("sdxl_theight", 1024)], split_re="\\s+")
|
||||
cropw, croph = parse_floats(args[2], [d.get("sdxl_cwidth", 0), d.get("sdxl_cheight", 0)], split_re="\\s+")
|
||||
|
||||
opts = {
|
||||
"width": int(w),
|
||||
"height": int(h),
|
||||
"target_width": int(tw),
|
||||
"target_height": int(th),
|
||||
"crop_w": int(cropw),
|
||||
"crop_h": int(croph),
|
||||
}
|
||||
return text, opts
|
||||
|
||||
|
||||
def get_style(text, default_style="comfy", default_normalization="none"):
|
||||
text, styles = get_function(text, "STYLE", [default_style, default_normalization])
|
||||
if not styles:
|
||||
return default_style, default_normalization, text
|
||||
style, normalization = styles[0]
|
||||
style = style.strip()
|
||||
normalization = normalization.strip()
|
||||
if style not in AVAILABLE_STYLES:
|
||||
log.warning("Unrecognized prompt style: %s. Using %s", style, default_style)
|
||||
style = default_style
|
||||
|
||||
if normalization not in AVAILABLE_NORMALIZATIONS:
|
||||
log.warning("Unrecognized prompt normalization: %s. Using %s", normalization, default_normalization)
|
||||
normalization = default_normalization
|
||||
|
||||
return style, normalization, text
|
||||
|
||||
|
||||
def shuffle_chunk(shuffle, c):
|
||||
func, shuffle = shuffle
|
||||
shuffle_count = int(safe_float(shuffle[0], 0))
|
||||
_, separator, joiner = shuffle
|
||||
if separator == "default":
|
||||
separator = ","
|
||||
|
||||
if not separator:
|
||||
separator = ","
|
||||
|
||||
joiner = {
|
||||
"default": ",",
|
||||
"separator": separator,
|
||||
}.get(joiner, joiner)
|
||||
|
||||
log.info("%s arg=%s sep=%s join=%s", func, shuffle_count, separator, joiner)
|
||||
separated = c.split(separator)
|
||||
if func == "SHIFT":
|
||||
shuffle_count = shuffle_count % len(separated)
|
||||
permutation = separated[shuffle_count:] + separated[:shuffle_count]
|
||||
elif func == "SHUFFLE":
|
||||
SHUFFLE_GEN.manual_seed(shuffle_count)
|
||||
permutation = [separated[i] for i in torch.randperm(len(separated), generator=SHUFFLE_GEN)]
|
||||
else:
|
||||
# ??? should never get here
|
||||
permutation = separated
|
||||
|
||||
permutation = [p for p in permutation if p.strip()]
|
||||
if permutation != separated:
|
||||
c = joiner.join(permutation)
|
||||
return c
|
||||
|
||||
|
||||
def fix_word_ids(tokens):
|
||||
"""Fix word indexes. Tokenizing separately (when BREAKs exist) causes the indexes to restart which causes problems with some weighting algorithms that rely on them"""
|
||||
for key in tokens:
|
||||
max_idx = 0
|
||||
for group in range(len(tokens[key])):
|
||||
for i, token in enumerate(tokens[key][group]):
|
||||
if len(token) < 3:
|
||||
# No need to fix ids when they don't exist
|
||||
return tokens
|
||||
# Ignore zeros, they represent the padding token
|
||||
if token[2] != 0 and token[2] < max_idx:
|
||||
tokens[key][group][i] = (token[0], token[1], token[2] + max_idx)
|
||||
max_idx = max(max_idx, max(x for _, _, x in tokens[key][group]))
|
||||
return tokens
|
||||
|
||||
|
||||
def encode_prompt(
|
||||
clip, text, settings, default_style="comfy", default_normalization="none"
|
||||
) -> list[tuple[torch.Tensor, dict[str]]]:
|
||||
style, normalization, text = get_style(text, default_style, default_normalization)
|
||||
# defaults=None means there is no argument parsing at all
|
||||
text, l_prompts = get_function(text, "CLIP_L", defaults=None)
|
||||
chunks = re.split(r"\bBREAK\b", text)
|
||||
token_chunks = []
|
||||
need_word_ids = have_advanced_encode or style == "comfy" and normalization == "none"
|
||||
for c in chunks:
|
||||
c, shuffles = get_function(c.strip(), "(SHIFT|SHUFFLE)", ["0", "default", "default"], return_func_name=True)
|
||||
r = c
|
||||
for s in shuffles:
|
||||
r = shuffle_chunk(s, r)
|
||||
if r != c:
|
||||
log.info("Shuffled prompt chunk to %s", r)
|
||||
c = r
|
||||
t = clip.tokenize(c, return_word_ids=need_word_ids)
|
||||
token_chunks.append(t)
|
||||
tokens = token_chunks[0]
|
||||
|
||||
for key in tokens:
|
||||
for c in token_chunks[1:]:
|
||||
tokens[key].extend(c[key])
|
||||
|
||||
# Non-SDXL has only "l"
|
||||
if "g" in tokens and l_prompts:
|
||||
text_l = " ".join(l_prompts)
|
||||
log.info("Encoded SDXL CLIP_L prompt: %s", text_l)
|
||||
tokens["l"] = clip.tokenize(text_l, return_word_ids=need_word_ids)["l"]
|
||||
|
||||
if "g" in tokens and "l" in tokens and len(tokens["l"]) != len(tokens["g"]):
|
||||
empty = clip.tokenize("", return_word_ids=need_word_ids)
|
||||
while len(tokens["l"]) < len(tokens["g"]):
|
||||
tokens["l"] += empty["l"]
|
||||
while len(tokens["l"]) > len(tokens["g"]):
|
||||
tokens["g"] += empty["g"]
|
||||
|
||||
tokens = fix_word_ids(tokens)
|
||||
|
||||
newclip = clip
|
||||
if have_advanced_encode:
|
||||
newclip = clip.clone()
|
||||
if hasattr(clip.patcher.model, "clip_g"):
|
||||
newclip.patcher.add_object_patch(
|
||||
"clip_g.encode_token_weights",
|
||||
encoder_patch(style, normalization, clip.patcher.get_model_object("clip_g.encode_token_weights")),
|
||||
)
|
||||
if hasattr(clip.patcher.model, "clip_l"):
|
||||
newclip.patcher.add_object_patch(
|
||||
"clip_l.encode_token_weights",
|
||||
encoder_patch(style, normalization, clip.patcher.get_model_object("clip_l.encode_token_weights")),
|
||||
)
|
||||
|
||||
return newclip.encode_from_tokens_scheduled(tokens, add_dict=settings)
|
||||
|
||||
|
||||
def encoder_patch(style, normalization, orig_fn):
|
||||
if not have_advanced_encode or style == "comfy" and normalization == "none":
|
||||
return orig_fn
|
||||
else:
|
||||
log.debug("Encoding with style=%s, normalization=%s", style, normalization)
|
||||
return lambda t: adv_encode.advanced_encode_from_tokens(
|
||||
t, normalization, style, orig_fn, return_pooled=True, apply_to_pooled=False
|
||||
)
|
||||
|
||||
|
||||
def get_area(text):
|
||||
text, areas = get_function(text, "AREA", ["0 1", "0 1", "1"])
|
||||
if not areas:
|
||||
return text, None
|
||||
|
||||
args = areas[0]
|
||||
x, w = parse_floats(args[0], [0.0, 1.0], split_re="\\s+")
|
||||
y, h = parse_floats(args[1], [0.0, 1.0], split_re="\\s+")
|
||||
weight = safe_float(args[2], 1.0)
|
||||
|
||||
def is_pct(f):
|
||||
return f >= 0.0 and f <= 1.0
|
||||
|
||||
def is_pixel(f):
|
||||
return f == 0 or f > 1
|
||||
|
||||
if all(is_pct(v) for v in [h, w, y, x]):
|
||||
area = ("percentage", h, w, y, x)
|
||||
elif all(is_pixel(v) for v in [h, w, y, x]):
|
||||
area = (int(h) // 8, int(w) // 8, int(y) // 8, int(x) // 8)
|
||||
else:
|
||||
raise Exception(
|
||||
f"AREA specified with invalid size {x} {w}, {h} {y}. They must either all be percentages between 0 and 1 or positive integer pixel values excluding 1"
|
||||
)
|
||||
|
||||
return text, (area, weight)
|
||||
|
||||
|
||||
def get_mask_size(text, defaults):
|
||||
text, sizes = get_function(text, "MASK_SIZE", ["512", "512"])
|
||||
if not sizes:
|
||||
return text, (defaults.get("mask_width", 512), defaults.get("mask_height", 512))
|
||||
w, h = sizes[0]
|
||||
return text, (int(w), int(h))
|
||||
|
||||
|
||||
def make_mask(args, size, weight):
|
||||
x1, x2 = parse_floats(args[0], [0.0, 1.0], split_re="\\s+")
|
||||
y1, y2 = parse_floats(args[1], [0.0, 1.0], split_re="\\s+")
|
||||
|
||||
def is_pct(f):
|
||||
return f >= 0.0 and f <= 1.0
|
||||
|
||||
def is_pixel(f):
|
||||
return f == 0 or f > 1
|
||||
|
||||
if all(is_pct(v) for v in [x1, x2, y1, y2]):
|
||||
w, h = size
|
||||
xs = int(w * x1), int(w * x2)
|
||||
ys = int(h * y1), int(h * y2)
|
||||
elif all(is_pixel(v) for v in [x1, x2, y1, y2]):
|
||||
w, h = size
|
||||
xs = int(x1), int(x2)
|
||||
ys = int(y1), int(y2)
|
||||
else:
|
||||
raise Exception(
|
||||
f"MASK specified with invalid size {x1} {x2}, {y1} {y2}. They must either all be percentages between 0 and 1 or positive integer pixel values excluding 1"
|
||||
)
|
||||
|
||||
mask = torch.full((h, w), 0, dtype=torch.float32, device="cpu")
|
||||
mask[ys[0] : ys[1], xs[0] : xs[1]] = weight
|
||||
mask = mask.unsqueeze(0)
|
||||
log.info("Mask xs=%s, ys=%s, shape=%s, weight=%s", xs, ys, mask.shape, weight)
|
||||
return mask
|
||||
|
||||
|
||||
def get_mask(text, size, input_masks):
|
||||
"""Parse MASK(x1 x2, y1 y2, weight), IMASK(i, weight) and FEATHER(left top right bottom)"""
|
||||
# TODO: combine multiple masks
|
||||
text, masks = get_function(text, "MASK", ["0 1", "0 1", "1", "multiply"])
|
||||
text, imasks = get_function(text, "IMASK", ["0", "1", "multiply"])
|
||||
text, feathers = get_function(text, "FEATHER", ["0 0 0 0"])
|
||||
text, maskw = get_function(text, "MASKW", ["1.0"])
|
||||
if not masks and not imasks:
|
||||
return text, None, None
|
||||
|
||||
def feather(f, mask):
|
||||
l, t, r, b, *_ = [int(x) for x in parse_floats(f[0], [0, 0, 0, 0], split_re="\\s+")]
|
||||
mask = FeatherMask().feather(mask, l, t, r, b)[0]
|
||||
log.info("FeatherMask l=%s, t=%s, r=%s, b=%s", l, t, r, b)
|
||||
return mask
|
||||
|
||||
mask = None
|
||||
totalweight = 1.0
|
||||
if maskw:
|
||||
totalweight = safe_float(maskw[0][0], 1.0)
|
||||
i = 0
|
||||
for m in masks:
|
||||
weight = safe_float(m[2], 1.0)
|
||||
op = m[3]
|
||||
nextmask = make_mask(m, size, weight)
|
||||
if i < len(feathers):
|
||||
nextmask = feather(feathers[i], nextmask)
|
||||
i += 1
|
||||
if mask is not None:
|
||||
log.info("MaskComposite op=%s", op)
|
||||
mask = MaskComposite().combine(mask, nextmask, 0, 0, op)[0]
|
||||
else:
|
||||
mask = nextmask
|
||||
|
||||
for idx, w, op in imasks:
|
||||
idx = int(safe_float(idx, 0.0))
|
||||
w = safe_float(w, 1.0)
|
||||
if len(input_masks) < idx + 1:
|
||||
log.warn("IMASK index %s not found, ignoring...", idx)
|
||||
continue
|
||||
nextmask = input_masks[idx] * w
|
||||
if i < len(feathers):
|
||||
nextmask = feather(feathers[i], nextmask)
|
||||
i += 1
|
||||
if mask is not None:
|
||||
mask = MaskComposite().combine(mask, nextmask, 0, 0, op)[0]
|
||||
else:
|
||||
mask = nextmask
|
||||
|
||||
# apply leftover FEATHER() specs to the whole
|
||||
for f in feathers[i:]:
|
||||
mask = feather(f, mask)
|
||||
|
||||
return text, mask, totalweight
|
||||
|
||||
|
||||
def get_noise(text):
|
||||
text, noises = get_function(
|
||||
text,
|
||||
"NOISE",
|
||||
["0.0", "none"],
|
||||
)
|
||||
if not noises:
|
||||
return text, None, None
|
||||
w = 0
|
||||
# Only take seed from first noise spec, for simplicity
|
||||
seed = safe_float(noises[0][1], "none")
|
||||
if seed == "none":
|
||||
gen = None
|
||||
else:
|
||||
gen = torch.Generator()
|
||||
gen.manual_seed(int(seed))
|
||||
for n in noises:
|
||||
w += safe_float(n[0], 0.0)
|
||||
return text, max(min(w, 1.0), 0.0), gen
|
||||
|
||||
|
||||
def apply_noise(cond, weight, gen):
|
||||
if cond is None or not weight:
|
||||
return cond
|
||||
|
||||
n = torch.randn(cond.size(), generator=gen).to(cond)
|
||||
|
||||
return cond * (1 - weight) + n * weight
|
||||
|
||||
|
||||
def do_encode(clip, text, start_pct, end_pct, defaults, masks):
|
||||
# First style modifier applies to ANDed prompts too unless overridden
|
||||
style, normalization, text = get_style(text)
|
||||
text, mask_size = get_mask_size(text, defaults)
|
||||
|
||||
prompts = [p.strip() for p in re.split(r"\bAND\b", text)]
|
||||
|
||||
p, sdxl_opts = get_sdxl(prompts[0], defaults)
|
||||
prompts[0] = p
|
||||
|
||||
def weight(t):
|
||||
opts = {}
|
||||
m = re.search(r":(-?\d\.?\d*)(![A-Za-z]+)?$", t)
|
||||
if not m:
|
||||
return (1.0, opts, t)
|
||||
w = float(m[1])
|
||||
tag = m[2]
|
||||
t = t[: m.span()[0]]
|
||||
if tag == "!noscale":
|
||||
opts["scale"] = 1
|
||||
|
||||
return w, opts, t
|
||||
|
||||
conds = []
|
||||
scale = sum(abs(weight(p)[0]) for p in prompts if not ("AREA(" in p or "MASK(" in p))
|
||||
for prompt in prompts:
|
||||
prompt, mask, mask_weight = get_mask(prompt, mask_size, masks)
|
||||
w, opts, prompt = weight(prompt)
|
||||
text, noise_w, generator = get_noise(text)
|
||||
if not w:
|
||||
continue
|
||||
prompt, area = get_area(prompt)
|
||||
prompt, local_sdxl_opts = get_sdxl(prompt, defaults)
|
||||
settings = {"prompt": prompt}
|
||||
settings["strength"] = w
|
||||
settings.update(sdxl_opts)
|
||||
settings.update(local_sdxl_opts)
|
||||
if area:
|
||||
settings["area"] = area[0]
|
||||
settings["strength"] = area[1]
|
||||
settings["set_area_to_bounds"] = False
|
||||
if mask is not None:
|
||||
settings["mask"] = mask
|
||||
settings["mask_strength"] = mask_weight
|
||||
|
||||
settings["start_percent"] = start_pct
|
||||
settings["end_percent"] = end_pct
|
||||
x = encode_prompt(clip, prompt, settings, style, normalization)
|
||||
conds.extend(x)
|
||||
|
||||
return conds
|
||||
|
||||
|
||||
def debug_conds(conds):
|
||||
r = []
|
||||
for i, c in enumerate(conds):
|
||||
x = c[1].copy()
|
||||
if "pooled_output" in x:
|
||||
del x["pooled_output"]
|
||||
r.append((i, x))
|
||||
return r
|
||||
|
||||
|
||||
def lora_hooks_from_schedule(schedules):
|
||||
start_pct = 0.0
|
||||
|
||||
lora_cache = {}
|
||||
|
||||
all_hooks = []
|
||||
|
||||
prev_loras = {}
|
||||
|
||||
def create_hook(loraspec, start_pct, end_pct):
|
||||
nonlocal lora_cache
|
||||
hooks = []
|
||||
hook_kf = comfy.hooks.HookKeyframeGroup()
|
||||
for lora, info in loras.items():
|
||||
path = lora_name_to_file(lora)
|
||||
if not path:
|
||||
continue
|
||||
if path not in lora_cache:
|
||||
lora_cache[path] = comfy.utils.load_torch_file(
|
||||
folder_paths.get_full_path("loras", path), safe_load=True
|
||||
)
|
||||
new_hook = comfy.hooks.create_hook_lora(
|
||||
lora_cache[path], strength_model=info["weight"], strength_clip=info["weight_clip"]
|
||||
)
|
||||
# Set hook_ref so that identical hooks compare equal
|
||||
new_hook.hooks[0].hook_ref = f"pc-{path}-{info['weight']}-{info['weight_clip']}"
|
||||
hooks.append(new_hook)
|
||||
if start_pct > 0.0:
|
||||
kf = comfy.hooks.HookKeyframe(strength=0.0, start_percent=0.0)
|
||||
hook_kf.add(kf)
|
||||
kf = comfy.hooks.HookKeyframe(strength=1.0, start_percent=start_pct)
|
||||
hook_kf.add(kf)
|
||||
if end_pct < 1.0:
|
||||
kf = comfy.hooks.HookKeyframe(strength=0.0, start_percent=end_pct)
|
||||
hook_kf.add(kf)
|
||||
hooks = comfy.hooks.HookGroup.combine_all_hooks(hooks)
|
||||
if hooks:
|
||||
hooks.set_keyframes_on_hooks(hook_kf=hook_kf)
|
||||
return hooks
|
||||
|
||||
consolidated = []
|
||||
|
||||
prev_loras = {}
|
||||
for end_pct, c in reversed(list(schedules)):
|
||||
loras = c["loras"]
|
||||
if loras != prev_loras:
|
||||
consolidated.append((end_pct, loras))
|
||||
prev_loras = loras
|
||||
consolidated = reversed(consolidated)
|
||||
|
||||
for end_pct, loras in consolidated:
|
||||
log.info("Creating LoRA hook from %s to %s: %s", start_pct, end_pct, loras)
|
||||
hook = create_hook(loras, start_pct, end_pct)
|
||||
all_hooks.append(hook)
|
||||
start_pct = end_pct
|
||||
|
||||
del lora_cache
|
||||
|
||||
all_hooks = [x for x in all_hooks if x is not None]
|
||||
|
||||
if all_hooks:
|
||||
hooks = comfy.hooks.HookGroup.combine_all_hooks(all_hooks)
|
||||
return hooks
|
||||
|
||||
|
||||
def encode_schedule(clip, schedules):
|
||||
start_pct = 0.0
|
||||
conds = []
|
||||
for end_pct, c in schedules:
|
||||
if start_pct < end_pct:
|
||||
prompt = c["prompt"]
|
||||
cond = do_encode(clip, prompt, start_pct, end_pct, schedules.defaults, schedules.masks)
|
||||
conds.extend(cond)
|
||||
start_pct = end_pct
|
||||
log.debug("Conds at the end: %s", debug_conds(conds))
|
||||
|
||||
log.debug("Final cond info: %s", debug_conds(conds))
|
||||
return conds
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from math import ceil
|
||||
|
||||
logging.basicConfig()
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
if lark.__version__ == "0.12.0":
|
||||
x = "Your lark package reports an ancient version (0.12.0) and will not work. If you have the 'lark-parser' package in your Python environment, remove that and *reinstall* lark!"
|
||||
|
||||
+1
-261
@@ -1,60 +1,10 @@
|
||||
from collections import namedtuple
|
||||
from os import environ
|
||||
from pathlib import Path
|
||||
import re
|
||||
from math import lcm
|
||||
import time
|
||||
import logging
|
||||
import torch
|
||||
|
||||
|
||||
import nodes
|
||||
import folder_paths
|
||||
|
||||
import comfy.model_management
|
||||
|
||||
log = logging.getLogger("comfyui-prompt-control")
|
||||
|
||||
FORCE_CPU_OFFLOAD = bool(environ.get("COMFYUI_PC_CPU_OFFLOAD"))
|
||||
|
||||
|
||||
# Minimal Modelpatcher that doesn't do anything, for LoRA loading when not
|
||||
# interested in either CLIP or unet
|
||||
class DummyModelPatcher:
|
||||
class DummyTorchModel:
|
||||
def __init__(self):
|
||||
dummyconf = {
|
||||
"num_res_blocks": [],
|
||||
"channel_mult": [],
|
||||
"transformer_depth": [],
|
||||
"transformer_depth_output": [],
|
||||
"transformer_depth_middle": 0,
|
||||
}
|
||||
self.model_config = namedtuple("DummyConfig", ["unet_config"])(dummyconf)
|
||||
|
||||
def state_dict(self):
|
||||
return {}
|
||||
|
||||
def __init__(self):
|
||||
self.model = self.DummyTorchModel()
|
||||
self.cond_stage_model = self.DummyTorchModel()
|
||||
self.weight_inplace_update = True
|
||||
self.model_options = {}
|
||||
|
||||
def add_patches(self, patches, *args, **kwargs):
|
||||
return []
|
||||
|
||||
def patch_model(self):
|
||||
pass
|
||||
|
||||
def unpatch_model(self):
|
||||
pass
|
||||
|
||||
def clone(self):
|
||||
return self
|
||||
|
||||
|
||||
DUMMY_MODEL = DummyModelPatcher()
|
||||
log = logging.getLogger("comfyui-prompt-control-legacy")
|
||||
|
||||
|
||||
def find_closing_paren(text, start):
|
||||
@@ -118,15 +68,6 @@ def parse_strings(string, defaults, split_re=r"(?<!\\),", replace=(r"\,", ",")):
|
||||
return parse_args(splits, spec, strip=False)
|
||||
|
||||
|
||||
def equalize(*tensors):
|
||||
if all(t.shape[1] == tensors[0].shape[1] for t in tensors):
|
||||
return tensors
|
||||
|
||||
x = lcm(*(t.shape[1] for t in tensors))
|
||||
|
||||
return (t.repeat(1, x // t.shape[1], 1) for t in tensors)
|
||||
|
||||
|
||||
def safe_float(f, default):
|
||||
if f is None:
|
||||
return default
|
||||
@@ -136,89 +77,6 @@ def safe_float(f, default):
|
||||
return default
|
||||
|
||||
|
||||
def unpatch_model(model):
|
||||
if model:
|
||||
log.info("Unpatching model")
|
||||
model.unpatch_model()
|
||||
|
||||
|
||||
def clone_model(model):
|
||||
if not model:
|
||||
return None
|
||||
model = model.clone()
|
||||
if not environ.get("PC_NO_INPLACE_UPDATE"):
|
||||
model.weight_inplace_update = True
|
||||
return model
|
||||
|
||||
|
||||
def add_patches(model, patches, weight):
|
||||
model.add_patches(patches, weight)
|
||||
|
||||
|
||||
def patch_model(model, forget=False, orig=None):
|
||||
global FORCE_CPU_OFFLOAD
|
||||
try:
|
||||
return _patch_model(model, forget, orig, FORCE_CPU_OFFLOAD)
|
||||
except comfy.model_management.OOM_EXCEPTION:
|
||||
FORCE_CPU_OFFLOAD = True
|
||||
log.error("Ran out of memory while applying LoRAs, Forcing CPU offload from now on")
|
||||
# Unpatch to restore partially applied weights
|
||||
unpatch_model(model)
|
||||
raise
|
||||
|
||||
|
||||
def _patch_model(model, forget=False, orig=None, offload_to_cpu=False):
|
||||
if not model:
|
||||
return None
|
||||
if offload_to_cpu:
|
||||
saved_offload = model.offload_device
|
||||
model.offload_device = torch.device("cpu")
|
||||
log.info(
|
||||
"Patching model, model.load_device=%s model.model.device=%s cpu_offload=%s",
|
||||
model.load_device,
|
||||
model.model.device,
|
||||
model.offload_device == torch.device("cpu"),
|
||||
)
|
||||
if orig:
|
||||
model.backup = orig.backup
|
||||
model.patch_model(device_to=model.load_device)
|
||||
if offload_to_cpu:
|
||||
model.offload_device = saved_offload
|
||||
if forget:
|
||||
model.patches = {}
|
||||
model.object_patches = {}
|
||||
return model
|
||||
|
||||
|
||||
def get_callback(model):
|
||||
return model.model_options.get("prompt_control_callback")
|
||||
|
||||
|
||||
def set_callback(model, cb):
|
||||
model.model_options["prompt_control_callback"] = cb
|
||||
|
||||
|
||||
# Hack to temporarily override printing to stdout to stop log spam
|
||||
def suppress_print(f):
|
||||
def noop(*args):
|
||||
pass
|
||||
|
||||
p = print
|
||||
__builtins__["print"] = noop
|
||||
rootlogger = logging.getLogger()
|
||||
oldlevel = rootlogger.level
|
||||
try:
|
||||
rootlogger.setLevel(logging.ERROR)
|
||||
x = f()
|
||||
except BaseException:
|
||||
__builtins__["print"] = p
|
||||
rootlogger.setLevel(oldlevel)
|
||||
raise
|
||||
__builtins__["print"] = p
|
||||
rootlogger.setLevel(oldlevel)
|
||||
return x
|
||||
|
||||
|
||||
def lora_name_to_file(name):
|
||||
filenames = folder_paths.get_filename_list("loras")
|
||||
# Return exact matches as is
|
||||
@@ -231,121 +89,3 @@ def lora_name_to_file(name):
|
||||
if p.name == n or str(p) == n:
|
||||
return f
|
||||
return None
|
||||
|
||||
|
||||
def load_lbw():
|
||||
return nodes.NODE_CLASS_MAPPINGS.get("LoraLoaderBlockWeight //Inspire")
|
||||
|
||||
|
||||
def make_loader(filename, lbw):
|
||||
if not lbw:
|
||||
l = nodes.LoraLoader()
|
||||
|
||||
def loader(model, clip, model_weight, clip_weight, lbw):
|
||||
return suppress_print(lambda: l.load_lora(model, clip, filename, model_weight, clip_weight))
|
||||
|
||||
else:
|
||||
# This is already checked before calling make_loader
|
||||
l = load_lbw()()
|
||||
|
||||
def loader(model, clip, model_weight, clip_weight, lbw):
|
||||
spec = lbw["LBW"]
|
||||
lbw_a = safe_float(lbw.get("A"), 4.0)
|
||||
lbw_b = safe_float(lbw.get("B"), 1.0)
|
||||
m = model or DUMMY_MODEL
|
||||
c = clip or DUMMY_MODEL
|
||||
m, c, _ = suppress_print(
|
||||
lambda: l.doit(m, c, filename, model_weight, clip_weight, False, 0, lbw_a, lbw_b, "", spec)
|
||||
)
|
||||
if m is DUMMY_MODEL:
|
||||
m = None
|
||||
if c is DUMMY_MODEL:
|
||||
c = None
|
||||
return m, c
|
||||
|
||||
return loader
|
||||
|
||||
|
||||
def apply_loras_from_spec(
|
||||
loraspec, model=None, clip=None, orig_model=None, orig_clip=None, patch=False, cache=None, applied_loras=None
|
||||
):
|
||||
if applied_loras is None:
|
||||
applied_loras = {}
|
||||
actual_loraspec = {}
|
||||
additive = True
|
||||
for key in loraspec:
|
||||
if key in applied_loras and applied_loras[key] == loraspec[key]:
|
||||
continue
|
||||
if key in applied_loras and applied_loras[key] != loraspec[key]:
|
||||
additive = False
|
||||
actual_loraspec[key] = loraspec[key]
|
||||
|
||||
for key in applied_loras:
|
||||
if key not in loraspec:
|
||||
actual_loraspec = loraspec
|
||||
additive = False
|
||||
|
||||
backup_model = model
|
||||
if not additive:
|
||||
unpatch_model(model)
|
||||
# Reset clip to unpatched
|
||||
if clip:
|
||||
clip = orig_clip or clip
|
||||
|
||||
if cache is None:
|
||||
cache = {}
|
||||
if not loraspec:
|
||||
return model, clip
|
||||
|
||||
for name, params in actual_loraspec.items():
|
||||
m, c = model, clip
|
||||
w, w_clip = params["weight"], params["weight_clip"]
|
||||
if w == 0:
|
||||
m = None
|
||||
if w_clip == 0:
|
||||
c = None
|
||||
if not w and not c:
|
||||
continue
|
||||
|
||||
lbw = params.get("lbw")
|
||||
if lbw and not load_lbw():
|
||||
log.warning("LoraBlockWeight not available, ignoring LBW parameters")
|
||||
lbw = None
|
||||
|
||||
# Cache the loader instance so that it doesn't reload the LoRA from disk all the time
|
||||
cache_key = name, bool(lbw)
|
||||
loader = cache.get(cache_key)
|
||||
if not loader:
|
||||
f = lora_name_to_file(name)
|
||||
if not f:
|
||||
log.warning("Lora %s not found", name)
|
||||
continue
|
||||
log.info("Loading LoRA: %s", f)
|
||||
loader = make_loader(f, bool(lbw))
|
||||
cache[cache_key] = loader
|
||||
|
||||
m, c = loader(m, c, w, w_clip, lbw)
|
||||
model = m or model
|
||||
clip = c or clip
|
||||
if model:
|
||||
log.info("Applying LoRA: %s:%s, LBW=%s, additive=%s", name, params["weight"], bool(lbw), additive)
|
||||
if clip:
|
||||
log.info("Applying CLIP LoRA: %s:%s, LBW=%s, additive=%s", name, params["weight_clip"], bool(lbw), additive)
|
||||
|
||||
# forget patches so we don't double-patch
|
||||
model = patch_model(model, forget=True, orig=backup_model)
|
||||
return model, clip
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.start = None
|
||||
|
||||
def __enter__(self):
|
||||
self.start = time.time()
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
elapsed = time.time() - self.start
|
||||
if environ.get("PC_SHOW_TIMINGS"):
|
||||
log.info("Executed %s in %s seconds", self.name, elapsed)
|
||||
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
[project]
|
||||
name = "comfyui-prompt-control"
|
||||
description = "Nodes for convenient prompt editing, making many common operations prompt-controllable"
|
||||
name = "comfyui-prompt-control-legacy"
|
||||
description = "Legacy prompt control nodes. These exist only to allow old workflows to run"
|
||||
version = "1.2.1"
|
||||
license = { file = "LICENSE" }
|
||||
# some lark versions older than 1.1.9 apparently have a bug that breaks things, see https://github.com/asagi4/comfyui-prompt-control/issues/35
|
||||
dependencies = ["lark >= 1.1.9"]
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/asagi4/comfyui-prompt-control"
|
||||
Repository = "https://github.com/asagi4/comfyui-prompt-control-legacy"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "asagi4"
|
||||
DisplayName = "ComfyUI Prompt Control"
|
||||
DisplayName = "ComfyUI Prompt Control (LEGACY VERSION)"
|
||||
Icon = ""
|
||||
|
||||
Reference in New Issue
Block a user