mirror of
https://github.com/vladmandic/automatic
synced 2026-09-20 01:31:13 +02:00
fix prompt weighted lists
Signed-off-by: vladmandic <mandic00@live.com>
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
- fix upscaler init error should not block server
|
||||
- improve torch nvidia arch detection
|
||||
- add torch amd arch detection
|
||||
- fix prompt weighted lists and internal wildcards
|
||||
|
||||
## Update for 2026-04-01
|
||||
|
||||
|
||||
+28
-44
@@ -46,64 +46,48 @@ def apply_styles_to_prompt(prompt, styles):
|
||||
|
||||
|
||||
def select_from_weighted_list(inner: str) -> str:
|
||||
if not inner:
|
||||
def _split_weight(p: str):
|
||||
"""Split 'name:weight' where the colon separator must not be inside brackets. Returns (name, wstr) or None."""
|
||||
depth = 0
|
||||
last_colon = -1
|
||||
for i, c in enumerate(p):
|
||||
if c in '<([{':
|
||||
depth += 1
|
||||
elif c in '>)]}':
|
||||
if depth > 0:
|
||||
depth -= 1
|
||||
elif c == ':' and depth == 0:
|
||||
last_colon = i
|
||||
if last_colon < 0:
|
||||
return None
|
||||
return p[:last_colon].strip(), p[last_colon + 1:].strip()
|
||||
|
||||
if not inner or len(inner.strip()) == 0:
|
||||
return ''
|
||||
|
||||
parts = [p.strip() for p in inner.split('|') if p.strip()]
|
||||
weighted: dict[str, float] = {}
|
||||
unweighted = []
|
||||
|
||||
for p in parts:
|
||||
is_list = (p.startswith('(') and p.endswith(')')) or \
|
||||
(p.startswith('[') and p.endswith(']')) or \
|
||||
(p.startswith('{') and p.endswith('}')) or \
|
||||
(p.startswith('<') and p.endswith('>'))
|
||||
if (':' in p) and not is_list:
|
||||
name, wstr = p.split(':', 1)
|
||||
name = name.strip()
|
||||
split = _split_weight(p)
|
||||
if split is not None:
|
||||
name, wstr = split
|
||||
try:
|
||||
w = float(wstr.strip())
|
||||
w = float(wstr)
|
||||
except Exception:
|
||||
w = 0.0
|
||||
w = max(0.0, w)
|
||||
weighted[name] = weighted.get(name, 0.0) + w
|
||||
else:
|
||||
unweighted.append(p)
|
||||
weighted[p] = 1.0
|
||||
|
||||
W = sum(weighted.values())
|
||||
U = len(unweighted)
|
||||
|
||||
if U == 0: # only weighted options
|
||||
keys = list(weighted.keys())
|
||||
if not keys:
|
||||
return ''
|
||||
if W == 0.0:
|
||||
return ''
|
||||
if abs(W - 1.0) > 1e-12:
|
||||
weighted = {k: v / W for k, v in weighted.items()}
|
||||
else: # mix of weighted and unweighted
|
||||
if W > 1.0: # weighted probabilities consume whole mass -> normalize them, unweighted get 0
|
||||
for name in unweighted:
|
||||
weighted[name] = weighted.get(name, 0.0) + 1.0
|
||||
total_before = sum(weighted.values())
|
||||
if total_before > 0.0:
|
||||
weighted = {k: v / total_before for k, v in weighted.items()}
|
||||
else:
|
||||
remaining = 1.0 - W
|
||||
per = remaining / U if U > 0 else 0.0
|
||||
for name in unweighted:
|
||||
weighted[name] = weighted.get(name, 0.0) + per
|
||||
|
||||
items = list(weighted.items())
|
||||
if not items:
|
||||
return ''
|
||||
|
||||
total = sum(v for _, v in items)
|
||||
if total <= 0.0:
|
||||
return items[0][0]
|
||||
|
||||
names, weights = zip(*items, strict=False)
|
||||
return random.choices(names, weights=weights, k=1)[0]
|
||||
if len(weighted) == 0 or W <= 0.0:
|
||||
return inner
|
||||
weighted = {k: v / W for k, v in weighted.items()} # normalize to sum=1
|
||||
names, weights = zip(*weighted.items(), strict=False)
|
||||
choice = random.choices(names, weights=weights, k=1)[0]
|
||||
return choice
|
||||
|
||||
|
||||
def apply_curly_braces_to_prompt(prompt, seed=-1):
|
||||
|
||||
+39
-30
@@ -4,10 +4,11 @@ import sys
|
||||
import os
|
||||
from collections import Counter
|
||||
|
||||
|
||||
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.chdir(script_dir)
|
||||
|
||||
# --- test defition -------------------------------
|
||||
|
||||
# library
|
||||
fn = r'./modules/styles.py'
|
||||
# tested function
|
||||
@@ -15,9 +16,9 @@ funcname = 'select_from_weighted_list'
|
||||
# random needed
|
||||
ns = {'Dict': dict, 'random': __import__('random')}
|
||||
# number of samples to test
|
||||
tries = 2000
|
||||
tries = 10000
|
||||
# allowed deviation in percentage points
|
||||
tolerance_pct = 5
|
||||
tolerance_pct = 2.0
|
||||
# tests
|
||||
tests = [
|
||||
# - empty
|
||||
@@ -25,24 +26,33 @@ tests = [
|
||||
# - no weights
|
||||
[ "red|blonde|black", { 'black': 33, 'red': 33, 'blonde': 33 } ],
|
||||
# - full weights <= 1
|
||||
[ "red:0.1|blonde:0.9", { 'blonde': 90, 'red': 10 } ],
|
||||
[ "red:0.1|blonde:0.9", { 'red': 10, 'blonde': 90 } ],
|
||||
# - weights > 1 to test normalization
|
||||
[ "red:1|blonde:2|black:5", { 'blonde': 25, 'red': 12.5, 'black': 62.5 } ],
|
||||
[ "red:1|blonde:2|black:5", { 'red': 12.5, 'blonde': 25, 'black': 62.5 } ],
|
||||
# - disabling 0 weights to force one result
|
||||
[ "red:0|blonde|black:0", { 'blonde': 100 } ],
|
||||
# - weights <= 1 with distribution of the leftover
|
||||
[ "red:0.5|blonde|black:0.3|brown", { 'red': 50, 'black': 30, 'brown': 10, 'blonde': 10 } ],
|
||||
[ "red:0.5|blonde|black:0.3|brown", { 'red': 0.5, 'blonde': 1.0, 'black': 0.3, 'brown': 1.0 } ],
|
||||
# - weights > 1, unweightes should get default of 1
|
||||
[ "red:2|blonde|black", { 'red': 50, 'blonde': 25, 'black': 25 } ],
|
||||
# - ignore content of ()
|
||||
[ "red:0.5|(blonde:1.3)", { 'red': 50, '(blonde:1.3)': 50 } ],
|
||||
[ "red:0.5|(blonde:1.3)", { 'red': 50, '(blonde:1.3)': 100 } ],
|
||||
# - ignore content of ()
|
||||
[ "red:0.5|(blonde:1.3):0.5", { 'red': 50, '(blonde:1.3)': 50 } ],
|
||||
# - ignore content of []
|
||||
[ "red:0.5|[stuff:1.3]", { '[stuff:1.3]': 50, 'red': 50 } ],
|
||||
[ "red:0.5|[stuff:1.3]", { 'red': 50, '[stuff:1.3]': 100 } ],
|
||||
# - ignore content of <>
|
||||
[ "red:0.5|<lora:1.0>", { '<lora:1.0>': 50, 'red': 50 } ]
|
||||
[ "red:0.5|<lora:1.0>", { 'red': 50, '<lora:1.0>': 100 } ],
|
||||
# - simple list, 1 entry with lora with weights
|
||||
[ "red|<lora:test:1.0>|black", { 'black': 33, 'red': 33, '<lora:test:1.0>': 33 } ],
|
||||
# - simple list, 1 entry with loraand comma
|
||||
[ "red|blonde <lora:test:1.0>|black", { 'black': 33, 'red': 33, 'blonde <lora:test:1.0>': 33 } ],
|
||||
# - simple list, 1 entry with lora and comma
|
||||
[ "red|blonde, <lora:test:1.0>|black", { 'black': 33, 'red': 33, 'blonde, <lora:test:1.0>': 33 } ],
|
||||
# - simple list, 1 entry with lora and comma
|
||||
[ "red|blonde, <lora:test:1.0>|black:2", { 'black': 50, 'red': 25, 'blonde, <lora:test:1.0>': 25 } ],
|
||||
]
|
||||
|
||||
# -------------------------------------------------
|
||||
|
||||
with open(fn, 'r', encoding='utf-8') as f:
|
||||
src = f.read()
|
||||
@@ -57,7 +67,7 @@ with open(fn, 'r', encoding='utf-8') as f:
|
||||
end = min(end_candidates) if end_candidates else len(src)
|
||||
func_src = src[start:end]
|
||||
|
||||
exec(func_src, ns)
|
||||
exec(func_src, ns) # pylint: disable=exec-used
|
||||
func = ns.get(funcname)
|
||||
if func is None:
|
||||
print('Failed to extract function')
|
||||
@@ -65,16 +75,12 @@ with open(fn, 'r', encoding='utf-8') as f:
|
||||
|
||||
print('Running' , tries, 'isolated quick tests for ' + funcname + ':\n')
|
||||
|
||||
"""Print test summary."""
|
||||
print("\n" + "=" * 70)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 70)
|
||||
|
||||
for t in tests:
|
||||
print('INPUT:', t)
|
||||
print('Input:', t[0])
|
||||
samples = [func(t[0]) for _ in range(tries)]
|
||||
c = Counter(samples)
|
||||
print("SAMPLES: ", dict(c))
|
||||
print(" Expected:", dict(t[1]))
|
||||
print(" Samples:", dict(c))
|
||||
|
||||
# validation
|
||||
expected_pct = t[1]
|
||||
@@ -85,29 +91,32 @@ with open(fn, 'r', encoding='utf-8') as f:
|
||||
|
||||
if missing or unexpected:
|
||||
if missing:
|
||||
print("MISSING: ", sorted(missing))
|
||||
print(" Missing: ", sorted(missing))
|
||||
if unexpected:
|
||||
print("UNEXPECTED: ", sorted(unexpected))
|
||||
print("RESULT: FAILED (keys)")
|
||||
print('')
|
||||
print(" Unexpected: ", sorted(unexpected))
|
||||
print(" Result: FAIL keys")
|
||||
continue
|
||||
|
||||
failures = []
|
||||
W = sum(expected_pct.values())
|
||||
deviations = {}
|
||||
for k, pct in expected_pct.items():
|
||||
expected_count = tries * (pct / 100.0)
|
||||
pct = pct / W # normalize
|
||||
expected_count = tries * pct
|
||||
actual_count = c.get(k, 0)
|
||||
actual_pct = (actual_count / tries) * 100.0
|
||||
if abs(actual_pct - pct) > tolerance_pct:
|
||||
actual_pct = actual_count / tries
|
||||
deviation_pct = abs(actual_pct - pct)
|
||||
deviations[k] = deviation_pct
|
||||
if deviation_pct > tolerance_pct / 100.0:
|
||||
failures.append(
|
||||
f"{k}: expected {pct:.1f}%, got {actual_pct:.1f}% "
|
||||
f"({actual_count}/{tries})"
|
||||
)
|
||||
|
||||
print(" Deviations:", {k: f"{v*100:.2f}%" for k, v in deviations.items()})
|
||||
if failures:
|
||||
print("OUT OF RANGE: ")
|
||||
for line in failures:
|
||||
print(" - " + line)
|
||||
print("RESULT: FAILED (distribution)")
|
||||
print(" " + line)
|
||||
print(" Result: FAIL distribution")
|
||||
else:
|
||||
print("RESULT: PASSED")
|
||||
print('')
|
||||
print(" Result: PASS")
|
||||
|
||||
Reference in New Issue
Block a user