+
Syntax cheatsheet
+
Tutorial
+
Discussions
+
Report a bug
+
+
+
Combinations
+
+ Choose a number of terms from a list, in this case we choose two artists:
+
{2$$$$artist1|artist2|artist3}
+
+ If $$$$ is not provided, then 1$$$$ is assumed.
+
+ If the chosen number of terms is greater than the available terms, then some terms will be duplicated, otherwise chosen terms will be unique. This is useful in the case of wildcards, e.g.
+
{2$$$$__artist__}
is equivalent to
{2$$$$__artist__|__artist__}
+
+ A range can be provided:
+
{1-3$$$$artist1|artist2|artist3}
+ In this case, a random number of artists between 1 and 3 is chosen.
+
+ Options can be given weights:
+
{2::artist1|artist2}
+ In this case, artist1 will be chosen twice as often as artist2.
+
+ Wildcards can be used and the joiner can also be specified:
+
{{1-$$$$and$$$$__adjective__}}
+
+ Here, a random number between 1 and 3 words from adjective.txt will be chosen and joined together with the word 'and' instead of the default comma.
+
+
+
+
Wildcards
+ Find and manage wildcards in the Wildcards Manager tab.
+
+
+
+ You can add more wildcards by creating a text file with one term per line and name is mywildcards.txt. Place it in ${WILDCARD_DIR}.
__<folder>/mywildcards__
will then become available.
+
+
Variables
+ Set a variable like so:
+
$${season=!{summer|autumn|winter|spring}}
+
+ Now use it like this:
+
In $${season} I wear a $${season} shirt and $${season} trousers
+
+ For more details, and functionality, see the documentation (coming soon)
+
+ Find more settings on the
Settings tab
+
+ You are using
version ${VERSION} of the WebUI extension, and the underlying
dynamicprompts library is version ${LIB_VERSION}.
+
diff --git a/ex/dynamic-prompts/images/combinatorial_generation.png b/ex/dynamic-prompts/images/combinatorial_generation.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ae832c901e9fe880d5303990cdf21bda6d46d1e
Binary files /dev/null and b/ex/dynamic-prompts/images/combinatorial_generation.png differ
diff --git a/ex/dynamic-prompts/images/config_brackets.png b/ex/dynamic-prompts/images/config_brackets.png
new file mode 100644
index 0000000000000000000000000000000000000000..4fccaf5f774af4a6c7b25e91e51932bb9c6f54e5
Binary files /dev/null and b/ex/dynamic-prompts/images/config_brackets.png differ
diff --git a/ex/dynamic-prompts/images/emphasis.png b/ex/dynamic-prompts/images/emphasis.png
new file mode 100644
index 0000000000000000000000000000000000000000..2984a630827c5e3f3666fe2254bc70446c366e45
Binary files /dev/null and b/ex/dynamic-prompts/images/emphasis.png differ
diff --git a/ex/dynamic-prompts/images/extension.png b/ex/dynamic-prompts/images/extension.png
new file mode 100644
index 0000000000000000000000000000000000000000..026aa4456654282250c36f31380c3d0c047559c4
Binary files /dev/null and b/ex/dynamic-prompts/images/extension.png differ
diff --git a/ex/dynamic-prompts/images/feeling-lucky.png b/ex/dynamic-prompts/images/feeling-lucky.png
new file mode 100644
index 0000000000000000000000000000000000000000..84bf9fe8b263e6931f59c052682a4ee1c7cfe947
Binary files /dev/null and b/ex/dynamic-prompts/images/feeling-lucky.png differ
diff --git a/ex/dynamic-prompts/images/filmtypes.jpg b/ex/dynamic-prompts/images/filmtypes.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..08c7dc2b0404572fa21dd194b838c619fb19bd3b
Binary files /dev/null and b/ex/dynamic-prompts/images/filmtypes.jpg differ
diff --git a/ex/dynamic-prompts/images/icon-changelog.png b/ex/dynamic-prompts/images/icon-changelog.png
new file mode 100644
index 0000000000000000000000000000000000000000..36343fd99eace2a0d15e7c2e54128b9774d04c51
Binary files /dev/null and b/ex/dynamic-prompts/images/icon-changelog.png differ
diff --git a/ex/dynamic-prompts/images/icon-syntax.png b/ex/dynamic-prompts/images/icon-syntax.png
new file mode 100644
index 0000000000000000000000000000000000000000..bf072b90eb98daaeab0f71812f0b0ca508500589
Binary files /dev/null and b/ex/dynamic-prompts/images/icon-syntax.png differ
diff --git a/ex/dynamic-prompts/images/icon-tutorial.png b/ex/dynamic-prompts/images/icon-tutorial.png
new file mode 100644
index 0000000000000000000000000000000000000000..d925b08cfbf01044ecc20adeecb2b3b57e657923
Binary files /dev/null and b/ex/dynamic-prompts/images/icon-tutorial.png differ
diff --git a/ex/dynamic-prompts/images/installation.png b/ex/dynamic-prompts/images/installation.png
new file mode 100644
index 0000000000000000000000000000000000000000..d88153586d2144a931538267e1b2d6f4e3b868f0
Binary files /dev/null and b/ex/dynamic-prompts/images/installation.png differ
diff --git a/ex/dynamic-prompts/images/jinja_templates.png b/ex/dynamic-prompts/images/jinja_templates.png
new file mode 100644
index 0000000000000000000000000000000000000000..e038dcb868973bba3ab16c022ce5433ec53d84c8
Binary files /dev/null and b/ex/dynamic-prompts/images/jinja_templates.png differ
diff --git a/ex/dynamic-prompts/images/magic_prompt.png b/ex/dynamic-prompts/images/magic_prompt.png
new file mode 100644
index 0000000000000000000000000000000000000000..88aa9d71c46c5cc3bbb9b8875c0196d503a98511
Binary files /dev/null and b/ex/dynamic-prompts/images/magic_prompt.png differ
diff --git a/ex/dynamic-prompts/images/prompt_editing.jpg b/ex/dynamic-prompts/images/prompt_editing.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d51639006c267f3a1fc39797ce1a4683dc67a41f
Binary files /dev/null and b/ex/dynamic-prompts/images/prompt_editing.jpg differ
diff --git a/ex/dynamic-prompts/images/tutorial/artist_wildcards.png b/ex/dynamic-prompts/images/tutorial/artist_wildcards.png
new file mode 100644
index 0000000000000000000000000000000000000000..9cef0edd5123d83fd6dc72d288b104bde2ba7fab
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/artist_wildcards.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/attention-grabber.png b/ex/dynamic-prompts/images/tutorial/attention-grabber.png
new file mode 100644
index 0000000000000000000000000000000000000000..28d130942546b4d9fe2355c08fb848e9bcf6e4e5
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/attention-grabber.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/attention.png b/ex/dynamic-prompts/images/tutorial/attention.png
new file mode 100644
index 0000000000000000000000000000000000000000..b666cab47fa69a2f110bcef7561cab972e99aa45
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/attention.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/combinatorial.png b/ex/dynamic-prompts/images/tutorial/combinatorial.png
new file mode 100644
index 0000000000000000000000000000000000000000..22b8bcea06423bed2edad96410892f7364ed6959
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/combinatorial.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/install.png b/ex/dynamic-prompts/images/tutorial/install.png
new file mode 100644
index 0000000000000000000000000000000000000000..ea3a134cce265106081a6419d918d8b6201dd2ad
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/install.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/lucky-checkbox.png b/ex/dynamic-prompts/images/tutorial/lucky-checkbox.png
new file mode 100644
index 0000000000000000000000000000000000000000..b5673e6c3436cd809352d8e4c15fcb010ff3b5f6
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/lucky-checkbox.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/lucky.png b/ex/dynamic-prompts/images/tutorial/lucky.png
new file mode 100644
index 0000000000000000000000000000000000000000..a94fc006c4d57c77ed00043d553891172f0f7ead
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/lucky.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/magicprompt-mechwarrior.png b/ex/dynamic-prompts/images/tutorial/magicprompt-mechwarrior.png
new file mode 100644
index 0000000000000000000000000000000000000000..3d4ac2afc5abb0944437ae23e197e1188bb2e49e
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/magicprompt-mechwarrior.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/magicprompts.png b/ex/dynamic-prompts/images/tutorial/magicprompts.png
new file mode 100644
index 0000000000000000000000000000000000000000..5260679c666b096b633ce98ae4b7538b7e141ae3
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/magicprompts.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/mech.png b/ex/dynamic-prompts/images/tutorial/mech.png
new file mode 100644
index 0000000000000000000000000000000000000000..6ccc91b6d63293a65711fb209d555e5a9226a953
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/mech.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/platinum.png b/ex/dynamic-prompts/images/tutorial/platinum.png
new file mode 100644
index 0000000000000000000000000000000000000000..f3ef0d42712830cbf4e4b31e283f82a5b5e07950
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/platinum.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt1.png b/ex/dynamic-prompts/images/tutorial/prompt1.png
new file mode 100644
index 0000000000000000000000000000000000000000..2873633e317d87d80f9d2698f9cc84564385e2aa
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt1.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt2.png b/ex/dynamic-prompts/images/tutorial/prompt2.png
new file mode 100644
index 0000000000000000000000000000000000000000..b6815351415cfbdc726ca93acb92547a45448255
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt2.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt3.png b/ex/dynamic-prompts/images/tutorial/prompt3.png
new file mode 100644
index 0000000000000000000000000000000000000000..51d603c3c0bd1230583350dcfed083b85f2715c9
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt3.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt4a.png b/ex/dynamic-prompts/images/tutorial/prompt4a.png
new file mode 100644
index 0000000000000000000000000000000000000000..af2d32471ec665d033a4a7138f641df1cbf0ed10
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt4a.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt4b.png b/ex/dynamic-prompts/images/tutorial/prompt4b.png
new file mode 100644
index 0000000000000000000000000000000000000000..07c4399fe0c6d65d8a37ae428987535016491421
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt4b.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/prompt4c.png b/ex/dynamic-prompts/images/tutorial/prompt4c.png
new file mode 100644
index 0000000000000000000000000000000000000000..b735299c10016c14df650135259de3286a8128c5
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/prompt4c.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/surfer.png b/ex/dynamic-prompts/images/tutorial/surfer.png
new file mode 100644
index 0000000000000000000000000000000000000000..afbc6408632736993876e785976f90799db9d92f
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/surfer.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/ui-closed.png b/ex/dynamic-prompts/images/tutorial/ui-closed.png
new file mode 100644
index 0000000000000000000000000000000000000000..37eac659f8720328531718d40f5a1ccfa7aae3d5
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/ui-closed.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/ui-open.png b/ex/dynamic-prompts/images/tutorial/ui-open.png
new file mode 100644
index 0000000000000000000000000000000000000000..ebfa88b25f0b2189149d5fc5d939fd80255e6df8
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/ui-open.png differ
diff --git a/ex/dynamic-prompts/images/tutorial/wildcard_manager.png b/ex/dynamic-prompts/images/tutorial/wildcard_manager.png
new file mode 100644
index 0000000000000000000000000000000000000000..56c6567adf15b351509b0119dd677bbcaa1f176f
Binary files /dev/null and b/ex/dynamic-prompts/images/tutorial/wildcard_manager.png differ
diff --git a/ex/dynamic-prompts/images/weighting-colours.png b/ex/dynamic-prompts/images/weighting-colours.png
new file mode 100644
index 0000000000000000000000000000000000000000..fd29a81696efdab82dd83e7aca5b21f86243c26c
--- /dev/null
+++ b/ex/dynamic-prompts/images/weighting-colours.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0c3e8ab9648f835894754313b3bba1e5b81d00e71617e9fd3b6012c23e31d5d6
+size 9968868
diff --git a/ex/dynamic-prompts/images/weighting-us-population.png b/ex/dynamic-prompts/images/weighting-us-population.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ad8258588b68c7713e894af37b99476ca9a9f23
--- /dev/null
+++ b/ex/dynamic-prompts/images/weighting-us-population.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5aeb00eb861db98c91700cfe36a3333c9d0b91d1c282224753d560a302c93eb1
+size 8697265
diff --git a/ex/dynamic-prompts/images/write_prompts.png b/ex/dynamic-prompts/images/write_prompts.png
new file mode 100644
index 0000000000000000000000000000000000000000..a8452c88e3f0090a2e0fcc20effa277a2cc288fa
Binary files /dev/null and b/ex/dynamic-prompts/images/write_prompts.png differ
diff --git a/ex/dynamic-prompts/install.py b/ex/dynamic-prompts/install.py
new file mode 100644
index 0000000000000000000000000000000000000000..3f805589a371fa37f423fc5632dd163b0a80d778
--- /dev/null
+++ b/ex/dynamic-prompts/install.py
@@ -0,0 +1,28 @@
+import logging
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+def is_empty_line(line):
+ return line is None or line.strip() == "" or line.strip().startswith("#")
+
+
+def check_versions() -> None:
+ requirements = [
+ line
+ for line in (Path(__file__).parent / "requirements.txt")
+ .read_text()
+ .splitlines()
+ if not is_empty_line(line)
+ ]
+ pip_command = f"install {' '.join(requirements)}"
+ try:
+ from launch import run_pip # from AUTOMATIC1111
+
+ run_pip(pip_command, desc="sd-dynamic-prompts requirements.txt")
+ except Exception as e:
+ logger.exception(e)
+
+
+check_versions()
diff --git a/ex/dynamic-prompts/javascript/dynamic_prompting.js b/ex/dynamic-prompts/javascript/dynamic_prompting.js
new file mode 100644
index 0000000000000000000000000000000000000000..1f7599deb21ce1234d4d490ecdddea7fb7d9a103
--- /dev/null
+++ b/ex/dynamic-prompts/javascript/dynamic_prompting.js
@@ -0,0 +1,284 @@
+/* global gradioApp, get_uiCurrentTabContent, onUiUpdate, onUiLoaded */
+// prettier-ignore
+const SDDP_HELP_TEXTS = {
+ "sddp-disable-negative-prompt": "Don't use prompt magic on negative prompts.",
+ "sddp-dynamic-prompts-enabled": "Complete documentation is available at https://github.com/adieyal/sd-dynamic-prompts. Please report any issues on GitHub.",
+ "sddp-is-attention-grabber": "Add emphasis to a randomly selected keyword in the prompt.",
+ "sddp-is-combinatorial": "Generate all possible prompt combinations.",
+ "sddp-is-feelinglucky": "Generate random prompts from lexica.art (your prompt is used as a search query).",
+ "sddp-is-fixed-seed": "Use the same seed for all prompts in this batch",
+ "sddp-is-magicprompt": "Automatically update your prompt with interesting modifiers. (Runs slowly the first time)",
+ "sddp-magic-prompt-model": "Note: Each model will download between 300mb and 1.4gb of data on first use.",
+ "sddp-no-image-generation": "Disable image generation. Useful if you only want to generate text prompts. (1 image will still be generated to keep Auto1111 happy.).",
+ "sddp-unlink-seed-from-prompt": "If this is set, then random prompts are generated, even if the seed is the same.",
+ "sddp-write-prompts": "Write all generated prompts to a file",
+ "sddp-write-raw-template": "Write template into image metadata.",
+};
+
+class SDDPTreeView {
+ /**
+ * @constructor
+ * @property {object} handlers The attached event handlers
+ * @property {object} data The JSON object that represents the tree structure
+ * @property {Element} node The DOM element to render the tree in
+ */
+ constructor(data, node) {
+ this.handlers = {};
+ this.node = node;
+ this.data = data;
+ this.render();
+ }
+
+ /**
+ * Renders the tree view in the DOM
+ */
+ render = () => {
+ const container = this.node;
+ container.innerHTML = "";
+ this.data.forEach((item) => container.appendChild(this.renderNode(item)));
+ [...container.querySelectorAll(".tree-leaf-text,.tree-expando")].forEach(
+ (node) => node.addEventListener("click", this.handleClickEvent),
+ );
+ };
+
+ renderNode = (item) => {
+ const leaf = document.createElement("div");
+ const content = document.createElement("div");
+ const text = document.createElement("div");
+ const expando = document.createElement("div");
+ leaf.setAttribute("class", "tree-leaf");
+ content.setAttribute("class", "tree-leaf-content");
+ text.setAttribute("class", "tree-leaf-text");
+ const { children, name, expanded } = item;
+ text.textContent = name;
+ expando.setAttribute("class", `tree-expando ${expanded ? "expanded" : ""}`);
+ expando.textContent = expanded ? "-" : "+";
+ content.appendChild(expando);
+ content.appendChild(text);
+ leaf.appendChild(content);
+ if (children?.length > 0) {
+ const childrenDiv = document.createElement("div");
+ childrenDiv.setAttribute("class", "tree-child-leaves");
+ children.forEach((child) => {
+ childrenDiv.appendChild(this.renderNode(child));
+ });
+ if (!expanded) {
+ childrenDiv.classList.add("hidden");
+ }
+ leaf.appendChild(childrenDiv);
+ } else {
+ expando.classList.add("hidden");
+ content.setAttribute("data-item", JSON.stringify(item));
+ }
+ return leaf;
+ };
+
+ handleClickEvent = (event) => {
+ const parent = (event.target || event.currentTarget).parentNode;
+ const leaves = parent.parentNode.querySelector(".tree-child-leaves");
+ if (leaves) {
+ this.setSubtreeVisibility(
+ parent,
+ leaves,
+ leaves.classList.contains("hidden"),
+ );
+ } else {
+ this.emit("select", {
+ target: event,
+ data: JSON.parse(parent.getAttribute("data-item")),
+ });
+ }
+ };
+
+ /**
+ * Expands/collapses by the expando or the leaf text
+ * @param {Element} node The parent node that contains the leaves
+ * @param {Element} leaves The leaves wrapper element
+ * @param {boolean} visible Expand or collapse?
+ * @param {boolean} skipEmit Skip emitting the event?
+ */
+ setSubtreeVisibility(node, leaves, visible, skipEmit = false) {
+ leaves.classList.toggle("hidden", !visible);
+ node.querySelector(".tree-expando").textContent = visible ? "+" : "-";
+ if (skipEmit) {
+ return;
+ }
+ this.emit(visible ? "expand" : "collapse", {
+ target: node,
+ leaves,
+ });
+ }
+
+ on(name, callback, context = null) {
+ const handlers = this.handlers[name] || [];
+ handlers.push({ callback, context });
+ this.handlers[name] = handlers;
+ }
+
+ off(name, callback) {
+ this.handlers[name] = (this.handlers[name] || []).filter(
+ (handle) => handle.callback !== callback,
+ );
+ }
+
+ emit(name, ...args) {
+ (this.handlers[name] || []).forEach((handle) => {
+ window.setTimeout(() => {
+ handle.callback.apply(handle.context, args);
+ }, 0);
+ });
+ }
+}
+
+class SDDP_UI {
+ constructor() {
+ this.helpTextsConfigured = false;
+ this.wildcardsLoaded = false;
+ this.messageReadTimer = null;
+ this.lastMessage = null;
+ this.treeView = null;
+ }
+
+ configureHelpTexts() {
+ if (this.helpTextsConfigured) {
+ return;
+ }
+ // eslint-disable-next-line guard-for-in,no-restricted-syntax
+ for (const elemId in SDDP_HELP_TEXTS) {
+ const elem = gradioApp().getElementById(elemId);
+ if (elem) {
+ elem.setAttribute("title", SDDP_HELP_TEXTS[elemId]);
+ } else {
+ return; // Didn't find all elements...
+ }
+ }
+ this.helpTextsConfigured = true;
+ }
+
+ getInboxMessageText() {
+ return gradioApp().querySelector(
+ "#sddp-wildcard-s2c-message-textbox textarea",
+ )?.value;
+ }
+
+ formatPayload(payload) {
+ return JSON.stringify({ ...payload, id: Math.floor(+new Date()) }, null, 2);
+ }
+
+ sendAction(payload) {
+ const outbox = gradioApp().querySelector(
+ "#sddp-wildcard-c2s-message-textbox textarea",
+ );
+ outbox.value = this.formatPayload(payload);
+ // See https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/38b7186e6e3a4dffc93225308b822f0dae43a47d
+ window.updateInput?.(outbox);
+ gradioApp().querySelector("#sddp-wildcard-c2s-action-button").click();
+ }
+
+ requestWildcardTree() {
+ gradioApp().querySelector("#sddp-wildcard-load-tree-button")?.click();
+ }
+
+ doReadMessage() {
+ const messageText = this.getInboxMessageText();
+ if (!messageText || this.lastMessage === messageText) {
+ return;
+ }
+ this.lastMessage = messageText;
+ const message = JSON.parse(messageText);
+ const { action, success } = message;
+ if (action === "load tree" && success) {
+ this.setupTree(message.tree);
+ } else if (action === "load file" && success) {
+ this.loadFileIntoEditor(message);
+ } else {
+ console.warn("SDDP: Unknown message", message);
+ }
+ }
+
+ setupTree(content) {
+ let { treeView } = this;
+ if (!this.treeView) {
+ const treeDiv = gradioApp().querySelector("#sddp-wildcard-tree");
+ if (treeDiv) {
+ treeView = new SDDPTreeView(content, treeDiv);
+ treeView.on("select", this.onSelectNode.bind(this), null);
+ this.treeView = treeView;
+ }
+ } else {
+ treeView.data = content;
+ treeView.render();
+ }
+ }
+
+ onSelectNode(node) {
+ if (node.data?.name) {
+ this.sendAction({
+ action: "load file",
+ name: node.data.name,
+ });
+ }
+ }
+
+ loadFileIntoEditor(message) {
+ const editor = gradioApp().querySelector(
+ "#sddp-wildcard-file-editor textarea",
+ );
+ const name = gradioApp().querySelector("#sddp-wildcard-file-name textarea");
+ const saveButton = gradioApp().querySelector("#sddp-wildcard-save-button");
+ const { contents, wrapped_name: wrappedName, can_edit: canEdit } = message;
+ editor.value = contents;
+ name.value = wrappedName;
+ editor.readOnly = !canEdit;
+ saveButton.disabled = !canEdit;
+
+ // See https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/38b7186e6e3a4dffc93225308b822f0dae43a47d
+ window.updateInput?.(editor);
+ window.updateInput?.(name);
+ }
+
+ onWildcardManagerTabActivate() {
+ if (!this.wildcardsLoaded) {
+ this.requestWildcardTree();
+ this.wildcardsLoaded = true;
+ }
+ if (!this.messageReadTimer) {
+ this.messageReadTimer = setInterval(this.doReadMessage.bind(this), 120);
+ }
+ }
+
+ onDeleteTreeClick() {
+ // eslint-disable-next-line no-restricted-globals,no-alert
+ const sure = confirm("Are you sure you want to delete all your wildcards?");
+ return this.formatPayload({ action: "delete tree", sure });
+ }
+
+ onSaveFileClick() {
+ const json = JSON.parse(this.getInboxMessageText());
+ const contents = gradioApp().querySelector(
+ "#sddp-wildcard-file-editor textarea",
+ ).value;
+ return this.formatPayload({
+ action: "save wildcard",
+ wildcard: json,
+ contents,
+ });
+ }
+}
+
+const SDDP = new SDDP_UI();
+window.SDDP = SDDP;
+
+onUiUpdate(() => {
+ SDDP.configureHelpTexts();
+});
+
+onUiLoaded(() => {
+ // TODO: would be nicer to use `onUiTabChange`, but it may be broken
+ const mutationObserver = new MutationObserver(() => {
+ if (get_uiCurrentTabContent()?.id === "tab_sddp-wildcard-manager") {
+ SDDP.onWildcardManagerTabActivate();
+ }
+ });
+ mutationObserver.observe(gradioApp(), { childList: true, subtree: true });
+});
diff --git a/ex/dynamic-prompts/javascript/dynamic_prompting_hints.js b/ex/dynamic-prompts/javascript/dynamic_prompting_hints.js
new file mode 100644
index 0000000000000000000000000000000000000000..586445ebc6ec446c2279190c3b106273371828dc
--- /dev/null
+++ b/ex/dynamic-prompts/javascript/dynamic_prompting_hints.js
@@ -0,0 +1,61 @@
+/* global titles:true */
+// Mouseover tooltips for various UI elements.
+// `titles` is already defined by A1111, so we just merge into it...
+titles = {
+ ...titles,
+ "Dynamic Prompts enabled": "Disable dynamic prompts by unchecking this box.",
+
+ "Combinatorial generation": `
+Instead of generating random prompts from a template, combinatorial generation produces every possible prompt from the given string.
+The prompt 'I {love|hate} {New York|Chicago} in {June|July|August}' will produce 12 variants in total.
+
+The value of the 'Seed' field is only used for the first image. To change this, look for 'Fixed seed' in the 'Advanced options' section.`.trim(),
+
+ "Max generations (0 = all combinations - the batch count value is ignored)": `
+Limit the maximum number of prompts generated. 0 (default) will generate all images. Useful to prevent an unexpected combinatorial explosion.
+`.trim(),
+
+ "Combinatorial batches": `Re-run your combinatorial batch this many times with a different seed each time.`,
+
+ "Magic prompt": `
+Magic Prompt adds interesting modifiers to your prompt for a little bit of extra spice.
+The first time you use it, the MagicPrompt model is downloaded so be patient.
+If you're running low on VRAM, you might get a CUDA error.`.trim(),
+
+ "Max magic prompt length":
+ "Controls the maximum length in tokens of the generated prompt.",
+ "Magic prompt creativity":
+ "Adjusts the generated prompt. You will need to experiment with this setting.",
+ "Magic Prompt batch size":
+ "The number of prompts to generate per batch. Increasing this can speed up prompt generation at the expense of slightly increased VRAM usage.",
+
+ "I'm feeling lucky": `
+Uses the lexica.art API to create random prompts.
+The prompt in the main prompt box is used as a search string.
+Leaving the prompt box blank returns a list of completely randomly chosen prompts.
+Try it out, it can be quite fun.
+`.trim(),
+
+ "Attention grabber": `Randomly selects a keyword from the prompt and adds emphasis to it. Try this with Fixed Seed enabled.`,
+
+ "Write prompts to file": `
+The generated file is a slugified version of the prompt and can be found in the same directory as the generated images.
+E.g. in ./outputs/txt2img-images/.`.trim(),
+
+ "Don't generate images":
+ "Be sure to check the 'Write prompts to file' checkbox if you don't want to lose the generated prompts. Note, one image is still generated.",
+ "Enable Jinja2 templates":
+ "Jinja2 templates are an expressive alternative to the standard syntax. See the Help section below for instructions.",
+ "Unlink seed from prompt":
+ "Check this if you want to generate random prompts, even if your seed is fixed",
+ "Don't apply to negative prompts":
+ "Don't use prompt magic on negative prompts.",
+
+ "Fixed seed": `
+Select this if you want to use the same seed for every generated image.
+This is useful if you want to test prompt variations while using the same seed.
+If there are no wildcards then all the images will be identical.
+`.trim(),
+ "Write raw prompt to image":
+ "Write the prompt template into the image metadata",
+};
diff --git a/ex/dynamic-prompts/jinja2.md b/ex/dynamic-prompts/jinja2.md
new file mode 100644
index 0000000000000000000000000000000000000000..33ad9b66a1b7f32959b4eed849791fcbda347021
--- /dev/null
+++ b/ex/dynamic-prompts/jinja2.md
@@ -0,0 +1,296 @@
+# Jinja2 templates
+Jinja2 templates is an experimental feature that enables you to write prompts with an expressive templating language. This is an advanced feature and is only recommended for users who are comfortable writing scripts.
+
+To enable the feature, open the advanced accordion and select __Enable Jinja2 templates__.
+
+ Jinja2 templates is an experimental feature for advanced template generation. It is not recommended for general use unless you are comfortable with writing scripts.
+
+
Literals
+
+ I love red roses
+
+
+
Random choices
+
+ I love {{ choice('red', 'blue', 'green') }} roses
+
+ This will randomly choose one of the three colors.
+
+
Iterations
+
+
+ {% for colour in ['red', 'blue', 'green'] %}
+ {% prompt %}I love {{ colour }} roses{% endprompt %}
+ {% endfor %}
+
+
+ This will produce three prompts, one for each color. The prompt tag is used to mark the text that will be used as the prompt. If no prompt tag is present then only one prompt is assumed
+
+
Wildcards
+
+
+ {% for colour in wildcard("__colours__") %}
+ {% prompt %}I love {{ colour }} roses{% endprompt %}
+ {% endfor %}
+
+
+ This will produce one prompt for each colour in the wildcard.txt file.
+
+
Conditionals
+
+
+ {% for colour in ["red", "blue", "green"] %}
+ {% if colour == "red"}
+ {% prompt %}I love {{ colour }} roses{% endprompt %}
+ {% else %}
+ {% prompt %}I hate {{ colour }} roses{% endprompt %}
+ {% endif %}
+ {% endfor %}
+
+
+ This will produce the following prompts:
+
+ - I love red roses
+ - I hate blue roses
+ - I hate green roses
+
+
+ Jinja2 templates are based on the Jinja2 template engine. For more information see the
Jinja2 documentation..
+
+ If you are using these templates, please let me know if they are useful.
+
diff --git a/ex/dynamic-prompts/package.json b/ex/dynamic-prompts/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..90659e2d86ee393142e3b2e5e050fb4039eb8ac1
--- /dev/null
+++ b/ex/dynamic-prompts/package.json
@@ -0,0 +1,16 @@
+{
+ "devDependencies": {
+ "eslint": "^8.2.0",
+ "eslint-config-airbnb-base": "^15.0.0",
+ "eslint-config-prettier": "^8.8.0",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-prettier": "^4.2.1",
+ "prettier": "^2.8.7"
+ },
+ "scripts": {
+ "lint": "eslint . --ext .js"
+ },
+ "prettier": {
+ "trailingComma": "all"
+ }
+}
diff --git a/ex/dynamic-prompts/pyproject.toml b/ex/dynamic-prompts/pyproject.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a09fe19295e3b5e8c806c028e9d5278624c4211b
--- /dev/null
+++ b/ex/dynamic-prompts/pyproject.toml
@@ -0,0 +1,40 @@
+[tool.pytest.ini_options]
+minversion = "7.0"
+pythonpath = [
+ ".",
+]
+addopts = "--ignore=collections"
+
+[tool.ruff]
+target-version = "py310"
+select = [
+ "B",
+ "C",
+ "COM",
+ "E",
+ "F",
+ "I",
+ "UP",
+]
+ignore = [
+ "C901", # Complexity
+ "E501", # Line length
+ "B905",
+]
+unfixable = [
+ "B007", # Loop control variable not used within the loop body
+]
+
+[tool.coverage.run]
+branch = true
+omit = [
+ "install.py"
+]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "raise NotImplementedError",
+ "if TYPE_CHECKING:",
+ "if __name__ == .__main__.:",
+]
diff --git a/ex/dynamic-prompts/requirements.txt b/ex/dynamic-prompts/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..f097eda6b41e355f505a0a8fe85274dd2e9231e6
--- /dev/null
+++ b/ex/dynamic-prompts/requirements.txt
@@ -0,0 +1,2 @@
+send2trash==1.8.0
+dynamicprompts[attentiongrabber,magicprompt]==0.23.0
diff --git a/ex/dynamic-prompts/scripts/dynamic_prompting.py b/ex/dynamic-prompts/scripts/dynamic_prompting.py
new file mode 100644
index 0000000000000000000000000000000000000000..90bf559705d5c095bd703707108d89b4092901c2
--- /dev/null
+++ b/ex/dynamic-prompts/scripts/dynamic_prompting.py
@@ -0,0 +1,5 @@
+# Automatic1111 entry point.
+
+from sd_dynamic_prompts.dynamic_prompting import Script
+
+__all__ = ["Script"]
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/.python-version b/ex/dynamic-prompts/sd_dynamic_prompts/.python-version
new file mode 100644
index 0000000000000000000000000000000000000000..475ba515c04b5b7cf67a1517430691febc39a32e
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/.python-version
@@ -0,0 +1 @@
+3.7
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/__init__.py b/ex/dynamic-prompts/sd_dynamic_prompts/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..200c236308bd6c89ef9c696d03c0600e953e277a
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/__init__.py
@@ -0,0 +1 @@
+__version__ = "2.11.1"
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/attention_generator.py b/ex/dynamic-prompts/sd_dynamic_prompts/attention_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..9c70035995469ed46c83286e964e550e15fdc5bc
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/attention_generator.py
@@ -0,0 +1,39 @@
+import re
+
+from dynamicprompts.generators.attentiongenerator import AttentionGenerator
+
+# A1111 special syntax (LoRA, hypernet, etc.)
+A1111_SPECIAL_SYNTAX_RE = re.compile(r"\s*<[^>]+>")
+
+
+def remove_a1111_special_syntax_chunks(s: str) -> tuple[str, list[str]]:
+ """
+ Remove A1111 special syntax chunks from a string and return the string and the chunks.
+ """
+ chunks: list[str] = []
+
+ def put_chunk(m):
+ chunks.append(m.group(0))
+ return ""
+
+ return re.sub(A1111_SPECIAL_SYNTAX_RE, put_chunk, s), chunks
+
+
+def append_chunks(s: str, chunks: list[str]) -> str:
+ """
+ Append (A1111 special syntax) chunks to a string.
+ """
+ if not chunks:
+ return s
+ return f"{s}{''.join(chunks)}"
+
+
+class SpecialSyntaxAwareAttentionGenerator(AttentionGenerator):
+ """
+ Attention generator that is aware of A1111 special syntax (LoRA, hypernet, etc.).
+ """
+
+ def _add_emphasis(self, prompt: str) -> str:
+ prompt, special_chunks = remove_a1111_special_syntax_chunks(prompt)
+ prompt = super()._add_emphasis(prompt)
+ return append_chunks(prompt, special_chunks)
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/callbacks.py b/ex/dynamic-prompts/sd_dynamic_prompts/callbacks.py
new file mode 100644
index 0000000000000000000000000000000000000000..42f67076ddf076115eda9fc82d796749dd6f4567
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/callbacks.py
@@ -0,0 +1,74 @@
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+from typing import Any
+
+from dynamicprompts.wildcards import WildcardManager
+from modules import script_callbacks
+from modules.generation_parameters_copypaste import parse_generation_parameters
+from modules.script_callbacks import ImageSaveParams
+
+from sd_dynamic_prompts.pnginfo_saver import PngInfoSaver, PromptTemplates
+from sd_dynamic_prompts.prompt_writer import PromptWriter
+from sd_dynamic_prompts.settings import on_ui_settings
+from sd_dynamic_prompts.wildcards_tab import initialize as initialize_wildcards_tab
+
+logger = logging.getLogger(__name__)
+
+
+def register_pnginfo_saver(pnginfo_saver: PngInfoSaver) -> None:
+ def on_save(image_save_params: ImageSaveParams) -> None:
+ try:
+ if image_save_params.p:
+ png_info = image_save_params.pnginfo["parameters"]
+ image_prompts = PromptTemplates(
+ positive_template=image_save_params.p.prompt,
+ negative_template=image_save_params.p.negative_prompt,
+ )
+
+ updated_png_info = pnginfo_saver.update_pnginfo(
+ png_info,
+ image_prompts,
+ )
+ image_save_params.pnginfo["parameters"] = updated_png_info
+ except Exception:
+ logger.exception("Error save prompt file")
+
+ script_callbacks.on_before_image_saved(on_save)
+
+
+def register_prompt_writer(prompt_writer: PromptWriter) -> None:
+ def on_save(image_save_params: ImageSaveParams) -> None:
+ image_name = Path(image_save_params.filename)
+ prompt_filename = image_name.with_suffix(".csv")
+ prompt_writer.write_prompts(prompt_filename)
+
+ script_callbacks.on_before_image_saved(on_save)
+
+
+def register_on_infotext_pasted(pnginfo_saver: PngInfoSaver) -> None:
+ def on_infotext_pasted(infotext: str, parameters: dict[str, Any]) -> None:
+ new_parameters = {}
+ if "Prompt" in parameters and "Template:" in parameters["Prompt"]:
+ parameters = pnginfo_saver.strip_template_info(parameters)
+ new_parameters = parse_generation_parameters(parameters["Prompt"])
+ elif (
+ "Negative prompt" in parameters
+ and "Template:" in parameters["Negative prompt"]
+ ):
+ parameters = pnginfo_saver.strip_template_info(parameters)
+ new_parameters = parse_generation_parameters(parameters["Negative prompt"])
+ new_parameters["Negative prompt"] = new_parameters["Prompt"]
+ new_parameters["Prompt"] = parameters["Prompt"]
+ parameters.update(new_parameters)
+
+ script_callbacks.on_infotext_pasted(on_infotext_pasted)
+
+
+def register_settings():
+ script_callbacks.on_ui_settings(on_ui_settings)
+
+
+def register_wildcards_tab(wildcard_manager: WildcardManager):
+ initialize_wildcards_tab(wildcard_manager)
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/dynamic_prompting.py b/ex/dynamic-prompts/sd_dynamic_prompts/dynamic_prompting.py
new file mode 100644
index 0000000000000000000000000000000000000000..99b870f1658b0b7f6bc0f5cffa71e820533143c5
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/dynamic_prompting.py
@@ -0,0 +1,496 @@
+from __future__ import annotations
+
+import logging
+import math
+from pathlib import Path
+from string import Template
+
+import dynamicprompts
+import gradio as gr
+import modules.scripts as scripts
+import torch
+from dynamicprompts.generators.promptgenerator import GeneratorException
+from dynamicprompts.parser.parse import ParserConfig
+from dynamicprompts.wildcards import WildcardManager
+from modules import devices
+from modules.processing import fix_seed
+from modules.shared import opts
+
+from sd_dynamic_prompts import __version__, callbacks
+from sd_dynamic_prompts.element_ids import make_element_id
+from sd_dynamic_prompts.generator_builder import GeneratorBuilder
+from sd_dynamic_prompts.helpers import (
+ get_magicmodels_path,
+ get_seeds,
+ load_magicprompt_models,
+ should_freeze_prompt,
+)
+from sd_dynamic_prompts.pnginfo_saver import PngInfoSaver
+from sd_dynamic_prompts.prompt_writer import PromptWriter
+
+VERSION = __version__
+
+logger = logging.getLogger(__name__)
+logger.setLevel(logging.INFO)
+
+is_debug = getattr(opts, "is_debug", False)
+
+if is_debug:
+ logger.setLevel(logging.DEBUG)
+
+base_dir = Path(scripts.basedir())
+magicprompt_models_path = get_magicmodels_path(base_dir)
+
+
+def get_wildcard_dir() -> Path:
+ wildcard_dir = getattr(opts, "wildcard_dir", None)
+ if wildcard_dir is None:
+ wildcard_dir = base_dir / "wildcards"
+ wildcard_dir = Path(wildcard_dir)
+ try:
+ wildcard_dir.mkdir(parents=True, exist_ok=True)
+ except Exception:
+ logger.exception(f"Failed to create wildcard directory {wildcard_dir}")
+ return wildcard_dir
+
+
+def get_prompts(p):
+ original_prompt = p.all_prompts[0] if len(p.all_prompts) > 0 else p.prompt
+ original_negative_prompt = (
+ p.all_negative_prompts[0]
+ if len(p.all_negative_prompts) > 0
+ else p.negative_prompt
+ )
+
+ return original_prompt, original_negative_prompt
+
+
+device = devices.device
+# There might be a bug in auto1111 where the correct device is not inferred in some scenarios
+if device.type == "cuda" and not device.index:
+ device = torch.device("cuda:0")
+
+
+def generate_prompts(
+ prompt_generator,
+ negative_prompt_generator,
+ prompt,
+ negative_prompt,
+ num_prompts,
+):
+ all_prompts = prompt_generator.generate(prompt, num_prompts) or [""]
+ total_prompts = len(all_prompts)
+
+ all_negative_prompts = negative_prompt_generator.generate(
+ negative_prompt,
+ num_prompts,
+ ) or [""]
+
+ if len(all_negative_prompts) < total_prompts:
+ all_negative_prompts = all_negative_prompts * (
+ total_prompts // len(all_negative_prompts) + 1
+ )
+
+ all_negative_prompts = all_negative_prompts[:total_prompts]
+
+ return all_prompts, all_negative_prompts
+
+
+loaded_count = 0
+
+
+class Script(scripts.Script):
+ def __init__(self):
+ global loaded_count
+
+ loaded_count += 1
+
+ # This is a hack to make sure that the script is only loaded once
+ # Auto1111 calls the script twice, once for the txt2img and once for img2img
+ # These callbacks should only be registered once.
+
+ # When the Reload UI button in the settings tab is pressed, the script is loaded twice again
+ # Therefore we only register callbacks every second time the script is loaded
+ self._pnginfo_saver = PngInfoSaver()
+ self._prompt_writer = PromptWriter()
+ self._wildcard_manager = WildcardManager(get_wildcard_dir())
+
+ if loaded_count % 2 == 0:
+ return
+
+ callbacks.register_pnginfo_saver(self._pnginfo_saver)
+ callbacks.register_prompt_writer(self._prompt_writer)
+ callbacks.register_on_infotext_pasted(self._pnginfo_saver)
+ callbacks.register_settings()
+ callbacks.register_wildcards_tab(self._wildcard_manager)
+
+ def title(self):
+ return f"Dynamic Prompts v{VERSION}"
+
+ def show(self, is_img2img):
+ return scripts.AlwaysVisible
+
+ def ui(self, is_img2img):
+ html_path = base_dir / "helptext.html"
+ html = html_path.open().read()
+ html = Template(html).substitute(
+ WILDCARD_DIR=self._wildcard_manager.path,
+ VERSION=VERSION,
+ LIB_VERSION=dynamicprompts.__version__,
+ )
+
+ jinja_html_path = base_dir / "jinja_help.html"
+ jinja_help = jinja_html_path.open().read()
+
+ with gr.Group(elem_id=make_element_id("dynamic-prompting")):
+ with gr.Accordion("Dynamic Prompts", open=False):
+ is_enabled = gr.Checkbox(
+ label="Dynamic Prompts enabled",
+ value=True,
+ elem_id=make_element_id("dynamic-prompts-enabled"),
+ )
+
+ with gr.Group():
+ is_combinatorial = gr.Checkbox(
+ label="Combinatorial generation",
+ value=False,
+ elem_id=make_element_id("is-combinatorial"),
+ )
+
+ max_generations = gr.Slider(
+ label="Max generations (0 = all combinations - the batch count value is ignored)",
+ minimum=0,
+ maximum=1000,
+ step=1,
+ value=0,
+ elem_id=make_element_id("max-generations"),
+ )
+
+ combinatorial_batches = gr.Slider(
+ label="Combinatorial batches",
+ minimum=1,
+ maximum=10,
+ step=1,
+ value=1,
+ elem_id=make_element_id("combinatorial-times"),
+ )
+
+ with gr.Accordion("Prompt Magic", open=False):
+ with gr.Group():
+ try:
+ magicprompt_models = load_magicprompt_models(
+ magicprompt_models_path,
+ )
+ default_magicprompt_model = (
+ opts.dp_magicprompt_default_model
+ if hasattr(opts, "dp_magicprompt_default_model")
+ else magicprompt_models[0]
+ )
+ is_magic_model_available = True
+ except IndexError:
+ logger.warning(
+ f"The magicprompts config file at {magicprompt_models_path} does not contain any models.",
+ )
+
+ magicprompt_models = []
+ default_magicprompt_model = ""
+ is_magic_model_available = False
+
+ is_magic_prompt = gr.Checkbox(
+ label="Magic prompt",
+ value=False,
+ elem_id=make_element_id("is-magicprompt"),
+ interactive=is_magic_model_available,
+ )
+
+ magic_prompt_length = gr.Slider(
+ label="Max magic prompt length",
+ value=100,
+ minimum=30,
+ maximum=300,
+ step=10,
+ interactive=is_magic_model_available,
+ )
+
+ magic_temp_value = gr.Slider(
+ label="Magic prompt creativity",
+ value=0.7,
+ minimum=0.1,
+ maximum=3.0,
+ step=0.10,
+ interactive=is_magic_model_available,
+ )
+
+ magic_model = gr.Dropdown(
+ magicprompt_models,
+ value=default_magicprompt_model,
+ multiselect=False,
+ label="Magic prompt model",
+ elem_id=make_element_id("magic-prompt-model"),
+ interactive=is_magic_model_available,
+ )
+
+ magic_blocklist_regex = gr.Textbox(
+ label="Magic prompt blocklist regex",
+ value="",
+ elem_id=make_element_id("magic-prompt-blocklist-regex"),
+ placeholder=(
+ "Regular expression pattern for blocking terms out of the generated prompt. Applied case-insensitively. "
+ 'For instance, to block both "purple" and "interdimensional", you could use the pattern "purple|interdimensional".'
+ ),
+ interactive=is_magic_model_available,
+ )
+
+ is_feeling_lucky = gr.Checkbox(
+ label="I'm feeling lucky",
+ value=False,
+ elem_id=make_element_id("is-feelinglucky"),
+ )
+
+ with gr.Group():
+ is_attention_grabber = gr.Checkbox(
+ label="Attention grabber",
+ value=False,
+ elem_id=make_element_id("is-attention-grabber"),
+ )
+
+ min_attention = gr.Slider(
+ label="Minimum attention",
+ value=1.1,
+ minimum=-1,
+ maximum=2,
+ step=0.1,
+ )
+
+ max_attention = gr.Slider(
+ label="Maximum attention",
+ value=1.5,
+ minimum=-1,
+ maximum=2,
+ step=0.1,
+ )
+
+ disable_negative_prompt = gr.Checkbox(
+ label="Don't apply to negative prompts",
+ value=True,
+ elem_id=make_element_id("disable-negative-prompt"),
+ )
+
+ with gr.Accordion("Need help?", open=False):
+ gr.HTML(html)
+
+ with gr.Group():
+ with gr.Accordion("Jinja2 templates", open=False):
+ enable_jinja_templates = gr.Checkbox(
+ label="Enable Jinja2 templates",
+ value=False,
+ elem_id=make_element_id("enable-jinja-templates"),
+ )
+
+ with gr.Accordion("Help for Jinja2 templates", open=False):
+ gr.HTML(jinja_help)
+
+ with gr.Group():
+ with gr.Accordion("Advanced options", open=False):
+ gr.HTML(
+ "Some settings have been moved to the settings tab. Find them in the Dynamic Prompts section.",
+ )
+
+ unlink_seed_from_prompt = gr.Checkbox(
+ label="Unlink seed from prompt",
+ value=False,
+ elem_id=make_element_id("unlink-seed-from-prompt"),
+ )
+
+ use_fixed_seed = gr.Checkbox(
+ label="Fixed seed",
+ value=False,
+ elem_id=make_element_id("is-fixed-seed"),
+ )
+
+ gr.Checkbox(
+ label="Write raw prompt to image",
+ value=False,
+ visible=False, # For some reason, removing this line causes Auto1111 to hang
+ elem_id=make_element_id("write-raw-template"),
+ )
+
+ no_image_generation = gr.Checkbox(
+ label="Don't generate images",
+ value=False,
+ elem_id=make_element_id("no-image-generation"),
+ )
+
+ gr.Checkbox(
+ label="Write prompts to file",
+ value=False,
+ elem_id=make_element_id("write-prompts"),
+ visible=False, # For some reason, removing this line causes Auto1111 to hang
+ )
+
+ return [
+ is_enabled,
+ is_combinatorial,
+ combinatorial_batches,
+ is_magic_prompt,
+ is_feeling_lucky,
+ is_attention_grabber,
+ min_attention,
+ max_attention,
+ magic_prompt_length,
+ magic_temp_value,
+ use_fixed_seed,
+ unlink_seed_from_prompt,
+ disable_negative_prompt,
+ enable_jinja_templates,
+ no_image_generation,
+ max_generations,
+ magic_model,
+ magic_blocklist_regex,
+ ]
+
+ def process(
+ self,
+ p,
+ is_enabled,
+ is_combinatorial,
+ combinatorial_batches,
+ is_magic_prompt,
+ is_feeling_lucky,
+ is_attention_grabber,
+ min_attention,
+ max_attention,
+ magic_prompt_length,
+ magic_temp_value,
+ use_fixed_seed,
+ unlink_seed_from_prompt,
+ disable_negative_prompt,
+ enable_jinja_templates,
+ no_image_generation,
+ max_generations,
+ magic_model,
+ magic_blocklist_regex: str | None,
+ ):
+ if not is_enabled:
+ logger.debug("Dynamic prompts disabled - exiting")
+ return p
+
+ ignore_whitespace = opts.dp_ignore_whitespace
+
+ self._pnginfo_saver.enabled = opts.dp_write_raw_template
+ self._prompt_writer.enabled = opts.dp_write_prompts_to_file
+ self._limit_jinja_prompts = opts.dp_limit_jinja_prompts
+ self._auto_purge_cache = opts.dp_auto_purge_cache
+ magicprompt_batch_size = opts.dp_magicprompt_batch_size
+
+ parser_config = ParserConfig(
+ variant_start=opts.dp_parser_variant_start,
+ variant_end=opts.dp_parser_variant_end,
+ wildcard_wrap=opts.dp_parser_wildcard_wrap,
+ )
+
+ fix_seed(p)
+
+ original_prompt, original_negative_prompt = get_prompts(p)
+ original_seed = p.seed
+ num_images = p.n_iter * p.batch_size
+
+ if is_combinatorial:
+ if max_generations == 0:
+ num_images = None
+ else:
+ num_images = max_generations
+
+ combinatorial_batches = int(combinatorial_batches)
+ if self._auto_purge_cache:
+ self._wildcard_manager.clear_cache()
+
+ try:
+ logger.debug("Creating generator")
+
+ generator_builder = (
+ GeneratorBuilder(
+ self._wildcard_manager,
+ ignore_whitespace=ignore_whitespace,
+ parser_config=parser_config,
+ )
+ .set_is_feeling_lucky(is_feeling_lucky)
+ .set_is_attention_grabber(
+ is_attention_grabber,
+ min_attention,
+ max_attention,
+ )
+ .set_is_jinja_template(
+ enable_jinja_templates,
+ limit_prompts=self._limit_jinja_prompts,
+ )
+ .set_is_combinatorial(is_combinatorial, combinatorial_batches)
+ .set_is_magic_prompt(
+ is_magic_prompt=is_magic_prompt,
+ magic_model=magic_model,
+ magic_prompt_length=magic_prompt_length,
+ magic_temp_value=magic_temp_value,
+ magic_blocklist_regex=magic_blocklist_regex,
+ batch_size=magicprompt_batch_size,
+ device=device,
+ )
+ .set_is_dummy(False)
+ .set_unlink_seed_from_prompt(unlink_seed_from_prompt)
+ .set_seed(original_seed)
+ .set_context(p)
+ .set_freeze_prompt(should_freeze_prompt(p))
+ )
+
+ generator = generator_builder.create_generator()
+
+ if disable_negative_prompt:
+ generator_builder.disable_prompt_magic()
+ negative_generator = generator_builder.create_generator()
+ else:
+ negative_generator = generator
+
+ all_prompts, all_negative_prompts = generate_prompts(
+ generator,
+ negative_generator,
+ original_prompt,
+ original_negative_prompt,
+ num_images,
+ )
+
+ except GeneratorException as e:
+ logger.exception(e)
+ all_prompts = [str(e)]
+ all_negative_prompts = [str(e)]
+
+ updated_count = len(all_prompts)
+ p.n_iter = math.ceil(updated_count / p.batch_size)
+
+ p.all_seeds, p.all_subseeds = get_seeds(
+ p,
+ updated_count,
+ use_fixed_seed,
+ is_combinatorial,
+ combinatorial_batches,
+ )
+
+ logger.info(
+ f"Prompt matrix will create {updated_count} images in a total of {p.n_iter} batches.",
+ )
+
+ self._prompt_writer.set_data(
+ positive_template=original_prompt,
+ negative_template=original_negative_prompt,
+ positive_prompts=all_prompts,
+ negative_prompts=all_negative_prompts,
+ )
+
+ p.all_prompts = all_prompts
+ p.all_negative_prompts = all_negative_prompts
+ if no_image_generation:
+ logger.debug("No image generation requested - exiting")
+ # Need a minimum of batch size images to avoid errors
+ p.batch_size = 1
+ p.all_prompts = all_prompts[0:1]
+
+ p.prompt_for_display = original_prompt
+ p.prompt = original_prompt
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/element_ids.py b/ex/dynamic-prompts/sd_dynamic_prompts/element_ids.py
new file mode 100644
index 0000000000000000000000000000000000000000..49014f5e1751d688e8f49f6e9a16d6afecd142c8
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/element_ids.py
@@ -0,0 +1,5 @@
+UI_ELEMENT_ID_PREFIX = "sddp-"
+
+
+def make_element_id(name: str) -> str:
+ return UI_ELEMENT_ID_PREFIX + name
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/frozenprompt_generator.py b/ex/dynamic-prompts/sd_dynamic_prompts/frozenprompt_generator.py
new file mode 100644
index 0000000000000000000000000000000000000000..f228040acb6789de7735efdbfd809ec8360facdc
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/frozenprompt_generator.py
@@ -0,0 +1,20 @@
+from __future__ import annotations
+
+from dynamicprompts.generators.promptgenerator import PromptGenerator
+
+
+class FrozenPromptGenerator(PromptGenerator):
+ """
+ Generates a prompt once and repeats that prompt as num_images times
+ """
+
+ def __init__(self, prompt_generator: PromptGenerator):
+ self._generator = prompt_generator
+
+ def generate(
+ self,
+ template: str,
+ num_images: int = 1,
+ ) -> list[str]:
+ prompts = self._generator.generate(template, 1)
+ return num_images * prompts
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/generator_builder.py b/ex/dynamic-prompts/sd_dynamic_prompts/generator_builder.py
new file mode 100644
index 0000000000000000000000000000000000000000..62b81be054099af653ec6b6c588e546bfda688a2
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/generator_builder.py
@@ -0,0 +1,256 @@
+from __future__ import annotations
+
+import logging
+
+from dynamicprompts.generators import (
+ BatchedCombinatorialPromptGenerator,
+ CombinatorialPromptGenerator,
+ DummyGenerator,
+ FeelingLuckyGenerator,
+ JinjaGenerator,
+ PromptGenerator,
+ RandomPromptGenerator,
+)
+from dynamicprompts.parser.parse import default_parser_config
+
+from sd_dynamic_prompts.frozenprompt_generator import FrozenPromptGenerator
+
+logger = logging.getLogger(__name__)
+
+
+class GeneratorBuilder:
+ def __init__(
+ self,
+ wildcard_manager,
+ parser_config=default_parser_config,
+ ignore_whitespace=False,
+ ):
+ self._wildcard_manager = wildcard_manager
+
+ self._is_dummy = False
+ self._should_freeze_prompt = False
+ self._is_feeling_lucky = False
+ self._is_jinja_template = False
+ self._is_combinatorial = False
+ self._is_magic_prompt = False
+ self._is_attention_grabber = False
+
+ self._combinatorial_batches = 1
+ self._magic_model = None
+ self._magic_prompt_length = 100
+ self._magic_temp_value = 0.7
+ self._magic_blocklist_regex = None
+ self._min_attention = 1.1
+ self._max_attention = 1.5
+ self._device = 0
+ self._ignore_whitespace = ignore_whitespace
+ self._unlink_seed_from_prompt = False
+ self._seed = -1
+ self._context = None
+ self._parser_config = parser_config
+
+ def log_configuration(self):
+ logger.debug(
+ f"""
+ Creating generator:
+ is_dummy: {self._is_dummy}
+ is_feeling_lucky: {self._is_feeling_lucky}
+ enable_jinja_templates: {self._is_jinja_template}
+ is_combinatorial: {self._is_combinatorial}
+ is_magic_prompt: {self._is_magic_prompt}
+ combinatorial_batches: {self._combinatorial_batches}
+ magic_prompt_length: {self._magic_prompt_length}
+ magic_temp_value: {self._magic_temp_value}
+ magic_blocklist_regex: {self._magic_blocklist_regex}
+ is_attention_grabber: {self._is_attention_grabber}
+ min_attention: {self._min_attention}
+ max_attention: {self._max_attention}
+
+ """,
+ )
+
+ def set_is_dummy(self, is_dummy=True):
+ self._is_dummy = is_dummy
+ return self
+
+ def set_is_feeling_lucky(self, is_feeling_lucky=True):
+ self._is_feeling_lucky = is_feeling_lucky
+ return self
+
+ def set_is_attention_grabber(
+ self,
+ is_attention_grabber=True,
+ min_attention=1.1,
+ max_attention=1.5,
+ ):
+ self._is_attention_grabber = is_attention_grabber
+ self._min_attention = min_attention
+ self._max_attention = max_attention
+ return self
+
+ def set_is_jinja_template(self, is_jinja_template=True, limit_prompts=False):
+ self._is_jinja_template = is_jinja_template
+ self._limit_jinja_prompts = limit_prompts
+ return self
+
+ def set_is_combinatorial(self, is_combinatorial=True, combinatorial_batches=1):
+ self._is_combinatorial = is_combinatorial
+ self._combinatorial_batches = combinatorial_batches
+ return self
+
+ def set_is_magic_prompt(
+ self,
+ is_magic_prompt=True,
+ magic_model=None,
+ magic_prompt_length=100,
+ magic_temp_value=0.7,
+ device=0,
+ magic_blocklist_regex: str | None = None,
+ batch_size=1,
+ ):
+ if not magic_model:
+ self._is_magic_prompt = False
+ return self
+
+ self._magic_model = magic_model
+ self._magic_prompt_length = magic_prompt_length
+ self._magic_temp_value = magic_temp_value
+ self._magic_blocklist_regex = magic_blocklist_regex
+ self._is_magic_prompt = is_magic_prompt
+ self._magic_batch_size = batch_size
+ self._device = device
+
+ return self
+
+ def set_unlink_seed_from_prompt(self, unlink_seed_from_prompt=True):
+ self._unlink_seed_from_prompt = unlink_seed_from_prompt
+ return self
+
+ def set_seed(self, seed):
+ self._seed = seed
+ return self
+
+ def set_freeze_prompt(self, should_freeze: bool):
+ self._should_freeze_prompt = should_freeze
+ return self
+
+ def set_context(self, context):
+ self._context = context
+ return self
+
+ def disable_prompt_magic(self):
+ self.set_is_attention_grabber(False)
+ self.set_is_magic_prompt(False)
+ self.set_is_feeling_lucky(False)
+
+ return self
+
+ def create_generator(self):
+ if self._is_dummy:
+ return DummyGenerator()
+
+ elif self._is_feeling_lucky:
+ generator = FeelingLuckyGenerator()
+
+ elif self._is_jinja_template:
+ generator = self.create_jinja_generator(self._context)
+ else:
+ generator = self.create_basic_generator()
+
+ if self._is_magic_prompt:
+ from dynamicprompts.generators.magicprompt import MagicPromptGenerator
+
+ generator = MagicPromptGenerator(
+ generator,
+ model_name=self._magic_model,
+ device=self._device,
+ max_prompt_length=self._magic_prompt_length,
+ temperature=self._magic_temp_value,
+ seed=self._seed,
+ blocklist_regex=self._magic_blocklist_regex,
+ batch_size=self._magic_batch_size,
+ )
+
+ if self._is_attention_grabber:
+ try:
+ from sd_dynamic_prompts.attention_generator import (
+ SpecialSyntaxAwareAttentionGenerator,
+ )
+
+ generator = SpecialSyntaxAwareAttentionGenerator(
+ generator,
+ min_attention=self._min_attention,
+ max_attention=self._max_attention,
+ )
+ except ImportError as ie:
+ logger.error(f"Not using AttentionGenerator: {ie}")
+
+ if self._should_freeze_prompt:
+ generator = FrozenPromptGenerator(generator)
+ return generator
+
+ def create_basic_generator(
+ self,
+ ) -> PromptGenerator:
+ if self._is_combinatorial:
+ prompt_generator = CombinatorialPromptGenerator(
+ self._wildcard_manager,
+ parser_config=self._parser_config,
+ ignore_whitespace=self._ignore_whitespace,
+ )
+ prompt_generator = BatchedCombinatorialPromptGenerator(
+ prompt_generator,
+ batches=self._combinatorial_batches,
+ )
+ else:
+ prompt_generator = RandomPromptGenerator(
+ self._wildcard_manager,
+ seed=self._seed,
+ parser_config=self._parser_config,
+ unlink_seed_from_prompt=self._unlink_seed_from_prompt,
+ ignore_whitespace=self._ignore_whitespace,
+ )
+
+ return prompt_generator
+
+ def create_jinja_generator(self, p) -> PromptGenerator:
+ original_prompt = p.all_prompts[0] if len(p.all_prompts) > 0 else p.prompt
+ original_negative_prompt = (
+ p.all_negative_prompts[0]
+ if len(p.all_negative_prompts) > 0
+ else p.negative_prompt
+ )
+ context = {
+ "model": {
+ "filename": p.sd_model.sd_checkpoint_info.filename,
+ "title": p.sd_model.sd_checkpoint_info.title,
+ "hash": p.sd_model.sd_checkpoint_info.hash,
+ "model_name": p.sd_model.sd_checkpoint_info.model_name,
+ },
+ "image": {
+ "width": p.width,
+ "height": p.height,
+ },
+ "parameters": {
+ "steps": p.steps,
+ "batch_size": p.batch_size,
+ "num_batches": p.n_iter,
+ "width": p.width,
+ "height": p.height,
+ "cfg_scale": p.cfg_scale,
+ "sampler_name": p.sampler_name,
+ "seed": p.seed,
+ },
+ "prompt": {
+ "prompt": original_prompt,
+ "negative_prompt": original_negative_prompt,
+ },
+ }
+
+ generator = JinjaGenerator(
+ self._wildcard_manager,
+ context,
+ limit_prompts=self._limit_jinja_prompts,
+ ignore_whitespace=self._ignore_whitespace,
+ )
+ return generator
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/helpers.py b/ex/dynamic-prompts/sd_dynamic_prompts/helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..850358e734e8d9ba4cb731a530f8ca6bd1191a52
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/helpers.py
@@ -0,0 +1,66 @@
+from __future__ import annotations
+
+import logging
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+def get_seeds(
+ p,
+ num_seeds,
+ use_fixed_seed,
+ is_combinatorial=False,
+ combinatorial_batches=1,
+):
+ if p.subseed_strength != 0:
+ seed = int(p.all_seeds[0])
+ subseed = int(p.all_subseeds[0])
+ else:
+ seed = int(p.seed)
+ subseed = int(p.subseed)
+
+ if use_fixed_seed:
+ if is_combinatorial:
+ all_seeds = []
+ all_subseeds = [subseed] * num_seeds
+ for i in range(combinatorial_batches):
+ all_seeds.extend([seed + i] * (num_seeds // combinatorial_batches))
+ else:
+ all_seeds = [seed] * num_seeds
+ all_subseeds = [subseed] * num_seeds
+ else:
+ if p.subseed_strength == 0:
+ all_seeds = [seed + i for i in range(num_seeds)]
+ else:
+ all_seeds = [seed] * num_seeds
+
+ all_subseeds = [subseed + i for i in range(num_seeds)]
+
+ return all_seeds, all_subseeds
+
+
+def should_freeze_prompt(p):
+ # When using a variation seed, the prompt shouldn't change between generations
+ return p.subseed_strength > 0
+
+
+def load_magicprompt_models(modelfile: str) -> list[str]:
+ try:
+ models = []
+ with open(modelfile) as f:
+ for line in f:
+ # ignore comments and empty lines
+ line = line.split("#")[0].strip()
+ if line:
+ models.append(line)
+ return models
+ except FileNotFoundError:
+ logger.warning(f"Could not find magicprompts config file at {modelfile}")
+ return []
+
+
+def get_magicmodels_path(base_dir: str) -> str:
+ magicprompt_models_path = Path(base_dir / "config" / "magicprompt_models.txt")
+
+ return magicprompt_models_path
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/pnginfo_saver.py b/ex/dynamic-prompts/sd_dynamic_prompts/pnginfo_saver.py
new file mode 100644
index 0000000000000000000000000000000000000000..a4b03c6d1a9c48f9dbd4487e69464a69efe3f364
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/pnginfo_saver.py
@@ -0,0 +1,67 @@
+from __future__ import annotations
+
+import logging
+from dataclasses import dataclass
+from typing import Any
+
+logger = logging.getLogger(__name__)
+
+TEMPLATE_LABEL = "Template"
+NEGATIVE_TEMPLATE_LABEL = "Negative Template"
+
+
+@dataclass
+class PromptTemplates:
+ positive_template: str
+ negative_template: str
+
+
+class PngInfoSaver:
+ def __init__(self):
+ self._enabled = True
+
+ @property
+ def enabled(self) -> bool:
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, enabled: bool) -> None:
+ self._enabled = enabled
+
+ def update_pnginfo(self, parameters: str, prompt_templates: PromptTemplates) -> str:
+ if not self._enabled:
+ return parameters
+
+ if prompt_templates.positive_template:
+ parameters += f"\n{TEMPLATE_LABEL}: {prompt_templates.positive_template}"
+
+ if prompt_templates.negative_template:
+ parameters += (
+ f"\n{NEGATIVE_TEMPLATE_LABEL}: {prompt_templates.negative_template}"
+ )
+
+ return parameters
+
+ def strip_template_info(self, parameters: dict[str, Any]) -> str:
+ if "Prompt" in parameters and f"{TEMPLATE_LABEL}:" in parameters["Prompt"]:
+ parameters["Prompt"] = (
+ parameters["Prompt"].split(f"{TEMPLATE_LABEL}:")[0].strip()
+ )
+ elif "Negative prompt" in parameters:
+ split_by = None
+ if (
+ f"\n{TEMPLATE_LABEL}:" in parameters["Negative prompt"]
+ and f"\n{NEGATIVE_TEMPLATE_LABEL}:" in parameters["Negative prompt"]
+ ):
+ split_by = f"{TEMPLATE_LABEL}"
+ elif f"\n{NEGATIVE_TEMPLATE_LABEL}:" in parameters["Negative prompt"]:
+ split_by = f"\n{NEGATIVE_TEMPLATE_LABEL}:"
+ elif f"\n{TEMPLATE_LABEL}:" in parameters["Negative prompt"]:
+ split_by = f"\n{TEMPLATE_LABEL}:"
+
+ if split_by:
+ parameters["Negative prompt"] = (
+ parameters["Negative prompt"].split(split_by)[0].strip()
+ )
+
+ return parameters
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/prompt_writer.py b/ex/dynamic-prompts/sd_dynamic_prompts/prompt_writer.py
new file mode 100644
index 0000000000000000000000000000000000000000..d1acd788d64aef07d14d0fdd9f9888e6b9148607
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/prompt_writer.py
@@ -0,0 +1,61 @@
+from __future__ import annotations
+
+import csv
+from pathlib import Path
+
+from dynamicprompts import constants
+
+
+class PromptWriter:
+ def __init__(self):
+ self.reset()
+ self._enabled = False
+
+ def reset(self):
+ self._already_saved = False
+ self._positive_template = ""
+ self._negative_template = ""
+ self._positive_prompts = []
+ self._negative_prompts = []
+
+ @property
+ def enabled(self) -> bool:
+ return self._enabled
+
+ @enabled.setter
+ def enabled(self, value: bool) -> None:
+ self._enabled = value
+
+ def set_data(
+ self,
+ *,
+ positive_template: str,
+ negative_template: str,
+ positive_prompts: list[str],
+ negative_prompts: list[str],
+ ) -> None:
+ self.reset()
+
+ self._positive_template = positive_template
+ self._negative_template = negative_template
+ self._positive_prompts = positive_prompts
+ self._negative_prompts = negative_prompts
+
+ def write_prompts(self, path: Path | str) -> Path | None:
+ if not self._enabled or self._already_saved:
+ return None
+
+ self._already_saved = True
+
+ path = Path(path)
+ with path.open("w", encoding=constants.DEFAULT_ENCODING, errors="ignore") as f:
+ writer = csv.writer(f)
+ writer.writerow(["positive_prompt", "negative_prompt"])
+ writer.writerow([self._positive_template, self._negative_template])
+ for positive_prompt, negative_prompt in zip(
+ self._positive_prompts,
+ self._negative_prompts,
+ ):
+ writer.writerow([positive_prompt, negative_prompt])
+
+ return path
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/settings.py b/ex/dynamic-prompts/sd_dynamic_prompts/settings.py
new file mode 100644
index 0000000000000000000000000000000000000000..5db8cb784cd7ddf23a8842b6365ce01245733590
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/settings.py
@@ -0,0 +1,100 @@
+from pathlib import Path
+
+import gradio as gr
+from modules import scripts, shared
+
+from sd_dynamic_prompts.helpers import get_magicmodels_path, load_magicprompt_models
+
+base_dir = Path(scripts.basedir())
+
+
+def on_ui_settings():
+ section = "dynamicprompts", "Dynamic Prompts"
+ shared.opts.add_option(
+ key="dp_ignore_whitespace",
+ info=shared.OptionInfo(
+ False,
+ label="Ignore whitespace in prompts: All newlines, tabs, and multiple spaces are replaced by a single space",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_write_raw_template",
+ info=shared.OptionInfo(
+ False,
+ label="Save template to metadata: Write prompt template into the PNG metadata",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_write_prompts_to_file",
+ info=shared.OptionInfo(
+ False,
+ label="Write prompts to file: Create a new .txt file for every batch containing the prompt template as well as the generated prompts.",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_parser_variant_start",
+ info=shared.OptionInfo(
+ "{",
+ label="String to use as left bracket for parser variants, .e.g {variant1|variant2|variant3}",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_parser_variant_end",
+ info=shared.OptionInfo(
+ "}",
+ label="String to use as right bracket for parser variants, .e.g {variant1|variant2|variant3}",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_parser_wildcard_wrap",
+ info=shared.OptionInfo(
+ "__",
+ label="String to use as wrap for parser wildcard, .e.g __wildcard__",
+ section=section,
+ ),
+ )
+ shared.opts.add_option(
+ key="dp_limit_jinja_prompts",
+ info=shared.OptionInfo(
+ False,
+ label="Limit Jinja prompts: Limit the number of prompts to batch_count * batch_size. The default is to generate batch_count * batch_size * number of prompts generated by Jinja",
+ section=section,
+ ),
+ )
+
+ shared.opts.add_option(
+ key="dp_auto_purge_cache",
+ info=shared.OptionInfo(
+ False,
+ label="Automatically purge wildcard cache on every generation.",
+ section=section,
+ ),
+ )
+
+ magic_models = load_magicprompt_models(get_magicmodels_path(base_dir))
+ shared.opts.add_option(
+ key="dp_magicprompt_default_model",
+ info=shared.OptionInfo(
+ magic_models[0] if magic_models else "",
+ label="Default magic prompt model",
+ component=gr.Dropdown,
+ component_args={"choices": magic_models},
+ section=section,
+ ),
+ )
+
+ shared.opts.add_option(
+ key="dp_magicprompt_batch_size",
+ info=shared.OptionInfo(
+ 1,
+ label="Magic Prompt batch size (higher is faster but uses more memory)",
+ component=gr.Slider,
+ component_args={"minimum": 1, "maximum": 64, "step": 1},
+ section=section,
+ ),
+ )
diff --git a/ex/dynamic-prompts/sd_dynamic_prompts/wildcards_tab.py b/ex/dynamic-prompts/sd_dynamic_prompts/wildcards_tab.py
new file mode 100644
index 0000000000000000000000000000000000000000..e66f05375e6b93d52edae9b4a602d79b51692f64
--- /dev/null
+++ b/ex/dynamic-prompts/sd_dynamic_prompts/wildcards_tab.py
@@ -0,0 +1,286 @@
+from __future__ import annotations
+
+import json
+import logging
+import random
+import shutil
+import traceback
+from pathlib import Path
+
+import gradio as gr
+import modules.scripts as scripts
+from dynamicprompts.wildcards import WildcardManager
+from dynamicprompts.wildcards.collection import WildcardTextFile
+from dynamicprompts.wildcards.tree import WildcardTreeNode
+from modules import script_callbacks
+from send2trash import send2trash
+
+from sd_dynamic_prompts.element_ids import make_element_id
+
+COPY_COLLECTION_ACTION = "copy collection"
+LOAD_FILE_ACTION = "load file"
+LOAD_TREE_ACTION = "load tree"
+MESSAGE_PROCESSING_ACTION = "message processing"
+
+logger = logging.getLogger(__name__)
+
+wildcard_manager: WildcardManager
+
+collections_path = Path(scripts.basedir()) / "collections"
+
+
+def get_collection_dirs() -> dict[str, Path]:
+ """
+ Get a mapping of name -> subdirectory path for the extension's collections/ directory.
+ """
+ return {
+ str(pth.relative_to(collections_path)): pth
+ for pth in collections_path.iterdir()
+ if pth.is_dir()
+ }
+
+
+def initialize(manager: WildcardManager):
+ global wildcard_manager
+ wildcard_manager = manager
+ script_callbacks.on_ui_tabs(on_ui_tabs)
+
+
+def _format_node_for_json(
+ wildcard_manager: WildcardManager,
+ node: WildcardTreeNode,
+) -> list[dict]:
+ collections = [
+ {
+ "name": node.qualify_name(coll),
+ "wrappedName": wildcard_manager.to_wildcard(node.qualify_name(coll)),
+ "children": [],
+ }
+ for coll in sorted(node.collections)
+ ]
+ child_items = [
+ {"name": name, "children": _format_node_for_json(wildcard_manager, child_node)}
+ for name, child_node in sorted(node.child_nodes.items())
+ ]
+ return [*collections, *child_items]
+
+
+def get_wildcard_hierarchy_for_json():
+ return _format_node_for_json(wildcard_manager, wildcard_manager.tree.root)
+
+
+def on_ui_tabs():
+ header_html = f"""
+