personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_speaker_verdict.py
25 kB 710 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4"""Tests for the speaker verdict replay harness.""" 5 6from __future__ import annotations 7 8import json 9import socket 10from pathlib import Path 11from typing import Any 12 13import numpy as np 14import pytest 15 16from solstone.apps.speakers import encoder_config 17from solstone.observe.transcribe import diarize 18from tests import verify_speaker_differential as harness 19from tests import verify_speaker_verdict as verdict 20from tests._repo_inventory import assert_inventory_unchanged, repository_inventory 21from tests._speaker_differential_fixtures import EMBEDDING_MAX_ABS_TOLERANCE 22from tests.test_speaker_differential import _emit_model_free_bundle 23 24 25def _eps(*values: float) -> float: 26 positives = [abs(float(value)) for value in values if float(value) > 0] 27 return min(positives) / 10 28 29 30def _unit_vector(components: list[float]) -> np.ndarray: 31 remainder = 1.0 - sum(float(value) * float(value) for value in components) 32 assert remainder > 0 33 return np.array([*components, np.sqrt(remainder)], dtype=np.float32) 34 35 36def _axis(index: int, width: int) -> np.ndarray: 37 vector = np.zeros(width, dtype=np.float32) 38 vector[index] = 1.0 39 return vector 40 41 42def _statement_bundle( 43 rows: list[np.ndarray], ids: list[int] | None = None 44) -> harness.Bundle: 45 bundle = harness.new_bundle(producer="test") 46 statement_ids = np.array( 47 ids if ids is not None else list(range(1, len(rows) + 1)), 48 dtype=np.int32, 49 ) 50 harness._set_array( 51 bundle, 52 harness.STATEMENT_EMBEDDING_IDS, 53 statement_ids, 54 "statement_embeddings", 55 ) 56 harness._set_array( 57 bundle, 58 harness.STATEMENT_EMBEDDINGS, 59 np.asarray(rows, dtype=np.float32), 60 "statement_embeddings", 61 ) 62 return bundle 63 64 65def _prediction_bundle( 66 rows: list[tuple[int, float, float, int | np.integer]], 67) -> harness.Bundle: 68 bundle = harness.new_bundle(producer="test") 69 harness._set_array( 70 bundle, 71 harness.INPUT_DIARIZATION_IDS, 72 np.array([row[0] for row in rows], dtype=np.int32), 73 "inputs.diarization", 74 ) 75 harness._set_array( 76 bundle, 77 harness.INPUT_DIARIZATION_SPANS, 78 np.array([[row[1], row[2]] for row in rows], dtype=np.float32), 79 "inputs.diarization", 80 ) 81 harness._set_array( 82 bundle, 83 harness.DIARIZATION_STATEMENT_LABELS, 84 np.array([row[3] for row in rows], dtype=np.int32), 85 "statement_labels", 86 ) 87 return bundle 88 89 90def _cluster_bundle( 91 raw_embeddings: np.ndarray, silhouette_k: int, effective_k: int 92) -> harness.Bundle: 93 bundle = harness.new_bundle(producer="test") 94 harness._set_array( 95 bundle, 96 harness.DIARIZATION_INTERVAL_EMBEDDINGS, 97 raw_embeddings.astype(np.float32), 98 "interval_embeddings", 99 ) 100 harness._set_scalar( 101 bundle, 102 harness.DIARIZATION_SILHOUETTE_K, 103 silhouette_k, 104 "clustering", 105 ) 106 harness._set_scalar( 107 bundle, 108 harness.DIARIZATION_EFFECTIVE_K, 109 effective_k, 110 "clustering", 111 ) 112 return bundle 113 114 115def _refs( 116 *, 117 width: int, 118 owner_index: int, 119 entity_indices: list[int] | None = None, 120 owner_margin: float | None = encoder_config.OWNER_MARGIN_MIN, 121) -> verdict.ReferenceCentroids: 122 entity_indices = entity_indices or [] 123 return verdict.ReferenceCentroids( 124 owner_centroid=_axis(owner_index, width), 125 owner_threshold=encoder_config.OWNER_THRESHOLD, 126 owner_threshold_source="constant_default", 127 owner_margin=owner_margin, 128 owner_margin_source="constant_default", 129 entity_ids=tuple(f"entity_{index}" for index in entity_indices), 130 entity_centroids=tuple(_axis(index, width) for index in entity_indices), 131 entity_usable=tuple(True for _index in entity_indices), 132 ) 133 134 135def _reference_turns(*turns: tuple[float, float, str]) -> verdict.ReferenceTurns: 136 return verdict.ReferenceTurns( 137 "present", 138 tuple( 139 verdict.ReferenceTurn(start, end, speaker) for start, end, speaker in turns 140 ), 141 ) 142 143 144def _write_centroids( 145 path: Path, 146 *, 147 owner: np.ndarray | None, 148 entities: list[tuple[str, np.ndarray, bool]], 149 owner_meta: dict[str, Any] | None = None, 150) -> None: 151 manifest_owner = { 152 "state": harness.PRESENT if owner is not None else harness.ABSENT_NONE 153 } 154 if owner is not None: 155 manifest_owner["array"] = "owner.centroid" 156 if owner_meta: 157 manifest_owner.update(owner_meta) 158 manifest = { 159 "schema": verdict.REFERENCE_CENTROIDS_SCHEMA, 160 "schema_version": verdict.SCHEMA_VERSION, 161 "owner": manifest_owner, 162 "entities": [ 163 {"id": entity_id, "usable": usable} 164 for entity_id, _centroid, usable in entities 165 ], 166 } 167 arrays: dict[str, np.ndarray] = { 168 verdict.REFERENCE_CENTROIDS_MANIFEST_KEY: np.array( 169 json.dumps(manifest, sort_keys=True) 170 ), 171 } 172 if owner is not None: 173 arrays["owner.centroid"] = np.asarray(owner, dtype=np.float32) 174 if entities: 175 arrays["entities.centroids"] = np.asarray( 176 [centroid for _entity_id, centroid, _usable in entities], 177 dtype=np.float32, 178 ) 179 with path.open("wb") as fh: 180 np.savez(fh, **arrays) 181 182 183def test_family1_model_free_replay_matches_recorded_scalars( 184 monkeypatch: pytest.MonkeyPatch, 185) -> None: 186 _case, bundle, _reset = _emit_model_free_bundle(monkeypatch) 187 188 side = verdict._replay_cluster_side(bundle) 189 190 assert side["evaluated"] is True 191 assert side["recorded_mismatches"] == [] 192 assert side["replayed_silhouette_k"] == harness._scalar( 193 bundle, harness.DIARIZATION_SILHOUETTE_K 194 ) 195 assert side["replayed_effective_k"] == harness._scalar( 196 bundle, harness.DIARIZATION_EFFECTIVE_K 197 ) 198 199 200def test_silhouette_improvement_flip_and_no_flip( 201 monkeypatch: pytest.MonkeyPatch, 202) -> None: 203 eps = _eps(diarize.SILHOUETTE_IMPROVEMENT) 204 205 def fake_ahc(embs_n: np.ndarray, k: int) -> np.ndarray: 206 return np.arange(len(embs_n), dtype=np.int32) % int(k) 207 208 def fake_silhouette(embs_n: np.ndarray, labels: np.ndarray) -> float: 209 k = len(np.unique(labels)) 210 if k == 2: 211 return 0.0 212 if k == 3 and embs_n[0, 0] > embs_n[0, 1]: 213 return diarize.SILHOUETTE_IMPROVEMENT + eps 214 if k == 3: 215 return diarize.SILHOUETTE_IMPROVEMENT - eps 216 raise AssertionError(f"unexpected silhouette k {k}") 217 218 monkeypatch.setattr(diarize, "_ahc", fake_ahc) 219 monkeypatch.setattr(diarize, "_silhouette", fake_silhouette) 220 left = _cluster_bundle( 221 np.array([[0.0, 1.0], [0.0, 0.9], [0.1, 0.9], [0.2, 0.8]]), 222 2, 223 2, 224 ) 225 right = _cluster_bundle( 226 np.array([[1.0, 0.0], [0.9, 0.0], [0.9, 0.1], [0.8, 0.2]]), 227 3, 228 3, 229 ) 230 231 flip_report = verdict._family1_report(left, right) 232 no_flip_report = verdict._family1_report(left, harness.copy_bundle(left)) 233 234 assert flip_report["flip_count"] == 1 235 assert flip_report["classification"] == harness.UNEXPECTED_DIFFERS 236 assert no_flip_report["flip_count"] == 0 237 assert no_flip_report["classification"] == harness.EQUAL 238 239 240def test_max_k_cap_binds_and_does_not_bind(monkeypatch: pytest.MonkeyPatch) -> None: 241 def fake_ahc(embs_n: np.ndarray, k: int) -> np.ndarray: 242 return np.arange(len(embs_n), dtype=np.int32) % int(k) 243 244 def fake_silhouette(_embs_n: np.ndarray, labels: np.ndarray) -> float: 245 return float(len(np.unique(labels))) 246 247 monkeypatch.setattr(diarize, "_ahc", fake_ahc) 248 monkeypatch.setattr(diarize, "_silhouette", fake_silhouette) 249 capped = _cluster_bundle( 250 np.eye(diarize.MAX_K + 1, dtype=np.float32), 251 diarize.MAX_K, 252 diarize.MAX_K, 253 ) 254 uncapped_rows = max(3, diarize.MAX_K - 1) 255 if uncapped_rows - 1 >= diarize.MAX_K: 256 pytest.skip("MAX_K leaves no uncapped multi-cluster case") 257 uncapped = _cluster_bundle( 258 np.eye(uncapped_rows, dtype=np.float32), 259 1, 260 1, 261 ) 262 263 capped_side = verdict._replay_cluster_side(capped) 264 uncapped_side = verdict._replay_cluster_side(uncapped) 265 266 assert capped_side["replayed_effective_k"] == diarize.MAX_K 267 assert uncapped_side["replayed_effective_k"] < diarize.MAX_K 268 269 270def test_owner_threshold_flip_and_no_flip() -> None: 271 eps = _eps(encoder_config.OWNER_THRESHOLD) 272 refs = _refs(width=2, owner_index=0) 273 left = _statement_bundle([_unit_vector([encoder_config.OWNER_THRESHOLD - eps])]) 274 right = _statement_bundle([_unit_vector([encoder_config.OWNER_THRESHOLD + eps])]) 275 no_flip_left = _statement_bundle( 276 [_unit_vector([encoder_config.OWNER_THRESHOLD + eps])] 277 ) 278 no_flip_right = _statement_bundle( 279 [_unit_vector([encoder_config.OWNER_THRESHOLD + eps * 2])] 280 ) 281 282 flip_report, *_ = verdict._family2_report(left, right, refs) 283 no_flip_report, *_ = verdict._family2_report(no_flip_left, no_flip_right, refs) 284 285 assert flip_report["flip_count"] == 1 286 assert flip_report["classification"] == harness.UNEXPECTED_DIFFERS 287 assert no_flip_report["flip_count"] == 0 288 assert no_flip_report["classification"] == harness.EQUAL 289 290 291def test_owner_margin_flip_and_no_flip() -> None: 292 eps = _eps(encoder_config.OWNER_MARGIN_MIN) 293 owner_score = encoder_config.OWNER_THRESHOLD + encoder_config.OWNER_MARGIN_MIN + eps 294 entity_fail = owner_score - encoder_config.OWNER_MARGIN_MIN + eps 295 entity_pass = owner_score - encoder_config.OWNER_MARGIN_MIN - eps 296 refs = _refs(width=3, owner_index=0, entity_indices=[1]) 297 left = _statement_bundle([_unit_vector([owner_score, entity_fail])]) 298 right = _statement_bundle([_unit_vector([owner_score, entity_pass])]) 299 no_flip_right = _statement_bundle([_unit_vector([owner_score + eps, entity_pass])]) 300 301 flip_report, *_ = verdict._family2_report(left, right, refs) 302 no_flip_report, *_ = verdict._family2_report(right, no_flip_right, refs) 303 304 assert flip_report["flip_count"] == 1 305 assert flip_report["flips"][0]["underlying_values"]["left_owner_margin_declined"] 306 assert flip_report["classification"] == harness.UNEXPECTED_DIFFERS 307 assert no_flip_report["flip_count"] == 0 308 assert no_flip_report["classification"] == harness.EQUAL 309 310 311def test_asymmetric_owner_evaluability_is_not_a_flip() -> None: 312 refs = _refs(width=2, owner_index=0) 313 left = _statement_bundle([np.zeros(2, dtype=np.float32)]) 314 right = _statement_bundle([_unit_vector([encoder_config.OWNER_THRESHOLD])]) 315 316 report, *_ = verdict._family2_report(left, right, refs) 317 318 assert report["classification"] == harness.UNEXPECTED_DIFFERS 319 assert report["reason"] == "asymmetric_evaluability" 320 assert report["flip_count"] == 0 321 assert report["asymmetric_evaluability"] == [1] 322 323 324def test_functionally_equal_rollup_keeps_zero_flip_answer_visible( 325 monkeypatch: pytest.MonkeyPatch, 326) -> None: 327 _case, left, _reset = _emit_model_free_bundle(monkeypatch) 328 right = harness.copy_bundle(left) 329 embeddings = harness._array(right, harness.STATEMENT_EMBEDDINGS).copy() 330 embeddings[0, 0] += EMBEDDING_MAX_ABS_TOLERANCE / 2 331 harness.replace_array(right, harness.STATEMENT_EMBEDDINGS, embeddings) 332 333 report = verdict.compare_verdicts(left, right) 334 335 assert report["classification"] == harness.FUNCTIONALLY_EQUAL 336 assert ( 337 report["components"]["bundle_comparator"]["components"]["statement_embeddings"][ 338 "classification" 339 ] 340 == harness.FUNCTIONALLY_EQUAL 341 ) 342 assert report["components"]["decision_flips"]["flip_count"] == 0 343 344 345def test_acoustic_medium_threshold_flip_and_no_flip() -> None: 346 eps = _eps(encoder_config.ACOUSTIC_MEDIUM) 347 refs = _refs(width=3, owner_index=1, entity_indices=[0]) 348 left = _statement_bundle( 349 [_unit_vector([encoder_config.ACOUSTIC_MEDIUM - eps, 0.0])] 350 ) 351 right = _statement_bundle( 352 [_unit_vector([encoder_config.ACOUSTIC_MEDIUM + eps, 0.0])] 353 ) 354 no_flip_right = _statement_bundle( 355 [_unit_vector([encoder_config.ACOUSTIC_MEDIUM + eps * 2, 0.0])] 356 ) 357 358 left_owner = verdict._family2_report(left, right, refs) 359 flip_report = verdict._family3_report( 360 left, right, refs, left_owner[0], *left_owner[1:] 361 ) 362 right_owner = verdict._family2_report(right, no_flip_right, refs) 363 no_flip_report = verdict._family3_report( 364 right, no_flip_right, refs, right_owner[0], *right_owner[1:] 365 ) 366 367 assert flip_report["flip_count"] == 1 368 assert flip_report["classification"] == harness.UNEXPECTED_DIFFERS 369 assert no_flip_report["flip_count"] == 0 370 assert no_flip_report["classification"] == harness.EQUAL 371 372 373def test_acoustic_high_threshold_flip_and_no_flip() -> None: 374 eps = _eps(encoder_config.ACOUSTIC_HIGH) 375 refs = _refs(width=3, owner_index=1, entity_indices=[0]) 376 left = _statement_bundle([_unit_vector([encoder_config.ACOUSTIC_HIGH - eps, 0.0])]) 377 right = _statement_bundle([_unit_vector([encoder_config.ACOUSTIC_HIGH + eps, 0.0])]) 378 no_flip_right = _statement_bundle( 379 [_unit_vector([encoder_config.ACOUSTIC_HIGH + eps * 2, 0.0])] 380 ) 381 382 left_owner = verdict._family2_report(left, right, refs) 383 flip_report = verdict._family3_report( 384 left, right, refs, left_owner[0], *left_owner[1:] 385 ) 386 right_owner = verdict._family2_report(right, no_flip_right, refs) 387 no_flip_report = verdict._family3_report( 388 right, no_flip_right, refs, right_owner[0], *right_owner[1:] 389 ) 390 391 assert flip_report["flip_count"] == 1 392 assert flip_report["flips"][0]["left_outcome"]["tier"] == "medium" 393 assert flip_report["flips"][0]["right_outcome"]["tier"] == "high" 394 assert no_flip_report["flip_count"] == 0 395 assert no_flip_report["classification"] == harness.EQUAL 396 397 398def test_acoustic_margin_flip_and_no_flip() -> None: 399 eps = _eps(encoder_config.ACOUSTIC_MARGIN_MIN) 400 best = encoder_config.ACOUSTIC_HIGH + eps 401 runner_fail = best - encoder_config.ACOUSTIC_MARGIN_MIN + eps 402 runner_pass = best - encoder_config.ACOUSTIC_MARGIN_MIN - eps 403 refs = _refs(width=4, owner_index=2, entity_indices=[0, 1]) 404 left = _statement_bundle([_unit_vector([best, runner_fail, 0.0])]) 405 right = _statement_bundle([_unit_vector([best, runner_pass, 0.0])]) 406 no_flip_right = _statement_bundle([_unit_vector([best + eps, runner_pass, 0.0])]) 407 408 left_owner = verdict._family2_report(left, right, refs) 409 flip_report = verdict._family3_report( 410 left, right, refs, left_owner[0], *left_owner[1:] 411 ) 412 right_owner = verdict._family2_report(right, no_flip_right, refs) 413 no_flip_report = verdict._family3_report( 414 right, no_flip_right, refs, right_owner[0], *right_owner[1:] 415 ) 416 417 assert flip_report["flip_count"] == 1 418 assert flip_report["flips"][0]["left_outcome"]["demotion_causes"] == [ 419 "acoustic_margin_declined" 420 ] 421 assert no_flip_report["flip_count"] == 0 422 assert no_flip_report["classification"] == harness.EQUAL 423 424 425def test_owner_margin_decline_demotes_acoustic_high() -> None: 426 eps = _eps(encoder_config.OWNER_THRESHOLD, encoder_config.OWNER_MARGIN_MIN) 427 owner_declined_score = encoder_config.OWNER_THRESHOLD + eps 428 best = max( 429 encoder_config.ACOUSTIC_HIGH + eps, 430 owner_declined_score - encoder_config.OWNER_MARGIN_MIN + eps, 431 ) 432 owner_clear_score = encoder_config.OWNER_THRESHOLD - eps 433 refs = _refs(width=3, owner_index=1, entity_indices=[0]) 434 left = _statement_bundle([_unit_vector([best, owner_declined_score])]) 435 right = _statement_bundle([_unit_vector([best, owner_clear_score])]) 436 437 owner_report = verdict._family2_report(left, right, refs) 438 acoustic_report = verdict._family3_report( 439 left, right, refs, owner_report[0], *owner_report[1:] 440 ) 441 442 assert owner_report[0]["left_margin_declined_sids"] == [1] 443 assert acoustic_report["flip_count"] == 1 444 assert acoustic_report["flips"][0]["left_outcome"]["demotion_causes"] == [ 445 "owner_margin_declined" 446 ] 447 assert acoustic_report["flips"][0]["right_outcome"]["tier"] == "high" 448 449 450@pytest.mark.parametrize( 451 ("bundle", "reference_turns", "expected_der", "expected_breakdown"), 452 [ 453 ( 454 _prediction_bundle([(2, 5.0, 10.0, 2), (1, 0.0, 5.0, 1)]), 455 _reference_turns((0.0, 5.0, "a"), (5.0, 10.0, "b")), 456 0.0, 457 {"missed": 0.0, "false_alarm": 0.0, "confusion": 0.0, "denominator": 10.0}, 458 ), 459 ( 460 _prediction_bundle([(1, 10.0, 20.0, 1)]), 461 _reference_turns((0.0, 10.0, "a")), 462 2.0, 463 { 464 "missed": 10.0, 465 "false_alarm": 10.0, 466 "confusion": 0.0, 467 "denominator": 10.0, 468 }, 469 ), 470 ( 471 _prediction_bundle([(1, 0.0, 4.0, 1)]), 472 _reference_turns((0.0, 10.0, "a")), 473 0.6, 474 {"missed": 6.0, "false_alarm": 0.0, "confusion": 0.0, "denominator": 10.0}, 475 ), 476 ( 477 _prediction_bundle([(1, 4.0, 10.0, 1), (2, 0.0, 4.0, 2)]), 478 _reference_turns((0.0, 4.0, "a"), (4.0, 10.0, "b")), 479 0.0, 480 {"missed": 0.0, "false_alarm": 0.0, "confusion": 0.0, "denominator": 10.0}, 481 ), 482 ( 483 _prediction_bundle([(1, 0.0, 15.0, 1)]), 484 _reference_turns((0.0, 10.0, "a"), (5.0, 15.0, "b")), 485 0.5, 486 {"missed": 5.0, "false_alarm": 0.0, "confusion": 5.0, "denominator": 20.0}, 487 ), 488 ], 489) 490def test_der_hand_computed_cases( 491 bundle: harness.Bundle, 492 reference_turns: verdict.ReferenceTurns, 493 expected_der: float, 494 expected_breakdown: dict[str, float], 495) -> None: 496 score = verdict.score_der(bundle, reference_turns) 497 498 assert score["status"] == harness.PRESENT 499 assert score["der"] == pytest.approx(expected_der) 500 for key, value in expected_breakdown.items(): 501 assert score["breakdown"][key] == pytest.approx(value) 502 503 504def test_der_ignores_nan_and_null_predicted_spans() -> None: 505 bundle = _prediction_bundle( 506 [ 507 (1, 0.0, 10.0, harness.LABEL_NULL_SENTINEL), 508 (2, float("nan"), float("nan"), 2), 509 ] 510 ) 511 512 score = verdict.score_der(bundle, _reference_turns((0.0, 10.0, "a"))) 513 514 assert score["der"] == pytest.approx(1.0) 515 assert score["breakdown"]["missed"] == pytest.approx(10.0) 516 517 518def test_der_counts_distinct_speakers_not_overlapping_turns() -> None: 519 bundle = _prediction_bundle([(1, 0.0, 15.0, 1)]) 520 521 score = verdict.score_der( 522 bundle, 523 _reference_turns((0.0, 10.0, "a"), (5.0, 15.0, "a")), 524 ) 525 526 assert score["der"] == pytest.approx(0.0) 527 assert score["breakdown"]["denominator"] == pytest.approx(15.0) 528 529 530def test_der_reference_absent_empty_and_zero_denominator(tmp_path: Path) -> None: 531 bundle = _prediction_bundle([(1, 0.0, 1.0, 1)]) 532 empty_path = tmp_path / "empty-reference-turns.json" 533 empty_path.write_text("[]", encoding="utf-8") 534 535 absent = verdict._der_report(bundle, bundle, verdict.load_reference_turns(None)) 536 empty = verdict._der_report( 537 bundle, bundle, verdict.load_reference_turns(empty_path) 538 ) 539 zero = verdict.score_der(bundle, verdict.ReferenceTurns("present", ())) 540 541 assert absent["reason"] == "reference_turns_absent" 542 assert empty["reason"] == "reference_turns_empty" 543 assert zero["reason"] == "zero_reference_speech" 544 545 546def test_der_predicted_labels_field_state_gates_component() -> None: 547 bundle = harness.new_bundle(producer="test") 548 harness._set_array( 549 bundle, 550 harness.INPUT_DIARIZATION_IDS, 551 np.array([1], dtype=np.int32), 552 "inputs.diarization", 553 ) 554 harness._set_array( 555 bundle, 556 harness.INPUT_DIARIZATION_SPANS, 557 np.array([[0.0, 1.0]], dtype=np.float32), 558 "inputs.diarization", 559 ) 560 harness._set_state( 561 bundle, 562 harness.DIARIZATION_STATEMENT_LABELS, 563 harness.NOT_EVALUATED, 564 "statement_labels", 565 ) 566 567 score = verdict.score_der(bundle, _reference_turns((0.0, 1.0, "a"))) 568 569 assert score["status"] == harness.NOT_EVALUATED 570 assert score["reason"] == "predicted_labels_not_present" 571 572 573def test_reference_turn_reader_rejects_unknown_text_key(tmp_path: Path) -> None: 574 path = tmp_path / "reference-turns.json" 575 path.write_text( 576 json.dumps( 577 [{"start_s": 0.0, "end_s": 1.0, "speaker": "a", "text": "redacted"}] 578 ), 579 encoding="utf-8", 580 ) 581 582 with pytest.raises(harness.HarnessError, match="text"): 583 verdict.load_reference_turns(path) 584 585 586@pytest.mark.parametrize( 587 "payload", 588 [ 589 {"start_s": 0.0, "end_s": 1.0, "speaker": "a"}, 590 [{"start_s": -1.0, "end_s": 1.0, "speaker": "a"}], 591 [{"start_s": 1.0, "end_s": 1.0, "speaker": "a"}], 592 [{"start_s": "0", "end_s": 1.0, "speaker": "a"}], 593 [{"start_s": 0.0, "end_s": 1.0, "speaker": ""}], 594 [ 595 {"start_s": 2.0, "end_s": 3.0, "speaker": "a"}, 596 {"start_s": 0.0, "end_s": 1.0, "speaker": "b"}, 597 ], 598 ], 599) 600def test_reference_turn_reader_rejects_invalid_shapes( 601 tmp_path: Path, 602 payload: object, 603) -> None: 604 path = tmp_path / "reference-turns.json" 605 path.write_text(json.dumps(payload), encoding="utf-8") 606 607 with pytest.raises(harness.HarnessError): 608 verdict.load_reference_turns(path) 609 610 611def test_reference_centroid_file_manifest_is_single_source_of_truth( 612 tmp_path: Path, 613) -> None: 614 eps = _eps(encoder_config.OWNER_THRESHOLD) 615 path = tmp_path / "centroids.npz" 616 _write_centroids( 617 path, 618 owner=_axis(0, 2), 619 entities=[("entity_1", _axis(1, 2), True)], 620 owner_meta={"threshold": encoder_config.OWNER_THRESHOLD + eps, "margin": None}, 621 ) 622 623 refs = verdict.load_reference_centroids(path) 624 with np.load(path, allow_pickle=False) as payload: 625 assert set(payload.files) == { 626 verdict.REFERENCE_CENTROIDS_MANIFEST_KEY, 627 "owner.centroid", 628 "entities.centroids", 629 } 630 threshold_block = verdict._owner_thresholds(refs) 631 632 assert refs is not None 633 assert refs.owner_threshold_source == "supplied" 634 assert refs.owner_margin is None 635 assert refs.owner_margin_source == "supplied" 636 assert threshold_block["owner_margin"]["effective_value"] is None 637 638 639def test_comparator_failure_short_circuits_verdict() -> None: 640 left = harness.new_bundle(producer="left") 641 right = harness.new_bundle(producer="right") 642 643 report = verdict.compare_verdicts(left, right) 644 645 assert report["classification"] == harness.NOT_EVALUATED 646 assert report["failure"] == report["components"]["bundle_comparator"]["failure"] 647 assert set(report["components"]) == {"bundle_comparator"} 648 649 650def test_cli_does_not_use_network_or_write_inside_repository( 651 tmp_path: Path, 652 monkeypatch: pytest.MonkeyPatch, 653 capsys: pytest.CaptureFixture[str], 654) -> None: 655 connect_calls: list[object] = [] 656 657 def fail_connect(_self: socket.socket, address: object) -> None: 658 connect_calls.append(address) 659 raise AssertionError("network call attempted") 660 661 monkeypatch.setattr(socket.socket, "connect", fail_connect) 662 _case, bundle, _reset = _emit_model_free_bundle(monkeypatch) 663 output_dir = tmp_path / "speaker-verdict" 664 output_dir.mkdir() 665 left_path = output_dir / "left.npz" 666 right_path = output_dir / "right.npz" 667 centroids_path = output_dir / "centroids.npz" 668 reference_path = output_dir / "reference-turns.json" 669 report_path = output_dir / "report.json" 670 harness.write_bundle(bundle, left_path) 671 harness.write_bundle(harness.copy_bundle(bundle), right_path) 672 owner = np.zeros(harness._array(bundle, harness.STATEMENT_EMBEDDINGS).shape[1]) 673 owner[0] = 1.0 674 _write_centroids(centroids_path, owner=owner, entities=[]) 675 spans = harness._array(bundle, harness.INPUT_DIARIZATION_SPANS) 676 labels = harness._array(bundle, harness.DIARIZATION_STATEMENT_LABELS) 677 reference_turns = [ 678 {"start_s": float(start), "end_s": float(end), "speaker": str(int(label))} 679 for (start, end), label in zip(spans, labels) 680 if int(label) != int(harness.LABEL_NULL_SENTINEL) 681 and np.isfinite(start) 682 and np.isfinite(end) 683 and end > start 684 ] 685 reference_path.write_text(json.dumps(reference_turns), encoding="utf-8") 686 687 before_inventory = repository_inventory(harness.ROOT) 688 code = verdict.main( 689 [ 690 str(left_path), 691 str(right_path), 692 "--reference-centroids", 693 str(centroids_path), 694 "--reference-turns", 695 str(reference_path), 696 "--report", 697 str(report_path), 698 ] 699 ) 700 after_inventory = repository_inventory(harness.ROOT) 701 702 captured = capsys.readouterr() 703 assert code == 0 704 assert json.loads(report_path.read_text(encoding="utf-8"))["classification"] in { 705 harness.EQUAL, 706 harness.FUNCTIONALLY_EQUAL, 707 } 708 assert json.loads(captured.out)["schema"] == verdict.REPORT_SCHEMA 709 assert connect_calls == [] 710 assert_inventory_unchanged(before_inventory, after_inventory)