Rename model in Model View added.

- Generalized model/move to also support renaming.
This commit is contained in:
Christian Bastian
2024-02-25 23:02:02 -05:00
parent 839b93c9be
commit a4dd2f570b
4 changed files with 135 additions and 48 deletions

View File

@@ -34,7 +34,7 @@ I made this fork because the original repo was inactive and missing many things
- View model metadata, including training tags and bucket resolutions. - View model metadata, including training tags and bucket resolutions.
- Read, edit and save notes in a `.txt` file beside the model. - Read, edit and save notes in a `.txt` file beside the model.
- Change or remove a model's preview image (add a different one using a url or local upload). - Change or remove a model's preview image (add a different one using a url or local upload).
- Move or **permanently** remove models. - Rename, move or **permanently** remove models.
### ComfyUI Node Graph ### ComfyUI Node Graph
@@ -55,8 +55,16 @@ I made this fork because the original repo was inactive and missing many things
## TODO ## TODO
### Download Model
- Checkbox to optionally save description in `.txt` file for Civitai. (what about "About Model"?)
- Server setting to enable creating new folders (on download, on move).
### Download Model Info ### Download Model Info
- Auto-save notes? (requires debounce and save confirmation)
- Load workflow from preview (Should be easy to add with ComfyUI built-in clipboard.)
- Default weights on add/drag? (optional override on drag?)
- Optional (re)download `📥︎` model info from the internet and cache the text file locally. (requires checksum?) - Optional (re)download `📥︎` model info from the internet and cache the text file locally. (requires checksum?)
- Radio buttons to swap between downloaded and server view. - Radio buttons to swap between downloaded and server view.
@@ -89,13 +97,14 @@ I made this fork because the original repo was inactive and missing many things
- Filter directory dropdown - Filter directory dropdown
- Filter directory content in auto-suggest dropdown (not clear how this should be implemented) - Filter directory content in auto-suggest dropdown (not clear how this should be implemented)
- Filters dropdown - Filters dropdown
- Stable Diffusion model version, if applicable - Stable Diffusion model version, if applicable (Maybe dropdown list of "Base Models" is more pratical to impliment?)
- Favorites - Favorites
- Swap between `and` and `or` keyword search? (currently `and`) - Swap between `and` and `or` keyword search? (currently `and`)
### Code ### Code
- Javascript cleanup. - Javascript cleanup.
- Stop abusing popup/modal.
- Better abstraction and objectification. (After codebase settles down) - Better abstraction and objectification. (After codebase settles down)
- Separate into classes per tab? - Separate into classes per tab?
- HTML generation all inside main class? - HTML generation all inside main class?

View File

@@ -486,14 +486,9 @@ def download_file(url, filename, overwrite):
def download_image(image_uri, model_path, overwrite): def download_image(image_uri, model_path, overwrite):
extension = None # TODO: doesn't work for https://civitai.com/images/... _, extension = os.path.splitext(image_uri) # TODO: doesn't work for https://civitai.com/images/...
for image_extension in image_extensions: if not extension in image_extensions:
if image_uri.endswith(image_extension):
extension = image_extension
break
if extension is None:
raise ValueError("Invalid image type!") raise ValueError("Invalid image type!")
path_without_extension, _ = os.path.splitext(model_path) path_without_extension, _ = os.path.splitext(model_path)
file = path_without_extension + extension file = path_without_extension + extension
download_file(image_uri, file, overwrite) download_file(image_uri, file, overwrite)
@@ -607,12 +602,8 @@ async def download_model(request):
return web.json_response(result) return web.json_response(result)
name = formdata.get("name") name = formdata.get("name")
model_extension = None _, model_extension = os.path.splitext(name)
for ext in folder_paths_get_supported_pt_extensions(model_type): if not model_extension in folder_paths_get_supported_pt_extensions(model_type):
if name.endswith(ext):
model_extension = ext
break
if model_extension is None:
result["invalid"] = "name" result["invalid"] = "name"
return web.json_response(result) return web.json_response(result)
file_name = os.path.join(directory, name) file_name = os.path.join(directory, name)
@@ -646,21 +637,30 @@ async def move_model(request):
old_file = body.get("oldFile", None) old_file = body.get("oldFile", None)
if old_file is None: if old_file is None:
return web.json_response({ "success": False }) return web.json_response({ "success": False })
old_file, _ = search_path_to_system_path(old_file) old_file, old_model_type = search_path_to_system_path(old_file)
if not os.path.isfile(old_file): if not os.path.isfile(old_file):
return web.json_response({ "success": False }) return web.json_response({ "success": False })
_, filename = os.path.split(old_file) _, model_extension = os.path.splitext(old_file)
if not model_extension in folder_paths_get_supported_pt_extensions(old_model_type):
new_path = body.get("newDirectory", None) # cannot move arbitrary files
if new_path is None: return web.json_response({ "success": False })
return web.json_response({ "success": False })
new_path, _ = search_path_to_system_path(new_path) new_file = body.get("newFile", None)
if new_path is None: if new_file is None or new_file == "":
return web.json_response({ "success": False }) # cannot have empty name
if not os.path.isdir(new_path): return web.json_response({ "success": False })
new_file, new_model_type = search_path_to_system_path(new_file)
if not new_file.endswith(model_extension):
return web.json_response({ "success": False })
if os.path.isfile(new_file):
# cannot overwrite existing file
return web.json_response({ "success": False })
if not model_extension in folder_paths_get_supported_pt_extensions(new_model_type):
return web.json_response({ "success": False })
new_file_dir, _ = os.path.split(new_file)
if not os.path.isdir(new_file_dir):
return web.json_response({ "success": False }) return web.json_response({ "success": False })
new_file = os.path.join(new_path, filename)
if old_file == new_file: if old_file == new_file:
return web.json_response({ "success": False }) return web.json_response({ "success": False })
try: try:
@@ -704,12 +704,9 @@ async def delete_model(request):
if file is None: if file is None:
return web.json_response(result) return web.json_response(result)
is_model = None _, extension = os.path.split(file)
for ext in folder_paths_get_supported_pt_extensions(model_type): if not extension in folder_paths_get_supported_pt_extensions(model_type):
if file.endswith(ext): # cannot move arbitrary files
is_model = True
break
if not is_model:
return web.json_response(result) return web.json_response(result)
if os.path.isfile(file): if os.path.isfile(file):

View File

@@ -225,6 +225,10 @@
} }
/* model manager common */ /* model manager common */
.model-manager h1 {
min-width: 0;
}
.model-manager button, .model-manager button,
.model-manager select, .model-manager select,
.model-manager input { .model-manager input {
@@ -504,7 +508,6 @@
background-color: var(--bg-color); background-color: var(--bg-color);
border-radius: 16px; border-radius: 16px;
color: var(--fg-color); color: var(--fg-color);
padding: 16px;
width: auto; width: auto;
} }

View File

@@ -750,11 +750,24 @@ function modelWidgetIndex(nodeType) {
/** /**
* @param {string} path * @param {string} path
* @returns {string} * @returns {[string, string]}
*/ */
function pathToFileString(path) { function searchPath_split(path) {
const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1; const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\")) + 1;
return path.slice(i); return [path.slice(0, i), path.slice(i)];
}
/**
* @param {string} path
* @param {string[]} extensions
* @returns {[string, string]}
*/
function searchPath_splitExtension(path) {
const i = path.lastIndexOf(".");
if (i === -1) {
return [path, ""];
}
return [path.slice(0, i), path.slice(i)];
} }
/** /**
@@ -1016,7 +1029,7 @@ class ModelGrid {
event.stopPropagation(); event.stopPropagation();
} }
else if (modelType === "embeddings") { else if (modelType === "embeddings") {
const embeddingFile = pathToFileString(path); const [embeddingDirectory, embeddingFile] = searchPath_split(path);
const selectedNodes = app.canvas.selected_nodes; const selectedNodes = app.canvas.selected_nodes;
for (var i in selectedNodes) { for (var i in selectedNodes) {
const selectedNode = selectedNodes[i]; const selectedNode = selectedNodes[i];
@@ -1079,7 +1092,7 @@ class ModelGrid {
const nodeAtPos = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes); const nodeAtPos = app.graph.getNodeOnPos(pos[0], pos[1], app.canvas.visible_nodes);
if (nodeAtPos) { if (nodeAtPos) {
app.canvas.selectNode(nodeAtPos); app.canvas.selectNode(nodeAtPos);
const embeddingFile = pathToFileString(path); const [embeddingDirectory, embeddingFile] = searchPath_split(path);
target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension); target.value = insertEmbeddingIntoText(target.value, embeddingFile, removeEmbeddingExtension);
event.stopPropagation(); event.stopPropagation();
} }
@@ -1097,7 +1110,7 @@ class ModelGrid {
let success = false; let success = false;
if (nodeType === "Embedding") { if (nodeType === "Embedding") {
if (navigator.clipboard){ if (navigator.clipboard){
const embeddingFile = pathToFileString(path); const [embeddingDirectory, embeddingFile] = searchPath_split(path);
const embeddingText = insertEmbeddingIntoText("", embeddingFile, removeEmbeddingExtension); const embeddingText = insertEmbeddingIntoText("", embeddingFile, removeEmbeddingExtension);
navigator.clipboard.writeText(embeddingText); navigator.clipboard.writeText(embeddingText);
success = true; success = true;
@@ -1669,12 +1682,12 @@ class ModelManager extends ComfyDialog {
constructor() { constructor() {
super(); super();
const moveDestination = $el("input.search-text-area", { const moveDestinationInput = $el("input.search-text-area", {
placeholder: "/", placeholder: "/",
}); });
let searchDropdown = null; let searchDropdown = null;
searchDropdown = new DirectoryDropdown( searchDropdown = new DirectoryDropdown(
moveDestination, moveDestinationInput,
() => { () => {
searchDropdown.update( searchDropdown.update(
this.#data.modelDirectories, this.#data.modelDirectories,
@@ -1811,7 +1824,7 @@ class ModelManager extends ComfyDialog {
}, },
}), }),
$el("div.search-models", [ $el("div.search-models", [
moveDestination, moveDestinationInput,
searchDropdown.element, searchDropdown.element,
]), ]),
$el("button", { $el("button", {
@@ -1821,21 +1834,30 @@ class ModelManager extends ComfyDialog {
let moved = false; let moved = false;
if (confirmation) { if (confirmation) {
const container = this.#el.modelInfoContainer; const container = this.#el.modelInfoContainer;
const oldFile = container.dataset.path;
const [oldFilePath, oldFileName] = searchPath_split(oldFile);
const [_, extension] = searchPath_splitExtension(oldFile);
const newFile = (
moveDestinationInput.value +
this.#searchSeparator +
oldFileName +
extension
);
moved = await request( moved = await request(
`/model-manager/model/move`, `/model-manager/model/move`,
{ {
method: "POST", method: "POST",
body: JSON.stringify({ body: JSON.stringify({
"oldFile": container.dataset.path, "oldFile": oldFile,
"newDirectory": moveDestination.value, "newFile": newFile,
}), }),
} }
) )
.then((result) => { .then((result) => {
const moved = result["success"]; const moved = result["success"];
if (moved) if (moved)
{ {
moveDestination.value = ""; moveDestinationInput.value = "";
container.innerHTML = ""; container.innerHTML = "";
this.#el.modelInfoView.style.display = "none"; this.#el.modelInfoView.style.display = "none";
this.#modelTab_updateModels(); this.#modelTab_updateModels();
@@ -2043,7 +2065,63 @@ class ModelManager extends ComfyDialog {
const innerHtml = []; const innerHtml = [];
const filename = info["File Name"]; const filename = info["File Name"];
if (filename !== undefined && filename !== null && filename !== "") { if (filename !== undefined && filename !== null && filename !== "") {
innerHtml.push($el("h1", [filename])); innerHtml.push(
$el("div.row", {
style: { margin: "8px 0 16px 0" },
}, [
$el("h1", {
style: { margin: "0" },
}, [
filename,
]),
$el("div", [
$el("button.icon-button", {
textContent: "✎",
onclick: async(e) => {
const name = window.prompt("New model name:");
let renamed = false;
if (name !== null && name !== "") {
const container = this.#el.modelInfoContainer;
const oldFile = container.dataset.path;
const [oldFilePath, oldFileName] = searchPath_split(oldFile);
const [_, extension] = searchPath_splitExtension(oldFile);
const newFile = (
oldFilePath +
this.#searchSeparator +
name +
extension
);
renamed = await request(
`/model-manager/model/move`,
{
method: "POST",
body: JSON.stringify({
"oldFile": oldFile,
"newFile": newFile,
}),
}
)
.then((result) => {
const renamed = result["success"];
if (renamed)
{
container.innerHTML = "";
this.#el.modelInfoView.style.display = "none";
this.#modelTab_updateModels();
}
return renamed;
})
.catch(err => {
console.log(err);
return false;
});
}
buttonAlert(e.target, renamed);
},
}),
]),
]),
);
} }
if (info["Preview"]) { if (info["Preview"]) {
@@ -2102,7 +2180,7 @@ class ModelManager extends ComfyDialog {
elements.push($el("h2", [key + ":"])); elements.push($el("h2", [key + ":"]));
const noteArea = $el("textarea.comfy-multiline-input", { const noteArea = $el("textarea.comfy-multiline-input", {
value: value, value: value,
rows: 5, rows: 10,
}); });
elements.push(noteArea); elements.push(noteArea);
elements.push($el("button", { elements.push($el("button", {