Add files via upload

This commit is contained in:
0velia
2026-07-10 23:12:15 +07:00
committed by GitHub
parent 480c8e68d0
commit 51da712fda
4 changed files with 401 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
import os
import sys
import webview
def get_resource_path(relative_path):
""" Get the absolute path to the resource, working correctly both
during local development and after compilation inside PyInstaller """
try:
# PyInstaller creates a temporary folder and stores the path in _MEIPASS
base_path = sys._MEIPASS
except Exception:
base_path = os.path.dirname(os.path.realpath(__file__))
return os.path.join(base_path, relative_path)
if __name__ == "__main__":
# Locate the embedded HTML generator file
html_path = get_resource_path("generator.html")
# Create the standalone window environment
window = webview.create_window(
title="Universal Dropdown Node Generator",
url=html_path,
width=900,
height=850,
resizable=True
)
# Force the local OS renderer (WebView2 Edge Chromium on Windows)
# so the interface looks identical and functions smoothly offline on all PCs
webview.start(gui="cef" if os.name != "nt" else "mshtml" if not html_path else None)
+101
View File
@@ -0,0 +1,101 @@
import os
import sys
import subprocess
import shutil
def run_command(command, env=None):
"""Helper to run system commands and stream the output to the console"""
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=True,
text=True,
env=env
)
while True:
output = process.stdout.readline()
if output == '' and process.poll() is not None:
break
if output:
print(output.strip())
return process.poll()
if __name__ == "__main__":
current_dir = os.path.dirname(os.path.realpath(__file__))
venv_dir = os.path.join(current_dir, "builder_env")
# Calculate the root folder path (my_universal_dropdowns)
# Moving up two levels from: .../my_universal_dropdowns/generator/build.py
root_dir = os.path.abspath(os.path.join(current_dir, ".."))
final_exe_path = os.path.join(root_dir, "Node Builder Assistant.exe")
print("🚀 Starting One-Click Standalone Executable Builder...\n")
# 1. Create Virtual Environment
if not os.path.exists(venv_dir):
print("📦 Step 1: Creating isolated virtual environment...")
if run_command(f'"{sys.executable}" -m venv builder_env') != 0:
print("❌ Failed to create virtual environment.")
sys.exit(1)
else:
print("✨ Virtual environment already exists, skipping creation.")
# Determine paths inside venv based on OS
if os.name == 'nt': # Windows
pip_path = os.path.join(venv_dir, "Scripts", "pip.exe")
pyinstaller_path = os.path.join(venv_dir, "Scripts", "pyinstaller.exe")
else: # Mac/Linux fallback
pip_path = os.path.join(venv_dir, "bin", "pip")
pyinstaller_path = os.path.join(venv_dir, "bin", "pyinstaller")
# 2. Upgrade pip and install requirements
print("\n📥 Step 2: Installing requirements into isolated environment...")
run_command(f'"{pip_path}" install --upgrade pip')
req_file = os.path.join(current_dir, "requirements.txt")
if os.path.exists(req_file):
if run_command(f'"{pip_path}" install -r "{req_file}"') != 0:
print("❌ Failed to install dependencies.")
sys.exit(1)
else:
print("❌ requirements.txt not found! Please create it first.")
sys.exit(1)
# 3. Compile with PyInstaller
print("\n🛠️ Step 3: Compiling hyper-lean executable via PyInstaller...")
build_cmd = f'"{pyinstaller_path}" --clean --noconsole --onefile --add-data "generator.html;." app.py'
if run_command(build_cmd) == 0:
print("\n✅ Compilation successful!")
# 4. Relocate and Rename the Executable
print("\n🚚 Step 4: Moving and renaming executable to root directory...")
compiled_exe = os.path.join(current_dir, "dist", "app.exe")
try:
# Overwrite if it already exists from a previous build
if os.path.exists(final_exe_path):
os.remove(final_exe_path)
shutil.move(compiled_exe, final_exe_path)
print(f"✨ Successfully deployed to: {final_exe_path}")
except Exception as e:
print(f"❌ Failed to move or rename executable: {e}")
# 5. Clean up temporary build clutter
print("\n扫 Step 5: Cleaning up temporary build clutter...")
build_folder = os.path.join(current_dir, "build")
dist_folder = os.path.join(current_dir, "dist")
spec_file = os.path.join(current_dir, "app.spec")
if os.path.exists(build_folder):
shutil.rmtree(build_folder)
if os.path.exists(dist_folder):
shutil.rmtree(dist_folder)
if os.path.exists(spec_file):
os.remove(spec_file)
print("✨ Temporary build cache deleted. Process complete!")
else:
print("\n❌ PyInstaller compilation failed.")
+267
View File
@@ -0,0 +1,267 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ComfyUI Dropdown Node Generator Pro</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #1e1e2e; color: #cdd6f4; margin: 0; padding: 20px; }
.container { max-width: 850px; margin: 0 auto; background: #252538; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
h1, h2 { color: #89b4fa; margin-top: 0; }
.meta-group { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 25px; }
.form-group { margin-bottom: 15px; flex-direction: column; display: flex; }
label { margin-bottom: 5px; font-weight: bold; font-size: 0.9em; color: #a6adc8; }
input { padding: 10px; background: #313244; border: 1px solid #45475a; color: #cdd6f4; border-radius: 4px; font-size: 1rem; }
input:focus { border-color: #89b4fa; outline: none; }
.dropdown-item { background: #11111b; padding: 15px; border-radius: 6px; margin-bottom: 15px; border-left: 4px solid #f38ba8; position: relative; }
.dropdown-item .remove-btn { position: absolute; right: 15px; top: 15px; background: #f38ba8; color: #11111b; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-weight: bold; }
.upload-row { display: flex; gap: 10px; align-items: center; }
.upload-row input[type="text"] { flex-grow: 1; }
.file-label { display: inline-block; padding: 10px 15px; background: #cdd6f4; color: #11111b; font-weight: bold; border-radius: 4px; cursor: pointer; text-align: center; }
.file-input { display: none; }
.btn { padding: 12px 20px; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; font-size: 1rem; transition: background 0.2s; }
.btn-add { background: #a6e3a1; color: #11111b; margin-bottom: 20px; }
.btn-build { background: #89b4fa; color: #11111b; width: 100%; font-size: 1.1em; margin-top: 10px; margin-bottom: 20px; }
.btn-action { background: #a6e3a1; color: #11111b; margin-right: 10px; }
.btn-download { background: #abe9b3; color: #11111b; }
.btn:hover { opacity: 0.9; }
.action-group { display: none; margin-top: 15px; }
pre { background: #11111b; padding: 15px; border-radius: 6px; overflow-x: auto; max-height: 400px; border: 1px solid #45475a; margin-top: 10px; }
code { font-family: 'Courier New', Courier, monospace; color: #a6e3a1; }
/* Help Instructions Styles */
.help-panel { background: #181825; border: 1px solid #45475a; border-radius: 6px; padding: 20px; margin-bottom: 30px; }
.help-panel ul { padding-left: 20px; margin: 5px 0; }
.help-panel li { margin-bottom: 8px; color: #cdd6f4; }
.example-box { background: #313244; padding: 10px; border-radius: 4px; font-family: monospace; color: #f9e2af; margin-top: 5px; white-space: pre-line; }
</style>
</head>
<body>
<div class="container">
<h1>Universal Dropdown Node Generator</h1>
<div class="help-panel">
<h2 style="color: #f9e2af; font-size: 1.2rem; margin-bottom: 10px;">📋 Self-Contained Folder Structure & Formatting</h2>
<p style="font-size: 0.95em; color: #a6adc8;">The generated node aggregates all dropdown choices into a single comma-separated output. The node scans a subfolder named <code>lists</code> inside your custom node directory:</p>
<div class="example-box" style="color: #a6e3a1;">ComfyUI/
└── custom_nodes/
└── my_combination_node/
├── your_node_file.py
└── lists/
├── hair_length.txt
├── hair_color.txt
└── hairstyle.txt</div>
<ul style="margin-top: 15px;">
<li><strong>Formatting Rule:</strong> Your text files must feature exactly <strong>one option per line</strong>.</li>
<li><strong>Restrictions:</strong> No empty lines, no double quotes (<code>"</code>), and no backslashes (<code>\</code>). Validated on upload.</li>
</ul>
</div>
<div class="meta-group">
<div class="form-group">
<label for="displayName">UI Display Name</label>
<input type="text" id="displayName" value="Universal Text Selector" oninput="autoGenerateClassName(this.value)">
</div>
<div class="form-group">
<label for="nodeName">Python Class Name (Automatically Generated)</label>
<input type="text" id="nodeName" value="UniversalTextSelectorNode" readonly style="background: #2b2b3c; color: #b4befe; cursor: not-allowed;">
</div>
</div>
<h2>Dropdown Elements</h2>
<div id="dropdownsContainer"></div>
<button class="btn btn-add" onclick="addDropdown()">+ Add Dropdown Menu</button>
<button class="btn btn-build" onclick="generateNodeCode()">Build Custom Node</button>
<div id="resultSection" class="action-group">
<h2>Generated Code</h2>
<button class="btn btn-action" onclick="copyCode()">Copy Code</button>
<button class="btn btn-action btn-download" onclick="downloadCode()">Download Code</button>
<pre><code id="codeOutput"></code></pre>
</div>
</div>
<script>
let dropdownCount = 0;
function autoGenerateClassName(val) {
let sanitized = val.replace(/[^a-zA-Z0-9 ]/g, '');
let camelCase = sanitized.split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
if(!camelCase.endsWith('Node')) {
camelCase += 'Node';
}
document.getElementById('nodeName').value = camelCase || "MyDropdownNode";
}
function addDropdown() {
dropdownCount++;
const container = document.getElementById('dropdownsContainer');
const div = document.createElement('div');
div.className = 'dropdown-item';
div.id = `item_${dropdownCount}`;
div.innerHTML = `
<button class="remove-btn" onclick="removeDropdown(${dropdownCount})">X</button>
<div class="form-group" style="margin-bottom: 10px;">
<label>Dropdown Label Name (e.g., hair_length, hair_color)</label>
<input type="text" class="input-label" value="attribute_${dropdownCount}" oninput="updateFileName(this)">
</div>
<div class="form-group">
<label>Associated Text File Name (Must match your file in custom_node/lists/)</label>
<div class="upload-row">
<input type="text" class="input-file" value="attribute_${dropdownCount}.txt">
<label class="file-label">
Upload
<input type="file" accept=".txt" class="file-input" onchange="handleFileUpload(this, ${dropdownCount})">
</label>
</div>
</div>
`;
container.appendChild(div);
}
function removeDropdown(id) {
document.getElementById(`item_${id}`).remove();
}
function updateFileName(inputElement) {
const item = inputElement.closest('.dropdown-item');
const fileInput = item.querySelector('.input-file');
const sanitized = inputElement.value.toLowerCase().replace(/[^a-z0-9_]/g, '_');
fileInput.value = sanitized ? `${sanitized}.txt` : '';
}
function handleFileUpload(fileInput, id) {
if (fileInput.files.length > 0) {
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const textContent = e.target.result;
const lines = textContent.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const currentLine = lines[i].trim();
if (lines[i] === "" && i < lines.length - 1) {
alert(`❌ File Rejected! Empty line detected on line ${i + 1}.`);
fileInput.value = "";
return;
}
if (currentLine.includes('"') || currentLine.includes('\\')) {
alert(`❌ File Rejected! Illegal character (quotes or backslash) on line ${i + 1}.`);
fileInput.value = "";
return;
}
}
const fileName = file.name;
const item = document.getElementById(`item_${id}`);
item.querySelector('.input-file').value = fileName;
const baseName = fileName.replace(/\.[^/.]+$/, "");
const cleanLabel = baseName.toLowerCase().replace(/[^a-z0-9_]/g, '_');
item.querySelector('.input-label').value = cleanLabel;
};
reader.readAsText(file);
}
}
function generateNodeCode() {
const nodeName = document.getElementById('nodeName').value;
const displayName = document.getElementById('displayName').value;
const items = document.querySelectorAll('.dropdown-item');
if(items.length === 0) {
alert("Please add at least one dropdown element before building.");
return;
}
let configLines = [];
let readLines = [' "clip": ("CLIP",),'];
let params = ['clip'];
let variableNames = [];
items.forEach(item => {
const label = item.querySelector('.input-label').value.trim().replace(/[^a-zA-Z0-9_]/g, '_');
const file = item.querySelector('.input-file').value.trim();
configLines.push(` # Setup list for ${label}\n ${label}_list = ["default_val"]\n ${label}_path = os.path.join(lists_dir, "${file}")\n if os.path.exists(${label}_path):\n with open(${label}_path, "r", encoding="utf-8") as f:\n lines = [l.strip() for l in f.readlines() if l.strip()]\n if lines: ${label}_list = lines`);
readLines.push(` "${label}": (${label}_list, {"default": ${label}_list[0]}),`);
params.push(label);
variableNames.push(label);
});
const pythonCode = `import os
class ${nodeName}:
@classmethod
def INPUT_TYPES(cls):
current_dir = os.path.dirname(os.path.realpath(__file__))
lists_dir = os.path.join(current_dir, "lists")
if not os.path.exists(lists_dir):
os.makedirs(lists_dir, exist_ok=True)
${configLines.join('\n\n')}
return {
"required": {
${readLines.join('\n')}
}
}
RETURN_TYPES = ("STRING", "CONDITIONING",)
RETURN_NAMES = ("PROMPT_STRING", "CONDITIONING",)
FUNCTION = "process_selection"
CATEGORY = "Custom Selection"
def process_selection(self, ${params.join(', ')}):
selections = [${variableNames.join(', ')}]
combined_text = ", ".join([str(item) for item in selections if item])
tokens = clip.tokenize(combined_text)
cond, pooled = clip.encode_from_tokens(tokens, return_pooled=True)
conditioning_output = [[cond, {"pooled_output": pooled}]]
return (combined_text, conditioning_output,)
NODE_CLASS_MAPPINGS = {
"${nodeName}": ${nodeName}
}
NODE_DISPLAY_NAME_MAPPINGS = {
"${nodeName}": "${displayName}"
}
`;
document.getElementById('codeOutput').textContent = pythonCode;
document.getElementById('resultSection').style.display = 'block';
}
function copyCode() {
const codeText = document.getElementById('codeOutput').textContent;
navigator.clipboard.writeText(codeText).then(() => {
alert("Code copied to clipboard!");
});
}
function downloadCode() {
const codeText = document.getElementById('codeOutput').textContent;
const nodeName = document.getElementById('nodeName').value;
const blob = new Blob([codeText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${nodeName.toLowerCase()}.py`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
addDropdown();
</script>
</body>
</html>
+3
View File
@@ -0,0 +1,3 @@
pywebview>=5.1
pyinstaller>=6.5.0
pywin32>=306; sys_platform == 'win32'