diff --git a/examples/server/api.md b/examples/server/api.md index f8f24596c..b388e7d00 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 eaf31db7c..22a710b7e 100644 --- a/examples/server/routes_sdapi.cpp +++ b/examples/server/routes_sdapi.cpp @@ -301,6 +301,76 @@ static nlohmann::json prepare_info_field(const SDContextParams& ctx_params, return jsoninfo; } +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; + (void)data; + if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) { + return; + } + + 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, + frame.width, + frame.height, + frame.channel, + "", + 80); + if (image_bytes.empty()) { + return; + } + + std::string b64 = base64_encode(image_bytes); + std::lock_guard lock(g_preview.mutex); + 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); +} + +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); + } + + ~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(); + } + + SDAPIPreviewGuard(const SDAPIPreviewGuard&) = delete; + SDAPIPreviewGuard& operator=(const SDAPIPreviewGuard&) = delete; +}; + void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { ServerRuntime* runtime = &rt; @@ -317,7 +387,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,6 +406,7 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) { { std::lock_guard lock(*runtime->sd_ctx_mutex); + 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; @@ -528,4 +601,62 @@ 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", [](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 = 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 : ""; + 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 = job_count > 0 + ? (static_cast(job_no) + batch_progress) / static_cast(job_count) + : 0.0f; + 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"); + }); }