Add files via upload
This commit is contained in:
@@ -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)
|
||||
@@ -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.")
|
||||
@@ -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>
|
||||
@@ -0,0 +1,3 @@
|
||||
pywebview>=5.1
|
||||
pyinstaller>=6.5.0
|
||||
pywin32>=306; sys_platform == 'win32'
|
||||
Reference in New Issue
Block a user