mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-10-26 07:04:10 +08:00
492 lines
18 KiB
C++
492 lines
18 KiB
C++
// Extracted from main.cpp during server refactor.
|
|
|
|
#include "async_jobs.h"
|
|
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <iomanip>
|
|
#include <sstream>
|
|
#include <thread>
|
|
|
|
#include "common/log.h"
|
|
#include "common/media_io.h"
|
|
#include "common/resource_owners.hpp"
|
|
|
|
class JobCancellationMonitor {
|
|
std::mutex mutex_;
|
|
std::condition_variable cv_;
|
|
bool done_ = false;
|
|
std::thread thread_;
|
|
|
|
public:
|
|
// The caller holds sd_ctx_mutex until this monitor has joined, keeping the
|
|
// context alive and preventing cancellation from reaching another request.
|
|
JobCancellationMonitor(sd_ctx_t* ctx, const AsyncGenerationJob& job)
|
|
: thread_([this, ctx, &job]() {
|
|
std::unique_lock<std::mutex> lock(mutex_);
|
|
while (!done_) {
|
|
if (job.cancel_requested.load()) {
|
|
// Each generation pass resets the library flag, including ADetailer.
|
|
sd_cancel_generation(ctx, SD_CANCEL_ALL);
|
|
}
|
|
cv_.wait_for(lock, std::chrono::milliseconds(10), [this]() { return done_; });
|
|
}
|
|
}) {}
|
|
|
|
~JobCancellationMonitor() {
|
|
{
|
|
std::lock_guard<std::mutex> lock(mutex_);
|
|
done_ = true;
|
|
}
|
|
cv_.notify_one();
|
|
thread_.join();
|
|
}
|
|
};
|
|
|
|
static void preview_callback(int step, int frame_count, sd_image_t* frames, bool is_noisy, void* data) {
|
|
(void)is_noisy;
|
|
if (frame_count <= 0 || frames == nullptr || frames[0].data == nullptr) {
|
|
return;
|
|
}
|
|
|
|
AsyncGenerationJob* job = static_cast<AsyncGenerationJob*>(data);
|
|
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::PNG,
|
|
frame.data,
|
|
frame.width,
|
|
frame.height,
|
|
frame.channel,
|
|
"",
|
|
80);
|
|
if (image_bytes.empty()) {
|
|
return;
|
|
}
|
|
|
|
std::string b64 = base64_encode(image_bytes);
|
|
std::lock_guard<std::mutex> lock(job->preview_mutex);
|
|
job->preview_b64 = std::move(b64);
|
|
job->preview_step = step < 0 ? -step : step;
|
|
job->preview_pass = info.sample_pass;
|
|
job->preview_total_steps = info.total_steps;
|
|
}
|
|
|
|
static void clear_preview_callback() {
|
|
sd_set_preview_callback(nullptr, PREVIEW_NONE, 1, false, false, nullptr);
|
|
}
|
|
|
|
static void set_preview_callback(AsyncGenerationJob& job) {
|
|
preview_t mode = str_to_preview(job.kind == AsyncJobKind::ImgGen
|
|
? job.img_gen.preview_mode.c_str()
|
|
: job.vid_gen.preview_mode.c_str());
|
|
if (mode == PREVIEW_COUNT || mode == PREVIEW_NONE) {
|
|
return;
|
|
}
|
|
int interval = job.kind == AsyncJobKind::ImgGen
|
|
? job.img_gen.preview_interval
|
|
: job.vid_gen.preview_interval;
|
|
if (interval <= 0) {
|
|
interval = 1;
|
|
}
|
|
sd_set_preview_callback(preview_callback, mode, interval, true, false, &job);
|
|
}
|
|
|
|
const char* async_job_kind_name(AsyncJobKind kind) {
|
|
switch (kind) {
|
|
case AsyncJobKind::ImgGen:
|
|
return "img_gen";
|
|
case AsyncJobKind::VidGen:
|
|
return "vid_gen";
|
|
default:
|
|
return "img_gen";
|
|
}
|
|
}
|
|
|
|
const char* async_job_status_name(AsyncJobStatus status) {
|
|
switch (status) {
|
|
case AsyncJobStatus::Queued:
|
|
return "queued";
|
|
case AsyncJobStatus::Generating:
|
|
return "generating";
|
|
case AsyncJobStatus::Completed:
|
|
return "completed";
|
|
case AsyncJobStatus::Failed:
|
|
return "failed";
|
|
case AsyncJobStatus::Cancelled:
|
|
return "cancelled";
|
|
default:
|
|
return "failed";
|
|
}
|
|
}
|
|
|
|
void purge_expired_jobs(AsyncJobManager& manager) {
|
|
const int64_t now = unix_timestamp_now_ms();
|
|
|
|
for (auto it = manager.expired_jobs.begin(); it != manager.expired_jobs.end();) {
|
|
if (it->second <= now) {
|
|
it = manager.expired_jobs.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
|
|
for (auto it = manager.jobs.begin(); it != manager.jobs.end();) {
|
|
const auto& job = it->second;
|
|
if (job->completed_at == 0) {
|
|
++it;
|
|
continue;
|
|
}
|
|
|
|
int64_t ttl_seconds = job->status == AsyncJobStatus::Completed
|
|
? manager.completed_ttl_seconds
|
|
: manager.failed_ttl_seconds;
|
|
if (now - job->completed_at >= ttl_seconds * 1000) {
|
|
manager.expired_jobs[job->id] = now + std::max<int64_t>(ttl_seconds, 60) * 1000;
|
|
it = manager.jobs.erase(it);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
|
|
size_t count_pending_jobs(const AsyncJobManager& manager) {
|
|
size_t pending = 0;
|
|
for (const auto& entry : manager.jobs) {
|
|
if (entry.second->status == AsyncJobStatus::Queued ||
|
|
entry.second->status == AsyncJobStatus::Generating) {
|
|
++pending;
|
|
}
|
|
}
|
|
return pending;
|
|
}
|
|
|
|
std::string make_async_job_id(AsyncJobManager& manager) {
|
|
std::ostringstream oss;
|
|
oss << "job_" << std::hex << unix_timestamp_now_ms() << "_" << std::setw(8)
|
|
<< std::setfill('0') << manager.next_id++;
|
|
return oss.str();
|
|
}
|
|
|
|
bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
|
|
auto new_end = std::remove(manager.queue.begin(), manager.queue.end(), job.id);
|
|
if (new_end == manager.queue.end()) {
|
|
return false;
|
|
}
|
|
|
|
manager.queue.erase(new_end, manager.queue.end());
|
|
job.cancel_requested.store(true);
|
|
job.status = AsyncJobStatus::Cancelled;
|
|
job.completed_at = unix_timestamp_now_ms();
|
|
job.result_images_b64.clear();
|
|
job.result_generation_info = nullptr;
|
|
job.result_media_b64.clear();
|
|
job.result_media_mime_type.clear();
|
|
job.result_frame_count = 0;
|
|
job.result_fps = 0;
|
|
job.error_code = "cancelled";
|
|
job.error_message = "job cancelled by client";
|
|
return true;
|
|
}
|
|
|
|
json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJob& job) {
|
|
json result;
|
|
result["id"] = job.id;
|
|
result["kind"] = async_job_kind_name(job.kind);
|
|
result["status"] = async_job_status_name(job.status);
|
|
result["cancel_requested"] = job.cancel_requested.load();
|
|
result["created"] = job.created_at;
|
|
result["started"] = job.started_at == 0 ? json(nullptr) : json(job.started_at);
|
|
result["completed"] = job.completed_at == 0 ? json(nullptr) : json(job.completed_at);
|
|
result["queue_position"] = 0;
|
|
|
|
if (job.status == AsyncJobStatus::Queued) {
|
|
size_t position = 1;
|
|
for (const auto& queued_id : manager.queue) {
|
|
if (queued_id == job.id) {
|
|
result["queue_position"] = position;
|
|
break;
|
|
}
|
|
++position;
|
|
}
|
|
}
|
|
|
|
if (job.status == AsyncJobStatus::Completed) {
|
|
if (job.kind == AsyncJobKind::VidGen) {
|
|
result["result"] = {
|
|
{"output_format", job.vid_gen.output_format},
|
|
{"mime_type", job.result_media_mime_type},
|
|
{"fps", job.result_fps},
|
|
{"frame_count", job.result_frame_count},
|
|
{"b64_json", job.result_media_b64},
|
|
{"generation_info", job.result_generation_info},
|
|
};
|
|
} else {
|
|
json images = json::array();
|
|
for (size_t i = 0; i < job.result_images_b64.size(); ++i) {
|
|
images.push_back({{"index", i}, {"b64_json", job.result_images_b64[i]}, {"generation_info", job.result_generation_info.at(i)}});
|
|
}
|
|
result["result"] = {
|
|
{"output_format", job.img_gen.output_format},
|
|
{"images", images},
|
|
};
|
|
}
|
|
result["error"] = nullptr;
|
|
} else if (job.status == AsyncJobStatus::Failed ||
|
|
job.status == AsyncJobStatus::Cancelled) {
|
|
result["result"] = nullptr;
|
|
result["error"] = {
|
|
{"code",
|
|
job.error_code.empty()
|
|
? (job.status == AsyncJobStatus::Cancelled ? "cancelled" : "generation_failed")
|
|
: job.error_code},
|
|
{"message", job.error_message},
|
|
};
|
|
} else {
|
|
result["result"] = nullptr;
|
|
result["error"] = nullptr;
|
|
}
|
|
|
|
if (job.status == AsyncJobStatus::Generating) {
|
|
std::lock_guard<std::mutex> lock(job.preview_mutex);
|
|
if (!job.preview_b64.empty()) {
|
|
result["preview"] = {
|
|
{"pass", job.preview_pass},
|
|
{"step", job.preview_step},
|
|
{"total_steps", job.preview_total_steps},
|
|
{"b64_json", job.preview_b64},
|
|
};
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
bool execute_img_gen_job(ServerRuntime& runtime,
|
|
AsyncGenerationJob& job,
|
|
std::vector<std::string>& output_images,
|
|
json& output_info,
|
|
std::string& error_message) {
|
|
output_info = json::array();
|
|
sd_img_gen_params_t params = job.img_gen.to_sd_img_gen_params_t();
|
|
|
|
SDImageVec results;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
|
if (job.cancel_requested.load()) {
|
|
return false;
|
|
}
|
|
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
|
|
set_preview_callback(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;
|
|
}
|
|
}
|
|
|
|
const int num_results = results.count();
|
|
if (job.cancel_requested.load() || num_results <= 0) {
|
|
error_message = "generate_image returned no results";
|
|
return false;
|
|
}
|
|
|
|
EncodedImageFormat encoded_format = EncodedImageFormat::PNG;
|
|
if (job.img_gen.output_format == "jpeg") {
|
|
encoded_format = EncodedImageFormat::JPEG;
|
|
} else if (job.img_gen.output_format == "webp") {
|
|
encoded_format = EncodedImageFormat::WEBP;
|
|
}
|
|
|
|
for (int i = 0; i < num_results; ++i) {
|
|
if (results[i].data == nullptr) {
|
|
continue;
|
|
}
|
|
|
|
const std::string metadata = job.img_gen.gen_params.embed_image_metadata
|
|
? results.generation_info(i)
|
|
: "";
|
|
auto image_bytes = encode_image_to_vector(encoded_format,
|
|
results[i].data,
|
|
results[i].width,
|
|
results[i].height,
|
|
results[i].channel,
|
|
metadata,
|
|
job.img_gen.output_compression);
|
|
if (image_bytes.empty()) {
|
|
continue;
|
|
}
|
|
output_images.push_back(base64_encode(image_bytes));
|
|
output_info.push_back(json::parse(results.generation_info(i)));
|
|
}
|
|
|
|
if (output_images.empty()) {
|
|
error_message = "generate_image returned empty encoded outputs";
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool execute_vid_gen_job(ServerRuntime& runtime,
|
|
AsyncGenerationJob& job,
|
|
std::string& output_media_b64,
|
|
std::string& output_media_mime_type,
|
|
int& output_frame_count,
|
|
int& output_fps,
|
|
json& output_info,
|
|
std::string& error_message) {
|
|
sd_vid_gen_params_t params = job.vid_gen.to_sd_vid_gen_params_t();
|
|
std::string str_params;
|
|
|
|
SDImageVec results;
|
|
int num_results = 0;
|
|
sd_audio_t* generated_audio = nullptr;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
|
if (job.cancel_requested.load()) {
|
|
return false;
|
|
}
|
|
JobCancellationMonitor cancellation(runtime.sd_ctx, job);
|
|
set_preview_callback(job);
|
|
sd_video_generation_result_t* generated = nullptr;
|
|
if (generate_video(runtime.sd_ctx, ¶ms, &generated)) {
|
|
output_fps = generated->fps;
|
|
generated_audio = generated->audio;
|
|
generated->audio = nullptr;
|
|
results.adopt_video(generated, runtime.ctx_params->lora_model_dir);
|
|
output_info = json::parse(results.generation_info(0));
|
|
if (job.vid_gen.gen_params.embed_image_metadata)
|
|
str_params = results.generation_info(0);
|
|
}
|
|
clear_preview_callback();
|
|
}
|
|
|
|
num_results = results.count();
|
|
if (job.cancel_requested.load() || num_results <= 0) {
|
|
free_sd_audio(generated_audio);
|
|
error_message = "generate_video returned no results";
|
|
return false;
|
|
}
|
|
|
|
std::vector<uint8_t> video_bytes = create_video_from_sd_images_to_vector(job.vid_gen.output_format,
|
|
results.data(),
|
|
num_results,
|
|
output_fps,
|
|
job.vid_gen.output_compression,
|
|
generated_audio,
|
|
str_params);
|
|
free_sd_audio(generated_audio);
|
|
if (video_bytes.empty()) {
|
|
error_message = "failed to encode generated video container";
|
|
return false;
|
|
}
|
|
|
|
output_media_b64 = base64_encode(video_bytes);
|
|
output_media_mime_type = video_mime_type(job.vid_gen.output_format);
|
|
output_frame_count = num_results;
|
|
return true;
|
|
}
|
|
|
|
void async_job_worker(ServerRuntime& runtime) {
|
|
AsyncJobManager& manager = *runtime.async_job_manager;
|
|
|
|
while (true) {
|
|
std::shared_ptr<AsyncGenerationJob> job;
|
|
{
|
|
std::unique_lock<std::mutex> lock(manager.mutex);
|
|
manager.cv.wait(lock, [&]() { return manager.stop || !manager.queue.empty(); });
|
|
|
|
if (manager.stop && manager.queue.empty()) {
|
|
break;
|
|
}
|
|
|
|
purge_expired_jobs(manager);
|
|
if (manager.queue.empty()) {
|
|
continue;
|
|
}
|
|
|
|
const std::string job_id = manager.queue.front();
|
|
manager.queue.pop_front();
|
|
|
|
auto it = manager.jobs.find(job_id);
|
|
if (it == manager.jobs.end()) {
|
|
continue;
|
|
}
|
|
|
|
job = it->second;
|
|
job->status = AsyncJobStatus::Generating;
|
|
job->started_at = unix_timestamp_now_ms();
|
|
}
|
|
|
|
std::vector<std::string> output_images;
|
|
json output_info;
|
|
std::string output_media_b64;
|
|
std::string output_media_mime_type;
|
|
int output_frame_count = 0;
|
|
int output_fps = 0;
|
|
std::string error_message;
|
|
bool ok = false;
|
|
|
|
if (job->kind == AsyncJobKind::ImgGen) {
|
|
ok = execute_img_gen_job(runtime, *job, output_images, output_info, error_message);
|
|
} else if (job->kind == AsyncJobKind::VidGen) {
|
|
ok = execute_vid_gen_job(runtime,
|
|
*job,
|
|
output_media_b64,
|
|
output_media_mime_type,
|
|
output_frame_count,
|
|
output_fps,
|
|
output_info,
|
|
error_message);
|
|
} else {
|
|
error_message = "unsupported job kind";
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lock(manager.mutex);
|
|
auto it = manager.jobs.find(job->id);
|
|
if (it == manager.jobs.end()) {
|
|
continue;
|
|
}
|
|
|
|
job->completed_at = unix_timestamp_now_ms();
|
|
if (job->cancel_requested.load()) {
|
|
job->status = AsyncJobStatus::Cancelled;
|
|
job->error_code = "cancelled";
|
|
job->error_message = "job cancelled by client";
|
|
} else if (ok) {
|
|
job->status = AsyncJobStatus::Completed;
|
|
job->result_images_b64 = std::move(output_images);
|
|
job->result_generation_info = std::move(output_info);
|
|
job->result_media_b64 = std::move(output_media_b64);
|
|
job->result_media_mime_type = std::move(output_media_mime_type);
|
|
job->result_frame_count = output_frame_count;
|
|
job->result_fps = output_fps;
|
|
job->error_code.clear();
|
|
job->error_message.clear();
|
|
} else {
|
|
job->status = AsyncJobStatus::Failed;
|
|
job->error_code = "generation_failed";
|
|
job->error_message = error_message.empty() ? "unknown generation error" : error_message;
|
|
job->result_images_b64.clear();
|
|
job->result_generation_info = nullptr;
|
|
job->result_media_b64.clear();
|
|
job->result_media_mime_type.clear();
|
|
job->result_frame_count = 0;
|
|
job->result_fps = 0;
|
|
}
|
|
|
|
purge_expired_jobs(manager);
|
|
}
|
|
}
|
|
}
|