Add custom nodes, Civitai loras (LFS), and vast.ai setup script
Some checks failed
Python Linting / Run Ruff (push) Has been cancelled
Python Linting / Run Pylint (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.10, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.11, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-stable (12.1, , linux, 3.12, [self-hosted Linux], stable) (push) Has been cancelled
Full Comfy CI Workflow Runs / test-unix-nightly (12.1, , linux, 3.11, [self-hosted Linux], nightly) (push) Has been cancelled
Execution Tests / test (macos-latest) (push) Has been cancelled
Execution Tests / test (ubuntu-latest) (push) Has been cancelled
Execution Tests / test (windows-latest) (push) Has been cancelled
Test server launches without errors / test (push) Has been cancelled
Unit Tests / test (macos-latest) (push) Has been cancelled
Unit Tests / test (ubuntu-latest) (push) Has been cancelled
Unit Tests / test (windows-2022) (push) Has been cancelled

Includes 30 custom nodes committed directly, 7 Civitai-exclusive
loras stored via Git LFS, and a setup script that installs all
dependencies and downloads HuggingFace-hosted models on vast.ai.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-09 00:55:26 +00:00
parent 2b70ab9ad0
commit f09734b0ee
2274 changed files with 748556 additions and 3 deletions

View File

@@ -0,0 +1,32 @@
.rgthree-model-info-card {
display: block;
padding: 8px;
}
.-is-hidden {
display: none;
}
.rgthree-model-info-card {
display: flex;
flex-direction: row;
}
.rgthree-model-info-card > .rgthree-model-info-card-media-container {
width: 100px;
height: auto;
display: block;
margin: 0 8px 0 0;
padding: 0;
flex: 0 0 auto;
}
.rgthree-model-info-card > .rgthree-model-info-card-media-container > img,
.rgthree-model-info-card > .rgthree-model-info-card-media-container > video {
width: 100%;
height: 100%;
object-fit: contain;
}
.rgthree-model-info-card > .rgthree-model-info-card-data-container [bind*="name:"] {
font-size: 1.3em;
margin-bottom: 4px;
font-weight: bold;
}

View File

@@ -0,0 +1,18 @@
<template>
<div class="rgthree-model-info-card">
<figure class="rgthree-model-info-card-media-container">
<video if="images.0 && images.0.type == 'video'" bind="images.0.url:src" loop autoplay></video>
<img if="images.0 && images.0.type != 'video'" bind="images.0.url:src" loading="lazy" />
</figure>
<div class="rgthree-model-info-card-data-container">
<div bind="name:text"></div>
<div bind="file:text"></div>
<div bind="getModified(modified):text"></div>
<div bind="sha256:text"></div>
<div bind="hasInfoFile"></div>
<a if="getCivitaiLink(links)" bind="getCivitaiLink(links):href">Civitai</a>
</div>
</div>
</template>

View File

@@ -0,0 +1,41 @@
import { RgthreeCustomElement } from "../../common/components/base_custom_element.js";
export class RgthreeModelInfoCard extends RgthreeCustomElement {
constructor() {
super(...arguments);
this.data = {};
}
getModified(value, data, currentElement, contextElement) {
const date = new Date(value);
return String(`${date.toLocaleDateString()} ${date.toLocaleTimeString()}`);
}
getCivitaiLink(links) {
return (links === null || links === void 0 ? void 0 : links.find((i) => i.includes("civitai.com/models"))) || null;
}
setModelData(data) {
this.data = data;
}
hasBaseModel(baseModel) {
return this.data.baseModel === baseModel;
}
hasData(field) {
var _a;
if (field === "civitai") {
return !!((_a = this.getCivitaiLink(this.data.links)) === null || _a === void 0 ? void 0 : _a.length);
}
return !!this.data[field];
}
matchesQueryText(query) {
var _a;
return (_a = (this.data.name || this.data.file)) === null || _a === void 0 ? void 0 : _a.includes(query);
}
hide() {
this.classList.add("-is-hidden");
}
show() {
this.classList.remove("-is-hidden");
}
}
RgthreeModelInfoCard.NAME = "rgthree-model-info-card";
RgthreeModelInfoCard.TEMPLATES = "components/model-info-card.html";
RgthreeModelInfoCard.CSS = "components/model-info-card.css";
RgthreeModelInfoCard.USE_SHADOW = false;

View File

@@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>rgthree-comfy: Models Manager</title>
<link href="./models.css" rel="stylesheet">
<!-- <link rel="icon" href="./favicon.webp"> -->
</head>
<body>
<header>
<input type="text" placeholder="Search" id="searchbox" />
</header>
<ul id="models-list"></ul>
<script type="module">
import {ModelsInfoPage} from './models_info_page.js';
window.page = new ModelsInfoPage();
</script>
</body>
</html>

View File

@@ -0,0 +1,38 @@
:root {
--rgthree-bg-color: rgba(23, 23, 23, 0.9);
--rgthree-on-bg-color: rgba(48, 48, 48, 0.9);
}
html, body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
box-sizing: border-body;
background: var(--rgthree-bg-color);
}
*, *::before, *::after {
box-sizing: inherit;
}
[if-is=false] {
display: none;
}
.models-list {
list-style: none;
margin: 0;
padding: 0;
}
.model-item {
display: block;
margin: 8px;
}
rgthree-model-info-card {
background: var(--rgthree-on-bg-color);
margin: 16px;
display: block;
border-radius: 4px;
}

View File

@@ -0,0 +1,121 @@
import { createElement, getActionEls, query, queryAll } from "../common/utils_dom.js";
import { rgthreeApi } from "../common/rgthree_api.js";
import { RgthreeModelInfoCard } from "./components/model-info-card.js";
function parseQuery(query) {
const matches = query.match(/"[^\"]+"/g) || [];
for (const match of matches) {
let cleaned = match.substring(1, match.length - 1);
cleaned = cleaned.replace(/\s+/g, " ").trim().replace(/\s/g, "__SPACE__");
query = query.replace(match, ` ${cleaned} `);
}
const queryParts = query
.replace(/\s+/g, " ")
.trim()
.split(" ")
.map((p) => p.replace(/__SPACE__/g, " "));
return queryParts;
}
export class ModelsInfoPage {
constructor() {
this.selectBaseModel = createElement('select[name="baseModel"][on="change:filter"]');
this.searchbox = query("#searchbox");
this.modelsList = query("#models-list");
this.queryLast = "";
this.doSearchDebounce = 0;
console.log("hello model page");
this.init();
}
async init() {
this.searchbox.addEventListener("input", (e) => {
if (this.doSearchDebounce) {
return;
}
this.doSearchDebounce = setTimeout(() => {
this.doSearch();
this.doSearchDebounce = 0;
}, 250);
});
const loras = await rgthreeApi.getLorasInfo({ light: true });
console.log(loras);
const baseModels = new Set();
for (const lora of loras) {
const el = RgthreeModelInfoCard.create();
el.setModelData(lora);
el.bindWhenConnected(lora);
console.log(el);
lora.baseModel && baseModels.add(lora.baseModel);
this.modelsList.appendChild(createElement("li.model-item", { child: el }));
}
if (baseModels.size > 1) {
createElement(`option[value="ALL"][text="Choose base model."]`, {
parent: this.selectBaseModel,
});
for (const baseModel of baseModels.values()) {
createElement(`option[value="${baseModel}"][text="${baseModel}"]`, {
parent: this.selectBaseModel,
});
}
this.searchbox.insertAdjacentElement("afterend", this.selectBaseModel);
}
const data = getActionEls(document.body);
for (const dataItem of Object.values(data)) {
for (const [event, action] of Object.entries(dataItem.actions)) {
dataItem.el.addEventListener(event, (e) => {
if (typeof this[action] != "function") {
throw new Error(`"${action}" does not exist on instance.`);
}
this[action](e);
});
}
}
}
filter() {
const parts = parseQuery(this.queryLast);
const baseModel = this.selectBaseModel.value;
const els = queryAll(RgthreeModelInfoCard.NAME);
const shouldHide = (el) => {
let hide = baseModel !== "ALL" && !el.hasBaseModel(baseModel);
if (!hide) {
for (let part of parts) {
let negate = false;
if (part.startsWith("-")) {
negate = true;
part = part.substring(1);
}
if (!part)
continue;
if (part.startsWith("has:")) {
if (part === "has:civitai") {
hide = !el.hasData(part.replace("has:", ""));
}
}
else {
hide = !el.matchesQueryText(part);
}
hide = negate ? !hide : hide;
if (hide) {
break;
}
}
}
return hide;
};
for (const el of els) {
const hide = shouldHide(el);
if (hide) {
el.hide();
}
else {
el.show();
}
}
console.log("filter");
}
doSearch() {
const query = this.searchbox.value.trim();
if (this.queryLast != query) {
this.queryLast = query;
this.filter();
}
}
}