From 60b2d14e0430dd7006b6020bf479c51e6747b212 Mon Sep 17 00:00:00 2001 From: MehranMarxian Date: Mon, 9 Nov 2026 23:41:00 +0330 Subject: [PATCH] Add NitroFusion2 as a one-step Text to Image preset NitroFusion2 (ChenDY/NitroFusion2, released 2026-11-03) is an adversarially distilled fine-tune of Z_image_Turbo that renders in one step. It is Apache-2.0 and ungated, uses only core ComfyUI nodes, and shares Z_image_Turbo's qwen_3_4b encoder and ae VAE, so a user who already runs Z_image_Turbo downloads only the 12.3 GB model. txt2img-nitrofusion2 is the txt2img-z-image-turbo graph with three values changed, matching the authors' own one-step workflow: nf2-bf16.safetensors, 1 step, euler. It keeps that graph's node ids, so it reuses its injection map. It is its own preset rather than a file picked in Z_image_Turbo because running NitroFusion2 at Z_image_Turbo's 4 steps with res_multistep adds white speckle (measured on six prompts on 2026-11-09). On a 12 GB 4070 Ti at 1024 x 1024 it takes about 2.5 s against Z_image_Turbo's 5 s when only the seed changes, and 14 s against 17.5 s for a new prompt: the 8 GB encoder and the 12.3 GB model do not fit on the card together, so ComfyUI swaps them for every new prompt. The registry note records this so nobody advertises it as four times faster. nf2-* files were detected as an Unknown model family, which would have made the preset warn about its own model. They now read as Z_image_Turbo, matched as a file-name prefix so names that merely contain "nf2" are not claimed. Not wired up: the authors' two-step graph (its de-distillation LoRA is on for the first step only, which OpenLayer's LoRA insertion cannot express), LoRAs in general (untried on the one-step weights), and the GGUF files. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- README.md | 3 +- docs/index.html | 2 +- docs/llms.txt | 2 +- docs/model-guide.md | 1 + src/comfy/modelCompatibility.ts | 9 ++ src/comfy/presetRegistry.ts | 127 ++++++++++++++++++ src/comfy/types.ts | 1 + src/comfy/workflowBuilder.ts | 2 + src/workflows/api/txt2img-nitrofusion2.json | 141 ++++++++++++++++++++ tests/comfy/modelCompatibility.test.ts | 12 ++ tests/comfy/modelFolders.test.ts | 1 + tests/comfy/presetRegistry.test.ts | 36 ++++- tests/comfy/workflowBuilder.test.ts | 44 ++++++ 14 files changed, 377 insertions(+), 6 deletions(-) create mode 100644 src/workflows/api/txt2img-nitrofusion2.json 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",