File size: 8,223 Bytes
2552774
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
"""Queue MOSS-Image prompts against the local ComfyUI (http://127.0.0.1:8188).

  python3 moss.py "a gorilla in a nightclub..." --seeds 1 2 3
  python3 moss.py -f prompts.txt --enhance --lora r256
"""
import argparse, copy, json, os, sys, time, urllib.request

HERE = os.path.dirname(os.path.abspath(__file__))
URL = "http://127.0.0.1:8188"
OUT = os.path.join(HERE, "..", "..", "output")
MODELS = {
    "base": "moss_image_base_bf16.safetensors",
    "sft": "moss_image_sft_bf16.safetensors",
}
LORAS = {
    "b21": "moss_image_sft_lora_blocks_21_to_49_rank_512_bf16.safetensors",
    "r256": "moss_image_sft_lora_rank_256_bf16.safetensors",
    "r512": "moss_image_sft_lora_rank_512_bf16.safetensors",
}


def call(path, data=None):
    req = urllib.request.Request(URL + path, data=json.dumps(data).encode() if data is not None else None,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req) as r:
        return json.load(r)


def build(base, prompt, seed, a):
    g = copy.deepcopy(base)
    g["1"]["inputs"]["unet_name"] = MODELS.get(a.model, a.model)
    if a.swap_layers is not None or a.swap_other:
        # base model + exact donor weights for chosen layers/parts (LoRA chain is skipped)
        g["40"] = {"class_type": "UNETLoader", "inputs": {"unet_name": MODELS.get(a.donor, a.donor), "weight_dtype": "default"}}
        g["41"] = {"class_type": "MOSSBlockSwap", "inputs": {"model": ["1", 0], "donor": ["40", 0], "layers": a.swap_layers or "",
                   "parts": a.swap_parts, "include_other": a.swap_other, "ratio": a.swap_ratio}}
        g["16"]["inputs"]["model"] = ["41", 0]
        del g["30"]
    elif a.lora == "none":
        g["16"]["inputs"]["model"] = ["1", 0]
        del g["30"]
    else:
        g["30"]["inputs"]["lora_name"] = LORAS.get(a.lora, a.lora)
        g["30"]["inputs"]["strength_model"] = a.strength
    if a.lora2:
        g["31"] = {"class_type": "LoraLoaderModelOnly", "inputs": {"model": g["16"]["inputs"]["model"],
                   "lora_name": LORAS.get(a.lora2, a.lora2), "strength_model": a.strength2}}
        g["16"]["inputs"]["model"] = ["31", 0]
    if a.enhance:
        g["17"]["inputs"]["prompt"] = prompt
        g["17"]["inputs"]["sampling_mode.seed"] = seed
    else:
        g["5"]["inputs"]["text"] = prompt
        for k in ("17", "18", "19"):
            del g[k]
    if a.probe:
        g["42"] = {"class_type": "MOSSStripeProbe", "inputs": {"model": g["16"]["inputs"]["model"], "head_from": MODELS.get(a.model, a.model),
                   "tag": f"{a.probe}_s{seed}", "width": a.width, "height": a.height}}
        g["16"]["inputs"]["model"] = ["42", 0]
    if a.no_tsr:
        g["2"]["inputs"]["model"] = g["16"]["inputs"]["model"]
        del g["16"]
    if a.no_slg:
        for k in ("20", "22"):
            g[k]["inputs"]["model"] = g["2"]["inputs"]["model"]
        del g["2"]
    if a.sampler != "er_sde":
        g["21"] = {"class_type": "KSamplerSelect", "inputs": {"sampler_name": a.sampler}}
    if a.negative is not None:
        g["6"]["inputs"]["text"] = a.negative
    g["20"]["inputs"].update(noise_seed=seed, cfg=a.cfg)
    g["22"]["inputs"]["steps"] = a.steps
    g["7"]["inputs"].update(width=a.width, height=a.height)
    g["10"]["inputs"]["filename_prefix"] = "moss/" + a.prefix
    if a.window_steps:
        window(g, a)
    return g


def window(g, a):
    """Apply --window-lora only for sampling steps a..b (1-indexed) via ComfyUI hook keyframes on both conds.
    --window-steps "1-N" applies it for the whole run; strength 0 everywhere is a no-op control."""
    lo, hi = (int(v) for v in a.window_steps.split("-"))
    n = a.steps
    g["70"] = {"class_type": "CreateHookLoraModelOnly", "inputs": {"lora_name": a.window_lora, "strength_model": a.window_strength}}
    kf = ([(0.0, 0.0)] if lo > 1 else []) + [((lo - 1) / n, 1.0)] + ([(hi / n, 0.0)] if hi < n else [])
    prev = None
    for i, (pct, mult) in enumerate(kf):
        node = {"class_type": "CreateHookKeyframe", "inputs": {"strength_mult": mult, "start_percent": pct}}
        if prev:
            node["inputs"]["prev_hook_kf"] = [prev, 0]
        prev = str(71 + i)
        g[prev] = node
    g["75"] = {"class_type": "SetHookKeyframes", "inputs": {"hooks": ["70", 0], "hook_kf": [prev, 0]}}
    for cid, src in (("76", "5"), ("77", "6")):
        g[cid] = {"class_type": "ConditioningSetProperties", "inputs": {"cond_NEW": [src, 0], "strength": 1.0, "set_cond_area": "default", "hooks": ["75", 0]}}
    g["20"]["inputs"]["positive"], g["20"]["inputs"]["negative"] = ["76", 0], ["77", 0]


def main():
    p = argparse.ArgumentParser()
    p.add_argument("prompt", nargs="*")
    p.add_argument("-f", "--file", help="one prompt per paragraph (blank-line separated)")
    p.add_argument("--seeds", type=int, nargs="+", default=[16])
    p.add_argument("--enhance", action="store_true", help="rewrite the prompt with Qwen3.5 first")
    p.add_argument("--model", default="sft", help="base | sft | <filename>")
    p.add_argument("--lora", default="b21", help="b21 | r256 | none | <filename>")
    p.add_argument("--strength", type=float, default=-0.5)
    p.add_argument("--donor", default="sft", help="model whose exact weights are swapped in (with --swap-layers)")
    p.add_argument("--swap-layers", help='layers to take from --donor, e.g. "21-41" or "3,4"; enables block swap')
    p.add_argument("--swap-parts", default="all", help="all | comma list of qkv,o,ffn_up_gate,ffn_down,norm")
    p.add_argument("--swap-other", action="store_true", help="also swap non-layer weights (embedders, modulation, head)")
    p.add_argument("--swap-ratio", type=float, default=1.0)
    p.add_argument("--probe", help="record per-layer stripe spectra to output/moss/probe/<probe>_s<seed>.npz")
    p.add_argument("--window-steps", help='e.g. "5-12": apply --window-lora only for these sampling steps (1-indexed)')
    p.add_argument("--window-lora", default="moss_image_sft_banding_fix_L37-41_bf16.safetensors")
    p.add_argument("--window-strength", type=float, default=1.0)
    p.add_argument("--lora2", help="second LoRA chained after the first, e.g. --lora r512 --strength 1 --lora2 b21 --strength2 -0.5")
    p.add_argument("--strength2", type=float, default=1.0)
    p.add_argument("--no-tsr", action="store_true")
    p.add_argument("--no-slg", action="store_true")
    p.add_argument("--sampler", default="er_sde", help="er_sde (workflow's SamplerER_SDE) or any KSampler sampler name")
    p.add_argument("--steps", type=int, default=25)
    p.add_argument("--cfg", type=float, default=3.0)
    p.add_argument("--width", type=int, default=1024)
    p.add_argument("--height", type=int, default=1024)
    p.add_argument("--negative")
    p.add_argument("--prefix", default="moss")
    a = p.parse_args()

    prompts = [" ".join(a.prompt)] if a.prompt else []
    if a.file:
        prompts += [x.strip() for x in open(a.file).read().split("\n\n") if x.strip()]
    if not prompts:
        p.error("give a prompt or -f file")

    base = json.load(open(os.path.join(HERE, "moss_image_t2i_api.json")))
    jobs = []
    for prompt in prompts:
        for seed in a.seeds:
            r = call("/prompt", {"prompt": build(base, prompt, seed, a)})
            if r.get("node_errors"):
                sys.exit(json.dumps(r["node_errors"], indent=1))
            jobs.append((r["prompt_id"], prompt, seed))
    print(f"queued {len(jobs)}", flush=True)

    for pid, prompt, seed in jobs:
        while True:
            h = call(f"/history/{pid}").get(pid)
            if h and h.get("status", {}).get("completed") is not None:
                break
            time.sleep(1)
        if h["status"].get("status_str") != "success":
            print(f"FAILED seed {seed}: {json.dumps(h['status'].get('messages', [])[-1:])[:800]}")
            continue
        outs = h["outputs"]
        for img in outs.get("10", {}).get("images", []):
            print(os.path.normpath(os.path.join(OUT, img["subfolder"], img["filename"])), f"seed={seed}")
        if "19" in outs:
            print("  enhanced:", outs["19"]["text"][0][:1500])


if __name__ == "__main__":
    main()