From 141d3f6d3abb907d97eeb44e02151a3975b9f038 Mon Sep 17 00:00:00 2001 From: Shaw Date: Wed, 14 Oct 2026 22:11:09 -0400 Subject: [PATCH] asr: reject undersized streaming transcript buffers Assisted-by: Codex --- tools/omnivoice/tests/ffi-parakeet-asr.cpp | 13 +++++++++++-- tools/parakeet/include/parakeet.h | 3 ++- tools/parakeet/src/pk-stream.cpp | 13 ++++++------- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tools/omnivoice/tests/ffi-parakeet-asr.cpp b/tools/omnivoice/tests/ffi-parakeet-asr.cpp index edf93174f..62c867cd6 100644 --- a/tools/omnivoice/tests/ffi-parakeet-asr.cpp +++ b/tools/omnivoice/tests/ffi-parakeet-asr.cpp @@ -166,10 +166,19 @@ int main() { CHECK(count_words(fin) + 3 >= count_words(text) && count_words(fin) <= count_words(text) + 3, "concurrent stream word count %zu far from batch %zu", count_words(fin), count_words(text)); std::printf("concurrent feed/partial: %d polls, %zu words\n", polls, count_words(fin)); - // a tiny buffer: truncated, NUL-terminated, length < capacity + // Insufficient output space must not return a successful prefix. char tiny[5]; rc = eliza_inference_asr_stream_partial(cs, tiny, sizeof(tiny), nullptr, nullptr, &err); - CHECK(rc >= 0 && rc < (int) sizeof(tiny) && tiny[rc] == '\0', "tiny buffer rc %d", rc); + CHECK(rc < 0, "tiny partial buffer must fail, got %d", rc); + eliza_inference_free_string(err); + err = nullptr; + rc = eliza_inference_asr_stream_finish(cs, tiny, sizeof(tiny), nullptr, nullptr, &err); + CHECK(rc < 0, "tiny final buffer must fail, got %d", rc); + eliza_inference_free_string(err); + err = nullptr; + char retry[4096]; + rc = eliza_inference_asr_stream_finish(cs, retry, sizeof(retry), nullptr, nullptr, &err); + CHECK(rc > 0 && std::strcmp(fin, retry) == 0, "retry after small buffer lost transcript"); eliza_inference_asr_stream_close(cs); } } diff --git a/tools/parakeet/include/parakeet.h b/tools/parakeet/include/parakeet.h index 8d4f86407..9dfd65ddb 100644 --- a/tools/parakeet/include/parakeet.h +++ b/tools/parakeet/include/parakeet.h @@ -105,7 +105,8 @@ pk_stream * pk_stream_open(pk_model * model, pk_stream_params params); int pk_stream_feed(pk_stream * st, const float * pcm16k, size_t n_samples); /* Decode if at least min_hop_ms of new audio arrived since the last decode, * then write committed + tentative text. Returns bytes written (excluding - * NUL) or a negative pk_status. */ + * NUL) or a negative pk_status. An insufficient output buffer returns + * PK_E_INVALID without partial text; retry with more space. */ int pk_stream_partial(pk_stream * st, char * out, size_t cap, pk_cancel_cb cancel, void * cancel_user); /* Decode the remaining audio with full right context and write the final * transcript. */ diff --git a/tools/parakeet/src/pk-stream.cpp b/tools/parakeet/src/pk-stream.cpp index 9bb85656f..c23a28f9d 100644 --- a/tools/parakeet/src/pk-stream.cpp +++ b/tools/parakeet/src/pk-stream.cpp @@ -345,14 +345,13 @@ static int write_text(pk_stream * s, char * out, size_t cap) { all.insert(all.end(), s->tentative.begin(), s->tentative.end()); const std::string t = detokenize(s->m, all.data(), all.size()); if (!out || cap == 0) return (int) t.size(); - size_t n = std::min(t.size(), cap - 1); - // never cut a UTF-8 sequence: back off over continuation bytes - if (n < t.size()) { - while (n > 0 && ((unsigned char) t[n] & 0xC0) == 0x80) --n; + if (t.size() >= cap) { + out[0] = '\0'; + return PK_E_INVALID; } - std::memcpy(out, t.data(), n); - out[n] = '\0'; - return (int) n; + std::memcpy(out, t.data(), t.size()); + out[t.size()] = '\0'; + return (int) t.size(); } extern "C" int pk_stream_partial(pk_stream * s, char * out, size_t cap, pk_cancel_cb cancel, void * cu) {