From ea8e94d696cc8c5a6699d6f493bdfc98d5d52497 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Sun, 13 Oct 2024 01:42:09 -0700 Subject: [PATCH 01/16] adding dynamic_prompts script --- scripts/dynamic-prompts/dynamic_prompts.js | 544 +++++++++++++++++++++ scripts/dynamic-prompts/metadata.json | 6 + 2 files changed, 550 insertions(+) create mode 100644 scripts/dynamic-prompts/dynamic_prompts.js create mode 100644 scripts/dynamic-prompts/metadata.json diff --git a/scripts/dynamic-prompts/dynamic_prompts.js b/scripts/dynamic-prompts/dynamic_prompts.js new file mode 100644 index 0000000..71f72f6 --- /dev/null +++ b/scripts/dynamic-prompts/dynamic_prompts.js @@ -0,0 +1,544 @@ +//@api-1.0 +// dynamic prompts +// author zanshinmu +// v3.5 +/** + * Documentation for "Dynamic Prompts" Script (Version 3.5) for "Draw Things" + * + * This script generates dynamic prompts for the "Draw Things" application. Customize it to enhance your creative experience. + * + * Modifying Categories: + * - Categories are thematic elements like 'Locale', 'Adjective', etc. + * - To add a new category: Include it in the 'categories' object (e.g., "Futuristic": ["cybernetic", "AI-driven"]). + * - To modify an existing category: Add or remove elements directly in the category's list. + * + * Dynamic Prompt Strings: + * - The Prompts string structures your generated prompts. + * - Use curly braces {} to include category elements. Examples: + * - Single element: "{Locale}" might generate "neon-lit city." + * - Multiple elements: "{Adjective:2}" can give "rainy, bustling." + * - Random range: "{Object:1-3}" could return "hovercar" or "hovercar, android, neon sign." + * + * BatchCount: + * - BatchCount sets the number of prompts to generate. + * - Change its value with the slider to control output (e.g., `BatchCount = 15;` for fifteen prompts). + * + * Customize your script to create diverse and inspiring prompts for "Draw Things." + * + * User Interface: + * - Use UI Prompt: + * Process the prompt in the UI box instead of selecting random prompts. + * - Lock configuration: + * When selecting random prompts, do not change configurations. + * - Iterate Mode: + * Iterated generation of all combinations of dynamic prompt. Best used with UI Prompt. + * BatchCount is not used. Iterate mode can create very large numbers. + * To reduce the numbers, eliminate a few dynamic categories from your prompt. + */ + +//Version +const versionString = "3.5" +//Maximum iterations for Iterate Mode +const maxIter = 500 +//store selected prompt/LoRA data +let promptData; +let userPrompt = ''; +// Default example prompt for UI demonstrating category use +const defaultPrompt = "Cybervenues wide-angle shot of {weather} {time} {locale}" + +/* These are the prompts randomly selected from if UI Prompt isn't valid. + Modify prompts to provide dynamic prompts with LoRAs which will be randomly selected from if a valid dynamic prompt is not found in the UI. + As of 3.0.1 there can be a 'configuration' object for each prompt which contains arbitrary settings which will be passed on to the pipeline at runtime. These settings correspond to the possible values of pipeline.configuration + There is an important caveat: Sampler is tricky because it looks like a string with the sampler name but it's actually an integer from a lookup table, so you want to find the number that corresponds to the desired sampler and use that instead (without quotes). + + Here is the current sampler lookup table: + SamplerType = { + DPMPP_2M_KARRAS: 0, + EULER_A: 1, + DDIM: 2, + PLMS: 3, + DPMPP_SDE_KARRAS: 4, + UNI_PC: 5, + LCM: 6, + EULER_A_SUBSTEP: 7, + DPMPP_SDE_SUBSTEP: 8, + TCD: 9, + EULER_A_TRAILING: 10, + DPMPP_SDE_TRAILING: 11, + DPMPP_2M_AYS: 12, + EULER_A_AYS: 13, + DPMPP_SDE_AYS: 14, + DPMPP_2M_TRAILING: 15, + DDIM_TRAILING: 16 +} + */ + +const prompts = [ + { + prompt: "{colors:1-3} dominant {camera} shot of a {adjective} cyborg woman, {style}, {hairstyle} {haircolor} hair, {features}, {pose} in {weather} {time} {locale}", + negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", + model: "FLUX.1 [schnell] (8-bit)", + loras: [ + { file: "AntiBlur v1.0 [dev]", weight: 0.4} + ], + configuration: + {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} + }, + { + prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", + negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", + model: "RealVisXL v4.0", + loras: [ + { file: "Fix Hands Slider", weight: 0.3 }, + { file: "Pixel Art XL v1.1", weight: 0.4 } + ], + configuration: + {width:1024,height:1024,steps:28,sampler:0,guidanceScale:4.0} + }, + { + prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", + negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", + model: "RealVisXL v4.0", + loras: [ + { file: "Fix Hands Slider", weight: 0.3 }, + { file: "Pixel Art XL v1.1", weight: 0.4 } + ], + configuration: + {width:1024,height:1024,steps:28,sampler:0,guidanceScale:4.0} + } + + // Add more prompt objects as needed + // Empty file will be ignored +]; + +// Categories definition +const categories = { + time: [ + "morning", + "noon", + "night", + "midnight", + "sunset", + "sunrise" + ], + camera: [ + "full body", + "medium", + "close_up", + "establishing" + ], + locale: [ + "cityscape", + "street", + "alley", + "marketplace", + "industrial zone", + "waterfront", + "forest", + "rooftop", + "bridge", + "park" + ], + weather: [ + "rainy", + "smoggy", + "stormy", + "dusty" + ], + adjective: [ + "beautiful", + "moody", + "cute", + "tired", + "injured", + "cyborg", + "muscular" + ], + style: [ + "cyberpunk street", "dark gothic", "military armor", "elegant kimono", "eveningwear dress", "corpo uniform", "tech bodysuit" + ], + hairstyle: [ + "long", + "medium", + "shaved", + "short", + "wet", + "punk" + ], + haircolor: [ + "red", + "blonde", + "colored", + "brunette", + "natural", + "streaked" + ], + colors: [ + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "pink", + "brown", + "black", + "white", + "gray", + "cyan", + "magenta", + "teal", + "olive", + "maroon", + "navy", + "lime", + "indigo", + "gold", + "silver", + "bronze", + "salmon", + "coral", + "turquoise", + "peach", + "lavender", + "emerald", + "ruby", + "sapphire" + ], + features: [ + "{colors} glowing cyborg eyes", + "smoking", + "tech brella with {colors} glowing accents", + "cyborg arm", + "cyborg legs", + "optics implant", + "tech goggles" + ], + pose: [ + "action pose", + "poses", + "model pose", + "relaxing", + "sitting" + ], + malestyle: [ + "leather", + "denim", + "silk", + ] +}; + +// UI +const categoryNames = Object.keys(categories).join(', '); +const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; +const aboutText = "Selects randomly from " + `${prompts.length}`+ " dynamic prompts" + "\nGenerates batch images using '{}' to randomize categories in prompt" +const userSelection = requestFromUser("Dynamic Prompts", "Start", function() { + return [ + this.section(header, aboutText, []), + this.section("Categories", categoryNames, [ + this.textField(defaultPrompt, pipeline.prompts.prompt, true, 60), + this.slider(10, this.slider.fractional(0), 1, 2000, "batch count"), + this.switch(false, "Use UI Prompt"), + this.switch(false, "Lock configuration"), + this.switch(false, "Iterate Mode"), + this.switch(true, "Download Models") + ]) + ]; +}); +// Number of prompts to generate +let batchCount = userSelection[1][1]; +let uiPrompt = userSelection[1][0]; +let useUiPrompt = userSelection[1][2]; +let overrideModels = userSelection[1][3]; +let iterateMode = userSelection[1][4]; +let downloadModels = userSelection[1][5]; +if (iterateMode){ + console.log("Iterate Mode"); +} +//console.log(JSON.stringify(userSelection)); + +// Get configuration +const configuration = pipeline.configuration; +const defaultLoras = pipeline.configuration.loras; +//console.log(JSON.stringify(configuration)); +const uiNegPrompt = pipeline.prompts.negativePrompt; +if (isPromptValid(uiPrompt, categories)) { + if(useUiPrompt){ + console.log("Valid UI prompt detected."); + userPrompt = uiPrompt; // Use the UI prompt + } +} + +// Main batch loop +if (!iterateMode){ + for (let i = 0; i < batchCount; i++){ + render(getPrompt()); + let batchCountLog = `batch ${i + 1} of ${batchCount}\n`; + console.log(batchCountLog); + } +} else { + let dynprompt; + if (useUiPrompt){ + dynPrompt = userPrompt; + } else { + dynPrompt = getPromptString(selectRandomPrompt()); + } + p = computeTotalPromptCount(dynPrompt); + if (p > maxIter){ + console.log(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); + console.log("Reduce the number of categories used in prompt.") + return; + } + let k = 1; + console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); + for (let generatedPrompt of generatePrompts(dynPrompt)) { + console.log(`iterating render ${k} of ${p}\n`); + render(generatedPrompt); // Do something with each generated prompt + k++; + } +} + +function computeTotalPromptCount(dynamicPrompt) { + let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); + let totalCombinationCount = 1; + for (let placeholder of placeholders) { + const valueCount = categories[placeholder].length; // Get the number of values in each category + totalCombinationCount *= valueCount; // Multiply the counts to calculate the maximum possible number of combinations + } + return totalCombinationCount; +} + +function* generatePrompts(dynamicPrompt) { + function cartesian(...arrays) { + return arrays.reduce((acc, curr) => acc.flatMap(d => curr.map(e => [d, e].flat()))); + } + + let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); + let validPlaceholders = placeholders.filter(p => categories[p]); + let categoryValues = validPlaceholders.map(p => categories[p]); + let combinations = cartesian(...categoryValues); + + for (let combination of combinations) { + let prompt = dynamicPrompt; + validPlaceholders.forEach((placeholder, i) => { + prompt = prompt.replace(`{${placeholder}}`, combination[i]); + }); + yield prompt; + } +} + + +function selectRandomPrompt() { + // Generate a random index to select a random prompt + const randomIndex = Math.floor(Math.random() * prompts.length); + console.log(`Selected ${randomIndex} of ${prompts.length}`) + // Get the randomly selected prompt object + const selectedPrompt = prompts[randomIndex]; + // Extract prompt string, LoRa filenames, and weights + let myprompt = selectedPrompt.prompt; + let myneg = selectedPrompt.negativePrompt; + let mymodel = selectedPrompt.model; + let myconfig = selectedPrompt.configuration; + const loras = selectedPrompt.loras.map(lora => ({ file: lora.file, weight: lora.weight })); + let myloras = getAssociatedLoRas(loras); + myconfig.model = mymodel; + myconfig.loras=myloras; + // Store the promptData object + let promptData = { prompt: myprompt, negativePrompt: myneg, configuration: myconfig }; + if (downloadModels){ + getModels(promptData); + } + return promptData; +} + +//Download models if necessary +function getModels(promptData){ + let models = []; + //Model first + models.push(promptData.configuration.model); + //Initiate Download + pipeline.downloadBuiltins(models); + + //Loras + for (let i = 0; i < promptData.configuration.loras.length; i++) { + let lora = promptData.configuration.loras[i].file; + models.push(lora); + } + //Initiate Download + pipeline.downloadBuiltins(models); +} + +function loraNamestoFiles(loras){ + for (let i = 0; i < loras.length; i++) { + let loraname = loras[i].file; + let lorafile = pipeline.findLoRAByName(loraname); + loras[i]=lorafile; + } + return loras; +} + +// Function to get the prompt string from the object returned by selectRandomPrompt +function getPromptString(p) { + return p.prompt; +} + +// Function to extract and validate category names and their requested item count or range from the uiPrompt +function isPromptValid(uiPrompt, categories) { + const regex = /{(\w+)(?::(\d+)(?:-(\d+))?)?}/g; // Extended regex to capture syntax variations + let isValid = false; // Assume the prompt is invalid initially + + let match; + while ((match = regex.exec(uiPrompt)) !== null) { + const [fullMatch, categoryName, itemCount, rangeEnd] = match; + + // Check if the category name is valid + if (!(categoryName in categories)) { + console.log(`Invalid UI prompt: Category '${categoryName}' not found.`); + return false; // Invalid if the category doesn't exist + } + + // Validate item count or range syntax if specified + if (itemCount) { + isValid = true; // Assume valid syntax if itemCount exists + if (rangeEnd && (parseInt(itemCount) > parseInt(rangeEnd))) { + console.log(`Invalid UI prompt: Incorrect range '${itemCount}-${rangeEnd}' in category '${categoryName}'.`); + return false; // Invalid if the range start is greater than the range end + } + } else { + // Valid if only the category name is specified without itemCount or range + isValid = true; + } + } + + // Prompt is invalid if no categories or incorrect syntax is detected + if (!isValid) { + console.log("Invalid UI prompt: No valid categories or incorrect syntax detected."); + } + return isValid; +} + +//get prompt each iteration +function getPrompt () { + if (useUiPrompt) { + console.log("Using UI Prompt"); + promptString = userPrompt; + } else { + console.log("Selecting dynamic prompt and configuration."); + promptData = selectRandomPrompt(); + //console.log(promptData); + promptString = getPromptString(promptData); + } + return promptString +} + +function getAssociatedLoRas(loras) { + if(!overrideModels){ + let validLoras = setLoras(loras); + const mergedLoras = defaultLoras.concat(validLoras); + return mergedLoras + }else{ + return defaultLoras + } +} + +// is name in defaultLoras? +function isDefaultLora(lora){ + for (let i = 0; i < defaultLoras.length; i++) { + if (defaultLoras[i].file === lora.file){ + return true; + } else{ + return false; + } + } +} + + +// Only overwrite valid Loras from prompts +function setLoras (myLoras){ + let loras=[]; + for (let i = 0; i < myLoras.length; i++) { + if (myLoras[i].file !== ''){ + if(!isDefaultLora(myLoras[i])){ + loras.push(myLoras[i]); + } + } + } + return loras +} + +// Run pipeline +function render (promptString){ + let editedString = replaceWildcards(promptString, categories); + let neg; + let myConfiguration = configuration; + if (useUiPrompt){ + neg = uiNegPrompt; + } else { + neg = promptData.negativePrompt; + if (!overrideModels){ + myConfiguration.model = promptData.model; + //Default to random seed, configuration overrides + myConfiguration.seed = -1; + //Apply configuration changes, if any + myConfiguration = Object.assign(configuration, promptData.configuration); + myConfiguration.loras = loraNamestoFiles(promptData.configuration.loras); + } + } + //Clear canvas + canvas.clear(); + pipeline.run({ + configuration: myConfiguration, + prompt: editedString, + negativePrompt: neg + }); +} + +// Function to replace wildcards with random options +function replaceWildcards(promptString, categories) { + const wildcardRegex = /{(\w+)(?::(\d+)(?:-(\d+))?)?}/g; + + function replaceWildcard(match, categoryName, minCount, maxCount) { + const categoryOptions = categories[categoryName]; + if (categoryOptions) { + const count = getRandomCount(minCount, maxCount); + const options = new Set(); // Use a Set to ensure uniqueness + + while (options.size < count) { + let randomOption = categoryOptions[Math.floor(Math.random() * categoryOptions.length)]; + + // Check if the selected option contains another wildcard + if (wildcardRegex.test(randomOption)) { + // Recursively expand the wildcard in the selected option + randomOption = replaceWildcards(randomOption, categories); + } + + options.add(randomOption); + } + + return [...options].join(", "); + } + return match; // If category not found, return the original match + } + + // Recursively replace all wildcards in the prompt string + let editedString = promptString.replace(wildcardRegex, replaceWildcard); + + return editedString; +} + +// Function to get a random count within the specified range or default to 1 if range not provided +function getRandomCount(minCount, maxCount) { + const min = parseInt(minCount); + const max = parseInt(maxCount); + + if (!isNaN(min) && !isNaN(max)) { + // Both min and max are numbers, return a random number in this range + return Math.floor(Math.random() * (max - min + 1)) + min; + } else if (!isNaN(min)) { + // Only min is a number, return this number + return min; + } else { + // Default case when neither min nor max is a number + return 1; + } +} diff --git a/scripts/dynamic-prompts/metadata.json b/scripts/dynamic-prompts/metadata.json new file mode 100644 index 0000000..363ed3f --- /dev/null +++ b/scripts/dynamic-prompts/metadata.json @@ -0,0 +1,6 @@ +{ + "name": "Dynamic Prompts for Draw Things", + "author": "Zanshinmu", + "file": "dynamic_prompts.js", + "description": "Allows dynamic prompts features similar to sd-dynamic-prompts in Draw Things." +} From dcc00db7828d0c32a0c431ee458e1cae8b8fddaf Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Mon, 14 Oct 2024 13:32:27 -0700 Subject: [PATCH 02/16] Added render timer, bugfix --- scripts/dynamic-prompts/dynamic_prompts.js | 43 ++++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic_prompts.js b/scripts/dynamic-prompts/dynamic_prompts.js index 71f72f6..2cb927c 100644 --- a/scripts/dynamic-prompts/dynamic_prompts.js +++ b/scripts/dynamic-prompts/dynamic_prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author zanshinmu -// v3.5 +// v3.5.1 /** * Documentation for "Dynamic Prompts" Script (Version 3.5) for "Draw Things" * @@ -37,14 +37,14 @@ */ //Version -const versionString = "3.5" +const versionString = "3.5.1" //Maximum iterations for Iterate Mode const maxIter = 500 //store selected prompt/LoRA data let promptData; let userPrompt = ''; // Default example prompt for UI demonstrating category use -const defaultPrompt = "Cybervenues wide-angle shot of {weather} {time} {locale}" +const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" /* These are the prompts randomly selected from if UI Prompt isn't valid. Modify prompts to provide dynamic prompts with LoRAs which will be randomly selected from if a valid dynamic prompt is not found in the UI. @@ -272,9 +272,9 @@ if (isPromptValid(uiPrompt, categories)) { // Main batch loop if (!iterateMode){ for (let i = 0; i < batchCount; i++){ - render(getPrompt()); - let batchCountLog = `batch ${i + 1} of ${batchCount}\n`; + let batchCountLog = `Rendering batch ${i + 1} of ${batchCount}`; console.log(batchCountLog); + render(getPrompt()); } } else { let dynprompt; @@ -286,15 +286,15 @@ if (!iterateMode){ p = computeTotalPromptCount(dynPrompt); if (p > maxIter){ console.log(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); - console.log("Reduce the number of categories used in prompt.") - return; - } - let k = 1; - console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); - for (let generatedPrompt of generatePrompts(dynPrompt)) { - console.log(`iterating render ${k} of ${p}\n`); - render(generatedPrompt); // Do something with each generated prompt - k++; + console.log("Reduce the number of categories used in prompt."); + } else { + let k = 1; + console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); + for (let generatedPrompt of generatePrompts(dynPrompt)) { + console.log(`iterating render ${k} of ${p}\n`); + render(generatedPrompt); // Do something with each generated prompt + k++; + } } } @@ -331,7 +331,7 @@ function* generatePrompts(dynamicPrompt) { function selectRandomPrompt() { // Generate a random index to select a random prompt const randomIndex = Math.floor(Math.random() * prompts.length); - console.log(`Selected ${randomIndex} of ${prompts.length}`) + console.log(`Selected prompt/configuration ${randomIndex} of ${prompts.length}`) // Get the randomly selected prompt object const selectedPrompt = prompts[randomIndex]; // Extract prompt string, LoRa filenames, and weights @@ -423,7 +423,6 @@ function getPrompt () { console.log("Using UI Prompt"); promptString = userPrompt; } else { - console.log("Selecting dynamic prompt and configuration."); promptData = selectRandomPrompt(); //console.log(promptData); promptString = getPromptString(promptData); @@ -466,8 +465,18 @@ function setLoras (myLoras){ return loras } +function timer (start){ + const end = Date.now(); + const duration = end - start; + const minutes = Math.floor(duration / 60000); + let seconds = Math.floor((duration % 60000) / 1000); + seconds = seconds < 10 ? '0' + seconds : seconds; + console.log(`✔︎ Render time ‣ ${minutes}:${seconds}\n`); +} + // Run pipeline function render (promptString){ + let start = Date.now(); let editedString = replaceWildcards(promptString, categories); let neg; let myConfiguration = configuration; @@ -491,6 +500,8 @@ function render (promptString){ prompt: editedString, negativePrompt: neg }); + //Output render time elapsed + timer(start); } // Function to replace wildcards with random options From 0c79b42d0206adef8c0d148b5afb0537fda60b26 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Mon, 14 Oct 2024 17:03:11 -0700 Subject: [PATCH 03/16] Fixed wildcard expansion bug with deeply nested placeholders --- scripts/dynamic-prompts/dynamic_prompts.js | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic_prompts.js b/scripts/dynamic-prompts/dynamic_prompts.js index 2cb927c..cb01a64 100644 --- a/scripts/dynamic-prompts/dynamic_prompts.js +++ b/scripts/dynamic-prompts/dynamic_prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author zanshinmu -// v3.5.1 +// v3.5.2 /** * Documentation for "Dynamic Prompts" Script (Version 3.5) for "Draw Things" * @@ -37,7 +37,7 @@ */ //Version -const versionString = "3.5.1" +const versionString = "3.5.2" //Maximum iterations for Iterate Mode const maxIter = 500 //store selected prompt/LoRA data @@ -504,7 +504,6 @@ function render (promptString){ timer(start); } -// Function to replace wildcards with random options function replaceWildcards(promptString, categories) { const wildcardRegex = /{(\w+)(?::(\d+)(?:-(\d+))?)?}/g; @@ -516,13 +515,10 @@ function replaceWildcards(promptString, categories) { while (options.size < count) { let randomOption = categoryOptions[Math.floor(Math.random() * categoryOptions.length)]; - - // Check if the selected option contains another wildcard - if (wildcardRegex.test(randomOption)) { - // Recursively expand the wildcard in the selected option - randomOption = replaceWildcards(randomOption, categories); - } - + + // Recursively expand the wildcard in the selected option + randomOption = replaceWildcards(randomOption, categories); + options.add(randomOption); } @@ -531,12 +527,17 @@ function replaceWildcards(promptString, categories) { return match; // If category not found, return the original match } - // Recursively replace all wildcards in the prompt string - let editedString = promptString.replace(wildcardRegex, replaceWildcard); + let editedString = promptString; + + // Recursively replace wildcards until there are none left + while (wildcardRegex.test(editedString)) { + editedString = editedString.replace(wildcardRegex, replaceWildcard); + } return editedString; } + // Function to get a random count within the specified range or default to 1 if range not provided function getRandomCount(minCount, maxCount) { const min = parseInt(minCount); From bdf336a3e62d2ef9a70e9cc629c26b20123f48bc Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 18 Oct 2024 02:11:28 -0700 Subject: [PATCH 04/16] Fixed iterator bug, added output location, updated text, separated UI Prompt from main UI --- scripts/dynamic-prompts/dynamic_prompts.js | 107 +++++++++++++++------ 1 file changed, 77 insertions(+), 30 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic_prompts.js b/scripts/dynamic-prompts/dynamic_prompts.js index cb01a64..5f8aa34 100644 --- a/scripts/dynamic-prompts/dynamic_prompts.js +++ b/scripts/dynamic-prompts/dynamic_prompts.js @@ -1,9 +1,11 @@ //@api-1.0 // dynamic prompts -// author zanshinmu -// v3.5.2 +// author: zanshinmu +// v3.5.4 +// Discord Thread for Dynamic Prompts: +// https://discord.com/channels/1038516303666876436/1207467278426177736 /** - * Documentation for "Dynamic Prompts" Script (Version 3.5) for "Draw Things" + * Documentation for "Dynamic Prompts" Script (Version 3.5.4) for "Draw Things" * * This script generates dynamic prompts for the "Draw Things" application. Customize it to enhance your creative experience. * @@ -28,27 +30,32 @@ * User Interface: * - Use UI Prompt: * Process the prompt in the UI box instead of selecting random prompts. + * - Lock configuration: * When selecting random prompts, do not change configurations. + * - Iterate Mode: * Iterated generation of all combinations of dynamic prompt. Best used with UI Prompt. - * BatchCount is not used. Iterate mode can create very large numbers. - * To reduce the numbers, eliminate a few dynamic categories from your prompt. + * BatchCount is not used in Iterate mode. Iterate mode can create very large numbers. + * To reduce the numbers, eliminate categories from your prompt. */ //Version -const versionString = "3.5.2" +const versionString = "v3.5.4" //Maximum iterations for Iterate Mode const maxIter = 500 //store selected prompt/LoRA data let promptData; let userPrompt = ''; +let uiPrompt = ''; // Default example prompt for UI demonstrating category use const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" /* These are the prompts randomly selected from if UI Prompt isn't valid. Modify prompts to provide dynamic prompts with LoRAs which will be randomly selected from if a valid dynamic prompt is not found in the UI. + As of 3.0.1 there can be a 'configuration' object for each prompt which contains arbitrary settings which will be passed on to the pipeline at runtime. These settings correspond to the possible values of pipeline.configuration + There is an important caveat: Sampler is tricky because it looks like a string with the sampler name but it's actually an integer from a lookup table, so you want to find the number that corresponds to the desired sampler and use that instead (without quotes). Here is the current sampler lookup table: @@ -230,51 +237,77 @@ const categories = { // UI const categoryNames = Object.keys(categories).join(', '); +const okButton = "Start"; const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; -const aboutText = "Selects randomly from " + `${prompts.length}`+ " dynamic prompts" + "\nGenerates batch images using '{}' to randomize categories in prompt" -const userSelection = requestFromUser("Dynamic Prompts", "Start", function() { +const aboutText = "Selects randomly from " + `${prompts.length} dynamic prompts`+ + " located in the script's 'const prompts' object."; +const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { return [ - this.section(header, aboutText, []), - this.section("Categories", categoryNames, [ - this.textField(defaultPrompt, pipeline.prompts.prompt, true, 60), - this.slider(10, this.slider.fractional(0), 1, 2000, "batch count"), - this.switch(false, "Use UI Prompt"), + this.section(header, aboutText, [ + this.switch(false, "Enter UI Prompt"), this.switch(false, "Lock configuration"), this.switch(false, "Iterate Mode"), - this.switch(true, "Download Models") + this.switch(true, "Download Models"), + this.slider(10, this.slider.fractional(0), 1, 2000, "batch count"), + ]), + this.section("Output:", "Output rendered images to custom location", [ + this.directory('${filesystem.pictures.path}') ]) ]; }); -// Number of prompts to generate -let batchCount = userSelection[1][1]; -let uiPrompt = userSelection[1][0]; -let useUiPrompt = userSelection[1][2]; -let overrideModels = userSelection[1][3]; -let iterateMode = userSelection[1][4]; -let downloadModels = userSelection[1][5]; +// Parse UI input +let useUiPrompt = userSelection[0][0]; +let overrideModels = userSelection[0][1]; +let iterateMode = userSelection[0][2]; +let downloadModels = userSelection[0][3]; +let batchCount = userSelection[0][4]; +let outputDir = userSelection[1][0]; + if (iterateMode){ console.log("Iterate Mode"); } + +if (useUiPrompt) { + const userSelection = requestFromUser("Dynamic Prompts: UI Prompt", okButton, function() { + return [ + this.section("Dynamic Prompt Syntax","", [ + this.section("{category}","Replaces category with random item", []), + this.section("{category:2}","Replaces category with 2 random items", []), + this.section("{category:1-3}","Replaces category with 1 to 3 random items", []), + this.section("UI Prompt", "Modify the dynamic prompt to your desire", [ + this.textField(defaultPrompt, pipeline.prompts.prompt, true, 80), + this.section("Available Categories", categoryNames, []) + ]) + ]) + ]; // Closing bracket for the return array + }); + uiPrompt = userSelection[0][3][0]; +} //console.log(JSON.stringify(userSelection)); // Get configuration const configuration = pipeline.configuration; const defaultLoras = pipeline.configuration.loras; -//console.log(JSON.stringify(configuration)); const uiNegPrompt = pipeline.prompts.negativePrompt; -if (isPromptValid(uiPrompt, categories)) { - if(useUiPrompt){ + +if(useUiPrompt){ + if(isPromptValid(uiPrompt, categories)){ console.log("Valid UI prompt detected."); userPrompt = uiPrompt; // Use the UI prompt + } else { + console.log("Error, Invalid UI Prompt"); + return; } } // Main batch loop if (!iterateMode){ for (let i = 0; i < batchCount; i++){ - let batchCountLog = `Rendering batch ${i + 1} of ${batchCount}`; + let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; console.log(batchCountLog); render(getPrompt()); + //Save to custom location + customImageSave(pipeline, i); } } else { let dynprompt; @@ -291,13 +324,25 @@ if (!iterateMode){ let k = 1; console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); for (let generatedPrompt of generatePrompts(dynPrompt)) { - console.log(`iterating render ${k} of ${p}\n`); - render(generatedPrompt); // Do something with each generated prompt + console.log(`iterating render ${k} of ${p}\n${generatedPrompt}\n`); + render(generatedPrompt); + //Save to custom location + customImageSave(pipeline, k); k++; } } } +function customImageSave(pipeline, batchCount){ + if(outputDir) { + // Save File + let saveLocation = `${outputDir}/${pipeline.configuration.seed}_${Date.now()}_${batchCount}.png` + console.log(`Saving to ${saveLocation}\n\n`); + canvas.saveImage(saveLocation, true); // save the image currently on canvas to a file. + return saveLocation; + } +} + function computeTotalPromptCount(dynamicPrompt) { let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); let totalCombinationCount = 1; @@ -310,7 +355,10 @@ function computeTotalPromptCount(dynamicPrompt) { function* generatePrompts(dynamicPrompt) { function cartesian(...arrays) { - return arrays.reduce((acc, curr) => acc.flatMap(d => curr.map(e => [d, e].flat()))); + if (arrays.length === 0) return []; + return arrays.reduce((acc, curr) => { + return acc.flatMap(a => curr.map(b => [].concat(a, b))); + }, [[]]); } let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); @@ -327,11 +375,10 @@ function* generatePrompts(dynamicPrompt) { } } - function selectRandomPrompt() { // Generate a random index to select a random prompt const randomIndex = Math.floor(Math.random() * prompts.length); - console.log(`Selected prompt/configuration ${randomIndex} of ${prompts.length}`) + console.log(`Selected dynamic prompt ${randomIndex} of ${prompts.length}`) // Get the randomly selected prompt object const selectedPrompt = prompts[randomIndex]; // Extract prompt string, LoRa filenames, and weights From 153fd18dde8884ba8e08af51c6536775c69fb122 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 18 Oct 2024 02:35:38 -0700 Subject: [PATCH 05/16] Resolving conflicts with main repo --- .../{dynamic_prompts.js => dynamic-prompts.js} | 0 scripts/dynamic-prompts/metadata.json | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename scripts/dynamic-prompts/{dynamic_prompts.js => dynamic-prompts.js} (100%) diff --git a/scripts/dynamic-prompts/dynamic_prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js similarity index 100% rename from scripts/dynamic-prompts/dynamic_prompts.js rename to scripts/dynamic-prompts/dynamic-prompts.js diff --git a/scripts/dynamic-prompts/metadata.json b/scripts/dynamic-prompts/metadata.json index 363ed3f..5f93067 100644 --- a/scripts/dynamic-prompts/metadata.json +++ b/scripts/dynamic-prompts/metadata.json @@ -1,6 +1,6 @@ { - "name": "Dynamic Prompts for Draw Things", + "name": "Dynamic Prompts", "author": "Zanshinmu", - "file": "dynamic_prompts.js", + "file": "dynamic-prompts.js", "description": "Allows dynamic prompts features similar to sd-dynamic-prompts in Draw Things." } From e8a7afd10684947347dda935e342a75e2b74b2b3 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 18 Oct 2024 09:44:57 -0700 Subject: [PATCH 06/16] Fix to remove -1 from filename, disable batchSize > 1 --- scripts/dynamic-prompts/dynamic-prompts.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index 5f8aa34..f6c3c6e 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.4 +// v3.5.5 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** @@ -41,7 +41,7 @@ */ //Version -const versionString = "v3.5.4" +const versionString = "v3.5.5" //Maximum iterations for Iterate Mode const maxIter = 500 //store selected prompt/LoRA data @@ -527,14 +527,13 @@ function render (promptString){ let editedString = replaceWildcards(promptString, categories); let neg; let myConfiguration = configuration; + myConfiguration.batchSize = 1; if (useUiPrompt){ neg = uiNegPrompt; } else { neg = promptData.negativePrompt; if (!overrideModels){ myConfiguration.model = promptData.model; - //Default to random seed, configuration overrides - myConfiguration.seed = -1; //Apply configuration changes, if any myConfiguration = Object.assign(configuration, promptData.configuration); myConfiguration.loras = loraNamestoFiles(promptData.configuration.loras); From 2202615c78fce9f8f0c9cc7058d9a0b9fe04a62a Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 18 Oct 2024 15:32:18 -0700 Subject: [PATCH 07/16] Added image seed control, defaults to increment --- scripts/dynamic-prompts/dynamic-prompts.js | 66 ++++++++++++++++------ 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index f6c3c6e..a6a582c 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.5 +// v3.5.6 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** @@ -41,7 +41,7 @@ */ //Version -const versionString = "v3.5.5" +const versionString = "v3.5.6" //Maximum iterations for Iterate Mode const maxIter = 500 //store selected prompt/LoRA data @@ -237,31 +237,36 @@ const categories = { // UI const categoryNames = Object.keys(categories).join(', '); +const seedOptions = ["Random","Increment","Static"]; const okButton = "Start"; const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; const aboutText = "Selects randomly from " + `${prompts.length} dynamic prompts`+ " located in the script's 'const prompts' object."; + const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { return [ this.section(header, aboutText, [ - this.switch(false, "Enter UI Prompt"), - this.switch(false, "Lock configuration"), - this.switch(false, "Iterate Mode"), - this.switch(true, "Download Models"), - this.slider(10, this.slider.fractional(0), 1, 2000, "batch count"), - ]), - this.section("Output:", "Output rendered images to custom location", [ - this.directory('${filesystem.pictures.path}') + this.section("Seed Mode", "", [this.segmented(1, seedOptions), + this.slider(10, this.slider.fractional(0), 1, 2000, "batch count")]), + this.section("Output:", "Output rendered images to custom location", [ + this.directory(`${filesystem.pictures.path}`)]), + this.switch(false, "Enter UI Prompt"), + this.switch(false, "Lock configuration"), + this.switch(false, "Iterate Mode"), + this.switch(true, "Download Models"), ]) ]; }); + // Parse UI input -let useUiPrompt = userSelection[0][0]; -let overrideModels = userSelection[0][1]; -let iterateMode = userSelection[0][2]; -let downloadModels = userSelection[0][3]; -let batchCount = userSelection[0][4]; -let outputDir = userSelection[1][0]; +const seedMode = userSelection[0][0][0]; +const batchCount = userSelection[0][0][1]; +const outputDir = userSelection[0][1][0]; +const useUiPrompt = userSelection[0][2]; +const overrideModels = userSelection[0][3]; +const iterateMode = userSelection[0][4]; +const downloadModels = userSelection[0][5]; + if (iterateMode){ console.log("Iterate Mode"); @@ -283,7 +288,7 @@ if (useUiPrompt) { }); uiPrompt = userSelection[0][3][0]; } -//console.log(JSON.stringify(userSelection)); + // Get configuration const configuration = pipeline.configuration; @@ -527,7 +532,11 @@ function render (promptString){ let editedString = replaceWildcards(promptString, categories); let neg; let myConfiguration = configuration; + let mySeed = configuration.seed; myConfiguration.batchSize = 1; + // Set seed + myConfiguration.seed = getSeed(mySeed); + if (useUiPrompt){ neg = uiNegPrompt; } else { @@ -550,6 +559,29 @@ function render (promptString){ timer(start); } +function getSeed(oldSeed){ + const MAX_INT_32 = 2147483647; + let seed = 0; + + switch (seedMode) { + case 0: // Random + seed = Math.floor(Math.random() * (MAX_INT_32)); + break; + + case 1: // Iterate + if (seed < MAX_INT_32){ //Wrap if seed exceeds MAX_INT_32 + seed = ++oldSeed; + } + break; + + case 2: // Static + seed = oldSeed; + break; + } + console.log(`${seedOptions[seedMode]} Seed Mode, Seed: ${seed}`); + return seed; +} + function replaceWildcards(promptString, categories) { const wildcardRegex = /{(\w+)(?::(\d+)(?:-(\d+))?)?}/g; From d8bb74d62fe176eac8349bce3dd9fb9288251ee1 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Tue, 22 Oct 2024 14:33:09 -0700 Subject: [PATCH 08/16] Bugfix for prompts loras not setting weights and occasional DT crash. Now overrides UI configuration loras unless 'lock config' is enabled. --- scripts/dynamic-prompts/dynamic-prompts.js | 73 +++++++--------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index a6a582c..91a281e 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -41,13 +41,15 @@ */ //Version -const versionString = "v3.5.6" +const versionString = "v3.5.7"; //Maximum iterations for Iterate Mode -const maxIter = 500 +const maxIter = 500; +const DEBUG = false; //store selected prompt/LoRA data let promptData; let userPrompt = ''; let uiPrompt = ''; + // Default example prompt for UI demonstrating category use const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" @@ -292,7 +294,6 @@ if (useUiPrompt) { // Get configuration const configuration = pipeline.configuration; -const defaultLoras = pipeline.configuration.loras; const uiNegPrompt = pipeline.prompts.negativePrompt; if(useUiPrompt){ @@ -309,7 +310,7 @@ if(useUiPrompt){ if (!iterateMode){ for (let i = 0; i < batchCount; i++){ let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; - console.log(batchCountLog); + console.warn(batchCountLog); render(getPrompt()); //Save to custom location customImageSave(pipeline, i); @@ -323,13 +324,13 @@ if (!iterateMode){ } p = computeTotalPromptCount(dynPrompt); if (p > maxIter){ - console.log(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); - console.log("Reduce the number of categories used in prompt."); + console.warn(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); + console.warn("Reduce the number of categories used in prompt."); } else { let k = 1; console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); for (let generatedPrompt of generatePrompts(dynPrompt)) { - console.log(`iterating render ${k} of ${p}\n${generatedPrompt}\n`); + console.warn(`iterating render ${k} of ${p}\n${generatedPrompt}\n`); render(generatedPrompt); //Save to custom location customImageSave(pipeline, k); @@ -391,10 +392,11 @@ function selectRandomPrompt() { let myneg = selectedPrompt.negativePrompt; let mymodel = selectedPrompt.model; let myconfig = selectedPrompt.configuration; - const loras = selectedPrompt.loras.map(lora => ({ file: lora.file, weight: lora.weight })); - let myloras = getAssociatedLoRas(loras); + //Prepare LoRAs + const myLoras = resolveLoras(selectedPrompt.loras); + const loras = myLoras.map(lora => ({ file: lora.file, weight: lora.weight })); myconfig.model = mymodel; - myconfig.loras=myloras; + myconfig.loras = loras; // Store the promptData object let promptData = { prompt: myprompt, negativePrompt: myneg, configuration: myconfig }; if (downloadModels){ @@ -420,11 +422,17 @@ function getModels(promptData){ pipeline.downloadBuiltins(models); } -function loraNamestoFiles(loras){ +// Resolves whether LoRAs are filenames or +function resolveLoras(loras){ + const FILESUFFIX = ".ckpt"; for (let i = 0; i < loras.length; i++) { - let loraname = loras[i].file; - let lorafile = pipeline.findLoRAByName(loraname); - loras[i]=lorafile; + if (!loras[i].file.endsWith(FILESUFFIX)){ + let myfile = pipeline.findLoRAByName(loras[i].file).file; + if (DEBUG){ + console.log(`Filename ${loras[i].file} resolved to ${JSON.stringify(myfile)}`); + } + loras[i].file = myfile; + } } return loras; } @@ -482,41 +490,6 @@ function getPrompt () { return promptString } -function getAssociatedLoRas(loras) { - if(!overrideModels){ - let validLoras = setLoras(loras); - const mergedLoras = defaultLoras.concat(validLoras); - return mergedLoras - }else{ - return defaultLoras - } -} - -// is name in defaultLoras? -function isDefaultLora(lora){ - for (let i = 0; i < defaultLoras.length; i++) { - if (defaultLoras[i].file === lora.file){ - return true; - } else{ - return false; - } - } -} - - -// Only overwrite valid Loras from prompts -function setLoras (myLoras){ - let loras=[]; - for (let i = 0; i < myLoras.length; i++) { - if (myLoras[i].file !== ''){ - if(!isDefaultLora(myLoras[i])){ - loras.push(myLoras[i]); - } - } - } - return loras -} - function timer (start){ const end = Date.now(); const duration = end - start; @@ -545,7 +518,7 @@ function render (promptString){ myConfiguration.model = promptData.model; //Apply configuration changes, if any myConfiguration = Object.assign(configuration, promptData.configuration); - myConfiguration.loras = loraNamestoFiles(promptData.configuration.loras); + myConfiguration.loras = promptData.configuration.loras; } } //Clear canvas From 71b3ab6dd2529c827a96fe9ca5ac772bc0cd634b Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Tue, 22 Oct 2024 21:27:48 -0700 Subject: [PATCH 09/16] Preliminary fix for missing metadata, cleanup of code --- scripts/dynamic-prompts/dynamic-prompts.js | 93 ++++++++++------------ 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index 91a281e..c250d88 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.6 +// v3.5.7 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** @@ -45,8 +45,8 @@ const versionString = "v3.5.7"; //Maximum iterations for Iterate Mode const maxIter = 500; const DEBUG = false; -//store selected prompt/LoRA data -let promptData; +//store selected prompt data and UI config +let UICONFIG = pipeline.configuration; let userPrompt = ''; let uiPrompt = ''; @@ -291,9 +291,7 @@ if (useUiPrompt) { uiPrompt = userSelection[0][3][0]; } - -// Get configuration -const configuration = pipeline.configuration; +// Store UI negative prompt for later const uiNegPrompt = pipeline.prompts.negativePrompt; if(useUiPrompt){ @@ -311,18 +309,11 @@ if (!iterateMode){ for (let i = 0; i < batchCount; i++){ let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; console.warn(batchCountLog); - render(getPrompt()); - //Save to custom location - customImageSave(pipeline, i); + render(getPrompt(), i); } } else { - let dynprompt; - if (useUiPrompt){ - dynPrompt = userPrompt; - } else { - dynPrompt = getPromptString(selectRandomPrompt()); - } - p = computeTotalPromptCount(dynPrompt); + let promptData = getPrompt(); + p = computeTotalPromptCount(promptData.prompt); if (p > maxIter){ console.warn(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); console.warn("Reduce the number of categories used in prompt."); @@ -330,25 +321,15 @@ if (!iterateMode){ let k = 1; console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); for (let generatedPrompt of generatePrompts(dynPrompt)) { + let myConfig = promptData; + promptData.prompt = dynPrompt; console.warn(`iterating render ${k} of ${p}\n${generatedPrompt}\n`); - render(generatedPrompt); - //Save to custom location - customImageSave(pipeline, k); + render(generatedPrompt, k); k++; } } } -function customImageSave(pipeline, batchCount){ - if(outputDir) { - // Save File - let saveLocation = `${outputDir}/${pipeline.configuration.seed}_${Date.now()}_${batchCount}.png` - console.log(`Saving to ${saveLocation}\n\n`); - canvas.saveImage(saveLocation, true); // save the image currently on canvas to a file. - return saveLocation; - } -} - function computeTotalPromptCount(dynamicPrompt) { let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); let totalCombinationCount = 1; @@ -402,6 +383,7 @@ function selectRandomPrompt() { if (downloadModels){ getModels(promptData); } + console.warn(JSON.stringify(promptData)); return promptData; } @@ -437,11 +419,6 @@ function resolveLoras(loras){ return loras; } -// Function to get the prompt string from the object returned by selectRandomPrompt -function getPromptString(p) { - return p.prompt; -} - // Function to extract and validate category names and their requested item count or range from the uiPrompt function isPromptValid(uiPrompt, categories) { const regex = /{(\w+)(?::(\d+)(?:-(\d+))?)?}/g; // Extended regex to capture syntax variations @@ -479,15 +456,14 @@ function isPromptValid(uiPrompt, categories) { //get prompt each iteration function getPrompt () { + let promptData = {}; if (useUiPrompt) { console.log("Using UI Prompt"); - promptString = userPrompt; + promptData.prompt = userPrompt; } else { promptData = selectRandomPrompt(); - //console.log(promptData); - promptString = getPromptString(promptData); } - return promptString + return promptData; } function timer (start){ @@ -500,36 +476,51 @@ function timer (start){ } // Run pipeline -function render (promptString){ +function render (promptData, batchCount){ + // start timer let start = Date.now(); - let editedString = replaceWildcards(promptString, categories); + // set generated prompt + let generatedPrompt = replaceWildcards(promptData.prompt, categories); let neg; - let myConfiguration = configuration; - let mySeed = configuration.seed; - myConfiguration.batchSize = 1; - // Set seed - myConfiguration.seed = getSeed(mySeed); + let finalConfiguration = {}; + // Set seed according to user selection + let mySeed = getSeed(pipeline.configuration.seed); + console.log(JSON.stringify(UICONFIG)); if (useUiPrompt){ + finalConfiguration = UICONFIG; + finalConfiguration.loras = UICONFIG.loras; neg = uiNegPrompt; } else { - neg = promptData.negativePrompt; + neg = promptData.negativePrompt; if (!overrideModels){ - myConfiguration.model = promptData.model; //Apply configuration changes, if any - myConfiguration = Object.assign(configuration, promptData.configuration); - myConfiguration.loras = promptData.configuration.loras; + finalConfiguration = Object.assign(UICONFIG, promptData.configuration); + finalConfiguration.loras = promptData.configuration.loras; + console.log(finalConfiguration.model); } } + // Batch > 1 is no bueno + finalConfiguration.batchSize = 1; + finalConfiguration.seed = mySeed; //Clear canvas canvas.clear(); + console.warn(JSON.stringify(finalConfiguration)); pipeline.run({ - configuration: myConfiguration, - prompt: editedString, + configuration: finalConfiguration, + prompt: generatedPrompt, negativePrompt: neg }); //Output render time elapsed timer(start); + //Save Image if enabled + if(outputDir) { + // working around metadata bug by forcing our config onto the UI before saving + pipeline.configuration = finalConfiguration; + let savePath = `${outputDir}/${finalConfiguration.seed}_${Date.now()}_${batchCount}.png` + console.log(`Saving to ${savePath}\n\n`); + canvas.saveImage(savePath, true); // save the image currently on canvas to a file. + } } function getSeed(oldSeed){ From 2846afb5cc905bccfa25124efb70a15caad4628a Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Thu, 24 Oct 2024 16:58:51 -0700 Subject: [PATCH 10/16] Cleaned up iteration logic to handle edge cases. Added preliminary support for customizing saved filenames. LoRA lookup bugfixes. --- scripts/dynamic-prompts/dynamic-prompts.js | 212 ++++++++++++++++----- 1 file changed, 166 insertions(+), 46 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index c250d88..ec409b6 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -41,16 +41,15 @@ */ //Version -const versionString = "v3.5.7"; +const versionString = "v3.5.8"; //Maximum iterations for Iterate Mode const maxIter = 500; const DEBUG = false; //store selected prompt data and UI config -let UICONFIG = pipeline.configuration; let userPrompt = ''; let uiPrompt = ''; - -// Default example prompt for UI demonstrating category use +const UICONFIG = pipeline.configuration; +//Default example prompt for UI demonstrating category use const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" /* These are the prompts randomly selected from if UI Prompt isn't valid. @@ -309,11 +308,12 @@ if (!iterateMode){ for (let i = 0; i < batchCount; i++){ let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; console.warn(batchCountLog); - render(getPrompt(), i); + render(getDynamicPrompt(), i); } } else { - let promptData = getPrompt(); - p = computeTotalPromptCount(promptData.prompt); + const promptData = getDynamicPrompt(); + const dynPrompt = promptData.prompt; + p = computeTotalPromptCount(dynPrompt); if (p > maxIter){ console.warn(`Max iterations of ${maxIter} exceeded: Prompt total combinations = ${p}\n`); console.warn("Reduce the number of categories used in prompt."); @@ -331,34 +331,105 @@ if (!iterateMode){ } function computeTotalPromptCount(dynamicPrompt) { - let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); - let totalCombinationCount = 1; + // Find all unique top-level placeholders in the prompt + const regex = /{(\w+)}/g; + let match; + let placeholders = new Set(); + while ((match = regex.exec(dynamicPrompt)) !== null) { + placeholders.add(match[1]); + } + + // If no placeholders, return 1 + if (placeholders.size === 0) { + return 1; + } + + let totalCombinations = 1; + for (let placeholder of placeholders) { - const valueCount = categories[placeholder].length; // Get the number of values in each category - totalCombinationCount *= valueCount; // Multiply the counts to calculate the maximum possible number of combinations + const optionsCount = countPlaceholderOptions(placeholder, new Set()); + totalCombinations *= optionsCount; } - return totalCombinationCount; + + return totalCombinations; +} + +function countPlaceholderOptions(placeholder, seenPlaceholders) { + if (seenPlaceholders.has(placeholder)) { + throw new Error(`Circular reference detected for placeholder '{${placeholder}}'`); + } + + seenPlaceholders.add(placeholder); + + const values = categories[placeholder]; + if (!values) { + throw new Error(`Category '${placeholder}' not defined.`); + } + + let totalOptions = 0; + + for (let value of values) { + const optionsCount = countValueOptions(value, new Set(seenPlaceholders)); + totalOptions += optionsCount; + } + + seenPlaceholders.delete(placeholder); + + return totalOptions; +} + +function countValueOptions(value, seenPlaceholders) { + // Find all placeholders in the value + const regex = /{(\w+)}/g; + let match; + let placeholders = new Set(); + while ((match = regex.exec(value)) !== null) { + placeholders.add(match[1]); + } + + // If no placeholders, return 1 + if (placeholders.size === 0) { + return 1; + } + + let totalCombinations = 1; + + for (let placeholder of placeholders) { + const optionsCount = countPlaceholderOptions(placeholder, seenPlaceholders); + totalCombinations *= optionsCount; + } + + return totalCombinations; } function* generatePrompts(dynamicPrompt) { - function cartesian(...arrays) { - if (arrays.length === 0) return []; - return arrays.reduce((acc, curr) => { - return acc.flatMap(a => curr.map(b => [].concat(a, b))); - }, [[]]); + // Base case: if no placeholders, yield the prompt + if (!/{\w+}/.test(dynamicPrompt)) { + yield dynamicPrompt; + return; + } + + // Find the first placeholder in the prompt + const regex = /{(\w+)}/g; + const match = regex.exec(dynamicPrompt); + + if (!match) { + yield dynamicPrompt; + return; + } + + const placeholder = match[1]; + + const values = categories[placeholder]; + if (!values) { + throw new Error(`Category '${placeholder}' not defined.`); } - let placeholders = dynamicPrompt.match(/{(\w+)}/g).map(p => p.replace(/[{}]/g, '')); - let validPlaceholders = placeholders.filter(p => categories[p]); - let categoryValues = validPlaceholders.map(p => categories[p]); - let combinations = cartesian(...categoryValues); - - for (let combination of combinations) { - let prompt = dynamicPrompt; - validPlaceholders.forEach((placeholder, i) => { - prompt = prompt.replace(`{${placeholder}}`, combination[i]); - }); - yield prompt; + for (const value of values) { + // Replace all occurrences of the placeholder with the value + const newPrompt = dynamicPrompt.replace(new RegExp(`{${placeholder}}`, 'g'), value); + // Recursively generate prompts for the new prompt + yield* generatePrompts(newPrompt); } } @@ -383,7 +454,9 @@ function selectRandomPrompt() { if (downloadModels){ getModels(promptData); } - console.warn(JSON.stringify(promptData)); + if (DEBUG){ + console.warn(JSON.stringify(promptData)); + } return promptData; } @@ -404,19 +477,24 @@ function getModels(promptData){ pipeline.downloadBuiltins(models); } -// Resolves whether LoRAs are filenames or +// Resolves LoRAs to filename function resolveLoras(loras){ const FILESUFFIX = ".ckpt"; + const myLoras = []; for (let i = 0; i < loras.length; i++) { - if (!loras[i].file.endsWith(FILESUFFIX)){ - let myfile = pipeline.findLoRAByName(loras[i].file).file; - if (DEBUG){ - console.log(`Filename ${loras[i].file} resolved to ${JSON.stringify(myfile)}`); + let myLora = loras[i]; + let myname = myLora.file; + if (!myname.endsWith(FILESUFFIX)){ + let myfile = pipeline.findLoRAByName(myname).file; + // Assign resolved name + myLora.file = myfile; + myLoras.push(myLora); + if (DEBUG){ + console.log(`Filename ${myname} resolved to ${myfile}`); + } } - loras[i].file = myfile; } - } - return loras; + return myLoras; } // Function to extract and validate category names and their requested item count or range from the uiPrompt @@ -455,7 +533,7 @@ function isPromptValid(uiPrompt, categories) { } //get prompt each iteration -function getPrompt () { +function getDynamicPrompt () { let promptData = {}; if (useUiPrompt) { console.log("Using UI Prompt"); @@ -482,10 +560,12 @@ function render (promptData, batchCount){ // set generated prompt let generatedPrompt = replaceWildcards(promptData.prompt, categories); let neg; - let finalConfiguration = {}; + let finalConfiguration = Object.create(pipeline.configuration); // Set seed according to user selection let mySeed = getSeed(pipeline.configuration.seed); - console.log(JSON.stringify(UICONFIG)); + if (DEBUG){ + console.log(JSON.stringify(finalConfiguration)); + } if (useUiPrompt){ finalConfiguration = UICONFIG; @@ -497,15 +577,21 @@ function render (promptData, batchCount){ //Apply configuration changes, if any finalConfiguration = Object.assign(UICONFIG, promptData.configuration); finalConfiguration.loras = promptData.configuration.loras; - console.log(finalConfiguration.model); + if (DEBUG){ + console.log(finalConfiguration.model); + } } } // Batch > 1 is no bueno finalConfiguration.batchSize = 1; finalConfiguration.seed = mySeed; - //Clear canvas + canvas.clear(); - console.warn(JSON.stringify(finalConfiguration)); + + if(DEBUG){ + console.warn(JSON.stringify(finalConfiguration)); + } + pipeline.run({ configuration: finalConfiguration, prompt: generatedPrompt, @@ -514,15 +600,49 @@ function render (promptData, batchCount){ //Output render time elapsed timer(start); //Save Image if enabled + savetoImageDir(finalConfiguration, batchCount); +} + +function savetoImageDir(config, batchCount){ if(outputDir) { - // working around metadata bug by forcing our config onto the UI before saving - pipeline.configuration = finalConfiguration; - let savePath = `${outputDir}/${finalConfiguration.seed}_${Date.now()}_${batchCount}.png` + // worked around metadata bug by forcing our config onto the UI before saving + // but that didn't work consistently so trying something else + // Crazy thing is the metadata is borked with canvas.save, but fine + // if you reload the image in DT + const SamplerTypeReverse = Object.fromEntries( + Object.entries(SamplerType).map(([key, value]) => [value, key]) + ); + const seed = config.seed; + const model = new String(sanitize(config.model.replace('.ckpt'))).slice(0, 8); + const sampler = sanitize(SamplerTypeReverse[config.sampler]).slice(0, 8); + const steps = config.steps; + const time = getTimeString(); + let savePath = `${outputDir}/${model}_${sampler}_${steps}_${time}_${batchCount}.png` console.log(`Saving to ${savePath}\n\n`); canvas.saveImage(savePath, true); // save the image currently on canvas to a file. } } +function sanitize(text) { + // Replace characters that are not allowed in filenames with a hyphen + return text + .trim() // Remove leading/trailing whitespace + .toLowerCase() // Convert to lowercase for consistency + .replace(/[\/\\?%*:|"<>]/g, '-') // Replace invalid characters + .replace(/\s+/g, '_'); // Replace spaces with underscores +} + +function getTimeString() { + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); // Months are zero-based + const day = String(now.getDate()).padStart(2, '0'); + const hours = String(now.getHours()).padStart(2, '0'); + const minutes = String(now.getMinutes()).padStart(2, '0'); + + return `${year}${month}${day}${hours}${minutes}`; +} + function getSeed(oldSeed){ const MAX_INT_32 = 2147483647; let seed = 0; From 702698015eba7ffbf377b47eed0c76afe51b24f0 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 25 Oct 2024 04:24:38 -0700 Subject: [PATCH 11/16] bugfixes for prompts objects missing fields --- scripts/dynamic-prompts/dynamic-prompts.js | 70 ++++++++++++++-------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index ec409b6..5876e4d 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.7 +// v3.5.8 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** @@ -95,13 +95,12 @@ const prompts = [ { prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", - model: "RealVisXL v4.0", + model: "FLUX.1 [schnell] (8-bit)", loras: [ - { file: "Fix Hands Slider", weight: 0.3 }, - { file: "Pixel Art XL v1.1", weight: 0.4 } - ], + { file: "AntiBlur v1.0 [dev]", weight: 0.4} + ], configuration: - {width:1024,height:1024,steps:28,sampler:0,guidanceScale:4.0} + {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} }, { prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", @@ -440,23 +439,31 @@ function selectRandomPrompt() { // Get the randomly selected prompt object const selectedPrompt = prompts[randomIndex]; // Extract prompt string, LoRa filenames, and weights - let myprompt = selectedPrompt.prompt; - let myneg = selectedPrompt.negativePrompt; - let mymodel = selectedPrompt.model; - let myconfig = selectedPrompt.configuration; + const myprompt = selectedPrompt.prompt; + const myneg = selectedPrompt.negativePrompt; + const mymodel = selectedPrompt.model; + const loras = []; + let myconfig = {}; + if (typeof selectedPrompt.configuration !== 'undefined'){ + myconfig = selectedPrompt.configuration; + } else { + myconfig = {...UICONFIG}; + } //Prepare LoRAs const myLoras = resolveLoras(selectedPrompt.loras); - const loras = myLoras.map(lora => ({ file: lora.file, weight: lora.weight })); + if (typeof myLoras !== 'undefined'){ + myconfig.loras = myLoras.map(lora => ({ file: lora.file, weight: lora.weight })); + } myconfig.model = mymodel; - myconfig.loras = loras; // Store the promptData object - let promptData = { prompt: myprompt, negativePrompt: myneg, configuration: myconfig }; + const promptData = { prompt: myprompt, negativePrompt: myneg, configuration: myconfig }; if (downloadModels){ getModels(promptData); } if (DEBUG){ console.warn(JSON.stringify(promptData)); } + //throw new Error("BUTTS"); return promptData; } @@ -464,14 +471,16 @@ function selectRandomPrompt() { function getModels(promptData){ let models = []; //Model first - models.push(promptData.configuration.model); - //Initiate Download - pipeline.downloadBuiltins(models); + if (typeof promptData.configuration.model !== 'undefined'){ + models.push(promptData.configuration.model); + } //Loras - for (let i = 0; i < promptData.configuration.loras.length; i++) { - let lora = promptData.configuration.loras[i].file; - models.push(lora); + if (typeof promptData.configuration.loras !== 'undefined'){ + for (let i = 0; i < promptData.configuration.loras.length; i++) { + let lora = promptData.configuration.loras[i].file; + models.push(lora); + } } //Initiate Download pipeline.downloadBuiltins(models); @@ -479,12 +488,15 @@ function getModels(promptData){ // Resolves LoRAs to filename function resolveLoras(loras){ + if (!loras){ + return loras; + } const FILESUFFIX = ".ckpt"; const myLoras = []; for (let i = 0; i < loras.length; i++) { let myLora = loras[i]; let myname = myLora.file; - if (!myname.endsWith(FILESUFFIX)){ + if (!myname.endsWith(FILESUFFIX) && myname){ let myfile = pipeline.findLoRAByName(myname).file; // Assign resolved name myLora.file = myfile; @@ -613,8 +625,11 @@ function savetoImageDir(config, batchCount){ Object.entries(SamplerType).map(([key, value]) => [value, key]) ); const seed = config.seed; - const model = new String(sanitize(config.model.replace('.ckpt'))).slice(0, 8); - const sampler = sanitize(SamplerTypeReverse[config.sampler]).slice(0, 8); + let model = "undefined"; + if (typeof config.model !== 'undefined'){ + model = sanitize(config.model.replace('.ckpt')); + } + const sampler = sanitize(SamplerTypeReverse[config.sampler]); const steps = config.steps; const time = getTimeString(); let savePath = `${outputDir}/${model}_${sampler}_${steps}_${time}_${batchCount}.png` @@ -624,12 +639,19 @@ function savetoImageDir(config, batchCount){ } function sanitize(text) { + // Handle null case + if (typeof text === 'undefined'){ + let undef = "undefined"; + return undef; + } + const trunc = 16; // Replace characters that are not allowed in filenames with a hyphen return text .trim() // Remove leading/trailing whitespace .toLowerCase() // Convert to lowercase for consistency - .replace(/[\/\\?%*:|"<>]/g, '-') // Replace invalid characters - .replace(/\s+/g, '_'); // Replace spaces with underscores + .replace(new RegExp('[\/\\\\?%*:|"<>[]{}$#@&+;,.~()]', 'g'), '-') // Comprehensive now + .replace(/\s+/g, '_') // Replace spaces with underscores + .slice(0, trunc) // Truncate to trunc chars; } function getTimeString() { From 9eab12e016c07bb17148093e9e5b85ad39caaedf Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 25 Oct 2024 15:10:45 -0700 Subject: [PATCH 12/16] Added debug printer class, final bugfixes for release --- scripts/dynamic-prompts/dynamic-prompts.js | 102 ++++++++++++++++----- 1 file changed, 77 insertions(+), 25 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index 5876e4d..c2aac30 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -42,9 +42,61 @@ //Version const versionString = "v3.5.8"; -//Maximum iterations for Iterate Mode +//Maximum iterations for Iterate Mode, this is a good value for everything +//Macs with more resources can probably set this much higher const maxIter = 500; + +//Sure, you can turn this on if you like your console cluttered. :P const DEBUG = false; + +class DebugPrint { + static Level = Object.freeze({ + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR' + }); + + #debugMode; // Private field to store debug mode state + + /** + * Constructor for DebugPrint + * @param {boolean} [debugMode=false] - Whether to enable debug printing by default + */ + constructor(debugMode = false) { + this.#debugMode = debugMode; + } + + /** + * Prints a message if debug mode is enabled + * @param {string} message - The message to print + * @param {DebugPrint.Level} [level=DebugPrint.Level.INFO] - The log level of the message + */ + print(message, level = DebugPrint.Level.INFO) { + if (!Object.values(DebugPrint.Level).includes(level)) { + throw new Error(`Invalid log level: ${level}`); + } + + if (this.#debugMode) { + switch (level) { + case DebugPrint.Level.INFO: + console.log(`${message}`); + break; + + case DebugPrint.Level.WARN: + console.warn(`${message}`); + break; + + case DebugPrint.Level.ERROR: + console.error(`${message}`); + break; + + } + } + } +} + +// Initiate debug logger, set state to DEBUG; +const debug = new DebugPrint(DEBUG); //store selected prompt data and UI config let userPrompt = ''; let uiPrompt = ''; @@ -83,7 +135,7 @@ const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" const prompts = [ { - prompt: "{colors:1-3} dominant {camera} shot of a {adjective} cyborg woman, {style}, {hairstyle} {haircolor} hair, {features}, {pose} in {weather} {time} {locale}", + prompt: "{colors:1-3} dominant {camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "FLUX.1 [schnell] (8-bit)", loras: [ @@ -93,7 +145,7 @@ const prompts = [ {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} }, { - prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", + prompt: "{camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "FLUX.1 [schnell] (8-bit)", loras: [ @@ -103,7 +155,7 @@ const prompts = [ {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} }, { - prompt: "{camera} shot of a {adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}, {pose} in {weather} {time} {locale}", + prompt: "{camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "RealVisXL v4.0", loras: [ @@ -120,6 +172,10 @@ const prompts = [ // Categories definition const categories = { + cyborg: [ + "{adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}", + "{adjective} cyborg woman, {hairstyle} {haircolor} hair, {features} wearing {style}" + ], time: [ "morning", "noon", @@ -272,6 +328,8 @@ if (iterateMode){ console.log("Iterate Mode"); } + + if (useUiPrompt) { const userSelection = requestFromUser("Dynamic Prompts: UI Prompt", okButton, function() { return [ @@ -460,10 +518,7 @@ function selectRandomPrompt() { if (downloadModels){ getModels(promptData); } - if (DEBUG){ - console.warn(JSON.stringify(promptData)); - } - //throw new Error("BUTTS"); + debug.print(JSON.stringify(promptData), DebugPrint.Level.WARN); return promptData; } @@ -482,13 +537,18 @@ function getModels(promptData){ models.push(lora); } } - //Initiate Download - pipeline.downloadBuiltins(models); + if (!pipeline.areModelsDownloaded(models)){ + //Initiate Download + debug.print(`${JSON.stringify(models)} not clean, initiating download.`, DebugPrint.Level.WARN); + pipeline.downloadBuiltins(models); + } else { + debug.print(`${JSON.stringify(models)} clean, not initiating download.`, DebugPrint.Level.WARN); + } } // Resolves LoRAs to filename function resolveLoras(loras){ - if (!loras){ + if (typeof loras === 'undefined'){ return loras; } const FILESUFFIX = ".ckpt"; @@ -496,15 +556,13 @@ function resolveLoras(loras){ for (let i = 0; i < loras.length; i++) { let myLora = loras[i]; let myname = myLora.file; - if (!myname.endsWith(FILESUFFIX) && myname){ + if (!myname.endsWith(FILESUFFIX)){ let myfile = pipeline.findLoRAByName(myname).file; // Assign resolved name myLora.file = myfile; - myLoras.push(myLora); - if (DEBUG){ - console.log(`Filename ${myname} resolved to ${myfile}`); - } + debug.print(`Filename ${myname} resolved to ${myfile}`); } + myLoras.push(myLora); } return myLoras; } @@ -575,9 +633,7 @@ function render (promptData, batchCount){ let finalConfiguration = Object.create(pipeline.configuration); // Set seed according to user selection let mySeed = getSeed(pipeline.configuration.seed); - if (DEBUG){ - console.log(JSON.stringify(finalConfiguration)); - } + debug.print(JSON.stringify(finalConfiguration)); if (useUiPrompt){ finalConfiguration = UICONFIG; @@ -589,9 +645,7 @@ function render (promptData, batchCount){ //Apply configuration changes, if any finalConfiguration = Object.assign(UICONFIG, promptData.configuration); finalConfiguration.loras = promptData.configuration.loras; - if (DEBUG){ - console.log(finalConfiguration.model); - } + debug.print(finalConfiguration.model); } } // Batch > 1 is no bueno @@ -600,9 +654,7 @@ function render (promptData, batchCount){ canvas.clear(); - if(DEBUG){ - console.warn(JSON.stringify(finalConfiguration)); - } + debug.print(JSON.stringify(finalConfiguration), DebugPrint.Level.WARN); pipeline.run({ configuration: finalConfiguration, From f87f43d6ccdf9fac12b3b16641629de5e413d8c4 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Fri, 15 Nov 2024 14:12:44 -0800 Subject: [PATCH 13/16] Fixed model downloading. Added total rendering time. --- scripts/dynamic-prompts/dynamic-prompts.js | 76 ++++++++++++---------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index c2aac30..9b97527 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,7 +1,7 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.8 +// v3.5.9 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** @@ -41,7 +41,7 @@ */ //Version -const versionString = "v3.5.8"; +const versionString = "v3.5.9"; //Maximum iterations for Iterate Mode, this is a good value for everything //Macs with more resources can probably set this much higher const maxIter = 500; @@ -362,11 +362,14 @@ if(useUiPrompt){ // Main batch loop if (!iterateMode){ + const bstart = Date.now(); + const bmessage = "✔︎ Total render time ‣"; for (let i = 0; i < batchCount; i++){ let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; console.warn(batchCountLog); render(getDynamicPrompt(), i); } + elapsed(bstart, message = bmessage); } else { const promptData = getDynamicPrompt(); const dynPrompt = promptData.prompt; @@ -377,6 +380,8 @@ if (!iterateMode){ } else { let k = 1; console.log(`Iterating over dynamic prompt:\n '${dynPrompt}'\n Total combinations number ${p}.`); + const istart = Date.now(); + const imessage = "✔︎ Total iteration time ‣"; for (let generatedPrompt of generatePrompts(dynPrompt)) { let myConfig = promptData; promptData.prompt = dynPrompt; @@ -384,6 +389,7 @@ if (!iterateMode){ render(generatedPrompt, k); k++; } + elapsed(istart, message = imessage); } } @@ -512,41 +518,26 @@ function selectRandomPrompt() { if (typeof myLoras !== 'undefined'){ myconfig.loras = myLoras.map(lora => ({ file: lora.file, weight: lora.weight })); } - myconfig.model = mymodel; + myconfig.model = resolveModel(mymodel); // Store the promptData object const promptData = { prompt: myprompt, negativePrompt: myneg, configuration: myconfig }; - if (downloadModels){ - getModels(promptData); - } - debug.print(JSON.stringify(promptData), DebugPrint.Level.WARN); + debug.print(JSON.stringify(promptData), DebugPrint.Level.WARN); return promptData; } -//Download models if necessary -function getModels(promptData){ - let models = []; - //Model first - if (typeof promptData.configuration.model !== 'undefined'){ - models.push(promptData.configuration.model); - } - - //Loras - if (typeof promptData.configuration.loras !== 'undefined'){ - for (let i = 0; i < promptData.configuration.loras.length; i++) { - let lora = promptData.configuration.loras[i].file; - models.push(lora); - } - } - if (!pipeline.areModelsDownloaded(models)){ - //Initiate Download - debug.print(`${JSON.stringify(models)} not clean, initiating download.`, DebugPrint.Level.WARN); - pipeline.downloadBuiltins(models); +function resolveModel(model){ + if(downloadModels){ + let myNameArray = []; + myNameArray.push(model); + pipeline.downloadBuiltins(myNameArray); + debug.print(`Model ${model} downloaded`); } else { - debug.print(`${JSON.stringify(models)} clean, not initiating download.`, DebugPrint.Level.WARN); + debug.print('Download models disabled.'); } + return model; } -// Resolves LoRAs to filename +// Resolves LoRAs to filenames function resolveLoras(loras){ if (typeof loras === 'undefined'){ return loras; @@ -557,13 +548,24 @@ function resolveLoras(loras){ let myLora = loras[i]; let myname = myLora.file; if (!myname.endsWith(FILESUFFIX)){ + try{ let myfile = pipeline.findLoRAByName(myname).file; // Assign resolved name myLora.file = myfile; - debug.print(`Filename ${myname} resolved to ${myfile}`); + debug.print(`Filename ${myname} resolved to ${JSON.stringify(myfile)}`); + } catch (e){ + debug.print(`${e} Is it downloaded?`); + if(downloadModels){ + let myNameArray = []; + myNameArray.push(myname); + myLora.file = pipeline.downloadBuiltins(myNameArray); + } else { + myLora.file = myname; + } } - myLoras.push(myLora); } + myLoras.push(myLora); + } return myLoras; } @@ -614,13 +616,16 @@ function getDynamicPrompt () { return promptData; } -function timer (start){ +function elapsed (start, message = "✔︎ Render time ‣"){ const end = Date.now(); const duration = end - start; - const minutes = Math.floor(duration / 60000); + const hours = Math.floor(duration / 3600000); + const minutes = Math.floor((duration % 3600000) / 60000); // calculate the remaining minutes after subtracting the elapsed hours from the total duration let seconds = Math.floor((duration % 60000) / 1000); - seconds = seconds < 10 ? '0' + seconds : seconds; - console.log(`✔︎ Render time ‣ ${minutes}:${seconds}\n`); + const formattedHours = String(hours).padStart(2, '0'); + const formattedMinutes = String(minutes).padStart(2, '0'); + const formattedSeconds = String(seconds).padStart(2, '0'); + console.log(`${message} ${formattedHours}:${formattedMinutes}:${formattedSeconds}\n`); } // Run pipeline @@ -655,14 +660,13 @@ function render (promptData, batchCount){ canvas.clear(); debug.print(JSON.stringify(finalConfiguration), DebugPrint.Level.WARN); - pipeline.run({ configuration: finalConfiguration, prompt: generatedPrompt, negativePrompt: neg }); //Output render time elapsed - timer(start); + elapsed(start); //Save Image if enabled savetoImageDir(finalConfiguration, batchCount); } From 26be12dc4f28025706d5fd5332544dc6581de46f Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Tue, 19 Nov 2024 17:04:29 -0800 Subject: [PATCH 14/16] Preliminary fix for undefined LoRA error. Moved prompts and categories objects to a more non-dev friendly part of the script --- scripts/dynamic-prompts/dynamic-prompts.js | 135 +++++++++++---------- 1 file changed, 70 insertions(+), 65 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index 9b97527..e7b4491 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,11 +1,11 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.9 +// v3.5.9.1 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** - * Documentation for "Dynamic Prompts" Script (Version 3.5.4) for "Draw Things" + * Documentation for "Dynamic Prompts" Script (Version 3.5.9.1) for "Draw Things" * * This script generates dynamic prompts for the "Draw Things" application. Customize it to enhance your creative experience. * @@ -40,71 +40,10 @@ * To reduce the numbers, eliminate categories from your prompt. */ -//Version -const versionString = "v3.5.9"; -//Maximum iterations for Iterate Mode, this is a good value for everything -//Macs with more resources can probably set this much higher -const maxIter = 500; - -//Sure, you can turn this on if you like your console cluttered. :P -const DEBUG = false; - -class DebugPrint { - static Level = Object.freeze({ - INFO: 'INFO', - WARN: 'WARN', - ERROR: 'ERROR' - }); - - #debugMode; // Private field to store debug mode state - - /** - * Constructor for DebugPrint - * @param {boolean} [debugMode=false] - Whether to enable debug printing by default - */ - constructor(debugMode = false) { - this.#debugMode = debugMode; - } - - /** - * Prints a message if debug mode is enabled - * @param {string} message - The message to print - * @param {DebugPrint.Level} [level=DebugPrint.Level.INFO] - The log level of the message - */ - print(message, level = DebugPrint.Level.INFO) { - if (!Object.values(DebugPrint.Level).includes(level)) { - throw new Error(`Invalid log level: ${level}`); - } - - if (this.#debugMode) { - switch (level) { - case DebugPrint.Level.INFO: - console.log(`${message}`); - break; - - case DebugPrint.Level.WARN: - console.warn(`${message}`); - break; - - case DebugPrint.Level.ERROR: - console.error(`${message}`); - break; - - } - } - } -} - -// Initiate debug logger, set state to DEBUG; -const debug = new DebugPrint(DEBUG); -//store selected prompt data and UI config -let userPrompt = ''; -let uiPrompt = ''; -const UICONFIG = pipeline.configuration; //Default example prompt for UI demonstrating category use const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" -/* These are the prompts randomly selected from if UI Prompt isn't valid. +/* Following are the prompts randomly selected from if UI Prompt mode isn't enabled. Modify prompts to provide dynamic prompts with LoRAs which will be randomly selected from if a valid dynamic prompt is not found in the UI. As of 3.0.1 there can be a 'configuration' object for each prompt which contains arbitrary settings which will be passed on to the pipeline at runtime. These settings correspond to the possible values of pipeline.configuration @@ -170,7 +109,7 @@ const prompts = [ // Empty file will be ignored ]; -// Categories definition +// Categories definition: This is where you add your dynamic categories. const categories = { cyborg: [ "{adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}", @@ -291,6 +230,68 @@ const categories = { ] }; +//Version +const versionString = "v3.5.9.1"; +//Maximum iterations for Iterate Mode, this is a good value for everything +//Macs with more resources can probably set this much higher +const maxIter = 500; + +//Sure, you can turn this on if you like your console cluttered. :P +const DEBUG = false; + +class DebugPrint { + static Level = Object.freeze({ + INFO: 'INFO', + WARN: 'WARN', + ERROR: 'ERROR' + }); + + #debugMode; // Private field to store debug mode state + + /** + * Constructor for DebugPrint + * @param {boolean} [debugMode=false] - Whether to enable debug printing by default + */ + constructor(debugMode = false) { + this.#debugMode = debugMode; + } + + /** + * Prints a message if debug mode is enabled + * @param {string} message - The message to print + * @param {DebugPrint.Level} [level=DebugPrint.Level.INFO] - The log level of the message + */ + print(message, level = DebugPrint.Level.INFO) { + if (!Object.values(DebugPrint.Level).includes(level)) { + throw new Error(`Invalid log level: ${level}`); + } + + if (this.#debugMode) { + switch (level) { + case DebugPrint.Level.INFO: + console.log(`${message}`); + break; + + case DebugPrint.Level.WARN: + console.warn(`${message}`); + break; + + case DebugPrint.Level.ERROR: + console.error(`${message}`); + break; + + } + } + } +} + +// Initiate debug logger, set state to DEBUG; +const debug = new DebugPrint(DEBUG); +//store selected prompt data and UI config +let userPrompt = ''; +let uiPrompt = ''; +const UICONFIG = pipeline.configuration; + // UI const categoryNames = Object.keys(categories).join(', '); const seedOptions = ["Random","Increment","Static"]; @@ -547,6 +548,10 @@ function resolveLoras(loras){ for (let i = 0; i < loras.length; i++) { let myLora = loras[i]; let myname = myLora.file; + if (typeof myname === 'undefined'){ + debug.print("Empty LoRA", DebugPrint.level.WARN); + continue; + } if (!myname.endsWith(FILESUFFIX)){ try{ let myfile = pipeline.findLoRAByName(myname).file; From ac1e078173a6d52af6c2d26fdee440230eb28ab3 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Sat, 23 Nov 2024 10:51:13 -0800 Subject: [PATCH 15/16] Fixed typo that was hanging script. Added debug switch to UI. --- scripts/dynamic-prompts/dynamic-prompts.js | 89 +++++++++++----------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index e7b4491..0ad0790 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,11 +1,11 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.9.1 +// v3.5.9.2 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** - * Documentation for "Dynamic Prompts" Script (Version 3.5.9.1) for "Draw Things" + * Documentation for "Dynamic Prompts" Script (Version 3.5.9.2) for "Draw Things" * * This script generates dynamic prompts for the "Draw Things" application. Customize it to enhance your creative experience. * @@ -231,14 +231,51 @@ const categories = { }; //Version -const versionString = "v3.5.9.1"; +const versionString = "v3.5.9.2"; //Maximum iterations for Iterate Mode, this is a good value for everything //Macs with more resources can probably set this much higher const maxIter = 500; -//Sure, you can turn this on if you like your console cluttered. :P -const DEBUG = false; +//store selected prompt data and UI config +let userPrompt = ''; +let uiPrompt = ''; +const UICONFIG = pipeline.configuration; + +// UI +const categoryNames = Object.keys(categories).join(', '); +const seedOptions = ["Random","Increment","Static"]; +const okButton = "Start"; +const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; +const aboutText = "Selects randomly from " + `${prompts.length} dynamic prompts`+ + " located in the script's 'const prompts' object."; + +const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { + return [ + this.section(header, aboutText, [ + this.section("Seed Mode", "", [this.segmented(1, seedOptions), + this.slider(10, this.slider.fractional(0), 1, 2000, "batch count")]), + this.section("Output:", "Output rendered images to custom location", [ + this.directory(`${filesystem.pictures.path}`)]), + this.switch(false, "Enter UI Prompt"), + this.switch(false, "Lock configuration"), + this.switch(false, "Iterate Mode"), + this.switch(true, "Download Models"), + this.switch(false, "Debug") + ]) + ]; +}); +// Parse UI input +const seedMode = userSelection[0][0][0]; +const batchCount = userSelection[0][0][1]; +const outputDir = userSelection[0][1][0]; +const useUiPrompt = userSelection[0][2]; +const overrideModels = userSelection[0][3]; +const iterateMode = userSelection[0][4]; +const downloadModels = userSelection[0][5]; +const DEBUG = userSelection[0][6]; + +//DEBUG CLASS class DebugPrint { static Level = Object.freeze({ INFO: 'INFO', @@ -287,50 +324,11 @@ class DebugPrint { // Initiate debug logger, set state to DEBUG; const debug = new DebugPrint(DEBUG); -//store selected prompt data and UI config -let userPrompt = ''; -let uiPrompt = ''; -const UICONFIG = pipeline.configuration; - -// UI -const categoryNames = Object.keys(categories).join(', '); -const seedOptions = ["Random","Increment","Static"]; -const okButton = "Start"; -const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; -const aboutText = "Selects randomly from " + `${prompts.length} dynamic prompts`+ - " located in the script's 'const prompts' object."; - -const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { - return [ - this.section(header, aboutText, [ - this.section("Seed Mode", "", [this.segmented(1, seedOptions), - this.slider(10, this.slider.fractional(0), 1, 2000, "batch count")]), - this.section("Output:", "Output rendered images to custom location", [ - this.directory(`${filesystem.pictures.path}`)]), - this.switch(false, "Enter UI Prompt"), - this.switch(false, "Lock configuration"), - this.switch(false, "Iterate Mode"), - this.switch(true, "Download Models"), - ]) - ]; -}); - -// Parse UI input -const seedMode = userSelection[0][0][0]; -const batchCount = userSelection[0][0][1]; -const outputDir = userSelection[0][1][0]; -const useUiPrompt = userSelection[0][2]; -const overrideModels = userSelection[0][3]; -const iterateMode = userSelection[0][4]; -const downloadModels = userSelection[0][5]; - if (iterateMode){ console.log("Iterate Mode"); } - - if (useUiPrompt) { const userSelection = requestFromUser("Dynamic Prompts: UI Prompt", okButton, function() { return [ @@ -541,6 +539,7 @@ function resolveModel(model){ // Resolves LoRAs to filenames function resolveLoras(loras){ if (typeof loras === 'undefined'){ + debug.print("Undefined Loras"); return loras; } const FILESUFFIX = ".ckpt"; @@ -549,7 +548,7 @@ function resolveLoras(loras){ let myLora = loras[i]; let myname = myLora.file; if (typeof myname === 'undefined'){ - debug.print("Empty LoRA", DebugPrint.level.WARN); + debug.print("Empty LoRA", DebugPrint.Level.WARN); continue; } if (!myname.endsWith(FILESUFFIX)){ From e30d2abfd2b8a4fd2492d397cd6bd8898ffa8479 Mon Sep 17 00:00:00 2001 From: David Van de Ven Date: Wed, 12 Mar 2025 10:35:32 -0700 Subject: [PATCH 16/16] Adding 3.7.1 final for release --- scripts/dynamic-prompts/dynamic-prompts.js | 671 ++++++++++++++------- 1 file changed, 458 insertions(+), 213 deletions(-) diff --git a/scripts/dynamic-prompts/dynamic-prompts.js b/scripts/dynamic-prompts/dynamic-prompts.js index 0ad0790..c7fc1f8 100644 --- a/scripts/dynamic-prompts/dynamic-prompts.js +++ b/scripts/dynamic-prompts/dynamic-prompts.js @@ -1,79 +1,107 @@ //@api-1.0 // dynamic prompts // author: zanshinmu -// v3.5.9.2 +// v3.7.1 // Discord Thread for Dynamic Prompts: // https://discord.com/channels/1038516303666876436/1207467278426177736 /** - * Documentation for "Dynamic Prompts" Script (Version 3.5.9.2) for "Draw Things" + * Dynamic Prompts Script Documentation (Version 3.7.1) for Draw Things * - * This script generates dynamic prompts for the "Draw Things" application. Customize it to enhance your creative experience. + * OVERVIEW: + * This script enables dynamic prompt generation in the Draw Things application. + * Users can customize the script to create diverse and inspiring prompts. * - * Modifying Categories: - * - Categories are thematic elements like 'Locale', 'Adjective', etc. - * - To add a new category: Include it in the 'categories' object (e.g., "Futuristic": ["cybernetic", "AI-driven"]). - * - To modify an existing category: Add or remove elements directly in the category's list. + * CORE FEATURES: * - * Dynamic Prompt Strings: - * - The Prompts string structures your generated prompts. - * - Use curly braces {} to include category elements. Examples: - * - Single element: "{Locale}" might generate "neon-lit city." - * - Multiple elements: "{Adjective:2}" can give "rainy, bustling." - * - Random range: "{Object:1-3}" could return "hovercar" or "hovercar, android, neon sign." + * 1. Categories + * - Thematic elements like 'Locale', 'Adjective', etc. + * - Add new categories in 'categories' object: + * Example: "Futuristic": ["cybernetic", "AI-driven"] + * - Modify existing categories by editing their lists * - * BatchCount: - * - BatchCount sets the number of prompts to generate. - * - Change its value with the slider to control output (e.g., `BatchCount = 15;` for fifteen prompts). + * 2. Dynamic Prompt Syntax + * Use curly braces {} to include category elements: + * - Single element: {Locale} → "neon-lit city" + * - Multiple elements: {Adjective:2} → "rainy, bustling" + * - Random range: {Object:1-3} → "hovercar" or "hovercar, android, neon sign" * - * Customize your script to create diverse and inspiring prompts for "Draw Things." + * 3. Prompts Object + * - Contains prompts randomly selected when UI Prompt mode is disabled + * - Each prompt can have a configuration object with pipeline settings + * - Example structure: + * { + * prompt: "wide shot of {weather} {locale}", + * label: "Landscape Scene", // Added in 3.6: friendly name + * configuration: { + * guidanceScale: [2, 4], // Random value between 2-4 + * sampler: 1, // IMPORTANT: Must use number, not name + * steps: 20 + * } + * } * - * User Interface: - * - Use UI Prompt: - * Process the prompt in the UI box instead of selecting random prompts. - - * - Lock configuration: - * When selecting random prompts, do not change configurations. - - * - Iterate Mode: - * Iterated generation of all combinations of dynamic prompt. Best used with UI Prompt. - * BatchCount is not used in Iterate mode. Iterate mode can create very large numbers. - * To reduce the numbers, eliminate categories from your prompt. + * 4. Configuration Settings + * IMPORTANT: For sampler settings, you may use numbers or names + * (see Sampler Types table below) + * + * Numeric Values (3.6): + * - Two numbers = random range + * Example: guidanceScale:[2,4] → random between 2-4 + * - Three+ numbers = random selection + * Example: guidanceScale:[2, 3, 3.2, 4] → picks one value + * + * Other Settings: + * - clipLText/openClipGText: Support wildcards if enabled + * - label: Provides friendly name for prompts (added in 3.6) + * + * 5. User Interface Controls + * + * Main Options: + * - UI Prompt: Process prompts from UI box instead of random selection + * - Lock Configuration: Prevents config changes, random selection only generates prompts + * - Override Canvas: Customize canvas size, HRF, steps + * + * Generation Settings: + * - RenderCount: Controls number of prompts generated (adjust via slider) + * - Iterate Mode: + * • Generates all prompt combinations + * • Best used with UI Prompt + * • RenderCount ignored + * • Can generate large numbers; reduce by limiting categories + * + * System Options: + * - Download Models: Auto-downloads referenced models (default: enabled) + * - Debug Mode: Prints diagnostic information to console + * + * 6. Seed Modes + * - Random: Generates new random seed + * - Increment: Adds 1 to existing seed + * - Static: Maintains same seed + * + * 7. Output Options + * - Save rendered images to specified directory + * - Note: 'Saved Generated Images' setting is separate + * - Future updates will allow custom output filenames + * - Not compatible with batch size > 1 + * + * 8. Sampler Types + * Use these numbers or the names in configuration + * + * DPMPP_2M_KARRAS: 0 EULER_A: 1 + * DDIM: 2 PLMS: 3 + * DPMPP_SDE_KARRAS: 4 UNI_PC: 5 + * LCM: 6 EULER_A_SUBSTEP: 7 + * DPMPP_SDE_SUBSTEP: 8 TCD: 9 + * EULER_A_TRAILING: 10 DPMPP_SDE_TRAILING: 11 + * DPMPP_2M_AYS: 12 EULER_A_AYS: 13 + * DPMPP_SDE_AYS: 14 DPMPP_2M_TRAILING: 15 + * DDIM_TRAILING: 16 */ - //Default example prompt for UI demonstrating category use -const defaultPrompt = "wide-angle shot of {weather} {time} {locale}" - -/* Following are the prompts randomly selected from if UI Prompt mode isn't enabled. - Modify prompts to provide dynamic prompts with LoRAs which will be randomly selected from if a valid dynamic prompt is not found in the UI. - - As of 3.0.1 there can be a 'configuration' object for each prompt which contains arbitrary settings which will be passed on to the pipeline at runtime. These settings correspond to the possible values of pipeline.configuration - - There is an important caveat: Sampler is tricky because it looks like a string with the sampler name but it's actually an integer from a lookup table, so you want to find the number that corresponds to the desired sampler and use that instead (without quotes). - - Here is the current sampler lookup table: - SamplerType = { - DPMPP_2M_KARRAS: 0, - EULER_A: 1, - DDIM: 2, - PLMS: 3, - DPMPP_SDE_KARRAS: 4, - UNI_PC: 5, - LCM: 6, - EULER_A_SUBSTEP: 7, - DPMPP_SDE_SUBSTEP: 8, - TCD: 9, - EULER_A_TRAILING: 10, - DPMPP_SDE_TRAILING: 11, - DPMPP_2M_AYS: 12, - EULER_A_AYS: 13, - DPMPP_SDE_AYS: 14, - DPMPP_2M_TRAILING: 15, - DDIM_TRAILING: 16 -} - */ - +const defaultPrompt = "wide-angle shot of {weather} {time} {locale}"; +//Prompts object containing dynamic prompts and configurations const prompts = [ { + label: "Schnell photo dynamic colors", prompt: "{colors:1-3} dominant {camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "FLUX.1 [schnell] (8-bit)", @@ -81,9 +109,11 @@ const prompts = [ { file: "AntiBlur v1.0 [dev]", weight: 0.4} ], configuration: - {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} + {width:1152,height:896,steps:2,sampler:"EULER_A_TRAILING",guidanceScale:[2.5,4.5],separateClipL:true, + clipLText:"Photograph, sensual, professional {cliptones}",resolutionDependentShift:true} }, { + label: "Schnell photo", prompt: "{camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "FLUX.1 [schnell] (8-bit)", @@ -91,9 +121,11 @@ const prompts = [ { file: "AntiBlur v1.0 [dev]", weight: 0.4} ], configuration: - {width:1152,height:896,steps:2,sampler:10,guidanceScale:2.5,clipLText:"Photograph, sensual, professional",resolutionDependentShift:true} + {width:1152,height:896,steps:2,sampler:"EULER_A_TRAILING",guidanceScale:[2.5,4.5],separateClipL:true, + clipLText:"Photograph, sensual, professional {cliptones}",resolutionDependentShift:true} }, { + label: "RVXL4.0 photo", prompt: "{camera} shot of a {cyborg}, {pose} in {weather} {time} {locale}", negativePrompt: "NSFW, nude, blurry, 3d, drawing, anime", model: "RealVisXL v4.0", @@ -102,7 +134,7 @@ const prompts = [ { file: "Pixel Art XL v1.1", weight: 0.4 } ], configuration: - {width:1024,height:1024,steps:28,sampler:0,guidanceScale:4.0} + {width:1024,height:1024,steps:[28,20,32],sampler:"DPMPP_2M_KARRAS",guidanceScale:[2.5,4.5]} } // Add more prompt objects as needed @@ -111,127 +143,166 @@ const prompts = [ // Categories definition: This is where you add your dynamic categories. const categories = { - cyborg: [ + "cyborg": [ "{adjective} cyborg man, {hairstyle} {haircolor} hair, {features}, wearing {malestyle}", "{adjective} cyborg woman, {hairstyle} {haircolor} hair, {features} wearing {style}" - ], - time: [ + ], + "time": [ "morning", "noon", "night", "midnight", "sunset", "sunrise" - ], - camera: [ - "full body", - "medium", - "close_up", - "establishing" - ], - locale: [ - "cityscape", - "street", - "alley", - "marketplace", - "industrial zone", - "waterfront", - "forest", - "rooftop", - "bridge", - "park" - ], - weather: [ - "rainy", - "smoggy", - "stormy", - "dusty" - ], - adjective: [ - "beautiful", - "moody", - "cute", - "tired", - "injured", - "cyborg", - "muscular" - ], - style: [ - "cyberpunk street", "dark gothic", "military armor", "elegant kimono", "eveningwear dress", "corpo uniform", "tech bodysuit" - ], - hairstyle: [ - "long", - "medium", - "shaved", - "short", - "wet", - "punk" - ], - haircolor: [ - "red", - "blonde", - "colored", - "brunette", - "natural", - "streaked" - ], - colors: [ - "red", - "blue", - "green", - "yellow", - "orange", - "purple", - "pink", - "brown", - "black", - "white", - "gray", - "cyan", - "magenta", - "teal", - "olive", - "maroon", - "navy", - "lime", - "indigo", - "gold", - "silver", - "bronze", - "salmon", - "coral", - "turquoise", - "peach", - "lavender", - "emerald", - "ruby", - "sapphire" - ], - features: [ - "{colors} glowing cyborg eyes", - "smoking", - "tech brella with {colors} glowing accents", - "cyborg arm", - "cyborg legs", - "optics implant", - "tech goggles" - ], - pose: [ - "action pose", - "poses", - "model pose", - "relaxing", - "sitting" - ], - malestyle: [ - "leather", - "denim", - "silk", - ] + ], + "camera": [ + "full body", + "medium", + "close_up", + "establishing" + ], + "locale": [ + "A neon-lit skyline with towering skyscrapers, holographic billboards flicker above rain-slicked streets where autonomous vehicles glide past crowds dressed in futuristic fashion.", + "A neon-lit street pulses amidst towering skyscrapers with holographic ads, flanked by enigmatic alleys where rogue androids gather.", + "A neon-lit alley pulses with holographic ads and shadowy figures navigating rain-drenched cobblestones beneath towering skyscrapers.", + "A neon-lit bazaar thrives with life, dominated by towering holographic billboards and bustling vendors selling futuristic gadgets amid the hum of hover vehicles and shadowy figures exchanging encrypted data.", + "An expansive cyberpunk metropolis illuminated by neon lights, featuring towering industrial edifices and bustling harbors shrouded in perpetual twilight.", + "A vibrant riverside vista with skyscrapers casting holographic ads onto an artificial waterway, enveloped by relentless rain, capturing the dynamic spirit of a bustling cyberpunk metropolis.", + "A sprawling cyberpunk metropolis bathes under the glow of neon lights, where towering skyscrapers and holographic billboards illuminate rain-drenched streets.", + "A neon-lit rooftop overlooks an urban sprawl, with skyscrapers boasting holographic ads and drones navigating through smoggy skies.", + "A neon-lit bridge spans over the rain-soaked metropolis, alive with holographic billboards and bustling autonomous vehicles.", + "A neon-drenched urban expanse where towering holographic billboards dominate rain-slicked streets, alive with augmented reality ads and robotic vendors beneath a sky teeming with drones.", + "A bustling neon-lit metropolis where skyscrapers glisten with holographic ads and rain-slick streets hum with hovercars navigating through crowded alleys.", + "A vibrant neon-lit metropolis where towering skyscrapers and digital billboards cast an electric glow over the bustling, gritty streets awash with advanced technology.", + "Under a rain-soaked neon-lit skyline, towering skyscrapers and digital advertisements illuminate bustling streets where autonomous vehicles navigate past rogue androids mingling with futuristic fashion crowds at the vibrant bazaar beneath holographic billboards.", + "'A neon-drenched avenue buzzes with life beneath skyscrapers adorned with digital displays, where augmented reality ads blend seamlessly among futuristic fashionistas and rain-slicked streets pulse under towering buildings illuminated by holographic billboards amidst autonomous vehicles navigating through crowds in cybernetic attire.'", + "A neon-lit plaza buzzes beneath towering skyscrapers, where holographic displays flicker over rain-slicked streets filled with autonomous vehicles and futuristic fashionistas.", + "A neon-soaked boulevard thrums beneath towering skyscrapers and flickering holographic displays, where autonomous vehicles glide past avant-garde fashion-clad crowds on rain-drenched streets pulsing with life under glowing billboards; sleek hovercars weave through the futuristic cityscape adorned by cybernetic pedestrians.", + "A neon-lit avenue beneath skyscrapers draped in digital displays pulses with energy, where augmented reality ads mingle among futuristic fashionistas on rain-slicked streets alive with the hum of autonomous vehicles weaving through crowds clad in cybernetic attire under a canopy of towering buildings and holographic billboards.", + "A neon-drenched boulevard pulses beneath towering skyscrapers, alive with flickering holographic displays and sleek autonomous vehicles gliding past avant-garde crowds, while rain-slicked streets hum under glowing billboards where hovercars weave through futuristic cityscapes teeming with cybernetic pedestrians.", + "A vibrant, rain-slicked skyline with towering skyscrapers and digital advertisements cast neon hues over bustling streets filled with autonomous vehicles; nearby illuminated alleys pulse beneath looming towers where rogue androids mingle among crowds dressed in futuristic fashion at a buzzing bazaar under holographic billboards.", + "Neon lights bathe the rain-slicked streets beneath skyscrapers adorned with digital billboards and autonomous vehicles weaving through crowds clad in futuristic fashion, while holographic displays flicker above sleek hovercars gliding past cybernetic pedestrians in avant-garde attire." + ], + "weather": [ + "rainy", + "smoggy", + "stormy", + "dusty" + ], + "adjective": [ + "beautiful", + "moody", + "cute", + "tired", + "injured", + "cyborg", + "muscular" + ], + "style": [ + "cyberpunk street", + "dark gothic", + "military armor", + "elegant kimono", + "eveningwear dress", + "corpo uniform", + "tech bodysuit" + ], + "hairstyle": [ + "long", + "medium", + "shaved", + "short", + "wet", + "punk" + ], + "haircolor": [ + "red", + "blonde", + "colored", + "brunette", + "natural", + "streaked" + ], + "colors": [ + "red", + "blue", + "green", + "yellow", + "orange", + "purple", + "pink", + "brown", + "black", + "white", + "gray", + "cyan", + "magenta", + "teal", + "olive", + "maroon", + "navy", + "lime", + "indigo", + "gold", + "silver", + "bronze", + "salmon", + "coral", + "turquoise", + "peach", + "lavender", + "emerald", + "ruby", + "sapphire" + ], + "features": [ + "{colors} glowing cyborg eyes", + "smoking", + "tech brella with {colors} glowing accents", + "cyborg arm", + "cyborg legs", + "optics implant", + "tech goggles" + ], + "pose": [ + "action pose", + "poses", + "model pose", + "relaxing", + "sitting" + ], + "malestyle": [ + "leather", + "denim", + "silk" + ], + "cliptones": [ + "Shrouded in perpetual twilight", + "Overtaken by an eerie, pulsing gloom", + "Bathed in the glow of ominous neon", + "A tapestry of light and cutting-edge innovation", + "Where steel and circuitry meet the sky", + "Pulsating with the rhythm of progress", + "Scarred by the remnants of a forgotten era", + "A labyrinth of rust and neglected dreams", + "Shadows dance upon crumbling facades", + "Electric with the buzz of underground resistance", + "Graffiti-scarred walls whisper tales of defiance", + "The underbelly of society, exposed and raw", + "Veiled in a mist of intrigue and mystery", + "Where neon illusions beckon the brave", + "Secrets hide in every shadowy alleyway", + "A kaleidoscope of chaos, sound, and fury", + "Sensory overload in a sea of humanity", + "The city's frenetic heartbeat is palpable", + "Echoes of abandonment in empty streets", + "Nature's slow reclaiming of forgotten zones", + "Silence hangs heavy, punctuated by the wind" + ] }; //Version -const versionString = "v3.5.9.2"; +const versionString = "v3.7.1"; //Maximum iterations for Iterate Mode, this is a good value for everything //Macs with more resources can probably set this much higher const maxIter = 500; @@ -244,6 +315,7 @@ const UICONFIG = pipeline.configuration; // UI const categoryNames = Object.keys(categories).join(', '); const seedOptions = ["Random","Increment","Static"]; +const batchOptions=["1","2","3","4"]; const okButton = "Start"; const header = "Dynamic Prompts " + `${versionString}` + " by zanshinmu"; const aboutText = "Selects randomly from " + `${prompts.length} dynamic prompts`+ @@ -253,11 +325,13 @@ const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { return [ this.section(header, aboutText, [ this.section("Seed Mode", "", [this.segmented(1, seedOptions), - this.slider(10, this.slider.fractional(0), 1, 2000, "batch count")]), + this.slider(10, this.slider.fractional(0), 1, 2000, "render count")]), + this.section("Batch Size", "", [this.segmented(0, batchOptions)]), this.section("Output:", "Output rendered images to custom location", [ this.directory(`${filesystem.pictures.path}`)]), this.switch(false, "Enter UI Prompt"), this.switch(false, "Lock configuration"), + this.switch(false, "Override Canvas"), this.switch(false, "Iterate Mode"), this.switch(true, "Download Models"), this.switch(false, "Debug") @@ -267,13 +341,20 @@ const userSelection = requestFromUser("Dynamic Prompts", okButton, function() { // Parse UI input const seedMode = userSelection[0][0][0]; -const batchCount = userSelection[0][0][1]; -const outputDir = userSelection[0][1][0]; -const useUiPrompt = userSelection[0][2]; -const overrideModels = userSelection[0][3]; -const iterateMode = userSelection[0][4]; -const downloadModels = userSelection[0][5]; -const DEBUG = userSelection[0][6]; +const renderCount = userSelection[0][0][1]; +const batchCount = userSelection[0][1][0]+1; +const outputDir = userSelection[0][2][0]; +const useUiPrompt = userSelection[0][3]; +const overrideModels = userSelection[0][4]; +const overrideCanvas = userSelection[0][5]; +const iterateMode = userSelection[0][6]; +const downloadModels = userSelection[0][7]; +const DEBUG = userSelection[0][8]; + +//Throw error if batch size > 1 with save to directory +if (batchCount > 1 && outputDir){ + throw new Error("Batch size over 1 is incompatible with save to directory."); +} //DEBUG CLASS class DebugPrint { @@ -329,6 +410,64 @@ if (iterateMode){ console.log("Iterate Mode"); } +// Data for Canvas Override +const CANVAS_OVERRIDE = { + "canvas": [ + { + "pass": "2", + "width": 768, + "height": 768 + }, + { + "pass": "1", + "width": 960, + "height": 768 + } + ], + "config": { + "hrf": false, + "strength": 0.42, + "steps": 0 + } +}; + +if (overrideCanvas) { + const userSelection = requestFromUser("Dynamic Prompts: Override Canvas", "Go!", function() { + return [ + this.section( + "Hi-Res Fix", + "Set 2nd pass resolution", + [ + this.size(1280, 1024), + this.switch(false, "HRF"), + this.slider(.42, this.slider.percent, .01, 1, "strength"), + ] + ), + this.section( + "1st pass resolution", + "Set first pass resoltion", + [ + this.size(768, 768) + ] + ), + this.section( + "Set rendering steps", + "Higher than 0 enables override", + [ + this.slider(0, this.slider.fractional(0), 1, 150, "Steps") + ] + ) + ]; + }); + CANVAS_OVERRIDE["canvas"][1].width = userSelection[1][0].width; + CANVAS_OVERRIDE["canvas"][1].height = userSelection[1][0].height; + CANVAS_OVERRIDE["canvas"][0].width = userSelection[0][0].width; + CANVAS_OVERRIDE["canvas"][0].height = userSelection[0][0].height; + CANVAS_OVERRIDE["config"].hrf = userSelection[0][1]; + CANVAS_OVERRIDE["config"].strength = userSelection[0][2]; + CANVAS_OVERRIDE["config"].steps = userSelection[2][0]; +} + if (useUiPrompt) { const userSelection = requestFromUser("Dynamic Prompts: UI Prompt", okButton, function() { return [ @@ -336,7 +475,7 @@ if (useUiPrompt) { this.section("{category}","Replaces category with random item", []), this.section("{category:2}","Replaces category with 2 random items", []), this.section("{category:1-3}","Replaces category with 1 to 3 random items", []), - this.section("UI Prompt", "Modify the dynamic prompt to your desire", [ + this.section("UI Prompt", "Modify the dynamic prompt to your desire", [ this.textField(defaultPrompt, pipeline.prompts.prompt, true, 80), this.section("Available Categories", categoryNames, []) ]) @@ -363,9 +502,9 @@ if(useUiPrompt){ if (!iterateMode){ const bstart = Date.now(); const bmessage = "✔︎ Total render time ‣"; - for (let i = 0; i < batchCount; i++){ - let batchCountLog = `Rendering ${i + 1} of ${batchCount}`; - console.warn(batchCountLog); + for (let i = 0; i < renderCount; i++){ + let renderCountLog = `Rendering ${i + 1} of ${renderCount}`; + console.warn(renderCountLog); render(getDynamicPrompt(), i); } elapsed(bstart, message = bmessage); @@ -498,9 +637,13 @@ function* generatePrompts(dynamicPrompt) { function selectRandomPrompt() { // Generate a random index to select a random prompt const randomIndex = Math.floor(Math.random() * prompts.length); - console.log(`Selected dynamic prompt ${randomIndex} of ${prompts.length}`) // Get the randomly selected prompt object const selectedPrompt = prompts[randomIndex]; + if (typeof selectedPrompt.label !== 'undefined'){ + console.log(`Selected dynamic prompt ${randomIndex} of ${prompts.length}: '${selectedPrompt.label}'`); + } else { + console.log(`Selected dynamic prompt ${randomIndex} of ${prompts.length}`); + } // Extract prompt string, LoRa filenames, and weights const myprompt = selectedPrompt.prompt; const myneg = selectedPrompt.negativePrompt; @@ -508,7 +651,7 @@ function selectRandomPrompt() { const loras = []; let myconfig = {}; if (typeof selectedPrompt.configuration !== 'undefined'){ - myconfig = selectedPrompt.configuration; + myconfig = processDynConfig(selectedPrompt.configuration); } else { myconfig = {...UICONFIG}; } @@ -524,6 +667,71 @@ function selectRandomPrompt() { return promptData; } +function dynamicClipText(config){ + if (typeof config.separateClipL !=='undefined' && config.separateClipL){ + if (typeof config.clipLText !=='undefined'){ + config.clipLText = replaceWildcards(config.clipLText, categories); + debug.print(`Dynamic ClipL: ${config.clipLText}\n`); + } + } + if (typeof config.separateOpenClipG !=='undefined' && config.separateOpenClipG){ + if (typeof config.openClipGText !=='undefined'){ + config.openClipGText = replaceWildcards(config.openClipGText, categories); + debug.print(`Dynamic ClipG: ${config.openClipGText}\n`); + } + } + return config; +} + +function processDynConfig(config) { + // Loop through each key-value pair in the configuration object + for (const [key, value] of Object.entries(config)) { + if (Array.isArray(value) && value.every(isNumeric)) { // Check if the value is a numeric array + if (value.length === 2) { // Array with 2 elements: treat as range + const [min, max] = value; + + // Validation for numeric range + if (!isValidNumericRange(min, max)) { + debug.print(`Invalid numeric range for '${key}': [${min}, ${max}]`); + continue; + } + + // Generate random value within the range, preserving type (int/float) + const generatedValue = getRandomValueInRange(min, max); + config[key] = generatedValue; // Replace array with the generated random value + } else { // Array with 3 or more elements: select one randomly + if (!value.every(isNumeric)) { + debug.print(`Non-numeric values in array for '${key}': ${value}`); + continue; + } + + const selectedValue = value[Math.floor(Math.random() * value.length)]; + config[key] = selectedValue; // Replace array with the selected random value + } + } + } + return dynamicClipText(config); // Return the modified configuration object +} + +// Helper function to validate if a value is numeric (int or float) +function isNumeric(value) { + return typeof value === 'number' && !isNaN(value); +} + +// Helper function to validate a numeric range (ensures both ends are numeric and min <= max) +function isValidNumericRange(min, max) { + return isNumeric(min) && isNumeric(max) && min <= max; +} + +// Helper function to generate a random value within a range, preserving the type (int/float) +function getRandomValueInRange(min, max) { + if (Number.isInteger(min) && Number.isInteger(max)) { // Both ends are integers + return Math.floor(Math.random() * (max - min + 1)) + min; + } else { // Floats involved, generate a random float within the range, capped at 2 decimals + return Math.round((Math.random() * (max - min) + min) * 100) / 100; + } +} + function resolveModel(model){ if(downloadModels){ let myNameArray = []; @@ -633,7 +841,7 @@ function elapsed (start, message = "✔︎ Render time ‣"){ } // Run pipeline -function render (promptData, batchCount){ +function render (promptData, renderCount){ // start timer let start = Date.now(); // set generated prompt @@ -654,13 +862,17 @@ function render (promptData, batchCount){ //Apply configuration changes, if any finalConfiguration = Object.assign(UICONFIG, promptData.configuration); finalConfiguration.loras = promptData.configuration.loras; + finalConfiguration.sampler = resolveSampler(promptData.configuration.sampler); debug.print(finalConfiguration.model); } } - // Batch > 1 is no bueno - finalConfiguration.batchSize = 1; + finalConfiguration.batchSize = batchCount; finalConfiguration.seed = mySeed; + if (overrideCanvas){ + finalConfiguration = canvasOverride(finalConfiguration); + } + canvas.clear(); debug.print(JSON.stringify(finalConfiguration), DebugPrint.Level.WARN); @@ -672,30 +884,63 @@ function render (promptData, batchCount){ //Output render time elapsed elapsed(start); //Save Image if enabled - savetoImageDir(finalConfiguration, batchCount); -} - -function savetoImageDir(config, batchCount){ - if(outputDir) { - // worked around metadata bug by forcing our config onto the UI before saving - // but that didn't work consistently so trying something else - // Crazy thing is the metadata is borked with canvas.save, but fine - // if you reload the image in DT - const SamplerTypeReverse = Object.fromEntries( - Object.entries(SamplerType).map(([key, value]) => [value, key]) - ); - const seed = config.seed; - let model = "undefined"; - if (typeof config.model !== 'undefined'){ - model = sanitize(config.model.replace('.ckpt')); - } - const sampler = sanitize(SamplerTypeReverse[config.sampler]); - const steps = config.steps; - const time = getTimeString(); - let savePath = `${outputDir}/${model}_${sampler}_${steps}_${time}_${batchCount}.png` - console.log(`Saving to ${savePath}\n\n`); - canvas.saveImage(savePath, true); // save the image currently on canvas to a file. + savetoImageDir(finalConfiguration, renderCount); +} + +function canvasOverride(finalConfiguration){ + if (CANVAS_OVERRIDE["config"].steps){ + finalConfiguration.steps = CANVAS_OVERRIDE["config"].steps; + } + finalConfiguration.hiresFixStrength = CANVAS_OVERRIDE["config"].strength; + finalConfiguration.hiresFix = CANVAS_OVERRIDE["config"].hrf; + finalConfiguration.height = CANVAS_OVERRIDE["canvas"][0].height; + finalConfiguration.width = CANVAS_OVERRIDE["canvas"][0].width; + finalConfiguration.hiresFixHeight = CANVAS_OVERRIDE["canvas"][1].height; + finalConfiguration.hiresFixWidth = CANVAS_OVERRIDE["canvas"][1].width; + debug.print(`Canvas Override: ${JSON.stringify(CANVAS_OVERRIDE)}\n`); + console.log("Canvas Override Active\n"); + return finalConfiguration; +} + +function savetoImageDir(config, renderCount){ + if(outputDir && batchCount > 1) { + //BatchSize 1 is normal, save just the image + saveImage(config, renderCount); + } + // The workaround implementation was a failure +} + +function resolveSampler(sampler){ + if (Number.isInteger(sampler) && sampler >= 0 && sampler < Object.keys(SamplerType).length ) { + const samplerString = Object.keys(SamplerType).find(key => SamplerType[key] === sampler); + debug.print(`Sampler ${sampler} resolved to: ${samplerString}`); + return sampler; // + } else if (typeof sampler === 'string') { + // Handle string case + const index = SamplerType[sampler.toUpperCase()]; + debug.print(`Sampler ${sampler} resolved to: ${index}`); + return index; + } else { + debug.print("Error: sampler not resolved"); + return undefined; + } +} + +function saveImage(config, renderCount){ + const SamplerTypeReverse = Object.fromEntries( + Object.entries(SamplerType).map(([key, value]) => [value, key]) + ); + const seed = config.seed; + let model = "undefined"; + if (typeof config.model !== 'undefined'){ + model = sanitize(config.model.replace('.ckpt')); } + const sampler = sanitize(SamplerTypeReverse[config.sampler]); + const steps = config.steps; + const time = getTimeString(); + const savePath = `${outputDir}/${model}_${sampler}_${steps}_${time}_${renderCount}.png` + console.log(`Saving to ${savePath}\n\n`); + canvas.saveImage(savePath, true); // save the image currently on canvas to a file. } function sanitize(text) {