Skip to content

Streaming ASR: token-dedup false-positives on shared word prefixes cause text/word loss (no temporal check) #787

Description

@kekoborn

Summary

removeDuplicateTokenSequence() in StreamingAsrManager's per-chunk dedup path matches purely on token-ID equality, with no check that the matched span corresponds to the same region of audio. This causes false-positive "duplicate" detection whenever two different, unrelated words happen to share a short common subword prefix within the search window — corrupting the transcript by silently dropping the shared-prefix tokens from the second word, including whatever token carries the word-start (leading-space) marker.

This is a token-loss bug, not just a formatting/cosmetic issue: the removed tokens are gone from accumulatedTokens, so both the live streaming updates and finish()'s final reconstruction are corrupted.

Where

  • Sources/FluidAudio/ASR/AsrTranscription.swift, removeDuplicateTokenSequence(previous:current:maxOverlap:) (~line 446), specifically the "extended search" block (~line 484-509).
  • Called from transcribeStreamingChunk() (~line 257-268) with previousTokens = StreamingAsrManager's accumulatedTokens.

Repro (Russian streaming ASR)

Streaming a real conversation through StreamingAsrManager (parakeet-tdt-0.6b-v3-coreml, chunkSeconds: 11, leftContextSeconds: 2, rightContextSeconds: 2), two unrelated words spoken ~10+ seconds apart both start with "тран-":

  • трансформацию (earlier in the session)
  • транскрибируется (a later, different chunk)

Expected: ...трансформацию... транскрибируется... (two distinct words).
Actual: ...трансформациюскрибируется... — the leading "тран" of the second word is gone and it's glued directly onto the first, with no space.

Vocab-level evidence

Checked parakeet_v3_vocab.json:

6841 → " тра"   (leading space — word-start marker)
3726 → "тра"    (no leading space — mid-word continuation)
2394 → "ран"

Both words tokenize starting with the same [" тра", "ран"] (= " тран") sequence — 2 tokens, well within the default maxOverlap: 12 and boundarySearchFrames: 20. removeDuplicateTokenSequence finds this as a "duplicate" against the tail of previousTokens (from the unrelated earlier word) and strips it from the new chunk's tokens — including token 6841 (" тра"), which is what carries the space-before-word marker. Downstream detokenization then concatenates the remainder ("скрибируется") directly onto the previous word.

Why this is a general/structural issue, not a one-off

The matcher has no temporal-proximity check even though timestamps are already available at the call site (hypothesis.timestamps / adjustedTimestamps). Any two words separated in time that happen to share a ≥2-token common prefix within the search window are at risk — plausible fairly often in morphologically rich languages (Russian prefixes like транс-, при-, раз-, про-, etc.) combined with the model's fine-grained subword vocabulary (many 1-3 character Cyrillic pieces).

Suggested direction

Require the matched overlap to also be validated against timestamp/frame adjacency — i.e., only treat a token match as a genuine chunk-boundary duplicate if both occurrences fall within the actual audio overlap region (left/right context seconds), not just anywhere within an arbitrary token-count window. A coincidental prefix match between words spoken at very different times should never qualify.

Environment

  • FluidAudio: 0.12.6
  • Model: parakeet-tdt-0.6b-v3-coreml
  • Swift 6.3.2, macOS 26.5.1
  • StreamingAsrConfig(chunkSeconds: 11, hypothesisChunkSeconds: 1, leftContextSeconds: 2, rightContextSeconds: 2, minContextForConfirmation: 10, confirmationThreshold: 0.5)

Happy to provide more repro pairs from logged sessions if useful — raw audio isn't shareable (real user conversations), but I have several more token-boundary examples from production logs.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions