Files
EricRollei-Local_LLM_Prompt…/platform_rewriter_node.py
T
Eric HissandClaude Fable 5.1 4459af9ea6 V2.0.0: wildcard engine, platform profiles, MiniMax H3 default, API models
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
2026-09-21 00:28:58 -07:00

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)