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
174 lines
6.8 KiB
C++
174 lines
6.8 KiB
C++
// runtime::load_vad_model: the loader ASR VAD chunking uses for <family>.vad_model_path.
|
|
|
|
#include "engine/framework/runtime/registry.h"
|
|
#include "gguf.h"
|
|
|
|
#include <cstdint>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <optional>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <iterator>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#ifndef ENGINE_REPO_ROOT
|
|
#define ENGINE_REPO_ROOT "."
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
namespace rt = engine::runtime;
|
|
namespace fs = std::filesystem;
|
|
|
|
fs::path repo_path(const std::string & relative) {
|
|
return fs::path(ENGINE_REPO_ROOT) / relative;
|
|
}
|
|
|
|
void require(bool condition, const std::string & message) {
|
|
if (!condition) {
|
|
throw std::runtime_error(message);
|
|
}
|
|
}
|
|
|
|
std::string family_of(const fs::path & path, const std::optional<std::string> & hint = std::nullopt) {
|
|
rt::ModelLoadRequest request;
|
|
request.model_path = path;
|
|
request.family_hint = hint;
|
|
return rt::load_vad_model(request)->metadata().family;
|
|
}
|
|
|
|
std::string error_of(const std::function<void()> & action) {
|
|
try {
|
|
action();
|
|
} catch (const std::exception & error) {
|
|
return error.what();
|
|
}
|
|
return {};
|
|
}
|
|
|
|
struct TempDir {
|
|
fs::path path;
|
|
explicit TempDir(const std::string & name) : path(fs::temp_directory_path() / name) {
|
|
fs::remove_all(path);
|
|
fs::create_directories(path);
|
|
}
|
|
~TempDir() {
|
|
std::error_code ec;
|
|
fs::remove_all(path, ec);
|
|
}
|
|
};
|
|
|
|
// A valid safetensors file with one F32 tensor named "x" (not Silero weights).
|
|
void write_tiny_safetensors(const fs::path & path) {
|
|
std::string header = R"({"x":{"dtype":"F32","shape":[1],"data_offsets":[0,4]}})";
|
|
header.append((8 - header.size() % 8) % 8, ' ');
|
|
std::ofstream out(path, std::ios::binary);
|
|
const uint64_t size = header.size();
|
|
out.write(reinterpret_cast<const char *>(&size), sizeof(size));
|
|
out.write(header.data(), static_cast<std::streamsize>(header.size()));
|
|
const float value = 0.0f;
|
|
out.write(reinterpret_cast<const char *>(&value), sizeof(value));
|
|
}
|
|
|
|
const fs::path kSilero = repo_path("assets/framework/models/silero_vad/silero_vad_16k.safetensors");
|
|
|
|
void test_bundled_models() {
|
|
require(family_of(repo_path("assets/framework/models/silero_vad")) == "silero_vad", "silero dir");
|
|
require(family_of(kSilero) == "silero_vad", "silero file");
|
|
require(family_of(repo_path("assets/framework/models/marblenet_vad")) == "marblenet_vad", "marblenet dir");
|
|
require(family_of(repo_path("assets/framework/models/silero_vad"), "silero_vad") == "silero_vad", "hint");
|
|
}
|
|
|
|
void test_renamed_silero_file() {
|
|
// The Silero-only loader accepted Silero weights under any file name.
|
|
TempDir dir("audiocpp_vad_loader_renamed");
|
|
fs::copy_file(kSilero, dir.path / "my_vad.safetensors");
|
|
require(family_of(dir.path / "my_vad.safetensors") == "silero_vad", "renamed silero file");
|
|
}
|
|
|
|
void test_sibling_package_does_not_claim_file() {
|
|
// A package whose loader resolves a file to its folder (pulsevad) sits next to the named
|
|
// VAD file; the named file must still decide the family.
|
|
TempDir dir("audiocpp_vad_loader_sibling");
|
|
fs::copy_file(kSilero, dir.path / "silero_vad_16k.safetensors");
|
|
fs::copy_file(kSilero, dir.path / "my_vad.safetensors");
|
|
fs::copy_file(kSilero, dir.path / "pulsevad-f32.safetensors");
|
|
require(family_of(dir.path / "silero_vad_16k.safetensors") == "silero_vad", "exact silero file next to package");
|
|
require(family_of(dir.path / "my_vad.safetensors") == "silero_vad", "renamed silero file next to package");
|
|
}
|
|
|
|
void test_rejections() {
|
|
const auto non_model = error_of([] { (void)family_of(repo_path("README.md")); });
|
|
require(!non_model.empty(), "a non-model file was accepted");
|
|
require(non_model.find("built VAD families") != std::string::npos, "error lists built VAD families: " + non_model);
|
|
|
|
const auto non_vad_dir = error_of([] { (void)family_of(repo_path("docs")); });
|
|
require(!non_vad_dir.empty(), "a non-VAD directory was accepted");
|
|
|
|
const auto missing = error_of([] { (void)family_of(repo_path("assets/framework/models/does_not_exist")); });
|
|
require(!missing.empty(), "a missing path was accepted");
|
|
|
|
// Weights of a VAD family this binary lacks: say so instead of a Silero tensor error.
|
|
TempDir dir("audiocpp_vad_loader_foreign");
|
|
write_tiny_safetensors(dir.path / "other_vad.safetensors");
|
|
const auto foreign = error_of([&] { (void)family_of(dir.path / "other_vad.safetensors"); });
|
|
require(foreign.find("no Silero VAD tensors") != std::string::npos &&
|
|
foreign.find("built VAD families") != std::string::npos,
|
|
"foreign weights error: " + foreign);
|
|
|
|
const auto bad_hint = error_of([] { (void)family_of(kSilero, std::string("not_a_vad_family")); });
|
|
require(bad_hint.find("not built into this binary") != std::string::npos, "unknown hint error: " + bad_hint);
|
|
}
|
|
|
|
void test_standalone_embedded_vad() {
|
|
if (!rt::make_default_registry().supports_family("pulsevad")) {
|
|
std::cout << "[SKIP] standalone_embedded_vad: pulsevad is not built\n";
|
|
return;
|
|
}
|
|
TempDir dir("audiocpp_vad_loader_standalone");
|
|
std::ifstream spec(repo_path("model_specs/pulsevad.json"));
|
|
require(spec.good(), "open PulseVAD spec fixture");
|
|
const std::string json((std::istreambuf_iterator<char>(spec)), std::istreambuf_iterator<char>());
|
|
const auto path = dir.path / "vad.gguf";
|
|
auto * gguf = gguf_init_empty();
|
|
gguf_set_val_u32(gguf, "audiocpp.model_spec.version", 1);
|
|
gguf_set_val_str(gguf, "audiocpp.model_spec.family", "pulsevad");
|
|
gguf_set_val_str(gguf, "audiocpp.model_spec.json", json.c_str());
|
|
const bool written = gguf_write_to_file(gguf, path.string().c_str(), true);
|
|
gguf_free(gguf);
|
|
require(written, "write embedded VAD spec fixture");
|
|
|
|
const auto previous = fs::current_path();
|
|
fs::current_path(dir.path);
|
|
const auto error = error_of([&] {
|
|
require(family_of(path) == "pulsevad", "standalone embedded VAD family");
|
|
});
|
|
fs::current_path(previous);
|
|
require(error.empty(), "standalone embedded VAD: " + error);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
const std::vector<std::pair<const char *, void (*)()>> tests = {
|
|
{"bundled_models", test_bundled_models},
|
|
{"renamed_silero_file", test_renamed_silero_file},
|
|
{"sibling_package_does_not_claim_file", test_sibling_package_does_not_claim_file},
|
|
{"rejections", test_rejections},
|
|
{"standalone_embedded_vad", test_standalone_embedded_vad},
|
|
};
|
|
int failures = 0;
|
|
for (const auto & [name, test] : tests) {
|
|
try {
|
|
test();
|
|
std::cout << "[PASS] " << name << "\n";
|
|
} catch (const std::exception & error) {
|
|
std::cerr << "[FAIL] " << name << ": " << error.what() << "\n";
|
|
++failures;
|
|
}
|
|
}
|
|
return failures == 0 ? 0 : 1;
|
|
}
|