Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions tests/cpp/asr/test_cache_stream_geometry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,23 @@ check_model(const std::string& path, int gpu, int right) {
runner.take_encoder_frames(actual, actual_t, dim);
require(actual_t == expected_t, "wrong runner encoder frame count");
require(runner.chunks_processed() == step + 1, "wrong chunk consumption");
const auto emitted = static_cast<int64_t>(runner.rnnt_decode_stats().encoder_frames);
require(
emitted == static_cast<int64_t>(step + 1) * (right + 1),
"first/steady transition changed the absolute decoder frame offset");
require(
enc.cache_decoded_mel_frames(emitted) == consumed + fresh,
"decoded frontier differs from independently assembled real Mel coverage");
same_encoder(actual, expected);
if (step == 0)
first_output = actual;
consumed += fresh;
cached = std::min(enc.cache_left_ctx, cached + expected_t);
}
runner.reset();
require(
runner.rnnt_decode_stats().encoder_frames == 0,
"new stream retained the previous decoder frame clock");
// Preserve FE packet shapes as well: Metal FFT rounding can differ across
// packet shapes independently of the encoder's chunk geometry.
runner.feed_audio(audio.data(), first_samples - 1);
Expand Down Expand Up @@ -214,12 +224,18 @@ check_model(const std::string& path, int gpu, int right) {
require(runner.cache_filled_frames() == right + 1, "EOU changed encoder cache");
const int before = runner.chunks_processed();
require(before == 1, "EOU synthesized extra chunks");
require(
runner.rnnt_decode_stats().encoder_frames == static_cast<uint64_t>(right + 1),
"EOU reset the absolute decoder clock after the first chunk");
runner.feed_audio(audio.data() + first_samples, stride_samples - 1);
runner.step();
require(runner.chunks_processed() == before, "post-EOU reused first-chunk geometry");
runner.feed_audio(audio.data() + first_samples + stride_samples - 1, 1);
runner.step();
require(runner.chunks_processed() == before + 1, "post-EOU steady chunk was not processed");
require(
runner.rnnt_decode_stats().encoder_frames == static_cast<uint64_t>(2 * (right + 1)),
"post-EOU decoder clock did not retain the first chunk offset");
std::vector<float> output;
int count = 0, dim = 0;
runner.take_encoder_frames(output, count, dim);
Expand Down
58 changes: 58 additions & 0 deletions tests/cpp/asr/test_decoders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <string>
#include <vector>

#include "fastconformer.h"
#include "greedy_ctc_decoder.h"
#include "rnnt_greedy_decoder.h"

Expand Down Expand Up @@ -675,6 +676,62 @@ test_rnnt_punctuation_word_timing() {
check(ok, "rnnt: boundary-prefixed terminator stays with timed word");
}

void
test_rnnt_causal_startup_coordinates() {
// #46 shortened the first Mel chunk, not the decoder's emission-frame
// grid. Guard against applying the VAD frontier correction to word times,
// and against restarting the absolute frame offset after an EOU.
for (int right : {0, 3, 6, 13}) {
const auto cfg = make_cache_aware_config(EncoderConfig{}, right);
const int count = right + 1;
const int64_t first_mel = 1 + 8 * right;
MockRnntEngine eng;
RnntGreedyDecoder dec(&eng);
dec.set_compute_timestamps(true);
const int blank = eng.rnnt_config().blank_id;
std::vector<int> last_only(count, blank);
last_only.back() = 0;
eng.rounds = {last_only, {blank}};
std::vector<float> frames(count * eng.rnnt_config().joint_dim, 0.0f);
const auto first = dec.step(frames.data(), eng.rnnt_config().joint_dim, count, 0);
dec.finalize();
const auto words = dec.word_timings();
check(
first == std::vector<int>{0} && words.size() == 1,
"rnnt causal startup: control must emit a timed word");
if (words.size() != 1)
continue;
check(
words[0].start_frame == right && words[0].end_frame == count,
"rnnt causal startup: first word keeps the emission-frame grid");
check(
cfg.cache_decoded_mel_frames(count) == first_mel &&
words[0].start_frame * 8 == first_mel - 1,
"rnnt causal startup: emission anchor and exclusive Mel frontier differ");
dec.reset_utterance();
eng.rounds = {{1}, {blank}};
const auto next = dec.step(frames.data(), eng.rnnt_config().joint_dim, count, count);
dec.finalize();
const auto& after_eou = dec.word_timings();
check(
next == std::vector<int>{1} && after_eou.size() == 1 &&
after_eou[0].start_frame == count && after_eou[0].end_frame == count + 1 &&
dec.last_speech_frame() == count,
"rnnt causal startup: EOU preserves absolute word and token-silence coordinates");
check(
cfg.cache_decoded_mel_frames(2 * count) == first_mel + 8 * count,
"rnnt causal startup: second chunk advances by the steady-state stride");
dec.reset();
eng.rounds = {{2}, {blank}};
dec.step(frames.data(), eng.rnnt_config().joint_dim, count, 0);
dec.finalize();
const auto& reset = dec.word_timings();
check(
reset.size() == 1 && reset[0].start_frame == 0 && reset[0].end_frame == 1,
"rnnt causal startup: only a new stream restarts word coordinates at zero");
}
}

void
test_rnnt_open_word_needs_finalize() {
// The contract CacheStreamRunner::finalize() relies on: an open trailing
Expand Down Expand Up @@ -818,6 +875,7 @@ main() {
test_rnnt_endpoint_punctuation_continuity();
test_rnnt_state_threading_and_reset_utterance();
test_rnnt_word_timings();
test_rnnt_causal_startup_coordinates();
test_rnnt_punctuation_word_timing();
test_rnnt_open_word_needs_finalize();
test_tdt_duration_and_state_commit();
Expand Down