|
8 | 8 | #include <time.h> |
9 | 9 | #include <string> |
10 | 10 | #include <vector> |
| 11 | +#include <map> |
11 | 12 | #include <filesystem> |
| 13 | +#include <algorithm> |
12 | 14 | #include "gosd.h" |
13 | 15 |
|
14 | 16 | #define STB_IMAGE_IMPLEMENTATION |
|
23 | 25 | #define STB_IMAGE_RESIZE_STATIC |
24 | 26 | #include "stb_image_resize.h" |
25 | 27 | #include <stdlib.h> |
| 28 | +#include <regex> |
26 | 29 |
|
27 | 30 | // Names of the sampler method, same order as enum sample_method in stable-diffusion.h |
28 | 31 | const char* sample_method_str[] = { |
@@ -133,6 +136,13 @@ static std::vector<sd_embedding_t> embedding_vec; |
133 | 136 | // Storage for embedding strings (needs to persist as long as embedding_vec references them) |
134 | 137 | static std::vector<std::string> embedding_strings; |
135 | 138 |
|
| 139 | +// Storage for LoRAs (needs to persist for the lifetime of generation params) |
| 140 | +static std::vector<sd_lora_t> lora_vec; |
| 141 | +// Storage for LoRA strings (needs to persist as long as lora_vec references them) |
| 142 | +static std::vector<std::string> lora_strings; |
| 143 | +// Storage for lora_dir path |
| 144 | +static std::string lora_dir_path; |
| 145 | + |
136 | 146 | // Build embeddings vector from directory, similar to upstream CLI |
137 | 147 | static void build_embedding_vec(const char* embedding_dir) { |
138 | 148 | embedding_vec.clear(); |
@@ -186,6 +196,229 @@ static void build_embedding_vec(const char* embedding_dir) { |
186 | 196 | fprintf(stderr, "Loaded %zu embeddings from %s\n", embedding_vec.size(), embedding_dir); |
187 | 197 | } |
188 | 198 |
|
| 199 | +// Discover LoRA files in directory and build a map of name -> path |
| 200 | +static std::map<std::string, std::string> discover_lora_files(const char* lora_dir) { |
| 201 | + std::map<std::string, std::string> lora_map; |
| 202 | + |
| 203 | + if (!lora_dir || strlen(lora_dir) == 0) { |
| 204 | + fprintf(stderr, "LoRA directory not specified\n"); |
| 205 | + return lora_map; |
| 206 | + } |
| 207 | + |
| 208 | + if (!std::filesystem::exists(lora_dir) || !std::filesystem::is_directory(lora_dir)) { |
| 209 | + fprintf(stderr, "LoRA directory does not exist or is not a directory: %s\n", lora_dir); |
| 210 | + return lora_map; |
| 211 | + } |
| 212 | + |
| 213 | + static const std::vector<std::string> valid_ext = {".safetensors", ".ckpt", ".pt", ".gguf"}; |
| 214 | + |
| 215 | + fprintf(stderr, "Discovering LoRA files in: %s\n", lora_dir); |
| 216 | + |
| 217 | + for (const auto& entry : std::filesystem::directory_iterator(lora_dir)) { |
| 218 | + if (!entry.is_regular_file()) { |
| 219 | + continue; |
| 220 | + } |
| 221 | + |
| 222 | + auto path = entry.path(); |
| 223 | + std::string ext = path.extension().string(); |
| 224 | + |
| 225 | + bool valid = false; |
| 226 | + for (const auto& e : valid_ext) { |
| 227 | + if (ext == e) { |
| 228 | + valid = true; |
| 229 | + break; |
| 230 | + } |
| 231 | + } |
| 232 | + if (!valid) { |
| 233 | + continue; |
| 234 | + } |
| 235 | + |
| 236 | + std::string name = path.stem().string(); // stem() already removes extension |
| 237 | + std::string full_path = path.string(); |
| 238 | + |
| 239 | + // Store the name (without extension) -> full path mapping |
| 240 | + // This allows users to specify just the name in <lora:name:strength> |
| 241 | + lora_map[name] = full_path; |
| 242 | + |
| 243 | + fprintf(stderr, "Found LoRA file: %s -> %s\n", name.c_str(), full_path.c_str()); |
| 244 | + } |
| 245 | + |
| 246 | + fprintf(stderr, "Discovered %zu LoRA files in %s\n", lora_map.size(), lora_dir); |
| 247 | + return lora_map; |
| 248 | +} |
| 249 | + |
| 250 | +// Helper function to check if a path is absolute (matches upstream) |
| 251 | +static bool is_absolute_path(const std::string& p) { |
| 252 | +#ifdef _WIN32 |
| 253 | + // Windows: C:/path or C:\path |
| 254 | + return p.size() > 1 && std::isalpha(static_cast<unsigned char>(p[0])) && p[1] == ':'; |
| 255 | +#else |
| 256 | + // Unix: /path |
| 257 | + return !p.empty() && p[0] == '/'; |
| 258 | +#endif |
| 259 | +} |
| 260 | + |
| 261 | +// Parse LoRAs from prompt string (e.g., "<lora:name:1.0>" or "<lora:name>") |
| 262 | +// Returns a vector of LoRA info and the cleaned prompt with LoRA tags removed |
| 263 | +// Matches upstream implementation more closely |
| 264 | +static std::pair<std::vector<sd_lora_t>, std::string> parse_loras_from_prompt(const std::string& prompt, const char* lora_dir) { |
| 265 | + std::vector<sd_lora_t> loras; |
| 266 | + std::string cleaned_prompt = prompt; |
| 267 | + |
| 268 | + if (!lora_dir || strlen(lora_dir) == 0) { |
| 269 | + fprintf(stderr, "LoRA directory not set, cannot parse LoRAs from prompt\n"); |
| 270 | + return {loras, cleaned_prompt}; |
| 271 | + } |
| 272 | + |
| 273 | + // Discover LoRA files for name-based lookup |
| 274 | + std::map<std::string, std::string> discovered_lora_map = discover_lora_files(lora_dir); |
| 275 | + |
| 276 | + // Map to accumulate multipliers for the same LoRA (matches upstream) |
| 277 | + std::map<std::string, float> lora_map; |
| 278 | + std::map<std::string, float> high_noise_lora_map; |
| 279 | + |
| 280 | + static const std::regex re(R"(<lora:([^:>]+):([^>]+)>)"); |
| 281 | + static const std::vector<std::string> valid_ext = {".pt", ".safetensors", ".gguf"}; |
| 282 | + std::smatch m; |
| 283 | + |
| 284 | + std::string tmp = prompt; |
| 285 | + |
| 286 | + fprintf(stderr, "Parsing LoRAs from prompt: %s\n", prompt.c_str()); |
| 287 | + |
| 288 | + while (std::regex_search(tmp, m, re)) { |
| 289 | + std::string raw_path = m[1].str(); |
| 290 | + const std::string raw_mul = m[2].str(); |
| 291 | + |
| 292 | + float mul = 0.f; |
| 293 | + try { |
| 294 | + mul = std::stof(raw_mul); |
| 295 | + } catch (...) { |
| 296 | + tmp = m.suffix().str(); |
| 297 | + cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only); |
| 298 | + fprintf(stderr, "Invalid LoRA multiplier '%s', skipping\n", raw_mul.c_str()); |
| 299 | + continue; |
| 300 | + } |
| 301 | + |
| 302 | + bool is_high_noise = false; |
| 303 | + static const std::string prefix = "|high_noise|"; |
| 304 | + if (raw_path.rfind(prefix, 0) == 0) { |
| 305 | + raw_path.erase(0, prefix.size()); |
| 306 | + is_high_noise = true; |
| 307 | + } |
| 308 | + |
| 309 | + std::filesystem::path final_path; |
| 310 | + if (is_absolute_path(raw_path)) { |
| 311 | + final_path = raw_path; |
| 312 | + } else { |
| 313 | + // Try name-based lookup first |
| 314 | + auto it = discovered_lora_map.find(raw_path); |
| 315 | + if (it != discovered_lora_map.end()) { |
| 316 | + final_path = it->second; |
| 317 | + } else { |
| 318 | + // Try case-insensitive lookup |
| 319 | + bool found = false; |
| 320 | + for (const auto& pair : discovered_lora_map) { |
| 321 | + std::string lower_name = raw_path; |
| 322 | + std::string lower_key = pair.first; |
| 323 | + std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); |
| 324 | + std::transform(lower_key.begin(), lower_key.end(), lower_key.begin(), ::tolower); |
| 325 | + if (lower_name == lower_key) { |
| 326 | + final_path = pair.second; |
| 327 | + found = true; |
| 328 | + break; |
| 329 | + } |
| 330 | + } |
| 331 | + if (!found) { |
| 332 | + // Try as relative path in lora_dir |
| 333 | + final_path = std::filesystem::path(lora_dir) / raw_path; |
| 334 | + } |
| 335 | + } |
| 336 | + } |
| 337 | + |
| 338 | + // Try adding extensions if file doesn't exist |
| 339 | + if (!std::filesystem::exists(final_path)) { |
| 340 | + bool found = false; |
| 341 | + for (const auto& ext : valid_ext) { |
| 342 | + std::filesystem::path try_path = final_path; |
| 343 | + try_path += ext; |
| 344 | + if (std::filesystem::exists(try_path)) { |
| 345 | + final_path = try_path; |
| 346 | + found = true; |
| 347 | + break; |
| 348 | + } |
| 349 | + } |
| 350 | + if (!found) { |
| 351 | + fprintf(stderr, "WARNING: LoRA file not found: %s\n", final_path.lexically_normal().string().c_str()); |
| 352 | + tmp = m.suffix().str(); |
| 353 | + cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only); |
| 354 | + continue; |
| 355 | + } |
| 356 | + } |
| 357 | + |
| 358 | + // Normalize path (matches upstream) |
| 359 | + const std::string key = final_path.lexically_normal().string(); |
| 360 | + |
| 361 | + // Accumulate multiplier if same LoRA appears multiple times (matches upstream) |
| 362 | + if (is_high_noise) { |
| 363 | + high_noise_lora_map[key] += mul; |
| 364 | + } else { |
| 365 | + lora_map[key] += mul; |
| 366 | + } |
| 367 | + |
| 368 | + fprintf(stderr, "Parsed LoRA: path='%s', multiplier=%.2f, is_high_noise=%s\n", |
| 369 | + key.c_str(), mul, is_high_noise ? "true" : "false"); |
| 370 | + |
| 371 | + cleaned_prompt = std::regex_replace(cleaned_prompt, re, "", std::regex_constants::format_first_only); |
| 372 | + tmp = m.suffix().str(); |
| 373 | + } |
| 374 | + |
| 375 | + // Build final LoRA vector from accumulated maps (matches upstream) |
| 376 | + // Store all path strings first to ensure they persist |
| 377 | + for (const auto& kv : lora_map) { |
| 378 | + lora_strings.push_back(kv.first); |
| 379 | + } |
| 380 | + for (const auto& kv : high_noise_lora_map) { |
| 381 | + lora_strings.push_back(kv.first); |
| 382 | + } |
| 383 | + |
| 384 | + // Now build the LoRA vector with pointers to the stored strings |
| 385 | + size_t string_idx = 0; |
| 386 | + for (const auto& kv : lora_map) { |
| 387 | + sd_lora_t item; |
| 388 | + item.is_high_noise = false; |
| 389 | + item.path = lora_strings[string_idx].c_str(); |
| 390 | + item.multiplier = kv.second; |
| 391 | + loras.push_back(item); |
| 392 | + string_idx++; |
| 393 | + } |
| 394 | + |
| 395 | + for (const auto& kv : high_noise_lora_map) { |
| 396 | + sd_lora_t item; |
| 397 | + item.is_high_noise = true; |
| 398 | + item.path = lora_strings[string_idx].c_str(); |
| 399 | + item.multiplier = kv.second; |
| 400 | + loras.push_back(item); |
| 401 | + string_idx++; |
| 402 | + } |
| 403 | + |
| 404 | + // Clean up extra spaces |
| 405 | + std::regex space_regex(R"(\s+)"); |
| 406 | + cleaned_prompt = std::regex_replace(cleaned_prompt, space_regex, " "); |
| 407 | + // Trim leading/trailing spaces |
| 408 | + size_t first = cleaned_prompt.find_first_not_of(" \t"); |
| 409 | + if (first != std::string::npos) { |
| 410 | + cleaned_prompt.erase(0, first); |
| 411 | + } |
| 412 | + size_t last = cleaned_prompt.find_last_not_of(" \t"); |
| 413 | + if (last != std::string::npos) { |
| 414 | + cleaned_prompt.erase(last + 1); |
| 415 | + } |
| 416 | + |
| 417 | + fprintf(stderr, "Parsed %zu LoRA(s) from prompt. Cleaned prompt: %s\n", loras.size(), cleaned_prompt.c_str()); |
| 418 | + |
| 419 | + return {loras, cleaned_prompt}; |
| 420 | +} |
| 421 | + |
189 | 422 | // Copied from the upstream CLI |
190 | 423 | static void sd_log_cb(enum sd_log_level_t level, const char* log, void* data) { |
191 | 424 | //SDParams* params = (SDParams*)data; |
@@ -304,11 +537,17 @@ int load_model(const char *model, char *model_path, char* options[], int threads |
304 | 537 | std::filesystem::path lora_path(optval); |
305 | 538 | std::filesystem::path full_lora_path = model_path_str / lora_path; |
306 | 539 | lora_dir = strdup(full_lora_path.string().c_str()); |
307 | | - fprintf(stderr, "Lora dir resolved to: %s\n", lora_dir); |
| 540 | + lora_dir_path = full_lora_path.string(); |
| 541 | + fprintf(stderr, "LoRA dir resolved to: %s\n", lora_dir); |
308 | 542 | } else { |
309 | 543 | lora_dir = strdup(optval); |
| 544 | + lora_dir_path = std::string(optval); |
310 | 545 | fprintf(stderr, "No model path provided, using lora dir as-is: %s\n", lora_dir); |
311 | 546 | } |
| 547 | + // Discover LoRAs immediately when directory is set |
| 548 | + if (lora_dir && strlen(lora_dir) > 0) { |
| 549 | + discover_lora_files(lora_dir); |
| 550 | + } |
312 | 551 | } |
313 | 552 |
|
314 | 553 | // New parsing |
@@ -450,6 +689,14 @@ int load_model(const char *model, char *model_path, char* options[], int threads |
450 | 689 | ctx_params.taesd_path = taesd_path; |
451 | 690 | ctx_params.control_net_path = control_net_path; |
452 | 691 | ctx_params.lora_model_dir = lora_dir; |
| 692 | + if (lora_dir && strlen(lora_dir) > 0) { |
| 693 | + lora_dir_path = std::string(lora_dir); |
| 694 | + fprintf(stderr, "LoRA model directory set to: %s\n", lora_dir); |
| 695 | + // Discover LoRAs at load time for logging |
| 696 | + discover_lora_files(lora_dir); |
| 697 | + } else { |
| 698 | + fprintf(stderr, "WARNING: LoRA model directory not set. LoRAs in prompts will not be loaded.\n"); |
| 699 | + } |
453 | 700 | // Set embeddings array and count |
454 | 701 | ctx_params.embeddings = embedding_vec.empty() ? NULL : embedding_vec.data(); |
455 | 702 | ctx_params.embedding_count = static_cast<uint32_t>(embedding_vec.size()); |
@@ -546,9 +793,63 @@ sd_img_gen_params_t* sd_img_gen_params_new(void) { |
546 | 793 | return params; |
547 | 794 | } |
548 | 795 |
|
| 796 | +// Storage for cleaned prompt strings (needs to persist) |
| 797 | +static std::string cleaned_prompt_storage; |
| 798 | +static std::string cleaned_negative_prompt_storage; |
| 799 | + |
549 | 800 | void sd_img_gen_params_set_prompts(sd_img_gen_params_t *params, const char *prompt, const char *negative_prompt) { |
550 | | - params->prompt = prompt; |
551 | | - params->negative_prompt = negative_prompt; |
| 801 | + // Clear previous LoRA data |
| 802 | + lora_vec.clear(); |
| 803 | + lora_strings.clear(); |
| 804 | + |
| 805 | + // Parse LoRAs from prompt |
| 806 | + std::string prompt_str = prompt ? prompt : ""; |
| 807 | + std::string negative_prompt_str = negative_prompt ? negative_prompt : ""; |
| 808 | + |
| 809 | + // Get lora_dir from ctx_params if available, otherwise use stored path |
| 810 | + const char* lora_dir_to_use = ctx_params.lora_model_dir; |
| 811 | + if (!lora_dir_to_use || strlen(lora_dir_to_use) == 0) { |
| 812 | + lora_dir_to_use = lora_dir_path.empty() ? nullptr : lora_dir_path.c_str(); |
| 813 | + } |
| 814 | + |
| 815 | + auto [loras, cleaned_prompt] = parse_loras_from_prompt(prompt_str, lora_dir_to_use); |
| 816 | + lora_vec = loras; |
| 817 | + cleaned_prompt_storage = cleaned_prompt; |
| 818 | + |
| 819 | + // Also check negative prompt for LoRAs (though this is less common) |
| 820 | + auto [neg_loras, cleaned_negative] = parse_loras_from_prompt(negative_prompt_str, lora_dir_to_use); |
| 821 | + // Merge negative prompt LoRAs (though typically not used) |
| 822 | + if (!neg_loras.empty()) { |
| 823 | + fprintf(stderr, "Note: Found %zu LoRAs in negative prompt (may not be supported)\n", neg_loras.size()); |
| 824 | + } |
| 825 | + cleaned_negative_prompt_storage = cleaned_negative; |
| 826 | + |
| 827 | + // Set the cleaned prompts |
| 828 | + params->prompt = cleaned_prompt_storage.c_str(); |
| 829 | + params->negative_prompt = cleaned_negative_prompt_storage.c_str(); |
| 830 | + |
| 831 | + // Set LoRAs in params |
| 832 | + params->loras = lora_vec.empty() ? nullptr : lora_vec.data(); |
| 833 | + params->lora_count = static_cast<uint32_t>(lora_vec.size()); |
| 834 | + |
| 835 | + fprintf(stderr, "Set prompts with %zu LoRAs. Original prompt: %s\n", lora_vec.size(), prompt ? prompt : "(null)"); |
| 836 | + fprintf(stderr, "Cleaned prompt: %s\n", cleaned_prompt_storage.c_str()); |
| 837 | + |
| 838 | + // Debug: Verify LoRAs are set correctly |
| 839 | + if (params->loras && params->lora_count > 0) { |
| 840 | + fprintf(stderr, "DEBUG: LoRAs set in params structure:\n"); |
| 841 | + for (uint32_t i = 0; i < params->lora_count; i++) { |
| 842 | + fprintf(stderr, " params->loras[%u]: path='%s' (ptr=%p), multiplier=%.2f, is_high_noise=%s\n", |
| 843 | + i, |
| 844 | + params->loras[i].path ? params->loras[i].path : "(null)", |
| 845 | + (void*)params->loras[i].path, |
| 846 | + params->loras[i].multiplier, |
| 847 | + params->loras[i].is_high_noise ? "true" : "false"); |
| 848 | + } |
| 849 | + } else { |
| 850 | + fprintf(stderr, "DEBUG: No LoRAs set in params structure (loras=%p, lora_count=%u)\n", |
| 851 | + (void*)params->loras, params->lora_count); |
| 852 | + } |
552 | 853 | } |
553 | 854 |
|
554 | 855 | void sd_img_gen_params_set_dimensions(sd_img_gen_params_t *params, int width, int height) { |
@@ -740,6 +1041,20 @@ int gen_image(sd_img_gen_params_t *p, int steps, char *dst, float cfg_scale, cha |
740 | 1041 | } |
741 | 1042 | } |
742 | 1043 |
|
| 1044 | + // Log LoRA information |
| 1045 | + if (p->loras && p->lora_count > 0) { |
| 1046 | + fprintf(stderr, "Using %u LoRA(s) in generation:\n", p->lora_count); |
| 1047 | + for (uint32_t i = 0; i < p->lora_count; i++) { |
| 1048 | + fprintf(stderr, " LoRA[%u]: path='%s', multiplier=%.2f, is_high_noise=%s\n", |
| 1049 | + i, |
| 1050 | + p->loras[i].path ? p->loras[i].path : "(null)", |
| 1051 | + p->loras[i].multiplier, |
| 1052 | + p->loras[i].is_high_noise ? "true" : "false"); |
| 1053 | + } |
| 1054 | + } else { |
| 1055 | + fprintf(stderr, "No LoRAs specified for this generation\n"); |
| 1056 | + } |
| 1057 | + |
743 | 1058 | fprintf(stderr, "Generating image with params: \nctx\n---\n%s\ngen\n---\n%s\n", |
744 | 1059 | sd_ctx_params_to_str(&ctx_params), |
745 | 1060 | sd_img_gen_params_to_str(p)); |
@@ -802,3 +1117,4 @@ int unload() { |
802 | 1117 | free_sd_ctx(sd_c); |
803 | 1118 | return 0; |
804 | 1119 | } |
| 1120 | + |
0 commit comments