Compare commits

...

12 Commits
3.2.0 ... main

Author SHA1 Message Date
Serick
19a30beed4 Fix glob pattern matching for filenames with special characters (#335) 2025-11-11 11:20:18 +01:00
DominikDoom
89fee277e3 Fix model path breaking with commas in filenames
Fixes #332
2025-09-09 10:04:00 +02:00
DominikDoom
c4510663ca Fix lora / embed preview being below the textbox
This was a small visual regression for the normal webui introduced by #327
Should keep the fix working for forge
2025-09-09 09:57:50 +02:00
DominikDoom
8766965a30 Credit original author 2025-05-08 12:43:40 +02:00
Disty0
34e68e1628 Fix SDNext ModernUI by following the cursor (#327) 2025-05-05 20:44:51 +02:00
DominikDoom
41d185b616 Improve IME consistency
Might help with #326
2025-05-01 13:48:32 +02:00
DominikDoom
e0baa58ace Fix style appending to wrong node on forge classic
Fixes #323
2025-04-16 11:23:12 +02:00
DominikDoom
c1ef12d887 Fix weighted tags preventing normal tag completion
caused by filter applying to every tag instead of just one to one
Fixes #324
2025-04-15 21:56:16 +02:00
Serick
4fc122de4b Added support for Forge classic (#322)
Fixes issues due to removal of hypernetworks in Forge classic
2025-04-15 09:35:54 +02:00
re-unknown
c341ccccb6 Add TIPO configuration for tag prompt in third-party selectors (#319) 2025-03-23 14:26:34 +01:00
akoyaki ayagi
bda8701734 Add a character core tags list file for chant function (#317)
Alternative chant list ("<c:" or "<chant:" prefix) for 26k characters and their tag descriptions. Allows greater likeness even if the model doesn't know the character well.
2025-03-08 10:45:40 +01:00
undefined
63fca457a7 Indicate repeated tag (#313)
Shows 🔁 to mark a tag that has already been used in the prompt
2025-01-16 09:29:33 +01:00
5 changed files with 160323 additions and 28 deletions

View File

@@ -86,6 +86,13 @@ const thirdParty = {
"selectors": [
"Found tags",
]
},
"TIPO": {
"base": "#tab_txt2img",
"hasIds": false,
"selectors": [
"Tag Prompt"
]
}
}

View File

@@ -31,7 +31,8 @@ const autocompleteCSS = `
position: absolute;
z-index: 999;
max-width: calc(100% - 1.5rem);
margin: 5px 0 0 0;
flex-wrap: wrap;
gap: 10px;
}
.autocompleteResults {
background-color: var(--results-bg) !important;
@@ -44,11 +45,11 @@ const autocompleteCSS = `
overflow-y: var(--results-overflow-y);
overflow-x: hidden;
word-break: break-word;
margin-top: 10px; /* Margin to create space below the cursor */
}
.sideInfo {
display: none;
position: relative;
margin-left: 10px;
height: 18rem;
max-width: 16rem;
}
@@ -90,6 +91,10 @@ const autocompleteCSS = `
content: "✨";
margin-right: 2px;
}
.acMetaText span.used::after {
content: "🔁";
margin-right: 2px;
}
.acWikiLink {
padding: 0.5rem;
margin: -0.5rem 0 -0.5rem -0.5rem;
@@ -358,10 +363,13 @@ function showResults(textArea) {
parentDiv.style.display = "flex";
if (TAC_CFG.slidingPopup) {
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd).left;
let offset = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition, textArea.offsetWidth - parentDiv.offsetWidth);
let caretPosition = getCaretCoordinates(textArea, textArea.selectionEnd);
// Top cursor offset fix for SDNext modern UI, based on code by https://github.com/Nyx01
let offsetTop = textArea.offsetTop + caretPosition.top - textArea.scrollTop + 10; // Adjust this value for desired distance below cursor
let offsetLeft = Math.min(textArea.offsetLeft - textArea.scrollLeft + caretPosition.left, textArea.offsetWidth - parentDiv.offsetWidth);
parentDiv.style.left = `${offset}px`;
parentDiv.style.top = `${offsetTop}px`; // Position below the cursor
parentDiv.style.left = `${offsetLeft}px`;
} else {
if (parentDiv.style.left)
parentDiv.style.removeProperty("left");
@@ -626,12 +634,30 @@ async function insertTextAtCursor(textArea, result, tagword, tabCompletedWithout
updateInput(textArea);
// Update previous tags with the edited prompt to prevent re-searching the same term
let weightedTags = [...newPrompt.matchAll(WEIGHT_REGEX)]
.map(match => match[1]);
let tags = newPrompt.match(TAG_REGEX())
if (weightedTags !== null) {
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted)))
.concat(weightedTags);
let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
.map(match => match[1])
.sort((a, b) => a.length - b.length);
let tags = [...prompt.match(TAG_REGEX())].sort((a, b) => a.length - b.length);
if (weightedTags !== null && tags !== null) {
// Create a working copy of the normal tags
let workingTags = [...tags];
// For each weighted tag
for (const weightedTag of weightedTags) {
// Find first matching tag and remove it from working set
const matchIndex = workingTags.findIndex(tag =>
tag === weightedTag && !tag.startsWith("<[") && !tag.startsWith("$(")
);
if (matchIndex !== -1) {
// Remove the matched tag from the working set
workingTags.splice(matchIndex, 1);
}
}
// Combine filtered normal tags with weighted tags
tags = workingTags.concat(weightedTags);
}
previousTags = tags;
@@ -666,6 +692,30 @@ function addResultsToList(textArea, results, tagword, resetList) {
let tagColors = TAC_CFG.colorMap;
let mode = (document.querySelector(".dark") || gradioApp().querySelector(".dark")) ? 0 : 1;
let nextLength = Math.min(results.length, resultCount + TAC_CFG.resultStepLength);
const IS_DAN_OR_E621_TAG_FILE = (tagFileName.toLowerCase().startsWith("danbooru") || tagFileName.toLowerCase().startsWith("e621"));
const tagCount = {};
// Indicate if tag was used before
if (IS_DAN_OR_E621_TAG_FILE) {
const prompt = textArea.value.trim();
const tags = prompt.replaceAll('\n', ',').split(',').map(tag => tag.trim()).filter(tag => tag);
const unsanitizedTags = tags.map(tag => {
const weightedTags = [...tag.matchAll(WEIGHT_REGEX)].flat();
if (weightedTags.length === 2) {
return weightedTags[1];
} else {
// normal tags
return tag;
}
}).map(tag => tag.replaceAll(" ", "_").replaceAll("\\(", "(").replaceAll("\\)", ")"));
// Split tags by `,` and count tag
for (const tag of unsanitizedTags) {
tagCount[tag] = tagCount[tag] ? tagCount[tag] + 1 : 1;
}
}
for (let i = resultCount; i < nextLength; i++) {
let result = results[i];
@@ -734,8 +784,7 @@ function addResultsToList(textArea, results, tagword, resetList) {
if (
TAC_CFG.showWikiLinks &&
result.type === ResultType.tag &&
(tagFileName.toLowerCase().startsWith("danbooru") ||
tagFileName.toLowerCase().startsWith("e621"))
IS_DAN_OR_E621_TAG_FILE
) {
let wikiLink = document.createElement("a");
wikiLink.classList.add("acWikiLink");
@@ -828,7 +877,19 @@ function addResultsToList(textArea, results, tagword, resetList) {
// Add small ✨ marker to indicate usage sorting
if (result.usageBias) {
flexDiv.querySelector(".acMetaText").classList.add("biased");
flexDiv.title = "✨ Frequent tag. Ctrl/Cmd + click to reset usage count."
flexDiv.title = "✨ Frequent tag. Ctrl/Cmd + click to reset usage count.";
}
// Add 🔁 to indicate if tag was used before
if (IS_DAN_OR_E621_TAG_FILE && tagCount[result.text]) {
// Fix PR#313#issuecomment-2592551794
if (!(result.text === tagword && tagCount[result.text] === 1)) {
const textNode = flexDiv.querySelector(".acMetaText");
const span = document.createElement("span");
textNode.insertBefore(span, textNode.firstChild);
span.classList.add("used");
span.title = "🔁 The prompt already contains this tag";
}
}
// Check if it's a negative prompt
@@ -1087,11 +1148,29 @@ async function autocomplete(textArea, prompt, fixedTag = null) {
// Match tags with RegEx to get the last edited one
// We also match for the weighting format (e.g. "tag:1.0") here, and combine the two to get the full tag word set
let weightedTags = [...prompt.matchAll(WEIGHT_REGEX)]
.map(match => match[1]);
let tags = prompt.match(TAG_REGEX())
.map(match => match[1])
.sort((a, b) => a.length - b.length);
let tags = [...prompt.match(TAG_REGEX())].sort((a, b) => a.length - b.length);
if (weightedTags !== null && tags !== null) {
tags = tags.filter(tag => !weightedTags.some(weighted => tag.includes(weighted) && !tag.startsWith("<[") && !tag.startsWith("$(")))
.concat(weightedTags);
// Create a working copy of the normal tags
let workingTags = [...tags];
// For each weighted tag
for (const weightedTag of weightedTags) {
// Find first matching tag and remove it from working set
const matchIndex = workingTags.findIndex(tag =>
tag === weightedTag && !tag.startsWith("<[") && !tag.startsWith("$(")
);
if (matchIndex !== -1) {
// Remove the matched tag from the working set
workingTags.splice(matchIndex, 1);
}
}
// Combine filtered normal tags with weighted tags
tags = workingTags.concat(weightedTags);
}
// Guard for no tags
@@ -1436,6 +1515,12 @@ function addAutocompleteToArea(area) {
if (!e.inputType && !tacSelfTrigger) return;
tacSelfTrigger = false;
// Block hide we are composing (IME), so enter doesn't close the results
if (e.isComposing) {
hideBlocked = true;
setTimeout(() => { hideBlocked = false; }, 100);
}
debounce(autocomplete(area, area.value), TAC_CFG.delayTime);
checkKeywordInsertionUndo(area, e);
});
@@ -1561,7 +1646,7 @@ async function setup() {
} else {
acStyle.appendChild(document.createTextNode(css));
}
gradioApp().appendChild(acStyle);
document.head.appendChild(acStyle);
// Callback
await processQueue(QUEUE_AFTER_SETUP, null);

View File

@@ -20,9 +20,27 @@ except ImportError:
TAGS_PATH = Path(scripts.basedir()).joinpath("tags").absolute()
# The path to the folder containing the wildcards and embeddings
WILDCARD_PATH = FILE_DIR.joinpath("scripts/wildcards").absolute()
try: # SD.Next
WILDCARD_PATH = Path(shared.opts.wildcards_dir).absolute()
except Exception: # A1111
WILDCARD_PATH = FILE_DIR.joinpath("scripts/wildcards").absolute()
EMB_PATH = Path(shared.cmd_opts.embeddings_dir).absolute()
HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir).absolute()
# Forge Classic detection
try:
from modules_forge.forge_version import version as forge_version
IS_FORGE_CLASSIC = forge_version == "classic"
except ImportError:
IS_FORGE_CLASSIC = False
# Forge Classic skips it
if not IS_FORGE_CLASSIC:
try:
HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir).absolute()
except AttributeError:
HYP_PATH = None
else:
HYP_PATH = None
try:
LORA_PATH = Path(shared.cmd_opts.lora_dir).absolute()

View File

@@ -98,9 +98,9 @@ def sort_models(model_list, sort_method = None, name_has_subpath = False):
# During merging on the JS side we need to re-sort anyway, so here only the sort criteria are calculated.
# The list itself doesn't need to get sorted at this point.
if len(model_list[0]) > 2:
results = [f'{name},"{sorter(path, name, name_has_subpath)}",{meta}' for path, name, meta in model_list]
results = [f'"{name}","{sorter(path, name, name_has_subpath)}",{meta}' for path, name, meta in model_list]
else:
results = [f'{name},"{sorter(path, name, name_has_subpath)}"' for path, name in model_list]
results = [f'"{name}","{sorter(path, name, name_has_subpath)}"' for path, name in model_list]
return results
@@ -503,7 +503,14 @@ def write_style_names(*args, **kwargs):
def write_temp_files(skip_wildcard_refresh = False):
# Write wildcards to wc.txt if found
if WILDCARD_PATH.exists() and not skip_wildcard_refresh:
wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
try:
# Attempt to create a relative path, but fall back to an absolute path if not possible
relative_wildcard_path = WILDCARD_PATH.relative_to(FILE_DIR).as_posix()
except ValueError:
# If the paths are not relative, use the absolute path
relative_wildcard_path = WILDCARD_PATH.as_posix()
wildcards = [relative_wildcard_path] + get_wildcards()
if wildcards:
write_to_temp_file('wc.txt', wildcards)
@@ -515,7 +522,7 @@ def write_temp_files(skip_wildcard_refresh = False):
# Write yaml extension wildcards to umi_tags.txt and wc_yaml.json if found
get_yaml_wildcards()
if HYP_PATH.exists():
if HYP_PATH is not None and HYP_PATH.exists():
hypernets = get_hypernetworks()
if hypernets:
write_to_temp_file('hyp.txt', hypernets)
@@ -741,7 +748,7 @@ def api_tac(_: gr.Blocks, app: FastAPI):
return Response(status_code=404)
try:
json_candidates = glob.glob(base_path.as_posix() + f"/**/{filename}.json", recursive=True)
json_candidates = glob.glob(base_path.as_posix() + f"/**/{glob.escape(filename)}.json", recursive=True)
if json_candidates is not None and len(json_candidates) > 0 and Path(json_candidates[0]).is_file():
return FileResponse(json_candidates[0])
except Exception as e:
@@ -752,7 +759,7 @@ def api_tac(_: gr.Blocks, app: FastAPI):
return Response(status_code=404)
try:
img_glob = glob.glob(base_path.as_posix() + f"/**/{filename}.*", recursive=True)
img_glob = glob.glob(base_path.as_posix() + f"/**/{glob.escape(filename)}.*", recursive=True)
img_candidates = [img for img in img_glob if Path(img).suffix in [".png", ".jpg", ".jpeg", ".webp", ".gif"] and Path(img).is_file()]
if img_candidates is not None and len(img_candidates) > 0:
if blob:
@@ -781,7 +788,7 @@ def api_tac(_: gr.Blocks, app: FastAPI):
@app.get("/tacapi/v1/lora-cached-hash/{lora_name}")
async def get_lora_cached_hash(lora_name: str):
path_glob = glob.glob(LORA_PATH.as_posix() + f"/**/{lora_name}.*", recursive=True)
path_glob = glob.glob(LORA_PATH.as_posix() + f"/**/{glob.escape(lora_name)}.*", recursive=True)
paths = [lora for lora in path_glob if Path(lora).suffix in [".safetensors", ".ckpt", ".pt"] and Path(lora).is_file()]
if paths is not None and len(paths) > 0:
path = paths[0]

File diff suppressed because it is too large Load Diff