mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-31 10:28:43 -04:00
The bundled: fix in 842443cd7 shipped without a test, which is how the bug got
there: task 15 verified the form with a hand-written LoadModel that left
ModelFile empty, and that is the one shape the server never produces. Four cases
in streaming_driver_ctest, which already links loaded_model.cpp, pin the
PRODUCTION shapes instead. The first fails against the pre-fix source (returns
the joined /models/bundled:silero_vad); the other three are the branches the
bundled: lookup now runs in front of and must fall through for.
Three family names in the docs were the source directory rather than the
registered family, on pages whose whole argument is that these names cannot be
guessed: demucs is htdemucs (demucs/loader.cpp:22), roformer is
mel_band_roformer (roformer/assets.h:15), and moss is TWO families,
moss_tts_local and moss_tts_nano. The hyphenated ASR names are underscored to
match, here and in the compatibility table.
The supertonic dtype note claimed more than the evidence carries. The f16 abort
is a local observation, identical through TTS and TTSStream; upstream's
docs/gguf.md leaves the 16-bit column untested and records q8_0 as "No
(unsupported weight dtype)", which says unusable rather than fatal. Both are
still refused, because the allow list is what the family can run. Corrected in
family_gate.h, family_gate.cpp and the docs together, since the docs inherited
the wording from the code.
The importers tripwire says in the file that it is a tripwire: it exercises no
audio-cpp behaviour, and the registration assertion lives in backend_test.go.
Assisted-by: Claude:claude-opus-5 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
164 lines
5.8 KiB
C++
164 lines
5.8 KiB
C++
#include "family_gate.h"
|
|
|
|
#include <cctype>
|
|
#include <cstddef>
|
|
|
|
namespace audiocpp_backend {
|
|
namespace {
|
|
|
|
// PRECONDITION: `suffix` must already be lowercase. Both sides are folded, so
|
|
// this reads as symmetric, but only `value` can carry case in practice and a
|
|
// caller passing ".GGUF" would still work today for that reason alone. Do not
|
|
// rely on it: the fold on the suffix side is the only thing standing between
|
|
// this and a helper that answers false for every input, and it is not covered
|
|
// by any test, because with a lowercase suffix no input can distinguish it.
|
|
bool ends_with_ci(const std::string &value, const std::string &suffix) {
|
|
if (value.size() <= suffix.size()) {
|
|
return false; // a bare ".gguf" is an extension, not a model file
|
|
}
|
|
const size_t offset = value.size() - suffix.size();
|
|
for (size_t i = 0; i < suffix.size(); ++i) {
|
|
const auto lhs = static_cast<unsigned char>(value[offset + i]);
|
|
const auto rhs = static_cast<unsigned char>(suffix[i]);
|
|
if (std::tolower(lhs) != std::tolower(rhs)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool path_looks_like_gguf(const std::string &path) {
|
|
return ends_with_ci(path, ".gguf");
|
|
}
|
|
|
|
FamilyDecision decide_family(bool path_is_gguf, const std::string &embedded_family,
|
|
const std::string &configured_family) {
|
|
FamilyDecision decision;
|
|
|
|
if (!configured_family.empty()) {
|
|
decision.ok = true;
|
|
decision.family = configured_family;
|
|
return decision;
|
|
}
|
|
|
|
if (path_is_gguf) {
|
|
if (!embedded_family.empty()) {
|
|
decision.ok = true;
|
|
decision.family = embedded_family;
|
|
return decision;
|
|
}
|
|
decision.error =
|
|
"audio-cpp: this GGUF carries no 'audiocpp.model_spec.family' "
|
|
"metadata key, so it is not an audio.cpp model. Convert it with "
|
|
"audiocpp_gguf, or name the family explicitly with the model option "
|
|
"'family:<name>'";
|
|
return decision;
|
|
}
|
|
|
|
decision.error =
|
|
"audio-cpp: a model path that is not a standalone audio.cpp GGUF needs "
|
|
"an explicit 'family:<name>' model option, because the audio.cpp family "
|
|
"cannot be inferred from a safetensors or package directory";
|
|
return decision;
|
|
}
|
|
|
|
namespace {
|
|
|
|
// Families that ABORT THE PROCESS on a weight dtype they cannot handle, and the
|
|
// dtypes they can. See the header for why this is a list of crashes rather than
|
|
// a list of preferences.
|
|
//
|
|
// supertonic: upstream's docs/gguf.md:90 records its 16-bit GGUF column as
|
|
// "---", i.e. NOT TESTED, and its q8_0 as "No (unsupported weight dtype)". Only
|
|
// the `orig` package is marked Pass, and its 698 weight tensors are f32 while
|
|
// its 72 index and shape constants are i64. The f16 abort is a LOCAL
|
|
// OBSERVATION rather than an upstream claim, and it is attributed rather than
|
|
// assumed: it is identical through the unary TTS RPC and through TTSStream, so
|
|
// it is the packaging and not the streaming path. q8_0 was never run here and is
|
|
// refused on upstream's "unsupported weight dtype" alone, which is the weaker of
|
|
// the two claims. See the header for why keeping them apart matters.
|
|
//
|
|
// TO REMOVE AN ENTRY: bump AUDIO_CPP_VERSION past a fix, load a package in the
|
|
// refused dtype, and synthesise. If audio comes out, delete the entry. No test
|
|
// can do that for you, which is exactly why it is written here: the test beside
|
|
// this file pins WHAT the table says, not whether upstream has moved on. Do not
|
|
// widen an entry without running that, because what it prevents is a process
|
|
// death rather than a wrong answer.
|
|
struct DtypeAllowList {
|
|
// NULL TERMINATED, and the terminator occupies one of these slots: both
|
|
// loops below stop at the first nullptr and have no other bound, so an entry
|
|
// that named three dtypes would leave them reading past the end of the
|
|
// array. That is undefined behaviour rather than a wrong answer, and it is
|
|
// one keystroke away from any edit that widens an entry, so the terminator
|
|
// is asserted at compile time below rather than trusted.
|
|
static constexpr std::size_t kSlots = 3;
|
|
|
|
const char *family;
|
|
const char *allowed[kSlots];
|
|
};
|
|
|
|
constexpr DtypeAllowList kDtypeAllowLists[] = {
|
|
{"supertonic", {"f32", "i64", nullptr}},
|
|
};
|
|
|
|
constexpr bool allow_lists_are_terminated() {
|
|
for (const auto &entry : kDtypeAllowLists) {
|
|
if (entry.allowed[DtypeAllowList::kSlots - 1] != nullptr) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static_assert(allow_lists_are_terminated(),
|
|
"every DtypeAllowList must leave its last slot null: the lookups "
|
|
"below stop at the first nullptr and would otherwise read past "
|
|
"the end of the array");
|
|
|
|
const DtypeAllowList *find_allow_list(const std::string &family) {
|
|
for (const auto &entry : kDtypeAllowLists) {
|
|
if (family == entry.family) {
|
|
return &entry;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool family_has_weight_dtype_allow_list(const std::string &family) {
|
|
return find_allow_list(family) != nullptr;
|
|
}
|
|
|
|
bool weight_dtype_is_supported(const std::string &family, const std::string &dtype) {
|
|
const DtypeAllowList *list = find_allow_list(family);
|
|
if (list == nullptr) {
|
|
return true;
|
|
}
|
|
for (const char *const *name = list->allowed; *name != nullptr; ++name) {
|
|
if (dtype == *name) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::string supported_weight_dtypes(const std::string &family) {
|
|
const DtypeAllowList *list = find_allow_list(family);
|
|
if (list == nullptr) {
|
|
return {};
|
|
}
|
|
std::string out;
|
|
for (const char *const *name = list->allowed; *name != nullptr; ++name) {
|
|
if (!out.empty()) {
|
|
out += ", ";
|
|
}
|
|
out += *name;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
} // namespace audiocpp_backend
|