From fc350be2ceeeb859bcc201ac435591ed7d46821d Mon Sep 17 00:00:00 2001 From: leejet Date: Mon, 9 Nov 2026 02:32:27 +0800 Subject: [PATCH] feat(server): report generation stages independently of previews (#2303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Maxime DrĂ©an --- examples/server/README.md | 10 ++++ examples/server/api.md | 52 +++++++++++++++++ examples/server/async_jobs.cpp | 96 ++++++++++++++++++++++++++++--- examples/server/async_jobs.h | 14 +++++ examples/server/frontend | 2 +- include/stable-diffusion.h | 14 +++++ src/pipeline/diffusion_engine.cpp | 8 +++ src/pipeline/diffusion_engine.h | 5 ++ src/pipeline/image.cpp | 9 ++- src/pipeline/video.cpp | 8 ++- src/runtime/generation_progress.h | 42 ++++++++++++++ src/stable-diffusion.cpp | 10 ++++ 12 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 src/runtime/generation_progress.h diff --git a/examples/server/README.md b/examples/server/README.md index 8d8f687c..d93d829b 100644 --- a/examples/server/README.md +++ b/examples/server/README.md @@ -76,6 +76,16 @@ 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 generation status bar shows the current stage and elapsed time even when +Preview Mode is `none`. During sampling it shows completed steps for the current +pass; prompt encoding and VAE decoding show an activity indicator when their +total work is unknown. Sampling reaching 100% does not mean the job is finished: +decoding, ADetailer, and output encoding may still follow. +The canvas keeps the submitted output aspect ratio, and stage changes keep the +same layout. Editing the next request's resolution does not resize the current +preview or result. + 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 diff --git a/examples/server/api.md b/examples/server/api.md index 9c8d8c8c..f6096c67 100644 --- a/examples/server/api.md +++ b/examples/server/api.md @@ -775,6 +775,7 @@ Field types: | `queue_position` | `integer` | | `result` | `object \| null` | | `error` | `object \| null` | +| `progress` | `object` | | `preview` | `object \| null` | `created`, `started`, and `completed` are integer Unix timestamps in @@ -782,6 +783,57 @@ Field types: the elapsed generation time in seconds, including output encoding and excluding queue time. +`progress` is available independently of the preview setting, for both image +and video jobs. Continue polling until `status` is terminal: a sampling pass +can finish before decoding, ADetailer, or output encoding finishes. + +| Field | Type | Notes | +| --- | --- | --- | +| `progress.stage` | `string` | Current execution stage; see below | +| `progress.detail` | `string` | Stage detail, or an empty string | +| `progress.pass` | `integer` | Sampling pass number within the job; zero before sampling | +| `progress.completed` | `integer \| null` | Completed units in the current stage/pass | +| `progress.total` | `integer \| null` | Actual total units, or null when unknown | +| `progress.unit` | `string \| null` | `steps` for sampling, `images` for image output encoding; null when uncounted | +| `progress.stage_elapsed_ms` | `integer` | Elapsed time in the current stage/detail/pass | +| `progress.elapsed_ms` | `integer` | Job execution time, excluding queue time; zero before execution | + +Stages include `queued`, `waiting` (waiting for the execution lock), `preparing`, +`conditioning`, `sampling`, `vae_encode`, `vae_decode`, `audio_decode`, +`upscaling`, `adetailer`, and `encoding`. Stages may repeat or be skipped. +Details include `positive_prompt` / `negative_prompt` / `image_condition` for +conditioning, `high_noise` / `low_noise` for two-stage sampling, and `image` / +`video` for output encoding. During ADetailer, `detail` identifies the inner +stage, such as `conditioning` or `sampling`. Clients should tolerate unknown +stage, detail, and unit values. + +Sampling counts completed logical steps using the actual schedule, including +strength adjustments. Each new sampling pass increments `pass` and starts at +zero, including batch items, highres passes, high/low-noise passes, and ADetailer +passes. The ratio is **current-pass progress**, not overall job completion. +The progress snapshot is published atomically. Preview metadata still describes +the preview frame, which may be older than the current progress. + +Unknown totals are represented by null counts, not an estimated percentage. +For example, prompt encoding can report: + +```json +"progress": { + "stage": "conditioning", + "detail": "positive_prompt", + "pass": 0, + "completed": null, + "total": null, + "unit": null, + "stage_elapsed_ms": 3200, + "elapsed_ms": 4100 +} +``` + +Elapsed times freeze when a job completes, fails, or is cancelled; the last +stage is retained for inspection. Use `status` to determine the final outcome. +Elapsed time alone does not indicate that computation is advancing. + `preview` sub-fields: | Field | Type | Notes | diff --git a/examples/server/async_jobs.cpp b/examples/server/async_jobs.cpp index 7a801078..c730b9c1 100644 --- a/examples/server/async_jobs.cpp +++ b/examples/server/async_jobs.cpp @@ -92,6 +92,63 @@ static void set_preview_callback(AsyncGenerationJob& job) { sd_set_preview_callback(preview_callback, mode, interval, true, false, &job); } +static void update_job_progress(AsyncGenerationJob& job, const char* stage, const char* detail = "", int sample_pass = -1, int64_t completed = 0, int64_t total = 0, const char* unit = "") { + std::lock_guard lock(job.progress_mutex); + auto& progress = job.progress; + if (sample_pass < 0) { + sample_pass = progress.sample_pass; + } + if (progress.stage != stage || progress.detail != detail || progress.sample_pass != sample_pass) { + progress.stage_started = std::chrono::steady_clock::now(); + } + progress.stage = stage; + progress.detail = detail; + progress.sample_pass = sample_pass; + progress.completed = completed; + progress.total = total; + progress.unit = unit; +} + +static void finish_job_progress(AsyncGenerationJob& job) { + std::lock_guard lock(job.progress_mutex); + job.progress.finished = std::chrono::steady_clock::now(); +} + +class JobCallbacksGuard { + sd_ctx_t* ctx_; + AsyncGenerationJob& job_; + bool detailing_ = false; + + static void progress_callback(const sd_generation_progress_t* progress, void* data) { + auto& guard = *static_cast(data); + update_job_progress(guard.job_, guard.detailing_ ? "adetailer" : progress->stage, + guard.detailing_ ? progress->stage : progress->detail, + progress->sample_pass, progress->completed, progress->total, progress->unit); + } + +public: + JobCallbacksGuard(sd_ctx_t* ctx, AsyncGenerationJob& job) + : ctx_(ctx), job_(job) { + clear_preview_callback(); + set_preview_callback(job); + sd_set_generation_progress_callback(ctx_, progress_callback, this); + } + + ~JobCallbacksGuard() { + sd_set_generation_progress_callback(ctx_, nullptr, nullptr); + clear_preview_callback(); + } + + void begin_detailing() { + clear_preview_callback(); + detailing_ = true; + update_job_progress(job_, "adetailer"); + } + + JobCallbacksGuard(const JobCallbacksGuard&) = delete; + JobCallbacksGuard& operator=(const JobCallbacksGuard&) = delete; +}; + const char* async_job_kind_name(AsyncJobKind kind) { switch (kind) { case AsyncJobKind::ImgGen: @@ -186,6 +243,7 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) { job.result_fps = 0; job.error_code = "cancelled"; job.error_message = "job cancelled by client"; + finish_job_progress(job); return true; } @@ -200,6 +258,24 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo result["completed"] = job.completed_at == 0 ? json(nullptr) : json(job.completed_at); result["queue_position"] = 0; + { + std::lock_guard lock(job.progress_mutex); + const auto& progress = job.progress; + const auto end = progress.finished == std::chrono::steady_clock::time_point{} + ? std::chrono::steady_clock::now() + : progress.finished; + result["progress"] = { + {"stage", progress.stage}, + {"detail", progress.detail}, + {"pass", progress.sample_pass}, + {"completed", progress.total > 0 ? json(progress.completed) : json(nullptr)}, + {"total", progress.total > 0 ? json(progress.total) : json(nullptr)}, + {"unit", progress.unit.empty() ? json(nullptr) : json(progress.unit)}, + {"stage_elapsed_ms", std::chrono::duration_cast(end - progress.stage_started).count()}, + {"elapsed_ms", job.started_at == 0 ? 0 : std::max(0, (job.completed_at == 0 ? unix_timestamp_now_ms() : job.completed_at) - job.started_at)}, + }; + } + if (job.status == AsyncJobStatus::Queued) { size_t position = 1; for (const auto& queued_id : manager.queue) { @@ -278,17 +354,19 @@ bool execute_img_gen_job(ServerRuntime& runtime, return false; } JobCancellationMonitor cancellation(runtime.sd_ctx, job); - set_preview_callback(job); + JobCallbacksGuard callbacks(runtime.sd_ctx, job); sd_image_generation_result_t* generated = nullptr; generate_image(runtime.sd_ctx, ¶ms, &generated); results.adopt(generated, runtime.ctx_params->lora_model_dir); - clear_preview_callback(); if (job.cancel_requested.load()) { return false; } - if (!results.empty() && !apply_server_adetailer(runtime, job.img_gen, params, results)) { - error_message = "ADetailer failed"; - return false; + if (!results.empty() && job.img_gen.adetailer_enabled && !runtime.default_gen_params->ad_model_path.empty()) { + callbacks.begin_detailing(); + if (!apply_server_adetailer(runtime, job.img_gen, params, results)) { + error_message = "ADetailer failed"; + return false; + } } } @@ -305,6 +383,7 @@ bool execute_img_gen_job(ServerRuntime& runtime, encoded_format = EncodedImageFormat::WEBP; } + update_job_progress(job, "encoding", "image", -1, 0, num_results, "images"); for (int i = 0; i < num_results; ++i) { if (results[i].data == nullptr) { continue; @@ -325,6 +404,7 @@ bool execute_img_gen_job(ServerRuntime& runtime, } output_images.push_back(base64_encode(image_bytes)); output_info.push_back(json::parse(results.generation_info(i))); + update_job_progress(job, "encoding", "image", -1, i + 1, num_results, "images"); } if (output_images.empty()) { @@ -356,7 +436,7 @@ bool execute_vid_gen_job(ServerRuntime& runtime, return false; } JobCancellationMonitor cancellation(runtime.sd_ctx, job); - set_preview_callback(job); + JobCallbacksGuard callbacks(runtime.sd_ctx, job); sd_video_generation_result_t* generated = nullptr; if (generate_video(runtime.sd_ctx, ¶ms, &generated)) { output_fps = generated->fps; @@ -367,7 +447,6 @@ bool execute_vid_gen_job(ServerRuntime& runtime, if (job.vid_gen.gen_params.embed_image_metadata) str_params = results.generation_info(0); } - clear_preview_callback(); } num_results = results.count(); @@ -377,6 +456,7 @@ bool execute_vid_gen_job(ServerRuntime& runtime, return false; } + update_job_progress(job, "encoding", "video"); std::vector video_bytes = create_video_from_sd_images_to_vector(job.vid_gen.output_format, results.data(), num_results, @@ -425,6 +505,7 @@ void async_job_worker(ServerRuntime& runtime) { job = it->second; job->status = AsyncJobStatus::Generating; job->started_at = unix_timestamp_now_ms(); + update_job_progress(*job, "waiting"); } std::vector output_images; @@ -459,6 +540,7 @@ void async_job_worker(ServerRuntime& runtime) { } job->completed_at = unix_timestamp_now_ms(); + finish_job_progress(*job); if (job->cancel_requested.load()) { job->status = AsyncJobStatus::Cancelled; job->error_code = "cancelled"; diff --git a/examples/server/async_jobs.h b/examples/server/async_jobs.h index 6fa3cbd1..a4423056 100644 --- a/examples/server/async_jobs.h +++ b/examples/server/async_jobs.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -29,6 +30,17 @@ enum class AsyncJobStatus { const char* async_job_kind_name(AsyncJobKind kind); const char* async_job_status_name(AsyncJobStatus status); +struct AsyncJobProgress { + std::string stage = "queued"; + std::string detail; + std::string unit; + int sample_pass = 0; + int64_t completed = 0; + int64_t total = 0; + std::chrono::steady_clock::time_point stage_started = std::chrono::steady_clock::now(); + std::chrono::steady_clock::time_point finished{}; +}; + struct AsyncGenerationJob { std::string id; AsyncJobKind kind = AsyncJobKind::ImgGen; @@ -47,6 +59,8 @@ struct AsyncGenerationJob { int result_fps = 0; std::string error_code; std::string error_message; + mutable std::mutex progress_mutex; + AsyncJobProgress progress; mutable std::mutex preview_mutex; std::string preview_b64; int preview_pass = 0; diff --git a/examples/server/frontend b/examples/server/frontend index fa93429c..d3d83992 160000 --- a/examples/server/frontend +++ b/examples/server/frontend @@ -1 +1 @@ -Subproject commit fa93429c47c924054e6a9c14f8d7b8e06f57a0ec +Subproject commit d3d83992ff9bd54c99b11fc36771c13f8637376e diff --git a/include/stable-diffusion.h b/include/stable-diffusion.h index 53c6b705..7453a3f8 100644 --- a/include/stable-diffusion.h +++ b/include/stable-diffusion.h @@ -500,6 +500,15 @@ struct ggml_tensor; typedef void (*sd_log_cb_t)(enum sd_log_level_t level, const char* text, void* data); typedef void (*sd_progress_cb_t)(int step, int steps, float time, void* data); +typedef struct { + const char* stage; + const char* detail; + const char* unit; + int sample_pass; + int64_t completed; + int64_t total; +} sd_generation_progress_t; +typedef void (*sd_generation_progress_cb_t)(const sd_generation_progress_t* progress, void* data); typedef void (*sd_preview_cb_t)(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data); typedef struct { @@ -510,6 +519,11 @@ typedef bool (*sd_graph_eval_callback_t)(struct ggml_tensor* t, bool ask, void* SD_API void sd_set_log_callback(sd_log_cb_t sd_log_cb, void* data); SD_API void sd_set_progress_callback(sd_progress_cb_t cb, void* data); +// Context-local generation events, independent of preview and the legacy progress callback. +// Strings and the event are valid only during the synchronous callback. Callbacks must not throw. +// total == 0 means unknown; sample_pass counts from 1 since callback registration. +// Register/clear only while the context is idle. Passing nullptr clears the callback. +SD_API void sd_set_generation_progress_callback(sd_ctx_t* ctx, sd_generation_progress_cb_t cb, void* data); // In each sampling pass, a positive interval previews every Nth denoiser step, while a // negative interval previews only completed logical step -interval. Zero previews the final // completed step of the first sampling pass (base-resolution or high-noise). diff --git a/src/pipeline/diffusion_engine.cpp b/src/pipeline/diffusion_engine.cpp index 2a6683f0..1ec292c1 100644 --- a/src/pipeline/diffusion_engine.cpp +++ b/src/pipeline/diffusion_engine.cpp @@ -1807,6 +1807,7 @@ void StableDiffusionGGML::lora_stat() { } bool StableDiffusionGGML::apply_loras(const sd_lora_t* loras, uint32_t lora_count) { + generation_progress.report("preparing", "loras"); std::vector all_loras; all_loras.reserve(lora_count); for (uint32_t i = 0; i < lora_count; i++) { @@ -2243,6 +2244,7 @@ void StableDiffusionGGML::report_sample_progress(int step, ? (now - *last_progress_us) / 1000000.f : 0.f; pretty_progress(showstep, (int)total_steps, step_seconds); + generation_progress.report_sample(showstep, static_cast(total_steps)); if (last_progress_us != nullptr) { *last_progress_us = now; } @@ -2424,6 +2426,9 @@ sd::Tensor StableDiffusionGGML::sample(const std::shared_ptr(steps)); + generation_progress.begin_sample(static_cast(steps), + work_diffusion_model == high_noise_diffusion_model ? "high_noise" : high_noise_diffusion_model != nullptr ? "low_noise" + : ""); SamplePreviewContext preview = prepare_sample_preview_context(); sd::Tensor processed_init_latent = denoiser->process_latent_in(init_latent); @@ -2995,6 +3000,7 @@ int StableDiffusionGGML::align_video_frames(int frames) { } sd::Tensor StableDiffusionGGML::encode_to_vae_latents(const sd::Tensor& x) { + generation_progress.report("vae_encode"); auto latents = first_stage_model->encode(n_threads, x, vae_tiling_params, circular_x, circular_y); if (latents.empty()) { return {}; @@ -3015,6 +3021,7 @@ sd::Tensor StableDiffusionGGML::encode_first_stage(const sd::Tensor StableDiffusionGGML::decode_first_stage(const sd::Tensor& x, bool decode_video) { + generation_progress.report("vae_decode"); if (sd_version_is_pid(version) || sd_version_is_minit2i(version) || version == VERSION_IRIS) { return sd::ops::clamp((x + 1.f) * 0.5f, 0.0f, 1.0f); } @@ -3069,6 +3076,7 @@ sd::Tensor StableDiffusionGGML::decode_ltx_audio_latent(const sd::Tensor< if (audio_vae_model == nullptr || audio_latent.empty()) { return {}; } + generation_progress.report("audio_decode"); auto waveform = audio_vae_model->decode(n_threads, audio_latent); return waveform; } diff --git a/src/pipeline/diffusion_engine.h b/src/pipeline/diffusion_engine.h index 97e3acac..990a8b65 100644 --- a/src/pipeline/diffusion_engine.h +++ b/src/pipeline/diffusion_engine.h @@ -21,6 +21,7 @@ #include "model/adapter/lora.hpp" #include "model_builders.h" #include "model_manager.h" +#include "runtime/generation_progress.h" #include "stable-diffusion.h" class RNG; @@ -199,6 +200,7 @@ public: std::unique_ptr conditioning_cache_; std::vector conditioning_loras_; bool executing_ = false; + sd::GenerationProgress generation_progress; std::shared_ptr denoiser; std::vector file_alphas_cumprod; @@ -254,6 +256,9 @@ public: explicit ExecutionScope(StableDiffusionGGML& sd) : operation(sd) { + if (operation.acquired) { + sd.generation_progress.report("preparing"); + } ready = operation.acquired && sd.refresh_model_sources(); } diff --git a/src/pipeline/image.cpp b/src/pipeline/image.cpp index 03a6707d..1f7d5e45 100644 --- a/src/pipeline/image.cpp +++ b/src/pipeline/image.cpp @@ -531,6 +531,7 @@ namespace sd::pipeline { condition_params.ref_image_params = ref_image_params; + sd->generation_progress.report("conditioning", "extensions"); sd->prepare_generation_extensions(request->pm_params, request->pulid_params, condition_params, @@ -538,7 +539,8 @@ namespace sd::pipeline { sd->compute_ip_adapter_tokens(sd_img_gen_params->ip_adapter_image, sd_img_gen_params->ip_adapter_strength); int64_t prepare_start_ms = ggml_time_ms(); condition_params.zero_out_masked = false; - auto cond = sd->get_learned_condition(condition_params); + sd->generation_progress.report("conditioning", "positive_prompt"); + auto cond = sd->get_learned_condition(condition_params); if (cond.empty()) { LOG_ERROR("failed to encode prompt"); return std::nullopt; @@ -584,6 +586,7 @@ namespace sd::pipeline { // Keep source latents in CFG, but omit source images from the text encoder. condition_params.ref_images = nullptr; } + sd->generation_progress.report("conditioning", "negative_prompt"); uncond = sd->get_learned_condition(condition_params); if (uncond.empty()) { LOG_ERROR("failed to encode negative prompt"); @@ -615,6 +618,7 @@ namespace sd::pipeline { if (use_ref_latent_img_cfg) { condition_params.ref_images = &empty_ref_images; } + sd->generation_progress.report("conditioning", "image_condition"); img_uncond = sd->get_learned_condition(condition_params); if (img_uncond.empty()) { LOG_ERROR("failed to encode image guidance prompt"); @@ -830,6 +834,7 @@ namespace sd::pipeline { return {}; } + sd->generation_progress.report("upscaling", "hires"); sd::Tensor upscaled_tensor; if (request.hires.upscaler == SD_HIRES_UPSCALER_MODEL) { upscaled_tensor = upscaler->upscale_tensor(decoded); @@ -930,6 +935,7 @@ namespace sd::pipeline { ImageVaeAxesGuard axes_guard(sd, sd_img_gen_params, request); SamplePlan plan(sd, sd_img_gen_params, request); + sd->generation_progress.report("preparing", "inputs"); auto latents_opt = prepare_image_generation_latents(sd, sd_img_gen_params, &request, @@ -1027,6 +1033,7 @@ namespace sd::pipeline { } LOG_INFO("hires fix: upscaling to %dx%d", request.hires.target_width, request.hires.target_height); + sd->generation_progress.report("upscaling", "hires"); std::unique_ptr hires_upscaler; if (request.hires.upscaler == SD_HIRES_UPSCALER_MODEL) { if (sd->get_cancel_flag() == SD_CANCEL_ALL) { diff --git a/src/pipeline/video.cpp b/src/pipeline/video.cpp index 8c911b14..915dc54b 100644 --- a/src/pipeline/video.cpp +++ b/src/pipeline/video.cpp @@ -1210,7 +1210,8 @@ namespace sd::pipeline { condition_params.ref_image_params = ref_image_params; int64_t prepare_start_ms = ggml_time_ms(); - embeds.cond = sd->get_learned_condition(condition_params); + sd->generation_progress.report("conditioning", "positive_prompt"); + embeds.cond = sd->get_learned_condition(condition_params); if (embeds.cond.empty()) { LOG_ERROR("failed to encode video prompt"); return std::nullopt; @@ -1235,7 +1236,8 @@ namespace sd::pipeline { } if (request.use_uncond) { condition_params.text = request.negative_prompt; - embeds.uncond = sd->get_learned_condition(condition_params); + sd->generation_progress.report("conditioning", "negative_prompt"); + embeds.uncond = sd->get_learned_condition(condition_params); if (embeds.uncond.empty()) { LOG_ERROR("failed to encode negative video prompt"); return std::nullopt; @@ -1901,6 +1903,7 @@ namespace sd::pipeline { sd->reset_generation_extensions(); SamplePlan plan(sd, sd_vid_gen_params, request); + sd->generation_progress.report("preparing", "inputs"); auto latent_inputs_opt = prepare_video_generation_latents(sd, sd_vid_gen_params, &request, ref_image_params); if (!latent_inputs_opt.has_value()) { return false; @@ -2029,6 +2032,7 @@ namespace sd::pipeline { LOG_ERROR("cancelling generation before latent upscale"); return false; } + sd->generation_progress.report("upscaling", "hires"); int64_t upscale_start = ggml_time_ms(); sd::Tensor upscaled_latent; if (sd_version_is_ltxav(sd->version)) { diff --git a/src/runtime/generation_progress.h b/src/runtime/generation_progress.h new file mode 100644 index 00000000..a211931b --- /dev/null +++ b/src/runtime/generation_progress.h @@ -0,0 +1,42 @@ +#ifndef __SD_RUNTIME_GENERATION_PROGRESS_H__ +#define __SD_RUNTIME_GENERATION_PROGRESS_H__ + +#include "stable-diffusion.h" + +namespace sd { + + class GenerationProgress { + sd_generation_progress_cb_t callback_ = nullptr; + void* data_ = nullptr; + int sample_pass_ = 0; + const char* sample_detail_ = ""; + + public: + void set_callback(sd_generation_progress_cb_t callback, void* data) { + callback_ = callback; + data_ = data; + sample_pass_ = 0; + sample_detail_ = ""; + } + + void report(const char* stage, const char* detail = "", int64_t completed = 0, int64_t total = 0, const char* unit = "") const { + if (callback_ != nullptr) { + const sd_generation_progress_t progress{stage, detail, unit, sample_pass_, completed, total}; + callback_(&progress, data_); + } + } + + void begin_sample(int64_t total, const char* detail) { + ++sample_pass_; + sample_detail_ = detail; + report_sample(0, total); + } + + void report_sample(int64_t completed, int64_t total) const { + report("sampling", sample_detail_, completed, total, "steps"); + } + }; + +} // namespace sd + +#endif // __SD_RUNTIME_GENERATION_PROGRESS_H__ diff --git a/src/stable-diffusion.cpp b/src/stable-diffusion.cpp index 436328d7..4a7af2a2 100644 --- a/src/stable-diffusion.cpp +++ b/src/stable-diffusion.cpp @@ -698,6 +698,16 @@ struct sd_ctx_t { StableDiffusionGGML* sd = nullptr; }; +void sd_set_generation_progress_callback(sd_ctx_t* ctx, sd_generation_progress_cb_t cb, void* data) { + if (ctx == nullptr || ctx->sd == nullptr) { + return; + } + std::lock_guard lock(ctx->sd->execution_mutex); + if (!ctx->sd->executing_) { + ctx->sd->generation_progress.set_callback(cb, data); + } +} + sd_ctx_t* new_sd_ctx(const sd_ctx_params_t* sd_ctx_params) { sd_ctx_t* sd_ctx = (sd_ctx_t*)malloc(sizeof(sd_ctx_t)); if (sd_ctx == nullptr) {