Files
EricRollei-Local_LLM_Prompt…/core/profiles/official.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

226 lines
9.0 KiB
Python

"""
Official rewriter prompts: fetch once, cache, tailor in the open.
Several labs publish the system prompt their own prompt rewriter uses (Wan, LTX, Qwen-Image,
FLUX.2, Krea 2, Hunyuan, Ideogram, MiniMax H3). Those are the best available description of what
each model was trained to read, so a profile can point at one and the rewriter will use it as
the base of its system prompt - the same design as the Ideogram Magic Prompt node.
Nothing is shipped or redistributed: each file is downloaded from the vendor's repository the
first time it is needed and kept in <package>/cache/official_prompts/<profile id>/. To work
offline, put the file there yourself under the name shown in the node's notes output.
Three file shapes are understood:
text the whole file is the prompt
python_var a .py file; the prompt is a string assigned to `variable` (read with ast, never executed)
sections an Ideogram-style file with [SYSTEM] / [USER] headers; `variable` names the section
Tailoring is explicit and logged: `drop_lines` regexes remove matching lines (used to take the
vendor's content-moderation rules out of Wan's rewriter, which would otherwise silently replace
the user's prompt), and every removal is reported in the notes.
(c) Eric Hiss. Dual license, see LICENSE.
"""
from __future__ import annotations
import ast
import hashlib
import os
import re
import time
import urllib.request
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from .schema import OfficialPrompt, Profile
FETCH_TIMEOUT_SECONDS = 15
MIN_PROMPT_CHARS = 200
USER_AGENT = "comfyui-video-prompter-profiles"
AUTO_MAX_CHARS = 30000 # in auto mode, longer vendor prompts are skipped: they overflow small LLM contexts
MODE_AUTO = "auto" # use the vendor prompt when the profile has one and it is not huge; download once
MODE_ON = "on" # use it whatever its size
MODE_OFF = "off" # never; always build the instructions from the profile's own fields
MODE_REFRESH = "refresh" # like on, but download again first, replacing the cached copy
MODES = (MODE_AUTO, MODE_ON, MODE_OFF, MODE_REFRESH)
RETRY_AFTER_SECONDS = 600 # after a failed download, do not try that URL again for ten minutes
_FAILED_AT: Dict[str, float] = {}
@dataclass
class OfficialResult:
text: str = ""
used: bool = False
notes: List[str] = field(default_factory=list)
cache_file: str = ""
def cache_name(entry: OfficialPrompt) -> str:
"""Readable, collision-free file name for a URL."""
base = os.path.basename(entry.url.split("?")[0]) or "prompt.txt"
digest = hashlib.sha1(entry.url.encode("utf-8")).hexdigest()[:8]
stem, extension = os.path.splitext(base)
return f"{stem}.{digest}{extension or '.txt'}"
def _download(url: str) -> str:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request, timeout=FETCH_TIMEOUT_SECONDS) as response:
return response.read().decode("utf-8")
def _string_assigned(body: list, variable: str) -> Optional[str]:
for node in body:
if isinstance(node, ast.Assign):
targets = [target.id for target in node.targets if isinstance(target, ast.Name)]
value = node.value
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
targets = [node.target.id]
value = node.value
else:
continue
if variable in targets:
try:
literal = ast.literal_eval(value)
except (ValueError, SyntaxError):
return None
return literal if isinstance(literal, str) else None
return None
def extract_python_string(source: str, variable: str) -> Optional[str]:
"""
The string literal assigned to `variable`. The file is parsed with ast, never run.
"NAME" looks at module level; "function.NAME" looks inside that function, which is where
Qwen-Image keeps its prompts (polish_prompt_en.SYSTEM_PROMPT).
"""
try:
tree = ast.parse(source)
except SyntaxError:
return None
function_name, _, name = variable.rpartition(".")
if not function_name:
return _string_assigned(tree.body, name)
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == function_name:
return _string_assigned([child for child in ast.walk(node)], name)
return None
def extract_section(source: str, section: str) -> Optional[str]:
"""[SYSTEM] ... [USER] ... files, as used by Ideogram's magic prompt."""
wanted = (section or "system").lower()
sections: Dict[str, List[str]] = {}
current: Optional[str] = None
for line in source.splitlines():
stripped = line.strip()
if stripped.startswith("[") and stripped.endswith("]") and " " not in stripped:
current = stripped[1:-1].lower()
sections[current] = []
elif current is not None:
sections[current].append(line)
if wanted not in sections:
return None
return "\n".join(sections[wanted]).strip()
def _extract(entry: OfficialPrompt, source: str) -> Optional[str]:
if entry.format == "python_var":
return extract_python_string(source, entry.variable)
if entry.format == "sections":
return extract_section(source, entry.variable)
return source.strip()
def _drop_lines(text: str, patterns: List[str], notes: List[str]) -> str:
if not patterns:
return text
compiled = []
for pattern in patterns:
try:
compiled.append(re.compile(pattern))
except re.error as error:
notes.append(f"tailoring pattern {pattern!r} is not a valid regex ({error}); ignored")
kept: List[str] = []
removed = 0
for line in text.splitlines():
if any(regex.search(line) for regex in compiled):
removed += 1
continue
kept.append(line)
if removed:
notes.append(f"tailoring: removed {removed} line(s) from the official prompt (see the profile's drop_lines)")
return "\n".join(kept)
def load_official(profile: Profile, task: str, cache_dir: str, mode: str = MODE_AUTO) -> OfficialResult:
"""
The vendor's rewriter prompt for this profile and task, or an empty result.
Never raises: a missing network, a moved file or a changed variable name all come back as a
note, and the caller falls back to the prompt built from the profile's own fields.
"""
result = OfficialResult()
if mode == MODE_OFF:
return result
entry = profile.official_for(task)
if entry is None:
return result
folder = os.path.join(cache_dir, "official_prompts", profile.id)
path = os.path.join(folder, cache_name(entry))
result.cache_file = path
source: Optional[str] = None
if mode != MODE_REFRESH and os.path.isfile(path):
try:
with open(path, "r", encoding="utf-8") as handle:
source = handle.read()
except OSError as error:
result.notes.append(f"cached official prompt unreadable ({error})")
if source is None:
failed_at = _FAILED_AT.get(entry.url)
if failed_at is not None and mode != MODE_REFRESH and time.time() - failed_at < RETRY_AFTER_SECONDS:
result.notes.append(f"official prompt could not be downloaded a moment ago; using the built-in rules. Offline: save {entry.url} as {path}")
return result
try:
source = _download(entry.url)
os.makedirs(folder, exist_ok=True)
with open(path, "w", encoding="utf-8") as handle:
handle.write(source)
result.notes.append(f"official prompt downloaded and cached: {path}")
except Exception as error:
_FAILED_AT[entry.url] = time.time()
result.notes.append(
f"official prompt not available ({error}); using the built-in rules. "
f"To use it offline save {entry.url} as {path}"
)
return result
text = _extract(entry, source)
if not text or len(text) < MIN_PROMPT_CHARS:
where = f"variable {entry.variable}" if entry.format == "python_var" else f"format {entry.format}"
result.notes.append(f"official prompt file changed shape ({where} not found in {path}); using the built-in rules")
return result
if mode == MODE_AUTO and len(text) > AUTO_MAX_CHARS:
result.notes.append(
f"the official prompt is {len(text):,} characters, too long for a small LLM context, so the built-in "
f"rules were used. Set official_prompt to 'on' to use it anyway (needs a context of 12k tokens or more)."
)
return result
result.text = _drop_lines(text, entry.drop_lines, result.notes)
if entry.preamble:
result.text = f"{entry.preamble}\n\n{result.text}"
result.used = True
result.notes.append(f"base: official {profile.developer or profile.name} rewriter prompt ({entry.url})")
if entry.note:
result.notes.append(entry.note)
return result