apps/speakers: wespeaker thresholds + owner-bootstrap quality gates
Ship spec §1–§3 from cpo/specs/in-flight/speaker-attribution-wespeaker.md.
§4 (noisy-segment overlap guard) is deferred pending CPO spec revision.
- New apps/speakers/encoder_config.py with seven LOCKED constants
(ENCODER_ID + OWNER_THRESHOLD=0.43, ACOUSTIC_HIGH=0.36,
ACOUSTIC_MEDIUM=0.22, OWNER_BOOTSTRAP_MIN_STMTS=30,
OWNER_BOOTSTRAP_MIN_MEDIAN_DURATION_S=1.5,
OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25=0.30).
- owner.py and attribution.py import constants from encoder_config.py;
resemblyzer-era literals removed.
- _embed_statements persists durations_s lockstep with embeddings into
the per-segment .npz; _load_embeddings_file returns a 3-tuple and
every caller is updated (no compat shim).
- detect_owner_candidate adds three post-cluster quality gates
(too_few_stmts, median_duration_too_short, cluster_too_diffuse) that
defer candidate lock-in with recommendation="low_quality" and the
LOCKED diagnostic shape; awareness state surfaces low_quality
through api_owner_status, _owner_section, and the workspace UI
empty-state copy.
- Tests cover the encoder_config import contract, all three gate
paths, the 3-tuple loader (with and without durations_s), and the
low_quality api_owner_status branch.
Pre-existing flaky-fixture and voiceprints.npz metadata-serialization
bugs surfaced by the speaker test gate are fixed minimally to keep
make test-app APP=speakers green.
Co-Authored-By: OpenAI Codex <codex@openai.com>