voice_audio_multi/tests/lfm3_audio/test_lfm3_audio_s2s.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

126 lines
7.3 KiB
C++

// LFM3-Audio speech-to-speech on real weights (see lfm3_audio_model_test.h for the arguments and tolerances):
// - greedy replies to c_16k with the fixture's MaskGIT orders injected: the slot sequence that ends on its own
// (the fixture's slot pattern) and the one cut at max_tokens=96, whose reply artifact says ended=false;
// - streaming gives the offline text, reply artifact and audio for a sampled reply;
// - turn 2 after a replayed turn 1 (the full reply, and the cut one closed with an EOS frame and <|im_end|>):
// the prompt ids and the prefill logits of the fixture's re-prefill (gen/multiturn_*).
#include "lfm3_audio_model_test.h"
#include "engine/community_models/lfm3_audio/chat.h"
#include "engine/framework/io/binary.h"
#include <exception>
#include <numeric>
namespace {
using namespace lfm3_model_test;
const lfm3::Lfm3ReplyCheckpoint kCheckpoint{};
void check_greedy(Checks & checks, const Context & ctx, lfm3::Lfm3ChatSession & s2s, const std::string & tag, const std::string & max_tokens) {
ScopedDumps dumps(tag, ctx.fixture("gen/" + tag + "_frame_order.npy"));
const auto result = s2s.run(audio_request(read_audio(ctx.fixture("audio/c_16k.wav")),
{{"temperature", "0"}, {"repetition_penalty", "1"}, {"max_tokens", max_tokens}, {"return_codes", "true"},
{"seed", "42"}})); // MaskGIT orders past the fixture's frames are random
dumps.stop();
checks.expect(dumps.i32("prompt_ids") == ctx.i32("prompt/s2s_c_ids"), tag + " prompt ids");
const bool same = check_greedy_run(checks, ctx, dumps, tag, false);
const auto & artifact = result.output_artifacts.at(0);
const auto steps = lfm3::read_lfm3_reply_artifact(artifact, kCheckpoint);
const bool cut = tag.find("trunc") != std::string::npos;
// A run that parted from the reference may end elsewhere; the artifact must still say how it ended.
checks.expect(artifact.meta.at("ended") == (s2s.reached_max_tokens() ? "false" : "true"), tag + " reply artifact ended matches the run",
"ended=" + artifact.meta.at("ended") + ", " + std::to_string(steps.size()) + " steps");
checks.expect_or_info(same, s2s.reached_max_tokens() == cut, tag + std::string(cut ? " is cut at max_tokens" : " ends on its own"));
if (!cut) {
checks.expect_or_info(same, text_of(result) == ctx.result_text(tag), tag + " reply text", "\"" + text_of(result) + "\"");
}
}
void check_stream(Checks & checks, Context & ctx, lfm3::Lfm3ChatSession & s2s) {
const auto request = audio_request(read_audio(ctx.fixture("audio/c_16k.wav")), {{"seed", "42"}, {"max_tokens", "200"}, {"return_codes", "true"}});
const auto offline = s2s.run(request);
auto streaming = ctx.open<lfm3::Lfm3ChatSession>(runtime::VoiceTaskKind::SpeechToSpeech, runtime::RunMode::Streaming);
streaming->start_stream(request);
const auto & audio = *request.audio_input;
for (size_t at = 0; at < audio.samples.size(); at += 1600) {
const auto end = std::min(audio.samples.size(), at + 1600);
streaming->process_audio_chunk({audio.sample_rate, 1, static_cast<int64_t>(at), {audio.samples.begin() + at, audio.samples.begin() + end}});
}
std::string text;
std::vector<float> samples;
while (auto event = streaming->next_stream_event()) {
if (event->partial_text) text += event->partial_text->text;
if (event->audio_output) samples.insert(samples.end(), event->audio_output->samples.begin(), event->audio_output->samples.end());
}
const auto finished = streaming->finish_stream();
checks.expect(text == text_of(offline) && text_of(finished) == text, "s2s streaming text == offline", "\"" + text + "\"");
checks.expect(finished.output_artifacts.at(0).payload == offline.output_artifacts.at(0).payload, "s2s streaming reply artifact == offline");
const auto & want = offline.audio_output->samples;
checks.expect(samples.size() == want.size(), "s2s streaming audio length", std::to_string(samples.size()) + " vs " + std::to_string(want.size()));
if (samples.size() == want.size()) {
checks.expect_rel(diff(samples, want), ctx.tolerance("codec.tts_wave", true), "s2s streaming audio == offline");
}
}
void check_turn2(Checks & checks, const Context & ctx, lfm3::Lfm3ChatSession & s2s, const std::string & turn1, const std::string & tag) {
std::vector<lfm3::Lfm3Step> reply;
for (const auto & slot : fixture_slots(ctx, turn1)) {
reply.push_back({slot.token, slot.codes});
}
const bool ended = !reply.empty() && reply.back().codes.empty(); // ends on <|im_end|>; a cut reply ends inside its audio
auto request = audio_request(read_audio(ctx.fixture("audio/a_16k.wav")), {{"temperature", "0"}, {"max_tokens", "7"}});
request.input_artifacts = {
runtime::make_voice_artifact(runtime::ArtifactKind::Custom, lfm3::kLfm3QuestionArtifactId,
engine::io::read_binary_file(ctx.fixture("audio/c_16k.wav"))),
lfm3::make_lfm3_reply_artifact(reply, ended, kCheckpoint)};
ScopedDumps dumps(tag);
s2s.run(request);
dumps.stop();
const auto ids = dumps.i32("prompt_ids");
const auto want_ids = ctx.i32("gen/" + tag + "_prompt_ids");
checks.expect(ids == want_ids, tag + " turn-2 prompt ids", std::to_string(ids.size()) + " vs " + std::to_string(want_ids.size()));
const auto logits = dumps.f32("prefill_logits");
const auto want = ctx.f32("gen/" + tag + "_last_logits");
if (logits.size() != want.size()) {
checks.expect(false, tag + " turn-2 prefill logits", "size " + std::to_string(logits.size()));
return;
}
std::vector<int32_t> order(logits.size());
std::iota(order.begin(), order.end(), 0);
std::partial_sort(order.begin(), order.begin() + 20, order.end(), [&](int32_t a, int32_t b) { return logits[a] > logits[b]; });
const auto top20 = ctx.i32("gen/" + tag + "_top20_ids");
const auto overlap = std::count_if(order.begin(), order.begin() + 20, [&](int32_t id) { return std::find(top20.begin(), top20.end(), id) != top20.end(); });
checks.expect_rel(diff(logits, want), ctx.tolerance("backbone.last_logits"), tag + " turn-2 prefill logits",
", argmax " + std::to_string(order[0]) + " vs " + std::to_string(top20[0]) + ", top-20 overlap " + std::to_string(overlap) + "/20");
checks.expect_or_info(ctx.profile != Profile::Q4_K, order[0] == top20[0], tag + " turn-2 prefill argmax");
}
} // namespace
int main(int argc, char ** argv) {
int exit_code = kExitPass;
auto parsed = parse_context(argc, argv, "test_lfm3_audio_s2s", exit_code);
if (!parsed) {
return exit_code;
}
auto & ctx = *parsed;
Checks checks;
try {
auto s2s = ctx.open<lfm3::Lfm3ChatSession>(runtime::VoiceTaskKind::SpeechToSpeech, runtime::RunMode::Offline);
check_greedy(checks, ctx, *s2s, "s2s_c_greedy", "1024");
check_greedy(checks, ctx, *s2s, "s2s_c_greedy_trunc", "96");
check_stream(checks, ctx, *s2s);
check_turn2(checks, ctx, *s2s, "s2s_c_greedy", "multiturn_s2s_turn2");
check_turn2(checks, ctx, *s2s, "s2s_c_greedy_trunc", "multiturn_s2s_trunc_turn2");
} catch (const std::exception & error) {
std::cerr << "FAIL: " << error.what() << "\n";
return kExitFail;
}
return checks.finish("test_lfm3_audio_s2s");
}