Quality gates¶
How Voxint decides what to trust: which enhancement output is kept, which
diarized voices earn a speaker proposal, and what "grounded" means. Every
threshold below is a Settings field (see .env.example). The values shown are
the defaults, chosen conservatively for titanet-large embeddings and meant to be
calibrated against locally adjudicated same-/different-speaker pairs before you
loosen them.
LLM enhancement: best-effort by design¶
Enhancement is optional garnish over an immutable transcript (raw_text is
forever), and its failure semantics follow from that:
| Bound | Setting | Default |
|---|---|---|
| Per-attempt timeout | LLM_TIMEOUT_SECONDS |
300 s |
| Attempts per batch | LLM_ATTEMPTS_PER_BATCH |
2 |
| Batch size | LLM_BATCH_MAX_SEGMENTS / LLM_BATCH_MAX_CHARS |
32 / 12 000 |
| Per-run wall-clock budget | LLM_RUN_BUDGET_SECONDS |
4 h |
| Circuit breaker | LLM_CONSECUTIVE_FAILURE_LIMIT |
3 batches |
- Segments travel in contiguous, ID-keyed batches. A reply must return exactly the requested segment-index set or the whole batch is rejected; partial or misaligned output is never trusted.
- A failed batch (transport, HTTP error, malformed reply) retries once, then
its segments keep
enhanced_text = NULL. Three consecutive failed batches open the circuit for the rest of the run. The budget bounds total LLM time inside theenhance_matchstage lease. - An unreachable LLM never fails a run. Speaker matching still executes; only matching/persistence invariant violations fail the stage.
- Segment text is content, never instructions. The enhancement prompt instructs the model to treat every segment's words strictly as transcript content to edit. A segment that reads like a command ("ignore previous instructions", "reply with a single word", "you are now a translator", "drop the other segments") is still returned unchanged, not obeyed. This matters because transcripts are untrusted input and small local models will otherwise follow instructions embedded in speech; a hardened prompt measurably stops that (verified against the local-LLM qualification corpus). It is a best-effort guard, not a sandbox; the structural batch-integrity check above is the backstop that rejects any reply that still deviates.
- Name hints are consumed only on the BYO path. Enhancement can also surface
explicit spoken-name hints for speaker attribution, but only capable BYO models
have them parsed: the scoped bundled model (#67) does no attribution, so its
enhancement reply is not parsed for
name_hints(#85): a hallucinated hint from a weak model cannot fail the batch, and attribution stays exclusively on the BYO name producer. The enhancement prompt is identical on every path (removing the hints block measurably regressed the 4B model's segment faithfulness); only the reply parsing differs. - Requests are deterministic by default (
temperature = 0, greedy). The client carries a fixed sampling profile; greedy is deliberate for a faithfulness task (reproducible, conservative), not an accident.
Turn eligibility (matching input)¶
A diarization_turns row feeds matching only if it carries an embedding and its
overlap ratio (overlap_seconds / duration) is at most
MATCH_MAX_OVERLAP_RATIO (0.20); heavily overlapped speech contaminates pooled
embeddings. Skipped windows keep their auditable too_short / low_snr reason
and are ignored. Eligible turns are weighted by usable non-overlap seconds,
capped at MATCH_TURN_WEIGHT_CAP_SECONDS (10 s) so one long monologue can't
drown the rest of the evidence.
Cosine proposal and grounding gates¶
Per label: a duration-weighted unit centroid over eligible turns is compared
against each roster speaker's enrollment centroid (mean of unit vectors, never
max-over-enrollments), strictly within one embedding_space. The top speaker is
proposed only when all proposal gates pass; grounded is claimed only when
the stricter grounding set also passes:
| Gate | Proposal | Grounded |
|---|---|---|
| Eligible turns | ≥ 2 | ≥ 3 |
| Usable seconds | ≥ 6 | ≥ 10 |
| Raw cosine (top-1) | ≥ 0.60 | ≥ 0.70 |
| Top-1 vs top-2 margin | ≥ 0.05 | ≥ 0.08 |
| Vote agreement (duration-weighted) | ≥ 0.60 | ≥ 0.67 |
Vote agreement is the fraction of eligible-turn weight whose individually nearest roster speaker is the proposed one, a per-turn consistency check that catches labels whose centroid only averages into a speaker.
An unmatched or ineligible label produces no proposal row: absence of
evidence is not a low-confidence proposal. P4 never creates speakers rows for
unknown voices; that is the adjudication UI's job (P5).
Auto-enrollment (see operations.md) uses the proposal-tier thresholds when deciding whether a label matches an existing roster speaker. A label that meets proposal but not grounding is linked to the matched speaker (growing its centroid), not skipped.
Separately, every run records an observational
match_candidatesrow per label (issue #113): what the matcher decided (accepted / rejected / ineligible), the top candidate, and the cosine / margin / vote-agreement behind it, kept even for the near-misses that yield no proposal. This is the opposite bookkeeping from the proposal surface above: it exists precisely to make false rejects visible, so the gates can eventually be measured and calibrated (epic #112). It is diagnostic only and feeds no attribution.The enrichment draft layer (issue #37) records the complementary fact at a different layer: a completed producer invocation that substantiated nothing persists as an explicit
enrichment_producer_runsrow withoutcome = 'none'. The two rules do not conflict. "No proposal row" keeps weak evidence out of the matching surface, while "we looked and found nothing" is itself reviewable information about the search, not a low-confidence claim. Enrichment candidates never enter matching at all.
Confidence is not probability¶
speaker_assignments.confidence stores (cosine + 1) / 2, clamped to
[0, 1]: a transformed similarity, not a calibrated probability. Margin and
vote agreement act as separate decision gates rather than being blended into
an opaque score. llm_hint rows store confidence = NULL, because
model-reported confidence is not calibrated and is not recorded.
transcript_segments.confidence (issue #53) is the same kind of thing for ASR:
exp(avg_logprob) clamped to [0, 1], a transformed likelihood (the geometric
mean of the segment's token probabilities), not the probability that the
segment is correct. It is persisted verbatim from the whisper service, NULL when
the backend reports none (older runs never fabricate a value). The review console
flags segments below review_low_confidence_threshold (default 0.6) for triage
and labels them "uncertain, not necessarily wrong", never "N% correct". The
threshold is a configurable starting default, deliberately not a UI slider (a
non-technical operator mis-setting it would distrust the signal); refine it
against a real-corpus histogram before exposing any tuning UI. This reads existing
model output and does not touch inference; parity/contract gates are unaffected.
Draft review priority (issue #42, enrichment-triage.md)
is the same kind of thing for enrichment drafts: a read-time, explainable
attention-ordering score fused from name-match, voice support, independent
domains, source authority, and cross-source agreement. It is capped below
certainty, never stored, never auto-accepts, and is compared only within one
review surface. Being review ordering rather than inference, it too leaves
parity gates unaffected.
Named ≠ grounded¶
An LLM name hint (method = 'llm_hint', proposed_name) is review-side
evidence only: never a speaker_id, never grounded, even when the heard
name matches a roster speaker's display_name exactly. Enforcement is
layered. DB CHECK constraints (grounded ⇒ cosine + concrete speaker;
method-shape checks keep cosine and hint rows disjoint) combine with a single
typed writer, voxint.speakers.matching.replace_run_proposals, through which
every proposal insert passes. One hint per label: an explicit
self-introduction ("I'm Jane…") beats being named by someone else, and within
a kind the earliest heard wins.
Enrichment drafts (issue #37): read names stay suggestions¶
The "named ≠ grounded" rule extends unchanged to the enrichment draft layer.
A name mined from metadata, a transcript, or the web is stored in
enrichment_candidates as a suggestion about identity with its evidence,
and even an operator accepting it records only a profile_review_decisions
row: it never writes speakers.display_name, never creates a proposal, and
never resolves attribution (integration-tested). Draft score /
score_components are producer-local signals like the transformed cosine
above, not probabilities, and never comparable across producers. Effective
draft state is derived at read time (human decision > supersession stamp >
proposed) by enrichment/queries.py, mirroring how attribution is resolved
below.
The first producer (#38, names.offline) applies the rule with one further
distinction: only a self-introduction inside a cluster's own segment can
target that cluster (run_label). Every other signal (titles, descriptions,
channel names, "please welcome X") stays a run-level hint ("this name is
probably in the recording"), because knowing a name is present says nothing
about which voice it belongs to. Accepting a per-label suggestion prefills
the Enroll form but never submits it; the human act of enrollment, with its
acoustic eligibility gates, remains the only path from a heard name to a
roster identity.
Adjudication precedence (P5)¶
Attribution is resolved at read time, everywhere, by one rule
(adjudication/resolver.py):
- Effective human decision: the newest
adjudication_decisionsrow per (run, label), orderedcreated_at DESC, id DESC. Corrections are new appends; nothing is edited (the table's trigger forbids it). - Grounded cosine proposal: machine identity stands only at grounding strength.
- Otherwise the label is unresolved and the run sits in the review queue.
exclude suppresses speaker attribution, never transcript text. unknown is
a terminal human answer: the label leaves the queue without an identity.
llm_hint names are displayed as evidence and never resolve attribution.
Speaker enrollment builds its centroid from exactly the matching-side
eligibility rules and duration-capped weighting (imported from
speakers/matching.py). A label with no eligible embedded turns cannot be
enrolled; rule it unknown or assign it to an existing speaker instead.
Accepted risk: enrollment provenance is code-enforced¶
speaker_embeddings.source_* provenance consistency (the decision is an
assign for the same run/label/speaker) is guaranteed by the single writer
(adjudication/enrollment.py) plus the unique constraint on the source
decision id, not by cross-table DB triggers. A constraint trigger would be
the only way to make it structurally airtight, and it is deliberately omitted
as disproportionate for a single-writer path. Revisit if a second writer ever
appears.
Offline measurement¶
The quality gates above act at pipeline time. The measurement layer is the
offline harness: name accuracy against ground truth, acoustic agreement
verdicts, two-voter fusion, regression gate metrics. It is documented in
harness.md and exposed as voxint score … (file-based, DB-free).
The match_candidates evidence (issue #113) is what closes the loop: the
maintainer driver tools/export_match_evidence.py renders live runs, including
those near-miss decisions, into the harness input files, so the gates above can
be measured and a first attribution baseline produced (epic #112). The
attribution evaluation driver (tools/eval_attribution.py) carries the loop
further: it aligns corpus gold timing to predicted diarization slots, builds
FAR/FRR trials, and renders a dated baseline report. See the "Attribution
evaluation" sections of harness.md.
The grounded gate values above are the calibration target of issue #114. The
calibration runs a pre-registered selection on a dev split and a single
certification on a held-out confirm split, with the safety floor on impostor
clusters (open-set false accepts) rather than on cosine alone. The rule, the
tooling, and the CERTIFIED / NO_DECISION outcomes are documented in the
"Calibrating the auto-display band" section of harness.md.
See also¶
- architecture.md: where these gates sit in the pipeline and data model.
- gpu-contracts.md: the service contracts that produce the confidence, embedding, and diarization values gated here.
- enrichment-triage.md, harness.md, and the docs index.