From 79f692340da59df0328476557a592f272620488e Mon Sep 17 00:00:00 2001 From: Nicholas Beerbower Date: Tue, 22 Sep 2026 10:50:15 -0400 Subject: [PATCH 1/3] feat: add a stand-alone upscale endpoint to the server The library has had standalone upscaling all along -- `sd-cli -M upscale` runs an ESRGAN model with no diffusion model, no text encoder and no sampling, 512x512 to 2048x2048 in about two seconds -- but over HTTP it could only be reached as the hires stage of an image generation. That means paying for a generation nobody wanted, and it is not always survivable: a hires request against Qwen-Image-2.1 dies in GGML_ASSERT(a->ne[2] == b->ne[2]) at ggml.c:4557 and takes the server with it. A plain upscale cannot be expressed through hires in any case, since hires denoising strength must be greater than zero. The route is synchronous, like the sdapi ones, because two seconds of work does not need the async job machinery. It holds the generation context mutex so an upscale and a generation never use the device at the same time, resolves the model by the same name `capabilities` reports under `upscalers`, and answers in the shape the other image endpoints use. POST /sdcpp/v1/upscale {"image": "", "upscaler": "RealESRGAN_x4plus_anime_6B", "repeats": 1, "tile_size": 128, "output_format": "png"} Two additions to `capabilities` so a client can offer this only where it works: an `upscale` flag, since a server started without --hires-upscalers-dir has the route but nothing to run; and a `model` flag on each `upscalers` entry, because that list also carries the built-in scaling filters (None, Lanczos, the latent modes), which are choices for the second stage of a generation rather than models that can be run on their own. Both are documented in examples/server/api.md alongside the endpoint. --- examples/server/api.md | 50 ++++++++++- examples/server/routes_sdcpp.cpp | 146 +++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 1 deletion(-) diff --git a/examples/server/api.md b/examples/server/api.md index 3ec9a1da8f..841737ac3c 100644 --- a/examples/server/api.md +++ b/examples/server/api.md @@ -56,6 +56,7 @@ Current endpoints include: - `GET /sdcpp/v1/jobs/{id}` - `POST /sdcpp/v1/jobs/{id}/cancel` - `POST /sdcpp/v1/vid_gen` +- `POST /sdcpp/v1/upscale` ## `sd_cpp_extra_args` @@ -421,7 +422,8 @@ Top-level fields: | `samplers` | `array` | Available sampling methods | | `schedulers` | `array` | Available schedulers | | `loras` | `array` | Available LoRA entries | -| `upscalers` | `array` | Available model-backed highres upscalers | +| `upscalers` | `array` | Available highres upscalers, built-in and model-backed | +| `upscale` | `boolean` | Whether `POST /sdcpp/v1/upscale` can do anything here | | `limits` | `object` | Shared queue and size limits | `model` @@ -463,6 +465,7 @@ Shared nested fields: | Field | Type | Notes | | --- | --- | --- | | `upscalers[].name` | `string` | Built-in name or model stem; use this value in `hires.upscaler` | +| `upscalers[].model` | `boolean` | True for a model-backed upscaler, false for a built-in scaling filter | Built-in entries include `None`, `Lanczos`, `Nearest`, `Latent`, `Latent (nearest)`, `Latent (nearest-exact)`, `Latent (antialiased)`, `Latent (bicubic)`, and `Latent (bicubic antialiased)`. Model-backed entries are scanned from the top level of `--hires-upscalers-dir`; subdirectories are not scanned. @@ -628,6 +631,51 @@ Typical status codes: - `404 Not Found` - `410 Gone` +#### `POST /sdcpp/v1/upscale` + +Runs one model-backed upscaler over an image, with no generation involved. + +This is the HTTP equivalent of `sd-cli -M upscale`: no diffusion model, text +encoder or sampling is used, so it is fast enough to answer synchronously and +does not create a job. + +Request fields: + +| Field | Type | Notes | +| --- | --- | --- | +| `image` | `string` | Required. Base64 or data URL image | +| `upscaler` | `string` | A model-backed name from `upscalers`; the first one when omitted | +| `repeats` | `integer` | Run the upscaler this many times, 1 to 4 (default `1`) | +| `tile_size` | `integer` | Tile size, defaulting to the server's `--upscale-tile-size` | +| `output_format` | `string` | `png`, `jpeg`, or `webp` (default `png`) | +| `output_compression` | `integer` | Range is clamped to `0..100` | + +Response fields: + +| Field | Type | Notes | +| --- | --- | --- | +| `images` | `array` | One image | +| `images[].index` | `integer` | | +| `images[].b64_json` | `string` | Base64-encoded image bytes | +| `upscaler` | `string` | The upscaler actually used | +| `scale` | `integer` | The model's scale factor | +| `repeats` | `integer` | How many times it was run | +| `width` | `integer` | Result width | +| `height` | `integer` | Result height | +| `output_format` | `string` | Final encoded image format | + +Typical status codes: + +- `200 OK` +- `400 Bad Request` (no image, unreadable image, or no such upscaler) +- `500 Internal Server Error` + +Notes: + +- The upscaler models are three-channel; alpha is not preserved. +- The request holds the generation context lock, so an upscale and a + generation never run on the device at the same time. + #### `POST /sdcpp/v1/jobs/{id}/cancel` Attempts to cancel an accepted job. diff --git a/examples/server/routes_sdcpp.cpp b/examples/server/routes_sdcpp.cpp index 7253dbf496..45f7df8ba0 100644 --- a/examples/server/routes_sdcpp.cpp +++ b/examples/server/routes_sdcpp.cpp @@ -6,6 +6,8 @@ #include "async_jobs.h" #include "common/common.h" +#include "common/media_io.h" +#include "common/resource_owners.hpp" namespace fs = std::filesystem; @@ -239,37 +241,49 @@ static json make_capabilities_json(ServerRuntime& runtime) { available_upscalers.push_back({ {"name", "None"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Lanczos"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Nearest"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent (nearest)"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent (nearest-exact)"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent (antialiased)"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent (bicubic)"}, + {"model", false}, }); available_upscalers.push_back({ {"name", "Latent (bicubic antialiased)"}, + {"model", false}, }); + bool have_upscaler_models = false; { std::lock_guard lock(*runtime.upscaler_mutex); for (const auto& entry : *runtime.upscaler_cache) { available_upscalers.push_back({ {"name", entry.name}, + {"model", true}, }); + have_upscaler_models = true; } } @@ -348,6 +362,10 @@ static json make_capabilities_json(ServerRuntime& runtime) { result["features_by_mode"] = features_by_mode; result["loras"] = available_loras; result["upscalers"] = available_upscalers; + // Whether POST /sdcpp/v1/upscale will do anything here, so a client can + // offer upscaling only when it is actually available rather than finding + // out by being refused. + result["upscale"] = have_upscaler_models; return result; } @@ -413,6 +431,134 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(make_capabilities_json(*runtime).dump(), "application/json"); }); + // Upscaling on its own, without a generation wrapped around it. + // + // The library has had this all along -- `sd-cli -M upscale` runs an + // ESRGAN model with no diffusion model, no text encoder and no sampling, + // in a couple of seconds -- but the only way to reach it over HTTP was + // the hires stage of an image generation, which means paying for a + // generation you did not want and, on some models, does not survive the + // latent shapes involved. + svr.Post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) { + try { + if (req.body.empty()) { + res.status = 400; + res.set_content(R"({"error":"empty body"})", "application/json"); + return; + } + json body = json::parse(req.body); + + const std::string encoded = body.value("image", std::string()); + if (encoded.empty()) { + res.status = 400; + res.set_content(R"({"error":"image is required"})", "application/json"); + return; + } + SDImageOwner input; + if (!decode_base64_image(encoded, 3, 0, 0, input) || input.get().data == nullptr) { + res.status = 400; + res.set_content(R"({"error":"image could not be read"})", "application/json"); + return; + } + + // Named as `upscalers` in capabilities reports them; the first + // model-backed one when the request does not say. + refresh_upscaler_cache(*runtime); + const std::string wanted = body.value("upscaler", std::string()); + std::string model_path; + std::string used_name; + { + std::lock_guard lock(*runtime->upscaler_mutex); + for (const auto& entry : *runtime->upscaler_cache) { + if (wanted.empty() || entry.name == wanted) { + model_path = entry.fullpath; + used_name = entry.name; + break; + } + } + } + if (model_path.empty()) { + res.status = 400; + res.set_content(json({{"error", wanted.empty() + ? std::string("no upscaler models are available; " + "start the server with --hires-upscalers-dir") + : "no upscaler called " + wanted}}) + .dump(), + "application/json"); + return; + } + + const int tile_size = std::max(32, body.value("tile_size", runtime->default_gen_params->upscale_tile_size)); + const int repeats = std::clamp(body.value("repeats", 1), 1, 4); + + // One GPU: an upscale must not run while a generation is using it. + std::lock_guard ctx_lock(*runtime->sd_ctx_mutex); + UpscalerCtxPtr upscaler_ctx(new_upscaler_ctx(model_path.c_str(), + runtime->ctx_params->diffusion_conv_direct, + runtime->ctx_params->n_threads, + tile_size, + runtime->ctx_params->backend.c_str(), + runtime->ctx_params->params_backend.c_str())); + if (upscaler_ctx == nullptr) { + res.status = 500; + res.set_content(R"({"error":"the upscaler model could not be loaded"})", "application/json"); + return; + } + const int factor = get_upscale_factor(upscaler_ctx.get()); + + SDImageOwner current(input.release()); + for (int i = 0; i < repeats; ++i) { + sd_image_t* out_images = nullptr; + int out_count = 0; + if (!upscale(upscaler_ctx.get(), current.get(), (uint32_t)factor, &out_images, &out_count) || + out_count <= 0 || out_images[0].data == nullptr) { + free_sd_images(out_images, out_count); + res.status = 500; + res.set_content(R"({"error":"upscale failed"})", "application/json"); + return; + } + sd_image_t produced = out_images[0]; + out_images[0] = {0, 0, 0, nullptr}; + free_sd_images(out_images, out_count); + current.reset(produced); + } + + const std::string format = body.value("output_format", std::string("png")); + const int compression = std::clamp(body.value("output_compression", 100), 0, 100); + const sd_image_t result = current.get(); + auto image_bytes = encode_image_to_vector(format == "jpeg" ? EncodedImageFormat::JPEG + : format == "webp" ? EncodedImageFormat::WEBP + : EncodedImageFormat::PNG, + result.data, + result.width, + result.height, + result.channel, + "", + compression); + if (image_bytes.empty()) { + res.status = 500; + res.set_content(R"({"error":"the result could not be encoded"})", "application/json"); + return; + } + + json out; + out["upscaler"] = used_name; + out["scale"] = factor; + out["repeats"] = repeats; + out["width"] = result.width; + out["height"] = result.height; + out["output_format"] = format == "jpeg" ? "jpeg" : format == "webp" ? "webp" : "png"; + json images = json::array(); + images.push_back({{"index", 0}, {"b64_json", base64_encode(image_bytes)}}); + out["images"] = std::move(images); + res.set_content(out.dump(), "application/json"); + res.status = 200; + } catch (const std::exception& e) { + res.status = 500; + res.set_content(json({{"error", std::string("server_error: ") + e.what()}}).dump(), "application/json"); + } + }); + svr.Post("/sdcpp/v1/img_gen", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (req.body.empty()) { From 1ec9afab5ae59d698c8c597f0db8ff15d396f36d Mon Sep 17 00:00:00 2001 From: leejet Date: Thu, 24 Sep 2026 00:25:31 +0800 Subject: [PATCH 2/3] fix: validate upscale requests and reject incompatible models --- examples/server/api.md | 14 ++-- examples/server/routes_sdcpp.cpp | 139 +++++++++++++++++++++++-------- examples/server/runtime.cpp | 27 +++++- examples/server/runtime.h | 4 + include/stable-diffusion.h | 2 + src/name_conversion.cpp | 8 +- src/upscaler.cpp | 24 ++++++ 7 files changed, 168 insertions(+), 50 deletions(-) diff --git a/examples/server/api.md b/examples/server/api.md index 841737ac3c..cbc6a24eb3 100644 --- a/examples/server/api.md +++ b/examples/server/api.md @@ -423,7 +423,7 @@ Top-level fields: | `schedulers` | `array` | Available schedulers | | `loras` | `array` | Available LoRA entries | | `upscalers` | `array` | Available highres upscalers, built-in and model-backed | -| `upscale` | `boolean` | Whether `POST /sdcpp/v1/upscale` can do anything here | +| `upscale` | `boolean` | Whether a compatible RGB ESRGAN model is available for `POST /sdcpp/v1/upscale` | | `limits` | `object` | Shared queue and size limits | `model` @@ -466,6 +466,7 @@ Shared nested fields: | --- | --- | --- | | `upscalers[].name` | `string` | Built-in name or model stem; use this value in `hires.upscaler` | | `upscalers[].model` | `boolean` | True for a model-backed upscaler, false for a built-in scaling filter | +| `upscalers[].image_upscale` | `boolean` | Whether this model can be selected by `POST /sdcpp/v1/upscale`; false for latent upscalers and built-in filters | Built-in entries include `None`, `Lanczos`, `Nearest`, `Latent`, `Latent (nearest)`, `Latent (nearest-exact)`, `Latent (antialiased)`, `Latent (bicubic)`, and `Latent (bicubic antialiased)`. Model-backed entries are scanned from the top level of `--hires-upscalers-dir`; subdirectories are not scanned. @@ -479,6 +480,8 @@ Built-in entries include `None`, `Lanczos`, `Nearest`, `Latent`, `Latent (neares | `limits.max_height` | `integer` | | `limits.max_batch_count` | `integer` | | `limits.max_queue_size` | `integer` | +| `limits.max_upscale_width` | `integer` | +| `limits.max_upscale_height` | `integer` | Shared default fields used by both `img_gen` and `vid_gen`: @@ -633,7 +636,7 @@ Typical status codes: #### `POST /sdcpp/v1/upscale` -Runs one model-backed upscaler over an image, with no generation involved. +Runs one RGB ESRGAN upscaler over an image, with no generation involved. Latent upscaler models remain available for hires generation but cannot be used here. This is the HTTP equivalent of `sd-cli -M upscale`: no diffusion model, text encoder or sampling is used, so it is fast enough to answer synchronously and @@ -644,10 +647,10 @@ Request fields: | Field | Type | Notes | | --- | --- | --- | | `image` | `string` | Required. Base64 or data URL image | -| `upscaler` | `string` | A model-backed name from `upscalers`; the first one when omitted | +| `upscaler` | `string` | A name from `upscalers` with `image_upscale: true`; the first compatible entry when omitted | | `repeats` | `integer` | Run the upscaler this many times, 1 to 4 (default `1`) | | `tile_size` | `integer` | Tile size, defaulting to the server's `--upscale-tile-size` | -| `output_format` | `string` | `png`, `jpeg`, or `webp` (default `png`) | +| `output_format` | `string` | `png`, `jpeg`, or `webp` when built with WebP support (default `png`); unsupported formats return 400 | | `output_compression` | `integer` | Range is clamped to `0..100` | Response fields: @@ -667,11 +670,12 @@ Response fields: Typical status codes: - `200 OK` -- `400 Bad Request` (no image, unreadable image, or no such upscaler) +- `400 Bad Request` (invalid request, unsupported output format, unreadable image, incompatible upscaler, or output dimensions exceeding the limit) - `500 Internal Server Error` Notes: +- Final output dimensions, including all repeats, must not exceed 8192 pixels on either axis (`limits.max_upscale_width` and `limits.max_upscale_height`). Requests exceeding this bound are rejected before upscaling. - The upscaler models are three-channel; alpha is not preserved. - The request holds the generation context lock, so an upscale and a generation never run on the device at the same time. diff --git a/examples/server/routes_sdcpp.cpp b/examples/server/routes_sdcpp.cpp index 45f7df8ba0..65b32d9e64 100644 --- a/examples/server/routes_sdcpp.cpp +++ b/examples/server/routes_sdcpp.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "async_jobs.h" #include "common/common.h" @@ -11,6 +12,24 @@ namespace fs = std::filesystem; +static constexpr uint32_t k_max_upscale_dimension = 8192; + +static bool valid_upscale_dimensions(const sd_image_t& image, int factor, int repeats) { + if (image.width == 0 || image.height == 0 || factor < 1 || repeats < 1 || repeats > 4) { + return false; + } + uint32_t width = image.width; + uint32_t height = image.height; + for (int i = 0; i < repeats; ++i) { + if (width > k_max_upscale_dimension / factor || height > k_max_upscale_dimension / factor) { + return false; + } + width *= factor; + height *= factor; + } + return true; +} + static bool parse_cache_mode(const std::string& mode_str, sd_cache_mode_t& mode_out) { if (mode_str == "disabled") { mode_out = SD_CACHE_DISABLED; @@ -242,38 +261,47 @@ static json make_capabilities_json(ServerRuntime& runtime) { available_upscalers.push_back({ {"name", "None"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Lanczos"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Nearest"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent (nearest)"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent (nearest-exact)"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent (antialiased)"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent (bicubic)"}, {"model", false}, + {"image_upscale", false}, }); available_upscalers.push_back({ {"name", "Latent (bicubic antialiased)"}, {"model", false}, + {"image_upscale", false}, }); bool have_upscaler_models = false; { @@ -282,8 +310,9 @@ static json make_capabilities_json(ServerRuntime& runtime) { available_upscalers.push_back({ {"name", entry.name}, {"model", true}, + {"image_upscale", entry.image_upscale_factor > 0}, }); - have_upscaler_models = true; + have_upscaler_models = have_upscaler_models || entry.image_upscale_factor > 0; } } @@ -347,12 +376,14 @@ static json make_capabilities_json(ServerRuntime& runtime) { 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}, + {"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; @@ -362,9 +393,6 @@ static json make_capabilities_json(ServerRuntime& runtime) { result["features_by_mode"] = features_by_mode; result["loras"] = available_loras; result["upscalers"] = available_upscalers; - // Whether POST /sdcpp/v1/upscale will do anything here, so a client can - // offer upscaling only when it is actually available rather than finding - // out by being refused. result["upscale"] = have_upscaler_models; return result; } @@ -431,14 +459,6 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(make_capabilities_json(*runtime).dump(), "application/json"); }); - // Upscaling on its own, without a generation wrapped around it. - // - // The library has had this all along -- `sd-cli -M upscale` runs an - // ESRGAN model with no diffusion model, no text encoder and no sampling, - // in a couple of seconds -- but the only way to reach it over HTTP was - // the hires stage of an image generation, which means paying for a - // generation you did not want and, on some models, does not survive the - // latent shapes involved. svr.Post("/sdcpp/v1/upscale", [runtime](const httplib::Request& req, httplib::Response& res) { try { if (req.body.empty()) { @@ -447,6 +467,40 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } json body = json::parse(req.body); + if (!body.is_object()) { + res.status = 400; + res.set_content(R"({"error":"body must be an object"})", "application/json"); + return; + } + for (const char* key : {"repeats", "tile_size", "output_compression"}) { + if (!body.contains(key)) { + continue; + } + const auto& value = body[key]; + const bool valid = value.is_number_unsigned() + ? value.get() <= static_cast(std::numeric_limits::max()) + : value.is_number_integer() && value.get() >= std::numeric_limits::min() && + value.get() <= std::numeric_limits::max(); + if (!valid) { + res.status = 400; + res.set_content(json({{"error", std::string(key) + " must be a 32-bit integer"}}).dump(), "application/json"); + return; + } + } + ImgGenJobRequest output_options; + std::string error_message; + if (!assign_output_options(output_options, + body.value("output_format", std::string("png")), + body.value("output_compression", 100), + true, + error_message)) { + res.status = 400; + res.set_content(json({{"error", error_message}}).dump(), "application/json"); + return; + } + const int tile_size = std::max(32, body.value("tile_size", runtime->default_gen_params->upscale_tile_size)); + const int repeats = std::clamp(body.value("repeats", 1), 1, 4); + const std::string wanted = body.value("upscaler", std::string()); const std::string encoded = body.value("image", std::string()); if (encoded.empty()) { @@ -461,18 +515,17 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } - // Named as `upscalers` in capabilities reports them; the first - // model-backed one when the request does not say. refresh_upscaler_cache(*runtime); - const std::string wanted = body.value("upscaler", std::string()); + int model_scale = 0; std::string model_path; std::string used_name; { std::lock_guard lock(*runtime->upscaler_mutex); for (const auto& entry : *runtime->upscaler_cache) { - if (wanted.empty() || entry.name == wanted) { - model_path = entry.fullpath; - used_name = entry.name; + if (entry.image_upscale_factor > 0 && (wanted.empty() || entry.name == wanted)) { + model_path = entry.fullpath; + used_name = entry.name; + model_scale = entry.image_upscale_factor; break; } } @@ -480,16 +533,19 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { if (model_path.empty()) { res.status = 400; res.set_content(json({{"error", wanted.empty() - ? std::string("no upscaler models are available; " + ? std::string("no RGB ESRGAN upscaler models are available; " "start the server with --hires-upscalers-dir") - : "no upscaler called " + wanted}}) + : "no compatible image upscaler called " + wanted}}) .dump(), "application/json"); return; } - const int tile_size = std::max(32, body.value("tile_size", runtime->default_gen_params->upscale_tile_size)); - const int repeats = std::clamp(body.value("repeats", 1), 1, 4); + if (!valid_upscale_dimensions(input.get(), model_scale, repeats)) { + res.status = 400; + res.set_content(R"({"error":"upscaled dimensions must not exceed 8192 x 8192"})", "application/json"); + return; + } // One GPU: an upscale must not run while a generation is using it. std::lock_guard ctx_lock(*runtime->sd_ctx_mutex); @@ -505,6 +561,12 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } const int factor = get_upscale_factor(upscaler_ctx.get()); + // The model file may have changed since its metadata was cached. + if (!valid_upscale_dimensions(input.get(), factor, repeats)) { + res.status = 400; + res.set_content(R"({"error":"upscaled dimensions must not exceed 8192 x 8192"})", "application/json"); + return; + } SDImageOwner current(input.release()); for (int i = 0; i < repeats; ++i) { @@ -523,12 +585,12 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { current.reset(produced); } - const std::string format = body.value("output_format", std::string("png")); - const int compression = std::clamp(body.value("output_compression", 100), 0, 100); - const sd_image_t result = current.get(); - auto image_bytes = encode_image_to_vector(format == "jpeg" ? EncodedImageFormat::JPEG - : format == "webp" ? EncodedImageFormat::WEBP - : EncodedImageFormat::PNG, + const std::string& format = output_options.output_format; + const int compression = output_options.output_compression; + const sd_image_t result = current.get(); + auto image_bytes = encode_image_to_vector(format == "jpeg" ? EncodedImageFormat::JPEG + : format == "webp" ? EncodedImageFormat::WEBP + : EncodedImageFormat::PNG, result.data, result.width, result.height, @@ -547,12 +609,15 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) { out["repeats"] = repeats; out["width"] = result.width; out["height"] = result.height; - out["output_format"] = format == "jpeg" ? "jpeg" : format == "webp" ? "webp" : "png"; - json images = json::array(); + out["output_format"] = format; + json images = json::array(); images.push_back({{"index", 0}, {"b64_json", base64_encode(image_bytes)}}); - out["images"] = std::move(images); + out["images"] = std::move(images); res.set_content(out.dump(), "application/json"); res.status = 200; + } catch (const json::exception& e) { + res.status = 400; + res.set_content(json({{"error", "invalid request"}, {"message", e.what()}}).dump(), "application/json"); } catch (const std::exception& e) { res.status = 500; res.set_content(json({{"error", std::string("server_error: ") + e.what()}}).dump(), "application/json"); diff --git a/examples/server/runtime.cpp b/examples/server/runtime.cpp index cd0e2e722b..beb553e481 100644 --- a/examples/server/runtime.cpp +++ b/examples/server/runtime.cpp @@ -295,6 +295,11 @@ std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) { void refresh_upscaler_cache(ServerRuntime& rt) { std::vector new_cache; + std::vector previous_cache; + { + std::lock_guard lock(*rt.upscaler_mutex); + previous_cache = *rt.upscaler_cache; + } fs::path upscaler_dir = rt.ctx_params->hires_upscalers_dir; if (fs::exists(upscaler_dir) && fs::is_directory(upscaler_dir)) { @@ -308,10 +313,24 @@ void refresh_upscaler_cache(ServerRuntime& rt) { } UpscalerEntry upscaler_entry; - upscaler_entry.name = p.stem().u8string(); - upscaler_entry.fullpath = fs::absolute(p).lexically_normal().u8string(); - upscaler_entry.model_name = "ESRGAN_4x"; - upscaler_entry.path = p.filename().u8string(); + upscaler_entry.name = p.stem().u8string(); + upscaler_entry.fullpath = fs::absolute(p).lexically_normal().u8string(); + upscaler_entry.model_name = "ESRGAN_4x"; + upscaler_entry.path = p.filename().u8string(); + upscaler_entry.file_size = entry.file_size(); + upscaler_entry.last_modified = entry.last_write_time(); + auto previous = std::find_if(previous_cache.begin(), previous_cache.end(), [&](const UpscalerEntry& cached) { + return cached.fullpath == upscaler_entry.fullpath && + cached.file_size == upscaler_entry.file_size && + cached.last_modified == upscaler_entry.last_modified; + }); + upscaler_entry.image_upscale_factor = previous != previous_cache.end() + ? previous->image_upscale_factor + : get_upscaler_model_scale(upscaler_entry.fullpath.c_str()); + if (upscaler_entry.image_upscale_factor > 0) { + upscaler_entry.scale = upscaler_entry.image_upscale_factor; + upscaler_entry.model_name = "ESRGAN_" + std::to_string(upscaler_entry.scale) + "x"; + } new_cache.push_back(std::move(upscaler_entry)); } diff --git a/examples/server/runtime.h b/examples/server/runtime.h index 677bb0ba0c..b4560f1276 100644 --- a/examples/server/runtime.h +++ b/examples/server/runtime.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -43,6 +44,9 @@ struct UpscalerEntry { std::string fullpath; std::string model_name; int scale = 4; + int image_upscale_factor = 0; + uintmax_t file_size = 0; + std::filesystem::file_time_type last_modified; }; struct ServerRuntime { diff --git a/include/stable-diffusion.h b/include/stable-diffusion.h index 9173240be6..2410d96e8c 100644 --- a/include/stable-diffusion.h +++ b/include/stable-diffusion.h @@ -550,6 +550,8 @@ SD_API bool upscale(upscaler_ctx_t* upscaler_ctx, int* num_images_out); SD_API int get_upscale_factor(upscaler_ctx_t* upscaler_ctx); +// Reads model metadata only; returns 0 if the file is not a recognized RGB ESRGAN model. +SD_API int get_upscaler_model_scale(const char* model_path); typedef struct adetailer_ctx_t adetailer_ctx_t; diff --git a/src/name_conversion.cpp b/src/name_conversion.cpp index 6f1b598e88..3251682350 100644 --- a/src/name_conversion.cpp +++ b/src/name_conversion.cpp @@ -1349,9 +1349,8 @@ bool is_first_stage_model_name(const std::string& name) { } static std::string convert_esrgan_tensor_name(std::string name) { - static std::unordered_map esrgan_name_map; - - if (esrgan_name_map.empty()) { + static const auto esrgan_name_map = [] { + std::unordered_map esrgan_name_map; esrgan_name_map["model.0."] = "conv_first."; constexpr int max_num_blocks = 64; @@ -1377,7 +1376,8 @@ static std::string convert_esrgan_tensor_name(std::string name) { esrgan_name_map["model.7."] = "conv_last."; esrgan_name_map["model.8."] = "conv_hr."; esrgan_name_map["model.10."] = "conv_last."; - } + return esrgan_name_map; + }(); replace_with_prefix_map(name, esrgan_name_map); return name; diff --git a/src/upscaler.cpp b/src/upscaler.cpp index 341c76ecfc..625fd746b6 100644 --- a/src/upscaler.cpp +++ b/src/upscaler.cpp @@ -242,6 +242,30 @@ int get_upscale_factor(upscaler_ctx_t* upscaler_ctx) { return upscaler_ctx->upscaler->esrgan_upscaler->config.scale; } +int get_upscaler_model_scale(const char* model_path) { + if (model_path == nullptr || model_path[0] == '\0') { + return 0; + } + try { + ModelLoader loader; + if (!loader.init_from_file_and_convert_name(model_path, "", VERSION_ESRGAN)) { + return 0; + } + const auto& tensors = loader.get_tensor_storage_map(); + auto first = tensors.find("conv_first.weight"); + auto last = tensors.find("conv_last.weight"); + if (first == tensors.end() || last == tensors.end() || + tensors.count("body.0.rdb1.conv1.weight") == 0 || + first->second.n_dims != 4 || last->second.n_dims != 4 || + first->second.ne[2] != 3 || last->second.ne[3] != 3) { + return 0; + } + return ESRGANConfig::detect_from_weights(tensors).scale; + } catch (const std::exception&) { + return 0; + } +} + void free_upscaler_ctx(upscaler_ctx_t* upscaler_ctx) { if (upscaler_ctx->upscaler != nullptr) { delete upscaler_ctx->upscaler; From b4e1071c742973ed1da652251721d045b1a2a44e Mon Sep 17 00:00:00 2001 From: leejet Date: Thu, 24 Sep 2026 00:26:28 +0800 Subject: [PATCH 3/3] format code --- examples/server/routes_sdcpp.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/server/routes_sdcpp.cpp b/examples/server/routes_sdcpp.cpp index 65b32d9e64..80b57be7e2 100644 --- a/examples/server/routes_sdcpp.cpp +++ b/examples/server/routes_sdcpp.cpp @@ -376,14 +376,14 @@ static json make_capabilities_json(ServerRuntime& runtime) { 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}, + {"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;