personal memory agent
0

Configure Feed

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

solstone / scripts / build_core_fixtures.py
50 kB 1404 lines
1#!/usr/bin/env python3 2# SPDX-License-Identifier: AGPL-3.0-only 3# Copyright (c) 2026 sol pbc 4 5"""Build generated fixtures consumed by the Rust core workspace.""" 6 7from __future__ import annotations 8 9import argparse 10import base64 11import copy 12import hashlib 13import importlib.metadata 14import json 15import logging 16import math 17import platform 18import sqlite3 19import sys 20from collections.abc import Callable 21from dataclasses import dataclass 22from pathlib import Path 23from typing import Any 24 25import numpy as np 26 27from solstone.apps.speakers.encoder_config import ( 28 ACOUSTIC_HIGH, 29 ACOUSTIC_MARGIN_MIN, 30 ACOUSTIC_MEDIUM, 31 CC_CONFIDENCE_GATE, 32 CC_COVERAGE_GATE, 33 CONFIRM_MIN_DURATION_S, 34 CONFIRM_MIN_INTERVALS, 35 CONFIRM_MIN_SEGMENTS, 36 CONSOLIDATE_MERGE_THRESHOLD, 37 CONSOLIDATE_MIN_INTERVALS, 38 CONSOLIDATE_SUGGEST_MIN, 39 DIARIZE_MIN_OVERLAP, 40 ENCODER_ID, 41 MERGE_THRESHOLD, 42 NOISY_FLYWHEEL_OVERLAP_MAX, 43 OVERLAP_DETECTOR_ID, 44 OVERLAP_DETECTOR_SHA256, 45 OWNER_BOOTSTRAP_EVIDENCE_TIER_STANDARD, 46 OWNER_BOOTSTRAP_EVIDENCE_TIER_STRONG, 47 OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25, 48 OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25_STRONG, 49 OWNER_BOOTSTRAP_MIN_MEDIAN_DURATION_S, 50 OWNER_BOOTSTRAP_MIN_STMTS, 51 OWNER_BOOTSTRAP_PROVISIONAL_GUARD_MIN_TAGS, 52 OWNER_BOOTSTRAP_STRONG_EVIDENCE_MIN_STMTS, 53 OWNER_MARGIN_MIN, 54 OWNER_REBUILD_MAX_COHESION_DROP, 55 OWNER_REBUILD_MIN_CENTROID_AGREEMENT, 56 OWNER_REBUILD_MIN_CLUSTER_SIZE_RATIO, 57 OWNER_REBUILD_SUPERSEDED_SCAN_DAYS, 58 OWNER_THRESHOLD, 59 SLOT_ACTIVE_MIN_SHARE, 60 SOLO_CLUSTER_MIN_COSINE, 61 SPEAKER_EVIDENCE_MULTI_MIN, 62 SPEAKER_EVIDENCE_SINGLE_MAX, 63 SPEAKER_EVIDENCE_VERSION, 64 SPLIT_THRESHOLD, 65 STABILITY_THRESHOLD, 66 VP_DECAY_LAMBDA, 67 VP_OUTLIER_MIN_SAMPLES, 68 VP_OUTLIER_MIN_SIMILARITY, 69) 70from solstone.convey.contract.assemble import CALLOSUM_REGISTRY 71from solstone.observe.transcribe.diarize import ( 72 AHC_LINKAGE, 73 AHC_METRIC, 74 FRAMES_PER_WINDOW, 75 MAX_K, 76 MIN_FRAME_CONFIDENCE, 77 MIN_INTERVAL_S, 78 SILHOUETTE_IMPROVEMENT, 79 SINGLE_SPEAKER_CLASSES, 80 _ahc, 81 _assign_sentences, 82 _find_intervals, 83 _normalize_rows, 84 _pick_k_silhouette, 85 _silhouette, 86 _wespeaker_features, 87) 88from solstone.observe.transcribe.diarize import ( 89 SAMPLE_RATE as DIARIZE_SAMPLE_RATE, 90) 91from solstone.observe.transcribe.diarize import ( 92 STRIDE_S as DIARIZE_STRIDE_S, 93) 94from solstone.observe.transcribe.diarize import ( 95 WINDOW_S as DIARIZE_WINDOW_S, 96) 97from solstone.observe.transcribe.main import _compute_wespeaker_features 98from solstone.observe.transcribe.overlap import ( 99 _DIARIZE_STRIDE_S as OVERLAP_DIARIZE_STRIDE_S, 100) 101from solstone.observe.transcribe.overlap import ( 102 FRAMES_PER_WINDOW as OVERLAP_FRAMES_PER_WINDOW, 103) 104from solstone.observe.transcribe.overlap import ( 105 OVERLAP_CLASSES, 106 _speaker_window_stats, 107 decide_speaker_evidence, 108) 109from solstone.observe.transcribe.overlap import ( 110 STRIDE_S as OVERLAP_STRIDE_S, 111) 112from solstone.observe.transcribe.overlap import ( 113 WINDOW_S as OVERLAP_WINDOW_S, 114) 115from solstone.think import markdown as markdown_formatter 116from solstone.think.cogitate_contract import ( 117 COGITATE_ACCESS_TIERS, 118 COGITATE_READ_TOOL_NAMES, 119 COGITATE_RUNTIME_PREAMBLE, 120 FUTURE_ACCESS_TIERS, 121 TALENT_FINALIZATION_MODES, 122 capabilities_for_access_tier, 123) 124from solstone.think.indexer.edges import EDGES_SCHEMA_VERSION, _ensure_edges_schema 125 126ROOT = Path(__file__).resolve().parent.parent 127FIXTURE_DIR = ROOT / "core" / "fixtures" 128CALLOSUM_ARTIFACT_PATH = FIXTURE_DIR / "callosum_registry.json" 129COGITATE_ARTIFACT_PATH = FIXTURE_DIR / "cogitate_contract.json" 130EDGE_SCHEMA_ARTIFACT_PATH = FIXTURE_DIR / "edge_schema.json" 131MARKDOWN_CHUNKS_ARTIFACT_PATH = FIXTURE_DIR / "markdown_chunks.json" 132SPEAKER_FILTERBANK_ARTIFACT_PATH = FIXTURE_DIR / "speaker_filterbank.json" 133SPEAKER_STAGE_BOUNDARIES_ARTIFACT_PATH = FIXTURE_DIR / "speaker_stage_boundaries.json" 134OVERSIZED_SIZE_NORMALIZATION = "oversized_size" 135OVERSIZED_SIZE_TOKEN = "normalizedsize" 136# Filterbank rows are a scale/regime oracle for the production fbank stage. The 137# tolerance leaves headroom under SILHOUETTE_IMPROVEMENT and the 138# speaker-evidence thresholds while allowing native-library/architecture float noise. 139FILTERBANK_VALUE_ABS_TOLERANCE = 1e-2 140# Silhouette scores are compared with enough room for plausible float32 141# matmul/BLAS reduction noise across architectures, but far below the 142# SILHOUETTE_IMPROVEMENT decision margin. Selected k values and cluster 143# labels are compared exactly, so score tolerance cannot hide a branch flip. If 144# the arm64 leg proves 1e-6 too tight, that drift is a finding to report. 145CLUSTER_SCORE_ABS_TOLERANCE = 1e-6 146# generation_platform is diagnostic provenance. It is ignored so an arm64 check 147# passes iff kaldi/source identity and floats agree. 148SPEAKER_FIXTURE_IGNORED_JSON_POINTERS = ("/identity/generation_platform",) 149FLOAT_ROW_DECIMALS = 3 150SPEAKER_FILTERBANK_WAVEFORM_SEED = 20_260_724 151SPEAKER_FILTERBANK_DURATION_S = 2.0 152SPEAKER_FILTERBANK_NEAR_S = 0.60 153SPEAKER_FILTERBANK_FADE_S = 0.20 154SPEAKER_FILTERBANK_NEAR_AMPLITUDE = 1e-5 155SPEAKER_FILTERBANK_BROADBAND_AMPLITUDE = 3e-2 156SPEAKER_FILTERBANK_NEAR_ROWS = (5, 55) 157SPEAKER_FILTERBANK_BROADBAND_ROWS = (100, 190) 158K_DIVERGENCE_N = 32 159K_DIVERGENCE_SEED = 0 160# Found in prep by bounded seeded search over default_rng(seed).standard_normal. 161CLUSTER_PERTURB_N = 12 162CLUSTER_PERTURB_SEED = 15 163CLUSTER_PERTURB_ROW = 6 164CLUSTER_PERTURB_COL = 40 165CLUSTER_PERTURB_EPSILON = 0.03 166 167 168@dataclass(frozen=True) 169class ArtifactDescriptor: 170 build: Callable[[], Any] 171 comparison: str = "exact" 172 173 def render(self) -> str: 174 value = self.build() 175 if isinstance(value, str): 176 return value 177 return render_json(value) 178 179 180def _package_version(name: str) -> str: 181 try: 182 return importlib.metadata.version(name) 183 except importlib.metadata.PackageNotFoundError: 184 return "not-installed" 185 186 187def build_callosum_registry_fixture() -> dict[str, Any]: 188 return { 189 "fixture": "solstone-callosum-registry", 190 "fixture_version": 1, 191 "generated_by": "make core-fixtures", 192 "registry": { 193 tract: list(CALLOSUM_REGISTRY[tract]) for tract in sorted(CALLOSUM_REGISTRY) 194 }, 195 } 196 197 198def build_cogitate_contract_fixture() -> dict[str, Any]: 199 preamble_bytes = COGITATE_RUNTIME_PREAMBLE.encode("utf-8") 200 return { 201 "fixture": "solstone-cogitate-contract", 202 "fixture_version": 1, 203 "generated_by": "make core-fixtures", 204 "access_tiers": list(COGITATE_ACCESS_TIERS), 205 "capabilities": { 206 tier: { 207 "sol": capabilities_for_access_tier(tier).sol, 208 "reads": capabilities_for_access_tier(tier).reads, 209 "submit": capabilities_for_access_tier(tier).submit, 210 } 211 for tier in COGITATE_ACCESS_TIERS 212 }, 213 "future_access_tiers": list(FUTURE_ACCESS_TIERS), 214 "read_tools": list(COGITATE_READ_TOOL_NAMES), 215 "finalization_modes": list(TALENT_FINALIZATION_MODES), 216 "runtime_preamble": { 217 "digest": hashlib.sha256(preamble_bytes).hexdigest(), 218 "algorithm": "sha256", 219 "encoding": "utf-8", 220 "byte_length": len(preamble_bytes), 221 }, 222 } 223 224 225def build_edge_schema_fixture() -> dict[str, Any]: 226 conn = sqlite3.connect(":memory:") 227 228 def table_schema(table: str) -> dict[str, Any]: 229 columns = [ 230 {"name": row[1], "type": row[2], "notnull": row[3], "pk": row[5]} 231 for row in conn.execute(f"PRAGMA table_info({table})") 232 ] 233 indexes = [] 234 for row in conn.execute(f"PRAGMA index_list({table})"): 235 index_name = row[1] 236 indexes.append( 237 { 238 "name": index_name, 239 "unique": row[2], 240 "origin": row[3], 241 "columns": [ 242 column[2] 243 for column in conn.execute(f"PRAGMA index_info({index_name})") 244 ], 245 } 246 ) 247 indexes.sort(key=lambda index: index["name"]) 248 return {"columns": columns, "indexes": indexes} 249 250 try: 251 _ensure_edges_schema(conn) 252 row = conn.execute("SELECT path, mtime FROM edge_files").fetchone() 253 if row is None: 254 raise RuntimeError("edge schema sentinel is missing") 255 sentinel = {"path": row[0], "mtime": row[1]} 256 return { 257 "fixture": "solstone-edge-schema", 258 "fixture_version": 1, 259 "generated_by": "make core-fixtures", 260 "schema_version": EDGES_SCHEMA_VERSION, 261 "sentinel": sentinel, 262 "tables": { 263 "edge_files": table_schema("edge_files"), 264 "edges": table_schema("edges"), 265 }, 266 } 267 finally: 268 conn.close() 269 270 271def render_peer_json(peer: dict[str, Any]) -> str: 272 # Mirrors join_cli.py:233 and :496 exactly; keep this oracle in sync because 273 # join_cli has no extracted peer.json serializer to import here. 274 return json.dumps(peer, indent=2) + "\n" 275 276 277def build_link_join_observer_ascii_peer_json() -> str: 278 return render_peer_json( 279 { 280 "label": "laptop", 281 "paired_at": "1970-01-01T00:00:00Z", 282 "instance_id": "receiver-instance", 283 "home_label": "Home", 284 "fingerprint": "sha256:abc", 285 "local_endpoints": [], 286 "role": "", 287 } 288 ) 289 290 291def build_link_join_peer_non_ascii_peer_json() -> str: 292 return render_peer_json( 293 { 294 "label": "café", 295 "paired_at": "1970-01-01T00:00:00Z", 296 "instance_id": "receiver-instance", 297 "home_label": "Hôme", 298 "fingerprint": "sha256:abc", 299 "local_endpoints": [ 300 {"endpoint": "réseau-local", "port": 7657, "scope": "lan"}, 301 ], 302 "role": "peer", 303 } 304 ) 305 306 307def build_link_join_nested_endpoints_peer_json() -> str: 308 return render_peer_json( 309 { 310 "label": "laptop", 311 "paired_at": "1970-01-01T00:00:00Z", 312 "instance_id": "receiver-instance", 313 "home_label": "Home", 314 "fingerprint": "sha256:abc", 315 "local_endpoints": [ 316 { 317 "ip": "10.0.0.2", 318 "port": 7657, 319 "scope": "lan", 320 "meta": { 321 "first": "one", 322 "second": ["two", {"third": "three"}], 323 }, 324 }, 325 ], 326 "role": "", 327 } 328 ) 329 330 331def link_join_fixture_dir() -> Path: 332 return FIXTURE_DIR / "native-sol" / "link-join" 333 334 335def _markdown_fixture_cases() -> list[dict[str, Any]]: 336 long_line = "z" * (markdown_formatter._MAX_LINE_CHARS + 1) 337 non_ascii_under_line_bound = "é" * (markdown_formatter._MAX_LINE_CHARS - 1) 338 non_ascii_over_line_bound = "é" * (markdown_formatter._MAX_LINE_CHARS + 1) 339 non_ascii_chunk_body = "\n".join(["é" * 1300] * 3) 340 oversized_line = "alpha " * 300 341 oversized_body = "\n".join([oversized_line] * 3) 342 return [ 343 {"id": "empty", "input": ""}, 344 {"id": "whitespace_only", "input": " \n\t\n"}, 345 {"id": "heading_only", "input": "# Heading\n"}, 346 {"id": "thematic_break_only", "input": "---\n"}, 347 { 348 "id": "header_only_table", 349 "input": "| Name | Value |\n| --- | --- |\n", 350 }, 351 { 352 "id": "nested_heading_context", 353 "input": "# Root\n\n## Child\n\nalpha paragraph\n\n### Leaf\n\nbeta paragraph\n", 354 }, 355 { 356 "id": "ordinary_paragraphs", 357 "input": "# Notes\n\nalpha paragraph\n\nbeta paragraph\n", 358 }, 359 { 360 "id": "ordinary_list", 361 "input": "# Tasks\n\n- alpha item\n- beta item\n", 362 }, 363 { 364 "id": "intro_list", 365 "input": "# Tasks\n\nintro alpha\n\n- alpha item\n- beta item\n", 366 }, 367 { 368 "id": "intro_table", 369 "input": ( 370 "# Metrics\n\nintro alpha\n\n" 371 "| Name | Value |\n| --- | --- |\n| alpha | one |\n| beta | two |\n" 372 ), 373 }, 374 { 375 "id": "definition_2_of_4", 376 "input": ( 377 "# Definitions\n\n" 378 "- **alpha:** value one\n" 379 "- ordinary note.\n" 380 "- **beta:** value two\n" 381 "- ordinary other.\n" 382 ), 383 }, 384 { 385 "id": "definition_2_of_5", 386 "input": ( 387 "# Boundary\n\n" 388 "- **alpha:** value one\n" 389 "- ordinary note.\n" 390 "- **beta:** value two\n" 391 "- ordinary other.\n" 392 "- ordinary final.\n" 393 ), 394 }, 395 { 396 "id": "definition_1_of_2", 397 "input": "# Boundary\n\n- **alpha:** value one\n- ordinary note.\n", 398 }, 399 { 400 "id": "multi_row_table", 401 "input": ( 402 "# Matrix\n\n" 403 "| Name | Value |\n| --- | --- |\n" 404 "| alpha | one |\n| beta | two |\n| gamma | three |\n" 405 ), 406 }, 407 { 408 "id": "fenced_code_info", 409 "input": "# Code\n\n```python\nprint('alpha')\n```\n", 410 }, 411 { 412 "id": "blockquote_multi_paragraph", 413 "input": "# Quote\n\n> alpha quote\n>\n> beta quote\n", 414 }, 415 { 416 "id": "overlong_line", 417 "input": f"# Long\n\n{long_line}\n\nkept alpha\n", 418 }, 419 { 420 "id": "oversized_chunk", 421 "input": f"# Big\n\n{oversized_body}\n", 422 }, 423 { 424 "id": "loose_nested_list", 425 "input": "# Nested\n\n- parent alpha\n\n - child beta\n", 426 }, 427 { 428 "id": "two_paragraph_list_item", 429 "input": "# Loose\n\n- first alpha\n\n second beta\n", 430 }, 431 { 432 "id": "list_item_fenced_code", 433 "input": "# Item Code\n\n- alpha before\n\n ```python\n print('beta')\n ```\n", 434 }, 435 { 436 "id": "inline_link", 437 "input": "# Link\n\n[alpha](https://example.com/path/to-beta?q=gamma)\n", 438 }, 439 { 440 "id": "inline_image", 441 "input": '# Image\n\n![alt text](images/pic-alpha.png "title beta")\n', 442 }, 443 { 444 "id": "autolink", 445 "input": "# Auto\n\n<https://example.com/path?q=gamma>\n", 446 }, 447 { 448 "id": "reference_link", 449 "input": '# Reference\n\n[alpha][ref]\n\n[ref]: https://example.com/path "title beta"\n', 450 }, 451 { 452 "id": "inline_html", 453 "input": "# Html\n\nalpha <span>beta</span> gamma\n", 454 }, 455 { 456 "id": "non_ascii_line_under_char_bound_over_byte_bound", 457 "input": f"# Accent\n\n{non_ascii_under_line_bound}\n", 458 "token_comparison": False, 459 "token_comparison_reason": ( 460 "non-ASCII is outside the ASCII tokenizer-equivalence guarantee; " 461 "compare only chunk count and warnings" 462 ), 463 }, 464 { 465 "id": "non_ascii_line_over_char_bound", 466 "input": f"# Accent\n\n{non_ascii_over_line_bound}\n\nkept ascii\n", 467 "token_comparison": False, 468 "token_comparison_reason": ( 469 "non-ASCII is outside the ASCII tokenizer-equivalence guarantee; " 470 "compare only chunk count and warnings" 471 ), 472 }, 473 { 474 "id": "non_ascii_chunk_under_char_bound_over_byte_bound", 475 "input": f"# Accent\n\n{non_ascii_chunk_body}\n", 476 "token_comparison": False, 477 "token_comparison_reason": ( 478 "non-ASCII is outside the ASCII tokenizer-equivalence guarantee; " 479 "compare only chunk count and warnings" 480 ), 481 }, 482 ] 483 484 485class _WarningCapture(logging.Handler): 486 def __init__(self) -> None: 487 super().__init__(level=logging.WARNING) 488 self.messages: list[str] = [] 489 490 def emit(self, record: logging.LogRecord) -> None: 491 self.messages.append(record.getMessage()) 492 493 494def _format_markdown_with_warnings(text: str) -> tuple[list[dict[str, Any]], list[str]]: 495 logger = logging.getLogger(markdown_formatter.__name__) 496 handler = _WarningCapture() 497 logger.addHandler(handler) 498 try: 499 chunks, _meta = markdown_formatter.format_markdown(text) 500 finally: 501 logger.removeHandler(handler) 502 return chunks, handler.messages 503 504 505def _fts5_tokens(chunks: list[str]) -> list[list[str]]: 506 tokens: list[list[str]] = [[] for _chunk in chunks] 507 conn = sqlite3.connect(":memory:") 508 try: 509 conn.execute("CREATE VIRTUAL TABLE chunks USING fts5(content)") 510 conn.executemany( 511 "INSERT INTO chunks(content) VALUES (?)", 512 [(chunk,) for chunk in chunks], 513 ) 514 conn.execute("CREATE VIRTUAL TABLE vocab USING fts5vocab(chunks, 'instance')") 515 rows = conn.execute( 516 "SELECT doc, offset, term FROM vocab ORDER BY doc, offset" 517 ).fetchall() 518 finally: 519 conn.close() 520 521 for doc, _offset, term in rows: 522 tokens[int(doc) - 1].append(str(term)) 523 return tokens 524 525 526def _normalize_oversized_size_tokens(tokens: list[str]) -> list[str]: 527 normalized: list[str] = [] 528 i = 0 529 while i < len(tokens): 530 if i + 5 < len(tokens) and tokens[i : i + 5] == [ 531 "content", 532 "too", 533 "large", 534 "to", 535 "index", 536 ]: 537 normalized.extend(tokens[i : i + 5]) 538 j = i + 5 539 while j < len(tokens) and tokens[j] != "chars": 540 j += 1 541 if j < len(tokens): 542 normalized.append(OVERSIZED_SIZE_TOKEN) 543 normalized.append("chars") 544 i = j + 1 545 continue 546 normalized.append(tokens[i]) 547 i += 1 548 return normalized 549 550 551def _normalize_tokens(tokens: list[str], normalizations: list[str]) -> list[str]: 552 if OVERSIZED_SIZE_NORMALIZATION in normalizations: 553 tokens = _normalize_oversized_size_tokens(tokens) 554 return tokens 555 556 557def build_markdown_chunks_fixture() -> dict[str, Any]: 558 cases = [] 559 for case in _markdown_fixture_cases(): 560 token_comparison = case.get("token_comparison", True) 561 if token_comparison and not case["input"].isascii(): 562 raise RuntimeError(f"markdown fixture case is not ASCII-only: {case['id']}") 563 chunks, warnings = _format_markdown_with_warnings(case["input"]) 564 entry = { 565 "id": case["id"], 566 "input": case["input"], 567 "chunk_count": len(chunks), 568 "warnings": warnings, 569 } 570 if token_comparison: 571 rendered = [chunk["markdown"] for chunk in chunks] 572 tokens_by_chunk = _fts5_tokens(rendered) 573 chunk_entries = [] 574 for markdown, tokens in zip(rendered, tokens_by_chunk, strict=True): 575 normalizations = ( 576 [OVERSIZED_SIZE_NORMALIZATION] 577 if "[Content too large to index:" in markdown 578 else [] 579 ) 580 chunk_entry: dict[str, Any] = { 581 "markdown": markdown, 582 "tokens": _normalize_tokens(tokens, normalizations), 583 } 584 if normalizations: 585 chunk_entry["normalizations"] = normalizations 586 chunk_entries.append(chunk_entry) 587 entry["chunks"] = chunk_entries 588 else: 589 entry["token_comparison"] = False 590 entry["token_comparison_reason"] = case["token_comparison_reason"] 591 cases.append(entry) 592 593 return { 594 "fixture": "solstone-markdown-chunks", 595 "fixture_version": 1, 596 "generated_by": "make core-fixtures", 597 "constraints": { 598 "token_comparison_cases_ascii_only": True, 599 "token_comparison_false_behavior": ( 600 "non-ASCII cases record only chunk_count and warnings because " 601 "they are outside the ASCII tokenizer-equivalence guarantee" 602 ), 603 "max_line_chars": markdown_formatter._MAX_LINE_CHARS, 604 "max_chunk_chars": markdown_formatter._MAX_CHUNK_CHARS, 605 "normalizations": { 606 OVERSIZED_SIZE_NORMALIZATION: ( 607 "replace content-too-large size number tokens with " 608 f"{OVERSIZED_SIZE_TOKEN}" 609 ) 610 }, 611 "tokenizer": ( 612 "sqlite fts5(content) with fts5vocab(chunks, 'instance') " 613 "ordered by doc, offset" 614 ), 615 }, 616 "cases": cases, 617 } 618 619 620def _diarize_constants() -> dict[str, Any]: 621 return { 622 "SAMPLE_RATE": DIARIZE_SAMPLE_RATE, 623 "WINDOW_S": DIARIZE_WINDOW_S, 624 "STRIDE_S": DIARIZE_STRIDE_S, 625 "FRAMES_PER_WINDOW": FRAMES_PER_WINDOW, 626 "SINGLE_SPEAKER_CLASSES": sorted(SINGLE_SPEAKER_CLASSES), 627 "MIN_INTERVAL_S": MIN_INTERVAL_S, 628 "MIN_FRAME_CONFIDENCE": MIN_FRAME_CONFIDENCE, 629 "AHC_LINKAGE": AHC_LINKAGE, 630 "AHC_METRIC": AHC_METRIC, 631 "MAX_K": MAX_K, 632 "SILHOUETTE_IMPROVEMENT": SILHOUETTE_IMPROVEMENT, 633 } 634 635 636def _encoder_config_constants() -> dict[str, Any]: 637 return { 638 "ENCODER_ID": ENCODER_ID, 639 "OWNER_THRESHOLD": OWNER_THRESHOLD, 640 "OWNER_MARGIN_MIN": OWNER_MARGIN_MIN, 641 "ACOUSTIC_HIGH": ACOUSTIC_HIGH, 642 "ACOUSTIC_MEDIUM": ACOUSTIC_MEDIUM, 643 "ACOUSTIC_MARGIN_MIN": ACOUSTIC_MARGIN_MIN, 644 "SOLO_CLUSTER_MIN_COSINE": SOLO_CLUSTER_MIN_COSINE, 645 "VP_DECAY_LAMBDA": VP_DECAY_LAMBDA, 646 "VP_OUTLIER_MIN_SIMILARITY": VP_OUTLIER_MIN_SIMILARITY, 647 "VP_OUTLIER_MIN_SAMPLES": VP_OUTLIER_MIN_SAMPLES, 648 "CC_COVERAGE_GATE": CC_COVERAGE_GATE, 649 "CC_CONFIDENCE_GATE": CC_CONFIDENCE_GATE, 650 "OWNER_BOOTSTRAP_MIN_STMTS": OWNER_BOOTSTRAP_MIN_STMTS, 651 "OWNER_BOOTSTRAP_MIN_MEDIAN_DURATION_S": OWNER_BOOTSTRAP_MIN_MEDIAN_DURATION_S, 652 "OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25": OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25, 653 "OWNER_BOOTSTRAP_STRONG_EVIDENCE_MIN_STMTS": OWNER_BOOTSTRAP_STRONG_EVIDENCE_MIN_STMTS, 654 "OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25_STRONG": OWNER_BOOTSTRAP_MIN_INTRA_COSINE_P25_STRONG, 655 "OWNER_BOOTSTRAP_EVIDENCE_TIER_STANDARD": OWNER_BOOTSTRAP_EVIDENCE_TIER_STANDARD, 656 "OWNER_BOOTSTRAP_EVIDENCE_TIER_STRONG": OWNER_BOOTSTRAP_EVIDENCE_TIER_STRONG, 657 "OWNER_BOOTSTRAP_PROVISIONAL_GUARD_MIN_TAGS": OWNER_BOOTSTRAP_PROVISIONAL_GUARD_MIN_TAGS, 658 "OWNER_REBUILD_MIN_CENTROID_AGREEMENT": OWNER_REBUILD_MIN_CENTROID_AGREEMENT, 659 "OWNER_REBUILD_MIN_CLUSTER_SIZE_RATIO": OWNER_REBUILD_MIN_CLUSTER_SIZE_RATIO, 660 "OWNER_REBUILD_MAX_COHESION_DROP": OWNER_REBUILD_MAX_COHESION_DROP, 661 "OWNER_REBUILD_SUPERSEDED_SCAN_DAYS": OWNER_REBUILD_SUPERSEDED_SCAN_DAYS, 662 "NOISY_FLYWHEEL_OVERLAP_MAX": NOISY_FLYWHEEL_OVERLAP_MAX, 663 "SLOT_ACTIVE_MIN_SHARE": SLOT_ACTIVE_MIN_SHARE, 664 "SPEAKER_EVIDENCE_MULTI_MIN": SPEAKER_EVIDENCE_MULTI_MIN, 665 "SPEAKER_EVIDENCE_SINGLE_MAX": SPEAKER_EVIDENCE_SINGLE_MAX, 666 "DIARIZE_MIN_OVERLAP": DIARIZE_MIN_OVERLAP, 667 "SPEAKER_EVIDENCE_VERSION": SPEAKER_EVIDENCE_VERSION, 668 "OVERLAP_DETECTOR_ID": OVERLAP_DETECTOR_ID, 669 "OVERLAP_DETECTOR_SHA256": OVERLAP_DETECTOR_SHA256, 670 "MERGE_THRESHOLD": MERGE_THRESHOLD, 671 "SPLIT_THRESHOLD": SPLIT_THRESHOLD, 672 "STABILITY_THRESHOLD": STABILITY_THRESHOLD, 673 "CONSOLIDATE_MIN_INTERVALS": CONSOLIDATE_MIN_INTERVALS, 674 "CONSOLIDATE_MERGE_THRESHOLD": CONSOLIDATE_MERGE_THRESHOLD, 675 "CONSOLIDATE_SUGGEST_MIN": CONSOLIDATE_SUGGEST_MIN, 676 "CONFIRM_MIN_SEGMENTS": CONFIRM_MIN_SEGMENTS, 677 "CONFIRM_MIN_INTERVALS": CONFIRM_MIN_INTERVALS, 678 "CONFIRM_MIN_DURATION_S": CONFIRM_MIN_DURATION_S, 679 } 680 681 682def _speaker_fixture_identity(fixture: str) -> dict[str, Any]: 683 fixture_version = 2 if fixture == "solstone-speaker-filterbank" else 1 684 return { 685 "fixture": fixture, 686 "fixture_version": fixture_version, 687 "generated_by": "make core-fixtures", 688 "kaldi_native_fbank_version": _package_version("kaldi-native-fbank"), 689 "source_constants": { 690 "diarize": _diarize_constants(), 691 "encoder_config": _encoder_config_constants(), 692 "overlap": { 693 "WINDOW_S": OVERLAP_WINDOW_S, 694 "STRIDE_S": OVERLAP_STRIDE_S, 695 "_DIARIZE_STRIDE_S": OVERLAP_DIARIZE_STRIDE_S, 696 "FRAMES_PER_WINDOW": OVERLAP_FRAMES_PER_WINDOW, 697 "OVERLAP_CLASSES": list(OVERLAP_CLASSES), 698 }, 699 }, 700 "generation_platform": { 701 "system": platform.system(), 702 "machine": platform.machine(), 703 }, 704 } 705 706 707def _speaker_filterbank_waveform() -> np.ndarray: 708 total_samples = int(DIARIZE_SAMPLE_RATE * SPEAKER_FILTERBANK_DURATION_S) 709 near_samples = int(DIARIZE_SAMPLE_RATE * SPEAKER_FILTERBANK_NEAR_S) 710 fade_samples = int(DIARIZE_SAMPLE_RATE * SPEAKER_FILTERBANK_FADE_S) 711 rng = np.random.default_rng(SPEAKER_FILTERBANK_WAVEFORM_SEED) 712 near = ( 713 rng.standard_normal(total_samples).astype(np.float32) 714 * SPEAKER_FILTERBANK_NEAR_AMPLITUDE 715 ) 716 broadband = ( 717 rng.standard_normal(total_samples).astype(np.float32) 718 * SPEAKER_FILTERBANK_BROADBAND_AMPLITUDE 719 ) 720 envelope = np.zeros(total_samples, dtype=np.float32) 721 fade_start = near_samples 722 fade_end = near_samples + fade_samples 723 fade = 0.5 - 0.5 * np.cos(np.linspace(0.0, math.pi, fade_samples, dtype=np.float32)) 724 envelope[fade_start:fade_end] = fade 725 envelope[fade_end:] = 1.0 726 return (near + broadband * envelope).astype(np.float32) 727 728 729def _decimal_rows(matrix: np.ndarray) -> list[str]: 730 return [ 731 " ".join(f"{float(value):.{FLOAT_ROW_DECIMALS}f}" for value in row) 732 for row in matrix 733 ] 734 735 736def build_speaker_filterbank_fixture() -> dict[str, Any]: 737 audio = _speaker_filterbank_waveform() 738 diarize_features = _wespeaker_features(audio) 739 main_features = _compute_wespeaker_features(audio, DIARIZE_SAMPLE_RATE) 740 if not np.array_equal(diarize_features, main_features): 741 raise RuntimeError("WeSpeaker filterbank call sites diverged") 742 normalized = _normalize_rows(diarize_features) 743 return { 744 "identity": _speaker_fixture_identity("solstone-speaker-filterbank"), 745 "comparison": { 746 "mode": "float_rows", 747 "filterbank_abs_tolerance": FILTERBANK_VALUE_ABS_TOLERANCE, 748 }, 749 "waveform": { 750 "seed": SPEAKER_FILTERBANK_WAVEFORM_SEED, 751 "sample_rate": DIARIZE_SAMPLE_RATE, 752 "duration_s": SPEAKER_FILTERBANK_DURATION_S, 753 "near_silent_s": SPEAKER_FILTERBANK_NEAR_S, 754 "fade_s": SPEAKER_FILTERBANK_FADE_S, 755 "near_silent_amplitude": SPEAKER_FILTERBANK_NEAR_AMPLITUDE, 756 "broadband_amplitude": SPEAKER_FILTERBANK_BROADBAND_AMPLITUDE, 757 "near_silent_rows": list(SPEAKER_FILTERBANK_NEAR_ROWS), 758 "broadband_rows": list(SPEAKER_FILTERBANK_BROADBAND_ROWS), 759 "samples_f32_le_base64": base64.b64encode( 760 audio.astype("<f4", copy=False).tobytes() 761 ).decode("ascii"), 762 }, 763 "call_site_agreement": { 764 "array_equal": True, 765 "shape": list(diarize_features.shape), 766 }, 767 "matrices": { 768 "filterbank_cmn": { 769 "shape": list(diarize_features.shape), 770 "encoding": f"space-separated fixed decimal rows, {FLOAT_ROW_DECIMALS} places", 771 "rows": _decimal_rows(diarize_features), 772 }, 773 "row_l2_normalized": { 774 "shape": list(normalized.shape), 775 "encoding": f"space-separated fixed decimal rows, {FLOAT_ROW_DECIMALS} places", 776 "rows": _decimal_rows(normalized), 777 }, 778 }, 779 } 780 781 782def _pyannote_class_count() -> int: 783 return max(max(SINGLE_SPEAKER_CLASSES), max(OVERLAP_CLASSES)) + 1 784 785 786def _dominant_log_probs(classes: np.ndarray, *, seed: int) -> np.ndarray: 787 rng = np.random.default_rng(seed) 788 log_probs = rng.normal( 789 loc=-3.0, scale=0.05, size=(len(classes), _pyannote_class_count()) 790 ).astype(np.float32) 791 log_probs[np.arange(len(classes)), classes] = rng.normal( 792 loc=2.0, scale=0.03, size=len(classes) 793 ).astype(np.float32) 794 return log_probs 795 796 797def _interval_boundary_case(run_frames: int, *, seed: int) -> dict[str, Any]: 798 speech_class = min(SINGLE_SPEAKER_CLASSES) 799 classes = np.zeros(FRAMES_PER_WINDOW, dtype=np.int64) 800 classes[:run_frames] = speech_class 801 avg_log_probs = _dominant_log_probs(classes, seed=seed) 802 intervals = _find_intervals( 803 avg_log_probs, 804 audio_len_samples=DIARIZE_WINDOW_S * DIARIZE_SAMPLE_RATE, 805 ) 806 return { 807 "run_frames": run_frames, 808 "run_duration_s": run_frames * DIARIZE_WINDOW_S / FRAMES_PER_WINDOW, 809 "intervals": [ 810 {"start_s": start, "end_s": end, "local_class": local_class} 811 for start, end, local_class in intervals 812 ], 813 } 814 815 816def _speaker_evidence_else_branch_band( 817 overlap_fraction: float, decision: Any 818) -> dict[str, Any]: 819 preconditions = { 820 "multi_window_fraction_below_multi_min": ( 821 decision.multi_window_fraction < SPEAKER_EVIDENCE_MULTI_MIN 822 ), 823 "multi_window_fraction_below_single_max": ( 824 decision.multi_window_fraction < SPEAKER_EVIDENCE_SINGLE_MAX 825 ), 826 "overlap_fraction_below_diarize_min_overlap": ( 827 overlap_fraction < DIARIZE_MIN_OVERLAP 828 ), 829 "mean_window_overlap_share_at_or_above_diarize_min_overlap": ( 830 decision.mean_window_overlap_share >= DIARIZE_MIN_OVERLAP 831 ), 832 } 833 return { 834 "multi_window_fraction": decision.multi_window_fraction, 835 "speaker_evidence_multi_min": SPEAKER_EVIDENCE_MULTI_MIN, 836 "speaker_evidence_single_max": SPEAKER_EVIDENCE_SINGLE_MAX, 837 "overlap_fraction": overlap_fraction, 838 "diarize_min_overlap": DIARIZE_MIN_OVERLAP, 839 "mean_window_overlap_share": decision.mean_window_overlap_share, 840 "preconditions": preconditions, 841 } 842 843 844def _speaker_evidence_cases() -> dict[str, Any]: 845 single_class = min(SINGLE_SPEAKER_CLASSES) 846 second_class = sorted(SINGLE_SPEAKER_CLASSES)[1] 847 overlap_class = min(OVERLAP_CLASSES) 848 speech_frames = OVERLAP_FRAMES_PER_WINDOW 849 else_overlap_frames = math.ceil(DIARIZE_MIN_OVERLAP * speech_frames) 850 cases = { 851 "none": { 852 "overlap_fraction": 0.0, 853 "class_windows": [np.zeros(OVERLAP_FRAMES_PER_WINDOW, dtype=np.int64)], 854 }, 855 "single": { 856 "overlap_fraction": 0.0, 857 "class_windows": [ 858 np.full(OVERLAP_FRAMES_PER_WINDOW, single_class, dtype=np.int64) 859 ], 860 }, 861 "multi_by_active_slots": { 862 "overlap_fraction": 0.0, 863 "class_windows": [ 864 np.concatenate( 865 [ 866 np.full( 867 OVERLAP_FRAMES_PER_WINDOW // 2, 868 single_class, 869 dtype=np.int64, 870 ), 871 np.full( 872 OVERLAP_FRAMES_PER_WINDOW - OVERLAP_FRAMES_PER_WINDOW // 2, 873 second_class, 874 dtype=np.int64, 875 ), 876 ] 877 ) 878 ], 879 }, 880 "else_branch_overlap_ambiguity": { 881 "overlap_fraction": max( 882 0.0, DIARIZE_MIN_OVERLAP - min(DIARIZE_MIN_OVERLAP / 2, 0.001) 883 ), 884 "class_windows": [ 885 np.concatenate( 886 [ 887 np.full( 888 speech_frames - else_overlap_frames, 889 single_class, 890 dtype=np.int64, 891 ), 892 np.full( 893 else_overlap_frames, 894 overlap_class, 895 dtype=np.int64, 896 ), 897 ] 898 ) 899 ], 900 }, 901 } 902 results = {} 903 for name, case in cases.items(): 904 window_stats = [] 905 payload_windows = [] 906 for idx, classes in enumerate(case["class_windows"]): 907 stats = _speaker_window_stats( 908 _dominant_log_probs(classes, seed=300 + idx + len(results) * 10) 909 ) 910 window_stats.append(stats) 911 payload_windows.append( 912 { 913 "speech_frames": stats.speech_frames, 914 "active_slot_count": stats.active_slot_count, 915 "overlap_frames": stats.overlap_frames, 916 } 917 ) 918 decision = decide_speaker_evidence(case["overlap_fraction"], window_stats) 919 result = { 920 "overlap_fraction": case["overlap_fraction"], 921 "windows": payload_windows, 922 "decision": { 923 "speaker_evidence": decision.speaker_evidence, 924 "multi_window_fraction": decision.multi_window_fraction, 925 "mean_window_overlap_share": decision.mean_window_overlap_share, 926 }, 927 } 928 if name == "else_branch_overlap_ambiguity": 929 branch_band = _speaker_evidence_else_branch_band( 930 case["overlap_fraction"], decision 931 ) 932 if not all(branch_band["preconditions"].values()): 933 raise RuntimeError( 934 "speaker evidence ambiguity case no longer lands in the else branch" 935 ) 936 result["else_branch_band"] = branch_band 937 results[name] = result 938 return results 939 940 941def _cluster_case(rows: np.ndarray) -> dict[str, Any]: 942 normalized = _normalize_rows(rows.astype(np.float32)) 943 curve = [] 944 for k in range(2, min(MAX_K, len(rows) - 1) + 1): 945 labels = _ahc(normalized, k).astype(np.int32) 946 curve.append( 947 { 948 "k": k, 949 "silhouette": _silhouette(normalized, labels), 950 "labels": labels.tolist(), 951 } 952 ) 953 selected_k = _pick_k_silhouette(normalized, MAX_K) 954 plain_argmax_k = max(curve, key=lambda row: row["silhouette"])["k"] 955 return { 956 "selected_k": selected_k, 957 "plain_argmax_k": plain_argmax_k, 958 "curve": curve, 959 } 960 961 962def _seeded_rows(seed: int, n: int) -> np.ndarray: 963 return np.random.default_rng(seed).standard_normal((n, 64)).astype(np.float32) 964 965 966def build_speaker_stage_boundaries_fixture() -> dict[str, Any]: 967 kept = _interval_boundary_case(30, seed=201) 968 dropped = _interval_boundary_case(29, seed=202) 969 assigned = _assign_sentences( 970 [ 971 {"start": 0.1, "end": 0.3, "text": "inside"}, 972 {"start": 0.7, "end": 0.8, "text": "outside"}, 973 ], 974 [ 975 ( 976 kept["intervals"][0]["start_s"], 977 kept["intervals"][0]["end_s"], 978 kept["intervals"][0]["local_class"], 979 ) 980 ], 981 np.array([0], dtype=np.int32), 982 ) 983 984 divergence_rows = _seeded_rows(K_DIVERGENCE_SEED, K_DIVERGENCE_N) 985 divergence = _cluster_case(divergence_rows) 986 perturb_base_rows = _seeded_rows(CLUSTER_PERTURB_SEED, CLUSTER_PERTURB_N) 987 perturb_changed_rows = perturb_base_rows.copy() 988 perturb_changed_rows[CLUSTER_PERTURB_ROW, CLUSTER_PERTURB_COL] += ( 989 CLUSTER_PERTURB_EPSILON 990 ) 991 perturb_base = _cluster_case(perturb_base_rows) 992 perturb_changed = _cluster_case(perturb_changed_rows) 993 if perturb_base["selected_k"] == perturb_changed["selected_k"]: 994 raise RuntimeError("pinned clustering perturbation no longer flips selected k") 995 996 return { 997 "identity": _speaker_fixture_identity("solstone-speaker-stage-boundaries"), 998 "comparison": { 999 "mode": "mixed_exact_and_float", 1000 "cluster_score_abs_tolerance": CLUSTER_SCORE_ABS_TOLERANCE, 1001 }, 1002 "interval_boundary": { 1003 "kept_at_30_frames": kept, 1004 "dropped_at_29_frames": dropped, 1005 "min_interval_s": MIN_INTERVAL_S, 1006 "assigned_sentences": assigned, 1007 }, 1008 "speaker_evidence": _speaker_evidence_cases(), 1009 "k_selection_divergence": { 1010 "seed": K_DIVERGENCE_SEED, 1011 "n": K_DIVERGENCE_N, 1012 "case": divergence, 1013 }, 1014 "clustering_input_perturbation": { 1015 "seed": CLUSTER_PERTURB_SEED, 1016 "n": CLUSTER_PERTURB_N, 1017 "row": CLUSTER_PERTURB_ROW, 1018 "col": CLUSTER_PERTURB_COL, 1019 "epsilon": CLUSTER_PERTURB_EPSILON, 1020 "base": perturb_base, 1021 "perturbed": perturb_changed, 1022 }, 1023 } 1024 1025 1026def render_json(payload: dict[str, Any]) -> str: 1027 return json.dumps(payload, indent=2, sort_keys=True) + "\n" 1028 1029 1030def _relative_artifact_path(path: Path) -> str: 1031 return str(path.relative_to(ROOT)) 1032 1033 1034def _json_pointer(parts: tuple[str, ...]) -> str: 1035 return "/" + "/".join(parts) 1036 1037 1038def _get_json_path(payload: Any, pointer: str) -> Any: 1039 current = payload 1040 for part in pointer.strip("/").split("/"): 1041 if isinstance(current, list): 1042 current = current[int(part)] 1043 else: 1044 current = current[part] 1045 return current 1046 1047 1048def _remove_json_path(payload: Any, pointer: str) -> None: 1049 parts = pointer.strip("/").split("/") 1050 current = payload 1051 for part in parts[:-1]: 1052 if isinstance(current, list): 1053 current = current[int(part)] 1054 else: 1055 current = current[part] 1056 leaf = parts[-1] 1057 if isinstance(current, list): 1058 del current[int(leaf)] 1059 else: 1060 current.pop(leaf, None) 1061 1062 1063def _compare_exact_artifact(path: Path, current: str, expected: str) -> list[str]: 1064 if current == expected: 1065 return [] 1066 return [_relative_artifact_path(path)] 1067 1068 1069def _json_load_for_compare( 1070 path: Path, label: str, content: str 1071) -> tuple[Any, list[str]]: 1072 try: 1073 return json.loads(content), [] 1074 except json.JSONDecodeError as exc: 1075 return None, [ 1076 f"{_relative_artifact_path(path)}: {label} JSON parse failed: {exc}" 1077 ] 1078 1079 1080def _format_float_drift( 1081 path: Path, 1082 location: str, 1083 expected: float, 1084 actual: float, 1085 tolerance: float, 1086) -> str: 1087 diff = abs(actual - expected) 1088 return ( 1089 f"{_relative_artifact_path(path)}: {location} drifted; " 1090 f"expected={expected:.9g} actual={actual:.9g} " 1091 f"abs_diff={diff:.9g} tolerance={tolerance:.9g}" 1092 ) 1093 1094 1095def _format_float_value_failure( 1096 path: Path, 1097 location: str, 1098 expected: Any, 1099 actual: Any, 1100 tolerance: float, 1101 reason: str, 1102) -> str: 1103 return ( 1104 f"{_relative_artifact_path(path)}: {location} drifted; " 1105 f"expected={expected!r} actual={actual!r} " 1106 f"abs_diff={reason} tolerance={tolerance:.9g}" 1107 ) 1108 1109 1110def _compare_float_value( 1111 path: Path, 1112 location: str, 1113 current: Any, 1114 expected: Any, 1115 tolerance: float, 1116) -> list[str]: 1117 try: 1118 expected_value = float(expected) 1119 actual = float(current) 1120 except (TypeError, ValueError): 1121 return [ 1122 _format_float_value_failure( 1123 path, location, expected, current, tolerance, "unparseable" 1124 ) 1125 ] 1126 if not math.isfinite(expected_value) or not math.isfinite(actual): 1127 return [ 1128 _format_float_value_failure( 1129 path, location, expected, current, tolerance, "non-finite" 1130 ) 1131 ] 1132 if abs(actual - expected_value) > tolerance: 1133 return [_format_float_drift(path, location, expected_value, actual, tolerance)] 1134 return [] 1135 1136 1137def _compare_decimal_row_matrix( 1138 path: Path, 1139 current: Any, 1140 expected: Any, 1141 pointer: str, 1142 tolerance: float, 1143) -> list[str]: 1144 current_rows = _get_json_path(current, pointer) 1145 expected_rows = _get_json_path(expected, pointer) 1146 if not isinstance(current_rows, list) or not isinstance(expected_rows, list): 1147 return [f"{_relative_artifact_path(path)}: {pointer} is not a row list"] 1148 if len(current_rows) != len(expected_rows): 1149 return [ 1150 f"{_relative_artifact_path(path)}: {pointer} row count drifted; " 1151 f"expected={len(expected_rows)} actual={len(current_rows)}" 1152 ] 1153 failures: list[str] = [] 1154 for row_idx, (current_row, expected_row) in enumerate( 1155 zip(current_rows, expected_rows, strict=True) 1156 ): 1157 current_values = str(current_row).split() 1158 expected_values = str(expected_row).split() 1159 if len(current_values) != len(expected_values): 1160 failures.append( 1161 f"{_relative_artifact_path(path)}: {pointer}[{row_idx}] column count drifted; " 1162 f"expected={len(expected_values)} actual={len(current_values)}" 1163 ) 1164 continue 1165 for col_idx, (current_text, expected_text) in enumerate( 1166 zip(current_values, expected_values, strict=True) 1167 ): 1168 failures.extend( 1169 _compare_float_value( 1170 path, 1171 f"{pointer}[{row_idx}][{col_idx}]", 1172 current_text, 1173 expected_text, 1174 tolerance, 1175 ) 1176 ) 1177 return failures 1178 1179 1180def _compare_speaker_filterbank(path: Path, current: str, expected: str) -> list[str]: 1181 current_obj, errors = _json_load_for_compare(path, "current", current) 1182 expected_obj, expected_errors = _json_load_for_compare(path, "expected", expected) 1183 if errors or expected_errors: 1184 return errors + expected_errors 1185 1186 matrix_pointers = ( 1187 "/matrices/filterbank_cmn/rows", 1188 "/matrices/row_l2_normalized/rows", 1189 ) 1190 current_exact = copy.deepcopy(current_obj) 1191 expected_exact = copy.deepcopy(expected_obj) 1192 for pointer in SPEAKER_FIXTURE_IGNORED_JSON_POINTERS + matrix_pointers: 1193 _remove_json_path(current_exact, pointer) 1194 _remove_json_path(expected_exact, pointer) 1195 1196 failures: list[str] = [] 1197 if current_exact != expected_exact: 1198 failures.append( 1199 f"{_relative_artifact_path(path)}: non-float content drifted outside tolerance-managed paths" 1200 ) 1201 for pointer in matrix_pointers: 1202 failures.extend( 1203 _compare_decimal_row_matrix( 1204 path, 1205 current_obj, 1206 expected_obj, 1207 pointer, 1208 FILTERBANK_VALUE_ABS_TOLERANCE, 1209 ) 1210 ) 1211 return failures 1212 1213 1214def _compare_stage_value( 1215 path: Path, 1216 current: Any, 1217 expected: Any, 1218 ignored_json_pointers: frozenset[str], 1219 parts: tuple[str, ...] = (), 1220) -> list[str]: 1221 pointer = _json_pointer(parts) if parts else "/" 1222 if pointer in ignored_json_pointers: 1223 return [] 1224 if parts and parts[-1] == "silhouette": 1225 return _compare_float_value( 1226 path, 1227 pointer, 1228 current, 1229 expected, 1230 CLUSTER_SCORE_ABS_TOLERANCE, 1231 ) 1232 if isinstance(expected, dict): 1233 if not isinstance(current, dict): 1234 return [ 1235 f"{_relative_artifact_path(path)}: {pointer} type drifted; expected=dict actual={type(current).__name__}" 1236 ] 1237 if set(current) != set(expected): 1238 return [ 1239 f"{_relative_artifact_path(path)}: {pointer} keys drifted; " 1240 f"expected={sorted(expected)} actual={sorted(current)}" 1241 ] 1242 failures: list[str] = [] 1243 for key in sorted(expected): 1244 failures.extend( 1245 _compare_stage_value( 1246 path, 1247 current[key], 1248 expected[key], 1249 ignored_json_pointers, 1250 parts + (key,), 1251 ) 1252 ) 1253 return failures 1254 if isinstance(expected, list): 1255 if not isinstance(current, list): 1256 return [ 1257 f"{_relative_artifact_path(path)}: {pointer} type drifted; expected=list actual={type(current).__name__}" 1258 ] 1259 if len(current) != len(expected): 1260 return [ 1261 f"{_relative_artifact_path(path)}: {pointer} length drifted; " 1262 f"expected={len(expected)} actual={len(current)}" 1263 ] 1264 failures = [] 1265 for idx, (current_item, expected_item) in enumerate( 1266 zip(current, expected, strict=True) 1267 ): 1268 failures.extend( 1269 _compare_stage_value( 1270 path, 1271 current_item, 1272 expected_item, 1273 ignored_json_pointers, 1274 parts + (str(idx),), 1275 ) 1276 ) 1277 return failures 1278 if current != expected: 1279 return [ 1280 f"{_relative_artifact_path(path)}: {pointer} drifted; " 1281 f"expected={expected!r} actual={current!r}" 1282 ] 1283 return [] 1284 1285 1286def _compare_speaker_stage_boundaries( 1287 path: Path, current: str, expected: str 1288) -> list[str]: 1289 current_obj, errors = _json_load_for_compare(path, "current", current) 1290 expected_obj, expected_errors = _json_load_for_compare(path, "expected", expected) 1291 if errors or expected_errors: 1292 return errors + expected_errors 1293 return _compare_stage_value( 1294 path, 1295 current_obj, 1296 expected_obj, 1297 frozenset(SPEAKER_FIXTURE_IGNORED_JSON_POINTERS), 1298 ) 1299 1300 1301def compare_artifact( 1302 path: Path, descriptor: ArtifactDescriptor, current: str, expected: str 1303) -> list[str]: 1304 if descriptor.comparison == "exact": 1305 return _compare_exact_artifact(path, current, expected) 1306 if descriptor.comparison == "speaker_filterbank": 1307 return _compare_speaker_filterbank(path, current, expected) 1308 if descriptor.comparison == "speaker_stage_boundaries": 1309 return _compare_speaker_stage_boundaries(path, current, expected) 1310 raise ValueError(f"unknown comparison mode: {descriptor.comparison}") 1311 1312 1313def expected_outputs() -> dict[Path, ArtifactDescriptor]: 1314 return { 1315 CALLOSUM_ARTIFACT_PATH: ArtifactDescriptor( 1316 build_callosum_registry_fixture, 1317 ), 1318 COGITATE_ARTIFACT_PATH: ArtifactDescriptor( 1319 build_cogitate_contract_fixture, 1320 ), 1321 EDGE_SCHEMA_ARTIFACT_PATH: ArtifactDescriptor( 1322 build_edge_schema_fixture, 1323 ), 1324 MARKDOWN_CHUNKS_ARTIFACT_PATH: ArtifactDescriptor( 1325 build_markdown_chunks_fixture, 1326 ), 1327 SPEAKER_FILTERBANK_ARTIFACT_PATH: ArtifactDescriptor( 1328 build_speaker_filterbank_fixture, 1329 comparison="speaker_filterbank", 1330 ), 1331 SPEAKER_STAGE_BOUNDARIES_ARTIFACT_PATH: ArtifactDescriptor( 1332 build_speaker_stage_boundaries_fixture, 1333 comparison="speaker_stage_boundaries", 1334 ), 1335 link_join_fixture_dir() / "observer_ascii_peer.json": ArtifactDescriptor( 1336 build_link_join_observer_ascii_peer_json, 1337 ), 1338 link_join_fixture_dir() / "peer_non_ascii_peer.json": ArtifactDescriptor( 1339 build_link_join_peer_non_ascii_peer_json, 1340 ), 1341 link_join_fixture_dir() / "nested_endpoints_peer.json": ArtifactDescriptor( 1342 build_link_join_nested_endpoints_peer_json, 1343 ), 1344 } 1345 1346 1347def write_outputs() -> None: 1348 FIXTURE_DIR.mkdir(parents=True, exist_ok=True) 1349 for path, descriptor in expected_outputs().items(): 1350 content = descriptor.render() 1351 path.parent.mkdir(parents=True, exist_ok=True) 1352 path.write_text(content, encoding="utf-8") 1353 print(f"wrote {path.relative_to(ROOT)}") 1354 1355 1356def check_outputs() -> int: 1357 stale: list[str] = [] 1358 drift: list[str] = [] 1359 for path, descriptor in expected_outputs().items(): 1360 expected = descriptor.render() 1361 try: 1362 current = path.read_text(encoding="utf-8") 1363 except FileNotFoundError: 1364 current = "" 1365 errors = compare_artifact(path, descriptor, current, expected) 1366 if not errors: 1367 continue 1368 if descriptor.comparison == "exact": 1369 stale.extend(errors) 1370 else: 1371 drift.extend(errors) 1372 1373 if stale: 1374 paths = ", ".join(stale) 1375 print( 1376 f"Core generated fixtures are stale: {paths}. Run: make core-fixtures", 1377 file=sys.stderr, 1378 ) 1379 if drift: 1380 print("Core generated float fixtures drifted:", file=sys.stderr) 1381 for error in drift: 1382 print(f" {error}", file=sys.stderr) 1383 if stale or drift: 1384 return 1 1385 return 0 1386 1387 1388def main(argv: list[str] | None = None) -> int: 1389 parser = argparse.ArgumentParser(description=__doc__) 1390 parser.add_argument( 1391 "--check", 1392 action="store_true", 1393 help="Check generated fixtures without writing files.", 1394 ) 1395 args = parser.parse_args(argv) 1396 1397 if args.check: 1398 return check_outputs() 1399 write_outputs() 1400 return 0 1401 1402 1403if __name__ == "__main__": 1404 raise SystemExit(main())