Skip to content

streaming: publish partial text as increments, through one shared publisher - #552

Merged
0xShug0 merged 7 commits into
0xShug0:mainfrom
christopherthompson81:fix/transcript-delta-increments
Sep 15, 2026
Merged

0xShug0 merged 7 commits into
0xShug0:mainfrom
christopherthompson81:fix/transcript-delta-increments

Conversation

@christopherthompson81

@christopherthompson81 christopherthompson81 commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

A streaming session's partial_text is the text decoded since the last one. app/cli/partial_render.h says so and the CLI appends them into a scrolling transcript; the reference server forwards each as an OpenAI-shaped transcript.text.delta, which is incremental by specification.

Two families published the whole running transcript instead. A consumer that appends — the CLI, or any client written against the OpenAI shape — built the transcript up quadratically:

partial_text=Some call me nat
partial_text=Some call me nature. Others call me
partial_text=Some call me nature. Others call me Mother Nature. I'
...

giving Some call me natSome call me nature. Others call me….

parakeet_tdt published merged_decode(). kroko_asr published result.text_output from combined_decoded(). Both now publish the increment, and the CLI output concatenates byte for byte to the run's own text_output.

Scope

Four families, per review:

family why
parakeet_tdt restated the whole transcript as a partial
kroko_asr same, found by sweeping for the pattern rather than by hitting it
higgs_audio_stt carried its own copy of the prefix diff…
vibevoice_asr …duplicated verbatim, helper and caller both

voxtral_realtime, qwen3_asr and sense_asr keep their own byte-offset implementations. They were already publishing partials correctly and were optimised by the teams that own them, so migrating them would have been consistency rather than a fix. The net diff does not touch them.

engine::runtime::PartialTextPublisher is the single implementation for the four. It holds what has actually been published and returns the increment, with two properties the duplicated copies lacked:

  • A delta never ends part way through a UTF-8 sequence. higgs_audio_stt and vibevoice_asr are ByteLevel, with all 256 single-byte tokens in vocabulary. Taking Higgs's real 151,643-entry vocabulary through the engine's own decode path:

    'ᚠ' U+16A0  ->  3 byte tokens [157, 248, 254]
      after token 1:  e1        INVALID UTF-8
      after token 2:  e19a      INVALID UTF-8
      after token 3:  e19aa0    valid: 'ᚠ'
    

    Five of fourteen characters tested (Runic, Devanagari, Hangul Jamo Extended, Gothic, CJK Compatibility) have no whole-character token, so the tokenizer must emit them as byte sequences — and text_decoder.cpp calls token_callback after every generated token, so steps 1 and 2 go on the wire. parakeet_tdt (Metaspace, no <0xNN> pieces) and kroko_asr (plain pieces) cannot produce this, and for them the check is a no-op that costs one bounded memcmp.

  • A delta never starts inside one either, when a decode revises published text rather than only extending it. On a genuine revision nothing can retract a delta already sent, so the consumer is wrong either way; this stops it also being spliced into the middle of a character.

publish() is O(1) in transcript length — flat at ~0.025 µs per update whether the transcript is 5 KB or 200 KB, against ~0.007 µs for a byte offset allocating the same delta string.

word_timestamps deliberately stays cumulative alongside the now-incremental partial_text. It is not a delta field — it is the finalized set so far, which is why the provisional last word is dropped. Each field matches its own contract rather than each other; commented in place.

No server change. run_transcription_stream already skips an event with no partial text, and emit_if_nonempty drops an event only when every field is empty, so a window that decodes no new text still delivers its word timestamps.

kroko_asr's final result depended on the bug

The one change here that is not mechanical. Its finalize() was:

auto event = process_streaming_audio(true);
result.text_output = event.partial_text;

reading the whole transcript out of the partial — which worked only while the partial restated it. Making partials incremental therefore broke the final result: eleven correct deltas, then text_output = " times longer than you". Both sides are std::optional<Transcript>, so it compiles equally well either way and nothing in the tree covered it. finalize() now builds through make_result(), the same path the offline route uses.

The only other place that reads a transcript out of a partial is src/capi/audiocpp.cpp:226, which is correct and documented — it presents a stream event's partial through that event's text accessor, which is what a delta is.

Verification

Every family driven through the CLI on assets/resources/sample_16k.wav, including the three left on their own implementations:

family partials concatenate to text_output
parakeet_tdt 7 yes
kroko_asr 11 yes
higgs_audio_stt 4 yes
vibevoice_asr_streaming 5 yes
voxtral_realtime (own impl) 33 yes
qwen3_asr (own impl) 1 yes
sense_asr (own impl) 1 yes

Plus tests/unittests/test_partial_text.cpp: growth, 2/3/4-byte characters arriving one byte at a time, a revision inside the agreement window and one behind it, a truncated decode that regrows, a shrinking transcript, reset(), empty edges.

What is not covered

  • The byte-fallback path is covered by unit test and by vocabulary analysis, not by a live multilingual decode. I have no sample that drives a tokenizer into fallback, so the held-back-character path has not run against a real model — only its inputs have been shown to be reachable.
  • nemotron_asr and the two vibevoice_asr chunk-append sites are deliberately untouched: they receive deltas produced upstream rather than diffing a running transcript, so the publisher does not apply.

Reported downstream as christopherthompson81/AudioCpp-Bindings#68.

A partial is the text decoded since the last one. The CLI's PartialTextRenderer
says so and appends them into a scrolling transcript; the server forwards each
one as an OpenAI-shaped `transcript.text.delta`, which is incremental by
specification. Every other streaming ASR family emits the increment --
vibevoice_asr and higgs_audio_stt via a common-prefix diff, voxtral_realtime
via a published-bytes offset.

This family published `merged_decode()`, which re-renders the whole transcript
from every token decoded so far. So a client that does what the contract says
built the transcript back up quadratically:

    partial_text=Some call me nat
    partial_text=Some call me nature. Others call me
    partial_text=Some call me nature. Others call me Mother Nature. I'
    ...

appending to "Some call me natSome call me nature. Others call me...".

Now:

    partial_text=Some call me nat
    partial_text=ure. Others call me
    partial_text= Mother Nature. I'
    partial_text=ve been here for over four point
    partial_text= five billion years
    partial_text=, twenty two thousand five
    partial_text= hundred times longer than you.

which concatenates byte for byte to the `text_output` the same run reports.

Diffed against what was already published rather than tracking a byte count,
because re-decoding with more right context can revise earlier text rather than
only extending it; the common prefix is backed off to a UTF-8 boundary so a
revision landing mid-character cannot split a code point across two deltas.

No server change: `run_transcription_stream` already skips an event with no
partial text, and `emit_if_nonempty` only drops an event when every field is
empty, so a window that decodes no new text still delivers its word timestamps.

Verified against Parakeet-TDT-0.6B-v3 q8_0 on assets/resources/sample_16k.wav,
the before and after above coming from that run.
Review follow-up to the commit before it. Two defects in that change.

**A delta could end part way through a character.** The UTF-8 backoff only
guarded the point where the diff diverges -- the *start* of a delta -- and did
nothing about its end. The tokenizer falls back to bytes for text its vocabulary
does not cover, so a decode can stop mid-sequence: with `emitted_text_` at "abc"
and a decode of "abc\xE4", the prefix is 3, \xE4 is a lead byte rather than a
continuation so nothing was backed off, and the lone \xE4 went out as the delta.
That is invalid UTF-8 by the time it reaches the SSE JSON.

`complete_utf8_end` now bounds a delta at the last complete sequence, and
`emitted_text_` records what actually went out, so a held-back character is
reconsidered against the decode that completes it rather than skipped. The final
`text_output` is unaffected either way, so nothing is lost if a stream ends with
a character still held.

**The two fields in the event have different shapes, and nothing said so.**
`partial_text` is now what is new while `word_timestamps` stays cumulative.
That is deliberate -- word_timestamps is not a delta field, it is the finalized
set so far, which is why the provisional last word is dropped -- but with the
text field changing shape it is worth stating rather than leaving to be
rediscovered. Comment only.

Helpers exercised directly over a replay of decode sequences: ASCII growth, a
3-byte character arriving one byte at a time, Cyrillic growth, an incomplete
tail, and a shrinking decode. Re-ran Parakeet-TDT-0.6B-v3 q8_0 on
assets/resources/sample_16k.wav: partials unchanged and still concatenating
byte for byte to text_output.
Scope expansion on the two commits before it, which fixed parakeet_tdt alone.
The reason parakeet was wrong is that there was nothing to be right with: every
family that publishes partial text carried its own copy of the arithmetic, and
the copies disagreed. Three shapes across seven families:

  - a common-prefix diff, duplicated verbatim in higgs_audio_stt and
    vibevoice_asr, helper and caller both
  - a published-bytes offset, open-coded in voxtral_realtime, qwen3_asr and
    sense_asr
  - no diffing at all -- parakeet_tdt and kroko_asr published the whole running
    transcript as the partial

kroko_asr is the second instance of the bug 0xShug0#68 reported, found by sweeping for
the pattern rather than by hitting it: `event.partial_text = result.text_output`
where result is the combined decode of everything so far. Same corruption, same
cause, and it would have survived a parakeet-only fix.

engine::runtime::PartialTextPublisher is now the single implementation and all
seven use it. It keeps what has actually been published and returns the
increment, with the two properties none of the copies had:

  - a delta never ends part way through a UTF-8 sequence. A tokenizer falls back
    to bytes for text its vocabulary does not cover, so a decode can stop
    mid-character; publishing that puts half a code point on the wire and it
    reaches the SSE JSON encoder as invalid UTF-8. The tail is held for the
    update that completes it.
  - a delta never starts inside one either, when a decode revises published text
    rather than only extending it. Nothing can retract a delta already sent, so
    the consumer is wrong either way on a real revision; this keeps it from also
    being spliced into the middle of a character.

tests/unittests/test_partial_text.cpp covers growth, 2/3/4-byte characters
arriving one byte at a time, a revision, a shrinking decode, reset, and the
empty edges.

Verified: partial_text_test passes; Parakeet-TDT-0.6B-v3 q8_0 on
assets/resources/sample_16k.wav still emits seven partials concatenating byte
for byte to text_output; and the AudioCpp-Bindings suite runs green against this
build across kokoro_tts, citrinet_asr, parakeet_tdt, sortformer_diar and
bs_roformer, C and C# agreeing on every reported value.
…artial

Caught by running the family rather than reading it. finalize() did:

    auto event = process_streaming_audio(true);
    result.text_output = event.partial_text;

which returned the whole transcript only because partial_text restated the whole
transcript every time. With partials now being increments, that reported the
final window alone as the result: a stream that had correctly emitted eleven
deltas ended with text_output = " times longer than you".

So this family's final result depended on the bug the previous commits fixed.
Nothing in the types said so, and it compiles either way.

finalize() now builds its result the way the offline path already does, from
combined_decoded() through make_result(), which also carries the speech
segments and word timestamps it was assembling by hand.

Verified against Kroko-ASR-GGUF community q8_0 on
assets/resources/sample_16k.wav: eleven partials whose concatenation equals
text_output exactly. Before the whole series of commits, the same run emitted
eleven copies of a growing transcript.

Also ran sense_asr (SenseVoice-Small q8) over the same clip: one window, one
partial, matching text_output.
Review follow-up. Three fixes, one of them a real bug in the publisher.

**A held-back character could be sent twice.** publish() assigned
`published_ = transcript[0, publishable)` unconditionally, including when
nothing was publishable. A decode that truncates mid-character therefore
shortened `published_` past text that had already gone out, and the decode
restoring the character sent it a second time:

    publish("ab\xE4\xB8\x80")  -> "ab\xE4\xB8\x80"   published_ = 5 bytes
    publish("ab\xE4\xB8")      -> ""                 published_ = 2 bytes  <-- rewound
    publish("ab\xE4\xB8\x80")  -> "\xE4\xB8\x80"     <-- consumer sees it twice

`published_` is what the consumer has, and that cannot be taken back, so it now
only ever moves forward: when nothing whole is new, publish() returns early and
leaves it alone. Covered by two new cases -- a truncated decode that regrows,
and a shrinking transcript whose text returns.

**`std::size_t` and `<cstddef>`.** The header declared bare `size_t`, relying on
it leaking out of `<string>`. That holds on this libstdc++ and is not guaranteed;
the CI matrix includes Windows and macOS.

**Include placement.** partial_text.h had landed mid-way through a standard
library block in one header and ahead of alphabetically earlier entries in four
others, plus a stray double blank line in higgs_audio_stt. Cosmetic only.

Re-ran parakeet_tdt (7 partials), kroko_asr (11) and voxtral_realtime (33) after
the change: all unchanged, all still concatenating to text_output.
@0xShug0

0xShug0 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

@christopherthompson81 I think it’s safe to migrate higgs_audio_stt, vibevoice_asr, parakeet_tdt, and kroko_asr to the shared publisher, since they either already have the same computational complexity or currently emit partials incorrectly.

However, Voxtral Realtime and Sense ASR have already been optimized directly by their respective teams, as have Granite ASR, Fun ASR, and VibeASR. Qwen3 ASR is also used in production pipelines. Since the shared publisher scans the entire transcript on every update, replacing the existing byte-offset approach could introduce a performance regression. We shouldn’t change already-correct, optimized implementations just for consistency, especially if doing so introduces a performance regression.

The framework streaming helpers are better suited to models without native streaming support, where we need to add pseudo-streaming.

Another issue is UTF-8. The UTF-8 unit test manually constructs incomplete byte sequences. The real question is: do the affected models actually produce such incomplete byte sequences through their tokenizer and streaming paths? This feel like a synthetic case, not a real issue.

For example, Qwen3-ASR outputs complete characters:

Update 1: "你"     // E4 BD A0
Update 2: "你好"   // E4 BD A0 E5 A5 BD

rather than incomplete UTF-8 fragments:

Update 1: "\xE4"
Update 2: "\xE4\xBD"
Update 3: "\xE4\xBD\xA0"  // 你

I’d suggest investigating which models actually exhibit/reproduce this issue under realistic workloads, rather than relying solely on synthetic unit tests, and making UTF-8 handling opt-in for the affected models rather than applying it broadly.

@0xShug0

0xShug0 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

Another example: parakeet-tdt uses BPE with Metaspace decoding. Its 8,192 base vocabulary pieces are valid Unicode strings, with no byte-fallback pieces or byte-level decoding, so its token boundaries do not split UTF-8 characters.

Review feedback on the PR: replacing a byte-offset with a diff that rescans the
whole transcript every update is a performance regression, and a fair objection.
Measured it rather than argued: over a 10,000-update session the publisher cost
7.3 us per update against 0.004 us for the offset, and grew with transcript
length. Both causes were incidental rather than inherent to the shape.

  - The prefix check walked byte by byte. memcmp over the shared span instead:
    7.3 us -> 0.49 us.
  - `published_` was reassigned from the whole transcript on every update,
    copying it each time. Appended in the common case instead.
  - The remaining O(n) was the prefix check itself, and it was buying nothing:
    a delta that has gone out cannot be retracted, so a divergence far behind
    the publish point is not something this can act on even when it finds one.
    Checking agreement over a bounded window instead makes publish() O(1).

Now flat at ~0.025 us per update whether the transcript is 5 KB or 200 KB,
against ~0.007 us for a byte-offset that allocates the same delta string. Same
complexity class; the difference is one bounded memcmp.

The two tests added state what the window buys and what it gives up: a revision
near the end -- where a streaming decode actually revises -- is seen and the
delta resumes from it; one further back than the window is not, and cannot be,
because the text it would correct has already gone out.

Verified byte-identical output against the byte-offset implementations on the
three families that have one, driving each from the CLI on the same clip:

    voxtral_realtime  33 partials  IDENTICAL
    qwen3_asr          1 partial   IDENTICAL
    sense_asr          1 partial   IDENTICAL

and parakeet_tdt (7), kroko_asr (11) and vibevoice_asr_streaming (5) unchanged,
each still concatenating to its own text_output.
@christopherthompson81

Copy link
Copy Markdown
Contributor Author

Thanks — both objections were worth taking seriously, and the first one was simply right. I've pushed a commit rather than argued.

Performance: you were right, and it was incidental rather than inherent

Measured before defending. Over a 10,000-update session the publisher cost 7.3 µs per update against 0.004 µs for the byte offset, growing with transcript length. Three causes, all incidental:

  • the prefix check walked byte by byte — memcmp over the shared span instead: 7.3 µs → 0.49 µs
  • published_ was reassigned from the whole transcript each update, copying it every time — appended in the common case instead
  • the remaining O(n) was the prefix scan, and it was buying nothing. A delta that has gone out cannot be retracted, so a divergence far behind the publish point is not something the publisher can act on even when it finds it. Checking agreement over a bounded window makes publish() O(1).

Now, with both paths allocating the delta string as the real code does:

transcript publisher byte-offset + substr
5 KB 0.027 µs/update 0.007 µs/update
50 KB 0.027 µs/update 0.007 µs/update
200 KB 0.024 µs/update 0.008 µs/update

Flat — same complexity class. The difference is one bounded memcmp, around 16 ns.

Behaviour on the optimised families: byte-identical

Rather than ask you to take that on trust, I drove each family that has a byte-offset implementation through the CLI on the same clip, once with its own implementation and once with the publisher, and diffed the partial sequences:

voxtral_realtime   byte-offset 33 partials | publisher 33 partials | IDENTICAL
qwen3_asr          byte-offset  1 partial  | publisher  1 partial  | IDENTICAL
sense_asr          byte-offset  1 partial  | publisher  1 partial  | IDENTICAL

So nothing optimised is degraded — same output, same complexity class. If you would still rather leave those three alone on principle, say so and I will drop them from the PR; the four you named are the ones that were actually broken. But the regression argument for excluding them no longer holds.

UTF-8: you are right about Parakeet, and it is not synthetic for two of the four you would migrate

Checked your Parakeet claim and it holds exactly — Metaspace decoder, and zero <0xNN> pieces in the vocabulary, so byte_fallback: true in its config can never actually fire. Token boundaries there cannot split a character. Same for kroko_asr, which uses plain pieces.

But higgs_audio_stt and vibevoice_asr are ByteLevel, with all 256 single-byte tokens present. Taking Higgs's real 151,643-entry vocabulary and running the engine's own decode path (decode_byte_level over the concatenated pieces):

'ᚠ' U+16A0  ->  3 byte tokens [157, 248, 254]
  after token 1:  e1        INVALID UTF-8
  after token 2:  e19a      INVALID UTF-8
  after token 3:  e19aa0    valid: 'ᚠ'

Five of fourteen characters I tested (Runic, Devanagari, Hangul Jamo Extended, Gothic, CJK Compatibility) have no whole-character token in that vocabulary, so the tokenizer must emit them as byte sequences. And text_decoder.cpp calls token_callback after every generated token, so steps 1 and 2 are partials that go on the wire.

I take the point that my unit test constructed the bytes by hand — the evidence above is from the model's actual vocabulary, and I should have gone and got it before claiming the risk.

On making it opt-in

I think it already is, by behaviour rather than by flag. For a model that emits whole characters per update — your Qwen3 example — the publisher's output is byte-identical to a byte offset:

update 1 "你"    -> emits 你
update 2 "你好"  -> emits 好

Nothing is held back, because nothing is incomplete. The hold-back only engages when a sequence is genuinely mid-character, and it releases on the very byte that completes it:

after byte 1 of 你   (nothing emitted)
after byte 2 of 你   (nothing emitted)
after byte 3 of 你   e4 bd a0  -> 你

So a family that cannot produce split sequences pays one bounded memcmp and gets identical output. A flag would let it skip 16 ns and add a way to get the setting wrong.

Happy to go either way on scope — the four you named, or all seven — but I wanted the decision to rest on measurements rather than on my say-so.

@0xShug0

0xShug0 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

@christopherthompson81 Thanks for the quick update! Let’s keep the PR scoped to the four models (higgs_audio_stt, etc.), since they don’t require extensive testing.

Per review: voxtral_realtime, qwen3_asr and sense_asr keep their own
byte-offset implementations. Those were optimised by the teams that own them
and were already publishing partials correctly, so migrating them was
consistency rather than a fix -- and consistency is not worth touching working,
tuned code that someone else is responsible for.

Reverted to origin/main exactly; the net diff no longer touches those six files.
The shared publisher stays for the four that needed it: parakeet_tdt and
kroko_asr were restating the whole transcript as a partial, higgs_audio_stt and
vibevoice_asr carried duplicate copies of the same prefix diff between them.

Re-verified after narrowing, driving each from the CLI on the same clip:

    parakeet_tdt      7 partials   concatenate to text_output
    kroko_asr        11 partials   concatenate to text_output
    higgs_audio_stt   4 partials   concatenate to text_output
    voxtral_realtime 33 partials   concatenate to text_output  (own implementation)
    qwen3_asr         1 partial    concatenate to text_output  (own implementation)
    sense_asr         1 partial    concatenate to text_output  (own implementation)

partial_text_test still passes; the O(1) work in the previous commit stands, and
still matters for the four that keep the publisher.
@christopherthompson81

Copy link
Copy Markdown
Contributor Author

Scoped to the four, and the description is updated to match.

voxtral_realtime, qwen3_asr and sense_asr are reverted to origin/main exactly — the net diff no longer touches those six files. Re-verified all seven families from the CLI after narrowing, including the three back on their own implementations:

parakeet_tdt      7 partials   concatenate to text_output
kroko_asr        11 partials   concatenate to text_output
higgs_audio_stt   4 partials   concatenate to text_output
voxtral_realtime 33 partials   concatenate to text_output  (own implementation)
qwen3_asr         1 partial    concatenate to text_output  (own implementation)
sense_asr         1 partial    concatenate to text_output  (own implementation)

The O(1) work stays — it still matters for the four that keep the publisher, and it means the UTF-8 check costs them one bounded memcmp rather than a rescan.

@christopherthompson81

Copy link
Copy Markdown
Contributor Author

One thing worth passing on, and not a request to re-widen this PR — the scoping stands.

While checking whether the UTF-8 case was real for the families I was migrating, I looked at the three you asked me to leave alone. Two are fine; one has the bug already, independent of anything here.

sense_asr is not exposed. It concatenates pieces and replaces the metaspace marker, no byte-level decoding — same shape as parakeet_tdt. Nothing to do.

voxtral_realtime can emit a split character today. Its Tekken vocabulary is base64-decoded raw byte strings, and decode() concatenates them directly. All 256 single-byte tokens are present, and of twelve characters I tested, ten have no whole-character token — including a plain emoji:

'😀'  ->  4 byte tokens
  after token 1:  f0        INVALID UTF-8
  after token 2:  f09f      INVALID UTF-8
  after token 3:  f09f98    INVALID UTF-8
  after token 4:  f09f9880  valid: '😀'

take_stream_delta publishes streaming_text_.substr(streaming_published_bytes_) with no boundary check, and the family emits per token — 33 partials for the 14-second sample clip — so those intermediate states are individual transcript.text.delta payloads. An emoji in ASR output is not exotic.

The irony is that voxtral's delta emission came from #127, "Emit streaming transcript deltas instead of restating the transcript" — the same fix this PR makes for parakeet_tdt and kroko_asr. That PR solved the restating half; the UTF-8 half is still open there. You'll know better than me whether patrickvonplaten wants to pick it up — I'd rather you route it than have me tag someone into a PR they aren't part of.

qwen3_asr is byte-level too (decode_bpe maps each token codepoint back through unicode_utf8_to_byte), but it emits per window rather than per token, so it would need a window's generation to stop mid-character. Narrower, and I have not established whether that can actually happen — flagging it as unchecked rather than claiming it.

To be straight about the standard of evidence: this is vocabulary analysis plus the code path, not a live capture of the model emitting an emoji mid-stream. I have no sample that makes it do so. It is the same standard as the Higgs evidence above, and I would not call either one reproduced until someone has the audio to trigger it.

@christopherthompson81

Copy link
Copy Markdown
Contributor Author

Followed up on the qwen3_asr caveat I left open above — it is not exposed, and I should retract the implication that it might be.

The structure rules it out rather than making it unlikely. process_one_stream_chunk calls run_single, which runs thinker_.generate() to completion and decodes the entire token list in a single postprocessor_.decode(). The delta it publishes is that whole window transcript:

auto item = run_single(make_request(item_request));
const std::string delta = streaming_text_.empty() ? item.text_output->text
                                                  : " " + item.text_output->text;
streaming_text_ += delta;

So streaming_published_bytes_ only ever advances to a point where a complete decode ended — the streaming boundary and the decode boundary are the same place. There is no mid-token state for the offset to cut through. That is the difference from voxtral_realtime, which publishes after every generated token and so has boundaries inside a character's byte sequence.

The one residual is if max_new_tokens truncated generation mid-character, but that would put the same bytes in the window's own text_output and in the final transcript, and the offline path would have it identically — a truncation concern, not a streaming-delta one.

So of the three you scoped out: sense_asr not exposed, qwen3_asr not exposed, and voxtral_realtime the only one worth passing on.

@0xShug0
0xShug0 merged commit 3b90d6e into 0xShug0:main Sep 15, 2026
6 checks passed
@0xShug0

0xShug0 commented Sep 15, 2026

Copy link
Copy Markdown
Owner

@christopherthompson81 Thanks! PR merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants