212 lines
7.9 KiB
Python
212 lines
7.9 KiB
Python
import os
|
|
import folder_paths
|
|
import random
|
|
import sys
|
|
import subprocess
|
|
|
|
class Prompt_Library:
|
|
"""
|
|
• project dropdown: '<category>/<project>' choices at init
|
|
• index slider: free int from 1 to 99
|
|
• randomize flag: integer 0 oder 1 für Zufall
|
|
• editor_content: Textfeld zum Bearbeiten (wird nicht in Projekten gespeichert)
|
|
• WICHTIG: Nach Dateiauswahl "Load File" Button drücken!
|
|
• "Prompts Folder" Button öffnet den Prompt-Ordner
|
|
• "Refresh" Button aktualisiert die Liste der Prompt-Dateien
|
|
• "Load File" Button lädt die ausgewählte Datei ins Textfeld
|
|
• "Save to File" Button speichert Änderungen in die Datei
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.base = os.path.join(folder_paths.models_dir, "prompts")
|
|
self.refresh_projects()
|
|
self.current_file_path = None
|
|
self.editor_content = "" # Interner Editor-Inhalt
|
|
self.last_project = None # Zum Tracking von Projekt-Wechseln
|
|
|
|
def refresh_projects(self):
|
|
# Lese die Projekte jedes Mal neu ein
|
|
base = self.base
|
|
cats = [d for d in os.listdir(base) if os.path.isdir(os.path.join(base, d))]
|
|
proj_choices = []
|
|
for cat in sorted(cats):
|
|
folder = os.path.join(base, cat)
|
|
for fn in sorted(os.listdir(folder)):
|
|
if fn.lower().endswith(".txt"):
|
|
name = fn[:-4]
|
|
proj_choices.append(f"{cat}/{name}")
|
|
self._proj_choices = proj_choices
|
|
|
|
def load_file_content(self, project):
|
|
"""Lädt den Inhalt der ausgewählten Datei"""
|
|
try:
|
|
category, name = project.split("/", 1)
|
|
path = os.path.join(self.base, category, name + ".txt")
|
|
if os.path.isfile(path):
|
|
with open(path, encoding="utf-8") as f:
|
|
return f.read()
|
|
except:
|
|
pass
|
|
return ""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
instance = s()
|
|
instance.refresh_projects()
|
|
|
|
return {
|
|
"required": {
|
|
"project": (instance._proj_choices,),
|
|
"index": ("INT", {"default": 1, "min": 1, "max": 99}),
|
|
"randomize": ("INT", {"default": 0, "min": 0, "max": 1}),
|
|
"editor_content": ("STRING", {"multiline": True, "default": "", "dynamicPrompts": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "INT")
|
|
FUNCTION = "get_prompt"
|
|
OUTPUT_NODE = True
|
|
CATEGORY = "hexxacubic"
|
|
|
|
# --- BUTTON UI ---
|
|
@staticmethod
|
|
def ui_open_prompts_folder():
|
|
base = os.path.join(folder_paths.models_dir, "prompts")
|
|
if sys.platform == "win32":
|
|
os.startfile(base)
|
|
elif sys.platform == "darwin":
|
|
subprocess.Popen(["open", base])
|
|
else:
|
|
subprocess.Popen(["xdg-open", base])
|
|
return "Prompts-Ordner geöffnet."
|
|
|
|
def ui_refresh_projects(self):
|
|
self.refresh_projects()
|
|
return "Prompt-Liste aktualisiert."
|
|
|
|
def ui_load_file(self, project):
|
|
"""Lädt den Inhalt der ausgewählten Datei ins Textfeld"""
|
|
content = self.load_file_content(project)
|
|
self.editor_content = content
|
|
return {"ui": {"editor_content": content}}
|
|
|
|
def ui_save_to_file(self, project, editor_content):
|
|
"""Speichert den Editor-Inhalt in die ausgewählte Datei"""
|
|
try:
|
|
category, name = project.split("/", 1)
|
|
path = os.path.join(self.base, category, name + ".txt")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(editor_content)
|
|
self.editor_content = editor_content
|
|
return f"Gespeichert: {name}.txt"
|
|
except Exception as e:
|
|
return f"Fehler beim Speichern: {str(e)}"
|
|
|
|
@classmethod
|
|
def UI_BUTTONS(cls):
|
|
return [
|
|
{"label": "⚡ LOAD FILE ⚡", "method": "ui_load_file", "params": ["project"]},
|
|
{"label": "💾 Save to File", "method": "ui_save_to_file", "params": ["project", "editor_content"]},
|
|
{"label": "📁 Prompts Folder", "method": "ui_open_prompts_folder"},
|
|
{"label": "🔄 Refresh List", "method": "ui_refresh_projects"},
|
|
]
|
|
|
|
def get_prompt(self, project, index, randomize, editor_content):
|
|
# Nach jedem Render automatisch neu einlesen
|
|
self.refresh_projects()
|
|
|
|
# Auto-Load wenn Projekt gewechselt wurde oder Editor leer ist
|
|
if project != self.last_project or not editor_content.strip():
|
|
self.last_project = project
|
|
# Lade Dateiinhalt automatisch
|
|
loaded_content = self.load_file_content(project)
|
|
if loaded_content:
|
|
# Trigger UI Update
|
|
return self.process_content(project, index, randomize, loaded_content, trigger_reload=True)
|
|
|
|
# split 'category/project'
|
|
try:
|
|
category, name = project.split("/", 1)
|
|
except ValueError:
|
|
return "", "", project, index
|
|
|
|
path = os.path.join(self.base, category, name + ".txt")
|
|
self.current_file_path = path
|
|
|
|
if not os.path.isfile(path):
|
|
return "", "", project, index
|
|
|
|
return self.process_content(project, index, randomize, editor_content)
|
|
|
|
def process_content(self, project, index, randomize, content_to_parse, trigger_reload=False):
|
|
"""Verarbeitet den Content und gibt Prompts zurück"""
|
|
# Parse sections - auto-numbered based on ### markers
|
|
sections = {}
|
|
current_num = 0
|
|
current_lines = []
|
|
|
|
for line in content_to_parse.split('\n'):
|
|
line = line.rstrip("\n")
|
|
if line.startswith("###"):
|
|
# Save previous section if exists
|
|
if current_num > 0 and current_lines:
|
|
sections[current_num] = current_lines
|
|
# Start new section
|
|
current_num += 1
|
|
current_lines = []
|
|
else:
|
|
if current_num > 0: # Only collect lines after first ###
|
|
current_lines.append(line)
|
|
|
|
# Save last section if exists
|
|
if current_num > 0 and current_lines:
|
|
sections[current_num] = current_lines
|
|
|
|
# determine final idx
|
|
if sections:
|
|
max_idx = len(sections)
|
|
if randomize == 1:
|
|
idx = random.randint(1, max_idx)
|
|
else:
|
|
idx = min(index, max_idx)
|
|
else:
|
|
idx = index
|
|
|
|
# extract prompts
|
|
lines = sections.get(idx, [])
|
|
if '---' in lines:
|
|
sep = lines.index('---')
|
|
pos = "\n".join(lines[:sep]).strip()
|
|
neg = "\n".join(lines[sep+1:]).strip()
|
|
else:
|
|
pos = "\n".join(lines).strip()
|
|
neg = ""
|
|
|
|
# Global prompt (first section) voranstellen
|
|
if idx != 1 and 1 in sections:
|
|
global_lines = sections[1]
|
|
if '---' in global_lines:
|
|
sep = global_lines.index('---')
|
|
global_pos = "\n".join(global_lines[:sep]).strip()
|
|
global_neg = "\n".join(global_lines[sep+1:]).strip()
|
|
else:
|
|
global_pos = "\n".join(global_lines).strip()
|
|
global_neg = ""
|
|
|
|
# Global prompts mit Komma anhängen
|
|
if global_pos:
|
|
pos = global_pos + ", " + pos if pos else global_pos
|
|
if global_neg:
|
|
neg = global_neg + ", " + neg if neg else global_neg
|
|
|
|
# Wenn Auto-Load getriggert wurde, signalisiere UI-Update
|
|
if trigger_reload:
|
|
return {"ui": {"editor_content": content_to_parse}, "result": (pos, neg, project, idx)}
|
|
|
|
return pos, neg, project, idx
|
|
|
|
@classmethod
|
|
def IS_CHANGED(s, project, index, randomize, editor_content):
|
|
# Wichtig für ComfyUI um zu erkennen wenn sich Parameter ändern
|
|
return f"{project}_{index}_{randomize}_{hash(editor_content)}"
|