@@ -371,19 +371,23 @@ func (o *Orchestrator) RunConnection(ctx context.Context, s *session.Session, in
371371 if brainPrefetchResultVal == nil {
372372 return nil
373373 }
374- if brainPrefetchResultVal .canonicalInput != canonical {
374+ if ! brainPrefetchCanonicalCompatible ( brainPrefetchResultVal .canonicalInput , canonical ) {
375375 return nil
376376 }
377377 if brainPrefetchReadyAt .IsZero () || time .Since (brainPrefetchReadyAt ) > brainPrefetchFresh {
378378 return nil
379379 }
380+ wasExactMatch := brainPrefetchResultVal .canonicalInput == canonical
380381 out := & brainPrefetchResult {
381382 canonicalInput : brainPrefetchResultVal .canonicalInput ,
382383 deltas : append ([]string (nil ), brainPrefetchResultVal .deltas ... ),
383384 finalText : brainPrefetchResultVal .finalText ,
384385 }
385386 brainPrefetchResultVal = nil
386387 brainPrefetchReadyAt = time.Time {}
388+ if ! wasExactMatch {
389+ o .metrics .SessionEvents .WithLabelValues ("brain_prefetch_fuzzy_hit" ).Inc ()
390+ }
387391 return out
388392 }
389393 if ready := getReady (); ready != nil {
@@ -392,7 +396,7 @@ func (o *Orchestrator) RunConnection(ctx context.Context, s *session.Session, in
392396
393397 var done chan struct {}
394398 brainPrefetchMu .Lock ()
395- if brainPrefetchInFlight && brainPrefetchCanonical == canonical {
399+ if brainPrefetchInFlight && brainPrefetchCanonicalCompatible ( brainPrefetchCanonical , canonical ) {
396400 done = brainPrefetchDone
397401 }
398402 brainPrefetchMu .Unlock ()
@@ -2101,6 +2105,63 @@ func shouldSpeculateBrainCanonical(canonical string) bool {
21012105 return words >= brainPrefetchMinWords
21022106}
21032107
2108+ func brainPrefetchCanonicalCompatible (prefetchedCanonical , committedCanonical string ) bool {
2109+ prefetchedCanonical = strings .TrimSpace (prefetchedCanonical )
2110+ committedCanonical = strings .TrimSpace (committedCanonical )
2111+ if prefetchedCanonical == "" || committedCanonical == "" {
2112+ return false
2113+ }
2114+ if prefetchedCanonical == committedCanonical {
2115+ return true
2116+ }
2117+ // Common case: one canonical transcript is an incremental extension/rollback of the other.
2118+ if canonicalIsProgressiveContinuation (prefetchedCanonical , committedCanonical ) ||
2119+ canonicalIsProgressiveContinuation (committedCanonical , prefetchedCanonical ) {
2120+ return true
2121+ }
2122+ // Guarded fuzzy match for tiny STT corrections:
2123+ // require a strong shared word prefix and only allow one trailing mismatch.
2124+ pWords := strings .Fields (prefetchedCanonical )
2125+ cWords := strings .Fields (committedCanonical )
2126+ minWords := len (pWords )
2127+ if len (cWords ) < minWords {
2128+ minWords = len (cWords )
2129+ }
2130+ if minWords < 4 {
2131+ return false
2132+ }
2133+ if absInt (len (pWords )- len (cWords )) > 2 {
2134+ return false
2135+ }
2136+ shared := sharedWordPrefixCount (pWords , cWords )
2137+ return shared >= minWords - 1 && shared >= 3
2138+ }
2139+
2140+ func sharedWordPrefixCount (a , b []string ) int {
2141+ if len (a ) == 0 || len (b ) == 0 {
2142+ return 0
2143+ }
2144+ limit := len (a )
2145+ if len (b ) < limit {
2146+ limit = len (b )
2147+ }
2148+ count := 0
2149+ for i := 0 ; i < limit ; i ++ {
2150+ if a [i ] != b [i ] {
2151+ break
2152+ }
2153+ count ++
2154+ }
2155+ return count
2156+ }
2157+
2158+ func absInt (v int ) int {
2159+ if v < 0 {
2160+ return - v
2161+ }
2162+ return v
2163+ }
2164+
21042165func shouldStartBrainPrefetchEarly (partialText , canonical string , utteranceAge time.Duration ) bool {
21052166 words := wordsInCanonical (canonical )
21062167 normalized := normalizeSemanticEndpointText (partialText )
0 commit comments