personal memory agent

feat(speakers): port pyannote segmentation pass to Rust master

Add the pyannote segmentation model pass, per-window speaker statistics, and speaker-evidence decision logic to solstone-core-speakers. The pure crate stays dependency-free and inside the iOS gate. Add the pyannote ONNX role to solstone-core-speakers-onnx, which stays outside the iOS gate because ONNX Runtime remains host-only native linkage. This deliberately diverges from the Python structure in two places. Rust has one parameterised windowing pass where Python has three near-copies: overlap.compute_overlap_fraction, overlap.compute_overlap_and_logprobs, and diarize._run_pyannote. Rust also has one session-construction helper where Python loads pyannote twice under mismatched provider policies: overlap.py:145 uses _select_onnx_providers(), while diarize.py:78 hardcodes CPUExecutionProvider. Preserve and widen the single-site assertions rather than relaxing them. Both assertions previously scanned only include_str!("lib.rs") and would have counted zero after the module split. They now recursively walk every .rs file under src/ and assert a lower bound on files visited, so a broken walk fails loudly instead of passing vacuously. The ONNX assertion still requires exactly one Session::builder()? across the whole crate, covering both model roles through one construction path, one provider policy, and one error surface. The mel-bank assertion gets the same widening. CoreML and Apple-hardware behavior remain unverified. This host had no Apple hardware. The aarch64-apple-ios cross-target check passes, but nothing was executed on Apple silicon and no accelerator path was exercised. Use f64::round_ties_even() for frame_start because Python round() is banker's rounding while Rust f64::round() is half-away-from-zero. At stride 5, window index 1 gives start_sample 80000 and exactly 294.5: Python resolves that to 294, while f64::round() would produce 295. The wrong function would shift every downstream frame boundary in that window. The test pins the exact 294.5 case and asserts both the divergent Rust round() value and the correct ties-even value. core/fixtures/speaker_stage_boundaries.json gates the pure numeric stages and was never intended to gate the model pass. It carries no per-frame log-probability arrays, no window-start sequences, and no model-derived overlap fraction. speaker_evidence.*.overlap_fraction is a hand-chosen scalar input, 0.0 or 0.049. Every committed evidence case also has exactly one window, so the rule that only speech-bearing windows enter either denominator is unexercised by fixture; a port dropping that filter would still match all four committed decisions. The consequence is that the windowing, accumulation, frame-offset, averaged-argmax mechanics, and the speech-bearing denominator filter are covered by structural tests in CI and deferred to the real-corpus bundle differential for numeric confirmation. They are not ungated and not a fixture gap. The differential already records these as first-class bundle fields and compares them element-wise Python-vs-port: pyannote.avg_log_probs at tests/verify_speaker_differential.py:87, pyannote.window_stats at :88, evidence.mean_window_overlap_share at :92, evidence.overlap_fraction at :93, per_frame_argmax_agreement_fraction gated by LOGPROB_ARGMAX_AGREEMENT_MIN at :1011-1020, and window_stats_equal as an exact array comparison at :1064. Honesty table: Expectation Source ----------------------------------- ----------------------------- start/final/short pad hand-derived from fixture constants; implementation- independent; not committed accumulation order/count floor in-test hand-computation 294.5 -> 294 genuine independent Python oracle; not committed overlap from averaged argmax in-test hand-computation speech-bearing denominator filter in-test hand-computation per-window statistics triples committed fixture value at speaker_evidence.<case>. windows[0]; class sequence is test input decisions and both fractions committed fixture value at speaker_evidence.<case>. decision.* eleven constants committed fixture value at identity.source_constants.* Add a cross-language calibration-drift gate for eleven constants, each asserted equal to its identity.source_constants path so a Python-side recalibration turns the Rust tests red. Keep SPEAKER_EVIDENCE_MULTI_MIN, SPEAKER_EVIDENCE_SINGLE_MAX, and DIARIZE_MIN_OVERLAP as three separate constants even though all are 0.05 today; they are independent tuning controls. Test counts increase from 5 to 21 in solstone-core-speakers and from 5 to 7 in solstone-core-speakers-onnx. No core/fixtures/*, scripts/build_core_fixtures.py, or solstone/**/*.py files changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>


+1902 -599
7 changed files