From ccd731e7bc29d0084f446dbee15e2d000e1c6486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20du=20Hamel?= Date: Sat, 3 Oct 2026 16:35:47 +0200 Subject: [PATCH 1/2] feat: enable sdapi latent2RGB previews --- examples/server/routes_sdapi.cpp | 133 ++++++++++++++++++++++++++++++- 1 file changed, 132 insertions(+), 1 deletion(-) diff --git a/examples/server/routes_sdapi.cpp b/examples/server/routes_sdapi.cpp index eaf31db7c9..9515faf67f 100644 --- a/examples/server/routes_sdapi.cpp +++ b/examples/server/routes_sdapi.cpp @@ -301,6 +301,74 @@ static nlohmann::json prepare_info_field(const SDContextParams& ctx_params, return jsoninfo; } +namespace { + struct SdapiPreviewState { + std::mutex mutex; + std::string b64; + std::string id_task; + std::string job_type = "txt2img"; + int step = 0; + int total = 0; + bool active = false; + int job_count = 1; + int job_no = 0; + }; + + SdapiPreviewState g_preview; + +} + +static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) { + (void)is_noisy; + (void)data; + if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) { + return; + } + + const sd_image_t& frame = frames[0]; + + auto image_bytes = encode_image_to_vector(EncodedImageFormat::JPEG, + frame.data, + frame.width, + frame.height, + frame.channel, + "", + 80); + if (image_bytes.empty()) { + return; + } + + std::lock_guard lock(g_preview.mutex); + g_preview.b64 = base64_encode(image_bytes); + + if (step < g_preview.step) { + if (g_preview.job_no < g_preview.job_count - 1) { + g_preview.job_no++; + } + } + g_preview.step = step; +} + +static void set_preview_callback(const std::string& id_task, int steps, int count, const std::string& job_type) { + std::lock_guard lock(g_preview.mutex); + g_preview.id_task = id_task; + g_preview.total = steps; + g_preview.job_type = job_type; + g_preview.b64.clear(); + g_preview.step = 0; + g_preview.active = true; + g_preview.job_count = count > 0 ? count : 1; + g_preview.job_no = 0; + sd_set_preview_callback(preview_callback, PREVIEW_PROJ, 1, true, false, nullptr); +} + +static void clear_preview_callback() { + sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr); + std::lock_guard lock(g_preview.mutex); + g_preview.step = g_preview.total; + g_preview.active = false; +} + void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; @@ -317,7 +385,9 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { return; } - json j = json::parse(req.body); + std::string id_task = "sdapi"; + json j = json::parse(req.body); + id_task = j.value("id_task", id_task); ImgGenJobRequest request; std::string error_message; if (!build_sdapi_img_gen_request(j, *runtime, img2img, request, error_message)) { @@ -334,11 +404,16 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { { std::lock_guard lock(*runtime->sd_ctx_mutex); + set_preview_callback(id_task, + img_gen_params.sample_params.sample_steps, + img_gen_params.batch_count, + img2img ? "img2img" : "txt2img"); sd_image_t* raw_results = nullptr; if (!generate_image(runtime->sd_ctx, &img_gen_params, &raw_results, &num_results)) { raw_results = nullptr; num_results = 0; } + clear_preview_callback(); results.adopt(raw_results, num_results); } @@ -528,4 +603,60 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { r["sd_model_checkpoint"] = model_path.stem(); res.set_content(r.dump(), "application/json"); }); + + svr.Get("/sdapi/v1/progress", [runtime](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") : ""; + + if (req.has_param("skip_current_image")) { + std::string skip_str = req.get_param_value("skip_current_image"); + skip_current_image = (skip_str == "true" || skip_str == "1"); + } + + json r; + r["eta_relative"] = 0.0f; + json state; + state["skipped"] = false; + state["interrupted"] = false; + state["job_timestamp"] = "0"; + r["textinfo"] = nullptr; + + { + std::lock_guard lock(g_preview.mutex); + + bool is_matching_task = !has_id_param || (req_id_task == g_preview.id_task); + + int step = is_matching_task ? g_preview.step : 0; + int steps = is_matching_task ? g_preview.total : 0; + std::string b64 = is_matching_task ? g_preview.b64 : ""; + std::string job_type = is_matching_task ? g_preview.job_type : ""; + int job_count = is_matching_task ? g_preview.job_count : 1; + int job_no = is_matching_task ? g_preview.job_no : 0; + + float batch_progress = (steps > 0) ? (static_cast(step) / static_cast(steps)) : 0.0f; + if (batch_progress > 1.0f) + batch_progress = 1.0f; + + float overall_progress = (static_cast(job_no) + batch_progress) / static_cast(job_count); + if (overall_progress > 1.0f) + overall_progress = 1.0f; + + r["progress"] = overall_progress; + state["job"] = job_type; + state["sampling_step"] = step; + state["sampling_steps"] = steps; + state["job_count"] = job_count; + state["job_no"] = job_no; + r["state"] = state; + + if (!skip_current_image && !b64.empty()) { + r["current_image"] = b64; + } else { + r["current_image"] = nullptr; + } + } + + res.set_content(r.dump(), "application/json"); + }); } From d2ae8ee3f7830232c638ca63b47d0aa39a9e4a27 Mon Sep 17 00:00:00 2001 From: leejet Date: Tue, 6 Oct 2026 19:27:57 +0800 Subject: [PATCH 2/2] fix(server): correct SDAPI preview progress and task cleanup --- examples/server/api.md | 21 +++++++ examples/server/routes_sdapi.cpp | 102 +++++++++++++++---------------- 2 files changed, 72 insertions(+), 51 deletions(-) diff --git a/examples/server/api.md b/examples/server/api.md index f8f24596cf..b388e7d000 100644 --- a/examples/server/api.md +++ b/examples/server/api.md @@ -37,6 +37,7 @@ Current generation-related endpoints include: - `POST /sdapi/v1/txt2img` - `POST /sdapi/v1/img2img` +- `GET /sdapi/v1/progress` - `GET /sdapi/v1/loras` - `GET /sdapi/v1/upscalers` - `GET /sdapi/v1/latent-upscale-modes` @@ -278,6 +279,26 @@ Response fields: | `parameters` | `object` | Echo of the parsed outer request body | | `info` | `string` | Currently empty string | +#### `GET /sdapi/v1/progress` + +Poll this endpoint while a synchronous SDAPI generation request is running. +An optional `id_task` query parameter selects the task with the same `id_task` +provided in the generation request (default: `"sdapi"`). Set +`skip_current_image=true` or `1` to omit the preview image. + +`current_image` contains the latest base64 JPEG latent projection, or `null` +when unavailable. `state.sampling_step` is the positive logical step and +`state.sampling_steps` is the actual step count for the current sampling pass, +including schedule and img2img strength adjustments. `state.job_no` is the +zero-based pass index; `state.job_count` includes one pass per batch image and +an additional pass per image when highres fix is enabled. `progress` weights +these passes equally. Sampling progress can reach `1` before final decoding +finishes; `eta_relative` is currently always `0`. + +When no SDAPI task is active, or `id_task` does not match, the endpoint returns +`progress=0`, `state.job=""`, zero job/step counts, and `current_image=null`. +Completed and failed requests do not retain an active preview. + #### Discovery / Compatibility Endpoints Currently exposed: diff --git a/examples/server/routes_sdapi.cpp b/examples/server/routes_sdapi.cpp index 9515faf67f..22a710b7ea 100644 --- a/examples/server/routes_sdapi.cpp +++ b/examples/server/routes_sdapi.cpp @@ -301,22 +301,19 @@ static nlohmann::json prepare_info_field(const SDContextParams& ctx_params, return jsoninfo; } -namespace { - struct SdapiPreviewState { - std::mutex mutex; - std::string b64; - std::string id_task; - std::string job_type = "txt2img"; - int step = 0; - int total = 0; - bool active = false; - int job_count = 1; - int job_no = 0; - }; - - SdapiPreviewState g_preview; - -} +struct SDAPIPreviewState { + std::mutex mutex; + std::string b64; + std::string id_task; + std::string job_type; + int step = 0; + int total = 0; + bool active = false; + int64_t job_count = 0; + int job_no = 0; +}; + +static SDAPIPreviewState g_preview; static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) { (void)is_noisy; @@ -325,7 +322,8 @@ static void preview_callback(int step, int frame_count, sd_image_t* frames, bool return; } - const sd_image_t& frame = frames[0]; + const sd_image_t& frame = frames[0]; + const sd_preview_info_t info = sd_get_preview_info(); auto image_bytes = encode_image_to_vector(EncodedImageFormat::JPEG, frame.data, @@ -338,36 +336,40 @@ static void preview_callback(int step, int frame_count, sd_image_t* frames, bool return; } + std::string b64 = base64_encode(image_bytes); std::lock_guard lock(g_preview.mutex); - g_preview.b64 = base64_encode(image_bytes); + g_preview.b64 = std::move(b64); + g_preview.step = static_cast(std::min(step < 0 ? -static_cast(step) : step, + std::max(0, info.total_steps))); + g_preview.total = std::max(0, info.total_steps); + g_preview.job_no = std::max(0, info.sample_pass - 1); +} - if (step < g_preview.step) { - if (g_preview.job_no < g_preview.job_count - 1) { - g_preview.job_no++; - } +struct SDAPIPreviewGuard { + SDAPIPreviewGuard(const std::string& id_task, const sd_img_gen_params_t& params, const std::string& job_type) { + std::lock_guard lock(g_preview.mutex); + g_preview.id_task = id_task; + g_preview.total = 0; + g_preview.job_type = job_type; + g_preview.b64.clear(); + g_preview.step = 0; + g_preview.active = true; + // Each batch item has a base pass and, when enabled, a hires pass. + g_preview.job_count = static_cast(std::max(1, params.batch_count)) * (params.hires.enabled ? 2 : 1); + g_preview.job_no = 0; + sd_set_preview_callback(preview_callback, PREVIEW_PROJ, 1, true, false, nullptr); } - g_preview.step = step; -} -static void set_preview_callback(const std::string& id_task, int steps, int count, const std::string& job_type) { - std::lock_guard lock(g_preview.mutex); - g_preview.id_task = id_task; - g_preview.total = steps; - g_preview.job_type = job_type; - g_preview.b64.clear(); - g_preview.step = 0; - g_preview.active = true; - g_preview.job_count = count > 0 ? count : 1; - g_preview.job_no = 0; - sd_set_preview_callback(preview_callback, PREVIEW_PROJ, 1, true, false, nullptr); -} + ~SDAPIPreviewGuard() { + sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr); + std::lock_guard lock(g_preview.mutex); + g_preview.active = false; + g_preview.b64.clear(); + } -static void clear_preview_callback() { - sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr); - std::lock_guard lock(g_preview.mutex); - g_preview.step = g_preview.total; - g_preview.active = false; -} + SDAPIPreviewGuard(const SDAPIPreviewGuard&) = delete; + SDAPIPreviewGuard& operator=(const SDAPIPreviewGuard&) = delete; +}; void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; @@ -404,16 +406,12 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { { std::lock_guard lock(*runtime->sd_ctx_mutex); - set_preview_callback(id_task, - img_gen_params.sample_params.sample_steps, - img_gen_params.batch_count, - img2img ? "img2img" : "txt2img"); + SDAPIPreviewGuard preview_guard(id_task, img_gen_params, img2img ? "img2img" : "txt2img"); sd_image_t* raw_results = nullptr; if (!generate_image(runtime->sd_ctx, &img_gen_params, &raw_results, &num_results)) { raw_results = nullptr; num_results = 0; } - clear_preview_callback(); results.adopt(raw_results, num_results); } @@ -604,7 +602,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { res.set_content(r.dump(), "application/json"); }); - svr.Get("/sdapi/v1/progress", [runtime](const httplib::Request& req, httplib::Response& res) { + svr.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") : ""; @@ -625,20 +623,22 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { { std::lock_guard lock(g_preview.mutex); - bool is_matching_task = !has_id_param || (req_id_task == g_preview.id_task); + bool is_matching_task = g_preview.active && (!has_id_param || req_id_task == g_preview.id_task); int step = is_matching_task ? g_preview.step : 0; int steps = is_matching_task ? g_preview.total : 0; std::string b64 = is_matching_task ? g_preview.b64 : ""; std::string job_type = is_matching_task ? g_preview.job_type : ""; - int job_count = is_matching_task ? g_preview.job_count : 1; + int64_t job_count = is_matching_task ? g_preview.job_count : 0; int job_no = is_matching_task ? g_preview.job_no : 0; float batch_progress = (steps > 0) ? (static_cast(step) / static_cast(steps)) : 0.0f; if (batch_progress > 1.0f) batch_progress = 1.0f; - float overall_progress = (static_cast(job_no) + batch_progress) / static_cast(job_count); + float overall_progress = job_count > 0 + ? (static_cast(job_no) + batch_progress) / static_cast(job_count) + : 0.0f; if (overall_progress > 1.0f) overall_progress = 1.0f;