diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..381459476 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/start-server.sh text eol=lf +/start-server.bat text eol=crlf diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b717e56d9..54582e84c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,8 @@ on: "ggml", "examples/server/frontend", "examples/server/frontend/**", + "start-server.sh", + "start-server.bat", ] pull_request: types: [opened, synchronize, reopened] @@ -44,6 +46,8 @@ on: "ggml", "examples/server/frontend", "examples/server/frontend/**", + "start-server.sh", + "start-server.bat", ] env: @@ -107,7 +111,8 @@ jobs: run: | cp ggml/LICENSE ./build/bin/ggml.txt cp LICENSE ./build/bin/stable-diffusion.cpp.txt - zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* + chmod +x start-server.sh + zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* ./start-server.sh ./start-server.bat - name: Upload artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} @@ -170,7 +175,8 @@ jobs: run: | cp ggml/LICENSE ./build/bin/ggml.txt cp LICENSE ./build/bin/stable-diffusion.cpp.txt - zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-vulkan.zip ./build/bin/* + chmod +x start-server.sh + zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-vulkan.zip ./build/bin/* ./start-server.sh ./start-server.bat - name: Upload artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} @@ -325,7 +331,8 @@ jobs: run: | cp ggml/LICENSE ./build/bin/ggml.txt cp LICENSE ./build/bin/stable-diffusion.cpp.txt - zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* + chmod +x start-server.sh + zip -j sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-${{ steps.system-info.outputs.OS_NAME }}-${{ steps.system-info.outputs.OS_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}.zip ./build/bin/* ./start-server.sh ./start-server.bat - name: Upload artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} @@ -420,7 +427,7 @@ jobs: ls .\build\bin throw "Can't find stable-diffusion.dll" } - 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-${{ matrix.build }}-x64.zip $filePath + 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-${{ matrix.build }}-x64.zip $filePath .\start-server.sh .\start-server.bat - name: Copy and pack Cuda runtime id: pack_cuda_runtime @@ -575,7 +582,7 @@ jobs: - name: Pack artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} run: | - 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\* + 7z a sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-win-rocm-${{ env.ROCM_VERSION }}-x64.zip .\build\bin\* .\start-server.sh .\start-server.bat - name: Upload artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} @@ -737,7 +744,8 @@ jobs: run: | cp ggml/LICENSE ./build/bin/ggml.txt cp LICENSE ./build/bin/stable-diffusion.cpp.txt - zip -y -r sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip ./build/bin + chmod +x start-server.sh + zip -y -r sd-${{ env.BRANCH_NAME }}-${{ steps.commit.outputs.short }}-bin-${{ steps.system-info.outputs.OS_TYPE }}-Ubuntu-${{ env.UBUNTU_VERSION }}-${{ steps.system-info.outputs.CPU_ARCH }}-rocm-${{ matrix.ROCM_VERSION }}.zip ./build/bin ./start-server.sh ./start-server.bat - name: Upload artifacts if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/master' ) || github.event.inputs.create_release == 'true' }} diff --git a/.gitignore b/.gitignore index 80599b3f8..b55ea0f81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ tests/ .cache/ *.swp *.bat +!/start-server.bat +/start-server.local.sh +/start-server.local.bat +/start-server.local.ps1 *.bin *.exe *.gguf diff --git a/AGENTS.md b/AGENTS.md index 4953e2278..a48040869 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,8 @@ documentation. and debugging. * `examples/common/` - shared example support code. * `examples/server/` - server application built on top of the library. + Before modifying built-in recipes, read and follow + [the built-in recipe policy](examples/server/recipes/README.md). * `examples/server/frontend/` - git submodule containing independent frontend code. Avoid modifying it unless explicitly requested. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1f4c06792..ca64ad198 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,6 +33,17 @@ PRs should include: The [UIs list in the README](README.md#uis) only includes open-source projects that use `stable-diffusion.cpp` as a backend. Do not submit PRs to add closed-source UIs. +## Server Recipes + +Before adding or changing a built-in server recipe, read and follow the +[built-in recipe policy](examples/server/recipes/README.md). The policy governs +native editing, community editing LoRAs, distinct task checkpoints, external +adapters, native components and layer-count variants. Do not add recipes solely +for style or acceleration LoRAs, separately stored native components, or added +or removed model layers. Explain the task, component compatibility or capability +difference that justifies a new recipe, and verify its required components, +feature declarations and embedded API representation. + ## Code Style Format code according to the repository style before submitting changes. diff --git a/examples/server/CMakeLists.txt b/examples/server/CMakeLists.txt index 302a8d609..58260be0d 100644 --- a/examples/server/CMakeLists.txt +++ b/examples/server/CMakeLists.txt @@ -68,6 +68,8 @@ add_executable(${TARGET} ../common/media_io.cpp main.cpp runtime.cpp + model_catalog.cpp + routes_models.cpp async_jobs.cpp routes_index.cpp routes_openai.cpp @@ -88,6 +90,17 @@ else() message(STATUS "HAVE_INDEX_HTML disabled") endif() +file(GLOB BUILTIN_RECIPE_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/recipes/*.json") +set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${BUILTIN_RECIPE_FILES}) +# Keep recipes in separate literals to stay within MSVC's string size limit. +set(SD_BUILTIN_RECIPES "") +foreach(RECIPE_FILE IN LISTS BUILTIN_RECIPE_FILES) + file(READ "${RECIPE_FILE}" RECIPE_JSON) + string(APPEND SD_BUILTIN_RECIPES "R\"recipes(${RECIPE_JSON})recipes\",\n") +endforeach() +configure_file(recipes_builtin.h.in "${CMAKE_CURRENT_BINARY_DIR}/recipes_builtin.h" @ONLY) +target_include_directories(${TARGET} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}") + install(TARGETS ${TARGET} RUNTIME) target_link_libraries(${TARGET} PRIVATE stable-diffusion ${CMAKE_THREAD_LIBS_INIT}) if(SD_WEBP) diff --git a/examples/server/README.md b/examples/server/README.md index 7ead1d9e2..1bb49a59f 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -1,3 +1,248 @@ +# Model selection + +Start without model arguments to select models in the web UI: + +```sh +sd-server --model-dir ./models +``` + +From a source checkout, you can also use `sh ./start-server.sh` on Linux/macOS +or `.\start-server.bat` on Windows. The launchers look for the server relative +to the script: beside the script, then in `bin/Release/`, `bin/`, +`build/bin/Release/`, and `build/bin/`, in that order. +For a different build directory, set `SD_SERVER_PATH` to the executable path: + +```sh +SD_SERVER_PATH="./build_vulkan/bin/sd-server" sh ./start-server.sh --listen-port 8080 +``` + +```powershell +$env:SD_SERVER_PATH = ".\build_vulkan\bin\Release\sd-server.exe" +.\start-server.bat --listen-port 8080 +``` + +Binary release archives include both launchers. Keep the launchers under version +control and put personal settings in a separate file beside them: + +- Windows batch: `start-server.local.bat` +- Linux/macOS: `start-server.local.sh` + +These local files are ignored by Git and excluded from release archives. They +are loaded automatically after the launcher's defaults. They can override +`SD_SERVER_PATH`, `MODEL_DIR`, and `COMMANDLINE_ARGS`; there is no need to edit +the tracked launchers. For example: + +```bat +@echo off +set "MODEL_DIR=D:\stable-diffusion-webui\models" +set "COMMANDLINE_ARGS=--listen-port 8080 --offload-to-cpu" +``` + +```sh +MODEL_DIR="/data/stable-diffusion-webui/models" +COMMANDLINE_ARGS="--listen-port 8080 --offload-to-cpu" +``` + +Each launcher prints the resolved command before execution and forwards the +server's exit code. + +`SD_SERVER_PATH` inherits the environment value by default; leave it empty for +automatic discovery, or set it in the local file to select another executable. +Settings are applied in this order: launcher defaults, local configuration, +then invocation arguments. Repeated `--model-dir` arguments add roots, so they +do not replace the `MODEL_DIR` root. Local configuration files are shell scripts +and use the corresponding shell's syntax. + +The launchers preserve the current working directory, so the default +`MODEL_DIR="./models"` is relative to where you start the launcher. Arguments +passed when invoking the launcher follow these defaults. Repeated `--model-dir` +arguments add search roots rather than replacing `MODEL_DIR`; set `MODEL_DIR` in +the local configuration to change the first root. Other single-value options can override the defaults. +In the shell script, `COMMANDLINE_ARGS` is a +whitespace-separated list; pass values containing spaces as quoted arguments +when invoking the script. `MODEL_DIR` itself supports spaces in both scripts. +A leading `--version` bypasses the configured defaults. An explicit +`SD_SERVER_PATH` is used without falling back to another build if it is invalid. + +Choose a recipe, select the required component files, and click **Load models**. +Recipe and component selectors support searching by name or relative file ID. +Use the arrow keys and Enter to select, or Escape to cancel a search. With +**Apply recipe defaults** enabled, selecting a recipe immediately applies its +declared generation defaults while preserving prompts, input images and fields +not specified by the recipe. Disable it to keep your current parameters when +switching recipes. This preference defaults to enabled and is saved locally. +Model loading and server-info refreshes do not reapply defaults or reset the form. +Recipes declaring `video_frames` also switch the form to Video Generation; +other recipes switch to Image Generation. Generation is enabled after the +selected recipe is loaded. Changing a selection does not reload the model +until you click Load. +The server can also start with the existing model arguments shown below; +existing API clients then work without selecting a recipe. The UI automatically +selects **Command-line model (all features)** and uses the startup generation +defaults. This runtime-only recipe exposes all controls for the supported +image/video mode. Selecting another recipe previews a model switch; selecting +the command-line entry again restores its generation defaults. The entry is +removed when the command-line context is unloaded or replaced. + +Built-in recipes cover SD 1/2/3/3.5, SDXL and distilled checkpoints; +FLUX 1/2/3, Chroma, Qwen Image and editing variants; Z-Image, Anima, ERNIE, +LENS, PiD and other supported image families; AnimateDiff, Wan, LTX, +HunyuanVideo, LingBot Video and MiniMax-H3. Model-specific text encoder +labels identify the expected encoder family and size. Loading a video model enables **Video Generation**; I2V also requires a +start image. The Models panel can be collapsed and remembers its open state. +Its header shows elapsed loading time from the status API and keeps the final +time after success or failure, including after a page refresh. +Built-ins are embedded in the +binary from [recipes/](recipes/), with one JSON file per recipe. Adding or +editing a built-in requires rebuilding the server. User recipes are read from +`/recipes/*.json`. A valid user recipe with the same ID replaces +the entire built-in recipe. Across roots, the first valid user recipe wins; +overrides are logged. Duplicate user IDs within one root are rejected; invalid +recipes are reported without removing a valid recipe from another root or the +built-in version. Slots need only `id` and +`required`; the server supplies labels and search directories. Use `label` +only when a recipe needs a more specific display name. + +Recipes use `features` to enable specialized controls and `disabled_features` +to turn off shared defaults. Disabled features take precedence. The UI hides +unsupported fields, empty sections and irrelevant sampler/cache options, and +omits hidden values from generation requests. See the +[feature declarations](api.md#recipe-feature-declarations) for the defaults +and complete list of supported names. + +Built-in additions and changes must follow the [recipe policy](recipes/README.md). +Split recipes by task weights or added capabilities, not by style or acceleration +LoRAs. Quantizations, file formats and compatible layer-count variants share a +recipe. Native encoders, projectors and connectors stay in the model's recipe. +Community editing recipes require selecting the matching LoRA separately; a +recipe does not enable it automatically. + +See [generation defaults and sources](recipes/defaults.md) for the selected +publisher recommendations and reference-code defaults, with step defaults capped +at 20. Recipes link to the +original publisher's Hugging Face repository where one is available. DiT +recipes default to Euler, except for models requiring a specialized sampler. + +Compatible recipes offer a TAE slot alongside VAE. When the checkpoint does not +bundle a VAE, select at least one of VAE or TAE. The Models panel shows a reminder +below the model selectors until this requirement is met. A selected TAE is used for +encoding and decoding, including when a full VAE is also selected; use complete, +model-compatible TAE weights rather than decoder-only preview weights. Qwen +Image 2.1 uses the same recipe for its full VAE and TAEQI2.1 alternatives. + +The following variants have dedicated recipes: + +| Model | Recipe IDs / notes | +| --- | --- | +| Z-Image L2P | `z-image-l2p`; no VAE | +| LLaDA-Image | `llada-image` (base) and `llada-image-turbo`; use matching encoders and connectors | +| PixArt Alpha | `pixart-alpha-512`, `pixart-alpha-1024`; Alpha VAE scale and positional settings | +| PiD / PiD 1.5 | `pid` / `pid-15` for Flux or Z-Image VAE; append `-sd3`, `-flux2`, or `-qwen-image` for other latent formats | +| SDXL adapters | `sdxl-ip-adapter` supports classic and Plus ViT-H adapters; `sdxl-controlnet` requires compatible SDXL weights | +| FLUX.1 Fill / Depth | `flux1-fill` uses Init Image and Mask Image; `flux1-depth` uses a precomputed depth map in Control Image | +| SD2 / SDXL inpainting | `sd2-inpainting`, `sdxl-inpainting` | +| SD-Turbo | `sd-turbo`; single-file SD2.1-derived checkpoint | +| Community editing LoRAs | `anima-edit`, `krea2-raw-edit`, `krea2-turbo-edit`; apply a matching LoRA after loading | +| HunyuanVideo 1.5 | `hunyuan-video-15-t2v`, `hunyuan-video-15-i2v`; choose the task's checkpoint | +| Wan 2.1 / compatible single Wan 2.2 experts | `wan21-t2v`, `wan21-i2v`; the I2V recipe also accepts compatible SkyReels V2 weights | +| Qwen Image 2.1 Turbo | `qwen-image-21-turbo`; standalone Turbo weights with the fixed 8-step schedule, Euler and CFG 1 | +| MOSS Image | `moss-image-base`, `moss-image-sft`; choose the corresponding checkpoint | + +PiD always requires a reference image and uses the first one. Its recipes expose +reference inputs rather than ordinary image-to-image inputs. IP-Adapter recipes +expose a separate IP-Adapter image and strength. FLUX Canny/Depth consume Control +Image directly without a separate ControlNet; the UI does not calculate edge +or depth maps. Their strength is controlled by guidance, not ControlNet strength. +Krea2 edit recipes use the default `krea2_ostris_edit` reference preset, so the +editing LoRA must match that convention. Ordinary Anima and Krea2 recipes do not +expose reference-image editing inputs. + +Fill/Depth guidance defaults follow the [Diffusers FLUX documentation](https://huggingface.co/docs/diffusers/api/pipelines/flux). +SD-Turbo uses the [model card's one-step configuration](https://huggingface.co/stabilityai/sd-turbo), +with CFG disabled using scale 1 in sd.cpp. + +SVD is not included: legacy model recognition and UNet branches remain, but the +current runner/conditioning setup does not provide a dedicated SVD image-conditioning +path. Adding a recipe alone would not establish a working SVD pipeline. + +Reference images, masks, audio, first/last video frames, and generation options +such as upscaling remain generation inputs. Hardware and memory options remain +startup arguments. Tiny SD/Tiny SD2 and Diffusers LCM directories must first be +converted into single-file checkpoints for the file catalog. LCM checkpoints +and LoRAs use the matching SD family recipe; select LCM sampling and adjust +steps/CFG for the chosen weights. +Metadata inspection, conversion, standalone upscaling and ADetailer are CLI +workflows rather than model-loading recipes. ESRGAN and LTX latent upscalers +are selected through generation upscaling settings with `--hires-upscalers-dir`; +textual inversion uses the startup `--embd-dir` option. + +Repeat `--model-dir` to share multiple model directories, including an existing +SD WebUI model directory: + +```sh +sd-server --model-dir ./models --model-dir /data/stable-diffusion-webui/models +``` + +With no `--model-dir`, the server uses `./models`. Explicit roots replace that +default and are scanned in argument order. Duplicate canonical roots and file +paths are scanned once. Every root can contain a `recipes/` directory. +Directory names are matched case-insensitively on all platforms: + +| Category | Directory names | +| --- | --- | +| Checkpoints | `checkpoints`, `Stable-diffusion` | +| VAE | `vae`, `VAE` | +| LoRA | `loras`, `Lora` | +| ControlNet | `controlnet`, `ControlNet` | + +Other categories keep their existing directory names. Classification uses only +the containing directory: `*.vae.safetensors`, `*.vae.ckpt`, and similar names +inside `checkpoints/` or `Stable-diffusion/` remain checkpoints. + +Place model files in category directories, for example: + +```text +models/ + checkpoints/ + diffusion_models/ + text_encoders/ + vae/ + clip_vision/ + loras/ + recipes/ + my-flux.json +``` + +Subdirectories within a model category are supported. Use **Refresh files & +recipes** after adding files or changing recipes. Scanning lists file names +without loading model weights. Category membership does not guarantee +compatibility; the model loader performs the final compatibility check. +Symlinks resolving outside the model directory are rejected. Dropdown labels +omit the category prefix while retaining subdirectory names. Identical display +paths are displayed as full file paths, with source directories before the +filename instead of a hash suffix. The same applies to duplicate LoRA paths. File IDs include a +root qualifier for additional roots; clients should use the returned IDs. +Empty optional categories do not display missing-file hints. + +Place Tiny AutoEncoder (TAE) files in `vae/` and select them in the **TAE** +slot. The legacy `vae_approx/` directory is also scanned for TAE selections. + +LoRAs are discovered automatically in both `loras/` and `Lora/` under every +model root. Use `--lora-model-dir` to replace automatic discovery with one +explicit LoRA directory. Duplicate names remain separate selectable entries. The LoRA selectors support +searching by name or path with the same keyboard controls as model selectors. + +The server keeps one model context loaded. Loading or unloading is rejected +while generation requests or queued jobs are pending. A switch validates the +selection before unloading the old context, then loads the replacement. +If loading fails, the service stays online with no active model and reports +the error. Backend and memory options supplied at startup apply to subsequent +loads. Recipe defaults apply to new loads; explicitly supplied generation +request parameters take precedence. + +See [the API reference](api.md#model-management) for recipe fields, supported +slots, management endpoints, and compatibility behavior. + # Example The following example starts `sd-server` with a standalone diffusion model, VAE, and LLM text encoder: diff --git a/examples/server/api.md b/examples/server/api.md index cff4e8455..bd8de9562 100644 --- a/examples/server/api.md +++ b/examples/server/api.md @@ -59,6 +59,293 @@ Current endpoints include: - `POST /sdcpp/v1/vid_gen` - `POST /sdcpp/v1/upscale` +## Model management + +Model management is additive: existing generation request bodies, successful +responses, job polling and cancellation remain unchanged. All three API families +use the current loaded context. Starting with the existing model arguments +preserves command-line preloading and its generation defaults; it does not +require a recipe. + +Recipe defaults apply when a generation request omits the corresponding +parameters. Image editing endpoints retain their existing input-image size +inference when dimensions are omitted. + +Without a loaded model, generation endpoints return HTTP 503 with +`{"error":"No model is loaded","code":"model_not_loaded"}`. +While loading/unloading, endpoints that access the context return HTTP 503 +with `code: "model_loading"`. These errors retain the existing string-valued +`error` field. Job polling/cancellation, catalog queries and model status remain +available during loading. Model lists are empty when no model is loaded; +`/v1/models` retains the `sd-cpp-local` ID when ready. + +| Method | Endpoint | Response / behavior | +| --- | --- | --- | +| GET | `/sdcpp/v1/recipes` | `{recipes: [...], errors: [...]}` | +| GET | `/sdcpp/v1/model-files` | `{files: [{id, name, category, root, relative_path}], errors: [...]}` | +| POST | `/sdcpp/v1/models/refresh` | Rescan recipes and files; return `{errors: [...]}` | +| GET | `/sdcpp/v1/models/status` | Current state, recipe, selection, error, loading time and optional revision | +| POST | `/sdcpp/v1/models/load` | Load the submitted recipe and selection; return status after completion | +| POST | `/sdcpp/v1/models/unload` | Release the active context; return empty status | + +Load request: + +```json +{ + "recipe": "flux1-dev", + "models": { + "diffusion_model": "diffusion_models/flux1-dev.gguf", + "clip_l": "text_encoders/clip_l.safetensors", + "t5xxl": "text_encoders/t5xxl.safetensors", + "vae": "vae/ae.safetensors" + } +} +``` + +File IDs are opaque strings returned by `model-files`. The first model root +retains the existing relative IDs, such as `checkpoints/nested/model.gguf`. +Additional roots use `@/` so identical paths in different +roots remain selectable. Root qualifiers derive from the canonical root path; +adding unrelated roots or refreshing the catalog does not change them. Moving +a root changes its qualifier. Do not construct IDs or use display names in load +requests. + +The display `name` omits the physical category directory (including aliases such +as `Stable-diffusion`). Duplicate display paths use the full absolute file path, +with the source directories before the filename and no hash suffix. +`root` is an opaque source identifier, and +`relative_path` is the file path beneath that root. Duplicate canonical files +appear only once, using the first root that discovers them. Categories depend +on directories, never on filename suffixes such as `.vae.safetensors`. + +Repeated `--model-dir` arguments merge roots in order; absent arguments default +to `./models`. User recipes override built-ins; the earliest root containing a +valid user recipe wins across roots. Duplicate recipe IDs within one root are +rejected. Directory aliases do not change recipe slot categories. + +LoRA lists retain relative `path` IDs for `loras/` in the first model root and +for an explicit `--lora-model-dir`, and qualify other directories. The additive `display_path` field provides a readable +relative path, or the full absolute file path when needed to distinguish duplicates. +Continue sending `path` in structured LoRA requests. `--lora-model-dir` overrides +automatic discovery across all model roots. +Absolute paths, unknown files, wrong categories and +paths escaping the configured root are rejected. Optional slots can be omitted +or set to an empty string. Load/unload returns HTTP 409 (`model_busy`) while +requests or queued jobs are pending. Invalid configurations return HTTP 400 +(`invalid_model_config`) before unloading the current context. An initialization +failure returns HTTP 422 (`model_load_failed`) and leaves no active context. +The load HTTP request waits for completion; a separate client can poll status +while it runs. Refreshing the catalog never modifies an already loaded context. + +Status uses `state: empty | loading | ready | error`, `recipe` (ID or null), +`models` (slot-to-file-ID mapping), and `error` (string or null). A monotonically +increasing `revision` identifies runtime transitions within this server process. +Command-line preloading keeps `origin: command_line` and `recipe: null` for +compatibility, and adds `runtime_recipe`: a read-only `@command-line` recipe +with all feature declarations enabled. It is only available while that context +is loaded and cannot be submitted to `models/load`. The UI selects it by default +and uses command-line generation defaults, not defaults from a catalog recipe. +Capabilities also include this status as an additive `runtime` field. + +`load_elapsed_seconds` is the server-measured duration in seconds (a number or +null). It updates while a model is loading and freezes on success or failure. +Timing starts after selection validation and includes releasing the previous +model. It resets for each load and is null before any load and during/after +unloading. Command-line preloading reports its completed initialization time. +Poll `/sdcpp/v1/models/status` to display elapsed time, including while loading; +timer updates alone do not change `revision`. + +### Recipe feature declarations + +Recipes may declare `features` and `disabled_features` arrays. The server starts +with shared defaults, enables features declared by component slots and `features`, +then applies `disabled_features` last. A disabled feature always wins. Unknown +names, non-array values and duplicate names within an array are rejected. +Both fields are optional; existing recipes remain valid. + +```json +{ + "features": ["flow_shift", "distilled_guidance"], + "disabled_features": ["clip_skip", "lora"] +} +``` + +Default-enabled features are `img2img`, `mask_image`, `lora`, `vae_tiling`, +`cache`, `cache_dit` and `preview`. All other features below default to false: + +| Feature | Controls | +| --- | --- | +| `flow_shift` | Flow shift, where used by the selected scheduler | +| `distilled_guidance` | Distilled guidance embedding | +| `shifted_timestep` | SDXL timestep shifting | +| `img_cfg` | Image CFG | +| `slg` | Skip-layer guidance fields | +| `clip_skip` | CLIP Skip | +| `img2img`, `mask_image` | Image initialization/strength and masks | +| `ref_images` | Reference image inputs | +| `controlnet` | Control image and ControlNet strength | +| `control_image` | Control image without a separate ControlNet (FLUX Canny/Depth) | +| `ip_adapter` | IP-Adapter image and strength | +| `init_image`, `end_image` | Video start/end frames | +| `control_frames`, `vace` | Video control frames and VACE strength | +| `high_noise` | High-noise pass, MoE boundary and high-noise LoRAs | +| `lora`, `vae_tiling` | LoRA overrides and VAE tiling | +| `cache`, `cache_unet`, `cache_dit` | Cache panel and architecture-specific algorithms | +| `preview`, `preview_tae` | Preview panel and TAE preview option | + +A `control_net` slot enables `controlnet`, `ip_adapter` enables `ip_adapter`, +`high_noise_diffusion_model` enables `high_noise`, and `tae`/`taesd` enables `preview_tae`, unless explicitly disabled. +ControlNet controls are shown when previewing a supporting recipe, even before +a model file is selected. High-noise and TAE controls additionally require their +component to be selected. Runtime capabilities require the component to be loaded. +PuLID disables SLG while selected. +Image-only and video-only inputs are filtered by the active generation mode. +Eta depends on the selected sampler; SCM fields only apply to DBCache, +TaylorSeer and Cache-DIT. Empty panels and advanced sections are hidden. + +`GET /sdcpp/v1/recipes` adds a resolved boolean `effective_features` object to +each recipe. This is response-only metadata, like `source` and slot `categories`; +omit it when writing a user recipe file. The defaults are maintained by the +server, not duplicated by the frontend. + +`GET /sdcpp/v1/capabilities` adds `generation_features` for the loaded context, +derived from the active recipe and component availability without inferring +features from model-family names. When no model is loaded, every feature is +present and false. The UI can still preview a selected recipe using its +`effective_features` before loading. +Existing `features` / `features_by_mode` keys are retained and their model-specific +values reflect these declarations. A loaded recipe is snapshotted: refreshing +recipe files does not change the active context's capabilities until reloading. +Command-line preloading uses the runtime-only all-features recipe instead of model-family inference +and component paths; variant-specific recipe declarations are more precise. + +These declarations govern the Web UI and advertised capabilities, not API +request validation. Existing generation APIs continue accepting their existing +fields. The Web UI omits unsupported/hidden fields and ignores hidden image +inputs when building requests, so switching recipes cannot submit stale options. + +### User recipe format + +Contributions to the built-in catalog must follow the +[built-in recipe policy](recipes/README.md). Personal user recipes may define +workflow-specific presets without adding them to the built-in catalog. + +Each UTF-8 JSON file in `/recipes/` defines one complete recipe. +The maximum recipe file size is 1 MiB. Unknown fields, invalid types and +duplicate slots are errors. Built-ins in `examples/server/recipes/*.json` use +the same one-recipe-per-file schema: + +```json +{ + "schema_version": 1, + "id": "my-flux", + "name": "My FLUX", + "description": "FLUX.1 dev", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-dev", + "slots": [ + {"id": "diffusion_model", "required": true}, + {"id": "clip_l", "required": true}, + {"id": "t5xxl", "required": true}, + {"id": "vae", "required": false}, + {"id": "tae", "required": false} + ], + "required_any": [["vae", "tae"]], + "defaults": {"width": 1024, "height": 1024, "cfg_scale": 1.0, "distilled_guidance": 3.5, "sampling_method": "euler", "steps": 20} +} +``` + +IDs contain 1–128 ASCII letters, digits, dots, underscores or hyphens. +`name`, `schema_version: 1` and `slots` are required; `description` and +`model_url`, `required_any`, `defaults`, `context`, `features` and `disabled_features` are optional. Each slot requires only `id` and boolean `required`. +The server supplies the default label and search directories for each slot. +An optional nonempty `label` overrides the display name, for example +`{"id": "diffusion_model", "label": "Low-noise diffusion model", "required": true}`. +Legacy `category` fields remain accepted, but must match one of the slot's +supported directories; they cannot redefine or restrict its search directories. +At least one required `model` or `diffusion_model` slot is necessary. + +`model_url` is an optional HTTPS Hugging Face model repository link, shown beside +the recipe description. Built-ins link to the model's author/publisher, not repacks. +`required_any` contains groups of declared slot IDs. At least one model in each +group must be selected. For example, `"required_any": [["vae", "tae"]]` with both +slots marked `required: false` allows either the full VAE or a compatible TAE. +Selecting both is allowed; the current server uses TAE for encoding/decoding when +TAE is selected. Checkpoint recipes with a bundled VAE do not need this group. +Use complete, model-compatible TAE weights; decoder-only preview weights cannot +replace a full VAE. Pixel-space models and unsupported latent formats have no +TAE slot. + +| Slot IDs | Search directories | +| --- | --- | +| `model` | `checkpoints` | +| `diffusion_model`, `high_noise_diffusion_model`, `uncond_diffusion_model` | `diffusion_models` | +| `clip_l`, `clip_g`, `t5xxl`, `llm`, `audio_encoder`, `embeddings_connectors` | `text_encoders` | +| `clip_vision`, `llm_vision` | `clip_vision` | +| `vae`, `audio_vae` | `vae` | +| `tae` (legacy alias: `taesd`) | `vae` or `vae_approx` | +| `tokenizer` | `tokenizers` | +| `control_net` | `controlnet` | +| `ip_adapter` | `ipadapter` | +| `motion_module` | `motion_modules` | +| `photo_maker` | `photomaker` | +| `pulid_weights` | `pulid` | + +Built-in recipes use `tae` for the Tiny AutoEncoder slot. TAE selections can use files from either `vae/` or the legacy +`vae_approx/` directory. Existing recipes and load requests using `taesd` +remain supported. Do not specify both `tae` and `taesd` in the same recipe +or load request. + +Model files can use `.gguf`, `.safetensors`, `.sft`, `.ckpt`, `.pt`, `.pth` +or `.bin` extensions. The tokenizer category lists `.json` files only. +Custom recipes compose components supported by the backend; they do not add +new model architectures. + +Optional `context` settings apply to each model load and override the matching +startup setting. Supported fields are `vae_format` (`auto`, `flux`, `sd3`, +`flux2`, or `wan`) and `model_args` (a string in the same format as +`--model-args`; replaces the startup string). Other startup settings remain +unchanged. Switching to a recipe that omits these fields restores the startup +values. For example, PiD uses `{"vae_format": "flux"}` and Qwen Image Edit +2511 uses `{"model_args": "qwen_image_zero_cond_t=true"}`. + +Supported defaults: + +- Integer `width`/`height` (64–4096), `steps` (1–1000), `video_frames` + (1–4096), `fps` (1–240), `shifted_timestep` (0–1000), and + `high_noise_steps` (1–1000 or -1 for automatic). +- Numeric `cfg_scale`, `img_cfg_scale`, `distilled_guidance`, `flow_shift`, + `high_noise_cfg_scale`, and `high_noise_flow_shift` (0–1000). +- Numeric `strength` (0–1), used for image-to-image denoising, and + `moe_boundary` (0–1), used for automatic Wan MoE expert switching. +- `custom_sigmas`: an empty array or 2–1001 finite, nonnegative numbers. + A nonempty array overrides the generated schedule; the sigma count minus one + determines the actual step count. Include the final zero. An empty array uses + the generated schedule. Loaded recipe sigmas apply to generation requests + unless `sample_params.custom_sigmas` overrides them; switching recipes restores + that recipe's schedule or the startup default. +- Supported `sampling_method`/`scheduler` and + `high_noise_sampling_method`/`high_noise_scheduler` names. + +Other generation parameters continue to use startup defaults unless explicitly +supplied by the generation request. Built-in Wan recipes provide video defaults; +use `/sdcpp/v1/vid_gen` after loading a video model. +LoRAs are merged from `loras/` and `Lora/` in every model root unless +`--lora-model-dir` is supplied. + +Recipe responses fill in each slot's `label`, `category` (the legacy value, +if supplied, or the first search directory), and `categories` (the complete +server-defined list of search directories). Clients should filter model files +using `categories`; `category` remains available for older clients. +For example, `{"id": "tae", "required": false}` expands to +`{"id": "tae", "required": false, "label": "TAE", "category": "vae", "categories": ["vae", "vae_approx"]}`. + +Recipe responses also add `source: builtin | user` and, for user recipes, `file`. +Do not include `source`, `file`, or slot `categories` in a recipe file. A valid user recipe +replaces a built-in with the same ID in full; inheritance/partial merging is +not supported. Invalid recipes are skipped. Multiple valid user files sharing +an ID are all skipped, preserving any built-in with that ID. Catalog errors +are returned as `{file, message}` entries. + ## `sd_cpp_extra_args` `sd_cpp_extra_args` is an extension mechanism for the compatibility APIs. @@ -409,7 +696,7 @@ Common job shape: "id": "job_01HTXYZABC", "kind": "img_gen", "status": "queued", - "created": 1775401200, + "created": 1775401200000, "started": null, "completed": null, "queue_position": 2, @@ -433,6 +720,11 @@ Field types: | `error` | `object \| null` | | `preview` | `object \| null` | +`created`, `started`, and `completed` are integer Unix timestamps in +**milliseconds**. Subtract `started` from `completed` and divide by 1000 to get +the elapsed generation time in seconds, including output encoding and excluding +queue time. + `preview` sub-fields: | Field | Type | Notes | @@ -508,6 +800,7 @@ Shared nested fields: | --- | --- | | `loras[].name` | `string` | | `loras[].path` | `string` | +| `loras[].display_path` | `string` (display only) | `upscalers` @@ -662,7 +955,7 @@ Example response: "id": "job_01HTXYZABC", "kind": "img_gen", "status": "queued", - "created": 1775401200, + "created": 1775401200000, "poll_url": "/sdcpp/v1/jobs/job_01HTXYZABC" } ``` @@ -990,9 +1283,9 @@ Example completed job: "id": "job_01HTXYZABC", "kind": "img_gen", "status": "completed", - "created": 1775401200, - "started": 1775401203, - "completed": 1775401215, + "created": 1775401200000, + "started": 1775401203000, + "completed": 1775401215237, "queue_position": 0, "result": { "output_format": "png", @@ -1016,9 +1309,9 @@ Example failed job: "id": "job_01HTXYZABC", "kind": "img_gen", "status": "failed", - "created": 1775401200, - "started": 1775401203, - "completed": 1775401204, + "created": 1775401200000, + "started": 1775401203000, + "completed": 1775401204000, "queue_position": 0, "result": null, "error": { @@ -1037,9 +1330,9 @@ Example cancelled job: "id": "job_01HTXYZABC", "kind": "img_gen", "status": "cancelled", - "created": 1775401200, + "created": 1775401200000, "started": null, - "completed": 1775401202, + "completed": 1775401202000, "queue_position": 0, "result": null, "error": { @@ -1075,7 +1368,7 @@ Example response: "id": "job_01HTXYZVID", "kind": "vid_gen", "status": "queued", - "created": 1775401200, + "created": 1775401200000, "poll_url": "/sdcpp/v1/jobs/job_01HTXYZVID" } ``` @@ -1403,9 +1696,9 @@ Example completed job: "id": "job_01HTXYZVID", "kind": "vid_gen", "status": "completed", - "created": 1775401200, - "started": 1775401203, - "completed": 1775401215, + "created": 1775401200000, + "started": 1775401203000, + "completed": 1775401215237, "queue_position": 0, "result": { "output_format": "webm", @@ -1429,9 +1722,9 @@ Example failed job: "id": "job_01HTXYZVID", "kind": "vid_gen", "status": "failed", - "created": 1775401200, - "started": 1775401203, - "completed": 1775401204, + "created": 1775401200000, + "started": 1775401203000, + "completed": 1775401204000, "queue_position": 0, "result": null, "error": { @@ -1450,9 +1743,9 @@ Example cancelled job: "id": "job_01HTXYZVID", "kind": "vid_gen", "status": "cancelled", - "created": 1775401200, + "created": 1775401200000, "started": null, - "completed": 1775401202, + "completed": 1775401202000, "queue_position": 0, "result": null, "error": { diff --git a/examples/server/async_jobs.cpp b/examples/server/async_jobs.cpp index 88408088e..7f3c8a1bf 100644 --- a/examples/server/async_jobs.cpp +++ b/examples/server/async_jobs.cpp @@ -88,7 +88,7 @@ const char* async_job_status_name(AsyncJobStatus status) { } void purge_expired_jobs(AsyncJobManager& manager) { - const int64_t now = unix_timestamp_now(); + const int64_t now = unix_timestamp_now_ms(); for (auto it = manager.expired_jobs.begin(); it != manager.expired_jobs.end();) { if (it->second <= now) { @@ -108,8 +108,8 @@ void purge_expired_jobs(AsyncJobManager& manager) { int64_t ttl_seconds = job->status == AsyncJobStatus::Completed ? manager.completed_ttl_seconds : manager.failed_ttl_seconds; - if (now - job->completed_at >= ttl_seconds) { - manager.expired_jobs[job->id] = now + std::max(ttl_seconds, 60); + if (now - job->completed_at >= ttl_seconds * 1000) { + manager.expired_jobs[job->id] = now + std::max(ttl_seconds, 60) * 1000; it = manager.jobs.erase(it); } else { ++it; @@ -130,7 +130,7 @@ size_t count_pending_jobs(const AsyncJobManager& manager) { std::string make_async_job_id(AsyncJobManager& manager) { std::ostringstream oss; - oss << "job_" << std::hex << unix_timestamp_now() << "_" << std::setw(8) + oss << "job_" << std::hex << unix_timestamp_now_ms() << "_" << std::setw(8) << std::setfill('0') << manager.next_id++; return oss.str(); } @@ -143,7 +143,7 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) { manager.queue.erase(new_end, manager.queue.end()); job.status = AsyncJobStatus::Cancelled; - job.completed_at = unix_timestamp_now(); + job.completed_at = unix_timestamp_now_ms(); job.result_images_b64.clear(); job.result_media_b64.clear(); job.result_media_mime_type.clear(); @@ -373,7 +373,7 @@ void async_job_worker(ServerRuntime& runtime) { job = it->second; job->status = AsyncJobStatus::Generating; - job->started_at = unix_timestamp_now(); + job->started_at = unix_timestamp_now_ms(); } std::vector output_images; @@ -405,7 +405,7 @@ void async_job_worker(ServerRuntime& runtime) { continue; } - job->completed_at = unix_timestamp_now(); + job->completed_at = unix_timestamp_now_ms(); if (ok) { job->status = AsyncJobStatus::Completed; job->result_images_b64 = std::move(output_images); diff --git a/examples/server/async_jobs.h b/examples/server/async_jobs.h index 2fe79bb24..049a9d73b 100644 --- a/examples/server/async_jobs.h +++ b/examples/server/async_jobs.h @@ -32,7 +32,7 @@ struct AsyncGenerationJob { std::string id; AsyncJobKind kind = AsyncJobKind::ImgGen; AsyncJobStatus status = AsyncJobStatus::Queued; - int64_t created_at = unix_timestamp_now(); + int64_t created_at = unix_timestamp_now_ms(); int64_t started_at = 0; int64_t completed_at = 0; ImgGenJobRequest img_gen; diff --git a/examples/server/frontend b/examples/server/frontend index 8e7c11505..e36e34a2e 160000 --- a/examples/server/frontend +++ b/examples/server/frontend @@ -1 +1 @@ -Subproject commit 8e7c11505925c0dbf9aaea56a3a5058b8690d66b +Subproject commit e36e34a2e2204e3df654b16cadc112d8ce8f3174 diff --git a/examples/server/main.cpp b/examples/server/main.cpp index ddfd7334e..170877686 100644 --- a/examples/server/main.cpp +++ b/examples/server/main.cpp @@ -10,6 +10,7 @@ #include "async_jobs.h" #include "common/common.h" #include "common/resource_owners.hpp" +#include "model_catalog.h" #include "routes.h" #include "runtime.h" @@ -50,7 +51,7 @@ static void parse_args(int argc, const bool random_seed_requested = default_gen_params.seed < 0; if (!svr_params.resolve_and_validate() || - !ctx_params.resolve_and_validate(IMG_GEN) || + !ctx_params.resolve_and_validate(ctx_params.model_path.empty() && ctx_params.diffusion_model_path.empty() ? METADATA : IMG_GEN) || !default_gen_params.resolve_and_validate(IMG_GEN, ctx_params.lora_model_dir, ctx_params.hires_upscalers_dir)) { @@ -84,14 +85,19 @@ int main(int argc, const char** argv) { LOG_VERBOSE("version: %s", version_string().c_str()); LOG_VERBOSE("%s", sd_get_system_info()); - LOG_VERBOSE("%s", svr_params.to_string().c_str()); + LOG_DEBUG("%s", svr_params.to_string().c_str()); LOG_DEBUG("%s", ctx_params.to_string().c_str()); LOG_DEBUG("%s", default_gen_params.to_string().c_str()); sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(false); - SDCtxPtr sd_ctx(new_sd_ctx(&sd_ctx_params)); + ServerModels models; + if (!ctx_params.model_path.empty() || !ctx_params.diffusion_model_path.empty()) { + const auto started = std::chrono::steady_clock::now(); + models.context.reset(new_sd_ctx(&sd_ctx_params)); + models.status["load_elapsed_seconds"] = std::chrono::duration(std::chrono::steady_clock::now() - started).count(); + } - if (sd_ctx == nullptr) { + if ((!ctx_params.model_path.empty() || !ctx_params.diffusion_model_path.empty()) && !models.context) { LOG_ERROR("new_sd_ctx_t failed"); return 1; } @@ -104,7 +110,7 @@ int main(int argc, const char** argv) { std::mutex upscaler_mutex; AsyncJobManager async_job_manager; ServerRuntime runtime = { - sd_ctx.get(), + models.context.get(), &sd_ctx_mutex, &svr_params, &ctx_params, @@ -114,8 +120,16 @@ int main(int argc, const char** argv) { &upscaler_cache, &upscaler_mutex, &async_job_manager, + &models, }; + try { + initialize_server_models(runtime); + } catch (const std::exception& e) { + LOG_ERROR("model catalog initialization failed: %s", e.what()); + return 1; + } + std::thread async_worker(async_job_worker, std::ref(runtime)); httplib::Server svr; @@ -147,6 +161,7 @@ int main(int argc, const char** argv) { register_openai_api_endpoints(svr, runtime); register_sdapi_endpoints(svr, runtime); register_sdcpp_api_endpoints(svr, runtime); + register_model_endpoints(svr, runtime); LOG_INFO("listening on: http://%s:%d\n", svr_params.listen_ip.c_str(), svr_params.listen_port); svr.listen(svr_params.listen_ip, svr_params.listen_port); diff --git a/examples/server/model_catalog.cpp b/examples/server/model_catalog.cpp new file mode 100644 index 000000000..a3c59206d --- /dev/null +++ b/examples/server/model_catalog.cpp @@ -0,0 +1,664 @@ +#include "model_catalog.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common/log.h" +#include "recipes_builtin.h" + +namespace fs = std::filesystem; + +struct ModelSlot { + const char* id; + const char* label; + std::vector categories; + std::string SDContextParams::*field; +}; + +static const ModelSlot k_model_slots[] = { + {"model", "Checkpoint", {"checkpoints"}, &SDContextParams::model_path}, + {"diffusion_model", "Diffusion model", {"diffusion_models"}, &SDContextParams::diffusion_model_path}, + {"high_noise_diffusion_model", "High-noise diffusion model", {"diffusion_models"}, &SDContextParams::high_noise_diffusion_model_path}, + {"uncond_diffusion_model", "Unconditional diffusion model", {"diffusion_models"}, &SDContextParams::uncond_diffusion_model_path}, + {"clip_l", "CLIP-L", {"text_encoders"}, &SDContextParams::clip_l_path}, + {"clip_g", "CLIP-G", {"text_encoders"}, &SDContextParams::clip_g_path}, + {"clip_vision", "CLIP Vision", {"clip_vision"}, &SDContextParams::clip_vision_path}, + {"t5xxl", "T5", {"text_encoders"}, &SDContextParams::t5xxl_path}, + {"llm", "LLM Text Encoder", {"text_encoders"}, &SDContextParams::llm_path}, + {"llm_vision", "LLM Vision Encoder", {"clip_vision"}, &SDContextParams::llm_vision_path}, + {"tokenizer", "Tokenizer", {"tokenizers"}, &SDContextParams::tokenizer}, + {"vae", "VAE", {"vae"}, &SDContextParams::vae_path}, + {"audio_vae", "Audio VAE", {"vae"}, &SDContextParams::audio_vae_path}, + {"audio_encoder", "Audio Encoder", {"text_encoders"}, &SDContextParams::audio_encoder_path}, + {"embeddings_connectors", "Embeddings Connectors", {"text_encoders"}, &SDContextParams::embeddings_connectors_path}, + {"tae", "TAE", {"vae", "vae_approx"}, &SDContextParams::taesd_path}, + {"taesd", "TAE", {"vae_approx", "vae"}, &SDContextParams::taesd_path}, + {"control_net", "ControlNet", {"controlnet"}, &SDContextParams::control_net_path}, + {"ip_adapter", "IP-Adapter", {"ipadapter"}, &SDContextParams::ip_adapter_path}, + {"motion_module", "Motion Module", {"motion_modules"}, &SDContextParams::motion_module_path}, + {"photo_maker", "PhotoMaker", {"photomaker"}, &SDContextParams::photo_maker_path}, + {"pulid_weights", "PuLID", {"pulid"}, &SDContextParams::pulid_weights_path}, +}; + +static const ModelSlot* find_model_slot(const std::string& id) { + for (const auto& slot : k_model_slots) { + if (id == slot.id) { + return &slot; + } + } + return nullptr; +} + +static bool matches_model_category(const ModelSlot& slot, const std::string& category) { + return std::find(slot.categories.begin(), slot.categories.end(), category) != slot.categories.end(); +} + +static void clear_model_paths(SDContextParams& params) { + for (const auto& slot : k_model_slots) { + (params.*slot.field).clear(); + } +} + +std::string model_path_key(const fs::path& path) { +#ifdef _WIN32 + const auto value = path.generic_wstring(); + const int length = LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, value.data(), + static_cast(value.size()), nullptr, 0, nullptr, nullptr, 0); + if (length > 0) { + std::wstring folded(length, L'\0'); + LCMapStringEx(LOCALE_NAME_INVARIANT, LCMAP_LOWERCASE, value.data(), + static_cast(value.size()), &folded[0], length, nullptr, nullptr, 0); + return fs::path(folded).generic_u8string(); + } +#endif + return path.generic_u8string(); +} + +bool model_path_inside(const fs::path& root, const fs::path& path) { + auto parent = root.begin(); + auto child = path.begin(); + for (; parent != root.end(); ++parent, ++child) { + if (child == path.end() || model_path_key(*parent) != model_path_key(*child)) { + return false; + } + } + return child != path.end(); +} + +ModelRoot make_model_root(const fs::path& path, bool primary) { + ModelRoot root; + const auto absolute = fs::absolute(path); + std::error_code ec; + root.path = fs::weakly_canonical(absolute, ec); + if (ec) + root.path = absolute.lexically_normal(); + root.primary = primary; + uint64_t hash = 14695981039346656037ULL; + for (unsigned char c : model_path_key(root.path)) { + hash ^= c; + hash *= 1099511628211ULL; + } + std::ostringstream id; + id << '@' << std::hex << std::setfill('0') << std::setw(16) << hash; + root.id = id.str(); + root.label = root.path.filename().u8string() + " [" + root.id.substr(1, 8) + "]"; + return root; +} + +std::vector get_model_roots(const SDSvrParams& params) { + const auto directories = params.model_dirs.empty() ? std::vector{"models"} : params.model_dirs; + std::vector roots; + std::set seen; + for (size_t i = 0; i < directories.size(); ++i) { + auto root = make_model_root(fs::u8path(directories[i]), i == 0); + if (seen.insert(model_path_key(root.path)).second) + roots.push_back(std::move(root)); + } + return roots; +} + +std::vector model_category_directories(const ModelRoot& root, const std::string& category) { + std::vector directories; + if (!fs::exists(root.path)) + return directories; + for (const auto& entry : fs::directory_iterator(root.path)) { + auto name = entry.path().filename().u8string(); + std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) { return std::tolower(c); }); + if (name == category || (category == "checkpoints" && name == "stable-diffusion") || + (category == "loras" && name == "lora")) { + directories.push_back(entry.path()); + } + } + std::sort(directories.begin(), directories.end(), [&](const fs::path& a, const fs::path& b) { + const bool a_standard = a.filename().u8string() == category; + const bool b_standard = b.filename().u8string() == category; + return a_standard != b_standard ? a_standard : a < b; + }); + return directories; +} + +static bool valid_identifier(const std::string& id) { + return !id.empty() && id.size() <= 128 && + std::all_of(id.begin(), id.end(), [](unsigned char c) { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'; + }); +} + +static void check_keys(const json& object, const std::set& keys) { + if (!object.is_object()) { + throw std::runtime_error("expected an object"); + } + for (auto it = object.begin(); it != object.end(); ++it) { + if (!keys.count(it.key())) { + throw std::runtime_error("unknown field: " + it.key()); + } + } +} + +static void apply_recipe_defaults(const json& defaults, SDGenerationParams& params) { + check_keys(defaults, {"width", "height", "steps", "cfg_scale", "strength", "distilled_guidance", "sampling_method", "scheduler", "custom_sigmas", + "video_frames", "fps", "flow_shift", "moe_boundary", "high_noise_steps", "high_noise_cfg_scale", + "high_noise_flow_shift", "high_noise_sampling_method", "high_noise_scheduler", "img_cfg_scale", "shifted_timestep"}); + for (const char* key : {"width", "height", "steps", "video_frames", "fps", "high_noise_steps", "shifted_timestep"}) { + if (!defaults.contains(key)) { + continue; + } + if (!defaults[key].is_number_integer()) { + throw std::runtime_error(std::string(key) + " must be an integer"); + } + const int64_t value = defaults[key].get(); + const std::string field = key; + const bool dimension = field == "width" || field == "height"; + const int64_t maximum = dimension || field == "video_frames" ? 4096 : field == "fps" ? 240 + : 1000; + const int64_t minimum = dimension ? 64 : field == "shifted_timestep" ? 0 + : 1; + if ((value < minimum || value > maximum) && !(field == "high_noise_steps" && value == -1)) { + throw std::runtime_error(std::string(key) + " is out of range"); + } + if (std::string(key) == "width") + params.width = static_cast(value); + if (std::string(key) == "height") + params.height = static_cast(value); + if (std::string(key) == "steps") + params.sample_params.sample_steps = static_cast(value); + if (field == "shifted_timestep") + params.sample_params.shifted_timestep = static_cast(value); + if (field == "video_frames") + params.video_frames = static_cast(value); + if (field == "fps") + params.fps = static_cast(value); + if (field == "high_noise_steps") + params.high_noise_sample_params.sample_steps = static_cast(value); + } + for (const char* key : {"cfg_scale", "strength", "moe_boundary", "img_cfg_scale", "distilled_guidance", "flow_shift", "high_noise_cfg_scale", "high_noise_flow_shift"}) { + if (!defaults.contains(key)) { + continue; + } + if (!defaults[key].is_number()) { + throw std::runtime_error(std::string(key) + " must be a number"); + } + const float value = defaults[key].get(); + if (!std::isfinite(value) || value < 0 || value > ((std::string(key) == "strength" || std::string(key) == "moe_boundary") ? 1.f : 1000.f)) { + throw std::runtime_error(std::string(key) + " is out of range"); + } + if (std::string(key) == "strength") + params.strength = value; + if (std::string(key) == "moe_boundary") + params.moe_boundary = value; + if (std::string(key) == "cfg_scale") + params.sample_params.guidance.txt_cfg = value; + if (std::string(key) == "img_cfg_scale") + params.sample_params.guidance.img_cfg = value; + if (std::string(key) == "distilled_guidance") + params.sample_params.guidance.distilled_guidance = value; + if (std::string(key) == "flow_shift") + params.sample_params.flow_shift = value; + if (std::string(key) == "high_noise_cfg_scale") + params.high_noise_sample_params.guidance.txt_cfg = value; + if (std::string(key) == "high_noise_flow_shift") + params.high_noise_sample_params.flow_shift = value; + } + if (defaults.contains("custom_sigmas")) { + const auto& sigmas = defaults.at("custom_sigmas"); + if (!sigmas.is_array() || sigmas.size() == 1 || sigmas.size() > 1001) + throw std::runtime_error("custom_sigmas must be empty or contain 2-1001 values"); + for (const auto& sigma : sigmas) { + if (!sigma.is_number() || !std::isfinite(sigma.get()) || sigma.get() < 0) + throw std::runtime_error("custom_sigmas must contain finite nonnegative numbers"); + } + params.custom_sigmas = sigmas.get>(); + } + for (const char* key : {"sampling_method", "high_noise_sampling_method"}) { + if (!defaults.contains(key)) + continue; + auto& sample = std::string(key) == "sampling_method" ? params.sample_params : params.high_noise_sample_params; + const auto name = defaults.at(key).get(); + bool found = false; + for (int i = 0; i < SAMPLE_METHOD_COUNT; ++i) { + if (name == sd_sample_method_name(static_cast(i))) { + sample.sample_method = static_cast(i); + found = true; + break; + } + } + if (!found) + throw std::runtime_error(std::string("unknown ") + key); + } + for (const char* key : {"scheduler", "high_noise_scheduler"}) { + if (!defaults.contains(key)) + continue; + auto& sample = std::string(key) == "scheduler" ? params.sample_params : params.high_noise_sample_params; + const auto name = defaults.at(key).get(); + bool found = false; + for (int i = 0; i < SCHEDULER_COUNT; ++i) { + if (name == sd_scheduler_name(static_cast(i)) || + (name == "normal" && i == DISCRETE_SCHEDULER)) { + sample.scheduler = static_cast(i); + found = true; + break; + } + } + if (!found) + throw std::runtime_error(std::string("unknown ") + key); + } +} + +static void apply_recipe_context(const json& settings, SDContextParams& params) { + check_keys(settings, {"vae_format", "model_args"}); + if (settings.contains("vae_format")) { + const auto format = settings.at("vae_format").get(); + if (format != "auto" && format != "flux" && format != "sd3" && format != "flux2" && format != "wan") + throw std::runtime_error("unknown vae_format"); + params.vae_format = format; + } + if (settings.contains("model_args")) { + params.model_args = settings.at("model_args").get(); + } +} + +static const std::map k_recipe_features = { + {"img2img", true}, + {"mask_image", true}, + {"lora", true}, + {"vae_tiling", true}, + {"cache", true}, + {"cache_dit", true}, + {"preview", true}, + {"flow_shift", false}, + {"distilled_guidance", false}, + {"shifted_timestep", false}, + {"img_cfg", false}, + {"slg", false}, + {"clip_skip", false}, + {"ref_images", false}, + {"controlnet", false}, + {"control_image", false}, + {"ip_adapter", false}, + {"init_image", false}, + {"end_image", false}, + {"control_frames", false}, + {"vace", false}, + {"high_noise", false}, + {"cache_unet", false}, + {"preview_tae", false}, +}; + +static void validate_recipe_features(const json& recipe) { + for (const char* key : {"features", "disabled_features"}) { + if (!recipe.contains(key)) + continue; + const auto& values = recipe.at(key); + if (!values.is_array() || values.size() > k_recipe_features.size()) + throw std::runtime_error(std::string(key) + " must be an array of feature names"); + std::set seen; + for (const auto& value : values) { + const auto name = value.get(); + if (!k_recipe_features.count(name) || !seen.insert(name).second) + throw std::runtime_error("unknown or duplicate feature: " + name); + } + } +} + +json resolve_recipe_features(const json& recipe) { + json features = k_recipe_features; + for (const auto& slot : recipe.at("slots")) { + const auto id = slot.at("id").get(); + if (id == "ip_adapter") + features["ip_adapter"] = true; + if (id == "control_net") + features["controlnet"] = true; + if (id == "high_noise_diffusion_model") + features["high_noise"] = true; + if (id == "tae" || id == "taesd") + features["preview_tae"] = true; + } + for (const auto& name : recipe.value("features", json::array())) + features[name.get()] = true; + for (const auto& name : recipe.value("disabled_features", json::array())) + features[name.get()] = false; + return features; +} + +json model_generation_features(const ServerRuntime& runtime) { + if (!runtime.sd_ctx || runtime.models->active_recipe.is_null()) { + json features = k_recipe_features; + for (auto& feature : features) + feature = false; + return features; + } + json features = resolve_recipe_features(runtime.models->active_recipe); + if (runtime.models->active_recipe.value("source", "") == "command_line") + return features; + // Optional components only become usable once selected and loaded. + features["ip_adapter"] = features.value("ip_adapter", false) && !runtime.ctx_params->ip_adapter_path.empty(); + features["controlnet"] = features.value("controlnet", false) && !runtime.ctx_params->control_net_path.empty(); + features["high_noise"] = features.value("high_noise", false) && !runtime.ctx_params->high_noise_diffusion_model_path.empty(); + features["preview_tae"] = features.value("preview_tae", false) && !runtime.ctx_params->taesd_path.empty(); + if (!runtime.ctx_params->pulid_weights_path.empty()) + features["slg"] = false; + return features; +} + +static void validate_recipe(const json& recipe) { + check_keys(recipe, {"schema_version", "id", "name", "description", "model_url", "slots", "required_any", "defaults", "context", "features", "disabled_features"}); + if (!recipe.at("schema_version").is_number_integer() || recipe.at("schema_version") != 1 || + !valid_identifier(recipe.at("id").get()) || recipe.at("name").get().empty()) { + throw std::runtime_error("invalid schema_version, id or name"); + } + if (recipe.contains("description") && !recipe["description"].is_string()) { + throw std::runtime_error("description must be a string"); + } + if (recipe.contains("model_url")) { + const auto url = recipe.at("model_url").get(); + const std::string prefix = "https://huggingface.co/"; + if (url.size() <= prefix.size() || url.size() > 2048 || url.rfind(prefix, 0) != 0 || + std::any_of(url.begin(), url.end(), [](unsigned char c) { return std::isspace(c) || std::iscntrl(c); })) { + throw std::runtime_error("model_url must be a Hugging Face HTTPS URL"); + } + } + const auto& slots = recipe.at("slots"); + if (!slots.is_array() || slots.empty() || slots.size() > 32) { + throw std::runtime_error("slots must be a nonempty array of at most 32 entries"); + } + std::set ids; + bool has_required_model = false; + for (const auto& slot : slots) { + check_keys(slot, {"id", "label", "category", "required"}); + const auto id = slot.at("id").get(); + const auto* spec = find_model_slot(id); + if (!spec || !ids.insert(id == "taesd" ? "tae" : id).second || + (slot.contains("category") && !matches_model_category(*spec, slot.at("category").get())) || + (slot.contains("label") && slot.at("label").get().empty()) || !slot.at("required").is_boolean()) { + throw std::runtime_error("invalid or duplicate model slot: " + id); + } + has_required_model |= (id == "model" || id == "diffusion_model") && slot["required"].get(); + } + if (!has_required_model) { + throw std::runtime_error("a required model or diffusion_model slot is needed"); + } + if (recipe.contains("required_any")) { + const auto& groups = recipe.at("required_any"); + if (!groups.is_array() || groups.size() > 32) + throw std::runtime_error("required_any must be an array of at most 32 slot groups"); + for (const auto& group : groups) { + if (!group.is_array() || group.size() < 2 || group.size() > slots.size()) + throw std::runtime_error("required_any groups need at least two declared slots"); + std::set seen; + for (const auto& value : group) { + const auto id = value.get(); + if (!seen.insert(id).second || std::none_of(slots.begin(), slots.end(), [&](const json& slot) { return slot.at("id") == id; })) + throw std::runtime_error("unknown or duplicate required_any slot: " + id); + } + } + } + validate_recipe_features(recipe); + SDContextParams context; + apply_recipe_context(recipe.value("context", json::object()), context); + SDGenerationParams defaults; + apply_recipe_defaults(recipe.value("defaults", json::object()), defaults); +} + +static json normalize_recipe(json recipe) { + validate_recipe(recipe); + recipe["effective_features"] = resolve_recipe_features(recipe); + for (auto& slot : recipe.at("slots")) { + const auto& spec = *find_model_slot(slot.at("id").get()); + slot["label"] = slot.value("label", std::string(spec.label)); + slot["category"] = slot.value("category", spec.categories.front()); + slot["categories"] = spec.categories; + } + return recipe; +} + +static bool supported_model_file(const fs::path& path, const std::string& category) { + std::string ext = path.extension().u8string(); + std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return std::tolower(c); }); + if (category == "tokenizers") { + return ext == ".json"; + } + return ext == ".gguf" || ext == ".safetensors" || ext == ".sft" || + ext == ".ckpt" || ext == ".pt" || ext == ".pth" || ext == ".bin"; +} + +void refresh_model_catalog(ServerRuntime& runtime) { + auto& models = *runtime.models; + std::lock_guard lock(models.catalog_mutex); + std::map recipes; + std::map files; + json errors = json::array(); + auto report = [&](const std::string& file, const std::string& message) { + errors.push_back({{"file", file}, {"message", message}}); + }; + for (const char* builtin : k_builtin_recipes) { + auto recipe = normalize_recipe(json::parse(builtin)); + const auto id = recipe.at("id").get(); + recipe["source"] = "builtin"; + recipes[id] = std::move(recipe); + } + + std::set seen_files; + for (const auto& source : get_model_roots(*runtime.svr_params)) { + const auto& root = source.path; + auto display_path = [&](const fs::path& path) { + return source.id + "/" + path.lexically_relative(root).generic_u8string(); + }; + const fs::path recipe_dir = root / "recipes"; + std::map> user_recipes; + try { + if (fs::exists(recipe_dir)) { + if (!model_path_inside(root, fs::canonical(recipe_dir))) + throw std::runtime_error("recipes directory escapes model directory"); + for (const auto& entry : fs::directory_iterator(recipe_dir)) { + const std::string display = display_path(entry.path()); + if (entry.path().extension() != ".json") + continue; + try { + if (!entry.is_regular_file() || !model_path_inside(root, fs::canonical(entry.path()))) + throw std::runtime_error("recipe must be a file inside the model directory"); + if (entry.file_size() > 1024 * 1024) + throw std::runtime_error("recipe exceeds 1 MiB"); + std::ifstream input(entry.path(), std::ios::binary); + json recipe; + input >> recipe; + recipe = normalize_recipe(std::move(recipe)); + recipe["source"] = "user"; + recipe["file"] = entry.path().filename().u8string(); + recipe["root"] = source.id; + const auto id = recipe.at("id").get(); + user_recipes[id].push_back(std::move(recipe)); + } catch (const std::exception& e) { + report(display, e.what()); + } + } + } + } catch (const std::exception& e) { + report(display_path(recipe_dir), e.what()); + } + for (auto& item : user_recipes) { + if (item.second.size() != 1) { + for (const auto& recipe : item.second) + report(source.id + "/recipes/" + recipe["file"].get(), "duplicate user recipe id: " + item.first); + continue; + } + const auto existing = recipes.find(item.first); + if (existing != recipes.end()) { + if (existing->second["source"] == "user") { + LOG_WARN("recipe '%s' from %s is shadowed by an earlier model directory", item.first.c_str(), source.label.c_str()); + continue; + } + LOG_INFO("user recipe '%s' from %s overrides the builtin", item.first.c_str(), source.label.c_str()); + } + recipes[item.first] = std::move(item.second.front()); + } + + std::set categories; + for (const auto& slot : k_model_slots) + categories.insert(slot.categories.begin(), slot.categories.end()); + for (const auto& category : categories) { + try { + for (const auto& directory : model_category_directories(source, category)) { + if (!model_path_inside(root, fs::canonical(directory))) { + report(display_path(directory), "category directory escapes model directory"); + continue; + } + for (fs::recursive_directory_iterator it(directory), end; it != end; ++it) { + std::error_code path_error; + const fs::path canonical = fs::canonical(it->path(), path_error); + if (path_error || !model_path_inside(root, canonical)) { + it.disable_recursion_pending(); + report(display_path(it->path()), path_error ? path_error.message() : "path escapes model directory"); + continue; + } + if (!it->is_regular_file() || !supported_model_file(it->path(), category) || + !seen_files.insert(model_path_key(canonical)).second) + continue; + const auto relative = it->path().lexically_relative(root).generic_u8string(); + const auto id = source.primary ? relative : source.id + "/" + relative; + files[id] = {category, it->path().lexically_relative(directory).generic_u8string(), relative, source}; + } + } + } catch (const std::exception& e) { + report(source.id + "/" + category, e.what()); + } + } + } + std::map, size_t> names; + for (const auto& entry : files) + ++names[{entry.second.category, entry.second.name}]; + for (auto& entry : files) { + auto& file = entry.second; + if (names[{file.category, file.name}] > 1) + file.name = (file.root.path / fs::u8path(file.relative_path)).generic_u8string(); + } + models.recipes = std::move(recipes); + models.files = std::move(files); + models.catalog_errors = std::move(errors); +} + +json model_status(ServerRuntime& runtime) { + std::lock_guard lock(runtime.models->status_mutex); + auto& models = *runtime.models; + json status = models.status; + if (status["state"] == "loading" && !status["load_elapsed_seconds"].is_null()) { + status["load_elapsed_seconds"] = std::chrono::duration(std::chrono::steady_clock::now() - models.load_started_at).count(); + } + return status; +} + +void initialize_server_models(ServerRuntime& runtime) { + auto& models = *runtime.models; + models.base_context = *runtime.ctx_params; + clear_model_paths(models.base_context); + models.base_defaults = *runtime.default_gen_params; + if (runtime.sd_ctx) { + models.status["state"] = "ready"; + models.status["origin"] = "command_line"; + json features = json::array(); + for (const auto& feature : k_recipe_features) + features.push_back(feature.first); + models.active_recipe = { + {"id", "@command-line"}, + {"name", "Command-line model (all features)"}, + {"source", "command_line"}, + {"description", "Uses the loaded command-line model and its generation defaults."}, + {"slots", json::array()}, + {"defaults", json::object()}, + {"features", features}, + }; + models.active_recipe["effective_features"] = resolve_recipe_features(models.active_recipe); + models.status["runtime_recipe"] = models.active_recipe; + } + refresh_model_catalog(runtime); +} + +void prepare_model_load(ServerRuntime& runtime, const json& body, SDContextParams& context, SDGenerationParams& defaults, json* loaded_recipe) { + check_keys(body, {"recipe", "models"}); + const auto recipe_id = body.at("recipe").get(); + auto selection = body.at("models"); + if (!selection.is_object()) + throw std::runtime_error("models must be an object"); + auto& models = *runtime.models; + std::lock_guard lock(models.catalog_mutex); + const auto found = models.recipes.find(recipe_id); + if (found == models.recipes.end()) + throw std::runtime_error("unknown recipe"); + const auto& recipe = found->second; + if (loaded_recipe) + *loaded_recipe = recipe; + context = models.base_context; + defaults = models.base_defaults; + apply_recipe_context(recipe.value("context", json::object()), context); + apply_recipe_defaults(recipe.value("defaults", json::object()), defaults); + std::set allowed; + for (const auto& slot : recipe.at("slots")) { + const auto id = slot.at("id").get(); + allowed.insert(id); + if (id == "tae" || id == "taesd") { + const std::string alias = id == "tae" ? "taesd" : "tae"; + allowed.insert(alias); + if (selection.contains(alias)) { + if (selection.contains(id)) + throw std::runtime_error("specify only one of tae and taesd"); + selection[id] = selection.at(alias); + } + } + const std::string file_id = selection.value(id, std::string()); + if (file_id.empty()) { + if (slot.at("required").get()) + throw std::runtime_error("missing required model: " + id); + continue; + } + const auto file = models.files.find(file_id); + if (file == models.files.end() || !matches_model_category(*find_model_slot(id), file->second.category)) { + throw std::runtime_error("unknown model or incorrect category: " + id); + } + const auto& root = file->second.root.path; + const fs::path path = fs::canonical(root / fs::u8path(file->second.relative_path)); + if (!model_path_inside(root, path) || !fs::is_regular_file(path)) { + throw std::runtime_error("model file must remain inside the model directory: " + id); + } + context.*find_model_slot(id)->field = path.u8string(); + } + for (auto it = selection.begin(); it != selection.end(); ++it) { + if (!allowed.count(it.key())) + throw std::runtime_error("unknown model slot: " + it.key()); + } + for (const auto& group : recipe.value("required_any", json::array())) { + if (std::none_of(group.begin(), group.end(), [&](const json& value) { + return !selection.value(value.get(), std::string()).empty(); + })) { + throw std::runtime_error("select at least one model from: " + group.dump()); + } + } + if (!context.resolve_and_validate(IMG_GEN)) + throw std::runtime_error("invalid model context parameters"); +} diff --git a/examples/server/model_catalog.h b/examples/server/model_catalog.h new file mode 100644 index 000000000..7d49a1924 --- /dev/null +++ b/examples/server/model_catalog.h @@ -0,0 +1,56 @@ +#ifndef __SD_EXAMPLES_SERVER_MODEL_CATALOG_H__ +#define __SD_EXAMPLES_SERVER_MODEL_CATALOG_H__ + +#include +#include +#include +#include +#include + +#include "runtime.h" + +struct ModelRoot { + std::filesystem::path path; + std::string id; + std::string label; + bool primary = false; +}; + +struct ModelFile { + std::string category; + std::string name; + std::string relative_path; + ModelRoot root; +}; + +std::string model_path_key(const std::filesystem::path& path); +bool model_path_inside(const std::filesystem::path& root, const std::filesystem::path& path); +ModelRoot make_model_root(const std::filesystem::path& path, bool primary); +std::vector get_model_roots(const SDSvrParams& params); +std::vector model_category_directories(const ModelRoot& root, const std::string& category); + +struct ServerModels { + std::shared_mutex runtime_mutex; + std::mutex catalog_mutex; + std::mutex status_mutex; + SDCtxPtr context; + SDContextParams base_context; + SDGenerationParams base_defaults; + std::map recipes; + std::map files; + std::chrono::steady_clock::time_point load_started_at; + json active_recipe = nullptr; + json catalog_errors = json::array(); + json status = {{"state", "empty"}, {"recipe", nullptr}, {"models", json::object()}, {"error", nullptr}, {"load_elapsed_seconds", nullptr}}; +}; + +void refresh_model_catalog(ServerRuntime& runtime); +json model_status(ServerRuntime& runtime); +void initialize_server_models(ServerRuntime& runtime); + +json resolve_recipe_features(const json& recipe); +json model_generation_features(const ServerRuntime& runtime); + +void prepare_model_load(ServerRuntime& runtime, const json& body, SDContextParams& context, SDGenerationParams& defaults, json* loaded_recipe = nullptr); + +#endif diff --git a/examples/server/recipes/README.md b/examples/server/recipes/README.md new file mode 100644 index 000000000..5043f6d1b --- /dev/null +++ b/examples/server/recipes/README.md @@ -0,0 +1,79 @@ +# Built-in recipe policy + +Read these rules before adding or changing JSON files in this directory. A recipe +represents a supported model-loading and capability configuration, not a saved +prompt, a style preset, or a collection of sampling settings. + +## Required rules + +1. **Community editing adapters get a separate recipe.** When the base model + does not natively support instruction/reference-image editing and needs a + community LoRA to gain it, provide a dedicated editing recipe. Keep editing + inputs disabled in the base recipe. Examples: `anima` / `anima-edit` and + `krea2-raw` / `krea2-raw-edit`. +2. **Split different task weights; share native multi-task weights.** If editing + and text-to-image use different generation checkpoints, use separate recipes. + If the same checkpoint natively supports both, use one recipe. Examples: + `boogu-image` / `boogu-image-edit` are separate; `flux2-dev` and + `qwen-image-21` each cover both generation and editing. Apply the same rule + to video: separate HunyuanVideo 1.5 T2V/I2V and Wan 2.2 A14B T2V/I2V weights, + but share one `wan22-ti2v-5b` recipe for its native T2V/I2V checkpoint. +3. **Capability-extending weights get a separate recipe.** ControlNet, + IP-Adapter, PuLID, PhotoMaker, motion modules, and similar adapters that add + a distinct conditioning or generation capability belong in dedicated + recipes. Make the extension's model slots required when the schema provides + them. Do not add these as optional slots on the ordinary base recipe. +4. **A LoRA alone is not a reason to split.** If adding a LoRA does not change + supported functionality, reuse the base recipe. Style, character, quality, + acceleration and distillation LoRAs do not get dedicated built-in recipes. + Users select the LoRA and change steps, guidance or other generation settings + separately. Standalone base/turbo/distilled checkpoints still follow the task, + component and layer-count rules; a different filename alone is not a reason + for another recipe. +5. **Keep native components in the model's recipe.** Native vision encoders, + projectors, connectors and other components required by the model belong in + its existing recipe. Splitting a native component into another file does not + make it a capability-extending adapter under rule 3. For example, Qwen vision + projectors and LLaDA/LTX connectors do not get separate recipes. A slot may be + optional when its weights can be bundled in another selected file. +6. **Layer-count variants share the family recipe.** Adding, repeating, removing + or pruning model layers does not justify a separate recipe when the runtime + supports the variant with the same components and capabilities. This also + applies to distilled checkpoints whose relevant difference is reduced depth. + Use `flux1-dev` for FLUX.1 Lite, `sdxl-checkpoint` for SSD-1B and Segmind Vega, + and the corresponding SD 1/2 checkpoint recipes for Tiny SD/Tiny SD2. Only an + independent difference in task, required component compatibility or latent + format can justify splitting such a variant; document that difference. + +## Interpretation and review + +- Instruction/reference-image editing is distinct from generic image-to-image + denoising. The shared `img2img` capability does not imply native editing. +- Native components and external capability adapters are distinguished by their + role, not by whether their weights are stored in a separate file. LLaDA's + SigVQ-containing connectors remain in its shared generation/editing recipe. +- Do not merge models solely because they descend from the same family. Flex.2 + has different native inpainting/control conditioning, beyond its layer count + ([model card](https://huggingface.co/ostris/Flex.2-preview)). SDXS uses an + adjusted AutoEncoderTiny and a one-step model, rather than only a shallower + ordinary SD checkpoint ([runtime guide](../../../docs/distilled_sd.md#sdxs-512-dreamshaper)). + Their native components stay in their own model recipe. +- Quantization, file format, prompt wording, seeds, step counts and CFG values + are not grounds for another recipe. Differences in required components, + latent formats or supported capabilities must be documented. +- Evaluate what an adapter actually changes. Do not infer new functionality just + from a LoRA's name or from the presence of another weight file. In particular, + do not create an Omni-Kontext recipe solely because a LoRA is selected. +- A recipe does not load generation LoRAs automatically. Community-edit recipes + must explain that the matching LoRA is required and must be selected separately. +- Only advertise features implemented by the current runtime. Check the relevant + model documentation and code, and match each task's slots and feature flags to + its weights. Record any unverified inference paths in the change description. +- Before submitting, validate all recipe JSON, inspect the embedded recipe API, + and check that task-specific inputs appear only on the appropriate recipes. + A catalog check is not a substitute for model-generation verification. + +These are built-in catalog rules. Users may keep personal presets in +`/recipes/`; they need not add those presets to the built-in list. +See [the recipe schema and feature reference](../api.md#user-recipe-format) +for supported fields. diff --git a/examples/server/recipes/anima-edit.json b/examples/server/recipes/anima-edit.json new file mode 100644 index 000000000..f22c1b6cc --- /dev/null +++ b/examples/server/recipes/anima-edit.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "id": "anima-edit", + "name": "Anima Edit (LoRA)", + "description": "Requires a matching community editing LoRA. Apply it after loading and supply reference images; uses the cosmos_reference preset.", + "model_url": "https://huggingface.co/circlestone-labs/Anima", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 0.6B Base Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "scheduler": "discrete", + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/anima.json b/examples/server/recipes/anima.json new file mode 100644 index 000000000..4809bd4bf --- /dev/null +++ b/examples/server/recipes/anima.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "anima", + "name": "Anima", + "description": "Supports all Anima variants supported by sd.cpp. Use a compatible Qwen Image VAE or TAE; adjust generation settings for distilled variants.", + "model_url": "https://huggingface.co/circlestone-labs/Anima", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 0.6B Base Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/animatediff-sd15.json b/examples/server/recipes/animatediff-sd15.json new file mode 100644 index 000000000..a7c8338f1 --- /dev/null +++ b/examples/server/recipes/animatediff-sd15.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "id": "animatediff-sd15", + "name": "AnimateDiff SD 1.5", + "description": "Use Video Generation with an SD 1.5 checkpoint and compatible motion module.", + "model_url": "https://huggingface.co/guoyww/animatediff", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "motion_module", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 8, + "fps": 8, + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip", + "init_image" + ], + "disabled_features": [ + "cache_dit", + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/boogu-image-edit.json b/examples/server/recipes/boogu-image-edit.json new file mode 100644 index 000000000..ff1aaeefb --- /dev/null +++ b/examples/server/recipes/boogu-image-edit.json @@ -0,0 +1,50 @@ +{ + "schema_version": 1, + "id": "boogu-image-edit", + "name": "Boogu Image Edit", + "description": "Supply reference images. The separate vision projector is needed only when it is not bundled in the text encoder.", + "model_url": "https://huggingface.co/Boogu/Boogu-Image-0.1-Edit", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 8B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024, + "img_cfg_scale": 1, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/boogu-image.json b/examples/server/recipes/boogu-image.json new file mode 100644 index 000000000..24c6a42b2 --- /dev/null +++ b/examples/server/recipes/boogu-image.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "boogu-image", + "name": "Boogu Image Base", + "model_url": "https://huggingface.co/Boogu/Boogu-Image-0.1-Base", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 8B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/chroma-radiance.json b/examples/server/recipes/chroma-radiance.json new file mode 100644 index 000000000..6a0be8a56 --- /dev/null +++ b/examples/server/recipes/chroma-radiance.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "id": "chroma-radiance", + "name": "Chroma Radiance", + "description": "Pixel-space generation without a VAE. Also accepts the x0 and experimental patch-32 variants.", + "model_url": "https://huggingface.co/lodestones/Chroma1-Radiance", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true + } + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 3.5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "slg" + ], + "disabled_features": [ + "preview", + "vae_tiling" + ] +} diff --git a/examples/server/recipes/chroma.json b/examples/server/recipes/chroma.json new file mode 100644 index 000000000..b8d1da080 --- /dev/null +++ b/examples/server/recipes/chroma.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "chroma", + "name": "Chroma", + "model_url": "https://huggingface.co/lodestones/Chroma1-HD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 3, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/cosxl-edit.json b/examples/server/recipes/cosxl-edit.json new file mode 100644 index 000000000..49407fccd --- /dev/null +++ b/examples/server/recipes/cosxl-edit.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "cosxl-edit", + "name": "CosXL Edit", + "description": "Supply the image to edit as a reference image.", + "model_url": "https://huggingface.co/stabilityai/cosxl", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "img_cfg_scale": 1.5, + "sampling_method": "euler_a", + "scheduler": "karras" + }, + "features": [ + "cache_unet", + "clip_skip", + "img_cfg", + "ref_images", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/defaults.md b/examples/server/recipes/defaults.md new file mode 100644 index 000000000..2be4dfbf6 --- /dev/null +++ b/examples/server/recipes/defaults.md @@ -0,0 +1,124 @@ +# Built-in generation defaults + +Reviewed against model-publisher cards and reference implementations on 2026-10-20. + +These are starting values, not limits on supported resolutions or generation settings. +Built-in step defaults above 20 are reduced to 20; the cited sources may recommend more steps. +Users can still choose a higher step count. +When several aspect ratios or ranges are published, the selected example or square bucket is noted below. +A dash means no recipe override: the available primary sources did not establish a fixed value, or the size depends on the input image. +Ordinary DiT recipes explicitly use Euler; PiD retains its LCM sampling path. +Guidance-distilled models use `cfg_scale: 1` to disable external CFG in sd.cpp. FLUX embedded guidance is stored separately as `distilled_guidance`. +For community edit adapters and other optional extensions, these are the base-model settings unless the source specifies otherwise. + +| Recipe | CFG | Steps | Width × height | Source / notes | +| --- | --- | --- | --- | --- | +| `anima` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/circlestone-labs/Anima) The official ranges are 30–50 steps / CFG 4–5; CFG 4 selects the lower end. Edit uses the same base; tune the chosen editing LoRA separately. | +| `anima-edit` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/circlestone-labs/Anima) The official ranges are 30–50 steps / CFG 4–5; CFG 4 selects the lower end. Edit uses the same base; tune the chosen editing LoRA separately. | +| `animatediff-sd15` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/guoyww/AnimateDiff/main/configs/prompts/1_animate/1_3_animate_ToonYou.yaml) | +| `boogu-image` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/Boogu/Boogu-Image-0.1-Base) | +| `boogu-image-edit` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/Boogu/Boogu-Image-0.1-Edit) | +| `chroma` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/lodestones/Chroma1-HD/raw/main/README.md) | +| `chroma-radiance` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/lodestones/Chroma1-Radiance/raw/main/inference_reference_code.py) | +| `cosxl-edit` | — | — | — | [Source](https://huggingface.co/stabilityai/cosxl) No numerical generation defaults in the author’s model card; previous test values were removed. | +| `ernie-image` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/baidu/ERNIE-Image) | +| `ernie-image-turbo` | 1 | 8 | 1024 × 1024 | [Source](https://huggingface.co/baidu/ERNIE-Image-Turbo) | +| `flex2` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/ostris/Flex.2-preview) | +| `flux1-canny` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev) | +| `flux1-depth` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev) | +| `flux1-dev` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-dev) | +| `flux1-fill` | 1 | 20 | 1232 × 1632 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev) | +| `flux1-kontext` | 1 | 20 | — | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/flux/pipeline_flux_kontext.py) Output size normally follows the reference image; no fixed size is imposed by this recipe. | +| `flux1-pulid` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-dev) FLUX.1-dev defaults; identity conditioning is provided by PuLID. | +| `flux1-schnell` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.1-schnell) | +| `flux2-dev` | 1 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-dev) | +| `flux2-klein-4b` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) | +| `flux2-klein-9b` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-9B) | +| `flux2-klein-base-4b` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B) | +| `flux2-klein-base-9b` | 4 | 20 | 1024 × 1024 | [Source](https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B) | +| `flux3-klein` | 1 | 16 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/black-forest-labs/flux-3-image-klein/main/README.md) | +| `flux3-klein-base` | 4 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/black-forest-labs/flux-3-image-klein/main/src/flux3/pipeline.py) | +| `hidream-o1-dev` | 1 | 20 | 2048 × 2048 | [Source](https://huggingface.co/HiDream-ai/HiDream-O1-Image-Dev) | +| `hunyuan-video-15-i2v` | 6 | 20 | 1280 × 720 | [Source](https://huggingface.co/tencent/HunyuanVideo-1.5) 720p base I2V defaults; CFG/step-distilled and 480p checkpoints use different settings. | +| `hunyuan-video-15-t2v` | 6 | 20 | 1280 × 720 | [Source](https://huggingface.co/tencent/HunyuanVideo-1.5) 720p base T2V defaults; CFG-distilled / 480p checkpoints need their published settings. | +| `ideogram4` | 7 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/ideogram-oss/ideogram4/main/docs/inference.md) Constant-guidance pipeline defaults; this recipe does not reproduce the CLI’s per-step guidance schedule. | +| `instruct-pix2pix` | — | 10 | — | [Source](https://huggingface.co/timbrooks/instruct-pix2pix) Official quick-start example; output size follows the input image. | +| `iris-3b` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/speridlabs/iris-3b) | +| `krea2-raw` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/krea/Krea-2-Raw) Base model defaults; edit requires a matching community LoRA with its own settings. | +| `krea2-raw-edit` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/krea/Krea-2-Raw) Base model defaults; edit requires a matching community LoRA with its own settings. | +| `krea2-turbo` | 1 | 8 | 2048 × 2048 | [Source](https://huggingface.co/krea/Krea-2-Turbo) Official CLI example; edit requires a matching community LoRA. | +| `krea2-turbo-edit` | 1 | 8 | 2048 × 2048 | [Source](https://huggingface.co/krea/Krea-2-Turbo) Official CLI example; edit requires a matching community LoRA. | +| `lens` | 5 | 20 | 1440 × 1440 | [Source](https://huggingface.co/microsoft/Lens) Official model card: RL model, default base_resolution 1440. | +| `lens-turbo` | 1 | 4 | 1440 × 1440 | [Source](https://huggingface.co/microsoft/Lens-Turbo) Official model card: 4-step distilled model, default base_resolution 1440. | +| `lingbot-video-dense` | 3 | 20 | 832 × 480 | [Source](https://huggingface.co/robbyant/lingbot-video-dense-1.3b) Official first-stage inference example; optional refiner is separate. | +| `lingbot-video-moe` | 3 | 20 | 832 × 480 | [Source](https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b) Official inference example. | +| `llada-image` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/inclusionAI/LLaDA-Image) | +| `llada-image-turbo` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/inclusionAI/LLaDA-Image-Turbo) | +| `longcat-image` | 4 | 20 | 1344 × 768 | [Source](https://huggingface.co/meituan-longcat/LongCat-Image) | +| `longcat-image-edit` | 4.5 | 20 | — | [Source](https://huggingface.co/meituan-longcat/LongCat-Image-Edit) Reference-image-dependent output size; no fixed size is imposed. | +| `ltx23-dev` | 3 | 20 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Official single-stage parameters, before optional latent upscaling. | +| `ltx23-distilled` | 1 | 8 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Model card specifies 8 steps / CFG 1; size uses official pipeline’s first stage. | +| `ltx25-dev` | 3 | 20 | 768 × 512 | [Source](https://raw.githubusercontent.com/Lightricks/LTX-2/main/packages/ltx-pipelines/src/ltx_pipelines/utils/constants.py) Official non-distilled pipeline parameters for model versions >= 2.4, before optional upscaling. | +| `mage-flow` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) | +| `mage-flow-edit` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) | +| `mage-flow-turbo` | 1 | 4 | 1024 × 1024 | [Source](https://huggingface.co/microsoft/Mage-Flow) | +| `ming-image` | 1 | 12 | 2048 × 2048 | [Source](https://huggingface.co/inclusionAI/Ming-Image-0.1-Design) | +| `minimax-h3-fl2va` | 1 | — | 1344 × 768 | [Source](https://huggingface.co/docs/diffusers/main/en/api/pipelines/minimax_h3) Guidance-distilled; official 768p canvas. No fixed step count is prescribed here; the upstream API counts sigma grid points rather than sd.cpp denoising steps. | +| `minimax-h3-ref2va` | 1 | — | 1344 × 768 | [Source](https://huggingface.co/docs/diffusers/main/en/api/pipelines/minimax_h3) Guidance-distilled; official 768p canvas. No fixed step count is prescribed here; the upstream API counts sigma grid points rather than sd.cpp denoising steps. | +| `minit2i` | 2.5 | 20 | 512 × 512 | [Source](https://huggingface.co/MiniT2I/MiniT2I/raw/main/pipeline.py) B/16 example and native model size; L/16 uses CFG 6. | +| `moss-image-base` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base) | +| `moss-image-sft` | 3 | 20 | 1024 × 1024 | [Source](https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT) | +| `nitrosd-realism` | 1 | 1 | 1024 × 1024 | [Source](https://huggingface.co/ChenDY/NitroFusion) | +| `ovis-image` | 5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/ATH-MaaS/Ovis-Image-7B) | +| `pid` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-15` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-15-flux2` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-15-qwen-image` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-15-sd3` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-flux2` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-qwen-image` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pid-sd3` | 1 | 4 | 2048 × 2048 | [Source](https://huggingface.co/nvidia/PiD) Standalone PiD decoding at 2K; guidance is handled by the upstream generator. | +| `pixart-alpha-1024` | 4.5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_alpha.py) | +| `pixart-alpha-512` | 4.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_alpha.py) | +| `pixart-sigma` | 4.5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/pixart_alpha/pipeline_pixart_sigma.py) | +| `qwen-image` | 4 | 20 | 1328 × 1328 | [Source](https://huggingface.co/Qwen/Qwen-Image) Official square resolution bucket; true_cfg_scale maps to cfg_scale. | +| `qwen-image-21` | 1 | 20 | 2048 × 2048 | [Source](https://raw.githubusercontent.com/QwenLM/Qwen-Image-2.1/main/README.md) Original checkpoint defaults; select the matching TAEQI2.1 to replace its VAE. | +| `qwen-image-21-turbo` | 1 | 8 | 2048 × 2048 | [Model card](https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo), [fixed sigmas](https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo/blob/main/model_index.json). Standalone Turbo weights; the fixed 8-step schedule is applied on model load. | +| `qwen-image-edit` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit) Official editing example; output dimensions follow reference-image aspect ratio. | +| `qwen-image-edit-2509` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit-2509) Official editing example; output dimensions follow reference-image aspect ratio. | +| `qwen-image-edit-2511` | 4 | 20 | — | [Source](https://huggingface.co/Qwen/Qwen-Image-Edit-2511) Official editing example; output dimensions follow reference-image aspect ratio. | +| `qwen-image-layered` | 4 | 20 | 640 × 640 | [Source](https://huggingface.co/Qwen/Qwen-Image-Layered) Official recommended 640 resolution bucket; layered RGBA VAE is required. | +| `sd-turbo` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/stabilityai/sd-turbo) | +| `sd1-checkpoint` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) | +| `sd1-controlnet` | 7.5 | 20 | 512 × 512 | [Source](https://huggingface.co/lllyasviel/control_v11p_sd15_canny) | +| `sd1-inpainting` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) | +| `sd1-ip-adapter` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) SD 1.x base pipeline defaults; adapter strength is configured separately. | +| `sd2-512-checkpoint` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) | +| `sd2-768-checkpoint` | 7.5 | 20 | 768 × 768 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion.py) | +| `sd2-inpainting` | 7.5 | 20 | 512 × 512 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion/pipeline_stable_diffusion_inpaint.py) | +| `sd3-medium` | 7 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3-medium-diffusers) | +| `sd35-large` | 3.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3.5-large) | +| `sd35-medium` | 4.5 | 20 | 1024 × 1024 | [Source](https://huggingface.co/stabilityai/stable-diffusion-3.5-medium) | +| `sdxl-checkpoint` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) | +| `sdxl-controlnet` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) SDXL pipeline defaults; compatible ControlNet weights are required. | +| `sdxl-inpainting` | 8 | 20 | 1024 × 1024 | [Source](https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1/raw/main/README.md) | +| `sdxl-ip-adapter` | 5 | 20 | 1024 × 1024 | [Source](https://raw.githubusercontent.com/huggingface/diffusers/main/src/diffusers/pipelines/stable_diffusion_xl/pipeline_stable_diffusion_xl.py) SDXL base pipeline defaults; adapter strength is configured separately. | +| `sdxl-photomaker` | — | — | 1024 × 1024 | [Source](https://huggingface.co/TencentARC/PhotoMaker) Use settings for the selected compatible SDXL checkpoint; no unique CFG/steps are prescribed by the model card. | +| `sdxl-turbo` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/stabilityai/sdxl-turbo) | +| `sdxs` | 1 | 1 | 512 × 512 | [Source](https://huggingface.co/IDKiro/sdxs-512-dreamshaper) | +| `sefi-image` | 4 | 20 | — | [Source](https://huggingface.co/SeFi-Image/SeFi-Image-5B-Base) Base/RL family defaults; Turbo checkpoints require 4 steps / CFG 1. | +| `sensenova-u15` | 4 | 20 | 2048 × 2048 | [Source](https://raw.githubusercontent.com/OpenSenseNova/SenseNova-U1/main/examples/README.md) | +| `wan21-flf2v` | 5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) | +| `wan21-i2v` | 5 | 20 | 832 × 480 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) 480p base defaults; also accepts compatible SkyReels V2 and single Wan 2.2 I2V experts. | +| `wan21-t2v` | 5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) 14B base defaults; single Wan 2.2 experts and acceleration LoRAs need matching settings. | +| `wan21-vace` | 5 | 20 | 832 × 480 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.1/main/generate.py) | +| `wan22-i2v` | 3.5 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_i2v_A14B.py) | +| `wan22-s2v` | 4.5 | 20 | — | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_s2v_14B.py) The official S2V pipeline derives output dimensions from the input. | +| `wan22-t2v` | 3 | 20 | 1280 × 720 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_t2v_A14B.py) | +| `wan22-ti2v-5b` | 5 | 20 | 1280 × 704 | [Source](https://raw.githubusercontent.com/Wan-Video/Wan2.2/main/wan/configs/wan_ti2v_5B.py) | +| `z-image` | 4 | 20 | 720 × 1280 | [Source](https://huggingface.co/Tongyi-MAI/Z-Image) | +| `z-image-l2p` | — | — | — | [Source](https://huggingface.co/zhen-nan/L2P) No numerical inference recommendations in the author’s model card; previous test values were removed. | +| `z-image-turbo` | 1 | 9 | 1024 × 1024 | [Source](https://huggingface.co/Tongyi-MAI/Z-Image-Turbo) | + +TAE alternatives use the model-compatible weights listed by [TAESD](https://github.com/madebyollin/taesd) and [TAEHV](https://github.com/madebyollin/taehv). +TAE slots are omitted for pixel-space models, PiD, layered RGBA decoding and Mage-VAE paths that the current runtime does not map to a compatible TAE. diff --git a/examples/server/recipes/ernie-image-turbo.json b/examples/server/recipes/ernie-image-turbo.json new file mode 100644 index 000000000..5702f5efc --- /dev/null +++ b/examples/server/recipes/ernie-image-turbo.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "ernie-image-turbo", + "name": "ERNIE Image Turbo", + "model_url": "https://huggingface.co/baidu/ERNIE-Image-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Ministral 3 3B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 8, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/ernie-image.json b/examples/server/recipes/ernie-image.json new file mode 100644 index 000000000..a61000008 --- /dev/null +++ b/examples/server/recipes/ernie-image.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "ernie-image", + "name": "ERNIE Image", + "model_url": "https://huggingface.co/baidu/ERNIE-Image", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Ministral 3 3B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/flex2.json b/examples/server/recipes/flex2.json new file mode 100644 index 000000000..bbd900c39 --- /dev/null +++ b/examples/server/recipes/flex2.json @@ -0,0 +1,48 @@ +{ + "schema_version": 1, + "id": "flex2", + "name": "Flex.2 Preview", + "model_url": "https://huggingface.co/ostris/Flex.2-preview", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024, + "distilled_guidance": 3.5 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/flux1-canny.json b/examples/server/recipes/flux1-canny.json new file mode 100644 index 000000000..de9bb609a --- /dev/null +++ b/examples/server/recipes/flux1-canny.json @@ -0,0 +1,53 @@ +{ + "schema_version": 1, + "id": "flux1-canny", + "name": "FLUX.1 Canny dev", + "description": "Select Canny weights and supply a precomputed edge map as Control Image. Edge detection is not performed by this recipe.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Canny-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024, + "distilled_guidance": 30 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg", + "control_image" + ], + "disabled_features": [ + "mask_image" + ] +} diff --git a/examples/server/recipes/flux1-depth.json b/examples/server/recipes/flux1-depth.json new file mode 100644 index 000000000..0efefd275 --- /dev/null +++ b/examples/server/recipes/flux1-depth.json @@ -0,0 +1,54 @@ +{ + "schema_version": 1, + "id": "flux1-depth", + "name": "FLUX.1 Depth dev", + "description": "Select Depth weights and supply a precomputed depth map as Control Image. Depth estimation is not performed by this recipe.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Depth-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "distilled_guidance": 10, + "scheduler": "flux", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg", + "control_image" + ], + "disabled_features": [ + "mask_image" + ] +} diff --git a/examples/server/recipes/flux1-dev.json b/examples/server/recipes/flux1-dev.json new file mode 100644 index 000000000..93561958a --- /dev/null +++ b/examples/server/recipes/flux1-dev.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "flux1-dev", + "name": "FLUX.1 dev", + "description": "Supports FLUX.1 dev and compatible layer-count variants such as FLUX.1 Lite. Select the transformer with matching CLIP-L, T5XXL and FLUX.1 VAE.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024, + "distilled_guidance": 3.5 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/flux1-fill.json b/examples/server/recipes/flux1-fill.json new file mode 100644 index 000000000..5e6485ad1 --- /dev/null +++ b/examples/server/recipes/flux1-fill.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "id": "flux1-fill", + "name": "FLUX.1 Fill dev", + "description": "Select Fill weights and supply Init Image plus Mask Image for inpainting. Denoising strength defaults to 1 for a full denoising schedule.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Fill-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "distilled_guidance": 30, + "strength": 1.0, + "scheduler": "flux", + "cfg_scale": 1, + "steps": 20, + "width": 1232, + "height": 1632 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg", + "img_cfg" + ] +} diff --git a/examples/server/recipes/flux1-kontext.json b/examples/server/recipes/flux1-kontext.json new file mode 100644 index 000000000..d66e41a26 --- /dev/null +++ b/examples/server/recipes/flux1-kontext.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "flux1-kontext", + "name": "FLUX.1 Kontext dev", + "description": "Select FLUX.1 Kontext weights. Supports text-to-image and native reference-image editing; an editing LoRA is not required.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-Kontext-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "distilled_guidance": 2.5 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux1-pulid.json b/examples/server/recipes/flux1-pulid.json new file mode 100644 index 000000000..8203dd1b5 --- /dev/null +++ b/examples/server/recipes/flux1-pulid.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "id": "flux1-pulid", + "name": "FLUX.1 dev PuLID", + "description": "Supply a precomputed PuLID identity embedding with the generation request.", + "model_url": "https://huggingface.co/guozinan/PuLID", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "pulid_weights", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024, + "distilled_guidance": 3.5 + }, + "features": [ + "clip_skip", + "distilled_guidance", + "flow_shift", + "slg" + ], + "disabled_features": [ + "slg" + ] +} diff --git a/examples/server/recipes/flux1-schnell.json b/examples/server/recipes/flux1-schnell.json new file mode 100644 index 000000000..106fef550 --- /dev/null +++ b/examples/server/recipes/flux1-schnell.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "flux1-schnell", + "name": "FLUX.1 schnell", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 4, + "width": 1024, + "height": 1024 + }, + "features": [ + "clip_skip", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/flux2-dev.json b/examples/server/recipes/flux2-dev.json new file mode 100644 index 000000000..4d27ea16b --- /dev/null +++ b/examples/server/recipes/flux2-dev.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "flux2-dev", + "name": "FLUX.2 dev", + "description": "Supports text-to-image and reference-image editing.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-dev", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Mistral Small 3.2 24B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "distilled_guidance": 4, + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "distilled_guidance", + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux2-klein-4b.json b/examples/server/recipes/flux2-klein-4b.json new file mode 100644 index 000000000..f09360cfb --- /dev/null +++ b/examples/server/recipes/flux2-klein-4b.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "id": "flux2-klein-4b", + "name": "FLUX.2 Klein 4B", + "description": "Supports reference-image editing. The FLUX.2 small decoder is an alternative VAE.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-4B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 4, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux2-klein-9b.json b/examples/server/recipes/flux2-klein-9b.json new file mode 100644 index 000000000..ad10d6e51 --- /dev/null +++ b/examples/server/recipes/flux2-klein-9b.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "id": "flux2-klein-9b", + "name": "FLUX.2 Klein 9B", + "description": "Supports text-to-image and reference-image editing.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-9B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 8B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 4, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux2-klein-base-4b.json b/examples/server/recipes/flux2-klein-base-4b.json new file mode 100644 index 000000000..56c69f980 --- /dev/null +++ b/examples/server/recipes/flux2-klein-base-4b.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "id": "flux2-klein-base-4b", + "name": "FLUX.2 Klein Base 4B", + "description": "Supports text-to-image and reference-image editing.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-4B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux2-klein-base-9b.json b/examples/server/recipes/flux2-klein-base-9b.json new file mode 100644 index 000000000..e9bd53c6c --- /dev/null +++ b/examples/server/recipes/flux2-klein-base-9b.json @@ -0,0 +1,45 @@ +{ + "schema_version": 1, + "id": "flux2-klein-base-9b", + "name": "FLUX.2 Klein Base 9B", + "description": "Use the 9B base transformer with Qwen 3 8B. Supports reference-image editing.", + "model_url": "https://huggingface.co/black-forest-labs/FLUX.2-klein-base-9B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 8B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/flux3-klein-base.json b/examples/server/recipes/flux3-klein-base.json new file mode 100644 index 000000000..29f6f6819 --- /dev/null +++ b/examples/server/recipes/flux3-klein-base.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "flux3-klein-base", + "name": "FLUX.3 Image Klein Base", + "model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein-base", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 4, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/flux3-klein.json b/examples/server/recipes/flux3-klein.json new file mode 100644 index 000000000..164bce12a --- /dev/null +++ b/examples/server/recipes/flux3-klein.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "id": "flux3-klein", + "name": "FLUX.3 Image Klein", + "description": "Supports text-to-image and reference-image editing.", + "model_url": "https://huggingface.co/black-forest-labs/flux-3-image-klein", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 16, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/hidream-o1-dev.json b/examples/server/recipes/hidream-o1-dev.json new file mode 100644 index 000000000..8e95f5cea --- /dev/null +++ b/examples/server/recipes/hidream-o1-dev.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "id": "hidream-o1-dev", + "name": "HiDream O1 Image dev", + "description": "Bundled checkpoint; also supports reference-image editing.", + "model_url": "https://huggingface.co/HiDream-ai/HiDream-O1-Image-Dev", + "slots": [ + { + "id": "model", + "required": true + } + ], + "defaults": { + "cfg_scale": 1, + "steps": 20, + "width": 2048, + "height": 2048, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/hunyuan-video-15-i2v.json b/examples/server/recipes/hunyuan-video-15-i2v.json new file mode 100644 index 000000000..a4d672f7b --- /dev/null +++ b/examples/server/recipes/hunyuan-video-15-i2v.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "id": "hunyuan-video-15-i2v", + "name": "HunyuanVideo 1.5 I2V", + "description": "Select HunyuanVideo 1.5 I2V weights and supply a start image in Video Generation.", + "model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "t5xxl", + "required": true, + "label": "ByT5 Small GlyphXL" + }, + { + "id": "vae", + "required": false, + "label": "HunyuanVideo 1.5 VAE" + }, + { + "id": "tae", + "required": false, + "label": "TAEHV 1.5" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "cfg_scale": 6, + "steps": 20, + "width": 1280, + "height": 720, + "flow_shift": 7 + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/hunyuan-video-15-t2v.json b/examples/server/recipes/hunyuan-video-15-t2v.json new file mode 100644 index 000000000..d477b8063 --- /dev/null +++ b/examples/server/recipes/hunyuan-video-15-t2v.json @@ -0,0 +1,55 @@ +{ + "schema_version": 1, + "id": "hunyuan-video-15-t2v", + "name": "HunyuanVideo 1.5 T2V", + "description": "Select HunyuanVideo 1.5 T2V weights and use Video Generation. Use the separate I2V recipe for I2V weights.", + "model_url": "https://huggingface.co/tencent/HunyuanVideo-1.5", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "t5xxl", + "required": true, + "label": "ByT5 Small GlyphXL" + }, + { + "id": "vae", + "required": false, + "label": "HunyuanVideo 1.5 VAE" + }, + { + "id": "tae", + "required": false, + "label": "TAEHV 1.5" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "cfg_scale": 6, + "steps": 20, + "width": 1280, + "height": 720, + "flow_shift": 9 + }, + "features": [ + "flow_shift" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/ideogram4.json b/examples/server/recipes/ideogram4.json new file mode 100644 index 000000000..2d5230019 --- /dev/null +++ b/examples/server/recipes/ideogram4.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "ideogram4", + "name": "Ideogram 4", + "description": "Select the matching unconditional transformer for classifier-free guidance.", + "model_url": "https://huggingface.co/ideogram-ai/ideogram-4-fp8", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "uncond_diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 8B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 7, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/instruct-pix2pix.json b/examples/server/recipes/instruct-pix2pix.json new file mode 100644 index 000000000..e81950910 --- /dev/null +++ b/examples/server/recipes/instruct-pix2pix.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "instruct-pix2pix", + "name": "Instruct-Pix2Pix", + "description": "Supply the image to edit as a reference image.", + "model_url": "https://huggingface.co/timbrooks/instruct-pix2pix", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "img_cfg_scale": 1, + "sampling_method": "euler_a", + "scheduler": "karras", + "steps": 10 + }, + "features": [ + "cache_unet", + "clip_skip", + "img_cfg", + "ref_images" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/iris-3b.json b/examples/server/recipes/iris-3b.json new file mode 100644 index 000000000..c1b5d84fc --- /dev/null +++ b/examples/server/recipes/iris-3b.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "id": "iris-3b", + "name": "Iris 3B", + "description": "Pixel-space model; no VAE is required.", + "model_url": "https://huggingface.co/speridlabs/iris-3b", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + } + ], + "defaults": { + "cfg_scale": 3, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ], + "disabled_features": [ + "preview", + "vae_tiling" + ] +} diff --git a/examples/server/recipes/krea2-raw-edit.json b/examples/server/recipes/krea2-raw-edit.json new file mode 100644 index 000000000..8d3eb9c7b --- /dev/null +++ b/examples/server/recipes/krea2-raw-edit.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "krea2-raw-edit", + "name": "Krea 2 Raw Edit (LoRA)", + "description": "Requires a matching Ostris-compatible community editing LoRA. Apply it after loading and supply reference images; uses the krea2_ostris_edit preset. Select a separate vision projector when it is not bundled in the text encoder.", + "model_url": "https://huggingface.co/krea/Krea-2-Raw", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler", + "cfg_scale": 3.5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/krea2-raw.json b/examples/server/recipes/krea2-raw.json new file mode 100644 index 000000000..5878291ab --- /dev/null +++ b/examples/server/recipes/krea2-raw.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "krea2-raw", + "name": "Krea 2 Raw", + "description": "For reference-image editing with a community LoRA, use the corresponding Krea 2 Edit recipe.", + "model_url": "https://huggingface.co/krea/Krea-2-Raw", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 3.5, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/krea2-turbo-edit.json b/examples/server/recipes/krea2-turbo-edit.json new file mode 100644 index 000000000..fa248c083 --- /dev/null +++ b/examples/server/recipes/krea2-turbo-edit.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "krea2-turbo-edit", + "name": "Krea 2 Turbo Edit (LoRA)", + "description": "Requires a matching Ostris-compatible community editing LoRA. Apply it after loading and supply reference images; uses the krea2_ostris_edit preset. Select a separate vision projector when it is not bundled in the text encoder.", + "model_url": "https://huggingface.co/krea/Krea-2-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 8, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/krea2-turbo.json b/examples/server/recipes/krea2-turbo.json new file mode 100644 index 000000000..23dad12fd --- /dev/null +++ b/examples/server/recipes/krea2-turbo.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "krea2-turbo", + "name": "Krea 2 Turbo", + "description": "For reference-image editing with a community LoRA, use the corresponding Krea 2 Edit recipe.", + "model_url": "https://huggingface.co/krea/Krea-2-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 8, + "width": 2048, + "height": 2048, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/lens-turbo.json b/examples/server/recipes/lens-turbo.json new file mode 100644 index 000000000..e9282eafc --- /dev/null +++ b/examples/server/recipes/lens-turbo.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "lens-turbo", + "name": "LENS Turbo", + "model_url": "https://huggingface.co/microsoft/Lens-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "GPT-OSS 20B Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "GPT-OSS tokenizer.json" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 4, + "width": 1440, + "height": 1440, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/lens.json b/examples/server/recipes/lens.json new file mode 100644 index 000000000..0a8954229 --- /dev/null +++ b/examples/server/recipes/lens.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "lens", + "name": "LENS", + "model_url": "https://huggingface.co/microsoft/Lens", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "GPT-OSS 20B Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "GPT-OSS tokenizer.json" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 5, + "steps": 20, + "width": 1440, + "height": 1440, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/lingbot-video-dense.json b/examples/server/recipes/lingbot-video-dense.json new file mode 100644 index 000000000..5f313a0cd --- /dev/null +++ b/examples/server/recipes/lingbot-video-dense.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "lingbot-video-dense", + "name": "LingBot Video Dense 1.3B", + "description": "Use Video Generation.", + "model_url": "https://huggingface.co/robbyant/lingbot-video-dense-1.3b", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.1 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "video_frames": 33, + "cfg_scale": 3, + "steps": 20, + "width": 832, + "height": 480, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/lingbot-video-moe.json b/examples/server/recipes/lingbot-video-moe.json new file mode 100644 index 000000000..cdbce61ee --- /dev/null +++ b/examples/server/recipes/lingbot-video-moe.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "lingbot-video-moe", + "name": "LingBot Video MoE 30B A3B", + "description": "Use Video Generation.", + "model_url": "https://huggingface.co/robbyant/lingbot-video-moe-30b-a3b", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.1 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "video_frames": 33, + "cfg_scale": 3, + "steps": 20, + "width": 832, + "height": 480, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/llada-image-turbo.json b/examples/server/recipes/llada-image-turbo.json new file mode 100644 index 000000000..0589bb384 --- /dev/null +++ b/examples/server/recipes/llada-image-turbo.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "id": "llada-image-turbo", + "name": "LLaDA Image Turbo", + "description": "Use matching turbo text encoder and connectors. Editing needs connectors that include the SigVQ encoder.", + "model_url": "https://huggingface.co/inclusionAI/LLaDA-Image-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "LLaDA-Image Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "LLaDA tokenizer.json" + }, + { + "id": "embeddings_connectors", + "required": true, + "label": "LLaDA Turbo Connectors" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 4, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "ref_images" + ] +} diff --git a/examples/server/recipes/llada-image.json b/examples/server/recipes/llada-image.json new file mode 100644 index 000000000..27887c849 --- /dev/null +++ b/examples/server/recipes/llada-image.json @@ -0,0 +1,53 @@ +{ + "schema_version": 1, + "id": "llada-image", + "name": "LLaDA Image (base)", + "description": "Use matching base transformer, text encoder and connectors; do not mix them with Turbo components. Editing needs connectors with SigVQ and 1024x1024 output.", + "model_url": "https://huggingface.co/inclusionAI/LLaDA-Image", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "LLaDA-Image Base Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "LLaDA tokenizer.json" + }, + { + "id": "embeddings_connectors", + "required": true, + "label": "LLaDA Base Connectors" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "scheduler": "llada_image", + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "ref_images" + ] +} diff --git a/examples/server/recipes/longcat-image-edit.json b/examples/server/recipes/longcat-image-edit.json new file mode 100644 index 000000000..bc15ea2af --- /dev/null +++ b/examples/server/recipes/longcat-image-edit.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "id": "longcat-image-edit", + "name": "LongCat Image Edit", + "description": "Supply reference images for editing.", + "model_url": "https://huggingface.co/meituan-longcat/LongCat-Image-Edit", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4.5, + "steps": 20, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images", + "slg" + ] +} diff --git a/examples/server/recipes/longcat-image.json b/examples/server/recipes/longcat-image.json new file mode 100644 index 000000000..36e23e85e --- /dev/null +++ b/examples/server/recipes/longcat-image.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "longcat-image", + "name": "LongCat Image", + "model_url": "https://huggingface.co/meituan-longcat/LongCat-Image", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "width": 1344, + "height": 768, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/ltx23-dev.json b/examples/server/recipes/ltx23-dev.json new file mode 100644 index 000000000..83bb934e8 --- /dev/null +++ b/examples/server/recipes/ltx23-dev.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "id": "ltx23-dev", + "name": "LTX 2.3 dev", + "description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings. Optional distillation LoRAs use this recipe; select the LoRA and adjust the sampling settings separately.", + "model_url": "https://huggingface.co/Lightricks/LTX-2.3", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 3 12B Text Encoder" + }, + { + "id": "embeddings_connectors", + "required": true + }, + { + "id": "vae", + "required": false, + "label": "LTX 2.3 Video VAE" + }, + { + "id": "tae", + "required": false + }, + { + "id": "audio_vae", + "required": false, + "label": "LTX 2.3 Audio VAE" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "fps": 24, + "cfg_scale": 3, + "steps": 20, + "width": 768, + "height": 512 + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/ltx23-distilled.json b/examples/server/recipes/ltx23-distilled.json new file mode 100644 index 000000000..a2a6ac33c --- /dev/null +++ b/examples/server/recipes/ltx23-distilled.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "id": "ltx23-distilled", + "name": "LTX 2.3 Distilled 1.1", + "description": "Supports text, start-image and start/end-image video. Audio VAE is optional for silent video. Configure latent upscaling separately in generation settings.", + "model_url": "https://huggingface.co/Lightricks/LTX-2.3", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 3 12B Text Encoder" + }, + { + "id": "embeddings_connectors", + "required": true + }, + { + "id": "vae", + "required": false, + "label": "LTX 2.3 Video VAE" + }, + { + "id": "tae", + "required": false + }, + { + "id": "audio_vae", + "required": false, + "label": "LTX 2.3 Audio VAE" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 121, + "fps": 24, + "cfg_scale": 1, + "steps": 8, + "width": 768, + "height": 512 + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/ltx25-dev.json b/examples/server/recipes/ltx25-dev.json new file mode 100644 index 000000000..47efbc76f --- /dev/null +++ b/examples/server/recipes/ltx25-dev.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "id": "ltx25-dev", + "name": "LTX 2.5 dev", + "description": "Use the conv video VAE and LTX-specific Gemma 4 with bundled projection. Audio VAE is optional for silent video.", + "model_url": "https://huggingface.co/Lightricks/LTX-2.5", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "LTX-2.5 Gemma 4 12B with projection" + }, + { + "id": "vae", + "required": false, + "label": "LTX 2.5 Conv Video VAE" + }, + { + "id": "tae", + "required": false + }, + { + "id": "audio_vae", + "required": false, + "label": "LTX 2.5 Audio VAE" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "fps": 24, + "video_frames": 33, + "cfg_scale": 3, + "steps": 20, + "width": 768, + "height": 512 + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/mage-flow-edit.json b/examples/server/recipes/mage-flow-edit.json new file mode 100644 index 000000000..82d66f6bd --- /dev/null +++ b/examples/server/recipes/mage-flow-edit.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "id": "mage-flow-edit", + "name": "Mage Flow Edit", + "description": "Supply reference images. Select a separate vision projector when it is not bundled in the text encoder.", + "model_url": "https://huggingface.co/microsoft/Mage-Flow-Edit", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": true + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + } + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/mage-flow-turbo.json b/examples/server/recipes/mage-flow-turbo.json new file mode 100644 index 000000000..5cf9bb25a --- /dev/null +++ b/examples/server/recipes/mage-flow-turbo.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "id": "mage-flow-turbo", + "name": "Mage Flow Turbo", + "model_url": "https://huggingface.co/microsoft/Mage-Flow-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": true + } + ], + "defaults": { + "cfg_scale": 1, + "steps": 4, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/mage-flow.json b/examples/server/recipes/mage-flow.json new file mode 100644 index 000000000..4cbd15934 --- /dev/null +++ b/examples/server/recipes/mage-flow.json @@ -0,0 +1,31 @@ +{ + "schema_version": 1, + "id": "mage-flow", + "name": "Mage Flow", + "model_url": "https://huggingface.co/microsoft/Mage-Flow", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": true + } + ], + "defaults": { + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/ming-image.json b/examples/server/recipes/ming-image.json new file mode 100644 index 000000000..6c368e8e1 --- /dev/null +++ b/examples/server/recipes/ming-image.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "ming-image", + "name": "Ming Image 0.1", + "model_url": "https://huggingface.co/inclusionAI/Ming-Image-0.1-Design", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Ling Mini 2.0 Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Ling tokenizer.json" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 1, + "steps": 12, + "width": 2048, + "height": 2048, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/minimax-h3-fl2va.json b/examples/server/recipes/minimax-h3-fl2va.json new file mode 100644 index 000000000..397192391 --- /dev/null +++ b/examples/server/recipes/minimax-h3-fl2va.json @@ -0,0 +1,55 @@ +{ + "schema_version": 1, + "id": "minimax-h3-fl2va", + "name": "MiniMax H3 FL2VA", + "description": "Generate video and audio, optionally with start/end images. Supports full and pruned FL2VA weights. Audio VAE is optional for silent video.", + "model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "MiniMax-H3 Qwen 3 VL 32B Text Encoder" + }, + { + "id": "vae", + "required": false, + "label": "MiniMax H3 Video VAE" + }, + { + "id": "tae", + "required": false + }, + { + "id": "audio_vae", + "required": false, + "label": "MiniMax H3 Audio VAE" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "video_frames": 56, + "fps": 24, + "cfg_scale": 1, + "width": 1344, + "height": 768, + "sampling_method": "euler" + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/minimax-h3-ref2va.json b/examples/server/recipes/minimax-h3-ref2va.json new file mode 100644 index 000000000..085f5d5fe --- /dev/null +++ b/examples/server/recipes/minimax-h3-ref2va.json @@ -0,0 +1,54 @@ +{ + "schema_version": 1, + "id": "minimax-h3-ref2va", + "name": "MiniMax H3 Ref2VA", + "description": "Supply image, video or audio references with compatible Ref2VA weights. Audio VAE is optional for silent video.", + "model_url": "https://huggingface.co/MiniMaxAI/MiniMax-H3", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "MiniMax-H3 Qwen 3 VL 32B Text Encoder" + }, + { + "id": "vae", + "required": false, + "label": "MiniMax H3 Video VAE" + }, + { + "id": "tae", + "required": false + }, + { + "id": "audio_vae", + "required": false, + "label": "MiniMax H3 Audio VAE" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "video_frames": 56, + "fps": 24, + "cfg_scale": 1, + "width": 1344, + "height": 768, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/minit2i.json b/examples/server/recipes/minit2i.json new file mode 100644 index 000000000..2c7b94eba --- /dev/null +++ b/examples/server/recipes/minit2i.json @@ -0,0 +1,29 @@ +{ + "schema_version": 1, + "id": "minit2i", + "name": "MiniT2I", + "description": "Pixel-space model; no VAE is required.", + "model_url": "https://huggingface.co/MiniT2I/MiniT2I", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true, + "label": "FLAN-T5 Large" + } + ], + "defaults": { + "cfg_scale": 2.5, + "steps": 20, + "width": 512, + "height": 512, + "sampling_method": "euler" + }, + "disabled_features": [ + "preview", + "vae_tiling" + ] +} diff --git a/examples/server/recipes/moss-image-base.json b/examples/server/recipes/moss-image-base.json new file mode 100644 index 000000000..bc2cc7d9b --- /dev/null +++ b/examples/server/recipes/moss-image-base.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "moss-image-base", + "name": "MOSS Image Base", + "description": "Select the MOSS-Image-Base transformer with its matching text encoder and FLUX.2 VAE or TAE.", + "model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-Base", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3.5 9B Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Qwen 3.5 tokenizer.json" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 3, + "steps": 20, + "width": 1024, + "height": 1024, + "flow_shift": 5, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/moss-image-sft.json b/examples/server/recipes/moss-image-sft.json new file mode 100644 index 000000000..e0239b0bc --- /dev/null +++ b/examples/server/recipes/moss-image-sft.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "moss-image-sft", + "name": "MOSS Image SFT", + "description": "Select the MOSS-Image-SFT transformer with its matching text encoder and FLUX.2 VAE or TAE.", + "model_url": "https://huggingface.co/OpenMOSS-Team/MOSS-Image-SFT", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3.5 9B Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Qwen 3.5 tokenizer.json" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 3, + "steps": 20, + "width": 1024, + "height": 1024, + "flow_shift": 5, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/nitrosd-realism.json b/examples/server/recipes/nitrosd-realism.json new file mode 100644 index 000000000..75f2d43ff --- /dev/null +++ b/examples/server/recipes/nitrosd-realism.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "nitrosd-realism", + "name": "NitroSD Realism", + "model_url": "https://huggingface.co/ChenDY/NitroFusion", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "scheduler": "sgm_uniform", + "shifted_timestep": 250, + "cfg_scale": 1, + "steps": 1, + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/ovis-image.json b/examples/server/recipes/ovis-image.json new file mode 100644 index 000000000..21e9d9fce --- /dev/null +++ b/examples/server/recipes/ovis-image.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "ovis-image", + "name": "Ovis Image", + "model_url": "https://huggingface.co/ATH-MaaS/Ovis-Image-7B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Ovis 2.5 Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + }, + "features": [ + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/pid-15-flux2.json b/examples/server/recipes/pid-15-flux2.json new file mode 100644 index 000000000..4fb3db3a7 --- /dev/null +++ b/examples/server/recipes/pid-15-flux2.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-15-flux2", + "name": "PiD 1.5 (Flux.2 VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "flux2" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-15-qwen-image.json b/examples/server/recipes/pid-15-qwen-image.json new file mode 100644 index 000000000..9db9e7422 --- /dev/null +++ b/examples/server/recipes/pid-15-qwen-image.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-15-qwen-image", + "name": "PiD 1.5 (Qwen Image VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "wan" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-15-sd3.json b/examples/server/recipes/pid-15-sd3.json new file mode 100644 index 000000000..3980b8671 --- /dev/null +++ b/examples/server/recipes/pid-15-sd3.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-15-sd3", + "name": "PiD 1.5 (SD3 VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "sd3" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-15.json b/examples/server/recipes/pid-15.json new file mode 100644 index 000000000..f7913bee8 --- /dev/null +++ b/examples/server/recipes/pid-15.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-15", + "name": "PiD 1.5 (Flux / Z-Image VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "flux" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-flux2.json b/examples/server/recipes/pid-flux2.json new file mode 100644 index 000000000..9c802dc15 --- /dev/null +++ b/examples/server/recipes/pid-flux2.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-flux2", + "name": "PiD (Flux.2 VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching Flux.2 PiD checkpoint and VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "flux2" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-qwen-image.json b/examples/server/recipes/pid-qwen-image.json new file mode 100644 index 000000000..4503b1cd6 --- /dev/null +++ b/examples/server/recipes/pid-qwen-image.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-qwen-image", + "name": "PiD (Qwen Image VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching Qwen Image PiD checkpoint and VAE. Use the Qwen Image 2D VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "wan" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid-sd3.json b/examples/server/recipes/pid-sd3.json new file mode 100644 index 000000000..6dad2ca09 --- /dev/null +++ b/examples/server/recipes/pid-sd3.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid-sd3", + "name": "PiD (SD3 VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a matching SD3 PiD checkpoint and VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "sd3" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pid.json b/examples/server/recipes/pid.json new file mode 100644 index 000000000..247944965 --- /dev/null +++ b/examples/server/recipes/pid.json @@ -0,0 +1,47 @@ +{ + "schema_version": 1, + "id": "pid", + "name": "PiD (Flux / Z-Image VAE)", + "description": "Supply a reference image; the first image is used as the source latent. Select a Flux/Z-Image PiD checkpoint and matching Flux VAE.", + "model_url": "https://huggingface.co/nvidia/PiD", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Gemma 2 2B ELM Text Encoder" + }, + { + "id": "tokenizer", + "required": true, + "label": "Gemma 2 tokenizer.json" + }, + { + "id": "vae", + "required": true + } + ], + "context": { + "vae_format": "flux" + }, + "defaults": { + "sampling_method": "lcm", + "scheduler": "lcm", + "cfg_scale": 1, + "steps": 4, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ], + "disabled_features": [ + "img2img", + "mask_image", + "preview" + ] +} diff --git a/examples/server/recipes/pixart-alpha-1024.json b/examples/server/recipes/pixart-alpha-1024.json new file mode 100644 index 000000000..1144f87b2 --- /dev/null +++ b/examples/server/recipes/pixart-alpha-1024.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "pixart-alpha-1024", + "name": "PixArt Alpha XL 1024", + "description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.", + "model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "context": { + "model_args": "pixart_vae_scale_factor=0.18215" + }, + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler", + "cfg_scale": 4.5, + "steps": 20, + "width": 1024, + "height": 1024 + } +} diff --git a/examples/server/recipes/pixart-alpha-512.json b/examples/server/recipes/pixart-alpha-512.json new file mode 100644 index 000000000..804a7a0c9 --- /dev/null +++ b/examples/server/recipes/pixart-alpha-512.json @@ -0,0 +1,42 @@ +{ + "schema_version": 1, + "id": "pixart-alpha-512", + "name": "PixArt Alpha XL 512", + "description": "Use the matching PixArt Alpha transformer and SD 1.x-style VAE. Resolution/aspect-ratio micro-conditioning is not currently applied by the runtime.", + "model_url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-512x512", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "context": { + "model_args": "pixart_vae_scale_factor=0.18215,pixart_pos_embed_base_size=32,pixart_interpolation_scale=1" + }, + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler", + "cfg_scale": 4.5, + "steps": 20, + "width": 512, + "height": 512 + } +} diff --git a/examples/server/recipes/pixart-sigma.json b/examples/server/recipes/pixart-sigma.json new file mode 100644 index 000000000..ed4d2f0de --- /dev/null +++ b/examples/server/recipes/pixart-sigma.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "id": "pixart-sigma", + "name": "PixArt Sigma XL 1024", + "model_url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma-XL-2-1024-MS", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4.5, + "steps": 20, + "width": 1024, + "height": 1024, + "sampling_method": "euler" + } +} diff --git a/examples/server/recipes/qwen-image-21-turbo.json b/examples/server/recipes/qwen-image-21-turbo.json new file mode 100644 index 000000000..2ef9fa183 --- /dev/null +++ b/examples/server/recipes/qwen-image-21-turbo.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "id": "qwen-image-21-turbo", + "name": "Qwen Image 2.1 Turbo", + "description": "Use the standalone Turbo diffusion weights for generation and editing with the fixed 8-step schedule. Select the Qwen Image 2.1 VAE or TAEQI2.1, and supply a vision projector when needed.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 8B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image 2.1 VAE" + }, + { + "id": "tae", + "required": false, + "label": "TAEQI2.1" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 8, + "width": 2048, + "height": 2048, + "custom_sigmas": [ + 1.0, + 0.978453, + 0.95418, + 0.926626, + 0.89508, + 0.845148, + 0.704534, + 0.414568, + 0.0 + ] + }, + "features": [ + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image-21.json b/examples/server/recipes/qwen-image-21.json new file mode 100644 index 000000000..4b6c67b86 --- /dev/null +++ b/examples/server/recipes/qwen-image-21.json @@ -0,0 +1,50 @@ +{ + "schema_version": 1, + "id": "qwen-image-21", + "name": "Qwen Image 2.1", + "description": "Supports generation and editing. Select the full Qwen Image 2.1 VAE or TAEQI2.1. Supply a separate vision projector when needed. Select acceleration LoRAs and adjust their settings separately.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-2.1", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 8B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image 2.1 VAE" + }, + { + "id": "tae", + "required": false, + "label": "TAEQI2.1" + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 20, + "width": 2048, + "height": 2048 + }, + "features": [ + "flow_shift", + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image-edit-2509.json b/examples/server/recipes/qwen-image-edit-2509.json new file mode 100644 index 000000000..4c647a042 --- /dev/null +++ b/examples/server/recipes/qwen-image-edit-2509.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "qwen-image-edit-2509", + "name": "Qwen Image Edit 2509", + "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit-2509", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image-edit-2511.json b/examples/server/recipes/qwen-image-edit-2511.json new file mode 100644 index 000000000..cdd16db6c --- /dev/null +++ b/examples/server/recipes/qwen-image-edit-2511.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "id": "qwen-image-edit-2511", + "name": "Qwen Image Edit 2511", + "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit-2511", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "context": { + "model_args": "qwen_image_zero_cond_t=true" + }, + "defaults": { + "sampling_method": "euler", + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image-edit.json b/examples/server/recipes/qwen-image-edit.json new file mode 100644 index 000000000..5610a6c42 --- /dev/null +++ b/examples/server/recipes/qwen-image-edit.json @@ -0,0 +1,49 @@ +{ + "schema_version": 1, + "id": "qwen-image-edit", + "name": "Qwen Image Edit", + "description": "Supply reference images. Select a vision projector for GGUF text encoders; omit it if vision weights are included in the text encoder.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-Edit", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "llm_vision", + "required": false, + "label": "Vision projector (required for separate GGUF text encoders)" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image-layered.json b/examples/server/recipes/qwen-image-layered.json new file mode 100644 index 000000000..96863be31 --- /dev/null +++ b/examples/server/recipes/qwen-image-layered.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "qwen-image-layered", + "name": "Qwen Image Layered", + "description": "Supply a reference image and use the dedicated layered VAE.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image-Layered", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "vae", + "required": true, + "label": "Qwen Image Layered VAE" + } + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20, + "width": 640, + "height": 640 + }, + "features": [ + "flow_shift", + "img_cfg", + "ref_images" + ] +} diff --git a/examples/server/recipes/qwen-image.json b/examples/server/recipes/qwen-image.json new file mode 100644 index 000000000..417a621ae --- /dev/null +++ b/examples/server/recipes/qwen-image.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "id": "qwen-image", + "name": "Qwen Image", + "description": "Supports text-to-image and image-to-image; pruned transformer variants use the same components.", + "model_url": "https://huggingface.co/Qwen/Qwen-Image", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 2.5 VL 7B Text Encoder" + }, + { + "id": "vae", + "required": false, + "label": "Qwen Image VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20, + "width": 1328, + "height": 1328 + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/sd-turbo.json b/examples/server/recipes/sd-turbo.json new file mode 100644 index 000000000..7723c8107 --- /dev/null +++ b/examples/server/recipes/sd-turbo.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "sd-turbo", + "name": "SD Turbo", + "description": "Select a single-file SD-Turbo checkpoint. Uses one step with CFG disabled (scale 1 in sd.cpp); use more steps for partial image-to-image denoising.", + "model_url": "https://huggingface.co/stabilityai/sd-turbo", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "sampling_method": "euler", + "scheduler": "discrete", + "cfg_scale": 1, + "steps": 1, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd1-checkpoint.json b/examples/server/recipes/sd1-checkpoint.json new file mode 100644 index 000000000..38d238677 --- /dev/null +++ b/examples/server/recipes/sd1-checkpoint.json @@ -0,0 +1,35 @@ +{ + "schema_version": 1, + "id": "sd1-checkpoint", + "name": "SD 1.x", + "description": "Supports SD 1.x, compatible LCM checkpoints and layer-count variants. For LCM weights, select LCM sampling and adjust steps/CFG. Tiny SD Diffusers directories must first be converted into a single-file checkpoint.", + "model_url": "https://huggingface.co/CompVis/stable-diffusion-v1-4", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "label": "VAE override", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd1-controlnet.json b/examples/server/recipes/sd1-controlnet.json new file mode 100644 index 000000000..eeaee22fb --- /dev/null +++ b/examples/server/recipes/sd1-controlnet.json @@ -0,0 +1,38 @@ +{ + "schema_version": 1, + "id": "sd1-controlnet", + "name": "SD 1.5 ControlNet", + "description": "Supply a control image; input image and mask are optional for image-to-image or inpainting.", + "model_url": "https://huggingface.co/lllyasviel/control_v11p_sd15_canny", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "control_net", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd1-inpainting.json b/examples/server/recipes/sd1-inpainting.json new file mode 100644 index 000000000..f9f3064f3 --- /dev/null +++ b/examples/server/recipes/sd1-inpainting.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "id": "sd1-inpainting", + "name": "SD 1.5 Inpainting", + "description": "Select an inpainting checkpoint, then supply an input image and mask.", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip", + "img_cfg" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd1-ip-adapter.json b/examples/server/recipes/sd1-ip-adapter.json new file mode 100644 index 000000000..6c0ab1a6b --- /dev/null +++ b/examples/server/recipes/sd1-ip-adapter.json @@ -0,0 +1,43 @@ +{ + "schema_version": 1, + "id": "sd1-ip-adapter", + "name": "SD 1.5 IP-Adapter / Plus", + "description": "Select matching SD 1.5 IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.", + "model_url": "https://huggingface.co/h94/IP-Adapter", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "clip_vision", + "required": true, + "label": "CLIP Vision H" + }, + { + "id": "ip_adapter", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd2-512-checkpoint.json b/examples/server/recipes/sd2-512-checkpoint.json new file mode 100644 index 000000000..77838c280 --- /dev/null +++ b/examples/server/recipes/sd2-512-checkpoint.json @@ -0,0 +1,33 @@ +{ + "schema_version": 1, + "id": "sd2-512-checkpoint", + "name": "SD 2.1 512", + "description": "Supports SD 2.x 512 and compatible layer-count variants. Tiny SD2 Diffusers directories must first be converted into a single-file checkpoint.", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd2-768-checkpoint.json b/examples/server/recipes/sd2-768-checkpoint.json new file mode 100644 index 000000000..808b187d7 --- /dev/null +++ b/examples/server/recipes/sd2-768-checkpoint.json @@ -0,0 +1,32 @@ +{ + "schema_version": 1, + "id": "sd2-768-checkpoint", + "name": "SD 2.1 768", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 7.5, + "steps": 20, + "width": 768, + "height": 768 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd2-inpainting.json b/examples/server/recipes/sd2-inpainting.json new file mode 100644 index 000000000..4980f7f0a --- /dev/null +++ b/examples/server/recipes/sd2-inpainting.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "sd2-inpainting", + "name": "SD 2 Inpainting", + "description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler_a", + "cfg_scale": 7.5, + "steps": 20, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip", + "img_cfg" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sd3-medium.json b/examples/server/recipes/sd3-medium.json new file mode 100644 index 000000000..7c0e8fbff --- /dev/null +++ b/examples/server/recipes/sd3-medium.json @@ -0,0 +1,33 @@ +{ + "schema_version": 1, + "id": "sd3-medium", + "name": "SD 3 Medium", + "description": "Checkpoint with CLIP-L, CLIP-G and T5 included.", + "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3-medium-diffusers", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 7, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "clip_skip", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/sd35-large.json b/examples/server/recipes/sd35-large.json new file mode 100644 index 000000000..a231db99c --- /dev/null +++ b/examples/server/recipes/sd35-large.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "id": "sd35-large", + "name": "SD 3.5 Large", + "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-large", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "clip_g", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 3.5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "clip_skip", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/sd35-medium.json b/examples/server/recipes/sd35-medium.json new file mode 100644 index 000000000..75080253a --- /dev/null +++ b/examples/server/recipes/sd35-medium.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "id": "sd35-medium", + "name": "SD 3.5 Medium", + "model_url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-medium", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "clip_l", + "required": true + }, + { + "id": "clip_g", + "required": true + }, + { + "id": "t5xxl", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 4.5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "clip_skip", + "flow_shift", + "slg" + ] +} diff --git a/examples/server/recipes/sdxl-checkpoint.json b/examples/server/recipes/sdxl-checkpoint.json new file mode 100644 index 000000000..372d5a6d4 --- /dev/null +++ b/examples/server/recipes/sdxl-checkpoint.json @@ -0,0 +1,36 @@ +{ + "schema_version": 1, + "id": "sdxl-checkpoint", + "name": "SDXL", + "description": "Supports SDXL and compatible reduced-depth checkpoints such as SSD-1B and Segmind Vega.", + "model_url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "label": "VAE override", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxl-controlnet.json b/examples/server/recipes/sdxl-controlnet.json new file mode 100644 index 000000000..c50efbd2b --- /dev/null +++ b/examples/server/recipes/sdxl-controlnet.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "sdxl-controlnet", + "name": "SDXL ControlNet", + "description": "Select a compatible SDXL ControlNet and supply a preprocessed control image. Input image and mask are optional for image-to-image or inpainting.", + "model_url": "https://huggingface.co/diffusers/controlnet-canny-sdxl-1.0", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "control_net", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler_a", + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxl-inpainting.json b/examples/server/recipes/sdxl-inpainting.json new file mode 100644 index 000000000..574423d08 --- /dev/null +++ b/examples/server/recipes/sdxl-inpainting.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "id": "sdxl-inpainting", + "name": "SDXL Inpainting", + "description": "Select an inpainting checkpoint, then supply Init Image and Mask Image.", + "model_url": "https://huggingface.co/diffusers/stable-diffusion-xl-1.0-inpainting-0.1", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "label": "VAE override", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "euler_a", + "cfg_scale": 8, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep", + "img_cfg" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxl-ip-adapter.json b/examples/server/recipes/sdxl-ip-adapter.json new file mode 100644 index 000000000..b649fb1d5 --- /dev/null +++ b/examples/server/recipes/sdxl-ip-adapter.json @@ -0,0 +1,46 @@ +{ + "schema_version": 1, + "id": "sdxl-ip-adapter", + "name": "SDXL IP-Adapter / Plus", + "description": "Select matching SDXL ViT-H IP-Adapter or Plus weights and CLIP Vision H, then supply an IP-Adapter image.", + "model_url": "https://huggingface.co/h94/IP-Adapter", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "clip_vision", + "required": true, + "label": "CLIP Vision H" + }, + { + "id": "ip_adapter", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "scheduler": "discrete", + "sampling_method": "dpm++2m", + "cfg_scale": 5, + "steps": 20, + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxl-photomaker.json b/examples/server/recipes/sdxl-photomaker.json new file mode 100644 index 000000000..025fb7668 --- /dev/null +++ b/examples/server/recipes/sdxl-photomaker.json @@ -0,0 +1,37 @@ +{ + "schema_version": 1, + "id": "sdxl-photomaker", + "name": "SDXL PhotoMaker", + "description": "Select PhotoMaker v1 and supply identity images. Include the img trigger in the prompt.", + "model_url": "https://huggingface.co/TencentARC/PhotoMaker", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "photo_maker", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "width": 1024, + "height": 1024 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxl-turbo.json b/examples/server/recipes/sdxl-turbo.json new file mode 100644 index 000000000..8650e6c58 --- /dev/null +++ b/examples/server/recipes/sdxl-turbo.json @@ -0,0 +1,34 @@ +{ + "schema_version": 1, + "id": "sdxl-turbo", + "name": "SDXL Turbo", + "model_url": "https://huggingface.co/stabilityai/sdxl-turbo", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "defaults": { + "cfg_scale": 1, + "steps": 1, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip", + "shifted_timestep" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sdxs.json b/examples/server/recipes/sdxs.json new file mode 100644 index 000000000..19e218ac5 --- /dev/null +++ b/examples/server/recipes/sdxs.json @@ -0,0 +1,29 @@ +{ + "schema_version": 1, + "id": "sdxs", + "name": "SDXS", + "model_url": "https://huggingface.co/IDKiro/sdxs-512-dreamshaper", + "slots": [ + { + "id": "model", + "required": true + }, + { + "id": "vae", + "required": false + } + ], + "defaults": { + "cfg_scale": 1, + "steps": 1, + "width": 512, + "height": 512 + }, + "features": [ + "cache_unet", + "clip_skip" + ], + "disabled_features": [ + "cache_dit" + ] +} diff --git a/examples/server/recipes/sefi-image.json b/examples/server/recipes/sefi-image.json new file mode 100644 index 000000000..35e933360 --- /dev/null +++ b/examples/server/recipes/sefi-image.json @@ -0,0 +1,39 @@ +{ + "schema_version": 1, + "id": "sefi-image", + "name": "SeFi Image 5B RL", + "model_url": "https://huggingface.co/SeFi-Image/SeFi-Image-5B-Base", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 VL 4B Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "sampling_method": "euler" + }, + "features": [ + "slg" + ] +} diff --git a/examples/server/recipes/sensenova-u15.json b/examples/server/recipes/sensenova-u15.json new file mode 100644 index 000000000..afd20466b --- /dev/null +++ b/examples/server/recipes/sensenova-u15.json @@ -0,0 +1,24 @@ +{ + "schema_version": 1, + "id": "sensenova-u15", + "name": "SenseNova U1.5", + "description": "Bundled MoT checkpoint.", + "model_url": "https://huggingface.co/sensenova/SenseNova-U1.5-8B-MoT", + "slots": [ + { + "id": "model", + "required": true + } + ], + "defaults": { + "flow_shift": 3.0, + "cfg_scale": 4, + "steps": 20, + "width": 2048, + "height": 2048, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/wan21-flf2v.json b/examples/server/recipes/wan21-flf2v.json new file mode 100644 index 000000000..02f0e80a9 --- /dev/null +++ b/examples/server/recipes/wan21-flf2v.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "id": "wan21-flf2v", + "name": "Wan 2.1 FLF2V 14B", + "description": "Select FLF2V weights and supply both start and end images.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.1-FLF2V-14B-720P", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true, + "label": "UMT5-XXL" + }, + { + "id": "clip_vision", + "required": true, + "label": "CLIP Vision H" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.1 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 16, + "video_frames": 21, + "cfg_scale": 5, + "steps": 20, + "width": 1280, + "height": 720 + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan21-i2v.json b/examples/server/recipes/wan21-i2v.json new file mode 100644 index 000000000..d8d8d13ee --- /dev/null +++ b/examples/server/recipes/wan21-i2v.json @@ -0,0 +1,56 @@ +{ + "schema_version": 1, + "id": "wan21-i2v", + "name": "Wan 2.1 I2V", + "description": "Image-to-video with Wan 2.1, compatible SkyReels V2 weights, or a single Wan 2.2 I2V expert. CLIP Vision is required by Wan 2.1 I2V and is unused by Wan 2.2. Defaults target Wan 2.1 480p.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.1-I2V-14B-480P", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "label": "UMT5-XXL", + "required": true + }, + { + "id": "vae", + "label": "Wan 2.1 VAE", + "required": false + }, + { + "id": "tae", + "required": false + }, + { + "id": "clip_vision", + "label": "CLIP Vision H (Wan 2.1 I2V)", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "fps": 16, + "flow_shift": 3, + "cfg_scale": 5, + "steps": 20, + "width": 832, + "height": 480 + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan21-t2v.json b/examples/server/recipes/wan21-t2v.json new file mode 100644 index 000000000..367611c41 --- /dev/null +++ b/examples/server/recipes/wan21-t2v.json @@ -0,0 +1,50 @@ +{ + "schema_version": 1, + "id": "wan21-t2v", + "name": "Wan 2.1 T2V", + "description": "Text-to-video with Wan 2.1 or a compatible single Wan 2.2 T2V expert. Defaults target Wan 2.1 14B; adjust settings for the selected weights and acceleration LoRAs.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-14B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "label": "UMT5-XXL", + "required": true + }, + { + "id": "vae", + "label": "Wan 2.1 VAE", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "fps": 16, + "flow_shift": 5, + "cfg_scale": 5, + "steps": 20, + "width": 1280, + "height": 720 + }, + "features": [ + "flow_shift" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan21-vace.json b/examples/server/recipes/wan21-vace.json new file mode 100644 index 000000000..7b7f7e41b --- /dev/null +++ b/examples/server/recipes/wan21-vace.json @@ -0,0 +1,52 @@ +{ + "schema_version": 1, + "id": "wan21-vace", + "name": "Wan 2.1 VACE 1.3B / 14B", + "description": "Select matching VACE weights. Supports text, reference images and control video through Video Generation.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.1-VACE-14B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true, + "label": "UMT5-XXL" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.1 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "cfg_scale": 5, + "steps": 20, + "width": 832, + "height": 480, + "flow_shift": 16 + }, + "features": [ + "control_frames", + "flow_shift", + "init_image", + "vace" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan22-i2v.json b/examples/server/recipes/wan22-i2v.json new file mode 100644 index 000000000..62afdfce4 --- /dev/null +++ b/examples/server/recipes/wan22-i2v.json @@ -0,0 +1,62 @@ +{ + "schema_version": 1, + "id": "wan22-i2v", + "name": "Wan 2.2 I2V A14B", + "description": "Image-to-video. Select a start image in Video Generation after loading. LightX2V acceleration LoRAs must match the I2V low- and high-noise models; configure LoRAs and sampling separately.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B", + "slots": [ + { + "id": "diffusion_model", + "label": "Low-noise diffusion model", + "required": true + }, + { + "id": "high_noise_diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "label": "UMT5-XXL", + "required": true + }, + { + "id": "vae", + "label": "Wan 2.1 VAE", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "fps": 16, + "flow_shift": 5, + "high_noise_steps": -1, + "high_noise_cfg_scale": 3.5, + "high_noise_sampling_method": "euler", + "high_noise_flow_shift": 5, + "cfg_scale": 3.5, + "steps": 20, + "width": 1280, + "height": 720, + "moe_boundary": 0.9 + }, + "features": [ + "end_image", + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan22-s2v.json b/examples/server/recipes/wan22-s2v.json new file mode 100644 index 000000000..ba1ee4f7c --- /dev/null +++ b/examples/server/recipes/wan22-s2v.json @@ -0,0 +1,53 @@ +{ + "schema_version": 1, + "id": "wan22-s2v", + "name": "Wan 2.2 S2V 14B", + "description": "Supply a start image and reference audio through Video Generation.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.2-S2V-14B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true, + "label": "UMT5-XXL" + }, + { + "id": "audio_encoder", + "required": true, + "label": "Wav2Vec2 Large English" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.1 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "flow_shift": 3, + "video_frames": 33, + "cfg_scale": 4.5, + "steps": 20 + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan22-t2v.json b/examples/server/recipes/wan22-t2v.json new file mode 100644 index 000000000..905bcfa4f --- /dev/null +++ b/examples/server/recipes/wan22-t2v.json @@ -0,0 +1,60 @@ +{ + "schema_version": 1, + "id": "wan22-t2v", + "name": "Wan 2.2 T2V A14B", + "description": "Text-to-video. Use Video Generation after loading. LightX2V acceleration LoRAs must match the T2V low- and high-noise models; configure LoRAs and sampling separately.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B", + "slots": [ + { + "id": "diffusion_model", + "label": "Low-noise diffusion model", + "required": true + }, + { + "id": "high_noise_diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "label": "UMT5-XXL", + "required": true + }, + { + "id": "vae", + "label": "Wan 2.1 VAE", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "fps": 16, + "flow_shift": 12, + "high_noise_steps": -1, + "high_noise_cfg_scale": 4, + "high_noise_sampling_method": "euler", + "high_noise_flow_shift": 12, + "cfg_scale": 3, + "steps": 20, + "width": 1280, + "height": 720, + "moe_boundary": 0.875 + }, + "features": [ + "flow_shift" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/wan22-ti2v-5b.json b/examples/server/recipes/wan22-ti2v-5b.json new file mode 100644 index 000000000..3e2aa35b8 --- /dev/null +++ b/examples/server/recipes/wan22-ti2v-5b.json @@ -0,0 +1,50 @@ +{ + "schema_version": 1, + "id": "wan22-ti2v-5b", + "name": "Wan 2.2 TI2V 5B", + "description": "Use Wan 2.2 VAE. Supports text, start-image and start/end-image conditioning.", + "model_url": "https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "t5xxl", + "required": true, + "label": "UMT5-XXL" + }, + { + "id": "vae", + "required": false, + "label": "Wan 2.2 VAE" + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "video_frames": 33, + "cfg_scale": 5, + "steps": 20, + "width": 1280, + "height": 704, + "flow_shift": 5 + }, + "features": [ + "flow_shift", + "init_image" + ], + "disabled_features": [ + "img2img", + "mask_image" + ] +} diff --git a/examples/server/recipes/z-image-l2p.json b/examples/server/recipes/z-image-l2p.json new file mode 100644 index 000000000..0375f1387 --- /dev/null +++ b/examples/server/recipes/z-image-l2p.json @@ -0,0 +1,29 @@ +{ + "schema_version": 1, + "id": "z-image-l2p", + "name": "Z-Image L2P", + "description": "Pixel-space L2P checkpoint with Qwen 3 4B; no VAE is needed. Use dimensions divisible by 16.", + "model_url": "https://huggingface.co/zhen-nan/L2P", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 4B Text Encoder" + } + ], + "defaults": { + "sampling_method": "euler", + "scheduler": "discrete" + }, + "features": [ + "flow_shift" + ], + "disabled_features": [ + "vae_tiling", + "preview" + ] +} diff --git a/examples/server/recipes/z-image-turbo.json b/examples/server/recipes/z-image-turbo.json new file mode 100644 index 000000000..99737674e --- /dev/null +++ b/examples/server/recipes/z-image-turbo.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "z-image-turbo", + "name": "Z-Image Turbo", + "model_url": "https://huggingface.co/Tongyi-MAI/Z-Image-Turbo", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "sampling_method": "euler", + "cfg_scale": 1, + "steps": 9, + "width": 1024, + "height": 1024 + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes/z-image.json b/examples/server/recipes/z-image.json new file mode 100644 index 000000000..bcf2da7cd --- /dev/null +++ b/examples/server/recipes/z-image.json @@ -0,0 +1,41 @@ +{ + "schema_version": 1, + "id": "z-image", + "name": "Z-Image", + "model_url": "https://huggingface.co/Tongyi-MAI/Z-Image", + "slots": [ + { + "id": "diffusion_model", + "required": true + }, + { + "id": "llm", + "required": true, + "label": "Qwen 3 Text Encoder" + }, + { + "id": "vae", + "required": false + }, + { + "id": "tae", + "required": false + } + ], + "required_any": [ + [ + "vae", + "tae" + ] + ], + "defaults": { + "cfg_scale": 4, + "steps": 20, + "width": 720, + "height": 1280, + "sampling_method": "euler" + }, + "features": [ + "flow_shift" + ] +} diff --git a/examples/server/recipes_builtin.h.in b/examples/server/recipes_builtin.h.in new file mode 100644 index 000000000..ff31b4d2c --- /dev/null +++ b/examples/server/recipes_builtin.h.in @@ -0,0 +1,7 @@ +#ifndef __SD_EXAMPLES_SERVER_RECIPES_BUILTIN_H__ +#define __SD_EXAMPLES_SERVER_RECIPES_BUILTIN_H__ + +static const char* const k_builtin_recipes[] = { +@SD_BUILTIN_RECIPES@}; + +#endif diff --git a/examples/server/routes.h b/examples/server/routes.h index 1a4efb5b7..f77939184 100644 --- a/examples/server/routes.h +++ b/examples/server/routes.h @@ -9,3 +9,18 @@ void register_index_endpoints(httplib::Server& svr, const SDSvrParams& svr_param void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt); void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt); void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt); +void register_model_endpoints(httplib::Server& svr, ServerRuntime& rt); + +class RuntimeRoutes { + httplib::Server& server_; + ServerRuntime& runtime_; + + httplib::Server::Handler protect(httplib::Server::Handler handler); + +public: + RuntimeRoutes(httplib::Server& server, ServerRuntime& runtime) + : server_(server), runtime_(runtime) {} + + void get(const std::string& pattern, httplib::Server::Handler handler); + void post(const std::string& pattern, httplib::Server::Handler handler); +}; diff --git a/examples/server/routes_models.cpp b/examples/server/routes_models.cpp new file mode 100644 index 000000000..ea14eb821 --- /dev/null +++ b/examples/server/routes_models.cpp @@ -0,0 +1,150 @@ +#include "routes.h" + +#include "async_jobs.h" +#include "model_catalog.h" + +static void model_error(httplib::Response& res, int status, const std::string& code, const std::string& message) { + res.status = status; + res.set_content(json({{"error", message}, {"code", code}}).dump(), "application/json"); +} + +httplib::Server::Handler RuntimeRoutes::protect(httplib::Server::Handler handler) { + ServerRuntime* runtime = &runtime_; + return [runtime, handler](const httplib::Request& req, httplib::Response& res) { + std::shared_lock lock(runtime->models->runtime_mutex, std::try_to_lock); + if (!lock.owns_lock()) { + model_error(res, 503, "model_loading", "Model is loading or unloading"); + return; + } + const bool generation = req.path == "/sdcpp/v1/img_gen" || req.path == "/sdcpp/v1/vid_gen" || + req.path == "/sdapi/v1/txt2img" || req.path == "/sdapi/v1/img2img" || + req.path == "/v1/images/generations" || req.path == "/v1/images/edits"; + if (generation && !runtime->sd_ctx) { + model_error(res, 503, "model_not_loaded", "No model is loaded"); + return; + } + handler(req, res); + }; +} + +void RuntimeRoutes::get(const std::string& pattern, httplib::Server::Handler handler) { + // Job polling and cancellation only access the job manager, even during a model transition. + server_.Get(pattern, pattern.find("/sdcpp/v1/jobs/") != std::string::npos ? handler : protect(handler)); +} + +void RuntimeRoutes::post(const std::string& pattern, httplib::Server::Handler handler) { + server_.Post(pattern, pattern.find("/sdcpp/v1/jobs/") != std::string::npos ? handler : protect(handler)); +} + +static void set_model_status(ServerRuntime& runtime, const char* state, const json& recipe, const json& selection, const json& error = nullptr) { + std::lock_guard lock(runtime.models->status_mutex); + auto& models = *runtime.models; + const uint64_t revision = models.status.value("revision", uint64_t(0)) + 1; + json elapsed = nullptr; + if (!recipe.is_null()) { + const auto now = std::chrono::steady_clock::now(); + if (std::string(state) == "loading") { + models.load_started_at = now; + elapsed = 0.0; + } else { + elapsed = std::chrono::duration(now - models.load_started_at).count(); + } + } + models.status = {{"state", state}, {"recipe", recipe}, {"models", selection}, {"error", error}, {"revision", revision}, {"load_elapsed_seconds", elapsed}}; +} + +static void change_model(ServerRuntime& runtime, const httplib::Request& req, httplib::Response& res, bool unload) { + auto& models = *runtime.models; + std::unique_lock lock(models.runtime_mutex, std::try_to_lock); + if (!lock.owns_lock()) { + model_error(res, 409, "model_busy", "Model is in use or another model operation is running"); + return; + } + { + // Admission holds runtime_mutex through enqueue. Jobs stay pending until + // generation and metadata encoding finish, so no worker can retain this context here. + std::lock_guard jobs_lock(runtime.async_job_manager->mutex); + if (count_pending_jobs(*runtime.async_job_manager) != 0) { + model_error(res, 409, "model_busy", "Finish or cancel pending generation jobs before switching models"); + return; + } + } + + json body; + json loaded_recipe; + SDContextParams context; + SDGenerationParams defaults; + if (!unload) { + try { + body = json::parse(req.body); + prepare_model_load(runtime, body, context, defaults, &loaded_recipe); + } catch (const std::exception& e) { + model_error(res, 400, "invalid_model_config", e.what()); + return; + } + } + + const json recipe = unload ? json(nullptr) : body.at("recipe"); + const json selection = unload ? json::object() : body.at("models"); + set_model_status(runtime, "loading", recipe, selection); + try { + runtime.sd_ctx = nullptr; + models.context.reset(); + models.active_recipe = nullptr; + *runtime.ctx_params = unload ? models.base_context : std::move(context); + *runtime.default_gen_params = unload ? models.base_defaults : std::move(defaults); + if (!unload) { + auto params = runtime.ctx_params->to_sd_ctx_params_t(false); + models.context.reset(new_sd_ctx(¶ms)); + if (!models.context) { + throw std::runtime_error("Model initialization failed; check the server log for component compatibility and memory errors"); + } + runtime.sd_ctx = models.context.get(); + models.active_recipe = std::move(loaded_recipe); + } + set_model_status(runtime, unload ? "empty" : "ready", recipe, selection); + res.set_content(model_status(runtime).dump(), "application/json"); + } catch (const std::exception& e) { + runtime.sd_ctx = nullptr; + models.context.reset(); + models.active_recipe = nullptr; + *runtime.ctx_params = models.base_context; + *runtime.default_gen_params = models.base_defaults; + set_model_status(runtime, "error", recipe, selection, e.what()); + model_error(res, 422, "model_load_failed", e.what()); + } +} + +void register_model_endpoints(httplib::Server& svr, ServerRuntime& runtime) { + svr.Get("/sdcpp/v1/recipes", [&runtime](const httplib::Request&, httplib::Response& res) { + auto& models = *runtime.models; + std::lock_guard lock(models.catalog_mutex); + json items = json::array(); + for (const auto& entry : models.recipes) + items.push_back(entry.second); + res.set_content(json({{"recipes", items}, {"errors", models.catalog_errors}}).dump(), "application/json"); + }); + svr.Get("/sdcpp/v1/model-files", [&runtime](const httplib::Request&, httplib::Response& res) { + auto& models = *runtime.models; + std::lock_guard lock(models.catalog_mutex); + json items = json::array(); + for (const auto& entry : models.files) { + items.push_back({{"id", entry.first}, {"name", entry.second.name}, {"category", entry.second.category}, {"root", entry.second.root.id}, {"relative_path", entry.second.relative_path}}); + } + res.set_content(json({{"files", items}, {"errors", models.catalog_errors}}).dump(), "application/json"); + }); + svr.Post("/sdcpp/v1/models/refresh", [&runtime](const httplib::Request&, httplib::Response& res) { + refresh_model_catalog(runtime); + std::lock_guard lock(runtime.models->catalog_mutex); + res.set_content(json({{"errors", runtime.models->catalog_errors}}).dump(), "application/json"); + }); + svr.Get("/sdcpp/v1/models/status", [&runtime](const httplib::Request&, httplib::Response& res) { + res.set_content(model_status(runtime).dump(), "application/json"); + }); + svr.Post("/sdcpp/v1/models/load", [&runtime](const httplib::Request& req, httplib::Response& res) { + change_model(runtime, req, res, false); + }); + svr.Post("/sdcpp/v1/models/unload", [&runtime](const httplib::Request& req, httplib::Response& res) { + change_model(runtime, req, res, true); + }); +} diff --git a/examples/server/routes_openai.cpp b/examples/server/routes_openai.cpp index af0cdfaa1..4390f5838 100644 --- a/examples/server/routes_openai.cpp +++ b/examples/server/routes_openai.cpp @@ -36,7 +36,7 @@ static bool build_openai_generation_request(const httplib::Request& req, std::string output_format = j.value("output_format", "png"); int output_compression = j.value("output_compression", 100); int width = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->width : 512; - int height = runtime.default_gen_params->width > 0 ? runtime.default_gen_params->height : 512; + int height = runtime.default_gen_params->height > 0 ? runtime.default_gen_params->height : 512; if (!size.empty()) { auto pos = size.find('x'); if (pos != std::string::npos) { @@ -249,15 +249,18 @@ static bool execute_sync_img_gen_request(ServerRuntime& runtime, void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; + RuntimeRoutes routes(svr, rt); - svr.Get("/v1/models", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/v1/models", [runtime](const httplib::Request&, httplib::Response& res) { json r; r["data"] = json::array(); - r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}}); + if (runtime->sd_ctx) { + r["data"].push_back({{"id", "sd-cpp-local"}, {"object", "model"}, {"owned_by", "local"}}); + } res.set_content(r.dump(), "application/json"); }); - svr.Post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) { res.status = 400; @@ -273,7 +276,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } - LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str()); + LOG_DEBUG("%s\n", request.gen_params.to_string().c_str()); SDImageVec results; if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) { @@ -331,7 +334,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { } }); - svr.Post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) { res.status = 400; @@ -347,7 +350,7 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } - LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str()); + LOG_DEBUG("%s\n", request.gen_params.to_string().c_str()); SDImageVec results; if (!execute_sync_img_gen_request(*runtime, request, results, error_message)) { diff --git a/examples/server/routes_sdapi.cpp b/examples/server/routes_sdapi.cpp index 158b2afdc..4b8e7a5b1 100644 --- a/examples/server/routes_sdapi.cpp +++ b/examples/server/routes_sdapi.cpp @@ -10,6 +10,7 @@ #include "common/common.h" #include "common/media_io.h" #include "common/resource_owners.hpp" +#include "model_catalog.h" namespace fs = std::filesystem; @@ -115,8 +116,9 @@ static bool build_sdapi_img_gen_request(const json& j, request.gen_params.sample_params.sample_steps = steps; request.gen_params.batch_count = batch_size; request.gen_params.sample_params.guidance.txt_cfg = cfg_scale; - request.gen_params.width = j.value("width", -1); - request.gen_params.height = j.value("height", -1); + const bool recipe_dimensions = !img2img && model_status(runtime)["recipe"].is_string(); + request.gen_params.width = j.value("width", recipe_dimensions ? runtime.default_gen_params->width : -1); + request.gen_params.height = j.value("height", recipe_dimensions ? runtime.default_gen_params->height : -1); if (!img2img && j.value("enable_hr", false)) { request.gen_params.hires_enabled = true; @@ -151,6 +153,7 @@ static bool build_sdapi_img_gen_request(const json& j, } if (j.contains("lora") && j["lora"].is_array()) { + refresh_lora_cache(runtime); request.gen_params.lora_map.clear(); request.gen_params.high_noise_lora_map.clear(); @@ -373,6 +376,7 @@ struct SDAPIPreviewGuard { void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; + RuntimeRoutes routes(svr, rt); auto sdapi_any2img = [runtime](const httplib::Request& req, httplib::Response& res, bool img2img) { try { @@ -398,7 +402,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } - LOG_VERBOSE("%s\n", request.gen_params.to_string().c_str()); + LOG_DEBUG("%s\n", request.gen_params.to_string().c_str()); sd_img_gen_params_t img_gen_params = request.to_sd_img_gen_params_t(); SDImageVec results; @@ -473,15 +477,15 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { } }; - svr.Post("/sdapi/v1/txt2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) { + routes.post("/sdapi/v1/txt2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) { sdapi_any2img(req, res, false); }); - svr.Post("/sdapi/v1/img2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) { + routes.post("/sdapi/v1/img2img", [sdapi_any2img](const httplib::Request& req, httplib::Response& res) { sdapi_any2img(req, res, true); }); - svr.Get("/sdapi/v1/loras", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/loras", [runtime](const httplib::Request&, httplib::Response& res) { refresh_lora_cache(*runtime); json result = json::array(); @@ -489,8 +493,9 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { std::lock_guard lock(*runtime->lora_mutex); for (const auto& e : *runtime->lora_cache) { json item; - item["name"] = e.name; - item["path"] = e.path; + item["name"] = e.name; + item["path"] = e.path; + item["display_path"] = e.display_path; result.push_back(item); } } @@ -498,7 +503,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(result.dump(), "application/json"); }); - svr.Get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) { refresh_upscaler_cache(*runtime); auto make_builtin = [](const char* name) { @@ -532,7 +537,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(result.dump(), "application/json"); }); - svr.Get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) { json result = json::array({ {{"name", "Latent"}}, {{"name", "Latent (nearest)"}}, @@ -544,7 +549,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(result.dump(), "application/json"); }); - svr.Get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) { std::vector sampler_names; sampler_names.push_back("default"); for (int i = 0; i < SAMPLE_METHOD_COUNT; i++) { @@ -561,7 +566,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(r.dump(), "application/json"); }); - svr.Get("/sdapi/v1/schedulers", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/schedulers", [runtime](const httplib::Request&, httplib::Response& res) { std::vector scheduler_names; scheduler_names.push_back("default"); for (int i = 0; i < SCHEDULER_COUNT; i++) { @@ -580,7 +585,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(r.dump(), "application/json"); }); - svr.Get("/sdapi/v1/sd-models", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/sd-models", [runtime](const httplib::Request&, httplib::Response& res) { fs::path model_path = resolve_display_model_path(*runtime); json entry; entry["title"] = model_path.stem().u8string(); @@ -590,11 +595,13 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { entry["sha256"] = "8888888888888888888888888888888888888888888888888888888888888888"; entry["config"] = nullptr; json r = json::array(); - r.push_back(entry); + if (runtime->sd_ctx) { + r.push_back(entry); + } res.set_content(r.dump(), "application/json"); }); - svr.Get("/sdapi/v1/options", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdapi/v1/options", [runtime](const httplib::Request&, httplib::Response& res) { fs::path model_path = resolve_display_model_path(*runtime); json r; r["samples_format"] = "png"; @@ -602,7 +609,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(r.dump(), "application/json"); }); - svr.Get("/sdapi/v1/progress", [](const httplib::Request& req, httplib::Response& res) { + routes.get("/sdapi/v1/progress", [](const httplib::Request& req, httplib::Response& res) { bool skip_current_image = false; bool has_id_param = req.has_param("id_task"); std::string req_id_task = has_id_param ? req.get_param_value("id_task") : ""; diff --git a/examples/server/routes_sdcpp.cpp b/examples/server/routes_sdcpp.cpp index a6c8a1e8d..7e93e5edb 100644 --- a/examples/server/routes_sdcpp.cpp +++ b/examples/server/routes_sdcpp.cpp @@ -9,6 +9,7 @@ #include "common/common.h" #include "common/media_io.h" #include "common/resource_owners.hpp" +#include "model_catalog.h" namespace fs = std::filesystem; @@ -98,12 +99,13 @@ static fs::path resolve_display_model_path(const ServerRuntime& runtime) { return {}; } -static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector& skip_layers) { +static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector& skip_layers, const std::vector& custom_sigmas = {}) { const auto& guidance = sample_params.guidance; return { {"scheduler", capability_scheduler_name(sample_params.scheduler)}, {"sample_method", capability_sample_method_name(sample_params.sample_method)}, {"sample_steps", sample_params.sample_steps}, + {"custom_sigmas", custom_sigmas}, {"eta", finite_number_or_null(sample_params.eta)}, {"shifted_timestep", sample_params.shifted_timestep}, {"flow_shift", finite_number_or_null(sample_params.flow_shift)}, @@ -153,7 +155,7 @@ static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const {"increase_ref_index", defaults.increase_ref_index}, {"control_strength", defaults.control_strength}, {"ip_adapter_strength", defaults.ip_adapter_strength}, - {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)}, + {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers, defaults.custom_sigmas)}, {"hires", make_hires_json(defaults)}, {"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)}, {"cache_mode", defaults.cache_mode}, @@ -179,7 +181,7 @@ static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const {"fps", defaults.fps}, {"moe_boundary", defaults.moe_boundary}, {"vace_strength", defaults.vace_strength}, - {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)}, + {"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers, defaults.custom_sigmas)}, {"high_noise_sample_params", make_sample_params_json(defaults.high_noise_sample_params, defaults.high_noise_skip_layers)}, {"hires", make_hires_json(defaults)}, {"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)}, @@ -266,6 +268,7 @@ static json make_capabilities_json(ServerRuntime& runtime) { available_loras.push_back({ {"name", entry.name}, {"path", entry.path}, + {"display_path", entry.display_path}, }); } } @@ -344,18 +347,34 @@ static json make_capabilities_json(ServerRuntime& runtime) { default_vid_output_format = video_output_formats[0].get(); } + const json generation_features = model_generation_features(runtime); + auto recipe_features = [&](json features, bool video) { + features["init_image"] = generation_features.at(video ? "init_image" : "img2img"); + for (const char* key : {"mask_image", "ref_images", "end_image", "control_frames", "lora", "vae_tiling", "cache", "preview"}) { + if (features.contains(key) || std::string(key) == "ref_images") + features[key] = generation_features.at(key); + } + if (video) + features["high_noise_sample_params"] = generation_features.at("high_noise"); + else { + features["mask_image"] = generation_features.at("mask_image").get() && generation_features.at("img2img").get(); + features["control_image"] = generation_features.at("controlnet").get() || generation_features.at("control_image").get(); + features["ip_adapter_image"] = generation_features.at("ip_adapter"); + } + return features; + }; json defaults_by_mode = json::object(); json output_formats_by_mode = json::object(); json features_by_mode = json::object(); if (supports_img) { defaults_by_mode["img_gen"] = make_img_gen_defaults_json(defaults, default_img_output_format); output_formats_by_mode["img_gen"] = image_output_formats; - features_by_mode["img_gen"] = make_img_gen_features_json(); + features_by_mode["img_gen"] = recipe_features(make_img_gen_features_json(), false); } if (supports_vid) { defaults_by_mode["vid_gen"] = make_vid_gen_defaults_json(defaults, default_vid_output_format); output_formats_by_mode["vid_gen"] = video_output_formats; - features_by_mode["vid_gen"] = make_vid_gen_features_json(); + features_by_mode["vid_gen"] = recipe_features(make_vid_gen_features_json(), true); } json top_level_defaults = json::object(); @@ -383,19 +402,21 @@ static json make_capabilities_json(ServerRuntime& runtime) { {"stem", model_path.stem().u8string()}, {"path", model_path.u8string()}, }; - result["current_mode"] = current_mode; - result["supported_modes"] = supported_modes; - result["defaults"] = top_level_defaults; - result["defaults_by_mode"] = defaults_by_mode; - result["limits"] = { - {"min_width", 64}, - {"max_width", 4096}, - {"min_height", 64}, - {"max_height", 4096}, - {"max_batch_count", 8}, - {"max_queue_size", manager.max_pending_jobs}, - {"max_upscale_width", k_max_upscale_dimension}, - {"max_upscale_height", k_max_upscale_dimension}, + result["generation_features"] = generation_features; + result["runtime"] = model_status(runtime); + result["current_mode"] = current_mode; + result["supported_modes"] = supported_modes; + result["defaults"] = top_level_defaults; + result["defaults_by_mode"] = defaults_by_mode; + result["limits"] = { + {"min_width", 64}, + {"max_width", 4096}, + {"min_height", 64}, + {"max_height", 4096}, + {"max_batch_count", 8}, + {"max_queue_size", manager.max_pending_jobs}, + {"max_upscale_width", k_max_upscale_dimension}, + {"max_upscale_height", k_max_upscale_dimension}, }; result["samplers"] = samplers; result["schedulers"] = schedulers; @@ -470,13 +491,14 @@ static bool parse_vid_gen_request(const json& body, void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; + RuntimeRoutes routes(svr, rt); - svr.Get("/sdcpp/v1/capabilities", [runtime](const httplib::Request&, httplib::Response& res) { + routes.get("/sdcpp/v1/capabilities", [runtime](const httplib::Request&, httplib::Response& res) { res.status = 200; res.set_content(make_capabilities_json(*runtime).dump(), "application/json"); }); - svr.Post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (req.body.empty()) { res.status = 400; @@ -641,7 +663,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { } }); - svr.Post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (req.body.empty()) { res.status = 400; @@ -667,7 +689,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { std::shared_ptr job = std::make_shared(); job->kind = AsyncJobKind::ImgGen; job->status = AsyncJobStatus::Queued; - job->created_at = unix_timestamp_now(); + job->created_at = unix_timestamp_now_ms(); job->img_gen = std::move(request); { @@ -703,7 +725,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { } }); - svr.Post("/sdcpp/v1/vid_gen", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post("/sdcpp/v1/vid_gen", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (req.body.empty()) { res.status = 400; @@ -729,7 +751,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { std::shared_ptr job = std::make_shared(); job->kind = AsyncJobKind::VidGen; job->status = AsyncJobStatus::Queued; - job->created_at = unix_timestamp_now(); + job->created_at = unix_timestamp_now_ms(); job->vid_gen = std::move(request); { @@ -765,7 +787,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { } }); - svr.Get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) { AsyncJobManager& manager = *runtime->async_job_manager; std::lock_guard lock(manager.mutex); purge_expired_jobs(manager); @@ -787,7 +809,7 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(make_async_job_json(manager, *it->second).dump(), "application/json"); }); - svr.Post(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+)/cancel)", [runtime](const httplib::Request& req, httplib::Response& res) { + routes.post(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+)/cancel)", [runtime](const httplib::Request& req, httplib::Response& res) { AsyncJobManager& manager = *runtime->async_job_manager; std::lock_guard lock(manager.mutex); purge_expired_jobs(manager); diff --git a/examples/server/runtime.cpp b/examples/server/runtime.cpp index 76d89d397..34140ea35 100644 --- a/examples/server/runtime.cpp +++ b/examples/server/runtime.cpp @@ -11,6 +11,10 @@ #include "common/common.h" #include "common/log.h" +#include "model_catalog.h" + +#include +#include namespace fs = std::filesystem; @@ -210,6 +214,13 @@ ArgOptions SDSvrParams::get_options() { options.manual_options = { {"-h", "--help", "show this help message and exit", on_help_arg}, + {"", "--model-dir", "model root; repeat to search multiple directories (default: models)", + [this](int argc, const char** argv, int index) { + if (++index >= argc || argv[index][0] == '\0') + return -1; + model_dirs.push_back(argv_to_utf8(index, argv)); + return 1; + }}, }; add_log_options(options, log_level); return options; @@ -253,71 +264,79 @@ std::string SDSvrParams::to_string() const { void refresh_lora_cache(ServerRuntime& rt) { std::vector new_cache; - - if (rt.ctx_params->lora_model_dir.empty()) { - std::lock_guard lock(*rt.lora_mutex); - rt.lora_cache->clear(); - return; + std::vector directories; + if (!rt.ctx_params->lora_model_dir.empty()) { + directories.push_back(make_model_root(fs::u8path(rt.ctx_params->lora_model_dir), true)); + } else { + for (const auto& root : get_model_roots(*rt.svr_params)) { + try { + const auto paths = model_category_directories(root, "loras"); + for (size_t i = 0; i < paths.size(); ++i) { + if (!model_path_inside(root.path, fs::canonical(paths[i]))) { + LOG_WARN("LoRA directory escapes model directory: %s", paths[i].u8string().c_str()); + continue; + } + directories.push_back(make_model_root(paths[i], root.primary && lower_ascii(paths[i].filename().u8string()) == "loras")); + } + } catch (const std::exception& e) { + LOG_WARN("error while finding LoRA directories in %s: %s", root.label.c_str(), e.what()); + } + } } - - fs::path lora_dir = fs::u8path(rt.ctx_params->lora_model_dir); - std::error_code ec; - if (fs::exists(lora_dir, ec) && !ec && fs::is_directory(lora_dir, ec) && !ec) { + std::set seen; + std::map names; + for (const auto& directory : directories) { try { - auto it = fs::recursive_directory_iterator( - lora_dir, - fs::directory_options::skip_permission_denied, - ec); - auto end = fs::recursive_directory_iterator(); - while (!ec && it != end) { - std::error_code entry_ec; - bool is_reg = it->is_regular_file(entry_ec); - if (entry_ec || !is_reg) { - it.increment(ec); + if (!fs::exists(directory.path)) + continue; + for (fs::recursive_directory_iterator it(directory.path), end; it != end; ++it) { + std::error_code ec; + const auto canonical = fs::canonical(it->path(), ec); + if (ec || !model_path_inside(directory.path, canonical)) { + it.disable_recursion_pending(); + LOG_WARN("skipping inaccessible LoRA or path outside LoRA directory: %s", it->path().u8string().c_str()); continue; } - const fs::path& p = it->path(); - if (!is_supported_model_ext(p)) { - it.increment(ec); + const auto ext = lower_ascii(it->path().extension().u8string()); + if (!it->is_regular_file() || (!is_supported_model_ext(it->path()) && ext != ".ckpt" && ext != ".sft") || + !seen.insert(model_path_key(canonical)).second) continue; - } - - LoraEntry lora_entry; - lora_entry.name = p.stem().u8string(); - lora_entry.fullpath = p.u8string(); - std::string rel = p.lexically_relative(lora_dir).u8string(); - std::replace(rel.begin(), rel.end(), '\\', '/'); - lora_entry.path = rel; - - new_cache.push_back(std::move(lora_entry)); - it.increment(ec); + const auto relative = it->path().lexically_relative(directory.path).generic_u8string(); + LoraEntry entry; + entry.name = it->path().stem().u8string(); + entry.path = directory.primary ? relative : directory.id + "/" + relative; + entry.fullpath = it->path().u8string(); + entry.display_path = relative; + entry.root = directory.path; + ++names[relative]; + new_cache.push_back(std::move(entry)); } } catch (const std::exception& e) { - LOG_WARN("error while scanning lora directory '%s': %s", lora_dir.u8string().c_str(), e.what()); - return; + LOG_WARN("error while scanning LoRA directory '%s': %s", directory.path.u8string().c_str(), e.what()); } } - - if (ec) { - LOG_WARN("error while scanning lora directory '%s': %s", lora_dir.u8string().c_str(), ec.message().c_str()); - return; + for (auto& entry : new_cache) { + if (names[entry.display_path] > 1) + entry.display_path = fs::u8path(entry.fullpath).generic_u8string(); } - std::sort(new_cache.begin(), new_cache.end(), [](const LoraEntry& a, const LoraEntry& b) { return a.path < b.path; }); - - { - std::lock_guard lock(*rt.lora_mutex); - *rt.lora_cache = std::move(new_cache); - } + std::lock_guard lock(*rt.lora_mutex); + *rt.lora_cache = std::move(new_cache); } std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) { std::lock_guard lock(*rt.lora_mutex); auto it = std::find_if(rt.lora_cache->begin(), rt.lora_cache->end(), [&](const LoraEntry& entry) { return entry.path == path; }); - return it != rt.lora_cache->end() ? it->fullpath : ""; + if (it == rt.lora_cache->end()) + return ""; + std::error_code ec; + const auto canonical = fs::canonical(fs::u8path(it->fullpath), ec); + if (ec || !model_path_inside(it->root, canonical) || !fs::is_regular_file(canonical, ec) || ec) + return ""; + return canonical.u8string(); } void refresh_upscaler_cache(ServerRuntime& rt) { @@ -404,8 +423,8 @@ void refresh_upscaler_cache(ServerRuntime& rt) { } } -int64_t unix_timestamp_now() { - return std::chrono::duration_cast( +int64_t unix_timestamp_now_ms() { + return std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()) .count(); } diff --git a/examples/server/runtime.h b/examples/server/runtime.h index 5baa633a6..d9272f6c2 100644 --- a/examples/server/runtime.h +++ b/examples/server/runtime.h @@ -17,14 +17,16 @@ using json = nlohmann::json; struct ArgOptions; struct SDContextParams; struct AsyncJobManager; +struct ServerModels; struct SDSvrParams { std::string listen_ip = "127.0.0.1"; int listen_port = 1234; std::string serve_html_path; - bool normal_exit = false; + std::vector model_dirs; + bool normal_exit = false; sd_log_level_t log_level = SD_LOG_INFO; - bool color = false; + bool color = false; ArgOptions get_options(); bool validate(); @@ -36,6 +38,8 @@ struct LoraEntry { std::string name; std::string path; std::string fullpath; + std::string display_path; + std::filesystem::path root; }; struct UpscalerEntry { @@ -43,7 +47,7 @@ struct UpscalerEntry { std::string path; std::string fullpath; std::string model_name; - int scale = 4; + int scale = 4; int image_upscale_factor = 0; uintmax_t file_size = 0; std::filesystem::file_time_type last_modified; @@ -53,13 +57,14 @@ struct ServerRuntime { sd_ctx_t* sd_ctx; std::mutex* sd_ctx_mutex; const SDSvrParams* svr_params; - const SDContextParams* ctx_params; - const SDGenerationParams* default_gen_params; + SDContextParams* ctx_params; + SDGenerationParams* default_gen_params; std::vector* lora_cache; std::mutex* lora_mutex; std::vector* upscaler_cache; std::mutex* upscaler_mutex; AsyncJobManager* async_job_manager; + ServerModels* models; }; struct ImgGenJobRequest { @@ -105,4 +110,4 @@ std::string unsupported_generation_mode_error(SDMode mode); void refresh_lora_cache(ServerRuntime& rt); std::string get_lora_full_path(ServerRuntime& rt, const std::string& path); void refresh_upscaler_cache(ServerRuntime& rt); -int64_t unix_timestamp_now(); +int64_t unix_timestamp_now_ms(); diff --git a/start-server.bat b/start-server.bat new file mode 100644 index 000000000..60ea9e180 --- /dev/null +++ b/start-server.bat @@ -0,0 +1,45 @@ +@echo off +setlocal DisableDelayedExpansion + +set "SD_SERVER_PATH=%SD_SERVER_PATH%" +set "MODEL_DIR=./models" +set "COMMANDLINE_ARGS=" + +if exist "%~dp0start-server.local.bat" ( + call "%~dp0start-server.local.bat" + if errorlevel 1 exit /b 1 +) + +if defined SD_SERVER_PATH goto explicit + +for %%F in ( + "%~dp0sd-server.exe" + "%~dp0bin\Release\sd-server.exe" + "%~dp0bin\sd-server.exe" + "%~dp0build\bin\Release\sd-server.exe" + "%~dp0build\bin\sd-server.exe" +) do ( + if exist "%%~F" ( + set "SD_SERVER_PATH=%%~F" + goto run + ) +) + +>&2 echo sd-server.exe was not found. Build it first or set SD_SERVER_PATH to its executable path. +exit /b 1 + +:explicit +if exist "%SD_SERVER_PATH%" goto run +>&2 echo SD_SERVER_PATH does not exist: "%SD_SERVER_PATH%" +exit /b 1 + +:run +if "%~1"=="--version" goto version +echo Running: "%SD_SERVER_PATH%" --model-dir "%MODEL_DIR%" %COMMANDLINE_ARGS% %* +"%SD_SERVER_PATH%" --model-dir "%MODEL_DIR%" %COMMANDLINE_ARGS% %* +exit /b %errorlevel% + +:version +echo Running: "%SD_SERVER_PATH%" %* +"%SD_SERVER_PATH%" %* +exit /b %errorlevel% diff --git a/start-server.sh b/start-server.sh new file mode 100644 index 000000000..b73dc2745 --- /dev/null +++ b/start-server.sh @@ -0,0 +1,53 @@ +#!/bin/sh +set -eu + +SD_SERVER_PATH="${SD_SERVER_PATH:-}" +MODEL_DIR="./models" +COMMANDLINE_ARGS="" + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +if [ -f "$script_dir/start-server.local.sh" ]; then + . "$script_dir/start-server.local.sh" +fi + +if [ "${1:-}" != "--version" ]; then + # Split configured arguments on whitespace without expanding wildcards. + set -f + set -- --model-dir "$MODEL_DIR" $COMMANDLINE_ARGS "$@" +fi + +run_server() { + printf 'Running:' + for arg do + printf " '%s'" "$(printf '%s' "$arg" | sed "s/'/'\\\\''/g")" + done + printf '\n' + exec "$@" +} + +if [ -n "$SD_SERVER_PATH" ]; then + case "$SD_SERVER_PATH" in + */*) ;; + *) SD_SERVER_PATH="./$SD_SERVER_PATH" ;; + esac + if [ ! -f "$SD_SERVER_PATH" ] || [ ! -x "$SD_SERVER_PATH" ]; then + printf 'SD_SERVER_PATH is not an executable file: %s\n' "$SD_SERVER_PATH" >&2 + exit 1 + fi + run_server "$SD_SERVER_PATH" "$@" +fi + +for server in \ + "$script_dir/sd-server" \ + "$script_dir/bin/Release/sd-server" \ + "$script_dir/bin/sd-server" \ + "$script_dir/build/bin/Release/sd-server" \ + "$script_dir/build/bin/sd-server" +do + if [ -f "$server" ] && [ -x "$server" ]; then + run_server "$server" "$@" + fi +done + +printf '%s\n' 'sd-server was not found. Build it first or set SD_SERVER_PATH to its executable path.' >&2 +exit 1