Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Replacements in the batchalign3 Pipeline

Status: Current Last updated: 2026-05-19 20:22 EDT

This page documents how batchalign3 handles CHAT replacement annotations ([: ...]) end-to-end. For the canonical CHAT-format definition of what a replacement is (syntax, scope, AST shape, per-domain alignment rule), see the CHAT-format replacements reference in the chatter project. That reference is the source of truth; this doc is the pipeline-specific companion.

TL;DR

batchalign3 never emits [: ...] annotations. It preserves and consumes replacements that the user’s existing corpus contains. Domain-aware extraction routes the replacement form to %mor / %gra, and the original form to %wor / %pho / %sin / FA, per the CHAT-format rule. Four-site invariants in FA enforce that the two sides stay in sync; an analogous (currently undocumented) three- site invariant exists for %mor.

What batchalign3 Does and Doesn’t Do

OperationStatusWhere
Parse an existing replacement from input CHAT✅ Yestalkbank-parser (delegated; this repo doesn’t reimplement)
Preserve a replacement through extract→modify→inject round trips✅ Yesbatchalign content walkers
Route the right side to %mor/%gra extraction✅ Yesextract.rs::collect_replaced_word
Route the left side to %wor / FA / %pho / %sin✅ Yesfa/extraction.rs, extract.rs (per-domain branches)
Re-serialize to valid CHAT including the replacement✅ Yesmodel-layer WriteChat
Emit a new [: ...] annotation programmaticallyNoNothing in this codebase constructs a ReplacedWord
Sanitize an ASR token by wrapping it in [: ...]No (and would not work, replacement words are validated; see talkbank-tools doc §“Each Replacement Word Is Validated”)
Normalize a token via direct text mutation✅ Yes, separate mechanismasr_postprocess::cleanup (e.g. um&-um is a text edit, not a replacement annotation)

This distinction is the most common source of confusion: the batchalign3 pipeline produces “cleaned” word text by mutating AsrWord.text in place during ASR post-processing, NOT by emitting replacement annotations. A reader skimming the pipeline for “replacement” will find consume-side code only.

Per-Domain Extraction Policy

Domain-aware word extraction in ../chatter/crates/talkbank-transform/src/extract.rs::collect_replaced_word (line 129) is the central seam. When the walker encounters a ReplacedWord leaf, it picks one side of the pair based on the requested TierDomain:

Domain (caller)Side extractedUse
TierDomain::Morreplacement words (replaced.replacement.words)Stanza receives the corrected/intended form for UD analysis
TierDomain::Wororiginal word (replaced.word)%wor (timing) sees what was actually spoken
TierDomain::Phooriginal word%pho describes phonology of what was spoken
TierDomain::Sinoriginal word%sin records spelling-in-actual
Noneboth, recursing into all leavesfor transforms that traverse all content
flowchart TD
    asr["AsrWord stream\n(raw ASR tokens)"] -->|"asr_postprocess pipeline"| chat["ChatFile AST\n(includes user-supplied ReplacedWord nodes\nwhen reading existing CHAT)"]

    chat --> extract{"extract.rs::collect_replaced_word\nbranches on TierDomain"}

    extract -->|"Mor"| mor_words["Replacement words\n→ Stanza"]
    extract -->|"Wor / FA"| orig_w["Original word\n→ %wor / FA"]
    extract -->|"Pho"| orig_p["Original word\n→ %pho"]
    extract -->|"Sin"| orig_s["Original word\n→ %sin"]

    mor_words --> mor_inject["inject.rs::inject_morphosyntax\n(1:1 against replacement words)"]
    orig_w --> wor_inject["model-layer\nWorTier::from_words\n(1:1 against original)"]

    mor_inject --> chat
    wor_inject --> chat

    chat --> serialize["WriteChat → output .cha"]

This flow is the read-and-respect direction. The write-and-emit direction (a hypothetical “produce a new replacement from ASR output”) is not implemented anywhere in the pipeline.

The Four-Site Invariant (Forced Alignment)

For forced alignment specifically, the policy “a ReplacedWord contributes exactly one word to FA, the original spoken word, not the replacement words” is enforced at four code sites that must stay in sync. If any one site uses the wrong side, alignment desyncs by the delta in word count, and every subsequent timing in the same FA group shifts.

SiteFile:lineWhat it does
Extractioncrates/batchalign/src/chat_ops/fa/extraction.rsSends the original word to the FA worker
Countcrates/batchalign/src/chat_ops/fa/mod.rsCounts 1 for the ReplacedWord (regardless of replacement word count)
Injectioncrates/batchalign/src/chat_ops/fa/injection.rsConsumes 1 cursor slot, sets replaced.word.inline_bullet
Preservationcrates/batchalign/src/chat_ops/fa/mod.rs (collect_existing_fa_word_timings)Reads replaced.word.inline_bullet

The 2026-04-08 Bug This Invariant Prevents

Before this invariant was codified, an extraction site sent the replacement words while the count site still counted 1 for the ReplacedWord. For dis [: this], FA received 1 token (this) but the count expected 1 (dis), the symptom was correct for that word. But for <dis [: this] is> style content, the count and extraction disagreed, and every subsequent word in the FA group got the wrong timing. The invariant was named after that incident.

The Read-Only Test That Validates It

crates/batchalign/src/chat_ops/fa/tests/grouping_and_wor.rs::test_wor_policy_replacements_use_original_surface() constructs a fixture with what's is dis [: this] ? and asserts the extracted FA word list is ["what's", "is", "dis"]: note this is absent. If extraction drifts to using the replacement, the test fails loudly.

The Three-Site Invariant (%mor)

By symmetry with FA, an analogous invariant exists for %mor, extract, count, inject, that has historically been implicit. Naming it here so future readers can find it:

SiteFile:lineWhat it does
Extraction../chatter/crates/talkbank-transform/src/extract.rs:129 (collect_replaced_word)Sends the replacement words to Stanza
Countmodel.utterance.mor_alignable_word_count() (delegated to talkbank-model)Counts replacement-word count, not 1
Injectioncrates/batchalign-transform/src/morphosyntax/injection.rs::inject_resultsAsserts injected %mor item count == count, then injects

The reason this invariant has stayed implicit: extract/count/inject all delegate to talkbank-model’s domain-aware rules (TierDomain::Mor), so as long as the model’s per-domain rule is correct, the three sites stay synchronized for free. They are not independently coded against each other the way FA’s four sites are.

Test Coverage Gap

No test in this repo currently exercises %mor injection where the main-tier word is a ReplacedWord. The %mor injection code is correct by construction (it reuses TierDomain::Mor extraction), but a regression test would catch any future drift. Recommended fixture:

*CHI:	wanna [: want to] go .
%mor:	v|want inf|to v|go .
%gra:	1|3|AUX 2|3|MARK 3|0|ROOT 4|3|PUNCT

The test would assert:

  • Extraction under TierDomain::Mor yields ["want", "to", "go"] (not ["wanna", "go"]).
  • mor_alignable_word_count() returns 3 (not 2).
  • Injection succeeds when given a 3-item %mor line.

This is logged as an action item for follow-up; the analysis lives in the maintainers’ working notes.

What the Pipeline Does NOT Do

These are operations the pipeline deliberately does not perform. Listing them here so a contributor doesn’t reach for the wrong tool.

  • Emit replacements from ASR normalization. ASR cleanup mutates AsrWord.text directly (e.g. um&-um, 'cause(be)cause). The result is a single token whose surface form is CHAT-legal, there is no [: ...] wrapper.
  • Use replacements to carry CHAT-illegal text. Each replacement word goes through the standard Word validator. [: C-3PO] fails E220 the same way C-3PO on the main tier does: each replacement word is validated in its own right.
  • Generate replacements during retokenization. When Stanza re-tokenizes a word, the retokenize module rebuilds the AST in place (crates/batchalign-transform/src/retokenize/rebuild.rs). It preserves existing ReplacedWord nodes during reconstruction but does not create new ones.
  • Generate replacements during ASR retrace detection. Detected retraces produce WordKind::Retrace plus structural retrace nodes (Retrace, <...> [/]), which are a different mechanism. See crates/batchalign-transform/src/build_chat/utterances.rs::build_word_utterance for how retraces are emitted; WordKind::Replacement does not exist.

When to Reach for [%], [=], or [*] Instead

A common failure mode in this codebase has been reaching for [: ...] when what’s actually wanted is a free-form annotation that does not participate in word validation. The talkbank-model offers four such forms via ContentAnnotation (in talkbank-model/src/model/annotation/scoped/types.rs):

FormWhen to use
[% text]General comment about the word/utterance. Carries SmolStr (no word grammar applied). Right home for “ASR-original was X”.
[= text]Explanation of unclear speech. Idiomatic alongside xxx/yyy placeholders.
[+ text]Researcher note / context addition.
[* code]Error coding (with optional code).

These all attach as scoped_annotations and do not require their contents to satisfy CHAT word grammar. For ASR-introduced preservation use cases, [%] is the working candidate, not [:].

Source Citations

ConcernFile:line
Replacement extraction (per-domain branch)../chatter/crates/talkbank-transform/src/extract.rs:129 (collect_replaced_word)
FA extraction (uses original)crates/batchalign/src/chat_ops/fa/extraction.rs
FA countcrates/batchalign/src/chat_ops/fa/mod.rs
FA injectioncrates/batchalign/src/chat_ops/fa/injection.rs
FA preservationcrates/batchalign/src/chat_ops/fa/mod.rs
%mor injection (count check)crates/batchalign-transform/src/morphosyntax/injection.rs::inject_results
Retokenize preserves ReplacedWordcrates/batchalign-transform/src/retokenize/rebuild.rs
Read-only consumption testcrates/batchalign/src/chat_ops/fa/tests/grouping_and_wor.rs::test_wor_policy_replacements_use_original_surface
CHAT-format canonical referencethe CHAT-format replacements reference in the chatter project

See Also

  • CHAT Data Model (in the chatter project): how UtteranceContent variants (including ReplacedWord) flow through the pipeline; walk_words, WordItem, and per-domain extraction primitives.
  • ASR Token Pipeline, the disfluency / normalization rules that mutate AsrWord.text (a different mechanism from replacements).
  • The %mor Tier (in the chatter project): for what %mor represents and how it aligns.

This page last changed: 2026-07-30 (commit 1b974ba0). The whole book last changed: 2026-09-16 (commit 34d249d8).