personal memory agent
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\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())