voice_audio_multi/app/gguf/main.cpp
Fabio c36510c21e
Some checks failed
Linux Build / Linux cpu (push) Has been cancelled
Linux Build / Linux cpu-clang (push) Has been cancelled
Linux Build / Linux vulkan (push) Has been cancelled
mac-build / macOS CPU (push) Has been cancelled
Nix Build / Nix (cpu) (push) Has been cancelled
Nix Build / Nix (vulkan) (push) Has been cancelled
windows-build / Windows CPU (push) Has been cancelled
Initial import: audio.cpp with Orange Pi documentation
2026-11-04 19:05:22 +01:00

847 lines
43 KiB
C++

#include "engine/framework/assets/tensor_source.h"
#include "engine/framework/assets/weight_adapter.h"
#include "engine/framework/debug/trace.h"
#include "engine/framework/io/filesystem.h"
#include "engine/framework/io/json.h"
#include "engine/framework/runtime/options.h"
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <iostream>
#include <iterator>
#include <optional>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace {
namespace json = engine::io::json;
struct PackageSpecCandidate {
engine::assets::GgufEmbeddedModelSpec spec;
std::string origin;
int priority = 0;
};
const std::unordered_map<std::string, std::string_view> & converter_model_specs() {
static const std::unordered_map<std::string, std::string_view> specs = {
#include "gguf_converter_model_specs.inc"
};
return specs;
}
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;
}
bool is_gguf_path(const std::filesystem::path & path) {
return lower_ascii(path.extension().string()) == ".gguf";
}
// A GGUF this tool wrote carries its conversion namespaces in the tensor names
// ("<namespace>/<tensor>"), so re-converting one — requantising a published
// bf16 package to q8_0, say — can recover them instead of asking the caller to
// spell out namespaces the file already knows.
std::set<std::string> gguf_tensor_namespaces(const std::filesystem::path & path) {
std::set<std::string> namespaces;
const auto source = engine::assets::open_tensor_source(path);
for (const auto & tensor : source->tensors()) {
const auto separator = tensor.name.find('/');
namespaces.insert(separator == std::string::npos ? std::string() : tensor.name.substr(0, separator));
}
return namespaces;
}
std::set<std::string> input_namespaces(const engine::assets::TensorSourceInput & input) {
if (!input.tensor_prefix.empty())
return {input.tensor_prefix};
if (is_gguf_path(input.path)) {
auto namespaces = gguf_tensor_namespaces(input.path);
if (!namespaces.empty())
return namespaces;
}
return {std::string()};
}
bool excluded_sidecar(const std::filesystem::path & path, const std::filesystem::path & output) {
const std::string extension = lower_ascii(path.extension().string());
return extension == ".safetensors" || extension == ".gguf" || extension == ".bin" || extension == ".pt" ||
extension == ".pth" ||
std::filesystem::weakly_canonical(path) == std::filesystem::weakly_canonical(output) ||
std::filesystem::file_size(path) > 64u * 1024u * 1024u;
}
std::set<std::string>
planned_sidecar_destinations(const std::filesystem::path & root, const std::filesystem::path & output,
const std::vector<engine::assets::GgufEmbeddedFile> & explicit_sidecars,
bool embed_sidecars) {
std::set<std::string> destinations;
std::error_code error;
for (std::filesystem::recursive_directory_iterator it(root, error), end; !error && it != end; it.increment(error)) {
if (it->is_directory() && (it->path().filename() == ".cache" || it->path().filename() == ".git")) {
it.disable_recursion_pending();
continue;
}
if (!it->is_regular_file() ||
(embed_sidecars && excluded_sidecar(it->path(), output)) ||
(!embed_sidecars && std::filesystem::weakly_canonical(it->path()) ==
std::filesystem::weakly_canonical(output)))
continue;
destinations.insert(std::filesystem::relative(it->path(), root).lexically_normal().generic_string());
}
if (error)
throw std::runtime_error("failed to enumerate model sidecars: " + error.message());
std::set<std::string> explicit_destinations;
for (const auto & sidecar : explicit_sidecars) {
const auto destination = sidecar.destination.lexically_normal();
if (!engine::io::is_existing_file(sidecar.source_path)) {
throw std::runtime_error("embedded sidecar source does not exist: " + sidecar.source_path.string());
}
if (destination.empty() || destination.is_absolute() || *destination.begin() == "..") {
throw std::runtime_error("invalid embedded sidecar destination: " + sidecar.destination.string());
}
const auto normalized = destination.generic_string();
if (!explicit_destinations.insert(normalized).second) {
throw std::runtime_error("duplicate embedded sidecar destination: " + destination.generic_string());
}
destinations.insert(normalized);
}
return destinations;
}
PackageSpecCandidate parse_package_spec(const std::string & text, std::string origin, int priority) {
const auto root = json::parse(text);
PackageSpecCandidate candidate;
candidate.spec.family = json::require_string(root, "family");
if (candidate.spec.family.empty())
throw std::runtime_error("model package spec family is empty: " + origin);
const auto & sources = root.require("sources").as_array();
if (sources.empty())
throw std::runtime_error("model package spec has no sources: " + origin);
bool has_gguf = false;
for (const auto & source : sources) {
if (json::require_string(source, "format") == "gguf")
has_gguf = true;
}
if (!has_gguf)
throw std::runtime_error("model package spec has no GGUF source: " + origin);
candidate.spec.json = json::stringify(root);
candidate.origin = std::move(origin);
candidate.priority = priority;
return candidate;
}
void add_candidate(std::vector<PackageSpecCandidate> & candidates, PackageSpecCandidate candidate) {
for (auto & existing : candidates) {
if (existing.spec.family == candidate.spec.family && existing.spec.json == candidate.spec.json) {
if (candidate.priority < existing.priority)
existing = std::move(candidate);
return;
}
}
candidates.push_back(std::move(candidate));
}
void add_spec_file(std::vector<PackageSpecCandidate> & candidates, const std::filesystem::path & path, int priority) {
if (!engine::io::is_existing_file(path))
return;
add_candidate(candidates, parse_package_spec(engine::io::read_text_file(path),
std::filesystem::weakly_canonical(path).string(), priority));
}
void add_spec_path(std::vector<PackageSpecCandidate> & candidates, const std::filesystem::path & path,
const std::optional<std::string> & family, int priority) {
if (engine::io::is_existing_file(path)) {
add_spec_file(candidates, path, priority);
return;
}
if (!engine::io::is_existing_directory(path)) {
throw std::runtime_error("model package spec path does not exist: " + path.string());
}
if (family.has_value()) {
const auto file = path / (*family + ".json");
if (!engine::io::is_existing_file(file)) {
throw std::runtime_error("model package spec not found: " + file.string());
}
add_spec_file(candidates, file, priority);
return;
}
for (const auto & entry : std::filesystem::directory_iterator(path)) {
if (entry.is_regular_file() && lower_ascii(entry.path().extension().string()) == ".json") {
try {
add_spec_file(candidates, entry.path(), priority);
} catch (...) {
// A model_specs directory may contain unrelated JSON files. Only valid
// package specs participate in automatic matching.
}
}
}
}
void add_discovered_spec_directories(std::vector<PackageSpecCandidate> & candidates,
const std::filesystem::path & start,
const std::optional<std::string> & family,
int priority) {
auto cursor = std::filesystem::weakly_canonical(start);
while (!cursor.empty()) {
const auto directory = cursor / "model_specs";
if (engine::io::is_existing_directory(directory)) {
if (!family.has_value() || engine::io::is_existing_file(directory / (*family + ".json"))) {
add_spec_path(candidates, directory, family, priority);
}
}
const auto parent = cursor.parent_path();
if (parent == cursor || parent.empty())
break;
cursor = parent;
}
}
void add_converter_catalog(std::vector<PackageSpecCandidate> & candidates,
const std::optional<std::string> & family,
int priority) {
for (const auto & [catalog_family, text] : converter_model_specs()) {
if (family.has_value() && *family != catalog_family)
continue;
add_candidate(candidates,
parse_package_spec(std::string(text), "converter-catalog:" + catalog_family, priority));
}
}
const json::Value * embedded_spec_value(const json::Value & config) {
if (const auto * value = config.find("audiocpp_model_spec"))
return value;
if (const auto * value = config.find("model_spec"))
return value;
if (const auto * audiocpp = config.find("audiocpp"); audiocpp != nullptr && audiocpp->is_object()) {
if (const auto * value = audiocpp->find("model_spec"))
return value;
if (const auto * value = audiocpp->find("package_spec"))
return value;
}
return nullptr;
}
std::optional<std::string> infer_family_from_config(const json::Value & config) {
if (const auto * value = config.find("audiocpp_family"); value != nullptr && value->is_string()) {
return value->as_string();
}
if (const auto * audiocpp = config.find("audiocpp"); audiocpp != nullptr && audiocpp->is_object()) {
if (const auto * value = audiocpp->find("family"); value != nullptr && value->is_string()) {
return value->as_string();
}
}
const auto * model_type_value = config.find("model_type");
if (model_type_value == nullptr || !model_type_value->is_string())
return std::nullopt;
const std::string model_type = model_type_value->as_string();
if (model_type == "qwen3_asr") {
const auto * thinker = config.find("thinker_config");
if (thinker != nullptr && thinker->is_object()) {
const auto * thinker_type = thinker->find("model_type");
if (thinker_type != nullptr && thinker_type->is_string() &&
thinker_type->as_string() == "qwen3_forced_aligner") {
return "qwen3_forced_aligner";
}
}
return "qwen3_asr";
}
if (model_type == "nemotron3_5_asr")
return "nemotron_asr";
if (model_type == "vibevoice")
return "vibevoice_asr";
if (model_type == "higgs_audio_3")
return "higgs_audio_stt";
if (model_type == "cohere_asr")
return "hviske_asr";
if (model_type == "qwen3_tts")
return "qwen3_tts";
return std::nullopt;
}
std::optional<std::string> add_model_config_spec(std::vector<PackageSpecCandidate> & candidates,
const std::filesystem::path & model_root) {
const auto config_path = model_root / "config.json";
if (!engine::io::is_existing_file(config_path))
return std::nullopt;
const auto config = json::parse_file(config_path);
if (const auto * value = embedded_spec_value(config)) {
if (value->is_object()) {
add_candidate(candidates,
parse_package_spec(json::stringify(*value), config_path.string() + "#model_spec", 1));
} else if (value->is_string()) {
const std::string stored = value->as_string();
const size_t first_content = stored.find_first_not_of(" \t\r\n");
if (first_content == std::string::npos) {
throw std::runtime_error("config.json model_spec string is empty");
}
if (stored[first_content] == '{') {
add_candidate(candidates, parse_package_spec(stored, config_path.string() + "#model_spec", 1));
} else {
const auto stored_path = model_root / stored;
if (!engine::io::is_existing_file(stored_path)) {
throw std::runtime_error("config.json model_spec file does not exist: " + stored_path.string());
}
add_spec_file(candidates, stored_path, 1);
}
} else {
throw std::runtime_error("config.json model_spec must be an object, JSON "
"string, or relative path");
}
}
return infer_family_from_config(config);
}
const json::Value & require_gguf_source(const json::Value & spec) {
for (const auto & source : spec.require("sources").as_array()) {
if (json::require_string(source, "format") == "gguf")
return source;
}
throw std::runtime_error("model package spec has no GGUF source");
}
std::string tensor_prefix(const json::Value & value) {
if (value.is_string())
return {};
const auto * prefix = value.find("prefix");
return prefix == nullptr ? std::string() : prefix->as_string();
}
std::optional<std::string> required_destination(const json::Value & source, const std::string & reference) {
const size_t separator = reference.find(':');
if (separator == std::string::npos || separator == 0) {
throw std::runtime_error("invalid package resource reference: " + reference);
}
const std::string root_id = reference.substr(0, separator);
const std::string relative = reference.substr(separator + 1);
const auto & roots = source.require("roots").as_object();
const auto root = roots.find(root_id);
if (root == roots.end())
throw std::runtime_error("unknown package root: " + root_id);
if (root->second.as_string() == "$gguf")
return std::nullopt;
const auto destination = (std::filesystem::path(root->second.as_string()) / relative).lexically_normal();
if (destination.is_absolute() || destination.empty() || *destination.begin() == "..") {
throw std::runtime_error("unsafe GGUF package resource destination: " + destination.string());
}
return destination.generic_string();
}
std::vector<std::string> validate_candidate(const PackageSpecCandidate & candidate,
const std::set<std::string> & actual_prefixes,
const std::set<std::string> & sidecars,
bool reconversion) {
std::vector<std::string> errors;
try {
const auto spec = json::parse(candidate.spec.json);
const auto & source = require_gguf_source(spec);
std::set<std::string> expected_prefixes;
for (const auto & [unused, value] : source.require("tensors").as_object()) {
(void)unused;
expected_prefixes.insert(tensor_prefix(value));
}
// A namespace in `optional_tensors` is one the family's packages may or
// may not ship — an ACE-Step XL DiT against a turbo-only package. It
// cannot be required of every conversion, but a conversion that does
// supply it is building exactly the package the spec describes, so it
// is not unexpected either.
std::set<std::string> optional_prefixes;
if (const auto * optional_tensors = source.find("optional_tensors");
optional_tensors != nullptr && !optional_tensors->is_null()) {
for (const auto & [unused, value] : optional_tensors->as_object()) {
(void)unused;
optional_prefixes.insert(tensor_prefix(value));
}
}
// Requiring every namespace catches a fresh conversion that forgot an
// input. Re-converting a finished package proves nothing of the sort:
// its namespaces are already exactly what that package ships, and a
// package legitimately built with `--exclude-prefix` (an ACE-Step XL
// GGUF carries no turbo or base DiT) would otherwise be impossible to
// re-quantise even though the runtime loads it happily.
if (!reconversion) {
for (const auto & prefix : expected_prefixes) {
if (actual_prefixes.find(prefix) == actual_prefixes.end()) {
errors.push_back("missing tensor namespace '" + (prefix.empty() ? std::string("<root>") : prefix) +
"'");
}
}
}
for (const auto & prefix : actual_prefixes) {
if (expected_prefixes.find(prefix) == expected_prefixes.end() &&
optional_prefixes.find(prefix) == optional_prefixes.end()) {
errors.push_back("unexpected tensor namespace '" + (prefix.empty() ? std::string("<root>") : prefix) +
"'");
}
}
if (const auto * files = source.find("files")) {
for (const auto & [id, value] : files->as_object()) {
const auto destination = required_destination(source, value.as_string());
if (destination.has_value() && sidecars.find(*destination) == sidecars.end()) {
errors.push_back("missing required sidecar '" + id + "' at " + *destination);
}
}
}
} catch (const std::exception & error) {
errors.push_back(error.what());
}
return errors;
}
struct SidecarPlan {
std::filesystem::path root;
// Carried across by name rather than rediscovered by walking `root`. A
// directory walk applies excluded_sidecar(), which drops .bin, .pt, .pth and
// anything over 64 MiB — right for a source tree full of weights, wrong for
// files that were already chosen as a finished package's sidecars. Kokoro's
// voices are voices/*.bin, so a re-encode silently produced a package with
// none of its 54 voices while reporting embedded_sidecars=true.
std::vector<engine::assets::GgufEmbeddedFile> carried;
// The package being re-emitted, if any. The end-of-run check reads its sidecar
// list again from the file rather than trusting `carried`, so a bug in the
// carrying is caught by the check instead of hidden by it.
std::filesystem::path reused_from;
};
// What the conversion embeds, and where it looks for anything else. The small
// files (configs, tokenizers) that belong in the output: `--root` wins, and so
// does an explicit `--sidecar` set. Otherwise a GGUF input's own embedded copies
// are used — for a re-encode they are exactly the files that package ships,
// where the directory the file happens to sit in is only a guess. Everything
// else keeps using that directory, which is what every safetensors conversion
// does.
SidecarPlan plan_sidecars(const std::filesystem::path & requested,
const std::vector<engine::assets::TensorSourceInput> & inputs,
const std::vector<engine::assets::GgufEmbeddedFile> & explicit_sidecars,
bool embed_sidecars) {
if (!requested.empty())
return {std::filesystem::weakly_canonical(requested), {}, {}};
const auto parent = std::filesystem::weakly_canonical(inputs.front().path.parent_path());
if (!embed_sidecars || !explicit_sidecars.empty())
return {parent, {}, {}};
for (const auto & input : inputs) {
if (!is_gguf_path(input.path) || !engine::assets::gguf_has_embedded_sidecars(input.path))
continue;
const auto materialized = engine::assets::materialize_gguf_sidecars(input.path);
// ⚠ The names come from the GGUF, not from a walk of `materialized`. That
// directory is a cache keyed by path+size+mtime and shared between runs, so
// walking it would carry across anything that happened to be sitting in it.
// The file is the authority on what the package contains.
std::vector<engine::assets::GgufEmbeddedFile> carried;
for (const auto & name : engine::assets::gguf_embedded_sidecar_names(input.path)) {
const auto destination = std::filesystem::path(name).lexically_normal();
const auto source = materialized / destination;
if (!engine::io::is_existing_file(source)) {
throw std::runtime_error("sidecar '" + name + "' embedded in " + input.path.string() +
" did not materialise at " + source.string());
}
carried.push_back({source, destination});
}
std::cerr << "note: reusing the " << carried.size() << " sidecars embedded in " << input.path.string()
<< "; pass --root to override\n";
return {materialized, std::move(carried), input.path};
}
return {parent, {}, {}};
}
PackageSpecCandidate select_package_spec(const std::vector<engine::assets::TensorSourceInput> & inputs,
const std::filesystem::path & model_root, const std::filesystem::path & output,
const std::vector<engine::assets::GgufEmbeddedFile> & explicit_sidecars,
const std::optional<std::filesystem::path> & requested_spec,
std::optional<std::string> family,
bool embed_sidecars) {
// Every input is a GGUF this tool wrote for one family: the conversion is a
// re-encode of a finished package rather than an assembly of a new one.
const bool reconversion =
!inputs.empty() && std::all_of(inputs.begin(), inputs.end(),
[](const engine::assets::TensorSourceInput & input) {
return is_gguf_path(input.path) &&
engine::assets::read_gguf_embedded_model_spec(input.path).has_value();
});
std::vector<PackageSpecCandidate> candidates;
if (requested_spec.has_value()) {
add_spec_path(candidates, *requested_spec, family, 0);
} else {
const size_t config_candidate_count = candidates.size();
// A GGUF input states its own family and package spec. Trusting that is
// both more accurate than guessing from the catalog and safer: without
// it, a single-namespace GGUF matches whichever unrelated family's spec
// happens to accept one unnamed namespace, and the conversion is
// silently stamped with that family.
for (const auto & input : inputs) {
if (!is_gguf_path(input.path))
continue;
const auto embedded = engine::assets::read_gguf_embedded_model_spec(input.path);
if (!embedded.has_value())
continue;
add_candidate(candidates,
parse_package_spec(embedded->json, "embedded:" + input.path.string(), 0));
if (!family.has_value())
family = embedded->family;
}
const auto config_family = add_model_config_spec(candidates, model_root);
if (!family.has_value())
family = config_family;
// An explicitly requested family that none of those specs describes still
// falls through to the catalog, the way it did before they existed.
const bool describes_requested_family =
std::any_of(candidates.begin(), candidates.end(), [&family](const PackageSpecCandidate & candidate) {
return !family.has_value() || candidate.spec.family == *family;
});
if (candidates.size() == config_candidate_count || !describes_requested_family) {
if (engine::io::is_existing_file(model_root / "model_spec.json")) {
add_spec_file(candidates, model_root / "model_spec.json", 2);
}
if (engine::io::is_existing_directory(model_root / "model_specs")) {
add_spec_path(candidates, model_root / "model_specs", family, 2);
}
add_discovered_spec_directories(candidates, std::filesystem::current_path(), family, 3);
add_converter_catalog(candidates, family, 4);
}
}
if (candidates.empty()) {
throw std::runtime_error("no model package spec was found; pass --family/--model-spec, add "
"model_spec to config.json, "
"install model_specs/*.json, or use --allow-missing-model-spec for a "
"non-runtime tensor archive");
}
std::set<std::string> prefixes;
for (const auto & input : inputs) {
for (const auto & prefix : input_namespaces(input)) {
if (!prefixes.insert(prefix).second) {
throw std::runtime_error("duplicate tensor namespace in conversion inputs: '" +
(prefix.empty() ? std::string("<root>") : prefix) + "'");
}
}
}
const auto sidecars = planned_sidecar_destinations(model_root, output, explicit_sidecars, embed_sidecars);
std::vector<std::pair<PackageSpecCandidate, std::vector<std::string>>> matches;
std::vector<std::pair<std::string, std::vector<std::string>>> rejected;
std::optional<int> selected_priority;
for (const auto & candidate : candidates) {
if (family.has_value() && candidate.spec.family != *family)
continue;
if (!selected_priority.has_value() || candidate.priority < *selected_priority)
selected_priority = candidate.priority;
}
if (!selected_priority.has_value()) {
throw std::runtime_error("no model package spec was found for family '" +
(family.has_value() ? *family : std::string("<auto>")) + "'");
}
for (const auto & candidate : candidates) {
if ((family.has_value() && candidate.spec.family != *family) || candidate.priority != *selected_priority)
continue;
auto errors = validate_candidate(candidate, prefixes, sidecars, reconversion);
if (errors.empty())
matches.push_back({candidate, {}});
else
rejected.push_back({candidate.spec.family + " (" + candidate.origin + ")", std::move(errors)});
}
if (matches.empty()) {
std::ostringstream message;
message << "no model package spec matches the conversion inputs";
if (family.has_value())
message << " for family '" << *family << "'";
for (const auto & [name, errors] : rejected) {
message << "\n " << name << ':';
for (const auto & error : errors)
message << "\n - " << error;
}
throw std::runtime_error(message.str());
}
if (matches.size() != 1) {
std::ostringstream message;
message << "multiple model package specs match:";
for (const auto & match : matches)
message << ' ' << match.first.spec.family;
message << "; pass --family or --model-spec explicitly";
throw std::runtime_error(message.str());
}
return matches.front().first;
}
void print_usage() {
std::cout << "Usage: audiocpp_gguf --input [namespace=]<weights> [--input "
"namespace=<weights> ...] "
"--output <weights.gguf> --type "
"<orig|f16|bf16|q8_0|q2_k|q3_k|q4_k|q5_k|q6_k|tq2_1> "
"[--family <family>] [--model-spec <json-or-directory>] "
"[--root <model-dir>] [--sidecar <source>=<destination>] "
"[--bnb-nf4-type q8_0] [--exclude-prefix <logical-prefix>] "
"[--keep-type <tensor-prefix>*=<type>] "
"[--fold-weight-norm <tensor-prefix>*] "
"[--weight-adapter <normalized.safetensors>] [--adapter-strength <float>] "
"[--adapter-merge-mode delta_f32|rankwise_f32|rounded_bf16_delta] "
"[--log] "
"[--overwrite] [--no-sidecars] "
"[--allow-missing-model-spec]\n"
<< " audiocpp_gguf --inspect <model.gguf>\n";
}
} // namespace
int main(int argc, char ** argv) {
try {
std::vector<engine::assets::TensorSourceInput> inputs;
std::vector<engine::assets::GgufEmbeddedFile> sidecars;
std::filesystem::path output;
std::filesystem::path inspect_path;
std::filesystem::path sidecar_root;
std::optional<std::filesystem::path> model_spec_path;
std::optional<std::string> family;
std::string type;
engine::assets::GgufConversionOptions conversion_options;
std::filesystem::path adapter_path;
engine::assets::WeightAdapterReadOptions adapter_options;
adapter_options.require_scaling_metadata = true;
auto adapter_mode = engine::assets::WeightMergeMode::DeltaF32;
bool adapter_options_set = false;
bool overwrite = false;
bool embed_sidecars = true;
bool allow_missing_model_spec = false;
for (int i = 1; i < argc; ++i) {
const std::string arg = argv[i];
if (arg == "--help" || arg == "-h") {
print_usage();
return 0;
}
if (arg == "--log") {
engine::debug::configure_logging({true});
continue;
}
if (arg == "--overwrite") {
overwrite = true;
continue;
}
if (arg == "--no-sidecars") {
embed_sidecars = false;
continue;
}
if (arg == "--allow-missing-model-spec") {
allow_missing_model_spec = true;
continue;
}
if ((arg == "--input" || arg == "--output" || arg == "--type" || arg == "--inspect" || arg == "--root" ||
arg == "--sidecar" || arg == "--family" || arg == "--model-spec" || arg == "--model-spec-override" ||
arg == "--bnb-nf4-type" || arg == "--exclude-prefix" || arg == "--keep-type" ||
arg == "--weight-adapter" || arg == "--adapter-strength" || arg == "--adapter-merge-mode" ||
arg == "--fold-weight-norm") &&
i + 1 < argc) {
const std::string value = argv[++i];
if (arg == "--input") {
const auto separator = value.find('=');
if (separator == std::string::npos)
inputs.push_back({value, ""});
else
inputs.push_back({value.substr(separator + 1), value.substr(0, separator)});
} else if (arg == "--output")
output = value;
else if (arg == "--type")
type = value;
else if (arg == "--weight-adapter")
adapter_path = std::filesystem::u8path(value);
else if (arg == "--adapter-strength") {
adapter_options.strength = *engine::runtime::parse_finite_float_option(
{{"adapter_strength", value}}, {"adapter_strength"});
adapter_options_set = true;
} else if (arg == "--adapter-merge-mode") {
if (value == "delta_f32") adapter_mode = engine::assets::WeightMergeMode::DeltaF32;
else if (value == "rankwise_f32") adapter_mode = engine::assets::WeightMergeMode::RankWiseF32;
else if (value == "rounded_bf16_delta") adapter_mode = engine::assets::WeightMergeMode::RoundedBF16Delta;
else throw std::runtime_error("unsupported adapter merge mode: " + value);
adapter_options_set = true;
}
else if (arg == "--bnb-nf4-type")
conversion_options.bnb_nf4_type = engine::assets::parse_tensor_storage_type(value);
else if (arg == "--exclude-prefix")
conversion_options.excluded_tensor_prefixes.push_back(value);
else if (arg == "--fold-weight-norm")
conversion_options.folded_weight_norm_patterns.push_back(value);
else if (arg == "--inspect")
inspect_path = value;
else if (arg == "--root")
sidecar_root = value;
else if (arg == "--family")
family = value;
else if (arg == "--model-spec" || arg == "--model-spec-override") {
model_spec_path = std::filesystem::path(value);
} else if (arg == "--keep-type") {
const auto separator = value.find('=');
if (separator == std::string::npos || separator == 0 || separator + 1 == value.size()) {
throw std::runtime_error("--keep-type requires <tensor-prefix>*=<type>");
}
conversion_options.type_overrides.push_back({
value.substr(0, separator),
engine::assets::parse_tensor_storage_type(value.substr(separator + 1)),
});
} else {
const auto separator = value.find('=');
if (separator == std::string::npos || separator == 0 || separator + 1 == value.size()) {
throw std::runtime_error("--sidecar requires <source>=<destination>");
}
sidecars.push_back({value.substr(0, separator), value.substr(separator + 1)});
}
continue;
}
throw std::runtime_error("unknown or incomplete argument: " + arg);
}
if (!inspect_path.empty()) {
const auto source = engine::assets::open_tensor_source(inspect_path);
const auto tensors = source->tensors();
size_t scalar_count = 0;
std::set<std::string> namespaces;
for (const auto & tensor : tensors) {
if (tensor.shape.empty())
++scalar_count;
const auto separator = tensor.name.find('/');
if (separator != std::string::npos)
namespaces.insert(tensor.name.substr(0, separator));
}
std::cout << "gguf=" << std::filesystem::weakly_canonical(inspect_path).string() << "\n";
std::cout << "tensors=" << tensors.size() << "\n";
std::cout << "rank0_scalars=" << scalar_count << "\n";
// The count, not just the boolean: a package that lost 54 of its 59
// sidecars still reports true, which is how that went unnoticed.
const auto inspect_sidecars = engine::assets::gguf_embedded_sidecar_names(inspect_path);
std::cout << "embedded_sidecars=" << (inspect_sidecars.empty() ? "false" : "true") << "\n";
std::cout << "embedded_sidecar_count=" << inspect_sidecars.size() << "\n";
const auto model_spec = engine::assets::read_gguf_embedded_model_spec(inspect_path);
std::cout << "embedded_model_spec=" << (model_spec.has_value() ? "true" : "false") << "\n";
if (model_spec.has_value())
std::cout << "model_spec_family=" << model_spec->family << "\n";
for (const auto & value : namespaces)
std::cout << "namespace=" << value << "\n";
return 0;
}
if (inputs.empty() || output.empty() || type.empty()) {
print_usage();
return 2;
}
if (!embed_sidecars && !sidecars.empty()) {
throw std::runtime_error("--sidecar cannot be combined with --no-sidecars");
}
for (const auto & input : inputs) {
if (!std::filesystem::is_regular_file(input.path)) {
throw std::runtime_error("input tensor file does not exist: " + input.path.string());
}
}
if (std::filesystem::exists(output)) {
if (!overwrite) {
throw std::runtime_error("output already exists; pass --overwrite to replace it: " + output.string());
}
}
const auto storage_type = engine::assets::parse_tensor_storage_type(type);
const auto sidecar_plan = plan_sidecars(sidecar_root, inputs, sidecars, embed_sidecars);
const auto & resolved_sidecar_root = sidecar_plan.root;
// Empty unless the input was a GGUF whose sidecars are being reused, which
// plan_sidecars only does when the caller passed no --sidecar of its own,
// so these cannot collide with an explicit destination.
sidecars.insert(sidecars.end(), sidecar_plan.carried.begin(), sidecar_plan.carried.end());
std::optional<engine::assets::GgufEmbeddedModelSpec> embedded_model_spec;
if (!allow_missing_model_spec) {
embedded_model_spec =
select_package_spec(inputs, resolved_sidecar_root, output, sidecars, model_spec_path, family,
embed_sidecars).spec;
} else {
try {
embedded_model_spec =
select_package_spec(inputs, resolved_sidecar_root, output, sidecars, model_spec_path, family,
embed_sidecars).spec;
} catch (const std::exception & warning) {
std::cerr << "warning: creating GGUF without model package spec: " << warning.what() << "\n";
}
}
if (conversion_options.bnb_nf4_type.has_value() &&
*conversion_options.bnb_nf4_type != engine::assets::TensorStorageType::Q8_0) {
throw std::runtime_error("--bnb-nf4-type currently supports q8_0");
}
if (adapter_path.empty() && adapter_options_set) {
throw std::runtime_error("adapter strength/mode requires --weight-adapter");
}
if (!adapter_path.empty()) {
auto adapter = engine::assets::read_weight_adapter(adapter_path, adapter_options);
if (adapter.updates.empty() && adapter.replacements.empty()) {
throw std::runtime_error("weight adapter contains no updates or replacements");
}
for (auto & entry : adapter.updates) {
entry.second.merge_mode = adapter_mode;
}
conversion_options.weight_adapter = std::make_shared<engine::assets::WeightAdapter>(std::move(adapter));
conversion_options.weight_adapter_merge_mode = adapter_mode;
}
engine::assets::convert_tensor_sources_to_gguf(inputs, output, storage_type, overwrite, embed_sidecars,
resolved_sidecar_root, sidecars, embedded_model_spec,
conversion_options);
std::cout << "gguf=" << std::filesystem::weakly_canonical(output).string() << "\n";
std::cout << "weight_type=" << type << "\n";
std::cout << "tensor_sources=" << inputs.size() << "\n";
if (conversion_options.bnb_nf4_type.has_value())
std::cout << "bnb_nf4_type=q8_0\n";
for (const auto & prefix : conversion_options.excluded_tensor_prefixes)
std::cout << "excluded_prefix=" << prefix << "\n";
std::cout << "type_overrides=" << conversion_options.type_overrides.size() << "\n";
std::cout << "folded_weight_norm_patterns=" << conversion_options.folded_weight_norm_patterns.size() << "\n";
// ⚠ READ THE FINISHED FILE BACK AND CHECK IT AGAINST WHAT WAS ASKED FOR.
// This tool used to write a package missing 54 of its 59 sidecars, exit 0,
// and report embedded_sidecars=true -- because "true" was read from the
// output but was too coarse to notice, and the model-spec line was read
// from the in-memory intent rather than from the file at all. A claim
// about the output that is not read from the output is not a check.
const auto written_sidecars = engine::assets::gguf_embedded_sidecar_names(output);
// ⚠ The expected set is read from the INPUT package, not from the plan that
// produced the output. An earlier version of this check compared against
// planned_sidecar_destinations, which is built by the same filtered walk that
// caused the bug -- so it agreed with a run that dropped 54 voices. A check
// derived from the thing it is checking cannot fail.
if (embed_sidecars && !sidecar_plan.reused_from.empty()) {
const auto expected = engine::assets::gguf_embedded_sidecar_names(sidecar_plan.reused_from);
const std::set<std::string> expected_set(expected.begin(), expected.end());
const std::set<std::string> written(written_sidecars.begin(), written_sidecars.end());
std::vector<std::string> missing;
std::set_difference(expected_set.begin(), expected_set.end(), written.begin(), written.end(),
std::back_inserter(missing));
if (!missing.empty()) {
std::ostringstream report;
report << "re-encoding " << sidecar_plan.reused_from.string() << " kept " << written.size()
<< " of its " << expected_set.size() << " sidecars; missing:";
for (size_t i = 0; i < missing.size() && i < 8; ++i)
report << "\n " << missing[i];
if (missing.size() > 8)
report << "\n ... and " << (missing.size() - 8) << " more";
throw std::runtime_error(report.str());
}
}
// Read back, not reported from intent: a spec that failed to embed would
// otherwise still print true.
const auto written_spec = engine::assets::read_gguf_embedded_model_spec(output);
if (embedded_model_spec.has_value() && !written_spec.has_value()) {
throw std::runtime_error("model spec for family '" + embedded_model_spec->family +
"' was not embedded in " + output.string());
}
if (embedded_model_spec.has_value() && written_spec->family != embedded_model_spec->family) {
throw std::runtime_error("embedded model spec family is '" + written_spec->family + "', expected '" +
embedded_model_spec->family + "'");
}
std::cout << "embedded_sidecars=" << (written_sidecars.empty() ? "false" : "true") << "\n";
std::cout << "embedded_sidecar_count=" << written_sidecars.size() << "\n";
std::cout << "embedded_model_spec=" << (written_spec.has_value() ? "true" : "false") << "\n";
if (written_spec.has_value()) {
std::cout << "model_spec_family=" << written_spec->family << "\n";
}
return 0;
} catch (const std::exception & error) {
std::cerr << "error: " << error.what() << "\n";
return 1;
}
}