Files
DumiFlex 18e9243542 feat(tutorials): review round 3 (callout hold rule, A17 rewrite, AI3 image ending, starter library) (#357)
Callout hold rule, dead-pause fix after runs, A17 rewrite (toggle variables first), AI3 ends by rendering with the built pack's top bundle, AI1 line, box lines in A16/T10/T12, N13 at 20x, and three starter-library items (Daydream, Lights on or off, Shot per frame).
2026-11-11 22:58:26 +01:00

571 lines
17 KiB
Python

"""Writes tutorials/library.json, the library every tutorial starts from.
python tutorials/build_library.py
Each list holds one idea, and each option is written so an image model can't misread it.
$subject is a combine of who the person is ($role) and how they look ($hair, $eyes); what
they wear ($outfit) and hold ($held) stay their own variables, so a LoRA can swap one part.
The ids are fixed so the tests can name them.
"""
from __future__ import annotations
import json
from datetime import datetime, timezone
from pathlib import Path
OUT = Path(__file__).resolve().parent / "library.json"
NOW = datetime(2026, 11, 9, 12, 0, tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
ROLES = ("knight", "mage", "ranger", "inventor")
def opt(oid: str, value: str, tags: list[str] | None = None, **extra) -> dict:
o = {"id": oid, "value": value, "weight": 1, "sub_categories": tags or []}
o.update(extra)
return o
def wildcard(mid: str, name: str, var: str, options: list[dict], desc: str, **extra) -> dict:
tags: list[str] = []
for o in options:
for t in o["sub_categories"]:
if t not in tags:
tags.append(t)
payload = {"var_binding": var, "sub_categories": tags, "options": options}
payload.update(extra)
return {"id": mid, "type": "wildcard", "name": name, "description": desc, "payload": payload}
def branch(var: str, value: str, target: str, mode: str, text: str, *extra: tuple[str, str, str]):
b = {
"condition": {"var": var, "op": "equals", "value": value},
"action": {"target_var": target, "mode": mode, "value": text},
}
if extra:
b["extra_actions"] = [{"target_var": t, "mode": m, "value": v} for t, m, v in extra]
return b
def derivation(
mid: str, name: str, rid: str, branches: list[dict], desc: str, otherwise=None
) -> dict:
rule: dict = {"branches": branches, "id": rid}
if otherwise:
rule["else"] = otherwise
return {
"id": mid,
"type": "derivation",
"name": name,
"description": desc,
"payload": {"rules": [rule]},
}
def combine(mid: str, name: str, template: str, out: str, inputs: list[str], desc: str) -> dict:
return {
"id": mid,
"type": "combine",
"name": name,
"description": desc,
"payload": {"template": template, "output_var": out, "input_vars": inputs},
}
def constraint(mid: str, name: str, src: dict, tgt: dict, matrix: dict, desc: str) -> dict:
return {
"id": mid,
"type": "constraint",
"name": name,
"description": desc,
"payload": {
"source_wildcard_id": src["id"],
"target_wildcard_id": tgt["id"],
"source_wildcard_name": src["name"],
"target_wildcard_name": tgt["name"],
"exceptions": [],
"matrix": matrix,
},
}
# --- Lists --------------------------------------------------------------------------------
ROLE = wildcard(
"808878ba",
"Role",
"role",
[
opt("c8fc80d0", "a knight", ["knight"]),
opt("c243437e", "a battle mage", ["mage"]),
opt("784f4138", "a forest ranger", ["ranger"]),
opt("422b5317", "a clockwork inventor", ["inventor"]),
],
"Who the subject is. Its tags decide the outfit, the item and the place.",
)
COLORS = wildcard(
"bc15a82d",
"Hair colors",
"hair_color",
[
opt("7d1e78ef", "auburn"),
opt("6c3528de", "silver"),
opt("e6eda3f2", "jet black"),
opt("1f0c8a31", "copper red"),
opt("5a77e0c4", "platinum blonde"),
opt("9b2d6e10", "chestnut brown"),
],
"Switched off: only read through @{} references in Hair.",
)
HAIR = wildcard(
"4facf8ce",
"Hair",
"hair",
[
opt("a3ff1986", "short @{bc15a82d#Hair colors} hair"),
opt("e6d04f1d", "long @{bc15a82d#Hair colors} hair in a loose braid"),
opt("c71b55d2", "shoulder-length wavy @{bc15a82d#Hair colors} hair"),
opt("0e3f9a4b", "a shaved head"),
],
"Hair style; the colour comes from Hair colors through an @{} reference.",
)
EYES = wildcard(
"38c1b584",
"Eyes",
"eyes",
[
opt("2b8e61a0", "bright green eyes"),
opt("8c41d7f2", "pale blue eyes"),
opt("5e0a93bd", "dark brown eyes"),
opt("d36f2c18", "amber eyes"),
],
"Eye colour only.",
)
OUTFIT = wildcard(
"195238e2",
"Outfit",
"outfit",
[
opt(
"72f8e1d7",
"polished steel plate armor with a blue surcoat",
["knight"],
negative="rust, broken armor",
),
opt("4c0b7e95", "a chainmail shirt under a red tabard", ["knight"]),
opt("b5a2c3d1", "a long deep-blue robe embroidered with silver stars", ["mage"]),
opt("e81f4a6c", "a crimson long coat with brass buttons", ["mage"]),
opt("0d9c2b57", "a hooded green cloak over a brown leather jerkin", ["ranger"]),
opt("6a3e18f0", "a fur-trimmed leather hunting coat", ["ranger"]),
opt("f2c7d905", "a leather work apron over a white linen shirt", ["inventor"]),
opt("93b0e4a8", "a brown waistcoat with rolled-up shirt sleeves", ["inventor"]),
opt("5c61f0e2", "a plain grey wool tunic", fallback=True),
],
"Clothes only, tagged by role. The grey tunic is the fallback.",
)
HELD = wildcard(
"7a41c0de",
"Held item",
"held",
[
opt("1a7c3e90", "a longsword with a gold hilt", ["knight"]),
opt("2b8d4fa1", "a round wooden shield painted with a lion", ["knight"]),
opt("3c9e50b2", "a glowing oak staff", ["mage"]),
opt("4daf61c3", "an open spellbook with glowing pages", ["mage"]),
opt("5eb072d4", "a wooden longbow", ["ranger"]),
opt("6fc183e5", "a brass lantern", ["ranger"]),
opt("70d294f6", "a large brass wrench", ["inventor"]),
opt("81e3a507", "a small clockwork bird", ["inventor"]),
],
"What the subject holds, tagged by role.",
)
SETTING = wildcard(
"aa76ada8",
"Setting",
"setting",
[
opt("c1a0b2c3", "in a torch-lit stone throne room", ["castle"]),
opt("d2b1c3d4", "on the battlements of a stone castle", ["castle"]),
opt("e3c2d4e5", "in a misty pine forest at the edge of a cliff", ["forest"]),
opt("f4d3e5f6", "in a mossy clearing in an old oak forest", ["forest"]),
opt("a5e4f607", "on a crowded cobblestone market street", ["city"]),
opt("b6f50718", "in a cluttered workshop full of brass gears", ["city"]),
],
"Where. Its tags (castle, forest, city) meet Role in Role fits the place.",
)
TIME = wildcard(
"49cac59c",
"Time of day",
"time",
[
opt("7c740b5b", "at dawn"),
opt("7ddc9c86", "at sunset"),
opt("a681b8ca", "at night"),
],
"When. Lighting reads it.",
)
FRAMING = wildcard(
"3e5f7a9b",
"Framing",
"framing",
[
opt("0a1b2c3d", "A full-body shot"),
opt("1b2c3d4e", "A waist-up portrait"),
opt("2c3d4e5f", "A close-up portrait"),
],
"How the camera frames the subject. It starts the sentence.",
)
COMPANION = wildcard(
"8e1f3a5c",
"Companion",
"companion",
[
opt("9f203b4d", "A black raven sits on their shoulder."),
opt("a0314c5e", "A grey wolf stands at their side."),
opt("b1425d6f", "A small red fox sits at their feet."),
opt("c2536e70", "", is_null=True),
],
"A sentence to end on. The empty option means no companion, a quarter of the time.",
)
GEAR = wildcard(
"a1b2c3d4",
"Travel gear",
"gear",
[
opt("5e1a0001", "a hooded leather coat", ["cold", "boots"]),
opt("5e1a0002", "a fur-lined cloak", ["cold", "boots"]),
opt("5e1a0003", "a light linen robe", ["warm", "sandals", "slippers"]),
opt("5e1a0004", "a silk kimono", ["warm", "sandals", "slippers"]),
opt("5e1a0005", "a ranger's jerkin", ["warm", "boots", "sandals"]),
],
"Tag groups: CLIMATE describes the option, SHOES is an accepts axis read as $gear.SHOES.",
tag_groups={"CLIMATE": ["cold", "warm"], "SHOES": ["boots", "sandals", "slippers"]},
tag_group_kinds={"SHOES": "accepts"},
)
QUALITY = wildcard(
"9a7b5c3d",
"Quality per model",
"quality",
[
opt("9a7b0001", "score_9, score_8_up, score_7_up", ["pony"]),
opt("9a7b0002", "masterpiece, best quality, very aesthetic", ["illustrious"]),
opt("9a7b0003", "Sharp focus, rich detail and soft natural light.", ["klein"]),
],
"Quality words, tagged by model variant; a Pool filtered by $model_variant keeps the model's.",
tag_groups={"MODEL": ["pony", "illustrious", "klein"]},
)
# --- Rules and lines ------------------------------------------------------------------------
SUBJECT = combine(
"9d2e4f61",
"Subject",
"$role with $hair and $eyes",
"subject",
["role", "hair", "eyes"],
"The person: role, hair and eyes in one line.",
)
LIGHTING = derivation(
"5774d2a1",
"Lighting from time",
"e751ba3a",
[
branch(
"time",
"at night",
"lighting",
"replace",
"lit by pale moonlight and warm lantern glow",
("lighting", "negative", "overexposed, harsh sunlight"),
),
branch("time", "at sunset", "lighting", "replace", "with warm orange rim light"),
],
"Light that fits the time. At night it also adds negatives.",
otherwise={
"action": {"target_var": "lighting", "mode": "replace", "value": "in soft morning light"}
},
)
SCENE = combine(
"5d75075f",
"Scene",
"$setting $time, $lighting",
"scene",
["setting", "time", "lighting"],
"Where, when and the light, as one phrase.",
)
STYLE = {
"id": "c32f16d6",
"type": "fixed_values",
"name": "Style",
"description": "The look of every picture, with its negatives.",
"payload": {
"values": [
{
"id": "9af7bf95",
"name": "style",
"value": (
"A digital painting, painterly brush strokes, illustration,"
" sharp focus and rich color."
),
"negative": "photo, photorealistic, 3d render, blurry",
}
]
},
}
ONLY = {r: {r: {"mode": "only", "factor": 1}} for r in ROLES}
OUTFIT_FITS = constraint(
"6b8c0d2e",
"Outfit fits the role",
ROLE,
OUTFIT,
ONLY,
"Only rule: each role wears only outfits with its own tag.",
)
HELD_FITS = constraint(
"7c9d1e3f",
"Item fits the role",
ROLE,
HELD,
ONLY,
"Only rule: each role holds only items with its own tag.",
)
PLACE_FITS = constraint(
"7ff544c2",
"Role fits the place",
ROLE,
SETTING,
{
"knight": {
"castle": {"mode": "boost", "factor": 4},
"forest": {"mode": "reduce", "factor": 0.5},
},
"mage": {"castle": {"mode": "boost", "factor": 2}},
"ranger": {
"forest": {"mode": "boost", "factor": 4},
"city": {"mode": "reduce", "factor": 0.3},
},
"inventor": {
"city": {"mode": "boost", "factor": 4},
"castle": {"mode": "exclude", "factor": 0},
"forest": {"mode": "exclude", "factor": 0},
},
},
"Re-weights Setting by the Role picked upstream: a knight favours the castle,"
" an inventor stays in town, never in a castle or the forest.",
)
LORA_CHARACTER = derivation(
"dd2bee53",
"Character LoRA",
"0420af2a",
[branch("lora_character", "true", "subject", "replace", "$lora_character_words")],
"While $lora_character is true: the LoRA's trigger words replace the whole $subject.",
)
LORA_CLOTHES = derivation(
"435b5087",
"Its own clothes",
"7cf35a83",
[branch("lora_clothes", "true", "outfit", "replace", "$lora_outfit_words")],
"While $lora_clothes is true: the LoRA's trigger words replace $outfit.",
)
LORA_STYLE = derivation(
"804fee67",
"Style LoRA",
"ddfcd524",
[branch("style_lora", "true", "style", "append", " $lora_style_words")],
"While $style_lora is true: the LoRA's trigger words go after $style.",
)
def settings_branch(family: str, size: str, sampler: str, cfg: str) -> dict:
return branch(
"model_family",
family,
"size",
"replace",
size,
("sampler", "replace", sampler),
("cfg", "replace", cfg),
)
SETTINGS = derivation(
"5b008dc7",
"Settings per model",
"e4788601",
[
settings_branch("sd15", "512x768", "euler ancestral", "7"),
settings_branch("sdxl", "832x1216", "dpmpp 2m", "6"),
settings_branch("flux", "896x1152", "euler", "1"),
],
"Reads $model_family from WP Model Info: $size, $sampler and $cfg.",
otherwise={
"action": {"target_var": "size", "mode": "replace", "value": "1024x1024"},
"extra_actions": [
{"target_var": "sampler", "mode": "replace", "value": "euler"},
{"target_var": "cfg", "mode": "replace", "value": "5"},
],
},
)
# Items a video uses without building them on screen, so a viewer following along has them too.
DAYDREAM = wildcard(
"9e1a7c42",
"Daydream",
"daydream",
[opt("d1a7c420", "dreaming of being @{aa76ada8#Setting}")],
"Reads Setting through an @{} reference, so a constraint on Setting reaches inside it (A09).",
)
LIGHTS_SWITCH = derivation(
"b17a0ff1",
"Lights on or off",
"b17a0ff2",
[branch("use_lighting", "false", "lighting", "replace", "")],
"While $use_lighting is false (a WP Toggles switch), the lighting words go (A17).",
)
SHOT = derivation(
"5a0b51e1",
"Shot per frame",
"a51c0e01",
[branch("iteration", "1", "shot", "replace", "wide establishing shot")],
"Frame 1 is a wide establishing shot, every other frame a close-up (N05).",
otherwise={"action": {"target_var": "shot", "mode": "replace", "value": "close-up"}},
)
MODULES = [
ROLE, COLORS, HAIR, EYES, OUTFIT, HELD, SETTING, TIME, FRAMING, GEAR, COMPANION, QUALITY,
SUBJECT, LIGHTING, SCENE, STYLE, OUTFIT_FITS, HELD_FITS, PLACE_FITS,
LORA_CHARACTER, LORA_CLOTHES, LORA_STYLE, SETTINGS,
DAYDREAM, LIGHTS_SWITCH, SHOT,
] # fmt: skip
# --- Bundles ----------------------------------------------------------------------------------
def child(m: dict, enabled: bool = True) -> dict:
return {
"id": m["id"],
"type": m["type"],
"enabled": enabled,
"collapsed": True,
"meta": {"name": m["name"], "library_name": m["name"], "description": m["description"]},
"entries": [],
"payload": m["payload"],
}
def ref(b: dict) -> dict:
return {"id": b["id"], "type": "bundle", "name": b["name"], "color": b["color"]}
def bundle(bid: str, name: str, color: str, children: list[dict]) -> dict:
return {
"id": bid,
"name": name,
"description": "",
"color": color,
"category_id": None,
"tags": [],
"is_favorite": False,
"children": children,
}
SHARED = bundle("805bdfa4", "Shared lists", "#7a7f8a", [child(COLORS, enabled=False)])
LOOK = bundle("37af0127", "Look", "#9b6bff", [child(HAIR), child(EYES)])
WARDROBE = bundle(
"b748104b",
"Outfit",
"#e0a030",
[child(OUTFIT_FITS), child(HELD_FITS), child(OUTFIT), child(HELD)],
)
CHARACTER = bundle(
"7e3d3ec2",
"Character",
"#4f8cff",
[child(ROLE), ref(SHARED), ref(LOOK), child(SUBJECT), ref(WARDROBE)],
)
SCENE_B = bundle(
"86360bc3",
"Scene",
"#2fb380",
[
child(PLACE_FITS),
child(SETTING),
child(TIME),
child(LIGHTING),
child(SCENE),
child(FRAMING),
child(STYLE),
],
)
LORAS = bundle(
"3faee950",
"LoRA swaps",
"#d0508a",
[child(LORA_CHARACTER), child(LORA_CLOTHES), child(LORA_STYLE)],
)
BUNDLES = [LOOK, LORAS, CHARACTER, SHARED, SCENE_B, WARDROBE]
# --- File -----------------------------------------------------------------------------------
ROW = {
"category_id": None,
"tags": [],
"is_favorite": False,
"version": 1,
"created_at": NOW,
"updated_at": NOW,
"community_post_slug": None,
"community_version_number": None,
"content_rating": "safe",
}
KEYS = {
"wildcard": "wildcards",
"fixed_values": "fixed_values",
"combine": "combines",
"derivation": "derivations",
"constraint": "constraints",
}
def main() -> None:
out: dict = {"schema_version": 9, "exported_at": NOW, "bundles": []}
for key in KEYS.values():
out[key] = []
out["categories"] = []
out["templates"] = []
for b in BUNDLES:
out["bundles"].append({**ROW, **b})
for m in MODULES:
out[KEYS[m["type"]]].append({**ROW, **m})
OUT.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n", encoding="utf-8")
print(f"wrote {OUT} ({len(MODULES)} modules, {len(BUNDLES)} bundles)")
if __name__ == "__main__":
main()