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
1026 lines
41 KiB
C++
1026 lines
41 KiB
C++
#include "engine/framework/audio/chunking.h"
|
|
|
|
#include "test_assert.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
engine::runtime::SpeechSegment speech(int64_t start, int64_t end) {
|
|
engine::runtime::SpeechSegment segment;
|
|
segment.span.start_sample = start;
|
|
segment.span.end_sample = end;
|
|
segment.confidence = 1.0F;
|
|
return segment;
|
|
}
|
|
|
|
engine::runtime::WordTimestamp word(const std::string & text, int64_t start, int64_t end) {
|
|
engine::runtime::WordTimestamp timestamp;
|
|
timestamp.word = text;
|
|
timestamp.span.start_sample = start;
|
|
timestamp.span.end_sample = end;
|
|
return timestamp;
|
|
}
|
|
|
|
class MockVadSession final : public engine::runtime::IOfflineVoiceTaskSession {
|
|
public:
|
|
std::string family() const override {
|
|
return "mock_vad";
|
|
}
|
|
|
|
engine::runtime::VoiceTaskKind task_kind() const override {
|
|
return engine::runtime::VoiceTaskKind::Vad;
|
|
}
|
|
|
|
engine::runtime::RunMode run_mode() const override {
|
|
return engine::runtime::RunMode::Offline;
|
|
}
|
|
|
|
void prepare(const engine::runtime::SessionPreparationRequest & request) override {
|
|
engine::test::require(request.audio.has_value(), "mock VAD prepare audio contract");
|
|
prepared = true;
|
|
}
|
|
|
|
engine::runtime::TaskResult run(const engine::runtime::TaskRequest & request) override {
|
|
engine::test::require(prepared, "mock VAD run after prepare");
|
|
engine::test::require(request.audio_input.has_value(), "mock VAD run audio input");
|
|
++runs;
|
|
engine::runtime::TaskResult result;
|
|
result.speech_segments = segments;
|
|
return result;
|
|
}
|
|
|
|
bool prepared = false;
|
|
int runs = 0;
|
|
std::vector<engine::runtime::SpeechSegment> segments;
|
|
};
|
|
|
|
void require_span(
|
|
const engine::runtime::TimeSpan & span,
|
|
int64_t expected_start,
|
|
int64_t expected_end,
|
|
const std::string & label) {
|
|
engine::test::require_eq(span.start_sample, expected_start, label + " start");
|
|
engine::test::require_eq(span.end_sample, expected_end, label + " end");
|
|
}
|
|
|
|
void require_throws(const std::function<void()> & fn, const std::string & label) {
|
|
bool threw = false;
|
|
try {
|
|
fn();
|
|
} catch (const std::runtime_error &) {
|
|
threw = true;
|
|
}
|
|
engine::test::require(threw, label + " did not throw");
|
|
}
|
|
|
|
void require_chunk_invariants(
|
|
const std::vector<engine::runtime::TimeSpan> & chunks,
|
|
int64_t audio_samples,
|
|
int64_t max_chunk_samples,
|
|
const std::string & label) {
|
|
int64_t previous_end = 0;
|
|
for (size_t i = 0; i < chunks.size(); ++i) {
|
|
const auto & chunk = chunks[i];
|
|
engine::test::require(chunk.start_sample >= 0, label + " chunk start before zero");
|
|
engine::test::require(chunk.end_sample <= audio_samples, label + " chunk end outside audio");
|
|
engine::test::require(chunk.start_sample < chunk.end_sample, label + " empty chunk");
|
|
engine::test::require(chunk.start_sample >= previous_end, label + " chunks overlap or are unsorted");
|
|
engine::test::require(
|
|
chunk.end_sample - chunk.start_sample <= max_chunk_samples,
|
|
label + " chunk exceeds max length");
|
|
previous_end = chunk.end_sample;
|
|
}
|
|
}
|
|
|
|
void require_range_covered(
|
|
const std::vector<engine::runtime::TimeSpan> & chunks,
|
|
int64_t start,
|
|
int64_t end,
|
|
const std::string & label) {
|
|
int64_t cursor = start;
|
|
for (const auto & chunk : chunks) {
|
|
if (chunk.end_sample <= cursor) {
|
|
continue;
|
|
}
|
|
if (chunk.start_sample > cursor) {
|
|
break;
|
|
}
|
|
cursor = std::max(cursor, std::min(chunk.end_sample, end));
|
|
if (cursor >= end) {
|
|
return;
|
|
}
|
|
}
|
|
engine::test::require(false, label + " range is not fully covered");
|
|
}
|
|
|
|
void require_no_forbidden_gap_inside_chunks(
|
|
const std::vector<engine::runtime::TimeSpan> & chunks,
|
|
const std::vector<engine::runtime::TimeSpan> & speech_runs,
|
|
int64_t allowed_gap,
|
|
const std::string & label) {
|
|
for (const auto & chunk : chunks) {
|
|
for (size_t i = 1; i < speech_runs.size(); ++i) {
|
|
const int64_t gap_start = speech_runs[i - 1].end_sample;
|
|
const int64_t gap_end = speech_runs[i].start_sample;
|
|
const int64_t gap = gap_end - gap_start;
|
|
if (gap > allowed_gap &&
|
|
chunk.start_sample < gap_start &&
|
|
chunk.end_sample > gap_end) {
|
|
engine::test::require(false, label + " chunk crosses forbidden silence gap");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void test_fixed_chunks_plan_start_aligned_tail() {
|
|
const engine::audio::AudioChunkSpec spec{
|
|
4,
|
|
4,
|
|
engine::audio::AudioChunkPadMode::Zero,
|
|
engine::audio::AudioChunkTailAlignment::Start,
|
|
0,
|
|
};
|
|
const auto spans = engine::audio::plan_audio_chunks(10, spec);
|
|
engine::test::require_eq(spans.size(), static_cast<size_t>(3), "fixed start-aligned span count");
|
|
engine::test::require_eq(spans[0].index, 0, "span 0 index");
|
|
engine::test::require_eq(spans[0].output_start_sample, 0, "span 0 output start");
|
|
engine::test::require_eq(spans[0].copy_start_sample, 0, "span 0 copy start");
|
|
engine::test::require_eq(spans[0].valid_start_in_chunk, 0, "span 0 valid start");
|
|
engine::test::require_eq(spans[0].valid_samples, 4, "span 0 valid samples");
|
|
engine::test::require_eq(spans[1].output_start_sample, 4, "span 1 output start");
|
|
engine::test::require_eq(spans[1].valid_samples, 4, "span 1 valid samples");
|
|
engine::test::require_eq(spans[2].output_start_sample, 8, "span 2 output start");
|
|
engine::test::require_eq(spans[2].copy_start_sample, 8, "span 2 copy start");
|
|
engine::test::require_eq(spans[2].valid_start_in_chunk, 0, "span 2 valid start");
|
|
engine::test::require_eq(spans[2].valid_samples, 2, "span 2 valid samples");
|
|
}
|
|
|
|
void test_fixed_chunks_plan_overlapping_windows() {
|
|
const engine::audio::AudioChunkSpec spec{
|
|
4,
|
|
2,
|
|
engine::audio::AudioChunkPadMode::Zero,
|
|
engine::audio::AudioChunkTailAlignment::Start,
|
|
0,
|
|
};
|
|
const auto spans = engine::audio::plan_audio_chunks(9, spec);
|
|
engine::test::require_eq(spans.size(), static_cast<size_t>(5), "overlap fixed span count");
|
|
require_span({spans[0].output_start_sample, spans[0].output_start_sample + spans[0].valid_samples}, 0, 4, "overlap span 0 output");
|
|
require_span({spans[1].output_start_sample, spans[1].output_start_sample + spans[1].valid_samples}, 2, 6, "overlap span 1 output");
|
|
require_span({spans[2].output_start_sample, spans[2].output_start_sample + spans[2].valid_samples}, 4, 8, "overlap span 2 output");
|
|
require_span({spans[3].output_start_sample, spans[3].output_start_sample + spans[3].valid_samples}, 6, 9, "overlap span 3 output");
|
|
require_span({spans[4].output_start_sample, spans[4].output_start_sample + spans[4].valid_samples}, 8, 9, "overlap span 4 output");
|
|
for (size_t i = 0; i < spans.size(); ++i) {
|
|
engine::test::require_eq(spans[i].index, static_cast<int64_t>(i), "overlap span index " + std::to_string(i));
|
|
engine::test::require(spans[i].valid_samples > 0, "overlap span has no valid samples");
|
|
engine::test::require(spans[i].valid_start_in_chunk >= 0, "overlap span valid start before zero");
|
|
engine::test::require(
|
|
spans[i].valid_start_in_chunk + spans[i].valid_samples <= spec.chunk_samples,
|
|
"overlap span valid range exceeds chunk");
|
|
}
|
|
}
|
|
|
|
void test_fixed_chunks_plan_centered_tail_and_copy_zero_pad() {
|
|
const engine::audio::AudioChunkSpec spec{
|
|
4,
|
|
4,
|
|
engine::audio::AudioChunkPadMode::Zero,
|
|
engine::audio::AudioChunkTailAlignment::Center,
|
|
0,
|
|
};
|
|
const auto spans = engine::audio::plan_audio_chunks(10, spec);
|
|
engine::test::require_eq(spans.size(), static_cast<size_t>(3), "fixed centered span count");
|
|
engine::test::require_eq(spans[2].output_start_sample, 8, "centered tail output start");
|
|
engine::test::require_eq(spans[2].copy_start_sample, 7, "centered tail copy start");
|
|
engine::test::require_eq(spans[2].valid_start_in_chunk, 1, "centered tail valid start");
|
|
engine::test::require_eq(spans[2].valid_samples, 2, "centered tail valid samples");
|
|
|
|
const std::vector<float> input{
|
|
0.0F, 10.0F,
|
|
1.0F, 11.0F,
|
|
2.0F, 12.0F,
|
|
3.0F, 13.0F,
|
|
4.0F, 14.0F,
|
|
5.0F, 15.0F,
|
|
6.0F, 16.0F,
|
|
7.0F, 17.0F,
|
|
8.0F, 18.0F,
|
|
9.0F, 19.0F,
|
|
};
|
|
std::vector<float> output(8, -1.0F);
|
|
engine::audio::copy_interleaved_chunk_to_planar(output, input, 2, 10, spans[2], spec);
|
|
const std::vector<float> expected{
|
|
7.0F, 8.0F, 9.0F, 0.0F,
|
|
17.0F, 18.0F, 19.0F, 0.0F,
|
|
};
|
|
engine::test::require_eq(output.size(), expected.size(), "centered copy output size");
|
|
for (size_t i = 0; i < output.size(); ++i) {
|
|
engine::test::require_eq(output[i], expected[i], "centered copy sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
void test_copy_planar_chunk_reflect_padding() {
|
|
const engine::audio::AudioChunkSpec spec{
|
|
5,
|
|
5,
|
|
engine::audio::AudioChunkPadMode::Reflect,
|
|
engine::audio::AudioChunkTailAlignment::Start,
|
|
0,
|
|
};
|
|
const engine::audio::AudioChunkSpan span{
|
|
0,
|
|
0,
|
|
3,
|
|
-2,
|
|
2,
|
|
};
|
|
const std::vector<float> input{
|
|
10.0F, 11.0F, 12.0F, 13.0F,
|
|
20.0F, 21.0F, 22.0F, 23.0F,
|
|
};
|
|
std::vector<float> output(10, -1.0F);
|
|
engine::audio::copy_planar_chunk(output, input, 2, 4, span, spec);
|
|
const std::vector<float> expected{
|
|
12.0F, 11.0F, 10.0F, 11.0F, 12.0F,
|
|
22.0F, 21.0F, 20.0F, 21.0F, 22.0F,
|
|
};
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
engine::test::require_eq(output[i], expected[i], "reflect planar sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
void test_overlap_add_and_normalize_shared_counter() {
|
|
std::vector<float> output(6, 0.0F);
|
|
std::vector<float> weights(6, 0.0F);
|
|
const std::vector<float> window{1.0F, 0.5F, 0.5F, 1.0F};
|
|
const engine::audio::AudioChunkSpan first{
|
|
0,
|
|
0,
|
|
4,
|
|
0,
|
|
0,
|
|
};
|
|
const engine::audio::AudioChunkSpan second{
|
|
1,
|
|
2,
|
|
4,
|
|
2,
|
|
0,
|
|
};
|
|
engine::audio::overlap_add_planar_chunk(
|
|
output,
|
|
weights,
|
|
std::vector<float>{1.0F, 2.0F, 3.0F, 4.0F},
|
|
1,
|
|
6,
|
|
first,
|
|
window,
|
|
engine::audio::AudioChunkCounterMode::SharedAcrossLanes);
|
|
engine::audio::overlap_add_planar_chunk(
|
|
output,
|
|
weights,
|
|
std::vector<float>{10.0F, 20.0F, 30.0F, 40.0F},
|
|
1,
|
|
6,
|
|
second,
|
|
window,
|
|
engine::audio::AudioChunkCounterMode::SharedAcrossLanes);
|
|
engine::audio::normalize_overlap_added_planar(
|
|
output,
|
|
weights,
|
|
1,
|
|
6,
|
|
engine::audio::AudioChunkCounterMode::SharedAcrossLanes);
|
|
const std::vector<float> expected{
|
|
1.0F,
|
|
2.0F,
|
|
(3.0F * 0.5F + 10.0F * 1.0F) / 1.5F,
|
|
(4.0F * 1.0F + 20.0F * 0.5F) / 1.5F,
|
|
30.0F,
|
|
40.0F,
|
|
};
|
|
for (size_t i = 0; i < expected.size(); ++i) {
|
|
engine::test::require_close(output[i], expected[i], 1.0e-6F, "overlap-add sample " + std::to_string(i));
|
|
}
|
|
}
|
|
|
|
void test_fixed_chunks_validate_inputs() {
|
|
require_throws([]() {
|
|
(void) engine::audio::plan_audio_chunks(0, {4, 4});
|
|
}, "zero fixed chunk input samples");
|
|
require_throws([]() {
|
|
(void) engine::audio::plan_audio_chunks(10, {0, 4});
|
|
}, "zero fixed chunk size");
|
|
require_throws([]() {
|
|
(void) engine::audio::plan_audio_chunks(10, {4, 0});
|
|
}, "zero fixed hop size");
|
|
require_throws([]() {
|
|
(void) engine::audio::plan_audio_chunks(10, {4, 5});
|
|
}, "fixed hop larger than chunk");
|
|
require_throws([]() {
|
|
const std::vector<float> input{1.0F, 2.0F, 3.0F};
|
|
std::vector<float> output(4, 0.0F);
|
|
engine::audio::copy_planar_chunk(output, input, 2, 2, {0, 0, 2, 0, 0}, {2, 2});
|
|
}, "copy planar input size mismatch");
|
|
require_throws([]() {
|
|
std::vector<float> output(3, 0.0F);
|
|
std::vector<float> weights(2, 0.0F);
|
|
engine::audio::overlap_add_planar_chunk(
|
|
output,
|
|
weights,
|
|
std::vector<float>{1.0F, 2.0F},
|
|
1,
|
|
3,
|
|
{0, 2, 2, 0, 0},
|
|
std::vector<float>{1.0F, 1.0F},
|
|
engine::audio::AudioChunkCounterMode::SharedAcrossLanes);
|
|
}, "overlap-add span past output");
|
|
}
|
|
|
|
void test_vad_chunks_sort_pad_and_merge() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(400, 500),
|
|
speech(100, 200),
|
|
speech(225, 260),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
1000,
|
|
40,
|
|
10,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "sort-pad-merge");
|
|
require_span(chunks[0], 90, 270, "first VAD chunk");
|
|
require_span(chunks[1], 390, 510, "second VAD chunk");
|
|
require_range_covered(chunks, 90, 210, "first padded segment");
|
|
require_range_covered(chunks, 215, 270, "second padded segment");
|
|
require_range_covered(chunks, 390, 510, "third padded segment");
|
|
}
|
|
|
|
void test_vad_chunks_clamp_padding_to_audio_bounds() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(5, 20),
|
|
speech(985, 995),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
200,
|
|
0,
|
|
20,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "clamped VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "clamped-padding");
|
|
require_span(chunks[0], 0, 40, "left-clamped VAD chunk");
|
|
require_span(chunks[1], 965, 1000, "right-clamped VAD chunk");
|
|
}
|
|
|
|
void test_vad_chunks_respect_max_when_merging() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(0, 100),
|
|
speech(150, 250),
|
|
speech(300, 400),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
260,
|
|
100,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "max-bounded VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "max-bounded");
|
|
require_span(chunks[0], 0, 250, "max-bounded first chunk");
|
|
require_span(chunks[1], 300, 400, "max-bounded second chunk");
|
|
}
|
|
|
|
void test_vad_chunks_merge_gap_boundary() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(0, 100),
|
|
speech(150, 200),
|
|
speech(251, 300),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
500,
|
|
50,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "gap-boundary VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "gap-boundary");
|
|
require_span(chunks[0], 0, 200, "gap equal to threshold should merge");
|
|
require_span(chunks[1], 251, 300, "gap above threshold should not merge");
|
|
}
|
|
|
|
void test_vad_chunks_split_long_speech() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(0, 1000),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
400,
|
|
0,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(3), "split VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "split-long");
|
|
require_span(chunks[0], 0, 400, "split first chunk");
|
|
require_span(chunks[1], 400, 800, "split second chunk");
|
|
require_span(chunks[2], 800, 1000, "split third chunk");
|
|
require_range_covered(chunks, 0, 1000, "long speech");
|
|
}
|
|
|
|
void test_vad_chunks_do_not_overlap_when_max_blocks_merge() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(0, 300),
|
|
speech(250, 500),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
400,
|
|
100,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "non-overlap chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "non-overlap");
|
|
require_span(chunks[0], 0, 400, "non-overlap first chunk");
|
|
require_span(chunks[1], 400, 500, "non-overlap second chunk");
|
|
require_range_covered(chunks, 0, 500, "overlapping input speech");
|
|
}
|
|
|
|
void test_vad_chunks_nested_segments_keep_full_coverage() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 700),
|
|
speech(200, 300),
|
|
speech(650, 900),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
500,
|
|
100,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "nested VAD chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "nested");
|
|
require_span(chunks[0], 100, 600, "nested first chunk");
|
|
require_span(chunks[1], 600, 900, "nested second chunk");
|
|
require_range_covered(chunks, 100, 900, "nested and overlapping speech");
|
|
}
|
|
|
|
void test_vad_chunks_padding_overlap_merges_without_duplicate_coverage() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 150),
|
|
speech(175, 220),
|
|
speech(245, 280),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
500,
|
|
0,
|
|
20,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(1), "padding-overlap chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "padding-overlap");
|
|
require_span(chunks[0], 80, 300, "padding-overlap merged chunk");
|
|
require_range_covered(chunks, 80, 300, "padding-overlap speech run");
|
|
}
|
|
|
|
void test_vad_chunks_keep_large_silence_gap_outside_chunks() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 200),
|
|
speech(500, 600),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
1000,
|
|
100,
|
|
20,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "large-gap chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "large-gap");
|
|
require_span(chunks[0], 80, 220, "large-gap first chunk");
|
|
require_span(chunks[1], 480, 620, "large-gap second chunk");
|
|
require_no_forbidden_gap_inside_chunks(chunks, {{80, 220}, {480, 620}}, options.merge_gap_samples, "large-gap");
|
|
}
|
|
|
|
void test_vad_chunks_close_run_split_at_max_without_losing_speech() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(0, 300),
|
|
speech(350, 650),
|
|
speech(700, 1000),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
450,
|
|
50,
|
|
0,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(3), "close-run split chunk count");
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "close-run split");
|
|
require_range_covered(chunks, 0, 300, "close-run first speech");
|
|
require_range_covered(chunks, 350, 650, "close-run second speech");
|
|
require_range_covered(chunks, 700, 1000, "close-run third speech");
|
|
require_no_forbidden_gap_inside_chunks(chunks, {{0, 300}, {350, 650}, {700, 1000}}, options.merge_gap_samples, "close-run split");
|
|
}
|
|
|
|
void test_vad_chunks_stress_mixed_unsorted_segments() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(950, 990),
|
|
speech(120, 200),
|
|
speech(500, 900),
|
|
speech(180, 260),
|
|
speech(420, 520),
|
|
speech(0, 30),
|
|
};
|
|
const engine::audio::VadAudioChunkOptions options{
|
|
250,
|
|
50,
|
|
10,
|
|
};
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(segments, 1000, options);
|
|
require_chunk_invariants(chunks, 1000, options.max_chunk_samples, "stress");
|
|
require_range_covered(chunks, 0, 40, "stress first padded speech");
|
|
require_range_covered(chunks, 110, 270, "stress merged early speech");
|
|
require_range_covered(chunks, 410, 910, "stress long overlapping speech");
|
|
require_range_covered(chunks, 940, 1000, "stress final padded speech");
|
|
}
|
|
|
|
void test_vad_chunks_validate_inputs() {
|
|
const auto empty_chunks = engine::audio::plan_vad_audio_chunks({}, 1000, {400, 0, 0});
|
|
engine::test::require(empty_chunks.empty(), "empty VAD segments should produce no chunks");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 100),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {400, 0, 0});
|
|
}, "invalid speech segment");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 200),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {0, 0, 0});
|
|
}, "invalid max chunk size");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 200),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {400, -1, 0});
|
|
}, "negative merge gap");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 200),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {400, 0, -1});
|
|
}, "negative padding");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(900, 1001),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {400, 0, 0});
|
|
}, "speech segment past audio end");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(-1, 10),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 1000, {400, 0, 0});
|
|
}, "speech segment before audio start");
|
|
|
|
require_throws([]() {
|
|
const std::vector<engine::runtime::SpeechSegment> segments{
|
|
speech(100, 200),
|
|
};
|
|
(void) engine::audio::plan_vad_audio_chunks(segments, 0, {400, 0, 0});
|
|
}, "invalid audio length");
|
|
}
|
|
|
|
void test_vad_session_planner_runs_vad_internally() {
|
|
engine::runtime::AudioBuffer audio;
|
|
audio.sample_rate = 100;
|
|
audio.channels = 1;
|
|
audio.samples.assign(1000, 0.0F);
|
|
|
|
MockVadSession vad;
|
|
vad.segments = {
|
|
speech(100, 200),
|
|
speech(230, 300),
|
|
speech(700, 760),
|
|
};
|
|
|
|
const auto chunks = engine::audio::plan_vad_audio_chunks(audio, vad, {250, 50, 10});
|
|
engine::test::require(vad.prepared, "mock VAD was prepared");
|
|
engine::test::require_eq(vad.runs, 1, "mock VAD run count");
|
|
engine::test::require_eq(chunks.size(), static_cast<size_t>(2), "mock VAD planned chunk count");
|
|
require_span(chunks[0], 90, 310, "mock VAD first chunk");
|
|
require_span(chunks[1], 690, 770, "mock VAD second chunk");
|
|
}
|
|
|
|
void test_audio_chunk_mode_parser() {
|
|
std::unordered_map<std::string, std::string> options;
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::Auto,
|
|
"default audio chunk mode is auto");
|
|
options["audio_chunk_mode"] = "fixed";
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::Fixed,
|
|
"fixed audio chunk mode");
|
|
options["audio_chunk_mode"] = "vad";
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::Vad,
|
|
"vad audio chunk mode");
|
|
options["audio_chunk_mode"] = "silence";
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::Silence,
|
|
"parse silence mode");
|
|
options["audio_chunk_mode"] = "quiet_energy";
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::QuietEnergy,
|
|
"quiet-energy audio chunk mode");
|
|
options["audio_chunk_mode"] = "none";
|
|
engine::test::require(
|
|
engine::audio::parse_audio_chunk_mode(options) == engine::audio::AudioChunkMode::None,
|
|
"none audio chunk mode");
|
|
options["audio_chunk_mode"] = "json";
|
|
require_throws([&]() {
|
|
(void) engine::audio::parse_audio_chunk_mode(options);
|
|
}, "invalid audio chunk mode");
|
|
}
|
|
|
|
void test_slice_audio_buffer_preserves_channels() {
|
|
engine::runtime::AudioBuffer audio;
|
|
audio.sample_rate = 16000;
|
|
audio.channels = 2;
|
|
audio.samples = {
|
|
0.0F, 0.1F,
|
|
1.0F, 1.1F,
|
|
2.0F, 2.1F,
|
|
3.0F, 3.1F,
|
|
};
|
|
const auto slice = engine::audio::slice_audio_buffer(audio, {1, 3});
|
|
engine::test::require_eq(slice.sample_rate, 16000, "slice sample rate");
|
|
engine::test::require_eq(slice.channels, 2, "slice channels");
|
|
engine::test::require_eq(slice.samples.size(), static_cast<size_t>(4), "slice sample count");
|
|
engine::test::require_eq(slice.samples[0], 1.0F, "slice sample 0");
|
|
engine::test::require_eq(slice.samples[1], 1.1F, "slice sample 1");
|
|
engine::test::require_eq(slice.samples[2], 2.0F, "slice sample 2");
|
|
engine::test::require_eq(slice.samples[3], 2.1F, "slice sample 3");
|
|
}
|
|
|
|
void test_slice_audio_buffer_validate_inputs() {
|
|
engine::runtime::AudioBuffer audio;
|
|
audio.sample_rate = 16000;
|
|
audio.channels = 2;
|
|
audio.samples = {
|
|
0.0F, 0.1F,
|
|
1.0F,
|
|
};
|
|
require_throws([&]() {
|
|
(void) engine::audio::slice_audio_buffer(audio, {0, 1});
|
|
}, "slice non-divisible interleaved audio");
|
|
|
|
audio.samples = {
|
|
0.0F, 0.1F,
|
|
1.0F, 1.1F,
|
|
};
|
|
require_throws([&]() {
|
|
(void) engine::audio::slice_audio_buffer(audio, {1, 1});
|
|
}, "slice empty span");
|
|
require_throws([&]() {
|
|
(void) engine::audio::slice_audio_buffer(audio, {-1, 1});
|
|
}, "slice negative start");
|
|
require_throws([&]() {
|
|
(void) engine::audio::slice_audio_buffer(audio, {0, 3});
|
|
}, "slice past audio end");
|
|
|
|
audio.sample_rate = 0;
|
|
require_throws([&]() {
|
|
(void) engine::audio::slice_audio_buffer(audio, {0, 1});
|
|
}, "slice invalid sample rate");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_offsets_and_clips() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
const std::vector<engine::runtime::WordTimestamp> local{
|
|
word("left", -10, 30),
|
|
word("middle", 40, 80),
|
|
word("right", 90, 130),
|
|
};
|
|
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
local,
|
|
engine::runtime::TimeSpan{1000, 1100});
|
|
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(3), "merged chunk word count");
|
|
engine::test::require_eq(merged[0].word, std::string("left"), "left word kept");
|
|
require_span(merged[0].span, 1000, 1030, "left word clipped");
|
|
engine::test::require_eq(merged[1].word, std::string("middle"), "middle word kept");
|
|
require_span(merged[1].span, 1040, 1080, "middle word offset");
|
|
engine::test::require_eq(merged[2].word, std::string("right"), "right word kept");
|
|
require_span(merged[2].span, 1090, 1100, "right word clipped");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_appends_multiple_chunks() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("alpha", 5, 25), word("beta", 30, 50)},
|
|
engine::runtime::TimeSpan{0, 100});
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("gamma", 0, 20)},
|
|
engine::runtime::TimeSpan{100, 150});
|
|
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(3), "multi-chunk merged word count");
|
|
require_span(merged[0].span, 5, 25, "first chunk alpha");
|
|
require_span(merged[1].span, 30, 50, "first chunk beta");
|
|
require_span(merged[2].span, 100, 120, "second chunk gamma");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_keeps_non_overlapping_source_span() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{
|
|
word("left_context", 20, 60),
|
|
word("kept_boundary", 100, 180),
|
|
word("kept_inside", 260, 320),
|
|
word("right_context", 400, 450),
|
|
},
|
|
engine::runtime::TimeSpan{900, 1400},
|
|
engine::runtime::TimeSpan{1000, 1300});
|
|
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(2), "keep span merged word count");
|
|
engine::test::require_eq(merged[0].word, std::string("kept_boundary"), "boundary word kept");
|
|
require_span(merged[0].span, 1000, 1080, "boundary word full source span");
|
|
engine::test::require_eq(merged[1].word, std::string("kept_inside"), "inside word kept");
|
|
require_span(merged[1].span, 1160, 1220, "inside word source span");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_rescales_chunk_domain() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{
|
|
word("kept", 12000, 18000),
|
|
word("right_context", 20000, 23000),
|
|
},
|
|
engine::runtime::TimeSpan{16000, 32000},
|
|
engine::runtime::TimeSpan{20000, 28000},
|
|
16000,
|
|
24000);
|
|
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(1), "rescaled merged word count");
|
|
engine::test::require_eq(merged[0].word, std::string("kept"), "rescaled word kept");
|
|
require_span(merged[0].span, 36000, 42000, "rescaled word timestamp");
|
|
}
|
|
|
|
// The Qwen3 ASR forced aligner now returns spans in the uploaded audio's own
|
|
// sample domain, so the chunk merge must not rescale them to the frontend rate.
|
|
void test_chunk_word_timestamp_merge_keeps_input_rate_domain() {
|
|
const int64_t rate = 24000;
|
|
const int64_t chunk_samples = 12 * rate;
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
for (int64_t chunk = 0; chunk < 4; ++chunk) {
|
|
const int64_t begin = chunk * chunk_samples;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("first", 0, 300), word("last", chunk_samples - 300, chunk_samples)},
|
|
engine::runtime::TimeSpan{begin, begin + chunk_samples},
|
|
engine::runtime::TimeSpan{begin, begin + chunk_samples},
|
|
rate,
|
|
rate);
|
|
}
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(8), "input-rate merged word count");
|
|
require_span(merged.back().span, 48 * rate - 300, 48 * rate, "last word stays in the input rate");
|
|
}
|
|
|
|
void test_chunk_speech_metadata_merge_rescales_chunk_domain() {
|
|
engine::runtime::TaskResult chunk;
|
|
auto segment = speech(0, 24000);
|
|
segment.text = "speaker text";
|
|
chunk.speech_segments.push_back(segment);
|
|
engine::runtime::SpeakerTurn turn;
|
|
turn.span = engine::runtime::TimeSpan{12000, 18000};
|
|
turn.speaker_id = "SPEAKER_00";
|
|
turn.text = "turn text";
|
|
chunk.speaker_turns.push_back(turn);
|
|
chunk.word_timestamps.push_back(word("word", 12000, 18000));
|
|
|
|
engine::runtime::TaskResult merged;
|
|
engine::audio::append_chunk_speech_metadata(
|
|
merged,
|
|
chunk,
|
|
engine::runtime::TimeSpan{16000, 32000},
|
|
engine::runtime::TimeSpan{16000, 32000},
|
|
16000,
|
|
24000);
|
|
|
|
engine::test::require_eq(merged.speech_segments.size(), static_cast<size_t>(1), "rescaled speech segment count");
|
|
require_span(merged.speech_segments[0].span, 24000, 48000, "rescaled speech segment");
|
|
engine::test::require_eq(merged.speech_segments[0].text, std::string("speaker text"), "speech segment text kept");
|
|
engine::test::require_eq(merged.speaker_turns.size(), static_cast<size_t>(1), "rescaled speaker turn count");
|
|
require_span(merged.speaker_turns[0].span, 36000, 42000, "rescaled speaker turn");
|
|
engine::test::require_eq(merged.speaker_turns[0].speaker_id, std::string("SPEAKER_00"), "speaker id kept");
|
|
engine::test::require_eq(merged.speaker_turns[0].text, std::string("turn text"), "speaker turn text kept");
|
|
engine::test::require_eq(merged.word_timestamps.size(), static_cast<size_t>(1), "rescaled word count");
|
|
require_span(merged.word_timestamps[0].span, 36000, 42000, "rescaled metadata word");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_rejects_invalid_spans() {
|
|
require_throws(
|
|
[]() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("bad", 20, 10)},
|
|
engine::runtime::TimeSpan{0, 100});
|
|
},
|
|
"inverted word timestamp");
|
|
|
|
require_throws(
|
|
[]() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("bad", 0, 10)},
|
|
engine::runtime::TimeSpan{100, 90});
|
|
},
|
|
"inverted chunk span");
|
|
|
|
require_throws(
|
|
[]() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("bad", 0, 10)},
|
|
engine::runtime::TimeSpan{1000, 1100},
|
|
engine::runtime::TimeSpan{900, 1100});
|
|
},
|
|
"keep span outside source");
|
|
|
|
require_throws(
|
|
[]() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{word("bad", 0, 10)},
|
|
engine::runtime::TimeSpan{0, 100},
|
|
engine::runtime::TimeSpan{0, 100},
|
|
0,
|
|
24000);
|
|
},
|
|
"invalid source sample rate");
|
|
}
|
|
|
|
void test_chunk_word_timestamp_merge_drops_outside_words() {
|
|
std::vector<engine::runtime::WordTimestamp> merged;
|
|
engine::audio::append_chunk_word_timestamps(
|
|
merged,
|
|
{
|
|
word("kept", 20, 40),
|
|
word("outside", 120, 140),
|
|
word("also_kept", 50, 80),
|
|
},
|
|
engine::runtime::TimeSpan{1000, 1100});
|
|
|
|
engine::test::require_eq(merged.size(), static_cast<size_t>(2), "outside word dropped");
|
|
engine::test::require_eq(merged[0].word, std::string("kept"), "first valid word kept");
|
|
require_span(merged[0].span, 1020, 1040, "first valid word span");
|
|
engine::test::require_eq(merged[1].word, std::string("also_kept"), "second valid word kept");
|
|
require_span(merged[1].span, 1050, 1080, "second valid word span");
|
|
}
|
|
|
|
void test_chunk_speech_metadata_merge_drops_outside_spans() {
|
|
engine::runtime::TaskResult chunk;
|
|
auto kept_segment = speech(20, 40);
|
|
kept_segment.text = "kept segment";
|
|
chunk.speech_segments.push_back(kept_segment);
|
|
auto outside_segment = speech(120, 140);
|
|
outside_segment.text = "outside segment";
|
|
chunk.speech_segments.push_back(outside_segment);
|
|
|
|
engine::runtime::SpeakerTurn kept_turn;
|
|
kept_turn.span = engine::runtime::TimeSpan{50, 80};
|
|
kept_turn.speaker_id = "SPEAKER_00";
|
|
chunk.speaker_turns.push_back(kept_turn);
|
|
engine::runtime::SpeakerTurn outside_turn;
|
|
outside_turn.span = engine::runtime::TimeSpan{120, 140};
|
|
outside_turn.speaker_id = "SPEAKER_01";
|
|
chunk.speaker_turns.push_back(outside_turn);
|
|
|
|
engine::runtime::TaskResult merged;
|
|
engine::audio::append_chunk_speech_metadata(
|
|
merged,
|
|
chunk,
|
|
engine::runtime::TimeSpan{1000, 1100},
|
|
engine::runtime::TimeSpan{1000, 1100},
|
|
16000,
|
|
16000);
|
|
|
|
engine::test::require_eq(merged.speech_segments.size(), static_cast<size_t>(1), "outside speech segment dropped");
|
|
engine::test::require_eq(merged.speech_segments[0].text, std::string("kept segment"), "valid speech segment kept");
|
|
require_span(merged.speech_segments[0].span, 1020, 1040, "valid speech segment span");
|
|
engine::test::require_eq(merged.speaker_turns.size(), static_cast<size_t>(1), "outside speaker turn dropped");
|
|
engine::test::require_eq(merged.speaker_turns[0].speaker_id, std::string("SPEAKER_00"), "valid speaker turn kept");
|
|
require_span(merged.speaker_turns[0].span, 1050, 1080, "valid speaker turn span");
|
|
}
|
|
|
|
void test_silence_chunks() {
|
|
using engine::audio::plan_silence_audio_chunks;
|
|
const engine::audio::SilenceAudioChunkOptions options{28 * 16000, 30 * 16000};
|
|
require_throws([&]() {
|
|
(void) plan_silence_audio_chunks({}, options);
|
|
}, "empty silence planner input");
|
|
require_throws([]() {
|
|
(void) plan_silence_audio_chunks({1.0F}, {0, 1});
|
|
}, "zero silence max chunk size");
|
|
require_throws([]() {
|
|
(void) plan_silence_audio_chunks({1.0F}, {1, 0});
|
|
}, "zero silence trigger");
|
|
auto spans = plan_silence_audio_chunks(std::vector<float>(30 * 16000, 1.0F), options);
|
|
engine::test::require_eq(spans.size(), size_t{1}, "trigger includes exactly 30 seconds");
|
|
require_span(spans[0], 0, 30 * 16000, "short input unchanged");
|
|
|
|
// No gaps: both continuous sound and complete silence use hard cuts.
|
|
for (float value : {0.0F, 1.0F}) {
|
|
spans = plan_silence_audio_chunks(std::vector<float>(60 * 16000, value), options);
|
|
engine::test::require_eq(spans.size(), size_t{3}, "hard cut count");
|
|
require_span(spans[0], 0, 28 * 16000, "first hard cut");
|
|
require_span(spans[1], 28 * 16000, 56 * 16000, "second hard cut");
|
|
require_span(spans[2], 56 * 16000, 60 * 16000, "remaining audio");
|
|
}
|
|
|
|
std::vector<float> samples(8192, 0.0F);
|
|
std::fill(samples.begin(), samples.begin() + 2048, 1.0F);
|
|
std::fill(samples.begin() + 6144, samples.end(), 1.0F);
|
|
const auto below_trigger = plan_silence_audio_chunks(samples, {6000, 9000});
|
|
engine::test::require_eq(below_trigger.size(), size_t{1}, "auto policy can retain input above chunk limit");
|
|
require_span(below_trigger[0], 0, 8192, "auto policy trigger preserves whole input");
|
|
spans = plan_silence_audio_chunks(samples, {6000, 6000});
|
|
engine::test::require_eq(spans.size(), size_t{2}, "silence midpoint count");
|
|
require_span(spans[0], 0, 4352, "centered RMS silence midpoint");
|
|
require_span(spans[1], 4352, 8192, "silence retained without overlap");
|
|
for (auto & sample : samples) sample *= 0.125F;
|
|
const auto scaled = plan_silence_audio_chunks(samples, {6000, 6000});
|
|
engine::test::require_eq(scaled.size(), spans.size(), "relative threshold count");
|
|
for (size_t i = 0; i < spans.size(); ++i) {
|
|
require_span(scaled[i], spans[i].start_sample, spans[i].end_sample, "relative threshold boundaries");
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main() {
|
|
try {
|
|
test_silence_chunks();
|
|
test_fixed_chunks_plan_start_aligned_tail();
|
|
test_fixed_chunks_plan_overlapping_windows();
|
|
test_fixed_chunks_plan_centered_tail_and_copy_zero_pad();
|
|
test_copy_planar_chunk_reflect_padding();
|
|
test_overlap_add_and_normalize_shared_counter();
|
|
test_fixed_chunks_validate_inputs();
|
|
test_vad_chunks_sort_pad_and_merge();
|
|
test_vad_chunks_clamp_padding_to_audio_bounds();
|
|
test_vad_chunks_respect_max_when_merging();
|
|
test_vad_chunks_merge_gap_boundary();
|
|
test_vad_chunks_split_long_speech();
|
|
test_vad_chunks_do_not_overlap_when_max_blocks_merge();
|
|
test_vad_chunks_nested_segments_keep_full_coverage();
|
|
test_vad_chunks_padding_overlap_merges_without_duplicate_coverage();
|
|
test_vad_chunks_keep_large_silence_gap_outside_chunks();
|
|
test_vad_chunks_close_run_split_at_max_without_losing_speech();
|
|
test_vad_chunks_stress_mixed_unsorted_segments();
|
|
test_vad_chunks_validate_inputs();
|
|
test_vad_session_planner_runs_vad_internally();
|
|
test_audio_chunk_mode_parser();
|
|
test_slice_audio_buffer_preserves_channels();
|
|
test_slice_audio_buffer_validate_inputs();
|
|
test_chunk_word_timestamp_merge_offsets_and_clips();
|
|
test_chunk_word_timestamp_merge_appends_multiple_chunks();
|
|
test_chunk_word_timestamp_merge_keeps_non_overlapping_source_span();
|
|
test_chunk_word_timestamp_merge_rescales_chunk_domain();
|
|
test_chunk_word_timestamp_merge_keeps_input_rate_domain();
|
|
test_chunk_speech_metadata_merge_rescales_chunk_domain();
|
|
test_chunk_word_timestamp_merge_rejects_invalid_spans();
|
|
test_chunk_word_timestamp_merge_drops_outside_words();
|
|
test_chunk_speech_metadata_merge_drops_outside_spans();
|
|
std::cout << "audio_chunking_test passed\n";
|
|
} catch (const std::exception & ex) {
|
|
std::cerr << "audio_chunking_test failed: " << ex.what() << "\n";
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|