tag_autocomplete_helper.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. # This helper script scans folders for wildcards and embeddings and writes them
  2. # to a temporary file to expose it to the javascript side
  3. import gradio as gr
  4. from pathlib import Path
  5. from modules import scripts, script_callbacks, shared, sd_hijack
  6. import yaml
  7. try:
  8. from modules.paths import script_path, extensions_dir
  9. # Webui root path
  10. FILE_DIR = Path(script_path)
  11. # The extension base path
  12. EXT_PATH = Path(extensions_dir)
  13. except ImportError:
  14. # Webui root path
  15. FILE_DIR = Path().absolute()
  16. # The extension base path
  17. EXT_PATH = FILE_DIR.joinpath('extensions')
  18. # Tags base path
  19. TAGS_PATH = Path(scripts.basedir()).joinpath('tags')
  20. # The path to the folder containing the wildcards and embeddings
  21. WILDCARD_PATH = FILE_DIR.joinpath('scripts/wildcards')
  22. EMB_PATH = Path(shared.cmd_opts.embeddings_dir)
  23. HYP_PATH = Path(shared.cmd_opts.hypernetwork_dir)
  24. try:
  25. LORA_PATH = Path(shared.cmd_opts.lora_dir)
  26. except AttributeError:
  27. LORA_PATH = None
  28. try:
  29. LYCO_PATH = Path(shared.cmd_opts.lyco_dir)
  30. except AttributeError:
  31. LYCO_PATH = None
  32. def find_ext_wildcard_paths():
  33. """Returns the path to the extension wildcards folder"""
  34. found = list(EXT_PATH.glob('*/wildcards/'))
  35. return found
  36. # The path to the extension wildcards folder
  37. WILDCARD_EXT_PATHS = find_ext_wildcard_paths()
  38. # The path to the temporary files
  39. STATIC_TEMP_PATH = FILE_DIR.joinpath('tmp') # In the webui root, on windows it exists by default, on linux it doesn't
  40. TEMP_PATH = TAGS_PATH.joinpath('temp') # Extension specific temp files
  41. def get_wildcards():
  42. """Returns a list of all wildcards. Works on nested folders."""
  43. wildcard_files = list(WILDCARD_PATH.rglob("*.txt"))
  44. resolved = [w.relative_to(WILDCARD_PATH).as_posix(
  45. ) for w in wildcard_files if w.name != "put wildcards here.txt"]
  46. return resolved
  47. def get_ext_wildcards():
  48. """Returns a list of all extension wildcards. Works on nested folders."""
  49. wildcard_files = []
  50. for path in WILDCARD_EXT_PATHS:
  51. wildcard_files.append(path.relative_to(FILE_DIR).as_posix())
  52. wildcard_files.extend(p.relative_to(path).as_posix() for p in path.rglob("*.txt") if p.name != "put wildcards here.txt")
  53. wildcard_files.append("-----")
  54. return wildcard_files
  55. def get_ext_wildcard_tags():
  56. """Returns a list of all tags found in extension YAML files found under a Tags: key."""
  57. wildcard_tags = {} # { tag: count }
  58. yaml_files = []
  59. for path in WILDCARD_EXT_PATHS:
  60. yaml_files.extend(p for p in path.rglob("*.yml"))
  61. yaml_files.extend(p for p in path.rglob("*.yaml"))
  62. count = 0
  63. for path in yaml_files:
  64. try:
  65. with open(path, encoding="utf8") as file:
  66. data = yaml.safe_load(file)
  67. for item in data:
  68. if data[item] and 'Tags' in data[item]:
  69. wildcard_tags[count] = ','.join(data[item]['Tags'])
  70. count += 1
  71. else:
  72. print('Issue with tags found in ' + path.name + ' at item ' + item)
  73. except yaml.YAMLError as exc:
  74. print(exc)
  75. # Sort by count
  76. sorted_tags = sorted(wildcard_tags.items(), key=lambda item: item[1], reverse=True)
  77. output = []
  78. for tag, count in sorted_tags:
  79. output.append(f"{tag},{count}")
  80. return output
  81. def get_embeddings(sd_model):
  82. """Write a list of all embeddings with their version"""
  83. # Version constants
  84. V1_SHAPE = 768
  85. V2_SHAPE = 1024
  86. emb_v1 = []
  87. emb_v2 = []
  88. results = []
  89. try:
  90. # Get embedding dict from sd_hijack to separate v1/v2 embeddings
  91. emb_type_a = sd_hijack.model_hijack.embedding_db.word_embeddings
  92. emb_type_b = sd_hijack.model_hijack.embedding_db.skipped_embeddings
  93. # Get the shape of the first item in the dict
  94. emb_a_shape = -1
  95. emb_b_shape = -1
  96. if (len(emb_type_a) > 0):
  97. emb_a_shape = next(iter(emb_type_a.items()))[1].shape
  98. if (len(emb_type_b) > 0):
  99. emb_b_shape = next(iter(emb_type_b.items()))[1].shape
  100. # Add embeddings to the correct list
  101. if (emb_a_shape == V1_SHAPE):
  102. emb_v1 = list(emb_type_a.keys())
  103. elif (emb_a_shape == V2_SHAPE):
  104. emb_v2 = list(emb_type_a.keys())
  105. if (emb_b_shape == V1_SHAPE):
  106. emb_v1 = list(emb_type_b.keys())
  107. elif (emb_b_shape == V2_SHAPE):
  108. emb_v2 = list(emb_type_b.keys())
  109. # Get shape of current model
  110. #vec = sd_model.cond_stage_model.encode_embedding_init_text(",", 1)
  111. #model_shape = vec.shape[1]
  112. # Show relevant entries at the top
  113. #if (model_shape == V1_SHAPE):
  114. # results = [e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2]
  115. #elif (model_shape == V2_SHAPE):
  116. # results = [e + ",v2" for e in emb_v2] + [e + ",v1" for e in emb_v1]
  117. #else:
  118. # raise AttributeError # Fallback to old method
  119. results = sorted([e + ",v1" for e in emb_v1] + [e + ",v2" for e in emb_v2], key=lambda x: x.lower())
  120. except AttributeError:
  121. print("tag_autocomplete_helper: Old webui version or unrecognized model shape, using fallback for embedding completion.")
  122. # Get a list of all embeddings in the folder
  123. all_embeds = [str(e.relative_to(EMB_PATH)) for e in EMB_PATH.rglob("*") if e.suffix in {".bin", ".pt", ".png",'.webp', '.jxl', '.avif'}]
  124. # Remove files with a size of 0
  125. all_embeds = [e for e in all_embeds if EMB_PATH.joinpath(e).stat().st_size > 0]
  126. # Remove file extensions
  127. all_embeds = [e[:e.rfind('.')] for e in all_embeds]
  128. results = [e + "," for e in all_embeds]
  129. write_to_temp_file('emb.txt', results)
  130. def get_hypernetworks():
  131. """Write a list of all hypernetworks"""
  132. # Get a list of all hypernetworks in the folder
  133. all_hypernetworks = [str(h.name) for h in HYP_PATH.rglob("*") if h.suffix in {".pt"}]
  134. # Remove file extensions
  135. return sorted([h[:h.rfind('.')] for h in all_hypernetworks], key=lambda x: x.lower())
  136. def get_lora():
  137. """Write a list of all lora"""
  138. # Get a list of all lora in the folder
  139. all_lora = [str(l.name) for l in LORA_PATH.rglob("*") if l.suffix in {".safetensors", ".ckpt", ".pt"}]
  140. # Remove file extensions
  141. return sorted([l[:l.rfind('.')] for l in all_lora], key=lambda x: x.lower())
  142. def get_lyco():
  143. """Write a list of all LyCORIS/LOHA from https://github.com/KohakuBlueleaf/a1111-sd-webui-lycoris"""
  144. # Get a list of all LyCORIS in the folder
  145. all_lyco = [str(ly.name) for ly in LYCO_PATH.rglob("*") if ly.suffix in {".safetensors", ".ckpt", ".pt"}]
  146. # Remove file extensions
  147. return sorted([ly[:ly.rfind('.')] for ly in all_lyco], key=lambda x: x.lower())
  148. def write_tag_base_path():
  149. """Writes the tag base path to a fixed location temporary file"""
  150. with open(STATIC_TEMP_PATH.joinpath('tagAutocompletePath.txt'), 'w', encoding="utf-8") as f:
  151. f.write(TAGS_PATH.relative_to(FILE_DIR).as_posix())
  152. def write_to_temp_file(name, data):
  153. """Writes the given data to a temporary file"""
  154. with open(TEMP_PATH.joinpath(name), 'w', encoding="utf-8") as f:
  155. f.write(('\n'.join(data)))
  156. csv_files = []
  157. csv_files_withnone = []
  158. def update_tag_files():
  159. """Returns a list of all potential tag files"""
  160. global csv_files, csv_files_withnone
  161. files = [str(t.relative_to(TAGS_PATH)) for t in TAGS_PATH.glob("*.csv")]
  162. csv_files = files
  163. csv_files_withnone = ["None"] + files
  164. # Write the tag base path to a fixed location temporary file
  165. # to enable the javascript side to find our files regardless of extension folder name
  166. if not STATIC_TEMP_PATH.exists():
  167. STATIC_TEMP_PATH.mkdir(exist_ok=True)
  168. write_tag_base_path()
  169. update_tag_files()
  170. # Check if the temp path exists and create it if not
  171. if not TEMP_PATH.exists():
  172. TEMP_PATH.mkdir(parents=True, exist_ok=True)
  173. # Set up files to ensure the script doesn't fail to load them
  174. # even if no wildcards or embeddings are found
  175. write_to_temp_file('wc.txt', [])
  176. write_to_temp_file('wce.txt', [])
  177. write_to_temp_file('wcet.txt', [])
  178. write_to_temp_file('hyp.txt', [])
  179. write_to_temp_file('lora.txt', [])
  180. write_to_temp_file('lyco.txt', [])
  181. # Only reload embeddings if the file doesn't exist, since they are already re-written on model load
  182. if not TEMP_PATH.joinpath("emb.txt").exists():
  183. write_to_temp_file('emb.txt', [])
  184. # Write wildcards to wc.txt if found
  185. if WILDCARD_PATH.exists():
  186. wildcards = [WILDCARD_PATH.relative_to(FILE_DIR).as_posix()] + get_wildcards()
  187. if wildcards:
  188. write_to_temp_file('wc.txt', wildcards)
  189. # Write extension wildcards to wce.txt if found
  190. if WILDCARD_EXT_PATHS is not None:
  191. wildcards_ext = get_ext_wildcards()
  192. if wildcards_ext:
  193. write_to_temp_file('wce.txt', wildcards_ext)
  194. # Write yaml extension wildcards to wcet.txt if found
  195. wildcards_yaml_ext = get_ext_wildcard_tags()
  196. if wildcards_yaml_ext:
  197. write_to_temp_file('wcet.txt', wildcards_yaml_ext)
  198. # Write embeddings to emb.txt if found
  199. if EMB_PATH.exists():
  200. # Get embeddings after the model loaded callback
  201. script_callbacks.on_model_loaded(get_embeddings)
  202. if HYP_PATH.exists():
  203. hypernets = get_hypernetworks()
  204. if hypernets:
  205. write_to_temp_file('hyp.txt', hypernets)
  206. if LORA_PATH is not None and LORA_PATH.exists():
  207. lora = get_lora()
  208. if lora:
  209. write_to_temp_file('lora.txt', lora)
  210. if LYCO_PATH is not None and LYCO_PATH.exists():
  211. lyco = get_lyco()
  212. if lyco:
  213. write_to_temp_file('lyco.txt', lyco)
  214. # Register autocomplete options
  215. def on_ui_settings():
  216. TAC_SECTION = ("tac", "Tag Autocomplete")
  217. # Main tag file
  218. shared.opts.add_option("tac_tagFile", shared.OptionInfo("danbooru.csv", "Tag filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
  219. # Active in settings
  220. shared.opts.add_option("tac_active", shared.OptionInfo(True, "Enable Tag Autocompletion", section=TAC_SECTION))
  221. shared.opts.add_option("tac_activeIn.txt2img", shared.OptionInfo(True, "Active in txt2img (Requires restart)", section=TAC_SECTION))
  222. shared.opts.add_option("tac_activeIn.img2img", shared.OptionInfo(True, "Active in img2img (Requires restart)", section=TAC_SECTION))
  223. shared.opts.add_option("tac_activeIn.negativePrompts", shared.OptionInfo(True, "Active in negative prompts (Requires restart)", section=TAC_SECTION))
  224. shared.opts.add_option("tac_activeIn.thirdParty", shared.OptionInfo(True, "Active in third party textboxes [Dataset Tag Editor] (Requires restart)", section=TAC_SECTION))
  225. shared.opts.add_option("tac_activeIn.modelList", shared.OptionInfo("", "List of model names (with file extension) or their hashes to use as black/whitelist, separated by commas.", section=TAC_SECTION))
  226. shared.opts.add_option("tac_activeIn.modelListMode", shared.OptionInfo("Blacklist", "Mode to use for model list", gr.Dropdown, lambda: {"choices": ["Blacklist","Whitelist"]}, section=TAC_SECTION))
  227. # Results related settings
  228. shared.opts.add_option("tac_slidingPopup", shared.OptionInfo(True, "Move completion popup together with text cursor", section=TAC_SECTION))
  229. shared.opts.add_option("tac_maxResults", shared.OptionInfo(5, "Maximum results", section=TAC_SECTION))
  230. shared.opts.add_option("tac_showAllResults", shared.OptionInfo(False, "Show all results", section=TAC_SECTION))
  231. shared.opts.add_option("tac_resultStepLength", shared.OptionInfo(100, "How many results to load at once", section=TAC_SECTION))
  232. shared.opts.add_option("tac_delayTime", shared.OptionInfo(100, "Time in ms to wait before triggering completion again (Requires restart)", section=TAC_SECTION))
  233. shared.opts.add_option("tac_useWildcards", shared.OptionInfo(True, "Search for wildcards", section=TAC_SECTION))
  234. shared.opts.add_option("tac_useEmbeddings", shared.OptionInfo(True, "Search for embeddings", section=TAC_SECTION))
  235. shared.opts.add_option("tac_useHypernetworks", shared.OptionInfo(True, "Search for hypernetworks", section=TAC_SECTION))
  236. shared.opts.add_option("tac_useLoras", shared.OptionInfo(True, "Search for Loras", section=TAC_SECTION))
  237. shared.opts.add_option("tac_useLycos", shared.OptionInfo(True, "Search for LyCORIS/LoHa", section=TAC_SECTION))
  238. shared.opts.add_option("tac_showWikiLinks", shared.OptionInfo(False, "Show '?' next to tags, linking to its Danbooru or e621 wiki page (Warning: This is an external site and very likely contains NSFW examples!)", section=TAC_SECTION))
  239. # Insertion related settings
  240. shared.opts.add_option("tac_replaceUnderscores", shared.OptionInfo(True, "Replace underscores with spaces on insertion", section=TAC_SECTION))
  241. shared.opts.add_option("tac_escapeParentheses", shared.OptionInfo(True, "Escape parentheses on insertion", section=TAC_SECTION))
  242. shared.opts.add_option("tac_appendComma", shared.OptionInfo(True, "Append comma on tag autocompletion", section=TAC_SECTION))
  243. # Alias settings
  244. shared.opts.add_option("tac_alias.searchByAlias", shared.OptionInfo(True, "Search by alias", section=TAC_SECTION))
  245. shared.opts.add_option("tac_alias.onlyShowAlias", shared.OptionInfo(False, "Only show alias", section=TAC_SECTION))
  246. # Translation settings
  247. shared.opts.add_option("tac_translation.translationFile", shared.OptionInfo("None", "Translation filename", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
  248. shared.opts.add_option("tac_translation.oldFormat", shared.OptionInfo(False, "Translation file uses old 3-column translation format instead of the new 2-column one", section=TAC_SECTION))
  249. shared.opts.add_option("tac_translation.searchByTranslation", shared.OptionInfo(True, "Search by translation", section=TAC_SECTION))
  250. # Extra file settings
  251. shared.opts.add_option("tac_extra.extraFile", shared.OptionInfo("extra-quality-tags.csv", "Extra filename (for small sets of custom tags)", gr.Dropdown, lambda: {"choices": csv_files_withnone}, refresh=update_tag_files, section=TAC_SECTION))
  252. shared.opts.add_option("tac_extra.addMode", shared.OptionInfo("Insert before", "Mode to add the extra tags to the main tag list", gr.Dropdown, lambda: {"choices": ["Insert before","Insert after"]}, section=TAC_SECTION))
  253. # Custom mappings
  254. keymapDefault = """\
  255. {
  256. "MoveUp": "ArrowUp",
  257. "MoveDown": "ArrowDown",
  258. "JumpUp": "PageUp",
  259. "JumpDown": "PageDown",
  260. "JumpToStart": "Home",
  261. "JumpToEnd": "End",
  262. "ChooseSelected": "Enter",
  263. "ChooseFirstOrSelected": "Tab",
  264. "Close": "Escape"
  265. }\
  266. """
  267. colorDefault = """\
  268. {
  269. "danbooru": {
  270. "-1": ["red", "maroon"],
  271. "0": ["lightblue", "dodgerblue"],
  272. "1": ["indianred", "firebrick"],
  273. "3": ["violet", "darkorchid"],
  274. "4": ["lightgreen", "darkgreen"],
  275. "5": ["orange", "darkorange"]
  276. },
  277. "e621": {
  278. "-1": ["red", "maroon"],
  279. "0": ["lightblue", "dodgerblue"],
  280. "1": ["gold", "goldenrod"],
  281. "3": ["violet", "darkorchid"],
  282. "4": ["lightgreen", "darkgreen"],
  283. "5": ["tomato", "darksalmon"],
  284. "6": ["red", "maroon"],
  285. "7": ["whitesmoke", "black"],
  286. "8": ["seagreen", "darkseagreen"]
  287. }
  288. }\
  289. """
  290. keymapLabel = "Configure Hotkeys. For possible values, see https://www.w3.org/TR/uievents-key, or leave empty / set to 'None' to disable. Must be valid JSON."
  291. colorLabel = "Configure colors. See https://github.com/DominikDoom/a1111-sd-webui-tagcomplete#colors for info. Must be valid JSON."
  292. try:
  293. shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
  294. shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Code, lambda: {"language": "json", "interactive": True}, section=TAC_SECTION))
  295. except AttributeError:
  296. shared.opts.add_option("tac_keymap", shared.OptionInfo(keymapDefault, keymapLabel, gr.Textbox, section=TAC_SECTION))
  297. shared.opts.add_option("tac_colormap", shared.OptionInfo(colorDefault, colorLabel, gr.Textbox, section=TAC_SECTION))
  298. script_callbacks.on_ui_settings(on_ui_settings)