Commit 7bcd4ce for stable-diffusion.cpp

commit 7bcd4ce8ce69378115c176d46d4f30aed80c005a
Author: Zihao <Louis20120127@gmail.com>
Date:   Sun Nov 8 18:23:11 2026 +1100

    fix: clean LoRA multiplier and prompt spacing in image metadata (#2283)

diff --git a/examples/common/generation_metadata.cpp b/examples/common/generation_metadata.cpp
index 989e914..05205ff 100644
--- a/examples/common/generation_metadata.cpp
+++ b/examples/common/generation_metadata.cpp
@@ -1,5 +1,6 @@
 #include "generation_metadata.h"

+#include <cctype>
 #include <filesystem>
 #include <map>

@@ -70,6 +71,7 @@ std::string generation_info_to_webui(const std::string& text) {
     const auto& sampling = info.at("sample_params");
     std::string result   = info.value("prompt", "");
     if (info.contains("loras")) {
+        bool prompt_trimmed = false;
         for (const auto& lora : info.at("loras")) {
             // WebUI tags cannot restrict a LoRA to the high-noise model.
             if (lora.value("is_high_noise", false))
@@ -83,8 +85,15 @@ std::string generation_info_to_webui(const std::string& text) {
                 name.erase(extension);
             if (name.empty() || name.find_first_of(":<>\r\n") != std::string::npos)
                 continue;
-            if (!result.empty())
-                result += " ";
+            if (!result.empty()) {
+                if (!prompt_trimmed) {
+                    while (!result.empty() && std::isspace(static_cast<unsigned char>(result.back())))
+                        result.pop_back();
+                    prompt_trimmed = true;
+                }
+                if (!result.empty())
+                    result += " ";
+            }
             result += "<lora:" + name + ":" + lora.at("multiplier").dump() + ">";
         }
     }
diff --git a/src/generation_info.cpp b/src/generation_info.cpp
index cf40bc2..0d48291 100644
--- a/src/generation_info.cpp
+++ b/src/generation_info.cpp
@@ -1,6 +1,7 @@
 #include "generation_info.h"

 #include <cmath>
+#include <cstdio>
 #include <cstdlib>
 #include <cstring>

@@ -14,6 +15,21 @@ namespace sd {
         return value ? value : "";
     }

+    static double shortest_f32_as_double(float value) {
+        if (!std::isfinite(value)) {
+            return static_cast<double>(value);
+        }
+        char buf[64];
+        buf[0] = '\0';
+        for (int prec = 1; prec <= 9; ++prec) {
+            std::snprintf(buf, sizeof(buf), "%.*g", prec, static_cast<double>(value));
+            if (std::strtof(buf, nullptr) == value) {
+                break;
+            }
+        }
+        return std::strtod(buf, nullptr);
+    }
+
     static json sampling_info(const sd_sample_params_t& p, bool high_noise = false) {
         const auto& g = p.guidance;
         json info     = {{"sample_steps", p.sample_steps}, {"guidance", {{"txt_cfg", g.txt_cfg}, {"distilled_guidance", g.distilled_guidance}}}};
@@ -117,8 +133,13 @@ namespace sd {
         }
         if (p.lora_count > 0) {
             info["loras"] = json::array();
-            for (uint32_t i = 0; i < p.lora_count; ++i)
-                info["loras"].push_back({{"path", text(p.loras[i].path)}, {"multiplier", p.loras[i].multiplier}, {"is_high_noise", p.loras[i].is_high_noise}});
+            for (uint32_t i = 0; i < p.lora_count; ++i) {
+                // nlohmann::json promotes float to double, so dump the shortest
+                // decimal that round-trips the float32 (e.g. 0.85f stays 0.85
+                // instead of 0.8500000238418579) to keep replay exact.
+                const double multiplier = shortest_f32_as_double(p.loras[i].multiplier);
+                info["loras"].push_back({{"path", text(p.loras[i].path)}, {"multiplier", multiplier}, {"is_high_noise", p.loras[i].is_high_noise}});
+            }
         }
         return info;
     }