Command Flowcharts
Status: Current Last updated: 2026-09-16 03:36 EDT
Option-driven flowcharts for every batchalign processing command. Each diagram shows how CLI flags route through different code paths at runtime. For the higher-level dispatch sequence diagrams, see Command Lifecycles.
align
The most complex command. CLI flags control FA engine selection, timing mode, UTR pre-pass behavior, and incremental processing.
flowchart TD
start([align invoked]) --> read[Read CHAT file]
read --> resolve_audio[Resolve audio file]
resolve_audio --> ensure_wav[ensure_wav: convert mp4→wav if needed]
ensure_wav --> parse[parse_lenient → ChatFile]
parse --> reuse_check{Complete reusable\n%wor timing?}
reuse_check -->|Yes| reuse[Refresh main-tier bullets from %wor\nmechanically, resolve overlap, THEN\noptionally regenerate %wor from the\nresolved state]
reuse_check -->|No| count[count_utterance_timing → timed, untimed]
reuse --> done([Output .cha file])
count --> utr_check{untimed > 0?}
utr_check -->|No| skip_utr[Skip UTR, all timed]
utr_check -->|Yes| utr_engine_check{--utr engine\nconfigured?}
utr_engine_check -->|Yes: --utr| run_utr_pass["run_utr_pass()"]
utr_engine_check -->|No: --no-utr| warn_interp[Log warning\nFall back to interpolation]
run_utr_pass --> utr_done[Re-serialize CHAT\nwith recovered timing]
utr_done --> group
warn_interp --> group
skip_utr --> group
group[group_utterances → time windows]
group --> before_check{--before path\nprovided?}
before_check -->|Yes| incremental[process_fa_incremental\nDiff old vs new, copy stable %wor,\nreuse preserved groups]
before_check -->|No| full[process_fa\nProcess all groups]
incremental --> engine_select
full --> engine_select
engine_select{--fa-engine?}
engine_select -->|whisper| whisper_fa[Whisper engine\nonset times only\nmax_group_ms from the engine = 20000]
engine_select -->|wav2vec / cantonese| wav2vec_fa[Wave2Vec engines\nword start+end\nmax_group_ms from the engine = 15000]
whisper_fa --> pause_check
wav2vec_fa --> pause_check
pause_check{--pauses?}
pause_check -->|Yes| preserve[WordGapHealing::PreserveMeasured\nkeep each word's own end]
pause_check -->|No| heal[WordGapHealing::Heal\nextend a word to the next word's start\nwhen the gap is small and plausible]
preserve --> cache_check
heal --> cache_check
cache_check[Cache lookup: BLAKE3 keys]
cache_check --> worker_infer[execute_v2(task="fa") misses → Python FA worker\nprepared audio + prepared text]
worker_infer --> dp_align_fa[DP-align model output → transcript words]
dp_align_fa --> inject_fa[Inject word-level timings into AST]
inject_fa --> retry_check{FA\nsucceeded?}
retry_check -->|Yes| overlap_policy
retry_check -->|No + retryable| fallback_check{Untimed utts\nnot recovered?}
fallback_check -->|Yes + not tried| fallback_utr["Fallback: run_utr_pass()\n(at most once)"]
fallback_utr --> retry_loop[Retry FA with\nrecovered timing]
retry_loop --> cache_check
fallback_check -->|No or already tried| backoff[Backoff + retry]
backoff --> cache_check
overlap_policy{"--end-overlap-policy?\n(default: preserve-cross-speaker)"}
overlap_policy -->|"preserve-cross-speaker (default)"| resolve_same[Resolve same-speaker overlap from\nmeasured word hulls; cross-speaker untouched]
overlap_policy -->|clamp-all-adjacent| resolve_all[Same resolution for every\nadjacent pair, any speakers]
resolve_same --> wor_check
resolve_all --> wor_check
wor_check{--wor / --nowor?}
wor_check -->|--wor| gen_wor["Generate %wor tier\n(from the RESOLVED state)"]
wor_check -->|--nowor| skip_wor[Omit %wor tier]
gen_wor --> merge_check
skip_wor --> merge_check
merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations transform]
merge_check -->|No| validate
merge --> validate[Post-validate → serialize CHAT output]
validate --> done([Output .cha file])
UTR Detail: Strategy Selection (Auto disabled)
When --utr-strategy auto (the default), the strategy is currently always
GlobalUtr regardless of file content or language. Two-pass overlap-aware
recovery is reachable only via the explicit --utr-strategy two-pass
override.
flowchart TD
auto(["--utr-strategy auto\n(default)"]) --> always_global["GlobalUtr\n(monotonic single-pass)"]
explicit_global(["--utr-strategy global"]) --> force_global["GlobalUtr\n(explicit override)"]
explicit_two(["--utr-strategy two-pass"]) --> force_two["TwoPassOverlapUtr\n(explicit override)"]
Why Auto is currently disabled (2026-03-30): the previous content/
language-aware gate auto-picked TwoPassOverlapUtr for English files
containing +< or CA overlap markers. It was disabled after an operator
reported alignment regressions on real files; investigation found that
enforce_monotonicity() only checks start times, not end times, so
overlapping utterance bullets go uncorrected. The two-pass tuning was also
based on four corpora and never broadly validated. The previously-measured
gains under that mechanism (English: +4.3pp SBCSAE, +3.8pp Jefferson;
non-English on Hakka/Welsh/German/Serbian: GlobalUtr matched or beat
TwoPassOverlapUtr) are retained here as historical context for the
benchmark numbers that motivated the original gate, not as a description
of current behavior.
Implementation: ResolvedUtrStrategy::from_options() in
crates/batchalign/src/runner/dispatch/options.rs resolves Auto to global
and retains explicit two-pass tuning. The language-agnostic
overlap-detection helper select_strategy() in
crates/batchalign/src/chat_ops/fa/utr.rs (library) remains, but is no
longer called from the Auto path.
UTR Detail: run_utr_pass() internals
The UTR pre-pass and fallback share the same run_utr_pass() helper,
which chooses between full-file and partial-window ASR:
flowchart TD
entry(["run_utr_pass()"]) --> parse[Parse CHAT\ncount timed vs untimed]
parse --> zero{untimed == 0?}
zero -->|Yes| noop([Return: nothing to do])
zero -->|No| ratio{untimed < 50%\nAND audio > 60s?}
ratio -->|Yes| partial_mode
subgraph partial_mode [Partial-Window ASR]
direction TB
pw_find[find_untimed_windows\nPadding: 500ms, merge overlaps]
pw_find --> pw_loop["For each window (start, end):"]
pw_loop --> pw_seg_cache{Segment\ncache hit?}
pw_seg_cache -->|Hit| pw_use[Use cached segment ASR]
pw_seg_cache -->|Miss| pw_extract["extract_audio_segment()\nffmpeg -ss/-to → cached WAV"]
pw_extract --> pw_infer[infer_asr on segment]
pw_infer --> pw_store[Cache segment result]
pw_store --> pw_use
pw_use --> pw_offset[Offset token times\nby window start_ms]
pw_offset --> pw_loop
end
ratio -->|No| full_mode
subgraph full_mode [Full-File ASR]
direction TB
ff_cache{Full-file\ncache hit?}
ff_cache -->|Hit| ff_use[Use cached ASR]
ff_cache -->|Miss, non-Rev| ff_infer[infer_asr on full audio]
ff_cache -->|Miss, Rev| ff_rev{Validated raw Rev\nevidence?}
ff_rev -->|Hit| ff_project[Project retained timed words]
ff_rev -->|Typed miss| ff_rev_call[Authorized Rev request]
ff_rev_call --> ff_rev_commit[Validate + required durable commit]
ff_rev_commit --> ff_project
ff_project --> ff_store
ff_infer --> ff_store[Cache full result]
ff_store --> ff_use
end
partial_mode --> inject
full_mode --> inject
inject["inject_utr_timing()\nExact-subsequence fast path,\nelse global DP"]
inject --> result([Return updated CHAT + UtrResult])
UTR Detail: Zero-Duration Bullet Prevention
Zero-duration utterance bullets (•T_T•, where start_ms == end_ms) fail E362
validation and perpetuate across every subsequent align re-run: the FA
postprocess clamps all word timings to the utterance range [T, T], drops
every word bullet (nothing survives the start >= end check), and
update_utterance_bullet then has nothing to work from, so the zero-duration
bullet is preserved unchanged.
The root cause is Whisper’s 20ms DTW grid, which can return the same timestamp for multiple adjacent short words (“mhm”, “yeah”). This affects short backchannels in dense-dialogue corpora (first seen in OCSC).
BA3 applies a three-layer defence in crates/batchalign/src/chat_ops/fa/utr.rs
and crates/batchalign/src/chat_ops/fa/orchestrate.rs:
flowchart TD
asr["ASR token stream\n(Whisper DTW output)"] --> filter
subgraph L1 ["Layer 1: Token filter (dispatch/utr.rs)"]
filter{"token.end_ms\n<= token.start_ms?"}
filter -->|Yes| drop1[Drop token before UTR\nsees it at all]
filter -->|No| utr_in[Pass to UTR]
end
utr_in --> dp["Global DP alignment\nCHAT words ↔ ASR tokens"]
dp --> assign["Plan: per-utterance word matches\nand a typed UtrTimingProposal\n(Positive | NonPositive), decided once"]
subgraph L2 ["Layer 2: UTR span guard (utr.rs project_global_utr_plan)"]
assign --> zdcheck{"proposal is\nNonPositive?"}
zdcheck -->|Yes| drop2[Leave utterance untimed\ncount as unmatched]
zdcheck -->|No| mono_check
end
subgraph L3 ["Layer 3: UTR monotonicity pass (utr.rs project_global_utr_plan)"]
mono_check{"utt.start_ms\n< prev_non_overlap.end_ms?"}
mono_check -->|Yes: DTW collision| advance["Advance start_ms = prev.end_ms\nExtend end_ms if needed"]
mono_check -->|No| assign_bullet[Assign utterance bullet]
advance --> assign_bullet
end
assign_bullet --> fa_in["FA postprocess\n(orchestrate.rs)"]
subgraph L4 ["Safety net: monotonicity enforcement (orchestrate.rs)"]
fa_in --> mono_enforce{"prev.end_ms\n> next.start_ms?"}
mono_enforce -->|end_clamp safe| clamp_end[Clamp prev.end_ms\nto next.start_ms]
mono_enforce -->|would produce\nzero-duration| strip[Strip bullet entirely\nbetter untimed than •T_T•]
end
Why three layers instead of one?
Each layer catches a different failure mode:
| Layer | Where | What it catches |
|---|---|---|
| 1 | dispatch/utr.rs asr_response_to_utr_tokens | Whisper returning start==end for a single-frame token |
| 2 | utr.rs project_global_utr_plan reading the plan’s UtrTimingProposal::NonPositive | DP aligning an utterance to an ASR token range whose span is zero or negative. The verdict is decided once, where the plan is built (UtrTimingProposal::spanning); projection takes no token stream and cannot re-derive it |
| 3 | utr.rs project_global_utr_plan monotonicity post-pass | Two adjacent non-overlap utterances assigned to tokens with the same start_ms (DTW collision at a shared boundary) |
| Safety net | orchestrate.rs enforce_monotonicity | Residual overlaps from any source; when clamping would produce zero-duration, strip entirely |
Layer 3 is the root-cause fix. Layers 1 and 2 handle degenerate single-token
cases. The safety net handles anything that slips through (e.g. cross-speaker
overlap that is not marked with +<).
Why not just rely on the safety net? Because stripping a bullet destroys
timing, the utterance goes back to untimed and must be recovered by FA. The
UTR-level fixes preserve timing: advancing start_ms to prev.end_ms keeps
both utterances timed and valid.
BulletSource Provenance: The Self-Healing Design
The three layers above prevent UTR from producing bad bullets. A complementary mechanism ensures that even if UTR set a slightly imprecise window, FA word timings are authoritative after alignment.
Every Bullet carries a non-serialized source: BulletSource field (in
talkbank-model/src/model/content/bullet.rs):
BulletSource | Who sets it | update_utterance_bullet behavior |
|---|---|---|
Utr | UTR pre-pass via Bullet::utr_hint() | Overwrite with FA word span |
Authoritative | Parser (hand-linked), Bullet::new(), or FA-derived | Union (never shrink) |
BulletSource is #[serde(skip)]: it never appears in CHAT output and
doesn’t change the file format.
flowchart TD
utr_bullet["UTR sets\nBullet::utr_hint(800, 3000)\nBulletSource::Utr"]
hand_bullet["Parser reads hand-linked bullet\nBullet::new(37397, 42983)\nBulletSource::Authoritative"]
utr_bullet --> fa_inject["FA injects per-word timings\n1000_1500, 1500_2000"]
hand_bullet --> fa_inject
fa_inject --> update["update_utterance_bullet()"]
update --> check{"source?"}
check -->|"Utr"| overwrite["Overwrite: bullet = 1000_2000\nFA span is authoritative"]
check -->|"Authoritative"| union_op["Union: bullet = 37397_42983\npreserves filler/gesture coverage"]
check -->|"None"| set["Set: bullet = word span"]
overwrite --> auth["Mark result as Authoritative"]
union_op --> auth
set --> auth
Why union for authoritative bullets? Hand-linked utterances may start
before the first FA-alignable word (e.g., &-uh filler that FA returns None
for) or end after the last word (e.g., a trailing &=laughs gesture). Without
union, re-running FA on these utterances would silently shrink their timing,
losing the hand-annotated context coverage. This was a real bug
encountered in the ACWT corpus. The BulletSource design preserves
the correct
behavior for authoritative bullets while enabling the self-healing property
for UTR hints.
Contrast with batchalign2 (jan9 baseline, commit 84ad500b):
BA2 uses the same conceptual approach, DP alignment of ASR tokens against the reference transcript, utterance-level timing derived from word-level timing, but its implementation differs in two important ways:
-
Utterance timing is derived dynamically, not stored.
Utterance.alignmentindocument.py:182is a computed property: it scans forward/backward throughword.timeto find the first and last timed word. There is no explicit utterance bullet; the CHAT serializer writes it on demand. This means a zero-duration utterance bullet can only arise if the first and last timed words in an utterance share a timestamp, which BA2 prevents at the word level insidewhisper_fa.py:183-224: each word’send_msis set to the start of the next word, and any word wherestart >= endis dropped (word.time = None). -
No cross-utterance monotonicity enforcement. BA2 performs no check that
utt_n.alignment[0] >= utt_{n-1}.alignment[1]. Adjacent utterances can and do share start timestamps from DTW collisions; BA2 tolerates this because it never runs anenforce_monotonicitypass that would turn the shared start into a zero-duration span.
Why BA3 needs explicit monotonicity enforcement: BA3 separates UTR (bullet
assignment) from FA (word-level timing), whereas BA2 derives utterance timing
from word timing. This means BA3 can produce utterance bullets that are valid
on their own but conflict with each other (same start_ms), and the
enforce_monotonicity end-clamp pass, which BA2 does not have, converts
those into zero-duration spans. The Layer 3 fix eliminates the conflict at the
source before enforce_monotonicity ever sees it.
transcribe
Creates CHAT from audio. The longest pipeline, with optional follow-up commands chained automatically.
When do you need --diarize?
- Rev.AI (the default engine): Rev.AI returns multi-speaker labels
natively as part of its ASR response. Those labels are always applied
to the transcript, you get multi-speaker output without
--diarize. - Rev.AI with explicit
--diarize: BA3 runs a dedicated speaker stage on top of Rev output and treats its segments as authoritative before utterance segmentation. - Whisper-based engines (
whisper,whisper_hub,whisper_rs): these engines do not return speaker labels. Passing--diarize(or--diarization enabled) runs a dedicated speaker model as an additional stage. - Default:
--diarization auto= disabled. Identical to batchalign2’s--diarize/--nodiarize default=False. The old BA2 help text claiming Rev ignored--diarizewas stale; the pipeline wiring did not ignore it.
Rev.AI skip_postprocessing: For English (en) and French (fr),
Rev.AI is called with skip_postprocessing=true, matching BA2. This lets
BA3’s own BERT utterance segmentation model handle sentence boundaries from
raw ASR output, rather than relying on Rev.AI’s built-in punctuation which
produces giant monologue blobs. For all other languages, Rev.AI applies its
own post-processing.
flowchart TD
start([transcribe invoked]) --> resolve[Resolve audio file]
resolve --> ensure_wav[ensure_wav: convert if needed]
ensure_wav --> diarize_check{--diarization?}
diarize_check -->|"enabled"| transcribe_s["Command: transcribe_s\nASR + dedicated speaker relabeling\nRev or Whisper"]
diarize_check -->|"auto/disabled\n(default)"| transcribe_m["Command: transcribe\nDefault path\nRev labels used directly when present"]
transcribe_s --> engine_check
transcribe_m --> engine_check
engine_check{--asr-engine?}
engine_check -->|whisper| whisper[Whisper local ASR]
engine_check -->|rev| rev_cache["Rev.AI evidence resolution\ncontent-addressed lookup + per-key lease"]
engine_check -->|"whisperx, whisper_oai"| refused["Refused: engine not implemented"]
rev_cache --> rev_hit{valid evidence hit?}
rev_hit -->|Yes| rev_replay[Replay provider-shaped transcript]
rev_hit -->|No| rev_infer["Typed miss authorizes Rev.AI\nlanguage ID + submit + poll"]
rev_infer --> rev_commit[Validate and durably commit evidence]
rev_replay --> asr_tokens
rev_commit --> asr_tokens
whisper --> asr_tokens
asr_tokens["Raw ASR tokens\nword + start_s + end_s + optional speaker + confidence"]
asr_tokens --> convert["convert_asr_response()\nALWAYS groups tokens by speaker label\nNo use_speaker_labels parameter"]
convert --> dedicated_check{"--diarization enabled?"}
dedicated_check -->|No| postprocess
dedicated_check -->|Yes| speaker_v2["execute_v2(task=speaker)\nprepared audio → raw diarization segments\npyannoteAI Precision-2 by default"]
speaker_v2 --> postprocess
subgraph postprocess ["Rust post-processing: process_raw_asr()"]
direction TB
p1[1. Compound merging] --> p2[2. Timed word extraction\nseconds → milliseconds]
p2 --> p2check{lang=yue?}
p2check -->|Yes| p2d["2d. Cantonese normalization\nonce per monologue\nOpenCC + domain replacements"]
p2check -->|No| p3
p2d --> p3
p3[3. Multi-word splitting\ntimestamp interpolation]
p3 --> p4[4. Number expansion\ndigits → word form]
p4 --> p5[5. Long-turn splitting\nchunk at >300 words]
p5 --> p6[6. Retokenization\npunctuation-based utterance splitting]
end
postprocess --> speaker_apply{Dedicated speaker\nsegments present?}
speaker_apply -->|Yes| project["project_speakers_onto_chunks()\nAssign timed words by summed overlap\nSplit at speaker changes"]
speaker_apply -->|No| utseg_check{"with_utseg?\ndefault: true"}
project --> utseg_check
utseg_check -->|Yes| run_utseg[process_utseg_with_evidence\nBERT-based re-segmentation]
utseg_check -->|No| build_chat
run_utseg --> build_chat["build_chat → ChatFile AST\nHeaders, participants, %wor tiers"]
build_chat --> mor_check{"with_morphosyntax?\ndefault: false"}
mor_check -->|Yes| run_mor[process_morphosyntax\nPOS + lemma + depparse]
mor_check -->|No| merge_check
run_mor --> merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| output
merge --> output[Serialize → .cha output]
output --> done([Output .cha file])
diarize
Produces anonymous acoustic speaker turns without ASR or CHAT mutation. The
standalone command defaults to local Pyannote and can explicitly select
pyannoteAI Precision-2 or local NeMo. Integrated
transcribe --diarization enabled defaults to pyannoteAI Precision-2 and is
shown separately in the transcribe diagram above. Both use the same typed
raw/derived speaker-evidence cache.
flowchart TD
start([diarize invoked]) --> resolve[Resolve media inputs]
resolve --> source[Admit exact inference-source bytes\n+ versioned PCM-preparation recipe]
source --> key[Content + semantic request identity]
key --> derived{Validated derived turns?}
derived -->|hit| tracks
derived -->|miss| raw{Validated raw evidence?}
raw -->|hit| normalize[Versioned local normalization]
raw -->|miss / refresh| prepare[Rust materializes canonical\nmono 16 kHz float32 PCM]
prepare --> select{--speaker-engine}
select -->|pyannote default| local[Local TalkBank Pyannote]
select -->|pyannote-ai| cloud[Paid pyannoteAI Precision-2]
select -->|nemo| nemo[Local NeMo]
local --> validate[Validate + durably commit raw evidence]
cloud --> validate
nemo --> validate
validate --> normalize
normalize --> tracks[Map native labels deterministically\nto anonymous PAR0..PARn tracks]
tracks --> turns[Write one .turns.json artifact\nper input file]
turns -. optional later step .-> chatter["chatter rediarize\nproject tracks onto existing CHAT"]
chatter --> roles["chatter speaker-id + external evidence\nor adjudication assigns semantic CHAT roles"]
morphotag
Adds %mor and %gra tiers. Files are processed independently and
concurrently (bounded by num_workers).
flowchart TD
start([morphotag invoked]) --> parse[Parse file → AST]
parse --> ca_check{"@Options: CA\nin header?"}
ca_check -->|Yes| ca_passthrough[Serialize parsed file as-is\nNo %mor/%gra added\nNo provenance injected]
ca_passthrough --> done
ca_check -->|No| clear[Clear existing %mor/%gra tiers]
clear --> collect[collect_payloads\nPer-utterance word lists with language metadata]
collect --> retok_check{--retokenize?}
retok_check -->|Yes: --retokenize| stanza_retok[TokenizationMode::StanzaRetokenize\nStanza may split/merge words]
retok_check -->|No: --keeptokens| preserve[TokenizationMode::Preserve\nKeep original tokenization]
stanza_retok --> lang_check
preserve --> lang_check
lang_check{--skipmultilang?}
lang_check -->|Yes| skip_non_primary[MultilingualPolicy::SkipNonPrimary\nSkip utterances in non-primary language]
lang_check -->|No: --multilang| process_all[MultilingualPolicy::ProcessAll\nProcess all utterances regardless of language]
skip_non_primary --> cache
process_all --> cache
cache[Cache lookup: BLAKE3 keys\nwords + lang + terminator + special forms + engine version]
cache --> inject_hits[Inject cache hits immediately]
inject_hits --> worker[execute_v2(task="morphosyntax") misses\nprepared_text batch → Stanza NLP pipeline]
worker --> inject_results[inject_results → insert %mor/%gra tiers]
inject_results --> before_check{--before path?}
before_check -->|Yes| incremental[process_morphosyntax_incremental\nSkip NLP for unchanged utterances]
before_check -->|No| full_inject[Process all utterances]
incremental --> merge_check
full_inject --> merge_check
merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| validate
merge --> validate[Alignment validation\n%mor word count must match main tier]
validate --> done([Output .cha file])
@Options: CA pass-through: When the file’s header declares
@Options: CA, the pipeline skips morphotagging entirely and serializes
the parsed file unchanged (mirroring @Options: NoAlign for align).
The decision is made once per file from the option header; no
per-utterance content scan is involved.
utseg
Utterance segmentation. Pools all utterances across files into a single GPU batch.
flowchart TD
start([utseg invoked]) --> parse[Parse all files → ASTs]
parse --> collect[collect_payloads\nExtract word sequences per utterance]
collect --> cache[Cache lookup: BLAKE3 keys\nwords + lang]
cache --> worker[execute_v2(task="utseg") misses\nprepared_text batch → raw parse trees]
worker --> apply[Apply segmentation\nSplit/merge utterances at predicted boundaries]
apply --> merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| serialize
merge --> serialize[Serialize → .cha output]
serialize --> done([Output .cha files])
translate
Translates utterances and injects %xtra tiers.
flowchart TD
start([translate invoked]) --> parse[Parse all files → ASTs]
parse --> collect[collect_payloads\nExtract utterance text + source/target language]
collect --> cache[Cache lookup: BLAKE3 keys\ntext + src_lang + tgt_lang]
cache --> worker[execute_v2(task="translate") misses\nprepared_text batch → raw translations]
worker --> inject[inject %xtra tiers with translated text]
inject --> merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| serialize
merge --> serialize[Serialize → .cha output]
serialize --> done([Output .cha files])
coref
Coreference resolution. Document-level, sparse output, English-only.
flowchart TD
start([coref invoked]) --> parse[Parse all files → ASTs]
parse --> collect[collect_payloads\nExtract sentences: full document context]
collect --> worker[execute_v2(task="coref")\nprepared_text batch → structured chain refs]
worker --> inject[inject %xcoref tiers, sparse\nOnly utterances with coreferent mentions]
inject --> merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| serialize
merge --> serialize[Serialize → .cha output]
serialize --> done([Output .cha files])
style collect fill:#ffd,stroke:#aa0
note1[No caching: full-document context\nmakes per-utterance keys meaningless]
collect --- note1
compare
Reference-projection workflow. The released command now emits the projected reference transcript, and the benchmark/internal main-shaped path is a separate materializer rather than the command contract.
flowchart TD
start([compare invoked]) --> discover[Discover primary .cha files\nskip *.gold.cha companions]
discover --> pair[Pair FILE.cha with FILE.gold.cha]
pair --> found{Gold companion found?}
found -->|No| fail[Report file error]
found -->|Yes| morph[process_morphosyntax\nmain transcript only]
pair --> parse_gold[parse_lenient raw gold\n→ gold AST]
morph --> parse_main[MorphotaggedMain::from_proof\n→ the judged main document\n(never re-parsed from text)]
parse_main --> bundle[compare()\nconform + local window search + local DP\nComparisonBundle: main view, gold view,\nstructural word matches, metrics]
parse_gold --> bundle
bundle --> released[GoldProjectedCompareMaterializer\nproject_gold_structurally()]
bundle --> internal_main[MainAnnotatedCompareMaterializer (internal/benchmark)\ninject %xsrep / %xsmor on main]
released --> safe{Exact structural match?}
safe -->|Yes| copy[Copy %mor / %gra / %wor]
safe -->|No, full gold coverage| mor_only[Project %mor only]
safe -->|No, partial or unsafe| keep[Keep gold dependent tiers unchanged]
copy --> goldannot[Inject %xsrep / %xsmor on gold]
mor_only --> goldannot
keep --> goldannot
goldannot --> merge_check
internal_main --> internal_done([Internal main-annotated view])
merge_check -->|Yes| merge[merge_abbreviations]
merge_check -->|No| metrics[Write .compare.csv]
merge --> metrics
metrics --> done([Output .cha + .compare.csv])
The public command now uses the projected-reference branch. The main-annotated branch remains available only for internal consumers such as benchmark.
opensmile
Acoustic feature extraction. Rust resolves media, prepares typed audio, and sends a live V2 request to the Python worker.
flowchart TD
start([opensmile invoked]) --> resolve[Resolve audio files]
resolve --> prep[Rust audio prep\nprepare mono PCM artifact]
prep --> feature_check{--feature-set?}
feature_check -->|eGeMAPSv02| egemaps[eGeMAPSv02 features\n88 acoustic descriptors]
feature_check -->|ComParE_2016| compare[ComParE_2016 features\n6,373 acoustic descriptors]
feature_check -->|Custom| custom[Custom feature set name]
egemaps --> worker
compare --> worker
custom --> worker
worker[execute_v2(task=\"opensmile\") → Python worker\nExtracts acoustic features from prepared audio]
worker --> output[Write CSV output\nContent-type: csv]
output --> done([Output .csv files])
avqi
Acoustic Voice Quality Index. Rust resolves paired audio, prepares typed PCM
artifacts, and sends a live V2 request. Requires paired continuous speech
(.cs.wav) and sustained vowel (.sv.wav) audio.
flowchart TD
start([avqi invoked]) --> resolve[Resolve paired audio files\n.cs.wav + .sv.wav per speaker]
resolve --> prep[Rust audio prep\nprepare CS + SV PCM artifacts]
prep --> worker[execute_v2(task=\"avqi\") → Python worker\nparselmouth + torchaudio analysis]
worker --> output[Write AVQI results\nHarmonics-to-noise ratio, jitter, shimmer, etc.]
output --> done([Output results])
benchmark
Composite workflow that runs transcribe, then compare, and materializes both the hypothesis CHAT and the CSV metrics.
flowchart TD
start([benchmark invoked]) --> resolve[Resolve audio file + companion gold .cha]
resolve --> transcribe[Rust transcribe workflow\nProduce hypothesis CHAT]
transcribe --> compare[Rust compare workflow\nDP alignment + WER metrics]
compare --> merge_check{--merge-abbrev?}
merge_check -->|Yes| merge[Merge abbreviations in hypothesis CHAT output]
merge_check -->|No| output
merge --> output[Write hypothesis .cha + .compare.csv]
output --> done([Output results])
There is no CLI command literally named speaker; that name belongs to the
low-level worker task. User-facing diarization is available both inside
transcribe --diarization enabled and through standalone diarize.
Cross-Cutting: Cache Behavior
All processing commands (except coref) follow this cache interaction
pattern. The cache policy is controlled by --override-media-cache.
flowchart TD
start([Cache check]) --> policy{--override-media-cache?}
policy -->|No| lookup[BLAKE3 hash → cache lookup\nHot: moka in-memory\nCold: SQLite]
policy -->|Yes: --override-media-cache| skip_cache[Skip cache, force recompute]
lookup --> hit{Cache hit?}
hit -->|Yes| inject_cached[Inject cached result\nNo worker IPC needed]
hit -->|No| miss[Send to Python worker]
skip_cache --> miss
miss --> infer[Worker returns raw ML output]
infer --> cache_put[Store result in cache\nRow written under the task's namespace]
cache_put --> inject_fresh[Inject fresh result into AST]
inject_cached --> done([Continue pipeline])
inject_fresh --> done
Cross-Cutting: Incremental Processing (–before)
Supported by morphotag and align. Compares old vs new CHAT to skip
unchanged content.
flowchart TD
start([--before provided]) --> read_before[Read before file]
read_before --> diff[diff_chat: classify utterances\nAdded / Removed / Modified / Unchanged]
diff --> preserve[Preserve stable dependent tiers\nand refresh reusable timing]
preserve --> filter[Filter: only reprocess\nAdded + Modified content that still needs work]
filter --> process[Run NLP only where reuse and cache\ncannot satisfy the request]
process --> merge_results[Merge: preserved results from before\n+ fresh results for changed]
merge_results --> done([Full output with minimal recomputation])
This page last changed: 2026-09-16 (commit 197c81e6). The whole book last changed: 2026-09-16 (commit 34d249d8).