mcpbytes-pixel-sprite 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ """Fixed-layout pixel sprite generation and deterministic exports."""
@@ -0,0 +1,81 @@
1
+ """mcpbytes-pixel-sprite: 32x32 pixel-art character sprite sheets on your own GPU (or MCPBytes' with --cloud).
2
+
3
+ mcpbytes-pixel-sprite download fetch the pinned model files (~16 GB, once)
4
+ mcpbytes-pixel-sprite generate "a forest ranger" write sheets, frames, walk GIFs, Godot/Unity exports
5
+ mcpbytes-pixel-sprite mcp serve the create_pixel_sprite tool over MCP (stdio)
6
+ """
7
+
8
+ import argparse
9
+ import json
10
+ import sys
11
+ import time
12
+ from pathlib import Path
13
+
14
+ from mcpbytes_core import hub
15
+ from mcpbytes_core.errors import ToolError
16
+
17
+ from .engine import OFFLOAD_MODES
18
+ from .models import COMPONENTS
19
+
20
+
21
+ def log(message):
22
+ print(message, file=sys.stderr, flush=True)
23
+
24
+
25
+ def download():
26
+ if pending := hub.pending_bytes(COMPONENTS):
27
+ log(f"Downloading {pending / 1e9:.1f} GB of pinned model files from Hugging Face into its cache (resumable)")
28
+ hub.fetch(COMPONENTS)
29
+ log("Model files ready")
30
+
31
+
32
+ def generate(args):
33
+ from .generate import run_cloud, run_local
34
+
35
+ options = {"description": args.description, "variants": args.variants, "transparent": not args.opaque,
36
+ "palette_colors": args.palette_colors, "frame_duration_ms": args.frame_duration_ms,
37
+ "palette": args.palette, "seed": args.seed}
38
+ out = args.out or Path("pixel-sprites") / time.strftime("%Y%m%d-%H%M%S")
39
+ if args.cloud:
40
+ return run_cloud(options, args.reference, out, idempotency_key=args.idempotency_key, progress=log)
41
+ if hub.missing(COMPONENTS):
42
+ download()
43
+ return run_local(options, args.reference, out, offload=args.offload, progress=log)
44
+
45
+
46
+ def main(argv=None):
47
+ parser = argparse.ArgumentParser(prog="mcpbytes-pixel-sprite", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
48
+ commands = parser.add_subparsers(dest="command", required=True)
49
+ commands.add_parser("download", help="download the pinned model files (~16 GB) into the Hugging Face cache")
50
+ g = commands.add_parser("generate", help="generate 1-4 candidate sprite sheets from a description")
51
+ g.add_argument("description", help="one character, up to 4,000 characters")
52
+ g.add_argument("--reference", type=Path, help="PNG or JPEG of the character (yours to use)")
53
+ g.add_argument("--variants", type=int, default=1, help="candidate sheets, 1-4 (default 1)")
54
+ g.add_argument("--opaque", action="store_true", help="keep the background instead of removing it")
55
+ g.add_argument("--palette-colors", type=int, default=32, choices=(16, 32, 64))
56
+ g.add_argument("--palette", help="shared palette: 2-64 comma-separated #RRGGBB colors, no spaces")
57
+ g.add_argument("--frame-duration-ms", type=int, default=120, help="walk preview frame time, 40-1000 in steps of 10")
58
+ g.add_argument("--seed", type=int, help="0-4294967295; the same seed, inputs and hardware repeat a result")
59
+ g.add_argument("-o", "--out", type=Path, help="new or empty folder (default: pixel-sprites/<timestamp>)")
60
+ g.add_argument("--cloud", action="store_true", help="run on MCPBytes GPUs for credits (needs MCPBYTES_API_KEY)")
61
+ g.add_argument("--idempotency-key", help="with --cloud: the key printed by an interrupted run, to resume it without a new charge")
62
+ m = commands.add_parser("mcp", help="run the MCP server on stdio (for Claude, Cursor, VS Code and other MCP clients)")
63
+ m.add_argument("--cloud", action="store_true", help="serve the tool from MCPBytes GPUs for credits (needs MCPBYTES_API_KEY)")
64
+ for command in (g, m):
65
+ command.add_argument("--offload", choices=OFFLOAD_MODES, default="auto",
66
+ help="GPU memory mode: none needs ~17 GB, model ~8 GB, sequential less (slowest); auto picks by GPU size")
67
+ args = parser.parse_args(argv)
68
+ try:
69
+ if args.command == "download":
70
+ download()
71
+ elif args.command == "generate":
72
+ print(json.dumps(generate(args), indent=2))
73
+ else:
74
+ from .server import serve
75
+
76
+ serve(cloud=args.cloud, offload=args.offload)
77
+ except ToolError as e:
78
+ log(f"mcpbytes-pixel-sprite: {e}")
79
+ sys.exit(1)
80
+ except KeyboardInterrupt:
81
+ sys.exit(130)
@@ -0,0 +1,68 @@
1
+ """FLUX.2 [klein] base 4B with the pixel-spritesheet LoRA, loaded once from the pinned Hugging Face files."""
2
+
3
+ import sys
4
+ import threading
5
+
6
+ from mcpbytes_core import hub
7
+ from mcpbytes_core.gpu import require_cuda
8
+
9
+ from .inference import request_options
10
+ from .models import COMPONENTS, DOWNLOAD_COMMAND, ENCODER, LORA, TRANSFORMER
11
+
12
+ # GPU memory modes, chosen by the card's total memory. Peaks and per-image times measured at 512x512 on an
13
+ # RTX PRO 6000 with torch 2.8: "none" keeps every model resident (16.2 GiB reserved, 2.3 s); "model" moves
14
+ # whole models on and off the GPU per stage (7.8 GiB, 7.4 s; the hosted 24 GiB cards run this way);
15
+ # "sequential" streams layers (1.0 GiB, 34 s).
16
+ OFFLOAD_MODES = ("auto", "none", "model", "sequential")
17
+
18
+
19
+ def choose_offload(total_bytes):
20
+ return "none" if total_bytes >= 20 * 2**30 else "model" if total_bytes >= 10 * 2**30 else "sequential"
21
+
22
+
23
+ class Engine:
24
+ def __init__(self, offload="auto"):
25
+ torch = require_cuda()
26
+ roots = hub.locate(COMPONENTS, DOWNLOAD_COMMAND)
27
+ from diffusers import AutoencoderKLFlux2, FlowMatchEulerDiscreteScheduler, Flux2KleinPipeline, Flux2Transformer2DModel
28
+ from safetensors.torch import load_file
29
+ from transformers import Qwen2Tokenizer, Qwen3Config, Qwen3ForCausalLM
30
+
31
+ root = roots["main"]
32
+ # Official single-file conversion of the original base weights. The Qwen encoder has native
33
+ # Transformers keys and tied embeddings: no custom node runtime.
34
+ transformer = Flux2Transformer2DModel.from_single_file(str(root / TRANSFORMER), config=str(root), subfolder="transformer",
35
+ torch_dtype=torch.bfloat16, local_files_only=True)
36
+ encoder = Qwen3ForCausalLM.from_pretrained(None, config=Qwen3Config.from_pretrained(root / "text_encoder", local_files_only=True),
37
+ state_dict=load_file(str(roots["encoder"] / ENCODER)), dtype=torch.bfloat16, local_files_only=True)
38
+ self.pipe = Flux2KleinPipeline(transformer=transformer, text_encoder=encoder,
39
+ tokenizer=Qwen2Tokenizer.from_pretrained(root / "tokenizer", local_files_only=True),
40
+ vae=AutoencoderKLFlux2.from_pretrained(root / "vae", torch_dtype=torch.bfloat16, local_files_only=True),
41
+ scheduler=FlowMatchEulerDiscreteScheduler.from_pretrained(root / "scheduler", local_files_only=True),
42
+ is_distilled=False)
43
+ self.pipe.load_lora_weights(str(roots["lora"]), weight_name=LORA, adapter_name="character", local_files_only=True)
44
+ self.pipe.set_adapters(["character"], adapter_weights=[1.0])
45
+ self.pipe.set_progress_bar_config(disable=True) # stdout belongs to the MCP protocol in server mode
46
+ if offload == "auto":
47
+ offload = choose_offload(torch.cuda.get_device_properties(0).total_memory)
48
+ if offload == "model":
49
+ self.pipe.enable_model_cpu_offload()
50
+ elif offload == "sequential":
51
+ self.pipe.enable_sequential_cpu_offload()
52
+ else:
53
+ self.pipe.to("cuda")
54
+ self.torch, self.offload, self.lock = torch, offload, threading.Lock()
55
+ print(f"pixel sprite engine ready on {torch.cuda.get_device_name()} (offload: {offload})", file=sys.stderr, flush=True)
56
+
57
+ def render(self, description, reference, seed):
58
+ """One 512x512 raw sheet. `reference` is an RGB image or None."""
59
+ with self.lock:
60
+ try:
61
+ with self.torch.inference_mode():
62
+ return self.pipe(**request_options(description, reference),
63
+ generator=self.torch.Generator("cuda").manual_seed(seed)).images[0]
64
+ except Exception:
65
+ # Return memory to the offload hooks so the next request starts clean.
66
+ self.pipe.maybe_free_model_hooks()
67
+ self.torch.cuda.empty_cache()
68
+ raise
@@ -0,0 +1,54 @@
1
+ """Deterministic engine adapters from the same rectangles/timing used by PNG/GIF exports."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def engine_exports(manifest):
8
+ frames, animations = manifest["frames"], manifest["animations"]
9
+ ids = {frame["id"]: index for index, frame in enumerate(frames)}
10
+ resource = [f'[gd_resource type="SpriteFrames" load_steps={len(frames) + 2} format=3]',
11
+ '', '[ext_resource type="Texture2D" path="../sheet.png" id="1"]']
12
+ for index, frame in enumerate(frames):
13
+ rect = ", ".join(str(n) for n in frame["rect"])
14
+ resource.extend(['', f'[sub_resource type="AtlasTexture" id="Frame_{index}"]',
15
+ 'atlas = ExtResource("1")', f'region = Rect2({rect})', 'filter_clip = true'])
16
+ definitions = []
17
+ for name, animation in animations.items():
18
+ base = animation["durations_ms"][0]
19
+ sequence = ',\n'.join('{"duration": %.12g, "texture": SubResource("Frame_%d")}' % (duration / base, ids[frame])
20
+ for frame, duration in zip(animation["frames"], animation["durations_ms"]))
21
+ definitions.append('{"frames": [\n%s\n], "loop": %s, "name": &%s, "speed": %.12g}' %
22
+ (sequence, str(animation["loop"]).lower(), json.dumps(name), 1000 / base))
23
+ resource.extend(['', '[resource]', 'animations = [\n' + ',\n'.join(definitions) + '\n]'])
24
+ pivot = frames[0]["pivot"]
25
+ scene = f'''[gd_scene load_steps=2 format=3]
26
+
27
+ [ext_resource type="SpriteFrames" path="sprite_frames.tres" id="1"]
28
+
29
+ [node name="PixelCharacter" type="AnimatedSprite2D"]
30
+ texture_filter = 1
31
+ sprite_frames = ExtResource("1")
32
+ animation = &"walk_down"
33
+ autoplay = "walk_down"
34
+ centered = false
35
+ offset = Vector2({-pivot[0]}, {-pivot[1]})
36
+ '''
37
+ height = manifest["size"][1]
38
+ unity_frames = []
39
+ for frame in frames:
40
+ x, y, w, h = frame["rect"]
41
+ unity_frames.append({"name": frame["id"], "rect": {"x": x, "y": height - y - h, "width": w, "height": h},
42
+ "pivot": {"x": frame["pivot"][0] / w, "y": 1 - frame["pivot"][1] / h}})
43
+ unity = {"schema_version": 1, "sheet": "../sheet.png", "width": manifest["size"][0], "height": height,
44
+ "pixels_per_unit": manifest["cell_size"][1], "frames": unity_frames,
45
+ "animations": [{"name": name, "frames": animation["frames"], "durations_ms": animation["durations_ms"],
46
+ "loop": animation["loop"]} for name, animation in animations.items()]}
47
+ return {
48
+ "godot/sprite_frames.tres": ("\n".join(resource) + "\n").encode(),
49
+ "godot/character.tscn": scene.encode(),
50
+ "unity/import.json": (json.dumps(unity, indent=2, allow_nan=False) + "\n").encode(),
51
+ # Keep the helper as text in each archive: multiple candidates must not install duplicate C# classes.
52
+ "unity/MCPBytesSpriteImporter.cs.txt": (Path(__file__).parent / "templates/MCPBytesSpriteImporter.cs.txt").read_bytes(),
53
+ "IMPORTING.md": (Path(__file__).parent / "templates/IMPORTING.md").read_bytes(),
54
+ }
@@ -0,0 +1,139 @@
1
+ """create_pixel_sprite, run locally or on the hosted API. Both write the same files into one folder:
2
+ raw_N.png, sprite_N.png, sprite_N.json, sprite_N.zip per candidate N, and generation.json."""
3
+
4
+ import hashlib
5
+ import json
6
+ import re
7
+ import sys
8
+ import uuid
9
+ from importlib.metadata import version
10
+ from pathlib import Path
11
+
12
+ from PIL import Image
13
+
14
+ from mcpbytes_core.errors import ToolError
15
+ from mcpbytes_core.gpu import job_seed, stage_seed
16
+
17
+ from .inference import VERSION as INFERENCE_VERSION, inference_hash
18
+ from .models import COMPONENTS, MODEL_HASHES
19
+ from .pipeline import VERSION as PROCESSING_VERSION, package
20
+
21
+ TOOL = "create_pixel_sprite"
22
+ PALETTE = re.compile(r"#[a-fA-F0-9]{6}(,#[a-fA-F0-9]{6}){1,63}")
23
+ MAX_REFERENCE_PIXELS = 25_000_000 # the hosted tool's decoding bound
24
+
25
+
26
+ def stderr(message):
27
+ print(message, file=sys.stderr, flush=True)
28
+
29
+
30
+ def validate(options):
31
+ """Options with defaults applied, under the hosted tool's rules (create-pixel-sprite.ts), else invalid_options."""
32
+ def bad(message):
33
+ raise ToolError("invalid_options", message)
34
+
35
+ unknown = set(options) - {"description", "variants", "transparent", "palette_colors", "palette", "frame_duration_ms", "seed"}
36
+ if unknown:
37
+ bad(f"unknown options: {', '.join(sorted(unknown))}")
38
+ integer = lambda v: isinstance(v, int) and not isinstance(v, bool) # noqa: E731
39
+ description = options.get("description")
40
+ if not isinstance(description, str) or not 1 <= len(description.strip()) <= 4000:
41
+ bad("description must be 1 to 4,000 characters")
42
+ result = {"description": description.strip(), "variants": options.get("variants", 1), "transparent": options.get("transparent", True),
43
+ "palette_colors": options.get("palette_colors", 32), "frame_duration_ms": options.get("frame_duration_ms", 120)}
44
+ if not integer(result["variants"]) or not 1 <= result["variants"] <= 4:
45
+ bad("variants must be 1 to 4")
46
+ if not isinstance(result["transparent"], bool):
47
+ bad("transparent must be true or false")
48
+ if result["palette_colors"] not in (16, 32, 64) or not integer(result["palette_colors"]):
49
+ bad("palette_colors must be 16, 32 or 64")
50
+ if not integer(result["frame_duration_ms"]) or not 40 <= result["frame_duration_ms"] <= 1000 or result["frame_duration_ms"] % 10:
51
+ bad("frame_duration_ms must be 40 to 1,000 in steps of 10")
52
+ if options.get("palette") is not None:
53
+ if not isinstance(options["palette"], str) or not PALETTE.fullmatch(options["palette"]):
54
+ bad("palette must be 2 to 64 comma-separated #RRGGBB colors without spaces")
55
+ result["palette"] = options["palette"]
56
+ if options.get("seed") is not None:
57
+ if not integer(options["seed"]) or not 0 <= options["seed"] <= 0xFFFFFFFF:
58
+ bad("seed must be an integer from 0 to 4294967295")
59
+ result["seed"] = options["seed"]
60
+ return result
61
+
62
+
63
+ def prepare_output(out):
64
+ out = Path(out)
65
+ if out.exists() and (not out.is_dir() or any(out.iterdir())):
66
+ raise ToolError("output_exists", f"{out} is not an empty folder", "choose a new output folder; results are never overwritten")
67
+ out.mkdir(parents=True, exist_ok=True)
68
+ return out
69
+
70
+
71
+ def load_reference(path):
72
+ """(RGB image at most 1024 px, its SHA-256), checked like the hosted tool's PNG/JPEG input."""
73
+ data = Path(path).read_bytes()
74
+ try:
75
+ with Image.open(Path(path)) as image:
76
+ if image.format not in ("PNG", "JPEG"):
77
+ raise ToolError("invalid_image", "the reference must be a PNG or JPEG")
78
+ if image.width * image.height > MAX_REFERENCE_PIXELS:
79
+ raise ToolError("invalid_image", "the reference must have at most 25 million pixels")
80
+ reference = image.convert("RGB")
81
+ except (OSError, Image.DecompressionBombError) as e:
82
+ raise ToolError("invalid_image", f"cannot read the reference image: {e}") from None
83
+ reference.thumbnail((1024, 1024), Image.Resampling.LANCZOS)
84
+ return reference, hashlib.sha256(data).hexdigest()
85
+
86
+
87
+ def run_local(options, reference=None, out=".", engine=None, offload="auto", progress=stderr):
88
+ """Generate on this machine's GPU. Long-lived callers (the MCP server) pass a loaded `engine` to reuse."""
89
+ options = validate(options)
90
+ seed = job_seed(options.get("seed"))
91
+ image, reference_hash = load_reference(reference) if reference else (None, None)
92
+ out = prepare_output(out)
93
+ if engine is None:
94
+ from .engine import Engine
95
+
96
+ progress("Loading models")
97
+ engine = Engine(offload)
98
+ provenance = {"inference_version": INFERENCE_VERSION, "inference_sha256": inference_hash(bool(reference)),
99
+ "model_revision": COMPONENTS["main"]["revision"], "reference_sha256": reference_hash,
100
+ "source_revisions": [COMPONENTS[c]["revision"] for c in ("lora", "encoder")], "model_hashes": MODEL_HASHES,
101
+ "runtime": f"mcpbytes-pixel-sprite {version('mcpbytes-pixel-sprite')}"}
102
+ stages = []
103
+ for number in range(1, options["variants"] + 1):
104
+ progress(f"Creating sprite {number} of {options['variants']}")
105
+ stage = {**options, "variant": number, "seed": stage_seed(seed, f"sprite_{number}")}
106
+ raw = engine.render(options["description"], image, stage["seed"])
107
+ stages.append(package(raw, out, stage, provenance))
108
+ settings = {k: v for k, v in options.items() if k != "description"} # prompts stay out of generation.json, as in the cloud
109
+ (out / "generation.json").write_text(json.dumps({
110
+ "tool": TOOL, "runtime": "local", "options": {**settings, "seed": seed}, "inference_version": INFERENCE_VERSION,
111
+ "processing_version": PROCESSING_VERSION, "model_revisions": {c: s["revision"] for c, s in COMPONENTS.items()},
112
+ "stages": [{"stage_id": f"sprite_{s['variant']}", **s} for s in stages]}, indent=2, sort_keys=True) + "\n", encoding="utf-8")
113
+ return summarize(out, stages, seed=seed)
114
+
115
+
116
+ def run_cloud(options, reference=None, out=".", cloud=None, idempotency_key=None, progress=stderr):
117
+ """Generate on the hosted API for credits. Reuse `idempotency_key` after a lost response: no second charge."""
118
+ from mcpbytes_core.cloud import Cloud
119
+
120
+ options = validate(options)
121
+ cloud = cloud or Cloud()
122
+ out = prepare_output(out)
123
+ key = idempotency_key or str(uuid.uuid4())
124
+ progress(f"Request key {key}: reuse it with --idempotency-key if this run is interrupted")
125
+ upload_id = cloud.upload(reference) if reference else None
126
+ job = cloud.start(TOOL, options, key, upload_id)
127
+ progress(f"Job {job['id']}")
128
+ job = cloud.wait(job, lambda j: progress((j.get("workflow") or {}).get("stage") or j["status"]))
129
+ cloud.download(job, out)
130
+ stages = [json.loads((out / f"sprite_{n}.json").read_text(encoding="utf-8")) for n in range(1, options["variants"] + 1)]
131
+ return summarize(out, stages, job_id=job["id"], credits_charged=job.get("credits_charged"))
132
+
133
+
134
+ def summarize(out, stages, **extra):
135
+ return {"qa_status": "needs_review" if any(s["qa_status"] == "needs_review" for s in stages) else "unreviewed",
136
+ "output_dir": str(out.resolve()), "files": sorted(p.name for p in out.iterdir()),
137
+ "variants": [{"variant": s["variant"], "sheet": f"sprite_{s['variant']}.png", "archive": f"sprite_{s['variant']}.zip",
138
+ "qa_status": s["qa_status"], "warnings": s["warnings"]} for s in stages],
139
+ "notes": ["Review anatomy, direction, identity and transparency before using these sprites."], **extra}
@@ -0,0 +1,26 @@
1
+ """Versioned prompt/layout and fixed direct-pipeline settings, not caller-selected models."""
2
+
3
+ import hashlib
4
+ import json
5
+
6
+ VERSION = "sprite_diffusers_v1"
7
+ LAYOUT_PROMPT = (
8
+ "Four rows, four equal cells per row. Row one: three walking poses facing down, then arms raised. "
9
+ "Row two: three walking poses facing left, then a jumping-left still pose. "
10
+ "Row three: three walking poses facing right, then a jumping-right still pose. "
11
+ "Row four: three walking poses facing up, then a lying still pose. "
12
+ "One complete character in every cell, consistent identity and clothing. Leave space around every silhouette. "
13
+ "Plain solid magenta background (#ff00ff), no checkerboard, no lettering, no grid lines, no shadows. "
14
+ "Do not use magenta in the character."
15
+ )
16
+
17
+
18
+ def request_options(description, reference=None):
19
+ lead = "Pixel art spritesheet of the character in the reference. " if reference is not None else "Pixel art character spritesheet. "
20
+ return {"prompt": lead + description + ". " + LAYOUT_PROMPT, "image": reference,
21
+ "width": 512, "height": 512, "num_inference_steps": 20, "guidance_scale": 5.0, "num_images_per_prompt": 1}
22
+
23
+
24
+ def inference_hash(reference=False):
25
+ settings = request_options("", "reference" if reference else None)
26
+ return hashlib.sha256(json.dumps({"version": VERSION, **settings}, sort_keys=True).encode()).hexdigest()
@@ -0,0 +1,54 @@
1
+ """The exact Hugging Face files the hosted tool was qualified with (all Apache-2.0, ungated): about 16 GB.
2
+
3
+ Revisions are commits, never branches. Sizes and SHA-256s were read from the Hub for these commits.
4
+ """
5
+
6
+ COMPONENTS = {
7
+ "main": {
8
+ "repository": "black-forest-labs/FLUX.2-klein-base-4B",
9
+ "revision": "a3b4f4849157f664bdbc776fd7453c2783562f4d",
10
+ "files": {
11
+ "flux-2-klein-base-4b.safetensors": 7751105712,
12
+ "vae/config.json": 821,
13
+ "vae/diffusion_pytorch_model.safetensors": 168120878,
14
+ "LICENSE.md": 9584,
15
+ "README.md": 10084,
16
+ "model_index.json": 422,
17
+ "transformer/config.json": 531,
18
+ "text_encoder/config.json": 1536,
19
+ "text_encoder/generation_config.json": 214,
20
+ "scheduler/scheduler_config.json": 486,
21
+ "tokenizer/added_tokens.json": 707,
22
+ "tokenizer/chat_template.jinja": 4168,
23
+ "tokenizer/merges.txt": 1671853,
24
+ "tokenizer/special_tokens_map.json": 613,
25
+ "tokenizer/tokenizer.json": 11422654,
26
+ "tokenizer/tokenizer_config.json": 5404,
27
+ "tokenizer/vocab.json": 2776833,
28
+ },
29
+ },
30
+ "lora": {
31
+ "repository": "svntax-dev/pixel_spritesheet_4walk_small_lora_v1",
32
+ "revision": "cb8bffe5b9b92e656e31e7f940ec4a38047d6a88",
33
+ "files": {"pixel_4walk_small_flux2_klein_base_4b_v1.safetensors": 92426736, "README.md": 6181},
34
+ },
35
+ "encoder": {
36
+ "repository": "Comfy-Org/z_image_turbo",
37
+ "revision": "6fc90a3b1b653e935a0d175e260736de25b84df5",
38
+ "files": {"split_files/text_encoders/qwen_3_4b.safetensors": 8044982048, "README.md": 4785},
39
+ },
40
+ }
41
+
42
+ TRANSFORMER = "flux-2-klein-base-4b.safetensors"
43
+ LORA = "pixel_4walk_small_flux2_klein_base_4b_v1.safetensors"
44
+ ENCODER = "split_files/text_encoders/qwen_3_4b.safetensors"
45
+
46
+ # Weight hashes, recorded in each export's provenance.json as the hosted tool does.
47
+ MODEL_HASHES = sorted([
48
+ "9c5fed22b76baea749d88fc2abe3ad53245e7b21a0d353a762665eea00043b92", # main transformer
49
+ "ca70d2202afe6415bdbcb8793ba8cd99fd159cfe6192381504d6c4d3036e0f04", # main VAE
50
+ "a968c042c2443bf6bfed41ce833a0e06bf5bbb7961ac40b88fb7257a940ae54e", # LoRA
51
+ "6c671498573ac2f7a5501502ccce8d2b08ea6ca2f661c458e708f36b36edfc5a", # Qwen3 encoder
52
+ ])
53
+
54
+ DOWNLOAD_COMMAND = "mcpbytes-pixel-sprite download"