diff --git a/tests/cpp/asr/test_cache_stream_geometry.cpp b/tests/cpp/asr/test_cache_stream_geometry.cpp index db967d2..25dd9a4 100644 --- a/tests/cpp/asr/test_cache_stream_geometry.cpp +++ b/tests/cpp/asr/test_cache_stream_geometry.cpp @@ -155,6 +155,13 @@ 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(runner.rnnt_decode_stats().encoder_frames); + require( + emitted == static_cast(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; @@ -162,6 +169,9 @@ check_model(const std::string& path, int gpu, int right) { 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); @@ -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(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(2 * (right + 1)), + "post-EOU decoder clock did not retain the first chunk offset"); std::vector output; int count = 0, dim = 0; runner.take_encoder_frames(output, count, dim); diff --git a/tests/cpp/asr/test_decoders.cpp b/tests/cpp/asr/test_decoders.cpp index e7fe528..ad64b1e 100644 --- a/tests/cpp/asr/test_decoders.cpp +++ b/tests/cpp/asr/test_decoders.cpp @@ -18,6 +18,7 @@ #include #include +#include "fastconformer.h" #include "greedy_ctc_decoder.h" #include "rnnt_greedy_decoder.h" @@ -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 last_only(count, blank); + last_only.back() = 0; + eng.rounds = {last_only, {blank}}; + std::vector 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{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{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 @@ -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();