From c9d7c2539ff241743a17c3828eb2a0c3bae5091e Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Tue, 10 Nov 2026 21:10:40 -0800 Subject: [PATCH 1/2] parakeet: drop the NeMo padding mel frame before the encoder (#252) The constant-pad STFT emits floor(n/hop)+1 frames but NeMo counts floor(n/hop) as valid and masks the rest in pre_encode, attention and the conv module. We fed the padding frame unmasked, so whenever floor(n/hop) is a multiple of 8 it became an extra encoder frame that leaked into every real frame through attention, emptying or truncating transcripts. Drop it on the single and batch offline paths for non-kestrel checkpoints. Kestrel models (ultra) already mask it in-graph; streaming paths are untouched. --- src/arch/parakeet/model.cpp | 34 ++++++++- src/arch/parakeet/parakeet.h | 22 ++++++ tests/CMakeLists.txt | 8 ++ tests/parakeet_mel_valid_length_unit.cpp | 95 ++++++++++++++++++++++++ 4 files changed, 156 insertions(+), 3 deletions(-) create mode 100644 tests/parakeet_mel_valid_length_unit.cpp diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp index a877781e..1d01fdf7 100644 --- a/src/arch/parakeet/model.cpp +++ b/src/arch/parakeet/model.cpp @@ -139,6 +139,28 @@ ParakeetModel::~ParakeetModel() { plan.primary_kind = transcribe::BackendKind::Unknown; } +// Contract in parakeet.h. +int drop_nemo_padding_mel_frame(const ParakeetHParams & hp, + std::vector & mel, + int n_mels, + int n_frames, + int n_samples) { + if (hp.kestrel_length_masking || hp.fe_hop_length <= 0) { + return n_frames; + } + const int valid = n_samples / hp.fe_hop_length; + if (valid <= 0 || valid >= n_frames) { + return n_frames; + } + for (int m = 1; m < n_mels; ++m) { + const float * src = mel.data() + static_cast(m) * static_cast(n_frames); + float * dst = mel.data() + static_cast(m) * static_cast(valid); + std::memmove(dst, src, static_cast(valid) * sizeof(float)); + } + mel.resize(static_cast(n_mels) * static_cast(valid)); + return valid; +} + namespace { constexpr float kBnEps = 1e-5f; @@ -1060,6 +1082,14 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, return mst; } pc->t_mel_us = ggml_time_us() - t_mel_start; + // Dump the full frontend output (what NeMo's preprocessor returns) + // before the padding frame is dropped. + if (transcribe::debug::enabled()) { + const long long shape[2] = { mel_n_mels, mel_n_frames }; + transcribe::debug::dump_host_f32("enc.mel.in", pc->mel_buf.data(), static_cast(pc->mel_buf.size()), + shape, 2, "encoder.mel"); + } + mel_n_frames = drop_nemo_padding_mel_frame(pm->hparams, pc->mel_buf, mel_n_mels, mel_n_frames, n_samples); // Multitalker masked mode (NeMo mask_features): binarize the target // speaker's per-diar-frame activity, expand 8x to feature frames from @@ -1170,8 +1200,6 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, // byte-identical to the ggml ne=[n_frames, num_mels, 1, 1] input. ggml_backend_tensor_set(eb.mel_in, pc->mel_buf.data(), 0, pc->mel_buf.size() * sizeof(float)); - transcribe::debug::dump_tensor("enc.mel.in", eb.mel_in, "encoder.mel"); - int T_valid = -1; if (kestrel_masks) { std::vector real_tenc; @@ -2114,7 +2142,7 @@ transcribe_status run_batch(transcribe_session * session, if (st != TRANSCRIBE_OK || this_frames <= 0) { return false; } - nf[i] = this_frames; + nf[i] = drop_nemo_padding_mel_frame(pm->hparams, mels[i], this_mels, this_frames, n_samples[i]); n_mels_per[i] = this_mels; return true; }); diff --git a/src/arch/parakeet/parakeet.h b/src/arch/parakeet/parakeet.h index 30ae2743..ffd22ab8 100644 --- a/src/arch/parakeet/parakeet.h +++ b/src/arch/parakeet/parakeet.h @@ -40,6 +40,28 @@ inline int pre_encode_time_out(int input_frames, bool causal) { return causal ? (input_frames / 2 + 1) : ((input_frames - 1) / 2 + 1); } +// Drop the trailing center-pad mel frame NeMo does not count (#252). +// +// The constant-pad STFT emits floor(n / hop) + 1 frames, but NeMo's +// FilterbankFeatures.get_seq_len reports floor(n / hop): the last frame is +// padding (MelFrontend already zeroes it and leaves it out of the +// normalization stats). NeMo masks every pre_encode stage, the attention +// keys and the conv module to the lengths derived from that count. Masking a +// conv input to zero past its valid length is the same as truncating it +// there, so NeMo's valid encoder frames are exactly what the encoder computes +// on the first floor(n / hop) mel frames. Feeding the padding frame instead +// adds an encoder frame whenever floor(n / hop) is a multiple of 8, and that +// frame leaks into every real frame through attention. +// +// Compacts the row-major [n_mels, n_frames] buffer in place and returns the +// new frame count. No-op for kestrel-masked checkpoints, which mask the +// padding in-graph. +int drop_nemo_padding_mel_frame(const ParakeetHParams & hp, + std::vector & mel, + int n_mels, + int n_frames, + int n_samples); + // Host-side attention-mask helpers for the chunked-attention paths. Declared // here so the per-mask unit tests can call them without a ggml backend // or a GGUF on disk. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 110645b0..0f8ad7f0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -211,6 +211,14 @@ transcribe_apply_warnings(transcribe_parakeet_chunked_limited_window_mask_unit) add_test(NAME transcribe_parakeet_chunked_limited_window_mask_unit COMMAND transcribe_parakeet_chunked_limited_window_mask_unit) +# NeMo valid-length mel framing (#252): the trailing center-pad frame is +# dropped before the encoder on non-kestrel checkpoints. +add_executable(transcribe_parakeet_mel_valid_length_unit parakeet_mel_valid_length_unit.cpp) +target_link_libraries(transcribe_parakeet_mel_valid_length_unit PRIVATE transcribe ggml) +target_include_directories(transcribe_parakeet_mel_valid_length_unit PRIVATE ${CMAKE_SOURCE_DIR}/src) +transcribe_apply_warnings(transcribe_parakeet_mel_valid_length_unit) +add_test(NAME transcribe_parakeet_mel_valid_length_unit COMMAND transcribe_parakeet_mel_valid_length_unit) + # Long-form pause segmenter vs kestrel-generated expected cut points. add_executable(transcribe_parakeet_longform_unit parakeet_longform_unit.cpp) target_link_libraries(transcribe_parakeet_longform_unit PRIVATE transcribe ggml) diff --git a/tests/parakeet_mel_valid_length_unit.cpp b/tests/parakeet_mel_valid_length_unit.cpp new file mode 100644 index 00000000..6a0a0a48 --- /dev/null +++ b/tests/parakeet_mel_valid_length_unit.cpp @@ -0,0 +1,95 @@ +// parakeet_mel_valid_length_unit.cpp - drop_nemo_padding_mel_frame (#252). +// +// The Parakeet frontend emits floor(n / hop) + 1 mel frames; NeMo counts +// floor(n / hop) of them as valid. The helper must drop exactly the trailing +// frame of every mel row, keep the row-major [n_mels, n_frames] layout, and +// leave kestrel-masked checkpoints alone. + +#include "arch/parakeet/parakeet.h" + +#include +#include + +namespace { + +int g_failures = 0; + +void check(bool ok, const char * what) { + if (!ok) { + std::fprintf(stderr, "FAIL: %s\n", what); + ++g_failures; + } +} + +// Value of mel bin m at frame t: unique per cell. +float cell(int m, int t) { + return static_cast(m * 10000 + t); +} + +std::vector make_mel(int n_mels, int n_frames) { + std::vector mel(static_cast(n_mels) * static_cast(n_frames)); + for (int m = 0; m < n_mels; ++m) { + for (int t = 0; t < n_frames; ++t) { + mel[static_cast(m) * static_cast(n_frames) + static_cast(t)] = cell(m, t); + } + } + return mel; +} + +void run_case(int n_samples) { + using transcribe::parakeet::ParakeetHParams; + ParakeetHParams hp{}; + hp.fe_hop_length = 160; + + const int n_mels = 7; + const int n_frames = n_samples / hp.fe_hop_length + 1; // MelFrontend framing + const int valid = n_samples / hp.fe_hop_length; + + std::vector mel = make_mel(n_mels, n_frames); + const int got = transcribe::parakeet::drop_nemo_padding_mel_frame(hp, mel, n_mels, n_frames, n_samples); + check(got == valid, "returns floor(n / hop)"); + check(mel.size() == static_cast(n_mels) * static_cast(valid), "buffer resized to n_mels * valid"); + bool layout_ok = true; + for (int m = 0; m < n_mels && layout_ok; ++m) { + for (int t = 0; t < valid; ++t) { + if (mel[static_cast(m) * static_cast(valid) + static_cast(t)] != cell(m, t)) { + layout_ok = false; + break; + } + } + } + check(layout_ok, "rows compacted in place, frame order preserved"); + + // Kestrel-masked checkpoints mask the padding in-graph: untouched. + hp.kestrel_length_masking = true; + std::vector kmel = make_mel(n_mels, n_frames); + const int kgot = transcribe::parakeet::drop_nemo_padding_mel_frame(hp, kmel, n_mels, n_frames, n_samples); + check(kgot == n_frames, "kestrel: frame count unchanged"); + check(kmel == make_mel(n_mels, n_frames), "kestrel: buffer unchanged"); +} + +} // namespace + +int main() { + // The #252 clips (80640 -> 505 frames, 168960 -> 1057) plus their + // one-sample-shorter neighbours and a few unaligned lengths. + for (const int n : { 80640, 80639, 168960, 168959, 480000, 479999, 160, 161, 1601, 16000 }) { + run_case(n); + } + + // Fewer samples than one hop: nothing valid to keep; leave as is. + { + transcribe::parakeet::ParakeetHParams hp{}; + hp.fe_hop_length = 160; + std::vector mel = make_mel(3, 1); + check(transcribe::parakeet::drop_nemo_padding_mel_frame(hp, mel, 3, 1, 100) == 1, "sub-hop input unchanged"); + check(mel.size() == 3, "sub-hop buffer unchanged"); + } + + if (g_failures != 0) { + std::fprintf(stderr, "%d failure(s)\n", g_failures); + return 1; + } + std::printf("parakeet_mel_valid_length_unit: OK\n"); + return 0; +} From 66c6989a987b7b462e85986ff6c33ca239c33009 Mon Sep 17 00:00:00 2001 From: CJ Pais Date: Wed, 11 Nov 2026 14:39:34 +0800 Subject: [PATCH 2/2] mel: drop NeMo padding frame in parakeet, canary and canary-qwen --- src/arch/canary/model.cpp | 11 ++- src/arch/canary_qwen/model.cpp | 15 ++-- src/arch/parakeet/model.cpp | 31 ++------ src/arch/parakeet/parakeet.h | 22 ------ src/transcribe-mel.cpp | 14 ++++ src/transcribe-mel.h | 14 ++++ tests/CMakeLists.txt | 8 -- tests/mel_unit.cpp | 35 +++++++++ tests/parakeet_mel_valid_length_unit.cpp | 95 ------------------------ 9 files changed, 88 insertions(+), 157 deletions(-) delete mode 100644 tests/parakeet_mel_valid_length_unit.cpp diff --git a/src/arch/canary/model.cpp b/src/arch/canary/model.cpp index cdaf3ac6..8f4d57cd 100644 --- a/src/arch/canary/model.cpp +++ b/src/arch/canary/model.cpp @@ -791,6 +791,14 @@ transcribe_status run(transcribe_session * session, return mst; } cc->t_mel_us = ggml_time_us() - t_mel_start; + // Dump the full frontend output (what NeMo's preprocessor returns) + // before the padding frame is dropped. + if (transcribe::debug::enabled()) { + const long long shape[2] = { mel_n_mels, mel_n_frames }; + transcribe::debug::dump_host_f32("enc.mel.in", cc->mel_buf.data(), static_cast(cc->mel_buf.size()), + shape, 2, "encoder.mel"); + } + mel_n_frames = cm->mel->drop_nemo_padding_frame(cc->mel_buf, mel_n_frames, static_cast(n_samples)); // Input-length gate: T_enc must stay within enc_pos_emb_max_len or the // runtime pos table aliases past the trained range. T_enc is a @@ -859,7 +867,6 @@ transcribe_status run(transcribe_session * session, } ggml_backend_tensor_set(eb.mel_in, cc->mel_buf.data(), 0, cc->mel_buf.size() * sizeof(float)); - transcribe::debug::dump_tensor("enc.mel.in", eb.mel_in, "encoder.mel"); // Sinusoidal relative positional embedding. if (eb.pos_emb_in != nullptr) { @@ -1669,7 +1676,7 @@ transcribe_status run_batch(transcribe_session * session, if (cm->mel->compute(pcm[b], static_cast(n_samples[b]), mel_bufs[b], nm, nf, /*n_threads=*/1) == TRANSCRIBE_OK && nf > 0) { - mel_nf[b] = nf; + mel_nf[b] = cm->mel->drop_nemo_padding_frame(mel_bufs[b], nf, static_cast(n_samples[b])); valid[b] = 1; } return true; diff --git a/src/arch/canary_qwen/model.cpp b/src/arch/canary_qwen/model.cpp index 5b00b406..5eb31f0d 100644 --- a/src/arch/canary_qwen/model.cpp +++ b/src/arch/canary_qwen/model.cpp @@ -794,6 +794,14 @@ transcribe_status run(transcribe_session * context, return mst; } cc->t_mel_us = ggml_time_us() - t_mel_start; + // Dump the full frontend output (what NeMo's preprocessor returns) + // before the padding frame is dropped. + if (transcribe::debug::enabled()) { + const long long shape[2] = { mel_n_mels, mel_n_frames }; + transcribe::debug::dump_host_f32("enc.mel.in", cc->mel_buf.data(), static_cast(cc->mel_buf.size()), + shape, 2, "frontend.mel.norm"); + } + mel_n_frames = cm->mel->drop_nemo_padding_frame(cc->mel_buf, mel_n_frames, static_cast(n_samples)); // Encoder. if (cc->compute_ctx != nullptr) { @@ -835,11 +843,6 @@ transcribe_status run(transcribe_session * context, } ggml_backend_tensor_set(eb.mel_in, cc->mel_buf.data(), 0, cc->mel_buf.size() * sizeof(float)); - if (transcribe::debug::enabled()) { - const long long shape[2] = { mel_n_mels, mel_n_frames }; - transcribe::debug::dump_host_f32("enc.mel.in", cc->mel_buf.data(), static_cast(cc->mel_buf.size()), - shape, 2, "frontend.mel.norm"); - } // Upload relative-position embedding. if (eb.pos_emb_in != nullptr) { @@ -1411,7 +1414,7 @@ transcribe_status run_batch(transcribe_session * session, int nm = 0, nf = 0; if (cm->mel->compute(pcm[b], static_cast(n_samples[b]), mel_bufs[b], nm, nf, /*n_threads=*/1) == TRANSCRIBE_OK) { - mel_nf[b] = nf; + mel_nf[b] = cm->mel->drop_nemo_padding_frame(mel_bufs[b], nf, static_cast(n_samples[b])); } return true; }); diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp index 1d01fdf7..81be6f20 100644 --- a/src/arch/parakeet/model.cpp +++ b/src/arch/parakeet/model.cpp @@ -139,28 +139,6 @@ ParakeetModel::~ParakeetModel() { plan.primary_kind = transcribe::BackendKind::Unknown; } -// Contract in parakeet.h. -int drop_nemo_padding_mel_frame(const ParakeetHParams & hp, - std::vector & mel, - int n_mels, - int n_frames, - int n_samples) { - if (hp.kestrel_length_masking || hp.fe_hop_length <= 0) { - return n_frames; - } - const int valid = n_samples / hp.fe_hop_length; - if (valid <= 0 || valid >= n_frames) { - return n_frames; - } - for (int m = 1; m < n_mels; ++m) { - const float * src = mel.data() + static_cast(m) * static_cast(n_frames); - float * dst = mel.data() + static_cast(m) * static_cast(valid); - std::memmove(dst, src, static_cast(valid) * sizeof(float)); - } - mel.resize(static_cast(n_mels) * static_cast(valid)); - return valid; -} - namespace { constexpr float kBnEps = 1e-5f; @@ -1089,7 +1067,10 @@ transcribe_status run_one_shot_inner(ParakeetSession * pc, transcribe::debug::dump_host_f32("enc.mel.in", pc->mel_buf.data(), static_cast(pc->mel_buf.size()), shape, 2, "encoder.mel"); } - mel_n_frames = drop_nemo_padding_mel_frame(pm->hparams, pc->mel_buf, mel_n_mels, mel_n_frames, n_samples); + // Kestrel-masked checkpoints mask the padding frame in-graph instead. + if (!pm->hparams.kestrel_length_masking) { + mel_n_frames = pm->mel->drop_nemo_padding_frame(pc->mel_buf, mel_n_frames, static_cast(n_samples)); + } // Multitalker masked mode (NeMo mask_features): binarize the target // speaker's per-diar-frame activity, expand 8x to feature frames from @@ -2142,7 +2123,9 @@ transcribe_status run_batch(transcribe_session * session, if (st != TRANSCRIBE_OK || this_frames <= 0) { return false; } - nf[i] = drop_nemo_padding_mel_frame(pm->hparams, mels[i], this_mels, this_frames, n_samples[i]); + nf[i] = pm->hparams.kestrel_length_masking ? + this_frames : + pm->mel->drop_nemo_padding_frame(mels[i], this_frames, static_cast(n_samples[i])); n_mels_per[i] = this_mels; return true; }); diff --git a/src/arch/parakeet/parakeet.h b/src/arch/parakeet/parakeet.h index ffd22ab8..30ae2743 100644 --- a/src/arch/parakeet/parakeet.h +++ b/src/arch/parakeet/parakeet.h @@ -40,28 +40,6 @@ inline int pre_encode_time_out(int input_frames, bool causal) { return causal ? (input_frames / 2 + 1) : ((input_frames - 1) / 2 + 1); } -// Drop the trailing center-pad mel frame NeMo does not count (#252). -// -// The constant-pad STFT emits floor(n / hop) + 1 frames, but NeMo's -// FilterbankFeatures.get_seq_len reports floor(n / hop): the last frame is -// padding (MelFrontend already zeroes it and leaves it out of the -// normalization stats). NeMo masks every pre_encode stage, the attention -// keys and the conv module to the lengths derived from that count. Masking a -// conv input to zero past its valid length is the same as truncating it -// there, so NeMo's valid encoder frames are exactly what the encoder computes -// on the first floor(n / hop) mel frames. Feeding the padding frame instead -// adds an encoder frame whenever floor(n / hop) is a multiple of 8, and that -// frame leaks into every real frame through attention. -// -// Compacts the row-major [n_mels, n_frames] buffer in place and returns the -// new frame count. No-op for kestrel-masked checkpoints, which mask the -// padding in-graph. -int drop_nemo_padding_mel_frame(const ParakeetHParams & hp, - std::vector & mel, - int n_mels, - int n_frames, - int n_samples); - // Host-side attention-mask helpers for the chunked-attention paths. Declared // here so the per-mask unit tests can call them without a ggml backend // or a GGUF on disk. diff --git a/src/transcribe-mel.cpp b/src/transcribe-mel.cpp index af4e23ca..811c2d7b 100644 --- a/src/transcribe-mel.cpp +++ b/src/transcribe-mel.cpp @@ -373,6 +373,20 @@ int MelFrontend::n_frames_for(size_t n_samples) const { return static_cast(n_samples / static_cast(cfg_.hop_length)) + 1; } +int MelFrontend::drop_nemo_padding_frame(std::vector & mel, int n_frames, size_t n_samples) const { + const int valid = static_cast(n_samples / static_cast(cfg_.hop_length)); + if (valid >= n_frames) { + return n_frames; + } + const size_t n_mels = static_cast(cfg_.num_mels); + for (size_t m = 1; m < n_mels; ++m) { + std::memmove(mel.data() + m * static_cast(valid), mel.data() + m * static_cast(n_frames), + static_cast(valid) * sizeof(float)); + } + mel.resize(n_mels * static_cast(valid)); + return valid; +} + transcribe_status MelFrontend::compute(const float * pcm, size_t n_samples, std::vector & out_mel, diff --git a/src/transcribe-mel.h b/src/transcribe-mel.h index 49abaef9..57541fbf 100644 --- a/src/transcribe-mel.h +++ b/src/transcribe-mel.h @@ -173,6 +173,20 @@ class MelFrontend { // Matches NeMo: floor(n_samples / hop_length) + 1. int n_frames_for(size_t n_samples) const; + // Drop the trailing center-pad frame NeMo does not count (#252). + // + // The centered STFT emits floor(n / hop) + 1 frames, but NeMo's + // get_seq_len reports floor(n / hop) and masks everything past it in + // pre_encode, attention and the conv module. Masking a conv input to + // zero past its valid length equals truncating it there, so NeMo's valid + // encoder frames are what the encoder computes on the first + // floor(n / hop) mel frames. Fed unmasked, the padding frame becomes an + // extra encoder frame whenever floor(n / hop) is a multiple of 8. + // + // Truncates compute()'s row-major [num_mels, n_frames] buffer in place + // and returns the new frame count. + int drop_nemo_padding_frame(std::vector & mel, int n_frames, size_t n_samples) const; + // Read-only accessors for unit tests. Not part of the runtime // API; the goal is to validate the precomputed buffers in // isolation without running the full pipeline. diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0f8ad7f0..110645b0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -211,14 +211,6 @@ transcribe_apply_warnings(transcribe_parakeet_chunked_limited_window_mask_unit) add_test(NAME transcribe_parakeet_chunked_limited_window_mask_unit COMMAND transcribe_parakeet_chunked_limited_window_mask_unit) -# NeMo valid-length mel framing (#252): the trailing center-pad frame is -# dropped before the encoder on non-kestrel checkpoints. -add_executable(transcribe_parakeet_mel_valid_length_unit parakeet_mel_valid_length_unit.cpp) -target_link_libraries(transcribe_parakeet_mel_valid_length_unit PRIVATE transcribe ggml) -target_include_directories(transcribe_parakeet_mel_valid_length_unit PRIVATE ${CMAKE_SOURCE_DIR}/src) -transcribe_apply_warnings(transcribe_parakeet_mel_valid_length_unit) -add_test(NAME transcribe_parakeet_mel_valid_length_unit COMMAND transcribe_parakeet_mel_valid_length_unit) - # Long-form pause segmenter vs kestrel-generated expected cut points. add_executable(transcribe_parakeet_longform_unit parakeet_longform_unit.cpp) target_link_libraries(transcribe_parakeet_longform_unit PRIVATE transcribe ggml) diff --git a/tests/mel_unit.cpp b/tests/mel_unit.cpp index 218785b7..5bf31135 100644 --- a/tests/mel_unit.cpp +++ b/tests/mel_unit.cpp @@ -213,6 +213,40 @@ void test_n_frames_for() { CHECK(mf.n_frames_for(0) == 1); } +// #252: the frontend emits floor(n / hop) + 1 frames, NeMo counts +// floor(n / hop). The trailing frame of every mel row must go and the +// row-major layout must survive the in-place compaction. +void test_drop_nemo_padding_frame() { + const transcribe::MelFrontend mf(parakeet_config()); + const int n_mels = mf.num_mels(); + // The #252 clips (80640, 168960 samples) and unaligned neighbours. + for (const size_t n : { size_t{ 80640 }, size_t{ 80639 }, size_t{ 168960 }, size_t{ 160 }, size_t{ 1601 } }) { + const int n_frames = mf.n_frames_for(n); + const int valid = n_frames - 1; + std::vector mel(static_cast(n_mels) * static_cast(n_frames)); + for (int m = 0; m < n_mels; ++m) { + for (int t = 0; t < n_frames; ++t) { + mel[static_cast(m) * static_cast(n_frames) + static_cast(t)] = + static_cast(m * 10000 + t); + } + } + CHECK(mf.drop_nemo_padding_frame(mel, n_frames, n) == valid); + CHECK(mel.size() == static_cast(n_mels) * static_cast(valid)); + bool layout_ok = true; + for (int m = 0; m < n_mels; ++m) { + for (int t = 0; t < valid; ++t) { + layout_ok = + layout_ok && mel[static_cast(m) * static_cast(valid) + static_cast(t)] == + static_cast(m * 10000 + t); + } + } + CHECK(layout_ok); + // Already at the valid length: untouched. + CHECK(mf.drop_nemo_padding_frame(mel, valid, n) == valid); + CHECK(mel.size() == static_cast(n_mels) * static_cast(valid)); + } +} + // SpeechBrain (ecapa_tdnn) config: periodic Hamming, constant pad, no // pre-emphasis, 10*log10 with an 80 dB top-dB floor, per-bin mean only. // The filterbank is a synthetic one-hot bank (mel m reads FFT bin 3*m), @@ -317,6 +351,7 @@ int main() { test_window(); test_mel_filterbank(); test_n_frames_for(); + test_drop_nemo_padding_frame(); test_hamming_window(); test_sentence_mean(); diff --git a/tests/parakeet_mel_valid_length_unit.cpp b/tests/parakeet_mel_valid_length_unit.cpp deleted file mode 100644 index 6a0a0a48..00000000 --- a/tests/parakeet_mel_valid_length_unit.cpp +++ /dev/null @@ -1,95 +0,0 @@ -// parakeet_mel_valid_length_unit.cpp - drop_nemo_padding_mel_frame (#252). -// -// The Parakeet frontend emits floor(n / hop) + 1 mel frames; NeMo counts -// floor(n / hop) of them as valid. The helper must drop exactly the trailing -// frame of every mel row, keep the row-major [n_mels, n_frames] layout, and -// leave kestrel-masked checkpoints alone. - -#include "arch/parakeet/parakeet.h" - -#include -#include - -namespace { - -int g_failures = 0; - -void check(bool ok, const char * what) { - if (!ok) { - std::fprintf(stderr, "FAIL: %s\n", what); - ++g_failures; - } -} - -// Value of mel bin m at frame t: unique per cell. -float cell(int m, int t) { - return static_cast(m * 10000 + t); -} - -std::vector make_mel(int n_mels, int n_frames) { - std::vector mel(static_cast(n_mels) * static_cast(n_frames)); - for (int m = 0; m < n_mels; ++m) { - for (int t = 0; t < n_frames; ++t) { - mel[static_cast(m) * static_cast(n_frames) + static_cast(t)] = cell(m, t); - } - } - return mel; -} - -void run_case(int n_samples) { - using transcribe::parakeet::ParakeetHParams; - ParakeetHParams hp{}; - hp.fe_hop_length = 160; - - const int n_mels = 7; - const int n_frames = n_samples / hp.fe_hop_length + 1; // MelFrontend framing - const int valid = n_samples / hp.fe_hop_length; - - std::vector mel = make_mel(n_mels, n_frames); - const int got = transcribe::parakeet::drop_nemo_padding_mel_frame(hp, mel, n_mels, n_frames, n_samples); - check(got == valid, "returns floor(n / hop)"); - check(mel.size() == static_cast(n_mels) * static_cast(valid), "buffer resized to n_mels * valid"); - bool layout_ok = true; - for (int m = 0; m < n_mels && layout_ok; ++m) { - for (int t = 0; t < valid; ++t) { - if (mel[static_cast(m) * static_cast(valid) + static_cast(t)] != cell(m, t)) { - layout_ok = false; - break; - } - } - } - check(layout_ok, "rows compacted in place, frame order preserved"); - - // Kestrel-masked checkpoints mask the padding in-graph: untouched. - hp.kestrel_length_masking = true; - std::vector kmel = make_mel(n_mels, n_frames); - const int kgot = transcribe::parakeet::drop_nemo_padding_mel_frame(hp, kmel, n_mels, n_frames, n_samples); - check(kgot == n_frames, "kestrel: frame count unchanged"); - check(kmel == make_mel(n_mels, n_frames), "kestrel: buffer unchanged"); -} - -} // namespace - -int main() { - // The #252 clips (80640 -> 505 frames, 168960 -> 1057) plus their - // one-sample-shorter neighbours and a few unaligned lengths. - for (const int n : { 80640, 80639, 168960, 168959, 480000, 479999, 160, 161, 1601, 16000 }) { - run_case(n); - } - - // Fewer samples than one hop: nothing valid to keep; leave as is. - { - transcribe::parakeet::ParakeetHParams hp{}; - hp.fe_hop_length = 160; - std::vector mel = make_mel(3, 1); - check(transcribe::parakeet::drop_nemo_padding_mel_frame(hp, mel, 3, 1, 100) == 1, "sub-hop input unchanged"); - check(mel.size() == 3, "sub-hop buffer unchanged"); - } - - if (g_failures != 0) { - std::fprintf(stderr, "%d failure(s)\n", g_failures); - return 1; - } - std::printf("parakeet_mel_valid_length_unit: OK\n"); - return 0; -}