Skip to content

fix(azad-text): stop number conversion from mangling ordinary speech#3

Open
Montana wants to merge 1 commit into
spence:mainfrom
Montana:fix/number-word-conversion
Open

fix(azad-text): stop number conversion from mangling ordinary speech#3
Montana wants to merge 1 commit into
spence:mainfrom
Montana:fix/number-word-conversion

Conversation

@Montana

@Montana Montana commented Jul 4, 2026

Copy link
Copy Markdown

Hey @spence,

  • Digit sequences are cardinal-only, so ordinals are no longer consumed: 'give me one second' no longer becomes 'give me 1 2'. Ordinals still combine into cardinals ('twenty fourth' -> 24).
  • Plural scale words stay verbatim: 'hundreds of people' no longer becomes '100 of people' (generalizes the existing 'seconds' carve-out).
  • Number candidates ending in 'and' are rejected so the connective survives: 'between five and six' no longer becomes 'between 5 six'. Interior 'and' still parses ('two hundred and sixty six' -> 266).

Adds regression tests for all three cases; each fails on main.

Cheers,
Michael

- Digit sequences are cardinal-only, so ordinals are no longer consumed:
  'give me one second' no longer becomes 'give me 1 2'. Ordinals still
  combine into cardinals ('twenty fourth' -> 24).
- Plural scale words stay verbatim: 'hundreds of people' no longer
  becomes '100 of people' (generalizes the existing 'seconds' carve-out).
- Number candidates ending in 'and' are rejected so the connective
  survives: 'between five and six' no longer becomes 'between 5 six'.
  Interior 'and' still parses ('two hundred and sixty six' -> 266).

Adds regression tests for all three cases; each fails on main.
spence added a commit that referenced this pull request Jul 8, 2026
Goal #3 of the dual-stream rework: let the stronger 560ms model sharpen the
live caption in place while the user speaks, without churning it (goal #1).

The refined stream is fed back into the live display in dual-stream mode, but
`emit_replacement_live_display` now clamps how far back a refined correction can
reach via a mode-specific mutable-tail budget: legacy keeps its wide 36-token
tail; dual-stream uses a tight 12-token tail. Because the 560ms stream lags the
80ms stream by ~1-2 words and can re-segment, the tight tail freezes the
committed prefix so subtle corrections (and filler decisions) stick instead of
flip-flopping, while a large divergence can only ever rewrite the volatile tail.

Measured headless on a 156s turn vs legacy_stitch: dual applies 64 in-place
word-sharpening corrections (for->forty, week->weekly, per->percent, ...), all
<=2 tokens, with live-caption rollback max=2 == legacy max=2 — parity on churn,
same correction profile, and faster. Replaces the earlier finalize-only swap.

`stabilize_live_display_replacement` / `find_live_display_stable_boundary` take
the mutable tail as a parameter; callers pass the mode-appropriate budget.
spence added a commit that referenced this pull request Jul 8, 2026
Make dual-stream the app default now that it beats legacy_stitch headless on all
three priorities: zero-churn live caption (rollback max 2 == legacy), fast
high-quality finalization (faster on every measured turn, no stitch/bailout), and
subtle in-place corrections from the 560ms stream (goal #3). Takes effect on the
next install; the running app is untouched. Live smoke test to follow in an idle
window before retiring the legacy stitcher machinery.
spence added a commit that referenced this pull request Jul 9, 2026
…eze-append)

During speech the caption was composed from the refined 560ms re-decode, which
re-segments its trailing window as more audio arrives. Every re-decode painted
straight onto the caption, so on repeated-phrase content it visibly swapped
between equivalent phrasings mid-speech ("sub agents" <-> "subagents",
"they're" <-> "their") and toggled punctuation on words already shown
("possible" <-> "possible,"). The two prior gates were blind to this: they
checked only token count and normalized match-keys, so count-neutral and
growth-riding flips passed freely.

Replace the whole-candidate accept/hold decision with per-token freeze-append
(`splice_live_display_forward`): every already-shown word is immutable. A
candidate may only grow (append net-new trailing words), complete the last
word being typed ("gen" -> "generate"), or otherwise freeze `previous` and
append just its new tail. Interior re-segmentations and cosmetic (case/
punctuation) re-renders of shown words are never repainted live — they settle
in the single coalesced ReplaceLine at finalize, which sees the whole turn and
is dither-proof. New words and the growing edge still stream at refined
quality, so this keeps the live-refinement value without the flicker; only
corrections to already-shown words defer to finalize (goal #1 zero-churn >
goal #3 in-place sharpening).

Finalize output is byte-for-byte unchanged. Investigated via multi-agent
replay + offline simulation over real recorded live-caption event streams
(headless file-replay can't reproduce the churn — the fast feed starves the
refined worker); word- and punctuation-flicker both go 66/129 -> 0 with zero
leading-edge lag. Adds splice unit tests + a match-key golden equivalence
test; removes the retired whole-candidate classifier.
spence added a commit that referenced this pull request Jul 9, 2026
The lagging 560ms refined stream re-segments its trailing window ("sub agents"
<->"subagents", punctuation toggles) and folds each re-decode into the caption,
so already-shown words visibly swap phrasings mid-speech (goal #1 violation).
The mutable tail bounds how far back a refined replacement may reach; at 12 it
was loose enough to re-write a whole clause of settled text.

Make the tail a PipelineConfig knob (--live-display-mutable-tail) and tune the
default 12 -> 4 from recorded-turn replay: aggregate visible caption churn drops
~60% (42 -> 17 flips across four turns; the churniest turn 24 -> 7) with the
pasted finalize decode byte-identical on every turn (goal #2 untouched). The
only cost is the refined stream landing in-place touch-ups (goal #3, lowest
priority) a little later.

It cannot stall the caption the way the reverted freeze-append did: the
monotonic streaming stream still supersedes a held replacement, so a tighter
tail only enlarges the frozen prefix, never withholds forward progress. Realtime
(wall-clock-paced) replay confirms the caption still reaches full length and
keeps pace (54 frames, 41 words, identical to the loose tail).

Stabilizer mechanics unit-tests are pinned to an explicit tail so the tuning
default is decoupled; a new test guards that 4 freezes a settled swap 12 tokens
back that 12 would let through. caption_churn.py measures churn from the
--events-jsonl stream for future caption-change validation.
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.

1 participant