bit of an update
added multi line and improved prompt handling, also added a ideas string output.
This commit is contained in:
@@ -1,40 +1,55 @@
|
||||
# 🧠 NeonLLama ComfyUI Extension
|
||||
|
||||
This custom ComfyUI node transforms a core **idea** into a richly detailed **positive prompt** using a local [Ollama](https://ollama.com) LLM. It also lets you specify **"avoid"** content, which is:
|
||||
|
||||
- Used by the AI to **influence** the generated prompt by avoiding certain topics.
|
||||
- Returned **unchanged** as the **negative prompt** for Stable Diffusion or similar models.
|
||||
**NeonLLama** is a custom ComfyUI node that transforms one or more **idea lines** into vivid, richly detailed prompts using a local [Ollama](https://ollama.com) LLM. It also supports **avoid** content, which the model takes into account to steer generation — and is returned as a **negative prompt**.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- 🧠 Generates a vivid, descriptive **positive prompt** from an idea.
|
||||
- ⛔ Lets you define what the AI should avoid mentioning (used during generation).
|
||||
- 🎯 Returns your **avoid list directly as a negative prompt**, unmodified.
|
||||
- 🧮 Token-aware generation using `clip-vit-base-patch32` tokenizer.
|
||||
- 🔁 Retries until prompt fits within your token limits.
|
||||
- ⚙️ Configurable parameters like token ranges, model, retry attempts, etc.
|
||||
- 🧠 Generates a structured, richly visual **positive prompt** from a simple idea.
|
||||
- ✂️ Supports **multi-line idea inputs** — each line becomes its own generation.
|
||||
- ⛔ Accepts an "avoid" list to influence prompt generation by avoiding unwanted terms.
|
||||
- 🎯 Outputs the avoid list unmodified as the **negative prompt** (for Stable Diffusion, etc.).
|
||||
- 🧮 Accurate token control using `laion/CLIP-ViT-bigG-14-laion2B-39B-b160k` tokenizer.
|
||||
- 🔁 Retries intelligently until token limits are respected.
|
||||
- ⚙️ Highly configurable: model choice, token ranges, max attempts, regen flag, and more.
|
||||
- 🖨️ Returns all prompts in order and allows re-run on every use.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 How It Works
|
||||
|
||||
1. You input an **idea** (e.g., `"cyberpunk alley in heavy rain"`).
|
||||
2. You can add **avoid terms** (e.g., `"blur, soft lighting, extra limbs"`).
|
||||
3. The LLM uses both to generate a positive prompt:
|
||||
- The **idea** is expanded into a structured visual prompt.
|
||||
- The **avoid terms** are used to **steer the generation away** from unwanted content.
|
||||
4. The avoid list is also passed through untouched as the **negative prompt**.
|
||||
1. You input one or more **ideas** (each on a new line).
|
||||
2. You can optionally add **avoid terms** — things to steer the model away from.
|
||||
3. The model uses your idea(s) to generate descriptive prompts:
|
||||
- **Positive Prompt**: Visually rich, structured, short-phrase based prompt.
|
||||
- **Negative Prompt**: Your avoid terms, returned exactly as you wrote them.
|
||||
4. Prompt generation is token-aware and dynamically revised if too long or too short.
|
||||
5. Each idea line gets its own generated prompt; all are merged and returned.
|
||||
|
||||
---
|
||||
|
||||
## 📤 Outputs
|
||||
|
||||
| Output | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `prompt` | `STRING` | The **positive prompt**, generated by the LLM. |
|
||||
| `avoid` | `STRING` | The **negative prompt**, returned as provided. |
|
||||
| Output | Type | Description |
|
||||
|--------|--------|-----------------------------------------------------------------------------|
|
||||
| `prompt` | STRING | Generated **positive prompt(s)**, joined with `BREAK` separators. |
|
||||
| `negative` | STRING | The **avoid list**, passed through directly as the negative prompt. |
|
||||
| `idea` | STRING | The original idea(s) input, for traceability or UI purposes. |
|
||||
|
||||
---
|
||||
|
||||
## 📥 Inputs / Configuration Fields
|
||||
|
||||
| Name | Type | Description |
|
||||
|--------------------|----------|-----------------------------------------------------------------------------|
|
||||
| `model` | Dropdown | Select which locally running Ollama model to use. |
|
||||
| `idea` | Text | The concept or image prompt idea. Up to 3 lines, one idea per line. |
|
||||
| `negative` | Text | Words, elements, or themes to avoid. Used during gen + passed as negative. |
|
||||
| `max_tokens` | Int | Max token limit (default: 75, max: 231). |
|
||||
| `min_tokens` | Int | Minimum token requirement (default: 50). |
|
||||
| `max_attempts` | Int | Number of tries to get an acceptable prompt per idea. |
|
||||
| `regen_on_each_use`| Bool | Forces re-generation on each run, even if inputs didn’t change. |
|
||||
|
||||
---
|
||||
|
||||
@@ -42,41 +57,61 @@ This custom ComfyUI node transforms a core **idea** into a richly detailed **pos
|
||||
|
||||
**Inputs:**
|
||||
|
||||
- `idea`: `haunted subway station with broken lights`
|
||||
- `avoid`: `blood, gore, screaming`
|
||||
```text
|
||||
idea:
|
||||
haunted subway station with broken lights
|
||||
|
||||
avoid:
|
||||
blood, gore, screaming
|
||||
```
|
||||
|
||||
**Outputs:**
|
||||
|
||||
- `prompt`: *(LLM-generated)*
|
||||
`"dark abandoned subway, flickering fluorescent lights, cracked tiled walls, shadowy corners, old train cars, graffiti-covered pillars, dim green glow, debris scattered floor"`
|
||||
```text
|
||||
prompt:
|
||||
dark abandoned subway, flickering fluorescent lights, cracked tiled walls, shadowy corners, old train cars, graffiti-covered pillars, dim green glow, debris scattered floor
|
||||
|
||||
- `avoid`: *(Unchanged)*
|
||||
`"blood, gore, screaming"`
|
||||
|
||||
Use the `prompt` as your positive CLIP text, and `avoid` for negative conditioning.
|
||||
negative:
|
||||
blood, gore, screaming
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration Fields
|
||||
## 🔁 Token-Aware Generation
|
||||
|
||||
| Name | Type | Description |
|
||||
|------|------|-------------|
|
||||
| `model` | Dropdown | Select the Ollama model to use. |
|
||||
| `idea` | Multiline Text | The concept or image idea. |
|
||||
| `avoid` | Multiline Text | Words/themes to avoid (used by LLM + passed to negative prompt). |
|
||||
| `max_tokens` | Int | Maximum allowed tokens for the generated prompt. |
|
||||
| `min_tokens` | Int | Minimum token target. |
|
||||
| `max_attempts` | Int | Max retries to hit token range. |
|
||||
| `regen_on_each_use` | Bool | Force prompt regeneration every time node runs. |
|
||||
- Uses CLIP tokenizer (`laion/CLIP-ViT-bigG-14-laion2B-39B-b160k`) to measure tokens.
|
||||
- Tries up to `max_attempts` times to reach your `min_tokens` and not exceed `max_tokens`.
|
||||
- If prompt is too short, it adds detail; too long, it trims or rephrases.
|
||||
- Avoid terms are included in a non-generative system message and tracked between retries.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Regeneration Behavior
|
||||
## 🧠 Smart Prompt Structuring
|
||||
|
||||
If enabled, `regen_on_each_use` will force the node to re-generate the prompt every time it's executed, ensuring fresh output even if the inputs don’t change.
|
||||
The generation system instructs the LLM to:
|
||||
|
||||
- Use **short phrases**, not sentences.
|
||||
- **Avoid storytelling**, abstract ideas, and emotional language.
|
||||
- Preserve the **core visual themes** of your original idea exactly.
|
||||
- Avoid explanation or meta-commentary.
|
||||
- Support basic structure with connectors like `with`, `under`, `surrounded by`.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Requirements
|
||||
|
||||
- [Ollama](https://ollama.com) installed and running locally (on `localhost:11434`).
|
||||
- ComfyUI environment.
|
||||
- Python dependencies: `requests`, `tokenizers`.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
## ❤️ Credits
|
||||
|
||||
Built by Neon Lightning ⚡
|
||||
+125
-102
@@ -21,7 +21,7 @@ def fetch_ollama_models():
|
||||
|
||||
OLLAMA_MODELS = fetch_ollama_models()
|
||||
|
||||
tokenizer = Tokenizer.from_pretrained("openai/clip-vit-base-patch32")
|
||||
tokenizer = Tokenizer.from_pretrained("laion/CLIP-ViT-bigG-14-laion2B-39B-b160k")
|
||||
|
||||
def estimate_tokens(text):
|
||||
return tokenizer.encode(text).ids
|
||||
@@ -32,16 +32,16 @@ class OllamaPromptFromIdea:
|
||||
return {
|
||||
"required": {
|
||||
"model": (OLLAMA_MODELS, {"tooltip": "Select the Ollama model to generate prompts with."}),
|
||||
"idea": ("STRING", {"multiline": True, "default": "futuristic cyberpunk city", "tooltip": "Enter the core concept or theme for your prompt to ollama."}),
|
||||
"avoid": ("STRING", {"multiline": True, "default": "", "tooltip": "Words or themes to exclude from the prompt.(non ollama prompting)"}),
|
||||
"max_tokens": ("INT", {"default": 77, "min": 10, "max": 231, "tooltip": "Maximum token length for the generated prompt."}),
|
||||
"min_tokens": ("INT", {"default": 60, "min": 10, "max": 230, "tooltip": "Minimum token length for the generated prompt."}),
|
||||
"idea": ("STRING", {"multiline": True, "default": "futuristic cyberpunk city", "tooltip": "Enter the core concept or theme for your prompt to ollama\nYou can have seperated ideas if you have a hard return.\nOnly use up to 3 lines though. to a maximum. of 231 tokens."}),
|
||||
"negative": ("STRING", {"multiline": True, "default": "", "tooltip": "Words or themes to exclude from the prompt.(non ollama prompting)"}),
|
||||
"max_tokens": ("INT", {"default": 75, "min": 10, "max": 231, "tooltip": "Maximum token length for the generated prompt."}),
|
||||
"min_tokens": ("INT", {"default": 50, "min": 10, "max": 230, "tooltip": "Minimum token length for the generated prompt."}),
|
||||
"max_attempts": ("INT", {"default": 30, "min": 1, "max": 200, "tooltip": "Number of attempts to generate a prompt fitting token limits."}),
|
||||
"regen_on_each_use": ("BOOLEAN", {"default": True, "tooltip": "Force regeneration on each node execution."}),
|
||||
}
|
||||
}
|
||||
RETURN_TYPES = ("STRING", "STRING",)
|
||||
RETURN_NAMES = ("prompt", "avoid",)
|
||||
RETURN_TYPES = ("STRING", "STRING", "STRING",)
|
||||
RETURN_NAMES = ("prompt", "negative", "idea")
|
||||
FUNCTION = "generate_prompt"
|
||||
CATEGORY = "Ollama"
|
||||
|
||||
@@ -51,112 +51,135 @@ class OllamaPromptFromIdea:
|
||||
return float("NaN")
|
||||
return None
|
||||
|
||||
def generate_prompt(self, model, idea, avoid, max_tokens, min_tokens, max_attempts, regen_on_each_use):
|
||||
if not avoid:
|
||||
avoid = ""
|
||||
def generate_prompt(self, model, idea, negative, max_tokens, min_tokens, max_attempts, regen_on_each_use):
|
||||
if not negative:
|
||||
negative = ""
|
||||
token_min = min(min_tokens, max_tokens)
|
||||
token_expand_threshold = int(token_min * 0.75)
|
||||
last_output = None
|
||||
avoid_clause = f"\nABSOLUTELY avoid mentioning: {avoid.strip()}" if avoid.strip() else ""
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
if last_output is None:
|
||||
system_prompt = (
|
||||
f"Convert the following idea into a richly descriptive, visually detailed image prompt for Stable Diffusion XL. "
|
||||
f"Use short but expressive phrases, and allow natural connectors like 'with', 'and', or 'under'. "
|
||||
f"Focus on concrete, vivid visual elements – not abstract concepts. "
|
||||
f"Use multi-word descriptions where appropriate. "
|
||||
f"Do not include full sentences, storytelling, or subjective opinions. "
|
||||
f"Use only short descriptions. and don't describe feeling"
|
||||
f"Avoid overly generic or disconnected terms. "
|
||||
f"Target between {token_min} and {max_tokens} tokens. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nIdea: {idea}\nPrompt:"
|
||||
)
|
||||
else:
|
||||
token_count = len(estimate_tokens(last_output))
|
||||
if token_count > max_tokens:
|
||||
system_prompt = (
|
||||
f"The following prompt is too long (over {max_tokens} tokens). "
|
||||
f"Revise it to be shorter but keep visual richness and specificity. "
|
||||
f"Use compact phrases or brief expressions with light structure. "
|
||||
f"Avoid long sentences or reinterpreting the concept. "
|
||||
f"Use only short descriptions. and don't describe feeling"
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nShorter prompt:"
|
||||
|
||||
idea_list = [i.strip() for i in idea.strip().split("\n") if i.strip()]
|
||||
generated_prompts = []
|
||||
|
||||
for idx, sub_idea in enumerate(idea_list):
|
||||
print(f"\n🧠 Generating prompt for idea {idx + 1}: '{sub_idea}'")
|
||||
last_output = None
|
||||
used_phrases = []
|
||||
if negative.strip():
|
||||
used_phrases.append(negative.strip())
|
||||
|
||||
for attempt in range(1, max_attempts + 1):
|
||||
try:
|
||||
avoid_text = " | ".join(used_phrases)
|
||||
avoid_clause = ""
|
||||
if avoid_text.strip() and (negative.strip() or idx > 0):
|
||||
avoid_clause = (
|
||||
f"\nABSOLUTELY avoid using or repeating any of the following phrases or content but keep them in mind: {avoid_text}"
|
||||
)
|
||||
elif token_count < token_expand_threshold:
|
||||
if last_output is None:
|
||||
system_prompt = (
|
||||
f"The following prompt is too short (under {token_expand_threshold} tokens). "
|
||||
f"Expand it by adding specific, vivid imagery using short but rich phrases. "
|
||||
f"Include unique textures, lighting effects, environments, and visual motifs. "
|
||||
f"Light structure is allowed: use connectors like 'with', 'under', 'surrounded by', etc. "
|
||||
f"Do not repeat phrases or rearrange words – add new coherent, visual material. "
|
||||
f"Use only short descriptions. and don't describe feeling"
|
||||
f"Avoid full sentences or storylines. "
|
||||
f"Convert the following idea into a richly descriptive, visually detailed image prompt for Stable Diffusion XL. "
|
||||
f"Use short phrases, and allow natural connectors like 'with', 'and', or 'under'. "
|
||||
f"Focus on concrete, vivid visual elements – not abstract concepts. "
|
||||
f"Use multi-word descriptions only where needed. "
|
||||
f"Do not include full sentences, storytelling, or subjective opinions. "
|
||||
f"Use only short descriptions. and don't describe feeling. "
|
||||
f"Target between {token_min} and {max_tokens} tokens. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {idea} DO NOT CHANGE THE IDEA."
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {sub_idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nExpanded prompt:"
|
||||
f"\nIdea: {sub_idea}\nPrompt:"
|
||||
)
|
||||
else:
|
||||
system_prompt = (
|
||||
f"Revise the following prompt to improve clarity and vividness, while keeping all original ideas intact. "
|
||||
f"You may slightly structure the phrases for better flow. "
|
||||
f"Do not add new concepts or remove core elements. "
|
||||
f"Use only short descriptions. and don't describe feeling"
|
||||
f"Keep it between {token_min}–{max_tokens} tokens. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nRevised prompt:"
|
||||
)
|
||||
seed = random.randint(0, 99999999)
|
||||
response = requests.post(
|
||||
"http://localhost:11434/api/generate",
|
||||
json={
|
||||
"model": model,
|
||||
"prompt": system_prompt,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"seed": seed,
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
raw_result = response.json().get("response", "").strip()
|
||||
token_count = len(estimate_tokens(raw_result))
|
||||
print(f"Attempt {attempt} Ollama result: {raw_result}")
|
||||
print(f"→ Token count: {token_count} (target: {token_min}–{max_tokens})")
|
||||
if last_output is not None and raw_result.strip() == last_output.strip():
|
||||
print("⚠️ Prompt identical to last attempt. Restarting generation from scratch...\n")
|
||||
last_output = None
|
||||
token_count = len(estimate_tokens(last_output))
|
||||
if token_count > max_tokens:
|
||||
system_prompt = (
|
||||
f"The following prompt is too long (over {max_tokens} tokens). "
|
||||
f"Revise it to be shorter but keep visual richness and specificity. "
|
||||
f"Use compact phrases or brief expressions with light structure. "
|
||||
f"Avoid long sentences or reinterpreting the concept. "
|
||||
f"Use only short descriptions. and don't describe feeling. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {sub_idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nShorter prompt:"
|
||||
)
|
||||
elif token_count < token_expand_threshold:
|
||||
system_prompt = (
|
||||
f"The following prompt is too short (under {token_expand_threshold} tokens). "
|
||||
f"Expand it by adding specific, vivid imagery using short but rich phrases. "
|
||||
f"Include unique textures, lighting effects, environments, and visual motifs. "
|
||||
f"Light structure is allowed: use connectors like 'with', 'under', 'surrounded by', etc. "
|
||||
f"Do not repeat phrases or rearrange words – add new coherent, visual material. "
|
||||
f"Use only short descriptions. and don't describe feeling. "
|
||||
f"Avoid full sentences or storylines. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {sub_idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nExpanded prompt:"
|
||||
)
|
||||
else:
|
||||
system_prompt = (
|
||||
f"Revise the following prompt to improve clarity and vividness, while keeping all original ideas intact. "
|
||||
f"You may slightly structure the phrases for better flow. "
|
||||
f"Do not add new concepts or remove core elements. "
|
||||
f"Use only short descriptions. and don't describe feeling. "
|
||||
f"Keep it between {token_min}–{max_tokens} tokens. "
|
||||
f"{avoid_clause}"
|
||||
f"Reminder: You MUST preserve all core themes of the original idea. The original idea is: {sub_idea} DO NOT CHANGE THE IDEA."
|
||||
f"you MUST NOT ever talk about your thought process or explain how you generated the prompt."
|
||||
f"\nPrevious prompt: {last_output}\nRevised prompt:"
|
||||
)
|
||||
|
||||
seed = random.randint(0, 99999999)
|
||||
response = requests.post(
|
||||
"http://localhost:11434/api/generate",
|
||||
json={
|
||||
"model": model,
|
||||
"prompt": system_prompt,
|
||||
"stream": False,
|
||||
"options": {
|
||||
"seed": seed,
|
||||
"temperature": 0.7
|
||||
}
|
||||
},
|
||||
timeout=120,
|
||||
)
|
||||
response.raise_for_status()
|
||||
raw_result = response.json().get("response", "").strip()
|
||||
token_count = len(estimate_tokens(raw_result))
|
||||
print(f"Idea: {idx + 1} Attempt: {attempt}/{max_attempts} Ollama result: {raw_result}")
|
||||
print(f"→ Token count: {token_count} (target: {token_min}–{max_tokens})")
|
||||
|
||||
if last_output is not None and raw_result.strip() == last_output.strip():
|
||||
print("⚠️ Prompt identical to last attempt. Restarting generation from scratch...\n")
|
||||
last_output = None
|
||||
seed = random.randint(0, 99999999)
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
|
||||
if token_min <= token_count <= max_tokens:
|
||||
used_phrases.append(raw_result)
|
||||
print("✔️ Prompt accepted.")
|
||||
generated_prompts.append(raw_result)
|
||||
break
|
||||
last_output = raw_result
|
||||
reason = "too long" if token_count > max_tokens else "too short"
|
||||
print(f"⚠️ Prompt {reason}. Retrying...\n")
|
||||
time.sleep(0.5)
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"⚠️ Attempt {attempt}/{max_attempts} timed out. Retrying...\n")
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
if token_min <= token_count <= max_tokens:
|
||||
print("✔️ Prompt accepted.")
|
||||
return (raw_result, avoid)
|
||||
last_output = raw_result
|
||||
reason = "too long" if token_count > max_tokens else "too short"
|
||||
print(f"⚠️ Prompt {reason}. Retrying...\n")
|
||||
time.sleep(0.5)
|
||||
except requests.exceptions.Timeout:
|
||||
print(f"⚠️ Attempt {attempt} timed out. Retrying...\n")
|
||||
time.sleep(0.5)
|
||||
continue
|
||||
except Exception as e:
|
||||
error_msg = f"[Ollama Error] {str(e)}"
|
||||
print(error_msg)
|
||||
return (error_msg, "")
|
||||
print("❌ Max attempts reached. Returning fallback idea.")
|
||||
return (idea, avoid)
|
||||
except Exception as e:
|
||||
error_msg = f"[Ollama Error] {str(e)}"
|
||||
print(error_msg)
|
||||
return (error_msg, "")
|
||||
else:
|
||||
print(f"❌ Max attempts for idea '{sub_idea}' reached. Using original as fallback.")
|
||||
generated_prompts.append(sub_idea)
|
||||
outputend="\nBREAK\n".join(generated_prompts)
|
||||
print(f"output of:{outputend}")
|
||||
return (" BREAK ".join(generated_prompts), negative, idea)
|
||||
|
||||
def ui(self, inputs, outputs):
|
||||
prompt_str = outputs[0] if isinstance(outputs, (list, tuple)) and outputs else ""
|
||||
|
||||
Reference in New Issue
Block a user