diff --git a/README.md b/README.md
index cfa8e86..40f33fc 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,10 @@ See the [syntax documentation](docs/SYNTAX.md).
See the [cookbook](docs/COOKBOOK.md) for interesting usages.
+## Tools
+
+A tool `tools/convert_styles.py` exists to convert A1111 or SD.Next styles into a wildcards file.
+
## Contributing
To develop, I suggest doing so with the extension isolated from the UI (you can use a symlink to test it in the UI), and with its own virtual environment (venv or .venv), so the tests work and can be debugged properly.
diff --git a/docs/SYNTAX.md b/docs/SYNTAX.md
index 0fd58ff..fcf71d8 100644
--- a/docs/SYNTAX.md
+++ b/docs/SYNTAX.md
@@ -88,7 +88,7 @@ The parameters, the filter, and the setting of a variable are optional. The para
### Identifier
* Allowed characters are letters, numbers, underscore (`_`), dash (`-`), dot (`.`), and the path separators (`/` and `\`). It cannot start with an underscore because it would be ambiguous whether it's part of the name or just precedes the wildcard.
-* Can have a relative path and contain globbing formatting, to read multiple wildcards and merge their choices. Note that if there are no parameters specified, the globbing will use the ones from the first wildcard that matches and have parameters (sorted by keys), so if you don't want that you might want to specify them. Also note that, unlike with *Dynamic Prompts*, the wildcard name has to be specified with its full path (unless you use globbing).
+* Can have a relative path and contain globbing formatting, to read multiple wildcards and merge their choices. Note that if there are no parameters specified, the globbing will use the ones from the first wildcard that matches and have parameters (sorted by keys), so if you don't want that you might want to specify them. Also note that, unlike with *Dynamic Prompts*, the wildcard name has to be specified with its full path (unless you use globbing). Globbing cannot be at the start (you can't do, for example, `*/colors`).
* You can use variables, with the `${name}`, `${name:default}`, `` or `defaultecho>` formats, to build a dynamic identifier.
### Filter
@@ -196,7 +196,7 @@ The format is:
| `` | Sets a filter |
| `` | Removes the filter |
-The wildcard identifier supports globbing. The filter does not allow the `^` or `#` flags.
+The wildcard identifier supports globbing (but not at the start). The filter does not allow the `^` or `#` flags.
## Variables
diff --git a/ppp.code-workspace b/ppp.code-workspace
index c35c31d..8b8348c 100644
--- a/ppp.code-workspace
+++ b/ppp.code-workspace
@@ -54,6 +54,13 @@
"console": "integratedTerminal",
"justMyCode": false,
"cwd": "${workspaceFolder}"
+ },
+ {
+ "name": "Python Debugger (Current File)",
+ "type": "debugpy",
+ "request": "launch",
+ "program": "${file}",
+ "args": "${command:pickArgs}"
}
],
"compounds": []
diff --git a/ppp.py b/ppp.py
index 809af61..e86eaf4 100644
--- a/ppp.py
+++ b/ppp.py
@@ -328,6 +328,23 @@ class PromptPostProcessor: # pylint: disable=too-few-public-methods,too-many-in
else:
user_cfg = None
if user_cfg is not None:
+ if not isinstance(user_config_file, dict):
+ default_hosts = set((self.config.hosts or {}).keys())
+ user_hosts = set((user_cfg.hosts or {}).keys())
+ missing_hosts = default_hosts - user_hosts
+ if missing_hosts:
+ self.log(
+ logging.WARNING,
+ f"User configuration is missing host(s) from the default: {', '.join(sorted(missing_hosts))}. Consider updating your configuration file.",
+ )
+ default_models = set((self.config.models or {}).keys())
+ user_models = set((user_cfg.models or {}).keys())
+ missing_models = default_models - user_models
+ if missing_models:
+ self.log(
+ logging.WARNING,
+ f"User configuration is missing model(s) from the default: {', '.join(sorted(missing_models))}. Consider updating your configuration file.",
+ )
self.__merge_configuration(user_cfg)
self.models_config: dict[str, ModelConfig | None] = self.config.models or {}
diff --git a/ppp_classes.py b/ppp_classes.py
index 557162d..3a66ff3 100644
--- a/ppp_classes.py
+++ b/ppp_classes.py
@@ -18,6 +18,7 @@ class SUPPORTED_APPS(Enum):
comfyui = "comfyui"
a1111 = "a1111"
forge = "forge"
+ forgeneo = "forgeneo"
reforge = "reforge"
sdnext = "sdnext"
tests = "tests" # for testing purposes only, not a real app
@@ -26,7 +27,8 @@ class SUPPORTED_APPS(Enum):
SUPPORTED_APPS_NAMES = {
SUPPORTED_APPS.comfyui: "ComfyUI",
SUPPORTED_APPS.sdnext: "SD.Next",
- SUPPORTED_APPS.forge: "Forge",
+ SUPPORTED_APPS.forge: "Forge Classic",
+ SUPPORTED_APPS.forgeneo: "Forge Neo",
SUPPORTED_APPS.reforge: "reForge",
SUPPORTED_APPS.a1111: "A1111 (or compatible)",
SUPPORTED_APPS.tests: "Tests",
diff --git a/ppp_common.py b/ppp_common.py
index e063ccc..e80603d 100644
--- a/ppp_common.py
+++ b/ppp_common.py
@@ -1,10 +1,13 @@
import ast
+import csv
+from functools import reduce
import logging
from pathlib import Path
import re
import textwrap
import time
import lark
+import yaml
from ppp_logging import log
from ppp_classes import ONWARNING_CHOICES, PPPInterrupt, PPPState
@@ -237,3 +240,67 @@ def get_model_class_from_filename(filename: str) -> str:
return config.__class__.__name__ if config else ""
except Exception: # pylint: disable=broad-except
return ""
+
+
+def sanitize_wc_name(name: str) -> str:
+ # Remove invalid characters
+ return re.sub(r"[^a-zA-Z0-9-_]+", "", re.sub(r"_{2,}", "_", name.replace(" ", "_")))
+
+
+def convert_a1111_styles_to_wildcard(inp: Path, out: Path):
+ """
+ Converts styles from A1111 format to wildcard format in a YAML file.
+ Args:
+ inp (Path): The input CSV file path.
+ out (Path): The output YAML file path.
+ """
+ wildcards = {}
+ with open(inp, "r", encoding="utf-8-sig") as f:
+ for reg in csv.reader(f):
+ name = reg[0].strip()
+ if name.lower() == "name":
+ continue
+ positive = reg[1].strip()
+ negative = reg[2].strip()
+ if negative:
+ positive = f"{positive}{negative}"
+ name = sanitize_wc_name(name)
+ wildcards[name] = positive
+ if not wildcards:
+ raise RuntimeError(f"No styles found in {inp} to convert to wildcards.")
+ with open(out, "w", encoding="utf-8-sig") as f:
+ f.write(f"# Original names may contain characters that are replaced in the output.\n# Converted from {inp}\n")
+ yaml.dump(wildcards, f, allow_unicode=True)
+
+
+def convert_sdnext_styles_to_wildcard(inp: Path, out: Path):
+ """
+ Converts styles from SD.Next format to wildcard format in a YAML file.
+ Args:
+ inp (Path): The input folder path containing style json files or a single json file.
+ out (Path): The output YAML file path.
+ """
+ wildcards = {}
+ files = inp.glob("*.json") if inp.is_dir() else [inp]
+ for file in files:
+ with open(file, "r", encoding="utf-8-sig") as f:
+ data = yaml.safe_load(f)
+ if not isinstance(data, list):
+ continue
+ wildcards[file] = {}
+ for style in data:
+ name = style.get("name", "").strip()
+ positive = style.get("prompt", "").strip()
+ negative = style.get("negative", "").strip()
+ # extra = style.get("extra", "").strip()
+ if negative:
+ positive = f"{positive}{negative}"
+ name = sanitize_wc_name(name)
+ wildcards[file][name] = positive
+ if not reduce(lambda acc, d: acc or bool(d), wildcards.values(), False):
+ raise RuntimeError(f"No styles found in {inp} to convert to wildcards.")
+ with open(out, "w", encoding="utf-8-sig") as f:
+ f.write("# Original names may contain characters that are replaced in the output.\n")
+ for name, wcs in wildcards.items():
+ f.write(f"# Converted from {name}\n")
+ yaml.dump(wcs, f, allow_unicode=True)
diff --git a/ppp_config.yaml.defaults b/ppp_config.yaml.defaults
index 113ee32..5f598a3 100644
--- a/ppp_config.yaml.defaults
+++ b/ppp_config.yaml.defaults
@@ -50,6 +50,12 @@ hosts:
alternation: ok
and: ok
break: ok
+ forgeneo:
+ attention: ok
+ scheduling: ok
+ alternation: ok
+ and: ok
+ break: ok
reforge:
attention: ok
scheduling: ok
@@ -74,10 +80,11 @@ hosts:
# Supported base models, variants, and options
# Check supported models for each host in:
# A1111:
-# Forge: repositories\huggingface_guess\huggingface_guess\model_list.py
-# reForge:
-# SD.Next:
-# ComfyUI:
+# Forge Classic: repositories\huggingface_guess\huggingface_guess\model_list.py
+# Forge Neo: modules_forge\packages\huggingface_guess\model_list.py
+# reForge: ldm_patched\modules\supported_models.py
+# SD.Next: modules\shared_items.py
+# ComfyUI: ComfyUI\comfy\supported_models.py
models:
# We define supported models and how we detect them in each host application (by class or by a known boolean property, or null for not supported).
# We can also define here variants and some options.
@@ -86,6 +93,7 @@ models:
detect:
a1111: { property: "is_sd1" }
forge: { property: "is_sd1" }
+ forgeneo: { property: "is_sd1" }
reforge: { property: "is_sd1" }
sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend; cannot differentiate SD1 and SD2, we set both to True
comfyui: { class: ["SD15", "SD15_instructpix2pix"] }
@@ -93,6 +101,7 @@ models:
detect:
a1111: { property: "is_sd2" }
forge: { property: "is_sd2" }
+ forgeneo: null
reforge: { property: "is_sd2" }
sdnext: { class: ["LatentDiffusion", "StableDiffusionPipeline"] } # cannot differentiate SD1 and SD2, we set both to True; LatentDiffusion is for the original backend, StableDiffusionPipeline is for the diffusers backend
comfyui: { class: ["SD20", "SD21UnclipL", "SD21UnclipH", "LotusD"] }
@@ -100,6 +109,7 @@ models:
detect:
a1111: { property: "is_ssd" }
forge: null
+ forgeneo: null
reforge: { property: "is_ssd" }
sdnext: null
comfyui: { class: ["SSD1B"]}
@@ -107,6 +117,7 @@ models:
detect:
a1111: { property: "is_sdxl" }
forge: { property: "is_sdxl" }
+ forgeneo: { property: "is_sdxl" }
reforge: { property: "is_sdxl" }
sdnext: { class: ["StableDiffusionXLPipeline"] }
comfyui: { class: ["SDXL", "SDXLRefiner", "SDXL_instructpix2pix", "Segmind_Vega", "KOALA_700M", "KOALA_1B"] }
@@ -118,10 +129,12 @@ models:
# If specified as a list, it should be a list of dicts or strings
pony: { find_in_filename: "(?:pony|pny|pdxl)" }
illustrious: { find_in_filename: { regex: "(?:illustrious|illust|ilxl)", flags: ["IGNORECASE"] } }
+ noobai: { find_in_filename: "(?:noob|nai)" }
sd3: # Stable Diffusion 3
detect:
a1111: { property: "is_sd3" }
forge: { property: "is_sd3", class: ["SD3"] }
+ forgeneo: null
reforge: { property: "is_sd3" }
sdnext: { class: ["StableDiffusion3Pipeline"] }
comfyui: { class: ["SD3"] }
@@ -129,13 +142,15 @@ models:
detect:
a1111: null
forge: { class: ["Flux", "FluxSchnell"] }
+ forgeneo: { class: ["Flux", "FluxSchnell"] }
reforge: { class: ["Flux", "FluxInpaint", "FluxSchnell"] }
sdnext: { class: ["FluxPipeline"] }
comfyui: { class: ["Flux", "FluxInpaint", "FluxSchnell"] }
auraflow: # AuraFlow
detect:
a1111: null
- forge: null
+ forge: { class: ["AuraFlow"] }
+ forgeneo: null
reforge: { class: ["AuraFlow"] }
sdnext: { class: ["AuraFlowPipeline"] }
comfyui: { class: ["AuraFlow"] }
@@ -143,20 +158,31 @@ models:
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["PixArtAlpha", "PixArtSigma"] }
- sdnext: { class: ["PixArtAlphaPipeline"] }
+ sdnext: { class: ["PixArtAlphaPipeline", "PixArtSigmaPipeline"] }
comfyui: { class: ["PixArtAlpha", "PixArtSigma"] }
- lumina2: # Lumina2 & ZImage
+ lumina2: # Lumina2
detect:
a1111: null
forge: null
- reforge: { class: ["Lumina2", "ZImage"] }
- sdnext: null
- comfyui: { class: ["Lumina2", "ZImage"] }
+ forgeneo: { class: ["Lumina2"] }
+ reforge: { class: ["Lumina2"] }
+ sdnext: { class: ["Lumina2Pipeline"] }
+ comfyui: { class: ["Lumina2"] }
+ zimage: # ZImage
+ detect:
+ a1111: null
+ forge: null
+ forgeneo: { class: ["ZImage"] }
+ reforge: { class: ["ZImage"] }
+ sdnext: { class: ["ZImagePipeline"] }
+ comfyui: { class: ["ZImage"] }
ltxv: # LTXV
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: null
sdnext: null
comfyui: { class: ["LTXV", "LTXAV"] }
@@ -164,13 +190,15 @@ models:
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["CosmosT2V", "CosmosI2V"] }
- sdnext: null
+ sdnext: { class: ["Cosmos2TextToImagePipeline"] }
comfyui: { class: ["CosmosT2V", "CosmosI2V"] }
cosmospredict2: # CosmosPredict2
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["CosmosT2IPredict2", "CosmosI2VPredict2"] }
sdnext: null
comfyui: { class: ["CosmosT2IPredict2", "CosmosI2VPredict2"] }
@@ -178,20 +206,23 @@ models:
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["GenmoMochi"] }
sdnext: null
comfyui: { class: ["GenmoMochi"] }
hunyuan: # Hunyuan
detect:
a1111: null
- forge: null
+ forge: { class: ["HunyuanDiT", "HunyuanDiT1"] }
+ forgeneo: null
reforge: { class: ["HunyuanDiT", "HunyuanDiT1"] }
- sdnext: null
+ sdnext: { class: ["HunyuanDiTPipeline", "HunyuanImagePipeline"] }
comfyui: { class: ["HunyuanDiT", "HunyuanDiT1"] }
hunyuanvideo: # HunyuanVideo
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["HunyuanVideo", "HunyuanVideoI2V", "HunyuanVideoSkyreelsI2V", "HunyuanImage21", "HunyuanImage21Refiner", "HunyuanVideo15", "HunyuanVideo15_SR_Distilled"] }
sdnext: null
comfyui: { class: ["HunyuanVideo", "HunyuanVideoI2V", "HunyuanVideoSkyreelsI2V", "HunyuanImage21", "HunyuanImage21Refiner", "HunyuanVideo15", "HunyuanVideo15_SR_Distilled"] }
@@ -199,6 +230,7 @@ models:
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"] }
sdnext: null
comfyui: { class: ["Hunyuan3Dv2", "Hunyuan3Dv2_1", "Hunyuan3Dv2mini"] }
@@ -206,55 +238,95 @@ models:
detect:
a1111: null
forge: { class: ["WAN21_T2V", "WAN21_I2V"] }
+ forgeneo: { class: ["WAN21_T2V", "WAN21_I2V"] }
reforge: { class: ["WAN21_T2V", "WAN21_I2V", "WAN21_FunControl2V", "WAN21_Camera", "WAN22_Camera", "WAN21_Vace", "WAN21_HuMo", "WAN22_S2V", "WAN22_Animate", "WAN22_T2V"] }
- sdnext: null
- comfyui: { class: ["WAN21_T2V", "WAN21_I2V", "WAN21_FunControl2V", "WAN21_Camera", "WAN22_Camera", "WAN21_Vace", "WAN21_HuMo", "WAN22_S2V", "WAN22_Animate", "WAN22_T2V"] }
+ sdnext: { class: ["WanPipeline"] }
+ comfyui: { class: ["WAN21_T2V", "WAN21_I2V", "WAN21_FunControl2V", "WAN21_Camera", "WAN22_Camera", "WAN21_Vace", "WAN21_HuMo", "WAN22_S2V", "WAN22_Animate", "WAN22_T2V", "WAN21_FlowRVS", "WAN21_SCAIL", "WAN22_WanDancer"] }
hidream: # HiDream
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["HiDream"] }
- sdnext: null
- comfyui: { class: ["HiDream"] }
+ sdnext: { class: ["HiDreamImagePipeline"] }
+ comfyui: { class: ["HiDream", "HiDreamO1"] }
qwenimage: # QwenImage & QwenImageEdit
detect:
a1111: null
- forge: { class: ["QwenImage"] }
+ forge: null
+ forgeneo: { class: ["QwenImage"] }
reforge: null
- sdnext: null
+ sdnext: { class: ["QwenImagePipeline"] }
comfyui: { class: ["QwenImage"] }
chroma: # Chroma
detect:
a1111: null
- forge: { class: ["Chroma"] }
+ forge: null
+ forgeneo: { class: ["Chroma"] }
reforge: { class: ["Chroma", "ChromaRadiance"] }
- sdnext: null
+ sdnext: { class: ["ChromaPipeline"] }
comfyui: { class: ["Chroma", "ChromaRadiance"] }
omnigen2: # Omnigen 2
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: { class: ["Omnigen2"] }
- sdnext: null
+ sdnext: { class: ["OmniGen2Pipeline"] }
comfyui: { class: ["Omnigen2"] }
flux2: # Flux 2
detect:
a1111: null
forge: null
+ forgeneo: { class: ["Flux2K4B", "Flux2K9B"] }
reforge: null
- sdnext: null
- comfyui: { class: ["Flux2"] }
+ sdnext: { class: ["Flux2Pipeline", "Flux2KleinPipeline"] }
+ comfyui: { class: ["Flux2", "Flux2K4B", "Flux2K9B"] }
kandinsky5: # Kandinsky 5
detect:
a1111: null
forge: null
+ forgeneo: null
reforge: null
- sdnext: null
+ sdnext: { class: ["Kandinsky5T2IPipeline"] }
comfyui: { class: ["Kandinsky5", "Kandinsky5Image"] }
anima: # Anima
detect:
a1111: null
forge: null
+ forgeneo: { class: ["Anima"] }
+ reforge: null
+ sdnext: { class: ["AnimaText2ImagePipeline"] }
+ comfyui: { class: ["Anima"] }
+ longcatimage: # LongCatImage
+ detect:
+ a1111: null
+ forge: null
+ forgeneo: null
+ reforge: null
+ sdnext: { class: ["LongCatImagePipeline"] }
+ comfyui: { class: ["LongCatImage"] }
+ ernieimage: # ErnieImage
+ detect:
+ a1111: null
+ forge: null
+ forgeneo: { class: ["ErnieImage"] }
+ reforge: null
+ sdnext: { class: ["ErnieImagePipeline"] }
+ comfyui: { class: ["ErnieImage"] }
+ mugen: # Mugen
+ detect:
+ a1111: null
+ forge: null
+ forgeneo: { class: ["Mugen"] }
reforge: null
sdnext: null
- comfyui: { class: ["Anima"] }
+ comfyui: { class: ["Mugen"] }
+ cogvideox: # CogVideoX
+ detect:
+ a1111: null
+ forge: null
+ forgeneo: null
+ reforge: null
+ sdnext: null
+ comfyui: { class: ["CogVideoX_T2V", "CogVideoX_I2V", "CogVideoX_Inpaint"] }
diff --git a/scripts/ppp_script.py b/scripts/ppp_script.py
index afbf2a1..6eb7223 100644
--- a/scripts/ppp_script.py
+++ b/scripts/ppp_script.py
@@ -197,19 +197,16 @@ class PromptPostProcessorA1111Script(scripts.Script):
Returns:
None
"""
- app = (
- SUPPORTED_APPS.forge
- if hasattr(p.sd_model, "model_config")
- else (
- SUPPORTED_APPS.reforge
- if hasattr(p.sd_model, "forge_objects")
- else (
- SUPPORTED_APPS.sdnext
- if hasattr(p.sd_model, "is_sdxl") and not hasattr(p.sd_model, "is_ssd")
- else SUPPORTED_APPS.a1111
- )
- )
- )
+ app = SUPPORTED_APPS.a1111
+ if hasattr(p.sd_model, "model_config"):
+ app = SUPPORTED_APPS.forge
+ if not hasattr(p.sd_model, "is_sd2"):
+ app = SUPPORTED_APPS.forgeneo
+ elif hasattr(p.sd_model, "forge_objects"):
+ app = SUPPORTED_APPS.reforge
+ elif hasattr(p.sd_model, "is_sdxl") and not hasattr(p.sd_model, "is_ssd"):
+ app = SUPPORTED_APPS.sdnext
+
num_seeds = len(getattr(p, "all_seeds", []))
options = PPPStateOptions(
debug_level=DEBUG_LEVEL(getattr(opts, "ppp_gen_debug_level", PromptPostProcessor.DEFAULT_DEBUG_LEVEL)),
@@ -303,11 +300,13 @@ class PromptPostProcessorA1111Script(scripts.Script):
"app": app.value,
"models_path": models_path,
"model_filename": getattr(p.sd_model.sd_checkpoint_info, "filename", ""),
- "model_class": p.sd_model.__class__.__name__,
+ "model_class": (
+ p.sd_model.model_config.__class__.__name__
+ if app in (SUPPORTED_APPS.forge, SUPPORTED_APPS.forgeneo)
+ else p.sd_model.__class__.__name__
+ ),
"property_base": p.sd_model,
}
- if app == SUPPORTED_APPS.forge:
- env_info["model_class"] = p.sd_model.model_config.__class__.__name__
wc_wildcards_folders = getattr(opts, "ppp_wil_wildcardsfolders", "")
if wc_wildcards_folders == "":
wc_wildcards_folders = os.getenv("WILDCARD_DIR", PPPWildcards.DEFAULT_WILDCARDS_FOLDER)
diff --git a/tools/convert_styles.py b/tools/convert_styles.py
new file mode 100644
index 0000000..94f22df
--- /dev/null
+++ b/tools/convert_styles.py
@@ -0,0 +1,89 @@
+#!/usr/bin/env python3
+
+"""
+Converts style files (A1111 CSV or SD.Next JSON) to a PPP-compatible YAML wildcard file.
+
+Usage:
+ python convert_styles.py [--format {a1111,sdnext}]