diff --git a/__init__.py b/__init__.py index 1838249..8198ed6 100644 --- a/__init__.py +++ b/__init__.py @@ -116,25 +116,63 @@ def execute(self, preprocessor, image, resolution=512): return getattr(aux_class(), aux_class.FUNCTION)(**params) +########################################################################################################################## +WEB_DIRECTORY = "./web" +from server import PromptServer +from aiohttp import web +import folder_paths, comfy.controlnet +@PromptServer.instance.routes.get("/Preprocessor") +async def getStylesList(request): + cnmodelname = request.rel_url.query["name"] + return web.json_response([{"name":i} for i in preprocessor_options()]) class ControlNetPreprocessorSelector: @classmethod def INPUT_TYPES(s): - return { - "required": { - "preprocessor": (PREPROCESSOR_OPTIONS,), - } - } + return { "required": { "cn": ( ["none"]+folder_paths.get_filename_list("controlnet"), ), + "image": ("IMAGE",), }, + "hidden": { "prompt": "PROMPT", "my_unique_id": "UNIQUE_ID" }, + "optional": { "resolution": ("INT", {"default": 512, "min": 64, "max": 4096, "step": 64 } ) } } - RETURN_TYPES = (PREPROCESSOR_OPTIONS,) - RETURN_NAMES = ("preprocessor",) + RETURN_TYPES = ("CONTROL_NET","IMAGE") FUNCTION = "get_preprocessor" - CATEGORY = "ControlNet Preprocessors" + OUTPUT_NODE = True - def get_preprocessor(self, preprocessor: str): - return (preprocessor,) + def get_preprocessor(self, cn, image, resolution=512, prompt=None, my_unique_id=None): + controlnet = comfy.controlnet.load_controlnet( folder_paths.get_full_path("controlnet", cn) ) + pre = prompt[my_unique_id]["inputs"]['select_styles'] + print(prompt) + if pre == "": return (controlnet, image ) + else: + aux_class = AUX_NODE_MAPPINGS[pre] + input_types = aux_class.INPUT_TYPES() + input_types = { + **input_types["required"], + **(input_types["optional"] if "optional" in input_types else {}) + } + params = {} + for name, input_type in input_types.items(): + if name == "image": + params[name] = image + continue + + if name == "resolution": + params[name] = resolution + continue + + if len(input_type) == 2 and ("default" in input_type[1]): + params[name] = input_type[1]["default"] + continue + + default_values = { "INT": 0, "FLOAT": 0.0 } + if input_type[0] in default_values: params[name] = default_values[input_type[0]] + + predict = getattr(aux_class(), aux_class.FUNCTION)(**params) + if isinstance(predict, dict): return (controlnet,) + predict["result"] + else: return (controlnet,) + predict +########################################################################################################################## NODE_CLASS_MAPPINGS = { **AUX_NODE_MAPPINGS, diff --git a/web/index.css b/web/index.css new file mode 100644 index 0000000..37607b1 --- /dev/null +++ b/web/index.css @@ -0,0 +1,104 @@ +.Preprocessor{ + overflow: auto; +} +.Preprocessor .tools{ + display:flex; + justify-content:space-between; + height:20px; + padding-bottom:5px; + border-bottom:2px solid var(--border-color); +} +.Preprocessor .tools button.delete{ + height:20px; + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:11px; + background:var(--comfy-input-bg); + color:var(--error-text); + box-shadow:none; + cursor:pointer; +} +.Preprocessor .tools button.delete:hover{ + filter: brightness(1.2); +} +.Preprocessor .tools textarea.search{ + flex:1; + margin-left:10px; + height:10px; + line-height:8px; + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:15px; + background:var(--comfy-input-bg); + color:var(--input-text); + box-shadow:none; + padding:4px 10px; + outline: none; + resize: none; + appearance:none; +} +.Preprocessor-list{ + list-style: none; + padding: 0; + margin: 0; + min-height: 150px; + height: calc(100% - 30px); + overflow: auto; + /*display: flex;*/ + /*flex-wrap: wrap;*/ +} +.Preprocessor-list.no-top{ + height: auto; +} + +.Preprocessor-tag{ + display: inline-block; + vertical-align: middle; + margin-top: 0px; + margin-right: 0px; + padding:0px; + color: var(--input-text); + background-color: var(--comfy-input-bg); + border-radius: 8px; + border: 2px solid var(--border-color); + font-size:11px; + cursor:pointer; +} +.Preprocessor-tag.hide{ + display:none; +} +.Preprocessor-tag:hover{ + filter: brightness(1.2); +} +.Preprocessor-tag input{ + --ring-color: transparent; + position: relative; + box-shadow: none; + border: 2px solid var(--border-color); + border-radius: 2px; + background: linear-gradient(135deg, var(--comfy-menu-bg) 0%, var(--comfy-input-bg) 60%); +} +.Preprocessor-tag input[type=checkbox]:checked{ + border: 1px solid var(--theme-color-light); + background-color: var(--theme-color-light); + background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); +} +.Preprocessor-tag input[type=checkbox]{ + color-adjust: exact; + display: inline-block; + flex-shrink: 0; + vertical-align: middle; + appearance: none; + border: 2px solid var(--border-color); + background-origin: border-box; + padding: 0; + width: 1rem; + height: 1rem; + border-radius:4px; + color:var(--theme-color-light); + user-select: none; +} +.Preprocessor-tag span{ + margin:0 4px; + vertical-align: middle; +} \ No newline at end of file diff --git a/web/index.js b/web/index.js new file mode 100644 index 0000000..4ca0fc4 --- /dev/null +++ b/web/index.js @@ -0,0 +1,105 @@ + +import { app } from "../../../scripts/app.js"; +import { api } from "../../../scripts/api.js"; +import { $el } from "../../../scripts/ui.js"; + +const link = document.createElement("link"); link.rel = "stylesheet"; +link.href = "extensions/comfyui_controlnet_aux_pre/index.css"; +document.head.appendChild(link); + +let preprolist = {}; let controlnet = 'control_v11p_sd15_canny.pth'; + +function listsort(listdata,valuestr) { listdata.sort((a,b)=> valuestr.includes(b.name) - valuestr.includes(a.name)); }; + +function addhide(el,searchValue) { el.classList.toggle('hide', !( + el.dataset.name.toLowerCase().includes(searchValue.toLowerCase()) || + el.dataset.tag.toLowerCase().includes(searchValue.toLowerCase()) || + el.classList.contains('Preprocessor-tag-selected') ) ) }; + +function getTagList() { return preprolist[controlnet].map((tag, index) => { + return $el('label.Preprocessor-tag', + { dataset: { tag: tag.name, name: tag.name, index: index }, + $: (el) => { el.firstChild.onclick = () => { el.classList.toggle("Preprocessor-tag-selected"); }; }, }, + [ $el("input", { type: 'checkbox', name: tag.name }), $el("span", { textContent: tag.name }) ] ); } ); }; + +async function getprepro(el){ const resp = await api.fetchApi(`/Preprocessor?name=${controlnet}`); + if (resp.status === 200) { let data = await resp.json(); let mlist = ["","none"]; + + if(controlnet.includes("canny")){ mlist=["canny", "CannyEdgePreprocessor"] } + else if(controlnet.includes("depth")){ mlist=["depth", "MiDaS-DepthMapPreprocessor"] } + else if(controlnet.includes("lineart")){ mlist=["lineart", "LineArtPreprocessor"] } + else if(controlnet.includes("tile")){ mlist=["tile", "TilePreprocessor"] } + else if(controlnet.includes("scrib")){ mlist=["scrib", "FakeScribblePreprocessor"] } + else if(controlnet.includes("soft")){ mlist=["soft", "HEDPreprocessor"] } + else if(controlnet.includes("pose")){ mlist=["pose", "DWPreprocessor"] } + else if(controlnet.includes("normal")){ mlist=["normal", "BAE-NormalMapPreprocessor"] } + else if(controlnet.includes("semseg")){ mlist=["semseg", "OneFormer-ADE20K-SemSegPreprocessor"] } + else if(controlnet.includes("shuffle")){ mlist=["shuffle", "ShufflePreprocessor"] } + else if(controlnet.includes("ioclab_sd15_recolor")){ mlist=["image", "ImageLuminanceDetector"] } + else if(controlnet.includes("t2iadapter_color")){ mlist=["color", "ColorPreprocessor"] } + else if(controlnet.includes("sketch")){ mlist=["scrib", "FakeScribblePreprocessor"] } + + document.querySelector('.search').value = mlist[0]; listsort(data,mlist[1]); + + preprolist[controlnet] = data; el.innerHTML = ''; el.append(...getTagList()); + el.children[0].classList.add("Preprocessor-tag-selected"); el.children[0].children[0].checked = true; + } }; + +app.registerExtension({ + name: 'comfy.ControlNet Preprocessors.Preprocessor Selector', + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeData.name == 'ControlNetPreprocessorSelector'){ const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function() { const styles_id = this.widgets.findIndex((w) => w.name == 'cn'); + this.setProperty("values",[]); this.setSize([300, 350]); + + const toolsElement = $el('div.tools', [ //添加清空按钮搜索框 + $el('button.delete',{ textContent: 'Empty', + onclick:()=>{ selectorlist[0].querySelectorAll(".search").forEach(el=>{ el.value = '' }); + selectorlist[1].querySelectorAll(".Preprocessor-tag").forEach(el => { + el.classList.remove("Preprocessor-tag-selected"); + el.classList.remove("hide"); el.children[0].checked = false }) } }), + + $el('textarea.search',{ placeholder:"🔎 search", + oninput:(e)=>{ let searchValue = e.target.value; + selectorlist[1].querySelectorAll(".Preprocessor-tag").forEach(el => { addhide(el,searchValue); }) } }) + ]); + const stylesList = $el("ul.Preprocessor-list", []); + let selector = this.addDOMWidget( 'select_styles', "btn", $el('div.Preprocessor', [toolsElement, stylesList] ) ); + let selectorlist = selector.element.children; + + // 监听鼠标离开事件 + selectorlist[1].addEventListener('mouseleave', function(e) { const searchValue = document.querySelector('.search').value; + const selectedTags = Array.from(this.querySelectorAll('.Preprocessor-tag-selected')).map(el => el.dataset.tag); // 当前选中的标签值 + listsort(preprolist[controlnet],selectedTags); this.innerHTML = ''; this.append(...getTagList()); // 重新排序 + this.querySelectorAll('.Preprocessor-tag').forEach(el => { // 遍历所有标签 + const isSelected = selectedTags.includes(el.dataset.tag); //标签的选中状态 + if (isSelected) { el.classList.add("Preprocessor-tag-selected"); el.children[0].checked = true; } //更新样式标签的选中状态 + addhide(el,searchValue); }); // 同时处理搜索和隐藏逻辑 + }); + + //根据controlnet模型返回预处理器列表 + Object.defineProperty( this.widgets[styles_id], 'value', { get:()=>{ return controlnet }, + set:(value)=>{ controlnet = value; getprepro(selectorlist[1]) } }) + + //根据选中状态返回预处理器 + let style_select_values = '' + Object.defineProperty(selector, "value", { + set: (value) => { + selectorlist[1].querySelectorAll(".Preprocessor-tag").forEach(el => { + if (value.split(',').includes(el.dataset.tag)) { + el.classList.add("Preprocessor-tag-selected"); el.children[0].checked = true } }) }, + get: () => { + selectorlist[1].querySelectorAll(".Preprocessor-tag").forEach(el => { + if(el.classList.value.indexOf("Preprocessor-tag-selected")>=0){ + if(!this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"].push(el.dataset.tag); }} + else{ if(this.properties["values"].includes(el.dataset.tag)){ + this.properties["values"]= this.properties["values"].filter(v=>v!=el.dataset.tag); } } }); + style_select_values = this.properties["values"].join(','); + return style_select_values; } }); + + getprepro(selectorlist[1]); return onNodeCreated; + } + } + } +}) \ No newline at end of file