16. Minimal Diff Between Successive Partial Transcript Hypotheses
turn.eager_end event that lets a downstream LLM start generating before the turn is fully finalized (cartesia.ai/blog/ink-2), which implies the model continually revises a partial-transcript hypothesis as more audio arrives rather than emitting one final string. Inference: computing a common-prefix diff between successive partials so a downstream consumer only has to patch the tail is a plausible way such a consumer would handle Ink-2-style streaming partials; it is not a description of Ink-2's own internal output format.Problem
A streaming STT model doesn't emit one final transcript — it emits a sequence of partial hypotheses that get revised, word by word, as more audio arrives, before locking in. A downstream consumer (a live caption UI, or an LLM tracking the conversation) shouldn't reprocess the whole transcript on every partial update; it just needs to know how much of the previous hypothesis is still valid and what changed after that point.
Given the previous partial transcript and the new one, each as a list of words, return the minimal patch: the length of their common prefix, and the words from the new transcript that come after it.
Source: src/16_partial_transcript_diff.py
def diff_transcript(previous_words: list[str], new_words: list[str]) -> tuple[int, list[str]]:
>>> diff_transcript(["I", "want", "a"], ["I", "want", "an", "apple"])
(2, ['an', 'apple'])
>>> diff_transcript(["hello"], ["hello", "world"])
(1, ['world'])
Step-by-Step Approach
- Walk both word lists together with a single index
i, starting at 0. - Advance
iwhileiis within both lists' bounds andprevious_words[i] == new_words[i]— this finds the longest common prefix in a single linear pass. - Stop as soon as the words diverge, or either list runs out — whichever comes first bounds how far the shared prefix can extend.
- The final value of
iis the common prefix length; no separate comparison pass is needed. - The patch to apply is simply everything in
new_wordsfrom indexionward:new_words[i:]. Applying the patch means keepingprevious_words[:i]as-is and replacing the rest with this slice.
The key insight is that finding a common prefix and computing the replacement tail are the same single linear scan, not two separate passes — the index where the scan stops is simultaneously the prefix length and the start of the slice that needs to be sent as the patch.
Reference solution
def diff_transcript(previous_words: list[str], new_words: list[str]) -> tuple[int, list[str]]:
# two-pointer scan for the longest common prefix, O(min(len)) time
i = 0
limit = min(len(previous_words), len(new_words))
while i < limit and previous_words[i] == new_words[i]:
i += 1
# everything after the shared prefix is the patch to apply
return (i, new_words[i:])
Key Functions & Tricks
min(len(a), len(b))— bounds the scan so it never indexes past the shorter of the two lists.while i < limit and a[i] == b[i]: i += 1— single linear pass that finds the common-prefix length directly, with no separate comparison step.new_words[i:]— slices the replacement tail using the exact same index the prefix scan stopped at.
How to Recognize This Pattern
Reach for a common-prefix two-pointer scan whenever a problem asks for the minimal difference between two sequences that are expected to mostly agree at the start and diverge later — successive versions of a streaming hypothesis, revision history of a document, or autocomplete-style incremental typing are all this shape. The signal is "only send/apply what changed" framed around two ordered sequences. A common variation generalizes this to a full edit-distance / diff algorithm (like difflib or Myers diff) when changes can also happen in the middle of the sequence, not just after a stable prefix — that's a strictly harder DP problem, not a two-pointer one. A common pitfall is comparing the full lists for equality first (an O(n) pass) and only then computing the diff (another O(n) pass) instead of realizing a single scan does both jobs at once.