voice_audio_multi/tests/auk/flow_test.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

420 lines
31 KiB
C++

#include "engine/community_models/auk/flow.h"
#include "engine/community_models/auk/vae.h"
#include "engine/community_models/auk/conditioning.h"
#include "engine/community_models/auk/session.h"
#include "engine/framework/runtime/spec_backed_model.h"
#include "engine/framework/io/json.h"
#include "engine/framework/sampling/torch_random.h"
#include "engine/framework/audio/wav_writer.h"
#include "engine/framework/audio/wav_reader.h"
#include "engine/framework/debug/trace.h"
#include <cmath>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <chrono>
#include <algorithm>
int main(int argc, char ** argv) {
if ((argc != 5 && argc != 6 && argc != 7 && argc != 9) || std::string(argv[3]) != "--log" ||
(argc >= 7 && std::string(argv[5]) != "--instruct" && std::string(argv[5]) != "--session") ||
(argc == 9 && std::string(argv[7]) != "--benchmark-runs" && std::string(argv[7]) != "--alternate-fixture" && std::string(argv[7]) != "--task" && std::string(argv[7]) != "--session-lifetimes") ||
(argc == 6 && std::string(argv[5]) != "--cfg" && std::string(argv[5]) != "--cfg-explicit" && std::string(argv[5]) != "--sample" && std::string(argv[5]) != "--sample-explicit")) {
std::cerr << "Usage: auk_flow_test <auk_base.safetensors> <fixture-dir> --log <log-file> [--cfg|--cfg-explicit|--sample|--sample-explicit|--instruct <qwen-dir> [--benchmark-runs N|--alternate-fixture DIR]|--session <qwen-dir> [--benchmark-runs N|--alternate-fixture DIR|--task gen|--session-lifetimes N]]\n";
return 2;
}
engine::debug::configure_logging({true, std::string(argv[4])});
try {
const bool instruct = argc >= 7;
const bool alternate_fixture = argc == 9 && std::string(argv[7]) == "--alternate-fixture";
const bool explicit_task = argc == 9 && std::string(argv[7]) == "--task";
const bool lifetime_test = argc == 9 && std::string(argv[7]) == "--session-lifetimes";
if (explicit_task && std::string(argv[5]) != "--session") throw std::runtime_error("--task requires --session");
if (lifetime_test && std::string(argv[5]) != "--session") throw std::runtime_error("--session-lifetimes requires --session");
const int lifetimes = lifetime_test ? std::stoi(argv[8]) : 1;
if (lifetimes <= 0) throw std::runtime_error("session lifetimes must be positive");
const bool benchmark = argc == 9 && std::string(argv[7]) == "--benchmark-runs";
const int benchmark_runs = benchmark ? std::stoi(argv[8]) : 0;
if (benchmark && benchmark_runs <= 0) throw std::runtime_error("benchmark runs must be positive");
const bool cfg = argc >= 6;
const bool explicit_attention = instruct || (cfg && (std::string(argv[5]) == "--sample-explicit" || std::string(argv[5]) == "--cfg-explicit"));
const bool sample = instruct || (cfg && (std::string(argv[5]) == "--sample" || std::string(argv[5]) == "--sample-explicit"));
const auto source = engine::assets::open_tensor_source(argv[1]);
const std::filesystem::path directory = argv[2];
const auto inputs = engine::assets::open_tensor_source(directory / "reference.safetensors");
const auto expected = engine::assets::open_tensor_source(directory / (sample ? "reference.safetensors" : cfg ? "flow-cfg.safetensors" : "flow-embeddings.safetensors"));
if (argc >= 7 && std::string(argv[5]) == "--session") {
const std::filesystem::path qwen_dir = argv[6];
auto assets = std::make_shared<engine::models::auk::AukAssets>();
assets->model = source;
assets->qwen = engine::assets::open_tensor_source(qwen_dir / "model.safetensors.index.json");
assets->vae = engine::assets::open_tensor_source(std::filesystem::path(argv[1]).parent_path() / "vae.safetensors");
engine::tokenizers::LlamaBpeTokenizerSpec spec;
spec.tokenizer_config_path = qwen_dir / "tokenizer_config.json";
spec.tokenizer_json_path = qwen_dir / "tokenizer.json";
spec.pre_type = engine::tokenizers::LlamaBpePreTokenizer::Qwen2;
assets->tokenizer = engine::tokenizers::load_llama_bpe_tokenizer(spec);
engine::runtime::SessionOptions options;
options.backend = {engine::core::BackendType::Cuda, 0, 8};
options.options["auk.dtype"] = "fp32";
engine::core::ExecutionContext memory_observer(options.backend);
const auto before_session = memory_observer.memory_snapshot();
if (!before_session.available) throw std::runtime_error("CUDA memory snapshot unavailable");
engine::debug::trace_log_scalar("auk.session.device_used_before_bytes", before_session.used_bytes);
auto session = std::make_unique<engine::models::auk::AukSession>(
engine::runtime::TaskSpec{explicit_task ? engine::runtime::parse_voice_task_kind(argv[8]) : engine::runtime::VoiceTaskKind::Tts,
engine::runtime::RunMode::Offline}, options, assets, engine::runtime::require_model_contract("auk"));
session->prepare({});
std::vector<std::filesystem::path> fixtures{directory};
if (alternate_fixture) {
fixtures.emplace_back(argv[8]);
fixtures.push_back(directory);
}
std::vector<float> first_output;
engine::runtime::TaskRequest lifetime_request;
for (size_t index = 0; index < fixtures.size(); ++index) {
const auto manifest = engine::io::json::parse_file(fixtures[index] / "manifest.json");
if (manifest.require("dtype").as_string() != "fp32") throw std::runtime_error("session fixture must be FP32");
engine::runtime::TaskRequest request;
request.text_input = engine::runtime::Transcript{manifest.require("instruction").as_string(), ""};
for (const auto & message : manifest.require("messages").as_array()) {
for (const auto & content : message.require("content").as_array()) {
if (content.require("type").as_string() == "audio") {
if (request.audio_input) throw std::runtime_error("session fixture requires a single reference audio");
const auto wav = engine::audio::read_wav_f32(std::filesystem::path(content.require("audio").as_string()));
request.audio_input = engine::runtime::AudioBuffer{wav.sample_rate, wav.channels, wav.samples};
}
}
}
request.options = {
{"duration", engine::io::json::stringify_number(manifest.require("duration").as_number())},
{"seed", std::to_string(manifest.require("seed").as_i64())},
{"num_inference_steps", std::to_string(manifest.require("steps").as_i64())},
{"cfg_scale", engine::io::json::stringify_number(manifest.require("cfg").as_number())},
};
if (const auto * sway = manifest.find("sway")) {
request.options["sway_sampling_coef"] = engine::io::json::stringify_number(sway->as_number());
}
const auto result = session->run(request);
if (!result.audio_output || result.audio_output->sample_rate != 24000 || result.audio_output->channels != 1) {
throw std::runtime_error("session audio output contract failed");
}
const auto & actual = result.audio_output->samples;
const auto fixture = engine::assets::open_tensor_source(fixtures[index] / "reference.safetensors");
const auto reference = fixture->require_f32("waveform");
if (actual.size() != reference.size()) throw std::runtime_error("session waveform length mismatch");
double dot = 0, norm = 0, reference_norm = 0, error = 0;
for (size_t i = 0; i < actual.size(); ++i) {
dot += double(actual[i]) * reference[i];
norm += double(actual[i]) * actual[i];
reference_norm += double(reference[i]) * reference[i];
error += std::pow(double(actual[i]) - reference[i], 2);
}
const double cosine = dot / std::sqrt(norm * reference_norm);
engine::debug::trace_log_scalar("auk.session.waveform_cosine", cosine);
engine::debug::trace_log_scalar("auk.session.waveform_rmse", std::sqrt(error / actual.size()));
if (!(cosine > 0.99999)) throw std::runtime_error("session waveform parity gate failed");
if (session->run(request).audio_output->samples != actual) throw std::runtime_error("session repeat output changed");
if (request.audio_input && !explicit_task) {
auto voice_request = request;
voice_request.voice.emplace();
voice_request.voice->speaker.emplace();
voice_request.voice->speaker->audio = voice_request.audio_input;
voice_request.audio_input.reset();
if (session->run(voice_request).audio_output->samples != actual) {
throw std::runtime_error("speaker reference differs from audio_input reference");
}
engine::debug::log_message("AuK speaker-audio and audio_input references produce identical samples");
}
if (benchmark_runs) {
std::vector<double> milliseconds;
for (int run = 0; run < benchmark_runs + 2; ++run) {
const auto start = std::chrono::steady_clock::now();
const auto repeated = session->run(request);
const double elapsed = std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now() - start).count();
if (!repeated.audio_output || repeated.audio_output->samples != actual) {
throw std::runtime_error("session benchmark waveform changed");
}
if (run >= 2) {
milliseconds.push_back(elapsed);
engine::debug::trace_log_scalar("auk.session.inference_ms", elapsed);
}
}
std::sort(milliseconds.begin(), milliseconds.end());
const size_t middle = milliseconds.size() / 2;
const double median = (milliseconds[middle] + milliseconds[(milliseconds.size() - 1) / 2]) / 2;
engine::debug::trace_log_scalar("auk.session.inference_median_ms", median);
engine::debug::trace_log_scalar("auk.session.rtf", median * 24.0 / actual.size());
}
if (index == 0) {
first_output = actual;
if (lifetime_test) lifetime_request = request;
}
if (index == 2 && actual != first_output) throw std::runtime_error("session restored shape output changed");
engine::audio::write_pcm16_wav(fixtures[index] / "native-session.wav", 24000, 1, actual);
if (index + 1 == fixtures.size()) {
auto short_request = request;
short_request.options["duration"] = "0.1";
short_request.options["num_inference_steps"] = "1";
const auto short_result = session->run(short_request);
if (!short_result.audio_output || short_result.audio_output->samples.size() != 2400) {
throw std::runtime_error("0.1-second session duration must produce exactly 2400 samples");
}
engine::debug::log_message("AuK fractional duration contract passed: 0.1 seconds -> 2400 samples");
if (request.audio_input) {
auto default_request = short_request;
default_request.options.erase("duration");
const auto default_result = session->run(default_request);
// The fixture's valid prefix may be shorter than its padded latent storage.
const auto mask = fixture->require_tensor_data("fm.first.ref_mask");
const auto valid_frames = std::count(mask.bytes.begin(), mask.bytes.end(), std::byte{1});
if (!default_result.audio_output || default_result.audio_output->samples.size() !=
static_cast<size_t>(std::max<int64_t>(1, valid_frames) * 480)) {
throw std::runtime_error("default duration must match the valid reference prefix");
}
// Avoid a decimal boundary that Python's ceil can round into the next frame.
default_request.options["duration"] = engine::io::json::stringify_number(
(double(std::max<int64_t>(1, valid_frames)) - 0.5) * 480.0 / 24000.0);
if (session->run(default_request).audio_output->samples != default_result.audio_output->samples) {
throw std::runtime_error("default reference duration differs from explicit duration");
}
engine::debug::log_message("AuK default reference duration matches explicit duration");
}
if (session->run(request).audio_output->samples != actual) {
throw std::runtime_error("session output changed after restoring the original step count");
}
engine::debug::log_message("AuK original sampling schedule restored bit-identically after one-step request");
}
}
engine::debug::trace_log_scalar("auk.session.device_used_loaded_bytes", memory_observer.memory_snapshot().used_bytes);
session.reset();
engine::debug::trace_log_scalar("auk.session.device_used_after_destroy_bytes", memory_observer.memory_snapshot().used_bytes);
for (int lifetime = 1; lifetime < lifetimes; ++lifetime) {
engine::debug::trace_log_scalar("auk.session.lifetime", lifetime + 1);
session = std::make_unique<engine::models::auk::AukSession>(
engine::runtime::TaskSpec{engine::runtime::VoiceTaskKind::Tts, engine::runtime::RunMode::Offline},
options, assets, engine::runtime::require_model_contract("auk"));
session->prepare({});
const auto result = session->run(lifetime_request);
if (!result.audio_output || result.audio_output->sample_rate != 24000 ||
result.audio_output->channels != 1 || result.audio_output->samples != first_output) {
throw std::runtime_error("session recreation changed the seeded output");
}
engine::debug::trace_log_scalar("auk.session.device_used_loaded_bytes", memory_observer.memory_snapshot().used_bytes);
session.reset();
engine::debug::trace_log_scalar("auk.session.device_used_after_destroy_bytes", memory_observer.memory_snapshot().used_bytes);
}
engine::debug::log_message("AuK native offline session parity and repeat checks passed");
return 0;
}
const auto audio_shape = inputs->require_metadata("fm.first.x").shape;
const auto text_shape = inputs->require_metadata("fm.first.text").shape;
if (audio_shape.size() != 3 || text_shape.size() != 3 || audio_shape[0] != 1 || text_shape[0] != 1 ||
audio_shape[2] != 64 || text_shape[2] != 2048) {
throw std::runtime_error("unsupported flow fixture shapes");
}
auto audio = inputs->require_f32("fm.first.x");
auto text = inputs->require_f32("fm.first.text");
const int64_t reference_frames = inputs->require_metadata("fm.first.ref").shape.at(1);
const auto reference = reference_frames ? inputs->require_f32("fm.first.ref") : std::vector<float>{};
int64_t valid_reference_frames = 0;
if (reference_frames) {
const auto mask = inputs->require_tensor_data("fm.first.ref_mask");
if (mask.bytes.size() != static_cast<size_t>(reference_frames)) throw std::runtime_error("reference mask size mismatch");
valid_reference_frames = std::count(mask.bytes.begin(), mask.bytes.end(), std::byte{1});
for (int64_t i = 0; i < reference_frames; ++i) {
if (std::to_integer<int>(mask.bytes[i]) != (i < valid_reference_frames)) throw std::runtime_error("reference mask must be a valid prefix");
}
if (instruct) throw std::runtime_error("reference input is not yet wired into the integrated session test");
}
engine::core::ExecutionContext execution({engine::core::BackendType::Cuda, 0, 8});
bool parity_passed = true;
std::unique_ptr<engine::models::auk::AuKQwen25OmniConditioningRuntime> encoder;
std::shared_ptr<engine::tokenizers::LlamaBpeTokenizer> tokenizer;
std::string instruction;
uint64_t seed = 0;
engine::sampling::TorchCudaSamplingPolicy policy;
const auto compare = [&](const std::string & name, const std::vector<float> & actual,
const engine::assets::TensorSource * comparison_source = nullptr) {
const auto reference = (comparison_source ? comparison_source : expected.get())->require_f32(name);
if (actual.size() != reference.size()) throw std::runtime_error(name + " size mismatch");
double dot = 0, norm = 0, reference_norm = 0, error = 0;
for (size_t i = 0; i < actual.size(); ++i) {
if (!std::isfinite(actual[i])) throw std::runtime_error(name + " is non-finite");
dot += double(actual[i]) * reference[i];
norm += double(actual[i]) * actual[i];
reference_norm += double(reference[i]) * reference[i];
const double difference = double(actual[i]) - reference[i];
error += difference * difference;
}
const double cosine = dot / std::sqrt(norm * reference_norm);
std::ostringstream message;
message << std::setprecision(10) << name << " cosine=" << cosine << " RMSE=" << std::sqrt(error / actual.size());
engine::debug::log_message(message.str());
std::cout << message.str() << '\n';
if (!(cosine > 0.99999)) {
parity_passed = false;
engine::debug::log_message(name + " parity gate failed");
if (!sample) throw std::runtime_error(name + " parity gate failed");
}
};
if (instruct) {
const std::filesystem::path qwen_dir = argv[6];
const auto manifest = engine::io::json::parse_file(directory / "manifest.json");
if (manifest.require("dtype").as_string() != "fp32") {
throw std::runtime_error("native instruct integration currently requires an FP32 fixture");
}
instruction = manifest.require("instruction").as_string();
seed = static_cast<uint64_t>(manifest.require("seed").as_i64());
policy = engine::sampling::resolve_torch_cuda_sampling_policy(
engine::core::BackendType::Cuda, 0, "auk", "AuK");
const auto noise = engine::sampling::generate_torch_cuda_tensor_iterator_randn(
audio.size(), static_cast<uint64_t>(manifest.require("seed").as_i64()), 0, policy);
compare("fm.first.x", noise);
if (!parity_passed) throw std::runtime_error("native initial noise parity gate failed");
engine::debug::log_message(noise == audio
? "Native initial noise is bit-identical to Python"
: "Native initial noise passes cosine gate but is not bit-identical to Python");
audio = noise;
engine::tokenizers::LlamaBpeTokenizerSpec spec;
spec.tokenizer_config_path = qwen_dir / "tokenizer_config.json";
spec.tokenizer_json_path = qwen_dir / "tokenizer.json";
spec.pre_type = engine::tokenizers::LlamaBpePreTokenizer::Qwen2;
tokenizer = engine::tokenizers::load_llama_bpe_tokenizer(spec);
const auto condition = engine::models::auk::prepare_conditioning(
*tokenizer, manifest.require("instruction").as_string());
const auto qwen = engine::assets::open_tensor_source(qwen_dir / "model.safetensors.index.json");
encoder = std::make_unique<engine::models::auk::AuKQwen25OmniConditioningRuntime>(execution, *qwen, *source, condition.token_ids.size(), false, false);
text = encoder->encode(condition);
compare("fm.first.text", text);
if (!parity_passed) throw std::runtime_error("conditioning gate failed before sampling");
}
engine::models::auk::FlowRuntime runtime(execution, *source, audio_shape[1], text_shape[1], cfg, !explicit_attention, !sample,
reference_frames, valid_reference_frames);
if (sample) {
const auto vae = engine::assets::open_tensor_source(std::filesystem::path(argv[1]).parent_path() / "vae.safetensors");
engine::models::auk::VAEDecoderRuntime decoder(execution, *vae, audio_shape[1]);
engine::debug::log_message("Teacher-forced VAE before native sampling");
const std::string generated_key = reference_frames ? "vae.normalized_input" : "fm.output";
compare("waveform", decoder.decode(inputs->require_f32(generated_key)));
if (!parity_passed) throw std::runtime_error("teacher-forced VAE gate failed before sampling");
engine::debug::log_message("Native sampling with saved initial noise");
const auto latents = runtime.sample(audio, text, 32, -1.0F, 2.0F, reference);
compare(generated_key, latents);
if (runtime.sample(audio, text, 32, -1.0F, 2.0F, reference) != latents) throw std::runtime_error("repeated sampling changed output");
const auto waveform = decoder.decode(latents);
engine::audio::write_pcm16_wav(directory / (instruct ? "native-instruct.wav" : explicit_attention ? "native-flow-sampled-explicit.wav" : "native-flow-sampled.wav"), 24000, 1, waveform);
compare("waveform", waveform);
if (alternate_fixture) {
if (!parity_passed) throw std::runtime_error("primary parity gate failed before shape switching");
const std::filesystem::path alternate_directory = argv[8];
const auto alternate = engine::assets::open_tensor_source(alternate_directory / "reference.safetensors");
const auto manifest = engine::io::json::parse_file(alternate_directory / "manifest.json");
if (manifest.require("dtype").as_string() != "fp32") throw std::runtime_error("alternate fixture must be FP32");
const auto alternate_shape = alternate->require_metadata("fm.first.x").shape;
if (alternate_shape.size() != 3 || alternate_shape[0] != 1 || alternate_shape[2] != 64 ||
alternate_shape[1] == audio_shape[1]) throw std::runtime_error("alternate fixture must change valid audio length");
const auto condition = engine::models::auk::prepare_conditioning(
*tokenizer, manifest.require("instruction").as_string());
const auto alternate_noise = engine::sampling::generate_torch_cuda_tensor_iterator_randn(
alternate_shape[1] * 64, static_cast<uint64_t>(manifest.require("seed").as_i64()), 0, policy);
compare("fm.first.x", alternate_noise, alternate.get());
if (!parity_passed) throw std::runtime_error("alternate noise parity gate failed");
for (int cycle = 0; cycle < 3; ++cycle) {
encoder->prepare(condition.token_ids.size());
const auto conditioning = encoder->encode(condition);
compare("fm.first.text", conditioning, alternate.get());
decoder.prepare(alternate_shape[1]);
compare("waveform", decoder.decode(alternate->require_f32("fm.output")), alternate.get());
if (!parity_passed) throw std::runtime_error("alternate deterministic component gate failed");
runtime.prepare(alternate_shape[1], condition.token_ids.size());
const auto generated = runtime.sample(alternate_noise, conditioning);
compare("fm.output", generated, alternate.get());
const auto alternate_wave = decoder.decode(generated);
compare("waveform", alternate_wave, alternate.get());
if (!parity_passed) throw std::runtime_error("alternate sampling parity gate failed");
runtime.prepare(alternate_shape[1], condition.token_ids.size());
if (runtime.sample(alternate_noise, conditioning) != generated) throw std::runtime_error("same-shape flow prepare changed output");
encoder->prepare(text_shape[1]);
const auto primary_condition = engine::models::auk::prepare_conditioning(*tokenizer, instruction);
if (encoder->encode(primary_condition) != text) throw std::runtime_error("restored conditioning changed output");
runtime.prepare(audio_shape[1], text_shape[1]);
if (runtime.sample(audio, text) != latents) throw std::runtime_error("restored flow changed output");
decoder.prepare(audio_shape[1]);
if (decoder.decode(latents) != waveform) throw std::runtime_error("restored waveform changed output");
const auto memory = execution.memory_snapshot();
if (memory.available) engine::debug::trace_log_scalar("auk.instruct.cycle_device_used_bytes", memory.used_bytes);
}
engine::debug::log_message("AuK full instruct shape switching passed three round trips");
}
if (benchmark_runs && !parity_passed) throw std::runtime_error("parity gate failed before benchmark");
std::vector<double> timings;
for (int run = 0; run < benchmark_runs + (benchmark_runs ? 2 : 0); ++run) {
const auto start = std::chrono::steady_clock::now();
const auto condition = engine::models::auk::prepare_conditioning(*tokenizer, instruction);
const auto conditioning = encoder->encode(condition);
const auto encoded = std::chrono::steady_clock::now();
const auto noise = engine::sampling::generate_torch_cuda_tensor_iterator_randn(audio.size(), seed, 0, policy);
const auto generated = runtime.sample(noise, conditioning);
const auto sampled = std::chrono::steady_clock::now();
const auto repeated = decoder.decode(generated);
const auto done = std::chrono::steady_clock::now();
if (repeated != waveform) throw std::runtime_error("benchmark waveform changed");
if (run < 2) continue;
const double total = std::chrono::duration<double, std::milli>(done - start).count();
timings.push_back(total);
engine::debug::log_message("AuK inference_ms=" + std::to_string(total) +
" conditioning_ms=" + std::to_string(std::chrono::duration<double, std::milli>(encoded - start).count()) +
" sampling_ms=" + std::to_string(std::chrono::duration<double, std::milli>(sampled - encoded).count()) +
" vae_ms=" + std::to_string(std::chrono::duration<double, std::milli>(done - sampled).count()));
}
if (!timings.empty()) {
std::sort(timings.begin(), timings.end());
const double median = (timings[(timings.size() - 1) / 2] + timings[timings.size() / 2]) * 0.5;
engine::debug::log_message("AuK inference_median_ms=" + std::to_string(median) +
" RTF=" + std::to_string(median * 24.0 / waveform.size()));
}
return parity_passed ? 0 : 1;
}
const float times[] = {0.0F, 0.25F, 0.75F};
for (int step = 0; step < 3; ++step) {
const auto result = runtime.embed(audio, text, times[step], 2.0F, reference);
compare("time." + std::to_string(step), result.time);
compare("text", result.text);
compare("audio", result.audio);
for (const auto & entry : result.first_block_boundaries) {
compare("first." + entry.first + "." + std::to_string(step), entry.second);
}
compare("block.audio." + std::to_string(step), result.first_block_audio);
compare("block.text." + std::to_string(step), result.first_block_text);
compare("double.audio." + std::to_string(step), result.double_stream_audio);
compare("double.text." + std::to_string(step), result.double_stream_text);
compare("single." + std::to_string(step), result.single_stream);
compare("velocity." + std::to_string(step), result.velocity);
if (cfg) {
const auto middle = result.velocity.begin() + result.velocity.size() / 2;
compare("conditional." + std::to_string(step), {result.velocity.begin(), middle});
compare("unconditional." + std::to_string(step), {middle, result.velocity.end()});
compare("guided." + std::to_string(step), result.guided_velocity);
} else if (result.guided_velocity != result.velocity) {
throw std::runtime_error("unguided velocity changed");
}
const auto repeated = runtime.embed(audio, text, times[step], 2.0F, reference);
if (repeated.time != result.time || repeated.text != result.text || repeated.audio != result.audio ||
repeated.first_block_audio != result.first_block_audio || repeated.first_block_text != result.first_block_text ||
repeated.double_stream_audio != result.double_stream_audio || repeated.double_stream_text != result.double_stream_text ||
repeated.single_stream != result.single_stream || repeated.velocity != result.velocity ||
repeated.guided_velocity != result.guided_velocity) {
throw std::runtime_error("flow embedding reuse changed output");
}
}
return 0;
} catch (const std::exception & error) {
engine::debug::log_message(error.what());
std::cerr << error.what() << '\n';
return 1;
}
}