diff --git a/Main Pipeline.png b/Main Pipeline.png new file mode 100644 index 0000000..6aa04b2 Binary files /dev/null and b/Main Pipeline.png differ diff --git a/README.md b/README.md index 1a0fed4..600ed91 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,152 @@ -# ComfyUI-Prompt-DB -A powerful Prompt and LoRA database manager with a Web UI for ComfyUI +# šŸ“¦ ComfyUI Prompt Database + +A powerful prompt management system for ComfyUI. Store, categorize, search, and combine prompts with full LoRA integration — all from a beautiful built-in web UI. + +![Main Pipeline](Main%20Pipeline.png) + +--- + +## ✨ Features + +### šŸ—‚ļø Prompt Management (Web UI) +- **Categories** — organize prompts into named groups +- **Create / Edit / Delete** prompts with a slide-out drawer +- **Search** across names, text, and categories +- **Drag & Drop** — reorder prompts and move them between categories +- **Grid & List views** — toggle layout with persistent preference +- **Sorting** — by name (A→Z / Z→A), newest, oldest, category, or custom order +- **Export / Import** — backup your entire database as JSON; import with **Merge** (append new, skip duplicates) or **Overwrite** +- **Keyboard shortcuts** — `Ctrl+N` new prompt, `/` focus search, `Esc` close + +### šŸ“– LoRA Integration +- **Attach LoRAs** to any prompt with custom strength values (supports negative values) +- **LoRA auto-discovery** — scans your ComfyUI `loras` folder +- **Per-LoRA enable/disable** toggle in Apply Lora Stack (inline circle ā—) +- **Strength adjustment** — ā—€ ā–¶ arrows or manual input, range -2.0 to 2.0 +- **Reset to defaults** — restore original prompt-defined LoRA strengths + +### šŸ”— Node System +Four nodes that work together: + +| Node | Purpose | +|------|---------| +| **Prompt DB Manager** | Opens the web UI for managing your database | +| **Prompt Selector** | Pick one prompt from a category → outputs text + LoRA stack | +| **Prompt Combiner** | Connect multiple selectors → combines all prompts into one string + merged LoRA stack | +| **Apply Lora Stack** | Takes a LoRA stack and applies it to model + clip | + +--- + +## šŸš€ Installation + +### ComfyUI Manager +Search for **"Prompt Database"** in the ComfyUI Manager and install. + +### Manual +```bash +cd ComfyUI/custom_nodes/ +git clone https://github.com/your-username/comfyui-prompt-db.git +``` + +Restart ComfyUI. No additional dependencies required. + +--- + +## šŸ“‹ Nodes + +### Prompt DB Manager +Opens the Prompt Database web interface. Add it to your workflow and click the **"šŸ“‚ Open Manager"** button. + +**Outputs:** None (management only) + +--- + +### Prompt Selector +Select a single prompt from a category. + +**Outputs:** +| Output | Type | Description | +|--------|------|-------------| +| `prompt_text` | STRING | The selected prompt's text | +| `db_selection` | DB_SELECTION | Category name (connect to Combiner) | +| `lora_stack` | LORA_STACK | LoRAs attached to this prompt | + +--- + +### Prompt Combiner +Accepts multiple `DB_SELECTION` inputs (from Prompt Selectors). Each connected input shows a dropdown for selecting a prompt within that category. Combines all selected prompts into one string. + +**Inputs:** +- `separator` — how to join prompts: `, ` / newline / space / none +- `selection_1..N` — DB_SELECTION inputs (dynamic, auto-expands) + +**Outputs:** +| Output | Type | Description | +|--------|------|-------------| +| `combined_prompt` | STRING | All selected prompts joined by separator | +| `lora_stack` | LORA_STACK | Merged LoRAs from all selected prompts | + +**Features:** +- **Inline mute toggle** (ā— circle) — mute individual categories without disconnecting +- **ā—€ ā–¶ arrows** — cycle through prompts quickly +- **Click prompt name** — opens dropdown for full selection +- Prompts are combined **in input order** (selection_1, selection_2, ...) + +--- + +### Apply Lora Stack +Applies a `LORA_STACK` to model and clip. + +**Inputs:** +| Input | Type | Description | +|-------|------|-------------| +| `model` | MODEL | Base model | +| `clip` | CLIP | CLIP model | +| `lora_stack` | LORA_STACK | LoRAs to apply | + +**Outputs:** `model`, `clip` (with LoRAs applied) + +**Features:** +- **Per-LoRA toggle** — click the circle (ā—) to enable/disable individual LoRAs +- **Strength control** — ā—€ ā–¶ arrows to adjust, supports -2.0 to 2.0 range +- **Reset Values** button — restores all strengths to prompt defaults +- Missing LoRA files are skipped gracefully with a console warning + +--- + +## šŸ”§ Typical Workflow + +``` +[Prompt Selector: Character] ──► selection_1 ─┐ +[Prompt Selector: Mood] ──► selection_2 ─┤ +[Prompt Selector: Clothes] ──► selection_3 ─┼──► [Prompt Combiner] ──► combined_prompt ──► [CLIP Text Encode] +[Prompt Selector: Action] ──► selection_4 ─┤ │ +[Prompt Selector: Location] ──► selection_5 ā”€ā”˜ └── lora_stack ──► [Apply Lora Stack] ──► model/clip +``` + +1. Add a **Prompt DB Manager** node and create your categories & prompts +2. Add **Prompt Selector** nodes for each category +3. Connect them to a **Prompt Combiner** to merge everything +4. Route the `lora_stack` output to **Apply Lora Stack** + +--- + +## šŸ“ Data Storage + +Prompts are stored in `prompts.json` in the node pack directory. Back up this file or use the **Export** feature in the web UI. + +--- + +## āŒØļø Keyboard Shortcuts (Web UI) + +| Shortcut | Action | +|----------|--------| +| `Ctrl + N` | Create new prompt | +| `/` | Focus search bar | +| `Esc` | Close drawer / modal | + +--- + +## šŸ“„ License + +MIT diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c5c11b8 --- /dev/null +++ b/__init__.py @@ -0,0 +1,28 @@ +""" +ComfyUI Custom Node Pack: Prompt Database +========================================== +Allows storing, categorizing, and selecting prompts from a local JSON database. +""" + +from .nodes import PromptDBManager, PromptSelector, PromptCombiner, ApplyLoraStack + +# Register API routes on import +from . import api_routes # noqa: F401 + +NODE_CLASS_MAPPINGS = { + "PromptDBManager": PromptDBManager, + "PromptSelector": PromptSelector, + "PromptCombiner": PromptCombiner, + "ApplyLoraStack": ApplyLoraStack, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "PromptDBManager": "Prompt DB Manager", + "PromptSelector": "Prompt Selector", + "PromptCombiner": "Prompt Combiner", + "ApplyLoraStack": "Apply Lora Stack", +} + +WEB_DIRECTORY = "./web" + +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] diff --git a/api_routes.py b/api_routes.py new file mode 100644 index 0000000..537a3d5 --- /dev/null +++ b/api_routes.py @@ -0,0 +1,177 @@ +""" +REST API routes for Prompt Database CRUD operations. +Registered with ComfyUI's PromptServer on import. +""" + +import os +from aiohttp import web +from server import PromptServer +from . import db + +HTML_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "web") + +routes = PromptServer.instance.routes + + +@routes.get("/prompt_db/manager") +async def serve_manager(request): + """Serve the Prompt Database Manager HTML page.""" + html_path = os.path.join(HTML_DIR, "prompt_manager.html") + return web.FileResponse(html_path) + + +@routes.get("/prompt_db/categories") +async def get_categories(request): + categories = db.get_categories() + return web.json_response({"categories": categories}) + + +@routes.get("/prompt_db/prompts") +async def get_prompts(request): + category = request.query.get("category", None) + prompts = db.get_prompts(category) + return web.json_response({"prompts": prompts}) + + +@routes.get("/prompt_db/all") +async def get_all(request): + data = db.load_db() + return web.json_response(data) + + +@routes.get("/prompt_db/loras") +async def get_loras(request): + """Return list of available LoRA files from ComfyUI's models folder.""" + loras = db.get_available_loras() + return web.json_response({"loras": loras}) + + +@routes.post("/prompt_db/prompt") +async def add_prompt(request): + try: + body = await request.json() + category = body.get("category", "") + name = body.get("name", "") + text = body.get("text", "") + loras = body.get("loras", []) + + if not category or not name: + return web.json_response( + {"error": "category and name are required"}, status=400 + ) + + prompt = db.add_prompt(category, name, text, loras=loras) + return web.json_response({"prompt": prompt}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.put("/prompt_db/prompt/{prompt_id}") +async def update_prompt(request): + try: + prompt_id = request.match_info["prompt_id"] + body = await request.json() + result = db.update_prompt( + prompt_id, + category=body.get("category"), + name=body.get("name"), + text=body.get("text"), + loras=body.get("loras"), + ) + if result: + return web.json_response({"prompt": result}) + return web.json_response({"error": "Prompt not found"}, status=404) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.delete("/prompt_db/prompt/{prompt_id}") +async def delete_prompt(request): + try: + prompt_id = request.match_info["prompt_id"] + if db.delete_prompt(prompt_id): + return web.json_response({"success": True}) + return web.json_response({"error": "Prompt not found"}, status=404) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.post("/prompt_db/category") +async def add_category(request): + try: + body = await request.json() + name = body.get("name", "") + if not name: + return web.json_response({"error": "name is required"}, status=400) + if db.add_category(name): + return web.json_response({"success": True}) + return web.json_response({"error": "Category already exists"}, status=409) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.delete("/prompt_db/category/{name}") +async def delete_category(request): + try: + name = request.match_info["name"] + if db.delete_category(name): + return web.json_response({"success": True}) + return web.json_response({"error": "Category not found"}, status=404) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.put("/prompt_db/categories/reorder") +async def reorder_categories(request): + try: + body = await request.json() + ordered = body.get("categories", []) + if not ordered: + return web.json_response({"error": "categories list required"}, status=400) + if db.reorder_categories(ordered): + return web.json_response({"success": True}) + return web.json_response( + {"error": "Category list does not match existing categories"}, status=400 + ) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.put("/prompt_db/prompts/reorder") +async def reorder_prompts(request): + try: + body = await request.json() + ordered_ids = body.get("prompt_ids", []) + if not ordered_ids: + return web.json_response({"error": "prompt_ids list required"}, status=400) + if db.reorder_prompts(ordered_ids): + return web.json_response({"success": True}) + return web.json_response({"error": "Reorder failed"}, status=400) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.get("/prompt_db/export") +async def export_db_route(request): + try: + data = db.export_db() + return web.json_response(data) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +@routes.post("/prompt_db/import") +async def import_db_route(request): + try: + body = await request.json() + mode = body.get("mode", "merge") + incoming = body.get("data") + if not incoming or "categories" not in incoming or "prompts" not in incoming: + return web.json_response( + {"error": "Invalid import data: must have categories and prompts"}, + status=400, + ) + stats = db.import_db(incoming, mode) + return web.json_response({"success": True, **stats}) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) diff --git a/db.py b/db.py new file mode 100644 index 0000000..a77a250 --- /dev/null +++ b/db.py @@ -0,0 +1,279 @@ +""" +Thread-safe JSON database for storing prompts with categories. +Prompts can optionally have LoRA attachments. +""" + +import json +import os +import uuid +import threading + +DB_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "prompts.json") +_lock = threading.Lock() + +DEFAULT_DB = { + "categories": ["Location", "Character", "Style", "Mood"], + "prompts": [] +} + + +def load_db(): + """Load the database from disk. Returns default if file missing or corrupted.""" + with _lock: + if not os.path.exists(DB_FILE): + save_db_unsafe(DEFAULT_DB) + return DEFAULT_DB.copy() + try: + with open(DB_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + if "categories" not in data or "prompts" not in data: + data = DEFAULT_DB.copy() + save_db_unsafe(data) + # Ensure all prompts have a loras field + for p in data.get("prompts", []): + if "loras" not in p: + p["loras"] = [] + return data + except (json.JSONDecodeError, IOError): + save_db_unsafe(DEFAULT_DB) + return DEFAULT_DB.copy() + + +def save_db_unsafe(data): + """Save database without acquiring lock (caller must hold lock).""" + with open(DB_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + +def save_db(data): + """Save database to disk (thread-safe).""" + with _lock: + save_db_unsafe(data) + + +def get_categories(): + """Return list of category names.""" + data = load_db() + return data.get("categories", []) + + +def get_prompts(category=None): + """Return prompts, optionally filtered by category.""" + data = load_db() + prompts = data.get("prompts", []) + if category: + prompts = [p for p in prompts if p.get("category") == category] + return prompts + + +def get_prompt_by_id(prompt_id): + """Return a single prompt by its ID, or None.""" + data = load_db() + for p in data.get("prompts", []): + if p.get("id") == prompt_id: + return p + return None + + +def get_prompt_by_name(name, category=None): + """Return a single prompt by its name and optionally category, or None.""" + data = load_db() + for p in data.get("prompts", []): + if p.get("name") == name: + if category is None or p.get("category") == category: + return p + return None + + +def add_prompt(category, name, text, loras=None): + """Add a new prompt. Returns the new prompt dict.""" + with _lock: + data = _load_unsafe() + prompt = { + "id": str(uuid.uuid4()), + "category": category, + "name": name, + "text": text, + "loras": loras or [], + } + data["prompts"].append(prompt) + save_db_unsafe(data) + return prompt + + +def update_prompt(prompt_id, category=None, name=None, text=None, loras=None): + """Update an existing prompt by ID. Returns updated prompt or None.""" + with _lock: + data = _load_unsafe() + for p in data["prompts"]: + if p["id"] == prompt_id: + if category is not None: + p["category"] = category + if name is not None: + p["name"] = name + if text is not None: + p["text"] = text + if loras is not None: + p["loras"] = loras + save_db_unsafe(data) + return p + return None + + +def delete_prompt(prompt_id): + """Delete a prompt by ID. Returns True if found and deleted.""" + with _lock: + data = _load_unsafe() + original_len = len(data["prompts"]) + data["prompts"] = [p for p in data["prompts"] if p["id"] != prompt_id] + if len(data["prompts"]) < original_len: + save_db_unsafe(data) + return True + return False + + +def add_category(name): + """Add a new category. Returns True if added, False if already exists.""" + with _lock: + data = _load_unsafe() + if name in data["categories"]: + return False + data["categories"].append(name) + save_db_unsafe(data) + return True + + +def delete_category(name): + """Delete a category and all its prompts. Returns True if found and deleted.""" + with _lock: + data = _load_unsafe() + if name not in data["categories"]: + return False + data["categories"].remove(name) + data["prompts"] = [p for p in data["prompts"] if p["category"] != name] + save_db_unsafe(data) + return True + + +def reorder_categories(ordered_names): + """Reorder categories to match the given list. Returns True if successful.""" + with _lock: + data = _load_unsafe() + existing = set(data["categories"]) + given = set(ordered_names) + if existing != given: + return False + data["categories"] = list(ordered_names) + save_db_unsafe(data) + return True + + +def reorder_prompts(ordered_ids): + """Reorder prompts to match the given ID list. Unmentioned prompts keep their relative order at the end.""" + with _lock: + data = _load_unsafe() + by_id = {p["id"]: p for p in data["prompts"]} + reordered = [] + for pid in ordered_ids: + if pid in by_id: + reordered.append(by_id.pop(pid)) + # Append any remaining prompts not in the ordered list + for p in data["prompts"]: + if p["id"] in by_id: + reordered.append(p) + data["prompts"] = reordered + save_db_unsafe(data) + return True + + +def get_available_loras(): + """Return list of available LoRA filenames from ComfyUI's models folder.""" + try: + import folder_paths + return folder_paths.get_filename_list("loras") + except Exception: + return [] + + +def export_db(): + """Return full database dict for export.""" + return load_db() + + +def import_db(incoming, mode="merge"): + """Import prompts from an incoming dict. + mode='overwrite': replace entire DB. + mode='merge': add new categories and prompts, skip duplicates (by name+category). + Returns dict with stats: {categories_added, prompts_added, prompts_skipped}. + """ + with _lock: + if mode == "overwrite": + # Ensure all prompts have IDs and loras + for p in incoming.get("prompts", []): + if "id" not in p: + p["id"] = str(uuid.uuid4()) + if "loras" not in p: + p["loras"] = [] + save_db_unsafe(incoming) + return { + "categories_added": len(incoming.get("categories", [])), + "prompts_added": len(incoming.get("prompts", [])), + "prompts_skipped": 0, + } + + # Merge mode + data = _load_unsafe() + cats_added = 0 + prompts_added = 0 + prompts_skipped = 0 + + # Add missing categories + existing_cats = set(data["categories"]) + for cat in incoming.get("categories", []): + if cat not in existing_cats: + data["categories"].append(cat) + existing_cats.add(cat) + cats_added += 1 + + # Add prompts that don't exist yet (by name + category) + existing_prompts = {(p["name"], p["category"]) for p in data["prompts"]} + for p in incoming.get("prompts", []): + key = (p.get("name", ""), p.get("category", "")) + if key in existing_prompts: + prompts_skipped += 1 + continue + prompt = { + "id": p.get("id") or str(uuid.uuid4()), + "name": p.get("name", ""), + "category": p.get("category", ""), + "text": p.get("text", ""), + "loras": p.get("loras", []), + } + data["prompts"].append(prompt) + existing_prompts.add(key) + prompts_added += 1 + + save_db_unsafe(data) + return { + "categories_added": cats_added, + "prompts_added": prompts_added, + "prompts_skipped": prompts_skipped, + } + + +def _load_unsafe(): + """Load database without lock (internal use only).""" + if not os.path.exists(DB_FILE): + save_db_unsafe(DEFAULT_DB) + return DEFAULT_DB.copy() + try: + with open(DB_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + # Ensure all prompts have loras field + for p in data.get("prompts", []): + if "loras" not in p: + p["loras"] = [] + return data + except (json.JSONDecodeError, IOError): + save_db_unsafe(DEFAULT_DB) + return DEFAULT_DB.copy() diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..04c0d83 --- /dev/null +++ b/nodes.py @@ -0,0 +1,253 @@ +""" +ComfyUI custom nodes for Prompt Database. +""" + +import json as _json +import re as _re +import folder_paths + + +def _clean_name(name): + """Strip the šŸ“– LoRA marker suffix added by the JS frontend.""" + return _re.sub(r'\s*šŸ“–$', '', name).strip() +import comfy.sd +import comfy.utils +from . import db + + +class PromptDBManager: + """ + Management node — provides a UI button to open the prompt database manager. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": {}, + "hidden": { + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = () + FUNCTION = "manage" + CATEGORY = "Prompt Database" + OUTPUT_NODE = True + + def manage(self, unique_id=None): + return {} + + +class PromptSelector: + """ + Selector node — pick a category and prompt. + Outputs: + - prompt_text (STRING): the selected prompt text (no LoRA tags) + - db_selection (DB_SELECTION): the category name (for PromptCombiner) + - lora_stack (LORA_STACK): LoRAs attached to the selected prompt + """ + + @classmethod + def INPUT_TYPES(cls): + categories = db.get_categories() + if not categories: + categories = ["(no categories)"] + + all_prompts = db.get_prompts() + prompt_names = list({p["name"] for p in all_prompts}) + if not prompt_names: + prompt_names = ["(no prompts)"] + + return { + "required": { + "category": (categories, {"default": categories[0]}), + "prompt_name": (prompt_names, {"default": prompt_names[0]}), + }, + } + + RETURN_TYPES = ("STRING", "DB_SELECTION", "LORA_STACK") + RETURN_NAMES = ("prompt_text", "db_selection", "lora_stack") + FUNCTION = "select_prompt" + CATEGORY = "Prompt Database" + + def select_prompt(self, category, prompt_name): + prompt_name = _clean_name(prompt_name) + prompt = db.get_prompt_by_name(prompt_name, category) + if not prompt: + prompt = db.get_prompt_by_name(prompt_name) + if not prompt: + return ("", category, []) + + text = prompt["text"] + loras = prompt.get("loras", []) + # Normalize lora entries + lora_stack = [] + for l in loras: + lora_stack.append({ + "name": l.get("name", ""), + "strength": float(l.get("strength", 1.0)), + }) + + return (text, category, lora_stack) + + @classmethod + def IS_CHANGED(cls, category, prompt_name): + prompt_name = _clean_name(prompt_name) + prompt = db.get_prompt_by_name(prompt_name, category) + if prompt: + return prompt["text"] + str(prompt.get("loras", [])) + return "" + + +class PromptCombiner: + """ + Combiner node — accepts multiple DB_SELECTION inputs (category names), + shows a prompt dropdown per category inside the node, + and combines selected prompts into one STRING + merged LORA_STACK. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "separator": ( + ["comma_space", "newline", "space", "none"], + {"default": "comma_space"}, + ), + "prompt_selections": ( + "STRING", + {"default": "{}", "multiline": False}, + ), + }, + "optional": { + "selection_1": ("DB_SELECTION", {"forceInput": True}), + }, + } + + RETURN_TYPES = ("STRING", "LORA_STACK") + RETURN_NAMES = ("combined_prompt", "lora_stack") + FUNCTION = "combine" + CATEGORY = "Prompt Database" + OUTPUT_NODE = True + + def combine(self, separator="comma_space", prompt_selections="{}", **kwargs): + sep_map = { + "comma_space": ", ", + "newline": "\n", + "space": " ", + "none": "", + } + sep = sep_map.get(separator, ", ") + + try: + selections = _json.loads(prompt_selections) + except _json.JSONDecodeError: + selections = {} + + texts = [] + merged_loras = [] + seen_loras = set() + + selection_keys = [k for k in kwargs.keys() if k.startswith("selection_")] + selection_keys.sort(key=lambda k: int(k.split("_")[1])) + + for key in selection_keys: + category = kwargs[key] + if not category: + continue + prompt_name = _clean_name(selections.get(key, "")) + if not prompt_name: + continue + prompt = db.get_prompt_by_name(prompt_name, category) + if prompt: + texts.append(prompt["text"]) + # Collect LoRAs (deduplicate by name) + for l in prompt.get("loras", []): + lname = l.get("name", "") + if lname and lname not in seen_loras: + seen_loras.add(lname) + merged_loras.append({ + "name": lname, + "strength": float(l.get("strength", 1.0)), + }) + + combined = sep.join(texts) + return (combined, merged_loras) + + @classmethod + def IS_CHANGED(cls, separator, prompt_selections="{}", **kwargs): + return prompt_selections + str(kwargs) + + +class ApplyLoraStack: + """ + Applies LoRAs from a LORA_STACK to a model and clip. + The lora_config hidden widget stores per-LoRA overrides + (enable/disable + custom strength). + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "model": ("MODEL",), + "clip": ("CLIP",), + "lora_config": ( + "STRING", + {"default": "{}", "multiline": False}, + ), + }, + "optional": { + "lora_stack": ("LORA_STACK",), + }, + } + + RETURN_TYPES = ("MODEL", "CLIP") + RETURN_NAMES = ("model", "clip") + FUNCTION = "apply_loras" + CATEGORY = "Prompt Database" + + def apply_loras(self, model, clip, lora_config="{}", lora_stack=None): + if not lora_stack: + return (model, clip) + + # Parse overrides + try: + config = _json.loads(lora_config) + except _json.JSONDecodeError: + config = {} + + for lora_entry in lora_stack: + name = lora_entry.get("name", "") + default_strength = lora_entry.get("strength", 1.0) + if not name: + continue + + # Apply per-LoRA overrides from config + override = config.get(name, {}) + if not override.get("enabled", True): + print(f"[PromptDB] LoRA skipped (disabled): {name}") + continue + + strength = float(override.get("strength", default_strength)) + + # Load LoRA file + lora_path = folder_paths.get_full_path("loras", name) + if not lora_path: + print(f"[PromptDB] LoRA file not found: {name}") + continue + + try: + lora = comfy.utils.load_torch_file(lora_path, safe_load=True) + model, clip = comfy.sd.load_lora_for_models( + model, clip, lora, strength, strength + ) + print(f"[PromptDB] LoRA applied: {name} (strength={strength:.2f})") + except Exception as e: + print(f"[PromptDB] Error loading LoRA {name}: {e}") + + return (model, clip) + + @classmethod + def IS_CHANGED(cls, model, clip, lora_config="{}", lora_stack=None): + return str(lora_stack) + lora_config diff --git a/prompts.json b/prompts.json new file mode 100644 index 0000000..e69de29