Просмотр исходного кода

fix: take the sampler and scheduler lists from the schema, not the prose

dpm++2m_sde was missing from the node, and it exists: the server's own WebUI
offers it. Two lists disagreed and I had picked the wrong one.

/options/generation carries a values map with nice labels, and it has drifted
from the build - 15 samplers spelled dpmpp2m where the server accepts 21
spelled dpm++2m, and 11 schedulers against 17. The hand-written list this
replaced had the right spellings. The OpenAPI request schema is generated
from the running build and had them all, so it now decides what the values
are; the values map is consulted only for wording, and falls back to the bare
name where it calls something by a name that does not exist.

This was worse than a short dropdown. The server answers 202 to any sampler
name at all - including "not_a_sampler_at_all", which I sent, and which
completed - and silently falls back to a default. So every generation using a
name from the drifted list was producing an image with some other sampler,
with nothing anywhere to say so.

The generated nodes now also carry KNOWN_CHOICES, the list the server said it
accepts, so a value that is not among them can be reported rather than
quietly swapped.

Now offered: dpm++2m_sde, dpm++2m_sde_bt, euler_cfg_pp, euler_a_cfg_pp,
euler_ge, lms, and the schedulers flux, flux2, beta, normal, logit_normal,
ltx2. Verified end to end: a generation with sampler dpm++2m_sde and
scheduler karras queued, ran and produced an image.

The two saved workflows use euler and smoothstep, which are spelled the same
in both lists, so neither was affected.

Descriptions come across with the server's em-dashes turned into hyphens, to
match the house style everywhere else in the editor.

66 passed.
fszontagh 1 месяц назад
Родитель
Сommit
1577e5471b

+ 27 - 19
nodes/sdcpp/sdcpp-edit.js

@@ -171,17 +171,17 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password',
             description: 'A basic credential holding the sdcpp-restapi username and password',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
         },
-        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count — sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
-        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache — single threshold, simple", "Spectrum — frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
-        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass — twice as fast). Recommended: SD1.5: 7. SDXL: 4–8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
+        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count - sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
+        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache - single threshold, simple", "Spectrum - frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
+        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass - twice as fast). Recommended: SD1.5: 7. SDXL: 4-8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
-        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded — Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
-        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0–1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6–0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
-        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels — niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
+        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded - Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
+        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0-1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6-0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
+        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels - niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
-        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05–0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
+        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05-0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
@@ -190,34 +190,34 @@ const configSchema = {
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         maskImageBase64: { title: "Mask Image (base64)", description: "Base64-encoded inpainting mask. White areas are repainted, black areas preserved. Recommended: Optional. Empty mask = standard img2img (no inpainting). Leave empty for the architecture default.", type: "string" },
         maskImageBase64: { title: "Mask Image (base64)", description: "Base64-encoded inpainting mask. White areas are repainted, black areas preserved. Recommended: Optional. Empty mask = standard img2img (no inpainting). Leave empty for the architecture default.", type: "string" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags — these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
+        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"], enumLabels: ["(architecture default)", "DDIM trailing — required for some fine-tunes", "DPM2 — 2nd-order, balanced", "DPM++ 2M — fast, good quality", "DPM++ 2M v2 — improved schedule", "DPM++ 2S ancestral — popular for SDXL", "ER SDE — SDE sampler (added recently)", "Euler — simple, fast, deterministic", "Euler ancestral — adds noise each step (less reproducible, more variet", "Heun — 2nd-order, slower, sometimes higher quality", "IPNDM", "IPNDM-V", "LCM — for LCM-finetuned models (4–8 step generation)", "RES 2S", "RES multistep — flow-model variant", "TCD — for TCD-finetuned models"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler — some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"], enumLabels: ["(architecture default)", "Align Your Steps (AYS) — auto-tuned", "Bong Tangent", "Discrete — uniform across model timesteps (default)", "Exponential", "GITS", "Karras — concentrates more steps near the end, common for SDXL", "KL optimal", "LCM — for LCM samplers", "SGM uniform — for SGM-trained models", "Simple — used by Flux / SD3 flow models", "Smoothstep — Z-Image's recommended scheduler"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
-        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250–500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
+        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
-        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only — late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
-        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass — improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
-        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0–1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
-        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0–1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
+        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only - late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
+        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass - improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
+        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0-1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
+        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0-1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
-        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4–8. Recommended: 20–30 for SD1.5/SDXL, 20 for Flux Dev, 4–8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
-        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5–0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
+        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4-8. Recommended: 20-30 for SD1.5/SDXL, 20 for Flux Dev, 4-8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
+        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5-0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
-        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor — 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
+        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor - 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
-        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at — going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
+        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at - going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
@@ -385,6 +385,14 @@ const GENERATION_OPTIONS = [
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
 ];
 ];
 
 
+// What the server accepts for each choice, as it described them when this
+// file was generated. See scripts/gen-sdcpp-generation-options.py.
+const KNOWN_CHOICES = {
+    "cacheMode": ["easycache", "spectrum"],
+    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+};
+
 async function execute(config, input, context) {
 async function execute(config, input, context) {
     const server = normalizeServer(config.serverUrl);
     const server = normalizeServer(config.serverUrl);
     const timeout = config.timeout || 30000;
     const timeout = config.timeout || 30000;

+ 27 - 19
nodes/sdcpp/sdcpp-img2img.js

@@ -169,17 +169,17 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password',
             description: 'A basic credential holding the sdcpp-restapi username and password',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
         },
-        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count — sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
-        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache — single threshold, simple", "Spectrum — frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
-        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass — twice as fast). Recommended: SD1.5: 7. SDXL: 4–8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
+        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count - sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
+        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache - single threshold, simple", "Spectrum - frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
+        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass - twice as fast). Recommended: SD1.5: 7. SDXL: 4-8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
-        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded — Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
-        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0–1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6–0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
-        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels — niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
+        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded - Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
+        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0-1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6-0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
+        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels - niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
-        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05–0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
+        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05-0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
@@ -188,32 +188,32 @@ const configSchema = {
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         maskImageBase64: { title: "Mask Image (base64)", description: "Base64-encoded inpainting mask. White areas are repainted, black areas preserved. Recommended: Optional. Empty mask = standard img2img (no inpainting). Leave empty for the architecture default.", type: "string" },
         maskImageBase64: { title: "Mask Image (base64)", description: "Base64-encoded inpainting mask. White areas are repainted, black areas preserved. Recommended: Optional. Empty mask = standard img2img (no inpainting). Leave empty for the architecture default.", type: "string" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags — these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"], enumLabels: ["(architecture default)", "DDIM trailing — required for some fine-tunes", "DPM2 — 2nd-order, balanced", "DPM++ 2M — fast, good quality", "DPM++ 2M v2 — improved schedule", "DPM++ 2S ancestral — popular for SDXL", "ER SDE — SDE sampler (added recently)", "Euler — simple, fast, deterministic", "Euler ancestral — adds noise each step (less reproducible, more variet", "Heun — 2nd-order, slower, sometimes higher quality", "IPNDM", "IPNDM-V", "LCM — for LCM-finetuned models (4–8 step generation)", "RES 2S", "RES multistep — flow-model variant", "TCD — for TCD-finetuned models"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler — some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"], enumLabels: ["(architecture default)", "Align Your Steps (AYS) — auto-tuned", "Bong Tangent", "Discrete — uniform across model timesteps (default)", "Exponential", "GITS", "Karras — concentrates more steps near the end, common for SDXL", "KL optimal", "LCM — for LCM samplers", "SGM uniform — for SGM-trained models", "Simple — used by Flux / SD3 flow models", "Smoothstep — Z-Image's recommended scheduler"], default: "" },
+        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
-        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250–500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
+        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
-        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only — late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
-        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass — improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
-        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0–1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
-        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0–1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
+        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only - late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
+        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass - improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
+        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0-1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
+        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0-1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
-        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4–8. Recommended: 20–30 for SD1.5/SDXL, 20 for Flux Dev, 4–8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
-        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5–0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
+        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4-8. Recommended: 20-30 for SD1.5/SDXL, 20 for Flux Dev, 4-8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
+        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5-0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
-        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor — 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
+        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor - 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
-        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at — going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
+        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at - going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
@@ -379,6 +379,14 @@ const GENERATION_OPTIONS = [
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
 ];
 ];
 
 
+// What the server accepts for each choice, as it described them when this
+// file was generated. See scripts/gen-sdcpp-generation-options.py.
+const KNOWN_CHOICES = {
+    "cacheMode": ["easycache", "spectrum"],
+    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+};
+
 async function execute(config, input, context) {
 async function execute(config, input, context) {
     const server = normalizeServer(config.serverUrl);
     const server = normalizeServer(config.serverUrl);
     const timeout = config.timeout || 30000;
     const timeout = config.timeout || 30000;

+ 27 - 19
nodes/sdcpp/sdcpp-txt2img.js

@@ -175,52 +175,52 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password',
             description: 'A basic credential holding the sdcpp-restapi username and password',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
         },
-        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count — sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
-        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache — single threshold, simple", "Spectrum — frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
-        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass — twice as fast). Recommended: SD1.5: 7. SDXL: 4–8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
+        batchCount: { title: "Batch Count", description: "Number of independent images to produce in this single job. Each image gets a different seed (`seed`, `seed+1`, …). Total VRAM doesn't grow with batch_count - sd.cpp serializes. Recommended: 1 for interactive, higher when you want a grid of variations from one prompt. Leave empty for the architecture default.", type: "number" },
+        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache - single threshold, simple", "Spectrum - frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
+        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass - twice as fast). Recommended: SD1.5: 7. SDXL: 4-8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
-        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded — Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
-        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0–1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6–0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
-        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels — niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
+        controlImageBase64: { title: "ControlNet Image (base64)", description: "Base64-encoded pre-processed control image. Must match the ControlNet model loaded - Canny edges for canny model, depth map for depth model, etc. sd.cpp does not pre-process; you do that client-side. Recommended: Required when using ControlNet. Leave empty for the architecture default.", type: "string" },
+        controlStrength: { title: "ControlNet Strength", description: "How much the ControlNet influences the diffusion process (0.0-1.0). Lower = looser following, higher = tighter following at cost of overall coherence. Recommended: 0.6-0.9 for most uses. Lower for creative prompts where control should be a hint, not a constraint. Leave empty for the architecture default.", type: "number" },
+        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels - niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheEnd: { title: "EasyCache End (% of steps)", description: "Fraction of steps after which EasyCache turns off (last few steps fully recomputed for sharpness). Recommended: 0.95. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
         easycacheStart: { title: "EasyCache Start (% of steps)", description: "Fraction of steps before EasyCache becomes active. Small to skip the early high-noise steps where caching hurts. Recommended: 0.15. Leave empty for the architecture default.", type: "number" },
-        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05–0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
+        easycacheThreshold: { title: "EasyCache Threshold", description: "Reuse threshold for EasyCache. Higher = more aggressive reuse (faster but more quality loss). Range typically 0.05-0.4. Recommended: 0.2 starting point. Tune up for more speed, down for more quality. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
         height: { title: "Height (px)", description: "Output image height in pixels. Same divisibility constraint as `width`. Recommended: Match training resolution. For non-square: total pixel count close to native is more important than aspect ratio. Leave empty for the architecture default.", type: "number" },
         height: { title: "Height (px)", description: "Output image height in pixels. Same divisibility constraint as `width`. Recommended: Match training resolution. For non-square: total pixel count close to native is more important than aspect ratio. Leave empty for the architecture default.", type: "number" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         pmIdEmbedPath: { title: "PhotoMaker ID Embed Path", description: "Server-side path to a pre-computed PhotoMaker ID embedding. Skips per-generation ID encoding. Recommended: Empty unless you've pre-computed an embedding for repeated use. Leave empty for the architecture default.", type: "string" },
         pmIdEmbedPath: { title: "PhotoMaker ID Embed Path", description: "Server-side path to a pre-computed PhotoMaker ID embedding. Skips per-generation ID encoding. Recommended: Empty unless you've pre-computed an embedding for repeated use. Leave empty for the architecture default.", type: "string" },
-        pmIdImages: { title: "PhotoMaker ID Images (base64)", description: "Reference images for PhotoMaker identity preservation. Multiple faces of the same person. Recommended: 3–5 images of the same person from different angles when using PhotoMaker. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
+        pmIdImages: { title: "PhotoMaker ID Images (base64)", description: "Reference images for PhotoMaker identity preservation. Multiple faces of the same person. Recommended: 3-5 images of the same person from different angles when using PhotoMaker. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
         pmStyleStrength: { title: "PhotoMaker Style Strength", description: "PhotoMaker style-vs-identity tradeoff. Higher = more style influence, less identity preservation. Recommended: 20 (sd.cpp default). Drop to 10 for stronger identity preservation. Leave empty for the architecture default.", type: "number" },
         pmStyleStrength: { title: "PhotoMaker Style Strength", description: "PhotoMaker style-vs-identity tradeoff. Higher = more style influence, less identity preservation. Recommended: 20 (sd.cpp default). Drop to 10 for stronger identity preservation. Leave empty for the architecture default.", type: "number" },
-        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags — these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
+        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImageArgs: { title: "Reference Image Args", description: "Comma-separated k=v flags controlling reference-image preprocessing (e.g. `resize_before_vae=0,ref_index_mode=increase`). Replaces the previous auto_resize_ref_image / increase_ref_index bools. See sd.cpp docs. Recommended: Leave empty unless you need to override defaults. Leave empty for the architecture default.", type: "string" },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
         refImages: { title: "Reference Images (base64)", description: "Base64-encoded reference images for Flux Kontext / image-edit. Each image gets encoded into the conditioner alongside the text prompt. Recommended: Use for Flux Kontext or models that accept reference images. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"], enumLabels: ["(architecture default)", "DDIM trailing — required for some fine-tunes", "DPM2 — 2nd-order, balanced", "DPM++ 2M — fast, good quality", "DPM++ 2M v2 — improved schedule", "DPM++ 2S ancestral — popular for SDXL", "ER SDE — SDE sampler (added recently)", "Euler — simple, fast, deterministic", "Euler ancestral — adds noise each step (less reproducible, more variet", "Heun — 2nd-order, slower, sometimes higher quality", "IPNDM", "IPNDM-V", "LCM — for LCM-finetuned models (4–8 step generation)", "RES 2S", "RES multistep — flow-model variant", "TCD — for TCD-finetuned models"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler — some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"], enumLabels: ["(architecture default)", "Align Your Steps (AYS) — auto-tuned", "Bong Tangent", "Discrete — uniform across model timesteps (default)", "Exponential", "GITS", "Karras — concentrates more steps near the end, common for SDXL", "KL optimal", "LCM — for LCM samplers", "SGM uniform — for SGM-trained models", "Simple — used by Flux / SD3 flow models", "Smoothstep — Z-Image's recommended scheduler"], default: "" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
-        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250–500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
+        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
-        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only — late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
-        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass — improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
-        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0–1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
-        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0–1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
+        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only - late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
+        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass - improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
+        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0-1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
+        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0-1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
-        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4–8. Recommended: 20–30 for SD1.5/SDXL, 20 for Flux Dev, 4–8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
+        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4-8. Recommended: 20-30 for SD1.5/SDXL, 20 for Flux Dev, 4-8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscale: { title: "Upscale After Generate", description: "Run the loaded ESRGAN upscaler on the output image after generation. Requires an upscaler to be loaded via POST /upscaler/load. Recommended: On for one-shot 'generate then upscale' workflows. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
         upscaleAutoUnload: { title: "Auto-unload Upscaler", description: "Free upscaler VRAM right after the upscale step. Useful when you generated with `upscale: true` and want VRAM back for other work. Recommended: On. Leave empty for the architecture default.", type: "boolean" },
-        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor — 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
+        upscaleRepeats: { title: "Upscale Repeats", description: "Number of post-generation auto-upscale passes (each with the upscaler's native factor - 4× ESRGAN × 2 passes = 16×). DISTINCT from the `/upscale` endpoint's own `repeats` field, which controls passes inside a single upscale job. `upscale_repeats` only chains additional /upscale calls after txt2img / img2img finish. Recommended: 1. Two passes amplify artifacts, but produces large outputs from small inputs. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
-        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at — going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
+        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at - going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
         title: { type: "string", title: "Job Title", description: "Optional label stored with the job, useful for finding it again in the queue" },
@@ -387,6 +387,14 @@ const GENERATION_OPTIONS = [
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
     { setting: "ipAdapterStrength", server: "ip_adapter_strength" }
 ];
 ];
 
 
+// What the server accepts for each choice, as it described them when this
+// file was generated. See scripts/gen-sdcpp-generation-options.py.
+const KNOWN_CHOICES = {
+    "cacheMode": ["easycache", "spectrum"],
+    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+};
+
 async function execute(config, input, context) {
 async function execute(config, input, context) {
     const server = normalizeServer(config.serverUrl);
     const server = normalizeServer(config.serverUrl);
     const timeout = config.timeout || 30000;
     const timeout = config.timeout || 30000;

+ 23 - 15
nodes/sdcpp/sdcpp-txt2vid.js

@@ -171,13 +171,13 @@ const configSchema = {
             description: 'A basic credential holding the sdcpp-restapi username and password',
             description: 'A basic credential holding the sdcpp-restapi username and password',
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
             dynamicOptions: { source: 'credentials', filter: { type: ['sdcpp', 'basic'] } }
         },
         },
-        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache — single threshold, simple", "Spectrum — frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
-        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass — twice as fast). Recommended: SD1.5: 7. SDXL: 4–8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
+        cacheMode: { title: "Cache Mode", description: "DiT-model intermediate caching strategy. Skips redundant computation across consecutive sampler steps when the model's intermediate state hasn't changed enough to matter. `easycache` is the simplest; `spectrum` is the newest, generally best for Flux/SD3/Z-Image. Recommended: spectrum for Flux/SD3/Z-Image when generation is too slow. Off for SD1.5/SDXL (UNet is too small for caching to win). Leave empty for the architecture default.", type: "string", enum: ["", "easycache", "spectrum"], enumLabels: ["(architecture default)", "EasyCache - single threshold, simple", "Spectrum - frequency-domain analysis (best quality/speed tradeoff)"], default: "" },
+        cfgScale: { title: "CFG Scale", description: "Classifier-Free Guidance scale. Strength of pushing the cond toward the prompt vs the uncond. Higher = follows prompt more aggressively but can over-saturate. Set to 1.0 to disable CFG (skips the uncond pass - twice as fast). Recommended: SD1.5: 7. SDXL: 4-8. Flux Dev: 1 (CFG bypassed in flow models). Z-Image: 1. Schnell/Turbo: 1. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
         clipSkip: { title: "CLIP Skip", description: "Skip the last N layers of CLIP when encoding the prompt. -1 = use the model's recommended default. 2 is the classic anime-model setting. Recommended: -1 (auto). Set to 2 for anime/cartoon SD1.5 fine-tunes. Leave empty for the architecture default.", type: "number" },
         controlFrames: { title: "ControlNet Frames", description: "Per-frame control images for video generation. Length should match `video_frames` for full coverage. Recommended: Empty unless using video-ControlNet. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
         controlFrames: { title: "ControlNet Frames", description: "Per-frame control images for video generation. Length should match `video_frames` for full coverage. Recommended: Empty unless using video-ControlNet. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
-        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels — niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
+        customSigmas: { title: "Custom Sigma Schedule", description: "Replace the scheduler's noise sigma sequence with a hand-tuned one. Bypasses `scheduler`. Empty array = use the chosen scheduler. Recommended: Empty unless you're hand-tuning per-step noise levels - niche. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
         distilledGuidance: { title: "Distilled Guidance", description: "Distilled-CFG scale for models that bake the CFG behavior into the diffusion model itself (Flux). Replaces the runtime CFG split. Recommended: Flux Dev: 3.5 (sd.cpp default). Other architectures: ignored. Leave empty for the architecture default.", type: "number" },
-        endImageBase64: { title: "End Image (base64)", description: "Optional final-frame target for Wan 2.2 — forces the video to end on this image. Lets you bridge two stills with generated motion. Recommended: Empty unless doing image-to-image-bridged video. Leave empty for the architecture default.", type: "string" },
+        endImageBase64: { title: "End Image (base64)", description: "Optional final-frame target for Wan 2.2 - forces the video to end on this image. Lets you bridge two stills with generated motion. Recommended: Empty unless doing image-to-image-bridged video. Leave empty for the architecture default.", type: "string" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         eta: { title: "Eta", description: "Stochasticity parameter for DDIM-family samplers. 0 = deterministic, 1 = max stochasticity (matches DDPM noise schedule). Most other samplers ignore this. Recommended: 0 (deterministic, reproducible). Raise only with DDIM-trailing for some variation. Leave empty for the architecture default.", type: "number" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         expandPrompt: { title: "Expand Prompt Template", description: "If true, parse `prompt` for a1111-style dynamic-prompts syntax (`{a|b|c}`, `{N$$a|b|c}`) and create one queue item per variation. The response then carries `group_id` + `variation_count` + `job_ids[]` instead of a single `job_id`. Hard cap: 200 variations per request. Recommended: Enable when your prompt actually contains `{…}` syntax. Leave empty for the architecture default.", type: "boolean" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
         flowShift: { title: "Flow Shift", description: "Per-generation flow-matching shift parameter for Flux / SD3 / Z-Image models. Higher values shift sampling toward larger noise levels longer. Per-call override; the load-time `flow_shift` is the default if this isn't set. Recommended: Flux Dev: 1.0. Z-Image: 3.0. SD3 Medium: 3.0. Leave empty for the architecture default.", type: "number" },
@@ -194,31 +194,31 @@ const configSchema = {
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         initImageBase64: { title: "Init Image (base64)", description: "Base64-encoded source image for img2img / image-edit. Required for /img2img. Recommended: Required for img2img. Leave empty for the architecture default.", type: "string" },
         moeBoundary: { title: "MoE Expert Boundary", description: "For Wan 2.2 MoE: fraction of total steps at which to switch from high-noise to low-noise expert. 0 = use `high_noise_steps` directly. Recommended: 0.875 (Wan 2.2 default). Leave empty for the architecture default.", type: "number" },
         moeBoundary: { title: "MoE Expert Boundary", description: "For Wan 2.2 MoE: fraction of total steps at which to switch from high-noise to low-noise expert. 0 = use `high_noise_steps` directly. Recommended: 0.875 (Wan 2.2 default). Leave empty for the architecture default.", type: "number" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
         negativePrompt: { title: "Negative Prompt", description: "What to push the model away from. Has effect only when `cfg_scale > 1` (CFG is what compares cond vs uncond). Ignored on architectures that don't use CFG (most flow-matching models default to `cfg_scale = 1`). Recommended: Optional. Empty is a fine default for Flux / SD3 / Z-Image. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags — these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
-        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"], enumLabels: ["(architecture default)", "DDIM trailing — required for some fine-tunes", "DPM2 — 2nd-order, balanced", "DPM++ 2M — fast, good quality", "DPM++ 2M v2 — improved schedule", "DPM++ 2S ancestral — popular for SDXL", "ER SDE — SDE sampler (added recently)", "Euler — simple, fast, deterministic", "Euler ancestral — adds noise each step (less reproducible, more variet", "Heun — 2nd-order, slower, sometimes higher quality", "IPNDM", "IPNDM-V", "LCM — for LCM-finetuned models (4–8 step generation)", "RES 2S", "RES multistep — flow-model variant", "TCD — for TCD-finetuned models"], default: "" },
-        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler — some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"], enumLabels: ["(architecture default)", "Align Your Steps (AYS) — auto-tuned", "Bong Tangent", "Discrete — uniform across model timesteps (default)", "Exponential", "GITS", "Karras — concentrates more steps near the end, common for SDXL", "KL optimal", "LCM — for LCM samplers", "SGM uniform — for SGM-trained models", "Simple — used by Flux / SD3 flow models", "Smoothstep — Z-Image's recommended scheduler"], default: "" },
+        prompt: { title: "Prompt", description: "Text prompt fed to the model's text encoder (CLIP / T5 / LLM). Supports inline `<lora:name:weight>` tags - these are auto-extracted into the LoRA list before being sent to the encoder. Supports a1111-style dynamic-prompts (`{a|b|c}`) when `expand_prompt: true` is set. Recommended: Required. Leave empty for the architecture default.", type: "string", format: "textarea" },
+        sampler: { title: "Sampler", description: "Sampling algorithm. Different samplers can produce different images at the same seed; quality and speed differ too. Recommended: euler_a (general), dpmpp2m (SD1.5/SDXL), euler (Flux/SD3/Z-Image), lcm (LCM models). Leave empty for the architecture default.", type: "string", enum: ["", "euler", "euler_a", "heun", "dpm2", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "ipndm", "ipndm_v", "lcm", "ddim_trailing", "tcd", "res_multistep", "res_2s", "er_sde", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], enumLabels: ["(architecture default)", "Euler - simple, fast, deterministic", "Euler ancestral - adds noise each step (less reproducible, more variet", "Heun - 2nd-order, slower, sometimes higher quality", "DPM2 - 2nd-order, balanced", "dpm++2s_a", "dpm++2m", "dpm++2mv2", "IPNDM", "IPNDM-V", "LCM - for LCM-finetuned models (4-8 step generation)", "DDIM trailing - required for some fine-tunes", "TCD - for TCD-finetuned models", "RES multistep - flow-model variant", "RES 2S", "ER SDE - SDE sampler (added recently)", "euler_cfg_pp", "euler_a_cfg_pp", "euler_ge", "dpm++2m_sde", "dpm++2m_sde_bt", "lms"], default: "" },
+        scheduler: { title: "Scheduler", description: "Determines the noise schedule (the timesteps the sampler walks through). Pairs with the sampler - some combinations (e.g. karras+dpmpp2m) are well-tested, others may be off. Recommended: discrete or karras for SD1.5/SDXL; simple for Flux/SD3; smoothstep for Z-Image. Leave empty for the architecture default.", type: "string", enum: ["", "discrete", "karras", "exponential", "ays", "gits", "sgm_uniform", "simple", "smoothstep", "kl_optimal", "lcm", "bong_tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], enumLabels: ["(architecture default)", "Discrete - uniform across model timesteps (default)", "Karras - concentrates more steps near the end, common for SDXL", "Exponential", "Align Your Steps (AYS) - auto-tuned", "GITS", "SGM uniform - for SGM-trained models", "Simple - used by Flux / SD3 flow models", "Smoothstep - Z-Image's recommended scheduler", "KL optimal", "LCM - for LCM samplers", "Bong Tangent", "ltx2", "logit_normal", "flux", "flux2", "beta", "normal"], default: "" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
         seed: { title: "Seed", description: "RNG seed for the initial noise tensor (and stochastic samplers). -1 = pick a random one each generation. Same seed + same prompt + same model = same image. Recommended: -1 for variety. Pin a specific number for A/B comparing prompt or sampler changes. Leave empty for the architecture default.", type: "number" },
-        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250–500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
+        shiftedTimestep: { title: "Shifted Timestep", description: "Start the sampling schedule from a non-final timestep (used by NitroFusion and similar fast-sampling fine-tunes). 0 = standard schedule. 250-500 = NitroFusion's range. Recommended: 0 unless you're explicitly running a NitroFusion-style fine-tune. Leave empty for the architecture default.", type: "number" },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
         skipLayers: { title: "SLG Skip Layers", description: "Which transformer layers to skip during the SLG unconditional pass. SD3.5 Medium uses [7, 8, 9]. Recommended: [7,8,9] for SD3.5 Medium. Other models: leave default, ignored when slg_scale=0. Leave empty for the architecture default.", type: "array", items: {"type": "number"} },
-        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only — late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
-        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass — improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
-        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0–1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
-        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0–1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
+        slgEnd: { title: "SLG End (% of steps)", description: "Fraction of the sampler schedule at which SLG turns off. Recommended: 0.2 (early-cycle only - late-cycle SLG hurts quality). Leave empty for the architecture default.", type: "number" },
+        slgScale: { title: "SLG Scale", description: "Skip Layer Guidance scale. Selectively zeros out a few diffusion layers when computing the unconditional pass - improves anatomy / coherence on SD3.5-medium and similar. 0 = disabled. Recommended: 0 for most models. 2.5 for SD3.5 Medium with the recommended skip layers. Leave empty for the architecture default.", type: "number" },
+        slgStart: { title: "SLG Start (% of steps)", description: "Fraction of the sampler schedule (0.0-1.0) at which SLG kicks in. Recommended: 0.01 (almost from the start). Leave empty for the architecture default.", type: "number" },
+        spectrumFlexWindow: { title: "Spectrum Cache: flex window", description: "Spectrum-cache flexibility window (0.0-1.0). Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumLam: { title: "Spectrum Cache: λ", description: "Spectrum-cache regularization λ. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumM: { title: "Spectrum Cache: m", description: "Spectrum-cache moving-average length. Recommended: 5. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumStopPercent: { title: "Spectrum Cache: stop percent", description: "Fraction of steps after which spectrum cache disengages (last steps recomputed). Recommended: 0.8. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumW: { title: "Spectrum Cache: w", description: "Spectrum-cache `w` weight (frequency cutoff). Higher = retains more spectrum, less speedup. Recommended: 0.5 default. See sd.cpp PR #1322 for tuning. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWarmupSteps: { title: "Spectrum Cache: warmup steps", description: "Steps at the start of sampling before spectrum cache becomes active. Recommended: 2. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
         spectrumWindowSize: { title: "Spectrum Cache: window size", description: "Spectrum-cache analysis window size in steps. Recommended: 3. Leave empty for the architecture default.", type: "number" },
-        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4–8. Recommended: 20–30 for SD1.5/SDXL, 20 for Flux Dev, 4–8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
-        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5–0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
+        steps: { title: "Sampling Steps", description: "Number of denoising steps the sampler runs. More steps = closer to the model's converged output, with diminishing returns. Distilled models (Flux Schnell, SDXL Turbo, Z-Image Turbo) need only 4-8. Recommended: 20-30 for SD1.5/SDXL, 20 for Flux Dev, 4-8 for *-Turbo or Schnell variants. Leave empty for the architecture default.", type: "number" },
+        strength: { title: "Denoising Strength", description: "How much of the init image's noise to keep (0.0 = identical to init, 1.0 = ignore init entirely). Controls how aggressively img2img diverges from the input. Recommended: 0.5-0.75 for natural-looking edits. 0.9+ for radical reinterpretation. Leave empty for the architecture default.", type: "number" },
         vaceStrength: { title: "VACE Strength", description: "Strength of the VACE module (Wan 2.x video editing). 0 = disabled. Recommended: 1.0 when VACE is loaded. Leave empty for the architecture default.", type: "number" },
         vaceStrength: { title: "VACE Strength", description: "Strength of the VACE module (Wan 2.x video editing). 0 = disabled. Recommended: 1.0 when VACE is loaded. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileOverlap: { title: "VAE Tile Overlap", description: "Overlap fraction between VAE tiles for seam blending. Recommended: 0.5. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeX: { title: "VAE Tile Width", description: "Width of VAE tiles when tiling is on. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTileSizeY: { title: "VAE Tile Height", description: "Height of VAE tiles. 0 = use load-time default. Recommended: 0. Leave empty for the architecture default.", type: "number" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
         vaeTiling: { title: "VAE Tiling", description: "Per-generation override of the model-load `vae_tiling`. Process VAE encode/decode in tiles to reduce peak VRAM. Recommended: Enable for ≥2048 px outputs. Otherwise leave to the load-time default. Leave empty for the architecture default.", type: "boolean" },
         videoFrames: { title: "Video Frames", description: "Number of frames to generate (Wan / video models). Recommended: 33 for Wan 2.x (5-second clip @ 16 fps). Leave empty for the architecture default.", type: "number" },
         videoFrames: { title: "Video Frames", description: "Number of frames to generate (Wan / video models). Recommended: 33 for Wan 2.x (5-second clip @ 16 fps). Leave empty for the architecture default.", type: "number" },
-        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at — going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
+        width: { title: "Width (px)", description: "Output image width in pixels. Must be divisible by the model's patch size (typically 8 or 16). Architectures have native resolutions they were trained at - going far off them can degrade quality. Recommended: Match the architecture's training resolution: SD1.5=512, SDXL=1024, Flux/SD3/Z-Image=1024, Wan video=832. Leave empty for the architecture default.", type: "number" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterImageBase64: { title: "IP-Adapter Image (base64)", description: "A reference image whose style and subject guide the result, as base64. Needs an IP-Adapter loaded alongside the model - see the Load Model node. Recommended: Take it from a Download or Fetch Output node rather than pasting one in. Leave empty for the architecture default.", type: "string" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         ipAdapterStrength: { title: "IP-Adapter Strength", description: "How strongly the reference image guides the result. Recommended: 1.0 is the upstream default. Lower it when the reference is overwhelming the prompt. Leave empty for the architecture default.", type: "number" },
         refAudios: { title: "Reference Audios", description: "Reference audio as base64-encoded WAV, mono or stereo PCM (i16/i24/i32/f32). Recommended: For models that take audio guidance. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
         refAudios: { title: "Reference Audios", description: "Reference audio as base64-encoded WAV, mono or stereo PCM (i16/i24/i32/f32). Recommended: For models that take audio guidance. Leave empty for the architecture default.", type: "array", items: {"type": "string"} },
@@ -391,6 +391,14 @@ const GENERATION_OPTIONS = [
     { setting: "refVideos", server: "ref_videos" }
     { setting: "refVideos", server: "ref_videos" }
 ];
 ];
 
 
+// What the server accepts for each choice, as it described them when this
+// file was generated. See scripts/gen-sdcpp-generation-options.py.
+const KNOWN_CHOICES = {
+    "cacheMode": ["easycache", "spectrum"],
+    "sampler": ["ddim_trailing", "dpm2", "dpmpp2m", "dpmpp2mv2", "dpmpp2s_a", "er_sde", "euler", "euler_a", "heun", "ipndm", "ipndm_v", "lcm", "res_2s", "res_multistep", "tcd"],
+    "scheduler": ["ays", "bong_tangent", "discrete", "exponential", "gits", "karras", "kl_optimal", "lcm", "sgm_uniform", "simple", "smoothstep"]
+};
+
 async function execute(config, input, context) {
 async function execute(config, input, context) {
     const server = normalizeServer(config.serverUrl);
     const server = normalizeServer(config.serverUrl);
     const timeout = config.timeout || 30000;
     const timeout = config.timeout || 30000;

+ 7 - 1
nodes/sdcpp/sdcpp-upscale.js

@@ -79,7 +79,7 @@ const configSchema = {
         },
         },
         imageBase64: { title: "Source Image (base64)", description: "Base64-encoded source image for upscaling. Recommended: Required. Leave empty for the architecture default.", type: "string" },
         imageBase64: { title: "Source Image (base64)", description: "Base64-encoded source image for upscaling. Recommended: Required. Leave empty for the architecture default.", type: "string" },
         imageIndex: { title: "Source Image Index", description: "Which output of the source job to upscale. Used with `job_id` when the source job produced multiple images (e.g. batch_count > 1). Ignored when `job_id` is absent. 0-based: 0 = first output. Recommended: Leave at 0 unless the source job had batch_count > 1 and you want a specific image. Leave empty for the architecture default.", type: "number" },
         imageIndex: { title: "Source Image Index", description: "Which output of the source job to upscale. Used with `job_id` when the source job produced multiple images (e.g. batch_count > 1). Ignored when `job_id` is absent. 0-based: 0 = first output. Recommended: Leave at 0 unless the source job had batch_count > 1 and you want a specific image. Leave empty for the architecture default.", type: "number" },
-        jobId: { title: "Source Job ID", description: "Convenience: use the output of an existing completed job as the source for upscaling, instead of providing `image_base64`. handle_upscale loads `outputs[image_index]` from disk, base64-encodes it, and substitutes `image_base64` before the worker runs — so to the worker the two paths are indistinguishable. Recommended: Use this when chaining 'generate, then upscale' from a queued job. Leave empty for the architecture default.", type: "string" },
+        jobId: { title: "Source Job ID", description: "Convenience: use the output of an existing completed job as the source for upscaling, instead of providing `image_base64`. handle_upscale loads `outputs[image_index]` from disk, base64-encodes it, and substitutes `image_base64` before the worker runs - so to the worker the two paths are indistinguishable. Recommended: Use this when chaining 'generate, then upscale' from a queued job. Leave empty for the architecture default.", type: "string" },
         repeats: { title: "Upscale Repeats", description: "Run the upscaler N times. With a 4× model: 1 pass = 4×, 2 passes = 16×. Recommended: 1. Leave empty for the architecture default.", type: "number" },
         repeats: { title: "Upscale Repeats", description: "Run the upscaler N times. With a 4× model: 1 pass = 4×, 2 passes = 16×. Recommended: 1. Leave empty for the architecture default.", type: "number" },
         tileSize: { title: "Upscale Tile Size", description: "Tile size for streaming the source image through the upscaler. Smaller = less VRAM, more boundary artifacts. 0 = whole-image. Recommended: 128 default. Drop to 64 if hitting VRAM limits. Leave empty for the architecture default.", type: "number" },
         tileSize: { title: "Upscale Tile Size", description: "Tile size for streaming the source image through the upscaler. Smaller = less VRAM, more boundary artifacts. 0 = whole-image. Recommended: 128 default. Drop to 64 if hitting VRAM limits. Leave empty for the architecture default.", type: "number" },
         upscaleFactor: { title: "Upscale Factor", description: "Target upscale factor. 4 = 4× the source image (default for ESRGAN). Recommended: Match the loaded upscaler's native factor (4 for most ESRGAN variants). Leave empty for the architecture default.", type: "number" },
         upscaleFactor: { title: "Upscale Factor", description: "Target upscale factor. 4 = 4× the source image (default for ESRGAN). Recommended: Match the loaded upscaler's native factor (4 for most ESRGAN variants). Leave empty for the architecture default.", type: "number" },
@@ -205,6 +205,12 @@ const GENERATION_OPTIONS = [
     { setting: "upscaleFactor", server: "upscale_factor" }
     { setting: "upscaleFactor", server: "upscale_factor" }
 ];
 ];
 
 
+// What the server accepts for each choice, as it described them when this
+// file was generated. See scripts/gen-sdcpp-generation-options.py.
+const KNOWN_CHOICES = {
+
+};
+
 async function execute(config, input, context) {
 async function execute(config, input, context) {
     const server = normalizeServer(config.serverUrl);
     const server = normalizeServer(config.serverUrl);
     const timeout = config.timeout || 30000;
     const timeout = config.timeout || 30000;

+ 73 - 4
scripts/gen-sdcpp-generation-options.py

@@ -123,9 +123,17 @@ def js(value) -> str:
     return json.dumps(value, ensure_ascii=False)
     return json.dumps(value, ensure_ascii=False)
 
 
 
 
+# The server writes its descriptions with em-dashes. They are shown to people
+# in our editor, where the house style is a plain hyphen, and the meaning is
+# identical - so they are normalised on the way in rather than every reader
+# meeting two conventions in one form.
+def plain(text: str) -> str:
+    return str(text).replace('\u2014', '-').replace('\u2013', '-')
+
+
 def describe(opt: dict) -> str:
 def describe(opt: dict) -> str:
-    text = ' '.join(str(opt.get('description', '')).split())
-    hint = ' '.join(str(opt.get('recommended', '')).split())
+    text = ' '.join(plain(opt.get('description', '')).split())
+    hint = ' '.join(plain(opt.get('recommended', '')).split())
     if hint:
     if hint:
         text = f'{text} Recommended: {hint}' if text else f'Recommended: {hint}'
         text = f'{text} Recommended: {hint}' if text else f'Recommended: {hint}'
     # Every one of these is optional: the server fills an absent field from the
     # Every one of these is optional: the server fills an absent field from the
@@ -134,13 +142,42 @@ def describe(opt: dict) -> str:
     return f'{text} Leave empty for the architecture default.'
     return f'{text} Leave empty for the architecture default.'
 
 
 
 
+def openapi_enums(spec: dict) -> dict:
+    """The values each generation field accepts, from the request schema.
+
+    /options/generation carries a values map with nice labels, and it has
+    drifted: it lists 15 samplers spelled dpmpp2m where the server accepts 21
+    spelled dpm++2m. The OpenAPI schema is generated from the running build and
+    had them all, so it decides what the values are; the values map is only
+    consulted for wording.
+
+    This matters more than a missing entry in a dropdown. The server answers 202
+    to any sampler name at all - including one that is simply wrong - and
+    silently falls back to a default, so a misspelt value produces a different
+    image with nothing to say so.
+    """
+    enums = {}
+    schemas = spec.get('components', {}).get('schemas', {})
+    for schema in schemas.values():
+        for field, described in (schema.get('properties') or {}).items():
+            values = described.get('enum')
+            if values:
+                enums.setdefault(field, list(values))
+    return enums
+
+
 def prop_for(name: str, opt: dict) -> dict:
 def prop_for(name: str, opt: dict) -> dict:
     kind = opt.get('type')
     kind = opt.get('type')
     setting = camel(name)
     setting = camel(name)
-    prop: dict = {'title': opt.get('label') or setting, 'description': describe(opt)}
+    prop: dict = {'title': plain(opt.get('label') or setting), 'description': describe(opt)}
 
 
     if kind == 'select':
     if kind == 'select':
         values = opt.get('values') or {}
         values = opt.get('values') or {}
+        authoritative = OPENAPI_ENUMS.get(name)
+        if authoritative:
+            # Keep the wording from the values map where there is any, but the
+            # list itself comes from the schema.
+            values = {v: values.get(v, v) for v in authoritative}
         # The empty entry is what "leave it to the architecture" looks like in a
         # The empty entry is what "leave it to the architecture" looks like in a
         # dropdown; without it a select cannot express "unset". Some of the
         # dropdown; without it a select cannot express "unset". Some of the
         # server's own value maps already carry one, so it is not added twice.
         # server's own value maps already carry one, so it is not added twice.
@@ -148,7 +185,7 @@ def prop_for(name: str, opt: dict) -> dict:
         prop['type'] = 'string'
         prop['type'] = 'string'
         prop['enum'] = [''] + keys
         prop['enum'] = [''] + keys
         prop['enumLabels'] = ['(architecture default)'] + [
         prop['enumLabels'] = ['(architecture default)'] + [
-            ' '.join(str(values[k]).split())[:70] or k for k in keys
+            ' '.join(plain(values[k]).split())[:70] or k for k in keys
         ]
         ]
         prop['default'] = ''
         prop['default'] = ''
     elif kind == 'boolean':
     elif kind == 'boolean':
@@ -274,6 +311,28 @@ def rewrite(path: Path, endpoint: str, reference: dict) -> str:
 
 
     source = source[:start] + new_schema + source[end:]
     source = source[:start] + new_schema + source[end:]
 
 
+    # What the server says each choice-field accepts. Emitted because the
+    # server does not check: it answers 202 to any sampler name at all,
+    # including one that is simply a typo, and silently falls back to a default.
+    # A generation that quietly used a different sampler than the one asked for
+    # is not something anyone would notice from the result.
+    choice_rows = []
+    for name, opt in sorted(reference['options'].items()):
+        if opt.get('type') != 'select' or (endpoint not in opt.get('applies_to', [])
+                                           and name not in EXTRA_FOR.get(path.stem, [])):
+            continue
+        allowed = [k for k in (opt.get('values') or {}).keys() if k != '']
+        choice_rows.append(f"    {js(camel(name))}: {js(allowed)}")
+    choices_js = ("// What the server accepts for each choice, as it described them when this\n"
+                  "// file was generated. See scripts/gen-sdcpp-generation-options.py.\n"
+                  "const KNOWN_CHOICES = {\n" + ",\n".join(choice_rows) + "\n};\n")
+
+    if 'const KNOWN_CHOICES' in source:
+        source = re.sub(r'// What the server accepts for each choice.*?\n\};\n',
+                        choices_js, source, flags=re.S)
+    else:
+        source = source.replace('async function execute(', choices_js + '\n' + 'async function execute(', 1)
+
     # The setting-to-field table the request is built from.
     # The setting-to-field table the request is built from.
     rows = ',\n'.join(f"    {{ setting: {js(s)}, server: {js(n)} }}" for s, n in table)
     rows = ',\n'.join(f"    {{ setting: {js(s)}, server: {js(n)} }}" for s, n in table)
     table_js = (
     table_js = (
@@ -292,10 +351,20 @@ def rewrite(path: Path, endpoint: str, reference: dict) -> str:
     return source
     return source
 
 
 
 
+OPENAPI_ENUMS: dict = {}
+
+
 def main():
 def main():
+    global OPENAPI_ENUMS
     server = sys.argv[1] if len(sys.argv) > 1 else 'http://localhost:8077'
     server = sys.argv[1] if len(sys.argv) > 1 else 'http://localhost:8077'
     with urllib.request.urlopen(server.rstrip('/') + '/options/generation', timeout=30) as f:
     with urllib.request.urlopen(server.rstrip('/') + '/options/generation', timeout=30) as f:
         reference = json.loads(f.read().decode())
         reference = json.loads(f.read().decode())
+    # Both are unauthenticated. The schema decides what a field accepts; the
+    # reference above decides what it is called, what it does and where it
+    # belongs.
+    with urllib.request.urlopen(server.rstrip('/') + '/openapi.json', timeout=30) as f:
+        OPENAPI_ENUMS = openapi_enums(json.loads(f.read().decode()))
+    print(f'{len(OPENAPI_ENUMS)} field(s) have an authoritative list in the schema')
 
 
     root = Path(__file__).resolve().parent.parent / 'nodes' / 'sdcpp'
     root = Path(__file__).resolve().parent.parent / 'nodes' / 'sdcpp'
     for node, endpoint in NODES.items():
     for node, endpoint in NODES.items():