Files
EricRollei-Local_LLM_Prompt…/core/variability/survival.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

108 lines
4.3 KiB
Python

"""
Concept survival: did the meaning of each randomly chosen concept make it through the LLM rewrite?
A concept-bound wildcard pick may be reworded freely ("rusty" -> "oxidised steel flaking orange at the
seams"), so a string search cannot check it. This does it in embedding space: the rewritten prompt
is cut into overlapping windows of words, every window and every concept is embedded, and a
concept's score is its best cosine against any window. The best window is reported too - that is
the phrase carrying the concept, or, when the score is low, the nearest the prompt comes to it.
Scores depend on the embedder, so the threshold is a setting, not a constant. Measured with
Qwen3-VL-Embedding-2B (dev/try_directions_and_survival.py), on things, materials and places: a faithful
paraphrase scores 0.69-0.74, the same scene with the pick dropped or normalised 0.43-0.60, an unrelated
prompt 0.26-0.45. Anything the prompt states word for word scores 1.0 without being embedded.
Known limit, also measured: relations and conditions ("lit only from below", "the weather is indoors")
do NOT separate - an embedding hears the topic, not the relation, so window light scores as high as
light from below. The Concept Survival Check node offers an LLM judge for those.
(c) Eric Hiss. Dual license, see LICENSE.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from typing import List
import numpy as np
from .embedders import Embedder
WINDOW_SIZES = (4, 6, 10, 16, 24) # words per window
MAX_WINDOWS = 1200
@dataclass
class ConceptScore:
concept: str
score: float
carried_by: str
survived: bool
def text_windows(text: str, min_words: int = 1) -> List[str]:
"""
Clauses, sentences and sliding word windows of several sizes, de-duplicated, in reading order.
min_words drops fragments shorter than that: very short fragments ("room with a") embed vaguely and
score deceptively high against anything on the same topic.
"""
flat = " ".join((text or "").split())
if not flat:
return []
windows: List[str] = []
seen = set()
def add(candidate: str) -> None:
cleaned = candidate.strip(" ,.;:-")
if len(cleaned) > 2 and len(cleaned.split()) >= min_words and cleaned.lower() not in seen:
seen.add(cleaned.lower())
windows.append(cleaned)
for sentence in re.split(r"(?<=[.!?;])\s+", flat):
add(sentence)
for clause in re.split(r",\s+", sentence):
add(clause)
words = flat.split(" ")
for size in WINDOW_SIZES:
if size < min_words or len(words) <= size:
continue
stride = max(1, size // 3)
for start in range(0, len(words) - size + 1, stride):
add(" ".join(words[start:start + size]))
if not windows:
add(flat) if len(flat.split()) >= 1 else None
if not windows:
windows.append(flat)
return windows[:MAX_WINDOWS]
def score_concepts(embedder: Embedder, prompt: str, concepts: List[str], threshold: float) -> List[ConceptScore]:
concepts = [concept for concept in concepts if concept and concept.strip()]
if not concepts:
return []
lowered = " ".join((prompt or "").lower().split())
# Compare like with like: a window is never shorter than the longest concept, and never under 4 words.
longest = max(len(concept.split()) for concept in concepts)
windows = text_windows(prompt, min_words=max(4, longest))
results: List[ConceptScore] = []
to_embed = [concept for concept in concepts if " ".join(concept.lower().split()) not in lowered]
window_vectors = concept_vectors = None
if to_embed and windows:
window_vectors = embedder.encode(windows)
concept_vectors = embedder.encode(to_embed)
for concept in concepts:
if concept not in to_embed:
results.append(ConceptScore(concept, 1.0, concept, True))
continue
if window_vectors is None:
results.append(ConceptScore(concept, 0.0, "", False))
continue
similarities = window_vectors @ concept_vectors[to_embed.index(concept)]
best = int(np.argmax(similarities))
score = float(similarities[best])
results.append(ConceptScore(concept, score, windows[best], score >= threshold))
return results