/*
Copyright 2022 juenbug12851
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* @file
* @brief Pipeline stage: expand #name tokens via createRequire plugin loading (v1 / user-submitted, auto-fx/artists, danbooru substitution). Notes: notes/reference/prompt-dsl.md.
*/
import { createRequire } from "node:module";
// Dynamic prompt files are resolved by config-driven path and expanded
// synchronously (inside string-replace callbacks). Node 24 can `require()` ES
// modules synchronously, so a scoped require is the right tool for this
// plugin-style loading. The catalog lives under the repo-root data/ folder (an
// intentional exception to "code lives in src/" — these are prompt content);
// this file is in src/prompt-modules/, so the require base is `../../data/`.
const require = createRequire(import.meta.url);
/**
* When an anime/danbooru keyword dict is active, rewrite a trailing `, Person` to
* `{d-person}` so the anime character list is used instead of the generic person list.
* @param {string} prompt The prompt fragment.
* @param {object} settings The merged generation settings (`keywordsFilename`).
* @returns {string} The (possibly) substituted prompt.
*/
// Some keywords are better converted to danbooru if danbooru is in effect
function danbooruReplacer(prompt, settings) {
if (
settings.keywordsFilename == false ||
(!String(settings.keywordsFilename).startsWith("d/") && settings.keywordsFilename != "danbooru")
)
return prompt;
// Person (Case-Insensitive) followed by word-boundry, replace with
// {d-person} and carry over word boundry
prompt = prompt.replaceAll(/, ?Person/gim, "{d/person}");
return prompt;
}
/**
* Map a `user-`-prefixed dynamic-prompt name to its `user-submitted/` path.
* @param {string} name The dynamic-prompt key.
* @returns {string} The resolved path key.
*/
function convertToPath(name) {
if (!name.startsWith("user-")) return name;
name = name.replace("user-", "");
return `user-submitted/${name}`;
}
/**
* Load and run a v2 dynamic-prompt plugin (`#name`), applying danbooru substitution.
* @param {string} name The dynamic-prompt name (a trailing `-v2` is stripped).
* @param {object} settings The merged generation settings.
* @param {object} imageSettings The image settings.
* @param {object} upscaleSettings The upscale settings.
* @returns {string} The generated prompt fragment.
*/
function expandDynamicPromptV2(name, settings, imageSettings, upscaleSettings) {
// Remove -v2
name = name.replace("-v2", "");
// Read expansion file contents
return danbooruReplacer(
require(`../../data/${settings.dynamicPromptFiles}/${convertToPath(name)}`).default(
settings,
imageSettings,
upscaleSettings,
),
settings,
);
}
/**
* Load and run a frozen v1 dynamic-prompt plugin (`#name-v1`); v1 bakes in fx and
* artists, so auto-add is forced off.
* @param {string} name The dynamic-prompt name (a trailing `-v1` is stripped).
* @param {object} settings The merged generation settings.
* @param {object} imageSettings The image settings.
* @param {object} upscaleSettings The upscale settings.
* @returns {string} The generated prompt fragment.
*/
function expandDynamicPromptV1(name, settings, imageSettings, upscaleSettings) {
// V1 already includes these
settings.autoAddFx = false;
settings.autoAddArtists = false;
// Remove -v1
name = name.replace("-v1", "");
// Read expansion file contents
return danbooruReplacer(
require(`../../data/${settings.dynamicPromptFiles}/v1/${convertToPath(name)}`).default(
settings,
imageSettings,
upscaleSettings,
),
settings,
);
}
/**
* Dynamic-prompt pipeline stage: expand every `#name` token by running its plugin
* (recursively up to 10 passes; `-v1` and `user-` variants supported), then
* auto-append `#fx` / `#artists` when configured. Snapshots `imageSettings.origPostPrompt`.
* @param {string} prompt The prompt after the first expansion pass.
* @param {object} settings The merged generation settings.
* @param {object} imageSettings Image settings; receives `origPostPrompt` and auto-include flags.
* @param {object} [upscaleSettings] Passed through to plugins.
* @returns {string} The prompt with all dynamic prompts expanded.
*/
export default function (prompt, settings, imageSettings, upscaleSettings) {
// Check for these before expansion
const includedArtists =
prompt.includes("#artists") ||
prompt.includes("artist") || // In case someone uses an artist list but not dyn prompt
imageSettings.autoIncludedArtists;
const includedFx = prompt.includes("#fx") || imageSettings.autoIncludedFx;
// Max iterations in case of infinite loops
let maxCount = 10;
// Keep expanding expansions up to max levels
for (let i = 0; i < maxCount && /#([\w\-_]+)/gm.test(prompt); i++) {
prompt = prompt.replaceAll(/#([\w\-_]+)/gm, function (match, p1) {
if (p1.endsWith("-v1"))
return expandDynamicPromptV1(p1, settings, imageSettings, upscaleSettings);
else return expandDynamicPromptV2(p1, settings, imageSettings, upscaleSettings);
});
}
// Auto-append fx and artists if cofnigured to do so
// We do this afterwards because some modules may change the settings
// so we have to rprocess the modules first
// Auto-add fx first if requested to do so
if (settings.autoAddFx && !includedFx) {
prompt += `, ${expandDynamicPromptV2("fx", settings, imageSettings, upscaleSettings)}`;
imageSettings.autoIncludedFx = true;
}
// Auto-add artists second if requested to do so
if (settings.autoAddArtists && !includedArtists) {
prompt += `, ${expandDynamicPromptV2("artists", settings, imageSettings, upscaleSettings)}`;
imageSettings.autoIncludedArtists = true;
}
// Save the original post prompt
// The prompt after dynamic prompts but before the lists have been expanded on
imageSettings.origPostPrompt = prompt;
// Return prompt
return prompt;
}