mirror of
https://github.com/leejet/stable-diffusion.cpp.git
synced 2026-11-14 04:47:47 +00:00
Merge branch 'master' into ltx2.3
This commit is contained in:
commit
274ecd5d41
84 files changed
+7911
-3360
No files matched your search
+105
-72
@@ -4,26 +4,29 @@
|
||||
usage: ./bin/sd-cli [options]
|
||||
|
||||
CLI Options:
|
||||
-o, --output <string> path to write result image to. you can use printf-style %d format specifiers for image sequences (default:
|
||||
./output.png) (eg. output_%03d.png). For video generation, single-file outputs support .avi, .webm, and animated .webp
|
||||
--preview-path <string> path to write preview image to (default: ./preview.png). Multi-frame previews support .avi, .webm, and animated .webp
|
||||
--preview-interval <int> interval in denoising steps between consecutive updates of the image preview file (default is 1, meaning updating at
|
||||
every step)
|
||||
--output-begin-idx <int> starting index for output image sequence, must be non-negative (default 0 if specified %d in output path, 1 otherwise)
|
||||
--image <string> path to the image to inspect (for metadata mode)
|
||||
--metadata-format <string> metadata output format, one of [text, json] (default: text)
|
||||
--canny apply canny preprocessor (edge detection)
|
||||
--convert-name convert tensor name (for convert mode)
|
||||
-v, --verbose print extra info
|
||||
--color colors the logging tags according to level
|
||||
--taesd-preview-only prevents usage of taesd for decoding the final image. (for use with --preview tae)
|
||||
--preview-noisy enables previewing noisy inputs of the models rather than the denoised outputs
|
||||
--metadata-raw include raw hex previews for unparsed metadata payloads
|
||||
--metadata-brief truncate long metadata text values in text output
|
||||
--metadata-all include structural/container entries such as IHDR, IDAT, and non-metadata JPEG segments
|
||||
-M, --mode run mode, one of [img_gen, vid_gen, upscale, convert, metadata], default: img_gen
|
||||
--preview preview method. must be one of the following [none, proj, tae, vae] (default is none)
|
||||
-h, --help show this help message and exit
|
||||
-o, --output <string> path to write result image to. you can use printf-style %d format specifiers for image
|
||||
sequences (default: ./output.png) (eg. output_%03d.png). Single-file video outputs
|
||||
support .avi, .webm, and animated .webp
|
||||
--image <string> path to the image to inspect (for metadata mode)
|
||||
--metadata-format <string> metadata output format, one of [text, json] (default: text)
|
||||
--preview-path <string> path to write preview image to (default: ./preview.png). Multi-frame previews support
|
||||
.avi, .webm, and animated .webp
|
||||
--preview-interval <int> interval in denoising steps between consecutive updates of the image preview file
|
||||
(default is 1, meaning updating at every step)
|
||||
--output-begin-idx <int> starting index for output image sequence, must be non-negative (default 0 if specified
|
||||
%d in output path, 1 otherwise)
|
||||
--canny apply canny preprocessor (edge detection)
|
||||
--convert-name convert tensor name (for convert mode)
|
||||
-v, --verbose print extra info
|
||||
--color colors the logging tags according to level
|
||||
--taesd-preview-only prevents usage of taesd for decoding the final image. (for use with --preview tae)
|
||||
--preview-noisy enables previewing noisy inputs of the models rather than the denoised outputs
|
||||
--metadata-raw include raw hex previews for unparsed metadata payloads
|
||||
--metadata-brief truncate long metadata text values in text output
|
||||
--metadata-all include structural/container entries such as IHDR, IDAT, and non-metadata JPEG segments
|
||||
-M, --mode run mode, one of [img_gen, vid_gen, upscale, convert, metadata], default: img_gen
|
||||
--preview preview method. must be one of the following [none, proj, tae, vae] (default is none)
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Context Options:
|
||||
-m, --model <string> path to full model
|
||||
@@ -31,7 +34,8 @@ Context Options:
|
||||
--clip_g <string> path to the clip-g text encoder
|
||||
--clip_vision <string> path to the clip-vision encoder
|
||||
--t5xxl <string> path to the t5xxl text encoder
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image,
|
||||
mistral-small3.2 for flux2, ...)
|
||||
--llm_vision <string> path to the llm vit
|
||||
--qwen2vl <string> alias of --llm. Deprecated.
|
||||
--qwen2vl_vision <string> alias of --llm_vision. Deprecated.
|
||||
@@ -43,16 +47,16 @@ Context Options:
|
||||
--control-net <string> path to control net model
|
||||
--embd-dir <string> embeddings directory
|
||||
--lora-model-dir <string> lora model directory
|
||||
--hires-upscalers-dir <string> highres fix upscaler model directory
|
||||
--tensor-type-rules <string> weight type per tensor pattern (example: "^vae\.=f16,model\.=q8_0")
|
||||
--photo-maker <string> path to PHOTOMAKER model
|
||||
--upscale-model <string> path to esrgan model.
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0, then threads will be set to the number of
|
||||
CPU physical cores
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0,
|
||||
then threads will be set to the number of CPU physical cores
|
||||
--chroma-t5-mask-pad <int> t5 mask pad size of chroma
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--force-sdxl-vae-conv-scale force use of conv scale on sdxl vae
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM when needed
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM
|
||||
when needed
|
||||
--mmap whether to memory-map model
|
||||
--control-net-cpu keep controlnet in cpu (for low vram)
|
||||
--clip-on-cpu keep clip in cpu (for low vram)
|
||||
@@ -67,20 +71,19 @@ Context Options:
|
||||
--chroma-disable-dit-mask disable dit mask for chroma
|
||||
--qwen-image-zero-cond-t enable zero_cond_t for qwen image
|
||||
--chroma-enable-t5-mask enable t5 mask for chroma
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the
|
||||
type of the weight file
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K,
|
||||
q4_K). If not specified, the default is the type of the weight file
|
||||
--rng RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)
|
||||
--sampler-rng sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. In auto mode, if the model weights
|
||||
contain any quantized parameters, the at_runtime mode will be used; otherwise,
|
||||
immediately will be used.The immediately mode may have precision and
|
||||
compatibility issues with quantized parameters, but it usually offers faster inference
|
||||
speed and, in some cases, lower memory usage. The at_runtime mode, on the
|
||||
other hand, is exactly the opposite.
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1
|
||||
(overrides --vae-tile-size)
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow,
|
||||
flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is
|
||||
auto. In auto mode, if the model weights contain any quantized parameters,
|
||||
the at_runtime mode will be used; otherwise, immediately will be used.The
|
||||
immediately mode may have precision and compatibility issues with quantized
|
||||
parameters, but it usually offers faster inference speed and, in some cases,
|
||||
lower memory usage. The at_runtime mode, on the other hand, is exactly the
|
||||
opposite.
|
||||
|
||||
Generation Options:
|
||||
-p, --prompt <string> the prompt to render
|
||||
@@ -89,69 +92,99 @@ Generation Options:
|
||||
--end-img <string> path to the end image, required by flf2v
|
||||
--mask <string> path to the mask image
|
||||
--control-image <string> path to control image, control net
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames inside should be stored as images in
|
||||
lexicographical (character) order. For example, if the control video path is
|
||||
`frames`, the directory contain images such as 00.png, 01.png, ... etc.
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames
|
||||
inside should be stored as images in lexicographical (character) order. For
|
||||
example, if the control video path is `frames`, the directory contain images
|
||||
such as 00.png, 01.png, ... etc.
|
||||
--pm-id-images-dir <string> path to PHOTOMAKER input id images dir
|
||||
--pm-id-embed-path <string> path to PHOTOMAKER v2 id embed
|
||||
--hires-upscaler <string> highres fix upscaler, Lanczos, Nearest, Latent, Latent (nearest), Latent
|
||||
(nearest-exact), Latent (antialiased), Latent (bicubic), Latent (bicubic
|
||||
antialiased), or a model name under --hires-upscalers-dir (default: Latent)
|
||||
-H, --height <int> image height, in pixel space (default: 512)
|
||||
-W, --width <int> image width, in pixel space (default: 512)
|
||||
--steps <int> number of sample steps (default: 20)
|
||||
--high-noise-steps <int> (high noise) number of sample steps (default: -1 = auto)
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). <= 0 represents unspecified,
|
||||
will be 1 for SD1.x, 2 for SD2.x
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer
|
||||
(default: -1). <= 0 represents unspecified, will be 1 for SD1.x, 2 for SD2.x
|
||||
-b, --batch-count <int> batch count
|
||||
--video-frames <int> video frames (default: 1)
|
||||
--fps <int> fps (default: 24)
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for
|
||||
NitroSD-Vibrant
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for
|
||||
NitroSD-Realism around 250 and 500 for NitroSD-Vibrant
|
||||
--upscale-repeats <int> Run the ESRGAN upscaler this many times (default: 1)
|
||||
--upscale-tile-size <int> tile size for ESRGAN upscaling (default: 128)
|
||||
--hires-width <int> highres fix target width, 0 to use --hires-scale (default: 0)
|
||||
--hires-height <int> highres fix target height, 0 to use --hires-scale (default: 0)
|
||||
--hires-steps <int> highres fix second pass sample steps, 0 to reuse --steps (default: 0)
|
||||
--hires-upscale-tile-size <int> highres fix upscaler tile size, reserved for model-backed upscalers (default:
|
||||
128)
|
||||
--cfg-scale <float> unconditional guidance scale: (default: 7.0)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same
|
||||
as --cfg-scale)
|
||||
--guidance <float> distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5
|
||||
medium
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means
|
||||
disabled, a value of 2.5 is nice for sd3.5 medium
|
||||
--skip-layer-start <float> SLG enabling point (default: 0.01)
|
||||
--skip-layer-end <float> SLG disabling point (default: 0.2)
|
||||
--eta <float> noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)
|
||||
--eta <float> noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and
|
||||
res_2s; 1 for euler_a, er_sde and dpm++2s_a)
|
||||
--flow-shift <float> shift value for Flow models like SD3.x or WAN (default: auto)
|
||||
--high-noise-cfg-scale <float> (high noise) unconditional guidance scale: (default: 7.0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models
|
||||
(default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input
|
||||
(default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default:
|
||||
0)
|
||||
--high-noise-skip-layer-start <float> (high noise) SLG enabling point (default: 0.01)
|
||||
--high-noise-skip-layer-end <float> (high noise) SLG disabling point (default: 0.2)
|
||||
--high-noise-eta <float> (high noise) noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)
|
||||
--high-noise-eta <float> (high noise) noise multiplier (default: 0 for ddim_trailing, tcd,
|
||||
res_multistep and res_2s; 1 for euler_a, er_sde and dpm++2s_a)
|
||||
--strength <float> strength for noising/unnoising (default: 0.75)
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full
|
||||
destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if
|
||||
`--high-noise-steps` is set to -1
|
||||
--vace-strength <float> wan vace strength
|
||||
--increase-ref-index automatically increase the indices of references images based on the order they are listed (starting with 1).
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--hires-scale <float> highres fix scale when target size is not set (default: 2.0)
|
||||
--hires-denoising-strength <float> highres fix second pass denoising strength (default: 0.7)
|
||||
--increase-ref-index automatically increase the indices of references images based on the order
|
||||
they are listed (starting with 1).
|
||||
--disable-auto-resize-ref-image disable auto resize of ref images
|
||||
--disable-image-metadata do not embed generation metadata on image files
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--hires enable highres fix
|
||||
-s, --seed RNG seed (default: 42, use random seed for < 0)
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing,
|
||||
tcd, res_multistep, res_2s] (default: euler for Flux/SD3/Wan, euler_a
|
||||
otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm,
|
||||
ddim_trailing, tcd, res_multistep, res_2s] default: euler for Flux/SD3/Wan,
|
||||
euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple,
|
||||
kl_optimal, lcm, bong_tangent], default: discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0").
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m,
|
||||
dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s,
|
||||
er_sde] (default: euler for Flux/SD3/Wan, euler_a otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a,
|
||||
dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep,
|
||||
res_2s, er_sde] default: euler for Flux/SD3/Wan, euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits,
|
||||
smoothstep, sgm_uniform, simple, kl_optimal, lcm, bong_tangent], default:
|
||||
discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g.,
|
||||
"14.61,7.8,3.5,0.0").
|
||||
--skip-layers layers to skip for SLG steps (default: [7,8,9])
|
||||
--high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9])
|
||||
-r, --ref-image reference image for Flux Kontext models (can be used multiple times)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level),
|
||||
'spectrum' (UNET/DiT Chebyshev+Taylor forecasting)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET),
|
||||
'dbcache'/'taylorseer'/'cache-dit' (DiT block-level), 'spectrum' (UNET/DiT
|
||||
Chebyshev+Taylor forecasting)
|
||||
--cache-option named cache params (key=value format, comma-separated). easycache/ucache:
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup=;
|
||||
spectrum: w=,m=,lam=,window=,flex=,warmup=,stop=. Examples:
|
||||
"threshold=0.25" or "threshold=1.5,reset=0" or "w=0.4,window=2"
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit:
|
||||
Fn=,Bn=,threshold=,warmup=; spectrum: w=,m=,lam=,window=,flex=,warmup=,stop=.
|
||||
Examples: "threshold=0.25" or "threshold=1.5,reset=0"
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g.,
|
||||
"1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
--scm-policy SCM policy: 'dynamic' (default) or 'static'
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size
|
||||
if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size)
|
||||
```
|
||||
|
||||
Metadata mode inspects PNG/JPEG container metadata without loading any model:
|
||||
|
||||
+11
-2
@@ -279,7 +279,9 @@ void parse_args(int argc, const char** argv, SDCliParams& cli_params, SDContextP
|
||||
bool valid = cli_params.resolve_and_validate();
|
||||
if (valid && cli_params.mode != METADATA) {
|
||||
valid = ctx_params.resolve_and_validate(cli_params.mode) &&
|
||||
gen_params.resolve_and_validate(cli_params.mode, ctx_params.lora_model_dir);
|
||||
gen_params.resolve_and_validate(cli_params.mode,
|
||||
ctx_params.lora_model_dir,
|
||||
ctx_params.hires_upscalers_dir);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
@@ -501,7 +503,7 @@ int main(int argc, const char* argv[]) {
|
||||
|
||||
cli_params.verbose = true;
|
||||
sd_set_log_callback(sd_log_cb, (void*)&cli_params);
|
||||
LLM::GemmaTokenizer tokenizer;
|
||||
GemmaTokenizer tokenizer;
|
||||
auto tokens = tokenizer.tokenize("<html> 一只可爱的小猫");
|
||||
for (auto token : tokens) {
|
||||
LOG_INFO("%d", token);
|
||||
@@ -698,6 +700,13 @@ int main(int argc, const char* argv[]) {
|
||||
vae_decode_only = false;
|
||||
}
|
||||
|
||||
if (gen_params.hires_enabled &&
|
||||
(gen_params.resolved_hires_upscaler == SD_HIRES_UPSCALER_MODEL ||
|
||||
gen_params.resolved_hires_upscaler == SD_HIRES_UPSCALER_LANCZOS ||
|
||||
gen_params.resolved_hires_upscaler == SD_HIRES_UPSCALER_NEAREST)) {
|
||||
vae_decode_only = false;
|
||||
}
|
||||
|
||||
sd_ctx_params_t sd_ctx_params = ctx_params.to_sd_ctx_params_t(vae_decode_only, true, cli_params.taesd_preview);
|
||||
|
||||
SDImageVec results;
|
||||
|
||||
+226
-34
@@ -107,47 +107,60 @@ static bool is_absolute_path(const std::string& p) {
|
||||
|
||||
std::string ArgOptions::wrap_text(const std::string& text, size_t width, size_t indent) {
|
||||
std::ostringstream oss;
|
||||
size_t line_len = 0;
|
||||
size_t pos = 0;
|
||||
size_t line_len = 0;
|
||||
|
||||
while (pos < text.size()) {
|
||||
// Preserve manual newlines
|
||||
if (text[pos] == '\n') {
|
||||
oss << '\n'
|
||||
<< std::string(indent, ' ');
|
||||
line_len = indent;
|
||||
line_len = 0;
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add the character
|
||||
oss << text[pos];
|
||||
++line_len;
|
||||
++pos;
|
||||
if (std::isspace(static_cast<unsigned char>(text[pos]))) {
|
||||
++pos;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the current line exceeds width, try to break at the last space
|
||||
if (line_len >= width) {
|
||||
std::string current = oss.str();
|
||||
size_t back = current.size();
|
||||
size_t word_start = pos;
|
||||
while (pos < text.size() &&
|
||||
text[pos] != '\n' &&
|
||||
!std::isspace(static_cast<unsigned char>(text[pos]))) {
|
||||
++pos;
|
||||
}
|
||||
|
||||
// Find the last space (for a clean break)
|
||||
while (back > 0 && current[back - 1] != ' ' && current[back - 1] != '\n')
|
||||
--back;
|
||||
std::string word = text.substr(word_start, pos - word_start);
|
||||
while (!word.empty()) {
|
||||
size_t separator_len = line_len == 0 ? 0 : 1;
|
||||
if (line_len + separator_len + word.size() <= width) {
|
||||
if (separator_len > 0) {
|
||||
oss << ' ';
|
||||
++line_len;
|
||||
}
|
||||
oss << word;
|
||||
line_len += word.size();
|
||||
word.clear();
|
||||
continue;
|
||||
}
|
||||
|
||||
// If found a space to break on
|
||||
if (back > 0 && current[back - 1] != '\n') {
|
||||
std::string before = current.substr(0, back - 1);
|
||||
std::string after = current.substr(back);
|
||||
oss.str("");
|
||||
oss.clear();
|
||||
oss << before << "\n"
|
||||
<< std::string(indent, ' ') << after;
|
||||
} else {
|
||||
// If no space found, just break at width
|
||||
oss << "\n"
|
||||
if (line_len > 0) {
|
||||
oss << '\n'
|
||||
<< std::string(indent, ' ');
|
||||
line_len = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
size_t chunk_len = std::min(width, word.size());
|
||||
oss << word.substr(0, chunk_len);
|
||||
line_len = chunk_len;
|
||||
word.erase(0, chunk_len);
|
||||
if (!word.empty()) {
|
||||
oss << '\n'
|
||||
<< std::string(indent, ' ');
|
||||
line_len = 0;
|
||||
}
|
||||
line_len = indent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +364,10 @@ ArgOptions SDContextParams::get_options() {
|
||||
"--lora-model-dir",
|
||||
"lora model directory",
|
||||
&lora_model_dir},
|
||||
|
||||
{"",
|
||||
"--hires-upscalers-dir",
|
||||
"highres fix upscaler model directory",
|
||||
&hires_upscalers_dir},
|
||||
{"",
|
||||
"--tensor-type-rules",
|
||||
"weight type per tensor pattern (example: \"^vae\\.=f16,model\\.=q8_0\")",
|
||||
@@ -649,6 +665,7 @@ std::string SDContextParams::to_string() const {
|
||||
<< " wtype: " << sd_type_name(wtype) << ",\n"
|
||||
<< " tensor_type_rules: \"" << tensor_type_rules << "\",\n"
|
||||
<< " lora_model_dir: \"" << lora_model_dir << "\",\n"
|
||||
<< " hires_upscalers_dir: \"" << hires_upscalers_dir << "\",\n"
|
||||
<< " photo_maker_path: \"" << photo_maker_path << "\",\n"
|
||||
<< " rng_type: " << sd_rng_type_name(rng_type) << ",\n"
|
||||
<< " sampler_rng_type: " << sd_rng_type_name(sampler_rng_type) << ",\n"
|
||||
@@ -777,6 +794,12 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"--pm-id-embed-path",
|
||||
"path to PHOTOMAKER v2 id embed",
|
||||
&pm_id_embed_path},
|
||||
{"",
|
||||
"--hires-upscaler",
|
||||
"highres fix upscaler, Lanczos, Nearest, Latent, Latent (nearest), Latent (nearest-exact), "
|
||||
"Latent (antialiased), Latent (bicubic), Latent (bicubic antialiased), or a model name "
|
||||
"under --hires-upscalers-dir (default: Latent)",
|
||||
&hires_upscaler},
|
||||
};
|
||||
|
||||
options.int_options = {
|
||||
@@ -826,6 +849,22 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"--upscale-tile-size",
|
||||
"tile size for ESRGAN upscaling (default: 128)",
|
||||
&upscale_tile_size},
|
||||
{"",
|
||||
"--hires-width",
|
||||
"highres fix target width, 0 to use --hires-scale (default: 0)",
|
||||
&hires_width},
|
||||
{"",
|
||||
"--hires-height",
|
||||
"highres fix target height, 0 to use --hires-scale (default: 0)",
|
||||
&hires_height},
|
||||
{"",
|
||||
"--hires-steps",
|
||||
"highres fix second pass sample steps, 0 to reuse --steps (default: 0)",
|
||||
&hires_steps},
|
||||
{"",
|
||||
"--hires-upscale-tile-size",
|
||||
"highres fix upscaler tile size, reserved for model-backed upscalers (default: 128)",
|
||||
&hires_upscale_tile_size},
|
||||
};
|
||||
|
||||
options.float_options = {
|
||||
@@ -855,7 +894,7 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
&sample_params.guidance.slg.layer_end},
|
||||
{"",
|
||||
"--eta",
|
||||
"noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)",
|
||||
"noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a, er_sde and dpm++2s_a)",
|
||||
&sample_params.eta},
|
||||
{"",
|
||||
"--flow-shift",
|
||||
@@ -887,7 +926,7 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
&high_noise_sample_params.guidance.slg.layer_end},
|
||||
{"",
|
||||
"--high-noise-eta",
|
||||
"(high noise) noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)",
|
||||
"(high noise) noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a, er_sde and dpm++2s_a)",
|
||||
&high_noise_sample_params.eta},
|
||||
{"",
|
||||
"--strength",
|
||||
@@ -913,6 +952,14 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"--vae-tile-overlap",
|
||||
"tile overlap for vae tiling, in fraction of tile size (default: 0.5)",
|
||||
&vae_tiling_params.target_overlap},
|
||||
{"",
|
||||
"--hires-scale",
|
||||
"highres fix scale when target size is not set (default: 2.0)",
|
||||
&hires_scale},
|
||||
{"",
|
||||
"--hires-denoising-strength",
|
||||
"highres fix second pass denoising strength (default: 0.7)",
|
||||
&hires_denoising_strength},
|
||||
};
|
||||
|
||||
options.bool_options = {
|
||||
@@ -936,6 +983,11 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
"process vae in tiles to reduce memory usage",
|
||||
true,
|
||||
&vae_tiling_params.enabled},
|
||||
{"",
|
||||
"--hires",
|
||||
"enable highres fix",
|
||||
true,
|
||||
&hires_enabled},
|
||||
};
|
||||
|
||||
auto on_seed_arg = [&](int argc, const char** argv, int index) {
|
||||
@@ -1185,12 +1237,12 @@ ArgOptions SDGenerationParams::get_options() {
|
||||
on_seed_arg},
|
||||
{"",
|
||||
"--sampling-method",
|
||||
"sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s] "
|
||||
"sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s, er_sde] "
|
||||
"(default: euler for Flux/SD3/Wan, euler_a otherwise)",
|
||||
on_sample_method_arg},
|
||||
{"",
|
||||
"--high-noise-sampling-method",
|
||||
"(high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s]"
|
||||
"(high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s, er_sde]"
|
||||
" default: euler for Flux/SD3/Wan, euler_a otherwise",
|
||||
on_high_noise_sample_method_arg},
|
||||
{"",
|
||||
@@ -1424,6 +1476,37 @@ static bool parse_lora_json_field(const json& parent,
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool resolve_model_file_from_dir(const std::string& model_name,
|
||||
const std::string& model_dir,
|
||||
const std::vector<std::string>& valid_ext,
|
||||
const char* label,
|
||||
std::string& resolved_path) {
|
||||
if (model_dir.empty()) {
|
||||
LOG_ERROR("%s directory is empty", label);
|
||||
return false;
|
||||
}
|
||||
if (model_name.empty() ||
|
||||
model_name.find('/') != std::string::npos ||
|
||||
model_name.find('\\') != std::string::npos ||
|
||||
fs::path(model_name).has_root_path() ||
|
||||
fs::path(model_name).has_extension()) {
|
||||
LOG_ERROR("%s must be a model name without path or extension: %s", label, model_name.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
fs::path model_dir_path = model_dir;
|
||||
for (const auto& ext : valid_ext) {
|
||||
fs::path try_path = model_dir_path / (model_name + ext);
|
||||
if (fs::exists(try_path) && fs::is_regular_file(try_path)) {
|
||||
resolved_path = try_path.lexically_normal().string();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
LOG_ERROR("can not find %s %s in %s", label, model_name.c_str(), model_dir_path.lexically_normal().string().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool SDGenerationParams::from_json_str(
|
||||
const std::string& json_str,
|
||||
const std::function<std::string(const std::string&)>& lora_path_resolver) {
|
||||
@@ -1487,6 +1570,34 @@ bool SDGenerationParams::from_json_str(
|
||||
load_if_exists("increase_ref_index", increase_ref_index);
|
||||
load_if_exists("embed_image_metadata", embed_image_metadata);
|
||||
|
||||
if (j.contains("hires") && j["hires"].is_object()) {
|
||||
const json& hires_json = j["hires"];
|
||||
if (hires_json.contains("enabled") && hires_json["enabled"].is_boolean()) {
|
||||
hires_enabled = hires_json["enabled"];
|
||||
}
|
||||
if (hires_json.contains("upscaler") && hires_json["upscaler"].is_string()) {
|
||||
hires_upscaler = hires_json["upscaler"];
|
||||
}
|
||||
if (hires_json.contains("scale") && hires_json["scale"].is_number()) {
|
||||
hires_scale = hires_json["scale"];
|
||||
}
|
||||
if (hires_json.contains("target_width") && hires_json["target_width"].is_number_integer()) {
|
||||
hires_width = hires_json["target_width"];
|
||||
}
|
||||
if (hires_json.contains("target_height") && hires_json["target_height"].is_number_integer()) {
|
||||
hires_height = hires_json["target_height"];
|
||||
}
|
||||
if (hires_json.contains("steps") && hires_json["steps"].is_number_integer()) {
|
||||
hires_steps = hires_json["steps"];
|
||||
}
|
||||
if (hires_json.contains("denoising_strength") && hires_json["denoising_strength"].is_number()) {
|
||||
hires_denoising_strength = hires_json["denoising_strength"];
|
||||
}
|
||||
if (hires_json.contains("upscale_tile_size") && hires_json["upscale_tile_size"].is_number_integer()) {
|
||||
hires_upscale_tile_size = hires_json["upscale_tile_size"];
|
||||
}
|
||||
}
|
||||
|
||||
auto parse_sample_params_json = [&](const json& sample_json,
|
||||
sd_sample_params_t& target_params,
|
||||
std::vector<int>& target_skip_layers,
|
||||
@@ -1589,10 +1700,18 @@ bool SDGenerationParams::from_json_str(
|
||||
LOG_ERROR("invalid init_image");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "end_image", 3, width, height, end_image)) {
|
||||
LOG_ERROR("invalid end_image");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_array_json_field(j, "ref_images", 3, width, height, ref_images)) {
|
||||
LOG_ERROR("invalid ref_images");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_array_json_field(j, "control_frames", 3, width, height, control_frames)) {
|
||||
LOG_ERROR("invalid control_frames");
|
||||
return false;
|
||||
}
|
||||
if (!parse_image_json_field(j, "mask_image", 1, width, height, mask_image)) {
|
||||
LOG_ERROR("invalid mask_image");
|
||||
return false;
|
||||
@@ -1792,7 +1911,7 @@ bool SDGenerationParams::initialize_cache_params() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDGenerationParams::resolve(const std::string& lora_model_dir, bool strict) {
|
||||
bool SDGenerationParams::resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict) {
|
||||
if (high_noise_sample_params.sample_steps <= 0) {
|
||||
high_noise_sample_params.sample_steps = -1;
|
||||
}
|
||||
@@ -1811,6 +1930,27 @@ bool SDGenerationParams::resolve(const std::string& lora_model_dir, bool strict)
|
||||
sample_params.sample_steps = std::clamp(sample_params.sample_steps, 1, 100);
|
||||
}
|
||||
|
||||
hires_upscaler_model_path.clear();
|
||||
if (hires_enabled) {
|
||||
if (hires_upscaler.empty()) {
|
||||
hires_upscaler = "Latent";
|
||||
}
|
||||
resolved_hires_upscaler = str_to_sd_hires_upscaler(hires_upscaler.c_str());
|
||||
if (resolved_hires_upscaler == SD_HIRES_UPSCALER_NONE) {
|
||||
hires_enabled = false;
|
||||
} else if (resolved_hires_upscaler == SD_HIRES_UPSCALER_COUNT) {
|
||||
static const std::vector<std::string> valid_ext = {".gguf", ".safetensors", ".pt", ".pth"};
|
||||
if (!resolve_model_file_from_dir(hires_upscaler,
|
||||
hires_upscalers_dir,
|
||||
valid_ext,
|
||||
"hires upscaler",
|
||||
hires_upscaler_model_path)) {
|
||||
return false;
|
||||
}
|
||||
resolved_hires_upscaler = SD_HIRES_UPSCALER_MODEL;
|
||||
}
|
||||
}
|
||||
|
||||
prompt_with_lora = prompt;
|
||||
if (!lora_model_dir.empty()) {
|
||||
extract_and_remove_lora(lora_model_dir);
|
||||
@@ -1875,6 +2015,29 @@ bool SDGenerationParams::validate(SDMode mode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hires_enabled) {
|
||||
if (hires_width < 0 || hires_height < 0) {
|
||||
LOG_ERROR("error: hires target width and height must be >= 0");
|
||||
return false;
|
||||
}
|
||||
if (hires_scale <= 0.f && hires_width <= 0 && hires_height <= 0) {
|
||||
LOG_ERROR("error: hires scale must be positive when target size is not set");
|
||||
return false;
|
||||
}
|
||||
if (hires_steps < 0) {
|
||||
LOG_ERROR("error: hires steps must be >= 0");
|
||||
return false;
|
||||
}
|
||||
if (hires_denoising_strength <= 0.f || hires_denoising_strength > 1.f) {
|
||||
LOG_ERROR("error: hires denoising strength must be in (0.0, 1.0]");
|
||||
return false;
|
||||
}
|
||||
if (hires_upscale_tile_size < 1) {
|
||||
LOG_ERROR("error: hires upscale tile size must be positive");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode == UPSCALE) {
|
||||
if (init_image_path.length() == 0) {
|
||||
LOG_ERROR("error: upscale mode needs an init image (--init-img)\n");
|
||||
@@ -1885,8 +2048,11 @@ bool SDGenerationParams::validate(SDMode mode) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SDGenerationParams::resolve_and_validate(SDMode mode, const std::string& lora_model_dir, bool strict) {
|
||||
if (!resolve(lora_model_dir, strict)) {
|
||||
bool SDGenerationParams::resolve_and_validate(SDMode mode,
|
||||
const std::string& lora_model_dir,
|
||||
const std::string& hires_upscalers_dir,
|
||||
bool strict) {
|
||||
if (!resolve(lora_model_dir, hires_upscalers_dir, strict)) {
|
||||
return false;
|
||||
}
|
||||
if (!validate(mode)) {
|
||||
@@ -1957,6 +2123,16 @@ sd_img_gen_params_t SDGenerationParams::to_sd_img_gen_params_t() {
|
||||
params.pm_params = pm_params;
|
||||
params.vae_tiling_params = vae_tiling_params;
|
||||
params.cache = cache_params;
|
||||
|
||||
params.hires.enabled = hires_enabled;
|
||||
params.hires.upscaler = resolved_hires_upscaler;
|
||||
params.hires.model_path = hires_upscaler_model_path.empty() ? nullptr : hires_upscaler_model_path.c_str();
|
||||
params.hires.scale = hires_scale;
|
||||
params.hires.target_width = hires_width;
|
||||
params.hires.target_height = hires_height;
|
||||
params.hires.steps = hires_steps;
|
||||
params.hires.denoising_strength = hires_denoising_strength;
|
||||
params.hires.upscale_tile_size = hires_upscale_tile_size;
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -2081,6 +2257,15 @@ std::string SDGenerationParams::to_string() const {
|
||||
<< " seed: " << seed << ",\n"
|
||||
<< " upscale_repeats: " << upscale_repeats << ",\n"
|
||||
<< " upscale_tile_size: " << upscale_tile_size << ",\n"
|
||||
<< " hires: { enabled: " << (hires_enabled ? "true" : "false")
|
||||
<< ", upscaler: \"" << hires_upscaler << "\""
|
||||
<< ", model_path: \"" << hires_upscaler_model_path << "\""
|
||||
<< ", scale: " << hires_scale
|
||||
<< ", target_width: " << hires_width
|
||||
<< ", target_height: " << hires_height
|
||||
<< ", steps: " << hires_steps
|
||||
<< ", denoising_strength: " << hires_denoising_strength
|
||||
<< ", upscale_tile_size: " << hires_upscale_tile_size << " },\n"
|
||||
<< " vae_tiling_params: { "
|
||||
<< vae_tiling_params.enabled << ", "
|
||||
<< vae_tiling_params.tile_size_x << ", "
|
||||
@@ -2154,6 +2339,13 @@ std::string get_image_params(const SDContextParams& ctx_params, const SDGenerati
|
||||
if (gen_params.clip_skip != -1) {
|
||||
parameter_string += "Clip skip: " + std::to_string(gen_params.clip_skip) + ", ";
|
||||
}
|
||||
if (gen_params.hires_enabled) {
|
||||
parameter_string += "Hires upscale: " + gen_params.hires_upscaler + ", ";
|
||||
parameter_string += "Hires scale: " + std::to_string(gen_params.hires_scale) + ", ";
|
||||
parameter_string += "Hires resize: " + std::to_string(gen_params.hires_width) + "x" + std::to_string(gen_params.hires_height) + ", ";
|
||||
parameter_string += "Hires steps: " + std::to_string(gen_params.hires_steps) + ", ";
|
||||
parameter_string += "Denoising strength: " + std::to_string(gen_params.hires_denoising_strength) + ", ";
|
||||
}
|
||||
parameter_string += "Version: stable-diffusion.cpp";
|
||||
return parameter_string;
|
||||
}
|
||||
@@ -101,6 +101,7 @@ struct SDContextParams {
|
||||
sd_type_t wtype = SD_TYPE_COUNT;
|
||||
std::string tensor_type_rules;
|
||||
std::string lora_model_dir = ".";
|
||||
std::string hires_upscalers_dir;
|
||||
|
||||
std::map<std::string, std::string> embedding_map;
|
||||
std::vector<sd_embedding_t> embedding_vec;
|
||||
@@ -190,12 +191,23 @@ struct SDGenerationParams {
|
||||
int upscale_repeats = 1;
|
||||
int upscale_tile_size = 128;
|
||||
|
||||
bool hires_enabled = false;
|
||||
std::string hires_upscaler = "Latent";
|
||||
std::string hires_upscaler_model_path;
|
||||
float hires_scale = 2.f;
|
||||
int hires_width = 0;
|
||||
int hires_height = 0;
|
||||
int hires_steps = 0;
|
||||
float hires_denoising_strength = 0.7f;
|
||||
int hires_upscale_tile_size = 128;
|
||||
|
||||
std::map<std::string, float> lora_map;
|
||||
std::map<std::string, float> high_noise_lora_map;
|
||||
|
||||
// Derived and normalized fields.
|
||||
std::string prompt_with_lora; // for metadata record only
|
||||
std::vector<sd_lora_t> lora_vec;
|
||||
sd_hires_upscaler_t resolved_hires_upscaler;
|
||||
|
||||
// Owned execution payload.
|
||||
SDImageOwner init_image;
|
||||
@@ -225,9 +237,12 @@ struct SDGenerationParams {
|
||||
void set_width_and_height_if_unset(int w, int h);
|
||||
int get_resolved_width() const;
|
||||
int get_resolved_height() const;
|
||||
bool resolve(const std::string& lora_model_dir, bool strict = false);
|
||||
bool resolve(const std::string& lora_model_dir, const std::string& hires_upscalers_dir, bool strict = false);
|
||||
bool validate(SDMode mode);
|
||||
bool resolve_and_validate(SDMode mode, const std::string& lora_model_dir, bool strict = false);
|
||||
bool resolve_and_validate(SDMode mode,
|
||||
const std::string& lora_model_dir,
|
||||
const std::string& hires_upscalers_dir,
|
||||
bool strict = false);
|
||||
sd_img_gen_params_t to_sd_img_gen_params_t();
|
||||
sd_vid_gen_params_t to_sd_vid_gen_params_t();
|
||||
std::string to_string() const;
|
||||
|
||||
+253
-138
@@ -95,6 +95,57 @@ using WebPMuxPtr = std::unique_ptr<WebPMux, WebPMuxDeleter>;
|
||||
using WebPAnimEncoderPtr = std::unique_ptr<WebPAnimEncoder, WebPAnimEncoderDeleter>;
|
||||
#endif
|
||||
|
||||
#ifdef SD_USE_WEBM
|
||||
class MemoryMkvWriter : public mkvmuxer::IMkvWriter {
|
||||
public:
|
||||
mkvmuxer::int32 Write(const void* buf, mkvmuxer::uint32 len) override {
|
||||
if (buf == nullptr && len > 0) {
|
||||
return -1;
|
||||
}
|
||||
const size_t end_pos = position_ + static_cast<size_t>(len);
|
||||
if (end_pos > data_.size()) {
|
||||
data_.resize(end_pos);
|
||||
}
|
||||
if (len > 0) {
|
||||
memcpy(data_.data() + position_, buf, len);
|
||||
}
|
||||
position_ = end_pos;
|
||||
return 0;
|
||||
}
|
||||
|
||||
mkvmuxer::int64 Position() const override {
|
||||
return static_cast<mkvmuxer::int64>(position_);
|
||||
}
|
||||
|
||||
mkvmuxer::int32 Position(mkvmuxer::int64 position) override {
|
||||
if (position < 0) {
|
||||
return -1;
|
||||
}
|
||||
const size_t target = static_cast<size_t>(position);
|
||||
if (target > data_.size()) {
|
||||
data_.resize(target);
|
||||
}
|
||||
position_ = target;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool Seekable() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
void ElementStartNotify(mkvmuxer::uint64, mkvmuxer::int64) override {
|
||||
}
|
||||
|
||||
const std::vector<uint8_t>& data() const {
|
||||
return data_;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<uint8_t> data_;
|
||||
size_t position_ = 0;
|
||||
};
|
||||
#endif
|
||||
|
||||
bool read_binary_file_bytes(const char* path, std::vector<uint8_t>& data) {
|
||||
std::ifstream fin(fs::path(path), std::ios::binary);
|
||||
if (!fin) {
|
||||
@@ -570,6 +621,32 @@ void write_u16_le(FILE* f, uint16_t val) {
|
||||
fwrite(&val, 2, 1, f);
|
||||
}
|
||||
|
||||
void write_u32_le(std::vector<uint8_t>& data, uint32_t val) {
|
||||
data.push_back(static_cast<uint8_t>(val & 0xFF));
|
||||
data.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
|
||||
data.push_back(static_cast<uint8_t>((val >> 16) & 0xFF));
|
||||
data.push_back(static_cast<uint8_t>((val >> 24) & 0xFF));
|
||||
}
|
||||
|
||||
void write_u16_le(std::vector<uint8_t>& data, uint16_t val) {
|
||||
data.push_back(static_cast<uint8_t>(val & 0xFF));
|
||||
data.push_back(static_cast<uint8_t>((val >> 8) & 0xFF));
|
||||
}
|
||||
|
||||
void patch_u32_le(std::vector<uint8_t>& data, size_t offset, uint32_t val) {
|
||||
if (offset + 4 > data.size()) {
|
||||
return;
|
||||
}
|
||||
data[offset + 0] = static_cast<uint8_t>(val & 0xFF);
|
||||
data[offset + 1] = static_cast<uint8_t>((val >> 8) & 0xFF);
|
||||
data[offset + 2] = static_cast<uint8_t>((val >> 16) & 0xFF);
|
||||
data[offset + 3] = static_cast<uint8_t>((val >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
void write_fourcc(std::vector<uint8_t>& data, const char* fourcc) {
|
||||
data.insert(data.end(), fourcc, fourcc + 4);
|
||||
}
|
||||
|
||||
EncodedImageFormat encoded_image_format_from_path(const std::string& path) {
|
||||
std::string ext = fs::path(path).extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
||||
@@ -699,95 +776,96 @@ uint8_t* load_image_from_memory(const char* image_bytes,
|
||||
return load_image_common(true, image_bytes, len, width, height, expected_width, expected_height, expected_channel);
|
||||
}
|
||||
|
||||
int create_mjpg_avi_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> create_mjpg_avi_from_sd_images_to_vector(sd_image_t* images, int num_images, int fps, int quality) {
|
||||
if (num_images == 0) {
|
||||
fprintf(stderr, "Error: Image array is empty.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
FilePtr file(fopen(filename, "wb"));
|
||||
if (!file) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
FILE* f = file.get();
|
||||
|
||||
uint32_t width = images[0].width;
|
||||
uint32_t height = images[0].height;
|
||||
uint32_t channels = images[0].channel;
|
||||
if (channels != 3 && channels != 4) {
|
||||
fprintf(stderr, "Error: Unsupported channel count: %u\n", channels);
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
fwrite("RIFF", 4, 1, f);
|
||||
long riff_size_pos = ftell(f);
|
||||
write_u32_le(f, 0);
|
||||
fwrite("AVI ", 4, 1, f);
|
||||
// stb_image_write changes JPEG sampling behavior above quality 90.
|
||||
// MJPG AVI playback is more compatible when we keep the encoder on the
|
||||
// <= 90 path.
|
||||
const int mjpg_quality = std::clamp(quality, 1, 90);
|
||||
|
||||
fwrite("LIST", 4, 1, f);
|
||||
write_u32_le(f, 4 + 8 + 56 + 8 + 4 + 8 + 56 + 8 + 40);
|
||||
fwrite("hdrl", 4, 1, f);
|
||||
std::vector<uint8_t> avi_data;
|
||||
avi_data.reserve(static_cast<size_t>(num_images) * 1024);
|
||||
|
||||
fwrite("avih", 4, 1, f);
|
||||
write_u32_le(f, 56);
|
||||
write_u32_le(f, 1000000 / fps);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0x110);
|
||||
write_u32_le(f, num_images);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 1);
|
||||
write_u32_le(f, width * height * 3);
|
||||
write_u32_le(f, width);
|
||||
write_u32_le(f, height);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_fourcc(avi_data, "RIFF");
|
||||
const size_t riff_size_pos = avi_data.size();
|
||||
write_u32_le(avi_data, 0);
|
||||
write_fourcc(avi_data, "AVI ");
|
||||
|
||||
fwrite("LIST", 4, 1, f);
|
||||
write_u32_le(f, 4 + 8 + 56 + 8 + 40);
|
||||
fwrite("strl", 4, 1, f);
|
||||
write_fourcc(avi_data, "LIST");
|
||||
write_u32_le(avi_data, 4 + 8 + 56 + 8 + 4 + 8 + 56 + 8 + 40);
|
||||
write_fourcc(avi_data, "hdrl");
|
||||
|
||||
fwrite("strh", 4, 1, f);
|
||||
write_u32_le(f, 56);
|
||||
fwrite("vids", 4, 1, f);
|
||||
fwrite("MJPG", 4, 1, f);
|
||||
write_u32_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 1);
|
||||
write_u32_le(f, fps);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, num_images);
|
||||
write_u32_le(f, width * height * 3);
|
||||
write_u32_le(f, (uint32_t)-1);
|
||||
write_u32_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_u16_le(f, 0);
|
||||
write_fourcc(avi_data, "avih");
|
||||
write_u32_le(avi_data, 56);
|
||||
write_u32_le(avi_data, 1000000 / fps);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0x110);
|
||||
write_u32_le(avi_data, num_images);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 1);
|
||||
write_u32_le(avi_data, width * height * 3);
|
||||
write_u32_le(avi_data, width);
|
||||
write_u32_le(avi_data, height);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
|
||||
fwrite("strf", 4, 1, f);
|
||||
write_u32_le(f, 40);
|
||||
write_u32_le(f, 40);
|
||||
write_u32_le(f, width);
|
||||
write_u32_le(f, height);
|
||||
write_u16_le(f, 1);
|
||||
write_u16_le(f, 24);
|
||||
fwrite("MJPG", 4, 1, f);
|
||||
write_u32_le(f, width * height * 3);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_u32_le(f, 0);
|
||||
write_fourcc(avi_data, "LIST");
|
||||
write_u32_le(avi_data, 4 + 8 + 56 + 8 + 40);
|
||||
write_fourcc(avi_data, "strl");
|
||||
|
||||
fwrite("LIST", 4, 1, f);
|
||||
long movi_size_pos = ftell(f);
|
||||
write_u32_le(f, 0);
|
||||
fwrite("movi", 4, 1, f);
|
||||
write_fourcc(avi_data, "strh");
|
||||
write_u32_le(avi_data, 56);
|
||||
write_fourcc(avi_data, "vids");
|
||||
write_fourcc(avi_data, "MJPG");
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 1);
|
||||
write_u32_le(avi_data, fps);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, num_images);
|
||||
write_u32_le(avi_data, width * height * 3);
|
||||
write_u32_le(avi_data, static_cast<uint32_t>(-1));
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
write_u16_le(avi_data, 0);
|
||||
|
||||
write_fourcc(avi_data, "strf");
|
||||
write_u32_le(avi_data, 40);
|
||||
write_u32_le(avi_data, 40);
|
||||
write_u32_le(avi_data, width);
|
||||
write_u32_le(avi_data, height);
|
||||
write_u16_le(avi_data, 1);
|
||||
write_u16_le(avi_data, 24);
|
||||
write_fourcc(avi_data, "MJPG");
|
||||
write_u32_le(avi_data, width * height * 3);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
write_u32_le(avi_data, 0);
|
||||
|
||||
write_fourcc(avi_data, "LIST");
|
||||
const size_t movi_size_pos = avi_data.size();
|
||||
write_u32_le(avi_data, 0);
|
||||
write_fourcc(avi_data, "movi");
|
||||
|
||||
std::vector<avi_index_entry> index(static_cast<size_t>(num_images));
|
||||
std::vector<uint8_t> jpeg_data;
|
||||
@@ -801,55 +879,61 @@ int create_mjpg_avi_from_sd_images(const char* filename, sd_image_t* images, int
|
||||
buffer->insert(buffer->end(), src, src + size);
|
||||
};
|
||||
|
||||
if (!stbi_write_jpg_to_func(write_to_buf, &jpeg_data, images[i].width, images[i].height, channels, images[i].data, quality)) {
|
||||
if (!stbi_write_jpg_to_func(write_to_buf, &jpeg_data, images[i].width, images[i].height, channels, images[i].data, mjpg_quality)) {
|
||||
fprintf(stderr, "Error: Failed to encode JPEG frame.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
fwrite("00dc", 4, 1, f);
|
||||
write_u32_le(f, (uint32_t)jpeg_data.size());
|
||||
index[i].offset = ftell(f) - 8;
|
||||
index[i].size = (uint32_t)jpeg_data.size();
|
||||
fwrite(jpeg_data.data(), 1, jpeg_data.size(), f);
|
||||
index[i].offset = static_cast<uint32_t>(avi_data.size());
|
||||
write_fourcc(avi_data, "00dc");
|
||||
write_u32_le(avi_data, static_cast<uint32_t>(jpeg_data.size()));
|
||||
index[i].size = (uint32_t)jpeg_data.size();
|
||||
avi_data.insert(avi_data.end(), jpeg_data.begin(), jpeg_data.end());
|
||||
|
||||
if (jpeg_data.size() % 2) {
|
||||
fputc(0, f);
|
||||
avi_data.push_back(0);
|
||||
}
|
||||
}
|
||||
|
||||
long cur_pos = ftell(f);
|
||||
long movi_size = cur_pos - movi_size_pos - 4;
|
||||
fseek(f, movi_size_pos, SEEK_SET);
|
||||
write_u32_le(f, movi_size);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
const size_t movi_size = avi_data.size() - movi_size_pos - 4;
|
||||
patch_u32_le(avi_data, movi_size_pos, static_cast<uint32_t>(movi_size));
|
||||
|
||||
fwrite("idx1", 4, 1, f);
|
||||
write_u32_le(f, num_images * 16);
|
||||
write_fourcc(avi_data, "idx1");
|
||||
write_u32_le(avi_data, num_images * 16);
|
||||
for (int i = 0; i < num_images; i++) {
|
||||
fwrite("00dc", 4, 1, f);
|
||||
write_u32_le(f, 0x10);
|
||||
write_u32_le(f, index[i].offset);
|
||||
write_u32_le(f, index[i].size);
|
||||
write_fourcc(avi_data, "00dc");
|
||||
write_u32_le(avi_data, 0x10);
|
||||
write_u32_le(avi_data, index[i].offset);
|
||||
write_u32_le(avi_data, index[i].size);
|
||||
}
|
||||
|
||||
cur_pos = ftell(f);
|
||||
long file_size = cur_pos - riff_size_pos - 4;
|
||||
fseek(f, riff_size_pos, SEEK_SET);
|
||||
write_u32_le(f, file_size);
|
||||
fseek(f, cur_pos, SEEK_SET);
|
||||
const size_t file_size = avi_data.size() - riff_size_pos - 4;
|
||||
patch_u32_le(avi_data, riff_size_pos, static_cast<uint32_t>(file_size));
|
||||
|
||||
return avi_data;
|
||||
}
|
||||
|
||||
int create_mjpg_avi_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> avi_data = create_mjpg_avi_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
if (avi_data.empty()) {
|
||||
return -1;
|
||||
}
|
||||
if (!write_binary_file_bytes(filename, avi_data)) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef SD_USE_WEBP
|
||||
int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> create_animated_webp_from_sd_images_to_vector(sd_image_t* images, int num_images, int fps, int quality) {
|
||||
if (num_images == 0) {
|
||||
fprintf(stderr, "Error: Image array is empty.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
if (fps <= 0) {
|
||||
fprintf(stderr, "Error: FPS must be positive.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(images[0].width);
|
||||
@@ -857,14 +941,14 @@ int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images
|
||||
const int channels = static_cast<int>(images[0].channel);
|
||||
if (channels != 1 && channels != 3 && channels != 4) {
|
||||
fprintf(stderr, "Error: Unsupported channel count: %d\n", channels);
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
WebPAnimEncoderOptions anim_options;
|
||||
WebPConfig config;
|
||||
if (!WebPAnimEncoderOptionsInit(&anim_options) || !WebPConfigInit(&config)) {
|
||||
fprintf(stderr, "Error: Failed to initialize WebP animation encoder.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
config.quality = static_cast<float>(quality);
|
||||
@@ -875,13 +959,13 @@ int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images
|
||||
}
|
||||
if (!WebPValidateConfig(&config)) {
|
||||
fprintf(stderr, "Error: Invalid WebP encoder configuration.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
WebPAnimEncoderPtr enc(WebPAnimEncoderNew(width, height, &anim_options));
|
||||
if (enc == nullptr) {
|
||||
fprintf(stderr, "Error: Could not create WebPAnimEncoder object.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
const int frame_duration_ms = std::max(1, static_cast<int>(std::lround(1000.0 / static_cast<double>(fps))));
|
||||
@@ -891,13 +975,13 @@ int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images
|
||||
const sd_image_t& image = images[i];
|
||||
if (static_cast<int>(image.width) != width || static_cast<int>(image.height) != height) {
|
||||
fprintf(stderr, "Error: Frame dimensions do not match.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
WebPPictureGuard picture;
|
||||
if (!picture.initialized) {
|
||||
fprintf(stderr, "Error: Failed to initialize WebPPicture.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
picture.picture.use_argb = 1;
|
||||
picture.picture.width = width;
|
||||
@@ -921,12 +1005,12 @@ int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images
|
||||
|
||||
if (!picture_ok) {
|
||||
fprintf(stderr, "Error: Failed to import frame into WebPPicture.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!WebPAnimEncoderAdd(enc.get(), &picture.picture, timestamp_ms, &config)) {
|
||||
fprintf(stderr, "Error: Failed to add frame to animated WebP: %s\n", WebPAnimEncoderGetError(enc.get()));
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
timestamp_ms += frame_duration_ms;
|
||||
@@ -934,52 +1018,50 @@ int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images
|
||||
|
||||
if (!WebPAnimEncoderAdd(enc.get(), nullptr, timestamp_ms, nullptr)) {
|
||||
fprintf(stderr, "Error: Failed to finalize animated WebP frames: %s\n", WebPAnimEncoderGetError(enc.get()));
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
WebPDataGuard webp_data;
|
||||
if (!WebPAnimEncoderAssemble(enc.get(), &webp_data.data)) {
|
||||
fprintf(stderr, "Error: Failed to assemble animated WebP: %s\n", WebPAnimEncoderGetError(enc.get()));
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
FilePtr f(fopen(filename, "wb"));
|
||||
if (!f) {
|
||||
return std::vector<uint8_t>(webp_data.data.bytes, webp_data.data.bytes + webp_data.data.size);
|
||||
}
|
||||
|
||||
int create_animated_webp_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> webp_data = create_animated_webp_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
if (webp_data.empty()) {
|
||||
return -1;
|
||||
}
|
||||
if (!write_binary_file_bytes(filename, webp_data)) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
if (webp_data.data.size > 0 && fwrite(webp_data.data.bytes, 1, webp_data.data.size, f.get()) != webp_data.data.size) {
|
||||
fprintf(stderr, "Error: Failed to write animated WebP file.\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SD_USE_WEBM
|
||||
int create_webm_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> create_webm_from_sd_images_to_vector(sd_image_t* images, int num_images, int fps, int quality) {
|
||||
if (num_images == 0) {
|
||||
fprintf(stderr, "Error: Image array is empty.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
if (fps <= 0) {
|
||||
fprintf(stderr, "Error: FPS must be positive.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
const int width = static_cast<int>(images[0].width);
|
||||
const int height = static_cast<int>(images[0].height);
|
||||
if (width <= 0 || height <= 0) {
|
||||
fprintf(stderr, "Error: Invalid frame dimensions.\n");
|
||||
return -1;
|
||||
return {};
|
||||
}
|
||||
|
||||
mkvmuxer::MkvWriter writer;
|
||||
if (!writer.Open(filename)) {
|
||||
fprintf(stderr, "Error: Could not open WebM file for writing.\n");
|
||||
return -1;
|
||||
}
|
||||
MemoryMkvWriter writer;
|
||||
|
||||
const int ret = [&]() -> int {
|
||||
mkvmuxer::Segment segment;
|
||||
@@ -1045,30 +1127,63 @@ int create_webm_from_sd_images(const char* filename, sd_image_t* images, int num
|
||||
}
|
||||
return 0;
|
||||
}();
|
||||
writer.Close();
|
||||
return ret;
|
||||
if (ret != 0) {
|
||||
return {};
|
||||
}
|
||||
return writer.data();
|
||||
}
|
||||
|
||||
int create_webm_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::vector<uint8_t> webm_data = create_webm_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
if (webm_data.empty()) {
|
||||
return -1;
|
||||
}
|
||||
if (!write_binary_file_bytes(filename, webm_data)) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
int create_video_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::string path = filename ? filename : "";
|
||||
auto pos = path.find_last_of('.');
|
||||
std::string ext = pos == std::string::npos ? "" : path.substr(pos);
|
||||
for (char& ch : ext) {
|
||||
ch = static_cast<char>(tolower(static_cast<unsigned char>(ch)));
|
||||
std::vector<uint8_t> create_video_from_sd_images_to_vector(const std::string& output_format,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality) {
|
||||
std::string format = output_format;
|
||||
std::transform(format.begin(), format.end(), format.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(tolower(c)); });
|
||||
if (!format.empty() && format[0] == '.') {
|
||||
format.erase(format.begin());
|
||||
}
|
||||
|
||||
#ifdef SD_USE_WEBM
|
||||
if (ext == ".webm") {
|
||||
return create_webm_from_sd_images(filename, images, num_images, fps, quality);
|
||||
if (format == "webm") {
|
||||
return create_webm_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef SD_USE_WEBP
|
||||
if (ext == ".webp") {
|
||||
return create_animated_webp_from_sd_images(filename, images, num_images, fps, quality);
|
||||
if (format == "webp") {
|
||||
return create_animated_webp_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
}
|
||||
#endif
|
||||
|
||||
return create_mjpg_avi_from_sd_images(filename, images, num_images, fps, quality);
|
||||
return create_mjpg_avi_from_sd_images_to_vector(images, num_images, fps, quality);
|
||||
}
|
||||
|
||||
int create_video_from_sd_images(const char* filename, sd_image_t* images, int num_images, int fps, int quality) {
|
||||
std::string path = filename ? filename : "";
|
||||
auto pos = path.find_last_of('.');
|
||||
std::string ext = pos == std::string::npos ? "" : path.substr(pos);
|
||||
std::vector<uint8_t> video_data = create_video_from_sd_images_to_vector(ext, images, num_images, fps, quality);
|
||||
if (video_data.empty()) {
|
||||
return -1;
|
||||
}
|
||||
if (!write_binary_file_bytes(filename, video_data)) {
|
||||
perror("Error opening file for writing");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -58,6 +58,10 @@ int create_mjpg_avi_from_sd_images(const char* filename,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
std::vector<uint8_t> create_mjpg_avi_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
|
||||
#ifdef SD_USE_WEBP
|
||||
int create_animated_webp_from_sd_images(const char* filename,
|
||||
@@ -65,6 +69,10 @@ int create_animated_webp_from_sd_images(const char* filename,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
std::vector<uint8_t> create_animated_webp_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
#endif
|
||||
|
||||
#ifdef SD_USE_WEBM
|
||||
@@ -73,6 +81,10 @@ int create_webm_from_sd_images(const char* filename,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
std::vector<uint8_t> create_webm_from_sd_images_to_vector(sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
#endif
|
||||
|
||||
int create_video_from_sd_images(const char* filename,
|
||||
@@ -80,5 +92,10 @@ int create_video_from_sd_images(const char* filename,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
std::vector<uint8_t> create_video_from_sd_images_to_vector(const std::string& output_format,
|
||||
sd_image_t* images,
|
||||
int num_images,
|
||||
int fps,
|
||||
int quality = 90);
|
||||
|
||||
#endif // __MEDIA_IO_H__
|
||||
@@ -50,7 +50,13 @@ if(SD_SERVER_BUILD_FRONTEND AND EXISTS "${FRONTEND_DIR}")
|
||||
|
||||
set_source_files_properties("${GENERATED_HTML_HEADER}" PROPERTIES GENERATED TRUE)
|
||||
else()
|
||||
message(WARNING "pnpm not found, frontend build disabled")
|
||||
if(EXISTS "${GENERATED_HTML_HEADER}")
|
||||
message(STATUS "pnpm not found; using pre-built frontend header detected at ${GENERATED_HTML_HEADER}")
|
||||
set(HAVE_FRONTEND_BUILD ON)
|
||||
add_custom_target(${TARGET}_frontend)
|
||||
else()
|
||||
message(WARNING "pnpm not found; frontend build disabled.")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Frontend disabled or directory not found: ${FRONTEND_DIR}")
|
||||
|
||||
+84
-52
@@ -123,11 +123,11 @@ In this case, the server will load and serve the specified `index.html` file ins
|
||||
usage: ./bin/sd-server [options]
|
||||
|
||||
Svr Options:
|
||||
-l, --listen-ip <string> server listen ip (default: 127.0.0.1)
|
||||
-l, --listen-ip <string> server listen ip (default: 127.0.0.1)
|
||||
--serve-html-path <string> path to HTML file to serve at root (optional)
|
||||
--listen-port <int> server listen port (default: 1234)
|
||||
-v, --verbose print extra info
|
||||
--color colors the logging tags according to level
|
||||
--color colors the logging tags according to level
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Context Options:
|
||||
@@ -136,7 +136,8 @@ Context Options:
|
||||
--clip_g <string> path to the clip-g text encoder
|
||||
--clip_vision <string> path to the clip-vision encoder
|
||||
--t5xxl <string> path to the t5xxl text encoder
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image, mistral-small3.2 for flux2, ...)
|
||||
--llm <string> path to the llm text encoder. For example: (qwenvl2.5 for qwen-image,
|
||||
mistral-small3.2 for flux2, ...)
|
||||
--llm_vision <string> path to the llm vit
|
||||
--qwen2vl <string> alias of --llm. Deprecated.
|
||||
--qwen2vl_vision <string> alias of --llm_vision. Deprecated.
|
||||
@@ -148,16 +149,16 @@ Context Options:
|
||||
--control-net <string> path to control net model
|
||||
--embd-dir <string> embeddings directory
|
||||
--lora-model-dir <string> lora model directory
|
||||
--hires-upscalers-dir <string> highres fix upscaler model directory
|
||||
--tensor-type-rules <string> weight type per tensor pattern (example: "^vae\.=f16,model\.=q8_0")
|
||||
--photo-maker <string> path to PHOTOMAKER model
|
||||
--upscale-model <string> path to esrgan model.
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0, then threads will be set to the number of
|
||||
CPU physical cores
|
||||
-t, --threads <int> number of threads to use during computation (default: -1). If threads <= 0,
|
||||
then threads will be set to the number of CPU physical cores
|
||||
--chroma-t5-mask-pad <int> t5 mask pad size of chroma
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--force-sdxl-vae-conv-scale force use of conv scale on sdxl vae
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM when needed
|
||||
--offload-to-cpu place the weights in RAM to save VRAM, and automatically load them into VRAM
|
||||
when needed
|
||||
--mmap whether to memory-map model
|
||||
--control-net-cpu keep controlnet in cpu (for low vram)
|
||||
--clip-on-cpu keep clip in cpu (for low vram)
|
||||
@@ -172,20 +173,19 @@ Context Options:
|
||||
--chroma-disable-dit-mask disable dit mask for chroma
|
||||
--qwen-image-zero-cond-t enable zero_cond_t for qwen image
|
||||
--chroma-enable-t5-mask enable t5 mask for chroma
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K, q4_K). If not specified, the default is the
|
||||
type of the weight file
|
||||
--type weight type (examples: f32, f16, q4_0, q4_1, q5_0, q5_1, q8_0, q2_K, q3_K,
|
||||
q4_K). If not specified, the default is the type of the weight file
|
||||
--rng RNG, one of [std_default, cuda, cpu], default: cuda(sd-webui), cpu(comfyui)
|
||||
--sampler-rng sampler RNG, one of [std_default, cuda, cpu]. If not specified, use --rng
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow, flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is auto. In auto mode, if the model weights
|
||||
contain any quantized parameters, the at_runtime mode will be used; otherwise,
|
||||
immediately will be used.The immediately mode may have precision and
|
||||
compatibility issues with quantized parameters, but it usually offers faster inference
|
||||
speed and, in some cases, lower memory usage. The at_runtime mode, on the
|
||||
other hand, is exactly the opposite.
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size if < 1, in number of tiles per dim if >=1
|
||||
(overrides --vae-tile-size)
|
||||
--prediction prediction type override, one of [eps, v, edm_v, sd3_flow, flux_flow,
|
||||
flux2_flow]
|
||||
--lora-apply-mode the way to apply LoRA, one of [auto, immediately, at_runtime], default is
|
||||
auto. In auto mode, if the model weights contain any quantized parameters,
|
||||
the at_runtime mode will be used; otherwise, immediately will be used.The
|
||||
immediately mode may have precision and compatibility issues with quantized
|
||||
parameters, but it usually offers faster inference speed and, in some cases,
|
||||
lower memory usage. The at_runtime mode, on the other hand, is exactly the
|
||||
opposite.
|
||||
|
||||
Default Generation Options:
|
||||
-p, --prompt <string> the prompt to render
|
||||
@@ -194,65 +194,97 @@ Default Generation Options:
|
||||
--end-img <string> path to the end image, required by flf2v
|
||||
--mask <string> path to the mask image
|
||||
--control-image <string> path to control image, control net
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames inside should be stored as images in
|
||||
lexicographical (character) order. For example, if the control video path is
|
||||
`frames`, the directory contain images such as 00.png, 01.png, ... etc.
|
||||
--control-video <string> path to control video frames, It must be a directory path. The video frames
|
||||
inside should be stored as images in lexicographical (character) order. For
|
||||
example, if the control video path is `frames`, the directory contain images
|
||||
such as 00.png, 01.png, ... etc.
|
||||
--pm-id-images-dir <string> path to PHOTOMAKER input id images dir
|
||||
--pm-id-embed-path <string> path to PHOTOMAKER v2 id embed
|
||||
--hires-upscaler <string> highres fix upscaler, Lanczos, Nearest, Latent, Latent (nearest), Latent
|
||||
(nearest-exact), Latent (antialiased), Latent (bicubic), Latent (bicubic
|
||||
antialiased), or a model name under --hires-upscalers-dir (default: Latent)
|
||||
-H, --height <int> image height, in pixel space (default: 512)
|
||||
-W, --width <int> image width, in pixel space (default: 512)
|
||||
--steps <int> number of sample steps (default: 20)
|
||||
--high-noise-steps <int> (high noise) number of sample steps (default: -1 = auto)
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer (default: -1). <= 0 represents unspecified,
|
||||
will be 1 for SD1.x, 2 for SD2.x
|
||||
--clip-skip <int> ignore last layers of CLIP network; 1 ignores none, 2 ignores one layer
|
||||
(default: -1). <= 0 represents unspecified, will be 1 for SD1.x, 2 for SD2.x
|
||||
-b, --batch-count <int> batch count
|
||||
--video-frames <int> video frames (default: 1)
|
||||
--fps <int> fps (default: 24)
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for NitroSD-Realism around 250 and 500 for
|
||||
NitroSD-Vibrant
|
||||
--timestep-shift <int> shift timestep for NitroFusion models (default: 0). recommended N for
|
||||
NitroSD-Realism around 250 and 500 for NitroSD-Vibrant
|
||||
--upscale-repeats <int> Run the ESRGAN upscaler this many times (default: 1)
|
||||
--upscale-tile-size <int> tile size for ESRGAN upscaling (default: 128)
|
||||
--hires-width <int> highres fix target width, 0 to use --hires-scale (default: 0)
|
||||
--hires-height <int> highres fix target height, 0 to use --hires-scale (default: 0)
|
||||
--hires-steps <int> highres fix second pass sample steps, 0 to reuse --steps (default: 0)
|
||||
--hires-upscale-tile-size <int> highres fix upscaler tile size, reserved for model-backed upscalers (default:
|
||||
128)
|
||||
--cfg-scale <float> unconditional guidance scale: (default: 7.0)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same as --cfg-scale)
|
||||
--img-cfg-scale <float> image guidance scale for inpaint or instruct-pix2pix models: (default: same
|
||||
as --cfg-scale)
|
||||
--guidance <float> distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means disabled, a value of 2.5 is nice for sd3.5
|
||||
medium
|
||||
--slg-scale <float> skip layer guidance (SLG) scale, only for DiT models: (default: 0). 0 means
|
||||
disabled, a value of 2.5 is nice for sd3.5 medium
|
||||
--skip-layer-start <float> SLG enabling point (default: 0.01)
|
||||
--skip-layer-end <float> SLG disabling point (default: 0.2)
|
||||
--eta <float> noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)
|
||||
--eta <float> noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and
|
||||
res_2s; 1 for euler_a, er_sde and dpm++2s_a)
|
||||
--flow-shift <float> shift value for Flow models like SD3.x or WAN (default: auto)
|
||||
--high-noise-cfg-scale <float> (high noise) unconditional guidance scale: (default: 7.0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models (default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input (default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default: 0)
|
||||
--high-noise-img-cfg-scale <float> (high noise) image guidance scale for inpaint or instruct-pix2pix models
|
||||
(default: same as --cfg-scale)
|
||||
--high-noise-guidance <float> (high noise) distilled guidance scale for models with guidance input
|
||||
(default: 3.5)
|
||||
--high-noise-slg-scale <float> (high noise) skip layer guidance (SLG) scale, only for DiT models: (default:
|
||||
0)
|
||||
--high-noise-skip-layer-start <float> (high noise) SLG enabling point (default: 0.01)
|
||||
--high-noise-skip-layer-end <float> (high noise) SLG disabling point (default: 0.2)
|
||||
--high-noise-eta <float> (high noise) noise multiplier (default: 0 for ddim_trailing, tcd, res_multistep and res_2s; 1 for euler_a and dpm++2s_a)
|
||||
--high-noise-eta <float> (high noise) noise multiplier (default: 0 for ddim_trailing, tcd,
|
||||
res_multistep and res_2s; 1 for euler_a, er_sde and dpm++2s_a)
|
||||
--strength <float> strength for noising/unnoising (default: 0.75)
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if `--high-noise-steps` is set to -1
|
||||
--pm-style-strength <float>
|
||||
--control-strength <float> strength to apply Control Net (default: 0.9). 1.0 corresponds to full
|
||||
destruction of information in init image
|
||||
--moe-boundary <float> timestep boundary for Wan2.2 MoE model. (default: 0.875). Only enabled if
|
||||
`--high-noise-steps` is set to -1
|
||||
--vace-strength <float> wan vace strength
|
||||
--increase-ref-index automatically increase the indices of references images based on the order they are listed (starting with 1).
|
||||
--vae-tile-overlap <float> tile overlap for vae tiling, in fraction of tile size (default: 0.5)
|
||||
--hires-scale <float> highres fix scale when target size is not set (default: 2.0)
|
||||
--hires-denoising-strength <float> highres fix second pass denoising strength (default: 0.7)
|
||||
--increase-ref-index automatically increase the indices of references images based on the order
|
||||
they are listed (starting with 1).
|
||||
--disable-auto-resize-ref-image disable auto resize of ref images
|
||||
--disable-image-metadata do not embed generation metadata on image files
|
||||
--vae-tiling process vae in tiles to reduce memory usage
|
||||
--hires enable highres fix
|
||||
-s, --seed RNG seed (default: 42, use random seed for < 0)
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing,
|
||||
tcd, res_multistep, res_2s] (default: euler for Flux/SD3/Wan, euler_a
|
||||
otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm,
|
||||
ddim_trailing, tcd, res_multistep, res_2s] default: euler for Flux/SD3/Wan,
|
||||
euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits, smoothstep, sgm_uniform, simple,
|
||||
kl_optimal, lcm, bong_tangent], default: discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g., "14.61,7.8,3.5,0.0").
|
||||
--sampling-method sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a, dpm++2m,
|
||||
dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep, res_2s,
|
||||
er_sde] (default: euler for Flux/SD3/Wan, euler_a otherwise)
|
||||
--high-noise-sampling-method (high noise) sampling method, one of [euler, euler_a, heun, dpm2, dpm++2s_a,
|
||||
dpm++2m, dpm++2mv2, ipndm, ipndm_v, lcm, ddim_trailing, tcd, res_multistep,
|
||||
res_2s, er_sde] default: euler for Flux/SD3/Wan, euler_a otherwise
|
||||
--scheduler denoiser sigma scheduler, one of [discrete, karras, exponential, ays, gits,
|
||||
smoothstep, sgm_uniform, simple, kl_optimal, lcm, bong_tangent], default:
|
||||
discrete
|
||||
--sigmas custom sigma values for the sampler, comma-separated (e.g.,
|
||||
"14.61,7.8,3.5,0.0").
|
||||
--skip-layers layers to skip for SLG steps (default: [7,8,9])
|
||||
--high-noise-skip-layers (high noise) layers to skip for SLG steps (default: [7,8,9])
|
||||
-r, --ref-image reference image for Flux Kontext models (can be used multiple times)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET), 'dbcache'/'taylorseer'/'cache-dit' (DiT block-level), 'spectrum' (UNET/DiT Chebyshev+Taylor forecasting)
|
||||
--cache-mode caching method: 'easycache' (DiT), 'ucache' (UNET),
|
||||
'dbcache'/'taylorseer'/'cache-dit' (DiT block-level), 'spectrum' (UNET/DiT
|
||||
Chebyshev+Taylor forecasting)
|
||||
--cache-option named cache params (key=value format, comma-separated). easycache/ucache:
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit: Fn=,Bn=,threshold=,warmup=. Examples:
|
||||
"threshold=0.25" or "threshold=1.5,reset=0"
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g., "1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
threshold=,start=,end=,decay=,relative=,reset=; dbcache/taylorseer/cache-dit:
|
||||
Fn=,Bn=,threshold=,warmup=; spectrum: w=,m=,lam=,window=,flex=,warmup=,stop=.
|
||||
Examples: "threshold=0.25" or "threshold=1.5,reset=0"
|
||||
--scm-mask SCM steps mask for cache-dit: comma-separated 0/1 (e.g.,
|
||||
"1,1,1,0,0,1,0,0,1,0") - 1=compute, 0=can cache
|
||||
--scm-policy SCM policy: 'dynamic' (default) or 'static'
|
||||
--vae-tile-size tile size for vae tiling, format [X]x[Y] (default: 32x32)
|
||||
--vae-relative-tile-size relative tile size for vae tiling, format [X]x[Y], in fraction of image size
|
||||
if < 1, in number of tiles per dim if >=1 (overrides --vae-tile-size)
|
||||
```
|
||||
+587
-93
@@ -9,7 +9,7 @@ The server currently exposes three API families:
|
||||
- `sdcpp API` under `/sdcpp/v1/...`
|
||||
|
||||
The `sdcpp API` is the native API surface.
|
||||
Its request schema is also the canonical schema for `sd_cpp_extra_args`.
|
||||
Its request schema is the same schema used by `sd_cpp_extra_args`.
|
||||
|
||||
Global LoRA rule:
|
||||
|
||||
@@ -38,6 +38,8 @@ Current generation-related endpoints include:
|
||||
- `POST /sdapi/v1/txt2img`
|
||||
- `POST /sdapi/v1/img2img`
|
||||
- `GET /sdapi/v1/loras`
|
||||
- `GET /sdapi/v1/upscalers`
|
||||
- `GET /sdapi/v1/latent-upscale-modes`
|
||||
- `GET /sdapi/v1/samplers`
|
||||
- `GET /sdapi/v1/schedulers`
|
||||
- `GET /sdapi/v1/sd-models`
|
||||
@@ -55,8 +57,6 @@ Current endpoints include:
|
||||
- `POST /sdcpp/v1/jobs/{id}/cancel`
|
||||
- `POST /sdcpp/v1/vid_gen`
|
||||
|
||||
`POST /sdcpp/v1/vid_gen` is currently exposed but returns `501 Not Implemented`.
|
||||
|
||||
## `sd_cpp_extra_args`
|
||||
|
||||
`sd_cpp_extra_args` is an extension mechanism for the compatibility APIs.
|
||||
@@ -79,12 +79,12 @@ Behavior:
|
||||
- The JSON block is parsed using the same field rules as the `sdcpp API`.
|
||||
- The block is removed from the final prompt before generation.
|
||||
|
||||
Intended use:
|
||||
Supported use:
|
||||
|
||||
- extend `OpenAI API` requests with native `stable-diffusion.cpp` controls
|
||||
- extend `sdapi` requests with native `stable-diffusion.cpp` controls
|
||||
|
||||
Not intended use:
|
||||
Unsupported use:
|
||||
|
||||
- do not use `sd_cpp_extra_args` with `/sdcpp/v1/*`
|
||||
|
||||
@@ -218,6 +218,13 @@ Currently supported request fields:
|
||||
| `scheduler` | `string` | Scheduler name |
|
||||
| `lora` | `array<object>` | Structured LoRA list |
|
||||
| `extra_images` | `array<string>` | Base64 or data URL images |
|
||||
| `enable_hr` | `boolean` | Enable highres fix for `txt2img` |
|
||||
| `hr_upscaler` | `string` | `Lanczos`, `Nearest`, a latent mode such as `Latent (nearest-exact)`, or an upscaler model name from `/sdapi/v1/upscalers` |
|
||||
| `hr_scale` | `number` | Highres scale when resize target is not set |
|
||||
| `hr_resize_x` | `integer` | Highres target width, `0` to use scale |
|
||||
| `hr_resize_y` | `integer` | Highres target height, `0` to use scale |
|
||||
| `hr_steps` | `integer` | Highres second-pass sample steps, `0` to reuse `steps` |
|
||||
| `denoising_strength` | `number` | Highres denoising strength for `txt2img` |
|
||||
|
||||
Native extension fields:
|
||||
|
||||
@@ -243,6 +250,8 @@ Currently supported request fields:
|
||||
| `inpainting_mask_invert` | `integer` or `boolean` | Treated as invert flag |
|
||||
| `denoising_strength` | `number` | Clamped to `0.0..1.0` |
|
||||
|
||||
Highres fix fields are currently handled for `txt2img`; `img2img` uses `denoising_strength` as image-to-image strength.
|
||||
|
||||
Native extension fields:
|
||||
|
||||
- any `sdcpp API` fields embedded through `sd_cpp_extra_args` inside `prompt`
|
||||
@@ -260,6 +269,8 @@ Response fields:
|
||||
Currently exposed:
|
||||
|
||||
- `GET /sdapi/v1/loras`
|
||||
- `GET /sdapi/v1/upscalers`
|
||||
- `GET /sdapi/v1/latent-upscale-modes`
|
||||
- `GET /sdapi/v1/samplers`
|
||||
- `GET /sdapi/v1/schedulers`
|
||||
- `GET /sdapi/v1/sd-models`
|
||||
@@ -274,6 +285,26 @@ Response fields:
|
||||
| `[].name` | `string` | Display name derived from file stem |
|
||||
| `[].path` | `string` | Relative path under the configured LoRA directory |
|
||||
|
||||
`GET /sdapi/v1/upscalers`
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `[].name` | `string` | Built-in name or model stem |
|
||||
| `[].model_name` | `string \| null` | Model family label for model-backed upscalers |
|
||||
| `[].model_path` | `string \| null` | Absolute model path for model-backed upscalers |
|
||||
| `[].model_url` | `string \| null` | Currently always null |
|
||||
| `[].scale` | `integer` | Currently `4` |
|
||||
|
||||
Built-in entries include `None`, `Lanczos`, and `Nearest`. Model-backed entries are scanned from the top level of `--hires-upscalers-dir`; subdirectories are not scanned.
|
||||
|
||||
`GET /sdapi/v1/latent-upscale-modes`
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `[].name` | `string` | WebUI-compatible latent upscale mode name |
|
||||
|
||||
Built-in latent modes include `Latent`, `Latent (nearest)`, `Latent (nearest-exact)`, `Latent (antialiased)`, `Latent (bicubic)`, and `Latent (bicubic antialiased)`.
|
||||
|
||||
`GET /sdapi/v1/samplers`
|
||||
|
||||
| Field | Type | Notes |
|
||||
@@ -372,20 +403,26 @@ Field types:
|
||||
|
||||
Returns frontend-friendly capability metadata.
|
||||
|
||||
Typical contents:
|
||||
The mode-aware fields are the primary interface. The top-level compatibility fields are deprecated mirrors kept for older clients.
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `model` | `object` |
|
||||
| `defaults` | `object` |
|
||||
| `loras` | `array<object>` |
|
||||
| `samplers` | `array<string>` |
|
||||
| `schedulers` | `array<string>` |
|
||||
| `output_formats` | `array<string>` |
|
||||
| `limits` | `object` |
|
||||
| `features` | `object` |
|
||||
Top-level fields:
|
||||
|
||||
Nested fields currently returned:
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `model` | `object` | Loaded model metadata |
|
||||
| `current_mode` | `string` | The native generation mode mirrored by top-level compatibility fields |
|
||||
| `supported_modes` | `array<string>` | Supported native modes such as `img_gen` or `vid_gen` |
|
||||
| `defaults` | `object` | Deprecated compatibility mirror of `defaults_by_mode[current_mode]` |
|
||||
| `output_formats` | `array<string>` | Deprecated compatibility mirror of `output_formats_by_mode[current_mode]` |
|
||||
| `features` | `object` | Deprecated compatibility mirror of `features_by_mode[current_mode]` |
|
||||
| `defaults_by_mode` | `object` | Explicit defaults for each supported mode |
|
||||
| `output_formats_by_mode` | `object` | Explicit output formats for each supported mode |
|
||||
| `features_by_mode` | `object` | Explicit feature flags for each supported mode |
|
||||
| `samplers` | `array<string>` | Available sampling methods |
|
||||
| `schedulers` | `array<string>` | Available schedulers |
|
||||
| `loras` | `array<object>` | Available LoRA entries |
|
||||
| `upscalers` | `array<object>` | Available model-backed highres upscalers |
|
||||
| `limits` | `object` | Shared queue and size limits |
|
||||
|
||||
`model`
|
||||
|
||||
@@ -395,50 +432,24 @@ Nested fields currently returned:
|
||||
| `model.stem` | `string` |
|
||||
| `model.path` | `string` |
|
||||
|
||||
`defaults`
|
||||
Compatibility rules:
|
||||
|
||||
- `defaults`, `output_formats`, and `features` are deprecated compatibility mirrors
|
||||
- those three top-level fields always mirror `current_mode`
|
||||
- `supported_modes`, `defaults_by_mode`, `output_formats_by_mode`, and `features_by_mode` are the mode-aware fields
|
||||
|
||||
Mode-aware objects:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `defaults.prompt` | `string` |
|
||||
| `defaults.negative_prompt` | `string` |
|
||||
| `defaults.clip_skip` | `integer` |
|
||||
| `defaults.width` | `integer` |
|
||||
| `defaults.height` | `integer` |
|
||||
| `defaults.strength` | `number` |
|
||||
| `defaults.seed` | `integer` |
|
||||
| `defaults.batch_count` | `integer` |
|
||||
| `defaults.auto_resize_ref_image` | `boolean` |
|
||||
| `defaults.increase_ref_index` | `boolean` |
|
||||
| `defaults.control_strength` | `number` |
|
||||
| `defaults.sample_params` | `object` |
|
||||
| `defaults.sample_params.scheduler` | `string` |
|
||||
| `defaults.sample_params.sample_method` | `string` |
|
||||
| `defaults.sample_params.sample_steps` | `integer` |
|
||||
| `defaults.sample_params.eta` | `number \| null` |
|
||||
| `defaults.sample_params.shifted_timestep` | `integer` |
|
||||
| `defaults.sample_params.flow_shift` | `number \| null` |
|
||||
| `defaults.sample_params.guidance` | `object` |
|
||||
| `defaults.sample_params.guidance.txt_cfg` | `number` |
|
||||
| `defaults.sample_params.guidance.img_cfg` | `number \| null` |
|
||||
| `defaults.sample_params.guidance.distilled_guidance` | `number` |
|
||||
| `defaults.sample_params.guidance.slg` | `object` |
|
||||
| `defaults.sample_params.guidance.slg.layers` | `array<integer>` |
|
||||
| `defaults.sample_params.guidance.slg.layer_start` | `number` |
|
||||
| `defaults.sample_params.guidance.slg.layer_end` | `number` |
|
||||
| `defaults.sample_params.guidance.slg.scale` | `number` |
|
||||
| `defaults.vae_tiling_params` | `object` |
|
||||
| `defaults.vae_tiling_params.enabled` | `boolean` |
|
||||
| `defaults.vae_tiling_params.tile_size_x` | `integer` |
|
||||
| `defaults.vae_tiling_params.tile_size_y` | `integer` |
|
||||
| `defaults.vae_tiling_params.target_overlap` | `number` |
|
||||
| `defaults.vae_tiling_params.rel_size_x` | `number` |
|
||||
| `defaults.vae_tiling_params.rel_size_y` | `number` |
|
||||
| `defaults.cache_mode` | `string` |
|
||||
| `defaults.cache_option` | `string` |
|
||||
| `defaults.scm_mask` | `string` |
|
||||
| `defaults.scm_policy_dynamic` | `boolean` |
|
||||
| `defaults.output_format` | `string` |
|
||||
| `defaults.output_compression` | `integer` |
|
||||
| `defaults_by_mode.img_gen` | `object` |
|
||||
| `defaults_by_mode.vid_gen` | `object` |
|
||||
| `output_formats_by_mode.img_gen` | `array<string>` |
|
||||
| `output_formats_by_mode.vid_gen` | `array<string>` |
|
||||
| `features_by_mode.img_gen` | `object` |
|
||||
| `features_by_mode.vid_gen` | `object` |
|
||||
|
||||
Shared nested fields:
|
||||
|
||||
`loras`
|
||||
|
||||
@@ -447,6 +458,14 @@ Nested fields currently returned:
|
||||
| `loras[].name` | `string` |
|
||||
| `loras[].path` | `string` |
|
||||
|
||||
`upscalers`
|
||||
|
||||
| Field | Type | Notes |
|
||||
| --- | --- | --- |
|
||||
| `upscalers[].name` | `string` | Built-in name or model stem; use this value in `hires.upscaler` |
|
||||
|
||||
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.
|
||||
|
||||
`limits`
|
||||
|
||||
| Field | Type |
|
||||
@@ -458,19 +477,110 @@ Nested fields currently returned:
|
||||
| `limits.max_batch_count` | `integer` |
|
||||
| `limits.max_queue_size` | `integer` |
|
||||
|
||||
`features`
|
||||
Shared default fields used by both `img_gen` and `vid_gen`:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `features.init_image` | `boolean` |
|
||||
| `features.mask_image` | `boolean` |
|
||||
| `features.control_image` | `boolean` |
|
||||
| `features.ref_images` | `boolean` |
|
||||
| `features.lora` | `boolean` |
|
||||
| `features.vae_tiling` | `boolean` |
|
||||
| `features.cache` | `boolean` |
|
||||
| `features.cancel_queued` | `boolean` |
|
||||
| `features.cancel_generating` | `boolean` |
|
||||
| `prompt` | `string` |
|
||||
| `negative_prompt` | `string` |
|
||||
| `clip_skip` | `integer` |
|
||||
| `width` | `integer` |
|
||||
| `height` | `integer` |
|
||||
| `strength` | `number` |
|
||||
| `seed` | `integer` |
|
||||
| `sample_params` | `object` |
|
||||
| `sample_params.scheduler` | `string` |
|
||||
| `sample_params.sample_method` | `string` |
|
||||
| `sample_params.sample_steps` | `integer` |
|
||||
| `sample_params.eta` | `number \| null` |
|
||||
| `sample_params.shifted_timestep` | `integer` |
|
||||
| `sample_params.flow_shift` | `number \| null` |
|
||||
| `sample_params.guidance.txt_cfg` | `number` |
|
||||
| `sample_params.guidance.img_cfg` | `number \| null` |
|
||||
| `sample_params.guidance.distilled_guidance` | `number` |
|
||||
| `sample_params.guidance.slg.layers` | `array<integer>` |
|
||||
| `sample_params.guidance.slg.layer_start` | `number` |
|
||||
| `sample_params.guidance.slg.layer_end` | `number` |
|
||||
| `sample_params.guidance.slg.scale` | `number` |
|
||||
| `vae_tiling_params` | `object` |
|
||||
| `vae_tiling_params.enabled` | `boolean` |
|
||||
| `vae_tiling_params.tile_size_x` | `integer` |
|
||||
| `vae_tiling_params.tile_size_y` | `integer` |
|
||||
| `vae_tiling_params.target_overlap` | `number` |
|
||||
| `vae_tiling_params.rel_size_x` | `number` |
|
||||
| `vae_tiling_params.rel_size_y` | `number` |
|
||||
| `cache_mode` | `string` |
|
||||
| `cache_option` | `string` |
|
||||
| `scm_mask` | `string` |
|
||||
| `scm_policy_dynamic` | `boolean` |
|
||||
| `output_format` | `string` |
|
||||
| `output_compression` | `integer` |
|
||||
|
||||
`img_gen`-specific default fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `batch_count` | `integer` |
|
||||
| `auto_resize_ref_image` | `boolean` |
|
||||
| `increase_ref_index` | `boolean` |
|
||||
| `control_strength` | `number` |
|
||||
| `hires` | `object` |
|
||||
| `hires.enabled` | `boolean` |
|
||||
| `hires.upscaler` | `string` |
|
||||
| `hires.scale` | `number` |
|
||||
| `hires.target_width` | `integer` |
|
||||
| `hires.target_height` | `integer` |
|
||||
| `hires.steps` | `integer` |
|
||||
| `hires.denoising_strength` | `number` |
|
||||
| `hires.upscale_tile_size` | `integer` |
|
||||
|
||||
`vid_gen`-specific default fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `video_frames` | `integer` |
|
||||
| `fps` | `integer` |
|
||||
| `moe_boundary` | `number` |
|
||||
| `vace_strength` | `number` |
|
||||
| `high_noise_sample_params` | `object` |
|
||||
| `high_noise_sample_params.scheduler` | `string` |
|
||||
| `high_noise_sample_params.sample_method` | `string` |
|
||||
| `high_noise_sample_params.sample_steps` | `integer` |
|
||||
| `high_noise_sample_params.eta` | `number \| null` |
|
||||
| `high_noise_sample_params.shifted_timestep` | `integer` |
|
||||
| `high_noise_sample_params.flow_shift` | `number \| null` |
|
||||
| `high_noise_sample_params.guidance.txt_cfg` | `number` |
|
||||
| `high_noise_sample_params.guidance.img_cfg` | `number \| null` |
|
||||
| `high_noise_sample_params.guidance.distilled_guidance` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.layers` | `array<integer>` |
|
||||
| `high_noise_sample_params.guidance.slg.layer_start` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.layer_end` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.scale` | `number` |
|
||||
|
||||
Fields returned in `features_by_mode.img_gen`:
|
||||
|
||||
- `init_image`
|
||||
- `mask_image`
|
||||
- `control_image`
|
||||
- `ref_images`
|
||||
- `lora`
|
||||
- `vae_tiling`
|
||||
- `hires`
|
||||
- `cache`
|
||||
- `cancel_queued`
|
||||
- `cancel_generating`
|
||||
|
||||
Fields returned in `features_by_mode.vid_gen`:
|
||||
|
||||
- `init_image`
|
||||
- `end_image`
|
||||
- `control_frames`
|
||||
- `high_noise_sample_params`
|
||||
- `lora`
|
||||
- `vae_tiling`
|
||||
- `cache`
|
||||
- `cancel_queued`
|
||||
- `cancel_generating`
|
||||
|
||||
#### `POST /sdcpp/v1/img_gen`
|
||||
|
||||
@@ -521,9 +631,7 @@ Typical status codes:
|
||||
- `409 Conflict`
|
||||
- `410 Gone`
|
||||
|
||||
### Canonical Request Schema
|
||||
|
||||
The `sdcpp API` request body is the canonical native schema.
|
||||
### Request Body
|
||||
|
||||
Example:
|
||||
|
||||
@@ -569,6 +677,16 @@ Example:
|
||||
},
|
||||
|
||||
"lora": [],
|
||||
"hires": {
|
||||
"enabled": false,
|
||||
"upscaler": "Latent",
|
||||
"scale": 2.0,
|
||||
"target_width": 0,
|
||||
"target_height": 0,
|
||||
"steps": 0,
|
||||
"denoising_strength": 0.7,
|
||||
"upscale_tile_size": 128
|
||||
},
|
||||
|
||||
"vae_tiling_params": {
|
||||
"enabled": false,
|
||||
@@ -612,7 +730,7 @@ Channel expectations:
|
||||
If omitted or null:
|
||||
|
||||
- single-image fields map to an empty `sd_image_t`
|
||||
- array fields map to `nullptr + count = 0`
|
||||
- array fields map to an empty C-style array, represented as `pointer = nullptr` and `count = 0`
|
||||
|
||||
### Field Mapping Summary
|
||||
|
||||
@@ -673,12 +791,23 @@ Other native fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `hires` | `object` |
|
||||
| `hires.enabled` | `boolean` |
|
||||
| `hires.upscaler` | `string` |
|
||||
| `hires.scale` | `number` |
|
||||
| `hires.target_width` | `integer` |
|
||||
| `hires.target_height` | `integer` |
|
||||
| `hires.steps` | `integer` |
|
||||
| `hires.denoising_strength` | `number` |
|
||||
| `hires.upscale_tile_size` | `integer` |
|
||||
| `vae_tiling_params` | `object` |
|
||||
| `cache_mode` | `string` |
|
||||
| `cache_option` | `string` |
|
||||
| `scm_mask` | `string` |
|
||||
| `scm_policy_dynamic` | `boolean` |
|
||||
|
||||
For `hires.upscaler`, use `Lanczos`, `Nearest`, `Latent`, `Latent (nearest)`, `Latent (nearest-exact)`, `Latent (antialiased)`, `Latent (bicubic)`, `Latent (bicubic antialiased)`, or an `upscalers[].name` value from `GET /sdcpp/v1/capabilities`. Model-backed upscalers are resolved as `--hires-upscalers-dir / (name + ext)` and must live directly in that directory.
|
||||
|
||||
HTTP-only output fields:
|
||||
|
||||
| Field | Type |
|
||||
@@ -686,11 +815,11 @@ HTTP-only output fields:
|
||||
| `output_format` | `string` |
|
||||
| `output_compression` | `integer` |
|
||||
|
||||
### Optional Field Semantics
|
||||
### Optional Field Handling
|
||||
|
||||
Clients should preserve unset semantics for optional sampling fields.
|
||||
Optional sampling fields may be omitted.
|
||||
|
||||
If a user has not explicitly provided one of these fields, the client should omit it instead of injecting a guessed fallback:
|
||||
When omitted, backend defaults apply to these fields:
|
||||
|
||||
- `sample_params.scheduler`
|
||||
- `sample_params.sample_method`
|
||||
@@ -766,29 +895,394 @@ Example cancelled job:
|
||||
}
|
||||
```
|
||||
|
||||
### Validation and Retention
|
||||
### Submission Errors
|
||||
|
||||
Recommended behavior:
|
||||
`POST /sdcpp/v1/img_gen` may return:
|
||||
|
||||
- malformed JSON returns `400`
|
||||
- invalid image payloads return `400`
|
||||
- invalid parameter structure returns `400`
|
||||
- queue full returns `429` or `503`
|
||||
- accepted runtime failures transition the job to `failed`
|
||||
- unsupported in-progress cancellation may return `409`
|
||||
- `202 Accepted` when the job is created
|
||||
- `400 Bad Request` for an empty body, unsupported model mode, invalid JSON, or invalid generation parameters
|
||||
- `429 Too Many Requests` when the job queue is full
|
||||
- `500 Internal Server Error` for unexpected server exceptions during submission
|
||||
|
||||
Recommended retention controls:
|
||||
### `vid_gen`
|
||||
|
||||
- pending job limit
|
||||
- completed job TTL
|
||||
- failed job TTL
|
||||
The following section documents the native async contract for video generation.
|
||||
|
||||
### Future `vid_gen`
|
||||
#### `POST /sdcpp/v1/vid_gen`
|
||||
|
||||
Future `vid_gen` should reuse the same async job model:
|
||||
Submits an async video generation job.
|
||||
|
||||
- `POST /sdcpp/v1/vid_gen`
|
||||
- `GET /sdcpp/v1/jobs/{id}`
|
||||
- `POST /sdcpp/v1/jobs/{id}/cancel`
|
||||
Successful submission returns `202 Accepted`.
|
||||
|
||||
Its request body should mirror `sd_vid_gen_params_t` in the same way that `img_gen` mirrors `sd_img_gen_params_t`.
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "job_01HTXYZVID",
|
||||
"kind": "vid_gen",
|
||||
"status": "queued",
|
||||
"created": 1775401200,
|
||||
"poll_url": "/sdcpp/v1/jobs/job_01HTXYZVID"
|
||||
}
|
||||
```
|
||||
|
||||
Response fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `id` | `string` |
|
||||
| `kind` | `string` |
|
||||
| `status` | `string` |
|
||||
| `created` | `integer` |
|
||||
| `poll_url` | `string` |
|
||||
|
||||
### Request Body
|
||||
|
||||
Compared with `img_gen`, the `vid_gen` request body:
|
||||
|
||||
- `vid_gen` is a single video sequence job, so `batch_count` is not part of the request schema
|
||||
- `ref_images`, `mask_image`, `control_image`, `control_strength`, and `embed_image_metadata` are not part of the request schema
|
||||
- `vid_gen` adds `end_image`, `control_frames`, `high_noise_sample_params`, `video_frames`, `fps`, `moe_boundary`, and `vace_strength`
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"prompt": "a cat walking through a rainy alley",
|
||||
"negative_prompt": "",
|
||||
"clip_skip": -1,
|
||||
"width": 832,
|
||||
"height": 480,
|
||||
"strength": 0.75,
|
||||
"seed": -1,
|
||||
"video_frames": 33,
|
||||
"fps": 16,
|
||||
"moe_boundary": 0.875,
|
||||
"vace_strength": 1.0,
|
||||
|
||||
"init_image": null,
|
||||
"end_image": null,
|
||||
"control_frames": [],
|
||||
|
||||
"sample_params": {
|
||||
"scheduler": "discrete",
|
||||
"sample_method": "euler",
|
||||
"sample_steps": 28,
|
||||
"eta": 1.0,
|
||||
"shifted_timestep": 0,
|
||||
"custom_sigmas": [],
|
||||
"flow_shift": 0.0,
|
||||
"guidance": {
|
||||
"txt_cfg": 7.0,
|
||||
"img_cfg": 7.0,
|
||||
"distilled_guidance": 3.5,
|
||||
"slg": {
|
||||
"layers": [7, 8, 9],
|
||||
"layer_start": 0.01,
|
||||
"layer_end": 0.2,
|
||||
"scale": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"high_noise_sample_params": {
|
||||
"scheduler": "discrete",
|
||||
"sample_method": "euler",
|
||||
"sample_steps": -1,
|
||||
"eta": 1.0,
|
||||
"shifted_timestep": 0,
|
||||
"flow_shift": 0.0,
|
||||
"guidance": {
|
||||
"txt_cfg": 7.0,
|
||||
"img_cfg": 7.0,
|
||||
"distilled_guidance": 3.5,
|
||||
"slg": {
|
||||
"layers": [7, 8, 9],
|
||||
"layer_start": 0.01,
|
||||
"layer_end": 0.2,
|
||||
"scale": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"lora": [],
|
||||
|
||||
"vae_tiling_params": {
|
||||
"enabled": false,
|
||||
"tile_size_x": 0,
|
||||
"tile_size_y": 0,
|
||||
"target_overlap": 0.5,
|
||||
"rel_size_x": 0.0,
|
||||
"rel_size_y": 0.0
|
||||
},
|
||||
|
||||
"cache_mode": "disabled",
|
||||
"cache_option": "",
|
||||
"scm_mask": "",
|
||||
"scm_policy_dynamic": true,
|
||||
|
||||
"output_format": "webm",
|
||||
"output_compression": 100
|
||||
}
|
||||
```
|
||||
|
||||
### LoRA Rules
|
||||
|
||||
- The server only accepts explicit LoRA entries from the `lora` field.
|
||||
- Prompt-embedded `<lora:...>` tags are intentionally unsupported.
|
||||
- `lora[].is_high_noise` controls whether a LoRA applies only to the high-noise stage.
|
||||
|
||||
### Image and Frame Encoding Rules
|
||||
|
||||
Any image field accepts:
|
||||
|
||||
- a raw base64 string, or
|
||||
- a data URL such as `data:image/png;base64,...`
|
||||
|
||||
Channel expectations:
|
||||
|
||||
- `init_image`: 3 channels
|
||||
- `end_image`: 3 channels
|
||||
- `control_frames[]`: 3 channels
|
||||
|
||||
Frame ordering rules:
|
||||
|
||||
- `control_frames[]` order is the conditioning frame order
|
||||
- `control_frames[]` is preserved in request order
|
||||
|
||||
If omitted or null:
|
||||
|
||||
- single-image fields map to an empty `sd_image_t`
|
||||
- array fields map to an empty C-style array, represented as `pointer = nullptr` and `count = 0`
|
||||
|
||||
### Field Mapping Summary
|
||||
|
||||
Top-level scalar fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `prompt` | `string` |
|
||||
| `negative_prompt` | `string` |
|
||||
| `clip_skip` | `integer` |
|
||||
| `width` | `integer` |
|
||||
| `height` | `integer` |
|
||||
| `strength` | `number` |
|
||||
| `seed` | `integer` |
|
||||
| `video_frames` | `integer` |
|
||||
| `fps` | `integer` |
|
||||
| `moe_boundary` | `number` |
|
||||
| `vace_strength` | `number` |
|
||||
|
||||
Image and frame fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `init_image` | `string \| null` |
|
||||
| `end_image` | `string \| null` |
|
||||
| `control_frames` | `array<string>` |
|
||||
|
||||
LoRA fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `lora[].path` | `string` |
|
||||
| `lora[].multiplier` | `number` |
|
||||
| `lora[].is_high_noise` | `boolean` |
|
||||
|
||||
Sampling fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `sample_params.scheduler` | `string` |
|
||||
| `sample_params.sample_method` | `string` |
|
||||
| `sample_params.sample_steps` | `integer` |
|
||||
| `sample_params.eta` | `number` |
|
||||
| `sample_params.shifted_timestep` | `integer` |
|
||||
| `sample_params.custom_sigmas` | `array<number>` |
|
||||
| `sample_params.flow_shift` | `number` |
|
||||
| `sample_params.guidance.txt_cfg` | `number` |
|
||||
| `sample_params.guidance.img_cfg` | `number` |
|
||||
| `sample_params.guidance.distilled_guidance` | `number` |
|
||||
| `sample_params.guidance.slg.layers` | `array<integer>` |
|
||||
| `sample_params.guidance.slg.layer_start` | `number` |
|
||||
| `sample_params.guidance.slg.layer_end` | `number` |
|
||||
| `sample_params.guidance.slg.scale` | `number` |
|
||||
|
||||
High-noise sampling fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `high_noise_sample_params.scheduler` | `string` |
|
||||
| `high_noise_sample_params.sample_method` | `string` |
|
||||
| `high_noise_sample_params.sample_steps` | `integer` |
|
||||
| `high_noise_sample_params.eta` | `number` |
|
||||
| `high_noise_sample_params.shifted_timestep` | `integer` |
|
||||
| `high_noise_sample_params.flow_shift` | `number` |
|
||||
| `high_noise_sample_params.guidance.txt_cfg` | `number` |
|
||||
| `high_noise_sample_params.guidance.img_cfg` | `number` |
|
||||
| `high_noise_sample_params.guidance.distilled_guidance` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.layers` | `array<integer>` |
|
||||
| `high_noise_sample_params.guidance.slg.layer_start` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.layer_end` | `number` |
|
||||
| `high_noise_sample_params.guidance.slg.scale` | `number` |
|
||||
|
||||
Other native fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `vae_tiling_params` | `object` |
|
||||
| `cache_mode` | `string` |
|
||||
| `cache_option` | `string` |
|
||||
| `scm_mask` | `string` |
|
||||
| `scm_policy_dynamic` | `boolean` |
|
||||
|
||||
HTTP-only output fields:
|
||||
|
||||
| Field | Type |
|
||||
| --- | --- |
|
||||
| `output_format` | `string` |
|
||||
| `output_compression` | `integer` |
|
||||
|
||||
For `vid_gen`, `output_format` and `output_compression` control container encoding.
|
||||
`fps` is request metadata for the generated sequence and is echoed in the completed job result.
|
||||
|
||||
Allowed `output_format` values:
|
||||
|
||||
- `webm`
|
||||
- `webp`
|
||||
- `avi`
|
||||
|
||||
Output format behavior:
|
||||
|
||||
- `output_format` defaults to `webm`
|
||||
- `webp` means animated WebP
|
||||
- `avi` means MJPG AVI
|
||||
- `webm` requires the server to be built with WebM support; otherwise the request returns `400`
|
||||
|
||||
### Result Payload
|
||||
|
||||
Completed jobs return one encoded container payload, not a list of per-frame images.
|
||||
|
||||
Result fields:
|
||||
|
||||
- `result.b64_json` contains the whole encoded container file as base64
|
||||
- `result.mime_type` identifies the media type
|
||||
- `result.output_format` echoes the selected container format
|
||||
- `result.fps` echoes the effective playback FPS
|
||||
- `result.frame_count` reports the actual decoded frame count used to build the container
|
||||
|
||||
Expected MIME types:
|
||||
|
||||
| `output_format` | `mime_type` |
|
||||
| --- | --- |
|
||||
| `webm` | `video/webm` |
|
||||
| `webp` | `image/webp` |
|
||||
| `avi` | `video/x-msvideo` |
|
||||
|
||||
### Optional Field Handling
|
||||
|
||||
Optional sampling fields may be omitted.
|
||||
|
||||
When omitted, backend defaults apply to these fields:
|
||||
|
||||
- `sample_params.scheduler`
|
||||
- `sample_params.sample_method`
|
||||
- `sample_params.eta`
|
||||
- `sample_params.flow_shift`
|
||||
- `sample_params.guidance.img_cfg`
|
||||
- `high_noise_sample_params.scheduler`
|
||||
- `high_noise_sample_params.sample_method`
|
||||
- `high_noise_sample_params.eta`
|
||||
- `high_noise_sample_params.flow_shift`
|
||||
- `high_noise_sample_params.guidance.img_cfg`
|
||||
|
||||
`high_noise_sample_params` may also be omitted entirely.
|
||||
|
||||
### Frame Count Semantics
|
||||
|
||||
`video_frames` is the requested target length, but the current core video path internally normalizes the effective frame count to the largest `4n + 1` value that does not exceed the requested count.
|
||||
|
||||
Examples:
|
||||
|
||||
- `video_frames = 33` stays `33`
|
||||
- `video_frames = 34` becomes `33`
|
||||
- `video_frames = 32` becomes `29`
|
||||
|
||||
The completed job payload includes the actual decoded `frame_count`.
|
||||
|
||||
### Completion Result
|
||||
|
||||
Example completed job:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "job_01HTXYZVID",
|
||||
"kind": "vid_gen",
|
||||
"status": "completed",
|
||||
"created": 1775401200,
|
||||
"started": 1775401203,
|
||||
"completed": 1775401215,
|
||||
"queue_position": 0,
|
||||
"result": {
|
||||
"output_format": "webm",
|
||||
"mime_type": "video/webm",
|
||||
"fps": 16,
|
||||
"frame_count": 33,
|
||||
"b64_json": "GkXfo59ChoEBQveBAULygQRC84EIQo..."
|
||||
},
|
||||
"error": null
|
||||
}
|
||||
```
|
||||
|
||||
The response returns the encoded `.webm`, animated `.webp`, or `.avi` container payload directly.
|
||||
|
||||
### Failure Result
|
||||
|
||||
Example failed job:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "job_01HTXYZVID",
|
||||
"kind": "vid_gen",
|
||||
"status": "failed",
|
||||
"created": 1775401200,
|
||||
"started": 1775401203,
|
||||
"completed": 1775401204,
|
||||
"queue_position": 0,
|
||||
"result": null,
|
||||
"error": {
|
||||
"code": "generation_failed",
|
||||
"message": "generate_video returned no results"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Cancelled Result
|
||||
|
||||
Example cancelled job:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "job_01HTXYZVID",
|
||||
"kind": "vid_gen",
|
||||
"status": "cancelled",
|
||||
"created": 1775401200,
|
||||
"started": null,
|
||||
"completed": 1775401202,
|
||||
"queue_position": 0,
|
||||
"result": null,
|
||||
"error": {
|
||||
"code": "cancelled",
|
||||
"message": "job cancelled by client"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Submission Errors
|
||||
|
||||
`POST /sdcpp/v1/vid_gen` may return:
|
||||
|
||||
- `202 Accepted` when the job is created
|
||||
- `400 Bad Request` for an empty body, unsupported model mode, invalid JSON, invalid generation parameters, or an unsupported output format
|
||||
- `429 Too Many Requests` when the job queue is full
|
||||
- `500 Internal Server Error` for unexpected server exceptions during submission
|
||||
@@ -95,8 +95,12 @@ bool cancel_queued_job(AsyncJobManager& manager, AsyncGenerationJob& job) {
|
||||
job.status = AsyncJobStatus::Cancelled;
|
||||
job.completed_at = unix_timestamp_now();
|
||||
job.result_images_b64.clear();
|
||||
job.error_code = "cancelled";
|
||||
job.error_message = "job cancelled by client";
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -122,14 +126,24 @@ json make_async_job_json(const AsyncJobManager& manager, const AsyncGenerationJo
|
||||
}
|
||||
|
||||
if (job.status == AsyncJobStatus::Completed) {
|
||||
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]}});
|
||||
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},
|
||||
};
|
||||
} 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]}});
|
||||
}
|
||||
result["result"] = {
|
||||
{"output_format", job.img_gen.output_format},
|
||||
{"images", images},
|
||||
};
|
||||
}
|
||||
result["result"] = {
|
||||
{"output_format", job.img_gen.output_format},
|
||||
{"images", images},
|
||||
};
|
||||
result["error"] = nullptr;
|
||||
} else if (job.status == AsyncJobStatus::Failed ||
|
||||
job.status == AsyncJobStatus::Cancelled) {
|
||||
@@ -156,16 +170,15 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
sd_img_gen_params_t params = job.img_gen.to_sd_img_gen_params_t();
|
||||
|
||||
SDImageVec results;
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
sd_image_t* raw_results = generate_image(runtime.sd_ctx, ¶ms);
|
||||
num_results = params.batch_count;
|
||||
results.adopt(raw_results, num_results);
|
||||
results.adopt(raw_results, params.batch_count);
|
||||
}
|
||||
|
||||
if (results.empty() || num_results <= 0) {
|
||||
const int num_results = results.count();
|
||||
if (num_results <= 0) {
|
||||
error_message = "generate_image returned no results";
|
||||
return false;
|
||||
}
|
||||
@@ -208,6 +221,47 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
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,
|
||||
std::string& error_message) {
|
||||
sd_vid_gen_params_t params = job.vid_gen.to_sd_vid_gen_params_t();
|
||||
|
||||
SDImageVec results;
|
||||
int num_results = 0;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.sd_ctx_mutex);
|
||||
sd_image_t* raw_results = generate_video(runtime.sd_ctx, ¶ms, &num_results);
|
||||
results.adopt(raw_results, num_results);
|
||||
}
|
||||
|
||||
num_results = results.count();
|
||||
if (num_results <= 0) {
|
||||
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,
|
||||
job.vid_gen.gen_params.fps,
|
||||
job.vid_gen.output_compression);
|
||||
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;
|
||||
output_fps = job.vid_gen.gen_params.fps;
|
||||
return true;
|
||||
}
|
||||
|
||||
void async_job_worker(ServerRuntime& runtime) {
|
||||
AsyncJobManager& manager = *runtime.async_job_manager;
|
||||
|
||||
@@ -240,11 +294,23 @@ void async_job_worker(ServerRuntime& runtime) {
|
||||
}
|
||||
|
||||
std::vector<std::string> output_images;
|
||||
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, 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,
|
||||
error_message);
|
||||
} else {
|
||||
error_message = "unsupported job kind";
|
||||
}
|
||||
@@ -258,8 +324,12 @@ void async_job_worker(ServerRuntime& runtime) {
|
||||
|
||||
job->completed_at = unix_timestamp_now();
|
||||
if (ok) {
|
||||
job->status = AsyncJobStatus::Completed;
|
||||
job->result_images_b64 = std::move(output_images);
|
||||
job->status = AsyncJobStatus::Completed;
|
||||
job->result_images_b64 = std::move(output_images);
|
||||
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 {
|
||||
@@ -267,6 +337,10 @@ void async_job_worker(ServerRuntime& runtime) {
|
||||
job->error_code = "generation_failed";
|
||||
job->error_message = error_message.empty() ? "unknown generation error" : error_message;
|
||||
job->result_images_b64.clear();
|
||||
job->result_media_b64.clear();
|
||||
job->result_media_mime_type.clear();
|
||||
job->result_frame_count = 0;
|
||||
job->result_fps = 0;
|
||||
}
|
||||
|
||||
purge_expired_jobs(manager);
|
||||
|
||||
@@ -36,7 +36,12 @@ struct AsyncGenerationJob {
|
||||
int64_t started_at = 0;
|
||||
int64_t completed_at = 0;
|
||||
ImgGenJobRequest img_gen;
|
||||
VidGenJobRequest vid_gen;
|
||||
std::vector<std::string> result_images_b64;
|
||||
std::string result_media_b64;
|
||||
std::string result_media_mime_type;
|
||||
int result_frame_count = 0;
|
||||
int result_fps = 0;
|
||||
std::string error_code;
|
||||
std::string error_message;
|
||||
};
|
||||
@@ -63,4 +68,11 @@ bool execute_img_gen_job(ServerRuntime& runtime,
|
||||
AsyncGenerationJob& job,
|
||||
std::vector<std::string>& output_images,
|
||||
std::string& error_message);
|
||||
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,
|
||||
std::string& error_message);
|
||||
void async_job_worker(ServerRuntime& runtime);
|
||||
+1
-1
Submodule examples/server/frontend updated: 740475a7a6...797ccf8082.
@@ -48,7 +48,9 @@ static void parse_args(int argc,
|
||||
|
||||
if (!svr_params.resolve_and_validate() ||
|
||||
!ctx_params.resolve_and_validate(IMG_GEN) ||
|
||||
!default_gen_params.resolve_and_validate(IMG_GEN, ctx_params.lora_model_dir)) {
|
||||
!default_gen_params.resolve_and_validate(IMG_GEN,
|
||||
ctx_params.lora_model_dir,
|
||||
ctx_params.hires_upscalers_dir)) {
|
||||
print_usage(argv[0], options_vec);
|
||||
exit(1);
|
||||
}
|
||||
@@ -95,6 +97,8 @@ int main(int argc, const char** argv) {
|
||||
|
||||
std::vector<LoraEntry> lora_cache;
|
||||
std::mutex lora_mutex;
|
||||
std::vector<UpscalerEntry> upscaler_cache;
|
||||
std::mutex upscaler_mutex;
|
||||
AsyncJobManager async_job_manager;
|
||||
ServerRuntime runtime = {
|
||||
sd_ctx.get(),
|
||||
@@ -104,6 +108,8 @@ int main(int argc, const char** argv) {
|
||||
&default_gen_params,
|
||||
&lora_cache,
|
||||
&lora_mutex,
|
||||
&upscaler_cache,
|
||||
&upscaler_mutex,
|
||||
&async_job_manager,
|
||||
};
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ static bool build_openai_generation_request(const httplib::Request& req,
|
||||
}
|
||||
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", true)) {
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid params";
|
||||
return false;
|
||||
}
|
||||
@@ -212,7 +212,7 @@ static bool build_openai_edit_request(const httplib::Request& req,
|
||||
}
|
||||
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", true)) {
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid params";
|
||||
return false;
|
||||
}
|
||||
@@ -253,6 +253,12 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
|
||||
svr.Post("/v1/images/generations", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_openai_generation_request(req, *runtime, request, error_message)) {
|
||||
@@ -319,6 +325,12 @@ void register_openai_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
|
||||
svr.Post("/v1/images/edits", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
try {
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
ImgGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!build_openai_edit_request(req, *runtime, request, error_message)) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "routes.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <regex>
|
||||
#include <string_view>
|
||||
@@ -35,14 +36,20 @@ static fs::path resolve_display_model_path(const ServerRuntime& runtime) {
|
||||
return {};
|
||||
}
|
||||
|
||||
static std::string lower_ascii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static enum sample_method_t get_sdapi_sample_method(std::string name) {
|
||||
enum sample_method_t result = str_to_sample_method(name.c_str());
|
||||
if (result != SAMPLE_METHOD_COUNT) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::transform(name.begin(), name.end(), name.begin(),
|
||||
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
|
||||
name = lower_ascii(name);
|
||||
static const std::unordered_map<std::string_view, sample_method_t> hardcoded{
|
||||
{"euler a", EULER_A_SAMPLE_METHOD},
|
||||
{"k_euler_a", EULER_A_SAMPLE_METHOD},
|
||||
@@ -114,6 +121,18 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
request.gen_params.width = j.value("width", -1);
|
||||
request.gen_params.height = j.value("height", -1);
|
||||
|
||||
if (!img2img && j.value("enable_hr", false)) {
|
||||
request.gen_params.hires_enabled = true;
|
||||
request.gen_params.hires_scale = j.value("hr_scale", request.gen_params.hires_scale);
|
||||
request.gen_params.hires_width = j.value("hr_resize_x", request.gen_params.hires_width);
|
||||
request.gen_params.hires_height = j.value("hr_resize_y", request.gen_params.hires_height);
|
||||
request.gen_params.hires_steps = j.value("hr_steps", request.gen_params.hires_steps);
|
||||
request.gen_params.hires_denoising_strength =
|
||||
j.value("denoising_strength", request.gen_params.hires_denoising_strength);
|
||||
|
||||
request.gen_params.hires_upscaler = j.value("hr_upscaler", request.gen_params.hires_upscaler);
|
||||
}
|
||||
|
||||
std::string sd_cpp_extra_args_str = extract_and_remove_sd_cpp_extra_args(request.gen_params.prompt);
|
||||
if (!sd_cpp_extra_args_str.empty() && !request.gen_params.from_json_str(sd_cpp_extra_args_str)) {
|
||||
error_message = "invalid sd_cpp_extra_args";
|
||||
@@ -228,7 +247,7 @@ static bool build_sdapi_img_gen_request(const json& j,
|
||||
}
|
||||
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", true)) {
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid params";
|
||||
return false;
|
||||
}
|
||||
@@ -246,6 +265,11 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json j = json::parse(req.body);
|
||||
ImgGenJobRequest request;
|
||||
@@ -342,6 +366,52 @@ void register_sdapi_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/upscalers", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
refresh_upscaler_cache(*runtime);
|
||||
|
||||
auto make_builtin = [](const char* name) {
|
||||
json item;
|
||||
item["name"] = name;
|
||||
item["model_name"] = nullptr;
|
||||
item["model_path"] = nullptr;
|
||||
item["model_url"] = nullptr;
|
||||
item["scale"] = 4;
|
||||
return item;
|
||||
};
|
||||
|
||||
json result = json::array();
|
||||
result.push_back(make_builtin("None"));
|
||||
result.push_back(make_builtin("Lanczos"));
|
||||
result.push_back(make_builtin("Nearest"));
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime->upscaler_mutex);
|
||||
for (const auto& e : *runtime->upscaler_cache) {
|
||||
json item;
|
||||
item["name"] = e.name;
|
||||
item["model_name"] = e.model_name;
|
||||
item["model_path"] = e.fullpath;
|
||||
item["model_url"] = nullptr;
|
||||
item["scale"] = e.scale;
|
||||
result.push_back(item);
|
||||
}
|
||||
}
|
||||
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/latent-upscale-modes", [](const httplib::Request&, httplib::Response& res) {
|
||||
json result = json::array({
|
||||
{{"name", "Latent"}},
|
||||
{{"name", "Latent (nearest)"}},
|
||||
{{"name", "Latent (nearest-exact)"}},
|
||||
{{"name", "Latent (antialiased)"}},
|
||||
{{"name", "Latent (bicubic)"}},
|
||||
{{"name", "Latent (bicubic antialiased)"}},
|
||||
});
|
||||
res.set_content(result.dump(), "application/json");
|
||||
});
|
||||
|
||||
svr.Get("/sdapi/v1/samplers", [runtime](const httplib::Request&, httplib::Response& res) {
|
||||
std::vector<std::string> sampler_names;
|
||||
sampler_names.push_back("default");
|
||||
|
||||
@@ -75,48 +75,33 @@ static fs::path resolve_display_model_path(const ServerRuntime& runtime) {
|
||||
return {};
|
||||
}
|
||||
|
||||
static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
refresh_lora_cache(runtime);
|
||||
|
||||
AsyncJobManager& manager = *runtime.async_job_manager;
|
||||
const auto& defaults = *runtime.default_gen_params;
|
||||
const auto& sample_params = defaults.sample_params;
|
||||
const auto& guidance = sample_params.guidance;
|
||||
const fs::path model_path = resolve_display_model_path(runtime);
|
||||
json samplers = json::array();
|
||||
json schedulers = json::array();
|
||||
json output_formats = json::array({"png", "jpeg"});
|
||||
json available_loras = json::array();
|
||||
|
||||
for (int i = 0; i < SAMPLE_METHOD_COUNT; ++i) {
|
||||
samplers.push_back(sd_sample_method_name((sample_method_t)i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < SCHEDULER_COUNT; ++i) {
|
||||
schedulers.push_back(sd_scheduler_name((scheduler_t)i));
|
||||
}
|
||||
|
||||
#ifdef SD_USE_WEBP
|
||||
output_formats.push_back("webp");
|
||||
#endif
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.lora_mutex);
|
||||
for (const auto& entry : *runtime.lora_cache) {
|
||||
available_loras.push_back({
|
||||
{"name", entry.name},
|
||||
{"path", entry.path},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
json result;
|
||||
result["model"] = {
|
||||
{"name", model_path.filename().u8string()},
|
||||
{"stem", model_path.stem().u8string()},
|
||||
{"path", model_path.u8string()},
|
||||
static json make_sample_params_json(const sd_sample_params_t& sample_params, const std::vector<int>& skip_layers) {
|
||||
const auto& guidance = sample_params.guidance;
|
||||
return {
|
||||
{"scheduler", capability_scheduler_name(sample_params.scheduler)},
|
||||
{"sample_method", capability_sample_method_name(sample_params.sample_method)},
|
||||
{"sample_steps", sample_params.sample_steps},
|
||||
{"eta", finite_number_or_null(sample_params.eta)},
|
||||
{"shifted_timestep", sample_params.shifted_timestep},
|
||||
{"flow_shift", finite_number_or_null(sample_params.flow_shift)},
|
||||
{"guidance",
|
||||
{
|
||||
{"txt_cfg", guidance.txt_cfg},
|
||||
{"img_cfg", finite_number_or_null(guidance.img_cfg)},
|
||||
{"distilled_guidance", guidance.distilled_guidance},
|
||||
{"slg",
|
||||
{
|
||||
{"layers", skip_layers},
|
||||
{"layer_start", guidance.slg.layer_start},
|
||||
{"layer_end", guidance.slg.layer_end},
|
||||
{"scale", guidance.slg.scale},
|
||||
}},
|
||||
}},
|
||||
};
|
||||
result["defaults"] = {
|
||||
}
|
||||
|
||||
static json make_img_gen_defaults_json(const SDGenerationParams& defaults, const std::string& output_format) {
|
||||
return {
|
||||
{"prompt", defaults.prompt},
|
||||
{"negative_prompt", defaults.negative_prompt},
|
||||
{"clip_skip", defaults.clip_skip},
|
||||
@@ -128,59 +113,228 @@ static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
{"auto_resize_ref_image", defaults.auto_resize_ref_image},
|
||||
{"increase_ref_index", defaults.increase_ref_index},
|
||||
{"control_strength", defaults.control_strength},
|
||||
{"sample_params",
|
||||
{"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
|
||||
{"hires",
|
||||
{
|
||||
{"scheduler", capability_scheduler_name(sample_params.scheduler)},
|
||||
{"sample_method", capability_sample_method_name(sample_params.sample_method)},
|
||||
{"sample_steps", sample_params.sample_steps},
|
||||
{"eta", finite_number_or_null(sample_params.eta)},
|
||||
{"shifted_timestep", sample_params.shifted_timestep},
|
||||
{"flow_shift", finite_number_or_null(sample_params.flow_shift)},
|
||||
{"guidance",
|
||||
{
|
||||
{"txt_cfg", guidance.txt_cfg},
|
||||
{"img_cfg", finite_number_or_null(guidance.img_cfg)},
|
||||
{"distilled_guidance", guidance.distilled_guidance},
|
||||
{"slg",
|
||||
{
|
||||
{"layers", defaults.skip_layers},
|
||||
{"layer_start", guidance.slg.layer_start},
|
||||
{"layer_end", guidance.slg.layer_end},
|
||||
{"scale", guidance.slg.scale},
|
||||
}},
|
||||
}},
|
||||
{"enabled", defaults.hires_enabled},
|
||||
{"upscaler", defaults.hires_upscaler},
|
||||
{"scale", defaults.hires_scale},
|
||||
{"target_width", defaults.hires_width},
|
||||
{"target_height", defaults.hires_height},
|
||||
{"steps", defaults.hires_steps},
|
||||
{"denoising_strength", defaults.hires_denoising_strength},
|
||||
{"upscale_tile_size", defaults.hires_upscale_tile_size},
|
||||
}},
|
||||
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
|
||||
{"cache_mode", defaults.cache_mode},
|
||||
{"cache_option", defaults.cache_option},
|
||||
{"scm_mask", defaults.scm_mask},
|
||||
{"scm_policy_dynamic", defaults.scm_policy_dynamic},
|
||||
{"output_format", "png"},
|
||||
{"output_format", output_format},
|
||||
{"output_compression", 100},
|
||||
};
|
||||
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},
|
||||
}
|
||||
|
||||
static json make_vid_gen_defaults_json(const SDGenerationParams& defaults, const std::string& output_format) {
|
||||
return {
|
||||
{"prompt", defaults.prompt},
|
||||
{"negative_prompt", defaults.negative_prompt},
|
||||
{"clip_skip", defaults.clip_skip},
|
||||
{"width", defaults.width > 0 ? defaults.width : 512},
|
||||
{"height", defaults.height > 0 ? defaults.height : 512},
|
||||
{"strength", defaults.strength},
|
||||
{"seed", defaults.seed},
|
||||
{"video_frames", defaults.video_frames},
|
||||
{"fps", defaults.fps},
|
||||
{"moe_boundary", defaults.moe_boundary},
|
||||
{"vace_strength", defaults.vace_strength},
|
||||
{"sample_params", make_sample_params_json(defaults.sample_params, defaults.skip_layers)},
|
||||
{"high_noise_sample_params", make_sample_params_json(defaults.high_noise_sample_params, defaults.high_noise_skip_layers)},
|
||||
{"vae_tiling_params", make_vae_tiling_json(defaults.vae_tiling_params)},
|
||||
{"cache_mode", defaults.cache_mode},
|
||||
{"cache_option", defaults.cache_option},
|
||||
{"scm_mask", defaults.scm_mask},
|
||||
{"scm_policy_dynamic", defaults.scm_policy_dynamic},
|
||||
{"output_format", output_format},
|
||||
{"output_compression", 100},
|
||||
};
|
||||
result["samplers"] = samplers;
|
||||
result["schedulers"] = schedulers;
|
||||
result["output_formats"] = output_formats;
|
||||
result["features"] = {
|
||||
{"init_image", true},
|
||||
{"mask_image", true},
|
||||
{"control_image", true},
|
||||
{"ref_images", true},
|
||||
{"lora", true},
|
||||
{"vae_tiling", true},
|
||||
{"cache", true},
|
||||
}
|
||||
|
||||
static json make_img_gen_features_json() {
|
||||
return {
|
||||
{"init_image", true},
|
||||
{"mask_image", true},
|
||||
{"control_image", true},
|
||||
{"ref_images", true},
|
||||
{"lora", true},
|
||||
{"vae_tiling", true},
|
||||
{"hires", true},
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_vid_gen_features_json() {
|
||||
return {
|
||||
{"init_image", true},
|
||||
{"end_image", true},
|
||||
{"control_frames", true},
|
||||
{"high_noise_sample_params", true},
|
||||
{"lora", true},
|
||||
{"vae_tiling", true},
|
||||
{"cache", true},
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
}
|
||||
|
||||
static json make_capabilities_json(ServerRuntime& runtime) {
|
||||
refresh_lora_cache(runtime);
|
||||
refresh_upscaler_cache(runtime);
|
||||
|
||||
AsyncJobManager& manager = *runtime.async_job_manager;
|
||||
const auto& defaults = *runtime.default_gen_params;
|
||||
const fs::path model_path = resolve_display_model_path(runtime);
|
||||
const bool supports_img = runtime_supports_generation_mode(runtime, IMG_GEN);
|
||||
const bool supports_vid = runtime_supports_generation_mode(runtime, VID_GEN);
|
||||
json samplers = json::array();
|
||||
json schedulers = json::array();
|
||||
json image_output_formats = supported_img_output_formats();
|
||||
json video_output_formats = supported_vid_output_formats();
|
||||
json available_loras = json::array();
|
||||
json available_upscalers = json::array();
|
||||
json supported_modes = json::array();
|
||||
|
||||
for (int i = 0; i < SAMPLE_METHOD_COUNT; ++i) {
|
||||
samplers.push_back(sd_sample_method_name((sample_method_t)i));
|
||||
}
|
||||
|
||||
for (int i = 0; i < SCHEDULER_COUNT; ++i) {
|
||||
schedulers.push_back(sd_scheduler_name((scheduler_t)i));
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.lora_mutex);
|
||||
for (const auto& entry : *runtime.lora_cache) {
|
||||
available_loras.push_back({
|
||||
{"name", entry.name},
|
||||
{"path", entry.path},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
available_upscalers.push_back({
|
||||
{"name", "None"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Lanczos"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Nearest"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (nearest)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (nearest-exact)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (antialiased)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (bicubic)"},
|
||||
});
|
||||
available_upscalers.push_back({
|
||||
{"name", "Latent (bicubic antialiased)"},
|
||||
});
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*runtime.upscaler_mutex);
|
||||
for (const auto& entry : *runtime.upscaler_cache) {
|
||||
available_upscalers.push_back({
|
||||
{"name", entry.name},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (supports_img) {
|
||||
supported_modes.push_back("img_gen");
|
||||
}
|
||||
if (supports_vid) {
|
||||
supported_modes.push_back("vid_gen");
|
||||
}
|
||||
|
||||
std::string default_img_output_format = "png";
|
||||
std::string default_vid_output_format = "avi";
|
||||
if (!image_output_formats.empty()) {
|
||||
default_img_output_format = image_output_formats[0].get<std::string>();
|
||||
}
|
||||
if (!video_output_formats.empty()) {
|
||||
default_vid_output_format = video_output_formats[0].get<std::string>();
|
||||
}
|
||||
|
||||
json defaults_by_mode = json::object();
|
||||
json output_formats_by_mode = json::object();
|
||||
json features_by_mode = json::object();
|
||||
if (supports_img) {
|
||||
defaults_by_mode["img_gen"] = make_img_gen_defaults_json(defaults, default_img_output_format);
|
||||
output_formats_by_mode["img_gen"] = image_output_formats;
|
||||
features_by_mode["img_gen"] = make_img_gen_features_json();
|
||||
}
|
||||
if (supports_vid) {
|
||||
defaults_by_mode["vid_gen"] = make_vid_gen_defaults_json(defaults, default_vid_output_format);
|
||||
output_formats_by_mode["vid_gen"] = video_output_formats;
|
||||
features_by_mode["vid_gen"] = make_vid_gen_features_json();
|
||||
}
|
||||
|
||||
json top_level_defaults = json::object();
|
||||
json top_level_output_formats = json::array();
|
||||
json top_level_features = {
|
||||
{"cancel_queued", true},
|
||||
{"cancel_generating", false},
|
||||
};
|
||||
result["loras"] = available_loras;
|
||||
std::string current_mode = "";
|
||||
if (supports_img) {
|
||||
current_mode = "img_gen";
|
||||
top_level_defaults = defaults_by_mode["img_gen"];
|
||||
top_level_output_formats = output_formats_by_mode["img_gen"];
|
||||
top_level_features = features_by_mode["img_gen"];
|
||||
} else if (supports_vid) {
|
||||
current_mode = "vid_gen";
|
||||
top_level_defaults = defaults_by_mode["vid_gen"];
|
||||
top_level_output_formats = output_formats_by_mode["vid_gen"];
|
||||
top_level_features = features_by_mode["vid_gen"];
|
||||
}
|
||||
|
||||
json result;
|
||||
result["model"] = {
|
||||
{"name", model_path.filename().u8string()},
|
||||
{"stem", model_path.stem().u8string()},
|
||||
{"path", model_path.u8string()},
|
||||
};
|
||||
result["current_mode"] = current_mode;
|
||||
result["supported_modes"] = supported_modes;
|
||||
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},
|
||||
};
|
||||
result["samplers"] = samplers;
|
||||
result["schedulers"] = schedulers;
|
||||
result["output_formats"] = top_level_output_formats;
|
||||
result["output_formats_by_mode"] = output_formats_by_mode;
|
||||
result["features"] = top_level_features;
|
||||
result["features_by_mode"] = features_by_mode;
|
||||
result["loras"] = available_loras;
|
||||
result["upscalers"] = available_upscalers;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -204,7 +358,34 @@ static bool parse_img_gen_request(const json& body,
|
||||
return false;
|
||||
}
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", true)) {
|
||||
if (!request.gen_params.resolve_and_validate(IMG_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid generation parameters";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_vid_gen_request(const json& body,
|
||||
ServerRuntime& runtime,
|
||||
VidGenJobRequest& request,
|
||||
std::string& error_message) {
|
||||
request.gen_params = *runtime.default_gen_params;
|
||||
|
||||
refresh_lora_cache(runtime);
|
||||
if (!request.gen_params.from_json_str(body.dump(), [&](const std::string& path) {
|
||||
return get_lora_full_path(runtime, path);
|
||||
})) {
|
||||
error_message = "invalid generation parameters";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string output_format = body.value("output_format", "webm");
|
||||
int output_compression = body.value("output_compression", 100);
|
||||
if (!assign_output_options(request, output_format, output_compression, error_message)) {
|
||||
return false;
|
||||
}
|
||||
// Intentionally disable prompt-embedded LoRA tag parsing for server APIs.
|
||||
if (!request.gen_params.resolve_and_validate(VID_GEN, "", runtime.ctx_params->hires_upscalers_dir, true)) {
|
||||
error_message = "invalid generation parameters";
|
||||
return false;
|
||||
}
|
||||
@@ -226,6 +407,11 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
res.set_content(R"({"error":"empty body"})", "application/json");
|
||||
return;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, IMG_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(IMG_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json body = json::parse(req.body);
|
||||
ImgGenJobRequest request;
|
||||
@@ -276,9 +462,66 @@ void register_sdcpp_api_endpoints(httplib::Server& svr, ServerRuntime& rt) {
|
||||
}
|
||||
});
|
||||
|
||||
svr.Post("/sdcpp/v1/vid_gen", [](const httplib::Request&, httplib::Response& res) {
|
||||
res.status = 501;
|
||||
res.set_content(R"({"error":"vid_gen is reserved and not implemented yet"})", "application/json");
|
||||
svr.Post("/sdcpp/v1/vid_gen", [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;
|
||||
}
|
||||
if (!runtime_supports_generation_mode(*runtime, VID_GEN)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", unsupported_generation_mode_error(VID_GEN)}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
json body = json::parse(req.body);
|
||||
VidGenJobRequest request;
|
||||
std::string error_message;
|
||||
if (!parse_vid_gen_request(body, *runtime, request, error_message)) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", error_message}}).dump(), "application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
AsyncJobManager& manager = *runtime->async_job_manager;
|
||||
std::shared_ptr<AsyncGenerationJob> job = std::make_shared<AsyncGenerationJob>();
|
||||
job->kind = AsyncJobKind::VidGen;
|
||||
job->status = AsyncJobStatus::Queued;
|
||||
job->created_at = unix_timestamp_now();
|
||||
job->vid_gen = std::move(request);
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(manager.mutex);
|
||||
purge_expired_jobs(manager);
|
||||
if (count_pending_jobs(manager) >= manager.max_pending_jobs) {
|
||||
res.status = 429;
|
||||
res.set_content(R"({"error":"job queue is full"})", "application/json");
|
||||
return;
|
||||
}
|
||||
job->id = make_async_job_id(manager);
|
||||
manager.jobs[job->id] = job;
|
||||
manager.queue.push_back(job->id);
|
||||
}
|
||||
|
||||
manager.cv.notify_one();
|
||||
|
||||
json out;
|
||||
out["id"] = job->id;
|
||||
out["kind"] = async_job_kind_name(job->kind);
|
||||
out["status"] = async_job_status_name(job->status);
|
||||
out["created"] = job->created_at;
|
||||
out["poll_url"] = "/sdcpp/v1/jobs/" + job->id;
|
||||
|
||||
res.status = 202;
|
||||
res.set_content(out.dump(), "application/json");
|
||||
} catch (const json::parse_error& e) {
|
||||
res.status = 400;
|
||||
res.set_content(json({{"error", "invalid json"}, {"message", e.what()}}).dump(), "application/json");
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 500;
|
||||
res.set_content(json({{"error", "server_error"}, {"message", e.what()}}).dump(), "application/json");
|
||||
}
|
||||
});
|
||||
|
||||
svr.Get(R"(/sdcpp/v1/jobs/([A-Za-z0-9_\-]+))", [runtime](const httplib::Request& req, httplib::Response& res) {
|
||||
|
||||
+161
-15
@@ -1,6 +1,7 @@
|
||||
#include "runtime.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
@@ -13,6 +14,18 @@
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
static std::string lower_ascii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
static bool is_supported_model_ext(const fs::path& p) {
|
||||
auto ext = lower_ascii(p.extension().string());
|
||||
return ext == ".gguf" || ext == ".pt" || ext == ".pth" || ext == ".safetensors";
|
||||
}
|
||||
|
||||
static const std::string k_base64_chars =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
@@ -45,6 +58,44 @@ std::string normalize_output_format(std::string output_format) {
|
||||
return output_format;
|
||||
}
|
||||
|
||||
std::vector<std::string> supported_img_output_formats(bool allow_webp) {
|
||||
std::vector<std::string> formats = {"png", "jpeg"};
|
||||
#ifdef SD_USE_WEBP
|
||||
if (allow_webp) {
|
||||
formats.push_back("webp");
|
||||
}
|
||||
#else
|
||||
(void)allow_webp;
|
||||
#endif
|
||||
return formats;
|
||||
}
|
||||
|
||||
std::vector<std::string> supported_vid_output_formats() {
|
||||
std::vector<std::string> formats;
|
||||
#ifdef SD_USE_WEBM
|
||||
formats.push_back("webm");
|
||||
#endif
|
||||
#ifdef SD_USE_WEBP
|
||||
formats.push_back("webp");
|
||||
#endif
|
||||
formats.push_back("avi");
|
||||
return formats;
|
||||
}
|
||||
|
||||
static std::string valid_vid_output_formats_message() {
|
||||
const std::vector<std::string> formats = supported_vid_output_formats();
|
||||
|
||||
std::string message = "invalid output_format, must be one of [";
|
||||
for (size_t i = 0; i < formats.size(); ++i) {
|
||||
if (i > 0) {
|
||||
message += ", ";
|
||||
}
|
||||
message += formats[i];
|
||||
}
|
||||
message += "]";
|
||||
return message;
|
||||
}
|
||||
|
||||
bool assign_output_options(ImgGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
@@ -53,19 +104,88 @@ bool assign_output_options(ImgGenJobRequest& request,
|
||||
request.output_format = normalize_output_format(std::move(output_format));
|
||||
request.output_compression = std::clamp(output_compression, 0, 100);
|
||||
|
||||
const bool valid_format = request.output_format == "png" ||
|
||||
request.output_format == "jpeg" ||
|
||||
(allow_webp && request.output_format == "webp");
|
||||
const std::vector<std::string> valid_formats = supported_img_output_formats(allow_webp);
|
||||
const bool valid_format = std::find(valid_formats.begin(),
|
||||
valid_formats.end(),
|
||||
request.output_format) != valid_formats.end();
|
||||
if (!valid_format) {
|
||||
error_message = allow_webp
|
||||
? "invalid output_format, must be one of [png, jpeg, webp]"
|
||||
: "invalid output_format, must be one of [png, jpeg]";
|
||||
error_message = "invalid output_format, must be one of [";
|
||||
for (size_t i = 0; i < valid_formats.size(); ++i) {
|
||||
if (i > 0) {
|
||||
error_message += ", ";
|
||||
}
|
||||
error_message += valid_formats[i];
|
||||
}
|
||||
error_message += "]";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool assign_output_options(VidGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
std::string& error_message) {
|
||||
request.output_format = normalize_output_format(std::move(output_format));
|
||||
request.output_compression = std::clamp(output_compression, 0, 100);
|
||||
|
||||
if (request.output_format == "avi") {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (request.output_format == "webm") {
|
||||
#ifdef SD_USE_WEBM
|
||||
return true;
|
||||
#else
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
if (request.output_format == "webp") {
|
||||
#ifdef SD_USE_WEBP
|
||||
return true;
|
||||
#else
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
error_message = valid_vid_output_formats_message();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string video_mime_type(const std::string& output_format) {
|
||||
if (output_format == "webm") {
|
||||
return "video/webm";
|
||||
}
|
||||
if (output_format == "webp") {
|
||||
return "image/webp";
|
||||
}
|
||||
return "video/x-msvideo";
|
||||
}
|
||||
|
||||
bool runtime_supports_generation_mode(const ServerRuntime& runtime, SDMode mode) {
|
||||
if (mode == VID_GEN) {
|
||||
return sd_ctx_supports_video_generation(runtime.sd_ctx);
|
||||
}
|
||||
if (mode == IMG_GEN) {
|
||||
return sd_ctx_supports_image_generation(runtime.sd_ctx);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string unsupported_generation_mode_error(SDMode mode) {
|
||||
if (mode == VID_GEN) {
|
||||
return "loaded model does not support vid_gen";
|
||||
}
|
||||
if (mode == IMG_GEN) {
|
||||
return "loaded model does not support img_gen";
|
||||
}
|
||||
return "loaded model does not support requested mode";
|
||||
}
|
||||
|
||||
ArgOptions SDSvrParams::get_options() {
|
||||
ArgOptions options;
|
||||
|
||||
@@ -134,20 +254,12 @@ void refresh_lora_cache(ServerRuntime& rt) {
|
||||
|
||||
fs::path lora_dir = rt.ctx_params->lora_model_dir;
|
||||
if (fs::exists(lora_dir) && fs::is_directory(lora_dir)) {
|
||||
auto is_lora_ext = [](const fs::path& p) {
|
||||
auto ext = p.extension().string();
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
|
||||
return static_cast<char>(std::tolower(c));
|
||||
});
|
||||
return ext == ".gguf" || ext == ".pt" || ext == ".pth" || ext == ".safetensors";
|
||||
};
|
||||
|
||||
for (auto& entry : fs::recursive_directory_iterator(lora_dir)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const fs::path& p = entry.path();
|
||||
if (!is_lora_ext(p)) {
|
||||
if (!is_supported_model_ext(p)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -179,6 +291,40 @@ std::string get_lora_full_path(ServerRuntime& rt, const std::string& path) {
|
||||
return it != rt.lora_cache->end() ? it->fullpath : "";
|
||||
}
|
||||
|
||||
void refresh_upscaler_cache(ServerRuntime& rt) {
|
||||
std::vector<UpscalerEntry> new_cache;
|
||||
|
||||
fs::path upscaler_dir = rt.ctx_params->hires_upscalers_dir;
|
||||
if (fs::exists(upscaler_dir) && fs::is_directory(upscaler_dir)) {
|
||||
for (auto& entry : fs::directory_iterator(upscaler_dir)) {
|
||||
if (!entry.is_regular_file()) {
|
||||
continue;
|
||||
}
|
||||
const fs::path& p = entry.path();
|
||||
if (!is_supported_model_ext(p)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
new_cache.push_back(std::move(upscaler_entry));
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(new_cache.begin(), new_cache.end(), [](const UpscalerEntry& a, const UpscalerEntry& b) {
|
||||
return a.name < b.name;
|
||||
});
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(*rt.upscaler_mutex);
|
||||
*rt.upscaler_cache = std::move(new_cache);
|
||||
}
|
||||
}
|
||||
|
||||
int64_t unix_timestamp_now() {
|
||||
return std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
|
||||
@@ -37,6 +37,14 @@ struct LoraEntry {
|
||||
std::string fullpath;
|
||||
};
|
||||
|
||||
struct UpscalerEntry {
|
||||
std::string name;
|
||||
std::string path;
|
||||
std::string fullpath;
|
||||
std::string model_name;
|
||||
int scale = 4;
|
||||
};
|
||||
|
||||
struct ServerRuntime {
|
||||
sd_ctx_t* sd_ctx;
|
||||
std::mutex* sd_ctx_mutex;
|
||||
@@ -45,6 +53,8 @@ struct ServerRuntime {
|
||||
const SDGenerationParams* default_gen_params;
|
||||
std::vector<LoraEntry>* lora_cache;
|
||||
std::mutex* lora_mutex;
|
||||
std::vector<UpscalerEntry>* upscaler_cache;
|
||||
std::mutex* upscaler_mutex;
|
||||
AsyncJobManager* async_job_manager;
|
||||
};
|
||||
|
||||
@@ -58,13 +68,33 @@ struct ImgGenJobRequest {
|
||||
}
|
||||
};
|
||||
|
||||
struct VidGenJobRequest {
|
||||
SDGenerationParams gen_params;
|
||||
std::string output_format = "webm";
|
||||
int output_compression = 100;
|
||||
|
||||
sd_vid_gen_params_t to_sd_vid_gen_params_t() {
|
||||
return gen_params.to_sd_vid_gen_params_t();
|
||||
}
|
||||
};
|
||||
|
||||
std::string base64_encode(const std::vector<uint8_t>& bytes);
|
||||
std::string normalize_output_format(std::string output_format);
|
||||
std::vector<std::string> supported_img_output_formats(bool allow_webp = true);
|
||||
std::vector<std::string> supported_vid_output_formats();
|
||||
bool assign_output_options(ImgGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
bool allow_webp,
|
||||
std::string& error_message);
|
||||
bool assign_output_options(VidGenJobRequest& request,
|
||||
std::string output_format,
|
||||
int output_compression,
|
||||
std::string& error_message);
|
||||
std::string video_mime_type(const std::string& output_format);
|
||||
bool runtime_supports_generation_mode(const ServerRuntime& runtime, SDMode mode);
|
||||
std::string unsupported_generation_mode_error(SDMode mode);
|
||||
void refresh_lora_cache(ServerRuntime& rt);
|
||||
std::string get_lora_full_path(ServerRuntime& rt, const std::string& path);
|
||||
void refresh_upscaler_cache(ServerRuntime& rt);
|
||||
int64_t unix_timestamp_now();
|
||||
Reference in new issue
Block a user