Every node id, input name and existing widget position is unchanged, so saved
workflows load; new inputs are appended only.
Wildcards
- core/wildcards: Dynamic Prompts / Impact Pack grammar, tag queries, variables,
conditionals, concept / verbatim / loose bindings, reproducible seeds.
Wired into every enhancer node; reads ComfyUI/models/wildcards.
- New nodes: Wildcard Processor, Semantic Variation (direction axes),
Concept Survival Check. Starter lists in wildcards/.
- Random choices use a private generator; global random state is untouched.
Platform profiles
- core/profiles + profiles/*.yaml: one file per model (format, length,
negative prompt or none, quality tokens, text and dialogue syntax, vendor
rewriter prompt fetched once and cached). profiles_user/ overrides by id.
- New node: Platform Prompt Rewriter (prose, tags, edit instructions, JSON,
MiniMax H3 fields, Hailuo brackets), with validators for each format.
- Text-to-Image and Image-to-Image read profiles through platforms.py;
corrected facts for FLUX, Kontext, Chroma, Wan, AuraFlow, Kolors, NoobAI,
Qwen-Image, HunyuanImage. Negative prompt is empty for models without one.
- Hosted / API models: Seedream 5, Nano Banana, GPT Image, Midjourney V8,
FLUX.2 pro, Ideogram 3, Recraft V4, Seedance 2.0 / 2.5, Kling 3.0, Veo 3.1,
Gemini Omni Flash, Runway, Wan 3.0, Luma, hosted H3, Grok, Vidu, PixVerse.
Reports say how to switch the vendor's own prompt rewriting off.
- Illustrious: spaced tags, quality tags last, 25-80 words (OnomaAI paper).
Video nodes
- target_model and duration_seconds appended to the three video expanders.
Default target is MiniMax H3; wan22_video is the original path, unchanged.
A workflow saved earlier loads with the default until the input is set.
Also included: the Alice, Ideogram Magic Prompt and prompt-database nodes that
were already registered in __init__.py but had not been committed.
Fixes: v1 {animal} / {label:a|b} wildcards were inert in the video nodes;
Image-to-Video save_to_file raised NameError; T2I padded short-prompt models.
.gitattributes added (text=auto) so CRLF working files stop showing as
whole-file changes. prompt_database.db and the cached Ideogram prompt are
git-ignored.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VpCi6nw5UECMdZ7ayYJvN
155 lines
5.2 KiB
Python
155 lines
5.2 KiB
Python
"""
|
||
Search Prompt Database Node
|
||
ComfyUI node for keyword / filtered search through the prompt database.
|
||
|
||
Author: Eric Hiss (GitHub: EricRollei)
|
||
"""
|
||
|
||
import logging
|
||
from .prompt_database import PromptDatabase, PLATFORMS, CATEGORIES
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class SearchPromptDatabase:
|
||
"""
|
||
Search the local prompt database by keyword, category, platform,
|
||
rating, tags, or favorites. Returns the top matching prompt as a
|
||
string output that can be fed directly into a sampler or other node.
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.db = PromptDatabase()
|
||
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"search_query": ("STRING", {
|
||
"default": "",
|
||
"tooltip": (
|
||
"Keyword search. Supports AND, OR, NOT, and "
|
||
"\"quoted phrases\". Leave blank to browse all."
|
||
),
|
||
}),
|
||
"category": (CATEGORIES, {
|
||
"default": "any",
|
||
"tooltip": "Filter by category (or 'any' for all).",
|
||
}),
|
||
"platform": (PLATFORMS, {
|
||
"default": "any",
|
||
"tooltip": "Filter by target platform (or 'any' for all).",
|
||
}),
|
||
"min_rating": ("INT", {
|
||
"default": 0,
|
||
"min": 0,
|
||
"max": 5,
|
||
"step": 1,
|
||
"tooltip": "Only return prompts rated at least this value.",
|
||
}),
|
||
"favorites_only": ("BOOLEAN", {
|
||
"default": False,
|
||
"tooltip": "Show only prompts marked as favorites.",
|
||
}),
|
||
"tags_filter": ("STRING", {
|
||
"default": "",
|
||
"tooltip": "Comma-separated tags – all must be present in the result.",
|
||
}),
|
||
"order_by": (["newest", "oldest", "rating", "random"], {
|
||
"default": "newest",
|
||
"tooltip": "Sort order for results.",
|
||
}),
|
||
"result_index": ("INT", {
|
||
"default": 1,
|
||
"min": 1,
|
||
"max": 9999,
|
||
"step": 1,
|
||
"tooltip": "Which result to output (1 = top match).",
|
||
}),
|
||
"page_size": ("INT", {
|
||
"default": 20,
|
||
"min": 1,
|
||
"max": 100,
|
||
"step": 1,
|
||
"tooltip": "How many results to fetch per page.",
|
||
}),
|
||
},
|
||
}
|
||
|
||
RETURN_TYPES = ("STRING", "STRING", "INT", "STRING", "STRING")
|
||
RETURN_NAMES = ("prompt_text", "negative_prompt", "total_matches", "result_info", "all_results_summary")
|
||
FUNCTION = "search_prompts"
|
||
CATEGORY = "Eric Prompt Enhancers/Database"
|
||
OUTPUT_NODE = True
|
||
|
||
def search_prompts(
|
||
self,
|
||
search_query: str,
|
||
category: str,
|
||
platform: str,
|
||
min_rating: int,
|
||
favorites_only: bool,
|
||
tags_filter: str,
|
||
order_by: str,
|
||
result_index: int,
|
||
page_size: int,
|
||
):
|
||
try:
|
||
# Compute offset so result_index maps correctly
|
||
offset = 0
|
||
limit = max(result_index, page_size)
|
||
|
||
results, total = self.db.search(
|
||
query=search_query,
|
||
category=category,
|
||
platform=platform,
|
||
min_rating=min_rating,
|
||
favorites_only=favorites_only,
|
||
tags_filter=tags_filter,
|
||
limit=limit,
|
||
offset=offset,
|
||
order_by=order_by,
|
||
)
|
||
|
||
if not results:
|
||
return ("", "", 0, "No matching prompts found.", "")
|
||
|
||
# Pick the requested result
|
||
idx = min(result_index - 1, len(results) - 1)
|
||
chosen = results[idx]
|
||
|
||
# Build info line
|
||
info = (
|
||
f"Result {idx + 1}/{total} | "
|
||
f"ID #{chosen['id']} | "
|
||
f"Category: {chosen['category']} | "
|
||
f"Platform: {chosen['platform']} | "
|
||
f"Rating: {'★' * chosen['rating']}{'☆' * (5 - chosen['rating'])} | "
|
||
f"Tags: {chosen['tags'] or '(none)'} | "
|
||
f"Saved: {chosen['created_at']}"
|
||
)
|
||
|
||
# Summary of all fetched results
|
||
summary_lines = []
|
||
for i, r in enumerate(results[:page_size], start=1):
|
||
short = r["prompt_text"][:80].replace("\n", " ")
|
||
if len(r["prompt_text"]) > 80:
|
||
short += "..."
|
||
summary_lines.append(
|
||
f"[{i}] #{r['id']} ({'★' * r['rating']}) "
|
||
f"[{r['category']}/{r['platform']}] {short}"
|
||
)
|
||
summary = "\n".join(summary_lines)
|
||
|
||
return (
|
||
chosen["prompt_text"],
|
||
chosen.get("negative_prompt", ""),
|
||
total,
|
||
info,
|
||
summary,
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Search error: {e}")
|
||
return ("", "", 0, f"Error: {e}", "")
|