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
222 lines
12 KiB
Python
222 lines
12 KiB
Python
"""
|
|
Platform Prompt Rewriter.
|
|
|
|
One idea in, one prompt out, written the way the chosen model wants to read it. Everything the
|
|
node knows about a model comes from a YAML profile in profiles/ (see core/profiles/schema.py):
|
|
format (prose, tags, edit instruction, JSON, MiniMax H3 fields, Hailuo bracket commands), target
|
|
length and hard limits, whether a negative prompt exists, whether quality tokens help or hurt,
|
|
how rendered text and dialogue are written, and - where the vendor publishes one - the vendor's
|
|
own rewriter prompt, downloaded once and cached.
|
|
|
|
The node is thin on purpose: resolve wildcards, build the instructions from the profile, call
|
|
the LLM through the package's LLMBackend, check the answer, report. If the LLM cannot be reached
|
|
the idea passes through unchanged and the report says why.
|
|
|
|
Author: Eric Hiss (GitHub: EricRollei)
|
|
License: Dual License (Non-Commercial and Commercial Use). See LICENSE.
|
|
"""
|
|
|
|
from .core.profiles import RunFacts
|
|
from .profile_support import (
|
|
OFFICIAL_PROMPT_CHOICES,
|
|
TASK_CHOICES,
|
|
caption_image_tensor,
|
|
get_profile,
|
|
profile_ids,
|
|
rewrite_for_platform,
|
|
)
|
|
from .wildcard_support import (
|
|
enforce_verbatim,
|
|
is_changed_token,
|
|
llm_block,
|
|
normalise_seed,
|
|
report_lines,
|
|
resolve_wildcards,
|
|
wildcard_optional_inputs,
|
|
)
|
|
|
|
DEFAULT_PLATFORM = "krea2"
|
|
|
|
|
|
def _platform_choices():
|
|
choices = profile_ids()
|
|
return choices or [DEFAULT_PLATFORM]
|
|
|
|
|
|
class PlatformPromptRewriter:
|
|
CATEGORY = "Eric Prompt Enhancers"
|
|
FUNCTION = "rewrite"
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("prompt", "negative_prompt", "report", "system_prompt")
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
choices = _platform_choices()
|
|
default_platform = DEFAULT_PLATFORM if DEFAULT_PLATFORM in choices else choices[0]
|
|
return {
|
|
"required": {
|
|
"idea": ("STRING", {
|
|
"multiline": True, "default": "",
|
|
"tooltip": "What you want, in your own words. Wildcards work: __name__, {a|b|c}, __objects[metal AND handheld]__.",
|
|
}),
|
|
"platform": (choices, {
|
|
"default": default_platform,
|
|
"tooltip": (
|
|
"Target model. Listed as image models, then edit models, then video models.\n"
|
|
"Each entry is a YAML file in the package's profiles/ folder; put your own or edited\n"
|
|
"copies in profiles_user/ (same id = replaces the shipped one)."
|
|
),
|
|
}),
|
|
"task": (TASK_CHOICES, {
|
|
"default": "auto",
|
|
"tooltip": (
|
|
"auto = the model's main task.\n"
|
|
"t2i text-to-image, edit = edit instruction, t2v text-to-video,\n"
|
|
"i2v image-to-video, flf2v first+last frame, ref2v reference-to-video.\n"
|
|
"A task the model does not have falls back to its main one (the report says so)."
|
|
),
|
|
}),
|
|
"backend": (["lm_studio", "ollama", "qwen3_vl"], {"default": "lm_studio"}),
|
|
"endpoint": ("STRING", {
|
|
"default": "http://localhost:1234/v1",
|
|
"tooltip": "LM Studio / Ollama base URL. For qwen3_vl, optionally a local model path.",
|
|
}),
|
|
"model": ("STRING", {"default": "", "tooltip": "Blank = whatever model is loaded."}),
|
|
"temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.05}),
|
|
"max_tokens": ("INT", {
|
|
"default": 4096, "min": 256, "max": 32768,
|
|
"tooltip": "Room for the LLM's answer. Reasoning models and MiniMax H3 prompts need the most.",
|
|
}),
|
|
"official_prompt": (OFFICIAL_PROMPT_CHOICES, {
|
|
"default": "auto",
|
|
"tooltip": (
|
|
"Several labs publish the system prompt of their own prompt rewriter.\n"
|
|
"auto = use it when the profile has one and it is not huge; downloaded once, then cached\n"
|
|
"on = use it whatever its size (Qwen-Image's is ~35,000 characters)\n"
|
|
"off = always use the rules written in the profile (works offline)\n"
|
|
"refresh = download it again, then behave like on"
|
|
),
|
|
}),
|
|
},
|
|
"optional": {
|
|
"image_description": ("STRING", {
|
|
"forceInput": True,
|
|
"tooltip": "For i2v / edit: what the start or source image shows, from your own captioner. Used instead of captioning `image`.",
|
|
}),
|
|
"end_image_description": ("STRING", {
|
|
"forceInput": True,
|
|
"tooltip": "For flf2v: what the last frame shows. Used instead of captioning `end_image`.",
|
|
}),
|
|
"dialogue": ("STRING", {
|
|
"multiline": True, "default": "",
|
|
"tooltip": "Exact spoken lines for models with sound, one per line, e.g. Woman: I get off at the next station.\nUsed word for word. Empty = the rewriter invents speech only if your idea involves someone speaking.",
|
|
}),
|
|
"duration_seconds": ("FLOAT", {
|
|
"default": 0.0, "min": 0.0, "max": 60.0, "step": 0.5,
|
|
"tooltip": "Clip length for video models. 0 = not stated. The rewriter fits the action to it.",
|
|
}),
|
|
"aspect_ratio": ("STRING", {"default": "", "tooltip": "Optional, e.g. 16:9. The rewriter composes for it."}),
|
|
"audio": (["auto", "off"], {
|
|
"default": "auto",
|
|
"tooltip": "auto = write sound, music and dialogue when the model generates audio. off = no sound design.",
|
|
}),
|
|
"language": (["auto", "en", "zh"], {
|
|
"default": "auto",
|
|
"tooltip": "Language of the prompt. auto = the model's first language (English for every shipped profile).",
|
|
}),
|
|
"extra_instructions": ("STRING", {
|
|
"multiline": True, "default": "",
|
|
"tooltip": "Direction for the rewriter for this run, e.g. 'keep it under 60 words', 'night exterior, handheld'.",
|
|
}),
|
|
"negative_keywords": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "Comma-separated additions to the negative prompt. Passed through even for models that ignore negatives.",
|
|
}),
|
|
**wildcard_optional_inputs(),
|
|
# Appended later. Sockets only (no widgets), but still added at the end: the rule is append-only.
|
|
"image": ("IMAGE", {
|
|
"tooltip": (
|
|
"For i2v / edit / flf2v: the start or source image. It is captioned through the backend above,\n"
|
|
"which therefore needs a vision model loaded. A connected image_description wins over this."
|
|
),
|
|
}),
|
|
"end_image": ("IMAGE", {"tooltip": "For flf2v: the last frame. Captioned like `image`."}),
|
|
},
|
|
}
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, wildcard_seed=-1, **kwargs):
|
|
"""wildcard_seed = -1 means a fresh roll each run, which must defeat ComfyUI's cache."""
|
|
return is_changed_token(wildcard_seed)
|
|
|
|
def rewrite(self, idea, platform, task, backend, endpoint, model, temperature, max_tokens, official_prompt,
|
|
image=None, end_image=None, image_description="", end_image_description="", dialogue="",
|
|
duration_seconds=0.0, aspect_ratio="", audio="auto", language="auto", extra_instructions="",
|
|
negative_keywords="", wildcard_seed=-1, wildcard_llm_block=True, wildcard_instructions=""):
|
|
|
|
resolution = resolve_wildcards(idea or "", seed=normalise_seed(wildcard_seed), legacy=True,
|
|
upstream_instructions=wildcard_instructions)
|
|
resolved_idea = resolution.text
|
|
|
|
if not resolved_idea.strip():
|
|
return ("", "", "Empty idea - nothing to rewrite.", "")
|
|
|
|
# Images: a description wired in by the user wins; otherwise caption through the same backend.
|
|
profile_for_task = get_profile(platform)
|
|
caption_notes = []
|
|
has_start = image is not None or bool((image_description or "").strip())
|
|
has_end = end_image is not None or bool((end_image_description or "").strip())
|
|
if task == "auto" and profile_for_task is not None and has_start:
|
|
# An image on the input says what the job is: first+last frame, image-to-video, or an edit.
|
|
for candidate, wanted in (("flf2v", has_end), ("i2v", True), ("edit", True)):
|
|
if wanted and candidate in profile_for_task.tasks:
|
|
task = candidate
|
|
caption_notes.append(f"task auto -> {candidate}, because an image is connected")
|
|
break
|
|
resolved_task = profile_for_task.resolve_task(None if task == "auto" else task) if profile_for_task else task
|
|
if image is not None and not (image_description or "").strip():
|
|
purpose = "edit" if resolved_task == "edit" else "start"
|
|
image_description, note = caption_image_tensor(image, purpose, backend, endpoint, model)
|
|
caption_notes.append(note)
|
|
if end_image is not None and not (end_image_description or "").strip():
|
|
end_image_description, note = caption_image_tensor(end_image, "end", backend, endpoint, model)
|
|
caption_notes.append(note)
|
|
|
|
facts = RunFacts(
|
|
duration_seconds=float(duration_seconds or 0.0),
|
|
aspect_ratio=(aspect_ratio or "").strip(),
|
|
image_description=image_description or "",
|
|
end_image_description=end_image_description or "",
|
|
dialogue=dialogue or "",
|
|
audio=audio,
|
|
language="" if language == "auto" else language,
|
|
extra_instructions=extra_instructions or "",
|
|
)
|
|
custom_negatives = [part.strip() for part in (negative_keywords or "").split(",") if part.strip()]
|
|
|
|
result = rewrite_for_platform(
|
|
resolved_idea, platform, task=task, backend=backend, endpoint=endpoint, model=model,
|
|
temperature=temperature, max_tokens=max_tokens, facts=facts, official_mode=official_prompt,
|
|
extra_system=llm_block(resolution, wildcard_llm_block), custom_negatives=custom_negatives,
|
|
)
|
|
|
|
result.notes.extend(caption_notes)
|
|
profile = profile_for_task
|
|
prompt = result.prompt
|
|
if result.ok:
|
|
structured = profile is not None and profile.caption_style in ("json", "fields")
|
|
repaired, missing = enforce_verbatim(prompt, resolution)
|
|
if missing and structured:
|
|
# Appending text to JSON or to named fields would break the format, so only report it.
|
|
result.warnings.append("verbatim wildcard picks missing from the answer (not added, to keep the format valid): " + ", ".join(missing))
|
|
elif missing:
|
|
prompt = repaired
|
|
result.notes.append("verbatim wildcard picks the LLM dropped were added back: " + ", ".join(missing))
|
|
|
|
report = result.report(profile)
|
|
wildcard_report = report_lines(resolution)
|
|
if wildcard_report:
|
|
report = f"{report}\n{wildcard_report}"
|
|
print(f"[Platform Prompt Rewriter] {platform}: {'ok' if result.ok else 'FAILED'} ({result.base or 'no plan'})")
|
|
return (prompt, result.negative, report, result.system_prompt)
|