feat: return generation metadata with core results (#2206)

This commit is contained in:
leejet authored and GitHub committed 2026-10-25 03:18:03 +08:00
1 parent b29cad540a
commit a85db96aa5
37 files changed
+1528 -758

No files matched your search

+2
View File
@@ -2,8 +2,10 @@ set(TARGET sd-cli)
add_executable(${TARGET}
../common/common.cpp
../common/generation_metadata.cpp
../common/log.cpp
../common/media_io.cpp
../common/media_metadata.cpp
image_metadata.cpp
main.cpp
)
+8 -1
View File
@@ -31,14 +31,21 @@ rule for edge detection. See
[Image preprocessing](../../docs/image_preprocessing.md) for input selectors,
input defaults, downstream model processing, mask alignment, and examples.
Metadata mode inspects PNG/JPEG container metadata without loading any model:
Metadata mode inspects PNG, JPEG, WebP (including animated WebP), AVI, WebM, and Matroska
container metadata without loading any model. Use `--image` for both images and videos:
```bash
./bin/sd-cli -M metadata --image ./output.png
./bin/sd-cli -M metadata --image ./output.jpg --metadata-format json
./bin/sd-cli -M metadata --image ./output.webm
./bin/sd-cli -M metadata --image ./output.avi --metadata-format json
./bin/sd-cli -M metadata --image ./output.png --metadata-raw
./bin/sd-cli -M metadata --image ./output.png --metadata-all
```
For completely black or white images or videos, NaNs, and the `--linear-scale` /
`--attn-scale` workaround, see [Troubleshooting](../../docs/troubleshooting.md).
Generation outputs embed the core [generation record](../../docs/generation_metadata.md).
Images retain the native JSON and an additional WebUI-compatible infotext.
`--disable-image-metadata` omits both from files.
+225 -10
View File
@@ -2,12 +2,14 @@
#include <algorithm>
#include <cctype>
#include <codecvt>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <locale>
#include <map>
#include <set>
#include <sstream>
@@ -391,8 +393,39 @@ namespace {
json& report,
std::string& error);
json parse_exif_user_comment(const uint8_t* data, size_t size) {
if (size >= 8 && memcmp(data, "ASCII\0\0\0", 8) == 0) {
return trim_trailing_nuls(bytes_to_string(data + 8, data + size));
}
if (size < 8 || memcmp(data, "UNICODE\0", 8) != 0) {
return json{{"raw_hex_preview", hex_preview(data, size)}};
}
size_t offset = 8;
// WebUI's piexif writer uses big-endian UTF-16 without a BOM.
bool little_endian = false;
if (size >= 10 && ((data[8] == 0xfe && data[9] == 0xff) || (data[8] == 0xff && data[9] == 0xfe))) {
little_endian = data[8] == 0xff;
offset = 10;
}
if ((size - offset) % 2 != 0) {
return json{{"error", "truncated UTF-16 UserComment"}, {"raw_hex_preview", hex_preview(data, size)}};
}
std::u16string text;
for (; offset < size; offset += 2) {
text.push_back(static_cast<char16_t>(little_endian ? data[offset] | (uint16_t(data[offset + 1]) << 8)
: (uint16_t(data[offset]) << 8) | data[offset + 1]));
}
try {
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
return trim_trailing_nuls(converter.to_bytes(text));
} catch (const std::range_error&) {
return json{{"error", "invalid UTF-16 UserComment"}, {"raw_hex_preview", hex_preview(data, size)}};
}
}
json parse_exif_value(const std::vector<uint8_t>& data,
size_t value_offset,
uint16_t tag,
uint16_t type,
uint32_t count,
bool little_endian,
@@ -466,8 +499,9 @@ namespace {
break;
}
case 7:
value = bytes_to_string(data.data() + value_offset,
data.data() + value_offset + count);
value = tag == 0x9286 ? parse_exif_user_comment(data.data() + value_offset, count)
: json(bytes_to_string(data.data() + value_offset,
data.data() + value_offset + count));
break;
case 9: {
if (count == 1) {
@@ -591,6 +625,7 @@ namespace {
tag_json["value"] = parse_exif_value(data,
value_offset,
tag,
type,
value_count,
little_endian,
@@ -1098,6 +1133,178 @@ namespace {
return true;
}
static bool parse_avi_chunks(const std::vector<uint8_t>& data, size_t offset, size_t end, bool info, bool include_raw, json& result, std::string& error) {
while (offset < end) {
if (end - offset < 8) {
error = "truncated AVI chunk header";
return false;
}
const auto name = bytes_to_string(data.data() + offset, data.data() + offset + 4);
const size_t size = read_u32_le(data, offset + 4);
offset += 8;
if (size > end - offset || (size & 1u) > end - offset - size) {
error = "AVI chunk exceeds its container";
return false;
}
json entry = {{"entry_type", "chunk"}, {"name", name}, {"length", size}, {"metadata_like", info}};
if (name == "LIST") {
if (size < 4) {
error = "truncated AVI LIST type";
return false;
}
const auto type = bytes_to_string(data.data() + offset, data.data() + offset + 4);
entry["data"]["list_type"] = type;
if (type == "INFO" && !info &&
!parse_avi_chunks(data, offset + 4, offset + size, true, include_raw, result, error))
return false;
} else if (info) {
entry["data"]["text"] = trim_trailing_nuls(bytes_to_string(data.data() + offset, data.data() + offset + size));
} else {
append_raw_preview(entry, data.data() + offset, size, include_raw);
}
result["entries"].push_back(std::move(entry));
offset += size + (size & 1u);
}
return true;
}
static bool parse_avi(const std::vector<uint8_t>& data, bool include_raw, json& result, std::string& error) {
result = {{"format", "AVI"}, {"entries", json::array()}};
size_t offset = 0;
while (offset < data.size()) {
if (data.size() - offset < 12 || memcmp(data.data() + offset, "RIFF", 4) != 0 ||
memcmp(data.data() + offset + 8, offset == 0 ? "AVI " : "AVIX", 4) != 0) {
error = "invalid AVI RIFF header";
return false;
}
const size_t size = read_u32_le(data, offset + 4);
if (size < 4 || size > data.size() - offset - 8 ||
(size & 1u) > data.size() - offset - 8 - size) {
error = "AVI RIFF size exceeds file size";
return false;
}
if (!parse_avi_chunks(data, offset + 12, offset + 8 + size, false, include_raw, result, error))
return false;
offset += 8 + size + (size & 1u);
}
return true;
}
static bool read_ebml_uint(const std::vector<uint8_t>& data, size_t& offset, size_t end, bool id, uint64_t& value, bool& unknown) {
if (offset == end || data[offset] == 0)
return false;
uint8_t mask = 0x80;
size_t length = 1;
while (!(data[offset] & mask)) {
mask >>= 1;
++length;
}
if (length > (id ? 4u : 8u) || length > end - offset)
return false;
value = data[offset++] & (id ? 0xff : mask - 1);
for (size_t i = 1; i < length; ++i)
value = (value << 8) | data[offset++];
unknown = !id && value == (uint64_t(1) << (7 * length)) - 1;
return true;
}
static bool is_webm_segment_child(uint64_t id) {
return id == 0x114D9B74 || id == 0x1549A966 || id == 0x1654AE6B ||
id == 0x1F43B675 || id == 0x1C53BB6B || id == 0x1941A469 ||
id == 0x1043A770 || id == 0x1254C367;
}
static bool parse_webm_elements(const std::vector<uint8_t>& data, size_t& offset, size_t end, uint64_t parent, bool unknown_cluster, int depth, bool include_raw, json& result, std::string& error) {
if (depth > 16) {
error = "WebM tag nesting exceeds limit";
return false;
}
std::string tag_name;
std::string tag_text;
bool has_text = false;
while (offset < end) {
const auto start = offset;
uint64_t id, size;
bool unknown;
if (!read_ebml_uint(data, offset, end, true, id, unknown)) {
error = "invalid or truncated WebM element ID";
return false;
}
// An unknown-size Cluster ends at the next Segment-level element.
if (unknown_cluster && is_webm_segment_child(id)) {
offset = start;
return true;
}
if (!read_ebml_uint(data, offset, end, false, size, unknown) ||
(!unknown && size > end - offset)) {
error = "invalid or truncated WebM element size";
return false;
}
if (unknown && id != 0x18538067 && id != 0x1F43B675) {
error = "unsupported unknown-size WebM element";
return false;
}
const size_t stop = unknown ? end : offset + static_cast<size_t>(size);
const bool container = (parent == 0 && (id == 0x1A45DFA3 || id == 0x18538067)) ||
(parent == 0x18538067 && (id == 0x1254C367 || (id == 0x1F43B675 && unknown))) ||
(parent == 0x1254C367 && id == 0x7373) ||
((parent == 0x7373 || parent == 0x67C8) && id == 0x67C8);
if (parent == 0 && id == 0x18538067)
result["has_segment"] = true;
if (parent == 0x1A45DFA3 && id == 0x4282) {
const auto type = bytes_to_string(data.data() + offset, data.data() + stop);
if (type != "webm" && type != "matroska") {
error = "EBML document is not WebM or Matroska";
return false;
}
result["doc_type"] = type;
result["format"] = type == "webm" ? "WEBM" : "MATROSKA";
}
if (parent == 0x67C8 && (id == 0x45A3 || id == 0x4487)) {
auto value = bytes_to_string(data.data() + offset, data.data() + stop);
if (id == 0x45A3)
tag_name = std::move(value);
else {
tag_text = std::move(value);
has_text = true;
}
}
if (parent == 0 || parent == 0x18538067) {
std::ostringstream name;
name << "0x" << std::hex << std::uppercase << id;
json entry = {{"entry_type", "element"}, {"name", name.str()}, {"length", stop - offset}, {"metadata_like", false}};
if (unknown)
entry["unknown_size"] = true;
append_raw_preview(entry, data.data() + offset, stop - offset, include_raw);
result["entries"].push_back(std::move(entry));
}
if (container) {
if (!parse_webm_elements(data, offset, stop, id, id == 0x1F43B675 && unknown,
depth + 1, include_raw, result, error))
return false;
} else {
offset = stop;
}
}
if (parent == 0x67C8 && has_text) {
result["entries"].push_back({{"entry_type", "tag"}, {"name", tag_name}, {"metadata_like", true}, {"data", {{"text", tag_text}}}});
}
return true;
}
static bool parse_webm(const std::vector<uint8_t>& data, bool include_raw, json& result, std::string& error) {
result = {{"format", "WEBM"}, {"entries", json::array()}};
size_t offset = 0;
if (!parse_webm_elements(data, offset, data.size(), 0, false, 0, include_raw, result, error))
return false;
if (!result.contains("doc_type") || !result.value("has_segment", false)) {
error = "missing WebM document type or Segment";
return false;
}
result.erase("has_segment");
return true;
}
std::string abbreviate(const std::string& value, bool brief) {
if (!brief || value.size() <= 240) {
return value;
@@ -1116,7 +1323,7 @@ namespace {
return;
}
if (value.is_primitive()) {
out << prefix << key << ": " << value.dump() << "\n";
out << prefix << key << ": " << value.dump(-1, ' ', false, json::error_handler_t::replace) << "\n";
return;
}
if (value.is_array()) {
@@ -1125,7 +1332,7 @@ namespace {
if (item.is_string()) {
out << prefix << " - " << abbreviate(item.get<std::string>(), brief) << "\n";
} else if (item.is_primitive()) {
out << prefix << " - " << item.dump() << "\n";
out << prefix << " - " << item.dump(-1, ' ', false, json::error_handler_t::replace) << "\n";
} else {
out << prefix << " -\n";
for (auto it = item.begin(); it != item.end(); ++it) {
@@ -1158,10 +1365,12 @@ namespace {
}
for (const auto& entry : entries) {
const bool is_chunk = entry.value("entry_type", "") == "chunk";
const std::string name = is_chunk ? entry.value("name", "unknown")
: entry.value("marker", "unknown");
out << (is_chunk ? "Chunk: " : "Segment: ") << name << "\n";
const auto type = entry.value("entry_type", "");
const auto name = entry.value("name", entry.value("marker", "unknown"));
const char* label = type == "chunk" ? "Chunk" : type == "tag" ? "Tag"
: type == "element" ? "Element"
: "Segment";
out << label << ": " << name << "\n";
for (auto it = entry.begin(); it != entry.end(); ++it) {
if (it.key() == "entry_type" || it.key() == "name" || it.key() == "marker" ||
it.key() == "metadata_like") {
@@ -1211,7 +1420,13 @@ namespace {
return parse_webp(data, include_raw, report, error);
}
error = "unsupported image format; only PNG, JPEG, and WebP are supported";
if (data.size() >= 12 && memcmp(data.data(), "RIFF", 4) == 0 &&
memcmp(data.data() + 8, "AVI ", 4) == 0)
return parse_avi(data, include_raw, report, error);
if (data.size() >= 4 && read_u32_be(data, 0) == 0x1A45DFA3)
return parse_webm(data, include_raw, report, error);
error = "unsupported media format; only PNG, JPEG, WebP, AVI, WebM, and Matroska are supported";
return false;
}
@@ -1230,7 +1445,7 @@ bool print_image_metadata(const std::string& image_path,
if (options.output_format == MetadataOutputFormat::JSON) {
visible_report["file"] = image_path;
out << visible_report.dump(2) << "\n";
out << visible_report.dump(2, ' ', false, json::error_handler_t::replace) << "\n";
} else {
print_text_report(image_path, visible_report, options.include_structural, options.brief, out);
}
+49 -60
View File
@@ -69,7 +69,7 @@ struct SDCliParams {
&output_path},
{"",
"--image",
"path to the image to inspect (for metadata mode)",
"path to the image or video to inspect (for metadata mode)",
0,
&image_path},
{"",
@@ -227,7 +227,7 @@ struct SDCliParams {
}
} else {
if (image_path.empty()) {
LOG_ERROR("error: metadata mode needs an image path (--image)");
LOG_ERROR("error: metadata mode needs an image or video path (--image)");
return false;
}
if (metadata_format != "text" && metadata_format != "json") {
@@ -441,10 +441,11 @@ static fs::path get_video_audio_sidecar_path(const SDCliParams& cli_params) {
bool save_results(const SDCliParams& cli_params,
const SDContextParams& ctx_params,
const SDGenerationParams& gen_params,
sd_image_t* results,
SDImageVec& results,
int num_results,
const sd_audio_t* generated_audio = nullptr) {
if (results == nullptr || num_results <= 0) {
const sd_audio_t* generated_audio,
const std::string& video_info) {
if (results.empty() || num_results <= 0) {
return false;
}
@@ -487,12 +488,10 @@ bool save_results(const SDCliParams& cli_params,
if (!img.data)
return false;
int images_per_batch = gen_params.batch_count > 0 ? std::max(1, num_results / gen_params.batch_count) : 1;
const int64_t metadata_seed = cli_params.mode == VID_GEN ? gen_params.seed : gen_params.seed + idx / images_per_batch;
std::string params = gen_params.embed_image_metadata
? get_image_params(ctx_params, gen_params, metadata_seed, cli_params.mode)
: "";
const bool ok = write_image_to_file(path.u8string(), img.data, img.width, img.height, img.channel, params, cli_params.compression_quality);
std::string params = gen_params.embed_image_metadata
? results.generation_info(idx)
: "";
const bool ok = write_image_to_file(path.u8string(), img.data, img.width, img.height, img.channel, params, cli_params.compression_quality);
LOG_INFO("save result image %d to '%s' (%s)", idx, path.u8string().c_str(), ok ? "success" : "failure");
return ok;
};
@@ -534,7 +533,7 @@ bool save_results(const SDCliParams& cli_params,
if (ext_lower != ".avi" && ext_lower != ".webp" && ext_lower != ".webm")
ext = ".avi";
std::string params = gen_params.embed_image_metadata
? get_image_params(ctx_params, gen_params, gen_params.seed, cli_params.mode)
? video_info
: "";
fs::path video_path = base_path;
@@ -542,7 +541,7 @@ bool save_results(const SDCliParams& cli_params,
std::string final_ext_lower = ext.u8string();
std::transform(final_ext_lower.begin(), final_ext_lower.end(), final_ext_lower.begin(), ::tolower);
const bool mux_audio = generated_audio != nullptr && (final_ext_lower == ".avi" || final_ext_lower == ".webm");
if (create_video_from_sd_images(video_path.u8string().c_str(), results, num_results, gen_params.fps, cli_params.compression_quality, mux_audio ? generated_audio : nullptr, params) == 0) {
if (create_video_from_sd_images(video_path.u8string().c_str(), results.data(), num_results, gen_params.fps, cli_params.compression_quality, mux_audio ? generated_audio : nullptr, params) == 0) {
LOG_INFO("save result video to '%s'", video_path.u8string().c_str());
if (generated_audio != nullptr && !mux_audio) {
fs::path wav_path = video_path;
@@ -607,30 +606,29 @@ static bool apply_adetailer(sd_ctx_t* sd_ctx,
continue;
}
sd_img_gen_params_t ad_generation_params = img_gen_params;
ad_generation_params.seed = img_gen_params.seed + i;
ad_generation_params.seed = generation_seed(results.generation_info(i), img_gen_params.seed);
if (mode == IMG_GEN) {
ad_generation_params.width = 512;
ad_generation_params.height = 512;
ad_generation_params.strength = 0.4f;
}
sd_image_t* detailed_images = nullptr;
int detailed_count = 0;
sd_image_generation_result_t* detailed = nullptr;
if (!adetail_image(ad_ctx.get(),
sd_ctx,
results[i],
&ad_params,
&ad_generation_params,
&detailed_images,
&detailed_count) ||
detailed_count <= 0 || detailed_images == nullptr || detailed_images[0].data == nullptr) {
free_sd_images(detailed_images, detailed_count);
&detailed) ||
detailed == nullptr || detailed->count <= 0 || detailed->items[0].image.data == nullptr) {
free_sd_image_generation_result(detailed);
LOG_ERROR("ADetailer failed for image %d", i + 1);
return false;
}
results.append_generation_info(i, detailed->items[0].generation_info, ctx_params.lora_model_dir);
free(results[i].data);
results[i] = detailed_images[0];
detailed_images[0] = {0, 0, 0, nullptr};
free_sd_images(detailed_images, detailed_count);
results[i] = detailed->items[0].image;
detailed->items[0].image = {};
free_sd_image_generation_result(detailed);
}
return true;
}
@@ -884,6 +882,7 @@ int main(int argc, const char* argv[]) {
SDImageVec results;
int num_results = 0;
sd_audio_t* generated_audio = nullptr;
std::string video_info;
if (cli_params.mode == UPSCALE) {
num_results = 1;
@@ -896,18 +895,6 @@ int main(int argc, const char* argv[]) {
return 1;
}
if (gen_params.sample_params.sample_method == SAMPLE_METHOD_COUNT) {
gen_params.sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
}
if (gen_params.high_noise_sample_params.sample_method == SAMPLE_METHOD_COUNT) {
gen_params.high_noise_sample_params.sample_method = sd_get_default_sample_method(sd_ctx.get());
}
if (gen_params.sample_params.scheduler == SCHEDULER_COUNT) {
gen_params.sample_params.scheduler = sd_get_default_scheduler(sd_ctx.get(), gen_params.sample_params.sample_method);
}
sd_img_gen_params_t img_gen_params{};
const bool use_img_gen_params = cli_params.mode == IMG_GEN || cli_params.mode == ADETAILER;
if (use_img_gen_params) {
@@ -915,23 +902,24 @@ int main(int argc, const char* argv[]) {
}
if (cli_params.mode == IMG_GEN) {
sd_image_t* generated_images = nullptr;
if (!generate_image(sd_ctx.get(), &img_gen_params, &generated_images, &num_results)) {
generated_images = nullptr;
num_results = 0;
}
results.adopt(generated_images, num_results);
sd_image_generation_result_t* generated = nullptr;
generate_image(sd_ctx.get(), &img_gen_params, &generated);
results.adopt(generated, ctx_params.lora_model_dir);
num_results = results.count();
} else if (cli_params.mode == ADETAILER) {
num_results = 1;
results.push_back(gen_params.init_image.release());
} else if (cli_params.mode == VID_GEN) {
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
sd_image_t* generated_video = nullptr;
if (!generate_video(sd_ctx.get(), &vid_gen_params, &generated_video, &num_results, &generated_audio, &cli_params.preview_fps)) {
generated_video = nullptr;
sd_vid_gen_params_t vid_gen_params = gen_params.to_sd_vid_gen_params_t();
sd_video_generation_result_t* generated = nullptr;
if (generate_video(sd_ctx.get(), &vid_gen_params, &generated)) {
gen_params.fps = cli_params.preview_fps = generated->fps;
generated_audio = generated->audio;
generated->audio = nullptr;
num_results = generated->frame_count;
results.adopt_video(generated, ctx_params.lora_model_dir);
video_info = results.generation_info(0);
}
gen_params.fps = cli_params.preview_fps;
results.adopt(generated_video, num_results);
}
if (!results) {
@@ -971,21 +959,20 @@ int main(int argc, const char* argv[]) {
SDImageOwner current_image(results[i]);
results[i] = {0, 0, 0, nullptr};
for (int u = 0; u < gen_params.upscale_repeats; ++u) {
sd_image_t* upscaled_images = nullptr;
int upscaled_count = 0;
bool upscale_ok = upscale(upscaler_ctx.get(),
current_image.get(),
upscale_factor,
&upscaled_images,
&upscaled_count);
if (!upscale_ok || upscaled_count <= 0 || upscaled_images[0].data == nullptr) {
free_sd_images(upscaled_images, upscaled_count);
sd_image_generation_result_t* upscaled = nullptr;
bool upscale_ok = upscale(upscaler_ctx.get(),
current_image.get(),
upscale_factor,
&upscaled);
if (!upscale_ok || !upscaled || upscaled->count <= 0 || upscaled->items[0].image.data == nullptr) {
free_sd_image_generation_result(upscaled);
LOG_ERROR("upscale failed");
break;
}
sd_image_t upscaled_image = upscaled_images[0];
upscaled_images[0] = {0, 0, 0, nullptr};
free_sd_images(upscaled_images, upscaled_count);
results.append_generation_info(i, upscaled->items[0].generation_info);
sd_image_t upscaled_image = upscaled->items[0].image;
upscaled->items[0].image = {};
free_sd_image_generation_result(upscaled);
current_image.reset(upscaled_image);
}
results[i] = current_image.release(); // Set the final upscaled image as the result
@@ -993,7 +980,9 @@ int main(int argc, const char* argv[]) {
}
}
if (!save_results(cli_params, ctx_params, gen_params, results.data(), num_results, generated_audio)) {
if (!video_info.empty() && results.count() > 0)
video_info = results.generation_info(0);
if (!save_results(cli_params, ctx_params, gen_params, results, num_results, generated_audio, video_info)) {
free_sd_audio(generated_audio);
return 1;
}