Spaces:
Running
on
Zero
Running
on
Zero
import { app } from "../../../scripts/app.js"; | |
import { ComfyWidgets } from "../../../scripts/widgets.js"; | |
import { api } from "../../../scripts/api.js"; | |
import { $el, ComfyDialog } from "../../../scripts/ui.js"; | |
import { TextAreaAutoComplete } from "./common/autocomplete.js"; | |
import { ModelInfoDialog } from "./common/modelInfoDialog.js"; | |
import { LoraInfoDialog } from "./modelInfo.js"; | |
function parseCSV(csvText) { | |
const rows = []; | |
const delimiter = ","; | |
const quote = '"'; | |
let currentField = ""; | |
let inQuotedField = false; | |
function pushField() { | |
rows[rows.length - 1].push(currentField); | |
currentField = ""; | |
inQuotedField = false; | |
} | |
rows.push([]); // Initialize the first row | |
for (let i = 0; i < csvText.length; i++) { | |
const char = csvText[i]; | |
const nextChar = csvText[i + 1]; | |
// Special handling for backslash escaped quotes | |
if (char === "\\" && nextChar === quote) { | |
currentField += quote; | |
i++; | |
} | |
if (!inQuotedField) { | |
if (char === quote) { | |
inQuotedField = true; | |
} else if (char === delimiter) { | |
pushField(); | |
} else if (char === "\r" || char === "\n" || i === csvText.length - 1) { | |
pushField(); | |
if (nextChar === "\n") { | |
i++; // Handle Windows line endings (\r\n) | |
} | |
rows.push([]); // Start a new row | |
} else { | |
currentField += char; | |
} | |
} else { | |
if (char === quote && nextChar === quote) { | |
currentField += quote; | |
i++; // Skip the next quote | |
} else if (char === quote) { | |
inQuotedField = false; | |
} else if (char === "\r" || char === "\n" || i === csvText.length - 1) { | |
// Dont allow new lines in quoted text, assume its wrong | |
const parsed = parseCSV(currentField); | |
rows.pop(); | |
rows.push(...parsed); | |
inQuotedField = false; | |
currentField = ""; | |
rows.push([]); | |
} else { | |
currentField += char; | |
} | |
} | |
} | |
if (currentField || csvText[csvText.length - 1] === ",") { | |
pushField(); | |
} | |
// Remove the last row if it's empty | |
if (rows[rows.length - 1].length === 0) { | |
rows.pop(); | |
} | |
return rows; | |
} | |
async function getCustomWords() { | |
const resp = await api.fetchApi("/pysssss/autocomplete", { cache: "no-store" }); | |
if (resp.status === 200) { | |
return await resp.text(); | |
} | |
return undefined; | |
} | |
async function addCustomWords(text) { | |
if (!text) { | |
text = await getCustomWords(); | |
} | |
if (text) { | |
TextAreaAutoComplete.updateWords( | |
"pysssss.customwords", | |
parseCSV(text).reduce((p, n) => { | |
let text; | |
let priority; | |
let value; | |
let num; | |
switch (n.length) { | |
case 0: | |
return; | |
case 1: | |
// Single word | |
text = n[0]; | |
break; | |
case 2: | |
// Word,[priority|alias] | |
num = +n[1]; | |
if (isNaN(num)) { | |
text = n[0] + "🔄️" + n[1]; | |
value = n[0]; | |
} else { | |
text = n[0]; | |
priority = num; | |
} | |
break; | |
case 4: | |
// a1111 csv format? | |
value = n[0]; | |
priority = +n[2]; | |
const aliases = n[3]?.trim(); | |
if (aliases && aliases !== "null") { // Weird null in an example csv, maybe they are JSON.parsing the last column? | |
const split = aliases.split(","); | |
for (const text of split) { | |
p[text] = { text, priority, value }; | |
} | |
} | |
text = value; | |
break; | |
default: | |
// Word,alias,priority | |
text = n[1]; | |
value = n[0]; | |
priority = +n[2]; | |
break; | |
} | |
p[text] = { text, priority, value }; | |
return p; | |
}, {}) | |
); | |
} | |
} | |
function toggleLoras() { | |
[TextAreaAutoComplete.globalWords, TextAreaAutoComplete.globalWordsExclLoras] = [ | |
TextAreaAutoComplete.globalWordsExclLoras, | |
TextAreaAutoComplete.globalWords, | |
]; | |
} | |
class EmbeddingInfoDialog extends ModelInfoDialog { | |
async addInfo() { | |
super.addInfo(); | |
const info = await this.addCivitaiInfo(); | |
if (info) { | |
$el("div", { | |
parent: this.content, | |
innerHTML: info.description, | |
style: { | |
maxHeight: "250px", | |
overflow: "auto", | |
}, | |
}); | |
} | |
} | |
} | |
class CustomWordsDialog extends ComfyDialog { | |
async show() { | |
const text = await getCustomWords(); | |
this.words = $el("textarea", { | |
textContent: text, | |
style: { | |
width: "70vw", | |
height: "70vh", | |
}, | |
}); | |
const input = $el("input", { | |
style: { | |
flex: "auto", | |
}, | |
value: | |
"https://gist.githubusercontent.com/pythongosssss/1d3efa6050356a08cea975183088159a/raw/a18fb2f94f9156cf4476b0c24a09544d6c0baec6/danbooru-tags.txt", | |
}); | |
super.show( | |
$el( | |
"div", | |
{ | |
style: { | |
display: "flex", | |
flexDirection: "column", | |
overflow: "hidden", | |
maxHeight: "100%", | |
}, | |
}, | |
[ | |
$el("h2", { | |
textContent: "Custom Autocomplete Words", | |
style: { | |
color: "#fff", | |
marginTop: 0, | |
textAlign: "center", | |
fontFamily: "sans-serif", | |
}, | |
}), | |
$el( | |
"div", | |
{ | |
style: { | |
color: "#fff", | |
fontFamily: "sans-serif", | |
display: "flex", | |
alignItems: "center", | |
gap: "5px", | |
}, | |
}, | |
[ | |
$el("label", { textContent: "Load Custom List: " }), | |
input, | |
$el("button", { | |
textContent: "Load", | |
onclick: async () => { | |
try { | |
const res = await fetch(input.value); | |
if (res.status !== 200) { | |
throw new Error("Error loading: " + res.status + " " + res.statusText); | |
} | |
this.words.value = await res.text(); | |
} catch (error) { | |
alert("Error loading custom list, try manually copy + pasting the list"); | |
} | |
}, | |
}), | |
] | |
), | |
this.words, | |
] | |
) | |
); | |
} | |
createButtons() { | |
const btns = super.createButtons(); | |
const save = $el("button", { | |
type: "button", | |
textContent: "Save", | |
onclick: async (e) => { | |
try { | |
const res = await api.fetchApi("/pysssss/autocomplete", { method: "POST", body: this.words.value }); | |
if (res.status !== 200) { | |
throw new Error("Error saving: " + res.status + " " + res.statusText); | |
} | |
save.textContent = "Saved!"; | |
addCustomWords(this.words.value); | |
setTimeout(() => { | |
save.textContent = "Save"; | |
}, 500); | |
} catch (error) { | |
alert("Error saving word list!"); | |
console.error(error); | |
} | |
}, | |
}); | |
btns.unshift(save); | |
return btns; | |
} | |
} | |
const id = "pysssss.AutoCompleter"; | |
app.registerExtension({ | |
name: id, | |
init() { | |
const STRING = ComfyWidgets.STRING; | |
const SKIP_WIDGETS = new Set(["ttN xyPlot.x_values", "ttN xyPlot.y_values"]); | |
ComfyWidgets.STRING = function (node, inputName, inputData) { | |
const r = STRING.apply(this, arguments); | |
if (inputData[1]?.multiline) { | |
// Disabled on this input | |
const config = inputData[1]?.["pysssss.autocomplete"]; | |
if (config === false) return r; | |
// In list of widgets to skip | |
const id = `${node.comfyClass}.${inputName}`; | |
if (SKIP_WIDGETS.has(id)) return r; | |
let words; | |
let separator; | |
if (typeof config === "object") { | |
separator = config.separator; | |
words = {}; | |
if (config.words) { | |
// Custom wordlist, this will have been registered on setup | |
Object.assign(words, TextAreaAutoComplete.groups[node.comfyClass + "." + inputName] ?? {}); | |
} | |
for (const item of config.groups ?? []) { | |
if (item === "*") { | |
// This widget wants all global words included | |
Object.assign(words, TextAreaAutoComplete.globalWords); | |
} else { | |
// This widget wants a specific group included | |
Object.assign(words, TextAreaAutoComplete.groups[item] ?? {}); | |
} | |
} | |
} | |
new TextAreaAutoComplete(r.widget.inputEl, words, separator); | |
} | |
return r; | |
}; | |
TextAreaAutoComplete.globalSeparator = localStorage.getItem(id + ".AutoSeparate") ?? ", "; | |
const enabledSetting = app.ui.settings.addSetting({ | |
id, | |
name: "🐍 Text Autocomplete", | |
defaultValue: true, | |
type: (name, setter, value) => { | |
return $el("tr", [ | |
$el("td", [ | |
$el("label", { | |
for: id.replaceAll(".", "-"), | |
textContent: name, | |
}), | |
]), | |
$el("td", [ | |
$el( | |
"label", | |
{ | |
textContent: "Enabled ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el("input", { | |
id: id.replaceAll(".", "-"), | |
type: "checkbox", | |
checked: value, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.enabled = checked; | |
setter(checked); | |
}, | |
}), | |
] | |
), | |
$el( | |
"label.comfy-tooltip-indicator", | |
{ | |
title: "This requires other ComfyUI nodes/extensions that support using LoRAs in the prompt.", | |
textContent: "Loras enabled ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "checkbox", | |
checked: !!TextAreaAutoComplete.lorasEnabled, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.lorasEnabled = checked; | |
toggleLoras(); | |
localStorage.setItem(id + ".ShowLoras", TextAreaAutoComplete.lorasEnabled); | |
}, | |
}), | |
] | |
), | |
$el( | |
"label", | |
{ | |
textContent: "Auto-insert comma ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "checkbox", | |
checked: !!TextAreaAutoComplete.globalSeparator, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.globalSeparator = checked ? ", " : ""; | |
localStorage.setItem(id + ".AutoSeparate", TextAreaAutoComplete.globalSeparator); | |
}, | |
}), | |
] | |
), | |
$el( | |
"label", | |
{ | |
textContent: "Replace _ with space ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "checkbox", | |
checked: !!TextAreaAutoComplete.replacer, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.replacer = checked ? (v) => v.replaceAll("_", " ") : undefined; | |
localStorage.setItem(id + ".ReplaceUnderscore", checked); | |
}, | |
}), | |
] | |
), | |
$el( | |
"label", | |
{ | |
textContent: "Insert suggestion on: ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el( | |
"label", | |
{ | |
textContent: "Tab", | |
style: { | |
display: "block", | |
marginLeft: "20px", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "checkbox", | |
checked: !!TextAreaAutoComplete.insertOnTab, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.insertOnTab = checked; | |
localStorage.setItem(id + ".InsertOnTab", checked); | |
}, | |
}), | |
] | |
), | |
$el( | |
"label", | |
{ | |
textContent: "Enter", | |
style: { | |
display: "block", | |
marginLeft: "20px", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "checkbox", | |
checked: !!TextAreaAutoComplete.insertOnEnter, | |
onchange: (event) => { | |
const checked = !!event.target.checked; | |
TextAreaAutoComplete.insertOnEnter = checked; | |
localStorage.setItem(id + ".InsertOnEnter", checked); | |
}, | |
}), | |
] | |
), | |
] | |
), | |
$el( | |
"label", | |
{ | |
textContent: "Max suggestions: ", | |
style: { | |
display: "block", | |
}, | |
}, | |
[ | |
$el("input", { | |
type: "number", | |
value: +TextAreaAutoComplete.suggestionCount, | |
style: { | |
width: "80px" | |
}, | |
onchange: (event) => { | |
const value = +event.target.value; | |
TextAreaAutoComplete.suggestionCount = value;; | |
localStorage.setItem(id + ".SuggestionCount", TextAreaAutoComplete.suggestionCount); | |
}, | |
}), | |
] | |
), | |
$el("button", { | |
textContent: "Manage Custom Words", | |
onclick: () => { | |
app.ui.settings.element.close(); | |
new CustomWordsDialog().show(); | |
}, | |
style: { | |
fontSize: "14px", | |
display: "block", | |
marginTop: "5px", | |
}, | |
}), | |
]), | |
]); | |
}, | |
}); | |
TextAreaAutoComplete.enabled = enabledSetting.value; | |
TextAreaAutoComplete.replacer = localStorage.getItem(id + ".ReplaceUnderscore") === "true" ? (v) => v.replaceAll("_", " ") : undefined; | |
TextAreaAutoComplete.insertOnTab = localStorage.getItem(id + ".InsertOnTab") !== "false"; | |
TextAreaAutoComplete.insertOnEnter = localStorage.getItem(id + ".InsertOnEnter") !== "false"; | |
TextAreaAutoComplete.lorasEnabled = localStorage.getItem(id + ".ShowLoras") === "true"; | |
TextAreaAutoComplete.suggestionCount = +localStorage.getItem(id + ".SuggestionCount") || 20; | |
}, | |
setup() { | |
async function addEmbeddings() { | |
const embeddings = await api.getEmbeddings(); | |
const words = {}; | |
words["embedding:"] = { text: "embedding:" }; | |
for (const emb of embeddings) { | |
const v = `embedding:${emb}`; | |
words[v] = { | |
text: v, | |
info: () => new EmbeddingInfoDialog(emb).show("embeddings", emb), | |
use_replacer: false, | |
}; | |
} | |
TextAreaAutoComplete.updateWords("pysssss.embeddings", words); | |
} | |
async function addLoras() { | |
let loras; | |
try { | |
loras = LiteGraph.registered_node_types["LoraLoader"]?.nodeData.input.required.lora_name[0]; | |
} catch (error) {} | |
if (!loras?.length) { | |
loras = await api.fetchApi("/pysssss/loras", { cache: "no-store" }).then((res) => res.json()); | |
} | |
const words = {}; | |
words["lora:"] = { text: "lora:" }; | |
for (const lora of loras) { | |
const v = `<lora:${lora}:1.0>`; | |
words[v] = { | |
text: v, | |
info: () => new LoraInfoDialog(lora).show("loras", lora), | |
use_replacer: false, | |
}; | |
} | |
TextAreaAutoComplete.updateWords("pysssss.loras", words); | |
} | |
// store global words with/without loras | |
Promise.all([addEmbeddings(), addCustomWords()]) | |
.then(() => { | |
TextAreaAutoComplete.globalWordsExclLoras = Object.assign({}, TextAreaAutoComplete.globalWords); | |
}) | |
.then(addLoras) | |
.then(() => { | |
if (!TextAreaAutoComplete.lorasEnabled) { | |
toggleLoras(); // off by default | |
} | |
}); | |
}, | |
beforeRegisterNodeDef(_, def) { | |
// Process each input to see if there is a custom word list for | |
// { input: { required: { something: ["STRING", { "pysssss.autocomplete": ["groupid", ["custom", "words"] ] }] } } } | |
const inputs = { ...def.input?.required, ...def.input?.optional }; | |
for (const input in inputs) { | |
const config = inputs[input][1]?.["pysssss.autocomplete"]; | |
if (!config) continue; | |
if (typeof config === "object" && config.words) { | |
const words = {}; | |
for (const text of config.words || []) { | |
const obj = typeof text === "string" ? { text } : text; | |
words[obj.text] = obj; | |
} | |
TextAreaAutoComplete.updateWords(def.name + "." + input, words, false); | |
} | |
} | |
}, | |
}); | |