diff --git a/CLAUDE.md b/CLAUDE.md index 597604d..2dbe622 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,7 +54,7 @@ Load `dist/` in Photoshop with UXP Developer Tool (Add Plugin → `dist/manifest Don't grow it: new tools go in `src/ui/tools/.ts` (see `livePainting.ts`, `layerTools.ts`). - `src/ui/generationController.ts`: the only way to run a generation (submit → WS progress → poll `/history` → fetch `/view` → commit). Exactly one active run; stale runs cannot commit. -- `src/comfy/presetRegistry.ts`: source of truth for every preset (39): API workflow file, node +- `src/comfy/presetRegistry.ts`: source of truth for every preset (40): API workflow file, node ids + `class_type` + required inputs, injection targets, model loader, required models with download URL/size/licence gate. Setup screen, health checks, setup pack and hardware advisor derive from it (Live Painting graphs are the exception). diff --git a/README.md b/README.md index 195caf1..4fd8833 100644 --- a/README.md +++ b/README.md @@ -351,13 +351,14 @@ Almost every preset is one of these. Install a stack once and every tool that us costs 12.3 GB, not 20.7 GB. `ae.safetensors` is shared by Z_image_Turbo and Flux Fill.
-Text to Image — Klein, Z-Image, Krea-2, or any checkpoint you own +Text to Image — Klein, Z-Image, NitroFusion2, Krea-2, or any checkpoint you own | Preset | Needs | Extra | | :--- | :--- | :--- | | Standard checkpoint | any SD 1.x / SDXL checkpoint you already have | — | | FLUX.2 Klein | **Klein stack** | — | | Z_image_Turbo | **Z_image_Turbo stack** | — | +| NitroFusion2 | `nf2-bf16.safetensors` → `models/diffusion_models/` + the **Z_image_Turbo stack**'s `qwen_3_4b` and `ae` | +12.3 GB · Apache-2.0 · one step, a Z_image_Turbo fine-tune | | Krea-2 Turbo | **Krea-2 Turbo stack** | — | | Qwen-Image 2.1 | **Qwen-Image 2.1 stack** | research licence · the one preset with **Transparent Background** · native 2K | | Flux1-dev fp8 | `flux1-dev-fp8.safetensors` → `models/checkpoints/` (17.3 GB) | non-commercial licence | diff --git a/docs/index.html b/docs/index.html index a4e317a..8046127 100644 --- a/docs/index.html +++ b/docs/index.html @@ -119,7 +119,7 @@

What works now

Text to Image

Stable
-

Prompt, negative prompt, size, steps, CFG, and seed into your checkpoint of choice — SD 1.x, SDXL, FLUX.2 Klein, Z-Image-Turbo, Krea-2, and Qwen-Image 2.1, which can hand back a transparent cut-out layer and letters spelled exactly.

+

Prompt, negative prompt, size, steps, CFG, and seed into your checkpoint of choice — SD 1.x, SDXL, FLUX.2 Klein, Z-Image-Turbo, one-step NitroFusion2, Krea-2, and Qwen-Image 2.1, which can hand back a transparent cut-out layer and letters spelled exactly.

diff --git a/docs/llms.txt b/docs/llms.txt index 521d33b..c1653f9 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -33,7 +33,7 @@ First run: start with FLUX.2 Klein 4B (Apache-2.0, three files, 12.5 GB, no sign ## Supported models -FLUX.2 Klein 4B (Apache-2.0), SeedVR2 3B for generative upscale (Apache-2.0), Qwen-Image 2.1, Qwen-Image-Layered, Krea-2 Turbo, Z-Image Turbo, Flux Fill, and Stable Diffusion 1.5 checkpoints with your own LoRAs. Qwen-Image 2.1 weights are licensed for research and evaluation only, and Flux Fill is non-commercial, so those presets are opt-in and labelled. Check each model's licence before commercial use. +FLUX.2 Klein 4B (Apache-2.0), SeedVR2 3B for generative upscale (Apache-2.0), Qwen-Image 2.1, Qwen-Image-Layered, Krea-2 Turbo, Z-Image Turbo, NitroFusion2 (Apache-2.0, a one-step Z-Image Turbo fine-tune), Flux Fill, and Stable Diffusion 1.5 checkpoints with your own LoRAs. Qwen-Image 2.1 weights are licensed for research and evaluation only, and Flux Fill is non-commercial, so those presets are opt-in and labelled. Check each model's licence before commercial use. ## Links diff --git a/docs/model-guide.md b/docs/model-guide.md index 3205aec..c0e2bc2 100644 --- a/docs/model-guide.md +++ b/docs/model-guide.md @@ -37,6 +37,7 @@ These are practical starter guidelines, not strict rules. | Inpaint / Flux Fill | Flux Fill Dev | Experimental `inpaint-flux-fill-basic`; not production-ready. `inpaint-flux-fill-cropstitch` uses the same files and adds the `comfyui-inpaint-cropandstitch` node pack. | | Outpaint / Canvas Expansion | Flux Fill Dev | Experimental `outpaint-flux-fill-basic` using `ImagePadForOutpaint`. | | Z_image_Turbo Text/Image | Z_image_Turbo | Experimental dedicated presets in v0.4.3-alpha. | +| NitroFusion2 Text to Image | NitroFusion2 (a Z_image_Turbo fine-tune) | `txt2img-nitrofusion2`: 1 step, CFG 1, 1024 × 1024. Needs `nf2-bf16.safetensors` (12.3 GB, Apache-2.0, ungated) plus Z_image_Turbo's `qwen_3_4b` and `ae`. Keep steps at 1 or 2; at 4 it speckles. | | Prompt from Layer | Florence-2 PromptGen | Available for alpha testing through `prompt-from-layer-florence2` for describing captured Photoshop sources. | | Future Realtime Preview | To be tested | Likely needs a very fast dedicated workflow. | diff --git a/src/comfy/modelCompatibility.ts b/src/comfy/modelCompatibility.ts index 7d53b61..3a4a934 100644 --- a/src/comfy/modelCompatibility.ts +++ b/src/comfy/modelCompatibility.ts @@ -30,6 +30,15 @@ export function detectCheckpointFamily(checkpointName: string): ModelFamily { return "zImage"; } + // NitroFusion2 is a Z_image_Turbo fine-tune whose files are named nf2-*. + // Matched as a prefix rather than a substring: "nf2" is short enough to turn + // up inside unrelated names. ComfyUI lists files in subfolders as + // "folder/name", so the prefix is checked on the file name alone. + const fileName = normalized.slice(Math.max(normalized.lastIndexOf("/"), normalized.lastIndexOf("\\")) + 1); + if (fileName.startsWith("nf2-") || fileName.startsWith("nf2_") || normalized.includes("nitrofusion2")) { + return "zImage"; + } + // Must precede the plain "flux" test, which would otherwise swallow every // Flux.2 filename by substring. The two are not interchangeable: Flux.1 runs // through a plain KSampler, while Flux.2 needs the advanced sampler chain and diff --git a/src/comfy/presetRegistry.ts b/src/comfy/presetRegistry.ts index 69d6346..0408de0 100644 --- a/src/comfy/presetRegistry.ts +++ b/src/comfy/presetRegistry.ts @@ -59,6 +59,10 @@ const COMFY_ORG_FLUX1_DEV_REPO = "https://huggingface.co/Comfy-Org/flux1-dev"; const COMFY_ORG_FLUX2_DEV_REPO = "https://huggingface.co/Comfy-Org/flux2-dev"; const CITY96_FLUX2_DEV_GGUF_REPO = "https://huggingface.co/city96/FLUX.2-dev-gguf"; const COMFY_ORG_Z_IMAGE_TURBO_REPO = "https://huggingface.co/Comfy-Org/z_image_turbo"; +// NitroFusion2 is an Apache-2.0, ungated fine-tune of Z_image_Turbo, published by +// its authors rather than repackaged by Comfy-Org, so the model URL points at +// their repo. No licenseGate, for the same reason Klein has none. +const CHENDY_NITROFUSION2_REPO = "https://huggingface.co/ChenDY/NitroFusion2"; // Klein ships Apache-2.0 and ungated, unlike FLUX.1-dev and FLUX.2-dev, so // there is deliberately no licenseGate on the stack below -- the setup pack can // point straight at the file with no click-through. @@ -386,6 +390,32 @@ const Z_IMAGE_TURBO_STACK = [ } ] as const; +/** + * NitroFusion2 swaps only the diffusion model: it is Z_image_Turbo's own + * architecture (the bf16 file is byte-for-byte the same size) with + * adversarially distilled weights, so it reads the same qwen_3_4b encoder + * through CLIPLoader type lumina2 and decodes through the same ae VAE. The two + * shared entries are taken from Z_IMAGE_TURBO_STACK itself rather than copied, + * so the setup pack de-duplicates them and a user who already runs Z_image_Turbo + * downloads only the 12.3 GB model. + */ +const NITROFUSION2_STACK = [ + { + kind: "diffusion-model-stack", + objectInfoNode: "UNETLoader", + inputName: "unet_name", + label: "NitroFusion2 diffusion model", + modelName: "nf2-bf16.safetensors", + setupHint: "Install nf2-bf16.safetensors where ComfyUI's UNETLoader can find it.", + downloadUrl: `${CHENDY_NITROFUSION2_REPO}/resolve/main/nf2-bf16.safetensors`, + sourcePageUrl: CHENDY_NITROFUSION2_REPO, + downloadSizeBytes: 12309866400, + sha256: "a1e11422d84c1986ba106f892eda86b955b752627ec902aefff1fb939c7ac19c" + }, + Z_IMAGE_TURBO_STACK[1], + Z_IMAGE_TURBO_STACK[2] +] as const; + /** * ControlNet-as-model-patch weights for the Z_image_Turbo stack, applied * through the core `ModelPatchLoader` / `ZImageFunControlnet` pair rather than @@ -2589,6 +2619,30 @@ const Z_IMAGE_TURBO_TXT2IMG_CAPABILITY: WorkflowCapability = { } }; +const NITROFUSION2_TXT2IMG_CAPABILITY: WorkflowCapability = { + toolType: "txt2img", + loaderType: "diffusion-model-stack", + artistLabel: "Text to Image", + technicalLabel: "txt2img-nitrofusion2", + requiredPhotoshopInputs: [], + controls: ["prompt", "negativePrompt", "width", "height", "steps", "guidance", "seed"], + output: { + kind: "full-image", + size: "preset", + importBehavior: "new-layer" + }, + uiHints: { + showModelSelector: true, + modelSelectorLabel: "NitroFusion2 model", + primaryActionLabel: "Generate", + // CFG 1 means the sampler never reads the negative prompt, so a field for + // it would only look like it does something. + hiddenControls: ["negativePrompt"], + experimentalNote: + "NitroFusion2 is a one-step version of Z_image_Turbo: 1 step at CFG 1, at 1024 x 1024. Keep steps at 1 or 2. More steps do not improve it; at 4 it starts to speckle. It is fastest when you re-roll the same prompt with a new seed, because ComfyUI keeps the prompt encoded." + } +}; + const Z_IMAGE_TURBO_IMG2IMG_CAPABILITY: WorkflowCapability = { toolType: "img2img", loaderType: "diffusion-model-stack", @@ -5197,6 +5251,79 @@ export const WORKFLOW_PRESETS: WorkflowPresetDefinition[] = [ compatibilityNote: "Z_image_Turbo is a diffusion model stack, not a checkpoint. OpenLayer loads it through UNETLoader, CLIPLoader, and VAELoader." }, + { + id: "txt2img-nitrofusion2", + label: "txt2img-nitrofusion2", + displayName: "NitroFusion2", + mode: "txt2img", + description: "One-step text-to-image preset for NitroFusion2, a distilled fine-tune of Z_image_Turbo.", + workflowFile: "workflows/api/txt2img-nitrofusion2.json", + status: "stable", + recommendedSettings: { steps: 1, cfg: 1, width: 1024, height: 1024 }, + supportedModelFamilies: ["zImage"], + experimentalModelFamilies: ["unknown"], + modelSource: DIFFUSION_MODEL_SOURCE, + capability: NITROFUSION2_TXT2IMG_CAPABILITY, + modelStack: [...NITROFUSION2_STACK], + requiredModels: [...NITROFUSION2_STACK], + // The graph keeps the Z_image_Turbo graph's node ids, so its injection map + // is the same one; only the model file and sampler settings differ. + injections: Z_IMAGE_TURBO_TXT2IMG_INJECTIONS, + requiredNodes: [ + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.diffusionModelLoader, + classType: "UNETLoader", + requiredInputs: ["unet_name", "weight_dtype"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.clipLoader, + classType: "CLIPLoader", + requiredInputs: ["clip_name", "type"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.vaeLoader, + classType: "VAELoader", + requiredInputs: ["vae_name"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.modelSampling, + classType: "ModelSamplingAuraFlow", + requiredInputs: ["model", "shift"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.positivePrompt, + classType: "CLIPTextEncode", + requiredInputs: ["text", "clip"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.negativePrompt, + classType: "CLIPTextEncode", + requiredInputs: ["text", "clip"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.latentImage, + classType: "EmptySD3LatentImage", + requiredInputs: ["width", "height", "batch_size"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.sampler, + classType: "KSampler", + requiredInputs: ["model", "seed", "steps", "cfg", "sampler_name", "scheduler", "positive", "negative", "latent_image", "denoise"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.decode, + classType: "VAEDecode", + requiredInputs: ["samples", "vae"] + }, + { + id: Z_IMAGE_TURBO_TXT2IMG_NODES.saveImage, + classType: "SaveImage", + requiredInputs: ["images"] + } + ], + compatibilityNote: + "txt2img-nitrofusion2 is the txt2img-z-image-turbo graph with three values changed, matching the authors' own one-step ComfyUI workflow: nf2-bf16.safetensors in UNETLoader, 1 step instead of 4, and euler instead of res_multistep, with the same ModelSamplingAuraFlow shift 3, CFG 1 and simple scheduler. Those values are the point of the preset rather than a tuning choice. On 2026-11-09, running NitroFusion2 through the Z_image_Turbo preset's own settings (4 steps, res_multistep) on six prompts added white splatter and speckle that the one-step run did not have, which is why the model is its own preset and not just a file picked in Z_image_Turbo. Measured on the same day on a 12 GB RTX 4070 Ti at 1024 x 1024: about 2.5 s against Z_image_Turbo's 5 s when only the seed changes, but 14 s against 17.5 s for a new prompt, because the 8 GB encoder and the 12.3 GB model do not fit on the card together and ComfyUI swaps them for every new prompt. The authors' two-step graph is not used: it needs their de-distillation LoRA switched on for the first step only, across two KSamplerAdvanced nodes, which OpenLayer's LoRA insertion cannot express. It declares no loraInsertion either: no Z_image_Turbo LoRA has been tried on the one-step weights, and the Z_image_Turbo preset stays the place for LoRAs until one has. GGUF quantisations exist but need the ComfyUI-GGUF loader and are not wired up here." + }, { id: "img2img-z-image-turbo", label: "img2img-z-image-turbo", diff --git a/src/comfy/types.ts b/src/comfy/types.ts index 8995db7..34f976c 100644 --- a/src/comfy/types.ts +++ b/src/comfy/types.ts @@ -7,6 +7,7 @@ export type WorkflowPreset = | "img2img-flux2-klein" | "edit-flux2-klein" | "txt2img-z-image-turbo" + | "txt2img-nitrofusion2" | "img2img-z-image-turbo" | "txt2img-krea2-turbo" | "img2img-krea2-turbo" diff --git a/src/comfy/workflowBuilder.ts b/src/comfy/workflowBuilder.ts index 68e7db7..88fb89a 100644 --- a/src/comfy/workflowBuilder.ts +++ b/src/comfy/workflowBuilder.ts @@ -3,6 +3,7 @@ import img2imgBasicWorkflow from "../workflows/api/img2img-basic.json"; import txt2imgFlux1DevFp8Workflow from "../workflows/api/txt2img-flux1-dev-fp8.json"; import txt2imgFlux2DevGgufWorkflow from "../workflows/api/txt2img-flux2-dev-gguf.json"; import txt2imgZImageTurboWorkflow from "../workflows/api/txt2img-z-image-turbo.json"; +import txt2imgNitroFusion2Workflow from "../workflows/api/txt2img-nitrofusion2.json"; import img2imgZImageTurboWorkflow from "../workflows/api/img2img-z-image-turbo.json"; import txt2imgKrea2TurboWorkflow from "../workflows/api/txt2img-krea2-turbo.json"; import img2imgKrea2TurboWorkflow from "../workflows/api/img2img-krea2-turbo.json"; @@ -73,6 +74,7 @@ const WORKFLOW_TEMPLATES: Partial> = { "txt2img-flux1-dev-fp8": txt2imgFlux1DevFp8Workflow as ComfyWorkflow, "txt2img-flux2-dev-gguf": txt2imgFlux2DevGgufWorkflow as ComfyWorkflow, "txt2img-z-image-turbo": txt2imgZImageTurboWorkflow as ComfyWorkflow, + "txt2img-nitrofusion2": txt2imgNitroFusion2Workflow as ComfyWorkflow, "img2img-z-image-turbo": img2imgZImageTurboWorkflow as ComfyWorkflow, "txt2img-krea2-turbo": txt2imgKrea2TurboWorkflow as ComfyWorkflow, "img2img-krea2-turbo": img2imgKrea2TurboWorkflow as ComfyWorkflow, diff --git a/src/workflows/api/txt2img-nitrofusion2.json b/src/workflows/api/txt2img-nitrofusion2.json new file mode 100644 index 0000000..549b497 --- /dev/null +++ b/src/workflows/api/txt2img-nitrofusion2.json @@ -0,0 +1,141 @@ +{ + "20": { + "class_type": "UNETLoader", + "inputs": { + "unet_name": "nf2-bf16.safetensors", + "weight_dtype": "default" + }, + "_meta": { + "title": "Load NitroFusion2 Diffusion Model" + } + }, + "21": { + "class_type": "CLIPLoader", + "inputs": { + "clip_name": "qwen_3_4b.safetensors", + "type": "lumina2", + "device": "default" + }, + "_meta": { + "title": "Load Qwen CLIP" + } + }, + "22": { + "class_type": "VAELoader", + "inputs": { + "vae_name": "ae.safetensors" + }, + "_meta": { + "title": "Load AE VAE" + } + }, + "23": { + "class_type": "ModelSamplingAuraFlow", + "inputs": { + "model": [ + "20", + 0 + ], + "shift": 3 + }, + "_meta": { + "title": "NitroFusion2 Sampling" + } + }, + "5": { + "class_type": "EmptySD3LatentImage", + "inputs": { + "width": 1024, + "height": 1024, + "batch_size": 1 + }, + "_meta": { + "title": "Image Size" + } + }, + "6": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "", + "clip": [ + "21", + 0 + ] + }, + "_meta": { + "title": "Positive Prompt" + } + }, + "7": { + "class_type": "CLIPTextEncode", + "inputs": { + "text": "", + "clip": [ + "21", + 0 + ] + }, + "_meta": { + "title": "Negative Prompt" + } + }, + "3": { + "class_type": "KSampler", + "inputs": { + "seed": 1, + "steps": 1, + "cfg": 1, + "sampler_name": "euler", + "scheduler": "simple", + "denoise": 1, + "model": [ + "23", + 0 + ], + "positive": [ + "6", + 0 + ], + "negative": [ + "7", + 0 + ], + "latent_image": [ + "5", + 0 + ] + }, + "_meta": { + "title": "Sampler" + } + }, + "8": { + "class_type": "VAEDecode", + "inputs": { + "samples": [ + "3", + 0 + ], + "vae": [ + "22", + 0 + ] + }, + "_meta": { + "title": "Decode" + } + }, + "9": { + "class_type": "SaveImage", + "inputs": { + "filename_prefix": "OpenLayer_NitroFusion2", + "images": [ + "8", + 0 + ] + }, + "_meta": { + "title": "Save Image" + } + } +} diff --git a/tests/comfy/modelCompatibility.test.ts b/tests/comfy/modelCompatibility.test.ts index b82eb4a..26ed594 100644 --- a/tests/comfy/modelCompatibility.test.ts +++ b/tests/comfy/modelCompatibility.test.ts @@ -11,6 +11,18 @@ describe("model compatibility", () => { expect(detectCheckpointFamily("z_image_turbo_bf16.safetensors")).toBe("zImage"); }); + it("reads NitroFusion2's nf2 files as Z_image_Turbo, without claiming names that merely contain nf2", () => { + // NitroFusion2 is a Z_image_Turbo fine-tune; its files carry no "z_image". + // Read as Unknown, the NitroFusion2 preset would warn about its own model. + expect(detectCheckpointFamily("nf2-bf16.safetensors")).toBe("zImage"); + expect(detectCheckpointFamily("nf2-Q8_0.gguf")).toBe("zImage"); + expect(detectCheckpointFamily("NitroFusion2/nf2-bf16.safetensors")).toBe("zImage"); + expect(detectCheckpointFamily("NitroFusion2-int8.safetensors")).toBe("zImage"); + + expect(detectCheckpointFamily("conf2-mix.safetensors")).toBe("unknown"); + expect(detectCheckpointFamily("lnf2.safetensors")).toBe("unknown"); + }); + it("keeps Flux.2 apart from Flux.1, whatever the filename spells it", () => { // Every one of these contains "flux", so the plain Flux test would claim // all of them without the more specific check running first. The families diff --git a/tests/comfy/modelFolders.test.ts b/tests/comfy/modelFolders.test.ts index 084455c..1f35164 100644 --- a/tests/comfy/modelFolders.test.ts +++ b/tests/comfy/modelFolders.test.ts @@ -156,6 +156,7 @@ describe("required model inventory", () => { "diffusion_models/flux-2-klein-4b-fp8.safetensors", "diffusion_models/flux2-dev-Q4_K_M.gguf", "diffusion_models/krea2_turbo_fp8_scaled.safetensors", + "diffusion_models/nf2-bf16.safetensors", "diffusion_models/qwen_image_2.1_int8_convrot.safetensors", "diffusion_models/qwen_image_edit_2511_int8_convrot.safetensors", "diffusion_models/qwen_image_layered_fp8mixed.safetensors", diff --git a/tests/comfy/presetRegistry.test.ts b/tests/comfy/presetRegistry.test.ts index 0aef7e6..b6921ed 100644 --- a/tests/comfy/presetRegistry.test.ts +++ b/tests/comfy/presetRegistry.test.ts @@ -36,6 +36,7 @@ describe("presetRegistry", () => { "txt2img-flux1-dev-fp8", "txt2img-flux2-klein", "txt2img-z-image-turbo", + "txt2img-nitrofusion2", "txt2img-krea2-turbo", "txt2img-flux2-dev-gguf", "txt2img-qwen-image-21" @@ -412,10 +413,41 @@ describe("presetRegistry", () => { } }); - it("gives FLUX.2 Klein text-to-image its 1024 size, and no other preset a size", () => { + it("registers NitroFusion2 on Z_image_Turbo's encoder and VAE, so only the model is a new download", () => { + const nf2 = getWorkflowPreset("txt2img-nitrofusion2"); + const zImage = getWorkflowPreset("txt2img-z-image-turbo"); + + expect(nf2.mode).toBe("txt2img"); + expect(nf2.modelSource.kind).toBe("diffusion-model-stack"); + expect(nf2.supportedModelFamilies).toEqual(["zImage"]); + expect(nf2.requiredModels?.map((model) => model.modelName)).toEqual([ + "nf2-bf16.safetensors", + "qwen_3_4b.safetensors", + "ae.safetensors" + ]); + // The same entries, not lookalikes, so the setup pack counts them once. + expect(nf2.requiredModels?.slice(1)).toEqual(zImage.requiredModels?.slice(1)); + + const model = nf2.requiredModels?.[0]; + // Size and sha256 as published by ChenDY/NitroFusion2 and hashed locally + // from the downloaded file on 2026-11-09. + expect(model?.downloadUrl).toBe("https://huggingface.co/ChenDY/NitroFusion2/resolve/main/nf2-bf16.safetensors"); + expect(model?.downloadSizeBytes).toBe(12309866400); + expect(model?.sha256).toBe("a1e11422d84c1986ba106f892eda86b955b752627ec902aefff1fb939c7ac19c"); + // Apache-2.0 and ungated. + expect(model?.licenseGate).toBeUndefined(); + // The one-step weights have never been tried with a Z_image_Turbo LoRA. + expect(nf2.loraInsertion).toBeUndefined(); + expect(nf2.capability?.uiHints?.hiddenControls).toEqual(["negativePrompt"]); + }); + + it("gives FLUX.2 Klein and NitroFusion2 text-to-image their 1024 size, and no other preset a size", () => { expect(getRecommendedPresetSettings("txt2img-flux2-klein")).toEqual({ steps: 4, cfg: 1, width: 1024, height: 1024 }); + // One step is the model's operating point; at 4 it speckles (measured 2026-11-09). + expect(getRecommendedPresetSettings("txt2img-nitrofusion2")).toEqual({ steps: 1, cfg: 1, width: 1024, height: 1024 }); - for (const preset of listRunnableWorkflowPresets().filter((candidate) => candidate.id !== "txt2img-flux2-klein")) { + const sized = new Set(["txt2img-flux2-klein", "txt2img-nitrofusion2"]); + for (const preset of listRunnableWorkflowPresets().filter((candidate) => !sized.has(candidate.id))) { const recommended = getRecommendedPresetSettings(preset.id); expect(recommended.width, preset.id).toBeUndefined(); diff --git a/tests/comfy/workflowBuilder.test.ts b/tests/comfy/workflowBuilder.test.ts index af40b3f..2f51d4f 100644 --- a/tests/comfy/workflowBuilder.test.ts +++ b/tests/comfy/workflowBuilder.test.ts @@ -1044,6 +1044,50 @@ describe("workflowBuilder", () => { expect(result.workflow["3"].inputs.seed).toBe(77); }); + it("builds NitroFusion2 as the Z_image_Turbo graph with only its model and one-step sampler changed", async () => { + const result = await buildTxt2ImgWorkflow({ + presetId: "txt2img-nitrofusion2", + prompt: "a shop sign that says FRESH BREAD", + negativePrompt: "", + checkpointName: "nf2-bf16.safetensors", + width: 1024, + height: 1024, + steps: 1, + cfg: 1, + seed: 1005 + }); + const zImage = await buildTxt2ImgWorkflow({ + presetId: "txt2img-z-image-turbo", + prompt: "a shop sign that says FRESH BREAD", + negativePrompt: "", + checkpointName: "z_image_turbo_bf16.safetensors", + width: 1024, + height: 1024, + steps: 1, + cfg: 1, + seed: 1005 + }); + + expect(result.preset.id).toBe("txt2img-nitrofusion2"); + expect(result.workflow["20"].inputs.unet_name).toBe("nf2-bf16.safetensors"); + expect(result.workflow["6"].inputs.text).toBe("a shop sign that says FRESH BREAD"); + expect(result.workflow["3"].inputs.steps).toBe(1); + expect(result.workflow["3"].inputs.seed).toBe(1005); + // The authors' one-step operating point. res_multistep, which the Z-Image + // graph uses, is what speckled when NitroFusion2 was run through it. + expect(result.workflow["3"].inputs.sampler_name).toBe("euler"); + expect(result.workflow["3"].inputs.scheduler).toBe("simple"); + expect(result.workflow["23"].inputs.shift).toBe(3); + // Same encoder contract as Z_image_Turbo: lumina2, not Klein's flux2. + expect(result.workflow["21"].inputs).toEqual(zImage.workflow["21"].inputs); + expect(result.workflow["22"].inputs).toEqual(zImage.workflow["22"].inputs); + + // Everything else is the Z-Image graph: same nodes, same wiring. + const shape = (workflow: ComfyWorkflow) => + Object.entries(workflow).map(([id, node]) => `${id}:${node.class_type}`).sort(); + expect(shape(result.workflow)).toEqual(shape(zImage.workflow)); + }); + it("injects Z_image_Turbo image-to-image source and denoise", async () => { const result = await buildImg2ImgWorkflow({ presetId: "img2img-z-image-turbo",