voice_audio_multi/tests/unittests/test_fun_asr_nano_assets.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

495 lines
19 KiB
C++

#include "engine/framework/assets/resource_bundle.h"
#include "engine/framework/io/safetensors.h"
#include "engine/models/fun_asr_nano/assets.h"
#include "test_assert.h"
#include <cstring>
#include <filesystem>
#include <fstream>
#include <functional>
#include <iostream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace {
struct ResourceOptions {
bool include_tokenizer = true;
bool include_stem = true;
bool include_embedding = true;
};
std::vector<unsigned char> float_bytes(float value) {
std::vector<unsigned char> bytes(sizeof(value));
std::memcpy(bytes.data(), &value, sizeof(value));
return bytes;
}
void write_text(const std::filesystem::path &path, const std::string &text) {
std::ofstream output(path, std::ios::binary);
output << text;
}
std::string processor_config() {
return R"json({
"audio_token":"<|object_ref_start|>",
"default_transcription_prompt":"Transcribe the audio:",
"feature_extractor":{
"feature_size":80,
"sampling_rate":16000,
"frame_length":25,
"frame_shift":10,
"lfr_m":7,
"lfr_n":6,
"preemphasis":0.97,
"return_attention_mask":true
}
})json";
}
engine::assets::ResourceBundle make_resources(const std::filesystem::path &root,
const std::string &config_json,
ResourceOptions options = {}) {
std::filesystem::create_directories(root);
write_text(root / "config.json", config_json);
write_text(root / "generation_config.json",
R"json({"eos_token_id":151645,"bos_token_id":151643})json");
write_text(root / "processor_config.json", processor_config());
if (options.include_tokenizer) {
write_text(root / "tokenizer.json",
R"json({"version":"1.0","added_tokens":[]})json");
}
std::vector<engine::io::SafeTensorWriteEntry> tensors;
if (options.include_stem) {
tensors.push_back({
"model.audio_tower.stem.self_attn.q_proj.weight",
"F32",
{1},
float_bytes(1.0F),
});
}
if (options.include_embedding) {
tensors.push_back({
"model.language_model.embed_tokens.weight",
"F32",
{1},
float_bytes(1.0F),
});
}
engine::io::write_safetensors_file(root / "model.safetensors", tensors);
engine::assets::ResourceBundle resources(root);
resources.add_file("config", root / "config.json");
resources.add_file("generation_config", root / "generation_config.json");
resources.add_file("processor_config", root / "processor_config.json");
if (options.include_tokenizer) {
resources.add_file("tokenizer_json", root / "tokenizer.json");
}
resources.add_tensor_source("weights", root / "model.safetensors");
return resources;
}
std::string current_config() {
return R"json({
"model_type":"fun_asr_nano",
"audio_token_id":151646,
"projector_hidden_size":2048,
"tie_word_embeddings":true,
"encoder_config":{
"model_type":"fun_asr_nano_encoder",
"num_mel_bins":80,
"num_stacked_frames":7,
"d_model":512,
"encoder_attention_heads":4,
"encoder_ffn_dim":2048,
"encoder_layers":50,
"num_timestamp_prediction_blocks":20,
"kernel_size":11,
"activation_function":"relu",
"max_position_embeddings":2049
},
"adaptor_config":{
"d_model":1024,
"encoder_attention_heads":8,
"encoder_ffn_dim":256,
"encoder_layers":2,
"activation_function":"relu"
},
"text_config":{
"model_type":"qwen3",
"vocab_size":151936,
"hidden_size":1024,
"intermediate_size":3072,
"num_hidden_layers":28,
"num_attention_heads":16,
"num_key_value_heads":8,
"head_dim":128,
"max_position_embeddings":40960,
"rms_norm_eps":0.000001,
"rope_parameters":{"rope_theta":1000000},
"tie_word_embeddings":true
}
})json";
}
std::string legacy_config() {
return R"json({
"model_type":"fun_asr_nano",
"audio_token_id":151646,
"activation_function":"relu",
"adaptor_intermediate_size":2048,
"adaptor_num_attention_heads":8,
"adaptor_num_hidden_layers":2,
"tie_word_embeddings":true,
"encoder_config":{
"model_type":"fun_asr_nano_encoder",
"num_mel_bins":80,
"num_stacked_frames":7,
"d_model":512,
"encoder_attention_heads":4,
"encoder_ffn_dim":2048,
"encoder_layers":50,
"num_timestamp_prediction_blocks":20,
"kernel_size":11,
"activation_function":"relu"
},
"text_config":{
"model_type":"qwen3",
"vocab_size":151936,
"hidden_size":1024,
"intermediate_size":3072,
"num_hidden_layers":28,
"num_attention_heads":16,
"num_key_value_heads":8,
"head_dim":128,
"max_position_embeddings":40960,
"rms_norm_eps":0.000001,
"rope_parameters":{"rope_theta":1000000},
"tie_word_embeddings":true
}
})json";
}
std::string replace_once(std::string value, const std::string &from,
const std::string &to) {
const auto position = value.find(from);
if (position == std::string::npos) {
throw std::runtime_error("test fixture marker not found: " + from);
}
value.replace(position, from.size(), to);
return value;
}
void require_rejected(const std::filesystem::path &root,
const std::string &expected_message,
const std::function<void()> &action) {
bool rejected = false;
try {
action();
} catch (const std::runtime_error &error) {
rejected =
std::string(error.what()).find(expected_message) != std::string::npos;
}
engine::test::require(rejected,
"Fun-ASR did not reject invalid assets with: " +
expected_message);
std::filesystem::remove_all(root);
}
void require_published_dimensions(
const engine::models::fun_asr_nano::FunAsrNanoConfig &config) {
engine::test::require_eq(config.model_type, std::string("fun_asr_nano"),
"Fun-ASR model type");
engine::test::require_eq(config.encoder.input_size, int64_t{560},
"Fun-ASR encoder input size");
engine::test::require_eq(config.encoder.d_model, int64_t{512},
"Fun-ASR encoder width");
engine::test::require_eq(config.encoder.layers, int64_t{50},
"Fun-ASR main blocks");
engine::test::require_eq(config.encoder.timestamp_prediction_layers,
int64_t{20}, "Fun-ASR timestamp blocks");
engine::test::require_eq(config.encoder.attention_heads, int64_t{4},
"Fun-ASR encoder heads");
engine::test::require_eq(config.encoder.ffn_dim, int64_t{2048},
"Fun-ASR encoder FFN");
engine::test::require_eq(config.encoder.kernel_size, int64_t{11},
"Fun-ASR FSMN kernel");
engine::test::require_eq(config.encoder.max_position_embeddings,
int64_t{2049}, "Fun-ASR encoder position limit");
engine::test::require_eq(config.projector_hidden_size, int64_t{2048},
"Fun-ASR projector width");
engine::test::require_eq(config.adaptor.d_model, int64_t{1024},
"Fun-ASR adaptor width");
engine::test::require_eq(config.adaptor.attention_heads, int64_t{8},
"Fun-ASR adaptor heads");
engine::test::require_eq(config.adaptor.ffn_dim, int64_t{256},
"Fun-ASR adaptor FFN");
engine::test::require_eq(config.adaptor.layers, int64_t{2},
"Fun-ASR adaptor layers");
engine::test::require_eq(config.text.hidden_size, int64_t{1024},
"Fun-ASR text width");
engine::test::require_eq(config.text.intermediate_size, int64_t{3072},
"Fun-ASR text FFN");
engine::test::require_eq(config.text.layers, int64_t{28},
"Fun-ASR text layers");
engine::test::require_eq(config.text.attention_heads, int64_t{16},
"Fun-ASR text heads");
engine::test::require_eq(config.text.key_value_heads, int64_t{8},
"Fun-ASR KV heads");
engine::test::require_eq(config.text.head_dim, int64_t{128},
"Fun-ASR text head dimension");
engine::test::require_eq(config.text.vocab_size, int64_t{151936},
"Fun-ASR vocabulary");
engine::test::require_close(config.text.rope_theta, 1000000.0F, 0.0F,
"Fun-ASR RoPE theta");
engine::test::require_eq(config.frontend.sample_rate, 16000,
"Fun-ASR sample rate");
engine::test::require_eq(config.frontend.feature_size, int64_t{80},
"Fun-ASR frontend feature size");
engine::test::require_eq(config.frontend.frame_length_ms, int64_t{25},
"Fun-ASR frontend frame length");
engine::test::require_eq(config.frontend.frame_shift_ms, int64_t{10},
"Fun-ASR frontend frame shift");
engine::test::require_eq(config.frontend.lfr_m, int64_t{7},
"Fun-ASR LFR stack");
engine::test::require_eq(config.frontend.lfr_n, int64_t{6},
"Fun-ASR LFR stride");
engine::test::require_close(config.frontend.preemphasis, 0.97F, 0.0F,
"Fun-ASR frontend preemphasis");
}
void test_current_nested_config() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_nested_assets_test";
std::filesystem::remove_all(root);
auto assets = engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, current_config()));
require_published_dimensions(assets->config);
std::filesystem::remove_all(root);
}
void test_legacy_flat_adaptor_config() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_legacy_assets_test";
std::filesystem::remove_all(root);
auto assets = engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, legacy_config()));
require_published_dimensions(assets->config);
std::filesystem::remove_all(root);
}
void test_loads_model_directory_through_family_spec() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_model_spec_test";
std::filesystem::remove_all(root);
(void)make_resources(root, current_config());
auto assets = engine::models::fun_asr_nano::load_fun_asr_nano_assets(root);
require_published_dimensions(assets->config);
engine::test::require_eq(assets->resources.model_root(), root,
"Fun-ASR model root");
std::filesystem::remove_all(root);
}
void test_rejects_incompatible_adaptor_width() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_invalid_assets_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"d_model\":1024", "\"d_model\":768");
require_rejected(root, "adaptor", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_adaptor_shape() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_adaptor_shape_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"encoder_attention_heads\":8",
"\"encoder_attention_heads\":16");
require_rejected(root, "published adaptor architecture", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_encoder_shape() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_encoder_shape_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"d_model\":512", "\"d_model\":768");
require_rejected(root, "published encoder architecture", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_encoder_position_limit() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_encoder_position_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"max_position_embeddings\":2049",
"\"max_position_embeddings\":1024");
require_rejected(root, "published encoder architecture", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_text_shape() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_text_shape_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"intermediate_size\":3072",
"\"intermediate_size\":4096");
require_rejected(root, "published Qwen architecture", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_zero_text_attention_heads() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_zero_text_heads_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"num_attention_heads\":16",
"\"num_attention_heads\":0");
require_rejected(root, "text dimensions", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_projector_shape() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_projector_shape_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"projector_hidden_size\":2048",
"\"projector_hidden_size\":4096");
require_rejected(root, "projector hidden size", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_nonpublished_frontend_parameters() {
const std::vector<std::pair<std::string, std::string>> mutations = {
{"\"frame_length\":25", "\"frame_length\":30"},
{"\"frame_shift\":10", "\"frame_shift\":20"},
{"\"lfr_n\":6", "\"lfr_n\":5"},
{"\"preemphasis\":0.97", "\"preemphasis\":0.50"},
};
for (size_t index = 0; index < mutations.size(); ++index) {
const auto root =
std::filesystem::temp_directory_path() /
("audiocpp_fun_asr_frontend_shape_test_" + std::to_string(index));
std::filesystem::remove_all(root);
auto resources = make_resources(root, current_config());
write_text(root / "processor_config.json",
replace_once(processor_config(), mutations[index].first,
mutations[index].second));
require_rejected(root, "published frontend", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
std::move(resources));
});
}
}
void test_rejects_unsupported_activation() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_activation_assets_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"activation_function\":\"relu\"",
"\"activation_function\":\"gelu\"");
require_rejected(root, "ReLU encoder", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_untied_embeddings() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_untied_assets_test";
std::filesystem::remove_all(root);
const auto invalid =
replace_once(current_config(), "\"tie_word_embeddings\":true",
"\"tie_word_embeddings\":false");
require_rejected(root, "tied text embeddings", [&] {
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, invalid));
});
}
void test_rejects_missing_tokenizer() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_tokenizer_assets_test";
std::filesystem::remove_all(root);
require_rejected(root, "tokenizer_json", [&] {
ResourceOptions options;
options.include_tokenizer = false;
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, current_config(), options));
});
}
void test_rejects_missing_stem_tensor() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_stem_assets_test";
std::filesystem::remove_all(root);
require_rejected(root, "audio_tower.stem", [&] {
ResourceOptions options;
options.include_stem = false;
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, current_config(), options));
});
}
void test_rejects_missing_token_embedding() {
const auto root = std::filesystem::temp_directory_path() /
"audiocpp_fun_asr_embedding_assets_test";
std::filesystem::remove_all(root);
require_rejected(root, "embed_tokens", [&] {
ResourceOptions options;
options.include_embedding = false;
(void)engine::models::fun_asr_nano::load_fun_asr_nano_assets(
make_resources(root, current_config(), options));
});
}
} // namespace
int main() {
try {
test_current_nested_config();
test_legacy_flat_adaptor_config();
test_loads_model_directory_through_family_spec();
test_rejects_incompatible_adaptor_width();
test_rejects_nonpublished_adaptor_shape();
test_rejects_nonpublished_encoder_shape();
test_rejects_nonpublished_encoder_position_limit();
test_rejects_nonpublished_text_shape();
test_rejects_zero_text_attention_heads();
test_rejects_nonpublished_projector_shape();
test_rejects_nonpublished_frontend_parameters();
test_rejects_unsupported_activation();
test_rejects_untied_embeddings();
test_rejects_missing_tokenizer();
test_rejects_missing_stem_tensor();
test_rejects_missing_token_embedding();
} catch (const std::exception &error) {
std::cerr << error.what() << '\n';
return 1;
}
std::cout << "fun_asr_nano_assets_test passed\n";
return 0;
}